diff --git a/exp/examples/dialogs/main.go b/exp/examples/dialogs/main.go index f976f014f..1f1a8361b 100644 --- a/exp/examples/dialogs/main.go +++ b/exp/examples/dialogs/main.go @@ -135,6 +135,63 @@ func main() { dialog.Show() }) + openMenu := menu.AddSubmenu("Open") + openMenu.Add("Open File").OnClick(func(ctx *application.Context) { + result, _ := app.NewOpenFileDialog(). + CanChooseFiles(true). + Show() + if result != "" { + app.NewInfoDialog().SetMessage(result).Show() + } else { + app.NewInfoDialog().SetMessage("No file selected").Show() + } + }) + openMenu.Add("Open File (Show Hidden Files)").OnClick(func(ctx *application.Context) { + result, _ := app.NewOpenFileDialog(). + CanChooseFiles(true). + CanCreateDirectories(true). + ShowHiddenFiles(true). + Show() + if result != "" { + app.NewInfoDialog().SetMessage(result).Show() + } else { + app.NewInfoDialog().SetMessage("No file selected").Show() + } + }) + //openMenu.Add("Open Multiple Files (Show Hidden Files)").OnClick(func(ctx *application.Context) { + // result, _ := app.NewOpenMultipleFilesDialog(). + // CanChooseFiles(true). + // CanCreateDirectories(true). + // ShowHiddenFiles(true). + // Show() + // if len(result) > 0 { + // app.NewInfoDialog().SetMessage(strings.Join(result, ",")).Show() + // } else { + // app.NewInfoDialog().SetMessage("No file selected").Show() + // } + //}) + openMenu.Add("Open Directory").OnClick(func(ctx *application.Context) { + result, _ := app.NewOpenFileDialog(). + CanChooseDirectories(true). + Show() + if result != "" { + app.NewInfoDialog().SetMessage(result).Show() + } else { + app.NewInfoDialog().SetMessage("No directory selected").Show() + } + }) + openMenu.Add("Open Directory (Create Directories)").OnClick(func(ctx *application.Context) { + result, _ := app.NewOpenFileDialog(). + CanChooseDirectories(true). + CanCreateDirectories(true). + Show() + if result != "" { + app.NewInfoDialog().SetMessage(result).Show() + } else { + app.NewInfoDialog().SetMessage("No directory selected").Show() + } + }) + app.SetMenu(menu) app.NewWindow() diff --git a/exp/examples/kitchensink/icon.png b/exp/examples/kitchensink/icon.png deleted file mode 100644 index fc492c916..000000000 Binary files a/exp/examples/kitchensink/icon.png and /dev/null differ diff --git a/exp/examples/kitchensink/macos_icon.png b/exp/examples/kitchensink/macos_icon.png deleted file mode 100644 index 659d860e1..000000000 Binary files a/exp/examples/kitchensink/macos_icon.png and /dev/null differ diff --git a/exp/examples/kitchensink/main.go b/exp/examples/kitchensink/main.go index db1faf731..fb35f9e31 100644 --- a/exp/examples/kitchensink/main.go +++ b/exp/examples/kitchensink/main.go @@ -11,12 +11,6 @@ import ( "github.com/wailsapp/wails/exp/pkg/options" ) -//go:embed icon.png -var icon []byte - -//go:embed macos_icon.png -var macosIcon []byte - func main() { app := application.NewWithOptions(&options.Application{ Mac: &options.Mac{ @@ -67,9 +61,9 @@ func main() { mySystray := app.NewSystemTray() mySystray.SetLabel("Wails") if runtime.GOOS == "darwin" { - mySystray.SetTemplateIcon(macosIcon) + mySystray.SetTemplateIcon(application.DefaultMacTemplateIcon) } else { - mySystray.SetIcon(icon) + mySystray.SetIcon(application.DefaultApplicationIcon) } myMenu := app.NewMenu() myMenu.Add("Item 1") @@ -107,9 +101,9 @@ func main() { mySystray := app.NewSystemTray() mySystray.SetLabel("Wails is awesome") if runtime.GOOS == "darwin" { - mySystray.SetTemplateIcon(macosIcon) + mySystray.SetTemplateIcon(application.DefaultMacTemplateIcon) } else { - mySystray.SetIcon(icon) + mySystray.SetIcon(application.DefaultApplicationIcon) } mySystray.SetMenu(myMenu) mySystray.SetIconPosition(application.NSImageLeading) diff --git a/exp/pkg/application/application.go b/exp/pkg/application/application.go index 68a144c12..3408e9305 100644 --- a/exp/pkg/application/application.go +++ b/exp/pkg/application/application.go @@ -87,7 +87,7 @@ type App struct { // The main application menu ApplicationMenu *Menu - // About Dialog + // About MessageDialog name string description string icon []byte @@ -321,18 +321,26 @@ func (a *App) ShowAboutDialog() { } } -func (a *App) NewInfoDialog() *Dialog { - return newDialog(InfoDialog) +func (a *App) NewInfoDialog() *MessageDialog { + return newMessageDialog(InfoDialog) } -func (a *App) NewQuestionDialog() *Dialog { - return newDialog(QuestionDialog) +func (a *App) NewQuestionDialog() *MessageDialog { + return newMessageDialog(QuestionDialog) } -func (a *App) NewWarningDialog() *Dialog { - return newDialog(WarningDialog) +func (a *App) NewWarningDialog() *MessageDialog { + return newMessageDialog(WarningDialog) } -func (a *App) NewErrorDialog() *Dialog { - return newDialog(ErrorDialog) +func (a *App) NewErrorDialog() *MessageDialog { + return newMessageDialog(ErrorDialog) +} + +func (a *App) NewOpenDirectoryDialog() *MessageDialog { + return newMessageDialog(OpenDirectoryDialog) +} + +func (a *App) NewOpenFileDialog() *OpenFileDialog { + return newOpenFileDialog() } diff --git a/exp/pkg/application/dialogs.go b/exp/pkg/application/dialogs.go index 8fbfee492..680cf7898 100644 --- a/exp/pkg/application/dialogs.go +++ b/exp/pkg/application/dialogs.go @@ -1,12 +1,30 @@ package application +import "C" +import ( + "sync" +) + type DialogType int +var dialogID uint +var dialogIDLock sync.RWMutex + +func getDialogID() uint { + dialogIDLock.Lock() + defer dialogIDLock.Unlock() + dialogID++ + return dialogID +} + +var openFileResponses = make(map[uint]chan string) + const ( InfoDialog DialogType = iota QuestionDialog WarningDialog ErrorDialog + OpenDirectoryDialog ) type Button struct { @@ -20,19 +38,19 @@ func (b *Button) OnClick(callback func()) { b.callback = callback } -type dialogImpl interface { +type messageDialogImpl interface { show() } -type Dialog struct { +type MessageDialog struct { dialogType DialogType title string message string buttons []*Button + icon []byte // platform independent - impl dialogImpl - icon []byte + impl messageDialogImpl } var defaultTitles = map[DialogType]string{ @@ -42,36 +60,36 @@ var defaultTitles = map[DialogType]string{ ErrorDialog: "Error", } -func newDialog(dialogType DialogType) *Dialog { - return &Dialog{ +func newMessageDialog(dialogType DialogType) *MessageDialog { + return &MessageDialog{ dialogType: dialogType, title: defaultTitles[dialogType], } } -func (d *Dialog) SetTitle(title string) *Dialog { +func (d *MessageDialog) SetTitle(title string) *MessageDialog { d.title = title return d } -func (d *Dialog) SetMessage(message string) *Dialog { +func (d *MessageDialog) SetMessage(message string) *MessageDialog { d.message = message return d } -func (d *Dialog) Show() { +func (d *MessageDialog) Show() { if d.impl == nil { d.impl = newDialogImpl(d) } d.impl.show() } -func (d *Dialog) SetIcon(icon []byte) *Dialog { +func (d *MessageDialog) SetIcon(icon []byte) *MessageDialog { d.icon = icon return d } -func (d *Dialog) AddButton(s string) *Button { +func (d *MessageDialog) AddButton(s string) *Button { result := &Button{ label: s, } @@ -79,7 +97,7 @@ func (d *Dialog) AddButton(s string) *Button { return result } -func (d *Dialog) SetDefaultButton(button *Button) *Dialog { +func (d *MessageDialog) SetDefaultButton(button *Button) *MessageDialog { for _, b := range d.buttons { b.isDefault = false } @@ -87,10 +105,62 @@ func (d *Dialog) SetDefaultButton(button *Button) *Dialog { return d } -func (d *Dialog) SetCancelButton(button *Button) *Dialog { +func (d *MessageDialog) SetCancelButton(button *Button) *MessageDialog { for _, b := range d.buttons { b.isCancel = false } button.isCancel = true return d } + +type openFileDialogImpl interface { + show() ([]string, error) +} + +type OpenFileDialog struct { + id uint + canChooseDirectories bool + canChooseFiles bool + canCreateDirectories bool + showHiddenFiles bool + allowsMultipleSelection bool + + impl openFileDialogImpl +} + +func (d *OpenFileDialog) CanChooseFiles(canChooseFiles bool) *OpenFileDialog { + d.canChooseFiles = canChooseFiles + return d +} + +func (d *OpenFileDialog) CanChooseDirectories(canChooseDirectories bool) *OpenFileDialog { + d.canChooseDirectories = canChooseDirectories + return d +} + +func (d *OpenFileDialog) CanCreateDirectories(canCreateDirectories bool) *OpenFileDialog { + d.canCreateDirectories = canCreateDirectories + return d +} + +func (d *OpenFileDialog) ShowHiddenFiles(showHiddenFiles bool) *OpenFileDialog { + d.showHiddenFiles = showHiddenFiles + return d +} + +func (d *OpenFileDialog) Show() (string, error) { + if d.impl == nil { + d.impl = newOpenFileDialogImpl(d) + } + result, err := d.impl.show() + return result[0], err +} + +func newOpenFileDialog() *OpenFileDialog { + return &OpenFileDialog{ + id: getDialogID(), + canChooseDirectories: false, + canChooseFiles: true, + canCreateDirectories: false, + } +} diff --git a/exp/pkg/application/dialogs_darwin.go b/exp/pkg/application/dialogs_darwin.go index 722140e09..0c619231a 100644 --- a/exp/pkg/application/dialogs_darwin.go +++ b/exp/pkg/application/dialogs_darwin.go @@ -8,6 +8,9 @@ package application #import +extern void openFileDialogCallback(uint id, char* path); +extern void openFileDialogCallbackEnd(uint id); + static void showAboutBox(char* title, char *message, void *icon, int length) { // run on main thread @@ -99,9 +102,46 @@ static void alertAddButton(void *dialog, char *label, bool isDefault, bool isCan } } +static void showOpenFileDialog(unsigned int dialogID, bool canChooseFiles, bool canChooseDirectories, bool canCreateDirectories, bool showHiddenFiles, bool allowsMultipleSelection) { + + // run on main thread + dispatch_async(dispatch_get_main_queue(), ^{ + NSOpenPanel *panel = [NSOpenPanel openPanel]; + + [panel setCanChooseFiles:canChooseFiles]; + [panel setCanChooseDirectories:canChooseDirectories]; + [panel setCanCreateDirectories:canCreateDirectories]; + [panel setShowsHiddenFiles:showHiddenFiles]; + [panel setAllowsMultipleSelection:allowsMultipleSelection]; + + + // Show panel + [panel beginWithCompletionHandler:^(NSInteger result) { + const char *path = NULL; + if (result == NSModalResponseOK) { + if (allowsMultipleSelection) { + NSArray *urls = [panel URLs]; + for (NSURL *url in urls) { + path = [[url path] UTF8String]; + openFileDialogCallback(dialogID, (char *)path); + } + + } else { + NSURL *url = [panel URL]; + path = [[url path] UTF8String]; + openFileDialogCallback(dialogID, (char *)path); + } + } + openFileDialogCallbackEnd(dialogID); + }]; + }); +} + */ import "C" -import "unsafe" +import ( + "unsafe" +) const NSAlertStyleWarning = C.int(0) const NSAlertStyleInformational = C.int(1) @@ -123,7 +163,7 @@ func (m *macosApp) showAboutDialog(title string, message string, icon []byte) { } type macosDialog struct { - dialog *Dialog + dialog *MessageDialog nsDialog unsafe.Pointer } @@ -188,8 +228,56 @@ func (m *macosDialog) show() { } -func newDialogImpl(d *Dialog) *macosDialog { +func newDialogImpl(d *MessageDialog) *macosDialog { return &macosDialog{ dialog: d, } } + +type macosOpenFileDialog struct { + dialog *OpenFileDialog +} + +func newOpenFileDialogImpl(d *OpenFileDialog) *macosOpenFileDialog { + return &macosOpenFileDialog{ + dialog: d, + } +} + +func (m *macosOpenFileDialog) show() ([]string, error) { + openFileResponses[dialogID] = make(chan string) + C.showOpenFileDialog(C.uint(m.dialog.id), + C.bool(m.dialog.canChooseFiles), + C.bool(m.dialog.canChooseDirectories), + C.bool(m.dialog.canCreateDirectories), + C.bool(m.dialog.showHiddenFiles), + C.bool(m.dialog.allowsMultipleSelection)) + var result []string + for filename := range openFileResponses[m.dialog.id] { + result = append(result, filename) + } + return result, nil +} + +//export openFileDialogCallback +func openFileDialogCallback(id C.uint, path *C.char) { + // Covert the path to a string + filePath := C.GoString(path) + // put response on channel + channel, ok := openFileResponses[uint(id)] + if ok { + channel <- filePath + } else { + panic("No channel found for open file dialog") + } +} + +//export openFileDialogCallbackEnd +func openFileDialogCallbackEnd(id C.uint) { + channel, ok := openFileResponses[uint(id)] + if ok { + close(channel) + } else { + panic("No channel found for open file dialog") + } +}