5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-21 19:39:29 +08:00

Merge pull request #1089 from wailsapp/feature/fix-linux-dialogs

[linux] Fix dialogs
This commit is contained in:
Lea Anthony 2022-01-26 18:27:20 +11:00 committed by GitHub
commit 160b650833
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 428 additions and 89 deletions

View File

@ -17,16 +17,5 @@ func (a *App) Run() error {
// CreateApp creates the app! // CreateApp creates the app!
func CreateApp(_ *options.App) (*App, error) { func CreateApp(_ *options.App) (*App, error) {
// result := w32.MessageBox(0, return nil, fmt.Errorf(`Wails applications will not build without the correct build tags.`)
// `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
} }

View File

@ -17,16 +17,5 @@ func (a *App) Run() error {
// CreateApp creates the app! // CreateApp creates the app!
func CreateApp(_ *options.App) (*App, error) { func CreateApp(_ *options.App) (*App, error) {
// result := w32.MessageBox(0, return nil, fmt.Errorf(`Wails applications will not build without the correct build tags.`)
// `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
} }

View File

@ -78,12 +78,14 @@ func CreateApp(appoptions *options.App) (*App, error) {
ctx = context.WithValue(ctx, "events", eventHandler) ctx = context.WithValue(ctx, "events", eventHandler)
messageDispatcher := dispatcher.NewDispatcher(myLogger, appBindings, eventHandler) messageDispatcher := dispatcher.NewDispatcher(myLogger, appBindings, eventHandler)
appFrontend := desktop.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher) debug := IsDebug()
eventHandler.AddFrontend(appFrontend) ctx = context.WithValue(ctx, "debug", debug)
// Attach logger to context // Attach logger to context
ctx = context.WithValue(ctx, "logger", myLogger) ctx = context.WithValue(ctx, "logger", myLogger)
appFrontend := desktop.NewFrontend(ctx, appoptions, myLogger, appBindings, messageDispatcher)
eventHandler.AddFrontend(appFrontend)
result := &App{ result := &App{
ctx: ctx, ctx: ctx,
frontend: appFrontend, frontend: appFrontend,
@ -91,13 +93,10 @@ func CreateApp(appoptions *options.App) (*App, error) {
menuManager: menuManager, menuManager: menuManager,
startupCallback: appoptions.OnStartup, startupCallback: appoptions.OnStartup,
shutdownCallback: appoptions.OnShutdown, shutdownCallback: appoptions.OnShutdown,
debug: IsDebug(), debug: debug,
options: appoptions,
} }
result.options = appoptions
result.ctx = context.WithValue(result.ctx, "debug", result.debug)
return result, nil return result, nil
} }

View File

@ -0,0 +1,32 @@
package linux
/*
#include <stdlib.h>
*/
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{}
}

View File

@ -3,24 +3,87 @@
package linux 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 <stdlib.h>
#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) { 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) { 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) { 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) { 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)
} }

View File

@ -345,24 +345,22 @@ func (f *Frontend) processRequest(request unsafe.Pointer) {
content, mimeType, err := f.assets.Load(file) content, mimeType, err := f.assets.Load(file)
// TODO How to return 404/500 errors to webkit? // TODO How to return 404/500 errors to webkit?
if err != nil { //if err != nil {
var gerr *C.GError //if os.IsNotExist(err) {
if os.IsNotExist(err) { // f.dispatch(func() {
message := C.CString("not found") // message := C.CString("not found")
defer C.free(unsafe.Pointer(message)) // 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, C.g_error_new_literal(C.G_FILE_ERROR_NOENT, C.int(404), message))
C.webkit_uri_scheme_request_finish_error(req, gerr) // })
} else { //} else {
err = fmt.Errorf("Error processing request %s: %w", uri, err) // err = fmt.Errorf("Error processing request %s: %w", uri, err)
f.logger.Error(err.Error()) // f.logger.Error(err.Error())
message := C.CString("internal server error") // message := C.CString("internal server error")
defer C.free(unsafe.Pointer(message)) // 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, C.g_error_new_literal(C.G_FILE_ERROR_NOENT, C.int(500), message))
C.webkit_uri_scheme_request_finish_error(req, gerr) //}
} //return
C.g_error_free(gerr) //}
return
}
cContent := C.CString(string(content)) cContent := C.CString(string(content))
defer C.free(unsafe.Pointer(cContent)) defer C.free(unsafe.Pointer(cContent))

View File

@ -142,7 +142,7 @@ void connectButtons(void* webview) {
g_signal_connect(WEBKIT_WEB_VIEW(webview), "button-release-event", G_CALLBACK(buttonRelease), NULL); 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 // This is called when the close button on the window is pressed
gboolean close_button_pressed(GtkWidget *widget, GdkEvent *event, void* data) gboolean close_button_pressed(GtkWidget *widget, GdkEvent *event, void* data)
@ -196,13 +196,188 @@ int executeJS(gpointer data) {
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
void ExecuteOnMainThread(JSCallback* jscallback) { void ExecuteOnMainThread(void* f, gpointer jscallback) {
g_idle_add((GSourceFunc)executeJS, (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 "C"
import ( import (
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options"
"strings"
"unsafe" "unsafe"
) )
@ -253,6 +428,7 @@ func NewWindow(appoptions *options.App, debug bool) *Window {
if debug { if debug {
C.devtoolsEnabled(unsafe.Pointer(webview), C.int(1)) C.devtoolsEnabled(unsafe.Pointer(webview), C.int(1))
} }
// Setup window // Setup window
@ -384,6 +560,7 @@ func (w *Window) Run() {
case options.Maximised: case options.Maximised:
w.Maximise() w.Maximise()
} }
C.gtk_main() C.gtk_main()
w.Destroy() w.Destroy()
} }
@ -413,9 +590,9 @@ func (w *Window) SetTitle(title string) {
func (w *Window) ExecJS(js string) { func (w *Window) ExecJS(js string) {
jscallback := C.JSCallback{ jscallback := C.JSCallback{
webview: w.webview, 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() { func (w *Window) StartDrag() {
@ -425,3 +602,77 @@ func (w *Window) StartDrag() {
func (w *Window) Quit() { func (w *Window) Quit() {
C.gtk_main_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))
}

View File

@ -75,16 +75,16 @@ type OpenDialogOptions struct {
TreatPackagesAsDirectories bool TreatPackagesAsDirectories bool
} }
``` ```
| Field | Description | Win | Mac | | Field | Description | Win | Mac | Lin |
| -------------------------- | ---------------------------------------------- | --- | --- | | -------------------------- | ---------------------------------------------- | --- | --- | --- |
| DefaultDirectory | The directory the dialog will show when opened | ✅ | ✅ | | DefaultDirectory | The directory the dialog will show when opened | ✅ | ✅ | ✅ |
| DefaultFilename | The default filename | ✅ | ✅ | | DefaultFilename | The default filename | ✅ | ✅ | ✅ |
| Title | Title for the dialog | ✅ | ✅ | | Title | Title for the dialog | ✅ | ✅ | ✅ |
| [Filters](#filefilter) | A list of file filters | ✅ | ✅ | | [Filters](#filefilter) | A list of file filters | ✅ | ✅ | ✅ |
| ShowHiddenFiles | Show files hidden by the system | | ✅ | | ShowHiddenFiles | Show files hidden by the system | | ✅ | ✅ |
| CanCreateDirectories | Allow user to create directories | | ✅ | | CanCreateDirectories | Allow user to create directories | | ✅ | |
| ResolvesAliases | If true, returns the file not the alias | | ✅ | | ResolvesAliases | If true, returns the file not the alias | | ✅ | |
| TreatPackagesAsDirectories | Allow navigating into packages | | ✅ | | TreatPackagesAsDirectories | Allow navigating into packages | | ✅ | |
### SaveDialogOptions ### SaveDialogOptions
@ -101,15 +101,15 @@ type SaveDialogOptions struct {
} }
``` ```
| Field | Description | Win | Mac | | Field | Description | Win | Mac | Lin |
| -------------------------- | ---------------------------------------------- | --- | --- | | -------------------------- | ---------------------------------------------- | --- | --- | --- |
| DefaultDirectory | The directory the dialog will show when opened | ✅ | ✅ | | DefaultDirectory | The directory the dialog will show when opened | ✅ | ✅ | ✅ |
| DefaultFilename | The default filename | ✅ | ✅ | | DefaultFilename | The default filename | ✅ | ✅ | ✅ |
| Title | Title for the dialog | ✅ | ✅ | | Title | Title for the dialog | ✅ | ✅ | ✅ |
| [Filters](#filefilter) | A list of file filters | ✅ | ✅ | | [Filters](#filefilter) | A list of file filters | ✅ | ✅ | ✅ |
| ShowHiddenFiles | Show files hidden by the system | | ✅ | | ShowHiddenFiles | Show files hidden by the system | | ✅ | ✅ |
| CanCreateDirectories | Allow user to create directories | | ✅ | | CanCreateDirectories | Allow user to create directories | | ✅ | |
| TreatPackagesAsDirectories | Allow navigating into packages | | ✅ | | TreatPackagesAsDirectories | Allow navigating into packages | | ✅ | |
### MessageDialogOptions ### MessageDialogOptions
@ -123,20 +123,25 @@ type MessageDialogOptions struct {
CancelButton string CancelButton string
} }
``` ```
| Field | Description | Win | Mac | | Field | Description | Win | Mac | Lin |
| ------------- | ------------------------------------------------------------------------- | --- | --- | | ------------- | ------------------------------------------------------------------------- | --- | --- | --- |
| Type | The type of message dialog, eg question, info... | ✅ | ✅ | | Type | The type of message dialog, eg question, info... | ✅ | ✅ | ✅ |
| Title | Title for the dialog | ✅ | ✅ | | Title | Title for the dialog | ✅ | ✅ | ✅ |
| Message | The message to show the user | ✅ | ✅ | | Message | The message to show the user | ✅ | ✅ | ✅ |
| Buttons | A list of button titles | | ✅ | | Buttons | A list of button titles | | ✅ | |
| DefaultButton | The button with this text should be treated as default. Bound to `return` | | ✅ | | 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` | | ✅ | | CancelButton | The button with this text should be treated as cancel. Bound to `escape` | | ✅ | |
#### Windows #### Windows
Windows has standard dialog types in which the buttons are not customisable. 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" 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 #### Mac
A message dialog on Mac may specify up to 4 buttons. If no `DefaultButton` or `CancelButton` is given, the first button 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:
<br/> <br/>
<br/> <br/>
#### Linux
Linux allows you to use multiple file filters in dialog boxes. Each FileFilter will show up as a separate entry in the
dialog:
<div class="text--center">
<img src="/img/runtime/dialog_lin_filters.png" width="50%" style={{"box-shadow": "rgb(255 255 255 / 20%) 0px 4px 8px 0px, rgb(104 104 104) 0px 6px 20px 0px"}}/>
</div>
<br/>
<br/>
<br/>
#### Mac #### Mac
Mac dialogs only have the concept of a single set of patterns to filter files. If multiple FileFilters are provided, Mac dialogs only have the concept of a single set of patterns to filter files. If multiple FileFilters are provided,