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/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) {

View file

@ -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",
}
}

View file

@ -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 {

View file

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