diff --git a/exp/examples/dialogs/main.go b/exp/examples/dialogs/main.go index d6c40a3a1..97b436575 100644 --- a/exp/examples/dialogs/main.go +++ b/exp/examples/dialogs/main.go @@ -140,7 +140,7 @@ func main() { openMenu.Add("Open File").OnClick(func(ctx *application.Context) { result, _ := app.NewOpenFileDialog(). CanChooseFiles(true). - PromptForSingleFile() + PromptForSingleSelection() if result != "" { app.NewInfoDialog().SetMessage(result).Show() } else { @@ -152,7 +152,7 @@ func main() { CanChooseFiles(true). CanCreateDirectories(true). ShowHiddenFiles(true). - PromptForSingleFile() + PromptForSingleSelection() if result != "" { app.NewInfoDialog().SetMessage(result).Show() } else { @@ -165,7 +165,7 @@ func main() { CanCreateDirectories(true). ShowHiddenFiles(true). AttachToWindow(app.GetCurrentWindow()). - PromptForSingleFile() + PromptForSingleSelection() if result != "" { app.NewInfoDialog().SetMessage(result).Show() } else { @@ -177,7 +177,7 @@ func main() { CanChooseFiles(true). CanCreateDirectories(true). ShowHiddenFiles(true). - PromptForMultipleFiles() + PromptForMultipleSelection() if len(result) > 0 { app.NewInfoDialog().SetMessage(strings.Join(result, ",")).Show() } else { @@ -187,7 +187,7 @@ func main() { openMenu.Add("Open Directory").OnClick(func(ctx *application.Context) { result, _ := app.NewOpenFileDialog(). CanChooseDirectories(true). - PromptForSingleFile() + PromptForSingleSelection() if result != "" { app.NewInfoDialog().SetMessage(result).Show() } else { @@ -198,7 +198,7 @@ func main() { result, _ := app.NewOpenFileDialog(). CanChooseDirectories(true). CanCreateDirectories(true). - PromptForSingleFile() + PromptForSingleSelection() if result != "" { app.NewInfoDialog().SetMessage(result).Show() } else { @@ -206,6 +206,39 @@ func main() { } }) + saveMenu := menu.AddSubmenu("Save") + saveMenu.Add("Select File (Defaults)").OnClick(func(ctx *application.Context) { + result, _ := app.NewSaveFileDialog(). + PromptForSingleSelection() + if result != "" { + app.NewInfoDialog().SetMessage(result).Show() + } + }) + saveMenu.Add("Select File (Attach To Window)").OnClick(func(ctx *application.Context) { + result, _ := app.NewSaveFileDialog(). + AttachToWindow(app.GetCurrentWindow()). + PromptForSingleSelection() + if result != "" { + app.NewInfoDialog().SetMessage(result).Show() + } + }) + saveMenu.Add("Select File (Show Hidden Files)").OnClick(func(ctx *application.Context) { + result, _ := app.NewSaveFileDialog(). + ShowHiddenFiles(true). + PromptForSingleSelection() + if result != "" { + app.NewInfoDialog().SetMessage(result).Show() + } + }) + saveMenu.Add("Select File (Cannot Create Directories)").OnClick(func(ctx *application.Context) { + result, _ := app.NewSaveFileDialog(). + CanCreateDirectories(false). + PromptForSingleSelection() + if result != "" { + app.NewInfoDialog().SetMessage(result).Show() + } + }) + app.SetMenu(menu) app.NewWindow() diff --git a/exp/examples/plain/main.go b/exp/examples/plain/main.go index 64096a4b5..61329f7dd 100644 --- a/exp/examples/plain/main.go +++ b/exp/examples/plain/main.go @@ -12,6 +12,18 @@ import ( func main() { app := application.New() + // Create window + app.NewWindowWithOptions(&options.Window{ + Title: "Plain Bundle", + EnableDevTools: true, + HTML: `Plain Bundle

Plain Bundle

This is a plain bundle. It has no frontend code.

`, + CSS: `body { background-color: rgba(255, 255, 255, 0); } .main { color: white; margin: 20%; }`, + Mac: &options.MacWindow{ + InvisibleTitleBarHeight: 50, + Backdrop: options.MacBackdropTranslucent, + TitleBar: options.TitleBarHiddenInset, + }, + }) // Create window app.NewWindowWithOptions(&options.Window{ Title: "Plain Bundle", diff --git a/exp/examples/window/main.go b/exp/examples/window/main.go index 3ddaf6ebc..cb0dea554 100644 --- a/exp/examples/window/main.go +++ b/exp/examples/window/main.go @@ -23,7 +23,7 @@ func main() { EnableDevTools: true, Mac: &options.MacWindow{ Backdrop: options.MacBackdropTranslucent, - TitleBar: options.TitleBarHiddenInset, + TitleBar: options.TitleBarHiddenInsetUnified, }, }) myWindow.On(events.Mac.WindowDidBecomeMain, func() { diff --git a/exp/pkg/application/application.go b/exp/pkg/application/application.go index 3408e9305..067372a0d 100644 --- a/exp/pkg/application/application.go +++ b/exp/pkg/application/application.go @@ -152,7 +152,7 @@ func (a *App) NewSystemTray() *SystemTray { } func (a *App) Run() error { - a.impl = newPlatformApp(a.options) + a.impl = newPlatformApp(a) a.running = true go func() { @@ -344,3 +344,7 @@ func (a *App) NewOpenDirectoryDialog() *MessageDialog { func (a *App) NewOpenFileDialog() *OpenFileDialog { return newOpenFileDialog() } + +func (a *App) NewSaveFileDialog() *SaveFileDialog { + return newSaveFileDialog() +} diff --git a/exp/pkg/application/application_darwin.go b/exp/pkg/application/application_darwin.go index 4699383fe..ff13bbad1 100644 --- a/exp/pkg/application/application_darwin.go +++ b/exp/pkg/application/application_darwin.go @@ -20,6 +20,28 @@ static void init(void) { [NSApplication sharedApplication]; appDelegate = [[AppDelegate alloc] init]; [NSApp setDelegate:appDelegate]; + + [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) { + NSWindow* eventWindow = [event window]; + if (![eventWindow respondsToSelector:@selector(handleLeftMouseDown)]) { + return event; + } + + WindowDelegate* windowDelegate = (WindowDelegate*)[eventWindow delegate]; + [windowDelegate handleLeftMouseDown:event]; + return event; + }]; + + [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseUp handler:^NSEvent * _Nullable(NSEvent * _Nonnull event) { + NSWindow* eventWindow = [event window]; + if (![eventWindow respondsToSelector:@selector(handleLeftMouseUp)]) { + return event; + } + + WindowDelegate* windowDelegate = (WindowDelegate*)[eventWindow delegate]; + [windowDelegate handleLeftMouseUp:eventWindow]; + return event; + }]; } static void setActivationPolicy(int policy) { @@ -79,12 +101,14 @@ import "C" import ( "unsafe" + "github.com/wailsapp/wails/exp/pkg/events" + "github.com/wailsapp/wails/exp/pkg/options" ) type macosApp struct { - options *options.Application applicationMenu unsafe.Pointer + parent *App } func (m *macosApp) setIcon(icon []byte) { @@ -107,12 +131,19 @@ func (m *macosApp) setApplicationMenu(menu *Menu) { menu = defaultApplicationMenu() } menu.Update() + // Convert impl to macosMenu object m.applicationMenu = (menu.impl).(*macosMenu).nsMenu C.setApplicationMenu(m.applicationMenu) } func (m *macosApp) run() error { + m.parent.On(events.Mac.ApplicationDidFinishLaunching, func() { + if m.parent.options != nil && m.parent.options.Mac != nil { + C.setActivationPolicy(C.int(m.parent.options.Mac.ActivationPolicy)) + } + C.activateIgnoringOtherApps() + }) C.run() return nil } @@ -121,15 +152,14 @@ func (m *macosApp) destroy() { C.destroyApp() } -func newPlatformApp(appOptions *options.Application) *macosApp { +func newPlatformApp(app *App) *macosApp { + appOptions := app.options if appOptions == nil { appOptions = options.ApplicationDefaults } C.init() - C.setActivationPolicy(C.int(appOptions.Mac.ActivationPolicy)) - C.activateIgnoringOtherApps() return &macosApp{ - options: appOptions, + parent: app, } } diff --git a/exp/pkg/application/dialogs.go b/exp/pkg/application/dialogs.go index 6309f2f6c..7ba5b7628 100644 --- a/exp/pkg/application/dialogs.go +++ b/exp/pkg/application/dialogs.go @@ -7,6 +7,7 @@ import ( type DialogType int +// TODO: Make this a map and clear it when the dialog is closed var dialogID uint var dialogIDLock sync.RWMutex @@ -18,6 +19,7 @@ func getDialogID() uint { } var openFileResponses = make(map[uint]chan string) +var saveFileResponses = make(map[uint]chan string) const ( InfoDialog DialogType = iota @@ -154,7 +156,7 @@ func (d *OpenFileDialog) AttachToWindow(window *Window) *OpenFileDialog { return d } -func (d *OpenFileDialog) PromptForSingleFile() (string, error) { +func (d *OpenFileDialog) PromptForSingleSelection() (string, error) { d.allowsMultipleSelection = false if d.impl == nil { d.impl = newOpenFileDialogImpl(d) @@ -168,7 +170,7 @@ func (d *OpenFileDialog) PromptForSingleFile() (string, error) { return result, err } -func (d *OpenFileDialog) PromptForMultipleFiles() ([]string, error) { +func (d *OpenFileDialog) PromptForMultipleSelection() ([]string, error) { d.allowsMultipleSelection = true if d.impl == nil { d.impl = newOpenFileDialogImpl(d) @@ -181,6 +183,48 @@ func newOpenFileDialog() *OpenFileDialog { id: getDialogID(), canChooseDirectories: false, canChooseFiles: true, - canCreateDirectories: false, + canCreateDirectories: true, } } + +func newSaveFileDialog() *SaveFileDialog { + return &SaveFileDialog{ + id: getDialogID(), + canCreateDirectories: true, + } +} + +type SaveFileDialog struct { + id uint + canCreateDirectories bool + showHiddenFiles bool + window *Window + + impl saveFileDialogImpl +} + +type saveFileDialogImpl interface { + show() (string, error) +} + +func (d *SaveFileDialog) CanCreateDirectories(canCreateDirectories bool) *SaveFileDialog { + d.canCreateDirectories = canCreateDirectories + return d +} + +func (d *SaveFileDialog) ShowHiddenFiles(showHiddenFiles bool) *SaveFileDialog { + d.showHiddenFiles = showHiddenFiles + return d +} + +func (d *SaveFileDialog) AttachToWindow(window *Window) *SaveFileDialog { + d.window = window + return d +} + +func (d *SaveFileDialog) PromptForSingleSelection() (string, error) { + if d.impl == nil { + d.impl = newSaveFileDialogImpl(d) + } + return d.impl.show() +} diff --git a/exp/pkg/application/dialogs_darwin.go b/exp/pkg/application/dialogs_darwin.go index 67eb8406c..3bb004dca 100644 --- a/exp/pkg/application/dialogs_darwin.go +++ b/exp/pkg/application/dialogs_darwin.go @@ -10,6 +10,7 @@ package application extern void openFileDialogCallback(uint id, char* path); extern void openFileDialogCallbackEnd(uint id); +extern void saveFileDialogCallback(uint id, char* path); static void showAboutBox(char* title, char *message, void *icon, int length) { @@ -146,6 +147,52 @@ static void showOpenFileDialog(unsigned int dialogID, bool canChooseFiles, bool }); } +static void showSaveFileDialog(unsigned int dialogID, bool canCreateDirectories, bool showHiddenFiles, void *window) { + // run on main thread + dispatch_async(dispatch_get_main_queue(), ^{ + NSSavePanel *panel = [NSSavePanel savePanel]; + + [panel setCanCreateDirectories:canCreateDirectories]; + [panel setShowsHiddenFiles:showHiddenFiles]; + + //if (title != NULL) { + // [panel setTitle:[NSString stringWithUTF8String:title]]; + // free(title); + //} + + //if (defaultFilename != NULL) { + // [panel setNameFieldStringValue:[NSString stringWithUTF8String:defaultFilename]]; + // free(defaultFilename); + //} + // + //if (defaultDirectory != NULL) { + // [panel setDirectoryURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:defaultDirectory]]]; + // free(defaultDirectory); + //} + + if (window != NULL) { + [panel beginSheetModalForWindow:(__bridge NSWindow *)window completionHandler:^(NSInteger result) { + const char *path = NULL; + if (result == NSModalResponseOK) { + NSURL *url = [panel URL]; + const char *path = [[url path] UTF8String]; + } + saveFileDialogCallback(dialogID, (char *)path); + }]; + } else { + [panel beginWithCompletionHandler:^(NSInteger result) { + const char *path = NULL; + if (result == NSModalResponseOK) { + NSURL *url = [panel URL]; + const char *path = [[url path] UTF8String]; + } + saveFileDialogCallback(dialogID, (char *)path); + }]; + } + }); +} + + */ import "C" import ( @@ -296,3 +343,40 @@ func openFileDialogCallbackEnd(id C.uint) { panic("No channel found for open file dialog") } } + +type macosSaveFileDialog struct { + dialog *SaveFileDialog +} + +func newSaveFileDialogImpl(d *SaveFileDialog) *macosSaveFileDialog { + return &macosSaveFileDialog{ + dialog: d, + } +} + +func (m *macosSaveFileDialog) show() (string, error) { + saveFileResponses[dialogID] = make(chan string) + nsWindow := unsafe.Pointer(nil) + if m.dialog.window != nil { + // get NSWindow from window + nsWindow = m.dialog.window.impl.(*macosWindow).nsWindow + } + C.showSaveFileDialog(C.uint(m.dialog.id), + C.bool(m.dialog.canCreateDirectories), + C.bool(m.dialog.showHiddenFiles), + nsWindow) + return <-saveFileResponses[m.dialog.id], nil +} + +//export saveFileDialogCallback +func saveFileDialogCallback(id C.uint, path *C.char) { + // Covert the path to a string + filePath := C.GoString(path) + // put response on channel + channel, ok := saveFileResponses[uint(id)] + if ok { + channel <- filePath + } else { + panic("No channel found for save file dialog") + } +} diff --git a/exp/pkg/application/window_darwin.go b/exp/pkg/application/window_darwin.go index db0e187a4..f4471eac4 100644 --- a/exp/pkg/application/window_darwin.go +++ b/exp/pkg/application/window_darwin.go @@ -56,6 +56,15 @@ void* windowNew(unsigned int id, int width, int height) { return window; } +// setInvisibleTitleBarHeight sets the invisible title bar height +void setInvisibleTitleBarHeight(void* window, unsigned int height) { + NSWindow* nsWindow = (NSWindow*)window; + // Get delegate + WindowDelegate* delegate = (WindowDelegate*)[nsWindow delegate]; + // Set height + delegate.invisibleTitleBarHeight = height; +} + //// Make window toggle frameless //void windowSetFrameless(void* window, bool frameless) { // NSWindow* nsWindow = (NSWindow*)window; @@ -81,6 +90,18 @@ void windowSetTransparent(void* nsWindow) { }); } +void windowSetInvisibleTitleBar(void* nsWindow, unsigned int height) { + // On main thread + dispatch_async(dispatch_get_main_queue(), ^{ + NSWindow* window = (NSWindow*)nsWindow; + // Get delegate + WindowDelegate* delegate = (WindowDelegate*)[window delegate]; + // Set height + delegate.invisibleTitleBarHeight = height; + }); +} + + // Set the title of the NSWindow void windowSetTitle(void* nsWindow, char* title) { // Set window title on main thread @@ -407,7 +428,7 @@ void windowSetHideTitle(void* nsWindow, bool hideTitle) { } // Set Window use toolbar -void windowSetUseToolbar(void* nsWindow, bool useToolbar) { +void windowSetUseToolbar(void* nsWindow, bool useToolbar, int toolbarStyle) { // Set window use toolbar on main thread dispatch_async(dispatch_get_main_queue(), ^{ // get main window @@ -416,12 +437,35 @@ void windowSetUseToolbar(void* nsWindow, bool useToolbar) { NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"wails.toolbar"]; [toolbar autorelease]; [window setToolbar:toolbar]; + + // If macos 11 or higher, set toolbar style + if (@available(macOS 11.0, *)) { + [window setToolbarStyle:toolbarStyle]; + } + } else { [window setToolbar:nil]; } }); } +// Set window toolbar style +void windowSetToolbarStyle(void* nsWindow, int style) { + // use @available to check if the function is available + // if not, return + if (@available(macOS 11.0, *)) { + // Set window toolbar style on main thread + dispatch_async(dispatch_get_main_queue(), ^{ + // get main window + NSWindow* window = (NSWindow*)nsWindow; + // get toolbar + NSToolbar* toolbar = [window toolbar]; + // set toolbar style + [toolbar setShowsBaselineSeparator:style]; + }); + } +} + // Set Hide Toolbar Separator void windowSetHideToolbarSeparator(void* nsWindow, bool hideSeparator) { // Set window hide toolbar separator on main thread @@ -808,7 +852,9 @@ func (w *macosWindow) run() { C.windowSetHideTitleBar(w.nsWindow, C.bool(titleBarOptions.Hide)) C.windowSetHideTitle(w.nsWindow, C.bool(titleBarOptions.HideTitle)) C.windowSetFullSizeContent(w.nsWindow, C.bool(titleBarOptions.FullSizeContent)) - C.windowSetUseToolbar(w.nsWindow, C.bool(titleBarOptions.UseToolbar)) + if titleBarOptions.UseToolbar { + C.windowSetUseToolbar(w.nsWindow, C.bool(titleBarOptions.UseToolbar), C.int(titleBarOptions.ToolbarStyle)) + } C.windowSetHideToolbarSeparator(w.nsWindow, C.bool(titleBarOptions.HideToolbarSeparator)) } @@ -816,15 +862,18 @@ func (w *macosWindow) run() { C.windowSetAppearanceTypeByName(w.nsWindow, C.CString(string(macOptions.Appearance))) } - switch w.parent.options.StartState { - case options.WindowStateMaximised: - w.setMaximised() - case options.WindowStateMinimised: - w.setMinimised() - case options.WindowStateFullscreen: - w.setFullscreen() - + if macOptions.InvisibleTitleBarHeight != 0 { + C.windowSetInvisibleTitleBar(w.nsWindow, C.uint(macOptions.InvisibleTitleBarHeight)) } + } + + switch w.parent.options.StartState { + case options.WindowStateMaximised: + w.setMaximised() + case options.WindowStateMinimised: + w.setMinimised() + case options.WindowStateFullscreen: + w.setFullscreen() } C.windowCenter(w.nsWindow) @@ -832,7 +881,7 @@ func (w *macosWindow) run() { if w.parent.options.URL != "" { w.navigateToURL(w.parent.options.URL) } - // Ee need to wait for the HTML to load before we can execute the javascript + // We need to wait for the HTML to load before we can execute the javascript w.parent.On(events.Mac.WebViewDidFinishNavigation, func() { if w.parent.options.JS != "" { w.execJS(w.parent.options.JS) diff --git a/exp/pkg/application/window_delegate.h b/exp/pkg/application/window_delegate.h index 3927efc6f..71baebef9 100644 --- a/exp/pkg/application/window_delegate.h +++ b/exp/pkg/application/window_delegate.h @@ -11,6 +11,11 @@ @property bool hideOnClose; @property (retain) WKWebView* webView; @property unsigned int windowId; +@property (retain) NSEvent* leftMouseEvent; +@property unsigned int invisibleTitleBarHeight; + +- (void)handleLeftMouseUp:(NSWindow *)window; +- (void)handleLeftMouseDown:(NSEvent*)event; @end diff --git a/exp/pkg/application/window_delegate.m b/exp/pkg/application/window_delegate.m index 8f21e7086..5f4ffb762 100644 --- a/exp/pkg/application/window_delegate.m +++ b/exp/pkg/application/window_delegate.m @@ -37,10 +37,26 @@ extern void processMessage(unsigned int, const char*); processMessage(self.windowId, _m); } -- (void) mouseDown:(NSEvent*)someEvent { - NSLog(@"MOUSE DOWN!!!"); +- (void)handleLeftMouseDown:(NSEvent *)event { + self.leftMouseEvent = event; + NSWindow *window = [event window]; + WindowDelegate* delegate = (WindowDelegate*)[window delegate]; + + if( self.invisibleTitleBarHeight > 0 ) { + NSPoint location = [event locationInWindow]; + NSRect frame = [window frame]; + if( location.y > frame.size.height - self.invisibleTitleBarHeight ) { + [window performWindowDragWithEvent:event]; + return; + } + } } +- (void)handleLeftMouseUp:(NSWindow *)window { + self.leftMouseEvent = nil; +} + + // GENERATED EVENTS START - (void)windowDidBecomeKey:(NSNotification *)notification { processWindowEvent(self.windowId, EventWindowDidBecomeKey); diff --git a/exp/pkg/application/window_devtools.go b/exp/pkg/application/window_devtools.go index d8382dfa1..d73e0c3b3 100644 --- a/exp/pkg/application/window_devtools.go +++ b/exp/pkg/application/window_devtools.go @@ -24,11 +24,6 @@ void showDevTools(void *window) { WindowDelegate* delegate = (WindowDelegate*)[(NSWindow*)window delegate]; dispatch_async(dispatch_get_main_queue(), ^{ [delegate.webView._inspector show]; - //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. - // [delegate.webView._inspector detach]; - //}); }); } diff --git a/exp/pkg/options/mac.go b/exp/pkg/options/mac.go index 01a6c9d27..9851c925c 100644 --- a/exp/pkg/options/mac.go +++ b/exp/pkg/options/mac.go @@ -22,11 +22,27 @@ const ( MacBackdropTranslucent ) +type MacToolbarStyle int + +const ( + // MacToolbarStyleAutomatic - The default value. The style will be determined by the window's given configuration + MacToolbarStyleAutomatic MacToolbarStyle = iota + // MacToolbarStyleExpanded - The toolbar will appear below the window title + MacToolbarStyleExpanded + // MacToolbarStylePreference - The toolbar will appear below the window title and the items in the toolbar will attempt to have equal widths when possible + MacToolbarStylePreference + // MacToolbarStyleUnified - The window title will appear inline with the toolbar when visible + MacToolbarStyleUnified + // MacToolbarStyleUnifiedCompact - Same as MacToolbarStyleUnified, but with reduced margins in the toolbar allowing more focus to be on the contents of the window + MacToolbarStyleUnifiedCompact +) + // MacWindow contains macOS specific options type MacWindow struct { - Backdrop MacBackdrop - TitleBar *TitleBar - Appearance MacAppearanceType + Backdrop MacBackdrop + TitleBar *TitleBar + Appearance MacAppearanceType + InvisibleTitleBarHeight int } // TitleBar contains options for the Mac titlebar @@ -37,9 +53,10 @@ type TitleBar struct { FullSizeContent bool UseToolbar bool HideToolbarSeparator bool + ToolbarStyle MacToolbarStyle } -// TitleBarDefault results in the default Mac Titlebar +// TitleBarDefault results in the default Mac TitleBar var TitleBarDefault = &TitleBar{ AppearsTransparent: false, Hide: false, @@ -74,6 +91,18 @@ var TitleBarHiddenInset = &TitleBar{ HideToolbarSeparator: true, } +// TitleBarHiddenInsetUnified results in a hidden title bar with an alternative look where +// the traffic light buttons are even more inset from the window edge. +var TitleBarHiddenInsetUnified = &TitleBar{ + AppearsTransparent: true, + Hide: false, + HideTitle: true, + FullSizeContent: true, + UseToolbar: true, + HideToolbarSeparator: true, + ToolbarStyle: MacToolbarStyleUnified, +} + // MacAppearanceType is a type of Appearance for Cocoa windows type MacAppearanceType string