//this is the horizontal border between the textual contents of the TextField, 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 SCROLL_TICKS = 5
uses IOWindow
component provides TextField requires io.Output out, data.IntUtil iu, ui.Font, os.SystemInfo sysInfo, os.Clipboard clipboard, locale.KeyMapping keyMapping, encoding.StringUTF {
Mutex textLock = new Mutex()
bool passwordMask
StringUTF currentText
StringUTF placeholder
char maskedText[]
Font textFont
int width = 40
int height = 20
bool matchFontHeight
int cursorPosition
int selectStartPos
bool focus
int hScroll
bool shiftDown
byte keyState
bool mouseDragCapture
ClickableObject clickTarget
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)
ContextMenuSpec hintMenu
byte contextMode = CONTEXT_MENU
Color bgColor = new Color(230, 230, 250, 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 placeholderTextColor = new Color(90, 90, 90, 255)
TextField:TextField()
{
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
)
hintMenu = new ContextMenuSpec()
height = textFont.getFontMetrics().height + (TEXT_BORDER_V*2)
currentText = new StringUTF("")
}
void TextField:setBackground(store Color c)
{
bgColor = c
postRepaint()
}
void TextField:setBorder(store Color c)
{
borderColor = c
postRepaint()
}
void TextField:setHighlight(store Color c)
{
highlightColor = c
postRepaint()
}
void TextField:setTextColor(store Color c)
{
textColor = c
postRepaint()
}
void TextField:setFont(store Font f)
{
textFont = f
height = textFont.getFontMetrics().height + (TEXT_BORDER_V*2)
postRepaint()
}
char[] getSelectedText()
{
int s
int e
if (selectStartPos < cursorPosition)
{
s = selectStartPos
e = cursorPosition
}
else
{
e = selectStartPos
s = cursorPosition
}
return currentText.subString(s, e-s)
}
char[] getMask(int len)
{
char result[] = new char[len]
for (int i = 0; i < len; i++)
{
result[i] = "*"
}
return result
}
void deleteText(int s, int e)
{
currentText.delete(s, e-s)
maskedText = getMask(currentText.length())
}
void deleteSelection()
{
int s
int e
if (selectStartPos < cursorPosition)
{
s = selectStartPos
e = cursorPosition
}
else
{
e = selectStartPos
s = cursorPosition
}
if (s != e)
{
deleteText(s, e)
emitevent textChanged()
}
}
void insertText(char text[])
{
int oLen = currentText.length()
if (cursorPosition == currentText.length())
{
currentText.append(text)
}
else
{
currentText.insert(cursorPosition, text)
}
int nLen = currentText.length()
maskedText = getMask(currentText.length())
cursorPosition += (nLen - oLen)
selectStartPos = cursorPosition
emitevent textChanged()
}
bool TextField: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 (selectStartPos != cursorPosition)
{
deleteSelection()
if (selectStartPos > cursorPosition)
selectStartPos = cursorPosition
else
cursorPosition = selectStartPos
}
if (cursorPosition == currentText.length())
{
currentText.append(ch)
}
else
{
currentText.insert(cursorPosition, ch)
}
maskedText = new char[](maskedText, "*")
cursorPosition ++
selectStartPos = cursorPosition
}
updateCursorPosition()
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.DELETE)
{
mutex(textLock)
{
if (currentText.length() > 0)
{
if (cursorPosition == selectStartPos && cursorPosition < currentText.length())
{
selectStartPos = cursorPosition + 1
deleteSelection()
selectStartPos --
}
else
{
deleteSelection()
if (cursorPosition > selectStartPos)
{
cursorPosition = selectStartPos
}
else
{
selectStartPos = cursorPosition
}
}
}
updateCursorPosition()
}
}
if (kCode == KeyCode.BACKSPACE)
{
mutex(textLock)
{
if (currentText.length() > 0)
{
if (cursorPosition == selectStartPos && cursorPosition > 0)
{
selectStartPos = cursorPosition - 1
deleteSelection()
cursorPosition --
}
else
{
deleteSelection()
if (cursorPosition > selectStartPos)
{
cursorPosition = selectStartPos
}
else
{
selectStartPos = cursorPosition
}
}
}
updateCursorPosition()
}
}
if (kCode == KeyCode.RETURN)
{
if (clickTarget != null)
clickTarget.click(0, 0, MouseButtons.BUTTON_LEFT)
}
if (kCode == KeyCode.ARROW_LEFT)
{
if (cursorPosition > 0)
{
cursorPosition --
if (!shiftDown)
selectStartPos = cursorPosition
updateCursorPosition()
}
}
if (kCode == KeyCode.ARROW_RIGHT)
{
if (cursorPosition < currentText.length())
{
cursorPosition ++
if (!shiftDown)
selectStartPos = cursorPosition
updateCursorPosition()
}
}
if (kCode == KeyCode.END)
{
cursorPosition = currentText.length()
if (!shiftDown)
selectStartPos = cursorPosition
updateCursorPosition()
}
if (kCode == KeyCode.HOME)
{
cursorPosition = 0
if (!shiftDown)
selectStartPos = cursorPosition
updateCursorPosition()
}
postRepaint()
}
return true
}
bool TextField: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 TextField:getBounds()
{
return new Rect(xPosition, yPosition, width, height)
}
int findCursorPos(int x)
{
char test[]
for (int i = 0; i < currentText.length(); i++)
{
if (passwordMask)
test = new char[](test, maskedText[i])
else
test = new char[](test, currentText.subString(i, 1))
int text_xp = textFont.getTextWidth(test)
if (text_xp >= x) return i
}
return currentText.length()
}
void TextField:click(int x, int y, int button)
{
if (!focus)
{
setFocus()
postRepaint()
}
if (button == MouseButtons.BUTTON_RIGHT)
{
menu.xAnchor = x
menu.yAnchor = y
contextMode = CONTEXT_MENU
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 TextField:clickMulti(int x, int y, int button, int clicks)
{
focus = true
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
cursorPosition = findCursorPos(x + hScroll)
selectStartPos = cursorPosition
int xl = cursorPosition
while (xl < currentText.length())
{
char n[] = currentText.subString(xl, 1)
if (isAlphaNum(n))
{
xl ++
}
else
{
break
}
}
cursorPosition = xl
int xr = selectStartPos
while (xr > 0)
{
char n[] = currentText.subString(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 = currentText.length()
postRepaint()
}
}
void processMenuClick(MenuItem item)
{
if (item === copyMenu)
{
clipboard.setContent(getSelectedText())
}
else if (item === cutMenu)
{
clipboard.setContent(getSelectedText())
deleteSelection()
if (selectStartPos > cursorPosition)
selectStartPos = cursorPosition
else
cursorPosition = selectStartPos
updateCursorPosition()
emitevent repaint()
}
else if (item === pasteMenu)
{
char itext[] = clipboard.getContent()
deleteSelection()
if (selectStartPos > cursorPosition)
selectStartPos = cursorPosition
else
cursorPosition = selectStartPos
insertText(itext)
updateCursorPosition()
emitevent repaint()
}
else if (item === selectAllMenu)
{
selectStartPos = 0
cursorPosition = currentText.length()
updateCursorPosition()
emitevent repaint()
}
}
void TextField:contextClick(MenuItem item)
{
if (contextMode == CONTEXT_MENU)
{
processMenuClick(item)
}
else
{
setText(item.name)
cursorPosition = currentText.length()
selectStartPos = cursorPosition
}
}
void TextField: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 TextField:mouseUp(int x, int y, int button)
{
if (button == MouseButtons.BUTTON_LEFT)
{
mouseDragCapture = false
if (!focus)
{
setFocus()
postRepaint()
}
}
}
void TextField:mouseMove(int x, int y)
{
if (mouseDragCapture)
{
int newPos = findCursorPos(x + hScroll)
if (newPos != cursorPosition)
{
cursorPosition = newPos
postRepaint()
}
}
}
void TextField:mouseDown(int x, int y, int button)
{
if (button == MouseButtons.BUTTON_LEFT)
{
//locate this position in the text, and set the cursor there
cursorPosition = findCursorPos(x + hScroll)
if (!shiftDown)
selectStartPos = cursorPosition
mouseDragCapture = true
postRepaint()
}
}
void TextField:mouseWheel(int xAdd, int xSub, int yAdd, int ySub)
{
int maxScroll = 0
int textWidth = textFont.getTextWidth(currentText.getRaw())
if (textWidth > (width - (TEXT_BORDER_H*2) - CURSOR_WIDTH))
maxScroll = textWidth - (width - (TEXT_BORDER_H*2) - CURSOR_WIDTH)
if (yAdd != 0 && hScroll < SCROLL_TICKS)
hScroll = 0
else
hScroll -= (yAdd*SCROLL_TICKS)
if (ySub != 0 && (hScroll + (ySub*SCROLL_TICKS)) > maxScroll)
hScroll = maxScroll
else
hScroll += (ySub*SCROLL_TICKS)
postRepaint()
}
char[] TextField:getText()
{
return currentText.getRaw()
}
void TextField:setText(char text[])
{
currentText = new StringUTF(text)
char k[] = clone text
for (int i = 0; i < k.arrayLength; i++)
{
k[i] = "*"
}
maskedText = k
postRepaint()
}
void TextField:setPlaceholder(char text[])
{
if (text != null)
placeholder = new StringUTF(text)
else
placeholder = null
postRepaint()
}
void TextField:setPasswordMask(bool t)
{
passwordMask = t
postRepaint()
}
int getX(int pos)
{
char mt[] = new char[pos]
if (passwordMask)
mt =[] maskedText
else
mt = currentText.subString(0, pos)
return textFont.getTextWidth(mt)
}
void drawCursor(Canvas c, int pos)
{
int xp = getX(pos)
c.line(new Line2D(xp, 0, xp, height, new Color(0, 0, 0, 255)))
}
void drawSelectionBox(Canvas c)
{
if (selectStartPos != cursorPosition)
{
int startPos = 0
int endPos = 0
if (selectStartPos < cursorPosition)
{
startPos = selectStartPos
endPos = cursorPosition
}
else
{
startPos = cursorPosition
endPos = selectStartPos
}
int selectStartX = getX(startPos)
int selectEndX = getX(endPos)
c.rect(new Rect2D(selectStartX, 0, selectEndX - selectStartX, height, highlightColor))
}
}
//this function is called whenever the cursor moves; it updates (if needed) the scroll position of the text relative to the cursor
void updateCursorPosition()
{
int efx = getX(cursorPosition)
if (efx > (hScroll + (width-(TEXT_BORDER_H*2))))
{
hScroll = (efx - width) + (TEXT_BORDER_H*2) + CURSOR_WIDTH
}
else if (efx < hScroll)
{
hScroll = efx
}
//test if the total text width has become less than the scroll width (i.e., there's empty space and hScroll is not 0)
int qfx = getX(currentText.length())
if ((hScroll != 0) && (qfx + CURSOR_WIDTH < (hScroll + (width-(TEXT_BORDER_H*2)))))
{
if (qfx > (width-(TEXT_BORDER_H*2)))
hScroll = (qfx - width) + (TEXT_BORDER_H*2) + CURSOR_WIDTH
else
hScroll = 0
}
}
void TextField:paint(Canvas c)
{
c.rect(new Rect2D(xPosition, yPosition, width, height, bgColor))
c.pushSurface(new Rect(xPosition + TEXT_BORDER_H, yPosition, width - (TEXT_BORDER_H*2), height), hScroll, 0, 255)
c.rect(new Rect2D(hScroll, 0, width, height, bgColor))
mutex(textLock)
{
drawSelectionBox(c)
if (passwordMask)
c.text(new Point2D(0, TEXT_BORDER_V, textColor), textFont, maskedText)
else
c.text(new Point2D(0, TEXT_BORDER_V, textColor), textFont, currentText.getRaw())
if (focus)
{
drawCursor(c, cursorPosition)
}
if (currentText.length() == 0 && placeholder != null)
{
c.text(new Point2D(0, TEXT_BORDER_V, placeholderTextColor), textFont, placeholder.getRaw())
}
}
c.popSurface()
c.rectOutline(new Rect2D(xPosition, yPosition, width, height, borderColor))
}
void TextField:postRepaint()
{
emitevent repaint()
}
void TextField:setPosition(int x, int y)
{
xPosition = x
yPosition = y
}
Point TextField:getPosition()
{
return new Point(xPosition, yPosition)
}
WH TextField:getPreferredSize()
{
return new WH(width, height)
}
CursorSetEvent TextField:mouseOver(int x, int y)
{
return new CursorSetEvent(IOWindow.CURSOR_IBEAM)
}
void TextField:setFocus()
{
emitevent requestFocus()
}
void TextField:setDisabled(bool d)
{
disabled = d
postRepaint()
}
bool TextField:recvFocus()
{
focus = true
return true
}
HotKey[] TextField:getHotKeys()
{
return hkeys
}
void TextField:loseFocus()
{
focus = false
selectStartPos = cursorPosition
}
void TextField:setWidth(int w)
{
width = w
}
void TextField:setClickTarget(store ClickableObject o)
{
clickTarget = o
}
void TextField:setHintItems(store String items[])
{
if (items == null || items.arrayLength == 0)
{
emitevent contextMenuOff()
}
else
{
contextMode = CONTEXT_HINT
hintMenu.xAnchor = 0
hintMenu.yAnchor = height
hintMenu.items = new MenuItem[items.arrayLength]
for (int i = 0; i < items.arrayLength; i++)
{
hintMenu.items[i] = new MenuItem(items[i].string)
}
emitevent contextMenuOn(hintMenu)
}
}
void TextField:setCursorPos(int p)
{
if (p <= currentText.length())
{
cursorPosition = p
selectStartPos = cursorPosition
updateCursorPosition()
postRepaint()
}
else
{
throw new Exception("index out of bounds")
}
}
int TextField:getCursorPos()
{
return cursorPosition
}
}