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