diff --git a/internal/action/actions.go b/internal/action/actions.go index e6cd1a5d..620e8516 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"])) @@ -1855,6 +1888,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}) if h.Buf.NumCursors() > 1 { 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 1a9b14f8..200a972d 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" @@ -211,11 +210,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 @@ -261,7 +264,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 } @@ -333,6 +336,12 @@ func (h *BufPane) PluginCBRune(cb string, r rune) bool { return b } +func (h *BufPane) resetMouse() { + for me := range h.mousePressed { + delete(h.mousePressed, me) + } +} + // OpenBuffer opens the given buffer in this pane. func (h *BufPane) OpenBuffer(b *buffer.Buffer) { h.Buf.Close() @@ -343,7 +352,7 @@ 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 + h.resetMouse() // Set isOverwriteMode to false, because we assume we are in the default // mode when editor is opened h.isOverwriteMode = false @@ -457,50 +466,32 @@ 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, + } + 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) + } 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() @@ -829,6 +820,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 24b1f060..e1a54b79 100644 --- a/internal/action/defaults_darwin.go +++ b/internal/action/defaults_darwin.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", "AltShiftUp": "SpawnMultiCursorUp", @@ -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/defaults_other.go b/internal/action/defaults_other.go index 2edc48e0..a932688a 100644 --- a/internal/action/defaults_other.go +++ b/internal/action/defaults_other.go @@ -95,11 +95,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", @@ -178,8 +180,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/internal/action/tab.go b/internal/action/tab.go index 600ccc55..357eefbf 100644 --- a/internal/action/tab.go +++ b/internal/action/tab.go @@ -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 { @@ -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 // 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 -// If the event is a mouse event in a pane, that pane will become active and get -// the event +// If the event is a mouse press event in a pane, that pane will become active +// and get the event func (t *Tab) HandleEvent(event tcell.Event) { switch e := event.(type) { case *tcell.EventMouse: mx, my := e.Position() - switch e.Buttons() { - case tcell.Button1: + btn := e.Buttons() + switch { + case btn & ^(tcell.WheelUp|tcell.WheelDown|tcell.WheelLeft|tcell.WheelRight) != tcell.ButtonNone: + // button press or drag wasReleased := t.release t.release = false - if t.resizing != nil { - var size int - if t.resizing.Kind == views.STVert { - size = mx - t.resizing.X - } else { - size = my - t.resizing.Y + 1 + + if btn == tcell.Button1 { + if t.resizing != nil { + var size int + if t.resizing.Kind == views.STVert { + 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 { - t.resizing = t.GetMouseSplitNode(buffer.Loc{mx, my}) - if t.resizing != nil { - return - } - for i, p := range t.Panes { v := p.GetView() 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: - t.resizing = nil + case btn == tcell.ButtonNone: + // button release t.release = true + if t.resizing != nil { + t.resizing = nil + return + } default: + // wheel move for _, p := range t.Panes { v := p.GetView() inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height diff --git a/internal/screen/screen.go b/internal/screen/screen.go index 339e69aa..16c011e6 100644 --- a/internal/screen/screen.go +++ b/internal/screen/screen.go @@ -22,6 +22,10 @@ var Screen tcell.Screen // Events is the channel of tcell events 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 var lock sync.Mutex @@ -134,6 +138,10 @@ func TempStart(screenWasNil bool) { if !screenWasNil { Init() Unlock() + + if RestartCallback != nil { + RestartCallback() + } } } diff --git a/runtime/help/keybindings.md b/runtime/help/keybindings.md index 9670d888..90ad0205 100644 --- a/runtime/help/keybindings.md +++ b/runtime/help/keybindings.md @@ -409,8 +409,14 @@ mouse actions) ``` MouseLeft +MouseLeftDrag +MouseLeftRelease MouseMiddle +MouseMiddleDrag +MouseMiddleRelease MouseRight +MouseRightDrag +MouseRightRelease MouseWheelUp MouseWheelDown MouseWheelLeft @@ -524,11 +530,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", // Multi-cursor bindings "Alt-n": "SpawnMultiCursor", @@ -634,10 +642,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" } } ```