1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ui/shell_dialogs/select_file_dialog.h"
7 #import <Cocoa/Cocoa.h>
8 #include <CoreServices/CoreServices.h>
14 #include "base/files/file_util.h"
15 #include "base/logging.h"
16 #include "base/mac/bundle_locations.h"
17 #include "base/mac/foundation_util.h"
18 #include "base/mac/mac_util.h"
19 #include "base/mac/scoped_cftyperef.h"
20 #import "base/mac/scoped_nsobject.h"
21 #include "base/strings/sys_string_conversions.h"
22 #include "base/threading/thread_restrictions.h"
23 #import "ui/base/cocoa/nib_loading.h"
24 #include "ui/base/l10n/l10n_util_mac.h"
25 #include "ui/strings/grit/ui_strings.h"
29 const int kFileTypePopupTag = 1234;
31 CFStringRef CreateUTIFromExtension(const base::FilePath::StringType& ext) {
32 base::ScopedCFTypeRef<CFStringRef> ext_cf(base::SysUTF8ToCFStringRef(ext));
33 return UTTypeCreatePreferredIdentifierForTag(
34 kUTTagClassFilenameExtension, ext_cf.get(), NULL);
39 class SelectFileDialogImpl;
41 // A bridge class to act as the modal delegate to the save/open sheet and send
42 // the results to the C++ class.
43 @interface SelectFileDialogBridge : NSObject<NSOpenSavePanelDelegate> {
45 SelectFileDialogImpl* selectFileDialogImpl_; // WEAK; owns us
48 - (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s;
49 - (void)endedPanel:(NSSavePanel*)panel
50 didCancel:(bool)did_cancel
51 type:(ui::SelectFileDialog::Type)type
52 parentWindow:(NSWindow*)parentWindow;
54 // NSSavePanel delegate method
55 - (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url;
59 // Implementation of SelectFileDialog that shows Cocoa dialogs for choosing a
61 class SelectFileDialogImpl : public ui::SelectFileDialog {
63 explicit SelectFileDialogImpl(Listener* listener,
64 ui::SelectFilePolicy* policy);
66 // BaseShellDialog implementation.
67 bool IsRunning(gfx::NativeWindow parent_window) const override;
68 void ListenerDestroyed() override;
70 // Callback from ObjC bridge.
71 void FileWasSelected(NSSavePanel* dialog,
72 NSWindow* parent_window,
75 const std::vector<base::FilePath>& files,
79 // SelectFileDialog implementation.
80 // |params| is user data we pass back via the Listener interface.
81 void SelectFileImpl(Type type,
82 const base::string16& title,
83 const base::FilePath& default_path,
84 const FileTypeInfo* file_types,
86 const base::FilePath::StringType& default_extension,
87 gfx::NativeWindow owning_window,
88 void* params) override;
91 ~SelectFileDialogImpl() override;
93 // Gets the accessory view for the save dialog.
94 NSView* GetAccessoryView(const FileTypeInfo* file_types,
97 bool HasMultipleFileTypeChoicesImpl() override;
99 // The bridge for results from Cocoa to return to us.
100 base::scoped_nsobject<SelectFileDialogBridge> bridge_;
102 // A map from file dialogs to the |params| user data associated with them.
103 std::map<NSSavePanel*, void*> params_map_;
105 // The set of all parent windows for which we are currently running dialogs.
106 std::set<NSWindow*> parents_;
108 // A map from file dialogs to their types.
109 std::map<NSSavePanel*, Type> type_map_;
111 bool hasMultipleFileTypeChoices_;
113 DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImpl);
116 SelectFileDialogImpl::SelectFileDialogImpl(Listener* listener,
117 ui::SelectFilePolicy* policy)
118 : SelectFileDialog(listener, policy),
119 bridge_([[SelectFileDialogBridge alloc]
120 initWithSelectFileDialogImpl:this]) {
123 bool SelectFileDialogImpl::IsRunning(gfx::NativeWindow parent_window) const {
124 return parents_.find(parent_window) != parents_.end();
127 void SelectFileDialogImpl::ListenerDestroyed() {
131 void SelectFileDialogImpl::FileWasSelected(
133 NSWindow* parent_window,
136 const std::vector<base::FilePath>& files,
138 void* params = params_map_[dialog];
139 params_map_.erase(dialog);
140 parents_.erase(parent_window);
141 type_map_.erase(dialog);
143 [dialog setDelegate:nil];
148 if (was_cancelled || files.empty()) {
149 listener_->FileSelectionCanceled(params);
152 listener_->MultiFilesSelected(files, params);
154 listener_->FileSelected(files[0], index, params);
159 void SelectFileDialogImpl::SelectFileImpl(
161 const base::string16& title,
162 const base::FilePath& default_path,
163 const FileTypeInfo* file_types,
165 const base::FilePath::StringType& default_extension,
166 gfx::NativeWindow owning_window,
168 DCHECK(type == SELECT_FOLDER ||
169 type == SELECT_UPLOAD_FOLDER ||
170 type == SELECT_OPEN_FILE ||
171 type == SELECT_OPEN_MULTI_FILE ||
172 type == SELECT_SAVEAS_FILE);
173 parents_.insert(owning_window);
175 // Note: we need to retain the dialog as owning_window can be null.
176 // (See http://crbug.com/29213 .)
178 if (type == SELECT_SAVEAS_FILE)
179 dialog = [[NSSavePanel savePanel] retain];
181 dialog = [[NSOpenPanel openPanel] retain];
184 [dialog setMessage:base::SysUTF16ToNSString(title)];
186 NSString* default_dir = nil;
187 NSString* default_filename = nil;
188 if (!default_path.empty()) {
189 // The file dialog is going to do a ton of stats anyway. Not much
190 // point in eliminating this one.
191 base::ThreadRestrictions::ScopedAllowIO allow_io;
192 if (base::DirectoryExists(default_path)) {
193 default_dir = base::SysUTF8ToNSString(default_path.value());
195 default_dir = base::SysUTF8ToNSString(default_path.DirName().value());
197 base::SysUTF8ToNSString(default_path.BaseName().value());
201 NSArray* allowed_file_types = nil;
203 if (!file_types->extensions.empty()) {
204 // While the example given in the header for FileTypeInfo lists an example
205 // |file_types->extensions| value as
206 // { { "htm", "html" }, { "txt" } }
207 // it is not always the case that the given extensions in one of the sub-
208 // lists are all synonyms. In fact, in the case of a <select> element with
209 // multiple "accept" types, all the extensions allowed for all the types
210 // will be part of one list. To be safe, allow the types of all the
211 // specified extensions.
212 NSMutableSet* file_type_set = [NSMutableSet set];
213 for (size_t i = 0; i < file_types->extensions.size(); ++i) {
214 const std::vector<base::FilePath::StringType>& ext_list =
215 file_types->extensions[i];
216 for (size_t j = 0; j < ext_list.size(); ++j) {
217 base::ScopedCFTypeRef<CFStringRef> uti(
218 CreateUTIFromExtension(ext_list[j]));
219 [file_type_set addObject:base::mac::CFToNSCast(uti.get())];
221 // Always allow the extension itself, in case the UTI doesn't map
222 // back to the original extension correctly. This occurs with dynamic
223 // UTIs on 10.7 and 10.8.
224 // See http://crbug.com/148840, http://openradar.me/12316273
225 base::ScopedCFTypeRef<CFStringRef> ext_cf(
226 base::SysUTF8ToCFStringRef(ext_list[j]));
227 [file_type_set addObject:base::mac::CFToNSCast(ext_cf.get())];
230 allowed_file_types = [file_type_set allObjects];
232 if (type == SELECT_SAVEAS_FILE)
233 [dialog setAllowedFileTypes:allowed_file_types];
234 // else we'll pass it in when we run the open panel
236 if (file_types->include_all_files || file_types->extensions.empty())
237 [dialog setAllowsOtherFileTypes:YES];
239 if (file_types->extension_description_overrides.size() > 1) {
240 NSView* accessory_view = GetAccessoryView(file_types, file_type_index);
241 [dialog setAccessoryView:accessory_view];
244 // If no type info is specified, anything goes.
245 [dialog setAllowsOtherFileTypes:YES];
247 hasMultipleFileTypeChoices_ =
248 file_types ? file_types->extensions.size() > 1 : true;
250 if (!default_extension.empty())
251 [dialog setAllowedFileTypes:@[base::SysUTF8ToNSString(default_extension)]];
253 params_map_[dialog] = params;
254 type_map_[dialog] = type;
256 if (type == SELECT_SAVEAS_FILE) {
257 // When file extensions are hidden and removing the extension from
258 // the default filename gives one which still has an extension
259 // that OS X recognizes, it will get confused and think the user
260 // is trying to override the default extension. This happens with
261 // filenames like "foo.tar.gz" or "ball.of.tar.png". Work around
262 // this by never hiding extensions in that case.
263 base::FilePath::StringType penultimate_extension =
264 default_path.RemoveFinalExtension().FinalExtension();
265 if (!penultimate_extension.empty() &&
266 penultimate_extension.length() <= 5U) {
267 [dialog setExtensionHidden:NO];
269 [dialog setCanSelectHiddenExtension:YES];
272 NSOpenPanel* open_dialog = (NSOpenPanel*)dialog;
274 if (type == SELECT_OPEN_MULTI_FILE)
275 [open_dialog setAllowsMultipleSelection:YES];
277 [open_dialog setAllowsMultipleSelection:NO];
279 if (type == SELECT_FOLDER || type == SELECT_UPLOAD_FOLDER) {
280 [open_dialog setCanChooseFiles:NO];
281 [open_dialog setCanChooseDirectories:YES];
282 [open_dialog setCanCreateDirectories:YES];
283 NSString *prompt = (type == SELECT_UPLOAD_FOLDER)
284 ? l10n_util::GetNSString(IDS_SELECT_UPLOAD_FOLDER_BUTTON_TITLE)
285 : l10n_util::GetNSString(IDS_SELECT_FOLDER_BUTTON_TITLE);
286 [open_dialog setPrompt:prompt];
288 [open_dialog setCanChooseFiles:YES];
289 [open_dialog setCanChooseDirectories:NO];
292 [open_dialog setDelegate:bridge_.get()];
293 [open_dialog setAllowedFileTypes:allowed_file_types];
296 [dialog setDirectoryURL:[NSURL fileURLWithPath:default_dir]];
297 if (default_filename)
298 [dialog setNameFieldStringValue:default_filename];
299 [dialog beginSheetModalForWindow:owning_window
300 completionHandler:^(NSInteger result) {
301 [bridge_.get() endedPanel:dialog
302 didCancel:result != NSFileHandlingPanelOKButton
304 parentWindow:owning_window];
308 SelectFileDialogImpl::~SelectFileDialogImpl() {
309 // Walk through the open dialogs and close them all. Use a temporary vector
310 // to hold the pointers, since we can't delete from the map as we're iterating
312 std::vector<NSSavePanel*> panels;
313 for (std::map<NSSavePanel*, void*>::iterator it = params_map_.begin();
314 it != params_map_.end(); ++it) {
315 panels.push_back(it->first);
318 for (std::vector<NSSavePanel*>::iterator it = panels.begin();
319 it != panels.end(); ++it) {
324 NSView* SelectFileDialogImpl::GetAccessoryView(const FileTypeInfo* file_types,
325 int file_type_index) {
327 NSView* accessory_view = ui::GetViewFromNib(@"SaveAccessoryView");
331 NSPopUpButton* popup = [accessory_view viewWithTag:kFileTypePopupTag];
334 size_t type_count = file_types->extensions.size();
335 for (size_t type = 0; type < type_count; ++type) {
336 NSString* type_description;
337 if (type < file_types->extension_description_overrides.size()) {
338 type_description = base::SysUTF16ToNSString(
339 file_types->extension_description_overrides[type]);
341 // No description given for a list of extensions; pick the first one from
342 // the list (arbitrarily) and use its description.
343 const std::vector<base::FilePath::StringType>& ext_list =
344 file_types->extensions[type];
345 DCHECK(!ext_list.empty());
346 base::ScopedCFTypeRef<CFStringRef> uti(
347 CreateUTIFromExtension(ext_list[0]));
348 base::ScopedCFTypeRef<CFStringRef> description(
349 UTTypeCopyDescription(uti.get()));
352 [[base::mac::CFToNSCast(description.get()) retain] autorelease];
354 [popup addItemWithTitle:type_description];
357 [popup selectItemAtIndex:file_type_index - 1]; // 1-based
358 return accessory_view;
361 bool SelectFileDialogImpl::HasMultipleFileTypeChoicesImpl() {
362 return hasMultipleFileTypeChoices_;
365 @implementation SelectFileDialogBridge
367 - (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s {
370 selectFileDialogImpl_ = s;
375 - (void)endedPanel:(NSSavePanel*)panel
376 didCancel:(bool)did_cancel
377 type:(ui::SelectFileDialog::Type)type
378 parentWindow:(NSWindow*)parentWindow {
380 std::vector<base::FilePath> paths;
382 if (type == ui::SelectFileDialog::SELECT_SAVEAS_FILE) {
383 if ([[panel URL] isFileURL]) {
384 paths.push_back(base::mac::NSStringToFilePath([[panel URL] path]));
387 NSView* accessoryView = [panel accessoryView];
389 NSPopUpButton* popup = [accessoryView viewWithTag:kFileTypePopupTag];
391 // File type indexes are 1-based.
392 index = [popup indexOfSelectedItem] + 1;
398 CHECK([panel isKindOfClass:[NSOpenPanel class]]);
399 NSArray* urls = [static_cast<NSOpenPanel*>(panel) URLs];
400 for (NSURL* url in urls)
402 paths.push_back(base::FilePath(base::SysNSStringToUTF8([url path])));
406 bool isMulti = type == ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE;
407 selectFileDialogImpl_->FileWasSelected(panel,
416 - (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url {
417 return [url isFileURL];
424 SelectFileDialog* CreateMacSelectFileDialog(
425 SelectFileDialog::Listener* listener,
426 SelectFilePolicy* policy) {
427 return new SelectFileDialogImpl(listener, policy);