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,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)
}

View File

@@ -0,0 +1,125 @@
//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"
"syscall"
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
func genOFN(parent Controller, title, filter string, filterIndex uint, initialDir string, buf []uint16) *w32.OPENFILENAME {
var ofn w32.OPENFILENAME
ofn.StructSize = uint32(unsafe.Sizeof(ofn))
ofn.Owner = parent.Handle()
if filter != "" {
filterBuf := make([]uint16, len(filter)+1)
copy(filterBuf, syscall.StringToUTF16(filter))
// Replace '|' with the expected '\0'
for i, c := range filterBuf {
if byte(c) == '|' {
filterBuf[i] = uint16(0)
}
}
ofn.Filter = &filterBuf[0]
ofn.FilterIndex = uint32(filterIndex)
}
ofn.File = &buf[0]
ofn.MaxFile = uint32(len(buf))
if initialDir != "" {
ofn.InitialDir = syscall.StringToUTF16Ptr(initialDir)
}
if title != "" {
ofn.Title = syscall.StringToUTF16Ptr(title)
}
ofn.Flags = w32.OFN_FILEMUSTEXIST
return &ofn
}
func ShowOpenFileDlg(parent Controller, title, filter string, filterIndex uint, initialDir string) (filePath string, accepted bool) {
buf := make([]uint16, 1024)
ofn := genOFN(parent, title, filter, filterIndex, initialDir, buf)
if accepted = w32.GetOpenFileName(ofn); accepted {
filePath = syscall.UTF16ToString(buf)
}
return
}
func ShowSaveFileDlg(parent Controller, title, filter string, filterIndex uint, initialDir string) (filePath string, accepted bool) {
buf := make([]uint16, 1024)
ofn := genOFN(parent, title, filter, filterIndex, initialDir, buf)
if accepted = w32.GetSaveFileName(ofn); accepted {
filePath = syscall.UTF16ToString(buf)
}
return
}
func ShowBrowseFolderDlg(parent Controller, title string) (folder string, accepted bool) {
var bi w32.BROWSEINFO
bi.Owner = parent.Handle()
bi.Title = syscall.StringToUTF16Ptr(title)
bi.Flags = w32.BIF_RETURNONLYFSDIRS | w32.BIF_NEWDIALOGSTYLE
w32.CoInitialize()
ret := w32.SHBrowseForFolder(&bi)
w32.CoUninitialize()
folder = w32.SHGetPathFromIDList(ret)
accepted = folder != ""
return
}
// MsgBoxOkCancel basic pop up message. Returns 1 for OK and 2 for CANCEL.
func MsgBoxOkCancel(parent Controller, title, caption string) int {
return MsgBox(parent, title, caption, w32.MB_ICONEXCLAMATION|w32.MB_OKCANCEL)
}
func MsgBoxYesNo(parent Controller, title, caption string) int {
return MsgBox(parent, title, caption, w32.MB_ICONEXCLAMATION|w32.MB_YESNO)
}
func MsgBoxOk(parent Controller, title, caption string) {
MsgBox(parent, title, caption, w32.MB_ICONINFORMATION|w32.MB_OK)
}
// Warningf is generic warning message with OK and Cancel buttons. Returns 1 for OK.
func Warningf(parent Controller, format string, data ...interface{}) int {
caption := fmt.Sprintf(format, data...)
return MsgBox(parent, "Warning", caption, w32.MB_ICONWARNING|w32.MB_OKCANCEL)
}
// Printf is generic info message with OK button.
func Printf(parent Controller, format string, data ...interface{}) {
caption := fmt.Sprintf(format, data...)
MsgBox(parent, "Information", caption, w32.MB_ICONINFORMATION|w32.MB_OK)
}
// Errorf is generic error message with OK button.
func Errorf(parent Controller, format string, data ...interface{}) {
caption := fmt.Sprintf(format, data...)
MsgBox(parent, "Error", caption, w32.MB_ICONERROR|w32.MB_OK)
}
func MsgBox(parent Controller, title, caption string, flags uint) int {
var result int
if parent != nil {
result = w32.MessageBox(parent.Handle(), caption, title, flags)
} else {
result = w32.MessageBox(0, caption, title, flags)
}
return result
}

View File

@@ -0,0 +1,560 @@
//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"
"runtime"
"sync"
"syscall"
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
type ControlBase struct {
hwnd w32.HWND
font *Font
parent Controller
contextMenu *MenuItem
isForm bool
minWidth, minHeight int
maxWidth, maxHeight int
// General events
onCreate EventManager
onClose EventManager
// Focus events
onKillFocus EventManager
onSetFocus EventManager
// Drag and drop events
onDropFiles EventManager
// Mouse events
onLBDown EventManager
onLBUp EventManager
onLBDbl EventManager
onMBDown EventManager
onMBUp EventManager
onRBDown EventManager
onRBUp EventManager
onRBDbl EventManager
onMouseMove EventManager
// use MouseControl to capture onMouseHover and onMouseLeave events.
onMouseHover EventManager
onMouseLeave EventManager
// Keyboard events
onKeyUp EventManager
// Paint events
onPaint EventManager
onSize EventManager
m sync.Mutex
dispatchq []func()
}
// InitControl is called by controls: edit, button, treeview, listview, and so on.
func (cba *ControlBase) InitControl(className string, parent Controller, exstyle, style uint) {
cba.hwnd = CreateWindow(className, parent, exstyle, style)
if cba.hwnd == 0 {
panic("cannot create window for " + className)
}
cba.parent = parent
}
// InitWindow is called by custom window based controls such as split, panel, etc.
func (cba *ControlBase) InitWindow(className string, parent Controller, exstyle, style uint) {
RegClassOnlyOnce(className)
cba.hwnd = CreateWindow(className, parent, exstyle, style)
if cba.hwnd == 0 {
panic("cannot create window for " + className)
}
cba.parent = parent
}
// SetTheme for TreeView and ListView controls.
func (cba *ControlBase) SetTheme(appName string) error {
if hr := w32.SetWindowTheme(cba.hwnd, syscall.StringToUTF16Ptr(appName), nil); w32.FAILED(hr) {
return fmt.Errorf("SetWindowTheme %d", hr)
}
return nil
}
func (cba *ControlBase) Handle() w32.HWND {
return cba.hwnd
}
func (cba *ControlBase) SetHandle(hwnd w32.HWND) {
cba.hwnd = hwnd
}
func (cba *ControlBase) GetWindowDPI() (w32.UINT, w32.UINT) {
if w32.HasGetDpiForWindowFunc() {
// GetDpiForWindow is supported beginning with Windows 10, 1607 and is the most accureate
// one, especially it is consistent with the WM_DPICHANGED event.
dpi := w32.GetDpiForWindow(cba.hwnd)
return dpi, dpi
}
if w32.HasGetDPIForMonitorFunc() {
// GetDpiForWindow is supported beginning with Windows 8.1
monitor := w32.MonitorFromWindow(cba.hwnd, w32.MONITOR_DEFAULTTONEAREST)
if monitor == 0 {
return 0, 0
}
var dpiX, dpiY w32.UINT
w32.GetDPIForMonitor(monitor, w32.MDT_EFFECTIVE_DPI, &dpiX, &dpiY)
return dpiX, dpiY
}
// If none of the above is supported fallback to the System DPI.
screen := w32.GetDC(0)
x := w32.GetDeviceCaps(screen, w32.LOGPIXELSX)
y := w32.GetDeviceCaps(screen, w32.LOGPIXELSY)
w32.ReleaseDC(0, screen)
return w32.UINT(x), w32.UINT(y)
}
func (cba *ControlBase) SetAndClearStyleBits(set, clear uint32) error {
style := uint32(w32.GetWindowLong(cba.hwnd, w32.GWL_STYLE))
if style == 0 {
return fmt.Errorf("GetWindowLong")
}
if newStyle := style&^clear | set; newStyle != style {
if w32.SetWindowLong(cba.hwnd, w32.GWL_STYLE, newStyle) == 0 {
return fmt.Errorf("SetWindowLong")
}
}
return nil
}
func (cba *ControlBase) SetIsForm(isform bool) {
cba.isForm = isform
}
func (cba *ControlBase) SetText(caption string) {
w32.SetWindowText(cba.hwnd, caption)
}
func (cba *ControlBase) Text() string {
return w32.GetWindowText(cba.hwnd)
}
func (cba *ControlBase) Close() {
UnRegMsgHandler(cba.hwnd)
w32.DestroyWindow(cba.hwnd)
}
func (cba *ControlBase) SetTranslucentBackground() {
var accent = w32.ACCENT_POLICY{
AccentState: w32.ACCENT_ENABLE_BLURBEHIND,
}
var data w32.WINDOWCOMPOSITIONATTRIBDATA
data.Attrib = w32.WCA_ACCENT_POLICY
data.PvData = unsafe.Pointer(&accent)
data.CbData = unsafe.Sizeof(accent)
w32.SetWindowCompositionAttribute(cba.hwnd, &data)
}
func (cba *ControlBase) SetContentProtection(enable bool) {
if enable {
w32.SetWindowDisplayAffinity(uintptr(cba.hwnd), w32.WDA_EXCLUDEFROMCAPTURE)
} else {
w32.SetWindowDisplayAffinity(uintptr(cba.hwnd), w32.WDA_NONE)
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func (cba *ControlBase) clampSize(width, height int) (int, int) {
if cba.minWidth != 0 {
width = max(width, cba.minWidth)
}
if cba.maxWidth != 0 {
width = min(width, cba.maxWidth)
}
if cba.minHeight != 0 {
height = max(height, cba.minHeight)
}
if cba.maxHeight != 0 {
height = min(height, cba.maxHeight)
}
return width, height
}
func (cba *ControlBase) SetSize(width, height int) {
x, y := cba.Pos()
width, height = cba.clampSize(width, height)
width, height = cba.scaleWithWindowDPI(width, height)
w32.MoveWindow(cba.hwnd, x, y, width, height, true)
}
func (cba *ControlBase) SetMinSize(width, height int) {
cba.minWidth = width
cba.minHeight = height
// Ensure we set max if min > max
if cba.maxWidth > 0 {
cba.maxWidth = max(cba.minWidth, cba.maxWidth)
}
if cba.maxHeight > 0 {
cba.maxHeight = max(cba.minHeight, cba.maxHeight)
}
x, y := cba.Pos()
currentWidth, currentHeight := cba.Size()
clampedWidth, clampedHeight := cba.clampSize(currentWidth, currentHeight)
if clampedWidth != currentWidth || clampedHeight != currentHeight {
w32.MoveWindow(cba.hwnd, x, y, clampedWidth, clampedHeight, true)
}
}
func (cba *ControlBase) SetMaxSize(width, height int) {
cba.maxWidth = width
cba.maxHeight = height
// Ensure we set min if max > min
if cba.maxWidth > 0 {
cba.minWidth = min(cba.maxWidth, cba.minWidth)
}
if cba.maxHeight > 0 {
cba.minHeight = min(cba.maxHeight, cba.minHeight)
}
x, y := cba.Pos()
currentWidth, currentHeight := cba.Size()
clampedWidth, clampedHeight := cba.clampSize(currentWidth, currentHeight)
if clampedWidth != currentWidth || clampedHeight != currentHeight {
w32.MoveWindow(cba.hwnd, x, y, clampedWidth, clampedHeight, true)
}
}
func (cba *ControlBase) Size() (width, height int) {
rect := w32.GetWindowRect(cba.hwnd)
width = int(rect.Right - rect.Left)
height = int(rect.Bottom - rect.Top)
width, height = cba.scaleToDefaultDPI(width, height)
return
}
func (cba *ControlBase) Width() int {
rect := w32.GetWindowRect(cba.hwnd)
return int(rect.Right - rect.Left)
}
func (cba *ControlBase) Height() int {
rect := w32.GetWindowRect(cba.hwnd)
return int(rect.Bottom - rect.Top)
}
func (cba *ControlBase) SetPos(x, y int) {
info := getMonitorInfo(cba.hwnd)
workRect := info.RcWork
w32.SetWindowPos(cba.hwnd, w32.HWND_TOP, int(workRect.Left)+x, int(workRect.Top)+y, 0, 0, w32.SWP_NOSIZE)
}
func (cba *ControlBase) SetAlwaysOnTop(b bool) {
if b {
w32.SetWindowPos(cba.hwnd, w32.HWND_TOPMOST, 0, 0, 0, 0, w32.SWP_NOSIZE|w32.SWP_NOMOVE)
} else {
w32.SetWindowPos(cba.hwnd, w32.HWND_NOTOPMOST, 0, 0, 0, 0, w32.SWP_NOSIZE|w32.SWP_NOMOVE)
}
}
func (cba *ControlBase) Pos() (x, y int) {
rect := w32.GetWindowRect(cba.hwnd)
x = int(rect.Left)
y = int(rect.Top)
if !cba.isForm && cba.parent != nil {
x, y, _ = w32.ScreenToClient(cba.parent.Handle(), x, y)
}
return
}
func (cba *ControlBase) Visible() bool {
return w32.IsWindowVisible(cba.hwnd)
}
func (cba *ControlBase) ToggleVisible() bool {
visible := w32.IsWindowVisible(cba.hwnd)
if visible {
cba.Hide()
} else {
cba.Show()
}
return !visible
}
func (cba *ControlBase) ContextMenu() *MenuItem {
return cba.contextMenu
}
func (cba *ControlBase) SetContextMenu(menu *MenuItem) {
cba.contextMenu = menu
}
func (cba *ControlBase) Bounds() *Rect {
rect := w32.GetWindowRect(cba.hwnd)
if cba.isForm {
return &Rect{*rect}
}
return ScreenToClientRect(cba.hwnd, rect)
}
func (cba *ControlBase) ClientRect() *Rect {
rect := w32.GetClientRect(cba.hwnd)
return ScreenToClientRect(cba.hwnd, rect)
}
func (cba *ControlBase) ClientWidth() int {
rect := w32.GetClientRect(cba.hwnd)
return int(rect.Right - rect.Left)
}
func (cba *ControlBase) ClientHeight() int {
rect := w32.GetClientRect(cba.hwnd)
return int(rect.Bottom - rect.Top)
}
func (cba *ControlBase) Show() {
// WindowPos is used with HWND_TOPMOST to guarantee bring our app on top
// force set our main window on top
w32.SetWindowPos(
cba.hwnd,
w32.HWND_TOPMOST,
0, 0, 0, 0,
w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE,
)
// remove topmost to allow normal windows manipulations
w32.SetWindowPos(
cba.hwnd,
w32.HWND_NOTOPMOST,
0, 0, 0, 0,
w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE,
)
// put main window on tops foreground
w32.SetForegroundWindow(cba.hwnd)
}
func (cba *ControlBase) Hide() {
w32.ShowWindow(cba.hwnd, w32.SW_HIDE)
}
func (cba *ControlBase) Enabled() bool {
return w32.IsWindowEnabled(cba.hwnd)
}
func (cba *ControlBase) SetEnabled(b bool) {
w32.EnableWindow(cba.hwnd, b)
}
func (cba *ControlBase) SetFocus() {
w32.SetFocus(cba.hwnd)
}
func (cba *ControlBase) Invalidate(erase bool) {
// pRect := w32.GetClientRect(cba.hwnd)
// if cba.isForm {
// w32.InvalidateRect(cba.hwnd, pRect, erase)
// } else {
// rc := ScreenToClientRect(cba.hwnd, pRect)
// w32.InvalidateRect(cba.hwnd, rc.GetW32Rect(), erase)
// }
w32.InvalidateRect(cba.hwnd, nil, erase)
}
func (cba *ControlBase) Parent() Controller {
return cba.parent
}
func (cba *ControlBase) SetParent(parent Controller) {
cba.parent = parent
}
func (cba *ControlBase) Font() *Font {
return cba.font
}
func (cba *ControlBase) SetFont(font *Font) {
w32.SendMessage(cba.hwnd, w32.WM_SETFONT, uintptr(font.hfont), 1)
cba.font = font
}
func (cba *ControlBase) EnableDragAcceptFiles(b bool) {
w32.DragAcceptFiles(cba.hwnd, b)
}
func (cba *ControlBase) InvokeRequired() bool {
if cba.hwnd == 0 {
return false
}
windowThreadId, _ := w32.GetWindowThreadProcessId(cba.hwnd)
currentThreadId := w32.GetCurrentThreadId()
return windowThreadId != currentThreadId
}
func (cba *ControlBase) Invoke(f func()) {
if cba.tryInvokeOnCurrentGoRoutine(f) {
return
}
cba.m.Lock()
cba.dispatchq = append(cba.dispatchq, f)
cba.m.Unlock()
w32.PostMessage(cba.hwnd, wmInvokeCallback, 0, 0)
}
func (cba *ControlBase) PreTranslateMessage(msg *w32.MSG) bool {
if msg.Message == w32.WM_GETDLGCODE {
println("pretranslate, WM_GETDLGCODE")
}
return false
}
// Events
func (cba *ControlBase) OnCreate() *EventManager {
return &cba.onCreate
}
func (cba *ControlBase) OnClose() *EventManager {
return &cba.onClose
}
func (cba *ControlBase) OnKillFocus() *EventManager {
return &cba.onKillFocus
}
func (cba *ControlBase) OnSetFocus() *EventManager {
return &cba.onSetFocus
}
func (cba *ControlBase) OnDropFiles() *EventManager {
return &cba.onDropFiles
}
func (cba *ControlBase) OnLBDown() *EventManager {
return &cba.onLBDown
}
func (cba *ControlBase) OnLBUp() *EventManager {
return &cba.onLBUp
}
func (cba *ControlBase) OnLBDbl() *EventManager {
return &cba.onLBDbl
}
func (cba *ControlBase) OnMBDown() *EventManager {
return &cba.onMBDown
}
func (cba *ControlBase) OnMBUp() *EventManager {
return &cba.onMBUp
}
func (cba *ControlBase) OnRBDown() *EventManager {
return &cba.onRBDown
}
func (cba *ControlBase) OnRBUp() *EventManager {
return &cba.onRBUp
}
func (cba *ControlBase) OnRBDbl() *EventManager {
return &cba.onRBDbl
}
func (cba *ControlBase) OnMouseMove() *EventManager {
return &cba.onMouseMove
}
func (cba *ControlBase) OnMouseHover() *EventManager {
return &cba.onMouseHover
}
func (cba *ControlBase) OnMouseLeave() *EventManager {
return &cba.onMouseLeave
}
func (cba *ControlBase) OnPaint() *EventManager {
return &cba.onPaint
}
func (cba *ControlBase) OnSize() *EventManager {
return &cba.onSize
}
func (cba *ControlBase) OnKeyUp() *EventManager {
return &cba.onKeyUp
}
func (cba *ControlBase) scaleWithWindowDPI(width, height int) (int, int) {
dpix, dpiy := cba.GetWindowDPI()
scaledWidth := ScaleWithDPI(width, dpix)
scaledHeight := ScaleWithDPI(height, dpiy)
return scaledWidth, scaledHeight
}
func (cba *ControlBase) scaleToDefaultDPI(width, height int) (int, int) {
dpix, dpiy := cba.GetWindowDPI()
scaledWidth := ScaleToDefaultDPI(width, dpix)
scaledHeight := ScaleToDefaultDPI(height, dpiy)
return scaledWidth, scaledHeight
}
func (cba *ControlBase) tryInvokeOnCurrentGoRoutine(f func()) bool {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if cba.InvokeRequired() {
return false
}
f()
return true
}
func (cba *ControlBase) invokeCallbacks() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if cba.InvokeRequired() {
panic("InvokeCallbacks must always be called on the window thread")
}
cba.m.Lock()
q := append([]func(){}, cba.dispatchq...)
cba.dispatchq = []func(){}
cba.m.Unlock()
for _, v := range q {
v()
}
}

View File

@@ -0,0 +1,85 @@
//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"
)
type Controller interface {
Text() string
Enabled() bool
SetFocus()
Handle() w32.HWND
Invalidate(erase bool)
Parent() Controller
Pos() (x, y int)
Size() (w, h int)
Height() int
Width() int
Visible() bool
Bounds() *Rect
ClientRect() *Rect
SetText(s string)
SetEnabled(b bool)
SetPos(x, y int)
SetSize(w, h int)
EnableDragAcceptFiles(b bool)
Show()
Hide()
ContextMenu() *MenuItem
SetContextMenu(menu *MenuItem)
Font() *Font
SetFont(font *Font)
InvokeRequired() bool
Invoke(func())
PreTranslateMessage(msg *w32.MSG) bool
WndProc(msg uint32, wparam, lparam uintptr) uintptr
//General events
OnCreate() *EventManager
OnClose() *EventManager
// Focus events
OnKillFocus() *EventManager
OnSetFocus() *EventManager
//Drag and drop events
OnDropFiles() *EventManager
//Mouse events
OnLBDown() *EventManager
OnLBUp() *EventManager
OnLBDbl() *EventManager
OnMBDown() *EventManager
OnMBUp() *EventManager
OnRBDown() *EventManager
OnRBUp() *EventManager
OnRBDbl() *EventManager
OnMouseMove() *EventManager
// OnMouseLeave and OnMouseHover does not fire unless control called internalTrackMouseEvent.
// Use MouseControl for a how to example.
OnMouseHover() *EventManager
OnMouseLeave() *EventManager
//Keyboard events
OnKeyUp() *EventManager
//Paint events
OnPaint() *EventManager
OnSize() *EventManager
invokeCallbacks()
}

View File

@@ -0,0 +1,136 @@
//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"
// Dialog displayed as z-order top window until closed.
// It also disables parent window so it can not be clicked.
type Dialog struct {
Form
isModal bool
btnOk *PushButton
btnCancel *PushButton
onLoad EventManager
onOk EventManager
onCancel EventManager
}
func NewDialog(parent Controller) *Dialog {
dlg := new(Dialog)
dlg.isForm = true
dlg.isModal = true
RegClassOnlyOnce("winc_Dialog")
dlg.hwnd = CreateWindow("winc_Dialog", parent, w32.WS_EX_CONTROLPARENT, /* IMPORTANT */
w32.WS_SYSMENU|w32.WS_CAPTION|w32.WS_THICKFRAME /*|w32.WS_BORDER|w32.WS_POPUP*/)
dlg.parent = parent
// dlg might fail if icon resource is not embedded in the binary
if ico, err := NewIconFromResource(GetAppInstance(), uint16(AppIconID)); err == nil {
dlg.SetIcon(0, ico)
}
// Dlg forces display of focus rectangles, as soon as the user starts to type.
w32.SendMessage(dlg.hwnd, w32.WM_CHANGEUISTATE, w32.UIS_INITIALIZE, 0)
RegMsgHandler(dlg)
dlg.SetFont(DefaultFont)
dlg.SetText("Form")
dlg.SetSize(200, 100)
return dlg
}
func (dlg *Dialog) SetModal(modal bool) {
dlg.isModal = modal
}
// SetButtons wires up dialog events to buttons. btnCancel can be nil.
func (dlg *Dialog) SetButtons(btnOk *PushButton, btnCancel *PushButton) {
dlg.btnOk = btnOk
dlg.btnOk.SetDefault()
dlg.btnCancel = btnCancel
}
// Events
func (dlg *Dialog) OnLoad() *EventManager {
return &dlg.onLoad
}
func (dlg *Dialog) OnOk() *EventManager {
return &dlg.onOk
}
func (dlg *Dialog) OnCancel() *EventManager {
return &dlg.onCancel
}
// PreTranslateMessage handles dialog specific messages. IMPORTANT.
func (dlg *Dialog) PreTranslateMessage(msg *w32.MSG) bool {
if msg.Message >= w32.WM_KEYFIRST && msg.Message <= w32.WM_KEYLAST {
if w32.IsDialogMessage(dlg.hwnd, msg) {
return true
}
}
return false
}
// Show dialog performs special setup for dialog windows.
func (dlg *Dialog) Show() {
if dlg.isModal {
dlg.Parent().SetEnabled(false)
}
dlg.onLoad.Fire(NewEvent(dlg, nil))
dlg.Form.Show()
}
// Close dialog when you done with it.
func (dlg *Dialog) Close() {
if dlg.isModal {
dlg.Parent().SetEnabled(true)
}
dlg.ControlBase.Close()
}
func (dlg *Dialog) cancel() {
if dlg.btnCancel != nil {
dlg.btnCancel.onClick.Fire(NewEvent(dlg.btnCancel, nil))
}
dlg.onCancel.Fire(NewEvent(dlg, nil))
}
func (dlg *Dialog) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case w32.WM_COMMAND:
switch w32.LOWORD(uint32(wparam)) {
case w32.IDOK:
if dlg.btnOk != nil {
dlg.btnOk.onClick.Fire(NewEvent(dlg.btnOk, nil))
}
dlg.onOk.Fire(NewEvent(dlg, nil))
return w32.TRUE
case w32.IDCANCEL:
dlg.cancel()
return w32.TRUE
}
case w32.WM_CLOSE:
dlg.cancel() // use onCancel or dlg.btnCancel.OnClick to close
return 0
case w32.WM_DESTROY:
if dlg.isModal {
dlg.Parent().SetEnabled(true)
}
}
return w32.DefWindowProc(dlg.hwnd, msg, wparam, lparam)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,113 @@
//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"
type Edit struct {
ControlBase
onChange EventManager
}
const passwordChar = '*'
const nopasswordChar = ' '
func NewEdit(parent Controller) *Edit {
edt := new(Edit)
edt.InitControl("EDIT", parent, w32.WS_EX_CLIENTEDGE, w32.WS_CHILD|w32.WS_VISIBLE|w32.WS_TABSTOP|w32.ES_LEFT|
w32.ES_AUTOHSCROLL)
RegMsgHandler(edt)
edt.SetFont(DefaultFont)
edt.SetSize(200, 22)
return edt
}
// Events.
func (ed *Edit) OnChange() *EventManager {
return &ed.onChange
}
// Public methods.
func (ed *Edit) SetReadOnly(isReadOnly bool) {
w32.SendMessage(ed.hwnd, w32.EM_SETREADONLY, uintptr(w32.BoolToBOOL(isReadOnly)), 0)
}
// Public methods
func (ed *Edit) SetPassword(isPassword bool) {
if isPassword {
w32.SendMessage(ed.hwnd, w32.EM_SETPASSWORDCHAR, uintptr(passwordChar), 0)
} else {
w32.SendMessage(ed.hwnd, w32.EM_SETPASSWORDCHAR, 0, 0)
}
}
func (ed *Edit) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case w32.WM_COMMAND:
switch w32.HIWORD(uint32(wparam)) {
case w32.EN_CHANGE:
ed.onChange.Fire(NewEvent(ed, nil))
}
/*case w32.WM_GETDLGCODE:
println("Edit")
if wparam == w32.VK_RETURN {
return w32.DLGC_WANTALLKEYS
}*/
}
return w32.DefWindowProc(ed.hwnd, msg, wparam, lparam)
}
// MultiEdit is multiline text edit.
type MultiEdit struct {
ControlBase
onChange EventManager
}
func NewMultiEdit(parent Controller) *MultiEdit {
med := new(MultiEdit)
med.InitControl("EDIT", parent, w32.WS_EX_CLIENTEDGE, w32.WS_CHILD|w32.WS_VISIBLE|w32.WS_TABSTOP|w32.ES_LEFT|
w32.WS_VSCROLL|w32.WS_HSCROLL|w32.ES_MULTILINE|w32.ES_WANTRETURN|w32.ES_AUTOHSCROLL|w32.ES_AUTOVSCROLL)
RegMsgHandler(med)
med.SetFont(DefaultFont)
med.SetSize(200, 400)
return med
}
// Events
func (med *MultiEdit) OnChange() *EventManager {
return &med.onChange
}
// Public methods
func (med *MultiEdit) SetReadOnly(isReadOnly bool) {
w32.SendMessage(med.hwnd, w32.EM_SETREADONLY, uintptr(w32.BoolToBOOL(isReadOnly)), 0)
}
func (med *MultiEdit) AddLine(text string) {
if len(med.Text()) == 0 {
med.SetText(text)
} else {
med.SetText(med.Text() + "\r\n" + text)
}
}
func (med *MultiEdit) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case w32.WM_COMMAND:
switch w32.HIWORD(uint32(wparam)) {
case w32.EN_CHANGE:
med.onChange.Fire(NewEvent(med, nil))
}
}
return w32.DefWindowProc(med.hwnd, msg, wparam, lparam)
}

View File

@@ -0,0 +1,17 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
* Copyright (C) 2010-2013 Allen Dang. All Rights Reserved.
*/
package winc
type Event struct {
Sender Controller
Data interface{}
}
func NewEvent(sender Controller, data interface{}) *Event {
return &Event{Sender: sender, Data: data}
}

View File

@@ -0,0 +1,52 @@
//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"
)
type RawMsg struct {
Hwnd w32.HWND
Msg uint32
WParam, LParam uintptr
}
type MouseEventData struct {
X, Y int
Button int
Wheel int
}
type DropFilesEventData struct {
X, Y int
Files []string
}
type PaintEventData struct {
Canvas *Canvas
}
type LabelEditEventData struct {
Item ListItem
Text string
//PszText *uint16
}
/*type LVDBLClickEventData struct {
NmItem *w32.NMITEMACTIVATE
}*/
type KeyUpEventData struct {
VKey, Code int
}
type SizeEventData struct {
Type uint
X, Y int
}

View File

@@ -0,0 +1,24 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
* Copyright (C) 2010-2013 Allen Dang. All Rights Reserved.
*/
package winc
type EventHandler func(arg *Event)
type EventManager struct {
handler EventHandler
}
func (evm *EventManager) Fire(arg *Event) {
if evm.handler != nil {
evm.handler(arg)
}
}
func (evm *EventManager) Bind(handler EventHandler) {
evm.handler = handler
}

View File

@@ -0,0 +1,121 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
* Copyright (C) 2010-2013 Allen Dang. All Rights Reserved.
*/
package winc
import (
"syscall"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
const (
FontBold byte = 0x01
FontItalic byte = 0x02
FontUnderline byte = 0x04
FontStrikeOut byte = 0x08
)
func init() {
DefaultFont = NewFont("MS Shell Dlg 2", 8, 0)
}
type Font struct {
hfont w32.HFONT
family string
pointSize int
style byte
}
func NewFont(family string, pointSize int, style byte) *Font {
if style > FontBold|FontItalic|FontUnderline|FontStrikeOut {
panic("Invalid font style")
}
//Retrive screen DPI
hDC := w32.GetDC(0)
defer w32.ReleaseDC(0, hDC)
screenDPIY := w32.GetDeviceCaps(hDC, w32.LOGPIXELSY)
font := Font{
family: family,
pointSize: pointSize,
style: style,
}
font.hfont = font.createForDPI(screenDPIY)
if font.hfont == 0 {
panic("CreateFontIndirect failed")
}
return &font
}
func (fnt *Font) createForDPI(dpi int) w32.HFONT {
var lf w32.LOGFONT
lf.Height = int32(-w32.MulDiv(fnt.pointSize, dpi, 72))
if fnt.style&FontBold > 0 {
lf.Weight = w32.FW_BOLD
} else {
lf.Weight = w32.FW_NORMAL
}
if fnt.style&FontItalic > 0 {
lf.Italic = 1
}
if fnt.style&FontUnderline > 0 {
lf.Underline = 1
}
if fnt.style&FontStrikeOut > 0 {
lf.StrikeOut = 1
}
lf.CharSet = w32.DEFAULT_CHARSET
lf.OutPrecision = w32.OUT_TT_PRECIS
lf.ClipPrecision = w32.CLIP_DEFAULT_PRECIS
lf.Quality = w32.CLEARTYPE_QUALITY
lf.PitchAndFamily = w32.VARIABLE_PITCH | w32.FF_SWISS
src := syscall.StringToUTF16(fnt.family)
dest := lf.FaceName[:]
copy(dest, src)
return w32.CreateFontIndirect(&lf)
}
func (fnt *Font) GetHFONT() w32.HFONT {
return fnt.hfont
}
func (fnt *Font) Bold() bool {
return fnt.style&FontBold > 0
}
func (fnt *Font) Dispose() {
if fnt.hfont != 0 {
w32.DeleteObject(w32.HGDIOBJ(fnt.hfont))
}
}
func (fnt *Font) Family() string {
return fnt.family
}
func (fnt *Font) Italic() bool {
return fnt.style&FontItalic > 0
}
func (fnt *Font) StrikeOut() bool {
return fnt.style&FontStrikeOut > 0
}
func (fnt *Font) Underline() bool {
return fnt.style&FontUnderline > 0
}
func (fnt *Font) Style() byte {
return fnt.style
}

View File

@@ -0,0 +1,317 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
* Copyright (C) 2010-2013 Allen Dang. All Rights Reserved.
*/
package winc
import (
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
type LayoutManager interface {
Update()
}
// A Form is main window of the application.
type Form struct {
ControlBase
layoutMng LayoutManager
// Fullscreen / Unfullscreen
isFullscreen bool
previousWindowStyle uint32
previousWindowExStyle uint32
previousWindowPlacement w32.WINDOWPLACEMENT
}
func NewCustomForm(parent Controller, exStyle int, dwStyle uint) *Form {
fm := new(Form)
RegClassOnlyOnce("winc_Form")
fm.isForm = true
if exStyle == 0 {
exStyle = w32.WS_EX_CONTROLPARENT | w32.WS_EX_APPWINDOW
}
if dwStyle == 0 {
dwStyle = w32.WS_OVERLAPPEDWINDOW
}
fm.hwnd = CreateWindow("winc_Form", parent, uint(exStyle), dwStyle)
fm.parent = parent
// this might fail if icon resource is not embedded in the binary
if ico, err := NewIconFromResource(GetAppInstance(), uint16(AppIconID)); err == nil {
fm.SetIcon(0, ico)
}
// This forces display of focus rectangles, as soon as the user starts to type.
w32.SendMessage(fm.hwnd, w32.WM_CHANGEUISTATE, w32.UIS_INITIALIZE, 0)
RegMsgHandler(fm)
fm.SetFont(DefaultFont)
fm.SetText("Form")
return fm
}
func NewForm(parent Controller) *Form {
fm := new(Form)
RegClassOnlyOnce("winc_Form")
fm.isForm = true
fm.hwnd = CreateWindow("winc_Form", parent, w32.WS_EX_CONTROLPARENT|w32.WS_EX_APPWINDOW, w32.WS_OVERLAPPEDWINDOW)
fm.parent = parent
// this might fail if icon resource is not embedded in the binary
if ico, err := NewIconFromResource(GetAppInstance(), uint16(AppIconID)); err == nil {
fm.SetIcon(0, ico)
}
// This forces display of focus rectangles, as soon as the user starts to type.
w32.SendMessage(fm.hwnd, w32.WM_CHANGEUISTATE, w32.UIS_INITIALIZE, 0)
RegMsgHandler(fm)
fm.SetFont(DefaultFont)
fm.SetText("Form")
return fm
}
func (fm *Form) SetLayout(mng LayoutManager) {
fm.layoutMng = mng
}
// UpdateLayout refresh layout.
func (fm *Form) UpdateLayout() {
if fm.layoutMng != nil {
fm.layoutMng.Update()
}
}
func (fm *Form) NewMenu() *Menu {
hMenu := w32.CreateMenu()
if hMenu == 0 {
panic("failed CreateMenu")
}
m := &Menu{hMenu: hMenu, hwnd: fm.hwnd}
if !w32.SetMenu(fm.hwnd, hMenu) {
panic("failed SetMenu")
}
return m
}
func (fm *Form) DisableIcon() {
windowInfo := getWindowInfo(fm.hwnd)
frameless := windowInfo.IsPopup()
if frameless {
return
}
exStyle := w32.GetWindowLong(fm.hwnd, w32.GWL_EXSTYLE)
w32.SetWindowLong(fm.hwnd, w32.GWL_EXSTYLE, uint32(exStyle|w32.WS_EX_DLGMODALFRAME))
w32.SetWindowPos(fm.hwnd, 0, 0, 0, 0, 0,
uint(
w32.SWP_FRAMECHANGED|
w32.SWP_NOMOVE|
w32.SWP_NOSIZE|
w32.SWP_NOZORDER),
)
}
func (fm *Form) Maximise() {
w32.ShowWindow(fm.hwnd, w32.SW_MAXIMIZE)
}
func (fm *Form) Minimise() {
w32.ShowWindow(fm.hwnd, w32.SW_MINIMIZE)
}
func (fm *Form) Restore() {
// SC_RESTORE param for WM_SYSCOMMAND to restore app if it is minimized
const SC_RESTORE = 0xF120
// restore the minimized window, if it is
w32.SendMessage(
fm.hwnd,
w32.WM_SYSCOMMAND,
SC_RESTORE,
0,
)
w32.ShowWindow(fm.hwnd, w32.SW_SHOW)
}
// Public methods
func (fm *Form) Center() {
windowInfo := getWindowInfo(fm.hwnd)
frameless := windowInfo.IsPopup()
info := getMonitorInfo(fm.hwnd)
workRect := info.RcWork
screenMiddleW := workRect.Left + (workRect.Right-workRect.Left)/2
screenMiddleH := workRect.Top + (workRect.Bottom-workRect.Top)/2
var winRect *w32.RECT
if !frameless {
winRect = w32.GetWindowRect(fm.hwnd)
} else {
winRect = w32.GetClientRect(fm.hwnd)
}
winWidth := winRect.Right - winRect.Left
winHeight := winRect.Bottom - winRect.Top
windowX := screenMiddleW - (winWidth / 2)
windowY := screenMiddleH - (winHeight / 2)
w32.SetWindowPos(fm.hwnd, w32.HWND_TOP, int(windowX), int(windowY), int(winWidth), int(winHeight), w32.SWP_NOSIZE)
}
func (fm *Form) Fullscreen() {
if fm.isFullscreen {
return
}
fm.previousWindowStyle = uint32(w32.GetWindowLongPtr(fm.hwnd, w32.GWL_STYLE))
fm.previousWindowExStyle = uint32(w32.GetWindowLong(fm.hwnd, w32.GWL_EXSTYLE))
monitor := w32.MonitorFromWindow(fm.hwnd, w32.MONITOR_DEFAULTTOPRIMARY)
var monitorInfo w32.MONITORINFO
monitorInfo.CbSize = uint32(unsafe.Sizeof(monitorInfo))
if !w32.GetMonitorInfo(monitor, &monitorInfo) {
return
}
if !w32.GetWindowPlacement(fm.hwnd, &fm.previousWindowPlacement) {
return
}
// According to https://devblogs.microsoft.com/oldnewthing/20050505-04/?p=35703 one should use w32.WS_POPUP | w32.WS_VISIBLE
w32.SetWindowLong(fm.hwnd, w32.GWL_STYLE, fm.previousWindowStyle & ^uint32(w32.WS_OVERLAPPEDWINDOW) | (w32.WS_POPUP|w32.WS_VISIBLE))
w32.SetWindowLong(fm.hwnd, w32.GWL_EXSTYLE, fm.previousWindowExStyle & ^uint32(w32.WS_EX_DLGMODALFRAME))
fm.isFullscreen = true
w32.SetWindowPos(fm.hwnd, w32.HWND_TOP,
int(monitorInfo.RcMonitor.Left),
int(monitorInfo.RcMonitor.Top),
int(monitorInfo.RcMonitor.Right-monitorInfo.RcMonitor.Left),
int(monitorInfo.RcMonitor.Bottom-monitorInfo.RcMonitor.Top),
w32.SWP_NOOWNERZORDER|w32.SWP_FRAMECHANGED)
}
func (fm *Form) UnFullscreen() {
if !fm.isFullscreen {
return
}
w32.SetWindowLong(fm.hwnd, w32.GWL_STYLE, fm.previousWindowStyle)
w32.SetWindowLong(fm.hwnd, w32.GWL_EXSTYLE, fm.previousWindowExStyle)
w32.SetWindowPlacement(fm.hwnd, &fm.previousWindowPlacement)
fm.isFullscreen = false
w32.SetWindowPos(fm.hwnd, 0, 0, 0, 0, 0,
w32.SWP_NOMOVE|w32.SWP_NOSIZE|w32.SWP_NOZORDER|w32.SWP_NOOWNERZORDER|w32.SWP_FRAMECHANGED)
}
func (fm *Form) IsFullScreen() bool {
return fm.isFullscreen
}
// IconType: 1 - ICON_BIG; 0 - ICON_SMALL
func (fm *Form) SetIcon(iconType int, icon *Icon) {
if iconType > 1 {
panic("IconType is invalid")
}
w32.SendMessage(fm.hwnd, w32.WM_SETICON, uintptr(iconType), uintptr(icon.Handle()))
}
func (fm *Form) EnableMaxButton(b bool) {
SetStyle(fm.hwnd, b, w32.WS_MAXIMIZEBOX)
}
func (fm *Form) EnableMinButton(b bool) {
SetStyle(fm.hwnd, b, w32.WS_MINIMIZEBOX)
}
func (fm *Form) EnableSizable(b bool) {
SetStyle(fm.hwnd, b, w32.WS_THICKFRAME)
}
func (fm *Form) EnableDragMove(_ bool) {
//fm.isDragMove = b
}
func (fm *Form) EnableTopMost(b bool) {
tag := w32.HWND_NOTOPMOST
if b {
tag = w32.HWND_TOPMOST
}
w32.SetWindowPos(fm.hwnd, tag, 0, 0, 0, 0, w32.SWP_NOMOVE|w32.SWP_NOSIZE)
}
func (fm *Form) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case w32.WM_COMMAND:
if lparam == 0 && w32.HIWORD(uint32(wparam)) == 0 {
// Menu support.
actionID := uint16(w32.LOWORD(uint32(wparam)))
if action, ok := actionsByID[actionID]; ok {
action.onClick.Fire(NewEvent(fm, nil))
}
}
case w32.WM_KEYDOWN:
// Accelerator support.
key := Key(wparam)
if uint32(lparam)>>30 == 0 {
// Using TranslateAccelerators refused to work, so we handle them
// ourselves, at least for now.
shortcut := Shortcut{ModifiersDown(), key}
if action, ok := shortcut2Action[shortcut]; ok {
if action.Enabled() {
action.onClick.Fire(NewEvent(fm, nil))
}
}
}
case w32.WM_CLOSE:
return 0
case w32.WM_DESTROY:
w32.PostQuitMessage(0)
return 0
case w32.WM_SIZE, w32.WM_PAINT:
if fm.layoutMng != nil {
fm.layoutMng.Update()
}
case w32.WM_GETMINMAXINFO:
mmi := (*w32.MINMAXINFO)(unsafe.Pointer(lparam))
hasConstraints := false
if fm.minWidth > 0 || fm.minHeight > 0 {
hasConstraints = true
width, height := fm.scaleWithWindowDPI(fm.minWidth, fm.minHeight)
if width > 0 {
mmi.PtMinTrackSize.X = int32(width)
}
if height > 0 {
mmi.PtMinTrackSize.Y = int32(height)
}
}
if fm.maxWidth > 0 || fm.maxHeight > 0 {
hasConstraints = true
width, height := fm.scaleWithWindowDPI(fm.maxWidth, fm.maxHeight)
if width > 0 {
mmi.PtMaxTrackSize.X = int32(width)
}
if height > 0 {
mmi.PtMaxTrackSize.Y = int32(height)
}
}
if hasConstraints {
return 0
}
}
return w32.DefWindowProc(fm.hwnd, msg, wparam, lparam)
}

View File

@@ -0,0 +1,27 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
* Copyright (C) 2010-2013 Allen Dang. All Rights Reserved.
*/
package winc
import (
"syscall"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
// Private global variables.
var (
gAppInstance w32.HINSTANCE
gControllerRegistry map[w32.HWND]Controller
gRegisteredClasses []string
)
// Public global variables.
var (
GeneralWndprocCallBack = syscall.NewCallback(generalWndProc)
DefaultFont *Font
)

View File

@@ -0,0 +1,219 @@
//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"
"fmt"
"image"
"image/png"
"os"
"syscall"
"unsafe"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
var (
user32 = syscall.NewLazyDLL("user32.dll")
gdi32 = syscall.NewLazyDLL("gdi32.dll")
procGetIconInfo = user32.NewProc("GetIconInfo")
procDeleteObject = gdi32.NewProc("DeleteObject")
procGetObject = gdi32.NewProc("GetObjectW")
procGetDIBits = gdi32.NewProc("GetDIBits")
procCreateCompatibleDC = gdi32.NewProc("CreateCompatibleDC")
procSelectObject = gdi32.NewProc("SelectObject")
procDeleteDC = gdi32.NewProc("DeleteDC")
)
func init() {
// Validate DLL loads at initialization time to surface missing APIs early
if err := user32.Load(); err != nil {
panic(fmt.Sprintf("failed to load user32.dll: %v", err))
}
if err := gdi32.Load(); err != nil {
panic(fmt.Sprintf("failed to load gdi32.dll: %v", err))
}
}
// ICONINFO mirrors the Win32 ICONINFO struct
type ICONINFO struct {
FIcon int32
XHotspot uint32
YHotspot uint32
HbmMask uintptr
HbmColor uintptr
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183376.aspx
type BITMAPINFOHEADER struct {
BiSize uint32
BiWidth int32
BiHeight int32
BiPlanes uint16
BiBitCount uint16
BiCompression uint32
BiSizeImage uint32
BiXPelsPerMeter int32
BiYPelsPerMeter int32
BiClrUsed uint32
BiClrImportant uint32
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162938.aspx
type RGBQUAD struct {
RgbBlue byte
RgbGreen byte
RgbRed byte
RgbReserved byte
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183375.aspx
type BITMAPINFO struct {
BmiHeader BITMAPINFOHEADER
BmiColors *RGBQUAD
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd183371.aspx
type BITMAP struct {
BmType int32
BmWidth int32
BmHeight int32
BmWidthBytes int32
BmPlanes uint16
BmBitsPixel uint16
BmBits unsafe.Pointer
}
type Icon struct {
handle w32.HICON
}
func NewIconFromFile(path string) (*Icon, error) {
ico := new(Icon)
var err error
if ico.handle = w32.LoadIcon(0, syscall.StringToUTF16Ptr(path)); ico.handle == 0 {
err = errors.New(fmt.Sprintf("Cannot load icon from %s", path))
}
return ico, err
}
func NewIconFromResource(instance w32.HINSTANCE, resId uint16) (*Icon, error) {
ico := new(Icon)
var err error
if ico.handle = w32.LoadIconWithResourceID(instance, resId); ico.handle == 0 {
err = errors.New(fmt.Sprintf("Cannot load icon from resource with id %v", resId))
}
return ico, err
}
func ExtractIcon(fileName string, index int) (*Icon, error) {
ico := new(Icon)
var err error
if ico.handle = w32.ExtractIcon(fileName, index); ico.handle == 0 || ico.handle == 1 {
err = errors.New(fmt.Sprintf("Cannot extract icon from %s at index %v", fileName, index))
}
return ico, err
}
func SaveHIconAsPNG(hIcon w32.HICON, filePath string) error {
// Get icon info
var iconInfo ICONINFO
ret, _, err := procGetIconInfo.Call(
uintptr(hIcon),
uintptr(unsafe.Pointer(&iconInfo)),
)
if ret == 0 {
return err
}
defer procDeleteObject.Call(uintptr(iconInfo.HbmMask))
defer procDeleteObject.Call(uintptr(iconInfo.HbmColor))
// Get bitmap info
var bmp BITMAP
ret, _, err = procGetObject.Call(
uintptr(iconInfo.HbmColor),
unsafe.Sizeof(bmp),
uintptr(unsafe.Pointer(&bmp)),
)
if ret == 0 {
return err
}
// Get screen DC for GetDIBits (bitmap must not be selected into a DC)
screenDC := w32.GetDC(0)
if screenDC == 0 {
return fmt.Errorf("failed to get screen DC")
}
defer w32.ReleaseDC(0, screenDC)
// Prepare bitmap info header
var bi BITMAPINFO
bi.BmiHeader.BiSize = uint32(unsafe.Sizeof(bi.BmiHeader))
bi.BmiHeader.BiWidth = bmp.BmWidth
bi.BmiHeader.BiHeight = bmp.BmHeight
bi.BmiHeader.BiPlanes = 1
bi.BmiHeader.BiBitCount = 32
bi.BmiHeader.BiCompression = w32.BI_RGB
// Allocate memory for bitmap bits
width, height := int(bmp.BmWidth), int(bmp.BmHeight)
bufferSize := width * height * 4
bits := make([]byte, bufferSize)
// Get bitmap bits using screen DC (bitmap must not be selected into any DC)
ret, _, err = procGetDIBits.Call(
uintptr(screenDC),
uintptr(iconInfo.HbmColor),
0,
uintptr(bmp.BmHeight),
uintptr(unsafe.Pointer(&bits[0])),
uintptr(unsafe.Pointer(&bi)),
w32.DIB_RGB_COLORS,
)
if ret == 0 {
return fmt.Errorf("failed to get bitmap bits: %w", err)
}
// Create Go image
img := image.NewRGBA(image.Rect(0, 0, width, height))
// Convert DIB to RGBA
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
// DIB is bottom-up, so we need to invert Y
dibIndex := ((height-1-y)*width + x) * 4
// RGBA image is top-down
imgIndex := (y*width + x) * 4
// BGRA to RGBA
img.Pix[imgIndex] = bits[dibIndex+2] // R
img.Pix[imgIndex+1] = bits[dibIndex+1] // G
img.Pix[imgIndex+2] = bits[dibIndex] // B
img.Pix[imgIndex+3] = bits[dibIndex+3] // A
}
}
// Create output file
outFile, err := os.Create(filePath)
if err != nil {
return err
}
defer outFile.Close()
// Encode and save the image
return png.Encode(outFile, img)
}
func (ic *Icon) Destroy() bool {
return w32.DestroyIcon(ic.handle)
}
func (ic *Icon) Handle() w32.HICON {
return ic.handle
}

View File

@@ -0,0 +1,64 @@
//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 ImageList struct {
handle w32.HIMAGELIST
}
func NewImageList(cx, cy int) *ImageList {
return newImageList(cx, cy, w32.ILC_COLOR32, 0, 0)
}
func newImageList(cx, cy int, flags uint, cInitial, cGrow int) *ImageList {
imgl := new(ImageList)
imgl.handle = w32.ImageList_Create(cx, cy, flags, cInitial, cGrow)
return imgl
}
func (im *ImageList) Handle() w32.HIMAGELIST {
return im.handle
}
func (im *ImageList) Destroy() bool {
return w32.ImageList_Destroy(im.handle)
}
func (im *ImageList) SetImageCount(uNewCount uint) bool {
return w32.ImageList_SetImageCount(im.handle, uNewCount)
}
func (im *ImageList) ImageCount() int {
return w32.ImageList_GetImageCount(im.handle)
}
func (im *ImageList) AddIcon(icon *Icon) int {
return w32.ImageList_AddIcon(im.handle, icon.Handle())
}
func (im *ImageList) AddResIcon(iconID uint16) {
if ico, err := NewIconFromResource(GetAppInstance(), iconID); err == nil {
im.AddIcon(ico)
return
}
panic(fmt.Sprintf("missing icon with icon ID: %d", iconID))
}
func (im *ImageList) RemoveAll() bool {
return w32.ImageList_RemoveAll(im.handle)
}
func (im *ImageList) Remove(i int) bool {
return w32.ImageList_Remove(im.handle, i)
}

View File

@@ -0,0 +1,59 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
*/
package winc
import "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
type ImageView struct {
ControlBase
bmp *Bitmap
}
func NewImageView(parent Controller) *ImageView {
iv := new(ImageView)
iv.InitWindow("winc_ImageView", parent, w32.WS_EX_CONTROLPARENT, w32.WS_CHILD|w32.WS_VISIBLE)
RegMsgHandler(iv)
iv.SetFont(DefaultFont)
iv.SetText("")
iv.SetSize(200, 65)
return iv
}
func (iv *ImageView) DrawImageFile(filepath string) error {
bmp, err := NewBitmapFromFile(filepath, RGB(255, 255, 0))
if err != nil {
return err
}
iv.bmp = bmp
return nil
}
func (iv *ImageView) DrawImage(bmp *Bitmap) {
iv.bmp = bmp
}
func (iv *ImageView) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case w32.WM_SIZE, w32.WM_SIZING:
iv.Invalidate(true)
case w32.WM_ERASEBKGND:
return 1 // important
case w32.WM_PAINT:
if iv.bmp != nil {
canvas := NewCanvasFromHwnd(iv.hwnd)
defer canvas.Dispose()
iv.SetSize(iv.bmp.Size())
canvas.DrawBitmap(iv.bmp, 0, 0)
}
}
return w32.DefWindowProc(iv.hwnd, msg, wparam, lparam)
}

View File

@@ -0,0 +1,342 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
*/
package winc
import (
"fmt"
"time"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
type direction int
const (
DirNone direction = iota
DirX
DirY
DirX2
DirY2
)
var ImageBoxPen = NewPen(w32.PS_GEOMETRIC, 2, NewSolidColorBrush(RGB(140, 140, 220)))
var ImageBoxHiPen = NewPen(w32.PS_GEOMETRIC, 2, NewSolidColorBrush(RGB(220, 140, 140)))
var ImageBoxMarkBrush = NewSolidColorBrush(RGB(40, 40, 40))
var ImageBoxMarkPen = NewPen(w32.PS_GEOMETRIC, 2, ImageBoxMarkBrush)
type ImageBox struct {
Name string
Type int
X, Y, X2, Y2 int
underMouse bool // dynamic value
}
func (b *ImageBox) Rect() *Rect {
return NewRect(b.X, b.Y, b.X2, b.Y2)
}
// ImageViewBox is image view with boxes.
type ImageViewBox struct {
ControlBase
bmp *Bitmap
mouseLeft bool
modified bool // used by GUI to see if any image box modified
add bool
Boxes []*ImageBox // might be persisted to file
dragBox *ImageBox
selBox *ImageBox
dragStartX, dragStartY int
resize direction
onSelectedChange EventManager
onAdd EventManager
onModify EventManager
}
func NewImageViewBox(parent Controller) *ImageViewBox {
iv := new(ImageViewBox)
iv.InitWindow("winc_ImageViewBox", parent, w32.WS_EX_CONTROLPARENT, w32.WS_CHILD|w32.WS_VISIBLE)
RegMsgHandler(iv)
iv.SetFont(DefaultFont)
iv.SetText("")
iv.SetSize(200, 65)
return iv
}
func (iv *ImageViewBox) OnSelectedChange() *EventManager {
return &iv.onSelectedChange
}
func (iv *ImageViewBox) OnAdd() *EventManager {
return &iv.onAdd
}
func (iv *ImageViewBox) OnModify() *EventManager {
return &iv.onModify
}
func (iv *ImageViewBox) IsModified() bool { return iv.modified }
func (iv *ImageViewBox) SetModified(modified bool) { iv.modified = modified }
func (iv *ImageViewBox) IsLoaded() bool { return iv.bmp != nil }
func (iv *ImageViewBox) AddMode() bool { return iv.add }
func (iv *ImageViewBox) SetAddMode(add bool) { iv.add = add }
func (iv *ImageViewBox) HasSelected() bool { return iv.selBox != nil && iv.bmp != nil }
func (iv *ImageViewBox) wasModified() {
iv.modified = true
iv.onModify.Fire(NewEvent(iv, nil))
}
func (iv *ImageViewBox) DeleteSelected() {
if iv.selBox != nil {
for i, b := range iv.Boxes {
if b == iv.selBox {
iv.Boxes = append(iv.Boxes[:i], iv.Boxes[i+1:]...)
iv.selBox = nil
iv.Invalidate(true)
iv.wasModified()
iv.onSelectedChange.Fire(NewEvent(iv, nil))
return
}
}
}
}
func (iv *ImageViewBox) NameSelected() string {
if iv.selBox != nil {
return iv.selBox.Name
}
return ""
}
func (iv *ImageViewBox) SetNameSelected(name string) {
if iv.selBox != nil {
iv.selBox.Name = name
iv.wasModified()
}
}
func (iv *ImageViewBox) TypeSelected() int {
if iv.selBox != nil {
return iv.selBox.Type
}
return 0
}
func (iv *ImageViewBox) SetTypeSelected(typ int) {
if iv.selBox != nil {
iv.selBox.Type = typ
iv.wasModified()
}
}
func (ib *ImageViewBox) updateHighlight(x, y int) bool {
var changed bool
for _, b := range ib.Boxes {
under := x >= b.X && y >= b.Y && x <= b.X2 && y <= b.Y2
if b.underMouse != under {
changed = true
}
b.underMouse = under
/*if sel {
break // allow only one to be underMouse
}*/
}
return changed
}
func (ib *ImageViewBox) isUnderMouse(x, y int) *ImageBox {
for _, b := range ib.Boxes {
if x >= b.X && y >= b.Y && x <= b.X2 && y <= b.Y2 {
return b
}
}
return nil
}
func (ib *ImageViewBox) getCursor(x, y int) uint16 {
for _, b := range ib.Boxes {
switch d := ib.resizingDirection(b, x, y); d {
case DirY, DirY2:
return w32.IDC_SIZENS
case DirX, DirX2:
return w32.IDC_SIZEWE
}
// w32.IDC_SIZEALL or w32.IDC_SIZE for resize
}
return w32.IDC_ARROW
}
func (ib *ImageViewBox) resizingDirection(b *ImageBox, x, y int) direction {
if b == nil {
return DirNone
}
switch {
case b.X == x || b.X == x-1 || b.X == x+1:
return DirX
case b.X2 == x || b.X2 == x-1 || b.X2 == x+1:
return DirX2
case b.Y == y || b.Y == y-1 || b.Y == y+1:
return DirY
case b.Y2 == y || b.Y2 == y-1 || b.Y2 == y+1:
return DirY2
}
return DirNone
}
func (ib *ImageViewBox) resizeToDirection(b *ImageBox, x, y int) {
switch ib.resize {
case DirX:
b.X = x
case DirY:
b.Y = y
case DirX2:
b.X2 = x
case DirY2:
b.Y2 = y
}
}
func (ib *ImageViewBox) drag(b *ImageBox, x, y int) {
w, h := b.X2-b.X, b.Y2-b.Y
nx := ib.dragStartX - b.X
ny := ib.dragStartY - b.Y
b.X = x - nx
b.Y = y - ny
b.X2 = b.X + w
b.Y2 = b.Y + h
ib.dragStartX, ib.dragStartY = x, y
}
func (iv *ImageViewBox) DrawImageFile(filepath string) (err error) {
iv.bmp, err = NewBitmapFromFile(filepath, RGB(255, 255, 0))
iv.selBox = nil
iv.modified = false
iv.onSelectedChange.Fire(NewEvent(iv, nil))
iv.onModify.Fire(NewEvent(iv, nil))
return
}
func (iv *ImageViewBox) DrawImage(bmp *Bitmap) {
iv.bmp = bmp
iv.selBox = nil
iv.modified = false
iv.onSelectedChange.Fire(NewEvent(iv, nil))
iv.onModify.Fire(NewEvent(iv, nil))
}
func (iv *ImageViewBox) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case w32.WM_SIZE, w32.WM_SIZING:
iv.Invalidate(true)
case w32.WM_ERASEBKGND:
return 1 // important
case w32.WM_CREATE:
internalTrackMouseEvent(iv.hwnd)
case w32.WM_PAINT:
if iv.bmp != nil {
canvas := NewCanvasFromHwnd(iv.hwnd)
defer canvas.Dispose()
iv.SetSize(iv.bmp.Size())
canvas.DrawBitmap(iv.bmp, 0, 0)
for _, b := range iv.Boxes {
// old code used NewSystemColorBrush(w32.COLOR_BTNFACE) w32.COLOR_WINDOW
pen := ImageBoxPen
if b.underMouse {
pen = ImageBoxHiPen
}
canvas.DrawRect(b.Rect(), pen)
if b == iv.selBox {
x1 := []int{b.X, b.X2, b.X2, b.X}
y1 := []int{b.Y, b.Y, b.Y2, b.Y2}
for i := 0; i < len(x1); i++ {
r := NewRect(x1[i]-2, y1[i]-2, x1[i]+2, y1[i]+2)
canvas.DrawFillRect(r, ImageBoxMarkPen, ImageBoxMarkBrush)
}
}
}
}
case w32.WM_MOUSEMOVE:
x, y := genPoint(lparam)
if iv.dragBox != nil {
if iv.resize == DirNone {
iv.drag(iv.dragBox, x, y)
iv.wasModified()
} else {
iv.resizeToDirection(iv.dragBox, x, y)
iv.wasModified()
}
iv.Invalidate(true)
} else {
if !iv.add {
w32.SetCursor(w32.LoadCursorWithResourceID(0, iv.getCursor(x, y)))
}
// do not call repaint if underMouse item did not change.
if iv.updateHighlight(x, y) {
iv.Invalidate(true)
}
}
if iv.mouseLeft {
internalTrackMouseEvent(iv.hwnd)
iv.mouseLeft = false
}
case w32.WM_MOUSELEAVE:
iv.dragBox = nil
iv.mouseLeft = true
iv.updateHighlight(-1, -1)
iv.Invalidate(true)
case w32.WM_LBUTTONUP:
iv.dragBox = nil
case w32.WM_LBUTTONDOWN:
x, y := genPoint(lparam)
if iv.add {
now := time.Now()
s := fmt.Sprintf("field%s", now.Format("020405"))
b := &ImageBox{Name: s, underMouse: true, X: x, Y: y, X2: x + 150, Y2: y + 30}
iv.Boxes = append(iv.Boxes, b)
iv.selBox = b
iv.wasModified()
iv.onAdd.Fire(NewEvent(iv, nil))
} else {
iv.dragBox = iv.isUnderMouse(x, y)
iv.selBox = iv.dragBox
iv.dragStartX, iv.dragStartY = x, y
iv.resize = iv.resizingDirection(iv.dragBox, x, y)
}
iv.Invalidate(true)
iv.onSelectedChange.Fire(NewEvent(iv, nil))
case w32.WM_RBUTTONDOWN:
}
return w32.DefWindowProc(iv.hwnd, msg, wparam, lparam)
}

View File

@@ -0,0 +1,21 @@
//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"
)
func init() {
gControllerRegistry = make(map[w32.HWND]Controller)
gRegisteredClasses = make([]string, 0)
var si w32.GdiplusStartupInput
si.GdiplusVersion = 1
w32.GdiplusStartup(&si, nil)
}

View File

@@ -0,0 +1,440 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
* Copyright (C) 2010-2013 Allen Dang. All Rights Reserved.
*/
package winc
import (
"bytes"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
type Key uint16
func (k Key) String() string {
return key2string[k]
}
const (
KeyLButton Key = w32.VK_LBUTTON
KeyRButton Key = w32.VK_RBUTTON
KeyCancel Key = w32.VK_CANCEL
KeyMButton Key = w32.VK_MBUTTON
KeyXButton1 Key = w32.VK_XBUTTON1
KeyXButton2 Key = w32.VK_XBUTTON2
KeyBack Key = w32.VK_BACK
KeyTab Key = w32.VK_TAB
KeyClear Key = w32.VK_CLEAR
KeyReturn Key = w32.VK_RETURN
KeyShift Key = w32.VK_SHIFT
KeyControl Key = w32.VK_CONTROL
KeyAlt Key = w32.VK_MENU
KeyMenu Key = w32.VK_MENU
KeyPause Key = w32.VK_PAUSE
KeyCapital Key = w32.VK_CAPITAL
KeyKana Key = w32.VK_KANA
KeyHangul Key = w32.VK_HANGUL
KeyJunja Key = w32.VK_JUNJA
KeyFinal Key = w32.VK_FINAL
KeyHanja Key = w32.VK_HANJA
KeyKanji Key = w32.VK_KANJI
KeyEscape Key = w32.VK_ESCAPE
KeyConvert Key = w32.VK_CONVERT
KeyNonconvert Key = w32.VK_NONCONVERT
KeyAccept Key = w32.VK_ACCEPT
KeyModeChange Key = w32.VK_MODECHANGE
KeySpace Key = w32.VK_SPACE
KeyPrior Key = w32.VK_PRIOR
KeyNext Key = w32.VK_NEXT
KeyEnd Key = w32.VK_END
KeyHome Key = w32.VK_HOME
KeyLeft Key = w32.VK_LEFT
KeyUp Key = w32.VK_UP
KeyRight Key = w32.VK_RIGHT
KeyDown Key = w32.VK_DOWN
KeySelect Key = w32.VK_SELECT
KeyPrint Key = w32.VK_PRINT
KeyExecute Key = w32.VK_EXECUTE
KeySnapshot Key = w32.VK_SNAPSHOT
KeyInsert Key = w32.VK_INSERT
KeyDelete Key = w32.VK_DELETE
KeyHelp Key = w32.VK_HELP
Key0 Key = 0x30
Key1 Key = 0x31
Key2 Key = 0x32
Key3 Key = 0x33
Key4 Key = 0x34
Key5 Key = 0x35
Key6 Key = 0x36
Key7 Key = 0x37
Key8 Key = 0x38
Key9 Key = 0x39
KeyA Key = 0x41
KeyB Key = 0x42
KeyC Key = 0x43
KeyD Key = 0x44
KeyE Key = 0x45
KeyF Key = 0x46
KeyG Key = 0x47
KeyH Key = 0x48
KeyI Key = 0x49
KeyJ Key = 0x4A
KeyK Key = 0x4B
KeyL Key = 0x4C
KeyM Key = 0x4D
KeyN Key = 0x4E
KeyO Key = 0x4F
KeyP Key = 0x50
KeyQ Key = 0x51
KeyR Key = 0x52
KeyS Key = 0x53
KeyT Key = 0x54
KeyU Key = 0x55
KeyV Key = 0x56
KeyW Key = 0x57
KeyX Key = 0x58
KeyY Key = 0x59
KeyZ Key = 0x5A
KeyLWIN Key = w32.VK_LWIN
KeyRWIN Key = w32.VK_RWIN
KeyApps Key = w32.VK_APPS
KeySleep Key = w32.VK_SLEEP
KeyNumpad0 Key = w32.VK_NUMPAD0
KeyNumpad1 Key = w32.VK_NUMPAD1
KeyNumpad2 Key = w32.VK_NUMPAD2
KeyNumpad3 Key = w32.VK_NUMPAD3
KeyNumpad4 Key = w32.VK_NUMPAD4
KeyNumpad5 Key = w32.VK_NUMPAD5
KeyNumpad6 Key = w32.VK_NUMPAD6
KeyNumpad7 Key = w32.VK_NUMPAD7
KeyNumpad8 Key = w32.VK_NUMPAD8
KeyNumpad9 Key = w32.VK_NUMPAD9
KeyMultiply Key = w32.VK_MULTIPLY
KeyAdd Key = w32.VK_ADD
KeySeparator Key = w32.VK_SEPARATOR
KeySubtract Key = w32.VK_SUBTRACT
KeyDecimal Key = w32.VK_DECIMAL
KeyDivide Key = w32.VK_DIVIDE
KeyF1 Key = w32.VK_F1
KeyF2 Key = w32.VK_F2
KeyF3 Key = w32.VK_F3
KeyF4 Key = w32.VK_F4
KeyF5 Key = w32.VK_F5
KeyF6 Key = w32.VK_F6
KeyF7 Key = w32.VK_F7
KeyF8 Key = w32.VK_F8
KeyF9 Key = w32.VK_F9
KeyF10 Key = w32.VK_F10
KeyF11 Key = w32.VK_F11
KeyF12 Key = w32.VK_F12
KeyF13 Key = w32.VK_F13
KeyF14 Key = w32.VK_F14
KeyF15 Key = w32.VK_F15
KeyF16 Key = w32.VK_F16
KeyF17 Key = w32.VK_F17
KeyF18 Key = w32.VK_F18
KeyF19 Key = w32.VK_F19
KeyF20 Key = w32.VK_F20
KeyF21 Key = w32.VK_F21
KeyF22 Key = w32.VK_F22
KeyF23 Key = w32.VK_F23
KeyF24 Key = w32.VK_F24
KeyNumlock Key = w32.VK_NUMLOCK
KeyScroll Key = w32.VK_SCROLL
KeyLShift Key = w32.VK_LSHIFT
KeyRShift Key = w32.VK_RSHIFT
KeyLControl Key = w32.VK_LCONTROL
KeyRControl Key = w32.VK_RCONTROL
KeyLAlt Key = w32.VK_LMENU
KeyLMenu Key = w32.VK_LMENU
KeyRAlt Key = w32.VK_RMENU
KeyRMenu Key = w32.VK_RMENU
KeyBrowserBack Key = w32.VK_BROWSER_BACK
KeyBrowserForward Key = w32.VK_BROWSER_FORWARD
KeyBrowserRefresh Key = w32.VK_BROWSER_REFRESH
KeyBrowserStop Key = w32.VK_BROWSER_STOP
KeyBrowserSearch Key = w32.VK_BROWSER_SEARCH
KeyBrowserFavorites Key = w32.VK_BROWSER_FAVORITES
KeyBrowserHome Key = w32.VK_BROWSER_HOME
KeyVolumeMute Key = w32.VK_VOLUME_MUTE
KeyVolumeDown Key = w32.VK_VOLUME_DOWN
KeyVolumeUp Key = w32.VK_VOLUME_UP
KeyMediaNextTrack Key = w32.VK_MEDIA_NEXT_TRACK
KeyMediaPrevTrack Key = w32.VK_MEDIA_PREV_TRACK
KeyMediaStop Key = w32.VK_MEDIA_STOP
KeyMediaPlayPause Key = w32.VK_MEDIA_PLAY_PAUSE
KeyLaunchMail Key = w32.VK_LAUNCH_MAIL
KeyLaunchMediaSelect Key = w32.VK_LAUNCH_MEDIA_SELECT
KeyLaunchApp1 Key = w32.VK_LAUNCH_APP1
KeyLaunchApp2 Key = w32.VK_LAUNCH_APP2
KeyOEM1 Key = w32.VK_OEM_1
KeyOEMPlus Key = w32.VK_OEM_PLUS
KeyOEMComma Key = w32.VK_OEM_COMMA
KeyOEMMinus Key = w32.VK_OEM_MINUS
KeyOEMPeriod Key = w32.VK_OEM_PERIOD
KeyOEM2 Key = w32.VK_OEM_2
KeyOEM3 Key = w32.VK_OEM_3
KeyOEM4 Key = w32.VK_OEM_4
KeyOEM5 Key = w32.VK_OEM_5
KeyOEM6 Key = w32.VK_OEM_6
KeyOEM7 Key = w32.VK_OEM_7
KeyOEM8 Key = w32.VK_OEM_8
KeyOEM102 Key = w32.VK_OEM_102
KeyProcessKey Key = w32.VK_PROCESSKEY
KeyPacket Key = w32.VK_PACKET
KeyAttn Key = w32.VK_ATTN
KeyCRSel Key = w32.VK_CRSEL
KeyEXSel Key = w32.VK_EXSEL
KeyErEOF Key = w32.VK_EREOF
KeyPlay Key = w32.VK_PLAY
KeyZoom Key = w32.VK_ZOOM
KeyNoName Key = w32.VK_NONAME
KeyPA1 Key = w32.VK_PA1
KeyOEMClear Key = w32.VK_OEM_CLEAR
)
var key2string = map[Key]string{
KeyLButton: "LButton",
KeyRButton: "RButton",
KeyCancel: "Cancel",
KeyMButton: "MButton",
KeyXButton1: "XButton1",
KeyXButton2: "XButton2",
KeyBack: "Back",
KeyTab: "Tab",
KeyClear: "Clear",
KeyReturn: "Return",
KeyShift: "Shift",
KeyControl: "Control",
KeyAlt: "Alt / Menu",
KeyPause: "Pause",
KeyCapital: "Capital",
KeyKana: "Kana / Hangul",
KeyJunja: "Junja",
KeyFinal: "Final",
KeyHanja: "Hanja / Kanji",
KeyEscape: "Escape",
KeyConvert: "Convert",
KeyNonconvert: "Nonconvert",
KeyAccept: "Accept",
KeyModeChange: "ModeChange",
KeySpace: "Space",
KeyPrior: "Prior",
KeyNext: "Next",
KeyEnd: "End",
KeyHome: "Home",
KeyLeft: "Left",
KeyUp: "Up",
KeyRight: "Right",
KeyDown: "Down",
KeySelect: "Select",
KeyPrint: "Print",
KeyExecute: "Execute",
KeySnapshot: "Snapshot",
KeyInsert: "Insert",
KeyDelete: "Delete",
KeyHelp: "Help",
Key0: "0",
Key1: "1",
Key2: "2",
Key3: "3",
Key4: "4",
Key5: "5",
Key6: "6",
Key7: "7",
Key8: "8",
Key9: "9",
KeyA: "A",
KeyB: "B",
KeyC: "C",
KeyD: "D",
KeyE: "E",
KeyF: "F",
KeyG: "G",
KeyH: "H",
KeyI: "I",
KeyJ: "J",
KeyK: "K",
KeyL: "L",
KeyM: "M",
KeyN: "N",
KeyO: "O",
KeyP: "P",
KeyQ: "Q",
KeyR: "R",
KeyS: "S",
KeyT: "T",
KeyU: "U",
KeyV: "V",
KeyW: "W",
KeyX: "X",
KeyY: "Y",
KeyZ: "Z",
KeyLWIN: "LWIN",
KeyRWIN: "RWIN",
KeyApps: "Apps",
KeySleep: "Sleep",
KeyNumpad0: "Numpad0",
KeyNumpad1: "Numpad1",
KeyNumpad2: "Numpad2",
KeyNumpad3: "Numpad3",
KeyNumpad4: "Numpad4",
KeyNumpad5: "Numpad5",
KeyNumpad6: "Numpad6",
KeyNumpad7: "Numpad7",
KeyNumpad8: "Numpad8",
KeyNumpad9: "Numpad9",
KeyMultiply: "Multiply",
KeyAdd: "Add",
KeySeparator: "Separator",
KeySubtract: "Subtract",
KeyDecimal: "Decimal",
KeyDivide: "Divide",
KeyF1: "F1",
KeyF2: "F2",
KeyF3: "F3",
KeyF4: "F4",
KeyF5: "F5",
KeyF6: "F6",
KeyF7: "F7",
KeyF8: "F8",
KeyF9: "F9",
KeyF10: "F10",
KeyF11: "F11",
KeyF12: "F12",
KeyF13: "F13",
KeyF14: "F14",
KeyF15: "F15",
KeyF16: "F16",
KeyF17: "F17",
KeyF18: "F18",
KeyF19: "F19",
KeyF20: "F20",
KeyF21: "F21",
KeyF22: "F22",
KeyF23: "F23",
KeyF24: "F24",
KeyNumlock: "Numlock",
KeyScroll: "Scroll",
KeyLShift: "LShift",
KeyRShift: "RShift",
KeyLControl: "LControl",
KeyRControl: "RControl",
KeyLMenu: "LMenu",
KeyRMenu: "RMenu",
KeyBrowserBack: "BrowserBack",
KeyBrowserForward: "BrowserForward",
KeyBrowserRefresh: "BrowserRefresh",
KeyBrowserStop: "BrowserStop",
KeyBrowserSearch: "BrowserSearch",
KeyBrowserFavorites: "BrowserFavorites",
KeyBrowserHome: "BrowserHome",
KeyVolumeMute: "VolumeMute",
KeyVolumeDown: "VolumeDown",
KeyVolumeUp: "VolumeUp",
KeyMediaNextTrack: "MediaNextTrack",
KeyMediaPrevTrack: "MediaPrevTrack",
KeyMediaStop: "MediaStop",
KeyMediaPlayPause: "MediaPlayPause",
KeyLaunchMail: "LaunchMail",
KeyLaunchMediaSelect: "LaunchMediaSelect",
KeyLaunchApp1: "LaunchApp1",
KeyLaunchApp2: "LaunchApp2",
KeyOEM1: "OEM1",
KeyOEMPlus: "OEMPlus",
KeyOEMComma: "OEMComma",
KeyOEMMinus: "OEMMinus",
KeyOEMPeriod: "OEMPeriod",
KeyOEM2: "OEM2",
KeyOEM3: "OEM3",
KeyOEM4: "OEM4",
KeyOEM5: "OEM5",
KeyOEM6: "OEM6",
KeyOEM7: "OEM7",
KeyOEM8: "OEM8",
KeyOEM102: "OEM102",
KeyProcessKey: "ProcessKey",
KeyPacket: "Packet",
KeyAttn: "Attn",
KeyCRSel: "CRSel",
KeyEXSel: "EXSel",
KeyErEOF: "ErEOF",
KeyPlay: "Play",
KeyZoom: "Zoom",
KeyNoName: "NoName",
KeyPA1: "PA1",
KeyOEMClear: "OEMClear",
}
type Modifiers byte
func (m Modifiers) String() string {
return modifiers2string[m]
}
var modifiers2string = map[Modifiers]string{
ModShift: "Shift",
ModControl: "Ctrl",
ModControl | ModShift: "Ctrl+Shift",
ModAlt: "Alt",
ModAlt | ModShift: "Alt+Shift",
ModAlt | ModControl | ModShift: "Alt+Ctrl+Shift",
}
const (
ModShift Modifiers = 1 << iota
ModControl
ModAlt
)
func ModifiersDown() Modifiers {
var m Modifiers
if ShiftDown() {
m |= ModShift
}
if ControlDown() {
m |= ModControl
}
if AltDown() {
m |= ModAlt
}
return m
}
type Shortcut struct {
Modifiers Modifiers
Key Key
}
func (s Shortcut) String() string {
m := s.Modifiers.String()
if m == "" {
return s.Key.String()
}
b := new(bytes.Buffer)
b.WriteString(m)
b.WriteRune('+')
b.WriteString(s.Key.String())
return b.String()
}
func AltDown() bool {
return w32.GetKeyState(int32(KeyAlt))>>15 != 0
}
func ControlDown() bool {
return w32.GetKeyState(int32(KeyControl))>>15 != 0
}
func ShiftDown() bool {
return w32.GetKeyState(int32(KeyShift))>>15 != 0
}

View File

@@ -0,0 +1,31 @@
//go:build windows
/*
* Copyright (C) 2019 The Winc Authors. All Rights Reserved.
*/
package winc
import (
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)
type Label struct {
ControlBase
}
func NewLabel(parent Controller) *Label {
lb := new(Label)
lb.InitControl("STATIC", parent, 0, w32.WS_CHILD|w32.WS_VISIBLE|w32.SS_LEFTNOWORDWRAP)
RegMsgHandler(lb)
lb.SetFont(DefaultFont)
lb.SetText("Label")
lb.SetSize(100, 25)
return lb
}
func (lb *Label) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
return w32.DefWindowProc(lb.hwnd, msg, wparam, lparam)
}

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