- Provide default implementations of the file chooser dialogs (open, open multiple, save) on all platforms (issue #761).

- Add a new CefBrowserHost::RunFileDialog method that displays the specified file chooser dialog and returns the results asynchronously (issue #761).
- Add a new CefDialogHandler::OnFileDialog callback that allows the application to provide custom UI for file chooser dialogs (issue #761).

git-svn-id: https://chromiumembedded.googlecode.com/svn/trunk@862 5089003a-bbd8-11dd-ad1f-f1f9622dbc98
This commit is contained in:
Marshall Greenblatt
2012-10-16 19:28:07 +00:00
parent dc5ba49aaf
commit eda69594ef
56 changed files with 2475 additions and 264 deletions

View File

@@ -102,6 +102,99 @@ bool GetCefKeyEvent(const content::NativeWebKeyboardEvent& event,
return true;
}
class CefFileDialogCallbackImpl : public CefFileDialogCallback {
public:
explicit CefFileDialogCallbackImpl(
const CefBrowserHostImpl::RunFileChooserCallback& callback)
: callback_(callback) {
}
~CefFileDialogCallbackImpl() {
if (!callback_.is_null()) {
// The callback is still pending. Cancel it now.
if (CEF_CURRENTLY_ON_UIT()) {
CancelNow(callback_);
} else {
CEF_POST_TASK(CEF_UIT,
base::Bind(&CefFileDialogCallbackImpl::CancelNow, callback_));
}
}
}
virtual void Continue(const std::vector<CefString>& file_paths) OVERRIDE {
if (CEF_CURRENTLY_ON_UIT()) {
if (!callback_.is_null()) {
std::vector<FilePath> vec;
if (!file_paths.empty()) {
std::vector<CefString>::const_iterator it = file_paths.begin();
for (; it != file_paths.end(); ++it)
vec.push_back(FilePath(*it));
}
callback_.Run(vec);
callback_.Reset();
}
} else {
CEF_POST_TASK(CEF_UIT,
base::Bind(&CefFileDialogCallbackImpl::Continue, this, file_paths));
}
}
virtual void Cancel() OVERRIDE {
if (CEF_CURRENTLY_ON_UIT()) {
if (!callback_.is_null()) {
CancelNow(callback_);
callback_.Reset();
}
} else {
CEF_POST_TASK(CEF_UIT,
base::Bind(&CefFileDialogCallbackImpl::Cancel, this));
}
}
bool IsConnected() {
return !callback_.is_null();
}
void Disconnect() {
callback_.Reset();
}
private:
static void CancelNow(
const CefBrowserHostImpl::RunFileChooserCallback& callback) {
CEF_REQUIRE_UIT();
std::vector<FilePath> file_paths;
callback.Run(file_paths);
}
CefBrowserHostImpl::RunFileChooserCallback callback_;
IMPLEMENT_REFCOUNTING(CefFileDialogCallbackImpl);
};
class CefRunFileDialogCallbackWrapper
: public base::RefCountedThreadSafe<CefRunFileDialogCallbackWrapper> {
public:
CefRunFileDialogCallbackWrapper(CefRefPtr<CefBrowserHost> host,
CefRefPtr<CefRunFileDialogCallback> callback)
: host_(host),
callback_(callback) {
}
void Callback(const std::vector<FilePath>& file_paths) {
std::vector<CefString> paths;
if (file_paths.size() > 0) {
for (size_t i = 0; i < file_paths.size(); ++i)
paths.push_back(file_paths[i].value());
}
callback_->OnFileDialogDismissed(host_, paths);
}
private:
CefRefPtr<CefBrowserHost> host_;
CefRefPtr<CefRunFileDialogCallback> callback_;
};
} // namespace
@@ -356,6 +449,43 @@ void CefBrowserHostImpl::SetZoomLevel(double zoomLevel) {
}
}
void CefBrowserHostImpl::RunFileDialog(
FileDialogMode mode,
const CefString& title,
const CefString& default_file_name,
const std::vector<CefString>& accept_types,
CefRefPtr<CefRunFileDialogCallback> callback) {
DCHECK(callback.get());
if (!callback.get())
return;
content::FileChooserParams params;
switch (mode) {
case FILE_DIALOG_OPEN:
params.mode = content::FileChooserParams::Open;
break;
case FILE_DIALOG_OPEN_MULTIPLE:
params.mode = content::FileChooserParams::OpenMultiple;
break;
case FILE_DIALOG_SAVE:
params.mode = content::FileChooserParams::Save;
break;
}
params.title = title;
if (!default_file_name.empty())
params.default_file_name = FilePath(default_file_name);
if (!accept_types.empty()) {
std::vector<CefString>::const_iterator it = accept_types.begin();
for (; it != accept_types.end(); ++it)
params.accept_types.push_back(*it);
}
scoped_refptr<CefRunFileDialogCallbackWrapper> wrapper =
new CefRunFileDialogCallbackWrapper(this, callback);
RunFileChooser(params,
base::Bind(&CefRunFileDialogCallbackWrapper::Callback, wrapper));
}
// CefBrowser methods.
// -----------------------------------------------------------------------------
@@ -859,6 +989,14 @@ void CefBrowserHostImpl::OnSetFocus(cef_focus_source_t source) {
}
}
void CefBrowserHostImpl::RunFileChooser(
const content::FileChooserParams& params,
const RunFileChooserCallback& callback) {
CEF_POST_TASK(CEF_UIT,
base::Bind(&CefBrowserHostImpl::RunFileChooserOnUIThread, this, params,
callback));
}
// content::WebContentsDelegate methods.
// -----------------------------------------------------------------------------
@@ -1096,29 +1234,9 @@ void CefBrowserHostImpl::RunFileChooser(
if (!render_view_host)
return;
if (params.mode != content::FileChooserParams::Open &&
params.mode != content::FileChooserParams::OpenMultiple) {
NOTREACHED() << "unsupported file chooser mode requested";
return;
}
std::vector<FilePath> fileList;
PlatformRunFileChooser(tab, params, fileList);
const int kReadFilePermissions =
base::PLATFORM_FILE_OPEN |
base::PLATFORM_FILE_READ |
base::PLATFORM_FILE_EXCLUSIVE_READ |
base::PLATFORM_FILE_ASYNC;
// Convert FilePath list to SelectedFileInfo list.
std::vector<ui::SelectedFileInfo> selected_files;
for (size_t i = 0; i < fileList.size(); ++i)
selected_files.push_back(ui::SelectedFileInfo(fileList[i], FilePath()));
// Notify our RenderViewHost in all cases.
render_view_host->FilesSelectedInChooser(selected_files,
kReadFilePermissions);
RunFileChooserOnUIThread(params,
base::Bind(&CefBrowserHostImpl::OnRunFileChooserDelegateCallback, this,
tab));
}
void CefBrowserHostImpl::UpdatePreferredSize(content::WebContents* source,
@@ -1397,7 +1515,8 @@ CefBrowserHostImpl::CefBrowserHostImpl(const CefWindowInfo& window_info,
main_frame_id_(CefFrameHostImpl::kInvalidFrameId),
focused_frame_id_(CefFrameHostImpl::kInvalidFrameId),
is_in_onsetfocus_(false),
focus_on_editable_field_(false) {
focus_on_editable_field_(false),
file_chooser_pending_(false) {
web_contents_.reset(web_contents);
web_contents->SetDelegate(this);
@@ -1583,3 +1702,109 @@ void CefBrowserHostImpl::OnLoadEnd(CefRefPtr<CefFrame> frame,
}
}
}
void CefBrowserHostImpl::RunFileChooserOnUIThread(
const content::FileChooserParams& params,
const RunFileChooserCallback& callback) {
CEF_REQUIRE_UIT();
if (file_chooser_pending_) {
// Dismiss the new dialog immediately.
callback.Run(std::vector<FilePath>());
return;
}
if (params.mode == content::FileChooserParams::OpenFolder) {
NOTIMPLEMENTED();
callback.Run(std::vector<FilePath>());
return;
}
file_chooser_pending_ = true;
// Ensure that the |file_chooser_pending_| flag is cleared.
const RunFileChooserCallback& host_callback =
base::Bind(&CefBrowserHostImpl::OnRunFileChooserCallback, this, callback);
bool handled = false;
if (client_.get()) {
CefRefPtr<CefDialogHandler> handler = client_->GetDialogHandler();
if (handler.get()) {
cef_file_dialog_mode_t mode;
switch (params.mode) {
case content::FileChooserParams::Open:
mode = FILE_DIALOG_OPEN;
break;
case content::FileChooserParams::OpenMultiple:
mode = FILE_DIALOG_OPEN_MULTIPLE;
break;
case content::FileChooserParams::Save:
mode = FILE_DIALOG_SAVE;
break;
default:
NOTREACHED();
break;
}
std::vector<CefString> accept_types;
std::vector<string16>::const_iterator it = params.accept_types.begin();
for (; it != params.accept_types.end(); ++it)
accept_types.push_back(*it);
CefRefPtr<CefFileDialogCallbackImpl> callbackImpl(
new CefFileDialogCallbackImpl(host_callback));
handled = handler->OnFileDialog(this, mode, params.title,
params.default_file_name.value(),
accept_types, callbackImpl.get());
if (!handled) {
if (callbackImpl->IsConnected()) {
callbackImpl->Disconnect();
} else {
// User executed the callback even though they returned false.
NOTREACHED();
handled = true;
}
}
}
}
if (!handled)
PlatformRunFileChooser(params, host_callback);
}
void CefBrowserHostImpl::OnRunFileChooserCallback(
const RunFileChooserCallback& callback,
const std::vector<FilePath>& file_paths) {
CEF_REQUIRE_UIT();
file_chooser_pending_ = false;
// Execute the callback asynchronously.
CEF_POST_TASK(CEF_UIT, base::Bind(callback, file_paths));
}
void CefBrowserHostImpl::OnRunFileChooserDelegateCallback(
content::WebContents* tab,
const std::vector<FilePath>& file_paths) {
CEF_REQUIRE_UIT();
content::RenderViewHost* render_view_host = tab->GetRenderViewHost();
if (!render_view_host)
return;
const int kReadFilePermissions =
base::PLATFORM_FILE_OPEN |
base::PLATFORM_FILE_READ |
base::PLATFORM_FILE_EXCLUSIVE_READ |
base::PLATFORM_FILE_ASYNC;
// Convert FilePath list to SelectedFileInfo list.
std::vector<ui::SelectedFileInfo> selected_files;
for (size_t i = 0; i < file_paths.size(); ++i)
selected_files.push_back(ui::SelectedFileInfo(file_paths[i], FilePath()));
// Notify our RenderViewHost in all cases.
render_view_host->FilesSelectedInChooser(selected_files,
kReadFilePermissions);
}

View File

@@ -107,6 +107,12 @@ class CefBrowserHostImpl : public CefBrowserHost,
virtual CefString GetDevToolsURL(bool http_scheme) OVERRIDE;
virtual double GetZoomLevel() OVERRIDE;
virtual void SetZoomLevel(double zoomLevel) OVERRIDE;
virtual void RunFileDialog(
FileDialogMode mode,
const CefString& title,
const CefString& default_file_name,
const std::vector<CefString>& accept_types,
CefRefPtr<CefRunFileDialogCallback> callback) OVERRIDE;
// CefBrowser methods.
virtual CefRefPtr<CefBrowserHost> GetHost() OVERRIDE;
@@ -198,6 +204,16 @@ class CefBrowserHostImpl : public CefBrowserHost,
void OnSetFocus(cef_focus_source_t source);
// The argument vector will be empty if the dialog was cancelled.
typedef base::Callback<void(const std::vector<FilePath>&)>
RunFileChooserCallback;
// Run the file chooser dialog specified by |params|. Only a single dialog may
// be pending at any given time. |callback| will be executed asynchronously
// after the dialog is dismissed or if another dialog is already pending.
void RunFileChooser(const content::FileChooserParams& params,
const RunFileChooserCallback& callback);
private:
// content::WebContentsDelegate methods.
virtual content::WebContents* OpenURLFromTab(
@@ -342,13 +358,11 @@ class CefBrowserHostImpl : public CefBrowserHost,
// processing of shortcut keys.
void PlatformHandleKeyboardEvent(
const content::NativeWebKeyboardEvent& event);
// Invoke platform specific file open chooser.
void PlatformRunFileChooser(
content::WebContents* contents,
const content::FileChooserParams& params,
std::vector<FilePath>& files);
// Invoke platform specific handling for the external protocol.
void PlatformHandleExternalProtocol(const GURL& url);
// Invoke platform specific file chooser dialog.
void PlatformRunFileChooser(const content::FileChooserParams& params,
RunFileChooserCallback callback);
void OnAddressChange(CefRefPtr<CefFrame> frame,
const GURL& url);
@@ -362,6 +376,19 @@ class CefBrowserHostImpl : public CefBrowserHost,
void OnLoadEnd(CefRefPtr<CefFrame> frame,
const GURL& url);
// Continuation from RunFileChooser.
void RunFileChooserOnUIThread(const content::FileChooserParams& params,
const RunFileChooserCallback& callback);
// Used with RunFileChooser to clear the |file_chooser_pending_| flag.
void OnRunFileChooserCallback(const RunFileChooserCallback& callback,
const std::vector<FilePath>& file_paths);
// Used with WebContentsDelegate::RunFileChooser to notify the WebContents.
void OnRunFileChooserDelegateCallback(
content::WebContents* tab,
const std::vector<FilePath>& file_paths);
CefWindowInfo window_info_;
CefBrowserSettings settings_;
CefRefPtr<CefClient> client_;
@@ -431,6 +458,9 @@ class CefBrowserHostImpl : public CefBrowserHost,
// Used for creating and managing context menus.
scoped_ptr<CefMenuCreator> menu_creator_;
// True if a file chooser is currently pending.
bool file_chooser_pending_;
IMPLEMENT_REFCOUNTING(CefBrowserHostImpl);
DISALLOW_EVIL_CONSTRUCTORS(CefBrowserHostImpl);
};

View File

@@ -10,8 +10,14 @@
#include "libcef/browser/thread_util.h"
#include "base/bind.h"
#include "base/utf_string_conversions.h"
#include "content/public/browser/web_contents_view.h"
#include "content/public/common/file_chooser_params.h"
#include "content/public/common/renderer_preferences.h"
#include "grit/cef_strings.h"
#include "grit/ui_strings.h"
#include "net/base/mime_util.h"
#include "ui/base/l10n/l10n_util.h"
namespace {
@@ -25,6 +31,183 @@ void window_destroyed(GtkWidget* widget, CefBrowserHostImpl* browser) {
CEF_POST_TASK(CEF_UIT, base::Bind(DestroyBrowser, browser));
}
std::string 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_APP_AUDIO_FILES },
{ "image", IDS_APP_IMAGE_FILES },
{ "text", IDS_APP_TEXT_FILES },
{ "video", IDS_APP_VIDEO_FILES },
};
for (size_t i = 0;
i < sizeof(kWildCardMimeTypes) / sizeof(kWildCardMimeTypes[0]); ++i) {
if (mime_type == std::string(kWildCardMimeTypes[i].mime_type) + "/*")
return l10n_util::GetStringUTF8(kWildCardMimeTypes[i].string_id);
}
return std::string();
}
void AddFiltersForAcceptTypes(GtkFileChooser* chooser,
const std::vector<string16>& accept_types,
bool include_all_files) {
bool has_filter = false;
for (size_t i = 0; i < accept_types.size(); ++i) {
std::string ascii_type = UTF16ToASCII(accept_types[i]);
if (ascii_type.length()) {
// Just treat as extension if contains '.' as the first character.
if (ascii_type[0] == '.') {
GtkFileFilter* filter = gtk_file_filter_new();
std::string pattern = "*" + ascii_type;
gtk_file_filter_add_pattern(filter, pattern.c_str());
gtk_file_filter_set_name(filter, pattern.c_str());
gtk_file_chooser_add_filter(chooser, filter);
if (!has_filter)
has_filter = true;
} else {
// Otherwise convert mime type to one or more extensions.
GtkFileFilter* filter = NULL;
std::string description = GetDescriptionFromMimeType(ascii_type);
bool description_from_ext = description.empty();
std::vector<FilePath::StringType> ext;
net::GetExtensionsForMimeType(ascii_type, &ext);
for (size_t x = 0; x < ext.size(); ++x) {
if (!filter)
filter = gtk_file_filter_new();
std::string pattern = "*." + ext[x];
gtk_file_filter_add_pattern(filter, pattern.c_str());
if (description_from_ext) {
if (x != 0)
description += ";";
description += pattern;
}
}
if (filter) {
gtk_file_filter_set_name(filter, description.c_str());
gtk_file_chooser_add_filter(chooser, filter);
if (!has_filter)
has_filter = true;
}
}
}
}
// Add the *.* filter, but only if we have added other filters (otherwise it
// is implied).
if (include_all_files && has_filter) {
GtkFileFilter* filter = gtk_file_filter_new();
gtk_file_filter_add_pattern(filter, "*");
gtk_file_filter_set_name(filter,
l10n_util::GetStringUTF8(IDS_SAVEAS_ALL_FILES).c_str());
gtk_file_chooser_add_filter(chooser, filter);
}
}
bool RunFileDialog(const content::FileChooserParams& params,
CefWindowHandle widget,
std::vector<FilePath>* files) {
GtkFileChooserAction action;
const gchar* accept_button;
if (params.mode == content::FileChooserParams::Open ||
params.mode == content::FileChooserParams::OpenMultiple) {
action = GTK_FILE_CHOOSER_ACTION_OPEN;
accept_button = GTK_STOCK_OPEN;
} else if (params.mode == content::FileChooserParams::Save) {
action = GTK_FILE_CHOOSER_ACTION_SAVE;
accept_button = GTK_STOCK_SAVE;
} else {
NOTREACHED();
return false;
}
// Consider default file name if any.
FilePath default_file_name(params.default_file_name);
std::string base_name;
if (!default_file_name.empty())
base_name = default_file_name.BaseName().value();
std::string title;
if (!params.title.empty()) {
title = UTF16ToUTF8(params.title);
} else {
int string_id = 0;
switch (params.mode) {
case content::FileChooserParams::Open:
string_id = IDS_OPEN_FILE_DIALOG_TITLE;
break;
case content::FileChooserParams::OpenMultiple:
string_id = IDS_OPEN_FILES_DIALOG_TITLE;
break;
case content::FileChooserParams::Save:
string_id = IDS_SAVE_AS_DIALOG_TITLE;
break;
default:
break;
}
title = l10n_util::GetStringUTF8(string_id);
}
GtkWidget* window = gtk_widget_get_toplevel(GTK_WIDGET(widget));
GtkWidget* dialog = gtk_file_chooser_dialog_new(
title.c_str(),
GTK_WINDOW(window),
action,
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
accept_button, GTK_RESPONSE_ACCEPT,
NULL);
if (params.mode == content::FileChooserParams::OpenMultiple) {
gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), TRUE);
} else if (params.mode == content::FileChooserParams::Save) {
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog),
TRUE);
}
if (params.mode == content::FileChooserParams::Save && !base_name.empty()) {
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog),
base_name.c_str());
}
AddFiltersForAcceptTypes(GTK_FILE_CHOOSER(dialog), params.accept_types, true);
bool success = false;
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
if (params.mode == content::FileChooserParams::Open ||
params.mode == content::FileChooserParams::Save) {
char* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
files->push_back(FilePath(filename));
success = true;
} else if (params.mode == content::FileChooserParams::OpenMultiple) {
GSList* filenames =
gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
if (filenames) {
for (GSList* iter = filenames; iter != NULL;
iter = g_slist_next(iter)) {
FilePath path(static_cast<char*>(iter->data));
g_free(iter->data);
files->push_back(path);
}
g_slist_free(filenames);
success = true;
}
}
}
gtk_widget_destroy(dialog);
return success;
}
} // namespace
bool CefBrowserHostImpl::PlatformCreateWindow() {
@@ -133,10 +316,19 @@ void CefBrowserHostImpl::PlatformHandleKeyboardEvent(
}
void CefBrowserHostImpl::PlatformRunFileChooser(
content::WebContents* contents,
const content::FileChooserParams& params,
std::vector<FilePath>& files) {
NOTIMPLEMENTED();
RunFileChooserCallback callback) {
std::vector<FilePath> files;
if (params.mode == content::FileChooserParams::Open ||
params.mode == content::FileChooserParams::OpenMultiple ||
params.mode == content::FileChooserParams::Save) {
::RunFileDialog(params, PlatformGetWindowHandle(), &files);
} else {
NOTIMPLEMENTED();
}
callback.Run(files);
}
void CefBrowserHostImpl::PlatformHandleExternalProtocol(const GURL& url) {

View File

@@ -17,7 +17,10 @@
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_view.h"
#include "content/public/common/file_chooser_params.h"
#include "grit/ui_strings.h"
#include "net/base/mime_util.h"
#import "ui/base/cocoa/underlay_opengl_hosting_window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/rect.h"
@@ -76,20 +79,129 @@ NSMutableArray* GetFileTypesFromAcceptTypes(
if (ascii_type.length()) {
// Just treat as extension if contains '.' as the first character.
if (ascii_type[0] == '.') {
[acceptArray addObject:base::SysUTF8ToNSString(ascii_type)];
[acceptArray addObject:base::SysUTF8ToNSString(ascii_type.substr(1))];
} else {
// Otherwise convert mime to UTI.
NSString* mimeType = base::SysUTF8ToNSString(ascii_type);
NSString* UTI = [NSMakeCollectable(
UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType,
(CFStringRef) mimeType, NULL)) autorelease];
[acceptArray addObject:UTI];
// Otherwise convert mime type to one or more extensions.
std::vector<FilePath::StringType> ext;
net::GetExtensionsForMimeType(ascii_type, &ext);
for (size_t x = 0; x < ext.size(); ++x)
[acceptArray addObject:base::SysUTF8ToNSString(ext[x])];
}
}
}
return acceptArray;
}
void RunOpenFileDialog(const content::FileChooserParams& params,
NSView* view,
std::vector<FilePath>* files) {
NSOpenPanel* openPanel = [NSOpenPanel openPanel];
string16 title;
if (!params.title.empty()) {
title = params.title;
} else {
title = l10n_util::GetStringUTF16(
params.mode == content::FileChooserParams::Open ?
IDS_OPEN_FILE_DIALOG_TITLE : IDS_OPEN_FILES_DIALOG_TITLE);
}
[openPanel setTitle:base::SysUTF16ToNSString(title)];
// Consider default file name if any.
FilePath default_file_name(params.default_file_name);
if (!default_file_name.empty()) {
if (!default_file_name.BaseName().empty()) {
NSString* defaultName = base::SysUTF8ToNSString(
default_file_name.BaseName().value());
[openPanel setNameFieldStringValue:defaultName];
}
if (!default_file_name.DirName().empty()) {
NSString* defaultDir = base::SysUTF8ToNSString(
default_file_name.DirName().value());
[openPanel setDirectoryURL:[NSURL fileURLWithPath:defaultDir]];
}
}
// Consider supported file types
if (!params.accept_types.empty()) {
[openPanel setAllowedFileTypes:GetFileTypesFromAcceptTypes(
params.accept_types)];
}
// Further panel configuration.
[openPanel setAllowsOtherFileTypes:YES];
[openPanel setAllowsMultipleSelection:
(params.mode == content::FileChooserParams::OpenMultiple)];
[openPanel setCanChooseFiles:YES];
[openPanel setCanChooseDirectories:NO];
// Show panel.
[openPanel beginSheetModalForWindow:[view window] completionHandler:nil];
if ([openPanel runModal] == NSFileHandlingPanelOKButton) {
NSArray *urls = [openPanel URLs];
int i, count = [urls count];
for (i=0; i<count; i++) {
NSURL* url = [urls objectAtIndex:i];
if ([url isFileURL])
files->push_back(FilePath(base::SysNSStringToUTF8([url path])));
}
}
[NSApp endSheet:openPanel];
}
bool RunSaveFileDialog(const content::FileChooserParams& params,
NSView* view,
FilePath* file) {
NSSavePanel* savePanel = [NSSavePanel savePanel];
string16 title;
if (!params.title.empty())
title = params.title;
else
title = l10n_util::GetStringUTF16(IDS_SAVE_AS_DIALOG_TITLE);
[savePanel setTitle:base::SysUTF16ToNSString(title)];
// Consider default file name if any.
FilePath default_file_name(params.default_file_name);
if (!default_file_name.empty()) {
if (!default_file_name.BaseName().empty()) {
NSString* defaultName = base::SysUTF8ToNSString(
default_file_name.BaseName().value());
[savePanel setNameFieldStringValue:defaultName];
}
if (!default_file_name.DirName().empty()) {
NSString* defaultDir = base::SysUTF8ToNSString(
default_file_name.DirName().value());
[savePanel setDirectoryURL:[NSURL fileURLWithPath:defaultDir]];
}
}
// Consider supported file types
if (!params.accept_types.empty()) {
[savePanel setAllowedFileTypes:GetFileTypesFromAcceptTypes(
params.accept_types)];
}
[savePanel setAllowsOtherFileTypes:YES];
bool success = false;
[savePanel beginSheetModalForWindow:[view window] completionHandler:nil];
if ([savePanel runModal] == NSFileHandlingPanelOKButton) {
NSURL * url = [savePanel URL];
NSString* path = [url path];
*file = FilePath([path UTF8String]);
success = true;
}
[NSApp endSheet:savePanel];
return success;
}
} // namespace
bool CefBrowserHostImpl::PlatformViewText(const std::string& text) {
@@ -186,56 +298,22 @@ void CefBrowserHostImpl::PlatformHandleKeyboardEvent(
}
void CefBrowserHostImpl::PlatformRunFileChooser(
content::WebContents* contents,
const content::FileChooserParams& params,
std::vector<FilePath>& files) {
NSOpenPanel* openPanel = [NSOpenPanel openPanel];
if (!params.title.empty())
[openPanel setTitle:base::SysUTF16ToNSString(params.title)];
RunFileChooserCallback callback) {
std::vector<FilePath> files;
// Consider default file name if any.
FilePath default_file_name(params.default_file_name);
if (!default_file_name.empty()) {
if (!default_file_name.BaseName().empty()) {
NSString* defaultName = base::SysUTF8ToNSString(
default_file_name.BaseName().value());
[openPanel setNameFieldStringValue:defaultName];
}
if (!default_file_name.DirName().empty()) {
NSString* defaultDir = base::SysUTF8ToNSString(
default_file_name.DirName().value());
[openPanel setDirectoryURL:[NSURL fileURLWithPath:defaultDir]];
}
if (params.mode == content::FileChooserParams::Open ||
params.mode == content::FileChooserParams::OpenMultiple) {
RunOpenFileDialog(params, PlatformGetWindowHandle(), &files);
} else if (params.mode == content::FileChooserParams::Save) {
FilePath file;
if (RunSaveFileDialog(params, PlatformGetWindowHandle(), &file))
files.push_back(file);
} else {
NOTIMPLEMENTED();
}
// Consider supported file types
if (!params.accept_types.empty()) {
[openPanel setAllowedFileTypes:GetFileTypesFromAcceptTypes(
params.accept_types)];
}
// Further panel configuration.
[openPanel setAllowsOtherFileTypes:YES];
[openPanel setAllowsMultipleSelection:
(params.mode == content::FileChooserParams::OpenMultiple)];
[openPanel setCanChooseFiles:YES];
[openPanel setCanChooseDirectories:NO];
// Show panel.
NSView* view = contents->GetNativeView();
[openPanel beginSheetModalForWindow:[view window] completionHandler:nil];
if ([openPanel runModal] == NSFileHandlingPanelOKButton) {
NSArray *urls = [openPanel URLs];
int i, count = [urls count];
for (i=0; i<count; i++) {
NSURL* url = [urls objectAtIndex:i];
if ([url isFileURL])
files.push_back(FilePath(base::SysNSStringToUTF8([url path])));
}
}
[NSApp endSheet:openPanel];
callback.Run(files);
}
void CefBrowserHostImpl::PlatformHandleExternalProtocol(const GURL& url) {

View File

@@ -13,6 +13,7 @@
#include "libcef/browser/thread_util.h"
#include "base/i18n/case_conversion.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "base/win/registry.h"
@@ -20,6 +21,10 @@
#include "content/public/browser/native_web_keyboard_event.h"
#include "content/public/browser/web_contents_view.h"
#include "content/public/common/file_chooser_params.h"
#include "grit/cef_strings.h"
#include "grit/ui_strings.h"
#include "net/base/mime_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/win/hwnd_util.h"
#pragma comment(lib, "dwmapi.lib")
@@ -52,10 +57,171 @@ void WriteTextToFile(const std::string& data, const std::wstring& file_path) {
fclose(fp);
}
// From ui/base/dialogs/select_file_dialog_win.cc.
// Get the file type description from the registry. This will be "Text Document"
// for .txt files, "JPEG Image" for .jpg files, etc. If the registry doesn't
// have an entry for the file type, we return false, true if the description was
// found. 'file_ext' must be in form ".txt".
static bool GetRegistryDescriptionFromExtension(const std::wstring& file_ext,
std::wstring* reg_description) {
DCHECK(reg_description);
base::win::RegKey reg_ext(HKEY_CLASSES_ROOT, file_ext.c_str(), KEY_READ);
std::wstring reg_app;
if (reg_ext.ReadValue(NULL, &reg_app) == ERROR_SUCCESS && !reg_app.empty()) {
base::win::RegKey reg_link(HKEY_CLASSES_ROOT, reg_app.c_str(), KEY_READ);
if (reg_link.ReadValue(NULL, reg_description) == ERROR_SUCCESS)
return true;
}
return false;
}
// Set up a filter for a Save/Open dialog, which will consist of |file_ext| file
// extensions (internally separated by semicolons), |ext_desc| as the text
// descriptions of the |file_ext| types (optional), and (optionally) the default
// 'All Files' view. The purpose of the filter is to show only files of a
// particular type in a Windows Save/Open dialog box. The resulting filter is
// returned. The filters created here are:
// 1. only files that have 'file_ext' as their extension
// 2. all files (only added if 'include_all_files' is true)
// Example:
// file_ext: { "*.txt", "*.htm;*.html" }
// ext_desc: { "Text Document" }
// returned: "Text Document\0*.txt\0HTML Document\0*.htm;*.html\0"
// "All Files\0*.*\0\0" (in one big string)
// If a description is not provided for a file extension, it will be retrieved
// from the registry. If the file extension does not exist in the registry, it
// will be omitted from the filter, as it is likely a bogus extension.
std::wstring FormatFilterForExtensions(
const std::vector<std::wstring>& file_ext,
const std::vector<std::wstring>& ext_desc,
bool include_all_files) {
const std::wstring all_ext = L"*.*";
const std::wstring all_desc =
l10n_util::GetStringUTF16(IDS_APP_SAVEAS_ALL_FILES) +
L" (" + all_ext + L")";
DCHECK(file_ext.size() >= ext_desc.size());
if (file_ext.empty())
include_all_files = true;
std::wstring result;
for (size_t i = 0; i < file_ext.size(); ++i) {
std::wstring ext = file_ext[i];
std::wstring desc;
if (i < ext_desc.size())
desc = ext_desc[i];
if (ext.empty()) {
// Force something reasonable to appear in the dialog box if there is no
// extension provided.
include_all_files = true;
continue;
}
if (desc.empty()) {
DCHECK(ext.find(L'.') != std::wstring::npos);
std::wstring first_extension = ext.substr(ext.find(L'.'));
size_t first_separator_index = first_extension.find(L';');
if (first_separator_index != std::wstring::npos)
first_extension = first_extension.substr(0, first_separator_index);
// Find the extension name without the preceeding '.' character.
std::wstring ext_name = first_extension;
size_t ext_index = ext_name.find_first_not_of(L'.');
if (ext_index != std::wstring::npos)
ext_name = ext_name.substr(ext_index);
if (!GetRegistryDescriptionFromExtension(first_extension, &desc)) {
// The extension doesn't exist in the registry. Create a description
// based on the unknown extension type (i.e. if the extension is .qqq,
// the we create a description "QQQ File (.qqq)").
include_all_files = true;
desc = l10n_util::GetStringFUTF16(
IDS_APP_SAVEAS_EXTENSION_FORMAT,
base::i18n::ToUpper(WideToUTF16(ext_name)),
ext_name);
}
}
if (!desc.empty())
desc += L" (" + ext + L")";
else
desc = ext;
result.append(desc.c_str(), desc.size() + 1); // Append NULL too.
result.append(ext.c_str(), ext.size() + 1);
}
if (include_all_files) {
result.append(all_desc.c_str(), all_desc.size() + 1);
result.append(all_ext.c_str(), all_ext.size() + 1);
}
result.append(1, '\0'); // Double NULL required.
return result;
}
std::wstring 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_APP_AUDIO_FILES },
{ "image", IDS_APP_IMAGE_FILES },
{ "text", IDS_APP_TEXT_FILES },
{ "video", IDS_APP_VIDEO_FILES },
};
for (size_t i = 0; i < arraysize(kWildCardMimeTypes); ++i) {
if (mime_type == std::string(kWildCardMimeTypes[i].mime_type) + "/*")
return l10n_util::GetStringUTF16(kWildCardMimeTypes[i].string_id);
}
return std::wstring();
}
std::wstring GetFilterStringFromAcceptTypes(
const std::vector<string16>& accept_types) {
std::vector<std::wstring> extensions;
std::vector<std::wstring> descriptions;
for (size_t i = 0; i < accept_types.size(); ++i) {
std::string ascii_type = UTF16ToASCII(accept_types[i]);
if (ascii_type.length()) {
// Just treat as extension if contains '.' as the first character.
if (ascii_type[0] == '.') {
extensions.push_back(L"*" + ASCIIToWide(ascii_type));
descriptions.push_back(std::wstring());
} else {
// Otherwise convert mime type to one or more extensions.
std::vector<FilePath::StringType> ext;
std::wstring ext_str;
net::GetExtensionsForMimeType(ascii_type, &ext);
if (ext.size() > 0) {
for (size_t x = 0; x < ext.size(); ++x) {
if (x != 0)
ext_str += L";";
ext_str += L"*." + ext[x];
}
extensions.push_back(ext_str);
descriptions.push_back(GetDescriptionFromMimeType(ascii_type));
}
}
}
}
return FormatFilterForExtensions(extensions, descriptions, true);
}
// from chrome/browser/views/shell_dialogs_win.cc
bool RunOpenFileDialog(const std::wstring& filter, HWND owner, FilePath* path) {
bool RunOpenFileDialog(const content::FileChooserParams& params,
HWND owner,
FilePath* path) {
OPENFILENAME ofn;
// We must do this otherwise the ofn's FlagsEx may be initialized to random
@@ -64,26 +230,48 @@ bool RunOpenFileDialog(const std::wstring& filter, HWND owner, FilePath* path) {
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = owner;
wchar_t filename[MAX_PATH];
base::wcslcpy(filename, path->value().c_str(), arraysize(filename));
// Consider default file name if any.
FilePath default_file_name(params.default_file_name);
wchar_t filename[MAX_PATH] = {0};
ofn.lpstrFile = filename;
ofn.nMaxFile = MAX_PATH;
std::wstring directory;
if (!default_file_name.empty()) {
base::wcslcpy(filename, default_file_name.value().c_str(),
arraysize(filename));
directory = default_file_name.DirName().value();
ofn.lpstrInitialDir = directory.c_str();
}
std::wstring title;
if (!params.title.empty())
title = params.title;
else
title = l10n_util::GetStringUTF16(IDS_OPEN_FILE_DIALOG_TITLE);
if (!title.empty())
ofn.lpstrTitle = title.c_str();
// We use OFN_NOCHANGEDIR so that the user can rename or delete the directory
// without having to close Chrome first.
ofn.Flags = OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR;
ofn.Flags = OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR | OFN_EXPLORER |
OFN_ENABLESIZING;
if (!filter.empty()) {
std::wstring filter = GetFilterStringFromAcceptTypes(params.accept_types);
if (!filter.empty())
ofn.lpstrFilter = filter.c_str();
}
bool success = !!GetOpenFileName(&ofn);
if (success)
*path = FilePath(filename);
return success;
}
bool RunOpenMultiFileDialog(const std::wstring& filter, HWND owner,
bool RunOpenMultiFileDialog(const content::FileChooserParams& params,
HWND owner,
std::vector<FilePath>* paths) {
OPENFILENAME ofn;
@@ -99,14 +287,23 @@ bool RunOpenMultiFileDialog(const std::wstring& filter, HWND owner,
ofn.lpstrFile = filename.get();
ofn.nMaxFile = UNICODE_STRING_MAX_CHARS;
std::wstring title;
if (!params.title.empty())
title = params.title;
else
title = l10n_util::GetStringUTF16(IDS_OPEN_FILES_DIALOG_TITLE);
if (!title.empty())
ofn.lpstrTitle = title.c_str();
// We use OFN_NOCHANGEDIR so that the user can rename or delete the directory
// without having to close Chrome first.
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER
| OFN_HIDEREADONLY | OFN_ALLOWMULTISELECT;
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER |
OFN_HIDEREADONLY | OFN_ALLOWMULTISELECT | OFN_ENABLESIZING;
if (!filter.empty()) {
std::wstring filter = GetFilterStringFromAcceptTypes(params.accept_types);
if (!filter.empty())
ofn.lpstrFilter = filter.c_str();
}
bool success = !!GetOpenFileName(&ofn);
if (success) {
@@ -135,6 +332,57 @@ bool RunOpenMultiFileDialog(const std::wstring& filter, HWND owner,
return success;
}
bool RunSaveFileDialog(const content::FileChooserParams& params,
HWND owner,
FilePath* path) {
OPENFILENAME ofn;
// We must do this otherwise the ofn's FlagsEx may be initialized to random
// junk in release builds which can cause the Places Bar not to show up!
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = owner;
// Consider default file name if any.
FilePath default_file_name(params.default_file_name);
wchar_t filename[MAX_PATH] = {0};
ofn.lpstrFile = filename;
ofn.nMaxFile = MAX_PATH;
std::wstring directory;
if (!default_file_name.empty()) {
base::wcslcpy(filename, default_file_name.value().c_str(),
arraysize(filename));
directory = default_file_name.DirName().value();
ofn.lpstrInitialDir = directory.c_str();
}
std::wstring title;
if (!params.title.empty())
title = params.title;
else
title = l10n_util::GetStringUTF16(IDS_SAVE_AS_DIALOG_TITLE);
if (!title.empty())
ofn.lpstrTitle = title.c_str();
// We use OFN_NOCHANGEDIR so that the user can rename or delete the directory
// without having to close Chrome first.
ofn.Flags = OFN_OVERWRITEPROMPT | OFN_EXPLORER | OFN_ENABLESIZING |
OFN_NOCHANGEDIR | OFN_PATHMUSTEXIST;
std::wstring filter = GetFilterStringFromAcceptTypes(params.accept_types);
if (!filter.empty())
ofn.lpstrFilter = filter.c_str();
bool success = !!GetSaveFileName(&ofn);
if (success)
*path = FilePath(filename);
return success;
}
// According to Mozilla in uriloader/exthandler/win/nsOSHelperAppService.cpp:
// "Some versions of windows (Win2k before SP3, Win XP before SP1) crash in
@@ -374,16 +622,25 @@ void CefBrowserHostImpl::PlatformHandleKeyboardEvent(
}
void CefBrowserHostImpl::PlatformRunFileChooser(
content::WebContents* contents,
const content::FileChooserParams& params,
std::vector<FilePath>& files) {
if (params.mode == content::FileChooserParams::OpenMultiple) {
RunOpenMultiFileDialog(L"", PlatformGetWindowHandle(), &files);
RunFileChooserCallback callback) {
std::vector<FilePath> files;
if (params.mode == content::FileChooserParams::Open) {
FilePath file;
if (RunOpenFileDialog(params, PlatformGetWindowHandle(), &file))
files.push_back(file);
} else if (params.mode == content::FileChooserParams::OpenMultiple) {
RunOpenMultiFileDialog(params, PlatformGetWindowHandle(), &files);
} else if (params.mode == content::FileChooserParams::Save) {
FilePath file;
if (RunSaveFileDialog(params, PlatformGetWindowHandle(), &file))
files.push_back(file);
} else {
FilePath file_name;
if (RunOpenFileDialog(L"", PlatformGetWindowHandle(), &file_name))
files.push_back(file_name);
NOTIMPLEMENTED();
}
callback.Run(files);
}
void CefBrowserHostImpl::PlatformHandleExternalProtocol(const GURL& url) {

View File

@@ -20,6 +20,7 @@
#include "content/public/browser/browser_context.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/file_chooser_params.h"
#include "net/base/net_util.h"
using content::DownloadItem;
@@ -135,23 +136,54 @@ class CefBeforeDownloadCallbackImpl : public CefBeforeDownloadCallback {
if (!item || !item->IsInProgress())
return;
FilePath result;
bool handled = false;
if (show_dialog) {
#if defined(OS_WIN) || defined(OS_MACOSX) || defined(TOOLKIT_GTK)
WebContents* web_contents = item->GetWebContents();
result = CefDownloadManagerDelegate::PlatformChooseDownloadPath(
web_contents, suggested_path);
#else
NOTIMPLEMENTED();
#endif
} else {
result = suggested_path;
CefRefPtr<CefBrowserHostImpl> browser =
CefBrowserHostImpl::GetBrowserForContents(web_contents);
if (browser.get()) {
handled = true;
content::FileChooserParams params;
params.mode = content::FileChooserParams::Save;
if (!suggested_path.empty()) {
params.default_file_name = suggested_path;
if (!suggested_path.Extension().empty()) {
params.accept_types.push_back(
CefString(suggested_path.Extension()));
}
}
browser->RunFileChooser(params,
base::Bind(
&CefBeforeDownloadCallbackImpl::ChooseDownloadPathCallback,
callback));
}
}
callback.Run(result,
if (!handled) {
callback.Run(suggested_path,
DownloadItem::TARGET_DISPOSITION_OVERWRITE,
content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
suggested_path);
}
}
static void ChooseDownloadPathCallback(
const content::DownloadTargetCallback& callback,
const std::vector<FilePath>& file_paths) {
DCHECK_LE(file_paths.size(), (size_t) 1);
FilePath path;
if (file_paths.size() > 0)
path = file_paths.front();
// The download will be cancelled if |path| is empty.
callback.Run(path,
DownloadItem::TARGET_DISPOSITION_OVERWRITE,
content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
result);
path);
}
int32 download_id_;

View File

@@ -31,9 +31,6 @@ class CefDownloadManagerDelegate
virtual void UpdateItemInPersistentStore(
content::DownloadItem* item) OVERRIDE;
static FilePath PlatformChooseDownloadPath(content::WebContents* web_contents,
const FilePath& suggested_path);
private:
friend class base::RefCountedThreadSafe<CefDownloadManagerDelegate>;

View File

@@ -1,43 +0,0 @@
// Copyright (c) 2012 The Chromium Embedded Framework Authors.
// Portions copyright (c) 2011 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/download_manager_delegate.h"
#include <gtk/gtk.h>
#include "base/string_util.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_view.h"
// static
FilePath CefDownloadManagerDelegate::PlatformChooseDownloadPath(
content::WebContents* web_contents,
const FilePath& suggested_path) {
FilePath result;
gfx::NativeWindow parent_window =
web_contents->GetView()->GetTopLevelNativeWindow();
std::string base_name = suggested_path.BaseName().value();
GtkWidget* dialog = gtk_file_chooser_dialog_new(
"Save File",
parent_window,
GTK_FILE_CHOOSER_ACTION_SAVE,
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
NULL);
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog),
TRUE);
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog),
base_name.c_str());
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
result = FilePath(filename);
}
gtk_widget_destroy(dialog);
return result;
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) 2012 The Chromium Embedded Framework Authors.
// Portions copyright (c) 2011 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/download_manager_delegate.h"
#import <Cocoa/Cocoa.h>
#include "base/sys_string_conversions.h"
#include "content/public/browser/web_contents.h"
// static
FilePath CefDownloadManagerDelegate::PlatformChooseDownloadPath(
content::WebContents* web_contents,
const FilePath& suggested_path) {
FilePath result;
NSSavePanel* savePanel = [NSSavePanel savePanel];
if (!suggested_path.BaseName().empty()) {
NSString* defaultName = base::SysUTF8ToNSString(
suggested_path.BaseName().value());
[savePanel setNameFieldStringValue:defaultName];
}
if (!suggested_path.DirName().empty()) {
NSString* defaultDir = base::SysUTF8ToNSString(
suggested_path.DirName().value());
[savePanel setDirectoryURL:[NSURL fileURLWithPath:defaultDir]];
}
NSView* view = web_contents->GetNativeView();
[savePanel beginSheetModalForWindow:[view window] completionHandler:nil];
if ([savePanel runModal] == NSFileHandlingPanelOKButton) {
NSURL * url = [savePanel URL];
NSString* path = [url path];
result = FilePath([path UTF8String]);
}
[NSApp endSheet:savePanel];
return result;
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) 2012 The Chromium Embedded Framework Authors.
// Portions copyright (c) 2011 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/download_manager_delegate.h"
#include <commdlg.h>
#include <windows.h>
#include "base/string_util.h"
#include "content/public/browser/web_contents.h"
// static
FilePath CefDownloadManagerDelegate::PlatformChooseDownloadPath(
content::WebContents* web_contents,
const FilePath& suggested_path) {
FilePath result;
std::wstring file_part = FilePath(suggested_path).BaseName().value();
wchar_t file_name[MAX_PATH];
base::wcslcpy(file_name, file_part.c_str(), arraysize(file_name));
OPENFILENAME save_as;
ZeroMemory(&save_as, sizeof(save_as));
save_as.lStructSize = sizeof(OPENFILENAME);
save_as.hwndOwner = web_contents->GetNativeView();
save_as.lpstrFile = file_name;
save_as.nMaxFile = arraysize(file_name);
std::wstring directory;
if (!suggested_path.empty())
directory = suggested_path.DirName().value();
save_as.lpstrInitialDir = directory.c_str();
save_as.Flags = OFN_OVERWRITEPROMPT | OFN_EXPLORER | OFN_ENABLESIZING |
OFN_NOCHANGEDIR | OFN_PATHMUSTEXIST;
if (GetSaveFileName(&save_as))
result = FilePath(std::wstring(save_as.lpstrFile));
return result;
}

View File

@@ -162,6 +162,18 @@ need to be translated for each locale.-->
<message name="IDS_MENU_VIEW_SOURCE" desc="The text label for the View Source... menu item.">
View Source...
</message>
<message name="IDS_APP_AUDIO_FILES" desc="The text label for the Audio Files filter in file open/save dialogs.">
Audio Files
</message>
<message name="IDS_APP_IMAGE_FILES" desc="The text label for the Image Files filter in file open/save dialogs.">
Image Files
</message>
<message name="IDS_APP_TEXT_FILES" desc="The text label for the Text Files filter in file open/save dialogs.">
Text Files
</message>
<message name="IDS_APP_VIDEO_FILES" desc="The text label for the Video Files filter in file open/save dialogs.">
Video Files
</message>
</messages>
</release>
</grit>