From 78031b278f3303f1ef0343adf974e11184f193bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20P=2E=20Berrang=C3=A9?= Date: Tue, 26 May 2020 18:34:59 +0100 Subject: [PATCH] Add support for read only form items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a form item is readonly, any input events or mouse events that would cause its value to change are ignored. Read only items are rendered when text color that matches the label color to distinguish them from items that accept input. Signed-off-by: Daniel P. Berrangé --- checkbox.go | 56 +++++++++++++++++++++++++++++----- dropdown.go | 63 ++++++++++++++++++++++++++++++-------- inputfield.go | 84 +++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 173 insertions(+), 30 deletions(-) diff --git a/checkbox.go b/checkbox.go index 7c4b505..3ec8680 100644 --- a/checkbox.go +++ b/checkbox.go @@ -30,6 +30,12 @@ type Checkbox struct { // The text color of the input area. fieldTextColor tcell.Color + // The background color of the input area when readonly. + fieldBackgroundReadOnlyColor tcell.Color + + // The text color of the input area when readonly. + fieldTextReadOnlyColor tcell.Color + // An optional function which is called when the user changes the checked // state of this checkbox. changed func(checked bool) @@ -42,15 +48,21 @@ type Checkbox struct { // A callback function set by the Form class and called when the user leaves // this form item. finished func(tcell.Key) + + // If set the item does not respond to form input/mouse events + // that would change its contents + readonly bool } // NewCheckbox returns a new input field. func NewCheckbox() *Checkbox { return &Checkbox{ - Box: NewBox(), - labelColor: Styles.SecondaryTextColor, - fieldBackgroundColor: Styles.ContrastBackgroundColor, - fieldTextColor: Styles.PrimaryTextColor, + Box: NewBox(), + labelColor: Styles.SecondaryTextColor, + fieldBackgroundColor: Styles.ContrastBackgroundColor, + fieldTextColor: Styles.PrimaryTextColor, + fieldTextReadOnlyColor: Styles.SecondaryTextColor, + fieldBackgroundReadOnlyColor: Styles.ContrastBackgroundColor, } } @@ -65,6 +77,13 @@ func (c *Checkbox) IsChecked() bool { return c.checked } +// SetReadOnly sets whether the item responds to input +// or mouse events that would change its contents +func (c *Checkbox) SetReadOnly(readonly bool) *Checkbox { + c.readonly = readonly + return c +} + // SetLabel sets the text to be displayed before the input area. func (c *Checkbox) SetLabel(label string) *Checkbox { c.label = label @@ -101,6 +120,18 @@ func (c *Checkbox) SetFieldTextColor(color tcell.Color) *Checkbox { return c } +// SetFieldBackgroundReadOnlyColor sets the background color of the input area when readonly. +func (c *Checkbox) SetFieldBackgroundReadOnlyColor(color tcell.Color) *Checkbox { + c.fieldBackgroundReadOnlyColor = color + return c +} + +// SetFieldTextReadOnlyColor sets the text color of the input area when readonly. +func (c *Checkbox) SetFieldTextReadOnlyColor(color tcell.Color) *Checkbox { + c.fieldTextReadOnlyColor = color + return c +} + // SetFormAttributes sets attributes shared by all form items. func (c *Checkbox) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { c.labelWidth = labelWidth @@ -108,6 +139,8 @@ func (c *Checkbox) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldT c.backgroundColor = bgColor c.fieldTextColor = fieldTextColor c.fieldBackgroundColor = fieldBgColor + c.fieldTextReadOnlyColor = labelColor + c.fieldBackgroundReadOnlyColor = fieldBgColor return c } @@ -168,7 +201,9 @@ func (c *Checkbox) Draw(screen tcell.Screen) { // Draw checkbox. fieldStyle := tcell.StyleDefault.Background(c.fieldBackgroundColor).Foreground(c.fieldTextColor) - if c.focus.HasFocus() { + if c.readonly { + fieldStyle = tcell.StyleDefault.Background(c.fieldBackgroundReadOnlyColor).Foreground(c.fieldTextReadOnlyColor) + } else if c.focus.HasFocus() { fieldStyle = fieldStyle.Background(c.fieldTextColor).Foreground(c.fieldBackgroundColor) } checkedRune := 'X' @@ -184,6 +219,9 @@ func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr // Process key event. switch key := event.Key(); key { case tcell.KeyRune, tcell.KeyEnter: // Check. + if c.readonly { + break + } if key == tcell.KeyRune && event.Rune() != ' ' { break } @@ -214,9 +252,11 @@ func (c *Checkbox) MouseHandler() func(action MouseAction, event *tcell.EventMou // Process mouse event. if action == MouseLeftClick && y == rectY { setFocus(c) - c.checked = !c.checked - if c.changed != nil { - c.changed(c.checked) + if !c.readonly { + c.checked = !c.checked + if c.changed != nil { + c.changed(c.checked) + } } consumed = true } diff --git a/dropdown.go b/dropdown.go index 89d50c1..eaa5a25 100644 --- a/dropdown.go +++ b/dropdown.go @@ -56,6 +56,12 @@ type DropDown struct { // The text color of the input area. fieldTextColor tcell.Color + // The background color of the input area when readonly. + fieldBackgroundReadOnlyColor tcell.Color + + // The text color of the input area when readonly. + fieldTextReadOnlyColor tcell.Color + // The color for prefixes. prefixTextColor tcell.Color @@ -81,6 +87,10 @@ type DropDown struct { selected func(text string, index int) dragging bool // Set to true when mouse dragging is in progress. + + // If set the item does not respond to form input/mouse events + // that would change its contents + readonly bool } // NewDropDown returns a new drop-down. @@ -94,13 +104,15 @@ func NewDropDown() *DropDown { SetBackgroundColor(Styles.MoreContrastBackgroundColor) d := &DropDown{ - Box: NewBox(), - currentOption: -1, - list: list, - labelColor: Styles.SecondaryTextColor, - fieldBackgroundColor: Styles.ContrastBackgroundColor, - fieldTextColor: Styles.PrimaryTextColor, - prefixTextColor: Styles.ContrastSecondaryTextColor, + Box: NewBox(), + currentOption: -1, + list: list, + labelColor: Styles.SecondaryTextColor, + fieldBackgroundColor: Styles.ContrastBackgroundColor, + fieldTextColor: Styles.PrimaryTextColor, + prefixTextColor: Styles.ContrastSecondaryTextColor, + fieldTextReadOnlyColor: Styles.SecondaryTextColor, + fieldBackgroundReadOnlyColor: Styles.ContrastBackgroundColor, } d.focus = d @@ -158,6 +170,13 @@ func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix, return d } +// SetReadOnly sets whether the item responds to input +// or mouse events that would change its contents +func (d *DropDown) SetReadOnly(readonly bool) *DropDown { + d.readonly = readonly + return d +} + // SetLabel sets the text to be displayed before the input area. func (d *DropDown) SetLabel(label string) *DropDown { d.label = label @@ -202,6 +221,18 @@ func (d *DropDown) SetPrefixTextColor(color tcell.Color) *DropDown { return d } +// SetFieldBackgroundColor sets the background color of the options area when readonly. +func (d *DropDown) SetFieldBackgroundReadOnlyColor(color tcell.Color) *DropDown { + d.fieldBackgroundReadOnlyColor = color + return d +} + +// SetFieldTextRedaOnlyColor sets the text color of the options area when readonly. +func (d *DropDown) SetFieldTextReadOnlyColor(color tcell.Color) *DropDown { + d.fieldTextReadOnlyColor = color + return d +} + // SetFormAttributes sets attributes shared by all form items. func (d *DropDown) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { d.labelWidth = labelWidth @@ -209,6 +240,8 @@ func (d *DropDown) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldT d.backgroundColor = bgColor d.fieldTextColor = fieldTextColor d.fieldBackgroundColor = fieldBgColor + d.fieldTextReadOnlyColor = labelColor + d.fieldBackgroundReadOnlyColor = fieldBgColor return d } @@ -340,7 +373,9 @@ func (d *DropDown) Draw(screen tcell.Screen) { fieldWidth = rightLimit - x } fieldStyle := tcell.StyleDefault.Background(d.fieldBackgroundColor) - if d.GetFocusable().HasFocus() && !d.open { + if d.readonly { + fieldStyle = fieldStyle.Background(d.fieldBackgroundReadOnlyColor) + } else if d.GetFocusable().HasFocus() && !d.open { fieldStyle = fieldStyle.Background(d.fieldTextColor) } for index := 0; index < fieldWidth; index++ { @@ -360,14 +395,16 @@ func (d *DropDown) Draw(screen tcell.Screen) { } } else { color := d.fieldTextColor + if d.readonly { + color = d.fieldTextReadOnlyColor + } else if d.GetFocusable().HasFocus() && !d.open { + // Just show the current selection. + color = d.fieldBackgroundColor + } text := d.noSelection if d.currentOption >= 0 && d.currentOption < len(d.options) { text = d.currentOptionPrefix + d.options[d.currentOption].Text + d.currentOptionSuffix } - // Just show the current selection. - if d.GetFocusable().HasFocus() && !d.open { - color = d.fieldBackgroundColor - } Print(screen, text, x, y, fieldWidth, AlignLeft, color) } @@ -511,7 +548,7 @@ func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMou x, y := event.Position() _, rectY, _, _ := d.GetInnerRect() inRect := y == rectY - if !d.open && !inRect { + if !d.open && !inRect || d.readonly { return d.InRect(x, y), nil // No, and it's not expanded either. Ignore. } diff --git a/inputfield.go b/inputfield.go index d497bf9..2ba018c 100644 --- a/inputfield.go +++ b/inputfield.go @@ -51,6 +51,12 @@ type InputField struct { // The text color of the input area. fieldTextColor tcell.Color + // The background color of the input area when readonly. + fieldBackgroundReadOnlyColor tcell.Color + + // The text color of the input area when readonly. + fieldTextReadOnlyColor tcell.Color + // The text color of the placeholder. placeholderTextColor tcell.Color @@ -96,16 +102,22 @@ type InputField struct { fieldX int // The x-coordinate of the input field as determined during the last call to Draw(). offset int // The number of bytes of the text string skipped ahead while drawing. + + // If set the item does not respond to form input/mouse events + // that would change its contents + readonly bool } // NewInputField returns a new input field. func NewInputField() *InputField { return &InputField{ - Box: NewBox(), - labelColor: Styles.SecondaryTextColor, - fieldBackgroundColor: Styles.ContrastBackgroundColor, - fieldTextColor: Styles.PrimaryTextColor, - placeholderTextColor: Styles.ContrastSecondaryTextColor, + Box: NewBox(), + labelColor: Styles.SecondaryTextColor, + fieldBackgroundColor: Styles.ContrastBackgroundColor, + fieldTextColor: Styles.PrimaryTextColor, + placeholderTextColor: Styles.ContrastSecondaryTextColor, + fieldTextReadOnlyColor: Styles.SecondaryTextColor, + fieldBackgroundReadOnlyColor: Styles.ContrastBackgroundColor, } } @@ -124,6 +136,13 @@ func (i *InputField) GetText() string { return i.text } +// SetReadOnly sets whether the item responds to input +// or mouse events that would change its contents +func (i *InputField) SetReadOnly(readonly bool) *InputField { + i.readonly = readonly + return i +} + // SetLabel sets the text to be displayed before the input area. func (i *InputField) SetLabel(label string) *InputField { i.label = label @@ -172,6 +191,18 @@ func (i *InputField) SetPlaceholderTextColor(color tcell.Color) *InputField { return i } +// SetFieldBackgroundColor sets the background color of the input area when readonly. +func (i *InputField) SetFieldBackgroundReadOnlyColor(color tcell.Color) *InputField { + i.fieldBackgroundReadOnlyColor = color + return i +} + +// SetFieldTextReadOnlyColor sets the text color of the input area when readonly. +func (i *InputField) SetFieldTextReadOnlyColor(color tcell.Color) *InputField { + i.fieldTextReadOnlyColor = color + return i +} + // SetFormAttributes sets attributes shared by all form items. func (i *InputField) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem { i.labelWidth = labelWidth @@ -179,6 +210,8 @@ func (i *InputField) SetFormAttributes(labelWidth int, labelColor, bgColor, fiel i.backgroundColor = bgColor i.fieldTextColor = fieldTextColor i.fieldBackgroundColor = fieldBgColor + i.fieldTextReadOnlyColor = labelColor + i.fieldBackgroundReadOnlyColor = fieldBgColor return i } @@ -334,7 +367,11 @@ func (i *InputField) Draw(screen tcell.Screen) { if rightLimit-x < fieldWidth { fieldWidth = rightLimit - x } - fieldStyle := tcell.StyleDefault.Background(i.fieldBackgroundColor) + bgcolor := i.fieldBackgroundColor + if i.readonly { + bgcolor = i.fieldBackgroundReadOnlyColor + } + fieldStyle := tcell.StyleDefault.Background(bgcolor) for index := 0; index < fieldWidth; index++ { screen.SetContent(x+index, y, ' ', nil, fieldStyle) } @@ -344,16 +381,24 @@ func (i *InputField) Draw(screen tcell.Screen) { text := i.text if text == "" && i.placeholder != "" { // Draw placeholder text. - Print(screen, Escape(i.placeholder), x, y, fieldWidth, AlignLeft, i.placeholderTextColor) + color := i.placeholderTextColor + if i.readonly { + color = i.fieldTextReadOnlyColor + } + Print(screen, Escape(i.placeholder), x, y, fieldWidth, AlignLeft, color) i.offset = 0 } else { // Draw entered text. + color := i.fieldTextColor + if i.readonly { + color = i.fieldTextReadOnlyColor + } if i.maskCharacter > 0 { text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text)) } if fieldWidth >= stringWidth(text) { // We have enough space for the full text. - Print(screen, Escape(text), x, y, fieldWidth, AlignLeft, i.fieldTextColor) + Print(screen, Escape(text), x, y, fieldWidth, AlignLeft, color) i.offset = 0 iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { if textPos >= i.cursorPos { @@ -391,7 +436,7 @@ func (i *InputField) Draw(screen tcell.Screen) { } return false }) - Print(screen, Escape(text[i.offset:]), x, y, fieldWidth, AlignLeft, i.fieldTextColor) + Print(screen, Escape(text[i.offset:]), x, y, fieldWidth, AlignLeft, color) } } @@ -508,27 +553,45 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p case 'f': // Move word right. moveWordRight() default: + if i.readonly { + break + } if !add(event.Rune()) { return } } } else { + if i.readonly { + break + } // Other keys are simply accepted as regular characters. if !add(event.Rune()) { return } } case tcell.KeyCtrlU: // Delete all. + if i.readonly { + break + } i.text = "" i.cursorPos = 0 case tcell.KeyCtrlK: // Delete until the end of the line. + if i.readonly { + break + } i.text = i.text[:i.cursorPos] case tcell.KeyCtrlW: // Delete last word. + if i.readonly { + break + } lastWord := regexp.MustCompile(`\S+\s*$`) newText := lastWord.ReplaceAllString(i.text[:i.cursorPos], "") + i.text[i.cursorPos:] i.cursorPos -= len(i.text) - len(newText) i.text = newText case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete character before the cursor. + if i.readonly { + break + } iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { i.text = i.text[:textPos] + i.text[textPos+textWidth:] i.cursorPos -= textWidth @@ -538,6 +601,9 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p i.offset = 0 } case tcell.KeyDelete: // Delete character after the cursor. + if i.readonly { + break + } iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { i.text = i.text[:i.cursorPos] + i.text[i.cursorPos+textWidth:] return true