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.NodeEventHandler;
66 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
67 import org.simantics.scenegraph.g2d.events.command.Commands;
68 import org.simantics.scenegraph.utils.GeometryUtils;
69 import org.simantics.scenegraph.utils.NodeUtil;
70 import org.simantics.scl.runtime.function.Function1;
71 import org.simantics.scl.runtime.function.Function2;
72 import org.simantics.ui.colors.Colors;
73 import org.simantics.ui.dnd.LocalObjectTransferable;
74 import org.simantics.ui.dnd.MultiTransferable;
75 import org.simantics.ui.dnd.PlaintextTransfer;
76 import org.simantics.ui.fonts.Fonts;
77 import org.simantics.utils.threads.AWTThread;
79 import com.lowagie.text.pdf.PdfWriter;
81 import gnu.trove.list.array.TIntArrayList;
85 * TextNode which supports in-line editing.
87 * By default <code>TextNode</code> is in editable = false state. Use
88 * {@link #setEditable(boolean)} to make it editable.
90 * @author Hannu Niemistö <hannu.niemisto@vtt.fi>
91 * @author Marko Luukkainen <marko.luukkainen@vtt.fi>
92 * @author Tuukka Lehtonen <tuukka.lehtonen@semantum.fi>
95 * o proper support for defining clipping bounds for the text (needed for page templates) (currently through fixedWidth)
96 * o fix editing xOffset to work with fixed width and multi-line text
101 public class TextNode extends G2DNode implements IDynamicSelectionPainterNode, LoaderNode {
103 private static final long serialVersionUID = 654692698101485672L;
105 public static enum TextFlipping {
108 VerticalTextDownwards,
112 * TODO: justify existence for this
114 private static final BasicStroke RESET_STROKE = new BasicStroke(1);
117 * Src-over alpha composite instance with 50% opacity.
119 private static final AlphaComposite SrcOver_50 = AlphaComposite.SrcOver.derive(0.5f);
122 * For (inexact) measurement of rendered text bounds.
124 protected static final FontRenderContext FRC = new FontRenderContext(new AffineTransform(), true, true);
126 private static final Font FONT = Font.decode("Arial 6");
127 private static final Color SELECTION_BACKGROUND_COLOR = new Color(0x316ac5);
128 // private static final double MAX_CARET_POSITION = 1.0;
131 * The complete text visualized by this node.
133 protected String text = null;
136 * The font used to render the {@link #text}.
138 protected Font font = FONT;
141 * The color of the rendered text. Default value is {@value Color#black}.
143 protected Color color = Color.BLACK;
146 * The background color used for filling the background of the bounding box
147 * of the rendered text. <code>null</code> means no fill.
148 * Default value is <code>null</code>.
150 protected Color backgroundColor = null;
153 * The color used for drawing the expanded bounding box border for the
154 * rendered text. <code>null</code> means no border is rendered. Default
155 * value is <code>null</code>.
157 protected Color borderColor = null;
159 protected double scale = 1.0;
160 protected transient double scaleRecip = 1.0;
165 protected float borderWidth = 0.f;
167 protected double paddingX = 2.0;
168 protected double paddingY = 2.0;
171 * Horizontal text box alignment with respect to its origin. Default value is
174 protected byte horizontalAlignment = 0;
176 * Vertical text box alignment with respect to its origin. Default value is
179 protected byte verticalAlignment = 3;
182 * Tells if this node is still pending for real results or not.
184 protected static final int STATE_PENDING = (1 << 0);
185 protected static final int STATE_HOVER = (1 << 1);
186 protected static final int STATE_EDITABLE = (1 << 2);
187 protected static final int STATE_SHOW_SELECTION = (1 << 3);
188 protected static final int STATE_WRAP_TEXT = (1 << 4);
189 protected transient static final int STATE_EDITING = (1 << 5);
190 protected transient static final int STATE_VALID = (1 << 6);
191 protected transient static final int STATE_X_OFFSET_IS_DIRTY = (1 << 7);
192 protected static final int STATE_ALWAYS_ADD_LISTENERS = (1 << 8);
193 protected static final int STATE_LISTENERS_ADDED = (1 << 9);
194 protected static final int STATE_AUTOMATIC_TEXT_FLIP_ENABLED = (1 << 10);
195 protected static final int STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN = (1 << 11);
198 * A combination of all the STATE_ constants defined in this class,
199 * e.g. {@link #STATE_PENDING}.
201 protected int state = STATE_SHOW_SELECTION | STATE_WRAP_TEXT | STATE_VALID | STATE_X_OFFSET_IS_DIRTY;
203 protected RVI dataRVI = null;
206 int selectionTail = 0;
209 float fixedWidth = 0f;
211 private Rectangle2D targetBounds;
213 Function1<String, String> validator;
214 ITextListener textListener;
215 ITextContentFilter editContentFilter;
218 * The renderable line structures parsed from {@link #text} by
219 * {@link #parseLines(String)}, laid out by
220 * {@link #layoutLines(Line[], FontRenderContext)} and aligned by
221 * {@link #alignLines(Line[], Rectangle2D, byte, byte)}
223 protected transient Line[] lines = null;
224 protected transient FontMetrics fontMetrics = null;
227 * Stores the value of {@link #text} before edit mode was last entered. Used
228 * for restoring the original value if editing is cancelled.
230 private transient String textBeforeEdit = null;
231 protected transient TextEditActivation editActivation;
234 * Stores the last scaled bounds.
236 private transient Rectangle2D lastBounds = new Rectangle2D.Double();
239 * This must be nullified if anything that affects the result of
240 * {@link #getTightAlignedBoundsInLocal(Rectangle2D, FontRenderContext)}
241 * changes. It will cause the cached value to be recalculated on the next
244 private transient Rectangle2D tightBoundsCache = null;
249 // Mark this node as pending
250 NodeUtil.increasePending(this);
254 public void cleanup() {
259 protected boolean hasState(int flags) {
260 return (state & flags) == flags;
263 protected void setState(int flags) {
267 protected void setState(int flags, boolean set) {
271 this.state &= ~flags;
274 protected void clearState(int flags) {
275 this.state &= ~flags;
278 protected void setListeners(boolean add) {
285 protected void addListeners() {
286 if (!hasState(STATE_LISTENERS_ADDED)) {
287 addEventHandler(this);
288 setState(STATE_LISTENERS_ADDED);
292 protected void removeListeners() {
293 if (hasState(STATE_LISTENERS_ADDED)) {
294 removeEventHandler(this);
295 clearState(STATE_LISTENERS_ADDED);
300 * 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
303 public void setForceEventListening(boolean force) {
304 setState(STATE_ALWAYS_ADD_LISTENERS, force);
305 if (force && !hasState(STATE_EDITABLE)) {
311 * Enables or disables edit mode. It also sets
312 * the caret at the end of text all selects the
313 * whole text (this is the usual convention when
314 * beginning to edit one line texts).
316 * @return null if no change to edit state was made
318 public Boolean setEditMode(boolean edit) {
319 return setEditMode(edit, true);
323 * Enables or disables edit mode. It also sets
324 * the caret at the end of text all selects the
325 * whole text (this is the usual convention when
326 * beginning to edit one line texts).
328 * @return null if no change to edit state was made
330 protected Boolean setEditMode(boolean edit, boolean notify) {
331 if (edit && !hasState(STATE_EDITABLE))
333 if (hasState(STATE_EDITING) == edit)
335 setState(STATE_EDITING, edit);
337 caret = text != null ? text.length() : 0;
339 textBeforeEdit = text;
341 fireTextEditingStarted();
345 fireTextEditingEnded();
346 return Boolean.FALSE;
350 @SyncField({"editable"})
351 public void setEditable(boolean editable) {
352 boolean changed = hasState(STATE_EDITABLE) != editable;
353 setState(STATE_EDITABLE, editable);
354 if (hasState(STATE_EDITING) && !editable)
356 if (changed && !hasState(STATE_ALWAYS_ADD_LISTENERS)) {
357 setListeners(editable);
361 public boolean isEditable() {
362 return hasState(STATE_EDITABLE);
365 public boolean isEditMode() {
366 return hasState(STATE_EDITING);
369 @SyncField({"wrapText"})
370 public void setWrapText(boolean wrapText) {
371 setState(STATE_WRAP_TEXT, wrapText);
375 * @return Does the text box wrap text if
376 * the width of the box is fixed
378 public boolean isWrapText() {
379 return hasState(STATE_WRAP_TEXT);
382 @SyncField({"showSelection"})
383 public void setShowSelection(boolean showSelection) {
384 setState(STATE_SHOW_SELECTION, showSelection);
387 public boolean showsSelection() {
388 return hasState(STATE_SHOW_SELECTION);
395 * @param x not supported anymore, use {@link #setTransform(AffineTransform)} instead
396 * @param y not supported anymore, use {@link #setTransform(AffineTransform)} instead
399 @SyncField({"text", "font", "color", "x", "y", "scale"})
400 public void init(String text, Font font, Color color, double x, double y, double scale) {
402 if(this.text == null && text != null) NodeUtil.decreasePending(this);
404 if (hasState(STATE_EDITING))
407 this.text = new String(text != null ? text : "");
411 this.scaleRecip = 1.0 / scale;
413 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);
473 this.text = text != null ? text : "";
474 caret = Math.min(caret, this.text.length());
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) {
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) {
862 * Replaces the current selection with the content or inserts
863 * the content at caret. After the insertion the caret
864 * will be at the end of inserted text and selection will
868 @SyncField({"text","caret","selectionTail"})
869 protected void insert(String content) {
870 content = editContentFilter != null ? editContentFilter.filter(this, content) : content;
872 int selectionMin = Math.min(caret, selectionTail);
873 int selectionMax = Math.max(caret, selectionTail);
875 String begin = text.substring(0, selectionMin);
876 String end = text.substring(selectionMax);
877 text = begin + content + end;
878 caret = selectionMin + content.length();
879 selectionTail = caret;
881 assert (caret <= text.length());
882 //System.out.println(text + " " + caret );
884 if(validator != null) {
885 String error = validator.apply(text);
886 setState(STATE_VALID, (error == null));
893 protected void fireTextChanged() {
894 if(textListener != null)
895 textListener.textChanged();
900 protected void fireTextEditingStarted() {
901 if(textListener != null)
902 textListener.textEditingStarted();
906 protected void fireTextEditingCancelled() {
907 setState(STATE_VALID);
909 if (deactivateEdit()) {
910 if (textListener != null)
911 textListener.textEditingCancelled();
913 setEditMode(false, false);
915 if (textBeforeEdit != null)
916 setText(textBeforeEdit);
923 public void fireTextEditingEnded() {
924 if (!hasState(STATE_VALID)) {
925 fireTextEditingCancelled();
926 setState(STATE_VALID);
930 if (deactivateEdit()) {
931 if (textListener != null)
932 textListener.textEditingEnded();
934 setEditMode(false, false);
939 public void setTextListener(ITextListener listener) {
940 this.textListener = listener;
943 public void setValidator(Function1<String, String> validator) {
944 this.validator = validator;
947 public void setContentFilter(ITextContentFilter filter) {
948 this.editContentFilter = filter;
951 public void setRVI(RVI rvi) {
955 private void invalidateXOffset() {
956 setState(STATE_X_OFFSET_IS_DIRTY);
959 private void computeEditingXOffset() {
961 if(lines == null) return;
962 if(!hasState(STATE_X_OFFSET_IS_DIRTY)) return;
963 if(fixedWidth > 0f) {
966 // float[] coords = textLayout.getCaretInfo(TextHitInfo.afterOffset(caret));
967 // if(coords != null) {
968 // if(coords[0] > (fixedWidth*MAX_CARET_POSITION)) xOffset = (float)((fixedWidth*MAX_CARET_POSITION)-coords[0]);
978 clearState(STATE_X_OFFSET_IS_DIRTY);
982 @SyncField({"caret","selectionTail"})
983 protected void moveCaret(int move, boolean select) {
984 // prevent setting caret into line separator.
986 while (text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
988 } else if (move < 0) {
989 while (caret+move >= 0 && text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
995 if (caret > text.length())
996 caret = text.length();
998 selectionTail = caret;
1001 private Line findCaretLine() {
1002 // Find the line where caret is. Starting from first line.
1003 for(int i = 0; i < lines.length; i++) {
1004 Line line = lines[i];
1005 if(caret <= line.endOffset) {
1013 * Moves caret to next not letter or digit
1016 private void moveCaretCtrlLeft(boolean shiftDown) {
1017 Line line = findCaretLine();
1020 for(i = caret-1; i > line.startOffset; i--) {
1021 char c = line.document.charAt(i);
1022 if(!Character.isLetterOrDigit(c)) {
1026 moveCaret(i - caret, shiftDown);
1031 * Moves caret to previous non letter or digit
1034 private void moveCaretCtrlRight(boolean shiftDown) {
1035 Line line = findCaretLine();
1038 for(i = caret + 1; i < line.endOffset; i++) {
1039 char c = line.document.charAt(i);
1040 if(!Character.isLetterOrDigit(c)) {
1044 moveCaret(i - caret, shiftDown);
1049 * Moves caret to line end
1052 private void moveCaretEnd(boolean shiftDown) {
1053 Line line = findCaretLine();
1055 // Move caret to the end of the line
1056 moveCaret(line.endOffset - caret, shiftDown);
1060 * Moves caret to beginning of a line
1063 private void moveCaretHome(boolean shiftDown) {
1064 Line line = findCaretLine();
1066 // Move caret to the beginning of the line
1067 moveCaret(line.startOffset - caret, shiftDown);
1071 * Moves caret one row up and tries to maintain the location
1074 private void moveCaretRowUp(boolean shiftDown) {
1075 // Find the line where caret is. Starting from first line.
1076 for(int i = 0; i < lines.length; i++) {
1077 Line line = lines[i];
1078 if(caret <= line.endOffset) {
1079 // caret is in this line
1081 // Already on top line
1082 // Select the beginning of the line
1083 moveCaret(-caret, shiftDown);
1085 Line prevLine = lines[i-1];
1086 int prevLength = prevLine.endOffset - prevLine.startOffset;
1087 int posInCurRow = caret - line.startOffset;
1088 if(prevLength < posInCurRow)
1089 posInCurRow = prevLength;
1091 int newPos = prevLine.startOffset + posInCurRow;
1092 moveCaret(newPos - caret, shiftDown);
1100 * Moves caret one row down and tries to maintain the location
1103 private void moveCaretRowDown(boolean shiftDown) {
1104 // Find the line where caret is. Starting from last line.
1105 for(int i = lines.length - 1; i >= 0; i--) {
1106 Line line = lines[i];
1107 if(caret >= line.startOffset) {
1108 // caret is in this line
1109 if(i == lines.length - 1) {
1110 // Already on bottom line, cannot go below
1111 // Select to the end of the line
1112 moveCaret(line.endOffset - caret, shiftDown);
1114 Line prevLine = lines[i+1]; // Previous line
1116 // Find new caret position.
1117 // Either it is in the same index as before, or if the row
1118 // is not long enough, select the end of the row.
1119 int prevLength = prevLine.endOffset - prevLine.startOffset;
1120 int posInCurRow = caret - line.startOffset;
1121 if(prevLength < posInCurRow)
1122 posInCurRow = prevLength;
1123 int newPos = prevLine.startOffset + posInCurRow;
1124 moveCaret(newPos - caret, shiftDown);
1131 @SyncField({"caret","selectionTail"})
1132 protected void setCaret(int pos, boolean select) {
1136 if (caret > text.length())
1137 caret = text.length();
1139 selectionTail = caret;
1142 protected void setCaret(Point2D point) {
1143 setCaret(point, false);
1146 @SyncField({"caret","selectionTail"})
1147 protected void setCaret(Point2D point, boolean select) {
1149 for(int i = 0; i < lines.length; i++) {
1150 Line line = lines[i];
1151 Rectangle2D bounds = line.abbox;
1152 // Add heights of bboxes for determining the correct line
1154 lineY = bounds.getY();
1156 lineY += lines[i-1].abbox.getHeight();
1158 double lineHeight = bounds.getHeight();
1159 double hitY = point.getY() / scale;
1160 if(hitY >= lineY && hitY <= lineY + lineHeight) {
1161 // Hit is in this line
1162 float x = (float)(point.getX() / scale) - (float)line.abbox.getX();
1163 float y = (float)(point.getY() / scale - lineHeight * i) ;
1164 TextHitInfo info = line.layout.hitTestChar(x, y);
1165 caret = line.startOffset + info.getInsertionIndex();
1166 if (caret > line.endOffset)
1167 caret = line.endOffset;
1169 selectionTail = caret;
1174 invalidateXOffset();
1175 assert (caret <= text.length());
1179 public Rectangle2D getBoundsInLocal() {
1180 if(targetBounds != null)
1181 return targetBounds;
1183 return expandBounds( getTightAlignedBoundsInLocal(null) );
1186 protected Rectangle2D expandBounds(Rectangle2D r) {
1187 r.setRect(r.getX() * scale - paddingX, r.getY() * scale -paddingY, r.getWidth()*scale + paddingX + paddingX, r.getHeight()*scale + paddingY + paddingY);
1188 //System.out.println(" => " + r);
1192 protected Rectangle2D expandBoundsUnscaled(Rectangle2D r) {
1193 r.setRect(r.getX() - scaleRecip*paddingX, r.getY() -scaleRecip*paddingY, r.getWidth() + scaleRecip*paddingX + scaleRecip*paddingX, r.getHeight() + scaleRecip*paddingY + scaleRecip*paddingY);
1194 //System.out.println(" => " + r);
1198 protected Rectangle2D expandBounds(Rectangle2D r, double amount) {
1199 r.setRect(r.getX() - amount, r.getY() - amount, r.getWidth() + 2*amount, r.getHeight() + 2*amount);
1203 protected Rectangle2D expandBounds(Rectangle2D r, double left, double top, double right, double bottom) {
1204 r.setRect(r.getX() - left, r.getY() - top, r.getWidth() + left + right, r.getHeight() + top + bottom);
1208 private void resetCaches() {
1209 this.tightBoundsCache = null;
1211 this.fontMetrics = null;
1215 * Returns the tight bounds around the current text using the current font
1216 * in the specified rectangle. If the specified rectangle is
1217 * <code>null</code> a new Rectangle2D.Double instance will be created.
1222 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r) {
1223 return getTightAlignedBoundsInLocal(r, FRC);
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.
1232 * the rectangle where the result of the method is placed or
1233 * <code>null</code> to allocate new rectangle
1234 * @param frc current font render context
1235 * @return r or new Rectangle2D.Double instance containing the requested
1238 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r, FontRenderContext frc) {
1240 r = new Rectangle2D.Double();
1242 if (tightBoundsCache != null) {
1243 r.setFrame(tightBoundsCache);
1248 if (font == null || txt == null) {
1249 r.setFrame(0, 0, 2, 1);
1253 //System.out.println("TextNode.getTightAlignedBoundsInLocal('" + txt + "')");
1255 // Parse & layout (unaligned)
1256 Line[] lines = null;
1258 if(hasState(STATE_WRAP_TEXT)) {
1259 float width = fixedWidth;
1260 if(width <= 0 && targetBounds != null)
1261 width = (float) (((targetBounds.getWidth() - 2*paddingX)) * scaleRecip);
1263 lines = wrapLines(txt, font, width, frc);
1267 lines = parseLines(txt);
1268 this.lines = layoutLines(lines, frc);
1270 // Calculate tight bounds based on unaligned layout
1271 //System.out.println("Unaligned");
1272 tightBoundsCache = calculateBounds(lines, Line.BBOX, null);
1273 //System.out.println(" => " + tightBoundsCache);
1275 this.lines = layoutLinesX(lines, tightBoundsCache);
1276 // Align each line to the calculated tight bounds
1277 this.lines = alignLines(this.lines, tightBoundsCache, horizontalAlignment, verticalAlignment);
1279 // Calculate aligned bounds
1280 //System.out.println("Aligned");
1281 calculateBounds(lines, Line.ABBOX, tightBoundsCache);
1283 r.setFrame(tightBoundsCache);
1284 //System.out.println(" => " + tightBoundsCache);
1292 * the bounding box of all the whole laid out text (only bbox
1296 private Line[] layoutLinesX(Line[] lines, Rectangle2D bbox) {
1297 int lineCount = lines.length;
1298 for (int l = 0; l < lineCount; ++l) {
1299 Line line = lines[l];
1300 // Compute pen x position. If the paragraph is right-to-left we
1301 // will align the TextLayouts to the right edge of the panel.
1302 // Note: drawPosX is always where the LEFT of the text is placed.
1303 // NOTE: This changes based on horizontal alignment
1304 line.drawPosX = (float) (line.layout.isLeftToRight() ? 0f
1305 : tightBoundsCache.getWidth() - line.layout.getAdvance());
1312 * @param boundsProvider
1316 private static Rectangle2D calculateBounds(Line[] lines, BoundsProcedure boundsProvider, Rectangle2D result) {
1318 result = new Rectangle2D.Double();
1320 result.setFrame(0, 0, 0, 0);
1322 for (Line line : lines) {
1323 //System.out.println("line: " + line);
1324 Rectangle2D bbox = boundsProvider.getBounds(line);
1325 if (result.isEmpty())
1326 result.setFrame(bbox);
1328 Rectangle2D.union(result, bbox, result);
1329 //System.out.println("bounds: " + result);
1331 //System.out.println("final bounds: " + result);
1341 * @return aligned lines
1343 private Line[] alignLines(Line[] lines, Rectangle2D bbox, byte hAlign, byte vAlign) {
1344 // System.out.println("horizontal align: " + Alignment.values()[hAlign]);
1345 // System.out.println("vertical align : " + Alignment.values()[vAlign]);
1346 // System.out.println("bbox: " + bbox);
1348 // double ybase = 0;
1349 if(targetBounds != null) {
1350 /* In normal cases the bounding box moves when
1351 * typing. If target bounds are set, the text
1352 * is fitted into the box.
1356 xbase = (targetBounds.getMaxX()-paddingX) * scaleRecip;
1359 xbase = targetBounds.getCenterX() * scaleRecip;
1361 default: // Leading / Baseline
1368 for (Line line : lines) {
1374 xoffset = xbase - line.bbox.getWidth();
1377 xoffset = xbase - line.bbox.getWidth() / 2;
1379 default: // Leading / Baseline
1386 yoffset = line.layout.getAscent();
1389 yoffset = -bbox.getHeight() + line.layout.getAscent();
1392 yoffset = -bbox.getHeight() / 2 + line.layout.getAscent();
1396 line.alignOffset(xoffset, yoffset);
1406 private Line[] layoutLines(Line[] lines, FontRenderContext frc) {
1407 TextLayout emptyRowLayout = null;
1408 int lineCount = lines.length;
1410 for (int l = 0; l < lineCount; ++l) {
1411 Line line = lines[l];
1412 String lineText = line.getText();
1413 // " " because TextLayout requires non-empty text and
1414 // We don't want zero size for the text.
1415 if (lineText.isEmpty()) {
1417 if (emptyRowLayout == null)
1418 emptyRowLayout = new TextLayout(lineText, font, frc);
1419 line.layout = emptyRowLayout;
1421 line.layout = new TextLayout(lineText, font, frc);
1424 //y += line.layout.getAscent();
1426 y += line.layout.getDescent() + line.layout.getLeading() + line.layout.getAscent();
1428 Rectangle2D bbox = line.layout.getLogicalHighlightShape(0, lineText.length()).getBounds2D();
1429 bbox.setFrame(bbox.getX(), bbox.getY() + line.drawPosY, bbox.getWidth(), bbox.getHeight());
1437 * Splits the specified string into {@link Line} structures, one for each
1438 * line in the input text. The returned lines are only partially defined,
1439 * waiting to be laid out (see
1440 * {@link #layoutLines(Line[], FontRenderContext)})
1444 * @return parsed text lines as {@link Line} structures
1445 * @see #layoutLines(Line[], FontRenderContext)
1447 private static Line[] parseLines(String txt) {
1448 int len = txt.length();
1450 return new Line[] { new Line("", 0, 0) };
1452 TIntArrayList lfpos = new TIntArrayList();
1455 for (;pos < len; ++lineCount) {
1456 int nextlf = txt.indexOf('\n', pos);
1457 lfpos.add(nextlf != -1 ? nextlf : len);
1462 Line[] lines = new Line[lineCount];
1464 for (int i = 0; i < lineCount-1; ++i) {
1465 int lf = lfpos.getQuick(i);
1466 int cr = (lf > 0) && (txt.charAt(lf - 1) == '\r') ? lf - 1 : lf;
1467 lines[i] = new Line(txt, pos, cr);
1470 lines[lineCount - 1] = new Line(txt, pos, len);
1476 private static Line[] wrapLines(String txt, Font font, float fixedWidth, FontRenderContext frc) {
1477 if(txt == null || txt.isEmpty())
1480 ArrayList<Line> lines =
1481 new ArrayList<Line>();
1483 Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
1484 map.put(TextAttribute.FONT, font);
1485 AttributedString attributedText = new AttributedString(txt.isEmpty() ? "__ERROR__" : txt, map);
1487 AttributedCharacterIterator paragraph = attributedText.getIterator();
1488 int paragraphStart = paragraph.getBeginIndex();
1489 int paragraphEnd = paragraph.getEndIndex();
1490 LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
1492 float breakWidth = fixedWidth;
1494 // Force text to be vertical, by setting break width to 1, if the text area is narrower than "GGGG"
1496 // Set position to the index of the first character in the paragraph.
1497 lineMeasurer.setPosition(paragraphStart);
1499 // Get lines until the entire paragraph has been displayed.
1500 int next, limit, charat, position = 0;
1502 while ((position = lineMeasurer.getPosition()) < paragraphEnd) {
1504 // Find possible line break and set it as a limit to the next layout
1505 next = lineMeasurer.nextOffset(breakWidth);
1507 charat = txt.indexOf(System.getProperty("line.separator"),position+1);
1508 if(charat < next && charat != -1){
1512 lineMeasurer.nextLayout(breakWidth, limit, false);
1514 lines.add(new Line(txt, position, limit));
1517 return lines.toArray(new Line[lines.size()]);
1521 public String getClipboardContent() {
1522 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1523 Transferable clipData = clipboard.getContents(this);
1525 return (String) (clipData.getTransferData(DataFlavor.stringFlavor));
1526 } catch (Exception ee) {
1531 public void setClipboardContent(String content) {
1532 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1533 StringSelection data = new StringSelection(content);
1534 clipboard.setContents(data, data);
1538 public String toString() {
1539 return super.toString() + " [text=" + text + ", font=" + font + ", color=" + color + "]";
1543 protected boolean handleCommand(CommandEvent e) {
1544 if (!hasState(STATE_EDITING))
1547 if (Commands.SELECT_ALL.equals(e.command)) {
1555 protected boolean keyPressed(KeyPressedEvent event) {
1556 if (!hasState(STATE_EDITING))
1559 char c = event.character;
1560 boolean ctrl = event.isControlDown();
1561 boolean alt = event.isAltDown();
1563 // System.out.println("Key pressed '" + (event.character == 0 ? "\\0" : "" + c) + "' " + event.keyCode + " - " + Integer.toBinaryString(event.stateMask));
1564 // System.out.println("ctrl: " + ctrl);
1565 // System.out.println("alt: " + alt);
1567 switch (event.keyCode) {
1569 if (caret != selectionTail) {
1570 int selectionMin = Math.min(caret, selectionTail);
1571 int selectionMax = Math.max(caret, selectionTail);
1572 setClipboardContent(text.substring(selectionMin, selectionMax));
1577 if (caret != selectionTail) {
1578 int selectionMin = Math.min(caret, selectionTail);
1579 int selectionMax = Math.max(caret, selectionTail);
1580 setClipboardContent(text.substring(selectionMin, selectionMax));
1585 case KeyEvent.VK_RIGHT:
1587 // '\'' has the same keycode as VK_RIGHT but when right
1588 // arrow is pressed, event character is \0.
1589 moveCaretCtrlRight(event.isShiftDown());
1593 case KeyEvent.VK_LEFT:
1594 moveCaretCtrlLeft(event.isShiftDown());
1599 String content = getClipboardContent();
1605 // Replaced by #handleCommand
1606 // case KeyEvent.VK_A:
1612 case KeyEvent.VK_ENTER:
1614 insert(getLineSeparator());
1622 } else if (!ctrl && alt) {
1625 switch (event.keyCode) {
1626 case KeyEvent.VK_LEFT:
1627 moveCaret(-1, event.isShiftDown());
1629 case KeyEvent.VK_RIGHT:
1631 // '\'' has the same keycode as VK_RIGHT but when right
1632 // arrow is pressed, event character is \0.
1633 moveCaret(1, event.isShiftDown());
1636 // Intentional fallthrough to default case
1637 case KeyEvent.VK_UP:
1638 moveCaretRowUp(event.isShiftDown());
1640 case KeyEvent.VK_DOWN:
1641 moveCaretRowDown(event.isShiftDown());
1643 case KeyEvent.VK_HOME:
1644 moveCaretHome(event.isShiftDown());
1646 case KeyEvent.VK_END:
1647 moveCaretEnd(event.isShiftDown());
1650 case KeyEvent.VK_ENTER:
1651 fireTextEditingEnded();
1654 case KeyEvent.VK_ESCAPE:
1655 text = textBeforeEdit;
1657 clearState(STATE_EDITING);
1658 fireTextEditingCancelled();
1661 case KeyEvent.VK_BACK_SPACE:
1662 if(caret == selectionTail && caret > 0) {
1663 // line separator may use multiple characters, we want to remove that with one command
1664 String lineSep = getLineSeparator();
1665 int index = lineSep.indexOf(text.charAt(caret-1));
1670 selectionTail+= (lineSep.length()-index-1);
1676 case KeyEvent.VK_DELETE:
1677 if(caret == selectionTail && caret < text.length()) {
1678 String lineSep = getLineSeparator();
1679 int index = lineSep.indexOf(text.charAt(caret));
1683 selectionTail-= index;
1684 caret+= (lineSep.length()-index);
1693 if (c == 65535 || Character.getType(c) == Character.CONTROL) {
1696 //System.out.println("Char " + c + " " + Character.getType(c) + " " + text);
1697 insert(new String(new char[] {c}));
1701 // FIXME This is called even if just caret was moved.
1702 // This is currently necessary for repaints.
1704 invalidateXOffset();
1708 protected String getLineSeparator() {
1709 return System.getProperty("line.separator");
1712 protected void selectAll() {
1714 setCaret(text.length(), true);
1717 protected transient int hoverClick = 0;
1720 protected boolean mouseClicked(MouseClickEvent event) {
1721 if (event.button != MouseClickEvent.LEFT_BUTTON)
1724 if (hasState(STATE_HOVER)) {
1728 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1729 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1732 IElement e = DiagramNodeUtil.getElement(ctx, this);
1733 if (!hasState(STATE_EDITING)) {
1734 if (Boolean.TRUE.equals(setEditMode(true))) {
1735 editActivation = activateEdit(0, e, ctx);
1741 if (hasState(STATE_EDITING)) {
1742 fireTextEditingEnded();
1748 protected boolean mouseDoubleClicked(MouseDoubleClickedEvent event) {
1749 if (event.button != MouseClickEvent.LEFT_BUTTON)
1752 if (hitTest(event, 0)) {
1753 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1754 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1759 // Select the whole text.
1761 setCaret(text.length(), true);
1770 protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {
1771 if (!hasState(STATE_EDITING))
1774 Point2D local = controlToLocal( event.controlPosition );
1775 // FIXME: once the event coordinate systems are cleared up, remove this workaround
1776 local = parentToLocal(local);
1777 if (hasState(STATE_HOVER) && this.containsLocal(local)) {
1778 setCaret(local, event.isShiftDown());
1784 protected boolean mouseMoved(MouseMovedEvent event) {
1785 boolean hit = hitTest(event, 3.0);
1786 if (hit != hasState(STATE_HOVER)) {
1787 setState(STATE_HOVER, hit);
1793 private boolean isControlDown(MouseEvent e) {
1794 return e.isControlDown() || lastMouseEvent!=null?lastMouseEvent.isControlDown():false;
1797 protected boolean isShiftDown(MouseEvent e) {
1798 return e.isShiftDown() || lastMouseEvent!=null?lastMouseEvent.isShiftDown():false;
1801 // private boolean isAltDown(MouseEvent e) {
1802 // return e.isAltDown() || lastMouseEvent!=null?lastMouseEvent.isAltDown():false;
1806 protected boolean mouseDragged(MouseDragBegin e) {
1808 && (isControlDown(e) || isShiftDown(e))
1809 && e.context instanceof NodeEventHandler
1810 && (dataRVI != null || text != null))
1812 List<Transferable> trs = new ArrayList<>(2);
1813 if (dataRVI != null) {
1814 trs.add(new LocalObjectTransferable(dataRVI));
1815 trs.add(new PlaintextTransfer(dataRVI.toString()));
1816 } else if (text != null && !text.isEmpty()) {
1817 trs.add(new PlaintextTransfer(text));
1819 if (!trs.isEmpty()) {
1820 e.transferable = new MultiTransferable(trs);
1827 protected boolean hitTest(MouseEvent event, double tolerance) {
1828 Rectangle2D bounds = getBoundsInternal();
1831 Point2D localPos = NodeUtil.worldToLocal(this, event.controlPosition, new Point2D.Double());
1832 double x = localPos.getX();
1833 double y = localPos.getY();
1834 boolean hit = bounds.contains(x, y);
1838 public Rectangle2D getBoundsInternal() {
1839 Rectangle2D local = lastBounds;
1842 // TODO: potential spot for CPU/memory allocation optimization
1843 // by using more specialized implementations
1844 if (transform.isIdentity())
1846 return transform.createTransformedShape(local).getBounds2D();
1849 protected Color add(Color c, int r, int g, int b) {
1850 int nr = Math.min(255, c.getRed() + r);
1851 int ng = Math.min(255, c.getGreen() + g);
1852 int nb = Math.min(255, c.getBlue() + b);
1853 return new Color(nr, ng, nb);
1856 public TextEditActivation activateEdit(int mouseId, IElement e, ICanvasContext ctx) {
1857 EditDataNode data = EditDataNode.getNode(this);
1858 deactivateEdit(data, null);
1859 TextEditActivation result = new TextEditActivation(mouseId, e, ctx);
1860 data.setTextEditActivation(result);
1865 * @return <code>true</code> if this node is or was previously in editing
1868 protected boolean deactivateEdit() {
1869 boolean result = deactivateEdit( editActivation );
1870 result |= editActivation != null;
1871 editActivation = null;
1875 protected boolean deactivateEdit(TextEditActivation activation) {
1876 return deactivateEdit( EditDataNode.getNode(this), activation );
1879 protected boolean deactivateEdit(EditDataNode data, TextEditActivation activation) {
1880 TextEditActivation previous = data.getTextEditActivation();
1881 if (previous != null && (previous == activation || activation == null)) {
1883 data.setTextEditActivation(null);
1890 public int getEventMask() {
1891 return EventTypes.KeyPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask
1892 | EventTypes.MouseClickMask | EventTypes.MouseDragBeginMask | EventTypes.CommandMask;
1895 private MouseEvent lastMouseEvent = null;
1898 public boolean handleEvent(Event e) {
1899 if(e instanceof MouseEvent && !(e instanceof MouseDragBegin)) lastMouseEvent = (MouseEvent)e;
1900 return super.handleEvent(e);
1904 public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
1905 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
1909 public <T> T getProperty(String propertyName) {
1914 public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
1917 public void synchronizeText(String text) {
1921 public void synchronizeColor(RGB.Integer color) {
1922 this.color = Colors.awt(color);
1925 public void synchronizeFont(org.simantics.datatypes.literal.Font font) {
1926 setFont(Fonts.awt(font));
1929 public void synchronizeTransform(double[] data) {
1930 this.setTransform(new AffineTransform(data));
1933 public static void main(String[] args) {
1934 Line[] lines = parseLines("\n \n FOO \n\nBAR\n\n\n BAZ\n\n");
1935 System.out.println(Arrays.toString(lines));
1936 System.out.println(GeometryUtils.pointToMillimeter(1));
1937 System.out.println(GeometryUtils.pointToMillimeter(12));
1938 System.out.println(GeometryUtils.pointToMillimeter(72));
1941 ///////////////////////////////////////////////////////////////////////////
1942 // LEGACY CODE NEEDED BY INHERITED CLASSES FOR NOW
1943 ///////////////////////////////////////////////////////////////////////////
1945 protected double getHorizontalAlignOffset(Rectangle2D r) {
1946 switch (horizontalAlignment) {
1947 case 0: return 0; // Leading
1948 case 1: return -r.getWidth(); // Trailing
1949 case 2: return -r.getCenterX(); // Center
1954 protected double getVerticalAlignOffset() {
1955 FontMetrics fm = fontMetrics;
1958 switch (verticalAlignment) {
1959 case 0: return fm.getMaxAscent(); // Leading=top=maxascent
1960 case 1: return -fm.getMaxDescent(); // Trailing=bottom=maxdescent
1961 case 2: return fm.getMaxAscent() / 2; // Center=maxascent / 2
1967 ///////////////////////////////////////////////////////////////////////////
1969 ///////////////////////////////////////////////////////////////////////////