1 // Copyright (c) 2011 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 "chrome/browser/ui/shell_dialogs.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/file_util.h" 15 #include "base/logging.h" 16 #import "base/mac/cocoa_protocols.h" 17 #include "base/mac/mac_util.h" 18 #include "base/mac/scoped_cftyperef.h" 19 #import "base/memory/scoped_nsobject.h" 20 #include "base/sys_string_conversions.h" 21 #include "base/threading/thread_restrictions.h" 22 #include "grit/generated_resources.h" 23 #include "ui/base/l10n/l10n_util_mac.h" 24 25 static const int kFileTypePopupTag = 1234; 26 27 class SelectFileDialogImpl; 28 29 // A bridge class to act as the modal delegate to the save/open sheet and send 30 // the results to the C++ class. 31 @interface SelectFileDialogBridge : NSObject<NSOpenSavePanelDelegate> { 32 @private 33 SelectFileDialogImpl* selectFileDialogImpl_; // WEAK; owns us 34 } 35 36 - (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s; 37 - (void)endedPanel:(NSSavePanel*)panel 38 withReturn:(int)returnCode 39 context:(void*)context; 40 41 // NSSavePanel delegate method 42 - (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename; 43 44 @end 45 46 // Implementation of SelectFileDialog that shows Cocoa dialogs for choosing a 47 // file or folder. 48 class SelectFileDialogImpl : public SelectFileDialog { 49 public: 50 explicit SelectFileDialogImpl(Listener* listener); 51 virtual ~SelectFileDialogImpl(); 52 53 // BaseShellDialog implementation. 54 virtual bool IsRunning(gfx::NativeWindow parent_window) const; 55 virtual void ListenerDestroyed(); 56 57 // Callback from ObjC bridge. 58 void FileWasSelected(NSSavePanel* dialog, 59 NSWindow* parent_window, 60 bool was_cancelled, 61 bool is_multi, 62 const std::vector<FilePath>& files, 63 int index); 64 65 bool ShouldEnableFilename(NSSavePanel* dialog, NSString* filename); 66 67 struct SheetContext { 68 Type type; 69 NSWindow* owning_window; 70 }; 71 72 protected: 73 // SelectFileDialog implementation. 74 // |params| is user data we pass back via the Listener interface. 75 virtual void SelectFileImpl(Type type, 76 const string16& title, 77 const FilePath& default_path, 78 const FileTypeInfo* file_types, 79 int file_type_index, 80 const FilePath::StringType& default_extension, 81 gfx::NativeWindow owning_window, 82 void* params); 83 84 private: 85 // Gets the accessory view for the save dialog. 86 NSView* GetAccessoryView(const FileTypeInfo* file_types, 87 int file_type_index); 88 89 // The bridge for results from Cocoa to return to us. 90 scoped_nsobject<SelectFileDialogBridge> bridge_; 91 92 // A map from file dialogs to the |params| user data associated with them. 93 std::map<NSSavePanel*, void*> params_map_; 94 95 // The set of all parent windows for which we are currently running dialogs. 96 std::set<NSWindow*> parents_; 97 98 // A map from file dialogs to their types. 99 std::map<NSSavePanel*, Type> type_map_; 100 101 DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImpl); 102 }; 103 104 // static 105 SelectFileDialog* SelectFileDialog::Create(Listener* listener) { 106 return new SelectFileDialogImpl(listener); 107 } 108 109 SelectFileDialogImpl::SelectFileDialogImpl(Listener* listener) 110 : SelectFileDialog(listener), 111 bridge_([[SelectFileDialogBridge alloc] 112 initWithSelectFileDialogImpl:this]) { 113 } 114 115 SelectFileDialogImpl::~SelectFileDialogImpl() { 116 // Walk through the open dialogs and close them all. Use a temporary vector 117 // to hold the pointers, since we can't delete from the map as we're iterating 118 // through it. 119 std::vector<NSSavePanel*> panels; 120 for (std::map<NSSavePanel*, void*>::iterator it = params_map_.begin(); 121 it != params_map_.end(); ++it) { 122 panels.push_back(it->first); 123 } 124 125 for (std::vector<NSSavePanel*>::iterator it = panels.begin(); 126 it != panels.end(); ++it) { 127 [*it cancel:*it]; 128 } 129 } 130 131 bool SelectFileDialogImpl::IsRunning(gfx::NativeWindow parent_window) const { 132 return parents_.find(parent_window) != parents_.end(); 133 } 134 135 void SelectFileDialogImpl::ListenerDestroyed() { 136 listener_ = NULL; 137 } 138 139 void SelectFileDialogImpl::SelectFileImpl( 140 Type type, 141 const string16& title, 142 const FilePath& default_path, 143 const FileTypeInfo* file_types, 144 int file_type_index, 145 const FilePath::StringType& default_extension, 146 gfx::NativeWindow owning_window, 147 void* params) { 148 DCHECK(type == SELECT_FOLDER || 149 type == SELECT_OPEN_FILE || 150 type == SELECT_OPEN_MULTI_FILE || 151 type == SELECT_SAVEAS_FILE); 152 parents_.insert(owning_window); 153 154 // Note: we need to retain the dialog as owning_window can be null. 155 // (see http://crbug.com/29213) 156 NSSavePanel* dialog; 157 if (type == SELECT_SAVEAS_FILE) 158 dialog = [[NSSavePanel savePanel] retain]; 159 else 160 dialog = [[NSOpenPanel openPanel] retain]; 161 162 if (!title.empty()) 163 [dialog setTitle:base::SysUTF16ToNSString(title)]; 164 165 NSString* default_dir = nil; 166 NSString* default_filename = nil; 167 if (!default_path.empty()) { 168 // The file dialog is going to do a ton of stats anyway. Not much 169 // point in eliminating this one. 170 base::ThreadRestrictions::ScopedAllowIO allow_io; 171 if (file_util::DirectoryExists(default_path)) { 172 default_dir = base::SysUTF8ToNSString(default_path.value()); 173 } else { 174 default_dir = base::SysUTF8ToNSString(default_path.DirName().value()); 175 default_filename = 176 base::SysUTF8ToNSString(default_path.BaseName().value()); 177 } 178 } 179 180 NSMutableArray* allowed_file_types = nil; 181 if (file_types) { 182 if (!file_types->extensions.empty()) { 183 allowed_file_types = [NSMutableArray array]; 184 for (size_t i=0; i < file_types->extensions.size(); ++i) { 185 const std::vector<FilePath::StringType>& ext_list = 186 file_types->extensions[i]; 187 for (size_t j=0; j < ext_list.size(); ++j) { 188 [allowed_file_types addObject:base::SysUTF8ToNSString(ext_list[j])]; 189 } 190 } 191 } 192 if (type == SELECT_SAVEAS_FILE) 193 [dialog setAllowedFileTypes:allowed_file_types]; 194 // else we'll pass it in when we run the open panel 195 196 if (file_types->include_all_files) 197 [dialog setAllowsOtherFileTypes:YES]; 198 199 if (!file_types->extension_description_overrides.empty()) { 200 NSView* accessory_view = GetAccessoryView(file_types, file_type_index); 201 [dialog setAccessoryView:accessory_view]; 202 } 203 } else { 204 // If no type info is specified, anything goes. 205 [dialog setAllowsOtherFileTypes:YES]; 206 } 207 208 if (!default_extension.empty()) 209 [dialog setRequiredFileType:base::SysUTF8ToNSString(default_extension)]; 210 211 params_map_[dialog] = params; 212 type_map_[dialog] = type; 213 214 SheetContext* context = new SheetContext; 215 216 // |context| should never be NULL, but we are seeing indications otherwise. 217 // This CHECK is here to confirm if we are actually getting NULL 218 // |context|s. http://crbug.com/58959 219 CHECK(context); 220 context->type = type; 221 context->owning_window = owning_window; 222 223 if (type == SELECT_SAVEAS_FILE) { 224 [dialog beginSheetForDirectory:default_dir 225 file:default_filename 226 modalForWindow:owning_window 227 modalDelegate:bridge_.get() 228 didEndSelector:@selector(endedPanel:withReturn:context:) 229 contextInfo:context]; 230 } else { 231 NSOpenPanel* open_dialog = (NSOpenPanel*)dialog; 232 233 if (type == SELECT_OPEN_MULTI_FILE) 234 [open_dialog setAllowsMultipleSelection:YES]; 235 else 236 [open_dialog setAllowsMultipleSelection:NO]; 237 238 if (type == SELECT_FOLDER) { 239 [open_dialog setCanChooseFiles:NO]; 240 [open_dialog setCanChooseDirectories:YES]; 241 [open_dialog setCanCreateDirectories:YES]; 242 NSString *prompt = l10n_util::GetNSString(IDS_SELECT_FOLDER_BUTTON_TITLE); 243 [open_dialog setPrompt:prompt]; 244 } else { 245 [open_dialog setCanChooseFiles:YES]; 246 [open_dialog setCanChooseDirectories:NO]; 247 } 248 249 [open_dialog setDelegate:bridge_.get()]; 250 [open_dialog beginSheetForDirectory:default_dir 251 file:default_filename 252 types:allowed_file_types 253 modalForWindow:owning_window 254 modalDelegate:bridge_.get() 255 didEndSelector:@selector(endedPanel:withReturn:context:) 256 contextInfo:context]; 257 } 258 } 259 260 void SelectFileDialogImpl::FileWasSelected(NSSavePanel* dialog, 261 NSWindow* parent_window, 262 bool was_cancelled, 263 bool is_multi, 264 const std::vector<FilePath>& files, 265 int index) { 266 void* params = params_map_[dialog]; 267 params_map_.erase(dialog); 268 parents_.erase(parent_window); 269 type_map_.erase(dialog); 270 271 if (!listener_) 272 return; 273 274 if (was_cancelled) { 275 listener_->FileSelectionCanceled(params); 276 } else { 277 if (is_multi) { 278 listener_->MultiFilesSelected(files, params); 279 } else { 280 listener_->FileSelected(files[0], index, params); 281 } 282 } 283 } 284 285 NSView* SelectFileDialogImpl::GetAccessoryView(const FileTypeInfo* file_types, 286 int file_type_index) { 287 DCHECK(file_types); 288 scoped_nsobject<NSNib> nib ( 289 [[NSNib alloc] initWithNibNamed:@"SaveAccessoryView" 290 bundle:base::mac::MainAppBundle()]); 291 if (!nib) 292 return nil; 293 294 NSArray* objects; 295 BOOL success = [nib instantiateNibWithOwner:nil 296 topLevelObjects:&objects]; 297 if (!success) 298 return nil; 299 [objects makeObjectsPerformSelector:@selector(release)]; 300 301 // This is a one-object nib, but IB insists on creating a second object, the 302 // NSApplication. I don't know why. 303 size_t view_index = 0; 304 while (view_index < [objects count] && 305 ![[objects objectAtIndex:view_index] isKindOfClass:[NSView class]]) 306 ++view_index; 307 DCHECK(view_index < [objects count]); 308 NSView* accessory_view = [objects objectAtIndex:view_index]; 309 310 NSPopUpButton* popup = [accessory_view viewWithTag:kFileTypePopupTag]; 311 DCHECK(popup); 312 313 size_t type_count = file_types->extensions.size(); 314 for (size_t type = 0; type<type_count; ++type) { 315 NSString* type_description; 316 if (type < file_types->extension_description_overrides.size()) { 317 type_description = base::SysUTF16ToNSString( 318 file_types->extension_description_overrides[type]); 319 } else { 320 const std::vector<FilePath::StringType>& ext_list = 321 file_types->extensions[type]; 322 DCHECK(!ext_list.empty()); 323 NSString* type_extension = base::SysUTF8ToNSString(ext_list[0]); 324 base::mac::ScopedCFTypeRef<CFStringRef> uti( 325 UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, 326 (CFStringRef)type_extension, 327 NULL)); 328 base::mac::ScopedCFTypeRef<CFStringRef> description( 329 UTTypeCopyDescription(uti.get())); 330 331 type_description = 332 [NSString stringWithString:(NSString*)description.get()]; 333 } 334 [popup addItemWithTitle:type_description]; 335 } 336 337 [popup selectItemAtIndex:file_type_index-1]; // 1-based 338 return accessory_view; 339 } 340 341 bool SelectFileDialogImpl::ShouldEnableFilename(NSSavePanel* dialog, 342 NSString* filename) { 343 // If this is a single open file dialog, disable selecting packages. 344 if (type_map_[dialog] != SELECT_OPEN_FILE) 345 return true; 346 347 return ![[NSWorkspace sharedWorkspace] isFilePackageAtPath:filename]; 348 } 349 350 @implementation SelectFileDialogBridge 351 352 - (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s { 353 self = [super init]; 354 if (self != nil) { 355 selectFileDialogImpl_ = s; 356 } 357 return self; 358 } 359 360 - (void)endedPanel:(NSSavePanel*)panel 361 withReturn:(int)returnCode 362 context:(void*)context { 363 // |context| should never be NULL, but we are seeing indications otherwise. 364 // |This CHECK is here to confirm if we are actually getting NULL 365 // ||context|s. http://crbug.com/58959 366 CHECK(context); 367 368 int index = 0; 369 SelectFileDialogImpl::SheetContext* context_struct = 370 (SelectFileDialogImpl::SheetContext*)context; 371 372 SelectFileDialog::Type type = context_struct->type; 373 NSWindow* parentWindow = context_struct->owning_window; 374 delete context_struct; 375 376 bool isMulti = type == SelectFileDialog::SELECT_OPEN_MULTI_FILE; 377 378 std::vector<FilePath> paths; 379 bool did_cancel = returnCode == NSCancelButton; 380 if (!did_cancel) { 381 if (type == SelectFileDialog::SELECT_SAVEAS_FILE) { 382 paths.push_back(FilePath(base::SysNSStringToUTF8([panel filename]))); 383 384 NSView* accessoryView = [panel accessoryView]; 385 if (accessoryView) { 386 NSPopUpButton* popup = [accessoryView viewWithTag:kFileTypePopupTag]; 387 if (popup) { 388 // File type indexes are 1-based. 389 index = [popup indexOfSelectedItem] + 1; 390 } 391 } else { 392 index = 1; 393 } 394 } else { 395 CHECK([panel isKindOfClass:[NSOpenPanel class]]); 396 NSArray* filenames = [static_cast<NSOpenPanel*>(panel) filenames]; 397 for (NSString* filename in filenames) 398 paths.push_back(FilePath(base::SysNSStringToUTF8(filename))); 399 } 400 } 401 402 selectFileDialogImpl_->FileWasSelected(panel, 403 parentWindow, 404 did_cancel, 405 isMulti, 406 paths, 407 index); 408 [panel release]; 409 } 410 411 - (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename { 412 return selectFileDialogImpl_->ShouldEnableFilename(sender, filename); 413 } 414 415 @end 416