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
This commit is contained in:
Zachary Yedidia
2017-06-11 17:49:59 -04:00
parent ee84296dfe
commit 3270acdd00
4 changed files with 218 additions and 93 deletions

View File

@@ -8,13 +8,14 @@ import (
"github.com/yuin/gopher-lua" "github.com/yuin/gopher-lua"
"github.com/zyedidia/clipboard" "github.com/zyedidia/clipboard"
"github.com/zyedidia/tcell"
) )
// PreActionCall executes the lua pre callback if possible // 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 executeAction := true
for pl := range loadedPlugins { 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") { if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err) TermMessage(err)
continue continue
@@ -27,10 +28,10 @@ func PreActionCall(funcName string, view *View) bool {
} }
// PostActionCall executes the lua plugin callback if possible // 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 relocate := true
for pl := range loadedPlugins { 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") { if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err) TermMessage(err)
continue continue
@@ -51,6 +52,98 @@ func (v *View) deselect(index int) bool {
return false 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 // Center centers the view on the cursor
func (v *View) Center(usePlugin bool) bool { func (v *View) Center(usePlugin bool) bool {
if usePlugin && !PreActionCall("Center", v) { if usePlugin && !PreActionCall("Center", v) {

View File

@@ -10,8 +10,13 @@ import (
) )
var bindings map[Key][]func(*View, bool) bool var bindings map[Key][]func(*View, bool) bool
var mouseBindings map[Key][]func(*View, bool, *tcell.EventMouse) bool
var helpBinding string var helpBinding string
var mouseBindingActions = map[string]func(*View, bool, *tcell.EventMouse) bool{
"MousePress": (*View).MousePress,
}
var bindingActions = map[string]func(*View, bool) bool{ var bindingActions = map[string]func(*View, bool) bool{
"CursorUp": (*View).CursorUp, "CursorUp": (*View).CursorUp,
"CursorDown": (*View).CursorDown, "CursorDown": (*View).CursorDown,
@@ -91,11 +96,23 @@ var bindingActions = map[string]func(*View, bool) bool{
"ToggleMacro": (*View).ToggleMacro, "ToggleMacro": (*View).ToggleMacro,
"PlayMacro": (*View).PlayMacro, "PlayMacro": (*View).PlayMacro,
"Suspend": (*View).Suspend, "Suspend": (*View).Suspend,
"ScrollUp": (*View).ScrollUpAction,
"ScrollDown": (*View).ScrollDownAction,
// This was changed to InsertNewline but I don't want to break backwards compatibility // This was changed to InsertNewline but I don't want to break backwards compatibility
"InsertEnter": (*View).InsertNewline, "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{ var bindingKeys = map[string]tcell.Key{
"Up": tcell.KeyUp, "Up": tcell.KeyUp,
"Down": tcell.KeyDown, "Down": tcell.KeyDown,
@@ -230,12 +247,14 @@ var bindingKeys = map[string]tcell.Key{
type Key struct { type Key struct {
keyCode tcell.Key keyCode tcell.Key
modifiers tcell.ModMask modifiers tcell.ModMask
buttons tcell.ButtonMask
r rune r rune
} }
// InitBindings initializes the keybindings for micro // InitBindings initializes the keybindings for micro
func InitBindings() { func InitBindings() {
bindings = make(map[Key][]func(*View, bool) bool) bindings = make(map[Key][]func(*View, bool) bool)
mouseBindings = make(map[Key][]func(*View, bool, *tcell.EventMouse) bool)
var parsed map[string]string var parsed map[string]string
defaults := DefaultBindings() defaults := DefaultBindings()
@@ -301,6 +320,7 @@ modSearch:
return Key{ return Key{
keyCode: code, keyCode: code,
modifiers: modifiers, modifiers: modifiers,
buttons: -1,
r: 0, r: 0,
}, true }, true
} }
@@ -311,6 +331,16 @@ modSearch:
return Key{ return Key{
keyCode: code, keyCode: code,
modifiers: modifiers, 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, r: 0,
}, true }, true
} }
@@ -320,12 +350,13 @@ modSearch:
return Key{ return Key{
keyCode: tcell.KeyRune, keyCode: tcell.KeyRune,
modifiers: modifiers, modifiers: modifiers,
buttons: -1,
r: rune(k[0]), r: rune(k[0]),
}, true }, true
} }
// We don't know what happened. // We don't know what happened.
return Key{}, false return Key{buttons: -1}, false
} }
// findAction will find 'action' using string 'v' // findAction will find 'action' using string 'v'
@@ -339,6 +370,16 @@ func findAction(v string) (action func(*View, bool) bool) {
return action 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 // BindKey takes a key and an action and binds the two together
func BindKey(k, v string) { func BindKey(k, v string) {
key, ok := findKey(k) key, ok := findKey(k)
@@ -356,18 +397,31 @@ func BindKey(k, v string) {
actionNames := strings.Split(v, ",") actionNames := strings.Split(v, ",")
if actionNames[0] == "UnbindKey" { if actionNames[0] == "UnbindKey" {
delete(bindings, key) delete(bindings, key)
delete(mouseBindings, key)
if len(actionNames) == 1 { if len(actionNames) == 1 {
actionNames = make([]string, 0, 0) return
} else {
actionNames = append(actionNames[:0], actionNames[1:]...)
} }
actionNames = append(actionNames[:0], actionNames[1:]...)
} }
actions := make([]func(*View, bool) bool, 0, len(actionNames)) actions := make([]func(*View, bool) bool, 0, len(actionNames))
mouseActions := make([]func(*View, bool, *tcell.EventMouse) bool, 0, len(actionNames))
for _, actionName := range 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 // DefaultBindings returns a map containing micro's default keybindings
@@ -453,5 +507,11 @@ func DefaultBindings() map[string]string {
"F7": "Find", "F7": "Find",
"F10": "Quit", "F10": "Quit",
"Esc": "Escape", "Esc": "Escape",
// Mouse bindings
"MouseWheelUp": "ScrollUp",
"MouseWheelDown": "ScrollDown",
"MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary",
} }
} }

View File

@@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/yuin/gopher-lua" "github.com/yuin/gopher-lua"
"github.com/zyedidia/tcell"
"layeh.com/gopher-luar" "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{} { func unpack(old []string) []interface{} {
new := make([]interface{}, len(old)) new := make([]interface{}, len(old))
for i, v := range old { for i, v := range old {

View File

@@ -450,6 +450,35 @@ func (v *View) MoveToMouseClick(x, y int) {
v.Cursor.LastVisualX = v.Cursor.GetVisualX() 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 // HandleEvent handles an event passed by the main loop
func (v *View) HandleEvent(event tcell.Event) { func (v *View) HandleEvent(event tcell.Event) {
// This bool determines whether the view is relocated at the end of the function // 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: case *tcell.EventKey:
// Check first if input is a key binding, if it is we 'eat' the input and don't insert a rune // Check first if input is a key binding, if it is we 'eat' the input and don't insert a rune
isBinding := false isBinding := false
readonlyBindingsList := []string{"Delete", "Insert", "Backspace", "Cut", "Play", "Paste", "Move", "Add", "DuplicateLine", "Macro"}
if e.Key() != tcell.KeyRune || e.Modifiers() != 0 { if e.Key() != tcell.KeyRune || e.Modifiers() != 0 {
for key, actions := range bindings { for key, actions := range bindings {
if e.Key() == key.keyCode { if e.Key() == key.keyCode {
@@ -474,28 +502,7 @@ func (v *View) HandleEvent(event tcell.Event) {
if e.Modifiers() == key.modifiers { if e.Modifiers() == key.modifiers {
relocate = false relocate = false
isBinding = true isBinding = true
for _, action := range actions { relocate = v.ExecuteActions(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)
}
}
}
}
break break
} }
} }
@@ -544,59 +551,21 @@ func (v *View) HandleEvent(event tcell.Event) {
button := e.Buttons() 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 { 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: case tcell.ButtonNone:
// Mouse event with no click // Mouse event with no click
if !v.mouseReleased { if !v.mouseReleased {
@@ -615,14 +584,6 @@ func (v *View) HandleEvent(event tcell.Event) {
} }
v.mouseReleased = true 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)
} }
} }