Implement support for the XDG file chooser portal
This allows use on Linux of the XDG portal for file selection rather than directly calling out to the GTK/KDE dialogs. When the feature is enabled, a test is run asynchronously with the main thread's UI initialization to determine if the file chooser portal is available on the bus, and if so, it will be used as the default once the first dialog is opened. In order for this to work, support is added to the GtkUi platform code to "export" a window handle that can be shared with other clients of the display server (a hex ID on X11 or an xdg-foreign handle on Wayland). This handle can then be passed to the portal to ensure a transient dialog is created. Bug: 885292 Change-Id: I98ac13e93af59bc5069b68fac5c0767b93502391 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2992214 Reviewed-by: Thomas Anderson <thomasanderson@chromium.org> Reviewed-by: Nick Yamane <nickdiego@igalia.com> Reviewed-by: Ryo Hashimoto <hashimoto@chromium.org> Commit-Queue: Thomas Anderson <thomasanderson@chromium.org> Cr-Commit-Position: refs/heads/master@{#897839}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
23ab6d80c5
commit
2fa9c43b74
1
AUTHORS
1
AUTHORS
@ -930,6 +930,7 @@ Ruiyi Luo <luoruiyi2008@gmail.com>
|
||||
Rulong Chen <rulong.crl@alibaba-inc.com>
|
||||
Russell Davis <russell.davis@gmail.com>
|
||||
Ryan Ackley <ryanackley@gmail.com>
|
||||
Ryan Gonzalez <rymg19@gmail.com>
|
||||
Ryan Norton <rnorton10@gmail.com>
|
||||
Ryan Sleevi <ryan-chromium-dev@sleevi.com>
|
||||
Ryan Yoakum <ryoakum@skobalt.com>
|
||||
|
@ -27,7 +27,7 @@ LinuxUiDelegate::~LinuxUiDelegate() {
|
||||
instance_ = nullptr;
|
||||
}
|
||||
|
||||
bool LinuxUiDelegate::SetWidgetTransientFor(
|
||||
bool LinuxUiDelegate::ExportWindowHandle(
|
||||
uint32_t parent_widget,
|
||||
base::OnceCallback<void(const std::string&)> callback) {
|
||||
// This function should not be called when using a platform that doesn't
|
||||
|
@ -28,7 +28,7 @@ class COMPONENT_EXPORT(UI_BASE) LinuxUiDelegate {
|
||||
virtual LinuxUiBackend GetBackend() const = 0;
|
||||
|
||||
// Only implemented on Wayland.
|
||||
virtual bool SetWidgetTransientFor(
|
||||
virtual bool ExportWindowHandle(
|
||||
uint32_t parent_widget,
|
||||
base::OnceCallback<void(const std::string&)> callback);
|
||||
|
||||
|
@ -69,6 +69,8 @@ component("gtk") {
|
||||
"select_file_dialog_impl_gtk.cc",
|
||||
"select_file_dialog_impl_gtk.h",
|
||||
"select_file_dialog_impl_kde.cc",
|
||||
"select_file_dialog_impl_portal.cc",
|
||||
"select_file_dialog_impl_portal.h",
|
||||
"settings_provider.h",
|
||||
"settings_provider_gtk.cc",
|
||||
"settings_provider_gtk.h",
|
||||
@ -90,6 +92,8 @@ component("gtk") {
|
||||
deps = [
|
||||
":gtk_stubs",
|
||||
"//base",
|
||||
"//components/dbus/thread_linux",
|
||||
"//dbus",
|
||||
"//skia",
|
||||
|
||||
# GTK pulls pangoft2, which requires HarfBuzz symbols. When linking
|
||||
@ -115,6 +119,7 @@ component("gtk") {
|
||||
"//ui/shell_dialogs",
|
||||
"//ui/strings",
|
||||
"//ui/views",
|
||||
"//url",
|
||||
]
|
||||
|
||||
if (enable_basic_printing) {
|
||||
|
@ -1,5 +1,7 @@
|
||||
include_rules = [
|
||||
"+chrome/browser/themes/theme_properties.h",
|
||||
"+components/dbus/thread_linux",
|
||||
"+dbus",
|
||||
"+printing",
|
||||
"+third_party/skia",
|
||||
"+ui/aura",
|
||||
|
@ -330,6 +330,8 @@ GtkUi::GtkUi() {
|
||||
auto backend = delegate ? delegate->GetBackend() : ui::LinuxUiBackend::kX11;
|
||||
platform_ = CreateGtkUiPlatform(backend);
|
||||
|
||||
SelectFileDialogImpl::Initialize();
|
||||
|
||||
// Avoid GTK initializing atk-bridge, and let AuraLinux implementation
|
||||
// do it once it is ready.
|
||||
std::unique_ptr<base::Environment> env(base::Environment::Create());
|
||||
@ -346,6 +348,8 @@ GtkUi::GtkUi() {
|
||||
GtkUi::~GtkUi() {
|
||||
DCHECK_EQ(g_gtk_ui, this);
|
||||
g_gtk_ui = nullptr;
|
||||
|
||||
SelectFileDialogImpl::Shutdown();
|
||||
}
|
||||
|
||||
// static
|
||||
|
@ -5,8 +5,11 @@
|
||||
#ifndef UI_GTK_GTK_UI_PLATFORM_H_
|
||||
#define UI_GTK_GTK_UI_PLATFORM_H_
|
||||
|
||||
#include "base/callback_forward.h"
|
||||
#include "ui/gfx/native_widget_types.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
using GdkKeymap = struct _GdkKeymap;
|
||||
using GtkWindow = struct _GtkWindow;
|
||||
using GtkWidget = struct _GtkWidget;
|
||||
@ -33,6 +36,12 @@ class GtkUiPlatform {
|
||||
// and is supported only in X11 backend (both Aura and Ozone).
|
||||
virtual GdkWindow* GetGdkWindow(gfx::AcceleratedWidget window_id) = 0;
|
||||
|
||||
// Exports a prefixed, platform-dependent (X11 or Wayland) window handle for
|
||||
// an Aura window id, then calls the given callback with the handle.
|
||||
virtual bool ExportWindowHandle(
|
||||
gfx::AcceleratedWidget window_id,
|
||||
base::OnceCallback<void(std::string)> callback) = 0;
|
||||
|
||||
// Gtk dialog windows must be set transient for the browser window. This
|
||||
// function abstracts away such functionality.
|
||||
virtual bool SetGtkWidgetTransientFor(GtkWidget* widget,
|
||||
|
@ -9,6 +9,7 @@
|
||||
#include "base/command_line.h"
|
||||
#include "base/memory/ptr_util.h"
|
||||
#include "base/test/scoped_feature_list.h"
|
||||
#include "base/test/task_environment.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
#include "ui/base/ui_base_features.h"
|
||||
#include "ui/gtk/gtk_ui_factory.h"
|
||||
@ -57,6 +58,7 @@ class NativeThemeGtkRedirectedEquivalenceTest
|
||||
}
|
||||
}
|
||||
|
||||
base::test::TaskEnvironment task_environment_;
|
||||
std::unique_ptr<views::LinuxUI> gtk_ui_;
|
||||
};
|
||||
|
||||
|
@ -10,13 +10,16 @@
|
||||
#include "base/files/file_util.h"
|
||||
#include "base/nix/xdg_util.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "base/notreached.h"
|
||||
#include "base/threading/thread_restrictions.h"
|
||||
#include "ui/gtk/select_file_dialog_impl_portal.h"
|
||||
|
||||
namespace {
|
||||
|
||||
enum UseKdeFileDialogStatus { UNKNOWN, NO_KDE, YES_KDE };
|
||||
enum FileDialogChoice { kUnknown, kGtk, kKde, kPortal };
|
||||
|
||||
FileDialogChoice dialog_choice_ = kUnknown;
|
||||
|
||||
UseKdeFileDialogStatus use_kde_ = UNKNOWN;
|
||||
std::string& KDialogVersion() {
|
||||
static base::NoDestructor<std::string> version;
|
||||
return *version;
|
||||
@ -29,42 +32,68 @@ namespace gtk {
|
||||
base::FilePath* SelectFileDialogImpl::last_saved_path_ = nullptr;
|
||||
base::FilePath* SelectFileDialogImpl::last_opened_path_ = nullptr;
|
||||
|
||||
// static
|
||||
void SelectFileDialogImpl::Initialize() {
|
||||
SelectFileDialogImplPortal::StartAvailabilityTestInBackground();
|
||||
}
|
||||
|
||||
// static
|
||||
void SelectFileDialogImpl::Shutdown() {
|
||||
SelectFileDialogImplPortal::DestroyPortalConnection();
|
||||
}
|
||||
|
||||
// static
|
||||
ui::SelectFileDialog* SelectFileDialogImpl::Create(
|
||||
ui::SelectFileDialog::Listener* listener,
|
||||
std::unique_ptr<ui::SelectFilePolicy> policy) {
|
||||
if (use_kde_ == UNKNOWN) {
|
||||
// Start out assumimg we are not going to use KDE.
|
||||
use_kde_ = NO_KDE;
|
||||
if (dialog_choice_ == kUnknown) {
|
||||
// Start out assumimg we are going to use GTK.
|
||||
dialog_choice_ = kGtk;
|
||||
|
||||
// Check to see if KDE is the desktop environment.
|
||||
std::unique_ptr<base::Environment> env(base::Environment::Create());
|
||||
base::nix::DesktopEnvironment desktop =
|
||||
base::nix::GetDesktopEnvironment(env.get());
|
||||
if (desktop == base::nix::DESKTOP_ENVIRONMENT_KDE3 ||
|
||||
desktop == base::nix::DESKTOP_ENVIRONMENT_KDE4 ||
|
||||
desktop == base::nix::DESKTOP_ENVIRONMENT_KDE5) {
|
||||
// Check to see if the user dislikes the KDE file dialog.
|
||||
if (!env->HasVar("NO_CHROME_KDE_FILE_DIALOG")) {
|
||||
// Check to see if the KDE dialog works.
|
||||
if (SelectFileDialogImpl::CheckKDEDialogWorksOnUIThread(
|
||||
KDialogVersion())) {
|
||||
use_kde_ = YES_KDE;
|
||||
// Check to see if the portal is available.
|
||||
if (SelectFileDialogImplPortal::IsPortalAvailable()) {
|
||||
dialog_choice_ = kPortal;
|
||||
} else {
|
||||
// Make sure to kill the portal connection.
|
||||
SelectFileDialogImplPortal::DestroyPortalConnection();
|
||||
|
||||
// Check to see if KDE is the desktop environment.
|
||||
std::unique_ptr<base::Environment> env(base::Environment::Create());
|
||||
base::nix::DesktopEnvironment desktop =
|
||||
base::nix::GetDesktopEnvironment(env.get());
|
||||
if (desktop == base::nix::DESKTOP_ENVIRONMENT_KDE3 ||
|
||||
desktop == base::nix::DESKTOP_ENVIRONMENT_KDE4 ||
|
||||
desktop == base::nix::DESKTOP_ENVIRONMENT_KDE5) {
|
||||
// Check to see if the user dislikes the KDE file dialog.
|
||||
if (!env->HasVar("NO_CHROME_KDE_FILE_DIALOG")) {
|
||||
// Check to see if the KDE dialog works.
|
||||
if (SelectFileDialogImpl::CheckKDEDialogWorksOnUIThread(
|
||||
KDialogVersion())) {
|
||||
dialog_choice_ = kKde;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (use_kde_ == NO_KDE) {
|
||||
return SelectFileDialogImpl::NewSelectFileDialogImplGTK(listener,
|
||||
std::move(policy));
|
||||
switch (dialog_choice_) {
|
||||
case kGtk:
|
||||
return SelectFileDialogImpl::NewSelectFileDialogImplGTK(
|
||||
listener, std::move(policy));
|
||||
case kPortal:
|
||||
return SelectFileDialogImpl::NewSelectFileDialogImplPortal(
|
||||
listener, std::move(policy));
|
||||
case kKde: {
|
||||
std::unique_ptr<base::Environment> env(base::Environment::Create());
|
||||
base::nix::DesktopEnvironment desktop =
|
||||
base::nix::GetDesktopEnvironment(env.get());
|
||||
return SelectFileDialogImpl::NewSelectFileDialogImplKDE(
|
||||
listener, std::move(policy), desktop, KDialogVersion());
|
||||
}
|
||||
case kUnknown:
|
||||
NOTREACHED();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<base::Environment> env(base::Environment::Create());
|
||||
base::nix::DesktopEnvironment desktop =
|
||||
base::nix::GetDesktopEnvironment(env.get());
|
||||
return SelectFileDialogImpl::NewSelectFileDialogImplKDE(
|
||||
listener, std::move(policy), desktop, KDialogVersion());
|
||||
}
|
||||
|
||||
SelectFileDialogImpl::SelectFileDialogImpl(
|
||||
|
@ -24,6 +24,9 @@ namespace gtk {
|
||||
// Shared implementation SelectFileDialog used by SelectFileDialogImplGTK
|
||||
class SelectFileDialogImpl : public ui::SelectFileDialog {
|
||||
public:
|
||||
static void Initialize();
|
||||
static void Shutdown();
|
||||
|
||||
// Main factory method which returns correct type.
|
||||
static ui::SelectFileDialog* Create(
|
||||
Listener* listener,
|
||||
@ -39,6 +42,10 @@ class SelectFileDialogImpl : public ui::SelectFileDialog {
|
||||
std::unique_ptr<ui::SelectFilePolicy> policy,
|
||||
base::nix::DesktopEnvironment desktop,
|
||||
const std::string& kdialog_version);
|
||||
// Factory method for creating an XDG portal-backed SelectFileDialogImpl
|
||||
static SelectFileDialogImpl* NewSelectFileDialogImplPortal(
|
||||
Listener* listener,
|
||||
std::unique_ptr<ui::SelectFilePolicy> policy);
|
||||
|
||||
// Returns true if the SelectFileDialog class returned by
|
||||
// NewSelectFileDialogImplKDE will actually work.
|
||||
|
796
ui/gtk/select_file_dialog_impl_portal.cc
Normal file
796
ui/gtk/select_file_dialog_impl_portal.cc
Normal file
@ -0,0 +1,796 @@
|
||||
// Copyright 2021 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 "ui/gtk/select_file_dialog_impl_portal.h"
|
||||
|
||||
#include "base/bind.h"
|
||||
#include "base/containers/contains.h"
|
||||
#include "base/logging.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "base/notreached.h"
|
||||
#include "base/strings/string_piece.h"
|
||||
#include "base/strings/stringprintf.h"
|
||||
#include "components/dbus/thread_linux/dbus_thread_linux.h"
|
||||
#include "dbus/object_path.h"
|
||||
#include "dbus/property.h"
|
||||
#include "ui/aura/window_tree_host.h"
|
||||
#include "ui/base/l10n/l10n_util.h"
|
||||
#include "ui/base/linux/linux_ui_delegate.h"
|
||||
#include "ui/gfx/native_widget_types.h"
|
||||
#include "ui/gtk/gtk_ui.h"
|
||||
#include "ui/strings/grit/ui_strings.h"
|
||||
#include "url/url_util.h"
|
||||
|
||||
namespace gtk {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kDBusMethodNameHasOwner[] = "NameHasOwner";
|
||||
constexpr char kDBusMethodListActivatableNames[] = "ListActivatableNames";
|
||||
|
||||
constexpr char kXdgPortalService[] = "org.freedesktop.portal.Desktop";
|
||||
constexpr char kXdgPortalObject[] = "/org/freedesktop/portal/desktop";
|
||||
|
||||
constexpr int kXdgPortalRequiredVersion = 3;
|
||||
|
||||
constexpr char kXdgPortalRequestInterfaceName[] =
|
||||
"org.freedesktop.portal.Request";
|
||||
constexpr char kXdgPortalResponseSignal[] = "Response";
|
||||
|
||||
constexpr char kFileChooserInterfaceName[] =
|
||||
"org.freedesktop.portal.FileChooser";
|
||||
|
||||
constexpr char kFileChooserMethodOpenFile[] = "OpenFile";
|
||||
constexpr char kFileChooserMethodSaveFile[] = "SaveFile";
|
||||
|
||||
constexpr char kFileChooserOptionHandleToken[] = "handle_token";
|
||||
constexpr char kFileChooserOptionAcceptLabel[] = "accept_label";
|
||||
constexpr char kFileChooserOptionMultiple[] = "multiple";
|
||||
constexpr char kFileChooserOptionDirectory[] = "directory";
|
||||
constexpr char kFileChooserOptionFilters[] = "filters";
|
||||
constexpr char kFileChooserOptionCurrentFilter[] = "current_filter";
|
||||
constexpr char kFileChooserOptionCurrentFolder[] = "current_folder";
|
||||
constexpr char kFileChooserOptionCurrentName[] = "current_name";
|
||||
|
||||
constexpr int kFileChooserFilterKindGlob = 0;
|
||||
|
||||
constexpr char kFileUriPrefix[] = "file://";
|
||||
|
||||
struct FileChooserProperties : dbus::PropertySet {
|
||||
dbus::Property<uint32_t> version;
|
||||
|
||||
explicit FileChooserProperties(dbus::ObjectProxy* object_proxy)
|
||||
: dbus::PropertySet(object_proxy, kFileChooserInterfaceName, {}) {
|
||||
RegisterProperty("version", &version);
|
||||
}
|
||||
|
||||
~FileChooserProperties() override = default;
|
||||
};
|
||||
|
||||
void AppendStringOption(dbus::MessageWriter* writer,
|
||||
const std::string& name,
|
||||
const std::string& value) {
|
||||
dbus::MessageWriter option_writer(nullptr);
|
||||
writer->OpenDictEntry(&option_writer);
|
||||
|
||||
option_writer.AppendString(name);
|
||||
option_writer.AppendVariantOfString(value);
|
||||
|
||||
writer->CloseContainer(&option_writer);
|
||||
}
|
||||
|
||||
void AppendByteStringOption(dbus::MessageWriter* writer,
|
||||
const std::string& name,
|
||||
const std::string& value) {
|
||||
dbus::MessageWriter option_writer(nullptr);
|
||||
writer->OpenDictEntry(&option_writer);
|
||||
|
||||
option_writer.AppendString(name);
|
||||
|
||||
dbus::MessageWriter value_writer(nullptr);
|
||||
option_writer.OpenVariant("ay", &value_writer);
|
||||
|
||||
value_writer.AppendArrayOfBytes(
|
||||
reinterpret_cast<const std::uint8_t*>(value.c_str()),
|
||||
// size + 1 will include the null terminator.
|
||||
value.size() + 1);
|
||||
|
||||
option_writer.CloseContainer(&value_writer);
|
||||
writer->CloseContainer(&option_writer);
|
||||
}
|
||||
|
||||
void AppendBoolOption(dbus::MessageWriter* writer,
|
||||
const std::string& name,
|
||||
bool value) {
|
||||
dbus::MessageWriter option_writer(nullptr);
|
||||
writer->OpenDictEntry(&option_writer);
|
||||
|
||||
option_writer.AppendString(name);
|
||||
option_writer.AppendVariantOfBool(value);
|
||||
|
||||
writer->CloseContainer(&option_writer);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// static
|
||||
SelectFileDialogImpl* SelectFileDialogImpl::NewSelectFileDialogImplPortal(
|
||||
Listener* listener,
|
||||
std::unique_ptr<ui::SelectFilePolicy> policy) {
|
||||
return new SelectFileDialogImplPortal(listener, std::move(policy));
|
||||
}
|
||||
|
||||
SelectFileDialogImplPortal::SelectFileDialogImplPortal(
|
||||
Listener* listener,
|
||||
std::unique_ptr<ui::SelectFilePolicy> policy)
|
||||
: SelectFileDialogImpl(listener, std::move(policy)) {}
|
||||
|
||||
SelectFileDialogImplPortal::~SelectFileDialogImplPortal() = default;
|
||||
|
||||
// static
|
||||
void SelectFileDialogImplPortal::StartAvailabilityTestInBackground() {
|
||||
if (GetAvailabilityTestCompletionFlag()->IsSet())
|
||||
return;
|
||||
|
||||
dbus_thread_linux::GetTaskRunner()->PostTask(
|
||||
FROM_HERE,
|
||||
base::BindOnce(
|
||||
&SelectFileDialogImplPortal::CheckPortalAvailabilityOnBusThread));
|
||||
}
|
||||
|
||||
// static
|
||||
bool SelectFileDialogImplPortal::IsPortalAvailable() {
|
||||
if (!GetAvailabilityTestCompletionFlag()->IsSet())
|
||||
LOG(WARNING) << "Portal availiability checked before test was complete";
|
||||
|
||||
return is_portal_available_;
|
||||
}
|
||||
|
||||
// static
|
||||
void SelectFileDialogImplPortal::DestroyPortalConnection() {
|
||||
dbus_thread_linux::GetTaskRunner()->PostTask(
|
||||
FROM_HERE,
|
||||
base::BindOnce(&SelectFileDialogImplPortal::DestroyBusOnBusThread));
|
||||
}
|
||||
|
||||
bool SelectFileDialogImplPortal::IsRunning(
|
||||
gfx::NativeWindow parent_window) const {
|
||||
if (parent_window && parent_window->GetHost()) {
|
||||
auto window = parent_window->GetHost()->GetAcceleratedWidget();
|
||||
return parents_.find(window) != parents_.end();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::SelectFileImpl(
|
||||
Type type,
|
||||
const std::u16string& title,
|
||||
const base::FilePath& default_path,
|
||||
const FileTypeInfo* file_types,
|
||||
int file_type_index,
|
||||
const base::FilePath::StringType& default_extension,
|
||||
gfx::NativeWindow owning_window,
|
||||
void* params) {
|
||||
auto info = base::MakeRefCounted<DialogInfo>();
|
||||
info->type = type;
|
||||
info->main_task_runner = base::SequencedTaskRunnerHandle::Get();
|
||||
info->listener_params = params;
|
||||
|
||||
if (owning_window && owning_window->GetHost()) {
|
||||
info->parent = owning_window->GetHost()->GetAcceleratedWidget();
|
||||
parents_.insert(*info->parent);
|
||||
}
|
||||
|
||||
if (file_types)
|
||||
file_types_ = *file_types;
|
||||
|
||||
file_type_index_ = file_type_index;
|
||||
|
||||
PortalFilterSet filter_set = BuildFilterSet();
|
||||
|
||||
auto parent_handle_callback = base::BindOnce(
|
||||
&SelectFileDialogImplPortal::SelectFileImplWithParentHandle,
|
||||
base::Unretained(this),
|
||||
// We don't move info, because it will be needed below to cancel the open
|
||||
// on error.
|
||||
info, std::move(title), std::move(default_path), std::move(filter_set),
|
||||
std::move(default_extension));
|
||||
|
||||
if (info->parent) {
|
||||
if (!GtkUi::GetPlatform()->ExportWindowHandle(
|
||||
*info->parent, std::move(parent_handle_callback))) {
|
||||
LOG(ERROR) << "Failed to export window handle for portal select dialog";
|
||||
CancelOpenOnMainThread(std::move(info));
|
||||
}
|
||||
} else {
|
||||
// No parent, so just use a blank parent handle.
|
||||
std::move(parent_handle_callback).Run("");
|
||||
}
|
||||
}
|
||||
|
||||
bool SelectFileDialogImplPortal::HasMultipleFileTypeChoicesImpl() {
|
||||
return file_types_.extensions.size() > 1;
|
||||
}
|
||||
|
||||
// static
|
||||
void SelectFileDialogImplPortal::CheckPortalAvailabilityOnBusThread() {
|
||||
base::AtomicFlag* availability_test_complete =
|
||||
GetAvailabilityTestCompletionFlag();
|
||||
if (availability_test_complete->IsSet())
|
||||
return;
|
||||
|
||||
dbus::Bus* bus = AcquireBusOnBusThread();
|
||||
|
||||
dbus::ObjectProxy* dbus_proxy =
|
||||
bus->GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
|
||||
|
||||
if (IsPortalRunningOnBusThread(dbus_proxy) ||
|
||||
IsPortalActivatableOnBusThread(dbus_proxy)) {
|
||||
dbus::ObjectPath portal_path(kXdgPortalObject);
|
||||
dbus::ObjectProxy* portal =
|
||||
bus->GetObjectProxy(kXdgPortalService, portal_path);
|
||||
|
||||
FileChooserProperties properties(portal);
|
||||
if (!properties.GetAndBlock(&properties.version)) {
|
||||
LOG(ERROR) << "Failed to read portal version property";
|
||||
} else if (properties.version.value() >= kXdgPortalRequiredVersion) {
|
||||
is_portal_available_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
VLOG(1) << "File chooser portal available: "
|
||||
<< (is_portal_available_ ? "yes" : "no");
|
||||
availability_test_complete->Set();
|
||||
}
|
||||
|
||||
// static
|
||||
bool SelectFileDialogImplPortal::IsPortalRunningOnBusThread(
|
||||
dbus::ObjectProxy* dbus_proxy) {
|
||||
dbus::MethodCall method_call(DBUS_INTERFACE_DBUS, kDBusMethodNameHasOwner);
|
||||
dbus::MessageWriter writer(&method_call);
|
||||
writer.AppendString(kXdgPortalService);
|
||||
|
||||
std::unique_ptr<dbus::Response> response = dbus_proxy->CallMethodAndBlock(
|
||||
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
|
||||
if (!response)
|
||||
return false;
|
||||
|
||||
dbus::MessageReader reader(response.get());
|
||||
bool owned = false;
|
||||
if (!reader.PopBool(&owned)) {
|
||||
LOG(ERROR) << "Failed to read response";
|
||||
return false;
|
||||
}
|
||||
|
||||
return owned;
|
||||
}
|
||||
|
||||
// static
|
||||
bool SelectFileDialogImplPortal::IsPortalActivatableOnBusThread(
|
||||
dbus::ObjectProxy* dbus_proxy) {
|
||||
dbus::MethodCall method_call(DBUS_INTERFACE_DBUS,
|
||||
kDBusMethodListActivatableNames);
|
||||
|
||||
std::unique_ptr<dbus::Response> response = dbus_proxy->CallMethodAndBlock(
|
||||
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
|
||||
if (!response)
|
||||
return false;
|
||||
|
||||
dbus::MessageReader reader(response.get());
|
||||
std::vector<std::string> names;
|
||||
if (!reader.PopArrayOfStrings(&names)) {
|
||||
LOG(ERROR) << "Failed to read response";
|
||||
return false;
|
||||
}
|
||||
|
||||
return base::Contains(names, kXdgPortalService);
|
||||
}
|
||||
|
||||
SelectFileDialogImplPortal::PortalFilter::PortalFilter() = default;
|
||||
SelectFileDialogImplPortal::PortalFilter::PortalFilter(
|
||||
const PortalFilter& other) = default;
|
||||
SelectFileDialogImplPortal::PortalFilter::PortalFilter(PortalFilter&& other) =
|
||||
default;
|
||||
SelectFileDialogImplPortal::PortalFilter::~PortalFilter() = default;
|
||||
|
||||
SelectFileDialogImplPortal::PortalFilterSet::PortalFilterSet() = default;
|
||||
SelectFileDialogImplPortal::PortalFilterSet::PortalFilterSet(
|
||||
const PortalFilterSet& other) = default;
|
||||
SelectFileDialogImplPortal::PortalFilterSet::PortalFilterSet(
|
||||
PortalFilterSet&& other) = default;
|
||||
SelectFileDialogImplPortal::PortalFilterSet::~PortalFilterSet() = default;
|
||||
|
||||
SelectFileDialogImplPortal::DialogInfo::DialogInfo() = default;
|
||||
SelectFileDialogImplPortal::DialogInfo::~DialogInfo() = default;
|
||||
|
||||
// static
|
||||
scoped_refptr<dbus::Bus>*
|
||||
SelectFileDialogImplPortal::AcquireBusStorageOnBusThread() {
|
||||
static base::NoDestructor<scoped_refptr<dbus::Bus>> bus(nullptr);
|
||||
if (!*bus) {
|
||||
dbus::Bus::Options options;
|
||||
options.bus_type = dbus::Bus::SESSION;
|
||||
options.connection_type = dbus::Bus::PRIVATE;
|
||||
options.dbus_task_runner = dbus_thread_linux::GetTaskRunner();
|
||||
|
||||
*bus = base::MakeRefCounted<dbus::Bus>(options);
|
||||
}
|
||||
|
||||
return bus.get();
|
||||
}
|
||||
|
||||
// static
|
||||
dbus::Bus* SelectFileDialogImplPortal::AcquireBusOnBusThread() {
|
||||
return AcquireBusStorageOnBusThread()->get();
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::DestroyBusOnBusThread() {
|
||||
scoped_refptr<dbus::Bus>* bus_storage = AcquireBusStorageOnBusThread();
|
||||
(*bus_storage)->ShutdownAndBlock();
|
||||
|
||||
// If the connection is restarted later on, we need to make sure the entire
|
||||
// bus is newly created. Otherwise, references to an old, invalid task runner
|
||||
// may persist.
|
||||
bus_storage->reset();
|
||||
}
|
||||
|
||||
// static
|
||||
base::AtomicFlag*
|
||||
SelectFileDialogImplPortal::GetAvailabilityTestCompletionFlag() {
|
||||
static base::NoDestructor<base::AtomicFlag> flag;
|
||||
return flag.get();
|
||||
}
|
||||
|
||||
SelectFileDialogImplPortal::PortalFilterSet
|
||||
SelectFileDialogImplPortal::BuildFilterSet() {
|
||||
PortalFilterSet filter_set;
|
||||
|
||||
for (size_t i = 0; i < file_types_.extensions.size(); ++i) {
|
||||
PortalFilter filter;
|
||||
|
||||
for (const std::string& extension : file_types_.extensions[i]) {
|
||||
if (extension.empty())
|
||||
continue;
|
||||
|
||||
filter.patterns.insert(base::StringPrintf("*.%s", extension.c_str()));
|
||||
}
|
||||
|
||||
if (filter.patterns.empty())
|
||||
continue;
|
||||
|
||||
// If there is no matching description, use a default description based on
|
||||
// the filter.
|
||||
if (i < file_types_.extension_description_overrides.size()) {
|
||||
filter.name =
|
||||
base::UTF16ToUTF8(file_types_.extension_description_overrides[i]);
|
||||
} else {
|
||||
std::vector<std::string> patterns_vector(filter.patterns.begin(),
|
||||
filter.patterns.end());
|
||||
filter.name = base::JoinString(patterns_vector, ",");
|
||||
}
|
||||
|
||||
if (i == file_type_index_)
|
||||
filter_set.default_filter = filter;
|
||||
|
||||
filter_set.filters.push_back(std::move(filter));
|
||||
}
|
||||
|
||||
if (file_types_.include_all_files && !filter_set.filters.empty()) {
|
||||
// Add the *.* filter, but only if we have added other filters (otherwise it
|
||||
// is implied).
|
||||
PortalFilter filter;
|
||||
filter.name = l10n_util::GetStringUTF8(IDS_SAVEAS_ALL_FILES);
|
||||
filter.patterns.insert("*.*");
|
||||
|
||||
filter_set.filters.push_back(std::move(filter));
|
||||
}
|
||||
|
||||
return filter_set;
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::SelectFileImplWithParentHandle(
|
||||
scoped_refptr<DialogInfo> info,
|
||||
std::u16string title,
|
||||
base::FilePath default_path,
|
||||
PortalFilterSet filter_set,
|
||||
base::FilePath::StringType default_extension,
|
||||
std::string parent_handle) {
|
||||
dbus_thread_linux::GetTaskRunner()->PostTask(
|
||||
FROM_HERE,
|
||||
base::BindOnce(&SelectFileDialogImplPortal::SelectFileImplOnBusThread,
|
||||
base::Unretained(this), std::move(info), std::move(title),
|
||||
std::move(default_path), std::move(filter_set),
|
||||
std::move(default_extension), std::move(parent_handle)));
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::SelectFileImplOnBusThread(
|
||||
scoped_refptr<DialogInfo> info,
|
||||
std::u16string title,
|
||||
base::FilePath default_path,
|
||||
PortalFilterSet filter_set,
|
||||
base::FilePath::StringType default_extension,
|
||||
std::string parent_handle) {
|
||||
dbus::Bus* bus = AcquireBusOnBusThread();
|
||||
if (!bus->Connect())
|
||||
LOG(ERROR) << "Could not connect to bus for XDG portal";
|
||||
|
||||
std::string method;
|
||||
switch (info->type) {
|
||||
case SELECT_FOLDER:
|
||||
case SELECT_UPLOAD_FOLDER:
|
||||
case SELECT_EXISTING_FOLDER:
|
||||
case SELECT_OPEN_FILE:
|
||||
case SELECT_OPEN_MULTI_FILE:
|
||||
method = kFileChooserMethodOpenFile;
|
||||
break;
|
||||
case SELECT_SAVEAS_FILE:
|
||||
method = kFileChooserMethodSaveFile;
|
||||
break;
|
||||
case SELECT_NONE:
|
||||
NOTREACHED();
|
||||
break;
|
||||
}
|
||||
|
||||
dbus::MethodCall method_call(kFileChooserInterfaceName, method);
|
||||
dbus::MessageWriter writer(&method_call);
|
||||
|
||||
writer.AppendString(parent_handle);
|
||||
|
||||
if (!title.empty()) {
|
||||
writer.AppendString(base::UTF16ToUTF8(title));
|
||||
} else {
|
||||
int message_id = 0;
|
||||
if (info->type == SELECT_SAVEAS_FILE) {
|
||||
message_id = IDS_SAVEAS_ALL_FILES;
|
||||
} else if (info->type == SELECT_OPEN_MULTI_FILE) {
|
||||
message_id = IDS_OPEN_FILES_DIALOG_TITLE;
|
||||
} else {
|
||||
message_id = IDS_OPEN_FILE_DIALOG_TITLE;
|
||||
}
|
||||
writer.AppendString(l10n_util::GetStringUTF8(message_id));
|
||||
}
|
||||
|
||||
std::string response_handle_token =
|
||||
base::StringPrintf("handle_%d", handle_token_counter_++);
|
||||
|
||||
AppendOptions(&writer, info->type, response_handle_token, default_path,
|
||||
filter_set);
|
||||
|
||||
// The sender part of the handle object contains the D-Bus connection name
|
||||
// without the prefix colon and with all dots replaced with underscores.
|
||||
std::string sender_part;
|
||||
base::ReplaceChars(bus->GetConnectionName().substr(1), ".", "_",
|
||||
&sender_part);
|
||||
|
||||
dbus::ObjectPath expected_handle_path(
|
||||
base::StringPrintf("/org/freedesktop/portal/desktop/request/%s/%s",
|
||||
sender_part.c_str(), response_handle_token.c_str()));
|
||||
|
||||
info->response_handle =
|
||||
bus->GetObjectProxy(kXdgPortalService, expected_handle_path);
|
||||
ConnectToHandle(info);
|
||||
|
||||
dbus::ObjectPath portal_path(kXdgPortalObject);
|
||||
dbus::ObjectProxy* portal =
|
||||
bus->GetObjectProxy(kXdgPortalService, portal_path);
|
||||
portal->CallMethodWithErrorResponse(
|
||||
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
|
||||
base::BindOnce(&SelectFileDialogImplPortal::OnCallResponse,
|
||||
base::Unretained(this), base::Unretained(bus), info));
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::AppendOptions(
|
||||
dbus::MessageWriter* writer,
|
||||
Type type,
|
||||
const std::string& response_handle_token,
|
||||
const base::FilePath& default_path,
|
||||
const PortalFilterSet& filter_set) {
|
||||
dbus::MessageWriter options_writer(nullptr);
|
||||
writer->OpenArray("{sv}", &options_writer);
|
||||
|
||||
AppendStringOption(&options_writer, kFileChooserOptionHandleToken,
|
||||
response_handle_token);
|
||||
|
||||
if (type == SELECT_UPLOAD_FOLDER) {
|
||||
AppendStringOption(&options_writer, kFileChooserOptionAcceptLabel,
|
||||
l10n_util::GetStringUTF8(
|
||||
IDS_SELECT_UPLOAD_FOLDER_DIALOG_UPLOAD_BUTTON));
|
||||
}
|
||||
|
||||
if (type == SELECT_FOLDER || type == SELECT_UPLOAD_FOLDER ||
|
||||
type == SELECT_EXISTING_FOLDER) {
|
||||
AppendBoolOption(&options_writer, kFileChooserOptionDirectory, true);
|
||||
} else if (type == SELECT_OPEN_MULTI_FILE) {
|
||||
AppendBoolOption(&options_writer, kFileChooserOptionMultiple, true);
|
||||
}
|
||||
|
||||
if (type == SELECT_SAVEAS_FILE && !default_path.empty()) {
|
||||
if (CallDirectoryExistsOnUIThread(default_path)) {
|
||||
// If this is an existing directory, navigate to that directory, with no
|
||||
// filename.
|
||||
AppendByteStringOption(&options_writer, kFileChooserOptionCurrentFolder,
|
||||
default_path.value());
|
||||
} else {
|
||||
// The default path does not exist, or is an existing file. We use
|
||||
// current_folder followed by current_name, as per the recommendation of
|
||||
// the GTK docs and the pattern followed by SelectFileDialogImplGTK.
|
||||
AppendByteStringOption(&options_writer, kFileChooserOptionCurrentFolder,
|
||||
default_path.DirName().value());
|
||||
AppendStringOption(&options_writer, kFileChooserOptionCurrentName,
|
||||
default_path.BaseName().value());
|
||||
}
|
||||
}
|
||||
|
||||
AppendFiltersOption(&options_writer, filter_set.filters);
|
||||
if (filter_set.default_filter) {
|
||||
dbus::MessageWriter option_writer(nullptr);
|
||||
options_writer.OpenDictEntry(&option_writer);
|
||||
|
||||
option_writer.AppendString(kFileChooserOptionCurrentFilter);
|
||||
|
||||
dbus::MessageWriter value_writer(nullptr);
|
||||
option_writer.OpenVariant("(sa(us))", &value_writer);
|
||||
|
||||
AppendFilterStruct(&value_writer, *filter_set.default_filter);
|
||||
|
||||
option_writer.CloseContainer(&value_writer);
|
||||
options_writer.CloseContainer(&option_writer);
|
||||
}
|
||||
|
||||
writer->CloseContainer(&options_writer);
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::AppendFiltersOption(
|
||||
dbus::MessageWriter* writer,
|
||||
const std::vector<PortalFilter>& filters) {
|
||||
dbus::MessageWriter option_writer(nullptr);
|
||||
writer->OpenDictEntry(&option_writer);
|
||||
|
||||
option_writer.AppendString(kFileChooserOptionFilters);
|
||||
|
||||
dbus::MessageWriter variant_writer(nullptr);
|
||||
option_writer.OpenVariant("a(sa(us))", &variant_writer);
|
||||
|
||||
dbus::MessageWriter filters_writer(nullptr);
|
||||
variant_writer.OpenArray("(sa(us))", &filters_writer);
|
||||
|
||||
for (const PortalFilter& filter : filters) {
|
||||
AppendFilterStruct(&filters_writer, filter);
|
||||
}
|
||||
|
||||
variant_writer.CloseContainer(&filters_writer);
|
||||
option_writer.CloseContainer(&variant_writer);
|
||||
writer->CloseContainer(&option_writer);
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::AppendFilterStruct(
|
||||
dbus::MessageWriter* writer,
|
||||
const PortalFilter& filter) {
|
||||
dbus::MessageWriter filter_writer(nullptr);
|
||||
writer->OpenStruct(&filter_writer);
|
||||
|
||||
filter_writer.AppendString(filter.name);
|
||||
|
||||
dbus::MessageWriter patterns_writer(nullptr);
|
||||
filter_writer.OpenArray("(us)", &patterns_writer);
|
||||
|
||||
for (const std::string& pattern : filter.patterns) {
|
||||
dbus::MessageWriter pattern_writer(nullptr);
|
||||
patterns_writer.OpenStruct(&pattern_writer);
|
||||
|
||||
pattern_writer.AppendUint32(kFileChooserFilterKindGlob);
|
||||
pattern_writer.AppendString(pattern);
|
||||
|
||||
patterns_writer.CloseContainer(&pattern_writer);
|
||||
}
|
||||
|
||||
filter_writer.CloseContainer(&patterns_writer);
|
||||
writer->CloseContainer(&filter_writer);
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::ConnectToHandle(
|
||||
scoped_refptr<DialogInfo> info) {
|
||||
info->response_handle->ConnectToSignal(
|
||||
kXdgPortalRequestInterfaceName, kXdgPortalResponseSignal,
|
||||
base::BindRepeating(&SelectFileDialogImplPortal::OnResponseSignalEmitted,
|
||||
base::Unretained(this), info),
|
||||
base::BindOnce(&SelectFileDialogImplPortal::OnResponseSignalConnected,
|
||||
base::Unretained(this), info));
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::CompleteOpen(
|
||||
scoped_refptr<DialogInfo> info,
|
||||
std::vector<base::FilePath> paths) {
|
||||
info->response_handle->Detach();
|
||||
info->main_task_runner->PostTask(
|
||||
FROM_HERE,
|
||||
base::BindOnce(&SelectFileDialogImplPortal::CompleteOpenOnMainThread,
|
||||
base::Unretained(this), std::move(info),
|
||||
std::move(paths)));
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::CancelOpen(scoped_refptr<DialogInfo> info) {
|
||||
info->response_handle->Detach();
|
||||
info->main_task_runner->PostTask(
|
||||
FROM_HERE,
|
||||
base::BindOnce(&SelectFileDialogImplPortal::CancelOpenOnMainThread,
|
||||
base::Unretained(this), std::move(info)));
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::CompleteOpenOnMainThread(
|
||||
scoped_refptr<DialogInfo> info,
|
||||
std::vector<base::FilePath> paths) {
|
||||
UnparentOnMainThread(info.get());
|
||||
|
||||
if (listener_) {
|
||||
if (info->type == SELECT_OPEN_MULTI_FILE) {
|
||||
listener_->MultiFilesSelected(paths, info->listener_params);
|
||||
} else if (paths.size() > 1) {
|
||||
LOG(ERROR) << "Got >1 file URI from a single-file chooser";
|
||||
} else {
|
||||
// The meaning of the index isn't clear, and we can't determine what
|
||||
// filter was selected regardless, see select_file_dialog_impl_kde.cc.
|
||||
listener_->FileSelected(paths.front(), 1, info->listener_params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::CancelOpenOnMainThread(
|
||||
scoped_refptr<DialogInfo> info) {
|
||||
UnparentOnMainThread(info.get());
|
||||
|
||||
if (listener_)
|
||||
listener_->FileSelectionCanceled(info->listener_params);
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::UnparentOnMainThread(DialogInfo* info) {
|
||||
if (info->parent) {
|
||||
parents_.erase(*info->parent);
|
||||
info->parent.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::OnCallResponse(
|
||||
dbus::Bus* bus,
|
||||
scoped_refptr<DialogInfo> info,
|
||||
dbus::Response* response,
|
||||
dbus::ErrorResponse* error_response) {
|
||||
if (response) {
|
||||
dbus::MessageReader reader(response);
|
||||
dbus::ObjectPath actual_handle_path;
|
||||
if (!reader.PopObjectPath(&actual_handle_path)) {
|
||||
LOG(ERROR) << "Invalid portal response";
|
||||
} else {
|
||||
if (info->response_handle->object_path() != actual_handle_path) {
|
||||
VLOG(1) << "Re-attaching response handle to "
|
||||
<< actual_handle_path.value();
|
||||
|
||||
info->response_handle->Detach();
|
||||
info->response_handle =
|
||||
bus->GetObjectProxy(kXdgPortalService, actual_handle_path);
|
||||
ConnectToHandle(info);
|
||||
}
|
||||
|
||||
// Return before the operation is cancelled.
|
||||
return;
|
||||
}
|
||||
} else if (error_response) {
|
||||
std::string error_name = error_response->GetErrorName();
|
||||
std::string error_message;
|
||||
dbus::MessageReader reader(error_response);
|
||||
reader.PopString(&error_message);
|
||||
|
||||
LOG(ERROR) << "Portal returned error: " << error_name << ": "
|
||||
<< error_message;
|
||||
} else {
|
||||
NOTREACHED();
|
||||
}
|
||||
|
||||
// All error paths end up here.
|
||||
CancelOpen(std::move(info));
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::OnResponseSignalConnected(
|
||||
scoped_refptr<DialogInfo> info,
|
||||
const std::string& interface,
|
||||
const std::string& signal,
|
||||
bool connected) {
|
||||
if (!connected) {
|
||||
LOG(ERROR) << "Could not connect to Response signal";
|
||||
CancelOpen(std::move(info));
|
||||
}
|
||||
}
|
||||
|
||||
void SelectFileDialogImplPortal::OnResponseSignalEmitted(
|
||||
scoped_refptr<DialogInfo> info,
|
||||
dbus::Signal* signal) {
|
||||
dbus::MessageReader reader(signal);
|
||||
|
||||
std::vector<std::string> uris;
|
||||
if (!CheckResponseCode(&reader) || !ReadResponseResults(&reader, &uris)) {
|
||||
CancelOpen(std::move(info));
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<base::FilePath> paths = ConvertUrisToPaths(uris);
|
||||
if (!paths.empty())
|
||||
CompleteOpen(std::move(info), std::move(paths));
|
||||
else
|
||||
CancelOpen(std::move(info));
|
||||
}
|
||||
|
||||
bool SelectFileDialogImplPortal::CheckResponseCode(
|
||||
dbus::MessageReader* reader) {
|
||||
std::uint32_t response = 0;
|
||||
if (!reader->PopUint32(&response)) {
|
||||
LOG(ERROR) << "Failed to read response ID";
|
||||
return false;
|
||||
} else if (response != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SelectFileDialogImplPortal::ReadResponseResults(
|
||||
dbus::MessageReader* reader,
|
||||
std::vector<std::string>* uris) {
|
||||
dbus::MessageReader results_reader(nullptr);
|
||||
if (!reader->PopArray(&results_reader)) {
|
||||
LOG(ERROR) << "Failed to read file chooser variant";
|
||||
return false;
|
||||
}
|
||||
|
||||
while (results_reader.HasMoreData()) {
|
||||
dbus::MessageReader entry_reader(nullptr);
|
||||
std::string key;
|
||||
if (!results_reader.PopDictEntry(&entry_reader) ||
|
||||
!entry_reader.PopString(&key)) {
|
||||
LOG(ERROR) << "Failed to read response entry";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (key == "uris") {
|
||||
dbus::MessageReader uris_reader(nullptr);
|
||||
if (!entry_reader.PopVariant(&uris_reader) ||
|
||||
!uris_reader.PopArrayOfStrings(uris)) {
|
||||
LOG(ERROR) << "Failed to read response entry value";
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<base::FilePath> SelectFileDialogImplPortal::ConvertUrisToPaths(
|
||||
const std::vector<std::string>& uris) {
|
||||
std::vector<base::FilePath> paths;
|
||||
for (const std::string& uri : uris) {
|
||||
if (!base::StartsWith(uri, kFileUriPrefix, base::CompareCase::SENSITIVE)) {
|
||||
LOG(WARNING) << "Ignoring unknown file chooser URI: " << uri;
|
||||
continue;
|
||||
}
|
||||
|
||||
base::StringPiece encoded_path(uri);
|
||||
encoded_path.remove_prefix(strlen(kFileUriPrefix));
|
||||
|
||||
url::RawCanonOutputT<char16_t> decoded_path;
|
||||
url::DecodeURLEscapeSequences(encoded_path.data(), encoded_path.size(),
|
||||
url::DecodeURLMode::kUTF8OrIsomorphic,
|
||||
&decoded_path);
|
||||
paths.emplace_back(base::UTF16ToUTF8(
|
||||
base::StringPiece16(decoded_path.data(), decoded_path.length())));
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
bool SelectFileDialogImplPortal::is_portal_available_ = false;
|
||||
int SelectFileDialogImplPortal::handle_token_counter_ = 0;
|
||||
|
||||
} // namespace gtk
|
202
ui/gtk/select_file_dialog_impl_portal.h
Normal file
202
ui/gtk/select_file_dialog_impl_portal.h
Normal file
@ -0,0 +1,202 @@
|
||||
// Copyright 2021 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.
|
||||
|
||||
#ifndef UI_GTK_SELECT_FILE_DIALOG_IMPL_PORTAL_H_
|
||||
#define UI_GTK_SELECT_FILE_DIALOG_IMPL_PORTAL_H_
|
||||
|
||||
#include "base/memory/ref_counted.h"
|
||||
#include "base/memory/scoped_refptr.h"
|
||||
#include "base/sequenced_task_runner.h"
|
||||
#include "base/synchronization/atomic_flag.h"
|
||||
#include "dbus/bus.h"
|
||||
#include "dbus/message.h"
|
||||
#include "dbus/object_proxy.h"
|
||||
#include "ui/gtk/select_file_dialog_impl.h"
|
||||
|
||||
namespace gtk {
|
||||
|
||||
// Implementation of SelectFileDialog that has the XDG file chooser portal show
|
||||
// a platform-dependent file selection dialog. This acts as a modal dialog.
|
||||
class SelectFileDialogImplPortal : public SelectFileDialogImpl {
|
||||
public:
|
||||
SelectFileDialogImplPortal(Listener* listener,
|
||||
std::unique_ptr<ui::SelectFilePolicy> policy);
|
||||
|
||||
SelectFileDialogImplPortal(const SelectFileDialogImplPortal& other) = delete;
|
||||
SelectFileDialogImplPortal& operator=(
|
||||
const SelectFileDialogImplPortal& other) = delete;
|
||||
|
||||
// Starts running a test to check for the presence of the file chooser portal
|
||||
// on the D-Bus task runner. This should only be called once, preferably
|
||||
// around program start.
|
||||
static void StartAvailabilityTestInBackground();
|
||||
|
||||
// Checks if the file chooser portal is available. Blocks if the availability
|
||||
// test from above has not yet completed (which should generally not happen).
|
||||
static bool IsPortalAvailable();
|
||||
|
||||
// Destroys the connection to the bus.
|
||||
static void DestroyPortalConnection();
|
||||
|
||||
protected:
|
||||
~SelectFileDialogImplPortal() override;
|
||||
|
||||
// BaseShellDialog implementation:
|
||||
bool IsRunning(gfx::NativeWindow parent_window) const override;
|
||||
|
||||
// SelectFileDialog implementation.
|
||||
// |params| is user data we pass back via the Listener interface.
|
||||
void SelectFileImpl(Type type,
|
||||
const std::u16string& title,
|
||||
const base::FilePath& default_path,
|
||||
const FileTypeInfo* file_types,
|
||||
int file_type_index,
|
||||
const base::FilePath::StringType& default_extension,
|
||||
gfx::NativeWindow owning_window,
|
||||
void* params) override;
|
||||
|
||||
bool HasMultipleFileTypeChoicesImpl() override;
|
||||
|
||||
private:
|
||||
// A named set of patterns used as a dialog filter.
|
||||
struct PortalFilter {
|
||||
PortalFilter();
|
||||
PortalFilter(const PortalFilter& other);
|
||||
PortalFilter(PortalFilter&& other);
|
||||
~PortalFilter();
|
||||
|
||||
PortalFilter& operator=(const PortalFilter& other) = default;
|
||||
PortalFilter& operator=(PortalFilter&& other) = default;
|
||||
|
||||
std::string name;
|
||||
std::set<std::string> patterns;
|
||||
};
|
||||
|
||||
// A set of PortalFilters, potentially with a default.
|
||||
struct PortalFilterSet {
|
||||
PortalFilterSet();
|
||||
PortalFilterSet(const PortalFilterSet& other);
|
||||
PortalFilterSet(PortalFilterSet&& other);
|
||||
~PortalFilterSet();
|
||||
|
||||
PortalFilterSet& operator=(const PortalFilterSet& other) = default;
|
||||
PortalFilterSet& operator=(PortalFilterSet&& other) = default;
|
||||
|
||||
std::vector<PortalFilter> filters;
|
||||
absl::optional<PortalFilter> default_filter;
|
||||
};
|
||||
|
||||
// A wrapper over some shared contextual information that needs to be passed
|
||||
// around between various handler functions. This is ref-counted due to some
|
||||
// of the locations its used in having slightly unclear or error-prone
|
||||
// lifetimes.
|
||||
struct DialogInfo : base::RefCountedThreadSafe<DialogInfo> {
|
||||
DialogInfo();
|
||||
|
||||
// The response object handle that the portal will send a signal to upon the
|
||||
// dialog's completion.
|
||||
dbus::ObjectProxy* response_handle = nullptr;
|
||||
absl::optional<gfx::AcceleratedWidget> parent;
|
||||
Type type;
|
||||
// The task runner the SelectFileImpl method was called on.
|
||||
scoped_refptr<base::SequencedTaskRunner> main_task_runner;
|
||||
// The untyped params to pass to the listener.
|
||||
void* listener_params = nullptr;
|
||||
|
||||
private:
|
||||
friend class base::RefCountedThreadSafe<DialogInfo>;
|
||||
|
||||
~DialogInfo();
|
||||
};
|
||||
|
||||
static scoped_refptr<dbus::Bus>* AcquireBusStorageOnBusThread();
|
||||
static dbus::Bus* AcquireBusOnBusThread();
|
||||
|
||||
static void DestroyBusOnBusThread();
|
||||
|
||||
static void CheckPortalAvailabilityOnBusThread();
|
||||
|
||||
static bool IsPortalRunningOnBusThread(dbus::ObjectProxy* dbus_proxy);
|
||||
static bool IsPortalActivatableOnBusThread(dbus::ObjectProxy* dbus_proxy);
|
||||
|
||||
// Returns a flag, written by the D-Bus thread and read by the UI thread,
|
||||
// indicating whether or not the availability test has completed.
|
||||
static base::AtomicFlag* GetAvailabilityTestCompletionFlag();
|
||||
|
||||
PortalFilterSet BuildFilterSet();
|
||||
|
||||
void SelectFileImplWithParentHandle(
|
||||
scoped_refptr<DialogInfo> info,
|
||||
std::u16string title,
|
||||
base::FilePath default_path,
|
||||
PortalFilterSet filter_set,
|
||||
base::FilePath::StringType default_extension,
|
||||
std::string parent_handle);
|
||||
|
||||
void SelectFileImplOnBusThread(scoped_refptr<DialogInfo> info,
|
||||
std::u16string title,
|
||||
base::FilePath default_path,
|
||||
PortalFilterSet filter_set,
|
||||
base::FilePath::StringType default_extension,
|
||||
std::string parent_handle);
|
||||
|
||||
void AppendOptions(dbus::MessageWriter* writer,
|
||||
Type type,
|
||||
const std::string& response_handle_token,
|
||||
const base::FilePath& default_path,
|
||||
const PortalFilterSet& filter_set);
|
||||
void AppendFiltersOption(dbus::MessageWriter* writer,
|
||||
const std::vector<PortalFilter>& filters);
|
||||
void AppendFilterStruct(dbus::MessageWriter* writer,
|
||||
const PortalFilter& filter);
|
||||
|
||||
// Sets up listeners for the response handle's signals.
|
||||
void ConnectToHandle(scoped_refptr<DialogInfo> info);
|
||||
|
||||
// Completes an open call, notifying the listener with the given paths, and
|
||||
// marks the dialog as closed.
|
||||
void CompleteOpen(scoped_refptr<DialogInfo> info,
|
||||
std::vector<base::FilePath> paths);
|
||||
// Completes an open call, notifying the listener with a cancellation, and
|
||||
// marks the dialog as closed.
|
||||
void CancelOpen(scoped_refptr<DialogInfo> info);
|
||||
|
||||
void CompleteOpenOnMainThread(scoped_refptr<DialogInfo> info,
|
||||
std::vector<base::FilePath> paths);
|
||||
void CancelOpenOnMainThread(scoped_refptr<DialogInfo> info);
|
||||
|
||||
// Removes the DialogInfo parent. Must be called on the UI task runner.
|
||||
void UnparentOnMainThread(DialogInfo* info);
|
||||
|
||||
void OnCallResponse(dbus::Bus* bus,
|
||||
scoped_refptr<DialogInfo> info,
|
||||
dbus::Response* response,
|
||||
dbus::ErrorResponse* error_response);
|
||||
|
||||
void OnResponseSignalConnected(scoped_refptr<DialogInfo> info,
|
||||
const std::string& interface,
|
||||
const std::string& signal,
|
||||
bool connected);
|
||||
|
||||
void OnResponseSignalEmitted(scoped_refptr<DialogInfo> info,
|
||||
dbus::Signal* signal);
|
||||
|
||||
bool CheckResponseCode(dbus::MessageReader* reader);
|
||||
bool ReadResponseResults(dbus::MessageReader* reader,
|
||||
std::vector<std::string>* uris);
|
||||
std::vector<base::FilePath> ConvertUrisToPaths(
|
||||
const std::vector<std::string>& uris);
|
||||
|
||||
std::set<gfx::AcceleratedWidget> parents_;
|
||||
|
||||
// Written by the D-Bus thread and read by the UI thread.
|
||||
static bool is_portal_available_;
|
||||
|
||||
// Used by the D-Bus thread to generate unique handle tokens.
|
||||
static int handle_token_counter_;
|
||||
};
|
||||
|
||||
} // namespace gtk
|
||||
|
||||
#endif // UI_GTK_SELECT_FILE_DIALOG_IMPL_PORTAL_H_
|
@ -38,9 +38,9 @@ GdkWindow* GtkUiPlatformWayland::GetGdkWindow(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool GtkUiPlatformWayland::SetGtkWidgetTransientFor(
|
||||
GtkWidget* widget,
|
||||
gfx::AcceleratedWidget parent) {
|
||||
bool GtkUiPlatformWayland::ExportWindowHandle(
|
||||
gfx::AcceleratedWidget window_id,
|
||||
base::OnceCallback<void(std::string)> callback) {
|
||||
if (!gtk::GtkCheckVersion(3, 22)) {
|
||||
LOG(WARNING) << "set_transient_for_exported not supported in GTK version "
|
||||
<< gtk_get_major_version() << '.' << gtk_get_minor_version()
|
||||
@ -48,8 +48,17 @@ bool GtkUiPlatformWayland::SetGtkWidgetTransientFor(
|
||||
return false;
|
||||
}
|
||||
|
||||
return ui::LinuxUiDelegate::GetInstance()->SetWidgetTransientFor(
|
||||
parent, base::BindOnce(&GtkUiPlatformWayland::OnHandle,
|
||||
return ui::LinuxUiDelegate::GetInstance()->ExportWindowHandle(
|
||||
window_id,
|
||||
base::BindOnce(&GtkUiPlatformWayland::OnHandleForward,
|
||||
weak_factory_.GetWeakPtr(), std::move(callback)));
|
||||
}
|
||||
|
||||
bool GtkUiPlatformWayland::SetGtkWidgetTransientFor(
|
||||
GtkWidget* widget,
|
||||
gfx::AcceleratedWidget parent) {
|
||||
return ui::LinuxUiDelegate::GetInstance()->ExportWindowHandle(
|
||||
parent, base::BindOnce(&GtkUiPlatformWayland::OnHandleSetTransient,
|
||||
weak_factory_.GetWeakPtr(), widget));
|
||||
}
|
||||
|
||||
@ -63,8 +72,8 @@ void GtkUiPlatformWayland::ShowGtkWindow(GtkWindow* window) {
|
||||
gtk_window_present(window);
|
||||
}
|
||||
|
||||
void GtkUiPlatformWayland::OnHandle(GtkWidget* widget,
|
||||
const std::string& handle) {
|
||||
void GtkUiPlatformWayland::OnHandleSetTransient(GtkWidget* widget,
|
||||
const std::string& handle) {
|
||||
char* parent = const_cast<char*>(handle.c_str());
|
||||
if (gtk::GtkCheckVersion(4)) {
|
||||
auto* toplevel = GlibCast<GdkToplevel>(
|
||||
@ -77,6 +86,12 @@ void GtkUiPlatformWayland::OnHandle(GtkWidget* widget,
|
||||
}
|
||||
}
|
||||
|
||||
void GtkUiPlatformWayland::OnHandleForward(
|
||||
base::OnceCallback<void(std::string)> callback,
|
||||
const std::string& handle) {
|
||||
std::move(callback).Run("wayland:" + handle);
|
||||
}
|
||||
|
||||
int GtkUiPlatformWayland::GetGdkKeyState() {
|
||||
return ui::LinuxUiDelegate::GetInstance()->GetKeyState();
|
||||
}
|
||||
|
@ -24,6 +24,9 @@ class GtkUiPlatformWayland : public GtkUiPlatform {
|
||||
void OnInitialized(GtkWidget* widget) override;
|
||||
GdkKeymap* GetGdkKeymap() override;
|
||||
GdkWindow* GetGdkWindow(gfx::AcceleratedWidget window_id) override;
|
||||
bool ExportWindowHandle(
|
||||
gfx::AcceleratedWidget window_id,
|
||||
base::OnceCallback<void(std::string)> callback) override;
|
||||
bool SetGtkWidgetTransientFor(GtkWidget* widget,
|
||||
gfx::AcceleratedWidget parent) override;
|
||||
void ClearTransientFor(gfx::AcceleratedWidget parent) override;
|
||||
@ -33,7 +36,9 @@ class GtkUiPlatformWayland : public GtkUiPlatform {
|
||||
private:
|
||||
// Called when xdg-foreign exports a parent window passed in
|
||||
// SetGtkWidgetTransientFor.
|
||||
void OnHandle(GtkWidget* widget, const std::string& handle);
|
||||
void OnHandleSetTransient(GtkWidget* widget, const std::string& handle);
|
||||
void OnHandleForward(base::OnceCallback<void(std::string)> callback,
|
||||
const std::string& handle);
|
||||
|
||||
base::WeakPtrFactory<GtkUiPlatformWayland> weak_factory_{this};
|
||||
};
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
#include "base/check.h"
|
||||
#include "base/environment.h"
|
||||
#include "base/strings/stringprintf.h"
|
||||
#include "ui/base/x/x11_util.h"
|
||||
#include "ui/events/platform/x11/x11_event_source.h"
|
||||
#include "ui/gfx/native_widget_types.h"
|
||||
@ -59,6 +60,13 @@ GdkWindow* GtkUiPlatformX11::GetGdkWindow(gfx::AcceleratedWidget window_id) {
|
||||
return gdk_window;
|
||||
}
|
||||
|
||||
bool GtkUiPlatformX11::ExportWindowHandle(
|
||||
gfx::AcceleratedWidget window_id,
|
||||
base::OnceCallback<void(std::string)> callback) {
|
||||
std::move(callback).Run(base::StringPrintf("x11:%#x", window_id));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GtkUiPlatformX11::SetGtkWidgetTransientFor(GtkWidget* widget,
|
||||
gfx::AcceleratedWidget parent) {
|
||||
auto x11_window = static_cast<x11::Window>(
|
||||
|
@ -27,6 +27,9 @@ class GtkUiPlatformX11 : public GtkUiPlatform {
|
||||
void OnInitialized(GtkWidget* widget) override;
|
||||
GdkKeymap* GetGdkKeymap() override;
|
||||
GdkWindow* GetGdkWindow(gfx::AcceleratedWidget window_id) override;
|
||||
bool ExportWindowHandle(
|
||||
gfx::AcceleratedWidget window_id,
|
||||
base::OnceCallback<void(std::string)> callback) override;
|
||||
bool SetGtkWidgetTransientFor(GtkWidget* widget,
|
||||
gfx::AcceleratedWidget parent) override;
|
||||
void ClearTransientFor(gfx::AcceleratedWidget parent) override;
|
||||
|
@ -29,7 +29,7 @@ LinuxUiBackend LinuxUiDelegateWayland::GetBackend() const {
|
||||
return LinuxUiBackend::kWayland;
|
||||
}
|
||||
|
||||
bool LinuxUiDelegateWayland::SetWidgetTransientFor(
|
||||
bool LinuxUiDelegateWayland::ExportWindowHandle(
|
||||
gfx::AcceleratedWidget parent,
|
||||
base::OnceCallback<void(const std::string&)> callback) {
|
||||
auto* parent_window =
|
||||
|
@ -19,7 +19,7 @@ class LinuxUiDelegateWayland : public LinuxUiDelegate {
|
||||
|
||||
// LinuxUiDelegate:
|
||||
LinuxUiBackend GetBackend() const override;
|
||||
bool SetWidgetTransientFor(
|
||||
bool ExportWindowHandle(
|
||||
gfx::AcceleratedWidget parent,
|
||||
base::OnceCallback<void(const std::string&)> callback) override;
|
||||
int GetKeyState() override;
|
||||
|
Reference in New Issue
Block a user