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