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