diff --git a/kcms/desktoptheme/kcm.cpp b/kcms/desktoptheme/kcm.cpp index 94c9fd09a..20da2f173 100644 --- a/kcms/desktoptheme/kcm.cpp +++ b/kcms/desktoptheme/kcm.cpp @@ -1,283 +1,296 @@ /* This file is part of the KDE Project Copyright (c) 2014 Marco Martin Copyright (c) 2014 Vishesh Handa Copyright (c) 2016 David Rosca 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 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("Configure Desktop 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[IsLocalRole] = QByteArrayLiteral("isLocal"); 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; Q_EMIT selectedPluginChanged(m_selectedPlugin); updateNeedsSave(); } void KCMDesktopTheme::getNewThemes() { KNS3::DownloadDialog *dialog = new KNS3::DownloadDialog(QStringLiteral("plasma-themes.knsrc")); dialog->open(); connect(dialog, &QDialog::accepted, this, [this, dialog]() { if (!dialog->changedEntries().isEmpty()) { load(); delete dialog; } }); } void KCMDesktopTheme::installThemeFromFile(const QUrl &file) { qCDebug(KCM_DESKTOP_THEME) << "Installing ... " << file; const QString program = QStringLiteral("kpackagetool5"); const QStringList arguments = { QStringLiteral("--type"), QStringLiteral("Plasma/Theme"), QStringLiteral("--install"), file.toLocalFile()}; 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) { Q_UNUSED(exitStatus); if (exitCode == 0) { qCDebug(KCM_DESKTOP_THEME) << "Theme installed successfully :)"; load(); Q_EMIT showInfoMessage(i18n("Theme installed successfully.")); } else { qCWarning(KCM_DESKTOP_THEME) << "Theme installation failed." << exitCode; Q_EMIT showInfoMessage(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 showInfoMessage(i18n("Theme installation failed.")); }); 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) { 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); } } Q_INVOKABLE 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(); // 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('/', -1); const QString themeRoot = theme.left(themeSepIndex); int themeNameSepIndex = themeRoot.lastIndexOf('/', -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(isLocal, IsLocalRole); 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) { return; } m_defaultTheme->setThemeName(m_selectedPlugin); removeThemes(); updateNeedsSave(); } void KCMDesktopTheme::defaults() { setSelectedPlugin(QStringLiteral("default")); } +bool KCMDesktopTheme::canEditThemes() const +{ + return m_haveThemeExplorerInstalled; +} + +void KCMDesktopTheme::editTheme(const QString &theme) +{ + QProcess::startDetached(QStringLiteral("plasmathemeexplorer -t ") % theme); +} + void KCMDesktopTheme::updateNeedsSave() { setNeedsSave(!m_pendingRemoval.isEmpty() || m_selectedPlugin != m_defaultTheme->themeName()); } void KCMDesktopTheme::removeThemes() { 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(" ")); 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 showInfoMessage(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 showInfoMessage(i18n("Theme removal failed.")); process->deleteLater(); }); process->start(program, arguments); } } #include "kcm.moc" diff --git a/kcms/desktoptheme/kcm.h b/kcms/desktoptheme/kcm.h index a14f4cf8f..2509e9fbc 100644 --- a/kcms/desktoptheme/kcm.h +++ b/kcms/desktoptheme/kcm.h @@ -1,85 +1,90 @@ /* Copyright (c) 2014 Marco Martin Copyright (c) 2014 Vishesh Handa Copyright (c) 2016 David Rosca 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 namespace Plasma { class Svg; class Theme; } 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(bool canEditThemes READ canEditThemes CONSTANT) public: enum Roles { PluginNameRole = Qt::UserRole + 1, ThemeNameRole, IsLocalRole }; Q_ENUM(Roles) KCMDesktopTheme(QObject *parent, const QVariantList &args); ~KCMDesktopTheme(); QStandardItemModel *desktopThemeModel() const; QString selectedPlugin() const; void setSelectedPlugin(const QString &plugin); + bool canEditThemes() const; Q_INVOKABLE void getNewThemes(); 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 int indexOf(const QString &themeName) const; + Q_INVOKABLE void editTheme(const QString &themeName); + Q_SIGNALS: void selectedPluginChanged(const QString &plugin); void showInfoMessage(const QString &infoMessage); public Q_SLOTS: void load() Q_DECL_OVERRIDE; void save() Q_DECL_OVERRIDE; void defaults() Q_DECL_OVERRIDE; private: void removeThemes(); void updateNeedsSave(); QStandardItemModel *m_model; QString m_selectedPlugin; QStringList m_pendingRemoval; Plasma::Theme *m_defaultTheme; QHash m_themes; + bool m_haveThemeExplorerInstalled; }; 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 e58a15c07..93fdf0bd4 100644 --- a/kcms/desktoptheme/package/contents/ui/main.qml +++ b/kcms/desktoptheme/package/contents/ui/main.qml @@ -1,220 +1,241 @@ /* Copyright (c) 2014 Marco Martin Copyright (c) 2016 David Rosca 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.Private 1.0 import QtQuick.Controls 1.0 as QtControls import org.kde.kcm 1.0 import org.kde.kirigami 2.0 // for units +import org.kde.plasma.components 2.0 as PlasmaComponents //the round toolbutton Item { implicitWidth: Units.gridUnit * 20 implicitHeight: Units.gridUnit * 20 ConfigModule.quickHelp: i18n("This module lets you configure the desktop theme.") SystemPalette { id: syspal } ColumnLayout { anchors.fill: parent QtControls.ScrollView { Layout.fillWidth: true Layout.fillHeight: true verticalScrollBarPolicy: Qt.ScrollBarAlwaysOn GridView { id: grid model: kcm.desktopThemeModel cellWidth: Math.floor(grid.width / Math.max(Math.floor(grid.width / (Units.gridUnit * 12)), 3)) cellHeight: cellWidth / 1.6 onCountChanged: { grid.currentIndex = kcm.indexOf(kcm.selectedPlugin); grid.positionViewAtIndex(grid.currentIndex, GridView.Visible) } delegate: Item { property bool isLocal : model.isLocal property string pluginName : model.pluginName width: grid.cellWidth height: grid.cellHeight Rectangle { anchors { fill: parent margins: Units.smallSpacing } Connections { target: kcm onSelectedPluginChanged: { if (kcm.selectedPlugin == model.pluginName) { makeCurrentTimer.pendingIndex = index } } } Component.onCompleted: { if (kcm.selectedPlugin == model.pluginName) { makeCurrentTimer.pendingIndex = index } } - Item { + MouseArea { anchors { fill: parent margins: Units.smallSpacing * 2 } + hoverEnabled: true + onClicked: { + grid.currentIndex = index + kcm.selectedPlugin = model.pluginName + } + + Timer { + interval: 1000 + running: parent.containsMouse && !parent.pressedButtons + onTriggered: { + Tooltip.showText(parent, Qt.point(parent.mouseX, parent.mouseY), model.themeName); + } + } + ThemePreview { id: preview anchors { top: parent.top left: parent.left right: parent.right bottom: label.top } themeName: model.pluginName } + + PlasmaComponents.ToolButton { + anchors { + bottom: preview.bottom + right: preview.right + margins: units.smallSpacing + } + iconSource: "document-edit" + tooltip: i18("Edit theme") + flat: false + onClicked: kcm.editTheme(model.pluginName) + visible: kcm.canEditThemes + opacity: parent.containsMouse ? 1 : 0 + Behavior on opacity { + PropertyAnimation { + duration: units.longDuration + easing.type: Easing.OutQuad + } + } + } + QtControls.Label { id: label anchors { bottom: parent.bottom horizontalCenter: parent.horizontalCenter leftMargin: Units.smallSpacing * 2 rightMargin: Units.smallSpacing * 2 } height: paintedHeight width: parent.width color: "black" text: model.themeName elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter } } Rectangle { opacity: grid.currentIndex == index ? 1.0 : 0 anchors.fill: parent border.width: Units.smallSpacing * 2 border.color: syspal.highlight color: "transparent" Behavior on opacity { PropertyAnimation { duration: Units.longDuration easing.type: Easing.OutQuad } } } - MouseArea { - anchors.fill: parent - hoverEnabled: true - onClicked: { - grid.currentIndex = index - kcm.selectedPlugin = model.pluginName - } - Timer { - interval: 1000 - running: parent.containsMouse && !parent.pressedButtons - onTriggered: { - Tooltip.showText(parent, Qt.point(parent.mouseX, parent.mouseY), model.themeName); - } - } - } } } Timer { id: makeCurrentTimer interval: 100 repeat: false property int pendingIndex onPendingIndexChanged: makeCurrentTimer.restart() onTriggered: { grid.currentIndex = pendingIndex } } } } RowLayout { QtControls.Button { text: i18n("Get New Themes...") iconName: "get-hot-new-stuff" onClicked: kcm.getNewThemes() } QtControls.Button { text: i18n("Install from File...") iconName: "document-import" onClicked: fileDialogLoader.active = true; } QtControls.Button { text: i18n("Remove Theme") iconName: "edit-delete" enabled: grid.currentItem && grid.currentItem.isLocal onClicked: { kcm.removeTheme(grid.currentItem.pluginName); kcm.selectedPlugin = grid.currentItem.pluginName } } Item { Layout.fillWidth: true } QtControls.Label { id: infoLabel } } } Connections { target: kcm onShowInfoMessage: { infoLabel.text = infoMessage; hideInfoMessageTimer.restart(); } } Timer { id: hideInfoMessageTimer interval: 20 * 1000 onTriggered: { infoLabel.text = "" } } Loader { id: fileDialogLoader active: false sourceComponent: FileDialog { visible: true title: i18n("Open Theme") folder: shortcuts.home nameFilters: [ i18n("Theme Files (*.zip *.tar.gz *.tar.bz2)") ] onAccepted: { kcm.installThemeFromFile(fileUrls[0]) fileDialogLoader.active = false } onRejected: { fileDialogLoader.active = false } } } }