Skip to content

Commit

Permalink
Merge branch 'master' into image
Browse files Browse the repository at this point in the history
  • Loading branch information
rivo committed Jan 1, 2023
2 parents 326f2db + c9f4e98 commit 062ee21
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 67 deletions.
6 changes: 6 additions & 0 deletions demos/inputfield/autocomplete/main.go
Expand Up @@ -33,6 +33,12 @@ func main() {
}
return
})
inputField.SetAutocompletedFunc(func(text string, index, source int) bool {
if source != tview.AutocompletedNavigate {
inputField.SetText(text)
}
return source == tview.AutocompletedEnter || source == tview.AutocompletedClick
})
if err := app.EnableMouse(true).SetRoot(inputField, true).Run(); err != nil {
panic(err)
}
Expand Down
174 changes: 125 additions & 49 deletions inputfield.go
Expand Up @@ -11,15 +11,30 @@ import (
"github.com/rivo/uniseg"
)

const (
AutocompletedNavigate = iota // The user navigated the autocomplete list (using the errow keys).
AutocompletedTab // The user selected an autocomplete entry using the tab key.
AutocompletedEnter // The user selected an autocomplete entry using the enter key.
AutocompletedClick // The user selected an autocomplete entry by clicking the mouse button on it.
)

// InputField is a one-line box (three lines if there is a title) where the
// user can enter text. Use SetAcceptanceFunc() to accept or reject input,
// SetChangedFunc() to listen for changes, and SetMaskCharacter() to hide input
// from onlookers (e.g. for password input).
// user can enter text. Use [InputField.SetAcceptanceFunc] to accept or reject
// input, [InputField.SetChangedFunc] to listen for changes, and
// [InputField.SetMaskCharacter] to hide input from onlookers (e.g. for password
// input).
//
// The input field also has an optional autocomplete feature. It is initialized
// by the [InputField.SetAutocompleteFunc] function. For more control over the
// autocomplete drop-down's behavior, you can also set the
// [InputField.SetAutocompletedFunc].
//
// The following keys can be used for navigation and editing:
//
// - Left arrow: Move left by one character.
// - Right arrow: Move right by one character.
// - Down arrow: Open the autocomplete drop-down.
// - Tab, Enter: Select the current autocomplete entry.
// - Home, Ctrl-A, Alt-a: Move to the beginning of the line.
// - End, Ctrl-E, Alt-e: Move to the end of the line.
// - Alt-left, Alt-b: Move left by one word.
Expand Down Expand Up @@ -84,6 +99,14 @@ type InputField struct {
background tcell.Color
}

// An optional function which is called when the user selects an
// autocomplete entry. The text and index of the selected entry (within the
// list) is provided, as well as the user action causing the selection (one
// of the "Autocompleted" values). The function should return true if the
// autocomplete list should be closed. If nil, the input field will be
// updated automatically when the user navigates the autocomplete list.
autocompleted func(text string, index int, source int) bool

// An optional function which may reject the last character that was entered.
accept func(text string, ch rune) bool

Expand Down Expand Up @@ -273,6 +296,24 @@ func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entr
return i
}

// SetAutocompletedFunc sets a callback function which is invoked when the user
// selects an entry from the autocomplete drop-down list. The function is passed
// the text of the selected entry (stripped of any color tags), the index of the
// entry, and the user action that caused the selection, e.g.
// [AutocompletedNavigate]. It returns true if the autocomplete drop-down should
// be closed after the callback returns or false if it should remain open, in
// which case [InputField.Autocomplete] is called to update the drop-down's
// contents.
//
// If no such callback is set (or nil is provided), the input field will be
// updated with the selection any time the user navigates the autocomplete
// drop-down list. So this function essentially gives you more control over the
// autocomplete functionality.
func (i *InputField) SetAutocompletedFunc(autocompleted func(text string, index int, source int) bool) *InputField {
i.autocompleted = autocompleted
return i
}

// Autocomplete invokes the autocomplete callback (if there is one). If the
// length of the returned autocomplete entries slice is greater than 0, the
// input field will present the user with a corresponding drop-down list the
Expand Down Expand Up @@ -550,21 +591,6 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
return true
}

// Change the autocomplete selection.
autocompleteSelect := func(offset int) {
count := i.autocompleteList.GetItemCount()
newEntry := i.autocompleteList.GetCurrentItem() + offset
if newEntry >= count {
newEntry = 0
} else if newEntry < 0 {
newEntry = count - 1
}
i.autocompleteList.SetCurrentItem(newEntry)
currentText, _ = i.autocompleteList.GetItemText(newEntry) // Don't trigger changed function twice.
currentText = stripTags(currentText)
i.SetText(currentText)
}

// Finish up.
finish := func(key tcell.Key) {
if i.done != nil {
Expand All @@ -575,9 +601,51 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
}
}

// Process key event.
// If we have an autocomplete list, there are certain keys we will
// forward to it.
i.autocompleteListMutex.Lock()
defer i.autocompleteListMutex.Unlock()
if i.autocompleteList != nil {
i.autocompleteList.SetChangedFunc(nil)
switch key := event.Key(); key {
case tcell.KeyEscape: // Close the list.
i.autocompleteList = nil
return
case tcell.KeyEnter, tcell.KeyTab: // Intentional selection.
if i.autocompleted != nil {
index := i.autocompleteList.GetCurrentItem()
text, _ := i.autocompleteList.GetItemText(index)
source := AutocompletedEnter
if key == tcell.KeyTab {
source = AutocompletedTab
}
if i.autocompleted(stripTags(text), index, source) {
i.autocompleteList = nil
currentText = i.GetText()
}
} else {
i.autocompleteList = nil
}
return
case tcell.KeyDown, tcell.KeyUp, tcell.KeyPgDn, tcell.KeyPgUp:
i.autocompleteList.SetChangedFunc(func(index int, text, secondaryText string, shortcut rune) {
text = stripTags(text)
if i.autocompleted != nil {
if i.autocompleted(text, index, AutocompletedNavigate) {
i.autocompleteList = nil
currentText = i.GetText()
}
} else {
i.SetText(text)
currentText = stripTags(text) // We want to keep the autocomplete list open and unchanged.
}
})
i.autocompleteList.InputHandler()(event, setFocus)
return
}
}

// Process key event for the input field.
switch key := event.Key(); key {
case tcell.KeyRune: // Regular character.
if event.Modifiers()&tcell.ModAlt > 0 {
Expand Down Expand Up @@ -646,44 +714,52 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
home()
case tcell.KeyEnd, tcell.KeyCtrlE:
end()
case tcell.KeyEnter:
if i.autocompleteList != nil {
autocompleteSelect(0)
i.autocompleteList = nil
} else {
finish(key)
}
case tcell.KeyEscape:
if i.autocompleteList != nil {
i.autocompleteList = nil
} else {
finish(key)
}
case tcell.KeyTab:
if i.autocompleteList != nil {
autocompleteSelect(0)
} else {
finish(key)
}
case tcell.KeyDown:
if i.autocompleteList != nil {
autocompleteSelect(1)
} else {
finish(key)
}
case tcell.KeyUp, tcell.KeyBacktab: // Autocomplete selection.
if i.autocompleteList != nil {
autocompleteSelect(-1)
} else {
finish(key)
}
i.autocompleteListMutex.Unlock() // We're still holding a lock.
i.Autocomplete()
i.autocompleteListMutex.Lock()
case tcell.KeyEnter, tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
finish(key)
}
})
}

// MouseHandler returns the mouse handler for this primitive.
func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return i.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
currentText := i.GetText()
defer func() {
if i.GetText() != currentText {
i.Autocomplete()
if i.changed != nil {
i.changed(i.text)
}
}
}()

// If we have an autocomplete list, forward the mouse event to it.
i.autocompleteListMutex.Lock()
defer i.autocompleteListMutex.Unlock()
if i.autocompleteList != nil {
i.autocompleteList.SetChangedFunc(func(index int, text, secondaryText string, shortcut rune) {
text = stripTags(text)
if i.autocompleted != nil {
if i.autocompleted(text, index, AutocompletedClick) {
i.autocompleteList = nil
currentText = i.GetText()
}
return
}
i.SetText(text)
i.autocompleteList = nil
})
if consumed, _ = i.autocompleteList.MouseHandler()(action, event, setFocus); consumed {
setFocus(i)
return
}
}

// Is mouse event within the input field?
x, y := event.Position()
_, rectY, _, _ := i.GetInnerRect()
if !i.InRect(x, y) {
Expand Down
49 changes: 31 additions & 18 deletions list.go
Expand Up @@ -113,6 +113,8 @@ func (l *List) SetCurrentItem(index int) *List {

l.currentItem = index

l.adjustOffset()

return l
}

Expand Down Expand Up @@ -471,18 +473,6 @@ func (l *List) Draw(screen tcell.Screen) {
}
}

// Adjust offset to keep the current selection in view.
if l.currentItem < l.itemOffset {
l.itemOffset = l.currentItem
} else if l.showSecondaryText {
if 2*(l.currentItem-l.itemOffset) >= height-1 {
l.itemOffset = (2*l.currentItem + 3 - height) / 2
}
} else {
if l.currentItem-l.itemOffset >= height {
l.itemOffset = l.currentItem + 1 - height
}
}
if l.horizontalOffset < 0 {
l.horizontalOffset = 0
}
Expand Down Expand Up @@ -565,6 +555,23 @@ func (l *List) Draw(screen tcell.Screen) {
l.overflowing = overflowing
}

// adjustOffset adjusts the vertical offset to keep the current selection in
// view.
func (l *List) adjustOffset() {
_, _, _, height := l.GetInnerRect()
if l.currentItem < l.itemOffset {
l.itemOffset = l.currentItem
} else if l.showSecondaryText {
if 2*(l.currentItem-l.itemOffset) >= height-1 {
l.itemOffset = (2*l.currentItem + 3 - height) / 2
}
} else {
if l.currentItem-l.itemOffset >= height {
l.itemOffset = l.currentItem + 1 - height
}
}
}

// InputHandler returns the handler for this primitive.
func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
Expand Down Expand Up @@ -662,9 +669,12 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
}
}

if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil {
item := l.items[l.currentItem]
l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
if l.currentItem != previousItem && l.currentItem < len(l.items) {
if l.changed != nil {
item := l.items[l.currentItem]
l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
}
l.adjustOffset()
}
})
}
Expand Down Expand Up @@ -699,6 +709,7 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
// Process mouse event.
switch action {
case MouseLeftClick:
setFocus(l)
index := l.indexAtPoint(event.Position())
if index != -1 {
item := l.items[index]
Expand All @@ -708,12 +719,14 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
if l.selected != nil {
l.selected(index, item.MainText, item.SecondaryText, item.Shortcut)
}
if index != l.currentItem && l.changed != nil {
l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
if index != l.currentItem {
if l.changed != nil {
l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
}
l.adjustOffset()
}
l.currentItem = index
}
setFocus(l)
consumed = true
case MouseScrollUp:
if l.itemOffset > 0 {
Expand Down

0 comments on commit 062ee21

Please sign in to comment.