Index: CMakeLists.txt =================================================================== --- CMakeLists.txt +++ CMakeLists.txt @@ -71,7 +71,7 @@ # NAMESPACE KF5:: # ) -find_package(KF5I18n ${KF5_DEP_VERSION} REQUIRED) +find_package(KF5 ${KF5_DEP_VERSION} COMPONENTS I18n KIO REQUIRED) # ecm_setup_version(${KF5_VERSION} # VARIABLE_PREFIX KIRIGAMIADDONS Index: src/CMakeLists.txt =================================================================== --- src/CMakeLists.txt +++ src/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(dateandtime) +add_subdirectory(filepicker) Index: src/filepicker/CMakeLists.txt =================================================================== --- /dev/null +++ src/filepicker/CMakeLists.txt @@ -0,0 +1,22 @@ +set(filepicker_SRCS + lib/dirmodel.cpp + lib/dirmodelutils.cpp + lib/plugin.cpp +) + +add_definitions(-DTRANSLATION_DOMAIN=\"kirigami_filepicker\") + +add_library(filepickerplugin SHARED ${filepicker_SRCS}) +target_link_libraries(filepickerplugin + Qt5::Quick + Qt5::Qml + KF5::I18n + KF5::KIOCore +) + +install(TARGETS filepickerplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigamiaddons/filepicker) +install(FILES + FilePicker.qml + qmldir + DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirigamiaddons/filepicker) + Index: src/filepicker/FilePicker.qml =================================================================== --- /dev/null +++ src/filepicker/FilePicker.qml @@ -0,0 +1,152 @@ +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.kirigamiaddons.filepicker 0.1 + +Kirigami.ScrollablePage { + id: root + + property bool selectMultiple + property bool selectExisting + property var nameFilters: [] + property var mimeTypeFilters: [] + property string folder: utils.homePath + property string currentFile + property string acceptLabel + property bool selectFolder + + // result + property url fileUrl + property var fileUrls: [] + + onFileUrlsChanged: print("root.fileUrls", root.fileUrls) + onSelectFolder: print("existing:", root.selectExisting) + + signal accepted(var urls) + + function addOrRemoveUrl(url) { + print("addOrRemoveUrl") + var index = root.fileUrls.indexOf(url) + if (index > -1) { + print("pop") + // remove element + root.fileUrls.splice(index, 1) + } else { + print("push") + root.fileUrls.push(url) + } + root.fileUrlsChanged() + } + + title: root.title + header: Controls.ToolBar { + Row { + Controls.ToolButton { + icon.name: "folder-root-symbolic" + height: parent.height + width: height + onClicked: dirModel.folder = "file:///" + } + + Repeater { + model: utils.getUrlParts(dirModel.folder) + + Controls.ToolButton { + icon.name: "arrow-right" + text: modelData + onClicked: dirModel.folder = utils.indexOfUrl( + dirModel.folder, index) + } + } + } + } + footer: RowLayout { + visible: !root.selectExisting + height: root.selectExisting ? 0 : Kirigami.Units.gridUnit * 2 + Controls.TextField { + Layout.fillHeight: true + Layout.fillWidth: true + id: fileNameField + placeholderText: i18n("File name") + } + Controls.ToolButton { + Layout.fillHeight: true + icon.name: "dialog-ok-apply" + onClicked: { + root.fileUrl = dirModel.folder + "/" + fileNameField.text + root.accepted([root.fileUrl]) + } + } + } + + mainAction: Kirigami.Action { + visible: (root.selectMultiple || root.selectFolder) && root.selectExisting + text: root.acceptLabel ? root.acceptLabel : i18n("Select") + icon.name: "object-select-symbolic" + + onTriggered: root.accepted(root.fileUrls) + } + + DirModel { + id: dirModel + folder: root.folder + showDotFiles: false + } + + DirModelUtils { + id: utils + } + + Controls.BusyIndicator { + anchors.centerIn: parent + + width: Kirigami.Units.gridUnit * 4 + height: width + + visible: dirModel.isLoading + } + + ListView { + anchors.fill: parent + model: dirModel + clip: true + + delegate: Kirigami.BasicListItem { + text: model.name + icon: model.iconName + checkable: root.selectMultiple + checked: root.fileUrls.includes(model.url) + + onClicked: { + print("selectFolder", root.selectFolder) + if (model.isDir) { + print("isDir") + if (root.selectFolder) { + if (root.selectMultiple) { + root.addOrRemoveUrl(model.url) + } else { + root.fileUrl = model.url + } + } + + dirModel.folder = model.url + } else { + if (!root.selectFolder) { + print("selectFolder is disabled") + if (root.selectMultiple) { + root.addOrRemoveUrl(model.url) + } else { + print("selectMultiple is disabled") + console.log("Accepted", "selectMultiple", root.selectMultiple) + root.fileUrl = model.url + root.fileUrls = [model.url] + root.accepted(root.fileUrls) + } + } + } + } + } + } +} Index: src/filepicker/lib/dirmodel.h =================================================================== --- /dev/null +++ src/filepicker/lib/dirmodel.h @@ -0,0 +1,103 @@ +/* + * Copyright 2019 Linus Jahn + * Copyright 2019 Jonah BrĂ¼chert + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef DIRMODEL_H +#define DIRMODEL_H + +#include +#include +#include +class KCoreDirLister; +class KFileItemList; + +class DirModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(const QUrl& folder READ folder WRITE setFolder NOTIFY folderChanged) + Q_PROPERTY(bool showDotFiles READ showDotFiles WRITE setShowDotFiles NOTIFY showDotFilesChanged) + Q_PROPERTY(bool isLoading READ isLoading NOTIFY isLoadingChanged) + Q_PROPERTY(QString nameFilter READ nameFilter NOTIFY nameFilterChanged) + Q_PROPERTY(QStringList mimeFilters READ mimeFilters NOTIFY mimeFiltersChanged) + +public: + enum Roles { + Name = Qt::UserRole + 1, + Url, + IconName, + IsDir, + IsLink, + FileSize, + MimeType, + IsHidden, + IsReadable, + IsWritable, + ModificationTime + }; + + Q_ENUM(Roles) + + explicit DirModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + QHash roleNames() const override; + + QUrl folder() const; + void setFolder(const QUrl &folder); + + bool showDotFiles() const; + void setShowDotFiles(bool showDotFiles); + + bool isLoading() const; + void setIsLoading(bool isLoading); + + QString nameFilter() const; + void setNameFilter(const QString &nameFilter); + + QStringList mimeFilters() const; + void setMimeFilters(const QStringList &mimeFilters); + +Q_SIGNALS: + void folderChanged(); + void showDotFilesChanged(); + void isLoadingChanged(); + void nameFilterChanged(); + void mimeFiltersChanged(); + +private Q_SLOTS: + void handleCompleted(); + void handleNewItems(const KFileItemList &items); + void handleItemsDeleted(const KFileItemList &items); + void handleRedirection(const QUrl &oldUrl, const QUrl &newUrl); + +private: + KCoreDirLister *m_lister; + QVector m_items; + + QUrl m_folder; + bool m_showDotFiles; + bool m_isLoading; + QString m_nameFilter; + QStringList m_mimeFilters; +}; + +#endif // DIRMODEL_H Index: src/filepicker/lib/dirmodel.cpp =================================================================== --- /dev/null +++ src/filepicker/lib/dirmodel.cpp @@ -0,0 +1,212 @@ +/* + * Copyright 2019 Linus Jahn + * Copyright 2019 Jonah BrĂ¼chert + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "dirmodel.h" +#include +#include +#include + +DirModel::DirModel(QObject *parent) + : QAbstractListModel(parent), m_lister(new KCoreDirLister(this)), m_showDotFiles(m_lister->showingDotFiles()), m_isLoading(false) +{ + connect(m_lister, QOverload<>::of(&KCoreDirLister::completed), this, &DirModel::handleCompleted); + connect(m_lister, &KCoreDirLister::newItems, this, &DirModel::handleNewItems); + connect(m_lister, &KCoreDirLister::itemsDeleted, this, &DirModel::handleItemsDeleted); + connect(m_lister, QOverload::of(&KCoreDirLister::redirection), this, &DirModel::handleRedirection); +} + +int DirModel::rowCount(const QModelIndex &) const +{ + return m_items.size(); +} + +QVariant DirModel::data(const QModelIndex &index, int role) const +{ + if (!hasIndex(index.row(), index.column(), index.parent())) + return {}; + + switch (role) { + case Name: + return m_items.at(index.row()).name(); + case Url: + return m_items.at(index.row()).url(); + case IconName: + return m_items.at(index.row()).iconName(); + case IsDir: + return m_items.at(index.row()).isDir(); + case IsLink: + return m_items.at(index.row()).isLink(); + case FileSize: + return m_items.at(index.row()).size(); + case MimeType: + return m_items.at(index.row()).mimetype(); + case IsHidden: + return m_items.at(index.row()).isHidden(); + case IsReadable: + return m_items.at(index.row()).isReadable(); + case IsWritable: + return m_items.at(index.row()).isWritable(); + case ModificationTime: + return m_items.at(index.row()).time(KFileItem::ModificationTime); + default: + return {}; + } +} + +QHash DirModel::roleNames() const +{ + QHash roles; + roles[Name] = "name"; + roles[Url] = "url"; + roles[IconName] = "iconName"; + roles[IsDir] = "isDir"; + roles[IsLink] = "isLink"; + roles[FileSize] = "fileSize"; + roles[MimeType] = "mimeType"; + roles[IsHidden] = "isHidden"; + roles[IsReadable] = "isReadable"; + roles[IsWritable] = "isWritable"; + roles[ModificationTime] = "modificationTime"; + + return roles; +} + +QUrl DirModel::folder() const +{ + return m_folder; +} + +void DirModel::setFolder(const QUrl &folder) +{ + if (folder != m_folder) { + beginResetModel(); + m_items.clear(); + endResetModel(); + + setIsLoading(true); + m_lister->openUrl(folder); + // Set m_folder to where we actually are, not where we wanted to be + m_folder = m_lister->url(); + + emit folderChanged(); + } +} + +void DirModel::handleCompleted() +{ + setIsLoading(false); +} + +void DirModel::handleNewItems(const KFileItemList &items) +{ + beginInsertRows(QModelIndex(), rowCount(), rowCount() + items.size() - 1); + + for (const auto &item : items) + m_items << item; + + endInsertRows(); +} + +void DirModel::handleItemsDeleted(const KFileItemList &items) +{ + qDebug() << "handleItemsDeletede" << items.size(); + int i = 0; + QMutableVectorIterator itr(m_items); + while (itr.hasNext()) { + if (items.contains(itr.next())) { + beginRemoveRows(QModelIndex(), i, i); + itr.remove(); + endRemoveRows(); + } else { + i++; + } + } +} + +void DirModel::handleRedirection(const QUrl &, const QUrl &newUrl) +{ + m_folder = newUrl; + emit folderChanged(); +} + +bool DirModel::isLoading() const +{ + return m_isLoading; +} + +void DirModel::setIsLoading(bool isLoading) +{ + if (m_isLoading != isLoading) { + m_isLoading = isLoading; + emit isLoadingChanged(); + } +} + +bool DirModel::showDotFiles() const +{ + return m_showDotFiles; +} + +void DirModel::setShowDotFiles(bool showDotFiles) +{ + if (showDotFiles != m_showDotFiles) { + m_showDotFiles = showDotFiles; + + m_lister->setShowingDotFiles(showDotFiles); + m_lister->emitChanges(); + + emit showDotFilesChanged(); + } +} + +QString DirModel::nameFilter() const +{ + return m_nameFilter; +} + +void DirModel::setNameFilter(const QString &nameFilter) +{ + if (nameFilter != m_nameFilter) { + m_nameFilter = nameFilter; + + m_lister->setNameFilter(nameFilter); + m_lister->emitChanges(); + + emit nameFilterChanged(); + } +} + +QStringList DirModel::mimeFilters() const +{ + return m_mimeFilters; +} + +void DirModel::setMimeFilters(const QStringList &mimeFilters) +{ + if (mimeFilters != m_mimeFilters) { + m_mimeFilters = mimeFilters; + + m_lister->setMimeFilter(mimeFilters); + m_lister->emitChanges(); + + emit mimeFiltersChanged(); + } +} Index: src/filepicker/lib/dirmodelutils.h =================================================================== --- /dev/null +++ src/filepicker/lib/dirmodelutils.h @@ -0,0 +1,23 @@ +#ifndef DIRMODELUTILS_H +#define DIRMODELUTILS_H + +#include + +class DirModelUtils : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString homePath READ homePath NOTIFY homePathChanged) + +public: + explicit DirModelUtils(QObject *parent = nullptr); + +public Q_SLOTS: + QStringList getUrlParts(const QUrl &url) const; + QUrl indexOfUrl(const QUrl &url, int index) const; + QString homePath() const; + +Q_SIGNALS: + void homePathChanged(); +}; + +#endif // DIRMODELUTILS_H Index: src/filepicker/lib/dirmodelutils.cpp =================================================================== --- /dev/null +++ src/filepicker/lib/dirmodelutils.cpp @@ -0,0 +1,36 @@ +#include "dirmodelutils.h" +#include +#include +#include + +DirModelUtils::DirModelUtils(QObject *parent) : QObject(parent) +{ +} + +QStringList DirModelUtils::getUrlParts(const QUrl &url) const +{ + if (url.path() == QStringLiteral("/")) + return QStringList(); + return url.path().split(QStringLiteral("/")).mid(1); +} + +QUrl DirModelUtils::indexOfUrl(const QUrl &url, int index) const +{ + const QStringList urlParts = url.path().split(QStringLiteral("/")); + QString path = QStringLiteral("/"); + for (int i = 0; i < index + 1; i++) { + path += urlParts.at(i + 1); + path += QStringLiteral("/"); + } + + qDebug() << path; + return QUrl::fromLocalFile(path); +} + +QString DirModelUtils::homePath() const +{ + QUrl url(QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); + url.setScheme(QStringLiteral("file")); + + return url.toString(); +} Index: src/filepicker/lib/plugin.h =================================================================== --- /dev/null +++ src/filepicker/lib/plugin.h @@ -0,0 +1,11 @@ +#ifndef PLUGIN_H +#define PLUGIN_H + + +class KirigamiAddonsFilePickerPlugin +{ +public: + KirigamiAddonsFilePickerPlugin(); +}; + +#endif // PLUGIN_H \ No newline at end of file Index: src/filepicker/lib/plugin.cpp =================================================================== --- /dev/null +++ src/filepicker/lib/plugin.cpp @@ -0,0 +1,32 @@ +#include +#include +#include +#include + +#include "dirmodel.h" +#include "dirmodelutils.h" + +class KirigamiAddonsFilePickerPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + KirigamiAddonsFilePickerPlugin() = default; + ~KirigamiAddonsFilePickerPlugin() override = default; + void initializeEngine(QQmlEngine *engine, const char *uri) override { + Q_UNUSED(engine) + Q_UNUSED(uri) + + engine->rootContext()->setContextObject(new KLocalizedContext()); + } + void registerTypes(const char *uri) override; +}; + +void KirigamiAddonsFilePickerPlugin::registerTypes(const char *uri) +{ + qmlRegisterType(uri, 0, 1, "DirModel"); + qmlRegisterType(uri, 0, 1, "DirModelUtils"); +} + +#include "plugin.moc" Index: src/filepicker/qmldir =================================================================== --- /dev/null +++ src/filepicker/qmldir @@ -0,0 +1,4 @@ +module org.kde.kirigamiaddons.filepicker +plugin filepickerplugin + +FilePicker 0.1 FilePicker.qml Index: tests/filepicker.qml =================================================================== --- /dev/null +++ tests/filepicker.qml @@ -0,0 +1,82 @@ +import QtQuick 2.5 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.2 as QQC2 +import org.kde.kirigami 2.5 as Kirigami +import org.kde.kirigamiaddons.filepicker 0.1 as Addon + +Kirigami.ApplicationWindow { + id: test + property bool selectMultiple: false + property bool selectExisting: false + property bool selectFolder: false + + property Addon.FilePicker filePicker + + title: "File Picker Test" + pageStack.initialPage: Kirigami.Page { + title: "Test Page" + Component { + id: filePickerComponent + + Addon.FilePicker { + //onAccepted: (urls) => print(urls) + } + } + Kirigami.FormLayout { + anchors.fill: parent + QQC2.Switch { + id: folderSwitch + Kirigami.FormData.label: "select folder" + checked: test.selectFolder + onCheckedChanged: test.selectFolder = !test.selectFolder + } + QQC2.Switch { + id: existingSwitch + Kirigami.FormData.label: "select existing" + checked: test.selectExisting + onCheckedChanged: test.selectExisting = !test.selectExisting + } + QQC2.Switch { + id: multipleSwitch + Kirigami.FormData.label: "select multiple" + checked: test.selectMultiple + onCheckedChanged: test.selectMultiple = !test.selectMultiple + } + QQC2.Button { + Kirigami.FormData.label: "File Picker" + text: "Open File Picker" + onClicked: { + console.log("send multiple:", test.selectMultiple) + test.filePicker = pageStack.push(filePickerComponent, { + "title": "Open File", + "selectMultiple": test.selectMultiple, + "selectExisting": test.selectExisting, + "selectFolder": test.selectFolder + }) + console.log(test.filePicker) + } + } + + Connections { + target: test.filePicker + + onFileUrlsChanged: console.log(filePicker.fileUrls) + } + + QQC2.Label { + Kirigami.FormData.label: "result" + text: { + if (test.filePicker) { + console.log("File picker object exists") + +// print("URL", test.filePicker.fileUrl) +// print("URLs", test.filePicker.fileUrls) + return test.selectMultiple ? JSON.stringify(test.filePicker.fileUrls) : test.filePicker.fileUrl + } + + return "" + } + } + } + } +}