]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.diagram/src/org/simantics/diagram/elements/TextNode.java
Merge "Counting of pending nodes does not work if null text is saved as """
[simantics/platform.git] / bundles / org.simantics.diagram / src / org / simantics / diagram / elements / TextNode.java
index f948bc053c901b7a1e92201e5c4904fb61be6f6a..bf1bf563b32c9f9e89c2c2355902d2a5b5f1ef02 100644 (file)
-/*******************************************************************************\r
- * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
- * in Industry THTH ry.\r
- * All rights reserved. This program and the accompanying materials\r
- * are made available under the terms of the Eclipse Public License v1.0\r
- * which accompanies this distribution, and is available at\r
- * http://www.eclipse.org/legal/epl-v10.html\r
- *\r
- * Contributors:\r
- *     VTT Technical Research Centre of Finland - initial API and implementation\r
- *******************************************************************************/\r
-package org.simantics.diagram.elements;\r
-\r
-import gnu.trove.list.array.TIntArrayList;\r
-\r
-import java.awt.AlphaComposite;\r
-import java.awt.BasicStroke;\r
-import java.awt.Color;\r
-import java.awt.Composite;\r
-import java.awt.Font;\r
-import java.awt.FontMetrics;\r
-import java.awt.Graphics2D;\r
-import java.awt.RenderingHints;\r
-import java.awt.Shape;\r
-import java.awt.Toolkit;\r
-import java.awt.datatransfer.Clipboard;\r
-import java.awt.datatransfer.DataFlavor;\r
-import java.awt.datatransfer.StringSelection;\r
-import java.awt.datatransfer.Transferable;\r
-import java.awt.event.KeyEvent;\r
-import java.awt.font.FontRenderContext;\r
-import java.awt.font.LineBreakMeasurer;\r
-import java.awt.font.TextAttribute;\r
-import java.awt.font.TextHitInfo;\r
-import java.awt.font.TextLayout;\r
-import java.awt.geom.AffineTransform;\r
-import java.awt.geom.Point2D;\r
-import java.awt.geom.Rectangle2D;\r
-import java.io.IOException;\r
-import java.text.AttributedCharacterIterator;\r
-import java.text.AttributedString;\r
-import java.util.ArrayList;\r
-import java.util.Arrays;\r
-import java.util.Hashtable;\r
-\r
-import org.simantics.datatypes.literal.RGB;\r
-import org.simantics.db.layer0.variable.RVI;\r
-import org.simantics.diagram.elements.Line.BoundsProcedure;\r
-import org.simantics.g2d.canvas.ICanvasContext;\r
-import org.simantics.g2d.element.IElement;\r
-import org.simantics.scenegraph.IDynamicSelectionPainterNode;\r
-import org.simantics.scenegraph.LoaderNode;\r
-import org.simantics.scenegraph.ScenegraphUtils;\r
-import org.simantics.scenegraph.g2d.G2DNode;\r
-import org.simantics.scenegraph.g2d.G2DPDFRenderingHints;\r
-import org.simantics.scenegraph.g2d.events.Event;\r
-import org.simantics.scenegraph.g2d.events.EventTypes;\r
-import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;\r
-import org.simantics.scenegraph.g2d.events.command.CommandEvent;\r
-import org.simantics.scenegraph.g2d.events.command.Commands;\r
-import org.simantics.scenegraph.g2d.events.NodeEventHandler;\r
-import org.simantics.scenegraph.utils.GeometryUtils;\r
-import org.simantics.scenegraph.utils.NodeUtil;\r
-import org.simantics.scl.runtime.function.Function1;\r
-import org.simantics.scl.runtime.function.Function2;\r
-import org.simantics.ui.colors.Colors;\r
-import org.simantics.ui.dnd.LocalObjectTransferable;\r
-import org.simantics.ui.fonts.Fonts;\r
-import org.simantics.utils.threads.AWTThread;\r
-\r
-import com.lowagie.text.DocumentException;\r
-import com.lowagie.text.Element;\r
-import com.lowagie.text.Rectangle;\r
-import com.lowagie.text.pdf.FontMapper;\r
-import com.lowagie.text.pdf.PdfFormField;\r
-import com.lowagie.text.pdf.PdfWriter;\r
-import com.lowagie.text.pdf.TextField;\r
-\r
-\r
-/**\r
- * TextNode which supports in-line editing.\r
- * \r
- * By default <code>TextNode</code> is in editable = false state. Use\r
- * {@link #setEditable(boolean)} to make it editable.\r
- * \r
- * @author Hannu Niemist&ouml; <hannu.niemisto@vtt.fi>\r
- * @author Marko Luukkainen <marko.luukkainen@vtt.fi>\r
- * @author Tuukka Lehtonen <tuukka.lehtonen@semantum.fi>\r
- * \r
- * TODO:\r
- * o proper support for defining clipping bounds for the text (needed for page templates) (currently through fixedWidth)\r
- * o fix editing xOffset to work with fixed width and multi-line text\r
- * o \r
- * \r
- * @see Line\r
- * @see TextLayout\r
- */\r
-public class TextNode extends G2DNode implements IDynamicSelectionPainterNode, LoaderNode {\r
-\r
-    private static final long                serialVersionUID           = 654692698101485672L;\r
-\r
-    /**\r
-     * TODO: justify existence for this\r
-     */\r
-    private static final BasicStroke         RESET_STROKE               = new BasicStroke(1);\r
-\r
-    /**\r
-     * Src-over alpha composite instance with 50% opacity.\r
-     */\r
-    private static final AlphaComposite      SrcOver_50                 = AlphaComposite.SrcOver.derive(0.5f);\r
-\r
-    /**\r
-     * For (inexact) measurement of rendered text bounds.\r
-     */\r
-    protected static final FontRenderContext FRC = new FontRenderContext(new AffineTransform(), true, true);\r
-\r
-    private static final Font FONT = Font.decode("Arial 6");\r
-    private static final Color SELECTION_BACKGROUND_COLOR = new Color(0x316ac5);\r
-//    private static final double MAX_CARET_POSITION = 1.0;\r
-\r
-    /**\r
-     * The complete text visualized by this node.\r
-     */\r
-    protected String text = null;\r
-\r
-    /**\r
-     * The font used to render the {@link #text}.\r
-     */\r
-    protected Font font = FONT;\r
-\r
-    /**\r
-     * The color of the rendered text. Default value is {@value Color#black}.\r
-     */\r
-    protected Color color = Color.BLACK;\r
-\r
-    /**\r
-     * The background color used for filling the background of the bounding box\r
-     * of the rendered text. <code>null</code> means no fill.\r
-     * Default value is <code>null</code>.\r
-     */\r
-    protected Color backgroundColor = null;\r
-\r
-    /**\r
-     * The color used for drawing the expanded bounding box border for the\r
-     * rendered text. <code>null</code> means no border is rendered. Default\r
-     * value is <code>null</code>.\r
-     */\r
-    protected Color borderColor = null;\r
-\r
-    protected double scale = 1.0;\r
-    protected transient double scaleRecip = 1.0;\r
-\r
-    /**\r
-     * \r
-     */\r
-    protected float borderWidth = 0.f;\r
-\r
-    protected double paddingX = 2.0;\r
-    protected double paddingY = 2.0;\r
-\r
-    /**\r
-     * Horizontal text box alignment with respect to its origin. Default value is\r
-     * 0 (leading).\r
-     */\r
-    protected byte horizontalAlignment = 0;\r
-    /**\r
-     * Vertical text box alignment with respect to its origin. Default value is\r
-     * 3 (baseline).\r
-     */\r
-    protected byte verticalAlignment = 3;\r
-\r
-    /**\r
-     * Tells if this node is still pending for real results or not.\r
-     */\r
-    protected static final int STATE_PENDING = (1 << 0);\r
-    protected static final int STATE_HOVER   = (1 << 1);\r
-    protected static final int STATE_EDITABLE = (1 << 2);\r
-    protected static final int STATE_SHOW_SELECTION = (1 << 3);\r
-    protected static final int STATE_WRAP_TEXT = (1 << 4);\r
-    protected transient static final int STATE_EDITING = (1 << 5);\r
-    protected transient static final int STATE_VALID = (1 << 6);\r
-    protected transient static final int STATE_X_OFFSET_IS_DIRTY = (1 << 7);\r
-    protected static final int STATE_ALWAYS_ADD_LISTENERS = (1 << 8);\r
-    protected static final int STATE_LISTENERS_ADDED = (1 << 9);\r
-\r
-    /**\r
-     * A combination of all the STATE_ constants defined in this class,\r
-     * e.g. {@link #STATE_PENDING}.\r
-     */\r
-    protected int state = STATE_SHOW_SELECTION | STATE_WRAP_TEXT | STATE_VALID | STATE_X_OFFSET_IS_DIRTY;\r
-\r
-    protected RVI dataRVI = null;\r
-\r
-    int caret = 0;\r
-    int selectionTail = 0;\r
-    float xOffset = 0;\r
-\r
-    float fixedWidth = 0f;\r
-\r
-    private Rectangle2D targetBounds;\r
-\r
-    Function1<String, String> validator;\r
-    ITextListener textListener;\r
-    ITextContentFilter editContentFilter;\r
-\r
-    /**\r
-     * The renderable line structures parsed from {@link #text} by\r
-     * {@link #parseLines(String)}, laid out by\r
-     * {@link #layoutLines(Line[], FontRenderContext)} and aligned by\r
-     * {@link #alignLines(Line[], Rectangle2D, byte, byte)}\r
-     */\r
-    protected transient Line[]           lines                      = null;\r
-    protected transient FontMetrics      fontMetrics                = null;\r
-\r
-    /**\r
-     * Stores the value of {@link #text} before edit mode was last entered. Used\r
-     * for restoring the original value if editing is cancelled.\r
-     */\r
-    private transient String             textBeforeEdit             = null;\r
-    protected transient TextEditActivation editActivation;\r
-\r
-    /**\r
-     * Stores the last scaled bounds.\r
-     */\r
-    private transient Rectangle2D        lastBounds = new Rectangle2D.Double();\r
-\r
-    /**\r
-     * This must be nullified if anything that affects the result of\r
-     * {@link #getTightAlignedBoundsInLocal(Rectangle2D, FontRenderContext)}\r
-     * changes. It will cause the cached value to be recalculated on the next\r
-     * request.\r
-     */\r
-    private transient Rectangle2D        tightBoundsCache = null;\r
-\r
-    @Override\r
-    public void init() {\r
-        super.init();\r
-        // Mark this node as pending\r
-        NodeUtil.increasePending(this);\r
-    }\r
-\r
-    @Override\r
-    public void cleanup() {\r
-        removeListeners();\r
-        super.cleanup();\r
-    }\r
-\r
-    protected boolean hasState(int flags) {\r
-        return (state & flags) == flags;\r
-    }\r
-\r
-    protected void setState(int flags) {\r
-        this.state |= flags;\r
-    }\r
-\r
-    protected void setState(int flags, boolean set) {\r
-        if (set)\r
-            this.state |= flags;\r
-        else\r
-            this.state &= ~flags;\r
-    }\r
-\r
-    protected void clearState(int flags) {\r
-        this.state &= ~flags;\r
-    }\r
-\r
-    protected void setListeners(boolean add) {\r
-        if (add)\r
-            addListeners();\r
-        else\r
-            removeListeners();\r
-    }\r
-\r
-    protected void addListeners() {\r
-        if (!hasState(STATE_LISTENERS_ADDED)) {\r
-            addEventHandler(this);\r
-            setState(STATE_LISTENERS_ADDED);\r
-        }\r
-    }\r
-\r
-    protected void removeListeners() {\r
-        if (hasState(STATE_LISTENERS_ADDED)) {\r
-            removeEventHandler(this);\r
-            clearState(STATE_LISTENERS_ADDED);\r
-        }\r
-    }\r
-\r
-    /**\r
-     * 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
-     * @param force\r
-     */\r
-    public void setForceEventListening(boolean force) {\r
-        setState(STATE_ALWAYS_ADD_LISTENERS, force);\r
-        if (force && !hasState(STATE_EDITABLE)) {\r
-            setListeners(force);\r
-        }\r
-    }\r
-\r
-    /**\r
-     * Enables or disables edit mode. It also sets\r
-     * the caret at the end of text all selects the\r
-     * whole text (this is the usual convention when\r
-     * beginning to edit one line texts).\r
-     * @param edit\r
-     * @return null if no change to edit state was made\r
-     */\r
-    public Boolean setEditMode(boolean edit) {\r
-        return setEditMode(edit, true);\r
-    }\r
-\r
-    /**\r
-     * Enables or disables edit mode. It also sets\r
-     * the caret at the end of text all selects the\r
-     * whole text (this is the usual convention when\r
-     * beginning to edit one line texts).\r
-     * @param edit\r
-     * @return null if no change to edit state was made\r
-     */\r
-    protected Boolean setEditMode(boolean edit, boolean notify) {\r
-        if (edit && !hasState(STATE_EDITABLE))\r
-            return null;\r
-        if (hasState(STATE_EDITING) == edit)\r
-            return null;\r
-        setState(STATE_EDITING);\r
-        if (edit) {\r
-            caret = text != null ? text.length() : 0;\r
-            selectionTail = 0;\r
-            textBeforeEdit = text;\r
-            if (notify)\r
-                fireTextEditingStarted();\r
-            return Boolean.TRUE;\r
-        } else {\r
-            if (notify)\r
-                fireTextEditingEnded();\r
-            return Boolean.FALSE;\r
-        }\r
-    }\r
-\r
-    @SyncField({"editable"})\r
-    public void setEditable(boolean editable) {\r
-        boolean changed = hasState(STATE_EDITABLE) != editable;\r
-        setState(STATE_EDITABLE, editable);\r
-        if (hasState(STATE_EDITING) && !editable)\r
-            setEditMode(false);\r
-        if (changed && !hasState(STATE_ALWAYS_ADD_LISTENERS)) {\r
-            setListeners(editable);\r
-        }\r
-    }\r
-\r
-    public boolean isEditable() {\r
-        return hasState(STATE_EDITABLE);\r
-    }\r
-\r
-    public boolean isEditMode() {\r
-        return hasState(STATE_EDITING);\r
-    }\r
-    \r
-    @SyncField({"wrapText"})\r
-    public void setWrapText(boolean wrapText) {\r
-        setState(STATE_WRAP_TEXT, wrapText);\r
-    }\r
-    \r
-    /**\r
-     * @return Does the text box wrap text if \r
-     * the width of the box is fixed\r
-     */\r
-    public boolean isWrapText() {\r
-        return hasState(STATE_WRAP_TEXT);\r
-    }\r
-\r
-    @SyncField({"showSelection"})\r
-    public void setShowSelection(boolean showSelection) {\r
-        setState(STATE_SHOW_SELECTION, showSelection);\r
-    }\r
-\r
-    public boolean showsSelection() {\r
-        return hasState(STATE_SHOW_SELECTION);\r
-    }\r
-\r
-    /**\r
-     * @param text\r
-     * @param font\r
-     * @param color\r
-     * @param x not supported anymore, use {@link #setTransform(AffineTransform)} instead\r
-     * @param y not supported anymore, use {@link #setTransform(AffineTransform)} instead\r
-     * @param scale\r
-     */\r
-    @SyncField({"text", "font", "color", "x", "y", "scale"})\r
-    public void init(String text, Font font, Color color, double x, double y, double scale) {\r
-        // no value => value\r
-        if(this.text == null && text != null) NodeUtil.decreasePending(this);\r
-\r
-        if (hasState(STATE_EDITING))\r
-            return;\r
-\r
-        this.text = new String(text != null ? text : "");\r
-        this.font = font;\r
-        this.color = color;\r
-        this.scale = scale;\r
-        this.scaleRecip = 1.0 / scale;\r
-        this.caret = 0;\r
-        this.selectionTail = 0;\r
-\r
-        resetCaches();\r
-    }\r
-\r
-    @SyncField({"paddingX", "paddingY"})\r
-    public void setPadding(double x, double y) {\r
-        this.paddingX = x;\r
-        this.paddingY = y;\r
-    }\r
-\r
-    @SyncField({"color"})\r
-    public void setColor(Color color) {\r
-        this.color = color;\r
-    }\r
-\r
-    @SyncField({"backgroundColor"})\r
-    public void setBackgroundColor(Color color) {\r
-        this.backgroundColor = color;\r
-    }\r
-\r
-    @SyncField({"borderColor"})\r
-    public void setBorderColor(Color color) {\r
-        this.borderColor = color;\r
-    }\r
-\r
-    public String getText() {\r
-        return text;\r
-    }\r
-    \r
-    public String getTextBeforeEdit() {\r
-       return textBeforeEdit;\r
-    }\r
-\r
-    @SyncField({"text","caret","selectionTail"})\r
-    public void setText(String text) {\r
-        //System.out.println("TextNode.setText('" + text + "', " + editing + ")");\r
-        if (hasState(STATE_EDITING))\r
-            return;\r
-\r
-        // value => no value\r
-        if(this.text != null && text == null) NodeUtil.increasePending(this);\r
-        // no value => value\r
-        if(this.text == null && text != null) NodeUtil.decreasePending(this);\r
-\r
-        this.text = text != null ? text : "";\r
-        caret = Math.min(caret, this.text.length());\r
-        selectionTail = caret;\r
-\r
-        resetCaches();\r
-    }\r
-\r
-    @SyncField({"pending"})\r
-    public void setPending(boolean pending) {\r
-        boolean p = hasState(STATE_PENDING);\r
-        if(!p && pending) NodeUtil.increasePending(this);\r
-        if(p && !pending) NodeUtil.decreasePending(this);\r
-        if(p != pending)\r
-            setState(STATE_PENDING, pending);\r
-    }\r
-\r
-    @SyncField({"fixedWidth"})\r
-    public void setFixedWidth(float fixedWidth) {\r
-        if (fixedWidth < 0f)\r
-            throw new IllegalArgumentException("negative fixed width");\r
-        this.fixedWidth = fixedWidth;\r
-        invalidateXOffset();\r
-    }\r
-    \r
-    /**\r
-     * Bounds where the text box will be drawn\r
-     * @param bounds\r
-     */\r
-    public void setTargetBounds(Rectangle2D bounds) {\r
-        this.targetBounds = bounds;\r
-    }\r
-\r
-       final public void synchronizeWidth(float width) {\r
-               if (width >= 0.0f)\r
-                       setFixedWidth(width);\r
-       }\r
-\r
-       final public void synchronizeBorderWidth(float width) {\r
-               if (width >= 0.0f)\r
-                       setBorderWidth(width);\r
-       }\r
-\r
-    public final void synchronizeWrapText(boolean wrap) {\r
-        setState(STATE_WRAP_TEXT, wrap);\r
-    }\r
-\r
-    public boolean isHovering() {\r
-        return hasState(STATE_HOVER);\r
-    }\r
-\r
-    @SyncField({"hover"})\r
-    public void setHover(boolean hover) {\r
-        setState(STATE_HOVER, hover);\r
-        repaint();\r
-    }\r
-\r
-    public Font getFont() {\r
-        return font;\r
-    }\r
-\r
-    @SyncField({"font"})\r
-    public void setFont(Font font) {\r
-        this.font = font;\r
-        resetCaches();\r
-    }\r
-\r
-    public double getBorderWidth() {\r
-        return borderWidth;\r
-    }\r
-\r
-    @SyncField({"borderWidth"})\r
-    public void setBorderWidth(float width) {\r
-        this.borderWidth = width;\r
-    }\r
-\r
-    public void setBorderWidth(double width) {\r
-        setBorderWidth((float)width);\r
-    }\r
-\r
-    @SyncField({"horizontalAlignment"})\r
-    public void setHorizontalAlignment(byte horizontalAlignment) {\r
-        if (horizontalAlignment < 0 && horizontalAlignment > 2)\r
-            throw new IllegalArgumentException("Invalid horizontal alignment: " + horizontalAlignment + ", must be between 0 and 2");\r
-        this.horizontalAlignment = horizontalAlignment;\r
-        resetCaches();\r
-    }\r
-\r
-    final public void synchronizeHorizontalAlignment(byte horizontalAlignment) {\r
-        if (horizontalAlignment >= 0 && horizontalAlignment <= 2)\r
-            setHorizontalAlignment(horizontalAlignment);\r
-    }\r
-\r
-    public byte getHorizontalAlignment() {\r
-        return horizontalAlignment;\r
-    }\r
-\r
-    @SyncField({"verticalAlignment"})\r
-    public void setVerticalAlignment(byte verticalAlignment) {\r
-        if (verticalAlignment < 0 && verticalAlignment > 3)\r
-            throw new IllegalArgumentException("Invalid vertical alignment: " + verticalAlignment + ", must be between 0 and 3");\r
-        this.verticalAlignment = verticalAlignment;\r
-        resetCaches();\r
-    }\r
-\r
-    final public void synchronizeVerticalAlignment(byte verticalAlignment) {\r
-        if (verticalAlignment >= 0 && verticalAlignment <= 3)\r
-            setVerticalAlignment(verticalAlignment);\r
-    }\r
-\r
-    public byte getVerticalAlignment() {\r
-        return verticalAlignment;\r
-    }\r
-\r
-    /**\r
-     * Rendering is single-threaded so we can use a static rectangle for\r
-     * calculating the expanded bounds for the node.\r
-     */\r
-    private static transient ThreadLocal<Rectangle2D> tempBounds = new ThreadLocal<Rectangle2D>() {\r
-        @Override\r
-        protected Rectangle2D initialValue() {\r
-            return new Rectangle2D.Double();\r
-        }\r
-    };\r
-\r
-    /**\r
-     * Rendering is single-threaded so we can use a static AffineTransform to\r
-     * prevent continuous memory allocation during text rendering.\r
-     */\r
-    private static transient ThreadLocal<AffineTransform> tempAffineTransform = new ThreadLocal<AffineTransform>() {\r
-        @Override\r
-        protected AffineTransform initialValue() {\r
-            return new AffineTransform();\r
-        }\r
-    };\r
-\r
-    @Override\r
-    public void render(Graphics2D g) {\r
-        AffineTransform ot = g.getTransform();\r
-        render(g, true);\r
-        g.setTransform(ot);\r
-    }\r
-\r
-    /**\r
-     * Note: does not return transformation, stroke, color, etc. to their\r
-     * original states\r
-     * \r
-     * @param g\r
-     * @param applyTransform\r
-     */\r
-    public void render(Graphics2D g, boolean applyTransform) {\r
-        if (text == null || font == null || color == null)\r
-            return;\r
-\r
-        // Cache font metrics if necessary\r
-        if (fontMetrics == null)\r
-            fontMetrics = g.getFontMetrics(font);\r
-\r
-        Color color = this.color;\r
-        boolean isSelected = NodeUtil.isSelected(this, 1);\r
-        boolean hover = hasState(STATE_HOVER);\r
-        boolean editing = hasState(STATE_EDITING);\r
-\r
-        if (!isSelected && hover) {\r
-            color = add(color, 120, 120, 120);\r
-        }\r
-\r
-        if (applyTransform)\r
-            g.transform(transform);\r
-        // Apply separate legacy scale\r
-        if (scale != 1.0)\r
-            g.scale(scale, scale);\r
-\r
-        // Safety for not rendering when the scale of this text is too small.\r
-        // When the scale is too small it will cause internal exceptions while\r
-        // stroking fonts.\r
-        double currentScale = GeometryUtils.getScale(g.getTransform());\r
-        //System.out.println("currentScale: " + currentScale);\r
-        if (currentScale < 1e-6)\r
-            return;\r
-\r
-        g.setFont(font);\r
-        //g.translate(x, y);\r
-\r
-        // Calculate text clip rectangle.\r
-        // This updates textLayout if necessary.\r
-        Rectangle2D r = getTightAlignedBoundsInLocal(tempBounds.get(), fontMetrics.getFontRenderContext());\r
-\r
-        computeEditingXOffset();\r
-\r
-        if (fixedWidth > 0f)\r
-            r.setFrame(r.getMinX(), r.getMinY(), fixedWidth, r.getHeight());\r
-        if(targetBounds != null) {\r
-            double w = (targetBounds.getWidth() - paddingX * 2) * scaleRecip;\r
-            double h = (targetBounds.getHeight() - paddingY * 2) * scaleRecip;\r
-            double x = (targetBounds.getMinX() + paddingX) * scaleRecip;\r
-            double y = (targetBounds.getMinY() + paddingY) * scaleRecip;\r
-            r.setRect(x, y, w, h);\r
-        }\r
-\r
-        Rectangle2D textClip = r.getBounds2D();\r
-\r
-        expandBoundsUnscaled(r);\r
-\r
-        // Speed rendering optimization: don't draw text that is too small to\r
-        // read when not editing\r
-        boolean renderText = true;\r
-        if (!editing) {\r
-            Object renderingHint = g.getRenderingHint(RenderingHints.KEY_RENDERING);\r
-            if (renderingHint != RenderingHints.VALUE_RENDER_QUALITY) {\r
-                float textSizeMM = (float) currentScale * GeometryUtils.pointToMillimeter(font.getSize2D());\r
-                if (textSizeMM < 1.5f)\r
-                    renderText = false;\r
-            }\r
-        }\r
-\r
-        Shape clipSave = g.getClip();\r
-        g.setClip(textClip);\r
-\r
-        // PDF \r
-        PdfWriter writer = (PdfWriter) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_WRITER);\r
-        boolean isPdfField = false;\r
-        String fieldName = null;\r
-        if (writer != null) {\r
-               // TODO: replace this hack with proper text field name field\r
-               fieldName = NodeUtil.getNodeName(this);\r
-               isPdfField = ( fieldName.equals("approved_by") ||\r
-                                               fieldName.equals("checked_by") ||\r
-                                               fieldName.equals("designer name") ||\r
-                                               fieldName.equals("created_by") );\r
-        }\r
-\r
-        Color backgroundColor = hasState(STATE_VALID) ? this.backgroundColor : Color.red;\r
-\r
-        // RENDER\r
-        if ( !isPdfField ) {\r
-\r
-            // Fill background if necessary\r
-            if (backgroundColor != null) {\r
-                g.setColor(backgroundColor);\r
-                g.fill(r);\r
-            }\r
-\r
-            if (editing) {\r
-\r
-                int selectionMin = Math.min(caret, selectionTail);\r
-                int selectionMax = Math.max(caret, selectionTail);\r
-\r
-                // Base text\r
-                g.setColor(color);\r
-                renderText(g, xOffset);\r
-\r
-                Shape clip = g.getClip();\r
-\r
-                // Selection background & text\r
-                for (Line line : lines) {\r
-                    if (line.intersectsRange(selectionMin, selectionMax)) {\r
-                        Shape selShape = line.getLogicalHighlightShape(selectionMin, selectionMax);\r
-                        line.translate(g, xOffset, 0);\r
-                        g.setClip(selShape);\r
-                        g.setColor(SELECTION_BACKGROUND_COLOR);\r
-                        g.fill(selShape);\r
-                        g.setColor(Color.WHITE);\r
-                        //line.layout.draw(g, 0, 0);\r
-                        g.drawString(line.getText(), 0, 0);\r
-                        line.translateInv(g, xOffset, 0);\r
-                    }\r
-                }\r
-\r
-                g.setClip(clip);\r
-\r
-                // Caret\r
-                \r
-                renderCaret(g);\r
-                \r
-\r
-            } else {\r
-\r
-                if (renderText) {\r
-                    g.setColor(color);\r
-                    renderText(g, 0);\r
-                }\r
-\r
-            }\r
-        } else {\r
-            // PDF \r
-            // TODO: multiline support\r
-//                             try {\r
-                           AffineTransform at = g.getTransform();\r
-                           float height = writer.getPageSize().getHeight();\r
-                           Rectangle2D rr = textClip;\r
-       //                  Point2D pt1 = new Point2D.Double(rr.getX(), rr.getY()+rr.getHeight());\r
-       //                  Point2D pt2 = new Point2D.Double(rr.getX()+rr.getWidth(), rr.getY());\r
-                           Point2D pt1 = new Point2D.Double(0, 0);\r
-                           Point2D pt2 = new Point2D.Double(47.f/*+rr.getWidth()*/, -rr.getHeight());\r
-                           pt1 = at.transform(pt1, pt1);\r
-                           pt2 = at.transform(pt2, pt2);\r
-                               Rectangle rectangle = new Rectangle(\r
-                                               (float) pt1.getX(), \r
-                                               height-(float) pt1.getY(), \r
-                                               (float) pt2.getX(), \r
-                                               height-(float) pt2.getY()); \r
-\r
-                           FontMapper mapper = (FontMapper) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_FONTMAPPER);\r
-//                             FontMetrics fm    = g.getFontMetrics(font);\r
-\r
-                               // TODO Oikea leveys\r
-                               // TODO Uniikki nimi\r
-                               /*\r
-                               PdfFormField field = PdfFormField.createTextField(writer, false, false, 20);\r
-                               field.setFieldName(this.getId().toString());\r
-                               field.setWidget(rectangle, PdfAnnotation.HIGHLIGHT_NONE);\r
-                               field.setQuadding(PdfFormField.Q_RIGHT);\r
-                               field.setFieldFlags(PdfFormField.FF_READ_ONLY);\r
-                               field.setRotate(90);\r
-                               writer.addAnnotation(field);\r
-                               */\r
-\r
-\r
-                               // Signature Field\r
-                               /*\r
-                               if (text==null) {\r
-                                       PdfFormField field = PdfFormField.createSignature(writer);\r
-                                       field.setWidget(rectangle, PdfAnnotation.HIGHLIGHT_NONE);\r
-                                       field.setFieldName(fieldName);\r
-                                       field.setQuadding(PdfFormField.Q_LEFT);\r
-                                       field.setFlags(PdfAnnotation.FLAGS_PRINT);\r
-                                       //field.setFieldFlags(PdfFormField.FF_READ_ONLY)\r
-                                       field.setFieldFlags(PdfFormField.FF_EDIT);\r
-                                       field.setPage();\r
-                                       field.setMKBackgroundColor( backgroundColor!=null?Color.WHITE:backgroundColor );\r
-                                       PdfAppearance tp = PdfAppearance.createAppearance(writer, 72, 48);\r
-                                       tp.rectangle(rectangle);\r
-                                       tp.stroke();                            \r
-                                       field.setAppearance(PdfAnnotation.APPEARANCE_NORMAL, tp);\r
-                                       writer.addAnnotation(field);\r
-                               } else */ \r
-                               {\r
-                                       // Text Field\r
-                                               try {\r
-                                               TextField textField = new TextField(writer, rectangle, fieldName);\r
-                                               textField.setFieldName(fieldName);\r
-                                               textField.setFont(mapper.awtToPdf(font));\r
-                                               textField.setBorderStyle(0);\r
-                                           //textField.setAlignment(Element.ALIGN_LEFT);\r
-                                           textField.setAlignment(Element.ALIGN_BOTTOM);\r
-                                           textField.setRotation(90);\r
-                                       textField.setOptions(TextField.EDIT|TextField.DO_NOT_SPELL_CHECK);\r
-                                           if ( text!=null ) {\r
-                                                       textField.setText(text);\r
-                                           }\r
-                                           if ( color!=null ) {\r
-                                               textField.setTextColor(color);\r
-                                           }\r
-                                               textField.setBackgroundColor( backgroundColor!=null?Color.WHITE:backgroundColor );\r
-                                           PdfFormField field = textField.getTextField();\r
-                                           writer.addAnnotation(field);\r
-                                               } catch (IOException e) {\r
-                                                       e.printStackTrace();\r
-                                               } catch (DocumentException e) {\r
-                                                       e.printStackTrace();\r
-                                               }\r
-                               }\r
-\r
-//                             } catch (IOException e) {\r
-//                                     // TODO Auto-generated catch block\r
-//                                     e.printStackTrace();\r
-//                             } catch (DocumentException e) {\r
-//                                     // TODO Auto-generated catch block\r
-//                                     e.printStackTrace();\r
-//                             }\r
-        }\r
-        /// PDF\r
-\r
-        g.setClip(clipSave);\r
-\r
-        if (borderWidth > 0f && borderColor != null) {\r
-            g.setColor(borderColor);\r
-            g.setStroke(new BasicStroke((float) (scale*borderWidth)));\r
-            g.draw(r);\r
-        }\r
-\r
-        //System.out.println("bw: " + borderWidth);\r
-        if (isSelected && showsSelection()) {\r
-            Composite oc = g.getComposite();\r
-            g.setComposite(SrcOver_50);\r
-            g.setColor(Color.RED);\r
-            float bw = borderWidth;\r
-            double s = currentScale;\r
-            if (bw <= 0f) {\r
-                bw = (float) (1f / s);\r
-            } else {\r
-                bw *= 5f * scale;\r
-            }\r
-            g.setStroke(new BasicStroke(bw));\r
-            g.draw(r);\r
-            //g.draw(GeometryUtils.expandRectangle(r, 1.0));\r
-\r
-            g.setComposite(oc);\r
-        }\r
-\r
-        g.scale(scaleRecip, scaleRecip);\r
-        g.setStroke(RESET_STROKE);\r
-\r
-        lastBounds = getScaledOffsetBounds(r, lastBounds, scale, 0, 0);\r
-//        g.setColor(Color.MAGENTA); // DEBUG\r
-//        g.draw(lastBounds); // DEBUG\r
-//        g.setColor(Color.ORANGE); // DEBUG\r
-//        g.draw(getBoundsInLocal()); // DEBUG\r
-\r
-        renderSelectedHover(g, isSelected, hover);\r
-    }\r
-\r
-    private void renderCaret(Graphics2D g) {\r
-       g.setColor(Color.BLACK);\r
-       for (int i = 0; i < lines.length; i++) {\r
-               Line line = lines[i];\r
-               // prevent rendering caret twice on line changes\r
-               if (line.containsOffset(caret) &&                // line contains caret\r
-                  (caret != line.endOffset ||                   //caret is not in the end of the line\r
-                   i == lines.length-1 ||                       //caret is end of the last line\r
-                   lines[i+1].startOffset != line.endOffset)) { // beginning of the next line does not start withe the same index as current line\r
-              Shape[] caretShape = line.getCaretShapes(caret);\r
-              line.translate(g, xOffset, 0);\r
-              g.draw(caretShape[0]);\r
-              if (caretShape[1] != null)\r
-                  g.draw(caretShape[1]);\r
-              line.translateInv(g, xOffset, 0);\r
-          }\r
-        }\r
-    }\r
-    private void renderText(Graphics2D g, float xOffset) {\r
-        //g.draw(tightBoundsCache); // DEBUG\r
-        for (Line line : lines) {\r
-            //line.layout.draw(g, line.alignedPosX + xOffset, line.alignedPosY);\r
-            g.drawString(line.getText(), line.alignedPosX + xOffset, line.alignedPosY);\r
-            //g.draw(line.abbox); // DEBUG\r
-        }\r
-    }\r
-\r
-    protected Rectangle2D getScaledOffsetBounds(Rectangle2D originalBounds, Rectangle2D dst, double scale, double offsetX, double offsetY) {\r
-        AffineTransform btr = tempAffineTransform.get();\r
-        btr.setToTranslation(offsetX*scale, offsetY*scale);\r
-        btr.scale(scale, scale);\r
-        if (btr.isIdentity()) {\r
-            dst.setFrame(originalBounds);\r
-        } else {\r
-            dst.setFrame(btr.createTransformedShape(originalBounds).getBounds2D());\r
-        }\r
-        return dst;\r
-    }\r
-\r
-    /**\r
-     * Invoked when TextNode is selected and a mouse is hovering on top of it. Can be overridden to add custom rendering.\r
-     * \r
-     * @param g\r
-     */\r
-    protected void renderSelectedHover(Graphics2D g, boolean isSelected, boolean isHovering) {\r
-    }\r
-\r
-    /**\r
-     * Replaces the current selection with the content or inserts\r
-     * the content at caret. After the insertion the caret\r
-     * will be at the end of inserted text and selection will\r
-     * be empty.\r
-     * @param content\r
-     */\r
-    @SyncField({"text","caret","selectionTail"})\r
-    protected void insert(String content) {\r
-        content = editContentFilter != null ? editContentFilter.filter(this, content) : content; \r
-\r
-        int selectionMin = Math.min(caret, selectionTail);\r
-        int selectionMax = Math.max(caret, selectionTail);\r
-\r
-        String begin = text.substring(0, selectionMin);\r
-        String end = text.substring(selectionMax);\r
-        text = begin + content + end;\r
-        caret = selectionMin + content.length();\r
-        selectionTail = caret;\r
-\r
-        assert (caret <= text.length());\r
-        //System.out.println(text + " " + caret );\r
-\r
-        if(validator != null) {\r
-            String error = validator.apply(text);\r
-            setState(STATE_VALID, (error == null));\r
-        }\r
-\r
-        resetCaches();\r
-    }\r
-\r
-    @ServerSide\r
-    protected void fireTextChanged() {\r
-        if(textListener != null)\r
-            textListener.textChanged();\r
-        repaint();\r
-    }\r
-\r
-    @ServerSide\r
-    protected void fireTextEditingStarted() {\r
-        if(textListener != null)\r
-            textListener.textEditingStarted();\r
-    }\r
-\r
-    @ServerSide\r
-    protected void fireTextEditingCancelled() {\r
-        setState(STATE_VALID);\r
-\r
-        if (deactivateEdit()) {\r
-            if (textListener != null)\r
-                textListener.textEditingCancelled();\r
-\r
-            setEditMode(false, false);\r
-\r
-            if (textBeforeEdit != null)\r
-                setText(textBeforeEdit);\r
-\r
-            repaint();\r
-        }\r
-    }\r
-\r
-    @ServerSide\r
-    public void fireTextEditingEnded() {\r
-        if (!hasState(STATE_VALID)) {\r
-            fireTextEditingCancelled();\r
-            setState(STATE_VALID);\r
-            return;\r
-        }\r
-\r
-        if (deactivateEdit()) {\r
-            if (textListener != null)\r
-                textListener.textEditingEnded();\r
-\r
-            setEditMode(false, false);\r
-            repaint();\r
-        }\r
-    }\r
-\r
-    public void setTextListener(ITextListener listener) {\r
-        this.textListener = listener;\r
-    }\r
-\r
-    public void setValidator(Function1<String, String> validator) {\r
-        this.validator = validator;\r
-    }\r
-\r
-    public void setContentFilter(ITextContentFilter filter) {\r
-        this.editContentFilter = filter;\r
-    }\r
-\r
-    public void setRVI(RVI rvi) {\r
-        this.dataRVI = rvi;\r
-    }\r
-\r
-    private void invalidateXOffset() {\r
-        setState(STATE_X_OFFSET_IS_DIRTY);\r
-    }\r
-\r
-    private void computeEditingXOffset() {\r
-\r
-        if(lines == null) return;\r
-        if(!hasState(STATE_X_OFFSET_IS_DIRTY)) return;\r
-        if(fixedWidth > 0f) {\r
-\r
-            // TODO: implement\r
-//            float[] coords = textLayout.getCaretInfo(TextHitInfo.afterOffset(caret));\r
-//            if(coords != null) {\r
-//                if(coords[0] > (fixedWidth*MAX_CARET_POSITION)) xOffset = (float)((fixedWidth*MAX_CARET_POSITION)-coords[0]);\r
-//                else xOffset = 0;\r
-//            }\r
-\r
-        } else {\r
-\r
-            xOffset = 0;\r
-\r
-        }\r
-\r
-        clearState(STATE_X_OFFSET_IS_DIRTY);\r
-\r
-    }\r
-\r
-    @SyncField({"caret","selectionTail"})\r
-    protected void moveCaret(int move, boolean select) {\r
-       // prevent setting caret into line separator. \r
-       if (move > 0) {\r
-               while (text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)\r
-                       move++;\r
-       } else if (move < 0) {\r
-               while (caret+move >= 0 && text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)\r
-                       move--;\r
-       }\r
-        caret += move;\r
-        if(caret < 0)\r
-            caret = 0;\r
-        if (caret > text.length())\r
-            caret = text.length();\r
-        if(!select)\r
-            selectionTail = caret;\r
-    }\r
-    \r
-    private Line findCaretLine() {\r
-        // Find the line where caret is. Starting from first line.\r
-        for(int i = 0; i < lines.length; i++) {\r
-            Line line = lines[i];\r
-            if(caret <= line.endOffset) {\r
-                return line;\r
-            }\r
-        }\r
-        return null;\r
-    }\r
-    \r
-    /**\r
-     * Moves caret to next not letter or digit\r
-     * @param shiftDown\r
-     */\r
-    private void moveCaretCtrlLeft(boolean shiftDown) {\r
-        Line line = findCaretLine();\r
-        if(line != null) {\r
-            int i;\r
-            for(i = caret-1; i > line.startOffset; i--) {\r
-                char c = line.document.charAt(i);\r
-                if(!Character.isLetterOrDigit(c)) {\r
-                    break;\r
-                }\r
-            }\r
-            moveCaret(i - caret, shiftDown);\r
-        }\r
-    }\r
-    \r
-    /**\r
-     * Moves caret to previous non letter or digit\r
-     * @param shiftDown\r
-     */\r
-    private void moveCaretCtrlRight(boolean shiftDown) {\r
-        Line line = findCaretLine();\r
-        if(line != null) {\r
-            int i;\r
-            for(i = caret + 1; i < line.endOffset; i++) {\r
-                char c = line.document.charAt(i);\r
-                if(!Character.isLetterOrDigit(c)) {\r
-                    break;\r
-                }\r
-            }\r
-            moveCaret(i - caret, shiftDown);\r
-        }\r
-    }\r
-    \r
-    /**\r
-     * Moves caret to line end\r
-     * @param shiftDown\r
-     */\r
-    private void moveCaretEnd(boolean shiftDown) {\r
-        Line line = findCaretLine();\r
-        if(line != null)\r
-            // Move caret to the end of the line\r
-            moveCaret(line.endOffset - caret, shiftDown);\r
-    }\r
-    \r
-    /**\r
-     * Moves caret to beginning of a line\r
-     * @param shiftDown\r
-     */\r
-    private void moveCaretHome(boolean shiftDown) {\r
-        Line line = findCaretLine();\r
-        if(line != null)\r
-            // Move caret to the beginning of the line\r
-            moveCaret(line.startOffset - caret, shiftDown);\r
-    }\r
-    \r
-    /**\r
-     * Moves caret one row up and tries to maintain the location\r
-     * @param shiftDown\r
-     */\r
-    private void moveCaretRowUp(boolean shiftDown) {\r
-        // Find the line where caret is. Starting from first line.\r
-        for(int i = 0; i < lines.length; i++) {\r
-            Line line = lines[i];\r
-            if(caret <= line.endOffset) {\r
-                // caret is in this line\r
-                if(i == 0) {\r
-                    // Already on top line\r
-                    // Select the beginning of the line\r
-                    moveCaret(-caret, shiftDown);\r
-                } else {\r
-                    Line prevLine = lines[i-1];\r
-                    int prevLength = prevLine.endOffset - prevLine.startOffset;\r
-                    int posInCurRow = caret - line.startOffset;\r
-                    if(prevLength < posInCurRow)\r
-                        posInCurRow = prevLength;\r
-\r
-                    int newPos = prevLine.startOffset + posInCurRow;\r
-                    moveCaret(newPos - caret, shiftDown);\r
-                }\r
-                break;\r
-            }\r
-        }        \r
-    }\r
-    \r
-    /**\r
-     * Moves caret one row down and tries to maintain the location\r
-     * @param shiftDown\r
-     */\r
-    private void moveCaretRowDown(boolean shiftDown) {\r
-        // Find the line where caret is. Starting from last line.\r
-        for(int i = lines.length - 1; i >= 0; i--) {\r
-            Line line = lines[i];\r
-            if(caret >= line.startOffset) {\r
-                // caret is in this line\r
-                if(i == lines.length - 1) {\r
-                    // Already on bottom line, cannot go below\r
-                    // Select to the end of the line\r
-                    moveCaret(line.endOffset - caret, shiftDown);\r
-                } else {\r
-                    Line prevLine = lines[i+1]; // Previous line\r
-                    \r
-                    // Find new caret position. \r
-                    // Either it is in the same index as before, or if the row\r
-                    // is not long enough, select the end of the row.\r
-                    int prevLength = prevLine.endOffset - prevLine.startOffset;\r
-                    int posInCurRow = caret - line.startOffset;\r
-                    if(prevLength < posInCurRow)\r
-                        posInCurRow = prevLength;\r
-                    int newPos = prevLine.startOffset + posInCurRow;\r
-                    moveCaret(newPos - caret, shiftDown);\r
-                }\r
-                break;\r
-            }\r
-        }        \r
-    }\r
-\r
-    @SyncField({"caret","selectionTail"})\r
-    protected void setCaret(int pos, boolean select) {\r
-        caret = pos;\r
-        if (caret < 0)\r
-            caret = 0;\r
-        if (caret > text.length())\r
-            caret = text.length();\r
-        if (!select)\r
-            selectionTail = caret;\r
-    }\r
-\r
-    protected void setCaret(Point2D point) {\r
-       setCaret(point, false);\r
-    }\r
-\r
-    @SyncField({"caret","selectionTail"})\r
-    protected void setCaret(Point2D point, boolean select) {\r
-        double lineY = 0;\r
-        for(int i = 0; i < lines.length; i++) {\r
-            Line line = lines[i];\r
-            Rectangle2D bounds = line.abbox;\r
-            // Add heights of bboxes for determining the correct line\r
-            if(i == 0)\r
-                lineY = bounds.getY();\r
-            else\r
-                lineY += lines[i-1].abbox.getHeight();\r
-            \r
-            double lineHeight = bounds.getHeight();\r
-            double hitY = point.getY() / scale;\r
-            if(hitY >= lineY && hitY <= lineY + lineHeight) {\r
-                // Hit is in this line\r
-                float x = (float)(point.getX() / scale) - (float)line.abbox.getX();\r
-                float y = (float)(point.getY() / scale -  lineHeight * i) ;\r
-                TextHitInfo info = line.layout.hitTestChar(x, y);\r
-                caret = line.startOffset + info.getInsertionIndex();\r
-                if (caret > line.endOffset)\r
-                       caret = line.endOffset;\r
-                if (!select)\r
-                    selectionTail = caret;\r
-                repaint();\r
-                break;\r
-            }\r
-        }\r
-        invalidateXOffset();\r
-        assert (caret <= text.length());\r
-    }\r
-    \r
-    @Override\r
-    public Rectangle2D getBoundsInLocal() {\r
-        if(targetBounds != null)\r
-            return targetBounds;\r
-        else\r
-            return expandBounds( getTightAlignedBoundsInLocal(null) );\r
-    }\r
-\r
-    protected Rectangle2D expandBounds(Rectangle2D r) {\r
-        r.setRect(r.getX() * scale - paddingX, r.getY() * scale -paddingY, r.getWidth()*scale + paddingX + paddingX, r.getHeight()*scale + paddingY + paddingY);\r
-        //System.out.println("  => " + r);\r
-        return r;\r
-    }\r
-\r
-    protected Rectangle2D expandBoundsUnscaled(Rectangle2D r) {\r
-        r.setRect(r.getX() - scaleRecip*paddingX, r.getY() -scaleRecip*paddingY, r.getWidth() + scaleRecip*paddingX + scaleRecip*paddingX, r.getHeight() + scaleRecip*paddingY + scaleRecip*paddingY);\r
-        //System.out.println("  => " + r);\r
-        return r;\r
-    }\r
-\r
-    protected Rectangle2D expandBounds(Rectangle2D r, double amount) {\r
-        r.setRect(r.getX() - amount, r.getY() - amount, r.getWidth() + 2*amount, r.getHeight() + 2*amount);\r
-        return r;\r
-    }\r
-\r
-    protected Rectangle2D expandBounds(Rectangle2D r, double left, double top, double right, double bottom) {\r
-        r.setRect(r.getX() - left, r.getY() - top, r.getWidth() + left + right, r.getHeight() + top + bottom);\r
-        return r;\r
-    }\r
-\r
-    private void resetCaches() {\r
-        this.tightBoundsCache = null;\r
-        this.lines = null;\r
-        this.fontMetrics = null;\r
-    }\r
-\r
-    /**\r
-     * Returns the tight bounds around the current text using the current font\r
-     * in the specified rectangle. If the specified rectangle is\r
-     * <code>null</code> a new Rectangle2D.Double instance will be created.\r
-     * \r
-     * @param r\r
-     * @return\r
-     */\r
-    protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r) {\r
-        return getTightAlignedBoundsInLocal(r, FRC);\r
-    }\r
-\r
-    /**\r
-     * Returns the tight bounds around the current text using the current font\r
-     * in the specified rectangle. If the specified rectangle is\r
-     * <code>null</code> a new Rectangle2D.Double instance will be created.\r
-     * \r
-     * @param r\r
-     *            the rectangle where the result of the method is placed or\r
-     *            <code>null</code> to allocate new rectangle\r
-     * @param frc current font render context\r
-     * @return r or new Rectangle2D.Double instance containing the requested\r
-     *         text bounds\r
-     */\r
-    protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r, FontRenderContext frc) {\r
-        if (r == null)\r
-            r = new Rectangle2D.Double();\r
-\r
-        if (tightBoundsCache != null) {\r
-            r.setFrame(tightBoundsCache);\r
-            return r;\r
-        }\r
-\r
-        String txt = text;\r
-        if (font == null || txt == null) {\r
-            r.setFrame(0, 0, 2, 1);\r
-            return r;\r
-        }\r
-\r
-        //System.out.println("TextNode.getTightAlignedBoundsInLocal('" + txt + "')");\r
-\r
-        // Parse & layout (unaligned)\r
-        Line[] lines = null;\r
-        \r
-        if(hasState(STATE_WRAP_TEXT)) {\r
-            float width = fixedWidth;\r
-            if(width <= 0 && targetBounds != null)\r
-                width = (float) (((targetBounds.getWidth() - 2*paddingX)) * scaleRecip);\r
-            if(width > 0)\r
-                lines = wrapLines(txt, font, width, frc);\r
-        }\r
-         \r
-        if(lines == null)\r
-            lines = parseLines(txt);\r
-        this.lines = layoutLines(lines, frc);\r
-\r
-        // Calculate tight bounds based on unaligned layout\r
-        //System.out.println("Unaligned");\r
-        tightBoundsCache = calculateBounds(lines, Line.BBOX, null);\r
-        //System.out.println("  => " + tightBoundsCache);\r
-\r
-        this.lines = layoutLinesX(lines, tightBoundsCache);\r
-        // Align each line to the calculated tight bounds\r
-        this.lines = alignLines(this.lines, tightBoundsCache, horizontalAlignment, verticalAlignment);\r
-\r
-        // Calculate aligned bounds\r
-        //System.out.println("Aligned");\r
-        calculateBounds(lines, Line.ABBOX, tightBoundsCache);\r
-\r
-        r.setFrame(tightBoundsCache);\r
-        //System.out.println("  => " + tightBoundsCache);\r
-\r
-        return r;\r
-    }\r
-\r
-    /**\r
-     * @param lines\r
-     * @param bbox\r
-     *            the bounding box of all the whole laid out text (only bbox\r
-     *            size is used)\r
-     * @return\r
-     */\r
-    private Line[] layoutLinesX(Line[] lines, Rectangle2D bbox) {\r
-        int lineCount = lines.length;\r
-        for (int l = 0; l < lineCount; ++l) {\r
-            Line line = lines[l];\r
-            // Compute pen x position. If the paragraph is right-to-left we\r
-            // will align the TextLayouts to the right edge of the panel.\r
-            // Note: drawPosX is always where the LEFT of the text is placed.\r
-            // NOTE: This changes based on horizontal alignment\r
-            line.drawPosX = (float) (line.layout.isLeftToRight() ? 0f\r
-                    : tightBoundsCache.getWidth() - line.layout.getAdvance());\r
-        }\r
-        return lines;\r
-    }\r
-\r
-    /**\r
-     * @param lines\r
-     * @param boundsProvider\r
-     * @param result\r
-     * @return\r
-     */\r
-    private static Rectangle2D calculateBounds(Line[] lines, BoundsProcedure boundsProvider, Rectangle2D result) {\r
-        if (result == null)\r
-            result = new Rectangle2D.Double();\r
-        else\r
-            result.setFrame(0, 0, 0, 0);\r
-\r
-        for (Line line : lines) {\r
-            //System.out.println("line: " + line);\r
-            Rectangle2D bbox = boundsProvider.getBounds(line);\r
-            if (result.isEmpty())\r
-                result.setFrame(bbox);\r
-            else\r
-                Rectangle2D.union(result, bbox, result);\r
-            //System.out.println("bounds: " + result);\r
-        }\r
-        //System.out.println("final bounds: " + result);\r
-\r
-        return result;\r
-    }\r
-\r
-    /**\r
-     * @param lines\r
-     * @param bbox\r
-     * @param hAlign\r
-     * @param vAlign\r
-     * @return aligned lines\r
-     */\r
-    private Line[] alignLines(Line[] lines, Rectangle2D bbox, byte hAlign, byte vAlign) {\r
-//        System.out.println("horizontal align: " + Alignment.values()[hAlign]);\r
-//        System.out.println("vertical align  : " + Alignment.values()[vAlign]);\r
-//        System.out.println("bbox: " + bbox);\r
-        double xbase = 0;\r
-//        double ybase = 0;\r
-        if(targetBounds != null) {\r
-            /* In normal cases the bounding box moves when\r
-             * typing. If target bounds are set, the text\r
-             * is fitted into the box.\r
-             */\r
-            switch (hAlign) {\r
-            case 1: // Trailing\r
-                xbase = (targetBounds.getMaxX()-paddingX) * scaleRecip;\r
-                break;\r
-            case 2: // Center\r
-                xbase = targetBounds.getCenterX() * scaleRecip;\r
-                break;\r
-            default: // Leading / Baseline\r
-                // Do nothing\r
-                break;\r
-            }\r
-        }\r
-        \r
-        \r
-        for (Line line : lines) {\r
-            double xoffset = 0;\r
-            double yoffset = 0;\r
-\r
-            switch (hAlign) {\r
-            case 1: // Trailing\r
-                xoffset = xbase - line.bbox.getWidth();\r
-                break;\r
-            case 2: // Center\r
-                xoffset = xbase - line.bbox.getWidth() / 2;\r
-                break;\r
-            default: // Leading / Baseline\r
-                // Do nothing\r
-                break;\r
-            }\r
-\r
-            switch (vAlign) {\r
-            case 0:\r
-                yoffset = line.layout.getAscent();\r
-                break;\r
-            case 1:\r
-                yoffset = -bbox.getHeight() + line.layout.getAscent();\r
-                break;\r
-            case 2:\r
-                yoffset = -bbox.getHeight() / 2 + line.layout.getAscent();\r
-                break;\r
-            }\r
-\r
-            line.alignOffset(xoffset, yoffset);\r
-        }\r
-        return lines;\r
-    }\r
-\r
-    /**\r
-     * @param lines\r
-     * @param frc\r
-     * @return\r
-     */\r
-    private Line[] layoutLines(Line[] lines, FontRenderContext frc) {\r
-        TextLayout emptyRowLayout = null;\r
-        int lineCount = lines.length;\r
-        float y = 0;\r
-        for (int l = 0; l < lineCount; ++l) {\r
-            Line line = lines[l];\r
-            String lineText = line.getText();\r
-            // " " because TextLayout requires non-empty text and\r
-            // We don't want zero size for the text.\r
-            if (lineText.isEmpty()) {\r
-                lineText = " ";\r
-                if (emptyRowLayout == null)\r
-                    emptyRowLayout = new TextLayout(lineText, font, frc);\r
-                line.layout = emptyRowLayout;\r
-            } else {\r
-                line.layout = new TextLayout(lineText, font, frc);\r
-            }\r
-\r
-            //y += line.layout.getAscent();\r
-            line.drawPosY = y;\r
-            y += line.layout.getDescent() + line.layout.getLeading() + line.layout.getAscent();\r
-\r
-            Rectangle2D bbox = line.layout.getLogicalHighlightShape(0, lineText.length()).getBounds2D();\r
-            bbox.setFrame(bbox.getX(), bbox.getY() + line.drawPosY, bbox.getWidth(), bbox.getHeight());\r
-            line.bbox = bbox;\r
-        }\r
-\r
-        return lines;\r
-    }\r
-\r
-    /**\r
-     * Splits the specified string into {@link Line} structures, one for each\r
-     * line in the input text. The returned lines are only partially defined,\r
-     * waiting to be laid out (see\r
-     * {@link #layoutLines(Line[], FontRenderContext)})\r
-     * \r
-     * @param txt\r
-     *            input text\r
-     * @return parsed text lines as {@link Line} structures\r
-     * @see #layoutLines(Line[], FontRenderContext)\r
-     */\r
-    private static Line[] parseLines(String txt) {\r
-        int len = txt.length();\r
-        if (len == 0)\r
-            return new Line[] { new Line("", 0, 0) };\r
-\r
-        TIntArrayList lfpos = new TIntArrayList();\r
-        int pos = 0;\r
-        int lineCount = 1;\r
-        for (;pos < len; ++lineCount) {\r
-            int nextlf = txt.indexOf('\n', pos);\r
-            lfpos.add(nextlf != -1 ? nextlf : len);\r
-            if (nextlf == -1)\r
-                break;\r
-            pos = nextlf + 1;\r
-        }\r
-        Line[] lines = new Line[lineCount];\r
-        pos = 0;\r
-        for (int i = 0; i < lineCount-1; ++i) {\r
-            int lf = lfpos.getQuick(i);\r
-            int cr = (lf > 0) && (txt.charAt(lf - 1) == '\r') ? lf - 1 : lf;\r
-            lines[i] = new Line(txt, pos, cr);\r
-            pos = lf + 1;\r
-        }\r
-        lines[lineCount - 1] = new Line(txt, pos, len);\r
-\r
-        return lines;\r
-    }\r
-    \r
-    \r
-    private static Line[] wrapLines(String txt, Font font, float fixedWidth, FontRenderContext frc) {\r
-        if(txt == null || txt.isEmpty())\r
-            txt = " ";\r
-        \r
-        ArrayList<Line> lines = \r
-                new ArrayList<Line>();\r
-        \r
-        Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();\r
-        map.put(TextAttribute.FONT, font);\r
-        AttributedString attributedText = new AttributedString(txt.isEmpty() ? "__ERROR__" : txt, map);\r
-\r
-        AttributedCharacterIterator paragraph = attributedText.getIterator();\r
-        int paragraphStart = paragraph.getBeginIndex();\r
-        int paragraphEnd = paragraph.getEndIndex();\r
-        LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);\r
-\r
-        float breakWidth = fixedWidth;\r
-\r
-        // Force text to be vertical, by setting break width to 1, if the text area is narrower than "GGGG"\r
-\r
-        // Set position to the index of the first character in the paragraph.\r
-        lineMeasurer.setPosition(paragraphStart);\r
-\r
-        // Get lines until the entire paragraph has been displayed.\r
-        int next, limit, charat, position = 0;\r
-        \r
-        while ((position = lineMeasurer.getPosition()) < paragraphEnd) {\r
-\r
-            // Find possible line break and set it as a limit to the next layout\r
-            next = lineMeasurer.nextOffset(breakWidth);\r
-            limit = next;\r
-            charat = txt.indexOf(System.getProperty("line.separator"),position+1);\r
-            if(charat < next && charat != -1){\r
-                limit = charat;\r
-            }\r
-            \r
-            lineMeasurer.nextLayout(breakWidth, limit, false);\r
-            // Add Line\r
-            lines.add(new Line(txt, position, limit));\r
-        }\r
-\r
-        return lines.toArray(new Line[lines.size()]);\r
-    }\r
-    \r
-\r
-    public String getClipboardContent() {\r
-        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();\r
-        Transferable clipData = clipboard.getContents(this);\r
-        try {\r
-            return (String) (clipData.getTransferData(DataFlavor.stringFlavor));\r
-        } catch (Exception ee) {\r
-            return null;\r
-        }\r
-    }\r
-\r
-    public void setClipboardContent(String content) {\r
-        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();\r
-        StringSelection data = new StringSelection(content);\r
-        clipboard.setContents(data, data);\r
-    }\r
-\r
-    @Override\r
-    public String toString() {\r
-        return super.toString() + " [text=" + text + ", font=" + font + ", color=" + color + "]";\r
-    }\r
-\r
-    @Override\r
-    protected boolean handleCommand(CommandEvent e) {\r
-        if (!hasState(STATE_EDITING))\r
-            return false;\r
-\r
-        if (Commands.SELECT_ALL.equals(e.command)) {\r
-            selectAll();\r
-            return true;\r
-        }\r
-        return false;\r
-    }\r
-\r
-    @Override\r
-    protected boolean keyPressed(KeyPressedEvent event) {\r
-        if (!hasState(STATE_EDITING))\r
-            return false;\r
-\r
-        char c = event.character;\r
-        boolean ctrl = event.isControlDown();\r
-        boolean alt = event.isAltDown();\r
-\r
-//        System.out.println("Key pressed '" + (event.character == 0 ? "\\0" : "" + c) + "' " + event.keyCode + " - " + Integer.toBinaryString(event.stateMask));\r
-//        System.out.println("ctrl: " + ctrl);\r
-//        System.out.println("alt: " + alt);\r
-        if (ctrl && !alt) {\r
-            switch (event.keyCode) {\r
-                case KeyEvent.VK_C:\r
-                    if (caret != selectionTail) {\r
-                        int selectionMin = Math.min(caret, selectionTail);\r
-                        int selectionMax = Math.max(caret, selectionTail);\r
-                        setClipboardContent(text.substring(selectionMin, selectionMax));\r
-                    }\r
-                    break;\r
-                    \r
-                case KeyEvent.VK_X:\r
-                    if (caret != selectionTail) {\r
-                        int selectionMin = Math.min(caret, selectionTail);\r
-                        int selectionMax = Math.max(caret, selectionTail);\r
-                        setClipboardContent(text.substring(selectionMin, selectionMax));\r
-                        insert("");\r
-                    }\r
-                    break;\r
-\r
-                case KeyEvent.VK_RIGHT:\r
-                    if (c == '\0')  {\r
-                        // '\'' has the same keycode as VK_RIGHT but when right\r
-                        // arrow is pressed, event character is \0.\r
-                        moveCaretCtrlRight(event.isShiftDown());\r
-                    }\r
-                    break;\r
-                    \r
-                case KeyEvent.VK_LEFT:\r
-                        moveCaretCtrlLeft(event.isShiftDown());\r
-                        break;\r
-                    \r
-                case KeyEvent.VK_V:\r
-                {\r
-                    String content = getClipboardContent();\r
-                    if(content != null)\r
-                        insert(content);\r
-                    break;\r
-                }\r
-\r
-                // Replaced by #handleCommand\r
-//                case KeyEvent.VK_A:\r
-//                {\r
-//                    selectAll();\r
-//                    return true;\r
-//                }\r
-                \r
-                case KeyEvent.VK_ENTER:\r
-                {\r
-                    insert(getLineSeparator());\r
-                }\r
-                \r
-                break;\r
-\r
-                default:\r
-                    return false;\r
-            }\r
-        } else if (!ctrl && alt) {\r
-            return false;\r
-        } else {\r
-            switch (event.keyCode) {\r
-                case KeyEvent.VK_LEFT:\r
-                       moveCaret(-1, event.isShiftDown());\r
-                    break;\r
-                case KeyEvent.VK_RIGHT:\r
-                    if (c == '\0')  {\r
-                        // '\'' has the same keycode as VK_RIGHT but when right\r
-                        // arrow is pressed, event character is \0.\r
-                        moveCaret(1, event.isShiftDown());\r
-                        break;\r
-                    }\r
-                    // Intentional fallthrough to default case\r
-                case KeyEvent.VK_UP:\r
-                    moveCaretRowUp(event.isShiftDown());\r
-                    break;\r
-                case KeyEvent.VK_DOWN:\r
-                    moveCaretRowDown(event.isShiftDown());\r
-                    break;\r
-                case KeyEvent.VK_HOME:\r
-                    moveCaretHome(event.isShiftDown());\r
-                    break;\r
-                case KeyEvent.VK_END:\r
-                    moveCaretEnd(event.isShiftDown());\r
-                    break;\r
-\r
-                case KeyEvent.VK_ENTER:\r
-                    fireTextEditingEnded();\r
-                    return true;\r
-\r
-                case KeyEvent.VK_ESCAPE:\r
-                    text = textBeforeEdit;\r
-                    resetCaches();\r
-                    clearState(STATE_EDITING);\r
-                    fireTextEditingCancelled();\r
-                    return true;\r
-\r
-                case KeyEvent.VK_BACK_SPACE:\r
-                    if(caret == selectionTail && caret > 0) {\r
-                       // line separator may use multiple characters, we want to remove that with one command\r
-                       String lineSep = getLineSeparator();\r
-                       int index = lineSep.indexOf(text.charAt(caret-1));\r
-                       if (index == -1)\r
-                               --caret;\r
-                       else {\r
-                               caret-= (index+1);\r
-                               selectionTail+= (lineSep.length()-index-1);\r
-                       }\r
-                    }\r
-                    insert("");\r
-                    break;\r
-\r
-                case KeyEvent.VK_DELETE:\r
-                    if(caret == selectionTail && caret < text.length()) {\r
-                       String lineSep = getLineSeparator();\r
-                       int index = lineSep.indexOf(text.charAt(caret));\r
-                       if (index==-1)\r
-                               ++caret;\r
-                       else {\r
-                               selectionTail-= index;\r
-                               caret+= (lineSep.length()-index);\r
-                       }\r
-                    }\r
-                    insert("");\r
-                    break;\r
-\r
-                \r
-\r
-                default:\r
-                    if (c == 65535 || Character.getType(c) == Character.CONTROL) {\r
-                        return false;\r
-                    }\r
-                    //System.out.println("Char " + c + " " + Character.getType(c) + " " + text);\r
-                    insert(new String(new char[] {c}));\r
-            }\r
-        }\r
-\r
-        // FIXME This is called even if just caret was moved.\r
-        // This is currently necessary for repaints.\r
-        fireTextChanged();\r
-        invalidateXOffset();\r
-        return true;\r
-    }\r
-    \r
-    protected String getLineSeparator() {\r
-       return System.getProperty("line.separator");\r
-    }\r
-\r
-    protected void selectAll() {\r
-        setCaret(0, false);\r
-        setCaret(text.length(), true);\r
-    }\r
-\r
-    protected transient int hoverClick = 0;\r
-\r
-    @Override\r
-    protected boolean mouseClicked(MouseClickEvent event) {\r
-        if (event.button != MouseClickEvent.LEFT_BUTTON)\r
-            return false;\r
-        \r
-        if (hasState(STATE_HOVER)) {\r
-               hoverClick++;\r
-               if (hoverClick < 2)\r
-                       return false;\r
-            ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);\r
-            // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.\r
-            if (ctx == null)\r
-                return false;\r
-            IElement e = DiagramNodeUtil.getElement(ctx, this);\r
-            if (!hasState(STATE_EDITING)) {\r
-               if (Boolean.TRUE.equals(setEditMode(true))) {\r
-                       editActivation = activateEdit(0, e, ctx);\r
-                       repaint();\r
-               }\r
-            } \r
-        } else {\r
-               hoverClick = 0;\r
-            if (hasState(STATE_EDITING)) {\r
-                fireTextEditingEnded();\r
-            }\r
-        }\r
-        return false;\r
-    }\r
-    \r
-    protected boolean mouseDoubleClicked(MouseDoubleClickedEvent event) {\r
-        if (event.button != MouseClickEvent.LEFT_BUTTON)\r
-            return false;\r
-        \r
-        if (hitTest(event, 0)) {\r
-            ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);\r
-            // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.\r
-            if (ctx == null)\r
-                return false;\r
-            \r
-            if (text != null) {\r
-                // Select the whole text.\r
-                setCaret(0, false);\r
-                setCaret(text.length(), true);\r
-                repaint();\r
-            }\r
-        }\r
-        return false;\r
-    }\r
-\r
-\r
-    @Override\r
-    protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {\r
-        if (!hasState(STATE_EDITING))\r
-            return false;\r
-        \r
-        Point2D local = controlToLocal( event.controlPosition );\r
-        // FIXME: once the event coordinate systems are cleared up, remove this workaround\r
-        local = parentToLocal(local);\r
-        if (hasState(STATE_HOVER) && this.containsLocal(local)) {\r
-            setCaret(local, event.isShiftDown());\r
-        }\r
-        return false;\r
-    }\r
-\r
-    @Override\r
-    protected boolean mouseMoved(MouseMovedEvent event) {\r
-        boolean hit = hitTest(event, 3.0);\r
-        if (hit != hasState(STATE_HOVER)) {\r
-            setState(STATE_HOVER, hit);\r
-            repaint();\r
-        }\r
-        return false;\r
-    }\r
-\r
-    private boolean isControlDown(MouseEvent e) {\r
-       return e.isControlDown() || lastMouseEvent!=null?lastMouseEvent.isControlDown():false;\r
-    }\r
-\r
-    protected boolean isShiftDown(MouseEvent e) {\r
-       return e.isShiftDown() || lastMouseEvent!=null?lastMouseEvent.isShiftDown():false;\r
-    }\r
-\r
-//    private boolean isAltDown(MouseEvent e) {\r
-//     return e.isAltDown() || lastMouseEvent!=null?lastMouseEvent.isAltDown():false;\r
-//    }\r
-\r
-    @Override\r
-    protected boolean mouseDragged(MouseDragBegin e) {\r
-        if (isHovering()\r
-                && (isControlDown(e) || isShiftDown(e))\r
-                && e.context instanceof NodeEventHandler\r
-                && dataRVI != null)\r
-        {\r
-            e.transferable = new LocalObjectTransferable(dataRVI);\r
-        }\r
-        return false;\r
-    }\r
-\r
-    protected boolean hitTest(MouseEvent event, double tolerance) {\r
-        Rectangle2D bounds = getBoundsInternal();\r
-        if (bounds == null)\r
-            return false;\r
-        Point2D localPos = NodeUtil.worldToLocal(this, event.controlPosition, new Point2D.Double());\r
-        double x = localPos.getX();\r
-        double y = localPos.getY();\r
-        boolean hit = bounds.contains(x, y);\r
-        return hit;\r
-    }\r
-\r
-    public Rectangle2D getBoundsInternal() {\r
-        Rectangle2D local = lastBounds;\r
-        if (local == null)\r
-            return null;\r
-        // TODO: potential spot for CPU/memory allocation optimization\r
-        // by using more specialized implementations\r
-        if (transform.isIdentity())\r
-            return local;\r
-        return transform.createTransformedShape(local).getBounds2D();\r
-    }\r
-\r
-    protected Color add(Color c, int r, int g, int b)  {\r
-        int nr = Math.min(255, c.getRed() + r);\r
-        int ng = Math.min(255, c.getGreen() + g);\r
-        int nb = Math.min(255, c.getBlue() + b);\r
-        return new Color(nr, ng, nb);\r
-    }\r
-\r
-    public TextEditActivation activateEdit(int mouseId, IElement e, ICanvasContext ctx) {\r
-        EditDataNode data = EditDataNode.getNode(this);\r
-        deactivateEdit(data, null);\r
-        TextEditActivation result = new TextEditActivation(mouseId, e, ctx);\r
-        data.setTextEditActivation(result);\r
-        return result;\r
-    }\r
-\r
-    /**\r
-     * @return <code>true</code> if this node is or was previously in editing\r
-     *         state\r
-     */\r
-    protected boolean deactivateEdit() {\r
-        boolean result = deactivateEdit( editActivation );\r
-        result |= editActivation != null;\r
-        editActivation = null;\r
-        return result;\r
-    }\r
-\r
-    protected boolean deactivateEdit(TextEditActivation activation) {\r
-        return deactivateEdit( EditDataNode.getNode(this), activation );\r
-    }\r
-\r
-    protected boolean deactivateEdit(EditDataNode data, TextEditActivation activation) {\r
-        TextEditActivation previous = data.getTextEditActivation();\r
-        if (previous != null && (previous == activation || activation == null)) {\r
-            previous.release();\r
-            data.setTextEditActivation(null);\r
-            return true;\r
-        }\r
-        return false;\r
-    }\r
-\r
-    @Override\r
-    public int getEventMask() {\r
-        return EventTypes.KeyPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask\r
-                | EventTypes.MouseClickMask | EventTypes.CommandMask;\r
-    }\r
-\r
-    private MouseEvent lastMouseEvent = null;\r
-    \r
-    @Override\r
-    public boolean handleEvent(Event e) {\r
-       if(e instanceof MouseEvent && !(e instanceof MouseDragBegin)) lastMouseEvent = (MouseEvent)e;\r
-        return super.handleEvent(e);\r
-    }\r
-\r
-       @Override\r
-       public Function1<Object, Boolean> getPropertyFunction(String propertyName) {\r
-               return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);\r
-       }\r
-       \r
-       @Override\r
-       public <T> T getProperty(String propertyName) {\r
-               return null;\r
-       }\r
-       \r
-       @Override\r
-       public void setPropertyCallback(Function2<String, Object, Boolean> callback) {\r
-       }\r
-       \r
-       public void synchronizeText(String text) {\r
-               setText(text);\r
-       }\r
-\r
-       public void synchronizeColor(RGB.Integer color) {\r
-               this.color = Colors.awt(color);\r
-       }\r
-\r
-       public void synchronizeFont(org.simantics.datatypes.literal.Font font) {\r
-               setFont(Fonts.awt(font));\r
-       }\r
-\r
-       public void synchronizeTransform(double[] data) {\r
-               this.setTransform(new AffineTransform(data));\r
-       }\r
-\r
-       public static void main(String[] args) {\r
-               Line[] lines = parseLines("\n  \n FOO  \n\nBAR\n\n\n BAZ\n\n");\r
-               System.out.println(Arrays.toString(lines));\r
-               System.out.println(GeometryUtils.pointToMillimeter(1));\r
-               System.out.println(GeometryUtils.pointToMillimeter(12));\r
-               System.out.println(GeometryUtils.pointToMillimeter(72));\r
-       }\r
-\r
-    ///////////////////////////////////////////////////////////////////////////\r
-    // LEGACY CODE NEEDED BY INHERITED CLASSES FOR NOW\r
-    ///////////////////////////////////////////////////////////////////////////\r
-\r
-    protected double getHorizontalAlignOffset(Rectangle2D r) {\r
-        switch (horizontalAlignment) {\r
-            case 0: return 0; // Leading\r
-            case 1: return -r.getWidth(); // Trailing\r
-            case 2: return -r.getCenterX(); // Center\r
-            default: return 0;\r
-        }\r
-    }\r
-\r
-    protected double getVerticalAlignOffset() {\r
-        FontMetrics fm = fontMetrics;\r
-        if (fm == null)\r
-            return 0;\r
-        switch (verticalAlignment) {\r
-            case 0: return fm.getMaxAscent(); // Leading=top=maxascent\r
-            case 1: return -fm.getMaxDescent(); // Trailing=bottom=maxdescent\r
-            case 2: return fm.getMaxAscent() / 2; // Center=maxascent / 2\r
-            case 3: return 0;\r
-            default: return 0;\r
-        }\r
-    }\r
-\r
-    ///////////////////////////////////////////////////////////////////////////\r
-    // LEGACY CODE ENDS\r
-    ///////////////////////////////////////////////////////////////////////////\r
-\r
-}\r
+/*******************************************************************************
+ * Copyright (c) 2007, 2010 Association for Decentralized Information Management
+ * in Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     VTT Technical Research Centre of Finland - initial API and implementation
+ *******************************************************************************/
+package org.simantics.diagram.elements;
+
+import java.awt.AlphaComposite;
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Composite;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.Shape;
+import java.awt.Toolkit;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.StringSelection;
+import java.awt.datatransfer.Transferable;
+import java.awt.event.KeyEvent;
+import java.awt.font.FontRenderContext;
+import java.awt.font.LineBreakMeasurer;
+import java.awt.font.TextAttribute;
+import java.awt.font.TextHitInfo;
+import java.awt.font.TextLayout;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.text.AttributedCharacterIterator;
+import java.text.AttributedString;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Hashtable;
+import java.util.List;
+
+import org.simantics.datatypes.literal.RGB;
+import org.simantics.db.layer0.variable.RVI;
+import org.simantics.diagram.elements.Line.BoundsProcedure;
+import org.simantics.g2d.canvas.ICanvasContext;
+import org.simantics.g2d.element.IElement;
+import org.simantics.scenegraph.IDynamicSelectionPainterNode;
+import org.simantics.scenegraph.LoaderNode;
+import org.simantics.scenegraph.ScenegraphUtils;
+import org.simantics.scenegraph.g2d.G2DNode;
+import org.simantics.scenegraph.g2d.G2DPDFRenderingHints;
+import org.simantics.scenegraph.g2d.G2DRenderingHints;
+import org.simantics.scenegraph.g2d.G2DRenderingHints.TextRenderingMode;
+import org.simantics.scenegraph.g2d.events.Event;
+import org.simantics.scenegraph.g2d.events.EventTypes;
+import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
+import org.simantics.scenegraph.g2d.events.command.CommandEvent;
+import org.simantics.scenegraph.g2d.events.command.Commands;
+import org.simantics.scenegraph.utils.GeometryUtils;
+import org.simantics.scenegraph.utils.NodeUtil;
+import org.simantics.scl.runtime.function.Function1;
+import org.simantics.scl.runtime.function.Function2;
+import org.simantics.ui.colors.Colors;
+import org.simantics.ui.dnd.LocalObjectTransferable;
+import org.simantics.ui.dnd.MultiTransferable;
+import org.simantics.ui.dnd.PlaintextTransfer;
+import org.simantics.ui.fonts.Fonts;
+import org.simantics.utils.threads.AWTThread;
+
+import com.lowagie.text.pdf.PdfWriter;
+
+import gnu.trove.list.array.TIntArrayList;
+
+
+/**
+ * TextNode which supports in-line editing.
+ * 
+ * By default <code>TextNode</code> is in editable = false state. Use
+ * {@link #setEditable(boolean)} to make it editable.
+ * 
+ * @author Hannu Niemist&ouml; <hannu.niemisto@vtt.fi>
+ * @author Marko Luukkainen <marko.luukkainen@vtt.fi>
+ * @author Tuukka Lehtonen <tuukka.lehtonen@semantum.fi>
+ * 
+ * TODO:
+ * o proper support for defining clipping bounds for the text (needed for page templates) (currently through fixedWidth)
+ * o fix editing xOffset to work with fixed width and multi-line text
+ * 
+ * @see Line
+ * @see TextLayout
+ */
+public class TextNode extends G2DNode implements IDynamicSelectionPainterNode, LoaderNode {
+
+    private static final long                serialVersionUID           = 654692698101485672L;
+
+    public static enum TextFlipping {
+        Disabled,
+        VerticalTextUpwards,
+        VerticalTextDownwards,
+    }
+
+    /**
+     * TODO: justify existence for this
+     */
+    private static final BasicStroke         RESET_STROKE               = new BasicStroke(1);
+
+    /**
+     * Src-over alpha composite instance with 50% opacity.
+     */
+    private static final AlphaComposite      SrcOver_50                 = AlphaComposite.SrcOver.derive(0.5f);
+
+    /**
+     * For (inexact) measurement of rendered text bounds.
+     */
+    protected static final FontRenderContext FRC = new FontRenderContext(new AffineTransform(), true, true);
+
+    private static final Font FONT = Font.decode("Arial 6");
+    private static final Color SELECTION_BACKGROUND_COLOR = new Color(0x316ac5);
+//    private static final double MAX_CARET_POSITION = 1.0;
+
+    /**
+     * The complete text visualized by this node.
+     */
+    protected String text = null;
+
+    /**
+     * The font used to render the {@link #text}.
+     */
+    protected Font font = FONT;
+
+    /**
+     * The color of the rendered text. Default value is {@value Color#black}.
+     */
+    protected Color color = Color.BLACK;
+
+    /**
+     * The background color used for filling the background of the bounding box
+     * of the rendered text. <code>null</code> means no fill.
+     * Default value is <code>null</code>.
+     */
+    protected Color backgroundColor = null;
+
+    /**
+     * The color used for drawing the expanded bounding box border for the
+     * rendered text. <code>null</code> means no border is rendered. Default
+     * value is <code>null</code>.
+     */
+    protected Color borderColor = null;
+
+    protected double scale = 1.0;
+    protected transient double scaleRecip = 1.0;
+
+    /**
+     * 
+     */
+    protected float borderWidth = 0.f;
+
+    protected double paddingX = 2.0;
+    protected double paddingY = 2.0;
+
+    /**
+     * Horizontal text box alignment with respect to its origin. Default value is
+     * 0 (leading).
+     */
+    protected byte horizontalAlignment = 0;
+    /**
+     * Vertical text box alignment with respect to its origin. Default value is
+     * 3 (baseline).
+     */
+    protected byte verticalAlignment = 3;
+
+    /**
+     * Tells if this node is still pending for real results or not.
+     */
+    protected static final int STATE_PENDING = (1 << 0);
+    protected static final int STATE_HOVER   = (1 << 1);
+    protected static final int STATE_EDITABLE = (1 << 2);
+    protected static final int STATE_SHOW_SELECTION = (1 << 3);
+    protected static final int STATE_WRAP_TEXT = (1 << 4);
+    protected transient static final int STATE_EDITING = (1 << 5);
+    protected transient static final int STATE_VALID = (1 << 6);
+    protected transient static final int STATE_X_OFFSET_IS_DIRTY = (1 << 7);
+    protected static final int STATE_ALWAYS_ADD_LISTENERS = (1 << 8);
+    protected static final int STATE_LISTENERS_ADDED = (1 << 9);
+    protected static final int STATE_AUTOMATIC_TEXT_FLIP_ENABLED = (1 << 10);
+    protected static final int STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN = (1 << 11);
+
+    /**
+     * A combination of all the STATE_ constants defined in this class,
+     * e.g. {@link #STATE_PENDING}.
+     */
+    protected int state = STATE_SHOW_SELECTION | STATE_WRAP_TEXT | STATE_VALID | STATE_X_OFFSET_IS_DIRTY;
+
+    protected RVI dataRVI = null;
+
+    int caret = 0;
+    int selectionTail = 0;
+    float xOffset = 0;
+
+    float fixedWidth = 0f;
+
+    private Rectangle2D targetBounds;
+
+    Function1<String, String> validator;
+    ITextListener textListener;
+    ITextContentFilter editContentFilter;
+
+    /**
+     * The renderable line structures parsed from {@link #text} by
+     * {@link #parseLines(String)}, laid out by
+     * {@link #layoutLines(Line[], FontRenderContext)} and aligned by
+     * {@link #alignLines(Line[], Rectangle2D, byte, byte)}
+     */
+    protected transient Line[]           lines                      = null;
+    protected transient FontMetrics      fontMetrics                = null;
+
+    /**
+     * Stores the value of {@link #text} before edit mode was last entered. Used
+     * for restoring the original value if editing is cancelled.
+     */
+    private transient String             textBeforeEdit             = null;
+    protected transient TextEditActivation editActivation;
+
+    /**
+     * Stores the last scaled bounds.
+     */
+    private transient Rectangle2D        lastBounds = new Rectangle2D.Double();
+
+    /**
+     * This must be nullified if anything that affects the result of
+     * {@link #getTightAlignedBoundsInLocal(Rectangle2D, FontRenderContext)}
+     * changes. It will cause the cached value to be recalculated on the next
+     * request.
+     */
+    private transient Rectangle2D        tightBoundsCache = null;
+
+    @Override
+    public void init() {
+        super.init();
+        // Mark this node as pending
+        NodeUtil.increasePending(this);
+    }
+
+    @Override
+    public void cleanup() {
+        removeListeners();
+        super.cleanup();
+    }
+
+    protected boolean hasState(int flags) {
+        return (state & flags) == flags;
+    }
+
+    protected void setState(int flags) {
+        this.state |= flags;
+    }
+
+    protected void setState(int flags, boolean set) {
+        if (set)
+            this.state |= flags;
+        else
+            this.state &= ~flags;
+    }
+
+    protected void clearState(int flags) {
+        this.state &= ~flags;
+    }
+
+    protected void setListeners(boolean add) {
+        if (add)
+            addListeners();
+        else
+            removeListeners();
+    }
+
+    protected void addListeners() {
+        if (!hasState(STATE_LISTENERS_ADDED)) {
+            addEventHandler(this);
+            setState(STATE_LISTENERS_ADDED);
+        }
+    }
+
+    protected void removeListeners() {
+        if (hasState(STATE_LISTENERS_ADDED)) {
+            removeEventHandler(this);
+            clearState(STATE_LISTENERS_ADDED);
+        }
+    }
+
+    /**
+     * 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 
+     * @param force
+     */
+    public void setForceEventListening(boolean force) {
+        setState(STATE_ALWAYS_ADD_LISTENERS, force);
+        if (force && !hasState(STATE_EDITABLE)) {
+            setListeners(force);
+        }
+    }
+
+    /**
+     * Enables or disables edit mode. It also sets
+     * the caret at the end of text all selects the
+     * whole text (this is the usual convention when
+     * beginning to edit one line texts).
+     * @param edit
+     * @return null if no change to edit state was made
+     */
+    public Boolean setEditMode(boolean edit) {
+        return setEditMode(edit, true);
+    }
+
+    /**
+     * Enables or disables edit mode. It also sets
+     * the caret at the end of text all selects the
+     * whole text (this is the usual convention when
+     * beginning to edit one line texts).
+     * @param edit
+     * @return null if no change to edit state was made
+     */
+    protected Boolean setEditMode(boolean edit, boolean notify) {
+        if (edit && !hasState(STATE_EDITABLE))
+            return null;
+        if (hasState(STATE_EDITING) == edit)
+            return null;
+        setState(STATE_EDITING, edit);
+        if (edit) {
+            caret = text != null ? text.length() : 0;
+            selectionTail = 0;
+            textBeforeEdit = text;
+            if (notify)
+                fireTextEditingStarted();
+            return Boolean.TRUE;
+        } else {
+            if (notify)
+                fireTextEditingEnded();
+            return Boolean.FALSE;
+        }
+    }
+
+    @SyncField({"editable"})
+    public void setEditable(boolean editable) {
+        boolean changed = hasState(STATE_EDITABLE) != editable;
+        setState(STATE_EDITABLE, editable);
+        if (hasState(STATE_EDITING) && !editable)
+            setEditMode(false);
+        if (changed && !hasState(STATE_ALWAYS_ADD_LISTENERS)) {
+            setListeners(editable);
+        }
+    }
+
+    public boolean isEditable() {
+        return hasState(STATE_EDITABLE);
+    }
+
+    public boolean isEditMode() {
+        return hasState(STATE_EDITING);
+    }
+    
+    @SyncField({"wrapText"})
+    public void setWrapText(boolean wrapText) {
+        setState(STATE_WRAP_TEXT, wrapText);
+    }
+    
+    /**
+     * @return Does the text box wrap text if 
+     * the width of the box is fixed
+     */
+    public boolean isWrapText() {
+        return hasState(STATE_WRAP_TEXT);
+    }
+
+    @SyncField({"showSelection"})
+    public void setShowSelection(boolean showSelection) {
+        setState(STATE_SHOW_SELECTION, showSelection);
+    }
+
+    public boolean showsSelection() {
+        return hasState(STATE_SHOW_SELECTION);
+    }
+
+    /**
+     * @param text
+     * @param font
+     * @param color
+     * @param x not supported anymore, use {@link #setTransform(AffineTransform)} instead
+     * @param y not supported anymore, use {@link #setTransform(AffineTransform)} instead
+     * @param scale
+     */
+    @SyncField({"text", "font", "color", "x", "y", "scale"})
+    public void init(String text, Font font, Color color, double x, double y, double scale) {
+        // no value => value
+        if(this.text == null && text != null) NodeUtil.decreasePending(this);
+
+        if (hasState(STATE_EDITING))
+            return;
+
+        this.text = text;
+        this.font = font;
+        this.color = color;
+        this.scale = scale;
+        this.scaleRecip = 1.0 / scale;
+        this.caret = 0;
+        this.selectionTail = 0;
+        
+
+        resetCaches();
+    }
+
+    public void setAutomaticTextFlipping(TextFlipping type) {
+        switch (type) {
+        case Disabled:
+            clearState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED | STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
+            break;
+        case VerticalTextDownwards:
+            setState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED | STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
+            break;
+        case VerticalTextUpwards:
+            setState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED);
+            clearState(STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
+            break;
+        }
+    }
+
+    @SyncField({"paddingX", "paddingY"})
+    public void setPadding(double x, double y) {
+        this.paddingX = x;
+        this.paddingY = y;
+    }
+
+    @SyncField({"color"})
+    public void setColor(Color color) {
+        this.color = color;
+    }
+
+    @SyncField({"backgroundColor"})
+    public void setBackgroundColor(Color color) {
+        this.backgroundColor = color;
+    }
+
+    @SyncField({"borderColor"})
+    public void setBorderColor(Color color) {
+        this.borderColor = color;
+    }
+
+    public String getText() {
+        return text;
+    }
+    
+    public String getTextBeforeEdit() {
+       return textBeforeEdit;
+    }
+
+    @SyncField({"text","caret","selectionTail"})
+    public void setText(String text) {
+        //System.out.println("TextNode.setText('" + text + "', " + editing + ")");
+        if (hasState(STATE_EDITING))
+            return;
+
+        // value => no value
+        if(this.text != null && text == null) NodeUtil.increasePending(this);
+        // no value => value
+        if(this.text == null && text != null) NodeUtil.decreasePending(this);
+
+        this.text = text;
+        caret = text != null ? Math.min(caret, text.length()) : 0;
+        selectionTail = caret;
+
+        resetCaches();
+    }
+
+    @SyncField({"pending"})
+    public void setPending(boolean pending) {
+        boolean p = hasState(STATE_PENDING);
+        if(!p && pending) NodeUtil.increasePending(this);
+        if(p && !pending) NodeUtil.decreasePending(this);
+        if(p != pending)
+            setState(STATE_PENDING, pending);
+    }
+
+    @SyncField({"fixedWidth"})
+    public void setFixedWidth(float fixedWidth) {
+        if (fixedWidth < 0f)
+            throw new IllegalArgumentException("negative fixed width");
+        this.fixedWidth = fixedWidth;
+        invalidateXOffset();
+    }
+    
+    /**
+     * Bounds where the text box will be drawn
+     * @param bounds
+     */
+    public void setTargetBounds(Rectangle2D bounds) {
+        this.targetBounds = bounds;
+    }
+
+       final public void synchronizeWidth(float width) {
+               if (width >= 0.0f)
+                       setFixedWidth(width);
+       }
+
+       final public void synchronizeBorderWidth(float width) {
+               if (width >= 0.0f)
+                       setBorderWidth(width);
+       }
+
+    public final void synchronizeWrapText(boolean wrap) {
+        setState(STATE_WRAP_TEXT, wrap);
+    }
+
+    public boolean isHovering() {
+        return hasState(STATE_HOVER);
+    }
+
+    @SyncField({"hover"})
+    public void setHover(boolean hover) {
+        setState(STATE_HOVER, hover);
+        repaint();
+    }
+
+    public Font getFont() {
+        return font;
+    }
+
+    @SyncField({"font"})
+    public void setFont(Font font) {
+        this.font = font;
+        resetCaches();
+    }
+
+    public double getBorderWidth() {
+        return borderWidth;
+    }
+
+    @SyncField({"borderWidth"})
+    public void setBorderWidth(float width) {
+        this.borderWidth = width;
+    }
+
+    public void setBorderWidth(double width) {
+        setBorderWidth((float)width);
+    }
+
+    @SyncField({"horizontalAlignment"})
+    public void setHorizontalAlignment(byte horizontalAlignment) {
+        if (horizontalAlignment < 0 && horizontalAlignment > 2)
+            throw new IllegalArgumentException("Invalid horizontal alignment: " + horizontalAlignment + ", must be between 0 and 2");
+        this.horizontalAlignment = horizontalAlignment;
+        resetCaches();
+    }
+
+    final public void synchronizeHorizontalAlignment(byte horizontalAlignment) {
+        if (horizontalAlignment >= 0 && horizontalAlignment <= 2)
+            setHorizontalAlignment(horizontalAlignment);
+    }
+
+    public byte getHorizontalAlignment() {
+        return horizontalAlignment;
+    }
+
+    @SyncField({"verticalAlignment"})
+    public void setVerticalAlignment(byte verticalAlignment) {
+        if (verticalAlignment < 0 && verticalAlignment > 3)
+            throw new IllegalArgumentException("Invalid vertical alignment: " + verticalAlignment + ", must be between 0 and 3");
+        this.verticalAlignment = verticalAlignment;
+        resetCaches();
+    }
+
+    final public void synchronizeVerticalAlignment(byte verticalAlignment) {
+        if (verticalAlignment >= 0 && verticalAlignment <= 3)
+            setVerticalAlignment(verticalAlignment);
+    }
+
+    public byte getVerticalAlignment() {
+        return verticalAlignment;
+    }
+
+    /**
+     * Rendering is single-threaded so we can use a static rectangle for
+     * calculating the expanded bounds for the node.
+     */
+    private static transient ThreadLocal<Rectangle2D> tempBounds = new ThreadLocal<Rectangle2D>() {
+        @Override
+        protected Rectangle2D initialValue() {
+            return new Rectangle2D.Double();
+        }
+    };
+
+    /**
+     * Rendering is single-threaded so we can use a static AffineTransform to
+     * prevent continuous memory allocation during text rendering.
+     */
+    private static transient ThreadLocal<AffineTransform> tempAffineTransform = new ThreadLocal<AffineTransform>() {
+        @Override
+        protected AffineTransform initialValue() {
+            return new AffineTransform();
+        }
+    };
+
+    @Override
+    public void render(Graphics2D g) {
+        AffineTransform ot = g.getTransform();
+        render(g, true);
+        g.setTransform(ot);
+    }
+
+    /**
+     * Note: does not return transformation, stroke, color, etc. to their
+     * original states
+     * 
+     * @param g
+     * @param applyTransform
+     */
+    public void render(Graphics2D g, boolean applyTransform) {
+        if (text == null || font == null || color == null)
+            return;
+
+        // Cache font metrics if necessary
+        if (fontMetrics == null)
+            fontMetrics = g.getFontMetrics(font);
+
+        Color color = this.color;
+        boolean isSelected = NodeUtil.isSelected(this, 1);
+        boolean hover = hasState(STATE_HOVER);
+        boolean editing = hasState(STATE_EDITING);
+
+        if (!isSelected && hover) {
+            color = add(color, 120, 120, 120);
+        }
+
+        if (applyTransform)
+            g.transform(transform);
+        // Apply separate legacy scale
+        if (scale != 1.0)
+            g.scale(scale, scale);
+
+        // Safety for not rendering when the scale of this text is too small.
+        // When the scale is too small it will cause internal exceptions while
+        // stroking fonts.
+        AffineTransform curTr = g.getTransform();
+        double currentScale = GeometryUtils.getScale(curTr);
+        //System.out.println("currentScale: " + currentScale);
+        if (currentScale < 1e-6)
+            return;
+
+        g.setFont(font);
+        //g.translate(x, y);
+
+        // Calculate text clip rectangle.
+        // This updates textLayout if necessary.
+        Rectangle2D r = getTightAlignedBoundsInLocal(tempBounds.get(), fontMetrics.getFontRenderContext());
+
+        computeEditingXOffset();
+
+        if (fixedWidth > 0f)
+            r.setFrame(r.getMinX(), r.getMinY(), fixedWidth, r.getHeight());
+        if(targetBounds != null) {
+            double w = (targetBounds.getWidth() - paddingX * 2) * scaleRecip;
+            double h = (targetBounds.getHeight() - paddingY * 2) * scaleRecip;
+            double x = (targetBounds.getMinX() + paddingX) * scaleRecip;
+            double y = (targetBounds.getMinY() + paddingY) * scaleRecip;
+            r.setRect(x, y, w, h);
+        }
+
+        if (hasState(STATE_AUTOMATIC_TEXT_FLIP_ENABLED)) {
+            boolean needsXFlip;
+            boolean needsYFlip;
+            if (curTr.getScaleX() != 0) {
+                needsXFlip = curTr.getScaleX() < 0.0;
+                needsYFlip = curTr.getScaleY() < 0.0;
+            } else {
+                boolean flipAll = !hasState(STATE_AUTOMATIC_TEXT_FLIP_VERTICAL_DOWN);
+                needsXFlip = (curTr.getShearY() < 0.0) ^ flipAll;
+                needsYFlip = (curTr.getShearX() > 0.0) ^ flipAll;
+            }
+            if (needsXFlip || needsYFlip) {
+                double centerX = r.getWidth()*0.5 + r.getX();
+                double centerY = r.getHeight()*0.5 + r.getY();
+
+                g.translate(centerX, centerY);
+                g.scale(needsXFlip ? -1.0 : 1.0, needsYFlip ? -1.0 : 1.0);
+                g.translate(-centerX, -centerY);
+            }
+        }
+
+        Rectangle2D textClip = r.getBounds2D();
+
+        expandBoundsUnscaled(r);
+
+        // Speed rendering optimization: don't draw text that is too small to
+        // read when not editing
+        boolean renderText = true;
+        if (!editing) {
+            Object renderingHint = g.getRenderingHint(RenderingHints.KEY_RENDERING);
+            if (renderingHint != RenderingHints.VALUE_RENDER_QUALITY) {
+                float textSizeMM = (float) currentScale * GeometryUtils.pointToMillimeter(font.getSize2D());
+                if (textSizeMM < 1.5f)
+                    renderText = false;
+            }
+        }
+
+        Shape clipSave = g.getClip();
+        g.setClip(textClip);
+
+        // PDF 
+        PdfWriter writer = (PdfWriter) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_WRITER);
+        TextRenderingMode renderingMode = (TextRenderingMode) g.getRenderingHint(G2DRenderingHints.KEY_TEXT_RENDERING_MODE);
+        boolean renderAsText = writer != null || renderingMode == TextRenderingMode.AS_TEXT;
+        /// PDF
+
+        Color backgroundColor = hasState(STATE_VALID) ? this.backgroundColor : Color.red;
+
+        // RENDER
+        {
+            // Fill background if necessary
+            if (backgroundColor != null) {
+                g.setColor(backgroundColor);
+                g.fill(r);
+            }
+
+            if (editing) {
+
+                int selectionMin = Math.min(caret, selectionTail);
+                int selectionMax = Math.max(caret, selectionTail);
+
+                // Base text
+                g.setColor(color);
+                renderText(g, xOffset, renderAsText);
+
+                Shape clip = g.getClip();
+
+                // Selection background & text
+                for (Line line : lines) {
+                    if (line.intersectsRange(selectionMin, selectionMax)) {
+                        Shape selShape = line.getLogicalHighlightShape(selectionMin, selectionMax);
+                        line.translate(g, xOffset, 0);
+                        g.setClip(selShape);
+                        g.setColor(SELECTION_BACKGROUND_COLOR);
+                        g.fill(selShape);
+                        g.setColor(Color.WHITE);
+                        // #6459: render as text in PDF and paths on screen
+                        if (renderAsText)
+                            g.drawString(line.getText(), 0, 0);
+                        else
+                            line.layout.draw(g, 0, 0);
+                        line.translateInv(g, xOffset, 0);
+                    }
+                }
+
+                g.setClip(clip);
+
+                renderCaret(g);
+
+            } else {
+
+                if (renderText) {
+                    g.setColor(color);
+                    renderText(g, 0, renderAsText);
+                }
+
+            }
+        }
+
+        g.setClip(clipSave);
+
+        if (borderWidth > 0f && borderColor != null) {
+            g.setColor(borderColor);
+            g.setStroke(new BasicStroke((float) (scale*borderWidth)));
+            g.draw(r);
+        }
+
+        //System.out.println("bw: " + borderWidth);
+        if (isSelected && showsSelection()) {
+            Composite oc = g.getComposite();
+            g.setComposite(SrcOver_50);
+            g.setColor(Color.RED);
+            float bw = borderWidth;
+            double s = currentScale;
+            if (bw <= 0f) {
+                bw = (float) (1f / s);
+            } else {
+                bw *= 5f * scale;
+            }
+            g.setStroke(new BasicStroke(bw));
+            g.draw(r);
+            //g.draw(GeometryUtils.expandRectangle(r, 1.0));
+
+            g.setComposite(oc);
+        }
+
+        g.scale(scaleRecip, scaleRecip);
+        g.setStroke(RESET_STROKE);
+
+        lastBounds = getScaledOffsetBounds(r, lastBounds, scale, 0, 0);
+//        g.setColor(Color.MAGENTA); // DEBUG
+//        g.draw(lastBounds); // DEBUG
+//        g.setColor(Color.ORANGE); // DEBUG
+//        g.draw(getBoundsInLocal()); // DEBUG
+
+        renderSelectedHover(g, isSelected, hover);
+    }
+
+    private void renderCaret(Graphics2D g) {
+       g.setColor(Color.BLACK);
+       for (int i = 0; i < lines.length; i++) {
+               Line line = lines[i];
+               // prevent rendering caret twice on line changes
+               if (line.containsOffset(caret) &&                // line contains caret
+                  (caret != line.endOffset ||                   //caret is not in the end of the line
+                   i == lines.length-1 ||                       //caret is end of the last line
+                   lines[i+1].startOffset != line.endOffset)) { // beginning of the next line does not start withe the same index as current line
+              Shape[] caretShape = line.getCaretShapes(caret);
+              line.translate(g, xOffset, 0);
+              g.draw(caretShape[0]);
+              if (caretShape[1] != null)
+                  g.draw(caretShape[1]);
+              line.translateInv(g, xOffset, 0);
+          }
+        }
+    }
+    private void renderText(Graphics2D g, float xOffset, boolean renderAsText) {
+        //g.draw(tightBoundsCache); // DEBUG
+        for (Line line : lines) {
+            // #6459: render as text in PDF and paths on screen
+            if (renderAsText)
+                g.drawString(line.getText(), line.alignedPosX + xOffset, line.alignedPosY);
+            else
+                line.layout.draw(g, line.alignedPosX + xOffset, line.alignedPosY);
+            //g.draw(line.abbox); // DEBUG
+        }
+    }
+
+    protected Rectangle2D getScaledOffsetBounds(Rectangle2D originalBounds, Rectangle2D dst, double scale, double offsetX, double offsetY) {
+        AffineTransform btr = tempAffineTransform.get();
+        btr.setToTranslation(offsetX*scale, offsetY*scale);
+        btr.scale(scale, scale);
+        if (btr.isIdentity()) {
+            dst.setFrame(originalBounds);
+        } else {
+            dst.setFrame(btr.createTransformedShape(originalBounds).getBounds2D());
+        }
+        return dst;
+    }
+
+    /**
+     * Invoked when TextNode is selected and a mouse is hovering on top of it. Can be overridden to add custom rendering.
+     * 
+     * @param g
+     */
+    protected void renderSelectedHover(Graphics2D g, boolean isSelected, boolean isHovering) {
+    }
+
+    public String editText(String text) {
+
+        String error = validator != null ? validator.apply(text) : null;
+        if (error == null) {
+            this.text = text;
+            if (textListener != null) {
+                textListener.textEditingEnded();
+            }
+        }
+        return error;
+    }
+
+    /**
+     * Replaces the current selection with the content or inserts
+     * the content at caret. After the insertion the caret
+     * will be at the end of inserted text and selection will
+     * be empty.
+     * @param content
+     */
+    @SyncField({"text","caret","selectionTail"})
+    protected void insert(String content) {
+        content = editContentFilter != null ? editContentFilter.filter(this, content) : content; 
+
+        int selectionMin = Math.min(caret, selectionTail);
+        int selectionMax = Math.max(caret, selectionTail);
+
+        String begin = text.substring(0, selectionMin);
+        String end = text.substring(selectionMax);
+        text = begin + content + end;
+        caret = selectionMin + content.length();
+        selectionTail = caret;
+
+        assert (caret <= text.length());
+        //System.out.println(text + " " + caret );
+
+        if(validator != null) {
+            String error = validator.apply(text);
+            setState(STATE_VALID, (error == null));
+        }
+
+        resetCaches();
+    }
+
+    @ServerSide
+    protected void fireTextChanged() {
+        if(textListener != null)
+            textListener.textChanged();
+        repaint();
+    }
+
+    @ServerSide
+    protected void fireTextEditingStarted() {
+        if(textListener != null)
+            textListener.textEditingStarted();
+    }
+
+    @ServerSide
+    protected void fireTextEditingCancelled() {
+        setState(STATE_VALID);
+
+        if (deactivateEdit()) {
+            if (textListener != null)
+                textListener.textEditingCancelled();
+
+            setEditMode(false, false);
+
+            if (textBeforeEdit != null)
+                setText(textBeforeEdit);
+
+            repaint();
+        }
+    }
+
+    @ServerSide
+    public void fireTextEditingEnded() {
+        if (!hasState(STATE_VALID)) {
+            fireTextEditingCancelled();
+            setState(STATE_VALID);
+            return;
+        }
+
+        if (deactivateEdit()) {
+            if (textListener != null)
+                textListener.textEditingEnded();
+
+            setEditMode(false, false);
+            repaint();
+        }
+    }
+
+    public void setTextListener(ITextListener listener) {
+        this.textListener = listener;
+    }
+
+    public void setValidator(Function1<String, String> validator) {
+        this.validator = validator;
+    }
+
+    public void setContentFilter(ITextContentFilter filter) {
+        this.editContentFilter = filter;
+    }
+
+    public void setRVI(RVI rvi) {
+        this.dataRVI = rvi;
+    }
+
+    private void invalidateXOffset() {
+        setState(STATE_X_OFFSET_IS_DIRTY);
+    }
+
+    private void computeEditingXOffset() {
+
+        if(lines == null) return;
+        if(!hasState(STATE_X_OFFSET_IS_DIRTY)) return;
+        if(fixedWidth > 0f) {
+
+            // TODO: implement
+//            float[] coords = textLayout.getCaretInfo(TextHitInfo.afterOffset(caret));
+//            if(coords != null) {
+//                if(coords[0] > (fixedWidth*MAX_CARET_POSITION)) xOffset = (float)((fixedWidth*MAX_CARET_POSITION)-coords[0]);
+//                else xOffset = 0;
+//            }
+
+        } else {
+
+            xOffset = 0;
+
+        }
+
+        clearState(STATE_X_OFFSET_IS_DIRTY);
+
+    }
+
+    @SyncField({"caret","selectionTail"})
+    protected void moveCaret(int move, boolean select) {
+       // prevent setting caret into line separator. 
+       if (move > 0) {
+               while (text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
+                       move++;
+       } else if (move < 0) {
+               while (caret+move >= 0 && text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
+                       move--;
+       }
+        caret += move;
+        if(caret < 0)
+            caret = 0;
+        if (caret > text.length())
+            caret = text.length();
+        if(!select)
+            selectionTail = caret;
+    }
+    
+    private Line findCaretLine() {
+        // Find the line where caret is. Starting from first line.
+        for(int i = 0; i < lines.length; i++) {
+            Line line = lines[i];
+            if(caret <= line.endOffset) {
+                return line;
+            }
+        }
+        return null;
+    }
+    
+    /**
+     * Moves caret to next not letter or digit
+     * @param shiftDown
+     */
+    private void moveCaretCtrlLeft(boolean shiftDown) {
+        Line line = findCaretLine();
+        if(line != null) {
+            int i;
+            for(i = caret-1; i > line.startOffset; i--) {
+                char c = line.document.charAt(i);
+                if(!Character.isLetterOrDigit(c)) {
+                    break;
+                }
+            }
+            moveCaret(i - caret, shiftDown);
+        }
+    }
+    
+    /**
+     * Moves caret to previous non letter or digit
+     * @param shiftDown
+     */
+    private void moveCaretCtrlRight(boolean shiftDown) {
+        Line line = findCaretLine();
+        if(line != null) {
+            int i;
+            for(i = caret + 1; i < line.endOffset; i++) {
+                char c = line.document.charAt(i);
+                if(!Character.isLetterOrDigit(c)) {
+                    break;
+                }
+            }
+            moveCaret(i - caret, shiftDown);
+        }
+    }
+    
+    /**
+     * Moves caret to line end
+     * @param shiftDown
+     */
+    private void moveCaretEnd(boolean shiftDown) {
+        Line line = findCaretLine();
+        if(line != null)
+            // Move caret to the end of the line
+            moveCaret(line.endOffset - caret, shiftDown);
+    }
+    
+    /**
+     * Moves caret to beginning of a line
+     * @param shiftDown
+     */
+    private void moveCaretHome(boolean shiftDown) {
+        Line line = findCaretLine();
+        if(line != null)
+            // Move caret to the beginning of the line
+            moveCaret(line.startOffset - caret, shiftDown);
+    }
+    
+    /**
+     * Moves caret one row up and tries to maintain the location
+     * @param shiftDown
+     */
+    private void moveCaretRowUp(boolean shiftDown) {
+        // Find the line where caret is. Starting from first line.
+        for(int i = 0; i < lines.length; i++) {
+            Line line = lines[i];
+            if(caret <= line.endOffset) {
+                // caret is in this line
+                if(i == 0) {
+                    // Already on top line
+                    // Select the beginning of the line
+                    moveCaret(-caret, shiftDown);
+                } else {
+                    Line prevLine = lines[i-1];
+                    int prevLength = prevLine.endOffset - prevLine.startOffset;
+                    int posInCurRow = caret - line.startOffset;
+                    if(prevLength < posInCurRow)
+                        posInCurRow = prevLength;
+
+                    int newPos = prevLine.startOffset + posInCurRow;
+                    moveCaret(newPos - caret, shiftDown);
+                }
+                break;
+            }
+        }        
+    }
+    
+    /**
+     * Moves caret one row down and tries to maintain the location
+     * @param shiftDown
+     */
+    private void moveCaretRowDown(boolean shiftDown) {
+        // Find the line where caret is. Starting from last line.
+        for(int i = lines.length - 1; i >= 0; i--) {
+            Line line = lines[i];
+            if(caret >= line.startOffset) {
+                // caret is in this line
+                if(i == lines.length - 1) {
+                    // Already on bottom line, cannot go below
+                    // Select to the end of the line
+                    moveCaret(line.endOffset - caret, shiftDown);
+                } else {
+                    Line prevLine = lines[i+1]; // Previous line
+                    
+                    // Find new caret position. 
+                    // Either it is in the same index as before, or if the row
+                    // is not long enough, select the end of the row.
+                    int prevLength = prevLine.endOffset - prevLine.startOffset;
+                    int posInCurRow = caret - line.startOffset;
+                    if(prevLength < posInCurRow)
+                        posInCurRow = prevLength;
+                    int newPos = prevLine.startOffset + posInCurRow;
+                    moveCaret(newPos - caret, shiftDown);
+                }
+                break;
+            }
+        }        
+    }
+
+    @SyncField({"caret","selectionTail"})
+    protected void setCaret(int pos, boolean select) {
+        caret = pos;
+        if (caret < 0)
+            caret = 0;
+        if (caret > text.length())
+            caret = text.length();
+        if (!select)
+            selectionTail = caret;
+    }
+
+    protected void setCaret(Point2D point) {
+       setCaret(point, false);
+    }
+
+    @SyncField({"caret","selectionTail"})
+    protected void setCaret(Point2D point, boolean select) {
+        double lineY = 0;
+        for(int i = 0; i < lines.length; i++) {
+            Line line = lines[i];
+            Rectangle2D bounds = line.abbox;
+            // Add heights of bboxes for determining the correct line
+            if(i == 0)
+                lineY = bounds.getY();
+            else
+                lineY += lines[i-1].abbox.getHeight();
+            
+            double lineHeight = bounds.getHeight();
+            double hitY = point.getY() / scale;
+            if(hitY >= lineY && hitY <= lineY + lineHeight) {
+                // Hit is in this line
+                float x = (float)(point.getX() / scale) - (float)line.abbox.getX();
+                float y = (float)(point.getY() / scale -  lineHeight * i) ;
+                TextHitInfo info = line.layout.hitTestChar(x, y);
+                caret = line.startOffset + info.getInsertionIndex();
+                if (caret > line.endOffset)
+                       caret = line.endOffset;
+                if (!select)
+                    selectionTail = caret;
+                repaint();
+                break;
+            }
+        }
+        invalidateXOffset();
+        assert (caret <= text.length());
+    }
+    
+    @Override
+    public Rectangle2D getBoundsInLocal() {
+        if(targetBounds != null)
+            return targetBounds;
+        else
+            return expandBounds( getTightAlignedBoundsInLocal(null) );
+    }
+
+    protected Rectangle2D expandBounds(Rectangle2D r) {
+        r.setRect(r.getX() * scale - paddingX, r.getY() * scale -paddingY, r.getWidth()*scale + paddingX + paddingX, r.getHeight()*scale + paddingY + paddingY);
+        //System.out.println("  => " + r);
+        return r;
+    }
+
+    protected Rectangle2D expandBoundsUnscaled(Rectangle2D r) {
+        r.setRect(r.getX() - scaleRecip*paddingX, r.getY() -scaleRecip*paddingY, r.getWidth() + scaleRecip*paddingX + scaleRecip*paddingX, r.getHeight() + scaleRecip*paddingY + scaleRecip*paddingY);
+        //System.out.println("  => " + r);
+        return r;
+    }
+
+    protected Rectangle2D expandBounds(Rectangle2D r, double amount) {
+        r.setRect(r.getX() - amount, r.getY() - amount, r.getWidth() + 2*amount, r.getHeight() + 2*amount);
+        return r;
+    }
+
+    protected Rectangle2D expandBounds(Rectangle2D r, double left, double top, double right, double bottom) {
+        r.setRect(r.getX() - left, r.getY() - top, r.getWidth() + left + right, r.getHeight() + top + bottom);
+        return r;
+    }
+
+    private void resetCaches() {
+        this.tightBoundsCache = null;
+        this.lines = null;
+        this.fontMetrics = null;
+    }
+
+    /**
+     * Returns the tight bounds around the current text using the current font
+     * in the specified rectangle. If the specified rectangle is
+     * <code>null</code> a new Rectangle2D.Double instance will be created.
+     * 
+     * @param r
+     * @return
+     */
+    protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r) {
+        return getTightAlignedBoundsInLocal(r, FRC);
+    }
+
+    /**
+     * Returns the tight bounds around the current text using the current font
+     * in the specified rectangle. If the specified rectangle is
+     * <code>null</code> a new Rectangle2D.Double instance will be created.
+     * 
+     * @param r
+     *            the rectangle where the result of the method is placed or
+     *            <code>null</code> to allocate new rectangle
+     * @param frc current font render context
+     * @return r or new Rectangle2D.Double instance containing the requested
+     *         text bounds
+     */
+    protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r, FontRenderContext frc) {
+        if (r == null)
+            r = new Rectangle2D.Double();
+
+        if (tightBoundsCache != null) {
+            r.setFrame(tightBoundsCache);
+            return r;
+        }
+
+        String txt = text;
+        if (font == null || txt == null) {
+            r.setFrame(0, 0, 2, 1);
+            return r;
+        }
+
+        //System.out.println("TextNode.getTightAlignedBoundsInLocal('" + txt + "')");
+
+        // Parse & layout (unaligned)
+        Line[] lines = null;
+        
+        if(hasState(STATE_WRAP_TEXT)) {
+            float width = fixedWidth;
+            if(width <= 0 && targetBounds != null)
+                width = (float) (((targetBounds.getWidth() - 2*paddingX)) * scaleRecip);
+            if(width > 0)
+                lines = wrapLines(txt, font, width, frc);
+        }
+         
+        if(lines == null)
+            lines = parseLines(txt);
+        this.lines = layoutLines(lines, frc);
+
+        // Calculate tight bounds based on unaligned layout
+        //System.out.println("Unaligned");
+        tightBoundsCache = calculateBounds(lines, Line.BBOX, null);
+        //System.out.println("  => " + tightBoundsCache);
+
+        this.lines = layoutLinesX(lines, tightBoundsCache);
+        // Align each line to the calculated tight bounds
+        this.lines = alignLines(this.lines, tightBoundsCache, horizontalAlignment, verticalAlignment);
+
+        // Calculate aligned bounds
+        //System.out.println("Aligned");
+        calculateBounds(lines, Line.ABBOX, tightBoundsCache);
+
+        r.setFrame(tightBoundsCache);
+        //System.out.println("  => " + tightBoundsCache);
+
+        return r;
+    }
+
+    /**
+     * @param lines
+     * @param bbox
+     *            the bounding box of all the whole laid out text (only bbox
+     *            size is used)
+     * @return
+     */
+    private Line[] layoutLinesX(Line[] lines, Rectangle2D bbox) {
+        int lineCount = lines.length;
+        for (int l = 0; l < lineCount; ++l) {
+            Line line = lines[l];
+            // Compute pen x position. If the paragraph is right-to-left we
+            // will align the TextLayouts to the right edge of the panel.
+            // Note: drawPosX is always where the LEFT of the text is placed.
+            // NOTE: This changes based on horizontal alignment
+            line.drawPosX = (float) (line.layout.isLeftToRight() ? 0f
+                    : tightBoundsCache.getWidth() - line.layout.getAdvance());
+        }
+        return lines;
+    }
+
+    /**
+     * @param lines
+     * @param boundsProvider
+     * @param result
+     * @return
+     */
+    private static Rectangle2D calculateBounds(Line[] lines, BoundsProcedure boundsProvider, Rectangle2D result) {
+        if (result == null)
+            result = new Rectangle2D.Double();
+        else
+            result.setFrame(0, 0, 0, 0);
+
+        for (Line line : lines) {
+            //System.out.println("line: " + line);
+            Rectangle2D bbox = boundsProvider.getBounds(line);
+            if (result.isEmpty())
+                result.setFrame(bbox);
+            else
+                Rectangle2D.union(result, bbox, result);
+            //System.out.println("bounds: " + result);
+        }
+        //System.out.println("final bounds: " + result);
+
+        return result;
+    }
+
+    /**
+     * @param lines
+     * @param bbox
+     * @param hAlign
+     * @param vAlign
+     * @return aligned lines
+     */
+    private Line[] alignLines(Line[] lines, Rectangle2D bbox, byte hAlign, byte vAlign) {
+//        System.out.println("horizontal align: " + Alignment.values()[hAlign]);
+//        System.out.println("vertical align  : " + Alignment.values()[vAlign]);
+//        System.out.println("bbox: " + bbox);
+        double xbase = 0;
+//        double ybase = 0;
+        if(targetBounds != null) {
+            /* In normal cases the bounding box moves when
+             * typing. If target bounds are set, the text
+             * is fitted into the box.
+             */
+            switch (hAlign) {
+            case 1: // Trailing
+                xbase = (targetBounds.getMaxX()-paddingX) * scaleRecip;
+                break;
+            case 2: // Center
+                xbase = targetBounds.getCenterX() * scaleRecip;
+                break;
+            default: // Leading / Baseline
+                // Do nothing
+                break;
+            }
+        }
+        
+        
+        for (Line line : lines) {
+            double xoffset = 0;
+            double yoffset = 0;
+
+            switch (hAlign) {
+            case 1: // Trailing
+                xoffset = xbase - line.bbox.getWidth();
+                break;
+            case 2: // Center
+                xoffset = xbase - line.bbox.getWidth() / 2;
+                break;
+            default: // Leading / Baseline
+                // Do nothing
+                break;
+            }
+
+            switch (vAlign) {
+            case 0:
+                yoffset = line.layout.getAscent();
+                break;
+            case 1:
+                yoffset = -bbox.getHeight() + line.layout.getAscent();
+                break;
+            case 2:
+                yoffset = -bbox.getHeight() / 2 + line.layout.getAscent();
+                break;
+            }
+
+            line.alignOffset(xoffset, yoffset);
+        }
+        return lines;
+    }
+
+    /**
+     * @param lines
+     * @param frc
+     * @return
+     */
+    private Line[] layoutLines(Line[] lines, FontRenderContext frc) {
+        TextLayout emptyRowLayout = null;
+        int lineCount = lines.length;
+        float y = 0;
+        for (int l = 0; l < lineCount; ++l) {
+            Line line = lines[l];
+            String lineText = line.getText();
+            // " " because TextLayout requires non-empty text and
+            // We don't want zero size for the text.
+            if (lineText.isEmpty()) {
+                lineText = " ";
+                if (emptyRowLayout == null)
+                    emptyRowLayout = new TextLayout(lineText, font, frc);
+                line.layout = emptyRowLayout;
+            } else {
+                line.layout = new TextLayout(lineText, font, frc);
+            }
+
+            //y += line.layout.getAscent();
+            line.drawPosY = y;
+            y += line.layout.getDescent() + line.layout.getLeading() + line.layout.getAscent();
+
+            Rectangle2D bbox = line.layout.getLogicalHighlightShape(0, lineText.length()).getBounds2D();
+            // HighlightShape is not large enough, if font is italic.
+            Rectangle2D bbox2 = line.layout.getBounds();
+            bbox.add(bbox2);
+            bbox.setFrame(bbox.getX(), bbox.getY() + line.drawPosY, bbox.getWidth(), bbox.getHeight());
+            line.bbox = bbox;
+        }
+
+        return lines;
+    }
+
+    /**
+     * Splits the specified string into {@link Line} structures, one for each
+     * line in the input text. The returned lines are only partially defined,
+     * waiting to be laid out (see
+     * {@link #layoutLines(Line[], FontRenderContext)})
+     * 
+     * @param txt
+     *            input text
+     * @return parsed text lines as {@link Line} structures
+     * @see #layoutLines(Line[], FontRenderContext)
+     */
+    private static Line[] parseLines(String txt) {
+        int len = txt.length();
+        if (len == 0)
+            return new Line[] { new Line("", 0, 0) };
+
+        TIntArrayList lfpos = new TIntArrayList();
+        int pos = 0;
+        int lineCount = 1;
+        for (;pos < len; ++lineCount) {
+            int nextlf = txt.indexOf('\n', pos);
+            lfpos.add(nextlf != -1 ? nextlf : len);
+            if (nextlf == -1)
+                break;
+            pos = nextlf + 1;
+        }
+        Line[] lines = new Line[lineCount];
+        pos = 0;
+        for (int i = 0; i < lineCount-1; ++i) {
+            int lf = lfpos.getQuick(i);
+            int cr = (lf > 0) && (txt.charAt(lf - 1) == '\r') ? lf - 1 : lf;
+            lines[i] = new Line(txt, pos, cr);
+            pos = lf + 1;
+        }
+        lines[lineCount - 1] = new Line(txt, pos, len);
+
+        return lines;
+    }
+    
+    
+    private static Line[] wrapLines(String txt, Font font, float fixedWidth, FontRenderContext frc) {
+        if(txt == null || txt.isEmpty())
+            txt = " ";
+        
+        ArrayList<Line> lines = 
+                new ArrayList<Line>();
+        
+        Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
+        map.put(TextAttribute.FONT, font);
+        AttributedString attributedText = new AttributedString(txt.isEmpty() ? "__ERROR__" : txt, map);
+
+        AttributedCharacterIterator paragraph = attributedText.getIterator();
+        int paragraphStart = paragraph.getBeginIndex();
+        int paragraphEnd = paragraph.getEndIndex();
+        LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
+
+        float breakWidth = fixedWidth;
+
+        // Force text to be vertical, by setting break width to 1, if the text area is narrower than "GGGG"
+
+        // Set position to the index of the first character in the paragraph.
+        lineMeasurer.setPosition(paragraphStart);
+
+        // Get lines until the entire paragraph has been displayed.
+        int next, limit, charat, position = 0;
+        
+        while ((position = lineMeasurer.getPosition()) < paragraphEnd) {
+
+            // Find possible line break and set it as a limit to the next layout
+            next = lineMeasurer.nextOffset(breakWidth);
+            limit = next;
+            charat = txt.indexOf(System.getProperty("line.separator"),position+1);
+            if(charat < next && charat != -1){
+                limit = charat;
+            }
+            
+            lineMeasurer.nextLayout(breakWidth, limit, false);
+            // Add Line
+            lines.add(new Line(txt, position, limit));
+        }
+
+        return lines.toArray(new Line[lines.size()]);
+    }
+    
+
+    public String getClipboardContent() {
+        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+        Transferable clipData = clipboard.getContents(this);
+        try {
+            return (String) (clipData.getTransferData(DataFlavor.stringFlavor));
+        } catch (Exception ee) {
+            return null;
+        }
+    }
+
+    public void setClipboardContent(String content) {
+        Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+        StringSelection data = new StringSelection(content);
+        clipboard.setContents(data, data);
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + " [text=" + text + ", font=" + font + ", color=" + color + "]";
+    }
+
+    @Override
+    protected boolean handleCommand(CommandEvent e) {
+        if (!hasState(STATE_EDITING))
+            return false;
+
+        if (Commands.SELECT_ALL.equals(e.command)) {
+            selectAll();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected boolean keyPressed(KeyPressedEvent event) {
+        if (!hasState(STATE_EDITING))
+            return false;
+
+        char c = event.character;
+        boolean ctrl = event.isControlDown();
+        boolean alt = event.isAltDown();
+
+//        System.out.println("Key pressed '" + (event.character == 0 ? "\\0" : "" + c) + "' " + event.keyCode + " - " + Integer.toBinaryString(event.stateMask));
+//        System.out.println("ctrl: " + ctrl);
+//        System.out.println("alt: " + alt);
+        if (ctrl && !alt) {
+            switch (event.keyCode) {
+                case KeyEvent.VK_C:
+                    if (caret != selectionTail) {
+                        int selectionMin = Math.min(caret, selectionTail);
+                        int selectionMax = Math.max(caret, selectionTail);
+                        setClipboardContent(text.substring(selectionMin, selectionMax));
+                    }
+                    break;
+                    
+                case KeyEvent.VK_X:
+                    if (caret != selectionTail) {
+                        int selectionMin = Math.min(caret, selectionTail);
+                        int selectionMax = Math.max(caret, selectionTail);
+                        setClipboardContent(text.substring(selectionMin, selectionMax));
+                        insert("");
+                    }
+                    break;
+
+                case KeyEvent.VK_RIGHT:
+                    if (c == '\0')  {
+                        // '\'' has the same keycode as VK_RIGHT but when right
+                        // arrow is pressed, event character is \0.
+                        moveCaretCtrlRight(event.isShiftDown());
+                    }
+                    break;
+                    
+                case KeyEvent.VK_LEFT:
+                        moveCaretCtrlLeft(event.isShiftDown());
+                        break;
+                    
+                case KeyEvent.VK_V:
+                {
+                    String content = getClipboardContent();
+                    if(content != null)
+                        insert(content);
+                    break;
+                }
+
+                // Replaced by #handleCommand
+//                case KeyEvent.VK_A:
+//                {
+//                    selectAll();
+//                    return true;
+//                }
+                
+                case KeyEvent.VK_ENTER:
+                {
+                    insert(getLineSeparator());
+                }
+                
+                break;
+
+                default:
+                    return false;
+            }
+        } else if (!ctrl && alt) {
+            return false;
+        } else {
+            switch (event.keyCode) {
+                case KeyEvent.VK_LEFT:
+                       moveCaret(-1, event.isShiftDown());
+                    break;
+                case KeyEvent.VK_RIGHT:
+                    if (c == '\0')  {
+                        // '\'' has the same keycode as VK_RIGHT but when right
+                        // arrow is pressed, event character is \0.
+                        moveCaret(1, event.isShiftDown());
+                        break;
+                    }
+                    // Intentional fallthrough to default case
+                case KeyEvent.VK_UP:
+                    moveCaretRowUp(event.isShiftDown());
+                    break;
+                case KeyEvent.VK_DOWN:
+                    moveCaretRowDown(event.isShiftDown());
+                    break;
+                case KeyEvent.VK_HOME:
+                    moveCaretHome(event.isShiftDown());
+                    break;
+                case KeyEvent.VK_END:
+                    moveCaretEnd(event.isShiftDown());
+                    break;
+
+                case KeyEvent.VK_ENTER:
+                    fireTextEditingEnded();
+                    return true;
+
+                case KeyEvent.VK_ESCAPE:
+                    text = textBeforeEdit;
+                    resetCaches();
+                    clearState(STATE_EDITING);
+                    fireTextEditingCancelled();
+                    return true;
+
+                case KeyEvent.VK_BACK_SPACE:
+                    if(caret == selectionTail && caret > 0) {
+                       // line separator may use multiple characters, we want to remove that with one command
+                       String lineSep = getLineSeparator();
+                       int index = lineSep.indexOf(text.charAt(caret-1));
+                       if (index == -1)
+                               --caret;
+                       else {
+                               caret-= (index+1);
+                               selectionTail+= (lineSep.length()-index-1);
+                       }
+                    }
+                    insert("");
+                    break;
+
+                case KeyEvent.VK_DELETE:
+                    if(caret == selectionTail && caret < text.length()) {
+                       String lineSep = getLineSeparator();
+                       int index = lineSep.indexOf(text.charAt(caret));
+                       if (index==-1)
+                               ++caret;
+                       else {
+                               selectionTail-= index;
+                               caret+= (lineSep.length()-index);
+                       }
+                    }
+                    insert("");
+                    break;
+
+                
+
+                default:
+                    if (c == 65535 || Character.getType(c) == Character.CONTROL) {
+                        return false;
+                    }
+                    //System.out.println("Char " + c + " " + Character.getType(c) + " " + text);
+                    insert(new String(new char[] {c}));
+            }
+        }
+
+        // FIXME This is called even if just caret was moved.
+        // This is currently necessary for repaints.
+        fireTextChanged();
+        invalidateXOffset();
+        return true;
+    }
+    
+    protected String getLineSeparator() {
+       return System.getProperty("line.separator");
+    }
+
+    protected void selectAll() {
+        setCaret(0, false);
+        setCaret(text.length(), true);
+    }
+
+    protected transient int hoverClick = 0;
+
+    @Override
+    protected boolean mouseClicked(MouseClickEvent event) {
+        if (event.button != MouseClickEvent.LEFT_BUTTON)
+            return false;
+        
+        if (hasState(STATE_HOVER)) {
+               hoverClick++;
+               if (hoverClick < 2)
+                       return false;
+            ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
+            // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
+            if (ctx == null)
+                return false;
+            IElement e = DiagramNodeUtil.getElement(ctx, this);
+            if (!hasState(STATE_EDITING)) {
+               if (Boolean.TRUE.equals(setEditMode(true))) {
+                       editActivation = activateEdit(0, e, ctx);
+                       repaint();
+               }
+            } 
+        } else {
+               hoverClick = 0;
+            if (hasState(STATE_EDITING)) {
+                fireTextEditingEnded();
+            }
+        }
+        return false;
+    }
+    
+    protected boolean mouseDoubleClicked(MouseDoubleClickedEvent event) {
+        if (event.button != MouseClickEvent.LEFT_BUTTON)
+            return false;
+        
+        if (hitTest(event, 0)) {
+            ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
+            // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
+            if (ctx == null)
+                return false;
+            
+            if (text != null) {
+                // Select the whole text.
+                setCaret(0, false);
+                setCaret(text.length(), true);
+                repaint();
+            }
+        }
+        return false;
+    }
+
+
+    @Override
+    protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {
+        if (!hasState(STATE_EDITING))
+            return false;
+        
+        Point2D local = controlToLocal( event.controlPosition );
+        // FIXME: once the event coordinate systems are cleared up, remove this workaround
+        local = parentToLocal(local);
+        if (hasState(STATE_HOVER) && this.containsLocal(local)) {
+            setCaret(local, event.isShiftDown());
+        }
+        return false;
+    }
+
+    @Override
+    protected boolean mouseMoved(MouseMovedEvent event) {
+        boolean hit = hitTest(event, 3.0);
+        if (hit != hasState(STATE_HOVER)) {
+            setState(STATE_HOVER, hit);
+            repaint();
+        }
+        return false;
+    }
+
+    private boolean isControlDown(MouseEvent e) {
+       return e.isControlDown() || lastMouseEvent!=null?lastMouseEvent.isControlDown():false;
+    }
+
+    protected boolean isShiftDown(MouseEvent e) {
+       return e.isShiftDown() || lastMouseEvent!=null?lastMouseEvent.isShiftDown():false;
+    }
+
+//    private boolean isAltDown(MouseEvent e) {
+//     return e.isAltDown() || lastMouseEvent!=null?lastMouseEvent.isAltDown():false;
+//    }
+
+    @Override
+    protected boolean mouseDragged(MouseDragBegin e) {
+        if (isHovering()
+                && (isControlDown(e) || isShiftDown(e))
+                && (dataRVI != null || text != null))
+        {
+            List<Transferable> trs = new ArrayList<>(2);
+            if (dataRVI != null) {
+                trs.add(new LocalObjectTransferable(dataRVI));
+                trs.add(new PlaintextTransfer(dataRVI.toString()));
+            } else if (text != null && !text.isEmpty()) {
+                trs.add(new PlaintextTransfer(text));
+            }
+            if (!trs.isEmpty()) {
+                e.transferable = new MultiTransferable(trs);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    protected boolean hitTest(MouseEvent event, double tolerance) {
+        Rectangle2D bounds = getBoundsInternal();
+        if (bounds == null)
+            return false;
+        Point2D localPos = NodeUtil.worldToLocal(this, event.controlPosition, new Point2D.Double());
+        double x = localPos.getX();
+        double y = localPos.getY();
+        boolean hit = bounds.contains(x, y);
+        return hit;
+    }
+
+    public Rectangle2D getBoundsInternal() {
+        Rectangle2D local = lastBounds;
+        if (local == null)
+            return null;
+        // TODO: potential spot for CPU/memory allocation optimization
+        // by using more specialized implementations
+        if (transform.isIdentity())
+            return local;
+        return transform.createTransformedShape(local).getBounds2D();
+    }
+
+    protected Color add(Color c, int r, int g, int b)  {
+        int nr = Math.min(255, c.getRed() + r);
+        int ng = Math.min(255, c.getGreen() + g);
+        int nb = Math.min(255, c.getBlue() + b);
+        return new Color(nr, ng, nb);
+    }
+
+    public TextEditActivation activateEdit(int mouseId, IElement e, ICanvasContext ctx) {
+        EditDataNode data = EditDataNode.getNode(this);
+        deactivateEdit(data, null);
+        TextEditActivation result = new TextEditActivation(mouseId, e, ctx);
+        data.setTextEditActivation(result);
+        return result;
+    }
+
+    /**
+     * @return <code>true</code> if this node is or was previously in editing
+     *         state
+     */
+    protected boolean deactivateEdit() {
+        boolean result = deactivateEdit( editActivation );
+        result |= editActivation != null;
+        editActivation = null;
+        return result;
+    }
+
+    protected boolean deactivateEdit(TextEditActivation activation) {
+        return deactivateEdit( EditDataNode.getNode(this), activation );
+    }
+
+    protected boolean deactivateEdit(EditDataNode data, TextEditActivation activation) {
+        TextEditActivation previous = data.getTextEditActivation();
+        if (previous != null && (previous == activation || activation == null)) {
+            previous.release();
+            data.setTextEditActivation(null);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public int getEventMask() {
+        return EventTypes.KeyPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask
+                | EventTypes.MouseClickMask | EventTypes.MouseDragBeginMask | EventTypes.CommandMask;
+    }
+
+    private MouseEvent lastMouseEvent = null;
+    
+    @Override
+    public boolean handleEvent(Event e) {
+       if(e instanceof MouseEvent && !(e instanceof MouseDragBegin)) lastMouseEvent = (MouseEvent)e;
+        return super.handleEvent(e);
+    }
+
+       @Override
+       public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
+               return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
+       }
+       
+       @Override
+       public <T> T getProperty(String propertyName) {
+               return null;
+       }
+       
+       @Override
+       public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
+       }
+       
+       public void synchronizeText(String text) {
+               setText(text);
+       }
+
+       public void synchronizeColor(RGB.Integer color) {
+               this.color = Colors.awt(color);
+       }
+
+       public void synchronizeFont(org.simantics.datatypes.literal.Font font) {
+               setFont(Fonts.awt(font));
+       }
+
+       public void synchronizeTransform(double[] data) {
+               this.setTransform(new AffineTransform(data));
+       }
+
+       public static void main(String[] args) {
+               Line[] lines = parseLines("\n  \n FOO  \n\nBAR\n\n\n BAZ\n\n");
+               System.out.println(Arrays.toString(lines));
+               System.out.println(GeometryUtils.pointToMillimeter(1));
+               System.out.println(GeometryUtils.pointToMillimeter(12));
+               System.out.println(GeometryUtils.pointToMillimeter(72));
+       }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // LEGACY CODE NEEDED BY INHERITED CLASSES FOR NOW
+    ///////////////////////////////////////////////////////////////////////////
+
+    protected double getHorizontalAlignOffset(Rectangle2D r) {
+        switch (horizontalAlignment) {
+            case 0: return 0; // Leading
+            case 1: return -r.getWidth(); // Trailing
+            case 2: return -r.getCenterX(); // Center
+            default: return 0;
+        }
+    }
+
+    protected double getVerticalAlignOffset() {
+        FontMetrics fm = fontMetrics;
+        if (fm == null)
+            return 0;
+        switch (verticalAlignment) {
+            case 0: return fm.getMaxAscent(); // Leading=top=maxascent
+            case 1: return -fm.getMaxDescent(); // Trailing=bottom=maxdescent
+            case 2: return fm.getMaxAscent() / 2; // Center=maxascent / 2
+            case 3: return 0;
+            default: return 0;
+        }
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // LEGACY CODE ENDS
+    ///////////////////////////////////////////////////////////////////////////
+
+}