AyuGramDesktop/Telegram/SourceFiles/platform/linux/linux_xdp_file_dialog.cpp
Ilya Fedin 36acf60f7e Add XDG Desktop Portal based file dialog implementation from Qt
This allows to use portal dialogs more flexibly (e.g. fallback mechanism)
This also allows to have any changes we want for portal dialogs without patchig Qt

No more need to override QT_QPA_PLATFORM to use portal dialogs
2021-02-05 20:23:00 +04:00

536 lines
14 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "platform/linux/linux_xdp_file_dialog.h"
#include "platform/linux/specific_linux.h"
#include "storage/localstorage.h"
#include "base/qt_adapters.h"
#include <QtCore/qeventloop.h>
#include <QtDBus/QtDBus>
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCall>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QFile>
#include <QMetaType>
#include <QMimeType>
#include <QMimeDatabase>
#include <QRandomGenerator>
#include <QWindow>
namespace Platform {
namespace FileDialog {
namespace XDP {
namespace {
const char *filterRegExp =
"^(.*)\\(([a-zA-Z0-9_.,*? +;#\\-\\[\\]@\\{\\}/!<>\\$%&=^~:\\|]*)\\)$";
QStringList makeFilterList(const QString &filter) {
QString f(filter);
if (f.isEmpty())
return QStringList();
QString sep(QLatin1String(";;"));
int i = f.indexOf(sep, 0);
if (i == -1) {
if (f.indexOf(QLatin1Char('\n'), 0) != -1) {
sep = QLatin1Char('\n');
i = f.indexOf(sep, 0);
}
}
return f.split(sep);
}
} // namespace
bool Use(Type type) {
return UseXDGDesktopPortal()
&& (type != Type::ReadFolder || CanOpenDirectoryWithPortal());
}
bool Get(
QPointer<QWidget> parent,
QStringList &files,
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
Type type,
QString startFile) {
XDPFileDialog dialog(parent, caption, QString(), filter);
dialog.setModal(true);
if (type == Type::ReadFile || type == Type::ReadFiles) {
dialog.setFileMode((type == Type::ReadFiles) ? QFileDialog::ExistingFiles : QFileDialog::ExistingFile);
dialog.setAcceptMode(QFileDialog::AcceptOpen);
} else if (type == Type::ReadFolder) {
dialog.setAcceptMode(QFileDialog::AcceptOpen);
dialog.setFileMode(QFileDialog::Directory);
dialog.setOption(QFileDialog::ShowDirsOnly);
} else {
dialog.setFileMode(QFileDialog::AnyFile);
dialog.setAcceptMode(QFileDialog::AcceptSave);
}
if (startFile.isEmpty() || startFile.at(0) != '/') {
startFile = cDialogLastPath() + '/' + startFile;
}
dialog.selectFile(startFile);
int res = dialog.exec();
QString path = dialog.directory().path();
if (path != cDialogLastPath()) {
cSetDialogLastPath(path);
Local::writeSettings();
}
if (res == QDialog::Accepted) {
QStringList selectedFilesStrings;
ranges::transform(
dialog.selectedFiles(),
ranges::back_inserter(selectedFilesStrings),
[](const QUrl &url) { return url.path(); });
if (type == Type::ReadFiles) {
files = selectedFilesStrings;
} else {
files = selectedFilesStrings.mid(0, 1);
}
return true;
}
files = QStringList();
remoteContent = QByteArray();
return false;
}
QDBusArgument &operator <<(QDBusArgument &arg, const XDPFileDialog::FilterCondition &filterCondition) {
arg.beginStructure();
arg << filterCondition.type << filterCondition.pattern;
arg.endStructure();
return arg;
}
const QDBusArgument &operator >>(const QDBusArgument &arg, XDPFileDialog::FilterCondition &filterCondition) {
uint type;
QString filterPattern;
arg.beginStructure();
arg >> type >> filterPattern;
filterCondition.type = (XDPFileDialog::ConditionType)type;
filterCondition.pattern = filterPattern;
arg.endStructure();
return arg;
}
QDBusArgument &operator <<(QDBusArgument &arg, const XDPFileDialog::Filter filter) {
arg.beginStructure();
arg << filter.name << filter.filterConditions;
arg.endStructure();
return arg;
}
const QDBusArgument &operator >>(const QDBusArgument &arg, XDPFileDialog::Filter &filter) {
QString name;
XDPFileDialog::FilterConditionList filterConditions;
arg.beginStructure();
arg >> name >> filterConditions;
filter.name = name;
filter.filterConditions = filterConditions;
arg.endStructure();
return arg;
}
class XDPFileDialogPrivate {
public:
XDPFileDialogPrivate() {
}
WId winId = 0;
bool directoryMode = false;
bool modal = false;
bool multipleFiles = false;
bool saveFile = false;
QString acceptLabel;
QString directory;
QString title;
QStringList nameFilters;
QStringList mimeTypesFilters;
// maps user-visible name for portal to full name filter
QMap<QString, QString> userVisibleToNameFilter;
QString selectedMimeTypeFilter;
QString selectedNameFilter;
QStringList selectedFiles;
};
XDPFileDialog::XDPFileDialog(QWidget *parent, const QString &caption, const QString &directory, const QString &filter)
: QDialog(parent)
, d_ptr(new XDPFileDialogPrivate())
, _windowTitle(caption)
, _initialDirectory(directory) {
Q_D(XDPFileDialog);
auto filters = makeFilterList(filter);
const int numFilters = filters.count();
_nameFilters.reserve(numFilters);
for (int i = 0; i < numFilters; ++i) {
_nameFilters << filters[i].simplified();
}
accepted(
) | rpl::start_with_next([=] {
Q_EMIT accept();
}, _lifetime);
rejected(
) | rpl::start_with_next([=] {
Q_EMIT reject();
}, _lifetime);
}
XDPFileDialog::~XDPFileDialog() {
}
void XDPFileDialog::initializeDialog() {
Q_D(XDPFileDialog);
if (_fileMode == QFileDialog::ExistingFiles)
d->multipleFiles = true;
if (_fileMode == QFileDialog::Directory || _options.testFlag(QFileDialog::ShowDirsOnly))
d->directoryMode = true;
#if 0 // it is commented in GtkFileDialog for some reason, do the same
if (options()->isLabelExplicitlySet(QFileDialog::Accept))
d->acceptLabel = options()->labelText(QFileDialog::Accept);
#endif
if (!_windowTitle.isEmpty())
d->title = _windowTitle;
if (_acceptMode == QFileDialog::AcceptSave)
d->saveFile = true;
if (!_nameFilters.isEmpty())
d->nameFilters = _nameFilters;
#if 0 // what is the right way to implement this?
if (!options()->mimeTypeFilters().isEmpty())
d->mimeTypesFilters = options()->mimeTypeFilters();
if (!options()->initiallySelectedMimeTypeFilter().isEmpty())
d->selectedMimeTypeFilter = options()->initiallySelectedMimeTypeFilter();
#endif
const auto initialNameFilter = _nameFilters.isEmpty() ? QString() : _nameFilters.front();
if (!initialNameFilter.isEmpty())
d->selectedNameFilter = initialNameFilter;
setDirectory(_initialDirectory);
}
void XDPFileDialog::openPortal() {
Q_D(XDPFileDialog);
QDBusMessage message = QDBusMessage::createMethodCall(
QLatin1String("org.freedesktop.portal.Desktop"),
QLatin1String("/org/freedesktop/portal/desktop"),
QLatin1String("org.freedesktop.portal.FileChooser"),
d->saveFile ? QLatin1String("SaveFile") : QLatin1String("OpenFile"));
QString parentWindowId = QLatin1String("x11:") + QString::number(d->winId, 16);
QVariantMap options;
if (!d->acceptLabel.isEmpty())
options.insert(QLatin1String("accept_label"), d->acceptLabel);
options.insert(QLatin1String("modal"), d->modal);
options.insert(QLatin1String("multiple"), d->multipleFiles);
options.insert(QLatin1String("directory"), d->directoryMode);
if (d->saveFile) {
if (!d->directory.isEmpty())
options.insert(QLatin1String("current_folder"), QFile::encodeName(d->directory).append('\0'));
if (!d->selectedFiles.isEmpty())
options.insert(QLatin1String("current_file"), QFile::encodeName(d->selectedFiles.first()).append('\0'));
}
// Insert filters
qDBusRegisterMetaType<FilterCondition>();
qDBusRegisterMetaType<FilterConditionList>();
qDBusRegisterMetaType<Filter>();
qDBusRegisterMetaType<FilterList>();
FilterList filterList;
auto selectedFilterIndex = filterList.size() - 1;
d->userVisibleToNameFilter.clear();
if (!d->mimeTypesFilters.isEmpty()) {
for (const QString &mimeTypefilter : d->mimeTypesFilters) {
QMimeDatabase mimeDatabase;
QMimeType mimeType = mimeDatabase.mimeTypeForName(mimeTypefilter);
// Creates e.g. (1, "image/png")
FilterCondition filterCondition;
filterCondition.type = MimeType;
filterCondition.pattern = mimeTypefilter;
// Creates e.g. [((1, "image/png"))]
FilterConditionList filterConditions;
filterConditions << filterCondition;
// Creates e.g. [("Images", [((1, "image/png"))])]
Filter filter;
filter.name = mimeType.comment();
filter.filterConditions = filterConditions;
filterList << filter;
if (!d->selectedMimeTypeFilter.isEmpty() && d->selectedMimeTypeFilter == mimeTypefilter)
selectedFilterIndex = filterList.size() - 1;
}
} else if (!d->nameFilters.isEmpty()) {
for (const QString &nameFilter : d->nameFilters) {
// Do parsing:
// Supported format is ("Images (*.png *.jpg)")
QRegularExpression regexp(QString::fromLatin1(filterRegExp));
QRegularExpressionMatch match = regexp.match(nameFilter);
if (match.hasMatch()) {
QString userVisibleName = match.captured(1);
QStringList filterStrings = match.captured(2).split(QLatin1Char(' '), base::QStringSkipEmptyParts);
if (filterStrings.isEmpty()) {
LOG(("XDP File Dialog Error: Filter %1 is empty and will be ignored.").arg(userVisibleName));
continue;
}
FilterConditionList filterConditions;
for (const QString &filterString : filterStrings) {
FilterCondition filterCondition;
filterCondition.type = GlobalPattern;
filterCondition.pattern = filterString;
filterConditions << filterCondition;
}
Filter filter;
filter.name = userVisibleName;
filter.filterConditions = filterConditions;
filterList << filter;
d->userVisibleToNameFilter.insert(userVisibleName, nameFilter);
if (!d->selectedNameFilter.isEmpty() && d->selectedNameFilter == nameFilter)
selectedFilterIndex = filterList.size() - 1;
}
}
}
if (!filterList.isEmpty())
options.insert(QLatin1String("filters"), QVariant::fromValue(filterList));
if (selectedFilterIndex != -1)
options.insert(QLatin1String("current_filter"), QVariant::fromValue(filterList[selectedFilterIndex]));
options.insert(QLatin1String("handle_token"), QStringLiteral("qt%1").arg(QRandomGenerator::global()->generate()));
// TODO choices a(ssa(ss)s)
// List of serialized combo boxes to add to the file chooser.
message << parentWindowId << d->title << options;
QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this] (QDBusPendingCallWatcher *watcher) {
QDBusPendingReply<QDBusObjectPath> reply = *watcher;
if (reply.isError()) {
_reject.fire({});
} else {
QDBusConnection::sessionBus().connect(
nullptr,
reply.value().path(),
QLatin1String("org.freedesktop.portal.Request"),
QLatin1String("Response"),
this,
SLOT(gotResponse(uint,QVariantMap)));
}
});
}
bool XDPFileDialog::defaultNameFilterDisables() const {
return false;
}
void XDPFileDialog::setDirectory(const QUrl &directory) {
Q_D(XDPFileDialog);
d->directory = directory.path();
}
QUrl XDPFileDialog::directory() const {
Q_D(const XDPFileDialog);
return d->directory;
}
void XDPFileDialog::selectFile(const QUrl &filename) {
Q_D(XDPFileDialog);
d->selectedFiles << filename.path();
}
QList<QUrl> XDPFileDialog::selectedFiles() const {
Q_D(const XDPFileDialog);
QList<QUrl> files;
for (const QString &file : d->selectedFiles) {
files << QUrl(file);
}
return files;
}
void XDPFileDialog::setFilter() {
Q_D(XDPFileDialog);
}
void XDPFileDialog::selectMimeTypeFilter(const QString &filter) {
Q_D(XDPFileDialog);
}
QString XDPFileDialog::selectedMimeTypeFilter() const {
Q_D(const XDPFileDialog);
return d->selectedMimeTypeFilter;
}
void XDPFileDialog::selectNameFilter(const QString &filter) {
Q_D(XDPFileDialog);
}
QString XDPFileDialog::selectedNameFilter() const {
Q_D(const XDPFileDialog);
return d->selectedNameFilter;
}
int XDPFileDialog::exec() {
Q_D(XDPFileDialog);
bool deleteOnClose = testAttribute(Qt::WA_DeleteOnClose);
setAttribute(Qt::WA_DeleteOnClose, false);
bool wasShowModal = testAttribute(Qt::WA_ShowModal);
setAttribute(Qt::WA_ShowModal, true);
setResult(0);
show();
QPointer<QDialog> guard = this;
// HACK we have to avoid returning until we emit that the dialog was accepted or rejected
QEventLoop loop;
rpl::lifetime lifetime;
accepted(
) | rpl::start_with_next([&] {
loop.quit();
}, lifetime);
rejected(
) | rpl::start_with_next([&] {
loop.quit();
}, lifetime);
loop.exec();
if (guard.isNull())
return QDialog::Rejected;
setAttribute(Qt::WA_ShowModal, wasShowModal);
return result();
}
void XDPFileDialog::setVisible(bool visible) {
if (visible) {
if (testAttribute(Qt::WA_WState_ExplicitShowHide) && !testAttribute(Qt::WA_WState_Hidden)) {
return;
}
} else if (testAttribute(Qt::WA_WState_ExplicitShowHide) && testAttribute(Qt::WA_WState_Hidden)) {
return;
}
if (visible) {
showHelper(windowFlags(), windowModality(), parentWidget() ? parentWidget()->windowHandle() : nullptr);
} else {
hideHelper();
}
// Set WA_DontShowOnScreen so that QDialog::setVisible(visible) below
// updates the state correctly, but skips showing the non-native version:
setAttribute(Qt::WA_DontShowOnScreen);
QDialog::setVisible(visible);
}
void XDPFileDialog::hideHelper() {
Q_D(XDPFileDialog);
}
void XDPFileDialog::showHelper(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) {
Q_D(XDPFileDialog);
initializeDialog();
d->modal = windowModality != Qt::NonModal;
d->winId = parent ? parent->winId() : 0;
openPortal();
}
void XDPFileDialog::gotResponse(uint response, const QVariantMap &results) {
Q_D(XDPFileDialog);
if (!response) {
if (results.contains(QLatin1String("uris")))
d->selectedFiles = results.value(QLatin1String("uris")).toStringList();
if (results.contains(QLatin1String("current_filter"))) {
const Filter selectedFilter = qdbus_cast<Filter>(results.value(QStringLiteral("current_filter")));
if (!selectedFilter.filterConditions.empty() && selectedFilter.filterConditions[0].type == MimeType) {
// s.a. XDPFileDialog::openPortal which basically does the inverse
d->selectedMimeTypeFilter = selectedFilter.filterConditions[0].pattern;
d->selectedNameFilter.clear();
} else {
d->selectedNameFilter = d->userVisibleToNameFilter.value(selectedFilter.name);
d->selectedMimeTypeFilter.clear();
}
}
_accept.fire({});
} else {
_reject.fire({});
}
}
rpl::producer<> XDPFileDialog::accepted() {
return _accept.events();
}
rpl::producer<> XDPFileDialog::rejected() {
return _reject.events();
}
} // namespace XDP
} // namespace FileDialog
} // namespace Platform