Add ICU message format support
[chromium-blink-merge.git] / ui / shell_dialogs / select_file_dialog_mac.mm
blob9a4a5fd386792122f0c7d6a3ad1832d97f5f26a9
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/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/scoped_cftyperef.h"
19 #import "base/mac/scoped_nsobject.h"
20 #include "base/strings/sys_string_conversions.h"
21 #include "base/threading/thread_restrictions.h"
22 #import "ui/base/cocoa/nib_loading.h"
23 #include "ui/base/l10n/l10n_util_mac.h"
24 #include "ui/strings/grit/ui_strings.h"
26 namespace {
28 const int kFileTypePopupTag = 1234;
30 CFStringRef CreateUTIFromExtension(const base::FilePath::StringType& ext) {
31   base::ScopedCFTypeRef<CFStringRef> ext_cf(base::SysUTF8ToCFStringRef(ext));
32   return UTTypeCreatePreferredIdentifierForTag(
33       kUTTagClassFilenameExtension, ext_cf.get(), NULL);
36 }  // namespace
38 class SelectFileDialogImpl;
40 // A bridge class to act as the modal delegate to the save/open sheet and send
41 // the results to the C++ class.
42 @interface SelectFileDialogBridge : NSObject<NSOpenSavePanelDelegate> {
43  @private
44   SelectFileDialogImpl* selectFileDialogImpl_;  // WEAK; owns us
47 - (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s;
48 - (void)endedPanel:(NSSavePanel*)panel
49          didCancel:(bool)did_cancel
50               type:(ui::SelectFileDialog::Type)type
51       parentWindow:(NSWindow*)parentWindow;
53 // NSSavePanel delegate method
54 - (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url;
56 @end
58 // Implementation of SelectFileDialog that shows Cocoa dialogs for choosing a
59 // file or folder.
60 class SelectFileDialogImpl : public ui::SelectFileDialog {
61  public:
62   explicit SelectFileDialogImpl(Listener* listener,
63                                 ui::SelectFilePolicy* policy);
65   // BaseShellDialog implementation.
66   bool IsRunning(gfx::NativeWindow parent_window) const override;
67   void ListenerDestroyed() override;
69   // Callback from ObjC bridge.
70   void FileWasSelected(NSSavePanel* dialog,
71                        NSWindow* parent_window,
72                        bool was_cancelled,
73                        bool is_multi,
74                        const std::vector<base::FilePath>& files,
75                        int index);
77  protected:
78   // SelectFileDialog implementation.
79   // |params| is user data we pass back via the Listener interface.
80   void SelectFileImpl(Type type,
81                       const base::string16& title,
82                       const base::FilePath& default_path,
83                       const FileTypeInfo* file_types,
84                       int file_type_index,
85                       const base::FilePath::StringType& default_extension,
86                       gfx::NativeWindow owning_window,
87                       void* params) override;
89  private:
90   ~SelectFileDialogImpl() override;
92   // Gets the accessory view for the save dialog.
93   NSView* GetAccessoryView(const FileTypeInfo* file_types,
94                            int file_type_index);
96   bool HasMultipleFileTypeChoicesImpl() override;
98   // The bridge for results from Cocoa to return to us.
99   base::scoped_nsobject<SelectFileDialogBridge> bridge_;
101   // A map from file dialogs to the |params| user data associated with them.
102   std::map<NSSavePanel*, void*> params_map_;
104   // The set of all parent windows for which we are currently running dialogs.
105   std::set<NSWindow*> parents_;
107   // A map from file dialogs to their types.
108   std::map<NSSavePanel*, Type> type_map_;
110   bool hasMultipleFileTypeChoices_;
112   DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImpl);
115 SelectFileDialogImpl::SelectFileDialogImpl(Listener* listener,
116                                            ui::SelectFilePolicy* policy)
117     : SelectFileDialog(listener, policy),
118       bridge_([[SelectFileDialogBridge alloc]
119                initWithSelectFileDialogImpl:this]) {
122 bool SelectFileDialogImpl::IsRunning(gfx::NativeWindow parent_window) const {
123   return parents_.find(parent_window) != parents_.end();
126 void SelectFileDialogImpl::ListenerDestroyed() {
127   listener_ = NULL;
130 void SelectFileDialogImpl::FileWasSelected(
131     NSSavePanel* dialog,
132     NSWindow* parent_window,
133     bool was_cancelled,
134     bool is_multi,
135     const std::vector<base::FilePath>& files,
136     int index) {
137   void* params = params_map_[dialog];
138   params_map_.erase(dialog);
139   parents_.erase(parent_window);
140   type_map_.erase(dialog);
142   [dialog setDelegate:nil];
144   if (!listener_)
145     return;
147   if (was_cancelled || files.empty()) {
148     listener_->FileSelectionCanceled(params);
149   } else {
150     if (is_multi) {
151       listener_->MultiFilesSelected(files, params);
152     } else {
153       listener_->FileSelected(files[0], index, params);
154     }
155   }
158 void SelectFileDialogImpl::SelectFileImpl(
159     Type type,
160     const base::string16& title,
161     const base::FilePath& default_path,
162     const FileTypeInfo* file_types,
163     int file_type_index,
164     const base::FilePath::StringType& default_extension,
165     gfx::NativeWindow owning_window,
166     void* params) {
167   DCHECK(type == SELECT_FOLDER ||
168          type == SELECT_UPLOAD_FOLDER ||
169          type == SELECT_OPEN_FILE ||
170          type == SELECT_OPEN_MULTI_FILE ||
171          type == SELECT_SAVEAS_FILE);
172   parents_.insert(owning_window);
174   // Note: we need to retain the dialog as owning_window can be null.
175   // (See http://crbug.com/29213 .)
176   NSSavePanel* dialog;
177   if (type == SELECT_SAVEAS_FILE)
178     dialog = [[NSSavePanel savePanel] retain];
179   else
180     dialog = [[NSOpenPanel openPanel] retain];
182   if (!title.empty())
183     [dialog setMessage:base::SysUTF16ToNSString(title)];
185   NSString* default_dir = nil;
186   NSString* default_filename = nil;
187   if (!default_path.empty()) {
188     // The file dialog is going to do a ton of stats anyway. Not much
189     // point in eliminating this one.
190     base::ThreadRestrictions::ScopedAllowIO allow_io;
191     if (base::DirectoryExists(default_path)) {
192       default_dir = base::SysUTF8ToNSString(default_path.value());
193     } else {
194       default_dir = base::SysUTF8ToNSString(default_path.DirName().value());
195       default_filename =
196           base::SysUTF8ToNSString(default_path.BaseName().value());
197     }
198   }
200   NSArray* allowed_file_types = nil;
201   if (file_types) {
202     if (!file_types->extensions.empty()) {
203       // While the example given in the header for FileTypeInfo lists an example
204       // |file_types->extensions| value as
205       //   { { "htm", "html" }, { "txt" } }
206       // it is not always the case that the given extensions in one of the sub-
207       // lists are all synonyms. In fact, in the case of a <select> element with
208       // multiple "accept" types, all the extensions allowed for all the types
209       // will be part of one list. To be safe, allow the types of all the
210       // specified extensions.
211       NSMutableSet* file_type_set = [NSMutableSet set];
212       for (size_t i = 0; i < file_types->extensions.size(); ++i) {
213         const std::vector<base::FilePath::StringType>& ext_list =
214             file_types->extensions[i];
215         for (size_t j = 0; j < ext_list.size(); ++j) {
216           base::ScopedCFTypeRef<CFStringRef> uti(
217               CreateUTIFromExtension(ext_list[j]));
218           [file_type_set addObject:base::mac::CFToNSCast(uti.get())];
220           // Always allow the extension itself, in case the UTI doesn't map
221           // back to the original extension correctly. This occurs with dynamic
222           // UTIs on 10.7 and 10.8.
223           // See http://crbug.com/148840, http://openradar.me/12316273
224           base::ScopedCFTypeRef<CFStringRef> ext_cf(
225               base::SysUTF8ToCFStringRef(ext_list[j]));
226           [file_type_set addObject:base::mac::CFToNSCast(ext_cf.get())];
227         }
228       }
229       allowed_file_types = [file_type_set allObjects];
230     }
231     if (type == SELECT_SAVEAS_FILE)
232       [dialog setAllowedFileTypes:allowed_file_types];
233     // else we'll pass it in when we run the open panel
235     if (file_types->include_all_files || file_types->extensions.empty())
236       [dialog setAllowsOtherFileTypes:YES];
238     if (file_types->extension_description_overrides.size() > 1) {
239       NSView* accessory_view = GetAccessoryView(file_types, file_type_index);
240       [dialog setAccessoryView:accessory_view];
241     }
242   } else {
243     // If no type info is specified, anything goes.
244     [dialog setAllowsOtherFileTypes:YES];
245   }
246   hasMultipleFileTypeChoices_ =
247       file_types ? file_types->extensions.size() > 1 : true;
249   if (!default_extension.empty())
250     [dialog setAllowedFileTypes:@[base::SysUTF8ToNSString(default_extension)]];
252   params_map_[dialog] = params;
253   type_map_[dialog] = type;
255   if (type == SELECT_SAVEAS_FILE) {
256     // When file extensions are hidden and removing the extension from
257     // the default filename gives one which still has an extension
258     // that OS X recognizes, it will get confused and think the user
259     // is trying to override the default extension. This happens with
260     // filenames like "foo.tar.gz" or "ball.of.tar.png". Work around
261     // this by never hiding extensions in that case.
262     base::FilePath::StringType penultimate_extension =
263         default_path.RemoveFinalExtension().FinalExtension();
264     if (!penultimate_extension.empty() &&
265         penultimate_extension.length() <= 5U) {
266       [dialog setExtensionHidden:NO];
267     } else {
268       [dialog setCanSelectHiddenExtension:YES];
269     }
270   } else {
271     NSOpenPanel* open_dialog = (NSOpenPanel*)dialog;
273     if (type == SELECT_OPEN_MULTI_FILE)
274       [open_dialog setAllowsMultipleSelection:YES];
275     else
276       [open_dialog setAllowsMultipleSelection:NO];
278     if (type == SELECT_FOLDER || type == SELECT_UPLOAD_FOLDER) {
279       [open_dialog setCanChooseFiles:NO];
280       [open_dialog setCanChooseDirectories:YES];
281       [open_dialog setCanCreateDirectories:YES];
282       NSString *prompt = (type == SELECT_UPLOAD_FOLDER)
283           ? l10n_util::GetNSString(IDS_SELECT_UPLOAD_FOLDER_BUTTON_TITLE)
284           : l10n_util::GetNSString(IDS_SELECT_FOLDER_BUTTON_TITLE);
285       [open_dialog setPrompt:prompt];
286     } else {
287       [open_dialog setCanChooseFiles:YES];
288       [open_dialog setCanChooseDirectories:NO];
289     }
291     [open_dialog setDelegate:bridge_.get()];
292     [open_dialog setAllowedFileTypes:allowed_file_types];
293   }
294   if (default_dir)
295     [dialog setDirectoryURL:[NSURL fileURLWithPath:default_dir]];
296   if (default_filename)
297     [dialog setNameFieldStringValue:default_filename];
298   [dialog beginSheetModalForWindow:owning_window
299                  completionHandler:^(NSInteger result) {
300     [bridge_.get() endedPanel:dialog
301                     didCancel:result != NSFileHandlingPanelOKButton
302                          type:type
303                  parentWindow:owning_window];
304   }];
307 SelectFileDialogImpl::~SelectFileDialogImpl() {
308   // Walk through the open dialogs and close them all.  Use a temporary vector
309   // to hold the pointers, since we can't delete from the map as we're iterating
310   // through it.
311   std::vector<NSSavePanel*> panels;
312   for (std::map<NSSavePanel*, void*>::iterator it = params_map_.begin();
313        it != params_map_.end(); ++it) {
314     panels.push_back(it->first);
315   }
317   for (std::vector<NSSavePanel*>::iterator it = panels.begin();
318        it != panels.end(); ++it) {
319     [*it cancel:*it];
320   }
323 NSView* SelectFileDialogImpl::GetAccessoryView(const FileTypeInfo* file_types,
324                                                int file_type_index) {
325   DCHECK(file_types);
326   NSView* accessory_view = ui::GetViewFromNib(@"SaveAccessoryView");
327   if (!accessory_view)
328     return nil;
330   NSPopUpButton* popup = [accessory_view viewWithTag:kFileTypePopupTag];
331   DCHECK(popup);
333   size_t type_count = file_types->extensions.size();
334   for (size_t type = 0; type < type_count; ++type) {
335     NSString* type_description;
336     if (type < file_types->extension_description_overrides.size()) {
337       type_description = base::SysUTF16ToNSString(
338           file_types->extension_description_overrides[type]);
339     } else {
340       // No description given for a list of extensions; pick the first one from
341       // the list (arbitrarily) and use its description.
342       const std::vector<base::FilePath::StringType>& ext_list =
343           file_types->extensions[type];
344       DCHECK(!ext_list.empty());
345       base::ScopedCFTypeRef<CFStringRef> uti(
346           CreateUTIFromExtension(ext_list[0]));
347       base::ScopedCFTypeRef<CFStringRef> description(
348           UTTypeCopyDescription(uti.get()));
350       type_description =
351           [[base::mac::CFToNSCast(description.get()) retain] autorelease];
352     }
353     [popup addItemWithTitle:type_description];
354   }
356   [popup selectItemAtIndex:file_type_index - 1];  // 1-based
357   return accessory_view;
360 bool SelectFileDialogImpl::HasMultipleFileTypeChoicesImpl() {
361   return hasMultipleFileTypeChoices_;
364 @implementation SelectFileDialogBridge
366 - (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s {
367   self = [super init];
368   if (self != nil) {
369     selectFileDialogImpl_ = s;
370   }
371   return self;
374 - (void)endedPanel:(NSSavePanel*)panel
375          didCancel:(bool)did_cancel
376               type:(ui::SelectFileDialog::Type)type
377       parentWindow:(NSWindow*)parentWindow {
378   int index = 0;
379   std::vector<base::FilePath> paths;
380   if (!did_cancel) {
381     if (type == ui::SelectFileDialog::SELECT_SAVEAS_FILE) {
382       if ([[panel URL] isFileURL]) {
383         paths.push_back(base::mac::NSStringToFilePath([[panel URL] path]));
384       }
386       NSView* accessoryView = [panel accessoryView];
387       if (accessoryView) {
388         NSPopUpButton* popup = [accessoryView viewWithTag:kFileTypePopupTag];
389         if (popup) {
390           // File type indexes are 1-based.
391           index = [popup indexOfSelectedItem] + 1;
392         }
393       } else {
394         index = 1;
395       }
396     } else {
397       CHECK([panel isKindOfClass:[NSOpenPanel class]]);
398       NSArray* urls = [static_cast<NSOpenPanel*>(panel) URLs];
399       for (NSURL* url in urls)
400         if ([url isFileURL])
401           paths.push_back(base::FilePath(base::SysNSStringToUTF8([url path])));
402     }
403   }
405   bool isMulti = type == ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE;
406   selectFileDialogImpl_->FileWasSelected(panel,
407                                          parentWindow,
408                                          did_cancel,
409                                          isMulti,
410                                          paths,
411                                          index);
412   [panel release];
415 - (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url {
416   return [url isFileURL];
419 @end
421 namespace ui {
423 SelectFileDialog* CreateMacSelectFileDialog(
424     SelectFileDialog::Listener* listener,
425     SelectFilePolicy* policy) {
426   return new SelectFileDialogImpl(listener, policy);
429 }  // namespace ui