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) } }