diff --git a/v2/internal/appng/app_default_darwin.go b/v2/internal/appng/app_default_darwin.go index f0971f864..4cde85212 100644 --- a/v2/internal/appng/app_default_darwin.go +++ b/v2/internal/appng/app_default_darwin.go @@ -17,16 +17,5 @@ func (a *App) Run() error { // CreateApp creates the app! func CreateApp(_ *options.App) (*App, error) { - // result := w32.MessageBox(0, - // `Wails applications will not build without the correct build tags. - //Please use "wails build" or press "OK" to open the documentation on how to use "go build"`, - // "Error", - // w32.MB_ICONERROR|w32.MB_OKCANCEL) - // if result == 1 { - // exec.Command("rundll32", "url.dll,FileProtocolHandler", "https://wails.io").Start() - // } - - err := fmt.Errorf(`Wails applications will not build without the correct build tags.`) - - return nil, err + return nil, fmt.Errorf(`Wails applications will not build without the correct build tags.`) } diff --git a/v2/internal/appng/app_default_linux.go b/v2/internal/appng/app_default_linux.go index 3356debae..ec54dd107 100644 --- a/v2/internal/appng/app_default_linux.go +++ b/v2/internal/appng/app_default_linux.go @@ -17,16 +17,5 @@ func (a *App) Run() error { // CreateApp creates the app! func CreateApp(_ *options.App) (*App, error) { - // result := w32.MessageBox(0, - // `Wails applications will not build without the correct build tags. - //Please use "wails build" or press "OK" to open the documentation on how to use "go build"`, - // "Error", - // w32.MB_ICONERROR|w32.MB_OKCANCEL) - // if result == 1 { - // exec.Command("rundll32", "url.dll,FileProtocolHandler", "https://wails.io").Start() - // } - - err := fmt.Errorf(`Wails applications will not build without the correct build tags.`) - - return nil, err + return nil, fmt.Errorf(`Wails applications will not build without the correct build tags.`) } diff --git a/v2/internal/appng/app_production.go b/v2/internal/appng/app_production.go index c4a67e996..4161d2405 100644 --- a/v2/internal/appng/app_production.go +++ b/v2/internal/appng/app_production.go @@ -78,12 +78,14 @@ func CreateApp(appoptions *options.App) (*App, error) { ctx = context.WithValue(ctx, "events", eventHandler) messageDispatcher := dispatcher.NewDispatcher(myLogger, appBindings, eventHandler) - appFrontend := desktop.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher) - eventHandler.AddFrontend(appFrontend) - + debug := IsDebug() + ctx = context.WithValue(ctx, "debug", debug) // Attach logger to context ctx = context.WithValue(ctx, "logger", myLogger) + appFrontend := desktop.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher) + eventHandler.AddFrontend(appFrontend) + result := &App{ ctx: ctx, frontend: appFrontend, @@ -91,13 +93,10 @@ func CreateApp(appoptions *options.App) (*App, error) { menuManager: menuManager, startupCallback: appoptions.OnStartup, shutdownCallback: appoptions.OnShutdown, - debug: IsDebug(), + debug: debug, + options: appoptions, } - result.options = appoptions - - result.ctx = context.WithValue(result.ctx, "debug", result.debug) - return result, nil } diff --git a/v2/internal/frontend/desktop/linux/calloc.go b/v2/internal/frontend/desktop/linux/calloc.go new file mode 100644 index 000000000..8a8158ba8 --- /dev/null +++ b/v2/internal/frontend/desktop/linux/calloc.go @@ -0,0 +1,32 @@ +package linux + +/* +#include +*/ +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{} +} diff --git a/v2/internal/frontend/desktop/linux/dialog.go b/v2/internal/frontend/desktop/linux/dialog.go index 4e3f1e05b..c0d9158e5 100644 --- a/v2/internal/frontend/desktop/linux/dialog.go +++ b/v2/internal/frontend/desktop/linux/dialog.go @@ -3,24 +3,87 @@ package linux -import "github.com/wailsapp/wails/v2/internal/frontend" +import ( + "github.com/wailsapp/wails/v2/internal/frontend" + "unsafe" +) -func (f *Frontend) OpenFileDialog(dialogOptions frontend.OpenDialogOptions) (string, error) { - panic("implement me") +/* +#include +#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) { - panic("implement me") + f.mainWindow.OpenFileDialog(dialogOptions, 1, GTK_FILE_CHOOSER_ACTION_OPEN) + result := <-openFileResults + return result, nil } func (f *Frontend) OpenDirectoryDialog(dialogOptions frontend.OpenDialogOptions) (string, error) { - panic("implement me") + 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) { - panic("implement me") + 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) { - panic("implement me") + 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) } diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go index 1ad3436b7..89e1c21f6 100644 --- a/v2/internal/frontend/desktop/linux/frontend.go +++ b/v2/internal/frontend/desktop/linux/frontend.go @@ -345,24 +345,22 @@ func (f *Frontend) processRequest(request unsafe.Pointer) { content, mimeType, err := f.assets.Load(file) // TODO How to return 404/500 errors to webkit? - if err != nil { - var gerr *C.GError - if os.IsNotExist(err) { - message := C.CString("not found") - defer C.free(unsafe.Pointer(message)) - gerr = C.g_error_new_literal(C.G_FILE_ERROR_NOENT, C.int(404), message) - C.webkit_uri_scheme_request_finish_error(req, gerr) - } else { - err = fmt.Errorf("Error processing request %s: %w", uri, err) - f.logger.Error(err.Error()) - message := C.CString("internal server error") - defer C.free(unsafe.Pointer(message)) - gerr = C.g_error_new_literal(C.G_FILE_ERROR_NOENT, C.int(500), message) - C.webkit_uri_scheme_request_finish_error(req, gerr) - } - C.g_error_free(gerr) - return - } + //if err != nil { + //if os.IsNotExist(err) { + // f.dispatch(func() { + // message := C.CString("not found") + // defer C.free(unsafe.Pointer(message)) + // C.webkit_uri_scheme_request_finish_error(req, C.g_error_new_literal(C.G_FILE_ERROR_NOENT, C.int(404), message)) + // }) + //} else { + // err = fmt.Errorf("Error processing request %s: %w", uri, err) + // f.logger.Error(err.Error()) + // message := C.CString("internal server error") + // defer C.free(unsafe.Pointer(message)) + // C.webkit_uri_scheme_request_finish_error(req, C.g_error_new_literal(C.G_FILE_ERROR_NOENT, C.int(500), message)) + //} + //return + //} cContent := C.CString(string(content)) defer C.free(unsafe.Pointer(cContent)) diff --git a/v2/internal/frontend/desktop/linux/window.go b/v2/internal/frontend/desktop/linux/window.go index 9eca7af50..f27733f75 100644 --- a/v2/internal/frontend/desktop/linux/window.go +++ b/v2/internal/frontend/desktop/linux/window.go @@ -142,7 +142,7 @@ void connectButtons(void* webview) { g_signal_connect(WEBKIT_WEB_VIEW(webview), "button-release-event", G_CALLBACK(buttonRelease), NULL); } -extern void processURLRequest(WebKitURISchemeRequest *request); +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) @@ -196,13 +196,188 @@ int executeJS(gpointer data) { return G_SOURCE_REMOVE; } -void ExecuteOnMainThread(JSCallback* jscallback) { - g_idle_add((GSourceFunc)executeJS, (gpointer)jscallback); +void ExecuteOnMainThread(void* f, gpointer jscallback) { + g_idle_add((GSourceFunc)f, (gpointer)jscallback); } + +void extern processMessageDialogResult(char*); + +typedef struct MessageDialogOptions { + void* window; + char* title; + char* message; + int messageType; +} MessageDialogOptions; + +void messageDialog(gpointer 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*); + +typedef struct OpenFileDialogOptions { + void* webview; + char* title; + char* defaultFilename; + char* defaultDirectory; + int createDirectories; + int multipleFiles; + int showHiddenFiles; + GtkFileChooserAction action; + GtkFileFilter** filters; +} OpenFileDialogOptions; + +GtkFileFilter** allocFileFilterArray(size_t ln) { + return (GtkFileFilter**) malloc(ln * sizeof(GtkFileFilter*)); +} + +void freeFileFilterArray(GtkFileFilter** filters) { + free(filters); +} + +int opendialog(gpointer 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->webview, 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); + return G_SOURCE_REMOVE; +} + +GtkFileFilter* newFileFilter() { + GtkFileFilter* result = gtk_file_filter_new(); + g_object_ref(result); + return result; +} + */ import "C" import ( + "github.com/wailsapp/wails/v2/internal/frontend" "github.com/wailsapp/wails/v2/pkg/options" + "strings" "unsafe" ) @@ -253,6 +428,7 @@ func NewWindow(appoptions *options.App, debug bool) *Window { if debug { C.devtoolsEnabled(unsafe.Pointer(webview), C.int(1)) + } // Setup window @@ -384,6 +560,7 @@ func (w *Window) Run() { case options.Maximised: w.Maximise() } + C.gtk_main() w.Destroy() } @@ -413,9 +590,9 @@ func (w *Window) SetTitle(title string) { func (w *Window) ExecJS(js string) { jscallback := C.JSCallback{ webview: w.webview, - script: C.CString(js), + script: C.CString(js), } - C.ExecuteOnMainThread(&jscallback) + C.ExecuteOnMainThread(C.executeJS, C.gpointer(&jscallback)) } func (w *Window) StartDrag() { @@ -425,3 +602,77 @@ func (w *Window) StartDrag() { func (w *Window) Quit() { C.gtk_main_quit() } + +func (w *Window) OpenFileDialog(dialogOptions frontend.OpenDialogOptions, multipleFiles int, action C.GtkFileChooserAction) { + + data := C.OpenFileDialogOptions{ + webview: w.webview, + 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.ulong)(arraySize)) + filters := (*[1 << 30]*C.struct__GtkFileFilter)(unsafe.Pointer(data.filters)) + 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) + } + + C.ExecuteOnMainThread(C.opendialog, C.gpointer(&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) + } + C.ExecuteOnMainThread(C.messageDialog, C.gpointer(&data)) +} diff --git a/website/docs/reference/runtime/dialog.mdx b/website/docs/reference/runtime/dialog.mdx index fb47d34cb..88fc5ea40 100644 --- a/website/docs/reference/runtime/dialog.mdx +++ b/website/docs/reference/runtime/dialog.mdx @@ -75,16 +75,16 @@ type OpenDialogOptions struct { TreatPackagesAsDirectories bool } ``` -| Field | Description | Win | Mac | -| -------------------------- | ---------------------------------------------- | --- | --- | -| DefaultDirectory | The directory the dialog will show when opened | ✅ | ✅ | -| DefaultFilename | The default filename | ✅ | ✅ | -| Title | Title for the dialog | ✅ | ✅ | -| [Filters](#filefilter) | A list of file filters | ✅ | ✅ | -| ShowHiddenFiles | Show files hidden by the system | | ✅ | -| CanCreateDirectories | Allow user to create directories | | ✅ | -| ResolvesAliases | If true, returns the file not the alias | | ✅ | -| TreatPackagesAsDirectories | Allow navigating into packages | | ✅ | +| Field | Description | Win | Mac | Lin | +| -------------------------- | ---------------------------------------------- | --- | --- | --- | +| DefaultDirectory | The directory the dialog will show when opened | ✅ | ✅ | ✅ | +| DefaultFilename | The default filename | ✅ | ✅ | ✅ | +| Title | Title for the dialog | ✅ | ✅ | ✅ | +| [Filters](#filefilter) | A list of file filters | ✅ | ✅ | ✅ | +| ShowHiddenFiles | Show files hidden by the system | | ✅ | ✅ | +| CanCreateDirectories | Allow user to create directories | | ✅ | | +| ResolvesAliases | If true, returns the file not the alias | | ✅ | | +| TreatPackagesAsDirectories | Allow navigating into packages | | ✅ | | ### SaveDialogOptions @@ -101,15 +101,15 @@ type SaveDialogOptions struct { } ``` -| Field | Description | Win | Mac | -| -------------------------- | ---------------------------------------------- | --- | --- | -| DefaultDirectory | The directory the dialog will show when opened | ✅ | ✅ | -| DefaultFilename | The default filename | ✅ | ✅ | -| Title | Title for the dialog | ✅ | ✅ | -| [Filters](#filefilter) | A list of file filters | ✅ | ✅ | -| ShowHiddenFiles | Show files hidden by the system | | ✅ | -| CanCreateDirectories | Allow user to create directories | | ✅ | -| TreatPackagesAsDirectories | Allow navigating into packages | | ✅ | +| Field | Description | Win | Mac | Lin | +| -------------------------- | ---------------------------------------------- | --- | --- | --- | +| DefaultDirectory | The directory the dialog will show when opened | ✅ | ✅ | ✅ | +| DefaultFilename | The default filename | ✅ | ✅ | ✅ | +| Title | Title for the dialog | ✅ | ✅ | ✅ | +| [Filters](#filefilter) | A list of file filters | ✅ | ✅ | ✅ | +| ShowHiddenFiles | Show files hidden by the system | | ✅ | ✅ | +| CanCreateDirectories | Allow user to create directories | | ✅ | | +| TreatPackagesAsDirectories | Allow navigating into packages | | ✅ | | ### MessageDialogOptions @@ -123,20 +123,25 @@ type MessageDialogOptions struct { CancelButton string } ``` -| Field | Description | Win | Mac | -| ------------- | ------------------------------------------------------------------------- | --- | --- | -| Type | The type of message dialog, eg question, info... | ✅ | ✅ | -| Title | Title for the dialog | ✅ | ✅ | -| Message | The message to show the user | ✅ | ✅ | -| Buttons | A list of button titles | | ✅ | -| DefaultButton | The button with this text should be treated as default. Bound to `return` | | ✅ | -| CancelButton | The button with this text should be treated as cancel. Bound to `escape` | | ✅ | +| Field | Description | Win | Mac | Lin | +| ------------- | ------------------------------------------------------------------------- | --- | --- | --- | +| Type | The type of message dialog, eg question, info... | ✅ | ✅ | ✅ | +| Title | Title for the dialog | ✅ | ✅ | ✅ | +| Message | The message to show the user | ✅ | ✅ | ✅ | +| Buttons | A list of button titles | | ✅ | | +| DefaultButton | The button with this text should be treated as default. Bound to `return` | | ✅ | | +| CancelButton | The button with this text should be treated as cancel. Bound to `escape` | | ✅ | | #### Windows Windows has standard dialog types in which the buttons are not customisable. The value returned will be one of: "Ok", "Cancel", "Abort", "Retry", "Ignore", "Yes", "No", "Try Again" or "Continue" +#### Linux + +Linux has standard dialog types in which the buttons are not customisable. +The value returned will be one of: "Ok", "Cancel", "Yes", "No" + #### Mac A message dialog on Mac may specify up to 4 buttons. If no `DefaultButton` or `CancelButton` is given, the first button @@ -222,6 +227,19 @@ dialog:

+#### Linux + +Linux allows you to use multiple file filters in dialog boxes. Each FileFilter will show up as a separate entry in the +dialog: + +
+ +
+
+
+
+ + #### Mac Mac dialogs only have the concept of a single set of patterns to filter files. If multiple FileFilters are provided,