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

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

org.apollo.items.EmulatedTextItem ->
org.expeditee.agents.GraphFramesetLinks ->
org.expeditee.gio.FontManager ->
org.expeditee.gio.javafx.JavaFXFontManager ->
org.expeditee.gio.swing.SwingFontManager ->
org.expeditee.gui.MessageBay ->
org.expeditee.io.WebParser ->
org.expeditee.items.Text ->

The above files are been altered so that there is no longer any chance Font references will be shared amongst Text Items. How default Fonts are used has been altered so that a Text Item using the default font remember that this is the case. This is a follow up to an initial fix for the same problem that turned out to be insufficient.

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