From 34ac83b5942673c6ae48f4ed0abec96bc60d4f49 Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Tue, 25 Oct 2022 23:31:50 +0200 Subject: [PATCH] Introduce mouse release and mouse drag events Introduce separate mouse release and mouse drag (move while pressed) events: MouseLeftRelease, MouseLeftDrag, MouseRightRelease etc, to allow binding them to actions independently from mouse press events (MouseLeft, MouseRight etc). This change: - Makes it possible to handle mouse release and drag for arbitrary mouse events and actions (including Lua actions), not just for MouseLeft as in the current code. - Fixes issue #2599 with PastePrimary and MouseMultiCursor actions: selection is pasted not only when pressing MouseMiddle but also when moving mouse with MouseMiddle pressed; similarly, a new multicursor is added not only when pressing Ctrl-MouseLeft but also when moving mouse with Ctrl-MouseLeft pressed. My initial approach was not to introduce new events for mouse release and mouse drag but to pass "mouse released" info to action functions in addition to *tcell.EventMouse to let the action functions do the necessary checks (similarly to what MousePress is already doing). But then I realized it was a bad idea, since we still want to be able also to bind mouse events to regular key actions (such as PastePrimary) which don't care about mouse event info. --- internal/action/actions.go | 117 +++++++++++++++++++---------- internal/action/bindings.go | 13 +++- internal/action/bufpane.go | 86 +++++++++------------ internal/action/defaults_darwin.go | 22 +++--- internal/action/defaults_other.go | 22 +++--- internal/action/events.go | 23 +++++- runtime/help/keybindings.md | 28 ++++--- 7 files changed, 190 insertions(+), 121 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index dd92a364..bd37decd 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -51,53 +51,47 @@ func (h *BufPane) ScrollAdjust() { func (h *BufPane) MousePress(e *tcell.EventMouse) bool { b := h.Buf mx, my := e.Position() + // ignore click on the status line + if my >= h.BufView().Y+h.BufView().Height { + return false + } mouseLoc := h.LocFromVisual(buffer.Loc{mx, my}) h.Cursor.Loc = mouseLoc - if h.mouseReleased { - if b.NumCursors() > 1 { - b.ClearCursors() - h.Relocate() - h.Cursor = h.Buf.GetActiveCursor() - h.Cursor.Loc = mouseLoc - } - if time.Since(h.lastClickTime)/time.Millisecond < config.DoubleClickThreshold && (mouseLoc.X == h.lastLoc.X && mouseLoc.Y == h.lastLoc.Y) { - if h.doubleClick { - // Triple click - h.lastClickTime = time.Now() - h.tripleClick = true - h.doubleClick = false - - h.Cursor.SelectLine() - h.Cursor.CopySelection(clipboard.PrimaryReg) - } else { - // Double click - h.lastClickTime = time.Now() - - h.doubleClick = true - h.tripleClick = false - - h.Cursor.SelectWord() - h.Cursor.CopySelection(clipboard.PrimaryReg) - } - } else { - h.doubleClick = false - h.tripleClick = false + if b.NumCursors() > 1 { + b.ClearCursors() + h.Relocate() + h.Cursor = h.Buf.GetActiveCursor() + h.Cursor.Loc = mouseLoc + } + if time.Since(h.lastClickTime)/time.Millisecond < config.DoubleClickThreshold && (mouseLoc.X == h.lastLoc.X && mouseLoc.Y == h.lastLoc.Y) { + if h.doubleClick { + // Triple click h.lastClickTime = time.Now() - h.Cursor.OrigSelection[0] = h.Cursor.Loc - h.Cursor.CurSelection[0] = h.Cursor.Loc - h.Cursor.CurSelection[1] = h.Cursor.Loc - } - h.mouseReleased = false - } else if !h.mouseReleased { - if h.tripleClick { - h.Cursor.AddLineToSelection() - } else if h.doubleClick { - h.Cursor.AddWordToSelection() + h.tripleClick = true + h.doubleClick = false + + h.Cursor.SelectLine() + h.Cursor.CopySelection(clipboard.PrimaryReg) } else { - h.Cursor.SetSelectionEnd(h.Cursor.Loc) + // Double click + h.lastClickTime = time.Now() + + h.doubleClick = true + h.tripleClick = false + + h.Cursor.SelectWord() + h.Cursor.CopySelection(clipboard.PrimaryReg) } + } else { + h.doubleClick = false + h.tripleClick = false + h.lastClickTime = time.Now() + + h.Cursor.OrigSelection[0] = h.Cursor.Loc + h.Cursor.CurSelection[0] = h.Cursor.Loc + h.Cursor.CurSelection[1] = h.Cursor.Loc } h.Cursor.StoreVisualX() @@ -106,6 +100,45 @@ func (h *BufPane) MousePress(e *tcell.EventMouse) bool { return true } +func (h *BufPane) MouseDrag(e *tcell.EventMouse) bool { + mx, my := e.Position() + // ignore drag on the status line + if my >= h.BufView().Y+h.BufView().Height { + return false + } + h.Cursor.Loc = h.LocFromVisual(buffer.Loc{mx, my}) + + if h.tripleClick { + h.Cursor.AddLineToSelection() + } else if h.doubleClick { + h.Cursor.AddWordToSelection() + } else { + h.Cursor.SetSelectionEnd(h.Cursor.Loc) + } + + h.Cursor.StoreVisualX() + h.Relocate() + return true +} + +func (h *BufPane) MouseRelease(e *tcell.EventMouse) bool { + // We could finish the selection based on the release location as in the + // commented out code below, to allow text selections even in a terminal + // that doesn't support mouse motion events. But when the mouse click is + // within the scroll margin, that would cause a scroll and selection + // even for a simple mouse click, which is not good. + // if !h.doubleClick && !h.tripleClick { + // mx, my := e.Position() + // h.Cursor.Loc = h.LocFromVisual(buffer.Loc{mx, my}) + // h.Cursor.SetSelectionEnd(h.Cursor.Loc) + // } + + if h.Cursor.HasSelection() { + h.Cursor.CopySelection(clipboard.PrimaryReg) + } + return true +} + // ScrollUpAction scrolls the view up func (h *BufPane) ScrollUpAction() bool { h.ScrollUp(util.IntOpt(h.Buf.Settings["scrollspeed"])) @@ -1794,6 +1827,10 @@ func (h *BufPane) SpawnMultiCursorSelect() bool { func (h *BufPane) MouseMultiCursor(e *tcell.EventMouse) bool { b := h.Buf mx, my := e.Position() + // ignore click on the status line + if my >= h.BufView().Y+h.BufView().Height { + return false + } mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my}) c := buffer.NewCursor(b, mouseLoc) b.AddCursor(c) diff --git a/internal/action/bindings.go b/internal/action/bindings.go index 846e3d11..e2bf10a0 100644 --- a/internal/action/bindings.go +++ b/internal/action/bindings.go @@ -201,11 +201,20 @@ modSearch: }, true } + var mstate MouseState = MousePress + if strings.HasSuffix(k, "Drag") { + k = k[:len(k)-4] + mstate = MouseDrag + } else if strings.HasSuffix(k, "Release") { + k = k[:len(k)-7] + mstate = MouseRelease + } // See if we can find the key in bindingMouse if code, ok := mouseEvents[k]; ok { return MouseEvent{ - btn: code, - mod: modifiers, + btn: code, + mod: modifiers, + state: mstate, }, true } diff --git a/internal/action/bufpane.go b/internal/action/bufpane.go index 847f5641..c6d527a1 100644 --- a/internal/action/bufpane.go +++ b/internal/action/bufpane.go @@ -8,7 +8,6 @@ import ( lua "github.com/yuin/gopher-lua" "github.com/zyedidia/micro/v2/internal/buffer" - "github.com/zyedidia/micro/v2/internal/clipboard" "github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/display" ulua "github.com/zyedidia/micro/v2/internal/lua" @@ -200,11 +199,15 @@ type BufPane struct { // Cursor is the currently active buffer cursor Cursor *buffer.Cursor - // Since tcell doesn't differentiate between a mouse release event - // and a mouse move event with no keys pressed, we need to keep - // track of whether or not the mouse was pressed (or not released) last event to determine - // mouse release events - mouseReleased bool + // Since tcell doesn't differentiate between a mouse press event + // and a mouse move event with button pressed (nor between a mouse + // release event and a mouse move event with no buttons pressed), + // we need to keep track of whether or not the mouse was previously + // pressed, to determine mouse release and mouse drag events. + // Moreover, since in case of a release event tcell doesn't tell us + // which button was released, we need to keep track of which + // (possibly multiple) buttons were pressed previously. + mousePressed map[MouseEvent]bool // We need to keep track of insert key press toggle isOverwriteMode bool @@ -250,7 +253,7 @@ func newBufPane(buf *buffer.Buffer, win display.BWindow, tab *Tab) *BufPane { h.tab = tab h.Cursor = h.Buf.GetActiveCursor() - h.mouseReleased = true + h.mousePressed = make(map[MouseEvent]bool) return h } @@ -332,7 +335,9 @@ func (h *BufPane) OpenBuffer(b *buffer.Buffer) { h.initialRelocate() // Set mouseReleased to true because we assume the mouse is not being // pressed when the editor is opened - h.mouseReleased = true + for me := range h.mousePressed { + delete(h.mousePressed, me) + } // Set isOverwriteMode to false, because we assume we are in the default // mode when editor is opened h.isOverwriteMode = false @@ -432,50 +437,31 @@ func (h *BufPane) HandleEvent(event tcell.Event) { h.DoRuneInsert(e.Rune()) } case *tcell.EventMouse: - cancel := false - switch e.Buttons() { - case tcell.Button1: - _, my := e.Position() - if h.Buf.Type.Kind != buffer.BTInfo.Kind && h.Buf.Settings["statusline"].(bool) && my >= h.GetView().Y+h.GetView().Height-1 { - cancel = true - } - case tcell.ButtonNone: - // Mouse event with no click - if !h.mouseReleased { - // Mouse was just released - - // mx, my := e.Position() - // mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my}) - - // we could finish the selection based on the release location as described - // below but when the mouse click is within the scroll margin this will - // cause a scroll and selection even for a simple mouse click which is - // not good - // for terminals that don't support mouse motion events, selection via - // the mouse won't work but this is ok - - // Relocating here isn't really necessary because the cursor will - // be in the right place from the last mouse event - // However, if we are running in a terminal that doesn't support mouse motion - // events, this still allows the user to make selections, except only after they - // release the mouse - - // if !h.doubleClick && !h.tripleClick { - // h.Cursor.SetSelectionEnd(h.Cursor.Loc) - // } - if h.Cursor.HasSelection() { - h.Cursor.CopySelection(clipboard.PrimaryReg) - } - h.mouseReleased = true - } - } - - if !cancel { + if e.Buttons() != tcell.ButtonNone { me := MouseEvent{ - btn: e.Buttons(), - mod: metaToAlt(e.Modifiers()), + btn: e.Buttons(), + mod: metaToAlt(e.Modifiers()), + state: MousePress, } + if len(h.mousePressed) > 0 { + me.state = MouseDrag + } + + if e.Buttons() & ^(tcell.WheelUp|tcell.WheelDown|tcell.WheelLeft|tcell.WheelRight) != tcell.ButtonNone { + h.mousePressed[me] = true + } + h.DoMouseEvent(me, e) + } else { + // Mouse event with no click - mouse was just released. + // If there were multiple mouse buttons pressed, we don't know which one + // was actually released, so we assume they all were released. + for me := range h.mousePressed { + delete(h.mousePressed, me) + + me.state = MouseRelease + h.DoMouseEvent(me, e) + } } } h.Buf.MergeCursors() @@ -788,6 +774,8 @@ var BufKeyActions = map[string]BufKeyAction{ // BufMouseActions contains the list of all possible mouse actions the bufhandler could execute var BufMouseActions = map[string]BufMouseAction{ "MousePress": (*BufPane).MousePress, + "MouseDrag": (*BufPane).MouseDrag, + "MouseRelease": (*BufPane).MouseRelease, "MouseMultiCursor": (*BufPane).MouseMultiCursor, } diff --git a/internal/action/defaults_darwin.go b/internal/action/defaults_darwin.go index bae69910..558ca888 100644 --- a/internal/action/defaults_darwin.go +++ b/internal/action/defaults_darwin.go @@ -90,11 +90,13 @@ var bufdefaults = map[string]string{ "Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch", // Mouse bindings - "MouseWheelUp": "ScrollUp", - "MouseWheelDown": "ScrollDown", - "MouseLeft": "MousePress", - "MouseMiddle": "PastePrimary", - "Ctrl-MouseLeft": "MouseMultiCursor", + "MouseWheelUp": "ScrollUp", + "MouseWheelDown": "ScrollDown", + "MouseLeft": "MousePress", + "MouseLeftDrag": "MouseDrag", + "MouseLeftRelease": "MouseRelease", + "MouseMiddle": "PastePrimary", + "Ctrl-MouseLeft": "MouseMultiCursor", "Alt-n": "SpawnMultiCursor", "AltShiftUp": "SpawnMultiCursorUp", @@ -173,8 +175,10 @@ var infodefaults = map[string]string{ "Esc": "AbortCommand", // Mouse bindings - "MouseWheelUp": "HistoryUp", - "MouseWheelDown": "HistoryDown", - "MouseLeft": "MousePress", - "MouseMiddle": "PastePrimary", + "MouseWheelUp": "HistoryUp", + "MouseWheelDown": "HistoryDown", + "MouseLeft": "MousePress", + "MouseLeftDrag": "MouseDrag", + "MouseLeftRelease": "MouseRelease", + "MouseMiddle": "PastePrimary", } diff --git a/internal/action/defaults_other.go b/internal/action/defaults_other.go index 39a9e0e3..d6ce27ea 100644 --- a/internal/action/defaults_other.go +++ b/internal/action/defaults_other.go @@ -92,11 +92,13 @@ var bufdefaults = map[string]string{ "Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch", // Mouse bindings - "MouseWheelUp": "ScrollUp", - "MouseWheelDown": "ScrollDown", - "MouseLeft": "MousePress", - "MouseMiddle": "PastePrimary", - "Ctrl-MouseLeft": "MouseMultiCursor", + "MouseWheelUp": "ScrollUp", + "MouseWheelDown": "ScrollDown", + "MouseLeft": "MousePress", + "MouseLeftDrag": "MouseDrag", + "MouseLeftRelease": "MouseRelease", + "MouseMiddle": "PastePrimary", + "Ctrl-MouseLeft": "MouseMultiCursor", "Alt-n": "SpawnMultiCursor", "Alt-m": "SpawnMultiCursorSelect", @@ -175,8 +177,10 @@ var infodefaults = map[string]string{ "Esc": "AbortCommand", // Mouse bindings - "MouseWheelUp": "HistoryUp", - "MouseWheelDown": "HistoryDown", - "MouseLeft": "MousePress", - "MouseMiddle": "PastePrimary", + "MouseWheelUp": "HistoryUp", + "MouseWheelDown": "HistoryDown", + "MouseLeft": "MousePress", + "MouseLeftDrag": "MouseDrag", + "MouseLeftRelease": "MouseRelease", + "MouseMiddle": "PastePrimary", } diff --git a/internal/action/events.go b/internal/action/events.go index 3a4834f6..4addf1b5 100644 --- a/internal/action/events.go +++ b/internal/action/events.go @@ -100,11 +100,20 @@ func (k KeySequenceEvent) Name() string { return buf.String() } +type MouseState int + +const ( + MousePress = iota + MouseDrag + MouseRelease +) + // MouseEvent is a mouse event with a mouse button and // any possible key modifiers type MouseEvent struct { - btn tcell.ButtonMask - mod tcell.ModMask + btn tcell.ButtonMask + mod tcell.ModMask + state MouseState } func (m MouseEvent) Name() string { @@ -122,9 +131,17 @@ func (m MouseEvent) Name() string { mod = "Ctrl-" } + state := "" + switch m.state { + case MouseDrag: + state = "Drag" + case MouseRelease: + state = "Release" + } + for k, v := range mouseEvents { if v == m.btn { - return fmt.Sprintf("%s%s", mod, k) + return fmt.Sprintf("%s%s%s", mod, k, state) } } return "" diff --git a/runtime/help/keybindings.md b/runtime/help/keybindings.md index 8d14da7d..753e6fdf 100644 --- a/runtime/help/keybindings.md +++ b/runtime/help/keybindings.md @@ -407,8 +407,14 @@ mouse actions) ``` MouseLeft +MouseLeftDrag +MouseLeftRelease MouseMiddle +MouseMiddleDrag +MouseMiddleRelease MouseRight +MouseRightDrag +MouseRightRelease MouseWheelUp MouseWheelDown MouseWheelLeft @@ -520,11 +526,13 @@ conventions for text editing defaults. "Esc": "Escape", // Mouse bindings - "MouseWheelUp": "ScrollUp", - "MouseWheelDown": "ScrollDown", - "MouseLeft": "MousePress", - "MouseMiddle": "PastePrimary", - "Ctrl-MouseLeft": "MouseMultiCursor", + "MouseWheelUp": "ScrollUp", + "MouseWheelDown": "ScrollDown", + "MouseLeft": "MousePress", + "MouseLeftDrag": "MouseDrag", + "MouseLeftRelease": "MouseRelease", + "MouseMiddle": "PastePrimary", + "Ctrl-MouseLeft": "MouseMultiCursor", "Alt-n": "SpawnMultiCursor", "AltShiftUp": "SpawnMultiCursorUp", @@ -629,10 +637,12 @@ are given below: "Esc": "AbortCommand", // Mouse bindings - "MouseWheelUp": "HistoryUp", - "MouseWheelDown": "HistoryDown", - "MouseLeft": "MousePress", - "MouseMiddle": "PastePrimary" + "MouseWheelUp": "HistoryUp", + "MouseWheelDown": "HistoryDown", + "MouseLeft": "MousePress", + "MouseLeftDrag": "MouseDrag", + "MouseLeftRelease": "MouseRelease", + "MouseMiddle": "PastePrimary" } } ```