From e790b508a15e65aaf29cd1fd761128f946110cbf Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 21 Oct 2020 14:35:52 +1100 Subject: [PATCH] Some small quality of life improvements - Added Ctrl+A and Ctrl+E for moving around the edit bar a.la. Emacs - Added the "yank" and "paste" command to copy and paste a single cell value. These are mapped to "y" and "p". - Modified the edit bar so that entering commands is the only case where typing backspace on an empty edit bar will cancel out of edit mode --- commandmap.go | 41 +++++++++++++++++++++++++++++++++++++++-- frame.go | 7 +++++-- session.go | 2 ++ ui/stdcomps.go | 20 +++++++++++++++++--- 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/commandmap.go b/commandmap.go index e997b9a..157bfb0 100644 --- a/commandmap.go +++ b/commandmap.go @@ -276,7 +276,10 @@ func (cm *CommandMapping) RegisterViewCommands() { }) cm.Define("enter-command", "Enter command", "", func(ctx *CommandContext) error { - ctx.Frame().Prompt(PromptOptions{Prompt: ":"}, func(res string) error { + ctx.Frame().Prompt(PromptOptions{ + Prompt: ":", + CancelOnEmptyBackspace: true, + }, func(res string) error { return cm.Eval(ctx, res) }) return nil @@ -300,7 +303,15 @@ func (cm *CommandMapping) RegisterViewCommands() { grid := ctx.Frame().Grid() cellX, cellY := grid.CellPosition() - if _, isRwModel := ctx.ModelVC().Model().(RWModel); isRwModel { + if _, isRwModel := ctx.ModelVC().Model().(RWModel); !isRwModel { + return errors.New("Model is read-only") + } + + if len(ctx.Args()) == 1 { + if err := ctx.ModelVC().SetCellValue(cellY, cellX, ctx.Args()[0]); err != nil { + return err + } + } else { ctx.Frame().Prompt(PromptOptions{ Prompt: "> ", InitialValue: grid.Model().CellValue(cellX, cellY), @@ -314,6 +325,29 @@ func (cm *CommandMapping) RegisterViewCommands() { } return nil }) + cm.Define("yank", "Yank cell value", "", func(ctx *CommandContext) error { + grid := ctx.Frame().Grid() + cellX, cellY := grid.CellPosition() + + // TODO: allow ranges + ctx.Session().pasteBoard.SetCellValue(0, 0, grid.Model().CellValue(cellX, cellY)) + + return nil + }) + cm.Define("paste", "Paste cell value", "", func(ctx *CommandContext) error { + grid := ctx.Frame().Grid() + cellX, cellY := grid.CellPosition() + + // TODO: allow ranges + if _, isRwModel := ctx.ModelVC().Model().(RWModel); !isRwModel { + return errors.New("Model is read-only") + } + if err := ctx.ModelVC().SetCellValue(cellY, cellX, ctx.Session().pasteBoard.CellValue(0, 0)); err != nil { + return err + } + + return nil + }) cm.Define("save", "Save current file", "", func(ctx *CommandContext) error { wSource, isWSource := ctx.Session().Source.(WritableModelSource) @@ -378,6 +412,9 @@ func (cm *CommandMapping) RegisterViewKeyBindings() { cm.MapKey('/', cm.Command("search")) cm.MapKey('n', cm.Command("search-next")) + cm.MapKey('y', cm.Command("yank")) + cm.MapKey('p', cm.Command("paste")) + cm.MapKey('0', cm.Command("clear-row-marker")) cm.MapKey('1', cm.Command("mark-row-red")) cm.MapKey('2', cm.Command("mark-row-green")) diff --git a/frame.go b/frame.go index 6d72caf..a656d54 100644 --- a/frame.go +++ b/frame.go @@ -106,13 +106,16 @@ func (frame *Frame) Error(err error) { } type PromptOptions struct { - Prompt string - InitialValue string + Prompt string + InitialValue string + CancelOnEmptyBackspace bool } // Prompt the user for input. This switches the mode to entry mode. func (frame *Frame) Prompt(options PromptOptions, callback func(res string) error) { + frame.textEntry.Reset() frame.textEntry.Prompt = options.Prompt + frame.textEntry.CancelOnEmptyBackspace = options.CancelOnEmptyBackspace frame.textEntry.SetValue(options.InitialValue) frame.textEntry.OnCancel = frame.exitEntryMode diff --git a/session.go b/session.go index a8154b7..7418549 100644 --- a/session.go +++ b/session.go @@ -15,6 +15,7 @@ type Session struct { Commands *CommandMapping UIManager *ui.Ui modelController *ModelViewCtrl + pasteBoard RWModel LastSearch *regexp.Regexp } @@ -28,6 +29,7 @@ func NewSession(uiManager *ui.Ui, frame *Frame, source ModelSource) *Session { Commands: NewCommandMapping(), UIManager: uiManager, modelController: NewGridViewModel(model), + pasteBoard: NewSingleCellStdModel(), } frame.SetModel(&SessionGridModel{session.modelController}) diff --git a/ui/stdcomps.go b/ui/stdcomps.go index 7c7090b..5594523 100644 --- a/ui/stdcomps.go +++ b/ui/stdcomps.go @@ -57,6 +57,11 @@ type TextEntry struct { value string cursorOffset int displayOffset int + isDirty bool + + // CancelOnEmptyBackspace will cancel the text entry prompt if no other + // key was pressed and the prompt was empty. + CancelOnEmptyBackspace bool // Called when the user presses Enter OnEntry func(val string) @@ -65,6 +70,10 @@ type TextEntry struct { OnCancel func() } +func (te *TextEntry) Reset() { + te.isDirty = false +} + func (te *TextEntry) Remeasure(w, h int) (int, int) { return w, 1 } @@ -116,16 +125,18 @@ func (te *TextEntry) KeyPressed(key rune, mod int) { te.moveCursorBy(-1) } else if key == KeyArrowRight { te.moveCursorBy(1) - } else if key == KeyHome { + } else if (key == KeyHome) || (key == KeyCtrlA) { te.moveCursorTo(0) - } else if key == KeyEnd { + } else if (key == KeyEnd) || (key == KeyCtrlE) { te.moveCursorTo(len(te.value)) } else if (key == KeyBackspace) || (key == KeyBackspace2) { if mod&ModKeyAlt != 0 { te.backspaceWhile(unicode.IsSpace) te.backspaceWhile(func(r rune) bool { return !unicode.IsSpace(r) }) } else if te.cursorOffset == 0 { - te.cancelAndExit() + if te.CancelOnEmptyBackspace && !te.isDirty { + te.cancelAndExit() + } } else { te.backspace() } @@ -171,6 +182,7 @@ func (te *TextEntry) backspaceWhile(guard func(r rune) bool) { // Kill the line. If the cursor is at the end of the line, kill to the start. // Otherwise, trim the line. func (te *TextEntry) killLine() { + te.isDirty = true if te.cursorOffset < len(te.value) { te.value = te.value[:te.cursorOffset] } else { @@ -181,6 +193,7 @@ func (te *TextEntry) killLine() { // Inserts a rune at the cursor position func (te *TextEntry) insertRune(key rune) { + te.isDirty = true if te.cursorOffset >= len(te.value) { te.value += string(key) } else { @@ -191,6 +204,7 @@ func (te *TextEntry) insertRune(key rune) { // Remove the character at a specific position func (te *TextEntry) removeCharAtPos(pos int) { + te.isDirty = true if (pos >= 0) && (pos < len(te.value)) { te.value = te.value[:pos] + te.value[pos+1:] }