]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.diagram/src/org/simantics/diagram/elements/TextNode.java
Merge "Minor refactorings related to SCL constructors"
[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.NodeEventHandler;
66 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
67 import org.simantics.scenegraph.g2d.events.command.Commands;
68 import org.simantics.scenegraph.utils.GeometryUtils;
69 import org.simantics.scenegraph.utils.NodeUtil;
70 import org.simantics.scl.runtime.function.Function1;
71 import org.simantics.scl.runtime.function.Function2;
72 import org.simantics.ui.colors.Colors;
73 import org.simantics.ui.dnd.LocalObjectTransferable;
74 import org.simantics.ui.dnd.MultiTransferable;
75 import org.simantics.ui.dnd.PlaintextTransfer;
76 import org.simantics.ui.fonts.Fonts;
77 import org.simantics.utils.threads.AWTThread;
78
79 import com.lowagie.text.pdf.PdfWriter;
80
81 import gnu.trove.list.array.TIntArrayList;
82
83
84 /**
85  * TextNode which supports in-line editing.
86  * 
87  * By default <code>TextNode</code> is in editable = false state. Use
88  * {@link #setEditable(boolean)} to make it editable.
89  * 
90  * @author Hannu Niemist&ouml; <hannu.niemisto@vtt.fi>
91  * @author Marko Luukkainen <marko.luukkainen@vtt.fi>
92  * @author Tuukka Lehtonen <tuukka.lehtonen@semantum.fi>
93  * 
94  * TODO:
95  * o proper support for defining clipping bounds for the text (needed for page templates) (currently through fixedWidth)
96  * o fix editing xOffset to work with fixed width and multi-line text
97  * 
98  * @see Line
99  * @see TextLayout
100  */
101 public class TextNode extends G2DNode implements IDynamicSelectionPainterNode, LoaderNode {
102
103     private static final long                serialVersionUID           = 654692698101485672L;
104
105     public static enum TextFlipping {
106         Disabled,
107         VerticalTextUpwards,
108         VerticalTextDownwards,
109     }
110
111     /**
112      * TODO: justify existence for this
113      */
114     private static final BasicStroke         RESET_STROKE               = new BasicStroke(1);
115
116     /**
117      * Src-over alpha composite instance with 50% opacity.
118      */
119     private static final AlphaComposite      SrcOver_50                 = AlphaComposite.SrcOver.derive(0.5f);
120
121     /**
122      * For (inexact) measurement of rendered text bounds.
123      */
124     protected static final FontRenderContext FRC = new FontRenderContext(new AffineTransform(), true, true);
125
126     private static final Font FONT = Font.decode("Arial 6");
127     private static final Color SELECTION_BACKGROUND_COLOR = new Color(0x316ac5);
128 //    private static final double MAX_CARET_POSITION = 1.0;
129
130     /**
131      * The complete text visualized by this node.
132      */
133     protected String text = null;
134
135     /**
136      * The font used to render the {@link #text}.
137      */
138     protected Font font = FONT;
139
140     /**
141      * The color of the rendered text. Default value is {@value Color#black}.
142      */
143     protected Color color = Color.BLACK;
144
145     /**
146      * The background color used for filling the background of the bounding box
147      * of the rendered text. <code>null</code> means no fill.
148      * Default value is <code>null</code>.
149      */
150     protected Color backgroundColor = null;
151
152     /**
153      * The color used for drawing the expanded bounding box border for the
154      * rendered text. <code>null</code> means no border is rendered. Default
155      * value is <code>null</code>.
156      */
157     protected Color borderColor = null;
158
159     protected double scale = 1.0;
160     protected transient double scaleRecip = 1.0;
161
162     /**
163      * 
164      */
165     protected float borderWidth = 0.f;
166
167     protected double paddingX = 2.0;
168     protected double paddingY = 2.0;
169
170     /**
171      * Horizontal text box alignment with respect to its origin. Default value is
172      * 0 (leading).
173      */
174     protected byte horizontalAlignment = 0;
175     /**
176      * Vertical text box alignment with respect to its origin. Default value is
177      * 3 (baseline).
178      */
179     protected byte verticalAlignment = 3;
180
181     /**
182      * Tells if this node is still pending for real results or not.
183      */
184     protected static final int STATE_PENDING = (1 << 0);
185     protected static final int STATE_HOVER   = (1 << 1);
186     protected static final int STATE_EDITABLE = (1 << 2);
187     protected static final int STATE_SHOW_SELECTION = (1 << 3);
188     protected static final int STATE_WRAP_TEXT = (1 << 4);
189     protected transient static final int STATE_EDITING = (1 << 5);
190     protected transient static final int STATE_VALID = (1 << 6);
191     protected transient static final int STATE_X_OFFSET_IS_DIRTY = (1 << 7);
192     protected static final int STATE_ALWAYS_ADD_LISTENERS = (1 << 8);
193     protected static final int STATE_LISTENERS_ADDED = (1 << 9);
194     protected static final int STATE_AUTOMATIC_TEXT_FLIP_ENABLED = (1 << 10);
195     protected static final int STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN = (1 << 11);
196
197     /**
198      * A combination of all the STATE_ constants defined in this class,
199      * e.g. {@link #STATE_PENDING}.
200      */
201     protected int state = STATE_SHOW_SELECTION | STATE_WRAP_TEXT | STATE_VALID | STATE_X_OFFSET_IS_DIRTY;
202
203     protected RVI dataRVI = null;
204
205     int caret = 0;
206     int selectionTail = 0;
207     float xOffset = 0;
208
209     float fixedWidth = 0f;
210
211     private Rectangle2D targetBounds;
212
213     Function1<String, String> validator;
214     ITextListener textListener;
215     ITextContentFilter editContentFilter;
216
217     /**
218      * The renderable line structures parsed from {@link #text} by
219      * {@link #parseLines(String)}, laid out by
220      * {@link #layoutLines(Line[], FontRenderContext)} and aligned by
221      * {@link #alignLines(Line[], Rectangle2D, byte, byte)}
222      */
223     protected transient Line[]           lines                      = null;
224     protected transient FontMetrics      fontMetrics                = null;
225
226     /**
227      * Stores the value of {@link #text} before edit mode was last entered. Used
228      * for restoring the original value if editing is cancelled.
229      */
230     private transient String             textBeforeEdit             = null;
231     protected transient TextEditActivation editActivation;
232
233     /**
234      * Stores the last scaled bounds.
235      */
236     private transient Rectangle2D        lastBounds = new Rectangle2D.Double();
237
238     /**
239      * This must be nullified if anything that affects the result of
240      * {@link #getTightAlignedBoundsInLocal(Rectangle2D, FontRenderContext)}
241      * changes. It will cause the cached value to be recalculated on the next
242      * request.
243      */
244     private transient Rectangle2D        tightBoundsCache = null;
245
246     @Override
247     public void init() {
248         super.init();
249         // Mark this node as pending
250         NodeUtil.increasePending(this);
251     }
252
253     @Override
254     public void cleanup() {
255         removeListeners();
256         super.cleanup();
257     }
258
259     protected boolean hasState(int flags) {
260         return (state & flags) == flags;
261     }
262
263     protected void setState(int flags) {
264         this.state |= flags;
265     }
266
267     protected void setState(int flags, boolean set) {
268         if (set)
269             this.state |= flags;
270         else
271             this.state &= ~flags;
272     }
273
274     protected void clearState(int flags) {
275         this.state &= ~flags;
276     }
277
278     protected void setListeners(boolean add) {
279         if (add)
280             addListeners();
281         else
282             removeListeners();
283     }
284
285     protected void addListeners() {
286         if (!hasState(STATE_LISTENERS_ADDED)) {
287             addEventHandler(this);
288             setState(STATE_LISTENERS_ADDED);
289         }
290     }
291
292     protected void removeListeners() {
293         if (hasState(STATE_LISTENERS_ADDED)) {
294             removeEventHandler(this);
295             clearState(STATE_LISTENERS_ADDED);
296         }
297     }
298
299     /**
300      * Set to true to always enable event listening in this TextNode to allow the text node to keep track of hovering, etc. and to allow DnD even when 
301      * @param force
302      */
303     public void setForceEventListening(boolean force) {
304         setState(STATE_ALWAYS_ADD_LISTENERS, force);
305         if (force && !hasState(STATE_EDITABLE)) {
306             setListeners(force);
307         }
308     }
309
310     /**
311      * Enables or disables edit mode. It also sets
312      * the caret at the end of text all selects the
313      * whole text (this is the usual convention when
314      * beginning to edit one line texts).
315      * @param edit
316      * @return null if no change to edit state was made
317      */
318     public Boolean setEditMode(boolean edit) {
319         return setEditMode(edit, true);
320     }
321
322     /**
323      * Enables or disables edit mode. It also sets
324      * the caret at the end of text all selects the
325      * whole text (this is the usual convention when
326      * beginning to edit one line texts).
327      * @param edit
328      * @return null if no change to edit state was made
329      */
330     protected Boolean setEditMode(boolean edit, boolean notify) {
331         if (edit && !hasState(STATE_EDITABLE))
332             return null;
333         if (hasState(STATE_EDITING) == edit)
334             return null;
335         setState(STATE_EDITING, edit);
336         if (edit) {
337             caret = text != null ? text.length() : 0;
338             selectionTail = 0;
339             textBeforeEdit = text;
340             if (notify)
341                 fireTextEditingStarted();
342             return Boolean.TRUE;
343         } else {
344             if (notify)
345                 fireTextEditingEnded();
346             return Boolean.FALSE;
347         }
348     }
349
350     @SyncField({"editable"})
351     public void setEditable(boolean editable) {
352         boolean changed = hasState(STATE_EDITABLE) != editable;
353         setState(STATE_EDITABLE, editable);
354         if (hasState(STATE_EDITING) && !editable)
355             setEditMode(false);
356         if (changed && !hasState(STATE_ALWAYS_ADD_LISTENERS)) {
357             setListeners(editable);
358         }
359     }
360
361     public boolean isEditable() {
362         return hasState(STATE_EDITABLE);
363     }
364
365     public boolean isEditMode() {
366         return hasState(STATE_EDITING);
367     }
368     
369     @SyncField({"wrapText"})
370     public void setWrapText(boolean wrapText) {
371         setState(STATE_WRAP_TEXT, wrapText);
372     }
373     
374     /**
375      * @return Does the text box wrap text if 
376      * the width of the box is fixed
377      */
378     public boolean isWrapText() {
379         return hasState(STATE_WRAP_TEXT);
380     }
381
382     @SyncField({"showSelection"})
383     public void setShowSelection(boolean showSelection) {
384         setState(STATE_SHOW_SELECTION, showSelection);
385     }
386
387     public boolean showsSelection() {
388         return hasState(STATE_SHOW_SELECTION);
389     }
390
391     /**
392      * @param text
393      * @param font
394      * @param color
395      * @param x not supported anymore, use {@link #setTransform(AffineTransform)} instead
396      * @param y not supported anymore, use {@link #setTransform(AffineTransform)} instead
397      * @param scale
398      */
399     @SyncField({"text", "font", "color", "x", "y", "scale"})
400     public void init(String text, Font font, Color color, double x, double y, double scale) {
401         // no value => value
402         if(this.text == null && text != null) NodeUtil.decreasePending(this);
403
404         if (hasState(STATE_EDITING))
405             return;
406
407         this.text = new String(text != null ? text : "");
408         this.font = font;
409         this.color = color;
410         this.scale = scale;
411         this.scaleRecip = 1.0 / scale;
412         this.caret = 0;
413         this.selectionTail = 0;
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 != null ? text : "";
474         caret = Math.min(caret, this.text.length());
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     /**
862      * Replaces the current selection with the content or inserts
863      * the content at caret. After the insertion the caret
864      * will be at the end of inserted text and selection will
865      * be empty.
866      * @param content
867      */
868     @SyncField({"text","caret","selectionTail"})
869     protected void insert(String content) {
870         content = editContentFilter != null ? editContentFilter.filter(this, content) : content; 
871
872         int selectionMin = Math.min(caret, selectionTail);
873         int selectionMax = Math.max(caret, selectionTail);
874
875         String begin = text.substring(0, selectionMin);
876         String end = text.substring(selectionMax);
877         text = begin + content + end;
878         caret = selectionMin + content.length();
879         selectionTail = caret;
880
881         assert (caret <= text.length());
882         //System.out.println(text + " " + caret );
883
884         if(validator != null) {
885             String error = validator.apply(text);
886             setState(STATE_VALID, (error == null));
887         }
888
889         resetCaches();
890     }
891
892     @ServerSide
893     protected void fireTextChanged() {
894         if(textListener != null)
895             textListener.textChanged();
896         repaint();
897     }
898
899     @ServerSide
900     protected void fireTextEditingStarted() {
901         if(textListener != null)
902             textListener.textEditingStarted();
903     }
904
905     @ServerSide
906     protected void fireTextEditingCancelled() {
907         setState(STATE_VALID);
908
909         if (deactivateEdit()) {
910             if (textListener != null)
911                 textListener.textEditingCancelled();
912
913             setEditMode(false, false);
914
915             if (textBeforeEdit != null)
916                 setText(textBeforeEdit);
917
918             repaint();
919         }
920     }
921
922     @ServerSide
923     public void fireTextEditingEnded() {
924         if (!hasState(STATE_VALID)) {
925             fireTextEditingCancelled();
926             setState(STATE_VALID);
927             return;
928         }
929
930         if (deactivateEdit()) {
931             if (textListener != null)
932                 textListener.textEditingEnded();
933
934             setEditMode(false, false);
935             repaint();
936         }
937     }
938
939     public void setTextListener(ITextListener listener) {
940         this.textListener = listener;
941     }
942
943     public void setValidator(Function1<String, String> validator) {
944         this.validator = validator;
945     }
946
947     public void setContentFilter(ITextContentFilter filter) {
948         this.editContentFilter = filter;
949     }
950
951     public void setRVI(RVI rvi) {
952         this.dataRVI = rvi;
953     }
954
955     private void invalidateXOffset() {
956         setState(STATE_X_OFFSET_IS_DIRTY);
957     }
958
959     private void computeEditingXOffset() {
960
961         if(lines == null) return;
962         if(!hasState(STATE_X_OFFSET_IS_DIRTY)) return;
963         if(fixedWidth > 0f) {
964
965             // TODO: implement
966 //            float[] coords = textLayout.getCaretInfo(TextHitInfo.afterOffset(caret));
967 //            if(coords != null) {
968 //                if(coords[0] > (fixedWidth*MAX_CARET_POSITION)) xOffset = (float)((fixedWidth*MAX_CARET_POSITION)-coords[0]);
969 //                else xOffset = 0;
970 //            }
971
972         } else {
973
974             xOffset = 0;
975
976         }
977
978         clearState(STATE_X_OFFSET_IS_DIRTY);
979
980     }
981
982     @SyncField({"caret","selectionTail"})
983     protected void moveCaret(int move, boolean select) {
984         // prevent setting caret into line separator. 
985         if (move > 0) {
986                 while (text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
987                         move++;
988         } else if (move < 0) {
989                 while (caret+move >= 0 && text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
990                         move--;
991         }
992         caret += move;
993         if(caret < 0)
994             caret = 0;
995         if (caret > text.length())
996             caret = text.length();
997         if(!select)
998             selectionTail = caret;
999     }
1000     
1001     private Line findCaretLine() {
1002         // Find the line where caret is. Starting from first line.
1003         for(int i = 0; i < lines.length; i++) {
1004             Line line = lines[i];
1005             if(caret <= line.endOffset) {
1006                 return line;
1007             }
1008         }
1009         return null;
1010     }
1011     
1012     /**
1013      * Moves caret to next not letter or digit
1014      * @param shiftDown
1015      */
1016     private void moveCaretCtrlLeft(boolean shiftDown) {
1017         Line line = findCaretLine();
1018         if(line != null) {
1019             int i;
1020             for(i = caret-1; i > line.startOffset; i--) {
1021                 char c = line.document.charAt(i);
1022                 if(!Character.isLetterOrDigit(c)) {
1023                     break;
1024                 }
1025             }
1026             moveCaret(i - caret, shiftDown);
1027         }
1028     }
1029     
1030     /**
1031      * Moves caret to previous non letter or digit
1032      * @param shiftDown
1033      */
1034     private void moveCaretCtrlRight(boolean shiftDown) {
1035         Line line = findCaretLine();
1036         if(line != null) {
1037             int i;
1038             for(i = caret + 1; i < line.endOffset; i++) {
1039                 char c = line.document.charAt(i);
1040                 if(!Character.isLetterOrDigit(c)) {
1041                     break;
1042                 }
1043             }
1044             moveCaret(i - caret, shiftDown);
1045         }
1046     }
1047     
1048     /**
1049      * Moves caret to line end
1050      * @param shiftDown
1051      */
1052     private void moveCaretEnd(boolean shiftDown) {
1053         Line line = findCaretLine();
1054         if(line != null)
1055             // Move caret to the end of the line
1056             moveCaret(line.endOffset - caret, shiftDown);
1057     }
1058     
1059     /**
1060      * Moves caret to beginning of a line
1061      * @param shiftDown
1062      */
1063     private void moveCaretHome(boolean shiftDown) {
1064         Line line = findCaretLine();
1065         if(line != null)
1066             // Move caret to the beginning of the line
1067             moveCaret(line.startOffset - caret, shiftDown);
1068     }
1069     
1070     /**
1071      * Moves caret one row up and tries to maintain the location
1072      * @param shiftDown
1073      */
1074     private void moveCaretRowUp(boolean shiftDown) {
1075         // Find the line where caret is. Starting from first line.
1076         for(int i = 0; i < lines.length; i++) {
1077             Line line = lines[i];
1078             if(caret <= line.endOffset) {
1079                 // caret is in this line
1080                 if(i == 0) {
1081                     // Already on top line
1082                     // Select the beginning of the line
1083                     moveCaret(-caret, shiftDown);
1084                 } else {
1085                     Line prevLine = lines[i-1];
1086                     int prevLength = prevLine.endOffset - prevLine.startOffset;
1087                     int posInCurRow = caret - line.startOffset;
1088                     if(prevLength < posInCurRow)
1089                         posInCurRow = prevLength;
1090
1091                     int newPos = prevLine.startOffset + posInCurRow;
1092                     moveCaret(newPos - caret, shiftDown);
1093                 }
1094                 break;
1095             }
1096         }        
1097     }
1098     
1099     /**
1100      * Moves caret one row down and tries to maintain the location
1101      * @param shiftDown
1102      */
1103     private void moveCaretRowDown(boolean shiftDown) {
1104         // Find the line where caret is. Starting from last line.
1105         for(int i = lines.length - 1; i >= 0; i--) {
1106             Line line = lines[i];
1107             if(caret >= line.startOffset) {
1108                 // caret is in this line
1109                 if(i == lines.length - 1) {
1110                     // Already on bottom line, cannot go below
1111                     // Select to the end of the line
1112                     moveCaret(line.endOffset - caret, shiftDown);
1113                 } else {
1114                     Line prevLine = lines[i+1]; // Previous line
1115                     
1116                     // Find new caret position. 
1117                     // Either it is in the same index as before, or if the row
1118                     // is not long enough, select the end of the row.
1119                     int prevLength = prevLine.endOffset - prevLine.startOffset;
1120                     int posInCurRow = caret - line.startOffset;
1121                     if(prevLength < posInCurRow)
1122                         posInCurRow = prevLength;
1123                     int newPos = prevLine.startOffset + posInCurRow;
1124                     moveCaret(newPos - caret, shiftDown);
1125                 }
1126                 break;
1127             }
1128         }        
1129     }
1130
1131     @SyncField({"caret","selectionTail"})
1132     protected void setCaret(int pos, boolean select) {
1133         caret = pos;
1134         if (caret < 0)
1135             caret = 0;
1136         if (caret > text.length())
1137             caret = text.length();
1138         if (!select)
1139             selectionTail = caret;
1140     }
1141
1142     protected void setCaret(Point2D point) {
1143         setCaret(point, false);
1144     }
1145
1146     @SyncField({"caret","selectionTail"})
1147     protected void setCaret(Point2D point, boolean select) {
1148         double lineY = 0;
1149         for(int i = 0; i < lines.length; i++) {
1150             Line line = lines[i];
1151             Rectangle2D bounds = line.abbox;
1152             // Add heights of bboxes for determining the correct line
1153             if(i == 0)
1154                 lineY = bounds.getY();
1155             else
1156                 lineY += lines[i-1].abbox.getHeight();
1157             
1158             double lineHeight = bounds.getHeight();
1159             double hitY = point.getY() / scale;
1160             if(hitY >= lineY && hitY <= lineY + lineHeight) {
1161                 // Hit is in this line
1162                 float x = (float)(point.getX() / scale) - (float)line.abbox.getX();
1163                 float y = (float)(point.getY() / scale -  lineHeight * i) ;
1164                 TextHitInfo info = line.layout.hitTestChar(x, y);
1165                 caret = line.startOffset + info.getInsertionIndex();
1166                 if (caret > line.endOffset)
1167                         caret = line.endOffset;
1168                 if (!select)
1169                     selectionTail = caret;
1170                 repaint();
1171                 break;
1172             }
1173         }
1174         invalidateXOffset();
1175         assert (caret <= text.length());
1176     }
1177     
1178     @Override
1179     public Rectangle2D getBoundsInLocal() {
1180         if(targetBounds != null)
1181             return targetBounds;
1182         else
1183             return expandBounds( getTightAlignedBoundsInLocal(null) );
1184     }
1185
1186     protected Rectangle2D expandBounds(Rectangle2D r) {
1187         r.setRect(r.getX() * scale - paddingX, r.getY() * scale -paddingY, r.getWidth()*scale + paddingX + paddingX, r.getHeight()*scale + paddingY + paddingY);
1188         //System.out.println("  => " + r);
1189         return r;
1190     }
1191
1192     protected Rectangle2D expandBoundsUnscaled(Rectangle2D r) {
1193         r.setRect(r.getX() - scaleRecip*paddingX, r.getY() -scaleRecip*paddingY, r.getWidth() + scaleRecip*paddingX + scaleRecip*paddingX, r.getHeight() + scaleRecip*paddingY + scaleRecip*paddingY);
1194         //System.out.println("  => " + r);
1195         return r;
1196     }
1197
1198     protected Rectangle2D expandBounds(Rectangle2D r, double amount) {
1199         r.setRect(r.getX() - amount, r.getY() - amount, r.getWidth() + 2*amount, r.getHeight() + 2*amount);
1200         return r;
1201     }
1202
1203     protected Rectangle2D expandBounds(Rectangle2D r, double left, double top, double right, double bottom) {
1204         r.setRect(r.getX() - left, r.getY() - top, r.getWidth() + left + right, r.getHeight() + top + bottom);
1205         return r;
1206     }
1207
1208     private void resetCaches() {
1209         this.tightBoundsCache = null;
1210         this.lines = null;
1211         this.fontMetrics = null;
1212     }
1213
1214     /**
1215      * Returns the tight bounds around the current text using the current font
1216      * in the specified rectangle. If the specified rectangle is
1217      * <code>null</code> a new Rectangle2D.Double instance will be created.
1218      * 
1219      * @param r
1220      * @return
1221      */
1222     protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r) {
1223         return getTightAlignedBoundsInLocal(r, FRC);
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      *            the rectangle where the result of the method is placed or
1233      *            <code>null</code> to allocate new rectangle
1234      * @param frc current font render context
1235      * @return r or new Rectangle2D.Double instance containing the requested
1236      *         text bounds
1237      */
1238     protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r, FontRenderContext frc) {
1239         if (r == null)
1240             r = new Rectangle2D.Double();
1241
1242         if (tightBoundsCache != null) {
1243             r.setFrame(tightBoundsCache);
1244             return r;
1245         }
1246
1247         String txt = text;
1248         if (font == null || txt == null) {
1249             r.setFrame(0, 0, 2, 1);
1250             return r;
1251         }
1252
1253         //System.out.println("TextNode.getTightAlignedBoundsInLocal('" + txt + "')");
1254
1255         // Parse & layout (unaligned)
1256         Line[] lines = null;
1257         
1258         if(hasState(STATE_WRAP_TEXT)) {
1259             float width = fixedWidth;
1260             if(width <= 0 && targetBounds != null)
1261                 width = (float) (((targetBounds.getWidth() - 2*paddingX)) * scaleRecip);
1262             if(width > 0)
1263                 lines = wrapLines(txt, font, width, frc);
1264         }
1265          
1266         if(lines == null)
1267             lines = parseLines(txt);
1268         this.lines = layoutLines(lines, frc);
1269
1270         // Calculate tight bounds based on unaligned layout
1271         //System.out.println("Unaligned");
1272         tightBoundsCache = calculateBounds(lines, Line.BBOX, null);
1273         //System.out.println("  => " + tightBoundsCache);
1274
1275         this.lines = layoutLinesX(lines, tightBoundsCache);
1276         // Align each line to the calculated tight bounds
1277         this.lines = alignLines(this.lines, tightBoundsCache, horizontalAlignment, verticalAlignment);
1278
1279         // Calculate aligned bounds
1280         //System.out.println("Aligned");
1281         calculateBounds(lines, Line.ABBOX, tightBoundsCache);
1282
1283         r.setFrame(tightBoundsCache);
1284         //System.out.println("  => " + tightBoundsCache);
1285
1286         return r;
1287     }
1288
1289     /**
1290      * @param lines
1291      * @param bbox
1292      *            the bounding box of all the whole laid out text (only bbox
1293      *            size is used)
1294      * @return
1295      */
1296     private Line[] layoutLinesX(Line[] lines, Rectangle2D bbox) {
1297         int lineCount = lines.length;
1298         for (int l = 0; l < lineCount; ++l) {
1299             Line line = lines[l];
1300             // Compute pen x position. If the paragraph is right-to-left we
1301             // will align the TextLayouts to the right edge of the panel.
1302             // Note: drawPosX is always where the LEFT of the text is placed.
1303             // NOTE: This changes based on horizontal alignment
1304             line.drawPosX = (float) (line.layout.isLeftToRight() ? 0f
1305                     : tightBoundsCache.getWidth() - line.layout.getAdvance());
1306         }
1307         return lines;
1308     }
1309
1310     /**
1311      * @param lines
1312      * @param boundsProvider
1313      * @param result
1314      * @return
1315      */
1316     private static Rectangle2D calculateBounds(Line[] lines, BoundsProcedure boundsProvider, Rectangle2D result) {
1317         if (result == null)
1318             result = new Rectangle2D.Double();
1319         else
1320             result.setFrame(0, 0, 0, 0);
1321
1322         for (Line line : lines) {
1323             //System.out.println("line: " + line);
1324             Rectangle2D bbox = boundsProvider.getBounds(line);
1325             if (result.isEmpty())
1326                 result.setFrame(bbox);
1327             else
1328                 Rectangle2D.union(result, bbox, result);
1329             //System.out.println("bounds: " + result);
1330         }
1331         //System.out.println("final bounds: " + result);
1332
1333         return result;
1334     }
1335
1336     /**
1337      * @param lines
1338      * @param bbox
1339      * @param hAlign
1340      * @param vAlign
1341      * @return aligned lines
1342      */
1343     private Line[] alignLines(Line[] lines, Rectangle2D bbox, byte hAlign, byte vAlign) {
1344 //        System.out.println("horizontal align: " + Alignment.values()[hAlign]);
1345 //        System.out.println("vertical align  : " + Alignment.values()[vAlign]);
1346 //        System.out.println("bbox: " + bbox);
1347         double xbase = 0;
1348 //        double ybase = 0;
1349         if(targetBounds != null) {
1350             /* In normal cases the bounding box moves when
1351              * typing. If target bounds are set, the text
1352              * is fitted into the box.
1353              */
1354             switch (hAlign) {
1355             case 1: // Trailing
1356                 xbase = (targetBounds.getMaxX()-paddingX) * scaleRecip;
1357                 break;
1358             case 2: // Center
1359                 xbase = targetBounds.getCenterX() * scaleRecip;
1360                 break;
1361             default: // Leading / Baseline
1362                 // Do nothing
1363                 break;
1364             }
1365         }
1366         
1367         
1368         for (Line line : lines) {
1369             double xoffset = 0;
1370             double yoffset = 0;
1371
1372             switch (hAlign) {
1373             case 1: // Trailing
1374                 xoffset = xbase - line.bbox.getWidth();
1375                 break;
1376             case 2: // Center
1377                 xoffset = xbase - line.bbox.getWidth() / 2;
1378                 break;
1379             default: // Leading / Baseline
1380                 // Do nothing
1381                 break;
1382             }
1383
1384             switch (vAlign) {
1385             case 0:
1386                 yoffset = line.layout.getAscent();
1387                 break;
1388             case 1:
1389                 yoffset = -bbox.getHeight() + line.layout.getAscent();
1390                 break;
1391             case 2:
1392                 yoffset = -bbox.getHeight() / 2 + line.layout.getAscent();
1393                 break;
1394             }
1395
1396             line.alignOffset(xoffset, yoffset);
1397         }
1398         return lines;
1399     }
1400
1401     /**
1402      * @param lines
1403      * @param frc
1404      * @return
1405      */
1406     private Line[] layoutLines(Line[] lines, FontRenderContext frc) {
1407         TextLayout emptyRowLayout = null;
1408         int lineCount = lines.length;
1409         float y = 0;
1410         for (int l = 0; l < lineCount; ++l) {
1411             Line line = lines[l];
1412             String lineText = line.getText();
1413             // " " because TextLayout requires non-empty text and
1414             // We don't want zero size for the text.
1415             if (lineText.isEmpty()) {
1416                 lineText = " ";
1417                 if (emptyRowLayout == null)
1418                     emptyRowLayout = new TextLayout(lineText, font, frc);
1419                 line.layout = emptyRowLayout;
1420             } else {
1421                 line.layout = new TextLayout(lineText, font, frc);
1422             }
1423
1424             //y += line.layout.getAscent();
1425             line.drawPosY = y;
1426             y += line.layout.getDescent() + line.layout.getLeading() + line.layout.getAscent();
1427
1428             Rectangle2D bbox = line.layout.getLogicalHighlightShape(0, lineText.length()).getBounds2D();
1429             bbox.setFrame(bbox.getX(), bbox.getY() + line.drawPosY, bbox.getWidth(), bbox.getHeight());
1430             line.bbox = bbox;
1431         }
1432
1433         return lines;
1434     }
1435
1436     /**
1437      * Splits the specified string into {@link Line} structures, one for each
1438      * line in the input text. The returned lines are only partially defined,
1439      * waiting to be laid out (see
1440      * {@link #layoutLines(Line[], FontRenderContext)})
1441      * 
1442      * @param txt
1443      *            input text
1444      * @return parsed text lines as {@link Line} structures
1445      * @see #layoutLines(Line[], FontRenderContext)
1446      */
1447     private static Line[] parseLines(String txt) {
1448         int len = txt.length();
1449         if (len == 0)
1450             return new Line[] { new Line("", 0, 0) };
1451
1452         TIntArrayList lfpos = new TIntArrayList();
1453         int pos = 0;
1454         int lineCount = 1;
1455         for (;pos < len; ++lineCount) {
1456             int nextlf = txt.indexOf('\n', pos);
1457             lfpos.add(nextlf != -1 ? nextlf : len);
1458             if (nextlf == -1)
1459                 break;
1460             pos = nextlf + 1;
1461         }
1462         Line[] lines = new Line[lineCount];
1463         pos = 0;
1464         for (int i = 0; i < lineCount-1; ++i) {
1465             int lf = lfpos.getQuick(i);
1466             int cr = (lf > 0) && (txt.charAt(lf - 1) == '\r') ? lf - 1 : lf;
1467             lines[i] = new Line(txt, pos, cr);
1468             pos = lf + 1;
1469         }
1470         lines[lineCount - 1] = new Line(txt, pos, len);
1471
1472         return lines;
1473     }
1474     
1475     
1476     private static Line[] wrapLines(String txt, Font font, float fixedWidth, FontRenderContext frc) {
1477         if(txt == null || txt.isEmpty())
1478             txt = " ";
1479         
1480         ArrayList<Line> lines = 
1481                 new ArrayList<Line>();
1482         
1483         Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
1484         map.put(TextAttribute.FONT, font);
1485         AttributedString attributedText = new AttributedString(txt.isEmpty() ? "__ERROR__" : txt, map);
1486
1487         AttributedCharacterIterator paragraph = attributedText.getIterator();
1488         int paragraphStart = paragraph.getBeginIndex();
1489         int paragraphEnd = paragraph.getEndIndex();
1490         LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
1491
1492         float breakWidth = fixedWidth;
1493
1494         // Force text to be vertical, by setting break width to 1, if the text area is narrower than "GGGG"
1495
1496         // Set position to the index of the first character in the paragraph.
1497         lineMeasurer.setPosition(paragraphStart);
1498
1499         // Get lines until the entire paragraph has been displayed.
1500         int next, limit, charat, position = 0;
1501         
1502         while ((position = lineMeasurer.getPosition()) < paragraphEnd) {
1503
1504             // Find possible line break and set it as a limit to the next layout
1505             next = lineMeasurer.nextOffset(breakWidth);
1506             limit = next;
1507             charat = txt.indexOf(System.getProperty("line.separator"),position+1);
1508             if(charat < next && charat != -1){
1509                 limit = charat;
1510             }
1511             
1512             lineMeasurer.nextLayout(breakWidth, limit, false);
1513             // Add Line
1514             lines.add(new Line(txt, position, limit));
1515         }
1516
1517         return lines.toArray(new Line[lines.size()]);
1518     }
1519     
1520
1521     public String getClipboardContent() {
1522         Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1523         Transferable clipData = clipboard.getContents(this);
1524         try {
1525             return (String) (clipData.getTransferData(DataFlavor.stringFlavor));
1526         } catch (Exception ee) {
1527             return null;
1528         }
1529     }
1530
1531     public void setClipboardContent(String content) {
1532         Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
1533         StringSelection data = new StringSelection(content);
1534         clipboard.setContents(data, data);
1535     }
1536
1537     @Override
1538     public String toString() {
1539         return super.toString() + " [text=" + text + ", font=" + font + ", color=" + color + "]";
1540     }
1541
1542     @Override
1543     protected boolean handleCommand(CommandEvent e) {
1544         if (!hasState(STATE_EDITING))
1545             return false;
1546
1547         if (Commands.SELECT_ALL.equals(e.command)) {
1548             selectAll();
1549             return true;
1550         }
1551         return false;
1552     }
1553
1554     @Override
1555     protected boolean keyPressed(KeyPressedEvent event) {
1556         if (!hasState(STATE_EDITING))
1557             return false;
1558
1559         char c = event.character;
1560         boolean ctrl = event.isControlDown();
1561         boolean alt = event.isAltDown();
1562
1563 //        System.out.println("Key pressed '" + (event.character == 0 ? "\\0" : "" + c) + "' " + event.keyCode + " - " + Integer.toBinaryString(event.stateMask));
1564 //        System.out.println("ctrl: " + ctrl);
1565 //        System.out.println("alt: " + alt);
1566         if (ctrl && !alt) {
1567             switch (event.keyCode) {
1568                 case KeyEvent.VK_C:
1569                     if (caret != selectionTail) {
1570                         int selectionMin = Math.min(caret, selectionTail);
1571                         int selectionMax = Math.max(caret, selectionTail);
1572                         setClipboardContent(text.substring(selectionMin, selectionMax));
1573                     }
1574                     break;
1575                     
1576                 case KeyEvent.VK_X:
1577                     if (caret != selectionTail) {
1578                         int selectionMin = Math.min(caret, selectionTail);
1579                         int selectionMax = Math.max(caret, selectionTail);
1580                         setClipboardContent(text.substring(selectionMin, selectionMax));
1581                         insert("");
1582                     }
1583                     break;
1584
1585                 case KeyEvent.VK_RIGHT:
1586                     if (c == '\0')  {
1587                         // '\'' has the same keycode as VK_RIGHT but when right
1588                         // arrow is pressed, event character is \0.
1589                         moveCaretCtrlRight(event.isShiftDown());
1590                     }
1591                     break;
1592                     
1593                 case KeyEvent.VK_LEFT:
1594                         moveCaretCtrlLeft(event.isShiftDown());
1595                         break;
1596                     
1597                 case KeyEvent.VK_V:
1598                 {
1599                     String content = getClipboardContent();
1600                     if(content != null)
1601                         insert(content);
1602                     break;
1603                 }
1604
1605                 // Replaced by #handleCommand
1606 //                case KeyEvent.VK_A:
1607 //                {
1608 //                    selectAll();
1609 //                    return true;
1610 //                }
1611                 
1612                 case KeyEvent.VK_ENTER:
1613                 {
1614                     insert(getLineSeparator());
1615                 }
1616                 
1617                 break;
1618
1619                 default:
1620                     return false;
1621             }
1622         } else if (!ctrl && alt) {
1623             return false;
1624         } else {
1625             switch (event.keyCode) {
1626                 case KeyEvent.VK_LEFT:
1627                         moveCaret(-1, event.isShiftDown());
1628                     break;
1629                 case KeyEvent.VK_RIGHT:
1630                     if (c == '\0')  {
1631                         // '\'' has the same keycode as VK_RIGHT but when right
1632                         // arrow is pressed, event character is \0.
1633                         moveCaret(1, event.isShiftDown());
1634                         break;
1635                     }
1636                     // Intentional fallthrough to default case
1637                 case KeyEvent.VK_UP:
1638                     moveCaretRowUp(event.isShiftDown());
1639                     break;
1640                 case KeyEvent.VK_DOWN:
1641                     moveCaretRowDown(event.isShiftDown());
1642                     break;
1643                 case KeyEvent.VK_HOME:
1644                     moveCaretHome(event.isShiftDown());
1645                     break;
1646                 case KeyEvent.VK_END:
1647                     moveCaretEnd(event.isShiftDown());
1648                     break;
1649
1650                 case KeyEvent.VK_ENTER:
1651                     fireTextEditingEnded();
1652                     return true;
1653
1654                 case KeyEvent.VK_ESCAPE:
1655                     text = textBeforeEdit;
1656                     resetCaches();
1657                     clearState(STATE_EDITING);
1658                     fireTextEditingCancelled();
1659                     return true;
1660
1661                 case KeyEvent.VK_BACK_SPACE:
1662                     if(caret == selectionTail && caret > 0) {
1663                         // line separator may use multiple characters, we want to remove that with one command
1664                         String lineSep = getLineSeparator();
1665                         int index = lineSep.indexOf(text.charAt(caret-1));
1666                         if (index == -1)
1667                                 --caret;
1668                         else {
1669                                 caret-= (index+1);
1670                                 selectionTail+= (lineSep.length()-index-1);
1671                         }
1672                     }
1673                     insert("");
1674                     break;
1675
1676                 case KeyEvent.VK_DELETE:
1677                     if(caret == selectionTail && caret < text.length()) {
1678                         String lineSep = getLineSeparator();
1679                         int index = lineSep.indexOf(text.charAt(caret));
1680                         if (index==-1)
1681                                 ++caret;
1682                         else {
1683                                 selectionTail-= index;
1684                                 caret+= (lineSep.length()-index);
1685                         }
1686                     }
1687                     insert("");
1688                     break;
1689
1690                 
1691
1692                 default:
1693                     if (c == 65535 || Character.getType(c) == Character.CONTROL) {
1694                         return false;
1695                     }
1696                     //System.out.println("Char " + c + " " + Character.getType(c) + " " + text);
1697                     insert(new String(new char[] {c}));
1698             }
1699         }
1700
1701         // FIXME This is called even if just caret was moved.
1702         // This is currently necessary for repaints.
1703         fireTextChanged();
1704         invalidateXOffset();
1705         return true;
1706     }
1707     
1708     protected String getLineSeparator() {
1709         return System.getProperty("line.separator");
1710     }
1711
1712     protected void selectAll() {
1713         setCaret(0, false);
1714         setCaret(text.length(), true);
1715     }
1716
1717     protected transient int hoverClick = 0;
1718
1719     @Override
1720     protected boolean mouseClicked(MouseClickEvent event) {
1721         if (event.button != MouseClickEvent.LEFT_BUTTON)
1722             return false;
1723         
1724         if (hasState(STATE_HOVER)) {
1725                 hoverClick++;
1726                 if (hoverClick < 2)
1727                         return false;
1728             ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1729             // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1730             if (ctx == null)
1731                 return false;
1732             IElement e = DiagramNodeUtil.getElement(ctx, this);
1733             if (!hasState(STATE_EDITING)) {
1734                 if (Boolean.TRUE.equals(setEditMode(true))) {
1735                         editActivation = activateEdit(0, e, ctx);
1736                         repaint();
1737                 }
1738             } 
1739         } else {
1740                 hoverClick = 0;
1741             if (hasState(STATE_EDITING)) {
1742                 fireTextEditingEnded();
1743             }
1744         }
1745         return false;
1746     }
1747     
1748     protected boolean mouseDoubleClicked(MouseDoubleClickedEvent event) {
1749         if (event.button != MouseClickEvent.LEFT_BUTTON)
1750             return false;
1751         
1752         if (hitTest(event, 0)) {
1753             ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
1754             // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
1755             if (ctx == null)
1756                 return false;
1757             
1758             if (text != null) {
1759                 // Select the whole text.
1760                 setCaret(0, false);
1761                 setCaret(text.length(), true);
1762                 repaint();
1763             }
1764         }
1765         return false;
1766     }
1767
1768
1769     @Override
1770     protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {
1771         if (!hasState(STATE_EDITING))
1772             return false;
1773         
1774         Point2D local = controlToLocal( event.controlPosition );
1775         // FIXME: once the event coordinate systems are cleared up, remove this workaround
1776         local = parentToLocal(local);
1777         if (hasState(STATE_HOVER) && this.containsLocal(local)) {
1778             setCaret(local, event.isShiftDown());
1779         }
1780         return false;
1781     }
1782
1783     @Override
1784     protected boolean mouseMoved(MouseMovedEvent event) {
1785         boolean hit = hitTest(event, 3.0);
1786         if (hit != hasState(STATE_HOVER)) {
1787             setState(STATE_HOVER, hit);
1788             repaint();
1789         }
1790         return false;
1791     }
1792
1793     private boolean isControlDown(MouseEvent e) {
1794         return e.isControlDown() || lastMouseEvent!=null?lastMouseEvent.isControlDown():false;
1795     }
1796
1797     protected boolean isShiftDown(MouseEvent e) {
1798         return e.isShiftDown() || lastMouseEvent!=null?lastMouseEvent.isShiftDown():false;
1799     }
1800
1801 //    private boolean isAltDown(MouseEvent e) {
1802 //      return e.isAltDown() || lastMouseEvent!=null?lastMouseEvent.isAltDown():false;
1803 //    }
1804
1805     @Override
1806     protected boolean mouseDragged(MouseDragBegin e) {
1807         if (isHovering()
1808                 && (isControlDown(e) || isShiftDown(e))
1809                 && (dataRVI != null || text != null))
1810         {
1811             List<Transferable> trs = new ArrayList<>(2);
1812             if (dataRVI != null) {
1813                 trs.add(new LocalObjectTransferable(dataRVI));
1814                 trs.add(new PlaintextTransfer(dataRVI.toString()));
1815             } else if (text != null && !text.isEmpty()) {
1816                 trs.add(new PlaintextTransfer(text));
1817             }
1818             if (!trs.isEmpty()) {
1819                 e.transferable = new MultiTransferable(trs);
1820                 return true;
1821             }
1822         }
1823         return false;
1824     }
1825
1826     protected boolean hitTest(MouseEvent event, double tolerance) {
1827         Rectangle2D bounds = getBoundsInternal();
1828         if (bounds == null)
1829             return false;
1830         Point2D localPos = NodeUtil.worldToLocal(this, event.controlPosition, new Point2D.Double());
1831         double x = localPos.getX();
1832         double y = localPos.getY();
1833         boolean hit = bounds.contains(x, y);
1834         return hit;
1835     }
1836
1837     public Rectangle2D getBoundsInternal() {
1838         Rectangle2D local = lastBounds;
1839         if (local == null)
1840             return null;
1841         // TODO: potential spot for CPU/memory allocation optimization
1842         // by using more specialized implementations
1843         if (transform.isIdentity())
1844             return local;
1845         return transform.createTransformedShape(local).getBounds2D();
1846     }
1847
1848     protected Color add(Color c, int r, int g, int b)  {
1849         int nr = Math.min(255, c.getRed() + r);
1850         int ng = Math.min(255, c.getGreen() + g);
1851         int nb = Math.min(255, c.getBlue() + b);
1852         return new Color(nr, ng, nb);
1853     }
1854
1855     public TextEditActivation activateEdit(int mouseId, IElement e, ICanvasContext ctx) {
1856         EditDataNode data = EditDataNode.getNode(this);
1857         deactivateEdit(data, null);
1858         TextEditActivation result = new TextEditActivation(mouseId, e, ctx);
1859         data.setTextEditActivation(result);
1860         return result;
1861     }
1862
1863     /**
1864      * @return <code>true</code> if this node is or was previously in editing
1865      *         state
1866      */
1867     protected boolean deactivateEdit() {
1868         boolean result = deactivateEdit( editActivation );
1869         result |= editActivation != null;
1870         editActivation = null;
1871         return result;
1872     }
1873
1874     protected boolean deactivateEdit(TextEditActivation activation) {
1875         return deactivateEdit( EditDataNode.getNode(this), activation );
1876     }
1877
1878     protected boolean deactivateEdit(EditDataNode data, TextEditActivation activation) {
1879         TextEditActivation previous = data.getTextEditActivation();
1880         if (previous != null && (previous == activation || activation == null)) {
1881             previous.release();
1882             data.setTextEditActivation(null);
1883             return true;
1884         }
1885         return false;
1886     }
1887
1888     @Override
1889     public int getEventMask() {
1890         return EventTypes.KeyPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask
1891                 | EventTypes.MouseClickMask | EventTypes.MouseDragBeginMask | EventTypes.CommandMask;
1892     }
1893
1894     private MouseEvent lastMouseEvent = null;
1895     
1896     @Override
1897     public boolean handleEvent(Event e) {
1898         if(e instanceof MouseEvent && !(e instanceof MouseDragBegin)) lastMouseEvent = (MouseEvent)e;
1899         return super.handleEvent(e);
1900     }
1901
1902         @Override
1903         public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
1904                 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
1905         }
1906         
1907         @Override
1908         public <T> T getProperty(String propertyName) {
1909                 return null;
1910         }
1911         
1912         @Override
1913         public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
1914         }
1915         
1916         public void synchronizeText(String text) {
1917                 setText(text);
1918         }
1919
1920         public void synchronizeColor(RGB.Integer color) {
1921                 this.color = Colors.awt(color);
1922         }
1923
1924         public void synchronizeFont(org.simantics.datatypes.literal.Font font) {
1925                 setFont(Fonts.awt(font));
1926         }
1927
1928         public void synchronizeTransform(double[] data) {
1929                 this.setTransform(new AffineTransform(data));
1930         }
1931
1932         public static void main(String[] args) {
1933                 Line[] lines = parseLines("\n  \n FOO  \n\nBAR\n\n\n BAZ\n\n");
1934                 System.out.println(Arrays.toString(lines));
1935                 System.out.println(GeometryUtils.pointToMillimeter(1));
1936                 System.out.println(GeometryUtils.pointToMillimeter(12));
1937                 System.out.println(GeometryUtils.pointToMillimeter(72));
1938         }
1939
1940     ///////////////////////////////////////////////////////////////////////////
1941     // LEGACY CODE NEEDED BY INHERITED CLASSES FOR NOW
1942     ///////////////////////////////////////////////////////////////////////////
1943
1944     protected double getHorizontalAlignOffset(Rectangle2D r) {
1945         switch (horizontalAlignment) {
1946             case 0: return 0; // Leading
1947             case 1: return -r.getWidth(); // Trailing
1948             case 2: return -r.getCenterX(); // Center
1949             default: return 0;
1950         }
1951     }
1952
1953     protected double getVerticalAlignOffset() {
1954         FontMetrics fm = fontMetrics;
1955         if (fm == null)
1956             return 0;
1957         switch (verticalAlignment) {
1958             case 0: return fm.getMaxAscent(); // Leading=top=maxascent
1959             case 1: return -fm.getMaxDescent(); // Trailing=bottom=maxdescent
1960             case 2: return fm.getMaxAscent() / 2; // Center=maxascent / 2
1961             case 3: return 0;
1962             default: return 0;
1963         }
1964     }
1965
1966     ///////////////////////////////////////////////////////////////////////////
1967     // LEGACY CODE ENDS
1968     ///////////////////////////////////////////////////////////////////////////
1969
1970 }