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

Last change on this file since 1527 was 1527, checked in by bnemhaus, 4 years ago

home1 frame updated to include link to settings. Also now makes use of the AnchorCenterX anchor that was previously introduced.

File size: 101.7 KB
RevLine 
[919]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
[4]19package org.expeditee.items;
20
[777]21import java.io.File;
[1047]22import java.util.ArrayList;
[242]23import java.util.Collection;
[594]24import java.util.HashMap;
[242]25import java.util.HashSet;
[4]26import java.util.LinkedList;
27import java.util.List;
[1047]28import java.util.StringTokenizer;
[1210]29import java.util.stream.Stream;
[4]30
[1101]31import org.expeditee.core.Colour;
32import org.expeditee.core.Dimension;
33import org.expeditee.core.Fill;
34import org.expeditee.core.Font;
35import org.expeditee.core.GradientFill;
36import org.expeditee.core.Point;
37import org.expeditee.core.Range;
38import org.expeditee.core.Stroke;
39import org.expeditee.core.TextHitInfo;
40import org.expeditee.core.TextLayout;
41import org.expeditee.core.bounds.AxisAlignedBoxBounds;
42import org.expeditee.core.bounds.PolygonBounds;
[1413]43import org.expeditee.encryption.items.surrogates.EncryptionDetail;
[1101]44import org.expeditee.gio.EcosystemManager;
45import org.expeditee.gio.GraphicsManager;
46import org.expeditee.gio.gesture.StandardGestureActions;
[242]47import org.expeditee.gui.AttributeValuePair;
[1101]48import org.expeditee.gui.DisplayController;
[156]49import org.expeditee.gui.Frame;
[176]50import org.expeditee.gui.FrameIO;
[247]51import org.expeditee.gui.FrameUtils;
[613]52import org.expeditee.gui.FreeItems;
[1101]53import org.expeditee.gui.MessageBay;
[1410]54import org.expeditee.io.DefaultFrameWriter;
[906]55import org.expeditee.items.MagneticConstraint.MagneticConstraints;
[190]56import org.expeditee.math.ExpediteeJEP;
[632]57import org.expeditee.settings.experimental.ExperimentalFeatures;
[1101]58import org.expeditee.stats.Formatter;
[176]59import org.nfunk.jep.Node;
[4]60
61/**
62 * Represents text displayed on the screen, which may include multiple lines.
63 * All standard text properties can be set (font, style, size).
64 *
65 * @author jdm18
66 *
67 */
68public class Text extends Item {
[400]69 private static final int ADJUST_WIDTH_THRESHOLD = 200;
[1210]70
[1101]71 public static final char DELETE_CHARACTER = 0x7F;
[1210]72
[1101]73 public static final char BACKSPACE_CHARACTER = '\b';
[1210]74
[1101]75 public static final char TAB_CHARACTER = '\t';
[1210]76
[1101]77 public static final char ESC_CHARACTER = 0x1B;
[400]78
[115]79 public static String LINE_SEPARATOR = System.getProperty("line.separator");
[137]80
[776]81 // public static char[] BULLETS = { '\u2219', '\u2218', '\u2217' };
82 public static char[] BULLETS = { '\u25AA', '\u25AB', '\u2217' };
[137]83
[115]84 private static char DEFAULT_BULLET = BULLETS[2];
[137]85
[115]86 private static String DEFAULT_BULLET_STRING = DEFAULT_BULLET + " ";
[137]87
[88]88 private String[] _processedText = null;
[7]89
[88]90 public String[] getProcessedText() {
91 return _processedText;
[7]92 }
93
[88]94 public void setProcessedText(String[] tokens) {
95 _processedText = tokens;
[7]96 }
97
[4]98 public static final String FRAME_NAME_SEPARATOR = " on frame ";
99
[1101]100 /** The default font used to display text items if no font is specified. */
[4]101 public static final String DEFAULT_FONT = "Serif-Plain-18";
102
[1101]103 public static final Colour DEFAULT_COLOR = Colour.BLACK;
[4]104
105 public static final int MINIMUM_RANGED_CHARS = 2;
106
[105]107 public static final int NONE = 0;
[4]108
[105]109 public static final int UP = 1;
[4]110
[105]111 public static final int DOWN = 2;
[4]112
[105]113 public static final int LEFT = 3;
[4]114
[105]115 public static final int RIGHT = 4;
[4]116
[105]117 public static final int HOME = 5;
[4]118
[147]119 public static final int LINE_HOME = 9;
120
121 public static final int LINE_END = 10;
122
[105]123 public static final int END = 6;
[13]124
[105]125 public static final int PAGE_DOWN = 7;
[13]126
[105]127 public static final int PAGE_UP = 8;
128
[1101]129 /**
[1095]130 * Set the width to be IMPLICIT, but as wide as possible, a negative width value
131 * is one that is implicitly set by the system... a positive value is one
132 * explicitly set by the user.
[410]133 */
[1101]134 /**
[1210]135 * The maximum allowable width of the Text item. Actual width may be less than
136 * this value, subject to text wrapping. Negative values indicate the width was
137 * implicitly set by the system, positive values indicate explicit setting by
138 * the user. Initially set to be as wide as possible.
[1101]139 */
[1369]140 private Integer _width = -Integer.MAX_VALUE;
[1210]141
[1200]142 private Integer _minWidth = -Integer.MAX_VALUE;
[1369]143
[1379]144 private boolean _singleLine = false;
145
[147]146 private Justification _justification = Justification.left;
[4]147
[659]148 private float _spacing = -1;
[4]149
150 private int _word_spacing = -1;
151
[659]152 private float _initial_spacing = 0;
[4]153
[659]154 private float _letter_spacing = 0;
[4]155
156 // used during ranging out
157 private int _selectionStart = -1;
158
159 private int _selectionEnd = -1;
[1381]160
161 private int _tabIndex = -1;
[1095]162
[1101]163 /** Keeps track of the last Text item selected. */
164 private static Text _lastSelected = null;
[1210]165
[1101]166 // Range selection colours
167 /** Colour of selected range when for selecting text. */
168 public static final Colour RANGE_SELECT_COLOUR = Colour.FromRGB255(255, 160, 160);
169 /** Colour of selected range when for cutting text. */
170 public static final Colour RANGE_CUT_COLOUR = Colour.FromRGB255(160, 255, 160);
171 /** Colour of selected range when for copying text. */
172 public static final Colour RANGE_COPY_COLOUR = Colour.FromRGB255(160, 160, 255);
173 /** Colour of selected range when for deleting text. */
174 public static final Colour RANGE_DELETE_COLOUR = Colour.FromRGB255(235, 235, 140);
[1210]175
[1101]176 /** The colour to draw range selections in. */
177 private Colour _selectionColour = RANGE_SELECT_COLOUR;
[1210]178
[613]179 // whether autowrap is on/off for this item
180 protected boolean _autoWrap = false;
[137]181
[4]182 // text is broken up into lines
[1200]183 protected StringBuffer _text = new StringBuffer();
[1296]184
185 // place holder text to use if _text is empty.
186 protected StringBuffer _placeholder = new StringBuffer();
[4]187
[1210]188 private List<TextLayout> _textLayouts = new LinkedList<TextLayout>();
189 private List<TextLayout> _maskTextLayouts = new LinkedList<TextLayout>();
[1296]190 private List<TextLayout> _placeholderTextLayouts = new LinkedList<TextLayout>();
[4]191
192 // The font to display this text in
193 private Font _font;
[1210]194
[1200]195 // The optional mask character to us in place of the text's content.
196 private Integer _mask = null;
[1095]197
[1210]198 protected static void InitFontFamily(File fontFamilyDir) {
[836]199 File[] fontFiles = fontFamilyDir.listFiles();
[4]200
[836]201 for (File fontFile : fontFiles) {
202 String ext = "";
203 String fileName = fontFile.getName().toLowerCase();
204
205 int i = fileName.lastIndexOf('.');
206 int p = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\'));
207
208 if (i > p) {
[1095]209 ext = fileName.substring(i + 1);
[777]210 }
[836]211
212 if (ext.equals("ttf")) {
213
214 try {
[1101]215 Font font = EcosystemManager.getFontManager().registerFontFile(fontFile);
[836]216
[1101]217 if (font != null) {
[982]218
[1210]219 String font_family = font.getFamilyName();
220 if (!FONT_WHEEL_ADDITIONAL_LOOKUP.containsKey(font_family)) {
221
[1095]222 if (FONT_WHEEL_ADDITIONAL_LOOKUP.size() > 0) {
223 System.out.print(", ");
224 }
[1101]225 System.out.print("'" + font_family + "'");
[1095]226
227 FONT_WHEEL_ADDITIONAL_LOOKUP.put(font_family, font);
228
[1101]229 /*
[1210]230 * int cdut = font.canDisplayUpTo("09AZaz"); if (cdut >= 0) { // Some problem
231 * has occured (should return -1 to show all chars possible)
232 *
233 * System.out.println(" [Non-ASCII font]"); }
234 */
[1095]235 System.out.flush();
[982]236 }
237
[1244]238 //System.out.print("'" + font_family + "'");
239
[1095]240 } else {
241 System.err.println("Error: Failed to add custom True-Type Font file: " + fontFile);
[982]242 }
[1095]243
244 } catch (Exception e) {
[836]245 System.err.println("Failed to load custon font file: " + fontFile);
246 }
[777]247 }
248 }
[836]249 }
[1095]250
[836]251 public static void InitFonts() {
[1095]252
[1101]253 File fontDirectory = new File(FrameIO.FONT_PATH);
254 if (fontDirectory != null) {
255 File[] fontFamilyDirs = fontDirectory.listFiles();
256 if (fontFamilyDirs != null) {
[836]257
[1101]258 if (fontFamilyDirs.length > 0) {
259 System.out.println("Loading custom fonts:");
260 }
[836]261
[1101]262 for (File fontFamilyDir : fontFamilyDirs) {
263 if (fontFamilyDir.isDirectory()) {
264 InitFontFamily(fontFamilyDir);
[836]265 }
266 }
267 }
268 }
[777]269 }
270
[1047]271 /**
[1095]272 * Similar to parseArgs() in InteractiveWidget. Code based on routine used in
273 * Apache Ant: org.apache.tools.ant.types.Commandline::translateCommandline()
274 *
275 * @param toProcess
276 * the command line to process.
277 * @return the command line broken into strings. An empty or null toProcess
278 * parameter results in a zero sized array.
279 */
280 public static String[] parseArgsApache(String toProcess) {
281 if (toProcess == null || toProcess.length() == 0) {
282 // no command? no string
283 return new String[0];
284 }
285 // parse with a simple finite state machine
[836]286
[1095]287 final int normal = 0;
288 final int inQuote = 1;
289 final int inDoubleQuote = 2;
290 int state = normal;
291 final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true);
292 final ArrayList<String> result = new ArrayList<String>();
293 final StringBuilder current = new StringBuilder();
294 boolean lastTokenHasBeenQuoted = false;
[1047]295
[1095]296 while (tok.hasMoreTokens()) {
297 String nextTok = tok.nextToken();
298 switch (state) {
299 case inQuote:
300 if ("\'".equals(nextTok)) {
301 lastTokenHasBeenQuoted = true;
302 state = normal;
303 } else {
304 current.append(nextTok);
305 }
306 break;
307 case inDoubleQuote:
308 if ("\"".equals(nextTok)) {
309 lastTokenHasBeenQuoted = true;
310 state = normal;
311 } else {
312 current.append(nextTok);
313 }
314 break;
315 default:
316 if ("\'".equals(nextTok)) {
317 state = inQuote;
318 } else if ("\"".equals(nextTok)) {
319 state = inDoubleQuote;
320 } else if (" ".equals(nextTok)) {
321 if (lastTokenHasBeenQuoted || current.length() != 0) {
322 result.add(current.toString());
323 current.setLength(0);
324 }
325 } else {
326 current.append(nextTok);
327 }
328 lastTokenHasBeenQuoted = false;
329 break;
330 }
331 }
332 if (lastTokenHasBeenQuoted || current.length() != 0) {
333 result.add(current.toString());
334 }
335 if (state == inQuote || state == inDoubleQuote) {
336 System.err.println("Error: Unbalanced quotes -- failed to parse '" + toProcess + "'");
337 return null;
338 }
[1047]339
[1095]340 return result.toArray(new String[result.size()]);
341 }
[1047]342
[4]343 /**
344 * Creates a new Text Item with the given ID and text.
345 *
346 * @param id
347 * The id of this item
348 * @param text
349 * The text to use in this item
350 */
351 public Text(int id, String text) {
352 super();
353 _text.append(text);
354 rebuild(false);
355
356 setID(id);
357 }
[137]358
[108]359 /**
360 * Creates a text item which is not added to the frame.
[137]361 *
[108]362 * @param text
363 */
364 public Text(String text) {
365 super();
366 _text.append(text);
367 rebuild(false);
368 setID(-1);
369 }
[4]370
371 /**
372 * Creates a new Text Item with the given ID
373 *
374 * @param id
375 * The ID to of this item
376 */
377 public Text(int id) {
378 super();
379 setID(id);
380 }
381
[1101]382 public Text(int i, String string, Colour foreground, Colour background) {
[97]383 this(i, string);
384 this.setColor(foreground);
385 this.setBackgroundColor(background);
386 }
387
[4]388 /**
389 * @param width
[1095]390 * The maximum width of this item when justification is applied to
391 * it.
[4]392 */
393 @Override
[400]394 public void setWidth(Integer width) {
[185]395 invalidateAll();
[400]396
397 if (width == null) {
[362]398 setJustification(Justification.left);
[1258]399 setRightMargin(DisplayController.getFramePaintAreaWidth(), false);
[400]400 return;
[362]401 }
[1511]402
403 if (width == 0) {
404 System.err.println("Width of Zero: " + getText());
405 }
406
[1369]407 _width = width;
[776]408 rebuild(true);
[185]409 invalidateAll();
[1413]410
411 if (isSurrogate()) {
412 surrogatePropertyInheritance.put(DefaultFrameWriter.WIDTH_TO_SAVE_STR, false);
[1414]413 Item primary = getPrimary();
[1413]414 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.WIDTH_TO_SAVE_STR)) {
[1415]415 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
416 primary.primaryPropertyEncryption.put(DefaultFrameWriter.WIDTH_TO_SAVE_STR, inheritanceCheckOnSave);
[1413]417 }
418 }
[4]419 }
[1210]420
[1200]421 @Override
422 public void setMinWidth(final Integer width) {
423 invalidateAll();
[1210]424
[1200]425 if (width == null) {
426 setJustification(Justification.left);
[1258]427 setRightMargin(DisplayController.getFramePaintAreaWidth(), false);
[1200]428 return;
429 }
[1210]430
[1200]431 _minWidth = width;
432 rebuild(true);
433 invalidateAll();
[1413]434
435 if (isSurrogate()) {
436 surrogatePropertyInheritance.put(DefaultFrameWriter.MIN_WIDTH_TO_SAVE_STR, false);
[1414]437 Item primary = getPrimary();
[1413]438 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.MIN_WIDTH_TO_SAVE_STR)) {
[1415]439 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
440 primary.primaryPropertyEncryption.put(DefaultFrameWriter.MIN_WIDTH_TO_SAVE_STR, inheritanceCheckOnSave);
[1413]441 }
442 }
[1200]443 }
[4]444
445 /**
[1210]446 * <<<<<<< .mine Returns the maximum width of this Text item when justification
447 * is used. If the width is negative, it means no explicit width has been set
448 * ||||||| .r1094 Returns the maximum width of this Text item when justifcation
449 * is used. If the width is negative, it means no explicit width has been set
450 * ======= Returns the maximum width of this Text item when justifcation is
451 * used. If the width is negative, it means no explicit width has been set
452 * >>>>>>> .r1100
[4]453 *
454 * @return The maximum width of this Text item when justification is used
455 */
456 @Override
[1210]457 public Integer getWidth() {
[1413]458 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.WIDTH_TO_SAVE_STR)) {
[1414]459 return this.getPrimary().getWidth();
[1413]460 } else {
[1415]461 if (_width == null || _width <= 0)
462 return null;
[1413]463 return _width;
[1190]464 }
[4]465 }
466
[400]467 public Integer getAbsoluteWidth() {
[1415]468 if (_width == null || _width == Integer.MIN_VALUE) {
469 // When absoluting Integer.MIN_VALUE, the java API is defined to give you back Integer.MIN_VALUE!!?!
470 // This is because of the asymmetry of two's complement integer representation.
471 // We would prefer to use Integer.MAX_VALUE in this circumstance.
[400]472 return Integer.MAX_VALUE;
473 }
[1415]474
[1369]475 return Math.abs(_width);
[400]476 }
[1210]477
[1200]478 public Integer getMinWidth() {
[1413]479 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.MIN_WIDTH_TO_SAVE_STR)) {
[1414]480 return this.getPrimary().getMinWidth();
[1413]481 } else {
[1415]482 if (_minWidth == null || _minWidth <= 0)
483 return null;
[1413]484 return _minWidth;
[1200]485 }
486 }
[1210]487
[1200]488 public Integer getAbsoluteMinWidth() {
489 if (_minWidth == null) {
490 return Integer.MAX_VALUE;
491 }
492 return Math.abs(_minWidth);
493 }
[1369]494
[121]495 @Override
[1101]496 public Colour getHighlightColor() {
[1413]497 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.HIGHLIGHT_STR)) {
[1414]498 return this.getPrimary().getHighlightColor();
[1413]499 } else {
500 return _highlightColour;
[1190]501 }
[121]502 }
503
[4]504 /**
[1095]505 * Sets the justification of this Text item. The given integer should correspond
506 * to one of the JUSTIFICATION constants defined in Item
[4]507 *
508 * @param just
509 * The justification to apply to this Text item
510 */
[97]511 public void setJustification(Justification just) {
[362]512 invalidateAll();
513
514 // Only justification left works with 0 width
[400]515 // if (just != null && just != Justification.left && !hasWidth()) {
516 // // TODO Tighten this up so it subtracts the margin widths
517 // setWidth(getBoundsWidth());
518 // }
[362]519
[4]520 _justification = just;
[199]521 rebuild(true);
[362]522 invalidateAll();
[1413]523
524 if (isSurrogate()) {
525 surrogatePropertyInheritance.put(DefaultFrameWriter.JUSTIFICATION_STR, false);
[1414]526 Item primary = getPrimary();
[1413]527 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.JUSTIFICATION_STR)) {
[1415]528 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
529 primary.primaryPropertyEncryption.put(DefaultFrameWriter.JUSTIFICATION_STR, inheritanceCheckOnSave);
[1413]530 }
531 }
[4]532 }
533
534 /**
[1095]535 * Returns the current justification of this Text item. The default value left
536 * justification.
[4]537 *
[1101]538 * TODO: Why return null when justification is set to left? cts16
539 *
[4]540 * @return The justification of this Text item
541 */
[1210]542 public Justification getJustification() {
[1413]543 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.JUSTIFICATION_STR)) {
[1414]544 return ((Text) this.getPrimary()).getJustification();
[1410]545 } else {
546 if (_justification == null || _justification.equals(Justification.left)) {
547 return null;
548 }
549
550 return _justification;
[1190]551 }
[4]552 }
553
[1101]554 /**
555 * Gets the distance from the left of the bounding box that the given layout
556 * should be shifted to justify it.
557 *
558 * @param layout
[1210]559 * The line of text to calculate the justification offset for.
[1101]560 *
[1210]561 * @return The distance to shift the line of text by.
[1101]562 */
[4]563 private int getJustOffset(TextLayout layout) {
[1190]564 if (getJustification() == Justification.center) {
[400]565 return (int) ((getAbsoluteWidth() - layout.getAdvance()) / 2);
[1190]566 } else if (getJustification() == Justification.right) {
[400]567 return (int) (getAbsoluteWidth() - layout.getAdvance());
[1190]568 }
[4]569
570 return 0;
571 }
572
573 /**
[1095]574 * Sets the text displayed on the screen to the given String. It does not reset
575 * the formula, attributeValuePair or other cached values.
[4]576 *
577 * @param text
578 * The String to display on the screen when drawing this Item.
579 */
[176]580 @Override
[4]581 public void setText(String text) {
[390]582 setText(text, false);
583 }
[400]584
[390]585 public void setText(String text, Boolean clearCache) {
[376]586 // if (_text != null && text.length() < _text.length())
587 invalidateAll();
[4]588 _text = new StringBuffer(text);
[376]589
590 /*
[1095]591 * Always clearingCach remove formulas when moving in and out of XRay mode
[376]592 */
[400]593 if (clearCache) {
[390]594 clearCache();
[400]595 }
596
[247]597 rebuild(true);
[126]598 invalidateAll();
[4]599 }
600
601 public void setTextList(List<String> text) {
[1190]602 if (text == null || text.size() <= 0) {
[4]603 return;
[1190]604 }
[4]605
[376]606 invalidateAll();
607
608 StringBuffer sb = new StringBuffer();
609
610 for (int i = 0; i < text.size(); i++) {
611 sb.append(text.get(i)).append('\n');
[4]612 }
[376]613
[1190]614 if (sb.length() > 0) {
[376]615 sb.deleteCharAt(sb.length() - 1);
[1190]616 }
[376]617
618 setText(sb.toString());
619
620 rebuild(true);
621 invalidateAll();
[4]622 }
623
624 public void setAttributeValue(String value) {
[289]625 AttributeValuePair avp = new AttributeValuePair(getText(), false);
[286]626 avp.setValue(value);
627 setText(avp.toString());
[4]628 }
629
630 /**
[1095]631 * Inserts the given String at the start of the first line of this Text Item.
[4]632 *
633 * @param text
634 * The String to insert.
635 */
[1210]636 public void prependText(String text) {
[1101]637 invalidateAll();
[4]638 _text.insert(0, text);
639 rebuild(false);
[1101]640 invalidateAll();
[4]641 }
642
643 /**
[1095]644 * If the first line of text starts with the given String, then it is removed
645 * otherwise no action is taken.
[4]646 *
647 * @param text
648 * The String to remove from the first line of Text
649 */
650 public void removeText(String text) {
[121]651 if (_text.length() > 0 && _text.indexOf(text) == 0) {
[247]652 invalidateAll();
[4]653 _text.delete(0, text.length());
[1101]654 invalidateAll();
[121]655 }
[4]656
657 }
[7]658
[4]659 public void removeEndText(String textToRemove) {
660 int length = _text.length();
661 if (length > 0) {
662 int pos = _text.indexOf(textToRemove);
663 int textToRemoveLength = textToRemove.length();
664 if (pos + textToRemoveLength == length) {
[1101]665 // Need the invalidate all for dateStamp toggling
666 invalidateAll();
[4]667 _text.delete(pos, length);
[1101]668 invalidateAll();
[4]669 }
670 }
[137]671
[4]672 }
673
674 /**
675 * Appends the given String to any text already present in this Item
676 *
677 * @param text
678 * The String to append.
679 */
680 public void appendText(String text) {
[1101]681 invalidateAll();
[4]682 _text.append(text);
683 rebuild(false);
[1101]684 invalidateAll();
[4]685 }
[7]686
[404]687 /**
688 * Used by the frame reader to construct multi-line text items. It must run
689 * quickly, so that the system still responds well for long text items.
690 *
691 * @param text
692 */
[4]693 public void appendLine(String text) {
[1190]694 if (text == null) {
[4]695 text = "";
[1190]696 }
[7]697
[1190]698 if (_text.length() > 0) {
[4]699 _text.append('\n');
[1190]700 }
[7]701
[4]702 _text.append(text);
[1095]703
[776]704 rebuild(true);
[1413]705
706 if (isSurrogate()) {
707 surrogatePropertyInheritance.put(DefaultFrameWriter.TEXT_STR, false);
[1414]708 Item primary = getPrimary();
[1413]709 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.TEXT_STR)) {
[1415]710 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
711 primary.primaryPropertyEncryption.put(DefaultFrameWriter.TEXT_STR, inheritanceCheckOnSave);
[1413]712 }
713 }
[4]714 }
715
716 /**
717 * Tests if the first line of this Text starts with the given String.
718 *
719 * @param text
720 * The prefix to check for
[1095]721 * @return True if the first line starts with the given String, False otherwise.
[4]722 */
723 public boolean startsWith(String text) {
724 return startsWith(text, true);
725 }
726
727 public boolean startsWith(String text, boolean ignoreCase) {
[1190]728 if (text == null || _text == null || _text.length() < 1) {
[4]729 return false;
[1190]730 }
[4]731
[1190]732 if (ignoreCase) {
[1095]733 return _text.toString().toLowerCase().startsWith(text.toLowerCase());
[1190]734 } else {
[4]735 return _text.indexOf(text) == 0;
[1190]736 }
[4]737 }
738
739 /**
740 * Inserts a character into the Text of this Item.
741 *
742 * @param ch
743 * The character insert.
744 * @param mouseX
745 * The X position to insert the Strings at.
746 * @param mouseY
747 * The Y position to insert the Strings at.
748 */
[1101]749 public Point insertChar(char ch, float mouseX, float mouseY) {
[1190]750 if (ch != '\t') {
[4]751 return insertText("" + ch, mouseX, mouseY);
[1190]752 }
[80]753
754 return insertText(" " + ch, mouseX, mouseY);
[4]755 }
756
[80]757 /**
758 * @param index
759 * @return
760 */
761 private char getNextBullet(char bullet) {
[137]762 for (int i = 0; i < BULLETS.length - 1; i++) {
[1190]763 if (BULLETS[i] == bullet) {
[137]764 return BULLETS[i + 1];
[1190]765 }
[80]766 }
[115]767 return BULLETS[0];
[80]768 }
769
770 private char getPreviousBullet(char bullet) {
[137]771 for (int i = 1; i < BULLETS.length; i++) {
[1190]772 if (BULLETS[i] == bullet) {
[115]773 return BULLETS[i - 1];
[1190]774 }
[80]775 }
[115]776 return BULLETS[BULLETS.length - 1];
[80]777 }
778
[1101]779 public Point getLineEndPosition(float mouseY) {
[4]780 return getEdgePosition(getLinePosition(mouseY), false);
781 }
782
[1101]783 public Point getLineStartPosition(float mouseY) {
[4]784 return getEdgePosition(getLinePosition(mouseY), true);
785 }
786
[1101]787 public Point getParagraphEndPosition() {
[1210]788 return getEdgePosition(getTextLayouts().size() - 1, false);
[4]789 }
[98]790
[1101]791 public Point getParagraphStartPosition() {
[86]792 return getEdgePosition(0, true);
793 }
[4]794
[1101]795 private Point getEdgePosition(int line, boolean start) {
[4]796 // if there is no text yet, or the line is invalid
[1210]797 if (_text == null || _text.length() == 0 || line < 0 || line > getTextLayouts().size() - 1) {
[1101]798 return new Point(getX(), getY());
[1190]799 }
[4]800
[1210]801 TextLayout last = getTextLayouts().get(line);
[4]802 TextHitInfo hit;
[1190]803 if (start) {
[4]804 hit = last.getNextLeftHit(1);
[1190]805 } else {
[4]806 hit = last.getNextRightHit(last.getCharacterCount() - 1);
[1190]807 }
[4]808
809 // move the cursor to the new location
810 float[] caret = last.getCaretInfo(hit);
[105]811 float y = getLineDrop(last) * line;
[4]812
[107]813 float x = getX() + caret[0] + getJustOffset(last);
[1101]814
[1210]815 x = Math.min(x, (getX() - Item.MARGIN_RIGHT - (2 * getGravity()) + getBoundsWidth()));
[1101]816 return new Point((int) x, (int) (getY() + y + caret[1]));
[4]817 }
818
[1210]819 public void setSelectionStart(Point p) {
[1143]820 setSelectionStart(p.getX(), p.getY());
[1101]821 }
[1210]822
[176]823 public void setSelectionStart(float mouseX, float mouseY) {
[4]824 // determine what line is being pointed to
825 int line = getLinePosition(mouseY);
826
827 // get the character being pointed to
828 TextHitInfo hit = getCharPosition(line, mouseX);
[1210]829 _selectionStart = hit.getInsertionIndex() + getTextLayouts().get(line).getStartCharIndex();
830
[1101]831 // Clear the last selected
832 updateLastSelected();
[1210]833
[121]834 invalidateAll();
[4]835 }
836
[1210]837 public void setSelectionEnd(Point p) {
[1143]838 setSelectionEnd(p.getX(), p.getY());
[1101]839 }
840
[176]841 public void setSelectionEnd(float mouseX, float mouseY) {
[4]842 // determine what line is being pointed to
843 int line = getLinePosition(mouseY);
844
845 // get the character being pointed to
846 TextHitInfo hit = getCharPosition(line, mouseX);
[1210]847 _selectionEnd = hit.getInsertionIndex() + getTextLayouts().get(line).getStartCharIndex();
848
[1101]849 // Clear the last selected
850 updateLastSelected();
[1210]851
[121]852 invalidateAll();
[4]853 }
854
855 public void clearSelection() {
856 _selectionStart = -1;
857 _selectionEnd = -1;
[121]858 invalidateAll();
[4]859 }
860
861 public void clearSelectionEnd() {
862 _selectionEnd = -1;
[121]863 invalidateAll();
[4]864 }
[1210]865
[1101]866 /** Makes sure only one text has a selection at a time. */
[1210]867 public void updateLastSelected() {
[1101]868 if (_lastSelected != this) {
[1190]869 if (_lastSelected != null) {
870 _lastSelected.clearSelection();
871 }
[1101]872 _lastSelected = this;
873 }
874 }
[4]875
876 public String copySelectedText() {
[1190]877 if (_selectionStart < 0 || _selectionEnd < 0) {
[4]878 return null;
[1190]879 } else if (_selectionEnd > _text.length()) {
[4]880 _selectionEnd = _text.length();
[1190]881 }
[4]882
[1095]883 return _text.substring(Math.min(_selectionStart, _selectionEnd), Math.max(_selectionStart, _selectionEnd));
[4]884 }
885
886 public String cutSelectedText() {
[7]887 return replaceSelectedText("");
888 }
889
890 public String replaceSelectedText(String newText) {
[1190]891 if (_selectionStart < 0 || _selectionEnd < 0) {
[4]892 return null;
[1190]893 }
[137]894
[121]895 invalidateAll();
[137]896
[1190]897 if (_selectionEnd > _text.length()) {
[4]898 _selectionEnd = _text.length();
[1190]899 }
[4]900
901 int left = Math.min(_selectionStart, _selectionEnd);
902 int right = Math.max(_selectionStart, _selectionEnd);
903
[147]904 // Trim the text to remove new lines on the beginning and end of the
905 // string
[154]906 if (_text.charAt(left) == '\n') {
907 // if the entire line is being removed then remove one of the new
908 // lines, the first case checks if the last line is being removed
909 if (right >= _text.length() || _text.charAt(right) == '\n') {
[147]910 _text.deleteCharAt(left);
911 right--;
[154]912 } else {
[147]913 left++;
914 }
915 }
[154]916 // New lines are always at the start of the line for now...
917 // if(_text.charAt(right - 1) == '\n' && left < right){
918 // right--;
919 // }
[4]920 String s = _text.substring(left, right);
921
[7]922 _text.delete(left, right);
923 _text.insert(left, newText);
924 rebuild(true);
[421]925
[410]926 clearCache();
[137]927
[121]928 invalidateAll();
[4]929
930 return s;
931 }
932
[131]933 public int getSelectionSize() {
[1190]934 if (_selectionEnd < 0 || _selectionStart < 0) {
[4]935 return 0;
[1190]936 }
[4]937
[7]938 // System.out.println(_selectionStart + ":" + _selectionEnd);
[4]939
940 return Math.abs(_selectionEnd - _selectionStart);
941 }
942
943 /**
[1095]944 * Inserts the given String into the Text at the position given by the mouseX
945 * and mouseY coordinates
[4]946 *
947 * @param text
948 * The String to insert into this Text.
949 * @param mouseX
950 * The X position to insert the String
951 * @param mouseY
952 * The Y position to insert the String
953 * @return The new location that the mouse cursor should be moved to
954 */
[1210]955 public Point insertText(String text, float mouseX, float mouseY) {
[1101]956 final Point newPos = insertText(text, mouseX, mouseY, -1);
[906]957 return newPos;
[108]958 }
959
[1210]960 public Point insertText(final String text, final float mouseX, final float mouseY, int insertPos) {
[4]961 TextHitInfo hit;
[1210]962 TextLayout currentLayout = null;
[1101]963 int lineIndex;
[121]964 invalidateAll();
[4]965
[1210]966 // if it is a empty string then do not move the mouse
[1190]967 if (text == null || text.length() == 0) {
[1101]968 return new Point((int) mouseX, (int) mouseY);
[1190]969 }
[1210]970
971 // if there is no text yet then simply append parameter and rebuild before moving on.
972 // rebuild re-initialises the TextLayouts
973 // calculate were we are in the content (hit, lineIndex, currentLayout)
[4]974 if (_text == null || _text.length() == 0) {
975 _text = new StringBuffer().append(text);
976 rebuild(true);
[1210]977
978 assert (getTextLayouts().size() == 1);
979 currentLayout = getTextLayouts().get(0);
980 hit = currentLayout.getNextRightHit(0);
[1101]981 lineIndex = 0;
[1210]982 }
983 // otherwise we are inserting text and calculating the index into the content that we are at
984 else {
985
[376]986 clearCache();
[4]987 // determine what line is being pointed to
[1101]988 lineIndex = getLinePosition(mouseY);
[4]989
990 // get the character being pointed to
[1101]991 hit = getCharPosition(lineIndex, mouseX);
[4]992
[1210]993 int insertionIndex = hit.getInsertionIndex() + getTextLayouts().get(lineIndex).getStartCharIndex();
[4]994
[1101]995 if (lineIndex > 0 && hit.getInsertionIndex() == 0) {
[154]996 // Only move forward a char if the line begins with a hard line
997 // break... not a soft line break
[1101]998 if (_text.charAt(insertionIndex) == '\n') {
999 insertionIndex++;
[154]1000 }
1001 }
[4]1002
[1190]1003 if (insertPos < 0) {
[1101]1004 insertPos = insertionIndex;
[1190]1005 }
[108]1006
[4]1007 // if this is a backspace key
[1101]1008 if (text.charAt(0) == '\b') {
[324]1009 if (hasSelection()) {
[1101]1010 insertionIndex = deleteSelection(insertionIndex);
[324]1011 } else if (insertPos > 0) {
[108]1012 deleteChar(insertPos - 1);
[1190]1013 if (insertionIndex > 0) {
[1101]1014 insertionIndex--;
[1190]1015 }
[4]1016 }
[1101]1017 // if this is a delete key
1018 } else if (text.charAt(0) == (char) 0x7F) {
[324]1019 if (hasSelection()) {
[1101]1020 insertionIndex = deleteSelection(insertionIndex);
[324]1021 } else if (insertPos < _text.length()) {
[108]1022 deleteChar(insertPos);
[80]1023 }
[1101]1024 // this is a tab
1025 } else if (text.charAt(0) == '\t') {
[80]1026 // Text length greater than 1 signals a backwards tab
1027 if (text.length() > 1) {
[86]1028 // Find the first non space char to see if its a bullet
[80]1029 int index = 0;
1030 for (index = 0; index < _text.length(); index++) {
[1190]1031 if (!Character.isSpaceChar(_text.charAt(index))) {
[80]1032 break;
[1190]1033 }
[4]1034 }
[80]1035 // Check if there is a space after the bullet
[1095]1036 if (index < _text.length() - 1 && _text.charAt(index + 1) == ' ') {
[80]1037 // Change the bullet
[1095]1038 _text.setCharAt(index, getPreviousBullet(_text.charAt(index)));
[80]1039 }
1040 // Remove the spacing at the start
1041 for (int i = 0; i < TAB_STRING.length(); i++) {
[1095]1042 if (_text.length() > 0 && Character.isSpaceChar(_text.charAt(0))) {
[108]1043 deleteChar(0);
[1101]1044 insertionIndex--;
[1190]1045 } else {
[80]1046 break;
[1190]1047 }
[80]1048 }
1049 } else {
1050 // / Find the first non space char to see if its a bullet
1051 int index = 0;
1052 for (index = 0; index < _text.length(); index++) {
[1190]1053 if (!Character.isSpaceChar(_text.charAt(index))) {
[80]1054 break;
[1190]1055 }
[80]1056 }
1057 // Check if there is a space after the bullet
[1095]1058 if (index < _text.length() - 1 && _text.charAt(index + 1) == ' ') {
[80]1059 char nextBullet = getNextBullet(_text.charAt(index));
1060 // Change the bullet
1061 _text.setCharAt(index, nextBullet);
1062 }
1063 // Insert the spacing at the start
1064 insertString(TAB_STRING, 0);
[1101]1065 insertionIndex += TAB_STRING.length();
[4]1066 }
[1101]1067 // this is a normal insert
[4]1068 } else {
[108]1069 insertString(text, insertPos);
[1101]1070 insertionIndex += text.length();
[4]1071 }
1072
[137]1073 if (_text.length() == 0) {
[108]1074 rebuild(false);
[1101]1075 return new Point((int) this._x, (int) this._y);
[108]1076 }
1077
[1101]1078 int newLine = lineIndex;
[4]1079
1080 // if a rebuild is required
[199]1081 rebuild(true, false);
[1125]1082
[4]1083 // determine the new position the cursor should have
[1210]1084 for (int i = 0; i < getTextLayouts().size(); i++) {
1085 if (getTextLayouts().get(i).getEndCharIndex() + 1 >= insertionIndex) {
[1101]1086 newLine = i;
[4]1087 break;
1088 }
1089 }
1090
[1210]1091 currentLayout = getTextLayouts().get(newLine);
1092 insertionIndex -= currentLayout.getStartCharIndex();
[4]1093
[1101]1094 if (newLine == lineIndex) {
[1190]1095 if (insertionIndex > 0) {
[1210]1096 hit = currentLayout.getNextRightHit(insertionIndex - 1);
[1190]1097 } else {
[1210]1098 hit = currentLayout.getNextLeftHit(1);
[1190]1099 }
[1101]1100 } else if (newLine < lineIndex) {
[1210]1101 hit = currentLayout.getNextRightHit(insertionIndex - 1);
[4]1102 } else {
[1210]1103 hit = currentLayout.getNextRightHit(insertionIndex - 1);
[4]1104 }
1105
[1101]1106 lineIndex = newLine;
[4]1107 }
[1200]1108
[1210]1109 // If we have no mask then....
1110 // move the cursor to the new location
1111 float[] caret = currentLayout.getCaretInfo(hit);
1112 float y = getLineDrop(currentLayout) * lineIndex;
1113 y = getY() + y + caret[1];
[4]1114
[1210]1115 float x = getX() + caret[0] + getJustOffset(currentLayout);
1116 x = Math.min(x, (getX() - Item.MARGIN_RIGHT - (2 * getGravity()) + getBoundsWidth()));
[4]1117
[1210]1118 invalidateAll();
1119
1120 final Point newCursor = new Point(Math.round(x), Math.round(y));
1121 return newCursor;
[4]1122 }
1123
[336]1124 /**
[376]1125 *
1126 */
1127 private void clearCache() {
1128 _attributeValuePair = null;
1129 setProcessedText(null);
1130 setFormula(null);
1131 }
1132
1133 /**
[336]1134 * @param pos
1135 * @return
1136 */
1137 private int deleteSelection(int pos) {
1138 int selectionLength = getSelectionSize();
1139 cutSelectedText();
1140 clearSelection();
1141 pos -= selectionLength;
1142 return pos;
1143 }
1144
[1210]1145 public Point moveCursor(int direction, float mouseX, float mouseY, boolean setSelection, boolean wholeWord) {
[176]1146 if (setSelection) {
[376]1147 if (!hasSelection()) {
[176]1148 setSelectionStart(mouseX, mouseY);
1149 }
[376]1150 } else {
1151 // clearSelection();
[176]1152 }
1153
[1101]1154 Point resultPos = null;
[176]1155
[4]1156 // check for home or end keys
[147]1157 switch (direction) {
1158 case HOME:
[176]1159 resultPos = getParagraphStartPosition();
1160 break;
[147]1161 case END:
[176]1162 resultPos = getParagraphEndPosition();
1163 break;
[147]1164 case LINE_HOME:
[176]1165 resultPos = getLineStartPosition(mouseY);
1166 break;
[147]1167 case LINE_END:
[176]1168 resultPos = getLineEndPosition(mouseY);
1169 break;
1170 default:
1171 TextHitInfo hit;
1172 TextLayout current;
1173 int line;
[4]1174
[176]1175 // if there is no text yet
1176 if (_text == null || _text.length() == 0) {
[1101]1177 return new Point((int) mouseX, (int) mouseY);
[176]1178 // otherwise, move the cursor
1179 } else {
1180 // determine the line of text to check
1181 line = getLinePosition(mouseY);
[1190]1182 if (line < 0) {
[1210]1183 line = getTextLayouts().size() - 1;
[1190]1184 }
[4]1185
[176]1186 // if the cursor is moving up or down, change the line
[1190]1187 if (direction == UP) {
[176]1188 line = Math.max(line - 1, 0);
[1190]1189 } else if (direction == DOWN) {
[1210]1190 line = Math.min(line + 1, getTextLayouts().size() - 1);
[1190]1191 }
[4]1192
[176]1193 hit = getCharPosition(line, mouseX);
[4]1194
[176]1195 if (direction == LEFT) {
1196 if (hit.getInsertionIndex() > 0) {
[376]1197
1198 char prevChar = ' ';
1199 do {
[1210]1200 hit = getTextLayouts().get(line).getNextLeftHit(hit);
[376]1201
1202 // Stop if at the start of the line
[1190]1203 if (hit.getInsertionIndex() == 0) {
[376]1204 break;
[1190]1205 }
[376]1206 // Keep going if the char to the left is a
1207 // letterOrDigit
[1210]1208 prevChar = _text
1209 .charAt(hit.getInsertionIndex() - 1 + getTextLayouts().get(line).getStartCharIndex());
[1095]1210 } while (wholeWord && Character.isLetterOrDigit(prevChar));
[1210]1211
[1101]1212 // TODO Go to the start of the word instead of before the word
[1210]1213 char nextChar = _text
1214 .charAt(hit.getInsertionIndex() + getTextLayouts().get(line).getStartCharIndex());
1215
[1101]1216 // This takes care of hard line break in
[376]1217 if (line > 0 && nextChar == '\n') {
[176]1218 line--;
[1210]1219 hit = getTextLayouts().get(line)
1220 .getNextRightHit(getTextLayouts().get(line).getCharacterCount() - 1);
[176]1221 }
[1210]1222
1223 // This takes care of soft line breaks.
[176]1224 } else if (line > 0) {
[86]1225 line--;
[1210]1226 hit = getTextLayouts().get(line).getNextRightHit(getTextLayouts().get(line).getCharacterCount() - 1);
1227
[1101]1228 // Skip the spaces at the end of a line with soft linebreak
[1210]1229 while (hit.getCharIndex() > 0 && _text
1230 .charAt(getTextLayouts().get(line).getStartCharIndex() + hit.getCharIndex() - 1) == ' ') {
1231 hit = getTextLayouts().get(line).getNextLeftHit(hit);
[176]1232 }
[86]1233 }
[176]1234 } else if (direction == RIGHT) {
[1210]1235 if (hit.getInsertionIndex() < getTextLayouts().get(line).getCharacterCount()) {
1236 hit = getTextLayouts().get(line).getNextRightHit(hit);
[376]1237 // Skip whole word if needs be
[1210]1238 while (wholeWord && hit.getCharIndex() > 0
1239 && hit.getCharIndex() < getTextLayouts().get(line).getCharacterCount()
1240 && Character.isLetterOrDigit(_text
1241 .charAt(getTextLayouts().get(line).getStartCharIndex() + hit.getCharIndex() - 1))) {
1242 hit = getTextLayouts().get(line).getNextRightHit(hit);
[1101]1243 }
[1210]1244 } else if (line < getTextLayouts().size() - 1) {
[176]1245 line++;
[1210]1246 hit = getTextLayouts().get(line).getNextLeftHit(1);
[86]1247 }
[4]1248 }
[1210]1249 current = getTextLayouts().get(line);
[4]1250 }
1251
[176]1252 // move the cursor to the new location
1253 float[] caret = current.getCaretInfo(hit);
1254 float y = getLineDrop(current) * line;
1255
[1101]1256 resultPos = new Point((int) (getX() + caret[0] + getJustOffset(current)), (int) (getY() + y + caret[1]));
1257
[176]1258 break;
[4]1259 }
[1210]1260
[1190]1261 if (setSelection) {
1262 setSelectionEnd(resultPos.getX(), resultPos.getY());
1263 }
[1210]1264
[176]1265 return resultPos;
[4]1266 }
1267
1268 /**
1269 * Iterates through the given line string and returns the position of the
1270 * character being pointed at by the mouse.
1271 *
1272 * @param line
1273 * The index of the _text array of the String to be searched.
1274 * @param mouseX
1275 * The X coordinate of the mouse
1276 * @return The position in the string of the character being pointed at.
1277 */
[1210]1278 public TextHitInfo getCharPosition(final int line, float mouseX) {
1279 if (line < 0 || line >= getTextLayouts().size()) {
[4]1280 return null;
[1190]1281 }
[1210]1282
1283 final TextLayout layout = getTextLayouts().get(line);
[1143]1284 mouseX += getOffset().getX();
[4]1285 mouseX -= getJustOffset(layout);
1286
1287 return layout.hitTestChar(mouseX - getX(), 0);
1288 }
1289
[1101]1290 /**
[1210]1291 * Gets the index into the <code>_textLayout</code> list which corresponds to
1292 * the line covered by the given <code>mouseY</code> position.
[1101]1293 *
1294 * @param mouseY
[1210]1295 * The y-coordinate to test for line coverage.
[1101]1296 *
[1210]1297 * @return The line which occupies the given y-coordinate, or the last line if
1298 * none do.
[1101]1299 */
[108]1300 public int getLinePosition(float mouseY) {
[1143]1301 mouseY += getOffset().getY();
[4]1302
[107]1303 float y = getY();
[4]1304
[1210]1305 for (TextLayout text : getTextLayouts()) {
[4]1306 // calculate X to ensure it is in the shape
[1101]1307 AxisAlignedBoxBounds bounds = text.getLogicalHighlightShape(0, text.getCharacterCount());
[4]1308
[1190]1309 if (bounds.getWidth() < 1) {
1310 bounds.getSize().width = 10;
1311 }
[4]1312
[1101]1313 double x = bounds.getCentreX();
[4]1314
[1190]1315 if (bounds.contains((int) x, (int) (mouseY - y))) {
[1210]1316 return getTextLayouts().indexOf(text);
[1190]1317 }
[4]1318
1319 // check if the cursor is between lines
[1190]1320 if (mouseY - y < bounds.getMinY()) {
[1210]1321 return Math.max(0, getTextLayouts().indexOf(text) - 1);
[1190]1322 }
[4]1323
1324 y += getLineDrop(text);
1325 }
1326
[1210]1327 return getTextLayouts().size() - 1;
[4]1328 }
1329
1330 /**
1331 * Sets the Font that this text will be displayed with on the screen.
1332 *
1333 * @param font
1334 * The Font to display the Text of this Item in.
1335 */
[1210]1336 public void setFont(Font font) {
[130]1337 invalidateAll();
[1210]1338
[4]1339 _font = font;
[1125]1340
[4]1341 rebuild(false);
[137]1342
[130]1343 invalidateAll();
[1415]1344
[1413]1345 if (isSurrogate()) {
1346 surrogatePropertyInheritance.put(DefaultFrameWriter.FONT_STR, false);
[1414]1347 Item primary = getPrimary();
[1413]1348 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.FONT_STR)) {
[1415]1349 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
1350 primary.primaryPropertyEncryption.put(DefaultFrameWriter.FONT_STR, inheritanceCheckOnSave);
[1413]1351 }
1352 }
[4]1353 }
1354
1355 /**
[1101]1356 * Gets the font of this text item.
[4]1357 *
[1101]1358 * @return The Font assigned to this text item, or null if none is assigned.
[4]1359 */
1360 public Font getFont() {
[1413]1361 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.FONT_STR)) {
[1414]1362 return ((Text) this.getPrimary()).getFont();
[1410]1363 } else {
1364 return _font;
1365 }
[4]1366 }
1367
[1101]1368 /**
1369 * Gets the font that should be used to paint this text item during drawing.
1370 *
1371 * @return The font to paint the text item with.
1372 */
[1210]1373 public Font getPaintFont() {
[1369]1374 Font f = getFont();
[1125]1375 if (f == null) {
[1210]1376 _font = EcosystemManager.getFontManager().getDefaultFont().clone();
[1369]1377 f = _font;
[1116]1378 }
[1125]1379 return f;
[4]1380 }
1381
1382 public String getFamily() {
[1101]1383 return getPaintFont().getFamilyName();
[4]1384 }
1385
[1210]1386 public void setFamily(String newFamily) {
[1101]1387 setFont(new Font(newFamily, getFontStyle(), Math.round(getSize())));
[1095]1388
[879]1389 setLetterSpacing(this._letter_spacing);
[4]1390 }
1391
[1101]1392 public Font.Style getFontStyle() {
[4]1393 Font f = getPaintFont();
[1101]1394 return f.getStyle();
[4]1395 }
[7]1396
[72]1397 public static final String MONOSPACED_FONT = "monospaced";
1398
[982]1399 public static final String[] FONT_WHEEL = { "sansserif", "monospaced", "serif", "dialog", "dialoginput" };
[1095]1400 public static final char[] FONT_CHARS = { 's', 'm', 't', 'd', 'i' };
[7]1401
[1095]1402 // A hashtable to store the font family names that are loaded in through the TTF
1403 // files
1404 // provided in the 'assets' folder
1405 public static HashMap<String, Font> FONT_WHEEL_ADDITIONAL_LOOKUP = new HashMap<>();
1406
[7]1407 private static final int NEARBY_GRAVITY = 2;
1408
[348]1409 public static final int MINIMUM_FONT_SIZE = 6;
[72]1410
[4]1411 public void toggleFontFamily() {
1412 String fontFamily = getFamily().toLowerCase();
[7]1413 // set it to the first font by default
[4]1414 setFamily(FONT_WHEEL[0]);
[7]1415
[4]1416 for (int i = 0; i < FONT_WHEEL.length - 3; i++) {
[7]1417 if (fontFamily.equals(FONT_WHEEL[i])) {
1418 setFamily(FONT_WHEEL[i + 1]);
[4]1419 break;
1420 }
1421 }
1422 }
[7]1423
[4]1424 public void toggleFontStyle() {
[362]1425 invalidateAll();
[4]1426 Font currentFont = getPaintFont();
[1101]1427 Font.Style currentStyle = currentFont.getStyle();
1428 Font.Style newStyle = Font.Style.PLAIN;
1429 switch (currentStyle) {
[1210]1430 case PLAIN:
1431 newStyle = Font.Style.BOLD;
1432 break;
1433 case BOLD:
1434 newStyle = Font.Style.ITALIC;
1435 break;
1436 case ITALIC:
1437 newStyle = Font.Style.BOLD_ITALIC;
1438 break;
1439 default:
1440 newStyle = Font.Style.PLAIN;
1441 break;
[1101]1442 }
1443 setFont(new Font(currentFont.getFamilyName(), newStyle, currentFont.getSize()));
[362]1444 rebuild(true);
1445 invalidateAll();
[4]1446 }
1447
[362]1448 public void toggleBold() {
1449 invalidateAll();
1450 Font currentFont = getPaintFont();
[1101]1451 currentFont.toggleBold();
[1210]1452 // setFont(currentFont);
[362]1453 rebuild(true);
1454 invalidateAll();
1455 }
1456
1457 public void toggleItalics() {
1458 invalidateAll();
1459 Font currentFont = getPaintFont();
[1101]1460 currentFont.toggleItalic();
[1210]1461 // setFont(currentFont);
[362]1462 rebuild(true);
1463 invalidateAll();
1464 }
1465
[1210]1466 public void setFontStyle(String newFace) {
[1101]1467 Font currentFont = getPaintFont();
[4]1468 if (newFace == null || newFace.trim().length() == 0) {
[1101]1469 currentFont.setStyle(Font.Style.PLAIN);
[1210]1470 // setFont(currentFont);
[4]1471 return;
1472 }
1473
1474 newFace = newFace.toLowerCase().trim();
1475
[311]1476 if (newFace.equals("plain") || newFace.equals("p")) {
[1101]1477 currentFont.setStyle(Font.Style.PLAIN);
[311]1478 } else if (newFace.equals("bold") || newFace.equals("b")) {
[1101]1479 currentFont.setStyle(Font.Style.BOLD);
[311]1480 } else if (newFace.equals("italic") || newFace.equals("i")) {
[1101]1481 currentFont.setStyle(Font.Style.ITALIC);
[1210]1482 } else if (newFace.equals("bolditalic") || newFace.equals("italicbold") || newFace.equals("bi")
1483 || newFace.equals("ib")) {
[1101]1484 currentFont.setStyle(Font.Style.BOLD_ITALIC);
[4]1485 }
1486
[1210]1487 // setFont(currentFont);
1488
[4]1489 }
1490
1491 /**
1492 * Returns a String array of this Text object's text, split up into separate
1493 * lines.
1494 *
1495 * @return The String array with one element per line of text in this Item.
1496 */
[80]1497 public List<String> getTextList() {
[1190]1498 if (_text == null) {
[4]1499 return null;
[1190]1500 }
[4]1501 try {
1502 List<String> list = new LinkedList<String>();
1503
[410]1504 // Rebuilding prevents errors when displaying frame bitmaps
[1210]1505 if (getTextLayouts().size() == 0) {
[410]1506 rebuild(false);
1507 }
1508
[1210]1509 for (TextLayout layout : getTextLayouts()) {
[1101]1510 String text = layout.getLine().replaceAll("\n", "");
[1190]1511 if (!text.equals("")) {
1512 list.add(text);
1513 }
[4]1514 }
1515
1516 return list;
1517 } catch (Exception e) {
[1210]1518 System.out.println("Exception in Text::getTextList::message is: " + e.getMessage());
[4]1519 return null;
1520 }
1521 }
1522
[1190]1523 @Override
[80]1524 public String getText() {
[1413]1525 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.TEXT_STR)) {
[1414]1526 return this.getPrimary().getText();
[1413]1527 } else {
1528 return _text.toString();
1529 }
[4]1530 }
1531
1532 /**
1533 * Returns the first line of text in this Text Item
1534 *
1535 * @return The first line of Text
1536 */
1537 public String getFirstLine() {
[1190]1538 if (_text == null || _text.length() == 0) {
[4]1539 return null;
[1190]1540 }
[4]1541
[348]1542 // start at the first non-newLine char
1543 int index = 0;
1544 while (_text.charAt(index) == '\n') {
1545 index++;
1546 }
[4]1547
[348]1548 int nextNewLine = _text.indexOf("\n", index);
1549
1550 /* If there are no more newLines return the remaining text */
[1190]1551 if (nextNewLine < 0) {
[348]1552 return _text.substring(index);
[1190]1553 }
[348]1554
1555 return _text.substring(index, nextNewLine);
[4]1556 }
1557
1558 /**
1559 * Sets the inter-line spacing (in pixels) of this text.
1560 *
1561 * @param spacing
1562 * The number of pixels to allow between each line
1563 */
[659]1564 public void setSpacing(float spacing) {
[4]1565 _spacing = spacing;
[1101]1566 invalidateBounds();
[1413]1567
1568 if (isSurrogate()) {
1569 surrogatePropertyInheritance.put(DefaultFrameWriter.SPACING_STR, false);
[1414]1570 Item primary = getPrimary();
[1413]1571 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.SPACING_STR)) {
[1415]1572 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
1573 primary.primaryPropertyEncryption.put(DefaultFrameWriter.SPACING_STR, inheritanceCheckOnSave);
[1413]1574 }
1575 }
[4]1576 }
1577
1578 /**
1579 * Returns the inter-line spacing (in pixels) of this Text.
1580 *
1581 * @return The spacing (inter-line) in pixels of this Text.
1582 */
[659]1583 public float getSpacing() {
[1413]1584 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.SPACING_STR)) {
[1414]1585 return ((Text) this.getPrimary()).getSpacing();
[1410]1586 } else {
1587 return _spacing;
1588 }
[4]1589 }
1590
[1101]1591 /**
1592 * Gets the y-distance that should be advanced between this layout and the next.
1593 *
1594 * @param layout
[1210]1595 * The TextLayout to calculate line-drop for.
1596 *
1597 * @return The distance to advance in the y-direction before the next line.
[1101]1598 */
[1210]1599 protected float getLineDrop(TextLayout layout) {
[1101]1600 if (getSpacing() < 0) {
[1095]1601 return layout.getAscent() + layout.getDescent() + layout.getLeading();
[1101]1602 }
[4]1603
[105]1604 return layout.getAscent() + layout.getDescent() + getSpacing();
[4]1605 }
1606
1607 public void setWordSpacing(int spacing) {
1608 _word_spacing = spacing;
[1413]1609
1610 if (isSurrogate()) {
1611 surrogatePropertyInheritance.put(DefaultFrameWriter.WORD_SPACING_STR, false);
[1414]1612 Item primary = getPrimary();
[1413]1613 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.WORD_SPACING_STR)) {
[1415]1614 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
1615 primary.primaryPropertyEncryption.put(DefaultFrameWriter.WORD_SPACING_STR, inheritanceCheckOnSave);
[1413]1616 }
1617 }
[4]1618 }
1619
1620 public int getWordSpacing() {
[1413]1621 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.WORD_SPACING_STR)) {
[1414]1622 return ((Text) this.getPrimary()).getWordSpacing();
[1410]1623 } else {
1624 return _word_spacing;
1625 }
[4]1626 }
1627
[594]1628 /**
1629 * Sets the spacing (proportional to the font size) between letters
1630 *
1631 * @param spacing
[1095]1632 * Additional spacing to add between letters. See
1633 * {@link java.awt.font.TextAttribute#TRACKING}
[594]1634 */
[659]1635 public void setLetterSpacing(float spacing) {
[4]1636 _letter_spacing = spacing;
[1095]1637
[1101]1638 Font currentFont = getPaintFont();
1639 currentFont.setSpacing(spacing);
[1210]1640 // setFont(currentFont);
[1413]1641
1642 if (isSurrogate()) {
1643 surrogatePropertyInheritance.put(DefaultFrameWriter.LETTER_SPACING_STR, false);
[1414]1644 Item primary = getPrimary();
[1413]1645 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.LETTER_SPACING_STR)) {
[1415]1646 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
1647 primary.primaryPropertyEncryption.put(DefaultFrameWriter.LETTER_SPACING_STR, inheritanceCheckOnSave);
[1413]1648 }
1649 }
[4]1650 }
1651
[594]1652 /**
[1095]1653 * @return The spacing (proportional to the font size) between letters. See
1654 * {@link java.awt.font.TextAttribute#TRACKING}
[594]1655 */
[659]1656 public float getLetterSpacing() {
[1413]1657 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.LETTER_SPACING_STR)) {
[1414]1658 return ((Text) this.getPrimary()).getLetterSpacing();
[1410]1659 } else {
1660 return _letter_spacing;
1661 }
[4]1662 }
1663
[659]1664 public void setInitialSpacing(float spacing) {
[4]1665 _initial_spacing = spacing;
[1413]1666
1667 if (isSurrogate()) {
1668 surrogatePropertyInheritance.put(DefaultFrameWriter.INITIAL_SPACING_STR, false);
[1414]1669 Item primary = getPrimary();
[1413]1670 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.INITIAL_SPACING_STR)) {
[1415]1671 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
1672 primary.primaryPropertyEncryption.put(DefaultFrameWriter.INITIAL_SPACING_STR, inheritanceCheckOnSave);
[1413]1673 }
1674 }
[4]1675 }
1676
[659]1677 public float getInitialSpacing() {
[1413]1678 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.INITIAL_SPACING_STR)) {
[1414]1679 return ((Text) this.getPrimary()).getInitialSpacing();
[1410]1680 } else {
1681 return _initial_spacing;
1682 }
[4]1683 }
1684
[1095]1685 // @Override
[1210]1686 /*
1687 * public boolean intersectsOLD(Polygon p) { if (super.intersects(p)) { float
1688 * textY = getY();
1689 *
1690 * for (TextLayout text : _textLayouts) { // check left and right of each box
1691 * Rectangle2D textOutline = text.getLogicalHighlightShape(0,
1692 * text.getCharacterCount()).getBounds2D();
1693 * textOutline.setRect(textOutline.getX() + getX() - 1, textOutline.getY() +
1694 * textY - 1, textOutline.getWidth() + 2, textOutline.getHeight() + 2); if
1695 * (p.intersects(textOutline)) return true; textY += getLineDrop(text); } }
1696 * return false; }
1697 */
[7]1698
[1095]1699 // The following version of intersect uses a tighter definition for the text,
1700 // based on
1701 @Override
[1101]1702 public boolean intersects(PolygonBounds p) {
[909]1703 if (super.intersects(p)) {
[1095]1704 // float textY = getY();
[7]1705
[1210]1706 for (TextLayout text : getTextLayouts()) {
[1095]1707
[1101]1708 AxisAlignedBoxBounds text_pixel_bounds_rect = getPixelBounds(text);
[1095]1709
[909]1710 if (p.intersects(text_pixel_bounds_rect)) {
1711 return true;
1712 }
1713 }
1714 }
[1095]1715
[909]1716 return false;
1717 }
1718
[7]1719 @Override
[1101]1720 public boolean contains(Point mousePosition) {
[1143]1721 return contains(mousePosition.getX(), mousePosition.getY(), getGravity() * NEARBY_GRAVITY);
[7]1722 }
[1210]1723
1724 public boolean contains(int mouseX, int mouseY) {
[1101]1725 return contains(new Point(mouseX, mouseY));
1726 }
[7]1727
[1210]1728 public boolean contains(int mouseX, int mouseY, int gravity) {
[1143]1729 mouseX += getOffset().getX();
1730 mouseY += getOffset().getY();
[4]1731
[107]1732 float textY = getY();
1733 float textX = getX();
[4]1734
[1101]1735 AxisAlignedBoxBounds outline = getBoundingBox();
[1210]1736
[1190]1737 if (outline == null) {
1738 return false;
1739 }
[4]1740
1741 // Check if its outside the top and left and bottom bounds
[1210]1742 if (outline.getMinX() - mouseX > gravity || outline.getMinY() - mouseY > gravity
[1101]1743 || mouseY - (outline.getMinY() + outline.getHeight()) > gravity
1744 || mouseX - (outline.getMinX() + outline.getWidth()) > gravity) {
[4]1745 return false;
1746 }
[1210]1747
1748 if (this.getMinWidth() != null && outline.contains(mouseX, mouseY)) {
[1200]1749 return true;
1750 }
[4]1751
[1210]1752 for (TextLayout text : getTextLayouts()) {
[4]1753 // check left and right of each box
[1101]1754 AxisAlignedBoxBounds textOutline = text.getLogicalHighlightShape(0, text.getCharacterCount());
[4]1755
1756 // check if the cursor is within the top, bottom and within the
1757 // gravity of right
[348]1758 int justOffset = getJustOffset(text);
[1210]1759
1760 if (mouseY - textY > textOutline.getMinY()
1761 && mouseY - textY < textOutline.getMinY() + textOutline.getHeight()
1762 && mouseX - textX - justOffset < textOutline.getWidth() + gravity + Item.MARGIN_RIGHT) {
[4]1763 return true;
[1101]1764 }
[1210]1765
[4]1766 textY += getLineDrop(text);
1767 }
1768
1769 return false;
1770 }
1771
1772 /**
1773 * Updates the Polygon (rectangle) that surrounds this Text on the screen.
1774 */
[1190]1775 @Override
[1210]1776 public AxisAlignedBoxBounds updateBounds() {
1777 boolean isFakeLayout = false;
[4]1778 // if there is no text, there is nothing to do
[1190]1779 if (_text == null) {
1780 return null;
1781 }
[4]1782
[1200]1783 // if there is no text layouts and the text has no min width, do nothing
[1210]1784 if (getTextLayouts() == null || (getTextLayouts().size() < 1 && this.getMinWidth() == null)) {
[1190]1785 return null;
[1210]1786 }
[1101]1787
[1210]1788 if (this.getMinWidth() != null && getTextLayouts().size() == 0 && this.getFont() != null) {
[1511]1789 // could use any text, 'p' is used simply to get some text in correct font.
[1210]1790 getTextLayouts().add(TextLayout.getManager().layoutStringSimple("p", this.getFont()));
1791 isFakeLayout = true;
1792 }
1793
[906]1794 int preChangeWidth = 0;
[1101]1795 if (getOldBounds() != null) {
1796 preChangeWidth = AxisAlignedBoxBounds.getEnclosing(getOldBounds()).getWidth();
1797 }
[906]1798
[4]1799 int minX = Integer.MAX_VALUE;
[1415]1800 int maxX = Integer.MIN_VALUE + 1; // +1 makes it safe to do math.abs on
[4]1801
1802 int minY = Integer.MAX_VALUE;
[1415]1803 int maxY = Integer.MIN_VALUE + 1; // +1 makes it safe to do math.abs on
[4]1804
1805 float y = -1;
[1095]1806
[504]1807 // Fix concurrency error in ScaleFrameset
1808 List<TextLayout> tmpTextLayouts;
[1210]1809 synchronized (getTextLayouts()) {
1810 tmpTextLayouts = new LinkedList<TextLayout>(getTextLayouts());
[504]1811 }
[4]1812
[1200]1813 for (int index = 0; index < tmpTextLayouts.size(); index++) {
1814 final TextLayout layout = tmpTextLayouts.get(index);
[1101]1815 AxisAlignedBoxBounds bounds = layout.getLogicalHighlightShape(0, layout.getCharacterCount());
[1095]1816
[1190]1817 if (y < 0) {
[504]1818 y = 0;
[1210]1819 } else {
[504]1820 y += getLineDrop(layout);
[1190]1821 }
[1210]1822
[1200]1823 int minWidth = getAbsoluteMinWidth();
[4]1824
[1190]1825 minX = Math.min(minX, bounds.getMinX());
[1210]1826 maxX = minWidth < Integer.MAX_VALUE ? Math.max(minX + minWidth, bounds.getMaxX())
[1200]1827 : Math.max(maxX, bounds.getMaxX());
1828 minY = Math.min(minY, (int) (bounds.getMinY() + y));
[504]1829 maxY = Math.max(maxY, (int) (bounds.getMaxY() + y));
[4]1830 }
1831
[72]1832 minX -= getLeftMargin();
1833 maxX += Item.MARGIN_RIGHT;
[4]1834
[348]1835 // If its justification right or center then DONT limit the width
1836 if (getJustification() != null) {
[400]1837 maxX = Item.MARGIN_RIGHT + getAbsoluteWidth();
[348]1838 }
1839
[1190]1840 final int xPos = getX() + minX - getGravity();
1841 final int yPos = getY() + minY - getGravity();
1842 final int width = 2 * getGravity() + maxX - minX;
1843 final int height = 2 * getGravity() + maxY - minY;
1844 AxisAlignedBoxBounds ret = new AxisAlignedBoxBounds(xPos, yPos, width, height);
[1210]1845
[1101]1846 Dimension polySize = ret.getSize();
[1210]1847
1848 if (preChangeWidth != 0 && preChangeWidth != polySize.width) {
[1101]1849 if (polySize.width > preChangeWidth) {
1850 MagneticConstraints.getInstance().textGrown(this, polySize.width - preChangeWidth);
1851 } else {
1852 MagneticConstraints.getInstance().textShrunk(this, preChangeWidth - polySize.width);
1853 }
1854 }
[1210]1855
1856 if (isFakeLayout) {
1857 getTextLayouts().remove(0).release();
1858 }
1859
[1101]1860 return ret;
[72]1861 }
[4]1862
[1101]1863 // TODO it seems like this method has some exponential processing which
[4]1864 // makes items copy really slowly when there are lots of lines of text!
1865 // This needs to be fixed!!
[669]1866 public void rebuild(boolean limitWidth) {
[199]1867 rebuild(limitWidth, true);
1868 }
1869
1870 /**
1871 *
1872 * @param limitWidth
1873 * @param newLinebreakerAlways
1874 * true if a new line breaker should always be created.
1875 */
1876 private void rebuild(boolean limitWidth, boolean newLinebreakerAlways) {
[1210]1877 // TODO make this more efficient so it only clears annotation list when it
1878 // really has to
[156]1879 if (isAnnotation()) {
1880 Frame parent = getParent();
[176]1881 // parent can be null when running tests
[156]1882 if (parent != null) {
1883 parent.clearAnnotations();
1884 }
1885 }
1886
[4]1887 // if there is no text, there is nothing to do
[1296]1888 if ((_text == null || _text.length() == 0 ) && getPlaceholder() == null) {
[137]1889 // Frame parent = getParent();
1890 // if(parent != null)
1891 // parent.removeItem(this);
[4]1892 return;
[108]1893 }
[137]1894
[1210]1895 EcosystemManager.getTextLayoutManager().releaseLayouts(getTextLayouts());
1896 if (getTextLayouts() != null) {
1897 getTextLayouts().clear();
[1190]1898 }
[1210]1899 EcosystemManager.getTextLayoutManager().releaseLayouts(_maskTextLayouts);
1900 if (_maskTextLayouts != null) {
1901 _maskTextLayouts.clear();
1902 }
[1296]1903 EcosystemManager.getTextLayoutManager().releaseLayouts(_placeholderTextLayouts);
1904 if (_placeholderTextLayouts != null) {
1905 _placeholderTextLayouts.clear();
1906 }
[4]1907
[1101]1908 // Calculate the maximum allowable width of this line of text
1909 List<org.expeditee.core.Line> lines = null;
[1210]1910 if (_autoWrap || ExperimentalFeatures.AutoWrap.get()) {
[1101]1911 lines = new LinkedList<org.expeditee.core.Line>();
[1210]1912 if (DisplayController.getCurrentFrame() == null) {
1913 return;
1914 }
[1415]1915 for (Item item : DisplayController.getCurrentFrame().getSortedItems()) {
[1210]1916 if (item instanceof Line) {
1917 lines.add(new org.expeditee.core.Line(((Line) item).getStartItem().getPosition(),
1918 ((Line) item).getEndItem().getPosition()));
1919 }
1920 if (item instanceof Picture) {
1921 lines.add(new org.expeditee.core.Line(item.getPosition(),
1922 new Point(item.getX(), item.getY() + item.getHeight())));
1923 }
1924 }
1925 for (Item item : FreeItems.getInstance()) {
1926 if (item instanceof Line) {
1927 lines.add(new org.expeditee.core.Line(((Line) item).getStartItem().getPosition(),
1928 ((Line) item).getEndItem().getPosition()));
1929 }
1930 if (item instanceof Picture) {
1931 lines.add(new org.expeditee.core.Line(item.getPosition(),
1932 new Point(item.getX(), item.getY() + item.getHeight())));
1933 }
1934 }
[613]1935 }
[1095]1936
[1415]1937 float width = (float) Integer.MAX_VALUE;
[1210]1938 if (limitWidth) {
[1369]1939 if (_width == null) {
[1258]1940 width = DisplayController.getFramePaintAreaWidth() - getX();
[1210]1941 } else {
1942 width = getAbsoluteWidth();
1943 }
1944 }
[4]1945
[1296]1946 Font paintFont = getPaintFont();
1947 if (this._text != null && this._text.length() > 0) {
[1379]1948 List<TextLayout> proposedTextLayout =
[1296]1949 EcosystemManager.getTextLayoutManager().layoutString(
1950 _text.toString(),
1951 paintFont,
1952 new Point(getX(), getY()), lines != null ? lines.toArray(new org.expeditee.core.Line[1]) : null,
1953 (int) width,
1954 (int) getSpacing(),
1955 true,
1956 getJustification() == Justification.full
1957 );
[1379]1958 if (proposedTextLayout.size() > 1 && isSingleLineOnly()) {
1959 paintFont = paintFont.clone();
1960
1961 while(proposedTextLayout.size() > 1) {
[1392]1962 paintFont.setSize(Math.max(paintFont.getSize() - 1, MINIMUM_FONT_SIZE));
[1379]1963 proposedTextLayout =
1964 EcosystemManager.getTextLayoutManager().layoutString(
1965 _text.toString(),
1966 paintFont,
1967 new Point(getX(), getY()), lines != null ? lines.toArray(new org.expeditee.core.Line[1]) : null,
[1415]1968 paintFont.getSize() > MINIMUM_FONT_SIZE ? (int) width : Integer.MAX_VALUE,
[1379]1969 (int) getSpacing(),
1970 true,
1971 getJustification() == Justification.full
1972 );
1973 }
1974 }
1975 this._textLayouts = proposedTextLayout;
[1296]1976
1977 if (this.getMask() != null) {
1978 final Stream<Character> maskStream = _text.toString().chars().mapToObj(c -> (char) this.getMask().intValue());
1979 final StringBuilder sb = new StringBuilder();
1980 maskStream.forEach(c -> sb.append(c));
1981 this._maskTextLayouts =
1982 EcosystemManager.getTextLayoutManager().layoutString(
1983 sb.toString(),
1984 paintFont,
1985 new Point(getX(), getY()), lines != null ? lines.toArray(new org.expeditee.core.Line[1]) : null,
1986 (int) width,
1987 (int) getSpacing(),
1988 true,
1989 getJustification() == Justification.full
1990 );
1991 }
[1190]1992 }
[1296]1993 if (this.getPlaceholder() != null) {
1994 this._placeholderTextLayouts =
1995 EcosystemManager.getTextLayoutManager().layoutString(
1996 getPlaceholder(),
1997 paintFont,
1998 new Point(getX(), getY()), lines != null ? lines.toArray(new org.expeditee.core.Line[1]) : null,
1999 (int) width,
2000 (int) getSpacing(),
2001 true,
2002 getJustification() == Justification.full
2003 );
2004 }
[1101]2005 invalidateBounds();
[4]2006 }
[1095]2007
[1101]2008 /**
[1210]2009 * Calculates the maximum possible distance a line can extend to the right from
2010 * a given (x,y) point without crossing any of the lines in the given list.
[1101]2011 *
2012 * @param x
[1210]2013 * The x-coordinate of the beginning point.
[1101]2014 *
2015 * @param y
[1210]2016 * The y-coordinate of the beginning point.
[1101]2017 *
2018 * @param lines
[1210]2019 * A list of pairs of points describing the lines that should stop
2020 * the extension.
[1101]2021 *
[1210]2022 * @return The length of the extended line.
[1101]2023 */
[1210]2024 /*
2025 * private float getLineWidth(int x, float y, List<Point[]> lines) { float width
2026 * = FrameGraphics.getMaxFrameSize().width; for (Point[] l : lines) { // check
2027 * for lines that cross over our y if ((l[0].y >= y && l[1].y <= y) || (l[0].y
2028 * <= y && l[1].y >= y)) { float dX = l[0].x - l[1].x; float dY = l[0].y -
2029 * l[1].y; float newWidth; if (dX == 0) { newWidth = l[0].x; } else if (dY == 0)
2030 * { newWidth = Math.min(l[0].x, l[1].x); } else { //
2031 * System.out.print("gradient: " + (dY / dX)); newWidth = l[0].x + (y - l[0].y)
2032 * * dX / dY; } // System.out.println("dY:" + dY + " dX:" + dX + " width:" +
2033 * newWidth); if (newWidth < x) { continue; } if (newWidth < width) { width =
2034 * newWidth; } } } return width - x; }
2035 */
[4]2036
[400]2037 private boolean hasFixedWidth() {
[1369]2038 assert (_width != null);
2039 if (_width == null) {
[400]2040 justify(false);
2041 }
[1369]2042 return _width > 0;
[400]2043 }
2044
[4]2045 private int _alpha = -1;
2046
2047 public void setAlpha(int alpha) {
2048 _alpha = alpha;
2049 }
2050
[1101]2051 private Range<Integer> getSelectedRange(int line) {
[4]2052 if (_selectionEnd >= _text.length()) {
2053 _selectionEnd = _text.length();
2054 }
2055
[1190]2056 if (_selectionStart < 0) {
[4]2057 _selectionStart = 0;
[1190]2058 }
[4]2059
[1190]2060 if (_selectionStart < 0 || _selectionEnd < 0) {
[4]2061 return null;
[1190]2062 }
[4]2063
2064 int selectionLeft = Math.min(_selectionStart, _selectionEnd);
2065 int selectionRight = Math.max(_selectionStart, _selectionEnd);
2066
2067 // if the selection is after this line, return null
[1210]2068 if (getTextLayouts().get(line).getStartCharIndex() > selectionRight) {
[1190]2069 return null;
2070 }
[4]2071
2072 // if the selection is before this line, return null
[1210]2073 if (getTextLayouts().get(line).getEndCharIndex() < selectionLeft) {
[1190]2074 return null;
2075 }
[4]2076
2077 // Dont highlight a single char
2078 // if (selectionRight - selectionLeft <= MINIMUM_RANGED_CHARS)
2079 // return null;
2080
2081 // the selection occurs on this line, determine where it lies on the
2082 // line
[1210]2083 int start = Math.max(0, selectionLeft - getTextLayouts().get(line).getStartCharIndex());
[4]2084 // int end = Math.min(_lineOffsets.get(line) +
2085 // _textLayouts.get(line).getCharacterCount(), _selectionEnd);
[1210]2086 int end = Math.min(selectionRight - getTextLayouts().get(line).getStartCharIndex(),
2087 getTextLayouts().get(line).getCharacterCount());
[4]2088
2089 // System.out.println(line + ": " + start + "x" + end + " (" +
2090 // _selectionStart + "x" + _selectionEnd + ")");
[1101]2091 return new Range<Integer>(start, end, true, true);
[4]2092 }
[242]2093
[1101]2094 /** Sets the colour that should be used to render the selected range. */
[1210]2095 public void setSelectionColour(Colour colour) {
[1190]2096 if (colour == null) {
2097 colour = RANGE_SELECT_COLOUR;
2098 }
[1210]2099
[1101]2100 _selectionColour = colour;
[229]2101 }
[1210]2102
[1101]2103 /** Gets the colour that should be used to render the selected range. */
[1210]2104 public Colour getSelectionColour() {
[1101]2105 return _selectionColour;
2106 }
[229]2107
[4]2108 @Override
[1210]2109 public void paint() {
[1190]2110 if (!isVisible()) {
2111 return;
2112 }
[1296]2113
2114 boolean hasContent = !(_text == null || (_text.length() == 0 && getMinWidth() == null));
2115 boolean hasPlaceholderContent = getPlaceholder() != null;
[108]2116
[1210]2117
[1200]2118 // if the text to paint is empty string and there is no min width, do nothing.
[1296]2119 if (!hasContent && !hasPlaceholderContent) {
[1200]2120 return;
2121 }
[1095]2122
2123 if (_autoWrap || ExperimentalFeatures.AutoWrap.get()) {
[613]2124 invalidateAll();
[1095]2125
[4]2126 rebuild(true);
[1210]2127 } else if (getTextLayouts().size() < 1) {
[663]2128 clipFrameMargin();
[613]2129 rebuild(true);
[410]2130 // return;
[4]2131 }
[1210]2132
[137]2133 // check if its a vector item and paint all the vector stuff too if this
2134 // item is a free item
2135 // This will allow for dragging vectors around the place!
2136 if (hasVector() && isFloating()) {
[1101]2137 DisplayController.requestRefresh(false);
[137]2138 // TODO make this use a more efficient paint method...
2139 // Have the text item return a bigger repaint area if it has an
2140 // associated vector
2141 }
[1210]2142
[1101]2143 GraphicsManager g = EcosystemManager.getGraphicsManager();
2144 AxisAlignedBoxBounds bounds = (AxisAlignedBoxBounds) getBounds();
[137]2145
[4]2146 // the background is only cleared if required
2147 if (getBackgroundColor() != null) {
[1101]2148 Colour bgc = getBackgroundColor();
[278]2149 if (_alpha > 0) {
[1210]2150 bgc = new Colour(bgc.getRed(), bgc.getGreen(), bgc.getBlue(), Colour.FromComponent255(_alpha));
[247]2151 }
[278]2152
[1101]2153 Colour gradientColor = getGradientColor();
[1210]2154
[1101]2155 Fill fill;
[1210]2156
[1101]2157 if (gradientColor != null && bounds != null) {
[1210]2158 fill = new GradientFill(bgc,
2159 new Point((int) (bounds.getMinX() + bounds.getWidth() * 0.3), bounds.getMinY()), gradientColor,
2160 new Point((int) (bounds.getMinX() + bounds.getWidth() * 1.3), bounds.getMinY()));
[1101]2161 } else {
2162 fill = new Fill(bgc);
[247]2163 }
[278]2164
[1101]2165 g.drawRectangle(bounds, 0.0, fill, null, null, null);
[4]2166 }
[247]2167
[242]2168 if (hasVisibleBorder()) {
[1101]2169 Stroke borderStroke = new Stroke(getThickness(), DEFAULT_CAP, DEFAULT_JOIN);
2170 g.drawRectangle(bounds, 0.0, null, getPaintBorderColor(), borderStroke, null);
[242]2171 }
[247]2172
[282]2173 if (hasFormula()) {
[1101]2174 Stroke highlightStroke = new Stroke(1F, DEFAULT_CAP, DEFAULT_JOIN);
2175 Point start = getEdgePosition(0, true);
2176 Point end = getEdgePosition(0, false);
2177 g.drawLine(start, end, getPaintHighlightColor(), highlightStroke);
[282]2178 }
2179
[1210]2180 if (isHighlighted()) {
[1190]2181 Stroke highlightStroke = new Stroke(getHighlightThickness(), DEFAULT_CAP, DEFAULT_JOIN);
[1101]2182 Fill fill;
2183 if (HighlightMode.Enclosed.equals(getHighlightMode())) {
2184 fill = new Fill(getPaintHighlightColor());
2185 } else {
2186 fill = null;
2187 }
2188 g.drawRectangle(bounds, 0.0, fill, getPaintHighlightColor(), highlightStroke, null);
[108]2189 }
2190
[4]2191 float y = getY();
[1101]2192 Colour paintColour = getPaintColor();
2193 if (_alpha > 0) {
2194 paintColour = new Colour(paintColour);
2195 paintColour.setAlpha(Colour.FromComponent255(_alpha));
2196 }
[1296]2197 if (getTextLayouts() == _placeholderTextLayouts) {
2198 paintColour = paintColour.clone();
2199 paintColour.setAlpha(Colour.FromComponent255(60));
2200 }
[4]2201
[1101]2202 Colour selectionColour = getSelectionColour();
[4]2203
2204 // width -= getX();
2205 // int line = 0;
2206 // boolean tab = false;
[1210]2207 synchronized (getTextLayouts()) {
2208 for (int i = 0; i < getTextLayouts().size(); i++) {
2209 TextLayout layout = getTextLayouts().get(i);
[4]2210
[1101]2211 Range<Integer> selectedRange = getSelectedRange(i);
2212 if (selectedRange != null) {
[1210]2213 AxisAlignedBoxBounds highlight = layout.getLogicalHighlightShape(selectedRange.lowerBound,
2214 selectedRange.upperBound);
[1101]2215 highlight.getTopLeft().add(getX() + getJustOffset(layout), (int) y);
[1210]2216 g.drawRectangle(highlight, 0.0, new Fill(selectionColour), null, null, null);
[4]2217 }
2218
[1210]2219 if (layout.getCharacterCount() == 0) {
2220 continue;
2221 }
[1095]2222 int ldx = 1 + getX() + getJustOffset(layout); // Layout draw x
[1210]2223 g.drawTextLayout(layout, new Point(ldx, (int) y), paintColour);
[1095]2224
[4]2225 y += getLineDrop(layout);
2226 }
2227 }
[1210]2228
[1101]2229 paintLink();
[4]2230 }
[137]2231
[1101]2232 // TODO: Revise
[130]2233 @Override
[1210]2234 protected AxisAlignedBoxBounds getLinkDrawArea() {
[1101]2235 return getDrawingArea();
[130]2236 }
[4]2237
2238 /**
2239 * Determines if this text has any text in it.
2240 *
2241 * @return True if this Item has no text in it, false otherwise.
2242 */
[1210]2243 public boolean isEmpty() {
[1101]2244 return (_text == null || _text.length() == 0);
[4]2245 }
2246
2247 @Override
2248 public Text copy() {
2249 Text copy = new Text(getID());
2250 // copy standard item values
2251 Item.DuplicateItem(this, copy);
[98]2252
[4]2253 // copy values specific to text items
2254 copy.setSpacing(getSpacing());
2255 copy.setInitialSpacing(getInitialSpacing());
[400]2256
[1527]2257 copy.setWidth(getWidth());
[4]2258 copy.setJustification(getJustification());
2259 copy.setLetterSpacing(getLetterSpacing());
2260 copy.setWordSpacing(getWordSpacing());
[1527]2261 //copy.setWidth(getWidthToSave());
[1125]2262 copy.setFont(getFont().clone());
[1200]2263 copy.setMinWidth(getMinWidthToSave());
2264 copy.setMask(_mask);
[376]2265 if (hasFormula()) {
2266 copy.calculate(getFormula());
2267 } else {
2268 copy.setText(_text.toString());
2269 }
[108]2270 copy.setHidden(!isVisible());
[4]2271 return copy;
2272 }
2273
2274 @Override
[108]2275 public float getSize() {
[1416]2276 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.FONT_STR)) {
2277 return ((Text) this.getPrimary()).getSize();
2278 } else {
2279 return getPaintFont().getSize();
2280 }
2281
2282 //return getPaintFont().getSize();
[4]2283 }
2284
2285 /**
2286 * Returns the number of characters in this Text, excluding new lines.
2287 *
2288 * @return The sum of the length of each line of text
2289 */
2290 public int getLength() {
2291 return _text.length();
2292 }
2293
2294 @Override
[108]2295 public void setSize(float size) {
[130]2296 invalidateAll();
[311]2297 // size *= UserSettings.ScaleFactor;
[80]2298 // Dont want to have size set when duplicating a point which has size 0
[1190]2299 if (size < 0) {
[80]2300 return;
[1190]2301 }
[80]2302
[1190]2303 if (size < MINIMUM_FONT_SIZE) {
[80]2304 size = MINIMUM_FONT_SIZE;
[1190]2305 }
[1101]2306 Font currentFont = getPaintFont();
2307 currentFont.setSize((int) size);
[1210]2308 // setFont(currentFont);
[348]2309 rebuild(true);
[130]2310 invalidateAll();
[1416]2311
2312 if (isSurrogate()) {
2313 surrogatePropertyInheritance.put(DefaultFrameWriter.FONT_STR, false);
2314 Item primary = getPrimary();
2315 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.FONT_STR)) {
2316 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
2317 primary.primaryPropertyEncryption.put(DefaultFrameWriter.FONT_STR, inheritanceCheckOnSave);
2318 }
2319 }
[4]2320 }
2321
2322 @Override
2323 public void setAnnotation(boolean val) {
[1101]2324 float mouseX = DisplayController.getFloatMouseX();
2325 float mouseY = DisplayController.getFloatMouseY();
2326 Point newPoint = new Point();
[4]2327 if (val) {
2328 // if this is already an annotation, do nothing
[1190]2329 if (isAnnotation()) {
[4]2330 return;
[1190]2331 }
[1095]2332 if (!isLineEnd() && _text.length() > 0 && _text.charAt(0) == DEFAULT_BULLET) {
[1101]2333 newPoint.set(insertText("\b", mouseX, mouseY, 1));
[1190]2334 if (_text.length() > 0 && _text.charAt(0) == ' ') {
[1143]2335 newPoint.set(insertText("\b", newPoint.getX(), newPoint.getY(), 1));
[1190]2336 }
[80]2337 } else {
[1101]2338 newPoint.set(insertText("@", mouseX, mouseY, 0));
[4]2339 }
2340 } else {
2341 // if this is not an annotation, do nothing
[1190]2342 if (!isAnnotation()) {
[4]2343 return;
[1190]2344 }
[108]2345 if (!isLineEnd() && _text.charAt(0) == '@') {
[1101]2346 newPoint.set(insertText("\b", mouseX, mouseY, 1));
[1143]2347 newPoint.set(insertText(DEFAULT_BULLET_STRING, newPoint.getX(), newPoint.getY(), 0));
[80]2348 } else {
[1101]2349 newPoint.set(insertText("\b", mouseX, mouseY, 1));
[80]2350 }
2351 }
[247]2352 FrameUtils.setLastEdited(this);
[242]2353 rebuild(true);
[1143]2354 DisplayController.setCursorPosition(newPoint.getX(), newPoint.getY(), false);
[80]2355 }
2356
2357 /**
2358 *
2359 */
2360 private void insertString(String toInsert, int pos) {
2361 assert (toInsert.length() > 0);
[1101]2362 invalidateAll();
[80]2363 _text.insert(pos, toInsert);
[1101]2364 rebuild(false);
2365 invalidateAll();
[80]2366 }
2367
[108]2368 private void deleteChar(int pos) {
[80]2369 _text.deleteCharAt(pos);
2370
2371 if (_text.length() == 0) {
[1200]2372 if (this.getMinWidth() != null) {
[1210]2373 // final TextLayout base = _textLayouts.get(0);
2374 // _textLayouts.set(0, TextLayout.get(" ", base.getFont(), 0, 1));
2375 getTextLayouts().clear();
[1200]2376 }
[108]2377 if (this.isLineEnd()) {
[72]2378 // Remove and replace with a dot
[1101]2379 Item.replaceText(this);
2380 DisplayController.setCursorPosition(this._x, this._y);
[72]2381 }
[80]2382 return;
2383 }
[72]2384
[4]2385 }
2386
2387 @Override
2388 public boolean isAnnotation() {
[1190]2389 if (_text != null && _text.length() > 0 && _text.charAt(0) == '@') {
[4]2390 return true;
[1190]2391 }
[4]2392
2393 return false;
2394 }
2395
2396 public boolean isSpecialAnnotation() {
2397 assert _text != null;
2398 String s = _text.toString().toLowerCase();
2399 if (s.length() > 0 && s.indexOf("@") == 0) {
[1095]2400 if (s.equals("@old") || s.equals("@ao") || s.equals("@itemtemplate") || s.equals("@parent")
2401 || s.equals("@next") || s.equals("@previous") || s.equals("@first") || s.equals("@i")
[1190]2402 || s.equals("@iw") || s.equals("@f")) {
[4]2403 return true;
[1190]2404 }
[4]2405 }
2406
2407 return false;
2408 }
2409
2410 @Override
2411 public Item merge(Item merger, int mouseX, int mouseY) {
[50]2412 if (merger.isLineEnd()) {
[98]2413 // Merging line ends onto non line end text is a no-op
[1190]2414 if (!isLineEnd()) {
[87]2415 return null;
[1190]2416 }
[98]2417
[1190]2418 if (merger instanceof Text) {
[80]2419 insertText(((Text) merger).getText(), mouseX, mouseY);
[1190]2420 }
[71]2421
2422 // Set the position by moving the cursor before calling this
2423 // method!!
2424
[4]2425 List<Line> lines = new LinkedList<Line>();
[50]2426 lines.addAll(merger.getLines());
[71]2427 for (Line line : lines) {
[97]2428 line.replaceLineEnd(merger, this);
[50]2429 }
2430 merger.delete();
[71]2431 this.setOffset(0, 0);
[4]2432 return null;
2433 }
2434
[1190]2435 if (!(merger instanceof Text)) {
[4]2436 return merger;
[1190]2437 }
[4]2438
2439 Text merge = (Text) merger;
2440
2441 // insertText(merge.getText(), mouseX, mouseY);
2442
2443 // if the item being merged has a link
2444 if (merge.getLink() != null) {
2445 // if this item has a link, keep it on the cursor
2446 if (getLink() != null) {
2447 merge.setText(merge.getLink());
2448 merge.setLink(null);
2449 // return merge;
2450 // TODO get this to return the merged item and attach it to the
2451 // cursor only when the user presses the middle button.
[1190]2452 } else {
[4]2453 setLink(merge.getLink());
[1190]2454 }
[4]2455 }
2456
2457 return null;
2458 }
2459
2460 /**
2461 * Resets the position of the item to the default position for a title item.
2462 *
2463 */
2464 public void resetTitlePosition() {
[1190]2465 final int boundsHeight = getBoundsHeight();
2466 setPosition(MARGIN_LEFT, MARGIN_LEFT + boundsHeight);
[371]2467 Frame modelFrame = getParentOrCurrentFrame();
[400]2468 if (modelFrame != null) {
[1244]2469 int model_frame_name_x = modelFrame.getNameItem().getX();
2470 if (model_frame_name_x < DisplayController.MINIMUM_FRAME_WIDTH) {
[1258]2471 System.err.println("**** Text::resetTitlePostion(): value to be used as right margin from position of frameName < 512");
[1245]2472 System.err.println(" Overriding to ensure reasonable width for title");
[1244]2473 model_frame_name_x = DisplayController.MINIMUM_FRAME_WIDTH;
2474 }
2475
2476 setRightMargin(model_frame_name_x - MARGIN_LEFT, true);
[371]2477 } else {
[1095]2478 System.out.print("Error: text.resetTitlePosition, getParent or currentFrame returned null");
[400]2479 setRightMargin(MARGIN_LEFT, true);
[371]2480 }
[4]2481 }
2482
[80]2483 /**
2484 * Removes the set of characters up to the first space in this text item.
2485 *
2486 * @return the string that was removed.
2487 */
[4]2488 public String stripFirstWord() {
2489 int firstSpace = _text.toString().indexOf(' ');
2490
2491 // if there is only one word just make it blank
2492 if (firstSpace < 0 || firstSpace + 1 >= _text.length()) {
2493 String text = _text.toString();
2494 setText("");
2495 return text;
2496 }
2497
2498 String firstWord = _text.toString().substring(0, firstSpace);
[80]2499 setText(_text.toString().substring(firstSpace).trim());
[4]2500
2501 return firstWord;
2502 }
2503
[1190]2504 @Override
[4]2505 public String toString() {
2506 String message = "[" + getFirstLine() + "]" + FRAME_NAME_SEPARATOR;
2507
[1190]2508 if (getParent() != null) {
[80]2509 return message + getParent().getName();
[1190]2510 }
[4]2511 return message + getDateCreated();
2512 }
2513
2514 public Text getTemplateForm() {
2515 Text template = this.copy();
2516 template.setID(-1);
[1095]2517 // reset width of global templates so the widths of the items on the settings
2518 // frames don't cause issues
2519 // this is in response to the fact that FrameCreator.addItem() sets rightMargin
2520 // when it adds items
[776]2521 template.setWidth(null);
[376]2522 /*
[1095]2523 * The template must have text otherwise the bounds height will be zero!! This
2524 * will stop escape drop down from working if there is no item template
[376]2525 */
[4]2526 template.setText("@");
2527 return template;
2528 }
[7]2529
[1101]2530 /**
2531 * Checks if the given point is 'near' any line of the text item.
2532 */
[7]2533 @Override
2534 public boolean isNear(int x, int y) {
2535 if (super.isNear(x, y)) {
2536 // TODO check that it is actually near one of the lines of space
2537 // return contains(x, y, getGravity() * 2 + NEAR_DISTANCE);
2538 // at the moment contains ignores gravity when checking the top and
2539 // bottom of text lines... so the cursor must be between two text
2540 // lines
[107]2541 float textY = getY();
2542 float textX = getX();
[7]2543
[1210]2544 for (TextLayout text : getTextLayouts()) {
[7]2545 // check left and right of each box
[1101]2546 AxisAlignedBoxBounds textOutline = text.getLogicalHighlightShape(0, text.getCharacterCount());
[13]2547
[7]2548 // check if the cursor is within the top, bottom and within the
2549 // gravity of right
[1210]2550 if (y - textY > textOutline.getMinY() - NEAR_DISTANCE
2551 && y - textY < textOutline.getMinY() + textOutline.getHeight() + NEAR_DISTANCE
2552 && x - textX < textOutline.getWidth() + NEAR_DISTANCE) {
[7]2553 return true;
[1101]2554 }
[1210]2555
[7]2556 textY += getLineDrop(text);
2557 }
2558 }
2559 return false;
2560 }
[13]2561
[7]2562 @Override
[50]2563 public void anchor() {
2564 super.anchor();
2565 // ensure all text items have their selection cleared
2566 clearSelection();
2567 setAlpha(0);
[1190]2568 if (isLineEnd()) {
[1101]2569 DisplayController.setCursor(Item.DEFAULT_CURSOR);
[1190]2570 }
[400]2571
[421]2572 String text = _text.toString().trim();
2573
[663]2574 clipFrameMargin();
2575
2576 // Show the overlay stuff immediately if this is an overlay item
2577 if (hasLink() && (text.startsWith("@ao") || text.startsWith("@o"))) {
[1101]2578 StandardGestureActions.Refresh();
[663]2579 }
2580 }
[1095]2581
[663]2582 private void clipFrameMargin() {
[400]2583 if (!hasFixedWidth()) {
[1258]2584 int frameWidth = DisplayController.getFramePaintAreaWidth();
[400]2585 /*
[1095]2586 * Only change width if it is more than 150 pixels from the right of the screen
[400]2587 */
[663]2588 if (!_text.toString().contains(" ")) {
[421]2589 Integer width = getWidth();
[1190]2590 if (width == null || width < 0) {
[1415]2591 setWidth(Integer.MIN_VALUE + 1); // +1 makes it safe to do math.abs on
[1190]2592 }
[421]2593 } else if (frameWidth - getX() > ADJUST_WIDTH_THRESHOLD) {
[400]2594 justify(false);
[404]2595 // setRightMargin(frameWidth, false);
[400]2596 }
2597 }
[50]2598 }
[1095]2599
[1101]2600 public void justify(boolean fixWidth, PolygonBounds enclosure) {
[613]2601 // if autowrap is on, wrapping is done every time we draw
[1210]2602 if (ExperimentalFeatures.AutoWrap.get()) {
[1190]2603 return;
2604 }
[1095]2605
[1258]2606 Integer width = DisplayController.getFramePaintAreaWidth();
[71]2607
[400]2608 // Check if that text item is inside an enclosing rectangle...
2609 // Set its max width accordingly
2610 if (enclosure != null) {
[1101]2611 AxisAlignedBoxBounds bounds = AxisAlignedBoxBounds.getEnclosing(enclosure);
2612 if (bounds.getWidth() > 200 && getX() < bounds.getWidth() / 3 + bounds.getMinX()) {
2613 width = bounds.getMinX() + bounds.getWidth();
[400]2614 }
2615 }
2616
[1190]2617 if (getWidth() == null) {
2618 setRightMargin(width, fixWidth);
2619 }
[400]2620
[1101]2621 // Check for the annotation that restricts the width of text items on the frame
[400]2622 String widthString;
[1095]2623 if ((widthString = getParentOrCurrentFrame().getAnnotationValue("maxwidth")) != null) {
[400]2624 try {
2625 int oldWidth = getWidth();
2626 int maxWidth = Integer.parseInt(widthString);
[1190]2627 if (maxWidth < oldWidth) {
[400]2628 setWidth(maxWidth);
[1190]2629 }
[400]2630 } catch (NumberFormatException nfe) {
[1520]2631 nfe.printStackTrace();
[400]2632 }
2633 }
2634 }
2635
[920]2636 public void justify(boolean fixWidth) {
2637 // if autowrap is on, wrapping is done every time we draw
[1210]2638 if (ExperimentalFeatures.AutoWrap.get()) {
[1190]2639 return;
2640 }
[1101]2641
[920]2642 this.justify(fixWidth, FrameUtils.getEnlosingPolygon());
2643 }
2644
[71]2645 public void resetFrameNamePosition() {
[1258]2646 //Dimension maxSize = DisplayController.getSizeEnforceMinimum();
2647 Dimension maxSize = DisplayController.getFramePaintAreaSize();
2648
[80]2649 if (maxSize != null) {
[147]2650 // setMaxWidth(maxSize.width);
[80]2651 setPosition(maxSize.width - getBoundsWidth(), getBoundsHeight());
2652 }
[71]2653 }
[80]2654
[78]2655 @Override
[80]2656 protected int getLinkYOffset() {
[1210]2657 if (getTextLayouts().size() == 0) {
[137]2658 return 0;
[1190]2659 }
[1210]2660 return Math.round(-(getTextLayouts().get(0).getAscent() / 2));
[78]2661 }
[80]2662
2663 @Override
2664 public String getName() {
2665 return getFirstLine();
2666 }
2667
2668 public static final String TAB_STRING = " ";
2669
[1101]2670 public Point insertTab(char ch, float mouseX, float mouseY) {
[80]2671 return insertText("" + ch, mouseX, mouseY);
2672 }
2673
[1101]2674 public Point removeTab(char ch, float mouseX, float mouseY) {
[80]2675 // Insert a space as a flag that it is a backwards tab
2676 return insertText(ch + " ", mouseX, mouseY);
2677 }
[108]2678
[115]2679 public static boolean isBulletChar(char c) {
[137]2680 for (int i = 0; i < BULLETS.length; i++) {
[1190]2681 if (BULLETS[i] == c) {
[115]2682 return true;
[1190]2683 }
[115]2684 }
[137]2685 return c == '*' || c == '+' || c == '>' || c == '-' || c == 'o';
[115]2686 }
[137]2687
[1190]2688 @Override
[116]2689 public boolean hasOverlay() {
[1190]2690 if (!isAnnotation() || getLink() == null) {
[116]2691 return false;
[1190]2692 }
[145]2693 String text = getText().toLowerCase();
[147]2694 // TODO make it so can just check the _overlay variable
[1101]2695 // Mike can't remember the reason _overlay var can't be use! oops
[1190]2696 if (!text.startsWith("@")) {
[154]2697 return false;
[1190]2698 }
[1095]2699 return text.startsWith("@o") || text.startsWith("@ao") || text.startsWith("@v") || text.startsWith("@av");
[116]2700 }
[131]2701
2702 public boolean hasSelection() {
2703 return getSelectionSize() > 0;
2704 }
[143]2705
2706 /**
2707 * Dont save text items that are all white space.
2708 */
2709 @Override
2710 public boolean dontSave() {
2711 String text = getText();
2712 assert (text != null);
[1200]2713 return super.dontSave() || (text.trim().length() == 0 && this.getMinWidth() == null);
[143]2714 }
[147]2715
[176]2716 @Override
[291]2717 public boolean calculate(String formula) {
[1190]2718 if (DisplayController.isXRayMode()) {
[291]2719 return false;
[1190]2720 }
[199]2721
[176]2722 super.calculate(formula);
[242]2723 if (isFloating() || formula == null || formula.length() == 0) {
[291]2724 return false;
[176]2725 }
[376]2726 formula = formula.replace(':', '=');
2727
[196]2728 String lowercaseFormula = formula.toLowerCase();
[176]2729 ExpediteeJEP myParser = new ExpediteeJEP();
[278]2730
[247]2731 int nextVarNo = 1;
[278]2732
[242]2733 // Add variables from the containing rectangle if the item being
2734 // calculated is inside the enclosing rectangle
2735 Collection<Item> enclosed = getItemsInSameEnclosure();
2736 for (Item i : enclosed) {
[1190]2737 if (i == this) {
[247]2738 continue;
[1190]2739 }
[247]2740 if (i instanceof Text && !i.isAnnotation()) {
[376]2741 AttributeValuePair pair = i.getAttributeValuePair();
[242]2742 if (pair.hasPair()) {
2743 try {
[278]2744 double value = pair.getDoubleValue();
[247]2745 myParser.addVariable(pair.getAttribute(), value);
[278]2746 // myParser.addVariable("$" + nextVarNo++, value);
[247]2747 } catch (NumberFormatException nfe) {
2748 continue;
[242]2749 } catch (Exception e) {
2750 e.printStackTrace();
2751 }
[282]2752 } // else {
[1095]2753 // Add anonomous vars
[282]2754 try {
2755 double value = pair.getDoubleValue();
[1190]2756 if (value != Double.NaN) {
[376]2757 myParser.addVariable("$" + nextVarNo++, value);
[1190]2758 }
[282]2759 } catch (NumberFormatException nfe) {
2760 continue;
2761 } catch (Exception e) {
2762 e.printStackTrace();
[242]2763 }
[282]2764 // }
[242]2765 }
2766 }
2767
2768 // Add the variables from this frame
[176]2769 myParser.addVariables(this.getParentOrCurrentFrame());
2770 String linkedFrame = getAbsoluteLink();
[199]2771 // Add the relative frame variable if the item is linked
[176]2772 if (linkedFrame != null) {
[196]2773 Frame frame = FrameIO.LoadFrame(linkedFrame);
2774 myParser.addVariables(frame);
[199]2775 // If the frame is linked add vector variable for the frame
2776 if (lowercaseFormula.contains("$frame")) {
[1095]2777 myParser.addVectorVariable(frame.getNonAnnotationItems(true), "$frame");
[196]2778 }
[176]2779 }
[199]2780 // Add the relative box variable if this item is a line end
2781 if (this.isLineEnd()) {
2782 // if its a line end add the enclosed stuff as an @variable
2783 if (lowercaseFormula.contains("$box")) {
[214]2784 myParser.addVectorVariable(getEnclosedItems(), "$box");
[196]2785 }
2786 }
[176]2787 myParser.resetObserver();
[190]2788 try {
[220]2789 Node node = myParser.parse(formula);
[190]2790 String result = myParser.evaluate(node);
[176]2791 if (result != null) {
2792 this.setText(result);
[196]2793 this.setFormula(formula);
[376]2794
2795 if (!this.hasAction()) {
2796 setActionMark(false);
2797 setAction("extract formula");
2798 }
[176]2799 }
[196]2800 } catch (Throwable e) {
[1095]2801 // e.printStackTrace();
[376]2802 String formula2 = getFormula();
2803 this.setText(formula2);
2804 this.setFormula(formula2);
[291]2805 return false;
[199]2806 }
[376]2807
2808 _attributeValuePair = null;
2809
[291]2810 return true;
[176]2811 }
[190]2812
[199]2813 /**
[1095]2814 * Gets items which are in the same enclosure as this item. In the event more
2815 * than one enclosure meets this criteria, then the one returned is the one with
2816 * the smallest area. TODO: Improve the efficiency of this method
[247]2817 *
[242]2818 * @return
2819 */
2820 public Collection<Item> getItemsInSameEnclosure() {
2821 Collection<Item> sameEnclosure = null;
2822 Collection<Item> seen = new HashSet<Item>();
2823 Frame parent = getParentOrCurrentFrame();
2824 double enclosureArea = Double.MAX_VALUE;
2825 for (Item i : parent.getVisibleItems()) {
[376]2826 /*
[1095]2827 * Go through all the enclosures looking for one that includes this item
[376]2828 */
[247]2829 if (!seen.contains(i) && i.isEnclosed()) {
[242]2830 seen.addAll(i.getEnclosingDots());
2831 Collection<Item> enclosed = i.getEnclosedItems();
[247]2832 // Check if we have found an enclosure containing this item
2833 // Check it is smaller than any other enclosure found containing
2834 // this item
[1095]2835 if (enclosed.contains(this) && i.getEnclosedArea() < enclosureArea) {
[242]2836 sameEnclosure = enclosed;
2837 }
2838 }
2839 }
2840
[1190]2841 if (sameEnclosure == null) {
[242]2842 return new LinkedList<Item>();
[1190]2843 }
[247]2844
[242]2845 return sameEnclosure;
2846 }
[1095]2847
[242]2848 /**
[1095]2849 * Returns true if items of the parent frame should be recalculated when this
2850 * item is modified
[199]2851 */
[1190]2852 @Override
[190]2853 public boolean recalculateWhenChanged() {
[278]2854 if (/*
2855 * !isAnnotation() &&
[1190]2856 */(hasFormula() || isLineEnd())) {
[190]2857 return true;
[1190]2858 }
[190]2859 try {
[376]2860 AttributeValuePair avp = getAttributeValuePair();
2861
[1190]2862 if (!avp.getDoubleValue().equals(Double.NaN)) {
[376]2863 return true;
[1190]2864 }
[190]2865 } catch (Exception e) {
[376]2866 e.printStackTrace();
[190]2867 }
[376]2868
[190]2869 return false;
2870 }
[226]2871
2872 public float getLineHeight() {
[1210]2873 return getLineDrop(getTextLayouts().get(0));
[226]2874 }
[278]2875
2876 @Override
[1101]2877 public void setAnchorLeft(Integer anchor) {
[720]2878 if (!isLineEnd()) {
2879 super.setAnchorLeft(anchor);
2880 // Subtract off the link width
2881 if (anchor != null) {
2882 setX(anchor + getLeftMargin());
2883 }
2884 return;
2885 }
2886 invalidateFill();
2887 invalidateCommonTrait(ItemAppearence.PreMoved);
[1095]2888
[1101]2889 this._anchoring.setLeftAnchor(anchor);
[1095]2890
[720]2891 int oldX = getX();
2892 if (anchor != null) {
2893 float deltaX = anchor + getLeftMargin() - oldX;
2894 anchorConnected(AnchorEdgeType.Left, deltaX);
2895 }
[812]2896
[720]2897 invalidateCommonTrait(ItemAppearence.PostMoved);
2898 invalidateFill();
[1413]2899
2900 if (isSurrogate()) {
2901 surrogatePropertyInheritance.put(DefaultFrameWriter.ANCHOR_LEFT_STR, false);
[1414]2902 Item primary = getPrimary();
[1413]2903 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.ANCHOR_LEFT_STR)) {
[1415]2904 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
2905 primary.primaryPropertyEncryption.put(DefaultFrameWriter.ANCHOR_LEFT_STR, inheritanceCheckOnSave);
[1413]2906 }
2907 }
[720]2908 }
[1514]2909
2910 @Override
2911 public void setAnchorCenterX(Integer anchor) {
2912 if (!isLineEnd()) {
2913 super.setAnchorCenterX(anchor);
2914 // Subtract off the link width
2915 if (anchor != null) {
2916 int alignedToLeft = DisplayController.getFramePaintArea().getCentreX() + anchor;
2917 int alignedToCenter = alignedToLeft - (getBoundsWidth() / 2);
2918 setX(alignedToCenter);
2919 }
2920 return;
2921 }
2922 invalidateFill();
2923 invalidateCommonTrait(ItemAppearence.PostMoved);
2924
2925 this._anchoring.setCenterXAnchor(anchor);
2926
2927 int oldX = getX();
2928 if (anchor != null) {
2929 float deltaX = DisplayController.getFramePaintArea().getCentreX() - anchor - getBoundsWidth() + getLeftMargin() - oldX;
2930 anchorConnected(AnchorEdgeType.CenterY, deltaX);
2931 }
2932
2933 invalidateCommonTrait(ItemAppearence.PostMoved);
2934 invalidateFill();
2935
2936 if (isSurrogate()) {
2937 surrogatePropertyInheritance.put(DefaultFrameWriter.ANCHOR_CENTERX_STR, false);
2938 Item primary = getPrimary();
2939 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.ANCHOR_CENTERX_STR)) {
2940 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
2941 primary.primaryPropertyEncryption.put(DefaultFrameWriter.ANCHOR_CENTERX_STR, inheritanceCheckOnSave);
2942 }
2943 }
2944 }
[720]2945
2946 @Override
[1101]2947 public void setAnchorRight(Integer anchor) {
[278]2948 if (!isLineEnd()) {
2949 super.setAnchorRight(anchor);
2950 // Subtract off the link width
2951 if (anchor != null) {
[1258]2952 setX(DisplayController.getFramePaintAreaWidth() - anchor - getBoundsWidth() + getLeftMargin());
[1511]2953 //System.err.println("Text::setAnchorRight::boundsWidth=" + getBoundsWidth());
[278]2954 }
2955 return;
2956 }
2957 invalidateFill();
2958 invalidateCommonTrait(ItemAppearence.PreMoved);
[1095]2959
[1101]2960 this._anchoring.setRightAnchor(anchor);
[1095]2961
[278]2962 int oldX = getX();
2963 if (anchor != null) {
[1258]2964 float deltaX = DisplayController.getFramePaintAreaWidth() - anchor - getBoundsWidth()
[1210]2965 + getLeftMargin() - oldX;
[720]2966 anchorConnected(AnchorEdgeType.Right, deltaX);
[278]2967 }
[812]2968
[278]2969 invalidateCommonTrait(ItemAppearence.PostMoved);
2970 invalidateFill();
[1413]2971
2972 if (isSurrogate()) {
2973 surrogatePropertyInheritance.put(DefaultFrameWriter.ANCHOR_RIGHT_STR, false);
[1414]2974 Item primary = getPrimary();
[1413]2975 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.ANCHOR_RIGHT_STR)) {
[1415]2976 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
2977 primary.primaryPropertyEncryption.put(DefaultFrameWriter.ANCHOR_RIGHT_STR, inheritanceCheckOnSave);
[1413]2978 }
2979 }
[278]2980 }
[282]2981
[278]2982 @Override
[1101]2983 public void setAnchorTop(Integer anchor) {
[1210]2984 if (!isLineEnd()) {
[720]2985 super.setAnchorTop(anchor);
2986 if (anchor != null) {
[1210]2987 if (!getTextLayouts().isEmpty()) {
2988 final float ascent = getTextLayouts().get(0).getAscent();
[1200]2989 setY(anchor + ascent);
[1210]2990 } else if (this.getFont() != null) {
[1200]2991 // p could be any character
2992 final TextLayout fakeLayout = TextLayout.getManager().layoutStringSimple("p", this.getFont());
2993 final float ascent = fakeLayout.getAscent();
2994 EcosystemManager.getTextLayoutManager().releaseLayout(fakeLayout);
2995 setY(anchor + ascent);
[1210]2996 }
[720]2997 }
2998 return;
2999 }
3000 invalidateFill();
3001 invalidateCommonTrait(ItemAppearence.PreMoved);
[1095]3002
[1101]3003 this._anchoring.setTopAnchor(anchor);
[1095]3004
[720]3005 int oldY = getY();
3006 if (anchor != null) {
3007 float deltaY = anchor - oldY;
3008 anchorConnected(AnchorEdgeType.Top, deltaY);
3009 }
[812]3010
[720]3011 invalidateCommonTrait(ItemAppearence.PostMoved);
3012 invalidateFill();
[1413]3013
3014 if (isSurrogate()) {
3015 surrogatePropertyInheritance.put(DefaultFrameWriter.ANCHOR_TOP_STR, false);
[1414]3016 Item primary = getPrimary();
[1413]3017 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.ANCHOR_TOP_STR)) {
[1415]3018 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
3019 primary.primaryPropertyEncryption.put(DefaultFrameWriter.ANCHOR_TOP_STR, inheritanceCheckOnSave);
[1413]3020 }
3021 }
[720]3022 }
[1514]3023
3024 @Override
3025 public void setAnchorCenterY(Integer anchor) {
3026 if (!isLineEnd()) {
3027 super.setAnchorCenterY(anchor);
3028 if (anchor != null) {
3029 List<TextLayout> textLayouts = getTextLayouts();
3030 if (!textLayouts.isEmpty()) {
3031 int middle = textLayouts.size() / 2;
3032 float heightFromTopOfTextItem = 0;
3033 for (int i = 0; i <= middle; i++) {
3034 TextLayout tl = textLayouts.get(i);
3035 float ascent = tl.getAscent();
3036 float descent = tl.getDescent();
3037 heightFromTopOfTextItem += (ascent + descent);
3038 }
3039 heightFromTopOfTextItem -= textLayouts.get(middle).getDescent();
3040 float anchorCalc = anchor + this.getBoundsHeight() - heightFromTopOfTextItem;
3041 setY((DisplayController.getFramePaintAreaHeight() / 2) - anchorCalc);
3042 } else if (this.getFont() != null) {
3043 // p could be any character
3044 TextLayout fakeLayout = TextLayout.getManager().layoutStringSimple("p", this.getFont());
3045 float ascent = fakeLayout.getAscent();
3046 float descent = fakeLayout.getDescent();
3047 float middle = descent - ascent;
3048 float anchorCalc = anchor + this.getBoundsHeight() - middle;
3049 setY((DisplayController.getFramePaintAreaHeight() / 2) - anchorCalc);
3050 }
3051 }
3052 return;
3053 }
3054 }
[720]3055
3056 @Override
[1101]3057 public void setAnchorBottom(Integer anchor) {
[282]3058 if (!isLineEnd()) {
[278]3059 super.setAnchorBottom(anchor);
[873]3060 if (anchor != null) {
[1210]3061 if (!getTextLayouts().isEmpty()) {
3062 final float ascent = getTextLayouts().get(0).getAscent();
3063 final float descent = getTextLayouts().get(0).getDescent();
[1258]3064 setY(DisplayController.getFramePaintAreaHeight()
[1210]3065 - (anchor + this.getBoundsHeight() - ascent - descent));
[1200]3066 } else if (this.getFont() != null) {
3067 // p could be any character
3068 final TextLayout fakeLayout = TextLayout.getManager().layoutStringSimple("p", this.getFont());
3069 final float ascent = fakeLayout.getAscent();
3070 final float descent = fakeLayout.getDescent();
[1258]3071 setY(DisplayController.getFramePaintAreaHeight()
[1210]3072 - (anchor + this.getBoundsHeight() - ascent - descent));
[1200]3073 }
[1210]3074
[873]3075 }
[278]3076 return;
3077 }
3078 invalidateFill();
3079 invalidateCommonTrait(ItemAppearence.PreMoved);
[1095]3080
[1101]3081 this._anchoring.setBottomAnchor(anchor);
[1095]3082
[278]3083 int oldY = getY();
3084 if (anchor != null) {
[720]3085
[1258]3086 float deltaY = DisplayController.getFramePaintAreaHeight() - anchor - oldY;
[720]3087 anchorConnected(AnchorEdgeType.Bottom, deltaY);
[278]3088 }
[812]3089
[278]3090 invalidateCommonTrait(ItemAppearence.PostMoved);
3091 invalidateFill();
[1413]3092
3093 if (isSurrogate()) {
3094 surrogatePropertyInheritance.put(DefaultFrameWriter.ANCHOR_BOTTOM_STR, false);
[1414]3095 Item primary = getPrimary();
[1413]3096 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.ANCHOR_BOTTOM_STR)) {
[1415]3097 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
3098 primary.primaryPropertyEncryption.put(DefaultFrameWriter.ANCHOR_BOTTOM_STR, inheritanceCheckOnSave);
[1413]3099 }
3100 }
[278]3101 }
[311]3102
3103 @Override
3104 public void scale(Float scale, int originX, int originY) {
3105 setSize(getSize() * scale);
[400]3106
[475]3107 Integer width = getWidth();
3108 if (width != null) {
3109 setWidth(Math.round(width * scale));
3110 }
[1095]3111
[311]3112 super.scale(scale, originX, originY);
3113 rebuild(true);
3114 }
[410]3115
[1210]3116 protected AxisAlignedBoxBounds getPixelBounds(TextLayout layout) {
[909]3117 // Does 'layout' need to be synchronized (similar to _textLayouts below)??
3118 int x = getX();
3119 int y = getY();
3120
[1095]3121 int ldx = 1 + x + getJustOffset(layout); // Layout draw x
[1101]3122 AxisAlignedBoxBounds layout_rect = layout.getPixelBounds(ldx, y);
[909]3123
[1095]3124 return layout_rect;
[909]3125 }
[1210]3126
[1095]3127 /**
3128 * Creates the smallest possible rectangle object to enclose the Text Item
3129 * completely. Width of the rectangle is determined by the line in the Text Item
3130 * that protrudes to the right the most. Height of the rectangle is determined
3131 * by the number of lines in the Text Item.
3132 *
3133 * @return A rectangle enclosing the Text Item, without gravity represented.
3134 * @see #getPixelBoundsUnionTight()
3135 */
[1210]3136 public AxisAlignedBoxBounds getPixelBoundsUnion() {
3137 final AxisAlignedBoxBounds rect = getPixelBounds(getTextLayouts().get(0));
[1095]3138
[1101]3139 int cumulativeHeight = rect.getSize().height;
3140 int maxWidth = rect.getSize().width;
[1095]3141
[1210]3142 if (getTextLayouts().size() > 1) {
3143 for (int i = 1; i < getTextLayouts().size(); i++) {
3144 final AxisAlignedBoxBounds r = getPixelBounds(getTextLayouts().get(i));
3145 cumulativeHeight += getTextLayouts().get(i).getDescent() + getTextLayouts().get(i).getAscent();
[1190]3146 if (r.getSize().width > maxWidth) {
[1101]3147 maxWidth = r.getSize().width;
[1190]3148 }
[905]3149 }
[1095]3150 }
[1210]3151
[1101]3152 rect.getSize().width = maxWidth;
3153 rect.getSize().height = cumulativeHeight;
[1095]3154
3155 return rect;
3156 }
3157
3158 /**
3159 * Creates the smallest possible polygon to enclose the Text Item completely.
3160 * The shape of the polygon is determined by the length of each line, tightly
3161 * fitting the shape so that no white space is inside the resulting polygon.
3162 *
3163 * @return A polygon enclosing the Text Item, without gravity represented.
3164 * @see #getPixelBoundsUnion()
3165 */
[1210]3166 public PolygonBounds getPixelBoundsUnionTight() {
3167 final AxisAlignedBoxBounds rect = getPixelBounds(getTextLayouts().get(0));
3168 if (getTextLayouts().size() == 1) {
[1101]3169 return PolygonBounds.fromBox(rect);
[1095]3170 } else {
[1101]3171 final PolygonBounds poly = new PolygonBounds();
3172 poly.addPoint(rect.getMinX(), rect.getMinY());
3173 poly.addPoint(rect.getMaxX(), rect.getMinY());
[1210]3174 poly.addPoint(rect.getMaxX(), Math.round(rect.getMaxY() + getTextLayouts().get(0).getDescent()));
3175 int y = (int) (rect.getMaxY() + getTextLayouts().get(0).getDescent());
3176 for (int i = 1; i < getTextLayouts().size(); i++) {
3177 final AxisAlignedBoxBounds r = getPixelBounds(getTextLayouts().get(i));
[1101]3178 poly.addPoint(r.getMaxX(), y);
[1210]3179 poly.addPoint(r.getMaxX(), Math.round(y + r.getHeight() + getTextLayouts().get(i).getDescent()));
3180 y = Math.round(y + r.getHeight() + getTextLayouts().get(i).getDescent());
[905]3181 }
[1210]3182 poly.addPoint(rect.getMinX() + getPixelBounds(getTextLayouts().get(getTextLayouts().size() - 1)).getWidth(),
3183 Math.round(y + getTextLayouts().get(getTextLayouts().size() - 1).getDescent()));
3184 poly.addPoint(rect.getMinX(), Math.round(y + getTextLayouts().get(getTextLayouts().size() - 1).getDescent()));
[1095]3185 return poly;
[905]3186 }
3187 }
[1210]3188
[1101]3189 /*
[1210]3190 * public AxisAlignedBoxBounds getPixelBoundsUnion() { synchronized
3191 * (_textLayouts) {
3192 *
3193 * CombinationBoxBounds c = null;
3194 *
3195 * for (TextLayout layout: _textLayouts) { if (c == null) { c = new
3196 * CombinationBoxBounds(getPixelBounds(layout)); } else {
3197 * c.add(getPixelBounds(layout)); } }
3198 *
3199 * return AxisAlignedBoxBounds.getEnclosing(c);
3200 *
3201 * } }
3202 */
[1095]3203
3204 // public Rectangle getPixelBoundsUnion()
3205 // {
3206 // synchronized (_textLayouts) {
3207 //
3208 // int x = getX();
3209 // int y = getY();
3210 //
3211 // int min_xl = Integer.MAX_VALUE;
[1415]3212 // int max_xr = Integer.MIN_VALUE + 1; // +1 makes it safe to do math.abs on
[1095]3213 //
3214 // int min_yt = Integer.MAX_VALUE;
[1415]3215 // int max_yb = Integer.MIN_VALUE + 1; // +1 makes it safe to do math.abs on
[1095]3216 //
3217 //
3218 // for (int i = 0; i < _textLayouts.size(); i++) {
3219 // TextLayout layout = _textLayouts.get(i);
3220 //
3221 // int ldx = 1+x+getJustOffset(layout); // Layout draw x
3222 // Rectangle layout_rect = layout.getPixelBounds(null, ldx, y);
3223 //
3224 // int xl = layout_rect.x;
3225 // int xr = xl + layout_rect.width -1;
3226 //
3227 // int yt = layout_rect.y;
3228 // int yb = yt + layout_rect.height -1;
3229 //
3230 // min_xl = Math.min(min_xl,xl);
3231 // max_xr = Math.max(max_xr,xr);
3232 //
3233 // min_yt = Math.min(min_yt,yt);
3234 // max_yb = Math.max(max_yb,yb);
3235 // }
3236 //
3237 // if ((min_xl >= max_xr) || (min_yt >= max_yb)) {
3238 // // No valid rectangle are found
3239 // return null;
3240 // }
3241 //
3242 // return new Rectangle(min_xl,min_yt,max_xr-min_xl+1,max_yb-min_yt+1);
3243 //
3244 // }
3245 //
3246 // }
[410]3247 /*
3248 * Returns the SIMPLE statement contained by this text item.
3249 *
3250 */
3251 public String getStatement() {
3252 return getText().split("\\s+")[0];
3253 }
[1095]3254
[613]3255 public boolean getAutoWrap() {
[1413]3256 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.AUTO_WRAP_TO_SAVE_STR)) {
[1414]3257 return ((Text) this.getPrimary()).getAutoWrap();
[1410]3258 } else {
3259 return _autoWrap;
3260 }
[613]3261 }
[1095]3262
3263 // workaround since true is the default value and would not be displayed
3264 // normally
[613]3265 public String getAutoWrapToSave() {
[1413]3266 if (!getAutoWrap()) {
[613]3267 return null;
3268 }
3269 return "true";
3270 }
[1095]3271
[613]3272 public void setAutoWrap(boolean autoWrap) {
3273 _autoWrap = autoWrap;
[1413]3274
3275 if (isSurrogate()) {
3276 surrogatePropertyInheritance.put(DefaultFrameWriter.AUTO_WRAP_TO_SAVE_STR, false);
[1414]3277 Item primary = getPrimary();
[1413]3278 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.AUTO_WRAP_TO_SAVE_STR)) {
[1415]3279 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
3280 primary.primaryPropertyEncryption.put(DefaultFrameWriter.AUTO_WRAP_TO_SAVE_STR, inheritanceCheckOnSave);
[1413]3281 }
3282 }
[613]3283 }
[1101]3284
3285 /**
[1210]3286 * Creates a new Text Item whose text contains the given character. This method
3287 * also moves the mouse cursor to be pointing at the newly created Text Item
3288 * ready to insert the next character.
[1101]3289 *
3290 * @param start
3291 * The character to use as the initial text of this Item.
3292 * @return The newly created Text Item
3293 */
3294 public static Text createText(char start) {
[1210]3295 Text t = DisplayController.getCurrentFrame().createBlankText("" + start);
[1101]3296
3297 Point newMouse = t.insertChar(start, DisplayController.getMouseX(), DisplayController.getMouseY());
[1143]3298 DisplayController.setCursorPosition(newMouse.getX(), newMouse.getY(), false);
[1101]3299
3300 return t;
3301 }
3302
3303 /**
[1210]3304 * Creates a new Text Item with no text. The newly created Item is a copy of any
3305 * ItemTemplate if one is present, and inherits all the attributes of the
3306 * Template
[1101]3307 *
3308 * @return The newly created Text Item
3309 */
3310 public static Text createText() {
3311 return DisplayController.getCurrentFrame().createNewText();
3312 }
3313
3314 /**
[1210]3315 * If the given Item is null, then a new Text item is created with the current
3316 * date. If the given Item is not null, then the current date is prepended to
3317 * the Item's text
[1101]3318 *
3319 * @param toAdd
3320 * The Item to prepend the date to, or null
3321 */
3322 public static void AddDate(Item toAdd) {
3323 String date1 = Formatter.getDateTime();
3324 String date2 = Formatter.getDate();
3325 final String leftSeparator = " :";
3326 final String rightSeparator = ": ";
3327 String dateToAdd = date1 + rightSeparator;
3328 boolean prepend = false;
3329 boolean append = false;
3330
3331 // if the user is pointing at an item, add the date where ever the
3332 // cursor is pointing
3333 if (toAdd != null && toAdd instanceof Text) {
3334 // permission check
3335 if (!toAdd.hasPermission(UserAppliedPermission.full)) {
3336 MessageBay.displayMessage("Insufficicent permission to add the date to that item");
3337 return;
3338 }
3339
3340 Text textItem = (Text) toAdd;
3341
3342 String text = textItem.getText();
3343
3344 // check if the default date has already been put on this item
3345 if (text.startsWith(date1 + rightSeparator)) {
3346 textItem.removeText(date1 + rightSeparator);
3347 dateToAdd = date2 + rightSeparator;
3348 prepend = true;
3349 } else if (text.startsWith(date2 + rightSeparator)) {
3350 textItem.removeText(date2 + rightSeparator);
3351 dateToAdd = leftSeparator + date2;
3352 append = true;
3353 } else if (text.endsWith(leftSeparator + date2)) {
3354 textItem.removeEndText(leftSeparator + date2);
3355 append = true;
3356 dateToAdd = leftSeparator + date1;
3357 } else if (text.endsWith(leftSeparator + date1)) {
3358 textItem.removeEndText(leftSeparator + date1);
3359 if (textItem.getLength() > 0) {
3360 dateToAdd = "";
3361 prepend = true;
3362 } else {
3363 // use the default date format
3364 prepend = true;
3365 }
3366 }
3367
3368 if (prepend) {
3369 // add the date to the text item
3370 textItem.prependText(dateToAdd);
[1190]3371 if (dateToAdd.length() == textItem.getLength()) {
[1101]3372 DisplayController.setCursorPosition(textItem.getParagraphEndPosition());
[1190]3373 }
[1101]3374 } else if (append) {
3375 textItem.appendText(dateToAdd);
[1190]3376 if (dateToAdd.length() == textItem.getLength()) {
[1101]3377 DisplayController.setCursorPosition(textItem.getPosition());
[1190]3378 }
[1101]3379 } else {
3380 for (int i = 0; i < date1.length(); i++) {
3381 StandardGestureActions.processChar(date1.charAt(i), false);
3382 }
3383 }
3384
3385 textItem.getParent().setChanged(true);
3386 DisplayController.requestRefresh(true);
3387 // } else {
3388 // MessageBay
3389 // .displayMessage("Only text items can have the date prepended to
3390 // them");
3391 // }
3392 // otherwise, create a new text item
3393 } else {
3394 Text newText = createText();
3395 newText.setText(dateToAdd);
3396 DisplayController.getCurrentFrame().addItem(newText);
3397 DisplayController.getCurrentFrame().setChanged(true);
3398 DisplayController.requestRefresh(true);
3399
3400 DisplayController.setCursorPosition(newText.getParagraphEndPosition());
3401 }
3402 }
[1210]3403
3404 public Integer getMask() {
[1413]3405 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.MASK_STR)) {
[1414]3406 return ((Text) this.getPrimary()).getMask();
[1210]3407 } else {
[1413]3408 return _mask;
[1210]3409 }
[1200]3410 }
[1210]3411
3412 public void setMask(final Integer c) {
3413 _mask = c;
[1413]3414
3415 if (isSurrogate()) {
3416 surrogatePropertyInheritance.put(DefaultFrameWriter.MASK_STR, false);
[1414]3417 Item primary = getPrimary();
[1413]3418 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.MASK_STR)) {
[1415]3419 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
3420 primary.primaryPropertyEncryption.put(DefaultFrameWriter.MASK_STR, inheritanceCheckOnSave);
[1413]3421 }
3422 }
[1210]3423 }
[1296]3424
3425 public String getPlaceholder() {
[1413]3426 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.PLACEHOLDER_STR)) {
[1414]3427 return ((Text) this.getPrimary()).getPlaceholder();
[1410]3428 } else {
3429 if (_placeholder == null || _placeholder.length() == 0) {
3430 return null;
3431 }
3432 return _placeholder.toString();
[1296]3433 }
3434 }
[1210]3435
[1296]3436 public void setPlaceholder(String _placeholder) {
3437 this._placeholder = new StringBuffer(_placeholder);
[1413]3438
3439 if (isSurrogate()) {
3440 surrogatePropertyInheritance.put(DefaultFrameWriter.PLACEHOLDER_STR, false);
[1414]3441 Item primary = getPrimary();
[1413]3442 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.PLACEHOLDER_STR)) {
[1415]3443 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
3444 primary.primaryPropertyEncryption.put(DefaultFrameWriter.PLACEHOLDER_STR, inheritanceCheckOnSave);
[1413]3445 }
3446 }
[1296]3447 }
3448
[1210]3449 protected List<TextLayout> getTextLayouts() {
[1296]3450 if (this.getPlaceholder() != null && (this._text == null || this._text.length() == 0)) {
3451 return _placeholderTextLayouts;
3452 } else if (this.getMask() != null) {
[1210]3453 return _maskTextLayouts;
3454 } else {
3455 return _textLayouts;
3456 }
3457 }
[1379]3458
3459 public boolean isSingleLineOnly() {
[1413]3460 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.SINGLE_LINE_ONLY_STR)) {
[1414]3461 return ((Text) this.getPrimary()).isSingleLineOnly();
[1410]3462 } else {
3463 return _singleLine;
3464 }
[1379]3465 }
3466
3467 public void setSingleLineOnly(boolean _singleLine) {
3468 this._singleLine = _singleLine;
[1413]3469
3470 if (isSurrogate()) {
3471 surrogatePropertyInheritance.put(DefaultFrameWriter.SINGLE_LINE_ONLY_STR, false);
[1414]3472 Item primary = getPrimary();
[1413]3473 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.SINGLE_LINE_ONLY_STR)) {
[1415]3474 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
3475 primary.primaryPropertyEncryption.put(DefaultFrameWriter.SINGLE_LINE_ONLY_STR, inheritanceCheckOnSave);
[1413]3476 }
3477 }
[1379]3478 }
[1381]3479
3480 public int getTabIndex() {
[1413]3481 if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.TAB_INDEX_STR)) {
[1414]3482 return ((Text) this.getPrimary()).getTabIndex();
[1410]3483 } else {
3484 return this._tabIndex;
3485 }
[1381]3486 }
3487
3488 public void setTabIndex(int index) {
3489 this._tabIndex = index;
[1413]3490
3491 if (isSurrogate()) {
3492 surrogatePropertyInheritance.put(DefaultFrameWriter.TAB_INDEX_STR, false);
[1414]3493 Item primary = getPrimary();
[1413]3494 if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.TAB_INDEX_STR)) {
[1415]3495 EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave);
3496 primary.primaryPropertyEncryption.put(DefaultFrameWriter.TAB_INDEX_STR, inheritanceCheckOnSave);
[1413]3497 }
3498 }
[1381]3499 }
[1382]3500
3501 public Text getTabNext() {
3502 if (this._tabIndex >= 0) {
3503 Collection<Text> textItems = this.getParent().getTextItems();
3504 Text ret = null;
3505
3506 for (Text t: textItems) {
3507 if (t._tabIndex > this._tabIndex) {
3508 if (ret == null) {
3509 ret = t;
3510 } else if (t._tabIndex < ret._tabIndex) {
3511 ret = t;
3512 }
3513 }
3514 }
3515
3516 return ret;
3517 } else {
3518 return null;
3519 }
3520 }
3521
3522 public Text getTabPrevious() {
3523 if (this._tabIndex >= 0) {
3524 Collection<Text> textItems = this.getParent().getTextItems();
3525 Text ret = null;
3526
3527 for (Text t: textItems) {
3528 if (t._tabIndex < this._tabIndex) {
3529 if (ret == null) {
3530 ret = t;
3531 } else if (t._tabIndex > ret._tabIndex) {
3532 ret = t;
3533 }
3534 }
3535 }
3536
3537 return ret;
3538 } else {
3539 return null;
3540 }
3541 }
[4]3542}
Note: See TracBrowser for help on using the repository browser.