wails/v3/pkg/application/application.go
Lea Anthony 84b9021ea9
docs: Update dialogs documentation to match actual v3 API (#4793)
* All the documentation files have been updated. Here's a summary of the changes I made:

## Summary of Documentation Updates

### Key API Corrections Made:

1. **Dialog Creation**:
   - Changed `app.InfoDialog()` → `application.InfoDialog()` (package-level functions)
   - Also documented `app.Dialog.Info()` via DialogManager

2. **Question Dialog Buttons**:
   - Removed non-existent `SetButtons("Save", "Don't Save", "Cancel")` method
   - Now uses `AddButton("label")` which returns a `*Button`
   - Button callbacks via `button.OnClick(func() { ... })`

3. **Default/Cancel Buttons**:
   - Changed `SetDefaultButton("Cancel")` (string) → `SetDefaultButton(cancelBtn)` (`*Button`)
   - Changed `SetCancelButton("Cancel")` (string) → `SetCancelButton(cancelBtn)` (`*Button`)
   - Also documented fluent `SetAsDefault()` and `SetAsCancel()` methods on Button

4. **File Dialog Filters**:
   - Changed `SetFilters([]application.FileFilter{...})` → `AddFilter("Display Name", "*.ext;*.ext2")`

5. **Save Dialog Filename**:
   - Changed `SetDefaultFilename()` → `SetFilename()`

6. **Folder Selection**:
   - Removed non-existent `app.SelectFolderDialog()`
   - Now uses `application.OpenFileDialog().CanChooseDirectories(true).CanChooseFiles(false)`

7. **Show() Return Value**:
   - Removed incorrect `result, err := dialog.Show()` pattern
   - `Show()` returns nothing - responses are handled via button callbacks

### Files Modified:
- `docs/src/content/docs/features/dialogs/message.mdx` - Complete rewrite of question dialog examples
- `docs/src/content/docs/features/dialogs/overview.mdx` - Updated all code examples
- `docs/src/content/docs/features/dialogs/file.mdx` - Updated file dialog examples
- `v3/UNRELEASED_CHANGELOG.md` - Added changelog entry

* All changes are complete. Here's a summary of what was updated:

## Summary

I've updated the Wails v3 dialogs documentation to match the actual API. The documentation now consistently uses:

### DialogManager Pattern (Recommended)
- `app.Dialog.Info()`, `app.Dialog.Question()`, `app.Dialog.Warning()`, `app.Dialog.Error()`
- `app.Dialog.OpenFile()`, `app.Dialog.SaveFile()`

This is consistent with other runtime features like `app.Window`, `app.Event`, etc.

### Key API Corrections

1. **Dialog Creation**: Both `app.Dialog.*()` (DialogManager) and `application.*Dialog()` (package-level) are documented
2. **Question Dialog Buttons**: Use `AddButton("label")` which returns `*Button`, then `button.OnClick(callback)`
3. **Default/Cancel Buttons**: `SetDefaultButton(button)` and `SetCancelButton(button)` take `*Button` pointers
4. **File Filters**: Use `AddFilter("Display Name", "*.ext")` instead of `SetFilters()`
5. **Save Dialog Filename**: Use `SetFilename()` instead of `SetDefaultFilename()`
6. **Folder Selection**: Use `app.Dialog.OpenFile().CanChooseDirectories(true).CanChooseFiles(false)`

### Files Modified
- `docs/src/content/docs/features/dialogs/overview.mdx`
- `docs/src/content/docs/features/dialogs/message.mdx`
- `docs/src/content/docs/features/dialogs/file.mdx`
- `v3/UNRELEASED_CHANGELOG.md`

* Audit complete! All documentation files now consistently use `app.Dialog.*` for all code examples. Here's the summary:

## Audit Complete

All dialog documentation has been updated to consistently use the `app.Dialog.*` pattern:

### Changes Made:

**message.mdx:**
- All examples now use `app.Dialog.Info()`, `app.Dialog.Question()`, `app.Dialog.Warning()`, `app.Dialog.Error()`
- Function signatures updated to accept `app *application.App` parameter
- Nested dialog calls within callbacks also updated

**file.mdx:**
- All examples now use `app.Dialog.OpenFile()`, `app.Dialog.SaveFile()`
- Function signatures updated to accept `app *application.App` parameter
- All error/info dialogs within file operations also updated

**overview.mdx:**
- Already consistent from earlier edits

### Documentation Structure:
Each file now properly documents both approaches in a dedicated section:

```go
// Via DialogManager (recommended when you have an app instance):
app.Dialog.Info()
app.Dialog.OpenFile()

// Package-level functions (useful in callbacks or when app isn't in scope):
application.InfoDialog()
application.OpenFileDialog()
```

But all code examples use the `app.Dialog.*` pattern for consistency with other runtime features like `app.Window`, `app.Event`, etc.

* docs: Fix reference/dialogs.mdx and reference/application.mdx API documentation

Updated docs to match actual Wails v3 Dialogs API:

- reference/dialogs.mdx: Complete rewrite with correct API
  - Use `app.Dialog.OpenFile()` and `app.Dialog.SaveFile()` instead of `app.OpenFileDialog()`
  - Use `AddFilter("name", "pattern")` instead of `SetFilters([]FileFilter{...})`
  - Use `SetFilename()` instead of `SetDefaultFilename()`
  - Use `SetDirectory()` instead of `SetDefaultDirectory()`
  - Remove non-existent `SelectFolderDialog()` - use `OpenFile().CanChooseDirectories(true).CanChooseFiles(false)`
  - Use `AddButton()` with callbacks instead of `SetButtons()`
  - Use `SetDefaultButton(*Button)` instead of `SetDefaultButton(int)`
  - Document that `Show()` returns nothing, use callbacks

- reference/application.mdx: Fix Dialog Methods section
  - Use `app.Dialog.*` manager pattern
  - Show correct Question dialog with button callbacks
  - Fix file dialog examples with `AddFilter()`
  - Remove `SelectFolderDialog()` reference

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: Remove package-level dialog function references

Remove all references to package-level dialog functions
(application.InfoDialog(), application.OpenFileDialog(), etc.)
from documentation. Only the app.Dialog manager pattern
should be used.

Updated files:
- reference/dialogs.mdx
- features/dialogs/overview.mdx
- features/dialogs/message.mdx
- features/dialogs/file.mdx

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: Remove package-level dialog functions in favor of app.Dialog manager

BREAKING CHANGE: Remove package-level dialog functions. Use app.Dialog manager instead.

Removed functions:
- application.InfoDialog()
- application.QuestionDialog()
- application.WarningDialog()
- application.ErrorDialog()
- application.OpenFileDialog()
- application.SaveFileDialog()

Use the Dialog manager pattern instead:
- app.Dialog.Info()
- app.Dialog.Question()
- app.Dialog.Warning()
- app.Dialog.Error()
- app.Dialog.OpenFile()
- app.Dialog.SaveFile()

This aligns dialogs with other runtime managers like app.Window and app.Event.

Updated files:
- v3/pkg/application/application.go - Remove exported dialog functions
- v3/pkg/application/dialog_manager.go - Use internal newMessageDialog/newOpenFileDialog
- v3/pkg/application/messageprocessor_dialog.go - Use internal dialog constructors
- v3/examples/* - Update all examples to use app.Dialog pattern
- v3/internal/commands/appimage_testfiles/main.go - Update test file
- v3/UNRELEASED_CHANGELOG.md - Document breaking change

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Use application.Get() in dialogs-basic example and correct filter docs

- Update dialogs-basic helper functions to use application.Get() instead
  of passing app through function parameters
- Fix incorrect documentation claiming space/comma delimiters work for
  filter patterns (only semicolons are supported)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 06:05:40 +11:00

927 lines
22 KiB
Go

package application
import (
"context"
"embed"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"github.com/wailsapp/wails/v3/internal/assetserver"
"github.com/wailsapp/wails/v3/internal/assetserver/bundledassets"
"github.com/wailsapp/wails/v3/internal/assetserver/webview"
"github.com/wailsapp/wails/v3/internal/capabilities"
)
//go:embed assets/*
var alphaAssets embed.FS
var globalApplication *App
// AlphaAssets is the default assets for the alpha application
var AlphaAssets = AssetOptions{
Handler: BundledAssetFileServer(alphaAssets),
}
type EventListener struct {
callback func(app *ApplicationEvent)
}
func Get() *App {
return globalApplication
}
func New(appOptions Options) *App {
if globalApplication != nil {
return globalApplication
}
mergeApplicationDefaults(&appOptions)
result := newApplication(appOptions)
globalApplication = result
fatalHandler(result.handleFatalError)
if result.Logger == nil {
if result.isDebugMode {
result.Logger = DefaultLogger(result.options.LogLevel)
} else {
result.Logger = slog.New(slog.NewTextHandler(io.Discard, nil))
}
}
// Set up signal handling (platform-specific)
result.setupSignalHandler(appOptions)
result.logStartup()
result.logPlatformInfo()
result.customEventProcessor = NewWailsEventProcessor(result.Event.dispatch)
messageProc := NewMessageProcessor(result.Logger)
// Initialize transport (default to HTTP if not specified)
transport := appOptions.Transport
if transport == nil {
transport = NewHTTPTransport(HTTPTransportWithLogger(result.Logger))
}
err := transport.Start(result.ctx, messageProc)
if err != nil {
result.fatal("failed to start custom transport: %w", err)
}
// Register shutdown task to stop transport
result.OnShutdown(func() {
if err := transport.Stop(); err != nil {
result.error("failed to stop custom transport: %w", err)
}
})
// Auto-wire events if transport supports event delivery
if eventTransport, ok := transport.(WailsEventListener); ok {
result.wailsEventListeners = append(result.wailsEventListeners, eventTransport)
} else {
// otherwise fallback to IPC
result.wailsEventListeners = append(result.wailsEventListeners, &EventIPCTransport{
app: result,
})
}
middlewares := []assetserver.Middleware{
func(next http.Handler) http.Handler {
if m := appOptions.Assets.Middleware; m != nil {
return m(next)
}
return next
},
func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
path := req.URL.Path
switch path {
case "/wails/runtime.js":
err := assetserver.ServeFile(rw, path, bundledassets.RuntimeJS)
if err != nil {
result.fatal("unable to serve runtime.js: %w", err)
}
case "/wails/transport.js":
err := assetserver.ServeFile(rw, path, transport.JSClient())
if err != nil {
result.fatal("unable to serve transport.js: %w", err)
}
default:
next.ServeHTTP(rw, req)
}
})
},
}
if handler, ok := transport.(TransportHTTPHandler); ok {
middlewares = append(middlewares, handler.Handler())
}
opts := &assetserver.Options{
Handler: appOptions.Assets.Handler,
Middleware: assetserver.ChainMiddleware(middlewares...),
Logger: result.Logger,
}
if appOptions.Assets.DisableLogging {
opts.Logger = slog.New(slog.NewTextHandler(io.Discard, nil))
}
srv, err := assetserver.NewAssetServer(opts)
if err != nil {
result.fatal("application initialisation failed: %w", err)
}
result.assets = srv
result.assets.LogDetails()
// If transport implements AssetServerTransport, configure it to serve assets
if assetTransport, ok := transport.(AssetServerTransport); ok {
err := assetTransport.ServeAssets(srv)
if err != nil {
result.fatal("failed to configure transport for serving assets: %w", err)
}
result.info("Transport configured to serve assets")
}
result.bindings = NewBindings(appOptions.MarshalError, appOptions.BindAliases)
result.options.Services = slices.Clone(appOptions.Services)
// Process keybindings
if result.options.KeyBindings != nil {
result.keyBindings = processKeyBindingOptions(result.options.KeyBindings)
}
if appOptions.OnShutdown != nil {
result.OnShutdown(appOptions.OnShutdown)
}
// Initialize single instance manager if enabled
if appOptions.SingleInstance != nil {
manager, err := newSingleInstanceManager(result, appOptions.SingleInstance)
if err != nil {
if errors.Is(err, alreadyRunningError) && manager != nil {
err = manager.notifyFirstInstance()
if err != nil {
globalApplication.error("failed to notify first instance: %w", err)
}
os.Exit(appOptions.SingleInstance.ExitCode)
}
result.fatal("failed to initialize single instance manager: %w", err)
} else {
result.singleInstanceManager = manager
}
}
return result
}
func mergeApplicationDefaults(o *Options) {
if o.Name == "" {
o.Name = "My Wails Application"
}
if o.Description == "" {
o.Description = "An application written using Wails"
}
if o.Windows.WndClass == "" {
o.Windows.WndClass = "WailsWebviewWindow"
}
}
type (
platformApp interface {
run() error
destroy()
setApplicationMenu(menu *Menu)
name() string
getCurrentWindowID() uint
showAboutDialog(name string, description string, icon []byte)
setIcon(icon []byte)
on(id uint)
dispatchOnMainThread(id uint)
hide()
show()
getPrimaryScreen() (*Screen, error)
getScreens() ([]*Screen, error)
GetFlags(options Options) map[string]any
isOnMainThread() bool
isDarkMode() bool
getAccentColor() string
}
runnable interface {
Run()
}
)
// Messages sent from javascript get routed here
type windowMessage struct {
windowId uint
message string
originInfo *OriginInfo
}
type OriginInfo struct {
Origin string
TopOrigin string
IsMainFrame bool
}
var windowMessageBuffer = make(chan *windowMessage, 5)
// DropZoneDetails contains information about the HTML element
// at the location of a file drop.
type DropZoneDetails struct {
X int `json:"x"`
Y int `json:"y"`
ElementID string `json:"id"`
ClassList []string `json:"classList"`
Attributes map[string]string `json:"attributes,omitempty"`
}
type dragAndDropMessage struct {
windowId uint
filenames []string
X int
Y int
DropZone *DropZoneDetails
}
var windowDragAndDropBuffer = make(chan *dragAndDropMessage, 5)
func addDragAndDropMessage(windowId uint, filenames []string, dropZone *DropZoneDetails) {
windowDragAndDropBuffer <- &dragAndDropMessage{
windowId: windowId,
filenames: filenames,
DropZone: dropZone,
}
}
var _ webview.Request = &webViewAssetRequest{}
const webViewRequestHeaderWindowId = "x-wails-window-id"
const webViewRequestHeaderWindowName = "x-wails-window-name"
type webViewAssetRequest struct {
Request webview.Request
windowId uint
windowName string
}
var windowKeyEvents = make(chan *windowKeyEvent, 5)
type windowKeyEvent struct {
windowId uint
acceleratorString string
}
func (r *webViewAssetRequest) URL() (string, error) {
return r.Request.URL()
}
func (r *webViewAssetRequest) Method() (string, error) {
return r.Request.Method()
}
func (r *webViewAssetRequest) Header() (http.Header, error) {
h, err := r.Request.Header()
if err != nil {
return nil, err
}
hh := h.Clone()
hh.Set(webViewRequestHeaderWindowId, strconv.FormatUint(uint64(r.windowId), 10))
if r.windowName != "" {
hh.Set(webViewRequestHeaderWindowName, r.windowName)
}
return hh, nil
}
func (r *webViewAssetRequest) Body() (io.ReadCloser, error) {
return r.Request.Body()
}
func (r *webViewAssetRequest) Response() webview.ResponseWriter {
return r.Request.Response()
}
func (r *webViewAssetRequest) Close() error {
return r.Request.Close()
}
var webviewRequests = make(chan *webViewAssetRequest, 5)
type eventHook struct {
callback func(event *ApplicationEvent)
}
type App struct {
ctx context.Context
cancel context.CancelFunc
options Options
applicationEventListeners map[uint][]*EventListener
applicationEventListenersLock sync.RWMutex
applicationEventHooks map[uint][]*eventHook
applicationEventHooksLock sync.RWMutex
// Manager pattern for organized API
Window *WindowManager
ContextMenu *ContextMenuManager
KeyBinding *KeyBindingManager
Browser *BrowserManager
Env *EnvironmentManager
Dialog *DialogManager
Event *EventManager
Menu *MenuManager
Screen *ScreenManager
Clipboard *ClipboardManager
SystemTray *SystemTrayManager
// Windows
windows map[uint]Window
windowsLock sync.RWMutex
// System Trays
systemTrays map[uint]*SystemTray
systemTraysLock sync.Mutex
systemTrayID uint
systemTrayIDLock sync.RWMutex
// MenuItems
menuItems map[uint]*MenuItem
menuItemsLock sync.Mutex
// Starting and running
starting bool
running bool
runLock sync.Mutex
pendingRun []runnable
bindings *Bindings
// platform app
impl platformApp
// The main application menu (private - use app.Menu.GetApplicationMenu/SetApplicationMenu)
applicationMenu *Menu
clipboard *Clipboard
customEventProcessor *EventProcessor
Logger *slog.Logger
contextMenus map[string]*ContextMenu
contextMenusLock sync.RWMutex
assets *assetserver.AssetServer
startURL string
// Hooks
windowCreatedCallbacks []func(window Window)
pid int
// Capabilities
capabilities capabilities.Capabilities
isDebugMode bool
// Keybindings
keyBindings map[string]func(window Window)
keyBindingsLock sync.RWMutex
// Shutdown
performingShutdown bool
shutdownLock sync.Mutex
serviceShutdownLock sync.Mutex
// Shutdown tasks are run when the application is shutting down.
// They are run in the order they are added and run on the main thread.
// The application option `OnShutdown` is run first.
shutdownTasks []func()
// Platform-specific fields (includes signal handler on desktop)
platformSignalHandler
// Wails ApplicationEvent Listener related
wailsEventListenerLock sync.Mutex
wailsEventListeners []WailsEventListener
// singleInstanceManager handles single instance functionality
singleInstanceManager *singleInstanceManager
}
func (a *App) Config() Options {
return a.options
}
// Context returns the application context that is canceled when the application shuts down.
// This context should be used for graceful shutdown of goroutines and long-running operations.
func (a *App) Context() context.Context {
return a.ctx
}
func (a *App) handleWarning(msg string) {
if a.options.WarningHandler != nil {
a.options.WarningHandler(msg)
} else {
a.Logger.Warn(msg)
}
}
func (a *App) handleError(err error) {
if a.options.ErrorHandler != nil {
a.options.ErrorHandler(err)
} else {
a.Logger.Error(err.Error())
}
}
// RegisterService appends the given service to the list of bound services.
// Registered services will be bound and initialised
// in registration order upon calling [App.Run].
//
// RegisterService will log an error message
// and discard the given service
// if called after [App.Run].
func (a *App) RegisterService(service Service) {
a.runLock.Lock()
defer a.runLock.Unlock()
if a.starting || a.running {
a.error(
"services must be registered before running the application. Service '%s' will not be registered.",
getServiceName(service),
)
return
}
a.options.Services = append(a.options.Services, service)
}
func (a *App) handleFatalError(err error) {
a.handleError(&FatalError{err: err})
os.Exit(1)
}
func (a *App) init() {
a.ctx, a.cancel = context.WithCancel(context.Background())
a.applicationEventHooks = make(map[uint][]*eventHook)
a.applicationEventListeners = make(map[uint][]*EventListener)
a.windows = make(map[uint]Window)
a.systemTrays = make(map[uint]*SystemTray)
a.contextMenus = make(map[string]*ContextMenu)
a.keyBindings = make(map[string]func(window Window))
a.Logger = a.options.Logger
a.pid = os.Getpid()
a.wailsEventListeners = make([]WailsEventListener, 0)
// Initialize managers
a.Window = newWindowManager(a)
a.ContextMenu = newContextMenuManager(a)
a.KeyBinding = newKeyBindingManager(a)
a.Browser = newBrowserManager(a)
a.Env = newEnvironmentManager(a)
a.Dialog = newDialogManager(a)
a.Event = newEventManager(a)
a.Menu = newMenuManager(a)
a.Screen = newScreenManager(a)
a.Clipboard = newClipboardManager(a)
a.SystemTray = newSystemTrayManager(a)
}
func (a *App) Capabilities() capabilities.Capabilities {
return a.capabilities
}
//func (a *App) RegisterListener(listener WailsEventListener) {
// a.wailsEventListenerLock.Lock()
// a.wailsEventListeners = append(a.wailsEventListeners, listener)
// a.wailsEventListenerLock.Unlock()
//}
//
//func (a *App) RegisterServiceHandler(prefix string, handler http.Handler) {
// a.assets.AttachServiceHandler(prefix, handler)
//}
func (a *App) GetPID() int {
return a.pid
}
func (a *App) info(message string, args ...any) {
if a.Logger != nil {
go func() {
defer handlePanic()
a.Logger.Info(message, args...)
}()
}
}
func (a *App) debug(message string, args ...any) {
if a.Logger != nil {
go func() {
defer handlePanic()
a.Logger.Debug(message, args...)
}()
}
}
func (a *App) fatal(message string, args ...any) {
err := fmt.Errorf(message, args...)
a.handleFatalError(err)
}
func (a *App) warning(message string, args ...any) {
msg := fmt.Sprintf(message, args...)
a.handleWarning(msg)
}
func (a *App) error(message string, args ...any) {
a.handleError(fmt.Errorf(message, args...))
}
func (a *App) Run() error {
a.runLock.Lock()
// Prevent double invocations.
if a.starting || a.running {
a.runLock.Unlock()
return errors.New("application is running or a previous run has failed")
}
// Block further service registrations.
a.starting = true
a.runLock.Unlock()
// Ensure application context is cancelled in case of failures.
defer a.cancel()
// Call post-create hooks
err := a.preRun()
if err != nil {
return err
}
a.impl = newPlatformApp(a)
// Ensure services are shut down in case of failures.
defer a.shutdownServices()
// Ensure application context is canceled before service shutdown (duplicate calls don't hurt).
defer a.cancel()
// Startup services before dispatching any events.
// No need to hold the lock here because a.options.Services may only change when a.running is false.
services := a.options.Services
a.options.Services = nil
for i, service := range services {
if err := a.startupService(service); err != nil {
return fmt.Errorf("error starting service '%s': %w", getServiceName(service), err)
}
// Schedule started services for shutdown.
a.options.Services = services[:i+1]
}
go func() {
for {
event := <-applicationEvents
go a.Event.handleApplicationEvent(event)
}
}()
go func() {
for {
event := <-windowEvents
go a.handleWindowEvent(event)
}
}()
go func() {
for {
request := <-webviewRequests
go a.handleWebViewRequest(request)
}
}()
go func() {
for {
event := <-windowMessageBuffer
go a.handleWindowMessage(event)
}
}()
go func() {
for {
event := <-windowKeyEvents
go a.handleWindowKeyEvent(event)
}
}()
go func() {
for {
dragAndDropMessage := <-windowDragAndDropBuffer
a.Logger.Debug(
"[DragDropDebug] App.Run: Received message from windowDragAndDropBuffer",
"message",
fmt.Sprintf("%+v", dragAndDropMessage),
)
go a.handleDragAndDropMessage(dragAndDropMessage)
}
}()
go func() {
for {
menuItemID := <-menuItemClicked
go a.Menu.handleMenuItemClicked(menuItemID)
}
}()
a.runLock.Lock()
a.running = true
a.runLock.Unlock()
// No need to hold the lock here because
// - a.pendingRun may only change while a.running is false.
// - runnables are scheduled asynchronously anyway.
for _, pending := range a.pendingRun {
go func() {
defer handlePanic()
pending.Run()
}()
}
a.pendingRun = nil
// set the application menu
if runtime.GOOS == "darwin" {
a.impl.setApplicationMenu(a.applicationMenu)
}
if a.options.Icon != nil {
a.impl.setIcon(a.options.Icon)
}
return a.impl.run()
}
func (a *App) startupService(service Service) error {
err := a.bindings.Add(service)
if err != nil {
return fmt.Errorf("cannot bind service methods: %w", err)
}
if service.options.Route != "" {
handler, ok := service.Instance().(http.Handler)
if !ok {
handler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
http.Error(
rw,
fmt.Sprintf(
"Service '%s' does not handle HTTP requests",
getServiceName(service),
),
http.StatusServiceUnavailable,
)
})
}
a.assets.AttachServiceHandler(service.options.Route, handler)
}
if s, ok := service.instance.(ServiceStartup); ok {
a.debug("Starting up service:", "name", getServiceName(service))
return s.ServiceStartup(a.ctx, service.options)
}
return nil
}
func (a *App) shutdownServices() {
// Acquire lock to prevent double calls (defer in Run() + OnShutdown)
a.serviceShutdownLock.Lock()
defer a.serviceShutdownLock.Unlock()
// Ensure app context is cancelled first (duplicate calls don't hurt).
a.cancel()
for len(a.options.Services) > 0 {
last := len(a.options.Services) - 1
service := a.options.Services[last]
a.options.Services = a.options.Services[:last] // Prevent double shutdowns
if s, ok := service.instance.(ServiceShutdown); ok {
a.debug("Shutting down service:", "name", getServiceName(service))
if err := s.ServiceShutdown(); err != nil {
a.error("error shutting down service '%s': %w", getServiceName(service), err)
}
}
}
}
func (a *App) handleDragAndDropMessage(event *dragAndDropMessage) {
a.Logger.Debug(
"[DragDropDebug] App.handleDragAndDropMessage: Called with event",
"event",
fmt.Sprintf("%+v", event),
)
defer handlePanic()
// Get window from window map
a.windowsLock.Lock()
window, ok := a.windows[event.windowId]
a.windowsLock.Unlock()
if !ok {
a.warning("WebviewWindow #%d not found", event.windowId)
return
}
// Get callback from window
a.Logger.Debug(
"[DragDropDebug] App.handleDragAndDropMessage: Calling window.HandleDragAndDropMessage",
"windowID",
event.windowId,
)
window.HandleDragAndDropMessage(event.filenames, event.DropZone)
}
func (a *App) handleWindowMessage(event *windowMessage) {
defer handlePanic()
// Get window from window map
a.windowsLock.RLock()
window, ok := a.windows[event.windowId]
// Debug: log all window IDs
var ids []uint
for id := range a.windows {
ids = append(ids, id)
}
a.windowsLock.RUnlock()
a.info("handleWindowMessage: Looking for window", "windowId", event.windowId, "availableIDs", ids)
if !ok {
a.warning("WebviewWindow #%d not found", event.windowId)
return
}
// Check if the message starts with "wails:"
if strings.HasPrefix(event.message, "wails:") {
a.info("handleWindowMessage: Processing wails message", "message", event.message)
window.HandleMessage(event.message)
} else {
if a.options.RawMessageHandler != nil {
a.options.RawMessageHandler(window, event.message, event.originInfo)
}
}
}
func (a *App) handleWebViewRequest(request *webViewAssetRequest) {
defer handlePanic()
// Log that we're processing the request
url, _ := request.Request.URL()
a.info("handleWebViewRequest: Processing request", "url", url)
// IMPORTANT: pass the wrapper request so our injected headers (x-wails-window-id/name) are used
a.assets.ServeWebViewRequest(request)
a.info("handleWebViewRequest: Request processing complete", "url", url)
}
func (a *App) handleWindowEvent(event *windowEvent) {
defer handlePanic()
// Get window from window map
a.windowsLock.RLock()
window, ok := a.windows[event.WindowID]
a.windowsLock.RUnlock()
if !ok {
a.warning("Window #%d not found", event.WindowID)
return
}
window.HandleWindowEvent(event.EventID)
}
// OnShutdown adds a function to be run when the application is shutting down.
func (a *App) OnShutdown(f func()) {
if f == nil {
return
}
a.shutdownLock.Lock()
if !a.performingShutdown {
defer a.shutdownLock.Unlock()
a.shutdownTasks = append(a.shutdownTasks, f)
return
}
a.shutdownLock.Unlock()
InvokeAsync(f)
}
func (a *App) cleanup() {
a.shutdownLock.Lock()
if a.performingShutdown {
a.shutdownLock.Unlock()
return
}
a.cancel() // Cancel app context before running shutdown hooks.
a.performingShutdown = true
a.shutdownLock.Unlock()
// No need to hold the lock here because a.shutdownTasks
// may only change while a.performingShutdown is false.
for _, shutdownTask := range a.shutdownTasks {
InvokeSync(shutdownTask)
}
InvokeSync(func() {
a.shutdownServices()
a.windowsLock.RLock()
for _, window := range a.windows {
window.Close()
}
a.windows = nil
a.windowsLock.RUnlock()
a.systemTraysLock.Lock()
for _, systray := range a.systemTrays {
systray.destroy()
}
a.systemTrays = nil
a.systemTraysLock.Unlock()
// Cleanup single instance manager
if a.singleInstanceManager != nil {
a.singleInstanceManager.cleanup()
}
a.postQuit()
if a.options.PostShutdown != nil {
a.options.PostShutdown()
}
})
}
func (a *App) Quit() {
if a.impl != nil {
InvokeSync(a.impl.destroy)
}
}
func (a *App) SetIcon(icon []byte) {
if a.impl != nil {
a.impl.setIcon(icon)
}
}
func (a *App) dispatchOnMainThread(fn func()) {
// If we are on the main thread, just call the function
if a.impl.isOnMainThread() {
fn()
return
}
mainThreadFunctionStoreLock.Lock()
id := generateFunctionStoreID()
mainThreadFunctionStore[id] = fn
mainThreadFunctionStoreLock.Unlock()
// Call platform specific dispatch function
a.impl.dispatchOnMainThread(id)
}
func (a *App) Hide() {
if a.impl != nil {
a.impl.hide()
}
}
func (a *App) Show() {
if a.impl != nil {
a.impl.show()
}
}
func (a *App) runOrDeferToAppRun(r runnable) {
a.runLock.Lock()
if !a.running {
defer a.runLock.Unlock() // Defer unlocking for panic tolerance.
a.pendingRun = append(a.pendingRun, r)
return
}
// Unlock immediately to prevent deadlocks.
// No TOC/TOU risk here because a.running can never switch back to false.
a.runLock.Unlock()
r.Run()
}
func (a *App) handleWindowKeyEvent(event *windowKeyEvent) {
defer handlePanic()
// Get window from window map
a.windowsLock.RLock()
window, ok := a.windows[event.windowId]
a.windowsLock.RUnlock()
if !ok {
a.warning("WebviewWindow #%d not found", event.windowId)
return
}
// Get callback from window
window.HandleKeyEvent(event.acceleratorString)
}
func (a *App) shouldQuit() bool {
if a.options.ShouldQuit != nil {
return a.options.ShouldQuit()
}
return true
}