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

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

org.expeditee.items.Text ->

Fixed an issue with all text items having the same Font object.
Added some curly brackets to help David sleep at night.

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