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

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

Added Spell Checker
Added word count stats
Fixed some mail stuff

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