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 gnu.trove.list.array.TIntArrayList;
\r
16 import java.awt.AlphaComposite;
\r
17 import java.awt.BasicStroke;
\r
18 import java.awt.Color;
\r
19 import java.awt.Composite;
\r
20 import java.awt.Font;
\r
21 import java.awt.FontMetrics;
\r
22 import java.awt.Graphics2D;
\r
23 import java.awt.RenderingHints;
\r
24 import java.awt.Shape;
\r
25 import java.awt.Toolkit;
\r
26 import java.awt.datatransfer.Clipboard;
\r
27 import java.awt.datatransfer.DataFlavor;
\r
28 import java.awt.datatransfer.StringSelection;
\r
29 import java.awt.datatransfer.Transferable;
\r
30 import java.awt.event.KeyEvent;
\r
31 import java.awt.font.FontRenderContext;
\r
32 import java.awt.font.LineBreakMeasurer;
\r
33 import java.awt.font.TextAttribute;
\r
34 import java.awt.font.TextHitInfo;
\r
35 import java.awt.font.TextLayout;
\r
36 import java.awt.geom.AffineTransform;
\r
37 import java.awt.geom.Point2D;
\r
38 import java.awt.geom.Rectangle2D;
\r
39 import java.io.IOException;
\r
40 import java.text.AttributedCharacterIterator;
\r
41 import java.text.AttributedString;
\r
42 import java.util.ArrayList;
\r
43 import java.util.Arrays;
\r
44 import java.util.Hashtable;
\r
46 import org.simantics.datatypes.literal.RGB;
\r
47 import org.simantics.db.layer0.variable.RVI;
\r
48 import org.simantics.diagram.elements.Line.BoundsProcedure;
\r
49 import org.simantics.g2d.canvas.ICanvasContext;
\r
50 import org.simantics.g2d.element.IElement;
\r
51 import org.simantics.scenegraph.IDynamicSelectionPainterNode;
\r
52 import org.simantics.scenegraph.LoaderNode;
\r
53 import org.simantics.scenegraph.ScenegraphUtils;
\r
54 import org.simantics.scenegraph.g2d.G2DNode;
\r
55 import org.simantics.scenegraph.g2d.G2DPDFRenderingHints;
\r
56 import org.simantics.scenegraph.g2d.events.Event;
\r
57 import org.simantics.scenegraph.g2d.events.EventTypes;
\r
58 import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
\r
59 import org.simantics.scenegraph.g2d.events.MouseEvent;
\r
60 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonPressedEvent;
\r
61 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseClickEvent;
\r
62 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDoubleClickedEvent;
\r
63 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseDragBegin;
\r
64 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseMovedEvent;
\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.g2d.events.NodeEventHandler;
\r
68 import org.simantics.scenegraph.utils.GeometryUtils;
\r
69 import org.simantics.scenegraph.utils.NodeUtil;
\r
70 import org.simantics.scl.runtime.function.Function1;
\r
71 import org.simantics.scl.runtime.function.Function2;
\r
72 import org.simantics.ui.colors.Colors;
\r
73 import org.simantics.ui.dnd.LocalObjectTransferable;
\r
74 import org.simantics.ui.fonts.Fonts;
\r
75 import org.simantics.utils.threads.AWTThread;
\r
77 import com.lowagie.text.DocumentException;
\r
78 import com.lowagie.text.Element;
\r
79 import com.lowagie.text.Rectangle;
\r
80 import com.lowagie.text.pdf.FontMapper;
\r
81 import com.lowagie.text.pdf.PdfFormField;
\r
82 import com.lowagie.text.pdf.PdfWriter;
\r
83 import com.lowagie.text.pdf.TextField;
\r
87 * TextNode which supports in-line editing.
\r
89 * By default <code>TextNode</code> is in editable = false state. Use
\r
90 * {@link #setEditable(boolean)} to make it editable.
\r
92 * @author Hannu Niemistö <hannu.niemisto@vtt.fi>
\r
93 * @author Marko Luukkainen <marko.luukkainen@vtt.fi>
\r
94 * @author Tuukka Lehtonen <tuukka.lehtonen@semantum.fi>
\r
97 * o proper support for defining clipping bounds for the text (needed for page templates) (currently through fixedWidth)
\r
98 * o fix editing xOffset to work with fixed width and multi-line text
\r
104 public class TextNode extends G2DNode implements IDynamicSelectionPainterNode, LoaderNode {
\r
106 private static final long serialVersionUID = 654692698101485672L;
\r
109 * TODO: justify existence for this
\r
111 private static final BasicStroke RESET_STROKE = new BasicStroke(1);
\r
114 * Src-over alpha composite instance with 50% opacity.
\r
116 private static final AlphaComposite SrcOver_50 = AlphaComposite.SrcOver.derive(0.5f);
\r
119 * For (inexact) measurement of rendered text bounds.
\r
121 protected static final FontRenderContext FRC = new FontRenderContext(new AffineTransform(), true, true);
\r
123 private static final Font FONT = Font.decode("Arial 6");
\r
124 private static final Color SELECTION_BACKGROUND_COLOR = new Color(0x316ac5);
\r
125 // private static final double MAX_CARET_POSITION = 1.0;
\r
128 * The complete text visualized by this node.
\r
130 protected String text = null;
\r
133 * The font used to render the {@link #text}.
\r
135 protected Font font = FONT;
\r
138 * The color of the rendered text. Default value is {@value Color#black}.
\r
140 protected Color color = Color.BLACK;
\r
143 * The background color used for filling the background of the bounding box
\r
144 * of the rendered text. <code>null</code> means no fill.
\r
145 * Default value is <code>null</code>.
\r
147 protected Color backgroundColor = null;
\r
150 * The color used for drawing the expanded bounding box border for the
\r
151 * rendered text. <code>null</code> means no border is rendered. Default
\r
152 * value is <code>null</code>.
\r
154 protected Color borderColor = null;
\r
156 protected double scale = 1.0;
\r
157 protected transient double scaleRecip = 1.0;
\r
162 protected float borderWidth = 0.f;
\r
164 protected double paddingX = 2.0;
\r
165 protected double paddingY = 2.0;
\r
168 * Horizontal text box alignment with respect to its origin. Default value is
\r
171 protected byte horizontalAlignment = 0;
\r
173 * Vertical text box alignment with respect to its origin. Default value is
\r
176 protected byte verticalAlignment = 3;
\r
179 * Tells if this node is still pending for real results or not.
\r
181 protected static final int STATE_PENDING = (1 << 0);
\r
182 protected static final int STATE_HOVER = (1 << 1);
\r
183 protected static final int STATE_EDITABLE = (1 << 2);
\r
184 protected static final int STATE_SHOW_SELECTION = (1 << 3);
\r
185 protected static final int STATE_WRAP_TEXT = (1 << 4);
\r
186 protected transient static final int STATE_EDITING = (1 << 5);
\r
187 protected transient static final int STATE_VALID = (1 << 6);
\r
188 protected transient static final int STATE_X_OFFSET_IS_DIRTY = (1 << 7);
\r
189 protected static final int STATE_ALWAYS_ADD_LISTENERS = (1 << 8);
\r
190 protected static final int STATE_LISTENERS_ADDED = (1 << 9);
\r
193 * A combination of all the STATE_ constants defined in this class,
\r
194 * e.g. {@link #STATE_PENDING}.
\r
196 protected int state = STATE_SHOW_SELECTION | STATE_WRAP_TEXT | STATE_VALID | STATE_X_OFFSET_IS_DIRTY;
\r
198 protected RVI dataRVI = null;
\r
201 int selectionTail = 0;
\r
204 float fixedWidth = 0f;
\r
206 private Rectangle2D targetBounds;
\r
208 Function1<String, String> validator;
\r
209 ITextListener textListener;
\r
210 ITextContentFilter editContentFilter;
\r
213 * The renderable line structures parsed from {@link #text} by
\r
214 * {@link #parseLines(String)}, laid out by
\r
215 * {@link #layoutLines(Line[], FontRenderContext)} and aligned by
\r
216 * {@link #alignLines(Line[], Rectangle2D, byte, byte)}
\r
218 protected transient Line[] lines = null;
\r
219 protected transient FontMetrics fontMetrics = null;
\r
222 * Stores the value of {@link #text} before edit mode was last entered. Used
\r
223 * for restoring the original value if editing is cancelled.
\r
225 private transient String textBeforeEdit = null;
\r
226 protected transient TextEditActivation editActivation;
\r
229 * Stores the last scaled bounds.
\r
231 private transient Rectangle2D lastBounds = new Rectangle2D.Double();
\r
234 * This must be nullified if anything that affects the result of
\r
235 * {@link #getTightAlignedBoundsInLocal(Rectangle2D, FontRenderContext)}
\r
236 * changes. It will cause the cached value to be recalculated on the next
\r
239 private transient Rectangle2D tightBoundsCache = null;
\r
242 public void init() {
\r
244 // Mark this node as pending
\r
245 NodeUtil.increasePending(this);
\r
249 public void cleanup() {
\r
254 protected boolean hasState(int flags) {
\r
255 return (state & flags) == flags;
\r
258 protected void setState(int flags) {
\r
259 this.state |= flags;
\r
262 protected void setState(int flags, boolean set) {
\r
264 this.state |= flags;
\r
266 this.state &= ~flags;
\r
269 protected void clearState(int flags) {
\r
270 this.state &= ~flags;
\r
273 protected void setListeners(boolean add) {
\r
280 protected void addListeners() {
\r
281 if (!hasState(STATE_LISTENERS_ADDED)) {
\r
282 addEventHandler(this);
\r
283 setState(STATE_LISTENERS_ADDED);
\r
287 protected void removeListeners() {
\r
288 if (hasState(STATE_LISTENERS_ADDED)) {
\r
289 removeEventHandler(this);
\r
290 clearState(STATE_LISTENERS_ADDED);
\r
295 * 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
298 public void setForceEventListening(boolean force) {
\r
299 setState(STATE_ALWAYS_ADD_LISTENERS, force);
\r
300 if (force && !hasState(STATE_EDITABLE)) {
\r
301 setListeners(force);
\r
306 * Enables or disables edit mode. It also sets
\r
307 * the caret at the end of text all selects the
\r
308 * whole text (this is the usual convention when
\r
309 * beginning to edit one line texts).
\r
311 * @return null if no change to edit state was made
\r
313 public Boolean setEditMode(boolean edit) {
\r
314 return setEditMode(edit, true);
\r
318 * Enables or disables edit mode. It also sets
\r
319 * the caret at the end of text all selects the
\r
320 * whole text (this is the usual convention when
\r
321 * beginning to edit one line texts).
\r
323 * @return null if no change to edit state was made
\r
325 protected Boolean setEditMode(boolean edit, boolean notify) {
\r
326 if (edit && !hasState(STATE_EDITABLE))
\r
328 if (hasState(STATE_EDITING) == edit)
\r
330 setState(STATE_EDITING);
\r
332 caret = text != null ? text.length() : 0;
\r
334 textBeforeEdit = text;
\r
336 fireTextEditingStarted();
\r
337 return Boolean.TRUE;
\r
340 fireTextEditingEnded();
\r
341 return Boolean.FALSE;
\r
345 @SyncField({"editable"})
\r
346 public void setEditable(boolean editable) {
\r
347 boolean changed = hasState(STATE_EDITABLE) != editable;
\r
348 setState(STATE_EDITABLE, editable);
\r
349 if (hasState(STATE_EDITING) && !editable)
\r
350 setEditMode(false);
\r
351 if (changed && !hasState(STATE_ALWAYS_ADD_LISTENERS)) {
\r
352 setListeners(editable);
\r
356 public boolean isEditable() {
\r
357 return hasState(STATE_EDITABLE);
\r
360 public boolean isEditMode() {
\r
361 return hasState(STATE_EDITING);
\r
364 @SyncField({"wrapText"})
\r
365 public void setWrapText(boolean wrapText) {
\r
366 setState(STATE_WRAP_TEXT, wrapText);
\r
370 * @return Does the text box wrap text if
\r
371 * the width of the box is fixed
\r
373 public boolean isWrapText() {
\r
374 return hasState(STATE_WRAP_TEXT);
\r
377 @SyncField({"showSelection"})
\r
378 public void setShowSelection(boolean showSelection) {
\r
379 setState(STATE_SHOW_SELECTION, showSelection);
\r
382 public boolean showsSelection() {
\r
383 return hasState(STATE_SHOW_SELECTION);
\r
390 * @param x not supported anymore, use {@link #setTransform(AffineTransform)} instead
\r
391 * @param y not supported anymore, use {@link #setTransform(AffineTransform)} instead
\r
394 @SyncField({"text", "font", "color", "x", "y", "scale"})
\r
395 public void init(String text, Font font, Color color, double x, double y, double scale) {
\r
396 // no value => value
\r
397 if(this.text == null && text != null) NodeUtil.decreasePending(this);
\r
399 if (hasState(STATE_EDITING))
\r
402 this.text = new String(text != null ? text : "");
\r
404 this.color = color;
\r
405 this.scale = scale;
\r
406 this.scaleRecip = 1.0 / scale;
\r
408 this.selectionTail = 0;
\r
413 @SyncField({"paddingX", "paddingY"})
\r
414 public void setPadding(double x, double y) {
\r
419 @SyncField({"color"})
\r
420 public void setColor(Color color) {
\r
421 this.color = color;
\r
424 @SyncField({"backgroundColor"})
\r
425 public void setBackgroundColor(Color color) {
\r
426 this.backgroundColor = color;
\r
429 @SyncField({"borderColor"})
\r
430 public void setBorderColor(Color color) {
\r
431 this.borderColor = color;
\r
434 public String getText() {
\r
438 public String getTextBeforeEdit() {
\r
439 return textBeforeEdit;
\r
442 @SyncField({"text","caret","selectionTail"})
\r
443 public void setText(String text) {
\r
444 //System.out.println("TextNode.setText('" + text + "', " + editing + ")");
\r
445 if (hasState(STATE_EDITING))
\r
448 // value => no value
\r
449 if(this.text != null && text == null) NodeUtil.increasePending(this);
\r
450 // no value => value
\r
451 if(this.text == null && text != null) NodeUtil.decreasePending(this);
\r
453 this.text = text != null ? text : "";
\r
454 caret = Math.min(caret, this.text.length());
\r
455 selectionTail = caret;
\r
460 @SyncField({"pending"})
\r
461 public void setPending(boolean pending) {
\r
462 boolean p = hasState(STATE_PENDING);
\r
463 if(!p && pending) NodeUtil.increasePending(this);
\r
464 if(p && !pending) NodeUtil.decreasePending(this);
\r
466 setState(STATE_PENDING, pending);
\r
469 @SyncField({"fixedWidth"})
\r
470 public void setFixedWidth(float fixedWidth) {
\r
471 if (fixedWidth < 0f)
\r
472 throw new IllegalArgumentException("negative fixed width");
\r
473 this.fixedWidth = fixedWidth;
\r
474 invalidateXOffset();
\r
478 * Bounds where the text box will be drawn
\r
481 public void setTargetBounds(Rectangle2D bounds) {
\r
482 this.targetBounds = bounds;
\r
485 final public void synchronizeWidth(float width) {
\r
487 setFixedWidth(width);
\r
490 final public void synchronizeBorderWidth(float width) {
\r
492 setBorderWidth(width);
\r
495 public final void synchronizeWrapText(boolean wrap) {
\r
496 setState(STATE_WRAP_TEXT, wrap);
\r
499 public boolean isHovering() {
\r
500 return hasState(STATE_HOVER);
\r
503 @SyncField({"hover"})
\r
504 public void setHover(boolean hover) {
\r
505 setState(STATE_HOVER, hover);
\r
509 public Font getFont() {
\r
513 @SyncField({"font"})
\r
514 public void setFont(Font font) {
\r
519 public double getBorderWidth() {
\r
520 return borderWidth;
\r
523 @SyncField({"borderWidth"})
\r
524 public void setBorderWidth(float width) {
\r
525 this.borderWidth = width;
\r
528 public void setBorderWidth(double width) {
\r
529 setBorderWidth((float)width);
\r
532 @SyncField({"horizontalAlignment"})
\r
533 public void setHorizontalAlignment(byte horizontalAlignment) {
\r
534 if (horizontalAlignment < 0 && horizontalAlignment > 2)
\r
535 throw new IllegalArgumentException("Invalid horizontal alignment: " + horizontalAlignment + ", must be between 0 and 2");
\r
536 this.horizontalAlignment = horizontalAlignment;
\r
540 final public void synchronizeHorizontalAlignment(byte horizontalAlignment) {
\r
541 if (horizontalAlignment >= 0 && horizontalAlignment <= 2)
\r
542 setHorizontalAlignment(horizontalAlignment);
\r
545 public byte getHorizontalAlignment() {
\r
546 return horizontalAlignment;
\r
549 @SyncField({"verticalAlignment"})
\r
550 public void setVerticalAlignment(byte verticalAlignment) {
\r
551 if (verticalAlignment < 0 && verticalAlignment > 3)
\r
552 throw new IllegalArgumentException("Invalid vertical alignment: " + verticalAlignment + ", must be between 0 and 3");
\r
553 this.verticalAlignment = verticalAlignment;
\r
557 final public void synchronizeVerticalAlignment(byte verticalAlignment) {
\r
558 if (verticalAlignment >= 0 && verticalAlignment <= 3)
\r
559 setVerticalAlignment(verticalAlignment);
\r
562 public byte getVerticalAlignment() {
\r
563 return verticalAlignment;
\r
567 * Rendering is single-threaded so we can use a static rectangle for
\r
568 * calculating the expanded bounds for the node.
\r
570 private static transient ThreadLocal<Rectangle2D> tempBounds = new ThreadLocal<Rectangle2D>() {
\r
572 protected Rectangle2D initialValue() {
\r
573 return new Rectangle2D.Double();
\r
578 * Rendering is single-threaded so we can use a static AffineTransform to
\r
579 * prevent continuous memory allocation during text rendering.
\r
581 private static transient ThreadLocal<AffineTransform> tempAffineTransform = new ThreadLocal<AffineTransform>() {
\r
583 protected AffineTransform initialValue() {
\r
584 return new AffineTransform();
\r
589 public void render(Graphics2D g) {
\r
590 AffineTransform ot = g.getTransform();
\r
592 g.setTransform(ot);
\r
596 * Note: does not return transformation, stroke, color, etc. to their
\r
600 * @param applyTransform
\r
602 public void render(Graphics2D g, boolean applyTransform) {
\r
603 if (text == null || font == null || color == null)
\r
606 // Cache font metrics if necessary
\r
607 if (fontMetrics == null)
\r
608 fontMetrics = g.getFontMetrics(font);
\r
610 Color color = this.color;
\r
611 boolean isSelected = NodeUtil.isSelected(this, 1);
\r
612 boolean hover = hasState(STATE_HOVER);
\r
613 boolean editing = hasState(STATE_EDITING);
\r
615 if (!isSelected && hover) {
\r
616 color = add(color, 120, 120, 120);
\r
619 if (applyTransform)
\r
620 g.transform(transform);
\r
621 // Apply separate legacy scale
\r
623 g.scale(scale, scale);
\r
625 // Safety for not rendering when the scale of this text is too small.
\r
626 // When the scale is too small it will cause internal exceptions while
\r
628 double currentScale = GeometryUtils.getScale(g.getTransform());
\r
629 //System.out.println("currentScale: " + currentScale);
\r
630 if (currentScale < 1e-6)
\r
634 //g.translate(x, y);
\r
636 // Calculate text clip rectangle.
\r
637 // This updates textLayout if necessary.
\r
638 Rectangle2D r = getTightAlignedBoundsInLocal(tempBounds.get(), fontMetrics.getFontRenderContext());
\r
640 computeEditingXOffset();
\r
642 if (fixedWidth > 0f)
\r
643 r.setFrame(r.getMinX(), r.getMinY(), fixedWidth, r.getHeight());
\r
644 if(targetBounds != null) {
\r
645 double w = (targetBounds.getWidth() - paddingX * 2) * scaleRecip;
\r
646 double h = (targetBounds.getHeight() - paddingY * 2) * scaleRecip;
\r
647 double x = (targetBounds.getMinX() + paddingX) * scaleRecip;
\r
648 double y = (targetBounds.getMinY() + paddingY) * scaleRecip;
\r
649 r.setRect(x, y, w, h);
\r
652 Rectangle2D textClip = r.getBounds2D();
\r
654 expandBoundsUnscaled(r);
\r
656 // Speed rendering optimization: don't draw text that is too small to
\r
657 // read when not editing
\r
658 boolean renderText = true;
\r
660 Object renderingHint = g.getRenderingHint(RenderingHints.KEY_RENDERING);
\r
661 if (renderingHint != RenderingHints.VALUE_RENDER_QUALITY) {
\r
662 float textSizeMM = (float) currentScale * GeometryUtils.pointToMillimeter(font.getSize2D());
\r
663 if (textSizeMM < 1.5f)
\r
664 renderText = false;
\r
668 Shape clipSave = g.getClip();
\r
669 g.setClip(textClip);
\r
672 PdfWriter writer = (PdfWriter) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_WRITER);
\r
673 boolean isPdfField = false;
\r
674 String fieldName = null;
\r
675 if (writer != null) {
\r
676 // TODO: replace this hack with proper text field name field
\r
677 fieldName = NodeUtil.getNodeName(this);
\r
678 isPdfField = ( fieldName.equals("approved_by") ||
\r
679 fieldName.equals("checked_by") ||
\r
680 fieldName.equals("designer name") ||
\r
681 fieldName.equals("created_by") );
\r
684 Color backgroundColor = hasState(STATE_VALID) ? this.backgroundColor : Color.red;
\r
687 if ( !isPdfField ) {
\r
689 // Fill background if necessary
\r
690 if (backgroundColor != null) {
\r
691 g.setColor(backgroundColor);
\r
697 int selectionMin = Math.min(caret, selectionTail);
\r
698 int selectionMax = Math.max(caret, selectionTail);
\r
702 renderText(g, xOffset);
\r
704 Shape clip = g.getClip();
\r
706 // Selection background & text
\r
707 for (Line line : lines) {
\r
708 if (line.intersectsRange(selectionMin, selectionMax)) {
\r
709 Shape selShape = line.getLogicalHighlightShape(selectionMin, selectionMax);
\r
710 line.translate(g, xOffset, 0);
\r
711 g.setClip(selShape);
\r
712 g.setColor(SELECTION_BACKGROUND_COLOR);
\r
714 g.setColor(Color.WHITE);
\r
715 //line.layout.draw(g, 0, 0);
\r
716 g.drawString(line.getText(), 0, 0);
\r
717 line.translateInv(g, xOffset, 0);
\r
738 // TODO: multiline support
\r
740 AffineTransform at = g.getTransform();
\r
741 float height = writer.getPageSize().getHeight();
\r
742 Rectangle2D rr = textClip;
\r
743 // Point2D pt1 = new Point2D.Double(rr.getX(), rr.getY()+rr.getHeight());
\r
744 // Point2D pt2 = new Point2D.Double(rr.getX()+rr.getWidth(), rr.getY());
\r
745 Point2D pt1 = new Point2D.Double(0, 0);
\r
746 Point2D pt2 = new Point2D.Double(47.f/*+rr.getWidth()*/, -rr.getHeight());
\r
747 pt1 = at.transform(pt1, pt1);
\r
748 pt2 = at.transform(pt2, pt2);
\r
749 Rectangle rectangle = new Rectangle(
\r
750 (float) pt1.getX(),
\r
751 height-(float) pt1.getY(),
\r
752 (float) pt2.getX(),
\r
753 height-(float) pt2.getY());
\r
755 FontMapper mapper = (FontMapper) g.getRenderingHint(G2DPDFRenderingHints.KEY_PDF_FONTMAPPER);
\r
756 // FontMetrics fm = g.getFontMetrics(font);
\r
758 // TODO Oikea leveys
\r
759 // TODO Uniikki nimi
\r
761 PdfFormField field = PdfFormField.createTextField(writer, false, false, 20);
\r
762 field.setFieldName(this.getId().toString());
\r
763 field.setWidget(rectangle, PdfAnnotation.HIGHLIGHT_NONE);
\r
764 field.setQuadding(PdfFormField.Q_RIGHT);
\r
765 field.setFieldFlags(PdfFormField.FF_READ_ONLY);
\r
766 field.setRotate(90);
\r
767 writer.addAnnotation(field);
\r
774 PdfFormField field = PdfFormField.createSignature(writer);
\r
775 field.setWidget(rectangle, PdfAnnotation.HIGHLIGHT_NONE);
\r
776 field.setFieldName(fieldName);
\r
777 field.setQuadding(PdfFormField.Q_LEFT);
\r
778 field.setFlags(PdfAnnotation.FLAGS_PRINT);
\r
779 //field.setFieldFlags(PdfFormField.FF_READ_ONLY)
\r
780 field.setFieldFlags(PdfFormField.FF_EDIT);
\r
782 field.setMKBackgroundColor( backgroundColor!=null?Color.WHITE:backgroundColor );
\r
783 PdfAppearance tp = PdfAppearance.createAppearance(writer, 72, 48);
\r
784 tp.rectangle(rectangle);
\r
786 field.setAppearance(PdfAnnotation.APPEARANCE_NORMAL, tp);
\r
787 writer.addAnnotation(field);
\r
792 TextField textField = new TextField(writer, rectangle, fieldName);
\r
793 textField.setFieldName(fieldName);
\r
794 textField.setFont(mapper.awtToPdf(font));
\r
795 textField.setBorderStyle(0);
\r
796 //textField.setAlignment(Element.ALIGN_LEFT);
\r
797 textField.setAlignment(Element.ALIGN_BOTTOM);
\r
798 textField.setRotation(90);
\r
799 textField.setOptions(TextField.EDIT|TextField.DO_NOT_SPELL_CHECK);
\r
800 if ( text!=null ) {
\r
801 textField.setText(text);
\r
803 if ( color!=null ) {
\r
804 textField.setTextColor(color);
\r
806 textField.setBackgroundColor( backgroundColor!=null?Color.WHITE:backgroundColor );
\r
807 PdfFormField field = textField.getTextField();
\r
808 writer.addAnnotation(field);
\r
809 } catch (IOException e) {
\r
810 e.printStackTrace();
\r
811 } catch (DocumentException e) {
\r
812 e.printStackTrace();
\r
816 // } catch (IOException e) {
\r
817 // // TODO Auto-generated catch block
\r
818 // e.printStackTrace();
\r
819 // } catch (DocumentException e) {
\r
820 // // TODO Auto-generated catch block
\r
821 // e.printStackTrace();
\r
826 g.setClip(clipSave);
\r
828 if (borderWidth > 0f && borderColor != null) {
\r
829 g.setColor(borderColor);
\r
830 g.setStroke(new BasicStroke((float) (scale*borderWidth)));
\r
834 //System.out.println("bw: " + borderWidth);
\r
835 if (isSelected && showsSelection()) {
\r
836 Composite oc = g.getComposite();
\r
837 g.setComposite(SrcOver_50);
\r
838 g.setColor(Color.RED);
\r
839 float bw = borderWidth;
\r
840 double s = currentScale;
\r
842 bw = (float) (1f / s);
\r
846 g.setStroke(new BasicStroke(bw));
\r
848 //g.draw(GeometryUtils.expandRectangle(r, 1.0));
\r
850 g.setComposite(oc);
\r
853 g.scale(scaleRecip, scaleRecip);
\r
854 g.setStroke(RESET_STROKE);
\r
856 lastBounds = getScaledOffsetBounds(r, lastBounds, scale, 0, 0);
\r
857 // g.setColor(Color.MAGENTA); // DEBUG
\r
858 // g.draw(lastBounds); // DEBUG
\r
859 // g.setColor(Color.ORANGE); // DEBUG
\r
860 // g.draw(getBoundsInLocal()); // DEBUG
\r
862 renderSelectedHover(g, isSelected, hover);
\r
865 private void renderCaret(Graphics2D g) {
\r
866 g.setColor(Color.BLACK);
\r
867 for (int i = 0; i < lines.length; i++) {
\r
868 Line line = lines[i];
\r
869 // prevent rendering caret twice on line changes
\r
870 if (line.containsOffset(caret) && // line contains caret
\r
871 (caret != line.endOffset || //caret is not in the end of the line
\r
872 i == lines.length-1 || //caret is end of the last line
\r
873 lines[i+1].startOffset != line.endOffset)) { // beginning of the next line does not start withe the same index as current line
\r
874 Shape[] caretShape = line.getCaretShapes(caret);
\r
875 line.translate(g, xOffset, 0);
\r
876 g.draw(caretShape[0]);
\r
877 if (caretShape[1] != null)
\r
878 g.draw(caretShape[1]);
\r
879 line.translateInv(g, xOffset, 0);
\r
883 private void renderText(Graphics2D g, float xOffset) {
\r
884 //g.draw(tightBoundsCache); // DEBUG
\r
885 for (Line line : lines) {
\r
886 //line.layout.draw(g, line.alignedPosX + xOffset, line.alignedPosY);
\r
887 g.drawString(line.getText(), line.alignedPosX + xOffset, line.alignedPosY);
\r
888 //g.draw(line.abbox); // DEBUG
\r
892 protected Rectangle2D getScaledOffsetBounds(Rectangle2D originalBounds, Rectangle2D dst, double scale, double offsetX, double offsetY) {
\r
893 AffineTransform btr = tempAffineTransform.get();
\r
894 btr.setToTranslation(offsetX*scale, offsetY*scale);
\r
895 btr.scale(scale, scale);
\r
896 if (btr.isIdentity()) {
\r
897 dst.setFrame(originalBounds);
\r
899 dst.setFrame(btr.createTransformedShape(originalBounds).getBounds2D());
\r
905 * Invoked when TextNode is selected and a mouse is hovering on top of it. Can be overridden to add custom rendering.
\r
909 protected void renderSelectedHover(Graphics2D g, boolean isSelected, boolean isHovering) {
\r
913 * Replaces the current selection with the content or inserts
\r
914 * the content at caret. After the insertion the caret
\r
915 * will be at the end of inserted text and selection will
\r
919 @SyncField({"text","caret","selectionTail"})
\r
920 protected void insert(String content) {
\r
921 content = editContentFilter != null ? editContentFilter.filter(this, content) : content;
\r
923 int selectionMin = Math.min(caret, selectionTail);
\r
924 int selectionMax = Math.max(caret, selectionTail);
\r
926 String begin = text.substring(0, selectionMin);
\r
927 String end = text.substring(selectionMax);
\r
928 text = begin + content + end;
\r
929 caret = selectionMin + content.length();
\r
930 selectionTail = caret;
\r
932 assert (caret <= text.length());
\r
933 //System.out.println(text + " " + caret );
\r
935 if(validator != null) {
\r
936 String error = validator.apply(text);
\r
937 setState(STATE_VALID, (error == null));
\r
944 protected void fireTextChanged() {
\r
945 if(textListener != null)
\r
946 textListener.textChanged();
\r
951 protected void fireTextEditingStarted() {
\r
952 if(textListener != null)
\r
953 textListener.textEditingStarted();
\r
957 protected void fireTextEditingCancelled() {
\r
958 setState(STATE_VALID);
\r
960 if (deactivateEdit()) {
\r
961 if (textListener != null)
\r
962 textListener.textEditingCancelled();
\r
964 setEditMode(false, false);
\r
966 if (textBeforeEdit != null)
\r
967 setText(textBeforeEdit);
\r
974 public void fireTextEditingEnded() {
\r
975 if (!hasState(STATE_VALID)) {
\r
976 fireTextEditingCancelled();
\r
977 setState(STATE_VALID);
\r
981 if (deactivateEdit()) {
\r
982 if (textListener != null)
\r
983 textListener.textEditingEnded();
\r
985 setEditMode(false, false);
\r
990 public void setTextListener(ITextListener listener) {
\r
991 this.textListener = listener;
\r
994 public void setValidator(Function1<String, String> validator) {
\r
995 this.validator = validator;
\r
998 public void setContentFilter(ITextContentFilter filter) {
\r
999 this.editContentFilter = filter;
\r
1002 public void setRVI(RVI rvi) {
\r
1003 this.dataRVI = rvi;
\r
1006 private void invalidateXOffset() {
\r
1007 setState(STATE_X_OFFSET_IS_DIRTY);
\r
1010 private void computeEditingXOffset() {
\r
1012 if(lines == null) return;
\r
1013 if(!hasState(STATE_X_OFFSET_IS_DIRTY)) return;
\r
1014 if(fixedWidth > 0f) {
\r
1016 // TODO: implement
\r
1017 // float[] coords = textLayout.getCaretInfo(TextHitInfo.afterOffset(caret));
\r
1018 // if(coords != null) {
\r
1019 // if(coords[0] > (fixedWidth*MAX_CARET_POSITION)) xOffset = (float)((fixedWidth*MAX_CARET_POSITION)-coords[0]);
\r
1020 // else xOffset = 0;
\r
1029 clearState(STATE_X_OFFSET_IS_DIRTY);
\r
1033 @SyncField({"caret","selectionTail"})
\r
1034 protected void moveCaret(int move, boolean select) {
\r
1035 // prevent setting caret into line separator.
\r
1037 while (text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
\r
1039 } else if (move < 0) {
\r
1040 while (caret+move >= 0 && text.length()> caret+move && getLineSeparator().indexOf(text.charAt(caret+move)) > 0)
\r
1046 if (caret > text.length())
\r
1047 caret = text.length();
\r
1049 selectionTail = caret;
\r
1052 private Line findCaretLine() {
\r
1053 // Find the line where caret is. Starting from first line.
\r
1054 for(int i = 0; i < lines.length; i++) {
\r
1055 Line line = lines[i];
\r
1056 if(caret <= line.endOffset) {
\r
1064 * Moves caret to next not letter or digit
\r
1065 * @param shiftDown
\r
1067 private void moveCaretCtrlLeft(boolean shiftDown) {
\r
1068 Line line = findCaretLine();
\r
1069 if(line != null) {
\r
1071 for(i = caret-1; i > line.startOffset; i--) {
\r
1072 char c = line.document.charAt(i);
\r
1073 if(!Character.isLetterOrDigit(c)) {
\r
1077 moveCaret(i - caret, shiftDown);
\r
1082 * Moves caret to previous non letter or digit
\r
1083 * @param shiftDown
\r
1085 private void moveCaretCtrlRight(boolean shiftDown) {
\r
1086 Line line = findCaretLine();
\r
1087 if(line != null) {
\r
1089 for(i = caret + 1; i < line.endOffset; i++) {
\r
1090 char c = line.document.charAt(i);
\r
1091 if(!Character.isLetterOrDigit(c)) {
\r
1095 moveCaret(i - caret, shiftDown);
\r
1100 * Moves caret to line end
\r
1101 * @param shiftDown
\r
1103 private void moveCaretEnd(boolean shiftDown) {
\r
1104 Line line = findCaretLine();
\r
1106 // Move caret to the end of the line
\r
1107 moveCaret(line.endOffset - caret, shiftDown);
\r
1111 * Moves caret to beginning of a line
\r
1112 * @param shiftDown
\r
1114 private void moveCaretHome(boolean shiftDown) {
\r
1115 Line line = findCaretLine();
\r
1117 // Move caret to the beginning of the line
\r
1118 moveCaret(line.startOffset - caret, shiftDown);
\r
1122 * Moves caret one row up and tries to maintain the location
\r
1123 * @param shiftDown
\r
1125 private void moveCaretRowUp(boolean shiftDown) {
\r
1126 // Find the line where caret is. Starting from first line.
\r
1127 for(int i = 0; i < lines.length; i++) {
\r
1128 Line line = lines[i];
\r
1129 if(caret <= line.endOffset) {
\r
1130 // caret is in this line
\r
1132 // Already on top line
\r
1133 // Select the beginning of the line
\r
1134 moveCaret(-caret, shiftDown);
\r
1136 Line prevLine = lines[i-1];
\r
1137 int prevLength = prevLine.endOffset - prevLine.startOffset;
\r
1138 int posInCurRow = caret - line.startOffset;
\r
1139 if(prevLength < posInCurRow)
\r
1140 posInCurRow = prevLength;
\r
1142 int newPos = prevLine.startOffset + posInCurRow;
\r
1143 moveCaret(newPos - caret, shiftDown);
\r
1151 * Moves caret one row down and tries to maintain the location
\r
1152 * @param shiftDown
\r
1154 private void moveCaretRowDown(boolean shiftDown) {
\r
1155 // Find the line where caret is. Starting from last line.
\r
1156 for(int i = lines.length - 1; i >= 0; i--) {
\r
1157 Line line = lines[i];
\r
1158 if(caret >= line.startOffset) {
\r
1159 // caret is in this line
\r
1160 if(i == lines.length - 1) {
\r
1161 // Already on bottom line, cannot go below
\r
1162 // Select to the end of the line
\r
1163 moveCaret(line.endOffset - caret, shiftDown);
\r
1165 Line prevLine = lines[i+1]; // Previous line
\r
1167 // Find new caret position.
\r
1168 // Either it is in the same index as before, or if the row
\r
1169 // is not long enough, select the end of the row.
\r
1170 int prevLength = prevLine.endOffset - prevLine.startOffset;
\r
1171 int posInCurRow = caret - line.startOffset;
\r
1172 if(prevLength < posInCurRow)
\r
1173 posInCurRow = prevLength;
\r
1174 int newPos = prevLine.startOffset + posInCurRow;
\r
1175 moveCaret(newPos - caret, shiftDown);
\r
1182 @SyncField({"caret","selectionTail"})
\r
1183 protected void setCaret(int pos, boolean select) {
\r
1187 if (caret > text.length())
\r
1188 caret = text.length();
\r
1190 selectionTail = caret;
\r
1193 protected void setCaret(Point2D point) {
\r
1194 setCaret(point, false);
\r
1197 @SyncField({"caret","selectionTail"})
\r
1198 protected void setCaret(Point2D point, boolean select) {
\r
1200 for(int i = 0; i < lines.length; i++) {
\r
1201 Line line = lines[i];
\r
1202 Rectangle2D bounds = line.abbox;
\r
1203 // Add heights of bboxes for determining the correct line
\r
1205 lineY = bounds.getY();
\r
1207 lineY += lines[i-1].abbox.getHeight();
\r
1209 double lineHeight = bounds.getHeight();
\r
1210 double hitY = point.getY() / scale;
\r
1211 if(hitY >= lineY && hitY <= lineY + lineHeight) {
\r
1212 // Hit is in this line
\r
1213 float x = (float)(point.getX() / scale) - (float)line.abbox.getX();
\r
1214 float y = (float)(point.getY() / scale - lineHeight * i) ;
\r
1215 TextHitInfo info = line.layout.hitTestChar(x, y);
\r
1216 caret = line.startOffset + info.getInsertionIndex();
\r
1217 if (caret > line.endOffset)
\r
1218 caret = line.endOffset;
\r
1220 selectionTail = caret;
\r
1225 invalidateXOffset();
\r
1226 assert (caret <= text.length());
\r
1230 public Rectangle2D getBoundsInLocal() {
\r
1231 if(targetBounds != null)
\r
1232 return targetBounds;
\r
1234 return expandBounds( getTightAlignedBoundsInLocal(null) );
\r
1237 protected Rectangle2D expandBounds(Rectangle2D r) {
\r
1238 r.setRect(r.getX() * scale - paddingX, r.getY() * scale -paddingY, r.getWidth()*scale + paddingX + paddingX, r.getHeight()*scale + paddingY + paddingY);
\r
1239 //System.out.println(" => " + r);
\r
1243 protected Rectangle2D expandBoundsUnscaled(Rectangle2D r) {
\r
1244 r.setRect(r.getX() - scaleRecip*paddingX, r.getY() -scaleRecip*paddingY, r.getWidth() + scaleRecip*paddingX + scaleRecip*paddingX, r.getHeight() + scaleRecip*paddingY + scaleRecip*paddingY);
\r
1245 //System.out.println(" => " + r);
\r
1249 protected Rectangle2D expandBounds(Rectangle2D r, double amount) {
\r
1250 r.setRect(r.getX() - amount, r.getY() - amount, r.getWidth() + 2*amount, r.getHeight() + 2*amount);
\r
1254 protected Rectangle2D expandBounds(Rectangle2D r, double left, double top, double right, double bottom) {
\r
1255 r.setRect(r.getX() - left, r.getY() - top, r.getWidth() + left + right, r.getHeight() + top + bottom);
\r
1259 private void resetCaches() {
\r
1260 this.tightBoundsCache = null;
\r
1261 this.lines = null;
\r
1262 this.fontMetrics = null;
\r
1266 * Returns the tight bounds around the current text using the current font
\r
1267 * in the specified rectangle. If the specified rectangle is
\r
1268 * <code>null</code> a new Rectangle2D.Double instance will be created.
\r
1273 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r) {
\r
1274 return getTightAlignedBoundsInLocal(r, FRC);
\r
1278 * Returns the tight bounds around the current text using the current font
\r
1279 * in the specified rectangle. If the specified rectangle is
\r
1280 * <code>null</code> a new Rectangle2D.Double instance will be created.
\r
1283 * the rectangle where the result of the method is placed or
\r
1284 * <code>null</code> to allocate new rectangle
\r
1285 * @param frc current font render context
\r
1286 * @return r or new Rectangle2D.Double instance containing the requested
\r
1289 protected Rectangle2D getTightAlignedBoundsInLocal(Rectangle2D r, FontRenderContext frc) {
\r
1291 r = new Rectangle2D.Double();
\r
1293 if (tightBoundsCache != null) {
\r
1294 r.setFrame(tightBoundsCache);
\r
1298 String txt = text;
\r
1299 if (font == null || txt == null) {
\r
1300 r.setFrame(0, 0, 2, 1);
\r
1304 //System.out.println("TextNode.getTightAlignedBoundsInLocal('" + txt + "')");
\r
1306 // Parse & layout (unaligned)
\r
1307 Line[] lines = null;
\r
1309 if(hasState(STATE_WRAP_TEXT)) {
\r
1310 float width = fixedWidth;
\r
1311 if(width <= 0 && targetBounds != null)
\r
1312 width = (float) (((targetBounds.getWidth() - 2*paddingX)) * scaleRecip);
\r
1314 lines = wrapLines(txt, font, width, frc);
\r
1318 lines = parseLines(txt);
\r
1319 this.lines = layoutLines(lines, frc);
\r
1321 // Calculate tight bounds based on unaligned layout
\r
1322 //System.out.println("Unaligned");
\r
1323 tightBoundsCache = calculateBounds(lines, Line.BBOX, null);
\r
1324 //System.out.println(" => " + tightBoundsCache);
\r
1326 this.lines = layoutLinesX(lines, tightBoundsCache);
\r
1327 // Align each line to the calculated tight bounds
\r
1328 this.lines = alignLines(this.lines, tightBoundsCache, horizontalAlignment, verticalAlignment);
\r
1330 // Calculate aligned bounds
\r
1331 //System.out.println("Aligned");
\r
1332 calculateBounds(lines, Line.ABBOX, tightBoundsCache);
\r
1334 r.setFrame(tightBoundsCache);
\r
1335 //System.out.println(" => " + tightBoundsCache);
\r
1343 * the bounding box of all the whole laid out text (only bbox
\r
1347 private Line[] layoutLinesX(Line[] lines, Rectangle2D bbox) {
\r
1348 int lineCount = lines.length;
\r
1349 for (int l = 0; l < lineCount; ++l) {
\r
1350 Line line = lines[l];
\r
1351 // Compute pen x position. If the paragraph is right-to-left we
\r
1352 // will align the TextLayouts to the right edge of the panel.
\r
1353 // Note: drawPosX is always where the LEFT of the text is placed.
\r
1354 // NOTE: This changes based on horizontal alignment
\r
1355 line.drawPosX = (float) (line.layout.isLeftToRight() ? 0f
\r
1356 : tightBoundsCache.getWidth() - line.layout.getAdvance());
\r
1363 * @param boundsProvider
\r
1367 private static Rectangle2D calculateBounds(Line[] lines, BoundsProcedure boundsProvider, Rectangle2D result) {
\r
1368 if (result == null)
\r
1369 result = new Rectangle2D.Double();
\r
1371 result.setFrame(0, 0, 0, 0);
\r
1373 for (Line line : lines) {
\r
1374 //System.out.println("line: " + line);
\r
1375 Rectangle2D bbox = boundsProvider.getBounds(line);
\r
1376 if (result.isEmpty())
\r
1377 result.setFrame(bbox);
\r
1379 Rectangle2D.union(result, bbox, result);
\r
1380 //System.out.println("bounds: " + result);
\r
1382 //System.out.println("final bounds: " + result);
\r
1392 * @return aligned lines
\r
1394 private Line[] alignLines(Line[] lines, Rectangle2D bbox, byte hAlign, byte vAlign) {
\r
1395 // System.out.println("horizontal align: " + Alignment.values()[hAlign]);
\r
1396 // System.out.println("vertical align : " + Alignment.values()[vAlign]);
\r
1397 // System.out.println("bbox: " + bbox);
\r
1399 // double ybase = 0;
\r
1400 if(targetBounds != null) {
\r
1401 /* In normal cases the bounding box moves when
\r
1402 * typing. If target bounds are set, the text
\r
1403 * is fitted into the box.
\r
1406 case 1: // Trailing
\r
1407 xbase = (targetBounds.getMaxX()-paddingX) * scaleRecip;
\r
1410 xbase = targetBounds.getCenterX() * scaleRecip;
\r
1412 default: // Leading / Baseline
\r
1419 for (Line line : lines) {
\r
1420 double xoffset = 0;
\r
1421 double yoffset = 0;
\r
1424 case 1: // Trailing
\r
1425 xoffset = xbase - line.bbox.getWidth();
\r
1428 xoffset = xbase - line.bbox.getWidth() / 2;
\r
1430 default: // Leading / Baseline
\r
1437 yoffset = line.layout.getAscent();
\r
1440 yoffset = -bbox.getHeight() + line.layout.getAscent();
\r
1443 yoffset = -bbox.getHeight() / 2 + line.layout.getAscent();
\r
1447 line.alignOffset(xoffset, yoffset);
\r
1457 private Line[] layoutLines(Line[] lines, FontRenderContext frc) {
\r
1458 TextLayout emptyRowLayout = null;
\r
1459 int lineCount = lines.length;
\r
1461 for (int l = 0; l < lineCount; ++l) {
\r
1462 Line line = lines[l];
\r
1463 String lineText = line.getText();
\r
1464 // " " because TextLayout requires non-empty text and
\r
1465 // We don't want zero size for the text.
\r
1466 if (lineText.isEmpty()) {
\r
1468 if (emptyRowLayout == null)
\r
1469 emptyRowLayout = new TextLayout(lineText, font, frc);
\r
1470 line.layout = emptyRowLayout;
\r
1472 line.layout = new TextLayout(lineText, font, frc);
\r
1475 //y += line.layout.getAscent();
\r
1476 line.drawPosY = y;
\r
1477 y += line.layout.getDescent() + line.layout.getLeading() + line.layout.getAscent();
\r
1479 Rectangle2D bbox = line.layout.getLogicalHighlightShape(0, lineText.length()).getBounds2D();
\r
1480 bbox.setFrame(bbox.getX(), bbox.getY() + line.drawPosY, bbox.getWidth(), bbox.getHeight());
\r
1488 * Splits the specified string into {@link Line} structures, one for each
\r
1489 * line in the input text. The returned lines are only partially defined,
\r
1490 * waiting to be laid out (see
\r
1491 * {@link #layoutLines(Line[], FontRenderContext)})
\r
1495 * @return parsed text lines as {@link Line} structures
\r
1496 * @see #layoutLines(Line[], FontRenderContext)
\r
1498 private static Line[] parseLines(String txt) {
\r
1499 int len = txt.length();
\r
1501 return new Line[] { new Line("", 0, 0) };
\r
1503 TIntArrayList lfpos = new TIntArrayList();
\r
1505 int lineCount = 1;
\r
1506 for (;pos < len; ++lineCount) {
\r
1507 int nextlf = txt.indexOf('\n', pos);
\r
1508 lfpos.add(nextlf != -1 ? nextlf : len);
\r
1513 Line[] lines = new Line[lineCount];
\r
1515 for (int i = 0; i < lineCount-1; ++i) {
\r
1516 int lf = lfpos.getQuick(i);
\r
1517 int cr = (lf > 0) && (txt.charAt(lf - 1) == '\r') ? lf - 1 : lf;
\r
1518 lines[i] = new Line(txt, pos, cr);
\r
1521 lines[lineCount - 1] = new Line(txt, pos, len);
\r
1527 private static Line[] wrapLines(String txt, Font font, float fixedWidth, FontRenderContext frc) {
\r
1528 if(txt == null || txt.isEmpty())
\r
1531 ArrayList<Line> lines =
\r
1532 new ArrayList<Line>();
\r
1534 Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
\r
1535 map.put(TextAttribute.FONT, font);
\r
1536 AttributedString attributedText = new AttributedString(txt.isEmpty() ? "__ERROR__" : txt, map);
\r
1538 AttributedCharacterIterator paragraph = attributedText.getIterator();
\r
1539 int paragraphStart = paragraph.getBeginIndex();
\r
1540 int paragraphEnd = paragraph.getEndIndex();
\r
1541 LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, frc);
\r
1543 float breakWidth = fixedWidth;
\r
1545 // Force text to be vertical, by setting break width to 1, if the text area is narrower than "GGGG"
\r
1547 // Set position to the index of the first character in the paragraph.
\r
1548 lineMeasurer.setPosition(paragraphStart);
\r
1550 // Get lines until the entire paragraph has been displayed.
\r
1551 int next, limit, charat, position = 0;
\r
1553 while ((position = lineMeasurer.getPosition()) < paragraphEnd) {
\r
1555 // Find possible line break and set it as a limit to the next layout
\r
1556 next = lineMeasurer.nextOffset(breakWidth);
\r
1558 charat = txt.indexOf(System.getProperty("line.separator"),position+1);
\r
1559 if(charat < next && charat != -1){
\r
1563 lineMeasurer.nextLayout(breakWidth, limit, false);
\r
1565 lines.add(new Line(txt, position, limit));
\r
1568 return lines.toArray(new Line[lines.size()]);
\r
1572 public String getClipboardContent() {
\r
1573 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
\r
1574 Transferable clipData = clipboard.getContents(this);
\r
1576 return (String) (clipData.getTransferData(DataFlavor.stringFlavor));
\r
1577 } catch (Exception ee) {
\r
1582 public void setClipboardContent(String content) {
\r
1583 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
\r
1584 StringSelection data = new StringSelection(content);
\r
1585 clipboard.setContents(data, data);
\r
1589 public String toString() {
\r
1590 return super.toString() + " [text=" + text + ", font=" + font + ", color=" + color + "]";
\r
1594 protected boolean handleCommand(CommandEvent e) {
\r
1595 if (!hasState(STATE_EDITING))
\r
1598 if (Commands.SELECT_ALL.equals(e.command)) {
\r
1606 protected boolean keyPressed(KeyPressedEvent event) {
\r
1607 if (!hasState(STATE_EDITING))
\r
1610 char c = event.character;
\r
1611 boolean ctrl = event.isControlDown();
\r
1612 boolean alt = event.isAltDown();
\r
1614 // System.out.println("Key pressed '" + (event.character == 0 ? "\\0" : "" + c) + "' " + event.keyCode + " - " + Integer.toBinaryString(event.stateMask));
\r
1615 // System.out.println("ctrl: " + ctrl);
\r
1616 // System.out.println("alt: " + alt);
\r
1617 if (ctrl && !alt) {
\r
1618 switch (event.keyCode) {
\r
1619 case KeyEvent.VK_C:
\r
1620 if (caret != selectionTail) {
\r
1621 int selectionMin = Math.min(caret, selectionTail);
\r
1622 int selectionMax = Math.max(caret, selectionTail);
\r
1623 setClipboardContent(text.substring(selectionMin, selectionMax));
\r
1627 case KeyEvent.VK_X:
\r
1628 if (caret != selectionTail) {
\r
1629 int selectionMin = Math.min(caret, selectionTail);
\r
1630 int selectionMax = Math.max(caret, selectionTail);
\r
1631 setClipboardContent(text.substring(selectionMin, selectionMax));
\r
1636 case KeyEvent.VK_RIGHT:
\r
1638 // '\'' has the same keycode as VK_RIGHT but when right
\r
1639 // arrow is pressed, event character is \0.
\r
1640 moveCaretCtrlRight(event.isShiftDown());
\r
1644 case KeyEvent.VK_LEFT:
\r
1645 moveCaretCtrlLeft(event.isShiftDown());
\r
1648 case KeyEvent.VK_V:
\r
1650 String content = getClipboardContent();
\r
1651 if(content != null)
\r
1656 // Replaced by #handleCommand
\r
1657 // case KeyEvent.VK_A:
\r
1663 case KeyEvent.VK_ENTER:
\r
1665 insert(getLineSeparator());
\r
1673 } else if (!ctrl && alt) {
\r
1676 switch (event.keyCode) {
\r
1677 case KeyEvent.VK_LEFT:
\r
1678 moveCaret(-1, event.isShiftDown());
\r
1680 case KeyEvent.VK_RIGHT:
\r
1682 // '\'' has the same keycode as VK_RIGHT but when right
\r
1683 // arrow is pressed, event character is \0.
\r
1684 moveCaret(1, event.isShiftDown());
\r
1687 // Intentional fallthrough to default case
\r
1688 case KeyEvent.VK_UP:
\r
1689 moveCaretRowUp(event.isShiftDown());
\r
1691 case KeyEvent.VK_DOWN:
\r
1692 moveCaretRowDown(event.isShiftDown());
\r
1694 case KeyEvent.VK_HOME:
\r
1695 moveCaretHome(event.isShiftDown());
\r
1697 case KeyEvent.VK_END:
\r
1698 moveCaretEnd(event.isShiftDown());
\r
1701 case KeyEvent.VK_ENTER:
\r
1702 fireTextEditingEnded();
\r
1705 case KeyEvent.VK_ESCAPE:
\r
1706 text = textBeforeEdit;
\r
1708 clearState(STATE_EDITING);
\r
1709 fireTextEditingCancelled();
\r
1712 case KeyEvent.VK_BACK_SPACE:
\r
1713 if(caret == selectionTail && caret > 0) {
\r
1714 // line separator may use multiple characters, we want to remove that with one command
\r
1715 String lineSep = getLineSeparator();
\r
1716 int index = lineSep.indexOf(text.charAt(caret-1));
\r
1720 caret-= (index+1);
\r
1721 selectionTail+= (lineSep.length()-index-1);
\r
1727 case KeyEvent.VK_DELETE:
\r
1728 if(caret == selectionTail && caret < text.length()) {
\r
1729 String lineSep = getLineSeparator();
\r
1730 int index = lineSep.indexOf(text.charAt(caret));
\r
1734 selectionTail-= index;
\r
1735 caret+= (lineSep.length()-index);
\r
1744 if (c == 65535 || Character.getType(c) == Character.CONTROL) {
\r
1747 //System.out.println("Char " + c + " " + Character.getType(c) + " " + text);
\r
1748 insert(new String(new char[] {c}));
\r
1752 // FIXME This is called even if just caret was moved.
\r
1753 // This is currently necessary for repaints.
\r
1754 fireTextChanged();
\r
1755 invalidateXOffset();
\r
1759 protected String getLineSeparator() {
\r
1760 return System.getProperty("line.separator");
\r
1763 protected void selectAll() {
\r
1764 setCaret(0, false);
\r
1765 setCaret(text.length(), true);
\r
1768 protected transient int hoverClick = 0;
\r
1771 protected boolean mouseClicked(MouseClickEvent event) {
\r
1772 if (event.button != MouseClickEvent.LEFT_BUTTON)
\r
1775 if (hasState(STATE_HOVER)) {
\r
1777 if (hoverClick < 2)
\r
1779 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
\r
1780 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
\r
1783 IElement e = DiagramNodeUtil.getElement(ctx, this);
\r
1784 if (!hasState(STATE_EDITING)) {
\r
1785 if (Boolean.TRUE.equals(setEditMode(true))) {
\r
1786 editActivation = activateEdit(0, e, ctx);
\r
1792 if (hasState(STATE_EDITING)) {
\r
1793 fireTextEditingEnded();
\r
1799 protected boolean mouseDoubleClicked(MouseDoubleClickedEvent event) {
\r
1800 if (event.button != MouseClickEvent.LEFT_BUTTON)
\r
1803 if (hitTest(event, 0)) {
\r
1804 ICanvasContext ctx = DiagramNodeUtil.getCanvasContext(this);
\r
1805 // FIXME: needed only because eventdelegator registrations are done before adding node to scene graph.
\r
1809 if (text != null) {
\r
1810 // Select the whole text.
\r
1811 setCaret(0, false);
\r
1812 setCaret(text.length(), true);
\r
1821 protected boolean mouseButtonPressed(MouseButtonPressedEvent event) {
\r
1822 if (!hasState(STATE_EDITING))
\r
1825 Point2D local = controlToLocal( event.controlPosition );
\r
1826 // FIXME: once the event coordinate systems are cleared up, remove this workaround
\r
1827 local = parentToLocal(local);
\r
1828 if (hasState(STATE_HOVER) && this.containsLocal(local)) {
\r
1829 setCaret(local, event.isShiftDown());
\r
1835 protected boolean mouseMoved(MouseMovedEvent event) {
\r
1836 boolean hit = hitTest(event, 3.0);
\r
1837 if (hit != hasState(STATE_HOVER)) {
\r
1838 setState(STATE_HOVER, hit);
\r
1844 private boolean isControlDown(MouseEvent e) {
\r
1845 return e.isControlDown() || lastMouseEvent!=null?lastMouseEvent.isControlDown():false;
\r
1848 protected boolean isShiftDown(MouseEvent e) {
\r
1849 return e.isShiftDown() || lastMouseEvent!=null?lastMouseEvent.isShiftDown():false;
\r
1852 // private boolean isAltDown(MouseEvent e) {
\r
1853 // return e.isAltDown() || lastMouseEvent!=null?lastMouseEvent.isAltDown():false;
\r
1857 protected boolean mouseDragged(MouseDragBegin e) {
\r
1859 && (isControlDown(e) || isShiftDown(e))
\r
1860 && e.context instanceof NodeEventHandler
\r
1861 && dataRVI != null)
\r
1863 e.transferable = new LocalObjectTransferable(dataRVI);
\r
1868 protected boolean hitTest(MouseEvent event, double tolerance) {
\r
1869 Rectangle2D bounds = getBoundsInternal();
\r
1870 if (bounds == null)
\r
1872 Point2D localPos = NodeUtil.worldToLocal(this, event.controlPosition, new Point2D.Double());
\r
1873 double x = localPos.getX();
\r
1874 double y = localPos.getY();
\r
1875 boolean hit = bounds.contains(x, y);
\r
1879 public Rectangle2D getBoundsInternal() {
\r
1880 Rectangle2D local = lastBounds;
\r
1881 if (local == null)
\r
1883 // TODO: potential spot for CPU/memory allocation optimization
\r
1884 // by using more specialized implementations
\r
1885 if (transform.isIdentity())
\r
1887 return transform.createTransformedShape(local).getBounds2D();
\r
1890 protected Color add(Color c, int r, int g, int b) {
\r
1891 int nr = Math.min(255, c.getRed() + r);
\r
1892 int ng = Math.min(255, c.getGreen() + g);
\r
1893 int nb = Math.min(255, c.getBlue() + b);
\r
1894 return new Color(nr, ng, nb);
\r
1897 public TextEditActivation activateEdit(int mouseId, IElement e, ICanvasContext ctx) {
\r
1898 EditDataNode data = EditDataNode.getNode(this);
\r
1899 deactivateEdit(data, null);
\r
1900 TextEditActivation result = new TextEditActivation(mouseId, e, ctx);
\r
1901 data.setTextEditActivation(result);
\r
1906 * @return <code>true</code> if this node is or was previously in editing
\r
1909 protected boolean deactivateEdit() {
\r
1910 boolean result = deactivateEdit( editActivation );
\r
1911 result |= editActivation != null;
\r
1912 editActivation = null;
\r
1916 protected boolean deactivateEdit(TextEditActivation activation) {
\r
1917 return deactivateEdit( EditDataNode.getNode(this), activation );
\r
1920 protected boolean deactivateEdit(EditDataNode data, TextEditActivation activation) {
\r
1921 TextEditActivation previous = data.getTextEditActivation();
\r
1922 if (previous != null && (previous == activation || activation == null)) {
\r
1923 previous.release();
\r
1924 data.setTextEditActivation(null);
\r
1931 public int getEventMask() {
\r
1932 return EventTypes.KeyPressedMask | EventTypes.MouseMovedMask | EventTypes.MouseButtonPressedMask
\r
1933 | EventTypes.MouseClickMask | EventTypes.CommandMask;
\r
1936 private MouseEvent lastMouseEvent = null;
\r
1939 public boolean handleEvent(Event e) {
\r
1940 if(e instanceof MouseEvent && !(e instanceof MouseDragBegin)) lastMouseEvent = (MouseEvent)e;
\r
1941 return super.handleEvent(e);
\r
1945 public Function1<Object, Boolean> getPropertyFunction(String propertyName) {
\r
1946 return ScenegraphUtils.getMethodPropertyFunction(AWTThread.getThreadAccess(), this, propertyName);
\r
1950 public <T> T getProperty(String propertyName) {
\r
1955 public void setPropertyCallback(Function2<String, Object, Boolean> callback) {
\r
1958 public void synchronizeText(String text) {
\r
1962 public void synchronizeColor(RGB.Integer color) {
\r
1963 this.color = Colors.awt(color);
\r
1966 public void synchronizeFont(org.simantics.datatypes.literal.Font font) {
\r
1967 setFont(Fonts.awt(font));
\r
1970 public void synchronizeTransform(double[] data) {
\r
1971 this.setTransform(new AffineTransform(data));
\r
1974 public static void main(String[] args) {
\r
1975 Line[] lines = parseLines("\n \n FOO \n\nBAR\n\n\n BAZ\n\n");
\r
1976 System.out.println(Arrays.toString(lines));
\r
1977 System.out.println(GeometryUtils.pointToMillimeter(1));
\r
1978 System.out.println(GeometryUtils.pointToMillimeter(12));
\r
1979 System.out.println(GeometryUtils.pointToMillimeter(72));
\r
1982 ///////////////////////////////////////////////////////////////////////////
\r
1983 // LEGACY CODE NEEDED BY INHERITED CLASSES FOR NOW
\r
1984 ///////////////////////////////////////////////////////////////////////////
\r
1986 protected double getHorizontalAlignOffset(Rectangle2D r) {
\r
1987 switch (horizontalAlignment) {
\r
1988 case 0: return 0; // Leading
\r
1989 case 1: return -r.getWidth(); // Trailing
\r
1990 case 2: return -r.getCenterX(); // Center
\r
1991 default: return 0;
\r
1995 protected double getVerticalAlignOffset() {
\r
1996 FontMetrics fm = fontMetrics;
\r
1999 switch (verticalAlignment) {
\r
2000 case 0: return fm.getMaxAscent(); // Leading=top=maxascent
\r
2001 case 1: return -fm.getMaxDescent(); // Trailing=bottom=maxdescent
\r
2002 case 2: return fm.getMaxAscent() / 2; // Center=maxascent / 2
\r
2004 default: return 0;
\r
2008 ///////////////////////////////////////////////////////////////////////////
\r
2009 // LEGACY CODE ENDS
\r
2010 ///////////////////////////////////////////////////////////////////////////
\r