From c1e2bf46a69639086c949c8602f53cd6ce5a24e1 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 31 Mar 2023 03:25:46 -0400 Subject: [PATCH] TextBox supports copy/paste with keyboard commands --- backends/x/window.go | 4 +++ elements/containers/container.go | 4 +++ elements/containers/document.go | 4 +++ elements/containers/scroll.go | 4 +++ elements/core/core.go | 10 +++++++ elements/textbox.go | 47 ++++++++++++++++++++++++++++++++ parent.go | 3 ++ textmanip/textmanip.go | 11 ++++---- 8 files changed, 82 insertions(+), 5 deletions(-) diff --git a/backends/x/window.go b/backends/x/window.go index 5b3fa8e..5bde230 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -122,6 +122,10 @@ func (window *window) NotifyMinimumSizeChange (child tomo.Element) { window.childMinimumSizeChangeCallback(child.MinimumSize()) } +func (window *window) Window () tomo.Window { + return window +} + func (window *window) RequestFocus ( child tomo.Focusable, ) ( diff --git a/elements/containers/container.go b/elements/containers/container.go index 9c67ad5..904eb4c 100644 --- a/elements/containers/container.go +++ b/elements/containers/container.go @@ -204,6 +204,10 @@ func (element *Container) redoAll () { } } +func (element *Container) Window () tomo.Window { + return element.core.Window() +} + // NotifyMinimumSizeChange notifies the container that the minimum size of a // child element has changed. func (element *Container) NotifyMinimumSizeChange (child tomo.Element) { diff --git a/elements/containers/document.go b/elements/containers/document.go index 5de9751..f67511f 100644 --- a/elements/containers/document.go +++ b/elements/containers/document.go @@ -202,6 +202,10 @@ func (element *DocumentContainer) partition () { } } +func (element *DocumentContainer) Window () tomo.Window { + return element.core.Window() +} + // NotifyMinimumSizeChange notifies the container that the minimum size of a // child element has changed. func (element *DocumentContainer) NotifyMinimumSizeChange (child tomo.Element) { diff --git a/elements/containers/scroll.go b/elements/containers/scroll.go index 28d9b8c..2dbcbd1 100644 --- a/elements/containers/scroll.go +++ b/elements/containers/scroll.go @@ -110,6 +110,10 @@ func (element *ScrollContainer) disownChild (child tomo.Scrollable) { } } +func (element *ScrollContainer) Window () tomo.Window { + return element.core.Window() +} + // NotifyMinimumSizeChange notifies the container that the minimum size of a // child element has changed. func (element *ScrollContainer) NotifyMinimumSizeChange (child tomo.Element) { diff --git a/elements/core/core.go b/elements/core/core.go index f7b650f..1eaae56 100644 --- a/elements/core/core.go +++ b/elements/core/core.go @@ -122,6 +122,16 @@ func (control CoreControl) Parent () tomo.Parent { return control.core.parent } +// Window returns the window containing the element. +func (control CoreControl) Window () tomo.Window { + parent := control.Parent() + if parent == nil { + return nil + } else { + return parent.Window() + } +} + // Outer returns the outer element given when the control was constructed. func (control CoreControl) Outer () tomo.Element { return control.core.outer diff --git a/elements/textbox.go b/elements/textbox.go index 45f0cc9..fd8c054 100644 --- a/elements/textbox.go +++ b/elements/textbox.go @@ -1,7 +1,9 @@ package elements +import "io" import "image" import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/data" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/canvas" @@ -181,6 +183,34 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) case key == 'a' && modifiers.Control: element.dot.Start = 0 element.dot.End = len(element.text) + + case key == 'x' && modifiers.Control: + var lifted []rune + element.text, element.dot, lifted = textmanip.Lift ( + element.text, + element.dot) + if lifted != nil { + element.clipboardPut(lifted) + textChanged = true + } + + case key == 'c' && modifiers.Control: + element.clipboardPut(element.dot.Slice(element.text)) + + case key == 'v' && modifiers.Control: + window := element.core.Window() + if window == nil { break } + window.Paste (func (d data.Data, err error) { + if err != nil { return } + reader, ok := d[data.MimePlain] + if !ok { return } + bytes, _ := io.ReadAll(reader) + element.text, element.dot = textmanip.Type ( + element.text, + element.dot, + []rune(string(bytes))...) + element.notifyAsyncTextChange() + }) case key.Printable(): element.text, element.dot = textmanip.Type ( @@ -213,6 +243,13 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) } } +func (element *TextBox) clipboardPut (text []rune) { + window := element.core.Window() + if window != nil { + window.Copy(data.Bytes(data.MimePlain, []byte(string(text)))) + } +} + func (element *TextBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) { } func (element *TextBox) SetPlaceholder (placeholder string) { @@ -366,6 +403,16 @@ func (element *TextBox) updateMinimumSize () { element.placeholderDrawer.LineHeight().Round()) } +func (element *TextBox) notifyAsyncTextChange () { + element.runOnChange() + element.valueDrawer.SetText(element.text) + element.scrollToCursor() + if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { + parent.NotifyScrollBoundsChange(element) + } + element.redo() +} + func (element *TextBox) redo () { if element.core.HasImage () { element.draw() diff --git a/parent.go b/parent.go index 6f7d328..f27ee6d 100644 --- a/parent.go +++ b/parent.go @@ -6,6 +6,9 @@ type Parent interface { // minimum size has changed. This method is expected to be called by // child elements when their minimum size changes. NotifyMinimumSizeChange (child Element) + + // Window returns the window containing the parent. + Window () Window } // FocusableParent represents a parent with keyboard navigation support. diff --git a/textmanip/textmanip.go b/textmanip/textmanip.go index 5f2d181..b89ec03 100644 --- a/textmanip/textmanip.go +++ b/textmanip/textmanip.go @@ -48,6 +48,7 @@ func (dot Dot) Width () int { } func (dot Dot) Slice (text []rune) []rune { + dot = dot.Canon().Constrain(len(text)) return text[dot.Start:dot.End] } @@ -146,22 +147,22 @@ func Lift (text []rune, dot Dot) (result []rune, moved Dot, lifted []rune) { return } -func Type (text []rune, dot Dot, character rune) (result []rune, moved Dot) { +func Type (text []rune, dot Dot, characters ...rune) (result []rune, moved Dot) { dot = dot.Constrain(len(text)) if dot.Empty() { result = append(result, text[:dot.End]...) - result = append(result, character) + result = append(result, characters...) if dot.End < len(text) { result = append(result, text[dot.End:]...) } - moved = EmptyDot(dot.Add(1).End) + moved = EmptyDot(dot.Add(len(characters)).End) return } else { dot = dot.Canon() result = append(result, text[:dot.Start]...) - result = append(result, character) + result = append(result, characters...) result = append(result, text[dot.End:]...) - moved = EmptyDot(dot.Add(1).Start) + moved = EmptyDot(dot.Add(len(characters)).Start) return } }