diff --git a/kded/engine/vault.cpp b/kded/engine/vault.cpp index 3bf7139..36a283f 100644 --- a/kded/engine/vault.cpp +++ b/kded/engine/vault.cpp @@ -1,524 +1,574 @@ /* * Copyright 2017 by Ivan Cukic * * 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 "vault.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "backend_p.h" #include "asynqt/basic/all.h" #include "asynqt/wrappers/process.h" #include "asynqt/operations/listen.h" #include "asynqt/operations/cast.h" +#include + #define CFG_NAME "name" #define CFG_LAST_STATUS "lastStatus" #define CFG_MOUNT_POINT "mountPoint" #define CFG_BACKEND "backend" #define CFG_ACTIVITIES "activities" namespace PlasmaVault { class Vault::Private { public: Vault * const q; KSharedConfigPtr config; Device device; struct Data { QString name; MountPoint mountPoint; VaultInfo::Status status; QStringList activities; QString message; QString backendName; Backend::Ptr backend; }; using ExpectedData = AsynQt::Expected; ExpectedData data; void updateMessage(const QString &message) { if (!data) return; data->message = message; emit q->messageChanged(message); } void updateStatus() { if (data) { // Checking the status, and whether we should update it const auto oldStatus = data->status; const auto newStatus = isOpened() ? VaultInfo::Opened : isInitialized() ? VaultInfo::Closed : VaultInfo::NotInitialized; if (oldStatus == newStatus) return; data->status = newStatus; emit q->statusChanged(data->status); if (oldStatus == VaultInfo::Opened || newStatus == VaultInfo::Opened) { emit q->isOpenedChanged(newStatus); } if (oldStatus == VaultInfo::NotInitialized || newStatus == VaultInfo::NotInitialized) { emit q->isInitializedChanged(newStatus); } if (oldStatus == VaultInfo::Creating || oldStatus == VaultInfo::Opening || oldStatus == VaultInfo::Closing || oldStatus == VaultInfo::Destroying) { emit q->isBusyChanged(false); } // Saving the data for the current mount KConfigGroup generalConfig(config, "EncryptedDevices"); generalConfig.writeEntry(device.data(), true); KConfigGroup vaultConfig(config, device.data()); vaultConfig.writeEntry(CFG_LAST_STATUS, (int)data->status); vaultConfig.writeEntry(CFG_MOUNT_POINT, data->mountPoint.data()); vaultConfig.writeEntry(CFG_NAME, data->name); vaultConfig.writeEntry(CFG_BACKEND, data->backend->name()); vaultConfig.writeEntry(CFG_ACTIVITIES, data->activities); org::kde::KDirNotify::emitFilesAdded( QUrl::fromLocalFile(data->mountPoint.data())); config->sync(); } else { emit q->isOpenedChanged(false); emit q->isInitializedChanged(false); emit q->isBusyChanged(false); KConfigGroup generalConfig(config, "EncryptedDevices"); generalConfig.writeEntry(device.data(), false); KConfigGroup vaultConfig(config, device.data()); vaultConfig.writeEntry(CFG_LAST_STATUS, (int)VaultInfo::Error); // vaultConfig.deleteEntry(CFG_MOUNT_POINT); // vaultConfig.deleteEntry(CFG_NAME); // vaultConfig.deleteEntry(CFG_BACKEND); // vaultConfig.deleteEntry(CFG_ACTIVITIES); emit q->statusChanged(VaultInfo::Error); } config->sync(); } ExpectedData errorData(Error::Code error, const QString &message) const { qWarning() << "error: " << message; return ExpectedData::error(error, message); } ExpectedData loadVault(const Device &device, const QString &name = QString(), const MountPoint &mountPoint = MountPoint(), const Payload &payload = Payload()) const { if (!config->hasGroup(device.data())) { return errorData(Error::DeviceError, i18n("Unknown device")); } Data vaultData; const QString backendName = payload[KEY_BACKEND].toString(); const QStringList activities = payload[KEY_ACTIVITIES].toStringList(); // status should never be in this state, if we got an error, // d->data should not be valid vaultData.status = VaultInfo::Error; // Reading the mount data from the config const KConfigGroup vaultConfig(config, device.data()); vaultData.name = vaultConfig.readEntry(CFG_NAME, name); vaultData.mountPoint = MountPoint(vaultConfig.readEntry(CFG_MOUNT_POINT, mountPoint.data())); vaultData.backendName = vaultConfig.readEntry(CFG_BACKEND, backendName); vaultData.activities = vaultConfig.readEntry(CFG_ACTIVITIES, activities); const QDir mountPointDir(vaultData.mountPoint); return // If the backend is not known, we need to fail !Backend::availableBackends().contains(vaultData.backendName) ? errorData(Error::BackendError, i18n("Configured backend does not exist: %1", vaultData.backendName)) : // If the mount point is empty, we can not do anything vaultData.mountPoint.isEmpty() ? errorData(Error::MountPointError, i18n("Mount point is not specified")) : // Lets try to create the mount point !mountPointDir.exists() && !QDir().mkpath(vaultData.mountPoint) ? errorData(Error::MountPointError, i18n("Can not create the mount point")) : // Instantiate the backend if possible !(vaultData.backend = Backend::instance(vaultData.backendName)) ? errorData(Error::BackendError, i18n("Configured backend can not be instantiated: %1", vaultData.backendName)) : // otherwise ExpectedData::success(vaultData); } Private(Vault *parent, const Device &device) : q(parent) , config(KSharedConfig::openConfig(PLASMAVAULT_CONFIG_FILE)) , device(device) , data(loadVault(device)) { updateStatus(); } template T followFuture(VaultInfo::Status whileNotFinished, const T &future) { using namespace AsynQt::operators; emit q->isBusyChanged(true); data->status = whileNotFinished; return future | onSuccess([this] { updateStatus(); }); } bool isInitialized() const { return data && data->backend->isInitialized(device); } bool isOpened() const { return data && data->backend->isOpened(data->mountPoint); } }; Vault::Vault(const Device &device, QObject *parent) : QObject(parent) , d(new Private(this, device)) { } Vault::~Vault() { close(); } FutureResult<> Vault::create(const QString &name, const MountPoint &mountPoint, const Payload &payload) { using namespace AsynQt::operators; return // If the backend is already known, and the device is initialized, // we do not want to do it again d->data && d->data->backend->isInitialized(d->device) ? errorResult(Error::DeviceError, i18n("This device is already registered. Can not recreate it.")) : // Mount not open, check the error messages !(d->data = d->loadVault(d->device, name, mountPoint, payload)) ? errorResult(Error::BackendError, i18n("Unknown error, unable to create the backend.")) : // otherwise d->followFuture(VaultInfo::Creating, d->data->backend->initialize(name, d->device, mountPoint, payload)) | onSuccess([mountPoint] { // If we have successfully created the vault, // lets try to set its icon QFile dotDir(mountPoint + "/.directory"); if (dotDir.open(QIODevice::WriteOnly | QIODevice::Text)) { QTextStream out(&dotDir); out << "[Desktop Entry]\nIcon=folder-decrypted\n"; } }); } FutureResult<> Vault::open(const Payload &payload) { return // We can not mount something that has not been registered // with us before !d->data ? errorResult(Error::BackendError, i18n("Can not open an unknown vault.")) : // otherwise d->followFuture(VaultInfo::Opening, d->data->backend->open(d->device, d->data->mountPoint, payload)); } FutureResult<> Vault::close() { using namespace AsynQt::operators; return // We can not mount something that has not been registered // with us before !d->data ? errorResult(Error::BackendError, i18n("The vault is unknown, can not close it.")) : // otherwise d->followFuture(VaultInfo::Closing, d->data->backend->close(d->device, d->data->mountPoint)) | onSuccess([this] (const Result<> &result) { if (!isOpened() || result) { d->updateMessage(QString()); } else { // We want to check whether there is an application // that is accessing the vault AsynQt::Process::getOutput("lsof", { "-t", mountPoint() }) | cast() | onError([this] { d->updateMessage(i18n("Unable to close the vault, an application is using it")); }) | onSuccess([this] (const QString &result) { // based on ksolidnotify.cpp QStringList blockApps; const auto &pidList = result.split(QRegExp(QStringLiteral("\\s+")), QString::SkipEmptyParts); - KSysGuard::Processes procs; + if (pidList.size() == 0) { + d->updateMessage(i18n("Unable to close the vault, an application is using it")); + close(); - for (const QString &pidStr: pidList) { - int pid = pidStr.toInt(); - if (!pid) { - continue; - } + } else { + KSysGuard::Processes procs; + + for (const QString &pidStr: pidList) { + int pid = pidStr.toInt(); + if (!pid) { + continue; + } - procs.updateOrAddProcess(pid); + procs.updateOrAddProcess(pid); - KSysGuard::Process *proc = procs.getProcess(pid); + KSysGuard::Process *proc = procs.getProcess(pid); - if (!blockApps.contains(proc->name())) { - blockApps << proc->name(); + if (!blockApps.contains(proc->name())) { + blockApps << proc->name(); + } } - } - blockApps.removeDuplicates(); + blockApps.removeDuplicates(); - d->updateMessage(i18n("Unable to close the vault, it is used by %1", blockApps.join(", "))); + d->updateMessage(i18n("Unable to close the vault, it is used by %1", blockApps.join(", "))); + } }); } }); } +FutureResult<> Vault::configure() +{ + return close(); +} + + + +FutureResult<> Vault::forceClose() +{ + using namespace AsynQt::operators; + + AsynQt::await( + AsynQt::Process::getOutput("lsof", { "-t", mountPoint() }) + | cast() + | onError([this] { + d->updateMessage(i18n("Failed to fetch the list of applications using this vault")); + }) + | onSuccess([this] (const QString &result) { + // based on ksolidnotify.cpp + QStringList blockApps; + + const auto &pidList = + result.split(QRegExp(QStringLiteral("\\s+")), + QString::SkipEmptyParts); + + KSysGuard::Processes procs; + + for (const QString &pidStr: pidList) { + int pid = pidStr.toInt(); + if (!pid) { + continue; + } + + procs.sendSignal(pid, SIGKILL); + } + })); + + return close(); +} + + + FutureResult<> Vault::destroy(const Payload &payload) { return // We can not mount something that has not been registered // with us before !d->data ? errorResult(Error::BackendError, i18n("The vault is unknown, can not destroy it.")) : // otherwise d->followFuture(VaultInfo::Destroying, d->data->backend->destroy(d->device, d->data->mountPoint, payload)); } VaultInfo::Status Vault::status() const { return d->data->status; } bool Vault::isValid() const { return d->data; } QString Vault::name() const { return d->data->name; } Device Vault::device() const { return d->device; } MountPoint Vault::mountPoint() const { return d->data->mountPoint; } QList Vault::availableDevices() { const auto config = KSharedConfig::openConfig(PLASMAVAULT_CONFIG_FILE); const KConfigGroup general(config, "EncryptedDevices"); QList results; for (const auto& item: general.keyList()) { results << Device(item); } return results; } QStringList Vault::statusMessage() { for (const auto& backendName: Backend::availableBackends()) { auto backend = Backend::instance(backendName); backend->validateBackend(); } return {}; } QString Vault::message() const { if (!d->data) { return d->data.error().message(); } else { return d->data->message; } } bool Vault::isInitialized() const { return d->isInitialized(); } bool Vault::isOpened() const { return d->isOpened(); } QStringList Vault::activities() const { return d->data->activities; } bool Vault::isBusy() const { if (!d->data) { return false; } switch (status()) { case VaultInfo::Creating: case VaultInfo::Opening: case VaultInfo::Closing: case VaultInfo::Destroying: return true; default: return false; } } VaultInfo Vault::info() const { VaultInfo vaultInfo; vaultInfo.device = device(); vaultInfo.name = name(); vaultInfo.status = status(); vaultInfo.activities = activities(); vaultInfo.message = message(); return vaultInfo; } } // namespace PlasmaVault diff --git a/kded/engine/vault.h b/kded/engine/vault.h index 07285a9..66ab98d 100644 --- a/kded/engine/vault.h +++ b/kded/engine/vault.h @@ -1,113 +1,116 @@ /* * Copyright 2017 by Ivan Cukic * * 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 PLASMAVAULT_KDED_ENGINE_VAULT_H #define PLASMAVAULT_KDED_ENGINE_VAULT_H #include #include #include #include #include #include "types.h" #include "commandresult.h" class QDBusArgument; namespace PlasmaVault { #define KEY_NAME "vault-name" #define KEY_BACKEND "vault-backend" #define KEY_PASSWORD "vault-password" #define KEY_DEVICE "vault-device" #define KEY_MOUNT_POINT "vault-mount-point" #define KEY_ACTIVITIES "vault-activities" class Vault: public QObject { Q_OBJECT Q_PROPERTY(PlasmaVault::Device device READ device) Q_PROPERTY(QString mountPoint READ mountPoint NOTIFY mountPointChanged) Q_PROPERTY(VaultInfo::Status status READ status NOTIFY statusChanged) Q_PROPERTY(bool isInitialized READ isInitialized NOTIFY isInitializedChanged) Q_PROPERTY(bool isOpened READ isOpened NOTIFY isOpenedChanged) Q_PROPERTY(bool isBusy READ isBusy NOTIFY isBusyChanged) Q_PROPERTY(QStringList activities READ activities NOTIFY activitiesChanged) Q_PROPERTY(QString message READ message NOTIFY messageChanged) public: Vault(const Device &device, QObject *parent = nullptr); ~Vault(); typedef QHash Payload; bool isValid() const; FutureResult<> create(const QString &name, const MountPoint &mountPoint, const Payload &payload); FutureResult<> open(const Payload &payload); FutureResult<> close(); + FutureResult<> configure(); + FutureResult<> forceClose(); + FutureResult<> destroy(const Payload &payload); VaultInfo info() const; public Q_SLOTS: QString message() const; VaultInfo::Status status() const; bool isInitialized() const; bool isOpened() const; bool isBusy() const; QString name() const; Device device() const; MountPoint mountPoint() const; QStringList activities() const; Q_SIGNALS: void mountPointChanged(const QString &mountPoint); void statusChanged(VaultInfo::Status status); void isInitializedChanged(bool isInitialized); void isOpenedChanged(bool isOpened); void isBusyChanged(bool isBusy); void activitiesChanged(const QStringList &activities); void messageChanged(const QString &message); public: static QList availableDevices(); static QStringList statusMessage(); private: class Private; QScopedPointer d; }; } // namespace PlasmaVault #endif // include guard diff --git a/kded/service.cpp b/kded/service.cpp index b2056d0..67e9434 100644 --- a/kded/service.cpp +++ b/kded/service.cpp @@ -1,251 +1,298 @@ /* * Copyright 2017 by Ivan Cukic * * 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 "service.h" #include #include #include #include #include +#include #include "engine/vault.h" #include "engine/commandresult.h" #include "ui/vaultcreationwizard.h" K_PLUGIN_FACTORY_WITH_JSON(PlasmaVaultServiceFactory, "plasmavault.json", registerPlugin();) using namespace PlasmaVault; class PlasmaVaultService::Private { public: QHash knownVaults; KActivities::Consumer kamd; + Vault* vaultFor(const QString &device_) const + { + const Device device(device_); + + if (!knownVaults.contains(device)) { + return nullptr; + } + + return knownVaults[device]; + } + }; PlasmaVaultService::PlasmaVaultService(QObject * parent, const QVariantList&) : KDEDModule(parent) , d(new Private()) { connect(this, &KDEDModule::moduleRegistered, this, &PlasmaVaultService::slotRegistered); connect(&d->kamd, &KActivities::Consumer::currentActivityChanged, this, &PlasmaVaultService::onCurrentActivityChanged); init(); } PlasmaVaultService::~PlasmaVaultService() { } void PlasmaVaultService::init() { for (const Device &device: Vault::availableDevices()) { registerVault(new Vault(device, this)); } } PlasmaVault::VaultInfoList PlasmaVaultService::availableDevices() const { PlasmaVault::VaultInfoList result; for (const auto &vault: d->knownVaults.values()) { const auto vaultData = vault->info(); result << vaultData; } return result; } void PlasmaVaultService::requestNewVault() { const auto dialog = new VaultCreationWizard(); connect(dialog, &VaultCreationWizard::createdVault, this, &PlasmaVaultService::registerVault); dialog->show(); } void PlasmaVaultService::slotRegistered(const QDBusObjectPath &path) { if (path.path() == QLatin1String("/modules/plasmavault")) { emit registered(); } } void PlasmaVaultService::registerVault(Vault *vault) { if (!vault->isValid()) { qWarning() << "Warning: Trying to register an invalid vault: " << vault->device(); return; } if (d->knownVaults.contains(vault->device())) { qWarning() << "Warning: This one is already registered: " << vault->device(); return; } vault->setParent(this); d->knownVaults[vault->device()] = vault; connect(vault, &Vault::statusChanged, this, &PlasmaVaultService::onVaultStatusChanged); connect(vault, &Vault::messageChanged, this, &PlasmaVaultService::onVaultMessageChanged); emit vaultAdded(vault->info()); } void PlasmaVaultService::onVaultStatusChanged(VaultInfo::Status status) { Q_UNUSED(status); const auto vault = qobject_cast(sender()); emit vaultChanged(vault->info()); } void PlasmaVaultService::onVaultMessageChanged(const QString &message) { Q_UNUSED(message); const auto vault = qobject_cast(sender()); emit vaultChanged(vault->info()); } template class PasswordMountDialog: protected KPasswordDialog { //_ public: PasswordMountDialog(Vault *vault, Function function) : m_vault(vault) , m_function(function) { } void show() { KPasswordDialog::show(); } private: bool checkPassword() override { auto future = m_vault->open({ { KEY_PASSWORD, password() } }); const auto result = AsynQt::await(future); if (result) { m_function(); return true; } else { showErrorMessage(result.error().message()); return false; } } void hideEvent(QHideEvent *) override { deleteLater(); } Vault *m_vault; Function m_function; }; template void showPasswordMountDialog(Vault *vault, Function &&function) { auto dialog = new PasswordMountDialog( vault, std::forward(function)); dialog->show(); } //^ -void PlasmaVaultService::openVault(const QString &device_) +void PlasmaVaultService::openVault(const QString &device) { - const Device device(device_); + if (auto vault = d->vaultFor(device)) { + if (vault->isOpened()) return; - if (!d->knownVaults.contains(device)) return; - const auto vault = d->knownVaults[device]; + showPasswordMountDialog(vault, + [this, vault] { + emit vaultChanged(vault->info()); + }); + } +} - showPasswordMountDialog(vault, - [this, device] { - emit vaultChanged(d->knownVaults[device]->info()); - }); + + +void PlasmaVaultService::closeVault(const QString &device) +{ + if (auto vault = d->vaultFor(device)) { + if (!vault->isOpened()) return; + + vault->close(); + } +} + + + +void PlasmaVaultService::configureVault(const QString &device) +{ + if (auto vault = d->vaultFor(device)) { + vault->configure(); + } } -void PlasmaVaultService::closeVault(const QString &device_) +void PlasmaVaultService::forceCloseVault(const QString &device) { - const Device device(device_); + if (auto vault = d->vaultFor(device)) { + if (!vault->isOpened()) return; + + vault->forceClose(); + } +} - if (!d->knownVaults.contains(device)) return; - auto vault = d->knownVaults[device]; - vault->close(); + +void PlasmaVaultService::openVaultInFileManager(const QString &device) +{ + if (auto vault = d->vaultFor(device)) { + if (vault->isOpened()) { + new KRun(QUrl::fromLocalFile((QString)vault->mountPoint()), 0); + + } else { + showPasswordMountDialog(vault, [this, vault] { + emit vaultChanged(vault->info()); + new KRun(QUrl::fromLocalFile((QString)vault->mountPoint()), 0); + }); + } + } } void PlasmaVaultService::onCurrentActivityChanged( const QString ¤tActivity) { for (auto* vault: d->knownVaults.values()) { const auto vaultActivities = vault->activities(); if (!vaultActivities.isEmpty() && !vaultActivities.contains(currentActivity)) { vault->close(); } } } #include "service.moc" diff --git a/kded/service.h b/kded/service.h index e653d32..e618132 100644 --- a/kded/service.h +++ b/kded/service.h @@ -1,71 +1,75 @@ /* * Copyright 2017 by Ivan Cukic * * 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 PLASMAVAULT_KDED_SERVICE_H #define PLASMAVAULT_KDED_SERVICE_H #include #include #include #include class Q_DECL_EXPORT PlasmaVaultService : public KDEDModule { Q_CLASSINFO("D-Bus Interface", "org.kde.plasmavault") Q_OBJECT public: PlasmaVaultService(QObject *parent, const QVariantList&); ~PlasmaVaultService(); public Q_SLOTS: Q_SCRIPTABLE void init(); Q_SCRIPTABLE void requestNewVault(); Q_SCRIPTABLE void openVault(const QString &device); Q_SCRIPTABLE void closeVault(const QString &device); + Q_SCRIPTABLE void configureVault(const QString &device); + Q_SCRIPTABLE void forceCloseVault(const QString &device); + Q_SCRIPTABLE void openVaultInFileManager(const QString &device); + Q_SCRIPTABLE PlasmaVault::VaultInfoList availableDevices() const; Q_SIGNALS: void registered(); Q_SCRIPTABLE void vaultAdded(const PlasmaVault::VaultInfo &vaultData); Q_SCRIPTABLE void vaultRemoved(const QString &device); Q_SCRIPTABLE void vaultChanged(const PlasmaVault::VaultInfo &vaultData); private Q_SLOTS: void slotRegistered(const QDBusObjectPath &path); void registerVault(PlasmaVault::Vault *vault); void onVaultStatusChanged(PlasmaVault::VaultInfo::Status status); void onVaultMessageChanged(const QString &message); void onCurrentActivityChanged(const QString ¤tActivity); private: class Private; QScopedPointer d; }; #endif // include guard diff --git a/plasma/package/contents/ui/ActionItem.qml b/plasma/package/contents/ui/ActionItem.qml new file mode 100644 index 0000000..7cf3d01 --- /dev/null +++ b/plasma/package/contents/ui/ActionItem.qml @@ -0,0 +1,77 @@ +/* + * Copyright 2017 by Ivan Cukic + * + * 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 . + */ + +import QtQuick 2.1 +import QtQuick.Layouts 1.1 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtras + +MouseArea { + id: vaultItem + + property alias icon: actionIcon.source + property alias text: actionText.text + + signal activated() + + hoverEnabled: true + height: units.iconSizes.medium + + onContainsMouseChanged: { + vaultItem.ListView.view.currentIndex = (containsMouse ? index : -1) + } + + PlasmaCore.IconItem { + id: actionIcon + + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + margins: units.smallSpacing + } + + width: units.iconSizes.medium + height: units.iconSizes.medium + } + + PlasmaComponents.Label { + id: actionText + + anchors { + left: actionIcon.right + verticalCenter: parent.verticalCenter + margins: units.smallSpacing + } + + width: parent.width + height: undefined + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + onClicked: { + vaultItem.activated(); + } + } +} + diff --git a/plasma/package/contents/ui/VaultItem.qml b/plasma/package/contents/ui/VaultItem.qml index efafc7f..9b8939f 100644 --- a/plasma/package/contents/ui/VaultItem.qml +++ b/plasma/package/contents/ui/VaultItem.qml @@ -1,140 +1,229 @@ /* * Copyright 2017 by Ivan Cukic * * 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 . */ import QtQuick 2.1 import QtQuick.Layouts 1.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras MouseArea { id: vaultItem property alias icon: vaultIcon.source property alias name: vaultName.text property alias message: vaultMessage.text + property string device: "" property bool isOpened: false property bool isBusy: false + signal itemExpanded(variant item); + + function collapse() { + actionsList.visible = false; + } + property var vaultsModelActions: plasmoid.nativeInterface.vaultsModel.source() hoverEnabled: true - height: units.iconSizes.medium + (actionsList.visible ? actionsList.height : 0) + height: units.iconSizes.medium + 2 * units.smallSpacing + + (actionsList.visible ? (actionsList.height + actionsListSpacer.height) : 0) onContainsMouseChanged: { vaultItem.ListView.view.currentIndex = (containsMouse ? index : -1) } PlasmaCore.IconItem { id: vaultIcon anchors { left: parent.left - verticalCenter: parent.verticalCenter + top: parent.top margins: units.smallSpacing } width: units.iconSizes.medium height: units.iconSizes.medium PlasmaCore.IconItem { anchors { left: parent.left bottom: parent.bottom } width: units.iconSizes.small height: units.iconSizes.small visible: source != "" source: { return vaultItem.message != "" ? "emblem-error" : vaultItem.isOpened ? "emblem-mounted" : "" } } } PlasmaComponents.ToolButton { id: buttonOpenClose anchors { right: parent.right - verticalCenter: parent.verticalCenter + top: parent.top margins: units.smallSpacing } width: units.iconSizes.medium height: units.iconSizes.medium visible: !busyIndicator.visible iconName: isOpened ? "media-eject" : "media-mount" - onClicked: vaultsModelActions.toggle(model.device) + onClicked: { + vaultsModelActions.toggle(vaultItem.device); + collapse(); + itemExpanded(null); + } } PlasmaComponents.BusyIndicator { id: busyIndicator anchors.fill: buttonOpenClose visible: isBusy } Column { anchors { left: vaultIcon.right right: buttonOpenClose.left verticalCenter: parent.verticalCenter margins: units.smallSpacing leftMargin: 2 * units.smallSpacing } PlasmaComponents.Label { id: vaultName width: parent.width height: undefined elide: Text.ElideRight } PlasmaComponents.Label { id: vaultMessage width: parent.width height: undefined elide: Text.ElideRight visible: { return vaultMessage.text != ""; } } + + Item { + id: actionsListSpacer + + height: units.largeSpacing + width: parent.width + visible: actionsList.visible + } + + ListView { + id: actionsList + + height: units.iconSizes.medium * model.count + width: parent.width + visible: false + + highlight: PlasmaComponents.Highlight { + id: highlight + } + + model: actionsModel + + ListModel { + id: actionsModel + } + + delegate: ActionItem { + icon: model.icon + text: model.text + + width: parent.width + + onActivated: { + if (model.action == "file-manager") { + vaultsModelActions.openInFileManager(vaultItem.device); + + } else if (model.action == "force-close") { + vaultsModelActions.forceClose(vaultItem.device); + + } else if (model.action == "configure") { + vaultsModelActions.configure(vaultItem.device); + + } + + collapse(); + itemExpanded(null); + } + } + } } MouseArea { anchors.fill: parent + + visible: !actionsList.visible + onClicked: { - console.log("TODO: Mount and open dolphin"); + actionsList.visible = !actionsList.visible; + if (actionsList.visible) { + vaultItem.itemExpanded(vaultItem); + + actionsModel.clear(); + + actionsModel.append({ + "icon" : "system-file-manager", + "text" : i18nd("plasmavault-kde", "Open with File Manager"), + "action" : "file-manager" + }); + + if (vaultItem.message != "") { + actionsModel.append({ + "icon" : "window-close", + "text" : i18nd("plasmavault-kde", "Forcefully close "), + "action" : "force-close" + }); + } + + // actionsModel.append({ + // "icon" : "configure", + // "text" : i18nd("plasmavault-kde", "Configure Vault..."), + // "action" : "configure" + // }); + } } } } diff --git a/plasma/package/contents/ui/main.qml b/plasma/package/contents/ui/main.qml index b2de5dc..f0e5e8d 100644 --- a/plasma/package/contents/ui/main.qml +++ b/plasma/package/contents/ui/main.qml @@ -1,93 +1,103 @@ /* * Copyright 2017 by Ivan Cukic * * 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 . */ import QtQuick 2.1 import QtQuick.Layouts 1.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras Item { property var vaultsModel: plasmoid.nativeInterface.vaultsModel property var vaultsModelActions: plasmoid.nativeInterface.vaultsModel.source() + property var expandedItem: null + Binding { target: plasmoid property: "busy" value: vaultsModelActions.isBusy } Binding { target: plasmoid property: "icon" value: { return vaultsModelActions.hasError ? "plasmavault_error" : "plasmavault"; } } Plasmoid.fullRepresentation: ColumnLayout { anchors { fill: parent } Layout.minimumWidth: 200 Layout.minimumHeight: 200 // PlasmaExtras.Heading { // text: i18nd("plasmavault-kde", "Encrypted vaults") // } ListView { + id: vaultsList model: vaultsModel currentIndex: -1 Layout.fillWidth: true Layout.fillHeight: true highlight: PlasmaComponents.Highlight { id: highlight } delegate: VaultItem { icon: model.icon name: model.name message: model.message isOpened: model.isOpened + device: model.device width: parent.width - height: units.iconSizes.medium + 2 * units.smallSpacing + + onItemExpanded: { + if (expandedItem != null) { + expandedItem.collapse(); + } + expandedItem = item; + } } interactive: false } PlasmaComponents.Button { id: buttonCreateNewVault text: i18nd("plasmavault-kde", "Create a New Vault") onClicked: vaultsModelActions.requestNewVault() } } } diff --git a/plasma/vaultsmodel.cpp b/plasma/vaultsmodel.cpp index 2ce2917..41658bd 100644 --- a/plasma/vaultsmodel.cpp +++ b/plasma/vaultsmodel.cpp @@ -1,410 +1,436 @@ /* * Copyright 2017 by Ivan Cukic * * 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 "vaultsmodel.h" #include "vaultsmodel_p.h" #include #include #include using namespace PlasmaVault; using namespace AsynQt; using namespace AsynQt::operators; VaultsModel::Private::Private(VaultsModel *parent) : service( "org.kde.kded5" , "/modules/plasmavault" , QDBusConnection::sessionBus() ) , serviceWatcher( "org.kde.kded5" , QDBusConnection::sessionBus() ) , q(parent) { connect(&service, &org::kde::plasmavault::vaultAdded, this, &Private::onVaultAdded); connect(&service, &org::kde::plasmavault::vaultChanged, this, &Private::onVaultChanged); connect(&service, &org::kde::plasmavault::vaultRemoved, this, &Private::onVaultRemoved); connect(&serviceWatcher, &QDBusServiceWatcher::serviceOwnerChanged, this, [this] (const QString &service, const QString &oldOwner, const QString &newOwner) { Q_UNUSED(oldOwner); if (service != "org.kde.kded5") { return; } // If kded is not running, just clear all vault info, // otherwise load the data if (newOwner.isEmpty()) { clearData(); } else { loadData(); } }); // Try to load the data. This should start kded if it is not running // for some reason loadData(); } void VaultsModel::Private::loadData() { // Before loading the new data, lets forget everything clearData(); // Asynchronously load the devices DBus::asyncCall(&service, "availableDevices") | onSuccess([this] (const VaultInfoList &vaultList) { q->beginResetModel(); vaults.clear(); vaultKeys.clear(); busyVaults.clear(); errorVaults.clear(); for (const auto& vault: vaultList) { vaults[vault.device] = vault; vaultKeys << vault.device; if (vault.isBusy()) { busyVaults << vault.device; } if (!vault.message.isEmpty()) { errorVaults << vault.device; } } q->endResetModel(); emit q->isBusyChanged(busyVaults.count() != 0); emit q->hasErrorChanged(errorVaults.count() != 0); }); } void VaultsModel::Private::clearData() { q->beginResetModel(); vaultKeys.clear(); vaults.clear(); q->endResetModel(); } void VaultsModel::Private::onVaultAdded(const PlasmaVault::VaultInfo &vaultInfo) { const auto device = vaultInfo.device; if (vaults.contains(device)) return; q->beginInsertRows(QModelIndex(), vaultKeys.size(), vaultKeys.size()); vaults[device] = vaultInfo; vaultKeys << device; q->endInsertRows(); } void VaultsModel::Private::onVaultRemoved(const QString &device) { if (vaults.contains(device)) return; const auto row = vaultKeys.indexOf(device); q->beginRemoveRows(QModelIndex(), row, row); vaultKeys.removeAt(row); vaults.remove(device); q->endRemoveRows(); } void VaultsModel::Private::onVaultChanged( const PlasmaVault::VaultInfo &vaultInfo) { const auto device = vaultInfo.device; if (!vaultKeys.contains(device)) return; const auto row = vaultKeys.indexOf(device); // Lets see whether this warrants updates to the busy flag if (vaultInfo.isBusy() && !busyVaults.contains(device)) { busyVaults << device; if (busyVaults.count() == 1) { emit q->isBusyChanged(true); } } if (!vaultInfo.isBusy() && busyVaults.contains(device)) { busyVaults.remove(device); if (busyVaults.count() == 0) { emit q->isBusyChanged(false); } } // Lets see whether this warrants updates to the error flag if (!vaultInfo.message.isEmpty() && !errorVaults.contains(device)) { errorVaults << device; if (errorVaults.count() == 1) { emit q->hasErrorChanged(true); } } if (vaultInfo.message.isEmpty() && errorVaults.contains(device)) { errorVaults.remove(device); if (errorVaults.count() == 0) { emit q->hasErrorChanged(false); } } vaults[device] = vaultInfo; q->dataChanged(q->index(row), q->index(row)); } VaultsModel::VaultsModel(QObject *parent) : QAbstractListModel(parent) , d(new Private(this)) { } VaultsModel::~VaultsModel() { } int VaultsModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return d->vaultKeys.size(); } QVariant VaultsModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return {}; } const int row = index.row(); if (row >= d->vaultKeys.count()) { return {}; } const auto device = d->vaultKeys[row]; const auto &vault = d->vaults[device]; switch (role) { case VaultDevice: return vault.device; case VaultMountPoint: return vault.mountPoint; case VaultName: return vault.name.isEmpty() ? vault.device : vault.name; case VaultIcon: { QFileInfo fileInfo(vault.device); if (!fileInfo.exists()) { return "document-close"; } else switch (vault.status) { case VaultInfo::Error: return "document-close"; case VaultInfo::NotInitialized: return "folder-gray"; case VaultInfo::Closed: return "folder-encrypted"; case VaultInfo::Opened: return "folder-decrypted"; default: return ""; } } case VaultIsBusy: return vault.isBusy(); case VaultIsOpened: return vault.isOpened(); case VaultActivities: return vault.activities; case VaultMessage: return vault.message; } return {}; } QHash VaultsModel::roleNames() const { return { { VaultName, "name" }, { VaultIcon, "icon" }, { VaultDevice, "device" }, { VaultMountPoint, "mountPoint" }, { VaultIsBusy, "isBusy" }, { VaultIsOpened, "isOpened" }, { VaultActivities, "activities" }, { VaultStatus, "status" }, { VaultMessage, "message" } }; } void VaultsModel::refresh() { d->loadData(); } void VaultsModel::open(const QString &device) { if (!d->vaults.contains(device)) return; DBus::asyncCall<>(&d->service, "openVault", device); } void VaultsModel::close(const QString &device) { if (!d->vaults.contains(device)) return; DBus::asyncCall<>(&d->service, "closeVault", device); } void VaultsModel::toggle(const QString &device) { if (!d->vaults.contains(device)) return; const auto &vault = d->vaults[device]; if (vault.status == VaultInfo::Opened) { close(device); } else if (vault.status == VaultInfo::Closed) { open(device); } } +void VaultsModel::forceClose(const QString &device) +{ + if (!d->vaults.contains(device)) return; + + DBus::asyncCall<>(&d->service, "forceCloseVault", device); +} + + + +void VaultsModel::configure(const QString &device) +{ + if (!d->vaults.contains(device)) return; + + DBus::asyncCall<>(&d->service, "configureVault", device); +} + + + +void VaultsModel::openInFileManager(const QString &device) +{ + if (!d->vaults.contains(device)) return; + + DBus::asyncCall<>(&d->service, "openVaultInFileManager", device); +} + + void VaultsModel::requestNewVault() { DBus::asyncCall<>(&d->service, "requestNewVault"); } bool VaultsModel::isBusy() const { return !d->busyVaults.isEmpty(); } bool VaultsModel::hasError() const { return !d->errorVaults.isEmpty(); } SortedVaultsModelProxy::SortedVaultsModelProxy(QObject *parent) : QSortFilterProxyModel(parent) , m_source(new VaultsModel(this)) , m_kamd(new KActivities::Consumer(this)) { setSourceModel(m_source); connect(m_kamd, &KActivities::Consumer::currentActivityChanged, this, &SortedVaultsModelProxy::invalidate); connect(m_kamd, &KActivities::Consumer::serviceStatusChanged, this, &SortedVaultsModelProxy::invalidate); } bool SortedVaultsModelProxy::lessThan(const QModelIndex &left, const QModelIndex &right) const { const auto leftData = sourceModel()->data(left, VaultsModel::VaultName); const auto rightData = sourceModel()->data(right, VaultsModel::VaultName); return leftData < rightData; } bool SortedVaultsModelProxy::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { Q_UNUSED(sourceParent); const auto activities = m_source->index(sourceRow).data(VaultsModel::VaultActivities).toStringList(); const auto isOpened = m_source->index(sourceRow).data(VaultsModel::VaultIsOpened).toBool(); return activities.size() == 0 || isOpened || activities.contains(m_kamd->currentActivity()); } QObject *SortedVaultsModelProxy::source() const { return sourceModel(); } diff --git a/plasma/vaultsmodel.h b/plasma/vaultsmodel.h index 3c86095..f057011 100644 --- a/plasma/vaultsmodel.h +++ b/plasma/vaultsmodel.h @@ -1,111 +1,120 @@ /* * Copyright 2017 by Ivan Cukic * * 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 PLASMAVAULT_PLASMA_VAULTSMODEL_H #define PLASMAVAULT_PLASMA_VAULTSMODEL_H #include #include #include #include class VaultsModel: public QAbstractListModel { Q_OBJECT Q_PROPERTY(bool isBusy READ isBusy NOTIFY isBusyChanged) Q_PROPERTY(bool hasError READ hasError NOTIFY hasErrorChanged) public: VaultsModel(QObject *parent = nullptr); ~VaultsModel(); int rowCount(const QModelIndex &parent) const override; QVariant data(const QModelIndex &index, int role) const override; QHash roleNames() const override; enum Roles { VaultName = Qt::UserRole + 1, ///< The user defined name of the vault VaultDevice, ///< Path to the encrypted data VaultMountPoint, ///< Where the data should be visible from VaultIcon, ///< Icon for the fault VaultIsBusy, ///< Is the vault currently being opened or closed VaultIsOpened, ///< Is the vault open VaultStatus, ///< Status of the vault, @see VaultInfo::Status VaultActivities, ///< To which activities does this vault belong to VaultMessage ///< Message to be shown forthe vault }; public Q_SLOTS: // Refresh the model. This should be done automatically, no need to call // this function void refresh(); // Open (unlock) a vault void open(const QString &device); // Close the vault void close(const QString &device); // Open the vault if it is closed, or close it otherwise void toggle(const QString &device); // Opens the new-vault wizard void requestNewVault(); + // Kills all applications using the vault and closes it + void forceClose(const QString &device); + + // Opens the configuration dialogue for the vault + void configure(const QString &device); + + // Open in file manager + void openInFileManager(const QString &device); + bool isBusy() const; bool hasError() const; Q_SIGNALS: void isBusyChanged(bool isBusy); void hasErrorChanged(bool hasError); private: class Private; friend class Private; QScopedPointer d; }; class SortedVaultsModelProxy: public QSortFilterProxyModel { Q_OBJECT public: SortedVaultsModelProxy(QObject *parent); bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; public Q_SLOTS: QObject *source() const; private: VaultsModel *m_source; KActivities::Consumer *m_kamd; }; #endif // include guard