Merge pull request #2606 from dmaluka/mouse-release-and-drag-events

Introduce mouse release and mouse drag events
This commit is contained in:
Dmytro Maluka
2024-03-14 03:54:04 +01:00
committed by GitHub
9 changed files with 249 additions and 141 deletions

View File

@@ -51,53 +51,47 @@ func (h *BufPane) ScrollAdjust() {
func (h *BufPane) MousePress(e *tcell.EventMouse) bool { func (h *BufPane) MousePress(e *tcell.EventMouse) bool {
b := h.Buf b := h.Buf
mx, my := e.Position() 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}) mouseLoc := h.LocFromVisual(buffer.Loc{mx, my})
h.Cursor.Loc = mouseLoc 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 if b.NumCursors() > 1 {
h.doubleClick = false b.ClearCursors()
h.Relocate()
h.Cursor.SelectLine() h.Cursor = h.Buf.GetActiveCursor()
h.Cursor.CopySelection(clipboard.PrimaryReg) h.Cursor.Loc = mouseLoc
} else { }
// Double click if time.Since(h.lastClickTime)/time.Millisecond < config.DoubleClickThreshold && (mouseLoc.X == h.lastLoc.X && mouseLoc.Y == h.lastLoc.Y) {
h.lastClickTime = time.Now() if h.doubleClick {
// Triple click
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.lastClickTime = time.Now()
h.Cursor.OrigSelection[0] = h.Cursor.Loc h.tripleClick = true
h.Cursor.CurSelection[0] = h.Cursor.Loc h.doubleClick = false
h.Cursor.CurSelection[1] = h.Cursor.Loc
} h.Cursor.SelectLine()
h.mouseReleased = false h.Cursor.CopySelection(clipboard.PrimaryReg)
} else if !h.mouseReleased {
if h.tripleClick {
h.Cursor.AddLineToSelection()
} else if h.doubleClick {
h.Cursor.AddWordToSelection()
} else { } 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() h.Cursor.StoreVisualX()
@@ -106,6 +100,45 @@ func (h *BufPane) MousePress(e *tcell.EventMouse) bool {
return true 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 // ScrollUpAction scrolls the view up
func (h *BufPane) ScrollUpAction() bool { func (h *BufPane) ScrollUpAction() bool {
h.ScrollUp(util.IntOpt(h.Buf.Settings["scrollspeed"])) h.ScrollUp(util.IntOpt(h.Buf.Settings["scrollspeed"]))
@@ -1855,6 +1888,10 @@ func (h *BufPane) SpawnMultiCursorSelect() bool {
func (h *BufPane) MouseMultiCursor(e *tcell.EventMouse) bool { func (h *BufPane) MouseMultiCursor(e *tcell.EventMouse) bool {
b := h.Buf b := h.Buf
mx, my := e.Position() 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}) mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my})
if h.Buf.NumCursors() > 1 { if h.Buf.NumCursors() > 1 {

View File

@@ -201,11 +201,20 @@ modSearch:
}, true }, 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 // See if we can find the key in bindingMouse
if code, ok := mouseEvents[k]; ok { if code, ok := mouseEvents[k]; ok {
return MouseEvent{ return MouseEvent{
btn: code, btn: code,
mod: modifiers, mod: modifiers,
state: mstate,
}, true }, true
} }

View File

@@ -8,7 +8,6 @@ import (
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/micro/v2/internal/buffer" "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/config"
"github.com/zyedidia/micro/v2/internal/display" "github.com/zyedidia/micro/v2/internal/display"
ulua "github.com/zyedidia/micro/v2/internal/lua" ulua "github.com/zyedidia/micro/v2/internal/lua"
@@ -211,11 +210,15 @@ type BufPane struct {
// Cursor is the currently active buffer cursor // Cursor is the currently active buffer cursor
Cursor *buffer.Cursor Cursor *buffer.Cursor
// Since tcell doesn't differentiate between a mouse release event // Since tcell doesn't differentiate between a mouse press event
// and a mouse move event with no keys pressed, we need to keep // and a mouse move event with button pressed (nor between a mouse
// track of whether or not the mouse was pressed (or not released) last event to determine // release event and a mouse move event with no buttons pressed),
// mouse release events // we need to keep track of whether or not the mouse was previously
mouseReleased bool // 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 // We need to keep track of insert key press toggle
isOverwriteMode bool isOverwriteMode bool
@@ -261,7 +264,7 @@ func newBufPane(buf *buffer.Buffer, win display.BWindow, tab *Tab) *BufPane {
h.tab = tab h.tab = tab
h.Cursor = h.Buf.GetActiveCursor() h.Cursor = h.Buf.GetActiveCursor()
h.mouseReleased = true h.mousePressed = make(map[MouseEvent]bool)
return h return h
} }
@@ -333,6 +336,12 @@ func (h *BufPane) PluginCBRune(cb string, r rune) bool {
return b return b
} }
func (h *BufPane) resetMouse() {
for me := range h.mousePressed {
delete(h.mousePressed, me)
}
}
// OpenBuffer opens the given buffer in this pane. // OpenBuffer opens the given buffer in this pane.
func (h *BufPane) OpenBuffer(b *buffer.Buffer) { func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
h.Buf.Close() h.Buf.Close()
@@ -343,7 +352,7 @@ func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
h.initialRelocate() h.initialRelocate()
// Set mouseReleased to true because we assume the mouse is not being // Set mouseReleased to true because we assume the mouse is not being
// pressed when the editor is opened // pressed when the editor is opened
h.mouseReleased = true h.resetMouse()
// Set isOverwriteMode to false, because we assume we are in the default // Set isOverwriteMode to false, because we assume we are in the default
// mode when editor is opened // mode when editor is opened
h.isOverwriteMode = false h.isOverwriteMode = false
@@ -457,50 +466,32 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
h.DoRuneInsert(e.Rune()) h.DoRuneInsert(e.Rune())
} }
case *tcell.EventMouse: case *tcell.EventMouse:
cancel := false if e.Buttons() != tcell.ButtonNone {
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 {
me := MouseEvent{ me := MouseEvent{
btn: e.Buttons(), btn: e.Buttons(),
mod: metaToAlt(e.Modifiers()), mod: metaToAlt(e.Modifiers()),
state: MousePress,
}
isDrag := len(h.mousePressed) > 0
if e.Buttons() & ^(tcell.WheelUp|tcell.WheelDown|tcell.WheelLeft|tcell.WheelRight) != tcell.ButtonNone {
h.mousePressed[me] = true
}
if isDrag {
me.state = MouseDrag
} }
h.DoMouseEvent(me, e) 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() h.Buf.MergeCursors()
@@ -829,6 +820,8 @@ var BufKeyActions = map[string]BufKeyAction{
// BufMouseActions contains the list of all possible mouse actions the bufhandler could execute // BufMouseActions contains the list of all possible mouse actions the bufhandler could execute
var BufMouseActions = map[string]BufMouseAction{ var BufMouseActions = map[string]BufMouseAction{
"MousePress": (*BufPane).MousePress, "MousePress": (*BufPane).MousePress,
"MouseDrag": (*BufPane).MouseDrag,
"MouseRelease": (*BufPane).MouseRelease,
"MouseMultiCursor": (*BufPane).MouseMultiCursor, "MouseMultiCursor": (*BufPane).MouseMultiCursor,
} }

View File

@@ -92,11 +92,13 @@ var bufdefaults = map[string]string{
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch", "Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
// Mouse bindings // Mouse bindings
"MouseWheelUp": "ScrollUp", "MouseWheelUp": "ScrollUp",
"MouseWheelDown": "ScrollDown", "MouseWheelDown": "ScrollDown",
"MouseLeft": "MousePress", "MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary", "MouseLeftDrag": "MouseDrag",
"Ctrl-MouseLeft": "MouseMultiCursor", "MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
"Alt-n": "SpawnMultiCursor", "Alt-n": "SpawnMultiCursor",
"AltShiftUp": "SpawnMultiCursorUp", "AltShiftUp": "SpawnMultiCursorUp",
@@ -175,8 +177,10 @@ var infodefaults = map[string]string{
"Esc": "AbortCommand", "Esc": "AbortCommand",
// Mouse bindings // Mouse bindings
"MouseWheelUp": "HistoryUp", "MouseWheelUp": "HistoryUp",
"MouseWheelDown": "HistoryDown", "MouseWheelDown": "HistoryDown",
"MouseLeft": "MousePress", "MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary", "MouseLeftDrag": "MouseDrag",
"MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
} }

View File

@@ -95,11 +95,13 @@ var bufdefaults = map[string]string{
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch", "Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
// Mouse bindings // Mouse bindings
"MouseWheelUp": "ScrollUp", "MouseWheelUp": "ScrollUp",
"MouseWheelDown": "ScrollDown", "MouseWheelDown": "ScrollDown",
"MouseLeft": "MousePress", "MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary", "MouseLeftDrag": "MouseDrag",
"Ctrl-MouseLeft": "MouseMultiCursor", "MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
"Alt-n": "SpawnMultiCursor", "Alt-n": "SpawnMultiCursor",
"Alt-m": "SpawnMultiCursorSelect", "Alt-m": "SpawnMultiCursorSelect",
@@ -178,8 +180,10 @@ var infodefaults = map[string]string{
"Esc": "AbortCommand", "Esc": "AbortCommand",
// Mouse bindings // Mouse bindings
"MouseWheelUp": "HistoryUp", "MouseWheelUp": "HistoryUp",
"MouseWheelDown": "HistoryDown", "MouseWheelDown": "HistoryDown",
"MouseLeft": "MousePress", "MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary", "MouseLeftDrag": "MouseDrag",
"MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
} }

View File

@@ -100,11 +100,20 @@ func (k KeySequenceEvent) Name() string {
return buf.String() return buf.String()
} }
type MouseState int
const (
MousePress = iota
MouseDrag
MouseRelease
)
// MouseEvent is a mouse event with a mouse button and // MouseEvent is a mouse event with a mouse button and
// any possible key modifiers // any possible key modifiers
type MouseEvent struct { type MouseEvent struct {
btn tcell.ButtonMask btn tcell.ButtonMask
mod tcell.ModMask mod tcell.ModMask
state MouseState
} }
func (m MouseEvent) Name() string { func (m MouseEvent) Name() string {
@@ -122,9 +131,17 @@ func (m MouseEvent) Name() string {
mod = "Ctrl-" mod = "Ctrl-"
} }
state := ""
switch m.state {
case MouseDrag:
state = "Drag"
case MouseRelease:
state = "Release"
}
for k, v := range mouseEvents { for k, v := range mouseEvents {
if v == m.btn { if v == m.btn {
return fmt.Sprintf("%s%s", mod, k) return fmt.Sprintf("%s%s%s", mod, k, state)
} }
} }
return "" return ""

View File

@@ -164,6 +164,21 @@ func InitTabs(bufs []*buffer.Buffer) {
} }
} }
} }
screen.RestartCallback = func() {
// The mouse could be released after the screen was stopped, so that
// we couldn't catch the mouse release event and would erroneously think
// that it is still pressed. So need to reset the mouse release state
// after the screen is restarted.
for _, t := range Tabs.List {
t.release = true
for _, p := range t.Panes {
if bp, ok := p.(*BufPane); ok {
bp.resetMouse()
}
}
}
}
} }
func MainTab() *Tab { func MainTab() *Tab {
@@ -214,34 +229,40 @@ func NewTabFromPane(x, y, width, height int, pane Pane) *Tab {
// HandleEvent takes a tcell event and usually dispatches it to the current // HandleEvent takes a tcell event and usually dispatches it to the current
// active pane. However if the event is a resize or a mouse event where the user // active pane. However if the event is a resize or a mouse event where the user
// is interacting with the UI (resizing splits) then the event is consumed here // is interacting with the UI (resizing splits) then the event is consumed here
// If the event is a mouse event in a pane, that pane will become active and get // If the event is a mouse press event in a pane, that pane will become active
// the event // and get the event
func (t *Tab) HandleEvent(event tcell.Event) { func (t *Tab) HandleEvent(event tcell.Event) {
switch e := event.(type) { switch e := event.(type) {
case *tcell.EventMouse: case *tcell.EventMouse:
mx, my := e.Position() mx, my := e.Position()
switch e.Buttons() { btn := e.Buttons()
case tcell.Button1: switch {
case btn & ^(tcell.WheelUp|tcell.WheelDown|tcell.WheelLeft|tcell.WheelRight) != tcell.ButtonNone:
// button press or drag
wasReleased := t.release wasReleased := t.release
t.release = false t.release = false
if t.resizing != nil {
var size int if btn == tcell.Button1 {
if t.resizing.Kind == views.STVert { if t.resizing != nil {
size = mx - t.resizing.X var size int
} else { if t.resizing.Kind == views.STVert {
size = my - t.resizing.Y + 1 size = mx - t.resizing.X
} else {
size = my - t.resizing.Y + 1
}
t.resizing.ResizeSplit(size)
t.Resize()
return
}
if wasReleased {
t.resizing = t.GetMouseSplitNode(buffer.Loc{mx, my})
if t.resizing != nil {
return
}
} }
t.resizing.ResizeSplit(size)
t.Resize()
return
} }
if wasReleased { if wasReleased {
t.resizing = t.GetMouseSplitNode(buffer.Loc{mx, my})
if t.resizing != nil {
return
}
for i, p := range t.Panes { for i, p := range t.Panes {
v := p.GetView() v := p.GetView()
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
@@ -251,10 +272,15 @@ func (t *Tab) HandleEvent(event tcell.Event) {
} }
} }
} }
case tcell.ButtonNone: case btn == tcell.ButtonNone:
t.resizing = nil // button release
t.release = true t.release = true
if t.resizing != nil {
t.resizing = nil
return
}
default: default:
// wheel move
for _, p := range t.Panes { for _, p := range t.Panes {
v := p.GetView() v := p.GetView()
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height

View File

@@ -22,6 +22,10 @@ var Screen tcell.Screen
// Events is the channel of tcell events // Events is the channel of tcell events
var Events chan (tcell.Event) var Events chan (tcell.Event)
// RestartCallback is called when the screen is restarted after it was
// temporarily shut down
var RestartCallback func()
// The lock is necessary since the screen is polled on a separate thread // The lock is necessary since the screen is polled on a separate thread
var lock sync.Mutex var lock sync.Mutex
@@ -134,6 +138,10 @@ func TempStart(screenWasNil bool) {
if !screenWasNil { if !screenWasNil {
Init() Init()
Unlock() Unlock()
if RestartCallback != nil {
RestartCallback()
}
} }
} }

View File

@@ -409,8 +409,14 @@ mouse actions)
``` ```
MouseLeft MouseLeft
MouseLeftDrag
MouseLeftRelease
MouseMiddle MouseMiddle
MouseMiddleDrag
MouseMiddleRelease
MouseRight MouseRight
MouseRightDrag
MouseRightRelease
MouseWheelUp MouseWheelUp
MouseWheelDown MouseWheelDown
MouseWheelLeft MouseWheelLeft
@@ -524,11 +530,13 @@ conventions for text editing defaults.
"Esc": "Escape", "Esc": "Escape",
// Mouse bindings // Mouse bindings
"MouseWheelUp": "ScrollUp", "MouseWheelUp": "ScrollUp",
"MouseWheelDown": "ScrollDown", "MouseWheelDown": "ScrollDown",
"MouseLeft": "MousePress", "MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary", "MouseLeftDrag": "MouseDrag",
"Ctrl-MouseLeft": "MouseMultiCursor", "MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
// Multi-cursor bindings // Multi-cursor bindings
"Alt-n": "SpawnMultiCursor", "Alt-n": "SpawnMultiCursor",
@@ -634,10 +642,12 @@ are given below:
"Esc": "AbortCommand", "Esc": "AbortCommand",
// Mouse bindings // Mouse bindings
"MouseWheelUp": "HistoryUp", "MouseWheelUp": "HistoryUp",
"MouseWheelDown": "HistoryDown", "MouseWheelDown": "HistoryDown",
"MouseLeft": "MousePress", "MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary" "MouseLeftDrag": "MouseDrag",
"MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary"
} }
} }
``` ```