From 181417d2916b015925027efe74992a62a6c8b7bb Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 17 Mar 2018 12:32:00 +0200 Subject: [PATCH] Move InputField modifications to gomuks --- advanced-inputfield.go | 412 +++++++++++++++++++++++++++++++++++++++++ view-main.go | 4 +- 2 files changed, 414 insertions(+), 2 deletions(-) create mode 100644 advanced-inputfield.go diff --git a/advanced-inputfield.go b/advanced-inputfield.go new file mode 100644 index 0000000..b110d0d --- /dev/null +++ b/advanced-inputfield.go @@ -0,0 +1,412 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package main + +import ( + "math" + "regexp" + "strings" + "unicode/utf8" + + "github.com/gdamore/tcell" + "github.com/mattn/go-runewidth" + "github.com/zyedidia/clipboard" + "maunium.net/go/tview" +) + +// AdvancedInputField is a multi-line user-editable text area. +// +// Use SetMaskCharacter() to hide input from onlookers (e.g. for password +// input). +type AdvancedInputField struct { + *tview.Box + + // Cursor position + cursorOffset int + viewOffset int + + // The text that was entered. + text string + + // The text to be displayed before the input area. + label string + + // The text to be displayed in the input area when "text" is empty. + placeholder string + + // 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 text color of the placeholder. + placeholderTextColor tcell.Color + + // The screen width of the input area. A value of 0 means extend as much as + // possible. + fieldWidth int + + // A character to mask entered text (useful for password fields). A value of 0 + // disables masking. + maskCharacter rune + + // An optional function which may reject the last character that was entered. + accept func(text string, ch rune) bool + + // An optional function which is called when the input has changed. + changed func(text string) + + // An optional function which is called when the user indicated that they + // are done entering text. The key which was pressed is provided (tab, + // shift-tab, enter, or escape). + done func(tcell.Key) +} + +// NewAdvancedInputField returns a new input field. +func NewAdvancedInputField() *AdvancedInputField { + return &AdvancedInputField{ + Box: tview.NewBox(), + labelColor: tview.Styles.SecondaryTextColor, + fieldBackgroundColor: tview.Styles.ContrastBackgroundColor, + fieldTextColor: tview.Styles.PrimaryTextColor, + placeholderTextColor: tview.Styles.ContrastSecondaryTextColor, + } +} + +// SetText sets the current text of the input field. +func (field *AdvancedInputField) SetText(text string) *AdvancedInputField { + field.text = text + if field.changed != nil { + field.changed(text) + } + return field +} + +// GetText returns the current text of the input field. +func (field *AdvancedInputField) GetText() string { + return field.text +} + +// SetLabel sets the text to be displayed before the input area. +func (field *AdvancedInputField) SetLabel(label string) *AdvancedInputField { + field.label = label + return field +} + +// GetLabel returns the text to be displayed before the input area. +func (field *AdvancedInputField) GetLabel() string { + return field.label +} + +// SetPlaceholder sets the text to be displayed when the input text is empty. +func (field *AdvancedInputField) SetPlaceholder(text string) *AdvancedInputField { + field.placeholder = text + return field +} + +// SetLabelColor sets the color of the label. +func (field *AdvancedInputField) SetLabelColor(color tcell.Color) *AdvancedInputField { + field.labelColor = color + return field +} + +// SetFieldBackgroundColor sets the background color of the input area. +func (field *AdvancedInputField) SetFieldBackgroundColor(color tcell.Color) *AdvancedInputField { + field.fieldBackgroundColor = color + return field +} + +// SetFieldTextColor sets the text color of the input area. +func (field *AdvancedInputField) SetFieldTextColor(color tcell.Color) *AdvancedInputField { + field.fieldTextColor = color + return field +} + +// SetPlaceholderExtColor sets the text color of placeholder text. +func (field *AdvancedInputField) SetPlaceholderExtColor(color tcell.Color) *AdvancedInputField { + field.placeholderTextColor = color + return field +} + +// SetFormAttributes sets attributes shared by all form items. +func (field *AdvancedInputField) SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) tview.FormItem { + field.label = label + field.labelColor = labelColor + field.SetBackgroundColor(bgColor) + field.fieldTextColor = fieldTextColor + field.fieldBackgroundColor = fieldBgColor + return field +} + +// SetFieldWidth sets the screen width of the input area. A value of 0 means +// extend as much as possible. +func (field *AdvancedInputField) SetFieldWidth(width int) *AdvancedInputField { + field.fieldWidth = width + return field +} + +// GetFieldWidth returns this primitive's field width. +func (field *AdvancedInputField) GetFieldWidth() int { + return field.fieldWidth +} + +// SetMaskCharacter sets a character that masks user input on a screen. A value +// of 0 disables masking. +func (field *AdvancedInputField) SetMaskCharacter(mask rune) *AdvancedInputField { + field.maskCharacter = mask + return field +} + +// SetAcceptanceFunc sets a handler which may reject the last character that was +// entered (by returning false). +// +// This package defines a number of variables Prefixed with AdvancedInputField which may +// be used for common input (e.g. numbers, maximum text length). +func (field *AdvancedInputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *AdvancedInputField { + field.accept = handler + return field +} + +// SetChangedFunc sets a handler which is called whenever the text of the input +// field has changed. It receives the current text (after the change). +func (field *AdvancedInputField) SetChangedFunc(handler func(text string)) *AdvancedInputField { + field.changed = handler + return field +} + +// SetDoneFunc sets a handler which is called when the user is done entering +// text. The callback function is provided with the key that was pressed, which +// is one of the following: +// +// - KeyEnter: Done entering text. +// - KeyEscape: Abort text input. +// - KeyTab: Move to the next field. +// - KeyBacktab: Move to the previous field. +func (field *AdvancedInputField) SetDoneFunc(handler func(key tcell.Key)) *AdvancedInputField { + field.done = handler + return field +} + +// SetFinishedFunc calls SetDoneFunc(). +func (field *AdvancedInputField) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem { + return field.SetDoneFunc(handler) +} + +// Draw draws this primitive onto the screen. +func (field *AdvancedInputField) Draw(screen tcell.Screen) { + field.Box.Draw(screen) + + // Prepare + x, y, width, height := field.GetInnerRect() + rightLimit := x + width + if height < 1 || rightLimit <= x { + return + } + + // Draw label. + _, drawnWidth := tview.Print(screen, field.label, x, y, rightLimit-x, tview.AlignLeft, field.labelColor) + x += drawnWidth + + // Draw input area. + fieldWidth := field.fieldWidth + if fieldWidth == 0 { + fieldWidth = math.MaxInt32 + } + if rightLimit-x < fieldWidth { + fieldWidth = rightLimit - x + } + fieldStyle := tcell.StyleDefault.Background(field.fieldBackgroundColor) + for index := 0; index < fieldWidth; index++ { + screen.SetContent(x+index, y, ' ', nil, fieldStyle) + } + + text := field.text + if text == "" && field.placeholder != "" { + tview.Print(screen, field.placeholder, x, y, fieldWidth, tview.AlignLeft, field.placeholderTextColor) + } + + // Draw entered text. + if field.maskCharacter > 0 { + text = strings.Repeat(string(field.maskCharacter), utf8.RuneCountInString(field.text)) + } + textWidth := runewidth.StringWidth(text) + if field.cursorOffset >= textWidth { + fieldWidth-- + } + + // Recalculate view offset + if field.cursorOffset < field.viewOffset { + field.viewOffset = field.cursorOffset + } else if field.cursorOffset > field.viewOffset + fieldWidth { + field.viewOffset = field.cursorOffset - fieldWidth + } else if textWidth - field.viewOffset < fieldWidth { + field.viewOffset = textWidth - fieldWidth + } + // Make sure view offset didn't become negative + if field.viewOffset < 0 { + field.viewOffset = 0 + } + + // Draw entered text. + runes := []rune(text) + relPos := 0 + for pos := field.viewOffset; pos <= fieldWidth + field.viewOffset && pos < len(runes); pos++ { + ch := runes[pos] + w := runewidth.RuneWidth(ch) + _, _, style, _ := screen.GetContent(x+relPos, y) + style = style.Foreground(field.fieldTextColor) + for w > 0 { + screen.SetContent(x+relPos, y, ch, nil, style) + relPos++ + w-- + } + } + + // Set cursor. + if field.GetFocusable().HasFocus() { + field.setCursor(screen) + } +} + +func (field *AdvancedInputField) GetCursorOffset() int { + return field.cursorOffset +} + +func (field *AdvancedInputField) SetCursorOffset(offset int) *AdvancedInputField { + if offset < 0 { + offset = 0 + } else { + width := runewidth.StringWidth(field.text) + if offset >= width { + offset = width + } + } + field.cursorOffset = offset + return field +} + +// setCursor sets the cursor position. +func (field *AdvancedInputField) setCursor(screen tcell.Screen) { + x, y, width, _ := field.GetRect() + origX, origY := x, y + rightLimit := x + width + if field.HasBorder() { + x++ + y++ + rightLimit -= 2 + } + fieldWidth := runewidth.StringWidth(field.text) + if field.fieldWidth > 0 && fieldWidth > field.fieldWidth-1 { + fieldWidth = field.fieldWidth - 1 + } + x = x + tview.StringWidth(field.label) + field.cursorOffset - field.viewOffset + if x >= rightLimit { + x = rightLimit - 1 + } else if x < origX { + x = origY + } + screen.ShowCursor(x, y) +} + +var ( + lastWord = regexp.MustCompile(`\S+\s*$`) + firstWord = regexp.MustCompile(`^\s*\S+`) +) + +// InputHandler returns the handler for this primitive. +func (field *AdvancedInputField) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + return field.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + currentText := field.text + defer func() { + // Trigger changed events. + if field.text != currentText && field.changed != nil { + field.changed(field.text) + } + // Make sure cursor offset is valid + if field.cursorOffset < 0 { + field.cursorOffset = 0 + } + width := runewidth.StringWidth(field.text) + if field.cursorOffset > width { + field.cursorOffset = width + } + }() + + // Process key event. + switch key := event.Key(); key { + case tcell.KeyRune: // Regular character. + runes := []rune(field.text) + newText := string(runes[0:field.cursorOffset]) + string(event.Rune()) + string(runes[field.cursorOffset:]) + if field.accept != nil { + if !field.accept(newText, event.Rune()) { + break + } + } + field.text = newText + field.cursorOffset++ + case tcell.KeyCtrlV: + clip, _ := clipboard.ReadAll("clipboard") + runes := []rune(field.text) + field.text = string(runes[0:field.cursorOffset]) + clip + string(runes[field.cursorOffset:]) + field.cursorOffset += runewidth.StringWidth(clip) + case tcell.KeyLeft: // Move cursor left. + if event.Modifiers() == tcell.ModCtrl { + runes := []rune(field.text) + found := lastWord.FindString(string(runes[0:field.cursorOffset])) + field.cursorOffset -= runewidth.StringWidth(found) + } else { + field.cursorOffset-- + } + case tcell.KeyRight: // Move cursor right. + if event.Modifiers() == tcell.ModCtrl { + runes := []rune(field.text) + found := firstWord.FindString(string(runes[field.cursorOffset:])) + field.cursorOffset += runewidth.StringWidth(found) + } else { + field.cursorOffset++ + } + case tcell.KeyDelete: // Delete next character. + if field.cursorOffset >= runewidth.StringWidth(field.text) { + break + } + runes := []rune(field.text) + field.text = string(runes[0:field.cursorOffset]) + string(runes[field.cursorOffset + 1:]) + case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete last character. + if field.cursorOffset == 0 { + break + } + runes := []rune(field.text) + if key == tcell.KeyBackspace { // Ctrl+backspace + orig := string(runes[0:field.cursorOffset]) + replacement := lastWord.ReplaceAllString(orig, "") + field.text = replacement + string(runes[field.cursorOffset:]) + field.cursorOffset -= runewidth.StringWidth(orig) - runewidth.StringWidth(replacement) + } else { // Just backspace + field.text = string(runes[0:field.cursorOffset - 1]) + string(runes[field.cursorOffset:]) + field.cursorOffset-- + } + case tcell.KeyEnter, tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done. + if field.done != nil { + field.done(key) + } + } + }) +} diff --git a/view-main.go b/view-main.go index d291754..c08f7f3 100644 --- a/view-main.go +++ b/view-main.go @@ -31,7 +31,7 @@ type MainView struct { roomList *tview.List roomView *tview.Pages rooms map[string]*RoomView - input *tview.InputField + input *AdvancedInputField currentRoomIndex int roomIDs []string @@ -52,7 +52,7 @@ func (ui *GomuksUI) NewMainView() tview.Primitive { roomList: tview.NewList(), roomView: tview.NewPages(), rooms: make(map[string]*RoomView), - input: tview.NewInputField(), + input: NewAdvancedInputField(), matrix: ui.matrix, debug: ui.debug,