HomeForumSourceResearchGuide
Sign in to contribute to source. how it works
Component ui.TextField by barry
expand copy to clipboardexpand
//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
		}
	
	}
Revision history
To propose a new revision to this entity, use dana source put -uc your/new/version.dn -n ui.TextField -m "reason for update" -u yourUsername
Version 2 (this version) by barry
Notes for this version: Corrections to prepare for upcoming compiler strictness changes in function parameter qualifier equivalence
Version 1 by barry