Browse Source

feat: promisify dialog.showOpenDialog() (#16973)

* feat: promisify dialog.showOpenDialog()

* address feedback from review

* address feedback from review
Shelley Vohr 6 years ago
parent
commit
e05985145b

+ 16 - 12
atom/browser/api/atom_api_dialog.cc

@@ -16,6 +16,7 @@
 #include "atom/common/native_mate_converters/file_path_converter.h"
 #include "atom/common/native_mate_converters/image_converter.h"
 #include "atom/common/native_mate_converters/net_converter.h"
+#include "atom/common/promise_util.h"
 #include "native_mate/dictionary.h"
 
 #include "atom/common/node_includes.h"
@@ -51,18 +52,20 @@ void ShowMessageBox(int type,
   }
 }
 
-void ShowOpenDialog(const file_dialog::DialogSettings& settings,
-                    mate::Arguments* args) {
-  v8::Local<v8::Value> peek = args->PeekNext();
-  file_dialog::OpenDialogCallback callback;
-  if (mate::Converter<file_dialog::OpenDialogCallback>::FromV8(
-          args->isolate(), peek, &callback)) {
-    file_dialog::ShowOpenDialog(settings, callback);
-  } else {
-    std::vector<base::FilePath> paths;
-    if (file_dialog::ShowOpenDialog(settings, &paths))
-      args->Return(paths);
-  }
+void ShowOpenDialogSync(const file_dialog::DialogSettings& settings,
+                        mate::Arguments* args) {
+  std::vector<base::FilePath> paths;
+  if (file_dialog::ShowOpenDialogSync(settings, &paths))
+    args->Return(paths);
+}
+
+v8::Local<v8::Promise> ShowOpenDialog(
+    const file_dialog::DialogSettings& settings,
+    mate::Arguments* args) {
+  atom::util::Promise promise(args->isolate());
+  v8::Local<v8::Promise> handle = promise.GetHandle();
+  file_dialog::ShowOpenDialog(settings, std::move(promise));
+  return handle;
 }
 
 void ShowSaveDialog(const file_dialog::DialogSettings& settings,
@@ -86,6 +89,7 @@ void Initialize(v8::Local<v8::Object> exports,
   mate::Dictionary dict(context->GetIsolate(), exports);
   dict.SetMethod("showMessageBox", &ShowMessageBox);
   dict.SetMethod("showErrorBox", &atom::ShowErrorBox);
+  dict.SetMethod("showOpenDialogSync", &ShowOpenDialogSync);
   dict.SetMethod("showOpenDialog", &ShowOpenDialog);
   dict.SetMethod("showSaveDialog", &ShowSaveDialog);
 #if defined(OS_MACOSX) || defined(OS_WIN)

+ 1 - 1
atom/browser/common_web_contents_delegate.cc

@@ -454,7 +454,7 @@ void CommonWebContentsDelegate::DevToolsAddFileSystem(
     settings.parent_window = owner_window();
     settings.force_detached = offscreen_;
     settings.properties = file_dialog::FILE_DIALOG_OPEN_DIRECTORY;
-    if (!file_dialog::ShowOpenDialog(settings, &paths))
+    if (!file_dialog::ShowOpenDialogSync(settings, &paths))
       return;
 
     path = paths[0];

+ 6 - 12
atom/browser/ui/file_dialog.h

@@ -9,8 +9,11 @@
 #include <utility>
 #include <vector>
 
+#include "atom/common/native_mate_converters/file_path_converter.h"
+#include "atom/common/promise_util.h"
 #include "base/callback_forward.h"
 #include "base/files/file_path.h"
+#include "native_mate/dictionary.h"
 
 namespace atom {
 class NativeWindow;
@@ -34,20 +37,11 @@ enum FileDialogProperty {
 };
 
 #if defined(MAS_BUILD)
-typedef base::Callback<void(bool result,
-                            const std::vector<base::FilePath>& paths,
-                            const std::vector<std::string>& bookmarkData)>
-    OpenDialogCallback;
-
 typedef base::Callback<void(bool result,
                             const base::FilePath& path,
                             const std::string& bookmarkData)>
     SaveDialogCallback;
 #else
-typedef base::Callback<void(bool result,
-                            const std::vector<base::FilePath>& paths)>
-    OpenDialogCallback;
-
 typedef base::Callback<void(bool result, const base::FilePath& path)>
     SaveDialogCallback;
 #endif
@@ -70,11 +64,11 @@ struct DialogSettings {
   ~DialogSettings();
 };
 
-bool ShowOpenDialog(const DialogSettings& settings,
-                    std::vector<base::FilePath>* paths);
+bool ShowOpenDialogSync(const DialogSettings& settings,
+                        std::vector<base::FilePath>* paths);
 
 void ShowOpenDialog(const DialogSettings& settings,
-                    const OpenDialogCallback& callback);
+                    atom::util::Promise promise);
 
 bool ShowSaveDialog(const DialogSettings& settings, base::FilePath* path);
 

+ 19 - 14
atom/browser/ui/file_dialog_gtk.cc

@@ -131,8 +131,8 @@ class FileChooserDialog {
     RunAsynchronous();
   }
 
-  void RunOpenAsynchronous(const OpenDialogCallback& callback) {
-    open_callback_ = callback;
+  void RunOpenAsynchronous(atom::util::Promise promise) {
+    open_promise_.reset(new atom::util::Promise(std::move(promise)));
     RunAsynchronous();
   }
 
@@ -174,7 +174,7 @@ class FileChooserDialog {
 
   Filters filters_;
   SaveDialogCallback save_callback_;
-  OpenDialogCallback open_callback_;
+  std::unique_ptr<atom::util::Promise> open_promise_;
 
   // Callback for when we update the preview for the selection.
   CHROMEG_CALLBACK_0(FileChooserDialog, void, OnUpdatePreview, GtkWidget*);
@@ -190,11 +190,17 @@ void FileChooserDialog::OnFileDialogResponse(GtkWidget* widget, int response) {
       save_callback_.Run(true, GetFileName());
     else
       save_callback_.Run(false, base::FilePath());
-  } else if (!open_callback_.is_null()) {
-    if (response == GTK_RESPONSE_ACCEPT)
-      open_callback_.Run(true, GetFileNames());
-    else
-      open_callback_.Run(false, std::vector<base::FilePath>());
+  } else if (open_promise_) {
+    mate::Dictionary dict =
+        mate::Dictionary::CreateEmpty(open_promise_->isolate());
+    if (response == GTK_RESPONSE_ACCEPT) {
+      dict.Set("canceled", false);
+      dict.Set("filePaths", GetFileNames());
+    } else {
+      dict.Set("canceled", true);
+      dict.Set("filePaths", std::vector<base::FilePath>());
+    }
+    open_promise_->Resolve(dict.GetHandle());
   }
   delete this;
 }
@@ -252,8 +258,8 @@ void FileChooserDialog::OnUpdatePreview(GtkWidget* chooser) {
 
 }  // namespace
 
-bool ShowOpenDialog(const DialogSettings& settings,
-                    std::vector<base::FilePath>* paths) {
+bool ShowOpenDialogSync(const DialogSettings& settings,
+                        std::vector<base::FilePath>* paths) {
   GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN;
   if (settings.properties & FILE_DIALOG_OPEN_DIRECTORY)
     action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
@@ -265,19 +271,18 @@ bool ShowOpenDialog(const DialogSettings& settings,
   if (response == GTK_RESPONSE_ACCEPT) {
     *paths = open_dialog.GetFileNames();
     return true;
-  } else {
-    return false;
   }
+  return false;
 }
 
 void ShowOpenDialog(const DialogSettings& settings,
-                    const OpenDialogCallback& callback) {
+                    atom::util::Promise promise) {
   GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN;
   if (settings.properties & FILE_DIALOG_OPEN_DIRECTORY)
     action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
   FileChooserDialog* open_dialog = new FileChooserDialog(action, settings);
   open_dialog->SetupProperties(settings.properties);
-  open_dialog->RunOpenAsynchronous(callback);
+  open_dialog->RunOpenAsynchronous(std::move(promise));
 }
 
 bool ShowSaveDialog(const DialogSettings& settings, base::FilePath* path) {

+ 26 - 22
atom/browser/ui/file_dialog_mac.mm

@@ -268,8 +268,8 @@ void ReadDialogPaths(NSOpenPanel* dialog, std::vector<base::FilePath>* paths) {
 
 }  // namespace
 
-bool ShowOpenDialog(const DialogSettings& settings,
-                    std::vector<base::FilePath>* paths) {
+bool ShowOpenDialogSync(const DialogSettings& settings,
+                        std::vector<base::FilePath>* paths) {
   DCHECK(paths);
   NSOpenPanel* dialog = [NSOpenPanel openPanel];
 
@@ -287,58 +287,62 @@ bool ShowOpenDialog(const DialogSettings& settings,
 void OpenDialogCompletion(int chosen,
                           NSOpenPanel* dialog,
                           bool security_scoped_bookmarks,
-                          const OpenDialogCallback& callback) {
+                          atom::util::Promise promise) {
+  mate::Dictionary dict = mate::Dictionary::CreateEmpty(promise.isolate());
   if (chosen == NSFileHandlingPanelCancelButton) {
+    dict.Set("canceled", true);
+    dict.Set("filePaths", std::vector<base::FilePath>());
 #if defined(MAS_BUILD)
-    callback.Run(false, std::vector<base::FilePath>(),
-                 std::vector<std::string>());
-#else
-    callback.Run(false, std::vector<base::FilePath>());
+    dict.Set("bookmarks", std::vector<std::string>());
 #endif
+    promise.Resolve(dict.GetHandle());
   } else {
     std::vector<base::FilePath> paths;
+    dict.Set("canceled", false);
 #if defined(MAS_BUILD)
     std::vector<std::string> bookmarks;
-    if (security_scoped_bookmarks) {
+    if (security_scoped_bookmarks)
       ReadDialogPathsWithBookmarks(dialog, &paths, &bookmarks);
-    } else {
+    else
       ReadDialogPaths(dialog, &paths);
-    }
-    callback.Run(true, paths, bookmarks);
+    dict.Set("filePaths", paths);
+    dict.Set("bookmarks", bookmarks);
 #else
     ReadDialogPaths(dialog, &paths);
-    callback.Run(true, paths);
+    dict.Set("filePaths", paths);
 #endif
+    promise.Resolve(dict.GetHandle());
   }
 }
 
 void ShowOpenDialog(const DialogSettings& settings,
-                    const OpenDialogCallback& c) {
+                    atom::util::Promise promise) {
   NSOpenPanel* dialog = [NSOpenPanel openPanel];
 
   SetupDialog(dialog, settings);
   SetupDialogForProperties(dialog, settings.properties);
 
-  // Duplicate the callback object here since c is a reference and gcd would
-  // only store the pointer, by duplication we can force gcd to store a copy.
-  __block OpenDialogCallback callback = c;
   // Capture the value of the security_scoped_bookmarks settings flag
   // and pass it to the completion handler.
   bool security_scoped_bookmarks = settings.security_scoped_bookmarks;
 
+  __block atom::util::Promise p = std::move(promise);
+
   if (!settings.parent_window || !settings.parent_window->GetNativeWindow() ||
       settings.force_detached) {
     [dialog beginWithCompletionHandler:^(NSInteger chosen) {
-      OpenDialogCompletion(chosen, dialog, security_scoped_bookmarks, callback);
+      OpenDialogCompletion(chosen, dialog, security_scoped_bookmarks,
+                           std::move(p));
     }];
   } else {
     NSWindow* window =
         settings.parent_window->GetNativeWindow().GetNativeNSWindow();
-    [dialog beginSheetModalForWindow:window
-                   completionHandler:^(NSInteger chosen) {
-                     OpenDialogCompletion(chosen, dialog,
-                                          security_scoped_bookmarks, callback);
-                   }];
+    [dialog
+        beginSheetModalForWindow:window
+               completionHandler:^(NSInteger chosen) {
+                 OpenDialogCompletion(chosen, dialog, security_scoped_bookmarks,
+                                      std::move(p));
+               }];
   }
 }
 

+ 25 - 13
atom/browser/ui/file_dialog_win.cc

@@ -81,13 +81,23 @@ bool CreateDialogThread(RunState* run_state) {
   return true;
 }
 
+void OnDialogOpened(atom::util::Promise promise,
+                    bool canceled,
+                    std::vector<base::FilePath> paths) {
+  mate::Dictionary dict = mate::Dictionary::CreateEmpty(promise.isolate());
+  dict.Set("canceled", canceled);
+  dict.Set("filePaths", paths);
+  promise.Resolve(dict.GetHandle());
+}
+
 void RunOpenDialogInNewThread(const RunState& run_state,
                               const DialogSettings& settings,
-                              const OpenDialogCallback& callback) {
+                              atom::util::Promise promise) {
   std::vector<base::FilePath> paths;
-  bool result = ShowOpenDialog(settings, &paths);
-  run_state.ui_task_runner->PostTask(FROM_HERE,
-                                     base::Bind(callback, result, paths));
+  bool result = ShowOpenDialogSync(settings, &paths);
+  run_state.ui_task_runner->PostTask(
+      FROM_HERE,
+      base::BindOnce(&OnDialogOpened, std::move(promise), result, paths));
   run_state.ui_task_runner->DeleteSoon(FROM_HERE, run_state.dialog_thread);
 }
 
@@ -197,8 +207,8 @@ static void ApplySettings(IFileDialog* dialog, const DialogSettings& settings) {
   }
 }
 
-bool ShowOpenDialog(const DialogSettings& settings,
-                    std::vector<base::FilePath>* paths) {
+bool ShowOpenDialogSync(const DialogSettings& settings,
+                        std::vector<base::FilePath>* paths) {
   ATL::CComPtr<IFileOpenDialog> file_open_dialog;
   HRESULT hr = file_open_dialog.CoCreateInstance(CLSID_FileOpenDialog);
 
@@ -251,16 +261,18 @@ bool ShowOpenDialog(const DialogSettings& settings,
 }
 
 void ShowOpenDialog(const DialogSettings& settings,
-                    const OpenDialogCallback& callback) {
+                    atom::util::Promise promise) {
+  mate::Dictionary dict = mate::Dictionary::CreateEmpty(promise.isolate());
   RunState run_state;
   if (!CreateDialogThread(&run_state)) {
-    callback.Run(false, std::vector<base::FilePath>());
-    return;
+    dict.Set("canceled", true);
+    dict.Set("filePaths", std::vector<base::FilePath>());
+    promise.Resolve(dict.GetHandle());
+  } else {
+    run_state.dialog_thread->task_runner()->PostTask(
+        FROM_HERE, base::BindOnce(&RunOpenDialogInNewThread, run_state,
+                                  settings, std::move(promise)));
   }
-
-  run_state.dialog_thread->task_runner()->PostTask(
-      FROM_HERE,
-      base::Bind(&RunOpenDialogInNewThread, run_state, settings, callback));
 }
 
 bool ShowSaveDialog(const DialogSettings& settings, base::FilePath* path) {

+ 6 - 1
atom/browser/web_dialog_helper.cc

@@ -11,6 +11,7 @@
 #include "atom/browser/atom_browser_context.h"
 #include "atom/browser/native_window.h"
 #include "atom/browser/ui/file_dialog.h"
+#include "atom/common/native_mate_converters/callback.h"
 #include "base/bind.h"
 #include "base/files/file_enumerator.h"
 #include "base/files/file_path.h"
@@ -46,8 +47,12 @@ class FileSelectHelper : public base::RefCounted<FileSelectHelper>,
   }
 
   void ShowOpenDialog(const file_dialog::DialogSettings& settings) {
+    v8::Isolate* isolate = v8::Isolate::GetCurrent();
+    atom::util::Promise promise(isolate);
+
+    file_dialog::ShowOpenDialog(settings, std::move(promise));
     auto callback = base::Bind(&FileSelectHelper::OnOpenDialogDone, this);
-    file_dialog::ShowOpenDialog(settings, callback);
+    ignore_result(promise.Then(callback));
   }
 
   void ShowSaveDialog(const file_dialog::DialogSettings& settings) {

+ 79 - 10
docs/api/dialog.md

@@ -23,7 +23,67 @@ console.log(dialog)
 
 The `dialog` module has the following methods:
 
-### `dialog.showOpenDialog([browserWindow, ]options[, callback])`
+### `dialog.showOpenDialogSync([browserWindow, ]options)`
+
+* `browserWindow` [BrowserWindow](browser-window.md) (optional)
+* `options` Object
+  * `title` String (optional)
+  * `defaultPath` String (optional)
+  * `buttonLabel` String (optional) - Custom label for the confirmation button, when
+    left empty the default label will be used.
+  * `filters` [FileFilter[]](structures/file-filter.md) (optional)
+  * `properties` String[] (optional) - Contains which features the dialog should
+    use. The following values are supported:
+    * `openFile` - Allow files to be selected.
+    * `openDirectory` - Allow directories to be selected.
+    * `multiSelections` - Allow multiple paths to be selected.
+    * `showHiddenFiles` - Show hidden files in dialog.
+    * `createDirectory` _macOS_ - Allow creating new directories from dialog.
+    * `promptToCreate` _Windows_ - Prompt for creation if the file path entered
+      in the dialog does not exist. This does not actually create the file at
+      the path but allows non-existent paths to be returned that should be
+      created by the application.
+    * `noResolveAliases` _macOS_ - Disable the automatic alias (symlink) path
+      resolution. Selected aliases will now return the alias path instead of
+      their target path.
+    * `treatPackageAsDirectory` _macOS_ - Treat packages, such as `.app` folders,
+      as a directory instead of a file.
+  * `message` String (optional) _macOS_ - Message to display above input
+    boxes.
+  * `securityScopedBookmarks` Boolean (optional) _masOS_ _mas_ - Create [security scoped bookmarks](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.
+
+The `browserWindow` argument allows the dialog to attach itself to a parent window, making it modal.
+
+The `filters` specifies an array of file types that can be displayed or
+selected when you want to limit the user to a specific type. For example:
+
+```javascript
+{
+  filters: [
+    { name: 'Images', extensions: ['jpg', 'png', 'gif'] },
+    { name: 'Movies', extensions: ['mkv', 'avi', 'mp4'] },
+    { name: 'Custom File Type', extensions: ['as'] },
+    { name: 'All Files', extensions: ['*'] }
+  ]
+}
+```
+
+The `extensions` array should contain extensions without wildcards or dots (e.g.
+`'png'` is good but `'.png'` and `'*.png'` are bad). To show all files, use the
+`'*'` wildcard (no other wildcard is supported).
+
+**Note:** On Windows and Linux an open dialog can not be both a file selector
+and a directory selector, so if you set `properties` to
+`['openFile', 'openDirectory']` on these platforms, a directory selector will be
+shown.
+
+```js
+dialog.showOpenDialogSync(mainWindow, {
+  properties: ['openFile', 'openDirectory']
+})
+```
+
+### `dialog.showOpenDialog([browserWindow, ]options)`
 
 * `browserWindow` [BrowserWindow](browser-window.md) (optional)
 * `options` Object
@@ -52,11 +112,12 @@ The `dialog` module has the following methods:
     boxes.
   * `securityScopedBookmarks` Boolean (optional) _masOS_ _mas_ - Create [security scoped bookmarks](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.
 * `callback` Function (optional)
-  * `filePaths` String[] (optional) - An array of file paths chosen by the user. If the dialog is cancelled this will be `undefined`.
-  * `bookmarks` String[] (optional) _macOS_ _mas_ - An array matching the `filePaths` array of base64 encoded strings which contains security scoped bookmark data. `securityScopedBookmarks` must be enabled for this to be populated.
 
-Returns `String[] | undefined`, an array of file paths chosen by the user,
-if the callback is provided it returns `undefined`.
+Returns `Promise<Object>` - Resolve wih an object containing the following:
+
+* `canceled` - Boolean - whether or not the dialog was canceled. 
+* `filePaths` String[] (optional) - An array of file paths chosen by the user. If the dialog is cancelled this will be an empty array.
+* `bookmarks` String[] (optional) _macOS_ _mas_ - An array matching the `filePaths` array of base64 encoded strings which contains security scoped bookmark data. `securityScopedBookmarks` must be enabled for this to be populated.
 
 The `browserWindow` argument allows the dialog to attach itself to a parent window, making it modal.
 
@@ -78,14 +139,22 @@ The `extensions` array should contain extensions without wildcards or dots (e.g.
 `'png'` is good but `'.png'` and `'*.png'` are bad). To show all files, use the
 `'*'` wildcard (no other wildcard is supported).
 
-If a `callback` is passed, the API call will be asynchronous and the result
-will be passed via `callback(filenames)`.
-
 **Note:** On Windows and Linux an open dialog can not be both a file selector
 and a directory selector, so if you set `properties` to
 `['openFile', 'openDirectory']` on these platforms, a directory selector will be
 shown.
 
+```js
+dialog.showOpenDialog(mainWindow, {
+  properties: ['openFile', 'openDirectory']
+}).then(result => {
+  console.log(result.canceled)
+  console.log(result.filePaths)
+}).catch(err => {
+  console.log(err)
+})
+```
+
 ### `dialog.showSaveDialog([browserWindow, ]options[, callback])`
 
 * `browserWindow` [BrowserWindow](browser-window.md) (optional)
@@ -201,9 +270,9 @@ attached to the parent window, making it modal.
 
 On Windows the options are more limited, due to the Win32 APIs used:
 
- - The `message` argument is not used, as the OS provides its own confirmation
+* The `message` argument is not used, as the OS provides its own confirmation
    dialog.
- - The `browserWindow` argument is ignored since it is not possible to make
+* The `browserWindow` argument is ignored since it is not possible to make
    this confirmation dialog modal.
 
 ## Sheets

+ 1 - 1
docs/api/promisification.md

@@ -9,7 +9,6 @@ When a majority of affected functions are migrated, this flag will be enabled by
 ### Candidate Functions
 
 - [app.importCertificate(options, callback)](https://github.com/electron/electron/blob/master/docs/api/app.md#importCertificate)
-- [dialog.showOpenDialog([browserWindow, ]options[, callback])](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showOpenDialog)
 - [dialog.showSaveDialog([browserWindow, ]options[, callback])](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showSaveDialog)
 - [dialog.showMessageBox([browserWindow, ]options[, callback])](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showMessageBox)
 - [dialog.showCertificateTrustDialog([browserWindow, ]options, callback)](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showCertificateTrustDialog)
@@ -46,6 +45,7 @@ When a majority of affected functions are migrated, this flag will be enabled by
 - [debugger.sendCommand(method[, commandParams, callback])](https://github.com/electron/electron/blob/master/docs/api/debugger.md#sendCommand)
 - [desktopCapturer.getSources(options, callback)](https://github.com/electron/electron/blob/master/docs/api/desktop-capturer.md#getSources)
 - [netLog.stopLogging([callback])](https://github.com/electron/electron/blob/master/docs/api/net-log.md#stopLogging)
+- [dialog.showOpenDialog([browserWindow, ]options[, callback])](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showOpenDialog)
 - [protocol.isProtocolHandled(scheme, callback)](https://github.com/electron/electron/blob/master/docs/api/protocol.md#isProtocolHandled)
 - [shell.openExternal(url[, options, callback])](https://github.com/electron/electron/blob/master/docs/api/shell.md#openExternal)
 - [webviewTag.capturePage([rect, ]callback)](https://github.com/electron/electron/blob/master/docs/api/webview-tag.md#capturePage)

+ 42 - 56
lib/browser/api/dialog.js

@@ -1,6 +1,6 @@
 'use strict'
 
-const { app, BrowserWindow } = require('electron')
+const { app, BrowserWindow, deprecate } = require('electron')
 const binding = process.atomBinding('dialog')
 const v8Util = process.atomBinding('v8_util')
 
@@ -70,70 +70,54 @@ const checkAppInitialized = function () {
   }
 }
 
-module.exports = {
-  showOpenDialog: function (...args) {
-    checkAppInitialized()
-
-    let [window, options, callback] = parseArgs(...args)
-
-    if (options == null) {
-      options = {
-        title: 'Open',
-        properties: ['openFile']
-      }
-    }
+const openDialog = (sync, window, options) => {
+  checkAppInitialized()
 
-    let { buttonLabel, defaultPath, filters, properties, title, message, securityScopedBookmarks = false } = options
-
-    if (properties == null) {
-      properties = ['openFile']
-    } else 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 (title == null) {
-      title = ''
-    } else if (typeof title !== 'string') {
-      throw new TypeError('Title must be a string')
+  if (window.constructor !== BrowserWindow) options = window
+  if (options == null) {
+    options = {
+      title: 'Open',
+      properties: ['openFile']
     }
+  }
 
-    if (buttonLabel == null) {
-      buttonLabel = ''
-    } else if (typeof buttonLabel !== 'string') {
-      throw new TypeError('Button label must be a string')
+  const {
+    buttonLabel = '',
+    defaultPath = '',
+    filters = [],
+    properties = ['openFile'],
+    title = '',
+    message = '',
+    securityScopedBookmarks = false
+  } = 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 (defaultPath == null) {
-      defaultPath = ''
-    } else if (typeof defaultPath !== 'string') {
-      throw new TypeError('Default path must be a string')
-    }
+  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')
 
-    if (filters == null) {
-      filters = []
-    }
+  const settings = { title, buttonLabel, defaultPath, filters, message, securityScopedBookmarks, window }
+  settings.properties = dialogProperties
 
-    if (message == null) {
-      message = ''
-    } else if (typeof message !== 'string') {
-      throw new TypeError('Message must be a string')
-    }
+  return (sync) ? binding.showOpenDialogSync(settings) : binding.showOpenDialog(settings)
+}
 
-    const wrappedCallback = typeof callback === 'function' ? function (success, result, bookmarkData) {
-      return success ? callback(result, bookmarkData) : callback()
-    } : null
-    const settings = { title, buttonLabel, defaultPath, filters, message, securityScopedBookmarks, window }
-    settings.properties = dialogProperties
-    return binding.showOpenDialog(settings, wrappedCallback)
+module.exports = {
+  showOpenDialog: function (window, options) {
+    return openDialog(false, window, options)
+  },
+  showOpenDialogSync: function (window, options) {
+    return openDialog(true, window, options)
   },
-
   showSaveDialog: function (...args) {
     checkAppInitialized()
 
@@ -306,6 +290,8 @@ module.exports = {
   }
 }
 
+module.exports.showOpenDialog = deprecate.promisify(module.exports.showOpenDialog)
+
 // Mark standard asynchronous functions.
 v8Util.setHiddenValue(module.exports.showMessageBox, 'asynchronous', true)
 v8Util.setHiddenValue(module.exports.showOpenDialog, 'asynchronous', true)

+ 3 - 3
lib/browser/chrome-devtools.js

@@ -86,9 +86,9 @@ ipcMainUtils.handle('ELECTRON_INSPECTOR_SELECT_FILE', function (event) {
   return new Promise((resolve, reject) => {
     assertChromeDevTools(event.sender, 'window.UI.createFileSelectorElement()')
 
-    dialog.showOpenDialog({}, function (files) {
-      if (files) {
-        const path = files[0]
+    dialog.showOpenDialog({}, function (result) {
+      if (!result.canceled) {
+        const path = result.filePaths[0]
         fs.readFile(path, (error, data) => {
           if (error) {
             reject(error)

+ 4 - 2
spec/ts-smoke/electron/main.ts

@@ -479,7 +479,7 @@ contentTracing.startRecording(options, function () {
 // https://github.com/atom/electron/blob/master/docs/api/dialog.md
 
 // variant without browserWindow
-let openDialogResult: string[] = dialog.showOpenDialog({
+dialog.showOpenDialogSync({
   title: 'Testing showOpenDialog',
   defaultPath: '/var/log/syslog',
   filters: [{ name: '', extensions: [''] }],
@@ -487,11 +487,13 @@ let openDialogResult: string[] = dialog.showOpenDialog({
 })
 
 // variant with browserWindow
-openDialogResult = dialog.showOpenDialog(win3, {
+dialog.showOpenDialog(win3, {
   title: 'Testing showOpenDialog',
   defaultPath: '/var/log/syslog',
   filters: [{ name: '', extensions: [''] }],
   properties: ['openFile', 'openDirectory', 'multiSelections']
+}).then(ret => {
+  console.log(ret)
 })
 
 // global-shortcut