source: trunk/src/org/expeditee/items/Text.java@ 1095

Last change on this file since 1095 was 1095, checked in by bln4, 6 years ago

Fixed getPixelBoundsUnion()

  • Unfortunately, text layout objects relating to Text Item lines gave the (x,y) position of the entire Text Item, not that line specifically. In other words getPixelBounds(_textLayouts.get(0)).x == getPixelBounds(_textLayouts.get(1)).x. This meant that multiline Text Items were not producing the correct bounding box. A reimplementation has fixed this.

Added getPixelBoundsUnionTight()

  • Unlike getPixelBoundsUnion() that returns 'gravity-less' rectangle, getPixelBoundsUnionTight() returns a 'gravity-less' polygon. The outline of the polygon follows the length of individual lines in the Text Item.
File size: 75.3 KB
Line 
1/**
2 * Text.java
3 * Copyright (C) 2010 New Zealand Digital Library, http://expeditee.org
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19package org.expeditee.items;
20
21import java.awt.BasicStroke;
22import java.awt.Color;
23import java.awt.Dimension;
24import java.awt.Font;
25import java.awt.GradientPaint;
26import java.awt.Graphics2D;
27import java.awt.GraphicsEnvironment;
28import java.awt.Point;
29import java.awt.Polygon;
30import java.awt.Rectangle;
31import java.awt.Shape;
32import java.awt.Stroke;
33import java.awt.event.KeyEvent;
34import java.awt.event.MouseEvent;
35import java.awt.font.FontRenderContext;
36import java.awt.font.LineBreakMeasurer;
37import java.awt.font.TextAttribute;
38import java.awt.font.TextHitInfo;
39import java.awt.font.TextLayout;
40import java.awt.geom.AffineTransform;
41import java.awt.geom.Point2D;
42import java.awt.geom.Rectangle2D;
43import java.io.File;
44
45import java.text.AttributedString;
46import java.util.ArrayList;
47import java.util.Collection;
48import java.util.HashMap;
49import java.util.HashSet;
50import java.util.LinkedList;
51import java.util.List;
52import java.util.StringTokenizer;
53
54import org.expeditee.gui.AttributeValuePair;
55import org.expeditee.gui.DisplayIO;
56import org.expeditee.gui.Frame;
57import org.expeditee.gui.FrameGraphics;
58import org.expeditee.gui.FrameIO;
59import org.expeditee.gui.FrameKeyboardActions;
60import org.expeditee.gui.FrameMouseActions;
61import org.expeditee.gui.FrameUtils;
62import org.expeditee.gui.FreeItems;
63import org.expeditee.items.MagneticConstraint.MagneticConstraints;
64import org.expeditee.math.ExpediteeJEP;
65import org.expeditee.settings.experimental.ExperimentalFeatures;
66import org.nfunk.jep.Node;
67
68/**
69 * Represents text displayed on the screen, which may include multiple lines.
70 * All standard text properties can be set (font, style, size).
71 *
72 * @author jdm18
73 *
74 */
75public class Text extends Item {
76 private static final int ADJUST_WIDTH_THRESHOLD = 200;
77
78 public static String LINE_SEPARATOR = System.getProperty("line.separator");
79
80 // public static char[] BULLETS = { '\u2219', '\u2218', '\u2217' };
81 public static char[] BULLETS = { '\u25AA', '\u25AB', '\u2217' };
82
83 private static char DEFAULT_BULLET = BULLETS[2];
84
85 private static String DEFAULT_BULLET_STRING = DEFAULT_BULLET + " ";
86
87 private String[] _processedText = null;
88
89 public String[] getProcessedText() {
90 return _processedText;
91 }
92
93 public void setProcessedText(String[] tokens) {
94 _processedText = tokens;
95 }
96
97 public static final String FRAME_NAME_SEPARATOR = " on frame ";
98
99 /**
100 * The default font used to display text items if no font is specified.
101 */
102 public static final String DEFAULT_FONT = "Serif-Plain-18";
103
104 public static final Color DEFAULT_COLOR = Color.BLACK;
105
106 public static final int MINIMUM_RANGED_CHARS = 2;
107
108 public static final int NONE = 0;
109
110 public static final int UP = 1;
111
112 public static final int DOWN = 2;
113
114 public static final int LEFT = 3;
115
116 public static final int RIGHT = 4;
117
118 public static final int HOME = 5;
119
120 public static final int LINE_HOME = 9;
121
122 public static final int LINE_END = 10;
123
124 public static final int END = 6;
125
126 public static final int PAGE_DOWN = 7;
127
128 public static final int PAGE_UP = 8;
129
130 /*
131 * Set the width to be IMPLICIT, but as wide as possible, a negative width value
132 * is one that is implicitly set by the system... a positive value is one
133 * explicitly set by the user.
134 */
135 private Integer _maxWidth = Integer.MIN_VALUE + 1;
136
137 private Justification _justification = Justification.left;
138
139 private float _spacing = -1;
140
141 private int _word_spacing = -1;
142
143 private float _initial_spacing = 0;
144
145 private float _letter_spacing = 0;
146
147 // used during ranging out
148 private int _selectionStart = -1;
149
150 private int _selectionEnd = -1;
151
152 // whether autowrap is on/off for this item
153 protected boolean _autoWrap = false;
154
155 // text is broken up into lines
156 private StringBuffer _text = new StringBuffer();
157
158 private List<TextLayout> _textLayouts = new LinkedList<TextLayout>();
159
160 private List<Integer> _lineOffsets = new LinkedList<Integer>();
161
162 private FontRenderContext frc = null;
163 private LineBreakMeasurer _lineBreaker = null;
164
165 // The font to display this text in
166 private Font _font;
167
168 protected static void InitFontFamily(GraphicsEnvironment ge, File fontFamilyDir) {
169 File[] fontFiles = fontFamilyDir.listFiles();
170
171 for (File fontFile : fontFiles) {
172 String ext = "";
173 String fileName = fontFile.getName().toLowerCase();
174
175 int i = fileName.lastIndexOf('.');
176 int p = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\'));
177
178 if (i > p) {
179 ext = fileName.substring(i + 1);
180 }
181
182 if (ext.equals("ttf")) {
183
184 try {
185 Font font = Font.createFont(Font.TRUETYPE_FONT, fontFile);
186
187 boolean registered_status_ok = ge.registerFont(font);
188
189 if (registered_status_ok) {
190
191 String font_family = font.getFamily();
192 if (!FONT_WHEEL_ADDITIONAL_LOOKUP.containsKey(font_family)) {
193
194 if (FONT_WHEEL_ADDITIONAL_LOOKUP.size() > 0) {
195 System.out.print(", ");
196 }
197 System.out.print("'" + font.getFamily() + "'");
198
199 FONT_WHEEL_ADDITIONAL_LOOKUP.put(font_family, font);
200
201 int cdut = font.canDisplayUpTo("09AZaz");
202 if (cdut >= 0) {
203 // Some problem has occured (should return -1 to show all chars possible)
204
205 System.out.println(" [Non-ASCII font]");
206 }
207 System.out.flush();
208 }
209
210 } else {
211 System.err.println("Error: Failed to add custom True-Type Font file: " + fontFile);
212 }
213
214 } catch (Exception e) {
215 System.err.println("Failed to load custon font file: " + fontFile);
216 }
217 }
218 }
219 }
220
221 public static void InitFonts() {
222
223 GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
224
225 if (ge != null) {
226
227 File fontDirectory = new File(FrameIO.FONT_PATH);
228 if (fontDirectory != null) {
229 File[] fontFamilyDirs = fontDirectory.listFiles();
230 if (fontFamilyDirs != null) {
231
232 if (fontFamilyDirs.length > 0) {
233 System.out.println("Loading custom fonts:");
234 }
235
236 boolean first_item = true;
237 for (File fontFamilyDir : fontFamilyDirs) {
238 if (fontFamilyDir.isDirectory()) {
239 InitFontFamily(ge, fontFamilyDir);
240 }
241 }
242 System.out.println();
243 }
244 }
245 } else {
246 System.err.println("No graphics environment detected. Skipping the loading of the custom fonts");
247 }
248 }
249
250 /**
251 * Similar to parseArgs() in InteractiveWidget. Code based on routine used in
252 * Apache Ant: org.apache.tools.ant.types.Commandline::translateCommandline()
253 *
254 * @param toProcess
255 * the command line to process.
256 * @return the command line broken into strings. An empty or null toProcess
257 * parameter results in a zero sized array.
258 */
259 public static String[] parseArgsApache(String toProcess) {
260 if (toProcess == null || toProcess.length() == 0) {
261 // no command? no string
262 return new String[0];
263 }
264 // parse with a simple finite state machine
265
266 final int normal = 0;
267 final int inQuote = 1;
268 final int inDoubleQuote = 2;
269 int state = normal;
270 final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true);
271 final ArrayList<String> result = new ArrayList<String>();
272 final StringBuilder current = new StringBuilder();
273 boolean lastTokenHasBeenQuoted = false;
274
275 while (tok.hasMoreTokens()) {
276 String nextTok = tok.nextToken();
277 switch (state) {
278 case inQuote:
279 if ("\'".equals(nextTok)) {
280 lastTokenHasBeenQuoted = true;
281 state = normal;
282 } else {
283 current.append(nextTok);
284 }
285 break;
286 case inDoubleQuote:
287 if ("\"".equals(nextTok)) {
288 lastTokenHasBeenQuoted = true;
289 state = normal;
290 } else {
291 current.append(nextTok);
292 }
293 break;
294 default:
295 if ("\'".equals(nextTok)) {
296 state = inQuote;
297 } else if ("\"".equals(nextTok)) {
298 state = inDoubleQuote;
299 } else if (" ".equals(nextTok)) {
300 if (lastTokenHasBeenQuoted || current.length() != 0) {
301 result.add(current.toString());
302 current.setLength(0);
303 }
304 } else {
305 current.append(nextTok);
306 }
307 lastTokenHasBeenQuoted = false;
308 break;
309 }
310 }
311 if (lastTokenHasBeenQuoted || current.length() != 0) {
312 result.add(current.toString());
313 }
314 if (state == inQuote || state == inDoubleQuote) {
315 System.err.println("Error: Unbalanced quotes -- failed to parse '" + toProcess + "'");
316 return null;
317 }
318
319 return result.toArray(new String[result.size()]);
320 }
321
322 /**
323 * Creates a new Text Item with the given ID and text.
324 *
325 * @param id
326 * The id of this item
327 * @param text
328 * The text to use in this item
329 */
330 public Text(int id, String text) {
331 super();
332 _text.append(text);
333 rebuild(false);
334
335 setID(id);
336 }
337
338 /**
339 * Creates a text item which is not added to the frame.
340 *
341 * @param text
342 */
343 public Text(String text) {
344 super();
345 _text.append(text);
346 rebuild(false);
347 setID(-1);
348 }
349
350 /**
351 * Creates a new Text Item with the given ID
352 *
353 * @param id
354 * The ID to of this item
355 */
356 public Text(int id) {
357 super();
358 setID(id);
359 }
360
361 public Text(int i, String string, Color foreground, Color background) {
362 this(i, string);
363 this.setColor(foreground);
364 this.setBackgroundColor(background);
365 }
366
367 /**
368 * Sets the maximum width of this Text item when justifcation is used. passing
369 * in 0 or -1 means there is no maximum width
370 *
371 * @param width
372 * The maximum width of this item when justification is applied to
373 * it.
374 */
375 @Override
376 public void setWidth(Integer width) {
377 invalidateAll();
378
379 if (width == null) {
380 setJustification(Justification.left);
381 setRightMargin(FrameGraphics.getMaxFrameSize().width, false);
382 return;
383 }
384
385 _maxWidth = width;
386 rebuild(true);
387 invalidateAll();
388 }
389
390 /**
391 * Returns the maximum width of this Text item when justifcation is used. If the
392 * width is negative, it means no explicit width has been set
393 *
394 * @return The maximum width of this Text item when justification is used
395 */
396 @Override
397 public Integer getWidth() {
398 if (_maxWidth == null || _maxWidth <= 0)
399 return null;
400 return _maxWidth;
401 }
402
403 public Integer getAbsoluteWidth() {
404 if (_maxWidth == null) {
405 return Integer.MAX_VALUE;
406 }
407
408 return Math.abs(_maxWidth);
409 }
410
411 @Override
412 public Color getHighlightColor() {
413 if (_highlightColor.equals(getPaintBackgroundColor()))
414 return ALTERNATE_HIGHLIGHT;
415 return _highlightColor;
416 }
417
418 /**
419 * Sets the justification of this Text item. The given integer should correspond
420 * to one of the JUSTIFICATION constants defined in Item
421 *
422 * @param just
423 * The justification to apply to this Text item
424 */
425 public void setJustification(Justification just) {
426 invalidateAll();
427
428 // Only justification left works with 0 width
429 // if (just != null && just != Justification.left && !hasWidth()) {
430 // // TODO Tighten this up so it subtracts the margin widths
431 // setWidth(getBoundsWidth());
432 // }
433
434 _justification = just;
435 rebuild(true);
436 invalidateAll();
437 }
438
439 /**
440 * Returns the current justification of this Text item. The default value left
441 * justification.
442 *
443 * @return The justification of this Text item
444 */
445 public Justification getJustification() {
446 if (_justification == null || _justification.equals(Justification.left))
447 return null;
448 return _justification;
449 }
450
451 private int getJustOffset(TextLayout layout) {
452 if (getJustification() == Justification.center)
453 return (int) ((getAbsoluteWidth() - layout.getAdvance()) / 2);
454 else if (getJustification() == Justification.right)
455 return (int) (getAbsoluteWidth() - layout.getAdvance());
456
457 return 0;
458 }
459
460 /**
461 * Sets the text displayed on the screen to the given String. It does not reset
462 * the formula, attributeValuePair or other cached values.
463 *
464 * @param text
465 * The String to display on the screen when drawing this Item.
466 */
467 @Override
468 public void setText(String text) {
469 setText(text, false);
470 }
471
472 public void setText(String text, Boolean clearCache) {
473 // if (_text != null && text.length() < _text.length())
474 invalidateAll();
475 _text = new StringBuffer(text);
476
477 /*
478 * Always clearingCach remove formulas when moving in and out of XRay mode
479 */
480 if (clearCache) {
481 clearCache();
482 }
483
484 rebuild(true);
485 invalidateAll();
486 }
487
488 public void setTextList(List<String> text) {
489 if (text == null || text.size() <= 0)
490 return;
491
492 invalidateAll();
493
494 StringBuffer sb = new StringBuffer();
495
496 for (int i = 0; i < text.size(); i++) {
497 sb.append(text.get(i)).append('\n');
498 }
499
500 if (sb.length() > 0)
501 sb.deleteCharAt(sb.length() - 1);
502
503 setText(sb.toString());
504
505 rebuild(true);
506 invalidateAll();
507 }
508
509 public void setAttributeValue(String value) {
510 AttributeValuePair avp = new AttributeValuePair(getText(), false);
511 avp.setValue(value);
512 setText(avp.toString());
513 }
514
515 /**
516 * Inserts the given String at the start of the first line of this Text Item.
517 *
518 * @param text
519 * The String to insert.
520 */
521 public void prependText(String text) {
522
523 _text.insert(0, text);
524 rebuild(false);
525 }
526
527 /**
528 * If the first line of text starts with the given String, then it is removed
529 * otherwise no action is taken.
530 *
531 * @param text
532 * The String to remove from the first line of Text
533 */
534 public void removeText(String text) {
535 if (_text.length() > 0 && _text.indexOf(text) == 0) {
536 // Need the invalidate all for dateStamp toggling
537 invalidateAll();
538 _text.delete(0, text.length());
539 }
540
541 }
542
543 public void removeEndText(String textToRemove) {
544 int length = _text.length();
545 if (length > 0) {
546 // invalidateAll();
547 int pos = _text.indexOf(textToRemove);
548 int textToRemoveLength = textToRemove.length();
549 if (pos + textToRemoveLength == length) {
550 _text.delete(pos, length);
551 }
552 }
553
554 }
555
556 /**
557 * Appends the given String to any text already present in this Item
558 *
559 * @param text
560 * The String to append.
561 */
562 public void appendText(String text) {
563 _text.append(text);
564 rebuild(false);
565 }
566
567 /**
568 * Used by the frame reader to construct multi-line text items. It must run
569 * quickly, so that the system still responds well for long text items.
570 *
571 * @param text
572 */
573 public void appendLine(String text) {
574 if (text == null)
575 text = "";
576
577 if (_text.length() > 0)
578 _text.append('\n');
579
580 _text.append(text);
581
582 rebuild(true);
583 }
584
585 /**
586 * Tests if the first line of this Text starts with the given String.
587 *
588 * @param text
589 * The prefix to check for
590 * @return True if the first line starts with the given String, False otherwise.
591 */
592 public boolean startsWith(String text) {
593 return startsWith(text, true);
594 }
595
596 public boolean startsWith(String text, boolean ignoreCase) {
597 if (text == null || _text == null || _text.length() < 1)
598 return false;
599
600 if (ignoreCase)
601 return _text.toString().toLowerCase().startsWith(text.toLowerCase());
602 else
603 return _text.indexOf(text) == 0;
604 }
605
606 /**
607 * Inserts a character into the Text of this Item.
608 *
609 * @param ch
610 * The character insert.
611 * @param mouseX
612 * The X position to insert the Strings at.
613 * @param mouseY
614 * The Y position to insert the Strings at.
615 */
616 public Point2D.Float insertChar(char ch, float mouseX, float mouseY) {
617 if (ch != '\t') /* && ch != '\n' */
618 return insertText("" + ch, mouseX, mouseY);
619
620 return insertText(" " + ch, mouseX, mouseY);
621 }
622
623 /**
624 * @param index
625 * @return
626 */
627 private char getNextBullet(char bullet) {
628 for (int i = 0; i < BULLETS.length - 1; i++) {
629 if (BULLETS[i] == bullet)
630 return BULLETS[i + 1];
631 }
632 return BULLETS[0];
633 }
634
635 private char getPreviousBullet(char bullet) {
636 for (int i = 1; i < BULLETS.length; i++) {
637 if (BULLETS[i] == bullet)
638 return BULLETS[i - 1];
639 }
640 return BULLETS[BULLETS.length - 1];
641 }
642
643 public Point2D.Float getLineEndPosition(float mouseY) {
644 return getEdgePosition(getLinePosition(mouseY), false);
645 }
646
647 public Point2D.Float getLineStartPosition(float mouseY) {
648 return getEdgePosition(getLinePosition(mouseY), true);
649 }
650
651 public Point2D.Float getParagraphEndPosition() {
652 return getEdgePosition(_textLayouts.size() - 1, false);
653 }
654
655 public Point2D.Float getParagraphStartPosition() {
656 return getEdgePosition(0, true);
657 }
658
659 private Point2D.Float getEdgePosition(int line, boolean start) {
660 // if there is no text yet, or the line is invalid
661 if (_text == null || _text.length() == 0 || line < 0 || line > _textLayouts.size() - 1)
662 return new Point2D.Float(getX(), getY());
663
664 TextLayout last = _textLayouts.get(line);
665 TextHitInfo hit;
666 if (start)
667 hit = last.getNextLeftHit(1);
668 else
669 hit = last.getNextRightHit(last.getCharacterCount() - 1);
670
671 // move the cursor to the new location
672 float[] caret = last.getCaretInfo(hit);
673 float y = getLineDrop(last) * line;
674
675 float x = getX() + caret[0] + getJustOffset(last);
676 x = Math.min(x, (getX() - Item.MARGIN_RIGHT - (2 * getGravity()) + getBoundsWidth()));
677 return new Point2D.Float(x, getY() + y + caret[1]);
678 }
679
680 public void setSelectionStart(float mouseX, float mouseY) {
681 // determine what line is being pointed to
682 int line = getLinePosition(mouseY);
683
684 // get the character being pointed to
685 TextHitInfo hit = getCharPosition(line, mouseX);
686 _selectionStart = hit.getInsertionIndex() + _lineOffsets.get(line);
687 invalidateAll();
688 }
689
690 public void setSelectionEnd(float mouseX, float mouseY) {
691 // determine what line is being pointed to
692 int line = getLinePosition(mouseY);
693
694 // get the character being pointed to
695 TextHitInfo hit = getCharPosition(line, mouseX);
696 _selectionEnd = hit.getInsertionIndex() + _lineOffsets.get(line);
697 invalidateAll();
698 }
699
700 public void clearSelection() {
701 _selectionStart = -1;
702 _selectionEnd = -1;
703 invalidateAll();
704 }
705
706 public void clearSelectionEnd() {
707 _selectionEnd = -1;
708 invalidateAll();
709 }
710
711 public String copySelectedText() {
712 if (_selectionStart < 0 || _selectionEnd < 0)
713 return null;
714 else if (_selectionEnd > _text.length())
715 _selectionEnd = _text.length();
716
717 return _text.substring(Math.min(_selectionStart, _selectionEnd), Math.max(_selectionStart, _selectionEnd));
718 }
719
720 public String cutSelectedText() {
721 return replaceSelectedText("");
722 }
723
724 public String replaceSelectedText(String newText) {
725 if (_selectionStart < 0 || _selectionEnd < 0)
726 return null;
727
728 invalidateAll();
729
730 if (_selectionEnd > _text.length())
731 _selectionEnd = _text.length();
732
733 int left = Math.min(_selectionStart, _selectionEnd);
734 int right = Math.max(_selectionStart, _selectionEnd);
735
736 // Trim the text to remove new lines on the beginning and end of the
737 // string
738 if (_text.charAt(left) == '\n') {
739 // if the entire line is being removed then remove one of the new
740 // lines, the first case checks if the last line is being removed
741 if (right >= _text.length() || _text.charAt(right) == '\n') {
742 _text.deleteCharAt(left);
743 right--;
744 } else {
745 left++;
746 }
747 }
748 // New lines are always at the start of the line for now...
749 // if(_text.charAt(right - 1) == '\n' && left < right){
750 // right--;
751 // }
752 String s = _text.substring(left, right);
753
754 _text.delete(left, right);
755 _text.insert(left, newText);
756 rebuild(true);
757
758 clearCache();
759
760 invalidateAll();
761
762 return s;
763 }
764
765 public int getSelectionSize() {
766 if (_selectionEnd < 0 || _selectionStart < 0)
767 return 0;
768
769 // System.out.println(_selectionStart + ":" + _selectionEnd);
770
771 return Math.abs(_selectionEnd - _selectionStart);
772 }
773
774 /**
775 * Inserts the given String into the Text at the position given by the mouseX
776 * and mouseY coordinates
777 *
778 * @param text
779 * The String to insert into this Text.
780 * @param mouseX
781 * The X position to insert the String
782 * @param mouseY
783 * The Y position to insert the String
784 * @return The new location that the mouse cursor should be moved to
785 */
786 public Point2D.Float insertText(String text, float mouseX, float mouseY) {
787 final Point2D.Float newPos = insertText(text, mouseX, mouseY, -1);
788 return newPos;
789 }
790
791 public Point2D.Float insertText(String text, float mouseX, float mouseY, int insertPos) {
792 TextHitInfo hit;
793 TextLayout current = null;
794 int line;
795
796 invalidateAll();
797
798 // check for empty string
799 if (text == null || text.length() == 0)
800 return new Point2D.Float(mouseX, mouseY);
801
802 // if there is no text yet
803 if (_text == null || _text.length() == 0) {
804 _text = new StringBuffer().append(text);
805 // create the linebreaker and layouts
806 rebuild(true);
807 assert (_textLayouts.size() == 1);
808 current = _textLayouts.get(0);
809 hit = current.getNextRightHit(0);
810 line = 0;
811
812 // otherwise, we are inserting text
813 } else {
814 clearCache();
815 // determine what line is being pointed to
816 line = getLinePosition(mouseY);
817
818 // get the character being pointed to
819 hit = getCharPosition(line, mouseX);
820
821 int pos = hit.getInsertionIndex() + _lineOffsets.get(line);
822
823 if (line > 0 && hit.getInsertionIndex() == 0) {
824 // Only move forward a char if the line begins with a hard line
825 // break... not a soft line break
826 if (_text.charAt(pos) == '\n') {
827 pos++;
828 }
829 }
830
831 if (insertPos < 0)
832 insertPos = pos;
833
834 // if this is a backspace key
835 if (text.charAt(0) == KeyEvent.VK_BACK_SPACE) {
836 if (hasSelection()) {
837 pos = deleteSelection(pos);
838 } else if (insertPos > 0) {
839 deleteChar(insertPos - 1);
840 if (pos > 0)
841 pos--;
842 }
843 // if this is a delete key
844 } else if (text.charAt(0) == KeyEvent.VK_DELETE) {
845 if (hasSelection()) {
846 pos = deleteSelection(pos);
847 } else if (insertPos < _text.length()) {
848 deleteChar(insertPos);
849 }
850 // this is a tab
851 } else if (text.charAt(0) == KeyEvent.VK_TAB) {
852 // Text length greater than 1 signals a backwards tab
853 if (text.length() > 1) {
854 // Find the first non space char to see if its a bullet
855 int index = 0;
856 for (index = 0; index < _text.length(); index++) {
857 if (!Character.isSpaceChar(_text.charAt(index)))
858 break;
859 }
860 // Check if there is a space after the bullet
861 if (index < _text.length() - 1 && _text.charAt(index + 1) == ' ') {
862 // Change the bullet
863 _text.setCharAt(index, getPreviousBullet(_text.charAt(index)));
864 }
865 // Remove the spacing at the start
866 for (int i = 0; i < TAB_STRING.length(); i++) {
867 if (_text.length() > 0 && Character.isSpaceChar(_text.charAt(0))) {
868 deleteChar(0);
869 pos--;
870 } else
871 break;
872 }
873 _lineBreaker = null;
874 } else {
875 // / Find the first non space char to see if its a bullet
876 int index = 0;
877 for (index = 0; index < _text.length(); index++) {
878 if (!Character.isSpaceChar(_text.charAt(index)))
879 break;
880 }
881 // Check if there is a space after the bullet
882 if (index < _text.length() - 1 && _text.charAt(index + 1) == ' ') {
883 char nextBullet = getNextBullet(_text.charAt(index));
884 // Change the bullet
885 _text.setCharAt(index, nextBullet);
886 }
887 // Insert the spacing at the start
888 insertString(TAB_STRING, 0);
889 pos += TAB_STRING.length();
890 }
891 // this is a normal insert
892 } else {
893 insertString(text, insertPos);
894 pos += text.length();
895 }
896
897 if (_text.length() == 0) {
898 rebuild(false);
899 return new Point2D.Float(this._x, this._y);
900 }
901
902 int newLine = line;
903
904 // if a rebuild is required
905 rebuild(true, false);
906
907 // determine the new position the cursor should have
908 for (int i = 1; i < _lineOffsets.size(); i++) {
909 if (_lineOffsets.get(i) >= pos) {
910 newLine = i - 1;
911 break;
912 }
913 }
914
915 current = _textLayouts.get(newLine);
916 pos -= _lineOffsets.get(newLine);
917
918 if (newLine == line) {
919 if (pos > 0)
920 hit = current.getNextRightHit(pos - 1);
921 else
922 hit = current.getNextLeftHit(1);
923 } else if (newLine < line) {
924 hit = current.getNextRightHit(pos - 1);
925 } else {
926 hit = current.getNextRightHit(pos - 1);
927 }
928
929 line = newLine;
930 }
931
932 // move the cursor to the new location
933 float[] caret = current.getCaretInfo(hit);
934 float y = getLineDrop(current) * line;
935
936 float x = getX() + caret[0] + getJustOffset(current);
937 x = Math.min(x, (getX() - Item.MARGIN_RIGHT - (2 * getGravity()) + getBoundsWidth()));
938
939 invalidateAll();
940
941 return new Point2D.Float(Math.round(x), Math.round(getY() + y + caret[1]));
942 }
943
944 /**
945 *
946 */
947 private void clearCache() {
948 _attributeValuePair = null;
949 setProcessedText(null);
950 setFormula(null);
951 }
952
953 /**
954 * @param pos
955 * @return
956 */
957 private int deleteSelection(int pos) {
958 int selectionLength = getSelectionSize();
959 cutSelectedText();
960 clearSelection();
961 pos -= selectionLength;
962 return pos;
963 }
964
965 public Point2D.Float moveCursor(int direction, float mouseX, float mouseY, boolean setSelection,
966 boolean wholeWord) {
967 if (setSelection) {
968 if (!hasSelection()) {
969 setSelectionStart(mouseX, mouseY);
970 }
971 } else {
972 // clearSelection();
973 }
974
975 Point2D.Float resultPos = null;
976
977 // check for home or end keys
978 switch (direction) {
979 case HOME:
980 resultPos = getParagraphStartPosition();
981 break;
982 case END:
983 resultPos = getParagraphEndPosition();
984 break;
985 case LINE_HOME:
986 resultPos = getLineStartPosition(mouseY);
987 break;
988 case LINE_END:
989 resultPos = getLineEndPosition(mouseY);
990 break;
991 default:
992 TextHitInfo hit;
993 TextLayout current;
994 int line;
995
996 // if there is no text yet
997 if (_text == null || _text.length() == 0) {
998 return new Point2D.Float(mouseX, mouseY);
999 // otherwise, move the cursor
1000 } else {
1001 // determine the line of text to check
1002 line = getLinePosition(mouseY);
1003 if (line < 0)
1004 line = _textLayouts.size() - 1;
1005
1006 // if the cursor is moving up or down, change the line
1007 if (direction == UP)
1008 line = Math.max(line - 1, 0);
1009 else if (direction == DOWN)
1010 line = Math.min(line + 1, _textLayouts.size() - 1);
1011
1012 hit = getCharPosition(line, mouseX);
1013
1014 if (direction == LEFT) {
1015 if (hit.getInsertionIndex() > 0) {
1016
1017 char prevChar = ' ';
1018 do {
1019 hit = _textLayouts.get(line).getNextLeftHit(hit);
1020
1021 // Stop if at the start of the line
1022 if (hit.getInsertionIndex() == 0)
1023 break;
1024 // Keep going if the char to the left is a
1025 // letterOrDigit
1026 prevChar = _text.charAt(hit.getInsertionIndex() - 1 + _lineOffsets.get(line));
1027 } while (wholeWord && Character.isLetterOrDigit(prevChar));
1028 // TODO Go to the start of the word instead of before
1029 // the word
1030 char nextChar = _text.charAt(hit.getInsertionIndex() + _lineOffsets.get(line));
1031 /*
1032 * This takes care of hard line break in
1033 */
1034 if (line > 0 && nextChar == '\n') {
1035 line--;
1036 hit = _textLayouts.get(line)
1037 .getNextRightHit(_textLayouts.get(line).getCharacterCount() - 1);
1038 }
1039 // This takes care of soft line breaks.
1040 } else if (line > 0) {
1041 line--;
1042 hit = _textLayouts.get(line).getNextRightHit(_textLayouts.get(line).getCharacterCount() - 1);
1043 /*
1044 * Skip the spaces at the end of a line with soft linebreak
1045 */
1046 while (hit.getCharIndex() > 0
1047 && _text.charAt(_lineOffsets.get(line) + hit.getCharIndex() - 1) == ' ') {
1048 hit = _textLayouts.get(line).getNextLeftHit(hit);
1049 }
1050 }
1051 } else if (direction == RIGHT) {
1052 if (hit.getInsertionIndex() < _textLayouts.get(line).getCharacterCount()) {
1053 hit = _textLayouts.get(line).getNextRightHit(hit);
1054 // Skip whole word if needs be
1055 while (wholeWord && hit.getCharIndex() > 0
1056 && hit.getCharIndex() < _textLayouts.get(line).getCharacterCount() && Character
1057 .isLetterOrDigit(_text.charAt(_lineOffsets.get(line) + hit.getCharIndex() - 1)))
1058 hit = _textLayouts.get(line).getNextRightHit(hit);
1059 } else if (line < _textLayouts.size() - 1) {
1060 line++;
1061 hit = _textLayouts.get(line).getNextLeftHit(1);
1062 }
1063 }
1064 current = _textLayouts.get(line);
1065 }
1066
1067 // move the cursor to the new location
1068 float[] caret = current.getCaretInfo(hit);
1069 float y = getLineDrop(current) * line;
1070
1071 resultPos = new Point2D.Float(getX() + caret[0] + getJustOffset(current), getY() + y + caret[1]);
1072 break;
1073 }
1074 if (setSelection)
1075 setSelectionEnd(resultPos.x, resultPos.y);
1076 return resultPos;
1077 }
1078
1079 /**
1080 * Iterates through the given line string and returns the position of the
1081 * character being pointed at by the mouse.
1082 *
1083 * @param line
1084 * The index of the _text array of the String to be searched.
1085 * @param mouseX
1086 * The X coordinate of the mouse
1087 * @return The position in the string of the character being pointed at.
1088 */
1089 public TextHitInfo getCharPosition(int line, float mouseX) {
1090 if (line < 0 || line >= _textLayouts.size())
1091 return null;
1092
1093 TextLayout layout = _textLayouts.get(line);
1094 mouseX += getOffset().x;
1095 mouseX -= getJustOffset(layout);
1096
1097 return layout.hitTestChar(mouseX - getX(), 0);
1098 }
1099
1100 public int getLinePosition(float mouseY) {
1101 mouseY += getOffset().y;
1102
1103 float y = getY();
1104
1105 for (TextLayout text : _textLayouts) {
1106 // calculate X to ensure it is in the shape
1107 Rectangle2D bounds = text.getLogicalHighlightShape(0, text.getCharacterCount()).getBounds2D();
1108
1109 if (bounds.getWidth() < 1)
1110 bounds.setRect(bounds.getMinX(), bounds.getMinY(), 10, bounds.getHeight());
1111
1112 double x = bounds.getCenterX();
1113
1114 if (bounds.contains(x, mouseY - getY() - (y - getY())))
1115 return _textLayouts.indexOf(text);
1116
1117 // check if the cursor is between lines
1118 if (mouseY - getY() - (y - getY()) < bounds.getMinY())
1119 return Math.max(0, _textLayouts.indexOf(text) - 1);
1120
1121 y += getLineDrop(text);
1122 }
1123
1124 return _textLayouts.size() - 1;
1125 }
1126
1127 /**
1128 * Sets the Font that this text will be displayed with on the screen.
1129 *
1130 * @param font
1131 * The Font to display the Text of this Item in.
1132 */
1133 public void setFont(Font font) {
1134 invalidateAll();
1135 // all decoding occurs in the Utils class
1136 _font = font;
1137 // rejustify();
1138 rebuild(false);
1139
1140 invalidateAll();
1141 }
1142
1143 /**
1144 * Returns the Font that this Text is currently using when painting to the
1145 * screen
1146 *
1147 * @return The Font used to display this Text on the screen.
1148 */
1149 public Font getFont() {
1150 return _font;
1151 }
1152
1153 public Font getPaintFont() {
1154 if (getFont() == null)
1155 return Font.decode(DEFAULT_FONT);
1156
1157 return getFont();
1158 }
1159
1160 public String getFamily() {
1161 return getPaintFont().getFamily();
1162 }
1163
1164 public void setFamily(String newFamily) {
1165 String toDecode = newFamily + "-" + getFontStyle() + "-" + Math.round(getSize());
1166 setFont(Font.decode(toDecode));
1167
1168 setLetterSpacing(this._letter_spacing);
1169 }
1170
1171 public String getFontStyle() {
1172 Font f = getPaintFont();
1173 String s = "";
1174
1175 if (f.isPlain())
1176 s += "Plain";
1177
1178 if (f.isBold())
1179 s += "Bold";
1180
1181 if (f.isItalic())
1182 s += "Italic";
1183
1184 return s;
1185 }
1186
1187 public static final String MONOSPACED_FONT = "monospaced";
1188
1189 public static final String[] FONT_WHEEL = { "sansserif", "monospaced", "serif", "dialog", "dialoginput" };
1190 public static final char[] FONT_CHARS = { 's', 'm', 't', 'd', 'i' };
1191
1192 // A hashtable to store the font family names that are loaded in through the TTF
1193 // files
1194 // provided in the 'assets' folder
1195 public static HashMap<String, Font> FONT_WHEEL_ADDITIONAL_LOOKUP = new HashMap<>();
1196
1197 private static final int NEARBY_GRAVITY = 2;
1198
1199 public static final int MINIMUM_FONT_SIZE = 6;
1200
1201 public void toggleFontFamily() {
1202 String fontFamily = getFamily().toLowerCase();
1203 // set it to the first font by default
1204 setFamily(FONT_WHEEL[0]);
1205
1206 for (int i = 0; i < FONT_WHEEL.length - 3; i++) {
1207 if (fontFamily.equals(FONT_WHEEL[i])) {
1208 setFamily(FONT_WHEEL[i + 1]);
1209 break;
1210 }
1211 }
1212 }
1213
1214 public void toggleFontStyle() {
1215 invalidateAll();
1216 Font currentFont = getPaintFont();
1217 if (currentFont.isPlain())
1218 setFont(currentFont.deriveFont(Font.BOLD));
1219 else if (currentFont.isBold() && currentFont.isItalic())
1220 setFont(currentFont.deriveFont(Font.PLAIN));
1221 else if (currentFont.isBold())
1222 setFont(currentFont.deriveFont(Font.ITALIC));
1223 else
1224 setFont(currentFont.deriveFont(Font.ITALIC + Font.BOLD));
1225 rebuild(true);
1226 invalidateAll();
1227 }
1228
1229 public void toggleBold() {
1230 invalidateAll();
1231 Font currentFont = getPaintFont();
1232 int newStyle = currentFont.getStyle();
1233 if (currentFont.isBold()) {
1234 newStyle -= Font.BOLD;
1235 } else {
1236 newStyle += Font.BOLD;
1237 }
1238 setFont(currentFont.deriveFont(newStyle));
1239 rebuild(true);
1240 invalidateAll();
1241 }
1242
1243 public void toggleItalics() {
1244 invalidateAll();
1245 Font currentFont = getPaintFont();
1246 int newStyle = currentFont.getStyle();
1247 if (currentFont.isItalic()) {
1248 newStyle -= Font.ITALIC;
1249 } else {
1250 newStyle += Font.ITALIC;
1251 }
1252 setFont(currentFont.deriveFont(newStyle));
1253 rebuild(true);
1254 invalidateAll();
1255 }
1256
1257 public void setFontStyle(String newFace) {
1258 if (newFace == null || newFace.trim().length() == 0) {
1259 setFont(getPaintFont().deriveFont(Font.PLAIN));
1260 return;
1261 }
1262
1263 newFace = newFace.toLowerCase().trim();
1264
1265 if (newFace.equals("plain") || newFace.equals("p")) {
1266 setFont(getPaintFont().deriveFont(Font.PLAIN));
1267 } else if (newFace.equals("bold") || newFace.equals("b")) {
1268 setFont(getPaintFont().deriveFont(Font.BOLD));
1269 } else if (newFace.equals("italic") || newFace.equals("i")) {
1270 setFont(getPaintFont().deriveFont(Font.ITALIC));
1271 } else if (newFace.equals("bolditalic") || newFace.equals("italicbold") || newFace.equals("bi")
1272 || newFace.equals("ib")) {
1273 setFont(getPaintFont().deriveFont(Font.BOLD + Font.ITALIC));
1274 }
1275
1276 }
1277
1278 /**
1279 * Returns a String array of this Text object's text, split up into separate
1280 * lines.
1281 *
1282 * @return The String array with one element per line of text in this Item.
1283 */
1284 public List<String> getTextList() {
1285 if (_text == null)
1286 return null;
1287 try {
1288 List<String> list = new LinkedList<String>();
1289
1290 // Rebuilding prevents errors when displaying frame bitmaps
1291 if (_lineOffsets.size() == 0) {
1292 rebuild(false);
1293 }
1294
1295 int last = 0;
1296 for (int offset : _lineOffsets) {
1297 if (offset != last) {
1298 list.add(_text.substring(last, offset).replaceAll("\n", ""));
1299 }
1300 last = offset;
1301 }
1302
1303 return list;
1304 } catch (Exception e) {
1305 System.out.println(e.getMessage());
1306 return null;
1307 }
1308 }
1309
1310 public String getText() {
1311 return _text.toString();
1312 }
1313
1314 /**
1315 * Returns the first line of text in this Text Item
1316 *
1317 * @return The first line of Text
1318 */
1319 public String getFirstLine() {
1320 if (_text == null || _text.length() == 0)
1321 return null;
1322
1323 // start at the first non-newLine char
1324 int index = 0;
1325 while (_text.charAt(index) == '\n') {
1326 index++;
1327 }
1328
1329 int nextNewLine = _text.indexOf("\n", index);
1330
1331 /* If there are no more newLines return the remaining text */
1332 if (nextNewLine < 0)
1333 return _text.substring(index);
1334
1335 return _text.substring(index, nextNewLine);
1336 }
1337
1338 /**
1339 * Sets the inter-line spacing (in pixels) of this text.
1340 *
1341 * @param spacing
1342 * The number of pixels to allow between each line
1343 */
1344 public void setSpacing(float spacing) {
1345 _spacing = spacing;
1346 updatePolygon();
1347 }
1348
1349 /**
1350 * Returns the inter-line spacing (in pixels) of this Text.
1351 *
1352 * @return The spacing (inter-line) in pixels of this Text.
1353 */
1354 public float getSpacing() {
1355 return _spacing;
1356 }
1357
1358 private float getLineDrop(TextLayout layout) {
1359 if (getSpacing() < 0)
1360 return layout.getAscent() + layout.getDescent() + layout.getLeading();
1361
1362 return layout.getAscent() + layout.getDescent() + getSpacing();
1363 }
1364
1365 public void setWordSpacing(int spacing) {
1366 _word_spacing = spacing;
1367 }
1368
1369 public int getWordSpacing() {
1370 return _word_spacing;
1371 }
1372
1373 /**
1374 * Sets the spacing (proportional to the font size) between letters
1375 *
1376 * @param spacing
1377 * Additional spacing to add between letters. See
1378 * {@link java.awt.font.TextAttribute#TRACKING}
1379 */
1380 public void setLetterSpacing(float spacing) {
1381 _letter_spacing = spacing;
1382 HashMap<TextAttribute, Object> attr = new HashMap<TextAttribute, Object>();
1383 attr.put(TextAttribute.TRACKING, spacing);
1384
1385 if (this._font == null) {
1386 this._font = Font.decode(DEFAULT_FONT);
1387 }
1388
1389 this.setFont(this._font.deriveFont(attr));
1390 }
1391
1392 /**
1393 * @return The spacing (proportional to the font size) between letters. See
1394 * {@link java.awt.font.TextAttribute#TRACKING}
1395 */
1396 public float getLetterSpacing() {
1397 return _letter_spacing;
1398 }
1399
1400 public void setInitialSpacing(float spacing) {
1401 _initial_spacing = spacing;
1402 }
1403
1404 public float getInitialSpacing() {
1405 return _initial_spacing;
1406 }
1407
1408 // @Override
1409 public boolean intersectsOLD(Polygon p) {
1410 if (super.intersects(p)) {
1411 float textY = getY();
1412
1413 for (TextLayout text : _textLayouts) {
1414 // check left and right of each box
1415 Rectangle2D textOutline = text.getLogicalHighlightShape(0, text.getCharacterCount()).getBounds2D();
1416 textOutline.setRect(textOutline.getX() + getX() - 1, textOutline.getY() + textY - 1,
1417 textOutline.getWidth() + 2, textOutline.getHeight() + 2);
1418 if (p.intersects(textOutline))
1419 return true;
1420 textY += getLineDrop(text);
1421 }
1422 }
1423 return false;
1424 }
1425
1426 // The following version of intersect uses a tighter definition for the text,
1427 // based on
1428 @Override
1429 public boolean intersects(Polygon p) {
1430 if (super.intersects(p)) {
1431 // float textY = getY();
1432
1433 for (TextLayout text : _textLayouts) {
1434
1435 Rectangle text_pixel_bounds_rect = this.getPixelBounds(text);
1436
1437 if (p.intersects(text_pixel_bounds_rect)) {
1438 return true;
1439 }
1440 }
1441 }
1442
1443 return false;
1444 }
1445
1446 @Override
1447 public boolean contains(int mouseX, int mouseY) {
1448 return contains(mouseX, mouseY, getGravity() * NEARBY_GRAVITY);
1449 }
1450
1451 public boolean contains(int mouseX, int mouseY, int gravity) {
1452 mouseX += getOffset().x;
1453 mouseY += getOffset().y;
1454
1455 float textY = getY();
1456 float textX = getX();
1457
1458 Rectangle2D outline = getPolygon().getBounds2D();
1459
1460 // Check if its outside the top and left and bottom bounds
1461 if (outline.getX() - mouseX > gravity || outline.getY() - mouseY > gravity
1462 || mouseY - (outline.getY() + outline.getHeight()) > gravity
1463 || mouseX - (outline.getX() + outline.getWidth()) > gravity) {
1464 return false;
1465 }
1466
1467 for (TextLayout text : _textLayouts) {
1468 // check left and right of each box
1469 Rectangle2D textOutline = text.getLogicalHighlightShape(0, text.getCharacterCount()).getBounds2D();
1470
1471 // check if the cursor is within the top, bottom and within the
1472 // gravity of right
1473 int justOffset = getJustOffset(text);
1474
1475 if (mouseY - textY > textOutline.getY() && mouseY - textY < textOutline.getY() + textOutline.getHeight()
1476 && mouseX - textX - justOffset < textOutline.getWidth() + gravity + Item.MARGIN_RIGHT
1477 /* &&(justOffset == 0 || mouseX > textX + justOffset - gravity ) */)
1478 return true;
1479 textY += getLineDrop(text);
1480 }
1481
1482 return false;
1483 }
1484
1485 /**
1486 * Updates the Polygon (rectangle) that surrounds this Text on the screen.
1487 */
1488 public void updatePolygon() {
1489 // if there is no text, there is nothing to do
1490 if (_text == null)
1491 return;
1492
1493 int preChangeWidth = 0;
1494 if (_poly != null)
1495 preChangeWidth = _poly.getBounds().width;
1496
1497 _poly = new Polygon();
1498
1499 if (_textLayouts.size() < 1)
1500 return;
1501
1502 int minX = Integer.MAX_VALUE;
1503 int maxX = Integer.MIN_VALUE;
1504
1505 int minY = Integer.MAX_VALUE;
1506 int maxY = Integer.MIN_VALUE;
1507
1508 float y = -1;
1509
1510 // Fix concurrency error in ScaleFrameset
1511 List<TextLayout> tmpTextLayouts;
1512 synchronized (_textLayouts) {
1513 tmpTextLayouts = new LinkedList<TextLayout>(_textLayouts);
1514 }
1515
1516 for (TextLayout layout : tmpTextLayouts) {
1517 Rectangle2D bounds = layout.getLogicalHighlightShape(0, layout.getCharacterCount()).getBounds2D();
1518
1519 if (y < 0)
1520 y = 0;
1521 else
1522 y += getLineDrop(layout);
1523
1524 maxX = Math.max(maxX, (int) bounds.getMaxX());
1525 minX = Math.min(minX, (int) bounds.getMinX());
1526 maxY = Math.max(maxY, (int) (bounds.getMaxY() + y));
1527 minY = Math.min(minY, (int) (bounds.getMinY() + y));
1528 }
1529
1530 minX -= getLeftMargin();
1531 maxX += Item.MARGIN_RIGHT;
1532
1533 // If its justification right or center then DONT limit the width
1534 if (getJustification() != null) {
1535 maxX = Item.MARGIN_RIGHT + getAbsoluteWidth();
1536 }
1537
1538 _poly.addPoint(minX - getGravity(), minY - getGravity());
1539 _poly.addPoint(maxX + getGravity(), minY - getGravity());
1540 _poly.addPoint(maxX + getGravity(), maxY + getGravity());
1541 _poly.addPoint(minX - getGravity(), maxY + getGravity());
1542
1543 _poly.translate(getX(), getY());
1544
1545 if (preChangeWidth != 0 && preChangeWidth != _poly.getBounds().width)
1546 if (_poly.getBounds().width > preChangeWidth)
1547 MagneticConstraints.getInstance().textGrown(this, _poly.getBounds().width - preChangeWidth);
1548 else
1549 MagneticConstraints.getInstance().textShrunk(this, preChangeWidth - _poly.getBounds().width);
1550 }
1551
1552 // TODO it seems like this method has some exponencial processing which
1553 // makes items copy really slowly when there are lots of lines of text!
1554 // This needs to be fixed!!
1555 public void rebuild(boolean limitWidth) {
1556 rebuild(limitWidth, true);
1557 }
1558
1559 /**
1560 *
1561 * @param limitWidth
1562 * @param newLinebreakerAlways
1563 * true if a new line breaker should always be created.
1564 */
1565 private void rebuild(boolean limitWidth, boolean newLinebreakerAlways) {
1566 // TODO make this more efficient so it only clears annotation list when
1567 // it really has to
1568 if (isAnnotation()) {
1569 Frame parent = getParent();
1570 // parent can be null when running tests
1571 if (parent != null) {
1572 parent.clearAnnotations();
1573 }
1574 }
1575
1576 // if there is no text, there is nothing to do
1577 if (_text == null || _text.length() == 0) {
1578 // Frame parent = getParent();
1579 // if(parent != null)
1580 // parent.removeItem(this);
1581 return;
1582 }
1583
1584 if (_lineBreaker == null || newLinebreakerAlways) {
1585 AttributedString paragraphText = new AttributedString(_text.toString());
1586 paragraphText.addAttribute(TextAttribute.FONT, getPaintFont());
1587 frc = new FontRenderContext(null, true, true);
1588 _lineBreaker = new LineBreakMeasurer(paragraphText.getIterator(), frc);
1589 }
1590
1591 /*
1592 * float width = Float.MAX_VALUE;
1593 *
1594 * if (limitWidth) { width = getAbsoluteWidth(); // else if (getMaxWidth() > 0)
1595 * // width = Math.max(50, getMaxWidth() - getX() // - Item.MARGIN_RIGHT); }
1596 */
1597
1598 _textLayouts.clear();
1599 _lineOffsets.clear();
1600 // the first line always has a 0 offset
1601 _lineOffsets.add(0);
1602
1603 TextLayout layout;
1604
1605 float width;
1606 float lineHeight = Float.NaN;
1607 List<Point[]> lines = null;
1608
1609 if (_autoWrap || ExperimentalFeatures.AutoWrap.get()) {
1610 lines = new LinkedList<Point[]>();
1611 if (DisplayIO.getCurrentFrame() == null) {
1612 return;
1613 }
1614 for (Item item : DisplayIO.getCurrentFrame().getItems()) {
1615 if (item instanceof Line) {
1616 lines.add(new Point[] { ((Line) item).getStartItem().getPosition(),
1617 ((Line) item).getEndItem().getPosition() });
1618 }
1619 if (item instanceof Picture) {
1620 lines.add(
1621 new Point[] { item.getPosition(), new Point(item.getX(), item.getY() + item.getHeight()) });
1622 }
1623 }
1624 for (Item item : FreeItems.getInstance()) {
1625 if (item instanceof Line) {
1626 lines.add(new Point[] { ((Line) item).getStartItem().getPosition(),
1627 ((Line) item).getEndItem().getPosition() });
1628 }
1629 if (item instanceof Picture) {
1630 lines.add(
1631 new Point[] { item.getPosition(), new Point(item.getX(), item.getY() + item.getHeight()) });
1632 }
1633 }
1634 width = getLineWidth(getX(), getY(), lines);
1635 } else {
1636 width = Float.MAX_VALUE;
1637 if (limitWidth) {
1638 if (_maxWidth == null) {
1639 width = FrameGraphics.getMaxFrameSize().width - getX();
1640 } else {
1641 width = getAbsoluteWidth();
1642 }
1643 // else if (getMaxWidth() > 0)
1644 // width = Math.max(50, getMaxWidth() - getX()
1645 // - Item.MARGIN_RIGHT);
1646 }
1647 }
1648
1649 _lineBreaker.setPosition(0);
1650 boolean requireNextWord = false;
1651
1652 // --- Get the output of the LineBreakMeasurer and store it in a
1653 while (_lineBreaker.getPosition() < _text.length()) {
1654
1655 if (_autoWrap || ExperimentalFeatures.AutoWrap.get()) {
1656 requireNextWord = width < FrameGraphics.getMaxFrameSize().width - getX();
1657 }
1658
1659 layout = _lineBreaker.nextLayout(width, _text.length(), requireNextWord);
1660
1661 // lineBreaker does not break on newline
1662 // characters so they have to be check manually
1663 int start = _lineOffsets.get(_lineOffsets.size() - 1);
1664
1665 // int y = getY() + (getLineDrop(layout) * (_lineOffsets.size() - 1)
1666
1667 // check through the current line for newline characters
1668 for (int i = start + 1; i < _text.length(); i++) {
1669 if (_text.charAt(i) == '\n') {// || c == '\t'){
1670 _lineBreaker.setPosition(start);
1671 layout = _lineBreaker.nextLayout(width, i, requireNextWord);
1672 break;
1673 }
1674 }
1675
1676 _lineOffsets.add(_lineBreaker.getPosition());
1677
1678 if (layout == null) {
1679 layout = new TextLayout(" ", getPaintFont(), frc);
1680 }
1681
1682 if (/* hasWidth() && */getJustification() == Justification.full
1683 && _lineBreaker.getPosition() < _text.length())
1684 layout = layout.getJustifiedLayout(width);
1685
1686 _textLayouts.add(layout);
1687
1688 if (_autoWrap || ExperimentalFeatures.AutoWrap.get()) {
1689
1690 if (lineHeight != Float.NaN) {
1691 lineHeight = getLineDrop(layout);
1692 }
1693 width = getLineWidth(getX(), getY() + (lineHeight * (_textLayouts.size() - 1)), lines);
1694 }
1695 }
1696
1697 updatePolygon();
1698
1699 }
1700
1701 private float getLineWidth(int x, float y, List<Point[]> lines) {
1702 float width = FrameGraphics.getMaxFrameSize().width;
1703 for (Point[] l : lines) {
1704 // check for lines that cross over our y
1705 if ((l[0].y >= y && l[1].y <= y) || (l[0].y <= y && l[1].y >= y)) {
1706 float dX = l[0].x - l[1].x;
1707 float dY = l[0].y - l[1].y;
1708 float newWidth;
1709 if (dX == 0) {
1710 newWidth = l[0].x;
1711 } else if (dY == 0) {
1712 newWidth = Math.min(l[0].x, l[1].x);
1713 } else {
1714 // System.out.print("gradient: " + (dY / dX));
1715 newWidth = l[0].x + (y - l[0].y) * dX / dY;
1716 }
1717 // System.out.println("dY:" + dY + " dX:" + dX + " width:" + newWidth);
1718 if (newWidth < x) {
1719 continue;
1720 }
1721 if (newWidth < width) {
1722 width = newWidth;
1723 }
1724 }
1725 }
1726 return width - x;
1727 }
1728
1729 private boolean hasFixedWidth() {
1730 assert (_maxWidth != null);
1731 if (_maxWidth == null) {
1732 justify(false);
1733 }
1734 return _maxWidth > 0;
1735 }
1736
1737 private int _alpha = -1;
1738
1739 public void setAlpha(int alpha) {
1740 _alpha = alpha;
1741 }
1742
1743 private Point getSelectedRange(int line) {
1744 if (_selectionEnd >= _text.length()) {
1745 _selectionEnd = _text.length();
1746 }
1747
1748 if (_selectionStart < 0)
1749 _selectionStart = 0;
1750
1751 if (_selectionStart < 0 || _selectionEnd < 0)
1752 return null;
1753
1754 int selectionLeft = Math.min(_selectionStart, _selectionEnd);
1755 int selectionRight = Math.max(_selectionStart, _selectionEnd);
1756
1757 // if the selection is after this line, return null
1758 if (_lineOffsets.get(line) > selectionRight)
1759 return null;
1760
1761 // if the selection is before this line, return null
1762 if (_lineOffsets.get(line) < selectionLeft
1763 && _lineOffsets.get(line) + _textLayouts.get(line).getCharacterCount() < selectionLeft)
1764 return null;
1765
1766 // Dont highlight a single char
1767 // if (selectionRight - selectionLeft <= MINIMUM_RANGED_CHARS)
1768 // return null;
1769
1770 // the selection occurs on this line, determine where it lies on the
1771 // line
1772 int start = Math.max(0, selectionLeft - _lineOffsets.get(line));
1773 // int end = Math.min(_lineOffsets.get(line) +
1774 // _textLayouts.get(line).getCharacterCount(), _selectionEnd);
1775 int end = Math.min(selectionRight - _lineOffsets.get(line), +_textLayouts.get(line).getCharacterCount());
1776
1777 // System.out.println(line + ": " + start + "x" + end + " (" +
1778 // _selectionStart + "x" + _selectionEnd + ")");
1779 return new Point(start, end);
1780 }
1781
1782 /**
1783 * @param mouseButton
1784 * Either MouseEvent.BUTTON1, MouseEvent.BUTTON2 or
1785 * MouseEvent.BUTTON3.
1786 *
1787 * @return The color for the text selection based on the given mouse click
1788 */
1789 protected Color getSelectionColor(int mouseButton) {
1790
1791 /*
1792 * Color main = getPaintColor(); Color back = getPaintBackgroundColor();
1793 *
1794 * if (Math.abs(main.getRed() - back.getRed()) < 10 && Math.abs(main.getGreen()
1795 * - back.getGreen()) < 10 && Math.abs(main.getBlue() - back.getBlue()) < 10) {
1796 * selection = new Color(Math.abs(255 - main.getRed()), Math .abs(255 -
1797 * main.getGreen()), Math.abs(255 - main.getBlue())); } else { selection = new
1798 * Color((main.getRed() + (back.getRed() * 2)) / 3, (main.getGreen() +
1799 * (back.getGreen() * 2)) / 3, (main .getBlue() + (back.getBlue() * 2)) / 3); }
1800 */
1801 int green = 160;
1802 int red = 160;
1803 int blue = 160;
1804
1805 if (FrameMouseActions.wasDeleteClicked()) {
1806 green = 235;
1807 red = 235;
1808 blue = 140;
1809 } else if (mouseButton == MouseEvent.BUTTON1) {
1810 red = 255;
1811 } else if (mouseButton == MouseEvent.BUTTON2) {
1812 green = 255;
1813 } else if (mouseButton == MouseEvent.BUTTON3) {
1814 blue = 255;
1815 }
1816
1817 return new Color(red, green, blue);
1818 }
1819
1820 @Override
1821 public void paint(Graphics2D g) {
1822 if (!isVisible())
1823 return;
1824
1825 // if there is no text to paint, do nothing.
1826 if (_text == null || _text.length() == 0)
1827 return;
1828
1829 if (_autoWrap || ExperimentalFeatures.AutoWrap.get()) {
1830 invalidateAll();
1831
1832 rebuild(true);
1833 } else if (_textLayouts.size() < 1) {
1834 clipFrameMargin();
1835 rebuild(true);
1836 // return;
1837 }
1838
1839 // check if its a vector item and paint all the vector stuff too if this
1840 // item is a free item
1841 // This will allow for dragging vectors around the place!
1842 if (hasVector() && isFloating()) {
1843 FrameGraphics.requestRefresh(false);
1844 // TODO make this use a more efficient paint method...
1845 // Have the text item return a bigger repaint area if it has an
1846 // associated vector
1847 }
1848
1849 // the background is only cleared if required
1850 if (getBackgroundColor() != null) {
1851 Color bgc = getBackgroundColor();
1852 if (_alpha > 0) {
1853 bgc = new Color(bgc.getRed(), bgc.getGreen(), bgc.getBlue(), _alpha);
1854 }
1855 g.setColor(bgc);
1856
1857 Color gradientColor = getGradientColor();
1858 if (gradientColor != null) {
1859 // The painting is not efficient enough for gradients...
1860 Shape s = getPolygon();
1861 if (s != null) {
1862 Rectangle b = s.getBounds();
1863 GradientPaint gp = new GradientPaint((int) (b.x + b.width * 0.3), b.y, bgc,
1864 (int) (b.x + b.width * 1.3), b.y, gradientColor);
1865 g.setPaint(gp);
1866 }
1867 }
1868
1869 g.fillPolygon(getPolygon());
1870 }
1871
1872 if (hasVisibleBorder()) {
1873 g.setColor(getPaintBorderColor());
1874 Stroke borderStroke = new BasicStroke(getThickness(), CAP, JOIN);
1875 g.setStroke(borderStroke);
1876 g.drawPolygon(getPolygon());
1877 }
1878
1879 if (hasFormula()) {
1880 g.setColor(getPaintHighlightColor());
1881 Stroke highlightStroke = new BasicStroke(1F, CAP, JOIN);
1882 g.setStroke(highlightStroke);
1883
1884 Point2D.Float start = getEdgePosition(0, true);
1885 Point2D.Float end = getEdgePosition(0, false);
1886 g.drawLine(Math.round(start.x), Math.round(start.y), Math.round(end.x), Math.round(end.y));
1887 }
1888
1889 if (isHighlighted()) {
1890 g.setColor(getPaintHighlightColor());
1891 Stroke highlightStroke = new BasicStroke((float) getHighlightThickness(), CAP, JOIN);
1892 g.setStroke(highlightStroke);
1893 if (HighlightMode.Enclosed.equals(getHighlightMode()))
1894 g.fillPolygon(getPolygon());
1895 else
1896 g.drawPolygon(getPolygon());
1897 }
1898
1899 float y = getY();
1900 Color c = getPaintColor();
1901 if (_alpha > 0)
1902 c = new Color(c.getRed(), c.getGreen(), c.getBlue(), _alpha);
1903
1904 g.setColor(c);
1905
1906 Color selection = getSelectionColor(FrameMouseActions.getLastMouseButton());
1907
1908 // width -= getX();
1909 // int line = 0;
1910 // boolean tab = false;
1911 synchronized (_textLayouts) {
1912 for (int i = 0; i < _textLayouts.size(); i++) {
1913 TextLayout layout = _textLayouts.get(i);
1914
1915 Point p = getSelectedRange(i);
1916 if (p != null) {
1917 AffineTransform at = new AffineTransform();
1918 AffineTransform orig = g.getTransform();
1919 at.translate(getX() + getJustOffset(layout), y);
1920 g.setTransform(at);
1921
1922 g.setColor(selection);
1923 g.fill(layout.getLogicalHighlightShape(p.x, p.y));
1924
1925 g.setTransform(orig);
1926 g.setColor(c);
1927 }
1928
1929 int ldx = 1 + getX() + getJustOffset(layout); // Layout draw x
1930
1931 boolean debug = false;
1932 if (debug) {
1933 g.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), 40));
1934 Rectangle layout_rect = layout.getPixelBounds(null, ldx, y);
1935 g.fillRect(layout_rect.x, layout_rect.y, layout_rect.width, layout_rect.height);
1936 g.setColor(c);
1937 }
1938
1939 layout.draw(g, ldx, y);
1940
1941 /*
1942 * AffineTransform at = new AffineTransform(); AffineTransform orig =
1943 * g.getTransform(); at.translate(getX() + getJustOffset(layout), y);
1944 * g.setTransform(at); g.draw(layout.getLogicalHighlightShape(0,
1945 * layout.getCharacterCount())); g.setTransform(orig); /*
1946 * if(_text.charAt(_lineOffsets.get(line) + (layout.getCharacterCount() - 1)) ==
1947 * '\t'){ tab = true; x = (int) (getX() + x + 20 + layout.getVisibleAdvance());
1948 * }else{
1949 */
1950 y += getLineDrop(layout);
1951 /*
1952 * tab = false; }
1953 *
1954 * line++;
1955 */
1956 }
1957 }
1958
1959 paintLink(g);
1960 }
1961
1962 @Override
1963 protected Rectangle getLinkDrawArea() { // TODO: Revise
1964 return getDrawingArea()[0];
1965 }
1966
1967 /**
1968 * Determines if this text has any text in it.
1969 *
1970 * @return True if this Item has no text in it, false otherwise.
1971 */
1972 public boolean isEmpty() {
1973 if (_text == null || _text.length() == 0)
1974 return true;
1975
1976 return false;
1977 }
1978
1979 @Override
1980 public Text copy() {
1981 Text copy = new Text(getID());
1982 // copy standard item values
1983 Item.DuplicateItem(this, copy);
1984
1985 // copy values specific to text items
1986 copy.setSpacing(getSpacing());
1987 copy.setInitialSpacing(getInitialSpacing());
1988
1989 copy.setWidth(getWidthToSave());
1990 copy.setJustification(getJustification());
1991 copy.setLetterSpacing(getLetterSpacing());
1992 copy.setWordSpacing(getWordSpacing());
1993 copy.setWidth(getWidthToSave());
1994 copy.setFont(getFont());
1995 if (hasFormula()) {
1996 copy.calculate(getFormula());
1997 } else {
1998 copy.setText(_text.toString());
1999 }
2000 copy.setHidden(!isVisible());
2001 return copy;
2002 }
2003
2004 @Override
2005 public float getSize() {
2006 return getPaintFont().getSize2D();
2007 }
2008
2009 /**
2010 * Returns the number of characters in this Text, excluding new lines.
2011 *
2012 * @return The sum of the length of each line of text
2013 */
2014 public int getLength() {
2015 return _text.length();
2016 }
2017
2018 @Override
2019 public void setSize(float size) {
2020 invalidateAll();
2021 // size *= UserSettings.ScaleFactor;
2022 // Dont want to have size set when duplicating a point which has size 0
2023 if (size < 0)
2024 return;
2025
2026 if (size < MINIMUM_FONT_SIZE)
2027 size = MINIMUM_FONT_SIZE;
2028 setFont(getPaintFont().deriveFont(size));
2029 rebuild(true);
2030 invalidateAll();
2031 }
2032
2033 @Override
2034 public void setAnnotation(boolean val) {
2035 float mouseX = DisplayIO.getFloatMouseX();
2036 float mouseY = FrameMouseActions.MouseY;
2037 Point2D.Float newPoint = new Point2D.Float();
2038 if (val) {
2039 // if this is already an annotation, do nothing
2040 if (isAnnotation())
2041 return;
2042 if (!isLineEnd() && _text.length() > 0 && _text.charAt(0) == DEFAULT_BULLET) {
2043 newPoint.setLocation(insertText("" + (char) KeyEvent.VK_BACK_SPACE, mouseX, mouseY, 1));
2044 if (_text.length() > 0 && _text.charAt(0) == ' ')
2045 newPoint.setLocation(insertText("" + (char) KeyEvent.VK_BACK_SPACE, newPoint.x, newPoint.y, 1));
2046 } else {
2047 newPoint.setLocation(insertText("@", mouseX, mouseY, 0));
2048 }
2049 } else {
2050 // if this is not an annotation, do nothing
2051 if (!isAnnotation())
2052 return;
2053 if (!isLineEnd() && _text.charAt(0) == '@') {
2054 newPoint.setLocation(insertText("" + (char) KeyEvent.VK_BACK_SPACE, mouseX, mouseY, 1));
2055 newPoint.setLocation(insertText(DEFAULT_BULLET_STRING, newPoint.x, newPoint.y, 0));
2056 } else {
2057 newPoint.setLocation(insertText("" + (char) KeyEvent.VK_BACK_SPACE, mouseX, mouseY, 1));
2058 }
2059 }
2060 FrameUtils.setLastEdited(this);
2061 rebuild(true);
2062 DisplayIO.setCursorPosition(newPoint.x, newPoint.y, false);
2063 }
2064
2065 /**
2066 *
2067 */
2068 private void insertString(String toInsert, int pos) {
2069 assert (toInsert.length() > 0);
2070
2071 _text.insert(pos, toInsert);
2072
2073 if (toInsert.length() > 1) {
2074 _lineBreaker = null;
2075 }
2076
2077 if (_lineBreaker != null) {
2078 AttributedString inserting = new AttributedString(_text.toString());
2079 inserting.addAttribute(TextAttribute.FONT, getPaintFont());
2080 _lineBreaker.insertChar(inserting.getIterator(), pos);
2081 }
2082 }
2083
2084 private void deleteChar(int pos) {
2085 _text.deleteCharAt(pos);
2086
2087 if (_text.length() == 0) {
2088 if (this.isLineEnd()) {
2089 // Remove and replace with a dot
2090 FrameKeyboardActions.replaceText(this);
2091 DisplayIO.setCursorPosition(this._x, this._y);
2092 }
2093 return;
2094 }
2095
2096 if (_lineBreaker != null) {
2097 AttributedString inserting = new AttributedString(_text.toString());
2098 inserting.addAttribute(TextAttribute.FONT, getPaintFont());
2099 _lineBreaker.deleteChar(inserting.getIterator(), pos);
2100 }
2101
2102 }
2103
2104 @Override
2105 public boolean isAnnotation() {
2106 if (_text != null && _text.length() > 0 && _text.charAt(0) == '@')
2107 return true;
2108
2109 return false;
2110 }
2111
2112 public boolean isSpecialAnnotation() {
2113 assert _text != null;
2114 String s = _text.toString().toLowerCase();
2115 if (s.length() > 0 && s.indexOf("@") == 0) {
2116 if (s.equals("@old") || s.equals("@ao") || s.equals("@itemtemplate") || s.equals("@parent")
2117 || s.equals("@next") || s.equals("@previous") || s.equals("@first") || s.equals("@i")
2118 || s.equals("@iw") || s.equals("@f"))
2119 return true;
2120 }
2121
2122 return false;
2123 }
2124
2125 @Override
2126 public Item merge(Item merger, int mouseX, int mouseY) {
2127 if (merger.isLineEnd()) {
2128 // Merging line ends onto non line end text is a no-op
2129 if (!isLineEnd())
2130 return null;
2131
2132 if (merger instanceof Text)
2133 insertText(((Text) merger).getText(), mouseX, mouseY);
2134
2135 // Set the position by moving the cursor before calling this
2136 // method!!
2137
2138 List<Line> lines = new LinkedList<Line>();
2139 lines.addAll(merger.getLines());
2140 for (Line line : lines) {
2141 line.replaceLineEnd(merger, this);
2142 }
2143 merger.delete();
2144 this.setOffset(0, 0);
2145 return null;
2146 }
2147
2148 if (!(merger instanceof Text))
2149 return merger;
2150
2151 Text merge = (Text) merger;
2152
2153 // insertText(merge.getText(), mouseX, mouseY);
2154
2155 // if the item being merged has a link
2156 if (merge.getLink() != null) {
2157 // if this item has a link, keep it on the cursor
2158 if (getLink() != null) {
2159 merge.setText(merge.getLink());
2160 merge.setLink(null);
2161 // return merge;
2162 // TODO get this to return the merged item and attach it to the
2163 // cursor only when the user presses the middle button.
2164 } else
2165 setLink(merge.getLink());
2166 }
2167
2168 return null;
2169 }
2170
2171 /**
2172 * Resets the position of the item to the default position for a title item.
2173 *
2174 */
2175 public void resetTitlePosition() {
2176 setPosition(MARGIN_LEFT, MARGIN_LEFT + getBoundsHeight());
2177 Frame modelFrame = getParentOrCurrentFrame();
2178 if (modelFrame != null) {
2179 setRightMargin(modelFrame.getNameItem().getX() - MARGIN_LEFT, true);
2180 } else {
2181 System.out.print("Error: text.resetTitlePosition, getParent or currentFrame returned null");
2182 setRightMargin(MARGIN_LEFT, true);
2183 }
2184 }
2185
2186 /**
2187 * Removes the set of characters up to the first space in this text item.
2188 *
2189 * @return the string that was removed.
2190 */
2191 public String stripFirstWord() {
2192 int firstSpace = _text.toString().indexOf(' ');
2193
2194 // if there is only one word just make it blank
2195 if (firstSpace < 0 || firstSpace + 1 >= _text.length()) {
2196 String text = _text.toString();
2197 setText("");
2198 return text;
2199 }
2200
2201 String firstWord = _text.toString().substring(0, firstSpace);
2202 setText(_text.toString().substring(firstSpace).trim());
2203
2204 return firstWord;
2205 }
2206
2207 public String toString() {
2208 String message = "[" + getFirstLine() + "]" + FRAME_NAME_SEPARATOR;
2209
2210 if (getParent() != null)
2211 return message + getParent().getName();
2212 return message + getDateCreated();
2213 }
2214
2215 public Text getTemplateForm() {
2216 Text template = this.copy();
2217 template.setID(-1);
2218 // reset width of global templates so the widths of the items on the settings
2219 // frames don't cause issues
2220 // this is in response to the fact that FrameCreator.addItem() sets rightMargin
2221 // when it adds items
2222 template.setWidth(null);
2223 /*
2224 * The template must have text otherwise the bounds height will be zero!! This
2225 * will stop escape drop down from working if there is no item template
2226 */
2227 template.setText("@");
2228 return template;
2229 }
2230
2231 @Override
2232 public boolean isNear(int x, int y) {
2233 if (super.isNear(x, y)) {
2234 // TODO check that it is actually near one of the lines of space
2235 // return contains(x, y, getGravity() * 2 + NEAR_DISTANCE);
2236 // at the moment contains ignores gravity when checking the top and
2237 // bottom of text lines... so the cursor must be between two text
2238 // lines
2239 float textY = getY();
2240 float textX = getX();
2241
2242 for (TextLayout text : _textLayouts) {
2243 // check left and right of each box
2244 Rectangle2D textOutline = text.getLogicalHighlightShape(0, text.getCharacterCount()).getBounds2D();
2245
2246 // check if the cursor is within the top, bottom and within the
2247 // gravity of right
2248 if (y - textY > textOutline.getY() - NEAR_DISTANCE
2249 && y - textY < textOutline.getY() + textOutline.getHeight() + NEAR_DISTANCE
2250 && x - textX < textOutline.getWidth() + NEAR_DISTANCE)
2251 return true;
2252 textY += getLineDrop(text);
2253 }
2254 }
2255 return false;
2256 }
2257
2258 @Override
2259 public void anchor() {
2260 super.anchor();
2261 // ensure all text items have their selection cleared
2262 clearSelection();
2263 setAlpha(0);
2264 if (isLineEnd())
2265 DisplayIO.setCursor(Item.DEFAULT_CURSOR);
2266
2267 String text = _text.toString().trim();
2268
2269 clipFrameMargin();
2270
2271 // Show the overlay stuff immediately if this is an overlay item
2272 if (hasLink() && (text.startsWith("@ao") || text.startsWith("@o"))) {
2273 FrameKeyboardActions.Refresh();
2274 }
2275 }
2276
2277 private void clipFrameMargin() {
2278 if (!hasFixedWidth()) {
2279 int frameWidth = FrameGraphics.getMaxFrameSize().width;
2280 /*
2281 * Only change width if it is more than 150 pixels from the right of the screen
2282 */
2283 if (!_text.toString().contains(" ")) {
2284 Integer width = getWidth();
2285 if (width == null || width < 0)
2286 setWidth(Integer.MIN_VALUE + 1);
2287 } else if (frameWidth - getX() > ADJUST_WIDTH_THRESHOLD) {
2288 justify(false);
2289 // setRightMargin(frameWidth, false);
2290 }
2291 }
2292 }
2293
2294 public void justify(boolean fixWidth, Polygon enclosure) {
2295 // if autowrap is on, wrapping is done every time we draw
2296 if (ExperimentalFeatures.AutoWrap.get()) {
2297 return;
2298 }
2299
2300 Integer width = FrameGraphics.getMaxFrameSize().width;
2301
2302 // Check if that text item is inside an enclosing rectangle...
2303 // Set its max width accordingly
2304 if (enclosure != null) {
2305 Rectangle bounds = enclosure.getBounds();
2306 if (bounds.width > 200 && getX() < bounds.width / 3 + bounds.x) {
2307 width = bounds.x + bounds.width;
2308 }
2309 }
2310
2311 if (getWidth() == null)
2312 setRightMargin(width, fixWidth);
2313
2314 // Check for the annotation that restricts the width of text items
2315 // on the frame
2316 String widthString;
2317 if ((widthString = getParentOrCurrentFrame().getAnnotationValue("maxwidth")) != null) {
2318 try {
2319 int oldWidth = getWidth();
2320 int maxWidth = Integer.parseInt(widthString);
2321 if (maxWidth < oldWidth)
2322 setWidth(maxWidth);
2323 } catch (NumberFormatException nfe) {
2324 }
2325
2326 }
2327 }
2328
2329 public void justify(boolean fixWidth) {
2330 // if autowrap is on, wrapping is done every time we draw
2331 if (ExperimentalFeatures.AutoWrap.get()) {
2332 return;
2333 }
2334 this.justify(fixWidth, FrameUtils.getEnlosingPolygon());
2335 }
2336
2337 public void resetFrameNamePosition() {
2338 Dimension maxSize = FrameGraphics.getMaxFrameSize();
2339 if (maxSize != null) {
2340 // setMaxWidth(maxSize.width);
2341 setPosition(maxSize.width - getBoundsWidth(), getBoundsHeight());
2342 }
2343 }
2344
2345 @Override
2346 protected int getLinkYOffset() {
2347 if (_textLayouts.size() == 0)
2348 return 0;
2349 return Math.round(-(_textLayouts.get(0).getAscent() / 2));
2350 }
2351
2352 @Override
2353 public String getName() {
2354 return getFirstLine();
2355 }
2356
2357 public static final String TAB_STRING = " ";
2358
2359 public Point2D.Float insertTab(char ch, float mouseX, float mouseY) {
2360 return insertText("" + ch, mouseX, mouseY);
2361 }
2362
2363 public Point2D.Float removeTab(char ch, float mouseX, float mouseY) {
2364 // Insert a space as a flag that it is a backwards tab
2365 return insertText(ch + " ", mouseX, mouseY);
2366 }
2367
2368 public static boolean isBulletChar(char c) {
2369 for (int i = 0; i < BULLETS.length; i++) {
2370 if (BULLETS[i] == c)
2371 return true;
2372 }
2373 return c == '*' || c == '+' || c == '>' || c == '-' || c == 'o';
2374 }
2375
2376 public boolean hasOverlay() {
2377 if (!isAnnotation() || getLink() == null)
2378 return false;
2379 String text = getText().toLowerCase();
2380 // TODO make it so can just check the _overlay variable
2381 // Mike cant remember the reason _overlay var cant be use! opps
2382 if (!text.startsWith("@"))
2383 return false;
2384 return text.startsWith("@o") || text.startsWith("@ao") || text.startsWith("@v") || text.startsWith("@av");
2385 }
2386
2387 public boolean hasSelection() {
2388 return getSelectionSize() > 0;
2389 }
2390
2391 /**
2392 * Dont save text items that are all white space.
2393 */
2394 @Override
2395 public boolean dontSave() {
2396 String text = getText();
2397 assert (text != null);
2398 return text.trim().length() == 0 || super.dontSave();
2399 }
2400
2401 @Override
2402 public boolean calculate(String formula) {
2403 if (FrameGraphics.isXRayMode())
2404 return false;
2405
2406 super.calculate(formula);
2407 if (isFloating() || formula == null || formula.length() == 0) {
2408 return false;
2409 }
2410 formula = formula.replace(':', '=');
2411
2412 String lowercaseFormula = formula.toLowerCase();
2413 ExpediteeJEP myParser = new ExpediteeJEP();
2414
2415 int nextVarNo = 1;
2416
2417 // Add variables from the containing rectangle if the item being
2418 // calculated is inside the enclosing rectangle
2419 Collection<Item> enclosed = getItemsInSameEnclosure();
2420 for (Item i : enclosed) {
2421 if (i == this)
2422 continue;
2423 if (i instanceof Text && !i.isAnnotation()) {
2424 AttributeValuePair pair = i.getAttributeValuePair();
2425 if (pair.hasPair()) {
2426 try {
2427 double value = pair.getDoubleValue();
2428 myParser.addVariable(pair.getAttribute(), value);
2429 // myParser.addVariable("$" + nextVarNo++, value);
2430 } catch (NumberFormatException nfe) {
2431 continue;
2432 } catch (Exception e) {
2433 e.printStackTrace();
2434 }
2435 } // else {
2436 // Add anonomous vars
2437 try {
2438 double value = pair.getDoubleValue();
2439 if (value != Double.NaN)
2440 myParser.addVariable("$" + nextVarNo++, value);
2441 } catch (NumberFormatException nfe) {
2442 continue;
2443 } catch (Exception e) {
2444 e.printStackTrace();
2445 }
2446 // }
2447 }
2448 }
2449
2450 // Add the variables from this frame
2451 myParser.addVariables(this.getParentOrCurrentFrame());
2452 String linkedFrame = getAbsoluteLink();
2453 // Add the relative frame variable if the item is linked
2454 if (linkedFrame != null) {
2455 Frame frame = FrameIO.LoadFrame(linkedFrame);
2456 myParser.addVariables(frame);
2457 // If the frame is linked add vector variable for the frame
2458 if (lowercaseFormula.contains("$frame")) {
2459 myParser.addVectorVariable(frame.getNonAnnotationItems(true), "$frame");
2460 }
2461 }
2462 // Add the relative box variable if this item is a line end
2463 if (this.isLineEnd()) {
2464 // if its a line end add the enclosed stuff as an @variable
2465 if (lowercaseFormula.contains("$box")) {
2466 myParser.addVectorVariable(getEnclosedItems(), "$box");
2467 }
2468 }
2469 myParser.resetObserver();
2470 try {
2471 Node node = myParser.parse(formula);
2472 String result = myParser.evaluate(node);
2473 if (result != null) {
2474 this.setText(result);
2475 this.setFormula(formula);
2476
2477 if (!this.hasAction()) {
2478 setActionMark(false);
2479 setAction("extract formula");
2480 }
2481 }
2482 } catch (Throwable e) {
2483 // e.printStackTrace();
2484 String formula2 = getFormula();
2485 this.setText(formula2);
2486 this.setFormula(formula2);
2487 return false;
2488 }
2489
2490 _attributeValuePair = null;
2491
2492 return true;
2493 }
2494
2495 /**
2496 * Gets items which are in the same enclosure as this item. In the event more
2497 * than one enclosure meets this criteria, then the one returned is the one with
2498 * the smallest area. TODO: Improve the efficiency of this method
2499 *
2500 * @return
2501 */
2502 public Collection<Item> getItemsInSameEnclosure() {
2503 Collection<Item> sameEnclosure = null;
2504 Collection<Item> seen = new HashSet<Item>();
2505 Frame parent = getParentOrCurrentFrame();
2506 double enclosureArea = Double.MAX_VALUE;
2507 for (Item i : parent.getVisibleItems()) {
2508 /*
2509 * Go through all the enclosures looking for one that includes this item
2510 */
2511 if (!seen.contains(i) && i.isEnclosed()) {
2512 seen.addAll(i.getEnclosingDots());
2513 Collection<Item> enclosed = i.getEnclosedItems();
2514 // Check if we have found an enclosure containing this item
2515 // Check it is smaller than any other enclosure found containing
2516 // this item
2517 if (enclosed.contains(this) && i.getEnclosedArea() < enclosureArea) {
2518 sameEnclosure = enclosed;
2519 }
2520 }
2521 }
2522
2523 if (sameEnclosure == null)
2524 return new LinkedList<Item>();
2525
2526 return sameEnclosure;
2527 }
2528
2529 /**
2530 * Returns true if items of the parent frame should be recalculated when this
2531 * item is modified
2532 */
2533 public boolean recalculateWhenChanged() {
2534 if (/*
2535 * !isAnnotation() &&
2536 */(hasFormula() || isLineEnd()))
2537 return true;
2538 try {
2539 AttributeValuePair avp = getAttributeValuePair();
2540
2541 if (!avp.getDoubleValue().equals(Double.NaN))
2542 return true;
2543 } catch (Exception e) {
2544 e.printStackTrace();
2545 }
2546
2547 return false;
2548 }
2549
2550 public float getLineHeight() {
2551 return getLineDrop(_textLayouts.get(0));
2552 }
2553
2554 @Override
2555 public void setAnchorLeft(Float anchor) {
2556 if (!isLineEnd()) {
2557 super.setAnchorLeft(anchor);
2558 // Subtract off the link width
2559 if (anchor != null) {
2560 setX(anchor + getLeftMargin());
2561 }
2562 return;
2563 }
2564 invalidateFill();
2565 invalidateCommonTrait(ItemAppearence.PreMoved);
2566
2567 this._anchorLeft = anchor;
2568 this._anchorRight = null;
2569
2570 int oldX = getX();
2571 if (anchor != null) {
2572 float deltaX = anchor + getLeftMargin() - oldX;
2573 anchorConnected(AnchorEdgeType.Left, deltaX);
2574 }
2575
2576 invalidateCommonTrait(ItemAppearence.PostMoved);
2577 invalidateFill();
2578 }
2579
2580 @Override
2581 public void setAnchorRight(Float anchor) {
2582 if (!isLineEnd()) {
2583 super.setAnchorRight(anchor);
2584 // Subtract off the link width
2585 if (anchor != null) {
2586 setX(FrameGraphics.getMaxFrameSize().width - anchor - getBoundsWidth() + getLeftMargin());
2587 }
2588 return;
2589 }
2590 invalidateFill();
2591 invalidateCommonTrait(ItemAppearence.PreMoved);
2592
2593 this._anchorRight = anchor;
2594 this._anchorLeft = null;
2595
2596 int oldX = getX();
2597 if (anchor != null) {
2598 float deltaX = FrameGraphics.getMaxFrameSize().width - anchor - getBoundsWidth() + getLeftMargin() - oldX;
2599 anchorConnected(AnchorEdgeType.Right, deltaX);
2600 }
2601
2602 invalidateCommonTrait(ItemAppearence.PostMoved);
2603 invalidateFill();
2604 }
2605
2606 @Override
2607 public void setAnchorTop(Float anchor) {
2608 if (!isLineEnd()) {
2609 super.setAnchorTop(anchor);
2610 if (anchor != null) {
2611 setY(anchor + _textLayouts.get(0).getAscent());
2612 }
2613 return;
2614 }
2615 invalidateFill();
2616 invalidateCommonTrait(ItemAppearence.PreMoved);
2617
2618 this._anchorTop = anchor;
2619 this._anchorBottom = null;
2620
2621 int oldY = getY();
2622 if (anchor != null) {
2623 float deltaY = anchor - oldY;
2624 anchorConnected(AnchorEdgeType.Top, deltaY);
2625 }
2626
2627 invalidateCommonTrait(ItemAppearence.PostMoved);
2628 invalidateFill();
2629 }
2630
2631 @Override
2632 public void setAnchorBottom(Float anchor) {
2633 if (!isLineEnd()) {
2634 super.setAnchorBottom(anchor);
2635 if (anchor != null) {
2636 setY(FrameGraphics.getMaxFrameSize().height - (anchor + this.getBoundsHeight()
2637 - _textLayouts.get(0).getAscent() - _textLayouts.get(0).getDescent()));
2638 }
2639 return;
2640 }
2641 invalidateFill();
2642 invalidateCommonTrait(ItemAppearence.PreMoved);
2643
2644 this._anchorBottom = anchor;
2645 this._anchorTop = null;
2646
2647 int oldY = getY();
2648 if (anchor != null) {
2649
2650 float deltaY = FrameGraphics.getMaxFrameSize().height - anchor - oldY;
2651 anchorConnected(AnchorEdgeType.Bottom, deltaY);
2652 }
2653
2654 invalidateCommonTrait(ItemAppearence.PostMoved);
2655 invalidateFill();
2656 }
2657
2658 @Override
2659 public void scale(Float scale, int originX, int originY) {
2660 setSize(getSize() * scale);
2661
2662 Integer width = getWidth();
2663 if (width != null) {
2664 setWidth(Math.round(width * scale));
2665 }
2666
2667 super.scale(scale, originX, originY);
2668 rebuild(true);
2669 }
2670
2671 protected Rectangle getPixelBounds(TextLayout layout) {
2672 // Does 'layout' need to be synchronized (similar to _textLayouts below)??
2673 int x = getX();
2674 int y = getY();
2675
2676 int ldx = 1 + x + getJustOffset(layout); // Layout draw x
2677 Rectangle layout_rect = layout.getPixelBounds(null, ldx, y);
2678
2679 return layout_rect;
2680 }
2681
2682 /**
2683 * Creates the smallest possible rectangle object to enclose the Text Item
2684 * completely. Width of the rectangle is determined by the line in the Text Item
2685 * that protrudes to the right the most. Height of the rectangle is determined
2686 * by the number of lines in the Text Item.
2687 *
2688 * @return A rectangle enclosing the Text Item, without gravity represented.
2689 * @see #getPixelBoundsUnionTight()
2690 */
2691 public Rectangle getPixelBoundsUnion() {
2692 final Rectangle rect = getPixelBounds(_textLayouts.get(0));
2693
2694 int cumulativeHeight = rect.height;
2695 int maxWidth = rect.width;
2696
2697 if (_textLayouts.size() > 1) {
2698 for (int i = 1; i < _textLayouts.size(); i++) {
2699 final Rectangle r = getPixelBounds(_textLayouts.get(i));
2700 cumulativeHeight += _textLayouts.get(i).getDescent() + _textLayouts.get(i).getAscent();
2701 if (r.width > maxWidth)
2702 maxWidth = r.width;
2703 }
2704 }
2705
2706 rect.setSize(maxWidth, cumulativeHeight);
2707
2708 return rect;
2709 }
2710
2711 /**
2712 * Creates the smallest possible polygon to enclose the Text Item completely.
2713 * The shape of the polygon is determined by the length of each line, tightly
2714 * fitting the shape so that no white space is inside the resulting polygon.
2715 *
2716 * @return A polygon enclosing the Text Item, without gravity represented.
2717 * @see #getPixelBoundsUnion()
2718 */
2719 public Polygon getPixelBoundsUnionTight() {
2720 final Rectangle rect = getPixelBounds(_textLayouts.get(0));
2721 if (_textLayouts.size() == 1) {
2722 int x = (int) rect.getX();
2723 int y = (int) rect.getY();
2724 return new Polygon(new int[] { x, x + rect.width, x + rect.width, x },
2725 new int[] { y, y, y + rect.height, y + rect.height }, 4);
2726 } else {
2727 final Polygon poly = new Polygon();
2728 poly.addPoint(rect.x, rect.y);
2729 poly.addPoint(rect.x + rect.width, rect.y);
2730 poly.addPoint(rect.x + rect.width, Math.round(rect.y + rect.height + _textLayouts.get(0).getDescent()));
2731 int y = (int) (rect.y + rect.height + _textLayouts.get(0).getDescent());
2732 for (int i = 1; i < _textLayouts.size(); i++) {
2733 final Rectangle r = getPixelBounds(_textLayouts.get(i));
2734 poly.addPoint(r.x + r.width, y);
2735 poly.addPoint(r.x + r.width, Math.round(y + r.height + _textLayouts.get(i).getDescent()));
2736 y = Math.round(y + r.height + _textLayouts.get(i).getDescent());
2737 }
2738 poly.addPoint(rect.x + getPixelBounds(_textLayouts.get(_textLayouts.size() - 1)).width, Math.round(y + _textLayouts.get(_textLayouts.size() - 1).getDescent()));
2739 poly.addPoint(rect.x, Math.round(y + _textLayouts.get(_textLayouts.size() - 1).getDescent()));
2740 return poly;
2741 }
2742 }
2743
2744 // public Rectangle getPixelBoundsUnion()
2745 // {
2746 // synchronized (_textLayouts) {
2747 //
2748 // int x = getX();
2749 // int y = getY();
2750 //
2751 // int min_xl = Integer.MAX_VALUE;
2752 // int max_xr = Integer.MIN_VALUE;
2753 //
2754 // int min_yt = Integer.MAX_VALUE;
2755 // int max_yb = Integer.MIN_VALUE;
2756 //
2757 //
2758 // for (int i = 0; i < _textLayouts.size(); i++) {
2759 // TextLayout layout = _textLayouts.get(i);
2760 //
2761 // int ldx = 1+x+getJustOffset(layout); // Layout draw x
2762 // Rectangle layout_rect = layout.getPixelBounds(null, ldx, y);
2763 //
2764 // int xl = layout_rect.x;
2765 // int xr = xl + layout_rect.width -1;
2766 //
2767 // int yt = layout_rect.y;
2768 // int yb = yt + layout_rect.height -1;
2769 //
2770 // min_xl = Math.min(min_xl,xl);
2771 // max_xr = Math.max(max_xr,xr);
2772 //
2773 // min_yt = Math.min(min_yt,yt);
2774 // max_yb = Math.max(max_yb,yb);
2775 // }
2776 //
2777 // if ((min_xl >= max_xr) || (min_yt >= max_yb)) {
2778 // // No valid rectangle are found
2779 // return null;
2780 // }
2781 //
2782 // return new Rectangle(min_xl,min_yt,max_xr-min_xl+1,max_yb-min_yt+1);
2783 //
2784 // }
2785 //
2786 // }
2787 /*
2788 * Returns the SIMPLE statement contained by this text item.
2789 *
2790 */
2791 public String getStatement() {
2792 return getText().split("\\s+")[0];
2793 }
2794
2795 public boolean getAutoWrap() {
2796 return _autoWrap;
2797 }
2798
2799 // workaround since true is the default value and would not be displayed
2800 // normally
2801 public String getAutoWrapToSave() {
2802 if (!_autoWrap) {
2803 return null;
2804 }
2805 return "true";
2806 }
2807
2808 public void setAutoWrap(boolean autoWrap) {
2809 _autoWrap = autoWrap;
2810 }
2811}
Note: See TracBrowser for help on using the repository browser.