mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-29 20:46:04 +02:00
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
536 lines
14 KiB
C++
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
|