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 isPdfField = false;
\r
677 String fieldName = null;
\r
678 if (writer != null) {
\r
679 // TODO: replace this hack with proper text field name field
\r
680 fieldName = NodeUtil.getNodeName(this);
\r
681 isPdfField = ( fieldName.equals("approved_by") ||
\r
682 fieldName.equals("checked_by") ||
\r
683 fieldName.equals("designer name") ||
\r
684 fieldName.equals("created_by") );
\r
687 Color backgroundColor = hasState(STATE_VALID) ? this.backgroundColor : Color.red;
\r
690 if ( !isPdfField ) {
\r
692 // Fill background if necessary
\r
693 if (backgroundColor != null) {
\r
694 g.setColor(backgroundColor);
\r
700 int selectionMin = Math.min(caret, selectionTail);
\r
701 int selectionMax = Math.max(caret, selectionTail);
\r
705 renderText(g, xOffset);
\r
707 Shape clip = g.getClip();
\r
709 // Selection background & text
\r
710 for (Line line : lines) {
\r
711 if (line.intersectsRange(selectionMin, selectionMax)) {
\r
712 Shape selShape = line.getLogicalHighlightShape(selectionMin, selectionMax);
\r
713 line.translate(g, xOffset, 0);
\r
714 g.setClip(selShape);
\r
715 g.setColor(SELECTION_BACKGROUND_COLOR);
\r
717 g.setColor(Color.WHITE);
\r
718 //line.layout.draw(g, 0, 0);
\r
719 g.drawString(line.getText(), 0, 0);
\r
720 line.translateInv(g, xOffset, 0);
\r
741 // TODO: multiline support
\r
743 AffineTransform at = g.getTransform();
\r
744 float height = writer.getPageSize().getHeight();
\r
745 Rectangle2D rr = textClip;
\r
746 // Point2D pt1 = new Point2D.Double(rr.getX(), rr.getY()+rr.getHeight());
\r
747 // Point2D pt2 = new Point2D.Double(rr.getX()+rr.getWidth(), rr.getY());
\r
748 Point2D pt1 = new Point2D.Double(0, 0);
\r
749 Point2D pt2 = new Point2D.Double(47.f/*+rr.getWidth()*/, -rr.getHeight());
\r
750 pt1 = at.transform(pt1, pt1);
\r
751 pt2 = at.transform(pt2, pt2);
\r
752 Rectangle rectangle = new Rectangle(
\r
753 (float) pt1.getX(),
\r
754 height-(float) pt1.getY(),
\r
755 (float) pt2.getX(),
\r
756 height-(float) pt2.getY());
\r
758 FontMapper mapper = (FontMapper) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_FONTMAPPER);
\r
759 // FontMetrics fm = g.getFontMetrics(font);
\r
761 // TODO Oikea leveys
\r
762 // TODO Uniikki nimi
\r
764 PdfFormField field = PdfFormField.createTextField(writer, false, false, 20);
\r
765 field.setFieldName(this.getId().toString());
\r
766 field.setWidget(rectangle, PdfAnnotation.HIGHLIGHT_NONE);
\r
767 field.setQuadding(PdfFormField.Q_RIGHT);
\r
768 field.setFieldFlags(PdfFormField.FF_READ_ONLY);
\r
769 field.setRotate(90);
\r
770 writer.addAnnotation(field);
\r
777 PdfFormField field = PdfFormField.createSignature(writer);
\r
778 field.setWidget(rectangle, PdfAnnotation.HIGHLIGHT_NONE);
\r
779 field.setFieldName(fieldName);
\r
780 field.setQuadding(PdfFormField.Q_LEFT);
\r
781 field.setFlags(PdfAnnotation.FLAGS_PRINT);
\r
782 //field.setFieldFlags(PdfFormField.FF_READ_ONLY)
\r
783 field.setFieldFlags(PdfFormField.FF_EDIT);
\r
785 field.setMKBackgroundColor( backgroundColor!=null?Color.WHITE:backgroundColor );
\r
786 PdfAppearance tp = PdfAppearance.createAppearance(writer, 72, 48);
\r
787 tp.rectangle(rectangle);
\r
789 field.setAppearance(PdfAnnotation.APPEARANCE_NORMAL, tp);
\r
790 writer.addAnnotation(field);
\r
795 TextField textField = new TextField(writer, rectangle, fieldName);
\r
796 textField.setFieldName(fieldName);
\r
797 textField.setFont(mapper.awtToPdf(font));
\r
798 textField.setBorderStyle(0);
\r
799 //textField.setAlignment(Element.ALIGN_LEFT);
\r
800 textField.setAlignment(Element.ALIGN_BOTTOM);
\r
801 textField.setRotation(90);
\r
802 textField.setOptions(TextField.EDIT|TextField.DO_NOT_SPELL_CHECK);
\r
803 if ( text!=null ) {
\r
804 textField.setText(text);
\r
806 if ( color!=null ) {
\r
807 textField.setTextColor(color);
\r
809 textField.setBackgroundColor( backgroundColor!=null?Color.WHITE:backgroundColor );
\r
810 PdfFormField field = textField.getTextField();
\r
811 writer.addAnnotation(field);
\r
812 } catch (IOException e) {
\r
813 e.printStackTrace();
\r
814 } catch (DocumentException e) {
\r
815 e.printStackTrace();
\r
819 // } catch (IOException e) {
\r
820 // // TODO Auto-generated catch block
\r
821 // e.printStackTrace();
\r
822 // } catch (DocumentException e) {
\r
823 // // TODO Auto-generated catch block
\r
824 // e.printStackTrace();
\r
829 g.setClip(clipSave);
\r
831 if (borderWidth > 0f && borderColor != null) {
\r
832 g.setColor(borderColor);
\r
833 g.setStroke(new BasicStroke((float) (scale*borderWidth)));
\r
837 //System.out.println("bw: " + borderWidth);
\r
838 if (isSelected && showsSelection()) {
\r
839 Composite oc = g.getComposite();
\r
840 g.setComposite(SrcOver_50);
\r
841 g.setColor(Color.RED);
\r
842 float bw = borderWidth;
\r
843 double s = currentScale;
\r
845 bw = (float) (1f / s);
\r
849 g.setStroke(new BasicStroke(bw));
\r
851 //g.draw(GeometryUtils.expandRectangle(r, 1.0));
\r
853 g.setComposite(oc);
\r
856 g.scale(scaleRecip, scaleRecip);
\r
857 g.setStroke(RESET_STROKE);
\r
859 lastBounds = getScaledOffsetBounds(r, lastBounds, scale, 0, 0);
\r
860 // g.setColor(Color.MAGENTA); // DEBUG
\r
861 // g.draw(lastBounds); // DEBUG
\r
862 // g.setColor(Color.ORANGE); // DEBUG
\r
863 // g.draw(getBoundsInLocal()); // DEBUG
\r
865 renderSelectedHover(g, isSelected, hover);
\r
868 private void renderCaret(Graphics2D g) {
\r
869 g.setColor(Color.BLACK);
\r
870 for (int i = 0; i < lines.length; i++) {
\r
871 Line line = lines[i];
\r
872 // prevent rendering caret twice on line changes
\r
873 if (line.containsOffset(caret) && // line contains caret
\r
874 (caret != line.endOffset || //caret is not in the end of the line
\r
875 i == lines.length-1 || //caret is end of the last line
\r
876 lines[i+1].startOffset != line.endOffset)) { // beginning of the next line does not start withe the same index as current line
\r
877 Shape[] caretShape = line.getCaretShapes(caret);
\r
878 line.translate(g, xOffset, 0);
\r
879 g.draw(caretShape[0]);
\r
880 if (caretShape[1] != null)
\r
881 g.draw(caretShape[1]);
\r
882 line.translateInv(g, xOffset, 0);
\r
886 private void renderText(Graphics2D g, float xOffset) {
\r
887 //g.draw(tightBoundsCache); // DEBUG
\r
888 for (Line line : lines) {
\r
889 //line.layout.draw(g, line.alignedPosX + xOffset, line.alignedPosY);
\r
890 g.drawString(line.getText(), line.alignedPosX + xOffset, line.alignedPosY);
\r
891 //g.draw(line.abbox); // DEBUG
\r
895 protected Rectangle2D getScaledOffsetBounds(Rectangle2D originalBounds, Rectangle2D dst, double scale, double offsetX, double offsetY) {
\r
896 AffineTransform btr = tempAffineTransform.get();
\r
897 btr.setToTranslation(offsetX*scale, offsetY*scale);
\r
898 btr.scale(scale, scale);
\r
899 if (btr.isIdentity()) {
\r
900 dst.setFrame(originalBounds);
\r
902 dst.setFrame(btr.createTransformedShape(originalBounds).getBounds2D());
\r
908 * Invoked when TextNode is selected and a mouse is hovering on top of it. Can be overridden to add custom rendering.
\r
912 protected void renderSelectedHover(Graphics2D g, boolean isSelected, boolean isHovering) {
\r
916 * Replaces the current selection with the content or inserts
\r
917 * the content at caret. After the insertion the caret
\r
918 * will be at the end of inserted text and selection will
\r
922 @SyncField({"text","caret","selectionTail"})
\r
923 protected void insert(String content) {
\r
924 content = editContentFilter != null ? editContentFilter.filter(this, content) : content;
\r
926 int selectionMin = Math.min(caret, selectionTail);
\r
927 int selectionMax = Math.max(caret, selectionTail);
\r
929 String begin = text.substring(0, selectionMin);
\r
930 String end = text.substring(selectionMax);
\r
931 text = begin + content + end;
\r
932 caret = selectionMin + content.length();
\r
933 selectionTail = caret;
\r
935 assert (caret <= text.length());
\r
936 //System.out.println(text + " " + caret );
\r
938 if(validator != null) {
\r
939 String error = validator.apply(text);
\r
940 setState(STATE_VALID, (error == null));
\r
947 protected void fireTextChanged() {
\r
948 if(textListener != null)
\r
949 textListener.textChanged();
\r
954 protected void fireTextEditingStarted() {
\r
955 if(textListener != null)
\r
956 textListener.textEditingStarted();
\r
960 protected void fireTextEditingCancelled() {
\r
961 setState(STATE_VALID);
\r
963 if (deactivateEdit()) {
\r
964 if (textListener != null)
\r
965 textListener.textEditingCancelled();
\r
967 setEditMode(false, false);
\r
969 if (textBeforeEdit != null)
\r
970 setText(textBeforeEdit);
\r
977 public void fireTextEditingEnded() {
\r
978 if (!hasState(STATE_VALID)) {
\r
979 fireTextEditingCancelled();
\r
980 setState(STATE_VALID);
\r
984 if (deactivateEdit()) {
\r
985 if (textListener != null)
\r
986 textListener.textEditingEnded();
\r
988 setEditMode(false, false);
\r
993 public void setTextListener(ITextListener listener) {
\r
994 this.textListener = listener;
\r
997 public void setValidator(Function1<String, String> validator) {
\r
998 this.validator = validator;
\r
1001 public void setContentFilter(ITextContentFilter filter) {
\r
1002 this.editContentFilter = filter;
\r
1005 public void setRVI(RVI rvi) {
\r
1006 this.dataRVI = rvi;
\r
1009 private void invalidateXOffset() {
\r
1010 setState(STATE_X_OFFSET_IS_DIRTY);
\r
1013 private void computeEditingXOffset() {
\r
1015 if(lines == null) return;
\r
1016 if(!hasState(STATE_X_OFFSET_IS_DIRTY)) return;
\r
1017 if(fixedWidth > 0f) {
\r
1019 // TODO: implement
\r
1020 // float[] coords = textLayout.getCaretInfo(TextHitInfo.afterOffset(caret));
\r
1021 // if(coords != null) {
\r
1022 // if(coords[0] > (fixedWidth*MAX_CARET_POSITION)) xOffset = (float)((fixedWidth*MAX_CARET_POSITION)-coords[0]);
\r
1023 // else xOffset = 0;
\r
1032 clearState(STATE_X_OFFSET_IS_DIRTY);
\r
1036 @SyncField({"caret","selectionTail"})
\r
1037 protected void moveCaret(int move, boolean select) {
\r
1038 // prevent setting caret into line separator.
\r
1040 while (text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
\r
1042 } else if (move < 0) {
\r
1043 while (caret+move >= 0 && text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
\r
1049 if (caret > text.length())
\r
1050 caret = text.length();
\r
1052 selectionTail = caret;
\r
1055 private Line findCaretLine() {
\r
1056 // Find the line where caret is. Starting from first line.
\r
1057 for(int i = 0; i < lines.length; i++) {
\r
1058 Line line = lines[i];
\r
1059 if(caret <= line.endOffset) {
\r
1067 * Moves caret to next not letter or digit
\r
1068 * @param shiftDown
\r
1070 private void moveCaretCtrlLeft(boolean shiftDown) {
\r
1071 Line line = findCaretLine();
\r
1072 if(line != null) {
\r
1074 for(i = caret-1; i > line.startOffset; i--) {
\r
1075 char c = line.document.charAt(i);
\r
1076 if(!Character.isLetterOrDigit(c)) {
\r
1080 moveCaret(i - caret, shiftDown);
\r
1085 * Moves caret to previous non letter or digit
\r
1086 * @param shiftDown
\r
1088 private void moveCaretCtrlRight(boolean shiftDown) {
\r
1089 Line line = findCaretLine();
\r
1090 if(line != null) {
\r
1092 for(i = caret + 1; i < line.endOffset; i++) {
\r
1093 char c = line.document.charAt(i);
\r
1094 if(!Character.isLetterOrDigit(c)) {
\r
1098 moveCaret(i - caret, shiftDown);
\r
1103 * Moves caret to line end
\r
1104 * @param shiftDown
\r
1106 private void moveCaretEnd(boolean shiftDown) {
\r
1107 Line line = findCaretLine();
\r
1109 // Move caret to the end of the line
\r
1110 moveCaret(line.endOffset - caret, shiftDown);
\r
1114 * Moves caret to beginning of a line
\r
1115 * @param shiftDown
\r
1117 private void moveCaretHome(boolean shiftDown) {
\r
1118 Line line = findCaretLine();
\r
1120 // Move caret to the beginning of the line
\r
1121 moveCaret(line.startOffset - caret, shiftDown);
\r
1125 * Moves caret one row up and tries to maintain the location
\r
1126 * @param shiftDown
\r
1128 private void moveCaretRowUp(boolean shiftDown) {
\r
1129 // Find the line where caret is. Starting from first line.
\r
1130 for(int i = 0; i < lines.length; i++) {
\r
1131 Line line = lines[i];
\r
1132 if(caret <= line.endOffset) {
\r
1133 // caret is in this line
\r
1135 // Already on top line
\r
1136 // Select the beginning of the line
\r
1137 moveCaret(-caret, shiftDown);
\r
1139 Line prevLine = lines[i-1];
\r
1140 int prevLength = prevLine.endOffset - prevLine.startOffset;
\r
1141 int posInCurRow = caret - line.startOffset;
\r
1142 if(prevLength < posInCurRow)
\r
1143 posInCurRow = prevLength;
\r
1145 int newPos = prevLine.startOffset + posInCurRow;
\r
1146 moveCaret(newPos - caret, shiftDown);
\r
1154 * Moves caret one row down and tries to maintain the location
\r
1155 * @param shiftDown
\r
1157 private void moveCaretRowDown(boolean shiftDown) {
\r
1158 // Find the line where caret is. Starting from last line.
\r
1159 for(int i = lines.length - 1; i >= 0; i--) {
\r
1160 Line line = lines[i];
\r
1161 if(caret >= line.startOffset) {
\r
1162 // caret is in this line
\r
1163 if(i == lines.length - 1) {
\r
1164 // Already on bottom line, cannot go below
\r
1165 // Select to the end of the line
\r
1166 moveCaret(line.endOffset - caret, shiftDown);
\r
1168 Line prevLine = lines[i+1]; // Previous line
\r
1170 // Find new caret position.
\r
1171 // Either it is in the same index as before, or if the row
\r
1172 // is not long enough, select the end of the row.
\r
1173 int prevLength = prevLine.endOffset - prevLine.startOffset;
\r
1174 int posInCurRow = caret - line.startOffset;
\r
1175 if(prevLength < posInCurRow)
\r
1176 posInCurRow = prevLength;
\r
1177 int newPos = prevLine.startOffset + posInCurRow;
\r
1178 moveCaret(newPos - caret, shiftDown);
\r
1185 @SyncField({"caret","selectionTail"})
\r
1186 protected void setCaret(int pos, boolean select) {
\r
1190 if (caret > text.length())
\r
1191 caret = text.length();
\r
1193 selectionTail = caret;
\r
1196 protected void setCaret(Point2D point) {
\r
1197 setCaret(point, false);
\r
1200 @SyncField({"caret","selectionTail"})
\r
1201 protected void setCaret(Point2D point, boolean select) {
\r
1203 for(int i = 0; i < lines.length; i++) {
\r
1204 Line line = lines[i];
\r
1205 Rectangle2D bounds = line.abbox;
\r
1206 // Add heights of bboxes for determining the correct line
\r
1208 lineY = bounds.getY();
\r
1210 lineY += lines[i-1].abbox.getHeight();
\r
1212 double lineHeight = bounds.getHeight();
\r
1213 double hitY = point.getY() / scale;
\r
1214 if(hitY >= lineY && hitY <= lineY + lineHeight) {
\r
1215 // Hit is in this line
\r
1216 float x = (float)(point.getX() / scale) - (float)line.abbox.getX();
\r
1217 float y = (float)(point.getY() / scale - lineHeight * i) ;
\r
1218 TextHitInfo info = line.layout.hitTestChar(x, y);
\r
1219 caret = line.startOffset + info.getInsertionIndex();
\r
1220 if (caret > line.endOffset)
\r
1221 caret = line.endOffset;
\r
1223 selectionTail = caret;
\r
1228 invalidateXOffset();
\r
1229 assert (caret <= text.length());
\r
1233 public Rectangle2D getBoundsInLocal() {
\r
1234 if(targetBounds != null)
\r
1235 return targetBounds;
\r
1237 return expandBounds( getTightAlignedBoundsInLocal(null) );
\r
1240 protected Rectangle2D expandBounds(Rectangle2D r) {
\r
1241 r.setRect(r.getX() * scale - paddingX, r.getY() * scale -paddingY, r.getWidth()*scale + paddingX + paddingX, r.getHeight()*scale + paddingY + paddingY);
\r
1242 //System.out.println(" => " + r);
\r
1246 protected Rectangle2D expandBoundsUnscaled(Rectangle2D r) {
\r
1247 r.setRect(r.getX() - scaleRecip*paddingX, r.getY() -scaleRecip*paddingY, r.getWidth() + scaleRecip*paddingX + scaleRecip*paddingX, r.getHeight() + scaleRecip*paddingY + scaleRecip*paddingY);
\r
1248 //System.out.println(" => " + r);
\r
1252 protected Rectangle2D expandBounds(Rectangle2D r, double amount) {
\r
1253 r.setRect(r.getX() - amount, r.getY() - amount, r.getWidth() + 2*amount, r.getHeight() + 2*amount);
\r
1257 protected Rectangle2D expandBounds(Rectangle2D r, double left, double top, double right, double bottom) {
\r
1258 r.setRect(r.getX() - left, r.getY() - top, r.getWidth() + left + right, r.getHeight() + top + bottom);
\r
1262 private void resetCaches() {
\r
1263 this.tightBoundsCache = null;
\r
1264 this.lines = null;
\r
1265 this.fontMetrics = null;
\r
1269 * Returns the tight bounds around the current text using the current font
\r
1270 * in the specified rectangle. If the specified rectangle is
\r
1271 * <code>null</code> a new Rectangle2D.Double instance will be created.
\r
1276 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r) {
\r
1277 return getTightAlignedBoundsInLocal(r, FRC);
\r
1281 * Returns the tight bounds around the current text using the current font
\r
1282 * in the specified rectangle. If the specified rectangle is
\r
1283 * <code>null</code> a new Rectangle2D.Double instance will be created.
\r
1286 * the rectangle where the result of the method is placed or
\r
1287 * <code>null</code> to allocate new rectangle
\r
1288 * @param frc current font render context
\r
1289 * @return r or new Rectangle2D.Double instance containing the requested
\r
1292 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r, FontRenderContext frc) {
\r
1294 r = new Rectangle2D.Double();
\r
1296 if (tightBoundsCache != null) {
\r
1297 r.setFrame(tightBoundsCache);
\r
1301 String txt = text;
\r
1302 if (font == null || txt == null) {
\r
1303 r.setFrame(0, 0, 2, 1);
\r
1307 //System.out.println("TextNode.getTightAlignedBoundsInLocal('" + txt + "')");
\r
1309 // Parse & layout (unaligned)
\r
1310 Line[] lines = null;
\r
1312 if(hasState(STATE_WRAP_TEXT)) {
\r
1313 float width = fixedWidth;
\r
1314 if(width <= 0 && targetBounds != null)
\r
1315 width = (float) (((targetBounds.getWidth() - 2*paddingX)) * scaleRecip);
\r
1317 lines = wrapLines(txt, font, width, frc);
\r
1321 lines = parseLines(txt);
\r
1322 this.lines = layoutLines(lines, frc);
\r
1324 // Calculate tight bounds based on unaligned layout
\r
1325 //System.out.println("Unaligned");
\r
1326 tightBoundsCache = calculateBounds(lines, Line.BBOX, null);
\r
1327 //System.out.println(" => " + tightBoundsCache);
\r
1329 this.lines = layoutLinesX(lines, tightBoundsCache);
\r
1330 // Align each line to the calculated tight bounds
\r
1331 this.lines = alignLines(this.lines, tightBoundsCache, horizontalAlignment, verticalAlignment);
\r
1333 // Calculate aligned bounds
\r
1334 //System.out.println("Aligned");
\r
1335 calculateBounds(lines, Line.ABBOX, tightBoundsCache);
\r
1337 r.setFrame(tightBoundsCache);
\r
1338 //System.out.println(" => " + tightBoundsCache);
\r
1346 * the bounding box of all the whole laid out text (only bbox
\r
1350 private Line[] layoutLinesX(Line[] lines, Rectangle2D bbox) {
\r
1351 int lineCount = lines.length;
\r
1352 for (int l = 0; l < lineCount; ++l) {
\r
1353 Line line = lines[l];
\r
1354 // Compute pen x position. If the paragraph is right-to-left we
\r
1355 // will align the TextLayouts to the right edge of the panel.
\r
1356 // Note: drawPosX is always where the LEFT of the text is placed.
\r
1357 // NOTE: This changes based on horizontal alignment
\r
1358 line.drawPosX = (float) (line.layout.isLeftToRight() ? 0f
\r
1359 : tightBoundsCache.getWidth() - line.layout.getAdvance());
\r
1366 * @param boundsProvider
\r
1370 private static Rectangle2D calculateBounds(Line[] lines, BoundsProcedure boundsProvider, Rectangle2D result) {
\r
1371 if (result == null)
\r
1372 result = new Rectangle2D.Double();
\r
1374 result.setFrame(0, 0, 0, 0);
\r
1376 for (Line line : lines) {
\r
1377 //System.out.println("line: " + line);
\r
1378 Rectangle2D bbox = boundsProvider.getBounds(line);
\r
1379 if (result.isEmpty())
\r
1380 result.setFrame(bbox);
\r
1382 Rectangle2D.union(result, bbox, result);
\r
1383 //System.out.println("bounds: " + result);
\r
1385 //System.out.println("final bounds: " + result);
\r
1395 * @return aligned lines
\r
1397 private Line[] alignLines(Line[] lines, Rectangle2D bbox, byte hAlign, byte vAlign) {
\r
1398 // System.out.println("horizontal align: " + Alignment.values()[hAlign]);
\r
1399 // System.out.println("vertical align : " + Alignment.values()[vAlign]);
\r
1400 // System.out.println("bbox: " + bbox);
\r
1402 // double ybase = 0;
\r
1403 if(targetBounds != null) {
\r
1404 /* In normal cases the bounding box moves when
\r
1405 * typing. If target bounds are set, the text
\r
1406 * is fitted into the box.
\r
1409 case 1: // Trailing
\r
1410 xbase = (targetBounds.getMaxX()-paddingX) * scaleRecip;
\r
1413 xbase = targetBounds.getCenterX() * scaleRecip;
\r
1415 default: // Leading / Baseline
\r
1422 for (Line line : lines) {
\r
1423 double xoffset = 0;
\r
1424 double yoffset = 0;
\r
1427 case 1: // Trailing
\r
1428 xoffset = xbase - line.bbox.getWidth();
\r
1431 xoffset = xbase - line.bbox.getWidth() / 2;
\r
1433 default: // Leading / Baseline
\r
1440 yoffset = line.layout.getAscent();
\r
1443 yoffset = -bbox.getHeight() + line.layout.getAscent();
\r
1446 yoffset = -bbox.getHeight() / 2 + line.layout.getAscent();
\r
1450 line.alignOffset(xoffset, yoffset);
\r
1460 private Line[] layoutLines(Line[] lines, FontRenderContext frc) {
\r
1461 TextLayout emptyRowLayout = null;
\r
1462 int lineCount = lines.length;
\r
1464 for (int l = 0; l < lineCount; ++l) {
\r
1465 Line line = lines[l];
\r
1466 String lineText = line.getText();
\r
1467 // " " because TextLayout requires non-empty text and
\r
1468 // We don't want zero size for the text.
\r
1469 if (lineText.isEmpty()) {
\r
1471 if (emptyRowLayout == null)
\r
1472 emptyRowLayout = new TextLayout(lineText, font, frc);
\r
1473 line.layout = emptyRowLayout;
\r
1475 line.layout = new TextLayout(lineText, font, frc);
\r
1478 //y += line.layout.getAscent();
\r
1479 line.drawPosY = y;
\r
1480 y += line.layout.getDescent() + line.layout.getLeading() + line.layout.getAscent();
\r
1482 Rectangle2D bbox = line.layout.getLogicalHighlightShape(0, lineText.length()).getBounds2D();
\r
1483 bbox.setFrame(bbox.getX(), bbox.getY() + line.drawPosY, bbox.getWidth(), bbox.getHeight());
\r
1491 * Splits the specified string into {@link Line} structures, one for each
\r
1492 * line in the input text. The returned lines are only partially defined,
\r
1493 * waiting to be laid out (see
\r
1494 * {@link #layoutLines(Line[], FontRenderContext)})
\r
1498 * @return parsed text lines as {@link Line} structures
\r
1499 * @see #layoutLines(Line[], FontRenderContext)
\r
1501 private static Line[] parseLines(String txt) {
\r
1502 int len = txt.length();
\r
1504 return new Line[] { new Line("", 0, 0) };
\r
1506 TIntArrayList lfpos = new TIntArrayList();
\r
1508 int lineCount = 1;
\r
1509 for (;pos < len; ++lineCount) {
\r
1510 int nextlf = txt.indexOf('\n', pos);
\r
1511 lfpos.add(nextlf != -1 ? nextlf : len);
\r
1516 Line[] lines = new Line[lineCount];
\r
1518 for (int i = 0; i < lineCount-1; ++i) {
\r
1519 int lf = lfpos.getQuick(i);
\r
1520 int cr = (lf > 0) && (txt.charAt(lf - 1) == '\r') ? lf - 1 : lf;
\r
1521 lines[i] = new Line(txt, pos, cr);
\r
1524 lines[lineCount - 1] = new Line(txt, pos, len);
\r
1530 private static Line[] wrapLines(String txt, Font font, float fixedWidth, FontRenderContext frc) {
\r
1531 if(txt == null || txt.isEmpty())
\r
1534 ArrayList<Line> lines =
\r
1535 new ArrayList<Line>();
\r
1537 Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
\r
1538 map.put(TextAttribute.FONT, font);
\r
1539 AttributedString attributedText = new AttributedString(txt.isEmpty() ? "__ERROR__" : txt, map);
\r
1541 AttributedCharacterIterator paragraph = attributedText.getIterator();
\r
1542 int paragraphStart = paragraph.getBeginIndex();
\r
1543 int paragraphEnd = paragraph.getEndIndex();
\r
1544 LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
\r
1546 float breakWidth = fixedWidth;
\r
1548 // Force text to be vertical, by setting break width to 1, if the text area is narrower than "GGGG"
\r
1550 // Set position to the index of the first character in the paragraph.
\r
1551 lineMeasurer.setPosition(paragraphStart);
\r
1553 // Get lines until the entire paragraph has been displayed.
\r
1554 int next, limit, charat, position = 0;
\r
1556 while ((position = lineMeasurer.getPosition()) < paragraphEnd) {
\r
1558 // Find possible line break and set it as a limit to the next layout
\r
1559 next = lineMeasurer.nextOffset(breakWidth);
\r
1561 charat = txt.indexOf(System.getProperty("line.separator"),position+1);
\r
1562 if(charat < next && charat != -1){
\r
1566 lineMeasurer.nextLayout(breakWidth, limit, false);
\r
1568 lines.add(new Line(txt, position, limit));
\r
1571 return lines.toArray(new Line[lines.size()]);
\r
1575 public String getClipboardContent() {
\r
1576 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
\r
1577 Transferable clipData = clipboard.getContents(this);
\r
1579 return (String) (clipData.getTransferData(DataFlavor.stringFlavor));
\r
1580 } catch (Exception ee) {
\r
1585 public void setClipboardContent(String content) {
\r
1586 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
\r
1587 StringSelection data = new StringSelection(content);
\r
1588 clipboard.setContents(data, data);
\r
1592 public String toString() {
\r
1593 return super.toString() + " [text=" + text + ", font=" + font + ", color=" + color + "]";
\r
1597 protected boolean handleCommand(CommandEvent e) {
\r
1598 if (!hasState(STATE_EDITING))
\r
1601 if (Commands.SELECT_ALL.equals(e.command)) {
\r
1609 protected boolean keyPressed(KeyPressedEvent event) {
\r
1610 if (!hasState(STATE_EDITING))
\r
1613 char c = event.character;
\r
1614 boolean ctrl = event.isControlDown();
\r
1615 boolean alt = event.isAltDown();
\r
1617 // System.out.println("Key pressed '" + (event.character == 0 ? "\\0" : "" + c) + "' " + event.keyCode + " - " + Integer.toBinaryString(event.stateMask));
\r
1618 // System.out.println("ctrl: " + ctrl);
\r
1619 // System.out.println("alt: " + alt);
\r
1620 if (ctrl && !alt) {
\r
1621 switch (event.keyCode) {
\r
1622 case KeyEvent.VK_C:
\r
1623 if (caret != selectionTail) {
\r
1624 int selectionMin = Math.min(caret, selectionTail);
\r
1625 int selectionMax = Math.max(caret, selectionTail);
\r
1626 setClipboardContent(text.substring(selectionMin, selectionMax));
\r
1630 case KeyEvent.VK_X:
\r
1631 if (caret != selectionTail) {
\r
1632 int selectionMin = Math.min(caret, selectionTail);
\r
1633 int selectionMax = Math.max(caret, selectionTail);
\r
1634 setClipboardContent(text.substring(selectionMin, selectionMax));
\r
1639 case KeyEvent.VK_RIGHT:
\r
1641 // '\'' has the same keycode as VK_RIGHT but when right
\r
1642 // arrow is pressed, event character is \0.
\r
1643 moveCaretCtrlRight(event.isShiftDown());
\r
1647 case KeyEvent.VK_LEFT:
\r
1648 moveCaretCtrlLeft(event.isShiftDown());
\r
1651 case KeyEvent.VK_V:
\r
1653 String content = getClipboardContent();
\r
1654 if(content != null)
\r
1659 // Replaced by #handleCommand
\r
1660 // case KeyEvent.VK_A:
\r
1666 case KeyEvent.VK_ENTER:
\r
1668 insert(getLineSeparator());
\r
1676 } else if (!ctrl && alt) {
\r
1679 switch (event.keyCode) {
\r
1680 case KeyEvent.VK_LEFT:
\r
1681 moveCaret(-1, event.isShiftDown());
\r
1683 case KeyEvent.VK_RIGHT:
\r
1685 // '\'' has the same keycode as VK_RIGHT but when right
\r
1686 // arrow is pressed, event character is \0.
\r
1687 moveCaret(1, event.isShiftDown());
\r
1690 // Intentional fallthrough to default case
\r
1691 case KeyEvent.VK_UP:
\r
1692 moveCaretRowUp(event.isShiftDown());
\r
1694 case KeyEvent.VK_DOWN:
\r
1695 moveCaretRowDown(event.isShiftDown());
\r
1697 case KeyEvent.VK_HOME:
\r
1698 moveCaretHome(event.isShiftDown());
\r
1700 case KeyEvent.VK_END:
\r
1701 moveCaretEnd(event.isShiftDown());
\r
1704 case KeyEvent.VK_ENTER:
\r
1705 fireTextEditingEnded();
\r
1708 case KeyEvent.VK_ESCAPE:
\r
1709 text = textBeforeEdit;
\r
1711 clearState(STATE_EDITING);
\r
1712 fireTextEditingCancelled();
\r
1715 case KeyEvent.VK_BACK_SPACE:
\r
1716 if(caret == selectionTail && caret > 0) {
\r
1717 // line separator may use multiple characters, we want to remove that with one command
\r
1718 String lineSep = getLineSeparator();
\r
1719 int index = lineSep.indexOf(text.charAt(caret-1));
\r
1723 caret-= (index+1);
\r
1724 selectionTail+= (lineSep.length()-index-1);
\r
1730 case KeyEvent.VK_DELETE:
\r
1731 if(caret == selectionTail && caret < text.length()) {
\r
1732 String lineSep = getLineSeparator();
\r
1733 int index = lineSep.indexOf(text.charAt(caret));
\r
1737 selectionTail-= index;
\r
1738 caret+= (lineSep.length()-index);
\r
1747 if (c == 65535 || Character.getType(c) == Character.CONTROL) {
\r
1750 //System.out.println("Char " + c + " " + Character.getType(c) + " " + text);
\r
1751 insert(new String(new char[] {c}));
\r
1755 // FIXME This is called even if just caret was moved.
\r
1756 // This is currently necessary for repaints.
\r
1757 fireTextChanged();
\r
1758 invalidateXOffset();
\r
1762 protected String getLineSeparator() {
\r
1763 return System.getProperty("line.separator");
\r
1766 protected void selectAll() {
\r
1767 setCaret(0, false);
\r
1768 setCaret(text.length(), true);
\r
1771 protected transient int hoverClick = 0;
\r
1774 protected boolean mouseClicked(MouseClickEvent event) {
\r
1775 if (event.button != MouseClickEvent.LEFT_BUTTON)
\r
1778 if (hasState(STATE_HOVER)) {
\r
1780 if (hoverClick < 2)
\r
1782 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
\r
1783 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
\r
1786 IElement e = DiagramNodeUtil.getElement(ctx, this);
\r
1787 if (!hasState(STATE_EDITING)) {
\r
1788 if (Boolean.TRUE.equals(setEditMode(true))) {
\r
1789 editActivation = activateEdit(0, e, ctx);
\r
1795 if (hasState(STATE_EDITING)) {
\r
1796 fireTextEditingEnded();
\r
1802 protected boolean mouseDoubleClicked(MouseDoubleClickedEvent event) {
\r
1803 if (event.button != MouseClickEvent.LEFT_BUTTON)
\r
1806 if (hitTest(event, 0)) {
\r
1807 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
\r
1808 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
\r
1812 if (text != null) {
\r
1813 // Select the whole text.
\r
1814 setCaret(0, false);
\r
1815 setCaret(text.length(), true);
\r
1824 protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {
\r
1825 if (!hasState(STATE_EDITING))
\r
1828 Point2D local = controlToLocal( event.controlPosition );
\r
1829 // FIXME: once the event coordinate systems are cleared up, remove this workaround
\r
1830 local = parentToLocal(local);
\r
1831 if (hasState(STATE_HOVER) && this.containsLocal(local)) {
\r
1832 setCaret(local, event.isShiftDown());
\r
1838 protected boolean mouseMoved(MouseMovedEvent event) {
\r
1839 boolean hit = hitTest(event, 3.0);
\r
1840 if (hit != hasState(STATE_HOVER)) {
\r
1841 setState(STATE_HOVER, hit);
\r
1847 private boolean isControlDown(MouseEvent e) {
\r
1848 return e.isControlDown() || lastMouseEvent!=null?lastMouseEvent.isControlDown():false;
\r
1851 protected boolean isShiftDown(MouseEvent e) {
\r
1852 return e.isShiftDown() || lastMouseEvent!=null?lastMouseEvent.isShiftDown():false;
\r
1855 // private boolean isAltDown(MouseEvent e) {
\r
1856 // return e.isAltDown() || lastMouseEvent!=null?lastMouseEvent.isAltDown():false;
\r
1860 protected boolean mouseDragged(MouseDragBegin e) {
\r
1862 && (isControlDown(e) || isShiftDown(e))
\r
1863 && e.context instanceof NodeEventHandler
\r
1864 && (dataRVI != null || text != null))
\r
1866 List<Transferable> trs = new ArrayList<>(2);
\r
1867 if (dataRVI != null) {
\r
1868 trs.add(new LocalObjectTransferable(dataRVI));
\r
1869 trs.add(new PlaintextTransfer(dataRVI.toString()));
\r
1870 } else if (text != null && !text.isEmpty()) {
\r
1871 trs.add(new PlaintextTransfer(text));
\r
1873 if (!trs.isEmpty()) {
\r
1874 e.transferable = new MultiTransferable(trs);
\r
1881 protected boolean hitTest(MouseEvent event, double tolerance) {
\r
1882 Rectangle2D bounds = getBoundsInternal();
\r
1883 if (bounds == null)
\r
1885 Point2D localPos = NodeUtil.worldToLocal(this, event.controlPosition, new Point2D.Double());
\r
1886 double x = localPos.getX();
\r
1887 double y = localPos.getY();
\r
1888 boolean hit = bounds.contains(x, y);
\r
1892 public Rectangle2D getBoundsInternal() {
\r
1893 Rectangle2D local = lastBounds;
\r
1894 if (local == null)
\r
1896 // TODO: potential spot for CPU/memory allocation optimization
\r
1897 // by using more specialized implementations
\r
1898 if (transform.isIdentity())
\r
1900 return transform.createTransformedShape(local).getBounds2D();
\r
1903 protected Color add(Color c, int r, int g, int b) {
\r
1904 int nr = Math.min(255, c.getRed() + r);
\r
1905 int ng = Math.min(255, c.getGreen() + g);
\r
1906 int nb = Math.min(255, c.getBlue() + b);
\r
1907 return new Color(nr, ng, nb);
\r
1910 public TextEditActivation activateEdit(int mouseId, IElement e, ICanvasContext ctx) {
\r
1911 EditDataNode data = EditDataNode.getNode(this);
\r
1912 deactivateEdit(data, null);
\r
1913 TextEditActivation result = new TextEditActivation(mouseId, e, ctx);
\r
1914 data.setTextEditActivation(result);
\r
1919 * @return <code>true</code> if this node is or was previously in editing
\r
1922 protected boolean deactivateEdit() {
\r
1923 boolean result = deactivateEdit( editActivation );
\r
1924 result |= editActivation != null;
\r
1925 editActivation = null;
\r
1929 protected boolean deactivateEdit(TextEditActivation activation) {
\r
1930 return deactivateEdit( EditDataNode.getNode(this), activation );
\r
1933 protected boolean deactivateEdit(EditDataNode data, TextEditActivation activation) {
\r
1934 TextEditActivation previous = data.getTextEditActivation();
\r
1935 if (previous != null && (previous == activation || activation == null)) {
\r
1936 previous.release();
\r
1937 data.setTextEditActivation(null);
\r
1944 public int getEventMask() {
\r
1945 return EventTypes.KeyPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask
\r
1946 | EventTypes.MouseClickMask | EventTypes.MouseDragBeginMask | EventTypes.CommandMask;
\r
1949 private MouseEvent lastMouseEvent = null;
\r
1952 public boolean handleEvent(Event e) {
\r
1953 if(e instanceof MouseEvent && !(e instanceof MouseDragBegin)) lastMouseEvent = (MouseEvent)e;
\r
1954 return super.handleEvent(e);
\r
1958 public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
\r
1959 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
\r
1963 public <T> T getProperty(String propertyName) {
\r
1968 public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
\r
1971 public void synchronizeText(String text) {
\r
1975 public void synchronizeColor(RGB.Integer color) {
\r
1976 this.color = Colors.awt(color);
\r
1979 public void synchronizeFont(org.simantics.datatypes.literal.Font font) {
\r
1980 setFont(Fonts.awt(font));
\r
1983 public void synchronizeTransform(double[] data) {
\r
1984 this.setTransform(new AffineTransform(data));
\r
1987 public static void main(String[] args) {
\r
1988 Line[] lines = parseLines("\n \n FOO \n\nBAR\n\n\n BAZ\n\n");
\r
1989 System.out.println(Arrays.toString(lines));
\r
1990 System.out.println(GeometryUtils.pointToMillimeter(1));
\r
1991 System.out.println(GeometryUtils.pointToMillimeter(12));
\r
1992 System.out.println(GeometryUtils.pointToMillimeter(72));
\r
1995 ///////////////////////////////////////////////////////////////////////////
\r
1996 // LEGACY CODE NEEDED BY INHERITED CLASSES FOR NOW
\r
1997 ///////////////////////////////////////////////////////////////////////////
\r
1999 protected double getHorizontalAlignOffset(Rectangle2D r) {
\r
2000 switch (horizontalAlignment) {
\r
2001 case 0: return 0; // Leading
\r
2002 case 1: return -r.getWidth(); // Trailing
\r
2003 case 2: return -r.getCenterX(); // Center
\r
2004 default: return 0;
\r
2008 protected double getVerticalAlignOffset() {
\r
2009 FontMetrics fm = fontMetrics;
\r
2012 switch (verticalAlignment) {
\r
2013 case 0: return fm.getMaxAscent(); // Leading=top=maxascent
\r
2014 case 1: return -fm.getMaxDescent(); // Trailing=bottom=maxdescent
\r
2015 case 2: return fm.getMaxAscent() / 2; // Center=maxascent / 2
\r
2017 default: return 0;
\r
2021 ///////////////////////////////////////////////////////////////////////////
\r
2022 // LEGACY CODE ENDS
\r
2023 ///////////////////////////////////////////////////////////////////////////
\r