Allow selecting current word by double click

This commit is contained in:
UnicornFreedom 2025-08-22 11:19:18 +02:00
parent a8dc52f1b2
commit 4e9d7c96e2
No known key found for this signature in database
GPG Key ID: B4ED0DB6B940024F
3 changed files with 55 additions and 9 deletions

View File

@ -0,0 +1,5 @@
package ocelot.desktop.ui.event
import ocelot.desktop.geometry.Vector2D
case class DoubleClickEvent(button: MouseEvent.Button.Value, mousePos: Vector2D) extends Event

View File

@ -2,8 +2,8 @@ package ocelot.desktop.ui.event.handlers
import ocelot.desktop.geometry.Vector2D import ocelot.desktop.geometry.Vector2D
import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.handlers.MouseHandler.Tolerance import ocelot.desktop.ui.event.handlers.MouseHandler.{DoubleClickTime, Tolerance}
import ocelot.desktop.ui.event.{ClickEvent, DragEvent, MouseEvent} import ocelot.desktop.ui.event.{ClickEvent, DoubleClickEvent, DragEvent, MouseEvent}
import ocelot.desktop.ui.widget.Widget import ocelot.desktop.ui.widget.Widget
import scala.collection.mutable import scala.collection.mutable
@ -13,13 +13,18 @@ trait MouseHandler extends Widget {
private val prevPositions = new mutable.HashMap[MouseEvent.Button.Value, Vector2D]() private val prevPositions = new mutable.HashMap[MouseEvent.Button.Value, Vector2D]()
private val dragButtons = new mutable.HashSet[MouseEvent.Button.Value]() private val dragButtons = new mutable.HashSet[MouseEvent.Button.Value]()
private var lastClickPosition: Vector2D = Vector2D.Zero
private var lastClickTime: Long = 0
private var lastClickButton = MouseEvent.Button.Left
override def receiveMouseEvents: Boolean = receiveClickEvents || receiveDragEvents override def receiveMouseEvents: Boolean = receiveClickEvents || receiveDragEvents
protected def receiveClickEvents: Boolean = false protected def receiveClickEvents: Boolean = false
protected def receiveDragEvents: Boolean = false protected def receiveDragEvents: Boolean = false
/** If `true`, a [[ClickEvent]] will be registered even if the mouse button is released /**
* If `true`, a [[ClickEvent]] will be registered even if the mouse button is released
* outside the tolerance threshold, as long as it stays within the widget's bounds. * outside the tolerance threshold, as long as it stays within the widget's bounds.
*/ */
protected def allowClickReleaseOutsideThreshold: Boolean = !receiveDragEvents protected def allowClickReleaseOutsideThreshold: Boolean = !receiveDragEvents
@ -39,7 +44,7 @@ trait MouseHandler extends Widget {
if (allowClickReleaseOutsideThreshold) { if (allowClickReleaseOutsideThreshold) {
clippedBounds.contains(mousePos) clippedBounds.contains(mousePos)
} else { } else {
(p - mousePos).lengthSquared < Tolerance * Tolerance withinTolerance(p, mousePos)
} }
}) })
) )
@ -50,11 +55,23 @@ trait MouseHandler extends Widget {
if (clicked) { if (clicked) {
handleEvent(ClickEvent(button, mousePos)) handleEvent(ClickEvent(button, mousePos))
val inTimeForDoubleClick = (System.currentTimeMillis() - lastClickTime) < DoubleClickTime * 1000
val sameButton = lastClickButton == button
val roughlySamePosition = withinTolerance(lastClickPosition, mousePos)
if (inTimeForDoubleClick && sameButton && roughlySamePosition) {
handleEvent(DoubleClickEvent(button, mousePos))
}
lastClickTime = System.currentTimeMillis()
lastClickPosition = mousePos
lastClickButton = button
} }
startPositions.remove(button) startPositions.remove(button)
} }
private def withinTolerance(a: Vector2D, b: Vector2D): Boolean = (b - a).lengthSquared < Tolerance * Tolerance
override def update(): Unit = { override def update(): Unit = {
super.update() super.update()
@ -65,7 +82,7 @@ trait MouseHandler extends Widget {
val mousePos = UiHandler.mousePosition val mousePos = UiHandler.mousePosition
for ((button, startPos) <- startPositions) { for ((button, startPos) <- startPositions) {
if (!dragButtons.contains(button) && (startPos - mousePos).lengthSquared > Tolerance * Tolerance) { if (!dragButtons.contains(button) && !withinTolerance(startPos, mousePos)) {
handleEvent(DragEvent(DragEvent.State.Start, button, mousePos, startPos, Vector2D(0, 0))) handleEvent(DragEvent(DragEvent.State.Start, button, mousePos, startPos, Vector2D(0, 0)))
dragButtons += button dragButtons += button
prevPositions += (button -> mousePos) prevPositions += (button -> mousePos)
@ -90,4 +107,5 @@ trait MouseHandler extends Widget {
object MouseHandler { object MouseHandler {
private val Tolerance = 8 private val Tolerance = 8
private val DoubleClickTime = 0.2
} }

View File

@ -7,7 +7,7 @@ import ocelot.desktop.graphics.{Font, Graphics, IconSource}
import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.handlers.MouseHandler import ocelot.desktop.ui.event.handlers.MouseHandler
import ocelot.desktop.ui.event.sources.KeyEvents import ocelot.desktop.ui.event.sources.KeyEvents
import ocelot.desktop.ui.event.{DragEvent, KeyEvent, MouseEvent} 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, Selector, Text}
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
import ocelot.desktop.ui.widget.traits.HoverAnimation import ocelot.desktop.ui.widget.traits.HoverAnimation
@ -15,6 +15,8 @@ import ocelot.desktop.util.{DrawUtils, Register, Watcher}
import ocelot.desktop.util.animation.ColorAnimation import ocelot.desktop.util.animation.ColorAnimation
import org.lwjgl.input.Keyboard import org.lwjgl.input.Keyboard
import java.lang.Character.isWhitespace
class TextInput(val initialText: String = "") extends Widget with MouseHandler with HoverAnimation { class TextInput(val initialText: String = "") extends Widget with MouseHandler with HoverAnimation {
private val CursorBlinkTime = 2f private val CursorBlinkTime = 2f
@ -98,8 +100,12 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
// ------------------------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------------------------
override def minimumSize: Size2D = Size2D(200, 24) override def minimumSize: Size2D = Size2D(200, 24)
override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 24) override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 24)
override def receiveAllMouseEvents = true override def receiveAllMouseEvents = true
override def receiveDragEvents: Boolean = true override def receiveDragEvents: Boolean = true
override def receiveClickEvents: Boolean = true
private def mouseInBounds: Boolean = clippedBounds.contains(UiHandler.mousePosition)
protected def font: Font = Font.NormalFont protected def font: Font = Font.NormalFont
@ -121,7 +127,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
eventHandlers += { eventHandlers += {
case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if enabled => case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if enabled =>
val inBounds = clippedBounds.contains(UiHandler.mousePosition) val inBounds = mouseInBounds
if (isFocused && !inBounds) unfocus() if (isFocused && !inBounds) unfocus()
if (!isFocused && inBounds) focus() if (!isFocused && inBounds) focus()
if (isFocused) { if (isFocused) {
@ -132,8 +138,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
} }
case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Right) if isFocused => case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Right) if isFocused =>
val inBounds = clippedBounds.contains(UiHandler.mousePosition) if (mouseInBounds) {
if (inBounds) {
val menu = new ContextMenu val menu = new ContextMenu
if (selector.active) { if (selector.active) {
menu.addEntry(ContextMenuEntry("Cut", IconSource.Icons.Cut) { cutSelection() }) menu.addEntry(ContextMenuEntry("Cut", IconSource.Icons.Cut) { cutSelection() })
@ -149,6 +154,12 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
root.get.contextMenus.open(menu) root.get.contextMenus.open(menu)
} }
case event @ DoubleClickEvent(MouseEvent.Button.Left, _) if isFocused =>
if (mouseInBounds) {
selectWord()
event.consume()
}
case event @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, mouse) if isFocused => case event @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, mouse) if isFocused =>
val pos = pixelToCursorPosition(mouse.x - bounds.x) val pos = pixelToCursorPosition(mouse.x - bounds.x)
selector.active = true selector.active = true
@ -238,6 +249,11 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
selectAll() selectAll()
event.consume() event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_W, _)
if isFocused && KeyEvents.isControlDown =>
selectWord()
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_C, _) case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_C, _)
if isFocused && KeyEvents.isControlDown && selector.active => if isFocused && KeyEvents.isControlDown && selector.active =>
copySelection() copySelection()
@ -288,6 +304,13 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
selector.to = _text.chars.length selector.to = _text.chars.length
} }
private def selectWord(): Unit = {
selector.active = true
selector.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
}
private def copySelection(): Unit = { private def copySelection(): Unit = {
UiHandler.clipboard = selectedText UiHandler.clipboard = selectedText
} }