mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 22:55:48 +01:00
913 lines
22 KiB
Go
913 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))
|
|
}
|
|
}
|
|
|
|
// Initialize sanitizer and wrap logger
|
|
result.sanitizer = NewSanitizer(appOptions.SanitizeOptions)
|
|
if !result.sanitizer.IsDisabled() {
|
|
result.Logger = WrapLoggerWithSanitizer(result.Logger, result.sanitizer)
|
|
}
|
|
|
|
// 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.debug("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)
|
|
|
|
// DropTargetDetails contains information about the HTML element
|
|
// where files were dropped (the element with data-file-drop-target attribute).
|
|
type DropTargetDetails 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
|
|
DropTarget *DropTargetDetails
|
|
}
|
|
|
|
var windowDragAndDropBuffer = make(chan *dragAndDropMessage, 5)
|
|
|
|
func addDragAndDropMessage(windowId uint, filenames []string, dropTarget *DropTargetDetails) {
|
|
windowDragAndDropBuffer <- &dragAndDropMessage{
|
|
windowId: windowId,
|
|
filenames: filenames,
|
|
DropTarget: dropTarget,
|
|
}
|
|
}
|
|
|
|
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
|
|
sanitizer *Sanitizer
|
|
|
|
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
|
|
}
|
|
|
|
// Sanitizer returns the application's sanitizer for redacting sensitive data.
|
|
// Use this to sanitize your own data using the same rules as the application's log sanitization.
|
|
func (a *App) Sanitizer() *Sanitizer {
|
|
return a.sanitizer
|
|
}
|
|
|
|
// 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) 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
|
|
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) {
|
|
defer handlePanic()
|
|
a.windowsLock.Lock()
|
|
window, ok := a.windows[event.windowId]
|
|
a.windowsLock.Unlock()
|
|
if !ok {
|
|
a.warning("WebviewWindow #%d not found", event.windowId)
|
|
return
|
|
}
|
|
window.handleDragAndDropMessage(event.filenames, event.DropTarget)
|
|
}
|
|
|
|
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.debug("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.debug("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.debug("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.debug("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.Lock()
|
|
for _, window := range a.windows {
|
|
window.Close()
|
|
}
|
|
a.windows = nil
|
|
a.windowsLock.Unlock()
|
|
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
|
|
}
|