Switch remote deploy to vendored source builds

Move remote deployment to a vendored source bundle built on the target host via Docker so redeploys no longer require local cross-compilation or host Go installation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
GitHub Actions
2026-05-08 12:19:18 +08:00
parent bb27566e38
commit c1a0fe2949
1320 changed files with 497125 additions and 11 deletions

View File

@@ -0,0 +1,45 @@
package app
import (
"context"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/menumanager"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
)
// App defines a Wails application structure
type App struct {
frontend frontend.Frontend
logger *logger.Logger
options *options.App
menuManager *menumanager.Manager
// Indicates if the app is in debug mode
debug bool
// Indicates if the devtools is enabled
devtoolsEnabled bool
// OnStartup/OnShutdown
startupCallback func(ctx context.Context)
shutdownCallback func(ctx context.Context)
ctx context.Context
}
// Shutdown the application
func (a *App) Shutdown() {
if a.frontend != nil {
a.frontend.Quit()
}
}
// SetApplicationMenu sets the application menu
func (a *App) SetApplicationMenu(menu *menu.Menu) {
if a.frontend != nil {
a.frontend.MenuSetApplicationMenu(menu)
}
}

View File

@@ -0,0 +1,124 @@
//go:build bindings
package app
import (
"flag"
"os"
"path/filepath"
"github.com/leaanthony/gosod"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend/runtime/wrapper"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/project"
"github.com/wailsapp/wails/v2/pkg/options"
)
func (a *App) Run() error {
// Create binding exemptions - Ugly hack. There must be a better way
bindingExemptions := []interface{}{
a.options.OnStartup,
a.options.OnShutdown,
a.options.OnDomReady,
a.options.OnBeforeClose,
}
// Check for CLI Flags
bindingFlags := flag.NewFlagSet("bindings", flag.ContinueOnError)
var tsPrefixFlag *string
var tsPostfixFlag *string
var tsOutputTypeFlag *string
tsPrefix := os.Getenv("tsprefix")
if tsPrefix == "" {
tsPrefixFlag = bindingFlags.String("tsprefix", "", "Prefix for generated typescript entities")
}
tsSuffix := os.Getenv("tssuffix")
if tsSuffix == "" {
tsPostfixFlag = bindingFlags.String("tssuffix", "", "Suffix for generated typescript entities")
}
tsOutputType := os.Getenv("tsoutputtype")
if tsOutputType == "" {
tsOutputTypeFlag = bindingFlags.String("tsoutputtype", "", "Output type for generated typescript entities (classes|interfaces)")
}
_ = bindingFlags.Parse(os.Args[1:])
if tsPrefixFlag != nil {
tsPrefix = *tsPrefixFlag
}
if tsPostfixFlag != nil {
tsSuffix = *tsPostfixFlag
}
if tsOutputTypeFlag != nil {
tsOutputType = *tsOutputTypeFlag
}
appBindings := binding.NewBindings(a.logger, a.options.Bind, bindingExemptions, IsObfuscated(), a.options.EnumBind)
appBindings.SetTsPrefix(tsPrefix)
appBindings.SetTsSuffix(tsSuffix)
appBindings.SetOutputType(tsOutputType)
err := generateBindings(appBindings)
if err != nil {
return err
}
return nil
}
// CreateApp creates the app!
func CreateApp(appoptions *options.App) (*App, error) {
// Set up logger
myLogger := logger.New(appoptions.Logger)
myLogger.SetLogLevel(appoptions.LogLevel)
result := &App{
logger: myLogger,
options: appoptions,
}
return result, nil
}
func generateBindings(bindings *binding.Bindings) error {
cwd, err := os.Getwd()
if err != nil {
return err
}
projectConfig, err := project.Load(cwd)
if err != nil {
return err
}
wailsjsbasedir := filepath.Join(projectConfig.GetWailsJSDir(), "wailsjs")
runtimeDir := filepath.Join(wailsjsbasedir, "runtime")
_ = os.RemoveAll(runtimeDir)
extractor := gosod.New(wrapper.RuntimeWrapper)
err = extractor.Extract(runtimeDir, nil)
if err != nil {
return err
}
goBindingsDir := filepath.Join(wailsjsbasedir, "go")
err = os.RemoveAll(goBindingsDir)
if err != nil {
return err
}
_ = fs.MkDirs(goBindingsDir)
err = bindings.GenerateGoBindings(goBindingsDir)
if err != nil {
return err
}
return fs.SetPermissions(wailsjsbasedir, 0755)
}

View File

@@ -0,0 +1,7 @@
//go:build debug
package app
func IsDebug() bool {
return true
}

View File

@@ -0,0 +1,7 @@
//go:build !debug
package app
func IsDebug() bool {
return false
}

View File

@@ -0,0 +1,18 @@
//go:build !dev && !production && !bindings && (linux || darwin)
package app
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/options"
)
func (a *App) Run() error {
return nil
}
// CreateApp creates the app!
func CreateApp(_ *options.App) (*App, error) {
return nil, fmt.Errorf(`Wails applications will not build without the correct build tags.`)
}

View File

@@ -0,0 +1,27 @@
//go:build !dev && !production && !bindings && windows
package app
import (
"os/exec"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
"github.com/wailsapp/wails/v2/pkg/options"
)
func (a *App) Run() error {
return nil
}
// CreateApp creates the app!
func CreateApp(_ *options.App) (*App, error) {
result := w32.MessageBox(0,
`Wails applications will not build without the correct build tags.
Please use "wails build" or press "OK" to open the documentation on how to use "go build"`,
"Error",
w32.MB_ICONERROR|w32.MB_OKCANCEL)
if result == 1 {
exec.Command("rundll32", "url.dll,FileProtocolHandler", "https://wails.io/docs/guides/manual-builds").Start()
}
return nil, nil
}

View File

@@ -0,0 +1,298 @@
//go:build dev
package app
import (
"context"
"embed"
"flag"
"fmt"
iofs "io/fs"
"net"
"net/url"
"os"
"path/filepath"
"time"
"github.com/wailsapp/wails/v2/pkg/assetserver"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend/desktop"
"github.com/wailsapp/wails/v2/internal/frontend/devserver"
"github.com/wailsapp/wails/v2/internal/frontend/dispatcher"
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/menumanager"
pkglogger "github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/options"
)
func (a *App) Run() error {
err := a.frontend.Run(a.ctx)
a.frontend.RunMainLoop()
a.frontend.WindowClose()
if a.shutdownCallback != nil {
a.shutdownCallback(a.ctx)
}
return err
}
// CreateApp creates the app!
func CreateApp(appoptions *options.App) (*App, error) {
var err error
ctx := context.Background()
ctx = context.WithValue(ctx, "debug", true)
ctx = context.WithValue(ctx, "devtoolsEnabled", true)
// Set up logger if the appoptions.LogLevel is an invalid value, set it to the default log level
appoptions.LogLevel, err = pkglogger.StringToLogLevel(appoptions.LogLevel.String())
if err != nil {
return nil, err
}
myLogger := logger.New(appoptions.Logger)
myLogger.SetLogLevel(appoptions.LogLevel)
// Check for CLI Flags
devFlags := flag.NewFlagSet("dev", flag.ContinueOnError)
var assetdirFlag *string
var devServerFlag *string
var frontendDevServerURLFlag *string
var loglevelFlag *string
assetdir := os.Getenv("assetdir")
if assetdir == "" {
assetdirFlag = devFlags.String("assetdir", "", "Directory to serve assets")
}
devServer := os.Getenv("devserver")
if devServer == "" {
devServerFlag = devFlags.String("devserver", "", "Address to bind the wails dev server to")
}
frontendDevServerURL := os.Getenv("frontenddevserverurl")
if frontendDevServerURL == "" {
frontendDevServerURLFlag = devFlags.String("frontenddevserverurl", "", "URL of the external frontend dev server")
}
loglevel := os.Getenv("loglevel")
appLogLevel := appoptions.LogLevel.String()
if loglevel != "" {
appLogLevel = loglevel
}
loglevelFlag = devFlags.String("loglevel", appLogLevel, "Loglevel to use - Trace, Debug, Info, Warning, Error")
// If we weren't given the assetdir in the environment variables
if assetdir == "" {
// Parse args but ignore errors in case -appargs was used to pass in args for the app.
_ = devFlags.Parse(os.Args[1:])
if assetdirFlag != nil {
assetdir = *assetdirFlag
}
if devServerFlag != nil {
devServer = *devServerFlag
}
if frontendDevServerURLFlag != nil {
frontendDevServerURL = *frontendDevServerURLFlag
}
if loglevelFlag != nil {
loglevel = *loglevelFlag
}
}
assetConfig, err := assetserver.BuildAssetServerConfig(appoptions)
if err != nil {
return nil, err
}
if assetConfig.Assets == nil && frontendDevServerURL != "" {
myLogger.Warning("No AssetServer.Assets has been defined but a frontend DevServer, the frontend DevServer will not be used.")
frontendDevServerURL = ""
assetdir = ""
}
if frontendDevServerURL != "" {
_, port, err := net.SplitHostPort(devServer)
if err != nil {
return nil, fmt.Errorf("unable to determine port of DevServer: %s", err)
}
ctx = context.WithValue(ctx, "assetserverport", port)
ctx = context.WithValue(ctx, "frontenddevserverurl", frontendDevServerURL)
externalURL, err := url.Parse(frontendDevServerURL)
if err != nil {
return nil, err
}
if externalURL.Host == "" {
return nil, fmt.Errorf("Invalid frontend:dev:serverUrl missing protocol scheme?")
}
waitCb := func() { myLogger.Debug("Waiting for frontend DevServer '%s' to be ready", externalURL) }
if !checkPortIsOpen(externalURL.Host, time.Minute, waitCb) {
myLogger.Error("Timeout waiting for frontend DevServer")
}
handler := assetserver.NewExternalAssetsHandler(myLogger, assetConfig, externalURL)
assetConfig.Assets = nil
assetConfig.Handler = handler
assetConfig.Middleware = nil
myLogger.Info("Serving assets from frontend DevServer URL: %s", frontendDevServerURL)
} else {
if assetdir == "" {
// If no assetdir has been defined, let's try to infer it from the project root and the asset FS.
assetdir, err = tryInferAssetDirFromFS(assetConfig.Assets)
if err != nil {
return nil, fmt.Errorf("unable to infer the AssetDir from your Assets fs.FS: %w", err)
}
}
if assetdir != "" {
// Let's override the assets to serve from on disk, if needed
absdir, err := filepath.Abs(assetdir)
if err != nil {
return nil, err
}
myLogger.Info("Serving assets from disk: %s", absdir)
assetConfig.Assets = os.DirFS(absdir)
ctx = context.WithValue(ctx, "assetdir", assetdir)
}
}
// Migrate deprecated options to the new AssetServer option
appoptions.Assets = nil
appoptions.AssetsHandler = nil
appoptions.AssetServer = &assetConfig
if devServer != "" {
ctx = context.WithValue(ctx, "devserver", devServer)
}
if loglevel != "" {
level, err := pkglogger.StringToLogLevel(loglevel)
if err != nil {
return nil, err
}
// Only set the log level if it's different from the appoptions.LogLevel
if level != appoptions.LogLevel {
myLogger.SetLogLevel(level)
}
}
// Attach logger to context
ctx = context.WithValue(ctx, "logger", myLogger)
ctx = context.WithValue(ctx, "buildtype", "dev")
// Preflight checks
err = PreflightChecks(appoptions, myLogger)
if err != nil {
return nil, err
}
// Merge default options
options.MergeDefaults(appoptions)
var menuManager *menumanager.Manager
// Process the application menu
if appoptions.Menu != nil {
// Create the menu manager
menuManager = menumanager.NewManager()
err = menuManager.SetApplicationMenu(appoptions.Menu)
if err != nil {
return nil, err
}
}
// Create binding exemptions - Ugly hack. There must be a better way
bindingExemptions := []interface{}{
appoptions.OnStartup,
appoptions.OnShutdown,
appoptions.OnDomReady,
appoptions.OnBeforeClose,
}
appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, false, appoptions.EnumBind)
eventHandler := runtime.NewEvents(myLogger)
ctx = context.WithValue(ctx, "events", eventHandler)
messageDispatcher := dispatcher.NewDispatcher(ctx, myLogger, appBindings, eventHandler, appoptions.ErrorFormatter, appoptions.DisablePanicRecovery)
// Create the frontends and register to event handler
desktopFrontend := desktop.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher)
appFrontend := devserver.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher, menuManager, desktopFrontend)
eventHandler.AddFrontend(appFrontend)
eventHandler.AddFrontend(desktopFrontend)
ctx = context.WithValue(ctx, "frontend", appFrontend)
result := &App{
ctx: ctx,
frontend: appFrontend,
logger: myLogger,
menuManager: menuManager,
startupCallback: appoptions.OnStartup,
shutdownCallback: appoptions.OnShutdown,
debug: true,
devtoolsEnabled: true,
}
result.options = appoptions
return result, nil
}
func tryInferAssetDirFromFS(assets iofs.FS) (string, error) {
if _, isEmbedFs := assets.(embed.FS); !isEmbedFs {
// We only infer the assetdir for embed.FS assets
return "", nil
}
path, err := fs.FindPathToFile(assets, "index.html")
if err != nil {
return "", err
}
path, err = filepath.Abs(path)
if err != nil {
return "", err
}
if _, err := os.Stat(filepath.Join(path, "index.html")); err != nil {
if os.IsNotExist(err) {
err = fmt.Errorf(
"inferred assetdir '%s' does not exist or does not contain an 'index.html' file, "+
"please specify it with -assetdir or set it in wails.json",
path)
}
return "", err
}
return path, nil
}
func checkPortIsOpen(host string, timeout time.Duration, waitCB func()) (ret bool) {
if timeout == 0 {
timeout = time.Minute
}
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
conn, _ := net.DialTimeout("tcp", host, 2*time.Second)
if conn != nil {
conn.Close()
return true
}
waitCB()
time.Sleep(1 * time.Second)
}
return false
}

View File

@@ -0,0 +1,8 @@
//go:build devtools
package app
// Note: devtools flag is also added in debug builds
func IsDevtoolsEnabled() bool {
return true
}

View File

@@ -0,0 +1,9 @@
//go:build !devtools
package app
// IsDevtoolsEnabled returns true if devtools should be enabled
// Note: devtools flag is also added in debug builds
func IsDevtoolsEnabled() bool {
return false
}

View File

@@ -0,0 +1,8 @@
//go:build obfuscated
package app
// IsObfuscated returns true if the obfuscated build tag is set
func IsObfuscated() bool {
return true
}

View File

@@ -0,0 +1,8 @@
//go:build !obfuscated
package app
// IsObfuscated returns false if the obfuscated build tag is not set
func IsObfuscated() bool {
return false
}

View File

@@ -0,0 +1,12 @@
//go:build (linux || darwin) && !bindings
package app
import (
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/pkg/options"
)
func PreflightChecks(_ *options.App, _ *logger.Logger) error {
return nil
}

View File

@@ -0,0 +1,27 @@
//go:build windows && !bindings
package app
import (
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/wv2installer"
"github.com/wailsapp/wails/v2/pkg/options"
)
func PreflightChecks(options *options.App, logger *logger.Logger) error {
_ = options
// Process the webview2 runtime situation. We can pass a strategy in via the `webview2` flag for `wails build`.
// This will determine how wv2runtime.Process will handle a lack of valid runtime.
installedVersion, err := wv2installer.Process(options)
if installedVersion != "" {
logger.Debug("WebView2 Runtime Version '%s' installed. Minimum version required: %s.",
installedVersion, wv2installer.MinimumRuntimeVersion)
}
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,104 @@
//go:build production
package app
import (
"context"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend/desktop"
"github.com/wailsapp/wails/v2/internal/frontend/dispatcher"
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/menumanager"
"github.com/wailsapp/wails/v2/pkg/options"
)
func (a *App) Run() error {
err := a.frontend.Run(a.ctx)
a.frontend.RunMainLoop()
a.frontend.WindowClose()
if a.shutdownCallback != nil {
a.shutdownCallback(a.ctx)
}
return err
}
// CreateApp creates the app!
func CreateApp(appoptions *options.App) (*App, error) {
var err error
ctx := context.Background()
// Merge default options
options.MergeDefaults(appoptions)
debug := IsDebug()
devtoolsEnabled := IsDevtoolsEnabled()
ctx = context.WithValue(ctx, "debug", debug)
ctx = context.WithValue(ctx, "devtoolsEnabled", devtoolsEnabled)
// Set up logger
myLogger := logger.New(appoptions.Logger)
if IsDebug() {
myLogger.SetLogLevel(appoptions.LogLevel)
} else {
myLogger.SetLogLevel(appoptions.LogLevelProduction)
}
ctx = context.WithValue(ctx, "logger", myLogger)
ctx = context.WithValue(ctx, "obfuscated", IsObfuscated())
// Preflight Checks
err = PreflightChecks(appoptions, myLogger)
if err != nil {
return nil, err
}
// Create the menu manager
menuManager := menumanager.NewManager()
// Process the application menu
if appoptions.Menu != nil {
err = menuManager.SetApplicationMenu(appoptions.Menu)
if err != nil {
return nil, err
}
}
// Create binding exemptions - Ugly hack. There must be a better way
bindingExemptions := []interface{}{
appoptions.OnStartup,
appoptions.OnShutdown,
appoptions.OnDomReady,
appoptions.OnBeforeClose,
}
appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, IsObfuscated(), appoptions.EnumBind)
eventHandler := runtime.NewEvents(myLogger)
ctx = context.WithValue(ctx, "events", eventHandler)
// Attach logger to context
if debug {
ctx = context.WithValue(ctx, "buildtype", "debug")
} else {
ctx = context.WithValue(ctx, "buildtype", "production")
}
messageDispatcher := dispatcher.NewDispatcher(ctx, myLogger, appBindings, eventHandler, appoptions.ErrorFormatter, appoptions.DisablePanicRecovery)
appFrontend := desktop.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher)
eventHandler.AddFrontend(appFrontend)
ctx = context.WithValue(ctx, "frontend", appFrontend)
result := &App{
ctx: ctx,
frontend: appFrontend,
logger: myLogger,
menuManager: menuManager,
startupCallback: appoptions.OnStartup,
shutdownCallback: appoptions.OnShutdown,
debug: debug,
devtoolsEnabled: devtoolsEnabled,
options: appoptions,
}
return result, nil
}

View File

@@ -0,0 +1,384 @@
package binding
import (
"bufio"
"bytes"
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
"github.com/wailsapp/wails/v2/internal/typescriptify"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/logger"
)
type Bindings struct {
db *DB
logger logger.CustomLogger
exemptions slicer.StringSlicer
structsToGenerateTS map[string]map[string]interface{}
enumsToGenerateTS map[string]map[string]interface{}
tsPrefix string
tsSuffix string
tsInterface bool
obfuscate bool
}
// NewBindings returns a new Bindings object
func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}, obfuscate bool, enumsToBind []interface{}) *Bindings {
result := &Bindings{
db: newDB(),
logger: logger.CustomLogger("Bindings"),
structsToGenerateTS: make(map[string]map[string]interface{}),
enumsToGenerateTS: make(map[string]map[string]interface{}),
obfuscate: obfuscate,
}
for _, exemption := range exemptions {
if exemption == nil {
continue
}
name := runtime.FuncForPC(reflect.ValueOf(exemption).Pointer()).Name()
// Yuk yuk yuk! Is there a better way?
name = strings.TrimSuffix(name, "-fm")
result.exemptions.Add(name)
}
for _, enum := range enumsToBind {
result.AddEnumToGenerateTS(enum)
}
// Add the structs to bind
for _, ptr := range structPointersToBind {
err := result.Add(ptr)
if err != nil {
logger.Fatal("Error during binding: " + err.Error())
}
}
return result
}
// Add the given struct methods to the Bindings
func (b *Bindings) Add(structPtr interface{}) error {
methods, err := b.getMethods(structPtr)
if err != nil {
return fmt.Errorf("cannot bind value to app: %s", err.Error())
}
for _, method := range methods {
b.db.AddMethod(method.Path.Package, method.Path.Struct, method.Path.Name, method)
}
return nil
}
func (b *Bindings) DB() *DB {
return b.db
}
func (b *Bindings) ToJSON() (string, error) {
return b.db.ToJSON()
}
func (b *Bindings) GenerateModels() ([]byte, error) {
models := map[string]string{}
var seen slicer.StringSlicer
var seenEnumsPackages slicer.StringSlicer
allStructNames := b.getAllStructNames()
allStructNames.Sort()
allEnumNames := b.getAllEnumNames()
allEnumNames.Sort()
for packageName, structsToGenerate := range b.structsToGenerateTS {
thisPackageCode := ""
w := typescriptify.New()
w.WithPrefix(b.tsPrefix)
w.WithSuffix(b.tsSuffix)
w.WithInterface(b.tsInterface)
w.Namespace = packageName
w.WithBackupDir("")
w.KnownStructs = allStructNames
w.KnownEnums = allEnumNames
// sort the structs
var structNames []string
for structName := range structsToGenerate {
structNames = append(structNames, structName)
}
sort.Strings(structNames)
for _, structName := range structNames {
fqstructname := packageName + "." + structName
if seen.Contains(fqstructname) {
continue
}
structInterface := structsToGenerate[structName]
w.Add(structInterface)
}
// if we have enums for this package, add them as well
var enums, enumsExist = b.enumsToGenerateTS[packageName]
if enumsExist {
// Sort the enum names first to make the output deterministic
sortedEnumNames := make([]string, 0, len(enums))
for enumName := range enums {
sortedEnumNames = append(sortedEnumNames, enumName)
}
sort.Strings(sortedEnumNames)
for _, enumName := range sortedEnumNames {
enum := enums[enumName]
fqemumname := packageName + "." + enumName
if seen.Contains(fqemumname) {
continue
}
w.AddEnum(enum)
}
seenEnumsPackages.Add(packageName)
}
str, err := w.Convert(nil)
if err != nil {
return nil, err
}
thisPackageCode += str
seen.AddSlice(w.GetGeneratedStructs())
models[packageName] = thisPackageCode
}
// Add outstanding enums to the models that were not in packages with structs
for packageName, enumsToGenerate := range b.enumsToGenerateTS {
if seenEnumsPackages.Contains(packageName) {
continue
}
thisPackageCode := ""
w := typescriptify.New()
w.WithPrefix(b.tsPrefix)
w.WithSuffix(b.tsSuffix)
w.WithInterface(b.tsInterface)
w.Namespace = packageName
w.WithBackupDir("")
for enumName, enum := range enumsToGenerate {
fqemumname := packageName + "." + enumName
if seen.Contains(fqemumname) {
continue
}
w.AddEnum(enum)
}
str, err := w.Convert(nil)
if err != nil {
return nil, err
}
thisPackageCode += str
models[packageName] = thisPackageCode
}
// Sort the package names first to make the output deterministic
sortedPackageNames := make([]string, 0, len(models))
for packageName := range models {
sortedPackageNames = append(sortedPackageNames, packageName)
}
sort.Strings(sortedPackageNames)
var modelsData bytes.Buffer
for _, packageName := range sortedPackageNames {
modelData := models[packageName]
if strings.TrimSpace(modelData) == "" {
continue
}
modelsData.WriteString("export namespace " + packageName + " {\n")
sc := bufio.NewScanner(strings.NewReader(modelData))
for sc.Scan() {
modelsData.WriteString("\t" + sc.Text() + "\n")
}
modelsData.WriteString("\n}\n\n")
}
return modelsData.Bytes(), nil
}
func (b *Bindings) WriteModels(modelsDir string) error {
modelsData, err := b.GenerateModels()
if err != nil {
return err
}
// Don't write if we don't have anything
if len(modelsData) == 0 {
return nil
}
filename := filepath.Join(modelsDir, "models.ts")
err = os.WriteFile(filename, modelsData, 0o755)
if err != nil {
return err
}
return nil
}
func (b *Bindings) AddEnumToGenerateTS(e interface{}) {
enumType := reflect.TypeOf(e)
var packageName string
var enumName string
// enums should be represented as array of all possible values
if hasElements(enumType) {
enum := enumType.Elem()
// simple enum represented by struct with Value/TSName fields
if enum.Kind() == reflect.Struct {
_, tsNamePresented := enum.FieldByName("TSName")
enumT, valuePresented := enum.FieldByName("Value")
if tsNamePresented && valuePresented {
packageName = getPackageName(enumT.Type.String())
enumName = enumT.Type.Name()
} else {
return
}
// otherwise expecting implementation with TSName() https://github.com/tkrajina/typescriptify-golang-structs#enums-with-tsname
} else {
packageName = getPackageName(enumType.Elem().String())
enumName = enumType.Elem().Name()
}
if b.enumsToGenerateTS[packageName] == nil {
b.enumsToGenerateTS[packageName] = make(map[string]interface{})
}
if b.enumsToGenerateTS[packageName][enumName] != nil {
return
}
b.enumsToGenerateTS[packageName][enumName] = e
}
}
func (b *Bindings) AddStructToGenerateTS(packageName string, structName string, s interface{}) {
if b.structsToGenerateTS[packageName] == nil {
b.structsToGenerateTS[packageName] = make(map[string]interface{})
}
if b.structsToGenerateTS[packageName][structName] != nil {
return
}
b.structsToGenerateTS[packageName][structName] = s
// Iterate this struct and add any struct field references
structType := reflect.TypeOf(s)
for hasElements(structType) {
structType = structType.Elem()
}
for i := 0; i < structType.NumField(); i++ {
field := structType.Field(i)
if field.Anonymous || !field.IsExported() {
continue
}
kind := field.Type.Kind()
if kind == reflect.Struct {
fqname := field.Type.String()
sNameSplit := strings.SplitN(fqname, ".", 2)
if len(sNameSplit) < 2 {
continue
}
sName := sNameSplit[1]
pName := getPackageName(fqname)
a := reflect.New(field.Type)
if b.hasExportedJSONFields(field.Type) {
s := reflect.Indirect(a).Interface()
b.AddStructToGenerateTS(pName, sName, s)
}
} else {
fType := field.Type
for hasElements(fType) {
fType = fType.Elem()
}
if fType.Kind() == reflect.Struct {
fqname := fType.String()
sNameSplit := strings.SplitN(fqname, ".", 2)
if len(sNameSplit) < 2 {
continue
}
sName := sNameSplit[1]
pName := getPackageName(fqname)
a := reflect.New(fType)
if b.hasExportedJSONFields(fType) {
s := reflect.Indirect(a).Interface()
b.AddStructToGenerateTS(pName, sName, s)
}
}
}
}
}
func (b *Bindings) SetTsPrefix(prefix string) *Bindings {
b.tsPrefix = prefix
return b
}
func (b *Bindings) SetTsSuffix(postfix string) *Bindings {
b.tsSuffix = postfix
return b
}
func (b *Bindings) SetOutputType(outputType string) *Bindings {
if outputType == "interfaces" {
b.tsInterface = true
}
return b
}
func (b *Bindings) getAllStructNames() *slicer.StringSlicer {
var result slicer.StringSlicer
for packageName, structsToGenerate := range b.structsToGenerateTS {
for structName := range structsToGenerate {
result.Add(packageName + "." + structName)
}
}
return &result
}
func (b *Bindings) getAllEnumNames() *slicer.StringSlicer {
var result slicer.StringSlicer
for packageName, enumsToGenerate := range b.enumsToGenerateTS {
for enumName := range enumsToGenerate {
result.Add(packageName + "." + enumName)
}
}
return &result
}
func (b *Bindings) hasExportedJSONFields(typeOf reflect.Type) bool {
for i := 0; i < typeOf.NumField(); i++ {
jsonFieldName := ""
f := typeOf.Field(i)
// function, complex, and channel types cannot be json-encoded
if f.Type.Kind() == reflect.Chan ||
f.Type.Kind() == reflect.Func ||
f.Type.Kind() == reflect.UnsafePointer ||
f.Type.Kind() == reflect.Complex128 ||
f.Type.Kind() == reflect.Complex64 {
continue
}
jsonTag, hasTag := f.Tag.Lookup("json")
if !hasTag && f.IsExported() {
return true
}
if len(jsonTag) == 0 {
continue
}
jsonTagParts := strings.Split(jsonTag, ",")
if len(jsonTagParts) > 0 {
jsonFieldName = jsonTagParts[0]
}
for _, t := range jsonTagParts {
if t == "-" {
continue
}
}
if jsonFieldName != "" {
return true
}
}
return false
}

View File

@@ -0,0 +1,109 @@
package binding
import (
"encoding/json"
"fmt"
"reflect"
)
type BoundedMethodPath struct {
Package string
Struct string
Name string
}
func (p *BoundedMethodPath) FullName() string {
return fmt.Sprintf("%s.%s.%s", p.Package, p.Struct, p.Name)
}
// BoundMethod defines all the data related to a Go method that is
// bound to the Wails application
type BoundMethod struct {
Path *BoundedMethodPath `json:"path"`
Inputs []*Parameter `json:"inputs,omitempty"`
Outputs []*Parameter `json:"outputs,omitempty"`
Comments string `json:"comments,omitempty"`
Method reflect.Value `json:"-"`
}
// InputCount returns the number of inputs this bound method has
func (b *BoundMethod) InputCount() int {
return len(b.Inputs)
}
// OutputCount returns the number of outputs this bound method has
func (b *BoundMethod) OutputCount() int {
return len(b.Outputs)
}
// ParseArgs method converts the input json into the types expected by the method
func (b *BoundMethod) ParseArgs(args []json.RawMessage) ([]interface{}, error) {
result := make([]interface{}, b.InputCount())
if len(args) != b.InputCount() {
return nil, fmt.Errorf("received %d arguments to method '%s', expected %d", len(args), b.Path.FullName(), b.InputCount())
}
for index, arg := range args {
typ := b.Inputs[index].reflectType
inputValue := reflect.New(typ).Interface()
err := json.Unmarshal(arg, inputValue)
if err != nil {
return nil, err
}
if inputValue == nil {
result[index] = reflect.Zero(typ).Interface()
} else {
result[index] = reflect.ValueOf(inputValue).Elem().Interface()
}
}
return result, nil
}
// Call will attempt to call this bound method with the given args
func (b *BoundMethod) Call(args []interface{}) (interface{}, error) {
// Check inputs
expectedInputLength := len(b.Inputs)
actualInputLength := len(args)
if expectedInputLength != actualInputLength {
return nil, fmt.Errorf("%s takes %d inputs. Received %d", b.Path.FullName(), expectedInputLength, actualInputLength)
}
/** Convert inputs to reflect values **/
// Create slice for the input arguments to the method call
callArgs := make([]reflect.Value, expectedInputLength)
// Iterate over given arguments
for index, arg := range args {
// Save the converted argument
callArgs[index] = reflect.ValueOf(arg)
}
// Do the call
callResults := b.Method.Call(callArgs)
//** Check results **//
var returnValue interface{}
var err error
switch b.OutputCount() {
case 1:
// Loop over results and determine if the result
// is an error or not
for _, result := range callResults {
interfac := result.Interface()
temp, ok := interfac.(error)
if ok {
err = temp
} else {
returnValue = interfac
}
}
case 2:
returnValue = callResults[0].Interface()
if temp, ok := callResults[1].Interface().(error); ok {
err = temp
}
}
return returnValue, err
}

View File

@@ -0,0 +1,134 @@
package binding
import (
"encoding/json"
"sync"
"unsafe"
)
// DB is our database of method bindings
type DB struct {
// map[packagename] -> map[structname] -> map[methodname]*method
store map[string]map[string]map[string]*BoundMethod
// This uses fully qualified method names as a shortcut for store traversal.
// It used for performance gains at runtime
methodMap map[string]*BoundMethod
// This uses ids to reference bound methods at runtime
obfuscatedMethodArray []*ObfuscatedMethod
// Lock to ensure sync access to the data
lock sync.RWMutex
}
type ObfuscatedMethod struct {
method *BoundMethod
methodName string
}
func newDB() *DB {
return &DB{
store: make(map[string]map[string]map[string]*BoundMethod),
methodMap: make(map[string]*BoundMethod),
obfuscatedMethodArray: []*ObfuscatedMethod{},
}
}
// GetMethodFromStore returns the method for the given package/struct/method names
// nil is returned if any one of those does not exist
func (d *DB) GetMethodFromStore(packageName string, structName string, methodName string) *BoundMethod {
// Lock the db whilst processing and unlock on return
d.lock.RLock()
defer d.lock.RUnlock()
structMap, exists := d.store[packageName]
if !exists {
return nil
}
methodMap, exists := structMap[structName]
if !exists {
return nil
}
return methodMap[methodName]
}
// GetMethod returns the method for the given qualified method name
// qualifiedMethodName is "packagename.structname.methodname"
func (d *DB) GetMethod(qualifiedMethodName string) *BoundMethod {
// Lock the db whilst processing and unlock on return
d.lock.RLock()
defer d.lock.RUnlock()
return d.methodMap[qualifiedMethodName]
}
// GetObfuscatedMethod returns the method for the given ID
func (d *DB) GetObfuscatedMethod(id int) *BoundMethod {
// Lock the db whilst processing and unlock on return
d.lock.RLock()
defer d.lock.RUnlock()
if len(d.obfuscatedMethodArray) <= id {
return nil
}
return d.obfuscatedMethodArray[id].method
}
// AddMethod adds the given method definition to the db using the given qualified path: packageName.structName.methodName
func (d *DB) AddMethod(packageName string, structName string, methodName string, methodDefinition *BoundMethod) {
// Lock the db whilst processing and unlock on return
d.lock.Lock()
defer d.lock.Unlock()
// Get the map associated with the package name
structMap, exists := d.store[packageName]
if !exists {
// Create a new map for this packagename
d.store[packageName] = make(map[string]map[string]*BoundMethod)
structMap = d.store[packageName]
}
// Get the map associated with the struct name
methodMap, exists := structMap[structName]
if !exists {
// Create a new map for this packagename
structMap[structName] = make(map[string]*BoundMethod)
methodMap = structMap[structName]
}
// Store the method definition
methodMap[methodName] = methodDefinition
// Store in the methodMap
key := packageName + "." + structName + "." + methodName
d.methodMap[key] = methodDefinition
d.obfuscatedMethodArray = append(d.obfuscatedMethodArray, &ObfuscatedMethod{method: methodDefinition, methodName: key})
}
// ToJSON converts the method map to JSON
func (d *DB) ToJSON() (string, error) {
// Lock the db whilst processing and unlock on return
d.lock.RLock()
defer d.lock.RUnlock()
d.UpdateObfuscatedCallMap()
bytes, err := json.Marshal(&d.store)
// Return zero copy string as this string will be read only
result := *(*string)(unsafe.Pointer(&bytes))
return result, err
}
// UpdateObfuscatedCallMap sets up the secure call mappings
func (d *DB) UpdateObfuscatedCallMap() map[string]int {
mappings := make(map[string]int)
for id, k := range d.obfuscatedMethodArray {
mappings[k.methodName] = id
}
return mappings
}

View File

@@ -0,0 +1,248 @@
package binding
import (
"bytes"
_ "embed"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/leaanthony/slicer"
)
var (
mapRegex *regexp.Regexp
keyPackageIndex int
keyTypeIndex int
valueArrayIndex int
valuePackageIndex int
valueTypeIndex int
)
func init() {
mapRegex = regexp.MustCompile(`(?:map\[(?:(?P<keyPackage>\w+)\.)?(?P<keyType>\w+)])?(?P<valueArray>\[])?(?:\*?(?P<valuePackage>\w+)\.)?(?P<valueType>.+)`)
keyPackageIndex = mapRegex.SubexpIndex("keyPackage")
keyTypeIndex = mapRegex.SubexpIndex("keyType")
valueArrayIndex = mapRegex.SubexpIndex("valueArray")
valuePackageIndex = mapRegex.SubexpIndex("valuePackage")
valueTypeIndex = mapRegex.SubexpIndex("valueType")
}
func (b *Bindings) GenerateGoBindings(baseDir string) error {
store := b.db.store
var obfuscatedBindings map[string]int
if b.obfuscate {
obfuscatedBindings = b.db.UpdateObfuscatedCallMap()
}
for packageName, structs := range store {
packageDir := filepath.Join(baseDir, packageName)
err := fs.Mkdir(packageDir)
if err != nil {
return err
}
for structName, methods := range structs {
var jsoutput bytes.Buffer
jsoutput.WriteString(`// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
`)
var tsBody bytes.Buffer
var tsContent bytes.Buffer
tsContent.WriteString(`// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
`)
// Sort the method names alphabetically
methodNames := make([]string, 0, len(methods))
for methodName := range methods {
methodNames = append(methodNames, methodName)
}
sort.Strings(methodNames)
var importNamespaces slicer.StringSlicer
for _, methodName := range methodNames {
// Get the method details
methodDetails := methods[methodName]
// Generate JS
var args slicer.StringSlicer
for count := range methodDetails.Inputs {
arg := fmt.Sprintf("arg%d", count+1)
args.Add(arg)
}
argsString := args.Join(", ")
jsoutput.WriteString(fmt.Sprintf("\nexport function %s(%s) {", methodName, argsString))
jsoutput.WriteString("\n")
if b.obfuscate {
id := obfuscatedBindings[strings.Join([]string{packageName, structName, methodName}, ".")]
jsoutput.WriteString(fmt.Sprintf(" return ObfuscatedCall(%d, [%s]);", id, argsString))
} else {
jsoutput.WriteString(fmt.Sprintf(" return window['go']['%s']['%s']['%s'](%s);", packageName, structName, methodName, argsString))
}
jsoutput.WriteString("\n}\n")
// Generate TS
tsBody.WriteString(fmt.Sprintf("\nexport function %s(", methodName))
args.Clear()
for count, input := range methodDetails.Inputs {
arg := fmt.Sprintf("arg%d", count+1)
entityName := entityFullReturnType(input.TypeName, b.tsPrefix, b.tsSuffix, &importNamespaces)
args.Add(arg + ":" + goTypeToTypescriptType(entityName, &importNamespaces))
}
tsBody.WriteString(args.Join(",") + "):")
// now build Typescript return types
// If there is no return value or only returning error, TS returns Promise<void>
// If returning single value, TS returns Promise<type>
// If returning single value or error, TS returns Promise<type>
// If returning two values, TS returns Promise<type1|type2>
// Otherwise, TS returns Promise<type1> (instead of throwing Go error?)
var returnType string
if methodDetails.OutputCount() == 0 {
returnType = "Promise<void>"
} else if methodDetails.OutputCount() == 1 && methodDetails.Outputs[0].TypeName == "error" {
returnType = "Promise<void>"
} else {
outputTypeName := entityFullReturnType(methodDetails.Outputs[0].TypeName, b.tsPrefix, b.tsSuffix, &importNamespaces)
firstType := goTypeToTypescriptType(outputTypeName, &importNamespaces)
returnType = "Promise<" + firstType
if methodDetails.OutputCount() == 2 && methodDetails.Outputs[1].TypeName != "error" {
outputTypeName = entityFullReturnType(methodDetails.Outputs[1].TypeName, b.tsPrefix, b.tsSuffix, &importNamespaces)
secondType := goTypeToTypescriptType(outputTypeName, &importNamespaces)
returnType += "|" + secondType
}
returnType += ">"
}
tsBody.WriteString(returnType + ";\n")
}
importNamespaces.Deduplicate()
importNamespaces.Each(func(namespace string) {
tsContent.WriteString("import {" + namespace + "} from '../models';\n")
})
tsContent.WriteString(tsBody.String())
jsfilename := filepath.Join(packageDir, structName+".js")
err = os.WriteFile(jsfilename, jsoutput.Bytes(), 0o755)
if err != nil {
return err
}
tsfilename := filepath.Join(packageDir, structName+".d.ts")
err = os.WriteFile(tsfilename, tsContent.Bytes(), 0o755)
if err != nil {
return err
}
}
}
err := b.WriteModels(baseDir)
if err != nil {
return err
}
return nil
}
func fullyQualifiedName(packageName string, typeName string) string {
if len(packageName) > 0 {
return packageName + "." + typeName
}
switch true {
case len(typeName) == 0:
return ""
case typeName == "interface{}" || typeName == "interface {}":
return "any"
case typeName == "string":
return "string"
case typeName == "error":
return "Error"
case
strings.HasPrefix(typeName, "int"),
strings.HasPrefix(typeName, "uint"),
strings.HasPrefix(typeName, "float"):
return "number"
case typeName == "bool":
return "boolean"
default:
return "any"
}
}
var (
jsVariableUnsafeChars = regexp.MustCompile(`[^A-Za-z0-9_]`)
)
func arrayifyValue(valueArray string, valueType string) string {
valueType = strings.ReplaceAll(valueType, "*", "")
gidx := strings.IndexRune(valueType, '[')
if gidx > 0 { // its a generic type
rem := strings.SplitN(valueType, "[", 2)
valueType = rem[0] + "_" + jsVariableUnsafeChars.ReplaceAllLiteralString(rem[1], "_")
}
if len(valueArray) == 0 {
return valueType
}
return "Array<" + valueType + ">"
}
func goTypeToJSDocType(input string, importNamespaces *slicer.StringSlicer) string {
matches := mapRegex.FindStringSubmatch(input)
keyPackage := matches[keyPackageIndex]
keyType := matches[keyTypeIndex]
valueArray := matches[valueArrayIndex]
valuePackage := matches[valuePackageIndex]
valueType := matches[valueTypeIndex]
// fmt.Printf("input=%s, keyPackage=%s, keyType=%s, valueArray=%s, valuePackage=%s, valueType=%s\n",
// input,
// keyPackage,
// keyType,
// valueArray,
// valuePackage,
// valueType)
// byte array is special case
if valueArray == "[]" && valueType == "byte" {
return "string"
}
// if any packages, make sure they're saved
if len(keyPackage) > 0 {
importNamespaces.Add(keyPackage)
}
if len(valuePackage) > 0 {
importNamespaces.Add(valuePackage)
}
key := fullyQualifiedName(keyPackage, keyType)
var value string
if strings.HasPrefix(valueType, "map") {
value = goTypeToJSDocType(valueType, importNamespaces)
} else {
value = fullyQualifiedName(valuePackage, valueType)
}
if len(key) > 0 {
return fmt.Sprintf("Record<%s, %s>", key, arrayifyValue(valueArray, value))
}
return arrayifyValue(valueArray, value)
}
func goTypeToTypescriptType(input string, importNamespaces *slicer.StringSlicer) string {
return goTypeToJSDocType(input, importNamespaces)
}
func entityFullReturnType(input, prefix, suffix string, importNamespaces *slicer.StringSlicer) string {
if strings.ContainsRune(input, '.') {
nameSpace, returnType := getSplitReturn(input)
return nameSpace + "." + prefix + returnType + suffix
}
return input
}

View File

@@ -0,0 +1,28 @@
package binding
import "reflect"
// Parameter defines a Go method parameter
type Parameter struct {
Name string `json:"name,omitempty"`
TypeName string `json:"type"`
reflectType reflect.Type
}
func newParameter(Name string, Type reflect.Type) *Parameter {
return &Parameter{
Name: Name,
TypeName: Type.String(),
reflectType: Type,
}
}
// IsType returns true if the given
func (p *Parameter) IsType(typename string) bool {
return p.TypeName == typename
}
// IsError returns true if the parameter type is an error
func (p *Parameter) IsError() bool {
return p.IsType("error")
}

View File

@@ -0,0 +1,200 @@
package binding
import (
"fmt"
"reflect"
"runtime"
"strings"
)
// isStructPtr returns true if the value given is a
// pointer to a struct
func isStructPtr(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Ptr &&
reflect.ValueOf(value).Elem().Kind() == reflect.Struct
}
// isFunction returns true if the given value is a function
func isFunction(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Func
}
// isStruct returns true if the value given is a struct
func isStruct(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Struct
}
func normalizeStructName(name string) string {
return strings.ReplaceAll(
strings.ReplaceAll(
strings.ReplaceAll(
strings.ReplaceAll(
name,
",",
"-",
),
"*",
"",
),
"]",
"__",
),
"[",
"__",
)
}
func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
// Create result placeholder
var result []*BoundMethod
// Check type
if !isStructPtr(value) {
if isStruct(value) {
name := reflect.ValueOf(value).Type().Name()
return nil, fmt.Errorf("%s is a struct, not a pointer to a struct", name)
}
if isFunction(value) {
name := runtime.FuncForPC(reflect.ValueOf(value).Pointer()).Name()
return nil, fmt.Errorf("%s is a function, not a pointer to a struct. Wails v2 has deprecated the binding of functions. Please wrap your functions up in a struct and bind a pointer to that struct.", name)
}
return nil, fmt.Errorf("not a pointer to a struct.")
}
// Process Struct
structType := reflect.TypeOf(value)
structValue := reflect.ValueOf(value)
structName := structType.Elem().Name()
structNameNormalized := normalizeStructName(structName)
pkgPath := strings.TrimSuffix(structType.Elem().String(), fmt.Sprintf(".%s", structName))
// Process Methods
for i := 0; i < structType.NumMethod(); i++ {
methodDef := structType.Method(i)
methodName := methodDef.Name
method := structValue.MethodByName(methodName)
methodReflectName := runtime.FuncForPC(methodDef.Func.Pointer()).Name()
if b.exemptions.Contains(methodReflectName) {
continue
}
// Create new method
boundMethod := &BoundMethod{
Path: &BoundedMethodPath{
Package: pkgPath,
Struct: structNameNormalized,
Name: methodName,
},
Inputs: nil,
Outputs: nil,
Comments: "",
Method: method,
}
// Iterate inputs
methodType := method.Type()
inputParamCount := methodType.NumIn()
var inputs []*Parameter
for inputIndex := 0; inputIndex < inputParamCount; inputIndex++ {
input := methodType.In(inputIndex)
thisParam := newParameter("", input)
thisInput := input
if thisInput.Kind() == reflect.Slice {
thisInput = thisInput.Elem()
}
// Process struct pointer params
if thisInput.Kind() == reflect.Ptr {
if thisInput.Elem().Kind() == reflect.Struct {
typ := thisInput.Elem()
a := reflect.New(typ)
s := reflect.Indirect(a).Interface()
name := typ.Name()
packageName := getPackageName(thisInput.String())
b.AddStructToGenerateTS(packageName, name, s)
}
}
// Process struct params
if thisInput.Kind() == reflect.Struct {
a := reflect.New(thisInput)
s := reflect.Indirect(a).Interface()
name := thisInput.Name()
packageName := getPackageName(thisInput.String())
b.AddStructToGenerateTS(packageName, name, s)
}
inputs = append(inputs, thisParam)
}
boundMethod.Inputs = inputs
// Iterate outputs
// TODO: Determine what to do about limiting return types
// especially around errors.
outputParamCount := methodType.NumOut()
var outputs []*Parameter
for outputIndex := 0; outputIndex < outputParamCount; outputIndex++ {
output := methodType.Out(outputIndex)
thisParam := newParameter("", output)
thisOutput := output
if thisOutput.Kind() == reflect.Slice {
thisOutput = thisOutput.Elem()
}
// Process struct pointer params
if thisOutput.Kind() == reflect.Ptr {
if thisOutput.Elem().Kind() == reflect.Struct {
typ := thisOutput.Elem()
a := reflect.New(typ)
s := reflect.Indirect(a).Interface()
name := typ.Name()
packageName := getPackageName(thisOutput.String())
b.AddStructToGenerateTS(packageName, name, s)
}
}
// Process struct params
if thisOutput.Kind() == reflect.Struct {
a := reflect.New(thisOutput)
s := reflect.Indirect(a).Interface()
name := thisOutput.Name()
packageName := getPackageName(thisOutput.String())
b.AddStructToGenerateTS(packageName, name, s)
}
outputs = append(outputs, thisParam)
}
boundMethod.Outputs = outputs
// Save method in result
result = append(result, boundMethod)
}
return result, nil
}
func getPackageName(in string) string {
result := strings.Split(in, ".")[0]
result = strings.ReplaceAll(result, "[]", "")
result = strings.ReplaceAll(result, "*", "")
return result
}
func getSplitReturn(in string) (string, string) {
result := strings.SplitN(in, ".", 2)
return result[0], result[1]
}
func hasElements(typ reflect.Type) bool {
kind := typ.Kind()
return kind == reflect.Ptr || kind == reflect.Array || kind == reflect.Slice || kind == reflect.Map
}

View File

@@ -0,0 +1,5 @@
package frontend
type Calls interface {
Callback(message string)
}

View File

@@ -0,0 +1,33 @@
//
// AppDelegate.h
// test
//
// Created by Lea Anthony on 10/10/21.
//
#ifndef AppDelegate_h
#define AppDelegate_h
#import <Cocoa/Cocoa.h>
#import "WailsContext.h"
@interface AppDelegate : NSResponder <NSApplicationDelegate, NSTouchBarProvider>
@property bool alwaysOnTop;
@property bool startHidden;
@property (retain) NSString* singleInstanceUniqueId;
@property bool singleInstanceLockEnabled;
@property bool startFullscreen;
@property (retain) WailsWindow* mainWindow;
@end
extern void HandleOpenFile(char *);
extern void HandleSecondInstanceData(char * message);
void SendDataToFirstInstance(char * singleInstanceUniqueId, char * text);
char* GetMacOsNativeTempDir();
#endif /* AppDelegate_h */

View File

@@ -0,0 +1,100 @@
//
// AppDelegate.m
// test
//
// Created by Lea Anthony on 10/10/21.
//
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#import "AppDelegate.h"
#import "CustomProtocol.h"
#import "message.h"
@implementation AppDelegate
-(BOOL)application:(NSApplication *)sender openFile:(NSString *)filename
{
const char* utf8FileName = filename.UTF8String;
HandleOpenFile((char*)utf8FileName);
return YES;
}
- (BOOL)application:(NSApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<NSUserActivityRestoring>> * _Nullable))restorationHandler {
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
NSURL *url = userActivity.webpageURL;
if (url) {
HandleOpenURL((char*)[[url absoluteString] UTF8String]);
return YES;
}
}
return NO;
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {
return NO;
}
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
processMessage("Q");
return NSTerminateCancel;
}
- (void)applicationWillFinishLaunching:(NSNotification *)aNotification {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
if (self.alwaysOnTop) {
[self.mainWindow setLevel:NSFloatingWindowLevel];
}
if ( !self.startHidden ) {
[self.mainWindow makeKeyAndOrderFront:self];
}
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSApp activateIgnoringOtherApps:YES];
if ( self.startFullscreen ) {
NSWindowCollectionBehavior behaviour = [self.mainWindow collectionBehavior];
behaviour |= NSWindowCollectionBehaviorFullScreenPrimary;
[self.mainWindow setCollectionBehavior:behaviour];
[self.mainWindow toggleFullScreen:nil];
}
if ( self.singleInstanceLockEnabled ) {
[[NSDistributedNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleSecondInstanceNotification:) name:self.singleInstanceUniqueId object:nil];
}
}
void SendDataToFirstInstance(char * singleInstanceUniqueId, char * message) {
// we pass message in object because otherwise sandboxing will prevent us from sending it https://developer.apple.com/forums/thread/129437
NSString * myString = [NSString stringWithUTF8String:message];
[[NSDistributedNotificationCenter defaultCenter]
postNotificationName:[NSString stringWithUTF8String:singleInstanceUniqueId]
object:(__bridge const void *)(myString)
userInfo:nil
deliverImmediately:YES];
}
char* GetMacOsNativeTempDir() {
NSString *tempDir = NSTemporaryDirectory();
char *copy = strdup([tempDir UTF8String]);
return copy;
}
- (void)handleSecondInstanceNotification:(NSNotification *)note;
{
if (note.object != nil) {
NSString * message = (__bridge NSString *)note.object;
const char* utf8Message = message.UTF8String;
HandleSecondInstanceData((char*)utf8Message);
}
}
- (void)dealloc {
[super dealloc];
}
@synthesize touchBar;
@end

View File

@@ -0,0 +1,89 @@
//
// Application.h
// test
//
// Created by Lea Anthony on 10/10/21.
//
#ifndef Application_h
#define Application_h
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#import "WailsContext.h"
#define WindowStartsNormal 0
#define WindowStartsMaximised 1
#define WindowStartsMinimised 2
#define WindowStartsFullscreen 3
WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int zoomable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int contentProtection, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences, int singleInstanceLockEnabled, const char* singleInstanceUniqueId, bool enableDragAndDrop, bool disableWebViewDragAndDrop);
void Run(void*, const char* url);
void SetTitle(void* ctx, const char *title);
void Center(void* ctx);
void SetSize(void* ctx, int width, int height);
void SetAlwaysOnTop(void* ctx, int onTop);
void SetMinSize(void* ctx, int width, int height);
void SetMaxSize(void* ctx, int width, int height);
void SetPosition(void* ctx, int x, int y);
void Fullscreen(void* ctx);
void UnFullscreen(void* ctx);
void Minimise(void* ctx);
void UnMinimise(void* ctx);
void ToggleMaximise(void* ctx);
void Maximise(void* ctx);
void UnMaximise(void* ctx);
void Hide(void* ctx);
void Show(void* ctx);
void HideApplication(void* ctx);
void ShowApplication(void* ctx);
void SetBackgroundColour(void* ctx, int r, int g, int b, int a);
void ExecJS(void* ctx, const char*);
void Quit(void*);
void WindowPrint(void* ctx);
const char* GetSize(void *ctx);
const char* GetPosition(void *ctx);
const bool IsFullScreen(void *ctx);
const bool IsMinimised(void *ctx);
const bool IsMaximised(void *ctx);
/* Dialogs */
void MessageDialog(void *inctx, const char* dialogType, const char* title, const char* message, const char* button1, const char* button2, const char* button3, const char* button4, const char* defaultButton, const char* cancelButton, void* iconData, int iconDataLength);
void OpenFileDialog(void *inctx, const char* title, const char* defaultFilename, const char* defaultDirectory, int allowDirectories, int allowFiles, int canCreateDirectories, int treatPackagesAsDirectories, int resolveAliases, int showHiddenFiles, int allowMultipleSelection, const char* filters);
void SaveFileDialog(void *inctx, const char* title, const char* defaultFilename, const char* defaultDirectory, int canCreateDirectories, int treatPackagesAsDirectories, int showHiddenFiles, const char* filters);
/* Application Menu */
void* NewMenu(const char* name);
void AppendSubmenu(void* parent, void* child);
void AppendRole(void *inctx, void *inMenu, int role);
void SetAsApplicationMenu(void *inctx, void *inMenu);
void UpdateApplicationMenu(void *inctx);
void SetAbout(void *inctx, const char* title, const char* description, void* imagedata, int datalen);
void* AppendMenuItem(void* inctx, void* nsmenu, const char* label, const char* shortcutKey, int modifiers, int disabled, int checked, int menuItemID);
void AppendSeparator(void* inMenu);
void UpdateMenuItem(void* nsmenuitem, int checked);
void RunMainLoop(void);
void ReleaseContext(void *inctx);
/* Notifications */
bool IsNotificationAvailable(void *inctx);
bool CheckBundleIdentifier(void *inctx);
bool EnsureDelegateInitialized(void *inctx);
void RequestNotificationAuthorization(void *inctx, int channelID);
void CheckNotificationAuthorization(void *inctx, int channelID);
void SendNotification(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json);
void SendNotificationWithActions(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *categoryId, const char *actions_json);
void RegisterNotificationCategory(void *inctx, int channelID, const char *categoryId, const char *actions_json, bool hasReplyField, const char *replyPlaceholder, const char *replyButtonTitle);
void RemoveNotificationCategory(void *inctx, int channelID, const char *categoryId);
void RemoveAllPendingNotifications(void *inctx);
void RemovePendingNotification(void *inctx, const char *identifier);
void RemoveAllDeliveredNotifications(void *inctx);
void RemoveDeliveredNotification(void *inctx, const char *identifier);
NSString* safeInit(const char* input);
#endif /* Application_h */

View File

@@ -0,0 +1,501 @@
//go:build darwin
//
// Application.m
//
// Created by Lea Anthony on 10/10/21.
//
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#import "WailsContext.h"
#import "Application.h"
#import "AppDelegate.h"
#import "WindowDelegate.h"
#import "WailsMenu.h"
#import "WailsMenuItem.h"
WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int zoomable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int contentProtection, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences, int singleInstanceLockEnabled, const char* singleInstanceUniqueId, bool enableDragAndDrop, bool disableWebViewDragAndDrop) {
[NSApplication sharedApplication];
WailsContext *result = [WailsContext new];
result.devtoolsEnabled = devtoolsEnabled;
result.defaultContextMenuEnabled = defaultContextMenuEnabled;
if ( windowStartState == WindowStartsFullscreen ) {
fullscreen = 1;
}
[result CreateWindow:width :height :frameless :resizable :zoomable :fullscreen :fullSizeContent :hideTitleBar :titlebarAppearsTransparent :hideTitle :useToolbar :hideToolbarSeparator :webviewIsTransparent :hideWindowOnClose :safeInit(appearance) :windowIsTranslucent :minWidth :minHeight :maxWidth :maxHeight :fraudulentWebsiteWarningEnabled :preferences :enableDragAndDrop :disableWebViewDragAndDrop];
[result SetTitle:safeInit(title)];
[result Center];
if (contentProtection == 1 &&
[result.mainWindow respondsToSelector:@selector(setSharingType:)]) {
[result.mainWindow setSharingType:NSWindowSharingNone];
}
switch( windowStartState ) {
case WindowStartsMaximised:
[result.mainWindow zoom:nil];
break;
case WindowStartsMinimised:
//TODO: Can you start a mac app minimised?
break;
}
if ( startsHidden == 1 ) {
result.startHidden = true;
}
if ( fullscreen == 1 ) {
result.startFullscreen = true;
}
if ( singleInstanceLockEnabled == 1 ) {
result.singleInstanceLockEnabled = true;
result.singleInstanceUniqueId = safeInit(singleInstanceUniqueId);
}
result.alwaysOnTop = alwaysOnTop;
result.hideOnClose = hideWindowOnClose;
return result;
}
void ExecJS(void* inctx, const char *script) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSString *nsscript = safeInit(script);
ON_MAIN_THREAD(
[ctx ExecJS:nsscript];
[nsscript release];
);
}
void SetTitle(void* inctx, const char *title) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSString *_title = safeInit(title);
ON_MAIN_THREAD(
[ctx SetTitle:_title];
);
}
void SetBackgroundColour(void *inctx, int r, int g, int b, int a) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx SetBackgroundColour:r :g :b :a];
);
}
void SetSize(void* inctx, int width, int height) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx SetSize:width :height];
);
}
void SetAlwaysOnTop(void* inctx, int onTop) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx SetAlwaysOnTop:onTop];
);
}
void SetMinSize(void* inctx, int width, int height) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx SetMinSize:width :height];
);
}
void SetMaxSize(void* inctx, int width, int height) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx SetMaxSize:width :height];
);
}
void SetPosition(void* inctx, int x, int y) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx SetPosition:x :y];
);
}
void Center(void* inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx Center];
);
}
void Fullscreen(void* inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx Fullscreen];
);
}
void UnFullscreen(void* inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx UnFullscreen];
);
}
void Minimise(void* inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx Minimise];
);
}
void UnMinimise(void* inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx UnMinimise];
);
}
void Maximise(void* inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx Maximise];
);
}
void ToggleMaximise(void* inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx ToggleMaximise];
);
}
const char* GetSize(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSRect frame = [ctx.mainWindow frame];
NSString *result = [NSString stringWithFormat:@"%d,%d", (int)frame.size.width, (int)frame.size.height];
return [result UTF8String];
}
const char* GetPosition(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSScreen* screen = [ctx getCurrentScreen];
NSRect windowFrame = [ctx.mainWindow frame];
NSRect screenFrame = [screen visibleFrame];
int x = windowFrame.origin.x - screenFrame.origin.x;
int y = windowFrame.origin.y - screenFrame.origin.y;
y = screenFrame.size.height - y - windowFrame.size.height;
NSString *result = [NSString stringWithFormat:@"%d,%d",x,y];
return [result UTF8String];
}
const bool IsFullScreen(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
return [ctx IsFullScreen];
}
const bool IsMinimised(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
return [ctx IsMinimised];
}
const bool IsMaximised(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
return [ctx IsMaximised];
}
void UnMaximise(void* inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx UnMaximise];
);
}
void Quit(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
[NSApp stop:ctx];
[NSApp abortModal];
}
void Hide(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx Hide];
);
}
void Show(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx Show];
);
}
void HideApplication(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx HideApplication];
);
}
void ShowApplication(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
[ctx ShowApplication];
);
}
NSString* safeInit(const char* input) {
NSString *result = nil;
if (input != nil) {
result = [NSString stringWithUTF8String:input];
}
return result;
}
void MessageDialog(void *inctx, const char* dialogType, const char* title, const char* message, const char* button1, const char* button2, const char* button3, const char* button4, const char* defaultButton, const char* cancelButton, void* iconData, int iconDataLength) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSString *_dialogType = safeInit(dialogType);
NSString *_title = safeInit(title);
NSString *_message = safeInit(message);
NSString *_button1 = safeInit(button1);
NSString *_button2 = safeInit(button2);
NSString *_button3 = safeInit(button3);
NSString *_button4 = safeInit(button4);
NSString *_defaultButton = safeInit(defaultButton);
NSString *_cancelButton = safeInit(cancelButton);
ON_MAIN_THREAD(
[ctx MessageDialog:_dialogType :_title :_message :_button1 :_button2 :_button3 :_button4 :_defaultButton :_cancelButton :iconData :iconDataLength];
)
}
void OpenFileDialog(void *inctx, const char* title, const char* defaultFilename, const char* defaultDirectory, int allowDirectories, int allowFiles, int canCreateDirectories, int treatPackagesAsDirectories, int resolveAliases, int showHiddenFiles, int allowMultipleSelection, const char* filters) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSString *_title = safeInit(title);
NSString *_defaultFilename = safeInit(defaultFilename);
NSString *_defaultDirectory = safeInit(defaultDirectory);
NSString *_filters = safeInit(filters);
ON_MAIN_THREAD(
[ctx OpenFileDialog:_title :_defaultFilename :_defaultDirectory :allowDirectories :allowFiles :canCreateDirectories :treatPackagesAsDirectories :resolveAliases :showHiddenFiles :allowMultipleSelection :_filters];
)
}
void SaveFileDialog(void *inctx, const char* title, const char* defaultFilename, const char* defaultDirectory, int canCreateDirectories, int treatPackagesAsDirectories, int showHiddenFiles, const char* filters) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSString *_title = safeInit(title);
NSString *_defaultFilename = safeInit(defaultFilename);
NSString *_defaultDirectory = safeInit(defaultDirectory);
NSString *_filters = safeInit(filters);
ON_MAIN_THREAD(
[ctx SaveFileDialog:_title :_defaultFilename :_defaultDirectory :canCreateDirectories :treatPackagesAsDirectories :showHiddenFiles :_filters];
)
}
void AppendRole(void *inctx, void *inMenu, int role) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
WailsMenu *menu = (__bridge WailsMenu*) inMenu;
[menu appendRole :ctx :role];
}
void* NewMenu(const char *name) {
NSString *title = @"";
if (name != nil) {
title = [NSString stringWithUTF8String:name];
}
WailsMenu *result = [[WailsMenu new] initWithNSTitle:title];
return result;
}
void AppendSubmenu(void* inparent, void* inchild) {
WailsMenu *parent = (__bridge WailsMenu*) inparent;
WailsMenu *child = (__bridge WailsMenu*) inchild;
[parent appendSubmenu:child];
}
void SetAsApplicationMenu(void *inctx, void *inMenu) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
WailsMenu *menu = (__bridge WailsMenu*) inMenu;
ctx.applicationMenu = menu;
}
void UpdateApplicationMenu(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD(
NSApplication *app = [NSApplication sharedApplication];
[app setMainMenu:ctx.applicationMenu];
)
}
void SetAbout(void *inctx, const char* title, const char* description, void* imagedata, int datalen) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSString *_title = safeInit(title);
NSString *_description = safeInit(description);
[ctx SetAbout :_title :_description :imagedata :datalen];
}
void* AppendMenuItem(void* inctx, void* inMenu, const char* label, const char* shortcutKey, int modifiers, int disabled, int checked, int menuItemID) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
WailsMenu *menu = (__bridge WailsMenu*) inMenu;
NSString *_label = safeInit(label);
NSString *_shortcutKey = safeInit(shortcutKey);
return [menu AppendMenuItem:ctx :_label :_shortcutKey :modifiers :disabled :checked :menuItemID];
}
void UpdateMenuItem(void* nsmenuitem, int checked) {
ON_MAIN_THREAD(
WailsMenuItem *menuItem = (__bridge WailsMenuItem*) nsmenuitem;
[menuItem setState:(checked == 1?NSControlStateValueOn:NSControlStateValueOff)];
)
}
void AppendSeparator(void* inMenu) {
WailsMenu *menu = (__bridge WailsMenu*) inMenu;
[menu AppendSeparator];
}
bool IsNotificationAvailable(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
return [ctx IsNotificationAvailable];
}
bool CheckBundleIdentifier(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
return [ctx CheckBundleIdentifier];
}
bool EnsureDelegateInitialized(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
return [ctx EnsureDelegateInitialized];
}
void RequestNotificationAuthorization(void *inctx, int channelID) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx RequestNotificationAuthorization:channelID];
}
void CheckNotificationAuthorization(void *inctx, int channelID) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx CheckNotificationAuthorization:channelID];
}
void SendNotification(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx SendNotification:channelID :identifier :title :subtitle :body :data_json];
}
void SendNotificationWithActions(void *inctx, int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *categoryId, const char *actions_json) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx SendNotificationWithActions:channelID :identifier :title :subtitle :body :categoryId :actions_json];
}
void RegisterNotificationCategory(void *inctx, int channelID, const char *categoryId, const char *actions_json, bool hasReplyField, const char *replyPlaceholder, const char *replyButtonTitle) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx RegisterNotificationCategory:channelID :categoryId :actions_json :hasReplyField :replyPlaceholder :replyButtonTitle];
}
void RemoveNotificationCategory(void *inctx, int channelID, const char *categoryId) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx RemoveNotificationCategory:channelID :categoryId];
}
void RemoveAllPendingNotifications(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx RemoveAllPendingNotifications];
}
void RemovePendingNotification(void *inctx, const char *identifier) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx RemovePendingNotification:identifier];
}
void RemoveAllDeliveredNotifications(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx RemoveAllDeliveredNotifications];
}
void RemoveDeliveredNotification(void *inctx, const char *identifier) {
WailsContext *ctx = (__bridge WailsContext*)inctx;
[ctx RemoveDeliveredNotification:identifier];
}
void Run(void *inctx, const char* url) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSApplication *app = [NSApplication sharedApplication];
AppDelegate* delegate = [AppDelegate new];
[app setDelegate:(id)delegate];
ctx.appdelegate = delegate;
delegate.mainWindow = ctx.mainWindow;
delegate.alwaysOnTop = ctx.alwaysOnTop;
delegate.startHidden = ctx.startHidden;
delegate.singleInstanceLockEnabled = ctx.singleInstanceLockEnabled;
delegate.singleInstanceUniqueId = ctx.singleInstanceUniqueId;
delegate.startFullscreen = ctx.startFullscreen;
NSString *_url = safeInit(url);
[ctx loadRequest:_url];
[_url release];
[app setMainMenu:ctx.applicationMenu];
}
void RunMainLoop(void) {
NSApplication *app = [NSApplication sharedApplication];
[app run];
}
void ReleaseContext(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
[ctx release];
}
// Credit: https://stackoverflow.com/q/33319295
void WindowPrint(void *inctx) {
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 110000
if (@available(macOS 11.0, *)) {
ON_MAIN_THREAD(
WailsContext *ctx = (__bridge WailsContext*) inctx;
WKWebView* webView = ctx.webview;
// I think this should be exposed as a config
// It directly affects the printed output/PDF
NSPrintInfo *pInfo = [NSPrintInfo sharedPrintInfo];
pInfo.horizontalPagination = NSPrintingPaginationModeAutomatic;
pInfo.verticalPagination = NSPrintingPaginationModeAutomatic;
pInfo.verticallyCentered = YES;
pInfo.horizontallyCentered = YES;
pInfo.orientation = NSPaperOrientationLandscape;
pInfo.leftMargin = 0;
pInfo.rightMargin = 0;
pInfo.topMargin = 0;
pInfo.bottomMargin = 0;
NSPrintOperation *po = [webView printOperationWithPrintInfo:pInfo];
po.showsPrintPanel = YES;
po.showsProgressPanel = YES;
po.view.frame = webView.bounds;
[po runOperationModalForWindow:ctx.mainWindow delegate:ctx.mainWindow.delegate didRunSelector:nil contextInfo:nil];
)
}
#endif
}

View File

@@ -0,0 +1,14 @@
#ifndef CustomProtocol_h
#define CustomProtocol_h
#import <Cocoa/Cocoa.h>
extern void HandleOpenURL(char*);
@interface CustomProtocolSchemeHandler : NSObject
+ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent;
@end
void StartCustomProtocolHandler(void);
#endif /* CustomProtocol_h */

View File

@@ -0,0 +1,20 @@
#include "CustomProtocol.h"
@implementation CustomProtocolSchemeHandler
+ (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
[event paramDescriptorForKeyword:keyDirectObject];
NSString *urlStr = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
HandleOpenURL((char*)[[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String]);
}
@end
void StartCustomProtocolHandler(void) {
NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager];
[appleEventManager setEventHandler:[CustomProtocolSchemeHandler class]
andSelector:@selector(handleGetURLEvent:withReplyEvent:)
forEventClass:kInternetEventClass
andEventID: kAEGetURL];
}

View File

@@ -0,0 +1,17 @@
//
// Role.h
// test
//
// Created by Lea Anthony on 24/10/21.
//
#ifndef Role_h
#define Role_h
typedef int Role;
static const Role AppMenu = 1;
static const Role EditMenu = 2;
static const Role WindowMenu = 3;
#endif /* Role_h */

View File

@@ -0,0 +1,18 @@
//
// WailsAlert.h
// test
//
// Created by Lea Anthony on 20/10/21.
//
#ifndef WailsAlert_h
#define WailsAlert_h
#import <Cocoa/Cocoa.h>
@interface WailsAlert : NSAlert
- (void)addButton:(NSString*)text :(NSString*)defaultButton :(NSString*)cancelButton;
@end
#endif /* WailsAlert_h */

View File

@@ -0,0 +1,31 @@
//go:build darwin
//
// WailsAlert.m
// test
//
// Created by Lea Anthony on 20/10/21.
//
#import <Foundation/Foundation.h>
#import "WailsAlert.h"
@implementation WailsAlert
- (void)addButton:(NSString*)text :(NSString*)defaultButton :(NSString*)cancelButton {
if( text == nil ) {
return;
}
NSButton *button = [self addButtonWithTitle:text];
if( defaultButton != nil && [text isEqualToString:defaultButton]) {
[button setKeyEquivalent:@"\r"];
} else if( cancelButton != nil && [text isEqualToString:cancelButton]) {
[button setKeyEquivalent:@"\033"];
} else {
[button setKeyEquivalent:@""];
}
}
@end

View File

@@ -0,0 +1,123 @@
//
// WailsContext.h
// test
//
// Created by Lea Anthony on 10/10/21.
//
#ifndef WailsContext_h
#define WailsContext_h
#import <Cocoa/Cocoa.h>
#import <WebKit/WebKit.h>
#import "WailsWebView.h"
#if __has_include(<UniformTypeIdentifiers/UTType.h>)
#define USE_NEW_FILTERS
#import <UniformTypeIdentifiers/UTType.h>
#endif
#define ON_MAIN_THREAD(str) dispatch_async(dispatch_get_main_queue(), ^{ str; });
#define unicode(input) [NSString stringWithFormat:@"%C", input]
@interface WailsWindow : NSWindow
@property NSSize userMinSize;
@property NSSize userMaxSize;
- (BOOL) canBecomeKeyWindow;
- (void) applyWindowConstraints;
- (void) disableWindowConstraints;
@end
@interface WailsContext : NSObject <WKURLSchemeHandler,WKScriptMessageHandler,WKNavigationDelegate,WKUIDelegate>
@property (retain) WailsWindow* mainWindow;
@property (retain) WailsWebView* webview;
@property (nonatomic, assign) id appdelegate;
@property bool hideOnClose;
@property bool shuttingDown;
@property bool startHidden;
@property bool startFullscreen;
@property bool singleInstanceLockEnabled;
@property (retain) NSString* singleInstanceUniqueId;
@property (retain) NSEvent* mouseEvent;
@property bool alwaysOnTop;
@property bool devtoolsEnabled;
@property bool defaultContextMenuEnabled;
@property (retain) WKUserContentController* userContentController;
@property (retain) NSMenu* applicationMenu;
@property (retain) NSImage* aboutImage;
@property (retain) NSString* aboutTitle;
@property (retain) NSString* aboutDescription;
struct Preferences {
bool *tabFocusesLinks;
bool *textInteractionEnabled;
bool *fullscreenEnabled;
};
- (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)zoomable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString *)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight :(bool)fraudulentWebsiteWarningEnabled :(struct Preferences)preferences :(bool)enableDragAndDrop :(bool)disableWebViewDragAndDrop;
- (void) SetSize:(int)width :(int)height;
- (void) SetPosition:(int)x :(int) y;
- (void) SetMinSize:(int)minWidth :(int)minHeight;
- (void) SetMaxSize:(int)maxWidth :(int)maxHeight;
- (void) SetTitle:(NSString*)title;
- (void) SetAlwaysOnTop:(int)onTop;
- (void) Center;
- (void) Fullscreen;
- (void) UnFullscreen;
- (bool) IsFullScreen;
- (void) Minimise;
- (void) UnMinimise;
- (bool) IsMinimised;
- (void) Maximise;
- (void) ToggleMaximise;
- (void) UnMaximise;
- (bool) IsMaximised;
- (void) SetBackgroundColour:(int)r :(int)g :(int)b :(int)a;
- (void) HideMouse;
- (void) ShowMouse;
- (void) Hide;
- (void) Show;
- (void) HideApplication;
- (void) ShowApplication;
- (void) Quit;
- (void) MessageDialog :(NSString*)dialogType :(NSString*)title :(NSString*)message :(NSString*)button1 :(NSString*)button2 :(NSString*)button3 :(NSString*)button4 :(NSString*)defaultButton :(NSString*)cancelButton :(void*)iconData :(int)iconDataLength;
- (void) OpenFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)allowDirectories :(bool)allowFiles :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)resolveAliases :(bool)showHiddenFiles :(bool)allowMultipleSelection :(NSString*)filters;
- (void) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters;
- (bool) IsNotificationAvailable;
- (bool) CheckBundleIdentifier;
- (bool) EnsureDelegateInitialized;
- (void) RequestNotificationAuthorization:(int)channelID;
- (void) CheckNotificationAuthorization:(int)channelID;
- (void) SendNotification:(int)channelID :(const char *)identifier :(const char *)title :(const char *)subtitle :(const char *)body :(const char *)dataJSON;
- (void) SendNotificationWithActions:(int)channelID :(const char *)identifier :(const char *)title :(const char *)subtitle :(const char *)body :(const char *)categoryId :(const char *)actionsJSON;
- (void) RegisterNotificationCategory:(int)channelID :(const char *)categoryId :(const char *)actionsJSON :(bool)hasReplyField :(const char *)replyPlaceholder :(const char *)replyButtonTitle;
- (void) RemoveNotificationCategory:(int)channelID :(const char *)categoryId;
- (void) RemoveAllPendingNotifications;
- (void) RemovePendingNotification:(const char *)identifier;
- (void) RemoveAllDeliveredNotifications;
- (void) RemoveDeliveredNotification:(const char *)identifier;
- (void) loadRequest:(NSString*)url;
- (void) ExecJS:(NSString*)script;
- (NSScreen*) getCurrentScreen;
- (void) SetAbout :(NSString*)title :(NSString*)description :(void*)imagedata :(int)datalen;
- (void) dealloc;
@end
#endif /* WailsContext_h */

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
//
// WailsMenu.h
// test
//
// Created by Lea Anthony on 25/10/21.
//
#ifndef WailsMenu_h
#define WailsMenu_h
#import <Cocoa/Cocoa.h>
#import "Role.h"
#import "WailsMenu.h"
#import "WailsContext.h"
@interface WailsMenu : NSMenu
//- (void) AddMenuByRole :(Role)role;
- (WailsMenu*) initWithNSTitle :(NSString*)title;
- (void) appendSubmenu :(WailsMenu*)child;
- (void) appendRole :(WailsContext*)ctx :(Role)role;
- (NSMenuItem*) newMenuItemWithContext :(WailsContext*)ctx :(NSString*)title :(SEL)selector :(NSString*)key :(NSEventModifierFlags)flags;
- (void*) AppendMenuItem :(WailsContext*)ctx :(NSString*)label :(NSString *)shortcutKey :(int)modifiers :(bool)disabled :(bool)checked :(int)menuItemID;
- (void) AppendSeparator;
@end
#endif /* WailsMenu_h */

View File

@@ -0,0 +1,340 @@
//go:build darwin
//
// WailsMenu.m
// test
//
// Created by Lea Anthony on 25/10/21.
//
#import <Foundation/Foundation.h>
#import "WailsMenu.h"
#import "WailsMenuItem.h"
#import "Role.h"
@implementation WailsMenu
- (NSMenuItem*) newMenuItem :(NSString*)title :(SEL)selector :(NSString*)key :(NSEventModifierFlags)flags {
NSMenuItem *result = [[[NSMenuItem alloc] initWithTitle:title action:selector keyEquivalent:key] autorelease];
[result setKeyEquivalentModifierMask:flags];
return result;
}
- (NSMenuItem*) newMenuItemWithContext :(WailsContext*)ctx :(NSString*)title :(SEL)selector :(NSString*)key :(NSEventModifierFlags)flags {
NSMenuItem *result = [NSMenuItem new];
if ( title != nil ) {
[result setTitle:title];
}
if (selector != nil) {
[result setAction:selector];
}
if (key) {
[result setKeyEquivalent:key];
}
if( flags != 0 ) {
[result setKeyEquivalentModifierMask:flags];
}
result.target = ctx;
return result;
}
- (NSMenuItem*) newMenuItem :(NSString*)title :(SEL)selector :(NSString*)key {
return [self newMenuItem :title :selector :key :0];
}
- (WailsMenu*) initWithNSTitle:(NSString *)title {
if( title != nil ) {
[super initWithTitle:title];
} else {
[self init];
}
[self setAutoenablesItems:NO];
return self;
}
- (void) appendSubmenu :(WailsMenu*)child {
NSMenuItem *childMenuItem = [[NSMenuItem new] autorelease];
[childMenuItem setTitle:child.title];
[self addItem:childMenuItem];
[childMenuItem setSubmenu:child];
}
- (void) appendRole :(WailsContext*)ctx :(Role)role {
switch(role) {
case AppMenu:
{
NSString *appName = [NSRunningApplication currentApplication].localizedName;
if( appName == nil ) {
appName = [[NSProcessInfo processInfo] processName];
}
WailsMenu *appMenu = [[[WailsMenu new] initWithNSTitle:appName] autorelease];
if (ctx.aboutTitle != nil) {
[appMenu addItem:[self newMenuItemWithContext :ctx :[@"About " stringByAppendingString:appName] :@selector(About) :nil :0]];
[appMenu addItem:[NSMenuItem separatorItem]];
}
[appMenu addItem:[self newMenuItem:[@"Hide " stringByAppendingString:appName] :@selector(hide:) :@"h" :NSEventModifierFlagCommand]];
[appMenu addItem:[self newMenuItem:@"Hide Others" :@selector(hideOtherApplications:) :@"h" :(NSEventModifierFlagOption | NSEventModifierFlagCommand)]];
[appMenu addItem:[self newMenuItem:@"Show All" :@selector(unhideAllApplications:) :@""]];
[appMenu addItem:[NSMenuItem separatorItem]];
id quitTitle = [@"Quit " stringByAppendingString:appName];
NSMenuItem* quitMenuItem = [self newMenuItem:quitTitle :@selector(Quit) :@"q" :NSEventModifierFlagCommand];
quitMenuItem.target = ctx;
[appMenu addItem:quitMenuItem];
[self appendSubmenu:appMenu];
break;
}
case EditMenu:
{
WailsMenu *editMenu = [[[WailsMenu new] initWithNSTitle:@"Edit"] autorelease];
[editMenu addItem:[self newMenuItem:@"Undo" :@selector(undo:) :@"z" :NSEventModifierFlagCommand]];
[editMenu addItem:[self newMenuItem:@"Redo" :@selector(redo:) :@"z" :(NSEventModifierFlagShift | NSEventModifierFlagCommand)]];
[editMenu addItem:[NSMenuItem separatorItem]];
[editMenu addItem:[self newMenuItem:@"Cut" :@selector(cut:) :@"x" :NSEventModifierFlagCommand]];
[editMenu addItem:[self newMenuItem:@"Copy" :@selector(copy:) :@"c" :NSEventModifierFlagCommand]];
[editMenu addItem:[self newMenuItem:@"Paste" :@selector(paste:) :@"v" :NSEventModifierFlagCommand]];
[editMenu addItem:[self newMenuItem:@"Paste and Match Style" :@selector(pasteAsRichText:) :@"v" :(NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand)]];
[editMenu addItem:[self newMenuItem:@"Delete" :@selector(delete:) :[self accel:@"backspace"] :0]];
[editMenu addItem:[self newMenuItem:@"Select All" :@selector(selectAll:) :@"a" :NSEventModifierFlagCommand]];
[editMenu addItem:[NSMenuItem separatorItem]];
// NSMenuItem *speechMenuItem = [[NSMenuItem new] autorelease];
// [speechMenuItem setTitle:@"Speech"];
// [editMenu addItem:speechMenuItem];
WailsMenu *speechMenu = [[[WailsMenu new] initWithNSTitle:@"Speech"] autorelease];
[speechMenu addItem:[self newMenuItem:@"Start Speaking" :@selector(startSpeaking:) :@""]];
[speechMenu addItem:[self newMenuItem:@"Stop Speaking" :@selector(stopSpeaking:) :@""]];
[editMenu appendSubmenu:speechMenu];
[self appendSubmenu:editMenu];
break;
}
case WindowMenu:
{
WailsMenu *windowMenu = [[[WailsMenu new] initWithNSTitle:@"Window"] autorelease];
[windowMenu addItem:[self newMenuItem:@"Minimize" :@selector(performMiniaturize:) :@"m" :NSEventModifierFlagCommand]];
[windowMenu addItem:[self newMenuItem:@"Zoom" :@selector(performZoom:) :@""]];
[windowMenu addItem:[NSMenuItem separatorItem]];
[windowMenu addItem:[self newMenuItem:@"Full Screen" :@selector(enterFullScreenMode:) :@"f" :(NSEventModifierFlagControl | NSEventModifierFlagCommand)]];
[self appendSubmenu:windowMenu];
break;
}
}
}
- (void*) AppendMenuItem :(WailsContext*)ctx :(NSString*)label :(NSString *)shortcutKey :(int)modifiers :(bool)disabled :(bool)checked :(int)menuItemID {
NSString *nslabel = @"";
if (label != nil ) {
nslabel = label;
}
WailsMenuItem *menuItem = [WailsMenuItem new];
// Label
menuItem.title = nslabel;
// Process callback
menuItem.menuItemID = menuItemID;
menuItem.action = @selector(handleClick);
menuItem.target = menuItem;
// Shortcut
if (shortcutKey != nil) {
[menuItem setKeyEquivalent:[self accel:shortcutKey]];
[menuItem setKeyEquivalentModifierMask:modifiers];
}
// Enabled/Disabled
[menuItem setEnabled:!disabled];
// Checked
[menuItem setState:(checked ? NSControlStateValueOn : NSControlStateValueOff)];
[self addItem:menuItem];
return menuItem;
}
- (void) AppendSeparator {
[self addItem:[NSMenuItem separatorItem]];
}
- (NSString*) accel :(NSString*)key {
// Guard against no accelerator key
if( key == NULL ) {
return @"";
}
if( [key isEqualToString:@"backspace"] ) {
return unicode(0x0008);
}
if( [key isEqualToString:@"tab"] ) {
return unicode(0x0009);
}
if( [key isEqualToString:@"return"] ) {
return unicode(0x000d);
}
if( [key isEqualToString:@"enter"] ) {
return unicode(0x000d);
}
if( [key isEqualToString:@"escape"] ) {
return unicode(0x001b);
}
if( [key isEqualToString:@"left"] ) {
return unicode(0x001c);
}
if( [key isEqualToString:@"right"] ) {
return unicode(0x001d);
}
if( [key isEqualToString:@"up"] ) {
return unicode(0x001e);
}
if( [key isEqualToString:@"down"] ) {
return unicode(0x001f);
}
if( [key isEqualToString:@"space"] ) {
return unicode(0x0020);
}
if( [key isEqualToString:@"delete"] ) {
return unicode(0x007f);
}
if( [key isEqualToString:@"home"] ) {
return unicode(0x2196);
}
if( [key isEqualToString:@"end"] ) {
return unicode(0x2198);
}
if( [key isEqualToString:@"page up"] ) {
return unicode(0x21de);
}
if( [key isEqualToString:@"page down"] ) {
return unicode(0x21df);
}
if( [key isEqualToString:@"f1"] ) {
return unicode(0xf704);
}
if( [key isEqualToString:@"f2"] ) {
return unicode(0xf705);
}
if( [key isEqualToString:@"f3"] ) {
return unicode(0xf706);
}
if( [key isEqualToString:@"f4"] ) {
return unicode(0xf707);
}
if( [key isEqualToString:@"f5"] ) {
return unicode(0xf708);
}
if( [key isEqualToString:@"f6"] ) {
return unicode(0xf709);
}
if( [key isEqualToString:@"f7"] ) {
return unicode(0xf70a);
}
if( [key isEqualToString:@"f8"] ) {
return unicode(0xf70b);
}
if( [key isEqualToString:@"f9"] ) {
return unicode(0xf70c);
}
if( [key isEqualToString:@"f10"] ) {
return unicode(0xf70d);
}
if( [key isEqualToString:@"f11"] ) {
return unicode(0xf70e);
}
if( [key isEqualToString:@"f12"] ) {
return unicode(0xf70f);
}
if( [key isEqualToString:@"f13"] ) {
return unicode(0xf710);
}
if( [key isEqualToString:@"f14"] ) {
return unicode(0xf711);
}
if( [key isEqualToString:@"f15"] ) {
return unicode(0xf712);
}
if( [key isEqualToString:@"f16"] ) {
return unicode(0xf713);
}
if( [key isEqualToString:@"f17"] ) {
return unicode(0xf714);
}
if( [key isEqualToString:@"f18"] ) {
return unicode(0xf715);
}
if( [key isEqualToString:@"f19"] ) {
return unicode(0xf716);
}
if( [key isEqualToString:@"f20"] ) {
return unicode(0xf717);
}
if( [key isEqualToString:@"f21"] ) {
return unicode(0xf718);
}
if( [key isEqualToString:@"f22"] ) {
return unicode(0xf719);
}
if( [key isEqualToString:@"f23"] ) {
return unicode(0xf71a);
}
if( [key isEqualToString:@"f24"] ) {
return unicode(0xf71b);
}
if( [key isEqualToString:@"f25"] ) {
return unicode(0xf71c);
}
if( [key isEqualToString:@"f26"] ) {
return unicode(0xf71d);
}
if( [key isEqualToString:@"f27"] ) {
return unicode(0xf71e);
}
if( [key isEqualToString:@"f28"] ) {
return unicode(0xf71f);
}
if( [key isEqualToString:@"f29"] ) {
return unicode(0xf720);
}
if( [key isEqualToString:@"f30"] ) {
return unicode(0xf721);
}
if( [key isEqualToString:@"f31"] ) {
return unicode(0xf722);
}
if( [key isEqualToString:@"f32"] ) {
return unicode(0xf723);
}
if( [key isEqualToString:@"f33"] ) {
return unicode(0xf724);
}
if( [key isEqualToString:@"f34"] ) {
return unicode(0xf725);
}
if( [key isEqualToString:@"f35"] ) {
return unicode(0xf726);
}
// if( [key isEqualToString:@"Insert"] ) {
// return unicode(0xf727);
// }
// if( [key isEqualToString:@"PrintScreen"] ) {
// return unicode(0xf72e);
// }
// if( [key isEqualToString:@"ScrollLock"] ) {
// return unicode(0xf72f);
// }
if( [key isEqualToString:@"numLock"] ) {
return unicode(0xf739);
}
return key;
}
@end

View File

@@ -0,0 +1,22 @@
//
// WailsMenuItem.h
// test
//
// Created by Lea Anthony on 27/10/21.
//
#ifndef WailsMenuItem_h
#define WailsMenuItem_h
#import <Cocoa/Cocoa.h>
@interface WailsMenuItem : NSMenuItem
@property int menuItemID;
- (void) handleClick;
@end
#endif /* WailsMenuItem_h */

View File

@@ -0,0 +1,21 @@
//go:build darwin
//
// WailsMenuItem.m
// test
//
// Created by Lea Anthony on 27/10/21.
//
#import <Foundation/Foundation.h>
#import "WailsMenuItem.h"
#include "message.h"
@implementation WailsMenuItem
- (void) handleClick {
processCallback(self.menuItemID);
}
@end

View File

@@ -0,0 +1,14 @@
#ifndef WailsWebView_h
#define WailsWebView_h
#import <Cocoa/Cocoa.h>
#import <WebKit/WebKit.h>
// We will override WKWebView, so we can detect file drop in obj-c
// and grab their file path, to then inject into JS
@interface WailsWebView : WKWebView
@property bool disableWebViewDragAndDrop;
@property bool enableDragAndDrop;
@end
#endif /* WailsWebView_h */

View File

@@ -0,0 +1,122 @@
#import "WailsWebView.h"
#import "message.h"
@implementation WailsWebView
@synthesize disableWebViewDragAndDrop;
@synthesize enableDragAndDrop;
- (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)sender
{
if ( !enableDragAndDrop ) {
return [super prepareForDragOperation: sender];
}
if ( disableWebViewDragAndDrop ) {
return YES;
}
return [super prepareForDragOperation: sender];
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
if ( !enableDragAndDrop ) {
return [super performDragOperation: sender];
}
NSPasteboard *pboard = [sender draggingPasteboard];
// if no types, then we'll just let the WKWebView handle the drag-n-drop as normal
NSArray<NSPasteboardType> * types = [pboard types];
if( !types )
return [super performDragOperation: sender];
// getting all NSURL types
NSArray<Class> *url_class = @[[NSURL class]];
NSDictionary *options = @{};
NSArray<NSURL*> *files = [pboard readObjectsForClasses:url_class options:options];
// collecting all file paths
NSMutableArray *files_strs = [[NSMutableArray alloc] init];
for (NSURL *url in files)
{
const char *fs_path = [url fileSystemRepresentation]; //Will be UTF-8 encoded
NSString *fs_path_str = [[NSString alloc] initWithCString:fs_path encoding:NSUTF8StringEncoding];
[files_strs addObject:fs_path_str];
// NSLog( @"performDragOperation: file path: %s", fs_path );
}
NSString *joined=[files_strs componentsJoinedByString:@"\n"];
// Release the array of file paths
[files_strs release];
int dragXLocation = [sender draggingLocation].x - [self frame].origin.x;
int dragYLocation = [self frame].size.height - [sender draggingLocation].y; // Y coordinate is inverted, so we need to subtract from the height
// NSLog( @"draggingUpdated: X coord: %d", dragXLocation );
// NSLog( @"draggingUpdated: Y coord: %d", dragYLocation );
NSString *message = [NSString stringWithFormat:@"DD:%d:%d:%@", dragXLocation, dragYLocation, joined];
const char* res = message.UTF8String;
processMessage(res);
if ( disableWebViewDragAndDrop ) {
return YES;
}
return [super performDragOperation: sender];
}
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender {
if ( !enableDragAndDrop ) {
return [super draggingUpdated: sender];
}
NSPasteboard *pboard = [sender draggingPasteboard];
// if no types, then we'll just let the WKWebView handle the drag-n-drop as normal
NSArray<NSPasteboardType> * types = [pboard types];
if( !types ) {
return [super draggingUpdated: sender];
}
if ( disableWebViewDragAndDrop ) {
// we should call supper as otherwise events will not pass
[super draggingUpdated: sender];
// pass NSDragOperationGeneric = 4 to show regular hover for drag and drop. As we want to ignore webkit behaviours that depends on webpage
return 4;
}
return [super draggingUpdated: sender];
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
if ( !enableDragAndDrop ) {
return [super draggingEntered: sender];
}
NSPasteboard *pboard = [sender draggingPasteboard];
// if no types, then we'll just let the WKWebView handle the drag-n-drop as normal
NSArray<NSPasteboardType> * types = [pboard types];
if( !types ) {
return [super draggingEntered: sender];
}
if ( disableWebViewDragAndDrop ) {
// we should call supper as otherwise events will not pass
[super draggingEntered: sender];
// pass NSDragOperationGeneric = 4 to show regular hover for drag and drop. As we want to ignore webkit behaviours that depends on webpage
return 4;
}
return [super draggingEntered: sender];
}
@end

View File

@@ -0,0 +1,25 @@
//
// WindowDelegate.h
// test
//
// Created by Lea Anthony on 10/10/21.
//
#ifndef WindowDelegate_h
#define WindowDelegate_h
#import "WailsContext.h"
@interface WindowDelegate : NSObject <NSWindowDelegate>
@property bool hideOnClose;
@property (assign) WailsContext* ctx;
- (void)windowDidExitFullScreen:(NSNotification *)notification;
@end
#endif /* WindowDelegate_h */

View File

@@ -0,0 +1,38 @@
//go:build darwin
//
// WindowDelegate.m
// test
//
// Created by Lea Anthony on 10/10/21.
//
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#import "WindowDelegate.h"
#import "message.h"
#import "WailsContext.h"
@implementation WindowDelegate
- (BOOL)windowShouldClose:(WailsWindow *)sender {
if( self.hideOnClose ) {
[NSApp hide:nil];
return false;
}
processMessage("Q");
return false;
}
- (void)windowDidExitFullScreen:(NSNotification *)notification {
[self.ctx.mainWindow applyWindowConstraints];
}
- (void)windowWillEnterFullScreen:(NSNotification *)notification {
[self.ctx.mainWindow disableWindowConstraints];
}
- (NSApplicationPresentationOptions)window:(WailsWindow *)window willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions {
return NSApplicationPresentationAutoHideToolbar | NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationFullScreen;
}
@end

View File

@@ -0,0 +1,24 @@
//go:build darwin
// +build darwin
package darwin
import (
"fmt"
"github.com/pkg/browser"
"github.com/wailsapp/wails/v2/internal/frontend/utils"
)
// BrowserOpenURL Use the default browser to open the url
func (f *Frontend) BrowserOpenURL(rawURL string) {
url, err := utils.ValidateAndSanitizeURL(rawURL)
if err != nil {
f.logger.Error(fmt.Sprintf("Invalid URL %s", err.Error()))
return
}
// Specific method implementation
if err := browser.OpenURL(url); err != nil {
f.logger.Error("Unable to open default system browser")
}
}

View File

@@ -0,0 +1,51 @@
//go:build darwin
// +build darwin
package darwin
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import <Foundation/Foundation.h>
#import "Application.h"
#include <stdlib.h>
*/
import "C"
import (
"errors"
"strconv"
"github.com/wailsapp/wails/v2/pkg/menu"
)
func (f *Frontend) handleCallback(menuItemID uint) error {
menuItem := getMenuItemForID(menuItemID)
if menuItem == nil {
return errors.New("unknown menuItem ID: " + strconv.Itoa(int(menuItemID)))
}
wailsMenuItem := menuItem.wailsMenuItem
if wailsMenuItem.Type == menu.CheckboxType {
wailsMenuItem.Checked = !wailsMenuItem.Checked
C.UpdateMenuItem(menuItem.nsmenuitem, bool2Cint(wailsMenuItem.Checked))
}
if wailsMenuItem.Type == menu.RadioType {
// Ignore if we clicked the item that is already checked
if !wailsMenuItem.Checked {
for _, item := range menuItem.radioGroupMembers {
if item.wailsMenuItem.Checked {
item.wailsMenuItem.Checked = false
C.UpdateMenuItem(item.nsmenuitem, C.int(0))
}
}
wailsMenuItem.Checked = true
C.UpdateMenuItem(menuItem.nsmenuitem, C.int(1))
}
}
if wailsMenuItem.Click != nil {
go wailsMenuItem.Click(&menu.CallbackData{MenuItem: wailsMenuItem})
}
return nil
}

View File

@@ -0,0 +1,34 @@
//go:build darwin
package darwin
/*
#include <stdlib.h>
*/
import "C"
import "unsafe"
// Calloc handles alloc/dealloc of C data
type Calloc struct {
pool []unsafe.Pointer
}
// NewCalloc creates a new allocator
func NewCalloc() Calloc {
return Calloc{}
}
// String creates a new C string and retains a reference to it
func (c Calloc) String(in string) *C.char {
result := C.CString(in)
c.pool = append(c.pool, unsafe.Pointer(result))
return result
}
// Free frees all allocated C memory
func (c Calloc) Free() {
for _, str := range c.pool {
C.free(str)
}
c.pool = []unsafe.Pointer{}
}

View File

@@ -0,0 +1,50 @@
//go:build darwin
package darwin
import (
"os"
"os/exec"
)
// ensureUTF8Env returns the current environment with LANG set to en_US.UTF-8
// if it is not already set. This is needed because packaged macOS apps do not
// inherit the terminal's LANG variable, causing pbpaste/pbcopy to default to
// an ASCII-compatible encoding that mangles non-ASCII text.
func ensureUTF8Env() []string {
env := os.Environ()
if _, ok := os.LookupEnv("LANG"); !ok {
env = append(env, "LANG=en_US.UTF-8")
}
return env
}
func (f *Frontend) ClipboardGetText() (string, error) {
pasteCmd := exec.Command("pbpaste")
pasteCmd.Env = ensureUTF8Env()
out, err := pasteCmd.Output()
if err != nil {
return "", err
}
return string(out), nil
}
func (f *Frontend) ClipboardSetText(text string) error {
copyCmd := exec.Command("pbcopy")
copyCmd.Env = ensureUTF8Env()
in, err := copyCmd.StdinPipe()
if err != nil {
return err
}
if err := copyCmd.Start(); err != nil {
return err
}
if _, err := in.Write([]byte(text)); err != nil {
return err
}
if err := in.Close(); err != nil {
return err
}
return copyCmd.Wait()
}

View File

@@ -0,0 +1,196 @@
//go:build darwin
// +build darwin
package darwin
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import <Foundation/Foundation.h>
#import "Application.h"
#import "WailsContext.h"
*/
import "C"
import (
"encoding/json"
"fmt"
"strings"
"sync"
"unsafe"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/frontend"
)
// Obj-C dialog methods send the response to this channel
var (
messageDialogResponse = make(chan int)
openFileDialogResponse = make(chan string)
saveFileDialogResponse = make(chan string)
dialogLock sync.Mutex
)
// OpenDirectoryDialog prompts the user to select a directory
func (f *Frontend) OpenDirectoryDialog(options frontend.OpenDialogOptions) (string, error) {
results, err := f.openDialog(&options, false, false, true)
if err != nil {
return "", err
}
var selected string
if len(results) > 0 {
selected = results[0]
}
return selected, nil
}
func (f *Frontend) openDialog(options *frontend.OpenDialogOptions, multiple bool, allowfiles bool, allowdirectories bool) ([]string, error) {
dialogLock.Lock()
defer dialogLock.Unlock()
c := NewCalloc()
defer c.Free()
title := c.String(options.Title)
defaultFilename := c.String(options.DefaultFilename)
defaultDirectory := c.String(options.DefaultDirectory)
allowDirectories := bool2Cint(allowdirectories)
allowFiles := bool2Cint(allowfiles)
canCreateDirectories := bool2Cint(options.CanCreateDirectories)
treatPackagesAsDirectories := bool2Cint(options.TreatPackagesAsDirectories)
resolveAliases := bool2Cint(options.ResolvesAliases)
showHiddenFiles := bool2Cint(options.ShowHiddenFiles)
allowMultipleFileSelection := bool2Cint(multiple)
var filterStrings slicer.StringSlicer
if options.Filters != nil {
for _, filter := range options.Filters {
thesePatterns := strings.Split(filter.Pattern, ";")
for _, pattern := range thesePatterns {
pattern = strings.TrimSpace(pattern)
if pattern != "" {
filterStrings.Add(pattern)
}
}
}
filterStrings.Deduplicate()
}
filters := filterStrings.Join(";")
C.OpenFileDialog(f.mainWindow.context, title, defaultFilename, defaultDirectory, allowDirectories, allowFiles, canCreateDirectories, treatPackagesAsDirectories, resolveAliases, showHiddenFiles, allowMultipleFileSelection, c.String(filters))
result := <-openFileDialogResponse
var parsedResults []string
err := json.Unmarshal([]byte(result), &parsedResults)
return parsedResults, err
}
// OpenFileDialog prompts the user to select a file
func (f *Frontend) OpenFileDialog(options frontend.OpenDialogOptions) (string, error) {
results, err := f.openDialog(&options, false, true, false)
if err != nil {
return "", err
}
var selected string
if len(results) > 0 {
selected = results[0]
}
return selected, nil
}
// OpenMultipleFilesDialog prompts the user to select a file
func (f *Frontend) OpenMultipleFilesDialog(options frontend.OpenDialogOptions) ([]string, error) {
return f.openDialog(&options, true, true, false)
}
// SaveFileDialog prompts the user to select a file
func (f *Frontend) SaveFileDialog(options frontend.SaveDialogOptions) (string, error) {
dialogLock.Lock()
defer dialogLock.Unlock()
c := NewCalloc()
defer c.Free()
title := c.String(options.Title)
defaultFilename := c.String(options.DefaultFilename)
defaultDirectory := c.String(options.DefaultDirectory)
canCreateDirectories := bool2Cint(options.CanCreateDirectories)
treatPackagesAsDirectories := bool2Cint(options.TreatPackagesAsDirectories)
showHiddenFiles := bool2Cint(options.ShowHiddenFiles)
var filterStrings slicer.StringSlicer
if options.Filters != nil {
for _, filter := range options.Filters {
thesePatterns := strings.Split(filter.Pattern, ";")
for _, pattern := range thesePatterns {
pattern = strings.TrimSpace(pattern)
if pattern != "" {
filterStrings.Add(pattern)
}
}
}
filterStrings.Deduplicate()
}
filters := filterStrings.Join(";")
C.SaveFileDialog(f.mainWindow.context, title, defaultFilename, defaultDirectory, canCreateDirectories, treatPackagesAsDirectories, showHiddenFiles, c.String(filters))
result := <-saveFileDialogResponse
return result, nil
}
// MessageDialog show a message dialog to the user
func (f *Frontend) MessageDialog(options frontend.MessageDialogOptions) (string, error) {
dialogLock.Lock()
defer dialogLock.Unlock()
c := NewCalloc()
defer c.Free()
dialogType := c.String(string(options.Type))
title := c.String(options.Title)
message := c.String(options.Message)
defaultButton := c.String(options.DefaultButton)
cancelButton := c.String(options.CancelButton)
const MaxButtons = 4
var buttons [MaxButtons]*C.char
for index, buttonText := range options.Buttons {
if index == MaxButtons {
return "", fmt.Errorf("max %d buttons supported (%d given)", MaxButtons, len(options.Buttons))
}
buttons[index] = c.String(buttonText)
}
var iconData unsafe.Pointer
var iconDataLength C.int
if options.Icon != nil {
iconData = unsafe.Pointer(&options.Icon[0])
iconDataLength = C.int(len(options.Icon))
}
C.MessageDialog(f.mainWindow.context, dialogType, title, message, buttons[0], buttons[1], buttons[2], buttons[3], defaultButton, cancelButton, iconData, iconDataLength)
result := <-messageDialogResponse
selectedC := buttons[result]
var selected string
if selectedC != nil {
selected = options.Buttons[result]
}
return selected, nil
}
//export processMessageDialogResponse
func processMessageDialogResponse(selection int) {
messageDialogResponse <- selection
}
//export processOpenFileDialogResponse
func processOpenFileDialogResponse(cselection *C.char) {
selection := C.GoString(cselection)
openFileDialogResponse <- selection
}
//export processSaveFileDialogResponse
func processSaveFileDialogResponse(cselection *C.char) {
selection := C.GoString(cselection)
saveFileDialogResponse <- selection
}

View File

@@ -0,0 +1,525 @@
//go:build darwin
// +build darwin
package darwin
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import <Foundation/Foundation.h>
#import "Application.h"
#import "CustomProtocol.h"
#import "WailsContext.h"
#include <stdlib.h>
*/
import "C"
import (
"context"
"encoding/json"
"fmt"
"html/template"
"log"
"net"
"net/url"
"os"
"unsafe"
"github.com/wailsapp/wails/v2/pkg/assetserver"
"github.com/wailsapp/wails/v2/pkg/assetserver/webview"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/frontend/originvalidator"
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/pkg/options"
)
const startURL = "wails://wails/"
type bindingsMessage struct {
message string
source string
isMainFrame bool
}
var (
messageBuffer = make(chan string, 100)
bindingsMessageBuffer = make(chan *bindingsMessage, 100)
requestBuffer = make(chan webview.Request, 100)
callbackBuffer = make(chan uint, 10)
openFilepathBuffer = make(chan string, 100)
openUrlBuffer = make(chan string, 100)
secondInstanceBuffer = make(chan options.SecondInstanceData, 1)
)
type Frontend struct {
// Context
ctx context.Context
frontendOptions *options.App
logger *logger.Logger
debug bool
devtoolsEnabled bool
// Keep single instance lock file, so that it will not be GC and lock will exist while app is running
singleInstanceLockFile *os.File
// Assets
assets *assetserver.AssetServer
startURL *url.URL
// main window handle
mainWindow *Window
bindings *binding.Bindings
dispatcher frontend.Dispatcher
originValidator *originvalidator.OriginValidator
}
func (f *Frontend) RunMainLoop() {
C.RunMainLoop()
}
func (f *Frontend) WindowClose() {
C.ReleaseContext(f.mainWindow.context)
}
func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend {
result := &Frontend{
frontendOptions: appoptions,
logger: myLogger,
bindings: appBindings,
dispatcher: dispatcher,
ctx: ctx,
}
result.startURL, _ = url.Parse(startURL)
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
// this should be initialized as early as possible to handle first instance launch
C.StartCustomProtocolHandler()
if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil {
result.startURL = _starturl
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
} else {
if port, _ := ctx.Value("assetserverport").(string); port != "" {
result.startURL.Host = net.JoinHostPort(result.startURL.Host+".localhost", port)
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
}
var bindings string
var err error
if _obfuscated, _ := ctx.Value("obfuscated").(bool); !_obfuscated {
bindings, err = appBindings.ToJSON()
if err != nil {
log.Fatal(err)
}
} else {
appBindings.DB().UpdateObfuscatedCallMap()
}
assets, err := assetserver.NewAssetServerMainPage(bindings, appoptions, ctx.Value("assetdir") != nil, myLogger, runtime.RuntimeAssetsBundle)
if err != nil {
log.Fatal(err)
}
assets.ExpectedWebViewHost = result.startURL.Host
result.assets = assets
go result.startRequestProcessor()
}
go result.startMessageProcessor()
go result.startBindingsMessageProcessor()
go result.startCallbackProcessor()
go result.startFileOpenProcessor()
go result.startUrlOpenProcessor()
go result.startSecondInstanceProcessor()
return result
}
func (f *Frontend) startFileOpenProcessor() {
for filePath := range openFilepathBuffer {
f.ProcessOpenFileEvent(filePath)
}
}
func (f *Frontend) startUrlOpenProcessor() {
for url := range openUrlBuffer {
f.ProcessOpenUrlEvent(url)
}
}
func (f *Frontend) startSecondInstanceProcessor() {
for secondInstanceData := range secondInstanceBuffer {
if f.frontendOptions.SingleInstanceLock != nil &&
f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil {
f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData)
}
}
}
func (f *Frontend) startMessageProcessor() {
for message := range messageBuffer {
f.processMessage(message)
}
}
func (f *Frontend) startBindingsMessageProcessor() {
for msg := range bindingsMessageBuffer {
// Apple webkit doesn't provide origin of main frame. So we can't verify in case of iFrame that top level origin is allowed.
if !msg.isMainFrame {
f.logger.Error("Blocked request from not main frame")
continue
}
origin, err := f.originValidator.GetOriginFromURL(msg.source)
if err != nil {
f.logger.Error(fmt.Sprintf("failed to get origin for URL %q: %v", msg.source, err))
continue
}
allowed := f.originValidator.IsOriginAllowed(origin)
if !allowed {
f.logger.Error("Blocked request from unauthorized origin: %s", origin)
continue
}
f.processMessage(msg.message)
}
}
func (f *Frontend) startRequestProcessor() {
for request := range requestBuffer {
f.assets.ServeWebViewRequest(request)
}
}
func (f *Frontend) startCallbackProcessor() {
for callback := range callbackBuffer {
err := f.handleCallback(callback)
if err != nil {
println(err.Error())
}
}
}
func (f *Frontend) WindowReload() {
f.ExecJS("runtime.WindowReload();")
}
func (f *Frontend) WindowReloadApp() {
f.ExecJS(fmt.Sprintf("window.location.href = '%s';", f.startURL))
}
func (f *Frontend) WindowSetSystemDefaultTheme() {
}
func (f *Frontend) WindowSetLightTheme() {
}
func (f *Frontend) WindowSetDarkTheme() {
}
func (f *Frontend) Run(ctx context.Context) error {
f.ctx = ctx
if f.frontendOptions.SingleInstanceLock != nil {
f.singleInstanceLockFile = SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId)
}
_debug := ctx.Value("debug")
_devtoolsEnabled := ctx.Value("devtoolsEnabled")
if _debug != nil {
f.debug = _debug.(bool)
}
if _devtoolsEnabled != nil {
f.devtoolsEnabled = _devtoolsEnabled.(bool)
}
mainWindow := NewWindow(f.frontendOptions, f.debug, f.devtoolsEnabled)
f.mainWindow = mainWindow
f.mainWindow.Center()
go func() {
if f.frontendOptions.OnStartup != nil {
f.frontendOptions.OnStartup(f.ctx)
}
}()
mainWindow.Run(f.startURL.String())
return nil
}
func (f *Frontend) WindowCenter() {
f.mainWindow.Center()
}
func (f *Frontend) WindowSetAlwaysOnTop(onTop bool) {
f.mainWindow.SetAlwaysOnTop(onTop)
}
func (f *Frontend) WindowSetPosition(x, y int) {
f.mainWindow.SetPosition(x, y)
}
func (f *Frontend) WindowGetPosition() (int, int) {
return f.mainWindow.GetPosition()
}
func (f *Frontend) WindowSetSize(width, height int) {
f.mainWindow.SetSize(width, height)
}
func (f *Frontend) WindowGetSize() (int, int) {
return f.mainWindow.Size()
}
func (f *Frontend) WindowSetTitle(title string) {
f.mainWindow.SetTitle(title)
}
func (f *Frontend) WindowFullscreen() {
f.mainWindow.Fullscreen()
}
func (f *Frontend) WindowUnfullscreen() {
f.mainWindow.UnFullscreen()
}
func (f *Frontend) WindowShow() {
f.mainWindow.Show()
}
func (f *Frontend) WindowHide() {
f.mainWindow.Hide()
}
func (f *Frontend) Show() {
f.mainWindow.ShowApplication()
}
func (f *Frontend) Hide() {
f.mainWindow.HideApplication()
}
func (f *Frontend) WindowMaximise() {
f.mainWindow.Maximise()
}
func (f *Frontend) WindowToggleMaximise() {
f.mainWindow.ToggleMaximise()
}
func (f *Frontend) WindowUnmaximise() {
f.mainWindow.UnMaximise()
}
func (f *Frontend) WindowMinimise() {
f.mainWindow.Minimise()
}
func (f *Frontend) WindowUnminimise() {
f.mainWindow.UnMinimise()
}
func (f *Frontend) WindowSetMinSize(width int, height int) {
f.mainWindow.SetMinSize(width, height)
}
func (f *Frontend) WindowSetMaxSize(width int, height int) {
f.mainWindow.SetMaxSize(width, height)
}
func (f *Frontend) WindowSetBackgroundColour(col *options.RGBA) {
if col == nil {
return
}
f.mainWindow.SetBackgroundColour(col.R, col.G, col.B, col.A)
}
func (f *Frontend) ScreenGetAll() ([]frontend.Screen, error) {
return GetAllScreens(f.mainWindow.context)
}
func (f *Frontend) WindowIsMaximised() bool {
return f.mainWindow.IsMaximised()
}
func (f *Frontend) WindowIsMinimised() bool {
return f.mainWindow.IsMinimised()
}
func (f *Frontend) WindowIsNormal() bool {
return f.mainWindow.IsNormal()
}
func (f *Frontend) WindowIsFullscreen() bool {
return f.mainWindow.IsFullScreen()
}
func (f *Frontend) Quit() {
if f.frontendOptions.OnBeforeClose != nil {
go func() {
if !f.frontendOptions.OnBeforeClose(f.ctx) {
f.mainWindow.Quit()
}
}()
return
}
f.mainWindow.Quit()
}
func (f *Frontend) WindowPrint() {
f.mainWindow.Print()
}
type EventNotify struct {
Name string `json:"name"`
Data []interface{} `json:"data"`
}
func (f *Frontend) Notify(name string, data ...interface{}) {
notification := EventNotify{
Name: name,
Data: data,
}
payload, err := json.Marshal(notification)
if err != nil {
f.logger.Error(err.Error())
return
}
f.ExecJS(`window.wails.EventsNotify('` + template.JSEscapeString(string(payload)) + `');`)
}
func (f *Frontend) processMessage(message string) {
if message == "DomReady" {
if f.frontendOptions.OnDomReady != nil {
f.frontendOptions.OnDomReady(f.ctx)
}
return
}
if message == "runtime:ready" {
cmd := fmt.Sprintf("window.wails.setCSSDragProperties('%s', '%s');", f.frontendOptions.CSSDragProperty, f.frontendOptions.CSSDragValue)
f.ExecJS(cmd)
if f.frontendOptions.DragAndDrop != nil && f.frontendOptions.DragAndDrop.EnableFileDrop {
f.ExecJS("window.wails.flags.enableWailsDragAndDrop = true;")
}
return
}
if message == "wails:openInspector" {
showInspector(f.mainWindow.context)
return
}
//if strings.HasPrefix(message, "systemevent:") {
// f.processSystemEvent(message)
// return
//}
go func() {
result, err := f.dispatcher.ProcessMessage(message, f)
if err != nil {
f.logger.Error(err.Error())
f.Callback(result)
return
}
if result == "" {
return
}
switch result[0] {
case 'c':
// Callback from a method call
f.Callback(result[1:])
default:
f.logger.Info("Unknown message returned from dispatcher: %+v", result)
}
}()
}
func (f *Frontend) ProcessOpenFileEvent(filePath string) {
if f.frontendOptions.Mac != nil && f.frontendOptions.Mac.OnFileOpen != nil {
f.frontendOptions.Mac.OnFileOpen(filePath)
}
}
func (f *Frontend) ProcessOpenUrlEvent(url string) {
if f.frontendOptions.Mac != nil && f.frontendOptions.Mac.OnUrlOpen != nil {
f.frontendOptions.Mac.OnUrlOpen(url)
}
}
func (f *Frontend) Callback(message string) {
escaped, err := json.Marshal(message)
if err != nil {
panic(err)
}
f.ExecJS(`window.wails.Callback(` + string(escaped) + `);`)
}
func (f *Frontend) ExecJS(js string) {
f.mainWindow.ExecJS(js)
}
//func (f *Frontend) processSystemEvent(message string) {
// sl := strings.Split(message, ":")
// if len(sl) != 2 {
// f.logger.Error("Invalid system message: %s", message)
// return
// }
// switch sl[1] {
// case "fullscreen":
// f.mainWindow.DisableSizeConstraints()
// case "unfullscreen":
// f.mainWindow.EnableSizeConstraints()
// default:
// f.logger.Error("Unknown system message: %s", message)
// }
//}
//export processMessage
func processMessage(message *C.char) {
goMessage := C.GoString(message)
messageBuffer <- goMessage
}
//export processBindingMessage
func processBindingMessage(message *C.char, source *C.char, fromMainFrame bool) {
goMessage := C.GoString(message)
goSource := C.GoString(source)
bindingsMessageBuffer <- &bindingsMessage{
message: goMessage,
source: goSource,
isMainFrame: fromMainFrame,
}
}
//export processCallback
func processCallback(callbackID uint) {
callbackBuffer <- callbackID
}
//export processURLRequest
func processURLRequest(_ unsafe.Pointer, wkURLSchemeTask unsafe.Pointer) {
requestBuffer <- webview.NewRequest(wkURLSchemeTask)
}
//export HandleOpenFile
func HandleOpenFile(filePath *C.char) {
goFilepath := C.GoString(filePath)
openFilepathBuffer <- goFilepath
}
//export HandleOpenURL
func HandleOpenURL(url *C.char) {
goUrl := C.GoString(url)
openUrlBuffer <- goUrl
}

View File

@@ -0,0 +1,10 @@
//go:build darwin && !(dev || debug || devtools)
package darwin
import (
"unsafe"
)
func showInspector(_ unsafe.Pointer) {
}

View File

@@ -0,0 +1,78 @@
//go:build darwin && (dev || debug || devtools)
package darwin
// We are using private APIs here, make sure this is only included in a dev/debug build and not in a production build.
// Otherwise the binary might get rejected by the AppReview-Team when pushing it to the AppStore.
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import <Foundation/Foundation.h>
#import "WailsContext.h"
extern void processMessage(const char *message);
@interface _WKInspector : NSObject
- (void)show;
- (void)detach;
@end
@interface WKWebView ()
- (_WKInspector *)_inspector;
@end
void showInspector(void *inctx) {
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 120000
ON_MAIN_THREAD(
if (@available(macOS 12.0, *)) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
@try {
[ctx.webview._inspector show];
} @catch (NSException *exception) {
NSLog(@"Opening the inspector failed: %@", exception.reason);
return;
}
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// Detach must be deferred a little bit and is ignored directly after a show.
@try {
[ctx.webview._inspector detach];
} @catch (NSException *exception) {
NSLog(@"Detaching the inspector failed: %@", exception.reason);
}
});
} else {
NSLog(@"Opening the inspector needs at least MacOS 12");
}
);
#endif
}
void setupF12hotkey() {
[NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) {
if (event.keyCode == 111 &&
event.modifierFlags & NSEventModifierFlagFunction &&
event.modifierFlags & NSEventModifierFlagCommand &&
event.modifierFlags & NSEventModifierFlagShift) {
processMessage("wails:openInspector");
return nil;
}
return event;
}];
}
*/
import "C"
import (
"unsafe"
)
func init() {
C.setupF12hotkey()
}
func showInspector(context unsafe.Pointer) {
C.showInspector(context)
}

View File

@@ -0,0 +1,243 @@
//go:build ignore
// main.m
// test
//
// Created by Lea Anthony on 10/10/21.
//
// ****** This file is used for testing purposes only ******
#import <Foundation/Foundation.h>
#import "Application.h"
void processMessage(const char*t) {
NSLog(@"processMessage called");
}
void processMessageDialogResponse(int t) {
NSLog(@"processMessage called");
}
void processOpenFileDialogResponse(const char *t) {
NSLog(@"processMessage called %s", t);
}
void processSaveFileDialogResponse(const char *t) {
NSLog(@"processMessage called %s", t);
}
void processCallback(int callbackID) {
NSLog(@"Process callback %d", callbackID);
}
void processURLRequest(void *ctx, unsigned long long requestId, const char* url, const char *method, const char *headers, const void *body, int bodyLen) {
NSLog(@"processURLRequest called");
const char myByteArray[] = { 0x3c,0x68,0x31,0x3e,0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x21,0x3c,0x2f,0x68,0x31,0x3e };
// void *inctx, const char *url, int statusCode, const char *headers, void* data, int datalength
ProcessURLResponse(ctx, requestId, 200, "{\"Content-Type\": \"text/html\"}", (void*)myByteArray, 21);
}
unsigned char _Users_username_Pictures_SaltBae_png[] = {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x14,
0x08, 0x06, 0x00, 0x00, 0x00, 0x8d, 0x89, 0x1d, 0x0d, 0x00, 0x00, 0x00,
0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61,
0x05, 0x00, 0x00, 0x00, 0x20, 0x63, 0x48, 0x52, 0x4d, 0x00, 0x00, 0x7a,
0x26, 0x00, 0x00, 0x80, 0x84, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0x80,
0xe8, 0x00, 0x00, 0x75, 0x30, 0x00, 0x00, 0xea, 0x60, 0x00, 0x00, 0x3a,
0x98, 0x00, 0x00, 0x17, 0x70, 0x9c, 0xba, 0x51, 0x3c, 0x00, 0x00, 0x00,
0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x13, 0x00, 0x00, 0x0b,
0x13, 0x01, 0x00, 0x9a, 0x9c, 0x18, 0x00, 0x00, 0x01, 0xd5, 0x69, 0x54,
0x58, 0x74, 0x58, 0x4d, 0x4c, 0x3a, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x64,
0x6f, 0x62, 0x65, 0x2e, 0x78, 0x6d, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00,
0x3c, 0x78, 0x3a, 0x78, 0x6d, 0x70, 0x6d, 0x65, 0x74, 0x61, 0x20, 0x78,
0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x78, 0x3d, 0x22, 0x61, 0x64, 0x6f, 0x62,
0x65, 0x3a, 0x6e, 0x73, 0x3a, 0x6d, 0x65, 0x74, 0x61, 0x2f, 0x22, 0x20,
0x78, 0x3a, 0x78, 0x6d, 0x70, 0x74, 0x6b, 0x3d, 0x22, 0x58, 0x4d, 0x50,
0x20, 0x43, 0x6f, 0x72, 0x65, 0x20, 0x35, 0x2e, 0x34, 0x2e, 0x30, 0x22,
0x3e, 0x0a, 0x20, 0x20, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x52, 0x44,
0x46, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x72, 0x64, 0x66, 0x3d,
0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e,
0x77, 0x33, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x31, 0x39, 0x39, 0x39, 0x2f,
0x30, 0x32, 0x2f, 0x32, 0x32, 0x2d, 0x72, 0x64, 0x66, 0x2d, 0x73, 0x79,
0x6e, 0x74, 0x61, 0x78, 0x2d, 0x6e, 0x73, 0x23, 0x22, 0x3e, 0x0a, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44, 0x65,
0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x72, 0x64,
0x66, 0x3a, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x22, 0x22, 0x0a, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x78,
0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x74, 0x69, 0x66, 0x66, 0x3d, 0x22, 0x68,
0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f,
0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x69, 0x66, 0x66, 0x2f,
0x31, 0x2e, 0x30, 0x2f, 0x22, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x3c, 0x74, 0x69, 0x66, 0x66, 0x3a, 0x43, 0x6f,
0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x3e, 0x31, 0x3c,
0x2f, 0x74, 0x69, 0x66, 0x66, 0x3a, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65,
0x73, 0x73, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x3c, 0x74, 0x69, 0x66, 0x66, 0x3a, 0x4f, 0x72,
0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x31, 0x3c,
0x2f, 0x74, 0x69, 0x66, 0x66, 0x3a, 0x4f, 0x72, 0x69, 0x65, 0x6e, 0x74,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x3c, 0x74, 0x69, 0x66, 0x66, 0x3a, 0x50, 0x68,
0x6f, 0x74, 0x6f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x49, 0x6e, 0x74,
0x65, 0x72, 0x70, 0x72, 0x65, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3e,
0x32, 0x3c, 0x2f, 0x74, 0x69, 0x66, 0x66, 0x3a, 0x50, 0x68, 0x6f, 0x74,
0x6f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x49, 0x6e, 0x74, 0x65, 0x72,
0x70, 0x72, 0x65, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x44,
0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x3e, 0x0a,
0x20, 0x20, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x52, 0x44, 0x46,
0x3e, 0x0a, 0x3c, 0x2f, 0x78, 0x3a, 0x78, 0x6d, 0x70, 0x6d, 0x65, 0x74,
0x61, 0x3e, 0x0a, 0x02, 0xd8, 0x80, 0x05, 0x00, 0x00, 0x04, 0xdc, 0x49,
0x44, 0x41, 0x54, 0x38, 0x11, 0x1d, 0x94, 0x49, 0x6c, 0x1b, 0x65, 0x18,
0x86, 0x9f, 0x99, 0xf9, 0x67, 0xc6, 0x6b, 0xbc, 0x26, 0xce, 0xda, 0xa4,
0x25, 0x69, 0x0b, 0x2d, 0x28, 0x34, 0x2c, 0x95, 0x00, 0x89, 0x45, 0x08,
0x5a, 0x95, 0x03, 0x08, 0x09, 0x21, 0xe0, 0x80, 0x38, 0xc3, 0x85, 0x03,
0xe2, 0x00, 0x47, 0xc4, 0x1d, 0x38, 0x70, 0xe3, 0xc6, 0x01, 0x01, 0x42,
0x20, 0x54, 0x7a, 0x2a, 0x6b, 0x0b, 0x94, 0xd2, 0xd2, 0x25, 0x69, 0x9b,
0xa4, 0x0d, 0x2d, 0xa9, 0xb3, 0x78, 0x89, 0x9d, 0xf1, 0x2c, 0x9e, 0x85,
0x2f, 0xb5, 0x35, 0xb6, 0x35, 0x96, 0xde, 0x79, 0xdf, 0xef, 0x7f, 0x9f,
0x4f, 0xfb, 0xe0, 0xad, 0x37, 0x12, 0xfd, 0xf0, 0xb3, 0x9c, 0xfb, 0xb7,
0xc5, 0x8d, 0x46, 0x9b, 0x71, 0x5b, 0xf1, 0xd0, 0xf4, 0x18, 0xdb, 0xeb,
0x4b, 0x1c, 0xff, 0xf1, 0x57, 0x98, 0xdc, 0x87, 0x72, 0x3a, 0x8c, 0x3a,
0xcb, 0x8c, 0xea, 0x31, 0x35, 0xb7, 0xc3, 0x99, 0xba, 0xc3, 0xd7, 0xab,
0x3e, 0x87, 0x2a, 0x8a, 0xb3, 0xff, 0xdc, 0xe0, 0x9b, 0x8f, 0x5f, 0xa2,
0x1c, 0xc5, 0xfc, 0x72, 0xc9, 0x41, 0x99, 0x71, 0x48, 0xca, 0x84, 0x3c,
0x3e, 0xda, 0xd2, 0x05, 0x9a, 0xb1, 0xc7, 0x35, 0x67, 0x1c, 0xdd, 0x4c,
0x68, 0xeb, 0x26, 0xd9, 0x30, 0x26, 0x09, 0x23, 0x5c, 0x3f, 0xc2, 0xd3,
0x43, 0xc2, 0x24, 0x21, 0x4e, 0x34, 0x40, 0x27, 0x89, 0x13, 0xf9, 0x1e,
0x22, 0x6e, 0xd5, 0x45, 0x43, 0x63, 0xc6, 0xd2, 0x50, 0xa9, 0xc4, 0x67,
0x24, 0x15, 0x72, 0xa9, 0x7e, 0x95, 0xfa, 0x4f, 0x27, 0x78, 0x64, 0x76,
0x86, 0x23, 0x61, 0xc0, 0xf0, 0x58, 0x15, 0xc3, 0x29, 0x71, 0x06, 0x45,
0x2e, 0xa5, 0x48, 0xbb, 0x0a, 0x3d, 0x89, 0xa0, 0x8f, 0x08, 0x8a, 0x8e,
0x08, 0xbb, 0xc1, 0x8e, 0xb0, 0x8d, 0xdd, 0x0f, 0xc9, 0x84, 0x06, 0x65,
0x34, 0xf4, 0xed, 0x8d, 0xff, 0x58, 0xbd, 0xfc, 0x27, 0x17, 0x2f, 0x9e,
0xe3, 0xf0, 0x81, 0x49, 0x5e, 0xde, 0x5f, 0xe1, 0x9e, 0x82, 0xcd, 0xdc,
0x78, 0x8d, 0xd9, 0xb2, 0xc9, 0x56, 0x12, 0x32, 0x94, 0x4f, 0x91, 0xcb,
0x88, 0x68, 0xda, 0x42, 0x13, 0x77, 0x11, 0xa2, 0xa8, 0xc3, 0x5a, 0x5f,
0x46, 0x30, 0x65, 0x52, 0x29, 0xe4, 0x24, 0x4d, 0x8e, 0xcc, 0x68, 0x19,
0xe5, 0x76, 0xbb, 0xac, 0x5c, 0x98, 0xa7, 0xb3, 0xed, 0xd0, 0x37, 0x62,
0xa2, 0xb0, 0xc7, 0x89, 0xe5, 0x2e, 0x03, 0x0d, 0x97, 0x95, 0x46, 0x8f,
0x31, 0xd7, 0xa6, 0x63, 0x81, 0x65, 0x25, 0x84, 0xba, 0x45, 0x5f, 0x65,
0x31, 0x2c, 0x71, 0x6b, 0x77, 0x69, 0xf5, 0x7a, 0xbc, 0xb0, 0x3b, 0xcd,
0xf9, 0xa5, 0x90, 0xd1, 0xb0, 0xcd, 0xd4, 0xb0, 0xdc, 0xd7, 0xc4, 0xfa,
0xf0, 0x78, 0x95, 0x7b, 0x27, 0xab, 0x5c, 0x5e, 0x6e, 0xd2, 0xee, 0x05,
0xdc, 0xd8, 0xea, 0xf1, 0xf7, 0xe2, 0x1a, 0xc7, 0xee, 0x1a, 0x62, 0x2e,
0x1f, 0xe3, 0xe8, 0xb6, 0xc4, 0x4c, 0xd3, 0x6d, 0x6e, 0xd0, 0x6b, 0xfc,
0x4c, 0xe3, 0xd4, 0x1f, 0xc4, 0x4b, 0xf3, 0x1c, 0x2c, 0x65, 0x29, 0x67,
0x4d, 0xbe, 0xfb, 0xad, 0x45, 0x65, 0x0c, 0xea, 0x7e, 0x1f, 0x15, 0x6b,
0x09, 0x0b, 0x8b, 0xb7, 0x19, 0xc9, 0xa5, 0x78, 0x75, 0x6e, 0x18, 0xdf,
0xf5, 0x79, 0x72, 0xd0, 0xa2, 0x2d, 0xb3, 0x3a, 0xbb, 0xb4, 0x41, 0x3e,
0x53, 0xe6, 0xf4, 0xca, 0x3c, 0xa5, 0x7c, 0x86, 0xe9, 0xfd, 0x47, 0x18,
0x2e, 0xbd, 0xce, 0xd1, 0x97, 0x26, 0x78, 0xbc, 0x7e, 0x1d, 0xff, 0xcc,
0xa7, 0x5c, 0x71, 0x74, 0x16, 0xe3, 0x18, 0xd7, 0x1e, 0x23, 0xe8, 0xac,
0xa3, 0x0c, 0xcd, 0x60, 0x22, 0x6f, 0x43, 0x36, 0x43, 0x3b, 0x19, 0xc6,
0x08, 0x7a, 0xe0, 0x6c, 0xe3, 0x27, 0x8a, 0xdb, 0x4e, 0xc0, 0xd4, 0xa0,
0xcd, 0x27, 0xaf, 0xbd, 0xcb, 0x86, 0x36, 0xc6, 0xcc, 0xfe, 0x59, 0xd2,
0xca, 0x90, 0x93, 0x36, 0x70, 0xaf, 0x9c, 0xe4, 0xcb, 0x6f, 0x65, 0x54,
0xd9, 0x47, 0x59, 0x70, 0xbb, 0x74, 0x1b, 0x0e, 0x89, 0xe7, 0xa3, 0xc7,
0x12, 0x39, 0x63, 0xea, 0x68, 0x12, 0x6b, 0x53, 0x5c, 0x9e, 0xef, 0x76,
0xf0, 0x55, 0x86, 0x0d, 0x17, 0x56, 0x9a, 0x4d, 0x94, 0x95, 0x65, 0xe6,
0xbe, 0x67, 0x98, 0xbe, 0xfb, 0x21, 0x52, 0xd2, 0x43, 0xaf, 0x5d, 0x47,
0x6b, 0x5c, 0xa3, 0x59, 0xbf, 0xc2, 0x62, 0xdd, 0x26, 0xa5, 0x12, 0x6a,
0x41, 0x44, 0xdf, 0xbd, 0xcd, 0x92, 0x17, 0xa0, 0xb6, 0x03, 0x43, 0xba,
0x66, 0x91, 0xe9, 0xdc, 0xc2, 0xce, 0xed, 0xa1, 0xfc, 0xc0, 0x2b, 0x14,
0xff, 0xfd, 0x1e, 0x4b, 0xb3, 0xa9, 0x29, 0x87, 0x81, 0xd2, 0x04, 0x8e,
0x66, 0x89, 0x58, 0x00, 0x7e, 0x07, 0xaf, 0xdb, 0xa4, 0xbb, 0xb5, 0x49,
0xb9, 0xaa, 0x18, 0xb9, 0x77, 0x8e, 0xcd, 0xdb, 0x6d, 0x1e, 0x1c, 0xb5,
0x38, 0x7d, 0xa5, 0xcf, 0xaa, 0x08, 0xeb, 0x77, 0x3f, 0x35, 0xc7, 0xda,
0xfc, 0x02, 0xaa, 0xf6, 0x1c, 0xbb, 0x9f, 0x78, 0x9f, 0x89, 0x43, 0x47,
0xa4, 0x6f, 0x3d, 0x06, 0xed, 0x90, 0x92, 0x79, 0x95, 0xd4, 0xe4, 0xfd,
0x98, 0x66, 0x4a, 0x6a, 0xd7, 0xc7, 0x0b, 0x62, 0xa4, 0xe3, 0x8c, 0x4d,
0xc4, 0xe8, 0x85, 0x98, 0xe5, 0x46, 0x44, 0x26, 0x97, 0x21, 0xe9, 0xf7,
0xf9, 0x61, 0xc5, 0xe3, 0xd4, 0x66, 0x84, 0xd2, 0x70, 0xc9, 0xee, 0x79,
0x98, 0x43, 0xc7, 0x5e, 0x27, 0xb6, 0x8a, 0xd2, 0x5a, 0x1f, 0xf3, 0xa9,
0xf7, 0x88, 0xce, 0x7d, 0x85, 0x71, 0xe0, 0x79, 0x98, 0x7a, 0x90, 0x9e,
0x1b, 0xd0, 0x13, 0x52, 0x4a, 0x66, 0x97, 0x7d, 0x33, 0x1e, 0xed, 0xae,
0xc7, 0x87, 0x1f, 0x7d, 0xce, 0xc2, 0xd5, 0x3a, 0xe6, 0xde, 0x02, 0xcb,
0xdb, 0x3e, 0xbe, 0xa6, 0x91, 0x95, 0x62, 0x6b, 0x2f, 0xce, 0x90, 0x3c,
0xfd, 0xce, 0x71, 0x0e, 0xcc, 0x3e, 0x82, 0x13, 0xf4, 0x09, 0xd5, 0x00,
0x16, 0x82, 0x98, 0xb3, 0x49, 0x24, 0xb1, 0x83, 0xc8, 0xc0, 0xd6, 0x3a,
0x54, 0x33, 0xab, 0x14, 0x8c, 0x16, 0x4e, 0x38, 0xcc, 0xe5, 0xeb, 0x4d,
0x5e, 0x7b, 0xfb, 0x4d, 0xaa, 0x79, 0xa1, 0x45, 0x1c, 0x9b, 0xd2, 0x94,
0xcc, 0x0e, 0x8c, 0x52, 0x7a, 0x65, 0x17, 0xc7, 0xa9, 0x0c, 0x8e, 0xe2,
0xf7, 0xba, 0xa8, 0xc8, 0x13, 0x87, 0x32, 0x87, 0x0b, 0x27, 0x30, 0x36,
0x57, 0xe8, 0xea, 0x15, 0xce, 0x06, 0x65, 0x5e, 0x3d, 0x5a, 0x94, 0x53,
0xb7, 0x59, 0x58, 0xdf, 0x25, 0xc4, 0xe4, 0xc9, 0x65, 0x3d, 0xb4, 0xb4,
0x4e, 0x37, 0x0c, 0x29, 0x98, 0x4a, 0xe8, 0x11, 0xde, 0x85, 0x42, 0x43,
0x1c, 0xaa, 0x38, 0x55, 0xc4, 0xb4, 0x2c, 0x22, 0x3d, 0xcd, 0xfa, 0xea,
0x0d, 0xf4, 0x8d, 0x1f, 0xc9, 0x5f, 0xfa, 0x82, 0x6d, 0xc7, 0xe1, 0xa6,
0x57, 0xe3, 0x56, 0x6e, 0x96, 0xbf, 0x16, 0x1f, 0xa3, 0x54, 0xaa, 0x91,
0x16, 0x5a, 0xb2, 0xa9, 0x04, 0xaf, 0x67, 0xc9, 0xac, 0x6c, 0xfa, 0x32,
0x9e, 0x48, 0xea, 0xa5, 0x0b, 0x89, 0x3b, 0x54, 0x47, 0xf2, 0xa1, 0xf2,
0x2a, 0x4d, 0xeb, 0xf4, 0x17, 0xdc, 0xd4, 0x72, 0x6c, 0xb5, 0x36, 0x28,
0xb6, 0x7e, 0x17, 0x04, 0xd3, 0xac, 0x7a, 0x42, 0xc1, 0xf4, 0x6e, 0x9e,
0xbf, 0x6b, 0xb7, 0x3c, 0x3a, 0x21, 0x67, 0xcb, 0x41, 0x48, 0x07, 0x91,
0xde, 0x1a, 0xe2, 0xaa, 0x9c, 0xb1, 0x59, 0xdb, 0x12, 0x25, 0xc1, 0x32,
0x92, 0xea, 0xc9, 0xaf, 0x3b, 0x97, 0xca, 0xca, 0xfe, 0x5b, 0xfe, 0xe5,
0x33, 0x29, 0xeb, 0x16, 0x95, 0xd2, 0x24, 0xeb, 0xda, 0x30, 0xeb, 0x95,
0x1a, 0xd3, 0xf7, 0x0f, 0x51, 0x1c, 0xd9, 0x0b, 0x99, 0x12, 0x7a, 0x4a,
0xd0, 0xd3, 0x25, 0x9a, 0x88, 0x45, 0xb1, 0x04, 0x33, 0x2c, 0x8a, 0x99,
0x34, 0x6b, 0x75, 0x19, 0x91, 0x9d, 0x92, 0x29, 0x89, 0xa0, 0x2c, 0x8b,
0x9d, 0xd8, 0x7a, 0x5e, 0x04, 0x07, 0x87, 0x66, 0x28, 0x56, 0x67, 0xb9,
0xd6, 0xd2, 0x39, 0xd9, 0xec, 0x33, 0x30, 0xb2, 0x8b, 0xea, 0xae, 0x83,
0x18, 0xb9, 0x31, 0x34, 0xbb, 0x42, 0x22, 0x0b, 0x21, 0x96, 0x3c, 0x61,
0xac, 0xcb, 0x95, 0x60, 0x2a, 0xe9, 0x68, 0x79, 0x08, 0x36, 0x56, 0x65,
0x27, 0x4a, 0xd9, 0x83, 0x00, 0xcf, 0x0b, 0xf1, 0xfc, 0x10, 0x15, 0x0a,
0x6a, 0x75, 0x77, 0x8b, 0x86, 0xdc, 0x58, 0x57, 0x45, 0x52, 0xe9, 0x84,
0x81, 0x7c, 0x91, 0x28, 0x55, 0x23, 0x96, 0x13, 0xd7, 0x24, 0xbe, 0xac,
0x17, 0xfa, 0xf2, 0x78, 0x63, 0xc7, 0x82, 0x08, 0xda, 0xa6, 0xc5, 0x50,
0x55, 0x04, 0xe5, 0x65, 0x5b, 0x06, 0xde, 0xce, 0xf0, 0x24, 0xf3, 0x4e,
0x70, 0xb5, 0x15, 0x6a, 0x34, 0x7b, 0x11, 0x9d, 0xbe, 0x10, 0x53, 0xd0,
0xa8, 0x86, 0x2e, 0x76, 0xb6, 0x2a, 0x9d, 0x2c, 0x48, 0x3c, 0x5b, 0xa2,
0xc8, 0x3a, 0x37, 0xd4, 0x9d, 0xed, 0x6c, 0x4a, 0xab, 0x95, 0x6e, 0x08,
0x66, 0x3d, 0x5a, 0xad, 0x4d, 0x18, 0xc8, 0xca, 0xfa, 0xd5, 0x85, 0x6f,
0xf9, 0x5f, 0xde, 0x02, 0x30, 0xff, 0x03, 0x8c, 0x47, 0x35, 0xad, 0xbc,
0xbf, 0x26, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
0x42, 0x60, 0x82
};
unsigned int _Users_username_Pictures_SaltBae_png_len = 1863;
int main(int argc, const char * argv[]) {
// insert code here...
int frameless = 0;
int resizable = 1;
int zoomable = 0;
int fullscreen = 1;
int fullSizeContent = 1;
int hideTitleBar = 0;
int titlebarAppearsTransparent = 0;
int hideTitle = 0;
int useToolbar = 0;
int hideToolbarSeparator = 0;
int webviewIsTransparent = 1;
int alwaysOnTop = 0;
int hideWindowOnClose = 0;
const char* appearance = "NSAppearanceNameDarkAqua";
int windowIsTranslucent = 1;
int devtoolsEnabled = 1;
int defaultContextMenuEnabled = 1;
int windowStartState = 0;
int startsHidden = 0;
WailsContext *result = Create("OI OI!",400,400, frameless, resizable, zoomable, fullscreen, fullSizeContent, hideTitleBar, titlebarAppearsTransparent, hideTitle, useToolbar, hideToolbarSeparator, webviewIsTransparent, alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, devtoolsEnabled, defaultContextMenuEnabled, windowStartState,
startsHidden, 400, 400, 600, 600, false);
SetBackgroundColour(result, 255, 0, 0, 255);
void *m = NewMenu("");
SetAbout(result, "Fake title", "I am a description", _Users_username_Pictures_SaltBae_png, _Users_username_Pictures_SaltBae_png_len);
// AddMenuByRole(result, 1);
AppendRole(result, m, 1);
AppendRole(result, m, 2);
void* submenu = NewMenu("test");
void* menuITem = AppendMenuItem(result, submenu, "Woohoo", "p", 0, 0, 0, 470);
AppendSubmenu(m, submenu);
UpdateMenuItem(menuITem, 1);
SetAsApplicationMenu(result, m);
// SetPosition(result, 100, 100);
Run((void*)CFBridgingRetain(result));
return 0;
}

View File

@@ -0,0 +1,134 @@
//go:build darwin
// +build darwin
package darwin
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import <Foundation/Foundation.h>
#import "Application.h"
#import "WailsContext.h"
#include <stdlib.h>
*/
import "C"
import (
"unsafe"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
)
type NSMenu struct {
context unsafe.Pointer
nsmenu unsafe.Pointer
}
func NewNSMenu(context unsafe.Pointer, name string) *NSMenu {
c := NewCalloc()
defer c.Free()
title := c.String(name)
nsmenu := C.NewMenu(title)
return &NSMenu{
context: context,
nsmenu: nsmenu,
}
}
func (m *NSMenu) AddSubMenu(label string) *NSMenu {
result := NewNSMenu(m.context, label)
C.AppendSubmenu(m.nsmenu, result.nsmenu)
return result
}
func (m *NSMenu) AppendRole(role menu.Role) {
C.AppendRole(m.context, m.nsmenu, C.int(role))
}
type MenuItem struct {
id uint
nsmenuitem unsafe.Pointer
wailsMenuItem *menu.MenuItem
radioGroupMembers []*MenuItem
}
func (m *NSMenu) AddMenuItem(menuItem *menu.MenuItem) *MenuItem {
c := NewCalloc()
defer c.Free()
var modifier C.int
var key *C.char
if menuItem.Accelerator != nil {
modifier = C.int(keys.ToMacModifier(menuItem.Accelerator))
key = c.String(menuItem.Accelerator.Key)
}
result := &MenuItem{
wailsMenuItem: menuItem,
}
result.id = createMenuItemID(result)
result.nsmenuitem = C.AppendMenuItem(m.context, m.nsmenu, c.String(menuItem.Label), key, modifier, bool2Cint(menuItem.Disabled), bool2Cint(menuItem.Checked), C.int(result.id))
return result
}
//func (w *Window) SetApplicationMenu(menu *menu.Menu) {
//w.applicationMenu = menu
//processMenu(w, menu)
//}
func processMenu(parent *NSMenu, wailsMenu *menu.Menu) {
var radioGroups []*MenuItem
for _, menuItem := range wailsMenu.Items {
if menuItem.SubMenu != nil {
if len(radioGroups) > 0 {
processRadioGroups(radioGroups)
radioGroups = []*MenuItem{}
}
submenu := parent.AddSubMenu(menuItem.Label)
processMenu(submenu, menuItem.SubMenu)
} else {
lastMenuItem := processMenuItem(parent, menuItem)
if menuItem.Type == menu.RadioType {
radioGroups = append(radioGroups, lastMenuItem)
} else {
if len(radioGroups) > 0 {
processRadioGroups(radioGroups)
radioGroups = []*MenuItem{}
}
}
}
}
}
func processRadioGroups(groups []*MenuItem) {
for _, item := range groups {
item.radioGroupMembers = groups
}
}
func processMenuItem(parent *NSMenu, menuItem *menu.MenuItem) *MenuItem {
if menuItem.Hidden {
return nil
}
if menuItem.Role != 0 {
parent.AppendRole(menuItem.Role)
return nil
}
if menuItem.Type == menu.SeparatorType {
C.AppendSeparator(parent.nsmenu)
return nil
}
return parent.AddMenuItem(menuItem)
}
func (f *Frontend) MenuSetApplicationMenu(menu *menu.Menu) {
f.mainWindow.SetApplicationMenu(menu)
}
func (f *Frontend) MenuUpdateApplicationMenu() {
f.mainWindow.UpdateApplicationMenu()
}

View File

@@ -0,0 +1,54 @@
//go:build darwin
// +build darwin
package darwin
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import <Foundation/Foundation.h>
#import "Application.h"
#import "WailsContext.h"
#include <stdlib.h>
*/
import "C"
import (
"log"
"math"
"sync"
)
var (
menuItemToID = make(map[*MenuItem]uint)
idToMenuItem = make(map[uint]*MenuItem)
menuItemLock sync.Mutex
menuItemIDCounter uint = 0
)
func createMenuItemID(item *MenuItem) uint {
menuItemLock.Lock()
defer menuItemLock.Unlock()
counter := 0
for {
menuItemIDCounter++
value := idToMenuItem[menuItemIDCounter]
if value == nil {
break
}
counter++
if counter == math.MaxInt {
log.Fatal("insane amounts of menuitems detected! Aborting before the collapse of the world!")
}
}
idToMenuItem[menuItemIDCounter] = item
menuItemToID[item] = menuItemIDCounter
return menuItemIDCounter
}
func getMenuItemForID(id uint) *MenuItem {
menuItemLock.Lock()
defer menuItemLock.Unlock()
return idToMenuItem[id]
}

View File

@@ -0,0 +1,30 @@
//
// message.h
// test
//
// Created by Lea Anthony on 14/10/21.
//
#ifndef export_h
#define export_h
#ifdef __cplusplus
extern "C"
{
#endif
void processMessage(const char *);
void processBindingMessage(const char *, const char *, bool);
void processURLRequest(void *, void*);
void processMessageDialogResponse(int);
void processOpenFileDialogResponse(const char*);
void processSaveFileDialogResponse(const char*);
void processCallback(int);
#ifdef __cplusplus
}
#endif
#endif /* export_h */

View File

@@ -0,0 +1,465 @@
//go:build darwin
// +build darwin
package darwin
/*
#cgo CFLAGS:-x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000
#cgo LDFLAGS: -framework UserNotifications
#endif
#import "Application.h"
#import "WailsContext.h"
*/
import "C"
import (
"context"
"encoding/json"
"fmt"
"os"
"sync"
"time"
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend"
)
// Package-scoped variable only accessible within this file
var (
currentFrontend *Frontend
frontendMutex sync.RWMutex
// Notification channels
channels map[int]chan notificationChannel
channelsLock sync.Mutex
nextChannelID int
notificationResultCallback func(result frontend.NotificationResult)
callbackLock sync.RWMutex
)
const DefaultActionIdentifier = "DEFAULT_ACTION"
const AppleDefaultActionIdentifier = "com.apple.UNNotificationDefaultActionIdentifier"
// setCurrentFrontend sets the current frontend instance
// This is called when RequestNotificationAuthorization or CheckNotificationAuthorization is called
func setCurrentFrontend(f *Frontend) {
frontendMutex.Lock()
defer frontendMutex.Unlock()
currentFrontend = f
}
// getCurrentFrontend gets the current frontend instance
func getCurrentFrontend() *Frontend {
frontendMutex.RLock()
defer frontendMutex.RUnlock()
return currentFrontend
}
type notificationChannel struct {
Success bool
Error error
}
func (f *Frontend) InitializeNotifications() error {
if !f.IsNotificationAvailable() {
return fmt.Errorf("notifications are not available on this system")
}
if !f.checkBundleIdentifier() {
return fmt.Errorf("notifications require a valid bundle identifier")
}
if !bool(C.EnsureDelegateInitialized(f.mainWindow.context)) {
return fmt.Errorf("failed to initialize notification center delegate")
}
channels = make(map[int]chan notificationChannel)
nextChannelID = 0
setCurrentFrontend(f)
return nil
}
// CleanupNotifications is a macOS stub that does nothing.
// (Linux-specific cleanup)
func (f *Frontend) CleanupNotifications() {
// No cleanup needed on macOS
}
func (f *Frontend) IsNotificationAvailable() bool {
return bool(C.IsNotificationAvailable(f.mainWindow.context))
}
func (f *Frontend) checkBundleIdentifier() bool {
return bool(C.CheckBundleIdentifier(f.mainWindow.context))
}
func (f *Frontend) RequestNotificationAuthorization() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
defer cancel()
id, resultCh := f.registerChannel()
C.RequestNotificationAuthorization(f.mainWindow.context, C.int(id))
select {
case result := <-resultCh:
close(resultCh)
return result.Success, result.Error
case <-ctx.Done():
f.cleanupChannel(id)
return false, fmt.Errorf("notification authorization timed out after 3 minutes: %w", ctx.Err())
}
}
func (f *Frontend) CheckNotificationAuthorization() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
id, resultCh := f.registerChannel()
C.CheckNotificationAuthorization(f.mainWindow.context, C.int(id))
select {
case result := <-resultCh:
close(resultCh)
return result.Success, result.Error
case <-ctx.Done():
f.cleanupChannel(id)
return false, fmt.Errorf("notification authorization timed out after 15s: %w", ctx.Err())
}
}
// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
func (f *Frontend) SendNotification(options frontend.NotificationOptions) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cIdentifier := C.CString(options.ID)
cTitle := C.CString(options.Title)
cSubtitle := C.CString(options.Subtitle)
cBody := C.CString(options.Body)
defer C.free(unsafe.Pointer(cIdentifier))
defer C.free(unsafe.Pointer(cTitle))
defer C.free(unsafe.Pointer(cSubtitle))
defer C.free(unsafe.Pointer(cBody))
var cDataJSON *C.char
if options.Data != nil {
jsonData, err := json.Marshal(options.Data)
if err != nil {
return fmt.Errorf("failed to marshal notification data: %w", err)
}
cDataJSON = C.CString(string(jsonData))
defer C.free(unsafe.Pointer(cDataJSON))
}
id, resultCh := f.registerChannel()
C.SendNotification(f.mainWindow.context, C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cDataJSON)
select {
case result := <-resultCh:
close(resultCh)
if !result.Success {
if result.Error != nil {
return result.Error
}
return fmt.Errorf("sending notification failed")
}
return nil
case <-ctx.Done():
f.cleanupChannel(id)
return fmt.Errorf("sending notification timed out: %w", ctx.Err())
}
}
// SendNotificationWithActions sends a notification with additional actions and inputs.
// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category.
// If a NotificationCategory is not registered a basic notification will be sent.
func (f *Frontend) SendNotificationWithActions(options frontend.NotificationOptions) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cIdentifier := C.CString(options.ID)
cTitle := C.CString(options.Title)
cSubtitle := C.CString(options.Subtitle)
cBody := C.CString(options.Body)
cCategoryID := C.CString(options.CategoryID)
defer C.free(unsafe.Pointer(cIdentifier))
defer C.free(unsafe.Pointer(cTitle))
defer C.free(unsafe.Pointer(cSubtitle))
defer C.free(unsafe.Pointer(cBody))
defer C.free(unsafe.Pointer(cCategoryID))
var cDataJSON *C.char
if options.Data != nil {
jsonData, err := json.Marshal(options.Data)
if err != nil {
return fmt.Errorf("failed to marshal notification data: %w", err)
}
cDataJSON = C.CString(string(jsonData))
defer C.free(unsafe.Pointer(cDataJSON))
}
id, resultCh := f.registerChannel()
C.SendNotificationWithActions(f.mainWindow.context, C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cCategoryID, cDataJSON)
select {
case result := <-resultCh:
close(resultCh)
if !result.Success {
if result.Error != nil {
return result.Error
}
return fmt.Errorf("sending notification failed")
}
return nil
case <-ctx.Done():
f.cleanupChannel(id)
return fmt.Errorf("sending notification timed out: %w", ctx.Err())
}
}
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
// Registering a category with the same name as a previously registered NotificationCategory will override it.
func (f *Frontend) RegisterNotificationCategory(category frontend.NotificationCategory) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cCategoryID := C.CString(category.ID)
defer C.free(unsafe.Pointer(cCategoryID))
actionsJSON, err := json.Marshal(category.Actions)
if err != nil {
return fmt.Errorf("failed to marshal notification category: %w", err)
}
cActionsJSON := C.CString(string(actionsJSON))
defer C.free(unsafe.Pointer(cActionsJSON))
var cReplyPlaceholder, cReplyButtonTitle *C.char
if category.HasReplyField {
cReplyPlaceholder = C.CString(category.ReplyPlaceholder)
cReplyButtonTitle = C.CString(category.ReplyButtonTitle)
defer C.free(unsafe.Pointer(cReplyPlaceholder))
defer C.free(unsafe.Pointer(cReplyButtonTitle))
}
id, resultCh := f.registerChannel()
C.RegisterNotificationCategory(f.mainWindow.context, C.int(id), cCategoryID, cActionsJSON, C.bool(category.HasReplyField),
cReplyPlaceholder, cReplyButtonTitle)
select {
case result := <-resultCh:
close(resultCh)
if !result.Success {
if result.Error != nil {
return result.Error
}
return fmt.Errorf("category registration failed")
}
return nil
case <-ctx.Done():
f.cleanupChannel(id)
return fmt.Errorf("category registration timed out: %w", ctx.Err())
}
}
// RemoveNotificationCategory remove a previously registered NotificationCategory.
func (f *Frontend) RemoveNotificationCategory(categoryId string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cCategoryID := C.CString(categoryId)
defer C.free(unsafe.Pointer(cCategoryID))
id, resultCh := f.registerChannel()
C.RemoveNotificationCategory(f.mainWindow.context, C.int(id), cCategoryID)
select {
case result := <-resultCh:
close(resultCh)
if !result.Success {
if result.Error != nil {
return result.Error
}
return fmt.Errorf("category removal failed")
}
return nil
case <-ctx.Done():
f.cleanupChannel(id)
return fmt.Errorf("category removal timed out: %w", ctx.Err())
}
}
// RemoveAllPendingNotifications removes all pending notifications.
func (f *Frontend) RemoveAllPendingNotifications() error {
C.RemoveAllPendingNotifications(f.mainWindow.context)
return nil
}
// RemovePendingNotification removes a pending notification matching the unique identifier.
func (f *Frontend) RemovePendingNotification(identifier string) error {
cIdentifier := C.CString(identifier)
defer C.free(unsafe.Pointer(cIdentifier))
C.RemovePendingNotification(f.mainWindow.context, cIdentifier)
return nil
}
// RemoveAllDeliveredNotifications removes all delivered notifications.
func (f *Frontend) RemoveAllDeliveredNotifications() error {
C.RemoveAllDeliveredNotifications(f.mainWindow.context)
return nil
}
// RemoveDeliveredNotification removes a delivered notification matching the unique identifier.
func (f *Frontend) RemoveDeliveredNotification(identifier string) error {
cIdentifier := C.CString(identifier)
defer C.free(unsafe.Pointer(cIdentifier))
C.RemoveDeliveredNotification(f.mainWindow.context, cIdentifier)
return nil
}
// RemoveNotification is a macOS stub that always returns nil.
// Use one of the following instead:
// RemoveAllPendingNotifications
// RemovePendingNotification
// RemoveAllDeliveredNotifications
// RemoveDeliveredNotification
// (Linux-specific)
func (f *Frontend) RemoveNotification(identifier string) error {
return nil
}
func (f *Frontend) OnNotificationResponse(callback func(result frontend.NotificationResult)) {
callbackLock.Lock()
notificationResultCallback = callback
callbackLock.Unlock()
}
//export captureResult
func captureResult(channelID C.int, success C.bool, errorMsg *C.char) {
f := getCurrentFrontend()
if f == nil {
return
}
resultCh, exists := f.GetChannel(int(channelID))
if !exists {
return
}
var err error
if errorMsg != nil {
err = fmt.Errorf("%s", C.GoString(errorMsg))
}
resultCh <- notificationChannel{
Success: bool(success),
Error: err,
}
}
//export didReceiveNotificationResponse
func didReceiveNotificationResponse(jsonPayload *C.char, err *C.char) {
result := frontend.NotificationResult{}
if err != nil {
errMsg := C.GoString(err)
result.Error = fmt.Errorf("notification response error: %s", errMsg)
handleNotificationResult(result)
return
}
if jsonPayload == nil {
result.Error = fmt.Errorf("received nil JSON payload in notification response")
handleNotificationResult(result)
return
}
payload := C.GoString(jsonPayload)
var response frontend.NotificationResponse
if err := json.Unmarshal([]byte(payload), &response); err != nil {
result.Error = fmt.Errorf("failed to unmarshal notification response: %w", err)
handleNotificationResult(result)
return
}
if response.ActionIdentifier == AppleDefaultActionIdentifier {
response.ActionIdentifier = DefaultActionIdentifier
}
result.Response = response
handleNotificationResult(result)
}
func handleNotificationResult(result frontend.NotificationResult) {
callbackLock.Lock()
callback := notificationResultCallback
callbackLock.Unlock()
if callback != nil {
go func() {
defer func() {
if r := recover(); r != nil {
// Log panic but don't crash the app
fmt.Fprintf(os.Stderr, "panic in notification callback: %v\n", r)
}
}()
callback(result)
}()
}
}
// Helper methods
func (f *Frontend) registerChannel() (int, chan notificationChannel) {
channelsLock.Lock()
defer channelsLock.Unlock()
// Initialize channels map if it's nil
if channels == nil {
channels = make(map[int]chan notificationChannel)
nextChannelID = 0
}
id := nextChannelID
nextChannelID++
resultCh := make(chan notificationChannel, 1)
channels[id] = resultCh
return id, resultCh
}
func (f *Frontend) GetChannel(id int) (chan notificationChannel, bool) {
channelsLock.Lock()
defer channelsLock.Unlock()
if channels == nil {
return nil, false
}
ch, exists := channels[id]
if exists {
delete(channels, id)
}
return ch, exists
}
func (f *Frontend) cleanupChannel(id int) {
channelsLock.Lock()
defer channelsLock.Unlock()
if channels == nil {
return
}
if ch, exists := channels[id]; exists {
delete(channels, id)
close(ch)
}
}

View File

@@ -0,0 +1,118 @@
//go:build darwin
// +build darwin
package darwin
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit -framework AppKit
#import <Foundation/Foundation.h>
#include <AppKit/AppKit.h>
#include <stdlib.h>
#import "Application.h"
#import "WailsContext.h"
typedef struct Screen {
int isCurrent;
int isPrimary;
int height;
int width;
int pHeight;
int pWidth;
} Screen;
int GetNumScreens(){
return [[NSScreen screens] count];
}
int screenUniqueID(NSScreen *screen){
// adapted from https://stackoverflow.com/a/1237490/4188138
NSDictionary* screenDictionary = [screen deviceDescription];
NSNumber* screenID = [screenDictionary objectForKey:@"NSScreenNumber"];
CGDirectDisplayID aID = [screenID unsignedIntValue];
return aID;
}
Screen GetNthScreen(int nth, void *inctx){
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSArray<NSScreen *> *screens = [NSScreen screens];
NSScreen* nthScreen = [screens objectAtIndex:nth];
NSScreen* currentScreen = [ctx getCurrentScreen];
Screen returnScreen;
returnScreen.isCurrent = (int)(screenUniqueID(currentScreen)==screenUniqueID(nthScreen));
// TODO properly handle screen mirroring
// from apple documentation:
// https://developer.apple.com/documentation/appkit/nsscreen/1388393-screens?language=objc
// The screen at index 0 in the returned array corresponds to the primary screen of the users system. This is the screen that contains the menu bar and whose origin is at the point (0, 0). In the case of mirroring, the first screen is the largest drawable display; if all screens are the same size, it is the screen with the highest pixel depth. This primary screen may not be the same as the one returned by the mainScreen method, which returns the screen with the active window.
returnScreen.isPrimary = nth==0;
returnScreen.height = (int) nthScreen.frame.size.height;
returnScreen.width = (int) nthScreen.frame.size.width;
returnScreen.pWidth = 0;
returnScreen.pHeight = 0;
// https://stackoverflow.com/questions/13859109/how-to-programmatically-determine-native-pixel-resolution-of-retina-macbook-pro
CGDirectDisplayID sid = ((NSNumber *)[nthScreen.deviceDescription
objectForKey:@"NSScreenNumber"]).unsignedIntegerValue;
CFArrayRef ms = CGDisplayCopyAllDisplayModes(sid, NULL);
CFIndex n = CFArrayGetCount(ms);
for (int i = 0; i < n; i++) {
CGDisplayModeRef m = (CGDisplayModeRef) CFArrayGetValueAtIndex(ms, i);
if (CGDisplayModeGetIOFlags(m) & kDisplayModeNativeFlag) {
// This corresponds with "System Settings" -> General -> About -> Displays
returnScreen.pWidth = CGDisplayModeGetPixelWidth(m);
returnScreen.pHeight = CGDisplayModeGetPixelHeight(m);
break;
}
}
CFRelease(ms);
if (returnScreen.pWidth == 0 || returnScreen.pHeight == 0) {
// If there was no native resolution take a best fit approach and use the backing pixel size.
NSRect pSize = [nthScreen convertRectToBacking:nthScreen.frame];
returnScreen.pHeight = (int) pSize.size.height;
returnScreen.pWidth = (int) pSize.size.width;
}
return returnScreen;
}
*/
import "C"
import (
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend"
)
func GetAllScreens(wailsContext unsafe.Pointer) ([]frontend.Screen, error) {
err := error(nil)
screens := []frontend.Screen{}
numScreens := int(C.GetNumScreens())
for screeNum := 0; screeNum < numScreens; screeNum++ {
screenNumC := C.int(screeNum)
cScreen := C.GetNthScreen(screenNumC, wailsContext)
screen := frontend.Screen{
Height: int(cScreen.height),
Width: int(cScreen.width),
IsCurrent: cScreen.isCurrent == C.int(1),
IsPrimary: cScreen.isPrimary == C.int(1),
Size: frontend.ScreenSize{
Height: int(cScreen.height),
Width: int(cScreen.width),
},
PhysicalSize: frontend.ScreenSize{
Height: int(cScreen.pHeight),
Width: int(cScreen.pWidth),
},
}
screens = append(screens, screen)
}
return screens, err
}

View File

@@ -0,0 +1,95 @@
//go:build darwin
// +build darwin
package darwin
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa
#import "AppDelegate.h"
*/
import "C"
import (
"encoding/json"
"fmt"
"os"
"strings"
"syscall"
"unsafe"
"github.com/wailsapp/wails/v2/pkg/options"
)
func SetupSingleInstance(uniqueID string) *os.File {
lockFilePath := getTempDir()
lockFileName := uniqueID + ".lock"
file, err := createLockFile(lockFilePath + "/" + lockFileName)
// if lockFile exist send notification to second instance
if err != nil {
c := NewCalloc()
defer c.Free()
singleInstanceUniqueId := c.String(uniqueID)
data, err := options.NewSecondInstanceData()
if err != nil {
return nil
}
serialized, err := json.Marshal(data)
if err != nil {
return nil
}
C.SendDataToFirstInstance(singleInstanceUniqueId, c.String(string(serialized)))
os.Exit(0)
}
return file
}
//export HandleSecondInstanceData
func HandleSecondInstanceData(secondInstanceMessage *C.char) {
message := C.GoString(secondInstanceMessage)
var secondInstanceData options.SecondInstanceData
err := json.Unmarshal([]byte(message), &secondInstanceData)
if err == nil {
secondInstanceBuffer <- secondInstanceData
}
}
// createLockFile tries to create a file with given name and acquire an
// exclusive lock on it. If the file already exists AND is still locked, it will
// fail.
func createLockFile(filename string) (*os.File, error) {
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0o600)
if err != nil {
fmt.Printf("Failed to open lockfile %s: %s", filename, err)
return nil, err
}
err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err != nil {
// Flock failed for some other reason than other instance already lock it. Print it in logs for possible debugging.
if !strings.Contains(err.Error(), "resource temporarily unavailable") {
fmt.Printf("Failed to lock lockfile %s: %s", filename, err)
}
file.Close()
return nil, err
}
return file, nil
}
// If app is sandboxed, golang os.TempDir() will return path that will not be accessible. So use native macOS temp dir function.
func getTempDir() string {
cstring := C.GetMacOsNativeTempDir()
path := C.GoString(cstring)
C.free(unsafe.Pointer(cstring))
return path
}

View File

@@ -0,0 +1,313 @@
//go:build darwin
// +build darwin
package darwin
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import <Foundation/Foundation.h>
#import "Application.h"
#import "WailsContext.h"
#include <stdlib.h>
*/
import "C"
import (
"log"
"runtime"
"strconv"
"strings"
"unsafe"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
)
func init() {
runtime.LockOSThread()
}
type Window struct {
context unsafe.Pointer
applicationMenu *menu.Menu
}
func bool2Cint(value bool) C.int {
if value {
return C.int(1)
}
return C.int(0)
}
func bool2CboolPtr(value bool) *C.bool {
v := C.bool(value)
return &v
}
func NewWindow(frontendOptions *options.App, debug bool, devtools bool) *Window {
c := NewCalloc()
defer c.Free()
frameless := bool2Cint(frontendOptions.Frameless)
resizable := bool2Cint(!frontendOptions.DisableResize)
fullscreen := bool2Cint(frontendOptions.Fullscreen)
alwaysOnTop := bool2Cint(frontendOptions.AlwaysOnTop)
hideWindowOnClose := bool2Cint(frontendOptions.HideWindowOnClose)
startsHidden := bool2Cint(frontendOptions.StartHidden)
devtoolsEnabled := bool2Cint(devtools)
defaultContextMenuEnabled := bool2Cint(debug || frontendOptions.EnableDefaultContextMenu)
singleInstanceEnabled := bool2Cint(frontendOptions.SingleInstanceLock != nil)
var fullSizeContent, hideTitleBar, zoomable, hideTitle, useToolbar, webviewIsTransparent C.int
var titlebarAppearsTransparent, hideToolbarSeparator, windowIsTranslucent, contentProtection C.int
var appearance, title *C.char
var preferences C.struct_Preferences
width := C.int(frontendOptions.Width)
height := C.int(frontendOptions.Height)
minWidth := C.int(frontendOptions.MinWidth)
minHeight := C.int(frontendOptions.MinHeight)
maxWidth := C.int(frontendOptions.MaxWidth)
maxHeight := C.int(frontendOptions.MaxHeight)
windowStartState := C.int(int(frontendOptions.WindowStartState))
title = c.String(frontendOptions.Title)
singleInstanceUniqueIdStr := ""
if frontendOptions.SingleInstanceLock != nil {
singleInstanceUniqueIdStr = frontendOptions.SingleInstanceLock.UniqueId
}
singleInstanceUniqueId := c.String(singleInstanceUniqueIdStr)
enableFraudulentWebsiteWarnings := C.bool(frontendOptions.EnableFraudulentWebsiteDetection)
enableDragAndDrop := C.bool(frontendOptions.DragAndDrop != nil && frontendOptions.DragAndDrop.EnableFileDrop)
disableWebViewDragAndDrop := C.bool(frontendOptions.DragAndDrop != nil && frontendOptions.DragAndDrop.DisableWebViewDrop)
if frontendOptions.Mac != nil {
mac := frontendOptions.Mac
if mac.TitleBar != nil {
fullSizeContent = bool2Cint(mac.TitleBar.FullSizeContent)
hideTitleBar = bool2Cint(mac.TitleBar.HideTitleBar)
hideTitle = bool2Cint(mac.TitleBar.HideTitle)
useToolbar = bool2Cint(mac.TitleBar.UseToolbar)
titlebarAppearsTransparent = bool2Cint(mac.TitleBar.TitlebarAppearsTransparent)
hideToolbarSeparator = bool2Cint(mac.TitleBar.HideToolbarSeparator)
}
if mac.Preferences != nil {
if mac.Preferences.TabFocusesLinks.IsSet() {
preferences.tabFocusesLinks = bool2CboolPtr(mac.Preferences.TabFocusesLinks.Get())
}
if mac.Preferences.TextInteractionEnabled.IsSet() {
preferences.textInteractionEnabled = bool2CboolPtr(mac.Preferences.TextInteractionEnabled.Get())
}
if mac.Preferences.FullscreenEnabled.IsSet() {
preferences.fullscreenEnabled = bool2CboolPtr(mac.Preferences.FullscreenEnabled.Get())
}
}
zoomable = bool2Cint(!frontendOptions.Mac.DisableZoom)
windowIsTranslucent = bool2Cint(mac.WindowIsTranslucent)
webviewIsTransparent = bool2Cint(mac.WebviewIsTransparent)
contentProtection = bool2Cint(mac.ContentProtection)
appearance = c.String(string(mac.Appearance))
}
var context *C.WailsContext = C.Create(title, width, height, frameless, resizable, zoomable, fullscreen, fullSizeContent,
hideTitleBar, titlebarAppearsTransparent, hideTitle, useToolbar, hideToolbarSeparator, webviewIsTransparent,
alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, contentProtection, devtoolsEnabled, defaultContextMenuEnabled,
windowStartState, startsHidden, minWidth, minHeight, maxWidth, maxHeight, enableFraudulentWebsiteWarnings,
preferences, singleInstanceEnabled, singleInstanceUniqueId, enableDragAndDrop, disableWebViewDragAndDrop,
)
// Create menu
result := &Window{
context: unsafe.Pointer(context),
}
if frontendOptions.BackgroundColour != nil {
result.SetBackgroundColour(frontendOptions.BackgroundColour.R, frontendOptions.BackgroundColour.G, frontendOptions.BackgroundColour.B, frontendOptions.BackgroundColour.A)
}
if frontendOptions.Mac != nil && frontendOptions.Mac.About != nil {
title := c.String(frontendOptions.Mac.About.Title)
description := c.String(frontendOptions.Mac.About.Message)
var icon unsafe.Pointer
var length C.int
if frontendOptions.Mac.About.Icon != nil {
icon = unsafe.Pointer(&frontendOptions.Mac.About.Icon[0])
length = C.int(len(frontendOptions.Mac.About.Icon))
}
C.SetAbout(result.context, title, description, icon, length)
}
if frontendOptions.Menu != nil {
result.SetApplicationMenu(frontendOptions.Menu)
}
if debug && frontendOptions.Debug.OpenInspectorOnStartup {
showInspector(result.context)
}
return result
}
func (w *Window) Center() {
C.Center(w.context)
}
func (w *Window) Run(url string) {
_url := C.CString(url)
C.Run(w.context, _url)
C.free(unsafe.Pointer(_url))
}
func (w *Window) Quit() {
C.Quit(w.context)
}
func (w *Window) SetBackgroundColour(r uint8, g uint8, b uint8, a uint8) {
C.SetBackgroundColour(w.context, C.int(r), C.int(g), C.int(b), C.int(a))
}
func (w *Window) ExecJS(js string) {
_js := C.CString(js)
C.ExecJS(w.context, _js)
C.free(unsafe.Pointer(_js))
}
func (w *Window) SetPosition(x int, y int) {
C.SetPosition(w.context, C.int(x), C.int(y))
}
func (w *Window) SetSize(width int, height int) {
C.SetSize(w.context, C.int(width), C.int(height))
}
func (w *Window) SetAlwaysOnTop(onTop bool) {
C.SetAlwaysOnTop(w.context, bool2Cint(onTop))
}
func (w *Window) SetTitle(title string) {
t := C.CString(title)
C.SetTitle(w.context, t)
C.free(unsafe.Pointer(t))
}
func (w *Window) Maximise() {
C.Maximise(w.context)
}
func (w *Window) ToggleMaximise() {
C.ToggleMaximise(w.context)
}
func (w *Window) UnMaximise() {
C.UnMaximise(w.context)
}
func (w *Window) IsMaximised() bool {
return (bool)(C.IsMaximised(w.context))
}
func (w *Window) Minimise() {
C.Minimise(w.context)
}
func (w *Window) UnMinimise() {
C.UnMinimise(w.context)
}
func (w *Window) IsMinimised() bool {
return (bool)(C.IsMinimised(w.context))
}
func (w *Window) IsNormal() bool {
return !w.IsMaximised() && !w.IsMinimised() && !w.IsFullScreen()
}
func (w *Window) SetMinSize(width int, height int) {
C.SetMinSize(w.context, C.int(width), C.int(height))
}
func (w *Window) SetMaxSize(width int, height int) {
C.SetMaxSize(w.context, C.int(width), C.int(height))
}
func (w *Window) Fullscreen() {
C.Fullscreen(w.context)
}
func (w *Window) UnFullscreen() {
C.UnFullscreen(w.context)
}
func (w *Window) IsFullScreen() bool {
return (bool)(C.IsFullScreen(w.context))
}
func (w *Window) Show() {
C.Show(w.context)
}
func (w *Window) Hide() {
C.Hide(w.context)
}
func (w *Window) ShowApplication() {
C.ShowApplication(w.context)
}
func (w *Window) HideApplication() {
C.HideApplication(w.context)
}
func parseIntDuo(temp string) (int, int) {
split := strings.Split(temp, ",")
x, err := strconv.Atoi(split[0])
if err != nil {
log.Fatal(err)
}
y, err := strconv.Atoi(split[1])
if err != nil {
log.Fatal(err)
}
return x, y
}
func (w *Window) GetPosition() (int, int) {
var _result *C.char = C.GetPosition(w.context)
temp := C.GoString(_result)
return parseIntDuo(temp)
}
func (w *Window) Size() (int, int) {
var _result *C.char = C.GetSize(w.context)
temp := C.GoString(_result)
return parseIntDuo(temp)
}
func (w *Window) SetApplicationMenu(inMenu *menu.Menu) {
w.applicationMenu = inMenu
w.UpdateApplicationMenu()
}
func (w *Window) UpdateApplicationMenu() {
mainMenu := NewNSMenu(w.context, "")
if w.applicationMenu != nil {
processMenu(mainMenu, w.applicationMenu)
}
C.SetAsApplicationMenu(w.context, mainMenu.nsmenu)
C.UpdateApplicationMenu(w.context)
}
func (w Window) Print() {
C.WindowPrint(w.context)
}

View File

@@ -0,0 +1,20 @@
//go:build darwin
// +build darwin
package desktop
import (
"context"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/darwin"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/pkg/options"
)
func NewFrontend(ctx context.Context, appoptions *options.App, logger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) frontend.Frontend {
return darwin.NewFrontend(ctx, appoptions, logger, appBindings, dispatcher)
}

View File

@@ -0,0 +1,17 @@
//go:build linux
// +build linux
package desktop
import (
"context"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/linux"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/pkg/options"
)
func NewFrontend(ctx context.Context, appoptions *options.App, logger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) frontend.Frontend {
return linux.NewFrontend(ctx, appoptions, logger, appBindings, dispatcher)
}

View File

@@ -0,0 +1,17 @@
//go:build windows
// +build windows
package desktop
import (
"context"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/pkg/options"
)
func NewFrontend(ctx context.Context, appoptions *options.App, logger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) frontend.Frontend {
return windows.NewFrontend(ctx, appoptions, logger, appBindings, dispatcher)
}

View File

@@ -0,0 +1,23 @@
//go:build linux
// +build linux
package linux
import (
"fmt"
"github.com/pkg/browser"
"github.com/wailsapp/wails/v2/internal/frontend/utils"
)
// BrowserOpenURL Use the default browser to open the url
func (f *Frontend) BrowserOpenURL(rawURL string) {
url, err := utils.ValidateAndSanitizeURL(rawURL)
if err != nil {
f.logger.Error(fmt.Sprintf("Invalid URL %s", err.Error()))
return
}
// Specific method implementation
if err := browser.OpenURL(url); err != nil {
f.logger.Error("Unable to open default system browser")
}
}

View File

@@ -0,0 +1,35 @@
//go:build linux
// +build linux
package linux
/*
#include <stdlib.h>
*/
import "C"
import "unsafe"
// Calloc handles alloc/dealloc of C data
type Calloc struct {
pool []unsafe.Pointer
}
// NewCalloc creates a new allocator
func NewCalloc() Calloc {
return Calloc{}
}
// String creates a new C string and retains a reference to it
func (c Calloc) String(in string) *C.char {
result := C.CString(in)
c.pool = append(c.pool, unsafe.Pointer(result))
return result
}
// Free frees all allocated C memory
func (c Calloc) Free() {
for _, str := range c.pool {
C.free(str)
}
c.pool = []unsafe.Pointer{}
}

View File

@@ -0,0 +1,51 @@
//go:build linux
// +build linux
package linux
/*
#cgo linux pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
static gchar* GetClipboardText() {
GtkClipboard *clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
return gtk_clipboard_wait_for_text(clip);
}
static void SetClipboardText(gchar* text) {
GtkClipboard *clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
gtk_clipboard_set_text(clip, text, -1);
clip = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
gtk_clipboard_set_text(clip, text, -1);
}
*/
import "C"
import "sync"
func (f *Frontend) ClipboardGetText() (string, error) {
var text string
var wg sync.WaitGroup
wg.Add(1)
invokeOnMainThread(func() {
ctxt := C.GetClipboardText()
defer C.g_free(C.gpointer(ctxt))
text = C.GoString(ctxt)
wg.Done()
})
wg.Wait()
return text, nil
}
func (f *Frontend) ClipboardSetText(text string) error {
invokeOnMainThread(func() {
ctxt := (*C.gchar)(C.CString(text))
defer C.g_free(C.gpointer(ctxt))
C.SetClipboardText(ctxt)
})
return nil
}

View File

@@ -0,0 +1,89 @@
//go:build linux
// +build linux
package linux
import (
"github.com/wailsapp/wails/v2/internal/frontend"
"unsafe"
)
/*
#include <stdlib.h>
#include "gtk/gtk.h"
*/
import "C"
const (
GTK_FILE_CHOOSER_ACTION_OPEN C.GtkFileChooserAction = C.GTK_FILE_CHOOSER_ACTION_OPEN
GTK_FILE_CHOOSER_ACTION_SAVE C.GtkFileChooserAction = C.GTK_FILE_CHOOSER_ACTION_SAVE
GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER C.GtkFileChooserAction = C.GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER
)
var openFileResults = make(chan []string)
var messageDialogResult = make(chan string)
func (f *Frontend) OpenFileDialog(dialogOptions frontend.OpenDialogOptions) (result string, err error) {
f.mainWindow.OpenFileDialog(dialogOptions, 0, GTK_FILE_CHOOSER_ACTION_OPEN)
results := <-openFileResults
if len(results) == 1 {
return results[0], nil
}
return "", nil
}
func (f *Frontend) OpenMultipleFilesDialog(dialogOptions frontend.OpenDialogOptions) ([]string, error) {
f.mainWindow.OpenFileDialog(dialogOptions, 1, GTK_FILE_CHOOSER_ACTION_OPEN)
result := <-openFileResults
return result, nil
}
func (f *Frontend) OpenDirectoryDialog(dialogOptions frontend.OpenDialogOptions) (string, error) {
f.mainWindow.OpenFileDialog(dialogOptions, 0, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER)
result := <-openFileResults
if len(result) == 1 {
return result[0], nil
}
return "", nil
}
func (f *Frontend) SaveFileDialog(dialogOptions frontend.SaveDialogOptions) (string, error) {
options := frontend.OpenDialogOptions{
DefaultDirectory: dialogOptions.DefaultDirectory,
DefaultFilename: dialogOptions.DefaultFilename,
Title: dialogOptions.Title,
Filters: dialogOptions.Filters,
ShowHiddenFiles: dialogOptions.ShowHiddenFiles,
CanCreateDirectories: dialogOptions.CanCreateDirectories,
}
f.mainWindow.OpenFileDialog(options, 0, GTK_FILE_CHOOSER_ACTION_SAVE)
results := <-openFileResults
if len(results) == 1 {
return results[0], nil
}
return "", nil
}
func (f *Frontend) MessageDialog(dialogOptions frontend.MessageDialogOptions) (string, error) {
f.mainWindow.MessageDialog(dialogOptions)
return <-messageDialogResult, nil
}
//export processOpenFileResult
func processOpenFileResult(carray **C.char) {
// Create a Go slice from the C array
var result []string
goArray := (*[1024]*C.char)(unsafe.Pointer(carray))[:1024:1024]
for _, s := range goArray {
if s == nil {
break
}
result = append(result, C.GoString(s))
}
openFileResults <- result
}
//export processMessageDialogResult
func processMessageDialogResult(result *C.char) {
messageDialogResult <- C.GoString(result)
}

View File

@@ -0,0 +1,589 @@
//go:build linux
// +build linux
package linux
/*
#cgo linux pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
#include "gtk/gtk.h"
#include "webkit2/webkit2.h"
// CREDIT: https://github.com/rainycape/magick
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
static void fix_signal(int signum)
{
struct sigaction st;
if (sigaction(signum, NULL, &st) < 0) {
goto fix_signal_error;
}
st.sa_flags |= SA_ONSTACK;
if (sigaction(signum, &st, NULL) < 0) {
goto fix_signal_error;
}
return;
fix_signal_error:
fprintf(stderr, "error fixing handler for signal %d, please "
"report this issue to "
"https://github.com/wailsapp/wails: %s\n",
signum, strerror(errno));
}
static void install_signal_handlers()
{
#if defined(SIGCHLD)
fix_signal(SIGCHLD);
#endif
#if defined(SIGHUP)
fix_signal(SIGHUP);
#endif
#if defined(SIGINT)
fix_signal(SIGINT);
#endif
#if defined(SIGQUIT)
fix_signal(SIGQUIT);
#endif
#if defined(SIGABRT)
fix_signal(SIGABRT);
#endif
#if defined(SIGFPE)
fix_signal(SIGFPE);
#endif
#if defined(SIGTERM)
fix_signal(SIGTERM);
#endif
#if defined(SIGBUS)
fix_signal(SIGBUS);
#endif
#if defined(SIGSEGV)
fix_signal(SIGSEGV);
#endif
#if defined(SIGXCPU)
fix_signal(SIGXCPU);
#endif
#if defined(SIGXFSZ)
fix_signal(SIGXFSZ);
#endif
}
static gboolean install_signal_handlers_idle(gpointer data) {
(void)data;
install_signal_handlers();
return G_SOURCE_REMOVE;
}
static void fix_signal_handlers_after_gtk_init() {
g_idle_add(install_signal_handlers_idle, NULL);
}
*/
import "C"
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"net"
"net/url"
"os"
"runtime"
"strings"
"sync"
"text/template"
"unsafe"
"github.com/wailsapp/wails/v2/pkg/assetserver"
"github.com/wailsapp/wails/v2/pkg/assetserver/webview"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/frontend/originvalidator"
wailsruntime "github.com/wailsapp/wails/v2/internal/frontend/runtime"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/pkg/options"
)
var initOnce = sync.Once{}
const startURL = "wails://wails/"
var secondInstanceBuffer = make(chan options.SecondInstanceData, 1)
type Frontend struct {
// Context
ctx context.Context
frontendOptions *options.App
logger *logger.Logger
debug bool
devtoolsEnabled bool
// Assets
assets *assetserver.AssetServer
startURL *url.URL
// main window handle
mainWindow *Window
bindings *binding.Bindings
dispatcher frontend.Dispatcher
originValidator *originvalidator.OriginValidator
}
func (f *Frontend) RunMainLoop() {
C.gtk_main()
}
func (f *Frontend) WindowClose() {
f.mainWindow.Destroy()
}
func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend {
initOnce.Do(func() {
runtime.LockOSThread()
// Set GDK_BACKEND=x11 if currently unset and XDG_SESSION_TYPE is unset, unspecified or x11 to prevent warnings
if os.Getenv("GDK_BACKEND") == "" && (os.Getenv("XDG_SESSION_TYPE") == "" || os.Getenv("XDG_SESSION_TYPE") == "unspecified" || os.Getenv("XDG_SESSION_TYPE") == "x11") {
_ = os.Setenv("GDK_BACKEND", "x11")
}
if ok := C.gtk_init_check(nil, nil); ok != 1 {
panic(errors.New("failed to init GTK"))
}
})
result := &Frontend{
frontendOptions: appoptions,
logger: myLogger,
bindings: appBindings,
dispatcher: dispatcher,
ctx: ctx,
}
result.startURL, _ = url.Parse(startURL)
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil {
result.startURL = _starturl
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
} else {
if port, _ := ctx.Value("assetserverport").(string); port != "" {
result.startURL.Host = net.JoinHostPort(result.startURL.Host+".localhost", port)
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
}
var bindings string
var err error
if _obfuscated, _ := ctx.Value("obfuscated").(bool); !_obfuscated {
bindings, err = appBindings.ToJSON()
if err != nil {
log.Fatal(err)
}
} else {
appBindings.DB().UpdateObfuscatedCallMap()
}
assets, err := assetserver.NewAssetServerMainPage(bindings, appoptions, ctx.Value("assetdir") != nil, myLogger, wailsruntime.RuntimeAssetsBundle)
if err != nil {
log.Fatal(err)
}
result.assets = assets
go result.startRequestProcessor()
}
go result.startMessageProcessor()
go result.startBindingsMessageProcessor()
var _debug = ctx.Value("debug")
var _devtoolsEnabled = ctx.Value("devtoolsEnabled")
if _debug != nil {
result.debug = _debug.(bool)
}
if _devtoolsEnabled != nil {
result.devtoolsEnabled = _devtoolsEnabled.(bool)
}
result.mainWindow = NewWindow(appoptions, result.debug, result.devtoolsEnabled)
C.fix_signal_handlers_after_gtk_init()
if appoptions.Linux != nil && appoptions.Linux.ProgramName != "" {
prgname := C.CString(appoptions.Linux.ProgramName)
C.g_set_prgname(prgname)
C.free(unsafe.Pointer(prgname))
}
go result.startSecondInstanceProcessor()
return result
}
func (f *Frontend) startMessageProcessor() {
for message := range messageBuffer {
f.processMessage(message)
}
}
func (f *Frontend) startBindingsMessageProcessor() {
for msg := range bindingsMessageBuffer {
origin, err := f.originValidator.GetOriginFromURL(msg.source)
if err != nil {
f.logger.Error(fmt.Sprintf("failed to get origin for URL %q: %v", msg.source, err))
continue
}
allowed := f.originValidator.IsOriginAllowed(origin)
if !allowed {
f.logger.Error("Blocked request from unauthorized origin: %s", origin)
continue
}
f.processMessage(msg.message)
}
}
func (f *Frontend) WindowReload() {
f.ExecJS("runtime.WindowReload();")
}
func (f *Frontend) WindowSetSystemDefaultTheme() {
return
}
func (f *Frontend) WindowSetLightTheme() {
return
}
func (f *Frontend) WindowSetDarkTheme() {
return
}
func (f *Frontend) Run(ctx context.Context) error {
f.ctx = ctx
go func() {
if f.frontendOptions.OnStartup != nil {
f.frontendOptions.OnStartup(f.ctx)
}
}()
if f.frontendOptions.SingleInstanceLock != nil {
SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId)
}
f.mainWindow.Run(f.startURL.String())
return nil
}
func (f *Frontend) WindowCenter() {
f.mainWindow.Center()
}
func (f *Frontend) WindowSetAlwaysOnTop(b bool) {
f.mainWindow.SetKeepAbove(b)
}
func (f *Frontend) WindowSetPosition(x, y int) {
f.mainWindow.SetPosition(x, y)
}
func (f *Frontend) WindowGetPosition() (int, int) {
return f.mainWindow.GetPosition()
}
func (f *Frontend) WindowSetSize(width, height int) {
f.mainWindow.SetSize(width, height)
}
func (f *Frontend) WindowGetSize() (int, int) {
return f.mainWindow.Size()
}
func (f *Frontend) WindowSetTitle(title string) {
f.mainWindow.SetTitle(title)
}
func (f *Frontend) WindowFullscreen() {
if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false {
f.ExecJS("window.wails.flags.enableResize = false;")
}
f.mainWindow.Fullscreen()
}
func (f *Frontend) WindowUnfullscreen() {
if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false {
f.ExecJS("window.wails.flags.enableResize = true;")
}
f.mainWindow.UnFullscreen()
}
func (f *Frontend) WindowReloadApp() {
f.ExecJS(fmt.Sprintf("window.location.href = '%s';", f.startURL))
}
func (f *Frontend) WindowShow() {
f.mainWindow.Show()
}
func (f *Frontend) WindowHide() {
f.mainWindow.Hide()
}
func (f *Frontend) Show() {
f.mainWindow.Show()
}
func (f *Frontend) Hide() {
f.mainWindow.Hide()
}
func (f *Frontend) WindowMaximise() {
f.mainWindow.Maximise()
}
func (f *Frontend) WindowToggleMaximise() {
f.mainWindow.ToggleMaximise()
}
func (f *Frontend) WindowUnmaximise() {
f.mainWindow.UnMaximise()
}
func (f *Frontend) WindowMinimise() {
f.mainWindow.Minimise()
}
func (f *Frontend) WindowUnminimise() {
f.mainWindow.UnMinimise()
}
func (f *Frontend) WindowSetMinSize(width int, height int) {
f.mainWindow.SetMinSize(width, height)
}
func (f *Frontend) WindowSetMaxSize(width int, height int) {
f.mainWindow.SetMaxSize(width, height)
}
func (f *Frontend) WindowSetBackgroundColour(col *options.RGBA) {
if col == nil {
return
}
f.mainWindow.SetBackgroundColour(col.R, col.G, col.B, col.A)
}
func (f *Frontend) ScreenGetAll() ([]Screen, error) {
return GetAllScreens(f.mainWindow.asGTKWindow())
}
func (f *Frontend) WindowIsMaximised() bool {
return f.mainWindow.IsMaximised()
}
func (f *Frontend) WindowIsMinimised() bool {
return f.mainWindow.IsMinimised()
}
func (f *Frontend) WindowIsNormal() bool {
return f.mainWindow.IsNormal()
}
func (f *Frontend) WindowIsFullscreen() bool {
return f.mainWindow.IsFullScreen()
}
func (f *Frontend) Quit() {
if f.frontendOptions.OnBeforeClose != nil {
go func() {
if !f.frontendOptions.OnBeforeClose(f.ctx) {
f.mainWindow.Quit()
}
}()
return
}
f.mainWindow.Quit()
}
func (f *Frontend) WindowPrint() {
f.ExecJS("window.print();")
}
type EventNotify struct {
Name string `json:"name"`
Data []interface{} `json:"data"`
}
func (f *Frontend) Notify(name string, data ...interface{}) {
notification := EventNotify{
Name: name,
Data: data,
}
payload, err := json.Marshal(notification)
if err != nil {
f.logger.Error(err.Error())
return
}
f.mainWindow.ExecJS(`window.wails.EventsNotify('` + template.JSEscapeString(string(payload)) + `');`)
}
var edgeMap = map[string]uintptr{
"n-resize": C.GDK_WINDOW_EDGE_NORTH,
"ne-resize": C.GDK_WINDOW_EDGE_NORTH_EAST,
"e-resize": C.GDK_WINDOW_EDGE_EAST,
"se-resize": C.GDK_WINDOW_EDGE_SOUTH_EAST,
"s-resize": C.GDK_WINDOW_EDGE_SOUTH,
"sw-resize": C.GDK_WINDOW_EDGE_SOUTH_WEST,
"w-resize": C.GDK_WINDOW_EDGE_WEST,
"nw-resize": C.GDK_WINDOW_EDGE_NORTH_WEST,
}
func (f *Frontend) processMessage(message string) {
if message == "DomReady" {
if f.frontendOptions.OnDomReady != nil {
f.frontendOptions.OnDomReady(f.ctx)
}
return
}
if message == "drag" {
if !f.mainWindow.IsFullScreen() {
f.startDrag()
}
return
}
if message == "wails:showInspector" {
f.mainWindow.ShowInspector()
return
}
if strings.HasPrefix(message, "resize:") {
if !f.mainWindow.IsFullScreen() {
sl := strings.Split(message, ":")
if len(sl) != 2 {
f.logger.Info("Unknown message returned from dispatcher: %+v", message)
return
}
edge := edgeMap[sl[1]]
err := f.startResize(edge)
if err != nil {
f.logger.Error(err.Error())
}
}
return
}
if message == "runtime:ready" {
cmd := fmt.Sprintf(
"window.wails.setCSSDragProperties('%s', '%s');\n"+
"window.wails.setCSSDropProperties('%s', '%s');\n"+
"window.wails.flags.deferDragToMouseMove = true;",
f.frontendOptions.CSSDragProperty,
f.frontendOptions.CSSDragValue,
f.frontendOptions.DragAndDrop.CSSDropProperty,
f.frontendOptions.DragAndDrop.CSSDropValue,
)
f.ExecJS(cmd)
if f.frontendOptions.Frameless && f.frontendOptions.DisableResize == false {
f.ExecJS("window.wails.flags.enableResize = true;")
}
if f.frontendOptions.DragAndDrop.EnableFileDrop {
f.ExecJS("window.wails.flags.enableWailsDragAndDrop = true;")
}
return
}
go func() {
result, err := f.dispatcher.ProcessMessage(message, f)
if err != nil {
f.logger.Error(err.Error())
f.Callback(result)
return
}
if result == "" {
return
}
switch result[0] {
case 'c':
// Callback from a method call
f.Callback(result[1:])
default:
f.logger.Info("Unknown message returned from dispatcher: %+v", result)
}
}()
}
func (f *Frontend) Callback(message string) {
escaped, err := json.Marshal(message)
if err != nil {
panic(err)
}
f.ExecJS(`window.wails.Callback(` + string(escaped) + `);`)
}
func (f *Frontend) startDrag() {
f.mainWindow.StartDrag()
}
func (f *Frontend) startResize(edge uintptr) error {
f.mainWindow.StartResize(edge)
return nil
}
func (f *Frontend) ExecJS(js string) {
f.mainWindow.ExecJS(js)
}
type bindingsMessage struct {
message string
source string
}
var messageBuffer = make(chan string, 100)
var bindingsMessageBuffer = make(chan *bindingsMessage, 100)
//export processMessage
func processMessage(message *C.char) {
goMessage := C.GoString(message)
messageBuffer <- goMessage
}
//export processBindingMessage
func processBindingMessage(message *C.char, source *C.char) {
goMessage := C.GoString(message)
goSource := C.GoString(source)
bindingsMessageBuffer <- &bindingsMessage{
message: goMessage,
source: goSource,
}
}
var requestBuffer = make(chan webview.Request, 100)
func (f *Frontend) startRequestProcessor() {
for request := range requestBuffer {
f.assets.ServeWebViewRequest(request)
}
}
//export processURLRequest
func processURLRequest(request unsafe.Pointer) {
requestBuffer <- webview.NewRequest(request)
}
func (f *Frontend) startSecondInstanceProcessor() {
for secondInstanceData := range secondInstanceBuffer {
if f.frontendOptions.SingleInstanceLock != nil &&
f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil {
f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData)
}
}
}

View File

@@ -0,0 +1,85 @@
//go:build linux
// +build linux
package linux
/*
#cgo linux pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
#include "gtk/gtk.h"
static GtkCheckMenuItem *toGtkCheckMenuItem(void *pointer) { return (GTK_CHECK_MENU_ITEM(pointer)); }
extern void blockClick(GtkWidget* menuItem, gulong handler_id);
extern void unblockClick(GtkWidget* menuItem, gulong handler_id);
*/
import "C"
import (
"unsafe"
"github.com/wailsapp/wails/v2/pkg/menu"
)
func GtkMenuItemWithLabel(label string) *C.GtkWidget {
cLabel := C.CString(label)
result := C.gtk_menu_item_new_with_label(cLabel)
C.free(unsafe.Pointer(cLabel))
return result
}
func GtkCheckMenuItemWithLabel(label string) *C.GtkWidget {
cLabel := C.CString(label)
result := C.gtk_check_menu_item_new_with_label(cLabel)
C.free(unsafe.Pointer(cLabel))
return result
}
func GtkRadioMenuItemWithLabel(label string, group *C.GSList) *C.GtkWidget {
cLabel := C.CString(label)
result := C.gtk_radio_menu_item_new_with_label(group, cLabel)
C.free(unsafe.Pointer(cLabel))
return result
}
//export handleMenuItemClick
func handleMenuItemClick(gtkWidget unsafe.Pointer) {
// Make sure to execute the final callback on a new goroutine otherwise if the callback e.g. tries to open a dialog, the
// main thread will get blocked and so the message loop blocks. As a result the app will block and shows a
// "not responding" dialog.
item := gtkSignalToMenuItem[(*C.GtkWidget)(gtkWidget)]
switch item.Type {
case menu.CheckboxType:
item.Checked = !item.Checked
checked := C.int(0)
if item.Checked {
checked = C.int(1)
}
for _, gtkCheckbox := range gtkCheckboxCache[item] {
handler := gtkSignalHandlers[gtkCheckbox]
C.blockClick(gtkCheckbox, handler)
C.gtk_check_menu_item_set_active(C.toGtkCheckMenuItem(unsafe.Pointer(gtkCheckbox)), checked)
C.unblockClick(gtkCheckbox, handler)
}
go item.Click(&menu.CallbackData{MenuItem: item})
case menu.RadioType:
gtkRadioItems := gtkRadioMenuCache[item]
active := C.gtk_check_menu_item_get_active(C.toGtkCheckMenuItem(gtkWidget))
if int(active) == 1 {
for _, gtkRadioItem := range gtkRadioItems {
handler := gtkSignalHandlers[gtkRadioItem]
C.blockClick(gtkRadioItem, handler)
C.gtk_check_menu_item_set_active(C.toGtkCheckMenuItem(unsafe.Pointer(gtkRadioItem)), 1)
C.unblockClick(gtkRadioItem, handler)
}
item.Checked = true
go item.Click(&menu.CallbackData{MenuItem: item})
} else {
item.Checked = false
}
default:
go item.Click(&menu.CallbackData{MenuItem: item})
}
}

View File

@@ -0,0 +1,78 @@
//go:build linux
// +build linux
package linux
/*
#cgo linux pkg-config: gtk+-3.0
#include <stdio.h>
#include "gtk/gtk.h"
extern gboolean invokeCallbacks(void *);
static inline void triggerInvokesOnMainThread() {
g_idle_add((GSourceFunc)invokeCallbacks, NULL);
}
*/
import "C"
import (
"runtime"
"sync"
"unsafe"
"golang.org/x/sys/unix"
)
var (
m sync.Mutex
mainTid int
dispatchq []func()
)
func invokeOnMainThread(f func()) {
if tryInvokeOnCurrentGoRoutine(f) {
return
}
m.Lock()
dispatchq = append(dispatchq, f)
m.Unlock()
C.triggerInvokesOnMainThread()
}
func tryInvokeOnCurrentGoRoutine(f func()) bool {
m.Lock()
mainThreadID := mainTid
m.Unlock()
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if mainThreadID != unix.Gettid() {
return false
}
f()
return true
}
//export invokeCallbacks
func invokeCallbacks(_ unsafe.Pointer) C.gboolean {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
m.Lock()
if mainTid == 0 {
mainTid = unix.Gettid()
}
q := append([]func(){}, dispatchq...)
dispatchq = []func(){}
m.Unlock()
for _, v := range q {
v()
}
return C.G_SOURCE_REMOVE
}

View File

@@ -0,0 +1,110 @@
//go:build linux
// +build linux
package linux
/*
#cgo linux pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
#include "gtk/gtk.h"
*/
import "C"
import (
"github.com/wailsapp/wails/v2/pkg/menu/keys"
)
var namedKeysToGTK = map[string]C.guint{
"backspace": C.guint(0xff08),
"tab": C.guint(0xff09),
"return": C.guint(0xff0d),
"enter": C.guint(0xff0d),
"escape": C.guint(0xff1b),
"left": C.guint(0xff51),
"right": C.guint(0xff53),
"up": C.guint(0xff52),
"down": C.guint(0xff54),
"space": C.guint(0xff80),
"delete": C.guint(0xff9f),
"home": C.guint(0xff95),
"end": C.guint(0xff9c),
"page up": C.guint(0xff9a),
"page down": C.guint(0xff9b),
"f1": C.guint(0xffbe),
"f2": C.guint(0xffbf),
"f3": C.guint(0xffc0),
"f4": C.guint(0xffc1),
"f5": C.guint(0xffc2),
"f6": C.guint(0xffc3),
"f7": C.guint(0xffc4),
"f8": C.guint(0xffc5),
"f9": C.guint(0xffc6),
"f10": C.guint(0xffc7),
"f11": C.guint(0xffc8),
"f12": C.guint(0xffc9),
"f13": C.guint(0xffca),
"f14": C.guint(0xffcb),
"f15": C.guint(0xffcc),
"f16": C.guint(0xffcd),
"f17": C.guint(0xffce),
"f18": C.guint(0xffcf),
"f19": C.guint(0xffd0),
"f20": C.guint(0xffd1),
"f21": C.guint(0xffd2),
"f22": C.guint(0xffd3),
"f23": C.guint(0xffd4),
"f24": C.guint(0xffd5),
"f25": C.guint(0xffd6),
"f26": C.guint(0xffd7),
"f27": C.guint(0xffd8),
"f28": C.guint(0xffd9),
"f29": C.guint(0xffda),
"f30": C.guint(0xffdb),
"f31": C.guint(0xffdc),
"f32": C.guint(0xffdd),
"f33": C.guint(0xffde),
"f34": C.guint(0xffdf),
"f35": C.guint(0xffe0),
"numlock": C.guint(0xff7f),
}
func acceleratorToGTK(accelerator *keys.Accelerator) (C.guint, C.GdkModifierType) {
key := parseKey(accelerator.Key)
mods := parseModifiers(accelerator.Modifiers)
return key, mods
}
func parseKey(key string) C.guint {
var result C.guint
result, found := namedKeysToGTK[key]
if found {
return result
}
// Check for unknown namedkeys
// Check if we only have a single character
if len(key) != 1 {
return C.guint(0)
}
keyval := rune(key[0])
return C.gdk_unicode_to_keyval(C.guint(keyval))
}
func parseModifiers(modifiers []keys.Modifier) C.GdkModifierType {
var result C.GdkModifierType
for _, modifier := range modifiers {
switch modifier {
case keys.ShiftKey:
result |= C.GDK_SHIFT_MASK
case keys.ControlKey, keys.CmdOrCtrlKey:
result |= C.GDK_CONTROL_MASK
case keys.OptionOrAltKey:
result |= C.GDK_MOD1_MASK
}
}
return result
}

View File

@@ -0,0 +1,169 @@
//go:build linux
// +build linux
package linux
/*
#cgo linux pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
#include "gtk/gtk.h"
static GtkMenuItem *toGtkMenuItem(void *pointer) { return (GTK_MENU_ITEM(pointer)); }
static GtkMenuShell *toGtkMenuShell(void *pointer) { return (GTK_MENU_SHELL(pointer)); }
static GtkCheckMenuItem *toGtkCheckMenuItem(void *pointer) { return (GTK_CHECK_MENU_ITEM(pointer)); }
static GtkRadioMenuItem *toGtkRadioMenuItem(void *pointer) { return (GTK_RADIO_MENU_ITEM(pointer)); }
extern void handleMenuItemClick(void*);
void blockClick(GtkWidget* menuItem, gulong handler_id) {
g_signal_handler_block (menuItem, handler_id);
}
void unblockClick(GtkWidget* menuItem, gulong handler_id) {
g_signal_handler_unblock (menuItem, handler_id);
}
gulong connectClick(GtkWidget* menuItem) {
return g_signal_connect(menuItem, "activate", G_CALLBACK(handleMenuItemClick), (void*)menuItem);
}
void addAccelerator(GtkWidget* menuItem, GtkAccelGroup* group, guint key, GdkModifierType mods) {
gtk_widget_add_accelerator(menuItem, "activate", group, key, mods, GTK_ACCEL_VISIBLE);
}
*/
import "C"
import (
"unsafe"
"github.com/wailsapp/wails/v2/pkg/menu"
)
var menuIdCounter int
var menuItemToId map[*menu.MenuItem]int
var menuIdToItem map[int]*menu.MenuItem
var gtkCheckboxCache map[*menu.MenuItem][]*C.GtkWidget
var gtkMenuCache map[*menu.MenuItem]*C.GtkWidget
var gtkRadioMenuCache map[*menu.MenuItem][]*C.GtkWidget
var gtkSignalHandlers map[*C.GtkWidget]C.gulong
var gtkSignalToMenuItem map[*C.GtkWidget]*menu.MenuItem
func (f *Frontend) MenuSetApplicationMenu(menu *menu.Menu) {
f.mainWindow.SetApplicationMenu(menu)
}
func (f *Frontend) MenuUpdateApplicationMenu() {
f.mainWindow.SetApplicationMenu(f.mainWindow.applicationMenu)
}
func (w *Window) SetApplicationMenu(inmenu *menu.Menu) {
if inmenu == nil {
return
}
// Setup accelerator group
w.accels = C.gtk_accel_group_new()
C.gtk_window_add_accel_group(w.asGTKWindow(), w.accels)
menuItemToId = make(map[*menu.MenuItem]int)
menuIdToItem = make(map[int]*menu.MenuItem)
gtkCheckboxCache = make(map[*menu.MenuItem][]*C.GtkWidget)
gtkMenuCache = make(map[*menu.MenuItem]*C.GtkWidget)
gtkRadioMenuCache = make(map[*menu.MenuItem][]*C.GtkWidget)
gtkSignalHandlers = make(map[*C.GtkWidget]C.gulong)
gtkSignalToMenuItem = make(map[*C.GtkWidget]*menu.MenuItem)
// Increase ref count?
w.menubar = C.gtk_menu_bar_new()
processMenu(w, inmenu)
C.gtk_widget_show(w.menubar)
}
func processMenu(window *Window, menu *menu.Menu) {
for _, menuItem := range menu.Items {
if menuItem.SubMenu != nil {
submenu := processSubmenu(menuItem, window.accels)
C.gtk_menu_shell_append(C.toGtkMenuShell(unsafe.Pointer(window.menubar)), submenu)
}
}
}
func processSubmenu(menuItem *menu.MenuItem, group *C.GtkAccelGroup) *C.GtkWidget {
existingMenu := gtkMenuCache[menuItem]
if existingMenu != nil {
return existingMenu
}
gtkMenu := C.gtk_menu_new()
submenu := GtkMenuItemWithLabel(menuItem.Label)
for _, menuItem := range menuItem.SubMenu.Items {
menuID := menuIdCounter
menuIdToItem[menuID] = menuItem
menuItemToId[menuItem] = menuID
menuIdCounter++
processMenuItem(gtkMenu, menuItem, group)
}
C.gtk_menu_item_set_submenu(C.toGtkMenuItem(unsafe.Pointer(submenu)), gtkMenu)
gtkMenuCache[menuItem] = existingMenu
return submenu
}
var currentRadioGroup *C.GSList
func processMenuItem(parent *C.GtkWidget, menuItem *menu.MenuItem, group *C.GtkAccelGroup) {
if menuItem.Hidden {
return
}
if menuItem.Type != menu.RadioType {
currentRadioGroup = nil
}
if menuItem.Type == menu.SeparatorType {
result := C.gtk_separator_menu_item_new()
C.gtk_menu_shell_append(C.toGtkMenuShell(unsafe.Pointer(parent)), result)
return
}
var result *C.GtkWidget
switch menuItem.Type {
case menu.TextType:
result = GtkMenuItemWithLabel(menuItem.Label)
case menu.CheckboxType:
result = GtkCheckMenuItemWithLabel(menuItem.Label)
if menuItem.Checked {
C.gtk_check_menu_item_set_active(C.toGtkCheckMenuItem(unsafe.Pointer(result)), 1)
}
gtkCheckboxCache[menuItem] = append(gtkCheckboxCache[menuItem], result)
case menu.RadioType:
result = GtkRadioMenuItemWithLabel(menuItem.Label, currentRadioGroup)
currentRadioGroup = C.gtk_radio_menu_item_get_group(C.toGtkRadioMenuItem(unsafe.Pointer(result)))
if menuItem.Checked {
C.gtk_check_menu_item_set_active(C.toGtkCheckMenuItem(unsafe.Pointer(result)), 1)
}
gtkRadioMenuCache[menuItem] = append(gtkRadioMenuCache[menuItem], result)
case menu.SubmenuType:
result = processSubmenu(menuItem, group)
}
C.gtk_menu_shell_append(C.toGtkMenuShell(unsafe.Pointer(parent)), result)
C.gtk_widget_show(result)
if menuItem.Click != nil {
handler := C.connectClick(result)
gtkSignalHandlers[result] = handler
gtkSignalToMenuItem[result] = menuItem
}
if menuItem.Disabled {
C.gtk_widget_set_sensitive(result, 0)
}
if menuItem.Accelerator != nil {
key, mods := acceleratorToGTK(menuItem.Accelerator)
C.addAccelerator(result, group, key, mods)
}
}

View File

@@ -0,0 +1,594 @@
//go:build linux
// +build linux
package linux
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/godbus/dbus/v5"
"github.com/wailsapp/wails/v2/internal/frontend"
)
var (
conn *dbus.Conn
categories map[string]frontend.NotificationCategory = make(map[string]frontend.NotificationCategory)
categoriesLock sync.RWMutex
notifications map[uint32]*notificationData = make(map[uint32]*notificationData)
notificationsLock sync.RWMutex
notificationResultCallback func(result frontend.NotificationResult)
callbackLock sync.RWMutex
appName string
cancel context.CancelFunc
)
type notificationData struct {
ID string
Title string
Subtitle string
Body string
CategoryID string
Data map[string]interface{}
DBusID uint32
ActionMap map[string]string
}
const (
dbusNotificationInterface = "org.freedesktop.Notifications"
dbusNotificationPath = "/org/freedesktop/Notifications"
DefaultActionIdentifier = "DEFAULT_ACTION"
)
// Creates a new Notifications Service.
func (f *Frontend) InitializeNotifications() error {
// Clean up any previous initialization
f.CleanupNotifications()
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable: %w", err)
}
appName = filepath.Base(exe)
_conn, err := dbus.ConnectSessionBus()
if err != nil {
return fmt.Errorf("failed to connect to session bus: %w", err)
}
conn = _conn
if err := f.loadCategories(); err != nil {
f.logger.Warning("Failed to load notification categories: %v", err)
}
var signalCtx context.Context
signalCtx, cancel = context.WithCancel(context.Background())
if err := f.setupSignalHandling(signalCtx); err != nil {
return fmt.Errorf("failed to set up notification signal handling: %w", err)
}
return nil
}
// CleanupNotifications cleans up notification resources
func (f *Frontend) CleanupNotifications() {
if cancel != nil {
cancel()
cancel = nil
}
if conn != nil {
conn.Close()
conn = nil
}
}
func (f *Frontend) IsNotificationAvailable() bool {
return true
}
// RequestNotificationAuthorization is a Linux stub that always returns true, nil.
// (authorization is macOS-specific)
func (f *Frontend) RequestNotificationAuthorization() (bool, error) {
return true, nil
}
// CheckNotificationAuthorization is a Linux stub that always returns true.
// (authorization is macOS-specific)
func (f *Frontend) CheckNotificationAuthorization() (bool, error) {
return true, nil
}
// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
func (f *Frontend) SendNotification(options frontend.NotificationOptions) error {
if conn == nil {
return fmt.Errorf("notifications not initialized")
}
hints := map[string]dbus.Variant{}
body := options.Body
if options.Subtitle != "" {
body = options.Subtitle + "\n" + body
}
defaultActionID := "default"
actions := []string{defaultActionID, "Default"}
actionMap := map[string]string{
defaultActionID: DefaultActionIdentifier,
}
hints["x-notification-id"] = dbus.MakeVariant(options.ID)
if options.Data != nil {
userData, err := json.Marshal(options.Data)
if err == nil {
hints["x-user-data"] = dbus.MakeVariant(string(userData))
}
}
// Call the Notify method on the D-Bus interface
obj := conn.Object(dbusNotificationInterface, dbusNotificationPath)
call := obj.Call(
dbusNotificationInterface+".Notify",
0,
appName,
uint32(0),
"", // Icon
options.Title,
body,
actions,
hints,
int32(-1),
)
if call.Err != nil {
return fmt.Errorf("failed to send notification: %w", call.Err)
}
var dbusID uint32
if err := call.Store(&dbusID); err != nil {
return fmt.Errorf("failed to store notification ID: %w", err)
}
notification := &notificationData{
ID: options.ID,
Title: options.Title,
Subtitle: options.Subtitle,
Body: options.Body,
Data: options.Data,
DBusID: dbusID,
ActionMap: actionMap,
}
notificationsLock.Lock()
notifications[dbusID] = notification
notificationsLock.Unlock()
return nil
}
// SendNotificationWithActions sends a notification with additional actions.
func (f *Frontend) SendNotificationWithActions(options frontend.NotificationOptions) error {
if conn == nil {
return fmt.Errorf("notifications not initialized")
}
categoriesLock.RLock()
category, exists := categories[options.CategoryID]
categoriesLock.RUnlock()
if options.CategoryID == "" || !exists {
// Fall back to basic notification
return f.SendNotification(options)
}
body := options.Body
if options.Subtitle != "" {
body = options.Subtitle + "\n" + body
}
var actions []string
actionMap := make(map[string]string)
defaultActionID := "default"
actions = append(actions, defaultActionID, "Default")
actionMap[defaultActionID] = DefaultActionIdentifier
for _, action := range category.Actions {
actions = append(actions, action.ID, action.Title)
actionMap[action.ID] = action.ID
}
hints := map[string]dbus.Variant{}
hints["x-notification-id"] = dbus.MakeVariant(options.ID)
hints["x-category-id"] = dbus.MakeVariant(options.CategoryID)
if options.Data != nil {
userData, err := json.Marshal(options.Data)
if err == nil {
hints["x-user-data"] = dbus.MakeVariant(string(userData))
}
}
obj := conn.Object(dbusNotificationInterface, dbusNotificationPath)
call := obj.Call(
dbusNotificationInterface+".Notify",
0,
appName,
uint32(0),
"", // Icon
options.Title,
body,
actions,
hints,
int32(-1),
)
if call.Err != nil {
return fmt.Errorf("failed to send notification: %w", call.Err)
}
var dbusID uint32
if err := call.Store(&dbusID); err != nil {
return fmt.Errorf("failed to store notification ID: %w", err)
}
notification := &notificationData{
ID: options.ID,
Title: options.Title,
Subtitle: options.Subtitle,
Body: options.Body,
CategoryID: options.CategoryID,
Data: options.Data,
DBusID: dbusID,
ActionMap: actionMap,
}
notificationsLock.Lock()
notifications[dbusID] = notification
notificationsLock.Unlock()
return nil
}
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
func (f *Frontend) RegisterNotificationCategory(category frontend.NotificationCategory) error {
categoriesLock.Lock()
categories[category.ID] = category
categoriesLock.Unlock()
if err := f.saveCategories(); err != nil {
f.logger.Warning("Failed to save notification categories: %v", err)
}
return nil
}
// RemoveNotificationCategory removes a previously registered NotificationCategory.
func (f *Frontend) RemoveNotificationCategory(categoryId string) error {
categoriesLock.Lock()
delete(categories, categoryId)
categoriesLock.Unlock()
if err := f.saveCategories(); err != nil {
f.logger.Warning("Failed to save notification categories: %v", err)
}
return nil
}
// RemoveAllPendingNotifications attempts to remove all active notifications.
func (f *Frontend) RemoveAllPendingNotifications() error {
notificationsLock.Lock()
dbusIDs := make([]uint32, 0, len(notifications))
for id := range notifications {
dbusIDs = append(dbusIDs, id)
}
notificationsLock.Unlock()
for _, id := range dbusIDs {
f.closeNotification(id)
}
return nil
}
// RemovePendingNotification removes a pending notification.
func (f *Frontend) RemovePendingNotification(identifier string) error {
var dbusID uint32
found := false
notificationsLock.Lock()
for id, notif := range notifications {
if notif.ID == identifier {
dbusID = id
found = true
break
}
}
notificationsLock.Unlock()
if !found {
return nil
}
return f.closeNotification(dbusID)
}
// RemoveAllDeliveredNotifications functionally equivalent to RemoveAllPendingNotification on Linux.
func (f *Frontend) RemoveAllDeliveredNotifications() error {
return f.RemoveAllPendingNotifications()
}
// RemoveDeliveredNotification functionally equivalent RemovePendingNotification on Linux.
func (f *Frontend) RemoveDeliveredNotification(identifier string) error {
return f.RemovePendingNotification(identifier)
}
// RemoveNotification removes a notification by identifier.
func (f *Frontend) RemoveNotification(identifier string) error {
return f.RemovePendingNotification(identifier)
}
func (f *Frontend) OnNotificationResponse(callback func(result frontend.NotificationResult)) {
callbackLock.Lock()
defer callbackLock.Unlock()
notificationResultCallback = callback
}
// Helper method to close a notification.
func (f *Frontend) closeNotification(id uint32) error {
if conn == nil {
return fmt.Errorf("notifications not initialized")
}
obj := conn.Object(dbusNotificationInterface, dbusNotificationPath)
call := obj.Call(dbusNotificationInterface+".CloseNotification", 0, id)
if call.Err != nil {
return fmt.Errorf("failed to close notification: %w", call.Err)
}
return nil
}
func (f *Frontend) getConfigDir() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("failed to get user config directory: %w", err)
}
appConfigDir := filepath.Join(configDir, appName)
if err := os.MkdirAll(appConfigDir, 0755); err != nil {
return "", fmt.Errorf("failed to create app config directory: %w", err)
}
return appConfigDir, nil
}
// Save notification categories.
func (f *Frontend) saveCategories() error {
configDir, err := f.getConfigDir()
if err != nil {
return err
}
categoriesFile := filepath.Join(configDir, "notification-categories.json")
categoriesLock.RLock()
categoriesData, err := json.MarshalIndent(categories, "", " ")
categoriesLock.RUnlock()
if err != nil {
return fmt.Errorf("failed to marshal notification categories: %w", err)
}
if err := os.WriteFile(categoriesFile, categoriesData, 0644); err != nil {
return fmt.Errorf("failed to write notification categories to disk: %w", err)
}
return nil
}
// Load notification categories.
func (f *Frontend) loadCategories() error {
configDir, err := f.getConfigDir()
if err != nil {
return err
}
categoriesFile := filepath.Join(configDir, "notification-categories.json")
if _, err := os.Stat(categoriesFile); os.IsNotExist(err) {
return nil
}
categoriesData, err := os.ReadFile(categoriesFile)
if err != nil {
return fmt.Errorf("failed to read notification categories from disk: %w", err)
}
_categories := make(map[string]frontend.NotificationCategory)
if err := json.Unmarshal(categoriesData, &_categories); err != nil {
return fmt.Errorf("failed to unmarshal notification categories: %w", err)
}
categoriesLock.Lock()
categories = _categories
categoriesLock.Unlock()
return nil
}
// Setup signal handling for notification actions.
func (f *Frontend) setupSignalHandling(ctx context.Context) error {
if err := conn.AddMatchSignal(
dbus.WithMatchInterface(dbusNotificationInterface),
dbus.WithMatchMember("ActionInvoked"),
); err != nil {
return err
}
if err := conn.AddMatchSignal(
dbus.WithMatchInterface(dbusNotificationInterface),
dbus.WithMatchMember("NotificationClosed"),
); err != nil {
return err
}
c := make(chan *dbus.Signal, 10)
conn.Signal(c)
go f.handleSignals(ctx, c)
return nil
}
// Handle incoming D-Bus signals.
func (f *Frontend) handleSignals(ctx context.Context, c chan *dbus.Signal) {
for {
select {
case <-ctx.Done():
return
case signal, ok := <-c:
if !ok {
return
}
switch signal.Name {
case dbusNotificationInterface + ".ActionInvoked":
f.handleActionInvoked(signal)
case dbusNotificationInterface + ".NotificationClosed":
f.handleNotificationClosed(signal)
}
}
}
}
// Handle ActionInvoked signal.
func (f *Frontend) handleActionInvoked(signal *dbus.Signal) {
if len(signal.Body) < 2 {
return
}
dbusID, ok := signal.Body[0].(uint32)
if !ok {
return
}
actionID, ok := signal.Body[1].(string)
if !ok {
return
}
notificationsLock.Lock()
notification, exists := notifications[dbusID]
if exists {
delete(notifications, dbusID)
}
notificationsLock.Unlock()
if !exists {
return
}
appActionID, ok := notification.ActionMap[actionID]
if !ok {
appActionID = actionID
}
response := frontend.NotificationResponse{
ID: notification.ID,
ActionIdentifier: appActionID,
Title: notification.Title,
Subtitle: notification.Subtitle,
Body: notification.Body,
CategoryID: notification.CategoryID,
UserInfo: notification.Data,
}
result := frontend.NotificationResult{
Response: response,
}
handleNotificationResult(result)
}
func handleNotificationResult(result frontend.NotificationResult) {
callbackLock.Lock()
callback := notificationResultCallback
callbackLock.Unlock()
if callback != nil {
go func() {
defer func() {
if r := recover(); r != nil {
// Log panic but don't crash the app
fmt.Fprintf(os.Stderr, "panic in notification callback: %v\n", r)
}
}()
callback(result)
}()
}
}
// Handle NotificationClosed signal.
// Reason codes:
// 1 - expired timeout
// 2 - dismissed by user (click on X)
// 3 - closed by CloseNotification call
// 4 - undefined/reserved
func (f *Frontend) handleNotificationClosed(signal *dbus.Signal) {
if len(signal.Body) < 2 {
return
}
dbusID, ok := signal.Body[0].(uint32)
if !ok {
return
}
reason, ok := signal.Body[1].(uint32)
if !ok {
reason = 0 // Unknown reason
}
notificationsLock.Lock()
notification, exists := notifications[dbusID]
if exists {
delete(notifications, dbusID)
}
notificationsLock.Unlock()
if !exists {
return
}
if reason == 2 {
response := frontend.NotificationResponse{
ID: notification.ID,
ActionIdentifier: DefaultActionIdentifier,
Title: notification.Title,
Subtitle: notification.Subtitle,
Body: notification.Body,
CategoryID: notification.CategoryID,
UserInfo: notification.Data,
}
result := frontend.NotificationResult{
Response: response,
}
handleNotificationResult(result)
}
}

View File

@@ -0,0 +1,91 @@
//go:build linux
// +build linux
package linux
/*
#cgo linux pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
#cgo CFLAGS: -w
#include <stdio.h>
#include "webkit2/webkit2.h"
#include "gtk/gtk.h"
#include "gdk/gdk.h"
typedef struct Screen {
int isCurrent;
int isPrimary;
int height;
int width;
int scale;
} Screen;
int GetNMonitors(GtkWindow *window){
GdkWindow *gdk_window = gtk_widget_get_window(GTK_WIDGET(window));
GdkDisplay *display = gdk_window_get_display(gdk_window);
return gdk_display_get_n_monitors(display);
}
Screen GetNThMonitor(int monitor_num, GtkWindow *window){
GdkWindow *gdk_window = gtk_widget_get_window(GTK_WIDGET(window));
GdkDisplay *display = gdk_window_get_display(gdk_window);
GdkMonitor *monitor = gdk_display_get_monitor(display,monitor_num);
GdkMonitor *currentMonitor = gdk_display_get_monitor_at_window(display,gdk_window);
Screen screen;
GdkRectangle geometry;
gdk_monitor_get_geometry(monitor,&geometry);
screen.isCurrent = currentMonitor==monitor;
screen.isPrimary = gdk_monitor_is_primary(monitor);
screen.height = geometry.height;
screen.width = geometry.width;
screen.scale = gdk_monitor_get_scale_factor(monitor);
return screen;
}
*/
import "C"
import (
"sync"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/internal/frontend"
)
type Screen = frontend.Screen
func GetAllScreens(window *C.GtkWindow) ([]Screen, error) {
if window == nil {
return nil, errors.New("window is nil, cannot perform screen operations")
}
var wg sync.WaitGroup
var screens []Screen
wg.Add(1)
invokeOnMainThread(func() {
numMonitors := C.GetNMonitors(window)
for i := 0; i < int(numMonitors); i++ {
cMonitor := C.GetNThMonitor(C.int(i), window)
screen := Screen{
IsCurrent: cMonitor.isCurrent == 1,
IsPrimary: cMonitor.isPrimary == 1,
Width: int(cMonitor.width),
Height: int(cMonitor.height),
Size: frontend.ScreenSize{
Width: int(cMonitor.width),
Height: int(cMonitor.height),
},
PhysicalSize: frontend.ScreenSize{
Width: int(cMonitor.width * cMonitor.scale),
Height: int(cMonitor.height * cMonitor.scale),
},
}
screens = append(screens, screen)
}
wg.Done()
})
wg.Wait()
return screens, nil
}

View File

@@ -0,0 +1,77 @@
//go:build linux
// +build linux
package linux
import (
"encoding/json"
"github.com/godbus/dbus/v5"
"github.com/wailsapp/wails/v2/pkg/options"
"log"
"os"
"strings"
)
type dbusHandler func(string)
func (f dbusHandler) SendMessage(message string) *dbus.Error {
f(message)
return nil
}
func SetupSingleInstance(uniqueID string) {
id := "wails_app_" + strings.ReplaceAll(strings.ReplaceAll(uniqueID, "-", "_"), ".", "_")
dbusName := "org." + id + ".SingleInstance"
dbusPath := "/org/" + id + "/SingleInstance"
conn, err := dbus.ConnectSessionBus()
// if we will reach any error during establishing connection or sending message we will just continue.
// It should not be the case that such thing will happen actually, but just in case.
if err != nil {
return
}
f := dbusHandler(func(message string) {
var secondInstanceData options.SecondInstanceData
err := json.Unmarshal([]byte(message), &secondInstanceData)
if err == nil {
secondInstanceBuffer <- secondInstanceData
}
})
err = conn.Export(f, dbus.ObjectPath(dbusPath), dbusName)
if err != nil {
return
}
reply, err := conn.RequestName(dbusName, dbus.NameFlagDoNotQueue)
if err != nil {
return
}
// if name already taken, try to send args to existing instance, if no success just launch new instance
if reply == dbus.RequestNameReplyExists {
data := options.SecondInstanceData{
Args: os.Args[1:],
}
data.WorkingDirectory, err = os.Getwd()
if err != nil {
log.Printf("Failed to get working directory: %v", err)
return
}
serialized, err := json.Marshal(data)
if err != nil {
log.Printf("Failed to marshal data: %v", err)
return
}
err = conn.Object(dbusName, dbus.ObjectPath(dbusPath)).Call(dbusName+".SendMessage", 0, string(serialized)).Store()
if err != nil {
return
}
os.Exit(1)
}
}

View File

@@ -0,0 +1,32 @@
//go:build linux
package linux
/*
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
#include "webkit2/webkit2.h"
*/
import "C"
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/linux"
"github.com/wailsapp/wails/v2/pkg/assetserver/webview"
)
func validateWebKit2Version(options *options.App) {
if C.webkit_get_major_version() == 2 && C.webkit_get_minor_version() >= webview.Webkit2MinMinorVersion {
return
}
msg := linux.DefaultMessages()
if options.Linux != nil && options.Linux.Messages != nil {
msg = options.Linux.Messages
}
v := fmt.Sprintf("2.%d.0", webview.Webkit2MinMinorVersion)
showModalDialogAndExit("WebKit2GTK", fmt.Sprintf(msg.WebKit2GTKMinRequired, v))
}

View File

@@ -0,0 +1,891 @@
#include <JavaScriptCore/JavaScript.h>
#include <gtk/gtk.h>
#include <webkit2/webkit2.h>
#include <stdio.h>
#include <limits.h>
#include <stdint.h>
#include <string.h>
#include <locale.h>
#include "window.h"
// These are the x,y,time & button of the last mouse down event
// It's used for window dragging
static float xroot = 0.0f;
static float yroot = 0.0f;
static int dragTime = -1;
static uint mouseButton = 0;
static int wmIsWayland = -1;
static int decoratorWidth = -1;
static int decoratorHeight = -1;
// casts
void ExecuteOnMainThread(void *f, gpointer jscallback)
{
g_idle_add((GSourceFunc)f, (gpointer)jscallback);
}
GtkWidget *GTKWIDGET(void *pointer)
{
return GTK_WIDGET(pointer);
}
GtkWindow *GTKWINDOW(void *pointer)
{
return GTK_WINDOW(pointer);
}
GtkContainer *GTKCONTAINER(void *pointer)
{
return GTK_CONTAINER(pointer);
}
GtkBox *GTKBOX(void *pointer)
{
return GTK_BOX(pointer);
}
extern void processMessage(char *);
extern void processBindingMessage(char *, char *);
static void sendMessageToBackend(WebKitUserContentManager *contentManager,
WebKitJavascriptResult *result,
void *data)
{
// Retrieve webview from content manager
WebKitWebView *webview = WEBKIT_WEB_VIEW(g_object_get_data(G_OBJECT(contentManager), "webview"));
const char *current_uri = webview ? webkit_web_view_get_uri(webview) : NULL;
char *uri = current_uri ? g_strdup(current_uri) : NULL;
#if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22
JSCValue *value = webkit_javascript_result_get_js_value(result);
char *message = jsc_value_to_string(value);
#else
JSGlobalContextRef context = webkit_javascript_result_get_global_context(result);
JSValueRef value = webkit_javascript_result_get_value(result);
JSStringRef js = JSValueToStringCopy(context, value, NULL);
size_t messageSize = JSStringGetMaximumUTF8CStringSize(js);
char *message = g_new(char, messageSize);
JSStringGetUTF8CString(js, message, messageSize);
JSStringRelease(js);
#endif
processBindingMessage(message, uri);
g_free(message);
if (uri) {
g_free(uri);
}
}
static bool isNULLRectangle(GdkRectangle input)
{
return input.x == -1 && input.y == -1 && input.width == -1 && input.height == -1;
}
static gboolean onWayland()
{
switch (wmIsWayland)
{
case -1:
{
char *gdkBackend = getenv("XDG_SESSION_TYPE");
if(gdkBackend != NULL && strcmp(gdkBackend, "wayland") == 0)
{
wmIsWayland = 1;
return TRUE;
}
wmIsWayland = 0;
return FALSE;
}
case 1:
return TRUE;
default:
return FALSE;
}
}
static GdkMonitor *getCurrentMonitor(GtkWindow *window)
{
// Get the monitor that the window is currently on
GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(window));
GdkWindow *gdk_window = gtk_widget_get_window(GTK_WIDGET(window));
if (gdk_window == NULL)
{
return NULL;
}
GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, gdk_window);
return GDK_MONITOR(monitor);
}
static GdkRectangle getCurrentMonitorGeometry(GtkWindow *window)
{
GdkMonitor *monitor = getCurrentMonitor(window);
GdkRectangle result;
if (monitor == NULL)
{
result.x = result.y = result.height = result.width = -1;
return result;
}
// Get the geometry of the monitor
gdk_monitor_get_geometry(monitor, &result);
return result;
}
static int getCurrentMonitorScaleFactor(GtkWindow *window)
{
GdkMonitor *monitor = getCurrentMonitor(window);
return gdk_monitor_get_scale_factor(monitor);
}
// window
ulong SetupInvokeSignal(void *contentManager)
{
return g_signal_connect((WebKitUserContentManager *)contentManager, "script-message-received::external", G_CALLBACK(sendMessageToBackend), NULL);
}
void SetWindowIcon(GtkWindow *window, const guchar *buf, gsize len)
{
GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
if (!loader)
{
return;
}
if (gdk_pixbuf_loader_write(loader, buf, len, NULL) && gdk_pixbuf_loader_close(loader, NULL))
{
GdkPixbuf *pixbuf = gdk_pixbuf_loader_get_pixbuf(loader);
if (pixbuf)
{
gtk_window_set_icon(window, pixbuf);
}
}
g_object_unref(loader);
}
void SetWindowTransparency(GtkWidget *widget)
{
GdkScreen *screen = gtk_widget_get_screen(widget);
GdkVisual *visual = gdk_screen_get_rgba_visual(screen);
if (visual != NULL && gdk_screen_is_composited(screen))
{
gtk_widget_set_app_paintable(widget, true);
gtk_widget_set_visual(widget, visual);
}
}
static GtkCssProvider *windowCssProvider = NULL;
void SetBackgroundColour(void *data)
{
// set webview's background color
RGBAOptions *options = (RGBAOptions *)data;
GdkRGBA colour = {options->r / 255.0, options->g / 255.0, options->b / 255.0, options->a / 255.0};
if (options->windowIsTranslucent != NULL && options->windowIsTranslucent == TRUE)
{
colour.alpha = 0.0;
}
webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(options->webview), &colour);
// set window's background color
// Get the name of the current locale
char *old_locale, *saved_locale;
old_locale = setlocale(LC_ALL, NULL);
// Copy the name so it wont be clobbered by setlocale.
saved_locale = strdup(old_locale);
if (saved_locale == NULL)
return;
//Now change the locale to english for so printf always converts floats with a dot decimal separator
setlocale(LC_ALL, "en_US.UTF-8");
gchar *str = g_strdup_printf("#webview-box {background-color: rgba(%d, %d, %d, %1.1f);}", options->r, options->g, options->b, options->a / 255.0);
//Restore the original locale.
setlocale(LC_ALL, saved_locale);
free(saved_locale);
if (windowCssProvider == NULL)
{
windowCssProvider = gtk_css_provider_new();
gtk_style_context_add_provider(
gtk_widget_get_style_context(GTK_WIDGET(options->webviewBox)),
GTK_STYLE_PROVIDER(windowCssProvider),
GTK_STYLE_PROVIDER_PRIORITY_USER);
g_object_unref(windowCssProvider);
}
gtk_css_provider_load_from_data(windowCssProvider, str, -1, NULL);
g_free(str);
}
static gboolean setTitle(gpointer data)
{
SetTitleArgs *args = (SetTitleArgs *)data;
gtk_window_set_title(args->window, args->title);
free((void *)args->title);
free((void *)data);
return G_SOURCE_REMOVE;
}
void SetTitle(GtkWindow *window, char *title)
{
SetTitleArgs *args = malloc(sizeof(SetTitleArgs));
args->window = window;
args->title = title;
ExecuteOnMainThread(setTitle, (gpointer)args);
}
static gboolean setPosition(gpointer data)
{
SetPositionArgs *args = (SetPositionArgs *)data;
gtk_window_move((GtkWindow *)args->window, args->x, args->y);
free(args);
return G_SOURCE_REMOVE;
}
void SetPosition(void *window, int x, int y)
{
GdkRectangle monitorDimensions = getCurrentMonitorGeometry(window);
if (isNULLRectangle(monitorDimensions))
{
return;
}
SetPositionArgs *args = malloc(sizeof(SetPositionArgs));
args->window = window;
args->x = monitorDimensions.x + x;
args->y = monitorDimensions.y + y;
ExecuteOnMainThread(setPosition, (gpointer)args);
}
void SetMinMaxSize(GtkWindow *window, int min_width, int min_height, int max_width, int max_height)
{
GdkGeometry size;
size.min_width = size.min_height = size.max_width = size.max_height = 0;
GdkRectangle monitorSize = getCurrentMonitorGeometry(window);
if (isNULLRectangle(monitorSize))
{
return;
}
int flags = GDK_HINT_MAX_SIZE | GDK_HINT_MIN_SIZE;
size.max_height = (max_height == 0 ? monitorSize.height : max_height);
size.max_width = (max_width == 0 ? monitorSize.width : max_width);
size.min_height = min_height;
size.min_width = min_width;
// On Wayland window manager get the decorators and calculate the differences from the windows' size.
if(onWayland())
{
if(decoratorWidth == -1 && decoratorHeight == -1)
{
int windowWidth, windowHeight;
gtk_window_get_size(window, &windowWidth, &windowHeight);
GtkAllocation windowAllocation;
gtk_widget_get_allocation(GTK_WIDGET(window), &windowAllocation);
decoratorWidth = (windowAllocation.width-windowWidth);
decoratorHeight = (windowAllocation.height-windowHeight);
}
// Add the decorator difference to the window so fullscreen and maximise can fill the window.
size.max_height = decoratorHeight+size.max_height;
size.max_width = decoratorWidth+size.max_width;
}
gtk_window_set_geometry_hints(window, NULL, &size, flags);
}
// function to disable the context menu but propagate the event
static gboolean disableContextMenu(GtkWidget *widget, WebKitContextMenu *context_menu, GdkEvent *event, WebKitHitTestResult *hit_test_result, gpointer data)
{
// return true to disable the context menu
return TRUE;
}
void DisableContextMenu(void *webview)
{
// Disable the context menu but propagate the event
g_signal_connect(WEBKIT_WEB_VIEW(webview), "context-menu", G_CALLBACK(disableContextMenu), NULL);
}
static gboolean buttonPress(GtkWidget *widget, GdkEventButton *event, void *dummy)
{
if (event == NULL)
{
xroot = yroot = 0.0f;
dragTime = -1;
return FALSE;
}
mouseButton = event->button;
if (event->button == 3)
{
return FALSE;
}
if (event->type == GDK_BUTTON_PRESS && event->button == 1)
{
xroot = event->x_root;
yroot = event->y_root;
dragTime = event->time;
}
return FALSE;
}
static gboolean buttonRelease(GtkWidget *widget, GdkEventButton *event, void *dummy)
{
if (event == NULL || (event->type == GDK_BUTTON_RELEASE && event->button == 1))
{
xroot = yroot = 0.0f;
dragTime = -1;
}
return FALSE;
}
void ConnectButtons(void *webview)
{
g_signal_connect(WEBKIT_WEB_VIEW(webview), "button-press-event", G_CALLBACK(buttonPress), NULL);
g_signal_connect(WEBKIT_WEB_VIEW(webview), "button-release-event", G_CALLBACK(buttonRelease), NULL);
}
int IsFullscreen(GtkWidget *widget)
{
GdkWindow *gdkwindow = gtk_widget_get_window(widget);
GdkWindowState state = gdk_window_get_state(GDK_WINDOW(gdkwindow));
return state & GDK_WINDOW_STATE_FULLSCREEN;
}
int IsMaximised(GtkWidget *widget)
{
GdkWindow *gdkwindow = gtk_widget_get_window(widget);
GdkWindowState state = gdk_window_get_state(GDK_WINDOW(gdkwindow));
return state & GDK_WINDOW_STATE_MAXIMIZED && !(state & GDK_WINDOW_STATE_FULLSCREEN);
}
int IsMinimised(GtkWidget *widget)
{
GdkWindow *gdkwindow = gtk_widget_get_window(widget);
GdkWindowState state = gdk_window_get_state(GDK_WINDOW(gdkwindow));
return state & GDK_WINDOW_STATE_ICONIFIED;
}
gboolean Center(gpointer data)
{
GtkWindow *window = (GtkWindow *)data;
// Get the geometry of the monitor
GdkRectangle m = getCurrentMonitorGeometry(window);
if (isNULLRectangle(m))
{
return G_SOURCE_REMOVE;
}
// Get the window width/height
int windowWidth, windowHeight;
gtk_window_get_size(window, &windowWidth, &windowHeight);
int newX = ((m.width - windowWidth) / 2) + m.x;
int newY = ((m.height - windowHeight) / 2) + m.y;
// Place the window at the center of the monitor
gtk_window_move(window, newX, newY);
return G_SOURCE_REMOVE;
}
gboolean Show(gpointer data)
{
gtk_widget_show((GtkWidget *)data);
return G_SOURCE_REMOVE;
}
gboolean Hide(gpointer data)
{
gtk_widget_hide((GtkWidget *)data);
return G_SOURCE_REMOVE;
}
gboolean Maximise(gpointer data)
{
gtk_window_maximize((GtkWindow *)data);
return G_SOURCE_REMOVE;
}
gboolean UnMaximise(gpointer data)
{
gtk_window_unmaximize((GtkWindow *)data);
return G_SOURCE_REMOVE;
}
gboolean Minimise(gpointer data)
{
gtk_window_iconify((GtkWindow *)data);
return G_SOURCE_REMOVE;
}
gboolean UnMinimise(gpointer data)
{
gtk_window_present((GtkWindow *)data);
return G_SOURCE_REMOVE;
}
gboolean Fullscreen(gpointer data)
{
GtkWindow *window = (GtkWindow *)data;
// Get the geometry of the monitor.
GdkRectangle m = getCurrentMonitorGeometry(window);
if (isNULLRectangle(m))
{
return G_SOURCE_REMOVE;
}
int scale = getCurrentMonitorScaleFactor(window);
SetMinMaxSize(window, 0, 0, m.width * scale, m.height * scale);
gtk_window_fullscreen(window);
return G_SOURCE_REMOVE;
}
gboolean UnFullscreen(gpointer data)
{
gtk_window_unfullscreen((GtkWindow *)data);
return G_SOURCE_REMOVE;
}
static void webviewLoadChanged(WebKitWebView *web_view, WebKitLoadEvent load_event, gpointer data)
{
if (load_event == WEBKIT_LOAD_FINISHED)
{
processMessage("DomReady");
}
}
extern void processURLRequest(void *request);
// This is called when the close button on the window is pressed
gboolean close_button_pressed(GtkWidget *widget, GdkEvent *event, void *data)
{
processMessage("Q");
// since we handle the close in processMessage tell GTK to not invoke additional handlers - see:
// https://docs.gtk.org/gtk3/signal.Widget.delete-event.html
return TRUE;
}
char *droppedFiles = NULL;
static void onDragDataReceived(GtkWidget *self, GdkDragContext *context, gint x, gint y, GtkSelectionData *selection_data, guint target_type, guint time, gpointer data)
{
if(selection_data == NULL || (gtk_selection_data_get_length(selection_data) <= 0) || target_type != 2)
{
return;
}
if(droppedFiles != NULL) {
free(droppedFiles);
droppedFiles = NULL;
}
gchar **filenames = NULL;
filenames = g_uri_list_extract_uris((const gchar *)gtk_selection_data_get_data(selection_data));
if (filenames == NULL) // If unable to retrieve filenames:
{
g_strfreev(filenames);
return;
}
droppedFiles = calloc((size_t)gtk_selection_data_get_length(selection_data), 1);
int iter = 0;
while(filenames[iter] != NULL) // The last URI list element is NULL.
{
if(iter != 0)
{
strncat(droppedFiles, "\n", 1);
}
char *filename = g_filename_from_uri(filenames[iter], NULL, NULL);
if (filename == NULL)
{
break;
}
strncat(droppedFiles, filename, strlen(filename));
free(filename);
iter++;
}
g_strfreev(filenames);
}
static gboolean onDragDrop(GtkWidget* self, GdkDragContext* context, gint x, gint y, guint time, gpointer user_data)
{
if(droppedFiles == NULL)
{
return FALSE;
}
size_t resLen = strlen(droppedFiles)+(sizeof(gint)*2)+6;
char *res = calloc(resLen, 1);
snprintf(res, resLen, "DD:%d:%d:%s", x, y, droppedFiles);
if(droppedFiles != NULL) {
free(droppedFiles);
droppedFiles = NULL;
}
processMessage(res);
return FALSE;
}
// WebView
GtkWidget *SetupWebview(void *contentManager, GtkWindow *window, int hideWindowOnClose, int gpuPolicy, int disableWebViewDragAndDrop, int enableDragAndDrop)
{
GtkWidget *webview = webkit_web_view_new_with_user_content_manager((WebKitUserContentManager *)contentManager);
// Store webview reference in the content manager
g_object_set_data(G_OBJECT((WebKitUserContentManager *)contentManager), "webview", webview);
// gtk_container_add(GTK_CONTAINER(window), webview);
WebKitWebContext *context = webkit_web_context_get_default();
webkit_web_context_register_uri_scheme(context, "wails", (WebKitURISchemeRequestCallback)processURLRequest, NULL, NULL);
g_signal_connect(G_OBJECT(webview), "load-changed", G_CALLBACK(webviewLoadChanged), NULL);
if(disableWebViewDragAndDrop)
{
gtk_drag_dest_unset(webview);
}
if(enableDragAndDrop)
{
g_signal_connect(G_OBJECT(webview), "drag-data-received", G_CALLBACK(onDragDataReceived), NULL);
g_signal_connect(G_OBJECT(webview), "drag-drop", G_CALLBACK(onDragDrop), NULL);
}
if (hideWindowOnClose)
{
g_signal_connect(GTK_WIDGET(window), "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
}
else
{
g_signal_connect(GTK_WIDGET(window), "delete-event", G_CALLBACK(close_button_pressed), NULL);
}
WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webview));
webkit_settings_set_user_agent_with_application_details(settings, "wails.io", "");
switch (gpuPolicy)
{
case 0:
webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
break;
case 1:
webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND);
break;
case 2:
webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_NEVER);
break;
default:
webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ON_DEMAND);
}
return webview;
}
void DevtoolsEnabled(void *webview, int enabled, bool showInspector)
{
WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webview));
gboolean genabled = enabled == 1 ? true : false;
webkit_settings_set_enable_developer_extras(settings, genabled);
if (genabled && showInspector)
{
ShowInspector(webview);
}
}
void LoadIndex(void *webview, char *url)
{
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(webview), url);
}
static gboolean startDrag(gpointer data)
{
DragOptions *options = (DragOptions *)data;
// Ignore non-toplevel widgets
GtkWidget *window = gtk_widget_get_toplevel(GTK_WIDGET(options->webview));
if (!GTK_IS_WINDOW(window))
{
free(data);
return G_SOURCE_REMOVE;
}
gtk_window_begin_move_drag(options->mainwindow, mouseButton, xroot, yroot, dragTime);
free(data);
return G_SOURCE_REMOVE;
}
void StartDrag(void *webview, GtkWindow *mainwindow)
{
DragOptions *data = malloc(sizeof(DragOptions));
data->webview = webview;
data->mainwindow = mainwindow;
ExecuteOnMainThread(startDrag, (gpointer)data);
}
static gboolean startResize(gpointer data)
{
ResizeOptions *options = (ResizeOptions *)data;
// Ignore non-toplevel widgets
GtkWidget *window = gtk_widget_get_toplevel(GTK_WIDGET(options->webview));
if (!GTK_IS_WINDOW(window))
{
free(data);
return G_SOURCE_REMOVE;
}
gtk_window_begin_resize_drag(options->mainwindow, options->edge, mouseButton, xroot, yroot, dragTime);
free(data);
return G_SOURCE_REMOVE;
}
void StartResize(void *webview, GtkWindow *mainwindow, GdkWindowEdge edge)
{
ResizeOptions *data = malloc(sizeof(ResizeOptions));
data->webview = webview;
data->mainwindow = mainwindow;
data->edge = edge;
ExecuteOnMainThread(startResize, (gpointer)data);
}
void ExecuteJS(void *data)
{
struct JSCallback *js = data;
webkit_web_view_run_javascript(js->webview, js->script, NULL, NULL, NULL);
free(js->script);
}
void extern processMessageDialogResult(char *);
void MessageDialog(void *data)
{
GtkDialogFlags flags;
GtkMessageType messageType;
MessageDialogOptions *options = (MessageDialogOptions *)data;
if (options->messageType == 0)
{
messageType = GTK_MESSAGE_INFO;
flags = GTK_BUTTONS_OK;
}
else if (options->messageType == 1)
{
messageType = GTK_MESSAGE_ERROR;
flags = GTK_BUTTONS_OK;
}
else if (options->messageType == 2)
{
messageType = GTK_MESSAGE_QUESTION;
flags = GTK_BUTTONS_YES_NO;
}
else
{
messageType = GTK_MESSAGE_WARNING;
flags = GTK_BUTTONS_OK;
}
GtkWidget *dialog;
dialog = gtk_message_dialog_new(GTK_WINDOW(options->window),
GTK_DIALOG_DESTROY_WITH_PARENT,
messageType,
flags,
options->message, NULL);
gtk_window_set_title(GTK_WINDOW(dialog), options->title);
GtkResponseType result = gtk_dialog_run(GTK_DIALOG(dialog));
if (result == GTK_RESPONSE_YES)
{
processMessageDialogResult("Yes");
}
else if (result == GTK_RESPONSE_NO)
{
processMessageDialogResult("No");
}
else if (result == GTK_RESPONSE_OK)
{
processMessageDialogResult("OK");
}
else if (result == GTK_RESPONSE_CANCEL)
{
processMessageDialogResult("Cancel");
}
else
{
processMessageDialogResult("");
}
gtk_widget_destroy(dialog);
free(options->title);
free(options->message);
}
void extern processOpenFileResult(void *);
GtkFileFilter **AllocFileFilterArray(size_t ln)
{
return (GtkFileFilter **)malloc(ln * sizeof(GtkFileFilter *));
}
void freeFileFilterArray(GtkFileFilter **filters)
{
free(filters);
}
void Opendialog(void *data)
{
struct OpenFileDialogOptions *options = data;
char *label = "_Open";
if (options->action == GTK_FILE_CHOOSER_ACTION_SAVE)
{
label = "_Save";
}
GtkWidget *dlgWidget = gtk_file_chooser_dialog_new(options->title, options->window, options->action,
"_Cancel", GTK_RESPONSE_CANCEL,
label, GTK_RESPONSE_ACCEPT,
NULL);
GtkFileChooser *fc = GTK_FILE_CHOOSER(dlgWidget);
// filters
if (options->filters != 0)
{
int index = 0;
GtkFileFilter *thisFilter;
while (options->filters[index] != NULL)
{
thisFilter = options->filters[index];
gtk_file_chooser_add_filter(fc, thisFilter);
index++;
}
}
gtk_file_chooser_set_local_only(fc, FALSE);
if (options->multipleFiles == 1)
{
gtk_file_chooser_set_select_multiple(fc, TRUE);
}
gtk_file_chooser_set_do_overwrite_confirmation(fc, TRUE);
if (options->createDirectories == 1)
{
gtk_file_chooser_set_create_folders(fc, TRUE);
}
if (options->showHiddenFiles == 1)
{
gtk_file_chooser_set_show_hidden(fc, TRUE);
}
if (options->defaultDirectory != NULL)
{
gtk_file_chooser_set_current_folder(fc, options->defaultDirectory);
free(options->defaultDirectory);
}
if (options->action == GTK_FILE_CHOOSER_ACTION_SAVE)
{
if (options->defaultFilename != NULL)
{
gtk_file_chooser_set_current_name(fc, options->defaultFilename);
free(options->defaultFilename);
}
}
gint response = gtk_dialog_run(GTK_DIALOG(dlgWidget));
// Max 1024 files to select
char **result = calloc(1024, sizeof(char *));
int resultIndex = 0;
if (response == GTK_RESPONSE_ACCEPT)
{
GSList *filenames = gtk_file_chooser_get_filenames(fc);
GSList *iter = filenames;
while (iter)
{
result[resultIndex++] = (char *)iter->data;
iter = g_slist_next(iter);
if (resultIndex == 1024)
{
break;
}
}
processOpenFileResult(result);
iter = filenames;
while (iter)
{
g_free(iter->data);
iter = g_slist_next(iter);
}
}
else
{
processOpenFileResult(result);
}
free(result);
// Release filters
if (options->filters != NULL)
{
int index = 0;
GtkFileFilter *thisFilter;
while (options->filters[index] != 0)
{
thisFilter = options->filters[index];
g_object_unref(thisFilter);
index++;
}
freeFileFilterArray(options->filters);
}
gtk_widget_destroy(dlgWidget);
free(options->title);
}
GtkFileFilter *newFileFilter()
{
GtkFileFilter *result = gtk_file_filter_new();
g_object_ref(result);
return result;
}
void ShowInspector(void *webview) {
WebKitWebInspector *inspector = webkit_web_view_get_inspector(WEBKIT_WEB_VIEW(webview));
webkit_web_inspector_show(WEBKIT_WEB_INSPECTOR(inspector));
}
void sendShowInspectorMessage() {
processMessage("wails:showInspector");
}
void InstallF12Hotkey(void *window)
{
// When the user presses Ctrl+Shift+F12, call ShowInspector
GtkAccelGroup *accel_group = gtk_accel_group_new();
gtk_window_add_accel_group(GTK_WINDOW(window), accel_group);
GClosure *closure = g_cclosure_new(G_CALLBACK(sendShowInspectorMessage), window, NULL);
gtk_accel_group_connect(accel_group, GDK_KEY_F12, GDK_CONTROL_MASK | GDK_SHIFT_MASK, GTK_ACCEL_VISIBLE, closure);
}

View File

@@ -0,0 +1,479 @@
//go:build linux
// +build linux
package linux
/*
#cgo linux pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
#include <JavaScriptCore/JavaScript.h>
#include <gtk/gtk.h>
#include <webkit2/webkit2.h>
#include <stdio.h>
#include <limits.h>
#include <stdint.h>
#include "window.h"
*/
import "C"
import (
"log"
"strings"
"sync"
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/linux"
)
func gtkBool(input bool) C.gboolean {
if input {
return C.gboolean(1)
}
return C.gboolean(0)
}
type Window struct {
appoptions *options.App
debug bool
devtoolsEnabled bool
gtkWindow unsafe.Pointer
contentManager unsafe.Pointer
webview unsafe.Pointer
applicationMenu *menu.Menu
menubar *C.GtkWidget
webviewBox *C.GtkWidget
vbox *C.GtkWidget
accels *C.GtkAccelGroup
minWidth, minHeight, maxWidth, maxHeight int
}
func bool2Cint(value bool) C.int {
if value {
return C.int(1)
}
return C.int(0)
}
func NewWindow(appoptions *options.App, debug bool, devtoolsEnabled bool) *Window {
validateWebKit2Version(appoptions)
result := &Window{
appoptions: appoptions,
debug: debug,
devtoolsEnabled: devtoolsEnabled,
minHeight: appoptions.MinHeight,
minWidth: appoptions.MinWidth,
maxHeight: appoptions.MaxHeight,
maxWidth: appoptions.MaxWidth,
}
gtkWindow := C.gtk_window_new(C.GTK_WINDOW_TOPLEVEL)
C.g_object_ref_sink(C.gpointer(gtkWindow))
result.gtkWindow = unsafe.Pointer(gtkWindow)
webviewName := C.CString("webview-box")
defer C.free(unsafe.Pointer(webviewName))
result.webviewBox = C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0)
C.gtk_widget_set_name(result.webviewBox, webviewName)
result.vbox = C.gtk_box_new(C.GTK_ORIENTATION_VERTICAL, 0)
C.gtk_container_add(result.asGTKContainer(), result.vbox)
result.contentManager = unsafe.Pointer(C.webkit_user_content_manager_new())
external := C.CString("external")
defer C.free(unsafe.Pointer(external))
C.webkit_user_content_manager_register_script_message_handler(result.cWebKitUserContentManager(), external)
C.SetupInvokeSignal(result.contentManager)
var webviewGpuPolicy int
if appoptions.Linux != nil {
webviewGpuPolicy = int(appoptions.Linux.WebviewGpuPolicy)
} else {
// workaround for https://github.com/wailsapp/wails/issues/2977
webviewGpuPolicy = int(linux.WebviewGpuPolicyNever)
}
webview := C.SetupWebview(
result.contentManager,
result.asGTKWindow(),
bool2Cint(appoptions.HideWindowOnClose),
C.int(webviewGpuPolicy),
bool2Cint(appoptions.DragAndDrop != nil && appoptions.DragAndDrop.DisableWebViewDrop),
bool2Cint(appoptions.DragAndDrop != nil && appoptions.DragAndDrop.EnableFileDrop),
)
result.webview = unsafe.Pointer(webview)
buttonPressedName := C.CString("button-press-event")
defer C.free(unsafe.Pointer(buttonPressedName))
C.ConnectButtons(unsafe.Pointer(webview))
if devtoolsEnabled {
C.DevtoolsEnabled(unsafe.Pointer(webview), C.int(1), C.bool(debug && appoptions.Debug.OpenInspectorOnStartup))
// Install Ctrl-Shift-F12 hotkey to call ShowInspector
C.InstallF12Hotkey(unsafe.Pointer(gtkWindow))
}
if !(debug || appoptions.EnableDefaultContextMenu) {
C.DisableContextMenu(unsafe.Pointer(webview))
}
// Set background colour
RGBA := appoptions.BackgroundColour
result.SetBackgroundColour(RGBA.R, RGBA.G, RGBA.B, RGBA.A)
// Setup window
result.SetKeepAbove(appoptions.AlwaysOnTop)
result.SetResizable(!appoptions.DisableResize)
result.SetDefaultSize(appoptions.Width, appoptions.Height)
result.SetDecorated(!appoptions.Frameless)
result.SetTitle(appoptions.Title)
result.SetMinSize(appoptions.MinWidth, appoptions.MinHeight)
result.SetMaxSize(appoptions.MaxWidth, appoptions.MaxHeight)
if appoptions.Linux != nil {
if appoptions.Linux.Icon != nil {
result.SetWindowIcon(appoptions.Linux.Icon)
}
if appoptions.Linux.WindowIsTranslucent {
C.SetWindowTransparency(gtkWindow)
}
}
// Menu
result.SetApplicationMenu(appoptions.Menu)
return result
}
func (w *Window) asGTKWidget() *C.GtkWidget {
return C.GTKWIDGET(w.gtkWindow)
}
func (w *Window) asGTKWindow() *C.GtkWindow {
return C.GTKWINDOW(w.gtkWindow)
}
func (w *Window) asGTKContainer() *C.GtkContainer {
return C.GTKCONTAINER(w.gtkWindow)
}
func (w *Window) cWebKitUserContentManager() *C.WebKitUserContentManager {
return (*C.WebKitUserContentManager)(w.contentManager)
}
func (w *Window) Fullscreen() {
C.ExecuteOnMainThread(C.Fullscreen, C.gpointer(w.asGTKWindow()))
}
func (w *Window) UnFullscreen() {
if !w.IsFullScreen() {
return
}
C.ExecuteOnMainThread(C.UnFullscreen, C.gpointer(w.asGTKWindow()))
w.SetMinSize(w.minWidth, w.minHeight)
w.SetMaxSize(w.maxWidth, w.maxHeight)
}
func (w *Window) Destroy() {
C.gtk_widget_destroy(w.asGTKWidget())
C.g_object_unref(C.gpointer(w.gtkWindow))
}
func (w *Window) Close() {
C.gtk_window_close(w.asGTKWindow())
}
func (w *Window) Center() {
C.ExecuteOnMainThread(C.Center, C.gpointer(w.asGTKWindow()))
}
func (w *Window) SetPosition(x int, y int) {
invokeOnMainThread(func() {
C.SetPosition(unsafe.Pointer(w.asGTKWindow()), C.int(x), C.int(y))
})
}
func (w *Window) Size() (int, int) {
var width, height C.int
var wg sync.WaitGroup
wg.Add(1)
invokeOnMainThread(func() {
C.gtk_window_get_size(w.asGTKWindow(), &width, &height)
wg.Done()
})
wg.Wait()
return int(width), int(height)
}
func (w *Window) GetPosition() (int, int) {
var width, height C.int
var wg sync.WaitGroup
wg.Add(1)
invokeOnMainThread(func() {
C.gtk_window_get_position(w.asGTKWindow(), &width, &height)
wg.Done()
})
wg.Wait()
return int(width), int(height)
}
func (w *Window) SetMaxSize(maxWidth int, maxHeight int) {
w.maxHeight = maxHeight
w.maxWidth = maxWidth
invokeOnMainThread(func() {
C.SetMinMaxSize(w.asGTKWindow(), C.int(w.minWidth), C.int(w.minHeight), C.int(w.maxWidth), C.int(w.maxHeight))
})
}
func (w *Window) SetMinSize(minWidth int, minHeight int) {
w.minHeight = minHeight
w.minWidth = minWidth
invokeOnMainThread(func() {
C.SetMinMaxSize(w.asGTKWindow(), C.int(w.minWidth), C.int(w.minHeight), C.int(w.maxWidth), C.int(w.maxHeight))
})
}
func (w *Window) Show() {
C.ExecuteOnMainThread(C.Show, C.gpointer(w.asGTKWindow()))
}
func (w *Window) Hide() {
C.ExecuteOnMainThread(C.Hide, C.gpointer(w.asGTKWindow()))
}
func (w *Window) Maximise() {
C.ExecuteOnMainThread(C.Maximise, C.gpointer(w.asGTKWindow()))
}
func (w *Window) UnMaximise() {
C.ExecuteOnMainThread(C.UnMaximise, C.gpointer(w.asGTKWindow()))
}
func (w *Window) Minimise() {
C.ExecuteOnMainThread(C.Minimise, C.gpointer(w.asGTKWindow()))
}
func (w *Window) UnMinimise() {
C.ExecuteOnMainThread(C.UnMinimise, C.gpointer(w.asGTKWindow()))
}
func (w *Window) IsFullScreen() bool {
result := C.IsFullscreen(w.asGTKWidget())
if result != 0 {
return true
}
return false
}
func (w *Window) IsMaximised() bool {
result := C.IsMaximised(w.asGTKWidget())
return result > 0
}
func (w *Window) IsMinimised() bool {
result := C.IsMinimised(w.asGTKWidget())
return result > 0
}
func (w *Window) IsNormal() bool {
return !w.IsMaximised() && !w.IsMinimised() && !w.IsFullScreen()
}
func (w *Window) SetBackgroundColour(r uint8, g uint8, b uint8, a uint8) {
windowIsTranslucent := false
if w.appoptions.Linux != nil && w.appoptions.Linux.WindowIsTranslucent {
windowIsTranslucent = true
}
data := C.RGBAOptions{
r: C.uchar(r),
g: C.uchar(g),
b: C.uchar(b),
a: C.uchar(a),
webview: w.webview,
webviewBox: unsafe.Pointer(w.webviewBox),
windowIsTranslucent: gtkBool(windowIsTranslucent),
}
invokeOnMainThread(func() { C.SetBackgroundColour(unsafe.Pointer(&data)) })
}
func (w *Window) SetWindowIcon(icon []byte) {
if len(icon) == 0 {
return
}
C.SetWindowIcon(w.asGTKWindow(), (*C.guchar)(&icon[0]), (C.gsize)(len(icon)))
}
func (w *Window) Run(url string) {
if w.menubar != nil {
C.gtk_box_pack_start(C.GTKBOX(unsafe.Pointer(w.vbox)), w.menubar, 0, 0, 0)
}
C.gtk_box_pack_start(C.GTKBOX(unsafe.Pointer(w.webviewBox)), C.GTKWIDGET(w.webview), 1, 1, 0)
C.gtk_box_pack_start(C.GTKBOX(unsafe.Pointer(w.vbox)), w.webviewBox, 1, 1, 0)
_url := C.CString(url)
C.LoadIndex(w.webview, _url)
defer C.free(unsafe.Pointer(_url))
if w.appoptions.StartHidden {
w.Hide()
}
C.gtk_widget_show_all(w.asGTKWidget())
w.Center()
switch w.appoptions.WindowStartState {
case options.Fullscreen:
w.Fullscreen()
case options.Minimised:
w.Minimise()
case options.Maximised:
w.Maximise()
}
}
func (w *Window) SetKeepAbove(top bool) {
C.gtk_window_set_keep_above(w.asGTKWindow(), gtkBool(top))
}
func (w *Window) SetResizable(resizable bool) {
C.gtk_window_set_resizable(w.asGTKWindow(), gtkBool(resizable))
}
func (w *Window) SetDefaultSize(width int, height int) {
C.gtk_window_set_default_size(w.asGTKWindow(), C.int(width), C.int(height))
}
func (w *Window) SetSize(width int, height int) {
C.gtk_window_resize(w.asGTKWindow(), C.gint(width), C.gint(height))
}
func (w *Window) SetDecorated(frameless bool) {
C.gtk_window_set_decorated(w.asGTKWindow(), gtkBool(frameless))
}
func (w *Window) SetTitle(title string) {
C.SetTitle(w.asGTKWindow(), C.CString(title))
}
func (w *Window) ExecJS(js string) {
jscallback := C.JSCallback{
webview: w.webview,
script: C.CString(js),
}
invokeOnMainThread(func() { C.ExecuteJS(unsafe.Pointer(&jscallback)) })
}
func (w *Window) StartDrag() {
C.StartDrag(w.webview, w.asGTKWindow())
}
func (w *Window) StartResize(edge uintptr) {
C.StartResize(w.webview, w.asGTKWindow(), C.GdkWindowEdge(edge))
}
func (w *Window) Quit() {
C.gtk_main_quit()
}
func (w *Window) OpenFileDialog(dialogOptions frontend.OpenDialogOptions, multipleFiles int, action C.GtkFileChooserAction) {
data := C.OpenFileDialogOptions{
window: w.asGTKWindow(),
title: C.CString(dialogOptions.Title),
multipleFiles: C.int(multipleFiles),
action: action,
}
if len(dialogOptions.Filters) > 0 {
// Create filter array
mem := NewCalloc()
arraySize := len(dialogOptions.Filters) + 1
data.filters = C.AllocFileFilterArray((C.size_t)(arraySize))
filters := unsafe.Slice((**C.struct__GtkFileFilter)(unsafe.Pointer(data.filters)), arraySize)
for index, filter := range dialogOptions.Filters {
thisFilter := C.gtk_file_filter_new()
C.g_object_ref(C.gpointer(thisFilter))
if filter.DisplayName != "" {
cName := mem.String(filter.DisplayName)
C.gtk_file_filter_set_name(thisFilter, cName)
}
if filter.Pattern != "" {
for _, thisPattern := range strings.Split(filter.Pattern, ";") {
cThisPattern := mem.String(thisPattern)
C.gtk_file_filter_add_pattern(thisFilter, cThisPattern)
}
}
// Add filter to array
filters[index] = thisFilter
}
mem.Free()
filters[arraySize-1] = nil
}
if dialogOptions.CanCreateDirectories {
data.createDirectories = C.int(1)
}
if dialogOptions.ShowHiddenFiles {
data.showHiddenFiles = C.int(1)
}
if dialogOptions.DefaultFilename != "" {
data.defaultFilename = C.CString(dialogOptions.DefaultFilename)
}
if dialogOptions.DefaultDirectory != "" {
data.defaultDirectory = C.CString(dialogOptions.DefaultDirectory)
}
invokeOnMainThread(func() { C.Opendialog(unsafe.Pointer(&data)) })
}
func (w *Window) MessageDialog(dialogOptions frontend.MessageDialogOptions) {
data := C.MessageDialogOptions{
window: w.gtkWindow,
title: C.CString(dialogOptions.Title),
message: C.CString(dialogOptions.Message),
}
switch dialogOptions.Type {
case frontend.InfoDialog:
data.messageType = C.int(0)
case frontend.ErrorDialog:
data.messageType = C.int(1)
case frontend.QuestionDialog:
data.messageType = C.int(2)
case frontend.WarningDialog:
data.messageType = C.int(3)
}
invokeOnMainThread(func() { C.MessageDialog(unsafe.Pointer(&data)) })
}
func (w *Window) ToggleMaximise() {
if w.IsMaximised() {
w.UnMaximise()
} else {
w.Maximise()
}
}
func (w *Window) ShowInspector() {
invokeOnMainThread(func() { C.ShowInspector(w.webview) })
}
// showModalDialogAndExit shows a modal dialog and exits the app.
func showModalDialogAndExit(title, message string) {
go func() {
data := C.MessageDialogOptions{
title: C.CString(title),
message: C.CString(message),
messageType: C.int(1),
}
C.MessageDialog(unsafe.Pointer(&data))
}()
<-messageDialogResult
log.Fatal(message)
}

View File

@@ -0,0 +1,128 @@
#ifndef window_h
#define window_h
#include <JavaScriptCore/JavaScript.h>
#include <gtk/gtk.h>
#include <webkit2/webkit2.h>
#include <stdio.h>
#include <limits.h>
#include <stdint.h>
typedef struct DragOptions
{
void *webview;
GtkWindow *mainwindow;
} DragOptions;
typedef struct ResizeOptions
{
void *webview;
GtkWindow *mainwindow;
GdkWindowEdge edge;
} ResizeOptions;
typedef struct JSCallback
{
void *webview;
char *script;
} JSCallback;
typedef struct MessageDialogOptions
{
void *window;
char *title;
char *message;
int messageType;
} MessageDialogOptions;
typedef struct OpenFileDialogOptions
{
GtkWindow *window;
char *title;
char *defaultFilename;
char *defaultDirectory;
int createDirectories;
int multipleFiles;
int showHiddenFiles;
GtkFileChooserAction action;
GtkFileFilter **filters;
} OpenFileDialogOptions;
typedef struct RGBAOptions
{
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a;
void *webview;
void *webviewBox;
gboolean windowIsTranslucent;
} RGBAOptions;
typedef struct SetTitleArgs
{
GtkWindow *window;
char *title;
} SetTitleArgs;
typedef struct SetPositionArgs
{
int x;
int y;
void *window;
} SetPositionArgs;
void ExecuteOnMainThread(void *f, gpointer jscallback);
GtkWidget *GTKWIDGET(void *pointer);
GtkWindow *GTKWINDOW(void *pointer);
GtkContainer *GTKCONTAINER(void *pointer);
GtkBox *GTKBOX(void *pointer);
// window
ulong SetupInvokeSignal(void *contentManager);
void SetWindowIcon(GtkWindow *window, const guchar *buf, gsize len);
void SetWindowTransparency(GtkWidget *widget);
void SetBackgroundColour(void *data);
void SetTitle(GtkWindow *window, char *title);
void SetPosition(void *window, int x, int y);
void SetMinMaxSize(GtkWindow *window, int min_width, int min_height, int max_width, int max_height);
void DisableContextMenu(void *webview);
void ConnectButtons(void *webview);
int IsFullscreen(GtkWidget *widget);
int IsMaximised(GtkWidget *widget);
int IsMinimised(GtkWidget *widget);
gboolean Center(gpointer data);
gboolean Show(gpointer data);
gboolean Hide(gpointer data);
gboolean Maximise(gpointer data);
gboolean UnMaximise(gpointer data);
gboolean Minimise(gpointer data);
gboolean UnMinimise(gpointer data);
gboolean Fullscreen(gpointer data);
gboolean UnFullscreen(gpointer data);
// WebView
GtkWidget *SetupWebview(void *contentManager, GtkWindow *window, int hideWindowOnClose, int gpuPolicy, int disableWebViewDragAndDrop, int enableDragAndDrop);
void LoadIndex(void *webview, char *url);
void DevtoolsEnabled(void *webview, int enabled, bool showInspector);
void ExecuteJS(void *data);
// Drag
void StartDrag(void *webview, GtkWindow *mainwindow);
void StartResize(void *webview, GtkWindow *mainwindow, GdkWindowEdge edge);
// Dialog
void MessageDialog(void *data);
GtkFileFilter **AllocFileFilterArray(size_t ln);
void Opendialog(void *data);
// Inspector
void sendShowInspectorMessage();
void ShowInspector(void *webview);
void InstallF12Hotkey(void *window);
#endif /* window_h */

View File

@@ -0,0 +1,43 @@
//go:build windows
// +build windows
package windows
import (
"fmt"
"github.com/pkg/browser"
"github.com/wailsapp/wails/v2/internal/frontend/utils"
"golang.org/x/sys/windows"
)
var fallbackBrowserPaths = []string{
`\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`,
`\Program Files\Google\Chrome\Application\chrome.exe`,
`\Program Files (x86)\Google\Chrome\Application\chrome.exe`,
`\Program Files\Mozilla Firefox\firefox.exe`,
}
// BrowserOpenURL Use the default browser to open the url
func (f *Frontend) BrowserOpenURL(rawURL string) {
url, err := utils.ValidateAndSanitizeURL(rawURL)
if err != nil {
f.logger.Error(fmt.Sprintf("Invalid URL %s", err.Error()))
return
}
// Specific method implementation
err = browser.OpenURL(url)
if err == nil {
return
}
for _, fallback := range fallbackBrowserPaths {
if err := openBrowser(fallback, url); err == nil {
return
}
}
f.logger.Error("Unable to open default system browser")
}
func openBrowser(path, url string) error {
return windows.ShellExecute(0, nil, windows.StringToUTF16Ptr(path), windows.StringToUTF16Ptr(url), nil, windows.SW_SHOWNORMAL)
}

View File

@@ -0,0 +1,16 @@
//go:build windows
// +build windows
package windows
import (
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32"
)
func (f *Frontend) ClipboardGetText() (string, error) {
return win32.GetClipboardText()
}
func (f *Frontend) ClipboardSetText(text string) error {
return win32.SetClipboardText(text)
}

View File

@@ -0,0 +1,210 @@
//go:build windows
// +build windows
package windows
import (
"path/filepath"
"strings"
"syscall"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
"github.com/wailsapp/wails/v2/internal/go-common-file-dialog/cfd"
"golang.org/x/sys/windows"
)
func (f *Frontend) getHandleForDialog() w32.HWND {
if f.mainWindow.IsVisible() {
return f.mainWindow.Handle()
}
return 0
}
func getDefaultFolder(folder string) (string, error) {
if folder == "" {
return "", nil
}
return filepath.Abs(folder)
}
// OpenDirectoryDialog prompts the user to select a directory
func (f *Frontend) OpenDirectoryDialog(options frontend.OpenDialogOptions) (string, error) {
defaultFolder, err := getDefaultFolder(options.DefaultDirectory)
if err != nil {
return "", err
}
config := cfd.DialogConfig{
Title: options.Title,
Role: "PickFolder",
Folder: defaultFolder,
}
result, err := f.showCfdDialog(
func() (cfd.Dialog, error) {
return cfd.NewSelectFolderDialog(config)
}, false)
if err != nil && err != cfd.ErrCancelled {
return "", err
}
return result.(string), nil
}
// OpenFileDialog prompts the user to select a file
func (f *Frontend) OpenFileDialog(options frontend.OpenDialogOptions) (string, error) {
defaultFolder, err := getDefaultFolder(options.DefaultDirectory)
if err != nil {
return "", err
}
config := cfd.DialogConfig{
Folder: defaultFolder,
FileFilters: convertFilters(options.Filters),
FileName: options.DefaultFilename,
Title: options.Title,
}
result, err := f.showCfdDialog(
func() (cfd.Dialog, error) {
return cfd.NewOpenFileDialog(config)
}, false)
if err != nil && err != cfd.ErrCancelled {
return "", err
}
return result.(string), nil
}
// OpenMultipleFilesDialog prompts the user to select a file
func (f *Frontend) OpenMultipleFilesDialog(options frontend.OpenDialogOptions) ([]string, error) {
defaultFolder, err := getDefaultFolder(options.DefaultDirectory)
if err != nil {
return nil, err
}
config := cfd.DialogConfig{
Title: options.Title,
Role: "OpenMultipleFiles",
FileFilters: convertFilters(options.Filters),
FileName: options.DefaultFilename,
Folder: defaultFolder,
}
result, err := f.showCfdDialog(
func() (cfd.Dialog, error) {
return cfd.NewOpenMultipleFilesDialog(config)
}, true)
if err != nil && err != cfd.ErrCancelled {
return nil, err
}
return result.([]string), nil
}
// SaveFileDialog prompts the user to select a file
func (f *Frontend) SaveFileDialog(options frontend.SaveDialogOptions) (string, error) {
defaultFolder, err := getDefaultFolder(options.DefaultDirectory)
if err != nil {
return "", err
}
config := cfd.DialogConfig{
Title: options.Title,
Role: "SaveFile",
FileFilters: convertFilters(options.Filters),
FileName: options.DefaultFilename,
Folder: defaultFolder,
}
if len(options.Filters) > 0 {
config.DefaultExtension = strings.TrimPrefix(strings.Split(options.Filters[0].Pattern, ";")[0], "*")
}
result, err := f.showCfdDialog(
func() (cfd.Dialog, error) {
return cfd.NewSaveFileDialog(config)
}, false)
if err != nil && err != cfd.ErrCancelled {
return "", err
}
return result.(string), nil
}
func (f *Frontend) showCfdDialog(newDlg func() (cfd.Dialog, error), isMultiSelect bool) (any, error) {
return invokeSync(f.mainWindow, func() (any, error) {
dlg, err := newDlg()
if err != nil {
return nil, err
}
defer func() {
err := dlg.Release()
if err != nil {
println("ERROR: Unable to release dialog:", err.Error())
}
}()
dlg.SetParentWindowHandle(f.getHandleForDialog())
if multi, _ := dlg.(cfd.OpenMultipleFilesDialog); multi != nil && isMultiSelect {
return multi.ShowAndGetResults()
}
return dlg.ShowAndGetResult()
})
}
func calculateMessageDialogFlags(options frontend.MessageDialogOptions) uint32 {
var flags uint32
switch options.Type {
case frontend.InfoDialog:
flags = windows.MB_OK | windows.MB_ICONINFORMATION
case frontend.ErrorDialog:
flags = windows.MB_ICONERROR | windows.MB_OK
case frontend.QuestionDialog:
flags = windows.MB_YESNO
if strings.TrimSpace(strings.ToLower(options.DefaultButton)) == "no" {
flags |= windows.MB_DEFBUTTON2
}
case frontend.WarningDialog:
flags = windows.MB_OK | windows.MB_ICONWARNING
}
return flags
}
// MessageDialog show a message dialog to the user
func (f *Frontend) MessageDialog(options frontend.MessageDialogOptions) (string, error) {
title, err := syscall.UTF16PtrFromString(options.Title)
if err != nil {
return "", err
}
message, err := syscall.UTF16PtrFromString(options.Message)
if err != nil {
return "", err
}
flags := calculateMessageDialogFlags(options)
button, _ := windows.MessageBox(windows.HWND(f.getHandleForDialog()), message, title, flags|windows.MB_SYSTEMMODAL)
// This maps MessageBox return values to strings
responses := []string{"", "Ok", "Cancel", "Abort", "Retry", "Ignore", "Yes", "No", "", "", "Try Again", "Continue"}
result := "Error"
if int(button) < len(responses) {
result = responses[button]
}
return result, nil
}
func convertFilters(filters []frontend.FileFilter) []cfd.FileFilter {
var result []cfd.FileFilter
for _, filter := range filters {
result = append(result, cfd.FileFilter(filter))
}
return result
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,203 @@
//go:build windows
// +build windows
package windows
import (
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
"strings"
)
var ModifierMap = map[keys.Modifier]winc.Modifiers{
keys.ShiftKey: winc.ModShift,
keys.ControlKey: winc.ModControl,
keys.OptionOrAltKey: winc.ModAlt,
keys.CmdOrCtrlKey: winc.ModControl,
}
func acceleratorToWincShortcut(accelerator *keys.Accelerator) winc.Shortcut {
if accelerator == nil {
return winc.NoShortcut
}
inKey := strings.ToUpper(accelerator.Key)
key, exists := keyMap[inKey]
if !exists {
return winc.NoShortcut
}
var modifiers winc.Modifiers
if _, exists := shiftMap[inKey]; exists {
modifiers = winc.ModShift
}
for _, mod := range accelerator.Modifiers {
modifiers |= ModifierMap[mod]
}
return winc.Shortcut{
Modifiers: modifiers,
Key: key,
}
}
var shiftMap = map[string]struct{}{
"~": {},
")": {},
"!": {},
"@": {},
"#": {},
"$": {},
"%": {},
"^": {},
"&": {},
"*": {},
"(": {},
"_": {},
"PLUS": {},
"<": {},
">": {},
"?": {},
":": {},
`"`: {},
"{": {},
"}": {},
"|": {},
}
var keyMap = map[string]winc.Key{
"0": winc.Key0,
"1": winc.Key1,
"2": winc.Key2,
"3": winc.Key3,
"4": winc.Key4,
"5": winc.Key5,
"6": winc.Key6,
"7": winc.Key7,
"8": winc.Key8,
"9": winc.Key9,
"A": winc.KeyA,
"B": winc.KeyB,
"C": winc.KeyC,
"D": winc.KeyD,
"E": winc.KeyE,
"F": winc.KeyF,
"G": winc.KeyG,
"H": winc.KeyH,
"I": winc.KeyI,
"J": winc.KeyJ,
"K": winc.KeyK,
"L": winc.KeyL,
"M": winc.KeyM,
"N": winc.KeyN,
"O": winc.KeyO,
"P": winc.KeyP,
"Q": winc.KeyQ,
"R": winc.KeyR,
"S": winc.KeyS,
"T": winc.KeyT,
"U": winc.KeyU,
"V": winc.KeyV,
"W": winc.KeyW,
"X": winc.KeyX,
"Y": winc.KeyY,
"Z": winc.KeyZ,
"F1": winc.KeyF1,
"F2": winc.KeyF2,
"F3": winc.KeyF3,
"F4": winc.KeyF4,
"F5": winc.KeyF5,
"F6": winc.KeyF6,
"F7": winc.KeyF7,
"F8": winc.KeyF8,
"F9": winc.KeyF9,
"F10": winc.KeyF10,
"F11": winc.KeyF11,
"F12": winc.KeyF12,
"F13": winc.KeyF13,
"F14": winc.KeyF14,
"F15": winc.KeyF15,
"F16": winc.KeyF16,
"F17": winc.KeyF17,
"F18": winc.KeyF18,
"F19": winc.KeyF19,
"F20": winc.KeyF20,
"F21": winc.KeyF21,
"F22": winc.KeyF22,
"F23": winc.KeyF23,
"F24": winc.KeyF24,
"`": winc.KeyOEM3,
",": winc.KeyOEMComma,
".": winc.KeyOEMPeriod,
"/": winc.KeyOEM2,
";": winc.KeyOEM1,
"'": winc.KeyOEM7,
"[": winc.KeyOEM4,
"]": winc.KeyOEM6,
`\`: winc.KeyOEM5,
"~": winc.KeyOEM3, //
")": winc.Key0,
"!": winc.Key1,
"@": winc.Key2,
"#": winc.Key3,
"$": winc.Key4,
"%": winc.Key5,
"^": winc.Key6,
"&": winc.Key7,
"*": winc.Key8,
"(": winc.Key9,
"_": winc.KeyOEMMinus,
"PLUS": winc.KeyOEMPlus,
"<": winc.KeyOEMComma,
">": winc.KeyOEMPeriod,
"?": winc.KeyOEM2,
":": winc.KeyOEM1,
`"`: winc.KeyOEM7,
"{": winc.KeyOEM4,
"}": winc.KeyOEM6,
"|": winc.KeyOEM5,
"SPACE": winc.KeySpace,
"TAB": winc.KeyTab,
"CAPSLOCK": winc.KeyCapital,
"NUMLOCK": winc.KeyNumlock,
"SCROLLLOCK": winc.KeyScroll,
"BACKSPACE": winc.KeyBack,
"DELETE": winc.KeyDelete,
"INSERT": winc.KeyInsert,
"RETURN": winc.KeyReturn,
"ENTER": winc.KeyReturn,
"UP": winc.KeyUp,
"DOWN": winc.KeyDown,
"LEFT": winc.KeyLeft,
"RIGHT": winc.KeyRight,
"HOME": winc.KeyHome,
"END": winc.KeyEnd,
"PAGEUP": winc.KeyPrior,
"PAGEDOWN": winc.KeyNext,
"ESCAPE": winc.KeyEscape,
"ESC": winc.KeyEscape,
"VOLUMEUP": winc.KeyVolumeUp,
"VOLUMEDOWN": winc.KeyVolumeDown,
"VOLUMEMUTE": winc.KeyVolumeMute,
"MEDIANEXTTRACK": winc.KeyMediaNextTrack,
"MEDIAPREVIOUSTRACK": winc.KeyMediaPrevTrack,
"MEDIASTOP": winc.KeyMediaStop,
"MEDIAPLAYPAUSE": winc.KeyMediaPlayPause,
"PRINTSCREEN": winc.KeyPrint,
"NUM0": winc.KeyNumpad0,
"NUM1": winc.KeyNumpad1,
"NUM2": winc.KeyNumpad2,
"NUM3": winc.KeyNumpad3,
"NUM4": winc.KeyNumpad4,
"NUM5": winc.KeyNumpad5,
"NUM6": winc.KeyNumpad6,
"NUM7": winc.KeyNumpad7,
"NUM8": winc.KeyNumpad8,
"NUM9": winc.KeyNumpad9,
"nummult": winc.KeyMultiply,
"numadd": winc.KeyAdd,
"numsub": winc.KeySubtract,
"numdec": winc.KeyDecimal,
"numdiv": winc.KeyDivide,
}

View File

@@ -0,0 +1,132 @@
//go:build windows
// +build windows
package windows
import (
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc"
"github.com/wailsapp/wails/v2/pkg/menu"
)
var checkboxMap = map[*menu.MenuItem][]*winc.MenuItem{}
var radioGroupMap = map[*menu.MenuItem][]*winc.MenuItem{}
func toggleCheckBox(menuItem *menu.MenuItem) {
menuItem.Checked = !menuItem.Checked
for _, wincMenu := range checkboxMap[menuItem] {
wincMenu.SetChecked(menuItem.Checked)
}
}
func addCheckBoxToMap(menuItem *menu.MenuItem, wincMenuItem *winc.MenuItem) {
if checkboxMap[menuItem] == nil {
checkboxMap[menuItem] = []*winc.MenuItem{}
}
checkboxMap[menuItem] = append(checkboxMap[menuItem], wincMenuItem)
}
func toggleRadioItem(menuItem *menu.MenuItem) {
menuItem.Checked = !menuItem.Checked
for _, wincMenu := range radioGroupMap[menuItem] {
wincMenu.SetChecked(menuItem.Checked)
}
}
func addRadioItemToMap(menuItem *menu.MenuItem, wincMenuItem *winc.MenuItem) {
if radioGroupMap[menuItem] == nil {
radioGroupMap[menuItem] = []*winc.MenuItem{}
}
radioGroupMap[menuItem] = append(radioGroupMap[menuItem], wincMenuItem)
}
func (w *Window) SetApplicationMenu(menu *menu.Menu) {
w.applicationMenu = menu
processMenu(w, menu)
}
func processMenu(window *Window, menu *menu.Menu) {
mainMenu := window.NewMenu()
for _, menuItem := range menu.Items {
submenu := mainMenu.AddSubMenu(menuItem.Label)
if menuItem.SubMenu != nil {
for _, menuItem := range menuItem.SubMenu.Items {
processMenuItem(submenu, menuItem)
}
}
}
mainMenu.Show()
}
func processMenuItem(parent *winc.MenuItem, menuItem *menu.MenuItem) {
if menuItem.Hidden {
return
}
switch menuItem.Type {
case menu.SeparatorType:
parent.AddSeparator()
case menu.TextType:
shortcut := acceleratorToWincShortcut(menuItem.Accelerator)
newItem := parent.AddItem(menuItem.Label, shortcut)
//if menuItem.Tooltip != "" {
// newItem.SetToolTip(menuItem.Tooltip)
//}
if menuItem.Click != nil {
newItem.OnClick().Bind(func(e *winc.Event) {
menuItem.Click(&menu.CallbackData{
MenuItem: menuItem,
})
})
}
newItem.SetEnabled(!menuItem.Disabled)
case menu.CheckboxType:
shortcut := acceleratorToWincShortcut(menuItem.Accelerator)
newItem := parent.AddItem(menuItem.Label, shortcut)
newItem.SetCheckable(true)
newItem.SetChecked(menuItem.Checked)
//if menuItem.Tooltip != "" {
// newItem.SetToolTip(menuItem.Tooltip)
//}
if menuItem.Click != nil {
newItem.OnClick().Bind(func(e *winc.Event) {
toggleCheckBox(menuItem)
menuItem.Click(&menu.CallbackData{
MenuItem: menuItem,
})
})
}
newItem.SetEnabled(!menuItem.Disabled)
addCheckBoxToMap(menuItem, newItem)
case menu.RadioType:
shortcut := acceleratorToWincShortcut(menuItem.Accelerator)
newItem := parent.AddItemRadio(menuItem.Label, shortcut)
newItem.SetCheckable(true)
newItem.SetChecked(menuItem.Checked)
//if menuItem.Tooltip != "" {
// newItem.SetToolTip(menuItem.Tooltip)
//}
if menuItem.Click != nil {
newItem.OnClick().Bind(func(e *winc.Event) {
toggleRadioItem(menuItem)
menuItem.Click(&menu.CallbackData{
MenuItem: menuItem,
})
})
}
newItem.SetEnabled(!menuItem.Disabled)
addRadioItemToMap(menuItem, newItem)
case menu.SubmenuType:
submenu := parent.AddSubMenu(menuItem.Label)
for _, menuItem := range menuItem.SubMenu.Items {
processMenuItem(submenu, menuItem)
}
}
}
func (f *Frontend) MenuSetApplicationMenu(menu *menu.Menu) {
f.mainWindow.SetApplicationMenu(menu)
}
func (f *Frontend) MenuUpdateApplicationMenu() {
processMenu(f.mainWindow, f.mainWindow.applicationMenu)
}

View File

@@ -0,0 +1,489 @@
//go:build windows
// +build windows
package windows
import (
"encoding/base64"
"encoding/json"
"log"
"sync"
wintoast "git.sr.ht/~jackmordaunt/go-toast/v2/wintoast"
"github.com/google/uuid"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
"fmt"
"os"
"path/filepath"
_ "unsafe" // for go:linkname
"git.sr.ht/~jackmordaunt/go-toast/v2"
"golang.org/x/sys/windows/registry"
)
var (
categories map[string]frontend.NotificationCategory
categoriesLock sync.RWMutex
appName string
appGUID string
iconPath string = ""
exePath string
iconOnce sync.Once
iconErr error
notificationResultCallback func(result frontend.NotificationResult)
callbackLock sync.RWMutex
)
const DefaultActionIdentifier = "DEFAULT_ACTION"
const (
ToastRegistryPath = `Software\Classes\AppUserModelId\`
ToastRegistryGuidKey = "CustomActivator"
NotificationCategoriesRegistryPath = `SOFTWARE\%s\NotificationCategories`
NotificationCategoriesRegistryKey = "Categories"
)
// NotificationPayload combines the action ID and user data into a single structure
type NotificationPayload struct {
Action string `json:"action"`
Options frontend.NotificationOptions `json:"payload,omitempty"`
}
func (f *Frontend) InitializeNotifications() error {
categoriesLock.Lock()
defer categoriesLock.Unlock()
categories = make(map[string]frontend.NotificationCategory)
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable: %w", err)
}
exePath = exe
appName = filepath.Base(exePath)
appGUID, err = getGUID()
if err != nil {
return err
}
iconPath = filepath.Join(os.TempDir(), appName+appGUID+".png")
// Create the registry key for the toast activator
key, _, err := registry.CreateKey(registry.CURRENT_USER,
`Software\Classes\CLSID\`+appGUID+`\LocalServer32`, registry.ALL_ACCESS)
if err != nil {
return fmt.Errorf("failed to create CLSID key: %w", err)
}
defer key.Close()
if err := key.SetStringValue("", fmt.Sprintf("\"%s\" %%1", exePath)); err != nil {
return fmt.Errorf("failed to set CLSID server path: %w", err)
}
toast.SetAppData(toast.AppData{
AppID: appName,
GUID: appGUID,
IconPath: iconPath,
ActivationExe: exePath,
})
toast.SetActivationCallback(func(args string, data []toast.UserData) {
result := frontend.NotificationResult{}
actionIdentifier, options, err := parseNotificationResponse(args)
if err != nil {
result.Error = err
} else {
// Subtitle is retained but was not shown with the notification
response := frontend.NotificationResponse{
ID: options.ID,
ActionIdentifier: actionIdentifier,
Title: options.Title,
Subtitle: options.Subtitle,
Body: options.Body,
CategoryID: options.CategoryID,
UserInfo: options.Data,
}
if userText, found := getUserText(data); found {
response.UserText = userText
}
result.Response = response
}
handleNotificationResult(result)
})
// Register the COM class factory for toast activation.
// This is required for Windows to activate the app when users interact with notifications.
// The go-toast library's SetAppData and SetActivationCallback handle the callback setup,
// but the COM class factory registration is not exposed via public APIs, so we use
// go:linkname to access the internal registerClassFactory function.
if err := registerToastClassFactory(wintoast.ClassFactory); err != nil {
return fmt.Errorf("CoRegisterClassObject failed: %w", err)
}
return loadCategoriesFromRegistry()
}
// registerToastClassFactory registers the COM class factory required for Windows toast notification activation.
// This function uses go:linkname to access the unexported registerClassFactory function from go-toast.
// The class factory is necessary for Windows COM activation when users click notification actions.
// Without this registration, notification actions will not activate the application.
//
// This is a workaround until go-toast exports this functionality via a public API.
// See: https://git.sr.ht/~jackmordaunt/go-toast
//
//go:linkname registerToastClassFactory git.sr.ht/~jackmordaunt/go-toast/v2/wintoast.registerClassFactory
func registerToastClassFactory(factory *wintoast.IClassFactory) error
// CleanupNotifications is a Windows stub that does nothing.
// (Linux-specific cleanup)
func (f *Frontend) CleanupNotifications() {
// No cleanup needed on Windows
}
func (f *Frontend) IsNotificationAvailable() bool {
return true
}
func (f *Frontend) RequestNotificationAuthorization() (bool, error) {
return true, nil
}
func (f *Frontend) CheckNotificationAuthorization() (bool, error) {
return true, nil
}
// SendNotification sends a basic notification with a name, title, and body. All other options are ignored on Windows.
// (subtitle is only available on macOS and Linux)
func (f *Frontend) SendNotification(options frontend.NotificationOptions) error {
if err := f.saveIconToDir(); err != nil {
f.logger.Warning("Error saving icon: %v", err)
}
n := toast.Notification{
Title: options.Title,
Body: options.Body,
ActivationType: toast.Foreground,
ActivationArguments: DefaultActionIdentifier,
}
encodedPayload, err := encodePayload(DefaultActionIdentifier, options)
if err != nil {
return fmt.Errorf("failed to encode notification payload: %w", err)
}
n.ActivationArguments = encodedPayload
return n.Push()
}
// SendNotificationWithActions sends a notification with additional actions and inputs.
// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category.
// If a NotificationCategory is not registered a basic notification will be sent.
// (subtitle is only available on macOS and Linux)
func (f *Frontend) SendNotificationWithActions(options frontend.NotificationOptions) error {
if err := f.saveIconToDir(); err != nil {
f.logger.Warning("Error saving icon: %v", err)
}
categoriesLock.RLock()
nCategory, categoryExists := categories[options.CategoryID]
categoriesLock.RUnlock()
if options.CategoryID == "" || !categoryExists {
f.logger.Warning("Category '%s' not found, sending basic notification without actions", options.CategoryID)
return f.SendNotification(options)
}
n := toast.Notification{
Title: options.Title,
Body: options.Body,
ActivationType: toast.Foreground,
ActivationArguments: DefaultActionIdentifier,
}
for _, action := range nCategory.Actions {
n.Actions = append(n.Actions, toast.Action{
Content: action.Title,
Arguments: action.ID,
})
}
if nCategory.HasReplyField {
n.Inputs = append(n.Inputs, toast.Input{
ID: "userText",
Placeholder: nCategory.ReplyPlaceholder,
})
n.Actions = append(n.Actions, toast.Action{
Content: nCategory.ReplyButtonTitle,
Arguments: "TEXT_REPLY",
InputID: "userText",
})
}
encodedPayload, err := encodePayload(n.ActivationArguments, options)
if err != nil {
return fmt.Errorf("failed to encode notification payload: %w", err)
}
n.ActivationArguments = encodedPayload
for index := range n.Actions {
encodedPayload, err := encodePayload(n.Actions[index].Arguments, options)
if err != nil {
return fmt.Errorf("failed to encode notification payload: %w", err)
}
n.Actions[index].Arguments = encodedPayload
}
return n.Push()
}
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
// Registering a category with the same name as a previously registered NotificationCategory will override it.
func (f *Frontend) RegisterNotificationCategory(category frontend.NotificationCategory) error {
categoriesLock.Lock()
defer categoriesLock.Unlock()
categories[category.ID] = frontend.NotificationCategory{
ID: category.ID,
Actions: category.Actions,
HasReplyField: category.HasReplyField,
ReplyPlaceholder: category.ReplyPlaceholder,
ReplyButtonTitle: category.ReplyButtonTitle,
}
return saveCategoriesToRegistry()
}
// RemoveNotificationCategory removes a previously registered NotificationCategory.
func (f *Frontend) RemoveNotificationCategory(categoryId string) error {
categoriesLock.Lock()
defer categoriesLock.Unlock()
delete(categories, categoryId)
return saveCategoriesToRegistry()
}
// RemoveAllPendingNotifications is a Windows stub that always returns nil.
// (macOS and Linux only)
func (f *Frontend) RemoveAllPendingNotifications() error {
return nil
}
// RemovePendingNotification is a Windows stub that always returns nil.
// (macOS and Linux only)
func (f *Frontend) RemovePendingNotification(_ string) error {
return nil
}
// RemoveAllDeliveredNotifications is a Windows stub that always returns nil.
// (macOS and Linux only)
func (f *Frontend) RemoveAllDeliveredNotifications() error {
return nil
}
// RemoveDeliveredNotification is a Windows stub that always returns nil.
// (macOS and Linux only)
func (f *Frontend) RemoveDeliveredNotification(_ string) error {
return nil
}
// RemoveNotification is a Windows stub that always returns nil.
// (Linux-specific)
func (f *Frontend) RemoveNotification(identifier string) error {
return nil
}
func (f *Frontend) OnNotificationResponse(callback func(result frontend.NotificationResult)) {
callbackLock.Lock()
defer callbackLock.Unlock()
notificationResultCallback = callback
}
func (f *Frontend) saveIconToDir() error {
iconOnce.Do(func() {
hIcon := w32.ExtractIcon(exePath, 0)
if hIcon == 0 {
iconErr = fmt.Errorf("ExtractIcon failed for %s", exePath)
return
}
defer w32.DestroyIcon(hIcon)
iconErr = winc.SaveHIconAsPNG(hIcon, iconPath)
})
return iconErr
}
func saveCategoriesToRegistry() error {
// We assume lock is held by caller
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, appName)
key, _, err := registry.CreateKey(
registry.CURRENT_USER,
registryPath,
registry.ALL_ACCESS,
)
if err != nil {
return err
}
defer key.Close()
data, err := json.Marshal(categories)
if err != nil {
return err
}
return key.SetStringValue(NotificationCategoriesRegistryKey, string(data))
}
func loadCategoriesFromRegistry() error {
// We assume lock is held by caller
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, appName)
key, err := registry.OpenKey(
registry.CURRENT_USER,
registryPath,
registry.QUERY_VALUE,
)
if err != nil {
if err == registry.ErrNotExist {
// Not an error, no saved categories
return nil
}
return fmt.Errorf("failed to open registry key: %w", err)
}
defer key.Close()
data, _, err := key.GetStringValue(NotificationCategoriesRegistryKey)
if err != nil {
if err == registry.ErrNotExist {
// No value yet, but key exists
return nil
}
return fmt.Errorf("failed to read categories from registry: %w", err)
}
_categories := make(map[string]frontend.NotificationCategory)
if err := json.Unmarshal([]byte(data), &_categories); err != nil {
return fmt.Errorf("failed to parse notification categories from registry: %w", err)
}
categories = _categories
return nil
}
func getUserText(data []toast.UserData) (string, bool) {
for _, d := range data {
if d.Key == "userText" {
return d.Value, true
}
}
return "", false
}
// encodePayload combines an action ID and user data into a single encoded string
func encodePayload(actionID string, options frontend.NotificationOptions) (string, error) {
payload := NotificationPayload{
Action: actionID,
Options: options,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return actionID, err
}
encodedPayload := base64.StdEncoding.EncodeToString(jsonData)
return encodedPayload, nil
}
// decodePayload extracts the action ID and user data from an encoded payload
func decodePayload(encodedString string) (string, frontend.NotificationOptions, error) {
jsonData, err := base64.StdEncoding.DecodeString(encodedString)
if err != nil {
return encodedString, frontend.NotificationOptions{}, fmt.Errorf("failed to decode base64 payload: %w", err)
}
var payload NotificationPayload
if err := json.Unmarshal(jsonData, &payload); err != nil {
return encodedString, frontend.NotificationOptions{}, fmt.Errorf("failed to unmarshal notification payload: %w", err)
}
return payload.Action, payload.Options, nil
}
// parseNotificationResponse updated to use structured payload decoding
func parseNotificationResponse(response string) (action string, options frontend.NotificationOptions, err error) {
actionID, options, err := decodePayload(response)
if err != nil {
log.Printf("Warning: Failed to decode notification response: %v", err)
return response, frontend.NotificationOptions{}, err
}
return actionID, options, nil
}
func handleNotificationResult(result frontend.NotificationResult) {
callbackLock.RLock()
callback := notificationResultCallback
callbackLock.RUnlock()
if callback != nil {
go func() {
defer func() {
if r := recover(); r != nil {
// Log panic but don't crash the app
fmt.Fprintf(os.Stderr, "panic in notification callback: %v\n", r)
}
}()
callback(result)
}()
}
}
// Helper functions
func getGUID() (string, error) {
keyPath := ToastRegistryPath + appName
k, err := registry.OpenKey(registry.CURRENT_USER, keyPath, registry.QUERY_VALUE)
if err == nil {
guid, _, err := k.GetStringValue(ToastRegistryGuidKey)
k.Close()
if err == nil && guid != "" {
return guid, nil
}
}
guid := generateGUID()
k, _, err = registry.CreateKey(registry.CURRENT_USER, keyPath, registry.WRITE)
if err != nil {
return "", fmt.Errorf("failed to create registry key: %w", err)
}
defer k.Close()
if err := k.SetStringValue(ToastRegistryGuidKey, guid); err != nil {
return "", fmt.Errorf("failed to write GUID to registry: %w", err)
}
return guid, nil
}
func generateGUID() string {
guid := uuid.New()
return fmt.Sprintf("{%s}", guid.String())
}

View File

@@ -0,0 +1,129 @@
//go:build windows
// +build windows
package windows
import (
"fmt"
"syscall"
"unsafe"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
func MonitorsEqual(first w32.MONITORINFO, second w32.MONITORINFO) bool {
// Checks to make sure all the fields are the same.
// A cleaner way would be to check identity of devices. but I couldn't find a way of doing that using the win32 API
return first.DwFlags == second.DwFlags &&
first.RcMonitor.Top == second.RcMonitor.Top &&
first.RcMonitor.Bottom == second.RcMonitor.Bottom &&
first.RcMonitor.Right == second.RcMonitor.Right &&
first.RcMonitor.Left == second.RcMonitor.Left &&
first.RcWork.Top == second.RcWork.Top &&
first.RcWork.Bottom == second.RcWork.Bottom &&
first.RcWork.Right == second.RcWork.Right &&
first.RcWork.Left == second.RcWork.Left
}
func GetMonitorInfo(hMonitor w32.HMONITOR) (*w32.MONITORINFO, error) {
// Adapted from winc.utils.getMonitorInfo TODO: add this to win32
// See docs for
//https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmonitorinfoa
var info w32.MONITORINFO
info.CbSize = uint32(unsafe.Sizeof(info))
succeeded := w32.GetMonitorInfo(hMonitor, &info)
if !succeeded {
return &info, errors.New("Windows call to getMonitorInfo failed")
}
return &info, nil
}
func EnumProc(hMonitor w32.HMONITOR, hdcMonitor w32.HDC, lprcMonitor *w32.RECT, screenContainer *ScreenContainer) uintptr {
// adapted from https://stackoverflow.com/a/23492886/4188138
// see docs for the following pages to better understand this function
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-monitorenumproc
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-monitorinfo
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-monitorfromwindow
ourMonitorData := Screen{}
currentMonHndl := w32.MonitorFromWindow(screenContainer.mainWinHandle, w32.MONITOR_DEFAULTTONEAREST)
currentMonInfo, currErr := GetMonitorInfo(currentMonHndl)
if currErr != nil {
screenContainer.errors = append(screenContainer.errors, currErr)
screenContainer.monitors = append(screenContainer.monitors, Screen{})
// not sure what the consequences of returning false are, so let's just return true and handle it ourselves
return w32.TRUE
}
monInfo, err := GetMonitorInfo(hMonitor)
if err != nil {
screenContainer.errors = append(screenContainer.errors, err)
screenContainer.monitors = append(screenContainer.monitors, Screen{})
return w32.TRUE
}
width := lprcMonitor.Right - lprcMonitor.Left
height := lprcMonitor.Bottom - lprcMonitor.Top
ourMonitorData.IsPrimary = monInfo.DwFlags&w32.MONITORINFOF_PRIMARY == 1
ourMonitorData.Height = int(height)
ourMonitorData.Width = int(width)
ourMonitorData.IsCurrent = MonitorsEqual(*currentMonInfo, *monInfo)
ourMonitorData.PhysicalSize.Width = int(width)
ourMonitorData.PhysicalSize.Height = int(height)
var dpiX, dpiY uint
w32.GetDPIForMonitor(hMonitor, w32.MDT_EFFECTIVE_DPI, &dpiX, &dpiY)
if dpiX == 0 || dpiY == 0 {
screenContainer.errors = append(screenContainer.errors, fmt.Errorf("unable to get DPI for screen"))
screenContainer.monitors = append(screenContainer.monitors, Screen{})
return w32.TRUE
}
ourMonitorData.Size.Width = winc.ScaleToDefaultDPI(ourMonitorData.PhysicalSize.Width, dpiX)
ourMonitorData.Size.Height = winc.ScaleToDefaultDPI(ourMonitorData.PhysicalSize.Height, dpiY)
// the reason we need a container is that we have don't know how many times this function will be called
// this "append" call could potentially do an allocation and rewrite the pointer to monitors. So we save the pointer in screenContainer.monitors
// and retrieve the values after all EnumProc calls
// If EnumProc is multi-threaded, this could be problematic. Although, I don't think it is.
screenContainer.monitors = append(screenContainer.monitors, ourMonitorData)
// let's keep screenContainer.errors the same size as screenContainer.monitors in case we want to match them up later if necessary
screenContainer.errors = append(screenContainer.errors, nil)
return w32.TRUE
}
type ScreenContainer struct {
monitors []Screen
errors []error
mainWinHandle w32.HWND
}
func GetAllScreens(mainWinHandle w32.HWND) ([]Screen, error) {
// TODO fix hack of container sharing by having a proper data sharing mechanism between windows and the runtime
monitorContainer := ScreenContainer{mainWinHandle: mainWinHandle}
returnErr := error(nil)
errorStrings := []string{}
dc := w32.GetDC(0)
defer w32.ReleaseDC(0, dc)
succeeded := w32.EnumDisplayMonitors(dc, nil, syscall.NewCallback(EnumProc), unsafe.Pointer(&monitorContainer))
if !succeeded {
return monitorContainer.monitors, errors.New("Windows call to EnumDisplayMonitors failed")
}
for idx, err := range monitorContainer.errors {
if err != nil {
errorStrings = append(errorStrings, fmt.Sprintf("Error from monitor #%v, %v", idx+1, err))
}
}
if len(errorStrings) > 0 {
returnErr = fmt.Errorf("%v errors encountered: %v", len(errorStrings), errorStrings)
}
return monitorContainer.monitors, returnErr
}

View File

@@ -0,0 +1,136 @@
//go:build windows
package windows
import (
"encoding/json"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
"github.com/wailsapp/wails/v2/pkg/options"
"golang.org/x/sys/windows"
"log"
"os"
"syscall"
"unsafe"
)
type COPYDATASTRUCT struct {
dwData uintptr
cbData uint32
lpData uintptr
}
// WMCOPYDATA_SINGLE_INSTANCE_DATA we define our own type for WM_COPYDATA message
const WMCOPYDATA_SINGLE_INSTANCE_DATA = 1542
func SendMessage(hwnd w32.HWND, data string) {
arrUtf16, _ := syscall.UTF16FromString(data)
pCopyData := new(COPYDATASTRUCT)
pCopyData.dwData = WMCOPYDATA_SINGLE_INSTANCE_DATA
pCopyData.cbData = uint32(len(arrUtf16)*2 + 1)
pCopyData.lpData = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(data)))
w32.SendMessage(hwnd, w32.WM_COPYDATA, 0, uintptr(unsafe.Pointer(pCopyData)))
}
// SetupSingleInstance single instance Windows app
func SetupSingleInstance(uniqueId string) {
id := "wails-app-" + uniqueId
className := id + "-sic"
windowName := id + "-siw"
mutexName := id + "sim"
_, err := windows.CreateMutex(nil, false, windows.StringToUTF16Ptr(mutexName))
if err != nil {
if err == windows.ERROR_ALREADY_EXISTS {
// app is already running
hwnd := w32.FindWindowW(windows.StringToUTF16Ptr(className), windows.StringToUTF16Ptr(windowName))
if hwnd != 0 {
data := options.SecondInstanceData{
Args: os.Args[1:],
}
data.WorkingDirectory, err = os.Getwd()
if err != nil {
log.Printf("Failed to get working directory: %v", err)
return
}
serialized, err := json.Marshal(data)
if err != nil {
log.Printf("Failed to marshal data: %v", err)
return
}
SendMessage(hwnd, string(serialized))
// exit second instance of app after sending message
os.Exit(0)
}
// if we got any other unknown error we will just start new application instance
}
} else {
createEventTargetWindow(className, windowName)
}
}
func createEventTargetWindow(className string, windowName string) w32.HWND {
// callback handler in the event target window
wndProc := func(
hwnd w32.HWND, msg uint32, wparam w32.WPARAM, lparam w32.LPARAM,
) w32.LRESULT {
if msg == w32.WM_COPYDATA {
ldata := (*COPYDATASTRUCT)(unsafe.Pointer(lparam))
if ldata.dwData == WMCOPYDATA_SINGLE_INSTANCE_DATA {
serialized := windows.UTF16PtrToString((*uint16)(unsafe.Pointer(ldata.lpData)))
var secondInstanceData options.SecondInstanceData
err := json.Unmarshal([]byte(serialized), &secondInstanceData)
if err == nil {
secondInstanceBuffer <- secondInstanceData
}
}
return w32.LRESULT(0)
}
return w32.DefWindowProc(hwnd, msg, wparam, lparam)
}
var class w32.WNDCLASSEX
class.Size = uint32(unsafe.Sizeof(class))
class.Style = 0
class.WndProc = syscall.NewCallback(wndProc)
class.ClsExtra = 0
class.WndExtra = 0
class.Instance = w32.GetModuleHandle("")
class.Icon = 0
class.Cursor = 0
class.Background = 0
class.MenuName = nil
class.ClassName = windows.StringToUTF16Ptr(className)
class.IconSm = 0
w32.RegisterClassEx(&class)
// create event window that will not be visible for user
hwnd := w32.CreateWindowEx(
0,
windows.StringToUTF16Ptr(className),
windows.StringToUTF16Ptr(windowName),
0,
0,
0,
0,
0,
w32.HWND_MESSAGE,
0,
w32.GetModuleHandle(""),
nil,
)
return hwnd
}

View File

@@ -0,0 +1,67 @@
//go:build windows
package windows
import (
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32"
"github.com/wailsapp/wails/v2/pkg/options/windows"
)
func (w *Window) UpdateTheme() {
// Don't redraw theme if nothing has changed
if !w.themeChanged {
return
}
w.themeChanged = false
if win32.IsCurrentlyHighContrastMode() {
return
}
if !win32.SupportsThemes() {
return
}
var isDarkMode bool
switch w.theme {
case windows.SystemDefault:
isDarkMode = win32.IsCurrentlyDarkMode()
case windows.Dark:
isDarkMode = true
case windows.Light:
isDarkMode = false
}
win32.SetTheme(w.Handle(), isDarkMode)
// Custom theme processing
winOptions := w.frontendOptions.Windows
var customTheme *windows.ThemeSettings
if winOptions != nil {
customTheme = winOptions.CustomTheme
}
// Custom theme
if win32.SupportsCustomThemes() && customTheme != nil {
if w.isActive {
if isDarkMode {
win32.SetTitleBarColour(w.Handle(), customTheme.DarkModeTitleBar)
win32.SetTitleTextColour(w.Handle(), customTheme.DarkModeTitleText)
win32.SetBorderColour(w.Handle(), customTheme.DarkModeBorder)
} else {
win32.SetTitleBarColour(w.Handle(), customTheme.LightModeTitleBar)
win32.SetTitleTextColour(w.Handle(), customTheme.LightModeTitleText)
win32.SetBorderColour(w.Handle(), customTheme.LightModeBorder)
}
} else {
if isDarkMode {
win32.SetTitleBarColour(w.Handle(), customTheme.DarkModeTitleBarInactive)
win32.SetTitleTextColour(w.Handle(), customTheme.DarkModeTitleTextInactive)
win32.SetBorderColour(w.Handle(), customTheme.DarkModeBorderInactive)
} else {
win32.SetTitleBarColour(w.Handle(), customTheme.LightModeTitleBarInactive)
win32.SetTitleTextColour(w.Handle(), customTheme.LightModeTitleTextInactive)
win32.SetBorderColour(w.Handle(), customTheme.LightModeBorderInactive)
}
}
}
}

View File

@@ -0,0 +1,143 @@
//go:build windows
/*
* Based on code originally from https://github.com/atotto/clipboard. Copyright (c) 2013 Ato Araki. All rights reserved.
*/
package win32
import (
"runtime"
"syscall"
"time"
"unsafe"
)
const (
cfUnicodetext = 13
gmemMoveable = 0x0002
)
// waitOpenClipboard opens the clipboard, waiting for up to a second to do so.
func waitOpenClipboard() error {
started := time.Now()
limit := started.Add(time.Second)
var r uintptr
var err error
for time.Now().Before(limit) {
r, _, err = procOpenClipboard.Call(0)
if r != 0 {
return nil
}
time.Sleep(time.Millisecond)
}
return err
}
func GetClipboardText() (string, error) {
// LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
// Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if formatAvailable, _, err := procIsClipboardFormatAvailable.Call(cfUnicodetext); formatAvailable == 0 {
return "", err
}
err := waitOpenClipboard()
if err != nil {
return "", err
}
h, _, err := procGetClipboardData.Call(cfUnicodetext)
if h == 0 {
_, _, _ = procCloseClipboard.Call()
return "", err
}
l, _, err := kernelGlobalLock.Call(h)
if l == 0 {
_, _, _ = procCloseClipboard.Call()
return "", err
}
text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:])
r, _, err := kernelGlobalUnlock.Call(h)
if r == 0 {
_, _, _ = procCloseClipboard.Call()
return "", err
}
closed, _, err := procCloseClipboard.Call()
if closed == 0 {
return "", err
}
return text, nil
}
func SetClipboardText(text string) error {
// LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
// Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
err := waitOpenClipboard()
if err != nil {
return err
}
r, _, err := procEmptyClipboard.Call(0)
if r == 0 {
_, _, _ = procCloseClipboard.Call()
return err
}
data, err := syscall.UTF16FromString(text)
if err != nil {
return err
}
// "If the hMem parameter identifies a memory object, the object must have
// been allocated using the function with the GMEM_MOVEABLE flag."
h, _, err := kernelGlobalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
if h == 0 {
_, _, _ = procCloseClipboard.Call()
return err
}
defer func() {
if h != 0 {
kernelGlobalFree.Call(h)
}
}()
l, _, err := kernelGlobalLock.Call(h)
if l == 0 {
_, _, _ = procCloseClipboard.Call()
return err
}
r, _, err = kernelLstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0])))
if r == 0 {
_, _, _ = procCloseClipboard.Call()
return err
}
r, _, err = kernelGlobalUnlock.Call(h)
if r == 0 {
if err.(syscall.Errno) != 0 {
_, _, _ = procCloseClipboard.Call()
return err
}
}
r, _, err = procSetClipboardData.Call(cfUnicodetext, h)
if r == 0 {
_, _, _ = procCloseClipboard.Call()
return err
}
h = 0 // suppress deferred cleanup
closed, _, err := procCloseClipboard.Call()
if closed == 0 {
return err
}
return nil
}

View File

@@ -0,0 +1,57 @@
//go:build windows
package win32
import (
"syscall"
"github.com/wailsapp/wails/v2/internal/system/operatingsystem"
)
type HRESULT int32
type HANDLE uintptr
type HMONITOR HANDLE
var (
moduser32 = syscall.NewLazyDLL("user32.dll")
procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW")
procGetWindowLong = moduser32.NewProc("GetWindowLongW")
procSetClassLong = moduser32.NewProc("SetClassLongW")
procSetClassLongPtr = moduser32.NewProc("SetClassLongPtrW")
procShowWindow = moduser32.NewProc("ShowWindow")
procIsWindowVisible = moduser32.NewProc("IsWindowVisible")
procGetWindowRect = moduser32.NewProc("GetWindowRect")
procGetMonitorInfo = moduser32.NewProc("GetMonitorInfoW")
procMonitorFromWindow = moduser32.NewProc("MonitorFromWindow")
procIsClipboardFormatAvailable = moduser32.NewProc("IsClipboardFormatAvailable")
procOpenClipboard = moduser32.NewProc("OpenClipboard")
procCloseClipboard = moduser32.NewProc("CloseClipboard")
procEmptyClipboard = moduser32.NewProc("EmptyClipboard")
procGetClipboardData = moduser32.NewProc("GetClipboardData")
procSetClipboardData = moduser32.NewProc("SetClipboardData")
)
var (
moddwmapi = syscall.NewLazyDLL("dwmapi.dll")
procDwmSetWindowAttribute = moddwmapi.NewProc("DwmSetWindowAttribute")
procDwmExtendFrameIntoClientArea = moddwmapi.NewProc("DwmExtendFrameIntoClientArea")
)
var (
modwingdi = syscall.NewLazyDLL("gdi32.dll")
procCreateSolidBrush = modwingdi.NewProc("CreateSolidBrush")
)
var (
kernel32 = syscall.NewLazyDLL("kernel32")
kernelGlobalAlloc = kernel32.NewProc("GlobalAlloc")
kernelGlobalFree = kernel32.NewProc("GlobalFree")
kernelGlobalLock = kernel32.NewProc("GlobalLock")
kernelGlobalUnlock = kernel32.NewProc("GlobalUnlock")
kernelLstrcpy = kernel32.NewProc("lstrcpyW")
)
var windowsVersion, _ = operatingsystem.GetWindowsVersionInfo()
func IsWindowsVersionAtLeast(major, minor, buildNumber int) bool {
return windowsVersion.Major >= major &&
windowsVersion.Minor >= minor &&
windowsVersion.Build >= buildNumber
}

View File

@@ -0,0 +1,119 @@
//go:build windows
package win32
import (
"unsafe"
"golang.org/x/sys/windows/registry"
)
type DWMWINDOWATTRIBUTE int32
const DwmwaUseImmersiveDarkModeBefore20h1 DWMWINDOWATTRIBUTE = 19
const DwmwaUseImmersiveDarkMode DWMWINDOWATTRIBUTE = 20
const DwmwaBorderColor DWMWINDOWATTRIBUTE = 34
const DwmwaCaptionColor DWMWINDOWATTRIBUTE = 35
const DwmwaTextColor DWMWINDOWATTRIBUTE = 36
const DwmwaSystemBackdropType DWMWINDOWATTRIBUTE = 38
const SPI_GETHIGHCONTRAST = 0x0042
const HCF_HIGHCONTRASTON = 0x00000001
// BackdropType defines the type of translucency we wish to use
type BackdropType int32
func dwmSetWindowAttribute(hwnd uintptr, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute unsafe.Pointer, cbAttribute uintptr) {
ret, _, err := procDwmSetWindowAttribute.Call(
hwnd,
uintptr(dwAttribute),
uintptr(pvAttribute),
cbAttribute)
if ret != 0 {
_ = err
// println(err.Error())
}
}
func SupportsThemes() bool {
// We can't support Windows versions before 17763
return IsWindowsVersionAtLeast(10, 0, 17763)
}
func SupportsCustomThemes() bool {
return IsWindowsVersionAtLeast(10, 0, 17763)
}
func SupportsBackdropTypes() bool {
return IsWindowsVersionAtLeast(10, 0, 22621)
}
func SupportsImmersiveDarkMode() bool {
return IsWindowsVersionAtLeast(10, 0, 18985)
}
func SetTheme(hwnd uintptr, useDarkMode bool) {
if SupportsThemes() {
attr := DwmwaUseImmersiveDarkModeBefore20h1
if SupportsImmersiveDarkMode() {
attr = DwmwaUseImmersiveDarkMode
}
var winDark int32
if useDarkMode {
winDark = 1
}
dwmSetWindowAttribute(hwnd, attr, unsafe.Pointer(&winDark), unsafe.Sizeof(winDark))
}
}
func EnableTranslucency(hwnd uintptr, backdrop BackdropType) {
if SupportsBackdropTypes() {
dwmSetWindowAttribute(hwnd, DwmwaSystemBackdropType, unsafe.Pointer(&backdrop), unsafe.Sizeof(backdrop))
} else {
println("Warning: Translucency type unavailable on Windows < 22621")
}
}
func SetTitleBarColour(hwnd uintptr, titleBarColour int32) {
dwmSetWindowAttribute(hwnd, DwmwaCaptionColor, unsafe.Pointer(&titleBarColour), unsafe.Sizeof(titleBarColour))
}
func SetTitleTextColour(hwnd uintptr, titleTextColour int32) {
dwmSetWindowAttribute(hwnd, DwmwaTextColor, unsafe.Pointer(&titleTextColour), unsafe.Sizeof(titleTextColour))
}
func SetBorderColour(hwnd uintptr, titleBorderColour int32) {
dwmSetWindowAttribute(hwnd, DwmwaBorderColor, unsafe.Pointer(&titleBorderColour), unsafe.Sizeof(titleBorderColour))
}
func IsCurrentlyDarkMode() bool {
key, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE)
if err != nil {
return false
}
defer key.Close()
AppsUseLightTheme, _, err := key.GetIntegerValue("AppsUseLightTheme")
if err != nil {
return false
}
return AppsUseLightTheme == 0
}
type highContrast struct {
CbSize uint32
DwFlags uint32
LpszDefaultScheme *int16
}
func IsCurrentlyHighContrastMode() bool {
var result highContrast
result.CbSize = uint32(unsafe.Sizeof(result))
res, _, err := procSystemParametersInfo.Call(SPI_GETHIGHCONTRAST, uintptr(result.CbSize), uintptr(unsafe.Pointer(&result)), 0)
if res == 0 {
_ = err
return false
}
r := result.DwFlags&HCF_HIGHCONTRASTON == HCF_HIGHCONTRASTON
return r
}

View File

@@ -0,0 +1,223 @@
//go:build windows
package win32
import (
"fmt"
"log"
"strconv"
"syscall"
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc"
)
const (
WS_MAXIMIZE = 0x01000000
WS_MINIMIZE = 0x20000000
GWL_STYLE = -16
MONITOR_DEFAULTTOPRIMARY = 0x00000001
)
const (
SW_HIDE = 0
SW_NORMAL = 1
SW_SHOWNORMAL = 1
SW_SHOWMINIMIZED = 2
SW_MAXIMIZE = 3
SW_SHOWMAXIMIZED = 3
SW_SHOWNOACTIVATE = 4
SW_SHOW = 5
SW_MINIMIZE = 6
SW_SHOWMINNOACTIVE = 7
SW_SHOWNA = 8
SW_RESTORE = 9
SW_SHOWDEFAULT = 10
SW_FORCEMINIMIZE = 11
)
const (
GCLP_HBRBACKGROUND int32 = -10
)
// Power
const (
// WM_POWERBROADCAST - Notifies applications that a power-management event has occurred.
WM_POWERBROADCAST = 536
// PBT_APMPOWERSTATUSCHANGE - Power status has changed.
PBT_APMPOWERSTATUSCHANGE = 10
// PBT_APMRESUMEAUTOMATIC -Operation is resuming automatically from a low-power state. This message is sent every time the system resumes.
PBT_APMRESUMEAUTOMATIC = 18
// PBT_APMRESUMESUSPEND - Operation is resuming from a low-power state. This message is sent after PBT_APMRESUMEAUTOMATIC if the resume is triggered by user input, such as pressing a key.
PBT_APMRESUMESUSPEND = 7
// PBT_APMSUSPEND - System is suspending operation.
PBT_APMSUSPEND = 4
// PBT_POWERSETTINGCHANGE - A power setting change event has been received.
PBT_POWERSETTINGCHANGE = 32787
)
// http://msdn.microsoft.com/en-us/library/windows/desktop/bb773244.aspx
type MARGINS struct {
CxLeftWidth, CxRightWidth, CyTopHeight, CyBottomHeight int32
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162897.aspx
type RECT struct {
Left, Top, Right, Bottom int32
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd145065.aspx
type MONITORINFO struct {
CbSize uint32
RcMonitor RECT
RcWork RECT
DwFlags uint32
}
func ExtendFrameIntoClientArea(hwnd uintptr, extend bool) {
// -1: Adds the default frame styling (aero shadow and e.g. rounded corners on Windows 11)
// Also shows the caption buttons if transparent ant translucent but they don't work.
// 0: Adds the default frame styling but no aero shadow, does not show the caption buttons.
// 1: Adds the default frame styling (aero shadow and e.g. rounded corners on Windows 11) but no caption buttons
// are shown if transparent ant translucent.
var margins MARGINS
if extend {
margins = MARGINS{1, 1, 1, 1} // Only extend 1 pixel to have the default frame styling but no caption buttons
}
if err := dwmExtendFrameIntoClientArea(hwnd, &margins); err != nil {
log.Fatal(fmt.Errorf("DwmExtendFrameIntoClientArea failed: %s", err))
}
}
func IsVisible(hwnd uintptr) bool {
ret, _, _ := procIsWindowVisible.Call(hwnd)
return ret != 0
}
func IsWindowFullScreen(hwnd uintptr) bool {
wRect := GetWindowRect(hwnd)
m := MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY)
var mi MONITORINFO
mi.CbSize = uint32(unsafe.Sizeof(mi))
if !GetMonitorInfo(m, &mi) {
return false
}
return wRect.Left == mi.RcMonitor.Left &&
wRect.Top == mi.RcMonitor.Top &&
wRect.Right == mi.RcMonitor.Right &&
wRect.Bottom == mi.RcMonitor.Bottom
}
func IsWindowMaximised(hwnd uintptr) bool {
style := uint32(getWindowLong(hwnd, GWL_STYLE))
return style&WS_MAXIMIZE != 0
}
func IsWindowMinimised(hwnd uintptr) bool {
style := uint32(getWindowLong(hwnd, GWL_STYLE))
return style&WS_MINIMIZE != 0
}
func RestoreWindow(hwnd uintptr) {
showWindow(hwnd, SW_RESTORE)
}
func ShowWindow(hwnd uintptr) {
showWindow(hwnd, SW_SHOW)
}
func ShowWindowMaximised(hwnd uintptr) {
showWindow(hwnd, SW_MAXIMIZE)
}
func ShowWindowMinimised(hwnd uintptr) {
showWindow(hwnd, SW_MINIMIZE)
}
func SetBackgroundColour(hwnd uintptr, r, g, b uint8) {
col := winc.RGB(r, g, b)
hbrush, _, _ := procCreateSolidBrush.Call(uintptr(col))
setClassLongPtr(hwnd, GCLP_HBRBACKGROUND, hbrush)
}
func IsWindowNormal(hwnd uintptr) bool {
return !IsWindowMaximised(hwnd) && !IsWindowMinimised(hwnd) && !IsWindowFullScreen(hwnd)
}
func dwmExtendFrameIntoClientArea(hwnd uintptr, margins *MARGINS) error {
ret, _, _ := procDwmExtendFrameIntoClientArea.Call(
hwnd,
uintptr(unsafe.Pointer(margins)))
if ret != 0 {
return syscall.GetLastError()
}
return nil
}
func setClassLongPtr(hwnd uintptr, param int32, val uintptr) bool {
proc := procSetClassLongPtr
if strconv.IntSize == 32 {
/*
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclasslongptrw
Note: To write code that is compatible with both 32-bit and 64-bit Windows, use SetClassLongPtr.
When compiling for 32-bit Windows, SetClassLongPtr is defined as a call to the SetClassLong function
=> We have to do this dynamically when directly calling the DLL procedures
*/
proc = procSetClassLong
}
ret, _, _ := proc.Call(
hwnd,
uintptr(param),
val,
)
return ret != 0
}
func getWindowLong(hwnd uintptr, index int) int32 {
ret, _, _ := procGetWindowLong.Call(
hwnd,
uintptr(index))
return int32(ret)
}
func showWindow(hwnd uintptr, cmdshow int) bool {
ret, _, _ := procShowWindow.Call(
hwnd,
uintptr(cmdshow))
return ret != 0
}
func GetWindowRect(hwnd uintptr) *RECT {
var rect RECT
procGetWindowRect.Call(
hwnd,
uintptr(unsafe.Pointer(&rect)))
return &rect
}
func MonitorFromWindow(hwnd uintptr, dwFlags uint32) HMONITOR {
ret, _, _ := procMonitorFromWindow.Call(
hwnd,
uintptr(dwFlags),
)
return HMONITOR(ret)
}
func GetMonitorInfo(hMonitor HMONITOR, lmpi *MONITORINFO) bool {
ret, _, _ := procGetMonitorInfo.Call(
uintptr(hMonitor),
uintptr(unsafe.Pointer(lmpi)),
)
return ret != 0
}

View File

@@ -0,0 +1,12 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

View File

@@ -0,0 +1,12 @@
# This is the official list of 'Winc' authors for copyright purposes.
# Names should be added to this file as
# Name or Organization <email address>
# The email address is not required for organizations.
# Please keep the list sorted.
# Contributors
# ============
Tad Vizbaras <tad@etasoft.com>

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 winc Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,181 @@
# winc
** This is a fork of [tadvi/winc](https://github.com/tadvi/winc) for the sole purpose of integration
with [Wails](https://github.com/wailsapp/wails). This repository comes with ***no support*** **
Common library for Go GUI apps on Windows. It is for Windows OS only. This makes library smaller than some other UI
libraries for Go.
Design goals: minimalism and simplicity.
## Dependencies
No other dependencies except Go standard library.
## Building
If you want to package icon files and other resources into binary **rsrc** tool is recommended:
rsrc -manifest app.manifest -ico=app.ico,application_edit.ico,application_error.ico -o rsrc.syso
Here app.manifest is XML file in format:
```
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="App" type="win32"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
</assembly>
```
Most Windows applications do not display command prompt. Build your Go project with flag to indicate that it is Windows
GUI binary:
go build -ldflags="-H windowsgui"
## Samples
Best way to learn how to use the library is to look at the included **examples** projects.
## Setup
1. Make sure you have a working Go installation and build environment, see more for details on page below.
http://golang.org/doc/install
2. go get github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc
## Icons
When rsrc is used to pack icons into binary it displays IDs of the packed icons.
```
rsrc -manifest app.manifest -ico=app.ico,lightning.ico,edit.ico,application_error.ico -o rsrc.syso
Manifest ID: 1
Icon app.ico ID: 10
Icon lightning.ico ID: 13
Icon edit.ico ID: 16
Icon application_error.ico ID: 19
```
Use IDs to reference packed icons.
```
const myIcon = 13
btn.SetResIcon(myIcon) // Set icon on the button.
```
Included source **examples** use basic building via `release.bat` files. Note that icon IDs are order dependent. So if
you change they order in -ico flag then icon IDs will be different. If you want to keep order the same, just add new
icons to the end of -ico comma separated list.
## Layout Manager
SimpleDock is default layout manager.
Current design of docking and split views allows building simple apps but if you need to have multiple split views in
few different directions you might need to create your own layout manager.
Important point is to have **one** control inside SimpleDock set to dock as **Fill**. Controls that are not set to any
docking get placed using SetPos() function. So you can have Panel set to dock at the Top and then have another dock to
arrange controls inside that Panel or have controls placed using SetPos() at fixed positions.
![Example layout with two toolbars and status bar](dock_topbottom.png)
This is basic layout. Instead of toolbars and status bar you can have Panel or any other control that can resize. Panel
can have its own internal Dock that will arrange other controls inside of it.
![Example layout with two toolbars and navigation on the left](dock_topleft.png)
This is layout with extra control(s) on the left. Left side is usually treeview or listview.
The rule is simple: you either dock controls using SimpleDock OR use SetPos() to set them at fixed positions. That's it.
At some point **winc** may get more sophisticated layout manager.
## Dialog Screens
Dialog screens are not based on Windows resource files (.rc). They are just windows with controls placed at fixed
coordinates. This works fine for dialog screens up to 10-14 controls.
# Minimal Demo
```
package main
import (
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc"
)
func main() {
mainWindow := winc.NewForm(nil)
mainWindow.SetSize(400, 300) // (width, height)
mainWindow.SetText("Hello World Demo")
edt := winc.NewEdit(mainWindow)
edt.SetPos(10, 20)
// Most Controls have default size unless SetSize is called.
edt.SetText("edit text")
btn := winc.NewPushButton(mainWindow)
btn.SetText("Show or Hide")
btn.SetPos(40, 50) // (x, y)
btn.SetSize(100, 40) // (width, height)
btn.OnClick().Bind(func(e *winc.Event) {
if edt.Visible() {
edt.Hide()
} else {
edt.Show()
}
})
mainWindow.Center()
mainWindow.Show()
mainWindow.OnClose().Bind(wndOnClose)
winc.RunMainLoop() // Must call to start event loop.
}
func wndOnClose(arg *winc.Event) {
winc.Exit()
}
```
![Hello World](examples/hello.png)
Result of running sample_minimal.
## Create Your Own
It is good practice to create your own controls based on existing structures and event model. Library contains some of
the controls built that way: IconButton (button.go), ErrorPanel (panel.go), MultiEdit (edit.go), etc. Please look at
existing controls as examples before building your own.
When designing your own controls keep in mind that types have to be converted from Go into Win32 API and back. This is
usually due to string UTF8 and UTF16 conversions. But there are other types of conversions too.
When developing your own controls you might also need to:
import "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
w32 has Win32 API low level constants and functions.
Look at **sample_control** for example of custom built window.
## Companion Package
[Go package for Windows Systray icon, menu and notifications](https://github.com/tadvi/systray)
## Credits
This library is built on
[AllenDang/gform Windows GUI framework for Go](https://github.com/AllenDang/gform)
**winc** takes most design decisions from **gform** and adds many more controls and code samples to it.

View File

@@ -0,0 +1,109 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
* Copyright (C) 2010-2013 Allen Dang. All Rights Reserved.
*/
package winc
import (
"runtime"
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
var (
// resource compilation tool assigns app.ico ID of 3
// rsrc -manifest app.manifest -ico app.ico -o rsrc.syso
AppIconID = 3
)
func init() {
runtime.LockOSThread()
gAppInstance = w32.GetModuleHandle("")
if gAppInstance == 0 {
panic("Error occurred in App.Init")
}
// Initialize the common controls
var initCtrls w32.INITCOMMONCONTROLSEX
initCtrls.DwSize = uint32(unsafe.Sizeof(initCtrls))
initCtrls.DwICC =
w32.ICC_LISTVIEW_CLASSES | w32.ICC_PROGRESS_CLASS | w32.ICC_TAB_CLASSES |
w32.ICC_TREEVIEW_CLASSES | w32.ICC_BAR_CLASSES
w32.InitCommonControlsEx(&initCtrls)
}
// SetAppIcon sets resource icon ID for the apps windows.
func SetAppIcon(appIconID int) {
AppIconID = appIconID
}
func GetAppInstance() w32.HINSTANCE {
return gAppInstance
}
func PreTranslateMessage(msg *w32.MSG) bool {
// This functions is called by the MessageLoop. It processes the
// keyboard accelerator keys and calls Controller.PreTranslateMessage for
// keyboard and mouse events.
processed := false
if (msg.Message >= w32.WM_KEYFIRST && msg.Message <= w32.WM_KEYLAST) ||
(msg.Message >= w32.WM_MOUSEFIRST && msg.Message <= w32.WM_MOUSELAST) {
if msg.Hwnd != 0 {
if controller := GetMsgHandler(msg.Hwnd); controller != nil {
// Search the chain of parents for pretranslated messages.
for p := controller; p != nil; p = p.Parent() {
if processed = p.PreTranslateMessage(msg); processed {
break
}
}
}
}
}
return processed
}
// RunMainLoop processes messages in main application loop.
func RunMainLoop() int {
m := (*w32.MSG)(unsafe.Pointer(w32.GlobalAlloc(0, uint32(unsafe.Sizeof(w32.MSG{})))))
defer w32.GlobalFree(w32.HGLOBAL(unsafe.Pointer(m)))
for w32.GetMessage(m, 0, 0, 0) != 0 {
if !PreTranslateMessage(m) {
w32.TranslateMessage(m)
w32.DispatchMessage(m)
}
}
w32.GdiplusShutdown()
return int(m.WParam)
}
// PostMessages processes recent messages. Sometimes helpful for instant window refresh.
func PostMessages() {
m := (*w32.MSG)(unsafe.Pointer(w32.GlobalAlloc(0, uint32(unsafe.Sizeof(w32.MSG{})))))
defer w32.GlobalFree(w32.HGLOBAL(unsafe.Pointer(m)))
for i := 0; i < 10; i++ {
if w32.GetMessage(m, 0, 0, 0) != 0 {
if !PreTranslateMessage(m) {
w32.TranslateMessage(m)
w32.DispatchMessage(m)
}
}
}
}
func Exit() {
w32.PostQuitMessage(0)
}

View File

@@ -0,0 +1,112 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
* Copyright (C) 2010-2013 Allen Dang. All Rights Reserved.
*/
package winc
import (
"errors"
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
type Bitmap struct {
handle w32.HBITMAP
width, height int
}
func assembleBitmapFromHBITMAP(hbitmap w32.HBITMAP) (*Bitmap, error) {
var dib w32.DIBSECTION
if w32.GetObject(w32.HGDIOBJ(hbitmap), unsafe.Sizeof(dib), unsafe.Pointer(&dib)) == 0 {
return nil, errors.New("GetObject for HBITMAP failed")
}
return &Bitmap{
handle: hbitmap,
width: int(dib.DsBmih.BiWidth),
height: int(dib.DsBmih.BiHeight),
}, nil
}
func NewBitmapFromFile(filepath string, background Color) (*Bitmap, error) {
var gpBitmap *uintptr
var err error
gpBitmap, err = w32.GdipCreateBitmapFromFile(filepath)
if err != nil {
return nil, err
}
defer w32.GdipDisposeImage(gpBitmap)
var hbitmap w32.HBITMAP
// Reverse RGB to BGR to satisfy gdiplus color schema.
hbitmap, err = w32.GdipCreateHBITMAPFromBitmap(gpBitmap, uint32(RGB(background.B(), background.G(), background.R())))
if err != nil {
return nil, err
}
return assembleBitmapFromHBITMAP(hbitmap)
}
func NewBitmapFromResource(instance w32.HINSTANCE, resName *uint16, resType *uint16, background Color) (*Bitmap, error) {
var gpBitmap *uintptr
var err error
var hRes w32.HRSRC
hRes, err = w32.FindResource(w32.HMODULE(instance), resName, resType)
if err != nil {
return nil, err
}
resSize := w32.SizeofResource(w32.HMODULE(instance), hRes)
pResData := w32.LockResource(w32.LoadResource(w32.HMODULE(instance), hRes))
resBuffer := w32.GlobalAlloc(w32.GMEM_MOVEABLE, resSize)
pResBuffer := w32.GlobalLock(resBuffer)
w32.MoveMemory(pResBuffer, pResData, resSize)
stream := w32.CreateStreamOnHGlobal(resBuffer, false)
gpBitmap, err = w32.GdipCreateBitmapFromStream(stream)
if err != nil {
return nil, err
}
defer stream.Release()
defer w32.GlobalUnlock(resBuffer)
defer w32.GlobalFree(resBuffer)
defer w32.GdipDisposeImage(gpBitmap)
var hbitmap w32.HBITMAP
// Reverse gform.RGB to BGR to satisfy gdiplus color schema.
hbitmap, err = w32.GdipCreateHBITMAPFromBitmap(gpBitmap, uint32(RGB(background.B(), background.G(), background.R())))
if err != nil {
return nil, err
}
return assembleBitmapFromHBITMAP(hbitmap)
}
func (bm *Bitmap) Dispose() {
if bm.handle != 0 {
w32.DeleteObject(w32.HGDIOBJ(bm.handle))
bm.handle = 0
}
}
func (bm *Bitmap) GetHBITMAP() w32.HBITMAP {
return bm.handle
}
func (bm *Bitmap) Size() (int, int) {
return bm.width, bm.height
}
func (bm *Bitmap) Height() int {
return bm.height
}
func (bm *Bitmap) Width() int {
return bm.width
}

View File

@@ -0,0 +1,74 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
* Copyright (C) 2010-2013 Allen Dang. All Rights Reserved.
*/
package winc
import (
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
var DefaultBackgroundBrush = NewSystemColorBrush(w32.COLOR_BTNFACE)
type Brush struct {
hBrush w32.HBRUSH
logBrush w32.LOGBRUSH
}
func NewSolidColorBrush(color Color) *Brush {
lb := w32.LOGBRUSH{LbStyle: w32.BS_SOLID, LbColor: w32.COLORREF(color)}
hBrush := w32.CreateBrushIndirect(&lb)
if hBrush == 0 {
panic("Faild to create solid color brush")
}
return &Brush{hBrush, lb}
}
func NewSystemColorBrush(colorIndex int) *Brush {
//lb := w32.LOGBRUSH{LbStyle: w32.BS_SOLID, LbColor: w32.COLORREF(colorIndex)}
lb := w32.LOGBRUSH{LbStyle: w32.BS_NULL}
hBrush := w32.GetSysColorBrush(colorIndex)
if hBrush == 0 {
panic("GetSysColorBrush failed")
}
return &Brush{hBrush, lb}
}
func NewHatchedColorBrush(color Color) *Brush {
lb := w32.LOGBRUSH{LbStyle: w32.BS_HATCHED, LbColor: w32.COLORREF(color)}
hBrush := w32.CreateBrushIndirect(&lb)
if hBrush == 0 {
panic("Faild to create solid color brush")
}
return &Brush{hBrush, lb}
}
func NewNullBrush() *Brush {
lb := w32.LOGBRUSH{LbStyle: w32.BS_NULL}
hBrush := w32.CreateBrushIndirect(&lb)
if hBrush == 0 {
panic("Failed to create null brush")
}
return &Brush{hBrush, lb}
}
func (br *Brush) GetHBRUSH() w32.HBRUSH {
return br.hBrush
}
func (br *Brush) GetLOGBRUSH() *w32.LOGBRUSH {
return &br.logBrush
}
func (br *Brush) Dispose() {
if br.hBrush != 0 {
w32.DeleteObject(w32.HGDIOBJ(br.hBrush))
br.hBrush = 0
}
}

View File

@@ -0,0 +1,156 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
* Copyright (C) 2010-2013 Allen Dang. All Rights Reserved.
*/
package winc
import (
"fmt"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
type Button struct {
ControlBase
onClick EventManager
}
func (bt *Button) OnClick() *EventManager {
return &bt.onClick
}
func (bt *Button) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case w32.WM_COMMAND:
bt.onClick.Fire(NewEvent(bt, nil))
/*case w32.WM_LBUTTONDOWN:
w32.SetCapture(bt.Handle())
case w32.WM_LBUTTONUP:
w32.ReleaseCapture()*/
/*case win.WM_GETDLGCODE:
println("GETDLGCODE")*/
}
return w32.DefWindowProc(bt.hwnd, msg, wparam, lparam)
//return bt.W32Control.WndProc(msg, wparam, lparam)
}
func (bt *Button) Checked() bool {
result := w32.SendMessage(bt.hwnd, w32.BM_GETCHECK, 0, 0)
return result == w32.BST_CHECKED
}
func (bt *Button) SetChecked(checked bool) {
wparam := w32.BST_CHECKED
if !checked {
wparam = w32.BST_UNCHECKED
}
w32.SendMessage(bt.hwnd, w32.BM_SETCHECK, uintptr(wparam), 0)
}
// SetIcon sets icon on the button. Recommended icons are 32x32 with 32bit color depth.
func (bt *Button) SetIcon(ico *Icon) {
w32.SendMessage(bt.hwnd, w32.BM_SETIMAGE, w32.IMAGE_ICON, uintptr(ico.handle))
}
func (bt *Button) SetResIcon(iconID uint16) {
if ico, err := NewIconFromResource(GetAppInstance(), iconID); err == nil {
bt.SetIcon(ico)
return
}
panic(fmt.Sprintf("missing icon with icon ID: %d", iconID))
}
type PushButton struct {
Button
}
func NewPushButton(parent Controller) *PushButton {
pb := new(PushButton)
pb.InitControl("BUTTON", parent, 0, w32.BS_PUSHBUTTON|w32.WS_TABSTOP|w32.WS_VISIBLE|w32.WS_CHILD)
RegMsgHandler(pb)
pb.SetFont(DefaultFont)
pb.SetText("Button")
pb.SetSize(100, 22)
return pb
}
// SetDefault is used for dialogs to set default button.
func (pb *PushButton) SetDefault() {
pb.SetAndClearStyleBits(w32.BS_DEFPUSHBUTTON, w32.BS_PUSHBUTTON)
}
// IconButton does not display text, requires SetResIcon call.
type IconButton struct {
Button
}
func NewIconButton(parent Controller) *IconButton {
pb := new(IconButton)
pb.InitControl("BUTTON", parent, 0, w32.BS_ICON|w32.WS_TABSTOP|w32.WS_VISIBLE|w32.WS_CHILD)
RegMsgHandler(pb)
pb.SetFont(DefaultFont)
// even if text would be set it would not be displayed
pb.SetText("")
pb.SetSize(100, 22)
return pb
}
type CheckBox struct {
Button
}
func NewCheckBox(parent Controller) *CheckBox {
cb := new(CheckBox)
cb.InitControl("BUTTON", parent, 0, w32.WS_TABSTOP|w32.WS_VISIBLE|w32.WS_CHILD|w32.BS_AUTOCHECKBOX)
RegMsgHandler(cb)
cb.SetFont(DefaultFont)
cb.SetText("CheckBox")
cb.SetSize(100, 22)
return cb
}
type RadioButton struct {
Button
}
func NewRadioButton(parent Controller) *RadioButton {
rb := new(RadioButton)
rb.InitControl("BUTTON", parent, 0, w32.WS_TABSTOP|w32.WS_VISIBLE|w32.WS_CHILD|w32.BS_AUTORADIOBUTTON)
RegMsgHandler(rb)
rb.SetFont(DefaultFont)
rb.SetText("RadioButton")
rb.SetSize(100, 22)
return rb
}
type GroupBox struct {
Button
}
func NewGroupBox(parent Controller) *GroupBox {
gb := new(GroupBox)
gb.InitControl("BUTTON", parent, 0, w32.WS_CHILD|w32.WS_VISIBLE|w32.WS_GROUP|w32.BS_GROUPBOX)
RegMsgHandler(gb)
gb.SetFont(DefaultFont)
gb.SetText("GroupBox")
gb.SetSize(100, 100)
return gb
}

View File

@@ -0,0 +1,159 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
* Copyright (C) 2010-2013 Allen Dang. All Rights Reserved.
*/
package winc
import (
"fmt"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
type Canvas struct {
hwnd w32.HWND
hdc w32.HDC
doNotDispose bool
}
var nullBrush = NewNullBrush()
func NewCanvasFromHwnd(hwnd w32.HWND) *Canvas {
hdc := w32.GetDC(hwnd)
if hdc == 0 {
panic(fmt.Sprintf("Create canvas from %v failed.", hwnd))
}
return &Canvas{hwnd: hwnd, hdc: hdc, doNotDispose: false}
}
func NewCanvasFromHDC(hdc w32.HDC) *Canvas {
if hdc == 0 {
panic("Cannot create canvas from invalid HDC.")
}
return &Canvas{hdc: hdc, doNotDispose: true}
}
func (ca *Canvas) Dispose() {
if !ca.doNotDispose && ca.hdc != 0 {
if ca.hwnd == 0 {
w32.DeleteDC(ca.hdc)
} else {
w32.ReleaseDC(ca.hwnd, ca.hdc)
}
ca.hdc = 0
}
}
func (ca *Canvas) DrawBitmap(bmp *Bitmap, x, y int) {
cdc := w32.CreateCompatibleDC(0)
defer w32.DeleteDC(cdc)
hbmpOld := w32.SelectObject(cdc, w32.HGDIOBJ(bmp.GetHBITMAP()))
defer w32.SelectObject(cdc, w32.HGDIOBJ(hbmpOld))
w, h := bmp.Size()
w32.BitBlt(ca.hdc, x, y, w, h, cdc, 0, 0, w32.SRCCOPY)
}
func (ca *Canvas) DrawStretchedBitmap(bmp *Bitmap, rect *Rect) {
cdc := w32.CreateCompatibleDC(0)
defer w32.DeleteDC(cdc)
hbmpOld := w32.SelectObject(cdc, w32.HGDIOBJ(bmp.GetHBITMAP()))
defer w32.SelectObject(cdc, w32.HGDIOBJ(hbmpOld))
w, h := bmp.Size()
rc := rect.GetW32Rect()
w32.StretchBlt(ca.hdc, int(rc.Left), int(rc.Top), int(rc.Right), int(rc.Bottom), cdc, 0, 0, w, h, w32.SRCCOPY)
}
func (ca *Canvas) DrawIcon(ico *Icon, x, y int) bool {
return w32.DrawIcon(ca.hdc, x, y, ico.Handle())
}
// DrawFillRect draw and fill rectangle with color.
func (ca *Canvas) DrawFillRect(rect *Rect, pen *Pen, brush *Brush) {
w32Rect := rect.GetW32Rect()
previousPen := w32.SelectObject(ca.hdc, w32.HGDIOBJ(pen.GetHPEN()))
defer w32.SelectObject(ca.hdc, previousPen)
previousBrush := w32.SelectObject(ca.hdc, w32.HGDIOBJ(brush.GetHBRUSH()))
defer w32.SelectObject(ca.hdc, previousBrush)
w32.Rectangle(ca.hdc, w32Rect.Left, w32Rect.Top, w32Rect.Right, w32Rect.Bottom)
}
func (ca *Canvas) DrawRect(rect *Rect, pen *Pen) {
w32Rect := rect.GetW32Rect()
previousPen := w32.SelectObject(ca.hdc, w32.HGDIOBJ(pen.GetHPEN()))
defer w32.SelectObject(ca.hdc, previousPen)
// nullBrush is used to make interior of the rect transparent
previousBrush := w32.SelectObject(ca.hdc, w32.HGDIOBJ(nullBrush.GetHBRUSH()))
defer w32.SelectObject(ca.hdc, previousBrush)
w32.Rectangle(ca.hdc, w32Rect.Left, w32Rect.Top, w32Rect.Right, w32Rect.Bottom)
}
func (ca *Canvas) FillRect(rect *Rect, brush *Brush) {
w32.FillRect(ca.hdc, rect.GetW32Rect(), brush.GetHBRUSH())
}
func (ca *Canvas) DrawEllipse(rect *Rect, pen *Pen) {
w32Rect := rect.GetW32Rect()
previousPen := w32.SelectObject(ca.hdc, w32.HGDIOBJ(pen.GetHPEN()))
defer w32.SelectObject(ca.hdc, previousPen)
// nullBrush is used to make interior of the rect transparent
previousBrush := w32.SelectObject(ca.hdc, w32.HGDIOBJ(nullBrush.GetHBRUSH()))
defer w32.SelectObject(ca.hdc, previousBrush)
w32.Ellipse(ca.hdc, w32Rect.Left, w32Rect.Top, w32Rect.Right, w32Rect.Bottom)
}
// DrawFillEllipse draw and fill ellipse with color.
func (ca *Canvas) DrawFillEllipse(rect *Rect, pen *Pen, brush *Brush) {
w32Rect := rect.GetW32Rect()
previousPen := w32.SelectObject(ca.hdc, w32.HGDIOBJ(pen.GetHPEN()))
defer w32.SelectObject(ca.hdc, previousPen)
previousBrush := w32.SelectObject(ca.hdc, w32.HGDIOBJ(brush.GetHBRUSH()))
defer w32.SelectObject(ca.hdc, previousBrush)
w32.Ellipse(ca.hdc, w32Rect.Left, w32Rect.Top, w32Rect.Right, w32Rect.Bottom)
}
func (ca *Canvas) DrawLine(x, y, x2, y2 int, pen *Pen) {
w32.MoveToEx(ca.hdc, x, y, nil)
previousPen := w32.SelectObject(ca.hdc, w32.HGDIOBJ(pen.GetHPEN()))
defer w32.SelectObject(ca.hdc, previousPen)
w32.LineTo(ca.hdc, int32(x2), int32(y2))
}
// Refer win32 DrawText document for uFormat.
func (ca *Canvas) DrawText(text string, rect *Rect, format uint, font *Font, textColor Color) {
previousFont := w32.SelectObject(ca.hdc, w32.HGDIOBJ(font.GetHFONT()))
defer w32.SelectObject(ca.hdc, w32.HGDIOBJ(previousFont))
previousBkMode := w32.SetBkMode(ca.hdc, w32.TRANSPARENT)
defer w32.SetBkMode(ca.hdc, previousBkMode)
previousTextColor := w32.SetTextColor(ca.hdc, w32.COLORREF(textColor))
defer w32.SetTextColor(ca.hdc, previousTextColor)
w32.DrawText(ca.hdc, text, len(text), rect.GetW32Rect(), format)
}

View File

@@ -0,0 +1,26 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
* Copyright (C) 2010-2013 Allen Dang. All Rights Reserved.
*/
package winc
type Color uint32
func RGB(r, g, b byte) Color {
return Color(uint32(r) | uint32(g)<<8 | uint32(b)<<16)
}
func (c Color) R() byte {
return byte(c & 0xff)
}
func (c Color) G() byte {
return byte((c >> 8) & 0xff)
}
func (c Color) B() byte {
return byte((c >> 16) & 0xff)
}

View File

@@ -0,0 +1,70 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
*/
package winc
import (
"syscall"
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
type ComboBox struct {
ControlBase
onSelectedChange EventManager
}
func NewComboBox(parent Controller) *ComboBox {
cb := new(ComboBox)
cb.InitControl("COMBOBOX", parent, 0, w32.WS_CHILD|w32.WS_VISIBLE|w32.WS_TABSTOP|w32.WS_VSCROLL|w32.CBS_DROPDOWNLIST)
RegMsgHandler(cb)
cb.SetFont(DefaultFont)
cb.SetSize(200, 400)
return cb
}
func (cb *ComboBox) DeleteAllItems() bool {
return w32.SendMessage(cb.hwnd, w32.CB_RESETCONTENT, 0, 0) == w32.TRUE
}
func (cb *ComboBox) InsertItem(index int, str string) bool {
lp := uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(str)))
return w32.SendMessage(cb.hwnd, w32.CB_INSERTSTRING, uintptr(index), lp) != w32.CB_ERR
}
func (cb *ComboBox) DeleteItem(index int) bool {
return w32.SendMessage(cb.hwnd, w32.CB_DELETESTRING, uintptr(index), 0) != w32.CB_ERR
}
func (cb *ComboBox) SelectedItem() int {
return int(int32(w32.SendMessage(cb.hwnd, w32.CB_GETCURSEL, 0, 0)))
}
func (cb *ComboBox) SetSelectedItem(value int) bool {
return int(int32(w32.SendMessage(cb.hwnd, w32.CB_SETCURSEL, uintptr(value), 0))) == value
}
func (cb *ComboBox) OnSelectedChange() *EventManager {
return &cb.onSelectedChange
}
// Message processor
func (cb *ComboBox) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case w32.WM_COMMAND:
code := w32.HIWORD(uint32(wparam))
switch code {
case w32.CBN_SELCHANGE:
cb.onSelectedChange.Fire(NewEvent(cb, nil))
}
}
return w32.DefWindowProc(cb.hwnd, msg, wparam, lparam)
//return cb.W32Control.WndProc(msg, wparam, lparam)
}

Some files were not shown because too many files have changed in this diff Show More