-/*******************************************************************************\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ö <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.NodeEventHandler;
+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ö <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 = new String(text != null ? 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 != null ? text : "";
+ caret = Math.min(caret, this.text.length());
+ 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) {
+ }
+
+ /**
+ * 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();
+ 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))
+ && e.context instanceof NodeEventHandler
+ && (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
+ ///////////////////////////////////////////////////////////////////////////
+
+}