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