diff --git a/kcms/desktoptheme/kcm.h b/kcms/desktoptheme/kcm.h --- a/kcms/desktoptheme/kcm.h +++ b/kcms/desktoptheme/kcm.h @@ -2,6 +2,7 @@ Copyright (c) 2014 Marco Martin Copyright (c) 2014 Vishesh Handa Copyright (c) 2016 David Rosca + Copyright (c) 2018 Kai Uwe Broulik This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public @@ -23,26 +24,31 @@ #include +#include + namespace Plasma { class Svg; class Theme; } +class QQuickItem; class QStandardItemModel; class KCMDesktopTheme : public KQuickAddons::ConfigModule { Q_OBJECT Q_PROPERTY(QStandardItemModel *desktopThemeModel READ desktopThemeModel CONSTANT) Q_PROPERTY(QString selectedPlugin READ selectedPlugin WRITE setSelectedPlugin NOTIFY selectedPluginChanged) + Q_PROPERTY(int selectedPluginIndex READ selectedPluginIndex NOTIFY selectedPluginIndexChanged) Q_PROPERTY(bool canEditThemes READ canEditThemes CONSTANT) public: enum Roles { PluginNameRole = Qt::UserRole + 1, ThemeNameRole, DescriptionRole, - IsLocalRole + IsLocalRole, + PendingDeletionRole }; Q_ENUM(Roles) @@ -53,20 +59,22 @@ QString selectedPlugin() const; void setSelectedPlugin(const QString &plugin); + int selectedPluginIndex() const; + bool canEditThemes() const; - Q_INVOKABLE void getNewThemes(); + Q_INVOKABLE void getNewStuff(QQuickItem *ctx); Q_INVOKABLE void installThemeFromFile(const QUrl &file); - Q_INVOKABLE void removeTheme(const QString &name); - Q_INVOKABLE void applyPlasmaTheme(QQuickItem *item, const QString &themeName); + Q_INVOKABLE void setPendingDeletion(int index, bool pending); - Q_INVOKABLE int indexOf(const QString &themeName) const; + Q_INVOKABLE void applyPlasmaTheme(QQuickItem *item, const QString &themeName); Q_INVOKABLE void editTheme(const QString &themeName); Q_SIGNALS: void selectedPluginChanged(const QString &plugin); + void selectedPluginIndexChanged(); void showSuccessMessage(const QString &message); void showErrorMessage(const QString &message); @@ -76,15 +84,18 @@ void defaults() override; private: - void removeThemes(); void updateNeedsSave(); + void processPendingDeletions(); + QStandardItemModel *m_model; QString m_selectedPlugin; QStringList m_pendingRemoval; Plasma::Theme *m_defaultTheme; QHash m_themes; bool m_haveThemeExplorerInstalled; + + QPointer m_newStuffDialog; }; Q_DECLARE_LOGGING_CATEGORY(KCM_DESKTOP_THEME) diff --git a/kcms/desktoptheme/kcm.cpp b/kcms/desktoptheme/kcm.cpp --- a/kcms/desktoptheme/kcm.cpp +++ b/kcms/desktoptheme/kcm.cpp @@ -2,6 +2,7 @@ Copyright (c) 2014 Marco Martin Copyright (c) 2014 Vishesh Handa Copyright (c) 2016 David Rosca + Copyright (c) 2018 Kai Uwe Broulik This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public @@ -32,6 +33,7 @@ #include #include #include +#include #include #include @@ -64,6 +66,7 @@ roles[ThemeNameRole] = QByteArrayLiteral("themeName"); roles[DescriptionRole] = QByteArrayLiteral("description"); roles[IsLocalRole] = QByteArrayLiteral("isLocal"); + roles[PendingDeletionRole] = QByteArrayLiteral("pendingDeletion"); m_model->setItemRoleNames(roles); m_haveThemeExplorerInstalled = !QStandardPaths::findExecutable(QStringLiteral("plasmathemeexplorer")).isEmpty(); @@ -90,21 +93,51 @@ return; } m_selectedPlugin = plugin; - Q_EMIT selectedPluginChanged(m_selectedPlugin); + emit selectedPluginChanged(m_selectedPlugin); + emit selectedPluginIndexChanged(); updateNeedsSave(); } -void KCMDesktopTheme::getNewThemes() +int KCMDesktopTheme::selectedPluginIndex() const { - KNS3::DownloadDialog *dialog = new KNS3::DownloadDialog(QStringLiteral("plasma-themes.knsrc")); - dialog->open(); + const auto results = m_model->match(m_model->index(0, 0), PluginNameRole, m_selectedPlugin); + if (results.count() == 1) { + return results.first().row(); + } - connect(dialog, &QDialog::accepted, this, [this, dialog]() { - if (!dialog->changedEntries().isEmpty()) { - load(); - delete dialog; - } - }); + return -1; +} + +void KCMDesktopTheme::setPendingDeletion(int index, bool pending) +{ + QModelIndex idx = m_model->index(index, 0); + + m_model->setData(idx, pending, PendingDeletionRole); + + if (pending && selectedPluginIndex() == index) { + // move to the next non-pending theme + const auto nonPending = m_model->match(idx, PendingDeletionRole, false); + setSelectedPlugin(nonPending.first().data(PluginNameRole).toString()); + } + + updateNeedsSave(); +} + +void KCMDesktopTheme::getNewStuff(QQuickItem *ctx) +{ + if (!m_newStuffDialog) { + m_newStuffDialog = new KNS3::DownloadDialog(QStringLiteral("plasma-themes.knsrc")); + m_newStuffDialog.data()->setWindowTitle(i18n("Download New Desktop Themes")); + m_newStuffDialog->setWindowModality(Qt::WindowModal); + m_newStuffDialog->winId(); // so it creates the windowHandle(); + connect(m_newStuffDialog.data(), &KNS3::DownloadDialog::accepted, this, &KCMDesktopTheme::load); + } + + if (ctx && ctx->window()) { + m_newStuffDialog->windowHandle()->setTransientParent(ctx->window()); + } + + m_newStuffDialog.data()->show(); } void KCMDesktopTheme::installThemeFromFile(const QUrl &file) @@ -117,15 +150,13 @@ qCDebug(KCM_DESKTOP_THEME) << program << arguments.join(QStringLiteral(" ")); QProcess *myProcess = new QProcess(this); connect(myProcess, static_cast(&QProcess::finished), - this, [this](int exitCode, QProcess::ExitStatus exitStatus) { + this, [this, myProcess](int exitCode, QProcess::ExitStatus exitStatus) { Q_UNUSED(exitStatus); if (exitCode == 0) { - qCDebug(KCM_DESKTOP_THEME) << "Theme installed successfully :)"; load(); - Q_EMIT showSuccessMessage(i18n("Theme installed successfully.")); } else { - qCWarning(KCM_DESKTOP_THEME) << "Theme installation failed." << exitCode; Q_EMIT showErrorMessage(i18n("Theme installation failed.")); + } }); @@ -138,17 +169,6 @@ myProcess->start(program, arguments); } -void KCMDesktopTheme::removeTheme(const QString &name) -{ - Q_ASSERT(!m_pendingRemoval.contains(name)); - Q_ASSERT(!m_model->findItems(name).isEmpty()); - - m_pendingRemoval.append(name); - m_model->removeRow(m_model->findItems(name).at(0)->row()); - - updateNeedsSave(); -} - void KCMDesktopTheme::applyPlasmaTheme(QQuickItem *item, const QString &themeName) { if (!item) { @@ -167,16 +187,6 @@ } } -int KCMDesktopTheme::indexOf(const QString &themeName) const -{ - for (int i = 0; i < m_model->rowCount(); ++i) { - if (m_model->data(m_model->index(i, 0), PluginNameRole).toString() == themeName) { - return i; - } - } - return -1; -} - void KCMDesktopTheme::load() { m_pendingRemoval.clear(); @@ -222,6 +232,7 @@ item->setData(name, ThemeNameRole); item->setData(df.readComment(), DescriptionRole); item->setData(isLocal, IsLocalRole); + item->setData(false, PendingDeletionRole); m_model->appendRow(item); } } @@ -234,18 +245,23 @@ void KCMDesktopTheme::save() { - if (m_defaultTheme->themeName() == m_selectedPlugin) { - return; + if (m_defaultTheme->themeName() != m_selectedPlugin) { + m_defaultTheme->setThemeName(m_selectedPlugin); } - m_defaultTheme->setThemeName(m_selectedPlugin); - removeThemes(); + processPendingDeletions(); updateNeedsSave(); } void KCMDesktopTheme::defaults() { setSelectedPlugin(QStringLiteral("default")); + + // can this be done more elegantly? + const auto pendingDeletions = m_model->match(m_model->index(0, 0), PendingDeletionRole, true); + for (const QModelIndex &idx : pendingDeletions) { + m_model->setData(idx, false, PendingDeletionRole); + } } bool KCMDesktopTheme::canEditThemes() const @@ -260,38 +276,46 @@ void KCMDesktopTheme::updateNeedsSave() { - setNeedsSave(!m_pendingRemoval.isEmpty() || m_selectedPlugin != m_defaultTheme->themeName()); + setNeedsSave(!m_model->match(m_model->index(0, 0), PendingDeletionRole, true).isEmpty() + || m_selectedPlugin != m_defaultTheme->themeName()); } -void KCMDesktopTheme::removeThemes() +void KCMDesktopTheme::processPendingDeletions() { const QString program = QStringLiteral("plasmapkg2"); - Q_FOREACH (const QString &name, m_pendingRemoval) { - const QStringList arguments = {QStringLiteral("-t"), QStringLiteral("theme"), QStringLiteral("-r"), name}; - qCDebug(KCM_DESKTOP_THEME) << program << arguments.join(QStringLiteral(" ")); + const auto pendingDeletions = m_model->match(m_model->index(0, 0), PendingDeletionRole, true, -1 /*all*/); + QVector persistentPendingDeletions; + // turn into persistent model index so we can delete as we go + std::transform(pendingDeletions.begin(), pendingDeletions.end(), + std::back_inserter(persistentPendingDeletions), [](const QModelIndex &idx) { + return QPersistentModelIndex(idx); + }); + + for (const QPersistentModelIndex &idx : persistentPendingDeletions) { + const QString pluginName = idx.data(PluginNameRole).toString(); + const QString displayName = idx.data(Qt::DisplayRole).toString(); + + Q_ASSERT(pluginName != m_selectedPlugin); + + const QStringList arguments = {QStringLiteral("-t"), QStringLiteral("theme"), QStringLiteral("-r"), pluginName}; + QProcess *process = new QProcess(this); - connect(process, static_cast(&QProcess::finished), - this, [this, process](int exitCode, QProcess::ExitStatus exitStatus) { - Q_UNUSED(exitStatus); - if (exitCode == 0) { - qCDebug(KCM_DESKTOP_THEME) << "Theme removed successfully :)"; - load(); - } else { - qCWarning(KCM_DESKTOP_THEME) << "Theme removal failed." << exitCode; - Q_EMIT showErrorMessage(i18n("Theme removal failed.")); - } - process->deleteLater(); - }); - - connect(process, static_cast(&QProcess::error), - this, [this, process](QProcess::ProcessError e) { - qCWarning(KCM_DESKTOP_THEME) << "Theme removal failed: " << e; - Q_EMIT showErrorMessage(i18n("Theme removal failed.")); - process->deleteLater(); - }); + connect(process, static_cast(&QProcess::finished), this, + [this, process, idx, pluginName, displayName](int exitCode, QProcess::ExitStatus exitStatus) { + Q_UNUSED(exitStatus); + if (exitCode == 0) { + m_model->removeRow(idx.row()); + } else { + emit showErrorMessage(i18n("Removing theme failed: %1", + QString::fromLocal8Bit(process->readAllStandardOutput().trimmed()))); + m_model->setData(idx, false, PendingDeletionRole); + } + process->deleteLater(); + }); process->start(program, arguments); + process->waitForFinished(); // needed so it deletes fine when "OK" is clicked and the dialog destroyed } } diff --git a/kcms/desktoptheme/package/contents/ui/main.qml b/kcms/desktoptheme/package/contents/ui/main.qml --- a/kcms/desktoptheme/package/contents/ui/main.qml +++ b/kcms/desktoptheme/package/contents/ui/main.qml @@ -29,7 +29,17 @@ KCM.ConfigModule.quickHelp: i18n("This module lets you configure the desktop theme.") view.model: kcm.desktopThemeModel - view.currentIndex: kcm.indexOf(kcm.selectedPlugin) + view.currentIndex: kcm.selectedPluginIndex + + DropArea { + anchors.fill: parent + onEntered: { + if (!drag.hasUrls) { + drag.accepted = false; + } + } + onDropped: kcm.installThemeFromFile(drop.urls[0]) + } view.remove: Transition { ParallelAnimation { @@ -52,6 +62,11 @@ text: model.themeName toolTip: model.description || model.themeName + opacity: model.pendingDeletion ? 0.3 : 1 + Behavior on opacity { + NumberAnimation { duration: Kirigami.Units.longDuration } + } + thumbnailAvailable: true thumbnail: ThemePreview { id: preview @@ -63,14 +78,22 @@ Kirigami.Action { iconName: "document-edit" tooltip: i18n("Edit Theme") + enabled: !model.pendingDeletion visible: kcm.canEditThemes onTriggered: kcm.editTheme(model.pluginName) }, Kirigami.Action { iconName: "edit-delete" tooltip: i18n("Remove Theme") enabled: model.isLocal - onTriggered: kcm.removeTheme(model.pluginName) + visible: !model.pendingDeletion + onTriggered: kcm.setPendingDeletion(model.index, true); + }, + Kirigami.Action { + iconName: "edit-undo" + tooltip: i18n("Restore Theme") + visible: model.pendingDeletion + onTriggered: kcm.setPendingDeletion(model.index, false); } ] @@ -114,7 +137,7 @@ QtControls.Button { text: i18n("Get New Themes...") icon.name: "get-hot-new-stuff" - onClicked: kcm.getNewThemes() + onClicked: kcm.getNewStuff(this) } } }