//NEXT: optimise, e.g. by storing character lengths, fragment lengths, line heights
//BUG: placing the cursor at the start of a subline renders the cursor at the far right of that line :-S
//NOTE: we could potentially separate out the text storage and management logic into a separate API TextModel
// - and consider simplifying getTextRegion() similar to deleteSelection(), again avoiding the from-start scan :-S
// - it's maybe worth totally reworking insertTextTo(), it's extremely complicated and is only called from one place!
//this is the horizontal border between the textual contents of the TextArea, and the outer edge of its background
const int TEXT_BORDER_H = 3
//and the vertical border
const int TEXT_BORDER_V = 1
const int CURSOR_WIDTH = 1
const byte CONTEXT_MENU = 0
const byte CONTEXT_HINT = 1
const int SCR_BUTTON_HEIGHT = 20
const int SCR_BUTTON_WIDTH = 20
const int LINE_NUMBER_MARGIN = 8
uses IOWindow
//each line of text is made of a list of text fragments
data TextFrag {
const byte T_TEXT = 1 //regular text, can be expanded and contracted
const byte T_PARTICLE = 2 //a particle, like a space or full-stop, which act as word-wrap points, and aren't expanded or contracted
const byte T_TAB = 3 //a tab
byte type
StringUTF value
TextFrag next
TextFrag prev
}
//text on a "logical" line may be split over multiple display lines, e.g. in a word-wrap mode
data TextContent {
TextFrag frags
TextFrag fragsEnd
TextContent next
TextContent prev
}
data TextLine {
TextContent text
TextContent textEnd
int charLength
int pixelLength
int lineCount
//these references track the in-order list of lines in the text area
TextLine next
TextLine prev
//these references are used to keep a sorted list of line-lengths (by pixel length) for scroll area calculations
TextLine nextLen
TextLine prevLen
}
data CursorPos {
int character
int yPosition
TextLine line
TextContent subline
int subCharacter
}
component provides TextArea(Destructor) requires io.Output out, data.IntUtil iu, ui.Font, os.SystemInfo sysInfo, os.Clipboard clipboard, locale.KeyMapping keyMapping, encoding.StringUTF, ScrollBar, ScrollBar:h, data.StringUtil stringUtil {
Mutex textLock = new Mutex()
TextLine lines
TextLine lastLine
TextLine shortestLine
TextLine longestLine
//number of actual lines
int lineCount
//number of "virtual" or "view" lines, which may be greater than actual lines rent when word-wrap is enabled
int lineCountV
//NOTE: these could both be CursorPos instances...?
int cursorPosition
TextLine cursorLine
int cursorPositionY
TextContent cursorSubline
int selectStartPos
TextLine selectStartLine
int selectStartY
TextContent selectSubline
Font textFont
int width = 40
int height = 20
int textAreaWidth = 20
int textAreaHeight = 20
int cursorHeight = 20
int lineNumberAreaWidth = 0
int TAB_SPACES = 4
int spaceWidth = 2
bool focus
bool shiftDown
bool mouseDragCapture
byte keyState
bool showScrollV = true
ScrollBar scrollV
int yScroll
bool showScrollH = true
ScrollBar scrollH
int xScroll
GraphicsObject clickDown
bool lineNumbers = false
bool wordWrap = false
bool autoIndent = false
byte cursorType
int SCROLL_TICKS = 20
ContextMenuSpec menu
MenuItem cutMenu = new MenuItem("Cut", "Ctrl+X")
MenuItem copyMenu = new MenuItem("Copy", "Ctrl+C")
MenuItem pasteMenu = new MenuItem("Paste", "Ctrl+V")
MenuItem selectAllMenu= new MenuItem("Select All", "Ctrl+A")
HotKey hkeys[]
HotKey hkCut = new HotKey(KeyState.KEY_CTRL, KeyCode.X)
HotKey hkCopy = new HotKey(KeyState.KEY_CTRL, KeyCode.C)
HotKey hkPaste = new HotKey(KeyState.KEY_CTRL, KeyCode.V)
HotKey hkSelectAll = new HotKey(KeyState.KEY_CTRL, KeyCode.A)
Color bgColor = new Color(250, 250, 255, 255)
Color borderColor = new Color(180, 180, 190, 255)
Color highlightColor = new Color(160, 160, 200, 255)
Color textColor = new Color(0, 0, 0, 255)
Color textColorLN = new Color(0, 0, 90, 255)
TextArea:TextArea()
{
textFont = new Font(sysInfo.getSystemFont(false), 15)
menu = new ContextMenuSpec()
menu.items = new MenuItem[](cutMenu,
copyMenu,
pasteMenu,
selectAllMenu
)
hkeys = new HotKey[](hkCut,
hkCopy,
hkPaste,
hkSelectAll
)
height = textFont.getFontMetrics().height + (TEXT_BORDER_V*2)
cursorHeight = textFont.getFontMetrics().height + (TEXT_BORDER_V*2)
SCROLL_TICKS = cursorHeight
lines = makeLine()
lastLine = lines
cursorLine = lines
selectStartLine = cursorLine
shortestLine = lines
longestLine = lines
scrollV = new ScrollBar()
scrollV.setPosition(width - SCR_BUTTON_WIDTH, 0)
scrollH = new ScrollBar:h()
scrollH.setPosition(0, height - SCR_BUTTON_WIDTH)
spaceWidth = textFont.getTextWidth(" ")
sinkevent ScrollEvents(scrollV)
sinkevent ScrollEvents(scrollH)
}
TextLine makeLine()
{
TextLine newLine = new TextLine(new TextContent(new TextFrag(TextFrag.T_TEXT, new StringUTF(""))))
newLine.textEnd = newLine.text
newLine.text.fragsEnd = newLine.text.frags
newLine.lineCount = 1
return newLine
}
void insertFragAfter(TextLine line, TextFrag newFrag, TextFrag after)
{
if (after.next != null)
after.next.prev = newFrag
else
line.textEnd.fragsEnd = newFrag
newFrag.next = after.next
newFrag.prev = after
after.next = newFrag
}
void insertTab(TextLine line, int atCharacter)
{
TextFrag newFrag = new TextFrag(TextFrag.T_TAB, new StringUTF("\t"))
if (atCharacter == 0)
{
newFrag.next = line.text.frags
line.text.frags.prev = newFrag
line.text.frags = newFrag
}
TextFrag fw = line.text.frags
int pos = atCharacter
while (fw != null)
{
if (fw.type == TextFrag.T_TEXT)
{
if (fw.value.length() == pos)
{
//insert the tab after this text section
insertFragAfter(line, newFrag, fw)
break
}
else if (pos < fw.value.length())
{
//break this text section into two, with the tab inserted in the middle
TextFrag fragB = new TextFrag(TextFrag.T_TEXT, new StringUTF(fw.value.subString(pos, fw.value.length() - pos)))
fw.value = new StringUTF(fw.value.subString(0, pos))
insertFragAfter(line, fragB, fw)
insertFragAfter(line, newFrag, fw)
break
}
else
{
pos -= fw.value.length()
}
}
else
{
if (pos == 1)
{
//insert the tab after this tab
insertFragAfter(line, newFrag, fw)
break
}
else
{
pos --
}
}
fw = fw.next
}
}
void appendTab(TextLine line)
{
if (line.textEnd.frags === line.textEnd.fragsEnd && line.textEnd.frags.value.length() == 0)
{
line.textEnd.frags.type = TextFrag.T_TAB
line.textEnd.frags.value.append("\t")
}
else
{
TextFrag newFrag = new TextFrag(TextFrag.T_TAB, new StringUTF("\t"))
newFrag.prev = line.textEnd.fragsEnd
line.textEnd.fragsEnd.next = newFrag
line.textEnd.fragsEnd = newFrag
}
}
//given an absolute cursor position on a line, return the relative position on the given subline
int getSublinePos(TextLine line, TextContent c, int pos)
{
TextContent cw = line.text
while (cw !== c && cw != null)
{
pos -= getSublineCharLength(cw)
cw = cw.next
}
return pos
}
//given a relative position on a given subline, return the absolute cursor position on the line
int getLineCursorPos(TextLine line, TextContent c, int pos)
{
TextContent cw = line.text
int len = 0
while (cw !== c)
{
len += getSublineCharLength(cw)
cw = cw.next
}
return len + pos
}
int getSublineCharLength(TextContent cw)
{
int len = 0
TextFrag fw = cw.frags
while (fw != null)
{
if (fw.type == TextFrag.T_TEXT || fw.type == TextFrag.T_PARTICLE)
{
len += fw.value.length()
}
else
{
len ++
}
fw = fw.next
}
return len
}
int getLineCharLength(TextLine line)
{
int len = 0
TextContent cw = line.text
while (cw != null)
{
len += getSublineCharLength(cw)
cw = cw.next
}
return len
}
eventsink ScrollEvents(EventData ed)
{
if (ed.source === scrollV && ed.type == ScrollBar.[scrollMoved])
{
yScroll = scrollV.getScrollPos()
}
else if (ed.source === scrollV && ed.type == ScrollBar.[repaint])
{
postRepaint()
}
else if (ed.source === scrollH && ed.type == ScrollBar.[scrollMoved])
{
xScroll = scrollH.getScrollPos()
}
else if (ed.source === scrollH && ed.type == ScrollBar.[repaint])
{
postRepaint()
}
}
void TextArea:setBackground(store Color c)
{
bgColor = c
postRepaint()
}
void TextArea:setBorder(store Color c)
{
borderColor = c
postRepaint()
}
void TextArea:setHighlight(store Color c)
{
highlightColor = c
postRepaint()
}
void TextArea:setTextColor(store Color c)
{
textColor = c
postRepaint()
}
void TextArea:setFont(store Font f)
{
textFont = f
cursorHeight = textFont.getFontMetrics().height + (TEXT_BORDER_V*2)
spaceWidth = textFont.getTextWidth(" ")
if (wordWrap) wrapLines()
postRepaint()
}
char[] getCharacters(TextLine line, int start, int len)
{
char result[] = null
TextContent tc = line.text
TextFrag fw = tc.frags
while (start > fw.value.length())
{
start -= fw.value.length()
fw = fw.next
if (fw == null)
{
tc = tc.next
fw = tc.frags
}
}
//out.println("start: $start len $len [$(fw.value.length())]")
if (start + len <= fw.value.length())
{
result = new char[](result, fw.value.subString(start, len))
return result
}
else
{
int dlen = (fw.value.length() - start)
len -= dlen
result = new char[](result, fw.value.subString(start, dlen))
fw = fw.next
if (fw == null)
{
tc = tc.next
fw = tc.frags
}
}
while (len >= fw.value.length())
{
TextFrag td = fw
len -= fw.value.length()
result = new char[](result, fw.value.getRaw())
fw = fw.next
if (len == 0)
{
return result
}
if (fw == null)
{
tc = tc.next
fw = tc.frags
}
}
if (len > 0)
{
result = new char[](result, fw.value.subString(0, len))
}
return result
}
char[] getTextRegion(TextLine startLine, int startPos, TextLine endLine, int endPos, char lineSeparator[])
{
if (startLine !== endLine)
{
//here we use the process from highlight, but to collect those sections (and add newline characters between lines!)
char result[] = null
TextLine ssline = startLine
TextLine seline = endLine
int sspos = startPos
int sepos = endPos
int cy = 0
bool inSelect
TextLine lw = lines
while (lw != null)
{
if (lw === ssline)
{
inSelect = true
result = getCharacters(lw, sspos, getLineCharLength(lw)-sspos)
}
else if (lw === seline)
{
result = new char[](result, lineSeparator, getCharacters(lw, 0, sepos))
}
else if (inSelect)
{
result = new char[](result, lineSeparator, getCharacters(lw, 0, getLineCharLength(lw)))
}
if (lw === seline)
{
inSelect = false
break
}
lw = lw.next
cy += cursorHeight
}
return result
}
else
{
int s = startPos
int e = endPos
return getCharacters(startLine, s, e-s)
}
return null
}
char[] getSelectedText()
{
if (selectStartLine !== cursorLine)
{
//here we use the process from highlight, but to collect those sections (and add newline characters between lines!)
TextLine ssline
TextLine seline
int sspos
int sepos
if (cursorPositionY < selectStartY)
{
ssline = cursorLine
seline = selectStartLine
sspos = cursorPosition
sepos = selectStartPos
}
else
{
ssline = selectStartLine
seline = cursorLine
sspos = selectStartPos
sepos = cursorPosition
}
return getTextRegion(ssline, sspos, seline, sepos, "\n")
}
else
{
int s
int e
if (selectStartPos < cursorPosition)
{
s = selectStartPos
e = cursorPosition
}
else
{
e = selectStartPos
s = cursorPosition
}
return getCharacters(cursorLine, s, e-s)
}
}
void updateScrollMax()
{
//vertical scroll
int maxHeight = (lineCountV * cursorHeight) + cursorHeight
int maxScrollY = 0
if (maxHeight > textAreaHeight)
maxScrollY = maxHeight - textAreaHeight
scrollV.setMaxValue(maxScrollY)
//horizontal scroll
int maxWidth = (longestLine.pixelLength) + CURSOR_WIDTH
int maxScrollX = 0
if (maxWidth > textAreaWidth)
maxScrollX = maxWidth - textAreaWidth
scrollH.setMaxValue(maxScrollX)
}
void removeLine(TextLine td)
{
if (td.prev != null) td.prev.next = td.next
if (td.next != null) td.next.prev = td.prev
if (lines === td) lines = td.next
if (lastLine === td) lastLine = td.prev
td.prev = null
td.next = null
lineCount --
lineCountV -= td.lineCount
td = null
int newWidth = getNumberAreaWidth(lineCount)
if (newWidth != lineNumberAreaWidth)
{
lineNumberAreaWidth = newWidth
updateTextAreaWidth()
if (wordWrap) wrapLines()
}
updateScrollMax()
}
void addLine(TextLine nl)
{
TextLine tw = shortestLine
while ((tw != null) && (nl.pixelLength > tw.pixelLength))
{
tw = tw.nextLen
}
if (tw != null)
{
nl.nextLen = tw
if (tw.prevLen != null)
tw.prevLen.nextLen = nl
else
shortestLine = nl
nl.prevLen = tw.prevLen
tw.prevLen = nl
}
else
{
longestLine.nextLen = nl
nl.prevLen = longestLine
longestLine = nl
}
lineCount ++
lineCountV ++
int newWidth = getNumberAreaWidth(lineCount)
if (newWidth != lineNumberAreaWidth)
{
lineNumberAreaWidth = newWidth
updateTextAreaWidth()
if (wordWrap) wrapLines()
}
updateScrollMax()
}
void removeFrag(TextLine line, TextFrag frag)
{
if (line.text === frag)
{
line.text.frags = frag.next
line.text.frags.prev = null
}
else if (line.textEnd.fragsEnd === frag)
{
line.textEnd.fragsEnd = frag.prev
line.textEnd.fragsEnd.next = null
}
else
{
frag.prev.next = frag.next
frag.next.prev = frag.prev
}
frag.next = null
frag.prev = null
}
void clearLine(TextLine line)
{
TextContent sw = line.text
int lineHeight = 0
while (sw != null)
{
TextContent td = sw
TextFrag fw = sw.frags
while (fw != null)
{
TextFrag ftd = fw
fw = fw.next
ftd.next = null
ftd.prev = null
}
sw = sw.next
td.frags = null
td.fragsEnd = null
td.next = null
td.prev = null
}
line.text = null
line.textEnd = null
line.text = new TextContent(new TextFrag(TextFrag.T_TEXT, new StringUTF("")))
line.textEnd = line.text
line.text.fragsEnd = line.text.frags
updateLineLength(line)
}
void deleteCharacters(TextLine line, int start, int len)
{
//NOTE: this function never deletes TextContent sub-lines because wrapLine() always resets each line, so the work is done there
if (start == 0 && len == getLineCharLength(line))
{
clearLine(line)
}
else
{
TextContent tc = line.text
TextFrag fw = tc.frags
while (start > fw.value.length())
{
start -= fw.value.length()
fw = fw.next
if (fw == null)
{
tc = tc.next
fw = tc.frags
}
}
//out.println("start: $start len $len [$(fw.value.length())]")
if (start + len <= fw.value.length())
{
fw.value.delete(start, len)
return
}
else
{
int dlen = (fw.value.length() - start)
len -= dlen
fw.value.delete(start, dlen)
fw = fw.next
if (fw == null)
{
tc = tc.next
fw = tc.frags
}
}
while (len >= fw.value.length())
{
TextFrag td = fw
len -= fw.value.length()
fw = fw.next
if (td === tc.frags)
{
if (td.next != null) td.next.prev = null
tc.frags = td.next
}
else if (td === tc.fragsEnd)
{
tc.fragsEnd = td.prev
td.prev.next = null
}
else
{
td.prev.next = td.next
td.next.prev = td.prev
}
td.prev = null
td.next = null
if (len == 0)
{
TextContent cd = tc
tc = tc.next
return
}
if (fw == null)
{
TextContent cd = tc
tc = tc.next
fw = tc.frags
}
}
if (len > 0)
{
//TODO: if tc is not the starting one, and this delete would leave the frag empty, delete the frag, and if it's the only frag in the tc, delete the tc as well
fw.value.delete(0, len)
}
}
}
void appendLine(TextLine line, TextLine append)
{
/*
if (line.textEnd.type == TextFrag.T_TEXT && append.text.type == TextFrag.T_TEXT)
{
line.textEnd.value.append(append.text.getRaw())
append.text = append.text.next
append.text.prev = null
}
*/
line.textEnd.fragsEnd.next = append.text.frags
append.text.frags.prev = line.textEnd.fragsEnd
line.textEnd.fragsEnd = append.textEnd.fragsEnd
//if we're appending to an empty line, we'll have a zero-length text starting frag, which we remove (some functions assume no zero-length frags)
if (line.textEnd.frags.type != TextFrag.T_TAB && line.textEnd.frags.value.length() == 0)
{
line.textEnd.frags.next.prev = null
line.textEnd.frags = line.textEnd.frags.next
}
}
void deleteSelection()
{
if (selectStartLine !== cursorLine)
{
//use process from highlight, but to delete those sections
TextLine ssline
TextLine seline
int sspos
int sepos
int yFinish = 0
if (cursorPositionY < selectStartY)
{
ssline = cursorLine
seline = selectStartLine
sspos = cursorPosition
sepos = selectStartPos
yFinish = cursorPositionY
}
else
{
ssline = selectStartLine
seline = cursorLine
sspos = selectStartPos
sepos = cursorPosition
yFinish = selectStartY
}
//deleteCharacters on the end of the starting line, if appropriate
// - delete each intermediate line we reach seline
// - deleteCharacters on seline, if appropriate
// - append the last line to the first line, if appropriate
// (in general we want to minimise the number of places we deal directly with frags/fragsEnd, to scope word-wrap calculations to a few functions)
int e = getLineCharLength(ssline)
deleteCharacters(ssline, sspos, e - sspos)
if (wordWrap) wrapLine(ssline) //deleteCharacters() assumes wrapLine is always called after it, to re-flow fragments over sublines
while (ssline.next !== seline)
{
removeLine(ssline.next)
}
//now we need to join seline onto ssline
if (sepos < getLineCharLength(seline))
{
//join seline onto ssline
deleteCharacters(seline, 0, sepos)
if (wordWrap) wrapLine(seline)
removeLine(seline)
appendLine(ssline, seline)
}
else
{
cursorLine = seline.next
removeLine(seline)
}
cursorLine = ssline
selectStartLine = ssline
selectStartPos = sspos
selectStartY = yFinish
cursorPositionY = yFinish
emitevent textChanged()
}
else
{
int s
int e
if (selectStartPos < cursorPosition)
{
s = selectStartPos
e = cursorPosition
}
else
{
e = selectStartPos
s = cursorPosition
}
if (s != e)
{
deleteCharacters(cursorLine, s, e-s)
emitevent textChanged()
}
}
}
void indentLine(TextLine line)
{
insertTab(line, 0)
}
void indentSelection()
{
TextLine ssline
TextLine seline
if (cursorPositionY < selectStartY)
{
ssline = cursorLine
seline = selectStartLine
}
else
{
ssline = selectStartLine
seline = cursorLine
}
indentLine(ssline)
if (ssline === seline) return
ssline = ssline.next
while (ssline !== seline)
{
indentLine(ssline)
ssline = ssline.next
}
indentLine(ssline)
if (cursorPositionY < selectStartY)
selectStartPos += 1
else
cursorPosition += 1
}
void unindentLine(TextLine line)
{
if (line.text.frags.type == TextFrag.T_TAB)
{
if (line.text.frags.next != null)
{
line.text.frags.next.prev = null
line.text.frags = line.text.frags.next
}
else
{
line.text.frags.type = TextFrag.T_TEXT
line.text.frags.value = new StringUTF("")
}
}
//TODO: else if it's a space, try to delete up to TAB_SPACES of them
}
void unindentSelection()
{
TextLine ssline
TextLine seline
if (cursorPositionY < selectStartY)
{
ssline = cursorLine
seline = selectStartLine
}
else
{
ssline = selectStartLine
seline = cursorLine
}
unindentLine(ssline)
if (ssline === seline)
{
if (cursorPositionY < selectStartY)
selectStartPos -= 1
else
cursorPosition -= 1
return
}
ssline = ssline.next
while (ssline !== seline)
{
unindentLine(ssline)
ssline = ssline.next
}
unindentLine(ssline)
if (cursorPositionY < selectStartY)
selectStartPos -= 1
else
cursorPosition -= 1
}
void appendTextTo(TextLine line, char txt[], opt bool isParticle)
{
if (!isParticle && line.textEnd.fragsEnd.type == TextFrag.T_TEXT)
{
line.textEnd.fragsEnd.value.append(txt)
}
else
{
TextFrag nf
if (line.textEnd.fragsEnd.value.length() == 0)
{
nf = line.textEnd.fragsEnd
nf.value = new StringUTF(txt)
nf.type = TextFrag.T_TEXT
}
else
{
nf = new TextFrag(TextFrag.T_TEXT, new StringUTF(txt))
line.textEnd.fragsEnd.next = nf
nf.prev = line.textEnd.fragsEnd
line.textEnd.fragsEnd = nf
}
if (isParticle) nf.type = TextFrag.T_PARTICLE
}
}
void insertTextTo(TextLine line, int atCharacter, char txt[], opt bool isParticle)
{
if (atCharacter == 0)
{
if (!isParticle && line.text.frags.type == TextFrag.T_TEXT)
{
line.text.frags.value.insert(0, txt)
}
else
{
TextFrag nf = null
if (isParticle)
nf = new TextFrag(TextFrag.T_PARTICLE, new StringUTF(txt))
else
nf = new TextFrag(TextFrag.T_TEXT, new StringUTF(txt))
nf.next = line.text.frags
line.text.frags.prev = nf
line.text.frags = nf
}
return
}
else
{
//search for insertion position
TextContent tc = line.text
int ndx = 0
while (tc != null)
{
TextFrag fw = tc.frags
while (fw != null)
{
int thisLen = fw.value.length()
if (fw.type == TextFrag.T_TAB) thisLen = 1
if ((ndx + thisLen) == atCharacter)
{
//append, in appropriate mode
if (!isParticle && fw.type == TextFrag.T_TEXT)
{
fw.value.append(txt)
}
else
{
TextFrag nf = new TextFrag(TextFrag.T_TEXT, new StringUTF(txt))
if (isParticle) nf.type = TextFrag.T_PARTICLE
nf.next = fw.next
if (fw.next != null)
fw.next.prev = nf
else
tc.fragsEnd = nf
fw.next = nf
nf.prev = fw
}
return
}
else if ((ndx + thisLen) > atCharacter)
{
//insert in the middle, either as text or by splitting fragments
if (!isParticle && fw.type == TextFrag.T_TEXT)
{
fw.value.insert(atCharacter - ndx, txt)
}
else
{
int qpos = atCharacter - ndx
TextFrag nf = new TextFrag(TextFrag.T_TEXT, new StringUTF(fw.value.subString(qpos, fw.value.length() - qpos)))
fw.value.delete(qpos, fw.value.length() - qpos)
//out.println("split frag: '$(fw.value.getRaw())' / '$(nf.value.getRaw())'")
TextFrag nfB = new TextFrag(TextFrag.T_PARTICLE, new StringUTF(txt))
nfB.next = nf
nfB.prev = fw
nf.prev = nfB
nf.next = fw.next
if (fw.next != null)
fw.next.prev = nf
else
line.text.fragsEnd = nf
fw.next = nfB
//out.println("seqe: frag: '$(fw.value.getRaw())' -> '$(fw.next.value.getRaw())' -> '$(fw.next.next.value.getRaw())'")
}
return
}
else
{
//move to next frag
ndx += thisLen
}
fw = fw.next
}
tc = tc.next
}
}
}
bool isFilterCharacter(char k[])
{
if (k == "\r")
return true
return false
}
bool isLineWrapCharacter(char k[])
{
if (k == " ")
return true
return false
}
void appendText(TextLine toLine, char text[])
{
int lastIndex = 0
for (int i = 0; i < text.arrayLength; i++)
{
char k[] = text[i]
if (k == "\n")
{
//make a new line
char nxt[] = text.subString(lastIndex, i - lastIndex)
appendTextTo(toLine, nxt)
updateLineLength(toLine)
TextLine ntl = makeLine()
ntl.next = toLine.next
ntl.prev = toLine
if (toLine.next != null)
toLine.next.prev = ntl
else
lastLine = ntl
toLine.next = ntl
toLine = ntl
cursorPositionY += cursorHeight
addLine(ntl)
lastIndex = i + 1
}
else if (isFilterCharacter(k) || k == "\t" || isLineWrapCharacter(k))
{
if (lastIndex != i)
{
char nxt[] = text.subString(lastIndex, i - lastIndex)
appendTextTo(toLine, nxt)
updateLineLength(toLine)
}
lastIndex = i + 1
if (k == "\t")
{
appendTab(toLine)
}
else if (isLineWrapCharacter(k))
{
appendTextTo(toLine, k, true)
updateLineLength(toLine)
}
}
}
char nxt[] = text.subString(lastIndex, text.arrayLength - lastIndex)
appendTextTo(toLine, nxt)
updateLineLength(toLine)
}
void insertText(char text[])
{
int lastIndex = 0
char appendX[] = null
if (cursorPosition < getLineCharLength(cursorLine))
{
appendX = getCharacters(cursorLine, cursorPosition, getLineCharLength(cursorLine) - cursorPosition)
deleteCharacters(cursorLine, cursorPosition, getLineCharLength(cursorLine) - cursorPosition)
if (wordWrap) wrapLine(cursorLine)
}
appendText(cursorLine, text)
if (appendX != null)
{
appendText(cursorLine, appendX)
updateLineLength(cursorLine)
}
cursorPosition = getLineCharLength(cursorLine)
selectStartLine = cursorLine
selectStartPos = cursorPosition
selectStartY = cursorPositionY
emitevent textChanged()
}
bool isWhitespace(char value[])
{
for (int i = 0; i < value.arrayLength; i++)
{
if (value[i] != " " && value[i] != "\t")
return false
}
return true
}
void splitLineDown()
{
//check if any text comes after this cursor position on the current line; if so move it down to the next line
TextLine ntl = makeLine()
addLine(ntl)
char str[] = null
if (cursorPosition < getLineCharLength(cursorLine))
{
str = getCharacters(cursorLine, cursorPosition, getLineCharLength(cursorLine) - cursorPosition)
deleteCharacters(cursorLine, cursorPosition, getLineCharLength(cursorLine) - cursorPosition)
updateLineLength(cursorLine)
}
//if we're NOT currently on the last line, we need to INSERT a line after the current one
if (cursorLine.next != null)
{
ntl.next = cursorLine.next
ntl.prev = cursorLine
cursorLine.next.prev = ntl
cursorLine.next = ntl
}
else
{
lastLine.next = ntl
ntl.prev = lastLine
lastLine = ntl
}
cursorLine = ntl
cursorSubline = cursorLine.text
selectStartLine = cursorLine
cursorPosition = 0
selectStartPos = 0
bool insertion = false
if (autoIndent)
{
TextFrag fw = ntl.prev.text.frags
while (fw != null)
{
if (!isWhitespace(fw.value.getRaw()))
{
break
}
else
{
insertText(fw.value.getRaw())
insertion = true
}
fw = fw.next
}
}
if (str != null)
{
int prevCP = cursorPosition
insertText(str)
cursorPosition = prevCP
selectStartPos = cursorPosition
insertion = true
}
if (!insertion)
{
cursorPositionY += cursorHeight
selectStartY = cursorPositionY
}
}
void updateLineLength(TextLine line)
{
//re-calculate the length-in-pixels of this line
int preLength = line.pixelLength
line.pixelLength = 0
TextFrag fw = line.text.frags
while (fw != null)
{
if (fw.type == TextFrag.T_TEXT || fw.type == TextFrag.T_PARTICLE)
line.pixelLength += textFont.getTextWidth(fw.value.getRaw())
else
line.pixelLength += (TAB_SPACES * spaceWidth)
fw = fw.next
}
//re-calculate word-wrap status, if appropriate
if (wordWrap) wrapLine(line)
//...and now adjust sorting order relative to lines around us
if (preLength < line.pixelLength && longestLine !== line)
{
TextLine tw = line.nextLen
while ((tw != null) && (line.pixelLength > tw.pixelLength))
{
tw = tw.nextLen
}
if (tw !== line.nextLen)
{
//remove it from its old position
if (line.prevLen != null)
line.prevLen.nextLen = line.nextLen
else
shortestLine = line.nextLen
line.nextLen.prevLen = line.prevLen
//add it to its new position
if (tw != null)
{
tw.prevLen.nextLen = line
line.prevLen = tw.prevLen
tw.prevLen = line
}
else
{
line.prevLen = longestLine
longestLine.nextLen = line
longestLine = line
}
line.nextLen = tw
}
}
else if (line.pixelLength < preLength && shortestLine !== line)
{
//search in the other direction (we got shorter)
TextLine tw = line.prevLen
while ((tw != null) && (line.pixelLength < tw.pixelLength))
{
tw = tw.prevLen
}
if (tw !== line.prevLen)
{
//remove it from its old position
if (line.nextLen != null)
line.nextLen.prevLen = line.prevLen
else
longestLine = line.prevLen
line.prevLen.nextLen = line.nextLen
//add it to its new position
if (tw != null)
{
tw.nextLen.prevLen = line
line.nextLen = tw.nextLen
tw.nextLen = line
}
else
{
line.nextLen = shortestLine
shortestLine.prevLen = line
shortestLine = line
}
line.prevLen = tw
}
}
updateScrollMax()
}
void printLine(char dbg[], TextLine forLine)
{
out.print(dbg)
TextContent sw = forLine.text
int lineHeight = 0
while (sw != null)
{
TextFrag fw = sw.frags
while (fw != null)
{
if (fw.type == TextFrag.T_TEXT || fw.type == TextFrag.T_PARTICLE)
{
out.print("'$(fw.value.getRaw())' ")
}
else
{
out.print("")
}
fw = fw.next
}
sw = sw.next
}
out.println("")
}
bool TextArea:keyDown(int keyCode)
{
//here we check if the key press was a printable character and if so insert it, else handle other key strokes
char ch[] = keyMapping.getCharacter(keyCode, keyState)
byte kCode
if (ch != null)
{
mutex(textLock)
{
if (selectStartLine !== cursorLine || selectStartPos != cursorPosition)
{
deleteSelection()
if (selectStartPos > cursorPosition)
selectStartPos = cursorPosition
else
cursorPosition = selectStartPos
}
if (cursorPosition == getLineCharLength(cursorLine))
{
appendTextTo(cursorLine, ch, isLineWrapCharacter(ch))
}
else
{
insertTextTo(cursorLine, cursorPosition, ch, isLineWrapCharacter(ch))
}
cursorPosition ++
selectStartPos = cursorPosition
//printLine("DBG-UL:", cursorLine)
updateLineLength(cursorLine)
//printLine("DBG-UC:", cursorLine)
}
updateCursorPosition()
//printLine("DBG-TC:", cursorLine)
emitevent textChanged()
postRepaint()
}
else if ((kCode = keyMapping.getKeyCode(keyCode)) != KeyCode.OTHER)
{
if (kCode == KeyCode.SHIFT_LEFT || kCode == KeyCode.SHIFT_RIGHT)
{
keyState |= KeyState.KEY_SHIFT
shiftDown = true
return true
}
if (kCode == KeyCode.CTRL_LEFT || kCode == KeyCode.CTRL_RIGHT)
{
keyState |= KeyState.KEY_CTRL
return true
}
if (kCode == KeyCode.ALT_LEFT)
{
keyState |= KeyState.KEY_ALT
return true
}
if (kCode == KeyCode.ALT_RIGHT)
{
keyState |= KeyState.KEY_ALTGR
return true
}
if (kCode == KeyCode.CMD)
{
keyState |= KeyState.KEY_CMD
return true
}
if (kCode == KeyCode.TAB)
{
mutex(textLock)
{
if (selectStartLine !== cursorLine || (selectStartPos != cursorPosition && (selectStartPos == 0 || cursorPosition == 0)))
{
//indent+/- this text region
if (!shiftDown)
indentSelection()
else
unindentSelection()
}
else
{
if (selectStartPos != cursorPosition)
{
deleteSelection()
if (selectStartPos > cursorPosition)
selectStartPos = cursorPosition
else
cursorPosition = selectStartPos
}
if (cursorPosition == getLineCharLength(cursorLine))
{
appendTab(cursorLine)
}
else
{
insertTab(cursorLine, cursorPosition)
}
cursorPosition ++
selectStartPos = cursorPosition
}
updateLineLength(cursorLine)
updateCursorPosition()
}
}
if (kCode == KeyCode.DELETE)
{
mutex(textLock)
{
if (cursorLine === selectStartLine && cursorPosition == selectStartPos)
{
//deal with cursor at the end of a line, deleting the next line and appending any content to this one
if (cursorPosition < getLineCharLength(cursorLine))
{
selectStartPos = cursorPosition + 1
deleteSelection()
selectStartPos --
updateLineLength(cursorLine)
}
else if (cursorLine.next != null)
{
TextLine td = cursorLine.next
appendLine(cursorLine, cursorLine.next)
removeLine(td)
updateLineLength(cursorLine)
}
}
else
{
deleteSelection()
if (cursorPosition > selectStartPos)
{
cursorPosition = selectStartPos
}
else
{
selectStartPos = cursorPosition
}
}
}
updateCursorPosition()
}
if (kCode == KeyCode.BACKSPACE)
{
mutex(textLock)
{
if (cursorLine === selectStartLine && cursorPosition == selectStartPos)
{
//deal with cursor at the start of a line, deleting the current line and appending any content to the previous one
if (cursorPosition > 0)
{
selectStartPos = cursorPosition - 1
deleteSelection()
cursorPosition --
updateLineLength(cursorLine)
}
else if (cursorLine.prev != null)
{
cursorPosition = cursorLine.prev.text.frags.value.length()
selectStartPos = cursorPosition
cursorPositionY -= cursorHeight
TextLine td = cursorLine
appendLine(cursorLine.prev, cursorLine)
cursorLine = cursorLine.prev
removeLine(td)
selectStartLine = cursorLine
selectStartY = cursorPositionY
updateLineLength(cursorLine)
}
}
else
{
deleteSelection()
updateLineLength(cursorLine)
if (cursorPosition > selectStartPos)
{
cursorPosition = selectStartPos
}
else
{
selectStartPos = cursorPosition
}
}
}
updateCursorPosition()
}
if (kCode == KeyCode.RETURN)
{
mutex(textLock)
{
//delete selection, if any
deleteSelection()
splitLineDown()
updateCursorPosition()
}
}
if (kCode == KeyCode.ARROW_LEFT)
{
int thisLinePos = getSublinePos(cursorLine, cursorSubline, cursorPosition)
if (thisLinePos > 0)
{
cursorPosition --
if (!shiftDown)
{
selectStartPos = cursorPosition
selectStartLine = cursorLine
}
updateCursorPosition()
}
else if (cursorSubline.prev != null)
{
TextContent goto = cursorSubline.prev
int lineEndPos = getSublineCharLength(goto)
int gotoPos = getLineCursorPos(cursorLine, goto, lineEndPos)
cursorPosition = gotoPos
cursorSubline = goto
cursorPositionY -= cursorHeight
if (!shiftDown)
{
selectStartPos = cursorPosition
selectStartLine = cursorLine
selectSubline = cursorSubline
selectStartY = cursorPositionY
}
updateCursorPosition()
}
else if (cursorLine.prev != null)
{
TextLine goto = cursorLine.prev
TextContent gotoSub = goto.textEnd
cursorPosition = getLineCharLength(goto)
cursorPositionY -= cursorHeight
cursorLine = goto
cursorSubline = gotoSub
if (!shiftDown)
{
selectStartPos = cursorPosition
selectStartLine = cursorLine
selectSubline = cursorSubline
selectStartY = cursorPositionY
}
updateCursorPosition()
}
}
if (kCode == KeyCode.ARROW_RIGHT)
{
int thisLinePos = getSublinePos(cursorLine, cursorSubline, cursorPosition)
if (thisLinePos < getSublineCharLength(cursorSubline))
{
cursorPosition ++
if (!shiftDown)
{
selectStartPos = cursorPosition
selectStartLine = cursorLine
}
updateCursorPosition()
}
else if (cursorSubline.next != null)
{
TextContent goto = cursorSubline.next
int gotoPos = getLineCursorPos(cursorLine, goto, 0)
cursorPosition = gotoPos
cursorSubline = goto
cursorPositionY += cursorHeight
if (!shiftDown)
{
selectStartPos = cursorPosition
selectStartLine = cursorLine
selectSubline = cursorSubline
selectStartY = cursorPositionY
}
updateCursorPosition()
}
else if (cursorLine.next != null)
{
cursorPosition = 0
cursorPositionY += cursorHeight
cursorLine = cursorLine.next
cursorSubline = cursorLine.text
if (!shiftDown)
{
selectStartPos = cursorPosition
selectStartLine = cursorLine
selectSubline = cursorSubline
selectStartY = cursorPositionY
}
updateCursorPosition()
}
}
if (kCode == KeyCode.ARROW_UP)
{
if (cursorSubline.prev != null)
{
TextContent goto = cursorSubline.prev
int thisLinePos = getSublinePos(cursorLine, cursorSubline, cursorPosition)
int gotoPos = getLineCursorPos(cursorLine, goto, thisLinePos)
int lineEndPos = getSublineCharLength(goto)
if (thisLinePos > lineEndPos)
{
gotoPos = getLineCursorPos(cursorLine, goto, lineEndPos)
}
cursorSubline = goto
cursorPosition = gotoPos
cursorPositionY -= cursorHeight
if (!shiftDown)
{
selectStartPos = cursorPosition
selectStartLine = cursorLine
selectSubline = cursorSubline
selectStartY = cursorPositionY
}
updateCursorPosition()
}
else if (cursorLine.prev != null)
{
TextLine goto = cursorLine.prev
TextContent gotoSub = goto.textEnd
int thisLinePos = getSublinePos(cursorLine, cursorSubline, cursorPosition)
int lineEndPos = getSublineCharLength(gotoSub)
if (thisLinePos > lineEndPos)
{
cursorPosition = getLineCursorPos(goto, gotoSub, lineEndPos)
}
else
{
cursorPosition = getLineCursorPos(goto, gotoSub, thisLinePos)
}
cursorLine = goto
cursorSubline = gotoSub
cursorPositionY -= cursorHeight
if (!shiftDown)
{
selectStartPos = cursorPosition
selectStartLine = cursorLine
selectStartY = cursorPositionY
}
updateCursorPosition()
}
else
{
cursorPosition = 0
selectStartPos = 0
}
}
if (kCode == KeyCode.ARROW_DOWN)
{
if (cursorSubline.next != null)
{
TextContent goto = cursorSubline.next
int thisLinePos = getSublinePos(cursorLine, cursorSubline, cursorPosition)
int gotoPos = getLineCursorPos(cursorLine, goto, thisLinePos)
int lineEndPos = getSublineCharLength(goto)
if (thisLinePos > lineEndPos)
{
gotoPos = getLineCursorPos(cursorLine, goto, lineEndPos)
}
cursorSubline = goto
cursorPosition = gotoPos
cursorPositionY += cursorHeight
if (!shiftDown)
{
selectStartPos = cursorPosition
selectStartLine = cursorLine
selectSubline = cursorSubline
selectStartY = cursorPositionY
}
updateCursorPosition()
}
else if (cursorLine.next != null)
{
TextLine goto = cursorLine.next
TextContent gotoSub = goto.text
int thisLinePos = getSublinePos(cursorLine, cursorSubline, cursorPosition)
int lineEndPos = getSublineCharLength(gotoSub)
if (thisLinePos > lineEndPos)
{
cursorPosition = getLineCursorPos(goto, gotoSub, lineEndPos)
}
else
{
cursorPosition = getLineCursorPos(goto, gotoSub, thisLinePos)
}
cursorLine = goto
cursorSubline = gotoSub
cursorPositionY += cursorHeight
if (!shiftDown)
{
selectStartPos = cursorPosition
selectStartLine = cursorLine
selectStartY = cursorPositionY
}
updateCursorPosition()
}
else
{
cursorPosition = getLineCharLength(cursorLine)
selectStartPos = cursorPosition
}
}
if (kCode == KeyCode.END)
{
cursorPosition = getLineCharLength(cursorLine)
//cursorPositionY = getLineHeight(cursorLine) //TODO.
if (!shiftDown)
selectStartPos = cursorPosition
updateCursorPosition()
}
if (kCode == KeyCode.HOME)
{
cursorPosition = 0
if (!shiftDown)
selectStartPos = cursorPosition
updateCursorPosition()
}
postRepaint()
}
return true
}
bool TextArea:keyUp(int keyCode)
{
byte kCode
if ((kCode = keyMapping.getKeyCode(keyCode)) != KeyCode.OTHER)
{
if (kCode == KeyCode.SHIFT_LEFT || kCode == KeyCode.SHIFT_RIGHT)
{
keyState &= ~KeyState.KEY_SHIFT
shiftDown = false
return true
}
if (kCode == KeyCode.CTRL_LEFT || kCode == KeyCode.CTRL_RIGHT)
{
keyState &= ~KeyState.KEY_CTRL
return true
}
if (kCode == KeyCode.ALT_LEFT)
{
keyState &= ~KeyState.KEY_ALT
}
if (kCode == KeyCode.ALT_RIGHT)
{
keyState &= ~KeyState.KEY_ALTGR
return true
}
if (kCode == KeyCode.CMD)
{
keyState &= ~KeyState.KEY_CMD
}
}
return true
}
Rect TextArea:getBounds()
{
return new Rect(xPosition, yPosition, width, height)
}
int getLineNumberAreaWidth()
{
char lnMax[] = lineCount.makeString()
int len = textFont.getTextWidth(lnMax) + LINE_NUMBER_MARGIN
return len
}
CursorPos findCursorPos(int x, int y)
{
if (lineNumbers) x -= getLineNumberAreaWidth()
//out.println("FCP $x.$y")
int lnIndex = 0
//first we find the line
TextLine lw = lines
int lineBottom = 0
int lineTop = 0
while (lw != null)
{
lineTop = lineBottom
lineBottom += getLineHeight(lw)
//out.println("LB: $lineBottom / LH $(getLineHeight(lw)) / LT $lineTop")
if (y < lineBottom)
break
lnIndex ++
lw = lw.next
}
if (lw == null)
{
//out.println("line-revert")
lw = lastLine
lineBottom -= cursorHeight
}
//now find the character on that line
TextContent cw = lw.text
int charCount = 0
while ((lineTop + cursorHeight) < y && cw.next != null)
{
charCount += getSublineCharLength(cw)
cw = cw.next
lineTop += cursorHeight
}
//we're now on the right subline
int subIndex = 0
int totalWidth = 0
TextFrag fw = cw.frags
for (int i = 0; i < getSublineCharLength(cw); i++)
{
int ptw = totalWidth
if (fw.type == TextFrag.T_TEXT || fw.type == TextFrag.T_PARTICLE)
{
//out.println("'$(fw.value.getRaw())' $(fw.value.length()) / $subIndex")
totalWidth += textFont.getTextWidth(fw.value.subString(subIndex, 1))
charCount ++
subIndex ++
if (subIndex == fw.value.length())
{
fw = fw.next
subIndex = 0
}
}
else if (fw.type == TextFrag.T_TAB)
{
int nspaces = TAB_SPACES - (charCount % TAB_SPACES)
totalWidth += nspaces * spaceWidth
fw = fw.next
}
if (totalWidth >= x)
{
//decide which side of the character is closer to the actual x location
if ((totalWidth - x) < (x - ptw))
return new CursorPos(charCount, lineTop, lw, cw)
else
return new CursorPos(charCount-1, lineTop, lw, cw)
}
}
return new CursorPos(getLineCursorPos(lw, cw, getSublineCharLength(cw)), lineTop, lw, cw)
}
void TextArea:click(int x, int y, int button)
{
if (showScrollV && (x >= width-SCR_BUTTON_WIDTH && x <= width))
{
scrollV.click(x - (width-SCR_BUTTON_WIDTH), y, button)
}
else if (showScrollH && (y >= height-SCR_BUTTON_WIDTH && y <= height))
{
scrollH.click(x, y - (height - SCR_BUTTON_HEIGHT), button)
}
else
{
if (!focus)
{
setFocus()
postRepaint()
}
if (button == MouseButtons.BUTTON_RIGHT)
{
menu.xAnchor = x
menu.yAnchor = y
emitevent contextMenuOn(menu)
}
}
}
bool isAlphaNum(char x[])
{
if (x.arrayLength > 1) return false
if ((x[0] >= "a") && (x[0] <= "z")) return true
if ((x[0] >= "A") && (x[0] <= "Z")) return true
if ((x[0] >= "0") && (x[0] <= "9")) return true
return false
}
void TextArea:clickMulti(int x, int y, int button, int clicks)
{
if (showScrollV && (x >= width-SCR_BUTTON_WIDTH && x <= width))
{
scrollV.click(x - (width-SCR_BUTTON_WIDTH), y, button)
}
else if (showScrollH && (y >= height-SCR_BUTTON_WIDTH && y <= height))
{
scrollH.click(x, y - (height - SCR_BUTTON_HEIGHT), button)
}
else
{
if (button == MouseButtons.BUTTON_LEFT && clicks == 2)
{
//check if this position is inside a word, with no special characters; if so, highlight that word and place the cursor at its end
// - we keep doing subString() on either side of the cursor position, for a length of one character, to see if that single character is a character, and if so we keep expanding our selection box on both sides until we hit something else
CursorPos cp = findCursorPos(x + xScroll, y + yScroll)
cursorPosition = cp.character
selectStartPos = cursorPosition
int xl = cursorPosition
while (xl < getLineCharLength(cursorLine))
{
char n[] = getCharacters(cursorLine, xl, 1)
if (isAlphaNum(n))
{
xl ++
}
else
{
break
}
}
cursorPosition = xl
int xr = selectStartPos
while (xr > 0)
{
char n[] = getCharacters(cursorLine, xr-1, 1)
if (isAlphaNum(n))
{
xr --
}
else
{
break
}
}
selectStartPos = xr
updateCursorPosition()
postRepaint()
}
else if (button == MouseButtons.BUTTON_LEFT && clicks == 3)
{
//select all
selectStartPos = 0
cursorPosition = getLineCharLength(cursorLine)
postRepaint()
}
}
}
void processMenuClick(MenuItem item)
{
if (item === copyMenu)
{
clipboard.setContent(getSelectedText())
}
else if (item === cutMenu)
{
mutex(textLock)
{
clipboard.setContent(getSelectedText())
deleteSelection()
if (selectStartPos > cursorPosition)
selectStartPos = cursorPosition
else
cursorPosition = selectStartPos
}
updateCursorPosition()
emitevent repaint()
}
else if (item === pasteMenu)
{
mutex(textLock)
{
char itext[] = clipboard.getContent()
deleteSelection()
if (selectStartPos > cursorPosition)
selectStartPos = cursorPosition
else
cursorPosition = selectStartPos
StringUTF tmp = new StringUTF(itext)
int ncp = cursorPosition + tmp.length()
insertText(itext)
cursorPosition = ncp
selectStartPos = cursorPosition
updateCursorPosition()
}
emitevent repaint()
}
else if (item === selectAllMenu)
{
selectStartLine = lines
cursorLine = lastLine
selectStartPos = 0
cursorPosition = getLineCharLength(cursorLine)
selectStartY = 0
cursorPositionY = lineCountV * cursorHeight
updateCursorPosition()
emitevent repaint()
}
}
void TextArea:contextClick(MenuItem item)
{
processMenuClick(item)
}
void TextArea:hotKeyClick(HotKey h)
{
if (h === hkCut)
processMenuClick(cutMenu)
else if (h === hkCopy)
processMenuClick(copyMenu)
else if (h === hkPaste)
processMenuClick(pasteMenu)
else if (h === hkSelectAll)
processMenuClick(selectAllMenu)
}
void TextArea:mouseWheel(int xAdd, int xSub, int yAdd, int ySub)
{
int maxHeight = (lineCountV * cursorHeight) + cursorHeight
int maxScrollY = 0
if (maxHeight > textAreaHeight)
maxScrollY = maxHeight - textAreaHeight
if (ySub != 0)
{
if (yScroll + (ySub*SCROLL_TICKS) > maxScrollY)
{
scrollV.setScrollPos(maxScrollY)
yScroll = maxScrollY
}
else
{
scrollV.setScrollPos(yScroll + (ySub*SCROLL_TICKS))
yScroll += (ySub*SCROLL_TICKS)
}
}
if (yAdd != 0)
{
if (yScroll < (yAdd*SCROLL_TICKS))
{
scrollV.setScrollPos(0)
yScroll = 0
}
else
{
scrollV.setScrollPos(yScroll - (yAdd*SCROLL_TICKS))
yScroll -= (yAdd*SCROLL_TICKS)
}
}
postRepaint()
}
void TextArea:mouseUp(int x, int y, int button)
{
if (showScrollV && clickDown === scrollV)
{
scrollV.mouseUp(x - (width-SCR_BUTTON_WIDTH), y, button)
}
else if (showScrollH && clickDown === scrollH)
{
scrollH.mouseUp(x, y - height, button)
}
else if (button == MouseButtons.BUTTON_LEFT)
{
mouseDragCapture = false
}
clickDown = null
}
void TextArea:mouseMove(int x, int y)
{
if (showScrollV && ((x >= width-SCR_BUTTON_WIDTH && x <= width) || clickDown === scrollV))
{
if (cursorType != IOWindow.CURSOR_DEFAULT)
{
emitevent setCursor(new CursorSetEvent(IOWindow.CURSOR_DEFAULT))
cursorType = IOWindow.CURSOR_DEFAULT
}
if (clickDown === scrollV)
{
scrollV.mouseMove(x, y)
}
}
else if (showScrollH && ((y >= height-SCR_BUTTON_HEIGHT && y <= height) || clickDown === scrollH))
{
if (cursorType != IOWindow.CURSOR_DEFAULT)
{
emitevent setCursor(new CursorSetEvent(IOWindow.CURSOR_DEFAULT))
cursorType = IOWindow.CURSOR_DEFAULT
}
if (clickDown === scrollH)
{
scrollH.mouseMove(x, y)
}
}
else
{
if (cursorType != IOWindow.CURSOR_IBEAM)
{
emitevent setCursor(new CursorSetEvent(IOWindow.CURSOR_IBEAM))
cursorType = IOWindow.CURSOR_IBEAM
}
if (mouseDragCapture)
{
CursorPos cp = findCursorPos(x + xScroll, y + yScroll)
if (cp.character != cursorPosition || cp.line != cursorLine)
{
cursorPosition = cp.character
cursorLine = cp.line
cursorPositionY = cp.yPosition
postRepaint()
}
}
}
}
void TextArea:mouseDown(int x, int y, int button)
{
if (showScrollV && (x >= width-SCR_BUTTON_WIDTH && x <= width))
{
scrollV.mouseDown(x - (width-SCR_BUTTON_WIDTH), y, button)
clickDown = scrollV
}
else if (showScrollH && (y >= height-SCR_BUTTON_WIDTH && y <= height))
{
scrollH.mouseDown(x, y - (height - SCR_BUTTON_HEIGHT), button)
clickDown = scrollH
}
else if (button == MouseButtons.BUTTON_LEFT)
{
//locate this position in the text, and set the cursor there
CursorPos cp = findCursorPos(x + xScroll, y + yScroll)
cursorPosition = cp.character
cursorLine = cp.line
cursorSubline = cp.subline
cursorPositionY = cp.yPosition
if (!shiftDown)
{
selectStartPos = cursorPosition
selectStartLine = cursorLine
selectSubline = cursorSubline
selectStartY = cursorPositionY
}
mouseDragCapture = true
clickDown = null
postRepaint()
}
}
char[] TextArea:getText(opt char lineSeparator[])
{
if (lineSeparator == null) lineSeparator = "\n"
return getTextRegion(lines, 0, lastLine, getLineCharLength(lastLine), lineSeparator)
}
void clearText()
{
bool wrapNote = wordWrap
wordWrap = false
TextLine lw = lines.next
while (lw != null)
{
TextLine td = lw
lw = lw.next
clearLine(td)
td.next = null
td.prev = null
td = null
}
lw = shortestLine
while (lw != null)
{
TextLine td = lw
lw = lw.nextLen
td.nextLen = null
td.prevLen = null
td = null
}
lineCount = 0
lineCountV = 0
lines.next = null
clearLine(lines)
lastLine = lines
shortestLine = lines
longestLine = lines
lines.pixelLength = 0
lines.charLength = 0
lines.lineCount = 1
cursorPosition = 0
selectStartPos = cursorPosition
cursorLine = lines
selectStartLine = cursorLine
wordWrap = wrapNote
}
void TextArea:setText(char text[])
{
mutex(textLock)
{
//delete all lines except the first one, clear the first line, then do a text-insert
clearText()
insertText(text)
}
updateScrollMax()
postRepaint()
}
int getY(TextLine forLine)
{
int y = 0
TextLine lw = lines
while (lw !== forLine)
{
y += getLineHeight(lw)
lw = lw.next
}
return y
}
int getSubX(TextContent tc, int pos)
{
int xpos = 0
int charCount = 0
TextFrag fw = tc.frags
while (pos > 0 && fw != null)
{
if (fw.type == TextFrag.T_TEXT || fw.type == TextFrag.T_PARTICLE)
{
//out.println(" - chk $(fw.value.length()) / '$(fw.value.getRaw())' pos $pos")
if (fw.value.length() >= pos)
{
char mt[] = fw.value.subString(0, pos)
//out.println(" - ret m")
return xpos + textFont.getTextWidth(mt)
}
else
{
xpos += textFont.getTextWidth(fw.value.getRaw())
pos -= fw.value.length()
charCount += fw.value.length()
}
}
else
{
//out.println(" - chk T")
int nspaces = TAB_SPACES - (charCount % TAB_SPACES)
xpos += (nspaces * spaceWidth)
pos --
charCount += nspaces
}
fw = fw.next
}
return xpos
}
Point getXY(TextLine forLine, int pos)
{
int y = 0
TextContent cw = forLine.text
//out.println("getXY @ $pos")
int xpos = 0
int charCount = 0
int subline = 0
while (pos > 0 && cw != null)
{
TextFrag fw = cw.frags
xpos = 0
//out.println("ln")
while (pos > 0 && fw != null)
{
if (fw.type == TextFrag.T_TEXT || fw.type == TextFrag.T_PARTICLE)
{
//out.println(" - chk $(fw.value.length()) / '$(fw.value.getRaw())' pos $pos")
if (fw.value.length() >= pos)
{
char mt[] = fw.value.subString(0, pos)
//out.println(" - ret m [$xpos, $(textFont.getTextWidth(mt))] after '$mt'")
return new Point(xpos + textFont.getTextWidth(mt), y + (cursorHeight * subline))
}
else
{
xpos += textFont.getTextWidth(fw.value.getRaw())
pos -= fw.value.length()
charCount += fw.value.length()
}
}
else
{
//out.println(" - chk T")
int nspaces = TAB_SPACES - (charCount % TAB_SPACES)
xpos += (nspaces * spaceWidth)
pos --
charCount += nspaces
}
fw = fw.next
}
cw = cw.next
if (cw != null) subline ++
}
//out.println(" - ret e")
return new Point(xpos, y + (cursorHeight * subline))
}
void drawCursor(Canvas c, TextLine forLine, int pos, int ypos)
{
Point p = getXY(forLine, pos)
//out.println("DC $(p.x).$(p.y) yp $ypos")
c.line(new Line2D(p.x, ypos, p.x, ypos+cursorHeight, new Color(0, 0, 0, 255)))
}
void highlightLine(Canvas c, TextLine lw, int sspos, int sepos, int cy)
{
TextContent tc = lw.text
int totalLen = 0
bool inSelect = false
if (sspos == sepos) return
while (tc != null)
{
int sublen = getSublineCharLength(tc)
int acclen = sublen + totalLen
if (!inSelect && (sspos < acclen) && (sepos <= acclen))
{
//single-line highlight
int xs = getSubX(tc, sspos - totalLen)
int xe = getSubX(tc, sepos - totalLen)
c.rect(new Rect2D(xs, cy, xe - xs, cursorHeight, highlightColor))
return
}
else if (!inSelect && sspos < acclen)
{
int xs = getSubX(tc, sspos - totalLen)
int xe = getSubX(tc, sublen)
c.rect(new Rect2D(xs, cy, xe - xs, cursorHeight, highlightColor))
inSelect = true
}
else if (sepos <= acclen)
{
int xs = getSubX(tc, 0)
int xe = getSubX(tc, sepos - totalLen)
c.rect(new Rect2D(xs, cy, xe - xs, cursorHeight, highlightColor))
return
}
else if (inSelect)
{
int xs = getSubX(tc, 0)
int xe = getSubX(tc, sublen)
c.rect(new Rect2D(xs, cy, xe - xs, cursorHeight, highlightColor))
}
tc = tc.next
cy += cursorHeight
totalLen = acclen
}
}
void drawSelectionBox(Canvas c)
{
if (selectStartLine !== cursorLine)
{
TextLine ssline
TextLine seline
int sspos
int sepos
if (cursorPositionY < selectStartY)
{
ssline = cursorLine
seline = selectStartLine
sspos = cursorPosition
sepos = selectStartPos
}
else
{
ssline = selectStartLine
seline = cursorLine
sspos = selectStartPos
sepos = cursorPosition
}
TextLine lw = ssline
int cy = getY(lw)
highlightLine(c, lw, sspos, getLineCharLength(lw), cy)
cy += getLineHeight(lw)
lw = lw.next
while (lw !== seline)
{
highlightLine(c, lw, 0, getLineCharLength(lw), cy)
cy += getLineHeight(lw)
lw = lw.next
}
highlightLine(c, lw, 0, sepos, cy)
}
else if (selectStartPos != cursorPosition)
{
int startPos = 0
int endPos = 0
if (selectStartPos < cursorPosition)
{
startPos = selectStartPos
endPos = cursorPosition
}
else
{
startPos = cursorPosition
endPos = selectStartPos
}
int cy = getY(cursorLine) //NOTE: this "getY" is only needed because we don't track the cursorPositionY for start-of-line
highlightLine(c, cursorLine, startPos, endPos, cy)
}
}
//this function is called whenever the cursor moves; it updates (if needed) the scroll position of the text relative to the cursor
void updateCursorPosition()
{
Point cp = getXY(cursorLine, cursorPosition)
if (cp.x > (xScroll + textAreaWidth))
{
xScroll = (cp.x - textAreaWidth) + CURSOR_WIDTH
scrollH.setScrollPos(xScroll)
}
else if (cp.x < xScroll)
{
xScroll = cp.x
scrollH.setScrollPos(xScroll)
}
//test if the total text width has become less than the scroll width (i.e., there's empty space and xScroll is not 0)
Point qcp = getXY(cursorLine, getLineCharLength(cursorLine))
if ((xScroll != 0) && (qcp.x + CURSOR_WIDTH < (xScroll + textAreaWidth)))
{
if (qcp.x > (textAreaWidth))
xScroll = (qcp.x - textAreaWidth) + CURSOR_WIDTH
else
xScroll = 0
scrollH.setScrollPos(xScroll)
}
//check if the cursor is out of view
int efy = getY(cursorLine)
if (efy > (yScroll + textAreaHeight))
{
yScroll = (efy - textAreaHeight)
scrollV.setScrollPos(yScroll)
}
else if ((efy-cursorHeight) < yScroll)
{
yScroll = (efy-cursorHeight)
scrollV.setScrollPos(yScroll)
}
//identify the subline of the cursor, for correct cursor-key navigation
if (wordWrap)
{
TextContent tc = cursorLine.text
int count = 0
while (tc != null)
{
int slc = getSublineCharLength(tc)
if ((cursorPosition >= count) && (cursorPosition <= (count + slc)))
{
cursorSubline = tc
break
}
count += slc
tc = tc.next
}
}
}
int drawLine(Canvas c, TextLine lw, int yDraw, int viewTop, int viewBottom)
{
TextContent sw = lw.text
int lineHeight = getLineHeight(lw)
if (((yDraw + lineHeight) >= viewTop) && (yDraw <= viewBottom))
{
while (sw != null)
{
TextFrag fw = sw.frags
int xpos = 0
int charCount = 0
while (fw != null)
{
if (fw.type == TextFrag.T_TEXT || fw.type == TextFrag.T_PARTICLE)
{
c.text(new Point2D(xpos, yDraw, textColor), textFont, fw.value.getRaw())
charCount += fw.value.length()
xpos += textFont.getTextWidth(fw.value.getRaw())
}
else
{
int nspaces = TAB_SPACES - (charCount % TAB_SPACES)
xpos += (nspaces * spaceWidth)
charCount += nspaces
}
fw = fw.next
}
yDraw += cursorHeight
sw = sw.next
}
}
return lineHeight
}
int getLineHeight(TextLine t)
{
return t.lineCount * cursorHeight
}
void TextArea:paint(Canvas c)
{
c.rect(new Rect2D(xPosition, yPosition, width, height, bgColor))
int xOffset = 0
if (lineNumbers)
{
xOffset = lineNumberAreaWidth
}
c.pushSurface(new Rect(xPosition + xOffset + TEXT_BORDER_H, yPosition, width - (TEXT_BORDER_H*2) - lineNumberAreaWidth, height), xScroll, yScroll, 255)
c.rect(new Rect2D(xScroll, yScroll, width, height, bgColor))
mutex(textLock)
{
drawSelectionBox(c)
int yDraw = TEXT_BORDER_V
TextLine lw = lines
while (lw != null)
{
int yAdd = drawLine(c, lw, yDraw, yScroll, yScroll + height)
lw = lw.next
yDraw += yAdd
}
if (focus)
{
drawCursor(c, cursorLine, cursorPosition, cursorPositionY)
}
}
c.popSurface()
if (lineNumbers)
{
c.pushSurface(new Rect(xPosition + TEXT_BORDER_H, yPosition, width - (TEXT_BORDER_H*2), height), xScroll, yScroll, 255)
c.rect(new Rect2D(0, yScroll, lineNumberAreaWidth, height, bgColor))
int viewTop = yScroll
int viewBottom = yScroll + height
int yDraw = TEXT_BORDER_V
TextLine lw = lines
int ln = 0
while (lw != null)
{
int yAdd = getLineHeight(lw)
if (((yDraw + yAdd) >= viewTop) && (yDraw <= viewBottom))
{
c.text(new Point2D(0, yDraw, textColorLN), textFont, ln.makeString())
}
else if (yDraw > viewBottom)
{
break
}
lw = lw.next
yDraw += yAdd
ln ++
}
c.popSurface()
}
if (showScrollV)
{
scrollV.paint(c)
}
if (showScrollH)
{
scrollH.paint(c)
}
if (showScrollV && showScrollH)
{
//fill the square with something...
c.rect(new Rect2D(xPosition + (width-SCR_BUTTON_WIDTH-1), yPosition + (height-SCR_BUTTON_HEIGHT-1), SCR_BUTTON_WIDTH, SCR_BUTTON_HEIGHT, bgColor))
}
c.rectOutline(new Rect2D(xPosition, yPosition, width, height, borderColor))
}
void unwrapLine(TextLine lw)
{
//reset to a single line
while (lw.text.next != null)
{
if (lw.text.next.frags != null)
{
lw.text.fragsEnd.next = lw.text.next.frags
lw.text.next.frags.prev = lw.text.fragsEnd
lw.text.fragsEnd = lw.text.next.fragsEnd
}
lw.text.next.frags = null
lw.text.next.fragsEnd = null
lw.text.next.prev = null
lw.text.next = lw.text.next.next
}
lw.textEnd = lw.text
lineCountV -= lw.lineCount - 1
lw.lineCount = 1
}
void unwrapLines()
{
TextLine lw = lines
while (lw != null)
{
unwrapLine(lw)
lw = lw.next
}
}
void wrapLine(TextLine lw)
{
//reset to a single line
unwrapLine(lw)
Point cp = null
int efy = 0
//recalculate
TextFrag fw = lw.text.frags
int pixelLength = 0
while (fw != null)
{
int test = pixelLength + textFont.getTextWidth(fw.value.getRaw())
if ((test > textAreaWidth) && (pixelLength > 0))
{
//move it to a new line
//out.println("new line W")
TextContent ntc = new TextContent(fw, lw.textEnd.fragsEnd)
lw.textEnd.fragsEnd = fw.prev
lw.textEnd.fragsEnd.next = null
lw.textEnd.next = ntc
ntc.prev = lw.textEnd
lw.textEnd = ntc
pixelLength = textFont.getTextWidth(fw.value.getRaw())
lw.lineCount ++
}
else
{
pixelLength = test
}
fw = fw.next
}
lineCountV += lw.lineCount - 1
if (cursorLine === lw)
{
efy = getY(cursorLine)
cp = getXY(cursorLine, cursorPosition)
cursorPositionY = efy + cp.y
}
updateScrollMax()
}
void wrapLines()
{
//re-calculate from scratch
TextLine lw = lines
while (lw != null)
{
wrapLine(lw)
lw = lw.next
}
}
void TextArea:postRepaint()
{
emitevent repaint()
}
void TextArea:setPosition(int x, int y)
{
xPosition = x
yPosition = y
scrollV.setPosition(xPosition + (width - SCR_BUTTON_WIDTH - 1), yPosition+1)
scrollH.setPosition(xPosition + 1, yPosition + (height - SCR_BUTTON_HEIGHT - 1))
}
Point TextArea:getPosition()
{
return new Point(xPosition, yPosition)
}
WH TextArea:getPreferredSize()
{
return new WH(width, height)
}
CursorSetEvent TextArea:mouseOver(int x, int y)
{
cursorType = IOWindow.CURSOR_IBEAM
return new CursorSetEvent(IOWindow.CURSOR_IBEAM)
}
void TextArea:setFocus()
{
emitevent requestFocus()
}
void TextArea:setDisabled(bool d)
{
disabled = d
postRepaint()
}
bool TextArea:recvFocus()
{
focus = true
return true
}
HotKey[] TextArea:getHotKeys()
{
return hkeys
}
void TextArea:loseFocus()
{
focus = false
selectStartPos = cursorPosition
}
void positionScrollH()
{
if (lineNumbers)
{
char lnMax[] = lineCount.makeString()
int len = textFont.getTextWidth(lnMax) + LINE_NUMBER_MARGIN
scrollH.setPosition(xPosition + len + 1, scrollH.getPosition().y)
if (!showScrollV)
scrollH.setLength(width - len - 2)
else
scrollH.setLength(width - len - SCR_BUTTON_HEIGHT - 2)
}
else
{
scrollH.setPosition(xPosition + 1, yPosition + (height - SCR_BUTTON_HEIGHT - 1))
if (!showScrollV)
scrollH.setLength(width - 2)
else
scrollH.setLength(width - SCR_BUTTON_HEIGHT - 2)
}
}
void TextArea:setSize(int w, int h)
{
width = w
height = h
updateTextAreaWidth()
textAreaHeight = height
if (showScrollV)
{
scrollV.setPosition(xPosition + (width - SCR_BUTTON_WIDTH - 1), yPosition+1)
if (!showScrollH)
scrollV.setLength(height - 2)
else
scrollV.setLength(height - SCR_BUTTON_HEIGHT - 2)
}
if (showScrollH)
{
textAreaHeight -= SCR_BUTTON_WIDTH
positionScrollH()
}
if (wordWrap) wrapLines()
updateScrollMax()
postRepaint()
}
void TextArea:setCursorPos(int p)
{
if (p <= getLineCharLength(cursorLine))
{
cursorPosition = p
selectStartPos = cursorPosition
updateCursorPosition()
postRepaint()
}
else
{
throw new Exception("index out of bounds")
}
}
int TextArea:getCursorPos()
{
return cursorPosition
}
void TextArea:setWordWrap(bool v)
{
wordWrap = v
if (wordWrap)
{
wrapLines()
showScrollH = false
}
else
{
unwrapLines()
showScrollH = true
}
postRepaint()
}
int getNumberAreaWidth(int largestNumber)
{
char lnMax[] = largestNumber.makeString()
return textFont.getTextWidth(lnMax) + LINE_NUMBER_MARGIN
}
void updateTextAreaWidth()
{
int newWidth = width-(TEXT_BORDER_H*2)
if (showScrollV)
{
newWidth -= SCR_BUTTON_WIDTH
}
newWidth -= lineNumberAreaWidth
textAreaWidth = newWidth
}
void TextArea:setLineNumbers(bool v)
{
lineNumbers = v
lineNumberAreaWidth = getNumberAreaWidth(lineCount)
updateTextAreaWidth()
if (wordWrap) wrapLines()
positionScrollH()
postRepaint()
}
void TextArea:setAutoIndent(bool v)
{
autoIndent = v
}
void Destructor:destroy()
{
clearText()
}
}