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

Last change on this file since 914 was 914, checked in by davidb, 10 years ago

Implicit Boxing

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