/******************************************************************************* * 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.io.IOException; 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.events.Event; import org.simantics.scenegraph.g2d.events.EventTypes; import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin; import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent; import org.simantics.scenegraph.g2d.events.NodeEventHandler; import org.simantics.scenegraph.g2d.events.command.CommandEvent; import org.simantics.scenegraph.g2d.events.command.Commands; import org.simantics.scenegraph.utils.GeometryUtils; import org.simantics.scenegraph.utils.NodeUtil; import org.simantics.scl.runtime.function.Function1; import org.simantics.scl.runtime.function.Function2; import org.simantics.ui.colors.Colors; import org.simantics.ui.dnd.LocalObjectTransferable; import org.simantics.ui.dnd.MultiTransferable; import org.simantics.ui.dnd.PlaintextTransfer; import org.simantics.ui.fonts.Fonts; import org.simantics.utils.threads.AWTThread; import com.lowagie.text.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; 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 * 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, edit); if (edit) { caret = text != null ? text.length() : 0; selectionTail = 0; textBeforeEdit = text; if (notify) fireTextEditingStarted(); return Boolean.TRUE; } else { if (notify) fireTextEditingEnded(); return Boolean.FALSE; } } @SyncField({"editable"}) public void setEditable(boolean editable) { boolean changed = hasState(STATE_EDITABLE) != editable; setState(STATE_EDITABLE, editable); if (hasState(STATE_EDITING) && !editable) setEditMode(false); if (changed && !hasState(STATE_ALWAYS_ADD_LISTENERS)) { setListeners(editable); } } public boolean isEditable() { return hasState(STATE_EDITABLE); } public boolean isEditMode() { return hasState(STATE_EDITING); } @SyncField({"wrapText"}) public void setWrapText(boolean wrapText) { setState(STATE_WRAP_TEXT, wrapText); } /** * @return Does the text box wrap text if * the width of the box is fixed */ public boolean isWrapText() { return hasState(STATE_WRAP_TEXT); } @SyncField({"showSelection"}) public void setShowSelection(boolean showSelection) { setState(STATE_SHOW_SELECTION, showSelection); } public boolean showsSelection() { return hasState(STATE_SHOW_SELECTION); } /** * @param text * @param font * @param color * @param x not supported anymore, use {@link #setTransform(AffineTransform)} instead * @param y not supported anymore, use {@link #setTransform(AffineTransform)} instead * @param scale */ @SyncField({"text", "font", "color", "x", "y", "scale"}) public void init(String text, Font font, Color color, double x, double y, double scale) { // no value => value if(this.text == null && text != null) NodeUtil.decreasePending(this); if (hasState(STATE_EDITING)) return; this.text = new String(text != null ? text : ""); this.font = font; this.color = color; this.scale = scale; this.scaleRecip = 1.0 / scale; this.caret = 0; this.selectionTail = 0; resetCaches(); } @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 || 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 /////////////////////////////////////////////////////////////////////////// }