From b4c669ff86b29363f5551fb1c464937e2849297e Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sun, 28 Feb 2021 22:08:23 +1100 Subject: [PATCH] Support custom protocols --- v2/internal/app/desktop.go | 14 +++ v2/internal/ffenestri/ffenestri_darwin.c | 38 +++++++- v2/internal/ffenestri/ffenestri_darwin.go | 5 + v2/internal/ffenestri/ffenestri_darwin.h | 6 ++ .../message/messageparser.go | 3 +- v2/internal/messagedispatcher/message/url.go | 20 ++++ v2/internal/subsystem/url.go | 94 +++++++++++++++++++ v2/pkg/options/mac/mac.go | 1 + 8 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 v2/internal/messagedispatcher/message/url.go create mode 100644 v2/internal/subsystem/url.go diff --git a/v2/internal/app/desktop.go b/v2/internal/app/desktop.go index 44ca8de1d..456749394 100644 --- a/v2/internal/app/desktop.go +++ b/v2/internal/app/desktop.go @@ -35,6 +35,7 @@ type App struct { //binding *subsystem.Binding call *subsystem.Call menu *subsystem.Menu + url *subsystem.URL dispatcher *messagedispatcher.Dispatcher menuManager *menumanager.Manager @@ -160,6 +161,19 @@ func (a *App) Run() error { return err } + if a.options.Mac.URLHandlers != nil { + // Start the url handler subsystem + url, err := subsystem.NewURL(a.servicebus, a.logger, a.options.Mac.URLHandlers) + if err != nil { + return err + } + a.url = url + err = a.url.Start() + if err != nil { + return err + } + } + // Start the eventing subsystem eventsubsystem, err := subsystem.NewEvent(ctx, a.servicebus, a.logger) if err != nil { diff --git a/v2/internal/ffenestri/ffenestri_darwin.c b/v2/internal/ffenestri/ffenestri_darwin.c index abee9ff83..68155f163 100644 --- a/v2/internal/ffenestri/ffenestri_darwin.c +++ b/v2/internal/ffenestri/ffenestri_darwin.c @@ -46,6 +46,15 @@ int hashmap_log(void *const context, struct hashmap_element_s *const e) { return 0; } +void filelog(const char *message) { + FILE *fp = fopen("/tmp/wailslog.txt", "ab"); + if (fp != NULL) + { + fputs(message, fp); + fclose(fp); + } +} + // Utility function to visualise a hashmap void dumpHashmap(const char *name, struct hashmap_s *hashmap) { printf("%s = { ", name); @@ -113,6 +122,7 @@ struct Application { int useToolBar; int hideToolbarSeparator; int windowBackgroundIsTranslucent; + int hasURLHandlers; // Menu Menu *applicationMenu; @@ -1143,17 +1153,31 @@ void DarkModeEnabled(struct Application *app, const char *callbackID) { ); } +void getURL(id self, SEL selector, id event, id replyEvent) { + id desc = msg(event, s("paramDescriptorForKeyword:"), keyDirectObject); + id url = msg(desc, s("stringValue")); + const char* curl = cstr(url); + const char* message = concat("UC", curl); + messageFromWindowCallback(message); + MEMFREE(message); +} + + void createDelegate(struct Application *app) { // Define delegate Class delegateClass = objc_allocateClassPair((Class) c("NSObject"), "AppDelegate", 0); bool resultAddProtoc = class_addProtocol(delegateClass, objc_getProtocol("NSApplicationDelegate")); class_addMethod(delegateClass, s("applicationShouldTerminateAfterLastWindowClosed:"), (IMP) no, "c@:@"); -// class_addMethod(delegateClass, s("applicationWillTerminate:"), (IMP) closeWindow, "v@:@"); class_addMethod(delegateClass, s("applicationWillFinishLaunching:"), (IMP) willFinishLaunching, "v@:@"); // All Menu Items use a common callback class_addMethod(delegateClass, s("menuItemCallback:"), (IMP)menuItemCallback, "v@:@"); + // If there are URL Handlers, register the callback method + if( app->hasURLHandlers ) { + class_addMethod(delegateClass, s("getUrl:withReplyEvent:"), (IMP) getURL, "i@:@@"); + } + // Script handler class_addMethod(delegateClass, s("userContentController:didReceiveScriptMessage:"), (IMP) messageHandler, "v@:@@"); objc_registerClassPair(delegateClass); @@ -1162,6 +1186,12 @@ void createDelegate(struct Application *app) { id delegate = msg((id)delegateClass, s("new")); objc_setAssociatedObject(delegate, "application", (id)app, OBJC_ASSOCIATION_ASSIGN); + // If there are URL Handlers, register a listener for them + if( app->hasURLHandlers ) { + id eventManager = msg(c("NSAppleEventManager"), s("sharedAppleEventManager")); + msg(eventManager, s("setEventHandler:andSelector:forEventClass:andEventID:"), delegate, s("getUrl:withReplyEvent:"), kInternetEventClass, kAEGetURL); + } + // Theme change listener class_addMethod(delegateClass, s("themeChanged:"), (IMP) themeChanged, "v@:@@"); @@ -1831,6 +1861,10 @@ void SetActivationPolicy(struct Application* app, int policy) { app->activationPolicy = policy; } +void HasURLHandlers(struct Application* app) { + app->hasURLHandlers = 1; +} + // Quit will stop the cocoa application and free up all the memory // used by the application void Quit(struct Application *app) { @@ -1904,6 +1938,8 @@ void* NewApplication(const char *title, int width, int height, int resizable, in result->activationPolicy = NSApplicationActivationPolicyRegular; + result->hasURLHandlers = 0; + return (void*) result; } diff --git a/v2/internal/ffenestri/ffenestri_darwin.go b/v2/internal/ffenestri/ffenestri_darwin.go index b44be0f87..bd6086dcd 100644 --- a/v2/internal/ffenestri/ffenestri_darwin.go +++ b/v2/internal/ffenestri/ffenestri_darwin.go @@ -90,5 +90,10 @@ func (a *Application) processPlatformSettings() error { } } + // Process URL Handlers + if a.config.Mac.URLHandlers != nil { + C.HasURLHandlers(a.app) + } + return nil } diff --git a/v2/internal/ffenestri/ffenestri_darwin.h b/v2/internal/ffenestri/ffenestri_darwin.h index 83219c03c..73bf15d88 100644 --- a/v2/internal/ffenestri/ffenestri_darwin.h +++ b/v2/internal/ffenestri/ffenestri_darwin.h @@ -14,6 +14,10 @@ // Macros to make it slightly more sane #define msg objc_msgSend +#define kInternetEventClass 'GURL' +#define kAEGetURL 'GURL' +#define keyDirectObject '----' + #define c(str) (id)objc_getClass(str) #define s(str) sel_registerName(str) #define u(str) sel_getUid(str) @@ -118,4 +122,6 @@ void SetActivationPolicy(struct Application* app, int policy); void* lookupStringConstant(id constantName); +void HasURLHandlers(struct Application* app); + #endif \ No newline at end of file diff --git a/v2/internal/messagedispatcher/message/messageparser.go b/v2/internal/messagedispatcher/message/messageparser.go index fd7a4ae9e..362070b0b 100644 --- a/v2/internal/messagedispatcher/message/messageparser.go +++ b/v2/internal/messagedispatcher/message/messageparser.go @@ -21,13 +21,14 @@ var messageParsers = map[byte]func(string) (*parsedMessage, error){ 'M': menuMessageParser, 'T': trayMessageParser, 'X': contextMenusMessageParser, + 'U': urlMessageParser, } // Parse will attempt to parse the given message func Parse(message string) (*parsedMessage, error) { if len(message) == 0 { - return nil, fmt.Errorf("MessageParser received blank message"); + return nil, fmt.Errorf("MessageParser received blank message") } parseMethod := messageParsers[message[0]] diff --git a/v2/internal/messagedispatcher/message/url.go b/v2/internal/messagedispatcher/message/url.go new file mode 100644 index 000000000..1bdc2f903 --- /dev/null +++ b/v2/internal/messagedispatcher/message/url.go @@ -0,0 +1,20 @@ +package message + +import "fmt" + +// urlMessageParser does what it says on the tin! +func urlMessageParser(message string) (*parsedMessage, error) { + + // Sanity check: URL messages must be at least 2 bytes + if len(message) < 2 { + return nil, fmt.Errorf("log message was an invalid length") + } + + // Switch on the log type + switch message[1] { + case 'C': + return &parsedMessage{Topic: "url:handler", Data: message[2:]}, nil + default: + return nil, fmt.Errorf("url message type '%c' invalid", message[1]) + } +} diff --git a/v2/internal/subsystem/url.go b/v2/internal/subsystem/url.go new file mode 100644 index 000000000..e8dd56b02 --- /dev/null +++ b/v2/internal/subsystem/url.go @@ -0,0 +1,94 @@ +package subsystem + +import ( + "context" + "strings" + "sync" + + "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/internal/servicebus" +) + +// URL is the URL Handler subsystem. It handles messages with topics starting +// with "url:" +type URL struct { + urlChannel <-chan *servicebus.Message + + // quit flag + shouldQuit bool + + // Logger! + logger *logger.Logger + + // Context for shutdown + ctx context.Context + cancel context.CancelFunc + + // internal waitgroup + wg sync.WaitGroup + + // Handlers + handlers map[string]func(string) +} + +// NewURL creates a new log subsystem +func NewURL(bus *servicebus.ServiceBus, logger *logger.Logger, handlers map[string]func(string)) (*URL, error) { + + // Subscribe to log messages + urlChannel, err := bus.Subscribe("url") + if err != nil { + return nil, err + } + + ctx, cancel := context.WithCancel(context.Background()) + + result := &URL{ + urlChannel: urlChannel, + logger: logger, + ctx: ctx, + cancel: cancel, + handlers: handlers, + } + + return result, nil +} + +// Start the subsystem +func (u *URL) Start() error { + + u.wg.Add(1) + + // Spin off a go routine + go func() { + defer u.logger.Trace("URL Shutdown") + + for u.shouldQuit == false { + select { + case <-u.ctx.Done(): + u.wg.Done() + return + case urlMessage := <-u.urlChannel: + messageType := strings.TrimPrefix(urlMessage.Topic(), "url:") + switch messageType { + case "handler": + url := urlMessage.Data().(string) + splitURL := strings.Split(url, ":") + protocol := splitURL[0] + callback, ok := u.handlers[protocol] + if ok { + go callback(url) + } + default: + u.logger.Error("unknown url message: %+v", urlMessage) + } + } + } + }() + + return nil +} + +func (u *URL) Close() { + u.cancel() + u.wg.Wait() +} diff --git a/v2/pkg/options/mac/mac.go b/v2/pkg/options/mac/mac.go index 3108a6d62..6e568d557 100644 --- a/v2/pkg/options/mac/mac.go +++ b/v2/pkg/options/mac/mac.go @@ -20,4 +20,5 @@ type Options struct { TrayMenus []*menu.TrayMenu ContextMenus []*menu.ContextMenu ActivationPolicy ActivationPolicy + URLHandlers map[string]func(string) }