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

Last change on this file since 1047 was 1047, checked in by davidb, 8 years ago

Anchor top and left added as minus options to an @i item. Relocation of ParseArgsApache(). Addition of getData type function for Bool. Some typos fixed in comments. Left click on image refinement: it check the pixel clicked on in the image, and if the same as the background, it does a back() operation rather than forming a new link

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