Browse Source

feat: add property customization to save dialogs (#19672)

Shelley Vohr 5 years ago
parent
commit
28466a39d8

+ 15 - 2
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<Object>` - Resolve with an object containing the following:

+ 28 - 9
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)
 }

+ 1 - 1
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;
 

+ 17 - 9
shell/browser/ui/file_dialog.h

@@ -24,18 +24,26 @@ namespace file_dialog {
 typedef std::pair<std::string, std::vector<std::string>> Filter;
 typedef std::vector<Filter> 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;

+ 21 - 8
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<base::FilePath>* 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) {

+ 21 - 10
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

+ 11 - 6
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);
 

+ 5 - 5
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();