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

Last change on this file since 1382 was 1382, checked in by bln4, 5 years ago

Functionality for tab index. When a user is cursored over a text item with a _tabIndex property and hits tab (or shift tab), instead of adding a tab to the text item, it will find another text item on the frame with the next highest (or lowerest) _tabIndex property and move the cursor to it.

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