/** * 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 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.CombinationBoxBounds; import org.expeditee.core.bounds.PolygonBounds; 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.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 _maxWidth = -Integer.MAX_VALUE; 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; /** 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 private StringBuffer _text = new StringBuffer(); private List _textLayouts = new LinkedList(); // The font to display this text in private Font _font; 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); } } System.out.println(); } } } /** * 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); } /** <<<<<<< .mine * Sets the maximum width of this Text item when justification is used. * passing in 0 or -1 means there is no maximum width ||||||| .r1094 * Sets the maximum width of this Text item when justifcation is used. * passing in 0 or -1 means there is no maximum width ======= * Sets the maximum width of this Text item when justifcation is used. passing * in 0 or -1 means there is no maximum width >>>>>>> .r1100 * * @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.getFramePaintArea().getWidth(), false); return; } _maxWidth = width; rebuild(true); invalidateAll(); } /** <<<<<<< .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 (_maxWidth == null || _maxWidth <= 0) return null; return _maxWidth; } public Integer getAbsoluteWidth() { if (_maxWidth == null) { return Integer.MAX_VALUE; } return Math.abs(_maxWidth); } @Override public Colour getHighlightColor() { if (_highlightColour.equals(getPaintBackgroundColor())) return ALTERNATE_HIGHLIGHT; 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(); } /** * 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 (_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); } /** * 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') /* && ch != '\n' */ 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(_textLayouts.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 > _textLayouts.size() - 1) return new Point(getX(), getY()); TextLayout last = _textLayouts.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.x, p.y); } 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() + _textLayouts.get(line).getStartCharIndex(); // Clear the last selected updateLastSelected(); invalidateAll(); } public void setSelectionEnd(Point p) { setSelectionEnd(p.x, p.y); } 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() + _textLayouts.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(String text, float mouseX, float mouseY, int insertPos) { TextHitInfo hit; TextLayout current = null; int lineIndex; invalidateAll(); // check for empty string if (text == null || text.length() == 0) return new Point((int) mouseX, (int) mouseY); // if there is no text yet if (_text == null || _text.length() == 0) { _text = new StringBuffer().append(text); // create the linebreaker and layouts rebuild(true); assert (_textLayouts.size() == 1); current = _textLayouts.get(0); hit = current.getNextRightHit(0); lineIndex = 0; // otherwise, we are inserting text } 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() + _textLayouts.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 < _textLayouts.size(); i++) { if (_textLayouts.get(i).getEndCharIndex() + 1 >= insertionIndex) { newLine = i; break; } } current = _textLayouts.get(newLine); insertionIndex -= current.getStartCharIndex(); if (newLine == lineIndex) { if (insertionIndex > 0) hit = current.getNextRightHit(insertionIndex - 1); else hit = current.getNextLeftHit(1); } else if (newLine < lineIndex) { hit = current.getNextRightHit(insertionIndex - 1); } else { hit = current.getNextRightHit(insertionIndex - 1); } lineIndex = newLine; } // move the cursor to the new location float[] caret = current.getCaretInfo(hit); float y = getLineDrop(current) * lineIndex; float x = getX() + caret[0] + getJustOffset(current); x = Math.min( x, (getX() - Item.MARGIN_RIGHT - (2 * getGravity()) + getBoundsWidth()) ); invalidateAll(); return new Point(Math.round(x), Math.round(getY() + y + caret[1])); } /** * */ 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 = _textLayouts.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, _textLayouts.size() - 1); hit = getCharPosition(line, mouseX); if (direction == LEFT) { if (hit.getInsertionIndex() > 0) { char prevChar = ' '; do { hit = _textLayouts.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 + _textLayouts.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() + _textLayouts.get(line).getStartCharIndex()); // This takes care of hard line break in if (line > 0 && nextChar == '\n') { line--; hit = _textLayouts.get(line).getNextRightHit(_textLayouts.get(line).getCharacterCount() - 1); } // This takes care of soft line breaks. } else if (line > 0) { line--; hit = _textLayouts.get(line).getNextRightHit(_textLayouts.get(line).getCharacterCount() - 1); // Skip the spaces at the end of a line with soft linebreak while (hit.getCharIndex() > 0 && _text.charAt(_textLayouts.get(line).getStartCharIndex() + hit.getCharIndex() - 1) == ' ') { hit = _textLayouts.get(line).getNextLeftHit(hit); } } } else if (direction == RIGHT) { if (hit.getInsertionIndex() < _textLayouts.get(line).getCharacterCount()) { hit = _textLayouts.get(line).getNextRightHit(hit); // Skip whole word if needs be while (wholeWord && hit.getCharIndex() > 0 && hit.getCharIndex() < _textLayouts.get(line).getCharacterCount() && Character.isLetterOrDigit(_text.charAt(_textLayouts.get(line).getStartCharIndex() + hit.getCharIndex() - 1))) { hit = _textLayouts.get(line).getNextRightHit(hit); } } else if (line < _textLayouts.size() - 1) { line++; hit = _textLayouts.get(line).getNextLeftHit(1); } } current = _textLayouts.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.x, resultPos.y); 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(int line, float mouseX) { if (line < 0 || line >= _textLayouts.size()) return null; TextLayout layout = _textLayouts.get(line); mouseX += getOffset().x; 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().y; float y = getY(); for (TextLayout text : _textLayouts) { // 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 _textLayouts.indexOf(text); // check if the cursor is between lines if (mouseY - y < bounds.getMinY()) return Math.max(0, _textLayouts.indexOf(text) - 1); y += getLineDrop(text); } return _textLayouts.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(); // all decoding occurs in the Utils class _font = font; // rejustify(); rebuild(false); invalidateAll(); } /** * Gets the font of this text item. * * @return The Font assigned to this text item, or null if none is assigned. */ public Font getFont() { 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() { if (getFont() == null) return EcosystemManager.getFontManager().getDefaultFont(); return getFont(); } 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 (_textLayouts.size() == 0) { rebuild(false); } for (TextLayout layout : _textLayouts) { String text = layout.getLine().replaceAll("\n", ""); if (!text.equals("")) list.add(text); } return list; } catch (Exception e) { System.out.println(e.getMessage()); return null; } } public String getText() { 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(); } /** * Returns the inter-line spacing (in pixels) of this Text. * * @return The spacing (inter-line) in pixels of this Text. */ public float getSpacing() { 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. */ private 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; } public int getWordSpacing() { 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); } /** * @return The spacing (proportional to the font size) between letters. See * {@link java.awt.font.TextAttribute#TRACKING} */ public float getLetterSpacing() { return _letter_spacing; } public void setInitialSpacing(float spacing) { _initial_spacing = spacing; } public float getInitialSpacing() { 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 : _textLayouts) { 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.x, mousePosition.y, 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().x; mouseY += getOffset().y; 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; } for (TextLayout text : _textLayouts) { // 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. */ public AxisAlignedBoxBounds updateBounds() { // if there is no text, there is nothing to do if (_text == null) return null; if (_textLayouts == null || _textLayouts.size() < 1) return null; int preChangeWidth = 0; if (getOldBounds() != null) { preChangeWidth = AxisAlignedBoxBounds.getEnclosing(getOldBounds()).getWidth(); } int minX = Integer.MAX_VALUE; int maxX = Integer.MIN_VALUE; int minY = Integer.MAX_VALUE; int maxY = Integer.MIN_VALUE; float y = -1; // Fix concurrency error in ScaleFrameset List tmpTextLayouts; synchronized (_textLayouts) { tmpTextLayouts = new LinkedList(_textLayouts); } for (TextLayout layout : tmpTextLayouts) { AxisAlignedBoxBounds bounds = layout.getLogicalHighlightShape(0, layout.getCharacterCount()); if (y < 0) y = 0; else y += getLineDrop(layout); maxX = Math.max(maxX, (int) bounds.getMaxX()); minX = Math.min(minX, (int) bounds.getMinX()); maxY = Math.max(maxY, (int) (bounds.getMaxY() + y)); minY = Math.min(minY, (int) (bounds.getMinY() + 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(); } AxisAlignedBoxBounds ret = new AxisAlignedBoxBounds(getX() + minX - getGravity(), getY() + minY - getGravity(), 2 * getGravity() + maxX - minX, 2 * getGravity() + maxY - minY); 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); } } 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) { // Frame parent = getParent(); // if(parent != null) // parent.removeItem(this); return; } EcosystemManager.getTextLayoutManager().releaseLayouts(_textLayouts); if (_textLayouts != null) _textLayouts.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().getItems()) { 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.MAX_VALUE; if (limitWidth) { if(_maxWidth == null) { width = DisplayController.getFramePaintArea().getWidth() - getX(); } else { width = getAbsoluteWidth(); } } _textLayouts = EcosystemManager.getTextLayoutManager().layoutString(_text.toString(), getPaintFont(), 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 (_maxWidth != null); if (_maxWidth == null) { justify(false); } return _maxWidth > 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 (_textLayouts.get(line).getStartCharIndex() > selectionRight) return null; // if the selection is before this line, return null if (_textLayouts.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 - _textLayouts.get(line).getStartCharIndex()); // int end = Math.min(_lineOffsets.get(line) + // _textLayouts.get(line).getCharacterCount(), _selectionEnd); int end = Math.min(selectionRight - _textLayouts.get(line).getStartCharIndex(), _textLayouts.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; // if there is no text to paint, do nothing. if (_text == null || _text.length() == 0) return; if (_autoWrap || ExperimentalFeatures.AutoWrap.get()) { invalidateAll(); rebuild(true); } else if (_textLayouts.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((float) 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)); } Colour selectionColour = getSelectionColour(); // width -= getX(); // int line = 0; // boolean tab = false; synchronized (_textLayouts) { for (int i = 0; i < _textLayouts.size(); i++) { TextLayout layout = _textLayouts.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); } 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(getWidthToSave()); copy.setJustification(getJustification()); copy.setLetterSpacing(getLetterSpacing()); copy.setWordSpacing(getWordSpacing()); copy.setWidth(getWidthToSave()); copy.setFont(getFont()); if (hasFormula()) { copy.calculate(getFormula()); } else { copy.setText(_text.toString()); } copy.setHidden(!isVisible()); return copy; } @Override public float 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(); } @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.x, newPoint.y, 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.x, newPoint.y, 0)); } else { newPoint.set(insertText("\b", mouseX, mouseY, 1)); } } FrameUtils.setLastEdited(this); rebuild(true); DisplayController.setCursorPosition(newPoint.x, newPoint.y, 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.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() { setPosition(MARGIN_LEFT, MARGIN_LEFT + getBoundsHeight()); Frame modelFrame = getParentOrCurrentFrame(); if (modelFrame != null) { setRightMargin(modelFrame.getNameItem().getX() - 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; } 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 : _textLayouts) { // 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.getFramePaintArea().getWidth(); /* * 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); } 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.getFramePaintArea().getWidth(); // 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) { } } } 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.getFramePaintArea().getSize(); if (maxSize != null) { // setMaxWidth(maxSize.width); setPosition(maxSize.width - getBoundsWidth(), getBoundsHeight()); } } @Override protected int getLinkYOffset() { if (_textLayouts.size() == 0) return 0; return Math.round(-(_textLayouts.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'; } 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 text.trim().length() == 0 || super.dontSave(); } @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 */ 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(_textLayouts.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(); } @Override public void setAnchorRight(Integer anchor) { if (!isLineEnd()) { super.setAnchorRight(anchor); // Subtract off the link width if (anchor != null) { setX(DisplayController.getFramePaintArea().getWidth() - anchor - getBoundsWidth() + getLeftMargin()); } return; } invalidateFill(); invalidateCommonTrait(ItemAppearence.PreMoved); this._anchoring.setRightAnchor(anchor); int oldX = getX(); if (anchor != null) { float deltaX = DisplayController.getFramePaintArea().getWidth() - anchor - getBoundsWidth() + getLeftMargin() - oldX; anchorConnected(AnchorEdgeType.Right, deltaX); } invalidateCommonTrait(ItemAppearence.PostMoved); invalidateFill(); } @Override public void setAnchorTop(Integer anchor) { if (!isLineEnd()) { super.setAnchorTop(anchor); if (anchor != null) { setY(anchor + _textLayouts.get(0).getAscent()); } 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(); } @Override public void setAnchorBottom(Integer anchor) { if (!isLineEnd()) { super.setAnchorBottom(anchor); if (anchor != null) { setY(DisplayController.getFramePaintArea().getHeight() - (anchor + this.getBoundsHeight() - _textLayouts.get(0).getAscent() - _textLayouts.get(0).getDescent())); } return; } invalidateFill(); invalidateCommonTrait(ItemAppearence.PreMoved); this._anchoring.setBottomAnchor(anchor); int oldY = getY(); if (anchor != null) { float deltaY = DisplayController.getFramePaintArea().getHeight() - anchor - oldY; anchorConnected(AnchorEdgeType.Bottom, deltaY); } invalidateCommonTrait(ItemAppearence.PostMoved); invalidateFill(); } @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(_textLayouts.get(0)); int cumulativeHeight = rect.getSize().height; int maxWidth = rect.getSize().width; if (_textLayouts.size() > 1) { for (int i = 1; i < _textLayouts.size(); i++) { final AxisAlignedBoxBounds r = getPixelBounds(_textLayouts.get(i)); cumulativeHeight += _textLayouts.get(i).getDescent() + _textLayouts.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(_textLayouts.get(0)); if (_textLayouts.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() + _textLayouts.get(0).getDescent())); int y = (int) (rect.getMaxY() + _textLayouts.get(0).getDescent()); for (int i = 1; i < _textLayouts.size(); i++) { final AxisAlignedBoxBounds r = getPixelBounds(_textLayouts.get(i)); poly.addPoint(r.getMaxX(), y); poly.addPoint(r.getMaxX(), Math.round(y + r.getHeight() + _textLayouts.get(i).getDescent())); y = Math.round(y + r.getHeight() + _textLayouts.get(i).getDescent()); } poly.addPoint(rect.getMinX() + getPixelBounds(_textLayouts.get(_textLayouts.size() - 1)).getWidth(), Math.round(y + _textLayouts.get(_textLayouts.size() - 1).getDescent())); poly.addPoint(rect.getMinX(), Math.round(y + _textLayouts.get(_textLayouts.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; // // int min_yt = Integer.MAX_VALUE; // int max_yb = Integer.MIN_VALUE; // // // 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() { return _autoWrap; } // workaround since true is the default value and would not be displayed // normally public String getAutoWrapToSave() { if (!_autoWrap) { return null; } return "true"; } public void setAutoWrap(boolean autoWrap) { _autoWrap = autoWrap; } /** * 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.x, newMouse.y, 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()); } } }