Home | History | Annotate | Download | only in shell_dialogs
      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