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