From 83427a87d90bbd17755c81bf65dea05c7f9eb3d5 Mon Sep 17 00:00:00 2001 From: pierre Date: Sun, 8 Sep 2024 10:13:22 +0200 Subject: [PATCH 01/50] Add fyne.App.Clipboard method, fixes #4418 --- app.go | 5 +++++ app/app.go | 15 ++++++++++----- app/app_gl.go | 2 +- app/app_mobile.go | 2 +- app_test.go | 4 ++++ cmd/fyne_demo/main.go | 24 ++++++++---------------- internal/driver/glfw/clipboard.go | 2 ++ internal/driver/glfw/driver.go | 4 ++++ internal/driver/mobile/clipboard.go | 2 ++ internal/driver/mobile/driver.go | 4 ++++ test/app.go | 4 ++++ test/driver.go | 4 ++++ theme/themedtestapp_test.go | 4 ++++ window.go | 2 ++ 14 files changed, 55 insertions(+), 23 deletions(-) diff --git a/app.go b/app.go index 74ac0bf89a..eadd1c8cd5 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..7072e33849 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.Clipboard, id) } diff --git a/app/app_mobile.go b/app/app_mobile.go index 0303c202a2..d7b7287f91 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.MobileClipboard, id) d.(mobile.ConfiguredDriver).SetOnConfigurationChanged(func(c *mobile.Configuration) { internalapp.SystemTheme = c.SystemTheme 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..07b6cbbbeb 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, 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, 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, w.Canvas().Focused()) }) pasteItem.Shortcut = pasteShortcut performFind := func() { fmt.Println("Menu Find") } @@ -251,16 +251,8 @@ 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) { - switch sh := s.(type) { - case *fyne.ShortcutCopy: - sh.Clipboard = w.Clipboard() - case *fyne.ShortcutCut: - sh.Clipboard = w.Clipboard() - case *fyne.ShortcutPaste: - sh.Clipboard = w.Clipboard() - } - if focused, ok := w.Canvas().Focused().(fyne.Shortcutable); ok { +func shortcutFocused(s fyne.Shortcut, f fyne.Focusable) { + 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..a8a9e7d31e 100644 --- a/internal/driver/glfw/clipboard.go +++ b/internal/driver/glfw/clipboard.go @@ -14,6 +14,8 @@ import ( // Declare conformity with Clipboard interface var _ fyne.Clipboard = (*clipboard)(nil) +var Clipboard clipboard + // clipboard represents the system clipboard type clipboard struct{} diff --git a/internal/driver/glfw/driver.go b/internal/driver/glfw/driver.go index 5f9f254b58..3aa4de6d9d 100644 --- a/internal/driver/glfw/driver.go +++ b/internal/driver/glfw/driver.go @@ -68,6 +68,10 @@ func toOSIcon(icon []byte) ([]byte, error) { return buf.Bytes(), nil } +func (d *gLDriver) Clipboard() fyne.Clipboard { + return &clipboard{} +} + func (d *gLDriver) 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/clipboard.go b/internal/driver/mobile/clipboard.go index d83a6c6308..7e1168dfac 100644 --- a/internal/driver/mobile/clipboard.go +++ b/internal/driver/mobile/clipboard.go @@ -7,6 +7,8 @@ import ( // Declare conformity with Clipboard interface var _ fyne.Clipboard = (*mobileClipboard)(nil) +var MobileClipboard mobileClipboard + // mobileClipboard represents the system mobileClipboard type mobileClipboard struct { } diff --git a/internal/driver/mobile/driver.go b/internal/driver/mobile/driver.go index 41675ee581..49c306df5c 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 &mobileClipboard{} +} + 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/test/app.go b/test/app.go index 4e5e427a59..d06318dafc 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 nil +} + 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/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/window.go b/window.go index 3e366acb96..604d05c598 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 } From 2999a08aa25695faccccf9b2ea32d7077350fc22 Mon Sep 17 00:00:00 2001 From: pierre Date: Tue, 10 Sep 2024 08:40:55 +0200 Subject: [PATCH 02/50] add missing Clipboard for various platforms; update tests --- app/app_software.go | 2 +- app/app_test.go | 22 +++++++++++++++++++ internal/driver/glfw/clipboard_goxjs.go | 2 ++ internal/driver/glfw/window_test.go | 28 +++---------------------- test/clipboard.go | 2 ++ widget/entry.go | 2 +- widget/entry_internal_test.go | 4 ++-- 7 files changed, 33 insertions(+), 29 deletions(-) diff --git a/app/app_software.go b/app/app_software.go index ccdcb097cf..ead475812d 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.Clipboard, id) } diff --git a/app/app_test.go b/app/app_test.go index ce67ca2cca..afa20931e8 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -48,3 +48,25 @@ func TestFyneApp_SetIcon(t *testing.T) { assert.Equal(t, setIcon, app.Icon()) } + +func TestFynaApp_Clipboard(t *testing.T) { + app := NewWithID("io.fyne.test") + + 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/internal/driver/glfw/clipboard_goxjs.go b/internal/driver/glfw/clipboard_goxjs.go index 239158f746..094ff0dd14 100644 --- a/internal/driver/glfw/clipboard_goxjs.go +++ b/internal/driver/glfw/clipboard_goxjs.go @@ -10,6 +10,8 @@ import ( // Declare conformity with Clipboard interface var _ fyne.Clipboard = (*clipboard)(nil) +var Clipboard clipboard + // clipboard represents the system clipboard type clipboard struct { window *glfw.Window diff --git a/internal/driver/glfw/window_test.go b/internal/driver/glfw/window_test.go index 00c5e13222..5ab7ae99ce 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", Clipboard.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", Clipboard.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", Clipboard.Content()) } func TestWindow_CloseInterception(t *testing.T) { diff --git a/test/clipboard.go b/test/clipboard.go index db37f3c154..2cd4408714 100644 --- a/test/clipboard.go +++ b/test/clipboard.go @@ -2,6 +2,8 @@ package test import "fyne.io/fyne/v2" +var Clipboard clipboard + type clipboard struct { content string } diff --git a/widget/entry.go b/widget/entry.go index 3e42c3a184..953123c529 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 8af9f2f617..85d7c7e277 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) From dd612d76b538d00faf5a7d158d7cacc64116b75d Mon Sep 17 00:00:00 2001 From: pierre Date: Wed, 11 Sep 2024 08:02:49 +0200 Subject: [PATCH 03/50] test: return proper Clipboard --- test/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/app.go b/test/app.go index d06318dafc..b611c1328c 100644 --- a/test/app.go +++ b/test/app.go @@ -64,7 +64,7 @@ func (a *app) Quit() { } func (a *app) Clipboard() fyne.Clipboard { - return nil + return &Clipboard } func (a *app) UniqueID() string { From 3482cae356eb01d5496db68f178b8298ee65b777 Mon Sep 17 00:00:00 2001 From: pierre Date: Thu, 12 Sep 2024 18:44:22 +0200 Subject: [PATCH 04/50] Clipboard: remove singleton --- internal/driver/glfw/clipboard.go | 4 +++- internal/driver/glfw/clipboard_goxjs.go | 4 +++- internal/driver/glfw/driver.go | 4 ---- internal/driver/glfw/window.go | 12 ++++++------ internal/driver/glfw/window_test.go | 6 +++--- internal/driver/mobile/clipboard.go | 4 +++- internal/driver/mobile/window.go | 14 +++++--------- test/app.go | 2 +- test/clipboard.go | 2 -- test/window.go | 9 ++++----- 10 files changed, 28 insertions(+), 33 deletions(-) diff --git a/internal/driver/glfw/clipboard.go b/internal/driver/glfw/clipboard.go index a8a9e7d31e..570a04f721 100644 --- a/internal/driver/glfw/clipboard.go +++ b/internal/driver/glfw/clipboard.go @@ -14,7 +14,9 @@ import ( // Declare conformity with Clipboard interface var _ fyne.Clipboard = (*clipboard)(nil) -var Clipboard clipboard +func NewClipboard() fyne.Clipboard { + return &clipboard{} +} // clipboard represents the system clipboard type clipboard struct{} diff --git a/internal/driver/glfw/clipboard_goxjs.go b/internal/driver/glfw/clipboard_goxjs.go index 094ff0dd14..4322d6d9e3 100644 --- a/internal/driver/glfw/clipboard_goxjs.go +++ b/internal/driver/glfw/clipboard_goxjs.go @@ -10,7 +10,9 @@ import ( // Declare conformity with Clipboard interface var _ fyne.Clipboard = (*clipboard)(nil) -var Clipboard clipboard +func NewClipboard() fyne.Clipboard { + return &clipboard{} +} // clipboard represents the system clipboard type clipboard struct { diff --git a/internal/driver/glfw/driver.go b/internal/driver/glfw/driver.go index 3aa4de6d9d..5f9f254b58 100644 --- a/internal/driver/glfw/driver.go +++ b/internal/driver/glfw/driver.go @@ -68,10 +68,6 @@ func toOSIcon(icon []byte) ([]byte, error) { return buf.Bytes(), nil } -func (d *gLDriver) Clipboard() fyne.Clipboard { - return &clipboard{} -} - func (d *gLDriver) 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/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 5ab7ae99ce..645a4ac606 100644 --- a/internal/driver/glfw/window_test.go +++ b/internal/driver/glfw/window_test.go @@ -1640,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", Clipboard.Content()) + assert.Equal(t, "Testing", NewClipboard().Content()) e.SetText("Testing2") e.DoubleTapped(nil) @@ -1651,14 +1651,14 @@ func TestWindow_ClipboardCopy_DisabledEntry(t *testing.T) { w.WaitForEvents() assert.Equal(t, "Testing2", e.Text) - assert.Equal(t, "Testing", 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", 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 7e1168dfac..3fe3a87705 100644 --- a/internal/driver/mobile/clipboard.go +++ b/internal/driver/mobile/clipboard.go @@ -7,7 +7,9 @@ import ( // Declare conformity with Clipboard interface var _ fyne.Clipboard = (*mobileClipboard)(nil) -var MobileClipboard mobileClipboard +func NewClipboard() fyne.Clipboard { + return &mobileClipboard{} +} // mobileClipboard represents the system mobileClipboard type mobileClipboard struct { 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 b611c1328c..0969eaaa39 100644 --- a/test/app.go +++ b/test/app.go @@ -64,7 +64,7 @@ func (a *app) Quit() { } func (a *app) Clipboard() fyne.Clipboard { - return &Clipboard + return NewClipboard() } func (a *app) UniqueID() string { diff --git a/test/clipboard.go b/test/clipboard.go index 2cd4408714..db37f3c154 100644 --- a/test/clipboard.go +++ b/test/clipboard.go @@ -2,8 +2,6 @@ package test import "fyne.io/fyne/v2" -var Clipboard clipboard - type clipboard struct { content string } 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() { From 992f11341678d1e6f3947f0c9a9f209b37a4f614 Mon Sep 17 00:00:00 2001 From: pierre Date: Thu, 12 Sep 2024 18:57:15 +0200 Subject: [PATCH 05/50] app: add missing NewClipboard() --- app/app_gl.go | 2 +- app/app_mobile.go | 2 +- app/app_software.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/app_gl.go b/app/app_gl.go index 7072e33849..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(), &glfw.Clipboard, id) + return newAppWithDriver(glfw.NewGLDriver(), glfw.NewClipboard(), id) } diff --git a/app/app_mobile.go b/app/app_mobile.go index d7b7287f91..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, &mobile.MobileClipboard, 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 ead475812d..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()), &test.Clipboard, id) + return newAppWithDriver(test.NewDriverWithPainter(software.NewPainter()), test.NewClipboard(), id) } From 616fff24ab77cfe0a5d6a1459d7b3b230012a6ed Mon Sep 17 00:00:00 2001 From: pierre Date: Thu, 12 Sep 2024 19:22:24 +0200 Subject: [PATCH 06/50] app: instantiate a window to initialize glfw for clipboard tests --- app/app_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app/app_test.go b/app/app_test.go index afa20931e8..6135d941d7 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -51,6 +51,7 @@ func TestFyneApp_SetIcon(t *testing.T) { func TestFynaApp_Clipboard(t *testing.T) { app := NewWithID("io.fyne.test") + app.NewWindow("test") text := "My content from test window" cb := app.Clipboard() From d6a142f5899afbd385adfe426eaeb742a8043ed7 Mon Sep 17 00:00:00 2001 From: pierre Date: Fri, 13 Sep 2024 07:53:20 +0200 Subject: [PATCH 07/50] app: Clipboard: use test.NewTempApp and NewTempWindow --- app/app_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/app_test.go b/app/app_test.go index 6135d941d7..0546042557 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/test" _ "fyne.io/fyne/v2/test" ) @@ -50,8 +51,8 @@ func TestFyneApp_SetIcon(t *testing.T) { } func TestFynaApp_Clipboard(t *testing.T) { - app := NewWithID("io.fyne.test") - app.NewWindow("test") + app := test.NewTempApp(t) + test.NewTempWindow(t, nil) text := "My content from test window" cb := app.Clipboard() From 395444b3c9638f0007981c1dbd9dbbad0ceeaa76 Mon Sep 17 00:00:00 2001 From: pierre Date: Sat, 14 Sep 2024 10:15:42 +0200 Subject: [PATCH 08/50] app: remove duplicat import decl --- app/app_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/app/app_test.go b/app/app_test.go index 0546042557..9d6f613ba4 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -7,7 +7,6 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/test" - _ "fyne.io/fyne/v2/test" ) func TestDummyApp(t *testing.T) { From a26d50222745c03c04d244ff270bd7f81cc87c7f Mon Sep 17 00:00:00 2001 From: pierre Date: Sat, 14 Sep 2024 10:50:29 +0200 Subject: [PATCH 09/50] Clipboard: do not return pointer for a clipboard instance --- internal/driver/glfw/clipboard.go | 12 ++++++------ internal/driver/glfw/clipboard_goxjs.go | 8 ++++---- internal/driver/mobile/clipboard.go | 4 ++-- internal/driver/mobile/clipboard_android.go | 4 ++-- internal/driver/mobile/clipboard_desktop.go | 4 ++-- internal/driver/mobile/clipboard_ios.go | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/internal/driver/glfw/clipboard.go b/internal/driver/glfw/clipboard.go index 570a04f721..cb0bfb3fa0 100644 --- a/internal/driver/glfw/clipboard.go +++ b/internal/driver/glfw/clipboard.go @@ -12,17 +12,17 @@ import ( ) // Declare conformity with Clipboard interface -var _ fyne.Clipboard = (*clipboard)(nil) +var _ fyne.Clipboard = clipboard{} func NewClipboard() fyne.Clipboard { - return &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() @@ -38,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() @@ -47,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) @@ -63,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 4322d6d9e3..e066a70dd5 100644 --- a/internal/driver/glfw/clipboard_goxjs.go +++ b/internal/driver/glfw/clipboard_goxjs.go @@ -8,10 +8,10 @@ import ( ) // Declare conformity with Clipboard interface -var _ fyne.Clipboard = (*clipboard)(nil) +var _ fyne.Clipboard = clipboard{} func NewClipboard() fyne.Clipboard { - return &clipboard{} + return clipboard{} } // clipboard represents the system clipboard @@ -20,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() @@ -29,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/mobile/clipboard.go b/internal/driver/mobile/clipboard.go index 3fe3a87705..d08f1b3ae4 100644 --- a/internal/driver/mobile/clipboard.go +++ b/internal/driver/mobile/clipboard.go @@ -5,10 +5,10 @@ import ( ) // Declare conformity with Clipboard interface -var _ fyne.Clipboard = (*mobileClipboard)(nil) +var _ fyne.Clipboard = mobileClipboard{} func NewClipboard() fyne.Clipboard { - return &mobileClipboard{} + return mobileClipboard{} } // mobileClipboard represents the system mobileClipboard 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)) From 6cdb4729f241f1366dd53333f610c612f1a30b1f Mon Sep 17 00:00:00 2001 From: Pierre Curto Date: Wed, 25 Sep 2024 11:33:54 +0200 Subject: [PATCH 10/50] internal/driver/mobile: use NewClipboard() instead of returning a new instance --- internal/driver/mobile/driver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/driver/mobile/driver.go b/internal/driver/mobile/driver.go index 49c306df5c..5f7dd9b559 100644 --- a/internal/driver/mobile/driver.go +++ b/internal/driver/mobile/driver.go @@ -101,7 +101,7 @@ func (d *driver) currentWindow() *window { } func (d *driver) Clipboard() fyne.Clipboard { - return &mobileClipboard{} + return NewClipboard() } func (d *driver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) { From da68bb0387705fb20ce5f7ed2d3b3fe910e09580 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Thu, 12 Sep 2024 10:17:06 +0100 Subject: [PATCH 11/50] Fix an indexing error on Darwin introduced some time ago --- dialog/file.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dialog/file.go b/dialog/file.go index 419fd73b05..68382d4848 100644 --- a/dialog/file.go +++ b/dialog/file.go @@ -895,7 +895,7 @@ func getFavoriteOrder() []string { } if runtime.GOOS == "darwin" { - order[4] = "Movies" + order[5] = "Movies" } return order From f8f87173b20dd021ca44da9c0c044a4cfc937fbf Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Thu, 12 Sep 2024 10:17:24 +0100 Subject: [PATCH 12/50] Remember last location and go back to it when file dialog is opened again --- dialog/file.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/dialog/file.go b/dialog/file.go index 68382d4848..c0a7b3a4c0 100644 --- a/dialog/file.go +++ b/dialog/file.go @@ -30,7 +30,10 @@ const ( GridView ) -const viewLayoutKey = "fyne:fileDialogViewLayout" +const ( + viewLayoutKey = "fyne:fileDialogViewLayout" + lastFolderKey = "fyne:fileDialogLastFolder" +) type textWidget interface { fyne.Widget @@ -438,6 +441,7 @@ func (f *fileDialog) setLocation(dir fyne.URI) error { return err } + fyne.CurrentApp().Preferences().SetString(lastFolderKey, dir.String()) isFav := false for i, fav := range f.favorites { if fav.loc == nil { @@ -665,7 +669,18 @@ func showFile(file *FileDialog) *fileDialog { d.win = widget.NewModalPopUp(ui, file.parent.Canvas()) d.win.Resize(size) - d.setLocation(file.effectiveStartingDir()) + starting := file.effectiveStartingDir() + lastPath := fyne.CurrentApp().Preferences().String(lastFolderKey) + if lastPath != "" { + parsed, err := storage.ParseURI(lastPath) + if err == nil { + dir, err := storage.ListerForURI(parsed) + if err == nil { + starting = dir + } + } + } + d.setLocation(starting) d.win.Show() if file.save { d.win.Canvas.Focus(d.fileName.(*widget.Entry)) From 5136aa3e60ae34fd2ebec995a4a22d279580cc2f Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Thu, 12 Sep 2024 10:23:45 +0100 Subject: [PATCH 13/50] Add test for remembering start dir --- dialog/file_test.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/dialog/file_test.go b/dialog/file_test.go index 3c0ae0e323..4e3373e96e 100644 --- a/dialog/file_test.go +++ b/dialog/file_test.go @@ -46,7 +46,6 @@ func comparePaths(t *testing.T, u1, u2 fyne.ListableURI) bool { } func TestEffectiveStartingDir(t *testing.T) { - homeString, err := os.UserHomeDir() if err != nil { t.Skipf("os.Gethome() failed, cannot run this test on this system (error stat()-ing ../) error was '%s'", err) @@ -105,7 +104,28 @@ func TestEffectiveStartingDir(t *testing.T) { t.Errorf("Expected effectiveStartingDir() to be '%s', but it was '%s'", expect, res) } +} + +func TestFileDialogStartRemember(t *testing.T) { + testPath, err := filepath.Abs("./testdata") + assert.Nil(t, err) + start, err := storage.ListerForURI(storage.NewFileURI(testPath)) + if err != nil { + t.Skipf("could not get lister for working directory: %s", err) + } + + w := test.NewTempWindow(t, widget.NewLabel("Content")) + d := NewFileOpen(nil, w) + d.SetLocation(start) + d.Show() + + assert.Equal(t, start.String(), d.dialog.dir.String()) + d.Hide() + d2 := NewFileOpen(nil, w) + d2.Show() + assert.Equal(t, start.String(), d.dialog.dir.String()) + d2.Hide() } func TestFileDialogResize(t *testing.T) { From f83fd29543669bf8cdc79e38c6da9780ffa8f4c0 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Thu, 12 Sep 2024 11:10:15 +0100 Subject: [PATCH 14/50] Fix order when code requests a starting directory --- dialog/file.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/dialog/file.go b/dialog/file.go index c0a7b3a4c0..bc9b4a8a49 100644 --- a/dialog/file.go +++ b/dialog/file.go @@ -623,6 +623,18 @@ func (f *FileDialog) effectiveStartingDir() fyne.ListableURI { } + // last used + lastPath := fyne.CurrentApp().Preferences().String(lastFolderKey) + if lastPath != "" { + parsed, err := storage.ParseURI(lastPath) + if err == nil { + dir, err := storage.ListerForURI(parsed) + if err == nil { + return dir + } + } + } + // Try app storage app := fyne.CurrentApp() if hasAppFiles(app) { @@ -669,18 +681,7 @@ func showFile(file *FileDialog) *fileDialog { d.win = widget.NewModalPopUp(ui, file.parent.Canvas()) d.win.Resize(size) - starting := file.effectiveStartingDir() - lastPath := fyne.CurrentApp().Preferences().String(lastFolderKey) - if lastPath != "" { - parsed, err := storage.ParseURI(lastPath) - if err == nil { - dir, err := storage.ListerForURI(parsed) - if err == nil { - starting = dir - } - } - } - d.setLocation(starting) + d.setLocation(file.effectiveStartingDir()) d.win.Show() if file.save { d.win.Canvas.Focus(d.fileName.(*widget.Entry)) From 06ced7303555df4b7873f7c0f6b29ed167748253 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Thu, 12 Sep 2024 18:16:16 +0100 Subject: [PATCH 15/50] Add missing doc for the last two additions --- dialog/file.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dialog/file.go b/dialog/file.go index bc9b4a8a49..738cfb7b78 100644 --- a/dialog/file.go +++ b/dialog/file.go @@ -605,6 +605,8 @@ func (f *fileDialog) getDataItem(id int) (fyne.URI, bool) { // // - file.startingDirectory if non-empty, os.Stat()-able, and uses the file:// // URI scheme +// - previously used file open/close folder within this app +// - the current app's document storage, if App.Storage() documents have been saved // - os.UserHomeDir() // - os.Getwd() // - "/" (should be filesystem root on all supported platforms) From c277e9b11e94e8bd85d3f54f43bc104250a07e4f Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Fri, 6 Sep 2024 21:17:02 +0100 Subject: [PATCH 16/50] Bring calendar in from fyne-x --- widget/calendar.go | 213 ++++++++++++++++++++++++++++++++++++++++ widget/calendar_test.go | 95 ++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 widget/calendar.go create mode 100644 widget/calendar_test.go diff --git a/widget/calendar.go b/widget/calendar.go new file mode 100644 index 0000000000..a17dd56e42 --- /dev/null +++ b/widget/calendar.go @@ -0,0 +1,213 @@ +package widget + +import ( + "math" + "strconv" + "strings" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" +) + +// Declare conformity with Layout interface +var _ fyne.Layout = (*calendarLayout)(nil) + +const ( + daysPerWeek = 7 + maxWeeksPerMonth = 6 +) + +// Calendar creates a new date time picker which returns a time object +// +// Since: 2.6 +type Calendar struct { + BaseWidget + currentTime time.Time + + monthPrevious *Button + monthNext *Button + monthLabel *Label + + dates *fyne.Container + + OnChanged func(time.Time) `json:"-"` +} + +// NewCalendar creates a calendar instance +// +// Since: 2.6 +func NewCalendar(cT time.Time, changed func(time.Time)) *Calendar { + c := &Calendar{ + currentTime: cT, + OnChanged: changed, + } + + c.ExtendBaseWidget(c) + + return c +} + +// CreateRenderer returns a new WidgetRenderer for this widget. +// This should not be called by regular code, it is used internally to render a widget. +func (c *Calendar) CreateRenderer() fyne.WidgetRenderer { + c.monthPrevious = NewButtonWithIcon("", theme.NavigateBackIcon(), func() { + c.currentTime = c.currentTime.AddDate(0, -1, 0) + // Dates are 'normalised', forcing date to start from the start of the month ensures move from March to February + c.currentTime = time.Date(c.currentTime.Year(), c.currentTime.Month(), 1, 0, 0, 0, 0, c.currentTime.Location()) + c.monthLabel.SetText(c.monthYear()) + c.dates.Objects = c.calendarObjects() + }) + c.monthPrevious.Importance = LowImportance + + c.monthNext = NewButtonWithIcon("", theme.NavigateNextIcon(), func() { + c.currentTime = c.currentTime.AddDate(0, 1, 0) + c.monthLabel.SetText(c.monthYear()) + c.dates.Objects = c.calendarObjects() + }) + c.monthNext.Importance = LowImportance + + c.monthLabel = NewLabel(c.monthYear()) + + nav := &fyne.Container{Layout: layout.NewBorderLayout(nil, nil, c.monthPrevious, c.monthNext), + Objects: []fyne.CanvasObject{c.monthPrevious, c.monthNext, + &fyne.Container{Layout: layout.NewCenterLayout(), Objects: []fyne.CanvasObject{c.monthLabel}}}} + + c.dates = &fyne.Container{Layout: newCalendarLayout(), Objects: c.calendarObjects()} + + dateContainer := &fyne.Container{Layout: layout.NewBorderLayout(nav, nil, nil, nil), + Objects: []fyne.CanvasObject{nav, c.dates}} + + return NewSimpleRenderer(dateContainer) +} + +func (c *Calendar) calendarObjects() []fyne.CanvasObject { + var columnHeadings []fyne.CanvasObject + for i := 0; i < daysPerWeek; i++ { + j := i + 1 + if j == daysPerWeek { + j = 0 + } + + t := NewLabel(strings.ToUpper(time.Weekday(j).String()[:3])) + t.Alignment = fyne.TextAlignCenter + columnHeadings = append(columnHeadings, t) + } + columnHeadings = append(columnHeadings, c.daysOfMonth()...) + + return columnHeadings +} + +func (c *Calendar) dateForButton(dayNum int) time.Time { + oldName, off := c.currentTime.Zone() + return time.Date(c.currentTime.Year(), c.currentTime.Month(), dayNum, c.currentTime.Hour(), c.currentTime.Minute(), 0, 0, time.FixedZone(oldName, off)).In(c.currentTime.Location()) +} + +func (c *Calendar) daysOfMonth() []fyne.CanvasObject { + start := time.Date(c.currentTime.Year(), c.currentTime.Month(), 1, 0, 0, 0, 0, c.currentTime.Location()) + var buttons []fyne.CanvasObject + + //account for Go time pkg starting on sunday at index 0 + dayIndex := int(start.Weekday()) + if dayIndex == 0 { + dayIndex += daysPerWeek + } + + //add spacers if week doesn't start on Monday + for i := 0; i < dayIndex-1; i++ { + buttons = append(buttons, layout.NewSpacer()) + } + + for d := start; d.Month() == start.Month(); d = d.AddDate(0, 0, 1) { + + dayNum := d.Day() + s := strconv.Itoa(dayNum) + b := NewButton(s, func() { + + selectedDate := c.dateForButton(dayNum) + + c.OnChanged(selectedDate) + }) + b.Importance = LowImportance + + buttons = append(buttons, b) + } + + return buttons +} + +func (c *Calendar) monthYear() string { + return c.currentTime.Format("January 2006") +} + +type calendarLayout struct { + cellSize fyne.Size +} + +func newCalendarLayout() fyne.Layout { + return &calendarLayout{} +} + +// Layout is called to pack all child objects into a specified size. +// For a calendar grid this will pack objects into a table format. +func (g *calendarLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + weeks := 1 + day := 0 + for i, child := range objects { + if !child.Visible() { + continue + } + + if day%daysPerWeek == 0 && i >= daysPerWeek { + weeks++ + } + day++ + } + + g.cellSize = fyne.NewSize(size.Width/float32(daysPerWeek), + size.Height/float32(weeks)) + row, col := 0, 0 + i := 0 + for _, child := range objects { + if !child.Visible() { + continue + } + + lead := g.getLeading(row, col) + trail := g.getTrailing(row, col) + child.Move(lead) + child.Resize(fyne.NewSize(trail.X, trail.Y).Subtract(lead)) + + if (i+1)%daysPerWeek == 0 { + row++ + col = 0 + } else { + col++ + } + i++ + } +} + +// MinSize sets the minimum size for the calendar +func (g *calendarLayout) MinSize(_ []fyne.CanvasObject) fyne.Size { + pad := theme.Padding() + largestMin := NewLabel("22").MinSize() + return fyne.NewSize(largestMin.Width*daysPerWeek+pad*(daysPerWeek-1), + largestMin.Height*maxWeeksPerMonth+pad*(maxWeeksPerMonth-1)) +} + +// Get the leading edge position of a grid cell. +// The row and col specify where the cell is in the calendar. +func (g *calendarLayout) getLeading(row, col int) fyne.Position { + x := (g.cellSize.Width) * float32(col) + y := (g.cellSize.Height) * float32(row) + + return fyne.NewPos(float32(math.Round(float64(x))), float32(math.Round(float64(y)))) +} + +// Get the trailing edge position of a grid cell. +// The row and col specify where the cell is in the calendar. +func (g *calendarLayout) getTrailing(row, col int) fyne.Position { + return g.getLeading(row+1, col+1) +} diff --git a/widget/calendar_test.go b/widget/calendar_test.go new file mode 100644 index 0000000000..37233ca500 --- /dev/null +++ b/widget/calendar_test.go @@ -0,0 +1,95 @@ +package widget + +import ( + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/test" + "fyne.io/fyne/v2/widget" +) + +func TestNewCalendar(t *testing.T) { + now := time.Now() + c := NewCalendar(now, func(time.Time) {}) + assert.Equal(t, now.Day(), c.currentTime.Day()) + assert.Equal(t, int(now.Month()), int(c.currentTime.Month())) + assert.Equal(t, now.Year(), c.currentTime.Year()) + + _ = test.WidgetRenderer(c) // and render + assert.Equal(t, now.Format("January 2006"), c.monthLabel.Text) +} + +func TestNewCalendar_ButtonDate(t *testing.T) { + date := time.Now() + c := NewCalendar(date, func(time.Time) {}) + _ = test.WidgetRenderer(c) // and render + + endNextMonth := date.AddDate(0, 1, 0).AddDate(0, 0, -(date.Day() - 1)) + last := endNextMonth.AddDate(0, 0, -1) + + firstDate := firstDateButton(c.dates) + assert.Equal(t, "1", firstDate.Text) + lastDate := c.dates.Objects[len(c.dates.Objects)-1].(*widget.Button) + assert.Equal(t, strconv.Itoa(last.Day()), lastDate.Text) +} + +func TestNewCalendar_Next(t *testing.T) { + date := time.Now() + c := NewCalendar(date, func(time.Time) {}) + _ = test.WidgetRenderer(c) // and render + + assert.Equal(t, date.Format("January 2006"), c.monthLabel.Text) + + test.Tap(c.monthNext) + date = date.AddDate(0, 1, 0) + assert.Equal(t, date.Format("January 2006"), c.monthLabel.Text) +} + +func TestNewCalendar_Previous(t *testing.T) { + date := time.Now() + c := NewCalendar(date, func(time.Time) {}) + _ = test.WidgetRenderer(c) // and render + + assert.Equal(t, date.Format("January 2006"), c.monthLabel.Text) + + test.Tap(c.monthPrevious) + date = date.AddDate(0, -1, 0) + assert.Equal(t, date.Format("January 2006"), c.monthLabel.Text) +} + +func TestNewCalendar_Resize(t *testing.T) { + date := time.Now() + c := NewCalendar(date, func(time.Time) {}) + r := test.WidgetRenderer(c) // and render + layout := c.dates.Layout.(*calendarLayout) + + baseSize := c.MinSize() + r.Layout(baseSize) + min := layout.cellSize + + r.Layout(baseSize.AddWidthHeight(100, 0)) + assert.Greater(t, layout.cellSize.Width, min.Width) + assert.Equal(t, layout.cellSize.Height, min.Height) + + r.Layout(baseSize.AddWidthHeight(0, 100)) + assert.Equal(t, layout.cellSize.Width, min.Width) + assert.Greater(t, layout.cellSize.Height, min.Height) + + r.Layout(baseSize.AddWidthHeight(100, 100)) + assert.Greater(t, layout.cellSize.Width, min.Width) + assert.Greater(t, layout.cellSize.Height, min.Height) +} + +func firstDateButton(c *fyne.Container) *widget.Button { + for _, b := range c.Objects { + if nonBlank, ok := b.(*widget.Button); ok { + return nonBlank + } + } + + return nil +} From 916592c7ccb040cf90f7f857715e4dcaaa99d814 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Fri, 6 Sep 2024 21:55:26 +0100 Subject: [PATCH 17/50] Add basic DateEntry type with a static format for picking or typing a date --- cmd/fyne_demo/tutorials/icons.go | 1 + cmd/fyne_demo/tutorials/widget.go | 3 + theme/bundled-icons.go | 5 ++ theme/gen.go | 1 + theme/icons.go | 19 +++- widget/date_entry.go | 142 ++++++++++++++++++++++++++++++ 6 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 widget/date_entry.go diff --git a/cmd/fyne_demo/tutorials/icons.go b/cmd/fyne_demo/tutorials/icons.go index d136737472..fa5fcb8551 100644 --- a/cmd/fyne_demo/tutorials/icons.go +++ b/cmd/fyne_demo/tutorials/icons.go @@ -187,6 +187,7 @@ func loadIcons() []iconInfo { {"VolumeUp", theme.VolumeUpIcon()}, {"AccountIcon", theme.AccountIcon()}, + {"CalendarIcon", theme.CalendarIcon()}, {"LoginIcon", theme.LoginIcon()}, {"LogoutIcon", theme.LogoutIcon()}, diff --git a/cmd/fyne_demo/tutorials/widget.go b/cmd/fyne_demo/tutorials/widget.go index f0c6a005bd..a5bd58ab78 100644 --- a/cmd/fyne_demo/tutorials/widget.go +++ b/cmd/fyne_demo/tutorials/widget.go @@ -374,6 +374,8 @@ func makeInputTab(_ fyne.Window) fyne.CanvasObject { "Option Z", }) selectEntry.PlaceHolder = "Type or select" + dateEntry := widget.NewDateEntry() + dateEntry.PlaceHolder = "Choose a date" disabledCheck := widget.NewCheck("Disabled check", func(bool) {}) disabledCheck.Disable() checkGroup := widget.NewCheckGroup([]string{"CheckGroup Item 1", "CheckGroup Item 2"}, func(s []string) { fmt.Println("selected", s) }) @@ -388,6 +390,7 @@ func makeInputTab(_ fyne.Window) fyne.CanvasObject { return container.NewVBox( widget.NewSelect([]string{"Option 1", "Option 2", "Option 3"}, func(s string) { fmt.Println("selected", s) }), selectEntry, + dateEntry, widget.NewCheck("Check", func(on bool) { fmt.Println("checked", on) }), disabledCheck, checkGroup, diff --git a/theme/bundled-icons.go b/theme/bundled-icons.go index f16676a82b..85d89ca658 100644 --- a/theme/bundled-icons.go +++ b/theme/bundled-icons.go @@ -450,6 +450,11 @@ var accountIconRes = &fyne.StaticResource{ StaticContent: []byte(""), } +var calendarIconRes = &fyne.StaticResource{ + StaticName: "calendar.svg", + StaticContent: []byte("\n \n\n"), +} + var loginIconRes = &fyne.StaticResource{ StaticName: "login.svg", StaticContent: []byte(""), diff --git a/theme/gen.go b/theme/gen.go index f99906dd83..39c2ad7993 100644 --- a/theme/gen.go +++ b/theme/gen.go @@ -192,6 +192,7 @@ func main() { bundleIcon("upload", f) bundleIcon("account", f) + bundleIcon("calendar", f) bundleIcon("login", f) bundleIcon("logout", f) diff --git a/theme/icons.go b/theme/icons.go index 2082c60cb9..a0fc817849 100644 --- a/theme/icons.go +++ b/theme/icons.go @@ -456,6 +456,11 @@ const ( // Since: 2.1 IconNameAccount fyne.ThemeIconName = "account" + // IconNameCalendar is the name of theme lookup for calendar icon. + // + // Since: 2.6 + IconNameCalendar fyne.ThemeIconName = "calendar" + // IconNameLogin is the name of theme lookup for login icon. // // Since: 2.1 @@ -599,9 +604,10 @@ var ( IconNameStorage: NewThemedResource(storageIconRes), IconNameUpload: NewThemedResource(uploadIconRes), - IconNameAccount: NewThemedResource(accountIconRes), - IconNameLogin: NewThemedResource(loginIconRes), - IconNameLogout: NewThemedResource(logoutIconRes), + IconNameAccount: NewThemedResource(accountIconRes), + IconNameCalendar: NewThemedResource(calendarIconRes), + IconNameLogin: NewThemedResource(loginIconRes), + IconNameLogout: NewThemedResource(logoutIconRes), IconNameList: NewThemedResource(listIconRes), IconNameGrid: NewThemedResource(gridIconRes), @@ -1285,6 +1291,13 @@ func AccountIcon() fyne.Resource { return safeIconLookup(IconNameAccount) } +// CalendarIcon returns a resource containing the standard account icon for the current theme +// +// Since: 2.6 +func CalendarIcon() fyne.Resource { + return safeIconLookup(IconNameCalendar) +} + // LoginIcon returns a resource containing the standard login icon for the current theme func LoginIcon() fyne.Resource { return safeIconLookup(IconNameLogin) diff --git a/widget/date_entry.go b/widget/date_entry.go new file mode 100644 index 0000000000..e3a4d03872 --- /dev/null +++ b/widget/date_entry.go @@ -0,0 +1,142 @@ +package widget + +import ( + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +const dateFormat = "02 Jan 2006" + +// DateEntry is an input field which supports selecting from a fixed set of options. +// +// Since: 2.6 +type DateEntry struct { + Entry + Date time.Time + OnChanged func(time.Time) `json:"-"` + + dropDown *Calendar + popUp *PopUp +} + +// NewDateEntry creates a date input where the date can be selected or typed. +// +// Since: 2.6 +func NewDateEntry() *DateEntry { + e := &DateEntry{} + e.ExtendBaseWidget(e) + e.Wrapping = fyne.TextWrap(fyne.TextTruncateClip) + return e +} + +// CreateRenderer returns a new renderer for this select entry. +// +// Implements: fyne.Widget +func (e *DateEntry) CreateRenderer() fyne.WidgetRenderer { + e.ExtendBaseWidget(e) + e.Validator = func(in string) error { + _, err := time.Parse(dateFormat, in) + return err + } + e.Entry.OnChanged = func(in string) { + t, err := time.Parse(dateFormat, in) + if err != nil { + return + } + + e.Date = t + + if f := e.OnChanged; f != nil { + f(t) + } + } + + if e.ActionItem == nil { + e.ActionItem = e.setupDropDown() + if e.Disabled() { + e.ActionItem.(fyne.Disableable).Disable() + } + } + + return e.Entry.CreateRenderer() +} + +// Enable this widget, updating any style or features appropriately. +// +// Implements: fyne.DisableableWidget +func (e *DateEntry) Enable() { + if e.ActionItem != nil { + e.ActionItem.(fyne.Disableable).Enable() + } + e.Entry.Enable() +} + +// Disable this widget so that it cannot be interacted with, updating any style appropriately. +// +// Implements: fyne.DisableableWidget +func (e *DateEntry) Disable() { + if e.ActionItem != nil { + e.ActionItem.(fyne.Disableable).Disable() + } + e.Entry.Disable() +} + +// MinSize returns the minimal size of the select entry. +// +// Implements: fyne.Widget +func (e *DateEntry) MinSize() fyne.Size { + e.ExtendBaseWidget(e) + return e.Entry.MinSize() +} + +// Move changes the relative position of the date entry. +// +// Implements: fyne.Widget +func (e *DateEntry) Move(pos fyne.Position) { + e.Entry.Move(pos) + if e.popUp != nil { + e.popUp.Move(e.popUpPos()) + } +} + +// Resize changes the size of the date entry. +// +// Implements: fyne.Widget +func (e *DateEntry) Resize(size fyne.Size) { + e.Entry.Resize(size) + if e.popUp != nil { + e.popUp.Resize(fyne.NewSize(size.Width, e.popUp.Size().Height)) + } +} + +func (e *DateEntry) popUpPos() fyne.Position { + entryPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(e.super()) + return entryPos.Add(fyne.NewPos(0, e.Size().Height-e.Theme().Size(theme.SizeNameInputBorder))) +} + +func (e *DateEntry) setDate(d time.Time) { + e.Date = d + if e.popUp != nil { + e.popUp.Hide() + } + + e.Entry.SetText(d.Format(dateFormat)) +} + +func (e *DateEntry) setupDropDown() *Button { + if e.dropDown == nil { + e.dropDown = NewCalendar(time.Now(), e.setDate) + } + dropDownButton := NewButton("", func() { + c := fyne.CurrentApp().Driver().CanvasForObject(e.super()) + + e.popUp = NewPopUp(e.dropDown, c) + e.popUp.ShowAtPosition(e.popUpPos()) + e.popUp.Resize(fyne.NewSize(e.Size().Width, e.popUp.MinSize().Height)) + }) + dropDownButton.Importance = LowImportance + dropDownButton.SetIcon(e.Theme().Icon(theme.IconNameCalendar)) + return dropDownButton +} From 36ff5fdbee14a84f9f4d3ac34b69dee2933bee36 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Sat, 7 Sep 2024 20:36:28 +0100 Subject: [PATCH 18/50] Add missed icon --- theme/icons/calendar.svg | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 theme/icons/calendar.svg diff --git a/theme/icons/calendar.svg b/theme/icons/calendar.svg new file mode 100644 index 0000000000..6c448c22aa --- /dev/null +++ b/theme/icons/calendar.svg @@ -0,0 +1,5 @@ + + + From c2b04f700913375e26b8965ce853c3edc202b625 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Sat, 7 Sep 2024 20:54:36 +0100 Subject: [PATCH 19/50] Fix accidental import loop --- widget/calendar_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/widget/calendar_test.go b/widget/calendar_test.go index 37233ca500..68203aacf8 100644 --- a/widget/calendar_test.go +++ b/widget/calendar_test.go @@ -9,7 +9,6 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/test" - "fyne.io/fyne/v2/widget" ) func TestNewCalendar(t *testing.T) { @@ -33,7 +32,7 @@ func TestNewCalendar_ButtonDate(t *testing.T) { firstDate := firstDateButton(c.dates) assert.Equal(t, "1", firstDate.Text) - lastDate := c.dates.Objects[len(c.dates.Objects)-1].(*widget.Button) + lastDate := c.dates.Objects[len(c.dates.Objects)-1].(*Button) assert.Equal(t, strconv.Itoa(last.Day()), lastDate.Text) } @@ -84,9 +83,9 @@ func TestNewCalendar_Resize(t *testing.T) { assert.Greater(t, layout.cellSize.Height, min.Height) } -func firstDateButton(c *fyne.Container) *widget.Button { +func firstDateButton(c *fyne.Container) *Button { for _, b := range c.Objects { - if nonBlank, ok := b.(*widget.Button); ok { + if nonBlank, ok := b.(*Button); ok { return nonBlank } } From 23f0724017f9160cd94cbc970772814c4b338abb Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Thu, 12 Sep 2024 11:45:22 +0100 Subject: [PATCH 20/50] Allow Date to be nil --- widget/date_entry.go | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/widget/date_entry.go b/widget/date_entry.go index e3a4d03872..366314560a 100644 --- a/widget/date_entry.go +++ b/widget/date_entry.go @@ -14,8 +14,8 @@ const dateFormat = "02 Jan 2006" // Since: 2.6 type DateEntry struct { Entry - Date time.Time - OnChanged func(time.Time) `json:"-"` + Date *time.Time + OnChanged func(*time.Time) `json:"-"` dropDown *Calendar popUp *PopUp @@ -41,15 +41,22 @@ func (e *DateEntry) CreateRenderer() fyne.WidgetRenderer { return err } e.Entry.OnChanged = func(in string) { + if in == "" { + e.Date = nil + + if f := e.OnChanged; f != nil { + f(nil) + } + } t, err := time.Parse(dateFormat, in) if err != nil { return } - e.Date = t + e.Date = &t if f := e.OnChanged; f != nil { - f(t) + f(&t) } } @@ -111,13 +118,22 @@ func (e *DateEntry) Resize(size fyne.Size) { } } +func (e *DateEntry) SetDate(d *time.Time) { + if d == nil { + e.Date = nil + e.Entry.SetText("") + } + + e.setDate(*d) +} + func (e *DateEntry) popUpPos() fyne.Position { entryPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(e.super()) return entryPos.Add(fyne.NewPos(0, e.Size().Height-e.Theme().Size(theme.SizeNameInputBorder))) } func (e *DateEntry) setDate(d time.Time) { - e.Date = d + e.Date = &d if e.popUp != nil { e.popUp.Hide() } From 106cdeb00bd885e7ce4f7221d56a26b5f083a06c Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Thu, 12 Sep 2024 17:41:36 +0100 Subject: [PATCH 21/50] Fix warnings --- widget/date_entry.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/widget/date_entry.go b/widget/date_entry.go index 366314560a..0a9cc4ede7 100644 --- a/widget/date_entry.go +++ b/widget/date_entry.go @@ -118,10 +118,14 @@ func (e *DateEntry) Resize(size fyne.Size) { } } +// SetDate will update the widget to a specific date. +// You can pass nil to unselect a date. func (e *DateEntry) SetDate(d *time.Time) { if d == nil { e.Date = nil e.Entry.SetText("") + + return } e.setDate(*d) From 7064f5c17f3c2bdcb1ce8f13fb8763b3392b4ec3 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Thu, 12 Sep 2024 18:06:40 +0100 Subject: [PATCH 22/50] Localise weekday names --- lang/translations/base.en.json | 17 ++++++++++++++++- widget/calendar.go | 10 +++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lang/translations/base.en.json b/lang/translations/base.en.json index 559985864d..58535ebeaf 100644 --- a/lang/translations/base.en.json +++ b/lang/translations/base.en.json @@ -28,5 +28,20 @@ }, "file.parent": { "other": "Parent" - } + }, + + "monday": "Monday", + "monday.short": "Mon", + "tuesday": "Tuesday", + "tuesday.short": "Tue", + "wednesday": "Wednesday", + "wednesday.short": "Wed", + "thursday": "Thursday", + "thursday.short": "Thu", + "friday": "Friday", + "friday.short": "Fri", + "saturday": "Saturday", + "saturday.short": "Sat", + "sunday": "Sunday", + "sunday.short": "Sun" } diff --git a/widget/calendar.go b/widget/calendar.go index a17dd56e42..1c7c1ceaf9 100644 --- a/widget/calendar.go +++ b/widget/calendar.go @@ -7,6 +7,7 @@ import ( "time" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/lang" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" ) @@ -90,7 +91,7 @@ func (c *Calendar) calendarObjects() []fyne.CanvasObject { j = 0 } - t := NewLabel(strings.ToUpper(time.Weekday(j).String()[:3])) + t := NewLabel(shortDayName(time.Weekday(j).String())) t.Alignment = fyne.TextAlignCenter columnHeadings = append(columnHeadings, t) } @@ -211,3 +212,10 @@ func (g *calendarLayout) getLeading(row, col int) fyne.Position { func (g *calendarLayout) getTrailing(row, col int) fyne.Position { return g.getLeading(row+1, col+1) } + +func shortDayName(in string) string { + lower := strings.ToLower(in) + key := lower + ".short" + long := lang.X(lower, in) + return strings.ToUpper(lang.X(key, long[:3])) +} From 18fe7b3293dbde52fe21ed128650490b32e63dd8 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Thu, 12 Sep 2024 18:06:56 +0100 Subject: [PATCH 23/50] Don't assume disablable --- widget/date_entry.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/widget/date_entry.go b/widget/date_entry.go index 0a9cc4ede7..4bb415b235 100644 --- a/widget/date_entry.go +++ b/widget/date_entry.go @@ -75,7 +75,9 @@ func (e *DateEntry) CreateRenderer() fyne.WidgetRenderer { // Implements: fyne.DisableableWidget func (e *DateEntry) Enable() { if e.ActionItem != nil { - e.ActionItem.(fyne.Disableable).Enable() + if d, ok := e.ActionItem.(fyne.Disableable); ok { + d.Enable() + } } e.Entry.Enable() } @@ -85,7 +87,9 @@ func (e *DateEntry) Enable() { // Implements: fyne.DisableableWidget func (e *DateEntry) Disable() { if e.ActionItem != nil { - e.ActionItem.(fyne.Disableable).Disable() + if d, ok := e.ActionItem.(fyne.Disableable); ok { + d.Disable() + } } e.Entry.Disable() } From bfc8ecc2bbdee61aca7a9d79c241e524a3e0c291 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Thu, 12 Sep 2024 18:07:22 +0100 Subject: [PATCH 24/50] Only create the minSize label once --- widget/calendar.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/widget/calendar.go b/widget/calendar.go index 1c7c1ceaf9..9116a62d7b 100644 --- a/widget/calendar.go +++ b/widget/calendar.go @@ -20,6 +20,8 @@ const ( maxWeeksPerMonth = 6 ) +var minCellContent = NewLabel("22") + // Calendar creates a new date time picker which returns a time object // // Since: 2.6 @@ -193,7 +195,7 @@ func (g *calendarLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { // MinSize sets the minimum size for the calendar func (g *calendarLayout) MinSize(_ []fyne.CanvasObject) fyne.Size { pad := theme.Padding() - largestMin := NewLabel("22").MinSize() + largestMin := minCellContent.MinSize() return fyne.NewSize(largestMin.Width*daysPerWeek+pad*(daysPerWeek-1), largestMin.Height*maxWeeksPerMonth+pad*(maxWeeksPerMonth-1)) } From 1d90c9fe9e4d7cbd17cfd4aa8663e1ce35f14e27 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Fri, 13 Sep 2024 17:43:39 +0100 Subject: [PATCH 25/50] Remove a few excess whitelines --- widget/calendar.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/widget/calendar.go b/widget/calendar.go index 9116a62d7b..5761ecd697 100644 --- a/widget/calendar.go +++ b/widget/calendar.go @@ -48,7 +48,6 @@ func NewCalendar(cT time.Time, changed func(time.Time)) *Calendar { } c.ExtendBaseWidget(c) - return c } @@ -123,11 +122,9 @@ func (c *Calendar) daysOfMonth() []fyne.CanvasObject { } for d := start; d.Month() == start.Month(); d = d.AddDate(0, 0, 1) { - dayNum := d.Day() s := strconv.Itoa(dayNum) b := NewButton(s, func() { - selectedDate := c.dateForButton(dayNum) c.OnChanged(selectedDate) From 144528e4a77d653c7d4190bd307e275364f97c20 Mon Sep 17 00:00:00 2001 From: pierre Date: Sat, 14 Sep 2024 11:44:07 +0200 Subject: [PATCH 26/50] widget.Entry: validate when pasting from clipboard Fixes #5058. --- widget/entry.go | 1 + widget/entry_internal_test.go | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/widget/entry.go b/widget/entry.go index 953123c529..0f6b6198ba 100644 --- a/widget/entry.go +++ b/widget/entry.go @@ -1122,6 +1122,7 @@ func (e *Entry) pasteFromClipboard(clipboard fyne.Clipboard) { cb := e.OnChanged e.propertyLock.Unlock() + e.validate() if cb != nil { cb(content) // We know that the text has changed. } diff --git a/widget/entry_internal_test.go b/widget/entry_internal_test.go index 85d7c7e277..07552b8f4b 100644 --- a/widget/entry_internal_test.go +++ b/widget/entry_internal_test.go @@ -389,6 +389,28 @@ func TestEntry_PasteFromClipboard_MultilineWrapping(t *testing.T) { assert.Equal(t, 7, entry.CursorColumn) } +func TestEntry_PasteFromClipboardValidation(t *testing.T) { + entry := NewEntry() + var triggered int + entry.Validator = func(s string) error { + triggered++ + return nil + } + + w := test.NewApp().NewWindow("") + defer w.Close() + w.SetContent(entry) + + testContent := "test" + + clipboard := fyne.CurrentApp().Driver().AllWindows()[0].Clipboard() + clipboard.SetContent(testContent) + + entry.pasteFromClipboard(clipboard) + + assert.Equal(t, 2, triggered) +} + func TestEntry_PlaceholderTextStyle(t *testing.T) { e := NewEntry() e.TextStyle = fyne.TextStyle{Bold: true, Italic: true} From fb10b2b1b30b9107ec943a5f7160cebb7e5b4526 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Sun, 15 Sep 2024 22:01:33 +0100 Subject: [PATCH 27/50] Fix issue where about replace code could cause items to remove if refreshed during runtime --- internal/driver/glfw/menu_darwin.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/driver/glfw/menu_darwin.m b/internal/driver/glfw/menu_darwin.m index 66157be086..b40be98252 100644 --- a/internal/driver/glfw/menu_darwin.m +++ b/internal/driver/glfw/menu_darwin.m @@ -83,11 +83,14 @@ void handleException(const char* m, id e) { exceptionCallback([[NSString stringWithFormat:@"%s failed: %@", m, e] UTF8String]); } +int replacedAbout = 0; + const void* insertDarwinMenuItem(const void* m, const char* label, const char* keyEquivalent, unsigned int keyEquivalentModifierMask, int nextId, int index, bool isSeparator, const void *imageData, unsigned int imageDataLength) { NSMenu* menu = (NSMenu*)m; NSMenuItem* item; - if (strcmp(label, "About") == 0) { + if (strcmp(label, "About") == 0 && !replacedAbout) { + replacedAbout = 1; item = [menu itemArray][0]; [item setAction:@selector(tapped:)]; [item setTarget:[FyneMenuHandler class]]; From 6be64f2a8003374b87bba09a6d2edf999b46b76b Mon Sep 17 00:00:00 2001 From: pierre Date: Sat, 14 Sep 2024 20:49:38 +0200 Subject: [PATCH 28/50] cache: use one lock/unclock cycle to remove expired items. Fixes #5129. --- internal/cache/base.go | 31 ++++++------------------------- internal/cache/base_test.go | 1 - internal/cache/svg.go | 9 ++------- internal/cache/text.go | 11 ++--------- 4 files changed, 10 insertions(+), 42 deletions(-) diff --git a/internal/cache/base.go b/internal/cache/base.go index c42cb3ce2a..b68d4bab36 100644 --- a/internal/cache/base.go +++ b/internal/cache/base.go @@ -12,7 +12,6 @@ var ( cacheDuration = 1 * time.Minute cleanTaskInterval = cacheDuration / 2 - expiredObjects = make([]fyne.CanvasObject, 0, 50) lastClean time.Time skippedCleanWithCanvasRefresh = false @@ -161,45 +160,27 @@ func ResetThemeCaches() { // destroyExpiredCanvases deletes objects from the canvases cache. func destroyExpiredCanvases(now time.Time) { - expiredObjects = expiredObjects[:0] - canvasesLock.RLock() + canvasesLock.Lock() for obj, cinfo := range canvases { if cinfo.isExpired(now) { - expiredObjects = append(expiredObjects, obj) - } - } - canvasesLock.RUnlock() - if len(expiredObjects) > 0 { - canvasesLock.Lock() - for i, exp := range expiredObjects { - delete(canvases, exp) - expiredObjects[i] = nil + delete(canvases, obj) } - canvasesLock.Unlock() } + canvasesLock.Unlock() } // destroyExpiredRenderers deletes the renderer from the cache and calls // renderer.Destroy() func destroyExpiredRenderers(now time.Time) { - expiredObjects = expiredObjects[:0] - renderersLock.RLock() + renderersLock.Lock() for wid, rinfo := range renderers { if rinfo.isExpired(now) { rinfo.renderer.Destroy() overrides.Delete(wid) - expiredObjects = append(expiredObjects, wid) + delete(renderers, wid) } } - renderersLock.RUnlock() - if len(expiredObjects) > 0 { - renderersLock.Lock() - for i, exp := range expiredObjects { - delete(renderers, exp.(fyne.Widget)) - expiredObjects[i] = nil - } - renderersLock.Unlock() - } + renderersLock.Unlock() } // matchesACanvas returns true if the canvas represented by the canvasInfo object matches one of diff --git a/internal/cache/base_test.go b/internal/cache/base_test.go index adffe67209..f87ad62767 100644 --- a/internal/cache/base_test.go +++ b/internal/cache/base_test.go @@ -264,7 +264,6 @@ func (t *timeMock) setTime(min, sec int) { } func testClearAll() { - expiredObjects = make([]fyne.CanvasObject, 0, 50) skippedCleanWithCanvasRefresh = false canvases = make(map[fyne.CanvasObject]*canvasInfo, 1024) svgs.Range(func(key, _ any) bool { diff --git a/internal/cache/svg.go b/internal/cache/svg.go index 94911acfca..b21fb711cb 100644 --- a/internal/cache/svg.go +++ b/internal/cache/svg.go @@ -46,18 +46,13 @@ type svgInfo struct { // destroyExpiredSvgs destroys expired svgs cache data. func destroyExpiredSvgs(now time.Time) { - expiredSvgs := make([]string, 0, 20) svgs.Range(func(key, value any) bool { - s, sinfo := key.(string), value.(*svgInfo) + sinfo := value.(*svgInfo) if sinfo.isExpired(now) { - expiredSvgs = append(expiredSvgs, s) + svgs.Delete(key) } return true }) - - for _, exp := range expiredSvgs { - svgs.Delete(exp) - } } func overriddenName(name string, o fyne.CanvasObject) string { diff --git a/internal/cache/text.go b/internal/cache/text.go index f41bb55b67..0cbf754e9f 100644 --- a/internal/cache/text.go +++ b/internal/cache/text.go @@ -58,18 +58,11 @@ func SetFontMetrics(text string, fontSize float32, style fyne.TextStyle, source // destroyExpiredFontMetrics destroys expired fontSizeCache entries func destroyExpiredFontMetrics(now time.Time) { - expiredObjs := make([]fontSizeEntry, 0, 50) - fontSizeLock.RLock() + fontSizeLock.Lock() for k, v := range fontSizeCache { if v.isExpired(now) { - expiredObjs = append(expiredObjs, k) + delete(fontSizeCache, k) } } - fontSizeLock.RUnlock() - - fontSizeLock.Lock() - for _, k := range expiredObjs { - delete(fontSizeCache, k) - } fontSizeLock.Unlock() } From 72e71797232d9fcd3212ca03a5d1fe4b9d2acc6c Mon Sep 17 00:00:00 2001 From: Jonathan Hall Date: Mon, 9 Sep 2024 20:27:43 +0200 Subject: [PATCH 29/50] test: Add RenderObjectToMarkup and RenderToMarkup functions --- test/test.go | 20 ++++++++++++++++++++ test/test_test.go | 18 ++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/test/test.go b/test/test.go index 2989d6236b..7cc288dc3f 100644 --- a/test/test.go +++ b/test/test.go @@ -47,6 +47,19 @@ func AssertObjectRendersToImage(t *testing.T, masterFilename string, o fyne.Canv return AssertRendersToImage(t, masterFilename, c, msgAndArgs...) } +// RenderObjectToMarkup renders the given [fyne.io/fyne/v2.CanvasObject] to a markup string. +// +// Since: 2.6 +func RenderObjectToMarkup(o fyne.CanvasObject) string { + c := NewCanvas() + c.SetPadded(false) + size := o.MinSize().Max(o.Size()) + c.SetContent(o) + c.Resize(size) // ensure we are large enough for current size + + return snapshot(c) +} + // AssertObjectRendersToMarkup asserts that the given `CanvasObject` renders the same markup as the one stored in the master file. // The master filename is relative to the `testdata` directory which is relative to the test. // The test `t` fails if the rendered markup is not equal to the loaded master markup. @@ -89,6 +102,13 @@ func AssertRendersToImage(t *testing.T, masterFilename string, c fyne.Canvas, ms return test.AssertImageMatches(t, masterFilename, c.Capture(), msgAndArgs...) } +// RenderToMarkup renders the given [fyne.io/fyne/v2.Canvas] to a markup string. +// +// Since: 2.6 +func RenderToMarkup(c fyne.Canvas) string { + return snapshot(c) +} + // AssertRendersToMarkup asserts that the given canvas renders the same markup as the one stored in the master file. // The master filename is relative to the `testdata` directory which is relative to the test. // The test `t` fails if the rendered markup is not equal to the loaded master markup. diff --git a/test/test_test.go b/test/test_test.go index 71eef68587..cc18a685e1 100644 --- a/test/test_test.go +++ b/test/test_test.go @@ -39,6 +39,15 @@ func TestAssertObjectRendersToImage(t *testing.T) { test.AssertObjectRendersToImage(t, "circle.png", obj) } +func TestRenderObjectToMarkup(t *testing.T) { + obj := canvas.NewCircle(color.Black) + obj.Resize(fyne.NewSize(20, 20)) + + want, err := os.ReadFile("testdata/circle.xml") + require.NoError(t, err) + assert.Equal(t, string(want), test.RenderObjectToMarkup(obj), "existing master is equal to rendered markup") +} + func TestAssertObjectRendersToMarkup(t *testing.T) { obj := canvas.NewCircle(color.Black) obj.Resize(fyne.NewSize(20, 20)) @@ -82,6 +91,15 @@ func TestAssertRendersToImage(t *testing.T) { } } +func TestRenderToMarkup(t *testing.T) { + c := test.NewCanvas() + c.SetContent(canvas.NewCircle(color.Black)) + + want, err := os.ReadFile("testdata/markup_master.xml") + require.NoError(t, err) + assert.Equal(t, string(want), test.RenderToMarkup(c), "existing master is equal to rendered markup") +} + func TestAssertRendersToMarkup(t *testing.T) { c := test.NewCanvas() c.SetContent(canvas.NewCircle(color.Black)) From 32a94235085111c99c1a3b1d8377d4f2b5389bc9 Mon Sep 17 00:00:00 2001 From: Jonathan Hall Date: Fri, 13 Sep 2024 13:21:59 +0200 Subject: [PATCH 30/50] Work around Windows line endings in tests --- test/test_test.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/test_test.go b/test/test_test.go index cc18a685e1..c4c515ab13 100644 --- a/test/test_test.go +++ b/test/test_test.go @@ -1,8 +1,10 @@ package test_test import ( + "bytes" "image/color" "os" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -45,7 +47,10 @@ func TestRenderObjectToMarkup(t *testing.T) { want, err := os.ReadFile("testdata/circle.xml") require.NoError(t, err) - assert.Equal(t, string(want), test.RenderObjectToMarkup(obj), "existing master is equal to rendered markup") + // Fix Windows newlines + want = bytes.ReplaceAll(want, []byte("\r\n"), []byte("\n")) + got := strings.ReplaceAll(test.RenderObjectToMarkup(obj), "\r\n", "\n") + assert.Equal(t, string(want), got, "existing master is equal to rendered markup") } func TestAssertObjectRendersToMarkup(t *testing.T) { @@ -97,7 +102,10 @@ func TestRenderToMarkup(t *testing.T) { want, err := os.ReadFile("testdata/markup_master.xml") require.NoError(t, err) - assert.Equal(t, string(want), test.RenderToMarkup(c), "existing master is equal to rendered markup") + // Fix Windows newlines + want = bytes.ReplaceAll(want, []byte("\r\n"), []byte("\n")) + got := strings.ReplaceAll(test.RenderToMarkup(c), "\r\n", "\n") + assert.Equal(t, string(want), got, "existing master is equal to rendered markup") } func TestAssertRendersToMarkup(t *testing.T) { From 48025925da4a8b3f3c477fde9df5e3464d09e9d0 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Sun, 22 Sep 2024 13:31:01 +0100 Subject: [PATCH 31/50] Fix possible crash with badly formatted json translation --- lang/lang.go | 6 +++++- lang/translations/base.sv.json | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) delete mode 100644 lang/translations/base.sv.json diff --git a/lang/lang.go b/lang/lang.go index 50f15cdbc1..6a2e58bfc7 100644 --- a/lang/lang.go +++ b/lang/lang.go @@ -161,8 +161,12 @@ func AddTranslationsFS(fs embed.FS, dir string) (retErr error) { func addLanguage(data []byte, name string) error { f, err := bundle.ParseMessageFileBytes(data, name) + if err != nil { + return err + } + translated = append(translated, f.Tag) - return err + return nil } func init() { diff --git a/lang/translations/base.sv.json b/lang/translations/base.sv.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/lang/translations/base.sv.json +++ /dev/null @@ -1 +0,0 @@ -{} From dfdf8f7893390abecc9f8028e0e644e560d631a2 Mon Sep 17 00:00:00 2001 From: Alexis Hildebrandt Date: Fri, 20 Sep 2024 22:53:40 +0200 Subject: [PATCH 32/50] Add installDir (-o) option to get to allow users to specify the directory of where the got application should be installed. --- cmd/fyne/internal/commands/get.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/fyne/internal/commands/get.go b/cmd/fyne/internal/commands/get.go index f6af676c0d..d50b4cd3ed 100644 --- a/cmd/fyne/internal/commands/get.go +++ b/cmd/fyne/internal/commands/get.go @@ -35,6 +35,12 @@ func Get() *cli.Command { Usage: "For darwin and Windows targets an appID in the form of a reversed domain name is required, for ios this must match a valid provisioning profile", Destination: &g.AppID, }, + &cli.StringFlag{ + Name: "installDir", + Aliases: []string{"o"}, + Usage: "A specific location to install to, rather than the OS default.", + Destination: &g.installDir, + }, }, Action: func(ctx *cli.Context) error { if ctx.Args().Len() != 1 { @@ -50,6 +56,7 @@ func Get() *cli.Command { // Getter is the command that can handle downloading and installing Fyne apps to the current platform. type Getter struct { *appData + installDir string } // NewGetter returns a command that can handle the download and install of GUI apps built using Fyne. @@ -98,7 +105,7 @@ func (g *Getter) Get(pkg string) error { path = filepath.Join(path, dir) } - install := &Installer{appData: g.appData, srcDir: path, release: true} + install := &Installer{appData: g.appData, installDir: g.installDir, srcDir: path, release: true} if err := install.validate(); err != nil { return fmt.Errorf("failed to set up installer: %w", err) } From 8d2017e19e4d36953ab34c903d3b5c6720b39086 Mon Sep 17 00:00:00 2001 From: Jim Orcheson Date: Sun, 8 Sep 2024 07:09:32 -0400 Subject: [PATCH 33/50] Add data binding to Select widget Add NewSelectWithData function. Add Select.Bind and Select.Unbind methods to bind a string to Select.Selected. --- widget/select.go | 68 +++++++++++++++++++++++++++++++++++++++++++ widget/select_test.go | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/widget/select.go b/widget/select.go index c1ac6114a4..ecd6a62466 100644 --- a/widget/select.go +++ b/widget/select.go @@ -1,10 +1,12 @@ package widget import ( + "fmt" "image/color" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/theme" ) @@ -24,6 +26,8 @@ type Select struct { PlaceHolder string OnChanged func(string) `json:"-"` + binder basicBinder + focused bool hovered bool popUp *PopUpMenu @@ -47,6 +51,26 @@ func NewSelect(options []string, changed func(string)) *Select { return s } +// NewSelectWithData returns a new select widget connected to the specified data source. +func NewSelectWithData(options []string, data binding.String) *Select { + sel := NewSelect(options, nil) + sel.Bind(data) + + return sel +} + +// Bind connects the specified data source to this select. +// The current value will be displayed and any changes in the data will cause the widget +// to update. +func (s *Select) Bind(data binding.String) { + s.binder.SetCallback(s.updateFromData) + s.binder.Bind(data) + + s.OnChanged = func(_ string) { + s.binder.CallWithData(s.writeData) + } +} + // ClearSelected clears the current option of the select widget. After // clearing the current option, the Select widget's PlaceHolder will // be displayed. @@ -239,6 +263,13 @@ func (s *Select) TypedRune(_ rune) { // intentionally left blank } +// Unbind disconnects any configured data source from this Select. +// The current value will remain at the last value of the data source. +func (s *Select) Unbind() { + s.OnChanged = nil + s.binder.Unbind() +} + func (s *Select) popUpPos() fyne.Position { buttonPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(s.super()) return buttonPos.Add(fyne.NewPos(0, s.Size().Height-s.Theme().Size(theme.SizeNameInputBorder))) @@ -279,6 +310,23 @@ func (s *Select) tapAnimation() { } } +func (s *Select) updateFromData(data binding.DataItem) { + if data == nil { + return + } + stringSource, ok := data.(binding.String) + if !ok { + return + } + + val, err := stringSource.Get() + if err != nil { + return + } + s.SetSelected(val) + +} + func (s *Select) updateSelected(text string) { s.Selected = text @@ -289,6 +337,26 @@ func (s *Select) updateSelected(text string) { s.Refresh() } +func (s *Select) writeData(data binding.DataItem) { + if data == nil { + return + } + stringTarget, ok := data.(binding.String) + if !ok { + return + } + currentValue, err := stringTarget.Get() + if err != nil { + return + } + if currentValue != s.Selected { + err := stringTarget.Set(s.Selected) + if err != nil { + fyne.LogError(fmt.Sprintf("Failed to set binding value to %s", s.Selected), err) + } + } +} + type selectRenderer struct { icon *Icon label *RichText diff --git a/widget/select_test.go b/widget/select_test.go index 70c176bca3..f429b2109d 100644 --- a/widget/select_test.go +++ b/widget/select_test.go @@ -8,6 +8,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/test" @@ -25,6 +26,14 @@ func TestNewSelect(t *testing.T) { assert.Equal(t, "", combo.Selected) } +func TestNewSelectWithData(t *testing.T) { + data := binding.NewString() + combo := widget.NewSelectWithData([]string{"1", "2", "3"}, data) + + assert.Equal(t, 3, len(combo.Options)) + assert.Equal(t, "", combo.Selected) +} + func TestSelect_Align(t *testing.T) { test.NewTempApp(t) @@ -43,6 +52,52 @@ func TestSelect_Align(t *testing.T) { assertRendersToPlatformMarkup(t, "select/%s/trailing.xml", c) } +func TestSelect_Options(t *testing.T) { + s := widget.NewSelect([]string{"1", "2", "3"}, nil) + s.SetSelected("2") + assert.Equal(t, "2", s.Selected) + + s.SetOptions([]string{"4", "5"}) + assert.Equal(t, "2", s.Selected) + s.Selected = "" + assert.Equal(t, "", s.Selected) +} + +func TestSelect_Binding(t *testing.T) { + s := widget.NewSelect([]string{"1", "2", "3"}, nil) + s.SetSelected("2") + assert.Equal(t, "2", s.Selected) + waitForBinding() // this time it is the de-echo before binding + + str := binding.NewString() + s.Bind(str) + waitForBinding() + value, err := str.Get() + assert.Nil(t, err) + assert.Equal(t, "", value) + assert.Equal(t, "2", s.Selected) // no match to options, so keep previous value + + err = str.Set("3") + assert.Nil(t, err) + waitForBinding() + assert.Equal(t, "3", s.Selected) + + s.Unbind() + assert.Nil(t, s.OnChanged) + err = str.Set("1") + assert.Nil(t, err) + val1, err := str.Get() + assert.Nil(t, err) + assert.Equal(t, "1", val1) + assert.Equal(t, "3", s.Selected) + + s.SetSelected("2") + val1, err = str.Get() + assert.Nil(t, err) + assert.Equal(t, "1", val1) + assert.Equal(t, "2", s.Selected) +} + func TestSelect_ChangeTheme(t *testing.T) { test.NewTempApp(t) From 54e11f68fb5fb33b81e68e4ee378afae969f4743 Mon Sep 17 00:00:00 2001 From: jim Date: Sun, 8 Sep 2024 07:44:23 -0400 Subject: [PATCH 34/50] Update TeastNewSelectWithData Test that binding actually occurred. --- widget/select_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/widget/select_test.go b/widget/select_test.go index f429b2109d..aeb525ca37 100644 --- a/widget/select_test.go +++ b/widget/select_test.go @@ -32,6 +32,11 @@ func TestNewSelectWithData(t *testing.T) { assert.Equal(t, 3, len(combo.Options)) assert.Equal(t, "", combo.Selected) + + err := data.Set("2") + assert.Nil(t, err) + waitForBinding() + assert.Equal(t, "2", combo.Selected) } func TestSelect_Align(t *testing.T) { From 7eb34025de65506cb742c39357fbbdafc9e781d0 Mon Sep 17 00:00:00 2001 From: Jim Orcheson Date: Sun, 8 Sep 2024 12:05:59 -0400 Subject: [PATCH 35/50] Add Since line to public APIs --- widget/select.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/widget/select.go b/widget/select.go index ecd6a62466..b73390179f 100644 --- a/widget/select.go +++ b/widget/select.go @@ -52,6 +52,8 @@ func NewSelect(options []string, changed func(string)) *Select { } // NewSelectWithData returns a new select widget connected to the specified data source. +// +// Since 2.0 func NewSelectWithData(options []string, data binding.String) *Select { sel := NewSelect(options, nil) sel.Bind(data) @@ -62,6 +64,8 @@ func NewSelectWithData(options []string, data binding.String) *Select { // Bind connects the specified data source to this select. // The current value will be displayed and any changes in the data will cause the widget // to update. +// +// Since 2.0 func (s *Select) Bind(data binding.String) { s.binder.SetCallback(s.updateFromData) s.binder.Bind(data) @@ -265,6 +269,8 @@ func (s *Select) TypedRune(_ rune) { // Unbind disconnects any configured data source from this Select. // The current value will remain at the last value of the data source. +// +// Since 2.0 func (s *Select) Unbind() { s.OnChanged = nil s.binder.Unbind() From fff9c458f6ff76f8121bd32c86d7c00816f995ae Mon Sep 17 00:00:00 2001 From: Jim Orcheson Date: Mon, 9 Sep 2024 08:40:17 -0400 Subject: [PATCH 36/50] Change Since to 2.6 from 2.0 --- widget/select.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/widget/select.go b/widget/select.go index b73390179f..50a3f9c5c4 100644 --- a/widget/select.go +++ b/widget/select.go @@ -53,7 +53,7 @@ func NewSelect(options []string, changed func(string)) *Select { // NewSelectWithData returns a new select widget connected to the specified data source. // -// Since 2.0 +// Since 2.6 func NewSelectWithData(options []string, data binding.String) *Select { sel := NewSelect(options, nil) sel.Bind(data) @@ -65,7 +65,7 @@ func NewSelectWithData(options []string, data binding.String) *Select { // The current value will be displayed and any changes in the data will cause the widget // to update. // -// Since 2.0 +// Since 2.6 func (s *Select) Bind(data binding.String) { s.binder.SetCallback(s.updateFromData) s.binder.Bind(data) @@ -270,7 +270,7 @@ func (s *Select) TypedRune(_ rune) { // Unbind disconnects any configured data source from this Select. // The current value will remain at the last value of the data source. // -// Since 2.0 +// Since 2.6 func (s *Select) Unbind() { s.OnChanged = nil s.binder.Unbind() From 453d3cb176d9f60be7bf0c34b571ee036fecfd0a Mon Sep 17 00:00:00 2001 From: Jim Orcheson Date: Mon, 23 Sep 2024 09:22:32 -0400 Subject: [PATCH 37/50] Change Since 2.6 to Since: 2.6 --- widget/select.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/widget/select.go b/widget/select.go index 50a3f9c5c4..3e776c05d3 100644 --- a/widget/select.go +++ b/widget/select.go @@ -53,7 +53,7 @@ func NewSelect(options []string, changed func(string)) *Select { // NewSelectWithData returns a new select widget connected to the specified data source. // -// Since 2.6 +// Since: 2.6 func NewSelectWithData(options []string, data binding.String) *Select { sel := NewSelect(options, nil) sel.Bind(data) @@ -65,7 +65,7 @@ func NewSelectWithData(options []string, data binding.String) *Select { // The current value will be displayed and any changes in the data will cause the widget // to update. // -// Since 2.6 +// Since: 2.6 func (s *Select) Bind(data binding.String) { s.binder.SetCallback(s.updateFromData) s.binder.Bind(data) @@ -270,7 +270,7 @@ func (s *Select) TypedRune(_ rune) { // Unbind disconnects any configured data source from this Select. // The current value will remain at the last value of the data source. // -// Since 2.6 +// Since: 2.6 func (s *Select) Unbind() { s.OnChanged = nil s.binder.Unbind() From e30975aac1c2aa8902ad15b07baf29d95c540497 Mon Sep 17 00:00:00 2001 From: Peter Freund Date: Mon, 23 Sep 2024 20:28:29 +0200 Subject: [PATCH 38/50] Added translation using Weblate (Swedish) --- lang/translations/base.sv.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 lang/translations/base.sv.json diff --git a/lang/translations/base.sv.json b/lang/translations/base.sv.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lang/translations/base.sv.json @@ -0,0 +1 @@ +{} From 77386448c1944378a28f70e2f96fcffbb9216772 Mon Sep 17 00:00:00 2001 From: Peter Freund Date: Mon, 23 Sep 2024 18:30:13 +0000 Subject: [PATCH 39/50] Translated using Weblate (Swedish) Currently translated at 100.0% (39 of 39 strings) Translation: Fyne/Fyne Translate-URL: https://hosted.weblate.org/projects/fyne/fyne/sv/ --- lang/translations/base.sv.json | 46 +++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/lang/translations/base.sv.json b/lang/translations/base.sv.json index 0967ef424b..d22bcb8abc 100644 --- a/lang/translations/base.sv.json +++ b/lang/translations/base.sv.json @@ -1 +1,45 @@ -{} +{ + "OK": "OK", + "Save": "Spara", + "Advanced": "Avancerad", + "Confirm": "Bekräfta", + "Error": "Fel", + "Cancel": "Avbryt", + "Copy": "Kopiera", + "Create Folder": "Skapa mapp", + "Cut": "Klipp ut", + "Enter filename": "Ange filnamn", + "Favourites": "Favoriter", + "File": "Arkiv", + "Folder": "Mapp", + "New Folder": "Ny mapp", + "Open": "Öppna", + "Select all": "Markera allt", + "Undo": "Ångra", + "Yes": "Ja", + "file.name": { + "other": "Namn" + }, + "Show Hidden Files": "Visa dolda filer", + "thursday": "Torsdag", + "thursday.short": "Tor", + "friday": "Fredag", + "friday.short": "Fre", + "saturday": "Lördag", + "saturday.short": "Lör", + "sunday": "Söndag", + "sunday.short": "Sön", + "No": "Nej", + "Paste": "Klistra in", + "Quit": "Avsluta", + "Redo": "Gör om", + "monday": "Måndag", + "monday.short": "Mån", + "file.parent": { + "other": "Överordnad" + }, + "tuesday": "Tisdag", + "tuesday.short": "Tis", + "wednesday": "Onsdag", + "wednesday.short": "Ons" +} From 7b6f833fbe025ace97c2cdcfe3216e8944992c18 Mon Sep 17 00:00:00 2001 From: pierre Date: Tue, 17 Sep 2024 21:49:01 +0200 Subject: [PATCH 40/50] internal/driver/mobile: return storage.ErrNotExists instead of custom error Fixes #3353. --- internal/driver/mobile/file_android.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/driver/mobile/file_android.go b/internal/driver/mobile/file_android.go index b266c92916..1bbf82297e 100644 --- a/internal/driver/mobile/file_android.go +++ b/internal/driver/mobile/file_android.go @@ -25,6 +25,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/internal/driver/mobile/app" + "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/storage/repository" ) @@ -90,7 +91,7 @@ func nativeFileOpen(f *fileOpen) (io.ReadCloser, error) { ret := openStream(f.uri.String()) if ret == nil { - return nil, errors.New("resource not found at URI") + return nil, storage.ErrNotExists } stream := &javaStream{} @@ -122,7 +123,7 @@ func nativeFileSave(f *fileSave) (io.WriteCloser, error) { ret := saveStream(f.uri.String()) if ret == nil { - return nil, errors.New("resource not found at URI") + return nil, storage.ErrNotExists } stream := &javaStream{} From 219e79885f479fb1d3a0f86fc1924788bb3ec29e Mon Sep 17 00:00:00 2001 From: Jonathan Hall Date: Mon, 9 Sep 2024 21:41:37 +0200 Subject: [PATCH 41/50] Update root package GoDoc to include internal links --- animation.go | 2 +- app.go | 12 ++++++------ canvas.go | 4 ++-- canvasobject.go | 18 ++++++++--------- cloud.go | 2 +- container.go | 40 +++++++++++++++++++------------------- driver.go | 6 +++--- event.go | 4 ++-- geometry.go | 34 ++++++++++++++++---------------- layout.go | 6 +++--- menu.go | 4 ++-- notification.go | 2 +- overlay_stack.go | 2 +- resource.go | 8 ++++---- settings.go | 2 +- shortcut.go | 50 ++++++++++++++++++++++++------------------------ text.go | 4 ++-- uri.go | 16 ++++++++-------- widget.go | 16 ++++++++-------- window.go | 2 +- 20 files changed, 118 insertions(+), 116 deletions(-) diff --git a/animation.go b/animation.go index a8aeba12fb..2883774390 100644 --- a/animation.go +++ b/animation.go @@ -45,7 +45,7 @@ type Animation struct { } // NewAnimation creates a very basic animation where the callback function will be called for every -// rendered frame between time.Now() and the specified duration. The callback values start at 0.0 and +// rendered frame between [time.Now] and the specified duration. The callback values start at 0.0 and // will be 1.0 when the animation completes. // // Since: 2.0 diff --git a/app.go b/app.go index eadd1c8cd5..ac1343935c 100644 --- a/app.go +++ b/app.go @@ -7,9 +7,9 @@ import ( // An App is the definition of a graphical application. // Apps can have multiple windows, by default they will exit when all windows -// have been closed. This can be modified using SetMaster() or SetCloseIntercept(). -// To start an application you need to call Run() somewhere in your main() function. -// Alternatively use the window.ShowAndRun() function for your main window. +// have been closed. This can be modified using SetMaster or SetCloseIntercept. +// To start an application you need to call Run somewhere in your main function. +// Alternatively use the [fyne.io/fyne/v2.Window.ShowAndRun] function for your main window. type App interface { // Create a new window for the application. // The first window to open is considered the "master" and when closed @@ -27,7 +27,7 @@ type App interface { // SetIcon sets the icon resource used for this application instance. SetIcon(Resource) - // Run the application - this starts the event loop and waits until Quit() + // Run the application - this starts the event loop and waits until [App.Quit] // is called or the last window closes. // This should be called near the end of a main() function as it will block. Run() @@ -43,7 +43,7 @@ type App interface { Driver() Driver // UniqueID returns the application unique identifier, if set. - // This must be set for use of the Preferences() functions... see NewWithId(string) + // This must be set for use of the [App.Preferences]. see [NewWithID]. UniqueID() string // SendNotification sends a system notification that will be displayed in the operating system's notification area. @@ -75,7 +75,7 @@ type App interface { CloudProvider() CloudProvider // get the (if any) configured provider // SetCloudProvider allows developers to specify how this application should integrate with cloud services. - // See `fyne.io/cloud` package for implementation details. + // See [fyne.io/cloud] package for implementation details. // // Since: 2.3 SetCloudProvider(CloudProvider) // configure cloud for this app diff --git a/canvas.go b/canvas.go index 8869af25a0..8fd97d74cb 100644 --- a/canvas.go +++ b/canvas.go @@ -2,7 +2,7 @@ package fyne import "image" -// Canvas defines a graphical canvas to which a CanvasObject or Container can be added. +// Canvas defines a graphical canvas to which a [CanvasObject] or Container can be added. // Each canvas has a scale which is automatically applied during the render process. type Canvas interface { Content() CanvasObject @@ -31,7 +31,7 @@ type Canvas interface { // Size returns the current size of this canvas Size() Size // Scale returns the current scale (multiplication factor) this canvas uses to render - // The pixel size of a CanvasObject can be found by multiplying by this value. + // The pixel size of a [CanvasObject] can be found by multiplying by this value. Scale() float32 // Overlays returns the overlay stack. diff --git a/canvasobject.go b/canvasobject.go index 05ab7716e8..0566627285 100644 --- a/canvasobject.go +++ b/canvasobject.go @@ -36,7 +36,7 @@ type CanvasObject interface { Refresh() } -// Disableable describes any CanvasObject that can be disabled. +// Disableable describes any [CanvasObject] that can be disabled. // This is primarily used with objects that also implement the Tappable interface. type Disableable interface { Enable() @@ -44,19 +44,19 @@ type Disableable interface { Disabled() bool } -// DoubleTappable describes any CanvasObject that can also be double tapped. +// DoubleTappable describes any [CanvasObject] that can also be double tapped. type DoubleTappable interface { DoubleTapped(*PointEvent) } -// Draggable indicates that a CanvasObject can be dragged. +// Draggable indicates that a [CanvasObject] can be dragged. // This is used for any item that the user has indicated should be moved across the screen. type Draggable interface { Dragged(*DragEvent) DragEnd() } -// Focusable describes any CanvasObject that can respond to being focused. +// Focusable describes any [CanvasObject] that can respond to being focused. // It will receive the FocusGained and FocusLost events appropriately. // When focused it will also have TypedRune called as text is input and // TypedKey called when other keys are pressed. @@ -75,18 +75,18 @@ type Focusable interface { TypedKey(*KeyEvent) } -// Scrollable describes any CanvasObject that can also be scrolled. +// Scrollable describes any [CanvasObject] that can also be scrolled. // This is mostly used to implement the widget.ScrollContainer. type Scrollable interface { Scrolled(*ScrollEvent) } -// SecondaryTappable describes a CanvasObject that can be right-clicked or long-tapped. +// SecondaryTappable describes a [CanvasObject] that can be right-clicked or long-tapped. type SecondaryTappable interface { TappedSecondary(*PointEvent) } -// Shortcutable describes any CanvasObject that can respond to shortcut commands (quit, cut, copy, and paste). +// Shortcutable describes any [CanvasObject] that can respond to shortcut commands (quit, cut, copy, and paste). type Shortcutable interface { TypedShortcut(Shortcut) } @@ -95,12 +95,12 @@ type Shortcutable interface { // // Since: 2.1 type Tabbable interface { - // AcceptsTab() is a hook called by the key press handling logic. + // AcceptsTab is a hook called by the key press handling logic. // If it returns true then the Tab key events will be sent using TypedKey. AcceptsTab() bool } -// Tappable describes any CanvasObject that can also be tapped. +// Tappable describes any [CanvasObject] that can also be tapped. // This should be implemented by buttons etc that wish to handle pointer interactions. type Tappable interface { Tapped(*PointEvent) diff --git a/cloud.go b/cloud.go index c44e53c514..2e815bb4b9 100644 --- a/cloud.go +++ b/cloud.go @@ -1,7 +1,7 @@ package fyne // CloudProvider specifies the identifying information of a cloud provider. -// This information is mostly used by the `fyne.io/cloud ShowSettings' user flow. +// This information is mostly used by the [fyne.io/cloud.ShowSettings] user flow. // // Since: 2.3 type CloudProvider interface { diff --git a/container.go b/container.go index 6d7efc8cbd..a9357591cf 100644 --- a/container.go +++ b/container.go @@ -2,32 +2,32 @@ package fyne import "sync" -// Declare conformity to CanvasObject +// Declare conformity to [CanvasObject] var _ CanvasObject = (*Container)(nil) -// Container is a CanvasObject that contains a collection of child objects. +// Container is a [CanvasObject] that contains a collection of child objects. // The layout of the children is set by the specified Layout. type Container struct { size Size // The current size of the Container position Position // The current position of the Container Hidden bool // Is this Container hidden - Layout Layout // The Layout algorithm for arranging child CanvasObjects + Layout Layout // The Layout algorithm for arranging child [CanvasObject]s lock sync.Mutex - Objects []CanvasObject // The set of CanvasObjects this container holds + Objects []CanvasObject // The set of [CanvasObject]s this container holds } -// NewContainer returns a new Container instance holding the specified CanvasObjects. +// NewContainer returns a new [Container] instance holding the specified [CanvasObject]s. // -// Deprecated: Use container.NewWithoutLayout() to create a container that uses manual layout. +// Deprecated: Use [fyne.io/fyne/v2/container.NewWithoutLayout] to create a container that uses manual layout. func NewContainer(objects ...CanvasObject) *Container { return NewContainerWithoutLayout(objects...) } -// NewContainerWithoutLayout returns a new Container instance holding the specified -// CanvasObjects that are manually arranged. +// NewContainerWithoutLayout returns a new [Container] instance holding the specified +// [CanvasObject]s that are manually arranged. // -// Deprecated: Use container.NewWithoutLayout() instead +// Deprecated: Use [fyne.io/fyne/v2/container.NewWithoutLayout] instead. func NewContainerWithoutLayout(objects ...CanvasObject) *Container { ret := &Container{ Objects: objects, @@ -37,10 +37,10 @@ func NewContainerWithoutLayout(objects ...CanvasObject) *Container { return ret } -// NewContainerWithLayout returns a new Container instance holding the specified -// CanvasObjects which will be laid out according to the specified Layout. +// NewContainerWithLayout returns a new [Container] instance holding the specified +// [CanvasObject]s which will be laid out according to the specified Layout. // -// Deprecated: Use container.New() instead +// Deprecated: Use [fyne.io/fyne/v2/container.New] instead. func NewContainerWithLayout(layout Layout, objects ...CanvasObject) *Container { ret := &Container{ Objects: objects, @@ -66,9 +66,9 @@ func (c *Container) Add(add CanvasObject) { c.layout() } -// AddObject adds another CanvasObject to the set this Container holds. +// AddObject adds another [CanvasObject] to the set this Container holds. // -// Deprecated: Use replacement Add() function +// Deprecated: Use [Container.Add] instead. func (c *Container) AddObject(o CanvasObject) { c.Add(o) } @@ -83,8 +83,8 @@ func (c *Container) Hide() { repaint(c) } -// MinSize calculates the minimum size of a Container. -// This is delegated to the Layout, if specified, otherwise it will mimic MaxLayout. +// MinSize calculates the minimum size of c. +// This is delegated to the [Container.Layout], if specified, otherwise it will be calculated. func (c *Container) MinSize() Size { if c.Layout != nil { return c.Layout.MinSize(c.Objects) @@ -104,7 +104,7 @@ func (c *Container) Move(pos Position) { repaint(c) } -// Position gets the current position of this Container, relative to its parent. +// Position gets the current position of c relative to its parent. func (c *Container) Position() Position { return c.position } @@ -127,7 +127,7 @@ func (c *Container) Refresh() { // Remove updates the contents of this container to no longer include the specified object. // This method is not intended to be used inside a loop, to remove all the elements. -// It is much more efficient to call RemoveAll() instead. +// It is much more efficient to call [Container.RemoveAll) instead. func (c *Container) Remove(rem CanvasObject) { c.lock.Lock() defer c.lock.Unlock() @@ -158,7 +158,7 @@ func (c *Container) RemoveAll() { c.layout() } -// Resize sets a new size for the Container. +// Resize sets a new size for c. func (c *Container) Resize(size Size) { if c.size == size { return @@ -177,7 +177,7 @@ func (c *Container) Show() { c.Hidden = false } -// Size returns the current size of this container. +// Size returns the current size c. func (c *Container) Size() Size { return c.size } diff --git a/driver.go b/driver.go index 21c4906fb5..cfeb6b123d 100644 --- a/driver.go +++ b/driver.go @@ -15,9 +15,9 @@ type Driver interface { // If the source is specified it will be used, otherwise the current theme will be asked for the font. RenderedTextSize(text string, fontSize float32, style TextStyle, source Resource) (size Size, baseline float32) - // CanvasForObject returns the canvas that is associated with a given CanvasObject. + // CanvasForObject returns the canvas that is associated with a given [CanvasObject]. CanvasForObject(CanvasObject) Canvas - // AbsolutePositionForObject returns the position of a given CanvasObject relative to the top/left of a canvas. + // AbsolutePositionForObject returns the position of a given [CanvasObject] relative to the top/left of a canvas. AbsolutePositionForObject(CanvasObject) Position // Device returns the device that the application is currently running on. @@ -34,7 +34,7 @@ type Driver interface { StopAnimation(*Animation) // DoubleTapDelay returns the maximum duration where a second tap after a first one - // will be considered a DoubleTap instead of two distinct Tap events. + // will be considered a [DoubleTap] instead of two distinct [Tap] events. // // Since: 2.5 DoubleTapDelay() time.Duration diff --git a/event.go b/event.go index 6646e653ec..0f006056fd 100644 --- a/event.go +++ b/event.go @@ -1,7 +1,7 @@ package fyne // HardwareKey contains information associated with physical key events -// Most applications should use KeyName for cross-platform compatibility. +// Most applications should use [KeyName] for cross-platform compatibility. type HardwareKey struct { // ScanCode represents a hardware ID for (normally desktop) keyboard events. ScanCode int @@ -16,7 +16,7 @@ type KeyEvent struct { } // PointEvent describes a pointer input event. The position is relative to the -// top-left of the CanvasObject this is triggered on. +// top-left of the [CanvasObject] this is triggered on. type PointEvent struct { AbsolutePosition Position // The absolute position of the event Position Position // The relative position of the event diff --git a/geometry.go b/geometry.go index e28fd867b7..e0dec55f75 100644 --- a/geometry.go +++ b/geometry.go @@ -1,8 +1,10 @@ package fyne -var _ Vector2 = (*Delta)(nil) -var _ Vector2 = (*Position)(nil) -var _ Vector2 = (*Size)(nil) +var ( + _ Vector2 = (*Delta)(nil) + _ Vector2 = (*Position)(nil) + _ Vector2 = (*Size)(nil) +) // Vector2 marks geometry types that can operate as a coordinate vector. type Vector2 interface { @@ -15,12 +17,12 @@ type Delta struct { DX, DY float32 } -// NewDelta returns a newly allocated Delta representing a movement in the X and Y axis. +// NewDelta returns a newly allocated [Delta] representing a movement in the X and Y axis. func NewDelta(dx float32, dy float32) Delta { return Delta{DX: dx, DY: dy} } -// Components returns the X and Y elements of this Delta. +// Components returns the X and Y elements of v. func (v Delta) Components() (float32, float32) { return v.DX, v.DY } @@ -30,26 +32,26 @@ func (v Delta) IsZero() bool { return v.DX == 0.0 && v.DY == 0.0 } -// Position describes a generic X, Y coordinate relative to a parent Canvas -// or CanvasObject. +// Position describes a generic X, Y coordinate relative to a parent [Canvas] +// or [CanvasObject]. type Position struct { X float32 // The position from the parent's left edge Y float32 // The position from the parent's top edge } -// NewPos returns a newly allocated Position representing the specified coordinates. +// NewPos returns a newly allocated [Position] representing the specified coordinates. func NewPos(x float32, y float32) Position { return Position{x, y} } -// NewSquareOffsetPos returns a newly allocated Position with the same x and y position. +// NewSquareOffsetPos returns a newly allocated [Position] with the same x and y position. // // Since: 2.4 func NewSquareOffsetPos(length float32) Position { return Position{length, length} } -// Add returns a new Position that is the result of offsetting the current +// Add returns a new [Position] that is the result of offsetting the current // position by p2 X and Y. func (p Position) Add(v Vector2) Position { // NOTE: Do not simplify to `return p.AddXY(v.Components())`, it prevents inlining. @@ -57,12 +59,12 @@ func (p Position) Add(v Vector2) Position { return Position{p.X + x, p.Y + y} } -// AddXY returns a new Position by adding x and y to the current one. +// AddXY returns a new [Position] by adding x and y to the current one. func (p Position) AddXY(x, y float32) Position { return Position{p.X + x, p.Y + y} } -// Components returns the X and Y elements of this Position +// Components returns the X and Y elements of p. func (p Position) Components() (float32, float32) { return p.X, p.Y } @@ -72,7 +74,7 @@ func (p Position) IsZero() bool { return p.X == 0.0 && p.Y == 0.0 } -// Subtract returns a new Position that is the result of offsetting the current +// Subtract returns a new [Position] that is the result of offsetting the current // position by p2 -X and -Y. func (p Position) Subtract(v Vector2) Position { // NOTE: Do not simplify to `return p.SubtractXY(v.Components())`, it prevents inlining. @@ -80,7 +82,7 @@ func (p Position) Subtract(v Vector2) Position { return Position{p.X - x, p.Y - y} } -// SubtractXY returns a new Position by subtracting x and y from the current one. +// SubtractXY returns a new [Position] by subtracting x and y from the current one. func (p Position) SubtractXY(x, y float32) Position { return Position{p.X - x, p.Y - y} } @@ -121,7 +123,7 @@ func (s Size) IsZero() bool { return s.Width == 0.0 && s.Height == 0.0 } -// Max returns a new Size that is the maximum of the current Size and s2. +// Max returns a new [Size] that is the maximum of the current Size and s2. func (s Size) Max(v Vector2) Size { x, y := v.Components() @@ -131,7 +133,7 @@ func (s Size) Max(v Vector2) Size { return NewSize(maxW, maxH) } -// Min returns a new Size that is the minimum of the current Size and s2. +// Min returns a new [Size] that is the minimum of s and v. func (s Size) Min(v Vector2) Size { x, y := v.Components() diff --git a/layout.go b/layout.go index f00b4a0cd8..8a3d8047ef 100644 --- a/layout.go +++ b/layout.go @@ -1,11 +1,11 @@ package fyne -// Layout defines how CanvasObjects may be laid out in a specified Size. +// Layout defines how [CanvasObject]s may be laid out in a specified Size. type Layout interface { - // Layout will manipulate the listed CanvasObjects Size and Position + // Layout will manipulate the listed [CanvasObject]s Size and Position // to fit within the specified size. Layout([]CanvasObject, Size) // MinSize calculates the smallest size that will fit the listed - // CanvasObjects using this Layout algorithm. + // [CanvasObject]s using this Layout algorithm. MinSize(objects []CanvasObject) Size } diff --git a/menu.go b/menu.go index e88215c36b..7a3c54736b 100644 --- a/menu.go +++ b/menu.go @@ -7,13 +7,13 @@ type systemTrayDriver interface { } // Menu stores the information required for a standard menu. -// A menu can pop down from a MainMenu or could be a pop out menu. +// A menu can pop down from a [MainMenu] or could be a pop out menu. type Menu struct { Label string Items []*MenuItem } -// NewMenu creates a new menu given the specified label (to show in a MainMenu) and list of items to display. +// NewMenu creates a new menu given the specified label (to show in a [MainMenu]) and list of items to display. func NewMenu(label string, items ...*MenuItem) *Menu { return &Menu{Label: label, Items: items} } diff --git a/notification.go b/notification.go index 340173de4f..a068623d85 100644 --- a/notification.go +++ b/notification.go @@ -5,7 +5,7 @@ type Notification struct { Title, Content string } -// NewNotification creates a notification that can be passed to App.SendNotification. +// NewNotification creates a notification that can be passed to [App.SendNotification]. func NewNotification(title, content string) *Notification { return &Notification{Title: title, Content: content} } diff --git a/overlay_stack.go b/overlay_stack.go index 69be64e472..eb7c9c7658 100644 --- a/overlay_stack.go +++ b/overlay_stack.go @@ -1,6 +1,6 @@ package fyne -// OverlayStack is a stack of CanvasObjects intended to be used as overlays of a Canvas. +// OverlayStack is a stack of [CanvasObject]s intended to be used as overlays of a [Canvas]. type OverlayStack interface { // Add adds an overlay on the top of the overlay stack. Add(overlay CanvasObject) diff --git a/resource.go b/resource.go index d2d3bb67d9..95e32370dd 100644 --- a/resource.go +++ b/resource.go @@ -16,8 +16,8 @@ type Resource interface { Content() []byte } -// ThemedResource is a version of a resource that can be updated to match a certain theme colour. -// The `ThemeColorName` will be used to look up the color for the current theme and colorize the resource. +// ThemedResource is a version of a resource that can be updated to match a certain theme color. +// The [ThemeColorName] will be used to look up the color for the current theme and colorize the resource. // // Since: 2.5 type ThemedResource interface { @@ -55,7 +55,7 @@ func NewStaticResource(name string, content []byte) *StaticResource { } } -// LoadResourceFromPath creates a new StaticResource in memory using the contents of the specified file. +// LoadResourceFromPath creates a new [StaticResource] in memory using the contents of the specified file. func LoadResourceFromPath(path string) (Resource, error) { bytes, err := os.ReadFile(filepath.Clean(path)) if err != nil { @@ -66,7 +66,7 @@ func LoadResourceFromPath(path string) (Resource, error) { return NewStaticResource(name, bytes), nil } -// LoadResourceFromURLString creates a new StaticResource in memory using the body of the specified URL. +// LoadResourceFromURLString creates a new [StaticResource] in memory using the body of the specified URL. func LoadResourceFromURLString(urlStr string) (Resource, error) { res, err := http.Get(urlStr) if err != nil { diff --git a/settings.go b/settings.go index b6ad42c144..9475d0cc3e 100644 --- a/settings.go +++ b/settings.go @@ -8,7 +8,7 @@ const ( BuildStandard BuildType = iota // BuildDebug is used when a developer would like more information and visual output for app debugging. BuildDebug - // BuildRelease is a final production build, it is like BuildStandard but will use distribution certificates. + // BuildRelease is a final production build, it is like [BuildStandard] but will use distribution certificates. // A release build is typically going to connect to live services and is not usually used during development. BuildRelease ) diff --git a/shortcut.go b/shortcut.go index ebe33ea0e3..4fcef3cae0 100644 --- a/shortcut.go +++ b/shortcut.go @@ -5,7 +5,7 @@ import ( ) // ShortcutHandler is a default implementation of the shortcut handler -// for the canvasObject +// for [CanvasObject]. type ShortcutHandler struct { entry sync.Map // map[string]func(Shortcut) } @@ -50,16 +50,16 @@ type ShortcutPaste struct { var _ KeyboardShortcut = (*ShortcutPaste)(nil) -// Key returns the KeyName for this shortcut. +// Key returns the [KeyName] for this shortcut. // -// Implements: KeyboardShortcut +// Implements: [KeyboardShortcut] func (se *ShortcutPaste) Key() KeyName { return KeyV } -// Mod returns the KeyModifier for this shortcut. +// Mod returns the [KeyModifier] for this shortcut. // -// Implements: KeyboardShortcut +// Implements: [KeyboardShortcut] func (se *ShortcutPaste) Mod() KeyModifier { return KeyModifierShortcutDefault } @@ -76,16 +76,16 @@ type ShortcutCopy struct { var _ KeyboardShortcut = (*ShortcutCopy)(nil) -// Key returns the KeyName for this shortcut. +// Key returns the [KeyName] for this shortcut. // -// Implements: KeyboardShortcut +// Implements: [KeyboardShortcut] func (se *ShortcutCopy) Key() KeyName { return KeyC } -// Mod returns the KeyModifier for this shortcut. +// Mod returns the [KeyModifier] for this shortcut. // -// Implements: KeyboardShortcut +// Implements: [KeyboardShortcut] func (se *ShortcutCopy) Mod() KeyModifier { return KeyModifierShortcutDefault } @@ -102,16 +102,16 @@ type ShortcutCut struct { var _ KeyboardShortcut = (*ShortcutCut)(nil) -// Key returns the KeyName for this shortcut. +// Key returns the [KeyName] for this shortcut. // -// Implements: KeyboardShortcut +// Implements: [KeyboardShortcut] func (se *ShortcutCut) Key() KeyName { return KeyX } -// Mod returns the KeyModifier for this shortcut. +// Mod returns the [KeyModifier] for this shortcut. // -// Implements: KeyboardShortcut +// Implements: [KeyboardShortcut] func (se *ShortcutCut) Mod() KeyModifier { return KeyModifierShortcutDefault } @@ -126,16 +126,16 @@ type ShortcutSelectAll struct{} var _ KeyboardShortcut = (*ShortcutSelectAll)(nil) -// Key returns the KeyName for this shortcut. +// Key returns the [KeyName] for this shortcut. // -// Implements: KeyboardShortcut +// Implements: [KeyboardShortcut] func (se *ShortcutSelectAll) Key() KeyName { return KeyA } -// Mod returns the KeyModifier for this shortcut. +// Mod returns the [KeyModifier] for this shortcut. // -// Implements: KeyboardShortcut +// Implements: [KeyboardShortcut] func (se *ShortcutSelectAll) Mod() KeyModifier { return KeyModifierShortcutDefault } @@ -152,16 +152,16 @@ type ShortcutUndo struct{} var _ KeyboardShortcut = (*ShortcutUndo)(nil) -// Key returns the KeyName for this shortcut. +// Key returns the [KeyName] for this shortcut. // -// Implements: KeyboardShortcut +// Implements: [KeyboardShortcut] func (se *ShortcutUndo) Key() KeyName { return KeyZ } -// Mod returns the KeyModifier for this shortcut. +// Mod returns the [KeyModifier] for this shortcut. // -// Implements: KeyboardShortcut +// Implements: [KeyboardShortcut] func (se *ShortcutUndo) Mod() KeyModifier { return KeyModifierShortcutDefault } @@ -178,16 +178,16 @@ type ShortcutRedo struct{} var _ KeyboardShortcut = (*ShortcutRedo)(nil) -// Key returns the KeyName for this shortcut. +// Key returns the [KeyName] for this shortcut. // -// Implements: KeyboardShortcut +// Implements: [KeyboardShortcut] func (se *ShortcutRedo) Key() KeyName { return KeyY } -// Mod returns the KeyModifier for this shortcut. +// Mod returns the [KeyModifier] for this shortcut. // -// Implements: KeyboardShortcut +// Implements: [KeyboardShortcut] func (se *ShortcutRedo) Mod() KeyModifier { return KeyModifierShortcutDefault } diff --git a/text.go b/text.go index 3c8b6df12f..a300811184 100644 --- a/text.go +++ b/text.go @@ -41,7 +41,7 @@ const ( // TextTruncate trims the text to the widget's width, no wrapping is applied. // If an entry is asked to truncate it will provide scrolling capabilities. // - // Deprecated: Use `TextTruncateClip` value of the widget `Truncation` field instead + // Deprecated: Use [TextTruncateClip] value of the widget `Truncation` field instead TextTruncate // TextWrapBreak trims the line of characters to the widget's width adding the excess as new line. // An Entry with text wrapping will scroll vertically if there is not enough space for all the text. @@ -62,7 +62,7 @@ type TextStyle struct { // Since: 2.1 TabWidth int // Width of tabs in spaces // Since: 2.5 - // Currently only supported by the TextGrid widget. + // Currently only supported by [fyne.io/fyne/v2/widget.TextGrid]. Underline bool // Should text be underlined. } diff --git a/uri.go b/uri.go index 4cde1a1e04..a4228306d2 100644 --- a/uri.go +++ b/uri.go @@ -26,9 +26,9 @@ type URIWriteCloser interface { // system. // // In general, it is expected that URI implementations follow IETF RFC3896. -// Implementations are highly recommended to utilize net/url to implement URI -// parsing methods, especially Scheme(), AUthority(), Path(), Query(), and -// Fragment(). +// Implementations are highly recommended to utilize [net/url] to implement URI +// parsing methods, especially [net/url/url.Scheme], [net/url/url.Authority], +// [net/url/url.Path], [net/url/url.Query], and [net/url/url.Fragment]. type URI interface { fmt.Stringer @@ -39,7 +39,7 @@ type URI interface { Extension() string // Name should return the base name of the item referenced by the URI. - // For example, the Name() of 'file://foo/bar.baz' is 'bar.baz'. + // For example, the name of 'file://foo/bar.baz' is 'bar.baz'. Name() string // MimeType should return the content type of the resource referenced @@ -57,8 +57,8 @@ type URI interface { // Authority should return the URI authority, as defined by IETF // RFC3986. // - // NOTE: the RFC3986 can be obtained by combining the User and Host - // Fields of net/url's URL structure. Consult IETF RFC3986, section + // NOTE: the RFC3986 can be obtained by combining the [net/url.URL.User] + // and [net/url.URL.Host]. Consult IETF RFC3986, section // 3.2, pp. 17. // // Since: 2.0 @@ -81,7 +81,7 @@ type URI interface { Fragment() string } -// ListableURI represents a URI that can have child items, most commonly a +// ListableURI represents a [URI] that can have child items, most commonly a // directory on disk in the native filesystem. // // Since: 1.4 @@ -92,7 +92,7 @@ type ListableURI interface { List() ([]URI, error) } -// URIWithIcon describes a URI that should be rendered with a certain icon in file browsers. +// URIWithIcon describes a [URI] that should be rendered with a certain icon in file browsers. // // Since: 2.5 type URIWithIcon interface { diff --git a/widget.go b/widget.go index 07741aa976..644a7a456c 100644 --- a/widget.go +++ b/widget.go @@ -1,33 +1,33 @@ package fyne -// Widget defines the standard behaviours of any widget. This extends the -// CanvasObject - a widget behaves in the same basic way but will encapsulate +// Widget defines the standard behaviours of any widget. This extends +// [CanvasObject]. A widget behaves in the same basic way but will encapsulate // many child objects to create the rendered widget. type Widget interface { CanvasObject - // CreateRenderer returns a new WidgetRenderer for this widget. + // CreateRenderer returns a new [WidgetRenderer] for this widget. // This should not be called by regular code, it is used internally to render a widget. CreateRenderer() WidgetRenderer } // WidgetRenderer defines the behaviour of a widget's implementation. -// This is returned from a widget's declarative object through the CreateRenderer() -// function and should be exactly one instance per widget in memory. +// This is returned from a widget's declarative object through [Widget.CreateRenderer] +// and should be exactly one instance per widget in memory. type WidgetRenderer interface { // Destroy is a hook that is called when the renderer is being destroyed. // This happens at some time after the widget is no longer visible, and - // once destroyed a renderer will not be reused. + // once destroyed, a renderer will not be reused. // Renderers should dispose and clean up any related resources, if necessary. Destroy() // Layout is a hook that is called if the widget needs to be laid out. - // This should never call Refresh. + // This should never call [Refresh]. Layout(Size) // MinSize returns the minimum size of the widget that is rendered by this renderer. MinSize() Size // Objects returns all objects that should be drawn. Objects() []CanvasObject // Refresh is a hook that is called if the widget has updated and needs to be redrawn. - // This might trigger a Layout. + // This might trigger a [Layout]. Refresh() } diff --git a/window.go b/window.go index 604d05c598..bc7492c9ef 100644 --- a/window.go +++ b/window.go @@ -65,7 +65,7 @@ type Window interface { SetOnClosed(func()) // SetCloseIntercept sets a function that runs instead of closing if defined. - // Close() should be called explicitly in the interceptor to close the window. + // [Window.Close] should be called explicitly in the interceptor to close the window. // // Since: 1.4 SetCloseIntercept(func()) From b7647965afdde67d6f8bb530075ec14c12902d04 Mon Sep 17 00:00:00 2001 From: ErikKalkoken Date: Sat, 28 Sep 2024 17:01:34 +0200 Subject: [PATCH 42/50] Improve doc strings for file dialogs --- dialog/file.go | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/dialog/file.go b/dialog/file.go index 738cfb7b78..05500c6a88 100644 --- a/dialog/file.go +++ b/dialog/file.go @@ -831,33 +831,40 @@ func (f *FileDialog) SetView(v ViewLayout) { } // NewFileOpen creates a file dialog allowing the user to choose a file to open. -// The callback function will run when the dialog closes. The URI will be nil -// when the user cancels or when nothing is selected. +// +// The callback function will run when the dialog closes and provide a reader for the chosen file. +// The reader must be closed by the callback. +// The reader will be nil, when the user cancels or when nothing is selected. // // The dialog will appear over the window specified when Show() is called. -func NewFileOpen(callback func(fyne.URIReadCloser, error), parent fyne.Window) *FileDialog { +func NewFileOpen(callback func(reader fyne.URIReadCloser, err error), parent fyne.Window) *FileDialog { dialog := &FileDialog{callback: callback, parent: parent} return dialog } // NewFileSave creates a file dialog allowing the user to choose a file to save // to (new or overwrite). If the user chooses an existing file they will be -// asked if they are sure. The callback function will run when the dialog -// closes. The URI will be nil when the user cancels or when nothing is -// selected. +// asked if they are sure. +// +// The callback function will run when the dialog closes and provide a writer for the chosen file. +// The writer must be closed by the callback. +// The writer will be nil, when the user cancels or when nothing is selected. // // The dialog will appear over the window specified when Show() is called. -func NewFileSave(callback func(fyne.URIWriteCloser, error), parent fyne.Window) *FileDialog { +func NewFileSave(callback func(writer fyne.URIWriteCloser, err error), parent fyne.Window) *FileDialog { dialog := &FileDialog{callback: callback, parent: parent, save: true} return dialog } // ShowFileOpen creates and shows a file dialog allowing the user to choose a -// file to open. The callback function will run when the dialog closes. The URI -// will be nil when the user cancels or when nothing is selected. +// file to open. +// +// The callback function will run when the dialog closes and provide a reader for the chosen file. +// The reader must be closed by the callback. +// The reader will be nil, when the user cancels or when nothing is selected. // // The dialog will appear over the window specified. -func ShowFileOpen(callback func(fyne.URIReadCloser, error), parent fyne.Window) { +func ShowFileOpen(callback func(reader fyne.URIReadCloser, err error), parent fyne.Window) { dialog := NewFileOpen(callback, parent) if fileOpenOSOverride(dialog) { return @@ -867,12 +874,14 @@ func ShowFileOpen(callback func(fyne.URIReadCloser, error), parent fyne.Window) // ShowFileSave creates and shows a file dialog allowing the user to choose a // file to save to (new or overwrite). If the user chooses an existing file they -// will be asked if they are sure. The callback function will run when the -// dialog closes. The URI will be nil when the user cancels or when nothing is -// selected. +// will be asked if they are sure. +// +// The callback function will run when the dialog closes and provide a writer for the chosen file. +// The writer must be closed by the callback. +// The writer will be nil, when the user cancels or when nothing is selected. // // The dialog will appear over the window specified. -func ShowFileSave(callback func(fyne.URIWriteCloser, error), parent fyne.Window) { +func ShowFileSave(callback func(writer fyne.URIWriteCloser, err error), parent fyne.Window) { dialog := NewFileSave(callback, parent) if fileSaveOSOverride(dialog) { return From 54a2d884a0804241ce488bc724d9be1ab18e435e Mon Sep 17 00:00:00 2001 From: ErikKalkoken Date: Mon, 30 Sep 2024 19:30:37 +0200 Subject: [PATCH 43/50] Further improve comments for file dialogs --- dialog/file.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dialog/file.go b/dialog/file.go index 05500c6a88..32a0bc3859 100644 --- a/dialog/file.go +++ b/dialog/file.go @@ -833,8 +833,8 @@ func (f *FileDialog) SetView(v ViewLayout) { // NewFileOpen creates a file dialog allowing the user to choose a file to open. // // The callback function will run when the dialog closes and provide a reader for the chosen file. -// The reader must be closed by the callback. -// The reader will be nil, when the user cancels or when nothing is selected. +// The reader will be nil when the user cancels or when nothing is selected. +// When the reader isn't nil it must be closed by the callback. // // The dialog will appear over the window specified when Show() is called. func NewFileOpen(callback func(reader fyne.URIReadCloser, err error), parent fyne.Window) *FileDialog { @@ -847,8 +847,8 @@ func NewFileOpen(callback func(reader fyne.URIReadCloser, err error), parent fyn // asked if they are sure. // // The callback function will run when the dialog closes and provide a writer for the chosen file. -// The writer must be closed by the callback. -// The writer will be nil, when the user cancels or when nothing is selected. +// The writer will be nil when the user cancels or when nothing is selected. +// When the writer isn't nil it must be closed by the callback. // // The dialog will appear over the window specified when Show() is called. func NewFileSave(callback func(writer fyne.URIWriteCloser, err error), parent fyne.Window) *FileDialog { @@ -860,8 +860,8 @@ func NewFileSave(callback func(writer fyne.URIWriteCloser, err error), parent fy // file to open. // // The callback function will run when the dialog closes and provide a reader for the chosen file. -// The reader must be closed by the callback. -// The reader will be nil, when the user cancels or when nothing is selected. +// The reader will be nil when the user cancels or when nothing is selected. +// When the reader isn't nil it must be closed by the callback. // // The dialog will appear over the window specified. func ShowFileOpen(callback func(reader fyne.URIReadCloser, err error), parent fyne.Window) { @@ -877,8 +877,8 @@ func ShowFileOpen(callback func(reader fyne.URIReadCloser, err error), parent fy // will be asked if they are sure. // // The callback function will run when the dialog closes and provide a writer for the chosen file. -// The writer must be closed by the callback. -// The writer will be nil, when the user cancels or when nothing is selected. +// The writer will be nil when the user cancels or when nothing is selected. +// When the writer isn't nil it must be closed by the callback. // // The dialog will appear over the window specified. func ShowFileSave(callback func(writer fyne.URIWriteCloser, err error), parent fyne.Window) { From 70984a8cecb8178912769575232a7942ba2c0a8d Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Sat, 28 Sep 2024 15:23:19 +0100 Subject: [PATCH 44/50] Update to the latest breaking API changes in go-text --- go.mod | 4 ++-- go.sum | 10 +++++----- internal/painter/font.go | 31 +++++++++++++++---------------- internal/test/text.go | 4 ++-- 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index 1a479179b5..a9c5a089c9 100644 --- a/go.mod +++ b/go.mod @@ -14,8 +14,8 @@ require ( github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a github.com/go-ole/go-ole v1.2.6 - github.com/go-text/render v0.1.1-0.20240418202334-dd62631dae9b - github.com/go-text/typesetting v0.1.0 + github.com/go-text/render v0.2.0 + github.com/go-text/typesetting v0.2.0 github.com/godbus/dbus/v5 v5.1.0 github.com/jackmordaunt/icns/v2 v2.2.6 github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 diff --git a/go.sum b/go.sum index 758640c713..617cc24c83 100644 --- a/go.sum +++ b/go.sum @@ -99,11 +99,11 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-text/render v0.1.1-0.20240418202334-dd62631dae9b h1:daoFn+Aw8EIQZO9kYWwHL01FqwwpCl2nTeVEYbsgRHk= -github.com/go-text/render v0.1.1-0.20240418202334-dd62631dae9b/go.mod h1:jqEuNMenrmj6QRnkdpeaP0oKGFLDNhDkVKwGjsWWYU4= -github.com/go-text/typesetting v0.1.0 h1:vioSaLPYcHwPEPLT7gsjCGDCoYSbljxoHJzMnKwVvHw= -github.com/go-text/typesetting v0.1.0/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI= -github.com/go-text/typesetting-utils v0.0.0-20240329101916-eee87fb235a3 h1:levTnuLLUmpavLGbJYLJA7fQnKeS7P1eCdAlM+vReXk= +github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc= +github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU= +github.com/go-text/typesetting v0.2.0 h1:fbzsgbmk04KiWtE+c3ZD4W2nmCRzBqrqQOvYlwAOdho= +github.com/go-text/typesetting v0.2.0/go.mod h1:2+owI/sxa73XA581LAzVuEBZ3WEEV2pXeDswCH/3i1I= +github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66 h1:GUrm65PQPlhFSKjLPGOZNPNxLCybjzjYBzjfoBGaDUY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= diff --git a/internal/painter/font.go b/internal/painter/font.go index 02f1dc2d00..80a3182fdf 100644 --- a/internal/painter/font.go +++ b/internal/painter/font.go @@ -13,7 +13,6 @@ import ( "github.com/go-text/typesetting/font" "github.com/go-text/typesetting/fontscan" "github.com/go-text/typesetting/language" - "github.com/go-text/typesetting/opentype/api/metadata" "github.com/go-text/typesetting/shaping" "golang.org/x/image/math/fixed" @@ -44,7 +43,7 @@ func loadMap() { } } -func lookupLangFont(family string, aspect metadata.Aspect) font.Face { +func lookupLangFont(family string, aspect font.Aspect) *font.Face { mapLock.Lock() defer mapLock.Unlock() load.Do(loadMap) @@ -57,7 +56,7 @@ func lookupLangFont(family string, aspect metadata.Aspect) font.Face { return fm.ResolveFaceForLang(l) } -func lookupRuneFont(r rune, family string, aspect metadata.Aspect) font.Face { +func lookupRuneFont(r rune, family string, aspect font.Aspect) *font.Face { mapLock.Lock() defer mapLock.Unlock() load.Do(loadMap) @@ -72,18 +71,18 @@ func lookupRuneFont(r rune, family string, aspect metadata.Aspect) font.Face { func lookupFaces(theme, fallback, emoji fyne.Resource, family string, style fyne.TextStyle) (faces *dynamicFontMap) { f1 := loadMeasureFont(theme) if theme == fallback { - faces = &dynamicFontMap{family: family, faces: []font.Face{f1}} + faces = &dynamicFontMap{family: family, faces: []*font.Face{f1}} } else { f2 := loadMeasureFont(fallback) - faces = &dynamicFontMap{family: family, faces: []font.Face{f1, f2}} + faces = &dynamicFontMap{family: family, faces: []*font.Face{f1, f2}} } - aspect := metadata.Aspect{Style: metadata.StyleNormal} + aspect := font.Aspect{Style: font.StyleNormal} if style.Italic { - aspect.Style = metadata.StyleItalic + aspect.Style = font.StyleItalic } if style.Bold { - aspect.Weight = metadata.WeightBold + aspect.Weight = font.WeightBold } if emoji != nil { @@ -107,7 +106,7 @@ func CachedFontFace(style fyne.TextStyle, source fyne.Resource, o fyne.CanvasObj if face == nil { face = loadMeasureFont(theme.TextFont()) } - faces := &dynamicFontMap{family: source.Name(), faces: []font.Face{face}} + faces := &dynamicFontMap{family: source.Name(), faces: []*font.Face{face}} val = &FontCacheItem{Fonts: faces} fontCustomCache.Store(source, val) @@ -145,10 +144,10 @@ func CachedFontFace(style fyne.TextStyle, source fyne.Resource, o fyne.CanvasObj f1 := loadMeasureFont(th) if th == fallback { - faces = &dynamicFontMap{family: fontscan.SansSerif, faces: []font.Face{f1}} + faces = &dynamicFontMap{family: fontscan.SansSerif, faces: []*font.Face{f1}} } else { f2 := loadMeasureFont(fallback) - faces = &dynamicFontMap{family: fontscan.SansSerif, faces: []font.Face{f1, f2}} + faces = &dynamicFontMap{family: fontscan.SansSerif, faces: []*font.Face{f1, f2}} } default: faces = lookupFaces(font1, theme.DefaultTextFont(), emoji, fontscan.SansSerif, style) @@ -192,7 +191,7 @@ func DrawString(dst draw.Image, s string, color color.Color, f shaping.Fontmap, }) } -func loadMeasureFont(data fyne.Resource) font.Face { +func loadMeasureFont(data fyne.Resource) *font.Face { loaded, err := font.ParseTTF(bytes.NewReader(data.Content())) if err != nil { fyne.LogError("font load error", err) @@ -353,11 +352,11 @@ type noopLogger struct{} func (n noopLogger) Printf(string, ...interface{}) {} type dynamicFontMap struct { - faces []font.Face + faces []*font.Face family string } -func (d *dynamicFontMap) ResolveFace(r rune) font.Face { +func (d *dynamicFontMap) ResolveFace(r rune) *font.Face { for _, f := range d.faces { if _, ok := f.NominalGlyph(r); ok { @@ -365,7 +364,7 @@ func (d *dynamicFontMap) ResolveFace(r rune) font.Face { } } - toAdd := lookupRuneFont(r, d.family, metadata.Aspect{}) + toAdd := lookupRuneFont(r, d.family, font.Aspect{}) if toAdd != nil { d.addFace(toAdd) return toAdd @@ -374,6 +373,6 @@ func (d *dynamicFontMap) ResolveFace(r rune) font.Face { return d.faces[0] } -func (d *dynamicFontMap) addFace(f font.Face) { +func (d *dynamicFontMap) addFace(f *font.Face) { d.faces = append(d.faces, f) } diff --git a/internal/test/text.go b/internal/test/text.go index a4a230faf0..12cac53fcc 100644 --- a/internal/test/text.go +++ b/internal/test/text.go @@ -2,9 +2,9 @@ package test import "github.com/go-text/typesetting/font" -type FontMap []font.Face +type FontMap []*font.Face -func (f FontMap) ResolveFace(r rune) font.Face { +func (f FontMap) ResolveFace(r rune) *font.Face { if len(f) == 1 { return f[0] } From ddf4252ac1d61720690a81cfac82d201581e1541 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Mon, 30 Sep 2024 21:17:10 +0200 Subject: [PATCH 45/50] Modernise CI infrastructure with Go 1.22 This mainly does two things: - Uses Go 1.22 instead of 1.21 as the highest version (I don't have 1.23 on Fedora 40 yet so did not want to bump to latest just yet). - We now always pull the latest setup-go-faster action in v1 without having to manually change version. --- .github/workflows/license_check.yml | 4 ++-- .github/workflows/mobile_tests.yml | 4 ++-- .github/workflows/platform_tests.yml | 8 ++++---- .github/workflows/static_analysis.yml | 4 ++-- .github/workflows/web_tests.yml | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/license_check.yml b/.github/workflows/license_check.yml index d41c3a6449..bd21560a30 100644 --- a/.github/workflows/license_check.yml +++ b/.github/workflows/license_check.yml @@ -10,9 +10,9 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false - - uses: WillAbides/setup-go-faster@v1.13.0 + - uses: WillAbides/setup-go-faster@v1 with: - go-version: '1.21.x' + go-version: '1.22.x' - name: Install lian run: go install lucor.dev/lian@latest diff --git a/.github/workflows/mobile_tests.yml b/.github/workflows/mobile_tests.yml index 4919d08d18..5414654041 100644 --- a/.github/workflows/mobile_tests.yml +++ b/.github/workflows/mobile_tests.yml @@ -9,13 +9,13 @@ jobs: strategy: fail-fast: false matrix: - go-version: ['1.19.x', '1.21.x'] + go-version: ['1.19.x', '1.22.x'] steps: - uses: actions/checkout@v4 with: persist-credentials: false - - uses: WillAbides/setup-go-faster@v1.13.0 + - uses: WillAbides/setup-go-faster@v1 with: go-version: ${{ matrix.go-version }} diff --git a/.github/workflows/platform_tests.yml b/.github/workflows/platform_tests.yml index c56cb7c0a6..d2c50b3da1 100644 --- a/.github/workflows/platform_tests.yml +++ b/.github/workflows/platform_tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: ['1.19.x', '1.21.x'] + go-version: ['1.19.x', '1.22.x'] os: [ubuntu-latest, macos-latest] include: - os: ubuntu-latest @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false - - uses: WillAbides/setup-go-faster@v1.13.0 + - uses: WillAbides/setup-go-faster@v1 with: go-version: ${{ matrix.go-version }} @@ -51,12 +51,12 @@ jobs: strategy: fail-fast: false matrix: - go-version: ['1.20.x', '1.21.x'] + go-version: ['1.20.x', '1.22.x'] steps: - uses: actions/checkout@v4 with: persist-credentials: false - - uses: WillAbides/setup-go-faster@v1.13.0 + - uses: WillAbides/setup-go-faster@v1 with: go-version: ${{ matrix.go-version }} diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 489c47a755..6acb8cb6fa 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -13,9 +13,9 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false - - uses: WillAbides/setup-go-faster@v1.13.0 + - uses: WillAbides/setup-go-faster@v1 with: - go-version: '1.21.x' + go-version: '1.22.x' - name: Get dependencies run: >- diff --git a/.github/workflows/web_tests.yml b/.github/workflows/web_tests.yml index dea71861ae..5f4fe6e52b 100644 --- a/.github/workflows/web_tests.yml +++ b/.github/workflows/web_tests.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false - - uses: WillAbides/setup-go-faster@v1.13.0 + - uses: WillAbides/setup-go-faster@v1 with: go-version: '1.19.x' From 7ba5aa5075a861091657589af804f9b1b810f043 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Mon, 30 Sep 2024 21:29:17 +0200 Subject: [PATCH 46/50] Update CI and code to use Staticcheck v0.5.1 This fixes a few code errors that slipped through review (thankfully now handled by Staticcheck for us) and makes sure we are using the latest version. --- .github/workflows/static_analysis.yml | 2 +- cmd/fyne/internal/mobile/binres/binres_test.go | 2 +- widget/check_group.go | 2 +- widget/list.go | 4 ++-- widget/radio_group.go | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 6acb8cb6fa..0a13b58c9b 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -33,7 +33,7 @@ jobs: run: | go install golang.org/x/tools/cmd/goimports@latest go install github.com/fzipp/gocyclo/cmd/gocyclo@latest - go install honnef.co/go/tools/cmd/staticcheck@v0.4.6 + go install honnef.co/go/tools/cmd/staticcheck@v0.5.1 go install github.com/mattn/goveralls@latest - name: Vet diff --git a/cmd/fyne/internal/mobile/binres/binres_test.go b/cmd/fyne/internal/mobile/binres/binres_test.go index f6d5e8e6e3..92fdb67446 100644 --- a/cmd/fyne/internal/mobile/binres/binres_test.go +++ b/cmd/fyne/internal/mobile/binres/binres_test.go @@ -246,7 +246,7 @@ func compareElements(have, want *XML) error { } } if buf.Len() > 0 { - return fmt.Errorf(buf.String()) + return errors.New(buf.String()) } return nil } diff --git a/widget/check_group.go b/widget/check_group.go index 6fc258485e..e2725b35ed 100644 --- a/widget/check_group.go +++ b/widget/check_group.go @@ -182,7 +182,7 @@ type checkGroupRenderer struct { // Layout the components of the checks widget func (r *checkGroupRenderer) Layout(_ fyne.Size) { count := 1 - if r.items != nil && len(r.items) > 0 { + if len(r.items) > 0 { count = len(r.items) } var itemHeight, itemWidth float32 diff --git a/widget/list.go b/widget/list.go index d640e13cb7..f51d6e3c65 100644 --- a/widget/list.go +++ b/widget/list.go @@ -169,7 +169,7 @@ func (l *List) scrollTo(id ListItemID) { separatorThickness := l.Theme().Size(theme.SizeNamePadding) y := float32(0) lastItemHeight := l.itemMin.Height - if l.itemHeights == nil || len(l.itemHeights) == 0 { + if len(l.itemHeights) == 0 { y = (float32(id) * l.itemMin.Height) + (float32(id) * separatorThickness) } else { for i := 0; i < id; i++ { @@ -368,7 +368,7 @@ func (l *List) contentMinSize() fyne.Size { } items := l.Length() - if l.itemHeights == nil || len(l.itemHeights) == 0 { + if len(l.itemHeights) == 0 { return fyne.NewSize(l.itemMin.Width, (l.itemMin.Height+separatorThickness)*float32(items)-separatorThickness) } diff --git a/widget/radio_group.go b/widget/radio_group.go index 51944a6740..3cf9bcb11f 100644 --- a/widget/radio_group.go +++ b/widget/radio_group.go @@ -156,7 +156,7 @@ type radioGroupRenderer struct { // Layout the components of the radio widget func (r *radioGroupRenderer) Layout(_ fyne.Size) { count := 1 - if r.items != nil && len(r.items) > 0 { + if len(r.items) > 0 { count = len(r.items) } var itemHeight, itemWidth float32 From 4bfe10dfc22a565e5aa20804f23088b9f526c951 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Mon, 30 Sep 2024 21:35:32 +0200 Subject: [PATCH 47/50] Add a missing import Haven't quite gotten used to Nvim yet ;) --- cmd/fyne/internal/mobile/binres/binres_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/fyne/internal/mobile/binres/binres_test.go b/cmd/fyne/internal/mobile/binres/binres_test.go index 92fdb67446..4a410885dc 100644 --- a/cmd/fyne/internal/mobile/binres/binres_test.go +++ b/cmd/fyne/internal/mobile/binres/binres_test.go @@ -8,6 +8,7 @@ import ( "bytes" "encoding" "encoding/xml" + "errors" "fmt" "math" "os" From 5e52896071d2355479e28d4a5db133c7d031f93b Mon Sep 17 00:00:00 2001 From: Jacalz Date: Mon, 30 Sep 2024 21:38:52 +0200 Subject: [PATCH 48/50] Bump Go version in CI to 1.23 This should be safer than ever given that some new features are dependent on the version in go.mod. --- .github/workflows/license_check.yml | 2 +- .github/workflows/mobile_tests.yml | 2 +- .github/workflows/platform_tests.yml | 2 +- .github/workflows/static_analysis.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/license_check.yml b/.github/workflows/license_check.yml index bd21560a30..c859a6699d 100644 --- a/.github/workflows/license_check.yml +++ b/.github/workflows/license_check.yml @@ -12,7 +12,7 @@ jobs: persist-credentials: false - uses: WillAbides/setup-go-faster@v1 with: - go-version: '1.22.x' + go-version: '1.23.x' - name: Install lian run: go install lucor.dev/lian@latest diff --git a/.github/workflows/mobile_tests.yml b/.github/workflows/mobile_tests.yml index 5414654041..65aeead2bf 100644 --- a/.github/workflows/mobile_tests.yml +++ b/.github/workflows/mobile_tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: ['1.19.x', '1.22.x'] + go-version: ['1.19.x', '1.23.x'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/platform_tests.yml b/.github/workflows/platform_tests.yml index d2c50b3da1..6644219cf8 100644 --- a/.github/workflows/platform_tests.yml +++ b/.github/workflows/platform_tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: ['1.19.x', '1.22.x'] + go-version: ['1.19.x', '1.23.x'] os: [ubuntu-latest, macos-latest] include: - os: ubuntu-latest diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 0a13b58c9b..8c2cb77a78 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -15,7 +15,7 @@ jobs: persist-credentials: false - uses: WillAbides/setup-go-faster@v1 with: - go-version: '1.22.x' + go-version: '1.23.x' - name: Get dependencies run: >- From c2cdd54ba9565d91715d51a93c853479caef35db Mon Sep 17 00:00:00 2001 From: Jacalz Date: Tue, 1 Oct 2024 20:33:27 +0200 Subject: [PATCH 49/50] Fix staticcheck error for deprecation --- widget/entry_internal_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/widget/entry_internal_test.go b/widget/entry_internal_test.go index 07552b8f4b..f5e507dbfd 100644 --- a/widget/entry_internal_test.go +++ b/widget/entry_internal_test.go @@ -397,17 +397,17 @@ func TestEntry_PasteFromClipboardValidation(t *testing.T) { return nil } - w := test.NewApp().NewWindow("") + a := test.NewTempApp(t) + w := a.NewWindow("") defer w.Close() w.SetContent(entry) testContent := "test" - clipboard := fyne.CurrentApp().Driver().AllWindows()[0].Clipboard() + clipboard := a.Clipboard() clipboard.SetContent(testContent) entry.pasteFromClipboard(clipboard) - assert.Equal(t, 2, triggered) } From 50b23bbd1f554fabea0de4851345124eee2f36f9 Mon Sep 17 00:00:00 2001 From: Pierre Curto Date: Wed, 2 Oct 2024 15:40:17 +0200 Subject: [PATCH 50/50] cmd/fyne_demo: set the clipboard for Copy/Cut/Paste shortcuts --- cmd/fyne_demo/main.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/cmd/fyne_demo/main.go b/cmd/fyne_demo/main.go index 07b6cbbbeb..3a4c571f33 100644 --- a/cmd/fyne_demo/main.go +++ b/cmd/fyne_demo/main.go @@ -135,17 +135,17 @@ func makeMenu(a fyne.App, w fyne.Window) *fyne.MainMenu { cutShortcut := &fyne.ShortcutCut{Clipboard: a.Clipboard()} cutItem := fyne.NewMenuItem("Cut", func() { - shortcutFocused(cutShortcut, w.Canvas().Focused()) + shortcutFocused(cutShortcut, a.Clipboard(), w.Canvas().Focused()) }) cutItem.Shortcut = cutShortcut copyShortcut := &fyne.ShortcutCopy{Clipboard: a.Clipboard()} copyItem := fyne.NewMenuItem("Copy", func() { - shortcutFocused(copyShortcut, w.Canvas().Focused()) + shortcutFocused(copyShortcut, a.Clipboard(), w.Canvas().Focused()) }) copyItem.Shortcut = copyShortcut pasteShortcut := &fyne.ShortcutPaste{Clipboard: a.Clipboard()} pasteItem := fyne.NewMenuItem("Paste", func() { - shortcutFocused(pasteShortcut, w.Canvas().Focused()) + shortcutFocused(pasteShortcut, a.Clipboard(), w.Canvas().Focused()) }) pasteItem.Shortcut = pasteShortcut performFind := func() { fmt.Println("Menu Find") } @@ -251,7 +251,15 @@ func makeNav(setTutorial func(tutorial tutorials.Tutorial), loadPrevious bool) f return container.NewBorder(nil, themes, nil, nil, tree) } -func shortcutFocused(s fyne.Shortcut, f fyne.Focusable) { +func shortcutFocused(s fyne.Shortcut, cb fyne.Clipboard, f fyne.Focusable) { + switch sh := s.(type) { + case *fyne.ShortcutCopy: + sh.Clipboard = cb + case *fyne.ShortcutCut: + sh.Clipboard = cb + case *fyne.ShortcutPaste: + sh.Clipboard = cb + } if focused, ok := f.(fyne.Shortcutable); ok { focused.TypedShortcut(s) }