| // Copyright (c) 2012 The Chromium Embedded Framework Authors. |
| // Portions copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "libcef/browser/native/file_dialog_runner_mac.h" |
| |
| #import <Cocoa/Cocoa.h> |
| #import <CoreServices/CoreServices.h> |
| |
| #include "libcef/browser/browser_host_impl.h" |
| |
| #include "base/mac/mac_util.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "cef/grit/cef_strings.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "net/base/mime_util.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/strings/grit/ui_strings.h" |
| |
| namespace { |
| |
| base::string16 GetDescriptionFromMimeType(const std::string& mime_type) { |
| // Check for wild card mime types and return an appropriate description. |
| static const struct { |
| const char* mime_type; |
| int string_id; |
| } kWildCardMimeTypes[] = { |
| {"audio", IDS_AUDIO_FILES}, |
| {"image", IDS_IMAGE_FILES}, |
| {"text", IDS_TEXT_FILES}, |
| {"video", IDS_VIDEO_FILES}, |
| }; |
| |
| for (size_t i = 0; i < base::size(kWildCardMimeTypes); ++i) { |
| if (mime_type == std::string(kWildCardMimeTypes[i].mime_type) + "/*") |
| return l10n_util::GetStringUTF16(kWildCardMimeTypes[i].string_id); |
| } |
| |
| return base::string16(); |
| } |
| |
| void AddFilters(NSPopUpButton* button, |
| const std::vector<base::string16>& accept_filters, |
| bool include_all_files, |
| std::vector<std::vector<base::string16>>* all_extensions) { |
| for (size_t i = 0; i < accept_filters.size(); ++i) { |
| const base::string16& filter = accept_filters[i]; |
| if (filter.empty()) |
| continue; |
| |
| std::vector<base::string16> extensions; |
| base::string16 description; |
| |
| size_t sep_index = filter.find('|'); |
| if (sep_index != std::string::npos) { |
| // Treat as a filter of the form "Filter Name|.ext1;.ext2;.ext3". |
| description = filter.substr(0, sep_index); |
| |
| const std::vector<base::string16>& ext = base::SplitString( |
| filter.substr(sep_index + 1), base::ASCIIToUTF16(";"), |
| base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| for (size_t x = 0; x < ext.size(); ++x) { |
| const base::string16& file_ext = ext[x]; |
| if (!file_ext.empty() && file_ext[0] == '.') |
| extensions.push_back(file_ext); |
| } |
| } else if (filter[0] == '.') { |
| // Treat as an extension beginning with the '.' character. |
| extensions.push_back(filter); |
| } else { |
| // Otherwise convert mime type to one or more extensions. |
| const std::string& ascii = base::UTF16ToASCII(filter); |
| std::vector<base::FilePath::StringType> ext; |
| net::GetExtensionsForMimeType(ascii, &ext); |
| if (!ext.empty()) { |
| for (size_t x = 0; x < ext.size(); ++x) |
| extensions.push_back(base::ASCIIToUTF16("." + ext[x])); |
| description = GetDescriptionFromMimeType(ascii); |
| } |
| } |
| |
| if (extensions.empty()) |
| continue; |
| |
| // Don't display a crazy number of extensions since the NSPopUpButton width |
| // will keep growing. |
| const size_t kMaxExtensions = 10; |
| |
| base::string16 ext_str; |
| for (size_t x = 0; x < std::min(kMaxExtensions, extensions.size()); ++x) { |
| const base::string16& pattern = base::ASCIIToUTF16("*") + extensions[x]; |
| if (x != 0) |
| ext_str += base::ASCIIToUTF16(";"); |
| ext_str += pattern; |
| } |
| |
| if (extensions.size() > kMaxExtensions) |
| ext_str += base::ASCIIToUTF16(";..."); |
| |
| if (description.empty()) { |
| description = ext_str; |
| } else { |
| description += |
| base::ASCIIToUTF16(" (") + ext_str + base::ASCIIToUTF16(")"); |
| } |
| |
| [button addItemWithTitle:base::SysUTF16ToNSString(description)]; |
| |
| all_extensions->push_back(extensions); |
| } |
| |
| // Add the *.* filter, but only if we have added other filters (otherwise it |
| // is implied). |
| if (include_all_files && !all_extensions->empty()) { |
| [button addItemWithTitle:base::SysUTF8ToNSString("All Files (*)")]; |
| all_extensions->push_back(std::vector<base::string16>()); |
| } |
| } |
| |
| } // namespace |
| |
| // Used to manage the file type filter in the NSSavePanel/NSOpenPanel. |
| @interface CefFilterDelegate : NSObject { |
| @private |
| NSSavePanel* panel_; |
| std::vector<std::vector<base::string16>> extensions_; |
| int selected_index_; |
| } |
| - (id)initWithPanel:(NSSavePanel*)panel |
| andAcceptFilters:(const std::vector<base::string16>&)accept_filters |
| andFilterIndex:(int)index; |
| - (void)setFilter:(int)index; |
| - (int)filter; |
| - (void)filterSelectionChanged:(id)sender; |
| - (void)setFileExtension; |
| @end |
| |
| @implementation CefFilterDelegate |
| |
| - (id)initWithPanel:(NSSavePanel*)panel |
| andAcceptFilters:(const std::vector<base::string16>&)accept_filters |
| andFilterIndex:(int)index { |
| if (self = [super init]) { |
| DCHECK(panel); |
| panel_ = panel; |
| selected_index_ = 0; |
| |
| NSPopUpButton* button = [[NSPopUpButton alloc] init]; |
| AddFilters(button, accept_filters, true, &extensions_); |
| [button sizeToFit]; |
| [button setTarget:self]; |
| [button setAction:@selector(filterSelectionChanged:)]; |
| |
| if (index < static_cast<int>(extensions_.size())) { |
| [button selectItemAtIndex:index]; |
| [self setFilter:index]; |
| } |
| |
| [panel_ setAccessoryView:button]; |
| } |
| return self; |
| } |
| |
| // Set the current filter index. |
| - (void)setFilter:(int)index { |
| DCHECK(index >= 0 && index < static_cast<int>(extensions_.size())); |
| selected_index_ = index; |
| |
| // Set the selectable file types. For open panels this limits the files that |
| // can be selected. For save panels this applies a default file extenion when |
| // the dialog is dismissed if none is already provided. |
| NSMutableArray* acceptArray = nil; |
| if (!extensions_[index].empty()) { |
| acceptArray = [[NSMutableArray alloc] init]; |
| for (size_t i = 0; i < extensions_[index].size(); ++i) { |
| [acceptArray |
| addObject:base::SysUTF16ToNSString(extensions_[index][i].substr(1))]; |
| } |
| } |
| [panel_ setAllowedFileTypes:acceptArray]; |
| |
| if (![panel_ isKindOfClass:[NSOpenPanel class]]) { |
| // For save panels set the file extension. |
| [self setFileExtension]; |
| } |
| } |
| |
| // Returns the current filter index. |
| - (int)filter { |
| return selected_index_; |
| } |
| |
| // Called when the selected filter is changed via the NSPopUpButton. |
| - (void)filterSelectionChanged:(id)sender { |
| NSPopUpButton* button = (NSPopUpButton*)sender; |
| [self setFilter:[button indexOfSelectedItem]]; |
| } |
| |
| // Set the extension on the currently selected file name. |
| - (void)setFileExtension { |
| const std::vector<base::string16>& filter = extensions_[selected_index_]; |
| if (filter.empty()) { |
| // All extensions are allowed so don't change anything. |
| return; |
| } |
| |
| base::FilePath path(base::SysNSStringToUTF8([panel_ nameFieldStringValue])); |
| |
| // If the file name currently includes an extension from |filter| then don't |
| // change anything. |
| base::string16 extension = base::UTF8ToUTF16(path.Extension()); |
| if (!extension.empty()) { |
| for (size_t i = 0; i < filter.size(); ++i) { |
| if (filter[i] == extension) |
| return; |
| } |
| } |
| |
| // Change the extension to the first value in |filter|. |
| path = path.ReplaceExtension(base::UTF16ToUTF8(filter[0])); |
| [panel_ setNameFieldStringValue:base::SysUTF8ToNSString(path.value())]; |
| } |
| |
| @end |
| |
| CefFileDialogRunnerMac::CefFileDialogRunnerMac() : weak_ptr_factory_(this) {} |
| |
| void CefFileDialogRunnerMac::Run(CefBrowserHostImpl* browser, |
| const FileChooserParams& params, |
| RunFileChooserCallback callback) { |
| callback_ = std::move(callback); |
| |
| int filter_index = params.selected_accept_filter; |
| NSView* owner = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(browser->GetWindowHandle()); |
| auto weak_this = weak_ptr_factory_.GetWeakPtr(); |
| |
| if (params.mode == blink::mojom::FileChooserParams::Mode::kOpen || |
| params.mode == blink::mojom::FileChooserParams::Mode::kOpenMultiple || |
| params.mode == blink::mojom::FileChooserParams::Mode::kUploadFolder) { |
| RunOpenFileDialog(weak_this, params, owner, filter_index); |
| } else if (params.mode == blink::mojom::FileChooserParams::Mode::kSave) { |
| RunSaveFileDialog(weak_this, params, owner, filter_index); |
| } else { |
| NOTIMPLEMENTED(); |
| } |
| } |
| |
| // static |
| void CefFileDialogRunnerMac::RunOpenFileDialog( |
| base::WeakPtr<CefFileDialogRunnerMac> weak_this, |
| const CefFileDialogRunner::FileChooserParams& params, |
| NSView* view, |
| int filter_index) { |
| NSOpenPanel* openPanel = [NSOpenPanel openPanel]; |
| |
| base::string16 title; |
| if (!params.title.empty()) { |
| title = params.title; |
| } else { |
| title = l10n_util::GetStringUTF16( |
| params.mode == blink::mojom::FileChooserParams::Mode::kOpen |
| ? IDS_OPEN_FILE_DIALOG_TITLE |
| : (params.mode == |
| blink::mojom::FileChooserParams::Mode::kOpenMultiple |
| ? IDS_OPEN_FILES_DIALOG_TITLE |
| : IDS_SELECT_FOLDER_DIALOG_TITLE)); |
| } |
| [openPanel setTitle:base::SysUTF16ToNSString(title)]; |
| |
| std::string filename, directory; |
| if (!params.default_file_name.empty()) { |
| if (params.mode == blink::mojom::FileChooserParams::Mode::kUploadFolder || |
| params.default_file_name.EndsWithSeparator()) { |
| // The value is only a directory. |
| directory = params.default_file_name.value(); |
| } else { |
| // The value is a file name and possibly a directory. |
| filename = params.default_file_name.BaseName().value(); |
| directory = params.default_file_name.DirName().value(); |
| } |
| } |
| if (!filename.empty()) { |
| [openPanel setNameFieldStringValue:base::SysUTF8ToNSString(filename)]; |
| } |
| if (!directory.empty()) { |
| [openPanel setDirectoryURL:[NSURL fileURLWithPath:base::SysUTF8ToNSString( |
| directory)]]; |
| } |
| |
| CefFilterDelegate* filter_delegate = nil; |
| if (params.mode != blink::mojom::FileChooserParams::Mode::kUploadFolder && |
| !params.accept_types.empty()) { |
| // Add the file filter control. |
| filter_delegate = |
| [[CefFilterDelegate alloc] initWithPanel:openPanel |
| andAcceptFilters:params.accept_types |
| andFilterIndex:filter_index]; |
| } |
| |
| // Further panel configuration. |
| [openPanel setAllowsOtherFileTypes:YES]; |
| [openPanel setAllowsMultipleSelection: |
| (params.mode == |
| blink::mojom::FileChooserParams::Mode::kOpenMultiple)]; |
| [openPanel |
| setCanChooseFiles:(params.mode != |
| blink::mojom::FileChooserParams::Mode::kUploadFolder)]; |
| [openPanel |
| setCanChooseDirectories:(params.mode == blink::mojom::FileChooserParams:: |
| Mode::kUploadFolder)]; |
| [openPanel setShowsHiddenFiles:!params.hidereadonly]; |
| |
| // Show panel. |
| [openPanel |
| beginSheetModalForWindow:[view window] |
| completionHandler:^(NSInteger returnCode) { |
| int filter_index_to_use = (filter_delegate != nil) |
| ? [filter_delegate filter] |
| : filter_index; |
| if (returnCode == NSFileHandlingPanelOKButton) { |
| std::vector<base::FilePath> files; |
| files.reserve(openPanel.URLs.count); |
| for (NSURL* url in openPanel.URLs) { |
| if (url.isFileURL) |
| files.push_back(base::FilePath(url.path.UTF8String)); |
| } |
| std::move(weak_this->callback_) |
| .Run(filter_index_to_use, files); |
| } else { |
| std::move(weak_this->callback_) |
| .Run(filter_index_to_use, std::vector<base::FilePath>()); |
| } |
| }]; |
| } |
| |
| // static |
| void CefFileDialogRunnerMac::RunSaveFileDialog( |
| base::WeakPtr<CefFileDialogRunnerMac> weak_this, |
| const CefFileDialogRunner::FileChooserParams& params, |
| NSView* view, |
| int filter_index) { |
| NSSavePanel* savePanel = [NSSavePanel savePanel]; |
| |
| base::string16 title; |
| if (!params.title.empty()) |
| title = params.title; |
| else |
| title = l10n_util::GetStringUTF16(IDS_SAVE_AS_DIALOG_TITLE); |
| [savePanel setTitle:base::SysUTF16ToNSString(title)]; |
| |
| std::string filename, directory; |
| if (!params.default_file_name.empty()) { |
| if (params.default_file_name.EndsWithSeparator()) { |
| // The value is only a directory. |
| directory = params.default_file_name.value(); |
| } else { |
| // The value is a file name and possibly a directory. |
| filename = params.default_file_name.BaseName().value(); |
| directory = params.default_file_name.DirName().value(); |
| } |
| } |
| if (!filename.empty()) { |
| [savePanel setNameFieldStringValue:base::SysUTF8ToNSString(filename)]; |
| } |
| if (!directory.empty()) { |
| [savePanel setDirectoryURL:[NSURL fileURLWithPath:base::SysUTF8ToNSString( |
| directory)]]; |
| } |
| |
| CefFilterDelegate* filter_delegate = nil; |
| if (!params.accept_types.empty()) { |
| // Add the file filter control. |
| filter_delegate = |
| [[CefFilterDelegate alloc] initWithPanel:savePanel |
| andAcceptFilters:params.accept_types |
| andFilterIndex:filter_index]; |
| } |
| |
| [savePanel setAllowsOtherFileTypes:YES]; |
| [savePanel setShowsHiddenFiles:!params.hidereadonly]; |
| |
| // Show panel. |
| [savePanel |
| beginSheetModalForWindow:view.window |
| completionHandler:^(NSInteger resultCode) { |
| int filter_index_to_use = (filter_delegate != nil) |
| ? [filter_delegate filter] |
| : filter_index; |
| if (resultCode == NSFileHandlingPanelOKButton) { |
| NSURL* url = savePanel.URL; |
| const char* path = url.path.UTF8String; |
| std::vector<base::FilePath> files(1, base::FilePath(path)); |
| std::move(weak_this->callback_) |
| .Run(filter_index_to_use, files); |
| } else { |
| std::move(weak_this->callback_) |
| .Run(filter_index_to_use, std::vector<base::FilePath>()); |
| } |
| }]; |
| } |