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

Last change on this file since 1101 was 1101, checked in by davidb, 6 years ago

Merging of Bryce's change in getPixelBoundsUnion() with Corey's logic/graphics separation

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