/** * Text.java * Copyright (C) 2010 New Zealand Digital Library, http://expeditee.org * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.expeditee.items; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.StringTokenizer; import java.util.stream.Stream; import org.expeditee.core.Colour; import org.expeditee.core.Dimension; import org.expeditee.core.Fill; import org.expeditee.core.Font; import org.expeditee.core.GradientFill; import org.expeditee.core.Point; import org.expeditee.core.Range; import org.expeditee.core.Stroke; import org.expeditee.core.TextHitInfo; import org.expeditee.core.TextLayout; import org.expeditee.core.bounds.AxisAlignedBoxBounds; import org.expeditee.core.bounds.PolygonBounds; import org.expeditee.encryption.items.surrogates.EncryptionDetail; import org.expeditee.gio.EcosystemManager; import org.expeditee.gio.GraphicsManager; import org.expeditee.gio.gesture.StandardGestureActions; import org.expeditee.gui.AttributeValuePair; import org.expeditee.gui.DisplayController; import org.expeditee.gui.Frame; import org.expeditee.gui.FrameIO; import org.expeditee.gui.FrameUtils; import org.expeditee.gui.FreeItems; import org.expeditee.gui.MessageBay; import org.expeditee.io.DefaultFrameWriter; import org.expeditee.items.MagneticConstraint.MagneticConstraints; import org.expeditee.math.ExpediteeJEP; import org.expeditee.settings.experimental.ExperimentalFeatures; import org.expeditee.stats.Formatter; import org.nfunk.jep.Node; /** * Represents text displayed on the screen, which may include multiple lines. * All standard text properties can be set (font, style, size). * * @author jdm18 * */ public class Text extends Item { private static final int ADJUST_WIDTH_THRESHOLD = 200; public static final char DELETE_CHARACTER = 0x7F; public static final char BACKSPACE_CHARACTER = '\b'; public static final char TAB_CHARACTER = '\t'; public static final char ESC_CHARACTER = 0x1B; public static String LINE_SEPARATOR = System.getProperty("line.separator"); // public static char[] BULLETS = { '\u2219', '\u2218', '\u2217' }; public static char[] BULLETS = { '\u25AA', '\u25AB', '\u2217' }; private static char DEFAULT_BULLET = BULLETS[2]; private static String DEFAULT_BULLET_STRING = DEFAULT_BULLET + " "; private String[] _processedText = null; public String[] getProcessedText() { return _processedText; } public void setProcessedText(String[] tokens) { _processedText = tokens; } public static final String FRAME_NAME_SEPARATOR = " on frame "; /** The default font used to display text items if no font is specified. */ public static final String DEFAULT_FONT = "Serif-Plain-18"; public static final Colour DEFAULT_COLOR = Colour.BLACK; public static final int MINIMUM_RANGED_CHARS = 2; public static final int NONE = 0; public static final int UP = 1; public static final int DOWN = 2; public static final int LEFT = 3; public static final int RIGHT = 4; public static final int HOME = 5; public static final int LINE_HOME = 9; public static final int LINE_END = 10; public static final int END = 6; public static final int PAGE_DOWN = 7; public static final int PAGE_UP = 8; /** * Set the width to be IMPLICIT, but as wide as possible, a negative width value * is one that is implicitly set by the system... a positive value is one * explicitly set by the user. */ /** * The maximum allowable width of the Text item. Actual width may be less than * this value, subject to text wrapping. Negative values indicate the width was * implicitly set by the system, positive values indicate explicit setting by * the user. Initially set to be as wide as possible. */ private Integer _width = -Integer.MAX_VALUE; private Integer _minWidth = -Integer.MAX_VALUE; private boolean _singleLine = false; private Justification _justification = Justification.left; private float _spacing = -1; private int _word_spacing = -1; private float _initial_spacing = 0; private float _letter_spacing = 0; // used during ranging out private int _selectionStart = -1; private int _selectionEnd = -1; private int _tabIndex = -1; /** Keeps track of the last Text item selected. */ private static Text _lastSelected = null; // Range selection colours /** Colour of selected range when for selecting text. */ public static final Colour RANGE_SELECT_COLOUR = Colour.FromRGB255(255, 160, 160); /** Colour of selected range when for cutting text. */ public static final Colour RANGE_CUT_COLOUR = Colour.FromRGB255(160, 255, 160); /** Colour of selected range when for copying text. */ public static final Colour RANGE_COPY_COLOUR = Colour.FromRGB255(160, 160, 255); /** Colour of selected range when for deleting text. */ public static final Colour RANGE_DELETE_COLOUR = Colour.FromRGB255(235, 235, 140); /** The colour to draw range selections in. */ private Colour _selectionColour = RANGE_SELECT_COLOUR; // whether autowrap is on/off for this item protected boolean _autoWrap = false; // text is broken up into lines protected StringBuffer _text = new StringBuffer(); // place holder text to use if _text is empty. protected StringBuffer _placeholder = new StringBuffer(); private List _textLayouts = new LinkedList(); private List _maskTextLayouts = new LinkedList(); private List _placeholderTextLayouts = new LinkedList(); // The font to display this text in private Font _font; // The optional mask character to us in place of the text's content. private Integer _mask = null; protected static void InitFontFamily(File fontFamilyDir) { File[] fontFiles = fontFamilyDir.listFiles(); for (File fontFile : fontFiles) { String ext = ""; String fileName = fontFile.getName().toLowerCase(); int i = fileName.lastIndexOf('.'); int p = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\')); if (i > p) { ext = fileName.substring(i + 1); } if (ext.equals("ttf")) { try { Font font = EcosystemManager.getFontManager().registerFontFile(fontFile); if (font != null) { String font_family = font.getFamilyName(); if (!FONT_WHEEL_ADDITIONAL_LOOKUP.containsKey(font_family)) { if (FONT_WHEEL_ADDITIONAL_LOOKUP.size() > 0) { System.out.print(", "); } System.out.print("'" + font_family + "'"); FONT_WHEEL_ADDITIONAL_LOOKUP.put(font_family, font); /* * int cdut = font.canDisplayUpTo("09AZaz"); if (cdut >= 0) { // Some problem * has occured (should return -1 to show all chars possible) * * System.out.println(" [Non-ASCII font]"); } */ System.out.flush(); } //System.out.print("'" + font_family + "'"); } else { System.err.println("Error: Failed to add custom True-Type Font file: " + fontFile); } } catch (Exception e) { System.err.println("Failed to load custon font file: " + fontFile); } } } } public static void InitFonts() { File fontDirectory = new File(FrameIO.FONT_PATH); if (fontDirectory != null) { File[] fontFamilyDirs = fontDirectory.listFiles(); if (fontFamilyDirs != null) { if (fontFamilyDirs.length > 0) { System.out.println("Loading custom fonts:"); } for (File fontFamilyDir : fontFamilyDirs) { if (fontFamilyDir.isDirectory()) { InitFontFamily(fontFamilyDir); } } } } } /** * Similar to parseArgs() in InteractiveWidget. Code based on routine used in * Apache Ant: org.apache.tools.ant.types.Commandline::translateCommandline() * * @param toProcess * the command line to process. * @return the command line broken into strings. An empty or null toProcess * parameter results in a zero sized array. */ public static String[] parseArgsApache(String toProcess) { if (toProcess == null || toProcess.length() == 0) { // no command? no string return new String[0]; } // parse with a simple finite state machine final int normal = 0; final int inQuote = 1; final int inDoubleQuote = 2; int state = normal; final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true); final ArrayList result = new ArrayList(); final StringBuilder current = new StringBuilder(); boolean lastTokenHasBeenQuoted = false; while (tok.hasMoreTokens()) { String nextTok = tok.nextToken(); switch (state) { case inQuote: if ("\'".equals(nextTok)) { lastTokenHasBeenQuoted = true; state = normal; } else { current.append(nextTok); } break; case inDoubleQuote: if ("\"".equals(nextTok)) { lastTokenHasBeenQuoted = true; state = normal; } else { current.append(nextTok); } break; default: if ("\'".equals(nextTok)) { state = inQuote; } else if ("\"".equals(nextTok)) { state = inDoubleQuote; } else if (" ".equals(nextTok)) { if (lastTokenHasBeenQuoted || current.length() != 0) { result.add(current.toString()); current.setLength(0); } } else { current.append(nextTok); } lastTokenHasBeenQuoted = false; break; } } if (lastTokenHasBeenQuoted || current.length() != 0) { result.add(current.toString()); } if (state == inQuote || state == inDoubleQuote) { System.err.println("Error: Unbalanced quotes -- failed to parse '" + toProcess + "'"); return null; } return result.toArray(new String[result.size()]); } /** * Creates a new Text Item with the given ID and text. * * @param id * The id of this item * @param text * The text to use in this item */ public Text(int id, String text) { super(); _text.append(text); rebuild(false); setID(id); } /** * Creates a text item which is not added to the frame. * * @param text */ public Text(String text) { super(); _text.append(text); rebuild(false); setID(-1); } /** * Creates a new Text Item with the given ID * * @param id * The ID to of this item */ public Text(int id) { super(); setID(id); } public Text(int i, String string, Colour foreground, Colour background) { this(i, string); this.setColor(foreground); this.setBackgroundColor(background); } /** * @param width * The maximum width of this item when justification is applied to * it. */ @Override public void setWidth(Integer width) { invalidateAll(); if (width == null) { setJustification(Justification.left); setRightMargin(DisplayController.getFramePaintAreaWidth(), false); return; } if (width == 0) { System.err.println("Width of Zero: " + getText()); } _width = width; rebuild(true); invalidateAll(); if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.WIDTH_TO_SAVE_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.WIDTH_TO_SAVE_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.WIDTH_TO_SAVE_STR, inheritanceCheckOnSave); } } } @Override public void setMinWidth(final Integer width) { invalidateAll(); if (width == null) { setJustification(Justification.left); setRightMargin(DisplayController.getFramePaintAreaWidth(), false); return; } _minWidth = width; rebuild(true); invalidateAll(); if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.MIN_WIDTH_TO_SAVE_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.MIN_WIDTH_TO_SAVE_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.MIN_WIDTH_TO_SAVE_STR, inheritanceCheckOnSave); } } } /** * <<<<<<< .mine Returns the maximum width of this Text item when justification * is used. If the width is negative, it means no explicit width has been set * ||||||| .r1094 Returns the maximum width of this Text item when justifcation * is used. If the width is negative, it means no explicit width has been set * ======= Returns the maximum width of this Text item when justifcation is * used. If the width is negative, it means no explicit width has been set * >>>>>>> .r1100 * * @return The maximum width of this Text item when justification is used */ @Override public Integer getWidth() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.WIDTH_TO_SAVE_STR)) { return this.getPrimary().getWidth(); } else { if (_width == null || _width <= 0) return null; return _width; } } public Integer getAbsoluteWidth() { if (_width == null || _width == Integer.MIN_VALUE) { // When absoluting Integer.MIN_VALUE, the java API is defined to give you back Integer.MIN_VALUE!!?! // This is because of the asymmetry of two's complement integer representation. // We would prefer to use Integer.MAX_VALUE in this circumstance. return Integer.MAX_VALUE; } return Math.abs(_width); } public Integer getMinWidth() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.MIN_WIDTH_TO_SAVE_STR)) { return this.getPrimary().getMinWidth(); } else { if (_minWidth == null || _minWidth <= 0) return null; return _minWidth; } } public Integer getAbsoluteMinWidth() { if (_minWidth == null) { return Integer.MAX_VALUE; } return Math.abs(_minWidth); } @Override public Colour getHighlightColor() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.HIGHLIGHT_STR)) { return this.getPrimary().getHighlightColor(); } else { return _highlightColour; } } /** * Sets the justification of this Text item. The given integer should correspond * to one of the JUSTIFICATION constants defined in Item * * @param just * The justification to apply to this Text item */ public void setJustification(Justification just) { invalidateAll(); // Only justification left works with 0 width // if (just != null && just != Justification.left && !hasWidth()) { // // TODO Tighten this up so it subtracts the margin widths // setWidth(getBoundsWidth()); // } _justification = just; rebuild(true); invalidateAll(); if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.JUSTIFICATION_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.JUSTIFICATION_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.JUSTIFICATION_STR, inheritanceCheckOnSave); } } } /** * Returns the current justification of this Text item. The default value left * justification. * * TODO: Why return null when justification is set to left? cts16 * * @return The justification of this Text item */ public Justification getJustification() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.JUSTIFICATION_STR)) { return ((Text) this.getPrimary()).getJustification(); } else { if (_justification == null || _justification.equals(Justification.left)) { return null; } return _justification; } } /** * Gets the distance from the left of the bounding box that the given layout * should be shifted to justify it. * * @param layout * The line of text to calculate the justification offset for. * * @return The distance to shift the line of text by. */ private int getJustOffset(TextLayout layout) { if (getJustification() == Justification.center) { return (int) ((getAbsoluteWidth() - layout.getAdvance()) / 2); } else if (getJustification() == Justification.right) { return (int) (getAbsoluteWidth() - layout.getAdvance()); } return 0; } /** * Sets the text displayed on the screen to the given String. It does not reset * the formula, attributeValuePair or other cached values. * * @param text * The String to display on the screen when drawing this Item. */ @Override public void setText(String text) { setText(text, false); } public void setText(String text, Boolean clearCache) { // if (_text != null && text.length() < _text.length()) invalidateAll(); _text = new StringBuffer(text); /* * Always clearingCach remove formulas when moving in and out of XRay mode */ if (clearCache) { clearCache(); } rebuild(true); invalidateAll(); } public void setTextList(List text) { if (text == null || text.size() <= 0) { return; } invalidateAll(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < text.size(); i++) { sb.append(text.get(i)).append('\n'); } if (sb.length() > 0) { sb.deleteCharAt(sb.length() - 1); } setText(sb.toString()); rebuild(true); invalidateAll(); } public void setAttributeValue(String value) { AttributeValuePair avp = new AttributeValuePair(getText(), false); avp.setValue(value); setText(avp.toString()); } /** * Inserts the given String at the start of the first line of this Text Item. * * @param text * The String to insert. */ public void prependText(String text) { invalidateAll(); _text.insert(0, text); rebuild(false); invalidateAll(); } /** * If the first line of text starts with the given String, then it is removed * otherwise no action is taken. * * @param text * The String to remove from the first line of Text */ public void removeText(String text) { if (_text.length() > 0 && _text.indexOf(text) == 0) { invalidateAll(); _text.delete(0, text.length()); invalidateAll(); } } public void removeEndText(String textToRemove) { int length = _text.length(); if (length > 0) { int pos = _text.indexOf(textToRemove); int textToRemoveLength = textToRemove.length(); if (pos + textToRemoveLength == length) { // Need the invalidate all for dateStamp toggling invalidateAll(); _text.delete(pos, length); invalidateAll(); } } } /** * Appends the given String to any text already present in this Item * * @param text * The String to append. */ public void appendText(String text) { invalidateAll(); _text.append(text); rebuild(false); invalidateAll(); } /** * Used by the frame reader to construct multi-line text items. It must run * quickly, so that the system still responds well for long text items. * * @param text */ public void appendLine(String text) { if (text == null) { text = ""; } if (_text.length() > 0) { _text.append('\n'); } _text.append(text); rebuild(true); if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.TEXT_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.TEXT_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.TEXT_STR, inheritanceCheckOnSave); } } } /** * Tests if the first line of this Text starts with the given String. * * @param text * The prefix to check for * @return True if the first line starts with the given String, False otherwise. */ public boolean startsWith(String text) { return startsWith(text, true); } public boolean startsWith(String text, boolean ignoreCase) { if (text == null || _text == null || _text.length() < 1) { return false; } if (ignoreCase) { return _text.toString().toLowerCase().startsWith(text.toLowerCase()); } else { return _text.indexOf(text) == 0; } } /** * Inserts a character into the Text of this Item. * * @param ch * The character insert. * @param mouseX * The X position to insert the Strings at. * @param mouseY * The Y position to insert the Strings at. */ public Point insertChar(char ch, float mouseX, float mouseY) { if (ch != '\t') { return insertText("" + ch, mouseX, mouseY); } return insertText(" " + ch, mouseX, mouseY); } /** * @param index * @return */ private char getNextBullet(char bullet) { for (int i = 0; i < BULLETS.length - 1; i++) { if (BULLETS[i] == bullet) { return BULLETS[i + 1]; } } return BULLETS[0]; } private char getPreviousBullet(char bullet) { for (int i = 1; i < BULLETS.length; i++) { if (BULLETS[i] == bullet) { return BULLETS[i - 1]; } } return BULLETS[BULLETS.length - 1]; } public Point getLineEndPosition(float mouseY) { return getEdgePosition(getLinePosition(mouseY), false); } public Point getLineStartPosition(float mouseY) { return getEdgePosition(getLinePosition(mouseY), true); } public Point getParagraphEndPosition() { return getEdgePosition(getTextLayouts().size() - 1, false); } public Point getParagraphStartPosition() { return getEdgePosition(0, true); } private Point getEdgePosition(int line, boolean start) { // if there is no text yet, or the line is invalid if (_text == null || _text.length() == 0 || line < 0 || line > getTextLayouts().size() - 1) { return new Point(getX(), getY()); } TextLayout last = getTextLayouts().get(line); TextHitInfo hit; if (start) { hit = last.getNextLeftHit(1); } else { hit = last.getNextRightHit(last.getCharacterCount() - 1); } // move the cursor to the new location float[] caret = last.getCaretInfo(hit); float y = getLineDrop(last) * line; float x = getX() + caret[0] + getJustOffset(last); x = Math.min(x, (getX() - Item.MARGIN_RIGHT - (2 * getGravity()) + getBoundsWidth())); return new Point((int) x, (int) (getY() + y + caret[1])); } public void setSelectionStart(Point p) { setSelectionStart(p.getX(), p.getY()); } public void setSelectionStart(float mouseX, float mouseY) { // determine what line is being pointed to int line = getLinePosition(mouseY); // get the character being pointed to TextHitInfo hit = getCharPosition(line, mouseX); _selectionStart = hit.getInsertionIndex() + getTextLayouts().get(line).getStartCharIndex(); // Clear the last selected updateLastSelected(); invalidateAll(); } public void setSelectionEnd(Point p) { setSelectionEnd(p.getX(), p.getY()); } public void setSelectionEnd(float mouseX, float mouseY) { // determine what line is being pointed to int line = getLinePosition(mouseY); // get the character being pointed to TextHitInfo hit = getCharPosition(line, mouseX); _selectionEnd = hit.getInsertionIndex() + getTextLayouts().get(line).getStartCharIndex(); // Clear the last selected updateLastSelected(); invalidateAll(); } public void clearSelection() { _selectionStart = -1; _selectionEnd = -1; invalidateAll(); } public void clearSelectionEnd() { _selectionEnd = -1; invalidateAll(); } /** Makes sure only one text has a selection at a time. */ public void updateLastSelected() { if (_lastSelected != this) { if (_lastSelected != null) { _lastSelected.clearSelection(); } _lastSelected = this; } } public String copySelectedText() { if (_selectionStart < 0 || _selectionEnd < 0) { return null; } else if (_selectionEnd > _text.length()) { _selectionEnd = _text.length(); } return _text.substring(Math.min(_selectionStart, _selectionEnd), Math.max(_selectionStart, _selectionEnd)); } public String cutSelectedText() { return replaceSelectedText(""); } public String replaceSelectedText(String newText) { if (_selectionStart < 0 || _selectionEnd < 0) { return null; } invalidateAll(); if (_selectionEnd > _text.length()) { _selectionEnd = _text.length(); } int left = Math.min(_selectionStart, _selectionEnd); int right = Math.max(_selectionStart, _selectionEnd); // Trim the text to remove new lines on the beginning and end of the // string if (_text.charAt(left) == '\n') { // if the entire line is being removed then remove one of the new // lines, the first case checks if the last line is being removed if (right >= _text.length() || _text.charAt(right) == '\n') { _text.deleteCharAt(left); right--; } else { left++; } } // New lines are always at the start of the line for now... // if(_text.charAt(right - 1) == '\n' && left < right){ // right--; // } String s = _text.substring(left, right); _text.delete(left, right); _text.insert(left, newText); rebuild(true); clearCache(); invalidateAll(); return s; } public int getSelectionSize() { if (_selectionEnd < 0 || _selectionStart < 0) { return 0; } // System.out.println(_selectionStart + ":" + _selectionEnd); return Math.abs(_selectionEnd - _selectionStart); } /** * Inserts the given String into the Text at the position given by the mouseX * and mouseY coordinates * * @param text * The String to insert into this Text. * @param mouseX * The X position to insert the String * @param mouseY * The Y position to insert the String * @return The new location that the mouse cursor should be moved to */ public Point insertText(String text, float mouseX, float mouseY) { final Point newPos = insertText(text, mouseX, mouseY, -1); return newPos; } public Point insertText(final String text, final float mouseX, final float mouseY, int insertPos) { TextHitInfo hit; TextLayout currentLayout = null; int lineIndex; invalidateAll(); // if it is a empty string then do not move the mouse if (text == null || text.length() == 0) { return new Point((int) mouseX, (int) mouseY); } // if there is no text yet then simply append parameter and rebuild before moving on. // rebuild re-initialises the TextLayouts // calculate were we are in the content (hit, lineIndex, currentLayout) if (_text == null || _text.length() == 0) { _text = new StringBuffer().append(text); rebuild(true); assert (getTextLayouts().size() == 1); currentLayout = getTextLayouts().get(0); hit = currentLayout.getNextRightHit(0); lineIndex = 0; } // otherwise we are inserting text and calculating the index into the content that we are at else { clearCache(); // determine what line is being pointed to lineIndex = getLinePosition(mouseY); // get the character being pointed to hit = getCharPosition(lineIndex, mouseX); int insertionIndex = hit.getInsertionIndex() + getTextLayouts().get(lineIndex).getStartCharIndex(); if (lineIndex > 0 && hit.getInsertionIndex() == 0) { // Only move forward a char if the line begins with a hard line // break... not a soft line break if (_text.charAt(insertionIndex) == '\n') { insertionIndex++; } } if (insertPos < 0) { insertPos = insertionIndex; } // if this is a backspace key if (text.charAt(0) == '\b') { if (hasSelection()) { insertionIndex = deleteSelection(insertionIndex); } else if (insertPos > 0) { deleteChar(insertPos - 1); if (insertionIndex > 0) { insertionIndex--; } } // if this is a delete key } else if (text.charAt(0) == (char) 0x7F) { if (hasSelection()) { insertionIndex = deleteSelection(insertionIndex); } else if (insertPos < _text.length()) { deleteChar(insertPos); } // this is a tab } else if (text.charAt(0) == '\t') { // Text length greater than 1 signals a backwards tab if (text.length() > 1) { // Find the first non space char to see if its a bullet int index = 0; for (index = 0; index < _text.length(); index++) { if (!Character.isSpaceChar(_text.charAt(index))) { break; } } // Check if there is a space after the bullet if (index < _text.length() - 1 && _text.charAt(index + 1) == ' ') { // Change the bullet _text.setCharAt(index, getPreviousBullet(_text.charAt(index))); } // Remove the spacing at the start for (int i = 0; i < TAB_STRING.length(); i++) { if (_text.length() > 0 && Character.isSpaceChar(_text.charAt(0))) { deleteChar(0); insertionIndex--; } else { break; } } } else { // / Find the first non space char to see if its a bullet int index = 0; for (index = 0; index < _text.length(); index++) { if (!Character.isSpaceChar(_text.charAt(index))) { break; } } // Check if there is a space after the bullet if (index < _text.length() - 1 && _text.charAt(index + 1) == ' ') { char nextBullet = getNextBullet(_text.charAt(index)); // Change the bullet _text.setCharAt(index, nextBullet); } // Insert the spacing at the start insertString(TAB_STRING, 0); insertionIndex += TAB_STRING.length(); } // this is a normal insert } else { insertString(text, insertPos); insertionIndex += text.length(); } if (_text.length() == 0) { rebuild(false); return new Point((int) this._x, (int) this._y); } int newLine = lineIndex; // if a rebuild is required rebuild(true, false); // determine the new position the cursor should have for (int i = 0; i < getTextLayouts().size(); i++) { if (getTextLayouts().get(i).getEndCharIndex() + 1 >= insertionIndex) { newLine = i; break; } } currentLayout = getTextLayouts().get(newLine); insertionIndex -= currentLayout.getStartCharIndex(); if (newLine == lineIndex) { if (insertionIndex > 0) { hit = currentLayout.getNextRightHit(insertionIndex - 1); } else { hit = currentLayout.getNextLeftHit(1); } } else if (newLine < lineIndex) { hit = currentLayout.getNextRightHit(insertionIndex - 1); } else { hit = currentLayout.getNextRightHit(insertionIndex - 1); } lineIndex = newLine; } // If we have no mask then.... // move the cursor to the new location float[] caret = currentLayout.getCaretInfo(hit); float y = getLineDrop(currentLayout) * lineIndex; y = getY() + y + caret[1]; float x = getX() + caret[0] + getJustOffset(currentLayout); x = Math.min(x, (getX() - Item.MARGIN_RIGHT - (2 * getGravity()) + getBoundsWidth())); invalidateAll(); final Point newCursor = new Point(Math.round(x), Math.round(y)); return newCursor; } /** * */ private void clearCache() { _attributeValuePair = null; setProcessedText(null); setFormula(null); } /** * @param pos * @return */ private int deleteSelection(int pos) { int selectionLength = getSelectionSize(); cutSelectedText(); clearSelection(); pos -= selectionLength; return pos; } public Point moveCursor(int direction, float mouseX, float mouseY, boolean setSelection, boolean wholeWord) { if (setSelection) { if (!hasSelection()) { setSelectionStart(mouseX, mouseY); } } else { // clearSelection(); } Point resultPos = null; // check for home or end keys switch (direction) { case HOME: resultPos = getParagraphStartPosition(); break; case END: resultPos = getParagraphEndPosition(); break; case LINE_HOME: resultPos = getLineStartPosition(mouseY); break; case LINE_END: resultPos = getLineEndPosition(mouseY); break; default: TextHitInfo hit; TextLayout current; int line; // if there is no text yet if (_text == null || _text.length() == 0) { return new Point((int) mouseX, (int) mouseY); // otherwise, move the cursor } else { // determine the line of text to check line = getLinePosition(mouseY); if (line < 0) { line = getTextLayouts().size() - 1; } // if the cursor is moving up or down, change the line if (direction == UP) { line = Math.max(line - 1, 0); } else if (direction == DOWN) { line = Math.min(line + 1, getTextLayouts().size() - 1); } hit = getCharPosition(line, mouseX); if (direction == LEFT) { if (hit.getInsertionIndex() > 0) { char prevChar = ' '; do { hit = getTextLayouts().get(line).getNextLeftHit(hit); // Stop if at the start of the line if (hit.getInsertionIndex() == 0) { break; } // Keep going if the char to the left is a // letterOrDigit prevChar = _text .charAt(hit.getInsertionIndex() - 1 + getTextLayouts().get(line).getStartCharIndex()); } while (wholeWord && Character.isLetterOrDigit(prevChar)); // TODO Go to the start of the word instead of before the word char nextChar = _text .charAt(hit.getInsertionIndex() + getTextLayouts().get(line).getStartCharIndex()); // This takes care of hard line break in if (line > 0 && nextChar == '\n') { line--; hit = getTextLayouts().get(line) .getNextRightHit(getTextLayouts().get(line).getCharacterCount() - 1); } // This takes care of soft line breaks. } else if (line > 0) { line--; hit = getTextLayouts().get(line).getNextRightHit(getTextLayouts().get(line).getCharacterCount() - 1); // Skip the spaces at the end of a line with soft linebreak while (hit.getCharIndex() > 0 && _text .charAt(getTextLayouts().get(line).getStartCharIndex() + hit.getCharIndex() - 1) == ' ') { hit = getTextLayouts().get(line).getNextLeftHit(hit); } } } else if (direction == RIGHT) { if (hit.getInsertionIndex() < getTextLayouts().get(line).getCharacterCount()) { hit = getTextLayouts().get(line).getNextRightHit(hit); // Skip whole word if needs be while (wholeWord && hit.getCharIndex() > 0 && hit.getCharIndex() < getTextLayouts().get(line).getCharacterCount() && Character.isLetterOrDigit(_text .charAt(getTextLayouts().get(line).getStartCharIndex() + hit.getCharIndex() - 1))) { hit = getTextLayouts().get(line).getNextRightHit(hit); } } else if (line < getTextLayouts().size() - 1) { line++; hit = getTextLayouts().get(line).getNextLeftHit(1); } } current = getTextLayouts().get(line); } // move the cursor to the new location float[] caret = current.getCaretInfo(hit); float y = getLineDrop(current) * line; resultPos = new Point((int) (getX() + caret[0] + getJustOffset(current)), (int) (getY() + y + caret[1])); break; } if (setSelection) { setSelectionEnd(resultPos.getX(), resultPos.getY()); } return resultPos; } /** * Iterates through the given line string and returns the position of the * character being pointed at by the mouse. * * @param line * The index of the _text array of the String to be searched. * @param mouseX * The X coordinate of the mouse * @return The position in the string of the character being pointed at. */ public TextHitInfo getCharPosition(final int line, float mouseX) { if (line < 0 || line >= getTextLayouts().size()) { return null; } final TextLayout layout = getTextLayouts().get(line); mouseX += getOffset().getX(); mouseX -= getJustOffset(layout); return layout.hitTestChar(mouseX - getX(), 0); } /** * Gets the index into the _textLayout list which corresponds to * the line covered by the given mouseY position. * * @param mouseY * The y-coordinate to test for line coverage. * * @return The line which occupies the given y-coordinate, or the last line if * none do. */ public int getLinePosition(float mouseY) { mouseY += getOffset().getY(); float y = getY(); for (TextLayout text : getTextLayouts()) { // calculate X to ensure it is in the shape AxisAlignedBoxBounds bounds = text.getLogicalHighlightShape(0, text.getCharacterCount()); if (bounds.getWidth() < 1) { bounds.getSize().width = 10; } double x = bounds.getCentreX(); if (bounds.contains((int) x, (int) (mouseY - y))) { return getTextLayouts().indexOf(text); } // check if the cursor is between lines if (mouseY - y < bounds.getMinY()) { return Math.max(0, getTextLayouts().indexOf(text) - 1); } y += getLineDrop(text); } return getTextLayouts().size() - 1; } /** * Sets the Font that this text will be displayed with on the screen. * * @param font * The Font to display the Text of this Item in. */ public void setFont(Font font) { invalidateAll(); _font = font; rebuild(false); invalidateAll(); if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.FONT_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.FONT_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.FONT_STR, inheritanceCheckOnSave); } } } /** * Gets the font of this text item. * * @return The Font assigned to this text item, or null if none is assigned. */ public Font getFont() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.FONT_STR)) { return ((Text) this.getPrimary()).getFont(); } else { return _font; } } /** * Gets the font that should be used to paint this text item during drawing. * * @return The font to paint the text item with. */ public Font getPaintFont() { Font f = getFont(); if (f == null) { _font = EcosystemManager.getFontManager().getDefaultFont().clone(); f = _font; } return f; } public String getFamily() { return getPaintFont().getFamilyName(); } public void setFamily(String newFamily) { setFont(new Font(newFamily, getFontStyle(), Math.round(getSize()))); setLetterSpacing(this._letter_spacing); } public Font.Style getFontStyle() { Font f = getPaintFont(); return f.getStyle(); } public static final String MONOSPACED_FONT = "monospaced"; public static final String[] FONT_WHEEL = { "sansserif", "monospaced", "serif", "dialog", "dialoginput" }; public static final char[] FONT_CHARS = { 's', 'm', 't', 'd', 'i' }; // A hashtable to store the font family names that are loaded in through the TTF // files // provided in the 'assets' folder public static HashMap FONT_WHEEL_ADDITIONAL_LOOKUP = new HashMap<>(); private static final int NEARBY_GRAVITY = 2; public static final int MINIMUM_FONT_SIZE = 6; public void toggleFontFamily() { String fontFamily = getFamily().toLowerCase(); // set it to the first font by default setFamily(FONT_WHEEL[0]); for (int i = 0; i < FONT_WHEEL.length - 3; i++) { if (fontFamily.equals(FONT_WHEEL[i])) { setFamily(FONT_WHEEL[i + 1]); break; } } } public void toggleFontStyle() { invalidateAll(); Font currentFont = getPaintFont(); Font.Style currentStyle = currentFont.getStyle(); Font.Style newStyle = Font.Style.PLAIN; switch (currentStyle) { case PLAIN: newStyle = Font.Style.BOLD; break; case BOLD: newStyle = Font.Style.ITALIC; break; case ITALIC: newStyle = Font.Style.BOLD_ITALIC; break; default: newStyle = Font.Style.PLAIN; break; } setFont(new Font(currentFont.getFamilyName(), newStyle, currentFont.getSize())); rebuild(true); invalidateAll(); } public void toggleBold() { invalidateAll(); Font currentFont = getPaintFont(); currentFont.toggleBold(); // setFont(currentFont); rebuild(true); invalidateAll(); } public void toggleItalics() { invalidateAll(); Font currentFont = getPaintFont(); currentFont.toggleItalic(); // setFont(currentFont); rebuild(true); invalidateAll(); } public void setFontStyle(String newFace) { Font currentFont = getPaintFont(); if (newFace == null || newFace.trim().length() == 0) { currentFont.setStyle(Font.Style.PLAIN); // setFont(currentFont); return; } newFace = newFace.toLowerCase().trim(); if (newFace.equals("plain") || newFace.equals("p")) { currentFont.setStyle(Font.Style.PLAIN); } else if (newFace.equals("bold") || newFace.equals("b")) { currentFont.setStyle(Font.Style.BOLD); } else if (newFace.equals("italic") || newFace.equals("i")) { currentFont.setStyle(Font.Style.ITALIC); } else if (newFace.equals("bolditalic") || newFace.equals("italicbold") || newFace.equals("bi") || newFace.equals("ib")) { currentFont.setStyle(Font.Style.BOLD_ITALIC); } // setFont(currentFont); } /** * Returns a String array of this Text object's text, split up into separate * lines. * * @return The String array with one element per line of text in this Item. */ public List getTextList() { if (_text == null) { return null; } try { List list = new LinkedList(); // Rebuilding prevents errors when displaying frame bitmaps if (getTextLayouts().size() == 0) { rebuild(false); } for (TextLayout layout : getTextLayouts()) { String text = layout.getLine().replaceAll("\n", ""); if (!text.equals("")) { list.add(text); } } return list; } catch (Exception e) { System.out.println("Exception in Text::getTextList::message is: " + e.getMessage()); return null; } } @Override public String getText() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.TEXT_STR)) { return this.getPrimary().getText(); } else { return _text.toString(); } } /** * Returns the first line of text in this Text Item * * @return The first line of Text */ public String getFirstLine() { if (_text == null || _text.length() == 0) { return null; } // start at the first non-newLine char int index = 0; while (_text.charAt(index) == '\n') { index++; } int nextNewLine = _text.indexOf("\n", index); /* If there are no more newLines return the remaining text */ if (nextNewLine < 0) { return _text.substring(index); } return _text.substring(index, nextNewLine); } /** * Sets the inter-line spacing (in pixels) of this text. * * @param spacing * The number of pixels to allow between each line */ public void setSpacing(float spacing) { _spacing = spacing; invalidateBounds(); if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.SPACING_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.SPACING_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.SPACING_STR, inheritanceCheckOnSave); } } } /** * Returns the inter-line spacing (in pixels) of this Text. * * @return The spacing (inter-line) in pixels of this Text. */ public float getSpacing() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.SPACING_STR)) { return ((Text) this.getPrimary()).getSpacing(); } else { return _spacing; } } /** * Gets the y-distance that should be advanced between this layout and the next. * * @param layout * The TextLayout to calculate line-drop for. * * @return The distance to advance in the y-direction before the next line. */ protected float getLineDrop(TextLayout layout) { if (getSpacing() < 0) { return layout.getAscent() + layout.getDescent() + layout.getLeading(); } return layout.getAscent() + layout.getDescent() + getSpacing(); } public void setWordSpacing(int spacing) { _word_spacing = spacing; if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.WORD_SPACING_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.WORD_SPACING_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.WORD_SPACING_STR, inheritanceCheckOnSave); } } } public int getWordSpacing() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.WORD_SPACING_STR)) { return ((Text) this.getPrimary()).getWordSpacing(); } else { return _word_spacing; } } /** * Sets the spacing (proportional to the font size) between letters * * @param spacing * Additional spacing to add between letters. See * {@link java.awt.font.TextAttribute#TRACKING} */ public void setLetterSpacing(float spacing) { _letter_spacing = spacing; Font currentFont = getPaintFont(); currentFont.setSpacing(spacing); // setFont(currentFont); if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.LETTER_SPACING_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.LETTER_SPACING_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.LETTER_SPACING_STR, inheritanceCheckOnSave); } } } /** * @return The spacing (proportional to the font size) between letters. See * {@link java.awt.font.TextAttribute#TRACKING} */ public float getLetterSpacing() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.LETTER_SPACING_STR)) { return ((Text) this.getPrimary()).getLetterSpacing(); } else { return _letter_spacing; } } public void setInitialSpacing(float spacing) { _initial_spacing = spacing; if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.INITIAL_SPACING_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.INITIAL_SPACING_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.INITIAL_SPACING_STR, inheritanceCheckOnSave); } } } public float getInitialSpacing() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.INITIAL_SPACING_STR)) { return ((Text) this.getPrimary()).getInitialSpacing(); } else { return _initial_spacing; } } // @Override /* * public boolean intersectsOLD(Polygon p) { if (super.intersects(p)) { float * textY = getY(); * * for (TextLayout text : _textLayouts) { // check left and right of each box * Rectangle2D textOutline = text.getLogicalHighlightShape(0, * text.getCharacterCount()).getBounds2D(); * textOutline.setRect(textOutline.getX() + getX() - 1, textOutline.getY() + * textY - 1, textOutline.getWidth() + 2, textOutline.getHeight() + 2); if * (p.intersects(textOutline)) return true; textY += getLineDrop(text); } } * return false; } */ // The following version of intersect uses a tighter definition for the text, // based on @Override public boolean intersects(PolygonBounds p) { if (super.intersects(p)) { // float textY = getY(); for (TextLayout text : getTextLayouts()) { AxisAlignedBoxBounds text_pixel_bounds_rect = getPixelBounds(text); if (p.intersects(text_pixel_bounds_rect)) { return true; } } } return false; } @Override public boolean contains(Point mousePosition) { return contains(mousePosition.getX(), mousePosition.getY(), getGravity() * NEARBY_GRAVITY); } public boolean contains(int mouseX, int mouseY) { return contains(new Point(mouseX, mouseY)); } public boolean contains(int mouseX, int mouseY, int gravity) { mouseX += getOffset().getX(); mouseY += getOffset().getY(); float textY = getY(); float textX = getX(); AxisAlignedBoxBounds outline = getBoundingBox(); if (outline == null) { return false; } // Check if its outside the top and left and bottom bounds if (outline.getMinX() - mouseX > gravity || outline.getMinY() - mouseY > gravity || mouseY - (outline.getMinY() + outline.getHeight()) > gravity || mouseX - (outline.getMinX() + outline.getWidth()) > gravity) { return false; } if (this.getMinWidth() != null && outline.contains(mouseX, mouseY)) { return true; } for (TextLayout text : getTextLayouts()) { // check left and right of each box AxisAlignedBoxBounds textOutline = text.getLogicalHighlightShape(0, text.getCharacterCount()); // check if the cursor is within the top, bottom and within the // gravity of right int justOffset = getJustOffset(text); if (mouseY - textY > textOutline.getMinY() && mouseY - textY < textOutline.getMinY() + textOutline.getHeight() && mouseX - textX - justOffset < textOutline.getWidth() + gravity + Item.MARGIN_RIGHT) { return true; } textY += getLineDrop(text); } return false; } /** * Updates the Polygon (rectangle) that surrounds this Text on the screen. */ @Override public AxisAlignedBoxBounds updateBounds() { boolean isFakeLayout = false; // if there is no text, there is nothing to do if (_text == null) { return null; } // if there is no text layouts and the text has no min width, do nothing if (getTextLayouts() == null || (getTextLayouts().size() < 1 && this.getMinWidth() == null)) { return null; } if (this.getMinWidth() != null && getTextLayouts().size() == 0 && this.getFont() != null) { // could use any text, 'p' is used simply to get some text in correct font. getTextLayouts().add(TextLayout.getManager().layoutStringSimple("p", this.getFont())); isFakeLayout = true; } int preChangeWidth = 0; if (getOldBounds() != null) { preChangeWidth = AxisAlignedBoxBounds.getEnclosing(getOldBounds()).getWidth(); } int minX = Integer.MAX_VALUE; int maxX = Integer.MIN_VALUE + 1; // +1 makes it safe to do math.abs on int minY = Integer.MAX_VALUE; int maxY = Integer.MIN_VALUE + 1; // +1 makes it safe to do math.abs on float y = -1; // Fix concurrency error in ScaleFrameset List tmpTextLayouts; synchronized (getTextLayouts()) { tmpTextLayouts = new LinkedList(getTextLayouts()); } for (int index = 0; index < tmpTextLayouts.size(); index++) { final TextLayout layout = tmpTextLayouts.get(index); AxisAlignedBoxBounds bounds = layout.getLogicalHighlightShape(0, layout.getCharacterCount()); if (y < 0) { y = 0; } else { y += getLineDrop(layout); } int minWidth = getAbsoluteMinWidth(); minX = Math.min(minX, bounds.getMinX()); maxX = minWidth < Integer.MAX_VALUE ? Math.max(minX + minWidth, bounds.getMaxX()) : Math.max(maxX, bounds.getMaxX()); minY = Math.min(minY, (int) (bounds.getMinY() + y)); maxY = Math.max(maxY, (int) (bounds.getMaxY() + y)); } minX -= getLeftMargin(); maxX += Item.MARGIN_RIGHT; // If its justification right or center then DONT limit the width if (getJustification() != null) { maxX = Item.MARGIN_RIGHT + getAbsoluteWidth(); } final int xPos = getX() + minX - getGravity(); final int yPos = getY() + minY - getGravity(); final int width = 2 * getGravity() + maxX - minX; final int height = 2 * getGravity() + maxY - minY; AxisAlignedBoxBounds ret = new AxisAlignedBoxBounds(xPos, yPos, width, height); Dimension polySize = ret.getSize(); if (preChangeWidth != 0 && preChangeWidth != polySize.width) { if (polySize.width > preChangeWidth) { MagneticConstraints.getInstance().textGrown(this, polySize.width - preChangeWidth); } else { MagneticConstraints.getInstance().textShrunk(this, preChangeWidth - polySize.width); } } if (isFakeLayout) { getTextLayouts().remove(0).release(); } return ret; } // TODO it seems like this method has some exponential processing which // makes items copy really slowly when there are lots of lines of text! // This needs to be fixed!! public void rebuild(boolean limitWidth) { rebuild(limitWidth, true); } /** * * @param limitWidth * @param newLinebreakerAlways * true if a new line breaker should always be created. */ private void rebuild(boolean limitWidth, boolean newLinebreakerAlways) { // TODO make this more efficient so it only clears annotation list when it // really has to if (isAnnotation()) { Frame parent = getParent(); // parent can be null when running tests if (parent != null) { parent.clearAnnotations(); } } // if there is no text, there is nothing to do if ((_text == null || _text.length() == 0 ) && getPlaceholder() == null) { // Frame parent = getParent(); // if(parent != null) // parent.removeItem(this); return; } EcosystemManager.getTextLayoutManager().releaseLayouts(getTextLayouts()); if (getTextLayouts() != null) { getTextLayouts().clear(); } EcosystemManager.getTextLayoutManager().releaseLayouts(_maskTextLayouts); if (_maskTextLayouts != null) { _maskTextLayouts.clear(); } EcosystemManager.getTextLayoutManager().releaseLayouts(_placeholderTextLayouts); if (_placeholderTextLayouts != null) { _placeholderTextLayouts.clear(); } // Calculate the maximum allowable width of this line of text List lines = null; if (_autoWrap || ExperimentalFeatures.AutoWrap.get()) { lines = new LinkedList(); if (DisplayController.getCurrentFrame() == null) { return; } for (Item item : DisplayController.getCurrentFrame().getSortedItems()) { if (item instanceof Line) { lines.add(new org.expeditee.core.Line(((Line) item).getStartItem().getPosition(), ((Line) item).getEndItem().getPosition())); } if (item instanceof Picture) { lines.add(new org.expeditee.core.Line(item.getPosition(), new Point(item.getX(), item.getY() + item.getHeight()))); } } for (Item item : FreeItems.getInstance()) { if (item instanceof Line) { lines.add(new org.expeditee.core.Line(((Line) item).getStartItem().getPosition(), ((Line) item).getEndItem().getPosition())); } if (item instanceof Picture) { lines.add(new org.expeditee.core.Line(item.getPosition(), new Point(item.getX(), item.getY() + item.getHeight()))); } } } float width = (float) Integer.MAX_VALUE; if (limitWidth) { if (_width == null) { width = DisplayController.getFramePaintAreaWidth() - getX(); } else { width = getAbsoluteWidth(); } } Font paintFont = getPaintFont(); if (this._text != null && this._text.length() > 0) { List proposedTextLayout = EcosystemManager.getTextLayoutManager().layoutString( _text.toString(), paintFont, new Point(getX(), getY()), lines != null ? lines.toArray(new org.expeditee.core.Line[1]) : null, (int) width, (int) getSpacing(), true, getJustification() == Justification.full ); if (proposedTextLayout.size() > 1 && isSingleLineOnly()) { paintFont = paintFont.clone(); while(proposedTextLayout.size() > 1) { paintFont.setSize(Math.max(paintFont.getSize() - 1, MINIMUM_FONT_SIZE)); proposedTextLayout = EcosystemManager.getTextLayoutManager().layoutString( _text.toString(), paintFont, new Point(getX(), getY()), lines != null ? lines.toArray(new org.expeditee.core.Line[1]) : null, paintFont.getSize() > MINIMUM_FONT_SIZE ? (int) width : Integer.MAX_VALUE, (int) getSpacing(), true, getJustification() == Justification.full ); } } this._textLayouts = proposedTextLayout; if (this.getMask() != null) { final Stream maskStream = _text.toString().chars().mapToObj(c -> (char) this.getMask().intValue()); final StringBuilder sb = new StringBuilder(); maskStream.forEach(c -> sb.append(c)); this._maskTextLayouts = EcosystemManager.getTextLayoutManager().layoutString( sb.toString(), paintFont, new Point(getX(), getY()), lines != null ? lines.toArray(new org.expeditee.core.Line[1]) : null, (int) width, (int) getSpacing(), true, getJustification() == Justification.full ); } } if (this.getPlaceholder() != null) { this._placeholderTextLayouts = EcosystemManager.getTextLayoutManager().layoutString( getPlaceholder(), paintFont, new Point(getX(), getY()), lines != null ? lines.toArray(new org.expeditee.core.Line[1]) : null, (int) width, (int) getSpacing(), true, getJustification() == Justification.full ); } invalidateBounds(); } /** * Calculates the maximum possible distance a line can extend to the right from * a given (x,y) point without crossing any of the lines in the given list. * * @param x * The x-coordinate of the beginning point. * * @param y * The y-coordinate of the beginning point. * * @param lines * A list of pairs of points describing the lines that should stop * the extension. * * @return The length of the extended line. */ /* * private float getLineWidth(int x, float y, List lines) { float width * = FrameGraphics.getMaxFrameSize().width; for (Point[] l : lines) { // check * for lines that cross over our y if ((l[0].y >= y && l[1].y <= y) || (l[0].y * <= y && l[1].y >= y)) { float dX = l[0].x - l[1].x; float dY = l[0].y - * l[1].y; float newWidth; if (dX == 0) { newWidth = l[0].x; } else if (dY == 0) * { newWidth = Math.min(l[0].x, l[1].x); } else { // * System.out.print("gradient: " + (dY / dX)); newWidth = l[0].x + (y - l[0].y) * * dX / dY; } // System.out.println("dY:" + dY + " dX:" + dX + " width:" + * newWidth); if (newWidth < x) { continue; } if (newWidth < width) { width = * newWidth; } } } return width - x; } */ private boolean hasFixedWidth() { assert (_width != null); if (_width == null) { justify(false); } return _width > 0; } private int _alpha = -1; public void setAlpha(int alpha) { _alpha = alpha; } private Range getSelectedRange(int line) { if (_selectionEnd >= _text.length()) { _selectionEnd = _text.length(); } if (_selectionStart < 0) { _selectionStart = 0; } if (_selectionStart < 0 || _selectionEnd < 0) { return null; } int selectionLeft = Math.min(_selectionStart, _selectionEnd); int selectionRight = Math.max(_selectionStart, _selectionEnd); // if the selection is after this line, return null if (getTextLayouts().get(line).getStartCharIndex() > selectionRight) { return null; } // if the selection is before this line, return null if (getTextLayouts().get(line).getEndCharIndex() < selectionLeft) { return null; } // Dont highlight a single char // if (selectionRight - selectionLeft <= MINIMUM_RANGED_CHARS) // return null; // the selection occurs on this line, determine where it lies on the // line int start = Math.max(0, selectionLeft - getTextLayouts().get(line).getStartCharIndex()); // int end = Math.min(_lineOffsets.get(line) + // _textLayouts.get(line).getCharacterCount(), _selectionEnd); int end = Math.min(selectionRight - getTextLayouts().get(line).getStartCharIndex(), getTextLayouts().get(line).getCharacterCount()); // System.out.println(line + ": " + start + "x" + end + " (" + // _selectionStart + "x" + _selectionEnd + ")"); return new Range(start, end, true, true); } /** Sets the colour that should be used to render the selected range. */ public void setSelectionColour(Colour colour) { if (colour == null) { colour = RANGE_SELECT_COLOUR; } _selectionColour = colour; } /** Gets the colour that should be used to render the selected range. */ public Colour getSelectionColour() { return _selectionColour; } @Override public void paint() { if (!isVisible()) { return; } boolean hasContent = !(_text == null || (_text.length() == 0 && getMinWidth() == null)); boolean hasPlaceholderContent = getPlaceholder() != null; // if the text to paint is empty string and there is no min width, do nothing. if (!hasContent && !hasPlaceholderContent) { return; } if (_autoWrap || ExperimentalFeatures.AutoWrap.get()) { invalidateAll(); rebuild(true); } else if (getTextLayouts().size() < 1) { clipFrameMargin(); rebuild(true); // return; } // check if its a vector item and paint all the vector stuff too if this // item is a free item // This will allow for dragging vectors around the place! if (hasVector() && isFloating()) { DisplayController.requestRefresh(false); // TODO make this use a more efficient paint method... // Have the text item return a bigger repaint area if it has an // associated vector } GraphicsManager g = EcosystemManager.getGraphicsManager(); AxisAlignedBoxBounds bounds = (AxisAlignedBoxBounds) getBounds(); // the background is only cleared if required if (getBackgroundColor() != null) { Colour bgc = getBackgroundColor(); if (_alpha > 0) { bgc = new Colour(bgc.getRed(), bgc.getGreen(), bgc.getBlue(), Colour.FromComponent255(_alpha)); } Colour gradientColor = getGradientColor(); Fill fill; if (gradientColor != null && bounds != null) { fill = new GradientFill(bgc, new Point((int) (bounds.getMinX() + bounds.getWidth() * 0.3), bounds.getMinY()), gradientColor, new Point((int) (bounds.getMinX() + bounds.getWidth() * 1.3), bounds.getMinY())); } else { fill = new Fill(bgc); } g.drawRectangle(bounds, 0.0, fill, null, null, null); } if (hasVisibleBorder()) { Stroke borderStroke = new Stroke(getThickness(), DEFAULT_CAP, DEFAULT_JOIN); g.drawRectangle(bounds, 0.0, null, getPaintBorderColor(), borderStroke, null); } if (hasFormula()) { Stroke highlightStroke = new Stroke(1F, DEFAULT_CAP, DEFAULT_JOIN); Point start = getEdgePosition(0, true); Point end = getEdgePosition(0, false); g.drawLine(start, end, getPaintHighlightColor(), highlightStroke); } if (isHighlighted()) { Stroke highlightStroke = new Stroke(getHighlightThickness(), DEFAULT_CAP, DEFAULT_JOIN); Fill fill; if (HighlightMode.Enclosed.equals(getHighlightMode())) { fill = new Fill(getPaintHighlightColor()); } else { fill = null; } g.drawRectangle(bounds, 0.0, fill, getPaintHighlightColor(), highlightStroke, null); } float y = getY(); Colour paintColour = getPaintColor(); if (_alpha > 0) { paintColour = new Colour(paintColour); paintColour.setAlpha(Colour.FromComponent255(_alpha)); } if (getTextLayouts() == _placeholderTextLayouts) { paintColour = paintColour.clone(); paintColour.setAlpha(Colour.FromComponent255(60)); } Colour selectionColour = getSelectionColour(); // width -= getX(); // int line = 0; // boolean tab = false; synchronized (getTextLayouts()) { for (int i = 0; i < getTextLayouts().size(); i++) { TextLayout layout = getTextLayouts().get(i); Range selectedRange = getSelectedRange(i); if (selectedRange != null) { AxisAlignedBoxBounds highlight = layout.getLogicalHighlightShape(selectedRange.lowerBound, selectedRange.upperBound); highlight.getTopLeft().add(getX() + getJustOffset(layout), (int) y); g.drawRectangle(highlight, 0.0, new Fill(selectionColour), null, null, null); } if (layout.getCharacterCount() == 0) { continue; } int ldx = 1 + getX() + getJustOffset(layout); // Layout draw x g.drawTextLayout(layout, new Point(ldx, (int) y), paintColour); y += getLineDrop(layout); } } paintLink(); } // TODO: Revise @Override protected AxisAlignedBoxBounds getLinkDrawArea() { return getDrawingArea(); } /** * Determines if this text has any text in it. * * @return True if this Item has no text in it, false otherwise. */ public boolean isEmpty() { return (_text == null || _text.length() == 0); } @Override public Text copy() { Text copy = new Text(getID()); // copy standard item values Item.DuplicateItem(this, copy); // copy values specific to text items copy.setSpacing(getSpacing()); copy.setInitialSpacing(getInitialSpacing()); copy.setWidth(getWidth()); copy.setJustification(getJustification()); copy.setLetterSpacing(getLetterSpacing()); copy.setWordSpacing(getWordSpacing()); //copy.setWidth(getWidthToSave()); copy.setFont(getFont().clone()); copy.setMinWidth(getMinWidthToSave()); copy.setMask(_mask); if (hasFormula()) { copy.calculate(getFormula()); } else { copy.setText(_text.toString()); } copy.setHidden(!isVisible()); return copy; } @Override public float getSize() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.FONT_STR)) { return ((Text) this.getPrimary()).getSize(); } else { return getPaintFont().getSize(); } //return getPaintFont().getSize(); } /** * Returns the number of characters in this Text, excluding new lines. * * @return The sum of the length of each line of text */ public int getLength() { return _text.length(); } @Override public void setSize(float size) { invalidateAll(); // size *= UserSettings.ScaleFactor; // Dont want to have size set when duplicating a point which has size 0 if (size < 0) { return; } if (size < MINIMUM_FONT_SIZE) { size = MINIMUM_FONT_SIZE; } Font currentFont = getPaintFont(); currentFont.setSize((int) size); // setFont(currentFont); rebuild(true); invalidateAll(); if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.FONT_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.FONT_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.FONT_STR, inheritanceCheckOnSave); } } } @Override public void setAnnotation(boolean val) { float mouseX = DisplayController.getFloatMouseX(); float mouseY = DisplayController.getFloatMouseY(); Point newPoint = new Point(); if (val) { // if this is already an annotation, do nothing if (isAnnotation()) { return; } if (!isLineEnd() && _text.length() > 0 && _text.charAt(0) == DEFAULT_BULLET) { newPoint.set(insertText("\b", mouseX, mouseY, 1)); if (_text.length() > 0 && _text.charAt(0) == ' ') { newPoint.set(insertText("\b", newPoint.getX(), newPoint.getY(), 1)); } } else { newPoint.set(insertText("@", mouseX, mouseY, 0)); } } else { // if this is not an annotation, do nothing if (!isAnnotation()) { return; } if (!isLineEnd() && _text.charAt(0) == '@') { newPoint.set(insertText("\b", mouseX, mouseY, 1)); newPoint.set(insertText(DEFAULT_BULLET_STRING, newPoint.getX(), newPoint.getY(), 0)); } else { newPoint.set(insertText("\b", mouseX, mouseY, 1)); } } FrameUtils.setLastEdited(this); rebuild(true); DisplayController.setCursorPosition(newPoint.getX(), newPoint.getY(), false); } /** * */ private void insertString(String toInsert, int pos) { assert (toInsert.length() > 0); invalidateAll(); _text.insert(pos, toInsert); rebuild(false); invalidateAll(); } private void deleteChar(int pos) { _text.deleteCharAt(pos); if (_text.length() == 0) { if (this.getMinWidth() != null) { // final TextLayout base = _textLayouts.get(0); // _textLayouts.set(0, TextLayout.get(" ", base.getFont(), 0, 1)); getTextLayouts().clear(); } if (this.isLineEnd()) { // Remove and replace with a dot Item.replaceText(this); DisplayController.setCursorPosition(this._x, this._y); } return; } } @Override public boolean isAnnotation() { if (_text != null && _text.length() > 0 && _text.charAt(0) == '@') { return true; } return false; } public boolean isSpecialAnnotation() { assert _text != null; String s = _text.toString().toLowerCase(); if (s.length() > 0 && s.indexOf("@") == 0) { if (s.equals("@old") || s.equals("@ao") || s.equals("@itemtemplate") || s.equals("@parent") || s.equals("@next") || s.equals("@previous") || s.equals("@first") || s.equals("@i") || s.equals("@iw") || s.equals("@f")) { return true; } } return false; } @Override public Item merge(Item merger, int mouseX, int mouseY) { if (merger.isLineEnd()) { // Merging line ends onto non line end text is a no-op if (!isLineEnd()) { return null; } if (merger instanceof Text) { insertText(((Text) merger).getText(), mouseX, mouseY); } // Set the position by moving the cursor before calling this // method!! List lines = new LinkedList(); lines.addAll(merger.getLines()); for (Line line : lines) { line.replaceLineEnd(merger, this); } merger.delete(); this.setOffset(0, 0); return null; } if (!(merger instanceof Text)) { return merger; } Text merge = (Text) merger; // insertText(merge.getText(), mouseX, mouseY); // if the item being merged has a link if (merge.getLink() != null) { // if this item has a link, keep it on the cursor if (getLink() != null) { merge.setText(merge.getLink()); merge.setLink(null); // return merge; // TODO get this to return the merged item and attach it to the // cursor only when the user presses the middle button. } else { setLink(merge.getLink()); } } return null; } /** * Resets the position of the item to the default position for a title item. * */ public void resetTitlePosition() { final int boundsHeight = getBoundsHeight(); setPosition(MARGIN_LEFT, MARGIN_LEFT + boundsHeight); Frame modelFrame = getParentOrCurrentFrame(); if (modelFrame != null) { int model_frame_name_x = modelFrame.getNameItem().getX(); if (model_frame_name_x < DisplayController.MINIMUM_FRAME_WIDTH) { System.err.println("**** Text::resetTitlePostion(): value to be used as right margin from position of frameName < 512"); System.err.println(" Overriding to ensure reasonable width for title"); model_frame_name_x = DisplayController.MINIMUM_FRAME_WIDTH; } setRightMargin(model_frame_name_x - MARGIN_LEFT, true); } else { System.out.print("Error: text.resetTitlePosition, getParent or currentFrame returned null"); setRightMargin(MARGIN_LEFT, true); } } /** * Removes the set of characters up to the first space in this text item. * * @return the string that was removed. */ public String stripFirstWord() { int firstSpace = _text.toString().indexOf(' '); // if there is only one word just make it blank if (firstSpace < 0 || firstSpace + 1 >= _text.length()) { String text = _text.toString(); setText(""); return text; } String firstWord = _text.toString().substring(0, firstSpace); setText(_text.toString().substring(firstSpace).trim()); return firstWord; } @Override public String toString() { String message = "[" + getFirstLine() + "]" + FRAME_NAME_SEPARATOR; if (getParent() != null) { return message + getParent().getName(); } return message + getDateCreated(); } public Text getTemplateForm() { Text template = this.copy(); template.setID(-1); // reset width of global templates so the widths of the items on the settings // frames don't cause issues // this is in response to the fact that FrameCreator.addItem() sets rightMargin // when it adds items template.setWidth(null); /* * The template must have text otherwise the bounds height will be zero!! This * will stop escape drop down from working if there is no item template */ template.setText("@"); return template; } /** * Checks if the given point is 'near' any line of the text item. */ @Override public boolean isNear(int x, int y) { if (super.isNear(x, y)) { // TODO check that it is actually near one of the lines of space // return contains(x, y, getGravity() * 2 + NEAR_DISTANCE); // at the moment contains ignores gravity when checking the top and // bottom of text lines... so the cursor must be between two text // lines float textY = getY(); float textX = getX(); for (TextLayout text : getTextLayouts()) { // check left and right of each box AxisAlignedBoxBounds textOutline = text.getLogicalHighlightShape(0, text.getCharacterCount()); // check if the cursor is within the top, bottom and within the // gravity of right if (y - textY > textOutline.getMinY() - NEAR_DISTANCE && y - textY < textOutline.getMinY() + textOutline.getHeight() + NEAR_DISTANCE && x - textX < textOutline.getWidth() + NEAR_DISTANCE) { return true; } textY += getLineDrop(text); } } return false; } @Override public void anchor() { super.anchor(); // ensure all text items have their selection cleared clearSelection(); setAlpha(0); if (isLineEnd()) { DisplayController.setCursor(Item.DEFAULT_CURSOR); } String text = _text.toString().trim(); clipFrameMargin(); // Show the overlay stuff immediately if this is an overlay item if (hasLink() && (text.startsWith("@ao") || text.startsWith("@o"))) { StandardGestureActions.Refresh(); } } private void clipFrameMargin() { if (!hasFixedWidth()) { int frameWidth = DisplayController.getFramePaintAreaWidth(); /* * Only change width if it is more than 150 pixels from the right of the screen */ if (!_text.toString().contains(" ")) { Integer width = getWidth(); if (width == null || width < 0) { setWidth(Integer.MIN_VALUE + 1); // +1 makes it safe to do math.abs on } } else if (frameWidth - getX() > ADJUST_WIDTH_THRESHOLD) { justify(false); // setRightMargin(frameWidth, false); } } } public void justify(boolean fixWidth, PolygonBounds enclosure) { // if autowrap is on, wrapping is done every time we draw if (ExperimentalFeatures.AutoWrap.get()) { return; } Integer width = DisplayController.getFramePaintAreaWidth(); // Check if that text item is inside an enclosing rectangle... // Set its max width accordingly if (enclosure != null) { AxisAlignedBoxBounds bounds = AxisAlignedBoxBounds.getEnclosing(enclosure); if (bounds.getWidth() > 200 && getX() < bounds.getWidth() / 3 + bounds.getMinX()) { width = bounds.getMinX() + bounds.getWidth(); } } if (getWidth() == null) { setRightMargin(width, fixWidth); } // Check for the annotation that restricts the width of text items on the frame String widthString; if ((widthString = getParentOrCurrentFrame().getAnnotationValue("maxwidth")) != null) { try { int oldWidth = getWidth(); int maxWidth = Integer.parseInt(widthString); if (maxWidth < oldWidth) { setWidth(maxWidth); } } catch (NumberFormatException nfe) { nfe.printStackTrace(); } } } public void justify(boolean fixWidth) { // if autowrap is on, wrapping is done every time we draw if (ExperimentalFeatures.AutoWrap.get()) { return; } this.justify(fixWidth, FrameUtils.getEnlosingPolygon()); } public void resetFrameNamePosition() { //Dimension maxSize = DisplayController.getSizeEnforceMinimum(); Dimension maxSize = DisplayController.getFramePaintAreaSize(); if (maxSize != null) { // setMaxWidth(maxSize.width); setPosition(maxSize.width - getBoundsWidth(), getBoundsHeight()); } } @Override protected int getLinkYOffset() { if (getTextLayouts().size() == 0) { return 0; } return Math.round(-(getTextLayouts().get(0).getAscent() / 2)); } @Override public String getName() { return getFirstLine(); } public static final String TAB_STRING = " "; public Point insertTab(char ch, float mouseX, float mouseY) { return insertText("" + ch, mouseX, mouseY); } public Point removeTab(char ch, float mouseX, float mouseY) { // Insert a space as a flag that it is a backwards tab return insertText(ch + " ", mouseX, mouseY); } public static boolean isBulletChar(char c) { for (int i = 0; i < BULLETS.length; i++) { if (BULLETS[i] == c) { return true; } } return c == '*' || c == '+' || c == '>' || c == '-' || c == 'o'; } @Override public boolean hasOverlay() { if (!isAnnotation() || getLink() == null) { return false; } String text = getText().toLowerCase(); // TODO make it so can just check the _overlay variable // Mike can't remember the reason _overlay var can't be use! oops if (!text.startsWith("@")) { return false; } return text.startsWith("@o") || text.startsWith("@ao") || text.startsWith("@v") || text.startsWith("@av"); } public boolean hasSelection() { return getSelectionSize() > 0; } /** * Dont save text items that are all white space. */ @Override public boolean dontSave() { String text = getText(); assert (text != null); return super.dontSave() || (text.trim().length() == 0 && this.getMinWidth() == null); } @Override public boolean calculate(String formula) { if (DisplayController.isXRayMode()) { return false; } super.calculate(formula); if (isFloating() || formula == null || formula.length() == 0) { return false; } formula = formula.replace(':', '='); String lowercaseFormula = formula.toLowerCase(); ExpediteeJEP myParser = new ExpediteeJEP(); int nextVarNo = 1; // Add variables from the containing rectangle if the item being // calculated is inside the enclosing rectangle Collection enclosed = getItemsInSameEnclosure(); for (Item i : enclosed) { if (i == this) { continue; } if (i instanceof Text && !i.isAnnotation()) { AttributeValuePair pair = i.getAttributeValuePair(); if (pair.hasPair()) { try { double value = pair.getDoubleValue(); myParser.addVariable(pair.getAttribute(), value); // myParser.addVariable("$" + nextVarNo++, value); } catch (NumberFormatException nfe) { continue; } catch (Exception e) { e.printStackTrace(); } } // else { // Add anonomous vars try { double value = pair.getDoubleValue(); if (value != Double.NaN) { myParser.addVariable("$" + nextVarNo++, value); } } catch (NumberFormatException nfe) { continue; } catch (Exception e) { e.printStackTrace(); } // } } } // Add the variables from this frame myParser.addVariables(this.getParentOrCurrentFrame()); String linkedFrame = getAbsoluteLink(); // Add the relative frame variable if the item is linked if (linkedFrame != null) { Frame frame = FrameIO.LoadFrame(linkedFrame); myParser.addVariables(frame); // If the frame is linked add vector variable for the frame if (lowercaseFormula.contains("$frame")) { myParser.addVectorVariable(frame.getNonAnnotationItems(true), "$frame"); } } // Add the relative box variable if this item is a line end if (this.isLineEnd()) { // if its a line end add the enclosed stuff as an @variable if (lowercaseFormula.contains("$box")) { myParser.addVectorVariable(getEnclosedItems(), "$box"); } } myParser.resetObserver(); try { Node node = myParser.parse(formula); String result = myParser.evaluate(node); if (result != null) { this.setText(result); this.setFormula(formula); if (!this.hasAction()) { setActionMark(false); setAction("extract formula"); } } } catch (Throwable e) { // e.printStackTrace(); String formula2 = getFormula(); this.setText(formula2); this.setFormula(formula2); return false; } _attributeValuePair = null; return true; } /** * Gets items which are in the same enclosure as this item. In the event more * than one enclosure meets this criteria, then the one returned is the one with * the smallest area. TODO: Improve the efficiency of this method * * @return */ public Collection getItemsInSameEnclosure() { Collection sameEnclosure = null; Collection seen = new HashSet(); Frame parent = getParentOrCurrentFrame(); double enclosureArea = Double.MAX_VALUE; for (Item i : parent.getVisibleItems()) { /* * Go through all the enclosures looking for one that includes this item */ if (!seen.contains(i) && i.isEnclosed()) { seen.addAll(i.getEnclosingDots()); Collection enclosed = i.getEnclosedItems(); // Check if we have found an enclosure containing this item // Check it is smaller than any other enclosure found containing // this item if (enclosed.contains(this) && i.getEnclosedArea() < enclosureArea) { sameEnclosure = enclosed; } } } if (sameEnclosure == null) { return new LinkedList(); } return sameEnclosure; } /** * Returns true if items of the parent frame should be recalculated when this * item is modified */ @Override public boolean recalculateWhenChanged() { if (/* * !isAnnotation() && */(hasFormula() || isLineEnd())) { return true; } try { AttributeValuePair avp = getAttributeValuePair(); if (!avp.getDoubleValue().equals(Double.NaN)) { return true; } } catch (Exception e) { e.printStackTrace(); } return false; } public float getLineHeight() { return getLineDrop(getTextLayouts().get(0)); } @Override public void setAnchorLeft(Integer anchor) { if (!isLineEnd()) { super.setAnchorLeft(anchor); // Subtract off the link width if (anchor != null) { setX(anchor + getLeftMargin()); } return; } invalidateFill(); invalidateCommonTrait(ItemAppearence.PreMoved); this._anchoring.setLeftAnchor(anchor); int oldX = getX(); if (anchor != null) { float deltaX = anchor + getLeftMargin() - oldX; anchorConnected(AnchorEdgeType.Left, deltaX); } invalidateCommonTrait(ItemAppearence.PostMoved); invalidateFill(); if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.ANCHOR_LEFT_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.ANCHOR_LEFT_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.ANCHOR_LEFT_STR, inheritanceCheckOnSave); } } } @Override public void setAnchorCenterX(Integer anchor) { if (!isLineEnd()) { super.setAnchorCenterX(anchor); // Subtract off the link width if (anchor != null) { int alignedToLeft = DisplayController.getFramePaintArea().getCentreX() + anchor; int alignedToCenter = alignedToLeft - (getBoundsWidth() / 2); setX(alignedToCenter); } return; } invalidateFill(); invalidateCommonTrait(ItemAppearence.PostMoved); this._anchoring.setCenterXAnchor(anchor); int oldX = getX(); if (anchor != null) { float deltaX = DisplayController.getFramePaintArea().getCentreX() - anchor - getBoundsWidth() + getLeftMargin() - oldX; anchorConnected(AnchorEdgeType.CenterY, deltaX); } invalidateCommonTrait(ItemAppearence.PostMoved); invalidateFill(); if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.ANCHOR_CENTERX_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.ANCHOR_CENTERX_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.ANCHOR_CENTERX_STR, inheritanceCheckOnSave); } } } @Override public void setAnchorRight(Integer anchor) { if (!isLineEnd()) { super.setAnchorRight(anchor); // Subtract off the link width if (anchor != null) { setX(DisplayController.getFramePaintAreaWidth() - anchor - getBoundsWidth() + getLeftMargin()); //System.err.println("Text::setAnchorRight::boundsWidth=" + getBoundsWidth()); } return; } invalidateFill(); invalidateCommonTrait(ItemAppearence.PreMoved); this._anchoring.setRightAnchor(anchor); int oldX = getX(); if (anchor != null) { float deltaX = DisplayController.getFramePaintAreaWidth() - anchor - getBoundsWidth() + getLeftMargin() - oldX; anchorConnected(AnchorEdgeType.Right, deltaX); } invalidateCommonTrait(ItemAppearence.PostMoved); invalidateFill(); if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.ANCHOR_RIGHT_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.ANCHOR_RIGHT_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.ANCHOR_RIGHT_STR, inheritanceCheckOnSave); } } } @Override public void setAnchorTop(Integer anchor) { if (!isLineEnd()) { super.setAnchorTop(anchor); if (anchor != null) { if (!getTextLayouts().isEmpty()) { final float ascent = getTextLayouts().get(0).getAscent(); setY(anchor + ascent); } else if (this.getFont() != null) { // p could be any character final TextLayout fakeLayout = TextLayout.getManager().layoutStringSimple("p", this.getFont()); final float ascent = fakeLayout.getAscent(); EcosystemManager.getTextLayoutManager().releaseLayout(fakeLayout); setY(anchor + ascent); } } return; } invalidateFill(); invalidateCommonTrait(ItemAppearence.PreMoved); this._anchoring.setTopAnchor(anchor); int oldY = getY(); if (anchor != null) { float deltaY = anchor - oldY; anchorConnected(AnchorEdgeType.Top, deltaY); } invalidateCommonTrait(ItemAppearence.PostMoved); invalidateFill(); if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.ANCHOR_TOP_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.ANCHOR_TOP_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.ANCHOR_TOP_STR, inheritanceCheckOnSave); } } } @Override public void setAnchorCenterY(Integer anchor) { if (!isLineEnd()) { super.setAnchorCenterY(anchor); if (anchor != null) { List textLayouts = getTextLayouts(); if (!textLayouts.isEmpty()) { int middle = textLayouts.size() / 2; float heightFromTopOfTextItem = 0; for (int i = 0; i <= middle; i++) { TextLayout tl = textLayouts.get(i); float ascent = tl.getAscent(); float descent = tl.getDescent(); heightFromTopOfTextItem += (ascent + descent); } heightFromTopOfTextItem -= textLayouts.get(middle).getDescent(); float anchorCalc = anchor + this.getBoundsHeight() - heightFromTopOfTextItem; setY((DisplayController.getFramePaintAreaHeight() / 2) - anchorCalc); } else if (this.getFont() != null) { // p could be any character TextLayout fakeLayout = TextLayout.getManager().layoutStringSimple("p", this.getFont()); float ascent = fakeLayout.getAscent(); float descent = fakeLayout.getDescent(); float middle = descent - ascent; float anchorCalc = anchor + this.getBoundsHeight() - middle; setY((DisplayController.getFramePaintAreaHeight() / 2) - anchorCalc); } } return; } } @Override public void setAnchorBottom(Integer anchor) { if (!isLineEnd()) { super.setAnchorBottom(anchor); if (anchor != null) { if (!getTextLayouts().isEmpty()) { final float ascent = getTextLayouts().get(0).getAscent(); final float descent = getTextLayouts().get(0).getDescent(); setY(DisplayController.getFramePaintAreaHeight() - (anchor + this.getBoundsHeight() - ascent - descent)); } else if (this.getFont() != null) { // p could be any character final TextLayout fakeLayout = TextLayout.getManager().layoutStringSimple("p", this.getFont()); final float ascent = fakeLayout.getAscent(); final float descent = fakeLayout.getDescent(); setY(DisplayController.getFramePaintAreaHeight() - (anchor + this.getBoundsHeight() - ascent - descent)); } } return; } invalidateFill(); invalidateCommonTrait(ItemAppearence.PreMoved); this._anchoring.setBottomAnchor(anchor); int oldY = getY(); if (anchor != null) { float deltaY = DisplayController.getFramePaintAreaHeight() - anchor - oldY; anchorConnected(AnchorEdgeType.Bottom, deltaY); } invalidateCommonTrait(ItemAppearence.PostMoved); invalidateFill(); if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.ANCHOR_BOTTOM_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.ANCHOR_BOTTOM_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.ANCHOR_BOTTOM_STR, inheritanceCheckOnSave); } } } @Override public void scale(Float scale, int originX, int originY) { setSize(getSize() * scale); Integer width = getWidth(); if (width != null) { setWidth(Math.round(width * scale)); } super.scale(scale, originX, originY); rebuild(true); } protected AxisAlignedBoxBounds getPixelBounds(TextLayout layout) { // Does 'layout' need to be synchronized (similar to _textLayouts below)?? int x = getX(); int y = getY(); int ldx = 1 + x + getJustOffset(layout); // Layout draw x AxisAlignedBoxBounds layout_rect = layout.getPixelBounds(ldx, y); return layout_rect; } /** * Creates the smallest possible rectangle object to enclose the Text Item * completely. Width of the rectangle is determined by the line in the Text Item * that protrudes to the right the most. Height of the rectangle is determined * by the number of lines in the Text Item. * * @return A rectangle enclosing the Text Item, without gravity represented. * @see #getPixelBoundsUnionTight() */ public AxisAlignedBoxBounds getPixelBoundsUnion() { final AxisAlignedBoxBounds rect = getPixelBounds(getTextLayouts().get(0)); int cumulativeHeight = rect.getSize().height; int maxWidth = rect.getSize().width; if (getTextLayouts().size() > 1) { for (int i = 1; i < getTextLayouts().size(); i++) { final AxisAlignedBoxBounds r = getPixelBounds(getTextLayouts().get(i)); cumulativeHeight += getTextLayouts().get(i).getDescent() + getTextLayouts().get(i).getAscent(); if (r.getSize().width > maxWidth) { maxWidth = r.getSize().width; } } } rect.getSize().width = maxWidth; rect.getSize().height = cumulativeHeight; return rect; } /** * Creates the smallest possible polygon to enclose the Text Item completely. * The shape of the polygon is determined by the length of each line, tightly * fitting the shape so that no white space is inside the resulting polygon. * * @return A polygon enclosing the Text Item, without gravity represented. * @see #getPixelBoundsUnion() */ public PolygonBounds getPixelBoundsUnionTight() { final AxisAlignedBoxBounds rect = getPixelBounds(getTextLayouts().get(0)); if (getTextLayouts().size() == 1) { return PolygonBounds.fromBox(rect); } else { final PolygonBounds poly = new PolygonBounds(); poly.addPoint(rect.getMinX(), rect.getMinY()); poly.addPoint(rect.getMaxX(), rect.getMinY()); poly.addPoint(rect.getMaxX(), Math.round(rect.getMaxY() + getTextLayouts().get(0).getDescent())); int y = (int) (rect.getMaxY() + getTextLayouts().get(0).getDescent()); for (int i = 1; i < getTextLayouts().size(); i++) { final AxisAlignedBoxBounds r = getPixelBounds(getTextLayouts().get(i)); poly.addPoint(r.getMaxX(), y); poly.addPoint(r.getMaxX(), Math.round(y + r.getHeight() + getTextLayouts().get(i).getDescent())); y = Math.round(y + r.getHeight() + getTextLayouts().get(i).getDescent()); } poly.addPoint(rect.getMinX() + getPixelBounds(getTextLayouts().get(getTextLayouts().size() - 1)).getWidth(), Math.round(y + getTextLayouts().get(getTextLayouts().size() - 1).getDescent())); poly.addPoint(rect.getMinX(), Math.round(y + getTextLayouts().get(getTextLayouts().size() - 1).getDescent())); return poly; } } /* * public AxisAlignedBoxBounds getPixelBoundsUnion() { synchronized * (_textLayouts) { * * CombinationBoxBounds c = null; * * for (TextLayout layout: _textLayouts) { if (c == null) { c = new * CombinationBoxBounds(getPixelBounds(layout)); } else { * c.add(getPixelBounds(layout)); } } * * return AxisAlignedBoxBounds.getEnclosing(c); * * } } */ // public Rectangle getPixelBoundsUnion() // { // synchronized (_textLayouts) { // // int x = getX(); // int y = getY(); // // int min_xl = Integer.MAX_VALUE; // int max_xr = Integer.MIN_VALUE + 1; // +1 makes it safe to do math.abs on // // int min_yt = Integer.MAX_VALUE; // int max_yb = Integer.MIN_VALUE + 1; // +1 makes it safe to do math.abs on // // // for (int i = 0; i < _textLayouts.size(); i++) { // TextLayout layout = _textLayouts.get(i); // // int ldx = 1+x+getJustOffset(layout); // Layout draw x // Rectangle layout_rect = layout.getPixelBounds(null, ldx, y); // // int xl = layout_rect.x; // int xr = xl + layout_rect.width -1; // // int yt = layout_rect.y; // int yb = yt + layout_rect.height -1; // // min_xl = Math.min(min_xl,xl); // max_xr = Math.max(max_xr,xr); // // min_yt = Math.min(min_yt,yt); // max_yb = Math.max(max_yb,yb); // } // // if ((min_xl >= max_xr) || (min_yt >= max_yb)) { // // No valid rectangle are found // return null; // } // // return new Rectangle(min_xl,min_yt,max_xr-min_xl+1,max_yb-min_yt+1); // // } // // } /* * Returns the SIMPLE statement contained by this text item. * */ public String getStatement() { return getText().split("\\s+")[0]; } public boolean getAutoWrap() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.AUTO_WRAP_TO_SAVE_STR)) { return ((Text) this.getPrimary()).getAutoWrap(); } else { return _autoWrap; } } // workaround since true is the default value and would not be displayed // normally public String getAutoWrapToSave() { if (!getAutoWrap()) { return null; } return "true"; } public void setAutoWrap(boolean autoWrap) { _autoWrap = autoWrap; if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.AUTO_WRAP_TO_SAVE_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.AUTO_WRAP_TO_SAVE_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.AUTO_WRAP_TO_SAVE_STR, inheritanceCheckOnSave); } } } /** * Creates a new Text Item whose text contains the given character. This method * also moves the mouse cursor to be pointing at the newly created Text Item * ready to insert the next character. * * @param start * The character to use as the initial text of this Item. * @return The newly created Text Item */ public static Text createText(char start) { Text t = DisplayController.getCurrentFrame().createBlankText("" + start); Point newMouse = t.insertChar(start, DisplayController.getMouseX(), DisplayController.getMouseY()); DisplayController.setCursorPosition(newMouse.getX(), newMouse.getY(), false); return t; } /** * Creates a new Text Item with no text. The newly created Item is a copy of any * ItemTemplate if one is present, and inherits all the attributes of the * Template * * @return The newly created Text Item */ public static Text createText() { return DisplayController.getCurrentFrame().createNewText(); } /** * If the given Item is null, then a new Text item is created with the current * date. If the given Item is not null, then the current date is prepended to * the Item's text * * @param toAdd * The Item to prepend the date to, or null */ public static void AddDate(Item toAdd) { String date1 = Formatter.getDateTime(); String date2 = Formatter.getDate(); final String leftSeparator = " :"; final String rightSeparator = ": "; String dateToAdd = date1 + rightSeparator; boolean prepend = false; boolean append = false; // if the user is pointing at an item, add the date where ever the // cursor is pointing if (toAdd != null && toAdd instanceof Text) { // permission check if (!toAdd.hasPermission(UserAppliedPermission.full)) { MessageBay.displayMessage("Insufficicent permission to add the date to that item"); return; } Text textItem = (Text) toAdd; String text = textItem.getText(); // check if the default date has already been put on this item if (text.startsWith(date1 + rightSeparator)) { textItem.removeText(date1 + rightSeparator); dateToAdd = date2 + rightSeparator; prepend = true; } else if (text.startsWith(date2 + rightSeparator)) { textItem.removeText(date2 + rightSeparator); dateToAdd = leftSeparator + date2; append = true; } else if (text.endsWith(leftSeparator + date2)) { textItem.removeEndText(leftSeparator + date2); append = true; dateToAdd = leftSeparator + date1; } else if (text.endsWith(leftSeparator + date1)) { textItem.removeEndText(leftSeparator + date1); if (textItem.getLength() > 0) { dateToAdd = ""; prepend = true; } else { // use the default date format prepend = true; } } if (prepend) { // add the date to the text item textItem.prependText(dateToAdd); if (dateToAdd.length() == textItem.getLength()) { DisplayController.setCursorPosition(textItem.getParagraphEndPosition()); } } else if (append) { textItem.appendText(dateToAdd); if (dateToAdd.length() == textItem.getLength()) { DisplayController.setCursorPosition(textItem.getPosition()); } } else { for (int i = 0; i < date1.length(); i++) { StandardGestureActions.processChar(date1.charAt(i), false); } } textItem.getParent().setChanged(true); DisplayController.requestRefresh(true); // } else { // MessageBay // .displayMessage("Only text items can have the date prepended to // them"); // } // otherwise, create a new text item } else { Text newText = createText(); newText.setText(dateToAdd); DisplayController.getCurrentFrame().addItem(newText); DisplayController.getCurrentFrame().setChanged(true); DisplayController.requestRefresh(true); DisplayController.setCursorPosition(newText.getParagraphEndPosition()); } } public Integer getMask() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.MASK_STR)) { return ((Text) this.getPrimary()).getMask(); } else { return _mask; } } public void setMask(final Integer c) { _mask = c; if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.MASK_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.MASK_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.MASK_STR, inheritanceCheckOnSave); } } } public String getPlaceholder() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.PLACEHOLDER_STR)) { return ((Text) this.getPrimary()).getPlaceholder(); } else { if (_placeholder == null || _placeholder.length() == 0) { return null; } return _placeholder.toString(); } } public void setPlaceholder(String _placeholder) { this._placeholder = new StringBuffer(_placeholder); if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.PLACEHOLDER_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.PLACEHOLDER_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.PLACEHOLDER_STR, inheritanceCheckOnSave); } } } protected List getTextLayouts() { if (this.getPlaceholder() != null && (this._text == null || this._text.length() == 0)) { return _placeholderTextLayouts; } else if (this.getMask() != null) { return _maskTextLayouts; } else { return _textLayouts; } } public boolean isSingleLineOnly() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.SINGLE_LINE_ONLY_STR)) { return ((Text) this.getPrimary()).isSingleLineOnly(); } else { return _singleLine; } } public void setSingleLineOnly(boolean _singleLine) { this._singleLine = _singleLine; if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.SINGLE_LINE_ONLY_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.SINGLE_LINE_ONLY_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.SINGLE_LINE_ONLY_STR, inheritanceCheckOnSave); } } } public int getTabIndex() { if (isSurrogate() && surrogatePropertyInheritance.get(DefaultFrameWriter.TAB_INDEX_STR)) { return ((Text) this.getPrimary()).getTabIndex(); } else { return this._tabIndex; } } public void setTabIndex(int index) { this._tabIndex = index; if (isSurrogate()) { surrogatePropertyInheritance.put(DefaultFrameWriter.TAB_INDEX_STR, false); Item primary = getPrimary(); if (subjectToInheritanceCheckOnSave(DefaultFrameWriter.TAB_INDEX_STR)) { EncryptionDetail inheritanceCheckOnSave = new EncryptionDetail(EncryptionDetail.Type.InheritanceCheckOnSave); primary.primaryPropertyEncryption.put(DefaultFrameWriter.TAB_INDEX_STR, inheritanceCheckOnSave); } } } public Text getTabNext() { if (this._tabIndex >= 0) { Collection textItems = this.getParent().getTextItems(); Text ret = null; for (Text t: textItems) { if (t._tabIndex > this._tabIndex) { if (ret == null) { ret = t; } else if (t._tabIndex < ret._tabIndex) { ret = t; } } } return ret; } else { return null; } } public Text getTabPrevious() { if (this._tabIndex >= 0) { Collection textItems = this.getParent().getTextItems(); Text ret = null; for (Text t: textItems) { if (t._tabIndex < this._tabIndex) { if (ret == null) { ret = t; } else if (t._tabIndex > ret._tabIndex) { ret = t; } } } return ret; } else { return null; } } }