diff --git a/CHANGELOG b/CHANGELOG index ac877ab..fac14df 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 diff --git a/button.go b/button.go index 3ca6255..d00dd03 100644 --- a/button.go +++ b/button.go @@ -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()) } } }) diff --git a/doc.go b/doc.go index 7fc1477..8c8de20 100644 --- a/doc.go +++ b/doc.go @@ -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 diff --git a/go.mod b/go.mod index da79b38..41655cd 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index a90c888..54722a8 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/keys.go b/keys.go new file mode 100644 index 0000000..be291c6 --- /dev/null +++ b/keys.go @@ -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"}, +} diff --git a/list.go b/list.go index 4962bf6..d44ea7d 100644 --- a/list.go +++ b/list.go @@ -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) } } diff --git a/styles.go b/styles.go index eb16257..667f0fb 100644 --- a/styles.go +++ b/styles.go @@ -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{ diff --git a/treeview.go b/treeview.go index 4d929de..0dff419 100644 --- a/treeview.go +++ b/treeview.go @@ -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() diff --git a/util.go b/util.go index 574cfda..3f93604 100644 --- a/util.go +++ b/util.go @@ -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 +}