mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-19 18:49:19 +01:00
Simplify code, allow Shift+Click selections
This commit is contained in:
parent
b6e68d40bd
commit
7316baa390
@ -8,7 +8,7 @@ import ocelot.desktop.ui.UiHandler
|
||||
import ocelot.desktop.ui.event.handlers.MouseHandler
|
||||
import ocelot.desktop.ui.event.sources.KeyEvents
|
||||
import ocelot.desktop.ui.event.{DoubleClickEvent, DragEvent, KeyEvent, MouseEvent}
|
||||
import ocelot.desktop.ui.widget.TextInput.{Cursor, Selector, Text}
|
||||
import ocelot.desktop.ui.widget.TextInput.{Cursor, Selection, Text}
|
||||
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
|
||||
import ocelot.desktop.ui.widget.traits.HoverAnimation
|
||||
import ocelot.desktop.util.{DrawUtils, Register, Watcher}
|
||||
@ -29,15 +29,15 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
// model
|
||||
private val _text: Text = new Text(initialText.codePoints().toArray)
|
||||
private val cursor: Cursor = new Cursor()
|
||||
private val selector: Selector = new Selector()
|
||||
private val selection: Selection = new Selection()
|
||||
|
||||
// view
|
||||
private var isFocused = false
|
||||
private var scroll = 0f
|
||||
private var blinkTimer = 0f
|
||||
private var cursorOffset = 0f
|
||||
private var selectorStartOffset = 0f
|
||||
private var selectorEndOffset = 0f
|
||||
private var selectionStartOffset = 0f
|
||||
private var selectionEndOffset = 0f
|
||||
|
||||
private val enabledRegister = Register.sampling(enabled)
|
||||
|
||||
@ -47,9 +47,9 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
adjustScroll()
|
||||
})
|
||||
|
||||
selector.onChange((from, to) => {
|
||||
selectorStartOffset = charsWidth(_text.chars, 0, from min to)
|
||||
selectorEndOffset = charsWidth(_text.chars, 0, from max to)
|
||||
selection.onChange((from, to) => {
|
||||
selectionStartOffset = charsWidth(_text.chars, 0, from min to)
|
||||
selectionEndOffset = charsWidth(_text.chars, 0, from max to)
|
||||
})
|
||||
|
||||
private val foregroundAnimation = new ColorAnimation(targetForegroundColor, 7f)
|
||||
@ -70,11 +70,11 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
def text: String = new String(_text.chars, 0, _text.chars.length)
|
||||
def text_=(value: String): Unit = {
|
||||
_text.chars = value.codePoints().toArray
|
||||
selector.active = false
|
||||
selection.active = false
|
||||
cursor.position = cursor.position max 0 min _text.chars.length
|
||||
}
|
||||
|
||||
private def selectedText: String = new String(_text.chars, selector.start, selector.length)
|
||||
private def selectedText: String = new String(_text.chars, selection.start, selection.length)
|
||||
|
||||
protected var placeholder: Array[Int] = Array.empty
|
||||
def placeholder_=(value: String): Unit = placeholder = value.codePoints().toArray
|
||||
@ -127,103 +127,85 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
}
|
||||
|
||||
eventHandlers += {
|
||||
case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if enabled =>
|
||||
case MouseEvent(MouseEvent.State.Pressed, button) if enabled =>
|
||||
val inBounds = mouseInBounds
|
||||
if (isFocused && !inBounds) unfocus()
|
||||
if (!isFocused && inBounds) focus()
|
||||
if (isFocused) {
|
||||
val pos = pixelToCursorPosition(UiHandler.mousePosition.x - bounds.x)
|
||||
cursor.position = _text.chars.length.min(pos).max(0)
|
||||
selector.from = cursor.position
|
||||
selector.active = false
|
||||
val clamped = _text.chars.length.min(pos).max(0)
|
||||
// a bit of special logic: right click moves cursor position, but only if we clicked outside the current selection
|
||||
// otherwise it would be impossible to do context operations on the selected text
|
||||
if (button != MouseEvent.Button.Right || !selection.active || clamped < selection.from || clamped > selection.to) {
|
||||
setCursorAndSelectionPosition(clamped)
|
||||
}
|
||||
// more special logic: keep `selection.from` updated even if there is no selection active
|
||||
// in case there will be a follow-up drag event, which usually has some lag between initial click and following drag,
|
||||
// and we would not want our mouse selection to lag
|
||||
if (!selection.active) {
|
||||
selection.from = clamped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Right) if isFocused =>
|
||||
if (mouseInBounds) {
|
||||
val menu = new ContextMenu
|
||||
if (selector.active) {
|
||||
menu.addEntry(ContextMenuEntry("Cut", IconSource.Icons.Cut) { cutSelection() })
|
||||
menu.addEntry(ContextMenuEntry("Copy", IconSource.Icons.Copy) { copySelection() })
|
||||
}
|
||||
if (UiHandler.clipboard.nonEmpty) {
|
||||
menu.addEntry(ContextMenuEntry("Paste", IconSource.Icons.Paste) { pasteSelection() })
|
||||
}
|
||||
if (menu.children.nonEmpty) {
|
||||
menu.addSeparator()
|
||||
}
|
||||
menu.addEntry(ContextMenuEntry("Select all") { selectAll() })
|
||||
root.get.contextMenus.open(menu)
|
||||
eventHandlers += {
|
||||
case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Right) if isFocused && mouseInBounds =>
|
||||
val menu = new ContextMenu
|
||||
if (selection.active) {
|
||||
menu.addEntry(ContextMenuEntry("Cut", IconSource.Icons.Cut) { cutSelection() })
|
||||
menu.addEntry(ContextMenuEntry("Copy", IconSource.Icons.Copy) { copySelection() })
|
||||
}
|
||||
if (UiHandler.clipboard.nonEmpty) {
|
||||
menu.addEntry(ContextMenuEntry("Paste", IconSource.Icons.Paste) { pasteSelection() })
|
||||
}
|
||||
if (menu.children.nonEmpty) {
|
||||
menu.addSeparator()
|
||||
}
|
||||
menu.addEntry(ContextMenuEntry("Select all") { selectAll() })
|
||||
root.get.contextMenus.open(menu)
|
||||
|
||||
case event @ DoubleClickEvent(MouseEvent.Button.Left, _) if isFocused =>
|
||||
if (mouseInBounds) {
|
||||
selectWord()
|
||||
event.consume()
|
||||
}
|
||||
case DoubleClickEvent(MouseEvent.Button.Left, _) if isFocused && mouseInBounds =>
|
||||
selectWord()
|
||||
|
||||
case event @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, mouse) if isFocused =>
|
||||
val pos = pixelToCursorPosition(mouse.x - bounds.x)
|
||||
selector.active = true
|
||||
selector.to = pos
|
||||
selection.active = true
|
||||
selection.to = pos
|
||||
cursor.position = pos
|
||||
event.consume()
|
||||
|
||||
case event @ DragEvent(DragEvent.State.Drag, MouseEvent.Button.Left, mouse) if isFocused =>
|
||||
val pos = pixelToCursorPosition(mouse.x - bounds.x)
|
||||
selector.to = pos
|
||||
selection.to = pos
|
||||
cursor.position = pos
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_LEFT, _) if isFocused =>
|
||||
if (KeyEvents.isShiftDown && !selector.active) {
|
||||
selector.active = true
|
||||
selector.from = cursor.position
|
||||
}
|
||||
if (selector.active && !KeyEvents.isShiftDown) {
|
||||
cursor.position = selector.start
|
||||
selector.active = false
|
||||
if (selection.active && !KeyEvents.isShiftDown) {
|
||||
setCursorAndSelectionPosition(selection.start)
|
||||
} else if (cursor.position > 0) {
|
||||
cursor.position -= 1
|
||||
if (selector.active) selector.to = cursor.position
|
||||
setCursorAndSelectionPosition(cursor.position - 1)
|
||||
}
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_RIGHT, _) if isFocused =>
|
||||
if (KeyEvents.isShiftDown && !selector.active) {
|
||||
selector.active = true
|
||||
selector.from = cursor.position
|
||||
}
|
||||
if (selector.active && !KeyEvents.isShiftDown) {
|
||||
cursor.position = selector.end
|
||||
selector.active = false
|
||||
if (selection.active && !KeyEvents.isShiftDown) {
|
||||
setCursorAndSelectionPosition(selection.end)
|
||||
} else if (cursor.position < _text.chars.length) {
|
||||
cursor.position += 1
|
||||
if (selector.active) selector.to = cursor.position
|
||||
setCursorAndSelectionPosition(cursor.position + 1)
|
||||
}
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_HOME, _) if isFocused =>
|
||||
if (!selector.active && KeyEvents.isShiftDown) {
|
||||
selector.active = true
|
||||
selector.from = cursor.position
|
||||
}
|
||||
if (selector.active && !KeyEvents.isShiftDown) selector.active = false
|
||||
cursor.position = 0
|
||||
if (selector.active && KeyEvents.isShiftDown) selector.to = cursor.position
|
||||
setCursorAndSelectionPosition(0)
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_END, _) if isFocused =>
|
||||
if (!selector.active && KeyEvents.isShiftDown) {
|
||||
selector.active = true
|
||||
selector.from = cursor.position
|
||||
}
|
||||
if (selector.active && !KeyEvents.isShiftDown) selector.active = false
|
||||
cursor.position = _text.chars.length
|
||||
if (selector.active && KeyEvents.isShiftDown) selector.to = cursor.position
|
||||
setCursorAndSelectionPosition(_text.chars.length)
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_BACK, _) if isFocused =>
|
||||
if (selector.active) {
|
||||
if (selection.active) {
|
||||
deleteSelection()
|
||||
} else {
|
||||
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
|
||||
@ -235,7 +217,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_DELETE, _) if isFocused =>
|
||||
if (selector.active) {
|
||||
if (selection.active) {
|
||||
deleteSelection()
|
||||
} else {
|
||||
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
|
||||
@ -256,12 +238,12 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_C, _)
|
||||
if isFocused && KeyEvents.isControlDown && selector.active =>
|
||||
if isFocused && KeyEvents.isControlDown && selection.active =>
|
||||
copySelection()
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_X, _)
|
||||
if isFocused && KeyEvents.isControlDown && selector.active =>
|
||||
if isFocused && KeyEvents.isControlDown && selection.active =>
|
||||
cutSelection()
|
||||
event.consume()
|
||||
|
||||
@ -279,7 +261,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, _, char) if isFocused && !char.isControl =>
|
||||
if (selector.active) deleteSelection()
|
||||
if (selection.active) deleteSelection()
|
||||
writeChar(char)
|
||||
event.consume()
|
||||
}
|
||||
@ -299,17 +281,34 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
pos
|
||||
}
|
||||
|
||||
/**
|
||||
* Generally you can just do `cursor.position = x`.
|
||||
* But this method will take care of correctly repositioning the selection as well.
|
||||
*/
|
||||
private def setCursorAndSelectionPosition(position: Int): Unit = {
|
||||
if (!selection.active && KeyEvents.isShiftDown && cursor.position != position) {
|
||||
selection.active = true
|
||||
selection.from = cursor.position
|
||||
} else if (selection.active && !KeyEvents.isShiftDown) {
|
||||
selection.active = false
|
||||
}
|
||||
cursor.position = position
|
||||
if (selection.active) {
|
||||
selection.to = cursor.position
|
||||
}
|
||||
}
|
||||
|
||||
private def selectAll(): Unit = {
|
||||
selector.active = true
|
||||
selector.from = 0
|
||||
selector.to = _text.chars.length
|
||||
selection.active = true
|
||||
selection.from = 0
|
||||
selection.to = _text.chars.length
|
||||
}
|
||||
|
||||
private def selectWord(): Unit = {
|
||||
selector.active = true
|
||||
selector.from = 0 max (_text.chars.lastIndexWhere(isWhitespace, cursor.position - 1) + 1)
|
||||
selection.active = true
|
||||
selection.from = 0 max (_text.chars.lastIndexWhere(isWhitespace, cursor.position - 1) + 1)
|
||||
val to = _text.chars.indexWhere(isWhitespace, cursor.position)
|
||||
selector.to = if (to >= 0 && to < _text.chars.length) to else _text.chars.length
|
||||
selection.to = if (to >= 0 && to < _text.chars.length) to else _text.chars.length
|
||||
}
|
||||
|
||||
private def copySelection(): Unit = {
|
||||
@ -322,14 +321,14 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
}
|
||||
|
||||
private def pasteSelection(): Unit = {
|
||||
if (selector.active) deleteSelection()
|
||||
if (selection.active) deleteSelection()
|
||||
writeString(UiHandler.clipboard)
|
||||
}
|
||||
|
||||
private def deleteSelection(): Unit = {
|
||||
selector.active = false
|
||||
_text.chars = _text.chars.take(selector.start) ++ _text.chars.drop(selector.end)
|
||||
cursor.position = selector.start
|
||||
selection.active = false
|
||||
_text.chars = _text.chars.take(selection.start) ++ _text.chars.drop(selection.end)
|
||||
cursor.position = selection.start
|
||||
}
|
||||
|
||||
private def writeString(string: String): Unit = {
|
||||
@ -410,9 +409,9 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
|
||||
g.setScissor(position.x + 4, position.y, size.width - 8f, size.height)
|
||||
|
||||
if (selector.active) {
|
||||
val width = selectorEndOffset - selectorStartOffset
|
||||
g.rect(position.x + selectorStartOffset + 8 - scroll, position.y + 4, width, size.height - 8, BackgroundSelectedColor)
|
||||
if (selection.active) {
|
||||
val width = selectionEndOffset - selectionStartOffset
|
||||
g.rect(position.x + selectionStartOffset + 8 - scroll, position.y + 4, width, size.height - 8, BackgroundSelectedColor)
|
||||
}
|
||||
|
||||
g.background = Color.Transparent
|
||||
@ -422,8 +421,8 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
var charOffset = 0
|
||||
val charsToDisplay = if (_text.chars.nonEmpty || isFocused) _text.chars else placeholder
|
||||
for (char <- charsToDisplay) {
|
||||
if (selector.active) {
|
||||
g.foreground = if (charOffset >= selectorStartOffset && charOffset < selectorEndOffset) ForegroundSelectedColor else foreground
|
||||
if (selection.active) {
|
||||
g.foreground = if (charOffset >= selectionStartOffset && charOffset < selectionEndOffset) ForegroundSelectedColor else foreground
|
||||
}
|
||||
g.char(position.x + 8 + charOffset - scroll, position.y + 4, char)
|
||||
charOffset += g.font.charWidth(char)
|
||||
@ -444,7 +443,7 @@ object TextInput {
|
||||
def position: Int = value
|
||||
def position_=(newValue: Int): Unit = value = newValue
|
||||
}
|
||||
class Selector {
|
||||
class Selection {
|
||||
private val fromWatcher = Watcher(0)
|
||||
private val toWatcher = Watcher(0)
|
||||
|
||||
|
||||
@ -12,7 +12,9 @@ class Watcher[T](initialValue: T) {
|
||||
def value: T = _value
|
||||
def value_=(newValue: T): Unit = {
|
||||
dirty = _value != newValue
|
||||
_callback.foreach(_(newValue))
|
||||
if (dirty) {
|
||||
_callback.foreach(_(newValue))
|
||||
}
|
||||
_value = newValue
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user