Mailbox support for texture layers.
[chromium-blink-merge.git] / ui / base / dialogs / select_file_dialog_mac.mm
blob1f2efc80ee8670c3de030c86452d0eeb7ac07668
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/base/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/memory/scoped_nsobject.h"
21 #include "base/sys_string_conversions.h"
22 #include "base/threading/thread_restrictions.h"
23 #include "grit/ui_strings.h"
24 #import "ui/base/cocoa/nib_loading.h"
25 #include "ui/base/l10n/l10n_util_mac.h"
27 namespace {
29 const int kFileTypePopupTag = 1234;
31 CFStringRef CreateUTIFromExtension(const FilePath::StringType& ext) {
32   base::mac::ScopedCFTypeRef<CFStringRef> ext_cf(
33       base::SysUTF8ToCFStringRef(ext));
34   return UTTypeCreatePreferredIdentifierForTag(
35       kUTTagClassFilenameExtension, ext_cf.get(), NULL);
38 }  // namespace
40 class SelectFileDialogImpl;
42 // A bridge class to act as the modal delegate to the save/open sheet and send
43 // the results to the C++ class.
44 @interface SelectFileDialogBridge : NSObject<NSOpenSavePanelDelegate> {
45  @private
46   SelectFileDialogImpl* selectFileDialogImpl_;  // WEAK; owns us
49 - (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s;
50 - (void)endedPanel:(NSSavePanel*)panel
51          didCancel:(bool)did_cancel
52               type:(ui::SelectFileDialog::Type)type
53       parentWindow:(NSWindow*)parentWindow;
55 // NSSavePanel delegate method
56 - (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename;
58 @end
60 // Implementation of SelectFileDialog that shows Cocoa dialogs for choosing a
61 // file or folder.
62 class SelectFileDialogImpl : public ui::SelectFileDialog {
63  public:
64   explicit SelectFileDialogImpl(Listener* listener,
65                                 ui::SelectFilePolicy* policy);
67   // BaseShellDialog implementation.
68   virtual bool IsRunning(gfx::NativeWindow parent_window) const;
69   virtual void ListenerDestroyed();
71   // Callback from ObjC bridge.
72   void FileWasSelected(NSSavePanel* dialog,
73                        NSWindow* parent_window,
74                        bool was_cancelled,
75                        bool is_multi,
76                        const std::vector<FilePath>& files,
77                        int index);
79   bool ShouldEnableFilename(NSSavePanel* dialog, NSString* filename);
81  protected:
82   // SelectFileDialog implementation.
83   // |params| is user data we pass back via the Listener interface.
84   virtual void SelectFileImpl(Type type,
85                               const string16& title,
86                               const FilePath& default_path,
87                               const FileTypeInfo* file_types,
88                               int file_type_index,
89                               const 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();
102   // The bridge for results from Cocoa to return to us.
103   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(NSSavePanel* dialog,
135                                            NSWindow* parent_window,
136                                            bool was_cancelled,
137                                            bool is_multi,
138                                            const std::vector<FilePath>& files,
139                                            int index) {
140   void* params = params_map_[dialog];
141   params_map_.erase(dialog);
142   parents_.erase(parent_window);
143   type_map_.erase(dialog);
145   [dialog setDelegate:nil];
147   if (!listener_)
148     return;
150   if (was_cancelled || files.empty()) {
151     listener_->FileSelectionCanceled(params);
152   } else {
153     if (is_multi) {
154       listener_->MultiFilesSelected(files, params);
155     } else {
156       listener_->FileSelected(files[0], index, params);
157     }
158   }
161 bool SelectFileDialogImpl::ShouldEnableFilename(NSSavePanel* dialog,
162                                                 NSString* filename) {
163   // If this is a single open file dialog, disable selecting packages.
164   if (type_map_[dialog] != SELECT_OPEN_FILE)
165     return true;
167   return ![[NSWorkspace sharedWorkspace] isFilePackageAtPath:filename];
170 void SelectFileDialogImpl::SelectFileImpl(
171     Type type,
172     const string16& title,
173     const FilePath& default_path,
174     const FileTypeInfo* file_types,
175     int file_type_index,
176     const FilePath::StringType& default_extension,
177     gfx::NativeWindow owning_window,
178     void* params) {
179   DCHECK(type == SELECT_FOLDER ||
180          type == SELECT_OPEN_FILE ||
181          type == SELECT_OPEN_MULTI_FILE ||
182          type == SELECT_SAVEAS_FILE);
183   parents_.insert(owning_window);
185   // Note: we need to retain the dialog as owning_window can be null.
186   // (See http://crbug.com/29213 .)
187   NSSavePanel* dialog;
188   if (type == SELECT_SAVEAS_FILE)
189     dialog = [[NSSavePanel savePanel] retain];
190   else
191     dialog = [[NSOpenPanel openPanel] retain];
193   if (!title.empty())
194     [dialog setTitle:base::SysUTF16ToNSString(title)];
196   NSString* default_dir = nil;
197   NSString* default_filename = nil;
198   if (!default_path.empty()) {
199     // The file dialog is going to do a ton of stats anyway. Not much
200     // point in eliminating this one.
201     base::ThreadRestrictions::ScopedAllowIO allow_io;
202     if (file_util::DirectoryExists(default_path)) {
203       default_dir = base::SysUTF8ToNSString(default_path.value());
204     } else {
205       default_dir = base::SysUTF8ToNSString(default_path.DirName().value());
206       default_filename =
207           base::SysUTF8ToNSString(default_path.BaseName().value());
208     }
209   }
211   NSArray* allowed_file_types = nil;
212   if (file_types) {
213     if (!file_types->extensions.empty()) {
214       // While the example given in the header for FileTypeInfo lists an example
215       // |file_types->extensions| value as
216       //   { { "htm", "html" }, { "txt" } }
217       // it is not always the case that the given extensions in one of the sub-
218       // lists are all synonyms. In fact, in the case of a <select> element with
219       // multiple "accept" types, all the extensions allowed for all the types
220       // will be part of one list. To be safe, allow the types of all the
221       // specified extensions.
222       NSMutableSet* file_type_set = [NSMutableSet set];
223       for (size_t i = 0; i < file_types->extensions.size(); ++i) {
224         const std::vector<FilePath::StringType>& ext_list =
225             file_types->extensions[i];
226         for (size_t j = 0; j < ext_list.size(); ++j) {
227           base::mac::ScopedCFTypeRef<CFStringRef> uti(
228               CreateUTIFromExtension(ext_list[j]));
229           [file_type_set addObject:base::mac::CFToNSCast(uti.get())];
231           // Always allow the extension itself, in case the UTI doesn't map
232           // back to the original extension correctly. This occurs with dynamic
233           // UTIs on 10.7 and 10.8.
234           // See http://crbug.com/148840, http://openradar.me/12316273
235           base::mac::ScopedCFTypeRef<CFStringRef> ext_cf(
236               base::SysUTF8ToCFStringRef(ext_list[j]));
237           [file_type_set addObject:base::mac::CFToNSCast(ext_cf.get())];
238         }
239       }
240       allowed_file_types = [file_type_set allObjects];
241     }
242     if (type == SELECT_SAVEAS_FILE)
243       [dialog setAllowedFileTypes:allowed_file_types];
244     // else we'll pass it in when we run the open panel
246     if (file_types->include_all_files || file_types->extensions.empty())
247       [dialog setAllowsOtherFileTypes:YES];
249     if (file_types->extension_description_overrides.size() > 1) {
250       NSView* accessory_view = GetAccessoryView(file_types, file_type_index);
251       [dialog setAccessoryView:accessory_view];
252     }
253   } else {
254     // If no type info is specified, anything goes.
255     [dialog setAllowsOtherFileTypes:YES];
256   }
257   hasMultipleFileTypeChoices_ =
258       file_types ? file_types->extensions.size() > 1 : true;
260   if (!default_extension.empty())
261     [dialog setAllowedFileTypes:@[base::SysUTF8ToNSString(default_extension)]];
263   params_map_[dialog] = params;
264   type_map_[dialog] = type;
266   if (type == SELECT_SAVEAS_FILE) {
267     [dialog setCanSelectHiddenExtension:YES];
268   } else {
269     NSOpenPanel* open_dialog = (NSOpenPanel*)dialog;
271     if (type == SELECT_OPEN_MULTI_FILE)
272       [open_dialog setAllowsMultipleSelection:YES];
273     else
274       [open_dialog setAllowsMultipleSelection:NO];
276     if (type == SELECT_FOLDER) {
277       [open_dialog setCanChooseFiles:NO];
278       [open_dialog setCanChooseDirectories:YES];
279       [open_dialog setCanCreateDirectories:YES];
280       NSString *prompt = l10n_util::GetNSString(IDS_SELECT_FOLDER_BUTTON_TITLE);
281       [open_dialog setPrompt:prompt];
282     } else {
283       [open_dialog setCanChooseFiles:YES];
284       [open_dialog setCanChooseDirectories:NO];
285     }
287     [open_dialog setDelegate:bridge_.get()];
288     [open_dialog setAllowedFileTypes:allowed_file_types];
289   }
290   if (default_dir)
291     [dialog setDirectoryURL:[NSURL fileURLWithPath:default_dir]];
292   if (default_filename)
293     [dialog setNameFieldStringValue:default_filename];
294   [dialog beginSheetModalForWindow:owning_window
295                  completionHandler:^(NSInteger result) {
296     [bridge_.get() endedPanel:dialog
297                     didCancel:result != NSFileHandlingPanelOKButton
298                          type:type
299                  parentWindow:owning_window];
300   }];
303 SelectFileDialogImpl::~SelectFileDialogImpl() {
304   // Walk through the open dialogs and close them all.  Use a temporary vector
305   // to hold the pointers, since we can't delete from the map as we're iterating
306   // through it.
307   std::vector<NSSavePanel*> panels;
308   for (std::map<NSSavePanel*, void*>::iterator it = params_map_.begin();
309        it != params_map_.end(); ++it) {
310     panels.push_back(it->first);
311   }
313   for (std::vector<NSSavePanel*>::iterator it = panels.begin();
314        it != panels.end(); ++it) {
315     [*it cancel:*it];
316   }
319 NSView* SelectFileDialogImpl::GetAccessoryView(const FileTypeInfo* file_types,
320                                                int file_type_index) {
321   DCHECK(file_types);
322   NSView* accessory_view = ui::GetViewFromNib(@"SaveAccessoryView");
323   if (!accessory_view)
324     return nil;
326   NSPopUpButton* popup = [accessory_view viewWithTag:kFileTypePopupTag];
327   DCHECK(popup);
329   size_t type_count = file_types->extensions.size();
330   for (size_t type = 0; type < type_count; ++type) {
331     NSString* type_description;
332     if (type < file_types->extension_description_overrides.size()) {
333       type_description = base::SysUTF16ToNSString(
334           file_types->extension_description_overrides[type]);
335     } else {
336       // No description given for a list of extensions; pick the first one from
337       // the list (arbitrarily) and use its description.
338       const std::vector<FilePath::StringType>& ext_list =
339           file_types->extensions[type];
340       DCHECK(!ext_list.empty());
341       base::mac::ScopedCFTypeRef<CFStringRef> uti(
342           CreateUTIFromExtension(ext_list[0]));
343       base::mac::ScopedCFTypeRef<CFStringRef> description(
344           UTTypeCopyDescription(uti.get()));
346       type_description =
347           [[base::mac::CFToNSCast(description.get()) retain] autorelease];
348     }
349     [popup addItemWithTitle:type_description];
350   }
352   [popup selectItemAtIndex:file_type_index - 1];  // 1-based
353   return accessory_view;
356 bool SelectFileDialogImpl::HasMultipleFileTypeChoicesImpl() {
357   return hasMultipleFileTypeChoices_;
360 @implementation SelectFileDialogBridge
362 - (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s {
363   self = [super init];
364   if (self != nil) {
365     selectFileDialogImpl_ = s;
366   }
367   return self;
370 - (void)endedPanel:(NSSavePanel*)panel
371          didCancel:(bool)did_cancel
372               type:(ui::SelectFileDialog::Type)type
373       parentWindow:(NSWindow*)parentWindow {
374   int index = 0;
375   std::vector<FilePath> paths;
376   if (!did_cancel) {
377     if (type == ui::SelectFileDialog::SELECT_SAVEAS_FILE) {
378       if ([[panel URL] isFileURL])
379         paths.push_back(FilePath(base::SysNSStringToUTF8([[panel URL] path])));
381       NSView* accessoryView = [panel accessoryView];
382       if (accessoryView) {
383         NSPopUpButton* popup = [accessoryView viewWithTag:kFileTypePopupTag];
384         if (popup) {
385           // File type indexes are 1-based.
386           index = [popup indexOfSelectedItem] + 1;
387         }
388       } else {
389         index = 1;
390       }
391     } else {
392       CHECK([panel isKindOfClass:[NSOpenPanel class]]);
393       NSArray* urls = [static_cast<NSOpenPanel*>(panel) URLs];
394       for (NSURL* url in urls)
395         if ([url isFileURL])
396           paths.push_back(FilePath(base::SysNSStringToUTF8([url path])));
397     }
398   }
400   bool isMulti = type == ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE;
401   selectFileDialogImpl_->FileWasSelected(panel,
402                                          parentWindow,
403                                          did_cancel,
404                                          isMulti,
405                                          paths,
406                                          index);
407   [panel release];
410 - (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename {
411   return selectFileDialogImpl_->ShouldEnableFilename(sender, filename);
414 @end
416 namespace ui {
418 SelectFileDialog* CreateMacSelectFileDialog(
419     SelectFileDialog::Listener* listener,
420     SelectFilePolicy* policy) {
421   return new SelectFileDialogImpl(listener, policy);
424 }  // namespace ui