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