diff --git a/canvas.go b/canvas.go index 6ec2ec3833..d34869bc3b 100644 --- a/canvas.go +++ b/canvas.go @@ -17,4 +17,5 @@ type Canvas interface { SetOnTypedRune(func(rune)) OnTypedKey() func(*KeyEvent) SetOnTypedKey(func(*KeyEvent)) + AddShortcut(shortcut Shortcut, handler func(shortcut Shortcut)) } diff --git a/canvasobject.go b/canvasobject.go index dff0d61c54..810ac2c0a6 100644 --- a/canvasobject.go +++ b/canvasobject.go @@ -53,3 +53,8 @@ type Focusable interface { TypedRune(rune) TypedKey(*KeyEvent) } + +// Shortcutable describes any CanvasObject that can respond to shortcut commands (quit, cut, copy, and paste). +type Shortcutable interface { + TypedShortcut(shortcut Shortcut) bool +} diff --git a/clipboard.go b/clipboard.go new file mode 100644 index 0000000000..fe51b9bcfd --- /dev/null +++ b/clipboard.go @@ -0,0 +1,9 @@ +package fyne + +// Clipboard represents the system clipboard interface +type Clipboard interface { + // Content returns the clipboard content + Content() string + // SetContent sets the clipboard content + SetContent(content string) +} diff --git a/driver/desktop/key.go b/driver/desktop/key.go index b4add1172f..076321d6a2 100644 --- a/driver/desktop/key.go +++ b/driver/desktop/key.go @@ -1,6 +1,8 @@ package desktop -import "fyne.io/fyne" +import ( + "fyne.io/fyne" +) const ( // KeyShift represents the left or right shift key @@ -14,3 +16,17 @@ const ( // KeyMenu represents the left or right menu / application key KeyMenu fyne.KeyName = "Menu" ) + +// Modifier captures any key modifiers (shift etc) pressed during this key event +type Modifier int + +const ( + // ShiftModifier represents a shift key being held + ShiftModifier Modifier = 1 << iota + // ControlModifier represents the ctrl key being held + ControlModifier + // AltModifier represents either alt keys being held + AltModifier + // SuperModifier represents either super keys being held + SuperModifier +) diff --git a/driver/desktop/shortcut.go b/driver/desktop/shortcut.go new file mode 100644 index 0000000000..2466c77952 --- /dev/null +++ b/driver/desktop/shortcut.go @@ -0,0 +1,40 @@ +package desktop + +import ( + "strings" + + "fyne.io/fyne" +) + +// CustomShortcut describes a shortcut desktop event. +type CustomShortcut struct { + fyne.KeyName + Modifier +} + +// ShortcutName returns the shortcut name associated to the event +func (cs *CustomShortcut) ShortcutName() string { + id := &strings.Builder{} + id.WriteString("CustomDesktop:") + id.WriteString(modifierToString(cs.Modifier)) + id.WriteString("+") + id.WriteString(string(cs.KeyName)) + return id.String() +} + +func modifierToString(mods Modifier) string { + s := []string{} + if (mods & ShiftModifier) != 0 { + s = append(s, string(KeyShift)) + } + if (mods & ControlModifier) != 0 { + s = append(s, string(KeyControl)) + } + if (mods & AltModifier) != 0 { + s = append(s, string(KeyAlt)) + } + if (mods & SuperModifier) != 0 { + s = append(s, string(KeySuper)) + } + return strings.Join(s, "+") +} diff --git a/driver/desktop/shortcut_test.go b/driver/desktop/shortcut_test.go new file mode 100644 index 0000000000..7837159ebe --- /dev/null +++ b/driver/desktop/shortcut_test.go @@ -0,0 +1,80 @@ +package desktop + +import ( + "testing" + + "fyne.io/fyne" +) + +func TestCustomShortcut_Shortcut(t *testing.T) { + type fields struct { + KeyName fyne.KeyName + Modifier Modifier + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Ctrl+C", + fields: fields{ + KeyName: fyne.KeyC, + Modifier: ControlModifier, + }, + want: "CustomDesktop:Control+C", + }, + { + name: "Ctrl+Alt+Esc", + fields: fields{ + KeyName: fyne.KeyEscape, + Modifier: ControlModifier + AltModifier, + }, + want: "CustomDesktop:Control+Alt+Escape", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cs := &CustomShortcut{ + KeyName: tt.fields.KeyName, + Modifier: tt.fields.Modifier, + } + if got := cs.ShortcutName(); got != tt.want { + t.Errorf("CustomShortcut.ShortcutName() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_modifierToString(t *testing.T) { + type args struct { + } + tests := []struct { + name string + mods Modifier + want string + }{ + { + name: "None", + mods: 0, + want: "", + }, + { + name: "Ctrl", + mods: ControlModifier, + want: "Control", + }, + { + name: "Shift+Ctrl", + mods: ShiftModifier + ControlModifier, + want: "Shift+Control", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := modifierToString(tt.mods); got != tt.want { + t.Errorf("modifierToString() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/driver/gl/canvas.go b/driver/gl/canvas.go index 491558f772..7ccf1d76f9 100644 --- a/driver/gl/canvas.go +++ b/driver/gl/canvas.go @@ -17,6 +17,7 @@ type glCanvas struct { onTypedRune func(rune) onTypedKey func(*fyne.KeyEvent) + shortcut fyne.ShortcutHandler program uint32 scale float32 @@ -130,6 +131,10 @@ func (c *glCanvas) SetOnTypedKey(typed func(*fyne.KeyEvent)) { c.onTypedKey = typed } +func (c *glCanvas) AddShortcut(shortcut fyne.Shortcut, handler func(shortcut fyne.Shortcut)) { + c.shortcut.AddShortcut(shortcut, handler) +} + func (c *glCanvas) paint(size fyne.Size) { if c.Content() == nil { return diff --git a/driver/gl/clipboard.go b/driver/gl/clipboard.go new file mode 100644 index 0000000000..8f50d2fd90 --- /dev/null +++ b/driver/gl/clipboard.go @@ -0,0 +1,27 @@ +package gl + +import ( + "log" + + "github.com/go-gl/glfw/v3.2/glfw" +) + +// clipboard represents the system clipboard +type clipboard struct { + window *glfw.Window +} + +// Content returns the clipboard content +func (c *clipboard) Content() string { + content, err := c.window.GetClipboardString() + if err != nil { + log.Printf("unable to get clipboard string: %v", err) + return "" + } + return content +} + +// SetContent sets the clipboard content +func (c *clipboard) SetContent(content string) { + c.window.SetClipboardString(content) +} diff --git a/driver/gl/window.go b/driver/gl/window.go index 9afdc17e2f..5e5830314f 100644 --- a/driver/gl/window.go +++ b/driver/gl/window.go @@ -6,6 +6,7 @@ import ( _ "image/png" // for the icon "log" "os" + "runtime" "strconv" "fyne.io/fyne" @@ -37,6 +38,8 @@ type window struct { title string icon fyne.Resource + clipboard fyne.Clipboard + master bool fullScreen bool fixedSize bool @@ -277,6 +280,14 @@ func (w *window) ShowAndRun() { fyne.CurrentApp().Driver().Run() } +//Clipboard returns the system clipboard +func (w *window) Clipboard() fyne.Clipboard { + if w.clipboard == nil { + w.clipboard = &clipboard{window: w.viewport} + } + return w.clipboard +} + func (w *window) Content() fyne.CanvasObject { return w.canvas.content } @@ -495,6 +506,84 @@ func keyToName(key glfw.Key) fyne.KeyName { case glfw.KeyF12: return fyne.KeyF12 + case glfw.KeyKPEnter: + return fyne.KeyEnter + + // printable + case glfw.KeyA: + return fyne.KeyA + case glfw.KeyB: + return fyne.KeyB + case glfw.KeyC: + return fyne.KeyC + case glfw.KeyD: + return fyne.KeyD + case glfw.KeyE: + return fyne.KeyE + case glfw.KeyF: + return fyne.KeyF + case glfw.KeyG: + return fyne.KeyG + case glfw.KeyH: + return fyne.KeyH + case glfw.KeyI: + return fyne.KeyI + case glfw.KeyJ: + return fyne.KeyJ + case glfw.KeyK: + return fyne.KeyK + case glfw.KeyL: + return fyne.KeyL + case glfw.KeyM: + return fyne.KeyM + case glfw.KeyN: + return fyne.KeyN + case glfw.KeyO: + return fyne.KeyO + case glfw.KeyP: + return fyne.KeyP + case glfw.KeyQ: + return fyne.KeyQ + case glfw.KeyR: + return fyne.KeyR + case glfw.KeyS: + return fyne.KeyS + case glfw.KeyT: + return fyne.KeyT + case glfw.KeyU: + return fyne.KeyU + case glfw.KeyV: + return fyne.KeyV + case glfw.KeyW: + return fyne.KeyW + case glfw.KeyX: + return fyne.KeyX + case glfw.KeyY: + return fyne.KeyY + case glfw.KeyZ: + return fyne.KeyZ + case glfw.Key0: + return fyne.Key0 + case glfw.Key1: + return fyne.Key1 + case glfw.Key2: + return fyne.Key2 + case glfw.Key3: + return fyne.Key3 + case glfw.Key4: + return fyne.Key4 + case glfw.Key5: + return fyne.Key5 + case glfw.Key6: + return fyne.Key6 + case glfw.Key7: + return fyne.Key7 + case glfw.Key8: + return fyne.Key8 + case glfw.Key9: + return fyne.Key9 + + // desktop case glfw.KeyLeftShift: fallthrough case glfw.KeyRightShift: @@ -513,50 +602,134 @@ func keyToName(key glfw.Key) fyne.KeyName { return desktop.KeySuper case glfw.KeyMenu: return desktop.KeyMenu - - case glfw.KeyKPEnter: - return fyne.KeyEnter } return "" } func (w *window) keyPressed(viewport *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { - if w.canvas.Focused() == nil && w.canvas.onTypedKey == nil { - return - } if action != glfw.Press { // ignore key up return } - if key <= glfw.KeyWorld1 { // filter printable characters handled in charModInput + keyName := keyToName(key) + keyDesktopModifier := desktopModifier(mods) + + if keyDesktopModifier < desktop.ControlModifier { + // No shortcut detected, pass down to TypedKey and exit + keyEvent := &fyne.KeyEvent{Name: keyName} + if w.canvas.Focused() != nil { + go w.canvas.Focused().TypedKey(keyEvent) + } + if w.canvas.onTypedKey != nil { + go w.canvas.onTypedKey(keyEvent) + } return } - ev := new(fyne.KeyEvent) - ev.Name = keyToName(key) + var shortcut fyne.Shortcut + switch runtime.GOOS { + case "darwin": + switch keyName { + case fyne.KeyV: + // detect paste shortcut + if keyDesktopModifier == desktop.SuperModifier { + shortcut = &fyne.ShortcutPaste{ + Clipboard: w.Clipboard(), + } + } + case fyne.KeyC: + // detect copy shortcut + if keyDesktopModifier == desktop.SuperModifier { + shortcut = &fyne.ShortcutCopy{ + Clipboard: w.Clipboard(), + } + } + case fyne.KeyX: + // detect cut shortcut + if keyDesktopModifier == desktop.SuperModifier { + shortcut = &fyne.ShortcutCut{ + Clipboard: w.Clipboard(), + } + } + default: + shortcut = &desktop.CustomShortcut{ + KeyName: keyName, + Modifier: keyDesktopModifier, + } + } + default: + switch keyName { + case fyne.KeyV: + // detect paste shortcut + if keyDesktopModifier == desktop.ControlModifier { + shortcut = &fyne.ShortcutPaste{ + Clipboard: w.Clipboard(), + } + } + case fyne.KeyC: + // detect copy shortcut + if keyDesktopModifier == desktop.ControlModifier { + shortcut = &fyne.ShortcutCopy{ + Clipboard: w.Clipboard(), + } + } + case fyne.KeyX: + // detect cut shortcut + if keyDesktopModifier == desktop.ControlModifier { + shortcut = &fyne.ShortcutCut{ + Clipboard: w.Clipboard(), + } + } + default: + shortcut = &desktop.CustomShortcut{ + KeyName: keyName, + Modifier: keyDesktopModifier, + } + } + } - if w.canvas.Focused() != nil { - go w.canvas.Focused().TypedKey(ev) + if shortcutable, ok := w.canvas.Focused().(fyne.Shortcutable); ok { + if shortcutable.TypedShortcut(shortcut) { + return + } } - if w.canvas.onTypedKey != nil { - go w.canvas.onTypedKey(ev) + + w.canvas.shortcut.TypedShortcut(shortcut) +} + +func desktopModifier(mods glfw.ModifierKey) desktop.Modifier { + var m desktop.Modifier + if (mods & glfw.ModShift) != 0 { + m |= desktop.ShiftModifier } + if (mods & glfw.ModControl) != 0 { + m |= desktop.ControlModifier + } + if (mods & glfw.ModAlt) != 0 { + m |= desktop.AltModifier + } + if (mods & glfw.ModSuper) != 0 { + m |= desktop.SuperModifier + } + return m } +// charModInput defines the character with modifiers callback which is called when a +// Unicode character is input regardless of what modifier keys are used. +// +// The character with modifiers callback is intended for implementing custom +// Unicode character input. Characters do not map 1:1 to physical keys, +// as a key may produce zero, one or more characters. func (w *window) charModInput(viewport *glfw.Window, char rune, mods glfw.ModifierKey) { if w.canvas.Focused() == nil && w.canvas.onTypedRune == nil { return } - if mods == 0 || mods == glfw.ModShift { - if w.canvas.Focused() != nil { - w.canvas.Focused().TypedRune(char) - } - if w.canvas.onTypedRune != nil { - w.canvas.onTypedRune(char) - } - - return + if w.canvas.Focused() != nil { + w.canvas.Focused().TypedRune(char) + } + if w.canvas.onTypedRune != nil { + w.canvas.onTypedRune(char) } } diff --git a/driver/gl/window_test.go b/driver/gl/window_test.go index 2cb6b7dd8e..ce878c535f 100644 --- a/driver/gl/window_test.go +++ b/driver/gl/window_test.go @@ -10,6 +10,7 @@ import ( "fyne.io/fyne" "fyne.io/fyne/canvas" + "fyne.io/fyne/driver/desktop" _ "fyne.io/fyne/test" "fyne.io/fyne/theme" @@ -82,3 +83,44 @@ func TestWindow_SetPadded(t *testing.T) { assert.Equal(t, theme.Padding()*2+content.MinSize().Width, width) assert.Equal(t, theme.Padding(), content.Position().X) } + +func TestWindow_Clipboard(t *testing.T) { + d := NewGLDriver() + w := d.CreateWindow("Test") + + text := "My content from test window" + cb := w.Clipboard() + + cliboardContent := cb.Content() + if cliboardContent != "" { + // Current environment has some content stored in clipboard, + // set temporary to an empty string to allow test and restore later. + cb.SetContent("") + } + + assert.Empty(t, cb.Content()) + + cb.SetContent(text) + assert.Equal(t, text, cb.Content()) + + // Restore cliboardContent, if any + cb.SetContent(cliboardContent) +} + +func TestWindow_Shortcut(t *testing.T) { + d := NewGLDriver() + w := d.CreateWindow("Test") + + shortcutFullScreenWindow := &desktop.CustomShortcut{ + KeyName: fyne.KeyF12, + } + + w.Canvas().AddShortcut(shortcutFullScreenWindow, func(sc fyne.Shortcut) { + w.SetFullScreen(true) + }) + + assert.False(t, w.FullScreen()) + + w.Canvas().(*glCanvas).shortcut.TypedShortcut(shortcutFullScreenWindow) + assert.True(t, w.FullScreen()) +} diff --git a/key.go b/key.go index 93d4be2e29..d281664a71 100644 --- a/key.go +++ b/key.go @@ -65,4 +65,77 @@ const ( // KeyEnter is the enter/ return key (keypad) KeyEnter KeyName = "KP_Enter" + + // Key0 represents the key 0 + Key0 KeyName = "0" + // Key1 represents the key 1 + Key1 KeyName = "1" + // Key2 represents the key 2 + Key2 KeyName = "2" + // Key3 represents the key 3 + Key3 KeyName = "3" + // Key4 represents the key 4 + Key4 KeyName = "4" + // Key5 represents the key 5 + Key5 KeyName = "5" + // Key6 represents the key 6 + Key6 KeyName = "6" + // Key7 represents the key 7 + Key7 KeyName = "7" + // Key8 represents the key 8 + Key8 KeyName = "8" + // Key9 represents the key 9 + Key9 KeyName = "9" + // KeyA represents the key A + KeyA KeyName = "A" + // KeyB represents the key B + KeyB KeyName = "B" + // KeyC represents the key C + KeyC KeyName = "C" + // KeyD represents the key D + KeyD KeyName = "D" + // KeyE represents the key E + KeyE KeyName = "E" + // KeyF represents the key F + KeyF KeyName = "F" + // KeyG represents the key G + KeyG KeyName = "G" + // KeyH represents the key H + KeyH KeyName = "H" + // KeyI represents the key I + KeyI KeyName = "I" + // KeyJ represents the key J + KeyJ KeyName = "J" + // KeyK represents the key K + KeyK KeyName = "K" + // KeyL represents the key L + KeyL KeyName = "L" + // KeyM represents the key M + KeyM KeyName = "M" + // KeyN represents the key N + KeyN KeyName = "N" + // KeyO represents the key O + KeyO KeyName = "O" + // KeyP represents the key P + KeyP KeyName = "P" + // KeyQ represents the key Q + KeyQ KeyName = "Q" + // KeyR represents the key R + KeyR KeyName = "R" + // KeyS represents the key S + KeyS KeyName = "S" + // KeyT represents the key T + KeyT KeyName = "T" + // KeyU represents the key U + KeyU KeyName = "U" + // KeyV represents the key V + KeyV KeyName = "V" + // KeyW represents the key W + KeyW KeyName = "W" + // KeyX represents the key X + KeyX KeyName = "X" + // KeyY represents the key Y + KeyY KeyName = "Y" + // KeyZ represents the key Z + KeyZ KeyName = "Z" ) diff --git a/shortcut.go b/shortcut.go new file mode 100644 index 0000000000..e2a3eb2c90 --- /dev/null +++ b/shortcut.go @@ -0,0 +1,69 @@ +package fyne + +import ( + "sync" +) + +// ShortcutHandler is a default implementation of the shortcut handler +// for the canvasObject +type ShortcutHandler struct { + mu sync.RWMutex + entry map[string]func(Shortcut) +} + +// TypedShortcut handle the registered shortcut +func (sh *ShortcutHandler) TypedShortcut(shortcut Shortcut) bool { + if shortcut == nil { + return false + } + if sc, ok := sh.entry[shortcut.ShortcutName()]; ok { + sc(shortcut) + return true + } + return false +} + +// AddShortcut register an handler to be executed when the shortcut action is triggered +func (sh *ShortcutHandler) AddShortcut(shortcut Shortcut, handler func(shortcut Shortcut)) { + sh.mu.Lock() + defer sh.mu.Unlock() + if sh.entry == nil { + sh.entry = make(map[string]func(Shortcut)) + } + sh.entry[shortcut.ShortcutName()] = handler +} + +// Shortcut is the interface used to describe a shortcut action +type Shortcut interface { + ShortcutName() string +} + +// ShortcutPaste describes a shortcut paste action. +type ShortcutPaste struct { + Clipboard Clipboard +} + +// ShortcutName returns the shortcut name +func (se *ShortcutPaste) ShortcutName() string { + return "Paste" +} + +// ShortcutCopy describes a shortcut copy action. +type ShortcutCopy struct { + Clipboard Clipboard +} + +// ShortcutName returns the shortcut name +func (se *ShortcutCopy) ShortcutName() string { + return "Copy" +} + +// ShortcutCut describes a shortcut cut action. +type ShortcutCut struct { + Clipboard Clipboard +} + +// ShortcutName returns the shortcut name +func (se *ShortcutCut) ShortcutName() string { + return "Cut" +} diff --git a/test/testcanvas.go b/test/testcanvas.go index bfe36d677a..02e3f06abe 100644 --- a/test/testcanvas.go +++ b/test/testcanvas.go @@ -10,6 +10,8 @@ type testCanvas struct { onTypedRune func(rune) onTypedKey func(*fyne.KeyEvent) + + fyne.ShortcutHandler } func (c *testCanvas) Content() fyne.CanvasObject { diff --git a/test/testclipboard.go b/test/testclipboard.go new file mode 100644 index 0000000000..9f844b901d --- /dev/null +++ b/test/testclipboard.go @@ -0,0 +1,20 @@ +package test + +import "fyne.io/fyne" + +type testClipboard struct { + content string +} + +func (c *testClipboard) Content() string { + return c.content +} + +func (c *testClipboard) SetContent(content string) { + c.content = content +} + +// NewClipboard returns a single use in-memory clipboard used for testing +func NewClipboard() fyne.Clipboard { + return &testClipboard{} +} diff --git a/test/testwindow.go b/test/testwindow.go index b371d065fd..993ec0af69 100644 --- a/test/testwindow.go +++ b/test/testwindow.go @@ -13,7 +13,8 @@ type testWindow struct { padded bool onClosed func() - canvas fyne.Canvas + canvas fyne.Canvas + clipboard fyne.Clipboard } var windows = make([]fyne.Window, 0) @@ -73,6 +74,10 @@ func (w *testWindow) SetOnClosed(closed func()) { func (w *testWindow) Show() {} +func (w *testWindow) Clipboard() fyne.Clipboard { + return w.clipboard +} + func (w *testWindow) Hide() {} func (w *testWindow) Close() { @@ -114,6 +119,7 @@ func NewWindow(content fyne.CanvasObject) fyne.Window { canvas := NewCanvas() canvas.SetContent(content) window := &testWindow{canvas: canvas} + window.clipboard = &testClipboard{} windowsMutex.Lock() windows = append(windows, window) diff --git a/widget/entry.go b/widget/entry.go index 1cdbdb0974..1b099be4aa 100644 --- a/widget/entry.go +++ b/widget/entry.go @@ -2,6 +2,7 @@ package widget import ( "image/color" + "strings" "fyne.io/fyne" "fyne.io/fyne/canvas" @@ -108,7 +109,7 @@ func (e *entryRenderer) Objects() []fyne.CanvasObject { // Entry widget allows simple text to be input when focused. type Entry struct { baseWidget - + shortcut fyne.ShortcutHandler Text string PlaceHolder string OnChanged func(string) `json:"-"` @@ -331,6 +332,11 @@ func (e *Entry) TypedKey(key *fyne.KeyEvent) { Renderer(e).(*entryRenderer).moveCursor() } +// TypedShortcut implements the Shortcutable interface +func (e *Entry) TypedShortcut(shortcut fyne.Shortcut) bool { + return e.shortcut.TypedShortcut(shortcut) +} + // textProvider returns the text handler for this entry func (e *Entry) textProvider() *textProvider { return Renderer(e).(*entryRenderer).text @@ -407,10 +413,28 @@ func (e *Entry) CreateRenderer() fyne.WidgetRenderer { []fyne.CanvasObject{line, placeholder, text, cursor}, e} } +func (e *Entry) registerShortcut() { + scPaste := &fyne.ShortcutPaste{} + e.shortcut.AddShortcut(scPaste, func(se fyne.Shortcut) { + scPaste = se.(*fyne.ShortcutPaste) + text := scPaste.Clipboard.Content() + if !e.MultiLine { + // format clipboard content to be compatible with single line entry + text = strings.Replace(text, "\n", " ", -1) + } + provider := e.textProvider() + runes := []rune(text) + provider.insertAt(e.cursorTextPos(), runes) + e.CursorColumn += len(runes) + e.updateText(provider.String()) + Renderer(e).(*entryRenderer).moveCursor() + }) +} + // NewEntry creates a new single line entry widget. func NewEntry() *Entry { e := &Entry{} - + e.registerShortcut() Refresh(e) return e } @@ -418,7 +442,7 @@ func NewEntry() *Entry { // NewMultiLineEntry creates a new entry that allows multiple lines func NewMultiLineEntry() *Entry { e := &Entry{MultiLine: true} - + e.registerShortcut() Refresh(e) return e } @@ -426,7 +450,7 @@ func NewMultiLineEntry() *Entry { // NewPasswordEntry creates a new entry password widget func NewPasswordEntry() *Entry { e := &Entry{Password: true} - + e.registerShortcut() Refresh(e) return e } diff --git a/widget/entry_test.go b/widget/entry_test.go index 74953331f7..3edc9880dd 100644 --- a/widget/entry_test.go +++ b/widget/entry_test.go @@ -428,3 +428,66 @@ func TestPasswordEntry_Obfuscation(t *testing.T) { assert.Equal(t, "Hié™שרה", entry.Text) assert.Equal(t, "*******", entryRenderTexts(entry)[0].Text) } + +func TestEntry_OnPaste(t *testing.T) { + clipboard := test.NewClipboard() + shortuct := &fyne.ShortcutPaste{Clipboard: clipboard} + tests := []struct { + name string + entry *Entry + clipboardContent string + wantText string + }{ + { + name: "singleline: empty content", + entry: NewEntry(), + clipboardContent: "", + wantText: "", + }, + { + name: "singleline: simple text", + entry: NewEntry(), + clipboardContent: "clipboard content", + wantText: "clipboard content", + }, + { + name: "singleline: UTF8 text", + entry: NewEntry(), + clipboardContent: "Hié™שרה", + wantText: "Hié™שרה", + }, + { + name: "singleline: with new line", + entry: NewEntry(), + clipboardContent: "clipboard\ncontent", + wantText: "clipboard content", + }, + { + name: "singleline: with tab", + entry: NewEntry(), + clipboardContent: "clipboard\tcontent", + wantText: "clipboard\tcontent", + }, + { + name: "password: with new line", + entry: NewPasswordEntry(), + clipboardContent: "3SB=y+)z\nkHGK(hx6 -e_\"1TZu q^bF3^$u H[:e\"1O.", + wantText: `3SB=y+)z kHGK(hx6 -e_"1TZu q^bF3^$u H[:e"1O.`, + }, + { + name: "multiline: with new line", + entry: NewMultiLineEntry(), + clipboardContent: "clipboard\ncontent", + wantText: "clipboard\ncontent", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + clipboard.SetContent(tt.clipboardContent) + handled := tt.entry.TypedShortcut(shortuct) + assert.True(t, handled) + assert.Equal(t, tt.wantText, tt.entry.Text) + }) + } +} diff --git a/window.go b/window.go index 5c205b1fa9..95714a3696 100644 --- a/window.go +++ b/window.go @@ -68,4 +68,7 @@ type Window interface { // Canvas returns the canvas context to render in the window. // This can be useful to set a key handler for the window, for example. Canvas() Canvas + + //Clipboard returns the system clipboard + Clipboard() Clipboard }