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

Last change on this file since 873 was 873, checked in by ngw8, 10 years ago

Changed the way anchoring top and anchoring bottom works for text.

Summary since this is an insanely long commit message for a only few lines of changes:
Change to text top anchoring is essentially a bug fix, change to text bottom anchoring is a slight change in behaviour to make it consistent with all other anchoring.

Since text is positioned relative to the baseline of the first line, in order to have the top of the text against the top of the frame, its y-position should equal the distance from the top of the text to the baseline of the first line (i.e. getAscent()). Previously anchoring top was setting the y-position to [anchor] + [bounds height], which sort of worked for a single line of text, but didn't work well for multi-line text (since the text y-position was being offset by the text's entire height, not just the height of the first line).

Anchoring text to the bottom was previously setting the y-position to [FrameHeight] - [anchor], which meant the baseline of the first line of text was always positioned at [anchor] from the bottom (so setting anchorBottom to 0 with multi-line text caused all but the first line to be below the bottom of the frame). This didn't really seem like the expected behaviour for anything apart from single-line text, given that bottom-anchoring an image at 0 places its bottom edge (rather than its top edge, where it is positioned relative to) against the bottom edge of the frame, and right-anchoring a text item positions it so that the text _ends_ at [offset] from the right edge. To be consistent with all the other anchoring, bottom anchoring of text now positions the text so that the text ends at [offset] from the bottom of the frame.

Also had to change DefaultFrameWriter to write the text tag before the anchor tags, otherwise when reading in a frame setAnchorTop/Bottom would have no text to get the ascent/descent for. Frames saved before this commit that use anchor bottom/top will probably still throw an exception (that gets caught), but the frame should load and display fine.

This shouldn't break any functionality or default frames (only Exploratory Search overlays which I still need to update anyway), but may require existing frames that were using anchor top/bottom to be adjusted slightly. Feel free to revert if it does break significant stuff, although this does seem to be how anchoring text is intended to work.

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