diff --git a/v2/go.mod b/v2/go.mod index 53b14c636..ca1e65a0a 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -7,6 +7,7 @@ require ( github.com/fatih/structtag v1.2.0 github.com/fsnotify/fsnotify v1.4.9 github.com/gorilla/websocket v1.4.1 + github.com/harry1453/go-common-file-dialog v1.0.0 github.com/imdario/mergo v0.3.11 github.com/jackmordaunt/icns v1.0.0 github.com/leaanthony/clir v1.0.4 diff --git a/v2/go.sum b/v2/go.sum index 47464effd..fdfdb76bd 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -10,6 +10,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= @@ -29,8 +31,12 @@ github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgj github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/harry1453/go-common-file-dialog v1.0.0 h1:fzBAGhRTqWQyJw5xkm0PSsA+d3CBYBrfh+Nayb6U0nM= +github.com/harry1453/go-common-file-dialog v1.0.0/go.mod h1:3zwmbo7fy+uYGyaec74mu+Z9DPg0aEt10fSjjPwfyiY= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/jackmordaunt/icns v1.0.0 h1:RYSxplerf/l/DUd09AHtITwckkv/mqjVv4DjYdPmAMQ= diff --git a/v2/internal/ffenestri/ffenestri.go b/v2/internal/ffenestri/ffenestri.go index 5a921c66f..5d5939d15 100644 --- a/v2/internal/ffenestri/ffenestri.go +++ b/v2/internal/ffenestri/ffenestri.go @@ -40,6 +40,9 @@ type Application struct { // Logger logger logger.CustomLogger + + // Window handle (used by windows) + hwnd unsafe.Pointer } func (a *Application) saveMemoryReference(mem unsafe.Pointer) { diff --git a/v2/internal/ffenestri/ffenestri_client.go b/v2/internal/ffenestri/ffenestri_client.go index 9611769ad..73d7165ec 100644 --- a/v2/internal/ffenestri/ffenestri_client.go +++ b/v2/internal/ffenestri/ffenestri_client.go @@ -6,7 +6,9 @@ package ffenestri import "C" import ( + "runtime" "strconv" + "strings" "github.com/wailsapp/wails/v2/pkg/options/dialog" @@ -124,15 +126,45 @@ func (c *Client) WindowSetColour(colour int) { // OpenDialog will open a dialog with the given title and filter func (c *Client) OpenDialog(dialogOptions *dialog.OpenDialog, callbackID string) { + filters := []string{} + if runtime.GOOS == "darwin" { + for _, filter := range dialogOptions.Filters { + filters = append(filters, strings.Split(filter.Pattern, ",")...) + } + } C.OpenDialog(c.app.app, c.app.string2CString(callbackID), c.app.string2CString(dialogOptions.Title), - c.app.string2CString(dialogOptions.Filters), + c.app.string2CString(strings.Join(filters, ";")), c.app.string2CString(dialogOptions.DefaultFilename), c.app.string2CString(dialogOptions.DefaultDirectory), c.app.bool2Cint(dialogOptions.AllowFiles), c.app.bool2Cint(dialogOptions.AllowDirectories), - c.app.bool2Cint(dialogOptions.AllowMultiple), + c.app.bool2Cint(false), + c.app.bool2Cint(dialogOptions.ShowHiddenFiles), + c.app.bool2Cint(dialogOptions.CanCreateDirectories), + c.app.bool2Cint(dialogOptions.ResolvesAliases), + c.app.bool2Cint(dialogOptions.TreatPackagesAsDirectories), + ) +} + +// OpenMultipleDialog will open a dialog with the given title and filter +func (c *Client) OpenMultipleDialog(dialogOptions *dialog.OpenDialog, callbackID string) { + filters := []string{} + if runtime.GOOS == "darwin" { + for _, filter := range dialogOptions.Filters { + filters = append(filters, strings.Split(filter.Pattern, ",")...) + } + } + C.OpenDialog(c.app.app, + c.app.string2CString(callbackID), + c.app.string2CString(dialogOptions.Title), + c.app.string2CString(strings.Join(filters, ";")), + c.app.string2CString(dialogOptions.DefaultFilename), + c.app.string2CString(dialogOptions.DefaultDirectory), + c.app.bool2Cint(dialogOptions.AllowFiles), + c.app.bool2Cint(dialogOptions.AllowDirectories), + c.app.bool2Cint(true), c.app.bool2Cint(dialogOptions.ShowHiddenFiles), c.app.bool2Cint(dialogOptions.CanCreateDirectories), c.app.bool2Cint(dialogOptions.ResolvesAliases), @@ -142,10 +174,16 @@ func (c *Client) OpenDialog(dialogOptions *dialog.OpenDialog, callbackID string) // SaveDialog will open a dialog with the given title and filter func (c *Client) SaveDialog(dialogOptions *dialog.SaveDialog, callbackID string) { + filters := []string{} + if runtime.GOOS == "darwin" { + for _, filter := range dialogOptions.Filters { + filters = append(filters, strings.Split(filter.Pattern, ",")...) + } + } C.SaveDialog(c.app.app, c.app.string2CString(callbackID), c.app.string2CString(dialogOptions.Title), - c.app.string2CString(dialogOptions.Filters), + c.app.string2CString(strings.Join(filters, ";")), c.app.string2CString(dialogOptions.DefaultFilename), c.app.string2CString(dialogOptions.DefaultDirectory), c.app.bool2Cint(dialogOptions.ShowHiddenFiles), diff --git a/v2/internal/runtime/dialog.go b/v2/internal/runtime/dialog.go index 126bbb9b5..fe11051aa 100644 --- a/v2/internal/runtime/dialog.go +++ b/v2/internal/runtime/dialog.go @@ -1,3 +1,5 @@ +// +build !windows + package runtime import ( @@ -10,9 +12,11 @@ import ( // Dialog defines all Dialog related operations type Dialog interface { - Open(dialogOptions *dialogoptions.OpenDialog) []string - Save(dialogOptions *dialogoptions.SaveDialog) string - Message(dialogOptions *dialogoptions.MessageDialog) string + OpenFile(dialogOptions *dialogoptions.OpenDialog) (string, error) + OpenMultipleFiles(dialogOptions *dialogoptions.OpenDialog) ([]string, error) + OpenDirectory(dialogOptions *dialogoptions.OpenDialog) (string, error) + SaveFile(dialogOptions *dialogoptions.SaveDialog) (string, error) + Message(dialogOptions *dialogoptions.MessageDialog) (string, error) } // dialog exposes the Dialog interface @@ -44,8 +48,32 @@ func (r *dialog) processTitleAndFilter(params ...string) (string, string) { return title, filter } +func OpenDirectory(dialogOptions *dialogoptions.OpenDialog) (string, error) { + + // Create unique dialog callback + uniqueCallback := crypto.RandomID() + + // Subscribe to the respose channel + responseTopic := "dialog:opendirectoryselected:" + uniqueCallback + dialogResponseChannel, err := r.bus.Subscribe(responseTopic) + if err != nil { + return nil, fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error()) + } + + message := "dialog:selectdirectory:open:" + uniqueCallback + r.bus.Publish(message, dialogOptions) + + // Wait for result + var result *servicebus.Message = <-dialogResponseChannel + + // Delete subscription to response topic + r.bus.UnSubscribe(responseTopic) + + return result.Data().(string), nil +} + // Open prompts the user to select a file -func (r *dialog) Open(dialogOptions *dialogoptions.OpenDialog) []string { +func (r *dialog) OpenFile(dialogOptions *dialogoptions.OpenDialog) (string, error) { // Create unique dialog callback uniqueCallback := crypto.RandomID() @@ -54,7 +82,7 @@ func (r *dialog) Open(dialogOptions *dialogoptions.OpenDialog) []string { responseTopic := "dialog:openselected:" + uniqueCallback dialogResponseChannel, err := r.bus.Subscribe(responseTopic) if err != nil { - fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error()) + return nil, fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error()) } message := "dialog:select:open:" + uniqueCallback @@ -66,11 +94,36 @@ func (r *dialog) Open(dialogOptions *dialogoptions.OpenDialog) []string { // Delete subscription to response topic r.bus.UnSubscribe(responseTopic) - return result.Data().([]string) + return result.Data().(string), nil +} + +// OpenMultiple prompts the user to select a file +func (r *dialog) OpenMultipleFiles(dialogOptions *dialogoptions.OpenDialog) ([]string, error) { + + // Create unique dialog callback + uniqueCallback := crypto.RandomID() + + // Subscribe to the respose channel + responseTopic := "dialog:openmultipleselected:" + uniqueCallback + dialogResponseChannel, err := r.bus.Subscribe(responseTopic) + if err != nil { + return nil, fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error()) + } + + message := "dialog:select:openmultiple:" + uniqueCallback + r.bus.Publish(message, dialogOptions) + + // Wait for result + var result *servicebus.Message = <-dialogResponseChannel + + // Delete subscription to response topic + r.bus.UnSubscribe(responseTopic) + + return result.Data().(string), nil } // Save prompts the user to select a file -func (r *dialog) Save(dialogOptions *dialogoptions.SaveDialog) string { +func (r *dialog) SaveFile(dialogOptions *dialogoptions.SaveDialog) (string, error) { // Create unique dialog callback uniqueCallback := crypto.RandomID() @@ -79,7 +132,7 @@ func (r *dialog) Save(dialogOptions *dialogoptions.SaveDialog) string { responseTopic := "dialog:saveselected:" + uniqueCallback dialogResponseChannel, err := r.bus.Subscribe(responseTopic) if err != nil { - fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error()) + return nil, fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error()) } message := "dialog:select:save:" + uniqueCallback @@ -91,7 +144,7 @@ func (r *dialog) Save(dialogOptions *dialogoptions.SaveDialog) string { // Delete subscription to response topic r.bus.UnSubscribe(responseTopic) - return result.Data().(string) + return result.Data().(string), nil } // Message show a message to the user diff --git a/v2/internal/runtime/dialog_windows.go b/v2/internal/runtime/dialog_windows.go new file mode 100644 index 000000000..373f3a4c1 --- /dev/null +++ b/v2/internal/runtime/dialog_windows.go @@ -0,0 +1,141 @@ +// +build windows + +package runtime + +import ( + "golang.org/x/sys/windows" + "syscall" + + "github.com/harry1453/go-common-file-dialog/cfd" + "github.com/harry1453/go-common-file-dialog/cfdutil" + "github.com/wailsapp/wails/v2/internal/servicebus" + dialogoptions "github.com/wailsapp/wails/v2/pkg/options/dialog" +) + +// Dialog defines all Dialog related operations +type Dialog interface { + OpenFile(dialogOptions *dialogoptions.OpenDialog) (string, error) + OpenMultipleFiles(dialogOptions *dialogoptions.OpenDialog) ([]string, error) + OpenDirectory(dialogOptions *dialogoptions.OpenDialog) (string, error) + Save(dialogOptions *dialogoptions.SaveDialog) (string, error) + Message(dialogOptions *dialogoptions.MessageDialog) (string, error) +} + +// dialog exposes the Dialog interface +type dialog struct { + bus *servicebus.ServiceBus +} + +// newDialogs creates a new Dialogs struct +func newDialog(bus *servicebus.ServiceBus) Dialog { + return &dialog{ + bus: bus, + } +} + +// processTitleAndFilter return the title and filter from the given params. +// title is the first string, filter is the second +func (r *dialog) processTitleAndFilter(params ...string) (string, string) { + + var title, filter string + + if len(params) > 0 { + title = params[0] + } + + if len(params) > 1 { + filter = params[1] + } + + return title, filter +} + +func convertFilters(filters []dialogoptions.FileFilter) []cfd.FileFilter { + var result []cfd.FileFilter + for _, filter := range filters { + result = append(result, cfd.FileFilter(filter)) + } + return result +} + +func pickMultipleFiles(options *dialogoptions.OpenDialog) ([]string, error) { + + results, err := cfdutil.ShowOpenMultipleFilesDialog(cfd.DialogConfig{ + Title: options.Title, + Role: "OpenMultipleFiles", + FileFilters: convertFilters(options.Filters), + FileName: options.DefaultFilename, + Folder: options.DefaultDirectory, + }) + return results, err +} + +func (r *dialog) OpenMultipleFiles(options *dialogoptions.OpenDialog) ([]string, error) { + return pickMultipleFiles(options) +} + +func (r *dialog) OpenDirectory(options *dialogoptions.OpenDialog) (string, error) { + return cfdutil.ShowPickFolderDialog(cfd.DialogConfig{ + Title: options.Title, + Role: "PickFolder", + Folder: options.DefaultDirectory, + }) +} + +func (r *dialog) OpenFile(options *dialogoptions.OpenDialog) (string, error) { + result, err := cfdutil.ShowOpenFileDialog(cfd.DialogConfig{ + Folder: options.DefaultDirectory, + FileFilters: convertFilters(options.Filters), + FileName: options.DefaultFilename, + }) + return result, err +} + +// Save prompts the user to select a file +func (r *dialog) Save(options *dialogoptions.SaveDialog) (string, error) { + + result, err := cfdutil.ShowSaveFileDialog(cfd.DialogConfig{ + Title: options.Title, + Role: "SaveFile", + FileName: options.DefaultFilename, + FileFilters: convertFilters(options.Filters), + }) + return result, err +} + +// Message show a message to the user +func (r *dialog) Message(options *dialogoptions.MessageDialog) (string, error) { + + // TODO: error handling + title, err := syscall.UTF16PtrFromString(options.Title) + if err != nil { + return "", err + } + message, err := syscall.UTF16PtrFromString(options.Message) + if err != nil { + return "", err + } + var flags uint32 + switch options.Type { + case dialogoptions.InfoDialog: + flags = windows.MB_OK | windows.MB_ICONINFORMATION + case dialogoptions.ErrorDialog: + flags = windows.MB_ICONERROR | windows.MB_OK + case dialogoptions.QuestionDialog: + flags = windows.MB_YESNO + case dialogoptions.WarningDialog: + flags = windows.MB_OK | windows.MB_ICONWARNING + } + + result, _ := windows.MessageBox(0, message, title, flags|windows.MB_SYSTEMMODAL) + if options.Type == dialogoptions.QuestionDialog { + if result == 6 { // IDYES + return "Yes", nil + } + if result == 7 { // IDNO + return "No", nil + } + } + return "", nil + +} diff --git a/v2/internal/subsystem/call.go b/v2/internal/subsystem/call.go index e60e17509..03808fc38 100644 --- a/v2/internal/subsystem/call.go +++ b/v2/internal/subsystem/call.go @@ -140,7 +140,10 @@ func (c *Call) processSystemCall(payload *message.CallMessage, clientID string) if err != nil { c.logger.Error("Error decoding: %s", err) } - result := c.runtime.Dialog.Open(dialogOptions) + result, err := c.runtime.Dialog.OpenFile(dialogOptions) + if err != nil { + c.logger.Error("Error: %s", err) + } c.sendResult(result, payload, clientID) case "Dialog.Save": dialogOptions := new(dialog.SaveDialog) @@ -148,7 +151,10 @@ func (c *Call) processSystemCall(payload *message.CallMessage, clientID string) if err != nil { c.logger.Error("Error decoding: %s", err) } - result := c.runtime.Dialog.Save(dialogOptions) + result, err := c.runtime.Dialog.Save(dialogOptions) + if err != nil { + c.logger.Error("Error: %s", err) + } c.sendResult(result, payload, clientID) case "Dialog.Message": dialogOptions := new(dialog.MessageDialog) @@ -156,7 +162,10 @@ func (c *Call) processSystemCall(payload *message.CallMessage, clientID string) if err != nil { c.logger.Error("Error decoding: %s", err) } - result := c.runtime.Dialog.Message(dialogOptions) + result, err := c.runtime.Dialog.Message(dialogOptions) + if err != nil { + c.logger.Error("Error: %s", err) + } c.sendResult(result, payload, clientID) default: c.logger.Error("Unknown system call: %+v", callName) diff --git a/v2/pkg/options/dialog/dialog.go b/v2/pkg/options/dialog/dialog.go index 394f88bb6..dfaf4407b 100644 --- a/v2/pkg/options/dialog/dialog.go +++ b/v2/pkg/options/dialog/dialog.go @@ -1,14 +1,19 @@ package dialog +// FileFilter defines a filter for dialog boxes +type FileFilter struct { + DisplayName string // Filter information EG: "Image Files (*.jpg, *.png)" + Pattern string // semi-colon separated list of extensions, EG: "*.jpg;*.png" +} + // OpenDialog contains the options for the OpenDialog runtime method type OpenDialog struct { DefaultDirectory string DefaultFilename string Title string - Filters string + Filters []FileFilter AllowFiles bool AllowDirectories bool - AllowMultiple bool ShowHiddenFiles bool CanCreateDirectories bool ResolvesAliases bool @@ -20,7 +25,7 @@ type SaveDialog struct { DefaultDirectory string DefaultFilename string Title string - Filters string + Filters []FileFilter ShowHiddenFiles bool CanCreateDirectories bool TreatPackagesAsDirectories bool