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

Last change on this file since 586 was 586, checked in by jts21, 11 years ago

Fix Text items displaying negative widths when their width should be unset

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