diff --git a/LICENSE b/LICENSE index ab9d7b6..5840041 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 malivvan +Copyright (c) 2024 malivvan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 3128c30..9cba45c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Pure Go WebKitGTK binding for **Linux** and **FreeBSD**. ```sh # go 1.21.5+ -go get github.com/malivvan/webkitgtk +go get github.com/malivvan/webkitgtk@latest ``` ## Example @@ -43,9 +43,24 @@ func main() { } } ``` -## Running and building +## Running / Building -Running / building the application is the same as for any other Go program, aka. just `go run` and `go build`. +Running / building defaults to debug mode outputting logs to stderr. To build in release mode use the `release` build tag. + +```sh +go build -tags release -ldflags "-s -w" -trimpath +``` + +The resulting release binary will be about ~6MB in size and cam be compressed further with [UPX](https://upx.github.io/) to about ~2.5MB. + + +## Examples + +- [echo](examples/echo/echo.go) - call go functions from javascript +- [dialog](examples/dialog/dialog.go) - application spawning different types of dialog windows +- [handle](examples/handle/handle.go) - handle requests on the app:// uri scheme to serve embedded files +- [notify](examples/notify/notify.go) - application sending different types of notifications +- [systray](examples/systray/systray.go) - example application showing how to use the systray ## Dependencies Either diff --git a/api.go b/api.go index f776282..ccd2251 100644 --- a/api.go +++ b/api.go @@ -90,14 +90,16 @@ func apiHandler(bindings map[string]apiBinding, eval func(string), log func(inte eval("webkitAPI.reject(" + string(id) + ",'api not found')") return } - reply, err := binding.call(fn, req[cur:]) - if err != nil { - log("api reject", "id", id, "error", err) - eval("webkitAPI.reject(" + string(id) + ",'" + err.Error() + "')") - return - } - log("api resolve", "id", id, "reply", reply) - eval("webkitAPI.resolve(" + id + ",'" + reply + "')") + go func() { + reply, err := binding.call(fn, req[cur:]) + if err != nil { + log("api reject", "id", id, "error", err) + eval("webkitAPI.reject(" + string(id) + ",'" + err.Error() + "')") + return + } + log("api resolve", "id", id, "reply", reply) + eval("webkitAPI.resolve(" + id + ",'" + reply + "')") + }() } } @@ -117,7 +119,6 @@ func apiBind(api interface{}) (apiBinding, error) { if _, exists := binding[fn]; exists { return nil, fmt.Errorf("function %s already exists", fn) } - println(fn) var hasInput, hasOutput bool var inputType reflect.Type diff --git a/app.go b/app.go index 9845965..35ed242 100644 --- a/app.go +++ b/app.go @@ -3,66 +3,110 @@ package webkitgtk import ( "fmt" "github.com/ebitengine/purego" + "net/http" "os" "runtime" - "strings" "sync" "syscall" "time" ) -var globalApplication *App - func init() { runtime.LockOSThread() } +var _app *App + type App struct { - options AppOptions - pointer ptr - pid int - ident string - logger *logger + log logFunc + + id string // application id e.g. com.github.malivvan.webkitgtk.undefined + pid int // process id of the application + name string // application name e.g. Unnamed Application + icon []byte // application icon used to create desktop file + + thread *mainThread // thread is the mainthread runner + pointer ptr // gtk application pointer + + trayIcon []byte // trayIcon is the system tray icon (if not set icon will be used) + trayMenu *TrayMenu // trayMenu is the system tray menu + + systray *dbusSystray // systray is the dbus systray + notifier *dbusNotify // notifier is the dbus notifier + session *dbusSession // session is the dbus session - windows map[uint]*Window - windowsLock sync.RWMutex + windows map[uint]*Window // windows is the map of all windows + windowsLock sync.RWMutex // windowsLock is the lock for windows map - runOnce runOnce + dialogs map[uint]interface{} // dialogs is the map of all dialogs + dialogsLock sync.RWMutex // dialogsLock is the lock for dialogs map - //web context - context ptr + handler map[string]http.Handler // handler is the map of all http handlers + handlerLock sync.RWMutex // handlerLock is the lock for handler map + + webContext ptr // webContext is the global webkit web context + hold bool // hold indicates if the application stays alive after the last window is closed + ephemeral bool // ephemeral is the flag to indicate if the application is ephemeral + dataDir string // dataDir is the directory where the application data is stored + cacheDir string // cacheDir is the directory where the application cache is stored + cookiePolicy WebkitCookiePolicy // cookiePolicy is the cookie policy for the application + cacheModel WebkitCacheModel // cacheModel is the cache model for the application + + started deferredRunner // started is the deferred runner for post application startup +} + +func (a *App) Menu(icon []byte) *TrayMenu { + a.trayIcon = icon + if a.trayMenu == nil { + a.trayMenu = &TrayMenu{} + } + return a.trayMenu +} + +func (a *App) Handle(host string, handler http.Handler) { + a.handlerLock.Lock() + a.handler[host] = handler + a.handlerLock.Unlock() } func New(options AppOptions) *App { - if globalApplication != nil { - return globalApplication + if _app != nil { + return _app } /////////////////////////// // Apply defaults + if options.ID == "" { + options.ID = "com.github.malivvan.webkitgtk.undefined" + } if options.Name == "" { - options.Name = "undefined" - } else { - options.Name = strings.ToLower(options.Name) + options.Name = "Unnamed Application" + } + if options.Icon == nil { + options.Icon = defaultIcon } // Create app app := &App{ - options: options, - pid: syscall.Getpid(), - ident: fmt.Sprintf("org.webkit2gtk.%s", strings.Replace(options.Name, " ", "-", -1)), - windows: make(map[uint]*Window), - } + log: newLogFunc("app"), + pid: syscall.Getpid(), + id: options.ID, + name: options.Name, + icon: options.Icon, - // Setup debug logger - if options.Debug { - app.logger = &logger{ - prefix: "webkit2gtk: " + options.Name, - writer: LogWriter, - } + windows: make(map[uint]*Window), + dialogs: make(map[uint]interface{}), + handler: make(map[string]http.Handler), + + hold: options.Hold, + ephemeral: options.Ephemeral, + dataDir: options.DataDir, + cacheDir: options.CacheDir, + cookiePolicy: options.CookiePolicy, + cacheModel: options.CacheModel, } ///////////////////////////////////// - globalApplication = app // !important + _app = app // !important return app } @@ -85,77 +129,95 @@ func (a *App) CurrentWindow() *Window { return nil } -func (a *App) Quit() { - appDestroy(a.pointer) -} +func (a *App) Run() (err error) { + defer panicHandlerRecover() -func (a *App) Run() error { - defer processPanicHandlerRecover() + // >>> STARTUP startupTime := time.Now() - a.log("application startup...", "identifier", a.ident, "main_thread", mainThreadId, "pid", a.pid) + a.log("application startup...", "identifier", a.id, "pid", a.pid) // 1. Fix console spam (USR1) if err := os.Setenv("JSC_SIGNAL_FOR_GC", "20"); err != nil { - return err + return fmt.Errorf("failed to set JSC_SIGNAL_FOR_GC: %w", err) } // 2. Load shared libraries if err := a.loadSharedLibs(); err != nil { - return err + return fmt.Errorf("failed to load shared libraries: %w", err) } - // 3. Get Main Thread and create GTK Application - mainThreadId = lib.g.ThreadSelf() - a.pointer = lib.gtk.ApplicationNew(a.ident, uint(0)) + // 3. Validate application identifier + if !lib.g.ApplicationIdIsValid(a.id) { + return fmt.Errorf("invalid application identifier: %s", a.id) + } - // 4. Run deferred functions - a.runOnce.invoke(true) + // 4. Get Main Thread and create GTK Application + a.thread = newMainThread() + a.pointer = lib.gtk.ApplicationNew(a.id, uint(0)) + a.log("application created", "pointer", a.pointer, "thread", a.thread.ID()) - // 5. Setup activate signal ipc - app := ptr(a.pointer) - activate := func() { - a.log("application startup complete", "since_startup", time.Since(startupTime)) - lib.g.ApplicationHold(app) // allow running without a pointer + // 5. Establish DBUS session + var dbusPlugins []dbusPlugin + if a.trayMenu != nil { + a.systray = a.trayMenu.toTray(a.id, a.trayIcon) + dbusPlugins = append(dbusPlugins, a.systray) } + a.notifier = &dbusNotify{ + appName: a.id, + } + dbusPlugins = append(dbusPlugins, a.notifier) + a.session, err = newDBusSession(dbusPlugins) + if err != nil { + return fmt.Errorf("failed to create dbus session: %w", err) + } + + // 5. Setup activate signal ipc lib.g.SignalConnectData( - ptr(a.pointer), + a.pointer, "activate", - purego.NewCallback(activate), - app, + purego.NewCallback(func() { + + // 7. Allow running without a window + lib.g.ApplicationHold(a.pointer) + + // 8. Invoke deferred runners + a.started.invoke() + + // <<< STARTUP + a.log("application startup complete", "since_startup", time.Since(startupTime)) + }), + a.pointer, false, 0) - // 5. Run GTK Application - status := lib.g.ApplicationRun(a.pointer, 0, nil) - ///////////////////////////////////////////////// + // 6. Run GTK Application + status := lib.g.ApplicationRun(a.pointer, 0, nil) // BLOCKING - // 6. Shutdown + // >>> SHUTDOWN shutdownTime := time.Now() a.log("application shutdown...", "status", status) + + // 1. Close dbus session + a.session.close() + + // 2. Release GTK Application and dereference application pointer lib.g.ApplicationRelease(a.pointer) - lib.g.ObjectUnref(ptr(a.pointer)) - var err error - if status != 0 { + lib.g.ObjectUnref(a.pointer) + + // 3. Handle exit status + if status == 0 { + err = nil + } else { err = fmt.Errorf("exit code: %d", status) } - a.log("application shutdown done", "since_shutdown", time.Since(shutdownTime)) - return err -} -func appDestroy(application ptr) { - lib.g.ApplicationQuit(application) -} - -func (a *App) log(msg interface{}, kv ...interface{}) { - if a.logger == nil { - return - } - a.logger.log(msg, kv...) + // <<< SHUTDOWN + a.log("application shutdown done", "error", err, "since_shutdown", time.Since(shutdownTime)) + return err } -func fatal(message string, args ...interface{}) { - println("*********************** FATAL ***********************") - println(fmt.Sprintf(message, args...)) - println("*********************** FATAL ***********************") - os.Exit(1) +func (a *App) Quit() { + a.thread.InvokeSync(func() { + lib.g.ApplicationQuit(a.pointer) + }) } diff --git a/dbus.go b/dbus.go new file mode 100644 index 0000000..af6a131 --- /dev/null +++ b/dbus.go @@ -0,0 +1,1025 @@ +package webkitgtk + +import ( + "context" + "errors" + "fmt" + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + "sync" +) + +type dbusPlugin interface { + Start(*dbus.Conn) error + Signal(*dbus.Signal) + Stop() +} + +type dbusSession struct { + wg sync.WaitGroup + log logFunc + quit chan struct{} + plugins []dbusPlugin +} + +func newDBusSession(plugins []dbusPlugin) (*dbusSession, error) { + s := &dbusSession{plugins: plugins} + + s.log = newLogFunc("dbus") + + s.log("starting dbus session routine") + conn, err := dbus.SessionBus() + if err != nil { + return nil, fmt.Errorf("dbusSystray error: failed to connect to DBus: %v\n", err) + } + + for _, plugin := range s.plugins { + if err := plugin.Start(conn); err != nil { + return nil, fmt.Errorf("dbusSystray error: failed to start plugin: %v\n", err) + } + } + + s.wg.Add(1) + s.log("dbus session routine started") + go func() { + defer func() { + s.log("dbus session routine stopped") + s.wg.Done() + }() + + sc := make(chan *dbus.Signal, 10) + conn.Signal(sc) + s.quit = make(chan struct{}, 1) + for { + select { + case sig := <-sc: + s.log("dbus signal received", "signal", sig) + if sig == nil { + return // We get a nil signal when closing the window. + } + for _, plugin := range s.plugins { + plugin.Signal(sig) + } + case <-s.quit: + s.log("stopping dbus session routine") + for _, plugin := range s.plugins { + plugin.Stop() + } + _ = conn.Close() + close(s.quit) + s.quit = nil + return + } + } + }() + + return s, nil +} + +func (s *dbusSession) close() { + if s.quit != nil { + s.quit <- struct{}{} + } + s.wg.Wait() +} + +// dbusSignal is a common interface for all signals. +type dbusSignal interface { + Name() string + Interface() string + Sender() string + + path() dbus.ObjectPath + values() []interface{} +} + +// dbusEmit sends the given signal to the bus. +func dbusEmit(conn *dbus.Conn, s dbusSignal) error { + return conn.Emit(s.path(), s.Interface()+"."+s.Name(), s.values()...) +} + +// dbusAddMatchSignal registers a match rule for the given signal, +// opts are appended to the automatically generated signal's rules. +func dbusAddMatchSignal(conn *dbus.Conn, s dbusSignal, opts ...dbus.MatchOption) error { + return conn.AddMatchSignal(append([]dbus.MatchOption{ + dbus.WithMatchInterface(s.Interface()), + dbus.WithMatchMember(s.Name()), + }, opts...)...) +} + +// dbusRemoveMatchSignal unregisters the previously registered subscription. +func dbusRemoveMatchSignal(conn *dbus.Conn, s dbusSignal, opts ...dbus.MatchOption) error { + return conn.RemoveMatchSignal(append([]dbus.MatchOption{ + dbus.WithMatchInterface(s.Interface()), + dbus.WithMatchMember(s.Name()), + }, opts...)...) +} + +// dbusErrUnknownSignal is returned by dbusLookupStatusNotifierItemSignal when a signal cannot be resolved. +var dbusErrUnknownSignal = errors.New("unknown signal") + +// ############################################### +var dbusMenuIntrospectData = introspect.Interface{ + Name: "com.canonical.dbusmenu", + Methods: []introspect.Method{{Name: "GetLayout", Args: []introspect.Arg{ + {Name: "parentId", Type: "i", Direction: "in"}, + {Name: "recursionDepth", Type: "i", Direction: "in"}, + {Name: "propertyNames", Type: "as", Direction: "in"}, + {Name: "revision", Type: "u", Direction: "out"}, + {Name: "layout", Type: "(ia{sv}av)", Direction: "out"}, + }}, + {Name: "GetGroupProperties", Args: []introspect.Arg{ + {Name: "ids", Type: "ai", Direction: "in"}, + {Name: "propertyNames", Type: "as", Direction: "in"}, + {Name: "properties", Type: "a(ia{sv})", Direction: "out"}, + }}, + {Name: "GetProperty", Args: []introspect.Arg{ + {Name: "id", Type: "i", Direction: "in"}, + {Name: "name", Type: "s", Direction: "in"}, + {Name: "value", Type: "v", Direction: "out"}, + }}, + {Name: "Event", Args: []introspect.Arg{ + {Name: "id", Type: "i", Direction: "in"}, + {Name: "eventId", Type: "s", Direction: "in"}, + {Name: "data", Type: "v", Direction: "in"}, + {Name: "timestamp", Type: "u", Direction: "in"}, + }}, + {Name: "EventGroup", Args: []introspect.Arg{ + {Name: "events", Type: "a(isvu)", Direction: "in"}, + {Name: "idErrors", Type: "ai", Direction: "out"}, + }}, + {Name: "AboutToShow", Args: []introspect.Arg{ + {Name: "id", Type: "i", Direction: "in"}, + {Name: "needUpdate", Type: "b", Direction: "out"}, + }}, + {Name: "AboutToShowGroup", Args: []introspect.Arg{ + {Name: "ids", Type: "ai", Direction: "in"}, + {Name: "updatesNeeded", Type: "ai", Direction: "out"}, + {Name: "idErrors", Type: "ai", Direction: "out"}, + }}, + }, + Signals: []introspect.Signal{{Name: "ItemsPropertiesUpdated", Args: []introspect.Arg{ + {Name: "updatedProps", Type: "a(ia{sv})", Direction: "out"}, + {Name: "removedProps", Type: "a(ias)", Direction: "out"}, + }}, + {Name: "LayoutUpdated", Args: []introspect.Arg{ + {Name: "revision", Type: "u", Direction: "out"}, + {Name: "parent", Type: "i", Direction: "out"}, + }}, + {Name: "ItemActivationRequested", Args: []introspect.Arg{ + {Name: "id", Type: "i", Direction: "out"}, + {Name: "timestamp", Type: "u", Direction: "out"}, + }}, + }, + Properties: []introspect.Property{{Name: "Version", Type: "u", Access: "read"}, + {Name: "TextDirection", Type: "s", Access: "read"}, + {Name: "Status", Type: "s", Access: "read"}, + {Name: "IconThemePath", Type: "as", Access: "read"}, + }, + Annotations: []introspect.Annotation{}, +} + +func dbusLookupMenuSignal(signal *dbus.Signal) (dbusSignal, error) { + switch signal.Name { + case dbusMenuInterface + "." + "ItemsPropertiesUpdated": + v0, ok := signal.Body[0].([]struct { + V0 int32 + V1 map[string]dbus.Variant + }) + if !ok { + return nil, fmt.Errorf("prop .UpdatedProps is %T, not []struct {V0 int32;V1 map[string]dbus.Variant}", signal.Body[0]) + } + v1, ok := signal.Body[1].([]struct { + V0 int32 + V1 []string + }) + if !ok { + return nil, fmt.Errorf("prop .RemovedProps is %T, not []struct {V0 int32;V1 []string}", signal.Body[1]) + } + return &dbusMenuItemsPropertiesUpdatedSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &dbusMenuItemsPropertiesUpdatedSignalBody{ + UpdatedProps: v0, + RemovedProps: v1, + }, + }, nil + case dbusMenuInterface + "." + "LayoutUpdated": + v0, ok := signal.Body[0].(uint32) + if !ok { + return nil, fmt.Errorf("prop .Revision is %T, not uint32", signal.Body[0]) + } + v1, ok := signal.Body[1].(int32) + if !ok { + return nil, fmt.Errorf("prop .Parent is %T, not int32", signal.Body[1]) + } + return &dbusMenuLayoutUpdatedSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &dbusMenuLayoutUpdatedSignalBody{ + Revision: v0, + Parent: v1, + }, + }, nil + case dbusMenuInterface + "." + "ItemActivationRequested": + v0, ok := signal.Body[0].(int32) + if !ok { + return nil, fmt.Errorf("prop .Id is %T, not int32", signal.Body[0]) + } + v1, ok := signal.Body[1].(uint32) + if !ok { + return nil, fmt.Errorf("prop .Timestamp is %T, not uint32", signal.Body[1]) + } + return &dbusMenuItemActivationRequestedSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &dbusMenuItemActivationRequestedSignalBody{ + Id: v0, + Timestamp: v1, + }, + }, nil + default: + return nil, dbusErrUnknownSignal + } +} + +const dbusMenuInterface = "com.canonical.dbusmenu" + +type dbusMenuer interface { + // GetLayout is com.canonical.dbusmenu.GetLayout method. + GetLayout(parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct { + V0 int32 + V1 map[string]dbus.Variant + V2 []dbus.Variant + }, err *dbus.Error) + // GetGroupProperties is com.canonical.dbusmenu.GetGroupProperties method. + GetGroupProperties(ids []int32, propertyNames []string) (properties []struct { + V0 int32 + V1 map[string]dbus.Variant + }, err *dbus.Error) + // GetProperty is com.canonical.dbusmenu.GetProperty method. + GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) + // Event is com.canonical.dbusmenu.Event method. + Event(id int32, eventId string, data dbus.Variant, timestamp uint32) (err *dbus.Error) + // EventGroup is com.canonical.dbusmenu.EventGroup method. + EventGroup(events []struct { + V0 int32 + V1 string + V2 dbus.Variant + V3 uint32 + }) (idErrors []int32, err *dbus.Error) + // AboutToShow is com.canonical.dbusmenu.AboutToShow method. + AboutToShow(id int32) (needUpdate bool, err *dbus.Error) + // AboutToShowGroup is com.canonical.dbusmenu.AboutToShowGroup method. + AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) +} + +func dbusExportMenu(conn *dbus.Conn, path dbus.ObjectPath, v dbusMenuer) error { + return conn.ExportSubtreeMethodTable(map[string]interface{}{ + "GetLayout": v.GetLayout, + "GetGroupProperties": v.GetGroupProperties, + "GetProperty": v.GetProperty, + "Event": v.Event, + "EventGroup": v.EventGroup, + "AboutToShow": v.AboutToShow, + "AboutToShowGroup": v.AboutToShowGroup, + }, path, dbusMenuInterface) +} + +func dbusUnexportMenu(conn *dbus.Conn, path dbus.ObjectPath) error { + return conn.Export(nil, path, dbusMenuInterface) +} + +type dbusUnimplementedMenu struct{} + +func (*dbusUnimplementedMenu) iface() string { + return dbusMenuInterface +} + +func (*dbusUnimplementedMenu) GetLayout(parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct { + V0 int32 + V1 map[string]dbus.Variant + V2 []dbus.Variant +}, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*dbusUnimplementedMenu) GetGroupProperties(ids []int32, propertyNames []string) (properties []struct { + V0 int32 + V1 map[string]dbus.Variant +}, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*dbusUnimplementedMenu) GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*dbusUnimplementedMenu) Event(id int32, eventId string, data dbus.Variant, timestamp uint32) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*dbusUnimplementedMenu) EventGroup(events []struct { + V0 int32 + V1 string + V2 dbus.Variant + V3 uint32 +}) (idErrors []int32, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*dbusUnimplementedMenu) AboutToShow(id int32) (needUpdate bool, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*dbusUnimplementedMenu) AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func dbusNewMenu(object dbus.BusObject) *dbusMenu { + return &dbusMenu{object} +} + +type dbusMenu struct { + object dbus.BusObject +} + +func (o *dbusMenu) GetLayout(ctx context.Context, parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct { + V0 int32 + V1 map[string]dbus.Variant + V2 []dbus.Variant +}, err error) { + err = o.object.CallWithContext(ctx, dbusMenuInterface+".GetLayout", 0, parentId, recursionDepth, propertyNames).Store(&revision, &layout) + return +} + +func (o *dbusMenu) GetGroupProperties(ctx context.Context, ids []int32, propertyNames []string) (properties []struct { + V0 int32 + V1 map[string]dbus.Variant +}, err error) { + err = o.object.CallWithContext(ctx, dbusMenuInterface+".GetGroupProperties", 0, ids, propertyNames).Store(&properties) + return +} + +func (o *dbusMenu) GetProperty(ctx context.Context, id int32, name string) (value dbus.Variant, err error) { + err = o.object.CallWithContext(ctx, dbusMenuInterface+".GetProperty", 0, id, name).Store(&value) + return +} + +func (o *dbusMenu) Event(ctx context.Context, id int32, eventId string, data dbus.Variant, timestamp uint32) (err error) { + err = o.object.CallWithContext(ctx, dbusMenuInterface+".Event", 0, id, eventId, data, timestamp).Store() + return +} + +func (o *dbusMenu) EventGroup(ctx context.Context, events []struct { + V0 int32 + V1 string + V2 dbus.Variant + V3 uint32 +}) (idErrors []int32, err error) { + err = o.object.CallWithContext(ctx, dbusMenuInterface+".EventGroup", 0, events).Store(&idErrors) + return +} + +func (o *dbusMenu) AboutToShow(ctx context.Context, id int32) (needUpdate bool, err error) { + err = o.object.CallWithContext(ctx, dbusMenuInterface+".AboutToShow", 0, id).Store(&needUpdate) + return +} + +func (o *dbusMenu) AboutToShowGroup(ctx context.Context, ids []int32) (updatesNeeded []int32, idErrors []int32, err error) { + err = o.object.CallWithContext(ctx, dbusMenuInterface+".AboutToShowGroup", 0, ids).Store(&updatesNeeded, &idErrors) + return +} + +func (o *dbusMenu) GetVersion(ctx context.Context) (version uint32, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusMenuInterface, "Version").Store(&version) + return +} + +func (o *dbusMenu) GetTextDirection(ctx context.Context) (textDirection string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusMenuInterface, "TextDirection").Store(&textDirection) + return +} + +func (o *dbusMenu) GetStatus(ctx context.Context) (status string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusMenuInterface, "Status").Store(&status) + return +} + +func (o *dbusMenu) GetIconThemePath(ctx context.Context) (iconThemePath []string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusMenuInterface, "IconThemePath").Store(&iconThemePath) + return +} + +type dbusMenuItemsPropertiesUpdatedSignal struct { + sender string + Path dbus.ObjectPath + Body *dbusMenuItemsPropertiesUpdatedSignalBody +} + +func (s *dbusMenuItemsPropertiesUpdatedSignal) Name() string { + return "ItemsPropertiesUpdated" +} + +func (s *dbusMenuItemsPropertiesUpdatedSignal) Interface() string { + return dbusMenuInterface +} + +func (s *dbusMenuItemsPropertiesUpdatedSignal) Sender() string { + return s.sender +} + +func (s *dbusMenuItemsPropertiesUpdatedSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *dbusMenuItemsPropertiesUpdatedSignal) values() []interface{} { + return []interface{}{s.Body.UpdatedProps, s.Body.RemovedProps} +} + +type dbusMenuItemsPropertiesUpdatedSignalBody struct { + UpdatedProps []struct { + V0 int32 + V1 map[string]dbus.Variant + } + RemovedProps []struct { + V0 int32 + V1 []string + } +} + +type dbusMenuLayoutUpdatedSignal struct { + sender string + Path dbus.ObjectPath + Body *dbusMenuLayoutUpdatedSignalBody +} + +func (s *dbusMenuLayoutUpdatedSignal) Name() string { + return "LayoutUpdated" +} + +func (s *dbusMenuLayoutUpdatedSignal) Interface() string { + return dbusMenuInterface +} + +func (s *dbusMenuLayoutUpdatedSignal) Sender() string { + return s.sender +} + +func (s *dbusMenuLayoutUpdatedSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *dbusMenuLayoutUpdatedSignal) values() []interface{} { + return []interface{}{s.Body.Revision, s.Body.Parent} +} + +type dbusMenuLayoutUpdatedSignalBody struct { + Revision uint32 + Parent int32 +} + +type dbusMenuItemActivationRequestedSignal struct { + sender string + Path dbus.ObjectPath + Body *dbusMenuItemActivationRequestedSignalBody +} + +func (s *dbusMenuItemActivationRequestedSignal) Name() string { + return "ItemActivationRequested" +} + +func (s *dbusMenuItemActivationRequestedSignal) Interface() string { + return dbusMenuInterface +} + +func (s *dbusMenuItemActivationRequestedSignal) Sender() string { + return s.sender +} + +func (s *dbusMenuItemActivationRequestedSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *dbusMenuItemActivationRequestedSignal) values() []interface{} { + return []interface{}{s.Body.Id, s.Body.Timestamp} +} + +type dbusMenuItemActivationRequestedSignalBody struct { + Id int32 + Timestamp uint32 +} + +// ############################################################# +var dbusStatusNotifierItemIntrospectData = introspect.Interface{ + Name: "org.kde.StatusNotifierItem", + Methods: []introspect.Method{{Name: "ContextMenu", Args: []introspect.Arg{ + {Name: "x", Type: "i", Direction: "in"}, + {Name: "y", Type: "i", Direction: "in"}, + }}, + {Name: "Activate", Args: []introspect.Arg{ + {Name: "x", Type: "i", Direction: "in"}, + {Name: "y", Type: "i", Direction: "in"}, + }}, + {Name: "SecondaryActivate", Args: []introspect.Arg{ + {Name: "x", Type: "i", Direction: "in"}, + {Name: "y", Type: "i", Direction: "in"}, + }}, + {Name: "Scroll", Args: []introspect.Arg{ + {Name: "delta", Type: "i", Direction: "in"}, + {Name: "orientation", Type: "s", Direction: "in"}, + }}, + }, + Signals: []introspect.Signal{{Name: "NewTitle"}, + {Name: "NewIcon"}, + {Name: "NewAttentionIcon"}, + {Name: "NewOverlayIcon"}, + {Name: "NewStatus", Args: []introspect.Arg{ + {Name: "status", Type: "s", Direction: ""}, + }}, + {Name: "NewIconThemePath", Args: []introspect.Arg{ + {Name: "icon_theme_path", Type: "s", Direction: "out"}, + }}, + {Name: "NewMenu"}, + }, + Properties: []introspect.Property{{Name: "Category", Type: "s", Access: "read"}, + {Name: "Id", Type: "s", Access: "read"}, + {Name: "Title", Type: "s", Access: "read"}, + {Name: "Status", Type: "s", Access: "read"}, + {Name: "WindowId", Type: "i", Access: "read"}, + {Name: "IconThemePath", Type: "s", Access: "read"}, + {Name: "Menu", Type: "o", Access: "read"}, + {Name: "ItemIsMenu", Type: "b", Access: "read"}, + {Name: "IconName", Type: "s", Access: "read"}, + {Name: "IconPixmap", Type: "a(iiay)", Access: "read", Annotations: []introspect.Annotation{ + {Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusImageVector"}, + }}, + {Name: "OverlayIconName", Type: "s", Access: "read"}, + {Name: "OverlayIconPixmap", Type: "a(iiay)", Access: "read", Annotations: []introspect.Annotation{ + {Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusImageVector"}, + }}, + {Name: "AttentionIconName", Type: "s", Access: "read"}, + {Name: "AttentionIconPixmap", Type: "a(iiay)", Access: "read", Annotations: []introspect.Annotation{ + {Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusImageVector"}, + }}, + {Name: "AttentionMovieName", Type: "s", Access: "read"}, + {Name: "ToolTip", Type: "(sa(iiay)ss)", Access: "read", Annotations: []introspect.Annotation{ + {Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusToolTipStruct"}, + }}, + }, + Annotations: []introspect.Annotation{}, +} + +func dbusLookupStatusNotifierItemSignal(signal *dbus.Signal) (dbusSignal, error) { + switch signal.Name { + case dbusStatusNotifierItemInterface + "." + "NewTitle": + return &dbusStatusNotifierItemNewTitleSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &dbusStatusNotifierItemNewTitleSignalBody{}, + }, nil + case dbusStatusNotifierItemInterface + "." + "NewIcon": + return &dbusStatusNotifierItemNewIconSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &dbusStatusNotifierItemNewIconSignalBody{}, + }, nil + case dbusStatusNotifierItemInterface + "." + "NewAttentionIcon": + return &dbusStatusNotifierItemNewAttentionIconSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &dbusStatusNotifierItemNewAttentionIconSignalBody{}, + }, nil + case dbusStatusNotifierItemInterface + "." + "NewOverlayIcon": + return &dbusStatusNotifierItemNewOverlayIconSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &dbusStatusNotifierItemNewOverlayIconSignalBody{}, + }, nil + case dbusStatusNotifierItemInterface + "." + "NewStatus": + v0, ok := signal.Body[0].(string) + if !ok { + return nil, fmt.Errorf("prop .Status is %T, not string", signal.Body[0]) + } + return &dbusStatusNotifierItemNewStatusSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &dbusStatusNotifierItemNewStatusSignalBody{ + Status: v0, + }, + }, nil + case dbusStatusNotifierItemInterface + "." + "NewIconThemePath": + v0, ok := signal.Body[0].(string) + if !ok { + return nil, fmt.Errorf("prop .IconThemePath is %T, not string", signal.Body[0]) + } + return &dbusStatusNotifierItemNewIconThemePathSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &dbusStatusNotifierItemNewIconThemePathSignalBody{ + IconThemePath: v0, + }, + }, nil + case dbusStatusNotifierItemInterface + "." + "NewMenu": + return &dbusStatusNotifierItemNewMenuSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &dbusStatusNotifierItemNewMenuSignalBody{}, + }, nil + default: + return nil, dbusErrUnknownSignal + } +} + +const dbusStatusNotifierItemInterface = "org.kde.StatusNotifierItem" + +type dbusStatusNotifierItemer interface { + // ContextMenu is org.kde.dbusStatusNotifierItem.ContextMenu method. + ContextMenu(x int32, y int32) (err *dbus.Error) + // Activate is org.kde.dbusStatusNotifierItem.Activate method. + Activate(x int32, y int32) (err *dbus.Error) + // SecondaryActivate is org.kde.dbusStatusNotifierItem.SecondaryActivate method. + SecondaryActivate(x int32, y int32) (err *dbus.Error) + // Scroll is org.kde.dbusStatusNotifierItem.Scroll method. + Scroll(delta int32, orientation string) (err *dbus.Error) +} + +func dbusExportStatusNotifierItem(conn *dbus.Conn, path dbus.ObjectPath, v dbusStatusNotifierItemer) error { + return conn.ExportSubtreeMethodTable(map[string]interface{}{ + "ContextMenu": v.ContextMenu, + "Activate": v.Activate, + "SecondaryActivate": v.SecondaryActivate, + "Scroll": v.Scroll, + }, path, dbusStatusNotifierItemInterface) +} + +func dbusUnexportStatusNotifierItem(conn *dbus.Conn, path dbus.ObjectPath) error { + return conn.Export(nil, path, dbusStatusNotifierItemInterface) +} + +type dbusUnimplementedStatusNotifierItem struct{} + +func (*dbusUnimplementedStatusNotifierItem) iface() string { + return dbusStatusNotifierItemInterface +} + +func (*dbusUnimplementedStatusNotifierItem) ContextMenu(x int32, y int32) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*dbusUnimplementedStatusNotifierItem) Activate(x int32, y int32) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*dbusUnimplementedStatusNotifierItem) SecondaryActivate(x int32, y int32) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*dbusUnimplementedStatusNotifierItem) Scroll(delta int32, orientation string) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func dbusNewStatusNotifierItem(object dbus.BusObject) *dbusStatusNotifierItem { + return &dbusStatusNotifierItem{object} +} + +type dbusStatusNotifierItem struct { + object dbus.BusObject +} + +func (o *dbusStatusNotifierItem) ContextMenu(ctx context.Context, x int32, y int32) (err error) { + err = o.object.CallWithContext(ctx, dbusStatusNotifierItemInterface+".ContextMenu", 0, x, y).Store() + return +} + +func (o *dbusStatusNotifierItem) Activate(ctx context.Context, x int32, y int32) (err error) { + err = o.object.CallWithContext(ctx, dbusStatusNotifierItemInterface+".Activate", 0, x, y).Store() + return +} + +func (o *dbusStatusNotifierItem) SecondaryActivate(ctx context.Context, x int32, y int32) (err error) { + err = o.object.CallWithContext(ctx, dbusStatusNotifierItemInterface+".SecondaryActivate", 0, x, y).Store() + return +} + +func (o *dbusStatusNotifierItem) Scroll(ctx context.Context, delta int32, orientation string) (err error) { + err = o.object.CallWithContext(ctx, dbusStatusNotifierItemInterface+".Scroll", 0, delta, orientation).Store() + return +} + +func (o *dbusStatusNotifierItem) GetCategory(ctx context.Context) (category string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "Category").Store(&category) + return +} + +func (o *dbusStatusNotifierItem) GetId(ctx context.Context) (id string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "Id").Store(&id) + return +} + +func (o *dbusStatusNotifierItem) GetTitle(ctx context.Context) (title string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "Title").Store(&title) + return +} + +func (o *dbusStatusNotifierItem) GetStatus(ctx context.Context) (status string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "Status").Store(&status) + return +} + +func (o *dbusStatusNotifierItem) GetWindowId(ctx context.Context) (windowId int32, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "WindowId").Store(&windowId) + return +} + +func (o *dbusStatusNotifierItem) GetIconThemePath(ctx context.Context) (iconThemePath string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "IconThemePath").Store(&iconThemePath) + return +} + +func (o *dbusStatusNotifierItem) GetMenu(ctx context.Context) (menu dbus.ObjectPath, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "Menu").Store(&menu) + return +} + +func (o *dbusStatusNotifierItem) GetItemIsMenu(ctx context.Context) (itemIsMenu bool, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "ItemIsMenu").Store(&itemIsMenu) + return +} + +func (o *dbusStatusNotifierItem) GetIconName(ctx context.Context) (iconName string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "IconName").Store(&iconName) + return +} + +func (o *dbusStatusNotifierItem) GetIconPixmap(ctx context.Context) (iconPixmap []struct { + V0 int32 + V1 int32 + V2 []byte +}, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "IconPixmap").Store(&iconPixmap) + return +} + +func (o *dbusStatusNotifierItem) GetOverlayIconName(ctx context.Context) (overlayIconName string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "OverlayIconName").Store(&overlayIconName) + return +} + +func (o *dbusStatusNotifierItem) GetOverlayIconPixmap(ctx context.Context) (overlayIconPixmap []struct { + V0 int32 + V1 int32 + V2 []byte +}, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "OverlayIconPixmap").Store(&overlayIconPixmap) + return +} + +func (o *dbusStatusNotifierItem) GetAttentionIconName(ctx context.Context) (attentionIconName string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "AttentionIconName").Store(&attentionIconName) + return +} + +func (o *dbusStatusNotifierItem) GetAttentionIconPixmap(ctx context.Context) (attentionIconPixmap []struct { + V0 int32 + V1 int32 + V2 []byte +}, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "AttentionIconPixmap").Store(&attentionIconPixmap) + return +} + +func (o *dbusStatusNotifierItem) GetAttentionMovieName(ctx context.Context) (attentionMovieName string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "AttentionMovieName").Store(&attentionMovieName) + return +} + +func (o *dbusStatusNotifierItem) GetToolTip(ctx context.Context) (toolTip struct { + V0 string + V1 []struct { + V0 int32 + V1 int32 + V2 []byte + } + V2 string + V3 string +}, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, dbusStatusNotifierItemInterface, "ToolTip").Store(&toolTip) + return +} + +type dbusStatusNotifierItemNewTitleSignal struct { + sender string + Path dbus.ObjectPath + Body *dbusStatusNotifierItemNewTitleSignalBody +} + +func (s *dbusStatusNotifierItemNewTitleSignal) Name() string { + return "NewTitle" +} + +func (s *dbusStatusNotifierItemNewTitleSignal) Interface() string { + return dbusStatusNotifierItemInterface +} + +func (s *dbusStatusNotifierItemNewTitleSignal) Sender() string { + return s.sender +} + +func (s *dbusStatusNotifierItemNewTitleSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *dbusStatusNotifierItemNewTitleSignal) values() []interface{} { + return []interface{}{} +} + +type dbusStatusNotifierItemNewTitleSignalBody struct { +} + +type dbusStatusNotifierItemNewIconSignal struct { + sender string + Path dbus.ObjectPath + Body *dbusStatusNotifierItemNewIconSignalBody +} + +func (s *dbusStatusNotifierItemNewIconSignal) Name() string { + return "NewIcon" +} + +func (s *dbusStatusNotifierItemNewIconSignal) Interface() string { + return dbusStatusNotifierItemInterface +} + +func (s *dbusStatusNotifierItemNewIconSignal) Sender() string { + return s.sender +} + +func (s *dbusStatusNotifierItemNewIconSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *dbusStatusNotifierItemNewIconSignal) values() []interface{} { + return []interface{}{} +} + +type dbusStatusNotifierItemNewIconSignalBody struct { +} + +type dbusStatusNotifierItemNewAttentionIconSignal struct { + sender string + Path dbus.ObjectPath + Body *dbusStatusNotifierItemNewAttentionIconSignalBody +} + +func (s *dbusStatusNotifierItemNewAttentionIconSignal) Name() string { + return "NewAttentionIcon" +} + +func (s *dbusStatusNotifierItemNewAttentionIconSignal) Interface() string { + return dbusStatusNotifierItemInterface +} + +func (s *dbusStatusNotifierItemNewAttentionIconSignal) Sender() string { + return s.sender +} + +func (s *dbusStatusNotifierItemNewAttentionIconSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *dbusStatusNotifierItemNewAttentionIconSignal) values() []interface{} { + return []interface{}{} +} + +type dbusStatusNotifierItemNewAttentionIconSignalBody struct { +} + +type dbusStatusNotifierItemNewOverlayIconSignal struct { + sender string + Path dbus.ObjectPath + Body *dbusStatusNotifierItemNewOverlayIconSignalBody +} + +func (s *dbusStatusNotifierItemNewOverlayIconSignal) Name() string { + return "NewOverlayIcon" +} + +func (s *dbusStatusNotifierItemNewOverlayIconSignal) Interface() string { + return dbusStatusNotifierItemInterface +} + +func (s *dbusStatusNotifierItemNewOverlayIconSignal) Sender() string { + return s.sender +} + +func (s *dbusStatusNotifierItemNewOverlayIconSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *dbusStatusNotifierItemNewOverlayIconSignal) values() []interface{} { + return []interface{}{} +} + +type dbusStatusNotifierItemNewOverlayIconSignalBody struct { +} + +type dbusStatusNotifierItemNewStatusSignal struct { + sender string + Path dbus.ObjectPath + Body *dbusStatusNotifierItemNewStatusSignalBody +} + +func (s *dbusStatusNotifierItemNewStatusSignal) Name() string { + return "NewStatus" +} + +func (s *dbusStatusNotifierItemNewStatusSignal) Interface() string { + return dbusStatusNotifierItemInterface +} + +func (s *dbusStatusNotifierItemNewStatusSignal) Sender() string { + return s.sender +} + +func (s *dbusStatusNotifierItemNewStatusSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *dbusStatusNotifierItemNewStatusSignal) values() []interface{} { + return []interface{}{s.Body.Status} +} + +type dbusStatusNotifierItemNewStatusSignalBody struct { + Status string +} + +type dbusStatusNotifierItemNewIconThemePathSignal struct { + sender string + Path dbus.ObjectPath + Body *dbusStatusNotifierItemNewIconThemePathSignalBody +} + +func (s *dbusStatusNotifierItemNewIconThemePathSignal) Name() string { + return "NewIconThemePath" +} + +func (s *dbusStatusNotifierItemNewIconThemePathSignal) Interface() string { + return dbusStatusNotifierItemInterface +} + +func (s *dbusStatusNotifierItemNewIconThemePathSignal) Sender() string { + return s.sender +} + +func (s *dbusStatusNotifierItemNewIconThemePathSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *dbusStatusNotifierItemNewIconThemePathSignal) values() []interface{} { + return []interface{}{s.Body.IconThemePath} +} + +type dbusStatusNotifierItemNewIconThemePathSignalBody struct { + IconThemePath string +} + +type dbusStatusNotifierItemNewMenuSignal struct { + sender string + Path dbus.ObjectPath + Body *dbusStatusNotifierItemNewMenuSignalBody +} + +func (s *dbusStatusNotifierItemNewMenuSignal) Name() string { + return "NewMenu" +} + +func (s *dbusStatusNotifierItemNewMenuSignal) Interface() string { + return dbusStatusNotifierItemInterface +} + +func (s *dbusStatusNotifierItemNewMenuSignal) Sender() string { + return s.sender +} + +func (s *dbusStatusNotifierItemNewMenuSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *dbusStatusNotifierItemNewMenuSignal) values() []interface{} { + return []interface{}{} +} + +type dbusStatusNotifierItemNewMenuSignalBody struct { +} diff --git a/dialog.go b/dialog.go new file mode 100644 index 0000000..a008efd --- /dev/null +++ b/dialog.go @@ -0,0 +1,687 @@ +package webkitgtk + +import ( + "strings" + "sync" + "sync/atomic" + "unsafe" +) + +const ( + GSourceRemove int = 0 + + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gdk/gdkwindow.h#L121 + GdkHintMinSize = 1 << 1 + GdkHintMaxSize = 1 << 2 + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gdk/gdkevents.h#L512 + GdkWindowStateIconified = 1 << 1 + GdkWindowStateMaximized = 1 << 2 + GdkWindowStateFullscreen = 1 << 4 + + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/gtkmessagedialog.h#L87 + GtkButtonsNone int = 0 + GtkButtonsOk = 1 + GtkButtonsClose = 2 + GtkButtonsCancel = 3 + GtkButtonsYesNo = 4 + GtkButtonsOkCancel = 5 + + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/gtkdialog.h#L36 + GtkDialogModal = 1 << 0 + GtkDialogDestroyWithParent = 1 << 1 + GtkDialogUseHeaderBar = 1 << 2 // actions in header bar instead of action area + + GtkOrientationVertical = 1 +) + +var dialogMapID = make(map[uint64]struct{}) +var dialogIDLock sync.RWMutex + +func getDialogID() uint64 { + dialogIDLock.Lock() + defer dialogIDLock.Unlock() + dialogID := uint64(1) + for { + if _, ok := dialogMapID[dialogID]; !ok { + dialogMapID[dialogID] = struct{}{} + break + } + dialogID++ + if dialogID == 0 { + panic("no more dialog IDs") + } + } + return dialogID +} + +func freeDialogID(id uint64) { + dialogIDLock.Lock() + defer dialogIDLock.Unlock() + delete(dialogMapID, id) +} + +type Dialog struct { + app *App + window *Window +} + +func (a *App) Dialog() *Dialog { + return &Dialog{ + app: a, + window: a.CurrentWindow(), + } +} + +func (w *Window) Dialog() *Dialog { + return &Dialog{ + app: w.app, + window: w, + } +} +func (d *Dialog) Open(title string) *OpenFileDialog { + return d.open(title) +} + +func (d *Dialog) Save(title string) *SaveFileDialog { + return d.save(title) +} + +func (d *Dialog) Info(title string, message string, actions ...string) *MessageDialog { + if len(actions) == 0 { + actions = []string{"OK"} + } + return d.message(0, title, message, actions) +} + +func (d *Dialog) Warn(title string, message string, actions ...string) *MessageDialog { + if len(actions) == 0 { + actions = []string{"OK"} + } + return d.message(1, title, message, actions) +} + +func (d *Dialog) Ask(title string, message string, actions ...string) *MessageDialog { + if len(actions) == 0 { + actions = []string{"Yes", "No"} + } + return d.message(2, title, message, actions) +} + +func (d *Dialog) Fail(title string, message string, actions ...string) *MessageDialog { + if len(actions) == 0 { + actions = []string{"OK"} + } + return d.message(3, title, message, actions) +} + +func (d *Dialog) open(title string) *OpenFileDialog { + return &OpenFileDialog{ + app: d.app, + window: d.window, + log: newLogFunc("open-dialog"), + title: title, + canChooseDirectories: false, + canChooseFiles: true, + canCreateDirectories: true, + resolvesAliases: false, + } +} + +func (d *Dialog) save(title string) *SaveFileDialog { + return &SaveFileDialog{ + app: d.app, + window: d.window, + log: newLogFunc("save-dialog"), + title: title, + canCreateDirectories: true, + } +} + +// enum GtkMessageType: GtkMessageInfo = 0 GtkMessageWarning = 1 GtkMessageQuestion = 2 GtkMessageError = 3 +func (d *Dialog) message(dtype int, title string, message string, actions []string) *MessageDialog { + var buttons []*dialogMessageButton + for _, action := range actions { + buttons = append(buttons, &dialogMessageButton{ + Action: action, + Label: action, + }) + } + var firstButton *dialogMessageButton + if len(buttons) > 0 { + firstButton = buttons[0] + } + if firstButton != nil { + firstButton.IsDefault = true + } + var lastButton *dialogMessageButton + if len(buttons) > 0 { + lastButton = buttons[len(buttons)-1] + } + if lastButton != nil { + lastButton.IsCancel = true + } + return &MessageDialog{ + app: d.app, + window: d.window, + log: newLogFunc("msg-dialog"), + dtype: dtype, + title: title, + message: message, + buttons: buttons, + } +} + +type dialogMessageButton struct { + Action string + Label string + IsCancel bool + IsDefault bool +} + +type MessageDialog struct { + app *App + log logFunc + id atomic.Uint64 + result chan int + + dtype int + title string + message string + buttons []*dialogMessageButton + icon []byte + window *Window +} + +func (d *MessageDialog) run() { + if d.id.Load() == 0 { + id := getDialogID() + d.id.Store(id) + defer func() { + freeDialogID(id) + d.id.Store(0) + d.log("free", "id", id) + }() + d.log("open", "id", id, "title", d.title, "message", d.message) + action := d.app.thread.InvokeSyncWithResult(func() any { + return runMessageDialog(d) + }) + if d.result != nil { + result, ok := action.(int) + if !ok { + d.result <- -1 + } else { + d.result <- result + } + close(d.result) + } + } +} + +func (d *MessageDialog) SetIcon(icon []byte) *MessageDialog { + d.icon = icon + return d +} + +func (d *MessageDialog) AddDefault(action string, label string) *MessageDialog { + d.buttons = append(d.buttons, &dialogMessageButton{ + Action: action, + Label: label, + IsDefault: true, + }) + return d +} +func (d *MessageDialog) AddCancel(action string, label string) *MessageDialog { + d.buttons = append(d.buttons, &dialogMessageButton{ + Action: action, + Label: label, + IsCancel: true, + }) + return d +} + +func (d *MessageDialog) AddButton(action string, label string) *MessageDialog { + d.buttons = append(d.buttons, &dialogMessageButton{ + Action: action, + Label: label, + }) + return d +} + +func (d *MessageDialog) Show(callbacks ...func(int)) chan int { + d.result = make(chan int, 1) + d.app.started.run(d) + if len(callbacks) > 0 { + go func() { + result := <-d.result + for _, callback := range callbacks { + callback(result) + } + }() + return nil + } + return d.result +} + +type dialogFileFilter struct { + DisplayName string // Filter information EG: "Image Files (*.jpg, *.png)" + Pattern string // semicolon separated list of extensions, EG: "*.jpg;*.png" +} + +type OpenFileDialog struct { + app *App + window *Window + result chan []string + + id atomic.Uint64 + log logFunc + + title string + message string + buttonText string + directory string + filters []dialogFileFilter + canChooseDirectories bool + canChooseFiles bool + canCreateDirectories bool + showHiddenFiles bool + resolvesAliases bool + allowsMultipleSelection bool + hideExtension bool + canSelectHiddenExtension bool + treatsFilePackagesAsDirectories bool + allowsOtherFileTypes bool +} + +func (d *OpenFileDialog) run() { + if d.id.Load() == 0 { + id := getDialogID() + d.id.Store(id) + defer func() { + freeDialogID(id) + d.id.Store(0) + d.log("free", "id", id) + }() + d.log("open", "id", id, "title", d.title, "message", d.message, "directory", d.directory, "buttonText", d.buttonText, "filters", d.filters) + selections, err := d.app.thread.InvokeSyncWithResultAndError(func() (any, error) { + return runOpenFileDialog(d) + }) + if d.result != nil { + result, ok := selections.([]string) + if err != nil || !ok { + d.result <- []string{} + } else { + d.result <- result + } + close(d.result) + } + } +} + +func (d *OpenFileDialog) CanChooseFiles(canChooseFiles bool) *OpenFileDialog { + d.canChooseFiles = canChooseFiles + return d +} + +func (d *OpenFileDialog) CanChooseDirectories(canChooseDirectories bool) *OpenFileDialog { + d.canChooseDirectories = canChooseDirectories + return d +} + +func (d *OpenFileDialog) CanCreateDirectories(canCreateDirectories bool) *OpenFileDialog { + d.canCreateDirectories = canCreateDirectories + return d +} + +func (d *OpenFileDialog) AllowsOtherFileTypes(allowsOtherFileTypes bool) *OpenFileDialog { + d.allowsOtherFileTypes = allowsOtherFileTypes + return d +} +func (d *OpenFileDialog) AllowsMultipleSelection(allowsMultipleSelection bool) *OpenFileDialog { + d.allowsMultipleSelection = allowsMultipleSelection + return d +} + +func (d *OpenFileDialog) ShowHiddenFiles(showHiddenFiles bool) *OpenFileDialog { + d.showHiddenFiles = showHiddenFiles + return d +} + +func (d *OpenFileDialog) HideExtension(hideExtension bool) *OpenFileDialog { + d.hideExtension = hideExtension + return d +} + +func (d *OpenFileDialog) TreatsFilePackagesAsDirectories(treatsFilePackagesAsDirectories bool) *OpenFileDialog { + d.treatsFilePackagesAsDirectories = treatsFilePackagesAsDirectories + return d +} + +func (d *OpenFileDialog) ResolvesAliases(resolvesAliases bool) *OpenFileDialog { + d.resolvesAliases = resolvesAliases + return d +} + +// AddFilter adds a filter to the dialog. The filter is a display name and a semicolon separated list of extensions. +// EG: AddFilter("Image Files", "*.jpg;*.png") +func (d *OpenFileDialog) AddFilter(displayName, pattern string) *OpenFileDialog { + d.filters = append(d.filters, dialogFileFilter{ + DisplayName: strings.TrimSpace(displayName), + Pattern: strings.TrimSpace(pattern), + }) + return d +} + +func (d *OpenFileDialog) SetButtonText(text string) *OpenFileDialog { + d.buttonText = text + return d +} + +func (d *OpenFileDialog) SetDirectory(directory string) *OpenFileDialog { + d.directory = directory + return d +} + +func (d *OpenFileDialog) CanSelectHiddenExtension(canSelectHiddenExtension bool) *OpenFileDialog { + d.canSelectHiddenExtension = canSelectHiddenExtension + return d +} + +func (d *OpenFileDialog) Show(callbacks ...func([]string)) chan []string { + d.result = make(chan []string, 1) + d.app.started.run(d) + if len(callbacks) > 0 { + go func() { + result := <-d.result + for _, callback := range callbacks { + callback(result) + } + }() + return nil + } + return d.result +} + +type SaveFileDialog struct { + id atomic.Uint64 + log logFunc + + app *App + window *Window + result chan string + + canCreateDirectories bool + showHiddenFiles bool + canSelectHiddenExtension bool + allowOtherFileTypes bool + hideExtension bool + treatsFilePackagesAsDirectories bool + title string + message string + directory string + filename string + buttonText string + filters []dialogFileFilter +} + +func (d *SaveFileDialog) run() { + if d.id.Load() == 0 { + id := getDialogID() + d.id.Store(id) + defer func() { + freeDialogID(id) + d.id.Store(0) + d.log("free", "id", id) + }() + d.log("open", "id", id, "title", d.title, "message", d.message, "directory", d.directory, "filename", d.filename, "buttonText", d.buttonText, "filters", d.filters) + selections, err := d.app.thread.InvokeSyncWithResultAndError(func() (any, error) { + return runSaveFileDialog(d) + }) + if d.result != nil { + result, ok := selections.(string) + if err != nil || !ok { + d.result <- "" + } else { + d.result <- result + } + close(d.result) + } + } +} + +// AddFilter adds a filter to the dialog. The filter is a display name and a semicolon separated list of extensions. +// EG: AddFilter("Image Files", "*.jpg;*.png") +func (d *SaveFileDialog) AddFilter(displayName, pattern string) *SaveFileDialog { + d.filters = append(d.filters, dialogFileFilter{ + DisplayName: strings.TrimSpace(displayName), + Pattern: strings.TrimSpace(pattern), + }) + return d +} + +func (d *SaveFileDialog) CanCreateDirectories(canCreateDirectories bool) *SaveFileDialog { + d.canCreateDirectories = canCreateDirectories + return d +} + +func (d *SaveFileDialog) CanSelectHiddenExtension(canSelectHiddenExtension bool) *SaveFileDialog { + d.canSelectHiddenExtension = canSelectHiddenExtension + return d +} + +func (d *SaveFileDialog) ShowHiddenFiles(showHiddenFiles bool) *SaveFileDialog { + d.showHiddenFiles = showHiddenFiles + return d +} + +func (d *SaveFileDialog) SetDirectory(directory string) *SaveFileDialog { + d.directory = directory + return d +} + +func (d *SaveFileDialog) SetButtonText(text string) *SaveFileDialog { + d.buttonText = text + return d +} + +func (d *SaveFileDialog) SetFilename(filename string) *SaveFileDialog { + d.filename = filename + return d +} + +func (d *SaveFileDialog) AllowsOtherFileTypes(allowOtherFileTypes bool) *SaveFileDialog { + d.allowOtherFileTypes = allowOtherFileTypes + return d +} + +func (d *SaveFileDialog) HideExtension(hideExtension bool) *SaveFileDialog { + d.hideExtension = hideExtension + return d +} + +func (d *SaveFileDialog) TreatsFilePackagesAsDirectories(treatsFilePackagesAsDirectories bool) *SaveFileDialog { + d.treatsFilePackagesAsDirectories = treatsFilePackagesAsDirectories + return d +} + +func (d *SaveFileDialog) Show(callbacks ...func(string)) chan string { + d.result = make(chan string, 1) + d.app.started.run(d) + if len(callbacks) > 0 { + go func() { + result := <-d.result + for _, callback := range callbacks { + callback(result) + } + }() + return nil + } + return d.result +} + +func runChooserDialog(window windowPtr, allowMultiple, createFolders, showHidden bool, currentFolder, title string, action int, acceptLabel string, filters []dialogFileFilter) ([]string, error) { + GtkResponseCancel := 0 + GtkResponseAccept := 1 + + fc := lib.gtk.FileChooserDialogNew( + title, + window, + action, + "_Cancel", + GtkResponseCancel, + acceptLabel, + GtkResponseAccept, + 0) + + lib.gtk.FileChooserSetAction(fc, action) + + var gtkFilters []ptr + for _, filter := range filters { + f := lib.gtk.FileFilterNew() + lib.gtk.FileFilterSetName(f, filter.DisplayName) + lib.gtk.FileFilterAddPattern(f, filter.Pattern) + lib.gtk.FileChooserAddFilter(fc, f) + gtkFilters = append(gtkFilters, f) + } + lib.gtk.FileChooserSetSelectMultiple(fc, allowMultiple) + lib.gtk.FileChooserSetCreateFolders(fc, createFolders) + lib.gtk.FileChooserSetShowHidden(fc, showHidden) + + if currentFolder != "" { + lib.gtk.FileChooserSetCurrentFolder(fc, currentFolder) + } + + buildStringAndFree := func(s ptr) string { + bytes := []byte{} + p := unsafe.Pointer(s) + for { + val := *(*byte)(p) + if val == 0 { // this is the null terminator + break + } + bytes = append(bytes, val) + p = unsafe.Add(p, 1) + } + lib.g.Free(s) // so we don't have to iterate a second time + return string(bytes) + } + + response := lib.gtk.DialogRun(fc) + var selections []string + if response == GtkResponseAccept { + filenames := lib.gtk.FileChooserGetFilenames(fc) + iter := filenames + count := 0 + for { + selections = append(selections, buildStringAndFree(iter.data)) + iter = iter.next + if iter == nil || count == 1024 { + break + } + count++ + } + } + defer lib.gtk.WidgetDestroy(windowPtr(fc)) + return selections, nil +} + +// dialog related +func runOpenFileDialog(d *OpenFileDialog) ([]string, error) { + window := windowPtr(0) + if d.window != nil { + window = d.window.pointer + } + buttonText := d.buttonText + if buttonText == "" { + buttonText = "_Open" + } + return runChooserDialog( + window, + d.allowsMultipleSelection, + d.canCreateDirectories, + d.showHiddenFiles, + d.directory, + d.title, + 0, // GtkFileChooserActionOpen + buttonText, + d.filters) +} + +func runMessageDialog(d *MessageDialog) int { + window := windowPtr(0) + if d.window != nil { + window = d.window.pointer + } + + buttonMask := GtkButtonsOk + if len(d.buttons) > 0 { + buttonMask = GtkButtonsNone + } + dialog := lib.gtk.MessageDialogNew( + window, + GtkDialogModal|GtkDialogDestroyWithParent, + d.dtype, + buttonMask, + d.message) + + if d.title != "" { + lib.gtk.WindowSetTitle(dialog, d.title) + } + + GdkColorspaceRGB := 0 + if img, err := pngToImage(d.icon); err == nil { + gbytes := lib.g.BytesNewStatic(uintptr(unsafe.Pointer(&img.Pix[0])), len(img.Pix)) + + defer lib.g.BytesUnref(gbytes) + pixBuf := lib.gdk.PixbufNewFromBytes( + gbytes, + GdkColorspaceRGB, + 1, // has_alpha + 8, + img.Bounds().Dx(), + img.Bounds().Dy(), + img.Stride, + ) + image := lib.gtk.ImageNewFromPixbuf(pixBuf) + widgetSetVisible(image, false) + contentArea := lib.gtk.DialogGetContentArea(dialog) + lib.gtk.ContainerAdd(contentArea, image) + } + for i, button := range d.buttons { + lib.gtk.DialogAddButton( + dialog, + button.Label, + i, + ) + if button.IsDefault { + lib.gtk.DialogSetDefaultResponse(dialog, i) + } + } + defer lib.gtk.WidgetDestroy(dialog) + return lib.gtk.DialogRun(dialog) +} + +func runSaveFileDialog(d *SaveFileDialog) (string, error) { + window := windowPtr(0) + if d.window != nil { + window = d.window.pointer + } + buttonText := d.buttonText + if buttonText == "" { + buttonText = "_Save" + } + results, err := runChooserDialog( + window, + false, // multiple selection + d.canCreateDirectories, + d.showHiddenFiles, + d.directory, + d.title, + 1, // GtkFileChooserActionSave + buttonText, + d.filters) + + if err != nil || len(results) == 0 { + return "", err + } + return results[0], nil +} diff --git a/examples/dialog/dialog.go b/examples/dialog/dialog.go new file mode 100644 index 0000000..8eb7814 --- /dev/null +++ b/examples/dialog/dialog.go @@ -0,0 +1,116 @@ +package main + +import ( + ui "github.com/malivvan/webkitgtk" +) + +type DialogAPI struct { + app *ui.App +} + +type MessageDialog struct { + Title string `json:"title"` + Message string `json:"message"` + Actions []string `json:"actions"` +} + +type OpenDialog struct { + Title string `json:"title"` + Multiple bool `json:"multiple,omitempty"` +} + +type SaveDialog struct { + Title string `json:"title"` +} + +func (a *DialogAPI) Ask(req MessageDialog) (int, error) { + return <-a.app.Dialog().Ask(req.Title, req.Message, req.Actions...).Show(), nil +} +func (a *DialogAPI) Info(req MessageDialog) (int, error) { + return <-a.app.Dialog().Info(req.Title, req.Message).Show(), nil +} + +func (a *DialogAPI) Warn(req MessageDialog) (int, error) { + return <-a.app.Dialog().Warn(req.Title, req.Message).Show(), nil +} + +func (a *DialogAPI) Fail(req MessageDialog) (int, error) { + return <-a.app.Dialog().Fail(req.Title, req.Message).Show(), nil +} + +func (a *DialogAPI) Open(req OpenDialog) ([]string, error) { + return <-a.app.Dialog().Open(req.Title).AllowsMultipleSelection(req.Multiple).Show(), nil +} + +func (a *DialogAPI) Save(req SaveDialog) (string, error) { + return <-a.app.Dialog().Save(req.Title).Show(), nil +} + +func main() { + app := ui.New(ui.AppOptions{ + ID: "com.github.malivvan.webkitgtk.examples.dialog", + Name: "WebKitGTK Dialog Example", + }) + app.Open(ui.WindowOptions{ + Title: "dialog", + Width: 400, + Height: 220, + HTML: ` + + + + +
+
+ + + + + + + +
+ + + + `, + Define: map[string]interface{}{ + "dialog": &DialogAPI{app: app}, + }, + }) + if err := app.Run(); err != nil { + panic(err) + } +} diff --git a/examples/echo/echo.go b/examples/echo/echo.go index 46ae164..f644a39 100644 --- a/examples/echo/echo.go +++ b/examples/echo/echo.go @@ -12,11 +12,11 @@ func (a *API) Echo(msg string) (string, error) { func main() { app := ui.New(ui.AppOptions{ - Name: "echo", - Debug: true, + ID: "com.github.malivvan.webkitgtk.examples.api", + Name: "WebKitGTK API Example", }) app.Open(ui.WindowOptions{ - Title: "echo", + Title: "api", Width: 420, Height: 44, HTML: ` @@ -37,8 +37,7 @@ func main() { `, Define: map[string]interface{}{ - "appID": "echo", - "api": &API{}, + "api": &API{}, }, }) if err := app.Run(); err != nil { diff --git a/examples/handle/assets/favicon.ico b/examples/handle/assets/favicon.ico new file mode 100644 index 0000000..35cf258 Binary files /dev/null and b/examples/handle/assets/favicon.ico differ diff --git a/examples/handle/assets/index.html b/examples/handle/assets/index.html new file mode 100644 index 0000000..e97c1af --- /dev/null +++ b/examples/handle/assets/index.html @@ -0,0 +1,13 @@ + + + + + Handle Example + + + + + + Handle Example Error + + \ No newline at end of file diff --git a/examples/handle/assets/main.js b/examples/handle/assets/main.js new file mode 100644 index 0000000..52d765f --- /dev/null +++ b/examples/handle/assets/main.js @@ -0,0 +1,3 @@ +window.addEventListener('load', () => { + document.body.innerHTML = 'Handle Example'; +}); \ No newline at end of file diff --git a/examples/handle/assets/styles.css b/examples/handle/assets/styles.css new file mode 100644 index 0000000..9f9f9e9 --- /dev/null +++ b/examples/handle/assets/styles.css @@ -0,0 +1,5 @@ +body { + color:white; + background-color: black; + font-family: monospace; +} \ No newline at end of file diff --git a/examples/sudoku/sudoku.go b/examples/handle/handle.go similarity index 62% rename from examples/sudoku/sudoku.go rename to examples/handle/handle.go index d9b6c55..9b6b742 100644 --- a/examples/sudoku/sudoku.go +++ b/examples/handle/handle.go @@ -14,16 +14,14 @@ var assets embed.FS func main() { assets, _ := fs.Sub(assets, "assets") app := ui.New(ui.AppOptions{ - Name: "sudoku", - Debug: true, - Handle: map[string]http.Handler{ - "main": http.FileServer(http.FS(assets)), - }, + ID: "com.github.malivvan.webkitgtk.examples.handle", + Name: "WebKitGTK Handle Example", }) + app.Handle("main", http.FileServer(http.FS(assets))) app.Open(ui.WindowOptions{ - Title: "Sudoku", - Width: 600, - Height: 460, + Title: "Handle Example", + Width: 200, + Height: 40, URL: "app://main/", }) if err := app.Run(); err != nil { diff --git a/examples/notify/notify.go b/examples/notify/notify.go new file mode 100644 index 0000000..2b6f60d --- /dev/null +++ b/examples/notify/notify.go @@ -0,0 +1,64 @@ +package main + +import ( + _ "embed" + ui "github.com/malivvan/webkitgtk" +) + +type NotifyAPI struct { + app *ui.App +} + +type Notification struct { + Title string `json:"title"` + Body string `json:"body"` +} + +func (a *NotifyAPI) Notify(notification *Notification) (uint32, error) { + return a.app.Notify(notification.Title, notification.Body). + Action("open", func() { + println("action open") + }). + Closed(func() { + println("closed") + }).Show() + +} + +func main() { + + app := ui.New(ui.AppOptions{ + ID: "com.github.malivvan.webkitgtk.examples.notify", + Name: "WebKitGTK Notify Example", + }) + + app.Open(ui.WindowOptions{ + Title: "notify", + Width: 200, + Height: 90, + HTML: ` + + +
+
+ + + + `, + Define: map[string]interface{}{ + "api": &NotifyAPI{app: app}, + }, + }) + if err := app.Run(); err != nil { + panic(err) + } +} diff --git a/examples/sudoku/assets/main.js b/examples/sudoku/assets/main.js deleted file mode 100644 index c1c5dc0..0000000 --- a/examples/sudoku/assets/main.js +++ /dev/null @@ -1,135 +0,0 @@ -class Sudoku { - constructor(elem) { - this.elem = elem; - this.board = this.createBoard(); - this.controls = this.createControls(); - this.elem.appendChild(this.board) - this.elem.appendChild(this.controls) - } - - createControls() { - let controls = document.createElement("div"); - controls.classList.add("controls"); - let genButton = this.createButton("Generate", this.fillBoard.bind(this)); - let genAmount = this.createInput("number", 20, 0, 81); - controls.append(genButton, genAmount); - return controls; - } - - createButton(text, onClick) { - let button = document.createElement("button"); - button.innerText = text; - button.onclick = onClick; - return button; - } - - createInput(type, value, min, max) { - let input = document.createElement("input"); - input.type = type; - input.value = value; - input.min = min; - input.max = max; - return input; - } - - createBoard() { - let board = document.createElement('div'); - board.className = 'board'; - for (let i = 0; i < 9; i++) { - let row = document.createElement('div'); - row.className = 'row'; - for (let j = 0; j < 9; j++) { - let cell = this.createCell(i, j); - row.appendChild(cell); - } - board.appendChild(row); - } - return board; - } - - createCell(i, j) { - let cell = document.createElement('div'); - cell.className = 'cell'; - cell.id = `cell${i}${j}`; - cell.innerHTML = ' '; - cell.onclick = this.cellOnClick.bind(cell); - cell.oncontextmenu = this.cellOnRightClick.bind(cell); - return cell; - } - - cellOnClick() { - this.innerHTML = this.innerHTML === ' ' ? 1 : (parseInt(this.innerHTML) % 9) + 1; - } - - cellOnRightClick(e) { - e.preventDefault(); - this.innerHTML = this.innerHTML === ' ' ? 9 : (parseInt(this.innerHTML) - 1) || ' '; - } - - getValues(selector) { - let values = []; - for (let i = 0; i < 9; i++) { - values.push(parseInt(this.board.querySelector(selector(i)).innerHTML)); - } - return values; - } - - getRowValues = (row) => this.getValues(i => `#cell${row}${i}`); - getColValues = (col) => this.getValues(i => `#cell${i}${col}`); - - getValidValues(row, col) { - let rowValues = this.getRowValues(row); - let colValues = this.getColValues(col); - let squareValues = [1, 2, 3, 4, 5, 6, 7, 8, 9]; - return squareValues.filter(i => !rowValues.includes(i) && !colValues.includes(i)); - } - - getEmptyCells() { - let emptyCells = []; - for (let i = 0; i < 9; i++) { - for (let j = 0; j < 9; j++) { - if (this.board.querySelector(`#cell${i}${j}`).innerHTML === ' ') { - emptyCells.push([i, j]); - } - } - } - return emptyCells; - } - - fillCell(row, col) { - let cell = this.board.querySelector(`#cell${row}${col}`); - - let possibleValues = this.getValidValues(row, col); - if (possibleValues.length === 0) { - return false; - } - possibleValues.sort(() => Math.random() - 0.5); - cell.classList.add("cell-generated"); - cell.innerHTML = possibleValues.pop(); - return true; - } - - clearBoard() { - for (let i = 0; i < 9; i++) { - for (let j = 0; j < 9; j++) { - let cell = this.board.querySelector(`#cell${i}${j}`); - cell.classList.remove("cell-generated"); - cell.innerHTML = ' '; - } - } - } - - fillBoard() { - this.clearBoard(); - let emptyCells = this.getEmptyCells(); - emptyCells.sort(() => Math.random() - 0.5); - let amount = parseInt(this.controls.querySelector("input").value); - let n = 0; - while (n < amount) { - let [row, col] = emptyCells.pop(); - if (this.fillCell(row, col)) n++; - } - } -} - -let game = new Sudoku(document.body); \ No newline at end of file diff --git a/examples/systray/icon.png b/examples/systray/icon.png new file mode 100644 index 0000000..383a7e6 Binary files /dev/null and b/examples/systray/icon.png differ diff --git a/examples/systray/systray.go b/examples/systray/systray.go new file mode 100644 index 0000000..029732d --- /dev/null +++ b/examples/systray/systray.go @@ -0,0 +1,58 @@ +package main + +import ( + _ "embed" + ui "github.com/malivvan/webkitgtk" +) + +//go:embed icon.png +var icon []byte + +func main() { + app := ui.New(ui.AppOptions{ + ID: "com.github.malivvan.webkitgtk.examples.systray", + Name: "WebKitGTK Systray Example", + Hold: true, + }) + + menu := app.Menu(icon) + menu.Add("open").OnClick(func(checked bool) { + app.Open(ui.WindowOptions{ + Title: "tray", + Width: 160, + Height: 60, + HTML: ` + + + Tray Example + + `, + }) + }) + submenu := menu.AddSubmenu("submenu") + radio1 := submenu.AddRadio("radio 1", true).SetIcon(icon).OnClick(func(checked bool) { + println("radio 1", checked) + }) + radio2 := submenu.AddRadio("radio 2", false).SetIcon(icon).OnClick(func(checked bool) { + println("radio 2", checked) + }) + menu.AddSeparator() + menu.AddCheckbox("enable submenu items", true).SetIcon(icon).OnClick(func(checked bool) { + println("enable submenu items", checked) + radio1.SetDisabled(!checked) + radio2.SetDisabled(!checked) + }) + menu.AddCheckbox("show submenu", true).SetIcon(icon).OnClick(func(checked bool) { + println("show submenu", checked) + submenu.Item().SetHidden(!checked) + }) + menu.AddSeparator() + menu.Add("quit").OnClick(func(checked bool) { + println("quit") + app.Quit() + }) + + if err := app.Run(); err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod index 1bc8dac..e158556 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module github.com/malivvan/webkitgtk go 1.21.5 -require github.com/ebitengine/purego v0.6.0-alpha.2 +require ( + github.com/ebitengine/purego v0.6.0-alpha.4 + github.com/godbus/dbus/v5 v5.1.0 +) -require golang.org/x/sys v0.15.0 // indirect +require golang.org/x/sys v0.16.0 // indirect diff --git a/go.sum b/go.sum index f6bc6c9..39fcea7 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ -github.com/ebitengine/purego v0.6.0-alpha.2 h1:lYSvMtNBEjNGAzqPC5WP7bHUOxkFU3L+JZMdxK7krkw= -github.com/ebitengine/purego v0.6.0-alpha.2/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +github.com/ebitengine/purego v0.6.0-alpha.4 h1:mVb1sgZlxzZQ1tPu/XeKbWBRV9E5QobNltnwCzFoP2g= +github.com/ebitengine/purego v0.6.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/notify.go b/notify.go new file mode 100644 index 0000000..64b5877 --- /dev/null +++ b/notify.go @@ -0,0 +1,265 @@ +package webkitgtk + +import ( + "context" + "fmt" + "github.com/godbus/dbus/v5" + "sync" + "time" +) + +const ( + notifierDbusObjectPath = "/org/freedesktop/Notifications" + notifierDbusInterfacePath = "org.freedesktop.Notifications" +) + +type notificationAction struct { + label string + callback func() +} + +type Notification struct { + notifier *dbusNotify + + id uint32 // The notification ID. + replaceID uint32 // The optional notification to replace. + icon []byte // The optional icon data. + title string // The summary text briefly describing the notification. + message string // The optional detailed body text. + actions []notificationAction // The actions send a request message back to the notification client when invoked. + onClose []func() // The optional callback function to be called when the notification is closed. + hints map[string]interface{} // hints are a way to provide extra data to a notification server. + timeout time.Duration // The timeout since the display of the notification at which the notification should +} + +func (a *App) Notify(title, message string) *Notification { + return &Notification{ + notifier: a.notifier, + message: message, + title: title, + } +} + +func (n *Notification) Icon(icon []byte) *Notification { + n.icon = icon + return n +} + +func (n *Notification) Timeout(timeout time.Duration) *Notification { + n.timeout = timeout + return n +} + +func (n *Notification) Action(label string, callback func()) *Notification { + n.actions = append(n.actions, notificationAction{ + label: label, + callback: callback, + }) + return n +} + +func (n *Notification) Closed(callback func()) *Notification { + n.onClose = append(n.onClose, callback) + return n +} + +type dbusNotify struct { + log logFunc + conn *dbus.Conn + + appName string + appIcon string + + notifications_ sync.Mutex + notifications map[uint32]*Notification + + server string // Notification Server Name + vendor string // Notification Server Vendor + version string // Notification Server Version + specification string // Spec Version + + actionIcons bool // Supports using icons instead of text for displaying actions. + actions bool // The server will provide any specified actions to the user. + body bool // Supports body text. Some implementations may only show the summary. + bodyHyperlinks bool // The server supports hyperlinks in the notifications. + bodyImages bool // The server supports images in the notifications. + bodyMarkup bool // Supports markup in the body text. + iconMulti bool // The server will render an animation of all the frames in a given image array. + iconStatic bool // Supports display of exactly 1 frame of any given image array. + persistence bool // The server supports persistence of notifications. + sound bool // The server supports sounds on notifications. +} + +func (n *dbusNotify) Start(conn *dbus.Conn) error { + n.log = newLogFunc("dbus-notify") + n.conn = conn + n.notifications = make(map[uint32]*Notification) + ///////////// + + var d = make(chan *dbus.Call, 1) + var o = conn.Object(notifierDbusInterfacePath, notifierDbusObjectPath) + o.GoWithContext(context.Background(), + "org.freedesktop.Notifications.GetServerInformation", + 0, + d) + err := (<-d).Store(&n.server, + &n.vendor, + &n.version, + &n.specification) + if err != nil { + return fmt.Errorf("error getting notification server information: %w", err) + } + n.log("notification server information", "server", n.server, "vendor", n.vendor, "version", n.version, "specification", n.specification) + + //var d = make(chan *dbus.Call, 1) + //var o = n.dbus.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications") + var s = make([]string, 0) + o.GoWithContext(context.Background(), + "org.freedesktop.Notifications.GetCapabilities", + 0, + d) + err = (<-d).Store(&s) + if err != nil { + return fmt.Errorf("error getting notification server capabilities: %w", err) + } + for _, v := range s { + switch v { + case "action-icons": + n.actionIcons = true + break + case "actions": + n.actions = true + break + case "body": + n.body = true + break + case "body-hyperlinks": + n.bodyHyperlinks = true + break + case "body-images": + n.bodyImages = true + break + case "body-markup": + n.bodyMarkup = true + break + case "icon-multi": + n.iconMulti = true + break + case "icon-static": + n.iconStatic = true + break + case "persistence": + n.persistence = true + break + case "sound": + n.sound = true + break + } + } + + n.log("notification server capabilities", "action-icons", n.actionIcons, "actions", n.actions, "body", n.body, "body-hyperlinks", n.bodyHyperlinks, "body-images", n.bodyImages, "body-markup", n.bodyMarkup, "icon-multi", n.iconMulti, "icon-static", n.iconStatic, "persistence", n.persistence, "sound", n.sound) + n.log("started") + return nil +} + +// Show sends the information in the notification object to the server to be +// displayed. +func (n *Notification) Show() (uint32, error) { + + timeout := int32(-1) + if n.timeout > 0 { + timeout = int32(n.timeout.Milliseconds()) + } + + // We need to convert the interface type of the map to dbus.Variant as + // people dont want to have to import the dbus package just to make use + // of the notification hints. + hints := make(map[string]interface{}) + for k, v := range n.hints { + hints[k] = dbus.MakeVariant(v) + } + + var actions []string + if n.notifier.actions { + for _, v := range n.actions { + actions = append(actions, v.label) + actions = append(actions, v.label) + } + } + + var appIcon string + + var d = make(chan *dbus.Call, 1) + var o = n.notifier.conn.Object(notifierDbusInterfacePath, notifierDbusObjectPath) + o.GoWithContext(context.Background(), + "org.freedesktop.Notifications.Notify", + 0, + d, + n.notifier.appName, + n.replaceID, + appIcon, + n.title, + n.message, + actions, + hints, + timeout) + err := (<-d).Store(&n.id) + if err != nil { + return 0, fmt.Errorf("error showing notification: %w", err) + } + + n.notifier.notifications_.Lock() + n.notifier.notifications[n.id] = n + n.notifier.notifications_.Unlock() + + return n.id, nil +} +func (n *dbusNotify) Signal(sig *dbus.Signal) { + if sig.Path != notifierDbusObjectPath { + return + } + switch sig.Name { + case "org.freedesktop.Notifications.NotificationClosed": + id := sig.Body[0].(uint32) + + n.notifications_.Lock() + notification, ok := n.notifications[id] + delete(n.notifications, id) + n.notifications_.Unlock() + + n.log("notification closed", "id", id, "reason", sig.Body[1].(uint32)) + if !ok { + n.log("notification not found", "id", id) + return + } + + for _, onClose := range notification.onClose { + onClose() + } + + case "org.freedesktop.Notifications.ActionInvoked": + id := sig.Body[0].(uint32) + + n.notifications_.Lock() + notification, ok := n.notifications[id] + n.notifications_.Unlock() + + n.log("notification action invoked", "id", sig.Body[0].(uint32), "action", sig.Body[1].(string)) + if !ok { + n.log("notification not found", "id", id) + return + } + + action := sig.Body[1].(string) + for _, v := range notification.actions { + if v.label == action { + v.callback() + return + } + } + } +} + +func (n *dbusNotify) Stop() { + n.log("stopped") +} diff --git a/purego.go b/purego.go index 977939a..872104b 100644 --- a/purego.go +++ b/purego.go @@ -45,6 +45,10 @@ type ( } ) +var ( + nilRadioGroup gsListPtr = nil +) + type gError struct { domain uint32 code int @@ -91,6 +95,7 @@ var lib struct { ApplicationRegister func(ptr, ptr, ptr) ApplicationActivate func(ptr) GetApplicationName func() string + ApplicationIdIsValid func(string) bool ApplicationRelease func(ptr) ApplicationRun func(ptr, int, []string) int BytesNewStatic func(uintptr, int) uintptr @@ -149,19 +154,19 @@ var lib struct { ContainerAdd func(windowPtr, ptr) CssProviderLoadFromData func(ptr, string, int, ptr) CssProviderNew func() ptr - DialogAddButton func(ptr, string, int) - DialogGetContentArea func(ptr) ptr - DialogRun func(ptr) int - DialogSetDefaultResponse func(ptr, int) + DialogAddButton func(windowPtr, string, int) + DialogGetContentArea func(windowPtr) windowPtr + DialogRun func(windowPtr) int + DialogSetDefaultResponse func(windowPtr, int) DragDestSet func(webviewPtr, uint, ptr, uint, uint) - FileChooserAddFilter func(ptr, ptr) - FileChooserDialogNew func(string, ptr, int, string, int, string, int, ptr) ptr - FileChooserGetFilenames func(ptr) *gsList - FileChooserSetAction func(ptr, int) - FileChooserSetCreateFolders func(ptr, bool) - FileChooserSetCurrentFolder func(ptr, string) - FileChooserSetSelectMultiple func(ptr, bool) - FileChooserSetShowHidden func(ptr, bool) + FileChooserAddFilter func(windowPtr, ptr) + FileChooserDialogNew func(string, windowPtr, int, string, int, string, int, ptr) windowPtr + FileChooserGetFilenames func(windowPtr) *gsList + FileChooserSetAction func(windowPtr, int) + FileChooserSetCreateFolders func(windowPtr, bool) + FileChooserSetCurrentFolder func(windowPtr, string) + FileChooserSetSelectMultiple func(windowPtr, bool) + FileChooserSetShowHidden func(windowPtr, bool) FileFilterAddPattern func(ptr, string) FileFilterNew func() ptr FileFilterSetName func(ptr, string) @@ -172,32 +177,32 @@ var lib struct { MenuItemSetSubmenu func(ptr, ptr) MenuNew func() ptr MenuShellAppend func(ptr, ptr) - MessageDialogNew func(ptr, int, int, int, string) ptr - //RadioMenuItemGetGroup func(ptr) gsListPtr - //RadioMenuItemNewWithLabel func(gsListPtr, string) ptr - SeparatorMenuItemNew func() ptr - StyleContextAddProvider func(ptr, ptr, int) - TargetEntryFree func(ptr) - TargetEntryNew func(string, int, uint) ptr - WidgetDestroy func(windowPtr) - WidgetGetDisplay func(windowPtr) ptr - WidgetGetScreen func(windowPtr) ptr - WidgetGetStyleContext func(windowPtr) ptr - WidgetGetWindow func(windowPtr) windowPtr - WidgetHide func(ptr) - WidgetIsVisible func(ptr) bool - WidgetShow func(ptr) - WidgetShowAll func(windowPtr) - WidgetSetAppPaintable func(windowPtr, int) - WidgetSetName func(ptr, string) - WidgetSetSensitive func(ptr, int) - WidgetSetTooltipText func(windowPtr, string) - WidgetSetVisual func(windowPtr, ptr) - WindowClose func(windowPtr) - WindowFullscreen func(windowPtr) - WindowGetPosition func(windowPtr, *int, *int) bool - WindowGetSize func(windowPtr, *int, *int) - WindowHasToplevelFocus func(windowPtr) int + MessageDialogNew func(windowPtr, int, int, int, string) windowPtr + RadioMenuItemGetGroup func(ptr) gsListPtr + RadioMenuItemNewWithLabel func(gsListPtr, string) ptr + SeparatorMenuItemNew func() ptr + StyleContextAddProvider func(ptr, ptr, int) + TargetEntryFree func(ptr) + TargetEntryNew func(string, int, uint) ptr + WidgetDestroy func(windowPtr) + WidgetGetDisplay func(windowPtr) ptr + WidgetGetScreen func(windowPtr) ptr + WidgetGetStyleContext func(windowPtr) ptr + WidgetGetWindow func(windowPtr) windowPtr + WidgetHide func(ptr) + WidgetIsVisible func(ptr) bool + WidgetShow func(ptr) + WidgetShowAll func(windowPtr) + WidgetSetAppPaintable func(windowPtr, int) + WidgetSetName func(ptr, string) + WidgetSetSensitive func(ptr, int) + WidgetSetTooltipText func(windowPtr, string) + WidgetSetVisual func(windowPtr, ptr) + WindowClose func(windowPtr) + WindowFullscreen func(windowPtr) + WindowGetPosition func(windowPtr, *int, *int) bool + WindowGetSize func(windowPtr, *int, *int) + WindowHasToplevelFocus func(windowPtr) int //WindowKeepAbove func(pointer, bool) WindowMaximize func(windowPtr) WindowIconify func(windowPtr) @@ -382,6 +387,7 @@ var lib struct { WebContextGetWebsiteDataManager func(ptr) ptr CookieManagerSetPersistentStorage func(ptr, string, int) + CookieManagerSetAcceptPolicy func(ptr, int) WebContextGetCookieManager func(ptr) ptr WebViewGetUserContentManager func(webviewPtr) userContentManagerPtr @@ -392,6 +398,7 @@ var lib struct { WebsiteDataManagerGetLocalStorageDirectory func(ptr) string WebsiteDataManagerSetPersistentCredentialStorageEnabled func(ptr, bool) WebContextNewWithWebsiteDataManager func(ptr) ptr + WebContextSetCacheModel func(ptr, int) WebContextGetSandboxEnabled func(ptr) bool WebContextSetSandboxEnabled func(ptr, bool) WebContextAddPathToSandbox func(ptr, string) diff --git a/release.go b/release.go new file mode 100644 index 0000000..71eb110 --- /dev/null +++ b/release.go @@ -0,0 +1,8 @@ +//go:build release + +package webkitgtk + +func init() { + _RELEASE = true + LogWriter = nil +} diff --git a/settings.go b/settings.go index 856abf0..dc004f4 100644 --- a/settings.go +++ b/settings.go @@ -1,22 +1,18 @@ package webkitgtk -import ( - "net/http" -) - type AppOptions struct { - // The name of the app. - Name string + // ID is the unique identifier of the app in reverse domain notation. e.g. com.github.malivvan.webkitgtk + ID string - // Debug mode. - Debug bool + // Name is the name of the app. + Name string // Hold the app open after the last window is closed. Hold bool - // Handle internal app:// requests. - Handle map[string]http.Handler + // The icon of the app. + Icon []byte // Ephemeral mode disables all persistent storage. Ephemeral bool diff --git a/systray.go b/systray.go new file mode 100644 index 0000000..860bb34 --- /dev/null +++ b/systray.go @@ -0,0 +1,786 @@ +package webkitgtk + +import ( + _ "embed" + "fmt" + "github.com/ebitengine/purego" + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + "github.com/godbus/dbus/v5/prop" + "image" + "os" + "sync" +) + +const ( + dbusTrayItemPath = "/StatusNotifierItem" + dbusTrayMenuPath = "/StatusNotifierMenu" +) + +type dbusSystray struct { + log logFunc + label string + icon []byte + onOpen func() + onClose func() + menu *TrayMenu + conn *dbus.Conn + props *prop.Properties + menuProps *prop.Properties + menuVersion uint32 // need to bump this anytime we change anything + itemMap_ sync.Mutex + itemMap map[int32]*MenuItem +} + +// dbusMenu is a named struct to map into generated bindings. +// It represents the layout of a menu item +type dbusItem = struct { + V0 int32 // items' unique id + V1 map[string]dbus.Variant // layout properties + V2 []dbus.Variant // child menu(s) +} + +func (s *dbusSystray) processMenu(menu *TrayMenu, parentItem *MenuItem) { + + for _, item := range menu.items { + s.setMenuItem(item) + + item.dbusItem = &dbusItem{ + V0: item.id, + V1: map[string]dbus.Variant{}, + V2: []dbus.Variant{}, + } + + item.dbusItem.V1["enabled"] = dbus.MakeVariant(!item.disabled) + item.dbusItem.V1["visible"] = dbus.MakeVariant(!item.hidden) + if item.label != "" { + item.dbusItem.V1["label"] = dbus.MakeVariant(item.label) + } + if item.icon != nil { + item.dbusItem.V1["icon-data"] = dbus.MakeVariant(item.icon) + } + + switch item.itemType { + case checkbox: + item.dbusItem.V1["toggle-type"] = dbus.MakeVariant("checkmark") + v := dbus.MakeVariant(0) + if item.checked { + v = dbus.MakeVariant(1) + } + item.dbusItem.V1["toggle-state"] = v + case submenu: + item.dbusItem.V1["children-display"] = dbus.MakeVariant("submenu") + s.processMenu(item.submenu, item) + case text: + case radio: + item.dbusItem.V1["toggle-type"] = dbus.MakeVariant("radio") + v := dbus.MakeVariant(0) + if item.checked { + v = dbus.MakeVariant(1) + } + item.dbusItem.V1["toggle-state"] = v + case separator: + item.dbusItem.V1["type"] = dbus.MakeVariant("separator") + } + + parentItem.dbusItem.V2 = append(parentItem.dbusItem.V2, dbus.MakeVariant(item.dbusItem)) + } +} + +func (s *dbusSystray) refresh() { + s.menuVersion++ + if err := s.menuProps.Set("com.canonical.dbusmenu", "Version", + dbus.MakeVariant(s.menuVersion)); err != nil { + fmt.Errorf("systray error: failed to update menu version: %v", err) + return + } + if err := dbusEmit(s.conn, &dbusMenuLayoutUpdatedSignal{ + Path: dbusTrayMenuPath, + Body: &dbusMenuLayoutUpdatedSignalBody{ + Revision: s.menuVersion, + }, + }); err != nil { + fmt.Errorf("systray error: failed to emit layout updated signal: %v", err) + } +} + +func (s *dbusSystray) Start(conn *dbus.Conn) error { + err := dbusExportStatusNotifierItem(conn, dbusTrayItemPath, s) + if err != nil { + return fmt.Errorf("systray error: failed to export status notifier item: %v", err) + } + err = dbusExportMenu(conn, dbusTrayMenuPath, s) + if err != nil { + return fmt.Errorf("systray error: failed to export dbusmenu: %v", err) + } + + name := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", os.Getpid()) // register id 1 for this process + _, err = conn.RequestName(name, dbus.NameFlagDoNotQueue) + if err != nil { + fmt.Errorf("systray error: failed to request name: %s\n", err) + } + props, err := prop.Export(conn, dbusTrayItemPath, s.createPropSpec()) + if err != nil { + return fmt.Errorf("systray error: failed to export notifier properties to bus: %s\n", err) + } + menuProps, err := prop.Export(conn, dbusTrayMenuPath, s.createMenuPropSpec()) + if err != nil { + return fmt.Errorf("systray error: failed to export menu properties to bus: %s\n", err) + } + + s.conn = conn + s.props = props + s.menuProps = menuProps + + node := introspect.Node{ + Name: dbusTrayItemPath, + Interfaces: []introspect.Interface{ + introspect.IntrospectData, + prop.IntrospectData, + dbusStatusNotifierItemIntrospectData, + }, + } + + err = conn.Export(introspect.NewIntrospectable(&node), dbusTrayItemPath, "org.freedesktop.DBus.Introspectable") + if err != nil { + return fmt.Errorf("systray error: failed to export node introspection: %s\n", err) + } + menuNode := introspect.Node{ + Name: dbusTrayMenuPath, + Interfaces: []introspect.Interface{ + introspect.IntrospectData, + prop.IntrospectData, + dbusMenuIntrospectData, + }, + } + err = conn.Export(introspect.NewIntrospectable(&menuNode), dbusTrayMenuPath, + "org.freedesktop.DBus.Introspectable") + if err != nil { + return fmt.Errorf("systray error: failed to export menu node introspection: %s\n", err) + } + + s.register() + + if err := conn.AddMatchSignal( + dbus.WithMatchObjectPath("/org/freedesktop/DBus"), + dbus.WithMatchInterface("org.freedesktop.DBus"), + dbus.WithMatchSender("org.freedesktop.DBus"), + dbus.WithMatchMember("NameOwnerChanged"), + dbus.WithMatchArg(0, "org.kde.StatusNotifierWatcher"), + ); err != nil { + return fmt.Errorf("systray error: failed to register signal matching: %v\n", err) + } + + // init menu + rootItem := &MenuItem{ + tray: s, + dbusItem: &dbusItem{ + V0: int32(0), + V1: map[string]dbus.Variant{}, + V2: []dbus.Variant{}, + }, + } + s.itemMap = map[int32]*MenuItem{0: rootItem} + s.menu.processRadioGroups() + s.processMenu(s.menu, rootItem) + s.refresh() + + s.log("started") + return nil +} + +func (s *dbusSystray) Signal(sig *dbus.Signal) { + if sig == nil { + return // We get a nil signal when closing the window. + } + // sig.Body has the args, which are [name old_owner new_owner] + if sig.Body[2] != "" { + s.register() + } +} + +func (s *dbusSystray) Stop() { + s.log("stopped") +} + +func (s *dbusSystray) createMenuPropSpec() map[string]map[string]*prop.Prop { + return map[string]map[string]*prop.Prop{ + "com.canonical.dbusmenu": { + // update version each time we change something + "Version": { + Value: s.menuVersion, + Writable: true, + Emit: prop.EmitTrue, + Callback: nil, + }, + "TextDirection": { + Value: "ltr", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Status": { + Value: "normal", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "IconThemePath": { + Value: []string{}, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + }, + } +} + +func (s *dbusSystray) createPropSpec() map[string]map[string]*prop.Prop { + props := map[string]*prop.Prop{ + "Status": { + Value: "Active", // Passive, Active or NeedsAttention + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Title": { + Value: s.label, + Writable: true, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Id": { + Value: s.label, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Category": { + Value: "ApplicationStatus", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "IconData": { + Value: "", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + + "IconName": { + Value: "", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "IconThemePath": { + Value: "", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "ItemIsMenu": { + Value: true, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Menu": { + Value: dbus.ObjectPath(dbusTrayMenuPath), + Writable: true, + Emit: prop.EmitTrue, + Callback: nil, + }, + } + + if iconPx, err := iconToPX(s.icon); err == nil { + props["IconPixmap"] = &prop.Prop{ + Value: []iconPX{iconPx}, + Writable: true, + Emit: prop.EmitTrue, + Callback: nil, + } + } + + return map[string]map[string]*prop.Prop{ + "org.kde.StatusNotifierItem": props, + } +} + +func (s *dbusSystray) register() bool { + obj := s.conn.Object("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher") + call := obj.Call("org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem", 0, dbusTrayItemPath) + if call.Err != nil { + s.log("systray error: failed to register", "error", call.Err) + return false + } + + return true +} + +// AboutToShow is an implementation of the com.canonical.dbusmenu.AboutToShow method. +func (s *dbusSystray) AboutToShow(id int32) (needUpdate bool, err *dbus.Error) { + return +} + +// AboutToShowGroup is an implementation of the com.canonical.dbusmenu.AboutToShowGroup method. +func (s *dbusSystray) AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) { + return +} + +// GetProperty is an implementation of the com.canonical.dbusmenu.GetProperty method. +func (s *dbusSystray) GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) { + if item, ok := s.getMenuItem(id); ok { + if p, ok := item.dbusItem.V1[name]; ok { + return p, nil + } + } + return +} + +// Event is com.canonical.dbusmenu.Event method. +func (s *dbusSystray) Event(id int32, eventID string, data dbus.Variant, timestamp uint32) (err *dbus.Error) { + s.log("event", "id", id, "eventID", eventID, "data", data, "timestamp", timestamp) + if eventID == "clicked" { + if item, ok := s.getMenuItem(id); ok { + go item.handleClick() + } + } + return +} + +// EventGroup is an implementation of the com.canonical.dbusmenu.EventGroup method. +func (s *dbusSystray) EventGroup(events []struct { + V0 int32 + V1 string + V2 dbus.Variant + V3 uint32 +}) (idErrors []int32, err *dbus.Error) { + for _, event := range events { + if event.V1 == "clicked" { + item, ok := s.getMenuItem(event.V0) + if ok { + item.handleClick() + } + } + } + return +} + +// GetGroupProperties is an implementation of the com.canonical.dbusmenu.GetGroupProperties method. +func (s *dbusSystray) GetGroupProperties(ids []int32, propertyNames []string) (properties []struct { + V0 int32 + V1 map[string]dbus.Variant +}, err *dbus.Error) { + for _, id := range ids { + if m, ok := s.getMenuItem(id); ok { + p := struct { + V0 int32 + V1 map[string]dbus.Variant + }{ + V0: m.dbusItem.V0, + V1: make(map[string]dbus.Variant, len(m.dbusItem.V1)), + } + for k, v := range m.dbusItem.V1 { + p.V1[k] = v + } + properties = append(properties, p) + } + } + return properties, nil +} + +// GetLayout is an implementation of the com.canonical.dbusmenu.GetLayout method. +func (s *dbusSystray) GetLayout(parentID int32, recursionDepth int32, propertyNames []string) (revision uint32, layout dbusItem, err *dbus.Error) { + if m, ok := s.getMenuItem(parentID); ok { + return s.menuVersion, *m.dbusItem, nil + } + return +} + +// Activate implements org.kde.StatusNotifierItem.Activate method. +func (s *dbusSystray) Activate(x int32, y int32) (err *dbus.Error) { + s.log("Activate", x, y) + return +} + +// ContextMenu is org.kde.StatusNotifierItem.ContextMenu method +func (s *dbusSystray) ContextMenu(x int32, y int32) (err *dbus.Error) { + s.log("ContextMenu", x, y) + return +} + +func (s *dbusSystray) Scroll(delta int32, orientation string) (err *dbus.Error) { + s.log("Scroll", delta, orientation) + return +} + +// SecondaryActivate implements org.kde.StatusNotifierItem.SecondaryActivate method. +func (s *dbusSystray) SecondaryActivate(x int32, y int32) (err *dbus.Error) { + s.log("SecondaryActivate", x, y) + return +} + +type TrayMenu struct { + item *MenuItem + items []*MenuItem + label string + native ptr +} + +func (m *TrayMenu) toTray(label string, icon []byte) *dbusSystray { + if icon == nil { + icon = defaultIcon + } + return &dbusSystray{ + log: newLogFunc("dbus-systray"), + menu: m, + label: label, + icon: icon, + menuVersion: 1, + } +} + +func (m *TrayMenu) Add(label string) *MenuItem { + result := &MenuItem{ + label: label, + itemType: text, + } + m.items = append(m.items, result) + return result +} + +func (m *TrayMenu) AddSeparator() { + result := &MenuItem{ + itemType: separator, + } + m.items = append(m.items, result) +} + +func (m *TrayMenu) AddCheckbox(label string, checked bool) *MenuItem { + result := &MenuItem{ + label: label, + checked: checked, + itemType: checkbox, + } + m.items = append(m.items, result) + return result +} + +func (m *TrayMenu) AddRadio(label string, checked bool) *MenuItem { + result := &MenuItem{ + label: label, + checked: checked, + itemType: radio, + } + m.items = append(m.items, result) + return result +} + +func (m *TrayMenu) Update() { + m.processRadioGroups() + + if m.native == 0 { + m.native = lib.gtk.MenuNew() + } + m.update() +} + +func (m *TrayMenu) AddSubmenu(label string) *TrayMenu { + result := &MenuItem{ + label: label, + itemType: submenu, + } + result.submenu = &TrayMenu{ + item: result, + label: label, + } + m.items = append(m.items, result) + return result.submenu +} + +func (m *TrayMenu) Item() *MenuItem { + return m.item +} + +func (m *TrayMenu) processRadioGroups() { + var radioGroup []*MenuItem + for _, item := range m.items { + if item.itemType == submenu { + item.submenu.processRadioGroups() + continue + } + if item.itemType == radio { + radioGroup = append(radioGroup, item) + } else { + if len(radioGroup) > 0 { + for _, item := range radioGroup { + item.radioGroupMembers = radioGroup + } + radioGroup = nil + } + } + } + if len(radioGroup) > 0 { + for _, item := range radioGroup { + item.radioGroupMembers = radioGroup + } + } +} + +func (m *TrayMenu) SetLabel(label string) { + m.label = label +} + +func (m *TrayMenu) update() { + processMenu(m) +} + +func processMenu(m *TrayMenu) { + if m.native == 0 { + m.native = lib.gtk.MenuNew() + } + var currentRadioGroup gsListPtr + + for _, item := range m.items { + // drop the group if we have run out of radio items + if item.itemType != radio { + currentRadioGroup = nilRadioGroup + } + + switch item.itemType { + case submenu: + m.native = lib.gtk.MenuItemNewWithLabel(item.label) + processMenu(item.submenu) + item.submenu.native = lib.gtk.MenuNew() + lib.gtk.MenuItemSetSubmenu(item.native, item.submenu.native) + lib.gtk.MenuShellAppend(m.native, item.native) + case checkbox: + item.native = lib.gtk.CheckMenuItemNewWithLabel(item.label) + item.setChecked(item.checked) + item.setDisabled(item.disabled) + lib.gtk.MenuShellAppend(m.native, item.native) + case text: + item.native = lib.gtk.MenuItemNewWithLabel(item.label) + item.setDisabled(item.disabled) + lib.gtk.MenuShellAppend(m.native, item.native) + case radio: + item.native = lib.gtk.RadioMenuItemNewWithLabel(currentRadioGroup, item.label) + item.setChecked(item.checked) + item.setDisabled(item.disabled) + lib.gtk.MenuShellAppend(m.native, item.native) + currentRadioGroup = lib.gtk.RadioMenuItemGetGroup(item.native) + case separator: + lib.gtk.MenuShellAppend(m.native, lib.gtk.SeparatorMenuItemNew()) + } + + } + for _, item := range m.items { + if item.callback != nil { + handler := func() { + item := item + switch item.itemType { + case text, checkbox: + //menuItemClicked <- item.id + //println("text clicked") + case radio: + if lib.gtk.CheckMenuItemGetActive(item.native) == 1 { + //menuItemClicked <- item.id + //println("radio clicked") + } + } + } + item.handlerId = lib.g.SignalConnectObject( + item.native, + "activate", + ptr(purego.NewCallback(handler)), + item.native, + 0) + } + } +} + +type menuItemType int + +const ( + text menuItemType = iota + separator + checkbox + radio + submenu +) + +type MenuItem struct { + tray *dbusSystray + + id int32 + label string + tooltip string + disabled bool + checked bool + hidden bool + icon []byte + submenu *TrayMenu + callback func(bool) + itemType menuItemType + + dbusItem *dbusItem + native ptr + handlerId uint + radioGroupMembers []*MenuItem +} + +func (s *dbusSystray) setMenuItem(item *MenuItem) { + s.itemMap_.Lock() + defer s.itemMap_.Unlock() + item.tray = s + if item.id == 0 { + item.id = int32(len(s.itemMap)) + } + s.itemMap[item.id] = item +} + +func (s *dbusSystray) getMenuItem(id int32) (*MenuItem, bool) { + s.itemMap_.Lock() + defer s.itemMap_.Unlock() + item, ok := s.itemMap[id] + return item, ok +} + +func (item *MenuItem) handleClick() { + if item.itemType == checkbox { + item.checked = !item.checked + item.setChecked(item.checked) + } + if item.itemType == radio { + for _, member := range item.radioGroupMembers { + member.checked = false + if member != nil { + member.setChecked(false) + } + } + item.checked = true + if item != nil { + item.setChecked(true) + } + } + if item.callback != nil { + go item.callback(item.checked) + } +} +func (item *MenuItem) SetLabel(label string) *MenuItem { + item.label = label + if item.dbusItem != nil { + item.dbusItem.V1["label"] = dbus.MakeVariant(item.label) + item.tray.refresh() + } + return item +} + +func (item *MenuItem) SetDisabled(disabled bool) *MenuItem { + item.disabled = disabled + if item.dbusItem != nil { + item.setDisabled(item.disabled) + } + return item +} + +func (item *MenuItem) SetIcon(icon []byte) *MenuItem { + item.icon = icon + if item.dbusItem != nil { + item.dbusItem.V1["icon-data"] = dbus.MakeVariant(item.icon) + item.tray.refresh() + } + return item +} + +func (item *MenuItem) SetChecked(checked bool) *MenuItem { + item.checked = checked + if item.dbusItem != nil { + item.setChecked(item.checked) + } + return item +} + +func (item *MenuItem) SetHidden(hidden bool) *MenuItem { + item.hidden = hidden + if item.dbusItem != nil { + item.dbusItem.V1["visible"] = dbus.MakeVariant(!item.hidden) + item.tray.refresh() + } + return item +} + +func (item *MenuItem) Checked() bool { + return item.checked +} + +func (item *MenuItem) IsSeparator() bool { + return item.itemType == separator +} + +func (item *MenuItem) IsSubmenu() bool { + return item.itemType == submenu +} + +func (item *MenuItem) IsCheckbox() bool { + return item.itemType == checkbox +} + +func (item *MenuItem) IsRadio() bool { + return item.itemType == radio +} + +func (item *MenuItem) Hidden() bool { + return item.hidden +} + +func (item *MenuItem) OnClick(f func(bool)) *MenuItem { + item.callback = f + return item +} + +func (item *MenuItem) Label() string { + return item.label +} + +func (item *MenuItem) Enabled() bool { + return !item.disabled +} + +func (item *MenuItem) setDisabled(disabled bool) { + v := dbus.MakeVariant(!disabled) + if item.dbusItem.V1["toggle-state"] != v { + item.dbusItem.V1["enabled"] = v + item.tray.refresh() + } +} + +func (item *MenuItem) setChecked(checked bool) { + v := dbus.MakeVariant(0) + if checked { + v = dbus.MakeVariant(1) + } + if item.dbusItem.V1["toggle-state"] != v { + item.dbusItem.V1["toggle-state"] = v + item.tray.refresh() + } +} + +func ToARGB(img *image.RGBA) (int, int, []byte) { + w, h := img.Bounds().Dx(), img.Bounds().Dy() + data := make([]byte, w*h*4) + i := 0 + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + r, g, b, a := img.At(x, y).RGBA() + data[i] = byte(a) + data[i+1] = byte(r) + data[i+2] = byte(g) + data[i+3] = byte(b) + i += 4 + } + } + return w, h, data +} diff --git a/thread.go b/thread.go deleted file mode 100644 index 1a835f9..0000000 --- a/thread.go +++ /dev/null @@ -1,82 +0,0 @@ -package webkitgtk - -import ( - "github.com/ebitengine/purego" - "sync" -) - -var mainThreadId uint64 -var mainThreadFunctionStore = make(map[uint]func()) -var mainThreadFunctionStoreLock sync.RWMutex - -func isOnMainThread() bool { - return mainThreadId == lib.g.ThreadSelf() -} - -func generateFunctionStoreID() uint { - startID := 0 - for { - if _, ok := mainThreadFunctionStore[uint(startID)]; !ok { - return uint(startID) - } - startID++ - if startID == 0 { - fatal("Too many functions have been dispatched to the main thread") - } - } -} - -func invokeSync(fn func()) { - var wg sync.WaitGroup - wg.Add(1) - globalApplication.dispatchOnMainThread(func() { - defer processPanicHandlerRecover() - fn() - wg.Done() - }) - wg.Wait() -} - -func processPanicHandlerRecover() { - h := PanicHandler - if h == nil { - return - } - - if err := recover(); err != nil { - h(err) - } -} -func (a *App) dispatchOnMainThread(fn func()) { - // If we are on the main thread, just call the function - if isOnMainThread() { - fn() - return - } - - mainThreadFunctionStoreLock.Lock() - id := generateFunctionStoreID() - mainThreadFunctionStore[id] = fn - mainThreadFunctionStoreLock.Unlock() - - // Call platform specific dispatch function - dispatchOnMainThread(id) -} - -func dispatchOnMainThread(id uint) { - lib.g.IdleAdd(purego.NewCallback(func(ptr) int { - executeOnMainThread(id) - return gSourceRemove - })) -} - -func executeOnMainThread(callbackID uint) { - mainThreadFunctionStoreLock.RLock() - fn := mainThreadFunctionStore[callbackID] - if fn == nil { - fatal("dispatchCallback called with invalid id: %v", callbackID) - } - delete(mainThreadFunctionStore, callbackID) - mainThreadFunctionStoreLock.RUnlock() - fn() -} diff --git a/webkitgtk.go b/webkitgtk.go new file mode 100644 index 0000000..f5cf7cb --- /dev/null +++ b/webkitgtk.go @@ -0,0 +1,267 @@ +package webkitgtk + +import ( + "bytes" + _ "embed" + "fmt" + "github.com/ebitengine/purego" + "image" + "image/draw" + "image/png" + "os" + "strconv" + "strings" + "sync" +) + +var _RELEASE = false + +const uriScheme = "app" + +var PanicHandler = func(v any) { + panic(v) +} + +func panicHandlerRecover() { + h := PanicHandler + if h == nil { + return + } + if err := recover(); err != nil { + h(err) + } +} + +//go:embed examples/systray/icon.png +var defaultIcon []byte + +type iconPX struct { + W, H int + Pix []byte +} + +func iconToPX(icon []byte) (iconPX, error) { + img, err := pngToImage(icon) + if err != nil { + return iconPX{}, err + } + w, h, pix := imageToARGB(img) + return iconPX{ + W: w, + H: h, + Pix: pix, + }, nil +} +func pngToImage(data []byte) (*image.RGBA, error) { + img, err := png.Decode(bytes.NewReader(data)) + if err != nil { + return nil, err + } + + bounds := img.Bounds() + rgba := image.NewRGBA(bounds) + draw.Draw(rgba, bounds, img, bounds.Min, draw.Src) + return rgba, nil +} + +func imageToARGB(img *image.RGBA) (int, int, []byte) { + w, h := img.Bounds().Dx(), img.Bounds().Dy() + data := make([]byte, w*h*4) + i := 0 + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + r, g, b, a := img.At(x, y).RGBA() + data[i] = byte(a) + data[i+1] = byte(r) + data[i+2] = byte(g) + data[i+3] = byte(b) + i += 4 + } + } + return w, h, data +} + +type WebkitCookiePolicy int + +const ( + CookiesAcceptAll WebkitCookiePolicy = iota + CookiesRejectAll + CookiesNoThirdParty +) + +type WebkitCacheModel int + +const ( + CacheNone WebkitCacheModel = iota + CacheLite + CacheFull +) + +var LogWriter = os.Stderr + +type logFunc func(msg interface{}, keyvals ...interface{}) + +func newLogFunc(prefix string) logFunc { + return func(msg interface{}, keyvals ...interface{}) { + if LogWriter == nil { + return + } + var s strings.Builder + s.WriteString(prefix) + s.WriteString(": ") + s.WriteString(fmt.Sprintf("%v", msg)) + for i := 0; i < len(keyvals); i += 2 { + s.WriteString(" ") + s.WriteString(fmt.Sprintf("%v", keyvals[i])) + s.WriteString("=") + s.WriteString(fmt.Sprintf("%v", keyvals[i+1])) + } + s.WriteString("\n") + _, _ = LogWriter.Write([]byte(s.String())) + } +} + +type deferredRunner struct { + mutex sync.Mutex + running bool + runnables []runnable +} + +type runnable interface { + run() +} + +func (r *deferredRunner) run(runnable runnable) { + r.mutex.Lock() + defer r.mutex.Unlock() + if r.running { + runnable.run() + } else { + r.runnables = append(r.runnables, runnable) + } +} + +func (r *deferredRunner) invoke() { + r.mutex.Lock() + defer r.mutex.Unlock() + r.running = true + for _, runnable := range r.runnables { + go runnable.run() + } + r.runnables = nil +} + +type mainThread struct { + sync.Mutex + id uint64 + fnMap map[uint16]func() +} + +func newMainThread() *mainThread { + return &mainThread{ + id: lib.g.ThreadSelf(), + fnMap: make(map[uint16]func()), + } +} + +func (mt *mainThread) ID() uint64 { + return mt.id + +} + +func (mt *mainThread) Running() bool { + return mt.id == lib.g.ThreadSelf() +} + +func (mt *mainThread) register(fn func()) uint16 { + mt.Lock() + defer mt.Unlock() + + var id uint16 + for { + _, exist := mt.fnMap[id] + if !exist { + mt.fnMap[id] = fn + return id + } + id++ + if id == 0 { + panic("FATAL: Too many functions have been dispatched to the main thread") + os.Exit(1) + } + } +} +func (mt *mainThread) dispatch(fn func()) { + if mt.Running() { + fn() + return + } + id := mt.register(fn) + lib.g.IdleAdd(purego.NewCallback(func(ptr) int { + mt.Lock() + fn, exist := mt.fnMap[id] + if !exist { + mt.Unlock() + println("FATAL: main thread dispatch called with invalid id: " + strconv.Itoa(int(id))) + os.Exit(1) + } + delete(mt.fnMap, id) + mt.Unlock() + fn() + return 0 // gSourceRemove + })) +} + +func (mt *mainThread) InvokeSync(fn func()) { + var wg sync.WaitGroup + wg.Add(1) + mt.dispatch(func() { + defer panicHandlerRecover() + fn() + wg.Done() + }) + wg.Wait() +} + +func (mt *mainThread) InvokeAsync(fn func()) { + mt.dispatch(func() { + defer panicHandlerRecover() + fn() + }) +} + +func (mt *mainThread) InvokeSyncWithResult(fn func() any) (res any) { + var wg sync.WaitGroup + wg.Add(1) + mt.dispatch(func() { + defer panicHandlerRecover() + res = fn() + wg.Done() + }) + wg.Wait() + return res +} + +func (mt *mainThread) InvokeSyncWithError(fn func() error) (err error) { + var wg sync.WaitGroup + wg.Add(1) + mt.dispatch(func() { + defer panicHandlerRecover() + err = fn() + wg.Done() + }) + wg.Wait() + return +} + +func (mt *mainThread) InvokeSyncWithResultAndError(fn func() (any, error)) (res any, err error) { + var wg sync.WaitGroup + wg.Add(1) + mt.dispatch(func() { + defer panicHandlerRecover() + res, err = fn() + wg.Done() + }) + wg.Wait() + return res, err +} diff --git a/window.go b/window.go index abcb3f0..acc7105 100644 --- a/window.go +++ b/window.go @@ -116,7 +116,7 @@ type WindowOptions struct { Focused bool // If true, the window's devtools will be available - //DevToolsEnabled bool + DevToolsEnabled bool ///////////////// @@ -128,11 +128,11 @@ type WindowOptions struct { } type Window struct { + log logFunc options WindowOptions pointer windowPtr id uint app *App - logger *logger webview webviewPtr vbox ptr lastWidth int @@ -159,18 +159,7 @@ func (a *App) Open(options WindowOptions) *Window { id: getWindowID(), options: options, } - - if a.logger != nil { - newWindow.logger = &logger{ - prefix: a.logger.prefix + "-", - writer: a.logger.writer, - } - if options.Name != "" { - newWindow.logger.prefix += options.Name - } else { - newWindow.logger.prefix += strconv.Itoa(int(newWindow.id)) - } - } + newWindow.log = newLogFunc("window-" + strconv.Itoa(int(newWindow.id))) if options.Define != nil && len(options.Define) > 0 { newWindow.constants = make(map[string]string) @@ -194,7 +183,7 @@ func (a *App) Open(options WindowOptions) *Window { } } - a.runOnce.add(newWindow) + a.started.run(newWindow) return newWindow } @@ -203,7 +192,7 @@ func (w *Window) ID() uint { } func (w *Window) run() { - invokeSync(w.create) + w.app.thread.InvokeSync(w.create) } func (w *Window) create() { @@ -220,45 +209,48 @@ func (w *Window) create() { lib.g.ObjectRefSink(ptr(w.pointer)) ///////////////////////////////////////////////////////////////////// - // 1. Create the web context once. - if w.app.context == 0 { + // 1. Create the web webContext once. + if w.app.webContext == 0 { - // 1.1. Prepare the data manager for the web context. - cacheDir := w.app.options.CacheDir + // 1.1. Prepare the data manager for the web webContext. + cacheDir := w.app.cacheDir if cacheDir == "" { - cacheDir = filepath.Join(lib.g.GetHomeDir(), ".cache", "webkitgtk", w.app.options.Name) + cacheDir = filepath.Join(lib.g.GetHomeDir(), ".cache", "webkitgtk", w.app.name) } - dataDir := w.app.options.DataDir + dataDir := w.app.dataDir if dataDir == "" { - dataDir = filepath.Join(lib.g.GetHomeDir(), ".local", "share", "webkitgtk", w.app.options.Name) + dataDir = filepath.Join(lib.g.GetHomeDir(), ".local", "share", "webkitgtk", w.app.name) } - if w.app.options.Ephemeral { + if w.app.ephemeral { cacheDir = "" dataDir = "" } dataManager := lib.webkit.WebsiteDataManagerNew( "base-cache-directory", cacheDir, "base-data-directory", dataDir, - "is-ephemeral", w.app.options.Ephemeral, 0) - w.app.context = lib.webkit.WebContextNewWithWebsiteDataManager(dataManager) + "is-ephemeral", w.app.ephemeral, 0) + + w.app.webContext = lib.webkit.WebContextNewWithWebsiteDataManager(dataManager) + lib.webkit.WebContextSetCacheModel(w.app.webContext, int(w.app.cacheModel)) // 1.2. Configure additional data manager settings if not ephemeral. - if !w.app.options.Ephemeral { + if !w.app.ephemeral { lib.webkit.WebsiteDataManagerSetPersistentCredentialStorageEnabled(dataManager, true) - cookieManager := lib.webkit.WebContextGetCookieManager(w.app.context) + cookieManager := lib.webkit.WebContextGetCookieManager(w.app.webContext) lib.webkit.CookieManagerSetPersistentStorage(cookieManager, filepath.Join(dataDir, "cookies.db"), 1) + lib.webkit.CookieManagerSetAcceptPolicy(cookieManager, int(w.app.cookiePolicy)) - lib.webkit.WebContextSetFaviconDatabaseDirectory(w.app.context, filepath.Join(dataDir, "favicons")) - lib.webkit.WebContextSetWebExtensionsDirectory(w.app.context, filepath.Join(dataDir, "extensions")) + lib.webkit.WebContextSetFaviconDatabaseDirectory(w.app.webContext, filepath.Join(dataDir, "favicons")) + lib.webkit.WebContextSetWebExtensionsDirectory(w.app.webContext, filepath.Join(dataDir, "extensions")) } - // 1.3. Configure app URI scheme and register it with the web context. - securityManager := lib.webkit.WebContextGetSecurityManager(w.app.context) + // 1.3. Configure app URI scheme and register it with the web webContext. + securityManager := lib.webkit.WebContextGetSecurityManager(w.app.webContext) lib.webkit.SecurityManagerRegisterUriSchemeAsCorsEnabled(securityManager, uriScheme) lib.webkit.SecurityManagerRegisterUriSchemeAsSecure(securityManager, uriScheme) lib.webkit.WebContextRegisterUriScheme( - w.app.context, + w.app.webContext, uriScheme, ptr(purego.NewCallback(func(request ptr) { r := newUriSchemeRequest(request) @@ -273,12 +265,16 @@ func (w *Window) create() { rw := r.toResponseWriter() defer rw.Close() - if handler, exists := w.app.options.Handle[req.URL.Host]; exists { + w.app.handlerLock.RLock() + handler, exists := w.app.handler[req.URL.Host] + w.app.handlerLock.RUnlock() + if exists { + w.log("handler request", "host", req.URL.Host, "path", req.URL.Path) handler.ServeHTTP(rw, req) return } - w.log("no handler found for request", "host", req.URL.Host) + w.log("no handler found for request", "host", req.URL.Host, "path", req.URL.Path) http.Error(rw, "no handler found for request", http.StatusNotFound) })), 0, @@ -286,8 +282,8 @@ func (w *Window) create() { ) } - // 2. Create the webview add app URI scheme to the CORS allow list. - w.webview = lib.webkit.WebViewNewWithContext(w.app.context) + // 2. Create the webview run app URI scheme to the CORS allow list. + w.webview = lib.webkit.WebViewNewWithContext(w.app.webContext) uriSchemeEntry := lib.g.RefStringNew(uriScheme + "://*/*") defer lib.g.RefStringRelease(uriSchemeEntry) lib.webkit.WebViewSetCorsAllowlist(w.webview, uriSchemeEntry, 0) @@ -433,7 +429,11 @@ func (w *Window) create() { w.options.MaxHeight, ) } - w.SetTitle(w.options.Title) + + if w.options.Title != "" { + w.SetTitle(w.options.Title) + } + w.SetSize(w.options.Width, w.options.Height) w.SetZoom(w.options.Zoom) w.SetOverlay(w.options.Overlay) @@ -479,7 +479,7 @@ func (w *Window) create() { w.Center() // needs to be queued until after GTK starts up! } } - if w.app.options.Debug { + if w.options.DevToolsEnabled { w.ToggleDevTools() } @@ -843,22 +843,22 @@ func windowSetURL(webview webviewPtr, uri string) { func windowSetupSignalHandlers(windowId uint, window windowPtr, webview webviewPtr) { handleDelete := purego.NewCallback(func(ptr) { - globalApplication.windowsLock.RLock() - appWindow := globalApplication.windows[windowId] - globalApplication.windowsLock.RUnlock() + _app.windowsLock.RLock() + appWindow := _app.windows[windowId] + _app.windowsLock.RUnlock() if !appWindow.options.HideOnClose { windowDestroy(window) appWindow.log("pointer closed", "id", windowId, "name", appWindow.options.Name) - globalApplication.windowsLock.Lock() - delete(globalApplication.windows, windowId) - windowCount := len(globalApplication.windows) - globalApplication.windowsLock.Unlock() + _app.windowsLock.Lock() + delete(_app.windows, windowId) + windowCount := len(_app.windows) + _app.windowsLock.Unlock() - if windowCount == 0 && !globalApplication.options.Hold { - globalApplication.log("last window closed, quitting") - globalApplication.Quit() + if windowCount == 0 && !_app.hold { + _app.log("last window closed, quitting") + _app.Quit() } } else { appWindow.log("pointer hiding", "id", windowId, "name", appWindow.options.Name) @@ -873,9 +873,9 @@ func windowSetupSignalHandlers(windowId uint, window windowPtr, webview webviewP case 1: // LOAD_REDIRECTED case 2: // LOAD_COMMITTED case 3: // LOAD_FINISHED - globalApplication.windowsLock.RLock() - w := globalApplication.windows[windowId] - globalApplication.windowsLock.RUnlock() + _app.windowsLock.RLock() + w := _app.windows[windowId] + _app.windowsLock.RUnlock() w.log("initial load finished", "id", windowId, "name", w.options.Name) @@ -1103,10 +1103,3 @@ func (w *Window) startResize(border string) error { // FIXME: what do we need to do here? return nil } - -func (w *Window) log(msg interface{}, kv ...interface{}) { - if w.logger == nil { - return - } - w.logger.log(msg, kv...) -}