]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.diagram/src/org/simantics/diagram/elements/TextNode.java
Fixed synchronousness problem in drag event transferable handling
[simantics/platform.git] / bundles / org.simantics.diagram / src / org / simantics / diagram / elements / TextNode.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management
3  * in Industry THTH ry.
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
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.diagram.elements;
13
14 import java.awt.AlphaComposite;
15 import java.awt.BasicStroke;
16 import java.awt.Color;
17 import java.awt.Composite;
18 import java.awt.Font;
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;
43
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;
77
78 import com.lowagie.text.pdf.PdfWriter;
79
80 import gnu.trove.list.array.TIntArrayList;
81
82
83 /**
84  * TextNode which supports in-line editing.
85  * 
86  * By default <code>TextNode</code> is in editable = false state. Use
87  * {@link #setEditable(boolean)} to make it editable.
88  * 
89  * @author Hannu Niemist&ouml; <hannu.niemisto@vtt.fi>
90  * @author Marko Luukkainen <marko.luukkainen@vtt.fi>
91  * @author Tuukka Lehtonen <tuukka.lehtonen@semantum.fi>
92  * 
93  * TODO:
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
96  * 
97  * @see Line
98  * @see TextLayout
99  */
100 public class TextNode extends G2DNode implements IDynamicSelectionPainterNode, LoaderNode {
101
102     private static final long                serialVersionUID           = 654692698101485672L;
103
104     public static enum TextFlipping {
105         Disabled,
106         VerticalTextUpwards,
107         VerticalTextDownwards,
108     }
109
110     /**
111      * TODO: justify existence for this
112      */
113     private static final BasicStroke         RESET_STROKE               = new BasicStroke(1);
114
115     /**
116      * Src-over alpha composite instance with 50% opacity.
117      */
118     private static final AlphaComposite      SrcOver_50                 = AlphaComposite.SrcOver.derive(0.5f);
119
120     /**
121      * For (inexact) measurement of rendered text bounds.
122      */
123     protected static final FontRenderContext FRC = new FontRenderContext(new AffineTransform(), true, true);
124
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;
128
129     /**
130      * The complete text visualized by this node.
131      */
132     protected String text = null;
133
134     /**
135      * The font used to render the {@link #text}.
136      */
137     protected Font font = FONT;
138
139     /**
140      * The color of the rendered text. Default value is {@value Color#black}.
141      */
142     protected Color color = Color.BLACK;
143
144     /**
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>.
148      */
149     protected Color backgroundColor = null;
150
151     /**
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>.
155      */
156     protected Color borderColor = null;
157
158     protected double scale = 1.0;
159     protected transient double scaleRecip = 1.0;
160
161     /**
162      * 
163      */
164     protected float borderWidth = 0.f;
165
166     protected double paddingX = 2.0;
167     protected double paddingY = 2.0;
168
169     /**
170      * Horizontal text box alignment with respect to its origin. Default value is
171      * 0 (leading).
172      */
173     protected byte horizontalAlignment = 0;
174     /**
175      * Vertical text box alignment with respect to its origin. Default value is
176      * 3 (baseline).
177      */
178     protected byte verticalAlignment = 3;
179
180     /**
181      * Tells if this node is still pending for real results or not.
182      */
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);
195
196     /**
197      * A combination of all the STATE_ constants defined in this class,
198      * e.g. {@link #STATE_PENDING}.
199      */
200     protected int state = STATE_SHOW_SELECTION | STATE_WRAP_TEXT | STATE_VALID | STATE_X_OFFSET_IS_DIRTY;
201
202     protected RVI dataRVI = null;
203
204     int caret = 0;
205     int selectionTail = 0;
206     float xOffset = 0;
207
208     float fixedWidth = 0f;
209
210     private Rectangle2D targetBounds;
211
212     Function1<String, String> validator;
213     ITextListener textListener;
214     ITextContentFilter editContentFilter;
215
216     /**
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)}
221      */
222     protected transient Line[]           lines                      = null;
223     protected transient FontMetrics      fontMetrics                = null;
224
225     /**
226      * Stores the value of {@link #text} before edit mode was last entered. Used
227      * for restoring the original value if editing is cancelled.
228      */
229     private transient String             textBeforeEdit             = null;
230     protected transient TextEditActivation editActivation;
231
232     /**
233      * Stores the last scaled bounds.
234      */
235     private transient Rectangle2D        lastBounds = new Rectangle2D.Double();
236
237     /**
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
241      * request.
242      */
243     private transient Rectangle2D        tightBoundsCache = null;
244
245     @Override
246     public void init() {
247         super.init();
248         // Mark this node as pending
249         NodeUtil.increasePending(this);
250     }
251
252     @Override
253     public void cleanup() {
254         removeListeners();
255         super.cleanup();
256     }
257
258     protected boolean hasState(int flags) {
259         return (state & flags) == flags;
260     }
261
262     protected void setState(int flags) {
263         this.state |= flags;
264     }
265
266     protected void setState(int flags, boolean set) {
267         if (set)
268             this.state |= flags;
269         else
270             this.state &= ~flags;
271     }
272
273     protected void clearState(int flags) {
274         this.state &= ~flags;
275     }
276
277     protected void setListeners(boolean add) {
278         if (add)
279             addListeners();
280         else
281             removeListeners();
282     }
283
284     protected void addListeners() {
285         if (!hasState(STATE_LISTENERS_ADDED)) {
286             addEventHandler(this);
287             setState(STATE_LISTENERS_ADDED);
288         }
289     }
290
291     protected void removeListeners() {
292         if (hasState(STATE_LISTENERS_ADDED)) {
293             removeEventHandler(this);
294             clearState(STATE_LISTENERS_ADDED);
295         }
296     }
297
298     /**
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 
300      * @param force
301      */
302     public void setForceEventListening(boolean force) {
303         setState(STATE_ALWAYS_ADD_LISTENERS, force);
304         if (force && !hasState(STATE_EDITABLE)) {
305             setListeners(force);
306         }
307     }
308
309     /**
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).
314      * @param edit
315      * @return null if no change to edit state was made
316      */
317     public Boolean setEditMode(boolean edit) {
318         return setEditMode(edit, true);
319     }
320
321     /**
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).
326      * @param edit
327      * @return null if no change to edit state was made
328      */
329     protected Boolean setEditMode(boolean edit, boolean notify) {
330         if (edit && !hasState(STATE_EDITABLE))
331             return null;
332         if (hasState(STATE_EDITING) == edit)
333             return null;
334         setState(STATE_EDITING, edit);
335         if (edit) {
336             caret = text != null ? text.length() : 0;
337             selectionTail = 0;
338             textBeforeEdit = text;
339             if (notify)
340                 fireTextEditingStarted();
341             return Boolean.TRUE;
342         } else {
343             if (notify)
344                 fireTextEditingEnded();
345             return Boolean.FALSE;
346         }
347     }
348
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)
354             setEditMode(false);
355         if (changed && !hasState(STATE_ALWAYS_ADD_LISTENERS)) {
356             setListeners(editable);
357         }
358     }
359
360     public boolean isEditable() {
361         return hasState(STATE_EDITABLE);
362     }
363
364     public boolean isEditMode() {
365         return hasState(STATE_EDITING);
366     }
367     
368     @SyncField({"wrapText"})
369     public void setWrapText(boolean wrapText) {
370         setState(STATE_WRAP_TEXT, wrapText);
371     }
372     
373     /**
374      * @return Does the text box wrap text if 
375      * the width of the box is fixed
376      */
377     public boolean isWrapText() {
378         return hasState(STATE_WRAP_TEXT);
379     }
380
381     @SyncField({"showSelection"})
382     public void setShowSelection(boolean showSelection) {
383         setState(STATE_SHOW_SELECTION, showSelection);
384     }
385
386     public boolean showsSelection() {
387         return hasState(STATE_SHOW_SELECTION);
388     }
389
390     /**
391      * @param text
392      * @param font
393      * @param color
394      * @param x not supported anymore, use {@link #setTransform(AffineTransform)} instead
395      * @param y not supported anymore, use {@link #setTransform(AffineTransform)} instead
396      * @param scale
397      */
398     @SyncField({"text", "font", "color", "x", "y", "scale"})
399     public void init(String text, Font font, Color color, double x, double y, double scale) {
400         // no value => value
401         if(this.text == null && text != null) NodeUtil.decreasePending(this);
402
403         if (hasState(STATE_EDITING))
404             return;
405
406         this.text = new String(text != null ? text : "");
407         this.font = font;
408         this.color = color;
409         this.scale = scale;
410         this.scaleRecip = 1.0 / scale;
411         this.caret = 0;
412         this.selectionTail = 0;
413
414         resetCaches();
415     }
416
417     public void setAutomaticTextFlipping(TextFlipping type) {
418         switch (type) {
419         case Disabled:
420             clearState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED | STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
421             break;
422         case VerticalTextDownwards:
423             setState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED | STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
424             break;
425         case VerticalTextUpwards:
426             setState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED);
427             clearState(STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
428             break;
429         }
430     }
431
432     @SyncField({"paddingX", "paddingY"})
433     public void setPadding(double x, double y) {
434         this.paddingX = x;
435         this.paddingY = y;
436     }
437
438     @SyncField({"color"})
439     public void setColor(Color color) {
440         this.color = color;
441     }
442
443     @SyncField({"backgroundColor"})
444     public void setBackgroundColor(Color color) {
445         this.backgroundColor = color;
446     }
447
448     @SyncField({"borderColor"})
449     public void setBorderColor(Color color) {
450         this.borderColor = color;
451     }
452
453     public String getText() {
454         return text;
455     }
456     
457     public String getTextBeforeEdit() {
458         return textBeforeEdit;
459     }
460
461     @SyncField({"text","caret","selectionTail"})
462     public void setText(String text) {
463         //System.out.println("TextNode.setText('" + text + "', " + editing + ")");
464         if (hasState(STATE_EDITING))
465             return;
466
467         // value => no value
468         if(this.text != null && text == null) NodeUtil.increasePending(this);
469         // no value => value
470         if(this.text == null && text != null) NodeUtil.decreasePending(this);
471
472         this.text = text != null ? text : "";
473         caret = Math.min(caret, this.text.length());
474         selectionTail = caret;
475
476         resetCaches();
477     }
478
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);
484         if(p != pending)
485             setState(STATE_PENDING, pending);
486     }
487
488     @SyncField({"fixedWidth"})
489     public void setFixedWidth(float fixedWidth) {
490         if (fixedWidth < 0f)
491             throw new IllegalArgumentException("negative fixed width");
492         this.fixedWidth = fixedWidth;
493         invalidateXOffset();
494     }
495     
496     /**
497      * Bounds where the text box will be drawn
498      * @param bounds
499      */
500     public void setTargetBounds(Rectangle2D bounds) {
501         this.targetBounds = bounds;
502     }
503
504         final public void synchronizeWidth(float width) {
505                 if (width >= 0.0f)
506                         setFixedWidth(width);
507         }
508
509         final public void synchronizeBorderWidth(float width) {
510                 if (width >= 0.0f)
511                         setBorderWidth(width);
512         }
513
514     public final void synchronizeWrapText(boolean wrap) {
515         setState(STATE_WRAP_TEXT, wrap);
516     }
517
518     public boolean isHovering() {
519         return hasState(STATE_HOVER);
520     }
521
522     @SyncField({"hover"})
523     public void setHover(boolean hover) {
524         setState(STATE_HOVER, hover);
525         repaint();
526     }
527
528     public Font getFont() {
529         return font;
530     }
531
532     @SyncField({"font"})
533     public void setFont(Font font) {
534         this.font = font;
535         resetCaches();
536     }
537
538     public double getBorderWidth() {
539         return borderWidth;
540     }
541
542     @SyncField({"borderWidth"})
543     public void setBorderWidth(float width) {
544         this.borderWidth = width;
545     }
546
547     public void setBorderWidth(double width) {
548         setBorderWidth((float)width);
549     }
550
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;
556         resetCaches();
557     }
558
559     final public void synchronizeHorizontalAlignment(byte horizontalAlignment) {
560         if (horizontalAlignment >= 0 && horizontalAlignment <= 2)
561             setHorizontalAlignment(horizontalAlignment);
562     }
563
564     public byte getHorizontalAlignment() {
565         return horizontalAlignment;
566     }
567
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;
573         resetCaches();
574     }
575
576     final public void synchronizeVerticalAlignment(byte verticalAlignment) {
577         if (verticalAlignment >= 0 && verticalAlignment <= 3)
578             setVerticalAlignment(verticalAlignment);
579     }
580
581     public byte getVerticalAlignment() {
582         return verticalAlignment;
583     }
584
585     /**
586      * Rendering is single-threaded so we can use a static rectangle for
587      * calculating the expanded bounds for the node.
588      */
589     private static transient ThreadLocal<Rectangle2D> tempBounds = new ThreadLocal<Rectangle2D>() {
590         @Override
591         protected Rectangle2D initialValue() {
592             return new Rectangle2D.Double();
593         }
594     };
595
596     /**
597      * Rendering is single-threaded so we can use a static AffineTransform to
598      * prevent continuous memory allocation during text rendering.
599      */
600     private static transient ThreadLocal<AffineTransform> tempAffineTransform = new ThreadLocal<AffineTransform>() {
601         @Override
602         protected AffineTransform initialValue() {
603             return new AffineTransform();
604         }
605     };
606
607     @Override
608     public void render(Graphics2D g) {
609         AffineTransform ot = g.getTransform();
610         render(g, true);
611         g.setTransform(ot);
612     }
613
614     /**
615      * Note: does not return transformation, stroke, color, etc. to their
616      * original states
617      * 
618      * @param g
619      * @param applyTransform
620      */
621     public void render(Graphics2D g, boolean applyTransform) {
622         if (text == null || font == null || color == null)
623             return;
624
625         // Cache font metrics if necessary
626         if (fontMetrics == null)
627             fontMetrics = g.getFontMetrics(font);
628
629         Color color = this.color;
630         boolean isSelected = NodeUtil.isSelected(this, 1);
631         boolean hover = hasState(STATE_HOVER);
632         boolean editing = hasState(STATE_EDITING);
633
634         if (!isSelected && hover) {
635             color = add(color, 120, 120, 120);
636         }
637
638         if (applyTransform)
639             g.transform(transform);
640         // Apply separate legacy scale
641         if (scale != 1.0)
642             g.scale(scale, scale);
643
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
646         // stroking fonts.
647         AffineTransform curTr = g.getTransform();
648         double currentScale = GeometryUtils.getScale(curTr);
649         //System.out.println("currentScale: " + currentScale);
650         if (currentScale < 1e-6)
651             return;
652
653         g.setFont(font);
654         //g.translate(x, y);
655
656         // Calculate text clip rectangle.
657         // This updates textLayout if necessary.
658         Rectangle2D r = getTightAlignedBoundsInLocal(tempBounds.get(), fontMetrics.getFontRenderContext());
659
660         computeEditingXOffset();
661
662         if (fixedWidth > 0f)
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);
670         }
671
672         if (hasState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED)) {
673             boolean needsXFlip;
674             boolean needsYFlip;
675             if (curTr.getScaleX() != 0) {
676                 needsXFlip = curTr.getScaleX() < 0.0;
677                 needsYFlip = curTr.getScaleY() < 0.0;
678             } else {
679                 boolean flipAll = !hasState(STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
680                 needsXFlip = (curTr.getShearY() < 0.0) ^ flipAll;
681                 needsYFlip = (curTr.getShearX() > 0.0) ^ flipAll;
682             }
683             if (needsXFlip || needsYFlip) {
684                 double centerX = r.getWidth()*0.5 + r.getX();
685                 double centerY = r.getHeight()*0.5 + r.getY();
686
687                 g.translate(centerX, centerY);
688                 g.scale(needsXFlip ? -1.0 : 1.0, needsYFlip ? -1.0 : 1.0);
689                 g.translate(-centerX, -centerY);
690             }
691         }
692
693         Rectangle2D textClip = r.getBounds2D();
694
695         expandBoundsUnscaled(r);
696
697         // Speed rendering optimization: don't draw text that is too small to
698         // read when not editing
699         boolean renderText = true;
700         if (!editing) {
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)
705                     renderText = false;
706             }
707         }
708
709         Shape clipSave = g.getClip();
710         g.setClip(textClip);
711
712         // PDF 
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;
716         /// PDF
717
718         Color backgroundColor = hasState(STATE_VALID) ? this.backgroundColor : Color.red;
719
720         // RENDER
721         {
722             // Fill background if necessary
723             if (backgroundColor != null) {
724                 g.setColor(backgroundColor);
725                 g.fill(r);
726             }
727
728             if (editing) {
729
730                 int selectionMin = Math.min(caret, selectionTail);
731                 int selectionMax = Math.max(caret, selectionTail);
732
733                 // Base text
734                 g.setColor(color);
735                 renderText(g, xOffset, renderAsText);
736
737                 Shape clip = g.getClip();
738
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);
744                         g.setClip(selShape);
745                         g.setColor(SELECTION_BACKGROUND_COLOR);
746                         g.fill(selShape);
747                         g.setColor(Color.WHITE);
748                         // #6459: render as text in PDF and paths on screen
749                         if (renderAsText)
750                             g.drawString(line.getText(), 0, 0);
751                         else
752                             line.layout.draw(g, 0, 0);
753                         line.translateInv(g, xOffset, 0);
754                     }
755                 }
756
757                 g.setClip(clip);
758
759                 renderCaret(g);
760
761             } else {
762
763                 if (renderText) {
764                     g.setColor(color);
765                     renderText(g, 0, renderAsText);
766                 }
767
768             }
769         }
770
771         g.setClip(clipSave);
772
773         if (borderWidth > 0f && borderColor != null) {
774             g.setColor(borderColor);
775             g.setStroke(new BasicStroke((float) (scale*borderWidth)));
776             g.draw(r);
777         }
778
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;
786             if (bw <= 0f) {
787                 bw = (float) (1f / s);
788             } else {
789                 bw *= 5f * scale;
790             }
791             g.setStroke(new BasicStroke(bw));
792             g.draw(r);
793             //g.draw(GeometryUtils.expandRectangle(r, 1.0));
794
795             g.setComposite(oc);
796         }
797
798         g.scale(scaleRecip, scaleRecip);
799         g.setStroke(RESET_STROKE);
800
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
806
807         renderSelectedHover(g, isSelected, hover);
808     }
809
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);
825           }
826         }
827     }
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
832             if (renderAsText)
833                 g.drawString(line.getText(), line.alignedPosX + xOffset, line.alignedPosY);
834             else
835                 line.layout.draw(g, line.alignedPosX + xOffset, line.alignedPosY);
836             //g.draw(line.abbox); // DEBUG
837         }
838     }
839
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);
846         } else {
847             dst.setFrame(btr.createTransformedShape(originalBounds).getBounds2D());
848         }
849         return dst;
850     }
851
852     /**
853      * Invoked when TextNode is selected and a mouse is hovering on top of it. Can be overridden to add custom rendering.
854      * 
855      * @param g
856      */
857     protected void renderSelectedHover(Graphics2D g, boolean isSelected, boolean isHovering) {
858     }
859
860     /**
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
864      * be empty.
865      * @param content
866      */
867     @SyncField({"text","caret","selectionTail"})
868     protected void insert(String content) {
869         content = editContentFilter != null ? editContentFilter.filter(this, content) : content; 
870
871         int selectionMin = Math.min(caret, selectionTail);
872         int selectionMax = Math.max(caret, selectionTail);
873
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;
879
880         assert (caret <= text.length());
881         //System.out.println(text + " " + caret );
882
883         if(validator != null) {
884             String error = validator.apply(text);
885             setState(STATE_VALID, (error == null));
886         }
887
888         resetCaches();
889     }
890
891     @ServerSide
892     protected void fireTextChanged() {
893         if(textListener != null)
894             textListener.textChanged();
895         repaint();
896     }
897
898     @ServerSide
899     protected void fireTextEditingStarted() {
900         if(textListener != null)
901             textListener.textEditingStarted();
902     }
903
904     @ServerSide
905     protected void fireTextEditingCancelled() {
906         setState(STATE_VALID);
907
908         if (deactivateEdit()) {
909             if (textListener != null)
910                 textListener.textEditingCancelled();
911
912             setEditMode(false, false);
913
914             if (textBeforeEdit != null)
915                 setText(textBeforeEdit);
916
917             repaint();
918         }
919     }
920
921     @ServerSide
922     public void fireTextEditingEnded() {
923         if (!hasState(STATE_VALID)) {
924             fireTextEditingCancelled();
925             setState(STATE_VALID);
926             return;
927         }
928
929         if (deactivateEdit()) {
930             if (textListener != null)
931                 textListener.textEditingEnded();
932
933             setEditMode(false, false);
934             repaint();
935         }
936     }
937
938     public void setTextListener(ITextListener listener) {
939         this.textListener = listener;
940     }
941
942     public void setValidator(Function1<String, String> validator) {
943         this.validator = validator;
944     }
945
946     public void setContentFilter(ITextContentFilter filter) {
947         this.editContentFilter = filter;
948     }
949
950     public void setRVI(RVI rvi) {
951         this.dataRVI = rvi;
952     }
953
954     private void invalidateXOffset() {
955         setState(STATE_X_OFFSET_IS_DIRTY);
956     }
957
958     private void computeEditingXOffset() {
959
960         if(lines == null) return;
961         if(!hasState(STATE_X_OFFSET_IS_DIRTY)) return;
962         if(fixedWidth > 0f) {
963
964             // TODO: implement
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]);
968 //                else xOffset = 0;
969 //            }
970
971         } else {
972
973             xOffset = 0;
974
975         }
976
977         clearState(STATE_X_OFFSET_IS_DIRTY);
978
979     }
980
981     @SyncField({"caret","selectionTail"})
982     protected void moveCaret(int move, boolean select) {
983         // prevent setting caret into line separator. 
984         if (move > 0) {
985                 while (text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
986                         move++;
987         } else if (move < 0) {
988                 while (caret+move >= 0 && text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
989                         move--;
990         }
991         caret += move;
992         if(caret < 0)
993             caret = 0;
994         if (caret > text.length())
995             caret = text.length();
996         if(!select)
997             selectionTail = caret;
998     }
999     
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) {
1005                 return line;
1006             }
1007         }
1008         return null;
1009     }
1010     
1011     /**
1012      * Moves caret to next not letter or digit
1013      * @param shiftDown
1014      */
1015     private void moveCaretCtrlLeft(boolean shiftDown) {
1016         Line line = findCaretLine();
1017         if(line != null) {
1018             int i;
1019             for(i = caret-1; i > line.startOffset; i--) {
1020                 char c = line.document.charAt(i);
1021                 if(!Character.isLetterOrDigit(c)) {
1022                     break;
1023                 }
1024             }
1025             moveCaret(i - caret, shiftDown);
1026         }
1027     }
1028     
1029     /**
1030      * Moves caret to previous non letter or digit
1031      * @param shiftDown
1032      */
1033     private void moveCaretCtrlRight(boolean shiftDown) {
1034         Line line = findCaretLine();
1035         if(line != null) {
1036             int i;
1037             for(i = caret + 1; i < line.endOffset; i++) {
1038                 char c = line.document.charAt(i);
1039                 if(!Character.isLetterOrDigit(c)) {
1040                     break;
1041                 }
1042             }
1043             moveCaret(i - caret, shiftDown);
1044         }
1045     }
1046     
1047     /**
1048      * Moves caret to line end
1049      * @param shiftDown
1050      */
1051     private void moveCaretEnd(boolean shiftDown) {
1052         Line line = findCaretLine();
1053         if(line != null)
1054             // Move caret to the end of the line
1055             moveCaret(line.endOffset - caret, shiftDown);
1056     }
1057     
1058     /**
1059      * Moves caret to beginning of a line
1060      * @param shiftDown
1061      */
1062     private void moveCaretHome(boolean shiftDown) {
1063         Line line = findCaretLine();
1064         if(line != null)
1065             // Move caret to the beginning of the line
1066             moveCaret(line.startOffset - caret, shiftDown);
1067     }
1068     
1069     /**
1070      * Moves caret one row up and tries to maintain the location
1071      * @param shiftDown
1072      */
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
1079                 if(i == 0) {
1080                     // Already on top line
1081                     // Select the beginning of the line
1082                     moveCaret(-caret, shiftDown);
1083                 } else {
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;
1089
1090                     int newPos = prevLine.startOffset + posInCurRow;
1091                     moveCaret(newPos - caret, shiftDown);
1092                 }
1093                 break;
1094             }
1095         }        
1096     }
1097     
1098     /**
1099      * Moves caret one row down and tries to maintain the location
1100      * @param shiftDown
1101      */
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);
1112                 } else {
1113                     Line prevLine = lines[i+1]; // Previous line
1114                     
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);
1124                 }
1125                 break;
1126             }
1127         }        
1128     }
1129
1130     @SyncField({"caret","selectionTail"})
1131     protected void setCaret(int pos, boolean select) {
1132         caret = pos;
1133         if (caret < 0)
1134             caret = 0;
1135         if (caret > text.length())
1136             caret = text.length();
1137         if (!select)
1138             selectionTail = caret;
1139     }
1140
1141     protected void setCaret(Point2D point) {
1142         setCaret(point, false);
1143     }
1144
1145     @SyncField({"caret","selectionTail"})
1146     protected void setCaret(Point2D point, boolean select) {
1147         double lineY = 0;
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
1152             if(i == 0)
1153                 lineY = bounds.getY();
1154             else
1155                 lineY += lines[i-1].abbox.getHeight();
1156             
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;
1167                 if (!select)
1168                     selectionTail = caret;
1169                 repaint();
1170                 break;
1171             }
1172         }
1173         invalidateXOffset();
1174         assert (caret <= text.length());
1175     }
1176     
1177     @Override
1178     public Rectangle2D getBoundsInLocal() {
1179         if(targetBounds != null)
1180             return targetBounds;
1181         else
1182             return expandBounds( getTightAlignedBoundsInLocal(null) );
1183     }
1184
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);
1188         return r;
1189     }
1190
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);
1194         return r;
1195     }
1196
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);
1199         return r;
1200     }
1201
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);
1204         return r;
1205     }
1206
1207     private void resetCaches() {
1208         this.tightBoundsCache = null;
1209         this.lines = null;
1210         this.fontMetrics = null;
1211     }
1212
1213     /**
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.
1217      * 
1218      * @param r
1219      * @return
1220      */
1221     protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r) {
1222         return getTightAlignedBoundsInLocal(r, FRC);
1223     }
1224
1225     /**
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.
1229      * 
1230      * @param r
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
1235      *         text bounds
1236      */
1237     protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r, FontRenderContext frc) {
1238         if (r == null)
1239             r = new Rectangle2D.Double();
1240
1241         if (tightBoundsCache != null) {
1242             r.setFrame(tightBoundsCache);
1243             return r;
1244         }
1245
1246         String txt = text;
1247         if (font == null || txt == null) {
1248             r.setFrame(0, 0, 2, 1);
1249             return r;
1250         }
1251
1252         //System.out.println("TextNode.getTightAlignedBoundsInLocal('" + txt + "')");
1253
1254         // Parse & layout (unaligned)
1255         Line[] lines = null;
1256         
1257         if(hasState(STATE_WRAP_TEXT)) {
1258             float width = fixedWidth;
1259             if(width <= 0 && targetBounds != null)
1260                 width = (float) (((targetBounds.getWidth() - 2*paddingX)) * scaleRecip);
1261             if(width > 0)
1262                 lines = wrapLines(txt, font, width, frc);
1263         }
1264          
1265         if(lines == null)
1266             lines = parseLines(txt);
1267         this.lines = layoutLines(lines, frc);
1268
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);
1273
1274         this.lines = layoutLinesX(lines, tightBoundsCache);
1275         // Align each line to the calculated tight bounds
1276         this.lines = alignLines(this.lines, tightBoundsCache, horizontalAlignment, verticalAlignment);
1277
1278         // Calculate aligned bounds
1279         //System.out.println("Aligned");
1280         calculateBounds(lines, Line.ABBOX, tightBoundsCache);
1281
1282         r.setFrame(tightBoundsCache);
1283         //System.out.println("  => " + tightBoundsCache);
1284
1285         return r;
1286     }
1287
1288     /**
1289      * @param lines
1290      * @param bbox
1291      *            the bounding box of all the whole laid out text (only bbox
1292      *            size is used)
1293      * @return
1294      */
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());
1305         }
1306         return lines;
1307     }
1308
1309     /**
1310      * @param lines
1311      * @param boundsProvider
1312      * @param result
1313      * @return
1314      */
1315     private static Rectangle2D calculateBounds(Line[] lines, BoundsProcedure boundsProvider, Rectangle2D result) {
1316         if (result == null)
1317             result = new Rectangle2D.Double();
1318         else
1319             result.setFrame(0, 0, 0, 0);
1320
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);
1326             else
1327                 Rectangle2D.union(result, bbox, result);
1328             //System.out.println("bounds: " + result);
1329         }
1330         //System.out.println("final bounds: " + result);
1331
1332         return result;
1333     }
1334
1335     /**
1336      * @param lines
1337      * @param bbox
1338      * @param hAlign
1339      * @param vAlign
1340      * @return aligned lines
1341      */
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);
1346         double xbase = 0;
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.
1352              */
1353             switch (hAlign) {
1354             case 1: // Trailing
1355                 xbase = (targetBounds.getMaxX()-paddingX) * scaleRecip;
1356                 break;
1357             case 2: // Center
1358                 xbase = targetBounds.getCenterX() * scaleRecip;
1359                 break;
1360             default: // Leading / Baseline
1361                 // Do nothing
1362                 break;
1363             }
1364         }
1365         
1366         
1367         for (Line line : lines) {
1368             double xoffset = 0;
1369             double yoffset = 0;
1370
1371             switch (hAlign) {
1372             case 1: // Trailing
1373                 xoffset = xbase - line.bbox.getWidth();
1374                 break;
1375             case 2: // Center
1376                 xoffset = xbase - line.bbox.getWidth() / 2;
1377                 break;
1378             default: // Leading / Baseline
1379                 // Do nothing
1380                 break;
1381             }
1382
1383             switch (vAlign) {
1384             case 0:
1385                 yoffset = line.layout.getAscent();
1386                 break;
1387             case 1:
1388                 yoffset = -bbox.getHeight() + line.layout.getAscent();
1389                 break;
1390             case 2:
1391                 yoffset = -bbox.getHeight() / 2 + line.layout.getAscent();
1392                 break;
1393             }
1394
1395             line.alignOffset(xoffset, yoffset);
1396         }
1397         return lines;
1398     }
1399
1400     /**
1401      * @param lines
1402      * @param frc
1403      * @return
1404      */
1405     private Line[] layoutLines(Line[] lines, FontRenderContext frc) {
1406         TextLayout emptyRowLayout = null;
1407         int lineCount = lines.length;
1408         float y = 0;
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()) {
1415                 lineText = " ";
1416                 if (emptyRowLayout == null)
1417                     emptyRowLayout = new TextLayout(lineText, font, frc);
1418                 line.layout = emptyRowLayout;
1419             } else {
1420                 line.layout = new TextLayout(lineText, font, frc);
1421             }
1422
1423             //y += line.layout.getAscent();
1424             line.drawPosY = y;
1425             y += line.layout.getDescent() + line.layout.getLeading() + line.layout.getAscent();
1426
1427             Rectangle2D bbox = line.layout.getLogicalHighlightShape(0, lineText.length()).getBounds2D();
1428             bbox.setFrame(bbox.getX(), bbox.getY() + line.drawPosY, bbox.getWidth(), bbox.getHeight());
1429             line.bbox = bbox;
1430         }
1431
1432         return lines;
1433     }
1434
1435     /**
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)})
1440      * 
1441      * @param txt
1442      *            input text
1443      * @return parsed text lines as {@link Line} structures
1444      * @see #layoutLines(Line[], FontRenderContext)
1445      */
1446     private static Line[] parseLines(String txt) {
1447         int len = txt.length();
1448         if (len == 0)
1449             return new Line[] { new Line("", 0, 0) };
1450
1451         TIntArrayList lfpos = new TIntArrayList();
1452         int pos = 0;
1453         int lineCount = 1;
1454         for (;pos < len; ++lineCount) {
1455             int nextlf = txt.indexOf('\n', pos);
1456             lfpos.add(nextlf != -1 ? nextlf : len);
1457             if (nextlf == -1)
1458                 break;
1459             pos = nextlf + 1;
1460         }
1461         Line[] lines = new Line[lineCount];
1462         pos = 0;
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);
1467             pos = lf + 1;
1468         }
1469         lines[lineCount - 1] = new Line(txt, pos, len);
1470
1471         return lines;
1472     }
1473     
1474     
1475     private static Line[] wrapLines(String txt, Font font, float fixedWidth, FontRenderContext frc) {
1476         if(txt == null || txt.isEmpty())
1477             txt = " ";
1478         
1479         ArrayList<Line> lines = 
1480                 new ArrayList<Line>();
1481         
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);
1485
1486         AttributedCharacterIterator paragraph = attributedText.getIterator();
1487         int paragraphStart = paragraph.getBeginIndex();
1488         int paragraphEnd = paragraph.getEndIndex();
1489         LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
1490
1491         float breakWidth = fixedWidth;
1492
1493         // Force text to be vertical, by setting break width to 1, if the text area is narrower than "GGGG"
1494
1495         // Set position to the index of the first character in the paragraph.
1496         lineMeasurer.setPosition(paragraphStart);
1497
1498         // Get lines until the entire paragraph has been displayed.
1499         int next, limit, charat, position = 0;
1500         
1501         while ((position = lineMeasurer.getPosition()) < paragraphEnd) {
1502
1503             // Find possible line break and set it as a limit to the next layout
1504             next = lineMeasurer.nextOffset(breakWidth);
1505             limit = next;
1506             charat = txt.indexOf(System.getProperty("line.separator"),position+1);
1507             if(charat < next && charat != -1){
1508                 limit = charat;
1509             }
1510             
1511             lineMeasurer.nextLayout(breakWidth, limit, false);
1512             // Add Line
1513             lines.add(new Line(txt, position, limit));
1514         }
1515
1516         return lines.toArray(new Line[lines.size()]);
1517     }
1518     
1519
1520     public String getClipboardContent() {
1521         Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1522         Transferable clipData = clipboard.getContents(this);
1523         try {
1524             return (String) (clipData.getTransferData(DataFlavor.stringFlavor));
1525         } catch (Exception ee) {
1526             return null;
1527         }
1528     }
1529
1530     public void setClipboardContent(String content) {
1531         Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1532         StringSelection data = new StringSelection(content);
1533         clipboard.setContents(data, data);
1534     }
1535
1536     @Override
1537     public String toString() {
1538         return super.toString() + " [text=" + text + ", font=" + font + ", color=" + color + "]";
1539     }
1540
1541     @Override
1542     protected boolean handleCommand(CommandEvent e) {
1543         if (!hasState(STATE_EDITING))
1544             return false;
1545
1546         if (Commands.SELECT_ALL.equals(e.command)) {
1547             selectAll();
1548             return true;
1549         }
1550         return false;
1551     }
1552
1553     @Override
1554     protected boolean keyPressed(KeyPressedEvent event) {
1555         if (!hasState(STATE_EDITING))
1556             return false;
1557
1558         char c = event.character;
1559         boolean ctrl = event.isControlDown();
1560         boolean alt = event.isAltDown();
1561
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);
1565         if (ctrl && !alt) {
1566             switch (event.keyCode) {
1567                 case KeyEvent.VK_C:
1568                     if (caret != selectionTail) {
1569                         int selectionMin = Math.min(caret, selectionTail);
1570                         int selectionMax = Math.max(caret, selectionTail);
1571                         setClipboardContent(text.substring(selectionMin, selectionMax));
1572                     }
1573                     break;
1574                     
1575                 case KeyEvent.VK_X:
1576                     if (caret != selectionTail) {
1577                         int selectionMin = Math.min(caret, selectionTail);
1578                         int selectionMax = Math.max(caret, selectionTail);
1579                         setClipboardContent(text.substring(selectionMin, selectionMax));
1580                         insert("");
1581                     }
1582                     break;
1583
1584                 case KeyEvent.VK_RIGHT:
1585                     if (c == '\0')  {
1586                         // '\'' has the same keycode as VK_RIGHT but when right
1587                         // arrow is pressed, event character is \0.
1588                         moveCaretCtrlRight(event.isShiftDown());
1589                     }
1590                     break;
1591                     
1592                 case KeyEvent.VK_LEFT:
1593                         moveCaretCtrlLeft(event.isShiftDown());
1594                         break;
1595                     
1596                 case KeyEvent.VK_V:
1597                 {
1598                     String content = getClipboardContent();
1599                     if(content != null)
1600                         insert(content);
1601                     break;
1602                 }
1603
1604                 // Replaced by #handleCommand
1605 //                case KeyEvent.VK_A:
1606 //                {
1607 //                    selectAll();
1608 //                    return true;
1609 //                }
1610                 
1611                 case KeyEvent.VK_ENTER:
1612                 {
1613                     insert(getLineSeparator());
1614                 }
1615                 
1616                 break;
1617
1618                 default:
1619                     return false;
1620             }
1621         } else if (!ctrl && alt) {
1622             return false;
1623         } else {
1624             switch (event.keyCode) {
1625                 case KeyEvent.VK_LEFT:
1626                         moveCaret(-1, event.isShiftDown());
1627                     break;
1628                 case KeyEvent.VK_RIGHT:
1629                     if (c == '\0')  {
1630                         // '\'' has the same keycode as VK_RIGHT but when right
1631                         // arrow is pressed, event character is \0.
1632                         moveCaret(1, event.isShiftDown());
1633                         break;
1634                     }
1635                     // Intentional fallthrough to default case
1636                 case KeyEvent.VK_UP:
1637                     moveCaretRowUp(event.isShiftDown());
1638                     break;
1639                 case KeyEvent.VK_DOWN:
1640                     moveCaretRowDown(event.isShiftDown());
1641                     break;
1642                 case KeyEvent.VK_HOME:
1643                     moveCaretHome(event.isShiftDown());
1644                     break;
1645                 case KeyEvent.VK_END:
1646                     moveCaretEnd(event.isShiftDown());
1647                     break;
1648
1649                 case KeyEvent.VK_ENTER:
1650                     fireTextEditingEnded();
1651                     return true;
1652
1653                 case KeyEvent.VK_ESCAPE:
1654                     text = textBeforeEdit;
1655                     resetCaches();
1656                     clearState(STATE_EDITING);
1657                     fireTextEditingCancelled();
1658                     return true;
1659
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));
1665                         if (index == -1)
1666                                 --caret;
1667                         else {
1668                                 caret-= (index+1);
1669                                 selectionTail+= (lineSep.length()-index-1);
1670                         }
1671                     }
1672                     insert("");
1673                     break;
1674
1675                 case KeyEvent.VK_DELETE:
1676                     if(caret == selectionTail && caret < text.length()) {
1677                         String lineSep = getLineSeparator();
1678                         int index = lineSep.indexOf(text.charAt(caret));
1679                         if (index==-1)
1680                                 ++caret;
1681                         else {
1682                                 selectionTail-= index;
1683                                 caret+= (lineSep.length()-index);
1684                         }
1685                     }
1686                     insert("");
1687                     break;
1688
1689                 
1690
1691                 default:
1692                     if (c == 65535 || Character.getType(c) == Character.CONTROL) {
1693                         return false;
1694                     }
1695                     //System.out.println("Char " + c + " " + Character.getType(c) + " " + text);
1696                     insert(new String(new char[] {c}));
1697             }
1698         }
1699
1700         // FIXME This is called even if just caret was moved.
1701         // This is currently necessary for repaints.
1702         fireTextChanged();
1703         invalidateXOffset();
1704         return true;
1705     }
1706     
1707     protected String getLineSeparator() {
1708         return System.getProperty("line.separator");
1709     }
1710
1711     protected void selectAll() {
1712         setCaret(0, false);
1713         setCaret(text.length(), true);
1714     }
1715
1716     protected transient int hoverClick = 0;
1717
1718     @Override
1719     protected boolean mouseClicked(MouseClickEvent event) {
1720         if (event.button != MouseClickEvent.LEFT_BUTTON)
1721             return false;
1722         
1723         if (hasState(STATE_HOVER)) {
1724                 hoverClick++;
1725                 if (hoverClick < 2)
1726                         return false;
1727             ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1728             // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1729             if (ctx == null)
1730                 return false;
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);
1735                         repaint();
1736                 }
1737             } 
1738         } else {
1739                 hoverClick = 0;
1740             if (hasState(STATE_EDITING)) {
1741                 fireTextEditingEnded();
1742             }
1743         }
1744         return false;
1745     }
1746     
1747     protected boolean mouseDoubleClicked(MouseDoubleClickedEvent event) {
1748         if (event.button != MouseClickEvent.LEFT_BUTTON)
1749             return false;
1750         
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.
1754             if (ctx == null)
1755                 return false;
1756             
1757             if (text != null) {
1758                 // Select the whole text.
1759                 setCaret(0, false);
1760                 setCaret(text.length(), true);
1761                 repaint();
1762             }
1763         }
1764         return false;
1765     }
1766
1767
1768     @Override
1769     protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {
1770         if (!hasState(STATE_EDITING))
1771             return false;
1772         
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());
1778         }
1779         return false;
1780     }
1781
1782     @Override
1783     protected boolean mouseMoved(MouseMovedEvent event) {
1784         boolean hit = hitTest(event, 3.0);
1785         if (hit != hasState(STATE_HOVER)) {
1786             setState(STATE_HOVER, hit);
1787             repaint();
1788         }
1789         return false;
1790     }
1791
1792     private boolean isControlDown(MouseEvent e) {
1793         return e.isControlDown() || lastMouseEvent!=null?lastMouseEvent.isControlDown():false;
1794     }
1795
1796     protected boolean isShiftDown(MouseEvent e) {
1797         return e.isShiftDown() || lastMouseEvent!=null?lastMouseEvent.isShiftDown():false;
1798     }
1799
1800 //    private boolean isAltDown(MouseEvent e) {
1801 //      return e.isAltDown() || lastMouseEvent!=null?lastMouseEvent.isAltDown():false;
1802 //    }
1803
1804     @Override
1805     protected boolean mouseDragged(MouseDragBegin e) {
1806         if (isHovering()
1807                 && (isControlDown(e) || isShiftDown(e))
1808                 && (dataRVI != null || text != null))
1809         {
1810             List<Transferable> trs = new ArrayList<>(2);
1811             if (dataRVI != null) {
1812                 trs.add(new LocalObjectTransferable(dataRVI));
1813                 trs.add(new PlaintextTransfer(dataRVI.toString()));
1814             } else if (text != null && !text.isEmpty()) {
1815                 trs.add(new PlaintextTransfer(text));
1816             }
1817             if (!trs.isEmpty()) {
1818                 e.transferable = new MultiTransferable(trs);
1819                 return true;
1820             }
1821         }
1822         return false;
1823     }
1824
1825     protected boolean hitTest(MouseEvent event, double tolerance) {
1826         Rectangle2D bounds = getBoundsInternal();
1827         if (bounds == null)
1828             return false;
1829         Point2D localPos = NodeUtil.worldToLocal(this, event.controlPosition, new Point2D.Double());
1830         double x = localPos.getX();
1831         double y = localPos.getY();
1832         boolean hit = bounds.contains(x, y);
1833         return hit;
1834     }
1835
1836     public Rectangle2D getBoundsInternal() {
1837         Rectangle2D local = lastBounds;
1838         if (local == null)
1839             return null;
1840         // TODO: potential spot for CPU/memory allocation optimization
1841         // by using more specialized implementations
1842         if (transform.isIdentity())
1843             return local;
1844         return transform.createTransformedShape(local).getBounds2D();
1845     }
1846
1847     protected Color add(Color c, int r, int g, int b)  {
1848         int nr = Math.min(255, c.getRed() + r);
1849         int ng = Math.min(255, c.getGreen() + g);
1850         int nb = Math.min(255, c.getBlue() + b);
1851         return new Color(nr, ng, nb);
1852     }
1853
1854     public TextEditActivation activateEdit(int mouseId, IElement e, ICanvasContext ctx) {
1855         EditDataNode data = EditDataNode.getNode(this);
1856         deactivateEdit(data, null);
1857         TextEditActivation result = new TextEditActivation(mouseId, e, ctx);
1858         data.setTextEditActivation(result);
1859         return result;
1860     }
1861
1862     /**
1863      * @return <code>true</code> if this node is or was previously in editing
1864      *         state
1865      */
1866     protected boolean deactivateEdit() {
1867         boolean result = deactivateEdit( editActivation );
1868         result |= editActivation != null;
1869         editActivation = null;
1870         return result;
1871     }
1872
1873     protected boolean deactivateEdit(TextEditActivation activation) {
1874         return deactivateEdit( EditDataNode.getNode(this), activation );
1875     }
1876
1877     protected boolean deactivateEdit(EditDataNode data, TextEditActivation activation) {
1878         TextEditActivation previous = data.getTextEditActivation();
1879         if (previous != null && (previous == activation || activation == null)) {
1880             previous.release();
1881             data.setTextEditActivation(null);
1882             return true;
1883         }
1884         return false;
1885     }
1886
1887     @Override
1888     public int getEventMask() {
1889         return EventTypes.KeyPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask
1890                 | EventTypes.MouseClickMask | EventTypes.MouseDragBeginMask | EventTypes.CommandMask;
1891     }
1892
1893     private MouseEvent lastMouseEvent = null;
1894     
1895     @Override
1896     public boolean handleEvent(Event e) {
1897         if(e instanceof MouseEvent && !(e instanceof MouseDragBegin)) lastMouseEvent = (MouseEvent)e;
1898         return super.handleEvent(e);
1899     }
1900
1901         @Override
1902         public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
1903                 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
1904         }
1905         
1906         @Override
1907         public <T> T getProperty(String propertyName) {
1908                 return null;
1909         }
1910         
1911         @Override
1912         public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
1913         }
1914         
1915         public void synchronizeText(String text) {
1916                 setText(text);
1917         }
1918
1919         public void synchronizeColor(RGB.Integer color) {
1920                 this.color = Colors.awt(color);
1921         }
1922
1923         public void synchronizeFont(org.simantics.datatypes.literal.Font font) {
1924                 setFont(Fonts.awt(font));
1925         }
1926
1927         public void synchronizeTransform(double[] data) {
1928                 this.setTransform(new AffineTransform(data));
1929         }
1930
1931         public static void main(String[] args) {
1932                 Line[] lines = parseLines("\n  \n FOO  \n\nBAR\n\n\n BAZ\n\n");
1933                 System.out.println(Arrays.toString(lines));
1934                 System.out.println(GeometryUtils.pointToMillimeter(1));
1935                 System.out.println(GeometryUtils.pointToMillimeter(12));
1936                 System.out.println(GeometryUtils.pointToMillimeter(72));
1937         }
1938
1939     ///////////////////////////////////////////////////////////////////////////
1940     // LEGACY CODE NEEDED BY INHERITED CLASSES FOR NOW
1941     ///////////////////////////////////////////////////////////////////////////
1942
1943     protected double getHorizontalAlignOffset(Rectangle2D r) {
1944         switch (horizontalAlignment) {
1945             case 0: return 0; // Leading
1946             case 1: return -r.getWidth(); // Trailing
1947             case 2: return -r.getCenterX(); // Center
1948             default: return 0;
1949         }
1950     }
1951
1952     protected double getVerticalAlignOffset() {
1953         FontMetrics fm = fontMetrics;
1954         if (fm == null)
1955             return 0;
1956         switch (verticalAlignment) {
1957             case 0: return fm.getMaxAscent(); // Leading=top=maxascent
1958             case 1: return -fm.getMaxDescent(); // Trailing=bottom=maxdescent
1959             case 2: return fm.getMaxAscent() / 2; // Center=maxascent / 2
1960             case 3: return 0;
1961             default: return 0;
1962         }
1963     }
1964
1965     ///////////////////////////////////////////////////////////////////////////
1966     // LEGACY CODE ENDS
1967     ///////////////////////////////////////////////////////////////////////////
1968
1969 }