diff --git a/app.go b/app.go index e2f6c28e46..ac1343935c 100644 --- a/app.go +++ b/app.go @@ -79,6 +79,11 @@ type App interface { // // Since: 2.3 SetCloudProvider(CloudProvider) // configure cloud for this app + + // Clipboard returns the system clipboard. + // + // Since: 2.6 + Clipboard() Clipboard } var app atomic.Pointer[App] diff --git a/app/app.go b/app/app.go index 6c44d95141..8ce569e4c9 100644 --- a/app/app.go +++ b/app/app.go @@ -19,9 +19,10 @@ import ( var _ fyne.App = (*fyneApp)(nil) type fyneApp struct { - driver fyne.Driver - icon fyne.Resource - uniqueID string + driver fyne.Driver + clipboard fyne.Clipboard + icon fyne.Resource + uniqueID string cloud fyne.CloudProvider lifecycle app.Lifecycle @@ -109,6 +110,10 @@ func (a *fyneApp) newDefaultPreferences() *preferences { return p } +func (a *fyneApp) Clipboard() fyne.Clipboard { + return a.clipboard +} + // New returns a new application instance with the default driver and no unique ID (unless specified in FyneApp.toml) func New() fyne.App { if meta.ID == "" { @@ -137,8 +142,8 @@ func makeStoreDocs(id string, s *store) *internal.Docs { } } -func newAppWithDriver(d fyne.Driver, id string) fyne.App { - newApp := &fyneApp{uniqueID: id, driver: d} +func newAppWithDriver(d fyne.Driver, clipboard fyne.Clipboard, id string) fyne.App { + newApp := &fyneApp{uniqueID: id, clipboard: clipboard, driver: d} fyne.SetCurrentApp(newApp) newApp.prefs = newApp.newDefaultPreferences() diff --git a/app/app_gl.go b/app/app_gl.go index 1f34caaa2f..135c02d3d3 100644 --- a/app/app_gl.go +++ b/app/app_gl.go @@ -10,5 +10,5 @@ import ( // NewWithID returns a new app instance using the appropriate runtime driver. // The ID string should be globally unique to this app. func NewWithID(id string) fyne.App { - return newAppWithDriver(glfw.NewGLDriver(), id) + return newAppWithDriver(glfw.NewGLDriver(), glfw.NewClipboard(), id) } diff --git a/app/app_mobile.go b/app/app_mobile.go index 0303c202a2..80d623dd95 100644 --- a/app/app_mobile.go +++ b/app/app_mobile.go @@ -12,7 +12,7 @@ import ( // The ID string should be globally unique to this app. func NewWithID(id string) fyne.App { d := mobile.NewGoMobileDriver() - a := newAppWithDriver(d, id) + a := newAppWithDriver(d, mobile.NewClipboard(), id) d.(mobile.ConfiguredDriver).SetOnConfigurationChanged(func(c *mobile.Configuration) { internalapp.SystemTheme = c.SystemTheme diff --git a/app/app_software.go b/app/app_software.go index ccdcb097cf..2821ce9d94 100644 --- a/app/app_software.go +++ b/app/app_software.go @@ -11,5 +11,5 @@ import ( // NewWithID returns a new app instance using the test (headless) driver. // The ID string should be globally unique to this app. func NewWithID(id string) fyne.App { - return newAppWithDriver(test.NewDriverWithPainter(software.NewPainter()), id) + return newAppWithDriver(test.NewDriverWithPainter(software.NewPainter()), test.NewClipboard(), id) } diff --git a/app/app_test.go b/app/app_test.go index ce67ca2cca..9d6f613ba4 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" "fyne.io/fyne/v2" - _ "fyne.io/fyne/v2/test" + "fyne.io/fyne/v2/test" ) func TestDummyApp(t *testing.T) { @@ -48,3 +48,26 @@ func TestFyneApp_SetIcon(t *testing.T) { assert.Equal(t, setIcon, app.Icon()) } + +func TestFynaApp_Clipboard(t *testing.T) { + app := test.NewTempApp(t) + test.NewTempWindow(t, nil) + + text := "My content from test window" + cb := app.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 clipboardContent, if any + cb.SetContent(cliboardContent) +} diff --git a/app_test.go b/app_test.go index 1322b3c918..46c2ad697b 100644 --- a/app_test.go +++ b/app_test.go @@ -68,6 +68,10 @@ func (dummyApp) Metadata() AppMetadata { return AppMetadata{} } +func (dummyApp) Clipboard() Clipboard { + return nil +} + func TestSetCurrentApp(t *testing.T) { a := &dummyApp{} SetCurrentApp(a) diff --git a/cmd/fyne_demo/main.go b/cmd/fyne_demo/main.go index 6a1f0c53de..3a4c571f33 100644 --- a/cmd/fyne_demo/main.go +++ b/cmd/fyne_demo/main.go @@ -133,19 +133,19 @@ func makeMenu(a fyne.App, w fyne.Window) *fyne.MainMenu { openSettings() }) - cutShortcut := &fyne.ShortcutCut{Clipboard: w.Clipboard()} + cutShortcut := &fyne.ShortcutCut{Clipboard: a.Clipboard()} cutItem := fyne.NewMenuItem("Cut", func() { - shortcutFocused(cutShortcut, w) + shortcutFocused(cutShortcut, a.Clipboard(), w.Canvas().Focused()) }) cutItem.Shortcut = cutShortcut - copyShortcut := &fyne.ShortcutCopy{Clipboard: w.Clipboard()} + copyShortcut := &fyne.ShortcutCopy{Clipboard: a.Clipboard()} copyItem := fyne.NewMenuItem("Copy", func() { - shortcutFocused(copyShortcut, w) + shortcutFocused(copyShortcut, a.Clipboard(), w.Canvas().Focused()) }) copyItem.Shortcut = copyShortcut - pasteShortcut := &fyne.ShortcutPaste{Clipboard: w.Clipboard()} + pasteShortcut := &fyne.ShortcutPaste{Clipboard: a.Clipboard()} pasteItem := fyne.NewMenuItem("Paste", func() { - shortcutFocused(pasteShortcut, w) + shortcutFocused(pasteShortcut, a.Clipboard(), w.Canvas().Focused()) }) pasteItem.Shortcut = pasteShortcut performFind := func() { fmt.Println("Menu Find") } @@ -251,16 +251,16 @@ func makeNav(setTutorial func(tutorial tutorials.Tutorial), loadPrevious bool) f return container.NewBorder(nil, themes, nil, nil, tree) } -func shortcutFocused(s fyne.Shortcut, w fyne.Window) { +func shortcutFocused(s fyne.Shortcut, cb fyne.Clipboard, f fyne.Focusable) { switch sh := s.(type) { case *fyne.ShortcutCopy: - sh.Clipboard = w.Clipboard() + sh.Clipboard = cb case *fyne.ShortcutCut: - sh.Clipboard = w.Clipboard() + sh.Clipboard = cb case *fyne.ShortcutPaste: - sh.Clipboard = w.Clipboard() + sh.Clipboard = cb } - if focused, ok := w.Canvas().Focused().(fyne.Shortcutable); ok { + if focused, ok := f.(fyne.Shortcutable); ok { focused.TypedShortcut(s) } } diff --git a/internal/driver/glfw/clipboard.go b/internal/driver/glfw/clipboard.go index d0aac66d1a..cb0bfb3fa0 100644 --- a/internal/driver/glfw/clipboard.go +++ b/internal/driver/glfw/clipboard.go @@ -12,13 +12,17 @@ import ( ) // Declare conformity with Clipboard interface -var _ fyne.Clipboard = (*clipboard)(nil) +var _ fyne.Clipboard = clipboard{} + +func NewClipboard() fyne.Clipboard { + return clipboard{} +} // clipboard represents the system clipboard type clipboard struct{} // Content returns the clipboard content -func (c *clipboard) Content() string { +func (c clipboard) Content() string { // This retry logic is to work around the "Access Denied" error often thrown in windows PR#1679 if runtime.GOOS != "windows" { return c.content() @@ -34,7 +38,7 @@ func (c *clipboard) Content() string { return "" } -func (c *clipboard) content() string { +func (c clipboard) content() string { content := "" runOnMain(func() { content = glfw.GetClipboardString() @@ -43,7 +47,7 @@ func (c *clipboard) content() string { } // SetContent sets the clipboard content -func (c *clipboard) SetContent(content string) { +func (c clipboard) SetContent(content string) { // This retry logic is to work around the "Access Denied" error often thrown in windows PR#1679 if runtime.GOOS != "windows" { c.setContent(content) @@ -59,7 +63,7 @@ func (c *clipboard) SetContent(content string) { fyne.LogError("GLFW clipboard set failed", nil) } -func (c *clipboard) setContent(content string) { +func (c clipboard) setContent(content string) { runOnMain(func() { glfw.SetClipboardString(content) }) diff --git a/internal/driver/glfw/clipboard_goxjs.go b/internal/driver/glfw/clipboard_goxjs.go index 239158f746..e066a70dd5 100644 --- a/internal/driver/glfw/clipboard_goxjs.go +++ b/internal/driver/glfw/clipboard_goxjs.go @@ -8,7 +8,11 @@ import ( ) // Declare conformity with Clipboard interface -var _ fyne.Clipboard = (*clipboard)(nil) +var _ fyne.Clipboard = clipboard{} + +func NewClipboard() fyne.Clipboard { + return clipboard{} +} // clipboard represents the system clipboard type clipboard struct { @@ -16,7 +20,7 @@ type clipboard struct { } // Content returns the clipboard content -func (c *clipboard) Content() string { +func (c clipboard) Content() string { content := "" runOnMain(func() { content, _ = c.window.GetClipboardString() @@ -25,7 +29,7 @@ func (c *clipboard) Content() string { } // SetContent sets the clipboard content -func (c *clipboard) SetContent(content string) { +func (c clipboard) SetContent(content string) { runOnMain(func() { c.window.SetClipboardString(content) }) diff --git a/internal/driver/glfw/window.go b/internal/driver/glfw/window.go index 52cdfe6c2d..6639d79ba7 100644 --- a/internal/driver/glfw/window.go +++ b/internal/driver/glfw/window.go @@ -237,7 +237,7 @@ func (w *window) ShowAndRun() { // Clipboard returns the system clipboard func (w *window) Clipboard() fyne.Clipboard { - return &clipboard{} + return NewClipboard() } func (w *window) Content() fyne.CanvasObject { @@ -858,17 +858,17 @@ func (w *window) triggersShortcut(localizedKeyName fyne.KeyName, key fyne.KeyNam case fyne.KeyV: // detect paste shortcut shortcut = &fyne.ShortcutPaste{ - Clipboard: w.Clipboard(), + Clipboard: NewClipboard(), } case fyne.KeyC, fyne.KeyInsert: // detect copy shortcut shortcut = &fyne.ShortcutCopy{ - Clipboard: w.Clipboard(), + Clipboard: NewClipboard(), } case fyne.KeyX: // detect cut shortcut shortcut = &fyne.ShortcutCut{ - Clipboard: w.Clipboard(), + Clipboard: NewClipboard(), } case fyne.KeyA: // detect selectAll shortcut @@ -881,12 +881,12 @@ func (w *window) triggersShortcut(localizedKeyName fyne.KeyName, key fyne.KeyNam case fyne.KeyInsert: // detect paste shortcut shortcut = &fyne.ShortcutPaste{ - Clipboard: w.Clipboard(), + Clipboard: NewClipboard(), } case fyne.KeyDelete: // detect cut shortcut shortcut = &fyne.ShortcutCut{ - Clipboard: w.Clipboard(), + Clipboard: NewClipboard(), } } } diff --git a/internal/driver/glfw/window_test.go b/internal/driver/glfw/window_test.go index 00c5e13222..645a4ac606 100644 --- a/internal/driver/glfw/window_test.go +++ b/internal/driver/glfw/window_test.go @@ -1621,28 +1621,6 @@ func TestWindow_ManualFocus(t *testing.T) { assert.Equal(t, 1, content.unfocusedTimes) } -func TestWindow_Clipboard(t *testing.T) { - w := 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 clipboardContent, if any - cb.SetContent(cliboardContent) -} - func TestWindow_ClipboardCopy_DisabledEntry(t *testing.T) { w := createWindow("Test").(*window) e := widget.NewEntry() @@ -1662,7 +1640,7 @@ func TestWindow_ClipboardCopy_DisabledEntry(t *testing.T) { w.keyPressed(nil, glfw.KeyC, 0, glfw.Repeat, ctrlMod) w.WaitForEvents() - assert.Equal(t, "Testing", w.Clipboard().Content()) + assert.Equal(t, "Testing", NewClipboard().Content()) e.SetText("Testing2") e.DoubleTapped(nil) @@ -1673,14 +1651,14 @@ func TestWindow_ClipboardCopy_DisabledEntry(t *testing.T) { w.WaitForEvents() assert.Equal(t, "Testing2", e.Text) - assert.Equal(t, "Testing", w.Clipboard().Content()) + assert.Equal(t, "Testing", NewClipboard().Content()) // any other shortcut should be forbidden (Paste) w.keyPressed(nil, glfw.KeyV, 0, glfw.Repeat, ctrlMod) w.WaitForEvents() assert.Equal(t, "Testing2", e.Text) - assert.Equal(t, "Testing", w.Clipboard().Content()) + assert.Equal(t, "Testing", NewClipboard().Content()) } func TestWindow_CloseInterception(t *testing.T) { diff --git a/internal/driver/mobile/clipboard.go b/internal/driver/mobile/clipboard.go index d83a6c6308..d08f1b3ae4 100644 --- a/internal/driver/mobile/clipboard.go +++ b/internal/driver/mobile/clipboard.go @@ -5,7 +5,11 @@ import ( ) // Declare conformity with Clipboard interface -var _ fyne.Clipboard = (*mobileClipboard)(nil) +var _ fyne.Clipboard = mobileClipboard{} + +func NewClipboard() fyne.Clipboard { + return mobileClipboard{} +} // mobileClipboard represents the system mobileClipboard type mobileClipboard struct { diff --git a/internal/driver/mobile/clipboard_android.go b/internal/driver/mobile/clipboard_android.go index a15cd72196..8b9b19c2f7 100644 --- a/internal/driver/mobile/clipboard_android.go +++ b/internal/driver/mobile/clipboard_android.go @@ -18,7 +18,7 @@ import ( ) // Content returns the clipboard content for Android -func (c *mobileClipboard) Content() string { +func (c mobileClipboard) Content() string { content := "" app.RunOnJVM(func(vm, env, ctx uintptr) error { chars := C.getClipboardContent(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx)) @@ -34,7 +34,7 @@ func (c *mobileClipboard) Content() string { } // SetContent sets the clipboard content for Android -func (c *mobileClipboard) SetContent(content string) { +func (c mobileClipboard) SetContent(content string) { contentStr := C.CString(content) defer C.free(unsafe.Pointer(contentStr)) diff --git a/internal/driver/mobile/clipboard_desktop.go b/internal/driver/mobile/clipboard_desktop.go index 3860d388cd..76e8b91f3c 100644 --- a/internal/driver/mobile/clipboard_desktop.go +++ b/internal/driver/mobile/clipboard_desktop.go @@ -5,12 +5,12 @@ package mobile import "fyne.io/fyne/v2" // Content returns the clipboard content for mobile simulator runs -func (c *mobileClipboard) Content() string { +func (c mobileClipboard) Content() string { fyne.LogError("Clipboard is not supported in mobile simulation", nil) return "" } // SetContent sets the clipboard content for mobile simulator runs -func (c *mobileClipboard) SetContent(content string) { +func (c mobileClipboard) SetContent(content string) { fyne.LogError("Clipboard is not supported in mobile simulation", nil) } diff --git a/internal/driver/mobile/clipboard_ios.go b/internal/driver/mobile/clipboard_ios.go index 7ac97efd86..1ff2c99d6f 100644 --- a/internal/driver/mobile/clipboard_ios.go +++ b/internal/driver/mobile/clipboard_ios.go @@ -15,14 +15,14 @@ import "C" import "unsafe" // Content returns the clipboard content for iOS -func (c *mobileClipboard) Content() string { +func (c mobileClipboard) Content() string { content := C.getClipboardContent() return C.GoString(content) } // SetContent sets the clipboard content for iOS -func (c *mobileClipboard) SetContent(content string) { +func (c mobileClipboard) SetContent(content string) { contentStr := C.CString(content) defer C.free(unsafe.Pointer(contentStr)) diff --git a/internal/driver/mobile/driver.go b/internal/driver/mobile/driver.go index 41675ee581..5f7dd9b559 100644 --- a/internal/driver/mobile/driver.go +++ b/internal/driver/mobile/driver.go @@ -100,6 +100,10 @@ func (d *driver) currentWindow() *window { return last } +func (d *driver) Clipboard() fyne.Clipboard { + return NewClipboard() +} + func (d *driver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) { return painter.RenderedTextSize(text, textSize, style, source) } diff --git a/internal/driver/mobile/window.go b/internal/driver/mobile/window.go index aaa5d0b406..b119429cae 100644 --- a/internal/driver/mobile/window.go +++ b/internal/driver/mobile/window.go @@ -19,11 +19,10 @@ type window struct { onCloseIntercepted func() isChild bool - clipboard fyne.Clipboard - canvas *canvas - icon fyne.Resource - menu *fyne.MainMenu - handle uintptr // the window handle - currently just Android + canvas *canvas + icon fyne.Resource + menu *fyne.MainMenu + handle uintptr // the window handle - currently just Android } func (w *window) Title() string { @@ -203,10 +202,7 @@ func (w *window) Canvas() fyne.Canvas { } func (w *window) Clipboard() fyne.Clipboard { - if w.clipboard == nil { - w.clipboard = &mobileClipboard{} - } - return w.clipboard + return NewClipboard() } func (w *window) RunWithContext(f func()) { diff --git a/test/app.go b/test/app.go index 4e5e427a59..0969eaaa39 100644 --- a/test/app.go +++ b/test/app.go @@ -63,6 +63,10 @@ func (a *app) Quit() { // no-op } +func (a *app) Clipboard() fyne.Clipboard { + return NewClipboard() +} + func (a *app) UniqueID() string { return "testApp" // TODO should this be randomised? } diff --git a/test/driver.go b/test/driver.go index e4a30dc06d..9e093c484d 100644 --- a/test/driver.go +++ b/test/driver.go @@ -116,6 +116,10 @@ func (d *driver) Quit() { // no-op } +func (d *driver) Clipboard() fyne.Clipboard { + return nil +} + func (d *driver) removeWindow(w *window) { d.windowsMutex.Lock() i := 0 diff --git a/test/window.go b/test/window.go index 36b17dc7e1..84ec631c18 100644 --- a/test/window.go +++ b/test/window.go @@ -14,10 +14,9 @@ type window struct { onClosed func() onCloseIntercepted func() - canvas *canvas - clipboard clipboard - driver *driver - menu *fyne.MainMenu + canvas *canvas + driver *driver + menu *fyne.MainMenu } // NewTempWindow creates and registers a new window for test purposes. @@ -46,7 +45,7 @@ func (w *window) CenterOnScreen() { } func (w *window) Clipboard() fyne.Clipboard { - return &w.clipboard + return NewClipboard() } func (w *window) Close() { diff --git a/theme/themedtestapp_test.go b/theme/themedtestapp_test.go index e5b8a5668e..7bd5dbdd76 100644 --- a/theme/themedtestapp_test.go +++ b/theme/themedtestapp_test.go @@ -106,3 +106,7 @@ func (t *themedApp) ShowAnimations() bool { func (t *themedApp) AddChangeListener(chan fyne.Settings) { } + +func (t *themedApp) Clipboard() fyne.Clipboard { + return nil +} diff --git a/widget/entry.go b/widget/entry.go index 1d0fd93f3d..0f6b6198ba 100644 --- a/widget/entry.go +++ b/widget/entry.go @@ -589,7 +589,7 @@ func (e *Entry) TappedSecondary(pe *fyne.PointEvent) { } e.requestFocus() - clipboard := fyne.CurrentApp().Driver().AllWindows()[0].Clipboard() + clipboard := fyne.CurrentApp().Clipboard() super := e.super() undoItem := fyne.NewMenuItem(lang.L("Undo"), e.Undo) diff --git a/widget/entry_internal_test.go b/widget/entry_internal_test.go index 3982741138..f3ffe6a995 100644 --- a/widget/entry_internal_test.go +++ b/widget/entry_internal_test.go @@ -351,7 +351,7 @@ func TestEntry_PasteFromClipboard(t *testing.T) { testContent := "test" - clipboard := fyne.CurrentApp().Driver().AllWindows()[0].Clipboard() + clipboard := fyne.CurrentApp().Clipboard() clipboard.SetContent(testContent) entry.pasteFromClipboard(clipboard) @@ -372,7 +372,7 @@ func TestEntry_PasteFromClipboard_MultilineWrapping(t *testing.T) { assert.Equal(t, 0, entry.CursorRow) assert.Equal(t, 1, entry.CursorColumn) - clipboard := fyne.CurrentApp().Driver().AllWindows()[0].Clipboard() + clipboard := fyne.CurrentApp().Clipboard() clipboard.SetContent("esting entry") entry.pasteFromClipboard(clipboard) @@ -397,17 +397,11 @@ func TestEntry_PasteFromClipboardValidation(t *testing.T) { return nil } - w := test.NewApp().NewWindow("") - defer w.Close() - w.SetContent(entry) - testContent := "test" - - clipboard := fyne.CurrentApp().Driver().AllWindows()[0].Clipboard() + clipboard := test.NewTempApp(t).Clipboard() clipboard.SetContent(testContent) entry.pasteFromClipboard(clipboard) - assert.Equal(t, 2, triggered) } diff --git a/window.go b/window.go index e47bf778dc..bc7492c9ef 100644 --- a/window.go +++ b/window.go @@ -101,5 +101,7 @@ type Window interface { Canvas() Canvas // Clipboard returns the system clipboard + // + // Deprecated: use App.Clipboard() instead. Clipboard() Clipboard }