From dc257f1f86adcb210bba82a2374f8293810106c8 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Wed, 6 Aug 2014 12:44:02 +0800 Subject: [PATCH 1/5] Add "filters" parameter for file dialogs. --- atom/browser/api/atom_api_dialog.cc | 36 ++++++++++++++++++++++++----- atom/browser/api/lib/dialog.coffee | 4 ++++ atom/browser/native_window.cc | 3 ++- atom/browser/ui/file_dialog.h | 9 ++++++++ atom/browser/ui/file_dialog_gtk.cc | 4 ++++ atom/browser/ui/file_dialog_mac.mm | 4 ++++ atom/browser/ui/file_dialog_win.cc | 8 ++++++- 7 files changed, 60 insertions(+), 8 deletions(-) diff --git a/atom/browser/api/atom_api_dialog.cc b/atom/browser/api/atom_api_dialog.cc index a15a9d8e7e73..6305b5f24792 100644 --- a/atom/browser/api/atom_api_dialog.cc +++ b/atom/browser/api/atom_api_dialog.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include +#include #include #include "atom/browser/api/atom_api_window.h" @@ -16,6 +17,26 @@ #include "atom/common/node_includes.h" +namespace mate { + +template<> +struct Converter { + static bool FromV8(v8::Isolate* isolate, + v8::Handle val, + file_dialog::Filter* out) { + mate::Dictionary dict(isolate); + if (!ConvertFromV8(isolate, val, &dict)) + return false; + if (!dict.Get("name", &(out->first))) + return false; + if (!dict.Get("extensions", &(out->second))) + return false; + return true; + } +}; + +} // namespace mate + namespace { void ShowMessageBox(int type, @@ -41,6 +62,7 @@ void ShowMessageBox(int type, void ShowOpenDialog(const std::string& title, const base::FilePath& default_path, + const file_dialog::Filters& filters, int properties, atom::NativeWindow* window, mate::Arguments* args) { @@ -49,18 +71,19 @@ void ShowOpenDialog(const std::string& title, if (mate::Converter::FromV8(args->isolate(), peek, &callback)) { - file_dialog::ShowOpenDialog(window, title, default_path, properties, - callback); + file_dialog::ShowOpenDialog(window, title, default_path, filters, + properties, callback); } else { std::vector paths; - if (file_dialog::ShowOpenDialog(window, title, default_path, properties, - &paths)) + if (file_dialog::ShowOpenDialog(window, title, default_path, filters, + properties, &paths)) args->Return(paths); } } void ShowSaveDialog(const std::string& title, const base::FilePath& default_path, + const file_dialog::Filters& filters, atom::NativeWindow* window, mate::Arguments* args) { v8::Handle peek = args->PeekNext(); @@ -68,10 +91,11 @@ void ShowSaveDialog(const std::string& title, if (mate::Converter::FromV8(args->isolate(), peek, &callback)) { - file_dialog::ShowSaveDialog(window, title, default_path, callback); + file_dialog::ShowSaveDialog(window, title, default_path, filters, callback); } else { base::FilePath path; - if (file_dialog::ShowSaveDialog(window, title, default_path, &path)) + if (file_dialog::ShowSaveDialog(window, title, default_path, filters, + &path)) args->Return(path); } } diff --git a/atom/browser/api/lib/dialog.coffee b/atom/browser/api/lib/dialog.coffee index 19cae0b3a2c4..b95ff4d96a30 100644 --- a/atom/browser/api/lib/dialog.coffee +++ b/atom/browser/api/lib/dialog.coffee @@ -25,6 +25,7 @@ module.exports = options.title ?= '' options.defaultPath ?= '' + options.filters ?= [] wrappedCallback = if typeof callback is 'function' @@ -34,6 +35,7 @@ module.exports = binding.showOpenDialog String(options.title), String(options.defaultPath), + options.filters properties, window, wrappedCallback @@ -48,6 +50,7 @@ module.exports = options ?= title: 'Save' options.title ?= '' options.defaultPath ?= '' + options.filter ?= [] wrappedCallback = if typeof callback is 'function' @@ -57,6 +60,7 @@ module.exports = binding.showSaveDialog String(options.title), String(options.defaultPath), + options.filters window, wrappedCallback diff --git a/atom/browser/native_window.cc b/atom/browser/native_window.cc index ca3e8789f294..04f89d52187a 100644 --- a/atom/browser/native_window.cc +++ b/atom/browser/native_window.cc @@ -528,8 +528,9 @@ void NativeWindow::DevToolsSaveToFile(const std::string& url, if (it != saved_files_.end() && !save_as) { path = it->second; } else { + file_dialog::Filters filters; base::FilePath default_path(base::FilePath::FromUTF8Unsafe(url)); - if (!file_dialog::ShowSaveDialog(this, url, default_path, &path)) { + if (!file_dialog::ShowSaveDialog(this, url, default_path, filters, &path)) { base::StringValue url_value(url); CallDevToolsFunction("InspectorFrontendAPI.canceledSaveURL", &url_value); return; diff --git a/atom/browser/ui/file_dialog.h b/atom/browser/ui/file_dialog.h index 4a30df28b193..861ec14c06e8 100644 --- a/atom/browser/ui/file_dialog.h +++ b/atom/browser/ui/file_dialog.h @@ -6,6 +6,7 @@ #define ATOM_BROWSER_UI_FILE_DIALOG_H_ #include +#include #include #include "base/callback_forward.h" @@ -17,6 +18,10 @@ class NativeWindow; namespace file_dialog { +// +typedef std::pair > Filter; +typedef std::vector Filters; + enum FileDialogProperty { FILE_DIALOG_OPEN_FILE = 1, FILE_DIALOG_OPEN_DIRECTORY = 2, @@ -33,23 +38,27 @@ typedef base::Callback* paths); void ShowOpenDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, int properties, const OpenDialogCallback& callback); bool ShowSaveDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, base::FilePath* path); void ShowSaveDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, const SaveDialogCallback& callback); } // namespace file_dialog diff --git a/atom/browser/ui/file_dialog_gtk.cc b/atom/browser/ui/file_dialog_gtk.cc index 21cd14166500..c27001f5a370 100644 --- a/atom/browser/ui/file_dialog_gtk.cc +++ b/atom/browser/ui/file_dialog_gtk.cc @@ -167,6 +167,7 @@ void FileChooserDialog::OnFileDialogResponse(GtkWidget* widget, int response) { bool ShowOpenDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, int properties, std::vector* paths) { GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN; @@ -190,6 +191,7 @@ bool ShowOpenDialog(atom::NativeWindow* parent_window, void ShowOpenDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, int properties, const OpenDialogCallback& callback) { GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN; @@ -207,6 +209,7 @@ void ShowOpenDialog(atom::NativeWindow* parent_window, bool ShowSaveDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, base::FilePath* path) { FileChooserDialog save_dialog( GTK_FILE_CHOOSER_ACTION_SAVE, parent_window, title, default_path); @@ -223,6 +226,7 @@ bool ShowSaveDialog(atom::NativeWindow* parent_window, void ShowSaveDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, const SaveDialogCallback& callback) { FileChooserDialog* save_dialog = new FileChooserDialog( GTK_FILE_CHOOSER_ACTION_SAVE, parent_window, title, default_path); diff --git a/atom/browser/ui/file_dialog_mac.mm b/atom/browser/ui/file_dialog_mac.mm index e6292c30102b..fb62e4272861 100644 --- a/atom/browser/ui/file_dialog_mac.mm +++ b/atom/browser/ui/file_dialog_mac.mm @@ -83,6 +83,7 @@ void ReadDialogPaths(NSOpenPanel* dialog, std::vector* paths) { bool ShowOpenDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, int properties, std::vector* paths) { DCHECK(paths); @@ -102,6 +103,7 @@ bool ShowOpenDialog(atom::NativeWindow* parent_window, void ShowOpenDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, int properties, const OpenDialogCallback& c) { NSOpenPanel* dialog = [NSOpenPanel openPanel]; @@ -129,6 +131,7 @@ void ShowOpenDialog(atom::NativeWindow* parent_window, bool ShowSaveDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, base::FilePath* path) { DCHECK(path); NSSavePanel* dialog = [NSSavePanel savePanel]; @@ -146,6 +149,7 @@ bool ShowSaveDialog(atom::NativeWindow* parent_window, void ShowSaveDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, const SaveDialogCallback& c) { NSSavePanel* dialog = [NSSavePanel savePanel]; diff --git a/atom/browser/ui/file_dialog_win.cc b/atom/browser/ui/file_dialog_win.cc index e0436d96c07a..9a7873347a52 100644 --- a/atom/browser/ui/file_dialog_win.cc +++ b/atom/browser/ui/file_dialog_win.cc @@ -205,6 +205,7 @@ class FileDialog { bool ShowOpenDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, int properties, std::vector* paths) { int options = FOS_FORCEFILESYSTEM | FOS_FILEMUSTEXIST; @@ -255,6 +256,7 @@ bool ShowOpenDialog(atom::NativeWindow* parent_window, void ShowOpenDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, int properties, const OpenDialogCallback& callback) { std::vector paths; @@ -262,6 +264,7 @@ void ShowOpenDialog(atom::NativeWindow* parent_window, title, default_path, properties, + filters, &paths); callback.Run(result, paths); } @@ -269,6 +272,7 @@ void ShowOpenDialog(atom::NativeWindow* parent_window, bool ShowSaveDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, base::FilePath* path) { // TODO(zcbenz): Accept custom filters from caller. std::vector file_ext; @@ -312,9 +316,11 @@ bool ShowSaveDialog(atom::NativeWindow* parent_window, void ShowSaveDialog(atom::NativeWindow* parent_window, const std::string& title, const base::FilePath& default_path, + const Filters& filters, const SaveDialogCallback& callback) { base::FilePath path; - bool result = ShowSaveDialog(parent_window, title, default_path, &path); + bool result = ShowSaveDialog(parent_window, title, default_path, filters, + &path); callback.Run(result, path); } From 0721b34847a1794b1b98b4a6ebb023e8f69fd85f Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Wed, 6 Aug 2014 13:47:44 +0800 Subject: [PATCH 2/5] mac: Implement the filters option. --- atom/browser/api/lib/dialog.coffee | 5 +++- atom/browser/ui/file_dialog.h | 8 +++--- atom/browser/ui/file_dialog_mac.mm | 45 ++++++++++++++++++++++++++---- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/atom/browser/api/lib/dialog.coffee b/atom/browser/api/lib/dialog.coffee index b95ff4d96a30..9f51419dbde7 100644 --- a/atom/browser/api/lib/dialog.coffee +++ b/atom/browser/api/lib/dialog.coffee @@ -3,7 +3,10 @@ v8Util = process.atomBinding 'v8_util' BrowserWindow = require 'browser-window' fileDialogProperties = - openFile: 1, openDirectory: 2, multiSelections: 4, createDirectory: 8 + openFile: 1 << 0 + openDirectory: 1 << 1 + multiSelections: 1 << 2 + createDirectory: 1 << 3 messageBoxTypes = ['none', 'info', 'warning'] diff --git a/atom/browser/ui/file_dialog.h b/atom/browser/ui/file_dialog.h index 861ec14c06e8..70189c1f0580 100644 --- a/atom/browser/ui/file_dialog.h +++ b/atom/browser/ui/file_dialog.h @@ -23,10 +23,10 @@ typedef std::pair > Filter; typedef std::vector Filters; enum FileDialogProperty { - FILE_DIALOG_OPEN_FILE = 1, - FILE_DIALOG_OPEN_DIRECTORY = 2, - FILE_DIALOG_MULTI_SELECTIONS = 4, - FILE_DIALOG_CREATE_DIRECTORY = 8, + FILE_DIALOG_OPEN_FILE = 1 << 0, + FILE_DIALOG_OPEN_DIRECTORY = 1 << 1, + FILE_DIALOG_MULTI_SELECTIONS = 1 << 2, + FILE_DIALOG_CREATE_DIRECTORY = 1 << 3, }; typedef base::Callback ext_cf(base::SysUTF8ToCFStringRef(ext)); + return UTTypeCreatePreferredIdentifierForTag( + kUTTagClassFilenameExtension, ext_cf.get(), NULL); +} + +void SetAllowedFileTypes(NSSavePanel* dialog, const Filters& filters) { + NSMutableSet* file_type_set = [NSMutableSet set]; + for (size_t i = 0; i < filters.size(); ++i) { + const Filter& filter = filters[i]; + for (size_t j = 0; j < filter.second.size(); ++j) { + base::ScopedCFTypeRef uti( + CreateUTIFromExtension(filter.second[j])); + [file_type_set addObject:base::mac::CFToNSCast(uti.get())]; + + // Always allow the extension itself, in case the UTI doesn't map + // back to the original extension correctly. This occurs with dynamic + // UTIs on 10.7 and 10.8. + // See http://crbug.com/148840, http://openradar.me/12316273 + base::ScopedCFTypeRef ext_cf( + base::SysUTF8ToCFStringRef(filter.second[j])); + [file_type_set addObject:base::mac::CFToNSCast(ext_cf.get())]; + } + } + [dialog setAllowedFileTypes:[file_type_set allObjects]]; +} + void SetupDialog(NSSavePanel* dialog, const std::string& title, - const base::FilePath& default_path) { + const base::FilePath& default_path, + const Filters& filters) { if (!title.empty()) [dialog setTitle:base::SysUTF8ToNSString(title)]; @@ -39,7 +69,10 @@ void SetupDialog(NSSavePanel* dialog, [dialog setNameFieldStringValue:default_filename]; [dialog setCanSelectHiddenExtension:YES]; - [dialog setAllowsOtherFileTypes:YES]; + if (filters.empty()) + [dialog setAllowsOtherFileTypes:YES]; + else + SetAllowedFileTypes(dialog, filters); } void SetupDialogForProperties(NSOpenPanel* dialog, int properties) { @@ -89,7 +122,7 @@ bool ShowOpenDialog(atom::NativeWindow* parent_window, DCHECK(paths); NSOpenPanel* dialog = [NSOpenPanel openPanel]; - SetupDialog(dialog, title, default_path); + SetupDialog(dialog, title, default_path, filters); SetupDialogForProperties(dialog, properties); int chosen = RunModalDialog(dialog, parent_window); @@ -108,7 +141,7 @@ void ShowOpenDialog(atom::NativeWindow* parent_window, const OpenDialogCallback& c) { NSOpenPanel* dialog = [NSOpenPanel openPanel]; - SetupDialog(dialog, title, default_path); + SetupDialog(dialog, title, default_path, filters); SetupDialogForProperties(dialog, properties); // Duplicate the callback object here since c is a reference and gcd would @@ -136,7 +169,7 @@ bool ShowSaveDialog(atom::NativeWindow* parent_window, DCHECK(path); NSSavePanel* dialog = [NSSavePanel savePanel]; - SetupDialog(dialog, title, default_path); + SetupDialog(dialog, title, default_path, filters); int chosen = RunModalDialog(dialog, parent_window); if (chosen == NSFileHandlingPanelCancelButton || ![[dialog URL] isFileURL]) @@ -153,7 +186,7 @@ void ShowSaveDialog(atom::NativeWindow* parent_window, const SaveDialogCallback& c) { NSSavePanel* dialog = [NSSavePanel savePanel]; - SetupDialog(dialog, title, default_path); + SetupDialog(dialog, title, default_path, filters); __block SaveDialogCallback callback = c; From 5ba324ca9a6315d9b2eb7c7960a78eca6e18c17f Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Wed, 6 Aug 2014 14:49:02 +0800 Subject: [PATCH 3/5] gtk: Implement the filters option. --- atom/browser/ui/file_dialog_gtk.cc | 53 ++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/atom/browser/ui/file_dialog_gtk.cc b/atom/browser/ui/file_dialog_gtk.cc index c27001f5a370..6a890322decd 100644 --- a/atom/browser/ui/file_dialog_gtk.cc +++ b/atom/browser/ui/file_dialog_gtk.cc @@ -17,6 +17,7 @@ #include "atom/browser/native_window.h" #include "base/callback.h" #include "base/file_util.h" +#include "base/strings/string_util.h" #include "chrome/browser/ui/libgtk2ui/gtk2_signal.h" #include "ui/aura/window.h" #include "ui/aura/window_tree_host.h" @@ -45,12 +46,24 @@ void SetGtkTransientForAura(GtkWidget* dialog, aura::Window* parent) { g_object_set_data(G_OBJECT(dialog), kAuraTransientParent, parent); } +// Makes sure that .jpg also shows .JPG. +gboolean FileFilterCaseInsensitive(const GtkFileFilterInfo* file_info, + std::string* file_extension) { + return EndsWith(file_info->filename, *file_extension, false); +} + +// Deletes |data| when gtk_file_filter_add_custom() is done with it. +void OnFileFilterDataDestroyed(std::string* file_extension) { + delete file_extension; +} + class FileChooserDialog { public: FileChooserDialog(GtkFileChooserAction action, atom::NativeWindow* parent_window, const std::string& title, - const base::FilePath& default_path) + const base::FilePath& default_path, + const Filters& filters) : dialog_scope_(new atom::NativeWindow::DialogScope(parent_window)) { const char* confirm_text = GTK_STOCK_OK; if (action == GTK_FILE_CHOOSER_ACTION_SAVE) @@ -86,6 +99,9 @@ class FileChooserDialog { gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog_), default_path.value().c_str()); } + + if (!filters.empty()) + AddFilters(filters); } virtual ~FileChooserDialog() { @@ -135,6 +151,8 @@ class FileChooserDialog { GtkWidget* dialog() const { return dialog_; } private: + void AddFilters(const Filters& filters); + GtkWidget* dialog_; SaveDialogCallback save_callback_; @@ -162,6 +180,27 @@ void FileChooserDialog::OnFileDialogResponse(GtkWidget* widget, int response) { delete this; } +void FileChooserDialog::AddFilters(const Filters& filters) { + for (size_t i = 0; i < filters.size(); ++i) { + const Filter& filter = filters[i]; + GtkFileFilter* gtk_filter = gtk_file_filter_new(); + + for (size_t j = 0; j < filter.second.size(); ++j) { + scoped_ptr file_extension( + new std::string("." + filter.second[j])); + gtk_file_filter_add_custom( + gtk_filter, + GTK_FILE_FILTER_FILENAME, + reinterpret_cast(FileFilterCaseInsensitive), + file_extension.release(), + reinterpret_cast(OnFileFilterDataDestroyed)); + } + + gtk_file_filter_set_name(gtk_filter, filter.first.c_str()); + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog_), gtk_filter); + } +} + } // namespace bool ShowOpenDialog(atom::NativeWindow* parent_window, @@ -173,7 +212,8 @@ bool ShowOpenDialog(atom::NativeWindow* parent_window, GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN; if (properties & FILE_DIALOG_OPEN_DIRECTORY) action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; - FileChooserDialog open_dialog(action, parent_window, title, default_path); + FileChooserDialog open_dialog(action, parent_window, title, default_path, + filters); if (properties & FILE_DIALOG_MULTI_SELECTIONS) gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(open_dialog.dialog()), TRUE); @@ -198,7 +238,7 @@ void ShowOpenDialog(atom::NativeWindow* parent_window, if (properties & FILE_DIALOG_OPEN_DIRECTORY) action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER; FileChooserDialog* open_dialog = new FileChooserDialog( - action, parent_window, title, default_path); + action, parent_window, title, default_path, filters); if (properties & FILE_DIALOG_MULTI_SELECTIONS) gtk_file_chooser_set_select_multiple( GTK_FILE_CHOOSER(open_dialog->dialog()), TRUE); @@ -211,8 +251,8 @@ bool ShowSaveDialog(atom::NativeWindow* parent_window, const base::FilePath& default_path, const Filters& filters, base::FilePath* path) { - FileChooserDialog save_dialog( - GTK_FILE_CHOOSER_ACTION_SAVE, parent_window, title, default_path); + FileChooserDialog save_dialog(GTK_FILE_CHOOSER_ACTION_SAVE, parent_window, + title, default_path, filters); gtk_widget_show_all(save_dialog.dialog()); int response = gtk_dialog_run(GTK_DIALOG(save_dialog.dialog())); if (response == GTK_RESPONSE_ACCEPT) { @@ -229,7 +269,8 @@ void ShowSaveDialog(atom::NativeWindow* parent_window, const Filters& filters, const SaveDialogCallback& callback) { FileChooserDialog* save_dialog = new FileChooserDialog( - GTK_FILE_CHOOSER_ACTION_SAVE, parent_window, title, default_path); + GTK_FILE_CHOOSER_ACTION_SAVE, parent_window, title, default_path, + filters); save_dialog->RunSaveAsynchronous(callback); } From 47e0a61dd8a843beb55542961a1e855d8ff6c815 Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Wed, 6 Aug 2014 15:00:31 +0800 Subject: [PATCH 4/5] docs: Document the filters option. --- docs/api/dialog.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/api/dialog.md b/docs/api/dialog.md index ad3b2e62221d..1778e8fb3a82 100644 --- a/docs/api/dialog.md +++ b/docs/api/dialog.md @@ -19,6 +19,7 @@ console.log(dialog.showOpenDialog({ properties: [ 'openFile', 'openDirectory', ' * `options` Object * `title` String * `defaultPath` String + * `filters` Array * `properties` Array - Contains which features the dialog should use, can contain `openFile`, `openDirectory`, `multiSelections` and `createDirectory` @@ -27,6 +28,19 @@ console.log(dialog.showOpenDialog({ properties: [ 'openFile', 'openDirectory', ' On success, returns an array of file paths chosen by the user, otherwise returns `undefined`. +The `filters` specifies an array of file types that can be displayed or +selected, an example is: + +```javascript +{ + filters: [ + { name: 'Images', extensions: ['jpg', 'png', 'gif'] }, + { name: 'Movies', extensions: ['mkv', 'avi', 'mp4'] }, + { name: 'Custom File Type', extensions: ['as'] }, + ], +} +``` + If a `callback` is passed, the API call would be asynchronous and the result would be passed via `callback(filenames)` @@ -41,11 +55,15 @@ be showed. * `options` Object * `title` String * `defaultPath` String + * `filters` Array * `callback` Function On success, returns the path of file chosen by the user, otherwise returns `undefined`. +The `filters` specifies an array of file types that can be displayed, see +`dialog.showOpenDialog` for an example. + If a `callback` is passed, the API call would be asynchronous and the result would be passed via `callback(filename)` From fe9f94555ba6ba6a5a2837295693f5cc6c9b94cb Mon Sep 17 00:00:00 2001 From: Cheng Zhao Date: Wed, 6 Aug 2014 21:51:36 +0800 Subject: [PATCH 5/5] win: Implement the filters option. --- atom/browser/api/lib/dialog.coffee | 2 +- atom/browser/ui/file_dialog_win.cc | 199 ++++++++--------------------- 2 files changed, 53 insertions(+), 148 deletions(-) diff --git a/atom/browser/api/lib/dialog.coffee b/atom/browser/api/lib/dialog.coffee index 9f51419dbde7..2bb1e9f721fc 100644 --- a/atom/browser/api/lib/dialog.coffee +++ b/atom/browser/api/lib/dialog.coffee @@ -53,7 +53,7 @@ module.exports = options ?= title: 'Save' options.title ?= '' options.defaultPath ?= '' - options.filter ?= [] + options.filters ?= [] wrappedCallback = if typeof callback is 'function' diff --git a/atom/browser/ui/file_dialog_win.cc b/atom/browser/ui/file_dialog_win.cc index 9a7873347a52..0e4defdece4c 100644 --- a/atom/browser/ui/file_dialog_win.cc +++ b/atom/browser/ui/file_dialog_win.cc @@ -30,103 +30,30 @@ bool IsDirectory(const base::FilePath& path) { file_info.is_directory : path.EndsWithSeparator(); } -// Get the file type description from the registry. This will be "Text Document" -// for .txt files, "JPEG Image" for .jpg files, etc. If the registry doesn't -// have an entry for the file type, we return false, true if the description was -// found. 'file_ext' must be in form ".txt". -static bool GetRegistryDescriptionFromExtension(const std::wstring& file_ext, - std::wstring* reg_description) { - DCHECK(reg_description); - base::win::RegKey reg_ext(HKEY_CLASSES_ROOT, file_ext.c_str(), KEY_READ); - std::wstring reg_app; - if (reg_ext.ReadValue(NULL, ®_app) == ERROR_SUCCESS && !reg_app.empty()) { - base::win::RegKey reg_link(HKEY_CLASSES_ROOT, reg_app.c_str(), KEY_READ); - if (reg_link.ReadValue(NULL, reg_description) == ERROR_SUCCESS) - return true; - } - return false; -} - -// Set up a filter for a Save/Open dialog, which will consist of |file_ext| file -// extensions (internally separated by semicolons), |ext_desc| as the text -// descriptions of the |file_ext| types (optional), and (optionally) the default -// 'All Files' view. The purpose of the filter is to show only files of a -// particular type in a Windows Save/Open dialog box. The resulting filter is -// returned. The filters created here are: -// 1. only files that have 'file_ext' as their extension -// 2. all files (only added if 'include_all_files' is true) -// Example: -// file_ext: { "*.txt", "*.htm;*.html" } -// ext_desc: { "Text Document" } -// returned: "Text Document\0*.txt\0HTML Document\0*.htm;*.html\0" -// "All Files\0*.*\0\0" (in one big string) -// If a description is not provided for a file extension, it will be retrieved -// from the registry. If the file extension does not exist in the registry, it -// will be omitted from the filter, as it is likely a bogus extension. -void FormatFilterForExtensions( - std::vector* file_ext, - std::vector* ext_desc, - bool include_all_files, - std::vector* file_types) { - DCHECK(file_ext->size() >= ext_desc->size()); - - if (file_ext->empty()) - include_all_files = true; - - for (size_t i = 0; i < file_ext->size(); ++i) { - std::wstring ext = (*file_ext)[i]; - std::wstring desc; - if (i < ext_desc->size()) - desc = (*ext_desc)[i]; - - if (ext.empty()) { - // Force something reasonable to appear in the dialog box if there is no - // extension provided. - include_all_files = true; - continue; - } - - if (desc.empty()) { - DCHECK(ext.find(L'.') != std::wstring::npos); - std::wstring first_extension = ext.substr(ext.find(L'.')); - size_t first_separator_index = first_extension.find(L';'); - if (first_separator_index != std::wstring::npos) - first_extension = first_extension.substr(0, first_separator_index); - - // Find the extension name without the preceeding '.' character. - std::wstring ext_name = first_extension; - size_t ext_index = ext_name.find_first_not_of(L'.'); - if (ext_index != std::wstring::npos) - ext_name = ext_name.substr(ext_index); - - if (!GetRegistryDescriptionFromExtension(first_extension, &desc)) { - // The extension doesn't exist in the registry. Create a description - // based on the unknown extension type (i.e. if the extension is .qqq, - // the we create a description "QQQ File (.qqq)"). - include_all_files = true; - // TODO(zcbenz): should be localized. - desc = base::i18n::ToUpper(base::WideToUTF16(ext_name)) + L" File"; - } - desc += L" (*." + ext_name + L")"; - - // Store the description. - ext_desc->push_back(desc); - } - - COMDLG_FILTERSPEC spec = { (*ext_desc)[i].c_str(), (*file_ext)[i].c_str() }; - file_types->push_back(spec); +void ConvertFilters(const Filters& filters, + std::vector* buffer, + std::vector* filterspec) { + if (filters.empty()) { + COMDLG_FILTERSPEC spec = { L"All Files (*.*)", L"*.*" }; + filterspec->push_back(spec); + return; } - if (include_all_files) { - // TODO(zcbenz): Should be localized. - ext_desc->push_back(L"All Files (*.*)"); - file_ext->push_back(L"*.*"); + buffer->reserve(filters.size() * 2); + for (size_t i = 0; i < filters.size(); ++i) { + const Filter& filter = filters[i]; - COMDLG_FILTERSPEC spec = { - (*ext_desc)[ext_desc->size() - 1].c_str(), - (*file_ext)[file_ext->size() - 1].c_str(), - }; - file_types->push_back(spec); + COMDLG_FILTERSPEC spec; + buffer->push_back(base::UTF8ToWide(filter.first)); + spec.pszName = buffer->back().c_str(); + + std::vector extensions(filter.second); + for (size_t j = 0; j < extensions.size(); ++j) + extensions[j].insert(0, "*."); + buffer->push_back(base::UTF8ToWide(JoinString(extensions, ";"))); + spec.pszSpec = buffer->back().c_str(); + + filterspec->push_back(spec); } } @@ -135,26 +62,18 @@ void FormatFilterForExtensions( template class FileDialog { public: - FileDialog(const base::FilePath& default_path, - const std::string title, - int options, - const std::vector& file_ext, - const std::vector& desc_ext) - : file_ext_(file_ext), - desc_ext_(desc_ext) { - std::vector filters; - FormatFilterForExtensions(&file_ext_, &desc_ext_, true, &filters); - + FileDialog(const base::FilePath& default_path, const std::string& title, + const Filters& filters, int options) { std::wstring file_part; if (!IsDirectory(default_path)) file_part = default_path.BaseName().value(); - dialog_.reset(new T( - file_part.c_str(), - options, - NULL, - filters.data(), - filters.size())); + std::vector buffer; + std::vector filterspec; + ConvertFilters(filters, &buffer, &filterspec); + + dialog_.reset(new T(file_part.c_str(), options, NULL, + filterspec.data(), filterspec.size())); if (!title.empty()) GetPtr()->SetTitle(base::UTF8ToUTF16(title).c_str()); @@ -174,8 +93,6 @@ class FileDialog { IFileDialog* GetPtr() const { return dialog_->GetPtr(); } - const std::vector file_ext() const { return file_ext_; } - private: // Set up the initial directory for the dialog. void SetDefaultFolder(const base::FilePath file_path) { @@ -193,10 +110,6 @@ class FileDialog { scoped_ptr dialog_; - std::vector file_ext_; - std::vector desc_ext_; - std::vector filters_; - DISALLOW_COPY_AND_ASSIGN(FileDialog); }; @@ -215,11 +128,7 @@ bool ShowOpenDialog(atom::NativeWindow* parent_window, options |= FOS_ALLOWMULTISELECT; FileDialog open_dialog( - default_path, - title, - options, - std::vector(), - std::vector()); + default_path, title, filters, options); if (!open_dialog.Show(parent_window)) return false; @@ -263,8 +172,8 @@ void ShowOpenDialog(atom::NativeWindow* parent_window, bool result = ShowOpenDialog(parent_window, title, default_path, - properties, filters, + properties, &paths); callback.Run(result, paths); } @@ -274,42 +183,38 @@ bool ShowSaveDialog(atom::NativeWindow* parent_window, const base::FilePath& default_path, const Filters& filters, base::FilePath* path) { - // TODO(zcbenz): Accept custom filters from caller. - std::vector file_ext; - std::wstring extension = default_path.Extension(); - if (!extension.empty()) - file_ext.push_back(extension.insert(0, L"*")); - FileDialog save_dialog( - default_path, - title, - FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST | FOS_OVERWRITEPROMPT, - file_ext, - std::vector()); + default_path, title, filters, + FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST | FOS_OVERWRITEPROMPT); if (!save_dialog.Show(parent_window)) return false; - wchar_t file_name[MAX_PATH]; - HRESULT hr = save_dialog.GetDialog()->GetFilePath(file_name, MAX_PATH); + wchar_t buffer[MAX_PATH]; + HRESULT hr = save_dialog.GetDialog()->GetFilePath(buffer, MAX_PATH); if (FAILED(hr)) return false; + std::string file_name = base::WideToUTF8(std::wstring(buffer)); + // Append extension according to selected filter. - UINT filter_index = 1; - save_dialog.GetPtr()->GetFileTypeIndex(&filter_index); - std::wstring selected_filter = save_dialog.file_ext()[filter_index - 1]; - if (selected_filter != L"*.*") { - std::wstring result = file_name; - if (!EndsWith(result, selected_filter.substr(1), false)) { - if (result[result.length() - 1] != L'.') - result.push_back(L'.'); - result.append(selected_filter.substr(2)); - *path = base::FilePath(result); - return true; + if (!filters.empty()) { + UINT filter_index = 1; + save_dialog.GetPtr()->GetFileTypeIndex(&filter_index); + const Filter& filter = filters[filter_index - 1]; + + bool matched = false; + for (size_t i = 0; i < filter.second.size(); ++i) { + if (EndsWith(file_name, filter.second[i], false)) { + matched = true; + break;; + } } + + if (!matched && !filter.second.empty()) + file_name += ("." + filter.second[0]); } - *path = base::FilePath(file_name); + *path = base::FilePath(base::UTF8ToUTF16(file_name)); return true; }