diff --git a/kcms/desktoptheme/CMakeLists.txt b/kcms/desktoptheme/CMakeLists.txt index 9990ad900..804283a74 100644 --- a/kcms/desktoptheme/CMakeLists.txt +++ b/kcms/desktoptheme/CMakeLists.txt @@ -1,27 +1,29 @@ # KI18N Translation Domain for this library add_definitions(-DTRANSLATION_DOMAIN=\"kcm_desktoptheme\") set(kcm_desktoptheme_SRCS kcm.cpp ) add_library(kcm_desktoptheme MODULE ${kcm_desktoptheme_SRCS}) target_link_libraries(kcm_desktoptheme KF5::CoreAddons KF5::KCMUtils + KF5::KIOCore + KF5::KIOWidgets KF5::I18n KF5::Plasma KF5::Declarative KF5::QuickAddons KF5::NewStuff ) kcoreaddons_desktop_to_json(kcm_desktoptheme "kcm_desktoptheme.desktop" SERVICE_TYPES kcmodule.desktop) #this desktop file is installed only for retrocompatibility with sycoca install(FILES kcm_desktoptheme.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) install(FILES plasma-themes.knsrc DESTINATION ${KDE_INSTALL_CONFDIR}) install(TARGETS kcm_desktoptheme DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms) kpackage_install_package(package kcm_desktoptheme kcms) diff --git a/kcms/desktoptheme/kcm.cpp b/kcms/desktoptheme/kcm.cpp index b4a5379de..800b5c5b9 100644 --- a/kcms/desktoptheme/kcm.cpp +++ b/kcms/desktoptheme/kcm.cpp @@ -1,323 +1,367 @@ /* This file is part of the KDE Project 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 License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kcm.h" #include #include #include #include #include +#include +#include + #include #include #include #include #include #include #include #include +#include #include Q_LOGGING_CATEGORY(KCM_DESKTOP_THEME, "kcm_desktoptheme") K_PLUGIN_FACTORY_WITH_JSON(KCMDesktopThemeFactory, "kcm_desktoptheme.json", registerPlugin();) KCMDesktopTheme::KCMDesktopTheme(QObject *parent, const QVariantList &args) : KQuickAddons::ConfigModule(parent, args) , m_defaultTheme(new Plasma::Theme(this)) , m_haveThemeExplorerInstalled(false) { //This flag seems to be needed in order for QQuickWidget to work //see https://bugreports.qt-project.org/browse/QTBUG-40765 //also, it seems to work only if set in the kcm, not in the systemsettings' main qApp->setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); qmlRegisterType(); KAboutData* about = new KAboutData(QStringLiteral("kcm_desktoptheme"), i18n("Choose the Plasma theme"), QStringLiteral("0.1"), QString(), KAboutLicense::LGPL); about->addAuthor(i18n("David Rosca"), QString(), QStringLiteral("nowrep@gmail.com")); setAboutData(about); setButtons(Apply | Default | Help); m_model = new QStandardItemModel(this); QHash roles = m_model->roleNames(); roles[PluginNameRole] = QByteArrayLiteral("pluginName"); 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(); } KCMDesktopTheme::~KCMDesktopTheme() { delete m_defaultTheme; } QStandardItemModel *KCMDesktopTheme::desktopThemeModel() const { return m_model; } QString KCMDesktopTheme::selectedPlugin() const { return m_selectedPlugin; } void KCMDesktopTheme::setSelectedPlugin(const QString &plugin) { if (m_selectedPlugin == plugin) { return; } m_selectedPlugin = plugin; emit selectedPluginChanged(m_selectedPlugin); emit selectedPluginIndexChanged(); updateNeedsSave(); } int KCMDesktopTheme::selectedPluginIndex() const { const auto results = m_model->match(m_model->index(0, 0), PluginNameRole, m_selectedPlugin); if (results.count() == 1) { return results.first().row(); } return -1; } +bool KCMDesktopTheme::downloadingFile() const +{ + return m_tempCopyJob; +} + 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) +void KCMDesktopTheme::installThemeFromFile(const QUrl &url) +{ + if (url.isLocalFile()) { + installTheme(url.toLocalFile()); + return; + } + + if (m_tempCopyJob) { + return; + } + + m_tempInstallFile.reset(new QTemporaryFile()); + if (!m_tempInstallFile->open()) { + emit showErrorMessage(i18n("Unable to create a temporary file.")); + m_tempInstallFile.reset(); + return; + } + + m_tempCopyJob = KIO::file_copy(url, QUrl::fromLocalFile(m_tempInstallFile->fileName()), + -1, KIO::Overwrite); + m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true); + emit downloadingFileChanged(); + + connect(m_tempCopyJob, &KIO::FileCopyJob::result, this, [this, url](KJob *job) { + if (job->error() != KJob::NoError) { + emit showErrorMessage(i18n("Unable to download the theme: %1", job->errorText())); + return; + } + + installTheme(m_tempInstallFile->fileName()); + m_tempInstallFile.reset(); + }); + connect(m_tempCopyJob, &QObject::destroyed, this, &KCMDesktopTheme::downloadingFileChanged); +} + +void KCMDesktopTheme::installTheme(const QString &path) { - qCDebug(KCM_DESKTOP_THEME) << "Installing ... " << file; + qCDebug(KCM_DESKTOP_THEME) << "Installing ... " << path; const QString program = QStringLiteral("kpackagetool5"); - const QStringList arguments = { QStringLiteral("--type"), QStringLiteral("Plasma/Theme"), QStringLiteral("--install"), file.toLocalFile()}; + const QStringList arguments = { QStringLiteral("--type"), QStringLiteral("Plasma/Theme"), QStringLiteral("--install"), path}; qCDebug(KCM_DESKTOP_THEME) << program << arguments.join(QStringLiteral(" ")); QProcess *myProcess = new QProcess(this); connect(myProcess, static_cast(&QProcess::finished), this, [this, myProcess](int exitCode, QProcess::ExitStatus exitStatus) { Q_UNUSED(exitStatus); if (exitCode == 0) { emit showSuccessMessage(i18n("Theme installed successfully.")); load(); } else { Q_EMIT showErrorMessage(i18n("Theme installation failed.")); } }); connect(myProcess, static_cast(&QProcess::error), this, [this](QProcess::ProcessError e) { qCWarning(KCM_DESKTOP_THEME) << "Theme installation failed: " << e; Q_EMIT showErrorMessage(i18n("Theme installation failed.")); }); myProcess->start(program, arguments); } void KCMDesktopTheme::applyPlasmaTheme(QQuickItem *item, const QString &themeName) { if (!item) { return; } Plasma::Theme *theme = m_themes[themeName]; if (!theme) { theme = new Plasma::Theme(themeName, this); m_themes[themeName] = theme; } Q_FOREACH (Plasma::Svg *svg, item->findChildren()) { svg->setTheme(theme); svg->setUsingRenderingCache(false); } } void KCMDesktopTheme::load() { m_pendingRemoval.clear(); // Get all desktop themes QStringList themes; const QStringList &packs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("plasma/desktoptheme"), QStandardPaths::LocateDirectory); Q_FOREACH (const QString &ppath, packs) { const QDir cd(ppath); const QStringList &entries = cd.entryList(QDir::Dirs | QDir::Hidden | QDir::NoDotAndDotDot); Q_FOREACH (const QString &pack, entries) { const QString _metadata = ppath + QLatin1Char('/') + pack + QStringLiteral("/metadata.desktop"); if (QFile::exists(_metadata)) { themes << _metadata; } } } m_model->clear(); Q_FOREACH (const QString &theme, themes) { int themeSepIndex = theme.lastIndexOf(QLatin1Char('/'), -1); const QString themeRoot = theme.left(themeSepIndex); int themeNameSepIndex = themeRoot.lastIndexOf(QLatin1Char('/'), -1); const QString packageName = themeRoot.right(themeRoot.length() - themeNameSepIndex - 1); KDesktopFile df(theme); if (df.noDisplay()) { continue; } QString name = df.readName(); if (name.isEmpty()) { name = packageName; } const bool isLocal = QFileInfo(theme).isWritable(); if (m_model->findItems(packageName).isEmpty()) { QStandardItem *item = new QStandardItem; item->setText(packageName); item->setData(packageName, PluginNameRole); item->setData(name, ThemeNameRole); item->setData(df.readComment(), DescriptionRole); item->setData(isLocal, IsLocalRole); item->setData(false, PendingDeletionRole); m_model->appendRow(item); } } KConfigGroup cg(KSharedConfig::openConfig(QStringLiteral("plasmarc")), "Theme"); setSelectedPlugin(cg.readEntry("name", m_defaultTheme->themeName())); updateNeedsSave(); } void KCMDesktopTheme::save() { if (m_defaultTheme->themeName() != m_selectedPlugin) { m_defaultTheme->setThemeName(m_selectedPlugin); } 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 { return m_haveThemeExplorerInstalled; } void KCMDesktopTheme::editTheme(const QString &theme) { QProcess::startDetached(QStringLiteral("plasmathemeexplorer -t ") % theme); } void KCMDesktopTheme::updateNeedsSave() { setNeedsSave(!m_model->match(m_model->index(0, 0), PendingDeletionRole, true).isEmpty() || m_selectedPlugin != m_defaultTheme->themeName()); } void KCMDesktopTheme::processPendingDeletions() { const QString program = QStringLiteral("plasmapkg2"); 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, 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 } } #include "kcm.moc" diff --git a/kcms/desktoptheme/kcm.h b/kcms/desktoptheme/kcm.h index 814f71877..5621123cd 100644 --- a/kcms/desktoptheme/kcm.h +++ b/kcms/desktoptheme/kcm.h @@ -1,103 +1,120 @@ /* 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 License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KCM_DESKTOPTHEME_H #define _KCM_DESKTOPTHEME_H #include #include +class QTemporaryFile; + namespace Plasma { class Svg; class Theme; } +namespace KIO +{ +class FileCopyJob; +} + 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 downloadingFile READ downloadingFile NOTIFY downloadingFileChanged) Q_PROPERTY(bool canEditThemes READ canEditThemes CONSTANT) public: enum Roles { PluginNameRole = Qt::UserRole + 1, ThemeNameRole, DescriptionRole, IsLocalRole, PendingDeletionRole }; Q_ENUM(Roles) KCMDesktopTheme(QObject *parent, const QVariantList &args); ~KCMDesktopTheme() override; QStandardItemModel *desktopThemeModel() const; QString selectedPlugin() const; void setSelectedPlugin(const QString &plugin); int selectedPluginIndex() const; + bool downloadingFile() const; + bool canEditThemes() const; Q_INVOKABLE void getNewStuff(QQuickItem *ctx); - Q_INVOKABLE void installThemeFromFile(const QUrl &file); + Q_INVOKABLE void installThemeFromFile(const QUrl &url); Q_INVOKABLE void setPendingDeletion(int index, bool pending); 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 downloadingFileChanged(); + void showSuccessMessage(const QString &message); void showErrorMessage(const QString &message); public Q_SLOTS: void load() override; void save() override; void defaults() override; private: void updateNeedsSave(); void processPendingDeletions(); + void installTheme(const QString &path); + QStandardItemModel *m_model; QString m_selectedPlugin; QStringList m_pendingRemoval; Plasma::Theme *m_defaultTheme; QHash m_themes; bool m_haveThemeExplorerInstalled; QPointer m_newStuffDialog; + + QScopedPointer m_tempInstallFile; + QPointer m_tempCopyJob; }; Q_DECLARE_LOGGING_CATEGORY(KCM_DESKTOP_THEME) #endif // _KCM_DESKTOPTHEME_H diff --git a/kcms/desktoptheme/package/contents/ui/main.qml b/kcms/desktoptheme/package/contents/ui/main.qml index f3058f4a2..ebe009105 100644 --- a/kcms/desktoptheme/package/contents/ui/main.qml +++ b/kcms/desktoptheme/package/contents/ui/main.qml @@ -1,164 +1,166 @@ /* Copyright (c) 2014 Marco Martin 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 License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.1 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.0 import QtQuick.Controls 2.3 as QtControls import org.kde.kirigami 2.4 as Kirigami import org.kde.kconfig 1.0 // for KAuthorized import org.kde.kcm 1.1 as KCM KCM.GridViewKCM { KCM.ConfigModule.quickHelp: i18n("This module lets you choose the Plasma theme.") view.model: kcm.desktopThemeModel view.currentIndex: kcm.selectedPluginIndex + enabled: !kcm.downloadingFile + DropArea { anchors.fill: parent onEntered: { if (!drag.hasUrls) { drag.accepted = false; } } onDropped: kcm.installThemeFromFile(drop.urls[0]) } view.remove: Transition { ParallelAnimation { NumberAnimation { property: "scale"; to: 0.5; duration: Kirigami.Units.longDuration } NumberAnimation { property: "opacity"; to: 0.0; duration: Kirigami.Units.longDuration } } } view.removeDisplaced: Transition { SequentialAnimation { // wait for the "remove" animation to finish PauseAnimation { duration: Kirigami.Units.longDuration } NumberAnimation { properties: "x,y"; duration: Kirigami.Units.longDuration } } } view.delegate: KCM.GridDelegate { id: delegate 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 anchors.fill: parent themeName: model.pluginName } actions: [ 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 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); } ] onClicked: { kcm.selectedPlugin = model.pluginName; view.forceActiveFocus(); } } footer: ColumnLayout { Kirigami.InlineMessage { id: infoLabel Layout.fillWidth: true showCloseButton: true Connections { target: kcm onShowSuccessMessage: { infoLabel.type = Kirigami.MessageType.Positive; infoLabel.text = message; infoLabel.visible = true; } onShowErrorMessage: { infoLabel.type = Kirigami.MessageType.Error; infoLabel.text = message; infoLabel.visible = true; } } } RowLayout { Layout.alignment: Qt.AlignRight QtControls.Button { text: i18n("Install from File...") icon.name: "document-import" onClicked: fileDialogLoader.active = true; } QtControls.Button { text: i18n("Get New Plasma Themes...") icon.name: "get-hot-new-stuff" onClicked: kcm.getNewStuff(this) visible: KAuthorized.authorize("ghns") } } } Loader { id: fileDialogLoader active: false sourceComponent: FileDialog { title: i18n("Open Theme") folder: shortcuts.home nameFilters: [ i18n("Theme Files (*.zip *.tar.gz *.tar.bz2)") ] Component.onCompleted: open() onAccepted: { kcm.installThemeFromFile(fileUrls[0]) fileDialogLoader.active = false } onRejected: { fileDialogLoader.active = false } } } }