From 28466a39d81e8adbade67ff150b7037be4993db2 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 13 Aug 2019 13:40:07 -0700 Subject: [PATCH] feat: add property customization to save dialogs (#19672) --- docs/api/dialog.md | 17 ++++++++- lib/browser/api/dialog.js | 37 ++++++++++++++----- shell/browser/common_web_contents_delegate.cc | 2 +- shell/browser/ui/file_dialog.h | 26 ++++++++----- shell/browser/ui/file_dialog_gtk.cc | 29 +++++++++++---- shell/browser/ui/file_dialog_mac.mm | 31 +++++++++++----- shell/browser/ui/file_dialog_win.cc | 17 ++++++--- shell/browser/web_dialog_helper.cc | 10 ++--- 8 files changed, 119 insertions(+), 50 deletions(-) diff --git a/docs/api/dialog.md b/docs/api/dialog.md index 2579acfcb6c8..6e3065f5fd00 100644 --- a/docs/api/dialog.md +++ b/docs/api/dialog.md @@ -171,6 +171,13 @@ dialog.showOpenDialog(mainWindow, { displayed in front of the filename text field. * `showsTagField` Boolean (optional) _macOS_ - Show the tags input box, defaults to `true`. + * `properties` String[] (optional) + * `showHiddenFiles` - Show hidden files in dialog. + * `createDirectory` _macOS_ - Allow creating new directories from dialog. + * `treatPackageAsDirectory` _macOS_ - Treat packages, such as `.app` folders, + as a directory instead of a file. + * `showOverwriteConfirmation` _Linux_ - Sets whether the user will be presented a confirmation dialog if the user types a file name that already exists. + * `dontAddToRecent` _Windows_ - Do not add the item being saved to the recent documents list. * `securityScopedBookmarks` Boolean (optional) _macOS_ _mas_ - Create a [security scoped bookmark](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) when packaged for the Mac App Store. If this option is enabled and the file doesn't already exist a blank file will be created at the chosen path. Returns `String | undefined`, the path of the file chosen by the user; if the dialog is cancelled it returns `undefined`. @@ -193,8 +200,14 @@ The `filters` specifies an array of file types that can be displayed, see * `message` String (optional) _macOS_ - Message to display above text fields. * `nameFieldLabel` String (optional) _macOS_ - Custom label for the text displayed in front of the filename text field. - * `showsTagField` Boolean (optional) _macOS_ - Show the tags input box, - defaults to `true`. + * `showsTagField` Boolean (optional) _macOS_ - Show the tags input box, defaults to `true`. + * `properties` String[] (optional) + * `showHiddenFiles` - Show hidden files in dialog. + * `createDirectory` _macOS_ - Allow creating new directories from dialog. + * `treatPackageAsDirectory` _macOS_ - Treat packages, such as `.app` folders, + as a directory instead of a file. + * `showOverwriteConfirmation` _Linux_ - Sets whether the user will be presented a confirmation dialog if the user types a file name that already exists. + * `dontAddToRecent` _Windows_ - Do not add the item being saved to the recent documents list. * `securityScopedBookmarks` Boolean (optional) _macOS_ _mas_ - Create a [security scoped bookmark](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) when packaged for the Mac App Store. If this option is enabled and the file doesn't already exist a blank file will be created at the chosen path. Returns `Promise` - Resolve with an object containing the following: diff --git a/lib/browser/api/dialog.js b/lib/browser/api/dialog.js index 13e58f899ec6..017e95abb855 100644 --- a/lib/browser/api/dialog.js +++ b/lib/browser/api/dialog.js @@ -4,7 +4,20 @@ const { app, BrowserWindow, deprecate } = require('electron') const binding = process.electronBinding('dialog') const v8Util = process.electronBinding('v8_util') -const fileDialogProperties = { +const DialogType = { + OPEN: 'OPEN', + SAVE: 'SAVE' +} + +const saveFileDialogProperties = { + createDirectory: 1 << 0, + showHiddenFiles: 1 << 1, + treatPackageAsDirectory: 1 << 2, + showOverwriteConfirmation: 1 << 3, + dontAddToRecent: 1 << 4 +} + +const openFileDialogProperties = { openFile: 1 << 0, openDirectory: 1 << 1, multiSelections: 1 << 2, @@ -45,6 +58,16 @@ const checkAppInitialized = function () { } } +const setupDialogProperties = (type, properties) => { + const dialogPropertiesTypes = (type === DialogType.OPEN) ? openFileDialogProperties : saveFileDialogProperties + let dialogProperties = 0 + for (const prop in dialogPropertiesTypes) { + if (properties.includes(prop)) { + dialogProperties |= dialogPropertiesTypes[prop] + } + } +} + const saveDialog = (sync, window, options) => { checkAppInitialized() @@ -59,6 +82,7 @@ const saveDialog = (sync, window, options) => { buttonLabel = '', defaultPath = '', filters = [], + properties = [], title = '', message = '', securityScopedBookmarks = false, @@ -73,6 +97,8 @@ const saveDialog = (sync, window, options) => { if (typeof nameFieldLabel !== 'string') throw new TypeError('Name field label must be a string') const settings = { buttonLabel, defaultPath, filters, title, message, securityScopedBookmarks, nameFieldLabel, showsTagField, window } + settings.properties = setupDialogProperties(DialogType.SAVE, properties) + return (sync) ? binding.showSaveDialogSync(settings) : binding.showSaveDialog(settings) } @@ -103,20 +129,13 @@ const openDialog = (sync, window, options) => { if (!Array.isArray(properties)) throw new TypeError('Properties must be an array') - let dialogProperties = 0 - for (const prop in fileDialogProperties) { - if (properties.includes(prop)) { - dialogProperties |= fileDialogProperties[prop] - } - } - if (typeof title !== 'string') throw new TypeError('Title must be a string') if (typeof buttonLabel !== 'string') throw new TypeError('Button label must be a string') if (typeof defaultPath !== 'string') throw new TypeError('Default path must be a string') if (typeof message !== 'string') throw new TypeError('Message must be a string') const settings = { title, buttonLabel, defaultPath, filters, message, securityScopedBookmarks, window } - settings.properties = dialogProperties + settings.properties = setupDialogProperties(DialogType.OPEN, properties) return (sync) ? binding.showOpenDialogSync(settings) : binding.showOpenDialog(settings) } diff --git a/shell/browser/common_web_contents_delegate.cc b/shell/browser/common_web_contents_delegate.cc index ffaf692299ab..d7b774ef1a49 100644 --- a/shell/browser/common_web_contents_delegate.cc +++ b/shell/browser/common_web_contents_delegate.cc @@ -455,7 +455,7 @@ void CommonWebContentsDelegate::DevToolsAddFileSystem( file_dialog::DialogSettings settings; settings.parent_window = owner_window(); settings.force_detached = offscreen_; - settings.properties = file_dialog::FILE_DIALOG_OPEN_DIRECTORY; + settings.properties = file_dialog::OPEN_DIALOG_OPEN_DIRECTORY; if (!file_dialog::ShowOpenDialogSync(settings, &paths)) return; diff --git a/shell/browser/ui/file_dialog.h b/shell/browser/ui/file_dialog.h index 04f0b7e16c0b..56876243d162 100644 --- a/shell/browser/ui/file_dialog.h +++ b/shell/browser/ui/file_dialog.h @@ -24,18 +24,26 @@ namespace file_dialog { typedef std::pair> Filter; typedef std::vector Filters; -enum FileDialogProperty { - FILE_DIALOG_OPEN_FILE = 1 << 0, - FILE_DIALOG_OPEN_DIRECTORY = 1 << 1, - FILE_DIALOG_MULTI_SELECTIONS = 1 << 2, - FILE_DIALOG_CREATE_DIRECTORY = 1 << 3, // macOS - FILE_DIALOG_SHOW_HIDDEN_FILES = 1 << 4, - FILE_DIALOG_PROMPT_TO_CREATE = 1 << 5, // Windows - FILE_DIALOG_NO_RESOLVE_ALIASES = 1 << 6, // macOS - FILE_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY = 1 << 7, // macOS +enum OpenFileDialogProperty { + OPEN_DIALOG_OPEN_FILE = 1 << 0, + OPEN_DIALOG_OPEN_DIRECTORY = 1 << 1, + OPEN_DIALOG_MULTI_SELECTIONS = 1 << 2, + OPEN_DIALOG_CREATE_DIRECTORY = 1 << 3, // macOS + OPEN_DIALOG_SHOW_HIDDEN_FILES = 1 << 4, + OPEN_DIALOG_PROMPT_TO_CREATE = 1 << 5, // Windows + OPEN_DIALOG_NO_RESOLVE_ALIASES = 1 << 6, // macOS + OPEN_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY = 1 << 7, // macOS FILE_DIALOG_DONT_ADD_TO_RECENT = 1 << 8, // Windows }; +enum SaveFileDialogProperty { + SAVE_DIALOG_CREATE_DIRECTORY = 1 << 0, + SAVE_DIALOG_SHOW_HIDDEN_FILES = 1 << 1, + SAVE_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY = 1 << 2, // macOS + SAVE_DIALOG_SHOW_OVERWRITE_CONFIRMATION = 1 << 3, // Linux + SAVE_DIALOG_DONT_ADD_TO_RECENT = 1 << 4, // Windows +}; + struct DialogSettings { electron::NativeWindow* parent_window = nullptr; std::string title; diff --git a/shell/browser/ui/file_dialog_gtk.cc b/shell/browser/ui/file_dialog_gtk.cc index 5a989d429f6d..d039cee125c6 100644 --- a/shell/browser/ui/file_dialog_gtk.cc +++ b/shell/browser/ui/file_dialog_gtk.cc @@ -103,15 +103,26 @@ class FileChooserDialog { parent_->SetEnabled(true); } - void SetupProperties(int properties) { - const auto hasProp = [properties](FileDialogProperty prop) { + void SetupOpenProperties(int properties) { + const auto hasProp = [properties](OpenFileDialogProperty prop) { return gboolean((properties & prop) != 0); }; auto* file_chooser = GTK_FILE_CHOOSER(dialog()); gtk_file_chooser_set_select_multiple(file_chooser, - hasProp(FILE_DIALOG_MULTI_SELECTIONS)); + hasProp(OPEN_DIALOG_MULTI_SELECTIONS)); gtk_file_chooser_set_show_hidden(file_chooser, - hasProp(FILE_DIALOG_SHOW_HIDDEN_FILES)); + hasProp(OPEN_DIALOG_SHOW_HIDDEN_FILES)); + } + + void SetupSaveProperties(int properties) { + const auto hasProp = [properties](SaveFileDialogProperty prop) { + return gboolean((properties & prop) != 0); + }; + auto* file_chooser = GTK_FILE_CHOOSER(dialog()); + gtk_file_chooser_set_show_hidden(file_chooser, + hasProp(SAVE_DIALOG_SHOW_HIDDEN_FILES)); + gtk_file_chooser_set_do_overwrite_confirmation( + file_chooser, hasProp(SAVE_DIALOG_SHOW_OVERWRITE_CONFIRMATION)); } void RunAsynchronous() { @@ -267,10 +278,10 @@ void FileChooserDialog::OnUpdatePreview(GtkWidget* chooser) { bool ShowOpenDialogSync(const DialogSettings& settings, std::vector* paths) { GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN; - if (settings.properties & FILE_DIALOG_OPEN_DIRECTORY) + if (settings.properties & OPEN_DIALOG_OPEN_DIRECTORY) action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; FileChooserDialog open_dialog(action, settings); - open_dialog.SetupProperties(settings.properties); + open_dialog.SetupOpenProperties(settings.properties); gtk_widget_show_all(open_dialog.dialog()); int response = gtk_dialog_run(GTK_DIALOG(open_dialog.dialog())); @@ -284,15 +295,17 @@ bool ShowOpenDialogSync(const DialogSettings& settings, void ShowOpenDialog(const DialogSettings& settings, electron::util::Promise promise) { GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN; - if (settings.properties & FILE_DIALOG_OPEN_DIRECTORY) + if (settings.properties & OPEN_DIALOG_OPEN_DIRECTORY) action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; FileChooserDialog* open_dialog = new FileChooserDialog(action, settings); - open_dialog->SetupProperties(settings.properties); + open_dialog->SetupOpenProperties(settings.properties); open_dialog->RunOpenAsynchronous(std::move(promise)); } bool ShowSaveDialogSync(const DialogSettings& settings, base::FilePath* path) { FileChooserDialog save_dialog(GTK_FILE_CHOOSER_ACTION_SAVE, settings); + save_dialog.SetupSaveProperties(settings.properties); + gtk_widget_show_all(save_dialog.dialog()); int response = gtk_dialog_run(GTK_DIALOG(save_dialog.dialog())); if (response == GTK_RESPONSE_ACCEPT) { diff --git a/shell/browser/ui/file_dialog_mac.mm b/shell/browser/ui/file_dialog_mac.mm index 4bc61f76645b..aafd02fc5c5e 100644 --- a/shell/browser/ui/file_dialog_mac.mm +++ b/shell/browser/ui/file_dialog_mac.mm @@ -191,19 +191,28 @@ void SetupDialog(NSSavePanel* dialog, const DialogSettings& settings) { [dialog setNameFieldStringValue:default_filename]; } -void SetupDialogForProperties(NSOpenPanel* dialog, int properties) { - [dialog setCanChooseFiles:(properties & FILE_DIALOG_OPEN_FILE)]; - if (properties & FILE_DIALOG_OPEN_DIRECTORY) +void SetupOpenDialogForProperties(NSOpenPanel* dialog, int properties) { + [dialog setCanChooseFiles:(properties & OPEN_DIALOG_OPEN_FILE)]; + if (properties & OPEN_DIALOG_OPEN_DIRECTORY) [dialog setCanChooseDirectories:YES]; - if (properties & FILE_DIALOG_CREATE_DIRECTORY) + if (properties & OPEN_DIALOG_CREATE_DIRECTORY) [dialog setCanCreateDirectories:YES]; - if (properties & FILE_DIALOG_MULTI_SELECTIONS) + if (properties & OPEN_DIALOG_MULTI_SELECTIONS) [dialog setAllowsMultipleSelection:YES]; - if (properties & FILE_DIALOG_SHOW_HIDDEN_FILES) + if (properties & OPEN_DIALOG_SHOW_HIDDEN_FILES) [dialog setShowsHiddenFiles:YES]; - if (properties & FILE_DIALOG_NO_RESOLVE_ALIASES) + if (properties & OPEN_DIALOG_NO_RESOLVE_ALIASES) [dialog setResolvesAliases:NO]; - if (properties & FILE_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY) + if (properties & OPEN_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY) + [dialog setTreatsFilePackagesAsDirectories:YES]; +} + +void SetupSaveDialogForProperties(NSSavePanel* dialog, int properties) { + if (properties & SAVE_DIALOG_CREATE_DIRECTORY) + [dialog setCanCreateDirectories:YES]; + if (properties & SAVE_DIALOG_SHOW_HIDDEN_FILES) + [dialog setShowsHiddenFiles:YES]; + if (properties & SAVE_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY) [dialog setTreatsFilePackagesAsDirectories:YES]; } @@ -278,7 +287,7 @@ bool ShowOpenDialogSync(const DialogSettings& settings, NSOpenPanel* dialog = [NSOpenPanel openPanel]; SetupDialog(dialog, settings); - SetupDialogForProperties(dialog, settings.properties); + SetupOpenDialogForProperties(dialog, settings.properties); int chosen = RunModalDialog(dialog, settings); if (chosen == NSFileHandlingPanelCancelButton) @@ -324,7 +333,7 @@ void ShowOpenDialog(const DialogSettings& settings, NSOpenPanel* dialog = [NSOpenPanel openPanel]; SetupDialog(dialog, settings); - SetupDialogForProperties(dialog, settings.properties); + SetupOpenDialogForProperties(dialog, settings.properties); // Capture the value of the security_scoped_bookmarks settings flag // and pass it to the completion handler. @@ -355,6 +364,7 @@ bool ShowSaveDialogSync(const DialogSettings& settings, base::FilePath* path) { NSSavePanel* dialog = [NSSavePanel savePanel]; SetupDialog(dialog, settings); + SetupSaveDialogForProperties(dialog, settings.properties); int chosen = RunModalDialog(dialog, settings); if (chosen == NSFileHandlingPanelCancelButton || ![[dialog URL] isFileURL]) @@ -395,6 +405,7 @@ void ShowSaveDialog(const DialogSettings& settings, NSSavePanel* dialog = [NSSavePanel savePanel]; SetupDialog(dialog, settings); + SetupSaveDialogForProperties(dialog, settings.properties); [dialog setCanSelectHiddenExtension:YES]; // Capture the value of the security_scoped_bookmarks settings flag diff --git a/shell/browser/ui/file_dialog_win.cc b/shell/browser/ui/file_dialog_win.cc index 55a0d6e55481..d8ab7910f28d 100644 --- a/shell/browser/ui/file_dialog_win.cc +++ b/shell/browser/ui/file_dialog_win.cc @@ -226,13 +226,13 @@ bool ShowOpenDialogSync(const DialogSettings& settings, return false; DWORD options = FOS_FORCEFILESYSTEM | FOS_FILEMUSTEXIST; - if (settings.properties & FILE_DIALOG_OPEN_DIRECTORY) + if (settings.properties & OPEN_DIALOG_OPEN_DIRECTORY) options |= FOS_PICKFOLDERS; - if (settings.properties & FILE_DIALOG_MULTI_SELECTIONS) + if (settings.properties & OPEN_DIALOG_MULTI_SELECTIONS) options |= FOS_ALLOWMULTISELECT; - if (settings.properties & FILE_DIALOG_SHOW_HIDDEN_FILES) + if (settings.properties & OPEN_DIALOG_SHOW_HIDDEN_FILES) options |= FOS_FORCESHOWHIDDEN; - if (settings.properties & FILE_DIALOG_PROMPT_TO_CREATE) + if (settings.properties & OPEN_DIALOG_PROMPT_TO_CREATE) options |= FOS_CREATEPROMPT; if (settings.properties & FILE_DIALOG_DONT_ADD_TO_RECENT) options |= FOS_DONTADDTORECENT; @@ -294,8 +294,13 @@ bool ShowSaveDialogSync(const DialogSettings& settings, base::FilePath* path) { if (FAILED(hr)) return false; - file_save_dialog->SetOptions(FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST | - FOS_OVERWRITEPROMPT); + DWORD options = FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST | FOS_OVERWRITEPROMPT; + if (settings.properties & SAVE_DIALOG_SHOW_HIDDEN_FILES) + options |= FOS_FORCESHOWHIDDEN; + if (settings.properties & SAVE_DIALOG_DONT_ADD_TO_RECENT) + options |= FOS_DONTADDTORECENT; + + file_save_dialog->SetOptions(options); ApplySettings(file_save_dialog, settings); hr = ShowFileDialog(file_save_dialog, settings); diff --git a/shell/browser/web_dialog_helper.cc b/shell/browser/web_dialog_helper.cc index 85e028d31a51..ef1b2b398cc9 100644 --- a/shell/browser/web_dialog_helper.cc +++ b/shell/browser/web_dialog_helper.cc @@ -301,17 +301,17 @@ void WebDialogHelper::RunFileChooser( settings.default_path = params.default_file_name; file_select_helper->ShowSaveDialog(settings); } else { - int flags = file_dialog::FILE_DIALOG_CREATE_DIRECTORY; + int flags = file_dialog::OPEN_DIALOG_CREATE_DIRECTORY; switch (params.mode) { case FileChooserParams::Mode::kOpenMultiple: - flags |= file_dialog::FILE_DIALOG_MULTI_SELECTIONS; + flags |= file_dialog::OPEN_DIALOG_MULTI_SELECTIONS; FALLTHROUGH; case FileChooserParams::Mode::kOpen: - flags |= file_dialog::FILE_DIALOG_OPEN_FILE; - flags |= file_dialog::FILE_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY; + flags |= file_dialog::OPEN_DIALOG_OPEN_FILE; + flags |= file_dialog::OPEN_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY; break; case FileChooserParams::Mode::kUploadFolder: - flags |= file_dialog::FILE_DIALOG_OPEN_DIRECTORY; + flags |= file_dialog::OPEN_DIALOG_OPEN_DIRECTORY; break; default: NOTREACHED();