]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.diagram/src/org/simantics/diagram/elements/TextNode.java
Counting of pending nodes does not work if null text is saved as ""
[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 = 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
415         resetCaches();
416     }
417
418     public void setAutomaticTextFlipping(TextFlipping type) {
419         switch (type) {
420         case Disabled:
421             clearState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED | STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
422             break;
423         case VerticalTextDownwards:
424             setState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED | STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
425             break;
426         case VerticalTextUpwards:
427             setState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED);
428             clearState(STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
429             break;
430         }
431     }
432
433     @SyncField({"paddingX", "paddingY"})
434     public void setPadding(double x, double y) {
435         this.paddingX = x;
436         this.paddingY = y;
437     }
438
439     @SyncField({"color"})
440     public void setColor(Color color) {
441         this.color = color;
442     }
443
444     @SyncField({"backgroundColor"})
445     public void setBackgroundColor(Color color) {
446         this.backgroundColor = color;
447     }
448
449     @SyncField({"borderColor"})
450     public void setBorderColor(Color color) {
451         this.borderColor = color;
452     }
453
454     public String getText() {
455         return text;
456     }
457     
458     public String getTextBeforeEdit() {
459         return textBeforeEdit;
460     }
461
462     @SyncField({"text","caret","selectionTail"})
463     public void setText(String text) {
464         //System.out.println("TextNode.setText('" + text + "', " + editing + ")");
465         if (hasState(STATE_EDITING))
466             return;
467
468         // value => no value
469         if(this.text != null && text == null) NodeUtil.increasePending(this);
470         // no value => value
471         if(this.text == null && text != null) NodeUtil.decreasePending(this);
472
473         this.text = text;
474         caret = text != null ? Math.min(caret, text.length()) : 0;
475         selectionTail = caret;
476
477         resetCaches();
478     }
479
480     @SyncField({"pending"})
481     public void setPending(boolean pending) {
482         boolean p = hasState(STATE_PENDING);
483         if(!p && pending) NodeUtil.increasePending(this);
484         if(p && !pending) NodeUtil.decreasePending(this);
485         if(p != pending)
486             setState(STATE_PENDING, pending);
487     }
488
489     @SyncField({"fixedWidth"})
490     public void setFixedWidth(float fixedWidth) {
491         if (fixedWidth < 0f)
492             throw new IllegalArgumentException("negative fixed width");
493         this.fixedWidth = fixedWidth;
494         invalidateXOffset();
495     }
496     
497     /**
498      * Bounds where the text box will be drawn
499      * @param bounds
500      */
501     public void setTargetBounds(Rectangle2D bounds) {
502         this.targetBounds = bounds;
503     }
504
505         final public void synchronizeWidth(float width) {
506                 if (width >= 0.0f)
507                         setFixedWidth(width);
508         }
509
510         final public void synchronizeBorderWidth(float width) {
511                 if (width >= 0.0f)
512                         setBorderWidth(width);
513         }
514
515     public final void synchronizeWrapText(boolean wrap) {
516         setState(STATE_WRAP_TEXT, wrap);
517     }
518
519     public boolean isHovering() {
520         return hasState(STATE_HOVER);
521     }
522
523     @SyncField({"hover"})
524     public void setHover(boolean hover) {
525         setState(STATE_HOVER, hover);
526         repaint();
527     }
528
529     public Font getFont() {
530         return font;
531     }
532
533     @SyncField({"font"})
534     public void setFont(Font font) {
535         this.font = font;
536         resetCaches();
537     }
538
539     public double getBorderWidth() {
540         return borderWidth;
541     }
542
543     @SyncField({"borderWidth"})
544     public void setBorderWidth(float width) {
545         this.borderWidth = width;
546     }
547
548     public void setBorderWidth(double width) {
549         setBorderWidth((float)width);
550     }
551
552     @SyncField({"horizontalAlignment"})
553     public void setHorizontalAlignment(byte horizontalAlignment) {
554         if (horizontalAlignment < 0 && horizontalAlignment > 2)
555             throw new IllegalArgumentException("Invalid horizontal alignment: " + horizontalAlignment + ", must be between 0 and 2");
556         this.horizontalAlignment = horizontalAlignment;
557         resetCaches();
558     }
559
560     final public void synchronizeHorizontalAlignment(byte horizontalAlignment) {
561         if (horizontalAlignment >= 0 && horizontalAlignment <= 2)
562             setHorizontalAlignment(horizontalAlignment);
563     }
564
565     public byte getHorizontalAlignment() {
566         return horizontalAlignment;
567     }
568
569     @SyncField({"verticalAlignment"})
570     public void setVerticalAlignment(byte verticalAlignment) {
571         if (verticalAlignment < 0 && verticalAlignment > 3)
572             throw new IllegalArgumentException("Invalid vertical alignment: " + verticalAlignment + ", must be between 0 and 3");
573         this.verticalAlignment = verticalAlignment;
574         resetCaches();
575     }
576
577     final public void synchronizeVerticalAlignment(byte verticalAlignment) {
578         if (verticalAlignment >= 0 && verticalAlignment <= 3)
579             setVerticalAlignment(verticalAlignment);
580     }
581
582     public byte getVerticalAlignment() {
583         return verticalAlignment;
584     }
585
586     /**
587      * Rendering is single-threaded so we can use a static rectangle for
588      * calculating the expanded bounds for the node.
589      */
590     private static transient ThreadLocal<Rectangle2D> tempBounds = new ThreadLocal<Rectangle2D>() {
591         @Override
592         protected Rectangle2D initialValue() {
593             return new Rectangle2D.Double();
594         }
595     };
596
597     /**
598      * Rendering is single-threaded so we can use a static AffineTransform to
599      * prevent continuous memory allocation during text rendering.
600      */
601     private static transient ThreadLocal<AffineTransform> tempAffineTransform = new ThreadLocal<AffineTransform>() {
602         @Override
603         protected AffineTransform initialValue() {
604             return new AffineTransform();
605         }
606     };
607
608     @Override
609     public void render(Graphics2D g) {
610         AffineTransform ot = g.getTransform();
611         render(g, true);
612         g.setTransform(ot);
613     }
614
615     /**
616      * Note: does not return transformation, stroke, color, etc. to their
617      * original states
618      * 
619      * @param g
620      * @param applyTransform
621      */
622     public void render(Graphics2D g, boolean applyTransform) {
623         if (text == null || font == null || color == null)
624             return;
625
626         // Cache font metrics if necessary
627         if (fontMetrics == null)
628             fontMetrics = g.getFontMetrics(font);
629
630         Color color = this.color;
631         boolean isSelected = NodeUtil.isSelected(this, 1);
632         boolean hover = hasState(STATE_HOVER);
633         boolean editing = hasState(STATE_EDITING);
634
635         if (!isSelected && hover) {
636             color = add(color, 120, 120, 120);
637         }
638
639         if (applyTransform)
640             g.transform(transform);
641         // Apply separate legacy scale
642         if (scale != 1.0)
643             g.scale(scale, scale);
644
645         // Safety for not rendering when the scale of this text is too small.
646         // When the scale is too small it will cause internal exceptions while
647         // stroking fonts.
648         AffineTransform curTr = g.getTransform();
649         double currentScale = GeometryUtils.getScale(curTr);
650         //System.out.println("currentScale: " + currentScale);
651         if (currentScale < 1e-6)
652             return;
653
654         g.setFont(font);
655         //g.translate(x, y);
656
657         // Calculate text clip rectangle.
658         // This updates textLayout if necessary.
659         Rectangle2D r = getTightAlignedBoundsInLocal(tempBounds.get(), fontMetrics.getFontRenderContext());
660
661         computeEditingXOffset();
662
663         if (fixedWidth > 0f)
664             r.setFrame(r.getMinX(), r.getMinY(), fixedWidth, r.getHeight());
665         if(targetBounds != null) {
666             double w = (targetBounds.getWidth() - paddingX * 2) * scaleRecip;
667             double h = (targetBounds.getHeight() - paddingY * 2) * scaleRecip;
668             double x = (targetBounds.getMinX() + paddingX) * scaleRecip;
669             double y = (targetBounds.getMinY() + paddingY) * scaleRecip;
670             r.setRect(x, y, w, h);
671         }
672
673         if (hasState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED)) {
674             boolean needsXFlip;
675             boolean needsYFlip;
676             if (curTr.getScaleX() != 0) {
677                 needsXFlip = curTr.getScaleX() < 0.0;
678                 needsYFlip = curTr.getScaleY() < 0.0;
679             } else {
680                 boolean flipAll = !hasState(STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
681                 needsXFlip = (curTr.getShearY() < 0.0) ^ flipAll;
682                 needsYFlip = (curTr.getShearX() > 0.0) ^ flipAll;
683             }
684             if (needsXFlip || needsYFlip) {
685                 double centerX = r.getWidth()*0.5 + r.getX();
686                 double centerY = r.getHeight()*0.5 + r.getY();
687
688                 g.translate(centerX, centerY);
689                 g.scale(needsXFlip ? -1.0 : 1.0, needsYFlip ? -1.0 : 1.0);
690                 g.translate(-centerX, -centerY);
691             }
692         }
693
694         Rectangle2D textClip = r.getBounds2D();
695
696         expandBoundsUnscaled(r);
697
698         // Speed rendering optimization: don't draw text that is too small to
699         // read when not editing
700         boolean renderText = true;
701         if (!editing) {
702             Object renderingHint = g.getRenderingHint(RenderingHints.KEY_RENDERING);
703             if (renderingHint != RenderingHints.VALUE_RENDER_QUALITY) {
704                 float textSizeMM = (float) currentScale * GeometryUtils.pointToMillimeter(font.getSize2D());
705                 if (textSizeMM < 1.5f)
706                     renderText = false;
707             }
708         }
709
710         Shape clipSave = g.getClip();
711         g.setClip(textClip);
712
713         // PDF 
714         PdfWriter writer = (PdfWriter) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_WRITER);
715         TextRenderingMode renderingMode = (TextRenderingMode) g.getRenderingHint(G2DRenderingHints.KEY_TEXT_RENDERING_MODE);
716         boolean renderAsText = writer != null || renderingMode == TextRenderingMode.AS_TEXT;
717         /// PDF
718
719         Color backgroundColor = hasState(STATE_VALID) ? this.backgroundColor : Color.red;
720
721         // RENDER
722         {
723             // Fill background if necessary
724             if (backgroundColor != null) {
725                 g.setColor(backgroundColor);
726                 g.fill(r);
727             }
728
729             if (editing) {
730
731                 int selectionMin = Math.min(caret, selectionTail);
732                 int selectionMax = Math.max(caret, selectionTail);
733
734                 // Base text
735                 g.setColor(color);
736                 renderText(g, xOffset, renderAsText);
737
738                 Shape clip = g.getClip();
739
740                 // Selection background & text
741                 for (Line line : lines) {
742                     if (line.intersectsRange(selectionMin, selectionMax)) {
743                         Shape selShape = line.getLogicalHighlightShape(selectionMin, selectionMax);
744                         line.translate(g, xOffset, 0);
745                         g.setClip(selShape);
746                         g.setColor(SELECTION_BACKGROUND_COLOR);
747                         g.fill(selShape);
748                         g.setColor(Color.WHITE);
749                         // #6459: render as text in PDF and paths on screen
750                         if (renderAsText)
751                             g.drawString(line.getText(), 0, 0);
752                         else
753                             line.layout.draw(g, 0, 0);
754                         line.translateInv(g, xOffset, 0);
755                     }
756                 }
757
758                 g.setClip(clip);
759
760                 renderCaret(g);
761
762             } else {
763
764                 if (renderText) {
765                     g.setColor(color);
766                     renderText(g, 0, renderAsText);
767                 }
768
769             }
770         }
771
772         g.setClip(clipSave);
773
774         if (borderWidth > 0f && borderColor != null) {
775             g.setColor(borderColor);
776             g.setStroke(new BasicStroke((float) (scale*borderWidth)));
777             g.draw(r);
778         }
779
780         //System.out.println("bw: " + borderWidth);
781         if (isSelected && showsSelection()) {
782             Composite oc = g.getComposite();
783             g.setComposite(SrcOver_50);
784             g.setColor(Color.RED);
785             float bw = borderWidth;
786             double s = currentScale;
787             if (bw <= 0f) {
788                 bw = (float) (1f / s);
789             } else {
790                 bw *= 5f * scale;
791             }
792             g.setStroke(new BasicStroke(bw));
793             g.draw(r);
794             //g.draw(GeometryUtils.expandRectangle(r, 1.0));
795
796             g.setComposite(oc);
797         }
798
799         g.scale(scaleRecip, scaleRecip);
800         g.setStroke(RESET_STROKE);
801
802         lastBounds = getScaledOffsetBounds(r, lastBounds, scale, 0, 0);
803 //        g.setColor(Color.MAGENTA); // DEBUG
804 //        g.draw(lastBounds); // DEBUG
805 //        g.setColor(Color.ORANGE); // DEBUG
806 //        g.draw(getBoundsInLocal()); // DEBUG
807
808         renderSelectedHover(g, isSelected, hover);
809     }
810
811     private void renderCaret(Graphics2D g) {
812         g.setColor(Color.BLACK);
813         for (int i = 0; i < lines.length; i++) {
814                 Line line = lines[i];
815                 // prevent rendering caret twice on line changes
816                 if (line.containsOffset(caret) &&                // line contains caret
817                    (caret != line.endOffset ||                   //caret is not in the end of the line
818                     i == lines.length-1 ||                       //caret is end of the last line
819                     lines[i+1].startOffset != line.endOffset)) { // beginning of the next line does not start withe the same index as current line
820               Shape[] caretShape = line.getCaretShapes(caret);
821               line.translate(g, xOffset, 0);
822               g.draw(caretShape[0]);
823               if (caretShape[1] != null)
824                   g.draw(caretShape[1]);
825               line.translateInv(g, xOffset, 0);
826           }
827         }
828     }
829     private void renderText(Graphics2D g, float xOffset, boolean renderAsText) {
830         //g.draw(tightBoundsCache); // DEBUG
831         for (Line line : lines) {
832             // #6459: render as text in PDF and paths on screen
833             if (renderAsText)
834                 g.drawString(line.getText(), line.alignedPosX + xOffset, line.alignedPosY);
835             else
836                 line.layout.draw(g, line.alignedPosX + xOffset, line.alignedPosY);
837             //g.draw(line.abbox); // DEBUG
838         }
839     }
840
841     protected Rectangle2D getScaledOffsetBounds(Rectangle2D originalBounds, Rectangle2D dst, double scale, double offsetX, double offsetY) {
842         AffineTransform btr = tempAffineTransform.get();
843         btr.setToTranslation(offsetX*scale, offsetY*scale);
844         btr.scale(scale, scale);
845         if (btr.isIdentity()) {
846             dst.setFrame(originalBounds);
847         } else {
848             dst.setFrame(btr.createTransformedShape(originalBounds).getBounds2D());
849         }
850         return dst;
851     }
852
853     /**
854      * Invoked when TextNode is selected and a mouse is hovering on top of it. Can be overridden to add custom rendering.
855      * 
856      * @param g
857      */
858     protected void renderSelectedHover(Graphics2D g, boolean isSelected, boolean isHovering) {
859     }
860
861     public String editText(String text) {
862
863         String error = validator != null ? validator.apply(text) : null;
864         if (error == null) {
865             this.text = text;
866             if (textListener != null) {
867                 textListener.textEditingEnded();
868             }
869         }
870         return error;
871     }
872
873     /**
874      * Replaces the current selection with the content or inserts
875      * the content at caret. After the insertion the caret
876      * will be at the end of inserted text and selection will
877      * be empty.
878      * @param content
879      */
880     @SyncField({"text","caret","selectionTail"})
881     protected void insert(String content) {
882         content = editContentFilter != null ? editContentFilter.filter(this, content) : content; 
883
884         int selectionMin = Math.min(caret, selectionTail);
885         int selectionMax = Math.max(caret, selectionTail);
886
887         String begin = text.substring(0, selectionMin);
888         String end = text.substring(selectionMax);
889         text = begin + content + end;
890         caret = selectionMin + content.length();
891         selectionTail = caret;
892
893         assert (caret <= text.length());
894         //System.out.println(text + " " + caret );
895
896         if(validator != null) {
897             String error = validator.apply(text);
898             setState(STATE_VALID, (error == null));
899         }
900
901         resetCaches();
902     }
903
904     @ServerSide
905     protected void fireTextChanged() {
906         if(textListener != null)
907             textListener.textChanged();
908         repaint();
909     }
910
911     @ServerSide
912     protected void fireTextEditingStarted() {
913         if(textListener != null)
914             textListener.textEditingStarted();
915     }
916
917     @ServerSide
918     protected void fireTextEditingCancelled() {
919         setState(STATE_VALID);
920
921         if (deactivateEdit()) {
922             if (textListener != null)
923                 textListener.textEditingCancelled();
924
925             setEditMode(false, false);
926
927             if (textBeforeEdit != null)
928                 setText(textBeforeEdit);
929
930             repaint();
931         }
932     }
933
934     @ServerSide
935     public void fireTextEditingEnded() {
936         if (!hasState(STATE_VALID)) {
937             fireTextEditingCancelled();
938             setState(STATE_VALID);
939             return;
940         }
941
942         if (deactivateEdit()) {
943             if (textListener != null)
944                 textListener.textEditingEnded();
945
946             setEditMode(false, false);
947             repaint();
948         }
949     }
950
951     public void setTextListener(ITextListener listener) {
952         this.textListener = listener;
953     }
954
955     public void setValidator(Function1<String, String> validator) {
956         this.validator = validator;
957     }
958
959     public void setContentFilter(ITextContentFilter filter) {
960         this.editContentFilter = filter;
961     }
962
963     public void setRVI(RVI rvi) {
964         this.dataRVI = rvi;
965     }
966
967     private void invalidateXOffset() {
968         setState(STATE_X_OFFSET_IS_DIRTY);
969     }
970
971     private void computeEditingXOffset() {
972
973         if(lines == null) return;
974         if(!hasState(STATE_X_OFFSET_IS_DIRTY)) return;
975         if(fixedWidth > 0f) {
976
977             // TODO: implement
978 //            float[] coords = textLayout.getCaretInfo(TextHitInfo.afterOffset(caret));
979 //            if(coords != null) {
980 //                if(coords[0] > (fixedWidth*MAX_CARET_POSITION)) xOffset = (float)((fixedWidth*MAX_CARET_POSITION)-coords[0]);
981 //                else xOffset = 0;
982 //            }
983
984         } else {
985
986             xOffset = 0;
987
988         }
989
990         clearState(STATE_X_OFFSET_IS_DIRTY);
991
992     }
993
994     @SyncField({"caret","selectionTail"})
995     protected void moveCaret(int move, boolean select) {
996         // prevent setting caret into line separator. 
997         if (move > 0) {
998                 while (text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
999                         move++;
1000         } else if (move < 0) {
1001                 while (caret+move >= 0 && text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
1002                         move--;
1003         }
1004         caret += move;
1005         if(caret < 0)
1006             caret = 0;
1007         if (caret > text.length())
1008             caret = text.length();
1009         if(!select)
1010             selectionTail = caret;
1011     }
1012     
1013     private Line findCaretLine() {
1014         // Find the line where caret is. Starting from first line.
1015         for(int i = 0; i < lines.length; i++) {
1016             Line line = lines[i];
1017             if(caret <= line.endOffset) {
1018                 return line;
1019             }
1020         }
1021         return null;
1022     }
1023     
1024     /**
1025      * Moves caret to next not letter or digit
1026      * @param shiftDown
1027      */
1028     private void moveCaretCtrlLeft(boolean shiftDown) {
1029         Line line = findCaretLine();
1030         if(line != null) {
1031             int i;
1032             for(i = caret-1; i > line.startOffset; i--) {
1033                 char c = line.document.charAt(i);
1034                 if(!Character.isLetterOrDigit(c)) {
1035                     break;
1036                 }
1037             }
1038             moveCaret(i - caret, shiftDown);
1039         }
1040     }
1041     
1042     /**
1043      * Moves caret to previous non letter or digit
1044      * @param shiftDown
1045      */
1046     private void moveCaretCtrlRight(boolean shiftDown) {
1047         Line line = findCaretLine();
1048         if(line != null) {
1049             int i;
1050             for(i = caret + 1; i < line.endOffset; i++) {
1051                 char c = line.document.charAt(i);
1052                 if(!Character.isLetterOrDigit(c)) {
1053                     break;
1054                 }
1055             }
1056             moveCaret(i - caret, shiftDown);
1057         }
1058     }
1059     
1060     /**
1061      * Moves caret to line end
1062      * @param shiftDown
1063      */
1064     private void moveCaretEnd(boolean shiftDown) {
1065         Line line = findCaretLine();
1066         if(line != null)
1067             // Move caret to the end of the line
1068             moveCaret(line.endOffset - caret, shiftDown);
1069     }
1070     
1071     /**
1072      * Moves caret to beginning of a line
1073      * @param shiftDown
1074      */
1075     private void moveCaretHome(boolean shiftDown) {
1076         Line line = findCaretLine();
1077         if(line != null)
1078             // Move caret to the beginning of the line
1079             moveCaret(line.startOffset - caret, shiftDown);
1080     }
1081     
1082     /**
1083      * Moves caret one row up and tries to maintain the location
1084      * @param shiftDown
1085      */
1086     private void moveCaretRowUp(boolean shiftDown) {
1087         // Find the line where caret is. Starting from first line.
1088         for(int i = 0; i < lines.length; i++) {
1089             Line line = lines[i];
1090             if(caret <= line.endOffset) {
1091                 // caret is in this line
1092                 if(i == 0) {
1093                     // Already on top line
1094                     // Select the beginning of the line
1095                     moveCaret(-caret, shiftDown);
1096                 } else {
1097                     Line prevLine = lines[i-1];
1098                     int prevLength = prevLine.endOffset - prevLine.startOffset;
1099                     int posInCurRow = caret - line.startOffset;
1100                     if(prevLength < posInCurRow)
1101                         posInCurRow = prevLength;
1102
1103                     int newPos = prevLine.startOffset + posInCurRow;
1104                     moveCaret(newPos - caret, shiftDown);
1105                 }
1106                 break;
1107             }
1108         }        
1109     }
1110     
1111     /**
1112      * Moves caret one row down and tries to maintain the location
1113      * @param shiftDown
1114      */
1115     private void moveCaretRowDown(boolean shiftDown) {
1116         // Find the line where caret is. Starting from last line.
1117         for(int i = lines.length - 1; i >= 0; i--) {
1118             Line line = lines[i];
1119             if(caret >= line.startOffset) {
1120                 // caret is in this line
1121                 if(i == lines.length - 1) {
1122                     // Already on bottom line, cannot go below
1123                     // Select to the end of the line
1124                     moveCaret(line.endOffset - caret, shiftDown);
1125                 } else {
1126                     Line prevLine = lines[i+1]; // Previous line
1127                     
1128                     // Find new caret position. 
1129                     // Either it is in the same index as before, or if the row
1130                     // is not long enough, select the end of the row.
1131                     int prevLength = prevLine.endOffset - prevLine.startOffset;
1132                     int posInCurRow = caret - line.startOffset;
1133                     if(prevLength < posInCurRow)
1134                         posInCurRow = prevLength;
1135                     int newPos = prevLine.startOffset + posInCurRow;
1136                     moveCaret(newPos - caret, shiftDown);
1137                 }
1138                 break;
1139             }
1140         }        
1141     }
1142
1143     @SyncField({"caret","selectionTail"})
1144     protected void setCaret(int pos, boolean select) {
1145         caret = pos;
1146         if (caret < 0)
1147             caret = 0;
1148         if (caret > text.length())
1149             caret = text.length();
1150         if (!select)
1151             selectionTail = caret;
1152     }
1153
1154     protected void setCaret(Point2D point) {
1155         setCaret(point, false);
1156     }
1157
1158     @SyncField({"caret","selectionTail"})
1159     protected void setCaret(Point2D point, boolean select) {
1160         double lineY = 0;
1161         for(int i = 0; i < lines.length; i++) {
1162             Line line = lines[i];
1163             Rectangle2D bounds = line.abbox;
1164             // Add heights of bboxes for determining the correct line
1165             if(i == 0)
1166                 lineY = bounds.getY();
1167             else
1168                 lineY += lines[i-1].abbox.getHeight();
1169             
1170             double lineHeight = bounds.getHeight();
1171             double hitY = point.getY() / scale;
1172             if(hitY >= lineY && hitY <= lineY + lineHeight) {
1173                 // Hit is in this line
1174                 float x = (float)(point.getX() / scale) - (float)line.abbox.getX();
1175                 float y = (float)(point.getY() / scale -  lineHeight * i) ;
1176                 TextHitInfo info = line.layout.hitTestChar(x, y);
1177                 caret = line.startOffset + info.getInsertionIndex();
1178                 if (caret > line.endOffset)
1179                         caret = line.endOffset;
1180                 if (!select)
1181                     selectionTail = caret;
1182                 repaint();
1183                 break;
1184             }
1185         }
1186         invalidateXOffset();
1187         assert (caret <= text.length());
1188     }
1189     
1190     @Override
1191     public Rectangle2D getBoundsInLocal() {
1192         if(targetBounds != null)
1193             return targetBounds;
1194         else
1195             return expandBounds( getTightAlignedBoundsInLocal(null) );
1196     }
1197
1198     protected Rectangle2D expandBounds(Rectangle2D r) {
1199         r.setRect(r.getX() * scale - paddingX, r.getY() * scale -paddingY, r.getWidth()*scale + paddingX + paddingX, r.getHeight()*scale + paddingY + paddingY);
1200         //System.out.println("  => " + r);
1201         return r;
1202     }
1203
1204     protected Rectangle2D expandBoundsUnscaled(Rectangle2D r) {
1205         r.setRect(r.getX() - scaleRecip*paddingX, r.getY() -scaleRecip*paddingY, r.getWidth() + scaleRecip*paddingX + scaleRecip*paddingX, r.getHeight() + scaleRecip*paddingY + scaleRecip*paddingY);
1206         //System.out.println("  => " + r);
1207         return r;
1208     }
1209
1210     protected Rectangle2D expandBounds(Rectangle2D r, double amount) {
1211         r.setRect(r.getX() - amount, r.getY() - amount, r.getWidth() + 2*amount, r.getHeight() + 2*amount);
1212         return r;
1213     }
1214
1215     protected Rectangle2D expandBounds(Rectangle2D r, double left, double top, double right, double bottom) {
1216         r.setRect(r.getX() - left, r.getY() - top, r.getWidth() + left + right, r.getHeight() + top + bottom);
1217         return r;
1218     }
1219
1220     private void resetCaches() {
1221         this.tightBoundsCache = null;
1222         this.lines = null;
1223         this.fontMetrics = null;
1224     }
1225
1226     /**
1227      * Returns the tight bounds around the current text using the current font
1228      * in the specified rectangle. If the specified rectangle is
1229      * <code>null</code> a new Rectangle2D.Double instance will be created.
1230      * 
1231      * @param r
1232      * @return
1233      */
1234     protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r) {
1235         return getTightAlignedBoundsInLocal(r, FRC);
1236     }
1237
1238     /**
1239      * Returns the tight bounds around the current text using the current font
1240      * in the specified rectangle. If the specified rectangle is
1241      * <code>null</code> a new Rectangle2D.Double instance will be created.
1242      * 
1243      * @param r
1244      *            the rectangle where the result of the method is placed or
1245      *            <code>null</code> to allocate new rectangle
1246      * @param frc current font render context
1247      * @return r or new Rectangle2D.Double instance containing the requested
1248      *         text bounds
1249      */
1250     protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r, FontRenderContext frc) {
1251         if (r == null)
1252             r = new Rectangle2D.Double();
1253
1254         if (tightBoundsCache != null) {
1255             r.setFrame(tightBoundsCache);
1256             return r;
1257         }
1258
1259         String txt = text;
1260         if (font == null || txt == null) {
1261             r.setFrame(0, 0, 2, 1);
1262             return r;
1263         }
1264
1265         //System.out.println("TextNode.getTightAlignedBoundsInLocal('" + txt + "')");
1266
1267         // Parse & layout (unaligned)
1268         Line[] lines = null;
1269         
1270         if(hasState(STATE_WRAP_TEXT)) {
1271             float width = fixedWidth;
1272             if(width <= 0 && targetBounds != null)
1273                 width = (float) (((targetBounds.getWidth() - 2*paddingX)) * scaleRecip);
1274             if(width > 0)
1275                 lines = wrapLines(txt, font, width, frc);
1276         }
1277          
1278         if(lines == null)
1279             lines = parseLines(txt);
1280         this.lines = layoutLines(lines, frc);
1281
1282         // Calculate tight bounds based on unaligned layout
1283         //System.out.println("Unaligned");
1284         tightBoundsCache = calculateBounds(lines, Line.BBOX, null);
1285         //System.out.println("  => " + tightBoundsCache);
1286
1287         this.lines = layoutLinesX(lines, tightBoundsCache);
1288         // Align each line to the calculated tight bounds
1289         this.lines = alignLines(this.lines, tightBoundsCache, horizontalAlignment, verticalAlignment);
1290
1291         // Calculate aligned bounds
1292         //System.out.println("Aligned");
1293         calculateBounds(lines, Line.ABBOX, tightBoundsCache);
1294
1295         r.setFrame(tightBoundsCache);
1296         //System.out.println("  => " + tightBoundsCache);
1297
1298         return r;
1299     }
1300
1301     /**
1302      * @param lines
1303      * @param bbox
1304      *            the bounding box of all the whole laid out text (only bbox
1305      *            size is used)
1306      * @return
1307      */
1308     private Line[] layoutLinesX(Line[] lines, Rectangle2D bbox) {
1309         int lineCount = lines.length;
1310         for (int l = 0; l < lineCount; ++l) {
1311             Line line = lines[l];
1312             // Compute pen x position. If the paragraph is right-to-left we
1313             // will align the TextLayouts to the right edge of the panel.
1314             // Note: drawPosX is always where the LEFT of the text is placed.
1315             // NOTE: This changes based on horizontal alignment
1316             line.drawPosX = (float) (line.layout.isLeftToRight() ? 0f
1317                     : tightBoundsCache.getWidth() - line.layout.getAdvance());
1318         }
1319         return lines;
1320     }
1321
1322     /**
1323      * @param lines
1324      * @param boundsProvider
1325      * @param result
1326      * @return
1327      */
1328     private static Rectangle2D calculateBounds(Line[] lines, BoundsProcedure boundsProvider, Rectangle2D result) {
1329         if (result == null)
1330             result = new Rectangle2D.Double();
1331         else
1332             result.setFrame(0, 0, 0, 0);
1333
1334         for (Line line : lines) {
1335             //System.out.println("line: " + line);
1336             Rectangle2D bbox = boundsProvider.getBounds(line);
1337             if (result.isEmpty())
1338                 result.setFrame(bbox);
1339             else
1340                 Rectangle2D.union(result, bbox, result);
1341             //System.out.println("bounds: " + result);
1342         }
1343         //System.out.println("final bounds: " + result);
1344
1345         return result;
1346     }
1347
1348     /**
1349      * @param lines
1350      * @param bbox
1351      * @param hAlign
1352      * @param vAlign
1353      * @return aligned lines
1354      */
1355     private Line[] alignLines(Line[] lines, Rectangle2D bbox, byte hAlign, byte vAlign) {
1356 //        System.out.println("horizontal align: " + Alignment.values()[hAlign]);
1357 //        System.out.println("vertical align  : " + Alignment.values()[vAlign]);
1358 //        System.out.println("bbox: " + bbox);
1359         double xbase = 0;
1360 //        double ybase = 0;
1361         if(targetBounds != null) {
1362             /* In normal cases the bounding box moves when
1363              * typing. If target bounds are set, the text
1364              * is fitted into the box.
1365              */
1366             switch (hAlign) {
1367             case 1: // Trailing
1368                 xbase = (targetBounds.getMaxX()-paddingX) * scaleRecip;
1369                 break;
1370             case 2: // Center
1371                 xbase = targetBounds.getCenterX() * scaleRecip;
1372                 break;
1373             default: // Leading / Baseline
1374                 // Do nothing
1375                 break;
1376             }
1377         }
1378         
1379         
1380         for (Line line : lines) {
1381             double xoffset = 0;
1382             double yoffset = 0;
1383
1384             switch (hAlign) {
1385             case 1: // Trailing
1386                 xoffset = xbase - line.bbox.getWidth();
1387                 break;
1388             case 2: // Center
1389                 xoffset = xbase - line.bbox.getWidth() / 2;
1390                 break;
1391             default: // Leading / Baseline
1392                 // Do nothing
1393                 break;
1394             }
1395
1396             switch (vAlign) {
1397             case 0:
1398                 yoffset = line.layout.getAscent();
1399                 break;
1400             case 1:
1401                 yoffset = -bbox.getHeight() + line.layout.getAscent();
1402                 break;
1403             case 2:
1404                 yoffset = -bbox.getHeight() / 2 + line.layout.getAscent();
1405                 break;
1406             }
1407
1408             line.alignOffset(xoffset, yoffset);
1409         }
1410         return lines;
1411     }
1412
1413     /**
1414      * @param lines
1415      * @param frc
1416      * @return
1417      */
1418     private Line[] layoutLines(Line[] lines, FontRenderContext frc) {
1419         TextLayout emptyRowLayout = null;
1420         int lineCount = lines.length;
1421         float y = 0;
1422         for (int l = 0; l < lineCount; ++l) {
1423             Line line = lines[l];
1424             String lineText = line.getText();
1425             // " " because TextLayout requires non-empty text and
1426             // We don't want zero size for the text.
1427             if (lineText.isEmpty()) {
1428                 lineText = " ";
1429                 if (emptyRowLayout == null)
1430                     emptyRowLayout = new TextLayout(lineText, font, frc);
1431                 line.layout = emptyRowLayout;
1432             } else {
1433                 line.layout = new TextLayout(lineText, font, frc);
1434             }
1435
1436             //y += line.layout.getAscent();
1437             line.drawPosY = y;
1438             y += line.layout.getDescent() + line.layout.getLeading() + line.layout.getAscent();
1439
1440             Rectangle2D bbox = line.layout.getLogicalHighlightShape(0, lineText.length()).getBounds2D();
1441             // HighlightShape is not large enough, if font is italic.
1442             Rectangle2D bbox2 = line.layout.getBounds();
1443             bbox.add(bbox2);
1444             bbox.setFrame(bbox.getX(), bbox.getY() + line.drawPosY, bbox.getWidth(), bbox.getHeight());
1445             line.bbox = bbox;
1446         }
1447
1448         return lines;
1449     }
1450
1451     /**
1452      * Splits the specified string into {@link Line} structures, one for each
1453      * line in the input text. The returned lines are only partially defined,
1454      * waiting to be laid out (see
1455      * {@link #layoutLines(Line[], FontRenderContext)})
1456      * 
1457      * @param txt
1458      *            input text
1459      * @return parsed text lines as {@link Line} structures
1460      * @see #layoutLines(Line[], FontRenderContext)
1461      */
1462     private static Line[] parseLines(String txt) {
1463         int len = txt.length();
1464         if (len == 0)
1465             return new Line[] { new Line("", 0, 0) };
1466
1467         TIntArrayList lfpos = new TIntArrayList();
1468         int pos = 0;
1469         int lineCount = 1;
1470         for (;pos < len; ++lineCount) {
1471             int nextlf = txt.indexOf('\n', pos);
1472             lfpos.add(nextlf != -1 ? nextlf : len);
1473             if (nextlf == -1)
1474                 break;
1475             pos = nextlf + 1;
1476         }
1477         Line[] lines = new Line[lineCount];
1478         pos = 0;
1479         for (int i = 0; i < lineCount-1; ++i) {
1480             int lf = lfpos.getQuick(i);
1481             int cr = (lf > 0) && (txt.charAt(lf - 1) == '\r') ? lf - 1 : lf;
1482             lines[i] = new Line(txt, pos, cr);
1483             pos = lf + 1;
1484         }
1485         lines[lineCount - 1] = new Line(txt, pos, len);
1486
1487         return lines;
1488     }
1489     
1490     
1491     private static Line[] wrapLines(String txt, Font font, float fixedWidth, FontRenderContext frc) {
1492         if(txt == null || txt.isEmpty())
1493             txt = " ";
1494         
1495         ArrayList<Line> lines = 
1496                 new ArrayList<Line>();
1497         
1498         Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
1499         map.put(TextAttribute.FONT, font);
1500         AttributedString attributedText = new AttributedString(txt.isEmpty() ? "__ERROR__" : txt, map);
1501
1502         AttributedCharacterIterator paragraph = attributedText.getIterator();
1503         int paragraphStart = paragraph.getBeginIndex();
1504         int paragraphEnd = paragraph.getEndIndex();
1505         LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
1506
1507         float breakWidth = fixedWidth;
1508
1509         // Force text to be vertical, by setting break width to 1, if the text area is narrower than "GGGG"
1510
1511         // Set position to the index of the first character in the paragraph.
1512         lineMeasurer.setPosition(paragraphStart);
1513
1514         // Get lines until the entire paragraph has been displayed.
1515         int next, limit, charat, position = 0;
1516         
1517         while ((position = lineMeasurer.getPosition()) < paragraphEnd) {
1518
1519             // Find possible line break and set it as a limit to the next layout
1520             next = lineMeasurer.nextOffset(breakWidth);
1521             limit = next;
1522             charat = txt.indexOf(System.getProperty("line.separator"),position+1);
1523             if(charat < next && charat != -1){
1524                 limit = charat;
1525             }
1526             
1527             lineMeasurer.nextLayout(breakWidth, limit, false);
1528             // Add Line
1529             lines.add(new Line(txt, position, limit));
1530         }
1531
1532         return lines.toArray(new Line[lines.size()]);
1533     }
1534     
1535
1536     public String getClipboardContent() {
1537         Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1538         Transferable clipData = clipboard.getContents(this);
1539         try {
1540             return (String) (clipData.getTransferData(DataFlavor.stringFlavor));
1541         } catch (Exception ee) {
1542             return null;
1543         }
1544     }
1545
1546     public void setClipboardContent(String content) {
1547         Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1548         StringSelection data = new StringSelection(content);
1549         clipboard.setContents(data, data);
1550     }
1551
1552     @Override
1553     public String toString() {
1554         return super.toString() + " [text=" + text + ", font=" + font + ", color=" + color + "]";
1555     }
1556
1557     @Override
1558     protected boolean handleCommand(CommandEvent e) {
1559         if (!hasState(STATE_EDITING))
1560             return false;
1561
1562         if (Commands.SELECT_ALL.equals(e.command)) {
1563             selectAll();
1564             return true;
1565         }
1566         return false;
1567     }
1568
1569     @Override
1570     protected boolean keyPressed(KeyPressedEvent event) {
1571         if (!hasState(STATE_EDITING))
1572             return false;
1573
1574         char c = event.character;
1575         boolean ctrl = event.isControlDown();
1576         boolean alt = event.isAltDown();
1577
1578 //        System.out.println("Key pressed '" + (event.character == 0 ? "\\0" : "" + c) + "' " + event.keyCode + " - " + Integer.toBinaryString(event.stateMask));
1579 //        System.out.println("ctrl: " + ctrl);
1580 //        System.out.println("alt: " + alt);
1581         if (ctrl && !alt) {
1582             switch (event.keyCode) {
1583                 case KeyEvent.VK_C:
1584                     if (caret != selectionTail) {
1585                         int selectionMin = Math.min(caret, selectionTail);
1586                         int selectionMax = Math.max(caret, selectionTail);
1587                         setClipboardContent(text.substring(selectionMin, selectionMax));
1588                     }
1589                     break;
1590                     
1591                 case KeyEvent.VK_X:
1592                     if (caret != selectionTail) {
1593                         int selectionMin = Math.min(caret, selectionTail);
1594                         int selectionMax = Math.max(caret, selectionTail);
1595                         setClipboardContent(text.substring(selectionMin, selectionMax));
1596                         insert("");
1597                     }
1598                     break;
1599
1600                 case KeyEvent.VK_RIGHT:
1601                     if (c == '\0')  {
1602                         // '\'' has the same keycode as VK_RIGHT but when right
1603                         // arrow is pressed, event character is \0.
1604                         moveCaretCtrlRight(event.isShiftDown());
1605                     }
1606                     break;
1607                     
1608                 case KeyEvent.VK_LEFT:
1609                         moveCaretCtrlLeft(event.isShiftDown());
1610                         break;
1611                     
1612                 case KeyEvent.VK_V:
1613                 {
1614                     String content = getClipboardContent();
1615                     if(content != null)
1616                         insert(content);
1617                     break;
1618                 }
1619
1620                 // Replaced by #handleCommand
1621 //                case KeyEvent.VK_A:
1622 //                {
1623 //                    selectAll();
1624 //                    return true;
1625 //                }
1626                 
1627                 case KeyEvent.VK_ENTER:
1628                 {
1629                     insert(getLineSeparator());
1630                 }
1631                 
1632                 break;
1633
1634                 default:
1635                     return false;
1636             }
1637         } else if (!ctrl && alt) {
1638             return false;
1639         } else {
1640             switch (event.keyCode) {
1641                 case KeyEvent.VK_LEFT:
1642                         moveCaret(-1, event.isShiftDown());
1643                     break;
1644                 case KeyEvent.VK_RIGHT:
1645                     if (c == '\0')  {
1646                         // '\'' has the same keycode as VK_RIGHT but when right
1647                         // arrow is pressed, event character is \0.
1648                         moveCaret(1, event.isShiftDown());
1649                         break;
1650                     }
1651                     // Intentional fallthrough to default case
1652                 case KeyEvent.VK_UP:
1653                     moveCaretRowUp(event.isShiftDown());
1654                     break;
1655                 case KeyEvent.VK_DOWN:
1656                     moveCaretRowDown(event.isShiftDown());
1657                     break;
1658                 case KeyEvent.VK_HOME:
1659                     moveCaretHome(event.isShiftDown());
1660                     break;
1661                 case KeyEvent.VK_END:
1662                     moveCaretEnd(event.isShiftDown());
1663                     break;
1664
1665                 case KeyEvent.VK_ENTER:
1666                     fireTextEditingEnded();
1667                     return true;
1668
1669                 case KeyEvent.VK_ESCAPE:
1670                     text = textBeforeEdit;
1671                     resetCaches();
1672                     clearState(STATE_EDITING);
1673                     fireTextEditingCancelled();
1674                     return true;
1675
1676                 case KeyEvent.VK_BACK_SPACE:
1677                     if(caret == selectionTail && caret > 0) {
1678                         // line separator may use multiple characters, we want to remove that with one command
1679                         String lineSep = getLineSeparator();
1680                         int index = lineSep.indexOf(text.charAt(caret-1));
1681                         if (index == -1)
1682                                 --caret;
1683                         else {
1684                                 caret-= (index+1);
1685                                 selectionTail+= (lineSep.length()-index-1);
1686                         }
1687                     }
1688                     insert("");
1689                     break;
1690
1691                 case KeyEvent.VK_DELETE:
1692                     if(caret == selectionTail && caret < text.length()) {
1693                         String lineSep = getLineSeparator();
1694                         int index = lineSep.indexOf(text.charAt(caret));
1695                         if (index==-1)
1696                                 ++caret;
1697                         else {
1698                                 selectionTail-= index;
1699                                 caret+= (lineSep.length()-index);
1700                         }
1701                     }
1702                     insert("");
1703                     break;
1704
1705                 
1706
1707                 default:
1708                     if (c == 65535 || Character.getType(c) == Character.CONTROL) {
1709                         return false;
1710                     }
1711                     //System.out.println("Char " + c + " " + Character.getType(c) + " " + text);
1712                     insert(new String(new char[] {c}));
1713             }
1714         }
1715
1716         // FIXME This is called even if just caret was moved.
1717         // This is currently necessary for repaints.
1718         fireTextChanged();
1719         invalidateXOffset();
1720         return true;
1721     }
1722     
1723     protected String getLineSeparator() {
1724         return System.getProperty("line.separator");
1725     }
1726
1727     protected void selectAll() {
1728         setCaret(0, false);
1729         setCaret(text.length(), true);
1730     }
1731
1732     protected transient int hoverClick = 0;
1733
1734     @Override
1735     protected boolean mouseClicked(MouseClickEvent event) {
1736         if (event.button != MouseClickEvent.LEFT_BUTTON)
1737             return false;
1738         
1739         if (hasState(STATE_HOVER)) {
1740                 hoverClick++;
1741                 if (hoverClick < 2)
1742                         return false;
1743             ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1744             // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1745             if (ctx == null)
1746                 return false;
1747             IElement e = DiagramNodeUtil.getElement(ctx, this);
1748             if (!hasState(STATE_EDITING)) {
1749                 if (Boolean.TRUE.equals(setEditMode(true))) {
1750                         editActivation = activateEdit(0, e, ctx);
1751                         repaint();
1752                 }
1753             } 
1754         } else {
1755                 hoverClick = 0;
1756             if (hasState(STATE_EDITING)) {
1757                 fireTextEditingEnded();
1758             }
1759         }
1760         return false;
1761     }
1762     
1763     protected boolean mouseDoubleClicked(MouseDoubleClickedEvent event) {
1764         if (event.button != MouseClickEvent.LEFT_BUTTON)
1765             return false;
1766         
1767         if (hitTest(event, 0)) {
1768             ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1769             // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1770             if (ctx == null)
1771                 return false;
1772             
1773             if (text != null) {
1774                 // Select the whole text.
1775                 setCaret(0, false);
1776                 setCaret(text.length(), true);
1777                 repaint();
1778             }
1779         }
1780         return false;
1781     }
1782
1783
1784     @Override
1785     protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {
1786         if (!hasState(STATE_EDITING))
1787             return false;
1788         
1789         Point2D local = controlToLocal( event.controlPosition );
1790         // FIXME: once the event coordinate systems are cleared up, remove this workaround
1791         local = parentToLocal(local);
1792         if (hasState(STATE_HOVER) && this.containsLocal(local)) {
1793             setCaret(local, event.isShiftDown());
1794         }
1795         return false;
1796     }
1797
1798     @Override
1799     protected boolean mouseMoved(MouseMovedEvent event) {
1800         boolean hit = hitTest(event, 3.0);
1801         if (hit != hasState(STATE_HOVER)) {
1802             setState(STATE_HOVER, hit);
1803             repaint();
1804         }
1805         return false;
1806     }
1807
1808     private boolean isControlDown(MouseEvent e) {
1809         return e.isControlDown() || lastMouseEvent!=null?lastMouseEvent.isControlDown():false;
1810     }
1811
1812     protected boolean isShiftDown(MouseEvent e) {
1813         return e.isShiftDown() || lastMouseEvent!=null?lastMouseEvent.isShiftDown():false;
1814     }
1815
1816 //    private boolean isAltDown(MouseEvent e) {
1817 //      return e.isAltDown() || lastMouseEvent!=null?lastMouseEvent.isAltDown():false;
1818 //    }
1819
1820     @Override
1821     protected boolean mouseDragged(MouseDragBegin e) {
1822         if (isHovering()
1823                 && (isControlDown(e) || isShiftDown(e))
1824                 && (dataRVI != null || text != null))
1825         {
1826             List<Transferable> trs = new ArrayList<>(2);
1827             if (dataRVI != null) {
1828                 trs.add(new LocalObjectTransferable(dataRVI));
1829                 trs.add(new PlaintextTransfer(dataRVI.toString()));
1830             } else if (text != null && !text.isEmpty()) {
1831                 trs.add(new PlaintextTransfer(text));
1832             }
1833             if (!trs.isEmpty()) {
1834                 e.transferable = new MultiTransferable(trs);
1835                 return true;
1836             }
1837         }
1838         return false;
1839     }
1840
1841     protected boolean hitTest(MouseEvent event, double tolerance) {
1842         Rectangle2D bounds = getBoundsInternal();
1843         if (bounds == null)
1844             return false;
1845         Point2D localPos = NodeUtil.worldToLocal(this, event.controlPosition, new Point2D.Double());
1846         double x = localPos.getX();
1847         double y = localPos.getY();
1848         boolean hit = bounds.contains(x, y);
1849         return hit;
1850     }
1851
1852     public Rectangle2D getBoundsInternal() {
1853         Rectangle2D local = lastBounds;
1854         if (local == null)
1855             return null;
1856         // TODO: potential spot for CPU/memory allocation optimization
1857         // by using more specialized implementations
1858         if (transform.isIdentity())
1859             return local;
1860         return transform.createTransformedShape(local).getBounds2D();
1861     }
1862
1863     protected Color add(Color c, int r, int g, int b)  {
1864         int nr = Math.min(255, c.getRed() + r);
1865         int ng = Math.min(255, c.getGreen() + g);
1866         int nb = Math.min(255, c.getBlue() + b);
1867         return new Color(nr, ng, nb);
1868     }
1869
1870     public TextEditActivation activateEdit(int mouseId, IElement e, ICanvasContext ctx) {
1871         EditDataNode data = EditDataNode.getNode(this);
1872         deactivateEdit(data, null);
1873         TextEditActivation result = new TextEditActivation(mouseId, e, ctx);
1874         data.setTextEditActivation(result);
1875         return result;
1876     }
1877
1878     /**
1879      * @return <code>true</code> if this node is or was previously in editing
1880      *         state
1881      */
1882     protected boolean deactivateEdit() {
1883         boolean result = deactivateEdit( editActivation );
1884         result |= editActivation != null;
1885         editActivation = null;
1886         return result;
1887     }
1888
1889     protected boolean deactivateEdit(TextEditActivation activation) {
1890         return deactivateEdit( EditDataNode.getNode(this), activation );
1891     }
1892
1893     protected boolean deactivateEdit(EditDataNode data, TextEditActivation activation) {
1894         TextEditActivation previous = data.getTextEditActivation();
1895         if (previous != null && (previous == activation || activation == null)) {
1896             previous.release();
1897             data.setTextEditActivation(null);
1898             return true;
1899         }
1900         return false;
1901     }
1902
1903     @Override
1904     public int getEventMask() {
1905         return EventTypes.KeyPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask
1906                 | EventTypes.MouseClickMask | EventTypes.MouseDragBeginMask | EventTypes.CommandMask;
1907     }
1908
1909     private MouseEvent lastMouseEvent = null;
1910     
1911     @Override
1912     public boolean handleEvent(Event e) {
1913         if(e instanceof MouseEvent && !(e instanceof MouseDragBegin)) lastMouseEvent = (MouseEvent)e;
1914         return super.handleEvent(e);
1915     }
1916
1917         @Override
1918         public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
1919                 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
1920         }
1921         
1922         @Override
1923         public <T> T getProperty(String propertyName) {
1924                 return null;
1925         }
1926         
1927         @Override
1928         public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
1929         }
1930         
1931         public void synchronizeText(String text) {
1932                 setText(text);
1933         }
1934
1935         public void synchronizeColor(RGB.Integer color) {
1936                 this.color = Colors.awt(color);
1937         }
1938
1939         public void synchronizeFont(org.simantics.datatypes.literal.Font font) {
1940                 setFont(Fonts.awt(font));
1941         }
1942
1943         public void synchronizeTransform(double[] data) {
1944                 this.setTransform(new AffineTransform(data));
1945         }
1946
1947         public static void main(String[] args) {
1948                 Line[] lines = parseLines("\n  \n FOO  \n\nBAR\n\n\n BAZ\n\n");
1949                 System.out.println(Arrays.toString(lines));
1950                 System.out.println(GeometryUtils.pointToMillimeter(1));
1951                 System.out.println(GeometryUtils.pointToMillimeter(12));
1952                 System.out.println(GeometryUtils.pointToMillimeter(72));
1953         }
1954
1955     ///////////////////////////////////////////////////////////////////////////
1956     // LEGACY CODE NEEDED BY INHERITED CLASSES FOR NOW
1957     ///////////////////////////////////////////////////////////////////////////
1958
1959     protected double getHorizontalAlignOffset(Rectangle2D r) {
1960         switch (horizontalAlignment) {
1961             case 0: return 0; // Leading
1962             case 1: return -r.getWidth(); // Trailing
1963             case 2: return -r.getCenterX(); // Center
1964             default: return 0;
1965         }
1966     }
1967
1968     protected double getVerticalAlignOffset() {
1969         FontMetrics fm = fontMetrics;
1970         if (fm == null)
1971             return 0;
1972         switch (verticalAlignment) {
1973             case 0: return fm.getMaxAscent(); // Leading=top=maxascent
1974             case 1: return -fm.getMaxDescent(); // Trailing=bottom=maxdescent
1975             case 2: return fm.getMaxAscent() / 2; // Center=maxascent / 2
1976             case 3: return 0;
1977             default: return 0;
1978         }
1979     }
1980
1981     ///////////////////////////////////////////////////////////////////////////
1982     // LEGACY CODE ENDS
1983     ///////////////////////////////////////////////////////////////////////////
1984
1985 }