mirror of
https://github.com/rivo/tview.git
synced 2025-04-28 13:48:53 +08:00
This commit is contained in:
parent
c0e09651e4
commit
c3d1d4bcf9
@ -65,6 +65,8 @@ Add your issue here on GitHub. Feel free to get in touch if you have any questio
|
|||||||
|
|
||||||
(There are no corresponding tags in the project. I only keep such a history in this README.)
|
(There are no corresponding tags in the project. I only keep such a history in this README.)
|
||||||
|
|
||||||
|
- v0.20 (2019-07-08)
|
||||||
|
- Added autocomplete functionality to `InputField`.
|
||||||
- v0.19 (2018-10-28)
|
- v0.19 (2018-10-28)
|
||||||
- Added `QueueUpdate()` and `QueueEvent()` to `Application` to help with modifications to primitives from goroutines.
|
- Added `QueueUpdate()` and `QueueEvent()` to `Application` to help with modifications to primitives from goroutines.
|
||||||
- v0.18 (2018-10-18)
|
- v0.18 (2018-10-18)
|
||||||
|
39
demos/inputfield/autocomplete.go
Normal file
39
demos/inputfield/autocomplete.go
Normal file
File diff suppressed because one or more lines are too long
81
demos/inputfield/autocompleteasync.go
Normal file
81
demos/inputfield/autocompleteasync.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/rivo/tview"
|
||||||
|
)
|
||||||
|
|
||||||
|
type company struct {
|
||||||
|
Name string `json:name`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := tview.NewApplication()
|
||||||
|
inputField := tview.NewInputField().
|
||||||
|
SetLabel("Enter a company name: ").
|
||||||
|
SetFieldWidth(30).
|
||||||
|
SetDoneFunc(func(key tcell.Key) {
|
||||||
|
app.Stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set up autocomplete function.
|
||||||
|
var mutex sync.Mutex
|
||||||
|
prefixMap := make(map[string][]string)
|
||||||
|
inputField.SetAutocompleteFunc(func(currentText string) []string {
|
||||||
|
// Ignore empty text.
|
||||||
|
prefix := strings.TrimSpace(strings.ToLower(currentText))
|
||||||
|
if prefix == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we have entries for this text already?
|
||||||
|
mutex.Lock()
|
||||||
|
defer mutex.Unlock()
|
||||||
|
entries, ok := prefixMap[prefix]
|
||||||
|
if ok {
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
// No entries yet. Issue a request to the API in a goroutine.
|
||||||
|
go func() {
|
||||||
|
// Ignore errors in this demo.
|
||||||
|
url := "https://autocomplete.clearbit.com/v1/companies/suggest?query=" + url.QueryEscape(prefix)
|
||||||
|
res, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the result in the prefix map.
|
||||||
|
var companies []*company
|
||||||
|
dec := json.NewDecoder(res.Body)
|
||||||
|
if err := dec.Decode(&companies); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entries := make([]string, 0, len(companies))
|
||||||
|
for _, c := range companies {
|
||||||
|
entries = append(entries, c.Name)
|
||||||
|
}
|
||||||
|
mutex.Lock()
|
||||||
|
prefixMap[prefix] = entries
|
||||||
|
mutex.Unlock()
|
||||||
|
|
||||||
|
// Trigger an update to the input field.
|
||||||
|
inputField.Autocomplete()
|
||||||
|
|
||||||
|
// Also redraw the screen.
|
||||||
|
app.Draw()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := app.SetRoot(inputField, true).Run(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
@ -83,8 +83,9 @@ type DropDown struct {
|
|||||||
|
|
||||||
// NewDropDown returns a new drop-down.
|
// NewDropDown returns a new drop-down.
|
||||||
func NewDropDown() *DropDown {
|
func NewDropDown() *DropDown {
|
||||||
list := NewList().ShowSecondaryText(false)
|
list := NewList()
|
||||||
list.SetMainTextColor(Styles.PrimitiveBackgroundColor).
|
list.ShowSecondaryText(false).
|
||||||
|
SetMainTextColor(Styles.PrimitiveBackgroundColor).
|
||||||
SetSelectedTextColor(Styles.PrimitiveBackgroundColor).
|
SetSelectedTextColor(Styles.PrimitiveBackgroundColor).
|
||||||
SetSelectedBackgroundColor(Styles.PrimaryTextColor).
|
SetSelectedBackgroundColor(Styles.PrimaryTextColor).
|
||||||
SetHighlightFullLine(true).
|
SetHighlightFullLine(true).
|
||||||
|
160
inputfield.go
160
inputfield.go
@ -4,6 +4,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
@ -71,6 +72,16 @@ type InputField struct {
|
|||||||
// The number of bytes of the text string skipped ahead while drawing.
|
// The number of bytes of the text string skipped ahead while drawing.
|
||||||
offset int
|
offset int
|
||||||
|
|
||||||
|
// An optional autocomplete function which receives the current text of the
|
||||||
|
// input field and returns a slice of strings to be displayed in a drop-down
|
||||||
|
// selection.
|
||||||
|
autocomplete func(text string) []string
|
||||||
|
|
||||||
|
// The List object which shows the selectable autocomplete entries. If not
|
||||||
|
// nil, the list's main texts represent the current autocomplete entries.
|
||||||
|
autocompleteList *List
|
||||||
|
autocompleteListMutex sync.Mutex
|
||||||
|
|
||||||
// An optional function which may reject the last character that was entered.
|
// An optional function which may reject the last character that was entered.
|
||||||
accept func(text string, ch rune) bool
|
accept func(text string, ch rune) bool
|
||||||
|
|
||||||
@ -190,6 +201,70 @@ func (i *InputField) SetMaskCharacter(mask rune) *InputField {
|
|||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetAutocompleteFunc sets an autocomplete callback function which may return
|
||||||
|
// strings to be selected from a drop-down based on the current text of the
|
||||||
|
// input field. The drop-down appears only if len(entries) > 0. The callback is
|
||||||
|
// invoked in this function and whenever the current text changes or when
|
||||||
|
// Autocomplete() is called. Entries are cleared when the user selects an entry
|
||||||
|
// or presses Escape.
|
||||||
|
func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entries []string)) *InputField {
|
||||||
|
i.autocomplete = callback
|
||||||
|
i.Autocomplete()
|
||||||
|
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
|
||||||
|
// next time the input field is drawn.
|
||||||
|
//
|
||||||
|
// It is safe to call this function from any goroutine. Note that the input
|
||||||
|
// field is not redrawn automatically unless called from the main goroutine
|
||||||
|
// (e.g. in response to events).
|
||||||
|
func (i *InputField) Autocomplete() *InputField {
|
||||||
|
i.autocompleteListMutex.Lock()
|
||||||
|
defer i.autocompleteListMutex.Unlock()
|
||||||
|
if i.autocomplete == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we have any autocomplete entries?
|
||||||
|
entries := i.autocomplete(i.text)
|
||||||
|
if len(entries) == 0 {
|
||||||
|
// No entries, no list.
|
||||||
|
i.autocompleteList = nil
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a list if we have none.
|
||||||
|
if i.autocompleteList == nil {
|
||||||
|
i.autocompleteList = NewList()
|
||||||
|
i.autocompleteList.ShowSecondaryText(false).
|
||||||
|
SetMainTextColor(Styles.PrimitiveBackgroundColor).
|
||||||
|
SetSelectedTextColor(Styles.PrimitiveBackgroundColor).
|
||||||
|
SetSelectedBackgroundColor(Styles.PrimaryTextColor).
|
||||||
|
SetHighlightFullLine(true).
|
||||||
|
SetBackgroundColor(Styles.MoreContrastBackgroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill it with the entries.
|
||||||
|
currentEntry := -1
|
||||||
|
i.autocompleteList.Clear()
|
||||||
|
for index, entry := range entries {
|
||||||
|
i.autocompleteList.AddItem(entry, "", 0, nil)
|
||||||
|
if currentEntry < 0 && entry == i.text {
|
||||||
|
currentEntry = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the selection if we have one.
|
||||||
|
if currentEntry >= 0 {
|
||||||
|
i.autocompleteList.SetCurrentItem(currentEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
// SetAcceptanceFunc sets a handler which may reject the last character that was
|
// SetAcceptanceFunc sets a handler which may reject the last character that was
|
||||||
// entered (by returning false).
|
// entered (by returning false).
|
||||||
//
|
//
|
||||||
@ -319,6 +394,38 @@ func (i *InputField) Draw(screen tcell.Screen) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw autocomplete list.
|
||||||
|
i.autocompleteListMutex.Lock()
|
||||||
|
defer i.autocompleteListMutex.Unlock()
|
||||||
|
if i.autocompleteList != nil {
|
||||||
|
// How much space do we need?
|
||||||
|
lheight := i.autocompleteList.GetItemCount()
|
||||||
|
lwidth := 0
|
||||||
|
for index := 0; index < lheight; index++ {
|
||||||
|
entry, _ := i.autocompleteList.GetItemText(index)
|
||||||
|
width := TaggedStringWidth(entry)
|
||||||
|
if width > lwidth {
|
||||||
|
lwidth = width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We prefer to drop down but if there is no space, maybe drop up?
|
||||||
|
lx := x
|
||||||
|
ly := y + 1
|
||||||
|
_, sheight := screen.Size()
|
||||||
|
if ly+lheight >= sheight && ly-2 > lheight-ly {
|
||||||
|
ly = y - lheight
|
||||||
|
if ly < 0 {
|
||||||
|
ly = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ly+lheight >= sheight {
|
||||||
|
lheight = sheight - ly
|
||||||
|
}
|
||||||
|
i.autocompleteList.SetRect(lx, ly, lwidth, lheight)
|
||||||
|
i.autocompleteList.Draw(screen)
|
||||||
|
}
|
||||||
|
|
||||||
// Set cursor.
|
// Set cursor.
|
||||||
if i.focus.HasFocus() {
|
if i.focus.HasFocus() {
|
||||||
screen.ShowCursor(x+cursorScreenPos, y)
|
screen.ShowCursor(x+cursorScreenPos, y)
|
||||||
@ -331,8 +438,11 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
|
|||||||
// Trigger changed events.
|
// Trigger changed events.
|
||||||
currentText := i.text
|
currentText := i.text
|
||||||
defer func() {
|
defer func() {
|
||||||
if i.text != currentText && i.changed != nil {
|
if i.text != currentText {
|
||||||
i.changed(i.text)
|
i.Autocomplete()
|
||||||
|
if i.changed != nil {
|
||||||
|
i.changed(i.text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -370,7 +480,19 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finish up.
|
||||||
|
finish := func(key tcell.Key) {
|
||||||
|
if i.done != nil {
|
||||||
|
i.done(key)
|
||||||
|
}
|
||||||
|
if i.finished != nil {
|
||||||
|
i.finished(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process key event.
|
// Process key event.
|
||||||
|
i.autocompleteListMutex.Lock()
|
||||||
|
defer i.autocompleteListMutex.Unlock()
|
||||||
switch key := event.Key(); key {
|
switch key := event.Key(); key {
|
||||||
case tcell.KeyRune: // Regular character.
|
case tcell.KeyRune: // Regular character.
|
||||||
if event.Modifiers()&tcell.ModAlt > 0 {
|
if event.Modifiers()&tcell.ModAlt > 0 {
|
||||||
@ -435,12 +557,36 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
|
|||||||
home()
|
home()
|
||||||
case tcell.KeyEnd, tcell.KeyCtrlE:
|
case tcell.KeyEnd, tcell.KeyCtrlE:
|
||||||
end()
|
end()
|
||||||
case tcell.KeyEnter, tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done.
|
case tcell.KeyEnter, tcell.KeyEscape: // We might be done.
|
||||||
if i.done != nil {
|
if i.autocompleteList != nil {
|
||||||
i.done(key)
|
i.autocompleteList = nil
|
||||||
|
} else {
|
||||||
|
finish(key)
|
||||||
}
|
}
|
||||||
if i.finished != nil {
|
case tcell.KeyDown, tcell.KeyTab: // Autocomplete selection.
|
||||||
i.finished(key)
|
if i.autocompleteList != nil {
|
||||||
|
count := i.autocompleteList.GetItemCount()
|
||||||
|
newEntry := i.autocompleteList.GetCurrentItem() + 1
|
||||||
|
if newEntry >= count {
|
||||||
|
newEntry = 0
|
||||||
|
}
|
||||||
|
i.autocompleteList.SetCurrentItem(newEntry)
|
||||||
|
currentText, _ = i.autocompleteList.GetItemText(newEntry) // Don't trigger changed function twice.
|
||||||
|
i.SetText(currentText)
|
||||||
|
} else {
|
||||||
|
finish(key)
|
||||||
|
}
|
||||||
|
case tcell.KeyUp, tcell.KeyBacktab: // Autocomplete selection.
|
||||||
|
if i.autocompleteList != nil {
|
||||||
|
newEntry := i.autocompleteList.GetCurrentItem() - 1
|
||||||
|
if newEntry < 0 {
|
||||||
|
newEntry = i.autocompleteList.GetItemCount() - 1
|
||||||
|
}
|
||||||
|
i.autocompleteList.SetCurrentItem(newEntry)
|
||||||
|
currentText, _ = i.autocompleteList.GetItemText(newEntry) // Don't trigger changed function twice.
|
||||||
|
i.SetText(currentText)
|
||||||
|
} else {
|
||||||
|
finish(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user