From 3270acdd000bd453a6c83671485f2e8913f3b2eb Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Sun, 11 Jun 2017 17:49:59 -0400 Subject: [PATCH] Add functionality for binding mouse buttons This commit enables users to bind the mouse buttons (left, middle, right buttons and the scroll wheel). The default bindings now include the mouse bindings: "MouseWheelUp": "ScrollUp", "MouseWheelDown": "ScrollDown", "MouseLeft": "MousePress", "MouseMiddle": "PastePrimary", Mouse buttons can now also be bound to normal actions. For example: "MouseLeft": "Backspace" This also means that plugins can access mouse event callbacks in the standard way ('onAction'). More documentation for this will be coming soon. Fixes #542 --- cmd/micro/actions.go | 101 +++++++++++++++++++++++++++++++-- cmd/micro/bindings.go | 72 ++++++++++++++++++++++-- cmd/micro/plugin.go | 11 ++++ cmd/micro/view.go | 127 +++++++++++++++--------------------------- 4 files changed, 218 insertions(+), 93 deletions(-) diff --git a/cmd/micro/actions.go b/cmd/micro/actions.go index a4aab016..d8e7991c 100644 --- a/cmd/micro/actions.go +++ b/cmd/micro/actions.go @@ -8,13 +8,14 @@ import ( "github.com/yuin/gopher-lua" "github.com/zyedidia/clipboard" + "github.com/zyedidia/tcell" ) // PreActionCall executes the lua pre callback if possible -func PreActionCall(funcName string, view *View) bool { +func PreActionCall(funcName string, view *View, args ...interface{}) bool { executeAction := true for pl := range loadedPlugins { - ret, err := Call(pl+".pre"+funcName, view) + ret, err := Call(pl+".pre"+funcName, append([]interface{}{view}, args...)...) if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { TermMessage(err) continue @@ -27,10 +28,10 @@ func PreActionCall(funcName string, view *View) bool { } // PostActionCall executes the lua plugin callback if possible -func PostActionCall(funcName string, view *View) bool { +func PostActionCall(funcName string, view *View, args ...interface{}) bool { relocate := true for pl := range loadedPlugins { - ret, err := Call(pl+".on"+funcName, view) + ret, err := Call(pl+".on"+funcName, append([]interface{}{view}, args...)...) if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { TermMessage(err) continue @@ -51,6 +52,98 @@ func (v *View) deselect(index int) bool { return false } +// MousePress is the event that should happen when a normal click happens +// This is almost always bound to left click +func (v *View) MousePress(usePlugin bool, e *tcell.EventMouse) bool { + if usePlugin && !PreActionCall("MousePress", v, e) { + return false + } + + x, y := e.Position() + x -= v.lineNumOffset - v.leftCol + v.x + y += v.Topline - v.y + + // This is usually bound to left click + if v.mouseReleased { + v.MoveToMouseClick(x, y) + if time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold { + if v.doubleClick { + // Triple click + v.lastClickTime = time.Now() + + v.tripleClick = true + v.doubleClick = false + + v.Cursor.SelectLine() + v.Cursor.CopySelection("primary") + } else { + // Double click + v.lastClickTime = time.Now() + + v.doubleClick = true + v.tripleClick = false + + v.Cursor.SelectWord() + v.Cursor.CopySelection("primary") + } + } else { + v.doubleClick = false + v.tripleClick = false + v.lastClickTime = time.Now() + + v.Cursor.OrigSelection[0] = v.Cursor.Loc + v.Cursor.CurSelection[0] = v.Cursor.Loc + v.Cursor.CurSelection[1] = v.Cursor.Loc + } + v.mouseReleased = false + } else if !v.mouseReleased { + v.MoveToMouseClick(x, y) + if v.tripleClick { + v.Cursor.AddLineToSelection() + } else if v.doubleClick { + v.Cursor.AddWordToSelection() + } else { + v.Cursor.SetSelectionEnd(v.Cursor.Loc) + v.Cursor.CopySelection("primary") + } + } + + if usePlugin { + PostActionCall("MousePress", v, e) + } + return false +} + +// ScrollUpAction scrolls the view up +func (v *View) ScrollUpAction(usePlugin bool) bool { + if usePlugin && !PreActionCall("ScrollUp", v) { + return false + } + + scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64)) + v.ScrollUp(scrollspeed) + + if usePlugin { + PostActionCall("ScrollUp", v) + } + return false +} + +// ScrollDownAction scrolls the view up +func (v *View) ScrollDownAction(usePlugin bool) bool { + if usePlugin && !PreActionCall("ScrollDown", v) { + return false + } + + scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64)) + v.ScrollDown(scrollspeed) + + if usePlugin { + PostActionCall("ScrollDown", v) + } + return false +} + // Center centers the view on the cursor func (v *View) Center(usePlugin bool) bool { if usePlugin && !PreActionCall("Center", v) { diff --git a/cmd/micro/bindings.go b/cmd/micro/bindings.go index fc44fbc7..f18b2301 100644 --- a/cmd/micro/bindings.go +++ b/cmd/micro/bindings.go @@ -10,8 +10,13 @@ import ( ) var bindings map[Key][]func(*View, bool) bool +var mouseBindings map[Key][]func(*View, bool, *tcell.EventMouse) bool var helpBinding string +var mouseBindingActions = map[string]func(*View, bool, *tcell.EventMouse) bool{ + "MousePress": (*View).MousePress, +} + var bindingActions = map[string]func(*View, bool) bool{ "CursorUp": (*View).CursorUp, "CursorDown": (*View).CursorDown, @@ -91,11 +96,23 @@ var bindingActions = map[string]func(*View, bool) bool{ "ToggleMacro": (*View).ToggleMacro, "PlayMacro": (*View).PlayMacro, "Suspend": (*View).Suspend, + "ScrollUp": (*View).ScrollUpAction, + "ScrollDown": (*View).ScrollDownAction, // This was changed to InsertNewline but I don't want to break backwards compatibility "InsertEnter": (*View).InsertNewline, } +var bindingMouse = map[string]tcell.ButtonMask{ + "MouseLeft": tcell.Button1, + "MouseMiddle": tcell.Button2, + "MouseRight": tcell.Button3, + "MouseWheelUp": tcell.WheelUp, + "MouseWheelDown": tcell.WheelDown, + "MouseWheelLeft": tcell.WheelLeft, + "MouseWheelRight": tcell.WheelRight, +} + var bindingKeys = map[string]tcell.Key{ "Up": tcell.KeyUp, "Down": tcell.KeyDown, @@ -230,12 +247,14 @@ var bindingKeys = map[string]tcell.Key{ type Key struct { keyCode tcell.Key modifiers tcell.ModMask + buttons tcell.ButtonMask r rune } // InitBindings initializes the keybindings for micro func InitBindings() { bindings = make(map[Key][]func(*View, bool) bool) + mouseBindings = make(map[Key][]func(*View, bool, *tcell.EventMouse) bool) var parsed map[string]string defaults := DefaultBindings() @@ -301,6 +320,7 @@ modSearch: return Key{ keyCode: code, modifiers: modifiers, + buttons: -1, r: 0, }, true } @@ -311,6 +331,16 @@ modSearch: return Key{ keyCode: code, modifiers: modifiers, + buttons: -1, + r: 0, + }, true + } + + // See if we can find the key in bindingMouse + if code, ok := bindingMouse[k]; ok { + return Key{ + modifiers: modifiers, + buttons: code, r: 0, }, true } @@ -320,12 +350,13 @@ modSearch: return Key{ keyCode: tcell.KeyRune, modifiers: modifiers, + buttons: -1, r: rune(k[0]), }, true } // We don't know what happened. - return Key{}, false + return Key{buttons: -1}, false } // findAction will find 'action' using string 'v' @@ -339,6 +370,16 @@ func findAction(v string) (action func(*View, bool) bool) { return action } +func findMouseAction(v string) func(*View, bool, *tcell.EventMouse) bool { + action, ok := mouseBindingActions[v] + if !ok { + // If the user seems to be binding a function that doesn't exist + // We hope that it's a lua function that exists and bind it to that + action = LuaFunctionMouseBinding(v) + } + return action +} + // BindKey takes a key and an action and binds the two together func BindKey(k, v string) { key, ok := findKey(k) @@ -356,18 +397,31 @@ func BindKey(k, v string) { actionNames := strings.Split(v, ",") if actionNames[0] == "UnbindKey" { delete(bindings, key) + delete(mouseBindings, key) if len(actionNames) == 1 { - actionNames = make([]string, 0, 0) - } else { - actionNames = append(actionNames[:0], actionNames[1:]...) + return } + actionNames = append(actionNames[:0], actionNames[1:]...) } actions := make([]func(*View, bool) bool, 0, len(actionNames)) + mouseActions := make([]func(*View, bool, *tcell.EventMouse) bool, 0, len(actionNames)) for _, actionName := range actionNames { - actions = append(actions, findAction(actionName)) + if strings.HasPrefix(actionName, "Mouse") { + mouseActions = append(mouseActions, findMouseAction(actionName)) + } else { + actions = append(actions, findAction(actionName)) + } } - bindings[key] = actions + if len(actions) > 0 { + // Can't have a binding be both mouse and normal + delete(mouseBindings, key) + bindings[key] = actions + } else if len(mouseActions) > 0 { + // Can't have a binding be both mouse and normal + delete(bindings, key) + mouseBindings[key] = mouseActions + } } // DefaultBindings returns a map containing micro's default keybindings @@ -453,5 +507,11 @@ func DefaultBindings() map[string]string { "F7": "Find", "F10": "Quit", "Esc": "Escape", + + // Mouse bindings + "MouseWheelUp": "ScrollUp", + "MouseWheelDown": "ScrollDown", + "MouseLeft": "MousePress", + "MouseMiddle": "PastePrimary", } } diff --git a/cmd/micro/plugin.go b/cmd/micro/plugin.go index eba09f33..fba26628 100644 --- a/cmd/micro/plugin.go +++ b/cmd/micro/plugin.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/yuin/gopher-lua" + "github.com/zyedidia/tcell" "layeh.com/gopher-luar" ) @@ -60,6 +61,16 @@ func LuaFunctionBinding(function string) func(*View, bool) bool { } } +func LuaFunctionMouseBinding(function string) func(*View, bool, *tcell.EventMouse) bool { + return func(v *View, _ bool, e *tcell.EventMouse) bool { + _, err := Call(function, e) + if err != nil { + TermMessage(err) + } + return false + } +} + func unpack(old []string) []interface{} { new := make([]interface{}, len(old)) for i, v := range old { diff --git a/cmd/micro/view.go b/cmd/micro/view.go index 11f7e0cf..fef81ed4 100644 --- a/cmd/micro/view.go +++ b/cmd/micro/view.go @@ -450,6 +450,35 @@ func (v *View) MoveToMouseClick(x, y int) { v.Cursor.LastVisualX = v.Cursor.GetVisualX() } +func (v *View) ExecuteActions(actions []func(*View, bool) bool) bool { + relocate := false + readonlyBindingsList := []string{"Delete", "Insert", "Backspace", "Cut", "Play", "Paste", "Move", "Add", "DuplicateLine", "Macro"} + for _, action := range actions { + readonlyBindingsResult := false + funcName := ShortFuncName(action) + if v.Type.readonly == true { + // check for readonly and if true only let key bindings get called if they do not change the contents. + for _, readonlyBindings := range readonlyBindingsList { + if strings.Contains(funcName, readonlyBindings) { + readonlyBindingsResult = true + } + } + } + if !readonlyBindingsResult { + // call the key binding + relocate = action(v, true) || relocate + // Macro + if funcName != "ToggleMacro" && funcName != "PlayMacro" { + if recordingMacro { + curMacro = append(curMacro, action) + } + } + } + } + + return relocate +} + // HandleEvent handles an event passed by the main loop func (v *View) HandleEvent(event tcell.Event) { // This bool determines whether the view is relocated at the end of the function @@ -462,7 +491,6 @@ func (v *View) HandleEvent(event tcell.Event) { case *tcell.EventKey: // Check first if input is a key binding, if it is we 'eat' the input and don't insert a rune isBinding := false - readonlyBindingsList := []string{"Delete", "Insert", "Backspace", "Cut", "Play", "Paste", "Move", "Add", "DuplicateLine", "Macro"} if e.Key() != tcell.KeyRune || e.Modifiers() != 0 { for key, actions := range bindings { if e.Key() == key.keyCode { @@ -474,28 +502,7 @@ func (v *View) HandleEvent(event tcell.Event) { if e.Modifiers() == key.modifiers { relocate = false isBinding = true - for _, action := range actions { - readonlyBindingsResult := false - funcName := ShortFuncName(action) - if v.Type.readonly == true { - // check for readonly and if true only let key bindings get called if they do not change the contents. - for _, readonlyBindings := range readonlyBindingsList { - if strings.Contains(funcName, readonlyBindings) { - readonlyBindingsResult = true - } - } - } - if !readonlyBindingsResult { - // call the key binding - relocate = action(v, true) || relocate - // Macro - if funcName != "ToggleMacro" && funcName != "PlayMacro" { - if recordingMacro { - curMacro = append(curMacro, action) - } - } - } - } + relocate = v.ExecuteActions(actions) break } } @@ -544,59 +551,21 @@ func (v *View) HandleEvent(event tcell.Event) { button := e.Buttons() + for key, actions := range bindings { + if button == key.buttons { + relocate = v.ExecuteActions(actions) + } + } + + for key, actions := range mouseBindings { + if button == key.buttons { + for _, action := range actions { + action(v, true, e) + } + } + } + switch button { - case tcell.Button1: - // Left click - if v.mouseReleased { - v.MoveToMouseClick(x, y) - if time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold { - if v.doubleClick { - // Triple click - v.lastClickTime = time.Now() - - v.tripleClick = true - v.doubleClick = false - - v.Cursor.SelectLine() - v.Cursor.CopySelection("primary") - } else { - // Double click - v.lastClickTime = time.Now() - - v.doubleClick = true - v.tripleClick = false - - v.Cursor.SelectWord() - v.Cursor.CopySelection("primary") - } - } else { - v.doubleClick = false - v.tripleClick = false - v.lastClickTime = time.Now() - - v.Cursor.OrigSelection[0] = v.Cursor.Loc - v.Cursor.CurSelection[0] = v.Cursor.Loc - v.Cursor.CurSelection[1] = v.Cursor.Loc - } - v.mouseReleased = false - } else if !v.mouseReleased { - v.MoveToMouseClick(x, y) - if v.tripleClick { - v.Cursor.AddLineToSelection() - } else if v.doubleClick { - v.Cursor.AddWordToSelection() - } else { - v.Cursor.SetSelectionEnd(v.Cursor.Loc) - v.Cursor.CopySelection("primary") - } - } - case tcell.Button2: - // Check viewtype if readonly don't paste (readonly help and log view etc.) - if v.Type.readonly == false { - // Middle mouse button was clicked, - // We should paste primary - v.PastePrimary(true) - } case tcell.ButtonNone: // Mouse event with no click if !v.mouseReleased { @@ -615,14 +584,6 @@ func (v *View) HandleEvent(event tcell.Event) { } v.mouseReleased = true } - case tcell.WheelUp: - // Scroll up - scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64)) - v.ScrollUp(scrollspeed) - case tcell.WheelDown: - // Scroll down - scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64)) - v.ScrollDown(scrollspeed) } }