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/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/memory/scoped_nsobject.h"
21 #include "base/strings/sys_string_conversions.h"
22 #include "base/threading/thread_restrictions.h"
23 #include "grit/ui_strings.h"
24 #import "ui/base/cocoa/nib_loading.h"
25 #include "ui/base/l10n/l10n_util_mac.h"
29 const int kFileTypePopupTag = 1234;
31 CFStringRef CreateUTIFromExtension(const base::FilePath::StringType& ext) {
32 base::mac::ScopedCFTypeRef<CFStringRef> ext_cf(
33 base::SysUTF8ToCFStringRef(ext));
34 return UTTypeCreatePreferredIdentifierForTag(
35 kUTTagClassFilenameExtension, ext_cf.get(), NULL);
40 class SelectFileDialogImpl;
42 // A bridge class to act as the modal delegate to the save/open sheet and send
43 // the results to the C++ class.
44 @interface SelectFileDialogBridge : NSObject<NSOpenSavePanelDelegate> {
46 SelectFileDialogImpl* selectFileDialogImpl_; // WEAK; owns us
49 - (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s;
50 - (void)endedPanel:(NSSavePanel*)panel
51 didCancel:(bool)did_cancel
52 type:(ui::SelectFileDialog::Type)type
53 parentWindow:(NSWindow*)parentWindow;
55 // NSSavePanel delegate method
56 - (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename;
60 // Implementation of SelectFileDialog that shows Cocoa dialogs for choosing a
62 class SelectFileDialogImpl : public ui::SelectFileDialog {
64 explicit SelectFileDialogImpl(Listener* listener,
65 ui::SelectFilePolicy* policy);
67 // BaseShellDialog implementation.
68 virtual bool IsRunning(gfx::NativeWindow parent_window) const OVERRIDE;
69 virtual void ListenerDestroyed() OVERRIDE;
71 // Callback from ObjC bridge.
72 void FileWasSelected(NSSavePanel* dialog,
73 NSWindow* parent_window,
76 const std::vector<base::FilePath>& files,
79 bool ShouldEnableFilename(NSSavePanel* dialog, NSString* filename);
82 // SelectFileDialog implementation.
83 // |params| is user data we pass back via the Listener interface.
84 virtual void SelectFileImpl(
86 const string16& title,
87 const base::FilePath& default_path,
88 const FileTypeInfo* file_types,
90 const base::FilePath::StringType& default_extension,
91 gfx::NativeWindow owning_window,
92 void* params) OVERRIDE;
95 virtual ~SelectFileDialogImpl();
97 // Gets the accessory view for the save dialog.
98 NSView* GetAccessoryView(const FileTypeInfo* file_types,
101 virtual bool HasMultipleFileTypeChoicesImpl() OVERRIDE;
103 // The bridge for results from Cocoa to return to us.
104 scoped_nsobject<SelectFileDialogBridge> bridge_;
106 // A map from file dialogs to the |params| user data associated with them.
107 std::map<NSSavePanel*, void*> params_map_;
109 // The set of all parent windows for which we are currently running dialogs.
110 std::set<NSWindow*> parents_;
112 // A map from file dialogs to their types.
113 std::map<NSSavePanel*, Type> type_map_;
115 bool hasMultipleFileTypeChoices_;
117 DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImpl);
120 SelectFileDialogImpl::SelectFileDialogImpl(Listener* listener,
121 ui::SelectFilePolicy* policy)
122 : SelectFileDialog(listener, policy),
123 bridge_([[SelectFileDialogBridge alloc]
124 initWithSelectFileDialogImpl:this]) {
127 bool SelectFileDialogImpl::IsRunning(gfx::NativeWindow parent_window) const {
128 return parents_.find(parent_window) != parents_.end();
131 void SelectFileDialogImpl::ListenerDestroyed() {
135 void SelectFileDialogImpl::FileWasSelected(
137 NSWindow* parent_window,
140 const std::vector<base::FilePath>& files,
142 void* params = params_map_[dialog];
143 params_map_.erase(dialog);
144 parents_.erase(parent_window);
145 type_map_.erase(dialog);
147 [dialog setDelegate:nil];
152 if (was_cancelled || files.empty()) {
153 listener_->FileSelectionCanceled(params);
156 listener_->MultiFilesSelected(files, params);
158 listener_->FileSelected(files[0], index, params);
163 bool SelectFileDialogImpl::ShouldEnableFilename(NSSavePanel* dialog,
164 NSString* filename) {
165 // If this is a single open file dialog, disable selecting packages.
166 if (type_map_[dialog] != SELECT_OPEN_FILE)
169 return ![[NSWorkspace sharedWorkspace] isFilePackageAtPath:filename];
172 void SelectFileDialogImpl::SelectFileImpl(
174 const string16& title,
175 const base::FilePath& default_path,
176 const FileTypeInfo* file_types,
178 const base::FilePath::StringType& default_extension,
179 gfx::NativeWindow owning_window,
181 DCHECK(type == SELECT_FOLDER ||
182 type == SELECT_OPEN_FILE ||
183 type == SELECT_OPEN_MULTI_FILE ||
184 type == SELECT_SAVEAS_FILE);
185 parents_.insert(owning_window);
187 // Note: we need to retain the dialog as owning_window can be null.
188 // (See http://crbug.com/29213 .)
190 if (type == SELECT_SAVEAS_FILE)
191 dialog = [[NSSavePanel savePanel] retain];
193 dialog = [[NSOpenPanel openPanel] retain];
196 [dialog setTitle:base::SysUTF16ToNSString(title)];
198 NSString* default_dir = nil;
199 NSString* default_filename = nil;
200 if (!default_path.empty()) {
201 // The file dialog is going to do a ton of stats anyway. Not much
202 // point in eliminating this one.
203 base::ThreadRestrictions::ScopedAllowIO allow_io;
204 if (file_util::DirectoryExists(default_path)) {
205 default_dir = base::SysUTF8ToNSString(default_path.value());
207 default_dir = base::SysUTF8ToNSString(default_path.DirName().value());
209 base::SysUTF8ToNSString(default_path.BaseName().value());
213 NSArray* allowed_file_types = nil;
215 if (!file_types->extensions.empty()) {
216 // While the example given in the header for FileTypeInfo lists an example
217 // |file_types->extensions| value as
218 // { { "htm", "html" }, { "txt" } }
219 // it is not always the case that the given extensions in one of the sub-
220 // lists are all synonyms. In fact, in the case of a <select> element with
221 // multiple "accept" types, all the extensions allowed for all the types
222 // will be part of one list. To be safe, allow the types of all the
223 // specified extensions.
224 NSMutableSet* file_type_set = [NSMutableSet set];
225 for (size_t i = 0; i < file_types->extensions.size(); ++i) {
226 const std::vector<base::FilePath::StringType>& ext_list =
227 file_types->extensions[i];
228 for (size_t j = 0; j < ext_list.size(); ++j) {
229 base::mac::ScopedCFTypeRef<CFStringRef> uti(
230 CreateUTIFromExtension(ext_list[j]));
231 [file_type_set addObject:base::mac::CFToNSCast(uti.get())];
233 // Always allow the extension itself, in case the UTI doesn't map
234 // back to the original extension correctly. This occurs with dynamic
235 // UTIs on 10.7 and 10.8.
236 // See http://crbug.com/148840, http://openradar.me/12316273
237 base::mac::ScopedCFTypeRef<CFStringRef> ext_cf(
238 base::SysUTF8ToCFStringRef(ext_list[j]));
239 [file_type_set addObject:base::mac::CFToNSCast(ext_cf.get())];
242 allowed_file_types = [file_type_set allObjects];
244 if (type == SELECT_SAVEAS_FILE)
245 [dialog setAllowedFileTypes:allowed_file_types];
246 // else we'll pass it in when we run the open panel
248 if (file_types->include_all_files || file_types->extensions.empty())
249 [dialog setAllowsOtherFileTypes:YES];
251 if (file_types->extension_description_overrides.size() > 1) {
252 NSView* accessory_view = GetAccessoryView(file_types, file_type_index);
253 [dialog setAccessoryView:accessory_view];
256 // If no type info is specified, anything goes.
257 [dialog setAllowsOtherFileTypes:YES];
259 hasMultipleFileTypeChoices_ =
260 file_types ? file_types->extensions.size() > 1 : true;
262 if (!default_extension.empty())
263 [dialog setAllowedFileTypes:@[base::SysUTF8ToNSString(default_extension)]];
265 params_map_[dialog] = params;
266 type_map_[dialog] = type;
268 if (type == SELECT_SAVEAS_FILE) {
269 [dialog setCanSelectHiddenExtension:YES];
271 NSOpenPanel* open_dialog = (NSOpenPanel*)dialog;
273 if (type == SELECT_OPEN_MULTI_FILE)
274 [open_dialog setAllowsMultipleSelection:YES];
276 [open_dialog setAllowsMultipleSelection:NO];
278 if (type == SELECT_FOLDER) {
279 [open_dialog setCanChooseFiles:NO];
280 [open_dialog setCanChooseDirectories:YES];
281 [open_dialog setCanCreateDirectories:YES];
282 NSString *prompt = l10n_util::GetNSString(IDS_SELECT_FOLDER_BUTTON_TITLE);
283 [open_dialog setPrompt:prompt];
285 [open_dialog setCanChooseFiles:YES];
286 [open_dialog setCanChooseDirectories:NO];
289 [open_dialog setDelegate:bridge_.get()];
290 [open_dialog setAllowedFileTypes:allowed_file_types];
293 [dialog setDirectoryURL:[NSURL fileURLWithPath:default_dir]];
294 if (default_filename)
295 [dialog setNameFieldStringValue:default_filename];
296 [dialog beginSheetModalForWindow:owning_window
297 completionHandler:^(NSInteger result) {
298 [bridge_.get() endedPanel:dialog
299 didCancel:result != NSFileHandlingPanelOKButton
301 parentWindow:owning_window];
305 SelectFileDialogImpl::~SelectFileDialogImpl() {
306 // Walk through the open dialogs and close them all. Use a temporary vector
307 // to hold the pointers, since we can't delete from the map as we're iterating
309 std::vector<NSSavePanel*> panels;
310 for (std::map<NSSavePanel*, void*>::iterator it = params_map_.begin();
311 it != params_map_.end(); ++it) {
312 panels.push_back(it->first);
315 for (std::vector<NSSavePanel*>::iterator it = panels.begin();
316 it != panels.end(); ++it) {
321 NSView* SelectFileDialogImpl::GetAccessoryView(const FileTypeInfo* file_types,
322 int file_type_index) {
324 NSView* accessory_view = ui::GetViewFromNib(@"SaveAccessoryView");
328 NSPopUpButton* popup = [accessory_view viewWithTag:kFileTypePopupTag];
331 size_t type_count = file_types->extensions.size();
332 for (size_t type = 0; type < type_count; ++type) {
333 NSString* type_description;
334 if (type < file_types->extension_description_overrides.size()) {
335 type_description = base::SysUTF16ToNSString(
336 file_types->extension_description_overrides[type]);
338 // No description given for a list of extensions; pick the first one from
339 // the list (arbitrarily) and use its description.
340 const std::vector<base::FilePath::StringType>& ext_list =
341 file_types->extensions[type];
342 DCHECK(!ext_list.empty());
343 base::mac::ScopedCFTypeRef<CFStringRef> uti(
344 CreateUTIFromExtension(ext_list[0]));
345 base::mac::ScopedCFTypeRef<CFStringRef> description(
346 UTTypeCopyDescription(uti.get()));
349 [[base::mac::CFToNSCast(description.get()) retain] autorelease];
351 [popup addItemWithTitle:type_description];
354 [popup selectItemAtIndex:file_type_index - 1]; // 1-based
355 return accessory_view;
358 bool SelectFileDialogImpl::HasMultipleFileTypeChoicesImpl() {
359 return hasMultipleFileTypeChoices_;
362 @implementation SelectFileDialogBridge
364 - (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s {
367 selectFileDialogImpl_ = s;
372 - (void)endedPanel:(NSSavePanel*)panel
373 didCancel:(bool)did_cancel
374 type:(ui::SelectFileDialog::Type)type
375 parentWindow:(NSWindow*)parentWindow {
377 std::vector<base::FilePath> paths;
379 if (type == ui::SelectFileDialog::SELECT_SAVEAS_FILE) {
380 if ([[panel URL] isFileURL]) {
381 paths.push_back(base::FilePath(
382 base::SysNSStringToUTF8([[panel URL] path])));
385 NSView* accessoryView = [panel accessoryView];
387 NSPopUpButton* popup = [accessoryView viewWithTag:kFileTypePopupTag];
389 // File type indexes are 1-based.
390 index = [popup indexOfSelectedItem] + 1;
396 CHECK([panel isKindOfClass:[NSOpenPanel class]]);
397 NSArray* urls = [static_cast<NSOpenPanel*>(panel) URLs];
398 for (NSURL* url in urls)
400 paths.push_back(base::FilePath(base::SysNSStringToUTF8([url path])));
404 bool isMulti = type == ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE;
405 selectFileDialogImpl_->FileWasSelected(panel,
414 - (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename {
415 return selectFileDialogImpl_->ShouldEnableFilename(sender, filename);
422 SelectFileDialog* CreateMacSelectFileDialog(
423 SelectFileDialog::Listener* listener,
424 SelectFilePolicy* policy) {
425 return new SelectFileDialogImpl(listener, policy);