1 /*******************************************************************************
\r
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
\r
3 * in Industry THTH ry.
\r
4 * All rights reserved. This program and the accompanying materials
\r
5 * are made available under the terms of the Eclipse Public License v1.0
\r
6 * which accompanies this distribution, and is available at
\r
7 * http://www.eclipse.org/legal/epl-v10.html
\r
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.diagram.elements;
\r
14 import java.awt.AlphaComposite;
\r
15 import java.awt.BasicStroke;
\r
16 import java.awt.Color;
\r
17 import java.awt.Composite;
\r
18 import java.awt.Font;
\r
19 import java.awt.FontMetrics;
\r
20 import java.awt.Graphics2D;
\r
21 import java.awt.RenderingHints;
\r
22 import java.awt.Shape;
\r
23 import java.awt.Toolkit;
\r
24 import java.awt.datatransfer.Clipboard;
\r
25 import java.awt.datatransfer.DataFlavor;
\r
26 import java.awt.datatransfer.StringSelection;
\r
27 import java.awt.datatransfer.Transferable;
\r
28 import java.awt.event.KeyEvent;
\r
29 import java.awt.font.FontRenderContext;
\r
30 import java.awt.font.LineBreakMeasurer;
\r
31 import java.awt.font.TextAttribute;
\r
32 import java.awt.font.TextHitInfo;
\r
33 import java.awt.font.TextLayout;
\r
34 import java.awt.geom.AffineTransform;
\r
35 import java.awt.geom.Point2D;
\r
36 import java.awt.geom.Rectangle2D;
\r
37 import java.io.IOException;
\r
38 import java.text.AttributedCharacterIterator;
\r
39 import java.text.AttributedString;
\r
40 import java.util.ArrayList;
\r
41 import java.util.Arrays;
\r
42 import java.util.Hashtable;
\r
43 import java.util.List;
\r
45 import org.simantics.datatypes.literal.RGB;
\r
46 import org.simantics.db.layer0.variable.RVI;
\r
47 import org.simantics.diagram.elements.Line.BoundsProcedure;
\r
48 import org.simantics.g2d.canvas.ICanvasContext;
\r
49 import org.simantics.g2d.element.IElement;
\r
50 import org.simantics.scenegraph.IDynamicSelectionPainterNode;
\r
51 import org.simantics.scenegraph.LoaderNode;
\r
52 import org.simantics.scenegraph.ScenegraphUtils;
\r
53 import org.simantics.scenegraph.g2d.G2DNode;
\r
54 import org.simantics.scenegraph.g2d.G2DPDFRenderingHints;
\r
55 import org.simantics.scenegraph.g2d.events.Event;
\r
56 import org.simantics.scenegraph.g2d.events.EventTypes;
\r
57 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
\r
58 import org.simantics.scenegraph.g2d.events.MouseEvent;
\r
59 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
\r
60 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;
\r
61 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent;
\r
62 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;
\r
63 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
\r
64 import org.simantics.scenegraph.g2d.events.NodeEventHandler;
\r
65 import org.simantics.scenegraph.g2d.events.command.CommandEvent;
\r
66 import org.simantics.scenegraph.g2d.events.command.Commands;
\r
67 import org.simantics.scenegraph.utils.GeometryUtils;
\r
68 import org.simantics.scenegraph.utils.NodeUtil;
\r
69 import org.simantics.scl.runtime.function.Function1;
\r
70 import org.simantics.scl.runtime.function.Function2;
\r
71 import org.simantics.ui.colors.Colors;
\r
72 import org.simantics.ui.dnd.LocalObjectTransferable;
\r
73 import org.simantics.ui.dnd.MultiTransferable;
\r
74 import org.simantics.ui.dnd.PlaintextTransfer;
\r
75 import org.simantics.ui.fonts.Fonts;
\r
76 import org.simantics.utils.threads.AWTThread;
\r
78 import com.lowagie.text.DocumentException;
\r
79 import com.lowagie.text.Element;
\r
80 import com.lowagie.text.Rectangle;
\r
81 import com.lowagie.text.pdf.FontMapper;
\r
82 import com.lowagie.text.pdf.PdfFormField;
\r
83 import com.lowagie.text.pdf.PdfWriter;
\r
84 import com.lowagie.text.pdf.TextField;
\r
86 import gnu.trove.list.array.TIntArrayList;
\r
90 * TextNode which supports in-line editing.
\r
92 * By default <code>TextNode</code> is in editable = false state. Use
\r
93 * {@link #setEditable(boolean)} to make it editable.
\r
95 * @author Hannu Niemistö <hannu.niemisto@vtt.fi>
\r
96 * @author Marko Luukkainen <marko.luukkainen@vtt.fi>
\r
97 * @author Tuukka Lehtonen <tuukka.lehtonen@semantum.fi>
\r
100 * o proper support for defining clipping bounds for the text (needed for page templates) (currently through fixedWidth)
\r
101 * o fix editing xOffset to work with fixed width and multi-line text
\r
107 public class TextNode extends G2DNode implements IDynamicSelectionPainterNode, LoaderNode {
\r
109 private static final long serialVersionUID = 654692698101485672L;
\r
112 * TODO: justify existence for this
\r
114 private static final BasicStroke RESET_STROKE = new BasicStroke(1);
\r
117 * Src-over alpha composite instance with 50% opacity.
\r
119 private static final AlphaComposite SrcOver_50 = AlphaComposite.SrcOver.derive(0.5f);
\r
122 * For (inexact) measurement of rendered text bounds.
\r
124 protected static final FontRenderContext FRC = new FontRenderContext(new AffineTransform(), true, true);
\r
126 private static final Font FONT = Font.decode("Arial 6");
\r
127 private static final Color SELECTION_BACKGROUND_COLOR = new Color(0x316ac5);
\r
128 // private static final double MAX_CARET_POSITION = 1.0;
\r
131 * The complete text visualized by this node.
\r
133 protected String text = null;
\r
136 * The font used to render the {@link #text}.
\r
138 protected Font font = FONT;
\r
141 * The color of the rendered text. Default value is {@value Color#black}.
\r
143 protected Color color = Color.BLACK;
\r
146 * The background color used for filling the background of the bounding box
\r
147 * of the rendered text. <code>null</code> means no fill.
\r
148 * Default value is <code>null</code>.
\r
150 protected Color backgroundColor = null;
\r
153 * The color used for drawing the expanded bounding box border for the
\r
154 * rendered text. <code>null</code> means no border is rendered. Default
\r
155 * value is <code>null</code>.
\r
157 protected Color borderColor = null;
\r
159 protected double scale = 1.0;
\r
160 protected transient double scaleRecip = 1.0;
\r
165 protected float borderWidth = 0.f;
\r
167 protected double paddingX = 2.0;
\r
168 protected double paddingY = 2.0;
\r
171 * Horizontal text box alignment with respect to its origin. Default value is
\r
174 protected byte horizontalAlignment = 0;
\r
176 * Vertical text box alignment with respect to its origin. Default value is
\r
179 protected byte verticalAlignment = 3;
\r
182 * Tells if this node is still pending for real results or not.
\r
184 protected static final int STATE_PENDING = (1 << 0);
\r
185 protected static final int STATE_HOVER = (1 << 1);
\r
186 protected static final int STATE_EDITABLE = (1 << 2);
\r
187 protected static final int STATE_SHOW_SELECTION = (1 << 3);
\r
188 protected static final int STATE_WRAP_TEXT = (1 << 4);
\r
189 protected transient static final int STATE_EDITING = (1 << 5);
\r
190 protected transient static final int STATE_VALID = (1 << 6);
\r
191 protected transient static final int STATE_X_OFFSET_IS_DIRTY = (1 << 7);
\r
192 protected static final int STATE_ALWAYS_ADD_LISTENERS = (1 << 8);
\r
193 protected static final int STATE_LISTENERS_ADDED = (1 << 9);
\r
196 * A combination of all the STATE_ constants defined in this class,
\r
197 * e.g. {@link #STATE_PENDING}.
\r
199 protected int state = STATE_SHOW_SELECTION | STATE_WRAP_TEXT | STATE_VALID | STATE_X_OFFSET_IS_DIRTY;
\r
201 protected RVI dataRVI = null;
\r
204 int selectionTail = 0;
\r
207 float fixedWidth = 0f;
\r
209 private Rectangle2D targetBounds;
\r
211 Function1<String, String> validator;
\r
212 ITextListener textListener;
\r
213 ITextContentFilter editContentFilter;
\r
216 * The renderable line structures parsed from {@link #text} by
\r
217 * {@link #parseLines(String)}, laid out by
\r
218 * {@link #layoutLines(Line[], FontRenderContext)} and aligned by
\r
219 * {@link #alignLines(Line[], Rectangle2D, byte, byte)}
\r
221 protected transient Line[] lines = null;
\r
222 protected transient FontMetrics fontMetrics = null;
\r
225 * Stores the value of {@link #text} before edit mode was last entered. Used
\r
226 * for restoring the original value if editing is cancelled.
\r
228 private transient String textBeforeEdit = null;
\r
229 protected transient TextEditActivation editActivation;
\r
232 * Stores the last scaled bounds.
\r
234 private transient Rectangle2D lastBounds = new Rectangle2D.Double();
\r
237 * This must be nullified if anything that affects the result of
\r
238 * {@link #getTightAlignedBoundsInLocal(Rectangle2D, FontRenderContext)}
\r
239 * changes. It will cause the cached value to be recalculated on the next
\r
242 private transient Rectangle2D tightBoundsCache = null;
\r
245 public void init() {
\r
247 // Mark this node as pending
\r
248 NodeUtil.increasePending(this);
\r
252 public void cleanup() {
\r
257 protected boolean hasState(int flags) {
\r
258 return (state & flags) == flags;
\r
261 protected void setState(int flags) {
\r
262 this.state |= flags;
\r
265 protected void setState(int flags, boolean set) {
\r
267 this.state |= flags;
\r
269 this.state &= ~flags;
\r
272 protected void clearState(int flags) {
\r
273 this.state &= ~flags;
\r
276 protected void setListeners(boolean add) {
\r
283 protected void addListeners() {
\r
284 if (!hasState(STATE_LISTENERS_ADDED)) {
\r
285 addEventHandler(this);
\r
286 setState(STATE_LISTENERS_ADDED);
\r
290 protected void removeListeners() {
\r
291 if (hasState(STATE_LISTENERS_ADDED)) {
\r
292 removeEventHandler(this);
\r
293 clearState(STATE_LISTENERS_ADDED);
\r
298 * Set to true to always enable event listening in this TextNode to allow the text node to keep track of hovering, etc. and to allow DnD even when
\r
301 public void setForceEventListening(boolean force) {
\r
302 setState(STATE_ALWAYS_ADD_LISTENERS, force);
\r
303 if (force && !hasState(STATE_EDITABLE)) {
\r
304 setListeners(force);
\r
309 * Enables or disables edit mode. It also sets
\r
310 * the caret at the end of text all selects the
\r
311 * whole text (this is the usual convention when
\r
312 * beginning to edit one line texts).
\r
314 * @return null if no change to edit state was made
\r
316 public Boolean setEditMode(boolean edit) {
\r
317 return setEditMode(edit, true);
\r
321 * Enables or disables edit mode. It also sets
\r
322 * the caret at the end of text all selects the
\r
323 * whole text (this is the usual convention when
\r
324 * beginning to edit one line texts).
\r
326 * @return null if no change to edit state was made
\r
328 protected Boolean setEditMode(boolean edit, boolean notify) {
\r
329 if (edit && !hasState(STATE_EDITABLE))
\r
331 if (hasState(STATE_EDITING) == edit)
\r
333 setState(STATE_EDITING, edit);
\r
335 caret = text != null ? text.length() : 0;
\r
337 textBeforeEdit = text;
\r
339 fireTextEditingStarted();
\r
340 return Boolean.TRUE;
\r
343 fireTextEditingEnded();
\r
344 return Boolean.FALSE;
\r
348 @SyncField({"editable"})
\r
349 public void setEditable(boolean editable) {
\r
350 boolean changed = hasState(STATE_EDITABLE) != editable;
\r
351 setState(STATE_EDITABLE, editable);
\r
352 if (hasState(STATE_EDITING) && !editable)
\r
353 setEditMode(false);
\r
354 if (changed && !hasState(STATE_ALWAYS_ADD_LISTENERS)) {
\r
355 setListeners(editable);
\r
359 public boolean isEditable() {
\r
360 return hasState(STATE_EDITABLE);
\r
363 public boolean isEditMode() {
\r
364 return hasState(STATE_EDITING);
\r
367 @SyncField({"wrapText"})
\r
368 public void setWrapText(boolean wrapText) {
\r
369 setState(STATE_WRAP_TEXT, wrapText);
\r
373 * @return Does the text box wrap text if
\r
374 * the width of the box is fixed
\r
376 public boolean isWrapText() {
\r
377 return hasState(STATE_WRAP_TEXT);
\r
380 @SyncField({"showSelection"})
\r
381 public void setShowSelection(boolean showSelection) {
\r
382 setState(STATE_SHOW_SELECTION, showSelection);
\r
385 public boolean showsSelection() {
\r
386 return hasState(STATE_SHOW_SELECTION);
\r
393 * @param x not supported anymore, use {@link #setTransform(AffineTransform)} instead
\r
394 * @param y not supported anymore, use {@link #setTransform(AffineTransform)} instead
\r
397 @SyncField({"text", "font", "color", "x", "y", "scale"})
\r
398 public void init(String text, Font font, Color color, double x, double y, double scale) {
\r
399 // no value => value
\r
400 if(this.text == null && text != null) NodeUtil.decreasePending(this);
\r
402 if (hasState(STATE_EDITING))
\r
405 this.text = new String(text != null ? text : "");
\r
407 this.color = color;
\r
408 this.scale = scale;
\r
409 this.scaleRecip = 1.0 / scale;
\r
411 this.selectionTail = 0;
\r
416 @SyncField({"paddingX", "paddingY"})
\r
417 public void setPadding(double x, double y) {
\r
422 @SyncField({"color"})
\r
423 public void setColor(Color color) {
\r
424 this.color = color;
\r
427 @SyncField({"backgroundColor"})
\r
428 public void setBackgroundColor(Color color) {
\r
429 this.backgroundColor = color;
\r
432 @SyncField({"borderColor"})
\r
433 public void setBorderColor(Color color) {
\r
434 this.borderColor = color;
\r
437 public String getText() {
\r
441 public String getTextBeforeEdit() {
\r
442 return textBeforeEdit;
\r
445 @SyncField({"text","caret","selectionTail"})
\r
446 public void setText(String text) {
\r
447 //System.out.println("TextNode.setText('" + text + "', " + editing + ")");
\r
448 if (hasState(STATE_EDITING))
\r
451 // value => no value
\r
452 if(this.text != null && text == null) NodeUtil.increasePending(this);
\r
453 // no value => value
\r
454 if(this.text == null && text != null) NodeUtil.decreasePending(this);
\r
456 this.text = text != null ? text : "";
\r
457 caret = Math.min(caret, this.text.length());
\r
458 selectionTail = caret;
\r
463 @SyncField({"pending"})
\r
464 public void setPending(boolean pending) {
\r
465 boolean p = hasState(STATE_PENDING);
\r
466 if(!p && pending) NodeUtil.increasePending(this);
\r
467 if(p && !pending) NodeUtil.decreasePending(this);
\r
469 setState(STATE_PENDING, pending);
\r
472 @SyncField({"fixedWidth"})
\r
473 public void setFixedWidth(float fixedWidth) {
\r
474 if (fixedWidth < 0f)
\r
475 throw new IllegalArgumentException("negative fixed width");
\r
476 this.fixedWidth = fixedWidth;
\r
477 invalidateXOffset();
\r
481 * Bounds where the text box will be drawn
\r
484 public void setTargetBounds(Rectangle2D bounds) {
\r
485 this.targetBounds = bounds;
\r
488 final public void synchronizeWidth(float width) {
\r
490 setFixedWidth(width);
\r
493 final public void synchronizeBorderWidth(float width) {
\r
495 setBorderWidth(width);
\r
498 public final void synchronizeWrapText(boolean wrap) {
\r
499 setState(STATE_WRAP_TEXT, wrap);
\r
502 public boolean isHovering() {
\r
503 return hasState(STATE_HOVER);
\r
506 @SyncField({"hover"})
\r
507 public void setHover(boolean hover) {
\r
508 setState(STATE_HOVER, hover);
\r
512 public Font getFont() {
\r
516 @SyncField({"font"})
\r
517 public void setFont(Font font) {
\r
522 public double getBorderWidth() {
\r
523 return borderWidth;
\r
526 @SyncField({"borderWidth"})
\r
527 public void setBorderWidth(float width) {
\r
528 this.borderWidth = width;
\r
531 public void setBorderWidth(double width) {
\r
532 setBorderWidth((float)width);
\r
535 @SyncField({"horizontalAlignment"})
\r
536 public void setHorizontalAlignment(byte horizontalAlignment) {
\r
537 if (horizontalAlignment < 0 && horizontalAlignment > 2)
\r
538 throw new IllegalArgumentException("Invalid horizontal alignment: " + horizontalAlignment + ", must be between 0 and 2");
\r
539 this.horizontalAlignment = horizontalAlignment;
\r
543 final public void synchronizeHorizontalAlignment(byte horizontalAlignment) {
\r
544 if (horizontalAlignment >= 0 && horizontalAlignment <= 2)
\r
545 setHorizontalAlignment(horizontalAlignment);
\r
548 public byte getHorizontalAlignment() {
\r
549 return horizontalAlignment;
\r
552 @SyncField({"verticalAlignment"})
\r
553 public void setVerticalAlignment(byte verticalAlignment) {
\r
554 if (verticalAlignment < 0 && verticalAlignment > 3)
\r
555 throw new IllegalArgumentException("Invalid vertical alignment: " + verticalAlignment + ", must be between 0 and 3");
\r
556 this.verticalAlignment = verticalAlignment;
\r
560 final public void synchronizeVerticalAlignment(byte verticalAlignment) {
\r
561 if (verticalAlignment >= 0 && verticalAlignment <= 3)
\r
562 setVerticalAlignment(verticalAlignment);
\r
565 public byte getVerticalAlignment() {
\r
566 return verticalAlignment;
\r
570 * Rendering is single-threaded so we can use a static rectangle for
\r
571 * calculating the expanded bounds for the node.
\r
573 private static transient ThreadLocal<Rectangle2D> tempBounds = new ThreadLocal<Rectangle2D>() {
\r
575 protected Rectangle2D initialValue() {
\r
576 return new Rectangle2D.Double();
\r
581 * Rendering is single-threaded so we can use a static AffineTransform to
\r
582 * prevent continuous memory allocation during text rendering.
\r
584 private static transient ThreadLocal<AffineTransform> tempAffineTransform = new ThreadLocal<AffineTransform>() {
\r
586 protected AffineTransform initialValue() {
\r
587 return new AffineTransform();
\r
592 public void render(Graphics2D g) {
\r
593 AffineTransform ot = g.getTransform();
\r
595 g.setTransform(ot);
\r
599 * Note: does not return transformation, stroke, color, etc. to their
\r
603 * @param applyTransform
\r
605 public void render(Graphics2D g, boolean applyTransform) {
\r
606 if (text == null || font == null || color == null)
\r
609 // Cache font metrics if necessary
\r
610 if (fontMetrics == null)
\r
611 fontMetrics = g.getFontMetrics(font);
\r
613 Color color = this.color;
\r
614 boolean isSelected = NodeUtil.isSelected(this, 1);
\r
615 boolean hover = hasState(STATE_HOVER);
\r
616 boolean editing = hasState(STATE_EDITING);
\r
618 if (!isSelected && hover) {
\r
619 color = add(color, 120, 120, 120);
\r
622 if (applyTransform)
\r
623 g.transform(transform);
\r
624 // Apply separate legacy scale
\r
626 g.scale(scale, scale);
\r
628 // Safety for not rendering when the scale of this text is too small.
\r
629 // When the scale is too small it will cause internal exceptions while
\r
631 double currentScale = GeometryUtils.getScale(g.getTransform());
\r
632 //System.out.println("currentScale: " + currentScale);
\r
633 if (currentScale < 1e-6)
\r
637 //g.translate(x, y);
\r
639 // Calculate text clip rectangle.
\r
640 // This updates textLayout if necessary.
\r
641 Rectangle2D r = getTightAlignedBoundsInLocal(tempBounds.get(), fontMetrics.getFontRenderContext());
\r
643 computeEditingXOffset();
\r
645 if (fixedWidth > 0f)
\r
646 r.setFrame(r.getMinX(), r.getMinY(), fixedWidth, r.getHeight());
\r
647 if(targetBounds != null) {
\r
648 double w = (targetBounds.getWidth() - paddingX * 2) * scaleRecip;
\r
649 double h = (targetBounds.getHeight() - paddingY * 2) * scaleRecip;
\r
650 double x = (targetBounds.getMinX() + paddingX) * scaleRecip;
\r
651 double y = (targetBounds.getMinY() + paddingY) * scaleRecip;
\r
652 r.setRect(x, y, w, h);
\r
655 Rectangle2D textClip = r.getBounds2D();
\r
657 expandBoundsUnscaled(r);
\r
659 // Speed rendering optimization: don't draw text that is too small to
\r
660 // read when not editing
\r
661 boolean renderText = true;
\r
663 Object renderingHint = g.getRenderingHint(RenderingHints.KEY_RENDERING);
\r
664 if (renderingHint != RenderingHints.VALUE_RENDER_QUALITY) {
\r
665 float textSizeMM = (float) currentScale * GeometryUtils.pointToMillimeter(font.getSize2D());
\r
666 if (textSizeMM < 1.5f)
\r
667 renderText = false;
\r
671 Shape clipSave = g.getClip();
\r
672 g.setClip(textClip);
\r
675 PdfWriter writer = (PdfWriter) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_WRITER);
\r
676 boolean isRenderingPdf = writer != null;
\r
677 boolean isPdfField = false;
\r
678 String fieldName = null;
\r
679 if (writer != null) {
\r
680 // TODO: replace this hack with proper text field name field
\r
681 fieldName = NodeUtil.getNodeName(this);
\r
682 isPdfField = ( fieldName.equals("approved_by") ||
\r
683 fieldName.equals("checked_by") ||
\r
684 fieldName.equals("designer name") ||
\r
685 fieldName.equals("created_by") );
\r
688 Color backgroundColor = hasState(STATE_VALID) ? this.backgroundColor : Color.red;
\r
691 if ( !isPdfField ) {
\r
693 // Fill background if necessary
\r
694 if (backgroundColor != null) {
\r
695 g.setColor(backgroundColor);
\r
701 int selectionMin = Math.min(caret, selectionTail);
\r
702 int selectionMax = Math.max(caret, selectionTail);
\r
706 renderText(g, xOffset, isRenderingPdf);
\r
708 Shape clip = g.getClip();
\r
710 // Selection background & text
\r
711 for (Line line : lines) {
\r
712 if (line.intersectsRange(selectionMin, selectionMax)) {
\r
713 Shape selShape = line.getLogicalHighlightShape(selectionMin, selectionMax);
\r
714 line.translate(g, xOffset, 0);
\r
715 g.setClip(selShape);
\r
716 g.setColor(SELECTION_BACKGROUND_COLOR);
\r
718 g.setColor(Color.WHITE);
\r
719 // #6459: render as text in PDF and paths on screen
\r
720 if (isRenderingPdf)
\r
721 g.drawString(line.getText(), 0, 0);
\r
723 line.layout.draw(g, 0, 0);
\r
724 line.translateInv(g, xOffset, 0);
\r
739 renderText(g, 0, isRenderingPdf);
\r
745 // TODO: multiline support
\r
747 AffineTransform at = g.getTransform();
\r
748 float height = writer.getPageSize().getHeight();
\r
749 Rectangle2D rr = textClip;
\r
750 // Point2D pt1 = new Point2D.Double(rr.getX(), rr.getY()+rr.getHeight());
\r
751 // Point2D pt2 = new Point2D.Double(rr.getX()+rr.getWidth(), rr.getY());
\r
752 Point2D pt1 = new Point2D.Double(0, 0);
\r
753 Point2D pt2 = new Point2D.Double(47.f/*+rr.getWidth()*/, -rr.getHeight());
\r
754 pt1 = at.transform(pt1, pt1);
\r
755 pt2 = at.transform(pt2, pt2);
\r
756 Rectangle rectangle = new Rectangle(
\r
757 (float) pt1.getX(),
\r
758 height-(float) pt1.getY(),
\r
759 (float) pt2.getX(),
\r
760 height-(float) pt2.getY());
\r
762 FontMapper mapper = (FontMapper) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_FONTMAPPER);
\r
763 // FontMetrics fm = g.getFontMetrics(font);
\r
765 // TODO Oikea leveys
\r
766 // TODO Uniikki nimi
\r
768 PdfFormField field = PdfFormField.createTextField(writer, false, false, 20);
\r
769 field.setFieldName(this.getId().toString());
\r
770 field.setWidget(rectangle, PdfAnnotation.HIGHLIGHT_NONE);
\r
771 field.setQuadding(PdfFormField.Q_RIGHT);
\r
772 field.setFieldFlags(PdfFormField.FF_READ_ONLY);
\r
773 field.setRotate(90);
\r
774 writer.addAnnotation(field);
\r
781 PdfFormField field = PdfFormField.createSignature(writer);
\r
782 field.setWidget(rectangle, PdfAnnotation.HIGHLIGHT_NONE);
\r
783 field.setFieldName(fieldName);
\r
784 field.setQuadding(PdfFormField.Q_LEFT);
\r
785 field.setFlags(PdfAnnotation.FLAGS_PRINT);
\r
786 //field.setFieldFlags(PdfFormField.FF_READ_ONLY)
\r
787 field.setFieldFlags(PdfFormField.FF_EDIT);
\r
789 field.setMKBackgroundColor( backgroundColor!=null?Color.WHITE:backgroundColor );
\r
790 PdfAppearance tp = PdfAppearance.createAppearance(writer, 72, 48);
\r
791 tp.rectangle(rectangle);
\r
793 field.setAppearance(PdfAnnotation.APPEARANCE_NORMAL, tp);
\r
794 writer.addAnnotation(field);
\r
799 TextField textField = new TextField(writer, rectangle, fieldName);
\r
800 textField.setFieldName(fieldName);
\r
801 textField.setFont(mapper.awtToPdf(font));
\r
802 textField.setBorderStyle(0);
\r
803 //textField.setAlignment(Element.ALIGN_LEFT);
\r
804 textField.setAlignment(Element.ALIGN_BOTTOM);
\r
805 textField.setRotation(90);
\r
806 textField.setOptions(TextField.EDIT|TextField.DO_NOT_SPELL_CHECK);
\r
807 if ( text!=null ) {
\r
808 textField.setText(text);
\r
810 if ( color!=null ) {
\r
811 textField.setTextColor(color);
\r
813 textField.setBackgroundColor( backgroundColor!=null?Color.WHITE:backgroundColor );
\r
814 PdfFormField field = textField.getTextField();
\r
815 writer.addAnnotation(field);
\r
816 } catch (IOException e) {
\r
817 e.printStackTrace();
\r
818 } catch (DocumentException e) {
\r
819 e.printStackTrace();
\r
823 // } catch (IOException e) {
\r
824 // // TODO Auto-generated catch block
\r
825 // e.printStackTrace();
\r
826 // } catch (DocumentException e) {
\r
827 // // TODO Auto-generated catch block
\r
828 // e.printStackTrace();
\r
833 g.setClip(clipSave);
\r
835 if (borderWidth > 0f && borderColor != null) {
\r
836 g.setColor(borderColor);
\r
837 g.setStroke(new BasicStroke((float) (scale*borderWidth)));
\r
841 //System.out.println("bw: " + borderWidth);
\r
842 if (isSelected && showsSelection()) {
\r
843 Composite oc = g.getComposite();
\r
844 g.setComposite(SrcOver_50);
\r
845 g.setColor(Color.RED);
\r
846 float bw = borderWidth;
\r
847 double s = currentScale;
\r
849 bw = (float) (1f / s);
\r
853 g.setStroke(new BasicStroke(bw));
\r
855 //g.draw(GeometryUtils.expandRectangle(r, 1.0));
\r
857 g.setComposite(oc);
\r
860 g.scale(scaleRecip, scaleRecip);
\r
861 g.setStroke(RESET_STROKE);
\r
863 lastBounds = getScaledOffsetBounds(r, lastBounds, scale, 0, 0);
\r
864 // g.setColor(Color.MAGENTA); // DEBUG
\r
865 // g.draw(lastBounds); // DEBUG
\r
866 // g.setColor(Color.ORANGE); // DEBUG
\r
867 // g.draw(getBoundsInLocal()); // DEBUG
\r
869 renderSelectedHover(g, isSelected, hover);
\r
872 private void renderCaret(Graphics2D g) {
\r
873 g.setColor(Color.BLACK);
\r
874 for (int i = 0; i < lines.length; i++) {
\r
875 Line line = lines[i];
\r
876 // prevent rendering caret twice on line changes
\r
877 if (line.containsOffset(caret) && // line contains caret
\r
878 (caret != line.endOffset || //caret is not in the end of the line
\r
879 i == lines.length-1 || //caret is end of the last line
\r
880 lines[i+1].startOffset != line.endOffset)) { // beginning of the next line does not start withe the same index as current line
\r
881 Shape[] caretShape = line.getCaretShapes(caret);
\r
882 line.translate(g, xOffset, 0);
\r
883 g.draw(caretShape[0]);
\r
884 if (caretShape[1] != null)
\r
885 g.draw(caretShape[1]);
\r
886 line.translateInv(g, xOffset, 0);
\r
890 private void renderText(Graphics2D g, float xOffset, boolean isRenderingPdf) {
\r
891 //g.draw(tightBoundsCache); // DEBUG
\r
892 for (Line line : lines) {
\r
893 // #6459: render as text in PDF and paths on screen
\r
894 if (isRenderingPdf)
\r
895 g.drawString(line.getText(), line.alignedPosX + xOffset, line.alignedPosY);
\r
897 line.layout.draw(g, line.alignedPosX + xOffset, line.alignedPosY);
\r
898 //g.draw(line.abbox); // DEBUG
\r
902 protected Rectangle2D getScaledOffsetBounds(Rectangle2D originalBounds, Rectangle2D dst, double scale, double offsetX, double offsetY) {
\r
903 AffineTransform btr = tempAffineTransform.get();
\r
904 btr.setToTranslation(offsetX*scale, offsetY*scale);
\r
905 btr.scale(scale, scale);
\r
906 if (btr.isIdentity()) {
\r
907 dst.setFrame(originalBounds);
\r
909 dst.setFrame(btr.createTransformedShape(originalBounds).getBounds2D());
\r
915 * Invoked when TextNode is selected and a mouse is hovering on top of it. Can be overridden to add custom rendering.
\r
919 protected void renderSelectedHover(Graphics2D g, boolean isSelected, boolean isHovering) {
\r
923 * Replaces the current selection with the content or inserts
\r
924 * the content at caret. After the insertion the caret
\r
925 * will be at the end of inserted text and selection will
\r
929 @SyncField({"text","caret","selectionTail"})
\r
930 protected void insert(String content) {
\r
931 content = editContentFilter != null ? editContentFilter.filter(this, content) : content;
\r
933 int selectionMin = Math.min(caret, selectionTail);
\r
934 int selectionMax = Math.max(caret, selectionTail);
\r
936 String begin = text.substring(0, selectionMin);
\r
937 String end = text.substring(selectionMax);
\r
938 text = begin + content + end;
\r
939 caret = selectionMin + content.length();
\r
940 selectionTail = caret;
\r
942 assert (caret <= text.length());
\r
943 //System.out.println(text + " " + caret );
\r
945 if(validator != null) {
\r
946 String error = validator.apply(text);
\r
947 setState(STATE_VALID, (error == null));
\r
954 protected void fireTextChanged() {
\r
955 if(textListener != null)
\r
956 textListener.textChanged();
\r
961 protected void fireTextEditingStarted() {
\r
962 if(textListener != null)
\r
963 textListener.textEditingStarted();
\r
967 protected void fireTextEditingCancelled() {
\r
968 setState(STATE_VALID);
\r
970 if (deactivateEdit()) {
\r
971 if (textListener != null)
\r
972 textListener.textEditingCancelled();
\r
974 setEditMode(false, false);
\r
976 if (textBeforeEdit != null)
\r
977 setText(textBeforeEdit);
\r
984 public void fireTextEditingEnded() {
\r
985 if (!hasState(STATE_VALID)) {
\r
986 fireTextEditingCancelled();
\r
987 setState(STATE_VALID);
\r
991 if (deactivateEdit()) {
\r
992 if (textListener != null)
\r
993 textListener.textEditingEnded();
\r
995 setEditMode(false, false);
\r
1000 public void setTextListener(ITextListener listener) {
\r
1001 this.textListener = listener;
\r
1004 public void setValidator(Function1<String, String> validator) {
\r
1005 this.validator = validator;
\r
1008 public void setContentFilter(ITextContentFilter filter) {
\r
1009 this.editContentFilter = filter;
\r
1012 public void setRVI(RVI rvi) {
\r
1013 this.dataRVI = rvi;
\r
1016 private void invalidateXOffset() {
\r
1017 setState(STATE_X_OFFSET_IS_DIRTY);
\r
1020 private void computeEditingXOffset() {
\r
1022 if(lines == null) return;
\r
1023 if(!hasState(STATE_X_OFFSET_IS_DIRTY)) return;
\r
1024 if(fixedWidth > 0f) {
\r
1026 // TODO: implement
\r
1027 // float[] coords = textLayout.getCaretInfo(TextHitInfo.afterOffset(caret));
\r
1028 // if(coords != null) {
\r
1029 // if(coords[0] > (fixedWidth*MAX_CARET_POSITION)) xOffset = (float)((fixedWidth*MAX_CARET_POSITION)-coords[0]);
\r
1030 // else xOffset = 0;
\r
1039 clearState(STATE_X_OFFSET_IS_DIRTY);
\r
1043 @SyncField({"caret","selectionTail"})
\r
1044 protected void moveCaret(int move, boolean select) {
\r
1045 // prevent setting caret into line separator.
\r
1047 while (text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
\r
1049 } else if (move < 0) {
\r
1050 while (caret+move >= 0 && text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
\r
1056 if (caret > text.length())
\r
1057 caret = text.length();
\r
1059 selectionTail = caret;
\r
1062 private Line findCaretLine() {
\r
1063 // Find the line where caret is. Starting from first line.
\r
1064 for(int i = 0; i < lines.length; i++) {
\r
1065 Line line = lines[i];
\r
1066 if(caret <= line.endOffset) {
\r
1074 * Moves caret to next not letter or digit
\r
1075 * @param shiftDown
\r
1077 private void moveCaretCtrlLeft(boolean shiftDown) {
\r
1078 Line line = findCaretLine();
\r
1079 if(line != null) {
\r
1081 for(i = caret-1; i > line.startOffset; i--) {
\r
1082 char c = line.document.charAt(i);
\r
1083 if(!Character.isLetterOrDigit(c)) {
\r
1087 moveCaret(i - caret, shiftDown);
\r
1092 * Moves caret to previous non letter or digit
\r
1093 * @param shiftDown
\r
1095 private void moveCaretCtrlRight(boolean shiftDown) {
\r
1096 Line line = findCaretLine();
\r
1097 if(line != null) {
\r
1099 for(i = caret + 1; i < line.endOffset; i++) {
\r
1100 char c = line.document.charAt(i);
\r
1101 if(!Character.isLetterOrDigit(c)) {
\r
1105 moveCaret(i - caret, shiftDown);
\r
1110 * Moves caret to line end
\r
1111 * @param shiftDown
\r
1113 private void moveCaretEnd(boolean shiftDown) {
\r
1114 Line line = findCaretLine();
\r
1116 // Move caret to the end of the line
\r
1117 moveCaret(line.endOffset - caret, shiftDown);
\r
1121 * Moves caret to beginning of a line
\r
1122 * @param shiftDown
\r
1124 private void moveCaretHome(boolean shiftDown) {
\r
1125 Line line = findCaretLine();
\r
1127 // Move caret to the beginning of the line
\r
1128 moveCaret(line.startOffset - caret, shiftDown);
\r
1132 * Moves caret one row up and tries to maintain the location
\r
1133 * @param shiftDown
\r
1135 private void moveCaretRowUp(boolean shiftDown) {
\r
1136 // Find the line where caret is. Starting from first line.
\r
1137 for(int i = 0; i < lines.length; i++) {
\r
1138 Line line = lines[i];
\r
1139 if(caret <= line.endOffset) {
\r
1140 // caret is in this line
\r
1142 // Already on top line
\r
1143 // Select the beginning of the line
\r
1144 moveCaret(-caret, shiftDown);
\r
1146 Line prevLine = lines[i-1];
\r
1147 int prevLength = prevLine.endOffset - prevLine.startOffset;
\r
1148 int posInCurRow = caret - line.startOffset;
\r
1149 if(prevLength < posInCurRow)
\r
1150 posInCurRow = prevLength;
\r
1152 int newPos = prevLine.startOffset + posInCurRow;
\r
1153 moveCaret(newPos - caret, shiftDown);
\r
1161 * Moves caret one row down and tries to maintain the location
\r
1162 * @param shiftDown
\r
1164 private void moveCaretRowDown(boolean shiftDown) {
\r
1165 // Find the line where caret is. Starting from last line.
\r
1166 for(int i = lines.length - 1; i >= 0; i--) {
\r
1167 Line line = lines[i];
\r
1168 if(caret >= line.startOffset) {
\r
1169 // caret is in this line
\r
1170 if(i == lines.length - 1) {
\r
1171 // Already on bottom line, cannot go below
\r
1172 // Select to the end of the line
\r
1173 moveCaret(line.endOffset - caret, shiftDown);
\r
1175 Line prevLine = lines[i+1]; // Previous line
\r
1177 // Find new caret position.
\r
1178 // Either it is in the same index as before, or if the row
\r
1179 // is not long enough, select the end of the row.
\r
1180 int prevLength = prevLine.endOffset - prevLine.startOffset;
\r
1181 int posInCurRow = caret - line.startOffset;
\r
1182 if(prevLength < posInCurRow)
\r
1183 posInCurRow = prevLength;
\r
1184 int newPos = prevLine.startOffset + posInCurRow;
\r
1185 moveCaret(newPos - caret, shiftDown);
\r
1192 @SyncField({"caret","selectionTail"})
\r
1193 protected void setCaret(int pos, boolean select) {
\r
1197 if (caret > text.length())
\r
1198 caret = text.length();
\r
1200 selectionTail = caret;
\r
1203 protected void setCaret(Point2D point) {
\r
1204 setCaret(point, false);
\r
1207 @SyncField({"caret","selectionTail"})
\r
1208 protected void setCaret(Point2D point, boolean select) {
\r
1210 for(int i = 0; i < lines.length; i++) {
\r
1211 Line line = lines[i];
\r
1212 Rectangle2D bounds = line.abbox;
\r
1213 // Add heights of bboxes for determining the correct line
\r
1215 lineY = bounds.getY();
\r
1217 lineY += lines[i-1].abbox.getHeight();
\r
1219 double lineHeight = bounds.getHeight();
\r
1220 double hitY = point.getY() / scale;
\r
1221 if(hitY >= lineY && hitY <= lineY + lineHeight) {
\r
1222 // Hit is in this line
\r
1223 float x = (float)(point.getX() / scale) - (float)line.abbox.getX();
\r
1224 float y = (float)(point.getY() / scale - lineHeight * i) ;
\r
1225 TextHitInfo info = line.layout.hitTestChar(x, y);
\r
1226 caret = line.startOffset + info.getInsertionIndex();
\r
1227 if (caret > line.endOffset)
\r
1228 caret = line.endOffset;
\r
1230 selectionTail = caret;
\r
1235 invalidateXOffset();
\r
1236 assert (caret <= text.length());
\r
1240 public Rectangle2D getBoundsInLocal() {
\r
1241 if(targetBounds != null)
\r
1242 return targetBounds;
\r
1244 return expandBounds( getTightAlignedBoundsInLocal(null) );
\r
1247 protected Rectangle2D expandBounds(Rectangle2D r) {
\r
1248 r.setRect(r.getX() * scale - paddingX, r.getY() * scale -paddingY, r.getWidth()*scale + paddingX + paddingX, r.getHeight()*scale + paddingY + paddingY);
\r
1249 //System.out.println(" => " + r);
\r
1253 protected Rectangle2D expandBoundsUnscaled(Rectangle2D r) {
\r
1254 r.setRect(r.getX() - scaleRecip*paddingX, r.getY() -scaleRecip*paddingY, r.getWidth() + scaleRecip*paddingX + scaleRecip*paddingX, r.getHeight() + scaleRecip*paddingY + scaleRecip*paddingY);
\r
1255 //System.out.println(" => " + r);
\r
1259 protected Rectangle2D expandBounds(Rectangle2D r, double amount) {
\r
1260 r.setRect(r.getX() - amount, r.getY() - amount, r.getWidth() + 2*amount, r.getHeight() + 2*amount);
\r
1264 protected Rectangle2D expandBounds(Rectangle2D r, double left, double top, double right, double bottom) {
\r
1265 r.setRect(r.getX() - left, r.getY() - top, r.getWidth() + left + right, r.getHeight() + top + bottom);
\r
1269 private void resetCaches() {
\r
1270 this.tightBoundsCache = null;
\r
1271 this.lines = null;
\r
1272 this.fontMetrics = null;
\r
1276 * Returns the tight bounds around the current text using the current font
\r
1277 * in the specified rectangle. If the specified rectangle is
\r
1278 * <code>null</code> a new Rectangle2D.Double instance will be created.
\r
1283 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r) {
\r
1284 return getTightAlignedBoundsInLocal(r, FRC);
\r
1288 * Returns the tight bounds around the current text using the current font
\r
1289 * in the specified rectangle. If the specified rectangle is
\r
1290 * <code>null</code> a new Rectangle2D.Double instance will be created.
\r
1293 * the rectangle where the result of the method is placed or
\r
1294 * <code>null</code> to allocate new rectangle
\r
1295 * @param frc current font render context
\r
1296 * @return r or new Rectangle2D.Double instance containing the requested
\r
1299 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r, FontRenderContext frc) {
\r
1301 r = new Rectangle2D.Double();
\r
1303 if (tightBoundsCache != null) {
\r
1304 r.setFrame(tightBoundsCache);
\r
1308 String txt = text;
\r
1309 if (font == null || txt == null) {
\r
1310 r.setFrame(0, 0, 2, 1);
\r
1314 //System.out.println("TextNode.getTightAlignedBoundsInLocal('" + txt + "')");
\r
1316 // Parse & layout (unaligned)
\r
1317 Line[] lines = null;
\r
1319 if(hasState(STATE_WRAP_TEXT)) {
\r
1320 float width = fixedWidth;
\r
1321 if(width <= 0 && targetBounds != null)
\r
1322 width = (float) (((targetBounds.getWidth() - 2*paddingX)) * scaleRecip);
\r
1324 lines = wrapLines(txt, font, width, frc);
\r
1328 lines = parseLines(txt);
\r
1329 this.lines = layoutLines(lines, frc);
\r
1331 // Calculate tight bounds based on unaligned layout
\r
1332 //System.out.println("Unaligned");
\r
1333 tightBoundsCache = calculateBounds(lines, Line.BBOX, null);
\r
1334 //System.out.println(" => " + tightBoundsCache);
\r
1336 this.lines = layoutLinesX(lines, tightBoundsCache);
\r
1337 // Align each line to the calculated tight bounds
\r
1338 this.lines = alignLines(this.lines, tightBoundsCache, horizontalAlignment, verticalAlignment);
\r
1340 // Calculate aligned bounds
\r
1341 //System.out.println("Aligned");
\r
1342 calculateBounds(lines, Line.ABBOX, tightBoundsCache);
\r
1344 r.setFrame(tightBoundsCache);
\r
1345 //System.out.println(" => " + tightBoundsCache);
\r
1353 * the bounding box of all the whole laid out text (only bbox
\r
1357 private Line[] layoutLinesX(Line[] lines, Rectangle2D bbox) {
\r
1358 int lineCount = lines.length;
\r
1359 for (int l = 0; l < lineCount; ++l) {
\r
1360 Line line = lines[l];
\r
1361 // Compute pen x position. If the paragraph is right-to-left we
\r
1362 // will align the TextLayouts to the right edge of the panel.
\r
1363 // Note: drawPosX is always where the LEFT of the text is placed.
\r
1364 // NOTE: This changes based on horizontal alignment
\r
1365 line.drawPosX = (float) (line.layout.isLeftToRight() ? 0f
\r
1366 : tightBoundsCache.getWidth() - line.layout.getAdvance());
\r
1373 * @param boundsProvider
\r
1377 private static Rectangle2D calculateBounds(Line[] lines, BoundsProcedure boundsProvider, Rectangle2D result) {
\r
1378 if (result == null)
\r
1379 result = new Rectangle2D.Double();
\r
1381 result.setFrame(0, 0, 0, 0);
\r
1383 for (Line line : lines) {
\r
1384 //System.out.println("line: " + line);
\r
1385 Rectangle2D bbox = boundsProvider.getBounds(line);
\r
1386 if (result.isEmpty())
\r
1387 result.setFrame(bbox);
\r
1389 Rectangle2D.union(result, bbox, result);
\r
1390 //System.out.println("bounds: " + result);
\r
1392 //System.out.println("final bounds: " + result);
\r
1402 * @return aligned lines
\r
1404 private Line[] alignLines(Line[] lines, Rectangle2D bbox, byte hAlign, byte vAlign) {
\r
1405 // System.out.println("horizontal align: " + Alignment.values()[hAlign]);
\r
1406 // System.out.println("vertical align : " + Alignment.values()[vAlign]);
\r
1407 // System.out.println("bbox: " + bbox);
\r
1409 // double ybase = 0;
\r
1410 if(targetBounds != null) {
\r
1411 /* In normal cases the bounding box moves when
\r
1412 * typing. If target bounds are set, the text
\r
1413 * is fitted into the box.
\r
1416 case 1: // Trailing
\r
1417 xbase = (targetBounds.getMaxX()-paddingX) * scaleRecip;
\r
1420 xbase = targetBounds.getCenterX() * scaleRecip;
\r
1422 default: // Leading / Baseline
\r
1429 for (Line line : lines) {
\r
1430 double xoffset = 0;
\r
1431 double yoffset = 0;
\r
1434 case 1: // Trailing
\r
1435 xoffset = xbase - line.bbox.getWidth();
\r
1438 xoffset = xbase - line.bbox.getWidth() / 2;
\r
1440 default: // Leading / Baseline
\r
1447 yoffset = line.layout.getAscent();
\r
1450 yoffset = -bbox.getHeight() + line.layout.getAscent();
\r
1453 yoffset = -bbox.getHeight() / 2 + line.layout.getAscent();
\r
1457 line.alignOffset(xoffset, yoffset);
\r
1467 private Line[] layoutLines(Line[] lines, FontRenderContext frc) {
\r
1468 TextLayout emptyRowLayout = null;
\r
1469 int lineCount = lines.length;
\r
1471 for (int l = 0; l < lineCount; ++l) {
\r
1472 Line line = lines[l];
\r
1473 String lineText = line.getText();
\r
1474 // " " because TextLayout requires non-empty text and
\r
1475 // We don't want zero size for the text.
\r
1476 if (lineText.isEmpty()) {
\r
1478 if (emptyRowLayout == null)
\r
1479 emptyRowLayout = new TextLayout(lineText, font, frc);
\r
1480 line.layout = emptyRowLayout;
\r
1482 line.layout = new TextLayout(lineText, font, frc);
\r
1485 //y += line.layout.getAscent();
\r
1486 line.drawPosY = y;
\r
1487 y += line.layout.getDescent() + line.layout.getLeading() + line.layout.getAscent();
\r
1489 Rectangle2D bbox = line.layout.getLogicalHighlightShape(0, lineText.length()).getBounds2D();
\r
1490 bbox.setFrame(bbox.getX(), bbox.getY() + line.drawPosY, bbox.getWidth(), bbox.getHeight());
\r
1498 * Splits the specified string into {@link Line} structures, one for each
\r
1499 * line in the input text. The returned lines are only partially defined,
\r
1500 * waiting to be laid out (see
\r
1501 * {@link #layoutLines(Line[], FontRenderContext)})
\r
1505 * @return parsed text lines as {@link Line} structures
\r
1506 * @see #layoutLines(Line[], FontRenderContext)
\r
1508 private static Line[] parseLines(String txt) {
\r
1509 int len = txt.length();
\r
1511 return new Line[] { new Line("", 0, 0) };
\r
1513 TIntArrayList lfpos = new TIntArrayList();
\r
1515 int lineCount = 1;
\r
1516 for (;pos < len; ++lineCount) {
\r
1517 int nextlf = txt.indexOf('\n', pos);
\r
1518 lfpos.add(nextlf != -1 ? nextlf : len);
\r
1523 Line[] lines = new Line[lineCount];
\r
1525 for (int i = 0; i < lineCount-1; ++i) {
\r
1526 int lf = lfpos.getQuick(i);
\r
1527 int cr = (lf > 0) && (txt.charAt(lf - 1) == '\r') ? lf - 1 : lf;
\r
1528 lines[i] = new Line(txt, pos, cr);
\r
1531 lines[lineCount - 1] = new Line(txt, pos, len);
\r
1537 private static Line[] wrapLines(String txt, Font font, float fixedWidth, FontRenderContext frc) {
\r
1538 if(txt == null || txt.isEmpty())
\r
1541 ArrayList<Line> lines =
\r
1542 new ArrayList<Line>();
\r
1544 Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
\r
1545 map.put(TextAttribute.FONT, font);
\r
1546 AttributedString attributedText = new AttributedString(txt.isEmpty() ? "__ERROR__" : txt, map);
\r
1548 AttributedCharacterIterator paragraph = attributedText.getIterator();
\r
1549 int paragraphStart = paragraph.getBeginIndex();
\r
1550 int paragraphEnd = paragraph.getEndIndex();
\r
1551 LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
\r
1553 float breakWidth = fixedWidth;
\r
1555 // Force text to be vertical, by setting break width to 1, if the text area is narrower than "GGGG"
\r
1557 // Set position to the index of the first character in the paragraph.
\r
1558 lineMeasurer.setPosition(paragraphStart);
\r
1560 // Get lines until the entire paragraph has been displayed.
\r
1561 int next, limit, charat, position = 0;
\r
1563 while ((position = lineMeasurer.getPosition()) < paragraphEnd) {
\r
1565 // Find possible line break and set it as a limit to the next layout
\r
1566 next = lineMeasurer.nextOffset(breakWidth);
\r
1568 charat = txt.indexOf(System.getProperty("line.separator"),position+1);
\r
1569 if(charat < next && charat != -1){
\r
1573 lineMeasurer.nextLayout(breakWidth, limit, false);
\r
1575 lines.add(new Line(txt, position, limit));
\r
1578 return lines.toArray(new Line[lines.size()]);
\r
1582 public String getClipboardContent() {
\r
1583 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
\r
1584 Transferable clipData = clipboard.getContents(this);
\r
1586 return (String) (clipData.getTransferData(DataFlavor.stringFlavor));
\r
1587 } catch (Exception ee) {
\r
1592 public void setClipboardContent(String content) {
\r
1593 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
\r
1594 StringSelection data = new StringSelection(content);
\r
1595 clipboard.setContents(data, data);
\r
1599 public String toString() {
\r
1600 return super.toString() + " [text=" + text + ", font=" + font + ", color=" + color + "]";
\r
1604 protected boolean handleCommand(CommandEvent e) {
\r
1605 if (!hasState(STATE_EDITING))
\r
1608 if (Commands.SELECT_ALL.equals(e.command)) {
\r
1616 protected boolean keyPressed(KeyPressedEvent event) {
\r
1617 if (!hasState(STATE_EDITING))
\r
1620 char c = event.character;
\r
1621 boolean ctrl = event.isControlDown();
\r
1622 boolean alt = event.isAltDown();
\r
1624 // System.out.println("Key pressed '" + (event.character == 0 ? "\\0" : "" + c) + "' " + event.keyCode + " - " + Integer.toBinaryString(event.stateMask));
\r
1625 // System.out.println("ctrl: " + ctrl);
\r
1626 // System.out.println("alt: " + alt);
\r
1627 if (ctrl && !alt) {
\r
1628 switch (event.keyCode) {
\r
1629 case KeyEvent.VK_C:
\r
1630 if (caret != selectionTail) {
\r
1631 int selectionMin = Math.min(caret, selectionTail);
\r
1632 int selectionMax = Math.max(caret, selectionTail);
\r
1633 setClipboardContent(text.substring(selectionMin, selectionMax));
\r
1637 case KeyEvent.VK_X:
\r
1638 if (caret != selectionTail) {
\r
1639 int selectionMin = Math.min(caret, selectionTail);
\r
1640 int selectionMax = Math.max(caret, selectionTail);
\r
1641 setClipboardContent(text.substring(selectionMin, selectionMax));
\r
1646 case KeyEvent.VK_RIGHT:
\r
1648 // '\'' has the same keycode as VK_RIGHT but when right
\r
1649 // arrow is pressed, event character is \0.
\r
1650 moveCaretCtrlRight(event.isShiftDown());
\r
1654 case KeyEvent.VK_LEFT:
\r
1655 moveCaretCtrlLeft(event.isShiftDown());
\r
1658 case KeyEvent.VK_V:
\r
1660 String content = getClipboardContent();
\r
1661 if(content != null)
\r
1666 // Replaced by #handleCommand
\r
1667 // case KeyEvent.VK_A:
\r
1673 case KeyEvent.VK_ENTER:
\r
1675 insert(getLineSeparator());
\r
1683 } else if (!ctrl && alt) {
\r
1686 switch (event.keyCode) {
\r
1687 case KeyEvent.VK_LEFT:
\r
1688 moveCaret(-1, event.isShiftDown());
\r
1690 case KeyEvent.VK_RIGHT:
\r
1692 // '\'' has the same keycode as VK_RIGHT but when right
\r
1693 // arrow is pressed, event character is \0.
\r
1694 moveCaret(1, event.isShiftDown());
\r
1697 // Intentional fallthrough to default case
\r
1698 case KeyEvent.VK_UP:
\r
1699 moveCaretRowUp(event.isShiftDown());
\r
1701 case KeyEvent.VK_DOWN:
\r
1702 moveCaretRowDown(event.isShiftDown());
\r
1704 case KeyEvent.VK_HOME:
\r
1705 moveCaretHome(event.isShiftDown());
\r
1707 case KeyEvent.VK_END:
\r
1708 moveCaretEnd(event.isShiftDown());
\r
1711 case KeyEvent.VK_ENTER:
\r
1712 fireTextEditingEnded();
\r
1715 case KeyEvent.VK_ESCAPE:
\r
1716 text = textBeforeEdit;
\r
1718 clearState(STATE_EDITING);
\r
1719 fireTextEditingCancelled();
\r
1722 case KeyEvent.VK_BACK_SPACE:
\r
1723 if(caret == selectionTail && caret > 0) {
\r
1724 // line separator may use multiple characters, we want to remove that with one command
\r
1725 String lineSep = getLineSeparator();
\r
1726 int index = lineSep.indexOf(text.charAt(caret-1));
\r
1730 caret-= (index+1);
\r
1731 selectionTail+= (lineSep.length()-index-1);
\r
1737 case KeyEvent.VK_DELETE:
\r
1738 if(caret == selectionTail && caret < text.length()) {
\r
1739 String lineSep = getLineSeparator();
\r
1740 int index = lineSep.indexOf(text.charAt(caret));
\r
1744 selectionTail-= index;
\r
1745 caret+= (lineSep.length()-index);
\r
1754 if (c == 65535 || Character.getType(c) == Character.CONTROL) {
\r
1757 //System.out.println("Char " + c + " " + Character.getType(c) + " " + text);
\r
1758 insert(new String(new char[] {c}));
\r
1762 // FIXME This is called even if just caret was moved.
\r
1763 // This is currently necessary for repaints.
\r
1764 fireTextChanged();
\r
1765 invalidateXOffset();
\r
1769 protected String getLineSeparator() {
\r
1770 return System.getProperty("line.separator");
\r
1773 protected void selectAll() {
\r
1774 setCaret(0, false);
\r
1775 setCaret(text.length(), true);
\r
1778 protected transient int hoverClick = 0;
\r
1781 protected boolean mouseClicked(MouseClickEvent event) {
\r
1782 if (event.button != MouseClickEvent.LEFT_BUTTON)
\r
1785 if (hasState(STATE_HOVER)) {
\r
1787 if (hoverClick < 2)
\r
1789 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
\r
1790 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
\r
1793 IElement e = DiagramNodeUtil.getElement(ctx, this);
\r
1794 if (!hasState(STATE_EDITING)) {
\r
1795 if (Boolean.TRUE.equals(setEditMode(true))) {
\r
1796 editActivation = activateEdit(0, e, ctx);
\r
1802 if (hasState(STATE_EDITING)) {
\r
1803 fireTextEditingEnded();
\r
1809 protected boolean mouseDoubleClicked(MouseDoubleClickedEvent event) {
\r
1810 if (event.button != MouseClickEvent.LEFT_BUTTON)
\r
1813 if (hitTest(event, 0)) {
\r
1814 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
\r
1815 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
\r
1819 if (text != null) {
\r
1820 // Select the whole text.
\r
1821 setCaret(0, false);
\r
1822 setCaret(text.length(), true);
\r
1831 protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {
\r
1832 if (!hasState(STATE_EDITING))
\r
1835 Point2D local = controlToLocal( event.controlPosition );
\r
1836 // FIXME: once the event coordinate systems are cleared up, remove this workaround
\r
1837 local = parentToLocal(local);
\r
1838 if (hasState(STATE_HOVER) && this.containsLocal(local)) {
\r
1839 setCaret(local, event.isShiftDown());
\r
1845 protected boolean mouseMoved(MouseMovedEvent event) {
\r
1846 boolean hit = hitTest(event, 3.0);
\r
1847 if (hit != hasState(STATE_HOVER)) {
\r
1848 setState(STATE_HOVER, hit);
\r
1854 private boolean isControlDown(MouseEvent e) {
\r
1855 return e.isControlDown() || lastMouseEvent!=null?lastMouseEvent.isControlDown():false;
\r
1858 protected boolean isShiftDown(MouseEvent e) {
\r
1859 return e.isShiftDown() || lastMouseEvent!=null?lastMouseEvent.isShiftDown():false;
\r
1862 // private boolean isAltDown(MouseEvent e) {
\r
1863 // return e.isAltDown() || lastMouseEvent!=null?lastMouseEvent.isAltDown():false;
\r
1867 protected boolean mouseDragged(MouseDragBegin e) {
\r
1869 && (isControlDown(e) || isShiftDown(e))
\r
1870 && e.context instanceof NodeEventHandler
\r
1871 && (dataRVI != null || text != null))
\r
1873 List<Transferable> trs = new ArrayList<>(2);
\r
1874 if (dataRVI != null) {
\r
1875 trs.add(new LocalObjectTransferable(dataRVI));
\r
1876 trs.add(new PlaintextTransfer(dataRVI.toString()));
\r
1877 } else if (text != null && !text.isEmpty()) {
\r
1878 trs.add(new PlaintextTransfer(text));
\r
1880 if (!trs.isEmpty()) {
\r
1881 e.transferable = new MultiTransferable(trs);
\r
1888 protected boolean hitTest(MouseEvent event, double tolerance) {
\r
1889 Rectangle2D bounds = getBoundsInternal();
\r
1890 if (bounds == null)
\r
1892 Point2D localPos = NodeUtil.worldToLocal(this, event.controlPosition, new Point2D.Double());
\r
1893 double x = localPos.getX();
\r
1894 double y = localPos.getY();
\r
1895 boolean hit = bounds.contains(x, y);
\r
1899 public Rectangle2D getBoundsInternal() {
\r
1900 Rectangle2D local = lastBounds;
\r
1901 if (local == null)
\r
1903 // TODO: potential spot for CPU/memory allocation optimization
\r
1904 // by using more specialized implementations
\r
1905 if (transform.isIdentity())
\r
1907 return transform.createTransformedShape(local).getBounds2D();
\r
1910 protected Color add(Color c, int r, int g, int b) {
\r
1911 int nr = Math.min(255, c.getRed() + r);
\r
1912 int ng = Math.min(255, c.getGreen() + g);
\r
1913 int nb = Math.min(255, c.getBlue() + b);
\r
1914 return new Color(nr, ng, nb);
\r
1917 public TextEditActivation activateEdit(int mouseId, IElement e, ICanvasContext ctx) {
\r
1918 EditDataNode data = EditDataNode.getNode(this);
\r
1919 deactivateEdit(data, null);
\r
1920 TextEditActivation result = new TextEditActivation(mouseId, e, ctx);
\r
1921 data.setTextEditActivation(result);
\r
1926 * @return <code>true</code> if this node is or was previously in editing
\r
1929 protected boolean deactivateEdit() {
\r
1930 boolean result = deactivateEdit( editActivation );
\r
1931 result |= editActivation != null;
\r
1932 editActivation = null;
\r
1936 protected boolean deactivateEdit(TextEditActivation activation) {
\r
1937 return deactivateEdit( EditDataNode.getNode(this), activation );
\r
1940 protected boolean deactivateEdit(EditDataNode data, TextEditActivation activation) {
\r
1941 TextEditActivation previous = data.getTextEditActivation();
\r
1942 if (previous != null && (previous == activation || activation == null)) {
\r
1943 previous.release();
\r
1944 data.setTextEditActivation(null);
\r
1951 public int getEventMask() {
\r
1952 return EventTypes.KeyPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask
\r
1953 | EventTypes.MouseClickMask | EventTypes.MouseDragBeginMask | EventTypes.CommandMask;
\r
1956 private MouseEvent lastMouseEvent = null;
\r
1959 public boolean handleEvent(Event e) {
\r
1960 if(e instanceof MouseEvent && !(e instanceof MouseDragBegin)) lastMouseEvent = (MouseEvent)e;
\r
1961 return super.handleEvent(e);
\r
1965 public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
\r
1966 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
\r
1970 public <T> T getProperty(String propertyName) {
\r
1975 public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
\r
1978 public void synchronizeText(String text) {
\r
1982 public void synchronizeColor(RGB.Integer color) {
\r
1983 this.color = Colors.awt(color);
\r
1986 public void synchronizeFont(org.simantics.datatypes.literal.Font font) {
\r
1987 setFont(Fonts.awt(font));
\r
1990 public void synchronizeTransform(double[] data) {
\r
1991 this.setTransform(new AffineTransform(data));
\r
1994 public static void main(String[] args) {
\r
1995 Line[] lines = parseLines("\n \n FOO \n\nBAR\n\n\n BAZ\n\n");
\r
1996 System.out.println(Arrays.toString(lines));
\r
1997 System.out.println(GeometryUtils.pointToMillimeter(1));
\r
1998 System.out.println(GeometryUtils.pointToMillimeter(12));
\r
1999 System.out.println(GeometryUtils.pointToMillimeter(72));
\r
2002 ///////////////////////////////////////////////////////////////////////////
\r
2003 // LEGACY CODE NEEDED BY INHERITED CLASSES FOR NOW
\r
2004 ///////////////////////////////////////////////////////////////////////////
\r
2006 protected double getHorizontalAlignOffset(Rectangle2D r) {
\r
2007 switch (horizontalAlignment) {
\r
2008 case 0: return 0; // Leading
\r
2009 case 1: return -r.getWidth(); // Trailing
\r
2010 case 2: return -r.getCenterX(); // Center
\r
2011 default: return 0;
\r
2015 protected double getVerticalAlignOffset() {
\r
2016 FontMetrics fm = fontMetrics;
\r
2019 switch (verticalAlignment) {
\r
2020 case 0: return fm.getMaxAscent(); // Leading=top=maxascent
\r
2021 case 1: return -fm.getMaxDescent(); // Trailing=bottom=maxdescent
\r
2022 case 2: return fm.getMaxAscent() / 2; // Center=maxascent / 2
\r
2024 default: return 0;
\r
2028 ///////////////////////////////////////////////////////////////////////////
\r
2029 // LEGACY CODE ENDS
\r
2030 ///////////////////////////////////////////////////////////////////////////
\r