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