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