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

Last change on this file since 1511 was 1511, checked in by bnemhaus, 4 years ago

Frame::Parse has been updated to include a new boolean parameter. When true, widgets that are created as a result of the parse send not only notify the widget framework that they have been added, but are also visible. When false, they only notify they have been added.

The widget framework now distinguishes between added and visible widgets, this fixes a bug. Bug: when programmatically adding a widget to not the current frame, it never gets properly removed and therefore still catches click events from users. By distinguishing between adding and making visible this is avoided.


Another bug has been fixed. Bug: When setting a text item to have a right anchor, and then subsequently reducing the size of the window, this text item would get a width of zero assigned. This was caused by some issues with the logic of how right margins for items were calculated.

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