From 3d1c6509a38c0d9368925dc1ae2cd0f65dbd5b65 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Wed, 17 Dec 2025 21:05:53 +1100 Subject: [PATCH 1/7] feat(v3)!: MessageDialog.Show() returns clicked button label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: MessageDialog.Show() now returns a string instead of void. Make MessageDialog.Show() return the clicked button's label as a string, enabling synchronous dialog workflows. This allows developers to use switch statements on the result instead of callbacks for decision-making flows. All platforms now consistently block until the user clicks a button and return the label. Button callbacks are still called for backwards compatibility. Closes #4792 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- v3/UNRELEASED_CHANGELOG.md | 1 + v3/pkg/application/dialogs.go | 9 ++++++--- v3/pkg/application/dialogs_android.go | 3 ++- v3/pkg/application/dialogs_darwin.go | 10 +++++++++- v3/pkg/application/dialogs_ios.go | 5 +++-- v3/pkg/application/dialogs_linux.go | 11 ++++++++++- v3/pkg/application/dialogs_windows.go | 3 ++- 7 files changed, 33 insertions(+), 9 deletions(-) diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 8e46480384f..1fe32700b91 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -20,6 +20,7 @@ After processing, the content will be moved to the main changelog and this file ## Changed +- **BREAKING**: `MessageDialog.Show()` now returns the clicked button's label as a string, enabling synchronous dialog workflows (#4792) ## Fixed diff --git a/v3/pkg/application/dialogs.go b/v3/pkg/application/dialogs.go index d61f64886c5..287dbc4c2cd 100644 --- a/v3/pkg/application/dialogs.go +++ b/v3/pkg/application/dialogs.go @@ -66,7 +66,7 @@ func (b *Button) SetAsCancel() *Button { } type messageDialogImpl interface { - show() + show() string } type MessageDialogOptions struct { @@ -106,11 +106,14 @@ func (d *MessageDialog) SetTitle(title string) *MessageDialog { return d } -func (d *MessageDialog) Show() { +// Show displays the dialog and returns the label of the clicked button. +// This method blocks until the user clicks a button. +// If the dialog has button callbacks defined, they will still be called. +func (d *MessageDialog) Show() string { if d.impl == nil { d.impl = newDialogImpl(d) } - InvokeSync(d.impl.show) + return InvokeSyncWithResult(d.impl.show) } func (d *MessageDialog) SetIcon(icon []byte) *MessageDialog { diff --git a/v3/pkg/application/dialogs_android.go b/v3/pkg/application/dialogs_android.go index a962dfba0e6..6c6adfd7dd6 100644 --- a/v3/pkg/application/dialogs_android.go +++ b/v3/pkg/application/dialogs_android.go @@ -65,8 +65,9 @@ type androidDialog struct { dialog *MessageDialog } -func (d *androidDialog) show() { +func (d *androidDialog) show() string { // TODO: Implement using AlertDialog + return "" } func newDialogImpl(d *MessageDialog) *androidDialog { diff --git a/v3/pkg/application/dialogs_darwin.go b/v3/pkg/application/dialogs_darwin.go index 282f4a41edc..c0ef06029f3 100644 --- a/v3/pkg/application/dialogs_darwin.go +++ b/v3/pkg/application/dialogs_darwin.go @@ -371,7 +371,10 @@ type macosDialog struct { nsDialog unsafe.Pointer } -func (m *macosDialog) show() { +func (m *macosDialog) show() string { + // Channel to receive the result + resultChan := make(chan string, 1) + InvokeAsync(func() { // Mac can only have 4 Buttons on a dialog @@ -429,19 +432,24 @@ func (m *macosDialog) show() { var callBackID int callBackID = addDialogCallback(func(buttonPressed int) { + var buttonLabel string if len(m.dialog.Buttons) > buttonPressed { button := reversedButtons[buttonPressed] + buttonLabel = button.Label if button.Callback != nil { button.Callback() } } removeDialogCallback(callBackID) + resultChan <- buttonLabel }) C.dialogRunModal(m.nsDialog, parent, C.int(callBackID)) }) + // Wait for and return the result + return <-resultChan } func newDialogImpl(d *MessageDialog) *macosDialog { diff --git a/v3/pkg/application/dialogs_ios.go b/v3/pkg/application/dialogs_ios.go index 5caeed71e72..47236bb423b 100644 --- a/v3/pkg/application/dialogs_ios.go +++ b/v3/pkg/application/dialogs_ios.go @@ -65,8 +65,9 @@ type iosDialog struct { dialog *MessageDialog } -func (d *iosDialog) show() { +func (d *iosDialog) show() string { // TODO: Implement using UIAlertController + return "" } func newDialogImpl(d *MessageDialog) *iosDialog { @@ -87,4 +88,4 @@ func newOpenFileDialogImpl(_ *OpenFileDialogStruct) openFileDialogImpl { func newSaveFileDialogImpl(_ *SaveFileDialogStruct) saveFileDialogImpl { return &dialogsImpl{} -} \ No newline at end of file +} diff --git a/v3/pkg/application/dialogs_linux.go b/v3/pkg/application/dialogs_linux.go index a64c3f83fee..f750bb62bbf 100644 --- a/v3/pkg/application/dialogs_linux.go +++ b/v3/pkg/application/dialogs_linux.go @@ -27,7 +27,7 @@ type linuxDialog struct { dialog *MessageDialog } -func (m *linuxDialog) show() { +func (m *linuxDialog) show() string { windowId := getNativeApplication().getCurrentWindowID() window, _ := globalApplication.Window.GetByID(windowId) var parent uintptr @@ -38,10 +38,15 @@ func (m *linuxDialog) show() { } } + // Channel to receive the result + resultChan := make(chan string, 1) + InvokeAsync(func() { response := runQuestionDialog(pointer(parent), m.dialog) + var buttonLabel string if response >= 0 && response < len(m.dialog.Buttons) { button := m.dialog.Buttons[response] + buttonLabel = button.Label if button.Callback != nil { go func() { defer handlePanic() @@ -49,7 +54,11 @@ func (m *linuxDialog) show() { }() } } + resultChan <- buttonLabel }) + + // Wait for and return the result + return <-resultChan } func newDialogImpl(d *MessageDialog) *linuxDialog { diff --git a/v3/pkg/application/dialogs_windows.go b/v3/pkg/application/dialogs_windows.go index 73084d098e0..8d6c9b598c6 100644 --- a/v3/pkg/application/dialogs_windows.go +++ b/v3/pkg/application/dialogs_windows.go @@ -30,7 +30,7 @@ type windowsDialog struct { UseAppIcon bool } -func (m *windowsDialog) show() { +func (m *windowsDialog) show() string { title := w32.MustStringToUTF16Ptr(m.dialog.Title) message := w32.MustStringToUTF16Ptr(m.dialog.Message) @@ -72,6 +72,7 @@ func (m *windowsDialog) show() { } } } + return result } func newDialogImpl(d *MessageDialog) *windowsDialog { From 72e0c7a1c0169e58574ebb5a9980370b661190c8 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Wed, 17 Dec 2025 21:20:56 +1100 Subject: [PATCH 2/7] docs: update dialog examples to use Show() return value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update documentation and examples to demonstrate the new synchronous Show() return value pattern instead of callbacks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../content/docs/features/dialogs/message.mdx | 189 +++++++++--------- v3/examples/dialogs/main.go | 35 ++-- 2 files changed, 110 insertions(+), 114 deletions(-) diff --git a/docs/src/content/docs/features/dialogs/message.mdx b/docs/src/content/docs/features/dialogs/message.mdx index 4b718aaa661..83d591a7ade 100644 --- a/docs/src/content/docs/features/dialogs/message.mdx +++ b/docs/src/content/docs/features/dialogs/message.mdx @@ -127,31 +127,30 @@ func fetchData(app *application.App, url string) ([]byte, error) { ## Question dialog -Ask users questions and handle responses via button callbacks: +Ask users questions and handle responses. `Show()` returns the clicked button's label: ```go dialog := app.Dialog.Question(). SetTitle("Confirm"). SetMessage("Save changes before closing?") -save := dialog.AddButton("Save") -save.OnClick(func() { - saveChanges() -}) - -dontSave := dialog.AddButton("Don't Save") -dontSave.OnClick(func() { - // Continue without saving -}) - +dialog.AddButton("Save") +dialog.AddButton("Don't Save") cancel := dialog.AddButton("Cancel") -cancel.OnClick(func() { - // Don't close -}) -dialog.SetDefaultButton(save) +dialog.SetDefaultButton(dialog.Buttons[0]) // "Save" dialog.SetCancelButton(cancel) -dialog.Show() + +// Show() blocks and returns the clicked button's label +switch dialog.Show() { +case "Save": + saveChanges() + doClose() +case "Don't Save": + doClose() +case "Cancel": + // Don't close +} ``` **Use cases:** @@ -174,23 +173,23 @@ func closeDocument(app *application.App) { SetMessage("Do you want to save your changes?") save := dialog.AddButton("Save") - save.OnClick(func() { - if saveDocument() { - doClose() - } - }) - - dontSave := dialog.AddButton("Don't Save") - dontSave.OnClick(func() { - doClose() - }) - + dialog.AddButton("Don't Save") cancel := dialog.AddButton("Cancel") - // Cancel button has no callback - just closes the dialog dialog.SetDefaultButton(save) dialog.SetCancelButton(cancel) - dialog.Show() + + // Show() returns the clicked button's label + switch dialog.Show() { + case "Save": + if saveDocument() { + doClose() + } + case "Don't Save": + doClose() + case "Cancel": + // Don't close - do nothing + } } ``` @@ -233,28 +232,24 @@ dialog.Show() **Multiple buttons (Question):** -Use `AddButton()` to add buttons, which returns a `*Button` you can configure: +Use `AddButton()` to add buttons. `Show()` returns the label of the clicked button: ```go dialog := app.Dialog.Question(). SetMessage("Choose an action") -option1 := dialog.AddButton("Option 1") -option1.OnClick(func() { - handleOption1() -}) +dialog.AddButton("Option 1") +dialog.AddButton("Option 2") +dialog.AddButton("Option 3") -option2 := dialog.AddButton("Option 2") -option2.OnClick(func() { +switch dialog.Show() { +case "Option 1": + handleOption1() +case "Option 2": handleOption2() -}) - -option3 := dialog.AddButton("Option 3") -option3.OnClick(func() { +case "Option 3": handleOption3() -}) - -dialog.Show() +} ``` **Default and Cancel buttons:** @@ -266,17 +261,15 @@ Use `SetCancelButton()` to specify which button is triggered by Escape. dialog := app.Dialog.Question(). SetMessage("Delete file?") -deleteBtn := dialog.AddButton("Delete") -deleteBtn.OnClick(func() { - performDelete() -}) - +dialog.AddButton("Delete") cancelBtn := dialog.AddButton("Cancel") -// No callback needed - just dismisses dialog dialog.SetDefaultButton(cancelBtn) // Safe option as default dialog.SetCancelButton(cancelBtn) // Escape triggers Cancel -dialog.Show() + +if dialog.Show() == "Delete" { + performDelete() +} ``` You can also use the fluent `SetAsDefault()` and `SetAsCancel()` methods on buttons: @@ -285,13 +278,12 @@ You can also use the fluent `SetAsDefault()` and `SetAsCancel()` methods on butt dialog := app.Dialog.Question(). SetMessage("Delete file?") -dialog.AddButton("Delete").OnClick(func() { - performDelete() -}) - +dialog.AddButton("Delete") dialog.AddButton("Cancel").SetAsDefault().SetAsCancel() -dialog.Show() +if dialog.Show() == "Delete" { + performDelete() +} ``` **Best practices:** @@ -346,34 +338,35 @@ func deleteFiles(app *application.App, paths []string) { SetTitle("Confirm Delete"). SetMessage(message) - deleteBtn := dialog.AddButton("Delete") - deleteBtn.OnClick(func() { - // Perform deletion - var errs []error - for _, path := range paths { - if err := os.Remove(path); err != nil { - errs = append(errs, err) - } - } - - // Show result - if len(errs) > 0 { - app.Dialog.Error(). - SetTitle("Delete Failed"). - SetMessage(fmt.Sprintf("Failed to delete %d file(s)", len(errs))). - Show() - } else { - app.Dialog.Info(). - SetTitle("Delete Complete"). - SetMessage(fmt.Sprintf("Deleted %d file(s)", len(paths))). - Show() - } - }) - + dialog.AddButton("Delete") cancelBtn := dialog.AddButton("Cancel") dialog.SetDefaultButton(cancelBtn) dialog.SetCancelButton(cancelBtn) - dialog.Show() + + if dialog.Show() != "Delete" { + return // User cancelled + } + + // Perform deletion + var errs []error + for _, path := range paths { + if err := os.Remove(path); err != nil { + errs = append(errs, err) + } + } + + // Show result + if len(errs) > 0 { + app.Dialog.Error(). + SetTitle("Delete Failed"). + SetMessage(fmt.Sprintf("Failed to delete %d file(s)", len(errs))). + Show() + } else { + app.Dialog.Info(). + SetTitle("Delete Complete"). + SetMessage(fmt.Sprintf("Deleted %d file(s)", len(paths))). + Show() + } } ``` @@ -385,14 +378,13 @@ func confirmQuit(app *application.App) { SetTitle("Quit"). SetMessage("You have unsaved work. Are you sure you want to quit?") - yes := dialog.AddButton("Yes") - yes.OnClick(func() { - app.Quit() - }) - + dialog.AddButton("Yes") no := dialog.AddButton("No") dialog.SetDefaultButton(no) - dialog.Show() + + if dialog.Show() == "Yes" { + app.Quit() + } } ``` @@ -405,15 +397,14 @@ func showUpdateDialog(app *application.App) { SetMessage("A new version is available. The cancel button is selected when pressing escape.") download := dialog.AddButton("📥 Download") - download.OnClick(func() { - app.Dialog.Info().SetMessage("Downloading...").Show() - }) - cancel := dialog.AddButton("Cancel") dialog.SetDefaultButton(download) dialog.SetCancelButton(cancel) - dialog.Show() + + if dialog.Show() == "📥 Download" { + app.Dialog.Info().SetMessage("Downloading...").Show() + } } ``` @@ -427,17 +418,15 @@ func showCustomIconQuestion(app *application.App, iconBytes []byte) { SetIcon(iconBytes) likeIt := dialog.AddButton("I like it!") - likeIt.OnClick(func() { - app.Dialog.Info().SetMessage("Thanks!").Show() - }) + dialog.AddButton("Not so keen...") + dialog.SetDefaultButton(likeIt) - notKeen := dialog.AddButton("Not so keen...") - notKeen.OnClick(func() { + switch dialog.Show() { + case "I like it!": + app.Dialog.Info().SetMessage("Thanks!").Show() + case "Not so keen...": app.Dialog.Info().SetMessage("Too bad!").Show() - }) - - dialog.SetDefaultButton(likeIt) - dialog.Show() + } } ## Best Practices diff --git a/v3/examples/dialogs/main.go b/v3/examples/dialogs/main.go index e6e09661c50..3da9520a154 100644 --- a/v3/examples/dialogs/main.go +++ b/v3/examples/dialogs/main.go @@ -83,39 +83,46 @@ func main() { dialog := app.Dialog.Question() dialog.SetTitle("Quit") dialog.SetMessage("You have unsaved work. Are you sure you want to quit?") - dialog.AddButton("Yes").OnClick(func() { - app.Quit() - }) + dialog.AddButton("Yes") no := dialog.AddButton("No") dialog.SetDefaultButton(no) - dialog.Show() + // Show() returns the clicked button's label + result := dialog.Show() + if result == "Yes" { + app.Quit() + } }) questionMenu.Add("Question (With Cancel)").OnClick(func(ctx *application.Context) { dialog := app.Dialog.Question(). SetTitle("Update"). SetMessage("The cancel button is selected when pressing escape") download := dialog.AddButton("📥 Download") - download.OnClick(func() { - app.Dialog.Info().SetMessage("Downloading...").Show() - }) no := dialog.AddButton("Cancel") dialog.SetDefaultButton(download) dialog.SetCancelButton(no) - dialog.Show() + // Show() returns the clicked button's label - use switch for multiple options + switch dialog.Show() { + case "📥 Download": + app.Dialog.Info().SetMessage("Downloading...").Show() + case "Cancel": + // User cancelled + } }) questionMenu.Add("Question (Custom Icon)").OnClick(func(ctx *application.Context) { dialog := app.Dialog.Question() dialog.SetTitle("Custom Icon Example") dialog.SetMessage("Using a custom icon") dialog.SetIcon(icons.WailsLogoWhiteTransparent) - likeIt := dialog.AddButton("I like it!").OnClick(func() { + likeIt := dialog.AddButton("I like it!") + dialog.AddButton("Not so keen...") + dialog.SetDefaultButton(likeIt) + // Show() returns the clicked button's label + switch dialog.Show() { + case "I like it!": app.Dialog.Info().SetMessage("Thanks!").Show() - }) - dialog.AddButton("Not so keen...").OnClick(func() { + case "Not so keen...": app.Dialog.Info().SetMessage("Too bad!").Show() - }) - dialog.SetDefaultButton(likeIt) - dialog.Show() + } }) warningMenu := menu.AddSubmenu("Warning") From ffce968a1c5dda77f82d5645dd3a9bee3b3e2c7d Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 20 Dec 2025 15:58:53 +1100 Subject: [PATCH 3/7] feat(v3)!: MessageDialog.Show() returns (string, error) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change the signature of MessageDialog.Show() from returning just a string to returning (string, error) for consistency with other dialog methods and proper error handling. - Windows: Return error instead of calling handleFatalError() - macOS/Linux: Return nil error (no error paths currently) - Android/iOS: Return nil error (stub implementations) - Update documentation with new signature examples This is a breaking change for v3-alpha. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../content/docs/features/dialogs/message.mdx | 57 ++++++++++++++----- v3/UNRELEASED_CHANGELOG.md | 2 +- v3/pkg/application/dialogs.go | 7 ++- v3/pkg/application/dialogs_android.go | 4 +- v3/pkg/application/dialogs_darwin.go | 4 +- v3/pkg/application/dialogs_ios.go | 4 +- v3/pkg/application/dialogs_linux.go | 4 +- v3/pkg/application/dialogs_windows.go | 8 +-- 8 files changed, 60 insertions(+), 30 deletions(-) diff --git a/docs/src/content/docs/features/dialogs/message.mdx b/docs/src/content/docs/features/dialogs/message.mdx index 83d591a7ade..e5ebf67b2ca 100644 --- a/docs/src/content/docs/features/dialogs/message.mdx +++ b/docs/src/content/docs/features/dialogs/message.mdx @@ -127,7 +127,7 @@ func fetchData(app *application.App, url string) ([]byte, error) { ## Question dialog -Ask users questions and handle responses. `Show()` returns the clicked button's label: +Ask users questions and handle responses. `Show()` returns the clicked button's label and an error: ```go dialog := app.Dialog.Question(). @@ -141,8 +141,14 @@ cancel := dialog.AddButton("Cancel") dialog.SetDefaultButton(dialog.Buttons[0]) // "Save" dialog.SetCancelButton(cancel) -// Show() blocks and returns the clicked button's label -switch dialog.Show() { +// Show() blocks and returns the clicked button's label and an error +result, err := dialog.Show() +if err != nil { + // Handle error (dialog could not be displayed) + return +} + +switch result { case "Save": saveChanges() doClose() @@ -179,8 +185,14 @@ func closeDocument(app *application.App) { dialog.SetDefaultButton(save) dialog.SetCancelButton(cancel) - // Show() returns the clicked button's label - switch dialog.Show() { + // Show() returns the clicked button's label and an error + result, err := dialog.Show() + if err != nil { + // Handle error + return + } + + switch result { case "Save": if saveDocument() { doClose() @@ -232,7 +244,7 @@ dialog.Show() **Multiple buttons (Question):** -Use `AddButton()` to add buttons. `Show()` returns the label of the clicked button: +Use `AddButton()` to add buttons. `Show()` returns the label of the clicked button and an error: ```go dialog := app.Dialog.Question(). @@ -242,7 +254,13 @@ dialog.AddButton("Option 1") dialog.AddButton("Option 2") dialog.AddButton("Option 3") -switch dialog.Show() { +result, err := dialog.Show() +if err != nil { + // Handle error + return +} + +switch result { case "Option 1": handleOption1() case "Option 2": @@ -267,7 +285,13 @@ cancelBtn := dialog.AddButton("Cancel") dialog.SetDefaultButton(cancelBtn) // Safe option as default dialog.SetCancelButton(cancelBtn) // Escape triggers Cancel -if dialog.Show() == "Delete" { +result, err := dialog.Show() +if err != nil { + // Handle error + return +} + +if result == "Delete" { performDelete() } ``` @@ -281,7 +305,8 @@ dialog := app.Dialog.Question(). dialog.AddButton("Delete") dialog.AddButton("Cancel").SetAsDefault().SetAsCancel() -if dialog.Show() == "Delete" { +result, _ := dialog.Show() +if result == "Delete" { performDelete() } ``` @@ -343,8 +368,9 @@ func deleteFiles(app *application.App, paths []string) { dialog.SetDefaultButton(cancelBtn) dialog.SetCancelButton(cancelBtn) - if dialog.Show() != "Delete" { - return // User cancelled + result, err := dialog.Show() + if err != nil || result != "Delete" { + return // Error or user cancelled } // Perform deletion @@ -382,7 +408,8 @@ func confirmQuit(app *application.App) { no := dialog.AddButton("No") dialog.SetDefaultButton(no) - if dialog.Show() == "Yes" { + result, _ := dialog.Show() + if result == "Yes" { app.Quit() } } @@ -402,7 +429,8 @@ func showUpdateDialog(app *application.App) { dialog.SetDefaultButton(download) dialog.SetCancelButton(cancel) - if dialog.Show() == "📥 Download" { + result, _ := dialog.Show() + if result == "📥 Download" { app.Dialog.Info().SetMessage("Downloading...").Show() } } @@ -421,7 +449,8 @@ func showCustomIconQuestion(app *application.App, iconBytes []byte) { dialog.AddButton("Not so keen...") dialog.SetDefaultButton(likeIt) - switch dialog.Show() { + result, _ := dialog.Show() + switch result { case "I like it!": app.Dialog.Info().SetMessage("Thanks!").Show() case "Not so keen...": diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 1fe32700b91..eaf4355f9bb 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -20,7 +20,7 @@ After processing, the content will be moved to the main changelog and this file ## Changed -- **BREAKING**: `MessageDialog.Show()` now returns the clicked button's label as a string, enabling synchronous dialog workflows (#4792) +- **BREAKING**: `MessageDialog.Show()` now returns `(string, error)` - the clicked button's label and an error if the dialog could not be displayed. This enables synchronous dialog workflows and proper error handling (#4792) ## Fixed diff --git a/v3/pkg/application/dialogs.go b/v3/pkg/application/dialogs.go index 287dbc4c2cd..1b5925b4f64 100644 --- a/v3/pkg/application/dialogs.go +++ b/v3/pkg/application/dialogs.go @@ -66,7 +66,7 @@ func (b *Button) SetAsCancel() *Button { } type messageDialogImpl interface { - show() string + show() (string, error) } type MessageDialogOptions struct { @@ -109,11 +109,12 @@ func (d *MessageDialog) SetTitle(title string) *MessageDialog { // Show displays the dialog and returns the label of the clicked button. // This method blocks until the user clicks a button. // If the dialog has button callbacks defined, they will still be called. -func (d *MessageDialog) Show() string { +// Returns an error if the dialog could not be displayed. +func (d *MessageDialog) Show() (string, error) { if d.impl == nil { d.impl = newDialogImpl(d) } - return InvokeSyncWithResult(d.impl.show) + return InvokeSyncWithResultAndError(d.impl.show) } func (d *MessageDialog) SetIcon(icon []byte) *MessageDialog { diff --git a/v3/pkg/application/dialogs_android.go b/v3/pkg/application/dialogs_android.go index 6c6adfd7dd6..d27caffbc88 100644 --- a/v3/pkg/application/dialogs_android.go +++ b/v3/pkg/application/dialogs_android.go @@ -65,9 +65,9 @@ type androidDialog struct { dialog *MessageDialog } -func (d *androidDialog) show() string { +func (d *androidDialog) show() (string, error) { // TODO: Implement using AlertDialog - return "" + return "", nil } func newDialogImpl(d *MessageDialog) *androidDialog { diff --git a/v3/pkg/application/dialogs_darwin.go b/v3/pkg/application/dialogs_darwin.go index c0ef06029f3..bce48939ce2 100644 --- a/v3/pkg/application/dialogs_darwin.go +++ b/v3/pkg/application/dialogs_darwin.go @@ -371,7 +371,7 @@ type macosDialog struct { nsDialog unsafe.Pointer } -func (m *macosDialog) show() string { +func (m *macosDialog) show() (string, error) { // Channel to receive the result resultChan := make(chan string, 1) @@ -449,7 +449,7 @@ func (m *macosDialog) show() string { }) // Wait for and return the result - return <-resultChan + return <-resultChan, nil } func newDialogImpl(d *MessageDialog) *macosDialog { diff --git a/v3/pkg/application/dialogs_ios.go b/v3/pkg/application/dialogs_ios.go index 47236bb423b..f229021edef 100644 --- a/v3/pkg/application/dialogs_ios.go +++ b/v3/pkg/application/dialogs_ios.go @@ -65,9 +65,9 @@ type iosDialog struct { dialog *MessageDialog } -func (d *iosDialog) show() string { +func (d *iosDialog) show() (string, error) { // TODO: Implement using UIAlertController - return "" + return "", nil } func newDialogImpl(d *MessageDialog) *iosDialog { diff --git a/v3/pkg/application/dialogs_linux.go b/v3/pkg/application/dialogs_linux.go index f750bb62bbf..abb339a2785 100644 --- a/v3/pkg/application/dialogs_linux.go +++ b/v3/pkg/application/dialogs_linux.go @@ -27,7 +27,7 @@ type linuxDialog struct { dialog *MessageDialog } -func (m *linuxDialog) show() string { +func (m *linuxDialog) show() (string, error) { windowId := getNativeApplication().getCurrentWindowID() window, _ := globalApplication.Window.GetByID(windowId) var parent uintptr @@ -58,7 +58,7 @@ func (m *linuxDialog) show() string { }) // Wait for and return the result - return <-resultChan + return <-resultChan, nil } func newDialogImpl(d *MessageDialog) *linuxDialog { diff --git a/v3/pkg/application/dialogs_windows.go b/v3/pkg/application/dialogs_windows.go index 8d6c9b598c6..c4c18de04da 100644 --- a/v3/pkg/application/dialogs_windows.go +++ b/v3/pkg/application/dialogs_windows.go @@ -30,7 +30,7 @@ type windowsDialog struct { UseAppIcon bool } -func (m *windowsDialog) show() string { +func (m *windowsDialog) show() (string, error) { title := w32.MustStringToUTF16Ptr(m.dialog.Title) message := w32.MustStringToUTF16Ptr(m.dialog.Message) @@ -50,12 +50,12 @@ func (m *windowsDialog) show() string { // 3 is the application icon button, err = w32.MessageBoxWithIcon(parentWindow, message, title, 3, windows.MB_OK|windows.MB_USERICON) if err != nil { - globalApplication.handleFatalError(err) + return "", err } } else { button, err = windows.MessageBox(windows.HWND(parentWindow), message, title, flags|windows.MB_SYSTEMMODAL) if err != nil { - globalApplication.handleFatalError(err) + return "", err } } // This maps MessageBox return values to strings @@ -72,7 +72,7 @@ func (m *windowsDialog) show() string { } } } - return result + return result, nil } func newDialogImpl(d *MessageDialog) *windowsDialog { From da0e356d52e5981f243a6486e9e78602a5eaa3fb Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 20 Dec 2025 16:00:32 +1100 Subject: [PATCH 4/7] feat(v3): add WithButton() for builder pattern chaining MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add MessageDialog.WithButton() method that returns *MessageDialog for builder pattern chaining. This complements AddButton() which returns *Button for when you need to configure the button. Example usage: result, _ := app.Dialog.Question(). SetMessage("Choose"). WithButton("Option 1"). WithButton("Option 2"). Show() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../content/docs/features/dialogs/message.mdx | 32 +++++++++++++------ v3/UNRELEASED_CHANGELOG.md | 1 + v3/pkg/application/dialogs.go | 9 ++++++ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/docs/src/content/docs/features/dialogs/message.mdx b/docs/src/content/docs/features/dialogs/message.mdx index e5ebf67b2ca..683bb34f1c0 100644 --- a/docs/src/content/docs/features/dialogs/message.mdx +++ b/docs/src/content/docs/features/dialogs/message.mdx @@ -244,17 +244,17 @@ dialog.Show() **Multiple buttons (Question):** -Use `AddButton()` to add buttons. `Show()` returns the label of the clicked button and an error: +Use `WithButton()` for simple button additions with chaining, or `AddButton()` when you need to configure the button. +`Show()` returns the label of the clicked button and an error: ```go -dialog := app.Dialog.Question(). - SetMessage("Choose an action") - -dialog.AddButton("Option 1") -dialog.AddButton("Option 2") -dialog.AddButton("Option 3") - -result, err := dialog.Show() +// Using WithButton() for simple chaining +result, err := app.Dialog.Question(). + SetMessage("Choose an action"). + WithButton("Option 1"). + WithButton("Option 2"). + WithButton("Option 3"). + Show() if err != nil { // Handle error return @@ -270,6 +270,20 @@ case "Option 3": } ``` +Use `AddButton()` when you need to configure buttons (returns `*Button` for further configuration): + +```go +dialog := app.Dialog.Question(). + SetMessage("Choose an action") + +dialog.AddButton("Option 1").SetAsDefault() +dialog.AddButton("Option 2") +dialog.AddButton("Option 3").SetAsCancel() + +result, err := dialog.Show() +// ... +``` + **Default and Cancel buttons:** Use `SetDefaultButton()` to specify which button is highlighted and triggered by Enter. diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index eaf4355f9bb..4818dcd0702 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -17,6 +17,7 @@ After processing, the content will be moved to the main changelog and this file ## Added +- Add `MessageDialog.WithButton()` method for builder pattern chaining when adding buttons without configuration (#4792) ## Changed diff --git a/v3/pkg/application/dialogs.go b/v3/pkg/application/dialogs.go index 1b5925b4f64..6b672f85605 100644 --- a/v3/pkg/application/dialogs.go +++ b/v3/pkg/application/dialogs.go @@ -122,6 +122,8 @@ func (d *MessageDialog) SetIcon(icon []byte) *MessageDialog { return d } +// AddButton adds a button with the given label and returns the Button for further configuration. +// Use this when you need to configure the button (e.g., SetAsDefault, SetAsCancel, OnClick). func (d *MessageDialog) AddButton(s string) *Button { result := &Button{ Label: s, @@ -130,6 +132,13 @@ func (d *MessageDialog) AddButton(s string) *Button { return result } +// WithButton adds a button with the given label and returns the MessageDialog for chaining. +// Use this for simple button additions when no button configuration is needed. +func (d *MessageDialog) WithButton(s string) *MessageDialog { + d.Buttons = append(d.Buttons, &Button{Label: s}) + return d +} + func (d *MessageDialog) AddButtons(buttons []*Button) *MessageDialog { d.Buttons = buttons return d From e6994a2cf8b212dc743fee094c961d2d7f9685a2 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 20 Dec 2025 16:22:51 +1100 Subject: [PATCH 5/7] feat(v3): add WithDefaultButton/WithCancelButton and fix Linux Escape key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add WithDefaultButton() method for fluent API to add a button marked as default (Enter key triggers it) - Add WithCancelButton() method for fluent API to add a button marked as cancel (Escape key triggers it) - Fix IsCancel button not responding to Escape key on Linux (GTK) - GTK returns GTK_RESPONSE_DELETE_EVENT (-4) on Escape/window close - Now correctly maps this to the cancel button's index - Update documentation with new API methods and examples - Add example demonstrating the fluent Cancel/Default API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../content/docs/features/dialogs/message.mdx | 58 ++++++++++++++++++- v3/UNRELEASED_CHANGELOG.md | 3 + v3/examples/dialogs/main.go | 23 +++++++- v3/pkg/application/dialogs.go | 24 ++++++++ v3/pkg/application/linux_cgo.go | 13 ++++- v3/pkg/application/linux_purego.go | 13 ++++- 6 files changed, 127 insertions(+), 7 deletions(-) diff --git a/docs/src/content/docs/features/dialogs/message.mdx b/docs/src/content/docs/features/dialogs/message.mdx index 683bb34f1c0..1a5cd2e38c6 100644 --- a/docs/src/content/docs/features/dialogs/message.mdx +++ b/docs/src/content/docs/features/dialogs/message.mdx @@ -286,8 +286,34 @@ result, err := dialog.Show() **Default and Cancel buttons:** -Use `SetDefaultButton()` to specify which button is highlighted and triggered by Enter. -Use `SetCancelButton()` to specify which button is triggered by Escape. +There are multiple ways to configure default and cancel buttons: + +**1. Fluent API with `WithDefaultButton()` and `WithCancelButton()` (recommended for simple dialogs):** + +These methods add a button and mark it as default/cancel in a single call, perfect for method chaining: + +```go +result, err := app.Dialog.Question(). + SetTitle("Save Changes?"). + SetMessage("You have unsaved changes."). + WithDefaultButton("Save"). // Enter triggers this + WithButton("Don't Save"). + WithCancelButton("Cancel"). // Escape triggers this + Show() + +switch result { +case "Save": + saveChanges() +case "Don't Save": + discardChanges() +case "Cancel": + // User cancelled +} +``` + +**2. Using `SetDefaultButton()` and `SetCancelButton()` on the dialog:** + +Use these when you need a reference to the button for other purposes: ```go dialog := app.Dialog.Question(). @@ -310,7 +336,7 @@ if result == "Delete" { } ``` -You can also use the fluent `SetAsDefault()` and `SetAsCancel()` methods on buttons: +**3. Using `SetAsDefault()` and `SetAsCancel()` on individual buttons:** ```go dialog := app.Dialog.Question(). @@ -450,6 +476,32 @@ func showUpdateDialog(app *application.App) { } ``` +### Save Changes Dialog (Fluent API) + +Using the fluent `WithDefaultButton()` and `WithCancelButton()` methods for a clean, chainable API: + +```go +func promptSaveChanges(app *application.App) string { + result, _ := app.Dialog.Question(). + SetTitle("Save Changes?"). + SetMessage("You have unsaved changes. What would you like to do?"). + WithDefaultButton("Save"). + WithButton("Don't Save"). + WithCancelButton("Cancel"). + Show() + + switch result { + case "Save": + saveDocument() + return "saved" + case "Don't Save": + return "discarded" + default: + return "cancelled" + } +} +``` + ### Custom Icon Question ```go diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 959cd660b95..64c222e6578 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -18,6 +18,8 @@ After processing, the content will be moved to the main changelog and this file ## Added - Add `MessageDialog.WithButton()` method for builder pattern chaining when adding buttons without configuration (#4792) +- Add `MessageDialog.WithDefaultButton()` method for adding a button marked as default (Enter key) with builder pattern chaining (#4810) +- Add `MessageDialog.WithCancelButton()` method for adding a button marked as cancel (Escape key) with builder pattern chaining (#4810) ## Changed @@ -26,6 +28,7 @@ After processing, the content will be moved to the main changelog and this file ## Fixed +- Fix `IsCancel` button not responding to Escape key on Linux (GTK) (#4810) ## Deprecated diff --git a/v3/examples/dialogs/main.go b/v3/examples/dialogs/main.go index 3da9520a154..56ce2732c8c 100644 --- a/v3/examples/dialogs/main.go +++ b/v3/examples/dialogs/main.go @@ -87,7 +87,7 @@ func main() { no := dialog.AddButton("No") dialog.SetDefaultButton(no) // Show() returns the clicked button's label - result := dialog.Show() + result, _ := dialog.Show() if result == "Yes" { app.Quit() } @@ -101,13 +101,32 @@ func main() { dialog.SetDefaultButton(download) dialog.SetCancelButton(no) // Show() returns the clicked button's label - use switch for multiple options - switch dialog.Show() { + switch result, _ := dialog.Show(); result { case "📥 Download": app.Dialog.Info().SetMessage("Downloading...").Show() case "Cancel": // User cancelled } }) + questionMenu.Add("Question (Fluent Cancel/Default)").OnClick(func(ctx *application.Context) { + // Fluent API using WithDefaultButton and WithCancelButton + result, _ := app.Dialog.Question(). + SetTitle("Save Changes?"). + SetMessage("You have unsaved changes. What would you like to do?"). + WithDefaultButton("Save"). + WithButton("Don't Save"). + WithCancelButton("Cancel"). + Show() + + switch result { + case "Save": + app.Dialog.Info().SetMessage("Changes saved!").Show() + case "Don't Save": + app.Dialog.Info().SetMessage("Changes discarded.").Show() + case "Cancel": + // User cancelled - do nothing + } + }) questionMenu.Add("Question (Custom Icon)").OnClick(func(ctx *application.Context) { dialog := app.Dialog.Question() dialog.SetTitle("Custom Icon Example") diff --git a/v3/pkg/application/dialogs.go b/v3/pkg/application/dialogs.go index 6b672f85605..e41d29011b4 100644 --- a/v3/pkg/application/dialogs.go +++ b/v3/pkg/application/dialogs.go @@ -139,6 +139,30 @@ func (d *MessageDialog) WithButton(s string) *MessageDialog { return d } +// WithDefaultButton adds a button with the given label, marks it as the default button, +// and returns the MessageDialog for chaining. The default button is activated when the +// user presses Enter/Return. Only one button should be marked as default. +func (d *MessageDialog) WithDefaultButton(s string) *MessageDialog { + // Clear any existing default buttons + for _, b := range d.Buttons { + b.IsDefault = false + } + d.Buttons = append(d.Buttons, &Button{Label: s, IsDefault: true}) + return d +} + +// WithCancelButton adds a button with the given label, marks it as the cancel button, +// and returns the MessageDialog for chaining. The cancel button is activated when the +// user presses Escape. Only one button should be marked as cancel. +func (d *MessageDialog) WithCancelButton(s string) *MessageDialog { + // Clear any existing cancel buttons + for _, b := range d.Buttons { + b.IsCancel = false + } + d.Buttons = append(d.Buttons, &Button{Label: s, IsCancel: true}) + return d +} + func (d *MessageDialog) AddButtons(buttons []*Button) *MessageDialog { d.Buttons = buttons return d diff --git a/v3/pkg/application/linux_cgo.go b/v3/pkg/application/linux_cgo.go index 476636ade58..3bae3c0286b 100644 --- a/v3/pkg/application/linux_cgo.go +++ b/v3/pkg/application/linux_cgo.go @@ -1897,6 +1897,7 @@ func runQuestionDialog(parent pointer, options *MessageDialog) int { (*C.GtkContainer)(unsafe.Pointer(contentArea)), (*C.GtkWidget)(image)) } + cancelButtonIndex := -1 for i, button := range options.Buttons { cLabel := C.CString(button.Label) defer C.free(unsafe.Pointer(cLabel)) @@ -1906,10 +1907,20 @@ func runQuestionDialog(parent pointer, options *MessageDialog) int { if button.IsDefault { C.gtk_dialog_set_default_response((*C.GtkDialog)(dialog), index) } + if button.IsCancel { + cancelButtonIndex = i + } } defer C.gtk_widget_destroy((*C.GtkWidget)(dialog)) - return int(C.gtk_dialog_run((*C.GtkDialog)(unsafe.Pointer(dialog)))) + response := int(C.gtk_dialog_run((*C.GtkDialog)(unsafe.Pointer(dialog)))) + + // GTK_RESPONSE_DELETE_EVENT (-4) is triggered by Escape key or window close button. + // If a cancel button is defined, return its index instead. + if response == -4 && cancelButtonIndex >= 0 { + return cancelButtonIndex + } + return response } func runSaveFileDialog(dialog *SaveFileDialogStruct) (chan string, error) { diff --git a/v3/pkg/application/linux_purego.go b/v3/pkg/application/linux_purego.go index 5d6c1cd9524..c08e79b5976 100644 --- a/v3/pkg/application/linux_purego.go +++ b/v3/pkg/application/linux_purego.go @@ -1175,6 +1175,7 @@ func runQuestionDialog(parent pointer, options *MessageDialog) int { gtkContainerAdd(contentArea, image) } + cancelButtonIndex := -1 for i, button := range options.Buttons { gtkDialogAddButton( dialog, @@ -1184,9 +1185,19 @@ func runQuestionDialog(parent pointer, options *MessageDialog) int { if button.IsDefault { gtkDialogSetDefaultResponse(dialog, i) } + if button.IsCancel { + cancelButtonIndex = i + } } defer gtkWidgetDestroy(dialog) - return gtkDialogRun(dialog) + response := gtkDialogRun(dialog) + + // GTK_RESPONSE_DELETE_EVENT (-4) is triggered by Escape key or window close button. + // If a cancel button is defined, return its index instead. + if response == -4 && cancelButtonIndex >= 0 { + return cancelButtonIndex + } + return response } func runSaveFileDialog(dialog *SaveFileDialogStruct) (string, error) { From 8fcb46b4f9d6e1f9f6de02f960039a481c77d4ce Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 20 Dec 2025 16:26:01 +1100 Subject: [PATCH 6/7] fix(v3): fix Show() return value handling in example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address CodeRabbit review comment - switch dialog.Show() doesn't compile since Show() now returns (string, error). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- v3/examples/dialogs/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/examples/dialogs/main.go b/v3/examples/dialogs/main.go index 56ce2732c8c..a94e8343edd 100644 --- a/v3/examples/dialogs/main.go +++ b/v3/examples/dialogs/main.go @@ -136,7 +136,7 @@ func main() { dialog.AddButton("Not so keen...") dialog.SetDefaultButton(likeIt) // Show() returns the clicked button's label - switch dialog.Show() { + switch result, _ := dialog.Show(); result { case "I like it!": app.Dialog.Info().SetMessage("Thanks!").Show() case "Not so keen...": From 40db40402aa00f755688fb5bcd94945fe5744d54 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Mon, 22 Dec 2025 21:14:27 +1100 Subject: [PATCH 7/7] docs(dialogs): update MessageDialog docs for Buttons/Show/Result --- docs/src/content/docs/changelog.mdx | 24 +- docs/src/content/docs/concepts/lifecycle.mdx | 28 ++- .../docs/features/browser/integration.mdx | 52 ++-- .../content/docs/features/dialogs/file.mdx | 204 ++++++++-------- .../content/docs/features/dialogs/message.mdx | 231 +++++++++--------- .../docs/features/dialogs/overview.mdx | 230 ++++++++--------- .../content/docs/reference/application.mdx | 52 ++-- docs/src/content/docs/reference/dialogs.mdx | 198 ++++++--------- 8 files changed, 497 insertions(+), 522 deletions(-) diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx index e93284cf9ff..39868070097 100644 --- a/docs/src/content/docs/changelog.mdx +++ b/docs/src/content/docs/changelog.mdx @@ -43,7 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Changed - Update the documentation page for Wails v3 Asset Server by @ndianabasi - **BREAKING**: Remove package-level dialog functions (`application.InfoDialog()`, `application.QuestionDialog()`, etc.). Use the `app.Dialog` manager instead: `app.Dialog.Info()`, `app.Dialog.Question()`, `app.Dialog.Warning()`, `app.Dialog.Error()`, `app.Dialog.OpenFile()`, `app.Dialog.SaveFile()` -- Update dialogs documentation to match actual API: use `app.Dialog.*`, `AddButton()` with callbacks (not `SetButtons()`), `SetDefaultButton(*Button)` (not string), `AddFilter()` (not `SetFilters()`), `SetFilename()` (not `SetDefaultFilename()`), and `app.Dialog.OpenFile().CanChooseDirectories(true)` for folder selection +- Update dialogs documentation to match actual API: use `app.Dialog.*`, `Buttons(...)`, `Show() error` (non-blocking), `Result() (string, error)` (blocking), `AddFilter()`, `SetFilename()`, and `app.Dialog.OpenFile().CanChooseDirectories(true)` for folder selection ## Fixed - Fix crash on macOS when toggling window visibility via Hide()/Show() with ApplicationShouldTerminateAfterLastWindowClosed enabled (#4389) by @leaanthony @@ -197,30 +197,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## v3.0.0-alpha.29 - 2025-09-25 ## Added -- macOS: Shows native window controls in the menu bar in [#4588](https://github.com/wailsapp/wails/pull/4588) by @nidib -- Add macOS Dock service to hide/show app icon in the dock @popaprozac in [PR](https://github.com/wailsapp/wails/pull/4451) +- macOS: Shows native window controls in the menu bar in [#4588](https://github.com/wailsapp/wails/pull/4588) by @nidib +- Add macOS Dock service to hide/show app icon in the dock @popaprozac in [PR](https://github.com/wailsapp/wails/pull/4451) ## Changed -- macOS: Use `visibleFrame` instead of `frame` for window centering to exclude menu bar and dock areas +- macOS: Use `visibleFrame` instead of `frame` for window centering to exclude menu bar and dock areas ## Fixed -- Fixed redefinition error for liquid glass demo in [#4542](https://github.com/wailsapp/wails/pull/4542) by @Etesam913 -- Fixed issue where AssetServer can crash on MacOS in [#4576](https://github.com/wailsapp/wails/pull/4576) by @jghiloni -- Fixed compilation issue when building with NextJs. Fixed in [#4585](https://github.com/wailsapp/wails/pull/4585) by @rev42 +- Fixed redefinition error for liquid glass demo in [#4542](https://github.com/wailsapp/wails/pull/4542) by @Etesam913 +- Fixed issue where AssetServer can crash on MacOS in [#4576](https://github.com/wailsapp/wails/pull/4576) by @jghiloni +- Fixed compilation issue when building with NextJs. Fixed in [#4585](https://github.com/wailsapp/wails/pull/4585) by @rev42 - Fixed pipelines for nightly release in [#4597](https://github.com/wailsapp/wails/pull/4597) by @riadafridishibly ## v3.0.0-alpha.29 - 2025-09-25 ## Added -- Add macOS Dock service to hide/show app icon in the dock @popaprozac in [PR](https://github.com/wailsapp/wails/pull/4451) +- Add macOS Dock service to hide/show app icon in the dock @popaprozac in [PR](https://github.com/wailsapp/wails/pull/4451) ## Changed -- macOS: Use `visibleFrame` instead of `frame` for window centering to exclude menu bar and dock areas +- macOS: Use `visibleFrame` instead of `frame` for window centering to exclude menu bar and dock areas ## Fixed -- Fixed redefinition error for liquid glass demo in [#4542](https://github.com/wailsapp/wails/pull/4542) by @Etesam913 -- Fixed issue where AssetServer can crash on MacOS in [#4576](https://github.com/wailsapp/wails/pull/4576) by @jghiloni -- Fixed compilation issue when building with NextJs. Fixed in [#4585](https://github.com/wailsapp/wails/pull/4585) by @rev42 +- Fixed redefinition error for liquid glass demo in [#4542](https://github.com/wailsapp/wails/pull/4542) by @Etesam913 +- Fixed issue where AssetServer can crash on MacOS in [#4576](https://github.com/wailsapp/wails/pull/4576) by @jghiloni +- Fixed compilation issue when building with NextJs. Fixed in [#4585](https://github.com/wailsapp/wails/pull/4585) by @rev42 - Fixed pipelines for nightly release in [#4597](https://github.com/wailsapp/wails/pull/4597) by @riadafridishibly ## v3.0.0-alpha.27 - 2025-09-07 diff --git a/docs/src/content/docs/concepts/lifecycle.mdx b/docs/src/content/docs/concepts/lifecycle.mdx index c7f6c080583..36bd08625d6 100644 --- a/docs/src/content/docs/concepts/lifecycle.mdx +++ b/docs/src/content/docs/concepts/lifecycle.mdx @@ -277,15 +277,17 @@ app := application.New(application.Options{ } // Prompt the user - result, _ := application.QuestionDialog(). + result, _ := app.Dialog.Question(). SetTitle("Unsaved Changes"). SetMessage("You have unsaved changes. Quit anyway?"). - AddButton("Quit", "quit"). - AddButton("Cancel", "cancel"). - Show() + Buttons( + application.Button{Label: "Quit"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, + ). + Result() // Only quit if user clicked "Quit" - return result == "quit" + return result == "Quit" }, }) ``` @@ -503,19 +505,21 @@ window := app.Window.NewWithOptions(application.WebviewWindowOptions{ window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { if hasUnsavedChanges() { // Show dialog - result, _ := application.QuestionDialog(). + result, _ := app.Dialog.Question(). SetTitle("Unsaved Changes"). SetMessage("Save before closing?"). - AddButton("Save", "save"). - AddButton("Discard", "discard"). - AddButton("Cancel", "cancel"). - Show() + Buttons( + application.Button{Label: "Save", IsDefault: true}, + application.Button{Label: "Discard"}, + application.Button{Label: "Cancel", IsCancel: true}, + ). + Result() switch result { - case "save": + case "Save": saveChanges() // Allow close - case "cancel": + case "Cancel": e.Cancel() // Prevent close } // "discard" falls through and allows close diff --git a/docs/src/content/docs/features/browser/integration.mdx b/docs/src/content/docs/features/browser/integration.mdx index e1c12b90fa4..d91721f153e 100644 --- a/docs/src/content/docs/features/browser/integration.mdx +++ b/docs/src/content/docs/features/browser/integration.mdx @@ -153,16 +153,19 @@ func handleExternalLink(app *application.App, url string) { dialog := app.Dialog.Question() dialog.SetTitle("Open External Link") dialog.SetMessage(fmt.Sprintf("Open %s in your browser?", url)) - - dialog.AddButton("Open").OnClick(func() { - err := app.Browser.OpenURL(url) - if err != nil { - app.Logger.Error("Failed to open URL", "url", url, "error", err) - } - }) - - dialog.AddButton("Cancel") - dialog.Show() + + dialog.Buttons( + application.Button{Label: "Open"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, + ) + + result, _ := dialog.Result() + if result == "Open" { + err := app.Browser.OpenURL(url) + if err != nil { + app.Logger.Error("Failed to open URL", "url", url, "error", err) + } + } } func isValidURL(url string) bool { @@ -443,15 +446,18 @@ func openWithConfirmation(app *application.App, url string) { dialog := app.Dialog.Question() dialog.SetTitle("Open External Link") dialog.SetMessage(fmt.Sprintf("Open %s in your browser?", url)) - - dialog.AddButton("Open").OnClick(func() { - if err := app.Browser.OpenURL(url); err != nil { - showError(app, "Failed to open URL", err) - } - }) - - dialog.AddButton("Cancel") - dialog.Show() + + dialog.Buttons( + application.Button{Label: "Open"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, + ) + + result, _ := dialog.Result() + if result == "Open" { + if err := app.Browser.OpenURL(url); err != nil { + showError(app, "Failed to open URL", err) + } + } } func generateHTMLReport(app *application.App) { @@ -495,10 +501,10 @@ func generateHTMLReport(app *application.App) { } func showError(app *application.App, message string, err error) { - app.Dialog.Error(). - SetTitle("Error"). - SetMessage(fmt.Sprintf("%s: %v", message, err)). - Show() + _ = app.Dialog.Error(). + SetTitle("Error"). + SetMessage(fmt.Sprintf("%s: %v", message, err)). + Show() } ``` diff --git a/docs/src/content/docs/features/dialogs/file.mdx b/docs/src/content/docs/features/dialogs/file.mdx index bba6e3f03fd..3b851b81efc 100644 --- a/docs/src/content/docs/features/dialogs/file.mdx +++ b/docs/src/content/docs/features/dialogs/file.mdx @@ -148,16 +148,15 @@ if _, err := os.Stat(path); err == nil { SetTitle("Confirm Overwrite"). SetMessage("File already exists. Overwrite?") - overwriteBtn := dialog.AddButton("Overwrite") - overwriteBtn.OnClick(func() { - saveFile(path, data) - }) - - cancelBtn := dialog.AddButton("Cancel") - dialog.SetDefaultButton(cancelBtn) - dialog.SetCancelButton(cancelBtn) - dialog.Show() - return + dialog.Buttons( + application.Button{Label: "Overwrite"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, + ) + + result, err := dialog.Result() + if err != nil || result != "Overwrite" { + return + } } saveFile(path, data) @@ -263,10 +262,10 @@ func openImage(app *application.App) (image.Image, error) { img, _, err := image.Decode(file) if err != nil { - app.Dialog.Error(). - SetTitle("Invalid Image"). - SetMessage("Could not decode image file."). - Show() + _ = app.Dialog.Error(). + SetTitle("Invalid Image"). + SetMessage("Could not decode image file."). + Show() return nil, err } @@ -296,16 +295,15 @@ func saveDocument(app *application.App, content string) { SetTitle("Confirm Extension"). SetMessage(fmt.Sprintf("Save as %s file?", ext)) - saveBtn := dialog.AddButton("Save") - saveBtn.OnClick(func() { - doSave(app, path, content) - }) + dialog.Buttons( + application.Button{Label: "Save"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, + ) - cancelBtn := dialog.AddButton("Cancel") - dialog.SetDefaultButton(cancelBtn) - dialog.SetCancelButton(cancelBtn) - dialog.Show() - return + result, err := dialog.Result() + if err != nil || result != "Save" { + return + } } doSave(app, path, content) @@ -313,17 +311,17 @@ func saveDocument(app *application.App, content string) { func doSave(app *application.App, path, content string) { if err := os.WriteFile(path, []byte(content), 0644); err != nil { - app.Dialog.Error(). - SetTitle("Save Failed"). - SetMessage(err.Error()). - Show() + _ = app.Dialog.Error(). + SetTitle("Save Failed"). + SetMessage(err.Error()). + Show() return } - app.Dialog.Info(). - SetTitle("Saved"). - SetMessage("Document saved successfully!"). - Show() + _ = app.Dialog.Info(). + SetTitle("Saved"). + SetMessage("Document saved successfully!"). + Show() } ``` @@ -345,41 +343,44 @@ func processMultipleFiles(app *application.App) { SetTitle("Confirm Processing"). SetMessage(fmt.Sprintf("Process %d file(s)?", len(paths))) - processBtn := dialog.AddButton("Process") - processBtn.OnClick(func() { - // Process files - var errs []error - for i, path := range paths { - if err := processFile(path); err != nil { - errs = append(errs, err) - } - - // Update progress - // app.Event.Emit("progress", map[string]interface{}{ - // "current": i + 1, - // "total": len(paths), - // }) - _ = i // suppress unused variable warning in example - } + dialog.Buttons( + application.Button{Label: "Process"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, + ) + + result, err := dialog.Result() + if err != nil || result != "Process" { + return + } - // Show results - if len(errs) > 0 { - app.Dialog.Warning(). - SetTitle("Processing Complete"). - SetMessage(fmt.Sprintf("Processed %d files with %d errors.", - len(paths), len(errs))). - Show() - } else { - app.Dialog.Info(). - SetTitle("Success"). - SetMessage(fmt.Sprintf("Processed %d files successfully!", len(paths))). - Show() + // Process files + var errs []error + for i, path := range paths { + if err := processFile(path); err != nil { + errs = append(errs, err) } - }) - cancelBtn := dialog.AddButton("Cancel") - dialog.SetCancelButton(cancelBtn) - dialog.Show() + // Update progress + // app.Event.Emit("progress", map[string]interface{}{ + // "current": i + 1, + // "total": len(paths), + // }) + _ = i // suppress unused variable warning in example + } + + // Show results + if len(errs) > 0 { + _ = app.Dialog.Warning(). + SetTitle("Processing Complete"). + SetMessage(fmt.Sprintf("Processed %d files with %d errors.", + len(paths), len(errs))). + Show() + } else { + _ = app.Dialog.Info(). + SetTitle("Success"). + SetMessage(fmt.Sprintf("Processed %d files successfully!", len(paths))). + Show() + } } ``` @@ -406,10 +407,10 @@ func exportData(app *application.App, data []byte) { // Save file if err := os.WriteFile(path, data, 0644); err != nil { - app.Dialog.Error(). - SetTitle("Export Failed"). - SetMessage(err.Error()). - Show() + _ = app.Dialog.Error(). + SetTitle("Export Failed"). + SetMessage(err.Error()). + Show() return } @@ -418,13 +419,15 @@ func exportData(app *application.App, data []byte) { SetTitle("Export Complete"). SetMessage(fmt.Sprintf("Exported to %s", filename)) - openBtn := dialog.AddButton("Open Folder") - openBtn.OnClick(func() { - openFolder(folder) - }) + dialog.Buttons( + application.Button{Label: "Open Folder"}, + application.Button{Label: "OK", IsDefault: true, IsCancel: true}, + ) - dialog.AddButton("OK") - dialog.Show() + result, _ := dialog.Result() + if result == "Open Folder" { + openFolder(folder) + } } ``` @@ -445,20 +448,20 @@ func importConfiguration(app *application.App) { // Read file data, err := os.ReadFile(path) if err != nil { - app.Dialog.Error(). - SetTitle("Read Failed"). - SetMessage(err.Error()). - Show() + _ = app.Dialog.Error(). + SetTitle("Read Failed"). + SetMessage(err.Error()). + Show() return } // Validate configuration config, err := parseConfig(data) if err != nil { - app.Dialog.Error(). - SetTitle("Invalid Configuration"). - SetMessage("File is not a valid configuration."). - Show() + _ = app.Dialog.Error(). + SetTitle("Invalid Configuration"). + SetMessage("File is not a valid configuration."). + Show() return } @@ -467,26 +470,29 @@ func importConfiguration(app *application.App) { SetTitle("Confirm Import"). SetMessage("Import this configuration?") - importBtn := dialog.AddButton("Import") - importBtn.OnClick(func() { - // Apply configuration - if err := applyConfig(config); err != nil { - app.Dialog.Error(). - SetTitle("Import Failed"). - SetMessage(err.Error()). - Show() - return - } + dialog.Buttons( + application.Button{Label: "Import"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, + ) - app.Dialog.Info(). - SetTitle("Success"). - SetMessage("Configuration imported successfully!"). - Show() - }) + result, err := dialog.Result() + if err != nil || result != "Import" { + return + } - cancelBtn := dialog.AddButton("Cancel") - dialog.SetCancelButton(cancelBtn) - dialog.Show() + // Apply configuration + if err := applyConfig(config); err != nil { + _ = app.Dialog.Error(). + SetTitle("Import Failed"). + SetMessage(err.Error()). + Show() + return + } + + _ = app.Dialog.Info(). + SetTitle("Success"). + SetMessage("Configuration imported successfully!"). + Show() } ``` diff --git a/docs/src/content/docs/features/dialogs/message.mdx b/docs/src/content/docs/features/dialogs/message.mdx index 1a5cd2e38c6..563dad08210 100644 --- a/docs/src/content/docs/features/dialogs/message.mdx +++ b/docs/src/content/docs/features/dialogs/message.mdx @@ -29,7 +29,8 @@ All methods return a `*MessageDialog` that can be configured using method chaini Display informational messages: ```go -app.Dialog.Info(). + +_ = app.Dialog.Info(). SetTitle("Success"). SetMessage("File saved successfully!"). Show() @@ -49,10 +50,10 @@ func saveFile(app *application.App, path string, data []byte) error { return err } - app.Dialog.Info(). - SetTitle("File Saved"). - SetMessage(fmt.Sprintf("Saved to %s", filepath.Base(path))). - Show() + _ = app.Dialog.Info(). + SetTitle("File Saved"). + SetMessage(fmt.Sprintf("Saved to %s", filepath.Base(path))). + Show() return nil } @@ -63,7 +64,8 @@ func saveFile(app *application.App, path string, data []byte) error { Show warnings: ```go -app.Dialog.Warning(). + +_ = app.Dialog.Warning(). SetTitle("Warning"). SetMessage("This action cannot be undone."). Show() @@ -82,10 +84,10 @@ func checkDiskSpace(app *application.App) { available := getDiskSpace() if available < 100*1024*1024 { // Less than 100MB - app.Dialog.Warning(). - SetTitle("Low Disk Space"). - SetMessage(fmt.Sprintf("Only %d MB available.", available/(1024*1024))). - Show() + _ = app.Dialog.Warning(). + SetTitle("Low Disk Space"). + SetMessage(fmt.Sprintf("Only %d MB available.", available/(1024*1024))). + Show() } } ``` @@ -95,7 +97,8 @@ func checkDiskSpace(app *application.App) { Display errors: ```go -app.Dialog.Error(). + +_ = app.Dialog.Error(). SetTitle("Error"). SetMessage("Failed to connect to server."). Show() @@ -113,10 +116,10 @@ app.Dialog.Error(). func fetchData(app *application.App, url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { - app.Dialog.Error(). - SetTitle("Network Error"). - SetMessage(fmt.Sprintf("Failed to connect: %v", err)). - Show() + _ = app.Dialog.Error(). + SetTitle("Network Error"). + SetMessage(fmt.Sprintf("Failed to connect: %v", err)). + Show() return nil, err } defer resp.Body.Close() @@ -127,22 +130,21 @@ func fetchData(app *application.App, url string) ([]byte, error) { ## Question dialog -Ask users questions and handle responses. `Show()` returns the clicked button's label and an error: +Ask users questions and handle responses. `Show()` is non-blocking and returns an error if the dialog could not be displayed. Use `Result()` to block and retrieve the clicked button's label: ```go dialog := app.Dialog.Question(). SetTitle("Confirm"). SetMessage("Save changes before closing?") -dialog.AddButton("Save") -dialog.AddButton("Don't Save") -cancel := dialog.AddButton("Cancel") +dialog.Buttons( + application.Button{Label: "Save", IsDefault: true}, + application.Button{Label: "Don't Save"}, + application.Button{Label: "Cancel", IsCancel: true}, +) -dialog.SetDefaultButton(dialog.Buttons[0]) // "Save" -dialog.SetCancelButton(cancel) - -// Show() blocks and returns the clicked button's label and an error -result, err := dialog.Show() +// Result() blocks and returns the clicked button's label and an error +result, err := dialog.Result() if err != nil { // Handle error (dialog could not be displayed) return @@ -178,15 +180,14 @@ func closeDocument(app *application.App) { SetTitle("Unsaved Changes"). SetMessage("Do you want to save your changes?") - save := dialog.AddButton("Save") - dialog.AddButton("Don't Save") - cancel := dialog.AddButton("Cancel") - - dialog.SetDefaultButton(save) - dialog.SetCancelButton(cancel) + dialog.Buttons( + application.Button{Label: "Save", IsDefault: true}, + application.Button{Label: "Don't Save"}, + application.Button{Label: "Cancel", IsCancel: true}, + ) - // Show() returns the clicked button's label and an error - result, err := dialog.Show() + // Result() returns the clicked button's label and an error + result, err := dialog.Result() if err != nil { // Handle error return @@ -237,24 +238,27 @@ You can also add custom buttons: ```go dialog := app.Dialog.Info(). SetMessage("Done!") -ok := dialog.AddButton("Got it!") -dialog.SetDefaultButton(ok) -dialog.Show() + +dialog.Buttons( + application.Button{Label: "Got it!", IsDefault: true, IsCancel: true}, +) + +_ = dialog.Show() ``` **Multiple buttons (Question):** -Use `WithButton()` for simple button additions with chaining, or `AddButton()` when you need to configure the button. -`Show()` returns the label of the clicked button and an error: +Use `Buttons(...)` to define buttons. Use `Result()` to block and retrieve the clicked button's label: ```go -// Using WithButton() for simple chaining result, err := app.Dialog.Question(). SetMessage("Choose an action"). - WithButton("Option 1"). - WithButton("Option 2"). - WithButton("Option 3"). - Show() + Buttons( + application.Button{Label: "Option 1"}, + application.Button{Label: "Option 2"}, + application.Button{Label: "Option 3"}, + ). + Result() if err != nil { // Handle error return @@ -270,36 +274,36 @@ case "Option 3": } ``` -Use `AddButton()` when you need to configure buttons (returns `*Button` for further configuration): +You can configure default/cancel buttons inline: ```go dialog := app.Dialog.Question(). SetMessage("Choose an action") -dialog.AddButton("Option 1").SetAsDefault() -dialog.AddButton("Option 2") -dialog.AddButton("Option 3").SetAsCancel() +dialog.Buttons( + application.Button{Label: "Option 1", IsDefault: true}, + application.Button{Label: "Option 2"}, + application.Button{Label: "Option 3", IsCancel: true}, +) -result, err := dialog.Show() +result, err := dialog.Result() // ... ``` **Default and Cancel buttons:** -There are multiple ways to configure default and cancel buttons: - -**1. Fluent API with `WithDefaultButton()` and `WithCancelButton()` (recommended for simple dialogs):** - -These methods add a button and mark it as default/cancel in a single call, perfect for method chaining: +Use `IsDefault` and `IsCancel` to configure default/cancel behavior: ```go result, err := app.Dialog.Question(). SetTitle("Save Changes?"). SetMessage("You have unsaved changes."). - WithDefaultButton("Save"). // Enter triggers this - WithButton("Don't Save"). - WithCancelButton("Cancel"). // Escape triggers this - Show() + Buttons( + application.Button{Label: "Save", IsDefault: true}, + application.Button{Label: "Don't Save"}, + application.Button{Label: "Cancel", IsCancel: true}, + ). + Result() switch result { case "Save": @@ -311,21 +315,23 @@ case "Cancel": } ``` -**2. Using `SetDefaultButton()` and `SetCancelButton()` on the dialog:** +**2. Using `Default()` and `Cancel()` on the dialog:** -Use these when you need a reference to the button for other purposes: +Use these when you want to set default/cancel behavior by button label: ```go dialog := app.Dialog.Question(). SetMessage("Delete file?") -dialog.AddButton("Delete") -cancelBtn := dialog.AddButton("Cancel") +dialog.Buttons( + application.Button{Label: "Delete"}, + application.Button{Label: "Cancel"}, +) -dialog.SetDefaultButton(cancelBtn) // Safe option as default -dialog.SetCancelButton(cancelBtn) // Escape triggers Cancel +dialog.Default("Cancel") +dialog.Cancel("Cancel") -result, err := dialog.Show() +result, err := dialog.Result() if err != nil { // Handle error return @@ -336,21 +342,6 @@ if result == "Delete" { } ``` -**3. Using `SetAsDefault()` and `SetAsCancel()` on individual buttons:** - -```go -dialog := app.Dialog.Question(). - SetMessage("Delete file?") - -dialog.AddButton("Delete") -dialog.AddButton("Cancel").SetAsDefault().SetAsCancel() - -result, _ := dialog.Show() -if result == "Delete" { - performDelete() -} -``` - **Best practices:** - **1-3 buttons:** Don't overwhelm users - **Clear labels:** "Save" not "OK" @@ -366,7 +357,7 @@ app.Dialog.Info(). SetTitle("Custom Icon Example"). SetMessage("Using a custom icon"). SetIcon(myIconBytes). - Show() + Show() ``` ### Window Attachment @@ -378,8 +369,11 @@ dialog := app.Dialog.Question(). SetMessage("Window-specific question"). AttachToWindow(window) -dialog.AddButton("OK") -dialog.Show() +dialog.Buttons( + application.Button{Label: "OK", IsDefault: true, IsCancel: true}, +) + +_ = dialog.Show() ``` **Benefits:** @@ -403,12 +397,12 @@ func deleteFiles(app *application.App, paths []string) { SetTitle("Confirm Delete"). SetMessage(message) - dialog.AddButton("Delete") - cancelBtn := dialog.AddButton("Cancel") - dialog.SetDefaultButton(cancelBtn) - dialog.SetCancelButton(cancelBtn) + dialog.Buttons( + application.Button{Label: "Delete"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, + ) - result, err := dialog.Show() + result, err := dialog.Result() if err != nil || result != "Delete" { return // Error or user cancelled } @@ -423,15 +417,15 @@ func deleteFiles(app *application.App, paths []string) { // Show result if len(errs) > 0 { - app.Dialog.Error(). - SetTitle("Delete Failed"). - SetMessage(fmt.Sprintf("Failed to delete %d file(s)", len(errs))). - Show() + _ = app.Dialog.Error(). + SetTitle("Delete Failed"). + SetMessage(fmt.Sprintf("Failed to delete %d file(s)", len(errs))). + Show() } else { - app.Dialog.Info(). - SetTitle("Delete Complete"). - SetMessage(fmt.Sprintf("Deleted %d file(s)", len(paths))). - Show() + _ = app.Dialog.Info(). + SetTitle("Delete Complete"). + SetMessage(fmt.Sprintf("Deleted %d file(s)", len(paths))). + Show() } } ``` @@ -444,11 +438,12 @@ func confirmQuit(app *application.App) { SetTitle("Quit"). SetMessage("You have unsaved work. Are you sure you want to quit?") - dialog.AddButton("Yes") - no := dialog.AddButton("No") - dialog.SetDefaultButton(no) + dialog.Buttons( + application.Button{Label: "Yes"}, + application.Button{Label: "No", IsDefault: true, IsCancel: true}, + ) - result, _ := dialog.Show() + result, _ := dialog.Result() if result == "Yes" { app.Quit() } @@ -463,32 +458,31 @@ func showUpdateDialog(app *application.App) { SetTitle("Update"). SetMessage("A new version is available. The cancel button is selected when pressing escape.") - download := dialog.AddButton("📥 Download") - cancel := dialog.AddButton("Cancel") - - dialog.SetDefaultButton(download) - dialog.SetCancelButton(cancel) + dialog.Buttons( + application.Button{Label: "📥 Download", IsDefault: true}, + application.Button{Label: "Cancel", IsCancel: true}, + ) - result, _ := dialog.Show() + result, _ := dialog.Result() if result == "📥 Download" { - app.Dialog.Info().SetMessage("Downloading...").Show() + _ = app.Dialog.Info().SetMessage("Downloading...").Show() } } ``` -### Save Changes Dialog (Fluent API) - -Using the fluent `WithDefaultButton()` and `WithCancelButton()` methods for a clean, chainable API: +### Save Changes Dialog ```go func promptSaveChanges(app *application.App) string { result, _ := app.Dialog.Question(). SetTitle("Save Changes?"). SetMessage("You have unsaved changes. What would you like to do?"). - WithDefaultButton("Save"). - WithButton("Don't Save"). - WithCancelButton("Cancel"). - Show() + Buttons( + application.Button{Label: "Save", IsDefault: true}, + application.Button{Label: "Don't Save"}, + application.Button{Label: "Cancel", IsCancel: true}, + ). + Result() switch result { case "Save": @@ -511,16 +505,17 @@ func showCustomIconQuestion(app *application.App, iconBytes []byte) { SetMessage("Using a custom icon"). SetIcon(iconBytes) - likeIt := dialog.AddButton("I like it!") - dialog.AddButton("Not so keen...") - dialog.SetDefaultButton(likeIt) + dialog.Buttons( + application.Button{Label: "I like it!", IsDefault: true}, + application.Button{Label: "Not so keen..."}, + ) - result, _ := dialog.Show() + result, _ := dialog.Result() switch result { case "I like it!": - app.Dialog.Info().SetMessage("Thanks!").Show() + _ = app.Dialog.Info().SetMessage("Thanks!").Show() case "Not so keen...": - app.Dialog.Info().SetMessage("Too bad!").Show() + _ = app.Dialog.Info().SetMessage("Too bad!").Show() } } diff --git a/docs/src/content/docs/features/dialogs/overview.mdx b/docs/src/content/docs/features/dialogs/overview.mdx index 25dcaf9010c..0e74ea1f420 100644 --- a/docs/src/content/docs/features/dialogs/overview.mdx +++ b/docs/src/content/docs/features/dialogs/overview.mdx @@ -15,25 +15,25 @@ Wails provides **native system dialogs** that work across all platforms: message ```go // Information dialog -app.Dialog.Info(). + +_ = app.Dialog.Info(). SetTitle("Success"). SetMessage("File saved successfully!"). - Show() + Show() -// Question dialog with button callbacks +// Question dialog (use Result() to get clicked button label) dialog := app.Dialog.Question(). SetTitle("Confirm"). SetMessage("Delete this file?") -deleteBtn := dialog.AddButton("Delete") -deleteBtn.OnClick(func() { - deleteFile() -}) +dialog.Buttons( + application.Button{Label: "Delete"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, +) -cancelBtn := dialog.AddButton("Cancel") -dialog.SetDefaultButton(cancelBtn) -dialog.SetCancelButton(cancelBtn) -dialog.Show() +if result, _ := dialog.Result(); result == "Delete" { + deleteFile() +} // File open dialog path, _ := app.Dialog.OpenFile(). @@ -67,7 +67,7 @@ Display simple messages: app.Dialog.Info(). SetTitle("Welcome"). SetMessage("Welcome to our application!"). - Show() + Show() ``` **Use cases:** @@ -83,7 +83,7 @@ Show warnings: app.Dialog.Warning(). SetTitle("Warning"). SetMessage("This action cannot be undone."). - Show() + Show() ``` **Use cases:** @@ -99,7 +99,7 @@ Display errors: app.Dialog.Error(). SetTitle("Error"). SetMessage("Failed to save file: " + err.Error()). - Show() + Show() ``` **Use cases:** @@ -109,22 +109,21 @@ app.Dialog.Error(). ### Question dialog -Ask users questions and handle responses via button callbacks: +Ask users questions and handle responses via `Result()`: ```go dialog := app.Dialog.Question(). SetTitle("Confirm Delete"). SetMessage("Are you sure you want to delete this file?") -deleteBtn := dialog.AddButton("Delete") -deleteBtn.OnClick(func() { - deleteFile() -}) +dialog.Buttons( + application.Button{Label: "Delete"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, +) -cancelBtn := dialog.AddButton("Cancel") -dialog.SetDefaultButton(cancelBtn) -dialog.SetCancelButton(cancelBtn) -dialog.Show() +if result, _ := dialog.Result(); result == "Delete" { + deleteFile() +} ``` **Use cases:** @@ -216,53 +215,49 @@ Info, warning, and error dialogs display a default "OK" button: ```go app.Dialog.Info(). SetMessage("Done!"). - Show() + Show() ``` **Custom buttons for question dialogs:** -Use `AddButton()` to add buttons, which returns a `*Button` you can configure with callbacks: +Use `Buttons(...)` to define buttons and `Result()` to retrieve the clicked button label: ```go dialog := app.Dialog.Question(). SetMessage("Choose action") -save := dialog.AddButton("Save") -save.OnClick(func() { - saveDocument() -}) - -dontSave := dialog.AddButton("Don't Save") -dontSave.OnClick(func() { - discardChanges() -}) - -cancel := dialog.AddButton("Cancel") -// No callback needed - just dismisses dialog - -dialog.SetDefaultButton(save) -dialog.SetCancelButton(cancel) -dialog.Show() +dialog.Buttons( + application.Button{Label: "Save", IsDefault: true}, + application.Button{Label: "Don't Save"}, + application.Button{Label: "Cancel", IsCancel: true}, +) + +result, _ := dialog.Result() +switch result { +case "Save": + saveDocument() +case "Don't Save": + discardChanges() +} ``` **Default and Cancel buttons:** -Use `SetDefaultButton()` to specify which button is highlighted and triggered by Enter. -Use `SetCancelButton()` to specify which button is triggered by Escape. +Use `IsDefault` to specify which button is highlighted and triggered by Enter. +Use `IsCancel` to specify which button is triggered by Escape. ```go dialog := app.Dialog.Question(). SetMessage("Delete file?") -deleteBtn := dialog.AddButton("Delete") -deleteBtn.OnClick(func() { - performDelete() -}) +dialog.Buttons( + application.Button{Label: "Delete"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, +) -cancelBtn := dialog.AddButton("Cancel") -dialog.SetDefaultButton(cancelBtn) // Safe option highlighted by default -dialog.SetCancelButton(cancelBtn) // Escape triggers Cancel -dialog.Show() +if result, _ := dialog.Result(); result == "Delete" { + performDelete() +} ``` ### Window Attachment @@ -274,7 +269,7 @@ dialog := app.Dialog.Info(). SetMessage("Window-specific message"). AttachToWindow(window) -dialog.Show() + _ = dialog.Show() ``` **Behaviour:** @@ -301,9 +296,11 @@ dialog.Show() dialog := app.Dialog.Question(). SetMessage("Save changes?"). AttachToWindow(window) - dialog.AddButton("Yes") - dialog.AddButton("No") - dialog.Show() + dialog.Buttons( + application.Button{Label: "Yes"}, + application.Button{Label: "No", IsDefault: true, IsCancel: true}, + ) + _ = dialog.Show() ``` @@ -323,7 +320,7 @@ dialog.Show() app.Dialog.Error(). SetTitle("Error"). SetMessage("Operation failed"). - Show() + Show() ``` @@ -341,7 +338,7 @@ dialog.Show() // GTK dialog on Linux app.Dialog.Info(). SetMessage("Update complete"). - Show() + Show() ``` @@ -356,20 +353,22 @@ func deleteFile(app *application.App, path string) { SetTitle("Confirm Delete"). SetMessage(fmt.Sprintf("Delete %s?", filepath.Base(path))) - deleteBtn := dialog.AddButton("Delete") - deleteBtn.OnClick(func() { - if err := os.Remove(path); err != nil { - app.Dialog.Error(). - SetTitle("Delete Failed"). - SetMessage(err.Error()). - Show() - } - }) - - cancelBtn := dialog.AddButton("Cancel") - dialog.SetDefaultButton(cancelBtn) - dialog.SetCancelButton(cancelBtn) - dialog.Show() + dialog.Buttons( + application.Button{Label: "Delete"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, + ) + + result, err := dialog.Result() + if err != nil || result != "Delete" { + return + } + + if err := os.Remove(path); err != nil { + _ = app.Dialog.Error(). + SetTitle("Delete Failed"). + SetMessage(err.Error()). + Show() + } } ``` @@ -378,17 +377,17 @@ func deleteFile(app *application.App, path string) { ```go func saveDocument(app *application.App, path string, data []byte) { if err := os.WriteFile(path, data, 0644); err != nil { - app.Dialog.Error(). - SetTitle("Save Failed"). - SetMessage(fmt.Sprintf("Could not save file: %v", err)). - Show() + _ = app.Dialog.Error(). + SetTitle("Save Failed"). + SetMessage(fmt.Sprintf("Could not save file: %v", err)). + Show() return } - app.Dialog.Info(). - SetTitle("Success"). - SetMessage("File saved successfully!"). - Show() + _ = app.Dialog.Info(). + SetTitle("Success"). + SetMessage("File saved successfully!"). + Show() } ``` @@ -411,10 +410,10 @@ func selectImageFile(app *application.App) (string, error) { // Validate file if !isValidImage(path) { - app.Dialog.Error(). - SetTitle("Invalid File"). - SetMessage("Selected file is not a valid image."). - Show() + _ = app.Dialog.Error(). + SetTitle("Invalid File"). + SetMessage("Selected file is not a valid image."). + Show() return "", errors.New("invalid image") } @@ -431,37 +430,40 @@ func exportData(app *application.App) { SetTitle("Export Data"). SetMessage("Export all data to CSV?") - exportBtn := dialog.AddButton("Export") - exportBtn.OnClick(func() { - // Step 2: Select destination - path, err := app.Dialog.SaveFile(). - SetFilename("export.csv"). - AddFilter("CSV Files", "*.csv"). - PromptForSingleSelection() - - if err != nil || path == "" { - return - } - - // Step 3: Perform export - if err := performExport(path); err != nil { - app.Dialog.Error(). - SetTitle("Export Failed"). - SetMessage(err.Error()). - Show() - return - } - - // Step 4: Success - app.Dialog.Info(). - SetTitle("Export Complete"). - SetMessage("Data exported successfully!"). - Show() - }) - - cancelBtn := dialog.AddButton("Cancel") - dialog.SetCancelButton(cancelBtn) - dialog.Show() + dialog.Buttons( + application.Button{Label: "Export"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, + ) + + result, err := dialog.Result() + if err != nil || result != "Export" { + return + } + + // Step 2: Select destination + path, err := app.Dialog.SaveFile(). + SetFilename("export.csv"). + AddFilter("CSV Files", "*.csv"). + PromptForSingleSelection() + + if err != nil || path == "" { + return + } + + // Step 3: Perform export + if err := performExport(path); err != nil { + _ = app.Dialog.Error(). + SetTitle("Export Failed"). + SetMessage(err.Error()). + Show() + return + } + + // Step 4: Success + _ = app.Dialog.Info(). + SetTitle("Export Complete"). + SetMessage("Data exported successfully!"). + Show() } ``` diff --git a/docs/src/content/docs/reference/application.mdx b/docs/src/content/docs/reference/application.mdx index 050578a622e..6a16c40ea51 100644 --- a/docs/src/content/docs/reference/application.mdx +++ b/docs/src/content/docs/reference/application.mdx @@ -269,46 +269,48 @@ Dialogs are accessed through the `app.Dialog` manager. See [Dialogs API](/refere ```go // Information dialog -app.Dialog.Info(). - SetTitle("Success"). - SetMessage("Operation completed!"). - Show() + +_ = app.Dialog.Info(). + SetTitle("Success"). + SetMessage("Operation completed!"). + Show() // Error dialog -app.Dialog.Error(). - SetTitle("Error"). - SetMessage("Something went wrong."). - Show() + +_ = app.Dialog.Error(). + SetTitle("Error"). + SetMessage("Something went wrong."). + Show() // Warning dialog -app.Dialog.Warning(). - SetTitle("Warning"). - SetMessage("This action cannot be undone."). - Show() + +_ = app.Dialog.Warning(). + SetTitle("Warning"). + SetMessage("This action cannot be undone."). + Show() ``` ### Question Dialogs -Question dialogs use button callbacks to handle user responses: +Question dialogs use `Result()` to handle user responses: ```go dialog := app.Dialog.Question(). SetTitle("Confirm"). SetMessage("Continue?") -yes := dialog.AddButton("Yes") -yes.OnClick(func() { - // Handle yes -}) - -no := dialog.AddButton("No") -no.OnClick(func() { - // Handle no -}) +dialog.Buttons( + application.Button{Label: "Yes", IsDefault: true}, + application.Button{Label: "No", IsCancel: true}, +) -dialog.SetDefaultButton(yes) -dialog.SetCancelButton(no) -dialog.Show() +result, _ := dialog.Result() +switch result { +case "Yes": + // Handle yes +case "No": + // Handle no +} ``` ### File Dialogs diff --git a/docs/src/content/docs/reference/dialogs.mdx b/docs/src/content/docs/reference/dialogs.mdx index 73c29edf6b5..83602476a45 100644 --- a/docs/src/content/docs/reference/dialogs.mdx +++ b/docs/src/content/docs/reference/dialogs.mdx @@ -324,10 +324,10 @@ func (dm *DialogManager) Info() *MessageDialog **Example:** ```go -app.Dialog.Info(). - SetTitle("Success"). - SetMessage("File saved successfully!"). - Show() +_ = app.Dialog.Info(). + SetTitle("Success"). + SetMessage("File saved successfully!"). + Show() ``` ### Error() @@ -340,10 +340,10 @@ func (dm *DialogManager) Error() *MessageDialog **Example:** ```go -app.Dialog.Error(). - SetTitle("Error"). - SetMessage("Failed to save file: " + err.Error()). - Show() +_ = app.Dialog.Error(). + SetTitle("Error"). + SetMessage("Failed to save file: " + err.Error()). + Show() ``` ### Warning() @@ -356,10 +356,10 @@ func (dm *DialogManager) Warning() *MessageDialog **Example:** ```go -app.Dialog.Warning(). - SetTitle("Warning"). - SetMessage("This action cannot be undone."). - Show() +_ = app.Dialog.Warning(). + SetTitle("Warning"). + SetMessage("This action cannot be undone."). + Show() ``` ### Question() @@ -376,24 +376,19 @@ dialog := app.Dialog.Question(). SetTitle("Confirm"). SetMessage("Do you want to save changes?") -save := dialog.AddButton("Save") -save.OnClick(func() { - saveDocument() -}) - -dontSave := dialog.AddButton("Don't Save") -dontSave.OnClick(func() { - // Continue without saving -}) - -cancel := dialog.AddButton("Cancel") -cancel.OnClick(func() { - // Do nothing -}) +dialog.Buttons( + application.Button{Label: "Save", IsDefault: true}, + application.Button{Label: "Don't Save"}, + application.Button{Label: "Cancel", IsCancel: true}, +) -dialog.SetDefaultButton(save) -dialog.SetCancelButton(cancel) -dialog.Show() +result, _ := dialog.Result() +switch result { +case "Save": + saveDocument() +case "Don't Save": + // Continue without saving +} ``` ### MessageDialog Methods @@ -422,52 +417,36 @@ Sets a custom icon for the dialog. func (d *MessageDialog) SetIcon(icon []byte) *MessageDialog ``` -#### AddButton() +#### Buttons() -Adds a button to the dialog and returns the button for configuration. +Sets the dialog buttons. ```go -func (d *MessageDialog) AddButton(label string) *Button +func (d *MessageDialog) Buttons(buttons ...Button) *MessageDialog ``` -**Returns:** `*Button` - The button instance for further configuration - **Example:** ```go -button := dialog.AddButton("OK") -button.OnClick(func() { - // Handle click -}) +dialog.Buttons( + application.Button{Label: "Yes"}, + application.Button{Label: "No", IsDefault: true, IsCancel: true}, +) ``` -#### SetDefaultButton() - -Sets which button is the default (activated by pressing Enter). +#### Default() -```go -func (d *MessageDialog) SetDefaultButton(button *Button) *MessageDialog -``` +Marks the button with the given label as the default (activated by pressing Enter). -**Example:** ```go -yes := dialog.AddButton("Yes") -no := dialog.AddButton("No") -dialog.SetDefaultButton(yes) +func (d *MessageDialog) Default(label string) *MessageDialog ``` -#### SetCancelButton() +#### Cancel() -Sets which button is the cancel button (activated by pressing Escape). +Marks the button with the given label as the cancel button (activated by pressing Escape). ```go -func (d *MessageDialog) SetCancelButton(button *Button) *MessageDialog -``` - -**Example:** -```go -ok := dialog.AddButton("OK") -cancel := dialog.AddButton("Cancel") -dialog.SetCancelButton(cancel) +func (d *MessageDialog) Cancel(label string) *MessageDialog ``` #### AttachToWindow() @@ -480,38 +459,18 @@ func (d *MessageDialog) AttachToWindow(window Window) *MessageDialog #### Show() -Shows the dialog. Button callbacks handle user responses. - -```go -func (d *MessageDialog) Show() -``` - -**Note:** `Show()` does not return a value. Use button callbacks to handle user responses. - -### Button Methods - -#### OnClick() - -Sets the callback function for when the button is clicked. +Shows the dialog (non-blocking). ```go -func (b *Button) OnClick(callback func()) *Button +func (d *MessageDialog) Show() error ``` -#### SetAsDefault() +#### Result() -Marks this button as the default button. +Shows the dialog (blocking) and returns the clicked button label. ```go -func (b *Button) SetAsDefault() *Button -``` - -#### SetAsCancel() - -Marks this button as the cancel button. - -```go -func (b *Button) SetAsCancel() *Button +func (d *MessageDialog) Result() (string, error) ``` ## Complete Examples @@ -575,17 +534,17 @@ func (s *Service) DeleteItem(app *application.App, id string) { SetTitle("Confirm Delete"). SetMessage("Are you sure you want to delete this item?") - deleteBtn := dialog.AddButton("Delete") - deleteBtn.OnClick(func() { - deleteFromDatabase(id) - }) + dialog.Buttons( + application.Button{Label: "Delete"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, + ) - cancelBtn := dialog.AddButton("Cancel") - // Cancel does nothing + result, err := dialog.Result() + if err != nil || result != "Delete" { + return + } - dialog.SetDefaultButton(cancelBtn) // Default to Cancel for safety - dialog.SetCancelButton(cancelBtn) - dialog.Show() + deleteFromDatabase(id) } ``` @@ -597,23 +556,24 @@ func (s *Editor) PromptSaveChanges(app *application.App) { SetTitle("Unsaved Changes"). SetMessage("Do you want to save your changes before closing?") - save := dialog.AddButton("Save") - save.OnClick(func() { + dialog.Buttons( + application.Button{Label: "Save", IsDefault: true}, + application.Button{Label: "Don't Save"}, + application.Button{Label: "Cancel", IsCancel: true}, + ) + + result, err := dialog.Result() + if err != nil { + return + } + + switch result { + case "Save": s.Save() s.Close() - }) - - dontSave := dialog.AddButton("Don't Save") - dontSave.OnClick(func() { + case "Don't Save": s.Close() - }) - - cancel := dialog.AddButton("Cancel") - // Cancel does nothing, dialog closes - - dialog.SetDefaultButton(save) - dialog.SetCancelButton(cancel) - dialog.Show() + } } ``` @@ -632,7 +592,7 @@ func (s *Service) ProcessMultipleFiles(app *application.App) error { } if len(paths) == 0 { - app.Dialog.Info(). + _ = app.Dialog.Info(). SetTitle("No Files Selected"). SetMessage("Please select at least one file."). Show() @@ -643,7 +603,7 @@ func (s *Service) ProcessMultipleFiles(app *application.App) error { for _, path := range paths { err := processFile(path) if err != nil { - app.Dialog.Error(). + _ = app.Dialog.Error(). SetTitle("Processing Error"). SetMessage(fmt.Sprintf("Failed to process %s: %v", path, err)). Show() @@ -652,7 +612,7 @@ func (s *Service) ProcessMultipleFiles(app *application.App) error { } // Show completion - app.Dialog.Info(). + _ = app.Dialog.Info(). SetTitle("Complete"). SetMessage(fmt.Sprintf("Successfully processed %d files", len(paths))). Show() @@ -681,7 +641,7 @@ func (s *Service) SaveFile(app *application.App, data []byte) error { err = os.WriteFile(path, data, 0644) if err != nil { // Show error dialog - app.Dialog.Error(). + _ = app.Dialog.Error(). SetTitle("Save Failed"). SetMessage(fmt.Sprintf("Could not save file: %v", err)). Show() @@ -689,7 +649,7 @@ func (s *Service) SaveFile(app *application.App, data []byte) error { } // Show success - app.Dialog.Info(). + _ = app.Dialog.Info(). SetTitle("Success"). SetMessage("File saved successfully!"). Show() @@ -742,7 +702,6 @@ func (s *Service) OpenWithDefaults(app *application.App) (string, error) { - **Show confirmation for destructive actions** - Use Question dialogs - **Provide feedback** - Use Info dialogs for success messages - **Set sensible defaults** - Default directory, filename, etc. -- **Use callbacks for button actions** - Handle user responses properly ### Don't @@ -806,14 +765,15 @@ func (s *Service) OpenRecent(app *application.App, recentPath string) error { SetTitle("File Not Found"). SetMessage("The file no longer exists. Remove from recent files?") - remove := dialog.AddButton("Remove") - remove.OnClick(func() { - s.removeFromRecent(recentPath) - }) + dialog.Buttons( + application.Button{Label: "Remove"}, + application.Button{Label: "Cancel", IsDefault: true, IsCancel: true}, + ) - cancel := dialog.AddButton("Cancel") - dialog.SetCancelButton(cancel) - dialog.Show() + result, _ := dialog.Result() + if result == "Remove" { + s.removeFromRecent(recentPath) + } return err }