• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/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#include "grit/ui_strings.h"
24#import "ui/base/cocoa/nib_loading.h"
25#include "ui/base/l10n/l10n_util_mac.h"
26
27namespace {
28
29const int kFileTypePopupTag = 1234;
30
31CFStringRef 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
39class 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 shouldShowFilename:(NSString *)filename;
56
57@end
58
59// Implementation of SelectFileDialog that shows Cocoa dialogs for choosing a
60// file or folder.
61class SelectFileDialogImpl : public ui::SelectFileDialog {
62 public:
63  explicit SelectFileDialogImpl(Listener* listener,
64                                ui::SelectFilePolicy* policy);
65
66  // BaseShellDialog implementation.
67  virtual bool IsRunning(gfx::NativeWindow parent_window) const OVERRIDE;
68  virtual 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  bool ShouldEnableFilename(NSSavePanel* dialog, NSString* filename);
79
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;
92
93 private:
94  virtual ~SelectFileDialogImpl();
95
96  // Gets the accessory view for the save dialog.
97  NSView* GetAccessoryView(const FileTypeInfo* file_types,
98                           int file_type_index);
99
100  virtual bool HasMultipleFileTypeChoicesImpl() OVERRIDE;
101
102  // The bridge for results from Cocoa to return to us.
103  base::scoped_nsobject<SelectFileDialogBridge> bridge_;
104
105  // A map from file dialogs to the |params| user data associated with them.
106  std::map<NSSavePanel*, void*> params_map_;
107
108  // The set of all parent windows for which we are currently running dialogs.
109  std::set<NSWindow*> parents_;
110
111  // A map from file dialogs to their types.
112  std::map<NSSavePanel*, Type> type_map_;
113
114  bool hasMultipleFileTypeChoices_;
115
116  DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImpl);
117};
118
119SelectFileDialogImpl::SelectFileDialogImpl(Listener* listener,
120                                           ui::SelectFilePolicy* policy)
121    : SelectFileDialog(listener, policy),
122      bridge_([[SelectFileDialogBridge alloc]
123               initWithSelectFileDialogImpl:this]) {
124}
125
126bool SelectFileDialogImpl::IsRunning(gfx::NativeWindow parent_window) const {
127  return parents_.find(parent_window) != parents_.end();
128}
129
130void SelectFileDialogImpl::ListenerDestroyed() {
131  listener_ = NULL;
132}
133
134void 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);
145
146  [dialog setDelegate:nil];
147
148  if (!listener_)
149    return;
150
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  }
160}
161
162bool 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;
167
168  return ![[NSWorkspace sharedWorkspace] isFilePackageAtPath:filename];
169}
170
171void 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);
186
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];
194
195  if (!title.empty())
196    [dialog setMessage:base::SysUTF16ToNSString(title)];
197
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  }
212
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())];
232
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
247
248    if (file_types->include_all_files || file_types->extensions.empty())
249      [dialog setAllowsOtherFileTypes:YES];
250
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;
261
262  if (!default_extension.empty())
263    [dialog setAllowedFileTypes:@[base::SysUTF8ToNSString(default_extension)]];
264
265  params_map_[dialog] = params;
266  type_map_[dialog] = type;
267
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;
285
286    if (type == SELECT_OPEN_MULTI_FILE)
287      [open_dialog setAllowsMultipleSelection:YES];
288    else
289      [open_dialog setAllowsMultipleSelection:NO];
290
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    }
303
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  }];
318}
319
320SelectFileDialogImpl::~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  }
329
330  for (std::vector<NSSavePanel*>::iterator it = panels.begin();
331       it != panels.end(); ++it) {
332    [*it cancel:*it];
333  }
334}
335
336NSView* 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;
342
343  NSPopUpButton* popup = [accessory_view viewWithTag:kFileTypePopupTag];
344  DCHECK(popup);
345
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()));
362
363      type_description =
364          [[base::mac::CFToNSCast(description.get()) retain] autorelease];
365    }
366    [popup addItemWithTitle:type_description];
367  }
368
369  [popup selectItemAtIndex:file_type_index - 1];  // 1-based
370  return accessory_view;
371}
372
373bool SelectFileDialogImpl::HasMultipleFileTypeChoicesImpl() {
374  return hasMultipleFileTypeChoices_;
375}
376
377@implementation SelectFileDialogBridge
378
379- (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s {
380  self = [super init];
381  if (self != nil) {
382    selectFileDialogImpl_ = s;
383  }
384  return self;
385}
386
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      }
398
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  }
417
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];
426}
427
428- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename {
429  return selectFileDialogImpl_->ShouldEnableFilename(sender, filename);
430}
431
432@end
433
434namespace ui {
435
436SelectFileDialog* CreateMacSelectFileDialog(
437    SelectFileDialog::Listener* listener,
438    SelectFilePolicy* policy) {
439  return new SelectFileDialogImpl(listener, policy);
440}
441
442}  // namespace ui
443