Verified Commit 16e5d3ad authored by Jonah Brüchert's avatar Jonah Brüchert
Browse files

Implement mobile filechooser interface.

parent ac67e456
add_subdirectory(kirigami-filepicker)
add_definitions(-DTRANSLATION_DOMAIN="xdg-desktop-portal-kde")
include_directories(${Qt5PrintSupport_PRIVATE_INCLUDE_DIRS})
......@@ -68,7 +70,7 @@ target_link_libraries(xdg-desktop-portal-kde
KF5::WaylandClient
KF5::WidgetsAddons
KF5::WindowSystem
KirigamiFilepicker
Wayland::Client
)
......
......@@ -31,11 +31,15 @@
#include <QPushButton>
#include <QVBoxLayout>
#include <QUrl>
#include <QQmlApplicationEngine>
#include <KLocalizedString>
#include <KFileFilterCombo>
#include <KFileWidget>
#include <mobilefiledialog.h>
Q_LOGGING_CATEGORY(XdgDesktopPortalKdeFileChooser, "xdp-kde-file-chooser")
// Keep in sync with qflatpakfiledialog from flatpak-platform-plugin
......@@ -159,6 +163,7 @@ FileDialog::~FileDialog()
FileChooserPortal::FileChooserPortal(QObject *parent)
: QDBusAbstractAdaptor(parent)
, m_mobileFileDialog(nullptr)
{
qDBusRegisterMetaType<Filter>();
qDBusRegisterMetaType<Filters>();
......@@ -198,12 +203,6 @@ uint FileChooserPortal::OpenFile(const QDBusObjectPath &handle,
// mapping between filter strings and actual filters
QMap<QString, FilterList> allFilters;
// for handling of options - choices
QScopedPointer<QWidget> optionsWidget;
// to store IDs for choices along with corresponding comboboxes/checkboxes
QMap<QString, QCheckBox*> checkboxes;
QMap<QString, QComboBox*> comboboxes;
if (options.contains(QStringLiteral("accept_label"))) {
acceptLabel = options.value(QStringLiteral("accept_label")).toString();
}
......@@ -241,6 +240,46 @@ uint FileChooserPortal::OpenFile(const QDBusObjectPath &handle,
}
}
if (isMobile()) {
if (!m_mobileFileDialog) {
qCDebug(XdgDesktopPortalKdeFileChooser) << "Creating file dialog";
m_mobileFileDialog = new MobileFileDialog(this);
}
m_mobileFileDialog->setTitle(title);
// Always true when we are opening a file
m_mobileFileDialog->setSelectExisting(true);
m_mobileFileDialog->setSelectFolder(directory);
// currentName: not implemented
if (!acceptLabel.isEmpty()) {
m_mobileFileDialog->setAcceptLabel(acceptLabel);
}
if (!nameFilters.isEmpty()) {
m_mobileFileDialog->setNameFilters(nameFilters);
}
if (!mimeTypeFilters.isEmpty()) {
m_mobileFileDialog->setMimeTypeFilters(mimeTypeFilters);
}
uint retCode = m_mobileFileDialog->exec();
results.insert(QStringLiteral("uris"), m_mobileFileDialog->results());
return retCode;
}
// for handling of options - choices
QScopedPointer<QWidget> optionsWidget;
// to store IDs for choices along with corresponding comboboxes/checkboxes
QMap<QString, QCheckBox*> checkboxes;
QMap<QString, QComboBox*> comboboxes;
if (options.contains(QStringLiteral("choices"))) {
OptionList optionList = qdbus_cast<OptionList>(options.value(QStringLiteral("choices")));
optionsWidget.reset(CreateChoiceControls(optionList, checkboxes, comboboxes));
......@@ -270,8 +309,8 @@ uint FileChooserPortal::OpenFile(const QDBusObjectPath &handle,
if (fileDialog->exec() == QDialog::Accepted) {
QStringList files;
for (const QString &filename : fileDialog->m_fileWidget->selectedFiles()) {
QUrl url = QUrl::fromLocalFile(filename);
files << url.toDisplayString();
QUrl url = QUrl::fromLocalFile(filename);
files << url.toDisplayString();
}
if (files.isEmpty()) {
......@@ -329,12 +368,6 @@ uint FileChooserPortal::SaveFile(const QDBusObjectPath &handle,
// mapping between filter strings and actual filters
QMap<QString, FilterList> allFilters;
// for handling of options - choices
QScopedPointer<QWidget> optionsWidget;
// to store IDs for choices along with corresponding comboboxes/checkboxes
QMap<QString, QCheckBox*> checkboxes;
QMap<QString, QComboBox*> comboboxes;
if (options.contains(QStringLiteral("modal"))) {
modalDialog = options.value(QStringLiteral("modal")).toBool();
}
......@@ -376,6 +409,52 @@ uint FileChooserPortal::SaveFile(const QDBusObjectPath &handle,
}
}
if (isMobile()) {
if (!m_mobileFileDialog) {
qCDebug(XdgDesktopPortalKdeFileChooser) << "Creating file dialog";
m_mobileFileDialog = new MobileFileDialog(this);
}
m_mobileFileDialog->setTitle(title);
// Always false when we are saving a file
m_mobileFileDialog->setSelectExisting(false);
if (!currentFolder.isEmpty()) {
m_mobileFileDialog->setFolder(currentFolder);
}
if (!currentFile.isEmpty()) {
m_mobileFileDialog->setCurrentFile(currentFile);
}
// currentName: not implemented
if (!acceptLabel.isEmpty()) {
m_mobileFileDialog->setAcceptLabel(acceptLabel);
}
if (!nameFilters.isEmpty()) {
m_mobileFileDialog->setNameFilters(nameFilters);
}
if (!mimeTypeFilters.isEmpty()) {
m_mobileFileDialog->setMimeTypeFilters(mimeTypeFilters);
}
uint retCode = m_mobileFileDialog->exec();
results.insert(QStringLiteral("uris"), m_mobileFileDialog->results());
return retCode;
}
// for handling of options - choices
QScopedPointer<QWidget> optionsWidget;
// to store IDs for choices along with corresponding comboboxes/checkboxes
QMap<QString, QCheckBox*> checkboxes;
QMap<QString, QComboBox*> comboboxes;
if (options.contains(QStringLiteral("choices"))) {
OptionList optionList = qdbus_cast<OptionList>(options.value(QStringLiteral("choices")));
optionsWidget.reset(CreateChoiceControls(optionList, checkboxes, comboboxes));
......@@ -536,3 +615,9 @@ void FileChooserPortal::ExtractFilters(const QVariantMap &options, QStringList &
}
}
}
bool FileChooserPortal::isMobile() const
{
QByteArray mobile = qgetenv("QT_QUICK_CONTROLS_MOBILE");
return mobile == "true" || mobile == "1";
}
......@@ -29,6 +29,7 @@
class KFileWidget;
class QDialogButtonBox;
class MobileFileDialog;
class FileDialog : public QDialog
{
......@@ -105,6 +106,10 @@ private:
static void ExtractFilters(const QVariantMap &options, QStringList &nameFilters,
QStringList &mimeTypeFilters, QMap<QString, FilterList> &allFilters);
bool isMobile() const;
MobileFileDialog *m_mobileFileDialog;
};
#endif // XDG_DESKTOP_PORTAL_KDE_FILECHOOSER_H
set(filepicker_lib_SRCS
api/mobilefiledialog.cpp
declarative/filechooserqmlcallback.cpp
declarative/dirmodel.cpp
declarative/dirmodelutils.cpp
declarative/fileplacesmodel.cpp
declarative/filechooserqmlcallback.cpp
declarative/filepicker.qrc
)
add_library(KirigamiFilepicker STATIC ${filepicker_lib_SRCS})
target_include_directories(KirigamiFilepicker PRIVATE declarative)
target_link_libraries(KirigamiFilepicker
Qt5::Quick
Qt5::Qml
KF5::I18n
KF5::KIOCore
KF5::KIOFileWidgets
)
target_include_directories(KirigamiFilepicker PUBLIC api)
// SPDX-FileCopyrightText: 2020 Jonah Brüchert <jbb@kaidan.im>
//
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "mobilefiledialog.h"
#include <QQmlApplicationEngine>
#include <QGuiApplication>
#include <QStandardPaths>
#include <QQuickWindow>
#include <QQmlContext>
#include <QLoggingCategory>
#include <KLocalizedContext>
#include "dirmodel.h"
#include "dirmodelutils.h"
#include "fileplacesmodel.h"
#include "filechooserqmlcallback.h"
constexpr auto URI = "org.kde.kirigamifilepicker";
Q_LOGGING_CATEGORY(KirigamiFilepicker, "xdp-kde-file-chooser")
MobileFileDialog::MobileFileDialog(QObject *parent)
: QObject(parent)
, m_engine(new QQmlApplicationEngine(this))
, m_window(nullptr)
{
qmlRegisterType<DirModel>(URI, 0, 1, "DirModel");
qmlRegisterSingletonType<DirModelUtils>(URI, 0, 1, "DirModelUtils", [=](QQmlEngine *, QJSEngine *) {
return new DirModelUtils;
});
qmlRegisterType<FileChooserQmlCallback>(URI, 0, 1, "FileChooserCallback");
qmlRegisterType<FilePlacesModel>(URI, 0, 1, "FilePlacesModel");
Q_INIT_RESOURCE(filepicker);
auto *i18nContext = new KLocalizedContext(m_engine);
m_engine->rootContext()->setContextObject(i18nContext);
m_engine->load(QStringLiteral("qrc:/org.kde.kirigamifilepicker/FilePickerWindow.qml"));
m_window = qobject_cast<QQuickWindow *>(m_engine->rootObjects().first());
m_callback = m_window->findChild<FileChooserQmlCallback *>(QStringLiteral("callback"));
// Connect everything to callback
connect(m_callback, &FileChooserQmlCallback::accepted, this, &MobileFileDialog::accepted);
connect(m_callback, &FileChooserQmlCallback::titleChanged, this, &MobileFileDialog::titleChanged);
connect(m_callback, &FileChooserQmlCallback::selectMultipleChanged, this, &MobileFileDialog::selectMultipleChanged);
connect(m_callback, &FileChooserQmlCallback::selectExistingChanged, this, &MobileFileDialog::selectExistingChanged);
connect(m_callback, &FileChooserQmlCallback::nameFiltersChanged, this, &MobileFileDialog::nameFiltersChanged);
connect(m_callback, &FileChooserQmlCallback::mimeTypeFiltersChanged, this, &MobileFileDialog::mimeTypeFiltersChanged);
connect(m_callback, &FileChooserQmlCallback::folderChanged, this, &MobileFileDialog::folderChanged);
connect(m_callback, &FileChooserQmlCallback::currentFileChanged, this, &MobileFileDialog::currentFileChanged);
connect(m_callback, &FileChooserQmlCallback::acceptLabelChanged, this, &MobileFileDialog::acceptLabelChanged);
connect(m_callback, &FileChooserQmlCallback::selectFolderChanged, this, &MobileFileDialog::selectFolderChanged);
}
// FileDialog methods pass through to the callback to provide a nice c++ api
QString MobileFileDialog::title() const
{
return m_callback->title();
}
void MobileFileDialog::setTitle(const QString &title)
{
m_callback->setTitle(title);
}
bool MobileFileDialog::selectMultiple() const
{
return m_callback->selectMultiple();
}
void MobileFileDialog::setSelectMultiple(bool selectMultiple)
{
m_callback->setSelectMultiple(selectMultiple);
}
bool MobileFileDialog::selectExisting() const
{
return m_callback->selectExisting();
}
void MobileFileDialog::setSelectExisting(bool selectExisting)
{
m_callback->setSelectExisting(selectExisting);
}
QStringList MobileFileDialog::nameFilters() const
{
return m_callback->nameFilters();
}
void MobileFileDialog::setNameFilters(const QStringList &nameFilters)
{
m_callback->setNameFilters(nameFilters);
}
QStringList MobileFileDialog::mimeTypeFilters() const
{
return m_callback->mimeTypeFilters();
}
void MobileFileDialog::setMimeTypeFilters(const QStringList &mimeTypeFilters)
{
m_callback->setMimeTypeFilters(mimeTypeFilters);
}
QString MobileFileDialog::folder() const
{
return m_callback->folder();
}
void MobileFileDialog::setFolder(const QString &folder)
{
m_callback->setFolder(folder);
}
QString MobileFileDialog::currentFile() const
{
return m_callback->currentFile();
}
void MobileFileDialog::setCurrentFile(const QString &currentFile)
{
m_callback->setCurrentFile(currentFile);
}
QString MobileFileDialog::acceptLabel() const
{
return m_callback->acceptLabel();
}
void MobileFileDialog::setAcceptLabel(const QString &acceptLabel)
{
m_callback->setAcceptLabel(acceptLabel);
}
bool MobileFileDialog::selectFolder() const
{
return m_callback->selectFolder();
}
void MobileFileDialog::setSelectFolder(bool selectFolder)
{
m_callback->setSelectFolder(selectFolder);
}
uint MobileFileDialog::exec()
{
// Show window
m_window->setVisible(true);
m_window->raise();
m_window->requestActivate();
m_window->setIcon(QIcon::fromTheme(QStringLiteral("folder")));
// Reset old data
m_results.clear();
// Wait for it to exit
bool handled = false;
uint exitCode = 0;
const auto acceptedConn = connect(m_callback, &FileChooserQmlCallback::accepted, this, [this, &exitCode, &handled] (const QStringList &urls) {
for (const auto &filename : urls) {
m_results << QUrl(filename).toDisplayString();
}
handled = true;
exitCode = 0;
qDebug(KirigamiFilepicker) << "Got results" << m_results;
});
const auto cancelConn = connect(m_callback, &FileChooserQmlCallback::cancel, this, [&exitCode, &handled] {
qDebug(KirigamiFilepicker) << "Quit without results";
handled = true;
exitCode = 1;
});
while (!handled)
QGuiApplication::processEvents();
qDebug(KirigamiFilepicker) << "exiting file dialog";
// Disconnect signals, to avoid them being connected twice
// when the dialog is used again
disconnect(acceptedConn);
disconnect(cancelConn);
if (m_window) {
m_window->setVisible(false);
}
return exitCode;
}
QStringList MobileFileDialog::results() const
{
return m_results;
}
// SPDX-FileCopyrightText: 2020 Jonah Brüchert <jbb@kaidan.im>
//
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QStringList>
#include <QObject>
class QQmlApplicationEngine;
class FileChooserQmlCallback;
class QQuickWindow;
class MobileFileDialog : public QObject
{
Q_OBJECT
public:
MobileFileDialog(QObject *parent);
~MobileFileDialog() = default;
QString title() const;
void setTitle(const QString &title);
bool selectMultiple() const;
void setSelectMultiple(bool selectMultiple);
bool selectExisting() const;
void setSelectExisting(bool selectExisting);
QStringList nameFilters() const;
void setNameFilters(const QStringList &nameFilters);
QStringList mimeTypeFilters() const;
void setMimeTypeFilters(const QStringList &mimeTypeFilters);
QString folder() const;
void setFolder(const QString &folder);
QString currentFile() const;
void setCurrentFile(const QString &currentFile);
QString acceptLabel() const;
void setAcceptLabel(const QString &acceptLabel);
bool selectFolder() const;
void setSelectFolder(bool selectFolder);
QStringList results() const;
uint exec();
Q_SIGNALS:
void accepted(const QStringList &files);
void titleChanged();
void selectMultipleChanged();
void selectExistingChanged();
void nameFiltersChanged();
void mimeTypeFiltersChanged();
void folderChanged();
void currentFileChanged();
void acceptLabelChanged();
void selectFolderChanged();
private:
QQmlApplicationEngine *m_engine;
FileChooserQmlCallback *m_callback;
QStringList m_results;
QQuickWindow *m_window;
};
// SPDX-FileCopyrightText: 2020 Jonah Brüchert <jbb@kaidan.im>
//
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick 2.0
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.2 as Controls
import org.kde.kirigami 2.5 as Kirigami
import org.kde.kirigamifilepicker 0.1
Kirigami.OverlaySheet {
id: sheet
property string parentPath: ""
header: Kirigami.Heading {
text: i18n("Create new folder")
}
ColumnLayout {
Controls.Label {
Layout.fillWidth: true
wrapMode: Controls.Label.WordWrap
text: i18n("Create new folder in %1", sheet.parentPath.replace("file://", ""))
}
Controls.TextField {
id: nameField
Layout.fillWidth: true
placeholderText: i18n("folder name")
}
RowLayout {
Layout.fillWidth: true
Controls.Button {
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
text: i18n("Ok")
onClicked: {
DirModelUtils.mkdir(parentPath + "/" + nameField.text)
sheet.close()
}
}
Controls.Button {
Layout.alignment: Qt.AlignRight
Layout.fillWidth: true
text: i18n("Cancel")
onClicked: {
nameField.clear()
sheet.close()
}
}
}
}
}
// SPDX-FileCopyrightText: 2020 Jonah Brüchert <jbb@kaidan.im>
//
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.7
import QtQuick.Layouts 1.2
import QtQuick.Controls 2.2 as Controls
import org.kde.kirigami 2.5 as Kirigami
import org.kde.kirigamifilepicker 0.1
/**
* The FilePicker type provides a file picker wrapped in a Kirigmi.Page.
* It can be directly pushed to the pageStack.
*/
Kirigami.ScrollablePage {
id: root
property bool selectMultiple
property bool selectExisting
property var nameFilters: []
property var mimeTypeFilters: []
property alias folder: dirModel.folder
property string currentFile
property string acceptLabel
property bool selectFolder
CreateDirectorySheet {
id: createDirectorySheet
parentPath: dirModel.folder
}
contextualActions: [
Kirigami.Action {
icon.name: "folder"
text: i18n("Create folder")
visible: !root.selectExisting
onTriggered: createDirectorySheet.open()