1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.diagram.elements;
14 import java.awt.AlphaComposite;
15 import java.awt.BasicStroke;
16 import java.awt.Color;
17 import java.awt.Composite;
19 import java.awt.FontMetrics;
20 import java.awt.Graphics2D;
21 import java.awt.RenderingHints;
22 import java.awt.Shape;
23 import java.awt.Toolkit;
24 import java.awt.datatransfer.Clipboard;
25 import java.awt.datatransfer.DataFlavor;
26 import java.awt.datatransfer.StringSelection;
27 import java.awt.datatransfer.Transferable;
28 import java.awt.event.KeyEvent;
29 import java.awt.font.FontRenderContext;
30 import java.awt.font.LineBreakMeasurer;
31 import java.awt.font.TextAttribute;
32 import java.awt.font.TextHitInfo;
33 import java.awt.font.TextLayout;
34 import java.awt.geom.AffineTransform;
35 import java.awt.geom.Point2D;
36 import java.awt.geom.Rectangle2D;
37 import java.text.AttributedCharacterIterator;
38 import java.text.AttributedString;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.Hashtable;
42 import java.util.List;
44 import org.simantics.datatypes.literal.RGB;
45 import org.simantics.db.layer0.variable.RVI;
46 import org.simantics.diagram.elements.Line.BoundsProcedure;
47 import org.simantics.g2d.canvas.ICanvasContext;
48 import org.simantics.g2d.element.IElement;
49 import org.simantics.scenegraph.IDynamicSelectionPainterNode;
50 import org.simantics.scenegraph.LoaderNode;
51 import org.simantics.scenegraph.ScenegraphUtils;
52 import org.simantics.scenegraph.g2d.G2DNode;
53 import org.simantics.scenegraph.g2d.G2DPDFRenderingHints;
54 import org.simantics.scenegraph.g2d.G2DRenderingHints;
55 import org.simantics.scenegraph.g2d.G2DRenderingHints.TextRenderingMode;
56 import org.simantics.scenegraph.g2d.events.Event;
57 import org.simantics.scenegraph.g2d.events.EventTypes;
58 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
59 import org.simantics.scenegraph.g2d.events.MouseEvent;
60 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
61 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;
62 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent;
63 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;
64 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
65 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
66 import org.simantics.scenegraph.g2d.events.command.Commands;
67 import org.simantics.scenegraph.utils.GeometryUtils;
68 import org.simantics.scenegraph.utils.NodeUtil;
69 import org.simantics.scl.runtime.function.Function1;
70 import org.simantics.scl.runtime.function.Function2;
71 import org.simantics.ui.colors.Colors;
72 import org.simantics.ui.dnd.LocalObjectTransferable;
73 import org.simantics.ui.dnd.MultiTransferable;
74 import org.simantics.ui.dnd.PlaintextTransfer;
75 import org.simantics.ui.fonts.Fonts;
76 import org.simantics.utils.threads.AWTThread;
78 import com.lowagie.text.pdf.PdfWriter;
80 import gnu.trove.list.array.TIntArrayList;
84 * TextNode which supports in-line editing.
86 * By default <code>TextNode</code> is in editable = false state. Use
87 * {@link #setEditable(boolean)} to make it editable.
89 * @author Hannu Niemistö <hannu.niemisto@vtt.fi>
90 * @author Marko Luukkainen <marko.luukkainen@vtt.fi>
91 * @author Tuukka Lehtonen <tuukka.lehtonen@semantum.fi>
94 * o proper support for defining clipping bounds for the text (needed for page templates) (currently through fixedWidth)
95 * o fix editing xOffset to work with fixed width and multi-line text
100 public class TextNode extends G2DNode implements IDynamicSelectionPainterNode, LoaderNode {
102 private static final long serialVersionUID = 654692698101485672L;
104 public static enum TextFlipping {
107 VerticalTextDownwards,
111 * TODO: justify existence for this
113 private static final BasicStroke RESET_STROKE = new BasicStroke(1);
116 * Src-over alpha composite instance with 50% opacity.
118 private static final AlphaComposite SrcOver_50 = AlphaComposite.SrcOver.derive(0.5f);
121 * For (inexact) measurement of rendered text bounds.
123 protected static final FontRenderContext FRC = new FontRenderContext(new AffineTransform(), true, true);
125 private static final Font FONT = Font.decode("Arial 6");
126 private static final Color SELECTION_BACKGROUND_COLOR = new Color(0x316ac5);
127 // private static final double MAX_CARET_POSITION = 1.0;
130 * The complete text visualized by this node.
132 protected String text = null;
135 * The font used to render the {@link #text}.
137 protected Font font = FONT;
140 * The color of the rendered text. Default value is {@value Color#black}.
142 protected Color color = Color.BLACK;
145 * The background color used for filling the background of the bounding box
146 * of the rendered text. <code>null</code> means no fill.
147 * Default value is <code>null</code>.
149 protected Color backgroundColor = null;
152 * The color used for drawing the expanded bounding box border for the
153 * rendered text. <code>null</code> means no border is rendered. Default
154 * value is <code>null</code>.
156 protected Color borderColor = null;
158 protected double scale = 1.0;
159 protected transient double scaleRecip = 1.0;
164 protected float borderWidth = 0.f;
166 protected double paddingX = 2.0;
167 protected double paddingY = 2.0;
170 * Horizontal text box alignment with respect to its origin. Default value is
173 protected byte horizontalAlignment = 0;
175 * Vertical text box alignment with respect to its origin. Default value is
178 protected byte verticalAlignment = 3;
181 * Tells if this node is still pending for real results or not.
183 protected static final int STATE_PENDING = (1 << 0);
184 protected static final int STATE_HOVER = (1 << 1);
185 protected static final int STATE_EDITABLE = (1 << 2);
186 protected static final int STATE_SHOW_SELECTION = (1 << 3);
187 protected static final int STATE_WRAP_TEXT = (1 << 4);
188 protected transient static final int STATE_EDITING = (1 << 5);
189 protected transient static final int STATE_VALID = (1 << 6);
190 protected transient static final int STATE_X_OFFSET_IS_DIRTY = (1 << 7);
191 protected static final int STATE_ALWAYS_ADD_LISTENERS = (1 << 8);
192 protected static final int STATE_LISTENERS_ADDED = (1 << 9);
193 protected static final int STATE_AUTOMATIC_TEXT_FLIP_ENABLED = (1 << 10);
194 protected static final int STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN = (1 << 11);
197 * A combination of all the STATE_ constants defined in this class,
198 * e.g. {@link #STATE_PENDING}.
200 protected int state = STATE_SHOW_SELECTION | STATE_WRAP_TEXT | STATE_VALID | STATE_X_OFFSET_IS_DIRTY;
202 protected RVI dataRVI = null;
205 int selectionTail = 0;
208 float fixedWidth = 0f;
210 private Rectangle2D targetBounds;
212 Function1<String, String> validator;
213 ITextListener textListener;
214 ITextContentFilter editContentFilter;
217 * The renderable line structures parsed from {@link #text} by
218 * {@link #parseLines(String)}, laid out by
219 * {@link #layoutLines(Line[], FontRenderContext)} and aligned by
220 * {@link #alignLines(Line[], Rectangle2D, byte, byte)}
222 protected transient Line[] lines = null;
223 protected transient FontMetrics fontMetrics = null;
226 * Stores the value of {@link #text} before edit mode was last entered. Used
227 * for restoring the original value if editing is cancelled.
229 private transient String textBeforeEdit = null;
230 protected transient TextEditActivation editActivation;
233 * Stores the last scaled bounds.
235 private transient Rectangle2D lastBounds = new Rectangle2D.Double();
238 * This must be nullified if anything that affects the result of
239 * {@link #getTightAlignedBoundsInLocal(Rectangle2D, FontRenderContext)}
240 * changes. It will cause the cached value to be recalculated on the next
243 private transient Rectangle2D tightBoundsCache = null;
248 // Mark this node as pending
249 NodeUtil.increasePending(this);
253 public void cleanup() {
258 protected boolean hasState(int flags) {
259 return (state & flags) == flags;
262 protected void setState(int flags) {
266 protected void setState(int flags, boolean set) {
270 this.state &= ~flags;
273 protected void clearState(int flags) {
274 this.state &= ~flags;
277 protected void setListeners(boolean add) {
284 protected void addListeners() {
285 if (!hasState(STATE_LISTENERS_ADDED)) {
286 addEventHandler(this);
287 setState(STATE_LISTENERS_ADDED);
291 protected void removeListeners() {
292 if (hasState(STATE_LISTENERS_ADDED)) {
293 removeEventHandler(this);
294 clearState(STATE_LISTENERS_ADDED);
299 * Set to true to always enable event listening in this TextNode to allow the text node to keep track of hovering, etc. and to allow DnD even when
302 public void setForceEventListening(boolean force) {
303 setState(STATE_ALWAYS_ADD_LISTENERS, force);
304 if (force && !hasState(STATE_EDITABLE)) {
310 * Enables or disables edit mode. It also sets
311 * the caret at the end of text all selects the
312 * whole text (this is the usual convention when
313 * beginning to edit one line texts).
315 * @return null if no change to edit state was made
317 public Boolean setEditMode(boolean edit) {
318 return setEditMode(edit, true);
322 * Enables or disables edit mode. It also sets
323 * the caret at the end of text all selects the
324 * whole text (this is the usual convention when
325 * beginning to edit one line texts).
327 * @return null if no change to edit state was made
329 protected Boolean setEditMode(boolean edit, boolean notify) {
330 if (edit && !hasState(STATE_EDITABLE))
332 if (hasState(STATE_EDITING) == edit)
334 setState(STATE_EDITING, edit);
336 caret = text != null ? text.length() : 0;
338 textBeforeEdit = text;
340 fireTextEditingStarted();
344 fireTextEditingEnded();
345 return Boolean.FALSE;
349 @SyncField({"editable"})
350 public void setEditable(boolean editable) {
351 boolean changed = hasState(STATE_EDITABLE) != editable;
352 setState(STATE_EDITABLE, editable);
353 if (hasState(STATE_EDITING) && !editable)
355 if (changed && !hasState(STATE_ALWAYS_ADD_LISTENERS)) {
356 setListeners(editable);
360 public boolean isEditable() {
361 return hasState(STATE_EDITABLE);
364 public boolean isEditMode() {
365 return hasState(STATE_EDITING);
368 @SyncField({"wrapText"})
369 public void setWrapText(boolean wrapText) {
370 setState(STATE_WRAP_TEXT, wrapText);
374 * @return Does the text box wrap text if
375 * the width of the box is fixed
377 public boolean isWrapText() {
378 return hasState(STATE_WRAP_TEXT);
381 @SyncField({"showSelection"})
382 public void setShowSelection(boolean showSelection) {
383 setState(STATE_SHOW_SELECTION, showSelection);
386 public boolean showsSelection() {
387 return hasState(STATE_SHOW_SELECTION);
394 * @param x not supported anymore, use {@link #setTransform(AffineTransform)} instead
395 * @param y not supported anymore, use {@link #setTransform(AffineTransform)} instead
398 @SyncField({"text", "font", "color", "x", "y", "scale"})
399 public void init(String text, Font font, Color color, double x, double y, double scale) {
401 if(this.text == null && text != null) NodeUtil.decreasePending(this);
403 if (hasState(STATE_EDITING))
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(G2DRenderingHints.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) {
860 public String editText(String text) {
862 String error = validator != null ? validator.apply(text) : null;
865 if (textListener != null) {
866 textListener.textEditingEnded();
873 * Replaces the current selection with the content or inserts
874 * the content at caret. After the insertion the caret
875 * will be at the end of inserted text and selection will
879 @SyncField({"text","caret","selectionTail"})
880 protected void insert(String content) {
881 content = editContentFilter != null ? editContentFilter.filter(this, content) : content;
883 int selectionMin = Math.min(caret, selectionTail);
884 int selectionMax = Math.max(caret, selectionTail);
886 String begin = text.substring(0, selectionMin);
887 String end = text.substring(selectionMax);
888 text = begin + content + end;
889 caret = selectionMin + content.length();
890 selectionTail = caret;
892 assert (caret <= text.length());
893 //System.out.println(text + " " + caret );
895 if(validator != null) {
896 String error = validator.apply(text);
897 setState(STATE_VALID, (error == null));
904 protected void fireTextChanged() {
905 if(textListener != null)
906 textListener.textChanged();
911 protected void fireTextEditingStarted() {
912 if(textListener != null)
913 textListener.textEditingStarted();
917 protected void fireTextEditingCancelled() {
918 setState(STATE_VALID);
920 if (deactivateEdit()) {
921 if (textListener != null)
922 textListener.textEditingCancelled();
924 setEditMode(false, false);
926 if (textBeforeEdit != null)
927 setText(textBeforeEdit);
934 public void fireTextEditingEnded() {
935 if (!hasState(STATE_VALID)) {
936 fireTextEditingCancelled();
937 setState(STATE_VALID);
941 if (deactivateEdit()) {
942 if (textListener != null)
943 textListener.textEditingEnded();
945 setEditMode(false, false);
950 public void setTextListener(ITextListener listener) {
951 this.textListener = listener;
954 public void setValidator(Function1<String, String> validator) {
955 this.validator = validator;
958 public void setContentFilter(ITextContentFilter filter) {
959 this.editContentFilter = filter;
962 public void setRVI(RVI rvi) {
966 private void invalidateXOffset() {
967 setState(STATE_X_OFFSET_IS_DIRTY);
970 private void computeEditingXOffset() {
972 if(lines == null) return;
973 if(!hasState(STATE_X_OFFSET_IS_DIRTY)) return;
974 if(fixedWidth > 0f) {
977 // float[] coords = textLayout.getCaretInfo(TextHitInfo.afterOffset(caret));
978 // if(coords != null) {
979 // if(coords[0] > (fixedWidth*MAX_CARET_POSITION)) xOffset = (float)((fixedWidth*MAX_CARET_POSITION)-coords[0]);
989 clearState(STATE_X_OFFSET_IS_DIRTY);
993 @SyncField({"caret","selectionTail"})
994 protected void moveCaret(int move, boolean select) {
995 // prevent setting caret into line separator.
997 while (text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
999 } else if (move < 0) {
1000 while (caret+move >= 0 && text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
1006 if (caret > text.length())
1007 caret = text.length();
1009 selectionTail = caret;
1012 private Line findCaretLine() {
1013 // Find the line where caret is. Starting from first line.
1014 for(int i = 0; i < lines.length; i++) {
1015 Line line = lines[i];
1016 if(caret <= line.endOffset) {
1024 * Moves caret to next not letter or digit
1027 private void moveCaretCtrlLeft(boolean shiftDown) {
1028 Line line = findCaretLine();
1031 for(i = caret-1; i > line.startOffset; i--) {
1032 char c = line.document.charAt(i);
1033 if(!Character.isLetterOrDigit(c)) {
1037 moveCaret(i - caret, shiftDown);
1042 * Moves caret to previous non letter or digit
1045 private void moveCaretCtrlRight(boolean shiftDown) {
1046 Line line = findCaretLine();
1049 for(i = caret + 1; i < line.endOffset; i++) {
1050 char c = line.document.charAt(i);
1051 if(!Character.isLetterOrDigit(c)) {
1055 moveCaret(i - caret, shiftDown);
1060 * Moves caret to line end
1063 private void moveCaretEnd(boolean shiftDown) {
1064 Line line = findCaretLine();
1066 // Move caret to the end of the line
1067 moveCaret(line.endOffset - caret, shiftDown);
1071 * Moves caret to beginning of a line
1074 private void moveCaretHome(boolean shiftDown) {
1075 Line line = findCaretLine();
1077 // Move caret to the beginning of the line
1078 moveCaret(line.startOffset - caret, shiftDown);
1082 * Moves caret one row up and tries to maintain the location
1085 private void moveCaretRowUp(boolean shiftDown) {
1086 // Find the line where caret is. Starting from first line.
1087 for(int i = 0; i < lines.length; i++) {
1088 Line line = lines[i];
1089 if(caret <= line.endOffset) {
1090 // caret is in this line
1092 // Already on top line
1093 // Select the beginning of the line
1094 moveCaret(-caret, shiftDown);
1096 Line prevLine = lines[i-1];
1097 int prevLength = prevLine.endOffset - prevLine.startOffset;
1098 int posInCurRow = caret - line.startOffset;
1099 if(prevLength < posInCurRow)
1100 posInCurRow = prevLength;
1102 int newPos = prevLine.startOffset + posInCurRow;
1103 moveCaret(newPos - caret, shiftDown);
1111 * Moves caret one row down and tries to maintain the location
1114 private void moveCaretRowDown(boolean shiftDown) {
1115 // Find the line where caret is. Starting from last line.
1116 for(int i = lines.length - 1; i >= 0; i--) {
1117 Line line = lines[i];
1118 if(caret >= line.startOffset) {
1119 // caret is in this line
1120 if(i == lines.length - 1) {
1121 // Already on bottom line, cannot go below
1122 // Select to the end of the line
1123 moveCaret(line.endOffset - caret, shiftDown);
1125 Line prevLine = lines[i+1]; // Previous line
1127 // Find new caret position.
1128 // Either it is in the same index as before, or if the row
1129 // is not long enough, select the end of the row.
1130 int prevLength = prevLine.endOffset - prevLine.startOffset;
1131 int posInCurRow = caret - line.startOffset;
1132 if(prevLength < posInCurRow)
1133 posInCurRow = prevLength;
1134 int newPos = prevLine.startOffset + posInCurRow;
1135 moveCaret(newPos - caret, shiftDown);
1142 @SyncField({"caret","selectionTail"})
1143 protected void setCaret(int pos, boolean select) {
1147 if (caret > text.length())
1148 caret = text.length();
1150 selectionTail = caret;
1153 protected void setCaret(Point2D point) {
1154 setCaret(point, false);
1157 @SyncField({"caret","selectionTail"})
1158 protected void setCaret(Point2D point, boolean select) {
1160 for(int i = 0; i < lines.length; i++) {
1161 Line line = lines[i];
1162 Rectangle2D bounds = line.abbox;
1163 // Add heights of bboxes for determining the correct line
1165 lineY = bounds.getY();
1167 lineY += lines[i-1].abbox.getHeight();
1169 double lineHeight = bounds.getHeight();
1170 double hitY = point.getY() / scale;
1171 if(hitY >= lineY && hitY <= lineY + lineHeight) {
1172 // Hit is in this line
1173 float x = (float)(point.getX() / scale) - (float)line.abbox.getX();
1174 float y = (float)(point.getY() / scale - lineHeight * i) ;
1175 TextHitInfo info = line.layout.hitTestChar(x, y);
1176 caret = line.startOffset + info.getInsertionIndex();
1177 if (caret > line.endOffset)
1178 caret = line.endOffset;
1180 selectionTail = caret;
1185 invalidateXOffset();
1186 assert (caret <= text.length());
1190 public Rectangle2D getBoundsInLocal() {
1191 if(targetBounds != null)
1192 return targetBounds;
1194 return expandBounds( getTightAlignedBoundsInLocal(null) );
1197 protected Rectangle2D expandBounds(Rectangle2D r) {
1198 r.setRect(r.getX() * scale - paddingX, r.getY() * scale -paddingY, r.getWidth()*scale + paddingX + paddingX, r.getHeight()*scale + paddingY + paddingY);
1199 //System.out.println(" => " + r);
1203 protected Rectangle2D expandBoundsUnscaled(Rectangle2D r) {
1204 r.setRect(r.getX() - scaleRecip*paddingX, r.getY() -scaleRecip*paddingY, r.getWidth() + scaleRecip*paddingX + scaleRecip*paddingX, r.getHeight() + scaleRecip*paddingY + scaleRecip*paddingY);
1205 //System.out.println(" => " + r);
1209 protected Rectangle2D expandBounds(Rectangle2D r, double amount) {
1210 r.setRect(r.getX() - amount, r.getY() - amount, r.getWidth() + 2*amount, r.getHeight() + 2*amount);
1214 protected Rectangle2D expandBounds(Rectangle2D r, double left, double top, double right, double bottom) {
1215 r.setRect(r.getX() - left, r.getY() - top, r.getWidth() + left + right, r.getHeight() + top + bottom);
1219 private void resetCaches() {
1220 this.tightBoundsCache = null;
1222 this.fontMetrics = null;
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.
1233 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r) {
1234 return getTightAlignedBoundsInLocal(r, FRC);
1238 * Returns the tight bounds around the current text using the current font
1239 * in the specified rectangle. If the specified rectangle is
1240 * <code>null</code> a new Rectangle2D.Double instance will be created.
1243 * the rectangle where the result of the method is placed or
1244 * <code>null</code> to allocate new rectangle
1245 * @param frc current font render context
1246 * @return r or new Rectangle2D.Double instance containing the requested
1249 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r, FontRenderContext frc) {
1251 r = new Rectangle2D.Double();
1253 if (tightBoundsCache != null) {
1254 r.setFrame(tightBoundsCache);
1259 if (font == null || txt == null) {
1260 r.setFrame(0, 0, 2, 1);
1264 //System.out.println("TextNode.getTightAlignedBoundsInLocal('" + txt + "')");
1266 // Parse & layout (unaligned)
1267 Line[] lines = null;
1269 if(hasState(STATE_WRAP_TEXT)) {
1270 float width = fixedWidth;
1271 if(width <= 0 && targetBounds != null)
1272 width = (float) (((targetBounds.getWidth() - 2*paddingX)) * scaleRecip);
1274 lines = wrapLines(txt, font, width, frc);
1278 lines = parseLines(txt);
1279 this.lines = layoutLines(lines, frc);
1281 // Calculate tight bounds based on unaligned layout
1282 //System.out.println("Unaligned");
1283 tightBoundsCache = calculateBounds(lines, Line.BBOX, null);
1284 //System.out.println(" => " + tightBoundsCache);
1286 this.lines = layoutLinesX(lines, tightBoundsCache);
1287 // Align each line to the calculated tight bounds
1288 this.lines = alignLines(this.lines, tightBoundsCache, horizontalAlignment, verticalAlignment);
1290 // Calculate aligned bounds
1291 //System.out.println("Aligned");
1292 calculateBounds(lines, Line.ABBOX, tightBoundsCache);
1294 r.setFrame(tightBoundsCache);
1295 //System.out.println(" => " + tightBoundsCache);
1303 * the bounding box of all the whole laid out text (only bbox
1307 private Line[] layoutLinesX(Line[] lines, Rectangle2D bbox) {
1308 int lineCount = lines.length;
1309 for (int l = 0; l < lineCount; ++l) {
1310 Line line = lines[l];
1311 // Compute pen x position. If the paragraph is right-to-left we
1312 // will align the TextLayouts to the right edge of the panel.
1313 // Note: drawPosX is always where the LEFT of the text is placed.
1314 // NOTE: This changes based on horizontal alignment
1315 line.drawPosX = (float) (line.layout.isLeftToRight() ? 0f
1316 : tightBoundsCache.getWidth() - line.layout.getAdvance());
1323 * @param boundsProvider
1327 private static Rectangle2D calculateBounds(Line[] lines, BoundsProcedure boundsProvider, Rectangle2D result) {
1329 result = new Rectangle2D.Double();
1331 result.setFrame(0, 0, 0, 0);
1333 for (Line line : lines) {
1334 //System.out.println("line: " + line);
1335 Rectangle2D bbox = boundsProvider.getBounds(line);
1336 if (result.isEmpty())
1337 result.setFrame(bbox);
1339 Rectangle2D.union(result, bbox, result);
1340 //System.out.println("bounds: " + result);
1342 //System.out.println("final bounds: " + result);
1352 * @return aligned lines
1354 private Line[] alignLines(Line[] lines, Rectangle2D bbox, byte hAlign, byte vAlign) {
1355 // System.out.println("horizontal align: " + Alignment.values()[hAlign]);
1356 // System.out.println("vertical align : " + Alignment.values()[vAlign]);
1357 // System.out.println("bbox: " + bbox);
1359 // double ybase = 0;
1360 if(targetBounds != null) {
1361 /* In normal cases the bounding box moves when
1362 * typing. If target bounds are set, the text
1363 * is fitted into the box.
1367 xbase = (targetBounds.getMaxX()-paddingX) * scaleRecip;
1370 xbase = targetBounds.getCenterX() * scaleRecip;
1372 default: // Leading / Baseline
1379 for (Line line : lines) {
1385 xoffset = xbase - line.bbox.getWidth();
1388 xoffset = xbase - line.bbox.getWidth() / 2;
1390 default: // Leading / Baseline
1397 yoffset = line.layout.getAscent();
1400 yoffset = -bbox.getHeight() + line.layout.getAscent();
1403 yoffset = -bbox.getHeight() / 2 + line.layout.getAscent();
1407 line.alignOffset(xoffset, yoffset);
1417 private Line[] layoutLines(Line[] lines, FontRenderContext frc) {
1418 TextLayout emptyRowLayout = null;
1419 int lineCount = lines.length;
1421 for (int l = 0; l < lineCount; ++l) {
1422 Line line = lines[l];
1423 String lineText = line.getText();
1424 // " " because TextLayout requires non-empty text and
1425 // We don't want zero size for the text.
1426 if (lineText.isEmpty()) {
1428 if (emptyRowLayout == null)
1429 emptyRowLayout = new TextLayout(lineText, font, frc);
1430 line.layout = emptyRowLayout;
1432 line.layout = new TextLayout(lineText, font, frc);
1435 //y += line.layout.getAscent();
1437 y += line.layout.getDescent() + line.layout.getLeading() + line.layout.getAscent();
1439 Rectangle2D bbox = line.layout.getLogicalHighlightShape(0, lineText.length()).getBounds2D();
1440 bbox.setFrame(bbox.getX(), bbox.getY() + line.drawPosY, bbox.getWidth(), bbox.getHeight());
1448 * Splits the specified string into {@link Line} structures, one for each
1449 * line in the input text. The returned lines are only partially defined,
1450 * waiting to be laid out (see
1451 * {@link #layoutLines(Line[], FontRenderContext)})
1455 * @return parsed text lines as {@link Line} structures
1456 * @see #layoutLines(Line[], FontRenderContext)
1458 private static Line[] parseLines(String txt) {
1459 int len = txt.length();
1461 return new Line[] { new Line("", 0, 0) };
1463 TIntArrayList lfpos = new TIntArrayList();
1466 for (;pos < len; ++lineCount) {
1467 int nextlf = txt.indexOf('\n', pos);
1468 lfpos.add(nextlf != -1 ? nextlf : len);
1473 Line[] lines = new Line[lineCount];
1475 for (int i = 0; i < lineCount-1; ++i) {
1476 int lf = lfpos.getQuick(i);
1477 int cr = (lf > 0) && (txt.charAt(lf - 1) == '\r') ? lf - 1 : lf;
1478 lines[i] = new Line(txt, pos, cr);
1481 lines[lineCount - 1] = new Line(txt, pos, len);
1487 private static Line[] wrapLines(String txt, Font font, float fixedWidth, FontRenderContext frc) {
1488 if(txt == null || txt.isEmpty())
1491 ArrayList<Line> lines =
1492 new ArrayList<Line>();
1494 Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
1495 map.put(TextAttribute.FONT, font);
1496 AttributedString attributedText = new AttributedString(txt.isEmpty() ? "__ERROR__" : txt, map);
1498 AttributedCharacterIterator paragraph = attributedText.getIterator();
1499 int paragraphStart = paragraph.getBeginIndex();
1500 int paragraphEnd = paragraph.getEndIndex();
1501 LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
1503 float breakWidth = fixedWidth;
1505 // Force text to be vertical, by setting break width to 1, if the text area is narrower than "GGGG"
1507 // Set position to the index of the first character in the paragraph.
1508 lineMeasurer.setPosition(paragraphStart);
1510 // Get lines until the entire paragraph has been displayed.
1511 int next, limit, charat, position = 0;
1513 while ((position = lineMeasurer.getPosition()) < paragraphEnd) {
1515 // Find possible line break and set it as a limit to the next layout
1516 next = lineMeasurer.nextOffset(breakWidth);
1518 charat = txt.indexOf(System.getProperty("line.separator"),position+1);
1519 if(charat < next && charat != -1){
1523 lineMeasurer.nextLayout(breakWidth, limit, false);
1525 lines.add(new Line(txt, position, limit));
1528 return lines.toArray(new Line[lines.size()]);
1532 public String getClipboardContent() {
1533 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1534 Transferable clipData = clipboard.getContents(this);
1536 return (String) (clipData.getTransferData(DataFlavor.stringFlavor));
1537 } catch (Exception ee) {
1542 public void setClipboardContent(String content) {
1543 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1544 StringSelection data = new StringSelection(content);
1545 clipboard.setContents(data, data);
1549 public String toString() {
1550 return super.toString() + " [text=" + text + ", font=" + font + ", color=" + color + "]";
1554 protected boolean handleCommand(CommandEvent e) {
1555 if (!hasState(STATE_EDITING))
1558 if (Commands.SELECT_ALL.equals(e.command)) {
1566 protected boolean keyPressed(KeyPressedEvent event) {
1567 if (!hasState(STATE_EDITING))
1570 char c = event.character;
1571 boolean ctrl = event.isControlDown();
1572 boolean alt = event.isAltDown();
1574 // System.out.println("Key pressed '" + (event.character == 0 ? "\\0" : "" + c) + "' " + event.keyCode + " - " + Integer.toBinaryString(event.stateMask));
1575 // System.out.println("ctrl: " + ctrl);
1576 // System.out.println("alt: " + alt);
1578 switch (event.keyCode) {
1580 if (caret != selectionTail) {
1581 int selectionMin = Math.min(caret, selectionTail);
1582 int selectionMax = Math.max(caret, selectionTail);
1583 setClipboardContent(text.substring(selectionMin, selectionMax));
1588 if (caret != selectionTail) {
1589 int selectionMin = Math.min(caret, selectionTail);
1590 int selectionMax = Math.max(caret, selectionTail);
1591 setClipboardContent(text.substring(selectionMin, selectionMax));
1596 case KeyEvent.VK_RIGHT:
1598 // '\'' has the same keycode as VK_RIGHT but when right
1599 // arrow is pressed, event character is \0.
1600 moveCaretCtrlRight(event.isShiftDown());
1604 case KeyEvent.VK_LEFT:
1605 moveCaretCtrlLeft(event.isShiftDown());
1610 String content = getClipboardContent();
1616 // Replaced by #handleCommand
1617 // case KeyEvent.VK_A:
1623 case KeyEvent.VK_ENTER:
1625 insert(getLineSeparator());
1633 } else if (!ctrl && alt) {
1636 switch (event.keyCode) {
1637 case KeyEvent.VK_LEFT:
1638 moveCaret(-1, event.isShiftDown());
1640 case KeyEvent.VK_RIGHT:
1642 // '\'' has the same keycode as VK_RIGHT but when right
1643 // arrow is pressed, event character is \0.
1644 moveCaret(1, event.isShiftDown());
1647 // Intentional fallthrough to default case
1648 case KeyEvent.VK_UP:
1649 moveCaretRowUp(event.isShiftDown());
1651 case KeyEvent.VK_DOWN:
1652 moveCaretRowDown(event.isShiftDown());
1654 case KeyEvent.VK_HOME:
1655 moveCaretHome(event.isShiftDown());
1657 case KeyEvent.VK_END:
1658 moveCaretEnd(event.isShiftDown());
1661 case KeyEvent.VK_ENTER:
1662 fireTextEditingEnded();
1665 case KeyEvent.VK_ESCAPE:
1666 text = textBeforeEdit;
1668 clearState(STATE_EDITING);
1669 fireTextEditingCancelled();
1672 case KeyEvent.VK_BACK_SPACE:
1673 if(caret == selectionTail && caret > 0) {
1674 // line separator may use multiple characters, we want to remove that with one command
1675 String lineSep = getLineSeparator();
1676 int index = lineSep.indexOf(text.charAt(caret-1));
1681 selectionTail+= (lineSep.length()-index-1);
1687 case KeyEvent.VK_DELETE:
1688 if(caret == selectionTail && caret < text.length()) {
1689 String lineSep = getLineSeparator();
1690 int index = lineSep.indexOf(text.charAt(caret));
1694 selectionTail-= index;
1695 caret+= (lineSep.length()-index);
1704 if (c == 65535 || Character.getType(c) == Character.CONTROL) {
1707 //System.out.println("Char " + c + " " + Character.getType(c) + " " + text);
1708 insert(new String(new char[] {c}));
1712 // FIXME This is called even if just caret was moved.
1713 // This is currently necessary for repaints.
1715 invalidateXOffset();
1719 protected String getLineSeparator() {
1720 return System.getProperty("line.separator");
1723 protected void selectAll() {
1725 setCaret(text.length(), true);
1728 protected transient int hoverClick = 0;
1731 protected boolean mouseClicked(MouseClickEvent event) {
1732 if (event.button != MouseClickEvent.LEFT_BUTTON)
1735 if (hasState(STATE_HOVER)) {
1739 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1740 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1743 IElement e = DiagramNodeUtil.getElement(ctx, this);
1744 if (!hasState(STATE_EDITING)) {
1745 if (Boolean.TRUE.equals(setEditMode(true))) {
1746 editActivation = activateEdit(0, e, ctx);
1752 if (hasState(STATE_EDITING)) {
1753 fireTextEditingEnded();
1759 protected boolean mouseDoubleClicked(MouseDoubleClickedEvent event) {
1760 if (event.button != MouseClickEvent.LEFT_BUTTON)
1763 if (hitTest(event, 0)) {
1764 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1765 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1770 // Select the whole text.
1772 setCaret(text.length(), true);
1781 protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {
1782 if (!hasState(STATE_EDITING))
1785 Point2D local = controlToLocal( event.controlPosition );
1786 // FIXME: once the event coordinate systems are cleared up, remove this workaround
1787 local = parentToLocal(local);
1788 if (hasState(STATE_HOVER) && this.containsLocal(local)) {
1789 setCaret(local, event.isShiftDown());
1795 protected boolean mouseMoved(MouseMovedEvent event) {
1796 boolean hit = hitTest(event, 3.0);
1797 if (hit != hasState(STATE_HOVER)) {
1798 setState(STATE_HOVER, hit);
1804 private boolean isControlDown(MouseEvent e) {
1805 return e.isControlDown() || lastMouseEvent!=null?lastMouseEvent.isControlDown():false;
1808 protected boolean isShiftDown(MouseEvent e) {
1809 return e.isShiftDown() || lastMouseEvent!=null?lastMouseEvent.isShiftDown():false;
1812 // private boolean isAltDown(MouseEvent e) {
1813 // return e.isAltDown() || lastMouseEvent!=null?lastMouseEvent.isAltDown():false;
1817 protected boolean mouseDragged(MouseDragBegin e) {
1819 && (isControlDown(e) || isShiftDown(e))
1820 && (dataRVI != null || text != null))
1822 List<Transferable> trs = new ArrayList<>(2);
1823 if (dataRVI != null) {
1824 trs.add(new LocalObjectTransferable(dataRVI));
1825 trs.add(new PlaintextTransfer(dataRVI.toString()));
1826 } else if (text != null && !text.isEmpty()) {
1827 trs.add(new PlaintextTransfer(text));
1829 if (!trs.isEmpty()) {
1830 e.transferable = new MultiTransferable(trs);
1837 protected boolean hitTest(MouseEvent event, double tolerance) {
1838 Rectangle2D bounds = getBoundsInternal();
1841 Point2D localPos = NodeUtil.worldToLocal(this, event.controlPosition, new Point2D.Double());
1842 double x = localPos.getX();
1843 double y = localPos.getY();
1844 boolean hit = bounds.contains(x, y);
1848 public Rectangle2D getBoundsInternal() {
1849 Rectangle2D local = lastBounds;
1852 // TODO: potential spot for CPU/memory allocation optimization
1853 // by using more specialized implementations
1854 if (transform.isIdentity())
1856 return transform.createTransformedShape(local).getBounds2D();
1859 protected Color add(Color c, int r, int g, int b) {
1860 int nr = Math.min(255, c.getRed() + r);
1861 int ng = Math.min(255, c.getGreen() + g);
1862 int nb = Math.min(255, c.getBlue() + b);
1863 return new Color(nr, ng, nb);
1866 public TextEditActivation activateEdit(int mouseId, IElement e, ICanvasContext ctx) {
1867 EditDataNode data = EditDataNode.getNode(this);
1868 deactivateEdit(data, null);
1869 TextEditActivation result = new TextEditActivation(mouseId, e, ctx);
1870 data.setTextEditActivation(result);
1875 * @return <code>true</code> if this node is or was previously in editing
1878 protected boolean deactivateEdit() {
1879 boolean result = deactivateEdit( editActivation );
1880 result |= editActivation != null;
1881 editActivation = null;
1885 protected boolean deactivateEdit(TextEditActivation activation) {
1886 return deactivateEdit( EditDataNode.getNode(this), activation );
1889 protected boolean deactivateEdit(EditDataNode data, TextEditActivation activation) {
1890 TextEditActivation previous = data.getTextEditActivation();
1891 if (previous != null && (previous == activation || activation == null)) {
1893 data.setTextEditActivation(null);
1900 public int getEventMask() {
1901 return EventTypes.KeyPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask
1902 | EventTypes.MouseClickMask | EventTypes.MouseDragBeginMask | EventTypes.CommandMask;
1905 private MouseEvent lastMouseEvent = null;
1908 public boolean handleEvent(Event e) {
1909 if(e instanceof MouseEvent && !(e instanceof MouseDragBegin)) lastMouseEvent = (MouseEvent)e;
1910 return super.handleEvent(e);
1914 public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
1915 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
1919 public <T> T getProperty(String propertyName) {
1924 public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
1927 public void synchronizeText(String text) {
1931 public void synchronizeColor(RGB.Integer color) {
1932 this.color = Colors.awt(color);
1935 public void synchronizeFont(org.simantics.datatypes.literal.Font font) {
1936 setFont(Fonts.awt(font));
1939 public void synchronizeTransform(double[] data) {
1940 this.setTransform(new AffineTransform(data));
1943 public static void main(String[] args) {
1944 Line[] lines = parseLines("\n \n FOO \n\nBAR\n\n\n BAZ\n\n");
1945 System.out.println(Arrays.toString(lines));
1946 System.out.println(GeometryUtils.pointToMillimeter(1));
1947 System.out.println(GeometryUtils.pointToMillimeter(12));
1948 System.out.println(GeometryUtils.pointToMillimeter(72));
1951 ///////////////////////////////////////////////////////////////////////////
1952 // LEGACY CODE NEEDED BY INHERITED CLASSES FOR NOW
1953 ///////////////////////////////////////////////////////////////////////////
1955 protected double getHorizontalAlignOffset(Rectangle2D r) {
1956 switch (horizontalAlignment) {
1957 case 0: return 0; // Leading
1958 case 1: return -r.getWidth(); // Trailing
1959 case 2: return -r.getCenterX(); // Center
1964 protected double getVerticalAlignOffset() {
1965 FontMetrics fm = fontMetrics;
1968 switch (verticalAlignment) {
1969 case 0: return fm.getMaxAscent(); // Leading=top=maxascent
1970 case 1: return -fm.getMaxDescent(); // Trailing=bottom=maxdescent
1971 case 2: return fm.getMaxAscent() / 2; // Center=maxascent / 2
1977 ///////////////////////////////////////////////////////////////////////////
1979 ///////////////////////////////////////////////////////////////////////////