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