From 47b3275db48de684be18375672ea5c5671cad6e2 Mon Sep 17 00:00:00 2001 From: Oliver <480930+rivo@users.noreply.github.com> Date: Mon, 20 Mar 2023 07:26:01 +0100 Subject: [PATCH] Form elements can now also be disabled. Resolves #192 --- button.go | 41 ++++++++++++++++++++++++++++++++++++++++- checkbox.go | 40 ++++++++++++++++++++++++++++++++++++++-- dropdown.go | 32 +++++++++++++++++++++++++++++++- form.go | 48 ++++++++++++++++++++++++++++++++++-------------- image.go | 5 +++++ inputfield.go | 35 +++++++++++++++++++++++++++++++++++ textarea.go | 40 +++++++++++++++++++++++++++++++++++++++- textview.go | 10 ++++++++-- 8 files changed, 230 insertions(+), 21 deletions(-) diff --git a/button.go b/button.go index b54c49e..4363cea 100644 --- a/button.go +++ b/button.go @@ -10,6 +10,9 @@ import ( type Button struct { *Box + // If set to true, the button cannot be activated. + disabled bool + // The text to be displayed inside the button. text string @@ -19,6 +22,9 @@ type Button struct { // The button's style (when activated). activatedStyle tcell.Style + // The button's style (when disabled). + disabledStyle tcell.Style + // An optional function which is called when the button was selected. selected func() @@ -37,6 +43,7 @@ func NewButton(label string) *Button { text: label, style: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.PrimaryTextColor), activatedStyle: tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.InverseTextColor), + disabledStyle: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.ContrastSecondaryTextColor), } } @@ -83,6 +90,27 @@ func (b *Button) SetActivatedStyle(style tcell.Style) *Button { return b } +// SetDisabledStyle sets the style of the button used when it is disabled. +func (b *Button) SetDisabledStyle(style tcell.Style) *Button { + b.disabledStyle = style + return b +} + +// SetDisabled sets whether or not the button is disabled. Disabled buttons +// cannot be activated. +// +// If the button is part of a form, you should set focus to the form itself +// after calling this function to set focus to the next non-disabled form item. +func (b *Button) SetDisabled(disabled bool) *Button { + b.disabled = disabled + return b +} + +// IsDisabled returns whether or not the button is disabled. +func (b *Button) IsDisabled() bool { + return b.disabled +} + // SetSelectedFunc sets a handler which is called when the button was selected. func (b *Button) SetSelectedFunc(handler func()) *Button { b.selected = handler @@ -105,8 +133,11 @@ func (b *Button) SetExitFunc(handler func(key tcell.Key)) *Button { func (b *Button) Draw(screen tcell.Screen) { // Draw the box. style := b.style + if b.disabled { + style = b.disabledStyle + } _, backgroundColor, _ := style.Decompose() - if b.HasFocus() { + if b.HasFocus() && !b.disabled { style = b.activatedStyle _, backgroundColor, _ = style.Decompose() @@ -131,6 +162,10 @@ func (b *Button) Draw(screen tcell.Screen) { // InputHandler returns the handler for this primitive. func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return b.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { + if b.disabled { + return + } + // Process key event. switch key := event.Key(); key { case tcell.KeyEnter: // Selected. @@ -148,6 +183,10 @@ func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Prim // MouseHandler returns the mouse handler for this primitive. func (b *Button) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return b.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + if b.disabled { + return false, nil + } + if !b.InRect(event.Position()) { return false, nil } diff --git a/checkbox.go b/checkbox.go index 59475eb..a48a075 100644 --- a/checkbox.go +++ b/checkbox.go @@ -14,6 +14,9 @@ import ( type Checkbox struct { *Box + // Whether or not this checkbox is disabled/read-only. + disabled bool + // Whether or not this box is checked. checked bool @@ -135,6 +138,15 @@ func (c *Checkbox) GetFieldHeight() int { return 1 } +// SetDisabled sets whether or not the item is disabled / read-only. +func (c *Checkbox) SetDisabled(disabled bool) FormItem { + c.disabled = disabled + if c.finished != nil { + c.finished(-1) + } + return c +} + // SetChangedFunc sets a handler which is called when the checked state of this // checkbox was changed by the user. The handler function receives the new // state. @@ -161,6 +173,18 @@ func (c *Checkbox) SetFinishedFunc(handler func(key tcell.Key)) FormItem { return c } +// Focus is called when this primitive receives focus. +func (c *Checkbox) Focus(delegate func(p Primitive)) { + // If we're part of a form and this item is disabled, there's nothing the + // user can do here so we're finished. + if c.finished != nil && c.disabled { + c.finished(-1) + return + } + + c.Box.Focus(delegate) +} + // Draw draws this primitive onto the screen. func (c *Checkbox) Draw(screen tcell.Screen) { c.Box.DrawForSubclass(screen, c) @@ -186,9 +210,13 @@ func (c *Checkbox) Draw(screen tcell.Screen) { } // Draw checkbox. - fieldStyle := tcell.StyleDefault.Background(c.fieldBackgroundColor).Foreground(c.fieldTextColor) + fieldBackgroundColor := c.fieldBackgroundColor + if c.disabled { + fieldBackgroundColor = c.backgroundColor + } + fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor).Foreground(c.fieldTextColor) if c.HasFocus() { - fieldStyle = fieldStyle.Background(c.fieldTextColor).Foreground(c.fieldBackgroundColor) + fieldStyle = fieldStyle.Background(c.fieldTextColor).Foreground(fieldBackgroundColor) } checkboxWidth := uniseg.StringWidth(c.checkedString) checkedString := c.checkedString @@ -201,6 +229,10 @@ func (c *Checkbox) Draw(screen tcell.Screen) { // InputHandler returns the handler for this primitive. func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return c.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { + if c.disabled { + return + } + // Process key event. switch key := event.Key(); key { case tcell.KeyRune, tcell.KeyEnter: // Check. @@ -225,6 +257,10 @@ func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr // MouseHandler returns the mouse handler for this primitive. func (c *Checkbox) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return c.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + if c.disabled { + return false, nil + } + x, y := event.Position() _, rectY, _, _ := c.GetInnerRect() if !c.InRect(x, y) { diff --git a/dropdown.go b/dropdown.go index c65d291..feaf1bb 100644 --- a/dropdown.go +++ b/dropdown.go @@ -20,6 +20,9 @@ type dropDownOption struct { type DropDown struct { *Box + // Whether or not this drop-down is disabled/read-only. + disabled bool + // The options from which the user can choose. options []*dropDownOption @@ -249,6 +252,15 @@ func (d *DropDown) GetFieldHeight() int { return 1 } +// SetDisabled sets whether or not the item is disabled / read-only. +func (d *DropDown) SetDisabled(disabled bool) FormItem { + d.disabled = disabled + if d.finished != nil { + d.finished(-1) + } + return d +} + // AddOption adds a new selectable option to this drop-down. The "selected" // callback is called when this option was selected. It may be nil. func (d *DropDown) AddOption(text string, selected func()) *DropDown { @@ -371,6 +383,9 @@ func (d *DropDown) Draw(screen tcell.Screen) { if d.HasFocus() && !d.open { fieldStyle = fieldStyle.Background(d.fieldTextColor) } + if d.disabled { + fieldStyle = fieldStyle.Background(d.backgroundColor) + } for index := 0; index < fieldWidth; index++ { screen.SetContent(x+index, y, ' ', nil, fieldStyle) } @@ -393,7 +408,7 @@ func (d *DropDown) Draw(screen tcell.Screen) { text = d.currentOptionPrefix + d.options[d.currentOption].Text + d.currentOptionSuffix } // Just show the current selection. - if d.HasFocus() && !d.open { + if d.HasFocus() && !d.open && !d.disabled { color = d.fieldBackgroundColor } Print(screen, text, x, y, fieldWidth, AlignLeft, color) @@ -432,6 +447,10 @@ func (d *DropDown) Draw(screen tcell.Screen) { // InputHandler returns the handler for this primitive. func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { + if d.disabled { + return + } + // If the list has focus, let it process its own key events. if d.list.HasFocus() { if handler := d.list.InputHandler(); handler != nil { @@ -534,6 +553,13 @@ func (d *DropDown) closeList(setFocus func(Primitive)) { // Focus is called by the application when the primitive receives focus. func (d *DropDown) Focus(delegate func(p Primitive)) { + // If we're part of a form and this item is disabled, there's nothing the + // user can do here so we're finished. + if d.finished != nil && d.disabled { + d.finished(-1) + return + } + if d.open { delegate(d.list) } else { @@ -552,6 +578,10 @@ func (d *DropDown) HasFocus() bool { // MouseHandler returns the mouse handler for this primitive. func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + if d.disabled { + return false, nil + } + // Was the mouse event in the drop-down box itself (or on its label)? x, y := event.Position() rectX, rectY, rectWidth, _ := d.GetInnerRect() diff --git a/form.go b/form.go index 261acad..ee96d6d 100644 --- a/form.go +++ b/form.go @@ -45,6 +45,10 @@ type FormItem interface { // value, indicating that the action for the last known key should be // repeated. SetFinishedFunc(handler func(key tcell.Key)) FormItem + + // SetDisabled sets whether or not the item is disabled / read-only. A form + // must have at least one item that is not disabled. + SetDisabled(disabled bool) FormItem } // Form allows you to combine multiple one-line form elements into a vertical @@ -92,6 +96,9 @@ type Form struct { // The style of the buttons when they are focused. buttonActivatedStyle tcell.Style + // The style of the buttons when they are disabled. + buttonDisabledStyle tcell.Style + // The last (valid) key that wsa sent to a "finished" handler or -1 if no // such key is known yet. lastFinishedKey tcell.Key @@ -662,12 +669,6 @@ func (f *Form) Draw(screen tcell.Screen) { // 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 { - f.Box.Focus(delegate) - return - } - f.hasFocus = false - // 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 @@ -702,22 +703,41 @@ func (f *Form) Focus(delegate func(p Primitive)) { } } - // Set the handler for all items and buttons. + // Track whether a form item has focus. + var itemFocused bool + f.hasFocus = false + + // Set the handler and focus for all items and buttons. + for index, button := range f.buttons { + button.SetExitFunc(handler) + if f.focusedElement == index+len(f.items) { + if button.IsDisabled() { + f.focusedElement++ + if f.focusedElement >= len(f.items)+len(f.buttons) { + f.focusedElement = 0 + } + continue + } + + itemFocused = true + func(b *Button) { // Wrapping might not be necessary anymore in future Go versions. + defer delegate(b) + }(button) + } + } for index, item := range f.items { item.SetFinishedFunc(handler) if f.focusedElement == index { + itemFocused = true func(i FormItem) { // Wrapping might not be necessary anymore in future Go versions. defer delegate(i) }(item) } } - for index, button := range f.buttons { - button.SetExitFunc(handler) - if f.focusedElement == index+len(f.items) { - func(b *Button) { // Wrapping might not be necessary anymore in future Go versions. - defer delegate(b) - }(button) - } + + // If no item was focused, focus the form itself. + if !itemFocused { + f.Box.Focus(delegate) } } diff --git a/image.go b/image.go index 603180c..1d777ee 100644 --- a/image.go +++ b/image.go @@ -251,6 +251,11 @@ func (i *Image) GetFieldHeight() int { return i.height } +// SetDisabled sets whether or not the item is disabled / read-only. +func (i *Image) SetDisabled(disabled bool) FormItem { + return i // Images are always read-only. +} + // SetFormAttributes sets attributes shared by all form items. func (i *Image) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { i.labelWidth = labelWidth diff --git a/inputfield.go b/inputfield.go index 5ee009d..75e180b 100644 --- a/inputfield.go +++ b/inputfield.go @@ -49,6 +49,9 @@ const ( type InputField struct { *Box + // Whether or not this input field is disabled/read-only. + disabled bool + // The text that was entered. text string @@ -277,6 +280,15 @@ func (i *InputField) GetFieldHeight() int { return 1 } +// SetDisabled sets whether or not the item is disabled / read-only. +func (i *InputField) SetDisabled(disabled bool) FormItem { + i.disabled = disabled + if i.finished != nil { + i.finished(-1) + } + return i +} + // SetMaskCharacter sets a character that masks user input on a screen. A value // of 0 disables masking. func (i *InputField) SetMaskCharacter(mask rune) *InputField { @@ -403,6 +415,18 @@ func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) FormItem { return i } +// Focus is called when this primitive receives focus. +func (i *InputField) Focus(delegate func(p Primitive)) { + // If we're part of a form and this item is disabled, there's nothing the + // user can do here so we're finished. + if i.finished != nil && i.disabled { + i.finished(-1) + return + } + + i.Box.Focus(delegate) +} + // Blur is called when this primitive loses focus. func (i *InputField) Blur() { i.Box.Blur() @@ -450,6 +474,9 @@ func (i *InputField) Draw(screen tcell.Screen) { if rightLimit-x < fieldWidth { fieldWidth = rightLimit - x } + if i.disabled { + inputStyle = inputStyle.Background(i.backgroundColor) + } if inputBg != tcell.ColorDefault { for index := 0; index < fieldWidth; index++ { screen.SetContent(x+index, y, ' ', nil, inputStyle) @@ -552,6 +579,10 @@ func (i *InputField) Draw(screen tcell.Screen) { // InputHandler returns the handler for this primitive. func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { + if i.disabled { + return + } + // Trigger changed events. currentText := i.text defer func() { @@ -733,6 +764,10 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p // 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) { + if i.disabled { + return false, nil + } + currentText := i.GetText() defer func() { if i.GetText() != currentText { diff --git a/textarea.go b/textarea.go index 4a936c1..7a59db9 100644 --- a/textarea.go +++ b/textarea.go @@ -192,6 +192,9 @@ type textAreaUndoItem struct { type TextArea struct { *Box + // Whether or not this text area is disabled/read-only. + disabled bool + // The size of the text area. If set to 0, the text area will use the entire // available space. width, height int @@ -749,6 +752,15 @@ func (t *TextArea) GetFieldHeight() int { return t.height } +// SetDisabled sets whether or not the item is disabled / read-only. +func (t *TextArea) SetDisabled(disabled bool) FormItem { + t.disabled = disabled + if t.finished != nil { + t.finished(-1) + } + return t +} + // SetMaxLength sets the maximum number of bytes allowed in the text area. A // value of 0 means there is no limit. If the text area currently contains more // bytes than this, it may violate this constraint. @@ -848,6 +860,18 @@ func (t *TextArea) SetFinishedFunc(handler func(key tcell.Key)) FormItem { return t } +// Focus is called when this primitive receives focus. +func (t *TextArea) Focus(delegate func(p Primitive)) { + // If we're part of a form and this item is disabled, there's nothing the + // user can do here so we're finished. + if t.finished != nil && t.disabled { + t.finished(-1) + return + } + + t.Box.Focus(delegate) +} + // SetFormAttributes sets attributes shared by all form items. func (t *TextArea) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { t.labelWidth = labelWidth @@ -1066,7 +1090,10 @@ func (t *TextArea) Draw(screen tcell.Screen) { // Draw the input element if necessary. _, bg, _ := t.textStyle.Decompose() - if bg != t.GetBackgroundColor() { + if t.disabled { + bg = t.backgroundColor + } + if bg != t.backgroundColor { for row := 0; row < height; row++ { for column := 0; column < width; column++ { screen.SetContent(x+column, y+row, ' ', nil, t.textStyle) @@ -1144,6 +1171,9 @@ func (t *TextArea) Draw(screen tcell.Screen) { fromRow > line || fromRow == line && fromColumn > posX { style = t.textStyle + if t.disabled { + style = style.Background(t.backgroundColor) + } } // Draw character. @@ -1810,6 +1840,10 @@ func (t *TextArea) getSelectedText() string { // InputHandler returns the handler for this primitive. func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { + if t.disabled { + return + } + // All actions except a few specific ones are "other" actions. newLastAction := taActionOther defer func() { @@ -2197,6 +2231,10 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr // MouseHandler returns the mouse handler for this primitive. func (t *TextArea) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { + if t.disabled { + return false, nil + } + x, y := event.Position() rectX, rectY, _, _ := t.GetInnerRect() if !t.InRect(x, y) { diff --git a/textview.go b/textview.go index 16bec9b..9f6508c 100644 --- a/textview.go +++ b/textview.go @@ -309,6 +309,11 @@ func (t *TextView) GetFieldHeight() int { return t.height } +// SetDisabled sets whether or not the item is disabled / read-only. +func (t *TextView) SetDisabled(disabled bool) FormItem { + return t // Text views are always read-only. +} + // SetScrollable sets the flag that decides whether or not the text view is // scrollable. If true, text is kept in a buffer and can be navigated. If false, // the last line will always be visible. @@ -760,7 +765,6 @@ func (t *TextView) Focus(delegate func(p Primitive)) { // Implemented here with locking because this is used by layout primitives. t.Lock() defer t.Unlock() - t.Box.Focus(delegate) // But if we're part of a form and not scrollable, there's nothing the user // can do here so we're finished. @@ -768,6 +772,8 @@ func (t *TextView) Focus(delegate func(p Primitive)) { t.finished(-1) return } + + t.Box.Focus(delegate) } // HasFocus returns whether or not this primitive has focus. @@ -1148,7 +1154,7 @@ func (t *TextView) Draw(screen tcell.Screen) { // Draw the text element if necessary. _, bg, _ := t.textStyle.Decompose() - if bg != t.GetBackgroundColor() { + if bg != t.backgroundColor { for row := 0; row < height; row++ { for column := 0; column < width; column++ { screen.SetContent(x+column, y+row, ' ', nil, t.textStyle)