Merge branch 'v3-alpha' into vk/f208-fullscreen-api-c

This commit is contained in:
Lea Anthony 2025-12-17 22:42:20 +11:00 committed by GitHub
commit 5dbb3ec09e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1204 additions and 1076 deletions

View file

@ -30,6 +30,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
*/
## [Unreleased]
## v3.0.0-alpha.48 - 2025-12-16
## Added
- Add desktop environment detection on linux [PR #4797](https://github.com/wailsapp/wails/pull/4797)
## 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
## Fixed
- Fix crash on macOS when toggling window visibility via Hide()/Show() with ApplicationShouldTerminateAfterLastWindowClosed enabled (#4389) by @leaanthony
- Fix memory leak in context menus on macOS and Windows when repeatedly opening menus (#4012) by @leaanthony
- Fix context menu native resources not being reused on macOS, causing fresh menu creation on each display (#4012) by @leaanthony
## v3.0.0-alpha.47 - 2025-12-15
## Added
- Add `Window.Print()` method to JavaScript runtime for triggering print dialog from frontend (#4290) by @leaanthony
## Fixed
- Fix macOS dock icon click not showing hidden windows when app started with `Hidden: true` (#4583) by @leaanthony
- Fix macOS print dialog not opening due to incorrect window pointer type in CGO call (#4290) by @leaanthony
## v3.0.0-alpha.46 - 2025-12-14
## Added

View file

@ -41,9 +41,9 @@ ringqueue.go # Tiny LRU for mime cache
1. `wails3 dev` boots and **spawns your frontend dev server**
(Vite, SvelteKit, React-SWC …) by running the task defined in
`frontend/Taskfile.yml` (usually `npm run dev`).
`build/Taskfile.yml` (usually `npm run dev`).
2. Wails starts the **Dev Asset Server** listening on `localhost:<random>` and
tells the Go runtime to load `http://<host>:<port>` as the window URL.
tells the Go runtime to load `http://<host>:<VITE_PORT>` as the window URL.
3. Incoming requests are handled by `assetserver_dev.go`:
```
@ -77,8 +77,8 @@ ringqueue.go # Tiny LRU for mime cache
The dev proxy is **framework-agnostic**:
* The `wails.json` template injects two env vars:
`FRONTEND_DEV_HOST` & `FRONTEND_DEV_PORT`.
* The root `Taskfile.yml` task file injects two env vars:
`APP_NAME` & `WAILS_VITE_PORT`.
* Taskfiles for each template emit those vars before running their dev servers.
* `assetserver_dev.go` simply proxies to that target.
@ -143,18 +143,17 @@ type AssetServer interface {
Each official template (React, Vue, Svelte, Solid…) contains:
* `frontend/Taskfile.yml`
* `build/Taskfile.yml`
* `frontend/vite.config.ts` (or equivalent)
They export two tasks:
They export several tasks including:
| Task | Purpose |
|------|---------|
| `dev` | Starts the framework dev server on a **random free port** and prints it to stdout (`PORT=5173`). |
| `build` | Produces static assets into `dist/` + manifest for cache-busting. |
| `dev:frontend` | Starts the framework dev server on a **random free port** and prints it to stdout (`PORT=5173`). |
| `build:frontend` | Produces static assets into `dist/` + manifest for cache-busting. |
`internal/commands/dev.go` parses that stdout, sets `FRONTEND_DEV_*` env vars
and launches the **Dev Asset Server**.
`internal/commands/dev.go` sets the `FRONTEND_DEVSERVER_URL` env var using the hard-coded `localhost` as the `host` and the emitted `VITE_PORT` env var as the `port` and launches the **Dev Asset Server**.
Frameworks remain fully decoupled from Go:

View file

@ -10,20 +10,28 @@ import { Card, CardGrid } from "@astrojs/starlight/components";
## File dialogs
Wails provides **native file dialogs** with platform-appropriate appearance for opening files, saving files, and selecting folders. Simple API with file type filtering, multiple selection support, and default locations.
## Creating File Dialogs
File dialogs are accessed through the `app.Dialog` manager:
```go
app.Dialog.OpenFile()
app.Dialog.SaveFile()
```
## Open File dialog
Select files to open:
```go
path, err := app.OpenFileDialog().
path, err := app.Dialog.OpenFile().
SetTitle("Select Image").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg;*.gif"},
{DisplayName: "All Files", Pattern: "*.*"},
}).
AddFilter("Images", "*.png;*.jpg;*.gif").
AddFilter("All Files", "*.*").
PromptForSingleSelection()
if err != nil {
if err != nil || path == "" {
return
}
@ -39,15 +47,13 @@ openFile(path)
### Single File Selection
```go
path, err := app.OpenFileDialog().
path, err := app.Dialog.OpenFile().
SetTitle("Open Document").
SetFilters([]application.FileFilter{
{DisplayName: "Text Files", Pattern: "*.txt"},
{DisplayName: "All Files", Pattern: "*.*"},
}).
AddFilter("Text Files", "*.txt").
AddFilter("All Files", "*.*").
PromptForSingleSelection()
if err != nil {
if err != nil || path == "" {
// User cancelled or error occurred
return
}
@ -59,11 +65,9 @@ data, _ := os.ReadFile(path)
### Multiple File Selection
```go
paths, err := app.OpenFileDialog().
paths, err := app.Dialog.OpenFile().
SetTitle("Select Images").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg;*.jpeg;*.gif"},
}).
AddFilter("Images", "*.png;*.jpg;*.jpeg;*.gif").
PromptForMultipleSelection()
if err != nil {
@ -79,7 +83,7 @@ for _, path := range paths {
### With Default Directory
```go
path, err := app.OpenFileDialog().
path, err := app.Dialog.OpenFile().
SetTitle("Open File").
SetDirectory("/Users/me/Documents").
PromptForSingleSelection()
@ -90,16 +94,13 @@ path, err := app.OpenFileDialog().
Choose where to save:
```go
path, err := app.SaveFileDialog().
SetTitle("Save Document").
SetDefaultFilename("document.txt").
SetFilters([]application.FileFilter{
{DisplayName: "Text Files", Pattern: "*.txt"},
{DisplayName: "All Files", Pattern: "*.*"},
}).
path, err := app.Dialog.SaveFile().
SetFilename("document.txt").
AddFilter("Text Files", "*.txt").
AddFilter("All Files", "*.*").
PromptForSingleSelection()
if err != nil {
if err != nil || path == "" {
return
}
@ -115,48 +116,48 @@ saveFile(path, data)
### With Default Filename
```go
path, err := app.SaveFileDialog().
SetTitle("Export Data").
SetDefaultFilename("export.csv").
SetFilters([]application.FileFilter{
{DisplayName: "CSV Files", Pattern: "*.csv"},
}).
path, err := app.Dialog.SaveFile().
SetFilename("export.csv").
AddFilter("CSV Files", "*.csv").
PromptForSingleSelection()
```
### With Default Directory
```go
path, err := app.SaveFileDialog().
SetTitle("Save File").
path, err := app.Dialog.SaveFile().
SetDirectory("/Users/me/Documents").
SetDefaultFilename("untitled.txt").
SetFilename("untitled.txt").
PromptForSingleSelection()
```
### Overwrite Confirmation
```go
path, err := app.SaveFileDialog().
SetTitle("Save File").
SetDefaultFilename("document.txt").
path, err := app.Dialog.SaveFile().
SetFilename("document.txt").
PromptForSingleSelection()
if err != nil {
if err != nil || path == "" {
return
}
// Check if file exists
if _, err := os.Stat(path); err == nil {
result, _ := app.QuestionDialog().
dialog := app.Dialog.Question().
SetTitle("Confirm Overwrite").
SetMessage("File already exists. Overwrite?").
SetButtons("Overwrite", "Cancel").
Show()
if result != "Overwrite" {
return
}
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
}
saveFile(path, data)
@ -164,14 +165,16 @@ saveFile(path, data)
## Select Folder dialog
Choose a directory:
Choose a directory using the open file dialog with directory selection enabled:
```go
path, err := app.SelectFolderDialog().
path, err := app.Dialog.OpenFile().
SetTitle("Select Output Folder").
CanChooseDirectories(true).
CanChooseFiles(false).
PromptForSingleSelection()
if err != nil {
if err != nil || path == "" {
return
}
@ -187,66 +190,45 @@ exportToFolder(path)
### With Default Directory
```go
path, err := app.SelectFolderDialog().
path, err := app.Dialog.OpenFile().
SetTitle("Select Folder").
SetDirectory("/Users/me/Documents").
CanChooseDirectories(true).
CanChooseFiles(false).
PromptForSingleSelection()
```
## File Filters
Use the `AddFilter()` method to add file type filters to dialogs. Each call adds a new filter option.
### Basic Filters
```go
filters := []application.FileFilter{
{DisplayName: "Text Files", Pattern: "*.txt"},
{DisplayName: "All Files", Pattern: "*.*"},
}
path, _ := app.OpenFileDialog().
SetFilters(filters).
path, _ := app.Dialog.OpenFile().
AddFilter("Text Files", "*.txt").
AddFilter("All Files", "*.*").
PromptForSingleSelection()
```
### Multiple Extensions
Use semicolons to specify multiple extensions in a single filter:
```go
filters := []application.FileFilter{
{
DisplayName: "Images",
Pattern: "*.png;*.jpg;*.jpeg;*.gif;*.bmp",
},
{
DisplayName: "Documents",
Pattern: "*.txt;*.doc;*.docx;*.pdf",
},
{
DisplayName: "All Files",
Pattern: "*.*",
},
}
dialog := app.Dialog.OpenFile().
AddFilter("Images", "*.png;*.jpg;*.jpeg;*.gif;*.bmp").
AddFilter("Documents", "*.txt;*.doc;*.docx;*.pdf").
AddFilter("All Files", "*.*")
```
### Platform-Specific Patterns
### Pattern Format
**Windows:**
```go
// Semicolon-separated
Pattern: "*.png;*.jpg;*.gif"
```
Use **semicolons** to separate multiple extensions in a single filter:
**macOS/Linux:**
```go
// Space or comma-separated (both work)
Pattern: "*.png *.jpg *.gif"
// or
Pattern: "*.png,*.jpg,*.gif"
```
**Cross-platform (recommended):**
```go
// Use semicolons (works on all platforms)
Pattern: "*.png;*.jpg;*.gif"
// Multiple extensions separated by semicolons
AddFilter("Images", "*.png;*.jpg;*.gif")
```
## Complete Examples
@ -254,41 +236,40 @@ Pattern: "*.png;*.jpg;*.gif"
### Open Image File
```go
func openImage() (image.Image, error) {
path, err := app.OpenFileDialog().
func openImage(app *application.App) (image.Image, error) {
path, err := app.Dialog.OpenFile().
SetTitle("Select Image").
SetFilters([]application.FileFilter{
{
DisplayName: "Images",
Pattern: "*.png;*.jpg;*.jpeg;*.gif;*.bmp",
},
}).
AddFilter("Images", "*.png;*.jpg;*.jpeg;*.gif;*.bmp").
PromptForSingleSelection()
if err != nil {
return nil, err
}
if path == "" {
return nil, errors.New("no file selected")
}
// Open and decode image
file, err := os.Open(path)
if err != nil {
app.ErrorDialog().
app.Dialog.Error().
SetTitle("Open Failed").
SetMessage(err.Error()).
Show()
return nil, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
app.ErrorDialog().
app.Dialog.Error().
SetTitle("Invalid Image").
SetMessage("Could not decode image file.").
Show()
return nil, err
}
return img, nil
}
```
@ -296,214 +277,216 @@ func openImage() (image.Image, error) {
### Save Document with Validation
```go
func saveDocument(content string) error {
path, err := app.SaveFileDialog().
SetTitle("Save Document").
SetDefaultFilename("document.txt").
SetFilters([]application.FileFilter{
{DisplayName: "Text Files", Pattern: "*.txt"},
{DisplayName: "Markdown Files", Pattern: "*.md"},
{DisplayName: "All Files", Pattern: "*.*"},
}).
func saveDocument(app *application.App, content string) {
path, err := app.Dialog.SaveFile().
SetFilename("document.txt").
AddFilter("Text Files", "*.txt").
AddFilter("Markdown Files", "*.md").
AddFilter("All Files", "*.*").
PromptForSingleSelection()
if err != nil {
return err
if err != nil || path == "" {
return
}
// Validate extension
ext := filepath.Ext(path)
if ext != ".txt" && ext != ".md" {
result, _ := app.QuestionDialog().
dialog := app.Dialog.Question().
SetTitle("Confirm Extension").
SetMessage(fmt.Sprintf("Save as %s file?", ext)).
SetButtons("Save", "Cancel").
Show()
if result != "Save" {
return errors.New("cancelled")
}
SetMessage(fmt.Sprintf("Save as %s file?", ext))
saveBtn := dialog.AddButton("Save")
saveBtn.OnClick(func() {
doSave(app, path, content)
})
cancelBtn := dialog.AddButton("Cancel")
dialog.SetDefaultButton(cancelBtn)
dialog.SetCancelButton(cancelBtn)
dialog.Show()
return
}
// Save file
doSave(app, path, content)
}
func doSave(app *application.App, path, content string) {
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
app.ErrorDialog().
app.Dialog.Error().
SetTitle("Save Failed").
SetMessage(err.Error()).
Show()
return err
return
}
app.InfoDialog().
app.Dialog.Info().
SetTitle("Saved").
SetMessage("Document saved successfully!").
Show()
return nil
}
```
### Batch File Processing
```go
func processMultipleFiles() {
paths, err := app.OpenFileDialog().
func processMultipleFiles(app *application.App) {
paths, err := app.Dialog.OpenFile().
SetTitle("Select Files to Process").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg"},
}).
AddFilter("Images", "*.png;*.jpg").
PromptForMultipleSelection()
if err != nil || len(paths) == 0 {
return
}
// Confirm processing
result, _ := app.QuestionDialog().
dialog := app.Dialog.Question().
SetTitle("Confirm Processing").
SetMessage(fmt.Sprintf("Process %d file(s)?", len(paths))).
SetButtons("Process", "Cancel").
Show()
if result != "Process" {
return
}
// Process files
var errors []error
for i, path := range paths {
if err := processFile(path); err != nil {
errors = append(errors, err)
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
}
// Update progress
app.EmitEvent("progress", map[string]interface{}{
"current": i + 1,
"total": len(paths),
})
}
// Show results
if len(errors) > 0 {
app.WarningDialog().
SetTitle("Processing Complete").
SetMessage(fmt.Sprintf("Processed %d files with %d errors.",
len(paths), len(errors))).
Show()
} else {
app.InfoDialog().
SetTitle("Success").
SetMessage(fmt.Sprintf("Processed %d files successfully!", len(paths))).
Show()
}
// 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()
}
})
cancelBtn := dialog.AddButton("Cancel")
dialog.SetCancelButton(cancelBtn)
dialog.Show()
}
```
### Export with Folder Selection
```go
func exportData(data []byte) error {
func exportData(app *application.App, data []byte) {
// Select output folder
folder, err := app.SelectFolderDialog().
folder, err := app.Dialog.OpenFile().
SetTitle("Select Export Folder").
SetDirectory(getDefaultExportFolder()).
CanChooseDirectories(true).
CanChooseFiles(false).
PromptForSingleSelection()
if err != nil {
return err
if err != nil || folder == "" {
return
}
// Generate filename
filename := fmt.Sprintf("export_%s.csv",
filename := fmt.Sprintf("export_%s.csv",
time.Now().Format("2006-01-02_15-04-05"))
path := filepath.Join(folder, filename)
// Save file
if err := os.WriteFile(path, data, 0644); err != nil {
app.ErrorDialog().
app.Dialog.Error().
SetTitle("Export Failed").
SetMessage(err.Error()).
Show()
return err
return
}
// Show success with option to open folder
result, _ := app.QuestionDialog().
dialog := app.Dialog.Question().
SetTitle("Export Complete").
SetMessage(fmt.Sprintf("Exported to %s", filename)).
SetButtons("Open Folder", "OK").
Show()
if result == "Open Folder" {
SetMessage(fmt.Sprintf("Exported to %s", filename))
openBtn := dialog.AddButton("Open Folder")
openBtn.OnClick(func() {
openFolder(folder)
}
return nil
})
dialog.AddButton("OK")
dialog.Show()
}
```
### Import with Validation
```go
func importConfiguration() error {
path, err := app.OpenFileDialog().
func importConfiguration(app *application.App) {
path, err := app.Dialog.OpenFile().
SetTitle("Import Configuration").
SetFilters([]application.FileFilter{
{DisplayName: "JSON Files", Pattern: "*.json"},
{DisplayName: "YAML Files", Pattern: "*.yaml;*.yml"},
}).
AddFilter("JSON Files", "*.json").
AddFilter("YAML Files", "*.yaml;*.yml").
PromptForSingleSelection()
if err != nil {
return err
if err != nil || path == "" {
return
}
// Read file
data, err := os.ReadFile(path)
if err != nil {
app.ErrorDialog().
app.Dialog.Error().
SetTitle("Read Failed").
SetMessage(err.Error()).
Show()
return err
return
}
// Validate configuration
config, err := parseConfig(data)
if err != nil {
app.ErrorDialog().
app.Dialog.Error().
SetTitle("Invalid Configuration").
SetMessage("File is not a valid configuration.").
Show()
return err
return
}
// Confirm import
result, _ := app.QuestionDialog().
dialog := app.Dialog.Question().
SetTitle("Confirm Import").
SetMessage("Import this configuration?").
SetButtons("Import", "Cancel").
Show()
if result != "Import" {
return errors.New("cancelled")
}
// Apply configuration
if err := applyConfig(config); err != nil {
app.ErrorDialog().
SetTitle("Import Failed").
SetMessage(err.Error()).
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
}
app.Dialog.Info().
SetTitle("Success").
SetMessage("Configuration imported successfully!").
Show()
return err
}
app.InfoDialog().
SetTitle("Success").
SetMessage("Configuration imported successfully!").
Show()
return nil
})
cancelBtn := dialog.AddButton("Cancel")
dialog.SetCancelButton(cancelBtn)
dialog.Show()
}
```

View file

@ -10,12 +10,26 @@ import { Card, CardGrid } from "@astrojs/starlight/components";
## Message dialogs
Wails provides **native message dialogs** with platform-appropriate appearance: info, warning, error, and question dialogs with customisable titles, messages, and buttons. Simple API, native behaviour, accessible by default.
## Creating Dialogs
Message dialogs are accessed through the `app.Dialog` manager:
```go
app.Dialog.Info()
app.Dialog.Question()
app.Dialog.Warning()
app.Dialog.Error()
```
All methods return a `*MessageDialog` that can be configured using method chaining.
## Information dialog
Display informational messages:
```go
app.InfoDialog().
app.Dialog.Info().
SetTitle("Success").
SetMessage("File saved successfully!").
Show()
@ -30,16 +44,16 @@ app.InfoDialog().
**Example - Save confirmation:**
```go
func saveFile(path string, data []byte) error {
func saveFile(app *application.App, path string, data []byte) error {
if err := os.WriteFile(path, data, 0644); err != nil {
return err
}
app.InfoDialog().
app.Dialog.Info().
SetTitle("File Saved").
SetMessage(fmt.Sprintf("Saved to %s", filepath.Base(path))).
Show()
return nil
}
```
@ -49,7 +63,7 @@ func saveFile(path string, data []byte) error {
Show warnings:
```go
app.WarningDialog().
app.Dialog.Warning().
SetTitle("Warning").
SetMessage("This action cannot be undone.").
Show()
@ -64,11 +78,11 @@ app.WarningDialog().
**Example - Disk space warning:**
```go
func checkDiskSpace() {
func checkDiskSpace(app *application.App) {
available := getDiskSpace()
if available < 100*1024*1024 { // Less than 100MB
app.WarningDialog().
app.Dialog.Warning().
SetTitle("Low Disk Space").
SetMessage(fmt.Sprintf("Only %d MB available.", available/(1024*1024))).
Show()
@ -81,7 +95,7 @@ func checkDiskSpace() {
Display errors:
```go
app.ErrorDialog().
app.Dialog.Error().
SetTitle("Error").
SetMessage("Failed to connect to server.").
Show()
@ -96,41 +110,48 @@ app.ErrorDialog().
**Example - Network error:**
```go
func fetchData(url string) ([]byte, error) {
func fetchData(app *application.App, url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
app.ErrorDialog().
app.Dialog.Error().
SetTitle("Network Error").
SetMessage(fmt.Sprintf("Failed to connect: %v", err)).
Show()
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
```
## Question dialog
Ask users questions:
Ask users questions and handle responses via button callbacks:
```go
result, err := app.QuestionDialog().
dialog := app.Dialog.Question().
SetTitle("Confirm").
SetMessage("Save changes before closing?").
SetButtons("Save", "Don't Save", "Cancel").
SetDefaultButton("Save").
Show()
SetMessage("Save changes before closing?")
switch result {
case "Save":
save := dialog.AddButton("Save")
save.OnClick(func() {
saveChanges()
case "Don't Save":
})
dontSave := dialog.AddButton("Don't Save")
dontSave.OnClick(func() {
// Continue without saving
case "Cancel":
})
cancel := dialog.AddButton("Cancel")
cancel.OnClick(func() {
// Don't close
}
})
dialog.SetDefaultButton(save)
dialog.SetCancelButton(cancel)
dialog.Show()
```
**Use cases:**
@ -142,32 +163,34 @@ case "Cancel":
**Example - Unsaved changes:**
```go
func closeDocument() bool {
func closeDocument(app *application.App) {
if !hasUnsavedChanges() {
return true
doClose()
return
}
result, err := app.QuestionDialog().
dialog := app.Dialog.Question().
SetTitle("Unsaved Changes").
SetMessage("Do you want to save your changes?").
SetButtons("Save", "Don't Save", "Cancel").
SetDefaultButton("Save").
Show()
if err != nil {
return false
}
switch result {
case "Save":
return saveDocument()
case "Don't Save":
return true
case "Cancel":
return false
}
return false
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()
})
cancel := dialog.AddButton("Cancel")
// Cancel button has no callback - just closes the dialog
dialog.SetDefaultButton(save)
dialog.SetCancelButton(cancel)
dialog.Show()
}
```
@ -176,7 +199,7 @@ func closeDocument() bool {
### Title and Message
```go
dialog := app.InfoDialog().
dialog := app.Dialog.Info().
SetTitle("Operation Complete").
SetMessage("All files have been processed successfully.")
```
@ -190,30 +213,85 @@ dialog := app.InfoDialog().
**Single button (Info/Warning/Error):**
Info, warning, and error dialogs display a default "OK" button:
```go
// Default "OK" button
app.InfoDialog().
app.Dialog.Info().
SetMessage("Done!").
Show()
```
**Multiple buttons (Question):**
You can also add custom buttons:
```go
result, _ := app.QuestionDialog().
SetMessage("Choose an action").
SetButtons("Option 1", "Option 2", "Option 3").
Show()
dialog := app.Dialog.Info().
SetMessage("Done!")
ok := dialog.AddButton("Got it!")
dialog.SetDefaultButton(ok)
dialog.Show()
```
**Default button:**
**Multiple buttons (Question):**
Use `AddButton()` to add buttons, which returns a `*Button` you can configure:
```go
app.QuestionDialog().
SetMessage("Delete file?").
SetButtons("Delete", "Cancel").
SetDefaultButton("Cancel"). // Safe option
Show()
dialog := app.Dialog.Question().
SetMessage("Choose an action")
option1 := dialog.AddButton("Option 1")
option1.OnClick(func() {
handleOption1()
})
option2 := dialog.AddButton("Option 2")
option2.OnClick(func() {
handleOption2()
})
option3 := dialog.AddButton("Option 3")
option3.OnClick(func() {
handleOption3()
})
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.
```go
dialog := app.Dialog.Question().
SetMessage("Delete file?")
deleteBtn := dialog.AddButton("Delete")
deleteBtn.OnClick(func() {
performDelete()
})
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()
```
You can also use the fluent `SetAsDefault()` and `SetAsCancel()` methods on buttons:
```go
dialog := app.Dialog.Question().
SetMessage("Delete file?")
dialog.AddButton("Delete").OnClick(func() {
performDelete()
})
dialog.AddButton("Cancel").SetAsDefault().SetAsCancel()
dialog.Show()
```
**Best practices:**
@ -222,16 +300,29 @@ app.QuestionDialog().
- **Safe default:** Non-destructive action
- **Order matters:** Most likely action first (except Cancel)
### Custom Icon
Set a custom icon for the dialog:
```go
app.Dialog.Info().
SetTitle("Custom Icon Example").
SetMessage("Using a custom icon").
SetIcon(myIconBytes).
Show()
```
### Window Attachment
Attach to specific window:
```go
dialog := app.QuestionDialog().
dialog := app.Dialog.Question().
SetMessage("Window-specific question").
AttachToWindow(window)
result, _ := dialog.Show()
dialog.AddButton("OK")
dialog.Show()
```
**Benefits:**
@ -244,163 +335,111 @@ result, _ := dialog.Show()
### Confirm Destructive Action
```go
func deleteFiles(paths []string) error {
func deleteFiles(app *application.App, paths []string) {
// Confirm deletion
message := fmt.Sprintf("Delete %d file(s)?", len(paths))
if len(paths) == 1 {
message = fmt.Sprintf("Delete %s?", filepath.Base(paths[0]))
}
result, err := app.QuestionDialog().
dialog := app.Dialog.Question().
SetTitle("Confirm Delete").
SetMessage(message).
SetButtons("Delete", "Cancel").
SetDefaultButton("Cancel").
Show()
if err != nil || result != "Delete" {
return errors.New("cancelled")
}
// Perform deletion
var errors []error
for _, path := range paths {
if err := os.Remove(path); err != nil {
errors = append(errors, err)
}
}
// Show result
if len(errors) > 0 {
app.ErrorDialog().
SetTitle("Delete Failed").
SetMessage(fmt.Sprintf("Failed to delete %d file(s)", len(errors))).
Show()
return fmt.Errorf("%d errors", len(errors))
}
app.InfoDialog().
SetTitle("Delete Complete").
SetMessage(fmt.Sprintf("Deleted %d file(s)", len(paths))).
Show()
return nil
}
```
SetMessage(message)
### Error Handling with Retry
```go
func connectToServer(url string) error {
for attempts := 0; attempts < 3; attempts++ {
if err := tryConnect(url); err == nil {
return nil
}
if attempts < 2 {
result, _ := app.QuestionDialog().
SetTitle("Connection Failed").
SetMessage("Failed to connect. Retry?").
SetButtons("Retry", "Cancel").
Show()
if result != "Retry" {
return errors.New("cancelled")
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)
}
}
}
app.ErrorDialog().
SetTitle("Connection Failed").
SetMessage("Could not connect after 3 attempts.").
Show()
return errors.New("connection failed")
}
```
### Multi-Step Workflow
```go
func exportAndEmail() {
// Step 1: Confirm export
result, _ := app.QuestionDialog().
SetTitle("Export and Email").
SetMessage("Export data and send via email?").
SetButtons("Continue", "Cancel").
Show()
if result != "Continue" {
return
}
// Step 2: Export data
data, err := exportData()
if err != nil {
app.ErrorDialog().
SetTitle("Export Failed").
SetMessage(err.Error()).
Show()
return
}
// Step 3: Send email
if err := sendEmail(data); err != nil {
app.ErrorDialog().
SetTitle("Email Failed").
SetMessage(err.Error()).
Show()
return
}
// Step 4: Success
app.InfoDialog().
SetTitle("Success").
SetMessage("Data exported and emailed successfully!").
Show()
}
```
### Validation with Feedback
```go
func validateAndSave(data string) error {
// Validate
if err := validate(data); err != nil {
app.WarningDialog().
SetTitle("Validation Warning").
SetMessage(err.Error()).
Show()
result, _ := app.QuestionDialog().
SetMessage("Save anyway?").
SetButtons("Save", "Cancel").
SetDefaultButton("Cancel").
Show()
if result != "Save" {
return errors.New("cancelled")
// 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()
}
}
// Save
if err := save(data); err != nil {
app.ErrorDialog().
SetTitle("Save Failed").
SetMessage(err.Error()).
Show()
return err
}
app.InfoDialog().
SetTitle("Saved").
SetMessage("Data saved successfully!").
Show()
return nil
})
cancelBtn := dialog.AddButton("Cancel")
dialog.SetDefaultButton(cancelBtn)
dialog.SetCancelButton(cancelBtn)
dialog.Show()
}
```
### Quit Confirmation
```go
func confirmQuit(app *application.App) {
dialog := app.Dialog.Question().
SetTitle("Quit").
SetMessage("You have unsaved work. Are you sure you want to quit?")
yes := dialog.AddButton("Yes")
yes.OnClick(func() {
app.Quit()
})
no := dialog.AddButton("No")
dialog.SetDefaultButton(no)
dialog.Show()
}
```
### Update Dialog with Download Option
```go
func showUpdateDialog(app *application.App) {
dialog := app.Dialog.Question().
SetTitle("Update").
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()
}
```
### Custom Icon Question
```go
func showCustomIconQuestion(app *application.App, iconBytes []byte) {
dialog := app.Dialog.Question().
SetTitle("Custom Icon Example").
SetMessage("Using a custom icon").
SetIcon(iconBytes)
likeIt := dialog.AddButton("I like it!")
likeIt.OnClick(func() {
app.Dialog.Info().SetMessage("Thanks!").Show()
})
notKeen := dialog.AddButton("Not so keen...")
notKeen.OnClick(func() {
app.Dialog.Info().SetMessage("Too bad!").Show()
})
dialog.SetDefaultButton(likeIt)
dialog.Show()
}
## Best Practices
### ✅ Do

View file

@ -15,33 +15,48 @@ Wails provides **native system dialogs** that work across all platforms: message
```go
// Information dialog
app.InfoDialog().
app.Dialog.Info().
SetTitle("Success").
SetMessage("File saved successfully!").
Show()
// Question dialog
result, _ := app.QuestionDialog().
// Question dialog with button callbacks
dialog := app.Dialog.Question().
SetTitle("Confirm").
SetMessage("Delete this file?").
SetButtons("Delete", "Cancel").
Show()
SetMessage("Delete this file?")
if result == "Delete" {
deleteBtn := dialog.AddButton("Delete")
deleteBtn.OnClick(func() {
deleteFile()
}
})
cancelBtn := dialog.AddButton("Cancel")
dialog.SetDefaultButton(cancelBtn)
dialog.SetCancelButton(cancelBtn)
dialog.Show()
// File open dialog
path, _ := app.OpenFileDialog().
path, _ := app.Dialog.OpenFile().
SetTitle("Select Image").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg"},
}).
AddFilter("Images", "*.png;*.jpg").
PromptForSingleSelection()
```
**That's it!** Native dialogs with minimal code.
## Accessing Dialogs
Dialogs are accessed through the `app.Dialog` manager:
```go
app.Dialog.Info()
app.Dialog.Question()
app.Dialog.Warning()
app.Dialog.Error()
app.Dialog.OpenFile()
app.Dialog.SaveFile()
```
## dialog Types
### Information dialog
@ -49,7 +64,7 @@ path, _ := app.OpenFileDialog().
Display simple messages:
```go
app.InfoDialog().
app.Dialog.Info().
SetTitle("Welcome").
SetMessage("Welcome to our application!").
Show()
@ -65,7 +80,7 @@ app.InfoDialog().
Show warnings:
```go
app.WarningDialog().
app.Dialog.Warning().
SetTitle("Warning").
SetMessage("This action cannot be undone.").
Show()
@ -81,7 +96,7 @@ app.WarningDialog().
Display errors:
```go
app.ErrorDialog().
app.Dialog.Error().
SetTitle("Error").
SetMessage("Failed to save file: " + err.Error()).
Show()
@ -94,19 +109,22 @@ app.ErrorDialog().
### Question dialog
Ask users questions:
Ask users questions and handle responses via button callbacks:
```go
result, err := app.QuestionDialog().
dialog := app.Dialog.Question().
SetTitle("Confirm Delete").
SetMessage("Are you sure you want to delete this file?").
SetButtons("Delete", "Cancel").
SetDefaultButton("Cancel").
Show()
SetMessage("Are you sure you want to delete this file?")
if result == "Delete" {
deleteBtn := dialog.AddButton("Delete")
deleteBtn.OnClick(func() {
deleteFile()
}
})
cancelBtn := dialog.AddButton("Cancel")
dialog.SetDefaultButton(cancelBtn)
dialog.SetCancelButton(cancelBtn)
dialog.Show()
```
**Use cases:**
@ -121,15 +139,13 @@ if result == "Delete" {
Select files to open:
```go
path, err := app.OpenFileDialog().
path, err := app.Dialog.OpenFile().
SetTitle("Select Image").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg;*.gif"},
{DisplayName: "All Files", Pattern: "*.*"},
}).
AddFilter("Images", "*.png;*.jpg;*.gif").
AddFilter("All Files", "*.*").
PromptForSingleSelection()
if err == nil {
if err == nil && path != "" {
openFile(path)
}
```
@ -137,15 +153,15 @@ if err == nil {
**Multiple selection:**
```go
paths, err := app.OpenFileDialog().
paths, err := app.Dialog.OpenFile().
SetTitle("Select Images").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg"},
}).
AddFilter("Images", "*.png;*.jpg").
PromptForMultipleSelection()
for _, path := range paths {
processFile(path)
if err == nil {
for _, path := range paths {
processFile(path)
}
}
```
@ -154,30 +170,29 @@ for _, path := range paths {
Choose where to save:
```go
path, err := app.SaveFileDialog().
SetTitle("Save Document").
SetDefaultFilename("document.txt").
SetFilters([]application.FileFilter{
{DisplayName: "Text Files", Pattern: "*.txt"},
{DisplayName: "All Files", Pattern: "*.*"},
}).
path, err := app.Dialog.SaveFile().
SetFilename("document.txt").
AddFilter("Text Files", "*.txt").
AddFilter("All Files", "*.*").
PromptForSingleSelection()
if err == nil {
if err == nil && path != "" {
saveFile(path)
}
```
### Select Folder dialog
Choose a directory:
Choose a directory using the open file dialog with directory selection enabled:
```go
path, err := app.SelectFolderDialog().
path, err := app.Dialog.OpenFile().
SetTitle("Select Output Folder").
CanChooseDirectories(true).
CanChooseFiles(false).
PromptForSingleSelection()
if err == nil {
if err == nil && path != "" {
exportToFolder(path)
}
```
@ -187,42 +202,67 @@ if err == nil {
### Title and Message
```go
dialog := app.InfoDialog().
dialog := app.Dialog.Info().
SetTitle("Success").
SetMessage("Operation completed successfully!")
```
### Buttons
**Predefined buttons:**
**Default button for simple dialogs:**
Info, warning, and error dialogs display a default "OK" button:
```go
// OK button
app.InfoDialog().
app.Dialog.Info().
SetMessage("Done!").
Show()
// Yes/No buttons
result, _ := app.QuestionDialog().
SetMessage("Continue?").
SetButtons("Yes", "No").
Show()
// Custom buttons
result, _ := app.QuestionDialog().
SetMessage("Choose action").
SetButtons("Save", "Don't Save", "Cancel").
Show()
```
**Default button:**
**Custom buttons for question dialogs:**
Use `AddButton()` to add buttons, which returns a `*Button` you can configure with callbacks:
```go
app.QuestionDialog().
SetMessage("Delete file?").
SetButtons("Delete", "Cancel").
SetDefaultButton("Cancel"). // Highlighted by default
Show()
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()
```
**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.
```go
dialog := app.Dialog.Question().
SetMessage("Delete file?")
deleteBtn := dialog.AddButton("Delete")
deleteBtn.OnClick(func() {
performDelete()
})
cancelBtn := dialog.AddButton("Cancel")
dialog.SetDefaultButton(cancelBtn) // Safe option highlighted by default
dialog.SetCancelButton(cancelBtn) // Escape triggers Cancel
dialog.Show()
```
### Window Attachment
@ -230,7 +270,7 @@ app.QuestionDialog().
Attach dialog to specific window:
```go
dialog := app.InfoDialog().
dialog := app.Dialog.Info().
SetMessage("Window-specific message").
AttachToWindow(window)
@ -258,10 +298,12 @@ dialog.Show()
**Example:**
```go
// Appears as sheet on macOS
app.QuestionDialog().
dialog := app.Dialog.Question().
SetMessage("Save changes?").
AttachToWindow(window).
Show()
AttachToWindow(window)
dialog.AddButton("Yes")
dialog.AddButton("No")
dialog.Show()
```
</TabItem>
@ -278,7 +320,7 @@ dialog.Show()
**Example:**
```go
// Modal dialog on Windows
app.ErrorDialog().
app.Dialog.Error().
SetTitle("Error").
SetMessage("Operation failed").
Show()
@ -297,7 +339,7 @@ dialog.Show()
**Example:**
```go
// GTK dialog on Linux
app.InfoDialog().
app.Dialog.Info().
SetMessage("Update complete").
Show()
```
@ -309,35 +351,41 @@ dialog.Show()
### Confirm Before Destructive Action
```go
func deleteFile(path string) error {
result, err := app.QuestionDialog().
func deleteFile(app *application.App, path string) {
dialog := app.Dialog.Question().
SetTitle("Confirm Delete").
SetMessage(fmt.Sprintf("Delete %s?", filepath.Base(path))).
SetButtons("Delete", "Cancel").
SetDefaultButton("Cancel").
Show()
if err != nil || result != "Delete" {
return errors.New("cancelled")
}
return os.Remove(path)
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()
}
```
### Error Handling with dialog
```go
func saveDocument(path string, data []byte) {
func saveDocument(app *application.App, path string, data []byte) {
if err := os.WriteFile(path, data, 0644); err != nil {
app.ErrorDialog().
app.Dialog.Error().
SetTitle("Save Failed").
SetMessage(fmt.Sprintf("Could not save file: %v", err)).
Show()
return
}
app.InfoDialog().
app.Dialog.Info().
SetTitle("Success").
SetMessage("File saved successfully!").
Show()
@ -347,27 +395,29 @@ func saveDocument(path string, data []byte) {
### File Selection with Validation
```go
func selectImageFile() (string, error) {
path, err := app.OpenFileDialog().
func selectImageFile(app *application.App) (string, error) {
path, err := app.Dialog.OpenFile().
SetTitle("Select Image").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg;*.jpeg;*.gif"},
}).
AddFilter("Images", "*.png;*.jpg;*.jpeg;*.gif").
PromptForSingleSelection()
if err != nil {
return "", err
}
if path == "" {
return "", errors.New("no file selected")
}
// Validate file
if !isValidImage(path) {
app.ErrorDialog().
app.Dialog.Error().
SetTitle("Invalid File").
SetMessage("Selected file is not a valid image.").
Show()
return "", errors.New("invalid image")
}
return path, nil
}
```
@ -375,45 +425,43 @@ func selectImageFile() (string, error) {
### Multi-Step dialog Flow
```go
func exportData() {
func exportData(app *application.App) {
// Step 1: Confirm export
result, _ := app.QuestionDialog().
dialog := app.Dialog.Question().
SetTitle("Export Data").
SetMessage("Export all data to CSV?").
SetButtons("Export", "Cancel").
Show()
if result != "Export" {
return
}
// Step 2: Select destination
path, err := app.SaveFileDialog().
SetTitle("Save Export").
SetDefaultFilename("export.csv").
SetFilters([]application.FileFilter{
{DisplayName: "CSV Files", Pattern: "*.csv"},
}).
PromptForSingleSelection()
if err != nil {
return
}
// Step 3: Perform export
if err := performExport(path); err != nil {
app.ErrorDialog().
SetTitle("Export Failed").
SetMessage(err.Error()).
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()
return
}
// Step 4: Success
app.InfoDialog().
SetTitle("Export Complete").
SetMessage("Data exported successfully!").
Show()
})
cancelBtn := dialog.AddButton("Cancel")
dialog.SetCancelButton(cancelBtn)
dialog.Show()
}
```

View file

@ -15,7 +15,7 @@ Wails provides a **unified screen API** that works across all platforms. Get scr
```go
// Get all screens
screens := app.Screens.GetAll()
screens := app.Screen.GetAll()
for _, screen := range screens {
fmt.Printf("Screen: %s (%dx%d)\n",
@ -34,7 +34,7 @@ fmt.Printf("Primary: %s\n", primary.Name)
### All Screens
```go
screens := app.Screens.GetAll()
screens := app.Screen.GetAll()
for _, screen := range screens {
fmt.Printf("ID: %s\n", screen.ID)
@ -137,7 +137,7 @@ func centreOnScreen(window *application.WebviewWindow, screen *Screen) {
```go
func moveToScreen(window *application.WebviewWindow, screenIndex int) {
screens := app.Screens.GetAll()
screens := app.Screen.GetAll()
if screenIndex < 0 || screenIndex >= len(screens) {
return
@ -180,11 +180,11 @@ func positionBottomRight(window *application.WebviewWindow, screen *Screen) {
```go
func hasMultipleMonitors() bool {
return len(app.Screens.GetAll()) > 1
return len(app.Screen.GetAll()) > 1
}
func getMonitorCount() int {
return len(app.Screens.GetAll())
return len(app.Screen.GetAll())
}
```
@ -192,7 +192,7 @@ func getMonitorCount() int {
```go
func listMonitors() {
screens := app.Screens.GetAll()
screens := app.Screen.GetAll()
fmt.Printf("Found %d monitor(s):\n", len(screens))
@ -214,7 +214,7 @@ func listMonitors() {
```go
func chooseMonitor() (*Screen, error) {
screens := app.Screens.GetAll()
screens := app.Screen.GetAll()
if len(screens) == 1 {
return screens[0], nil
@ -257,7 +257,7 @@ func NewMultiMonitorManager(app *application.Application) *MultiMonitorManager {
}
func (m *MultiMonitorManager) CreateWindowOnScreen(screenIndex int) error {
screens := m.app.Screens.GetAll()
screens := m.app.Screen.GetAll()
if screenIndex < 0 || screenIndex >= len(screens) {
return errors.New("invalid screen index")
@ -284,7 +284,7 @@ func (m *MultiMonitorManager) CreateWindowOnScreen(screenIndex int) error {
}
func (m *MultiMonitorManager) CreateWindowOnEachScreen() {
screens := m.app.Screens.GetAll()
screens := m.app.Screen.GetAll()
for i := range screens {
m.CreateWindowOnScreen(i)
@ -304,7 +304,7 @@ type ScreenMonitor struct {
func NewScreenMonitor(app *application.Application) *ScreenMonitor {
return &ScreenMonitor{
app: app,
lastScreens: app.Screens.GetAll(),
lastScreens: app.Screen.GetAll(),
}
}
@ -323,7 +323,7 @@ func (sm *ScreenMonitor) Start() {
}
func (sm *ScreenMonitor) checkScreens() {
current := sm.app.Screens.GetAll()
current := sm.app.Screen.GetAll()
if len(current) != len(sm.lastScreens) {
sm.lastScreens = current
@ -365,7 +365,7 @@ func createDPIAwareWindow(screen *Screen) *application.WebviewWindow {
```go
func visualiseScreenLayout() string {
screens := app.Screens.GetAll()
screens := app.Screen.GetAll()
var layout strings.Builder
layout.WriteString("Screen Layout:\n\n")

View file

@ -261,96 +261,78 @@ app.OnApplicationEvent(application.EventApplicationShutdown, func(e *application
})
```
## dialog Methods
## Dialog Methods
### InfoDialog()
Dialogs are accessed through the `app.Dialog` manager. See [Dialogs API](/reference/dialogs/) for complete reference.
Creates an information dialog.
### Message Dialogs
```go
func (a *App) InfoDialog() InfoDialog
```
**Example:**
```go
app.InfoDialog().
// Information dialog
app.Dialog.Info().
SetTitle("Success").
SetMessage("Operation completed!").
Show()
```
### QuestionDialog()
Creates a question dialog.
```go
func (a *App) QuestionDialog() QuestionDialog
```
**Example:**
```go
result, _ := app.QuestionDialog().
SetTitle("Confirm").
SetMessage("Continue?").
SetButtons("Yes", "No").
// Error dialog
app.Dialog.Error().
SetTitle("Error").
SetMessage("Something went wrong.").
Show()
if result == "Yes" {
// Continue
}
// Warning dialog
app.Dialog.Warning().
SetTitle("Warning").
SetMessage("This action cannot be undone.").
Show()
```
### ErrorDialog()
### Question Dialogs
Creates an error dialog.
Question dialogs use button callbacks to handle user responses:
```go
func (a *App) ErrorDialog() ErrorDialog
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.SetDefaultButton(yes)
dialog.SetCancelButton(no)
dialog.Show()
```
### WarningDialog()
Creates a warning dialog.
### File Dialogs
```go
func (a *App) WarningDialog() WarningDialog
```
### OpenFileDialog()
Creates a file open dialog.
```go
func (a *App) OpenFileDialog() OpenFileDialog
```
**Example:**
```go
path, err := app.OpenFileDialog().
// Open file dialog
path, err := app.Dialog.OpenFile().
SetTitle("Select File").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg"},
}).
AddFilter("Images", "*.png;*.jpg").
PromptForSingleSelection()
```
### SaveFileDialog()
// Save file dialog
path, err := app.Dialog.SaveFile().
SetTitle("Save File").
SetFilename("document.pdf").
AddFilter("PDF", "*.pdf").
PromptForSingleSelection()
Creates a file save dialog.
```go
func (a *App) SaveFileDialog() SaveFileDialog
```
### SelectFolderDialog()
Creates a folder selection dialog.
```go
func (a *App) SelectFolderDialog() SelectFolderDialog
// Folder selection (use OpenFile with directory options)
path, err := app.Dialog.OpenFile().
SetTitle("Select Folder").
CanChooseDirectories(true).
CanChooseFiles(false).
PromptForSingleSelection()
```
## Logger

View file

@ -9,37 +9,50 @@ import { Card, CardGrid } from "@astrojs/starlight/components";
## Overview
The Dialogs API provides methods to show native file dialogs and message dialogs.
The Dialogs API provides methods to show native file dialogs and message dialogs. Access dialogs through the `app.Dialog` manager.
**Dialog Types:**
- **File Dialogs** - Open, Save, and Select Folder dialogs
- **File Dialogs** - Open and Save dialogs
- **Message Dialogs** - Info, Error, Warning, and Question dialogs
All dialogs are **native OS dialogs** that match the platform's look and feel.
## Accessing Dialogs
Dialogs are accessed through the `app.Dialog` manager:
```go
app.Dialog.OpenFile()
app.Dialog.SaveFile()
app.Dialog.Info()
app.Dialog.Question()
app.Dialog.Warning()
app.Dialog.Error()
```
## File Dialogs
### OpenFileDialog()
### OpenFile()
Creates a file open dialog.
```go
func (a *App) OpenFileDialog() *OpenFileDialog
func (dm *DialogManager) OpenFile() *OpenFileDialogStruct
```
**Example:**
```go
dialog := app.OpenFileDialog()
dialog := app.Dialog.OpenFile()
```
### OpenFileDialog Methods
### OpenFileDialogStruct Methods
#### SetTitle()
Sets the dialog title.
```go
func (d *OpenFileDialog) SetTitle(title string) *OpenFileDialog
func (d *OpenFileDialogStruct) SetTitle(title string) *OpenFileDialogStruct
```
**Example:**
@ -47,43 +60,87 @@ func (d *OpenFileDialog) SetTitle(title string) *OpenFileDialog
dialog.SetTitle("Select Image")
```
#### SetFilters()
#### AddFilter()
Sets file type filters.
Adds a file type filter.
```go
func (d *OpenFileDialog) SetFilters(filters []FileFilter) *OpenFileDialog
func (d *OpenFileDialogStruct) AddFilter(displayName, pattern string) *OpenFileDialogStruct
```
**FileFilter structure:**
```go
type FileFilter struct {
DisplayName string // "Images", "Documents", etc.
Pattern string // "*.png;*.jpg", "*.pdf;*.doc", etc.
}
```
**Parameters:**
- `displayName` - Filter description shown to user (e.g., "Images", "Documents")
- `pattern` - Semicolon-separated list of extensions (e.g., "*.png;*.jpg")
**Example:**
```go
dialog.SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg;*.gif"},
{DisplayName: "Documents", Pattern: "*.pdf;*.docx"},
{DisplayName: "All Files", Pattern: "*.*"},
})
dialog.AddFilter("Images", "*.png;*.jpg;*.gif").
AddFilter("Documents", "*.pdf;*.docx").
AddFilter("All Files", "*.*")
```
#### SetDefaultDirectory()
#### SetDirectory()
Sets the initial directory.
```go
func (d *OpenFileDialog) SetDefaultDirectory(path string) *OpenFileDialog
func (d *OpenFileDialogStruct) SetDirectory(directory string) *OpenFileDialogStruct
```
**Example:**
```go
homeDir, _ := os.UserHomeDir()
dialog.SetDefaultDirectory(homeDir)
dialog.SetDirectory(homeDir)
```
#### CanChooseDirectories()
Enables or disables directory selection.
```go
func (d *OpenFileDialogStruct) CanChooseDirectories(canChooseDirectories bool) *OpenFileDialogStruct
```
**Example (folder selection):**
```go
// Select folders instead of files
path, err := app.Dialog.OpenFile().
SetTitle("Select Folder").
CanChooseDirectories(true).
CanChooseFiles(false).
PromptForSingleSelection()
```
#### CanChooseFiles()
Enables or disables file selection.
```go
func (d *OpenFileDialogStruct) CanChooseFiles(canChooseFiles bool) *OpenFileDialogStruct
```
#### CanCreateDirectories()
Enables or disables creating new directories.
```go
func (d *OpenFileDialogStruct) CanCreateDirectories(canCreateDirectories bool) *OpenFileDialogStruct
```
#### ShowHiddenFiles()
Shows or hides hidden files.
```go
func (d *OpenFileDialogStruct) ShowHiddenFiles(showHiddenFiles bool) *OpenFileDialogStruct
```
#### AttachToWindow()
Attaches the dialog to a specific window.
```go
func (d *OpenFileDialogStruct) AttachToWindow(window Window) *OpenFileDialogStruct
```
#### PromptForSingleSelection()
@ -91,20 +148,18 @@ dialog.SetDefaultDirectory(homeDir)
Shows the dialog and returns the selected file.
```go
func (d *OpenFileDialog) PromptForSingleSelection() (string, error)
func (d *OpenFileDialogStruct) PromptForSingleSelection() (string, error)
```
**Returns:**
- `string` - Selected file path
- `error` - Error if cancelled or failed
- `string` - Selected file path (empty if cancelled)
- `error` - Error if dialog failed
**Example:**
```go
path, err := app.OpenFileDialog().
path, err := app.Dialog.OpenFile().
SetTitle("Select Image").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg;*.gif"},
}).
AddFilter("Images", "*.png;*.jpg;*.gif").
PromptForSingleSelection()
if err != nil {
@ -121,20 +176,18 @@ processFile(path)
Shows the dialog and returns multiple selected files.
```go
func (d *OpenFileDialog) PromptForMultipleSelection() ([]string, error)
func (d *OpenFileDialogStruct) PromptForMultipleSelection() ([]string, error)
```
**Returns:**
- `[]string` - Array of selected file paths
- `error` - Error if cancelled or failed
- `error` - Error if dialog failed
**Example:**
```go
paths, err := app.OpenFileDialog().
paths, err := app.Dialog.OpenFile().
SetTitle("Select Images").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg"},
}).
AddFilter("Images", "*.png;*.jpg").
PromptForMultipleSelection()
if err != nil {
@ -146,56 +199,70 @@ for _, path := range paths {
}
```
### SaveFileDialog()
### SaveFile()
Creates a file save dialog.
```go
func (a *App) SaveFileDialog() *SaveFileDialog
func (dm *DialogManager) SaveFile() *SaveFileDialogStruct
```
**Example:**
```go
dialog := app.SaveFileDialog()
dialog := app.Dialog.SaveFile()
```
### SaveFileDialog Methods
### SaveFileDialogStruct Methods
#### SetTitle()
Sets the dialog title.
```go
func (d *SaveFileDialog) SetTitle(title string) *SaveFileDialog
func (d *SaveFileDialogStruct) SetTitle(title string) *SaveFileDialogStruct
```
#### SetDefaultFilename()
#### SetFilename()
Sets the default filename.
```go
func (d *SaveFileDialog) SetDefaultFilename(filename string) *SaveFileDialog
func (d *SaveFileDialogStruct) SetFilename(filename string) *SaveFileDialogStruct
```
**Example:**
```go
dialog.SetDefaultFilename("document.pdf")
dialog.SetFilename("document.pdf")
```
#### SetFilters()
#### AddFilter()
Sets file type filters (same as OpenFileDialog).
Adds a file type filter.
```go
func (d *SaveFileDialog) SetFilters(filters []FileFilter) *SaveFileDialog
func (d *SaveFileDialogStruct) AddFilter(displayName, pattern string) *SaveFileDialogStruct
```
#### SetDefaultDirectory()
**Example:**
```go
dialog.AddFilter("PDF Document", "*.pdf").
AddFilter("Text Document", "*.txt")
```
Sets the initial directory (same as OpenFileDialog).
#### SetDirectory()
Sets the initial directory.
```go
func (d *SaveFileDialog) SetDefaultDirectory(path string) *SaveFileDialog
func (d *SaveFileDialogStruct) SetDirectory(directory string) *SaveFileDialogStruct
```
#### AttachToWindow()
Attaches the dialog to a specific window.
```go
func (d *SaveFileDialogStruct) AttachToWindow(window Window) *SaveFileDialogStruct
```
#### PromptForSingleSelection()
@ -203,17 +270,15 @@ func (d *SaveFileDialog) SetDefaultDirectory(path string) *SaveFileDialog
Shows the dialog and returns the save path.
```go
func (d *SaveFileDialog) PromptForSingleSelection() (string, error)
func (d *SaveFileDialogStruct) PromptForSingleSelection() (string, error)
```
**Example:**
```go
path, err := app.SaveFileDialog().
path, err := app.Dialog.SaveFile().
SetTitle("Save Document").
SetDefaultFilename("untitled.pdf").
SetFilters([]application.FileFilter{
{DisplayName: "PDF Document", Pattern: "*.pdf"},
}).
SetFilename("untitled.pdf").
AddFilter("PDF Document", "*.pdf").
PromptForSingleSelection()
if err != nil {
@ -225,49 +290,15 @@ if err != nil {
saveDocument(path)
```
### SelectFolderDialog()
### Folder Selection
Creates a folder selection dialog.
There is no separate `SelectFolderDialog`. Use `OpenFile()` with directory options:
```go
func (a *App) SelectFolderDialog() *SelectFolderDialog
```
**Example:**
```go
dialog := app.SelectFolderDialog()
```
### SelectFolderDialog Methods
#### SetTitle()
Sets the dialog title.
```go
func (d *SelectFolderDialog) SetTitle(title string) *SelectFolderDialog
```
#### SetDefaultDirectory()
Sets the initial directory.
```go
func (d *SelectFolderDialog) SetDefaultDirectory(path string) *SelectFolderDialog
```
#### PromptForSingleSelection()
Shows the dialog and returns the selected folder.
```go
func (d *SelectFolderDialog) PromptForSingleSelection() (string, error)
```
**Example:**
```go
path, err := app.SelectFolderDialog().
path, err := app.Dialog.OpenFile().
SetTitle("Select Output Folder").
CanChooseDirectories(true).
CanChooseFiles(false).
PromptForSingleSelection()
if err != nil {
@ -281,105 +312,98 @@ outputDir = path
## Message Dialogs
### InfoDialog()
All message dialogs return `*MessageDialog` and share the same methods.
### Info()
Creates an information dialog.
```go
func (a *App) InfoDialog() *InfoDialog
func (dm *DialogManager) Info() *MessageDialog
```
**Example:**
```go
dialog := app.InfoDialog()
```
### InfoDialog Methods
#### SetTitle()
Sets the dialog title.
```go
func (d *InfoDialog) SetTitle(title string) *InfoDialog
```
#### SetMessage()
Sets the dialog message.
```go
func (d *InfoDialog) SetMessage(message string) *InfoDialog
```
#### Show()
Shows the dialog.
```go
func (d *InfoDialog) Show()
```
**Example:**
```go
app.InfoDialog().
app.Dialog.Info().
SetTitle("Success").
SetMessage("File saved successfully!").
Show()
```
### ErrorDialog()
### Error()
Creates an error dialog.
```go
func (a *App) ErrorDialog() *ErrorDialog
func (dm *DialogManager) Error() *MessageDialog
```
**Methods:** Same as InfoDialog (SetTitle, SetMessage, Show)
**Example:**
```go
app.ErrorDialog().
app.Dialog.Error().
SetTitle("Error").
SetMessage("Failed to save file: " + err.Error()).
Show()
```
### WarningDialog()
### Warning()
Creates a warning dialog.
```go
func (a *App) WarningDialog() *WarningDialog
func (dm *DialogManager) Warning() *MessageDialog
```
**Methods:** Same as InfoDialog (SetTitle, SetMessage, Show)
**Example:**
```go
app.WarningDialog().
app.Dialog.Warning().
SetTitle("Warning").
SetMessage("This action cannot be undone.").
Show()
```
### QuestionDialog()
### Question()
Creates a question dialog with custom buttons.
```go
func (a *App) QuestionDialog() *QuestionDialog
func (dm *DialogManager) Question() *MessageDialog
```
### QuestionDialog Methods
**Example:**
```go
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.SetDefaultButton(save)
dialog.SetCancelButton(cancel)
dialog.Show()
```
### MessageDialog Methods
#### SetTitle()
Sets the dialog title.
```go
func (d *QuestionDialog) SetTitle(title string) *QuestionDialog
func (d *MessageDialog) SetTitle(title string) *MessageDialog
```
#### SetMessage()
@ -387,24 +411,33 @@ func (d *QuestionDialog) SetTitle(title string) *QuestionDialog
Sets the dialog message.
```go
func (d *QuestionDialog) SetMessage(message string) *QuestionDialog
func (d *MessageDialog) SetMessage(message string) *MessageDialog
```
#### SetButtons()
#### SetIcon()
Sets the button labels.
Sets a custom icon for the dialog.
```go
func (d *QuestionDialog) SetButtons(buttons ...string) *QuestionDialog
func (d *MessageDialog) SetIcon(icon []byte) *MessageDialog
```
**Parameters:**
- `buttons` - Variable number of button labels
#### AddButton()
Adds a button to the dialog and returns the button for configuration.
```go
func (d *MessageDialog) AddButton(label string) *Button
```
**Returns:** `*Button` - The button instance for further configuration
**Example:**
```go
dialog.SetButtons("Yes", "No")
dialog.SetButtons("Save", "Don't Save", "Cancel")
button := dialog.AddButton("OK")
button.OnClick(func() {
// Handle click
})
```
#### SetDefaultButton()
@ -412,13 +445,14 @@ dialog.SetButtons("Save", "Don't Save", "Cancel")
Sets which button is the default (activated by pressing Enter).
```go
func (d *QuestionDialog) SetDefaultButton(index int) *QuestionDialog
func (d *MessageDialog) SetDefaultButton(button *Button) *MessageDialog
```
**Example:**
```go
// Make "Yes" the default button
dialog.SetButtons("Yes", "No").SetDefaultButton(0)
yes := dialog.AddButton("Yes")
no := dialog.AddButton("No")
dialog.SetDefaultButton(yes)
```
#### SetCancelButton()
@ -426,49 +460,58 @@ dialog.SetButtons("Yes", "No").SetDefaultButton(0)
Sets which button is the cancel button (activated by pressing Escape).
```go
func (d *QuestionDialog) SetCancelButton(index int) *QuestionDialog
func (d *MessageDialog) SetCancelButton(button *Button) *MessageDialog
```
**Example:**
```go
// Make "Cancel" the cancel button
dialog.SetButtons("Save", "Don't Save", "Cancel").SetCancelButton(2)
ok := dialog.AddButton("OK")
cancel := dialog.AddButton("Cancel")
dialog.SetCancelButton(cancel)
```
#### AttachToWindow()
Attaches the dialog to a specific window.
```go
func (d *MessageDialog) AttachToWindow(window Window) *MessageDialog
```
#### Show()
Shows the dialog and returns the selected button.
Shows the dialog. Button callbacks handle user responses.
```go
func (d *QuestionDialog) Show() (string, error)
func (d *MessageDialog) Show()
```
**Returns:**
- `string` - The label of the button that was clicked
- `error` - Error if dialog failed to 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.
**Example:**
```go
result, err := app.QuestionDialog().
SetTitle("Confirm").
SetMessage("Do you want to save changes?").
SetButtons("Save", "Don't Save", "Cancel").
SetDefaultButton(0).
SetCancelButton(2).
Show()
func (b *Button) OnClick(callback func()) *Button
```
if err != nil {
return
}
#### SetAsDefault()
switch result {
case "Save":
saveDocument()
case "Don't Save":
// Continue without saving
case "Cancel":
return
}
Marks this button as the default button.
```go
func (b *Button) SetAsDefault() *Button
```
#### SetAsCancel()
Marks this button as the cancel button.
```go
func (b *Button) SetAsCancel() *Button
```
## Complete Examples
@ -477,16 +520,14 @@ case "Cancel":
```go
type FileService struct {
app *application.Application
app *application.App
}
func (s *FileService) OpenImage() (string, error) {
path, err := s.app.OpenFileDialog().
path, err := s.app.Dialog.OpenFile().
SetTitle("Select Image").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg;*.jpeg;*.gif"},
{DisplayName: "All Files", Pattern: "*.*"},
}).
AddFilter("Images", "*.png;*.jpg;*.jpeg;*.gif").
AddFilter("All Files", "*.*").
PromptForSingleSelection()
if err != nil {
@ -497,13 +538,11 @@ func (s *FileService) OpenImage() (string, error) {
}
func (s *FileService) SaveDocument(defaultName string) (string, error) {
path, err := s.app.SaveFileDialog().
path, err := s.app.Dialog.SaveFile().
SetTitle("Save Document").
SetDefaultFilename(defaultName).
SetFilters([]application.FileFilter{
{DisplayName: "PDF Document", Pattern: "*.pdf"},
{DisplayName: "Text Document", Pattern: "*.txt"},
}).
SetFilename(defaultName).
AddFilter("PDF Document", "*.pdf").
AddFilter("Text Document", "*.txt").
PromptForSingleSelection()
if err != nil {
@ -514,8 +553,10 @@ func (s *FileService) SaveDocument(defaultName string) (string, error) {
}
func (s *FileService) SelectOutputFolder() (string, error) {
path, err := s.app.SelectFolderDialog().
path, err := s.app.Dialog.OpenFile().
SetTitle("Select Output Folder").
CanChooseDirectories(true).
CanChooseFiles(false).
PromptForSingleSelection()
if err != nil {
@ -529,64 +570,61 @@ func (s *FileService) SelectOutputFolder() (string, error) {
### Confirmation Dialog Example
```go
func (s *Service) DeleteItem(id string) error {
// Confirm before deleting
result, err := s.app.QuestionDialog().
func (s *Service) DeleteItem(app *application.App, id string) {
dialog := app.Dialog.Question().
SetTitle("Confirm Delete").
SetMessage("Are you sure you want to delete this item?").
SetButtons("Delete", "Cancel").
SetDefaultButton(1). // Default to Cancel
SetCancelButton(1).
Show()
SetMessage("Are you sure you want to delete this item?")
if err != nil || result == "Cancel" {
return nil // User cancelled
}
deleteBtn := dialog.AddButton("Delete")
deleteBtn.OnClick(func() {
deleteFromDatabase(id)
})
// Perform deletion
return deleteFromDatabase(id)
cancelBtn := dialog.AddButton("Cancel")
// Cancel does nothing
dialog.SetDefaultButton(cancelBtn) // Default to Cancel for safety
dialog.SetCancelButton(cancelBtn)
dialog.Show()
}
```
### Save Changes Dialog
```go
func (s *Editor) PromptSaveChanges() (bool, error) {
result, err := s.app.QuestionDialog().
func (s *Editor) PromptSaveChanges(app *application.App) {
dialog := app.Dialog.Question().
SetTitle("Unsaved Changes").
SetMessage("Do you want to save your changes before closing?").
SetButtons("Save", "Don't Save", "Cancel").
SetDefaultButton(0).
SetCancelButton(2).
Show()
SetMessage("Do you want to save your changes before closing?")
if err != nil {
return false, err
}
save := dialog.AddButton("Save")
save.OnClick(func() {
s.Save()
s.Close()
})
switch result {
case "Save":
return s.Save()
case "Don't Save":
return true, nil
case "Cancel":
return false, nil
}
dontSave := dialog.AddButton("Don't Save")
dontSave.OnClick(func() {
s.Close()
})
return false, nil
cancel := dialog.AddButton("Cancel")
// Cancel does nothing, dialog closes
dialog.SetDefaultButton(save)
dialog.SetCancelButton(cancel)
dialog.Show()
}
```
### Multi-File Processing
```go
func (s *Service) ProcessMultipleFiles() error {
func (s *Service) ProcessMultipleFiles(app *application.App) error {
// Select multiple files
paths, err := s.app.OpenFileDialog().
paths, err := app.Dialog.OpenFile().
SetTitle("Select Files to Process").
SetFilters([]application.FileFilter{
{DisplayName: "Images", Pattern: "*.png;*.jpg"},
}).
AddFilter("Images", "*.png;*.jpg").
PromptForMultipleSelection()
if err != nil {
@ -594,7 +632,7 @@ func (s *Service) ProcessMultipleFiles() error {
}
if len(paths) == 0 {
s.app.InfoDialog().
app.Dialog.Info().
SetTitle("No Files Selected").
SetMessage("Please select at least one file.").
Show()
@ -602,25 +640,19 @@ func (s *Service) ProcessMultipleFiles() error {
}
// Process files
for i, path := range paths {
for _, path := range paths {
err := processFile(path)
if err != nil {
s.app.ErrorDialog().
app.Dialog.Error().
SetTitle("Processing Error").
SetMessage(fmt.Sprintf("Failed to process %s: %v", path, err)).
Show()
continue
}
// Show progress
s.app.EmitEvent("progress", map[string]interface{}{
"current": i + 1,
"total": len(paths),
})
}
// Show completion
s.app.InfoDialog().
app.Dialog.Info().
SetTitle("Complete").
SetMessage(fmt.Sprintf("Successfully processed %d files", len(paths))).
Show()
@ -632,14 +664,12 @@ func (s *Service) ProcessMultipleFiles() error {
### Error Handling with Dialogs
```go
func (s *Service) SaveFile(data []byte) error {
func (s *Service) SaveFile(app *application.App, data []byte) error {
// Select save location
path, err := s.app.SaveFileDialog().
path, err := app.Dialog.SaveFile().
SetTitle("Save File").
SetDefaultFilename("data.json").
SetFilters([]application.FileFilter{
{DisplayName: "JSON File", Pattern: "*.json"},
}).
SetFilename("data.json").
AddFilter("JSON File", "*.json").
PromptForSingleSelection()
if err != nil {
@ -651,7 +681,7 @@ func (s *Service) SaveFile(data []byte) error {
err = os.WriteFile(path, data, 0644)
if err != nil {
// Show error dialog
s.app.ErrorDialog().
app.Dialog.Error().
SetTitle("Save Failed").
SetMessage(fmt.Sprintf("Could not save file: %v", err)).
Show()
@ -659,7 +689,7 @@ func (s *Service) SaveFile(data []byte) error {
}
// Show success
s.app.InfoDialog().
app.Dialog.Info().
SetTitle("Success").
SetMessage("File saved successfully!").
Show()
@ -692,33 +722,32 @@ func (s *Service) GetDefaultDirectory() string {
}
}
func (s *Service) OpenWithDefaults() (string, error) {
return s.app.OpenFileDialog().
func (s *Service) OpenWithDefaults(app *application.App) (string, error) {
return app.Dialog.OpenFile().
SetTitle("Open File").
SetDefaultDirectory(s.GetDefaultDirectory()).
SetFilters([]application.FileFilter{
{DisplayName: "All Files", Pattern: "*.*"},
}).
SetDirectory(s.GetDefaultDirectory()).
AddFilter("All Files", "*.*").
PromptForSingleSelection()
}
```
## Best Practices
### Do
### Do
- **Use native dialogs** - They match the platform's look and feel
- **Provide clear titles** - Help users understand the purpose
- **Set appropriate filters** - Guide users to correct file types
- **Handle cancellation** - Check for errors (user may cancel)
- **Show confirmation for destructive actions** - Use QuestionDialog
- **Provide feedback** - Use InfoDialog for success messages
- **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
### Don't
- **Don't ignore errors** - User cancellation returns an error
- **Don't use ambiguous button labels** - "OK" vs "Save"/"Cancel"
- **Don't use ambiguous button labels** - Be specific: "Save"/"Cancel"
- **Don't overuse dialogs** - They interrupt workflow
- **Don't show errors for cancellation** - It's a normal action
- **Don't forget file filters** - Help users find the right files
@ -749,14 +778,14 @@ func (s *Service) OpenWithDefaults() (string, error) {
### "Save As" Pattern
```go
func (s *Service) SaveAs(currentPath string) (string, error) {
func (s *Service) SaveAs(app *application.App, currentPath string) (string, error) {
// Extract filename from current path
filename := filepath.Base(currentPath)
// Show save dialog
path, err := s.app.SaveFileDialog().
path, err := app.Dialog.SaveFile().
SetTitle("Save As").
SetDefaultFilename(filename).
SetFilename(filename).
PromptForSingleSelection()
if err != nil {
@ -770,18 +799,21 @@ func (s *Service) SaveAs(currentPath string) (string, error) {
### "Open Recent" Pattern
```go
func (s *Service) OpenRecent(recentPath string) error {
func (s *Service) OpenRecent(app *application.App, recentPath string) error {
// Check if file still exists
if _, err := os.Stat(recentPath); os.IsNotExist(err) {
result, _ := s.app.QuestionDialog().
dialog := app.Dialog.Question().
SetTitle("File Not Found").
SetMessage("The file no longer exists. Remove from recent files?").
SetButtons("Remove", "Cancel").
Show()
SetMessage("The file no longer exists. Remove from recent files?")
if result == "Remove" {
remove := dialog.AddButton("Remove")
remove.OnClick(func() {
s.removeFromRecent(recentPath)
}
})
cancel := dialog.AddButton("Cancel")
dialog.SetCancelButton(cancel)
dialog.Show()
return err
}

View file

@ -133,6 +133,12 @@ Generate bindings:
```bash
wails3 generate bindings
# Or, to include TypeScript definitions
wails3 generate bindings -ts
# For more options, see:
wails3 generate bindings -help
```
Use typed events in your frontend:
@ -944,4 +950,4 @@ On('custom-event', (event: WailsEvent) => {
// All methods are fully typed
const size: { width: number, height: number } = await Window.Size()
```
```

View file

@ -1,6 +1,6 @@
# Unreleased Changes
<!--
<!--
This file is used to collect changelog entries for the next v3-alpha release.
Add your changes under the appropriate sections below.
@ -16,17 +16,14 @@ After processing, the content will be moved to the main changelog and this file
-->
## Added
- Add `Window.Print()` method to JavaScript runtime for triggering print dialog from frontend (#4290) by @leaanthony
<!-- New features, capabilities, or enhancements -->
## Changed
<!-- Changes in existing functionality -->
## Fixed
- Fix macOS dock icon click not showing hidden windows when app started with `Hidden: true` (#4583) by @leaanthony
- Fix macOS print dialog not opening due to incorrect window pointer type in CGO call (#4290) by @leaanthony
- Fix fullscreen click-through on Windows when using `Frameless: true` with `BackgroundTypeTransparent` (#4408) by @anthropic-vibekanban
<!-- Bug fixes -->
- Fix fullscreen click-through on Windows when using `Frameless: true` with `BackgroundTypeTransparent` (#4408) by @anthropic-vibekanban
## Deprecated
<!-- Soon-to-be removed features -->

View file

@ -143,7 +143,7 @@ func main() {
sizeMenu.Add("Get Current WebviewWindow Size").OnClick(func(ctx *application.Context) {
currentWindow(func(w application.Window) {
width, height := w.Size()
application.InfoDialog().SetTitle("Current WebviewWindow Size").SetMessage("Width: " + strconv.Itoa(width) + " Height: " + strconv.Itoa(height)).Show()
app.Dialog.Info().SetTitle("Current WebviewWindow Size").SetMessage("Width: " + strconv.Itoa(width) + " Height: " + strconv.Itoa(height)).Show()
})
})
@ -174,7 +174,7 @@ func main() {
positionMenu.Add("Get Relative Position").OnClick(func(ctx *application.Context) {
currentWindow(func(w application.Window) {
x, y := w.RelativePosition()
application.InfoDialog().SetTitle("Current WebviewWindow Relative Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
app.Dialog.Info().SetTitle("Current WebviewWindow Relative Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
})
})
@ -241,24 +241,24 @@ func main() {
stateMenu.Add("Get Primary Screen").OnClick(func(ctx *application.Context) {
screen := app.Screen.GetPrimary()
msg := fmt.Sprintf("Screen: %+v", screen)
application.InfoDialog().SetTitle("Primary Screen").SetMessage(msg).Show()
app.Dialog.Info().SetTitle("Primary Screen").SetMessage(msg).Show()
})
stateMenu.Add("Get Screens").OnClick(func(ctx *application.Context) {
screens := app.Screen.GetAll()
for _, screen := range screens {
msg := fmt.Sprintf("Screen: %+v", screen)
application.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
app.Dialog.Info().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
}
})
stateMenu.Add("Get Screen for WebviewWindow").OnClick(func(ctx *application.Context) {
currentWindow(func(w application.Window) {
screen, err := w.GetScreen()
if err != nil {
application.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
app.Dialog.Error().SetTitle("Error").SetMessage(err.Error()).Show()
return
}
msg := fmt.Sprintf("Screen: %+v", screen)
application.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
app.Dialog.Info().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
})
})
app.Window.New()

View file

@ -30,28 +30,28 @@ func main() {
setClipboardMenu.Add("Set Text 'Hello'").OnClick(func(ctx *application.Context) {
success := app.Clipboard.SetText("Hello")
if !success {
application.InfoDialog().SetMessage("Failed to set clipboard text").Show()
app.Dialog.Info().SetMessage("Failed to set clipboard text").Show()
}
})
setClipboardMenu.Add("Set Text 'World'").OnClick(func(ctx *application.Context) {
success := app.Clipboard.SetText("World")
if !success {
application.InfoDialog().SetMessage("Failed to set clipboard text").Show()
app.Dialog.Info().SetMessage("Failed to set clipboard text").Show()
}
})
setClipboardMenu.Add("Set Text (current time)").OnClick(func(ctx *application.Context) {
success := app.Clipboard.SetText(time.Now().String())
if !success {
application.InfoDialog().SetMessage("Failed to set clipboard text").Show()
app.Dialog.Info().SetMessage("Failed to set clipboard text").Show()
}
})
getClipboardMenu := menu.AddSubmenu("Get Clipboard")
getClipboardMenu.Add("Get Text").OnClick(func(ctx *application.Context) {
result, ok := app.Clipboard.Text()
if !ok {
application.InfoDialog().SetMessage("Failed to get clipboard text").Show()
app.Dialog.Info().SetMessage("Failed to get clipboard text").Show()
} else {
application.InfoDialog().SetMessage("Got:\n\n" + result).Show()
app.Dialog.Info().SetMessage("Got:\n\n" + result).Show()
}
})
@ -59,9 +59,9 @@ func main() {
clearClipboardMenu.Add("Clear Text").OnClick(func(ctx *application.Context) {
success := app.Clipboard.SetText("")
if success {
application.InfoDialog().SetMessage("Clipboard text cleared").Show()
app.Dialog.Info().SetMessage("Clipboard text cleared").Show()
} else {
application.InfoDialog().SetMessage("Clipboard text not cleared").Show()
app.Dialog.Info().SetMessage("Clipboard text not cleared").Show()
}
})

View file

@ -44,7 +44,7 @@ func main() {
// Test 1: Basic file open with no filters (no window)
testMenu.Add("1. Basic Open (No Window)").OnClick(func(ctx *application.Context) {
result, err := application.OpenFileDialog().
result, err := app.Dialog.OpenFile().
CanChooseFiles(true).
PromptForSingleSelection()
showResult("Basic Open", result, err, nil)
@ -52,7 +52,7 @@ func main() {
// Test 1b: Basic file open with window
testMenu.Add("1b. Basic Open (With Window)").OnClick(func(ctx *application.Context) {
result, err := application.OpenFileDialog().
result, err := app.Dialog.OpenFile().
CanChooseFiles(true).
AttachToWindow(mainWindow).
PromptForSingleSelection()
@ -61,7 +61,7 @@ func main() {
// Test 2: Open with single extension filter
testMenu.Add("2. Single Filter").OnClick(func(ctx *application.Context) {
result, err := application.OpenFileDialog().
result, err := app.Dialog.OpenFile().
CanChooseFiles(true).
AddFilter("Text Files", "*.txt").
AttachToWindow(mainWindow).
@ -71,7 +71,7 @@ func main() {
// Test 3: Open with multiple extension filter
testMenu.Add("3. Multiple Filter").OnClick(func(ctx *application.Context) {
result, err := application.OpenFileDialog().
result, err := app.Dialog.OpenFile().
CanChooseFiles(true).
AddFilter("Documents", "*.txt;*.md;*.doc;*.docx").
AttachToWindow(mainWindow).
@ -81,7 +81,7 @@ func main() {
// Test 4: Multiple file selection
testMenu.Add("4. Multiple Selection").OnClick(func(ctx *application.Context) {
results, err := application.OpenFileDialog().
results, err := app.Dialog.OpenFile().
CanChooseFiles(true).
AddFilter("Images", "*.png;*.jpg;*.jpeg").
AttachToWindow(mainWindow).
@ -95,7 +95,7 @@ func main() {
// Test 5: Directory selection
testMenu.Add("5. Directory Selection").OnClick(func(ctx *application.Context) {
result, err := application.OpenFileDialog().
result, err := app.Dialog.OpenFile().
CanChooseDirectories(true).
CanChooseFiles(false).
AttachToWindow(mainWindow).
@ -105,7 +105,7 @@ func main() {
// Test 6: Save dialog with extension
testMenu.Add("6. Save Dialog").OnClick(func(ctx *application.Context) {
result, err := application.SaveFileDialog().
result, err := app.Dialog.SaveFile().
SetFilename("test.txt").
AddFilter("Text Files", "*.txt").
AttachToWindow(mainWindow).
@ -115,7 +115,7 @@ func main() {
// Test 7: Complex filters
testMenu.Add("7. Complex Filters").OnClick(func(ctx *application.Context) {
result, err := application.OpenFileDialog().
result, err := app.Dialog.OpenFile().
CanChooseFiles(true).
AddFilter("All Documents", "*.txt;*.md;*.doc;*.docx;*.pdf").
AddFilter("Text Files", "*.txt").
@ -129,7 +129,7 @@ func main() {
// Test 8: Hidden files
testMenu.Add("8. Show Hidden").OnClick(func(ctx *application.Context) {
result, err := application.OpenFileDialog().
result, err := app.Dialog.OpenFile().
CanChooseFiles(true).
ShowHiddenFiles(true).
AttachToWindow(mainWindow).
@ -140,7 +140,7 @@ func main() {
// Test 9: Default directory
testMenu.Add("9. Default Directory").OnClick(func(ctx *application.Context) {
home, _ := os.UserHomeDir()
result, err := application.OpenFileDialog().
result, err := app.Dialog.OpenFile().
CanChooseFiles(true).
SetDirectory(home).
AttachToWindow(mainWindow).
@ -151,7 +151,7 @@ func main() {
// Test 10: Full featured dialog
testMenu.Add("10. Full Featured").OnClick(func(ctx *application.Context) {
home, _ := os.UserHomeDir()
dialog := application.OpenFileDialog().
dialog := app.Dialog.OpenFile().
SetTitle("Full Featured Dialog").
SetDirectory(home).
CanChooseFiles(true).
@ -192,7 +192,7 @@ func showResult(test string, result string, err error, window *application.Webvi
return
}
if result == "" {
dialog := application.InfoDialog().
dialog := application.Get().Dialog.Info().
SetTitle(test).
SetMessage("No file selected")
if window != nil {
@ -201,7 +201,7 @@ func showResult(test string, result string, err error, window *application.Webvi
dialog.Show()
return
}
dialog := application.InfoDialog().
dialog := application.Get().Dialog.Info().
SetTitle(test).
SetMessage(fmt.Sprintf("Selected: %s\nType: %s", result, getFileType(result)))
if window != nil {
@ -212,7 +212,7 @@ func showResult(test string, result string, err error, window *application.Webvi
func showResults(test string, results []string, window *application.WebviewWindow) {
if len(results) == 0 {
dialog := application.InfoDialog().
dialog := application.Get().Dialog.Info().
SetTitle(test).
SetMessage("No files selected")
if window != nil {
@ -226,7 +226,7 @@ func showResults(test string, results []string, window *application.WebviewWindo
for _, result := range results {
message.WriteString(fmt.Sprintf("%s (%s)\n", result, getFileType(result)))
}
dialog := application.InfoDialog().
dialog := application.Get().Dialog.Info().
SetTitle(test).
SetMessage(message.String())
if window != nil {
@ -236,7 +236,7 @@ func showResults(test string, results []string, window *application.WebviewWindo
}
func showError(test string, err error, window *application.WebviewWindow) {
dialog := application.ErrorDialog().
dialog := application.Get().Dialog.Error().
SetTitle(test).
SetMessage(fmt.Sprintf("Error: %v", err))
if window != nil {
@ -249,11 +249,8 @@ func getFileType(path string) string {
if path == "" {
return "unknown"
}
ext := strings.ToLower(filepath.Ext(path))
ext := filepath.Ext(path)
if ext == "" {
if fi, err := os.Stat(path); err == nil && fi.IsDir() {
return "directory"
}
return "no extension"
}
return ext

View file

@ -36,24 +36,24 @@ func main() {
// Let's make a "Demo" menu
infoMenu := menu.AddSubmenu("Info")
infoMenu.Add("Info").OnClick(func(ctx *application.Context) {
dialog := application.InfoDialog()
dialog := app.Dialog.Info()
dialog.SetTitle("Custom Title")
dialog.SetMessage("This is a custom message")
dialog.Show()
})
infoMenu.Add("Info (Title only)").OnClick(func(ctx *application.Context) {
dialog := application.InfoDialog()
dialog := app.Dialog.Info()
dialog.SetTitle("Custom Title")
dialog.Show()
})
infoMenu.Add("Info (Message only)").OnClick(func(ctx *application.Context) {
dialog := application.InfoDialog()
dialog := app.Dialog.Info()
dialog.SetMessage("This is a custom message")
dialog.Show()
})
infoMenu.Add("Info (Custom Icon)").OnClick(func(ctx *application.Context) {
dialog := application.InfoDialog()
dialog := app.Dialog.Info()
dialog.SetTitle("Custom Icon Example")
dialog.SetMessage("Using a custom icon")
dialog.SetIcon(icons.ApplicationDarkMode256)
@ -65,14 +65,14 @@ func main() {
questionMenu := menu.AddSubmenu("Question")
questionMenu.Add("Question (No default)").OnClick(func(ctx *application.Context) {
dialog := application.QuestionDialog()
dialog := app.Dialog.Question()
dialog.SetMessage("No default button")
dialog.AddButton("Yes")
dialog.AddButton("No")
dialog.Show()
})
questionMenu.Add("Question (Attached to Window)").OnClick(func(ctx *application.Context) {
dialog := application.QuestionDialog()
dialog := app.Dialog.Question()
dialog.AttachToWindow(app.Window.Current())
dialog.SetMessage("No default button")
dialog.AddButton("Yes")
@ -80,7 +80,7 @@ func main() {
dialog.Show()
})
questionMenu.Add("Question (With Default)").OnClick(func(ctx *application.Context) {
dialog := application.QuestionDialog()
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() {
@ -91,12 +91,12 @@ func main() {
dialog.Show()
})
questionMenu.Add("Question (With Cancel)").OnClick(func(ctx *application.Context) {
dialog := application.QuestionDialog().
dialog := app.Dialog.Question().
SetTitle("Update").
SetMessage("The cancel button is selected when pressing escape")
download := dialog.AddButton("📥 Download")
download.OnClick(func() {
application.InfoDialog().SetMessage("Downloading...").Show()
app.Dialog.Info().SetMessage("Downloading...").Show()
})
no := dialog.AddButton("Cancel")
dialog.SetDefaultButton(download)
@ -104,15 +104,15 @@ func main() {
dialog.Show()
})
questionMenu.Add("Question (Custom Icon)").OnClick(func(ctx *application.Context) {
dialog := application.QuestionDialog()
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() {
application.InfoDialog().SetMessage("Thanks!").Show()
app.Dialog.Info().SetMessage("Thanks!").Show()
})
dialog.AddButton("Not so keen...").OnClick(func() {
application.InfoDialog().SetMessage("Too bad!").Show()
app.Dialog.Info().SetMessage("Too bad!").Show()
})
dialog.SetDefaultButton(likeIt)
dialog.Show()
@ -120,23 +120,23 @@ func main() {
warningMenu := menu.AddSubmenu("Warning")
warningMenu.Add("Warning").OnClick(func(ctx *application.Context) {
application.WarningDialog().
app.Dialog.Warning().
SetTitle("Custom Title").
SetMessage("This is a custom message").
Show()
})
warningMenu.Add("Warning (Title only)").OnClick(func(ctx *application.Context) {
dialog := application.WarningDialog()
dialog := app.Dialog.Warning()
dialog.SetTitle("Custom Title")
dialog.Show()
})
warningMenu.Add("Warning (Message only)").OnClick(func(ctx *application.Context) {
dialog := application.WarningDialog()
dialog := app.Dialog.Warning()
dialog.SetMessage("This is a custom message")
dialog.Show()
})
warningMenu.Add("Warning (Custom Icon)").OnClick(func(ctx *application.Context) {
dialog := application.WarningDialog()
dialog := app.Dialog.Warning()
dialog.SetTitle("Custom Icon Example")
dialog.SetMessage("Using a custom icon")
dialog.SetIcon(icons.ApplicationLightMode256)
@ -145,23 +145,23 @@ func main() {
errorMenu := menu.AddSubmenu("Error")
errorMenu.Add("Error").OnClick(func(ctx *application.Context) {
dialog := application.ErrorDialog()
dialog := app.Dialog.Error()
dialog.SetTitle("Ooops")
dialog.SetMessage("I accidentally the whole of Twitter")
dialog.Show()
})
errorMenu.Add("Error (Title Only)").OnClick(func(ctx *application.Context) {
dialog := application.ErrorDialog()
dialog := app.Dialog.Error()
dialog.SetTitle("Custom Title")
dialog.Show()
})
errorMenu.Add("Error (Custom Message)").OnClick(func(ctx *application.Context) {
application.ErrorDialog().
app.Dialog.Error().
SetMessage("This is a custom message").
Show()
})
errorMenu.Add("Error (Custom Icon)").OnClick(func(ctx *application.Context) {
dialog := application.ErrorDialog()
dialog := app.Dialog.Error()
dialog.SetTitle("Custom Icon Example")
dialog.SetMessage("Using a custom icon")
dialog.SetIcon(icons.WailsLogoWhite)
@ -170,90 +170,90 @@ func main() {
openMenu := menu.AddSubmenu("Open")
openMenu.Add("Open File").OnClick(func(ctx *application.Context) {
result, _ := application.OpenFileDialog().
result, _ := app.Dialog.OpenFile().
CanChooseFiles(true).
PromptForSingleSelection()
if result != "" {
application.InfoDialog().SetMessage(result).Show()
app.Dialog.Info().SetMessage(result).Show()
} else {
application.InfoDialog().SetMessage("No file selected").Show()
app.Dialog.Info().SetMessage("No file selected").Show()
}
})
openMenu.Add("Open File (Show Hidden Files)").OnClick(func(ctx *application.Context) {
result, _ := application.OpenFileDialog().
result, _ := app.Dialog.OpenFile().
CanChooseFiles(true).
CanCreateDirectories(true).
ShowHiddenFiles(true).
PromptForSingleSelection()
if result != "" {
application.InfoDialog().SetMessage(result).Show()
app.Dialog.Info().SetMessage(result).Show()
} else {
application.InfoDialog().SetMessage("No file selected").Show()
app.Dialog.Info().SetMessage("No file selected").Show()
}
})
openMenu.Add("Open File (Attach to window)").OnClick(func(ctx *application.Context) {
result, _ := application.OpenFileDialog().
result, _ := app.Dialog.OpenFile().
CanChooseFiles(true).
CanCreateDirectories(true).
ShowHiddenFiles(true).
AttachToWindow(app.Window.Current()).
PromptForSingleSelection()
if result != "" {
application.InfoDialog().SetMessage(result).Show()
app.Dialog.Info().SetMessage(result).Show()
} else {
application.InfoDialog().SetMessage("No file selected").Show()
app.Dialog.Info().SetMessage("No file selected").Show()
}
})
openMenu.Add("Open Multiple Files (Show Hidden Files)").OnClick(func(ctx *application.Context) {
result, _ := application.OpenFileDialog().
result, _ := app.Dialog.OpenFile().
CanChooseFiles(true).
CanCreateDirectories(true).
ShowHiddenFiles(true).
PromptForMultipleSelection()
if len(result) > 0 {
application.InfoDialog().SetMessage(strings.Join(result, ",")).Show()
app.Dialog.Info().SetMessage(strings.Join(result, ",")).Show()
} else {
application.InfoDialog().SetMessage("No file selected").Show()
app.Dialog.Info().SetMessage("No file selected").Show()
}
})
openMenu.Add("Open Directory").OnClick(func(ctx *application.Context) {
result, _ := application.OpenFileDialog().
result, _ := app.Dialog.OpenFile().
CanChooseDirectories(true).
CanChooseFiles(false).
PromptForSingleSelection()
if result != "" {
application.InfoDialog().SetMessage(result).Show()
app.Dialog.Info().SetMessage(result).Show()
} else {
application.InfoDialog().SetMessage("No directory selected").Show()
app.Dialog.Info().SetMessage("No directory selected").Show()
}
})
openMenu.Add("Open Directory (Create Directories)").OnClick(func(ctx *application.Context) {
result, _ := application.OpenFileDialog().
result, _ := app.Dialog.OpenFile().
CanChooseDirectories(true).
CanCreateDirectories(true).
CanChooseFiles(false).
PromptForSingleSelection()
if result != "" {
application.InfoDialog().SetMessage(result).Show()
app.Dialog.Info().SetMessage(result).Show()
} else {
application.InfoDialog().SetMessage("No directory selected").Show()
app.Dialog.Info().SetMessage("No directory selected").Show()
}
})
openMenu.Add("Open Directory (Resolves Aliases)").OnClick(func(ctx *application.Context) {
result, _ := application.OpenFileDialog().
result, _ := app.Dialog.OpenFile().
CanChooseDirectories(true).
CanCreateDirectories(true).
CanChooseFiles(false).
ResolvesAliases(true).
PromptForSingleSelection()
if result != "" {
application.InfoDialog().SetMessage(result).Show()
app.Dialog.Info().SetMessage(result).Show()
} else {
application.InfoDialog().SetMessage("No directory selected").Show()
app.Dialog.Info().SetMessage("No directory selected").Show()
}
})
openMenu.Add("Open File/Directory (Set Title)").OnClick(func(ctx *application.Context) {
dialog := application.OpenFileDialog().
dialog := app.Dialog.OpenFile().
CanChooseDirectories(true).
CanCreateDirectories(true).
ResolvesAliases(true)
@ -265,14 +265,14 @@ func main() {
result, _ := dialog.PromptForSingleSelection()
if result != "" {
application.InfoDialog().SetMessage(result).Show()
app.Dialog.Info().SetMessage(result).Show()
} else {
application.InfoDialog().SetMessage("No file/directory selected").Show()
app.Dialog.Info().SetMessage("No file/directory selected").Show()
}
})
openMenu.Add("Open (Full Example)").OnClick(func(ctx *application.Context) {
cwd, _ := os.Getwd()
dialog := application.OpenFileDialog().
dialog := app.Dialog.OpenFile().
SetTitle("Select a file").
SetMessage("Select a file to open").
SetButtonText("Let's do this!").
@ -294,46 +294,46 @@ func main() {
result, _ := dialog.PromptForSingleSelection()
if result != "" {
application.InfoDialog().SetMessage(result).Show()
app.Dialog.Info().SetMessage(result).Show()
} else {
application.InfoDialog().SetMessage("No file selected").Show()
app.Dialog.Info().SetMessage("No file selected").Show()
}
})
saveMenu := menu.AddSubmenu("Save")
saveMenu.Add("Select File (Defaults)").OnClick(func(ctx *application.Context) {
result, _ := application.SaveFileDialog().
result, _ := app.Dialog.SaveFile().
PromptForSingleSelection()
if result != "" {
application.InfoDialog().SetMessage(result).Show()
app.Dialog.Info().SetMessage(result).Show()
}
})
saveMenu.Add("Select File (Attach To WebviewWindow)").OnClick(func(ctx *application.Context) {
result, _ := application.SaveFileDialog().
result, _ := app.Dialog.SaveFile().
AttachToWindow(app.Window.Current()).
PromptForSingleSelection()
if result != "" {
application.InfoDialog().SetMessage(result).Show()
app.Dialog.Info().SetMessage(result).Show()
}
})
saveMenu.Add("Select File (Show Hidden Files)").OnClick(func(ctx *application.Context) {
result, _ := application.SaveFileDialog().
result, _ := app.Dialog.SaveFile().
ShowHiddenFiles(true).
PromptForSingleSelection()
if result != "" {
application.InfoDialog().SetMessage(result).Show()
app.Dialog.Info().SetMessage(result).Show()
}
})
saveMenu.Add("Select File (Cannot Create Directories)").OnClick(func(ctx *application.Context) {
result, _ := application.SaveFileDialog().
result, _ := app.Dialog.SaveFile().
CanCreateDirectories(false).
PromptForSingleSelection()
if result != "" {
application.InfoDialog().SetMessage(result).Show()
app.Dialog.Info().SetMessage(result).Show()
}
})
saveMenu.Add("Select File (Full Example)").OnClick(func(ctx *application.Context) {
result, _ := application.SaveFileDialog().
result, _ := app.Dialog.SaveFile().
CanCreateDirectories(false).
ShowHiddenFiles(true).
SetMessage("Select a file").
@ -346,7 +346,7 @@ func main() {
ShowHiddenFiles(true).
PromptForSingleSelection()
if result != "" {
application.InfoDialog().SetMessage(result).Show()
app.Dialog.Info().SetMessage(result).Show()
}
})

View file

@ -6,8 +6,10 @@ import (
"log"
)
var app *application.App
func main() {
app := application.New(application.Options{
app = application.New(application.Options{
Name: "Key Bindings Demo",
Description: "A demo of the Key Bindings Options",
Mac: application.MacOptions{
@ -15,7 +17,7 @@ func main() {
},
KeyBindings: map[string]func(window application.Window){
"shift+ctrl+c": func(window application.Window) {
selection, err := application.OpenFileDialog().
selection, err := app.Dialog.OpenFile().
CanChooseFiles(true).
CanCreateDirectories(true).
ShowHiddenFiles(true).

View file

@ -65,7 +65,7 @@ func main() {
})
window.OnWindowEvent(events.Common.WindowShow, func(event *application.WindowEvent) {
application.InfoDialog().
app.Dialog.Info().
SetTitle("File Opened").
SetMessage("Application opened with file: " + filename).
Show()

View file

@ -3,6 +3,7 @@ package main
import (
_ "embed"
"encoding/base64"
"fmt"
"log"
"runtime"
"strings"
@ -25,10 +26,11 @@ func main() {
// Check if running on macOS
if runtime.GOOS != "darwin" {
// Show dialog for non-macOS platforms
application.InfoDialog().
app.Dialog.Info().
SetTitle("macOS Only Demo").
SetMessage("The Liquid Glass effect is a macOS-specific feature that uses native NSGlassEffectView (macOS 15.0+) or NSVisualEffectView.\n\nThis demo is not available on " + runtime.GOOS + ".").
Show()
fmt.Println("The Liquid Glass effect is a macOS-specific feature. This demo is not available on", runtime.GOOS)
return
}

View file

@ -10,6 +10,8 @@ import (
//go:embed assets/*
var assets embed.FS
var app *application.App
type WindowService struct{}
func (s *WindowService) GeneratePanic() {
@ -27,7 +29,7 @@ func (s *WindowService) call2() {
// ==============================================
func main() {
app := application.New(application.Options{
app = application.New(application.Options{
Name: "Panic Handler Demo",
Description: "A demo of Handling Panics",
Assets: application.AssetOptions{
@ -44,7 +46,7 @@ func main() {
fmt.Printf("Time: %s\n", panicDetails.Time)
fmt.Printf("Error: %s\n", panicDetails.Error)
fmt.Printf("Stacktrace: %s\n", panicDetails.StackTrace)
application.InfoDialog().SetMessage("There was a panic!").Show()
app.Dialog.Info().SetMessage("There was a panic!").Show()
},
})

View file

@ -59,7 +59,7 @@ func main() {
myMenu.Add("Hello World!").OnClick(func(ctx *application.Context) {
println("Hello World!")
q := application.QuestionDialog().SetTitle("Ready?").SetMessage("Are you feeling ready?")
q := app.Dialog.Question().SetTitle("Ready?").SetMessage("Are you feeling ready?")
q.AddButton("Yes").OnClick(func() {
println("Awesome!")
})
@ -75,7 +75,7 @@ func main() {
myMenu.AddSeparator()
myMenu.AddCheckbox("Checked", true).OnClick(func(ctx *application.Context) {
println("Checked: ", ctx.ClickedMenuItem().Checked())
application.InfoDialog().SetTitle("Hello World!").SetMessage("Hello World!").Show()
app.Dialog.Info().SetTitle("Hello World!").SetMessage("Hello World!").Show()
})
myMenu.Add("Enabled").OnClick(func(ctx *application.Context) {
println("Click me!")

View file

@ -512,7 +512,7 @@ func main() {
sizeMenu.Add("Get Current WebviewWindow Size").OnClick(func(ctx *application.Context) {
currentWindow(func(w application.Window) {
width, height := w.Size()
application.InfoDialog().SetTitle("Current WebviewWindow Size").SetMessage("Width: " + strconv.Itoa(width) + " Height: " + strconv.Itoa(height)).Show()
app.Dialog.Info().SetTitle("Current WebviewWindow Size").SetMessage("Width: " + strconv.Itoa(width) + " Height: " + strconv.Itoa(height)).Show()
})
})
@ -544,7 +544,7 @@ func main() {
positionMenu.Add("Get Position").OnClick(func(ctx *application.Context) {
currentWindow(func(w application.Window) {
x, y := w.Position()
application.InfoDialog().SetTitle("Current WebviewWindow Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
app.Dialog.Info().SetTitle("Current WebviewWindow Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
})
})
@ -568,7 +568,7 @@ func main() {
positionMenu.Add("Get Relative Position").OnClick(func(ctx *application.Context) {
currentWindow(func(w application.Window) {
x, y := w.RelativePosition()
application.InfoDialog().SetTitle("Current WebviewWindow Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
app.Dialog.Info().SetTitle("Current WebviewWindow Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
})
})
@ -681,24 +681,24 @@ func main() {
stateMenu.Add("Get Primary Screen").OnClick(func(ctx *application.Context) {
screen := app.Screen.GetPrimary()
msg := fmt.Sprintf("Screen: %+v", screen)
application.InfoDialog().SetTitle("Primary Screen").SetMessage(msg).Show()
app.Dialog.Info().SetTitle("Primary Screen").SetMessage(msg).Show()
})
stateMenu.Add("Get Screens").OnClick(func(ctx *application.Context) {
screens := app.Screen.GetAll()
for _, screen := range screens {
msg := fmt.Sprintf("Screen: %+v", screen)
application.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
app.Dialog.Info().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
}
})
stateMenu.Add("Get Screen for WebviewWindow").OnClick(func(ctx *application.Context) {
currentWindow(func(w application.Window) {
screen, err := w.GetScreen()
if err != nil {
application.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
app.Dialog.Error().SetTitle("Error").SetMessage(err.Error()).Show()
return
}
msg := fmt.Sprintf("Screen: %+v", screen)
application.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
app.Dialog.Info().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
})
})
stateMenu.Add("Disable for 5s").OnClick(func(ctx *application.Context) {

View file

@ -225,7 +225,7 @@ func main() {
sizeMenu.Add("Get Current WebviewWindow Size").OnClick(func(ctx *application.Context) {
currentWindow(func(w application.Window) {
width, height := w.Size()
application.InfoDialog().SetTitle("Current WebviewWindow Size").SetMessage("Width: " + strconv.Itoa(width) + " Height: " + strconv.Itoa(height)).Show()
app.Dialog.Info().SetTitle("Current WebviewWindow Size").SetMessage("Width: " + strconv.Itoa(width) + " Height: " + strconv.Itoa(height)).Show()
})
})
@ -256,7 +256,7 @@ func main() {
positionMenu.Add("Get Relative Position").OnClick(func(ctx *application.Context) {
currentWindow(func(w application.Window) {
x, y := w.RelativePosition()
application.InfoDialog().SetTitle("Current WebviewWindow Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
app.Dialog.Info().SetTitle("Current WebviewWindow Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
})
})
@ -275,7 +275,7 @@ func main() {
positionMenu.Add("Get Position").OnClick(func(ctx *application.Context) {
currentWindow(func(w application.Window) {
x, y := w.Position()
application.InfoDialog().SetTitle("Current WebviewWindow Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
app.Dialog.Info().SetTitle("Current WebviewWindow Position").SetMessage("X: " + strconv.Itoa(x) + " Y: " + strconv.Itoa(y)).Show()
})
})
@ -342,24 +342,24 @@ func main() {
stateMenu.Add("Get Primary Screen").OnClick(func(ctx *application.Context) {
screen := app.Screen.GetPrimary()
msg := fmt.Sprintf("Screen: %+v", screen)
application.InfoDialog().SetTitle("Primary Screen").SetMessage(msg).Show()
app.Dialog.Info().SetTitle("Primary Screen").SetMessage(msg).Show()
})
stateMenu.Add("Get Screens").OnClick(func(ctx *application.Context) {
screens := app.Screen.GetAll()
for _, screen := range screens {
msg := fmt.Sprintf("Screen: %+v", screen)
application.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
app.Dialog.Info().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
}
})
stateMenu.Add("Get Screen for WebviewWindow").OnClick(func(ctx *application.Context) {
currentWindow(func(w application.Window) {
screen, err := w.GetScreen()
if err != nil {
application.ErrorDialog().SetTitle("Error").SetMessage(err.Error()).Show()
app.Dialog.Error().SetTitle("Error").SetMessage(err.Error()).Show()
return
}
msg := fmt.Sprintf("Screen: %+v", screen)
application.InfoDialog().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
app.Dialog.Info().SetTitle(fmt.Sprintf("Screen %s", screen.ID)).SetMessage(msg).Show()
})
})
stateMenu.Add("Disable for 5s").OnClick(func(ctx *application.Context) {

View file

@ -16,6 +16,9 @@ func getInfo() (map[string]string, bool) {
// Check session type (X11/Wayland)
result["XDG_SESSION_TYPE"] = getSessionType()
// Check desktop environment
result["Desktop Environment"] = getDesktopEnvironment()
// Check for NVIDIA driver
result["NVIDIA Driver"] = getNvidiaDriverInfo()
@ -30,6 +33,14 @@ func getSessionType() string {
return sessionType
}
func getDesktopEnvironment() string {
de := os.Getenv("XDG_CURRENT_DESKTOP")
if de == "" {
return "unset"
}
return de
}
func getNvidiaDriverInfo() string {
version, err := os.ReadFile("/sys/module/nvidia/version")
if err != nil {

View file

@ -1 +1 @@
v3.0.0-alpha.46
v3.0.0-alpha.48

View file

@ -863,30 +863,6 @@ func (a *App) SetIcon(icon []byte) {
}
}
func InfoDialog() *MessageDialog {
return newMessageDialog(InfoDialogType)
}
func QuestionDialog() *MessageDialog {
return newMessageDialog(QuestionDialogType)
}
func WarningDialog() *MessageDialog {
return newMessageDialog(WarningDialogType)
}
func ErrorDialog() *MessageDialog {
return newMessageDialog(ErrorDialogType)
}
func OpenFileDialog() *OpenFileDialogStruct {
return newOpenFileDialog()
}
func SaveFileDialog() *SaveFileDialogStruct {
return newSaveFileDialog()
}
func (a *App) dispatchOnMainThread(fn func()) {
// If we are on the main thread, just call the function
if a.impl.isOnMainThread() {

View file

@ -14,44 +14,44 @@ func newDialogManager(app *App) *DialogManager {
// OpenFile creates a file dialog for selecting files
func (dm *DialogManager) OpenFile() *OpenFileDialogStruct {
return OpenFileDialog()
return newOpenFileDialog()
}
// OpenFileWithOptions creates a file dialog with options
func (dm *DialogManager) OpenFileWithOptions(options *OpenFileDialogOptions) *OpenFileDialogStruct {
result := OpenFileDialog()
result := newOpenFileDialog()
result.SetOptions(options)
return result
}
// SaveFile creates a save file dialog
func (dm *DialogManager) SaveFile() *SaveFileDialogStruct {
return SaveFileDialog()
return newSaveFileDialog()
}
// SaveFileWithOptions creates a save file dialog with options
func (dm *DialogManager) SaveFileWithOptions(options *SaveFileDialogOptions) *SaveFileDialogStruct {
result := SaveFileDialog()
result := newSaveFileDialog()
result.SetOptions(options)
return result
}
// Info creates an information dialog
func (dm *DialogManager) Info() *MessageDialog {
return InfoDialog()
return newMessageDialog(InfoDialogType)
}
// Question creates a question dialog
func (dm *DialogManager) Question() *MessageDialog {
return QuestionDialog()
return newMessageDialog(QuestionDialogType)
}
// Warning creates a warning dialog
func (dm *DialogManager) Warning() *MessageDialog {
return WarningDialog()
return newMessageDialog(WarningDialogType)
}
// Error creates an error dialog
func (dm *DialogManager) Error() *MessageDialog {
return ErrorDialog()
return newMessageDialog(ErrorDialogType)
}

View file

@ -43,13 +43,13 @@ func (m *MessageProcessor) processDialogMethod(req *RuntimeRequest, window Windo
var dialog *MessageDialog
switch req.Method {
case DialogInfo:
dialog = InfoDialog()
dialog = newMessageDialog(InfoDialogType)
case DialogWarning:
dialog = WarningDialog()
dialog = newMessageDialog(WarningDialogType)
case DialogError:
dialog = ErrorDialog()
dialog = newMessageDialog(ErrorDialogType)
case DialogQuestion:
dialog = QuestionDialog()
dialog = newMessageDialog(QuestionDialogType)
}
var detached = args.Bool("Detached")
if detached == nil || !*detached {

View file

@ -18,3 +18,16 @@ func windowShouldUnconditionallyClose(windowId C.uint) C.bool {
globalApplication.debug("windowShouldUnconditionallyClose check", "windowId", windowId, "unconditionallyClose", unconditionallyClose)
return C.bool(unconditionallyClose)
}
//export windowIsHidden
func windowIsHidden(windowId C.uint) C.bool {
window, _ := globalApplication.Window.GetByID(uint(windowId))
if window == nil {
return C.bool(false)
}
webviewWindow, ok := window.(*WebviewWindow)
if !ok {
return C.bool(false)
}
return C.bool(webviewWindow.options.Hidden)
}

View file

@ -887,8 +887,11 @@ func (w *macosWebviewWindow) focus() {
}
func (w *macosWebviewWindow) openContextMenu(menu *Menu, data *ContextMenuData) {
// Create the menu
thisMenu := newMenuImpl(menu)
// Reuse existing impl if available, otherwise create new one
if menu.impl == nil {
menu.impl = newMenuImpl(menu)
}
thisMenu := menu.impl.(*macosMenu)
thisMenu.update()
C.windowShowMenu(w.nsWindow, thisMenu.nsMenu, C.int(data.X), C.int(data.Y))
}

View file

@ -10,6 +10,7 @@ extern void processDragItems(unsigned int windowId, char** arr, int length, int
extern void processWindowKeyDownEvent(unsigned int, const char*);
extern bool hasListeners(unsigned int);
extern bool windowShouldUnconditionallyClose(unsigned int);
extern bool windowIsHidden(unsigned int);
// Define custom glass effect style constants (these match the Go constants)
typedef NS_ENUM(NSInteger, MacLiquidGlassStyle) {
LiquidGlassStyleAutomatic = 0,
@ -319,6 +320,13 @@ typedef NS_ENUM(NSInteger, MacLiquidGlassStyle) {
if (windowShouldUnconditionallyClose(delegate.windowId)) {
return true;
}
// If the window is hidden (via Hide()), don't trigger close events.
// This fixes issue #4389 where hiding the last window with
// ApplicationShouldTerminateAfterLastWindowClosed=true would incorrectly
// trigger the close event sequence and destroy the window.
if (windowIsHidden(delegate.windowId)) {
return false;
}
// For user-initiated closes, emit WindowClosing event and let the application decide
processWindowEvent(delegate.windowId, EventWindowShouldClose);
return false;

View file

@ -1175,7 +1175,11 @@ func newWindowImpl(parent *WebviewWindow) *windowsWebviewWindow {
}
func (w *windowsWebviewWindow) openContextMenu(menu *Menu, _ *ContextMenuData) {
// Create the menu
// Destroy previous context menu if it exists to prevent memory leak
if w.currentlyOpenContextMenu != nil {
w.currentlyOpenContextMenu.Destroy()
}
// Create the menu from current Go-side menu state
thisMenu := NewPopupMenu(w.hwnd, menu)
thisMenu.Update()
w.currentlyOpenContextMenu = thisMenu