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);
}
}
}