diff --git a/src/main/scala/ocelot/desktop/ui/event/DoubleClickEvent.scala b/src/main/scala/ocelot/desktop/ui/event/DoubleClickEvent.scala new file mode 100644 index 0000000..de2c60b --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/event/DoubleClickEvent.scala @@ -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 diff --git a/src/main/scala/ocelot/desktop/ui/event/handlers/MouseHandler.scala b/src/main/scala/ocelot/desktop/ui/event/handlers/MouseHandler.scala index 05c4465..9d37c60 100644 --- a/src/main/scala/ocelot/desktop/ui/event/handlers/MouseHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/event/handlers/MouseHandler.scala @@ -2,8 +2,8 @@ package ocelot.desktop.ui.event.handlers import ocelot.desktop.geometry.Vector2D import ocelot.desktop.ui.UiHandler -import ocelot.desktop.ui.event.handlers.MouseHandler.Tolerance -import ocelot.desktop.ui.event.{ClickEvent, DragEvent, MouseEvent} +import ocelot.desktop.ui.event.handlers.MouseHandler.{DoubleClickTime, Tolerance} +import ocelot.desktop.ui.event.{ClickEvent, DoubleClickEvent, DragEvent, MouseEvent} import ocelot.desktop.ui.widget.Widget import scala.collection.mutable @@ -13,13 +13,18 @@ trait MouseHandler extends Widget { private val prevPositions = new mutable.HashMap[MouseEvent.Button.Value, Vector2D]() 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 protected def receiveClickEvents: 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. */ protected def allowClickReleaseOutsideThreshold: Boolean = !receiveDragEvents @@ -39,7 +44,7 @@ trait MouseHandler extends Widget { if (allowClickReleaseOutsideThreshold) { clippedBounds.contains(mousePos) } else { - (p - mousePos).lengthSquared < Tolerance * Tolerance + withinTolerance(p, mousePos) } }) ) @@ -50,11 +55,23 @@ trait MouseHandler extends Widget { if (clicked) { 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) } + private def withinTolerance(a: Vector2D, b: Vector2D): Boolean = (b - a).lengthSquared < Tolerance * Tolerance + override def update(): Unit = { super.update() @@ -65,7 +82,7 @@ trait MouseHandler extends Widget { val mousePos = UiHandler.mousePosition 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))) dragButtons += button prevPositions += (button -> mousePos) @@ -90,4 +107,5 @@ trait MouseHandler extends Widget { object MouseHandler { private val Tolerance = 8 + private val DoubleClickTime = 0.2 } diff --git a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala index 97e0e92..f2aca76 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala @@ -7,7 +7,7 @@ import ocelot.desktop.graphics.{Font, Graphics, IconSource} import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.handlers.MouseHandler 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.contextmenu.{ContextMenu, ContextMenuEntry} 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 org.lwjgl.input.Keyboard +import java.lang.Character.isWhitespace + class TextInput(val initialText: String = "") extends Widget with MouseHandler with HoverAnimation { 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 maximumSize: Size2D = Size2D(Float.PositiveInfinity, 24) + override def receiveAllMouseEvents = 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 @@ -121,7 +127,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w eventHandlers += { 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) focus() 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 => - val inBounds = clippedBounds.contains(UiHandler.mousePosition) - if (inBounds) { + if (mouseInBounds) { val menu = new ContextMenu if (selector.active) { 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) } + 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 => val pos = pixelToCursorPosition(mouse.x - bounds.x) selector.active = true @@ -238,6 +249,11 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w selectAll() 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, _) if isFocused && KeyEvents.isControlDown && selector.active => copySelection() @@ -288,6 +304,13 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w 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 = { UiHandler.clipboard = selectedText }