You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
445 lines
12 KiB
445 lines
12 KiB
package tview |
|
|
|
import ( |
|
"strings" |
|
|
|
"github.com/gdamore/tcell" |
|
) |
|
|
|
// DefaultFormFieldWidth is the default field screen width of form elements |
|
// whose field width is flexible (0). This is used in the Form class for |
|
// horizontal layouts. |
|
var DefaultFormFieldWidth = 10 |
|
|
|
// 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 |
|
|
|
// GetFieldWidth returns the width of the form item's field (the area which |
|
// is manipulated by the user) in number of screen cells. A value of 0 |
|
// indicates the the field width is flexible and may use as much space as |
|
// required. |
|
GetFieldWidth() int |
|
|
|
// 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 |
|
} |
|
|
|
// Form is a Box which contains multiple input fields, one per row. |
|
// |
|
// See https://github.com/rivo/tview/wiki/Form for an example. |
|
type Form struct { |
|
*Box |
|
|
|
// The items of the form (one row per item). |
|
items []FormItem |
|
|
|
// The buttons of the form. |
|
buttons []*Button |
|
|
|
// If set to true, instead of position items and buttons from top to bottom, |
|
// they are positioned from left to right. |
|
horizontal bool |
|
|
|
// The alignment of the buttons. |
|
buttonsAlign int |
|
|
|
// The number of empty rows between items. |
|
itemPadding int |
|
|
|
// The index of the item or button which has focus. (Items are counted first, |
|
// buttons are counted last.) |
|
focusedElement int |
|
|
|
// 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 |
|
|
|
// 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() |
|
} |
|
|
|
// NewForm returns a new form. |
|
func NewForm() *Form { |
|
box := NewBox().SetBorderPadding(1, 1, 1, 1) |
|
|
|
f := &Form{ |
|
Box: box, |
|
itemPadding: 1, |
|
labelColor: Styles.SecondaryTextColor, |
|
fieldBackgroundColor: Styles.ContrastBackgroundColor, |
|
fieldTextColor: Styles.PrimaryTextColor, |
|
buttonBackgroundColor: Styles.ContrastBackgroundColor, |
|
buttonTextColor: Styles.PrimaryTextColor, |
|
} |
|
|
|
f.focus = f |
|
|
|
return f |
|
} |
|
|
|
// SetItemPadding sets the number of empty rows between form items for vertical |
|
// layouts and the number of empty cells between form items for horizontal |
|
// layouts. |
|
func (f *Form) SetItemPadding(padding int) *Form { |
|
f.itemPadding = padding |
|
return f |
|
} |
|
|
|
// SetHorizontal sets the direction the form elements are laid out. If set to |
|
// true, instead of positioning them from top to bottom (the default), they are |
|
// positioned from left to right, moving into the next row if there is not |
|
// enough space. |
|
func (f *Form) SetHorizontal(horizontal bool) *Form { |
|
f.horizontal = horizontal |
|
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 |
|
} |
|
|
|
// SetButtonsAlign sets how the buttons align horizontally, one of AlignLeft |
|
// (the default), AlignCenter, and AlignRight. This is only |
|
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 |
|
} |
|
|
|
// AddInputField adds an input field to the form. It has a label, an optional |
|
// initial value, a field width (a value of 0 extends it as far as possible), |
|
// 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, fieldWidth int, accept func(textToCheck string, lastChar rune) bool, changed func(text string)) *Form { |
|
f.items = append(f.items, NewInputField(). |
|
SetLabel(label). |
|
SetText(value). |
|
SetFieldWidth(fieldWidth). |
|
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 width (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, fieldWidth int, mask rune, changed func(text string)) *Form { |
|
if mask == 0 { |
|
mask = '*' |
|
} |
|
f.items = append(f.items, NewInputField(). |
|
SetLabel(label). |
|
SetText(value). |
|
SetFieldWidth(fieldWidth). |
|
SetMaskCharacter(mask). |
|
SetChangedFunc(changed)) |
|
return f |
|
} |
|
|
|
// 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 |
|
} |
|
|
|
// 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 |
|
} |
|
|
|
// 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 |
|
} |
|
|
|
// Clear removes all input elements from the form, including the buttons if |
|
// specified. |
|
func (f *Form) Clear(includeButtons bool) *Form { |
|
f.items = nil |
|
if includeButtons { |
|
f.buttons = nil |
|
} |
|
f.focusedElement = 0 |
|
return f |
|
} |
|
|
|
// AddFormItem adds a new item to the form. This can be used to add your own |
|
// objects to the form. Note, however, that the Form class will override some |
|
// of its attributes to make it work in the form context. |
|
func (f *Form) AddFormItem(item FormItem) *Form { |
|
f.items = append(f.items, item) |
|
return f |
|
} |
|
|
|
// GetFormItem 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) GetFormItem(index int) FormItem { |
|
return f.items[index] |
|
} |
|
|
|
// 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 |
|
} |
|
|
|
// Draw draws this primitive onto the screen. |
|
func (f *Form) Draw(screen tcell.Screen) { |
|
f.Box.Draw(screen) |
|
|
|
// Determine the dimensions. |
|
x, y, width, height := f.GetInnerRect() |
|
bottomLimit := y + height |
|
rightLimit := x + width |
|
startX := x |
|
|
|
// Find the longest label. |
|
var maxLabelWidth int |
|
for _, item := range f.items { |
|
label := strings.TrimSpace(item.GetLabel()) |
|
labelWidth := StringWidth(label) |
|
if labelWidth > maxLabelWidth { |
|
maxLabelWidth = labelWidth |
|
} |
|
} |
|
maxLabelWidth++ // Add one space. |
|
|
|
// Set up and draw the input fields. |
|
for _, item := range f.items { |
|
// Stop if there is no more space. |
|
if y >= bottomLimit { |
|
return |
|
} |
|
|
|
// Calculate the space needed. |
|
label := strings.TrimSpace(item.GetLabel()) |
|
labelWidth := StringWidth(label) |
|
var itemWidth int |
|
if f.horizontal { |
|
fieldWidth := item.GetFieldWidth() |
|
if fieldWidth == 0 { |
|
fieldWidth = DefaultFormFieldWidth |
|
} |
|
label += " " |
|
labelWidth++ |
|
itemWidth = labelWidth + fieldWidth |
|
} else { |
|
// We want all fields to align vertically. |
|
label += strings.Repeat(" ", maxLabelWidth-labelWidth) |
|
itemWidth = width |
|
} |
|
|
|
// Advance to next line if there is no space. |
|
if f.horizontal && x+labelWidth+1 >= rightLimit { |
|
x = startX |
|
y += 2 |
|
} |
|
|
|
// Adjust the item's attributes. |
|
if x+itemWidth >= rightLimit { |
|
itemWidth = rightLimit - x |
|
} |
|
item.SetFormAttributes( |
|
label, |
|
f.labelColor, |
|
f.backgroundColor, |
|
f.fieldTextColor, |
|
f.fieldBackgroundColor, |
|
).SetRect(x, y, itemWidth, 1) |
|
|
|
// Draw items with focus last (in case of overlaps). |
|
if item.GetFocusable().HasFocus() { |
|
defer item.Draw(screen) |
|
} else { |
|
item.Draw(screen) |
|
} |
|
|
|
// Advance to next item. |
|
if f.horizontal { |
|
x += itemWidth + f.itemPadding |
|
} else { |
|
y += 1 + f.itemPadding |
|
} |
|
} |
|
|
|
// How wide are the buttons? |
|
buttonWidths := make([]int, len(f.buttons)) |
|
buttonsWidth := 0 |
|
for index, button := range f.buttons { |
|
w := StringWidth(button.GetLabel()) + 4 |
|
buttonWidths[index] = w |
|
buttonsWidth += w + 1 |
|
} |
|
buttonsWidth-- |
|
|
|
// Where do we place them? |
|
if !f.horizontal && x+buttonsWidth < rightLimit { |
|
if f.buttonsAlign == AlignRight { |
|
x = rightLimit - buttonsWidth |
|
} else if f.buttonsAlign == AlignCenter { |
|
x = (x + rightLimit - buttonsWidth) / 2 |
|
} |
|
|
|
// In vertical layouts, buttons always appear after an empty line. |
|
if f.itemPadding == 0 { |
|
y++ |
|
} |
|
} |
|
|
|
// Draw them. |
|
for index, button := range f.buttons { |
|
if y >= bottomLimit { |
|
return // Stop here. |
|
} |
|
|
|
space := rightLimit - x |
|
buttonWidth := buttonWidths[index] |
|
if f.horizontal { |
|
if space < buttonWidth-4 { |
|
x = startX |
|
y += 2 |
|
space = width |
|
} |
|
} else { |
|
if space < 1 { |
|
break // No space for this button anymore. |
|
} |
|
} |
|
if buttonWidth > space { |
|
buttonWidth = space |
|
} |
|
button.SetLabelColor(f.buttonTextColor). |
|
SetLabelColorActivated(f.buttonBackgroundColor). |
|
SetBackgroundColorActivated(f.buttonTextColor). |
|
SetBackgroundColor(f.buttonBackgroundColor). |
|
SetRect(x, y, buttonWidth, 1) |
|
button.Draw(screen) |
|
|
|
x += buttonWidth + 1 |
|
} |
|
} |
|
|
|
// Focus is called by the application when the primitive receives focus. |
|
func (f *Form) Focus(delegate func(p Primitive)) { |
|
if len(f.items)+len(f.buttons) == 0 { |
|
return |
|
} |
|
|
|
// 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 |
|
} |
|
handler := func(key tcell.Key) { |
|
switch key { |
|
case tcell.KeyTab, tcell.KeyEnter: |
|
f.focusedElement++ |
|
f.Focus(delegate) |
|
case tcell.KeyBacktab: |
|
f.focusedElement-- |
|
if f.focusedElement < 0 { |
|
f.focusedElement = len(f.items) + len(f.buttons) - 1 |
|
} |
|
f.Focus(delegate) |
|
case tcell.KeyEscape: |
|
if f.cancel != nil { |
|
f.cancel() |
|
} else { |
|
f.focusedElement = 0 |
|
f.Focus(delegate) |
|
} |
|
} |
|
} |
|
|
|
if f.focusedElement < len(f.items) { |
|
// We're selecting an item. |
|
item := f.items[f.focusedElement] |
|
item.SetFinishedFunc(handler) |
|
delegate(item) |
|
} else { |
|
// We're selecting a button. |
|
button := f.buttons[f.focusedElement-len(f.items)] |
|
button.SetBlurFunc(handler) |
|
delegate(button) |
|
} |
|
} |
|
|
|
// HasFocus returns whether or not this primitive has focus. |
|
func (f *Form) HasFocus() bool { |
|
for _, item := range f.items { |
|
if item.GetFocusable().HasFocus() { |
|
return true |
|
} |
|
} |
|
for _, button := range f.buttons { |
|
if button.focus.HasFocus() { |
|
return true |
|
} |
|
} |
|
return false |
|
}
|
|
|