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

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