1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.diagram.elements;
14 import java.awt.AlphaComposite;
15 import java.awt.BasicStroke;
16 import java.awt.Color;
17 import java.awt.Composite;
19 import java.awt.FontMetrics;
20 import java.awt.Graphics2D;
21 import java.awt.RenderingHints;
22 import java.awt.Shape;
23 import java.awt.Toolkit;
24 import java.awt.datatransfer.Clipboard;
25 import java.awt.datatransfer.DataFlavor;
26 import java.awt.datatransfer.StringSelection;
27 import java.awt.datatransfer.Transferable;
28 import java.awt.event.KeyEvent;
29 import java.awt.font.FontRenderContext;
30 import java.awt.font.LineBreakMeasurer;
31 import java.awt.font.TextAttribute;
32 import java.awt.font.TextHitInfo;
33 import java.awt.font.TextLayout;
34 import java.awt.geom.AffineTransform;
35 import java.awt.geom.Point2D;
36 import java.awt.geom.Rectangle2D;
37 import java.text.AttributedCharacterIterator;
38 import java.text.AttributedString;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.Hashtable;
42 import java.util.List;
44 import org.simantics.datatypes.literal.RGB;
45 import org.simantics.db.layer0.variable.RVI;
46 import org.simantics.diagram.elements.Line.BoundsProcedure;
47 import org.simantics.g2d.canvas.ICanvasContext;
48 import org.simantics.g2d.element.IElement;
49 import org.simantics.scenegraph.IDynamicSelectionPainterNode;
50 import org.simantics.scenegraph.LoaderNode;
51 import org.simantics.scenegraph.ScenegraphUtils;
52 import org.simantics.scenegraph.g2d.G2DNode;
53 import org.simantics.scenegraph.g2d.G2DPDFRenderingHints;
54 import org.simantics.scenegraph.g2d.events.Event;
55 import org.simantics.scenegraph.g2d.events.EventTypes;
56 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
57 import org.simantics.scenegraph.g2d.events.MouseEvent;
58 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
59 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;
60 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent;
61 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;
62 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
63 import org.simantics.scenegraph.g2d.events.NodeEventHandler;
64 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
65 import org.simantics.scenegraph.g2d.events.command.Commands;
66 import org.simantics.scenegraph.utils.GeometryUtils;
67 import org.simantics.scenegraph.utils.NodeUtil;
68 import org.simantics.scl.runtime.function.Function1;
69 import org.simantics.scl.runtime.function.Function2;
70 import org.simantics.ui.colors.Colors;
71 import org.simantics.ui.dnd.LocalObjectTransferable;
72 import org.simantics.ui.dnd.MultiTransferable;
73 import org.simantics.ui.dnd.PlaintextTransfer;
74 import org.simantics.ui.fonts.Fonts;
75 import org.simantics.utils.threads.AWTThread;
77 import com.lowagie.text.pdf.PdfWriter;
79 import gnu.trove.list.array.TIntArrayList;
83 * TextNode which supports in-line editing.
85 * By default <code>TextNode</code> is in editable = false state. Use
86 * {@link #setEditable(boolean)} to make it editable.
88 * @author Hannu Niemistö <hannu.niemisto@vtt.fi>
89 * @author Marko Luukkainen <marko.luukkainen@vtt.fi>
90 * @author Tuukka Lehtonen <tuukka.lehtonen@semantum.fi>
93 * o proper support for defining clipping bounds for the text (needed for page templates) (currently through fixedWidth)
94 * o fix editing xOffset to work with fixed width and multi-line text
99 public class TextNode extends G2DNode implements IDynamicSelectionPainterNode, LoaderNode {
101 private static final long serialVersionUID = 654692698101485672L;
103 public static enum TextFlipping {
106 VerticalTextDownwards,
110 * TODO: justify existence for this
112 private static final BasicStroke RESET_STROKE = new BasicStroke(1);
115 * Src-over alpha composite instance with 50% opacity.
117 private static final AlphaComposite SrcOver_50 = AlphaComposite.SrcOver.derive(0.5f);
120 * For (inexact) measurement of rendered text bounds.
122 protected static final FontRenderContext FRC = new FontRenderContext(new AffineTransform(), true, true);
124 private static final Font FONT = Font.decode("Arial 6");
125 private static final Color SELECTION_BACKGROUND_COLOR = new Color(0x316ac5);
126 // private static final double MAX_CARET_POSITION = 1.0;
129 * The complete text visualized by this node.
131 protected String text = null;
134 * The font used to render the {@link #text}.
136 protected Font font = FONT;
139 * The color of the rendered text. Default value is {@value Color#black}.
141 protected Color color = Color.BLACK;
144 * The background color used for filling the background of the bounding box
145 * of the rendered text. <code>null</code> means no fill.
146 * Default value is <code>null</code>.
148 protected Color backgroundColor = null;
151 * The color used for drawing the expanded bounding box border for the
152 * rendered text. <code>null</code> means no border is rendered. Default
153 * value is <code>null</code>.
155 protected Color borderColor = null;
157 protected double scale = 1.0;
158 protected transient double scaleRecip = 1.0;
163 protected float borderWidth = 0.f;
165 protected double paddingX = 2.0;
166 protected double paddingY = 2.0;
169 * Horizontal text box alignment with respect to its origin. Default value is
172 protected byte horizontalAlignment = 0;
174 * Vertical text box alignment with respect to its origin. Default value is
177 protected byte verticalAlignment = 3;
180 * Tells if this node is still pending for real results or not.
182 protected static final int STATE_PENDING = (1 << 0);
183 protected static final int STATE_HOVER = (1 << 1);
184 protected static final int STATE_EDITABLE = (1 << 2);
185 protected static final int STATE_SHOW_SELECTION = (1 << 3);
186 protected static final int STATE_WRAP_TEXT = (1 << 4);
187 protected transient static final int STATE_EDITING = (1 << 5);
188 protected transient static final int STATE_VALID = (1 << 6);
189 protected transient static final int STATE_X_OFFSET_IS_DIRTY = (1 << 7);
190 protected static final int STATE_ALWAYS_ADD_LISTENERS = (1 << 8);
191 protected static final int STATE_LISTENERS_ADDED = (1 << 9);
192 protected static final int STATE_AUTOMATIC_TEXT_FLIP_ENABLED = (1 << 10);
193 protected static final int STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN = (1 << 11);
196 * A combination of all the STATE_ constants defined in this class,
197 * e.g. {@link #STATE_PENDING}.
199 protected int state = STATE_SHOW_SELECTION | STATE_WRAP_TEXT | STATE_VALID | STATE_X_OFFSET_IS_DIRTY;
201 protected RVI dataRVI = null;
204 int selectionTail = 0;
207 float fixedWidth = 0f;
209 private Rectangle2D targetBounds;
211 Function1<String, String> validator;
212 ITextListener textListener;
213 ITextContentFilter editContentFilter;
216 * The renderable line structures parsed from {@link #text} by
217 * {@link #parseLines(String)}, laid out by
218 * {@link #layoutLines(Line[], FontRenderContext)} and aligned by
219 * {@link #alignLines(Line[], Rectangle2D, byte, byte)}
221 protected transient Line[] lines = null;
222 protected transient FontMetrics fontMetrics = null;
225 * Stores the value of {@link #text} before edit mode was last entered. Used
226 * for restoring the original value if editing is cancelled.
228 private transient String textBeforeEdit = null;
229 protected transient TextEditActivation editActivation;
232 * Stores the last scaled bounds.
234 private transient Rectangle2D lastBounds = new Rectangle2D.Double();
237 * This must be nullified if anything that affects the result of
238 * {@link #getTightAlignedBoundsInLocal(Rectangle2D, FontRenderContext)}
239 * changes. It will cause the cached value to be recalculated on the next
242 private transient Rectangle2D tightBoundsCache = null;
247 // Mark this node as pending
248 NodeUtil.increasePending(this);
252 public void cleanup() {
257 protected boolean hasState(int flags) {
258 return (state & flags) == flags;
261 protected void setState(int flags) {
265 protected void setState(int flags, boolean set) {
269 this.state &= ~flags;
272 protected void clearState(int flags) {
273 this.state &= ~flags;
276 protected void setListeners(boolean add) {
283 protected void addListeners() {
284 if (!hasState(STATE_LISTENERS_ADDED)) {
285 addEventHandler(this);
286 setState(STATE_LISTENERS_ADDED);
290 protected void removeListeners() {
291 if (hasState(STATE_LISTENERS_ADDED)) {
292 removeEventHandler(this);
293 clearState(STATE_LISTENERS_ADDED);
298 * Set to true to always enable event listening in this TextNode to allow the text node to keep track of hovering, etc. and to allow DnD even when
301 public void setForceEventListening(boolean force) {
302 setState(STATE_ALWAYS_ADD_LISTENERS, force);
303 if (force && !hasState(STATE_EDITABLE)) {
309 * Enables or disables edit mode. It also sets
310 * the caret at the end of text all selects the
311 * whole text (this is the usual convention when
312 * beginning to edit one line texts).
314 * @return null if no change to edit state was made
316 public Boolean setEditMode(boolean edit) {
317 return setEditMode(edit, true);
321 * Enables or disables edit mode. It also sets
322 * the caret at the end of text all selects the
323 * whole text (this is the usual convention when
324 * beginning to edit one line texts).
326 * @return null if no change to edit state was made
328 protected Boolean setEditMode(boolean edit, boolean notify) {
329 if (edit && !hasState(STATE_EDITABLE))
331 if (hasState(STATE_EDITING) == edit)
333 setState(STATE_EDITING, edit);
335 caret = text != null ? text.length() : 0;
337 textBeforeEdit = text;
339 fireTextEditingStarted();
343 fireTextEditingEnded();
344 return Boolean.FALSE;
348 @SyncField({"editable"})
349 public void setEditable(boolean editable) {
350 boolean changed = hasState(STATE_EDITABLE) != editable;
351 setState(STATE_EDITABLE, editable);
352 if (hasState(STATE_EDITING) && !editable)
354 if (changed && !hasState(STATE_ALWAYS_ADD_LISTENERS)) {
355 setListeners(editable);
359 public boolean isEditable() {
360 return hasState(STATE_EDITABLE);
363 public boolean isEditMode() {
364 return hasState(STATE_EDITING);
367 @SyncField({"wrapText"})
368 public void setWrapText(boolean wrapText) {
369 setState(STATE_WRAP_TEXT, wrapText);
373 * @return Does the text box wrap text if
374 * the width of the box is fixed
376 public boolean isWrapText() {
377 return hasState(STATE_WRAP_TEXT);
380 @SyncField({"showSelection"})
381 public void setShowSelection(boolean showSelection) {
382 setState(STATE_SHOW_SELECTION, showSelection);
385 public boolean showsSelection() {
386 return hasState(STATE_SHOW_SELECTION);
393 * @param x not supported anymore, use {@link #setTransform(AffineTransform)} instead
394 * @param y not supported anymore, use {@link #setTransform(AffineTransform)} instead
397 @SyncField({"text", "font", "color", "x", "y", "scale"})
398 public void init(String text, Font font, Color color, double x, double y, double scale) {
400 if(this.text == null && text != null) NodeUtil.decreasePending(this);
402 if (hasState(STATE_EDITING))
405 this.text = new String(text != null ? text : "");
409 this.scaleRecip = 1.0 / scale;
411 this.selectionTail = 0;
416 public void setAutomaticTextFlipping(TextFlipping type) {
419 clearState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED | STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
421 case VerticalTextDownwards:
422 setState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED | STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
424 case VerticalTextUpwards:
425 setState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED);
426 clearState(STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
431 @SyncField({"paddingX", "paddingY"})
432 public void setPadding(double x, double y) {
437 @SyncField({"color"})
438 public void setColor(Color color) {
442 @SyncField({"backgroundColor"})
443 public void setBackgroundColor(Color color) {
444 this.backgroundColor = color;
447 @SyncField({"borderColor"})
448 public void setBorderColor(Color color) {
449 this.borderColor = color;
452 public String getText() {
456 public String getTextBeforeEdit() {
457 return textBeforeEdit;
460 @SyncField({"text","caret","selectionTail"})
461 public void setText(String text) {
462 //System.out.println("TextNode.setText('" + text + "', " + editing + ")");
463 if (hasState(STATE_EDITING))
467 if(this.text != null && text == null) NodeUtil.increasePending(this);
469 if(this.text == null && text != null) NodeUtil.decreasePending(this);
471 this.text = text != null ? text : "";
472 caret = Math.min(caret, this.text.length());
473 selectionTail = caret;
478 @SyncField({"pending"})
479 public void setPending(boolean pending) {
480 boolean p = hasState(STATE_PENDING);
481 if(!p && pending) NodeUtil.increasePending(this);
482 if(p && !pending) NodeUtil.decreasePending(this);
484 setState(STATE_PENDING, pending);
487 @SyncField({"fixedWidth"})
488 public void setFixedWidth(float fixedWidth) {
490 throw new IllegalArgumentException("negative fixed width");
491 this.fixedWidth = fixedWidth;
496 * Bounds where the text box will be drawn
499 public void setTargetBounds(Rectangle2D bounds) {
500 this.targetBounds = bounds;
503 final public void synchronizeWidth(float width) {
505 setFixedWidth(width);
508 final public void synchronizeBorderWidth(float width) {
510 setBorderWidth(width);
513 public final void synchronizeWrapText(boolean wrap) {
514 setState(STATE_WRAP_TEXT, wrap);
517 public boolean isHovering() {
518 return hasState(STATE_HOVER);
521 @SyncField({"hover"})
522 public void setHover(boolean hover) {
523 setState(STATE_HOVER, hover);
527 public Font getFont() {
532 public void setFont(Font font) {
537 public double getBorderWidth() {
541 @SyncField({"borderWidth"})
542 public void setBorderWidth(float width) {
543 this.borderWidth = width;
546 public void setBorderWidth(double width) {
547 setBorderWidth((float)width);
550 @SyncField({"horizontalAlignment"})
551 public void setHorizontalAlignment(byte horizontalAlignment) {
552 if (horizontalAlignment < 0 && horizontalAlignment > 2)
553 throw new IllegalArgumentException("Invalid horizontal alignment: " + horizontalAlignment + ", must be between 0 and 2");
554 this.horizontalAlignment = horizontalAlignment;
558 final public void synchronizeHorizontalAlignment(byte horizontalAlignment) {
559 if (horizontalAlignment >= 0 && horizontalAlignment <= 2)
560 setHorizontalAlignment(horizontalAlignment);
563 public byte getHorizontalAlignment() {
564 return horizontalAlignment;
567 @SyncField({"verticalAlignment"})
568 public void setVerticalAlignment(byte verticalAlignment) {
569 if (verticalAlignment < 0 && verticalAlignment > 3)
570 throw new IllegalArgumentException("Invalid vertical alignment: " + verticalAlignment + ", must be between 0 and 3");
571 this.verticalAlignment = verticalAlignment;
575 final public void synchronizeVerticalAlignment(byte verticalAlignment) {
576 if (verticalAlignment >= 0 && verticalAlignment <= 3)
577 setVerticalAlignment(verticalAlignment);
580 public byte getVerticalAlignment() {
581 return verticalAlignment;
585 * Rendering is single-threaded so we can use a static rectangle for
586 * calculating the expanded bounds for the node.
588 private static transient ThreadLocal<Rectangle2D> tempBounds = new ThreadLocal<Rectangle2D>() {
590 protected Rectangle2D initialValue() {
591 return new Rectangle2D.Double();
596 * Rendering is single-threaded so we can use a static AffineTransform to
597 * prevent continuous memory allocation during text rendering.
599 private static transient ThreadLocal<AffineTransform> tempAffineTransform = new ThreadLocal<AffineTransform>() {
601 protected AffineTransform initialValue() {
602 return new AffineTransform();
607 public void render(Graphics2D g) {
608 AffineTransform ot = g.getTransform();
614 * Note: does not return transformation, stroke, color, etc. to their
618 * @param applyTransform
620 public void render(Graphics2D g, boolean applyTransform) {
621 if (text == null || font == null || color == null)
624 // Cache font metrics if necessary
625 if (fontMetrics == null)
626 fontMetrics = g.getFontMetrics(font);
628 Color color = this.color;
629 boolean isSelected = NodeUtil.isSelected(this, 1);
630 boolean hover = hasState(STATE_HOVER);
631 boolean editing = hasState(STATE_EDITING);
633 if (!isSelected && hover) {
634 color = add(color, 120, 120, 120);
638 g.transform(transform);
639 // Apply separate legacy scale
641 g.scale(scale, scale);
643 // Safety for not rendering when the scale of this text is too small.
644 // When the scale is too small it will cause internal exceptions while
646 AffineTransform curTr = g.getTransform();
647 double currentScale = GeometryUtils.getScale(curTr);
648 //System.out.println("currentScale: " + currentScale);
649 if (currentScale < 1e-6)
655 // Calculate text clip rectangle.
656 // This updates textLayout if necessary.
657 Rectangle2D r = getTightAlignedBoundsInLocal(tempBounds.get(), fontMetrics.getFontRenderContext());
659 computeEditingXOffset();
662 r.setFrame(r.getMinX(), r.getMinY(), fixedWidth, r.getHeight());
663 if(targetBounds != null) {
664 double w = (targetBounds.getWidth() - paddingX * 2) * scaleRecip;
665 double h = (targetBounds.getHeight() - paddingY * 2) * scaleRecip;
666 double x = (targetBounds.getMinX() + paddingX) * scaleRecip;
667 double y = (targetBounds.getMinY() + paddingY) * scaleRecip;
668 r.setRect(x, y, w, h);
671 if (hasState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED)) {
674 if (curTr.getScaleX() != 0) {
675 needsXFlip = curTr.getScaleX() < 0.0;
676 needsYFlip = curTr.getScaleY() < 0.0;
678 boolean flipAll = !hasState(STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
679 needsXFlip = (curTr.getShearY() < 0.0) ^ flipAll;
680 needsYFlip = (curTr.getShearX() > 0.0) ^ flipAll;
682 if (needsXFlip || needsYFlip) {
683 double centerX = r.getWidth()*0.5 + r.getX();
684 double centerY = r.getHeight()*0.5 + r.getY();
686 g.translate(centerX, centerY);
687 g.scale(needsXFlip ? -1.0 : 1.0, needsYFlip ? -1.0 : 1.0);
688 g.translate(-centerX, -centerY);
692 Rectangle2D textClip = r.getBounds2D();
694 expandBoundsUnscaled(r);
696 // Speed rendering optimization: don't draw text that is too small to
697 // read when not editing
698 boolean renderText = true;
700 Object renderingHint = g.getRenderingHint(RenderingHints.KEY_RENDERING);
701 if (renderingHint != RenderingHints.VALUE_RENDER_QUALITY) {
702 float textSizeMM = (float) currentScale * GeometryUtils.pointToMillimeter(font.getSize2D());
703 if (textSizeMM < 1.5f)
708 Shape clipSave = g.getClip();
712 PdfWriter writer = (PdfWriter) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_WRITER);
713 boolean isRenderingPdf = writer != null;
716 Color backgroundColor = hasState(STATE_VALID) ? this.backgroundColor : Color.red;
720 // Fill background if necessary
721 if (backgroundColor != null) {
722 g.setColor(backgroundColor);
728 int selectionMin = Math.min(caret, selectionTail);
729 int selectionMax = Math.max(caret, selectionTail);
733 renderText(g, xOffset, isRenderingPdf);
735 Shape clip = g.getClip();
737 // Selection background & text
738 for (Line line : lines) {
739 if (line.intersectsRange(selectionMin, selectionMax)) {
740 Shape selShape = line.getLogicalHighlightShape(selectionMin, selectionMax);
741 line.translate(g, xOffset, 0);
743 g.setColor(SELECTION_BACKGROUND_COLOR);
745 g.setColor(Color.WHITE);
746 // #6459: render as text in PDF and paths on screen
748 g.drawString(line.getText(), 0, 0);
750 line.layout.draw(g, 0, 0);
751 line.translateInv(g, xOffset, 0);
763 renderText(g, 0, isRenderingPdf);
771 if (borderWidth > 0f && borderColor != null) {
772 g.setColor(borderColor);
773 g.setStroke(new BasicStroke((float) (scale*borderWidth)));
777 //System.out.println("bw: " + borderWidth);
778 if (isSelected && showsSelection()) {
779 Composite oc = g.getComposite();
780 g.setComposite(SrcOver_50);
781 g.setColor(Color.RED);
782 float bw = borderWidth;
783 double s = currentScale;
785 bw = (float) (1f / s);
789 g.setStroke(new BasicStroke(bw));
791 //g.draw(GeometryUtils.expandRectangle(r, 1.0));
796 g.scale(scaleRecip, scaleRecip);
797 g.setStroke(RESET_STROKE);
799 lastBounds = getScaledOffsetBounds(r, lastBounds, scale, 0, 0);
800 // g.setColor(Color.MAGENTA); // DEBUG
801 // g.draw(lastBounds); // DEBUG
802 // g.setColor(Color.ORANGE); // DEBUG
803 // g.draw(getBoundsInLocal()); // DEBUG
805 renderSelectedHover(g, isSelected, hover);
808 private void renderCaret(Graphics2D g) {
809 g.setColor(Color.BLACK);
810 for (int i = 0; i < lines.length; i++) {
811 Line line = lines[i];
812 // prevent rendering caret twice on line changes
813 if (line.containsOffset(caret) && // line contains caret
814 (caret != line.endOffset || //caret is not in the end of the line
815 i == lines.length-1 || //caret is end of the last line
816 lines[i+1].startOffset != line.endOffset)) { // beginning of the next line does not start withe the same index as current line
817 Shape[] caretShape = line.getCaretShapes(caret);
818 line.translate(g, xOffset, 0);
819 g.draw(caretShape[0]);
820 if (caretShape[1] != null)
821 g.draw(caretShape[1]);
822 line.translateInv(g, xOffset, 0);
826 private void renderText(Graphics2D g, float xOffset, boolean isRenderingPdf) {
827 //g.draw(tightBoundsCache); // DEBUG
828 for (Line line : lines) {
829 // #6459: render as text in PDF and paths on screen
831 g.drawString(line.getText(), line.alignedPosX + xOffset, line.alignedPosY);
833 line.layout.draw(g, line.alignedPosX + xOffset, line.alignedPosY);
834 //g.draw(line.abbox); // DEBUG
838 protected Rectangle2D getScaledOffsetBounds(Rectangle2D originalBounds, Rectangle2D dst, double scale, double offsetX, double offsetY) {
839 AffineTransform btr = tempAffineTransform.get();
840 btr.setToTranslation(offsetX*scale, offsetY*scale);
841 btr.scale(scale, scale);
842 if (btr.isIdentity()) {
843 dst.setFrame(originalBounds);
845 dst.setFrame(btr.createTransformedShape(originalBounds).getBounds2D());
851 * Invoked when TextNode is selected and a mouse is hovering on top of it. Can be overridden to add custom rendering.
855 protected void renderSelectedHover(Graphics2D g, boolean isSelected, boolean isHovering) {
859 * Replaces the current selection with the content or inserts
860 * the content at caret. After the insertion the caret
861 * will be at the end of inserted text and selection will
865 @SyncField({"text","caret","selectionTail"})
866 protected void insert(String content) {
867 content = editContentFilter != null ? editContentFilter.filter(this, content) : content;
869 int selectionMin = Math.min(caret, selectionTail);
870 int selectionMax = Math.max(caret, selectionTail);
872 String begin = text.substring(0, selectionMin);
873 String end = text.substring(selectionMax);
874 text = begin + content + end;
875 caret = selectionMin + content.length();
876 selectionTail = caret;
878 assert (caret <= text.length());
879 //System.out.println(text + " " + caret );
881 if(validator != null) {
882 String error = validator.apply(text);
883 setState(STATE_VALID, (error == null));
890 protected void fireTextChanged() {
891 if(textListener != null)
892 textListener.textChanged();
897 protected void fireTextEditingStarted() {
898 if(textListener != null)
899 textListener.textEditingStarted();
903 protected void fireTextEditingCancelled() {
904 setState(STATE_VALID);
906 if (deactivateEdit()) {
907 if (textListener != null)
908 textListener.textEditingCancelled();
910 setEditMode(false, false);
912 if (textBeforeEdit != null)
913 setText(textBeforeEdit);
920 public void fireTextEditingEnded() {
921 if (!hasState(STATE_VALID)) {
922 fireTextEditingCancelled();
923 setState(STATE_VALID);
927 if (deactivateEdit()) {
928 if (textListener != null)
929 textListener.textEditingEnded();
931 setEditMode(false, false);
936 public void setTextListener(ITextListener listener) {
937 this.textListener = listener;
940 public void setValidator(Function1<String, String> validator) {
941 this.validator = validator;
944 public void setContentFilter(ITextContentFilter filter) {
945 this.editContentFilter = filter;
948 public void setRVI(RVI rvi) {
952 private void invalidateXOffset() {
953 setState(STATE_X_OFFSET_IS_DIRTY);
956 private void computeEditingXOffset() {
958 if(lines == null) return;
959 if(!hasState(STATE_X_OFFSET_IS_DIRTY)) return;
960 if(fixedWidth > 0f) {
963 // float[] coords = textLayout.getCaretInfo(TextHitInfo.afterOffset(caret));
964 // if(coords != null) {
965 // if(coords[0] > (fixedWidth*MAX_CARET_POSITION)) xOffset = (float)((fixedWidth*MAX_CARET_POSITION)-coords[0]);
975 clearState(STATE_X_OFFSET_IS_DIRTY);
979 @SyncField({"caret","selectionTail"})
980 protected void moveCaret(int move, boolean select) {
981 // prevent setting caret into line separator.
983 while (text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
985 } else if (move < 0) {
986 while (caret+move >= 0 && text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
992 if (caret > text.length())
993 caret = text.length();
995 selectionTail = caret;
998 private Line findCaretLine() {
999 // Find the line where caret is. Starting from first line.
1000 for(int i = 0; i < lines.length; i++) {
1001 Line line = lines[i];
1002 if(caret <= line.endOffset) {
1010 * Moves caret to next not letter or digit
1013 private void moveCaretCtrlLeft(boolean shiftDown) {
1014 Line line = findCaretLine();
1017 for(i = caret-1; i > line.startOffset; i--) {
1018 char c = line.document.charAt(i);
1019 if(!Character.isLetterOrDigit(c)) {
1023 moveCaret(i - caret, shiftDown);
1028 * Moves caret to previous non letter or digit
1031 private void moveCaretCtrlRight(boolean shiftDown) {
1032 Line line = findCaretLine();
1035 for(i = caret + 1; i < line.endOffset; i++) {
1036 char c = line.document.charAt(i);
1037 if(!Character.isLetterOrDigit(c)) {
1041 moveCaret(i - caret, shiftDown);
1046 * Moves caret to line end
1049 private void moveCaretEnd(boolean shiftDown) {
1050 Line line = findCaretLine();
1052 // Move caret to the end of the line
1053 moveCaret(line.endOffset - caret, shiftDown);
1057 * Moves caret to beginning of a line
1060 private void moveCaretHome(boolean shiftDown) {
1061 Line line = findCaretLine();
1063 // Move caret to the beginning of the line
1064 moveCaret(line.startOffset - caret, shiftDown);
1068 * Moves caret one row up and tries to maintain the location
1071 private void moveCaretRowUp(boolean shiftDown) {
1072 // Find the line where caret is. Starting from first line.
1073 for(int i = 0; i < lines.length; i++) {
1074 Line line = lines[i];
1075 if(caret <= line.endOffset) {
1076 // caret is in this line
1078 // Already on top line
1079 // Select the beginning of the line
1080 moveCaret(-caret, shiftDown);
1082 Line prevLine = lines[i-1];
1083 int prevLength = prevLine.endOffset - prevLine.startOffset;
1084 int posInCurRow = caret - line.startOffset;
1085 if(prevLength < posInCurRow)
1086 posInCurRow = prevLength;
1088 int newPos = prevLine.startOffset + posInCurRow;
1089 moveCaret(newPos - caret, shiftDown);
1097 * Moves caret one row down and tries to maintain the location
1100 private void moveCaretRowDown(boolean shiftDown) {
1101 // Find the line where caret is. Starting from last line.
1102 for(int i = lines.length - 1; i >= 0; i--) {
1103 Line line = lines[i];
1104 if(caret >= line.startOffset) {
1105 // caret is in this line
1106 if(i == lines.length - 1) {
1107 // Already on bottom line, cannot go below
1108 // Select to the end of the line
1109 moveCaret(line.endOffset - caret, shiftDown);
1111 Line prevLine = lines[i+1]; // Previous line
1113 // Find new caret position.
1114 // Either it is in the same index as before, or if the row
1115 // is not long enough, select the end of the row.
1116 int prevLength = prevLine.endOffset - prevLine.startOffset;
1117 int posInCurRow = caret - line.startOffset;
1118 if(prevLength < posInCurRow)
1119 posInCurRow = prevLength;
1120 int newPos = prevLine.startOffset + posInCurRow;
1121 moveCaret(newPos - caret, shiftDown);
1128 @SyncField({"caret","selectionTail"})
1129 protected void setCaret(int pos, boolean select) {
1133 if (caret > text.length())
1134 caret = text.length();
1136 selectionTail = caret;
1139 protected void setCaret(Point2D point) {
1140 setCaret(point, false);
1143 @SyncField({"caret","selectionTail"})
1144 protected void setCaret(Point2D point, boolean select) {
1146 for(int i = 0; i < lines.length; i++) {
1147 Line line = lines[i];
1148 Rectangle2D bounds = line.abbox;
1149 // Add heights of bboxes for determining the correct line
1151 lineY = bounds.getY();
1153 lineY += lines[i-1].abbox.getHeight();
1155 double lineHeight = bounds.getHeight();
1156 double hitY = point.getY() / scale;
1157 if(hitY >= lineY && hitY <= lineY + lineHeight) {
1158 // Hit is in this line
1159 float x = (float)(point.getX() / scale) - (float)line.abbox.getX();
1160 float y = (float)(point.getY() / scale - lineHeight * i) ;
1161 TextHitInfo info = line.layout.hitTestChar(x, y);
1162 caret = line.startOffset + info.getInsertionIndex();
1163 if (caret > line.endOffset)
1164 caret = line.endOffset;
1166 selectionTail = caret;
1171 invalidateXOffset();
1172 assert (caret <= text.length());
1176 public Rectangle2D getBoundsInLocal() {
1177 if(targetBounds != null)
1178 return targetBounds;
1180 return expandBounds( getTightAlignedBoundsInLocal(null) );
1183 protected Rectangle2D expandBounds(Rectangle2D r) {
1184 r.setRect(r.getX() * scale - paddingX, r.getY() * scale -paddingY, r.getWidth()*scale + paddingX + paddingX, r.getHeight()*scale + paddingY + paddingY);
1185 //System.out.println(" => " + r);
1189 protected Rectangle2D expandBoundsUnscaled(Rectangle2D r) {
1190 r.setRect(r.getX() - scaleRecip*paddingX, r.getY() -scaleRecip*paddingY, r.getWidth() + scaleRecip*paddingX + scaleRecip*paddingX, r.getHeight() + scaleRecip*paddingY + scaleRecip*paddingY);
1191 //System.out.println(" => " + r);
1195 protected Rectangle2D expandBounds(Rectangle2D r, double amount) {
1196 r.setRect(r.getX() - amount, r.getY() - amount, r.getWidth() + 2*amount, r.getHeight() + 2*amount);
1200 protected Rectangle2D expandBounds(Rectangle2D r, double left, double top, double right, double bottom) {
1201 r.setRect(r.getX() - left, r.getY() - top, r.getWidth() + left + right, r.getHeight() + top + bottom);
1205 private void resetCaches() {
1206 this.tightBoundsCache = null;
1208 this.fontMetrics = null;
1212 * Returns the tight bounds around the current text using the current font
1213 * in the specified rectangle. If the specified rectangle is
1214 * <code>null</code> a new Rectangle2D.Double instance will be created.
1219 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r) {
1220 return getTightAlignedBoundsInLocal(r, FRC);
1224 * Returns the tight bounds around the current text using the current font
1225 * in the specified rectangle. If the specified rectangle is
1226 * <code>null</code> a new Rectangle2D.Double instance will be created.
1229 * the rectangle where the result of the method is placed or
1230 * <code>null</code> to allocate new rectangle
1231 * @param frc current font render context
1232 * @return r or new Rectangle2D.Double instance containing the requested
1235 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r, FontRenderContext frc) {
1237 r = new Rectangle2D.Double();
1239 if (tightBoundsCache != null) {
1240 r.setFrame(tightBoundsCache);
1245 if (font == null || txt == null) {
1246 r.setFrame(0, 0, 2, 1);
1250 //System.out.println("TextNode.getTightAlignedBoundsInLocal('" + txt + "')");
1252 // Parse & layout (unaligned)
1253 Line[] lines = null;
1255 if(hasState(STATE_WRAP_TEXT)) {
1256 float width = fixedWidth;
1257 if(width <= 0 && targetBounds != null)
1258 width = (float) (((targetBounds.getWidth() - 2*paddingX)) * scaleRecip);
1260 lines = wrapLines(txt, font, width, frc);
1264 lines = parseLines(txt);
1265 this.lines = layoutLines(lines, frc);
1267 // Calculate tight bounds based on unaligned layout
1268 //System.out.println("Unaligned");
1269 tightBoundsCache = calculateBounds(lines, Line.BBOX, null);
1270 //System.out.println(" => " + tightBoundsCache);
1272 this.lines = layoutLinesX(lines, tightBoundsCache);
1273 // Align each line to the calculated tight bounds
1274 this.lines = alignLines(this.lines, tightBoundsCache, horizontalAlignment, verticalAlignment);
1276 // Calculate aligned bounds
1277 //System.out.println("Aligned");
1278 calculateBounds(lines, Line.ABBOX, tightBoundsCache);
1280 r.setFrame(tightBoundsCache);
1281 //System.out.println(" => " + tightBoundsCache);
1289 * the bounding box of all the whole laid out text (only bbox
1293 private Line[] layoutLinesX(Line[] lines, Rectangle2D bbox) {
1294 int lineCount = lines.length;
1295 for (int l = 0; l < lineCount; ++l) {
1296 Line line = lines[l];
1297 // Compute pen x position. If the paragraph is right-to-left we
1298 // will align the TextLayouts to the right edge of the panel.
1299 // Note: drawPosX is always where the LEFT of the text is placed.
1300 // NOTE: This changes based on horizontal alignment
1301 line.drawPosX = (float) (line.layout.isLeftToRight() ? 0f
1302 : tightBoundsCache.getWidth() - line.layout.getAdvance());
1309 * @param boundsProvider
1313 private static Rectangle2D calculateBounds(Line[] lines, BoundsProcedure boundsProvider, Rectangle2D result) {
1315 result = new Rectangle2D.Double();
1317 result.setFrame(0, 0, 0, 0);
1319 for (Line line : lines) {
1320 //System.out.println("line: " + line);
1321 Rectangle2D bbox = boundsProvider.getBounds(line);
1322 if (result.isEmpty())
1323 result.setFrame(bbox);
1325 Rectangle2D.union(result, bbox, result);
1326 //System.out.println("bounds: " + result);
1328 //System.out.println("final bounds: " + result);
1338 * @return aligned lines
1340 private Line[] alignLines(Line[] lines, Rectangle2D bbox, byte hAlign, byte vAlign) {
1341 // System.out.println("horizontal align: " + Alignment.values()[hAlign]);
1342 // System.out.println("vertical align : " + Alignment.values()[vAlign]);
1343 // System.out.println("bbox: " + bbox);
1345 // double ybase = 0;
1346 if(targetBounds != null) {
1347 /* In normal cases the bounding box moves when
1348 * typing. If target bounds are set, the text
1349 * is fitted into the box.
1353 xbase = (targetBounds.getMaxX()-paddingX) * scaleRecip;
1356 xbase = targetBounds.getCenterX() * scaleRecip;
1358 default: // Leading / Baseline
1365 for (Line line : lines) {
1371 xoffset = xbase - line.bbox.getWidth();
1374 xoffset = xbase - line.bbox.getWidth() / 2;
1376 default: // Leading / Baseline
1383 yoffset = line.layout.getAscent();
1386 yoffset = -bbox.getHeight() + line.layout.getAscent();
1389 yoffset = -bbox.getHeight() / 2 + line.layout.getAscent();
1393 line.alignOffset(xoffset, yoffset);
1403 private Line[] layoutLines(Line[] lines, FontRenderContext frc) {
1404 TextLayout emptyRowLayout = null;
1405 int lineCount = lines.length;
1407 for (int l = 0; l < lineCount; ++l) {
1408 Line line = lines[l];
1409 String lineText = line.getText();
1410 // " " because TextLayout requires non-empty text and
1411 // We don't want zero size for the text.
1412 if (lineText.isEmpty()) {
1414 if (emptyRowLayout == null)
1415 emptyRowLayout = new TextLayout(lineText, font, frc);
1416 line.layout = emptyRowLayout;
1418 line.layout = new TextLayout(lineText, font, frc);
1421 //y += line.layout.getAscent();
1423 y += line.layout.getDescent() + line.layout.getLeading() + line.layout.getAscent();
1425 Rectangle2D bbox = line.layout.getLogicalHighlightShape(0, lineText.length()).getBounds2D();
1426 bbox.setFrame(bbox.getX(), bbox.getY() + line.drawPosY, bbox.getWidth(), bbox.getHeight());
1434 * Splits the specified string into {@link Line} structures, one for each
1435 * line in the input text. The returned lines are only partially defined,
1436 * waiting to be laid out (see
1437 * {@link #layoutLines(Line[], FontRenderContext)})
1441 * @return parsed text lines as {@link Line} structures
1442 * @see #layoutLines(Line[], FontRenderContext)
1444 private static Line[] parseLines(String txt) {
1445 int len = txt.length();
1447 return new Line[] { new Line("", 0, 0) };
1449 TIntArrayList lfpos = new TIntArrayList();
1452 for (;pos < len; ++lineCount) {
1453 int nextlf = txt.indexOf('\n', pos);
1454 lfpos.add(nextlf != -1 ? nextlf : len);
1459 Line[] lines = new Line[lineCount];
1461 for (int i = 0; i < lineCount-1; ++i) {
1462 int lf = lfpos.getQuick(i);
1463 int cr = (lf > 0) && (txt.charAt(lf - 1) == '\r') ? lf - 1 : lf;
1464 lines[i] = new Line(txt, pos, cr);
1467 lines[lineCount - 1] = new Line(txt, pos, len);
1473 private static Line[] wrapLines(String txt, Font font, float fixedWidth, FontRenderContext frc) {
1474 if(txt == null || txt.isEmpty())
1477 ArrayList<Line> lines =
1478 new ArrayList<Line>();
1480 Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
1481 map.put(TextAttribute.FONT, font);
1482 AttributedString attributedText = new AttributedString(txt.isEmpty() ? "__ERROR__" : txt, map);
1484 AttributedCharacterIterator paragraph = attributedText.getIterator();
1485 int paragraphStart = paragraph.getBeginIndex();
1486 int paragraphEnd = paragraph.getEndIndex();
1487 LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
1489 float breakWidth = fixedWidth;
1491 // Force text to be vertical, by setting break width to 1, if the text area is narrower than "GGGG"
1493 // Set position to the index of the first character in the paragraph.
1494 lineMeasurer.setPosition(paragraphStart);
1496 // Get lines until the entire paragraph has been displayed.
1497 int next, limit, charat, position = 0;
1499 while ((position = lineMeasurer.getPosition()) < paragraphEnd) {
1501 // Find possible line break and set it as a limit to the next layout
1502 next = lineMeasurer.nextOffset(breakWidth);
1504 charat = txt.indexOf(System.getProperty("line.separator"),position+1);
1505 if(charat < next && charat != -1){
1509 lineMeasurer.nextLayout(breakWidth, limit, false);
1511 lines.add(new Line(txt, position, limit));
1514 return lines.toArray(new Line[lines.size()]);
1518 public String getClipboardContent() {
1519 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1520 Transferable clipData = clipboard.getContents(this);
1522 return (String) (clipData.getTransferData(DataFlavor.stringFlavor));
1523 } catch (Exception ee) {
1528 public void setClipboardContent(String content) {
1529 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1530 StringSelection data = new StringSelection(content);
1531 clipboard.setContents(data, data);
1535 public String toString() {
1536 return super.toString() + " [text=" + text + ", font=" + font + ", color=" + color + "]";
1540 protected boolean handleCommand(CommandEvent e) {
1541 if (!hasState(STATE_EDITING))
1544 if (Commands.SELECT_ALL.equals(e.command)) {
1552 protected boolean keyPressed(KeyPressedEvent event) {
1553 if (!hasState(STATE_EDITING))
1556 char c = event.character;
1557 boolean ctrl = event.isControlDown();
1558 boolean alt = event.isAltDown();
1560 // System.out.println("Key pressed '" + (event.character == 0 ? "\\0" : "" + c) + "' " + event.keyCode + " - " + Integer.toBinaryString(event.stateMask));
1561 // System.out.println("ctrl: " + ctrl);
1562 // System.out.println("alt: " + alt);
1564 switch (event.keyCode) {
1566 if (caret != selectionTail) {
1567 int selectionMin = Math.min(caret, selectionTail);
1568 int selectionMax = Math.max(caret, selectionTail);
1569 setClipboardContent(text.substring(selectionMin, selectionMax));
1574 if (caret != selectionTail) {
1575 int selectionMin = Math.min(caret, selectionTail);
1576 int selectionMax = Math.max(caret, selectionTail);
1577 setClipboardContent(text.substring(selectionMin, selectionMax));
1582 case KeyEvent.VK_RIGHT:
1584 // '\'' has the same keycode as VK_RIGHT but when right
1585 // arrow is pressed, event character is \0.
1586 moveCaretCtrlRight(event.isShiftDown());
1590 case KeyEvent.VK_LEFT:
1591 moveCaretCtrlLeft(event.isShiftDown());
1596 String content = getClipboardContent();
1602 // Replaced by #handleCommand
1603 // case KeyEvent.VK_A:
1609 case KeyEvent.VK_ENTER:
1611 insert(getLineSeparator());
1619 } else if (!ctrl && alt) {
1622 switch (event.keyCode) {
1623 case KeyEvent.VK_LEFT:
1624 moveCaret(-1, event.isShiftDown());
1626 case KeyEvent.VK_RIGHT:
1628 // '\'' has the same keycode as VK_RIGHT but when right
1629 // arrow is pressed, event character is \0.
1630 moveCaret(1, event.isShiftDown());
1633 // Intentional fallthrough to default case
1634 case KeyEvent.VK_UP:
1635 moveCaretRowUp(event.isShiftDown());
1637 case KeyEvent.VK_DOWN:
1638 moveCaretRowDown(event.isShiftDown());
1640 case KeyEvent.VK_HOME:
1641 moveCaretHome(event.isShiftDown());
1643 case KeyEvent.VK_END:
1644 moveCaretEnd(event.isShiftDown());
1647 case KeyEvent.VK_ENTER:
1648 fireTextEditingEnded();
1651 case KeyEvent.VK_ESCAPE:
1652 text = textBeforeEdit;
1654 clearState(STATE_EDITING);
1655 fireTextEditingCancelled();
1658 case KeyEvent.VK_BACK_SPACE:
1659 if(caret == selectionTail && caret > 0) {
1660 // line separator may use multiple characters, we want to remove that with one command
1661 String lineSep = getLineSeparator();
1662 int index = lineSep.indexOf(text.charAt(caret-1));
1667 selectionTail+= (lineSep.length()-index-1);
1673 case KeyEvent.VK_DELETE:
1674 if(caret == selectionTail && caret < text.length()) {
1675 String lineSep = getLineSeparator();
1676 int index = lineSep.indexOf(text.charAt(caret));
1680 selectionTail-= index;
1681 caret+= (lineSep.length()-index);
1690 if (c == 65535 || Character.getType(c) == Character.CONTROL) {
1693 //System.out.println("Char " + c + " " + Character.getType(c) + " " + text);
1694 insert(new String(new char[] {c}));
1698 // FIXME This is called even if just caret was moved.
1699 // This is currently necessary for repaints.
1701 invalidateXOffset();
1705 protected String getLineSeparator() {
1706 return System.getProperty("line.separator");
1709 protected void selectAll() {
1711 setCaret(text.length(), true);
1714 protected transient int hoverClick = 0;
1717 protected boolean mouseClicked(MouseClickEvent event) {
1718 if (event.button != MouseClickEvent.LEFT_BUTTON)
1721 if (hasState(STATE_HOVER)) {
1725 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1726 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1729 IElement e = DiagramNodeUtil.getElement(ctx, this);
1730 if (!hasState(STATE_EDITING)) {
1731 if (Boolean.TRUE.equals(setEditMode(true))) {
1732 editActivation = activateEdit(0, e, ctx);
1738 if (hasState(STATE_EDITING)) {
1739 fireTextEditingEnded();
1745 protected boolean mouseDoubleClicked(MouseDoubleClickedEvent event) {
1746 if (event.button != MouseClickEvent.LEFT_BUTTON)
1749 if (hitTest(event, 0)) {
1750 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1751 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1756 // Select the whole text.
1758 setCaret(text.length(), true);
1767 protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {
1768 if (!hasState(STATE_EDITING))
1771 Point2D local = controlToLocal( event.controlPosition );
1772 // FIXME: once the event coordinate systems are cleared up, remove this workaround
1773 local = parentToLocal(local);
1774 if (hasState(STATE_HOVER) && this.containsLocal(local)) {
1775 setCaret(local, event.isShiftDown());
1781 protected boolean mouseMoved(MouseMovedEvent event) {
1782 boolean hit = hitTest(event, 3.0);
1783 if (hit != hasState(STATE_HOVER)) {
1784 setState(STATE_HOVER, hit);
1790 private boolean isControlDown(MouseEvent e) {
1791 return e.isControlDown() || lastMouseEvent!=null?lastMouseEvent.isControlDown():false;
1794 protected boolean isShiftDown(MouseEvent e) {
1795 return e.isShiftDown() || lastMouseEvent!=null?lastMouseEvent.isShiftDown():false;
1798 // private boolean isAltDown(MouseEvent e) {
1799 // return e.isAltDown() || lastMouseEvent!=null?lastMouseEvent.isAltDown():false;
1803 protected boolean mouseDragged(MouseDragBegin e) {
1805 && (isControlDown(e) || isShiftDown(e))
1806 && e.context instanceof NodeEventHandler
1807 && (dataRVI != null || text != null))
1809 List<Transferable> trs = new ArrayList<>(2);
1810 if (dataRVI != null) {
1811 trs.add(new LocalObjectTransferable(dataRVI));
1812 trs.add(new PlaintextTransfer(dataRVI.toString()));
1813 } else if (text != null && !text.isEmpty()) {
1814 trs.add(new PlaintextTransfer(text));
1816 if (!trs.isEmpty()) {
1817 e.transferable = new MultiTransferable(trs);
1824 protected boolean hitTest(MouseEvent event, double tolerance) {
1825 Rectangle2D bounds = getBoundsInternal();
1828 Point2D localPos = NodeUtil.worldToLocal(this, event.controlPosition, new Point2D.Double());
1829 double x = localPos.getX();
1830 double y = localPos.getY();
1831 boolean hit = bounds.contains(x, y);
1835 public Rectangle2D getBoundsInternal() {
1836 Rectangle2D local = lastBounds;
1839 // TODO: potential spot for CPU/memory allocation optimization
1840 // by using more specialized implementations
1841 if (transform.isIdentity())
1843 return transform.createTransformedShape(local).getBounds2D();
1846 protected Color add(Color c, int r, int g, int b) {
1847 int nr = Math.min(255, c.getRed() + r);
1848 int ng = Math.min(255, c.getGreen() + g);
1849 int nb = Math.min(255, c.getBlue() + b);
1850 return new Color(nr, ng, nb);
1853 public TextEditActivation activateEdit(int mouseId, IElement e, ICanvasContext ctx) {
1854 EditDataNode data = EditDataNode.getNode(this);
1855 deactivateEdit(data, null);
1856 TextEditActivation result = new TextEditActivation(mouseId, e, ctx);
1857 data.setTextEditActivation(result);
1862 * @return <code>true</code> if this node is or was previously in editing
1865 protected boolean deactivateEdit() {
1866 boolean result = deactivateEdit( editActivation );
1867 result |= editActivation != null;
1868 editActivation = null;
1872 protected boolean deactivateEdit(TextEditActivation activation) {
1873 return deactivateEdit( EditDataNode.getNode(this), activation );
1876 protected boolean deactivateEdit(EditDataNode data, TextEditActivation activation) {
1877 TextEditActivation previous = data.getTextEditActivation();
1878 if (previous != null && (previous == activation || activation == null)) {
1880 data.setTextEditActivation(null);
1887 public int getEventMask() {
1888 return EventTypes.KeyPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask
1889 | EventTypes.MouseClickMask | EventTypes.MouseDragBeginMask | EventTypes.CommandMask;
1892 private MouseEvent lastMouseEvent = null;
1895 public boolean handleEvent(Event e) {
1896 if(e instanceof MouseEvent && !(e instanceof MouseDragBegin)) lastMouseEvent = (MouseEvent)e;
1897 return super.handleEvent(e);
1901 public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
1902 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
1906 public <T> T getProperty(String propertyName) {
1911 public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
1914 public void synchronizeText(String text) {
1918 public void synchronizeColor(RGB.Integer color) {
1919 this.color = Colors.awt(color);
1922 public void synchronizeFont(org.simantics.datatypes.literal.Font font) {
1923 setFont(Fonts.awt(font));
1926 public void synchronizeTransform(double[] data) {
1927 this.setTransform(new AffineTransform(data));
1930 public static void main(String[] args) {
1931 Line[] lines = parseLines("\n \n FOO \n\nBAR\n\n\n BAZ\n\n");
1932 System.out.println(Arrays.toString(lines));
1933 System.out.println(GeometryUtils.pointToMillimeter(1));
1934 System.out.println(GeometryUtils.pointToMillimeter(12));
1935 System.out.println(GeometryUtils.pointToMillimeter(72));
1938 ///////////////////////////////////////////////////////////////////////////
1939 // LEGACY CODE NEEDED BY INHERITED CLASSES FOR NOW
1940 ///////////////////////////////////////////////////////////////////////////
1942 protected double getHorizontalAlignOffset(Rectangle2D r) {
1943 switch (horizontalAlignment) {
1944 case 0: return 0; // Leading
1945 case 1: return -r.getWidth(); // Trailing
1946 case 2: return -r.getCenterX(); // Center
1951 protected double getVerticalAlignOffset() {
1952 FontMetrics fm = fontMetrics;
1955 switch (verticalAlignment) {
1956 case 0: return fm.getMaxAscent(); // Leading=top=maxascent
1957 case 1: return -fm.getMaxDescent(); // Trailing=bottom=maxdescent
1958 case 2: return fm.getMaxAscent() / 2; // Center=maxascent / 2
1964 ///////////////////////////////////////////////////////////////////////////
1966 ///////////////////////////////////////////////////////////////////////////