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

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

Added placeholders as a thing for text items. You can now provide default text for a text item. Inject the property 'Placeholder: <text>' were <text> is the placeholder text. When the placeholder is being used, the font color alpha is reduced.

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