From 662279901907621fff8c585070f9f78d6bc998e8 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 21 Apr 2023 00:52:34 -0400 Subject: [PATCH] Added a few context menus --- backends/x/event.go | 13 ---- backends/x/x.go | 6 +- elements/label.go | 75 +++++++++++++++++++---- elements/textbox.go | 145 ++++++++++++++++++++++++++++++-------------- 4 files changed, 163 insertions(+), 76 deletions(-) diff --git a/backends/x/event.go b/backends/x/event.go index ed701e5..b213197 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -40,7 +40,6 @@ func (window *window) handleExpose ( event xevent.ExposeEvent, ) { _, region := window.compressExpose(*event.ExposeEvent) - window.system.afterEvent() window.pushRegion(region) } @@ -79,8 +78,6 @@ func (window *window) handleConfigureNotify ( window.child.Invalidate() window.child.InvalidateLayout() } - - window.system.afterEvent() } } @@ -140,8 +137,6 @@ func (window *window) handleKeyPress ( focused, ok := window.focused.element.(tomo.KeyboardTarget) if ok { focused.HandleKeyDown(key, modifiers) } } - - window.system.afterEvent() } func (window *window) handleKeyRelease ( @@ -174,8 +169,6 @@ func (window *window) handleKeyRelease ( if window.focused != nil { focused, ok := window.focused.element.(tomo.KeyboardTarget) if ok { focused.HandleKeyUp(key, modifiers) } - - window.system.afterEvent() } } @@ -220,8 +213,6 @@ func (window *window) handleButtonPress ( } underneath.forMouseTargetContainers(callback) } - - window.system.afterEvent() } func (window *window) handleButtonRelease ( @@ -252,8 +243,6 @@ func (window *window) handleButtonRelease ( } dragging.forMouseTargetContainers(callback) } - - window.system.afterEvent() } func (window *window) handleMotionNotify ( @@ -279,8 +268,6 @@ func (window *window) handleMotionNotify ( child.HandleMotion(image.Pt(x, y)) } } - - window.system.afterEvent() } func (window *window) handleSelectionNotify ( diff --git a/backends/x/x.go b/backends/x/x.go index ec69708..7928f66 100644 --- a/backends/x/x.go +++ b/backends/x/x.go @@ -67,12 +67,12 @@ func (backend *Backend) Run () (err error) { <- pingAfter case callback := <- backend.doChannel: callback() - for _, window := range backend.windows { - window.system.afterEvent() - } case <- pingQuit: return } + for _, window := range backend.windows { + window.system.afterEvent() + } } } diff --git a/elements/label.go b/elements/label.go index f48b088..432c7e4 100644 --- a/elements/label.go +++ b/elements/label.go @@ -1,7 +1,10 @@ package elements +import "image" import "golang.org/x/image/math/fixed" 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/canvas" import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/default/theme" @@ -48,6 +51,32 @@ func (element *Label) Entity () tomo.Entity { return element.entity } +// Draw causes the element to draw to the specified destination canvas. +func (element *Label) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + + if element.wrap { + element.drawer.SetMaxWidth(bounds.Dx()) + element.drawer.SetMaxHeight(bounds.Dy()) + } + + element.entity.DrawBackground(destination) + + textBounds := element.drawer.LayoutBounds() + foreground := element.theme.Color ( + tomo.ColorForeground, + tomo.State { }) + element.drawer.Draw(destination, foreground, bounds.Min.Sub(textBounds.Min)) +} + +// Copy copies the label's textto the clipboard. +func (element *Label) Copy () { + window := element.entity.Window() + if window != nil { + window.Copy(data.Bytes(data.MimePlain, []byte(element.text))) + } +} + // EmCollapse forces a minimum width and height upon the label. The width is // measured in emspaces, and the height is measured in lines. If a zero value is // given for a dimension, its minimum will be determined by the label's content. @@ -122,22 +151,42 @@ func (element *Label) SetConfig (new tomo.Config) { element.entity.Invalidate() } -// Draw causes the element to draw to the specified destination canvas. -func (element *Label) Draw (destination canvas.Canvas) { - bounds := element.entity.Bounds() - - if element.wrap { - element.drawer.SetMaxWidth(bounds.Dx()) - element.drawer.SetMaxHeight(bounds.Dy()) +func (element *Label) HandleMouseDown ( + position image.Point, + button input.Button, + modifiers input.Modifiers, +) { + if button == input.ButtonRight { + element.contextMenu(position) + } +} + +func (element *Label) HandleMouseUp ( + position image.Point, + button input.Button, + modifiers input.Modifiers, +) { } + +func (element *Label) contextMenu (position image.Point) { + window := element.entity.Window() + menu, err := window.NewMenu(image.Rectangle { position, position }) + if err != nil { return } + + closeAnd := func (callback func ()) func () { + return func () { callback(); menu.Close() } } - element.entity.DrawBackground(destination) + copyButton := NewButton("Copy") + copyButton.ShowText(false) + copyButton.SetIcon(tomo.IconCopy) + copyButton.OnClick(closeAnd(element.Copy)) - textBounds := element.drawer.LayoutBounds() - foreground := element.theme.Color ( - tomo.ColorForeground, - tomo.State { }) - element.drawer.Draw(destination, foreground, bounds.Min.Sub(textBounds.Min)) + menu.Adopt (NewHBox ( + SpaceNone, + copyButton, + )) + copyButton.Focus() + menu.Show() } func (element *Label) updateMinimumSize () { diff --git a/elements/textbox.go b/elements/textbox.go index 6c20b39..5444ea0 100644 --- a/elements/textbox.go +++ b/elements/textbox.go @@ -151,7 +151,8 @@ func (element *TextBox) HandleMouseDown ( if !element.Enabled() { return } element.Focus() - if button == input.ButtonLeft { + switch button { + case input.ButtonLeft: runeIndex := element.atPosition(position) if runeIndex == -1 { return } @@ -165,6 +166,18 @@ func (element *TextBox) HandleMouseDown ( } element.entity.Invalidate() + case input.ButtonRight: + element.contextMenu(position) + } +} + +func (element *TextBox) HandleMouseUp ( + position image.Point, + button input.Button, + modifiers input.Modifiers, +) { + if button == input.ButtonLeft { + element.dragging = 0 } } @@ -217,23 +230,12 @@ func (element *TextBox) atPosition (position image.Point) int { fixedutil.Pt(position.Sub(offset).Add(textBoundsMin))) } -func (element *TextBox) HandleMouseUp ( - position image.Point, - button input.Button, - modifiers input.Modifiers, -) { - if button == input.ButtonLeft { - element.dragging = 0 - } -} - func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) { if element.onKeyDown != nil && element.onKeyDown(key, modifiers) { return } scrollMemory := element.scroll - altered := true textChanged := false switch { case key == input.KeyEnter: @@ -269,6 +271,8 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) element.dot, modifiers.Control) } + element.scrollToCursor() + element.entity.Invalidate() case key == input.KeyRight: if modifiers.Shift { @@ -282,38 +286,24 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) element.dot, modifiers.Control) } + element.scrollToCursor() + element.entity.Invalidate() case key == 'a' && modifiers.Control: element.dot.Start = 0 element.dot.End = len(element.text) + element.scrollToCursor() + element.entity.Invalidate() - 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 == 'x' && modifiers.Control: element.Cut() + case key == 'c' && modifiers.Control: element.Copy() + case key == 'v' && modifiers.Control: element.Paste() - case key == 'c' && modifiers.Control: - element.clipboardPut(element.dot.Slice(element.text)) - - case key == 'v' && modifiers.Control: - window := element.entity.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 == input.KeyMenu: + pos := fixedutil.RoundPt(element.valueDrawer.PositionAt(element.dot.End)). + Add(element.textOffset()) + pos.Y += element.valueDrawer.LineHeight().Round() + element.contextMenu(pos) case key.Printable(): element.text, element.dot = textmanip.Type ( @@ -321,29 +311,54 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) element.dot, rune(key)) textChanged = true - - default: - altered = false } if textChanged { element.runOnChange() element.valueDrawer.SetText(element.text) - } - - if altered { element.scrollToCursor() + element.entity.Invalidate() } if (textChanged || scrollMemory != element.scroll) { element.entity.NotifyScrollBoundsChange() } - - if altered { - element.entity.Invalidate() +} + +// Cut cuts the selected text in the text box and places it in the clipboard. +func (element *TextBox) Cut () { + var lifted []rune + element.text, element.dot, lifted = textmanip.Lift ( + element.text, + element.dot) + if lifted != nil { + element.clipboardPut(lifted) + element.notifyAsyncTextChange() } } +// Copy copies the selected text in the text box and places it in the clipboard. +func (element *TextBox) Copy () { + element.clipboardPut(element.dot.Slice(element.text)) +} + +// Paste pastes text data from the clipboard into the text box. +func (element *TextBox) Paste () { + window := element.entity.Window() + if window == nil { return } + 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() + }) +} + func (element *TextBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) { } // SetPlaceholder sets the element's placeholder text. @@ -482,6 +497,42 @@ func (element *TextBox) SetConfig (new tomo.Config) { element.entity.Invalidate() } +func (element *TextBox) contextMenu (position image.Point) { + window := element.entity.Window() + menu, err := window.NewMenu(image.Rectangle { position, position }) + if err != nil { return } + + closeAnd := func (callback func ()) func () { + return func () { callback(); menu.Close() } + } + + cutButton := NewButton("Cut") + cutButton.ShowText(false) + cutButton.SetIcon(tomo.IconCut) + cutButton.SetEnabled(!element.dot.Empty()) + cutButton.OnClick(closeAnd(element.Cut)) + + copyButton := NewButton("Copy") + copyButton.ShowText(false) + copyButton.SetIcon(tomo.IconCopy) + copyButton.SetEnabled(!element.dot.Empty()) + copyButton.OnClick(closeAnd(element.Copy)) + + pasteButton := NewButton("Paste") + pasteButton.ShowText(false) + pasteButton.SetIcon(tomo.IconPaste) + pasteButton.OnClick(closeAnd(element.Paste)) + + menu.Adopt (NewHBox ( + SpaceNone, + pasteButton, + copyButton, + cutButton, + )) + pasteButton.Focus() + menu.Show() +} + func (element *TextBox) runOnChange () { if element.onChange != nil { element.onChange()