diff --git a/kcms/style/CMakeLists.txt b/kcms/style/CMakeLists.txt index bc3398f2a..1a091945d 100644 --- a/kcms/style/CMakeLists.txt +++ b/kcms/style/CMakeLists.txt @@ -1,37 +1,48 @@ # KI18N Translation Domain for this library add_definitions(-DTRANSLATION_DOMAIN=\"kcm_style\") ########### next target ############### -set(kcm_style_PART_SRCS ../krdb/krdb.cpp styleconfdialog.cpp kcmstyle.cpp stylesmodel.cpp previewitem.cpp) +set(kcm_style_PART_SRCS + ../krdb/krdb.cpp + styleconfdialog.cpp + kcmstyle.cpp + stylesmodel.cpp + gtkthemesmodel.cpp + gtkpage.cpp + previewitem.cpp +) set(klauncher_xml ${KINIT_DBUS_INTERFACES_DIR}/kf5_org.kde.KLauncher.xml) qt5_add_dbus_interface(kcm_style_PART_SRCS ${klauncher_xml} klauncher_iface) ki18n_wrap_ui(kcm_style_PART_SRCS stylepreview.ui) kconfig_add_kcfg_files(kcm_style_PART_SRCS stylesettings.kcfgc GENERATE_MOC) add_library(kcm_style MODULE ${kcm_style_PART_SRCS}) target_link_libraries(kcm_style Qt5::X11Extras Qt5::DBus KF5::KCMUtils KF5::Completion KF5::I18n KF5::Notifications KF5::Plasma ${X11_LIBRARIES} KF5::KDELibs4Support KF5::GuiAddons KF5::QuickAddons KF5::WindowSystem + KF5::Archive + KF5::NewStuff ) kcoreaddons_desktop_to_json(kcm_style "kcm_style.desktop") install(FILES stylesettings.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) +install(FILES cgctheme.knsrc cgcgtk3.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) install(FILES kcm_style.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) install(TARGETS kcm_style DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms) kpackage_install_package(package kcm_style kcms) diff --git a/kcms/style/cgcgtk3.knsrc b/kcms/style/cgcgtk3.knsrc new file mode 100644 index 000000000..80290bfc6 --- /dev/null +++ b/kcms/style/cgcgtk3.knsrc @@ -0,0 +1,46 @@ +[KNewStuff3] +Name=GTK 3.x Themes +Name[ar]=سمات جتك الثّالثة +Name[ast]=Estilos pa GTK 3.x +Name[ca]=Temes GTK 3.x +Name[ca@valencia]=Temes GTK 3.x +Name[cs]=Motivy GTK 3.x +Name[da]=GTK 3.x-temaer +Name[de]=GTK 3.x-Designs +Name[el]=Θέματα GTK 3.x +Name[en_GB]=GTK 3.x Themes +Name[es]=Temas de GTK 3.x +Name[et]=GTK 3.x teemad +Name[eu]=GTK 3.x gaiak +Name[fi]=GTK 3.x -teemat +Name[fr]=Thèmes GTK 3.x +Name[gl]=Temas de GTK 3.x +Name[he]=ערכות נושא של GTK 3.x +Name[hu]=GTK 3.x témák +Name[id]=Tema GTK 3.x +Name[it]=Temi GTK 3.x +Name[ko]=GTK 3.x 테마 +Name[lt]=GTK 3.x apipavidalinimai +Name[nl]=GTK 3.x thema's +Name[nn]=GTK 3.x-tema +Name[pl]=Wygląd GTK 3.x +Name[pt]=Temas do GTK 3.x +Name[pt_BR]=Temas GTK 3.x +Name[ru]=Темы GTK 3.x +Name[sk]=Témy GTK 3.x +Name[sl]=Teme GTK 3.x +Name[sr]=ГТК 3.x теме +Name[sr@ijekavian]=ГТК 3.x теме +Name[sr@ijekavianlatin]=GTK 3.x teme +Name[sr@latin]=GTK 3.x teme +Name[sv]=GTK 3.x-teman +Name[tg]=Мавзӯъҳои GTK 3.x +Name[tr]=GTK 3.x Temaları +Name[uk]=Теми GTK 3.x +Name[x-test]=xxGTK 3.x Themesxx +Name[zh_CN]=GTK 3.x 主题 +Name[zh_TW]=GTK 3.x 主題 + +Categories=GTK 3.x Theme/Style +Uncompress=always +InstallPath=.themes diff --git a/kcms/style/cgctheme.knsrc b/kcms/style/cgctheme.knsrc new file mode 100644 index 000000000..664e8b8f7 --- /dev/null +++ b/kcms/style/cgctheme.knsrc @@ -0,0 +1,46 @@ +[KNewStuff3] +Name=GTK 2.x Themes +Name[ar]=سمات جتك الثّانية +Name[ast]=Estilos pa GTK 2.x +Name[ca]=Temes GTK 2.x +Name[ca@valencia]=Temes GTK 2.x +Name[cs]=Motivy GTK 2.x +Name[da]=GTK 2.x-temaer +Name[de]=GTK 2.x-Designs +Name[el]=Θέματα GTK 2.x +Name[en_GB]=GTK 2.x Themes +Name[es]=Temas de GTK 2.x +Name[et]=GTK 2.x teemad +Name[eu]=GTK 2.x gaiak +Name[fi]=GTK 2.x -teemat +Name[fr]=Thèmes GTK 2.x +Name[gl]=Temas de GTK 2.x +Name[he]=ערכות נושא של GTK 2.x +Name[hu]=GTK 2.x témák +Name[id]=Tema GTK 2.x +Name[it]=Temi GTK 2.x +Name[ko]=GTK 2.x 테마 +Name[lt]=GTK 2.x apipavidalinimai +Name[nl]=GTK 2.x thema's +Name[nn]=GTK 2.x-tema +Name[pl]=Wygląd GTK 2.x +Name[pt]=Temas do GTK 2.x +Name[pt_BR]=Temas GTK 2.x +Name[ru]=Темы GTK 2.x +Name[sk]=Témy GTK 2.x +Name[sl]=Teme GTK 2.x +Name[sr]=ГТК 2.x теме +Name[sr@ijekavian]=ГТК 2.x теме +Name[sr@ijekavianlatin]=GTK 2.x teme +Name[sr@latin]=GTK 2.x teme +Name[sv]=GTK 2.x-teman +Name[tg]=Мавзӯъҳои GTK 2.x +Name[tr]=GTK 2.x Temaları +Name[uk]=Теми GTK 2.x +Name[x-test]=xxGTK 2.x Themesxx +Name[zh_CN]=GTK 2.x 主题 +Name[zh_TW]=GTK 2.x 主題 + +Categories=GTK 2.x Theme/Style +Uncompress=always +InstallPath=.themes diff --git a/kcms/style/gtkpage.cpp b/kcms/style/gtkpage.cpp new file mode 100644 index 000000000..9ba7a8951 --- /dev/null +++ b/kcms/style/gtkpage.cpp @@ -0,0 +1,159 @@ +/* + * Copyright 2020 Mikhail Zolotukhin + * + * 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 +#include +#include +#include + +#include +#include +#include + +#include "gtkpage.h" +#include "gtkthemesmodel.h" + +GtkPage::GtkPage(QObject *parent) + : QObject(parent) + , m_gtk2ThemesModel(new GtkThemesModel(this)) + , m_gtk3ThemesModel(new GtkThemesModel(this)) + , gtkConfigInterface( + QStringLiteral("org.kde.GtkConfig"), + QStringLiteral("/GtkConfig"), + QStringLiteral("org.kde.GtkConfig") + ) +{ + connect(m_gtk2ThemesModel, &GtkThemesModel::themeRemoved, this, &GtkPage::onThemeRemoved); + connect(m_gtk3ThemesModel, &GtkThemesModel::themeRemoved, this, &GtkPage::onThemeRemoved); + + connect(m_gtk2ThemesModel, &GtkThemesModel::selectedThemeChanged, this, [this](){ + Q_EMIT gtkThemeSettingsChanged(); + }); + connect(m_gtk3ThemesModel, &GtkThemesModel::selectedThemeChanged, this, [this](){ + Q_EMIT gtkThemeSettingsChanged(); + }); +} + +GtkPage::~GtkPage() +{ + delete m_gtk2ThemesModel; + delete m_gtk3ThemesModel; +} + +QString GtkPage::gtk2ThemeFromConfig() +{ + QDBusReply dbusReply = gtkConfigInterface.call(QStringLiteral("gtk2Theme")); + return dbusReply.value(); +} + +QString GtkPage::gtk3ThemeFromConfig() +{ + QDBusReply dbusReply = gtkConfigInterface.call(QStringLiteral("gtk3Theme")); + return dbusReply.value(); +} + +void GtkPage::showGtk2Preview() +{ + gtkConfigInterface.call(QStringLiteral("showGtk2ThemePreview"), m_gtk2ThemesModel->selectedTheme()); +} + +void GtkPage::showGtk3Preview() +{ + gtkConfigInterface.call(QStringLiteral("showGtk3ThemePreview"), m_gtk3ThemesModel->selectedTheme()); +} + + +void GtkPage::installGtk2ThemeFromGHNS() +{ + KNS3::DownloadDialog downloadDialog(QStringLiteral("cgctheme.knsrc")); + downloadDialog.setWindowTitle(i18n("Download New GNOME/GTK2 Application Styles")); + downloadDialog.setWindowModality(Qt::WindowModal); + if (downloadDialog.exec()) { + load(); + } +} + +void GtkPage::installGtk3ThemeFromGHNS() +{ + KNS3::DownloadDialog downloadDialog(QStringLiteral("cgcgtk3.knsrc")); + downloadDialog.setWindowTitle(i18n("Download New GNOME/GTK3 Application Styles")); + downloadDialog.setWindowModality(Qt::WindowModal); + if (downloadDialog.exec()) { + load(); + } +} + +void GtkPage::onThemeRemoved() +{ + load(); + defaults(); + save(); +} + +void GtkPage::installGtkThemeFromFile(const QUrl &fileUrl) +{ + QString themesInstallDirectoryPath(QDir::homePath() + QStringLiteral("/.themes")); + QDir::home().mkpath(themesInstallDirectoryPath); + KTar themeArchive(fileUrl.path()); + themeArchive.open(QIODevice::ReadOnly); + + auto showError = [this, fileUrl]() { + Q_EMIT showErrorMessage(i18n("%1 is not a valid GTK Theme archive.", fileUrl.fileName())); + }; + + QString firstEntryName = themeArchive.directory()->entries().first(); + const KArchiveEntry *possibleThemeDirectory = themeArchive.directory()->entry(firstEntryName); + if (possibleThemeDirectory->isDirectory()) { + const KArchiveDirectory *themeDirectory = static_cast(possibleThemeDirectory); + QStringList archiveSubitems = themeDirectory->entries(); + + if (!archiveSubitems.contains(QStringLiteral("gtk-2.0")) && archiveSubitems.indexOf(QRegExp("gtk-3.*")) == -1) { + showError(); + return; + } + } else { + showError(); + return; + } + + themeArchive.directory()->copyTo(themesInstallDirectoryPath); + + load(); +} + +void GtkPage::save() +{ + gtkConfigInterface.call(QStringLiteral("setGtk2Theme"), m_gtk2ThemesModel->selectedTheme()); + gtkConfigInterface.call(QStringLiteral("setGtk3Theme"), m_gtk3ThemesModel->selectedTheme()); +} + +void GtkPage::defaults() +{ + Q_EMIT selectGtk2ThemeInCombobox(QStringLiteral("Breeze")); + Q_EMIT selectGtk3ThemeInCombobox(QStringLiteral("Breeze")); +} + +void GtkPage::load() +{ + m_gtk2ThemesModel->loadGtk2(); + m_gtk3ThemesModel->loadGtk3(); + Q_EMIT selectGtk2ThemeInCombobox(gtk2ThemeFromConfig()); + Q_EMIT selectGtk3ThemeInCombobox(gtk3ThemeFromConfig()); +} diff --git a/kcms/style/gtkpage.h b/kcms/style/gtkpage.h new file mode 100644 index 000000000..6d0689095 --- /dev/null +++ b/kcms/style/gtkpage.h @@ -0,0 +1,71 @@ +/* + * Copyright 2020 Mikhail Zolotukhin + * + * 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 . + */ + +#pragma once + +#include +#include + +#include "gtkthemesmodel.h" + +class GtkPage : public QObject +{ + Q_OBJECT + + Q_PROPERTY(GtkThemesModel *gtk2ThemesModel MEMBER m_gtk2ThemesModel NOTIFY gtk2ThemesModelChanged) + Q_PROPERTY(GtkThemesModel *gtk3ThemesModel MEMBER m_gtk3ThemesModel NOTIFY gtk3ThemesModelChanged) + +public: + GtkPage(QObject *parent = nullptr); + ~GtkPage() override; + + void load(); + void save(); + void defaults(); + +public Q_SLOTS: + QString gtk2ThemeFromConfig(); + QString gtk3ThemeFromConfig(); + + void showGtk2Preview(); + void showGtk3Preview(); + + void installGtkThemeFromFile(const QUrl &fileUrl); + void installGtk2ThemeFromGHNS(); + void installGtk3ThemeFromGHNS(); + + void onThemeRemoved(); + +Q_SIGNALS: + void gtk2ThemesModelChanged(GtkThemesModel *model); + void gtk3ThemesModelChanged(GtkThemesModel *model); + + void showErrorMessage(const QString &message); + void selectGtk2ThemeInCombobox(const QString &themeName); + void selectGtk3ThemeInCombobox(const QString &themeName); + + void gtkThemeSettingsChanged(); + +private: + GtkThemesModel *m_gtk2ThemesModel; + GtkThemesModel *m_gtk3ThemesModel; + + QDBusInterface gtkConfigInterface; +}; diff --git a/kcms/style/gtkthemesmodel.cpp b/kcms/style/gtkthemesmodel.cpp new file mode 100644 index 000000000..69b710ce0 --- /dev/null +++ b/kcms/style/gtkthemesmodel.cpp @@ -0,0 +1,177 @@ +/* + * Copyright 2020 Mikhail Zolotukhin + * + * 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 +#include +#include +#include + +#include + +#include "gtkthemesmodel.h" + +GtkThemesModel::GtkThemesModel(QObject* parent) + : QAbstractListModel(parent) + , m_selectedTheme(QStringLiteral("Breeze")) + , m_themesList() +{ + +} + +void GtkThemesModel::loadGtk2() +{ + QMap gtk2ThemesNames; + + for (const QString &possibleThemePath : possiblePathsToThemes()) { + // If the directory has a gtk-2.0 directory inside, it is the GTK2 theme for sure + QDir possibleThemeDirectory(possibleThemePath); + bool hasGtk2DirectoryInside = possibleThemeDirectory.exists(QStringLiteral("gtk-2.0")); + if (hasGtk2DirectoryInside) { + gtk2ThemesNames.insert(possibleThemeDirectory.dirName(), possibleThemeDirectory.path()); + } + } + + setThemesList(gtk2ThemesNames); +} + +void GtkThemesModel::loadGtk3() +{ + QMap gtk3ThemesNames; + + static const QStringList gtk3SubdirPattern(QStringLiteral("gtk-3.*")); + for (const QString &possibleThemePath : possiblePathsToThemes()) { + // If the directory contains any of gtk-3.X folders, it is the GTK3 theme for sure + QDir possibleThemeDirectory(possibleThemePath); + if (!possibleThemeDirectory.entryList(gtk3SubdirPattern, QDir::Dirs).isEmpty()) { + gtk3ThemesNames.insert(possibleThemeDirectory.dirName(), possibleThemeDirectory.path()); + } + } + + setThemesList(gtk3ThemesNames); +} + +QString GtkThemesModel::themePath(const QString &themeName) { + if (themeName.isEmpty()) { + return QString(); + } else { + return m_themesList.find(themeName).value(); + } +} + +QVariant GtkThemesModel::data(const QModelIndex& index, int role) const +{ + if (role == Qt::DisplayRole || role == Roles::ThemeNameRole) { + if (index.row() < 0 || index.row() > m_themesList.count()) { + return QVariant(); + } + + return m_themesList.keys().at(index.row()); + } else if (role == Roles::ThemePathRole) { + if (index.row() < 0 || index.row() > m_themesList.count()) { + return QVariant(); + } + + return m_themesList.values().at(index.row()); + } else { + return QVariant(); + } +} + +QHash GtkThemesModel::roleNames() const +{ + QHash roles = QAbstractListModel::roleNames(); + roles[Roles::ThemeNameRole] = "theme-name"; + roles[Roles::ThemePathRole] = "theme-path"; + + return roles; +} + +int GtkThemesModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_themesList.count(); +} + +void GtkThemesModel::setThemesList(const QMap& themes) +{ + beginResetModel(); + m_themesList = themes; + endResetModel(); +} + +QMap GtkThemesModel::themesList() { + return m_themesList; +} + +void GtkThemesModel::setSelectedTheme(const QString &themeName) +{ + m_selectedTheme = themeName; +} + +QString GtkThemesModel::selectedTheme() +{ + return m_selectedTheme; +} + +QStringList GtkThemesModel::possiblePathsToThemes() +{ + QStringList possibleThemesPaths; + + QStringList themesLocationsPaths = QStandardPaths::locateAll( + QStandardPaths::GenericDataLocation, + QStringLiteral("themes"), + QStandardPaths::LocateDirectory); + themesLocationsPaths << QDir::homePath() + QStringLiteral("/.themes"); + + for (const QString& themesLocationPath : themesLocationsPaths) { + QStringList possibleThemesDirectoriesNames = QDir(themesLocationPath).entryList(QDir::NoDotAndDotDot | QDir::AllDirs); + for (const QString &possibleThemeDirectoryName : possibleThemesDirectoriesNames) { + possibleThemesPaths += themesLocationPath + '/' + possibleThemeDirectoryName; + } + } + + return possibleThemesPaths; +} + +bool GtkThemesModel::selectedThemeRemovable() +{ + return themePath(m_selectedTheme).contains(QDir::homePath()); +} + +void GtkThemesModel::removeSelectedTheme() +{ + QString path = themePath(m_selectedTheme); + KIO::DeleteJob* deleteJob = KIO::del(QUrl::fromLocalFile(path), KIO::HideProgressInfo); + connect(deleteJob, &KJob::finished, this, [this](){ + Q_EMIT themeRemoved(); + }); +} + +int GtkThemesModel::findThemeIndex(const QString &themeName) +{ + return static_cast(std::distance(m_themesList.begin(), m_themesList.find(themeName))); +} + +void GtkThemesModel::setSelectedThemeDirty() +{ + Q_EMIT selectedThemeChanged(m_selectedTheme); +} diff --git a/kcms/style/gtkthemesmodel.h b/kcms/style/gtkthemesmodel.h new file mode 100644 index 000000000..43ff70f58 --- /dev/null +++ b/kcms/style/gtkthemesmodel.h @@ -0,0 +1,74 @@ +/* + * Copyright 2020 Mikhail Zolotukhin + * + * 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 . + */ + +#pragma once + +#include +#include + +class QStringList; +class QString; + +class GtkThemesModel : public QAbstractListModel { + Q_OBJECT + + Q_PROPERTY(QString selectedTheme READ selectedTheme WRITE setSelectedTheme NOTIFY selectedThemeChanged) + +public: + GtkThemesModel(QObject *parent = nullptr); + ~GtkThemesModel() override = default; + + enum Roles { + ThemeNameRole = Qt::UserRole + 1, + ThemePathRole + }; + + void loadGtk2(); + void loadGtk3(); + + void setThemesList(const QMap &themes); + QMap themesList(); + + void setSelectedTheme(const QString &themeName); + QString selectedTheme(); + Q_SIGNAL void selectedThemeChanged(const QString &themeName); + + Q_INVOKABLE bool selectedThemeRemovable(); + Q_INVOKABLE void removeSelectedTheme(); + Q_INVOKABLE int findThemeIndex(const QString &themeName); + Q_INVOKABLE void setSelectedThemeDirty(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Roles::ThemeNameRole) const override; + QHash roleNames() const override; + + void requestThemesListUpdate(); + +Q_SIGNALS: + void themeRemoved(); + +private: + QStringList possiblePathsToThemes(); + QString themePath(const QString &themeName); + + QString m_selectedTheme; + QMap m_themesList; +}; + diff --git a/kcms/style/kcmstyle.cpp b/kcms/style/kcmstyle.cpp index abd051c8d..e8f2b5a87 100644 --- a/kcms/style/kcmstyle.cpp +++ b/kcms/style/kcmstyle.cpp @@ -1,310 +1,342 @@ /* * KCMStyle * Copyright (C) 2002 Karol Szwed * Copyright (C) 2002 Daniel Molkentin * Copyright (C) 2007 Urs Wolfer * Copyright (C) 2009 by Davide Bettio * Copyright (C) 2019 Kai Uwe Broulik * Copyright (C) 2019 Cyril Rossi * * Portions Copyright (C) 2007 Paolo Capriotti * Portions Copyright (C) 2007 Ivan Cukic * Portions Copyright (C) 2008 by Petri Damsten * Portions Copyright (C) 2000 TrollTech AS. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License version 2 as published by the Free Software Foundation. * * 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; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kcmstyle.h" #include "styleconfdialog.h" #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include "../krdb/krdb.h" #include "stylesmodel.h" #include "previewitem.h" #include "stylesettings.h" +#include "gtkpage.h" K_PLUGIN_FACTORY_WITH_JSON(KCMStyleFactory, "kcm_style.json", registerPlugin();) extern "C" { Q_DECL_EXPORT void kcminit_style() { uint flags = KRdbExportQtSettings | KRdbExportGtkColors | KRdbExportQtColors | KRdbExportXftSettings | KRdbExportGtkTheme; KConfig _config( QStringLiteral("kcmdisplayrc"), KConfig::NoGlobals ); KConfigGroup config(&_config, "X11"); // This key is written by the "colors" module. bool exportKDEColors = config.readEntry("exportKDEColors", true); if (exportKDEColors) { flags |= KRdbExportColors; } runRdb( flags ); } } KCMStyle::KCMStyle(QObject *parent, const QVariantList &args) : KQuickAddons::ManagedConfigModule(parent, args) , m_settings(new StyleSettings(this)) , m_model(new StylesModel(this)) + , m_gtkPage() { qmlRegisterUncreatableType("org.kde.private.kcms.style", 1, 0, "KCM", QStringLiteral("Cannot create instances of KCM")); qmlRegisterType(); qmlRegisterType(); qmlRegisterType("org.kde.private.kcms.style", 1, 0, "PreviewItem"); KAboutData *about = new KAboutData( QStringLiteral("kcm_style"), i18n("Application Style"), QStringLiteral("2.0"), QString(), KAboutLicense::GPL, i18n("(c) 2002 Karol Szwed, Daniel Molkentin, (c) 2019 Kai Uwe Broulik")); about->addAuthor(i18n("Karol Szwed"), QString(), QStringLiteral("gallium@kde.org")); about->addAuthor(i18n("Daniel Molkentin"), QString(), QStringLiteral("molkentin@kde.org")); about->addAuthor(i18n("Kai Uwe Broulik"), QString(), QStringLiteral("kde@broulik.de")); setAboutData(about); + if (gtkConfigKdedModuleLoaded()) { + m_gtkPage = new GtkPage(this); + connect(m_gtkPage, &GtkPage::gtkThemeSettingsChanged, this, [this](){ + setNeedsSave(true); + }); + } connect(m_model, &StylesModel::selectedStyleChanged, this, [this](const QString &style) { m_settings->setWidgetStyle(style); }); connect(m_settings, &StyleSettings::widgetStyleChanged, this, [this] { m_model->setSelectedStyle(m_settings->widgetStyle()); }); connect(m_settings, &StyleSettings::iconsOnButtonsChanged, this, [this] { m_effectsDirty = true; }); connect(m_settings, &StyleSettings::iconsInMenusChanged, this, [this] { m_effectsDirty = true; }); } KCMStyle::~KCMStyle() = default; StylesModel *KCMStyle::model() const { return m_model; } StyleSettings *KCMStyle::styleSettings() const { return m_settings; } KCMStyle::ToolBarStyle KCMStyle::mainToolBarStyle() const { return m_mainToolBarStyle; } void KCMStyle::setMainToolBarStyle(ToolBarStyle style) { if (m_mainToolBarStyle != style) { m_mainToolBarStyle = style; emit mainToolBarStyleChanged(); const QMetaEnum toolBarStyleEnum = QMetaEnum::fromType(); m_settings->setToolButtonStyle(toolBarStyleEnum.valueToKey(m_mainToolBarStyle)); m_effectsDirty = true; } } KCMStyle::ToolBarStyle KCMStyle::otherToolBarStyle() const { return m_otherToolBarStyle; } void KCMStyle::setOtherToolBarStyle(ToolBarStyle style) { if (m_otherToolBarStyle != style) { m_otherToolBarStyle = style; emit otherToolBarStyleChanged(); const QMetaEnum toolBarStyleEnum = QMetaEnum::fromType(); m_settings->setToolButtonStyleOtherToolbars(toolBarStyleEnum.valueToKey(m_otherToolBarStyle)); m_effectsDirty = true; } } void KCMStyle::configure(const QString &styleName, QQuickItem *ctx) { if (m_styleConfigDialog) { return; } const QString configPage = m_model->styleConfigPage(styleName); if (configPage.isEmpty()) { return; } QLibrary library(KPluginLoader::findPlugin(configPage)); if (!library.load()) { qWarning() << "Failed to load style config page" << configPage << library.errorString(); emit showErrorMessage(i18n("There was an error loading the configuration dialog for this style.")); return; } auto allocPtr = library.resolve("allocate_kstyle_config"); if (!allocPtr) { qWarning() << "Failed to resolve allocate_kstyle_config in" << configPage; emit showErrorMessage(i18n("There was an error loading the configuration dialog for this style.")); return; } m_styleConfigDialog = new StyleConfigDialog(nullptr/*this*/, configPage); m_styleConfigDialog->setAttribute(Qt::WA_DeleteOnClose); m_styleConfigDialog->setWindowModality(Qt::WindowModal); m_styleConfigDialog->winId(); // so it creates windowHandle if (ctx && ctx->window()) { if (QWindow *actualWindow = QQuickRenderControl::renderWindowFor(ctx->window())) { m_styleConfigDialog->windowHandle()->setTransientParent(actualWindow); } } typedef QWidget*(* factoryRoutine)( QWidget* parent ); //Get the factory, and make the widget. factoryRoutine factory = (factoryRoutine)(allocPtr); //Grmbl. So here I am on my //"never use C casts" moralizing streak, and I find that one can't go void* -> function ptr //even with a reinterpret_cast. QWidget* pluginConfig = factory( m_styleConfigDialog.data() ); //Insert it in... m_styleConfigDialog->setMainWidget( pluginConfig ); //..and connect it to the wrapper connect(pluginConfig, SIGNAL(changed(bool)), m_styleConfigDialog.data(), SLOT(setDirty(bool))); connect(m_styleConfigDialog.data(), SIGNAL(defaults()), pluginConfig, SLOT(defaults())); connect(m_styleConfigDialog.data(), SIGNAL(save()), pluginConfig, SLOT(save())); connect(m_styleConfigDialog.data(), &QDialog::accepted, this, [this, styleName] { if (!m_styleConfigDialog->isDirty()) { return; } // Force re-rendering of the preview, to apply settings emit styleReconfigured(styleName); //For now, ask all KDE apps to recreate their styles to apply the setitngs KGlobalSettings::self()->emitChange(KGlobalSettings::StyleChanged); // When user edited a style, assume they want to use it, too m_settings->setWidgetStyle(styleName); // We call setNeedsSave(true) here to make sure we force style re-creation setNeedsSave(true); }); m_styleConfigDialog->show(); } +bool KCMStyle::gtkConfigKdedModuleLoaded() +{ + QDBusInterface kdedInterface( + QStringLiteral("org.kde.kded5"), + QStringLiteral("/kded"), + QStringLiteral("org.kde.kded5") + ); + QDBusReply loadedKdedModules = kdedInterface.call(QStringLiteral("loadedModules")); + return loadedKdedModules.value().contains(QStringLiteral("gtkconfig")); +} + void KCMStyle::load() { + if (m_gtkPage) { + m_gtkPage->load(); + } + ManagedConfigModule::load(); m_model->load(); m_previousStyle = m_settings->widgetStyle(); loadSettingsToModel(); m_effectsDirty = false; } void KCMStyle::save() { + if (m_gtkPage) { + m_gtkPage->save(); + } + // Check whether the new style can actually be loaded before saving it. // Otherwise apps will use the default style despite something else having been written to the config bool newStyleLoaded = false; if (m_settings->widgetStyle() != m_previousStyle) { QScopedPointer newStyle(QStyleFactory::create(m_settings->widgetStyle())); if (newStyle) { newStyleLoaded = true; m_previousStyle = m_settings->widgetStyle(); } else { const QString styleDisplay = m_model->data(m_model->index(m_model->indexOfStyle(m_settings->widgetStyle()), 0), Qt::DisplayRole).toString(); emit showErrorMessage(i18n("Failed to apply selected style '%1'.", styleDisplay)); // Reset selected style back to current in case of failure m_settings->setWidgetStyle(m_previousStyle); } } ManagedConfigModule::save(); // Export the changes we made to qtrc, and update all qt-only // applications on the fly, ensuring that we still follow the user's // export fonts/colors settings. uint flags = KRdbExportQtSettings | KRdbExportGtkTheme; KConfig _kconfig( QStringLiteral("kcmdisplayrc"), KConfig::NoGlobals ); KConfigGroup kconfig(&_kconfig, "X11"); bool exportKDEColors = kconfig.readEntry("exportKDEColors", true); if (exportKDEColors) { flags |= KRdbExportColors; } runRdb( flags ); // Now allow KDE apps to reconfigure themselves. if (newStyleLoaded) { KGlobalSettings::self()->emitChange(KGlobalSettings::StyleChanged); } if (m_effectsDirty) { KGlobalSettings::self()->emitChange(KGlobalSettings::SettingsChanged, KGlobalSettings::SETTINGS_STYLE); // ##### FIXME - Doesn't apply all settings correctly due to bugs in // KApplication/KToolbar KGlobalSettings::self()->emitChange(KGlobalSettings::ToolbarStyleChanged); } m_effectsDirty = false; } void KCMStyle::defaults() { + if (m_gtkPage) { + m_gtkPage->defaults(); + } + // TODO the old code had a fallback chain but do we actually support not having Breeze for Plasma? // defaultStyle() -> oxygen -> plastique -> windows -> platinum -> motif ManagedConfigModule::defaults(); loadSettingsToModel(); } void KCMStyle::loadSettingsToModel() { emit m_settings->widgetStyleChanged(); const QMetaEnum toolBarStyleEnum = QMetaEnum::fromType(); setMainToolBarStyle(static_cast(toolBarStyleEnum.keyToValue(qUtf8Printable(m_settings->toolButtonStyle())))); setOtherToolBarStyle(static_cast(toolBarStyleEnum.keyToValue(qUtf8Printable(m_settings->toolButtonStyleOtherToolbars())))); } #include "kcmstyle.moc" diff --git a/kcms/style/kcmstyle.h b/kcms/style/kcmstyle.h index 227b1400e..b62245d6a 100644 --- a/kcms/style/kcmstyle.h +++ b/kcms/style/kcmstyle.h @@ -1,101 +1,106 @@ /* * KCMStyle * Copyright (C) 2002 Karol Szwed * Copyright (C) 2002 Daniel Molkentin * Copyright (C) 2007 Urs Wolfer * Copyright (C) 2019 Kai Uwe Broulik * Copyright (C) 2019 Cyril Rossi * * Portions Copyright (C) TrollTech AS. * * Based on kcmdisplay * Copyright (C) 1997-2002 kcmdisplay Authors. * (see Help -> About Style Settings) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License version 2 as published by the Free Software Foundation. * * 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; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KCMSTYLE_H #define KCMSTYLE_H #include #include +#include "gtkpage.h" + class QQuickItem; class StyleSettings; class StylesModel; class StyleConfigDialog; class KCMStyle : public KQuickAddons::ManagedConfigModule { Q_OBJECT + Q_PROPERTY(GtkPage *gtkPage MEMBER m_gtkPage CONSTANT) Q_PROPERTY(StylesModel *model READ model CONSTANT) Q_PROPERTY(StyleSettings *styleSettings READ styleSettings CONSTANT) Q_PROPERTY(ToolBarStyle mainToolBarStyle READ mainToolBarStyle WRITE setMainToolBarStyle NOTIFY mainToolBarStyleChanged) Q_PROPERTY(ToolBarStyle otherToolBarStyle READ otherToolBarStyle WRITE setOtherToolBarStyle NOTIFY otherToolBarStyleChanged) public: KCMStyle(QObject *parent, const QVariantList &args); ~KCMStyle() override; enum ToolBarStyle { NoText, TextOnly, TextBesideIcon, TextUnderIcon }; Q_ENUM(ToolBarStyle) StylesModel *model() const; StyleSettings *styleSettings() const; ToolBarStyle mainToolBarStyle() const; void setMainToolBarStyle(ToolBarStyle style); Q_SIGNAL void mainToolBarStyleChanged(); ToolBarStyle otherToolBarStyle() const; void setOtherToolBarStyle(ToolBarStyle style); Q_SIGNAL void otherToolBarStyleChanged(); Q_INVOKABLE void configure(const QString &styleName, QQuickItem *ctx = nullptr); + Q_INVOKABLE bool gtkConfigKdedModuleLoaded(); void load() override; void save() override; void defaults() override; Q_SIGNALS: void showErrorMessage(const QString &message); void styleReconfigured(const QString &styleName); private: void loadSettingsToModel(); StyleSettings *m_settings; StylesModel *m_model; QString m_previousStyle; bool m_effectsDirty = false; ToolBarStyle m_mainToolBarStyle = NoText; ToolBarStyle m_otherToolBarStyle = NoText; QPointer m_styleConfigDialog; + GtkPage *m_gtkPage; }; #endif // __KCMSTYLE_H diff --git a/kcms/style/package/contents/ui/GtkStylePage.qml b/kcms/style/package/contents/ui/GtkStylePage.qml new file mode 100644 index 000000000..9e9890825 --- /dev/null +++ b/kcms/style/package/contents/ui/GtkStylePage.qml @@ -0,0 +1,198 @@ +/* + * Copyright 2020 Mikhail Zolotukhin + * + * 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.7 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.0 as QtDialogs +import QtQuick.Controls 2.10 as QtControls +import org.kde.kirigami 2.10 as Kirigami +import org.kde.private.kcms.style 1.0 as Private +import org.kde.kcm 1.2 as KCM + +Kirigami.Page { + id: gtkStylePage + title: i18n("GNOME/GTK Application Style") + + ColumnLayout { + anchors.fill: parent + + Kirigami.InlineMessage { + id: infoLabel + Layout.fillWidth: true + + showCloseButton: true + visible: false + + Connections { + target: kcm.gtkPage + onShowErrorMessage: { + infoLabel.type = Kirigami.MessageType.Error; + infoLabel.text = message; + infoLabel.visible = true; + } + } + } + + Kirigami.FormLayout { + wideMode: true + + Row { + Kirigami.FormData.label: i18n("GTK2 theme:") + + Flow { + spacing: Kirigami.Units.smallSpacing + + QtControls.ComboBox { + id: gtk2ThemeCombo + model: kcm.gtkPage.gtk2ThemesModel + currentIndex: model.findThemeIndex(kcm.gtkPage.gtk2ThemeFromConfig()) + onCurrentTextChanged: function() { + model.selectedTheme = currentText + gtk2RemoveButton.enabled = model.selectedThemeRemovable() + } + onActivated: model.setSelectedThemeDirty() + textRole: "theme-name" + + Connections { + target: kcm.gtkPage + onSelectGtk2ThemeInCombobox: function(themeName) { + gtk2ThemeCombo.currentIndex = gtk2ThemeCombo.model.findThemeIndex(themeName) + } + } + } + + QtControls.Button { + id: gtk2RemoveButton + icon.name: "edit-delete" + onClicked: gtk2ThemeCombo.model.removeSelectedTheme() + } + + QtControls.Button { + icon.name: "preview" + text: i18n("Preview...") + onClicked: kcm.gtkPage.showGtk2Preview() + } + } + } + + Row { + Kirigami.FormData.label: i18n("GTK3 theme:") + + Flow { + spacing: Kirigami.Units.smallSpacing + + QtControls.ComboBox { + id: gtk3ThemeCombo + model: kcm.gtkPage.gtk3ThemesModel + currentIndex: model.findThemeIndex(kcm.gtkPage.gtk3ThemeFromConfig()) + onCurrentTextChanged: function() { + model.selectedTheme = currentText + gtk3RemoveButton.enabled = model.selectedThemeRemovable() + } + onActivated: model.setSelectedThemeDirty() + textRole: "theme-name" + + Connections { + target: kcm.gtkPage + onSelectGtk3ThemeInCombobox: function(themeName) { + gtk3ThemeCombo.currentIndex = gtk3ThemeCombo.model.findThemeIndex(themeName) + } + } + } + + QtControls.Button { + id: gtk3RemoveButton + icon.name: "edit-delete" + onClicked: gtk3ThemeCombo.model.removeSelectedTheme() + } + + QtControls.Button { + icon.name: "preview" + text: i18n("Preview...") + onClicked: kcm.gtkPage.showGtk3Preview() + } + + } + } + + } + + Item { + Layout.fillHeight: true + } + + RowLayout { + Item { + Layout.fillWidth: true + } + + QtControls.Button { + icon.name: "document-import" + text: i18n("Install from File...") + onClicked: fileDialogLoader.active = true + } + + QtControls.Button { + icon.name: "get-hot-new-stuff" + text: i18n("Get New GNOME/GTK Application Styles...") + onClicked: ghnsMenu.open() + + QtControls.Menu { + id: ghnsMenu + + QtControls.MenuItem { + icon.name: "get-hot-new-stuff" + text: i18n("Get New GNOME/GTK2 Application Styles...") + onClicked: function() { + ghnsMenu.close() + kcm.gtkPage.installGtk2ThemeFromGHNS() + } + } + QtControls.MenuItem { + icon.name: "get-hot-new-stuff" + text: i18n("Get New GNOME/GTK3 Application Styles...") + onClicked: function() { + ghnsMenu.close() + kcm.gtkPage.installGtk3ThemeFromGHNS() + } + } + } + } + } + } + + Loader { + id: fileDialogLoader + active: false + sourceComponent: QtDialogs.FileDialog { + title: i18n("Select GTK Theme Archive") + folder: shortcuts.home + nameFilters: [ i18n("GTK Theme Archive (*.tar.xz *.tar.gz *.tar.bz2)") ] + Component.onCompleted: open() + onAccepted: { + kcm.gtkPage.installGtkThemeFromFile(fileUrls[0]) + fileDialogLoader.active = false + } + onRejected: { + fileDialogLoader.active = false + } + } + } +} diff --git a/kcms/style/package/contents/ui/main.qml b/kcms/style/package/contents/ui/main.qml index 39cad51ba..931b46027 100644 --- a/kcms/style/package/contents/ui/main.qml +++ b/kcms/style/package/contents/ui/main.qml @@ -1,138 +1,152 @@ /* * Copyright 2019 Kai Uwe Broulik * * 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.6 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 as QtControls import org.kde.kirigami 2.4 as Kirigami import org.kde.kcm 1.1 as KCM import org.kde.private.kcms.style 1.0 as Private KCM.GridViewKCM { id: root KCM.ConfigModule.quickHelp: i18n("This module allows you to modify the visual appearance of applications' user interface elements.") view.model: kcm.model view.currentIndex: kcm.model.selectedStyleIndex view.enabled: !kcm.styleSettings.isImmutable("widgetStyle") + function openGtkStyleSettings() { + kcm.push("GtkStylePage.qml"); + } + Component.onCompleted: { // The widget thumbnails are a bit more elaborate and need more room, especially when translated view.implicitCellWidth = Kirigami.Units.gridUnit * 20; view.implicitCellHeight = Kirigami.Units.gridUnit * 15; } // putting the InlineMessage as header item causes it to show up initially despite visible false header: ColumnLayout { Kirigami.InlineMessage { id: infoLabel Layout.fillWidth: true showCloseButton: true visible: false Connections { target: kcm onShowErrorMessage: { infoLabel.type = Kirigami.MessageType.Error; infoLabel.text = message; infoLabel.visible = true; } } } } view.delegate: KCM.GridDelegate { id: delegate text: model.display toolTip: model.description thumbnailAvailable: thumbnailItem.valid thumbnail: Item { anchors.fill: parent clip: thumbnailItem.implicitWidth * thumbnailItem.scale > width Private.PreviewItem { id: thumbnailItem anchors.centerIn: parent // Fit the widget's width in the grid view // Round up to the nearest 0.05, so that 0.95 gets scaled up to 1.0 // to avoid blurriness in case the widget is only slightly bigger scale: (Math.ceil(Math.min(1, parent.width / implicitWidth) * 20) / 20).toFixed(2) width: Math.max(parent.width, implicitWidth) // Scale the height back up as the background color comes from the style // and we don't want to leave an ugly gap above/below it height: parent.height / scale smooth: true styleName: model.styleName } Connections { target: kcm onStyleReconfigured: { if (styleName === model.styleName) { thumbnailItem.reload(); } } } } actions: [ Kirigami.Action { iconName: "document-edit" tooltip: i18n("Configure Style...") enabled: model.configurable onTriggered: kcm.configure(model.styleName, delegate) } ] onClicked: { kcm.model.selectedStyle = model.styleName; view.forceActiveFocus(); } } - footer: RowLayout { - Layout.fillWidth: true - - QtControls.Button { - id: effectSettingsButton - text: i18n("Configure Icons and Toolbars") - icon.name: "configure-toolbars" // proper icon? - checkable: true - checked: effectSettingsPopupLoader.item && effectSettingsPopupLoader.item.opened - onClicked: { - effectSettingsPopupLoader.active = true; - effectSettingsPopupLoader.item.open(); + footer: ColumnLayout { + RowLayout { + Layout.alignment: Qt.AlignLeft + + QtControls.Button { + id: effectSettingsButton + text: i18n("Configure Icons and Toolbars") + icon.name: "configure-toolbars" // proper icon? + checkable: true + checked: effectSettingsPopupLoader.item && effectSettingsPopupLoader.item.opened + onClicked: { + effectSettingsPopupLoader.active = true; + effectSettingsPopupLoader.item.open(); + } + } + + QtControls.Button { + id: gtkSettingsButton + visible: kcm.gtkConfigKdedModuleLoaded() + text: i18n("Configure GNOME/GTK Application Style...") + icon.name: "configure" + onClicked: root.openGtkStyleSettings() } } } Loader { id: effectSettingsPopupLoader active: false sourceComponent: EffectSettingsPopup { parent: effectSettingsButton y: -height } } }