From c8d49e02f708e4fbeb0d4fa55470255ac333f2e1 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Sun, 6 May 2018 13:58:40 +0300 Subject: [PATCH] New features --- box.go | 81 +++++++++++++++++------- button.go | 2 +- checkbox.go | 5 ++ dropdown.go | 5 ++ flex.go | 13 +++- form.go | 167 ++++++++++++++++++++++++++++++++++++------------- inputfield.go | 82 +++++++++++++++++++++--- listbox.go | 86 ++++++++++++++++++++----- modal.go | 13 +++- pages.go | 1 + radiobutton.go | 112 +++++++++++++++++++++++++++++---- table.go | 9 ++- textview.go | 114 +++++++++++++++++++++++++++++++-- 13 files changed, 576 insertions(+), 114 deletions(-) diff --git a/box.go b/box.go index 1dc3957..eec1ec6 100644 --- a/box.go +++ b/box.go @@ -1,6 +1,8 @@ package tview import ( + "math/rand" + "github.com/gdamore/tcell" ) @@ -13,9 +15,12 @@ import ( // // See https://github.com/rivo/tview/wiki/Box for an example. type Box struct { + id int // The position of the rect. x, y, width, height int + maxWidth, maxHeight, minWidth, minHeight int + // The inner rect reserved for the box's content. innerX, innerY, innerWidth, innerHeight int @@ -67,6 +72,7 @@ type Box struct { // NewBox returns a Box without a border. func NewBox() *Box { b := &Box{ + id: rand.Int(), width: 15, height: 10, innerX: -1, // Mark as uninitialized. @@ -80,15 +86,29 @@ func NewBox() *Box { return b } -// SetHeight sets height of the box -func (b *Box) SetHeight(height int) *Box { +// GetID returns unique id of box +func (b *Box) GetID() int { + return b.id +} + +// SetSize sets size of the box +func (b *Box) SetSize(width, height int) *Box { + b.width = width b.height = height return b } -// SetWidth sets width of the box -func (b *Box) SetWidth(width int) *Box { - b.width = width +// SetMinSize sets min size of the box +func (b *Box) SetMinSize(width, height int) *Box { + b.minWidth = width + b.minHeight = height + return b +} + +// SetMaxSize sets max size of the box +func (b *Box) SetMaxSize(width, height int) *Box { + b.maxWidth = width + b.maxHeight = height return b } @@ -114,7 +134,23 @@ func (b *Box) GetBorderPadding() (top, bottom, left, right int) { // GetRect returns the current position of the rectangle, x, y, width, and // height. func (b *Box) GetRect() (int, int, int, int) { - return b.x, b.y, b.width, b.height + width := b.width + switch { + case b.maxWidth > 0 && width > b.maxWidth: + width = b.maxWidth + case b.minWidth > 0 && width < b.minWidth: + width = b.minWidth + } + + height := b.height + switch { + case b.maxHeight > 0 && height > b.maxHeight: + height = b.maxHeight + case b.minHeight > 0 && height < b.minHeight: + height = b.minHeight + } + + return b.x, b.y, width, height } // GetInnerRect returns the position of the inner rectangle (x, y, width, @@ -256,8 +292,9 @@ func (b *Box) SetTitleAlign(align int) *Box { // Draw draws this primitive onto the screen. func (b *Box) Draw(screen tcell.Screen) { + _, _, width, height := b.GetRect() // Don't draw anything if there is no space. - if b.width <= 0 || b.height <= 0 { + if width <= 0 || height <= 0 { return } @@ -265,14 +302,14 @@ func (b *Box) Draw(screen tcell.Screen) { // Fill background. background := def.Background(b.backgroundColor) - for y := b.y; y < b.y+b.height; y++ { - for x := b.x; x < b.x+b.width; x++ { + for y := b.y; y < b.y+height; y++ { + for x := b.x; x < b.x+width; x++ { screen.SetContent(x, y, ' ', nil, background) } } // Draw border. - if b.border && b.width >= 2 && b.height >= 2 { + if b.border && width >= 2 && height >= 2 { border := background.Foreground(b.borderColor) var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune if b.focus.HasFocus() { @@ -290,34 +327,34 @@ func (b *Box) Draw(screen tcell.Screen) { bottomLeft = Styles.GraphicsBottomLeftCorner bottomRight = Styles.GraphicsBottomRightCorner } - for x := b.x + 1; x < b.x+b.width-1; x++ { + for x := b.x + 1; x < b.x+width-1; x++ { screen.SetContent(x, b.y, vertical, nil, border) - screen.SetContent(x, b.y+b.height-1, vertical, nil, border) + screen.SetContent(x, b.y+height-1, vertical, nil, border) } - for y := b.y + 1; y < b.y+b.height-1; y++ { + for y := b.y + 1; y < b.y+height-1; y++ { screen.SetContent(b.x, y, horizontal, nil, border) - screen.SetContent(b.x+b.width-1, y, horizontal, nil, border) + screen.SetContent(b.x+width-1, y, horizontal, nil, border) } screen.SetContent(b.x, b.y, topLeft, nil, border) - screen.SetContent(b.x+b.width-1, b.y, topRight, nil, border) - screen.SetContent(b.x, b.y+b.height-1, bottomLeft, nil, border) - screen.SetContent(b.x+b.width-1, b.y+b.height-1, bottomRight, nil, border) + screen.SetContent(b.x+width-1, b.y, topRight, nil, border) + screen.SetContent(b.x, b.y+height-1, bottomLeft, nil, border) + screen.SetContent(b.x+width-1, b.y+height-1, bottomRight, nil, border) // Draw title. - if b.title != "" && b.width >= 4 { + if b.title != "" && width >= 4 { title := " " + b.title + " " - _, printed := Print(screen, title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor) + _, printed := Print(screen, title, b.x+1, b.y, width-2, b.titleAlign, b.titleColor) if StringWidth(title)-printed > 0 && printed > 0 { - _, _, style, _ := screen.GetContent(b.x+b.width-2, b.y) + _, _, style, _ := screen.GetContent(b.x+width-2, b.y) fg, _, _ := style.Decompose() - Print(screen, string(Styles.GraphicsEllipsis), b.x+b.width-2, b.y, 1, AlignLeft, fg) + Print(screen, string(Styles.GraphicsEllipsis), b.x+width-2, b.y, 1, AlignLeft, fg) } } } // Call custom draw function. if b.draw != nil { - b.innerX, b.innerY, b.innerWidth, b.innerHeight = b.draw(screen, b.x, b.y, b.width, b.height) + b.innerX, b.innerY, b.innerWidth, b.innerHeight = b.draw(screen, b.x, b.y, width, height) } else { // Remember the inner rect. b.innerX = -1 diff --git a/button.go b/button.go index 3f1adc1..5cc1c40 100644 --- a/button.go +++ b/button.go @@ -144,7 +144,7 @@ func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Prim if b.selected != nil { b.selected() } - case tcell.KeyBacktab, tcell.KeyTab, tcell.KeyEscape: // Leave. No action. + case tcell.KeyBacktab, tcell.KeyTab, tcell.KeyEscape, tcell.KeyLeft, tcell.KeyRight: // Leave. No action. if b.blur != nil { b.blur(key) } diff --git a/checkbox.go b/checkbox.go index 25abe5a..9317339 100644 --- a/checkbox.go +++ b/checkbox.go @@ -209,6 +209,11 @@ func (c *Checkbox) SetFinishedFunc(handler func(key tcell.Key)) FormItem { return c } +// GetFinishedFunc returns SetDoneFunc(). +func (c *Checkbox) GetFinishedFunc() func(key tcell.Key) { + return c.finished +} + // Draw draws this primitive onto the screen. func (c *Checkbox) Draw(screen tcell.Screen) { c.Box.Draw(screen) diff --git a/dropdown.go b/dropdown.go index cf2f859..ca5e7a4 100644 --- a/dropdown.go +++ b/dropdown.go @@ -289,6 +289,11 @@ func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) FormItem { return d } +// GetFinishedFunc returns SetDoneFunc(). +func (d *DropDown) GetFinishedFunc() func(key tcell.Key) { + return d.finished +} + // Draw draws this primitive onto the screen. func (d *DropDown) Draw(screen tcell.Screen) { d.Box.Draw(screen) diff --git a/flex.go b/flex.go index 3e191f0..33460c0 100644 --- a/flex.go +++ b/flex.go @@ -83,6 +83,12 @@ func (f *Flex) AddItem(item Primitive, fixedSize, proportion int, focus bool) *F return f } +// AddModal adds a new model window +func (f *Flex) AddModal(item Primitive) *Flex { + f.items = append(f.items, flexItem{Item: item, FixedSize: -1, Proportion: 0, Focus: false}) + return f +} + // RemoveItem removes all items for the given primitive from the container, // keeping the order of the remaining items intact. func (f *Flex) RemoveItem(p Primitive) *Flex { @@ -139,7 +145,7 @@ func (f *Flex) Draw(screen tcell.Screen) { } for _, item := range f.items { size := item.FixedSize - if size <= 0 { + if size == 0 { size = distSize * item.Proportion / proportionSum distSize -= size proportionSum -= item.Proportion @@ -151,7 +157,9 @@ func (f *Flex) Draw(screen tcell.Screen) { item.Item.SetRect(x, pos, width, size) } } - pos += size + if size > 0 { + pos += size + } if item.Item != nil { if item.Item.GetFocusable().HasFocus() { @@ -175,6 +183,7 @@ func (f *Flex) Focus(delegate func(p Primitive)) { // HasFocus returns whether or not this primitive has focus. func (f *Flex) HasFocus() bool { + //fmt.Println(f.items) for _, item := range f.items { if item.Item != nil && item.Item.GetFocusable().HasFocus() { return true diff --git a/form.go b/form.go index 76ae968..f9b1588 100644 --- a/form.go +++ b/form.go @@ -2,7 +2,6 @@ package tview import ( "github.com/gdamore/tcell" - "github.com/rivo/tview" ) // DefaultFormFieldWidth is the default field screen width of form elements @@ -40,6 +39,8 @@ type FormItem interface { // 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 + + GetID() int } // Form allows you to combine multiple one-line form elements into a vertical @@ -64,8 +65,8 @@ type Form struct { // The alignment of the buttons. buttonsAlign int - buttonsPaddingTop int - buttonsPaddingSeparate int + buttonsPaddingTop int + buttonsIndent int // The number of empty rows between items. itemPadding int @@ -105,17 +106,17 @@ func NewForm() *Form { box := NewBox().SetBorderPadding(1, 1, 1, 1) f := &Form{ - Box: box, - align: AlignLeft, - columnPadding: 1, - itemPadding: 1, - buttonsPaddingTop: 2, - buttonsPaddingSeparate: 4, - labelColor: Styles.LabelTextColor, - fieldBackgroundColor: Styles.FieldBackgroundColor, - fieldTextColor: Styles.FieldTextColor, - buttonBackgroundColor: Styles.ButtonBackgroundColor, - buttonTextColor: Styles.ButtonTextColor, + Box: box, + align: AlignLeft, + columnPadding: 1, + itemPadding: 1, + buttonsPaddingTop: 2, + buttonsIndent: 4, + labelColor: Styles.LabelTextColor, + fieldBackgroundColor: Styles.FieldBackgroundColor, + fieldTextColor: Styles.FieldTextColor, + buttonBackgroundColor: Styles.ButtonBackgroundColor, + buttonTextColor: Styles.ButtonTextColor, } f.width = 0 @@ -146,6 +147,18 @@ func (f *Form) SetColumnPadding(padding int) *Form { return f } +// SetButtonPadding sets the number of empty rows between fields. +func (f *Form) SetButtonPadding(padding int) *Form { + f.buttonsPaddingTop = padding + return f +} + +// SetButtonIndent makes indent between buttons +func (f *Form) SetButtonIndent(padding int) *Form { + f.buttonsIndent = 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 @@ -305,6 +318,13 @@ func (f *Form) GetFormItem(index int) FormItem { return f.items[index] } +// GetFormButton 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) GetFormButton(index int) *Button { + return f.buttons[index] +} + // GetFormItemByLabel returns the first form element with the given label. If // no such element is found, nil is returned. Buttons are not searched and will // therefore not be returned. @@ -344,21 +364,7 @@ func (f *Form) GetRect() (int, int, int, int) { } if height == 0 { - maxHeight := make([]int, maxColumns) - if len(f.items) > 0 { - for i := 0; i < len(f.items); i++ { - column := f.itemsColumn[i] - _, _, _, h := f.items[i].GetRect() - maxHeight[column] += h + f.itemPadding - } - - for column := 0; column < maxColumns; column++ { - if height < maxHeight[column] { - height = maxHeight[column] - } - } - height -= f.itemPadding - } + height = f.getMaxHeightColumn() if len(f.buttons) > 0 { height += 1 + f.buttonsPaddingTop @@ -382,6 +388,28 @@ func (f *Form) getColoumnsCount() int { return maxColumns + 1 } +func (f *Form) getMaxHeightColumn() (height int) { + maxColumns := f.getColoumnsCount() + maxHeight := make([]int, maxColumns) + + if len(f.items) > 0 { + for i := 0; i < len(f.items); i++ { + column := f.itemsColumn[i] + _, _, _, h := f.items[i].GetRect() + maxHeight[column] += h + f.itemPadding + } + + for column := 0; column < maxColumns; column++ { + if height < maxHeight[column] { + height = maxHeight[column] + } + } + height -= f.itemPadding + } + + return +} + func (f *Form) getMaxWidthItems() (maxWidth, maxLabelWidth, maxFieldWidth []int) { maxColumns := f.getColoumnsCount() @@ -442,9 +470,9 @@ func (f *Form) Draw(screen tcell.Screen) { } switch f.align { - case tview.AlignCenter: + case AlignCenter: x += (boxWidth - maxWidth) / 2 - case tview.AlignRight: + case AlignRight: x += boxWidth - maxWidth } @@ -460,8 +488,8 @@ func (f *Form) Draw(screen tcell.Screen) { var focusedPosition struct{ x, y, width, height int } for index, item := range f.items { column := f.itemsColumn[index] - x = colX[column] - y = colY[column] + x := colX[column] + y := colY[column] _, _, leftPadding, rightPadding := item.GetBorderPadding() labelWidth := item.GetLabelWidth() + leftPadding fieldWidth := item.GetFieldWidth() + rightPadding @@ -523,13 +551,15 @@ func (f *Form) Draw(screen tcell.Screen) { colY[column] = y } + y = topLimit + f.getMaxHeightColumn() + // 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 + f.buttonsPaddingSeparate + buttonsWidth += w + f.buttonsIndent } buttonsWidth-- @@ -542,17 +572,16 @@ func (f *Form) Draw(screen tcell.Screen) { } // In vertical layouts, buttons always appear after an empty line. - if f.itemPadding == 0 { - y++ - } + // if f.itemPadding == 0 { + // y++ + // } } - if len(f.items) > 0 { - y -= f.itemPadding - } + if len(f.buttons) > 0 { y += f.buttonsPaddingTop } + // Calculate positions of buttons. for index, button := range f.buttons { space := rightLimit - x @@ -586,7 +615,7 @@ func (f *Form) Draw(screen tcell.Screen) { focusedPosition = positions[buttonIndex] } - x += buttonWidth + f.buttonsPaddingSeparate + x += buttonWidth + f.buttonsIndent } // Determine vertical offset based on the position of the focused item. @@ -651,10 +680,57 @@ func (f *Form) Focus(delegate func(p Primitive)) { if f.focusedElement < 0 || f.focusedElement >= len(f.items)+len(f.buttons) { f.focusedElement = 0 } - handler := func(key tcell.Key) { + + itemHandler := func(key tcell.Key) { switch key { case tcell.KeyTab, tcell.KeyEnter: + previous := f.focusedElement + for { + f.focusedElement++ + if f.focusedElement < len(f.items) && f.items[previous].GetID() == f.items[f.focusedElement].GetID() { + continue + } + break + } + 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) + } + } + } + + buttonHandler := func(key tcell.Key) { + switch key { + case tcell.KeyRight: + f.focusedElement-- + if f.focusedElement <= len(f.items)-1 { + f.focusedElement = len(f.items) + len(f.buttons) - 1 + } + f.Focus(delegate) + case tcell.KeyLeft: f.focusedElement++ + if f.focusedElement >= len(f.items)+len(f.buttons) { + f.focusedElement = len(f.items) + } + f.Focus(delegate) + case tcell.KeyTab: + for { + f.focusedElement++ + if f.focusedElement >= len(f.items) && f.focusedElement < len(f.items)+len(f.buttons) { + continue + } + break + } f.Focus(delegate) case tcell.KeyBacktab: f.focusedElement-- @@ -675,12 +751,13 @@ func (f *Form) Focus(delegate func(p Primitive)) { if f.focusedElement < len(f.items) { // We're selecting an item. item := f.items[f.focusedElement] - item.SetFinishedFunc(handler) + item.SetFinishedFunc(itemHandler) delegate(item) } else { // We're selecting a button. - button := f.buttons[f.focusedElement-len(f.items)] - button.SetBlurFunc(handler) + // fmt.Println(len(f.buttons) - 1 - (f.focusedElement - len(f.items))) + button := f.buttons[len(f.buttons)-1-(f.focusedElement-len(f.items))] + button.SetBlurFunc(buttonHandler) delegate(button) } } diff --git a/inputfield.go b/inputfield.go index 1a0633d..580a039 100644 --- a/inputfield.go +++ b/inputfield.go @@ -70,6 +70,8 @@ type InputField struct { // possible. fieldWidth int + cursorPosition int + // A character to mask entered text (useful for password fields). A value of 0 // disables masking. maskCharacter rune @@ -287,6 +289,11 @@ func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) FormItem { return i } +// GetFinishedFunc returns SetDoneFunc(). +func (i *InputField) GetFinishedFunc() func(key tcell.Key) { + return i.finished +} + // Draw draws this primitive onto the screen. func (i *InputField) Draw(screen tcell.Screen) { i.Box.Draw(screen) @@ -362,14 +369,27 @@ func (i *InputField) Draw(screen tcell.Screen) { Print(screen, i.placeholder, x, y, fieldWidth, AlignLeft, i.placeholderTextColor) } + textWidth := runewidth.StringWidth(text) // Draw entered text. if i.maskCharacter > 0 { text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text)) } + if i.cursorPosition < 0 { + i.cursorPosition = 0 + } + if i.cursorPosition > textWidth { + i.cursorPosition = textWidth + } + fieldWidth-- // We need one cell for the cursor. - if fieldWidth < runewidth.StringWidth(text) { + + if fieldWidth < textWidth { runes := []rune(text) - for pos := len(runes) - 1; pos >= 0; pos-- { + p := len(runes) + if i.cursorPosition-1 < textWidth-fieldWidth { + p = i.cursorPosition + fieldWidth + } + for pos := p - 1; pos >= 0; pos-- { ch := runes[pos] w := runewidth.RuneWidth(ch) if fieldWidth-w < 0 { @@ -413,14 +433,28 @@ func (i *InputField) setCursor(screen tcell.Screen) { y++ rightLimit -= 2 } - fieldWidth := runewidth.StringWidth(i.text) - if i.fieldWidth > 0 && fieldWidth > i.fieldWidth-1 { - fieldWidth = i.fieldWidth - 1 + + cursorIndent := i.cursorPosition + + textWidth := runewidth.StringWidth(i.text) + if textWidth > i.fieldWidth { + overflow := textWidth - (i.fieldWidth - 2) + if overflow > textWidth-cursorIndent { + + } + cursorIndent -= overflow + } + if cursorIndent < 0 { + cursorIndent = 0 + } + + if i.fieldWidth > 0 && cursorIndent > i.fieldWidth-1 { + cursorIndent = i.fieldWidth - 1 } if i.labelWidth > 0 { - x += i.labelWidth + 1 + fieldWidth + x += i.labelWidth + 1 + cursorIndent } else { - x += StringWidth(i.subLabel+i.label) + 1 + fieldWidth + x += StringWidth(i.subLabel+i.label) + 1 + cursorIndent } if x >= rightLimit { x = rightLimit - 1 @@ -442,12 +476,13 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p // Process key event. switch key := event.Key(); key { case tcell.KeyRune: // Regular character. - newText := i.text + string(event.Rune()) + newText := i.text[0:i.cursorPosition] + string(event.Rune()) + i.text[i.cursorPosition:] if i.accept != nil { if !i.accept(newText, event.Rune()) { break } } + i.cursorPosition++ i.text = newText case tcell.KeyCtrlU: // Delete all. i.text = "" @@ -455,11 +490,30 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p lastWord := regexp.MustCompile(`\s*\S+\s*$`) i.text = lastWord.ReplaceAllString(i.text, "") case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete last character. - if len(i.text) == 0 { + if len(i.text) == 0 || i.cursorPosition < 1 { + break + } + newText := i.text[0:i.cursorPosition-1] + i.text[i.cursorPosition:] + runes := []rune(i.text) + if i.accept != nil { + if !i.accept(newText, runes[i.cursorPosition-1]) { + break + } + } + i.cursorPosition-- + i.text = newText + case tcell.KeyDelete: + if len(i.text) == 0 || i.cursorPosition < 1 || len(i.text) == i.cursorPosition { break } + newText := i.text[0:i.cursorPosition] + i.text[i.cursorPosition+1:] runes := []rune(i.text) - i.text = string(runes[:len(runes)-1]) + if i.accept != nil { + if !i.accept(newText, runes[i.cursorPosition]) { + break + } + } + i.text = newText case tcell.KeyEnter, tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done. if i.done != nil { i.done(key) @@ -467,6 +521,14 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p if i.finished != nil { i.finished(key) } + case tcell.KeyLeft: + i.cursorPosition-- + case tcell.KeyRight: + i.cursorPosition++ + case tcell.KeyHome: + i.cursorPosition = 0 + case tcell.KeyEnd: + i.cursorPosition = runewidth.StringWidth(i.text) } }) } diff --git a/listbox.go b/listbox.go index 6352520..1e6c307 100644 --- a/listbox.go +++ b/listbox.go @@ -56,6 +56,7 @@ type ListBox struct { // The index of the currently selected item. currentItem int + offset int // Whether or not to show the secondary item texts. showSecondaryText bool @@ -84,6 +85,10 @@ type ListBox struct { // An optional function which is called when the user presses the Escape key. done func(tcell.Key) + + // A callback function set by the Form class and called when the user leaves + // this form item. + finished func(tcell.Key) } // NewListBox returns a new form. @@ -96,8 +101,8 @@ func NewListBox() *ListBox { selectedTextColor: Styles.PrimitiveBackgroundColor, selectedBackgroundColor: Styles.PrimaryTextColor, labelColor: Styles.SecondaryTextColor, - fieldBackgroundColor: Styles.ContrastBackgroundColor, - fieldTextColor: Styles.PrimaryTextColor, + fieldBackgroundColor: Styles.FieldBackgroundColor, + fieldTextColor: Styles.FieldTextColor, align: AlignLeft, labelFiller: " ", } @@ -109,7 +114,9 @@ func NewListBox() *ListBox { // SetCurrentItem sets the currently selected item by its index. This triggers // a "changed" event. func (l *ListBox) SetCurrentItem(index int) *ListBox { + _, _, _, height := l.GetInnerRect() l.currentItem = index + l.offset = l.currentItem - height/2 if l.currentItem < len(l.items) && l.changed != nil { item := l.items[l.currentItem] l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut) @@ -134,6 +141,14 @@ func (l *ListBox) GetCurrentItem() int { return l.currentItem } +// GetCurrentItemText returns the index of the currently selected list item. +func (l *ListBox) GetCurrentItemText() string { + if len(l.items) == 0 { + return "" + } + return l.items[l.currentItem].MainText +} + // SetMainTextColor sets the color of the items' main text. func (l *ListBox) SetMainTextColor(color tcell.Color) *ListBox { l.mainTextColor = color @@ -249,20 +264,21 @@ func (l *ListBox) Draw(screen tcell.Screen) { } // We want to keep the current selection in view. What is our offset? - var offset int if l.showSecondaryText { if l.currentItem >= height/2 { - offset = l.currentItem + 1 - (height / 2) + l.offset = l.currentItem + 1 - (height / 2) } } else { - if l.currentItem >= height { - offset = l.currentItem + 1 - height + if l.offset > 0 && l.offset > l.currentItem { + l.offset-- + } else if l.currentItem >= height && l.offset <= l.currentItem-height { + l.offset = l.currentItem + 1 - height } } // Draw the list items. for index, item := range l.items { - if index < offset { + if index < l.offset { continue } @@ -287,11 +303,14 @@ func (l *ListBox) Draw(screen tcell.Screen) { if fg == l.mainTextColor { fg = l.selectedTextColor } - style = style.Background(l.selectedBackgroundColor).Foreground(fg) + if l.focus.HasFocus() { + style = style.Background(l.fieldBackgroundColor).Foreground(fg) + } else { + style = style.Background(l.fieldTextColor).Foreground(l.fieldBackgroundColor).Underline(true) + } screen.SetContent(x+bx, y, m, c, style) } } - y++ if y >= bottomLimit { @@ -313,19 +332,49 @@ func (l *ListBox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pri previousItem := l.currentItem switch key := event.Key(); key { - case tcell.KeyTab, tcell.KeyDown, tcell.KeyRight: + case tcell.KeyTab, tcell.KeyBacktab: // We're done. + if l.done != nil { + l.done(key) + } + if l.finished != nil { + l.finished(key) + } + case tcell.KeyDown, tcell.KeyRight: l.currentItem++ - case tcell.KeyBacktab, tcell.KeyUp, tcell.KeyLeft: + if l.currentItem >= len(l.items) { + l.currentItem = 0 + l.offset = 0 + } + case tcell.KeyUp, tcell.KeyLeft: l.currentItem-- + if l.currentItem < 0 { + l.currentItem = len(l.items) - 1 + } case tcell.KeyHome: l.currentItem = 0 case tcell.KeyEnd: l.currentItem = len(l.items) - 1 case tcell.KeyPgDn: - l.currentItem += height + if l.currentItem < l.offset+height-1 { + l.currentItem = l.offset + height - 1 + } else { + l.currentItem += height + } + if l.currentItem >= len(l.items) { + l.currentItem = 0 + l.offset = 0 + } case tcell.KeyPgUp: - l.currentItem -= height + if l.currentItem > l.offset { + l.currentItem = l.offset + } else { + l.currentItem -= height + l.offset = l.currentItem + } case tcell.KeyEnter: + if len(l.items) == 0 { + break + } item := l.items[l.currentItem] if item.Selected != nil { item.Selected() @@ -337,6 +386,9 @@ func (l *ListBox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pri if l.done != nil { l.done(key) } + if l.finished != nil { + l.finished(key) + } case tcell.KeyRune: ch := event.Rune() if ch != ' ' { @@ -439,7 +491,13 @@ func (l *ListBox) GetLabel() string { // SetFinishedFunc calls SetDoneFunc(). func (l *ListBox) SetFinishedFunc(handler func(key tcell.Key)) FormItem { - return l.SetDoneFunc(handler) + l.finished = handler + return l +} + +// GetFinishedFunc returns SetDoneFunc(). +func (l *ListBox) GetFinishedFunc() func(key tcell.Key) { + return l.finished } // SetFormAttributes sets attributes shared by all form items. diff --git a/modal.go b/modal.go index 68757f5..4ee764c 100644 --- a/modal.go +++ b/modal.go @@ -40,9 +40,10 @@ func NewModal() *Modal { } m.form = NewForm(). SetButtonsAlign(AlignCenter). - SetButtonBackgroundColor(Styles.PrimitiveBackgroundColor). - SetButtonTextColor(Styles.PrimaryTextColor) - m.form.SetBackgroundColor(Styles.ContrastBackgroundColor).SetBorderPadding(0, 0, 0, 0) + SetButtonBackgroundColor(Styles.ButtonBackgroundColor). + SetButtonTextColor(Styles.ButtonTextColor) + m.form.SetButtonPadding(0) + m.form.SetBackgroundColor(Styles.ModalBackgroundColor).SetBorderPadding(0, 0, 0, 0) m.frame = NewFrame(m.form).SetBorders(0, 0, 1, 0, 0, 0) m.frame.SetBorder(true). SetBackgroundColor(Styles.ModalBackgroundColor). @@ -74,6 +75,12 @@ func (m *Modal) SetText(text string) *Modal { return m } +// SetTip is hard code, which will need to fix +func (m *Modal) SetTip() *Modal { + m.form.AddFormItem(NewInputField()) + return m +} + // AddButtons adds buttons to the window. There must be at least one button and // a "done" handler so the window can be closed again. func (m *Modal) AddButtons(labels []string) *Modal { diff --git a/pages.go b/pages.go index 9af7fe9..e0d5774 100644 --- a/pages.go +++ b/pages.go @@ -227,6 +227,7 @@ func (p *Pages) Focus(delegate func(p Primitive)) { topItem = page.Item } } + if topItem != nil { delegate(topItem) } diff --git a/radiobutton.go b/radiobutton.go index 9592904..9b15b51 100644 --- a/radiobutton.go +++ b/radiobutton.go @@ -76,11 +76,17 @@ type RadioButtons struct { labelWidth int + joinElements []*RadioButtons + currentElement int // An optional function which is called when the user indicated that they // are done selecting options. The key which was pressed is provided (tab, // shift-tab, or escape). done func(tcell.Key) + // A callback function set by the Form class and called when the user leaves + // this form item. + finished func(tcell.Key) + // If set to true, instead of position items and buttons from top to bottom, // they are positioned from left to right. horizontal bool @@ -90,6 +96,8 @@ type RadioButtons struct { // An optional function which is called when the user has navigated to a list // item. changed func(*RadioOption) + + inputHandler func() func(event *tcell.EventKey, setFocus func(p Primitive)) } // NewRadioButtons returns a new radio button primitive. @@ -108,10 +116,23 @@ func NewRadioButtons() *RadioButtons { } r.focus = r + r.joinElements = append(r.joinElements, r) + r.inputHandler = r.defaultInputHandler return r } +// Join combines two element the same type in one, for navigation +func (r *RadioButtons) Join(elements ...*RadioButtons) *RadioButtons { + for i := 0; i < len(elements); i++ { + elements[i].inputHandler = r.inputHandler + elements[i].id = r.id + elements[i].currentOption = -1 + } + r.joinElements = append(r.joinElements, elements...) + return r +} + // SetOptions replaces all current options with the ones provided func (r *RadioButtons) SetOptions(options []*RadioOption) *RadioButtons { r.options = options @@ -133,28 +154,81 @@ func (r *RadioButtons) SetAlign(align int) *RadioButtons { return r } +// Focus is called when this primitive receives focus. +func (r *RadioButtons) Focus(delegate func(p Primitive)) { + if r != r.joinElements[r.currentElement] { + delegate(r.joinElements[r.currentElement]) + return + } + r.hasFocus = true +} + // InputHandler returns the handler for this primitive. func (r *RadioButtons) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { + if r.inputHandler != nil { + return r.inputHandler() + } + return r.Box.InputHandler() +} + +// InputHandler returns the handler for this primitive. +func (r *RadioButtons) defaultInputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return r.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { + parent := r + r = parent.joinElements[parent.currentElement] + switch key := event.Key(); key { case tcell.KeyUp, tcell.KeyLeft: r.currentOption-- if r.currentOption < 0 { + if len(parent.joinElements) > 1 { + parent.currentElement-- + if parent.currentElement < 0 { + parent.currentElement = len(parent.joinElements) - 1 + } + nextElement := parent.joinElements[parent.currentElement] + setFocus(nextElement) + nextElement.currentOption = len(nextElement.options) - 1 + break + } r.currentOption = len(r.options) - 1 - int(math.Mod(float64(len(r.options)), float64(r.currentOption))) } + if r.changed != nil { + r.changed(r.options[r.currentOption]) + } case tcell.KeyDown, tcell.KeyRight: r.currentOption++ if r.currentOption >= len(r.options) { + if len(parent.joinElements) > 1 { + parent.currentElement++ + if parent.currentElement >= len(parent.joinElements) { + parent.currentElement = 0 + } + nextElement := parent.joinElements[parent.currentElement] + setFocus(nextElement) + nextElement.currentOption = 0 + break + } r.currentOption = int(math.Mod(float64(len(r.options)), float64(r.currentOption))) } + if r.changed != nil { + r.changed(r.options[r.currentOption]) + } case tcell.KeyEnter, tcell.KeyRune: // We're done. - r.selectedOption = r.currentOption + for index, element := range parent.joinElements { + if parent.currentElement != index { + element.currentOption = -1 + } + } if r.changed != nil { r.changed(r.options[r.currentOption]) } - case tcell.KeyTab, tcell.KeyBacktab: // We're done. - if r.done != nil { - r.done(key) + case tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done. + if parent.done != nil { + parent.done(key) + } + if parent.finished != nil { + parent.finished(key) } } }) @@ -261,7 +335,13 @@ func (r *RadioButtons) SetDoneFunc(handler func(key tcell.Key)) *RadioButtons { // SetFinishedFunc calls SetDoneFunc(). func (r *RadioButtons) SetFinishedFunc(handler func(key tcell.Key)) FormItem { - return r.SetDoneFunc(handler) + r.finished = handler + return r +} + +// GetFinishedFunc returns SetDoneFunc(). +func (r *RadioButtons) GetFinishedFunc() func(key tcell.Key) { + return r.finished } // SetLabel sets the text to be displayed before the input area. @@ -328,9 +408,9 @@ func (r *RadioButtons) SetHorizontal(horizontal bool) *RadioButtons { func (r *RadioButtons) SetCurrentOptionByName(name string) *RadioButtons { for i := 0; i < len(r.options); i++ { if r.options[i].Name == name { - r.selectedOption = i + r.currentOption = i if r.changed != nil { - r.changed(r.options[r.selectedOption]) + r.changed(r.options[r.currentOption]) } break } @@ -341,9 +421,9 @@ func (r *RadioButtons) SetCurrentOptionByName(name string) *RadioButtons { // SetCurrentOption sets the index of the currently selected option. This may // be a negative value to indicate that no option is currently selected. func (r *RadioButtons) SetCurrentOption(index int) *RadioButtons { - r.selectedOption = index + r.currentOption = index if r.changed != nil { - r.changed(r.options[r.selectedOption]) + r.changed(r.options[r.currentOption]) } return r } @@ -351,12 +431,20 @@ func (r *RadioButtons) SetCurrentOption(index int) *RadioButtons { // GetCurrentOption returns the index of the currently selected option as well // as its text. If no option was selected, -1 and an empty string is returned. func (r *RadioButtons) GetCurrentOption() *RadioOption { - return r.options[r.selectedOption] + r = r.joinElements[r.currentElement] + if len(r.options) > r.currentOption { + return r.options[r.currentOption] + } + return nil } // GetCurrentOptionName returns the name of the currently selected option. func (r *RadioButtons) GetCurrentOptionName() string { - return r.options[r.selectedOption].Name + r = r.joinElements[r.currentElement] + if len(r.options) > r.currentOption { + return r.options[r.currentOption].Name + } + return "" } // SetChangedFunc sets the function which is called when the user navigates to @@ -420,7 +508,7 @@ func (r *RadioButtons) Draw(screen tcell.Screen) { break } radioButton := Styles.GraphicsRadioUnchecked // Unchecked. - if index == r.selectedOption { + if index == r.currentOption { radioButton = Styles.GraphicsRadioChecked // Checked. } diff --git a/table.go b/table.go index 4fdb76e..9ddcfe4 100644 --- a/table.go +++ b/table.go @@ -411,7 +411,14 @@ func (t *Table) GetColumnCount() int { // GetPageCount returns quantity of pages func (t *Table) GetPageCount() int { - return t.GetRowCount() / t.visibleRows + _, _, _, height := t.GetInnerRect() + + visibleRows := height + if t.borders { + visibleRows = height / 2 + } + + return t.GetRowCount() / visibleRows } // ScrollToBeginning scrolls the table to the beginning to that the top left diff --git a/textview.go b/textview.go index 7370483..ced400b 100644 --- a/textview.go +++ b/textview.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "regexp" + "strings" "sync" "unicode/utf8" @@ -85,6 +86,8 @@ type TextView struct { sync.Mutex *Box + maxWidth, maxHeight, minWidth, minHeight int + // The text buffer. buffer []string @@ -160,11 +163,15 @@ type TextView struct { done func(tcell.Key) scrollEnded func(screen tcell.Screen) + + // A callback function set by the Form class and called when the user leaves + // this form item. + finished func(tcell.Key) } // NewTextView returns a new text view. func NewTextView() *TextView { - return &TextView{ + textview := &TextView{ Box: NewBox(), highlights: make(map[string]struct{}), lineOffset: -1, @@ -174,6 +181,23 @@ func NewTextView() *TextView { textColor: Styles.PrimaryTextColor, dynamicColors: false, } + textview.height = 0 + + return textview +} + +// SetMinSize sets min size of the box +func (t *TextView) SetMinSize(width, height int) *TextView { + t.minWidth = width + t.minHeight = height + return t +} + +// SetMaxSize sets max size of the box +func (t *TextView) SetMaxSize(width, height int) *TextView { + t.maxWidth = width + t.maxHeight = height + return t } // SetScrollable sets the flag that decides whether or not the text view is @@ -648,6 +672,31 @@ func (t *TextView) reindexBuffer(width int) { } } +// GetInnerRect returns the position of the inner rectangle (x, y, width, +// height), without the border and without any padding. +func (t *TextView) GetInnerRect() (int, int, int, int) { + x, y, boxWidth, boxHeight := t.Box.GetInnerRect() + + width := boxWidth + switch { + case t.maxWidth > 0 && width > t.maxWidth: + width = t.maxWidth + x += (boxWidth - width) / 2 + case t.minWidth > 0 && width < t.minWidth: + width = t.minWidth + } + + height := boxHeight + switch { + case t.maxHeight > 0 && height > t.maxHeight: + height = t.maxHeight + case t.minHeight > 0 && height < t.minHeight: + height = t.minHeight + } + + return x, y, width, height +} + // Draw draws this primitive onto the screen. func (t *TextView) Draw(screen tcell.Screen) { t.Lock() @@ -844,8 +893,8 @@ func (t *TextView) Draw(screen tcell.Screen) { if highlighted { fg, bg, _ := style.Decompose() if bg == tcell.ColorDefault { - r, g, b := fg.RGB() - c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255} + r, g, t := fg.RGB() + c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(t) / 255} _, _, li := c.Hcl() if li < .5 { bg = tcell.ColorWhite @@ -878,11 +927,13 @@ func (t *TextView) Draw(screen tcell.Screen) { func (t *TextView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { key := event.Key() - if key == tcell.KeyEscape || key == tcell.KeyEnter || key == tcell.KeyTab || key == tcell.KeyBacktab { if t.done != nil { t.done(key) } + if t.finished != nil { + t.finished(key) + } return } @@ -934,3 +985,58 @@ func (t *TextView) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr } }) } + +// GetLabel returns the text to be displayed before the input area. +func (t *TextView) GetLabel() string { + return strings.Join(t.buffer, " ") +} + +// GetLabelWidth returns label width. +func (t *TextView) GetLabelWidth() int { + return StringWidth(strings.Replace(strings.Join(t.buffer, " "), "%s", "", -1)) +} + +// SetFieldAlign sets the input alignment within the radiobutton box. This must be +// either AlignLeft, AlignCenter, or AlignRight. +func (t *TextView) SetFieldAlign(align int) FormItem { + t.align = align + return t +} + +// GetFieldWidth returns this primitive's field width. +func (t *TextView) GetFieldWidth() int { + return 0 +} + +// GetFieldAlign returns the input alignment within the radiobutton box. +func (t *TextView) GetFieldAlign() (align int) { + return t.align +} + +// SetFormAttributes sets attributes shared by all form items. +func (t *TextView) SetFormAttributes(labelWidth, fieldWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { + t.height = 1 + t.textColor = labelColor + t.backgroundColor = bgColor + return t +} + +// SetFinishedFunc sets a callback invoked when the user leaves this form item. +func (t *TextView) SetFinishedFunc(handler func(key tcell.Key)) FormItem { + t.finished = handler + return t +} + +// GetFinishedFunc returns SetDoneFunc(). +func (t *TextView) GetFinishedFunc() func(key tcell.Key) { + return t.finished +} + +// Focus is called when this primitive receives focus. +func (t *TextView) Focus(delegate func(p Primitive)) { + if t.finished != nil { + t.finished(tcell.KeyTAB) + return + } + t.hasFocus = true +}