• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2011 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 "chrome/browser/ui/shell_dialogs.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#import "base/mac/cocoa_protocols.h"
17#include "base/mac/mac_util.h"
18#include "base/mac/scoped_cftyperef.h"
19#import "base/memory/scoped_nsobject.h"
20#include "base/sys_string_conversions.h"
21#include "base/threading/thread_restrictions.h"
22#include "grit/generated_resources.h"
23#include "ui/base/l10n/l10n_util_mac.h"
24
25static const int kFileTypePopupTag = 1234;
26
27class SelectFileDialogImpl;
28
29// A bridge class to act as the modal delegate to the save/open sheet and send
30// the results to the C++ class.
31@interface SelectFileDialogBridge : NSObject<NSOpenSavePanelDelegate> {
32 @private
33  SelectFileDialogImpl* selectFileDialogImpl_;  // WEAK; owns us
34}
35
36- (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s;
37- (void)endedPanel:(NSSavePanel*)panel
38        withReturn:(int)returnCode
39           context:(void*)context;
40
41// NSSavePanel delegate method
42- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename;
43
44@end
45
46// Implementation of SelectFileDialog that shows Cocoa dialogs for choosing a
47// file or folder.
48class SelectFileDialogImpl : public SelectFileDialog {
49 public:
50  explicit SelectFileDialogImpl(Listener* listener);
51  virtual ~SelectFileDialogImpl();
52
53  // BaseShellDialog implementation.
54  virtual bool IsRunning(gfx::NativeWindow parent_window) const;
55  virtual void ListenerDestroyed();
56
57  // Callback from ObjC bridge.
58  void FileWasSelected(NSSavePanel* dialog,
59                       NSWindow* parent_window,
60                       bool was_cancelled,
61                       bool is_multi,
62                       const std::vector<FilePath>& files,
63                       int index);
64
65  bool ShouldEnableFilename(NSSavePanel* dialog, NSString* filename);
66
67  struct SheetContext {
68    Type type;
69    NSWindow* owning_window;
70  };
71
72 protected:
73  // SelectFileDialog implementation.
74  // |params| is user data we pass back via the Listener interface.
75  virtual void SelectFileImpl(Type type,
76                              const string16& title,
77                              const FilePath& default_path,
78                              const FileTypeInfo* file_types,
79                              int file_type_index,
80                              const FilePath::StringType& default_extension,
81                              gfx::NativeWindow owning_window,
82                              void* params);
83
84 private:
85  // Gets the accessory view for the save dialog.
86  NSView* GetAccessoryView(const FileTypeInfo* file_types,
87                           int file_type_index);
88
89  // The bridge for results from Cocoa to return to us.
90  scoped_nsobject<SelectFileDialogBridge> bridge_;
91
92  // A map from file dialogs to the |params| user data associated with them.
93  std::map<NSSavePanel*, void*> params_map_;
94
95  // The set of all parent windows for which we are currently running dialogs.
96  std::set<NSWindow*> parents_;
97
98  // A map from file dialogs to their types.
99  std::map<NSSavePanel*, Type> type_map_;
100
101  DISALLOW_COPY_AND_ASSIGN(SelectFileDialogImpl);
102};
103
104// static
105SelectFileDialog* SelectFileDialog::Create(Listener* listener) {
106  return new SelectFileDialogImpl(listener);
107}
108
109SelectFileDialogImpl::SelectFileDialogImpl(Listener* listener)
110    : SelectFileDialog(listener),
111      bridge_([[SelectFileDialogBridge alloc]
112               initWithSelectFileDialogImpl:this]) {
113}
114
115SelectFileDialogImpl::~SelectFileDialogImpl() {
116  // Walk through the open dialogs and close them all.  Use a temporary vector
117  // to hold the pointers, since we can't delete from the map as we're iterating
118  // through it.
119  std::vector<NSSavePanel*> panels;
120  for (std::map<NSSavePanel*, void*>::iterator it = params_map_.begin();
121       it != params_map_.end(); ++it) {
122    panels.push_back(it->first);
123  }
124
125  for (std::vector<NSSavePanel*>::iterator it = panels.begin();
126       it != panels.end(); ++it) {
127    [*it cancel:*it];
128  }
129}
130
131bool SelectFileDialogImpl::IsRunning(gfx::NativeWindow parent_window) const {
132  return parents_.find(parent_window) != parents_.end();
133}
134
135void SelectFileDialogImpl::ListenerDestroyed() {
136  listener_ = NULL;
137}
138
139void SelectFileDialogImpl::SelectFileImpl(
140    Type type,
141    const string16& title,
142    const FilePath& default_path,
143    const FileTypeInfo* file_types,
144    int file_type_index,
145    const FilePath::StringType& default_extension,
146    gfx::NativeWindow owning_window,
147    void* params) {
148  DCHECK(type == SELECT_FOLDER ||
149         type == SELECT_OPEN_FILE ||
150         type == SELECT_OPEN_MULTI_FILE ||
151         type == SELECT_SAVEAS_FILE);
152  parents_.insert(owning_window);
153
154  // Note: we need to retain the dialog as owning_window can be null.
155  // (see http://crbug.com/29213)
156  NSSavePanel* dialog;
157  if (type == SELECT_SAVEAS_FILE)
158    dialog = [[NSSavePanel savePanel] retain];
159  else
160    dialog = [[NSOpenPanel openPanel] retain];
161
162  if (!title.empty())
163    [dialog setTitle:base::SysUTF16ToNSString(title)];
164
165  NSString* default_dir = nil;
166  NSString* default_filename = nil;
167  if (!default_path.empty()) {
168    // The file dialog is going to do a ton of stats anyway. Not much
169    // point in eliminating this one.
170    base::ThreadRestrictions::ScopedAllowIO allow_io;
171    if (file_util::DirectoryExists(default_path)) {
172      default_dir = base::SysUTF8ToNSString(default_path.value());
173    } else {
174      default_dir = base::SysUTF8ToNSString(default_path.DirName().value());
175      default_filename =
176          base::SysUTF8ToNSString(default_path.BaseName().value());
177    }
178  }
179
180  NSMutableArray* allowed_file_types = nil;
181  if (file_types) {
182    if (!file_types->extensions.empty()) {
183      allowed_file_types = [NSMutableArray array];
184      for (size_t i=0; i < file_types->extensions.size(); ++i) {
185        const std::vector<FilePath::StringType>& ext_list =
186            file_types->extensions[i];
187        for (size_t j=0; j < ext_list.size(); ++j) {
188          [allowed_file_types addObject:base::SysUTF8ToNSString(ext_list[j])];
189        }
190      }
191    }
192    if (type == SELECT_SAVEAS_FILE)
193      [dialog setAllowedFileTypes:allowed_file_types];
194    // else we'll pass it in when we run the open panel
195
196    if (file_types->include_all_files)
197      [dialog setAllowsOtherFileTypes:YES];
198
199    if (!file_types->extension_description_overrides.empty()) {
200      NSView* accessory_view = GetAccessoryView(file_types, file_type_index);
201      [dialog setAccessoryView:accessory_view];
202    }
203  } else {
204    // If no type info is specified, anything goes.
205    [dialog setAllowsOtherFileTypes:YES];
206  }
207
208  if (!default_extension.empty())
209    [dialog setRequiredFileType:base::SysUTF8ToNSString(default_extension)];
210
211  params_map_[dialog] = params;
212  type_map_[dialog] = type;
213
214  SheetContext* context = new SheetContext;
215
216  // |context| should never be NULL, but we are seeing indications otherwise.
217  // This CHECK is here to confirm if we are actually getting NULL
218  // |context|s. http://crbug.com/58959
219  CHECK(context);
220  context->type = type;
221  context->owning_window = owning_window;
222
223  if (type == SELECT_SAVEAS_FILE) {
224    [dialog beginSheetForDirectory:default_dir
225                              file:default_filename
226                    modalForWindow:owning_window
227                     modalDelegate:bridge_.get()
228                    didEndSelector:@selector(endedPanel:withReturn:context:)
229                       contextInfo:context];
230  } else {
231    NSOpenPanel* open_dialog = (NSOpenPanel*)dialog;
232
233    if (type == SELECT_OPEN_MULTI_FILE)
234      [open_dialog setAllowsMultipleSelection:YES];
235    else
236      [open_dialog setAllowsMultipleSelection:NO];
237
238    if (type == SELECT_FOLDER) {
239      [open_dialog setCanChooseFiles:NO];
240      [open_dialog setCanChooseDirectories:YES];
241      [open_dialog setCanCreateDirectories:YES];
242      NSString *prompt = l10n_util::GetNSString(IDS_SELECT_FOLDER_BUTTON_TITLE);
243      [open_dialog setPrompt:prompt];
244    } else {
245      [open_dialog setCanChooseFiles:YES];
246      [open_dialog setCanChooseDirectories:NO];
247    }
248
249    [open_dialog setDelegate:bridge_.get()];
250    [open_dialog beginSheetForDirectory:default_dir
251                                   file:default_filename
252                                  types:allowed_file_types
253                         modalForWindow:owning_window
254                          modalDelegate:bridge_.get()
255                        didEndSelector:@selector(endedPanel:withReturn:context:)
256                            contextInfo:context];
257  }
258}
259
260void SelectFileDialogImpl::FileWasSelected(NSSavePanel* dialog,
261                                           NSWindow* parent_window,
262                                           bool was_cancelled,
263                                           bool is_multi,
264                                           const std::vector<FilePath>& files,
265                                           int index) {
266  void* params = params_map_[dialog];
267  params_map_.erase(dialog);
268  parents_.erase(parent_window);
269  type_map_.erase(dialog);
270
271  if (!listener_)
272    return;
273
274  if (was_cancelled) {
275    listener_->FileSelectionCanceled(params);
276  } else {
277    if (is_multi) {
278      listener_->MultiFilesSelected(files, params);
279    } else {
280      listener_->FileSelected(files[0], index, params);
281    }
282  }
283}
284
285NSView* SelectFileDialogImpl::GetAccessoryView(const FileTypeInfo* file_types,
286                                               int file_type_index) {
287  DCHECK(file_types);
288  scoped_nsobject<NSNib> nib (
289      [[NSNib alloc] initWithNibNamed:@"SaveAccessoryView"
290                               bundle:base::mac::MainAppBundle()]);
291  if (!nib)
292    return nil;
293
294  NSArray* objects;
295  BOOL success = [nib instantiateNibWithOwner:nil
296                              topLevelObjects:&objects];
297  if (!success)
298    return nil;
299  [objects makeObjectsPerformSelector:@selector(release)];
300
301  // This is a one-object nib, but IB insists on creating a second object, the
302  // NSApplication. I don't know why.
303  size_t view_index = 0;
304  while (view_index < [objects count] &&
305      ![[objects objectAtIndex:view_index] isKindOfClass:[NSView class]])
306    ++view_index;
307  DCHECK(view_index < [objects count]);
308  NSView* accessory_view = [objects objectAtIndex:view_index];
309
310  NSPopUpButton* popup = [accessory_view viewWithTag:kFileTypePopupTag];
311  DCHECK(popup);
312
313  size_t type_count = file_types->extensions.size();
314  for (size_t type = 0; type<type_count; ++type) {
315    NSString* type_description;
316    if (type < file_types->extension_description_overrides.size()) {
317      type_description = base::SysUTF16ToNSString(
318          file_types->extension_description_overrides[type]);
319    } else {
320      const std::vector<FilePath::StringType>& ext_list =
321          file_types->extensions[type];
322      DCHECK(!ext_list.empty());
323      NSString* type_extension = base::SysUTF8ToNSString(ext_list[0]);
324      base::mac::ScopedCFTypeRef<CFStringRef> uti(
325          UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension,
326                                                (CFStringRef)type_extension,
327                                                NULL));
328      base::mac::ScopedCFTypeRef<CFStringRef> description(
329          UTTypeCopyDescription(uti.get()));
330
331      type_description =
332          [NSString stringWithString:(NSString*)description.get()];
333    }
334    [popup addItemWithTitle:type_description];
335  }
336
337  [popup selectItemAtIndex:file_type_index-1];  // 1-based
338  return accessory_view;
339}
340
341bool SelectFileDialogImpl::ShouldEnableFilename(NSSavePanel* dialog,
342                                                NSString* filename) {
343  // If this is a single open file dialog, disable selecting packages.
344  if (type_map_[dialog] != SELECT_OPEN_FILE)
345    return true;
346
347  return ![[NSWorkspace sharedWorkspace] isFilePackageAtPath:filename];
348}
349
350@implementation SelectFileDialogBridge
351
352- (id)initWithSelectFileDialogImpl:(SelectFileDialogImpl*)s {
353  self = [super init];
354  if (self != nil) {
355    selectFileDialogImpl_ = s;
356  }
357  return self;
358}
359
360- (void)endedPanel:(NSSavePanel*)panel
361        withReturn:(int)returnCode
362           context:(void*)context {
363  // |context| should never be NULL, but we are seeing indications otherwise.
364  // |This CHECK is here to confirm if we are actually getting NULL
365  // ||context|s. http://crbug.com/58959
366  CHECK(context);
367
368  int index = 0;
369  SelectFileDialogImpl::SheetContext* context_struct =
370      (SelectFileDialogImpl::SheetContext*)context;
371
372  SelectFileDialog::Type type = context_struct->type;
373  NSWindow* parentWindow = context_struct->owning_window;
374  delete context_struct;
375
376  bool isMulti = type == SelectFileDialog::SELECT_OPEN_MULTI_FILE;
377
378  std::vector<FilePath> paths;
379  bool did_cancel = returnCode == NSCancelButton;
380  if (!did_cancel) {
381    if (type == SelectFileDialog::SELECT_SAVEAS_FILE) {
382      paths.push_back(FilePath(base::SysNSStringToUTF8([panel filename])));
383
384      NSView* accessoryView = [panel accessoryView];
385      if (accessoryView) {
386        NSPopUpButton* popup = [accessoryView viewWithTag:kFileTypePopupTag];
387        if (popup) {
388          // File type indexes are 1-based.
389          index = [popup indexOfSelectedItem] + 1;
390        }
391      } else {
392        index = 1;
393      }
394    } else {
395      CHECK([panel isKindOfClass:[NSOpenPanel class]]);
396      NSArray* filenames = [static_cast<NSOpenPanel*>(panel) filenames];
397      for (NSString* filename in filenames)
398        paths.push_back(FilePath(base::SysNSStringToUTF8(filename)));
399    }
400  }
401
402  selectFileDialogImpl_->FileWasSelected(panel,
403                                         parentWindow,
404                                         did_cancel,
405                                         isMulti,
406                                         paths,
407                                         index);
408  [panel release];
409}
410
411- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename {
412  return selectFileDialogImpl_->ShouldEnableFilename(sender, filename);
413}
414
415@end
416