/*******************************************************************************
* 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 isRenderingPdf = writer != null;
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, isRenderingPdf);
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 (isRenderingPdf)
g.drawString(line.getText(), 0, 0);
else
line.layout.draw(g, 0, 0);
line.translateInv(g, xOffset, 0);
}
}
g.setClip(clip);
// Caret
renderCaret(g);
} else {
if (renderText) {
g.setColor(color);
renderText(g, 0, isRenderingPdf);
}
}
} 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, boolean isRenderingPdf) {
//g.draw(tightBoundsCache); // DEBUG
for (Line line : lines) {
// #6459: render as text in PDF and paths on screen
if (isRenderingPdf)
g.drawString(line.getText(), line.alignedPosX + xOffset, line.alignedPosY);
else
line.layout.draw(g, line.alignedPosX + xOffset, line.alignedPosY);
//g.draw(line.abbox); // DEBUG
}
}
protected Rectangle2D getScaledOffsetBounds(Rectangle2D originalBounds, Rectangle2D dst, double scale, double offsetX, double offsetY) {
AffineTransform btr = tempAffineTransform.get();
btr.setToTranslation(offsetX*scale, offsetY*scale);
btr.scale(scale, scale);
if (btr.isIdentity()) {
dst.setFrame(originalBounds);
} else {
dst.setFrame(btr.createTransformedShape(originalBounds).getBounds2D());
}
return dst;
}
/**
* Invoked when TextNode is selected and a mouse is hovering on top of it. Can be overridden to add custom rendering.
*
* @param g
*/
protected void renderSelectedHover(Graphics2D g, boolean isSelected, boolean isHovering) {
}
/**
* Replaces the current selection with the content or inserts
* the content at caret. After the insertion the caret
* will be at the end of inserted text and selection will
* be empty.
* @param content
*/
@SyncField({"text","caret","selectionTail"})
protected void insert(String content) {
content = editContentFilter != null ? editContentFilter.filter(this, content) : content;
int selectionMin = Math.min(caret, selectionTail);
int selectionMax = Math.max(caret, selectionTail);
String begin = text.substring(0, selectionMin);
String end = text.substring(selectionMax);
text = begin + content + end;
caret = selectionMin + content.length();
selectionTail = caret;
assert (caret <= text.length());
//System.out.println(text + " " + caret );
if(validator != null) {
String error = validator.apply(text);
setState(STATE_VALID, (error == null));
}
resetCaches();
}
@ServerSide
protected void fireTextChanged() {
if(textListener != null)
textListener.textChanged();
repaint();
}
@ServerSide
protected void fireTextEditingStarted() {
if(textListener != null)
textListener.textEditingStarted();
}
@ServerSide
protected void fireTextEditingCancelled() {
setState(STATE_VALID);
if (deactivateEdit()) {
if (textListener != null)
textListener.textEditingCancelled();
setEditMode(false, false);
if (textBeforeEdit != null)
setText(textBeforeEdit);
repaint();
}
}
@ServerSide
public void fireTextEditingEnded() {
if (!hasState(STATE_VALID)) {
fireTextEditingCancelled();
setState(STATE_VALID);
return;
}
if (deactivateEdit()) {
if (textListener != null)
textListener.textEditingEnded();
setEditMode(false, false);
repaint();
}
}
public void setTextListener(ITextListener listener) {
this.textListener = listener;
}
public void setValidator(Function1 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