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

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