From c9421b4bd92748eb5de43119ea70f27c56c5847e Mon Sep 17 00:00:00 2001 From: Oliver <480930+rivo@users.noreply.github.com> Date: Sun, 4 Feb 2024 16:11:39 +0100 Subject: [PATCH] Enabled bracketed pasting. --- application.go | 90 ++++++++++++++++++++++++++++- box.go | 29 ++++++++-- demos/form/main.go | 2 +- demos/inputfield/main.go | 2 +- demos/presentation/main.go | 4 +- demos/textarea/main.go | 5 +- flex.go | 14 +++++ form.go | 23 ++++++++ frame.go | 13 +++++ grid.go | 112 +++++++++++++++++++++---------------- inputfield.go | 37 ++++++++++++ pages.go | 14 +++++ primitive.go | 11 ++++ textarea.go | 44 ++++++++++++--- 14 files changed, 327 insertions(+), 73 deletions(-) diff --git a/application.go b/application.go index 53d3f1c..7c3bdfe 100644 --- a/application.go +++ b/application.go @@ -1,6 +1,7 @@ package tview import ( + "strings" "sync" "time" @@ -83,6 +84,9 @@ type Application struct { // Set to true if mouse events are enabled. enableMouse bool + // Set to true if paste events are enabled. + enablePaste bool + // An optional capture function which receives a key event and returns the // event to be forwarded to the default input handler (nil if nothing should // be forwarded). @@ -145,6 +149,9 @@ func NewApplication() *Application { // forward the Ctrl-C event to primitives down the hierarchy, return a new // key event with the same key and modifiers, e.g. // tcell.NewEventKey(tcell.KeyCtrlC, 0, tcell.ModNone). +// +// Pasted key events are not forwarded to the input capture function if pasting +// is enabled (see [Application.EnablePaste]). func (a *Application) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) *Application { a.inputCapture = capture return a @@ -216,6 +223,26 @@ func (a *Application) EnableMouse(enable bool) *Application { return a } +// EnablePaste enables the capturing of paste events or disables them (if +// "false" is provided). This must be supported by the terminal. +// +// Widgets won't interpret paste events for navigation or selection purposes. +// Paste events are typically only used to insert a block of text into an +// [InputField] or a [TextArea]. +func (a *Application) EnablePaste(enable bool) *Application { + a.Lock() + defer a.Unlock() + if enable != a.enablePaste && a.screen != nil { + if enable { + a.screen.EnablePaste() + } else { + a.screen.DisablePaste() + } + } + a.enablePaste = enable + return a +} + // Run starts the application and thus the event loop. This function returns // when Stop() was called. func (a *Application) Run() error { @@ -239,6 +266,13 @@ func (a *Application) Run() error { } if a.enableMouse { a.screen.EnableMouse() + } else { + a.screen.DisableMouse() + } + if a.enablePaste { + a.screen.EnablePaste() + } else { + a.screen.DisablePaste() } } @@ -283,7 +317,7 @@ func (a *Application) Run() error { screen = <-a.screenReplacement if screen == nil { // No new screen. We're done. - a.QueueEvent(nil) + a.QueueEvent(nil) // Stop the event loop. return } @@ -291,6 +325,7 @@ func (a *Application) Run() error { a.Lock() a.screen = screen enableMouse := a.enableMouse + enablePaste := a.enablePaste a.Unlock() // Initialize and draw this screen. @@ -299,15 +334,27 @@ func (a *Application) Run() error { } if enableMouse { screen.EnableMouse() + } else { + screen.DisableMouse() + } + if enablePaste { + screen.EnablePaste() + } else { + screen.DisablePaste() } a.draw() } }() // Start event loop. + var ( + pasteBuffer strings.Builder + pasting bool // Set to true while we receive paste key events. + ) EventLoop: for { select { + // If we received an event, handle it. case event := <-a.events: if event == nil { break EventLoop @@ -315,6 +362,19 @@ EventLoop: switch event := event.(type) { case *tcell.EventKey: + // If we are pasting, collect runes, nothing else. + if pasting { + switch event.Key() { + case tcell.KeyRune: + pasteBuffer.WriteRune(event.Rune()) + case tcell.KeyEnter: + pasteBuffer.WriteRune('\n') + case tcell.KeyTab: + pasteBuffer.WriteRune('\t') + } + break + } + a.RLock() root := a.root inputCapture := a.inputCapture @@ -327,7 +387,7 @@ EventLoop: event = inputCapture(event) if event == nil { a.draw() - continue // Don't forward event. + break // Don't forward event. } draw = true } @@ -352,6 +412,30 @@ EventLoop: if draw { a.draw() } + case *tcell.EventPaste: + if !a.enablePaste { + break + } + if event.Start() { + pasting = true + pasteBuffer.Reset() + } else if event.End() { + pasting = false + a.RLock() + root := a.root + a.RUnlock() + if root != nil && root.HasFocus() && pasteBuffer.Len() > 0 { + // Pass paste event to the root primitive. + if handler := root.PasteHandler(); handler != nil { + handler(pasteBuffer.String(), func(p Primitive) { + a.SetFocus(p) + }) + } + + // Redraw. + a.draw() + } + } case *tcell.EventResize: if time.Since(lastRedraw) < redrawPause { if redrawTimer != nil { @@ -365,7 +449,7 @@ EventLoop: screen := a.screen a.RUnlock() if screen == nil { - continue + break } lastRedraw = time.Now() screen.Clear() diff --git a/box.go b/box.go index bd82a6b..78fe4bf 100644 --- a/box.go +++ b/box.go @@ -153,8 +153,8 @@ func (b *Box) GetDrawFunc() func(screen tcell.Screen, x, y, width, height int) ( return b.draw } -// WrapInputHandler wraps an input handler (see InputHandler()) with the -// functionality to capture input (see SetInputCapture()) before passing it +// WrapInputHandler wraps an input handler (see [Box.InputHandler]) with the +// functionality to capture input (see [Box.SetInputCapture]) before passing it // on to the provided (default) input handler. // // This is only meant to be used by subclassing primitives. @@ -169,11 +169,25 @@ func (b *Box) WrapInputHandler(inputHandler func(*tcell.EventKey, func(p Primiti } } -// InputHandler returns nil. +// InputHandler returns nil. Box has no default input handling. func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return b.WrapInputHandler(nil) } +// WrapPasteHandler wraps a paste handler (see [Box.PasteHandler]). +func (b *Box) WrapPasteHandler(pasteHandler func(string, func(p Primitive))) func(string, func(p Primitive)) { + return func(text string, setFocus func(p Primitive)) { + if pasteHandler != nil { + pasteHandler(text, setFocus) + } + } +} + +// PasteHandler returns nil. Box has no default paste handling. +func (b *Box) PasteHandler() func(pastedText string, setFocus func(p Primitive)) { + return b.WrapPasteHandler(nil) +} + // SetInputCapture installs a function which captures key events before they are // forwarded to the primitive's default key event handler. This function can // then choose to forward that key event (or a different one) to the default @@ -184,6 +198,9 @@ func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primiti // // This function can also be used on container primitives (like Flex, Grid, or // Form) as keyboard events will be handed down until they are handled. +// +// Pasted key events are not forwarded to the input capture function if pasting +// is enabled (see [Application.EnablePaste]). func (b *Box) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) *Box { b.inputCapture = capture return b @@ -195,8 +212,8 @@ func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey { return b.inputCapture } -// WrapMouseHandler wraps a mouse event handler (see MouseHandler()) with the -// functionality to capture mouse events (see SetMouseCapture()) before passing +// WrapMouseHandler wraps a mouse event handler (see [Box.MouseHandler]) with the +// functionality to capture mouse events (see [Box.SetMouseCapture]) before passing // them on to the provided (default) event handler. // // This is only meant to be used by subclassing primitives. @@ -212,7 +229,7 @@ func (b *Box) WrapMouseHandler(mouseHandler func(MouseAction, *tcell.EventMouse, } } -// MouseHandler returns nil. +// MouseHandler returns nil. Box has no default mouse handling. func (b *Box) 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 action == MouseLeftDown && b.InRect(event.Position()) { diff --git a/demos/form/main.go b/demos/form/main.go index 317eb59..ecb5f1b 100644 --- a/demos/form/main.go +++ b/demos/form/main.go @@ -20,7 +20,7 @@ func main() { app.Stop() }) form.SetBorder(true).SetTitle("Enter some data").SetTitleAlign(tview.AlignLeft) - if err := app.SetRoot(form, true).EnableMouse(true).Run(); err != nil { + if err := app.SetRoot(form, true).EnableMouse(true).EnablePaste(true).Run(); err != nil { panic(err) } } diff --git a/demos/inputfield/main.go b/demos/inputfield/main.go index 1666ac8..9ff90b5 100644 --- a/demos/inputfield/main.go +++ b/demos/inputfield/main.go @@ -16,7 +16,7 @@ func main() { SetDoneFunc(func(key tcell.Key) { app.Stop() }) - if err := app.SetRoot(inputField, true).EnableMouse(true).Run(); err != nil { + if err := app.SetRoot(inputField, true).EnableMouse(true).EnablePaste(true).Run(); err != nil { panic(err) } } diff --git a/demos/presentation/main.go b/demos/presentation/main.go index 42172cb..859cf64 100644 --- a/demos/presentation/main.go +++ b/demos/presentation/main.go @@ -1,7 +1,7 @@ /* A presentation of the tview package, implemented with tview. -Navigation +# Navigation The presentation will advance to the next slide when the primitive demonstrated in the current slide is left (usually by hitting Enter or Escape). Additionally, @@ -97,7 +97,7 @@ func main() { }) // Start the application. - if err := app.SetRoot(layout, true).EnableMouse(true).Run(); err != nil { + if err := app.SetRoot(layout, true).EnableMouse(true).EnablePaste(true).Run(); err != nil { panic(err) } } diff --git a/demos/textarea/main.go b/demos/textarea/main.go index 37cf067..922711b 100644 --- a/demos/textarea/main.go +++ b/demos/textarea/main.go @@ -35,7 +35,7 @@ func main() { updateInfos() mainView := tview.NewGrid(). - SetRows(3, 0). + SetRows(0, 1). AddItem(textArea, 0, 0, 1, 2, 0, 0, true). AddItem(helpInfo, 1, 0, 1, 1, 0, 0, false). AddItem(position, 1, 1, 1, 1, 0, 0, false) @@ -128,8 +128,7 @@ Double-click to select a word. return event }) - if err := app.SetRoot(pages, - true).EnableMouse(true).Run(); err != nil { + if err := app.SetRoot(pages, true).EnableMouse(true).EnablePaste(true).Run(); err != nil { panic(err) } } diff --git a/flex.go b/flex.go index 0cc2424..e8713eb 100644 --- a/flex.go +++ b/flex.go @@ -259,3 +259,17 @@ func (f *Flex) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit } }) } + +// PasteHandler returns the handler for this primitive. +func (f *Flex) PasteHandler() func(pastedText string, setFocus func(p Primitive)) { + return f.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) { + for _, item := range f.items { + if item.Item != nil && item.Item.HasFocus() { + if handler := item.Item.PasteHandler(); handler != nil { + handler(pastedText, setFocus) + return + } + } + } + }) +} diff --git a/form.go b/form.go index 23b95d7..b1b4cdc 100644 --- a/form.go +++ b/form.go @@ -867,3 +867,26 @@ func (f *Form) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit } }) } + +// PasteHandler returns the handler for this primitive. +func (f *Form) PasteHandler() func(pastedText string, setFocus func(p Primitive)) { + return f.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) { + for _, item := range f.items { + if item != nil && item.HasFocus() { + if handler := item.PasteHandler(); handler != nil { + handler(pastedText, setFocus) + return + } + } + } + + for _, button := range f.buttons { + if button.HasFocus() { + if handler := button.PasteHandler(); handler != nil { + handler(pastedText, setFocus) + return + } + } + } + }) +} diff --git a/frame.go b/frame.go index 0ddbb1a..fe2160e 100644 --- a/frame.go +++ b/frame.go @@ -220,3 +220,16 @@ func (f *Frame) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi } }) } + +// PasteHandler returns the handler for this primitive. +func (f *Frame) PasteHandler() func(pastedText string, setFocus func(p Primitive)) { + return f.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) { + if f.primitive == nil { + return + } + if handler := f.primitive.PasteHandler(); handler != nil { + handler(pastedText, setFocus) + return + } + }) +} diff --git a/grid.go b/grid.go index b6cccb7..8a855bc 100644 --- a/grid.go +++ b/grid.go @@ -266,55 +266,6 @@ func (g *Grid) HasFocus() bool { return g.Box.HasFocus() } -// InputHandler returns the handler for this primitive. -func (g *Grid) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { - return g.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { - if !g.hasFocus { - // Pass event on to child primitive. - for _, item := range g.items { - if item != nil && item.Item.HasFocus() { - if handler := item.Item.InputHandler(); handler != nil { - handler(event, setFocus) - return - } - } - } - return - } - - // Process our own key events if we have direct focus. - switch event.Key() { - case tcell.KeyRune: - switch event.Rune() { - case 'g': - g.rowOffset, g.columnOffset = 0, 0 - case 'G': - g.rowOffset = math.MaxInt32 - case 'j': - g.rowOffset++ - case 'k': - g.rowOffset-- - case 'h': - g.columnOffset-- - case 'l': - g.columnOffset++ - } - case tcell.KeyHome: - g.rowOffset, g.columnOffset = 0, 0 - case tcell.KeyEnd: - g.rowOffset = math.MaxInt32 - case tcell.KeyUp: - g.rowOffset-- - case tcell.KeyDown: - g.rowOffset++ - case tcell.KeyLeft: - g.columnOffset-- - case tcell.KeyRight: - g.columnOffset++ - } - }) -} - // Draw draws this primitive onto the screen. func (g *Grid) Draw(screen tcell.Screen) { g.Box.DrawForSubclass(screen, g) @@ -714,3 +665,66 @@ func (g *Grid) MouseHandler() func(action MouseAction, event *tcell.EventMouse, return }) } + +// InputHandler returns the handler for this primitive. +func (g *Grid) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { + return g.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { + if !g.hasFocus { + // Pass event on to child primitive. + for _, item := range g.items { + if item != nil && item.Item.HasFocus() { + if handler := item.Item.InputHandler(); handler != nil { + handler(event, setFocus) + return + } + } + } + return + } + + // Process our own key events if we have direct focus. + switch event.Key() { + case tcell.KeyRune: + switch event.Rune() { + case 'g': + g.rowOffset, g.columnOffset = 0, 0 + case 'G': + g.rowOffset = math.MaxInt32 + case 'j': + g.rowOffset++ + case 'k': + g.rowOffset-- + case 'h': + g.columnOffset-- + case 'l': + g.columnOffset++ + } + case tcell.KeyHome: + g.rowOffset, g.columnOffset = 0, 0 + case tcell.KeyEnd: + g.rowOffset = math.MaxInt32 + case tcell.KeyUp: + g.rowOffset-- + case tcell.KeyDown: + g.rowOffset++ + case tcell.KeyLeft: + g.columnOffset-- + case tcell.KeyRight: + g.columnOffset++ + } + }) +} + +// PasteHandler returns the handler for this primitive. +func (g *Grid) PasteHandler() func(pastedText string, setFocus func(p Primitive)) { + return g.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) { + for _, item := range g.items { + if item != nil && item.Item.HasFocus() { + if handler := item.Item.PasteHandler(); handler != nil { + handler(pastedText, setFocus) + return + } + } + } + }) +} diff --git a/inputfield.go b/inputfield.go index 0372d6f..6a6ec1e 100644 --- a/inputfield.go +++ b/inputfield.go @@ -63,6 +63,11 @@ var ( // // - Tab, BackTab, Enter, Escape: Finish editing. // +// Note that while pressing Tab or Enter is intercepted by the input field, it +// is possible to paste such characters into the input field, possibly resulting +// in multi-line input. You can use [InputField.SetAcceptanceFunc] to prevent +// this. +// // If autocomplete functionality is configured: // // - Down arrow: Open the autocomplete drop-down. @@ -386,6 +391,8 @@ func (i *InputField) Autocomplete() *InputField { // This package defines a number of variables prefixed with InputField which may // be used for common input (e.g. numbers, maximum text length). See for example // [InputFieldInteger]. +// +// When text is pasted, lastChar is 0. func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *InputField { i.accept = handler return i @@ -587,6 +594,11 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p i.autocompleteListMutex.Lock() case tcell.KeyEnter, tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab: finish(key) + case tcell.KeyCtrlV: + if i.accept != nil && !i.accept(i.textArea.getTextBeforeCursor()+i.textArea.GetClipboardText()+i.textArea.getTextAfterCursor(), 0) { + return + } + i.textArea.InputHandler()(event, setFocus) case tcell.KeyRune: if event.Modifiers()&tcell.ModAlt == 0 && i.accept != nil { // Check if this rune is accepted. @@ -660,3 +672,28 @@ func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventM return }) } + +// PasteHandler returns the handler for this primitive. +func (i *InputField) PasteHandler() func(pastedText string, setFocus func(p Primitive)) { + return i.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) { + // Input field may be disabled. + if i.textArea.GetDisabled() { + return + } + + // The autocomplete drop down may be open. + i.autocompleteListMutex.Lock() + defer i.autocompleteListMutex.Unlock() + if i.autocompleteList != nil { + return + } + + // We may not accept this text. + if i.accept != nil && !i.accept(i.textArea.getTextBeforeCursor()+pastedText+i.textArea.getTextAfterCursor(), 0) { + return + } + + // Forward the pasted text to the text area. + i.textArea.PasteHandler()(pastedText, setFocus) + }) +} diff --git a/pages.go b/pages.go index 2b97fbe..146cfa4 100644 --- a/pages.go +++ b/pages.go @@ -315,3 +315,17 @@ func (p *Pages) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi } }) } + +// PasteHandler returns the handler for this primitive. +func (p *Pages) PasteHandler() func(pastedText string, setFocus func(p Primitive)) { + return p.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) { + for _, page := range p.pages { + if page.Item.HasFocus() { + if handler := page.Item.PasteHandler(); handler != nil { + handler(pastedText, setFocus) + return + } + } + } + }) +} diff --git a/primitive.go b/primitive.go index 71a4ce9..6badaee 100644 --- a/primitive.go +++ b/primitive.go @@ -55,4 +55,15 @@ type Primitive interface { // subclass from Box, it is recommended that you wrap your handler using // Box.WrapMouseHandler() so you inherit that functionality. MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) + + // PasteHandler returns a handler which receives pasted text. + // It is called by the Application class. + // + // A value of nil may also be returned to stop the downward propagation of + // paste events. + // + // The Box class may provide functionality to intercept paste events in the + // future. If you subclass from Box, it is recommended that you wrap your + // handler using Box.WrapPasteHandler() so you inherit that functionality. + PasteHandler() func(text string, setFocus func(p Primitive)) } diff --git a/textarea.go b/textarea.go index 33b3e43..50b34f1 100644 --- a/textarea.go +++ b/textarea.go @@ -165,13 +165,15 @@ type textAreaUndoItem struct { // operating system's key bindings for copy+paste functionality may not have the // expected effect as tview will not be able to handle these keys. Pasting text // using your operating system's or terminal's own methods may be very slow as -// each character will be pasted individually. +// each character will be pasted individually. However, some terminals support +// pasting text blocks which is supported by the text area, see +// [Application.EnablePaste] for details. // -// The default clipboard is an internal text buffer, i.e. the operating system's -// clipboard is not used. If you want to implement your own clipboard (or make -// use of your operating system's clipboard), you can use -// [TextArea.SetClipboard] which provides all the functionality needed to -// implement your own clipboard. +// The default clipboard is an internal text buffer local to this text area +// instance, i.e. the operating system's clipboard is not used. If you want to +// implement your own clipboard (or make use of your operating system's +// clipboard), you can use [TextArea.SetClipboard] which provides all the +// functionality needed to implement your own clipboard. // // The text area also supports Undo: // @@ -917,7 +919,8 @@ func (t *TextArea) SetOffset(row, column int) *TextArea { // retrieve text from the clipboard (pasteFromClipboard). // // Providing nil values will cause the default clipboard implementation to be -// used. +// used. Note that the default clipboard is local to this text area instance. +// Copying text to other widgets will not work. func (t *TextArea) SetClipboard(copyToClipboard func(string), pasteFromClipboard func() string) *TextArea { t.copyToClipboard = copyToClipboard if t.copyToClipboard == nil { @@ -936,6 +939,12 @@ func (t *TextArea) SetClipboard(copyToClipboard func(string), pasteFromClipboard return t } +// GetClipboardText returns the current text of the clipboard by calling the +// pasteFromClipboard function set with [TextArea.SetClipboard]. +func (t *TextArea) GetClipboardText() string { + return t.pasteFromClipboard() +} + // SetChangedFunc sets a handler which is called whenever the text of the text // area has changed. func (t *TextArea) SetChangedFunc(handler func()) *TextArea { @@ -1218,7 +1227,7 @@ func (t *TextArea) Draw(screen tcell.Screen) { } }() - // No text / placeholder. + // No text, show placeholder. if t.length == 0 { t.lastHeight, t.lastWidth = height, width t.cursor.row, t.cursor.column, t.cursor.actualColumn, t.cursor.pos = 0, 0, 0, [3]int{1, 0, -1} @@ -1280,6 +1289,13 @@ func (t *TextArea) Draw(screen tcell.Screen) { } } + // Selected tabs are a bit special. + if cluster == "\t" && style == t.selectedStyle { + for colX := 0; colX < clusterWidth && posX+colX-columnOffset < width; colX++ { + screen.SetContent(x+posX+colX-columnOffset, y+posY, ' ', nil, style) + } + } + // Draw character. if posX+clusterWidth-columnOffset <= width && posX-columnOffset >= 0 && clusterWidth > 0 { screen.SetContent(x+posX-columnOffset, y+posY, runes[0], runes[1:], style) @@ -2414,3 +2430,15 @@ func (t *TextArea) MouseHandler() func(action MouseAction, event *tcell.EventMou return }) } + +// PasteHandler returns the handler for this primitive. +func (t *TextArea) PasteHandler() func(pastedText string, setFocus func(p Primitive)) { + return t.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) { + from, to, row := t.getSelection() + t.cursor.pos = t.replace(from, to, pastedText, false) + t.cursor.row = -1 + t.truncateLines(row - 1) + t.findCursor(true, row) + t.selectionStart = t.cursor + }) +}