Add Keys to allow default keybindings to be modified

This commit is contained in:
Trevor Slocum 2020-05-15 16:17:41 -07:00
parent 3feec6ebfb
commit 04a0149298
10 changed files with 154 additions and 115 deletions

View File

@ -1,5 +1,6 @@
v1.4.6 (WIP)
- Add Box.ShowFocus
- Add Keys to allow default keybindings to be modified
- Add List.GetOffset, List.SetOffset and List.SetSelectedTextAttributes
- Add TextView.SetMaxLines
- Add Vim-style keybindings to List

View File

@ -151,14 +151,13 @@ func (b *Button) Draw(screen tcell.Screen) {
func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return b.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
// Process key event.
switch key := event.Key(); key {
case tcell.KeyEnter: // Selected.
if matchesKeys(event, Keys.Select) {
if b.selected != nil {
b.selected()
}
case tcell.KeyBacktab, tcell.KeyTab, tcell.KeyEscape: // Leave. No action.
} else if matchesKeys(event, Keys.Cancel) || matchesKeys(event, Keys.PreviousField) || matchesKeys(event, Keys.NextField) {
if b.blur != nil {
b.blur(key)
b.blur(event.Key())
}
}
})

3
doc.go
View File

@ -144,7 +144,8 @@ feel of the primitives to your preferred style.
Keyboard Shortcuts
Widgets use keyboard shortcuts (a.k.a. keybindings) such as arrow keys and
H/J/k/L by default. You may override these shortcuts globally by setting a
H/J/k/L by default. You may replace these defaults by modifying the keybindings
listed in Keys. You may also override keyboard shortcuts globally by setting a
handler with Application.SetInputCapture.
cbind is a library which simplifies adding support for custom keybindings to

4
go.mod
View File

@ -7,6 +7,6 @@ require (
github.com/lucasb-eyer/go-colorful v1.0.3
github.com/mattn/go-runewidth v0.0.9
github.com/rivo/uniseg v0.1.0
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f // indirect
golang.org/x/text v0.3.2 // indirect
gitlab.com/tslocum/cbind v0.1.1
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 // indirect
)

8
go.sum
View File

@ -10,14 +10,18 @@ github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tW
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
gitlab.com/tslocum/cbind v0.1.1 h1:JXXtxMWHgWLvoF+QkrvcNvOQ59juy7OE1RhT7hZfdt0=
gitlab.com/tslocum/cbind v0.1.1/go.mod h1:rX7vkl0pUSg/yy427MmD1FZAf99S7WwpUlxF/qTpPqk=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f h1:mOhmO9WsBaJCNmaZHPtHs9wOcdqdKCjF6OPJlmDM3KI=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=

41
keys.go Normal file
View File

@ -0,0 +1,41 @@
package cview
// Key defines the keyboard shortcuts of an application.
type Key struct {
Cancel []string
Select []string
FirstItem []string
LastItem []string
PreviousItem []string
NextItem []string
PreviousField []string
NextField []string
PreviousPage []string
NextPage []string
ShowContextMenu []string
}
// Keys defines the keyboard shortcuts of an application.
var Keys = Key{
Cancel: []string{"Escape"},
Select: []string{"Enter", "Ctrl+J"}, // Ctrl+J = keypad enter
FirstItem: []string{"Home", "g"},
LastItem: []string{"End", "G"},
PreviousItem: []string{"Up", "k"},
NextItem: []string{"Down", "j"},
PreviousField: []string{"Backtab"},
NextField: []string{"Tab"},
PreviousPage: []string{"PageUp"},
NextPage: []string{"PageDown"},
ShowContextMenu: []string{"Alt+Enter"},
}

146
list.go
View File

@ -850,7 +850,9 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
l.Lock()
if event.Key() == tcell.KeyEscape {
previousItem := l.currentItem
if matchesKeys(event, Keys.Cancel) {
if l.ContextMenu.open {
l.Unlock()
@ -865,49 +867,8 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
l.Unlock()
}
return
} else if len(l.items) == 0 && (event.Key() != tcell.KeyEnter || event.Modifiers()&tcell.ModAlt == 0) {
l.Unlock()
return
}
previousItem := l.currentItem
switch key := event.Key(); key {
case tcell.KeyHome:
l.transform(TransformFirstItem)
case tcell.KeyEnd:
l.transform(TransformLastItem)
case tcell.KeyBacktab, tcell.KeyUp, tcell.KeyLeft:
l.transform(TransformPreviousItem)
case tcell.KeyTab, tcell.KeyDown, tcell.KeyRight:
l.transform(TransformNextItem)
case tcell.KeyPgUp:
l.transform(TransformPreviousPage)
case tcell.KeyPgDn:
l.transform(TransformNextPage)
case tcell.KeyEnter:
if event.Modifiers()&tcell.ModAlt != 0 {
// Do we show any shortcuts?
var showShortcuts bool
for _, item := range l.items {
if item.Shortcut != 0 {
showShortcuts = true
break
}
}
offsetX := 7
if showShortcuts {
offsetX += 4
}
offsetY := l.currentItem
if l.showSecondaryText {
offsetY *= 2
}
x, y, _, _ := l.GetInnerRect()
defer l.ContextMenu.show(l.currentItem, x+offsetX, y+offsetY, setFocus)
} else if l.currentItem >= 0 && l.currentItem < len(l.items) {
} else if matchesKeys(event, Keys.Select) {
if l.currentItem >= 0 && l.currentItem < len(l.items) {
item := l.items[l.currentItem]
if item.Enabled {
if item.Selected != nil {
@ -922,43 +883,74 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
}
}
}
case tcell.KeyRune:
ch := event.Rune()
if ch != ' ' {
// It's not a space bar. Is it a shortcut?
var found bool
for index, item := range l.items {
if item.Enabled && item.Shortcut == ch {
// We have a shortcut.
found = true
l.currentItem = index
break
}
}
if !found {
switch ch {
case 'g':
l.transform(TransformFirstItem)
case 'G':
l.transform(TransformLastItem)
case 'j':
l.transform(TransformNextItem)
case 'k':
l.transform(TransformPreviousItem)
}
} else if matchesKeys(event, Keys.ShowContextMenu) {
// Do we show any shortcuts?
var showShortcuts bool
for _, item := range l.items {
if item.Shortcut != 0 {
showShortcuts = true
break
}
}
item := l.items[l.currentItem]
if item.Selected != nil {
l.Unlock()
item.Selected()
l.Lock()
offsetX := 7
if showShortcuts {
offsetX += 4
}
if l.selected != nil {
l.Unlock()
l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
l.Lock()
offsetY := l.currentItem
if l.showSecondaryText {
offsetY *= 2
}
x, y, _, _ := l.GetInnerRect()
defer l.ContextMenu.show(l.currentItem, x+offsetX, y+offsetY, setFocus)
} else if len(l.items) == 0 {
l.Unlock()
return
}
var matchesShortcut bool
if event.Key() == tcell.KeyRune {
ch := event.Rune()
if ch != ' ' {
// It's not a space bar. Is it a shortcut?
for index, item := range l.items {
if item.Enabled && item.Shortcut == ch {
// We have a shortcut.
matchesShortcut = true
l.currentItem = index
item := l.items[l.currentItem]
if item.Selected != nil {
l.Unlock()
item.Selected()
l.Lock()
}
if l.selected != nil {
l.Unlock()
l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
l.Lock()
}
break
}
}
}
}
if !matchesShortcut {
if matchesKeys(event, Keys.FirstItem) {
l.transform(TransformFirstItem)
} else if matchesKeys(event, Keys.LastItem) {
l.transform(TransformLastItem)
} else if matchesKeys(event, Keys.PreviousItem) || matchesKeys(event, Keys.PreviousField) {
l.transform(TransformPreviousItem)
} else if matchesKeys(event, Keys.NextItem) || matchesKeys(event, Keys.NextField) {
l.transform(TransformNextItem)
} else if matchesKeys(event, Keys.PreviousPage) {
l.transform(TransformPreviousPage)
} else if matchesKeys(event, Keys.NextPage) {
l.transform(TransformNextPage)
}
}

View File

@ -31,7 +31,7 @@ type Theme struct {
ScrollBarColor tcell.Color // Scroll bar color.
}
// Styles defines the theme for applications. The default is for a black
// Styles defines the appearance of an application. The default is for a black
// background and some basic colors: black, white, yellow, green, cyan, and
// blue.
var Styles = Theme{

View File

@ -889,41 +889,25 @@ func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
// Because the tree is flattened into a list only at drawing time, we also
// postpone the (selection) movement to drawing time.
switch key := event.Key(); key {
case tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape:
if matchesKeys(event, Keys.Cancel) || matchesKeys(event, Keys.PreviousField) || matchesKeys(event, Keys.NextField) {
if t.done != nil {
t.Unlock()
t.done(key)
t.done(event.Key())
t.Lock()
}
case tcell.KeyDown, tcell.KeyRight:
t.movement = treeDown
case tcell.KeyUp, tcell.KeyLeft:
t.movement = treeUp
case tcell.KeyHome:
} else if matchesKeys(event, Keys.FirstItem) {
t.movement = treeHome
case tcell.KeyEnd:
} else if matchesKeys(event, Keys.LastItem) {
t.movement = treeEnd
case tcell.KeyPgDn, tcell.KeyCtrlF:
t.movement = treePageDown
case tcell.KeyPgUp, tcell.KeyCtrlB:
} else if matchesKeys(event, Keys.PreviousItem) || matchesKeys(event, Keys.PreviousField) {
t.movement = treeUp
} else if matchesKeys(event, Keys.NextItem) || matchesKeys(event, Keys.NextField) {
t.movement = treeDown
} else if matchesKeys(event, Keys.PreviousPage) {
t.movement = treePageUp
case tcell.KeyRune:
switch event.Rune() {
case 'g':
t.movement = treeHome
case 'G':
t.movement = treeEnd
case 'j':
t.movement = treeDown
case 'k':
t.movement = treeUp
case ' ':
t.Unlock()
selectNode()
t.Lock()
}
case tcell.KeyEnter:
} else if matchesKeys(event, Keys.NextPage) {
t.movement = treePageDown
} else if matchesKeys(event, Keys.Select) || event.Rune() == ' ' { // TODO space is hardcoded
t.Unlock()
selectNode()
t.Lock()

17
util.go
View File

@ -9,6 +9,7 @@ import (
"github.com/gdamore/tcell"
runewidth "github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
"gitlab.com/tslocum/cbind"
)
// Text alignment within a box.
@ -694,3 +695,19 @@ func RenderScrollBar(screen tcell.Screen, visibility ScrollBarVisibility, x int,
}
Print(screen, scrollBar, x, y, 1, AlignLeft, color)
}
// matchesKeys returns whether the EventKey is present in the list of keybinds.
func matchesKeys(event *tcell.EventKey, keybinds []string) bool {
enc, err := cbind.Encode(event.Modifiers(), event.Key(), event.Rune())
if err != nil {
return false
}
for _, k := range keybinds {
if k == enc {
return true
}
}
return false
}