Enabled bracketed pasting.

pull/952/head
Oliver 3 months ago
parent 007cbb1d13
commit c9421b4bd9

@ -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()

@ -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()) {

@ -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)
}
}

@ -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)
}
}

@ -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)
}
}

@ -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)
}
}

@ -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
}
}
}
})
}

@ -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
}
}
}
})
}

@ -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
}
})
}

@ -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
}
}
}
})
}

@ -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)
})
}

@ -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
}
}
}
})
}

@ -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))
}

@ -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
})
}

Loading…
Cancel
Save