Skip to content

Commit

Permalink
Merge pull request #5121 from pierrec/issue4418
Browse files Browse the repository at this point in the history
Decouple the Clipboard API from fyne.Window
  • Loading branch information
andydotxyz authored Oct 4, 2024
2 parents 68e61b1 + 50b23bb commit 4fab19a
Show file tree
Hide file tree
Showing 25 changed files with 124 additions and 90 deletions.
5 changes: 5 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
15 changes: 10 additions & 5 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion app/app_gl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion app/app_mobile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion app/app_software.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
25 changes: 24 additions & 1 deletion app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
4 changes: 4 additions & 0 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 11 additions & 11 deletions cmd/fyne_demo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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") }
Expand Down Expand Up @@ -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)
}
}
14 changes: 9 additions & 5 deletions internal/driver/glfw/clipboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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)
})
Expand Down
10 changes: 7 additions & 3 deletions internal/driver/glfw/clipboard_goxjs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ 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 {
window *glfw.Window
}

// Content returns the clipboard content
func (c *clipboard) Content() string {
func (c clipboard) Content() string {
content := ""
runOnMain(func() {
content, _ = c.window.GetClipboardString()
Expand All @@ -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)
})
Expand Down
12 changes: 6 additions & 6 deletions internal/driver/glfw/window.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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(),
}
}
}
Expand Down
28 changes: 3 additions & 25 deletions internal/driver/glfw/window_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand All @@ -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) {
Expand Down
6 changes: 5 additions & 1 deletion internal/driver/mobile/clipboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions internal/driver/mobile/clipboard_android.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))

Expand Down
4 changes: 2 additions & 2 deletions internal/driver/mobile/clipboard_desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading

0 comments on commit 4fab19a

Please sign in to comment.