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

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