| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/shell_dialogs/select_file_dialog.h" |
| |
| #import <Cocoa/Cocoa.h> |
| #include <CoreServices/CoreServices.h> |
| |
| #include <map> |
| #include <set> |
| #include <vector> |
| |
| #include "base/files/file_util.h" |
| #include "base/logging.h" |
| #include "base/mac/bundle_locations.h" |
| #include "base/mac/foundation_util.h" |
| #include "base/mac/scoped_cftyperef.h" |
| #import "base/mac/scoped_nsobject.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/threading/thread_restrictions.h" |
| #import "ui/base/cocoa/nib_loading.h" |
| #include "ui/base/l10n/l10n_util_mac.h" |
| #include "ui/strings/grit/ui_strings.h" |
| |
| namespace { |
| |
| const int kFileTypePopupTag = 1234; |
| |
| CFStringRef CreateUTIFromExtension(const base::FilePath::StringType& ext) { |
| base::ScopedCFTypeRef<CFStringRef> ext_cf(base::SysUTF8ToCFStringRef(ext)); |
| return UTTypeCreatePreferredIdentifierForTag( |
| kUTTagClassFilenameExtension, ext_cf.get(), NULL); |
| } |
| |
| } // namespace |
| |
| class SelectFileDialogImpl; |
| |
| // A bridge class to act as the modal delegate to the save/open sheet and send |
| // the results to the C++ class. |
| @interface SelectFileDialogBridge : NSObject<NSOpenSavePanelDelegate> { |
| @private |
| SelectFileDialogImpl* selectFileDialogImpl_; // WEAK; owns us |
| } |
| |
| - (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s; |
| - (void)endedPanel:(NSSavePanel*)panel |
| didCancel:(bool)did_cancel |
| type:(ui::SelectFileDialog::Type)type |
| parentWindow:(NSWindow*)parentWindow; |
| |
| // NSSavePanel delegate method |
| - (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url; |
| |
| @end |
| |
| // Implementation of SelectFileDialog that shows Cocoa dialogs for choosing a |
| // file or folder. |
| class SelectFileDialogImpl : public ui::SelectFileDialog { |
| public: |
| explicit SelectFileDialogImpl(Listener* listener, |
| ui::SelectFilePolicy* policy); |
| |
| // BaseShellDialog implementation. |
| bool IsRunning(gfx::NativeWindow parent_window) const override; |
| void ListenerDestroyed() override; |
| |
| // Callback from ObjC bridge. |
| void FileWasSelected(NSSavePanel* dialog, |
| NSWindow* parent_window, |
| bool was_cancelled, |
| bool is_multi, |
| const std::vector<base::FilePath>& files, |
| int index); |
| |
| protected: |
| // SelectFileDialog implementation. |
| // |params| is user data we pass back via the Listener interface. |
| void SelectFileImpl(Type type, |
| const base::string16& title, |
| const base::FilePath& default_path, |
| const FileTypeInfo* file_types, |
| int file_type_index, |
| const base::FilePath::StringType& default_extension, |
| gfx::NativeWindow owning_window, |
| void* params) override; |
| |
| private: |
| ~SelectFileDialogImpl() override; |
| |
| // Gets the accessory view for the save dialog. |
| NSView* GetAccessoryView(const FileTypeInfo* file_types, |
| int file_type_index); |
| |
| bool HasMultipleFileTypeChoicesImpl() override; |
| |
| // The bridge for results from Cocoa to return to us. |
| base::scoped_nsobject<SelectFileDialogBridge> bridge_; |
| |
| // A map from file dialogs to the |params| user data associated with them. |
| std::map<NSSavePanel*, void*> params_map_; |
| |
| // The set of all parent windows for which we are currently running dialogs. |
| std::set<NSWindow*> parents_; |
| |
| // A map from file dialogs to their types. |
| std::map<NSSavePanel*, Type> type_map_; |
| |
| bool hasMultipleFileTypeChoices_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImpl); |
| }; |
| |
| SelectFileDialogImpl::SelectFileDialogImpl(Listener* listener, |
| ui::SelectFilePolicy* policy) |
| : SelectFileDialog(listener, policy), |
| bridge_([[SelectFileDialogBridge alloc] |
| initWithSelectFileDialogImpl:this]) { |
| } |
| |
| bool SelectFileDialogImpl::IsRunning(gfx::NativeWindow parent_window) const { |
| return parents_.find(parent_window) != parents_.end(); |
| } |
| |
| void SelectFileDialogImpl::ListenerDestroyed() { |
| listener_ = NULL; |
| } |
| |
| void SelectFileDialogImpl::FileWasSelected( |
| NSSavePanel* dialog, |
| NSWindow* parent_window, |
| bool was_cancelled, |
| bool is_multi, |
| const std::vector<base::FilePath>& files, |
| int index) { |
| void* params = params_map_[dialog]; |
| params_map_.erase(dialog); |
| parents_.erase(parent_window); |
| type_map_.erase(dialog); |
| |
| [dialog setDelegate:nil]; |
| |
| if (!listener_) |
| return; |
| |
| if (was_cancelled || files.empty()) { |
| listener_->FileSelectionCanceled(params); |
| } else { |
| if (is_multi) { |
| listener_->MultiFilesSelected(files, params); |
| } else { |
| listener_->FileSelected(files[0], index, params); |
| } |
| } |
| } |
| |
| void SelectFileDialogImpl::SelectFileImpl( |
| Type type, |
| const base::string16& title, |
| const base::FilePath& default_path, |
| const FileTypeInfo* file_types, |
| int file_type_index, |
| const base::FilePath::StringType& default_extension, |
| gfx::NativeWindow owning_window, |
| void* params) { |
| DCHECK(type == SELECT_FOLDER || |
| type == SELECT_UPLOAD_FOLDER || |
| type == SELECT_OPEN_FILE || |
| type == SELECT_OPEN_MULTI_FILE || |
| type == SELECT_SAVEAS_FILE); |
| parents_.insert(owning_window); |
| |
| // Note: we need to retain the dialog as owning_window can be null. |
| // (See http://crbug.com/29213 .) |
| NSSavePanel* dialog; |
| if (type == SELECT_SAVEAS_FILE) |
| dialog = [[NSSavePanel savePanel] retain]; |
| else |
| dialog = [[NSOpenPanel openPanel] retain]; |
| |
| if (!title.empty()) |
| [dialog setMessage:base::SysUTF16ToNSString(title)]; |
| |
| NSString* default_dir = nil; |
| NSString* default_filename = nil; |
| if (!default_path.empty()) { |
| // The file dialog is going to do a ton of stats anyway. Not much |
| // point in eliminating this one. |
| base::ThreadRestrictions::ScopedAllowIO allow_io; |
| if (base::DirectoryExists(default_path)) { |
| default_dir = base::SysUTF8ToNSString(default_path.value()); |
| } else { |
| default_dir = base::SysUTF8ToNSString(default_path.DirName().value()); |
| default_filename = |
| base::SysUTF8ToNSString(default_path.BaseName().value()); |
| } |
| } |
| |
| NSArray* allowed_file_types = nil; |
| if (file_types) { |
| if (!file_types->extensions.empty()) { |
| // While the example given in the header for FileTypeInfo lists an example |
| // |file_types->extensions| value as |
| // { { "htm", "html" }, { "txt" } } |
| // it is not always the case that the given extensions in one of the sub- |
| // lists are all synonyms. In fact, in the case of a <select> element with |
| // multiple "accept" types, all the extensions allowed for all the types |
| // will be part of one list. To be safe, allow the types of all the |
| // specified extensions. |
| NSMutableSet* file_type_set = [NSMutableSet set]; |
| for (size_t i = 0; i < file_types->extensions.size(); ++i) { |
| const std::vector<base::FilePath::StringType>& ext_list = |
| file_types->extensions[i]; |
| for (size_t j = 0; j < ext_list.size(); ++j) { |
| base::ScopedCFTypeRef<CFStringRef> uti( |
| CreateUTIFromExtension(ext_list[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<CFStringRef> ext_cf( |
| base::SysUTF8ToCFStringRef(ext_list[j])); |
| [file_type_set addObject:base::mac::CFToNSCast(ext_cf.get())]; |
| } |
| } |
| allowed_file_types = [file_type_set allObjects]; |
| } |
| if (type == SELECT_SAVEAS_FILE) |
| [dialog setAllowedFileTypes:allowed_file_types]; |
| // else we'll pass it in when we run the open panel |
| |
| if (file_types->include_all_files || file_types->extensions.empty()) |
| [dialog setAllowsOtherFileTypes:YES]; |
| |
| if (file_types->extension_description_overrides.size() > 1) { |
| NSView* accessory_view = GetAccessoryView(file_types, file_type_index); |
| [dialog setAccessoryView:accessory_view]; |
| } |
| } else { |
| // If no type info is specified, anything goes. |
| [dialog setAllowsOtherFileTypes:YES]; |
| } |
| hasMultipleFileTypeChoices_ = |
| file_types ? file_types->extensions.size() > 1 : true; |
| |
| if (!default_extension.empty()) |
| [dialog setAllowedFileTypes:@[base::SysUTF8ToNSString(default_extension)]]; |
| |
| params_map_[dialog] = params; |
| type_map_[dialog] = type; |
| |
| if (type == SELECT_SAVEAS_FILE) { |
| // When file extensions are hidden and removing the extension from |
| // the default filename gives one which still has an extension |
| // that OS X recognizes, it will get confused and think the user |
| // is trying to override the default extension. This happens with |
| // filenames like "foo.tar.gz" or "ball.of.tar.png". Work around |
| // this by never hiding extensions in that case. |
| base::FilePath::StringType penultimate_extension = |
| default_path.RemoveFinalExtension().FinalExtension(); |
| if (!penultimate_extension.empty() && |
| penultimate_extension.length() <= 5U) { |
| [dialog setExtensionHidden:NO]; |
| } else { |
| [dialog setCanSelectHiddenExtension:YES]; |
| } |
| } else { |
| NSOpenPanel* open_dialog = (NSOpenPanel*)dialog; |
| |
| if (type == SELECT_OPEN_MULTI_FILE) |
| [open_dialog setAllowsMultipleSelection:YES]; |
| else |
| [open_dialog setAllowsMultipleSelection:NO]; |
| |
| if (type == SELECT_FOLDER || type == SELECT_UPLOAD_FOLDER) { |
| [open_dialog setCanChooseFiles:NO]; |
| [open_dialog setCanChooseDirectories:YES]; |
| [open_dialog setCanCreateDirectories:YES]; |
| NSString *prompt = (type == SELECT_UPLOAD_FOLDER) |
| ? l10n_util::GetNSString(IDS_SELECT_UPLOAD_FOLDER_BUTTON_TITLE) |
| : l10n_util::GetNSString(IDS_SELECT_FOLDER_BUTTON_TITLE); |
| [open_dialog setPrompt:prompt]; |
| } else { |
| [open_dialog setCanChooseFiles:YES]; |
| [open_dialog setCanChooseDirectories:NO]; |
| } |
| |
| [open_dialog setDelegate:bridge_.get()]; |
| [open_dialog setAllowedFileTypes:allowed_file_types]; |
| } |
| if (default_dir) |
| [dialog setDirectoryURL:[NSURL fileURLWithPath:default_dir]]; |
| if (default_filename) |
| [dialog setNameFieldStringValue:default_filename]; |
| [dialog beginSheetModalForWindow:owning_window |
| completionHandler:^(NSInteger result) { |
| [bridge_.get() endedPanel:dialog |
| didCancel:result != NSFileHandlingPanelOKButton |
| type:type |
| parentWindow:owning_window]; |
| }]; |
| } |
| |
| SelectFileDialogImpl::~SelectFileDialogImpl() { |
| // Walk through the open dialogs and close them all. Use a temporary vector |
| // to hold the pointers, since we can't delete from the map as we're iterating |
| // through it. |
| std::vector<NSSavePanel*> panels; |
| for (std::map<NSSavePanel*, void*>::iterator it = params_map_.begin(); |
| it != params_map_.end(); ++it) { |
| panels.push_back(it->first); |
| } |
| |
| for (std::vector<NSSavePanel*>::iterator it = panels.begin(); |
| it != panels.end(); ++it) { |
| [*it cancel:*it]; |
| } |
| } |
| |
| NSView* SelectFileDialogImpl::GetAccessoryView(const FileTypeInfo* file_types, |
| int file_type_index) { |
| DCHECK(file_types); |
| NSView* accessory_view = ui::GetViewFromNib(@"SaveAccessoryView"); |
| if (!accessory_view) |
| return nil; |
| |
| NSPopUpButton* popup = [accessory_view viewWithTag:kFileTypePopupTag]; |
| DCHECK(popup); |
| |
| size_t type_count = file_types->extensions.size(); |
| for (size_t type = 0; type < type_count; ++type) { |
| NSString* type_description; |
| if (type < file_types->extension_description_overrides.size()) { |
| type_description = base::SysUTF16ToNSString( |
| file_types->extension_description_overrides[type]); |
| } else { |
| // No description given for a list of extensions; pick the first one from |
| // the list (arbitrarily) and use its description. |
| const std::vector<base::FilePath::StringType>& ext_list = |
| file_types->extensions[type]; |
| DCHECK(!ext_list.empty()); |
| base::ScopedCFTypeRef<CFStringRef> uti( |
| CreateUTIFromExtension(ext_list[0])); |
| base::ScopedCFTypeRef<CFStringRef> description( |
| UTTypeCopyDescription(uti.get())); |
| |
| type_description = |
| [[base::mac::CFToNSCast(description.get()) retain] autorelease]; |
| } |
| [popup addItemWithTitle:type_description]; |
| } |
| |
| [popup selectItemAtIndex:file_type_index - 1]; // 1-based |
| return accessory_view; |
| } |
| |
| bool SelectFileDialogImpl::HasMultipleFileTypeChoicesImpl() { |
| return hasMultipleFileTypeChoices_; |
| } |
| |
| @implementation SelectFileDialogBridge |
| |
| - (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s { |
| self = [super init]; |
| if (self != nil) { |
| selectFileDialogImpl_ = s; |
| } |
| return self; |
| } |
| |
| - (void)endedPanel:(NSSavePanel*)panel |
| didCancel:(bool)did_cancel |
| type:(ui::SelectFileDialog::Type)type |
| parentWindow:(NSWindow*)parentWindow { |
| int index = 0; |
| std::vector<base::FilePath> paths; |
| if (!did_cancel) { |
| if (type == ui::SelectFileDialog::SELECT_SAVEAS_FILE) { |
| if ([[panel URL] isFileURL]) { |
| paths.push_back(base::mac::NSStringToFilePath([[panel URL] path])); |
| } |
| |
| NSView* accessoryView = [panel accessoryView]; |
| if (accessoryView) { |
| NSPopUpButton* popup = [accessoryView viewWithTag:kFileTypePopupTag]; |
| if (popup) { |
| // File type indexes are 1-based. |
| index = [popup indexOfSelectedItem] + 1; |
| } |
| } else { |
| index = 1; |
| } |
| } else { |
| CHECK([panel isKindOfClass:[NSOpenPanel class]]); |
| NSArray* urls = [static_cast<NSOpenPanel*>(panel) URLs]; |
| for (NSURL* url in urls) |
| if ([url isFileURL]) |
| paths.push_back(base::FilePath(base::SysNSStringToUTF8([url path]))); |
| } |
| } |
| |
| bool isMulti = type == ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE; |
| selectFileDialogImpl_->FileWasSelected(panel, |
| parentWindow, |
| did_cancel, |
| isMulti, |
| paths, |
| index); |
| [panel release]; |
| } |
| |
| - (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url { |
| return [url isFileURL]; |
| } |
| |
| @end |
| |
| namespace ui { |
| |
| SelectFileDialog* CreateMacSelectFileDialog( |
| SelectFileDialog::Listener* listener, |
| SelectFilePolicy* policy) { |
| return new SelectFileDialogImpl(listener, policy); |
| } |
| |
| } // namespace ui |