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