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. 4 5 #include "ui/shell_dialogs/select_file_dialog.h" 6 7 #import <Cocoa/Cocoa.h> 8 #include <CoreServices/CoreServices.h> 9 10 #include <map> 11 #include <set> 12 #include <vector> 13 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" 26 27 namespace { 28 29 const int kFileTypePopupTag = 1234; 30 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); 35 } 36 37 } // namespace 38 39 class SelectFileDialogImpl; 40 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> { 44 @private 45 SelectFileDialogImpl* selectFileDialogImpl_; // WEAK; owns us 46 } 47 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; 53 54 // NSSavePanel delegate method 55 - (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url; 56 57 @end 58 59 // Implementation of SelectFileDialog that shows Cocoa dialogs for choosing a 60 // file or folder. 61 class SelectFileDialogImpl : public ui::SelectFileDialog { 62 public: 63 explicit SelectFileDialogImpl(Listener* listener, 64 ui::SelectFilePolicy* policy); 65 66 // BaseShellDialog implementation. 67 virtual bool IsRunning(gfx::NativeWindow parent_window) const OVERRIDE; 68 virtual void ListenerDestroyed() OVERRIDE; 69 70 // Callback from ObjC bridge. 71 void FileWasSelected(NSSavePanel* dialog, 72 NSWindow* parent_window, 73 bool was_cancelled, 74 bool is_multi, 75 const std::vector<base::FilePath>& files, 76 int index); 77 78 bool ShouldEnableFilename(NSSavePanel* dialog, NSString* filename); 79 80 protected: 81 // SelectFileDialog implementation. 82 // |params| is user data we pass back via the Listener interface. 83 virtual void SelectFileImpl( 84 Type type, 85 const base::string16& title, 86 const base::FilePath& default_path, 87 const FileTypeInfo* file_types, 88 int file_type_index, 89 const base::FilePath::StringType& default_extension, 90 gfx::NativeWindow owning_window, 91 void* params) OVERRIDE; 92 93 private: 94 virtual ~SelectFileDialogImpl(); 95 96 // Gets the accessory view for the save dialog. 97 NSView* GetAccessoryView(const FileTypeInfo* file_types, 98 int file_type_index); 99 100 virtual bool HasMultipleFileTypeChoicesImpl() OVERRIDE; 101 102 // The bridge for results from Cocoa to return to us. 103 base::scoped_nsobject<SelectFileDialogBridge> bridge_; 104 105 // A map from file dialogs to the |params| user data associated with them. 106 std::map<NSSavePanel*, void*> params_map_; 107 108 // The set of all parent windows for which we are currently running dialogs. 109 std::set<NSWindow*> parents_; 110 111 // A map from file dialogs to their types. 112 std::map<NSSavePanel*, Type> type_map_; 113 114 bool hasMultipleFileTypeChoices_; 115 116 DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImpl); 117 }; 118 119 SelectFileDialogImpl::SelectFileDialogImpl(Listener* listener, 120 ui::SelectFilePolicy* policy) 121 : SelectFileDialog(listener, policy), 122 bridge_([[SelectFileDialogBridge alloc] 123 initWithSelectFileDialogImpl:this]) { 124 } 125 126 bool SelectFileDialogImpl::IsRunning(gfx::NativeWindow parent_window) const { 127 return parents_.find(parent_window) != parents_.end(); 128 } 129 130 void SelectFileDialogImpl::ListenerDestroyed() { 131 listener_ = NULL; 132 } 133 134 void SelectFileDialogImpl::FileWasSelected( 135 NSSavePanel* dialog, 136 NSWindow* parent_window, 137 bool was_cancelled, 138 bool is_multi, 139 const std::vector<base::FilePath>& files, 140 int index) { 141 void* params = params_map_[dialog]; 142 params_map_.erase(dialog); 143 parents_.erase(parent_window); 144 type_map_.erase(dialog); 145 146 [dialog setDelegate:nil]; 147 148 if (!listener_) 149 return; 150 151 if (was_cancelled || files.empty()) { 152 listener_->FileSelectionCanceled(params); 153 } else { 154 if (is_multi) { 155 listener_->MultiFilesSelected(files, params); 156 } else { 157 listener_->FileSelected(files[0], index, params); 158 } 159 } 160 } 161 162 bool SelectFileDialogImpl::ShouldEnableFilename(NSSavePanel* dialog, 163 NSString* filename) { 164 // If this is a single/multiple open file dialog, disable selecting packages. 165 if (type_map_[dialog] != SELECT_OPEN_FILE && 166 type_map_[dialog] != SELECT_OPEN_MULTI_FILE) 167 return true; 168 169 return ![[NSWorkspace sharedWorkspace] isFilePackageAtPath:filename]; 170 } 171 172 void SelectFileDialogImpl::SelectFileImpl( 173 Type type, 174 const base::string16& title, 175 const base::FilePath& default_path, 176 const FileTypeInfo* file_types, 177 int file_type_index, 178 const base::FilePath::StringType& default_extension, 179 gfx::NativeWindow owning_window, 180 void* params) { 181 DCHECK(type == SELECT_FOLDER || 182 type == SELECT_UPLOAD_FOLDER || 183 type == SELECT_OPEN_FILE || 184 type == SELECT_OPEN_MULTI_FILE || 185 type == SELECT_SAVEAS_FILE); 186 parents_.insert(owning_window); 187 188 // Note: we need to retain the dialog as owning_window can be null. 189 // (See http://crbug.com/29213 .) 190 NSSavePanel* dialog; 191 if (type == SELECT_SAVEAS_FILE) 192 dialog = [[NSSavePanel savePanel] retain]; 193 else 194 dialog = [[NSOpenPanel openPanel] retain]; 195 196 if (!title.empty()) 197 [dialog setMessage:base::SysUTF16ToNSString(title)]; 198 199 NSString* default_dir = nil; 200 NSString* default_filename = nil; 201 if (!default_path.empty()) { 202 // The file dialog is going to do a ton of stats anyway. Not much 203 // point in eliminating this one. 204 base::ThreadRestrictions::ScopedAllowIO allow_io; 205 if (base::DirectoryExists(default_path)) { 206 default_dir = base::SysUTF8ToNSString(default_path.value()); 207 } else { 208 default_dir = base::SysUTF8ToNSString(default_path.DirName().value()); 209 default_filename = 210 base::SysUTF8ToNSString(default_path.BaseName().value()); 211 } 212 } 213 214 NSArray* allowed_file_types = nil; 215 if (file_types) { 216 if (!file_types->extensions.empty()) { 217 // While the example given in the header for FileTypeInfo lists an example 218 // |file_types->extensions| value as 219 // { { "htm", "html" }, { "txt" } } 220 // it is not always the case that the given extensions in one of the sub- 221 // lists are all synonyms. In fact, in the case of a <select> element with 222 // multiple "accept" types, all the extensions allowed for all the types 223 // will be part of one list. To be safe, allow the types of all the 224 // specified extensions. 225 NSMutableSet* file_type_set = [NSMutableSet set]; 226 for (size_t i = 0; i < file_types->extensions.size(); ++i) { 227 const std::vector<base::FilePath::StringType>& ext_list = 228 file_types->extensions[i]; 229 for (size_t j = 0; j < ext_list.size(); ++j) { 230 base::ScopedCFTypeRef<CFStringRef> uti( 231 CreateUTIFromExtension(ext_list[j])); 232 [file_type_set addObject:base::mac::CFToNSCast(uti.get())]; 233 234 // Always allow the extension itself, in case the UTI doesn't map 235 // back to the original extension correctly. This occurs with dynamic 236 // UTIs on 10.7 and 10.8. 237 // See http://crbug.com/148840, http://openradar.me/12316273 238 base::ScopedCFTypeRef<CFStringRef> ext_cf( 239 base::SysUTF8ToCFStringRef(ext_list[j])); 240 [file_type_set addObject:base::mac::CFToNSCast(ext_cf.get())]; 241 } 242 } 243 allowed_file_types = [file_type_set allObjects]; 244 } 245 if (type == SELECT_SAVEAS_FILE) 246 [dialog setAllowedFileTypes:allowed_file_types]; 247 // else we'll pass it in when we run the open panel 248 249 if (file_types->include_all_files || file_types->extensions.empty()) 250 [dialog setAllowsOtherFileTypes:YES]; 251 252 if (file_types->extension_description_overrides.size() > 1) { 253 NSView* accessory_view = GetAccessoryView(file_types, file_type_index); 254 [dialog setAccessoryView:accessory_view]; 255 } 256 } else { 257 // If no type info is specified, anything goes. 258 [dialog setAllowsOtherFileTypes:YES]; 259 } 260 hasMultipleFileTypeChoices_ = 261 file_types ? file_types->extensions.size() > 1 : true; 262 263 if (!default_extension.empty()) 264 [dialog setAllowedFileTypes:@[base::SysUTF8ToNSString(default_extension)]]; 265 266 params_map_[dialog] = params; 267 type_map_[dialog] = type; 268 269 if (type == SELECT_SAVEAS_FILE) { 270 // When file extensions are hidden and removing the extension from 271 // the default filename gives one which still has an extension 272 // that OS X recognizes, it will get confused and think the user 273 // is trying to override the default extension. This happens with 274 // filenames like "foo.tar.gz" or "ball.of.tar.png". Work around 275 // this by never hiding extensions in that case. 276 base::FilePath::StringType penultimate_extension = 277 default_path.RemoveFinalExtension().FinalExtension(); 278 if (!penultimate_extension.empty() && 279 penultimate_extension.length() <= 5U) { 280 [dialog setExtensionHidden:NO]; 281 } else { 282 [dialog setCanSelectHiddenExtension:YES]; 283 } 284 } else { 285 NSOpenPanel* open_dialog = (NSOpenPanel*)dialog; 286 287 if (type == SELECT_OPEN_MULTI_FILE) 288 [open_dialog setAllowsMultipleSelection:YES]; 289 else 290 [open_dialog setAllowsMultipleSelection:NO]; 291 292 if (type == SELECT_FOLDER || type == SELECT_UPLOAD_FOLDER) { 293 [open_dialog setCanChooseFiles:NO]; 294 [open_dialog setCanChooseDirectories:YES]; 295 [open_dialog setCanCreateDirectories:YES]; 296 NSString *prompt = (type == SELECT_UPLOAD_FOLDER) 297 ? l10n_util::GetNSString(IDS_SELECT_UPLOAD_FOLDER_BUTTON_TITLE) 298 : l10n_util::GetNSString(IDS_SELECT_FOLDER_BUTTON_TITLE); 299 [open_dialog setPrompt:prompt]; 300 } else { 301 [open_dialog setCanChooseFiles:YES]; 302 [open_dialog setCanChooseDirectories:NO]; 303 } 304 305 [open_dialog setDelegate:bridge_.get()]; 306 [open_dialog setAllowedFileTypes:allowed_file_types]; 307 } 308 if (default_dir) 309 [dialog setDirectoryURL:[NSURL fileURLWithPath:default_dir]]; 310 if (default_filename) 311 [dialog setNameFieldStringValue:default_filename]; 312 [dialog beginSheetModalForWindow:owning_window 313 completionHandler:^(NSInteger result) { 314 [bridge_.get() endedPanel:dialog 315 didCancel:result != NSFileHandlingPanelOKButton 316 type:type 317 parentWindow:owning_window]; 318 }]; 319 } 320 321 SelectFileDialogImpl::~SelectFileDialogImpl() { 322 // Walk through the open dialogs and close them all. Use a temporary vector 323 // to hold the pointers, since we can't delete from the map as we're iterating 324 // through it. 325 std::vector<NSSavePanel*> panels; 326 for (std::map<NSSavePanel*, void*>::iterator it = params_map_.begin(); 327 it != params_map_.end(); ++it) { 328 panels.push_back(it->first); 329 } 330 331 for (std::vector<NSSavePanel*>::iterator it = panels.begin(); 332 it != panels.end(); ++it) { 333 [*it cancel:*it]; 334 } 335 } 336 337 NSView* SelectFileDialogImpl::GetAccessoryView(const FileTypeInfo* file_types, 338 int file_type_index) { 339 DCHECK(file_types); 340 NSView* accessory_view = ui::GetViewFromNib(@"SaveAccessoryView"); 341 if (!accessory_view) 342 return nil; 343 344 NSPopUpButton* popup = [accessory_view viewWithTag:kFileTypePopupTag]; 345 DCHECK(popup); 346 347 size_t type_count = file_types->extensions.size(); 348 for (size_t type = 0; type < type_count; ++type) { 349 NSString* type_description; 350 if (type < file_types->extension_description_overrides.size()) { 351 type_description = base::SysUTF16ToNSString( 352 file_types->extension_description_overrides[type]); 353 } else { 354 // No description given for a list of extensions; pick the first one from 355 // the list (arbitrarily) and use its description. 356 const std::vector<base::FilePath::StringType>& ext_list = 357 file_types->extensions[type]; 358 DCHECK(!ext_list.empty()); 359 base::ScopedCFTypeRef<CFStringRef> uti( 360 CreateUTIFromExtension(ext_list[0])); 361 base::ScopedCFTypeRef<CFStringRef> description( 362 UTTypeCopyDescription(uti.get())); 363 364 type_description = 365 [[base::mac::CFToNSCast(description.get()) retain] autorelease]; 366 } 367 [popup addItemWithTitle:type_description]; 368 } 369 370 [popup selectItemAtIndex:file_type_index - 1]; // 1-based 371 return accessory_view; 372 } 373 374 bool SelectFileDialogImpl::HasMultipleFileTypeChoicesImpl() { 375 return hasMultipleFileTypeChoices_; 376 } 377 378 @implementation SelectFileDialogBridge 379 380 - (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s { 381 self = [super init]; 382 if (self != nil) { 383 selectFileDialogImpl_ = s; 384 } 385 return self; 386 } 387 388 - (void)endedPanel:(NSSavePanel*)panel 389 didCancel:(bool)did_cancel 390 type:(ui::SelectFileDialog::Type)type 391 parentWindow:(NSWindow*)parentWindow { 392 int index = 0; 393 std::vector<base::FilePath> paths; 394 if (!did_cancel) { 395 if (type == ui::SelectFileDialog::SELECT_SAVEAS_FILE) { 396 if ([[panel URL] isFileURL]) { 397 paths.push_back(base::mac::NSStringToFilePath([[panel URL] path])); 398 } 399 400 NSView* accessoryView = [panel accessoryView]; 401 if (accessoryView) { 402 NSPopUpButton* popup = [accessoryView viewWithTag:kFileTypePopupTag]; 403 if (popup) { 404 // File type indexes are 1-based. 405 index = [popup indexOfSelectedItem] + 1; 406 } 407 } else { 408 index = 1; 409 } 410 } else { 411 CHECK([panel isKindOfClass:[NSOpenPanel class]]); 412 NSArray* urls = [static_cast<NSOpenPanel*>(panel) URLs]; 413 for (NSURL* url in urls) 414 if ([url isFileURL]) 415 paths.push_back(base::FilePath(base::SysNSStringToUTF8([url path]))); 416 } 417 } 418 419 bool isMulti = type == ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE; 420 selectFileDialogImpl_->FileWasSelected(panel, 421 parentWindow, 422 did_cancel, 423 isMulti, 424 paths, 425 index); 426 [panel release]; 427 } 428 429 - (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url { 430 if (![url isFileURL]) 431 return NO; 432 return selectFileDialogImpl_->ShouldEnableFilename(sender, [url path]); 433 } 434 435 @end 436 437 namespace ui { 438 439 SelectFileDialog* CreateMacSelectFileDialog( 440 SelectFileDialog::Listener* listener, 441 SelectFilePolicy* policy) { 442 return new SelectFileDialogImpl(listener, policy); 443 } 444 445 } // namespace ui 446