1 /*******************************************************************************
\r
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
\r
3 * in Industry THTH ry.
\r
4 * All rights reserved. This program and the accompanying materials
\r
5 * are made available under the terms of the Eclipse Public License v1.0
\r
6 * which accompanies this distribution, and is available at
\r
7 * http://www.eclipse.org/legal/epl-v10.html
\r
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.diagram.elements;
\r
14 import gnu.trove.list.array.TIntArrayList;
\r
16 import java.awt.AlphaComposite;
\r
17 import java.awt.BasicStroke;
\r
18 import java.awt.Color;
\r
19 import java.awt.Composite;
\r
20 import java.awt.Font;
\r
21 import java.awt.FontMetrics;
\r
22 import java.awt.Graphics2D;
\r
23 import java.awt.RenderingHints;
\r
24 import java.awt.Shape;
\r
25 import java.awt.Toolkit;
\r
26 import java.awt.datatransfer.Clipboard;
\r
27 import java.awt.datatransfer.DataFlavor;
\r
28 import java.awt.datatransfer.StringSelection;
\r
29 import java.awt.datatransfer.Transferable;
\r
30 import java.awt.event.KeyEvent;
\r
31 import java.awt.font.FontRenderContext;
\r
32 import java.awt.font.LineBreakMeasurer;
\r
33 import java.awt.font.TextAttribute;
\r
34 import java.awt.font.TextHitInfo;
\r
35 import java.awt.font.TextLayout;
\r
36 import java.awt.geom.AffineTransform;
\r
37 import java.awt.geom.Point2D;
\r
38 import java.awt.geom.Rectangle2D;
\r
39 import java.io.IOException;
\r
40 import java.text.AttributedCharacterIterator;
\r
41 import java.text.AttributedString;
\r
42 import java.util.ArrayList;
\r
43 import java.util.Arrays;
\r
44 import java.util.Hashtable;
\r
46 import org.simantics.datatypes.literal.RGB;
\r
47 import org.simantics.db.layer0.variable.RVI;
\r
48 import org.simantics.diagram.elements.Line.BoundsProcedure;
\r
49 import org.simantics.g2d.canvas.ICanvasContext;
\r
50 import org.simantics.g2d.element.IElement;
\r
51 import org.simantics.scenegraph.IDynamicSelectionPainterNode;
\r
52 import org.simantics.scenegraph.LoaderNode;
\r
53 import org.simantics.scenegraph.ScenegraphUtils;
\r
54 import org.simantics.scenegraph.g2d.G2DNode;
\r
55 import org.simantics.scenegraph.g2d.G2DPDFRenderingHints;
\r
56 import org.simantics.scenegraph.g2d.events.Event;
\r
57 import org.simantics.scenegraph.g2d.events.EventTypes;
\r
58 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
\r
59 import org.simantics.scenegraph.g2d.events.MouseEvent;
\r
60 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
\r
61 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;
\r
62 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent;
\r
63 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;
\r
64 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
\r
65 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
\r
66 import org.simantics.scenegraph.g2d.events.command.Commands;
\r
67 import org.simantics.scenegraph.g2d.events.NodeEventHandler;
\r
68 import org.simantics.scenegraph.utils.GeometryUtils;
\r
69 import org.simantics.scenegraph.utils.NodeUtil;
\r
70 import org.simantics.scl.runtime.function.Function1;
\r
71 import org.simantics.scl.runtime.function.Function2;
\r
72 import org.simantics.ui.colors.Colors;
\r
73 import org.simantics.ui.dnd.LocalObjectTransferable;
\r
74 import org.simantics.ui.fonts.Fonts;
\r
75 import org.simantics.utils.threads.AWTThread;
\r
77 import com.lowagie.text.DocumentException;
\r
78 import com.lowagie.text.Element;
\r
79 import com.lowagie.text.Rectangle;
\r
80 import com.lowagie.text.pdf.FontMapper;
\r
81 import com.lowagie.text.pdf.PdfFormField;
\r
82 import com.lowagie.text.pdf.PdfWriter;
\r
83 import com.lowagie.text.pdf.TextField;
\r
87 * TextNode which supports in-line editing.
\r
89 * By default <code>TextNode</code> is in editable = false state. Use
\r
90 * {@link #setEditable(boolean)} to make it editable.
\r
92 * @author Hannu Niemistö <hannu.niemisto@vtt.fi>
\r
93 * @author Marko Luukkainen <marko.luukkainen@vtt.fi>
\r
94 * @author Tuukka Lehtonen <tuukka.lehtonen@semantum.fi>
\r
97 * o proper support for defining clipping bounds for the text (needed for page templates) (currently through fixedWidth)
\r
98 * o fix editing xOffset to work with fixed width and multi-line text
\r
104 public class TextNode extends G2DNode implements IDynamicSelectionPainterNode, LoaderNode {
\r
106 private static final long serialVersionUID = 654692698101485672L;
\r
109 * TODO: justify existence for this
\r
111 private static final BasicStroke RESET_STROKE = new BasicStroke(1);
\r
114 * Src-over alpha composite instance with 50% opacity.
\r
116 private static final AlphaComposite SrcOver_50 = AlphaComposite.SrcOver.derive(0.5f);
\r
119 * For (inexact) measurement of rendered text bounds.
\r
121 protected static final FontRenderContext FRC = new FontRenderContext(new AffineTransform(), true, true);
\r
123 private static final Font FONT = Font.decode("Arial 6");
\r
124 private static final Color SELECTION_BACKGROUND_COLOR = new Color(0x316ac5);
\r
125 // private static final double MAX_CARET_POSITION = 1.0;
\r
128 * The complete text visualized by this node.
\r
130 protected String text = null;
\r
133 * Tells if this node is still pending for real results or not.
\r
135 protected boolean pending = false;
\r
138 * The font used to render the {@link #text}.
\r
140 protected Font font = FONT;
\r
143 * The color of the rendered text. Default value is {@value Color#black}.
\r
145 protected Color color = Color.BLACK;
\r
148 * The background color used for filling the background of the bounding box
\r
149 * of the rendered text. <code>null</code> means no fill.
\r
150 * Default value is <code>null</code>.
\r
152 protected Color backgroundColor = null;
\r
155 * The color used for drawing the expanded bounding box border for the
\r
156 * rendered text. <code>null</code> means no border is rendered. Default
\r
157 * value is <code>null</code>.
\r
159 protected Color borderColor = null;
\r
161 protected double scale = 1.0;
\r
162 protected transient double scaleRecip = 1.0;
\r
167 protected float borderWidth = 0.f;
\r
169 protected double paddingX = 2.0;
\r
170 protected double paddingY = 2.0;
\r
173 * Horizontal text box alignment with respect to its origin. Default value is
\r
176 protected byte horizontalAlignment = 0;
\r
178 * Vertical text box alignment with respect to its origin. Default value is
\r
181 protected byte verticalAlignment = 3;
\r
183 protected boolean hover = false;
\r
184 boolean editable = false;
\r
185 boolean showSelection = true;
\r
188 boolean wrapText = true;
\r
190 protected RVI dataRVI = null;
\r
193 int selectionTail = 0;
\r
196 float fixedWidth = 0f;
\r
198 private Rectangle2D targetBounds;
\r
200 Function1<String, String> validator;
\r
201 ITextListener textListener;
\r
202 ITextContentFilter editContentFilter;
\r
204 transient boolean editing = false;
\r
205 transient boolean valid = true;
\r
207 private transient boolean xOffsetIsDirty = true;
\r
210 * The renderable line structures parsed from {@link #text} by
\r
211 * {@link #parseLines(String)}, laid out by
\r
212 * {@link #layoutLines(Line[], FontRenderContext)} and aligned by
\r
213 * {@link #alignLines(Line[], Rectangle2D, byte, byte)}
\r
215 protected transient Line[] lines = null;
\r
216 protected transient FontMetrics fontMetrics = null;
\r
219 * Stores the value of {@link #text} before edit mode was last entered. Used
\r
220 * for restoring the original value if editing is cancelled.
\r
222 private transient String textBeforeEdit = null;
\r
223 protected transient TextEditActivation editActivation;
\r
226 * Stores the last scaled bounds.
\r
228 private transient Rectangle2D lastBounds = new Rectangle2D.Double();
\r
231 * This must be nullified if anything that affects the result of
\r
232 * {@link #getTightAlignedBoundsInLocal(Rectangle2D, FontRenderContext)}
\r
233 * changes. It will cause the cached value to be recalculated on the next
\r
236 private transient Rectangle2D tightBoundsCache = null;
\r
239 public void init() {
\r
241 // Mark this node as pending
\r
242 NodeUtil.increasePending(this);
\r
246 public void cleanup() {
\r
251 protected void addListeners() {
\r
252 addEventHandler(this);
\r
255 protected void removeListeners() {
\r
256 removeEventHandler(this);
\r
260 * Enables or disables edit mode. It also sets
\r
261 * the caret at the end of text all selects the
\r
262 * whole text (this is the usual convention when
\r
263 * beginning to edit one line texts).
\r
265 * @return null if no change to edit state was made
\r
267 public Boolean setEditMode(boolean edit) {
\r
268 return setEditMode(edit, true);
\r
272 * Enables or disables edit mode. It also sets
\r
273 * the caret at the end of text all selects the
\r
274 * whole text (this is the usual convention when
\r
275 * beginning to edit one line texts).
\r
277 * @return null if no change to edit state was made
\r
279 protected Boolean setEditMode(boolean edit, boolean notify) {
\r
280 if (edit && !editable)
\r
282 if (editing == edit)
\r
284 this.editing = edit;
\r
286 caret = text != null ? text.length() : 0;
\r
288 textBeforeEdit = text;
\r
290 fireTextEditingStarted();
\r
291 return Boolean.TRUE;
\r
294 fireTextEditingEnded();
\r
295 return Boolean.FALSE;
\r
299 @SyncField({"editable"})
\r
300 public void setEditable(boolean editable) {
\r
301 boolean changed = this.editable != editable;
\r
302 this.editable = editable;
\r
303 if (editing && !editable)
\r
304 setEditMode(false);
\r
313 public boolean isEditable() {
\r
317 public boolean isEditMode() {
\r
321 @SyncField({"wrapText"})
\r
322 public void setWrapText(boolean wrapText) {
\r
323 this.wrapText = wrapText;
\r
327 * @return Does the text box wrap text if
\r
328 * the width of the box is fixed
\r
330 public boolean isWrapText() {
\r
331 return this.wrapText;
\r
334 @SyncField({"showSelection"})
\r
335 public void setShowSelection(boolean showSelection) {
\r
336 this.showSelection = showSelection;
\r
339 public boolean showsSelection() {
\r
340 return showSelection;
\r
347 * @param x not supported anymore, use {@link #setTransform(AffineTransform)} instead
\r
348 * @param y not supported anymore, use {@link #setTransform(AffineTransform)} instead
\r
351 @SyncField({"text", "font", "color", "x", "y", "scale"})
\r
352 public void init(String text, Font font, Color color, double x, double y, double scale) {
\r
353 // no value => value
\r
354 if(this.text == null && text != null) NodeUtil.decreasePending(this);
\r
359 this.text = new String(text != null ? text : "");
\r
361 this.color = color;
\r
362 this.scale = scale;
\r
363 this.scaleRecip = 1.0 / scale;
\r
365 this.selectionTail = 0;
\r
370 @SyncField({"paddingX", "paddingY"})
\r
371 public void setPadding(double x, double y) {
\r
376 @SyncField({"color"})
\r
377 public void setColor(Color color) {
\r
378 this.color = color;
\r
381 @SyncField({"backgroundColor"})
\r
382 public void setBackgroundColor(Color color) {
\r
383 this.backgroundColor = color;
\r
386 @SyncField({"borderColor"})
\r
387 public void setBorderColor(Color color) {
\r
388 this.borderColor = color;
\r
391 public String getText() {
\r
395 public String getTextBeforeEdit() {
\r
396 return textBeforeEdit;
\r
399 @SyncField({"text","caret","selectionTail"})
\r
400 public void setText(String text) {
\r
401 //System.out.println("TextNode.setText('" + text + "', " + editing + ")");
\r
405 // value => no value
\r
406 if(this.text != null && text == null) NodeUtil.increasePending(this);
\r
407 // no value => value
\r
408 if(this.text == null && text != null) NodeUtil.decreasePending(this);
\r
410 this.text = text != null ? text : "";
\r
411 caret = Math.min(caret, this.text.length());
\r
412 selectionTail = caret;
\r
417 @SyncField({"pending"})
\r
418 public void setPending(boolean pending) {
\r
419 if(!this.pending && pending) NodeUtil.increasePending(this);
\r
420 if(this.pending && !pending) NodeUtil.decreasePending(this);
\r
421 this.pending = pending;
\r
424 @SyncField({"fixedWidth"})
\r
425 public void setFixedWidth(float fixedWidth) {
\r
426 if (fixedWidth < 0f)
\r
427 throw new IllegalArgumentException("negative fixed width");
\r
428 this.fixedWidth = fixedWidth;
\r
429 invalidateXOffset();
\r
433 * Bounds where the text box will be drawn
\r
436 public void setTargetBounds(Rectangle2D bounds) {
\r
437 this.targetBounds = bounds;
\r
440 final public void synchronizeWidth(float width) {
\r
442 setFixedWidth(width);
\r
445 final public void synchronizeBorderWidth(float width) {
\r
447 setBorderWidth(width);
\r
450 public final void synchronizeWrapText(boolean wrap) {
\r
454 public boolean isHovering() {
\r
458 @SyncField({"hover"})
\r
459 public void setHover(boolean hover) {
\r
460 this.hover = hover;
\r
464 public Font getFont() {
\r
468 @SyncField({"font"})
\r
469 public void setFont(Font font) {
\r
474 public double getBorderWidth() {
\r
475 return borderWidth;
\r
478 @SyncField({"borderWidth"})
\r
479 public void setBorderWidth(float width) {
\r
480 this.borderWidth = width;
\r
483 public void setBorderWidth(double width) {
\r
484 setBorderWidth((float)width);
\r
487 @SyncField({"horizontalAlignment"})
\r
488 public void setHorizontalAlignment(byte horizontalAlignment) {
\r
489 if (horizontalAlignment < 0 && horizontalAlignment > 2)
\r
490 throw new IllegalArgumentException("Invalid horizontal alignment: " + horizontalAlignment + ", must be between 0 and 2");
\r
491 this.horizontalAlignment = horizontalAlignment;
\r
495 final public void synchronizeHorizontalAlignment(byte horizontalAlignment) {
\r
496 if (horizontalAlignment >= 0 && horizontalAlignment <= 2)
\r
497 setHorizontalAlignment(horizontalAlignment);
\r
500 public byte getHorizontalAlignment() {
\r
501 return horizontalAlignment;
\r
504 @SyncField({"verticalAlignment"})
\r
505 public void setVerticalAlignment(byte verticalAlignment) {
\r
506 if (verticalAlignment < 0 && verticalAlignment > 3)
\r
507 throw new IllegalArgumentException("Invalid vertical alignment: " + verticalAlignment + ", must be between 0 and 3");
\r
508 this.verticalAlignment = verticalAlignment;
\r
512 final public void synchronizeVerticalAlignment(byte verticalAlignment) {
\r
513 if (verticalAlignment >= 0 && verticalAlignment <= 3)
\r
514 setVerticalAlignment(verticalAlignment);
\r
517 public byte getVerticalAlignment() {
\r
518 return verticalAlignment;
\r
522 * Rendering is single-threaded so we can use a static rectangle for
\r
523 * calculating the expanded bounds for the node.
\r
525 private static transient ThreadLocal<Rectangle2D> tempBounds = new ThreadLocal<Rectangle2D>() {
\r
527 protected Rectangle2D initialValue() {
\r
528 return new Rectangle2D.Double();
\r
533 * Rendering is single-threaded so we can use a static AffineTransform to
\r
534 * prevent continuous memory allocation during text rendering.
\r
536 private static transient ThreadLocal<AffineTransform> tempAffineTransform = new ThreadLocal<AffineTransform>() {
\r
538 protected AffineTransform initialValue() {
\r
539 return new AffineTransform();
\r
544 public void render(Graphics2D g) {
\r
545 AffineTransform ot = g.getTransform();
\r
547 g.setTransform(ot);
\r
551 * Note: does not return transformation, stroke, color, etc. to their
\r
555 * @param applyTransform
\r
557 public void render(Graphics2D g, boolean applyTransform) {
\r
558 if (text == null || font == null || color == null)
\r
561 // Cache font metrics if necessary
\r
562 if (fontMetrics == null)
\r
563 fontMetrics = g.getFontMetrics(font);
\r
565 Color color = this.color;
\r
566 boolean isSelected = NodeUtil.isSelected(this, 1);
\r
568 if (!isSelected && hover) {
\r
569 color = add(color, 120, 120, 120);
\r
572 if (applyTransform)
\r
573 g.transform(transform);
\r
574 // Apply separate legacy scale
\r
576 g.scale(scale, scale);
\r
578 // Safety for not rendering when the scale of this text is too small.
\r
579 // When the scale is too small it will cause internal exceptions while
\r
581 double currentScale = GeometryUtils.getScale(g.getTransform());
\r
582 //System.out.println("currentScale: " + currentScale);
\r
583 if (currentScale < 1e-6)
\r
587 //g.translate(x, y);
\r
589 // Calculate text clip rectangle.
\r
590 // This updates textLayout if necessary.
\r
591 Rectangle2D r = getTightAlignedBoundsInLocal(tempBounds.get(), fontMetrics.getFontRenderContext());
\r
593 computeEditingXOffset();
\r
595 if (fixedWidth > 0f)
\r
596 r.setFrame(r.getMinX(), r.getMinY(), fixedWidth, r.getHeight());
\r
597 if(targetBounds != null) {
\r
598 double w = (targetBounds.getWidth() - paddingX * 2) * scaleRecip;
\r
599 double h = (targetBounds.getHeight() - paddingY * 2) * scaleRecip;
\r
600 double x = (targetBounds.getMinX() + paddingX) * scaleRecip;
\r
601 double y = (targetBounds.getMinY() + paddingY) * scaleRecip;
\r
602 r.setRect(x, y, w, h);
\r
605 Rectangle2D textClip = r.getBounds2D();
\r
607 expandBoundsUnscaled(r);
\r
609 // Speed rendering optimization: don't draw text that is too small to
\r
610 // read when not editing
\r
611 boolean renderText = true;
\r
613 Object renderingHint = g.getRenderingHint(RenderingHints.KEY_RENDERING);
\r
614 if (renderingHint != RenderingHints.VALUE_RENDER_QUALITY) {
\r
615 float textSizeMM = (float) currentScale * GeometryUtils.pointToMillimeter(font.getSize2D());
\r
616 if (textSizeMM < 1.5f)
\r
617 renderText = false;
\r
621 Shape clipSave = g.getClip();
\r
622 g.setClip(textClip);
\r
625 PdfWriter writer = (PdfWriter) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_WRITER);
\r
626 boolean isPdfField = false;
\r
627 String fieldName = null;
\r
628 if (writer != null) {
\r
629 // TODO: replace this hack with proper text field name field
\r
630 fieldName = NodeUtil.getNodeName(this);
\r
631 isPdfField = ( fieldName.equals("approved_by") ||
\r
632 fieldName.equals("checked_by") ||
\r
633 fieldName.equals("designer name") ||
\r
634 fieldName.equals("created_by") );
\r
637 Color backgroundColor = valid ? this.backgroundColor : Color.red;
\r
640 if ( !isPdfField ) {
\r
642 // Fill background if necessary
\r
643 if (backgroundColor != null) {
\r
644 g.setColor(backgroundColor);
\r
650 int selectionMin = Math.min(caret, selectionTail);
\r
651 int selectionMax = Math.max(caret, selectionTail);
\r
655 renderText(g, xOffset);
\r
657 Shape clip = g.getClip();
\r
659 // Selection background & text
\r
660 for (Line line : lines) {
\r
661 if (line.intersectsRange(selectionMin, selectionMax)) {
\r
662 Shape selShape = line.getLogicalHighlightShape(selectionMin, selectionMax);
\r
663 line.translate(g, xOffset, 0);
\r
664 g.setClip(selShape);
\r
665 g.setColor(SELECTION_BACKGROUND_COLOR);
\r
667 g.setColor(Color.WHITE);
\r
668 //line.layout.draw(g, 0, 0);
\r
669 g.drawString(line.getText(), 0, 0);
\r
670 line.translateInv(g, xOffset, 0);
\r
691 // TODO: multiline support
\r
693 AffineTransform at = g.getTransform();
\r
694 float height = writer.getPageSize().getHeight();
\r
695 Rectangle2D rr = textClip;
\r
696 // Point2D pt1 = new Point2D.Double(rr.getX(), rr.getY()+rr.getHeight());
\r
697 // Point2D pt2 = new Point2D.Double(rr.getX()+rr.getWidth(), rr.getY());
\r
698 Point2D pt1 = new Point2D.Double(0, 0);
\r
699 Point2D pt2 = new Point2D.Double(47.f/*+rr.getWidth()*/, -rr.getHeight());
\r
700 pt1 = at.transform(pt1, pt1);
\r
701 pt2 = at.transform(pt2, pt2);
\r
702 Rectangle rectangle = new Rectangle(
\r
703 (float) pt1.getX(),
\r
704 height-(float) pt1.getY(),
\r
705 (float) pt2.getX(),
\r
706 height-(float) pt2.getY());
\r
708 FontMapper mapper = (FontMapper) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_FONTMAPPER);
\r
709 // FontMetrics fm = g.getFontMetrics(font);
\r
711 // TODO Oikea leveys
\r
712 // TODO Uniikki nimi
\r
714 PdfFormField field = PdfFormField.createTextField(writer, false, false, 20);
\r
715 field.setFieldName(this.getId().toString());
\r
716 field.setWidget(rectangle, PdfAnnotation.HIGHLIGHT_NONE);
\r
717 field.setQuadding(PdfFormField.Q_RIGHT);
\r
718 field.setFieldFlags(PdfFormField.FF_READ_ONLY);
\r
719 field.setRotate(90);
\r
720 writer.addAnnotation(field);
\r
727 PdfFormField field = PdfFormField.createSignature(writer);
\r
728 field.setWidget(rectangle, PdfAnnotation.HIGHLIGHT_NONE);
\r
729 field.setFieldName(fieldName);
\r
730 field.setQuadding(PdfFormField.Q_LEFT);
\r
731 field.setFlags(PdfAnnotation.FLAGS_PRINT);
\r
732 //field.setFieldFlags(PdfFormField.FF_READ_ONLY)
\r
733 field.setFieldFlags(PdfFormField.FF_EDIT);
\r
735 field.setMKBackgroundColor( backgroundColor!=null?Color.WHITE:backgroundColor );
\r
736 PdfAppearance tp = PdfAppearance.createAppearance(writer, 72, 48);
\r
737 tp.rectangle(rectangle);
\r
739 field.setAppearance(PdfAnnotation.APPEARANCE_NORMAL, tp);
\r
740 writer.addAnnotation(field);
\r
745 TextField textField = new TextField(writer, rectangle, fieldName);
\r
746 textField.setFieldName(fieldName);
\r
747 textField.setFont(mapper.awtToPdf(font));
\r
748 textField.setBorderStyle(0);
\r
749 //textField.setAlignment(Element.ALIGN_LEFT);
\r
750 textField.setAlignment(Element.ALIGN_BOTTOM);
\r
751 textField.setRotation(90);
\r
752 textField.setOptions(TextField.EDIT|TextField.DO_NOT_SPELL_CHECK);
\r
753 if ( text!=null ) {
\r
754 textField.setText(text);
\r
756 if ( color!=null ) {
\r
757 textField.setTextColor(color);
\r
759 textField.setBackgroundColor( backgroundColor!=null?Color.WHITE:backgroundColor );
\r
760 PdfFormField field = textField.getTextField();
\r
761 writer.addAnnotation(field);
\r
762 } catch (IOException e) {
\r
763 e.printStackTrace();
\r
764 } catch (DocumentException e) {
\r
765 e.printStackTrace();
\r
769 // } catch (IOException e) {
\r
770 // // TODO Auto-generated catch block
\r
771 // e.printStackTrace();
\r
772 // } catch (DocumentException e) {
\r
773 // // TODO Auto-generated catch block
\r
774 // e.printStackTrace();
\r
779 g.setClip(clipSave);
\r
781 if (borderWidth > 0f && borderColor != null) {
\r
782 g.setColor(borderColor);
\r
783 g.setStroke(new BasicStroke((float) (scale*borderWidth)));
\r
787 //System.out.println("bw: " + borderWidth);
\r
788 if (isSelected && showsSelection()) {
\r
789 Composite oc = g.getComposite();
\r
790 g.setComposite(SrcOver_50);
\r
791 g.setColor(Color.RED);
\r
792 float bw = borderWidth;
\r
793 double s = currentScale;
\r
795 bw = (float) (1f / s);
\r
799 g.setStroke(new BasicStroke(bw));
\r
801 //g.draw(GeometryUtils.expandRectangle(r, 1.0));
\r
803 g.setComposite(oc);
\r
806 g.scale(scaleRecip, scaleRecip);
\r
807 g.setStroke(RESET_STROKE);
\r
809 lastBounds = getScaledOffsetBounds(r, lastBounds, scale, 0, 0);
\r
810 // g.setColor(Color.MAGENTA); // DEBUG
\r
811 // g.draw(lastBounds); // DEBUG
\r
812 // g.setColor(Color.ORANGE); // DEBUG
\r
813 // g.draw(getBoundsInLocal()); // DEBUG
\r
815 renderSelectedHover(g, isSelected, hover);
\r
818 private void renderCaret(Graphics2D g) {
\r
819 g.setColor(Color.BLACK);
\r
820 for (int i = 0; i < lines.length; i++) {
\r
821 Line line = lines[i];
\r
822 // prevent rendering caret twice on line changes
\r
823 if (line.containsOffset(caret) && // line contains caret
\r
824 (caret != line.endOffset || //caret is not in the end of the line
\r
825 i == lines.length-1 || //caret is end of the last line
\r
826 lines[i+1].startOffset != line.endOffset)) { // beginning of the next line does not start withe the same index as current line
\r
827 Shape[] caretShape = line.getCaretShapes(caret);
\r
828 line.translate(g, xOffset, 0);
\r
829 g.draw(caretShape[0]);
\r
830 if (caretShape[1] != null)
\r
831 g.draw(caretShape[1]);
\r
832 line.translateInv(g, xOffset, 0);
\r
836 private void renderText(Graphics2D g, float xOffset) {
\r
837 //g.draw(tightBoundsCache); // DEBUG
\r
838 for (Line line : lines) {
\r
839 //line.layout.draw(g, line.alignedPosX + xOffset, line.alignedPosY);
\r
840 g.drawString(line.getText(), line.alignedPosX + xOffset, line.alignedPosY);
\r
841 //g.draw(line.abbox); // DEBUG
\r
845 protected Rectangle2D getScaledOffsetBounds(Rectangle2D originalBounds, Rectangle2D dst, double scale, double offsetX, double offsetY) {
\r
846 AffineTransform btr = tempAffineTransform.get();
\r
847 btr.setToTranslation(offsetX*scale, offsetY*scale);
\r
848 btr.scale(scale, scale);
\r
849 if (btr.isIdentity()) {
\r
850 dst.setFrame(originalBounds);
\r
852 dst.setFrame(btr.createTransformedShape(originalBounds).getBounds2D());
\r
858 * Invoked when TextNode is selected and a mouse is hovering on top of it. Can be overridden to add custom rendering.
\r
862 protected void renderSelectedHover(Graphics2D g, boolean isSelected, boolean isHovering) {
\r
866 * Replaces the current selection with the content or inserts
\r
867 * the content at caret. After the insertion the caret
\r
868 * will be at the end of inserted text and selection will
\r
872 @SyncField({"text","caret","selectionTail"})
\r
873 protected void insert(String content) {
\r
874 content = editContentFilter != null ? editContentFilter.filter(this, content) : content;
\r
876 int selectionMin = Math.min(caret, selectionTail);
\r
877 int selectionMax = Math.max(caret, selectionTail);
\r
879 String begin = text.substring(0, selectionMin);
\r
880 String end = text.substring(selectionMax);
\r
881 text = begin + content + end;
\r
882 caret = selectionMin + content.length();
\r
883 selectionTail = caret;
\r
885 assert (caret <= text.length());
\r
886 //System.out.println(text + " " + caret );
\r
888 if(validator != null) {
\r
889 String error = validator.apply(text);
\r
890 valid = (error == null);
\r
897 protected void fireTextChanged() {
\r
898 if(textListener != null)
\r
899 textListener.textChanged();
\r
904 protected void fireTextEditingStarted() {
\r
905 if(textListener != null)
\r
906 textListener.textEditingStarted();
\r
910 protected void fireTextEditingCancelled() {
\r
913 if (deactivateEdit()) {
\r
914 if (textListener != null)
\r
915 textListener.textEditingCancelled();
\r
917 setEditMode(false, false);
\r
919 if (textBeforeEdit != null)
\r
920 setText(textBeforeEdit);
\r
927 public void fireTextEditingEnded() {
\r
929 fireTextEditingCancelled();
\r
934 if (deactivateEdit()) {
\r
935 if (textListener != null)
\r
936 textListener.textEditingEnded();
\r
938 setEditMode(false, false);
\r
943 public void setTextListener(ITextListener listener) {
\r
944 this.textListener = listener;
\r
947 public void setValidator(Function1<String, String> validator) {
\r
948 this.validator = validator;
\r
951 public void setContentFilter(ITextContentFilter filter) {
\r
952 this.editContentFilter = filter;
\r
955 public void setRVI(RVI rvi) {
\r
956 this.dataRVI = rvi;
\r
959 private void invalidateXOffset() {
\r
960 xOffsetIsDirty = true;
\r
963 private void computeEditingXOffset() {
\r
965 if(lines == null) return;
\r
966 if(!xOffsetIsDirty) return;
\r
967 if(fixedWidth > 0f) {
\r
970 // float[] coords = textLayout.getCaretInfo(TextHitInfo.afterOffset(caret));
\r
971 // if(coords != null) {
\r
972 // if(coords[0] > (fixedWidth*MAX_CARET_POSITION)) xOffset = (float)((fixedWidth*MAX_CARET_POSITION)-coords[0]);
\r
973 // else xOffset = 0;
\r
982 xOffsetIsDirty = false;
\r
986 @SyncField({"caret","selectionTail"})
\r
987 protected void moveCaret(int move, boolean select) {
\r
988 // prevent setting caret into line separator.
\r
990 while (text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
\r
992 } else if (move < 0) {
\r
993 while (caret+move >= 0 && text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
\r
999 if (caret > text.length())
\r
1000 caret = text.length();
\r
1002 selectionTail = caret;
\r
1005 private Line findCaretLine() {
\r
1006 // Find the line where caret is. Starting from first line.
\r
1007 for(int i = 0; i < lines.length; i++) {
\r
1008 Line line = lines[i];
\r
1009 if(caret <= line.endOffset) {
\r
1017 * Moves caret to next not letter or digit
\r
1018 * @param shiftDown
\r
1020 private void moveCaretCtrlLeft(boolean shiftDown) {
\r
1021 Line line = findCaretLine();
\r
1022 if(line != null) {
\r
1024 for(i = caret-1; i > line.startOffset; i--) {
\r
1025 char c = line.document.charAt(i);
\r
1026 if(!Character.isLetterOrDigit(c)) {
\r
1030 moveCaret(i - caret, shiftDown);
\r
1035 * Moves caret to previous non letter or digit
\r
1036 * @param shiftDown
\r
1038 private void moveCaretCtrlRight(boolean shiftDown) {
\r
1039 Line line = findCaretLine();
\r
1040 if(line != null) {
\r
1042 for(i = caret + 1; i < line.endOffset; i++) {
\r
1043 char c = line.document.charAt(i);
\r
1044 if(!Character.isLetterOrDigit(c)) {
\r
1048 moveCaret(i - caret, shiftDown);
\r
1053 * Moves caret to line end
\r
1054 * @param shiftDown
\r
1056 private void moveCaretEnd(boolean shiftDown) {
\r
1057 Line line = findCaretLine();
\r
1059 // Move caret to the end of the line
\r
1060 moveCaret(line.endOffset - caret, shiftDown);
\r
1064 * Moves caret to beginning of a line
\r
1065 * @param shiftDown
\r
1067 private void moveCaretHome(boolean shiftDown) {
\r
1068 Line line = findCaretLine();
\r
1070 // Move caret to the beginning of the line
\r
1071 moveCaret(line.startOffset - caret, shiftDown);
\r
1075 * Moves caret one row up and tries to maintain the location
\r
1076 * @param shiftDown
\r
1078 private void moveCaretRowUp(boolean shiftDown) {
\r
1079 // Find the line where caret is. Starting from first line.
\r
1080 for(int i = 0; i < lines.length; i++) {
\r
1081 Line line = lines[i];
\r
1082 if(caret <= line.endOffset) {
\r
1083 // caret is in this line
\r
1085 // Already on top line
\r
1086 // Select the beginning of the line
\r
1087 moveCaret(-caret, shiftDown);
\r
1089 Line prevLine = lines[i-1];
\r
1090 int prevLength = prevLine.endOffset - prevLine.startOffset;
\r
1091 int posInCurRow = caret - line.startOffset;
\r
1092 if(prevLength < posInCurRow)
\r
1093 posInCurRow = prevLength;
\r
1095 int newPos = prevLine.startOffset + posInCurRow;
\r
1096 moveCaret(newPos - caret, shiftDown);
\r
1104 * Moves caret one row down and tries to maintain the location
\r
1105 * @param shiftDown
\r
1107 private void moveCaretRowDown(boolean shiftDown) {
\r
1108 // Find the line where caret is. Starting from last line.
\r
1109 for(int i = lines.length - 1; i >= 0; i--) {
\r
1110 Line line = lines[i];
\r
1111 if(caret >= line.startOffset) {
\r
1112 // caret is in this line
\r
1113 if(i == lines.length - 1) {
\r
1114 // Already on bottom line, cannot go below
\r
1115 // Select to the end of the line
\r
1116 moveCaret(line.endOffset - caret, shiftDown);
\r
1118 Line prevLine = lines[i+1]; // Previous line
\r
1120 // Find new caret position.
\r
1121 // Either it is in the same index as before, or if the row
\r
1122 // is not long enough, select the end of the row.
\r
1123 int prevLength = prevLine.endOffset - prevLine.startOffset;
\r
1124 int posInCurRow = caret - line.startOffset;
\r
1125 if(prevLength < posInCurRow)
\r
1126 posInCurRow = prevLength;
\r
1127 int newPos = prevLine.startOffset + posInCurRow;
\r
1128 moveCaret(newPos - caret, shiftDown);
\r
1135 @SyncField({"caret","selectionTail"})
\r
1136 protected void setCaret(int pos, boolean select) {
\r
1140 if (caret > text.length())
\r
1141 caret = text.length();
\r
1143 selectionTail = caret;
\r
1146 protected void setCaret(Point2D point) {
\r
1147 setCaret(point, false);
\r
1150 @SyncField({"caret","selectionTail"})
\r
1151 protected void setCaret(Point2D point, boolean select) {
\r
1153 for(int i = 0; i < lines.length; i++) {
\r
1154 Line line = lines[i];
\r
1155 Rectangle2D bounds = line.abbox;
\r
1156 // Add heights of bboxes for determining the correct line
\r
1158 lineY = bounds.getY();
\r
1160 lineY += lines[i-1].abbox.getHeight();
\r
1162 double lineHeight = bounds.getHeight();
\r
1163 double hitY = point.getY() / scale;
\r
1164 if(hitY >= lineY && hitY <= lineY + lineHeight) {
\r
1165 // Hit is in this line
\r
1166 float x = (float)(point.getX() / scale) - (float)line.abbox.getX();
\r
1167 float y = (float)(point.getY() / scale - lineHeight * i) ;
\r
1168 TextHitInfo info = line.layout.hitTestChar(x, y);
\r
1169 caret = line.startOffset + info.getInsertionIndex();
\r
1170 if (caret > line.endOffset)
\r
1171 caret = line.endOffset;
\r
1173 selectionTail = caret;
\r
1178 invalidateXOffset();
\r
1179 assert (caret <= text.length());
\r
1183 public Rectangle2D getBoundsInLocal() {
\r
1184 if(targetBounds != null)
\r
1185 return targetBounds;
\r
1187 return expandBounds( getTightAlignedBoundsInLocal(null) );
\r
1190 protected Rectangle2D expandBounds(Rectangle2D r) {
\r
1191 r.setRect(r.getX() * scale - paddingX, r.getY() * scale -paddingY, r.getWidth()*scale + paddingX + paddingX, r.getHeight()*scale + paddingY + paddingY);
\r
1192 //System.out.println(" => " + r);
\r
1196 protected Rectangle2D expandBoundsUnscaled(Rectangle2D r) {
\r
1197 r.setRect(r.getX() - scaleRecip*paddingX, r.getY() -scaleRecip*paddingY, r.getWidth() + scaleRecip*paddingX + scaleRecip*paddingX, r.getHeight() + scaleRecip*paddingY + scaleRecip*paddingY);
\r
1198 //System.out.println(" => " + r);
\r
1202 protected Rectangle2D expandBounds(Rectangle2D r, double amount) {
\r
1203 r.setRect(r.getX() - amount, r.getY() - amount, r.getWidth() + 2*amount, r.getHeight() + 2*amount);
\r
1207 protected Rectangle2D expandBounds(Rectangle2D r, double left, double top, double right, double bottom) {
\r
1208 r.setRect(r.getX() - left, r.getY() - top, r.getWidth() + left + right, r.getHeight() + top + bottom);
\r
1212 private void resetCaches() {
\r
1213 this.tightBoundsCache = null;
\r
1214 this.lines = null;
\r
1215 this.fontMetrics = null;
\r
1219 * Returns the tight bounds around the current text using the current font
\r
1220 * in the specified rectangle. If the specified rectangle is
\r
1221 * <code>null</code> a new Rectangle2D.Double instance will be created.
\r
1226 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r) {
\r
1227 return getTightAlignedBoundsInLocal(r, FRC);
\r
1231 * Returns the tight bounds around the current text using the current font
\r
1232 * in the specified rectangle. If the specified rectangle is
\r
1233 * <code>null</code> a new Rectangle2D.Double instance will be created.
\r
1236 * the rectangle where the result of the method is placed or
\r
1237 * <code>null</code> to allocate new rectangle
\r
1238 * @param frc current font render context
\r
1239 * @return r or new Rectangle2D.Double instance containing the requested
\r
1242 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r, FontRenderContext frc) {
\r
1244 r = new Rectangle2D.Double();
\r
1246 if (tightBoundsCache != null) {
\r
1247 r.setFrame(tightBoundsCache);
\r
1251 String txt = text;
\r
1252 if (font == null || txt == null) {
\r
1253 r.setFrame(0, 0, 2, 1);
\r
1257 //System.out.println("TextNode.getTightAlignedBoundsInLocal('" + txt + "')");
\r
1259 // Parse & layout (unaligned)
\r
1260 Line[] lines = null;
\r
1263 float width = fixedWidth;
\r
1264 if(width <= 0 && targetBounds != null)
\r
1265 width = (float) (((targetBounds.getWidth() - 2*paddingX)) * scaleRecip);
\r
1267 lines = wrapLines(txt, font, width, frc);
\r
1271 lines = parseLines(txt);
\r
1272 this.lines = layoutLines(lines, frc);
\r
1274 // Calculate tight bounds based on unaligned layout
\r
1275 //System.out.println("Unaligned");
\r
1276 tightBoundsCache = calculateBounds(lines, Line.BBOX, null);
\r
1277 //System.out.println(" => " + tightBoundsCache);
\r
1279 this.lines = layoutLinesX(lines, tightBoundsCache);
\r
1280 // Align each line to the calculated tight bounds
\r
1281 this.lines = alignLines(this.lines, tightBoundsCache, horizontalAlignment, verticalAlignment);
\r
1283 // Calculate aligned bounds
\r
1284 //System.out.println("Aligned");
\r
1285 calculateBounds(lines, Line.ABBOX, tightBoundsCache);
\r
1287 r.setFrame(tightBoundsCache);
\r
1288 //System.out.println(" => " + tightBoundsCache);
\r
1296 * the bounding box of all the whole laid out text (only bbox
\r
1300 private Line[] layoutLinesX(Line[] lines, Rectangle2D bbox) {
\r
1301 int lineCount = lines.length;
\r
1302 for (int l = 0; l < lineCount; ++l) {
\r
1303 Line line = lines[l];
\r
1304 // Compute pen x position. If the paragraph is right-to-left we
\r
1305 // will align the TextLayouts to the right edge of the panel.
\r
1306 // Note: drawPosX is always where the LEFT of the text is placed.
\r
1307 // NOTE: This changes based on horizontal alignment
\r
1308 line.drawPosX = (float) (line.layout.isLeftToRight() ? 0f
\r
1309 : tightBoundsCache.getWidth() - line.layout.getAdvance());
\r
1316 * @param boundsProvider
\r
1320 private static Rectangle2D calculateBounds(Line[] lines, BoundsProcedure boundsProvider, Rectangle2D result) {
\r
1321 if (result == null)
\r
1322 result = new Rectangle2D.Double();
\r
1324 result.setFrame(0, 0, 0, 0);
\r
1326 for (Line line : lines) {
\r
1327 //System.out.println("line: " + line);
\r
1328 Rectangle2D bbox = boundsProvider.getBounds(line);
\r
1329 if (result.isEmpty())
\r
1330 result.setFrame(bbox);
\r
1332 Rectangle2D.union(result, bbox, result);
\r
1333 //System.out.println("bounds: " + result);
\r
1335 //System.out.println("final bounds: " + result);
\r
1345 * @return aligned lines
\r
1347 private Line[] alignLines(Line[] lines, Rectangle2D bbox, byte hAlign, byte vAlign) {
\r
1348 // System.out.println("horizontal align: " + Alignment.values()[hAlign]);
\r
1349 // System.out.println("vertical align : " + Alignment.values()[vAlign]);
\r
1350 // System.out.println("bbox: " + bbox);
\r
1352 // double ybase = 0;
\r
1353 if(targetBounds != null) {
\r
1354 /* In normal cases the bounding box moves when
\r
1355 * typing. If target bounds are set, the text
\r
1356 * is fitted into the box.
\r
1359 case 1: // Trailing
\r
1360 xbase = (targetBounds.getMaxX()-paddingX) * scaleRecip;
\r
1363 xbase = targetBounds.getCenterX() * scaleRecip;
\r
1365 default: // Leading / Baseline
\r
1372 for (Line line : lines) {
\r
1373 double xoffset = 0;
\r
1374 double yoffset = 0;
\r
1377 case 1: // Trailing
\r
1378 xoffset = xbase - line.bbox.getWidth();
\r
1381 xoffset = xbase - line.bbox.getWidth() / 2;
\r
1383 default: // Leading / Baseline
\r
1390 yoffset = line.layout.getAscent();
\r
1393 yoffset = -bbox.getHeight() + line.layout.getAscent();
\r
1396 yoffset = -bbox.getHeight() / 2 + line.layout.getAscent();
\r
1400 line.alignOffset(xoffset, yoffset);
\r
1410 private Line[] layoutLines(Line[] lines, FontRenderContext frc) {
\r
1411 TextLayout emptyRowLayout = null;
\r
1412 int lineCount = lines.length;
\r
1414 for (int l = 0; l < lineCount; ++l) {
\r
1415 Line line = lines[l];
\r
1416 String lineText = line.getText();
\r
1417 // " " because TextLayout requires non-empty text and
\r
1418 // We don't want zero size for the text.
\r
1419 if (lineText.isEmpty()) {
\r
1421 if (emptyRowLayout == null)
\r
1422 emptyRowLayout = new TextLayout(lineText, font, frc);
\r
1423 line.layout = emptyRowLayout;
\r
1425 line.layout = new TextLayout(lineText, font, frc);
\r
1428 //y += line.layout.getAscent();
\r
1429 line.drawPosY = y;
\r
1430 y += line.layout.getDescent() + line.layout.getLeading() + line.layout.getAscent();
\r
1432 Rectangle2D bbox = line.layout.getLogicalHighlightShape(0, lineText.length()).getBounds2D();
\r
1433 bbox.setFrame(bbox.getX(), bbox.getY() + line.drawPosY, bbox.getWidth(), bbox.getHeight());
\r
1441 * Splits the specified string into {@link Line} structures, one for each
\r
1442 * line in the input text. The returned lines are only partially defined,
\r
1443 * waiting to be laid out (see
\r
1444 * {@link #layoutLines(Line[], FontRenderContext)})
\r
1448 * @return parsed text lines as {@link Line} structures
\r
1449 * @see #layoutLines(Line[], FontRenderContext)
\r
1451 private static Line[] parseLines(String txt) {
\r
1452 int len = txt.length();
\r
1454 return new Line[] { new Line("", 0, 0) };
\r
1456 TIntArrayList lfpos = new TIntArrayList();
\r
1458 int lineCount = 1;
\r
1459 for (;pos < len; ++lineCount) {
\r
1460 int nextlf = txt.indexOf('\n', pos);
\r
1461 lfpos.add(nextlf != -1 ? nextlf : len);
\r
1466 Line[] lines = new Line[lineCount];
\r
1468 for (int i = 0; i < lineCount-1; ++i) {
\r
1469 int lf = lfpos.getQuick(i);
\r
1470 int cr = (lf > 0) && (txt.charAt(lf - 1) == '\r') ? lf - 1 : lf;
\r
1471 lines[i] = new Line(txt, pos, cr);
\r
1474 lines[lineCount - 1] = new Line(txt, pos, len);
\r
1480 private static Line[] wrapLines(String txt, Font font, float fixedWidth, FontRenderContext frc) {
\r
1481 if(txt == null || txt.isEmpty())
\r
1484 ArrayList<Line> lines =
\r
1485 new ArrayList<Line>();
\r
1487 Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
\r
1488 map.put(TextAttribute.FONT, font);
\r
1489 AttributedString attributedText = new AttributedString(txt.isEmpty() ? "__ERROR__" : txt, map);
\r
1491 AttributedCharacterIterator paragraph = attributedText.getIterator();
\r
1492 int paragraphStart = paragraph.getBeginIndex();
\r
1493 int paragraphEnd = paragraph.getEndIndex();
\r
1494 LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
\r
1496 float breakWidth = fixedWidth;
\r
1498 // Force text to be vertical, by setting break width to 1, if the text area is narrower than "GGGG"
\r
1500 // Set position to the index of the first character in the paragraph.
\r
1501 lineMeasurer.setPosition(paragraphStart);
\r
1503 // Get lines until the entire paragraph has been displayed.
\r
1504 int next, limit, charat, position = 0;
\r
1506 while ((position = lineMeasurer.getPosition()) < paragraphEnd) {
\r
1508 // Find possible line break and set it as a limit to the next layout
\r
1509 next = lineMeasurer.nextOffset(breakWidth);
\r
1511 charat = txt.indexOf(System.getProperty("line.separator"),position+1);
\r
1512 if(charat < next && charat != -1){
\r
1516 lineMeasurer.nextLayout(breakWidth, limit, false);
\r
1518 lines.add(new Line(txt, position, limit));
\r
1521 return lines.toArray(new Line[lines.size()]);
\r
1525 public String getClipboardContent() {
\r
1526 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
\r
1527 Transferable clipData = clipboard.getContents(this);
\r
1529 return (String) (clipData.getTransferData(DataFlavor.stringFlavor));
\r
1530 } catch (Exception ee) {
\r
1535 public void setClipboardContent(String content) {
\r
1536 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
\r
1537 StringSelection data = new StringSelection(content);
\r
1538 clipboard.setContents(data, data);
\r
1542 public String toString() {
\r
1543 return super.toString() + " [text=" + text + ", font=" + font + ", color=" + color + "]";
\r
1547 protected boolean handleCommand(CommandEvent e) {
\r
1551 if (Commands.SELECT_ALL.equals(e.command)) {
\r
1559 protected boolean keyPressed(KeyPressedEvent event) {
\r
1563 char c = event.character;
\r
1564 boolean ctrl = event.isControlDown();
\r
1565 boolean alt = event.isAltDown();
\r
1567 // System.out.println("Key pressed '" + (event.character == 0 ? "\\0" : "" + c) + "' " + event.keyCode + " - " + Integer.toBinaryString(event.stateMask));
\r
1568 // System.out.println("ctrl: " + ctrl);
\r
1569 // System.out.println("alt: " + alt);
\r
1570 if (ctrl && !alt) {
\r
1571 switch (event.keyCode) {
\r
1572 case KeyEvent.VK_C:
\r
1573 if (caret != selectionTail) {
\r
1574 int selectionMin = Math.min(caret, selectionTail);
\r
1575 int selectionMax = Math.max(caret, selectionTail);
\r
1576 setClipboardContent(text.substring(selectionMin, selectionMax));
\r
1580 case KeyEvent.VK_X:
\r
1581 if (caret != selectionTail) {
\r
1582 int selectionMin = Math.min(caret, selectionTail);
\r
1583 int selectionMax = Math.max(caret, selectionTail);
\r
1584 setClipboardContent(text.substring(selectionMin, selectionMax));
\r
1589 case KeyEvent.VK_RIGHT:
\r
1591 // '\'' has the same keycode as VK_RIGHT but when right
\r
1592 // arrow is pressed, event character is \0.
\r
1593 moveCaretCtrlRight(event.isShiftDown());
\r
1597 case KeyEvent.VK_LEFT:
\r
1598 moveCaretCtrlLeft(event.isShiftDown());
\r
1601 case KeyEvent.VK_V:
\r
1603 String content = getClipboardContent();
\r
1604 if(content != null)
\r
1609 // Replaced by #handleCommand
\r
1610 // case KeyEvent.VK_A:
\r
1616 case KeyEvent.VK_ENTER:
\r
1618 insert(getLineSeparator());
\r
1626 } else if (!ctrl && alt) {
\r
1629 switch (event.keyCode) {
\r
1630 case KeyEvent.VK_LEFT:
\r
1631 moveCaret(-1, event.isShiftDown());
\r
1633 case KeyEvent.VK_RIGHT:
\r
1635 // '\'' has the same keycode as VK_RIGHT but when right
\r
1636 // arrow is pressed, event character is \0.
\r
1637 moveCaret(1, event.isShiftDown());
\r
1640 // Intentional fallthrough to default case
\r
1641 case KeyEvent.VK_UP:
\r
1642 moveCaretRowUp(event.isShiftDown());
\r
1644 case KeyEvent.VK_DOWN:
\r
1645 moveCaretRowDown(event.isShiftDown());
\r
1647 case KeyEvent.VK_HOME:
\r
1648 moveCaretHome(event.isShiftDown());
\r
1650 case KeyEvent.VK_END:
\r
1651 moveCaretEnd(event.isShiftDown());
\r
1654 case KeyEvent.VK_ENTER:
\r
1655 fireTextEditingEnded();
\r
1658 case KeyEvent.VK_ESCAPE:
\r
1659 text = textBeforeEdit;
\r
1662 fireTextEditingCancelled();
\r
1665 case KeyEvent.VK_BACK_SPACE:
\r
1666 if(caret == selectionTail && caret > 0) {
\r
1667 // line separator may use multiple characters, we want to remove that with one command
\r
1668 String lineSep = getLineSeparator();
\r
1669 int index = lineSep.indexOf(text.charAt(caret-1));
\r
1673 caret-= (index+1);
\r
1674 selectionTail+= (lineSep.length()-index-1);
\r
1680 case KeyEvent.VK_DELETE:
\r
1681 if(caret == selectionTail && caret < text.length()) {
\r
1682 String lineSep = getLineSeparator();
\r
1683 int index = lineSep.indexOf(text.charAt(caret));
\r
1687 selectionTail-= index;
\r
1688 caret+= (lineSep.length()-index);
\r
1697 if (c == 65535 || Character.getType(c) == Character.CONTROL) {
\r
1700 //System.out.println("Char " + c + " " + Character.getType(c) + " " + text);
\r
1701 insert(new String(new char[] {c}));
\r
1705 // FIXME This is called even if just caret was moved.
\r
1706 // This is currently necessary for repaints.
\r
1707 fireTextChanged();
\r
1708 invalidateXOffset();
\r
1712 protected String getLineSeparator() {
\r
1713 return System.getProperty("line.separator");
\r
1716 protected void selectAll() {
\r
1717 setCaret(0, false);
\r
1718 setCaret(text.length(), true);
\r
1721 protected transient int hoverClick = 0;
\r
1724 protected boolean mouseClicked(MouseClickEvent event) {
\r
1725 if (event.button != MouseClickEvent.LEFT_BUTTON)
\r
1730 if (hoverClick < 2)
\r
1732 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
\r
1733 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
\r
1736 IElement e = DiagramNodeUtil.getElement(ctx, this);
\r
1738 if (Boolean.TRUE.equals(setEditMode(true))) {
\r
1739 editActivation = activateEdit(0, e, ctx);
\r
1746 fireTextEditingEnded();
\r
1752 protected boolean mouseDoubleClicked(MouseDoubleClickedEvent event) {
\r
1753 if (event.button != MouseClickEvent.LEFT_BUTTON)
\r
1756 if (hitTest(event, 0)) {
\r
1757 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
\r
1758 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
\r
1762 if (text != null) {
\r
1763 // Select the whole text.
\r
1764 setCaret(0, false);
\r
1765 setCaret(text.length(), true);
\r
1774 protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {
\r
1778 Point2D local = controlToLocal( event.controlPosition );
\r
1779 // FIXME: once the event coordinate systems are cleared up, remove this workaround
\r
1780 local = parentToLocal(local);
\r
1781 if (hover && this.containsLocal(local)) {
\r
1782 setCaret(local, event.isShiftDown());
\r
1788 protected boolean mouseMoved(MouseMovedEvent event) {
\r
1789 boolean hit = hitTest(event, 3.0);
\r
1790 if (hit != hover) {
\r
1797 private boolean isControlDown(MouseEvent e) {
\r
1798 return e.isControlDown() || lastMouseEvent!=null?lastMouseEvent.isControlDown():false;
\r
1801 protected boolean isShiftDown(MouseEvent e) {
\r
1802 return e.isShiftDown() || lastMouseEvent!=null?lastMouseEvent.isShiftDown():false;
\r
1805 // private boolean isAltDown(MouseEvent e) {
\r
1806 // return e.isAltDown() || lastMouseEvent!=null?lastMouseEvent.isAltDown():false;
\r
1810 protected boolean mouseDragged(MouseDragBegin e) {
\r
1812 && (isControlDown(e) || isShiftDown(e))
\r
1813 && e.context instanceof NodeEventHandler
\r
1814 && dataRVI != null)
\r
1816 e.transferable = new LocalObjectTransferable(dataRVI);
\r
1821 protected boolean hitTest(MouseEvent event, double tolerance) {
\r
1822 Rectangle2D bounds = getBoundsInternal();
\r
1823 if (bounds == null)
\r
1825 Point2D localPos = NodeUtil.worldToLocal(this, event.controlPosition, new Point2D.Double());
\r
1826 double x = localPos.getX();
\r
1827 double y = localPos.getY();
\r
1828 boolean hit = bounds.contains(x, y);
\r
1832 public Rectangle2D getBoundsInternal() {
\r
1833 Rectangle2D local = lastBounds;
\r
1834 if (local == null)
\r
1836 // TODO: potential spot for CPU/memory allocation optimization
\r
1837 // by using more specialized implementations
\r
1838 if (transform.isIdentity())
\r
1840 return transform.createTransformedShape(local).getBounds2D();
\r
1843 protected Color add(Color c, int r, int g, int b) {
\r
1844 int nr = Math.min(255, c.getRed() + r);
\r
1845 int ng = Math.min(255, c.getGreen() + g);
\r
1846 int nb = Math.min(255, c.getBlue() + b);
\r
1847 return new Color(nr, ng, nb);
\r
1850 public TextEditActivation activateEdit(int mouseId, IElement e, ICanvasContext ctx) {
\r
1851 EditDataNode data = EditDataNode.getNode(this);
\r
1852 deactivateEdit(data, null);
\r
1853 TextEditActivation result = new TextEditActivation(mouseId, e, ctx);
\r
1854 data.setTextEditActivation(result);
\r
1859 * @return <code>true</code> if this node is or was previously in editing
\r
1862 protected boolean deactivateEdit() {
\r
1863 boolean result = deactivateEdit( editActivation );
\r
1864 result |= editActivation != null;
\r
1865 editActivation = null;
\r
1869 protected boolean deactivateEdit(TextEditActivation activation) {
\r
1870 return deactivateEdit( EditDataNode.getNode(this), activation );
\r
1873 protected boolean deactivateEdit(EditDataNode data, TextEditActivation activation) {
\r
1874 TextEditActivation previous = data.getTextEditActivation();
\r
1875 if (previous != null && (previous == activation || activation == null)) {
\r
1876 previous.release();
\r
1877 data.setTextEditActivation(null);
\r
1884 public int getEventMask() {
\r
1885 return EventTypes.KeyPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask
\r
1886 | EventTypes.MouseClickMask | EventTypes.CommandMask;
\r
1889 private MouseEvent lastMouseEvent = null;
\r
1892 public boolean handleEvent(Event e) {
\r
1893 if(e instanceof MouseEvent && !(e instanceof MouseDragBegin)) lastMouseEvent = (MouseEvent)e;
\r
1894 return super.handleEvent(e);
\r
1898 public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
\r
1899 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
\r
1903 public <T> T getProperty(String propertyName) {
\r
1908 public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
\r
1911 public void synchronizeText(String text) {
\r
1915 public void synchronizeColor(RGB.Integer color) {
\r
1916 this.color = Colors.awt(color);
\r
1919 public void synchronizeFont(org.simantics.datatypes.literal.Font font) {
\r
1920 setFont(Fonts.awt(font));
\r
1923 public void synchronizeTransform(double[] data) {
\r
1924 this.setTransform(new AffineTransform(data));
\r
1927 public static void main(String[] args) {
\r
1928 Line[] lines = parseLines("\n \n FOO \n\nBAR\n\n\n BAZ\n\n");
\r
1929 System.out.println(Arrays.toString(lines));
\r
1930 System.out.println(GeometryUtils.pointToMillimeter(1));
\r
1931 System.out.println(GeometryUtils.pointToMillimeter(12));
\r
1932 System.out.println(GeometryUtils.pointToMillimeter(72));
\r
1935 ///////////////////////////////////////////////////////////////////////////
\r
1936 // LEGACY CODE NEEDED BY INHERITED CLASSES FOR NOW
\r
1937 ///////////////////////////////////////////////////////////////////////////
\r
1939 protected double getHorizontalAlignOffset(Rectangle2D r) {
\r
1940 switch (horizontalAlignment) {
\r
1941 case 0: return 0; // Leading
\r
1942 case 1: return -r.getWidth(); // Trailing
\r
1943 case 2: return -r.getCenterX(); // Center
\r
1944 default: return 0;
\r
1948 protected double getVerticalAlignOffset() {
\r
1949 FontMetrics fm = fontMetrics;
\r
1952 switch (verticalAlignment) {
\r
1953 case 0: return fm.getMaxAscent(); // Leading=top=maxascent
\r
1954 case 1: return -fm.getMaxDescent(); // Trailing=bottom=maxdescent
\r
1955 case 2: return fm.getMaxAscent() / 2; // Center=maxascent / 2
\r
1957 default: return 0;
\r
1961 ///////////////////////////////////////////////////////////////////////////
\r
1962 // LEGACY CODE ENDS
\r
1963 ///////////////////////////////////////////////////////////////////////////
\r