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

Last change on this file since 307 was 307, checked in by ra33, 16 years ago

Can navigate with PgUp PgDn Home and End with remote framesets

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