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