2017-12-15 22:29:21 +08:00
|
|
|
package tview
|
|
|
|
|
|
|
|
import (
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/gdamore/tcell"
|
2018-01-11 22:45:52 +08:00
|
|
|
runewidth "github.com/mattn/go-runewidth"
|
2017-12-15 22:29:21 +08:00
|
|
|
)
|
|
|
|
|
2017-12-19 03:04:52 +08:00
|
|
|
// FormItem is the interface all form items must implement to be able to be
|
|
|
|
// included in a form.
|
|
|
|
type FormItem interface {
|
|
|
|
Primitive
|
|
|
|
|
|
|
|
// GetLabel returns the item's label text.
|
|
|
|
GetLabel() string
|
|
|
|
|
|
|
|
// SetFormAttributes sets a number of item attributes at once.
|
|
|
|
SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem
|
|
|
|
|
|
|
|
// SetEnteredFunc sets the handler function for when the user finished
|
|
|
|
// entering data into the item. The handler may receive events for the
|
|
|
|
// Enter key (we're done), the Escape key (cancel input), the Tab key (move to
|
|
|
|
// next field), and the Backtab key (move to previous field).
|
|
|
|
SetFinishedFunc(handler func(key tcell.Key)) FormItem
|
|
|
|
}
|
|
|
|
|
2017-12-15 22:29:21 +08:00
|
|
|
// Form is a Box which contains multiple input fields, one per row.
|
2018-01-07 23:39:06 +08:00
|
|
|
//
|
|
|
|
// See https://github.com/rivo/tview/wiki/Form for an example.
|
2017-12-15 22:29:21 +08:00
|
|
|
type Form struct {
|
2017-12-17 05:48:26 +08:00
|
|
|
*Box
|
2017-12-15 22:29:21 +08:00
|
|
|
|
|
|
|
// The items of the form (one row per item).
|
2017-12-19 03:04:52 +08:00
|
|
|
items []FormItem
|
2017-12-15 22:29:21 +08:00
|
|
|
|
2017-12-16 06:03:01 +08:00
|
|
|
// The buttons of the form.
|
|
|
|
buttons []*Button
|
|
|
|
|
2017-12-21 03:54:49 +08:00
|
|
|
// The alignment of the buttons.
|
|
|
|
buttonsAlign int
|
|
|
|
|
2017-12-15 22:29:21 +08:00
|
|
|
// The number of empty rows between items.
|
|
|
|
itemPadding int
|
|
|
|
|
2017-12-16 06:03:01 +08:00
|
|
|
// The index of the item or button which has focus. (Items are counted first,
|
|
|
|
// buttons are counted last.)
|
|
|
|
focusedElement int
|
2017-12-15 22:29:21 +08:00
|
|
|
|
|
|
|
// The label color.
|
|
|
|
labelColor tcell.Color
|
|
|
|
|
|
|
|
// The background color of the input area.
|
|
|
|
fieldBackgroundColor tcell.Color
|
|
|
|
|
|
|
|
// The text color of the input area.
|
|
|
|
fieldTextColor tcell.Color
|
2017-12-21 03:54:49 +08:00
|
|
|
|
|
|
|
// The background color of the buttons.
|
|
|
|
buttonBackgroundColor tcell.Color
|
|
|
|
|
|
|
|
// The color of the button text.
|
|
|
|
buttonTextColor tcell.Color
|
|
|
|
|
|
|
|
// An optional function which is called when the user hits Escape.
|
|
|
|
cancel func()
|
2017-12-15 22:29:21 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewForm returns a new form.
|
|
|
|
func NewForm() *Form {
|
2017-12-22 01:08:53 +08:00
|
|
|
box := NewBox().SetBorderPadding(1, 1, 1, 1)
|
2017-12-17 05:48:26 +08:00
|
|
|
|
|
|
|
f := &Form{
|
2017-12-21 03:54:49 +08:00
|
|
|
Box: box,
|
|
|
|
itemPadding: 1,
|
2018-01-10 16:43:32 +08:00
|
|
|
labelColor: Styles.SecondaryTextColor,
|
|
|
|
fieldBackgroundColor: Styles.ContrastBackgroundColor,
|
|
|
|
fieldTextColor: Styles.PrimaryTextColor,
|
|
|
|
buttonBackgroundColor: Styles.ContrastBackgroundColor,
|
|
|
|
buttonTextColor: Styles.PrimaryTextColor,
|
2017-12-15 22:29:21 +08:00
|
|
|
}
|
2017-12-17 05:48:26 +08:00
|
|
|
|
|
|
|
f.focus = f
|
|
|
|
|
|
|
|
return f
|
2017-12-15 22:29:21 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// SetItemPadding sets the number of empty rows between form items.
|
|
|
|
func (f *Form) SetItemPadding(padding int) *Form {
|
|
|
|
f.itemPadding = padding
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetLabelColor sets the color of the labels.
|
|
|
|
func (f *Form) SetLabelColor(color tcell.Color) *Form {
|
|
|
|
f.labelColor = color
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetFieldBackgroundColor sets the background color of the input areas.
|
|
|
|
func (f *Form) SetFieldBackgroundColor(color tcell.Color) *Form {
|
|
|
|
f.fieldBackgroundColor = color
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetFieldTextColor sets the text color of the input areas.
|
|
|
|
func (f *Form) SetFieldTextColor(color tcell.Color) *Form {
|
|
|
|
f.fieldTextColor = color
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
2017-12-21 03:54:49 +08:00
|
|
|
// SetButtonsAlign sets how the buttons align horizontally, one of AlignLeft
|
|
|
|
// (the default), AlignCenter, and AlignRight.
|
|
|
|
func (f *Form) SetButtonsAlign(align int) *Form {
|
|
|
|
f.buttonsAlign = align
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetButtonBackgroundColor sets the background color of the buttons.
|
|
|
|
func (f *Form) SetButtonBackgroundColor(color tcell.Color) *Form {
|
|
|
|
f.buttonBackgroundColor = color
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetButtonTextColor sets the color of the button texts.
|
|
|
|
func (f *Form) SetButtonTextColor(color tcell.Color) *Form {
|
|
|
|
f.buttonTextColor = color
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
2017-12-19 03:04:52 +08:00
|
|
|
// AddInputField adds an input field to the form. It has a label, an optional
|
|
|
|
// initial value, a field length (a value of 0 extends it as far as possible),
|
2018-01-10 17:40:51 +08:00
|
|
|
// an optional accept function to validate the item's value (set to nil to
|
|
|
|
// accept any text), and an (optional) callback function which is invoked when
|
|
|
|
// the input field's text has changed.
|
|
|
|
func (f *Form) AddInputField(label, value string, fieldLength int, accept func(textToCheck string, lastChar rune) bool, changed func(text string)) *Form {
|
2017-12-15 22:29:21 +08:00
|
|
|
f.items = append(f.items, NewInputField().
|
|
|
|
SetLabel(label).
|
|
|
|
SetText(value).
|
|
|
|
SetFieldLength(fieldLength).
|
2018-01-11 23:13:01 +08:00
|
|
|
SetAcceptanceFunc(accept).
|
|
|
|
SetChangedFunc(changed))
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddPasswordField adds a password field to the form. This is similar to an
|
|
|
|
// input field except that the user's input not shown. Instead, a "mask"
|
|
|
|
// character is displayed. The password field has a label, an optional initial
|
|
|
|
// value, a field length (a value of 0 extends it as far as possible), and an
|
|
|
|
// (optional) callback function which is invoked when the input field's text has
|
|
|
|
// changed.
|
|
|
|
func (f *Form) AddPasswordField(label, value string, fieldLength int, mask rune, changed func(text string)) *Form {
|
|
|
|
if mask == 0 {
|
|
|
|
mask = '*'
|
|
|
|
}
|
|
|
|
f.items = append(f.items, NewInputField().
|
|
|
|
SetLabel(label).
|
|
|
|
SetText(value).
|
|
|
|
SetFieldLength(fieldLength).
|
|
|
|
SetMaskCharacter(mask).
|
|
|
|
SetChangedFunc(changed))
|
2017-12-15 22:29:21 +08:00
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
2017-12-19 03:04:52 +08:00
|
|
|
// AddDropDown adds a drop-down element to the form. It has a label, options,
|
|
|
|
// and an (optional) callback function which is invoked when an option was
|
|
|
|
// selected.
|
|
|
|
func (f *Form) AddDropDown(label string, options []string, initialOption int, selected func(option string, optionIndex int)) *Form {
|
|
|
|
f.items = append(f.items, NewDropDown().
|
|
|
|
SetLabel(label).
|
|
|
|
SetCurrentOption(initialOption).
|
|
|
|
SetOptions(options, selected))
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
2017-12-21 03:54:49 +08:00
|
|
|
// AddCheckbox adds a checkbox to the form. It has a label, an initial state,
|
|
|
|
// and an (optional) callback function which is invoked when the state of the
|
|
|
|
// checkbox was changed by the user.
|
|
|
|
func (f *Form) AddCheckbox(label string, checked bool, changed func(checked bool)) *Form {
|
|
|
|
f.items = append(f.items, NewCheckbox().
|
|
|
|
SetLabel(label).
|
|
|
|
SetChecked(checked).
|
|
|
|
SetChangedFunc(changed))
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
2017-12-16 06:03:01 +08:00
|
|
|
// AddButton adds a new button to the form. The "selected" function is called
|
|
|
|
// when the user selects this button. It may be nil.
|
|
|
|
func (f *Form) AddButton(label string, selected func()) *Form {
|
|
|
|
f.buttons = append(f.buttons, NewButton(label).SetSelectedFunc(selected))
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
2018-01-10 17:40:51 +08:00
|
|
|
// GetElement returns the form element at the given position, starting with
|
|
|
|
// index 0. Elements are referenced in the order they were added. Buttons are
|
|
|
|
// not included.
|
|
|
|
func (f *Form) GetElement(index int) Primitive {
|
|
|
|
return f.items[index]
|
|
|
|
}
|
|
|
|
|
2017-12-21 03:54:49 +08:00
|
|
|
// SetCancelFunc sets a handler which is called when the user hits the Escape
|
|
|
|
// key.
|
|
|
|
func (f *Form) SetCancelFunc(callback func()) *Form {
|
|
|
|
f.cancel = callback
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
2017-12-15 22:29:21 +08:00
|
|
|
// Draw draws this primitive onto the screen.
|
|
|
|
func (f *Form) Draw(screen tcell.Screen) {
|
|
|
|
f.Box.Draw(screen)
|
|
|
|
|
|
|
|
// Determine the dimensions.
|
2017-12-22 01:08:53 +08:00
|
|
|
x, y, width, height := f.GetInnerRect()
|
2017-12-21 03:54:49 +08:00
|
|
|
bottomLimit := y + height
|
2017-12-16 06:03:01 +08:00
|
|
|
rightLimit := x + width
|
2017-12-15 22:29:21 +08:00
|
|
|
|
|
|
|
// Find the longest label.
|
|
|
|
var labelLength int
|
2017-12-19 03:04:52 +08:00
|
|
|
for _, item := range f.items {
|
|
|
|
label := strings.TrimSpace(item.GetLabel())
|
2018-01-11 22:45:52 +08:00
|
|
|
labelWidth := runewidth.StringWidth(label)
|
|
|
|
if labelWidth > labelLength {
|
|
|
|
labelLength = labelWidth
|
2017-12-15 22:29:21 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
labelLength++ // Add one space.
|
|
|
|
|
|
|
|
// Set up and draw the input fields.
|
2017-12-19 03:04:52 +08:00
|
|
|
for _, item := range f.items {
|
2017-12-15 22:29:21 +08:00
|
|
|
if y >= bottomLimit {
|
2017-12-16 06:03:01 +08:00
|
|
|
return // Stop here.
|
2017-12-15 22:29:21 +08:00
|
|
|
}
|
2017-12-19 03:04:52 +08:00
|
|
|
label := strings.TrimSpace(item.GetLabel())
|
|
|
|
item.SetFormAttributes(
|
2018-01-11 22:45:52 +08:00
|
|
|
label+strings.Repeat(" ", labelLength-runewidth.StringWidth(label)),
|
2017-12-19 03:04:52 +08:00
|
|
|
f.labelColor,
|
|
|
|
f.backgroundColor,
|
|
|
|
f.fieldTextColor,
|
|
|
|
f.fieldBackgroundColor,
|
|
|
|
).SetRect(x, y, width, 1)
|
|
|
|
if item.GetFocusable().HasFocus() {
|
|
|
|
defer item.Draw(screen)
|
|
|
|
} else {
|
|
|
|
item.Draw(screen)
|
|
|
|
}
|
2017-12-15 22:29:21 +08:00
|
|
|
y += 1 + f.itemPadding
|
|
|
|
}
|
2017-12-16 06:03:01 +08:00
|
|
|
|
2017-12-21 03:54:49 +08:00
|
|
|
// How wide are the buttons?
|
|
|
|
buttonWidths := make([]int, len(f.buttons))
|
|
|
|
buttonsWidth := 0
|
|
|
|
for index, button := range f.buttons {
|
2018-01-11 22:45:52 +08:00
|
|
|
width := runewidth.StringWidth(button.GetLabel()) + 4
|
2017-12-21 03:54:49 +08:00
|
|
|
buttonWidths[index] = width
|
|
|
|
buttonsWidth += width + 2
|
|
|
|
}
|
|
|
|
buttonsWidth -= 2
|
|
|
|
|
|
|
|
// Where do we place them?
|
|
|
|
if x+buttonsWidth < rightLimit {
|
|
|
|
if f.buttonsAlign == AlignRight {
|
|
|
|
x = rightLimit - buttonsWidth
|
|
|
|
} else if f.buttonsAlign == AlignCenter {
|
|
|
|
x = (x + rightLimit - buttonsWidth) / 2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Draw them.
|
2017-12-16 06:03:01 +08:00
|
|
|
if f.itemPadding == 0 {
|
|
|
|
y++
|
|
|
|
}
|
|
|
|
if y >= bottomLimit {
|
|
|
|
return // Stop here.
|
|
|
|
}
|
2017-12-21 03:54:49 +08:00
|
|
|
for index, button := range f.buttons {
|
2017-12-16 06:03:01 +08:00
|
|
|
space := rightLimit - x
|
|
|
|
if space < 1 {
|
2017-12-21 03:54:49 +08:00
|
|
|
break // No space for this button anymore.
|
2017-12-16 06:03:01 +08:00
|
|
|
}
|
2017-12-21 03:54:49 +08:00
|
|
|
buttonWidth := buttonWidths[index]
|
2017-12-16 06:03:01 +08:00
|
|
|
if buttonWidth > space {
|
|
|
|
buttonWidth = space
|
|
|
|
}
|
2017-12-21 03:54:49 +08:00
|
|
|
button.SetLabelColor(f.buttonTextColor).
|
|
|
|
SetLabelColorActivated(f.buttonBackgroundColor).
|
|
|
|
SetBackgroundColorActivated(f.buttonTextColor).
|
|
|
|
SetBackgroundColor(f.buttonBackgroundColor).
|
|
|
|
SetRect(x, y, buttonWidth, 1)
|
2017-12-16 06:03:01 +08:00
|
|
|
button.Draw(screen)
|
|
|
|
|
|
|
|
x += buttonWidth + 2
|
|
|
|
}
|
2017-12-15 22:29:21 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Focus is called by the application when the primitive receives focus.
|
2017-12-19 03:04:52 +08:00
|
|
|
func (f *Form) Focus(delegate func(p Primitive)) {
|
2017-12-16 06:03:01 +08:00
|
|
|
if len(f.items)+len(f.buttons) == 0 {
|
2017-12-15 22:29:21 +08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-12-16 06:03:01 +08:00
|
|
|
// Hand on the focus to one of our child elements.
|
|
|
|
if f.focusedElement < 0 || f.focusedElement >= len(f.items)+len(f.buttons) {
|
|
|
|
f.focusedElement = 0
|
2017-12-15 22:29:21 +08:00
|
|
|
}
|
2017-12-16 06:03:01 +08:00
|
|
|
handler := func(key tcell.Key) {
|
2017-12-15 22:29:21 +08:00
|
|
|
switch key {
|
2017-12-17 05:48:26 +08:00
|
|
|
case tcell.KeyTab, tcell.KeyEnter:
|
2017-12-16 06:03:01 +08:00
|
|
|
f.focusedElement++
|
2017-12-21 03:54:49 +08:00
|
|
|
f.Focus(delegate)
|
2017-12-16 06:03:01 +08:00
|
|
|
case tcell.KeyBacktab:
|
|
|
|
f.focusedElement--
|
|
|
|
if f.focusedElement < 0 {
|
|
|
|
f.focusedElement = len(f.items) + len(f.buttons) - 1
|
|
|
|
}
|
2017-12-21 03:54:49 +08:00
|
|
|
f.Focus(delegate)
|
2017-12-16 06:03:01 +08:00
|
|
|
case tcell.KeyEscape:
|
2017-12-21 03:54:49 +08:00
|
|
|
if f.cancel != nil {
|
|
|
|
f.cancel()
|
|
|
|
} else {
|
|
|
|
f.focusedElement = 0
|
|
|
|
f.Focus(delegate)
|
|
|
|
}
|
2017-12-15 22:29:21 +08:00
|
|
|
}
|
2017-12-16 06:03:01 +08:00
|
|
|
}
|
2017-12-19 03:04:52 +08:00
|
|
|
|
2017-12-16 06:03:01 +08:00
|
|
|
if f.focusedElement < len(f.items) {
|
|
|
|
// We're selecting an item.
|
2017-12-19 03:04:52 +08:00
|
|
|
item := f.items[f.focusedElement]
|
|
|
|
item.SetFinishedFunc(handler)
|
|
|
|
delegate(item)
|
2017-12-16 06:03:01 +08:00
|
|
|
} else {
|
|
|
|
// We're selecting a button.
|
|
|
|
button := f.buttons[f.focusedElement-len(f.items)]
|
|
|
|
button.SetBlurFunc(handler)
|
2017-12-19 03:04:52 +08:00
|
|
|
delegate(button)
|
2017-12-16 06:03:01 +08:00
|
|
|
}
|
2017-12-15 22:29:21 +08:00
|
|
|
}
|
|
|
|
|
2017-12-17 05:48:26 +08:00
|
|
|
// HasFocus returns whether or not this primitive has focus.
|
|
|
|
func (f *Form) HasFocus() bool {
|
|
|
|
for _, item := range f.items {
|
2017-12-19 03:04:52 +08:00
|
|
|
if item.GetFocusable().HasFocus() {
|
2017-12-17 05:48:26 +08:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, button := range f.buttons {
|
|
|
|
if button.focus.HasFocus() {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|