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

Last change on this file since 906 was 906, checked in by bln4, 10 years ago

Added Magnetic Constraints

Magnetic Constraints are a new type of constraint between items; particularly text. Items can have their neighbours set (for example: who is to my right) and then have custom actions run when a item and its neighbour interact in a defined way. For example: one text item grows enough for hits its right neighbour; so its right neighbour moves over to compensate.
Files changed: org.expeditee.{ gui.DisplayIO, io.DefaultFrameReader, io.DefaultFrameWriter, items.Item, items.Text }

Added ability to have 'delayed' properties on items. With the invention of Magnetic Constraints it was found that the saving and loading of properties now required (greatly benifited from) a way to set a property after all items have been loaded in; rather than as the perticular item was being loaded in. This is that ability.

Files changed: org.expeditee.io.{ DefaultFrameReader, ExpReader }

Let through some non letter characters as names of properties. The tokens ',', '', and '_' have been used as property names to describe neighbors. In order to do this the isValidLine(String) method was altered to let these through. This is not the ideal solution, and should be looked at later.

Files changed: org.expeditee.io.ExpReader

Bug fix: setting a custom title support was added previously. If a custom title had been set the title was: [Your Custom Title] ~ [Expeditee Title]. However the tildas where showing up even without a custom title set. This is now fixed.

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