• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 The Chromium Embedded Framework Authors.
2// Portions copyright (c) 2012 The Chromium Authors. All rights reserved.
3// Use of this source code is governed by a BSD-style license that can be
4// found in the LICENSE file.
5
6#include "libcef/browser/native/file_dialog_runner_mac.h"
7
8#import <Cocoa/Cocoa.h>
9#import <CoreServices/CoreServices.h>
10
11#include "libcef/browser/alloy/alloy_browser_host_impl.h"
12
13#include "base/mac/mac_util.h"
14#include "base/stl_util.h"
15#include "base/strings/string_split.h"
16#include "base/strings/string_util.h"
17#include "base/strings/sys_string_conversions.h"
18#include "base/strings/utf_string_conversions.h"
19#include "base/threading/thread_restrictions.h"
20#include "cef/grit/cef_strings.h"
21#include "chrome/grit/generated_resources.h"
22#include "net/base/mime_util.h"
23#include "ui/base/l10n/l10n_util.h"
24#include "ui/strings/grit/ui_strings.h"
25
26namespace {
27
28std::u16string GetDescriptionFromMimeType(const std::string& mime_type) {
29  // Check for wild card mime types and return an appropriate description.
30  static const struct {
31    const char* mime_type;
32    int string_id;
33  } kWildCardMimeTypes[] = {
34      {"audio", IDS_AUDIO_FILES},
35      {"image", IDS_IMAGE_FILES},
36      {"text", IDS_TEXT_FILES},
37      {"video", IDS_VIDEO_FILES},
38  };
39
40  for (size_t i = 0; i < base::size(kWildCardMimeTypes); ++i) {
41    if (mime_type == std::string(kWildCardMimeTypes[i].mime_type) + "/*")
42      return l10n_util::GetStringUTF16(kWildCardMimeTypes[i].string_id);
43  }
44
45  return std::u16string();
46}
47
48void AddFilters(NSPopUpButton* button,
49                const std::vector<std::u16string>& accept_filters,
50                bool include_all_files,
51                std::vector<std::vector<std::u16string>>* all_extensions) {
52  for (size_t i = 0; i < accept_filters.size(); ++i) {
53    const std::u16string& filter = accept_filters[i];
54    if (filter.empty())
55      continue;
56
57    std::vector<std::u16string> extensions;
58    std::u16string description;
59
60    size_t sep_index = filter.find('|');
61    if (sep_index != std::string::npos) {
62      // Treat as a filter of the form "Filter Name|.ext1;.ext2;.ext3".
63      description = filter.substr(0, sep_index);
64
65      const std::vector<std::u16string>& ext =
66          base::SplitString(filter.substr(sep_index + 1), u";",
67                            base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
68      for (size_t x = 0; x < ext.size(); ++x) {
69        const std::u16string& file_ext = ext[x];
70        if (!file_ext.empty() && file_ext[0] == '.')
71          extensions.push_back(file_ext);
72      }
73    } else if (filter[0] == '.') {
74      // Treat as an extension beginning with the '.' character.
75      extensions.push_back(filter);
76    } else {
77      // Otherwise convert mime type to one or more extensions.
78      const std::string& ascii = base::UTF16ToASCII(filter);
79      std::vector<base::FilePath::StringType> ext;
80      net::GetExtensionsForMimeType(ascii, &ext);
81      if (!ext.empty()) {
82        for (size_t x = 0; x < ext.size(); ++x)
83          extensions.push_back(u"." + base::ASCIIToUTF16(ext[x]));
84        description = GetDescriptionFromMimeType(ascii);
85      }
86    }
87
88    if (extensions.empty())
89      continue;
90
91    // Don't display a crazy number of extensions since the NSPopUpButton width
92    // will keep growing.
93    const size_t kMaxExtensions = 10;
94
95    std::u16string ext_str;
96    for (size_t x = 0; x < std::min(kMaxExtensions, extensions.size()); ++x) {
97      const std::u16string& pattern = u"*" + extensions[x];
98      if (x != 0)
99        ext_str += u";";
100      ext_str += pattern;
101    }
102
103    if (extensions.size() > kMaxExtensions)
104      ext_str += u";...";
105
106    if (description.empty()) {
107      description = ext_str;
108    } else {
109      description += u" (" + ext_str + u")";
110    }
111
112    [button addItemWithTitle:base::SysUTF16ToNSString(description)];
113
114    all_extensions->push_back(extensions);
115  }
116
117  // Add the *.* filter, but only if we have added other filters (otherwise it
118  // is implied).
119  if (include_all_files && !all_extensions->empty()) {
120    [button addItemWithTitle:base::SysUTF8ToNSString("All Files (*)")];
121    all_extensions->push_back(std::vector<std::u16string>());
122  }
123}
124
125}  // namespace
126
127// Used to manage the file type filter in the NSSavePanel/NSOpenPanel.
128@interface CefFilterDelegate : NSObject {
129 @private
130  NSSavePanel* panel_;
131  std::vector<std::vector<std::u16string>> extensions_;
132  int selected_index_;
133}
134- (id)initWithPanel:(NSSavePanel*)panel
135    andAcceptFilters:(const std::vector<std::u16string>&)accept_filters
136      andFilterIndex:(int)index;
137- (void)setFilter:(int)index;
138- (int)filter;
139- (void)filterSelectionChanged:(id)sender;
140- (void)setFileExtension;
141@end
142
143@implementation CefFilterDelegate
144
145- (id)initWithPanel:(NSSavePanel*)panel
146    andAcceptFilters:(const std::vector<std::u16string>&)accept_filters
147      andFilterIndex:(int)index {
148  if (self = [super init]) {
149    DCHECK(panel);
150    panel_ = panel;
151    selected_index_ = 0;
152
153    NSPopUpButton* button = [[NSPopUpButton alloc] init];
154    AddFilters(button, accept_filters, true, &extensions_);
155    [button sizeToFit];
156    [button setTarget:self];
157    [button setAction:@selector(filterSelectionChanged:)];
158
159    if (index < static_cast<int>(extensions_.size())) {
160      [button selectItemAtIndex:index];
161      [self setFilter:index];
162    }
163
164    [panel_ setAccessoryView:button];
165  }
166  return self;
167}
168
169// Set the current filter index.
170- (void)setFilter:(int)index {
171  DCHECK(index >= 0 && index < static_cast<int>(extensions_.size()));
172  selected_index_ = index;
173
174  // Set the selectable file types. For open panels this limits the files that
175  // can be selected. For save panels this applies a default file extenion when
176  // the dialog is dismissed if none is already provided.
177  NSMutableArray* acceptArray = nil;
178  if (!extensions_[index].empty()) {
179    acceptArray = [[NSMutableArray alloc] init];
180    for (size_t i = 0; i < extensions_[index].size(); ++i) {
181      [acceptArray
182          addObject:base::SysUTF16ToNSString(extensions_[index][i].substr(1))];
183    }
184  }
185  [panel_ setAllowedFileTypes:acceptArray];
186
187  if (![panel_ isKindOfClass:[NSOpenPanel class]]) {
188    // For save panels set the file extension.
189    [self setFileExtension];
190  }
191}
192
193// Returns the current filter index.
194- (int)filter {
195  return selected_index_;
196}
197
198// Called when the selected filter is changed via the NSPopUpButton.
199- (void)filterSelectionChanged:(id)sender {
200  NSPopUpButton* button = (NSPopUpButton*)sender;
201  [self setFilter:[button indexOfSelectedItem]];
202}
203
204// Set the extension on the currently selected file name.
205- (void)setFileExtension {
206  const std::vector<std::u16string>& filter = extensions_[selected_index_];
207  if (filter.empty()) {
208    // All extensions are allowed so don't change anything.
209    return;
210  }
211
212  base::FilePath path(base::SysNSStringToUTF8([panel_ nameFieldStringValue]));
213
214  // If the file name currently includes an extension from |filter| then don't
215  // change anything.
216  std::u16string extension = base::UTF8ToUTF16(path.Extension());
217  if (!extension.empty()) {
218    for (size_t i = 0; i < filter.size(); ++i) {
219      if (filter[i] == extension)
220        return;
221    }
222  }
223
224  // Change the extension to the first value in |filter|.
225  path = path.ReplaceExtension(base::UTF16ToUTF8(filter[0]));
226  [panel_ setNameFieldStringValue:base::SysUTF8ToNSString(path.value())];
227}
228
229@end
230
231CefFileDialogRunnerMac::CefFileDialogRunnerMac() : weak_ptr_factory_(this) {}
232
233void CefFileDialogRunnerMac::Run(AlloyBrowserHostImpl* browser,
234                                 const FileChooserParams& params,
235                                 RunFileChooserCallback callback) {
236  callback_ = std::move(callback);
237
238  int filter_index = params.selected_accept_filter;
239  NSView* owner = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(browser->GetWindowHandle());
240  auto weak_this = weak_ptr_factory_.GetWeakPtr();
241
242  if (params.mode == blink::mojom::FileChooserParams::Mode::kOpen ||
243      params.mode == blink::mojom::FileChooserParams::Mode::kOpenMultiple ||
244      params.mode == blink::mojom::FileChooserParams::Mode::kUploadFolder) {
245    RunOpenFileDialog(weak_this, params, owner, filter_index);
246  } else if (params.mode == blink::mojom::FileChooserParams::Mode::kSave) {
247    RunSaveFileDialog(weak_this, params, owner, filter_index);
248  } else {
249    NOTIMPLEMENTED();
250  }
251}
252
253// static
254void CefFileDialogRunnerMac::RunOpenFileDialog(
255    base::WeakPtr<CefFileDialogRunnerMac> weak_this,
256    const CefFileDialogRunner::FileChooserParams& params,
257    NSView* view,
258    int filter_index) {
259  NSOpenPanel* openPanel = [NSOpenPanel openPanel];
260
261  std::u16string title;
262  if (!params.title.empty()) {
263    title = params.title;
264  } else {
265    title = l10n_util::GetStringUTF16(
266        params.mode == blink::mojom::FileChooserParams::Mode::kOpen
267            ? IDS_OPEN_FILE_DIALOG_TITLE
268            : (params.mode ==
269                       blink::mojom::FileChooserParams::Mode::kOpenMultiple
270                   ? IDS_OPEN_FILES_DIALOG_TITLE
271                   : IDS_SELECT_FOLDER_DIALOG_TITLE));
272  }
273  [openPanel setTitle:base::SysUTF16ToNSString(title)];
274
275  std::string filename, directory;
276  if (!params.default_file_name.empty()) {
277    if (params.mode == blink::mojom::FileChooserParams::Mode::kUploadFolder ||
278        params.default_file_name.EndsWithSeparator()) {
279      // The value is only a directory.
280      directory = params.default_file_name.value();
281    } else {
282      // The value is a file name and possibly a directory.
283      filename = params.default_file_name.BaseName().value();
284      directory = params.default_file_name.DirName().value();
285    }
286  }
287  if (!filename.empty()) {
288    [openPanel setNameFieldStringValue:base::SysUTF8ToNSString(filename)];
289  }
290  if (!directory.empty()) {
291    [openPanel setDirectoryURL:[NSURL fileURLWithPath:base::SysUTF8ToNSString(
292                                                          directory)]];
293  }
294
295  CefFilterDelegate* filter_delegate = nil;
296  if (params.mode != blink::mojom::FileChooserParams::Mode::kUploadFolder &&
297      !params.accept_types.empty()) {
298    // Add the file filter control.
299    filter_delegate =
300        [[CefFilterDelegate alloc] initWithPanel:openPanel
301                                andAcceptFilters:params.accept_types
302                                  andFilterIndex:filter_index];
303  }
304
305  // Further panel configuration.
306  [openPanel setAllowsOtherFileTypes:YES];
307  [openPanel setAllowsMultipleSelection:
308                 (params.mode ==
309                  blink::mojom::FileChooserParams::Mode::kOpenMultiple)];
310  [openPanel
311      setCanChooseFiles:(params.mode !=
312                         blink::mojom::FileChooserParams::Mode::kUploadFolder)];
313  [openPanel
314      setCanChooseDirectories:(params.mode == blink::mojom::FileChooserParams::
315                                                  Mode::kUploadFolder)];
316  [openPanel setShowsHiddenFiles:!params.hidereadonly];
317
318  // Show panel.
319  [openPanel
320      beginSheetModalForWindow:[view window]
321             completionHandler:^(NSInteger returnCode) {
322               int filter_index_to_use = (filter_delegate != nil)
323                                             ? [filter_delegate filter]
324                                             : filter_index;
325               if (returnCode == NSFileHandlingPanelOKButton) {
326                 std::vector<base::FilePath> files;
327                 files.reserve(openPanel.URLs.count);
328                 for (NSURL* url in openPanel.URLs) {
329                   if (url.isFileURL)
330                     files.push_back(base::FilePath(url.path.UTF8String));
331                 }
332                 std::move(weak_this->callback_)
333                     .Run(filter_index_to_use, files);
334               } else {
335                 std::move(weak_this->callback_)
336                     .Run(filter_index_to_use, std::vector<base::FilePath>());
337               }
338             }];
339}
340
341// static
342void CefFileDialogRunnerMac::RunSaveFileDialog(
343    base::WeakPtr<CefFileDialogRunnerMac> weak_this,
344    const CefFileDialogRunner::FileChooserParams& params,
345    NSView* view,
346    int filter_index) {
347  NSSavePanel* savePanel = [NSSavePanel savePanel];
348
349  std::u16string title;
350  if (!params.title.empty())
351    title = params.title;
352  else
353    title = l10n_util::GetStringUTF16(IDS_SAVE_AS_DIALOG_TITLE);
354  [savePanel setTitle:base::SysUTF16ToNSString(title)];
355
356  std::string filename, directory;
357  if (!params.default_file_name.empty()) {
358    if (params.default_file_name.EndsWithSeparator()) {
359      // The value is only a directory.
360      directory = params.default_file_name.value();
361    } else {
362      // The value is a file name and possibly a directory.
363      filename = params.default_file_name.BaseName().value();
364      directory = params.default_file_name.DirName().value();
365    }
366  }
367  if (!filename.empty()) {
368    [savePanel setNameFieldStringValue:base::SysUTF8ToNSString(filename)];
369  }
370  if (!directory.empty()) {
371    [savePanel setDirectoryURL:[NSURL fileURLWithPath:base::SysUTF8ToNSString(
372                                                          directory)]];
373  }
374
375  CefFilterDelegate* filter_delegate = nil;
376  if (!params.accept_types.empty()) {
377    // Add the file filter control.
378    filter_delegate =
379        [[CefFilterDelegate alloc] initWithPanel:savePanel
380                                andAcceptFilters:params.accept_types
381                                  andFilterIndex:filter_index];
382  }
383
384  [savePanel setAllowsOtherFileTypes:YES];
385  [savePanel setShowsHiddenFiles:!params.hidereadonly];
386
387  // Show panel.
388  [savePanel
389      beginSheetModalForWindow:view.window
390             completionHandler:^(NSInteger resultCode) {
391               int filter_index_to_use = (filter_delegate != nil)
392                                             ? [filter_delegate filter]
393                                             : filter_index;
394               if (resultCode == NSFileHandlingPanelOKButton) {
395                 NSURL* url = savePanel.URL;
396                 const char* path = url.path.UTF8String;
397                 std::vector<base::FilePath> files(1, base::FilePath(path));
398                 std::move(weak_this->callback_)
399                     .Run(filter_index_to_use, files);
400               } else {
401                 std::move(weak_this->callback_)
402                     .Run(filter_index_to_use, std::vector<base::FilePath>());
403               }
404             }];
405}
406