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
This commit is contained in:
Leon Mika 2020-10-21 14:35:52 +11:00
parent 6e6e586f1d
commit e790b508a1
4 changed files with 63 additions and 7 deletions

View file

@ -276,7 +276,10 @@ func (cm *CommandMapping) RegisterViewCommands() {
}) })
cm.Define("enter-command", "Enter command", "", func(ctx *CommandContext) error { 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 cm.Eval(ctx, res)
}) })
return nil return nil
@ -300,7 +303,15 @@ func (cm *CommandMapping) RegisterViewCommands() {
grid := ctx.Frame().Grid() grid := ctx.Frame().Grid()
cellX, cellY := grid.CellPosition() 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{ ctx.Frame().Prompt(PromptOptions{
Prompt: "> ", Prompt: "> ",
InitialValue: grid.Model().CellValue(cellX, cellY), InitialValue: grid.Model().CellValue(cellX, cellY),
@ -314,6 +325,29 @@ func (cm *CommandMapping) RegisterViewCommands() {
} }
return nil 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 { cm.Define("save", "Save current file", "", func(ctx *CommandContext) error {
wSource, isWSource := ctx.Session().Source.(WritableModelSource) wSource, isWSource := ctx.Session().Source.(WritableModelSource)
@ -378,6 +412,9 @@ func (cm *CommandMapping) RegisterViewKeyBindings() {
cm.MapKey('/', cm.Command("search")) cm.MapKey('/', cm.Command("search"))
cm.MapKey('n', cm.Command("search-next")) 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('0', cm.Command("clear-row-marker"))
cm.MapKey('1', cm.Command("mark-row-red")) cm.MapKey('1', cm.Command("mark-row-red"))
cm.MapKey('2', cm.Command("mark-row-green")) cm.MapKey('2', cm.Command("mark-row-green"))

View file

@ -108,11 +108,14 @@ func (frame *Frame) Error(err error) {
type PromptOptions struct { type PromptOptions struct {
Prompt string Prompt string
InitialValue string InitialValue string
CancelOnEmptyBackspace bool
} }
// Prompt the user for input. This switches the mode to entry mode. // Prompt the user for input. This switches the mode to entry mode.
func (frame *Frame) Prompt(options PromptOptions, callback func(res string) error) { func (frame *Frame) Prompt(options PromptOptions, callback func(res string) error) {
frame.textEntry.Reset()
frame.textEntry.Prompt = options.Prompt frame.textEntry.Prompt = options.Prompt
frame.textEntry.CancelOnEmptyBackspace = options.CancelOnEmptyBackspace
frame.textEntry.SetValue(options.InitialValue) frame.textEntry.SetValue(options.InitialValue)
frame.textEntry.OnCancel = frame.exitEntryMode frame.textEntry.OnCancel = frame.exitEntryMode

View file

@ -15,6 +15,7 @@ type Session struct {
Commands *CommandMapping Commands *CommandMapping
UIManager *ui.Ui UIManager *ui.Ui
modelController *ModelViewCtrl modelController *ModelViewCtrl
pasteBoard RWModel
LastSearch *regexp.Regexp LastSearch *regexp.Regexp
} }
@ -28,6 +29,7 @@ func NewSession(uiManager *ui.Ui, frame *Frame, source ModelSource) *Session {
Commands: NewCommandMapping(), Commands: NewCommandMapping(),
UIManager: uiManager, UIManager: uiManager,
modelController: NewGridViewModel(model), modelController: NewGridViewModel(model),
pasteBoard: NewSingleCellStdModel(),
} }
frame.SetModel(&SessionGridModel{session.modelController}) frame.SetModel(&SessionGridModel{session.modelController})

View file

@ -57,6 +57,11 @@ type TextEntry struct {
value string value string
cursorOffset int cursorOffset int
displayOffset 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 // Called when the user presses Enter
OnEntry func(val string) OnEntry func(val string)
@ -65,6 +70,10 @@ type TextEntry struct {
OnCancel func() OnCancel func()
} }
func (te *TextEntry) Reset() {
te.isDirty = false
}
func (te *TextEntry) Remeasure(w, h int) (int, int) { func (te *TextEntry) Remeasure(w, h int) (int, int) {
return w, 1 return w, 1
} }
@ -116,16 +125,18 @@ func (te *TextEntry) KeyPressed(key rune, mod int) {
te.moveCursorBy(-1) te.moveCursorBy(-1)
} else if key == KeyArrowRight { } else if key == KeyArrowRight {
te.moveCursorBy(1) te.moveCursorBy(1)
} else if key == KeyHome { } else if (key == KeyHome) || (key == KeyCtrlA) {
te.moveCursorTo(0) te.moveCursorTo(0)
} else if key == KeyEnd { } else if (key == KeyEnd) || (key == KeyCtrlE) {
te.moveCursorTo(len(te.value)) te.moveCursorTo(len(te.value))
} else if (key == KeyBackspace) || (key == KeyBackspace2) { } else if (key == KeyBackspace) || (key == KeyBackspace2) {
if mod&ModKeyAlt != 0 { if mod&ModKeyAlt != 0 {
te.backspaceWhile(unicode.IsSpace) te.backspaceWhile(unicode.IsSpace)
te.backspaceWhile(func(r rune) bool { return !unicode.IsSpace(r) }) te.backspaceWhile(func(r rune) bool { return !unicode.IsSpace(r) })
} else if te.cursorOffset == 0 { } else if te.cursorOffset == 0 {
if te.CancelOnEmptyBackspace && !te.isDirty {
te.cancelAndExit() te.cancelAndExit()
}
} else { } else {
te.backspace() 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. // Kill the line. If the cursor is at the end of the line, kill to the start.
// Otherwise, trim the line. // Otherwise, trim the line.
func (te *TextEntry) killLine() { func (te *TextEntry) killLine() {
te.isDirty = true
if te.cursorOffset < len(te.value) { if te.cursorOffset < len(te.value) {
te.value = te.value[:te.cursorOffset] te.value = te.value[:te.cursorOffset]
} else { } else {
@ -181,6 +193,7 @@ func (te *TextEntry) killLine() {
// Inserts a rune at the cursor position // Inserts a rune at the cursor position
func (te *TextEntry) insertRune(key rune) { func (te *TextEntry) insertRune(key rune) {
te.isDirty = true
if te.cursorOffset >= len(te.value) { if te.cursorOffset >= len(te.value) {
te.value += string(key) te.value += string(key)
} else { } else {
@ -191,6 +204,7 @@ func (te *TextEntry) insertRune(key rune) {
// Remove the character at a specific position // Remove the character at a specific position
func (te *TextEntry) removeCharAtPos(pos int) { func (te *TextEntry) removeCharAtPos(pos int) {
te.isDirty = true
if (pos >= 0) && (pos < len(te.value)) { if (pos >= 0) && (pos < len(te.value)) {
te.value = te.value[:pos] + te.value[pos+1:] te.value = te.value[:pos] + te.value[pos+1:]
} }