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.G2DRenderingHints;
55 import org.simantics.scenegraph.g2d.G2DRenderingHints.TextRenderingMode;
56 import org.simantics.scenegraph.g2d.events.Event;
57 import org.simantics.scenegraph.g2d.events.EventTypes;
58 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
59 import org.simantics.scenegraph.g2d.events.MouseEvent;
60 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
61 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;
62 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent;
63 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;
64 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
65 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
66 import org.simantics.scenegraph.g2d.events.command.Commands;
67 import org.simantics.scenegraph.utils.GeometryUtils;
68 import org.simantics.scenegraph.utils.NodeUtil;
69 import org.simantics.scl.runtime.function.Function1;
70 import org.simantics.scl.runtime.function.Function2;
71 import org.simantics.ui.colors.Colors;
72 import org.simantics.ui.dnd.LocalObjectTransferable;
73 import org.simantics.ui.dnd.MultiTransferable;
74 import org.simantics.ui.dnd.PlaintextTransfer;
75 import org.simantics.ui.fonts.Fonts;
76 import org.simantics.utils.threads.AWTThread;
78 import com.lowagie.text.pdf.PdfWriter;
80 import gnu.trove.list.array.TIntArrayList;
84 * TextNode which supports in-line editing.
86 * By default <code>TextNode</code> is in editable = false state. Use
87 * {@link #setEditable(boolean)} to make it editable.
89 * @author Hannu Niemistö <hannu.niemisto@vtt.fi>
90 * @author Marko Luukkainen <marko.luukkainen@vtt.fi>
91 * @author Tuukka Lehtonen <tuukka.lehtonen@semantum.fi>
94 * o proper support for defining clipping bounds for the text (needed for page templates) (currently through fixedWidth)
95 * o fix editing xOffset to work with fixed width and multi-line text
100 public class TextNode extends G2DNode implements IDynamicSelectionPainterNode, LoaderNode {
102 private static final long serialVersionUID = 654692698101485672L;
104 public static enum TextFlipping {
107 VerticalTextDownwards,
111 * TODO: justify existence for this
113 private static final BasicStroke RESET_STROKE = new BasicStroke(1);
116 * Src-over alpha composite instance with 50% opacity.
118 private static final AlphaComposite SrcOver_50 = AlphaComposite.SrcOver.derive(0.5f);
121 * For (inexact) measurement of rendered text bounds.
123 protected static final FontRenderContext FRC = new FontRenderContext(new AffineTransform(), true, true);
125 private static final Font FONT = Font.decode("Arial 6");
126 private static final Color SELECTION_BACKGROUND_COLOR = new Color(0x316ac5);
127 // private static final double MAX_CARET_POSITION = 1.0;
130 * The complete text visualized by this node.
132 protected String text = null;
135 * The font used to render the {@link #text}.
137 protected Font font = FONT;
140 * The color of the rendered text. Default value is {@value Color#black}.
142 protected Color color = Color.BLACK;
145 * The background color used for filling the background of the bounding box
146 * of the rendered text. <code>null</code> means no fill.
147 * Default value is <code>null</code>.
149 protected Color backgroundColor = null;
152 * The color used for drawing the expanded bounding box border for the
153 * rendered text. <code>null</code> means no border is rendered. Default
154 * value is <code>null</code>.
156 protected Color borderColor = null;
158 protected double scale = 1.0;
159 protected transient double scaleRecip = 1.0;
164 protected float borderWidth = 0.f;
166 protected double paddingX = 2.0;
167 protected double paddingY = 2.0;
170 * Horizontal text box alignment with respect to its origin. Default value is
173 protected byte horizontalAlignment = 0;
175 * Vertical text box alignment with respect to its origin. Default value is
178 protected byte verticalAlignment = 3;
181 * Tells if this node is still pending for real results or not.
183 protected static final int STATE_PENDING = (1 << 0);
184 protected static final int STATE_HOVER = (1 << 1);
185 protected static final int STATE_EDITABLE = (1 << 2);
186 protected static final int STATE_SHOW_SELECTION = (1 << 3);
187 protected static final int STATE_WRAP_TEXT = (1 << 4);
188 protected transient static final int STATE_EDITING = (1 << 5);
189 protected transient static final int STATE_VALID = (1 << 6);
190 protected transient static final int STATE_X_OFFSET_IS_DIRTY = (1 << 7);
191 protected static final int STATE_ALWAYS_ADD_LISTENERS = (1 << 8);
192 protected static final int STATE_LISTENERS_ADDED = (1 << 9);
193 protected static final int STATE_AUTOMATIC_TEXT_FLIP_ENABLED = (1 << 10);
194 protected static final int STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN = (1 << 11);
197 * A combination of all the STATE_ constants defined in this class,
198 * e.g. {@link #STATE_PENDING}.
200 protected int state = STATE_SHOW_SELECTION | STATE_WRAP_TEXT | STATE_VALID | STATE_X_OFFSET_IS_DIRTY;
202 protected RVI dataRVI = null;
205 int selectionTail = 0;
208 float fixedWidth = 0f;
210 private Rectangle2D targetBounds;
212 Function1<String, String> validator;
213 ITextListener textListener;
214 ITextContentFilter editContentFilter;
217 * The renderable line structures parsed from {@link #text} by
218 * {@link #parseLines(String)}, laid out by
219 * {@link #layoutLines(Line[], FontRenderContext)} and aligned by
220 * {@link #alignLines(Line[], Rectangle2D, byte, byte)}
222 protected transient Line[] lines = null;
223 protected transient FontMetrics fontMetrics = null;
226 * Stores the value of {@link #text} before edit mode was last entered. Used
227 * for restoring the original value if editing is cancelled.
229 private transient String textBeforeEdit = null;
230 protected transient TextEditActivation editActivation;
233 * Stores the last scaled bounds.
235 private transient Rectangle2D lastBounds = new Rectangle2D.Double();
238 * This must be nullified if anything that affects the result of
239 * {@link #getTightAlignedBoundsInLocal(Rectangle2D, FontRenderContext)}
240 * changes. It will cause the cached value to be recalculated on the next
243 private transient Rectangle2D tightBoundsCache = null;
248 // Mark this node as pending
249 NodeUtil.increasePending(this);
253 public void cleanup() {
258 protected boolean hasState(int flags) {
259 return (state & flags) == flags;
262 protected void setState(int flags) {
266 protected void setState(int flags, boolean set) {
270 this.state &= ~flags;
273 protected void clearState(int flags) {
274 this.state &= ~flags;
277 protected void setListeners(boolean add) {
284 protected void addListeners() {
285 if (!hasState(STATE_LISTENERS_ADDED)) {
286 addEventHandler(this);
287 setState(STATE_LISTENERS_ADDED);
291 protected void removeListeners() {
292 if (hasState(STATE_LISTENERS_ADDED)) {
293 removeEventHandler(this);
294 clearState(STATE_LISTENERS_ADDED);
299 * 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
302 public void setForceEventListening(boolean force) {
303 setState(STATE_ALWAYS_ADD_LISTENERS, force);
304 if (force && !hasState(STATE_EDITABLE)) {
310 * Enables or disables edit mode. It also sets
311 * the caret at the end of text all selects the
312 * whole text (this is the usual convention when
313 * beginning to edit one line texts).
315 * @return null if no change to edit state was made
317 public Boolean setEditMode(boolean edit) {
318 return setEditMode(edit, true);
322 * Enables or disables edit mode. It also sets
323 * the caret at the end of text all selects the
324 * whole text (this is the usual convention when
325 * beginning to edit one line texts).
327 * @return null if no change to edit state was made
329 protected Boolean setEditMode(boolean edit, boolean notify) {
330 if (edit && !hasState(STATE_EDITABLE))
332 if (hasState(STATE_EDITING) == edit)
334 setState(STATE_EDITING, edit);
336 caret = text != null ? text.length() : 0;
338 textBeforeEdit = text;
340 fireTextEditingStarted();
344 fireTextEditingEnded();
345 return Boolean.FALSE;
349 @SyncField({"editable"})
350 public void setEditable(boolean editable) {
351 boolean changed = hasState(STATE_EDITABLE) != editable;
352 setState(STATE_EDITABLE, editable);
353 if (hasState(STATE_EDITING) && !editable)
355 if (changed && !hasState(STATE_ALWAYS_ADD_LISTENERS)) {
356 setListeners(editable);
360 public boolean isEditable() {
361 return hasState(STATE_EDITABLE);
364 public boolean isEditMode() {
365 return hasState(STATE_EDITING);
368 @SyncField({"wrapText"})
369 public void setWrapText(boolean wrapText) {
370 setState(STATE_WRAP_TEXT, wrapText);
374 * @return Does the text box wrap text if
375 * the width of the box is fixed
377 public boolean isWrapText() {
378 return hasState(STATE_WRAP_TEXT);
381 @SyncField({"showSelection"})
382 public void setShowSelection(boolean showSelection) {
383 setState(STATE_SHOW_SELECTION, showSelection);
386 public boolean showsSelection() {
387 return hasState(STATE_SHOW_SELECTION);
394 * @param x not supported anymore, use {@link #setTransform(AffineTransform)} instead
395 * @param y not supported anymore, use {@link #setTransform(AffineTransform)} instead
398 @SyncField({"text", "font", "color", "x", "y", "scale"})
399 public void init(String text, Font font, Color color, double x, double y, double scale) {
401 if(this.text == null && text != null) NodeUtil.decreasePending(this);
403 if (hasState(STATE_EDITING))
410 this.scaleRecip = 1.0 / scale;
412 this.selectionTail = 0;
418 public void setAutomaticTextFlipping(TextFlipping type) {
421 clearState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED | STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
423 case VerticalTextDownwards:
424 setState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED | STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
426 case VerticalTextUpwards:
427 setState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED);
428 clearState(STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
433 @SyncField({"paddingX", "paddingY"})
434 public void setPadding(double x, double y) {
439 @SyncField({"color"})
440 public void setColor(Color color) {
444 @SyncField({"backgroundColor"})
445 public void setBackgroundColor(Color color) {
446 this.backgroundColor = color;
449 @SyncField({"borderColor"})
450 public void setBorderColor(Color color) {
451 this.borderColor = color;
454 public String getText() {
458 public String getTextBeforeEdit() {
459 return textBeforeEdit;
462 @SyncField({"text","caret","selectionTail"})
463 public void setText(String text) {
464 //System.out.println("TextNode.setText('" + text + "', " + editing + ")");
465 if (hasState(STATE_EDITING))
469 if(this.text != null && text == null) NodeUtil.increasePending(this);
471 if(this.text == null && text != null) NodeUtil.decreasePending(this);
474 caret = text != null ? Math.min(caret, text.length()) : 0;
475 selectionTail = caret;
480 @SyncField({"pending"})
481 public void setPending(boolean pending) {
482 boolean p = hasState(STATE_PENDING);
483 if(!p && pending) NodeUtil.increasePending(this);
484 if(p && !pending) NodeUtil.decreasePending(this);
486 setState(STATE_PENDING, pending);
489 @SyncField({"fixedWidth"})
490 public void setFixedWidth(float fixedWidth) {
492 throw new IllegalArgumentException("negative fixed width");
493 this.fixedWidth = fixedWidth;
498 * Bounds where the text box will be drawn
501 public void setTargetBounds(Rectangle2D bounds) {
502 this.targetBounds = bounds;
505 final public void synchronizeWidth(float width) {
507 setFixedWidth(width);
510 final public void synchronizeBorderWidth(float width) {
512 setBorderWidth(width);
515 public final void synchronizeWrapText(boolean wrap) {
516 setState(STATE_WRAP_TEXT, wrap);
519 public boolean isHovering() {
520 return hasState(STATE_HOVER);
523 @SyncField({"hover"})
524 public void setHover(boolean hover) {
525 setState(STATE_HOVER, hover);
529 public Font getFont() {
534 public void setFont(Font font) {
539 public double getBorderWidth() {
543 @SyncField({"borderWidth"})
544 public void setBorderWidth(float width) {
545 this.borderWidth = width;
548 public void setBorderWidth(double width) {
549 setBorderWidth((float)width);
552 @SyncField({"horizontalAlignment"})
553 public void setHorizontalAlignment(byte horizontalAlignment) {
554 if (horizontalAlignment < 0 && horizontalAlignment > 2)
555 throw new IllegalArgumentException("Invalid horizontal alignment: " + horizontalAlignment + ", must be between 0 and 2");
556 this.horizontalAlignment = horizontalAlignment;
560 final public void synchronizeHorizontalAlignment(byte horizontalAlignment) {
561 if (horizontalAlignment >= 0 && horizontalAlignment <= 2)
562 setHorizontalAlignment(horizontalAlignment);
565 public byte getHorizontalAlignment() {
566 return horizontalAlignment;
569 @SyncField({"verticalAlignment"})
570 public void setVerticalAlignment(byte verticalAlignment) {
571 if (verticalAlignment < 0 && verticalAlignment > 3)
572 throw new IllegalArgumentException("Invalid vertical alignment: " + verticalAlignment + ", must be between 0 and 3");
573 this.verticalAlignment = verticalAlignment;
577 final public void synchronizeVerticalAlignment(byte verticalAlignment) {
578 if (verticalAlignment >= 0 && verticalAlignment <= 3)
579 setVerticalAlignment(verticalAlignment);
582 public byte getVerticalAlignment() {
583 return verticalAlignment;
587 * Rendering is single-threaded so we can use a static rectangle for
588 * calculating the expanded bounds for the node.
590 private static transient ThreadLocal<Rectangle2D> tempBounds = new ThreadLocal<Rectangle2D>() {
592 protected Rectangle2D initialValue() {
593 return new Rectangle2D.Double();
598 * Rendering is single-threaded so we can use a static AffineTransform to
599 * prevent continuous memory allocation during text rendering.
601 private static transient ThreadLocal<AffineTransform> tempAffineTransform = new ThreadLocal<AffineTransform>() {
603 protected AffineTransform initialValue() {
604 return new AffineTransform();
609 public void render(Graphics2D g) {
610 AffineTransform ot = g.getTransform();
616 * Note: does not return transformation, stroke, color, etc. to their
620 * @param applyTransform
622 public void render(Graphics2D g, boolean applyTransform) {
623 if (text == null || font == null || color == null)
626 // Cache font metrics if necessary
627 if (fontMetrics == null)
628 fontMetrics = g.getFontMetrics(font);
630 Color color = this.color;
631 boolean isSelected = NodeUtil.isSelected(this, 1);
632 boolean hover = hasState(STATE_HOVER);
633 boolean editing = hasState(STATE_EDITING);
635 if (!isSelected && hover && textListener != null) {
636 color = add(color, 120, 120, 120);
640 g.transform(transform);
641 // Apply separate legacy scale
643 g.scale(scale, scale);
645 // Safety for not rendering when the scale of this text is too small.
646 // When the scale is too small it will cause internal exceptions while
648 AffineTransform curTr = g.getTransform();
649 double currentScale = GeometryUtils.getScale(curTr);
650 //System.out.println("currentScale: " + currentScale);
651 if (currentScale < 1e-6)
657 // Calculate text clip rectangle.
658 // This updates textLayout if necessary.
659 Rectangle2D r = getTightAlignedBoundsInLocal(tempBounds.get(), fontMetrics.getFontRenderContext());
661 computeEditingXOffset();
664 r.setFrame(r.getMinX(), r.getMinY(), fixedWidth, r.getHeight());
665 if(targetBounds != null) {
666 double w = (targetBounds.getWidth() - paddingX * 2) * scaleRecip;
667 double h = (targetBounds.getHeight() - paddingY * 2) * scaleRecip;
668 double x = (targetBounds.getMinX() + paddingX) * scaleRecip;
669 double y = (targetBounds.getMinY() + paddingY) * scaleRecip;
670 r.setRect(x, y, w, h);
673 if (hasState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED)) {
676 if (curTr.getScaleX() != 0) {
677 needsXFlip = curTr.getScaleX() < 0.0;
678 needsYFlip = curTr.getScaleY() < 0.0;
680 boolean flipAll = !hasState(STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
681 needsXFlip = (curTr.getShearY() < 0.0) ^ flipAll;
682 needsYFlip = (curTr.getShearX() > 0.0) ^ flipAll;
684 if (needsXFlip || needsYFlip) {
685 double centerX = r.getWidth()*0.5 + r.getX();
686 double centerY = r.getHeight()*0.5 + r.getY();
688 g.translate(centerX, centerY);
689 g.scale(needsXFlip ? -1.0 : 1.0, needsYFlip ? -1.0 : 1.0);
690 g.translate(-centerX, -centerY);
694 Rectangle2D textClip = r.getBounds2D();
696 expandBoundsUnscaled(r);
698 // Speed rendering optimization: don't draw text that is too small to
699 // read when not editing
700 boolean renderText = true;
702 Object renderingHint = g.getRenderingHint(RenderingHints.KEY_RENDERING);
703 if (renderingHint != RenderingHints.VALUE_RENDER_QUALITY) {
704 float textSizeMM = (float) currentScale * GeometryUtils.pointToMillimeter(font.getSize2D());
705 if (textSizeMM < 1.5f)
710 Shape clipSave = g.getClip();
714 PdfWriter writer = (PdfWriter) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_WRITER);
715 TextRenderingMode renderingMode = (TextRenderingMode) g.getRenderingHint(G2DRenderingHints.KEY_TEXT_RENDERING_MODE);
716 boolean renderAsText = writer != null || renderingMode == TextRenderingMode.AS_TEXT;
719 Color backgroundColor = hasState(STATE_VALID) ? this.backgroundColor : Color.red;
723 // Fill background if necessary
724 if (backgroundColor != null) {
725 g.setColor(backgroundColor);
731 int selectionMin = Math.min(caret, selectionTail);
732 int selectionMax = Math.max(caret, selectionTail);
736 renderText(g, xOffset, renderAsText);
738 Shape clip = g.getClip();
740 // Selection background & text
741 for (Line line : lines) {
742 if (line.intersectsRange(selectionMin, selectionMax)) {
743 Shape selShape = line.getLogicalHighlightShape(selectionMin, selectionMax);
744 line.translate(g, xOffset, 0);
746 g.setColor(SELECTION_BACKGROUND_COLOR);
748 g.setColor(Color.WHITE);
749 // #6459: render as text in PDF and paths on screen
751 g.drawString(line.getText(), 0, 0);
753 line.layout.draw(g, 0, 0);
754 line.translateInv(g, xOffset, 0);
766 renderText(g, 0, renderAsText);
774 if (borderWidth > 0f && borderColor != null) {
775 g.setColor(borderColor);
776 g.setStroke(new BasicStroke((float) (scale*borderWidth)));
780 //System.out.println("bw: " + borderWidth);
781 if (isSelected && showsSelection()) {
782 Composite oc = g.getComposite();
783 g.setComposite(SrcOver_50);
784 g.setColor(Color.RED);
785 float bw = borderWidth;
786 double s = currentScale;
788 bw = (float) (1f / s);
792 g.setStroke(new BasicStroke(bw));
794 //g.draw(GeometryUtils.expandRectangle(r, 1.0));
799 g.scale(scaleRecip, scaleRecip);
800 g.setStroke(RESET_STROKE);
802 lastBounds = getScaledOffsetBounds(r, lastBounds, scale, 0, 0);
803 // g.setColor(Color.MAGENTA); // DEBUG
804 // g.draw(lastBounds); // DEBUG
805 // g.setColor(Color.ORANGE); // DEBUG
806 // g.draw(getBoundsInLocal()); // DEBUG
808 renderSelectedHover(g, isSelected, hover);
811 private void renderCaret(Graphics2D g) {
812 g.setColor(Color.BLACK);
813 for (int i = 0; i < lines.length; i++) {
814 Line line = lines[i];
815 // prevent rendering caret twice on line changes
816 if (line.containsOffset(caret) && // line contains caret
817 (caret != line.endOffset || //caret is not in the end of the line
818 i == lines.length-1 || //caret is end of the last line
819 lines[i+1].startOffset != line.endOffset)) { // beginning of the next line does not start withe the same index as current line
820 Shape[] caretShape = line.getCaretShapes(caret);
821 line.translate(g, xOffset, 0);
822 g.draw(caretShape[0]);
823 if (caretShape[1] != null)
824 g.draw(caretShape[1]);
825 line.translateInv(g, xOffset, 0);
829 private void renderText(Graphics2D g, float xOffset, boolean renderAsText) {
830 //g.draw(tightBoundsCache); // DEBUG
831 for (Line line : lines) {
832 // #6459: render as text in PDF and paths on screen
834 g.drawString(line.getText(), line.alignedPosX + xOffset, line.alignedPosY);
836 line.layout.draw(g, line.alignedPosX + xOffset, line.alignedPosY);
837 //g.draw(line.abbox); // DEBUG
841 protected Rectangle2D getScaledOffsetBounds(Rectangle2D originalBounds, Rectangle2D dst, double scale, double offsetX, double offsetY) {
842 AffineTransform btr = tempAffineTransform.get();
843 btr.setToTranslation(offsetX*scale, offsetY*scale);
844 btr.scale(scale, scale);
845 if (btr.isIdentity()) {
846 dst.setFrame(originalBounds);
848 dst.setFrame(btr.createTransformedShape(originalBounds).getBounds2D());
854 * Invoked when TextNode is selected and a mouse is hovering on top of it. Can be overridden to add custom rendering.
858 protected void renderSelectedHover(Graphics2D g, boolean isSelected, boolean isHovering) {
861 public String editText(String text) {
863 String error = validator != null ? validator.apply(text) : null;
866 if (textListener != null) {
867 textListener.textEditingEnded();
874 * Replaces the current selection with the content or inserts
875 * the content at caret. After the insertion the caret
876 * will be at the end of inserted text and selection will
880 @SyncField({"text","caret","selectionTail"})
881 protected void insert(String content) {
882 content = editContentFilter != null ? editContentFilter.filter(this, content) : content;
884 int selectionMin = Math.min(caret, selectionTail);
885 int selectionMax = Math.max(caret, selectionTail);
887 String begin = text.substring(0, selectionMin);
888 String end = text.substring(selectionMax);
889 text = begin + content + end;
890 caret = selectionMin + content.length();
891 selectionTail = caret;
893 assert (caret <= text.length());
894 //System.out.println(text + " " + caret );
896 if(validator != null) {
897 String error = validator.apply(text);
898 setState(STATE_VALID, (error == null));
905 protected void fireTextChanged() {
906 if(textListener != null)
907 textListener.textChanged();
912 protected void fireTextEditingStarted() {
913 if(textListener != null)
914 textListener.textEditingStarted();
918 protected void fireTextEditingCancelled() {
919 setState(STATE_VALID);
921 if (deactivateEdit()) {
922 if (textListener != null)
923 textListener.textEditingCancelled();
925 setEditMode(false, false);
927 if (textBeforeEdit != null)
928 setText(textBeforeEdit);
935 public void fireTextEditingEnded() {
936 if (!hasState(STATE_VALID)) {
937 fireTextEditingCancelled();
938 setState(STATE_VALID);
942 if (deactivateEdit()) {
943 if (textListener != null)
944 textListener.textEditingEnded();
946 setEditMode(false, false);
951 public void setTextListener(ITextListener listener) {
952 this.textListener = listener;
955 public void setValidator(Function1<String, String> validator) {
956 this.validator = validator;
959 public void setContentFilter(ITextContentFilter filter) {
960 this.editContentFilter = filter;
963 public void setRVI(RVI rvi) {
967 private void invalidateXOffset() {
968 setState(STATE_X_OFFSET_IS_DIRTY);
971 private void computeEditingXOffset() {
973 if(lines == null) return;
974 if(!hasState(STATE_X_OFFSET_IS_DIRTY)) return;
975 if(fixedWidth > 0f) {
978 // float[] coords = textLayout.getCaretInfo(TextHitInfo.afterOffset(caret));
979 // if(coords != null) {
980 // if(coords[0] > (fixedWidth*MAX_CARET_POSITION)) xOffset = (float)((fixedWidth*MAX_CARET_POSITION)-coords[0]);
990 clearState(STATE_X_OFFSET_IS_DIRTY);
994 @SyncField({"caret","selectionTail"})
995 protected void moveCaret(int move, boolean select) {
996 // prevent setting caret into line separator.
998 while (text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
1000 } else if (move < 0) {
1001 while (caret+move >= 0 && text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
1007 if (caret > text.length())
1008 caret = text.length();
1010 selectionTail = caret;
1013 private Line findCaretLine() {
1014 // Find the line where caret is. Starting from first line.
1015 for(int i = 0; i < lines.length; i++) {
1016 Line line = lines[i];
1017 if(caret <= line.endOffset) {
1025 * Moves caret to next not letter or digit
1028 private void moveCaretCtrlLeft(boolean shiftDown) {
1029 Line line = findCaretLine();
1032 for(i = caret-1; i > line.startOffset; i--) {
1033 char c = line.document.charAt(i);
1034 if(!Character.isLetterOrDigit(c)) {
1038 moveCaret(i - caret, shiftDown);
1043 * Moves caret to previous non letter or digit
1046 private void moveCaretCtrlRight(boolean shiftDown) {
1047 Line line = findCaretLine();
1050 for(i = caret + 1; i < line.endOffset; i++) {
1051 char c = line.document.charAt(i);
1052 if(!Character.isLetterOrDigit(c)) {
1056 moveCaret(i - caret, shiftDown);
1061 * Moves caret to line end
1064 private void moveCaretEnd(boolean shiftDown) {
1065 Line line = findCaretLine();
1067 // Move caret to the end of the line
1068 moveCaret(line.endOffset - caret, shiftDown);
1072 * Moves caret to beginning of a line
1075 private void moveCaretHome(boolean shiftDown) {
1076 Line line = findCaretLine();
1078 // Move caret to the beginning of the line
1079 moveCaret(line.startOffset - caret, shiftDown);
1083 * Moves caret one row up and tries to maintain the location
1086 private void moveCaretRowUp(boolean shiftDown) {
1087 // Find the line where caret is. Starting from first line.
1088 for(int i = 0; i < lines.length; i++) {
1089 Line line = lines[i];
1090 if(caret <= line.endOffset) {
1091 // caret is in this line
1093 // Already on top line
1094 // Select the beginning of the line
1095 moveCaret(-caret, shiftDown);
1097 Line prevLine = lines[i-1];
1098 int prevLength = prevLine.endOffset - prevLine.startOffset;
1099 int posInCurRow = caret - line.startOffset;
1100 if(prevLength < posInCurRow)
1101 posInCurRow = prevLength;
1103 int newPos = prevLine.startOffset + posInCurRow;
1104 moveCaret(newPos - caret, shiftDown);
1112 * Moves caret one row down and tries to maintain the location
1115 private void moveCaretRowDown(boolean shiftDown) {
1116 // Find the line where caret is. Starting from last line.
1117 for(int i = lines.length - 1; i >= 0; i--) {
1118 Line line = lines[i];
1119 if(caret >= line.startOffset) {
1120 // caret is in this line
1121 if(i == lines.length - 1) {
1122 // Already on bottom line, cannot go below
1123 // Select to the end of the line
1124 moveCaret(line.endOffset - caret, shiftDown);
1126 Line prevLine = lines[i+1]; // Previous line
1128 // Find new caret position.
1129 // Either it is in the same index as before, or if the row
1130 // is not long enough, select the end of the row.
1131 int prevLength = prevLine.endOffset - prevLine.startOffset;
1132 int posInCurRow = caret - line.startOffset;
1133 if(prevLength < posInCurRow)
1134 posInCurRow = prevLength;
1135 int newPos = prevLine.startOffset + posInCurRow;
1136 moveCaret(newPos - caret, shiftDown);
1143 @SyncField({"caret","selectionTail"})
1144 protected void setCaret(int pos, boolean select) {
1148 if (caret > text.length())
1149 caret = text.length();
1151 selectionTail = caret;
1154 protected void setCaret(Point2D point) {
1155 setCaret(point, false);
1158 @SyncField({"caret","selectionTail"})
1159 protected void setCaret(Point2D point, boolean select) {
1161 for(int i = 0; i < lines.length; i++) {
1162 Line line = lines[i];
1163 Rectangle2D bounds = line.abbox;
1164 // Add heights of bboxes for determining the correct line
1166 lineY = bounds.getY();
1168 lineY += lines[i-1].abbox.getHeight();
1170 double lineHeight = bounds.getHeight();
1171 double hitY = point.getY() / scale;
1172 if(hitY >= lineY && hitY <= lineY + lineHeight) {
1173 // Hit is in this line
1174 float x = (float)(point.getX() / scale) - (float)line.abbox.getX();
1175 float y = (float)(point.getY() / scale - lineHeight * i) ;
1176 TextHitInfo info = line.layout.hitTestChar(x, y);
1177 caret = line.startOffset + info.getInsertionIndex();
1178 if (caret > line.endOffset)
1179 caret = line.endOffset;
1181 selectionTail = caret;
1186 invalidateXOffset();
1187 assert (caret <= text.length());
1191 public Rectangle2D getBoundsInLocal() {
1192 if(targetBounds != null)
1193 return targetBounds;
1195 return expandBounds( getTightAlignedBoundsInLocal(null) );
1198 protected Rectangle2D expandBounds(Rectangle2D r) {
1199 r.setRect(r.getX() * scale - paddingX, r.getY() * scale -paddingY, r.getWidth()*scale + paddingX + paddingX, r.getHeight()*scale + paddingY + paddingY);
1200 //System.out.println(" => " + r);
1204 protected Rectangle2D expandBoundsUnscaled(Rectangle2D r) {
1205 r.setRect(r.getX() - scaleRecip*paddingX, r.getY() -scaleRecip*paddingY, r.getWidth() + scaleRecip*paddingX + scaleRecip*paddingX, r.getHeight() + scaleRecip*paddingY + scaleRecip*paddingY);
1206 //System.out.println(" => " + r);
1210 protected Rectangle2D expandBounds(Rectangle2D r, double amount) {
1211 r.setRect(r.getX() - amount, r.getY() - amount, r.getWidth() + 2*amount, r.getHeight() + 2*amount);
1215 protected Rectangle2D expandBounds(Rectangle2D r, double left, double top, double right, double bottom) {
1216 r.setRect(r.getX() - left, r.getY() - top, r.getWidth() + left + right, r.getHeight() + top + bottom);
1220 private void resetCaches() {
1221 this.tightBoundsCache = null;
1223 this.fontMetrics = null;
1227 * Returns the tight bounds around the current text using the current font
1228 * in the specified rectangle. If the specified rectangle is
1229 * <code>null</code> a new Rectangle2D.Double instance will be created.
1234 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r) {
1235 return getTightAlignedBoundsInLocal(r, FRC);
1239 * Returns the tight bounds around the current text using the current font
1240 * in the specified rectangle. If the specified rectangle is
1241 * <code>null</code> a new Rectangle2D.Double instance will be created.
1244 * the rectangle where the result of the method is placed or
1245 * <code>null</code> to allocate new rectangle
1246 * @param frc current font render context
1247 * @return r or new Rectangle2D.Double instance containing the requested
1250 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r, FontRenderContext frc) {
1252 r = new Rectangle2D.Double();
1254 if (tightBoundsCache != null) {
1255 r.setFrame(tightBoundsCache);
1260 if (font == null || txt == null) {
1261 r.setFrame(0, 0, 2, 1);
1265 //System.out.println("TextNode.getTightAlignedBoundsInLocal('" + txt + "')");
1267 // Parse & layout (unaligned)
1268 Line[] lines = null;
1270 if(hasState(STATE_WRAP_TEXT)) {
1271 float width = fixedWidth;
1272 if(width <= 0 && targetBounds != null)
1273 width = (float) (((targetBounds.getWidth() - 2*paddingX)) * scaleRecip);
1275 lines = wrapLines(txt, font, width, frc);
1279 lines = parseLines(txt);
1280 this.lines = layoutLines(lines, frc);
1282 // Calculate tight bounds based on unaligned layout
1283 //System.out.println("Unaligned");
1284 tightBoundsCache = calculateBounds(lines, Line.BBOX, null);
1285 //System.out.println(" => " + tightBoundsCache);
1287 this.lines = layoutLinesX(lines, tightBoundsCache);
1288 // Align each line to the calculated tight bounds
1289 this.lines = alignLines(this.lines, tightBoundsCache, horizontalAlignment, verticalAlignment);
1291 // Calculate aligned bounds
1292 //System.out.println("Aligned");
1293 calculateBounds(lines, Line.ABBOX, tightBoundsCache);
1295 r.setFrame(tightBoundsCache);
1296 //System.out.println(" => " + tightBoundsCache);
1304 * the bounding box of all the whole laid out text (only bbox
1308 private Line[] layoutLinesX(Line[] lines, Rectangle2D bbox) {
1309 int lineCount = lines.length;
1310 for (int l = 0; l < lineCount; ++l) {
1311 Line line = lines[l];
1312 // Compute pen x position. If the paragraph is right-to-left we
1313 // will align the TextLayouts to the right edge of the panel.
1314 // Note: drawPosX is always where the LEFT of the text is placed.
1315 // NOTE: This changes based on horizontal alignment
1316 line.drawPosX = (float) (line.layout.isLeftToRight() ? 0f
1317 : tightBoundsCache.getWidth() - line.layout.getAdvance());
1324 * @param boundsProvider
1328 private static Rectangle2D calculateBounds(Line[] lines, BoundsProcedure boundsProvider, Rectangle2D result) {
1330 result = new Rectangle2D.Double();
1332 result.setFrame(0, 0, 0, 0);
1334 for (Line line : lines) {
1335 //System.out.println("line: " + line);
1336 Rectangle2D bbox = boundsProvider.getBounds(line);
1337 if (result.isEmpty())
1338 result.setFrame(bbox);
1340 Rectangle2D.union(result, bbox, result);
1341 //System.out.println("bounds: " + result);
1343 //System.out.println("final bounds: " + result);
1353 * @return aligned lines
1355 private Line[] alignLines(Line[] lines, Rectangle2D bbox, byte hAlign, byte vAlign) {
1356 // System.out.println("horizontal align: " + Alignment.values()[hAlign]);
1357 // System.out.println("vertical align : " + Alignment.values()[vAlign]);
1358 // System.out.println("bbox: " + bbox);
1360 // double ybase = 0;
1361 if(targetBounds != null) {
1362 /* In normal cases the bounding box moves when
1363 * typing. If target bounds are set, the text
1364 * is fitted into the box.
1368 xbase = (targetBounds.getMaxX()-paddingX) * scaleRecip;
1371 xbase = targetBounds.getCenterX() * scaleRecip;
1373 default: // Leading / Baseline
1380 for (Line line : lines) {
1386 xoffset = xbase - line.bbox.getWidth();
1389 xoffset = xbase - line.bbox.getWidth() / 2;
1391 default: // Leading / Baseline
1398 yoffset = line.layout.getAscent();
1401 yoffset = -bbox.getHeight() + line.layout.getAscent();
1404 yoffset = -bbox.getHeight() / 2 + line.layout.getAscent();
1408 line.alignOffset(xoffset, yoffset);
1418 private Line[] layoutLines(Line[] lines, FontRenderContext frc) {
1419 TextLayout emptyRowLayout = null;
1420 int lineCount = lines.length;
1422 for (int l = 0; l < lineCount; ++l) {
1423 Line line = lines[l];
1424 String lineText = line.getText();
1425 // " " because TextLayout requires non-empty text and
1426 // We don't want zero size for the text.
1427 if (lineText.isEmpty()) {
1429 if (emptyRowLayout == null)
1430 emptyRowLayout = new TextLayout(lineText, font, frc);
1431 line.layout = emptyRowLayout;
1433 line.layout = new TextLayout(lineText, font, frc);
1436 //y += line.layout.getAscent();
1438 y += line.layout.getDescent() + line.layout.getLeading() + line.layout.getAscent();
1440 Rectangle2D bbox = line.layout.getLogicalHighlightShape(0, lineText.length()).getBounds2D();
1441 // HighlightShape is not large enough, if font is italic.
1442 Rectangle2D bbox2 = line.layout.getBounds();
1444 bbox.setFrame(bbox.getX(), bbox.getY() + line.drawPosY, bbox.getWidth(), bbox.getHeight());
1452 * Splits the specified string into {@link Line} structures, one for each
1453 * line in the input text. The returned lines are only partially defined,
1454 * waiting to be laid out (see
1455 * {@link #layoutLines(Line[], FontRenderContext)})
1459 * @return parsed text lines as {@link Line} structures
1460 * @see #layoutLines(Line[], FontRenderContext)
1462 private static Line[] parseLines(String txt) {
1463 int len = txt.length();
1465 return new Line[] { new Line("", 0, 0) };
1467 TIntArrayList lfpos = new TIntArrayList();
1470 for (;pos < len; ++lineCount) {
1471 int nextlf = txt.indexOf('\n', pos);
1472 lfpos.add(nextlf != -1 ? nextlf : len);
1477 Line[] lines = new Line[lineCount];
1479 for (int i = 0; i < lineCount-1; ++i) {
1480 int lf = lfpos.getQuick(i);
1481 int cr = (lf > 0) && (txt.charAt(lf - 1) == '\r') ? lf - 1 : lf;
1482 lines[i] = new Line(txt, pos, cr);
1485 lines[lineCount - 1] = new Line(txt, pos, len);
1491 private static Line[] wrapLines(String txt, Font font, float fixedWidth, FontRenderContext frc) {
1492 if(txt == null || txt.isEmpty())
1495 ArrayList<Line> lines =
1496 new ArrayList<Line>();
1498 Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
1499 map.put(TextAttribute.FONT, font);
1500 AttributedString attributedText = new AttributedString(txt.isEmpty() ? "__ERROR__" : txt, map);
1502 AttributedCharacterIterator paragraph = attributedText.getIterator();
1503 int paragraphStart = paragraph.getBeginIndex();
1504 int paragraphEnd = paragraph.getEndIndex();
1505 LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
1507 float breakWidth = fixedWidth;
1509 // Force text to be vertical, by setting break width to 1, if the text area is narrower than "GGGG"
1511 // Set position to the index of the first character in the paragraph.
1512 lineMeasurer.setPosition(paragraphStart);
1514 // Get lines until the entire paragraph has been displayed.
1515 int next, limit, charat, position = 0;
1517 while ((position = lineMeasurer.getPosition()) < paragraphEnd) {
1519 // Find possible line break and set it as a limit to the next layout
1520 next = lineMeasurer.nextOffset(breakWidth);
1522 charat = txt.indexOf(System.getProperty("line.separator"),position+1);
1523 if(charat < next && charat != -1){
1527 lineMeasurer.nextLayout(breakWidth, limit, false);
1529 lines.add(new Line(txt, position, limit));
1532 return lines.toArray(new Line[lines.size()]);
1536 public String getClipboardContent() {
1537 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1538 Transferable clipData = clipboard.getContents(this);
1540 return (String) (clipData.getTransferData(DataFlavor.stringFlavor));
1541 } catch (Exception ee) {
1546 public void setClipboardContent(String content) {
1547 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1548 StringSelection data = new StringSelection(content);
1549 clipboard.setContents(data, data);
1553 public String toString() {
1554 return super.toString() + " [text=" + text + ", font=" + font + ", color=" + color + "]";
1558 protected boolean handleCommand(CommandEvent e) {
1559 if (!hasState(STATE_EDITING))
1562 if (Commands.SELECT_ALL.equals(e.command)) {
1570 protected boolean keyPressed(KeyPressedEvent event) {
1571 if (!hasState(STATE_EDITING))
1574 char c = event.character;
1575 boolean ctrl = event.isControlDown();
1576 boolean alt = event.isAltDown();
1578 // System.out.println("Key pressed '" + (event.character == 0 ? "\\0" : "" + c) + "' " + event.keyCode + " - " + Integer.toBinaryString(event.stateMask));
1579 // System.out.println("ctrl: " + ctrl);
1580 // System.out.println("alt: " + alt);
1582 switch (event.keyCode) {
1584 if (caret != selectionTail) {
1585 int selectionMin = Math.min(caret, selectionTail);
1586 int selectionMax = Math.max(caret, selectionTail);
1587 setClipboardContent(text.substring(selectionMin, selectionMax));
1592 if (caret != selectionTail) {
1593 int selectionMin = Math.min(caret, selectionTail);
1594 int selectionMax = Math.max(caret, selectionTail);
1595 setClipboardContent(text.substring(selectionMin, selectionMax));
1600 case KeyEvent.VK_RIGHT:
1602 // '\'' has the same keycode as VK_RIGHT but when right
1603 // arrow is pressed, event character is \0.
1604 moveCaretCtrlRight(event.isShiftDown());
1608 case KeyEvent.VK_LEFT:
1609 moveCaretCtrlLeft(event.isShiftDown());
1614 String content = getClipboardContent();
1620 // Replaced by #handleCommand
1621 // case KeyEvent.VK_A:
1627 case KeyEvent.VK_ENTER:
1629 insert(getLineSeparator());
1637 } else if (!ctrl && alt) {
1640 switch (event.keyCode) {
1641 case KeyEvent.VK_LEFT:
1642 moveCaret(-1, event.isShiftDown());
1644 case KeyEvent.VK_RIGHT:
1646 // '\'' has the same keycode as VK_RIGHT but when right
1647 // arrow is pressed, event character is \0.
1648 moveCaret(1, event.isShiftDown());
1651 // Intentional fallthrough to default case
1652 case KeyEvent.VK_UP:
1653 moveCaretRowUp(event.isShiftDown());
1655 case KeyEvent.VK_DOWN:
1656 moveCaretRowDown(event.isShiftDown());
1658 case KeyEvent.VK_HOME:
1659 moveCaretHome(event.isShiftDown());
1661 case KeyEvent.VK_END:
1662 moveCaretEnd(event.isShiftDown());
1665 case KeyEvent.VK_ENTER:
1666 fireTextEditingEnded();
1669 case KeyEvent.VK_ESCAPE:
1670 text = textBeforeEdit;
1672 clearState(STATE_EDITING);
1673 fireTextEditingCancelled();
1676 case KeyEvent.VK_BACK_SPACE:
1677 if(caret == selectionTail && caret > 0) {
1678 // line separator may use multiple characters, we want to remove that with one command
1679 String lineSep = getLineSeparator();
1680 int index = lineSep.indexOf(text.charAt(caret-1));
1685 selectionTail+= (lineSep.length()-index-1);
1691 case KeyEvent.VK_DELETE:
1692 if(caret == selectionTail && caret < text.length()) {
1693 String lineSep = getLineSeparator();
1694 int index = lineSep.indexOf(text.charAt(caret));
1698 selectionTail-= index;
1699 caret+= (lineSep.length()-index);
1708 if (c == 65535 || Character.getType(c) == Character.CONTROL) {
1711 //System.out.println("Char " + c + " " + Character.getType(c) + " " + text);
1712 insert(new String(new char[] {c}));
1716 // FIXME This is called even if just caret was moved.
1717 // This is currently necessary for repaints.
1719 invalidateXOffset();
1723 protected String getLineSeparator() {
1724 return System.getProperty("line.separator");
1727 protected void selectAll() {
1729 setCaret(text.length(), true);
1732 protected transient int hoverClick = 0;
1735 protected boolean mouseClicked(MouseClickEvent event) {
1736 if (event.button != MouseClickEvent.LEFT_BUTTON)
1739 if (hasState(STATE_HOVER)) {
1743 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1744 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1747 IElement e = DiagramNodeUtil.getElement(ctx, this);
1748 if (!hasState(STATE_EDITING)) {
1749 if (Boolean.TRUE.equals(setEditMode(true))) {
1750 editActivation = activateEdit(0, e, ctx);
1756 if (hasState(STATE_EDITING)) {
1757 fireTextEditingEnded();
1763 protected boolean mouseDoubleClicked(MouseDoubleClickedEvent event) {
1764 if (event.button != MouseClickEvent.LEFT_BUTTON)
1767 if (hitTest(event, 0)) {
1768 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1769 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1774 // Select the whole text.
1776 setCaret(text.length(), true);
1785 protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {
1786 if (!hasState(STATE_EDITING))
1789 Point2D local = controlToLocal( event.controlPosition );
1790 // FIXME: once the event coordinate systems are cleared up, remove this workaround
1791 local = parentToLocal(local);
1792 if (hasState(STATE_HOVER) && this.containsLocal(local)) {
1793 setCaret(local, event.isShiftDown());
1799 protected boolean mouseMoved(MouseMovedEvent event) {
1800 boolean hit = hitTest(event, 3.0);
1801 if (hit != hasState(STATE_HOVER)) {
1802 setState(STATE_HOVER, hit);
1808 private boolean isControlDown(MouseEvent e) {
1809 return e.isControlDown() || lastMouseEvent!=null?lastMouseEvent.isControlDown():false;
1812 protected boolean isShiftDown(MouseEvent e) {
1813 return e.isShiftDown() || lastMouseEvent!=null?lastMouseEvent.isShiftDown():false;
1816 // private boolean isAltDown(MouseEvent e) {
1817 // return e.isAltDown() || lastMouseEvent!=null?lastMouseEvent.isAltDown():false;
1821 protected boolean mouseDragged(MouseDragBegin e) {
1823 && (isControlDown(e) || isShiftDown(e))
1824 && (dataRVI != null || text != null))
1826 List<Transferable> trs = new ArrayList<>(2);
1827 if (dataRVI != null) {
1828 trs.add(new LocalObjectTransferable(dataRVI));
1829 trs.add(new PlaintextTransfer(dataRVI.toString()));
1830 } else if (text != null && !text.isEmpty()) {
1831 trs.add(new PlaintextTransfer(text));
1833 if (!trs.isEmpty()) {
1834 e.transferable = new MultiTransferable(trs);
1841 protected boolean hitTest(MouseEvent event, double tolerance) {
1842 Rectangle2D bounds = getBoundsInternal();
1845 Point2D localPos = NodeUtil.worldToLocal(this, event.controlPosition, new Point2D.Double());
1846 double x = localPos.getX();
1847 double y = localPos.getY();
1848 boolean hit = bounds.contains(x, y);
1852 public Rectangle2D getBoundsInternal() {
1853 Rectangle2D local = lastBounds;
1856 // TODO: potential spot for CPU/memory allocation optimization
1857 // by using more specialized implementations
1858 if (transform.isIdentity())
1860 return transform.createTransformedShape(local).getBounds2D();
1863 protected Color add(Color c, int r, int g, int b) {
1864 int nr = Math.min(255, c.getRed() + r);
1865 int ng = Math.min(255, c.getGreen() + g);
1866 int nb = Math.min(255, c.getBlue() + b);
1867 return new Color(nr, ng, nb);
1870 public TextEditActivation activateEdit(int mouseId, IElement e, ICanvasContext ctx) {
1871 EditDataNode data = EditDataNode.getNode(this);
1872 deactivateEdit(data, null);
1873 TextEditActivation result = new TextEditActivation(mouseId, e, ctx);
1874 data.setTextEditActivation(result);
1879 * @return <code>true</code> if this node is or was previously in editing
1882 protected boolean deactivateEdit() {
1883 boolean result = deactivateEdit( editActivation );
1884 result |= editActivation != null;
1885 editActivation = null;
1889 protected boolean deactivateEdit(TextEditActivation activation) {
1890 return deactivateEdit( EditDataNode.getNode(this), activation );
1893 protected boolean deactivateEdit(EditDataNode data, TextEditActivation activation) {
1894 TextEditActivation previous = data.getTextEditActivation();
1895 if (previous != null && (previous == activation || activation == null)) {
1897 data.setTextEditActivation(null);
1904 public int getEventMask() {
1905 return EventTypes.KeyPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask
1906 | EventTypes.MouseClickMask | EventTypes.MouseDragBeginMask | EventTypes.CommandMask;
1909 private MouseEvent lastMouseEvent = null;
1912 public boolean handleEvent(Event e) {
1913 if(e instanceof MouseEvent && !(e instanceof MouseDragBegin)) lastMouseEvent = (MouseEvent)e;
1914 return super.handleEvent(e);
1918 public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
1919 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
1923 public <T> T getProperty(String propertyName) {
1928 public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
1931 public void synchronizeText(String text) {
1935 public void synchronizeColor(RGB.Integer color) {
1936 this.color = Colors.awt(color);
1939 public void synchronizeFont(org.simantics.datatypes.literal.Font font) {
1940 setFont(Fonts.awt(font));
1943 public void synchronizeTransform(double[] data) {
1944 this.setTransform(new AffineTransform(data));
1947 public static void main(String[] args) {
1948 Line[] lines = parseLines("\n \n FOO \n\nBAR\n\n\n BAZ\n\n");
1949 System.out.println(Arrays.toString(lines));
1950 System.out.println(GeometryUtils.pointToMillimeter(1));
1951 System.out.println(GeometryUtils.pointToMillimeter(12));
1952 System.out.println(GeometryUtils.pointToMillimeter(72));
1955 ///////////////////////////////////////////////////////////////////////////
1956 // LEGACY CODE NEEDED BY INHERITED CLASSES FOR NOW
1957 ///////////////////////////////////////////////////////////////////////////
1959 protected double getHorizontalAlignOffset(Rectangle2D r) {
1960 switch (horizontalAlignment) {
1961 case 0: return 0; // Leading
1962 case 1: return -r.getWidth(); // Trailing
1963 case 2: return -r.getCenterX(); // Center
1968 protected double getVerticalAlignOffset() {
1969 FontMetrics fm = fontMetrics;
1972 switch (verticalAlignment) {
1973 case 0: return fm.getMaxAscent(); // Leading=top=maxascent
1974 case 1: return -fm.getMaxDescent(); // Trailing=bottom=maxdescent
1975 case 2: return fm.getMaxAscent() / 2; // Center=maxascent / 2
1981 ///////////////////////////////////////////////////////////////////////////
1983 ///////////////////////////////////////////////////////////////////////////