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.G2DPDFRenderingHints.TextRenderingMode;
55 import org.simantics.scenegraph.g2d.events.Event;
56 import org.simantics.scenegraph.g2d.events.EventTypes;
57 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
58 import org.simantics.scenegraph.g2d.events.MouseEvent;
59 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
60 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;
61 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent;
62 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;
63 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
64 import org.simantics.scenegraph.g2d.events.NodeEventHandler;
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))
406 this.text = new String(text != null ? text : "");
410 this.scaleRecip = 1.0 / scale;
412 this.selectionTail = 0;
417 public void setAutomaticTextFlipping(TextFlipping type) {
420 clearState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED | STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
422 case VerticalTextDownwards:
423 setState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED | STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
425 case VerticalTextUpwards:
426 setState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED);
427 clearState(STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
432 @SyncField({"paddingX", "paddingY"})
433 public void setPadding(double x, double y) {
438 @SyncField({"color"})
439 public void setColor(Color color) {
443 @SyncField({"backgroundColor"})
444 public void setBackgroundColor(Color color) {
445 this.backgroundColor = color;
448 @SyncField({"borderColor"})
449 public void setBorderColor(Color color) {
450 this.borderColor = color;
453 public String getText() {
457 public String getTextBeforeEdit() {
458 return textBeforeEdit;
461 @SyncField({"text","caret","selectionTail"})
462 public void setText(String text) {
463 //System.out.println("TextNode.setText('" + text + "', " + editing + ")");
464 if (hasState(STATE_EDITING))
468 if(this.text != null && text == null) NodeUtil.increasePending(this);
470 if(this.text == null && text != null) NodeUtil.decreasePending(this);
472 this.text = text != null ? text : "";
473 caret = Math.min(caret, this.text.length());
474 selectionTail = caret;
479 @SyncField({"pending"})
480 public void setPending(boolean pending) {
481 boolean p = hasState(STATE_PENDING);
482 if(!p && pending) NodeUtil.increasePending(this);
483 if(p && !pending) NodeUtil.decreasePending(this);
485 setState(STATE_PENDING, pending);
488 @SyncField({"fixedWidth"})
489 public void setFixedWidth(float fixedWidth) {
491 throw new IllegalArgumentException("negative fixed width");
492 this.fixedWidth = fixedWidth;
497 * Bounds where the text box will be drawn
500 public void setTargetBounds(Rectangle2D bounds) {
501 this.targetBounds = bounds;
504 final public void synchronizeWidth(float width) {
506 setFixedWidth(width);
509 final public void synchronizeBorderWidth(float width) {
511 setBorderWidth(width);
514 public final void synchronizeWrapText(boolean wrap) {
515 setState(STATE_WRAP_TEXT, wrap);
518 public boolean isHovering() {
519 return hasState(STATE_HOVER);
522 @SyncField({"hover"})
523 public void setHover(boolean hover) {
524 setState(STATE_HOVER, hover);
528 public Font getFont() {
533 public void setFont(Font font) {
538 public double getBorderWidth() {
542 @SyncField({"borderWidth"})
543 public void setBorderWidth(float width) {
544 this.borderWidth = width;
547 public void setBorderWidth(double width) {
548 setBorderWidth((float)width);
551 @SyncField({"horizontalAlignment"})
552 public void setHorizontalAlignment(byte horizontalAlignment) {
553 if (horizontalAlignment < 0 && horizontalAlignment > 2)
554 throw new IllegalArgumentException("Invalid horizontal alignment: " + horizontalAlignment + ", must be between 0 and 2");
555 this.horizontalAlignment = horizontalAlignment;
559 final public void synchronizeHorizontalAlignment(byte horizontalAlignment) {
560 if (horizontalAlignment >= 0 && horizontalAlignment <= 2)
561 setHorizontalAlignment(horizontalAlignment);
564 public byte getHorizontalAlignment() {
565 return horizontalAlignment;
568 @SyncField({"verticalAlignment"})
569 public void setVerticalAlignment(byte verticalAlignment) {
570 if (verticalAlignment < 0 && verticalAlignment > 3)
571 throw new IllegalArgumentException("Invalid vertical alignment: " + verticalAlignment + ", must be between 0 and 3");
572 this.verticalAlignment = verticalAlignment;
576 final public void synchronizeVerticalAlignment(byte verticalAlignment) {
577 if (verticalAlignment >= 0 && verticalAlignment <= 3)
578 setVerticalAlignment(verticalAlignment);
581 public byte getVerticalAlignment() {
582 return verticalAlignment;
586 * Rendering is single-threaded so we can use a static rectangle for
587 * calculating the expanded bounds for the node.
589 private static transient ThreadLocal<Rectangle2D> tempBounds = new ThreadLocal<Rectangle2D>() {
591 protected Rectangle2D initialValue() {
592 return new Rectangle2D.Double();
597 * Rendering is single-threaded so we can use a static AffineTransform to
598 * prevent continuous memory allocation during text rendering.
600 private static transient ThreadLocal<AffineTransform> tempAffineTransform = new ThreadLocal<AffineTransform>() {
602 protected AffineTransform initialValue() {
603 return new AffineTransform();
608 public void render(Graphics2D g) {
609 AffineTransform ot = g.getTransform();
615 * Note: does not return transformation, stroke, color, etc. to their
619 * @param applyTransform
621 public void render(Graphics2D g, boolean applyTransform) {
622 if (text == null || font == null || color == null)
625 // Cache font metrics if necessary
626 if (fontMetrics == null)
627 fontMetrics = g.getFontMetrics(font);
629 Color color = this.color;
630 boolean isSelected = NodeUtil.isSelected(this, 1);
631 boolean hover = hasState(STATE_HOVER);
632 boolean editing = hasState(STATE_EDITING);
634 if (!isSelected && hover) {
635 color = add(color, 120, 120, 120);
639 g.transform(transform);
640 // Apply separate legacy scale
642 g.scale(scale, scale);
644 // Safety for not rendering when the scale of this text is too small.
645 // When the scale is too small it will cause internal exceptions while
647 AffineTransform curTr = g.getTransform();
648 double currentScale = GeometryUtils.getScale(curTr);
649 //System.out.println("currentScale: " + currentScale);
650 if (currentScale < 1e-6)
656 // Calculate text clip rectangle.
657 // This updates textLayout if necessary.
658 Rectangle2D r = getTightAlignedBoundsInLocal(tempBounds.get(), fontMetrics.getFontRenderContext());
660 computeEditingXOffset();
663 r.setFrame(r.getMinX(), r.getMinY(), fixedWidth, r.getHeight());
664 if(targetBounds != null) {
665 double w = (targetBounds.getWidth() - paddingX * 2) * scaleRecip;
666 double h = (targetBounds.getHeight() - paddingY * 2) * scaleRecip;
667 double x = (targetBounds.getMinX() + paddingX) * scaleRecip;
668 double y = (targetBounds.getMinY() + paddingY) * scaleRecip;
669 r.setRect(x, y, w, h);
672 if (hasState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED)) {
675 if (curTr.getScaleX() != 0) {
676 needsXFlip = curTr.getScaleX() < 0.0;
677 needsYFlip = curTr.getScaleY() < 0.0;
679 boolean flipAll = !hasState(STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
680 needsXFlip = (curTr.getShearY() < 0.0) ^ flipAll;
681 needsYFlip = (curTr.getShearX() > 0.0) ^ flipAll;
683 if (needsXFlip || needsYFlip) {
684 double centerX = r.getWidth()*0.5 + r.getX();
685 double centerY = r.getHeight()*0.5 + r.getY();
687 g.translate(centerX, centerY);
688 g.scale(needsXFlip ? -1.0 : 1.0, needsYFlip ? -1.0 : 1.0);
689 g.translate(-centerX, -centerY);
693 Rectangle2D textClip = r.getBounds2D();
695 expandBoundsUnscaled(r);
697 // Speed rendering optimization: don't draw text that is too small to
698 // read when not editing
699 boolean renderText = true;
701 Object renderingHint = g.getRenderingHint(RenderingHints.KEY_RENDERING);
702 if (renderingHint != RenderingHints.VALUE_RENDER_QUALITY) {
703 float textSizeMM = (float) currentScale * GeometryUtils.pointToMillimeter(font.getSize2D());
704 if (textSizeMM < 1.5f)
709 Shape clipSave = g.getClip();
713 PdfWriter writer = (PdfWriter) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_WRITER);
714 TextRenderingMode renderingMode = (TextRenderingMode) g.getRenderingHint(G2DPDFRenderingHints.KEY_TEXT_RENDERING_MODE);
715 boolean renderAsText = writer != null || renderingMode == TextRenderingMode.AS_TEXT;
718 Color backgroundColor = hasState(STATE_VALID) ? this.backgroundColor : Color.red;
722 // Fill background if necessary
723 if (backgroundColor != null) {
724 g.setColor(backgroundColor);
730 int selectionMin = Math.min(caret, selectionTail);
731 int selectionMax = Math.max(caret, selectionTail);
735 renderText(g, xOffset, renderAsText);
737 Shape clip = g.getClip();
739 // Selection background & text
740 for (Line line : lines) {
741 if (line.intersectsRange(selectionMin, selectionMax)) {
742 Shape selShape = line.getLogicalHighlightShape(selectionMin, selectionMax);
743 line.translate(g, xOffset, 0);
745 g.setColor(SELECTION_BACKGROUND_COLOR);
747 g.setColor(Color.WHITE);
748 // #6459: render as text in PDF and paths on screen
750 g.drawString(line.getText(), 0, 0);
752 line.layout.draw(g, 0, 0);
753 line.translateInv(g, xOffset, 0);
765 renderText(g, 0, renderAsText);
773 if (borderWidth > 0f && borderColor != null) {
774 g.setColor(borderColor);
775 g.setStroke(new BasicStroke((float) (scale*borderWidth)));
779 //System.out.println("bw: " + borderWidth);
780 if (isSelected && showsSelection()) {
781 Composite oc = g.getComposite();
782 g.setComposite(SrcOver_50);
783 g.setColor(Color.RED);
784 float bw = borderWidth;
785 double s = currentScale;
787 bw = (float) (1f / s);
791 g.setStroke(new BasicStroke(bw));
793 //g.draw(GeometryUtils.expandRectangle(r, 1.0));
798 g.scale(scaleRecip, scaleRecip);
799 g.setStroke(RESET_STROKE);
801 lastBounds = getScaledOffsetBounds(r, lastBounds, scale, 0, 0);
802 // g.setColor(Color.MAGENTA); // DEBUG
803 // g.draw(lastBounds); // DEBUG
804 // g.setColor(Color.ORANGE); // DEBUG
805 // g.draw(getBoundsInLocal()); // DEBUG
807 renderSelectedHover(g, isSelected, hover);
810 private void renderCaret(Graphics2D g) {
811 g.setColor(Color.BLACK);
812 for (int i = 0; i < lines.length; i++) {
813 Line line = lines[i];
814 // prevent rendering caret twice on line changes
815 if (line.containsOffset(caret) && // line contains caret
816 (caret != line.endOffset || //caret is not in the end of the line
817 i == lines.length-1 || //caret is end of the last line
818 lines[i+1].startOffset != line.endOffset)) { // beginning of the next line does not start withe the same index as current line
819 Shape[] caretShape = line.getCaretShapes(caret);
820 line.translate(g, xOffset, 0);
821 g.draw(caretShape[0]);
822 if (caretShape[1] != null)
823 g.draw(caretShape[1]);
824 line.translateInv(g, xOffset, 0);
828 private void renderText(Graphics2D g, float xOffset, boolean renderAsText) {
829 //g.draw(tightBoundsCache); // DEBUG
830 for (Line line : lines) {
831 // #6459: render as text in PDF and paths on screen
833 g.drawString(line.getText(), line.alignedPosX + xOffset, line.alignedPosY);
835 line.layout.draw(g, line.alignedPosX + xOffset, line.alignedPosY);
836 //g.draw(line.abbox); // DEBUG
840 protected Rectangle2D getScaledOffsetBounds(Rectangle2D originalBounds, Rectangle2D dst, double scale, double offsetX, double offsetY) {
841 AffineTransform btr = tempAffineTransform.get();
842 btr.setToTranslation(offsetX*scale, offsetY*scale);
843 btr.scale(scale, scale);
844 if (btr.isIdentity()) {
845 dst.setFrame(originalBounds);
847 dst.setFrame(btr.createTransformedShape(originalBounds).getBounds2D());
853 * Invoked when TextNode is selected and a mouse is hovering on top of it. Can be overridden to add custom rendering.
857 protected void renderSelectedHover(Graphics2D g, boolean isSelected, boolean isHovering) {
861 * Replaces the current selection with the content or inserts
862 * the content at caret. After the insertion the caret
863 * will be at the end of inserted text and selection will
867 @SyncField({"text","caret","selectionTail"})
868 protected void insert(String content) {
869 content = editContentFilter != null ? editContentFilter.filter(this, content) : content;
871 int selectionMin = Math.min(caret, selectionTail);
872 int selectionMax = Math.max(caret, selectionTail);
874 String begin = text.substring(0, selectionMin);
875 String end = text.substring(selectionMax);
876 text = begin + content + end;
877 caret = selectionMin + content.length();
878 selectionTail = caret;
880 assert (caret <= text.length());
881 //System.out.println(text + " " + caret );
883 if(validator != null) {
884 String error = validator.apply(text);
885 setState(STATE_VALID, (error == null));
892 protected void fireTextChanged() {
893 if(textListener != null)
894 textListener.textChanged();
899 protected void fireTextEditingStarted() {
900 if(textListener != null)
901 textListener.textEditingStarted();
905 protected void fireTextEditingCancelled() {
906 setState(STATE_VALID);
908 if (deactivateEdit()) {
909 if (textListener != null)
910 textListener.textEditingCancelled();
912 setEditMode(false, false);
914 if (textBeforeEdit != null)
915 setText(textBeforeEdit);
922 public void fireTextEditingEnded() {
923 if (!hasState(STATE_VALID)) {
924 fireTextEditingCancelled();
925 setState(STATE_VALID);
929 if (deactivateEdit()) {
930 if (textListener != null)
931 textListener.textEditingEnded();
933 setEditMode(false, false);
938 public void setTextListener(ITextListener listener) {
939 this.textListener = listener;
942 public void setValidator(Function1<String, String> validator) {
943 this.validator = validator;
946 public void setContentFilter(ITextContentFilter filter) {
947 this.editContentFilter = filter;
950 public void setRVI(RVI rvi) {
954 private void invalidateXOffset() {
955 setState(STATE_X_OFFSET_IS_DIRTY);
958 private void computeEditingXOffset() {
960 if(lines == null) return;
961 if(!hasState(STATE_X_OFFSET_IS_DIRTY)) return;
962 if(fixedWidth > 0f) {
965 // float[] coords = textLayout.getCaretInfo(TextHitInfo.afterOffset(caret));
966 // if(coords != null) {
967 // if(coords[0] > (fixedWidth*MAX_CARET_POSITION)) xOffset = (float)((fixedWidth*MAX_CARET_POSITION)-coords[0]);
977 clearState(STATE_X_OFFSET_IS_DIRTY);
981 @SyncField({"caret","selectionTail"})
982 protected void moveCaret(int move, boolean select) {
983 // prevent setting caret into line separator.
985 while (text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
987 } else if (move < 0) {
988 while (caret+move >= 0 && text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
994 if (caret > text.length())
995 caret = text.length();
997 selectionTail = caret;
1000 private Line findCaretLine() {
1001 // Find the line where caret is. Starting from first line.
1002 for(int i = 0; i < lines.length; i++) {
1003 Line line = lines[i];
1004 if(caret <= line.endOffset) {
1012 * Moves caret to next not letter or digit
1015 private void moveCaretCtrlLeft(boolean shiftDown) {
1016 Line line = findCaretLine();
1019 for(i = caret-1; i > line.startOffset; i--) {
1020 char c = line.document.charAt(i);
1021 if(!Character.isLetterOrDigit(c)) {
1025 moveCaret(i - caret, shiftDown);
1030 * Moves caret to previous non letter or digit
1033 private void moveCaretCtrlRight(boolean shiftDown) {
1034 Line line = findCaretLine();
1037 for(i = caret + 1; i < line.endOffset; i++) {
1038 char c = line.document.charAt(i);
1039 if(!Character.isLetterOrDigit(c)) {
1043 moveCaret(i - caret, shiftDown);
1048 * Moves caret to line end
1051 private void moveCaretEnd(boolean shiftDown) {
1052 Line line = findCaretLine();
1054 // Move caret to the end of the line
1055 moveCaret(line.endOffset - caret, shiftDown);
1059 * Moves caret to beginning of a line
1062 private void moveCaretHome(boolean shiftDown) {
1063 Line line = findCaretLine();
1065 // Move caret to the beginning of the line
1066 moveCaret(line.startOffset - caret, shiftDown);
1070 * Moves caret one row up and tries to maintain the location
1073 private void moveCaretRowUp(boolean shiftDown) {
1074 // Find the line where caret is. Starting from first line.
1075 for(int i = 0; i < lines.length; i++) {
1076 Line line = lines[i];
1077 if(caret <= line.endOffset) {
1078 // caret is in this line
1080 // Already on top line
1081 // Select the beginning of the line
1082 moveCaret(-caret, shiftDown);
1084 Line prevLine = lines[i-1];
1085 int prevLength = prevLine.endOffset - prevLine.startOffset;
1086 int posInCurRow = caret - line.startOffset;
1087 if(prevLength < posInCurRow)
1088 posInCurRow = prevLength;
1090 int newPos = prevLine.startOffset + posInCurRow;
1091 moveCaret(newPos - caret, shiftDown);
1099 * Moves caret one row down and tries to maintain the location
1102 private void moveCaretRowDown(boolean shiftDown) {
1103 // Find the line where caret is. Starting from last line.
1104 for(int i = lines.length - 1; i >= 0; i--) {
1105 Line line = lines[i];
1106 if(caret >= line.startOffset) {
1107 // caret is in this line
1108 if(i == lines.length - 1) {
1109 // Already on bottom line, cannot go below
1110 // Select to the end of the line
1111 moveCaret(line.endOffset - caret, shiftDown);
1113 Line prevLine = lines[i+1]; // Previous line
1115 // Find new caret position.
1116 // Either it is in the same index as before, or if the row
1117 // is not long enough, select the end of the row.
1118 int prevLength = prevLine.endOffset - prevLine.startOffset;
1119 int posInCurRow = caret - line.startOffset;
1120 if(prevLength < posInCurRow)
1121 posInCurRow = prevLength;
1122 int newPos = prevLine.startOffset + posInCurRow;
1123 moveCaret(newPos - caret, shiftDown);
1130 @SyncField({"caret","selectionTail"})
1131 protected void setCaret(int pos, boolean select) {
1135 if (caret > text.length())
1136 caret = text.length();
1138 selectionTail = caret;
1141 protected void setCaret(Point2D point) {
1142 setCaret(point, false);
1145 @SyncField({"caret","selectionTail"})
1146 protected void setCaret(Point2D point, boolean select) {
1148 for(int i = 0; i < lines.length; i++) {
1149 Line line = lines[i];
1150 Rectangle2D bounds = line.abbox;
1151 // Add heights of bboxes for determining the correct line
1153 lineY = bounds.getY();
1155 lineY += lines[i-1].abbox.getHeight();
1157 double lineHeight = bounds.getHeight();
1158 double hitY = point.getY() / scale;
1159 if(hitY >= lineY && hitY <= lineY + lineHeight) {
1160 // Hit is in this line
1161 float x = (float)(point.getX() / scale) - (float)line.abbox.getX();
1162 float y = (float)(point.getY() / scale - lineHeight * i) ;
1163 TextHitInfo info = line.layout.hitTestChar(x, y);
1164 caret = line.startOffset + info.getInsertionIndex();
1165 if (caret > line.endOffset)
1166 caret = line.endOffset;
1168 selectionTail = caret;
1173 invalidateXOffset();
1174 assert (caret <= text.length());
1178 public Rectangle2D getBoundsInLocal() {
1179 if(targetBounds != null)
1180 return targetBounds;
1182 return expandBounds( getTightAlignedBoundsInLocal(null) );
1185 protected Rectangle2D expandBounds(Rectangle2D r) {
1186 r.setRect(r.getX() * scale - paddingX, r.getY() * scale -paddingY, r.getWidth()*scale + paddingX + paddingX, r.getHeight()*scale + paddingY + paddingY);
1187 //System.out.println(" => " + r);
1191 protected Rectangle2D expandBoundsUnscaled(Rectangle2D r) {
1192 r.setRect(r.getX() - scaleRecip*paddingX, r.getY() -scaleRecip*paddingY, r.getWidth() + scaleRecip*paddingX + scaleRecip*paddingX, r.getHeight() + scaleRecip*paddingY + scaleRecip*paddingY);
1193 //System.out.println(" => " + r);
1197 protected Rectangle2D expandBounds(Rectangle2D r, double amount) {
1198 r.setRect(r.getX() - amount, r.getY() - amount, r.getWidth() + 2*amount, r.getHeight() + 2*amount);
1202 protected Rectangle2D expandBounds(Rectangle2D r, double left, double top, double right, double bottom) {
1203 r.setRect(r.getX() - left, r.getY() - top, r.getWidth() + left + right, r.getHeight() + top + bottom);
1207 private void resetCaches() {
1208 this.tightBoundsCache = null;
1210 this.fontMetrics = null;
1214 * Returns the tight bounds around the current text using the current font
1215 * in the specified rectangle. If the specified rectangle is
1216 * <code>null</code> a new Rectangle2D.Double instance will be created.
1221 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r) {
1222 return getTightAlignedBoundsInLocal(r, FRC);
1226 * Returns the tight bounds around the current text using the current font
1227 * in the specified rectangle. If the specified rectangle is
1228 * <code>null</code> a new Rectangle2D.Double instance will be created.
1231 * the rectangle where the result of the method is placed or
1232 * <code>null</code> to allocate new rectangle
1233 * @param frc current font render context
1234 * @return r or new Rectangle2D.Double instance containing the requested
1237 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r, FontRenderContext frc) {
1239 r = new Rectangle2D.Double();
1241 if (tightBoundsCache != null) {
1242 r.setFrame(tightBoundsCache);
1247 if (font == null || txt == null) {
1248 r.setFrame(0, 0, 2, 1);
1252 //System.out.println("TextNode.getTightAlignedBoundsInLocal('" + txt + "')");
1254 // Parse & layout (unaligned)
1255 Line[] lines = null;
1257 if(hasState(STATE_WRAP_TEXT)) {
1258 float width = fixedWidth;
1259 if(width <= 0 && targetBounds != null)
1260 width = (float) (((targetBounds.getWidth() - 2*paddingX)) * scaleRecip);
1262 lines = wrapLines(txt, font, width, frc);
1266 lines = parseLines(txt);
1267 this.lines = layoutLines(lines, frc);
1269 // Calculate tight bounds based on unaligned layout
1270 //System.out.println("Unaligned");
1271 tightBoundsCache = calculateBounds(lines, Line.BBOX, null);
1272 //System.out.println(" => " + tightBoundsCache);
1274 this.lines = layoutLinesX(lines, tightBoundsCache);
1275 // Align each line to the calculated tight bounds
1276 this.lines = alignLines(this.lines, tightBoundsCache, horizontalAlignment, verticalAlignment);
1278 // Calculate aligned bounds
1279 //System.out.println("Aligned");
1280 calculateBounds(lines, Line.ABBOX, tightBoundsCache);
1282 r.setFrame(tightBoundsCache);
1283 //System.out.println(" => " + tightBoundsCache);
1291 * the bounding box of all the whole laid out text (only bbox
1295 private Line[] layoutLinesX(Line[] lines, Rectangle2D bbox) {
1296 int lineCount = lines.length;
1297 for (int l = 0; l < lineCount; ++l) {
1298 Line line = lines[l];
1299 // Compute pen x position. If the paragraph is right-to-left we
1300 // will align the TextLayouts to the right edge of the panel.
1301 // Note: drawPosX is always where the LEFT of the text is placed.
1302 // NOTE: This changes based on horizontal alignment
1303 line.drawPosX = (float) (line.layout.isLeftToRight() ? 0f
1304 : tightBoundsCache.getWidth() - line.layout.getAdvance());
1311 * @param boundsProvider
1315 private static Rectangle2D calculateBounds(Line[] lines, BoundsProcedure boundsProvider, Rectangle2D result) {
1317 result = new Rectangle2D.Double();
1319 result.setFrame(0, 0, 0, 0);
1321 for (Line line : lines) {
1322 //System.out.println("line: " + line);
1323 Rectangle2D bbox = boundsProvider.getBounds(line);
1324 if (result.isEmpty())
1325 result.setFrame(bbox);
1327 Rectangle2D.union(result, bbox, result);
1328 //System.out.println("bounds: " + result);
1330 //System.out.println("final bounds: " + result);
1340 * @return aligned lines
1342 private Line[] alignLines(Line[] lines, Rectangle2D bbox, byte hAlign, byte vAlign) {
1343 // System.out.println("horizontal align: " + Alignment.values()[hAlign]);
1344 // System.out.println("vertical align : " + Alignment.values()[vAlign]);
1345 // System.out.println("bbox: " + bbox);
1347 // double ybase = 0;
1348 if(targetBounds != null) {
1349 /* In normal cases the bounding box moves when
1350 * typing. If target bounds are set, the text
1351 * is fitted into the box.
1355 xbase = (targetBounds.getMaxX()-paddingX) * scaleRecip;
1358 xbase = targetBounds.getCenterX() * scaleRecip;
1360 default: // Leading / Baseline
1367 for (Line line : lines) {
1373 xoffset = xbase - line.bbox.getWidth();
1376 xoffset = xbase - line.bbox.getWidth() / 2;
1378 default: // Leading / Baseline
1385 yoffset = line.layout.getAscent();
1388 yoffset = -bbox.getHeight() + line.layout.getAscent();
1391 yoffset = -bbox.getHeight() / 2 + line.layout.getAscent();
1395 line.alignOffset(xoffset, yoffset);
1405 private Line[] layoutLines(Line[] lines, FontRenderContext frc) {
1406 TextLayout emptyRowLayout = null;
1407 int lineCount = lines.length;
1409 for (int l = 0; l < lineCount; ++l) {
1410 Line line = lines[l];
1411 String lineText = line.getText();
1412 // " " because TextLayout requires non-empty text and
1413 // We don't want zero size for the text.
1414 if (lineText.isEmpty()) {
1416 if (emptyRowLayout == null)
1417 emptyRowLayout = new TextLayout(lineText, font, frc);
1418 line.layout = emptyRowLayout;
1420 line.layout = new TextLayout(lineText, font, frc);
1423 //y += line.layout.getAscent();
1425 y += line.layout.getDescent() + line.layout.getLeading() + line.layout.getAscent();
1427 Rectangle2D bbox = line.layout.getLogicalHighlightShape(0, lineText.length()).getBounds2D();
1428 bbox.setFrame(bbox.getX(), bbox.getY() + line.drawPosY, bbox.getWidth(), bbox.getHeight());
1436 * Splits the specified string into {@link Line} structures, one for each
1437 * line in the input text. The returned lines are only partially defined,
1438 * waiting to be laid out (see
1439 * {@link #layoutLines(Line[], FontRenderContext)})
1443 * @return parsed text lines as {@link Line} structures
1444 * @see #layoutLines(Line[], FontRenderContext)
1446 private static Line[] parseLines(String txt) {
1447 int len = txt.length();
1449 return new Line[] { new Line("", 0, 0) };
1451 TIntArrayList lfpos = new TIntArrayList();
1454 for (;pos < len; ++lineCount) {
1455 int nextlf = txt.indexOf('\n', pos);
1456 lfpos.add(nextlf != -1 ? nextlf : len);
1461 Line[] lines = new Line[lineCount];
1463 for (int i = 0; i < lineCount-1; ++i) {
1464 int lf = lfpos.getQuick(i);
1465 int cr = (lf > 0) && (txt.charAt(lf - 1) == '\r') ? lf - 1 : lf;
1466 lines[i] = new Line(txt, pos, cr);
1469 lines[lineCount - 1] = new Line(txt, pos, len);
1475 private static Line[] wrapLines(String txt, Font font, float fixedWidth, FontRenderContext frc) {
1476 if(txt == null || txt.isEmpty())
1479 ArrayList<Line> lines =
1480 new ArrayList<Line>();
1482 Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
1483 map.put(TextAttribute.FONT, font);
1484 AttributedString attributedText = new AttributedString(txt.isEmpty() ? "__ERROR__" : txt, map);
1486 AttributedCharacterIterator paragraph = attributedText.getIterator();
1487 int paragraphStart = paragraph.getBeginIndex();
1488 int paragraphEnd = paragraph.getEndIndex();
1489 LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
1491 float breakWidth = fixedWidth;
1493 // Force text to be vertical, by setting break width to 1, if the text area is narrower than "GGGG"
1495 // Set position to the index of the first character in the paragraph.
1496 lineMeasurer.setPosition(paragraphStart);
1498 // Get lines until the entire paragraph has been displayed.
1499 int next, limit, charat, position = 0;
1501 while ((position = lineMeasurer.getPosition()) < paragraphEnd) {
1503 // Find possible line break and set it as a limit to the next layout
1504 next = lineMeasurer.nextOffset(breakWidth);
1506 charat = txt.indexOf(System.getProperty("line.separator"),position+1);
1507 if(charat < next && charat != -1){
1511 lineMeasurer.nextLayout(breakWidth, limit, false);
1513 lines.add(new Line(txt, position, limit));
1516 return lines.toArray(new Line[lines.size()]);
1520 public String getClipboardContent() {
1521 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1522 Transferable clipData = clipboard.getContents(this);
1524 return (String) (clipData.getTransferData(DataFlavor.stringFlavor));
1525 } catch (Exception ee) {
1530 public void setClipboardContent(String content) {
1531 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1532 StringSelection data = new StringSelection(content);
1533 clipboard.setContents(data, data);
1537 public String toString() {
1538 return super.toString() + " [text=" + text + ", font=" + font + ", color=" + color + "]";
1542 protected boolean handleCommand(CommandEvent e) {
1543 if (!hasState(STATE_EDITING))
1546 if (Commands.SELECT_ALL.equals(e.command)) {
1554 protected boolean keyPressed(KeyPressedEvent event) {
1555 if (!hasState(STATE_EDITING))
1558 char c = event.character;
1559 boolean ctrl = event.isControlDown();
1560 boolean alt = event.isAltDown();
1562 // System.out.println("Key pressed '" + (event.character == 0 ? "\\0" : "" + c) + "' " + event.keyCode + " - " + Integer.toBinaryString(event.stateMask));
1563 // System.out.println("ctrl: " + ctrl);
1564 // System.out.println("alt: " + alt);
1566 switch (event.keyCode) {
1568 if (caret != selectionTail) {
1569 int selectionMin = Math.min(caret, selectionTail);
1570 int selectionMax = Math.max(caret, selectionTail);
1571 setClipboardContent(text.substring(selectionMin, selectionMax));
1576 if (caret != selectionTail) {
1577 int selectionMin = Math.min(caret, selectionTail);
1578 int selectionMax = Math.max(caret, selectionTail);
1579 setClipboardContent(text.substring(selectionMin, selectionMax));
1584 case KeyEvent.VK_RIGHT:
1586 // '\'' has the same keycode as VK_RIGHT but when right
1587 // arrow is pressed, event character is \0.
1588 moveCaretCtrlRight(event.isShiftDown());
1592 case KeyEvent.VK_LEFT:
1593 moveCaretCtrlLeft(event.isShiftDown());
1598 String content = getClipboardContent();
1604 // Replaced by #handleCommand
1605 // case KeyEvent.VK_A:
1611 case KeyEvent.VK_ENTER:
1613 insert(getLineSeparator());
1621 } else if (!ctrl && alt) {
1624 switch (event.keyCode) {
1625 case KeyEvent.VK_LEFT:
1626 moveCaret(-1, event.isShiftDown());
1628 case KeyEvent.VK_RIGHT:
1630 // '\'' has the same keycode as VK_RIGHT but when right
1631 // arrow is pressed, event character is \0.
1632 moveCaret(1, event.isShiftDown());
1635 // Intentional fallthrough to default case
1636 case KeyEvent.VK_UP:
1637 moveCaretRowUp(event.isShiftDown());
1639 case KeyEvent.VK_DOWN:
1640 moveCaretRowDown(event.isShiftDown());
1642 case KeyEvent.VK_HOME:
1643 moveCaretHome(event.isShiftDown());
1645 case KeyEvent.VK_END:
1646 moveCaretEnd(event.isShiftDown());
1649 case KeyEvent.VK_ENTER:
1650 fireTextEditingEnded();
1653 case KeyEvent.VK_ESCAPE:
1654 text = textBeforeEdit;
1656 clearState(STATE_EDITING);
1657 fireTextEditingCancelled();
1660 case KeyEvent.VK_BACK_SPACE:
1661 if(caret == selectionTail && caret > 0) {
1662 // line separator may use multiple characters, we want to remove that with one command
1663 String lineSep = getLineSeparator();
1664 int index = lineSep.indexOf(text.charAt(caret-1));
1669 selectionTail+= (lineSep.length()-index-1);
1675 case KeyEvent.VK_DELETE:
1676 if(caret == selectionTail && caret < text.length()) {
1677 String lineSep = getLineSeparator();
1678 int index = lineSep.indexOf(text.charAt(caret));
1682 selectionTail-= index;
1683 caret+= (lineSep.length()-index);
1692 if (c == 65535 || Character.getType(c) == Character.CONTROL) {
1695 //System.out.println("Char " + c + " " + Character.getType(c) + " " + text);
1696 insert(new String(new char[] {c}));
1700 // FIXME This is called even if just caret was moved.
1701 // This is currently necessary for repaints.
1703 invalidateXOffset();
1707 protected String getLineSeparator() {
1708 return System.getProperty("line.separator");
1711 protected void selectAll() {
1713 setCaret(text.length(), true);
1716 protected transient int hoverClick = 0;
1719 protected boolean mouseClicked(MouseClickEvent event) {
1720 if (event.button != MouseClickEvent.LEFT_BUTTON)
1723 if (hasState(STATE_HOVER)) {
1727 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1728 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1731 IElement e = DiagramNodeUtil.getElement(ctx, this);
1732 if (!hasState(STATE_EDITING)) {
1733 if (Boolean.TRUE.equals(setEditMode(true))) {
1734 editActivation = activateEdit(0, e, ctx);
1740 if (hasState(STATE_EDITING)) {
1741 fireTextEditingEnded();
1747 protected boolean mouseDoubleClicked(MouseDoubleClickedEvent event) {
1748 if (event.button != MouseClickEvent.LEFT_BUTTON)
1751 if (hitTest(event, 0)) {
1752 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1753 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1758 // Select the whole text.
1760 setCaret(text.length(), true);
1769 protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {
1770 if (!hasState(STATE_EDITING))
1773 Point2D local = controlToLocal( event.controlPosition );
1774 // FIXME: once the event coordinate systems are cleared up, remove this workaround
1775 local = parentToLocal(local);
1776 if (hasState(STATE_HOVER) && this.containsLocal(local)) {
1777 setCaret(local, event.isShiftDown());
1783 protected boolean mouseMoved(MouseMovedEvent event) {
1784 boolean hit = hitTest(event, 3.0);
1785 if (hit != hasState(STATE_HOVER)) {
1786 setState(STATE_HOVER, hit);
1792 private boolean isControlDown(MouseEvent e) {
1793 return e.isControlDown() || lastMouseEvent!=null?lastMouseEvent.isControlDown():false;
1796 protected boolean isShiftDown(MouseEvent e) {
1797 return e.isShiftDown() || lastMouseEvent!=null?lastMouseEvent.isShiftDown():false;
1800 // private boolean isAltDown(MouseEvent e) {
1801 // return e.isAltDown() || lastMouseEvent!=null?lastMouseEvent.isAltDown():false;
1805 protected boolean mouseDragged(MouseDragBegin e) {
1807 && (isControlDown(e) || isShiftDown(e))
1808 && e.context instanceof NodeEventHandler
1809 && (dataRVI != null || text != null))
1811 List<Transferable> trs = new ArrayList<>(2);
1812 if (dataRVI != null) {
1813 trs.add(new LocalObjectTransferable(dataRVI));
1814 trs.add(new PlaintextTransfer(dataRVI.toString()));
1815 } else if (text != null && !text.isEmpty()) {
1816 trs.add(new PlaintextTransfer(text));
1818 if (!trs.isEmpty()) {
1819 e.transferable = new MultiTransferable(trs);
1826 protected boolean hitTest(MouseEvent event, double tolerance) {
1827 Rectangle2D bounds = getBoundsInternal();
1830 Point2D localPos = NodeUtil.worldToLocal(this, event.controlPosition, new Point2D.Double());
1831 double x = localPos.getX();
1832 double y = localPos.getY();
1833 boolean hit = bounds.contains(x, y);
1837 public Rectangle2D getBoundsInternal() {
1838 Rectangle2D local = lastBounds;
1841 // TODO: potential spot for CPU/memory allocation optimization
1842 // by using more specialized implementations
1843 if (transform.isIdentity())
1845 return transform.createTransformedShape(local).getBounds2D();
1848 protected Color add(Color c, int r, int g, int b) {
1849 int nr = Math.min(255, c.getRed() + r);
1850 int ng = Math.min(255, c.getGreen() + g);
1851 int nb = Math.min(255, c.getBlue() + b);
1852 return new Color(nr, ng, nb);
1855 public TextEditActivation activateEdit(int mouseId, IElement e, ICanvasContext ctx) {
1856 EditDataNode data = EditDataNode.getNode(this);
1857 deactivateEdit(data, null);
1858 TextEditActivation result = new TextEditActivation(mouseId, e, ctx);
1859 data.setTextEditActivation(result);
1864 * @return <code>true</code> if this node is or was previously in editing
1867 protected boolean deactivateEdit() {
1868 boolean result = deactivateEdit( editActivation );
1869 result |= editActivation != null;
1870 editActivation = null;
1874 protected boolean deactivateEdit(TextEditActivation activation) {
1875 return deactivateEdit( EditDataNode.getNode(this), activation );
1878 protected boolean deactivateEdit(EditDataNode data, TextEditActivation activation) {
1879 TextEditActivation previous = data.getTextEditActivation();
1880 if (previous != null && (previous == activation || activation == null)) {
1882 data.setTextEditActivation(null);
1889 public int getEventMask() {
1890 return EventTypes.KeyPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask
1891 | EventTypes.MouseClickMask | EventTypes.MouseDragBeginMask | EventTypes.CommandMask;
1894 private MouseEvent lastMouseEvent = null;
1897 public boolean handleEvent(Event e) {
1898 if(e instanceof MouseEvent && !(e instanceof MouseDragBegin)) lastMouseEvent = (MouseEvent)e;
1899 return super.handleEvent(e);
1903 public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
1904 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
1908 public <T> T getProperty(String propertyName) {
1913 public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
1916 public void synchronizeText(String text) {
1920 public void synchronizeColor(RGB.Integer color) {
1921 this.color = Colors.awt(color);
1924 public void synchronizeFont(org.simantics.datatypes.literal.Font font) {
1925 setFont(Fonts.awt(font));
1928 public void synchronizeTransform(double[] data) {
1929 this.setTransform(new AffineTransform(data));
1932 public static void main(String[] args) {
1933 Line[] lines = parseLines("\n \n FOO \n\nBAR\n\n\n BAZ\n\n");
1934 System.out.println(Arrays.toString(lines));
1935 System.out.println(GeometryUtils.pointToMillimeter(1));
1936 System.out.println(GeometryUtils.pointToMillimeter(12));
1937 System.out.println(GeometryUtils.pointToMillimeter(72));
1940 ///////////////////////////////////////////////////////////////////////////
1941 // LEGACY CODE NEEDED BY INHERITED CLASSES FOR NOW
1942 ///////////////////////////////////////////////////////////////////////////
1944 protected double getHorizontalAlignOffset(Rectangle2D r) {
1945 switch (horizontalAlignment) {
1946 case 0: return 0; // Leading
1947 case 1: return -r.getWidth(); // Trailing
1948 case 2: return -r.getCenterX(); // Center
1953 protected double getVerticalAlignOffset() {
1954 FontMetrics fm = fontMetrics;
1957 switch (verticalAlignment) {
1958 case 0: return fm.getMaxAscent(); // Leading=top=maxascent
1959 case 1: return -fm.getMaxDescent(); // Trailing=bottom=maxdescent
1960 case 2: return fm.getMaxAscent() / 2; // Center=maxascent / 2
1966 ///////////////////////////////////////////////////////////////////////////
1968 ///////////////////////////////////////////////////////////////////////////