package com.drawbridge.text; import java.awt.Point; import java.util.LinkedList; import java.util.concurrent.CopyOnWriteArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.drawbridge.utils.Utils; /** * This model describes the underlying data for a DBDocument * * @author Alistair Stead * */ public class DBDocumentModel { public static final String NEWLINE = "\n"; private CopyOnWriteArrayList<String> linesModel = null; private String mRawText = null; DBDocument mParent = null; private int mLineWidth; static final Object LOCK = new LinesLock(); static class LinesLock {} static final Object LOCKRAW = new RawTextLock(); static class RawTextLock {} public DBDocumentModel(String inputText, int lineWidth, DBDocument parent) { if (parent == null) { throw new RuntimeException("DBDocumentModel cannot accept null parent!"); } linesModel = new CopyOnWriteArrayList<String>(); mParent = parent; mLineWidth = lineWidth; // Pre-processing (replace tabs) inputText = inputText.replace("\t", " "); setText(inputText); } public final Object getLinesLock() { return LOCK; } private Object getRawTextLock() { return LOCKRAW; } private void textToLines(String inputText) { synchronized(getLinesLock()){ // Reset linesModel linesModel = new CopyOnWriteArrayList<String>(); // split into lines LinkedList<String> initialLines = new LinkedList<String>(); String processedText = inputText; String currentChunk = processedText; String stringPattern = "[^\\\n]*[\\\n]"; // TODO fix this to handle // escaped new lines \\n and // make it use NEWLINE Pattern pattern = Pattern.compile(stringPattern); Matcher matcher = pattern.matcher(currentChunk); boolean found = false; int finalIndex = 0; while (matcher.find()) { found = true; finalIndex = matcher.end(); initialLines.add(matcher.group()); } if (finalIndex < currentChunk.length() && found) { initialLines.add(currentChunk.substring(finalIndex, currentChunk.length())); // Utils.out.println(this.getClass(),currentChunk.substring(finalIndex, // currentChunk.length()).replace(NEWLINE, "\\n")); } if (!found) { initialLines.add(currentChunk); // Utils.out.println(this.getClass(),currentChunk.replace(NEWLINE, // "\\n")); } LinkedList<String> finalLines = new LinkedList<String>(); for (int i = 0; i < initialLines.size(); i++) { String[] tokens = initialLines.get(i).split(" "); int fullWidth = getSizeOf(initialLines.get(i)); if (fullWidth > mLineWidth) { // We have to force wrap if (tokens.length == 1) { LinkedList<String> forcedWrapResult = dealWithLargeChunk(tokens[0], mLineWidth); finalLines.addAll(forcedWrapResult); finalLines.add(new String()); } else { // We can wrap normally if (finalLines.isEmpty()) { finalLines.add(new String()); } int currentLine = finalLines.size() - 1; if (DBDocumentModel.hasTrailingNewLine(finalLines.get(finalLines.size() - 1))) { currentLine++; finalLines.add(new String()); } // Do standard word wrap for (int t = 0; t < tokens.length; t++) { int sizeOfCurrentLine = getSizeOf(finalLines.get(currentLine) + tokens[t] + (t == tokens.length - 1 ? "" : " ")); if (sizeOfCurrentLine <= mLineWidth) { finalLines.set(currentLine, finalLines.get(currentLine) + tokens[t] + (t == tokens.length - 1 ? "" : " ")); } else { currentLine++; LinkedList<String> forcedWrapResult = dealWithLargeChunk(tokens[t], mLineWidth); finalLines.addAll(forcedWrapResult); finalLines.set(finalLines.size() - 1, finalLines.getLast() + (t == tokens.length - 1 ? "" : " ")); } } } } else { // We can just put it there finalLines.add(initialLines.get(i)); } // Add the newline at the end if (i == initialLines.size() - 1) { if (DBDocumentModel.hasTrailingNewLine(finalLines.getLast())) { finalLines.add(new String("")); } } } linesModel.clear(); linesModel.addAll(finalLines); } } public static boolean hasTrailingNewLine(String string) { if (string != null) { if (string.length() >= 1) { return (string.substring(string.length() - 1, string.length()).equals(NEWLINE)); } else return false; } else { return false; } } private LinkedList<String> dealWithLargeChunk(String chunk, int lineWidth) { LinkedList<String> result = new LinkedList<String>(); int maxIndex = 0; for (int i = 0; i < chunk.length(); i++) { int width = getSizeOf(chunk.substring(0, i)); if (width <= lineWidth) { maxIndex = i + 1; } } if (maxIndex > 0) { result.add(chunk.substring(0, maxIndex)); result.addAll(dealWithLargeChunk(chunk.substring(maxIndex, chunk.length()), lineWidth)); } return result; } public int getSizeOf(String string) { if(string == null) return 0; else if (string.length() >= 1) { if (DBDocumentModel.hasTrailingNewLine(string)) { string = string.substring(0, string.length() - 1); } } return mParent.getFontMetrics(mParent.mDocumentFont).stringWidth(string); } public CopyOnWriteArrayList<String> getLines() { return linesModel; } public String getRawText() { return mRawText; } public void setText(String rawText) { // Utils.out.println(getClass(), "setText"); synchronized(getLinesLock()){ linesModel = new CopyOnWriteArrayList<String>(); } synchronized(getRawTextLock()){ this.mRawText = rawText; } synchronized(getLinesLock()){ textToLines(mRawText); } } public void setLineWidth(int paddedWidth) { // Utils.out.println(getClass(), "setLineWidth"); this.mLineWidth = paddedWidth; // Utils.out.println(this.getClass(),"textToLines: " + mRawText); synchronized(getLinesLock()){ textToLines(this.mRawText); } } public int getNumberOfLines() { // Utils.out.println(getClass(), "getNumberOfLines"); synchronized(getLinesLock()){ return linesModel.size(); } } /** * Returns caret position from mouse position * * @param x * - assumed to be relative to text area * @param linePos * @return Point array - 0 contains pixels, 1 contains characters */ public Point getCharPosFromMouseClick(int x, int linePos) { // Utils.out.println(getClass(), "getCharPosFromMouseClick"); synchronized(getLinesLock()){ Point result = new Point(); if (linesModel.size() <= linePos || linePos < 0) throw new RuntimeException("Given invalid line position"); String line = linesModel.get(linePos); int closestIndex = 0; int closestIndexWidthDiff = Integer.MAX_VALUE; if (x <= 0) { result.x = 0; result.y = linePos; return result; } else if (x >= getSizeOf(line)) { if (DBDocumentModel.hasTrailingNewLine(line)) result.x = line.length() - 1; else result.x = line.length(); result.y = linePos; return result; } else { for (int i = 0; i < line.length(); i++) { int newWidthDiff = Math.abs(x - getSizeOf(line.substring(0, i))); if (newWidthDiff < closestIndexWidthDiff) { closestIndex = i; closestIndexWidthDiff = newWidthDiff; } } result.x = closestIndex; result.y = linePos; } return result; } } public void insertCharAt(String input, Point charPosition) { synchronized(getRawTextLock()){ int start = this.getStringIndexFromCharPos(charPosition); String result = this.getRawText().substring(0, start) + input + this.getRawText().substring(start, this.getRawText().length()); this.setText(result); } } public Point getCharPositionLeft(Point charPosition) { // Utils.out.println(getClass(), "getCharPositionLeft"); synchronized(getRawTextLock()){ int index = this.getStringIndexFromCharPos(charPosition); index--; return this.getCharPositionFromStringIndex(index); } } public Point getCharPositionRight(Point charPosition) { // Utils.out.println(getClass(), "getCharPositionRight"); synchronized(getRawTextLock()){ int index = this.getStringIndexFromCharPos(charPosition); index++; return this.getCharPositionFromStringIndex(index); } } public Point getCharPositionUp(Point charPosition) { // Utils.out.println(getClass(), "getCharPositionUp"); if (charPosition.y == 0) { // Do nothing } else { synchronized(getLinesLock()){ int pixelWidth = getSizeOf(linesModel.get(charPosition.y).substring(0, charPosition.x)); charPosition.y--; int lineAboveWidth = getSizeOf(linesModel.get(charPosition.y)); if (lineAboveWidth > pixelWidth) { int smallestDiffIndex = 0; int smallestDiff = Integer.MAX_VALUE; int length = DBDocumentModel.hasTrailingNewLine(linesModel.get(charPosition.y)) ? linesModel.get(charPosition.y).length() - 1 : linesModel.get(charPosition.y).length(); for (int i = 0; i < length; i++) { int newWidthDiff = Math.abs(pixelWidth - getSizeOf(linesModel.get(charPosition.y).substring(0, i))); if (newWidthDiff < smallestDiff) { smallestDiffIndex = i; smallestDiff = newWidthDiff; } } charPosition.x = smallestDiffIndex; } else { charPosition.x = linesModel.get(charPosition.y).length() - (DBDocumentModel.hasTrailingNewLine(linesModel.get(charPosition.y)) ? 1 : 0); } } } return new Point(charPosition.x, charPosition.y); } public Point getCharPositionDown(int charX, int lineY) { // Utils.out.println(getClass(), "getCharPositionDown"); // TODO Down and Up adhere to the last click position, not just directly // up and down synchronized(getLinesLock()){ if (lineY == linesModel.size() - 1) { // Do nothing } else { int pixelWidth = getSizeOf(linesModel.get(lineY).substring(0, charX)); lineY++; int lineAboveWidth = getSizeOf(linesModel.get(lineY)); if (lineAboveWidth > pixelWidth) { int smallestDiffIndex = 0; int smallestDiff = Integer.MAX_VALUE; for (int i = 0; i < linesModel.get(lineY).length(); i++) { int newWidthDiff = Math.abs(pixelWidth - getSizeOf(linesModel.get(lineY).substring(0, i))); if (newWidthDiff < smallestDiff) { smallestDiffIndex = i; smallestDiff = newWidthDiff; } } charX = smallestDiffIndex; } else { charX = linesModel.get(lineY).length(); } } return new Point(charX, lineY); } } public Point backspace(Point charPosition) { // Utils.out.println(getClass(), "backspace"); synchronized(getRawTextLock()){ int stringPosition = this.getStringIndexFromCharPos(charPosition); String result = this.getRawText().substring(0, Math.min(this.getRawText().length(), stringPosition)); Point backspacePoint = this.getCharPositionLeft(charPosition); result = this.getRawText().substring(0, stringPosition - 1) + this.getRawText().substring(stringPosition, this.getRawText().length()); this.setText(result); return backspacePoint; } } public void printRawText() { // Utils.out.println(getClass(), "printRawText"); synchronized(getRawTextLock()){ String longString = ""; for (int i = 0; i < linesModel.size(); i++) { longString += linesModel.get(i); // Utils.out.println(this.getClass(),linesModel.get(i).replace(NEWLINE, // "\\n")); } Utils.out.println(this.getClass(), longString.replace(NEWLINE, "\\n")); } } /** * Gets the word char positions for a given char. * * @param nearestCharPos * @return */ public Point[] getWordBoundaries(Point nearestCharPos) { Point[] result = new Point[2]; // Go through using a regex and check if the nearestCharPos is in the // token it;e int posInString = getStringIndexFromCharPos(nearestCharPos); String stringPattern = "[A-Za-z0-9']+"; Pattern pattern = Pattern.compile(stringPattern); Matcher matcher = pattern.matcher(this.getRawText()); int indexStart = posInString; int indexEnd = posInString; while (matcher.find()) { if (matcher.start() <= posInString && posInString <= matcher.end()) { indexStart = matcher.start(); indexEnd = matcher.end(); } } result[0] = getCharPositionFromStringIndex(indexStart); result[1] = getCharPositionFromStringIndex(indexEnd); return result; } public int getStringIndexFromCharPos(Point charPos) { // Utils.out.println(getClass(), "getStringIndexFromCharPos"); synchronized(getLinesLock()){ int result = 0; for (int i = 0; i <= charPos.y && i < linesModel.size(); i++) { if (i != charPos.y) { result += linesModel.get(i).length(); } else { result += charPos.x; } } return result; } } public Point getCharPositionFromStringIndex(int index) { // Utils.out.println(getClass(), "getCharPositionFromStringIndex"); Point result = new Point(0, 0); try { int indexLeft = index; int i = 0; synchronized(getLinesLock()){ for (; indexLeft - linesModel.get(i).length() > 0; i++) { indexLeft -= linesModel.get(i).length(); } result.y = i; result.x = indexLeft; // Overflow if need be if (linesModel.get(i).length() == indexLeft && DBDocumentModel.hasTrailingNewLine(linesModel.get(i))) { result.y++; result.x = 0; } } } catch (IndexOutOfBoundsException e) { Utils.err.println(getClass(), "IndexOutOfBoundsException!"); return new Point(0, 0); } return result; } /** * Allows removal of selected text * * @param markCharPosition * @param dotCharPosition */ public void remove(Point start, Point finish) { // Utils.out.println(getClass(), "remove"); int positionLeft = getStringIndexFromCharPos((DBDocumentModel.isCharPosLessThan(start, finish)) ? start : finish); int positionRight = getStringIndexFromCharPos((DBDocumentModel.isCharPosLessThan(start, finish)) ? finish : start); String newText = this.getRawText(); synchronized(getRawTextLock()){ newText = this.getRawText().substring(0, positionLeft) + this.getRawText().substring(positionRight, this.getRawText().length()); } this.setText(newText); } public boolean isDiallableInteger(Point start, Point finish) { String text = getRawTextSubstring(start, finish); String[] splitResult = text.split("[-]?[0-9]+"); if (splitResult.length == 0) return true; else return false; } public String getRawTextSubstring(Point start, Point finish) { // Utils.out.println(getClass(), "getRawTextSubstring"); int positionLeft, positionRight, positionLeftExtra, positionRightExtra; synchronized(getLinesLock()){ positionLeft = getStringIndexFromCharPos((DBDocumentModel.isCharPosLessThan(start, finish)) ? start : finish); positionRight = getStringIndexFromCharPos((DBDocumentModel.isCharPosLessThan(start, finish)) ? finish : start); positionLeftExtra = Math.min(Math.max(0, positionLeft), this.getRawText().length()); positionRightExtra = Math.max(Math.min(positionRight, this.getRawText().length()), 0); } synchronized(getRawTextLock()){ try { return this.getRawText().substring(Math.max(0, positionLeftExtra), Math.min(positionRightExtra, this.getRawText().length())); } catch (StringIndexOutOfBoundsException e) { Utils.err.println("Error in getRawTextSubstring:" + e.getMessage()); return ""; } } } public void replace(Point start, Point finish, String replacementString) { // Utils.out.println(getClass(), "replace"); String replacement = getRawText(); synchronized(getRawTextLock()){ int positionLeft = getStringIndexFromCharPos((DBDocumentModel.isCharPosLessThan(start, finish)) ? start : finish); int positionRight = getStringIndexFromCharPos((DBDocumentModel.isCharPosLessThan(start, finish)) ? finish : start); replacement = this.getRawText().substring(0, positionLeft); replacement += replacementString; replacement += this.getRawText().substring(positionRight, this.getRawText().length()); } this.setText(replacement); } /** * Returns true if the points are in the correct order * * @param p1 * @param p2 * @return */ public static boolean isCharPosLessThan(Point p1, Point p2) { if (p1.y < p2.y) { return true; } else if (p1.y == p2.y) { if (p1.x <= p2.x) { return true; } else { return false; } } else { return false; } } public Point getCoordinatesOfChar(Point p) { // Utils.out.println(getClass(), "getCoordinatesOfChar"); synchronized (getLinesLock()) { if (p.y < linesModel.size() && linesModel.get(p.y) != null && p.x < linesModel.get(p.y).length() && p.x < getSizeOf(linesModel.get(p.y))) { int x = mParent.getInsets().left; x += getSizeOf(linesModel.get(p.y).substring(0, p.x)); int y = mParent.getInsets().top + ((mParent.getLineHeight() + mParent.getLineSpacing()) * p.y) + (mParent.getLineHeight() / 2); return new Point(x, y); } else { return null; } } } public void delete(Point leftPosition) //onParseChange { // Utils.out.println(getClass(), "delete"); String oldRawText = getRawText(); Utils.out.println("oldRawTextLength:" + oldRawText.length()); int index = this.getStringIndexFromCharPos(leftPosition); if (index > -1 && oldRawText.length() > 0 && index < oldRawText.length()) { Utils.out.println("Index:" + index + " textLength:" + oldRawText.length()); oldRawText = (oldRawText.substring(0, index) + oldRawText.substring(index + 1, oldRawText.length())); setText(oldRawText); } } }