mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-28 20:19:36 +02:00
550 lines
15 KiB
C++
550 lines
15 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/platform_file_utilities.h"
|
|
#include "platform/linux/specific_linux.h"
|
|
#include "storage/localstorage.h"
|
|
#include "base/qt_adapters.h"
|
|
|
|
#include <QDBusConnection>
|
|
#include <QDBusMessage>
|
|
#include <QDBusPendingCall>
|
|
#include <QDBusPendingCallWatcher>
|
|
#include <QDBusPendingReply>
|
|
#include <QDBusMetaType>
|
|
|
|
#include <QEventLoop>
|
|
#include <QFile>
|
|
#include <QMetaType>
|
|
#include <QMimeType>
|
|
#include <QMimeDatabase>
|
|
#include <QRandomGenerator>
|
|
#include <QWindow>
|
|
#include <QRegularExpression>
|
|
|
|
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) {
|
|
static const auto docRegExp = QRegularExpression("^/run/user/\\d+/doc");
|
|
if (cDialogLastPath().isEmpty()
|
|
|| cDialogLastPath().contains(docRegExp)) {
|
|
InitLastPath();
|
|
}
|
|
|
|
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.setDirectory(QFileInfo(startFile).absoluteDir().absolutePath());
|
|
dialog.selectFile(startFile);
|
|
|
|
int res = dialog.exec();
|
|
|
|
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);
|
|
}
|
|
|
|
QString path = files.isEmpty()
|
|
? QString()
|
|
: QFileInfo(files.back()).absoluteDir().absolutePath();
|
|
|
|
if (!path.isEmpty()
|
|
&& !path.contains(docRegExp)
|
|
&& path != cDialogLastPath()) {
|
|
cSetDialogLastPath(path);
|
|
Local::writeSettings();
|
|
}
|
|
|
|
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
|