From d7f832c00e8445e6c3a1c9ac6e95fa202b2a0c36 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Mon, 28 Sep 2020 21:13:57 +1000 Subject: [PATCH] Support SaveDialog --- v2/.vscode/settings.json | 3 +- v2/internal/ffenestri/ffenestri.h | 3 +- v2/internal/ffenestri/ffenestri_client.go | 17 +++- v2/internal/ffenestri/ffenestri_darwin.c | 93 +++++++++++++++---- .../messagedispatcher/dispatchclient.go | 1 + .../messagedispatcher/message/dialog.go | 31 ++++--- .../messagedispatcher/messagedispatcher.go | 20 +++- v2/internal/runtime/goruntime/dialog.go | 26 ++++++ v2/pkg/options/dialog.go | 13 ++- v2/test/runtime/runtime.go | 32 ++++++- 10 files changed, 200 insertions(+), 39 deletions(-) diff --git a/v2/.vscode/settings.json b/v2/.vscode/settings.json index e6b0705f5..c021e502f 100644 --- a/v2/.vscode/settings.json +++ b/v2/.vscode/settings.json @@ -6,6 +6,7 @@ "__functional_03": "c", "functional": "c", "__locale": "c", - "locale": "c" + "locale": "c", + "chrono": "c" } } \ No newline at end of file diff --git a/v2/internal/ffenestri/ffenestri.h b/v2/internal/ffenestri/ffenestri.h index c7f13ff0f..c064a80a5 100644 --- a/v2/internal/ffenestri/ffenestri.h +++ b/v2/internal/ffenestri/ffenestri.h @@ -29,5 +29,6 @@ extern void Fullscreen(void *app); extern void UnFullscreen(void *app); extern void ToggleFullscreen(void *app); extern void DisableFrame(void *app); -extern void OpenDialog(void *appPointer, char *callbackID, char *title, char *filter, char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolveAliases, int treatPackagesAsDirectories); +extern void OpenDialog(void *appPointer, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolveAliases, int treatPackagesAsDirectories); +extern void SaveDialog(void *appPointer, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories); #endif diff --git a/v2/internal/ffenestri/ffenestri_client.go b/v2/internal/ffenestri/ffenestri_client.go index 1ecc01e48..83ef7d5a8 100644 --- a/v2/internal/ffenestri/ffenestri_client.go +++ b/v2/internal/ffenestri/ffenestri_client.go @@ -124,7 +124,8 @@ func (c *Client) OpenDialog(dialogOptions *options.OpenDialog, callbackID string C.OpenDialog(c.app.app, c.app.string2CString(callbackID), c.app.string2CString(dialogOptions.Title), - c.app.string2CString(dialogOptions.Filter), + c.app.string2CString(dialogOptions.Filters), + c.app.string2CString(dialogOptions.DefaultFilename), c.app.string2CString(dialogOptions.DefaultDirectory), c.app.bool2Cint(dialogOptions.AllowFiles), c.app.bool2Cint(dialogOptions.AllowDirectories), @@ -135,3 +136,17 @@ func (c *Client) OpenDialog(dialogOptions *options.OpenDialog, callbackID string c.app.bool2Cint(dialogOptions.TreatPackagesAsDirectories), ) } + +// SaveDialog will open a dialog with the given title and filter +func (c *Client) SaveDialog(dialogOptions *options.SaveDialog, callbackID string) { + C.SaveDialog(c.app.app, + c.app.string2CString(callbackID), + c.app.string2CString(dialogOptions.Title), + c.app.string2CString(dialogOptions.Filters), + c.app.string2CString(dialogOptions.DefaultFilename), + c.app.string2CString(dialogOptions.DefaultDirectory), + c.app.bool2Cint(dialogOptions.ShowHiddenFiles), + c.app.bool2Cint(dialogOptions.CanCreateDirectories), + c.app.bool2Cint(dialogOptions.TreatPackagesAsDirectories), + ) +} diff --git a/v2/internal/ffenestri/ffenestri_darwin.c b/v2/internal/ffenestri/ffenestri_darwin.c index ad72e08a5..5bc57792d 100644 --- a/v2/internal/ffenestri/ffenestri_darwin.c +++ b/v2/internal/ffenestri/ffenestri_darwin.c @@ -407,17 +407,8 @@ void SetPosition(struct Application *app, int x, int y) { ) } -// OpenFileDialog opens a dialog to select a file -// NOTE: The result is a string that will need to be freed! -char* OpenFileDialog(struct Application *app, char *title, char *filter) { - Debug("OpenFileDialog Called"); - char *filename = concat("","BogusOpenFilename"); - return filename; -} - // OpenDialog opens a dialog to select files/directories -// NOTE: The result is a string that will need to be freed! -void OpenDialog(struct Application *app, char *callbackID, char *title, char *filter, char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolveAliases, int treatPackagesAsDirectories) { +void OpenDialog(struct Application *app, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolveAliases, int treatPackagesAsDirectories) { Debug("OpenDialog Called with callback id: %s", callbackID); // Create an open panel @@ -430,9 +421,8 @@ void OpenDialog(struct Application *app, char *callbackID, char *title, char *fi msg(dialog, s("setTitle:"), str(title)); // Filters - if( filter != NULL && strlen(filter) > 0) { - Debug("Using filter: %s", filter); - id filterString = msg(str(filter), s("stringByReplacingOccurrencesOfString:withString:"), str("*."), str("")); + if( filters != NULL && strlen(filters) > 0) { + id filterString = msg(str(filters), s("stringByReplacingOccurrencesOfString:withString:"), str("*."), str("")); filterString = msg(filterString, s("stringByReplacingOccurrencesOfString:withString:"), str(" "), str("")); id filterList = msg(filterString, s("componentsSeparatedByString:"), str(",")); msg(dialog, s("setAllowedFileTypes:"), filterList); @@ -446,9 +436,9 @@ void OpenDialog(struct Application *app, char *callbackID, char *title, char *fi } // Default Filename - // if( defaultFilename != NULL && strlen(defaultFilename) > 0 ) { - // msg(dialog, s("setNameFieldStringValue:"), str(defaultFilename)); - // } + if( defaultFilename != NULL && strlen(defaultFilename) > 0 ) { + msg(dialog, s("setNameFieldStringValue:"), str(defaultFilename)); + } // Setup Options msg(dialog, s("setCanChooseFiles:"), allowFiles); @@ -488,7 +478,7 @@ void OpenDialog(struct Application *app, char *callbackID, char *title, char *fi json_delete(response); // Construct callback message. Format "D|" - const char *callback = concat("D", callbackID); + const char *callback = concat("DO", callbackID); const char *header = concat(callback, "|"); const char *responseMessage = concat(header, encoded); @@ -505,6 +495,75 @@ void OpenDialog(struct Application *app, char *callbackID, char *title, char *fi ) } +// SaveDialog opens a dialog to select files/directories +void SaveDialog(struct Application *app, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories) { + Debug("SaveDialog Called with callback id: %s", callbackID); + + // Create an open panel + ON_MAIN_THREAD( + + // Create the dialog + id dialog = msg(c("NSSavePanel"), s("savePanel")); + + // Valid but appears to do nothing.... :/ + msg(dialog, s("setTitle:"), str(title)); + + // Filters + if( filters != NULL && strlen(filters) > 0) { + id filterString = msg(str(filters), s("stringByReplacingOccurrencesOfString:withString:"), str("*."), str("")); + filterString = msg(filterString, s("stringByReplacingOccurrencesOfString:withString:"), str(" "), str("")); + id filterList = msg(filterString, s("componentsSeparatedByString:"), str(",")); + msg(dialog, s("setAllowedFileTypes:"), filterList); + } else { + msg(dialog, s("setAllowsOtherFileTypes:"), YES); + } + + // Default Directory + if( defaultDir != NULL && strlen(defaultDir) > 0 ) { + msg(dialog, s("setDirectoryURL:"), url(defaultDir)); + } + + // Default Filename + if( defaultFilename != NULL && strlen(defaultFilename) > 0 ) { + msg(dialog, s("setNameFieldStringValue:"), str(defaultFilename)); + } + + // Setup Options + msg(dialog, s("setShowsHiddenFiles:"), showHiddenFiles); + msg(dialog, s("setCanCreateDirectories:"), canCreateDirectories); + msg(dialog, s("setTreatsFilePackagesAsDirectories:"), treatPackagesAsDirectories); + + // Setup callback handler + msg(dialog, s("beginSheetModalForWindow:completionHandler:"), app->mainWindow, ^(id result) { + + // Default is blank + const char *filename = ""; + + // If the user selected some files + if( result == (id)1 ) { + // Grab the URL returned + id url = msg(dialog, s("URL")); + filename = (const char *)msg(msg(url, s("path")), s("UTF8String")); + } + + // Construct callback message. Format "DS|" + const char *callback = concat("DS", callbackID); + const char *header = concat(callback, "|"); + const char *responseMessage = concat(header, filename); + + // Send message to backend + app->sendMessageToBackend(responseMessage); + + // Free memory + free((void*)header); + free((void*)callback); + free((void*)responseMessage); + }); + + msg( c("NSApp"), s("runModalForWindow:"), app->mainWindow); + ) +} + const char *invoke = "window.external={invoke:function(x){window.webkit.messageHandlers.external.postMessage(x);}};"; // DisableFrame disables the window frame diff --git a/v2/internal/messagedispatcher/dispatchclient.go b/v2/internal/messagedispatcher/dispatchclient.go index 4894c11ab..c08f0c8a7 100644 --- a/v2/internal/messagedispatcher/dispatchclient.go +++ b/v2/internal/messagedispatcher/dispatchclient.go @@ -15,6 +15,7 @@ type Client interface { NotifyEvent(message string) CallResult(message string) OpenDialog(dialogOptions *options.OpenDialog, callbackID string) + SaveDialog(dialogOptions *options.SaveDialog, callbackID string) WindowSetTitle(title string) WindowShow() WindowHide() diff --git a/v2/internal/messagedispatcher/message/dialog.go b/v2/internal/messagedispatcher/message/dialog.go index b6770a1d7..1f94d3209 100644 --- a/v2/internal/messagedispatcher/message/dialog.go +++ b/v2/internal/messagedispatcher/message/dialog.go @@ -15,31 +15,38 @@ func dialogMessageParser(message string) (*parsedMessage, error) { } var topic = "bad topic from dialogMessageParser" - var data []string + var responseMessage *parsedMessage // Switch the event type (with or without data) switch message[0] { // Format of Dialog response messages: D|<[]string as json encoded string> case 'D': - idx := strings.IndexByte(message[1:], '|') + dialogType := message[1] + message = message[2:] + idx := strings.IndexByte(message, '|') if idx < 0 { return nil, fmt.Errorf("Invalid dialog response message format") } - callbackID := message[1 : idx+1] - jsonData := message[idx+2:] - topic = "dialog:openselected:" + callbackID + callbackID := message[:idx+1] + payloadData := message[idx+1:] - err := json.Unmarshal([]byte(jsonData), &data) - if err != nil { - return nil, err + switch dialogType { + case 'O': + var data []string + topic = "dialog:openselected:" + callbackID + err := json.Unmarshal([]byte(payloadData), &data) + if err != nil { + return nil, err + } + responseMessage = &parsedMessage{Topic: topic, Data: data} + case 'S': + topic = "dialog:saveselected:" + callbackID + responseMessage = &parsedMessage{Topic: topic, Data: payloadData} } default: return nil, fmt.Errorf("Invalid message to dialogMessageParser()") } - // Create a new parsed message struct - parsedMessage := &parsedMessage{Topic: topic, Data: data} - - return parsedMessage, nil + return responseMessage, nil } diff --git a/v2/internal/messagedispatcher/messagedispatcher.go b/v2/internal/messagedispatcher/messagedispatcher.go index ee1cc93d0..3be2b07c8 100644 --- a/v2/internal/messagedispatcher/messagedispatcher.go +++ b/v2/internal/messagedispatcher/messagedispatcher.go @@ -342,9 +342,27 @@ func (d *Dispatcher) processDialogMessage(result *servicebus.Message) { for _, client := range d.clients { client.frontend.OpenDialog(dialogOptions, callbackID) } + case "save": + dialogOptions, ok := result.Data().(*options.SaveDialog) + if !ok { + d.logger.Error("Invalid data for 'dialog:select:save' : %#v", result.Data()) + return + } + // This is hardcoded in the sender too + callbackID := splitTopic[3] + + // TODO: Work out what we mean in a multi window environment... + // For now we will just pick the first one + for _, client := range d.clients { + client.frontend.SaveDialog(dialogOptions, callbackID) + } default: - d.logger.Error("Unknown dialog command: %s", command) + d.logger.Error("Unknown dialog type: %s", dialogType) } + + default: + d.logger.Error("Unknown dialog command: %s", command) } + } diff --git a/v2/internal/runtime/goruntime/dialog.go b/v2/internal/runtime/goruntime/dialog.go index e06ca056d..0fc304ddb 100644 --- a/v2/internal/runtime/goruntime/dialog.go +++ b/v2/internal/runtime/goruntime/dialog.go @@ -11,6 +11,7 @@ import ( // Dialog defines all Dialog related operations type Dialog interface { Open(dialogOptions *options.OpenDialog) []string + Save(dialogOptions *options.SaveDialog) string } // dialog exposes the Dialog interface @@ -66,3 +67,28 @@ func (r *dialog) Open(dialogOptions *options.OpenDialog) []string { return result.Data().([]string) } + +// Save prompts the user to select a file +func (r *dialog) Save(dialogOptions *options.SaveDialog) string { + + // Create unique dialog callback + uniqueCallback := crypto.RandomID() + + // Subscribe to the respose channel + 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()) + } + + message := "dialog:select:save:" + 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) +} diff --git a/v2/pkg/options/dialog.go b/v2/pkg/options/dialog.go index 8f7234ba1..7371a9895 100644 --- a/v2/pkg/options/dialog.go +++ b/v2/pkg/options/dialog.go @@ -5,7 +5,7 @@ type OpenDialog struct { DefaultDirectory string DefaultFilename string Title string - Filter string + Filters string AllowFiles bool AllowDirectories bool AllowMultiple bool @@ -14,3 +14,14 @@ type OpenDialog struct { ResolveAliases bool TreatPackagesAsDirectories bool } + +// SaveDialog contains the options for the SaveDialog runtime method +type SaveDialog struct { + DefaultDirectory string + DefaultFilename string + Title string + Filters string + ShowHiddenFiles bool + CanCreateDirectories bool + TreatPackagesAsDirectories bool +} diff --git a/v2/test/runtime/runtime.go b/v2/test/runtime/runtime.go index f0db9b88c..cc6919d26 100644 --- a/v2/test/runtime/runtime.go +++ b/v2/test/runtime/runtime.go @@ -71,7 +71,7 @@ func (r *RuntimeTest) SetColour(colour int) { func (r *RuntimeTest) OpenFileDialog(title string, filter string) []string { dialogOptions := &options.OpenDialog{ Title: title, - Filter: filter, + Filters: filter, AllowFiles: true, } return r.runtime.Dialog.Open(dialogOptions) @@ -81,7 +81,7 @@ func (r *RuntimeTest) OpenFileDialog(title string, filter string) []string { func (r *RuntimeTest) OpenDirectoryDialog(title string, filter string) []string { dialogOptions := &options.OpenDialog{ Title: title, - Filter: filter, + Filters: filter, AllowDirectories: true, } return r.runtime.Dialog.Open(dialogOptions) @@ -91,7 +91,7 @@ func (r *RuntimeTest) OpenDirectoryDialog(title string, filter string) []string func (r *RuntimeTest) OpenDialog(title string, filter string) []string { dialogOptions := &options.OpenDialog{ Title: title, - Filter: filter, + Filters: filter, AllowDirectories: true, AllowFiles: true, } @@ -102,7 +102,7 @@ func (r *RuntimeTest) OpenDialog(title string, filter string) []string { func (r *RuntimeTest) OpenDialogMultiple(title string, filter string) []string { dialogOptions := &options.OpenDialog{ Title: title, - Filter: filter, + Filters: filter, AllowDirectories: true, AllowFiles: true, AllowMultiple: true, @@ -115,7 +115,7 @@ func (r *RuntimeTest) OpenDialogAllOptions(filter string, defaultDir string, def dialogOptions := &options.OpenDialog{ DefaultDirectory: defaultDir, DefaultFilename: defaultFilename, - Filter: filter, + Filters: filter, AllowFiles: true, AllowDirectories: true, ShowHiddenFiles: true, @@ -126,6 +126,28 @@ func (r *RuntimeTest) OpenDialogAllOptions(filter string, defaultDir string, def return r.runtime.Dialog.Open(dialogOptions) } +// SaveFileDialog will call the Runtime.Dialog.SaveDialog method requesting a File selection +func (r *RuntimeTest) SaveFileDialog(title string, filter string) string { + dialogOptions := &options.SaveDialog{ + Title: title, + Filters: filter, + } + return r.runtime.Dialog.Save(dialogOptions) +} + +// SaveDialogAllOptions will call the Runtime.Dialog.SaveDialog method allowing multiple selection +func (r *RuntimeTest) SaveDialogAllOptions(filter string, defaultDir string, defaultFilename string) string { + dialogOptions := &options.SaveDialog{ + DefaultDirectory: defaultDir, + DefaultFilename: defaultFilename, + Filters: filter, + ShowHiddenFiles: true, + CanCreateDirectories: true, + TreatPackagesAsDirectories: true, + } + return r.runtime.Dialog.Save(dialogOptions) +} + // HideWindow will call the Runtime.Window.Hide method and then call // Runtime.Window.Show 3 seconds later. func (r *RuntimeTest) HideWindow() {