diff --git a/kcms/style/CMakeLists.txt b/kcms/style/CMakeLists.txt --- a/kcms/style/CMakeLists.txt +++ b/kcms/style/CMakeLists.txt @@ -3,12 +3,12 @@ ########### next target ############### -set(kcm_style_PART_SRCS ../krdb/krdb.cpp styleconfdialog.cpp kcmstyle.cpp) +set(kcm_style_PART_SRCS ../krdb/krdb.cpp styleconfdialog.cpp kcmstyle.cpp stylesmodel.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 styleconfig.ui) +ki18n_wrap_ui(kcm_style_PART_SRCS stylepreview.ui) add_library(kcm_style MODULE ${kcm_style_PART_SRCS}) @@ -23,13 +23,13 @@ ${X11_LIBRARIES} KF5::KDELibs4Support KF5::GuiAddons + KF5::QuickAddons KF5::WindowSystem - KF5::NewStuff ) -install(TARGETS kcm_style DESTINATION ${KDE_INSTALL_PLUGINDIR}) +kcoreaddons_desktop_to_json(kcm_style "kcm_style.desktop") +install(FILES kcm_style.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) +install(TARGETS kcm_style DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms) -########### install files ############### - -install( FILES style.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR} ) +kpackage_install_package(package kcm_style kcms) diff --git a/kcms/style/Messages.sh b/kcms/style/Messages.sh --- a/kcms/style/Messages.sh +++ b/kcms/style/Messages.sh @@ -1,4 +1,4 @@ #! /usr/bin/env bash $EXTRACTRC *.ui >> rc.cpp -$XGETTEXT *.cpp -o $podir/kcmstyle.pot +$XGETTEXT `find . -name "*.cpp" -o -name "*.qml"` -o $podir/kcmstyle.pot rm -f rc.cpp diff --git a/kcms/style/style.desktop b/kcms/style/kcm_style.desktop rename from kcms/style/style.desktop rename to kcms/style/kcm_style.desktop diff --git a/kcms/style/kcmstyle.h b/kcms/style/kcmstyle.h --- a/kcms/style/kcmstyle.h +++ b/kcms/style/kcmstyle.h @@ -3,6 +3,7 @@ * Copyright (C) 2002 Karol Szwed * Copyright (C) 2002 Daniel Molkentin * Copyright (C) 2007 Urs Wolfer + * Copyright (C) 2019 Kai Uwe Broulik * * Portions Copyright (C) TrollTech AS. * @@ -28,75 +29,81 @@ #ifndef KCMSTYLE_H #define KCMSTYLE_H -#include -#include -#include +#include -#include +#include -class KConfig; -class StylePreview; -class StyleConfig; +class QQuickItem; -struct StyleEntry { - QString name; - QString desc; - QString configPage; - bool hidden; -}; +class StylesModel; +class StyleConfigDialog; -class KCMStyle : public KCModule +class KCMStyle : public KQuickAddons::ConfigModule { Q_OBJECT + Q_PROPERTY(StylesModel *model READ model CONSTANT) + + Q_PROPERTY(bool iconsOnButtons READ iconsOnButtons WRITE setIconsOnButtons NOTIFY iconsOnButtonsChanged) + Q_PROPERTY(bool iconsInMenus READ iconsInMenus WRITE setIconsInMenus NOTIFY iconsInMenusChanged) + Q_PROPERTY(ToolBarStyle mainToolBarStyle READ mainToolBarStyle WRITE setMainToolBarStyle NOTIFY mainToolBarStyleChanged) + Q_PROPERTY(ToolBarStyle otherToolBarStyle READ otherToolBarStyle WRITE setOtherToolBarStyle NOTIFY otherToolBarStyleChanged) + public: - KCMStyle( QWidget* parent, const QVariantList& ); + KCMStyle(QObject *parent, const QVariantList &args); ~KCMStyle() override; + enum ToolBarStyle { + NoText, + TextOnly, + TextBesideIcon, + TextUnderIcon + }; + Q_ENUM(ToolBarStyle) + + StylesModel *model() const; + + bool iconsOnButtons() const; + void setIconsOnButtons(bool enable); + Q_SIGNAL void iconsOnButtonsChanged(); + + bool iconsInMenus() const; + void setIconsInMenus(bool enable); + Q_SIGNAL void iconsInMenusChanged(); + + 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); + void load() override; void save() override; void defaults() override; static QString defaultStyle(); -protected: - bool findStyle( const QString& str, int& combobox_item ); - void switchStyle(const QString& styleName, bool force = false); - void setStyleRecursive(QWidget* w, QStyle* s); +Q_SIGNALS: + void showErrorMessage(const QString &message); - void loadStyle( KConfig& config ); - void loadEffects( KConfig& config ); - void addWhatsThis(); + void styleReconfigured(const QString &styleName); - void changeEvent( QEvent *event ) override; - -protected Q_SLOTS: - void styleSpecificConfig(); - void updateConfigButton(); +private: + StylesModel *m_model; - void setStyleDirty(); - void setEffectsDirty(); + bool m_selectedStyleDirty = false; + bool m_effectsDirty = false; - void styleChanged(); + bool m_iconsOnButtons = false; + bool m_iconsInMenus = false; + ToolBarStyle m_mainToolBarStyle = NoText; + ToolBarStyle m_otherToolBarStyle = NoText; -private: - QString currentStyle(); - static QString toolbarButtonText(int index); - static int toolbarButtonIndex(const QString &text); - static QString menuBarStyleText(int index); - static int menuBarStyleIndex(const QString &text); - - bool m_bStyleDirty, m_bEffectsDirty; - QHash styleEntries; - QMap nameToStyleKey; - - QVBoxLayout* mainLayout; - - // Widgets - StylePreview* stylePreview; - StyleConfig* styleConfig; - QStyle* appliedStyle; - QPalette palette; + QPointer m_styleConfigDialog; }; #endif // __KCMSTYLE_H diff --git a/kcms/style/kcmstyle.cpp b/kcms/style/kcmstyle.cpp --- a/kcms/style/kcmstyle.cpp +++ b/kcms/style/kcmstyle.cpp @@ -4,7 +4,8 @@ * Copyright (C) 2002 Daniel Molkentin * Copyright (C) 2007 Urs Wolfer * Copyright (C) 2009 by Davide Bettio - + * Copyright (C) 2019 Kai Uwe Broulik + * * Portions Copyright (C) 2007 Paolo Capriotti * Portions Copyright (C) 2007 Ivan Cukic * Portions Copyright (C) 2008 by Petri Damsten @@ -27,68 +28,33 @@ #include "kcmstyle.h" -#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) -#include -#endif - #include "styleconfdialog.h" -#include "ui_stylepreview.h" -#include "ui_styleconfig.h" - -#include -//#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include #include -#include -#include -#include -#include -#include +#include +#include #include #include -#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) -#include -#endif - #include "../krdb/krdb.h" -#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) -#include -#endif - -// X11 namespace cleanup -#undef Bool -#undef Below -#undef KeyPress -#undef KeyRelease - - -/**** DLL Interface for kcontrol ****/ - -#include -#include - -K_PLUGIN_FACTORY(KCMStyleFactory, registerPlugin();) -K_EXPORT_PLUGIN(KCMStyleFactory("kcmstyle")) +#include "stylesmodel.h" +#include "previewitem.h" +K_PLUGIN_FACTORY_WITH_JSON(KCMStyleFactory, "kcm_style.json", registerPlugin();) extern "C" { @@ -106,607 +72,295 @@ } } -class StylePreview : public QWidget, public Ui::StylePreview +QString KCMStyle::defaultStyle() { -public: - StylePreview(QWidget *parent = nullptr) - : QWidget(parent) - { - setupUi(this); - - // Ensure that the user can't toy with the child widgets. - // Method borrowed from Qt's qtconfig. - QList widgets = findChildren(); - foreach (QWidget* widget, widgets) - { - widget->installEventFilter(this); - widget->setFocusPolicy(Qt::NoFocus); - } - } + return QStringLiteral("Breeze"); +} - bool eventFilter( QObject* /* obj */, QEvent* ev ) override - { - switch( ev->type() ) - { - case QEvent::MouseButtonPress: - case QEvent::MouseButtonRelease: - case QEvent::MouseButtonDblClick: - case QEvent::MouseMove: - case QEvent::KeyPress: - case QEvent::KeyRelease: - case QEvent::Enter: - case QEvent::Leave: - case QEvent::Wheel: - case QEvent::ContextMenu: - return true; // ignore - default: - break; - } - return false; - } -}; +KCMStyle::KCMStyle(QObject *parent, const QVariantList &args) + : KQuickAddons::ConfigModule(parent, args) + , m_model(new StylesModel(this)) +{ + qmlRegisterUncreatableType("org.kde.private.kcms.style", 1, 0, "KCM", QStringLiteral("Cannot create instances of KCM")); + 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); + + connect(m_model, &StylesModel::selectedStyleChanged, this, [this] { + m_selectedStyleDirty = true; + setNeedsSave(true); + }); +} + +KCMStyle::~KCMStyle() = default; -class StyleConfig : public QWidget, public Ui::StyleConfig +StylesModel *KCMStyle::model() const { -public: - StyleConfig(QWidget *parent = nullptr) - : QWidget(parent) - { - setupUi(this); - } -}; + return m_model; +} -QString KCMStyle::defaultStyle() +bool KCMStyle::iconsOnButtons() const { -#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) - return QStringLiteral("breeze"); -#else - return QString(); // native style -#endif + return m_iconsOnButtons; } -KCMStyle::KCMStyle( QWidget* parent, const QVariantList& ) - : KCModule( parent ), appliedStyle(nullptr) +void KCMStyle::setIconsOnButtons(bool enable) { - setQuickHelp( i18n("This module allows you to modify the visual appearance " - "of applications' user interface elements.")); + if (m_iconsOnButtons != enable) { + m_iconsOnButtons = enable; + emit iconsOnButtonsChanged(); - m_bStyleDirty= false; - m_bEffectsDirty = false; + m_effectsDirty = true; + setNeedsSave(true); + } +} +bool KCMStyle::iconsInMenus() const +{ + return m_iconsInMenus; +} - KGlobal::dirs()->addResourceType("themes", "data", "kstyle/themes"); +void KCMStyle::setIconsInMenus(bool enable) +{ + if (m_iconsInMenus != enable) { + m_iconsInMenus = enable; + emit iconsInMenusChanged(); - KAboutData *about = - new KAboutData( QStringLiteral("kcmstyle"), i18n("Application Style"), QStringLiteral("1.0"), - QString(), KAboutLicense::GPL, - i18n("(c) 2002 Karol Szwed, Daniel Molkentin")); + m_effectsDirty = true; + setNeedsSave(true); + } +} - about->addAuthor(i18n("Karol Szwed"), QString(), QStringLiteral("gallium@kde.org")); - about->addAuthor(i18n("Daniel Molkentin"), QString(), QStringLiteral("molkentin@kde.org")); - setAboutData( about ); - - // Setup mainLayout - mainLayout = new QVBoxLayout( this ); - mainLayout->setContentsMargins(0, 0, 0, 0); - - styleConfig = new StyleConfig(); - - QHBoxLayout *previewLayout = new QHBoxLayout(); - QGroupBox *gbPreview = new QGroupBox( i18n( "Preview" ) ); - QHBoxLayout *previewLayoutInner = new QHBoxLayout(gbPreview); - previewLayout->setContentsMargins(0, 0, 0, 0); - previewLayoutInner->setContentsMargins(0, 0, 0, 0); - previewLayout->addStretch(); - previewLayout->addWidget( gbPreview ); - previewLayout->addStretch(); - - stylePreview = new StylePreview( gbPreview ); - previewLayoutInner->addWidget( stylePreview ); - - mainLayout->addWidget( styleConfig ); - mainLayout->addLayout( previewLayout ); - mainLayout->addStretch(); - - connect( styleConfig->comboStyle, SIGNAL(activated(int)), this, SLOT(styleChanged()) ); - connect( styleConfig->comboStyle, SIGNAL(activated(int)), this, SLOT(updateConfigButton()) ); - connect( styleConfig->pbConfigStyle, &QAbstractButton::clicked, this, &KCMStyle::styleSpecificConfig ); - connect( styleConfig->comboStyle, SIGNAL(activated(int)), this, SLOT(setStyleDirty()) ); - connect( styleConfig->cbIconsOnButtons, &QAbstractButton::toggled, this, &KCMStyle::setEffectsDirty ); - connect( styleConfig->cbIconsInMenus, &QAbstractButton::toggled, this, &KCMStyle::setEffectsDirty ); - connect( styleConfig->comboToolbarIcons, SIGNAL(activated(int)), this, SLOT(setEffectsDirty()) ); - connect( styleConfig->comboSecondaryToolbarIcons, SIGNAL(activated(int)), this, SLOT(setEffectsDirty()) ); - - addWhatsThis(); +KCMStyle::ToolBarStyle KCMStyle::mainToolBarStyle() const +{ + return m_mainToolBarStyle; } +void KCMStyle::setMainToolBarStyle(ToolBarStyle style) +{ + if (m_mainToolBarStyle != style) { + m_mainToolBarStyle = style; + emit mainToolBarStyleChanged(); + + m_effectsDirty = true; + setNeedsSave(true); + } +} -KCMStyle::~KCMStyle() +KCMStyle::ToolBarStyle KCMStyle::otherToolBarStyle() const { - qDeleteAll(styleEntries); - delete appliedStyle; + return m_otherToolBarStyle; } -void KCMStyle::updateConfigButton() +void KCMStyle::setOtherToolBarStyle(ToolBarStyle style) { - if (!styleEntries[currentStyle()] || styleEntries[currentStyle()]->configPage.isEmpty()) { - styleConfig->pbConfigStyle->setEnabled(false); - return; - } + if (m_otherToolBarStyle != style) { + m_otherToolBarStyle = style; + emit otherToolBarStyleChanged(); - // We don't check whether it's loadable here - - // lets us report an error and not waste time - // loading things if the user doesn't click the button - styleConfig->pbConfigStyle->setEnabled( true ); + m_effectsDirty = true; + setNeedsSave(true); + } } -void KCMStyle::styleSpecificConfig() +void KCMStyle::configure(const QString &styleName, QQuickItem *ctx) { - QString libname = styleEntries[currentStyle()]->configPage; + if (m_styleConfigDialog) { + return; + } - KLibrary library(libname); - if (!library.load()) { - KMessageBox::detailedError(this, - i18n("There was an error loading the configuration dialog for this style."), - library.errorString(), - i18n("Unable to Load Dialog")); + const QString configPage = m_model->styleConfigPage(styleName); + if (configPage.isEmpty()) { return; } - KLibrary::void_function_ptr allocPtr = library.resolveFunction("allocate_kstyle_config"); + 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; + } - if (!allocPtr) - { - KMessageBox::detailedError(this, - i18n("There was an error loading the configuration dialog for this style."), - library.errorString(), - i18n("Unable to Load Dialog")); + 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; } - //Create the container dialog - StyleConfigDialog* dial = new StyleConfigDialog(this, styleEntries[currentStyle()]->name); + 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( dial ); + QWidget* pluginConfig = factory( m_styleConfigDialog.data() ); //Insert it in... - dial->setMainWidget( pluginConfig ); + m_styleConfigDialog->setMainWidget( pluginConfig ); //..and connect it to the wrapper - connect(pluginConfig, SIGNAL(changed(bool)), dial, SLOT(setDirty(bool))); - connect(dial, SIGNAL(defaults()), pluginConfig, SLOT(defaults())); - connect(dial, SIGNAL(save()), pluginConfig, SLOT(save())); + 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; + } - if (dial->exec() == QDialog::Accepted && dial->isDirty() ) { // Force re-rendering of the preview, to apply settings - switchStyle(currentStyle(), true); + emit styleReconfigured(styleName); //For now, ask all KDE apps to recreate their styles to apply the setitngs KGlobalSettings::self()->emitChange(KGlobalSettings::StyleChanged); - // We call setStyleDirty here to make sure we force style re-creation - setStyleDirty(); - } + // When user edited a style, assume they want to use it, too + m_model->setSelectedStyle(styleName); - delete dial; -} + // We call setStyleDirty here to make sure we force style re-creation + m_selectedStyleDirty = true; + setNeedsSave(true); + }); -void KCMStyle::changeEvent( QEvent *event ) -{ - KCModule::changeEvent( event ); - if ( event->type() == QEvent::PaletteChange ) { - // Force re-rendering of the preview, to apply new palette - switchStyle(currentStyle(), true); - } + m_styleConfigDialog->show(); } void KCMStyle::load() { - KConfig config( QStringLiteral("kdeglobals"), KConfig::FullConfig ); + m_model->load(); - loadStyle( config ); - loadEffects( config ); + KConfig config(QStringLiteral("kdeglobals")); - m_bStyleDirty= false; - m_bEffectsDirty = false; - //Enable/disable the button for the initial style - updateConfigButton(); + // Current style + KConfigGroup kdeGroup = config.group("KDE"); + const QString widgetStyle = kdeGroup.readEntry("widgetStyle", KCMStyle::defaultStyle()); - emit changed( false ); -} + m_model->setSelectedStyle(widgetStyle); + + // Effects settings + setIconsOnButtons(kdeGroup.readEntry("ShowIconsOnPushButtons", true)); + setIconsInMenus(kdeGroup.readEntry("ShowIconsInMenuItems", true)); + + KConfigGroup toolBarGroup = config.group("Toolbar style"); + const QString mainToolBarStyle = toolBarGroup.readEntry("ToolButtonStyle", "TextBesideIcon"); + const QString otherToolBarStyle = toolBarGroup.readEntry("ToolButtonStyleOtherToolbars", "TextBesideIcon"); + const QMetaEnum toolBarStyleEnum = QMetaEnum::fromType(); + setMainToolBarStyle(static_cast(toolBarStyleEnum.keyToValue(qUtf8Printable(mainToolBarStyle)))); + setOtherToolBarStyle(static_cast(toolBarStyleEnum.keyToValue(qUtf8Printable(otherToolBarStyle)))); + + m_selectedStyleDirty = false; + m_effectsDirty = false; + setNeedsSave(false); +} void KCMStyle::save() { - // Don't do anything if we don't need to. - if ( !(m_bStyleDirty | m_bEffectsDirty ) ) + if (!m_selectedStyleDirty && !m_effectsDirty) { return; + } - // Save effects. - KConfig _config(QStringLiteral("kdeglobals"), KConfig::NoGlobals); - KConfigGroup config(&_config, "KDE"); - // Effects page - config.writeEntry( "ShowIconsOnPushButtons", styleConfig->cbIconsOnButtons->isChecked()); - config.writeEntry( "ShowIconsInMenuItems", styleConfig->cbIconsInMenus->isChecked()); + // 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_selectedStyleDirty) { + QScopedPointer newStyle(QStyleFactory::create(m_model->selectedStyle())); + if (newStyle) { + newStyleLoaded = true; + } else { + const QString styleDisplay = m_model->data(m_model->index(m_model->selectedStyleIndex(), 0), Qt::DisplayRole).toString(); + emit showErrorMessage(i18n("Failed to apply selected style '%1'.", styleDisplay)); + } + } - config.writeEntry("widgetStyle", currentStyle()); + KConfig config(QStringLiteral("kdeglobals")); - KConfigGroup toolbarStyleGroup(&_config, "Toolbar style"); - toolbarStyleGroup.writeEntry("ToolButtonStyle", - toolbarButtonText(styleConfig->comboToolbarIcons->currentIndex())); - toolbarStyleGroup.writeEntry("ToolButtonStyleOtherToolbars", - toolbarButtonText(styleConfig->comboSecondaryToolbarIcons->currentIndex())); + KConfigGroup kdeGroup = config.group("KDE"); - _config.sync(); + if (newStyleLoaded) { + kdeGroup.writeEntry("widgetStyle", m_model->selectedStyle()); + } + + kdeGroup.writeEntry("ShowIconsOnPushButtons", m_iconsOnButtons); + kdeGroup.writeEntry("ShowIconsInMenuItems", m_iconsInMenus); + + KConfigGroup toolBarGroup = config.group("Toolbar style"); + const QMetaEnum toolBarStyleEnum = QMetaEnum::fromType(); + + toolBarGroup.writeEntry("ToolButtonStyle", toolBarStyleEnum.valueToKey(m_mainToolBarStyle)); + toolBarGroup.writeEntry("ToolButtonStyleOtherToolbars", toolBarStyleEnum.valueToKey(m_otherToolBarStyle)); + + config.sync(); // 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. - if (m_bStyleDirty || m_bEffectsDirty) // Export only if necessary - { + if (m_selectedStyleDirty || m_effectsDirty) { uint flags = KRdbExportQtSettings | KRdbExportGtkTheme; KConfig _kconfig( QStringLiteral("kcmdisplayrc"), KConfig::NoGlobals ); KConfigGroup kconfig(&_kconfig, "X11"); bool exportKDEColors = kconfig.readEntry("exportKDEColors", true); - if (exportKDEColors) + if (exportKDEColors) { flags |= KRdbExportColors; + } runRdb( flags ); } // Now allow KDE apps to reconfigure themselves. - if ( m_bStyleDirty ) + if (newStyleLoaded) { KGlobalSettings::self()->emitChange(KGlobalSettings::StyleChanged); + } - if ( m_bEffectsDirty ) { + 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); - -#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) - // Send signal to all kwin instances - QDBusMessage message = - QDBusMessage::createSignal(QStringLiteral("/KWin"), QStringLiteral("org.kde.KWin"), QStringLiteral("reloadConfig")); - QDBusConnection::sessionBus().send(message); -#endif } - // Clean up - m_bStyleDirty = false; - m_bEffectsDirty = false; - emit changed( false ); -} - - -bool KCMStyle::findStyle( const QString& str, int& combobox_item ) -{ - StyleEntry* se = styleEntries[str.toLower()]; - - QString name = se ? se->name : str; - - combobox_item = 0; - - //look up name - for( int i = 0; i < styleConfig->comboStyle->count(); i++ ) - { - if ( styleConfig->comboStyle->itemText(i) == name ) - { - combobox_item = i; - return true; - } + // Reset selected style back to current in case of failure + if (!newStyleLoaded) { + const QString widgetStyle = kdeGroup.readEntry("widgetStyle", KCMStyle::defaultStyle()); + m_model->setSelectedStyle(widgetStyle); } - return false; + m_effectsDirty = false; + setNeedsSave(false); } - void KCMStyle::defaults() { - // Select default style - int item = 0; - bool found; - - found = findStyle( defaultStyle(), item ); - if (!found) - found = findStyle( QStringLiteral("oxygen"), item ); - if (!found) - found = findStyle( QStringLiteral("plastique"), item ); - if (!found) - found = findStyle( QStringLiteral("windows"), item ); - if (!found) - found = findStyle( QStringLiteral("platinum"), item ); - if (!found) - found = findStyle( QStringLiteral("motif"), item ); - - styleConfig->comboStyle->setCurrentIndex( item ); - - m_bStyleDirty = true; - switchStyle( currentStyle() ); // make resets visible - - // Effects - styleConfig->comboToolbarIcons->setCurrentIndex(toolbarButtonIndex(QStringLiteral("TextBesideIcon"))); - styleConfig->comboSecondaryToolbarIcons->setCurrentIndex(toolbarButtonIndex(QStringLiteral("TextBesideIcon"))); - styleConfig->cbIconsOnButtons->setChecked(true); - styleConfig->cbIconsInMenus->setChecked(true); - emit changed(true); - emit updateConfigButton(); -} - -void KCMStyle::setEffectsDirty() -{ - m_bEffectsDirty = true; - emit changed(true); -} - -void KCMStyle::setStyleDirty() -{ - m_bStyleDirty = true; - emit changed(true); -} - -// ---------------------------------------------------------------- -// All the Style Switching / Preview stuff -// ---------------------------------------------------------------- - -void KCMStyle::loadStyle( KConfig& config ) -{ - styleConfig->comboStyle->clear(); - // Create a dictionary of WidgetStyle to Name and Desc. mappings, - // as well as the config page info - qDeleteAll(styleEntries); - styleEntries.clear(); - - QString strWidgetStyle; - QStringList list = KGlobal::dirs()->findAllResources("themes", QStringLiteral("*.themerc"), - KStandardDirs::Recursive | - KStandardDirs::NoDuplicates); - for (QStringList::iterator it = list.begin(); it != list.end(); ++it) - { - KConfig config( *it, KConfig::SimpleConfig); - if ( !(config.hasGroup("KDE") && config.hasGroup("Misc")) ) - continue; - - KConfigGroup configGroup = config.group("KDE"); - - strWidgetStyle = configGroup.readEntry("WidgetStyle"); - if (strWidgetStyle.isNull()) - continue; - - // We have a widgetstyle, so lets read the i18n entries for it... - StyleEntry* entry = new StyleEntry; - configGroup = config.group("Misc"); - entry->name = configGroup.readEntry("Name"); - entry->desc = configGroup.readEntry("Comment", i18n("No description available.")); - entry->configPage = configGroup.readEntry("ConfigPage", QString()); - - // Check if this style should be shown - configGroup = config.group("Desktop Entry"); - entry->hidden = configGroup.readEntry("Hidden", false); - - // Insert the entry into our dictionary. - styleEntries.insert(strWidgetStyle.toLower(), entry); - } - - // Obtain all style names - QStringList allStyles = QStyleFactory::keys(); - - // Get translated names, remove all hidden style entries. - QStringList styles; - StyleEntry* entry; - for (QStringList::iterator it = allStyles.begin(); it != allStyles.end(); ++it) - { - QString id = (*it).toLower(); - // Find the entry. - if ( (entry = styleEntries[id]) != nullptr ) - { - // Do not add hidden entries - if (entry->hidden) - continue; - - styles += entry->name; - - nameToStyleKey[entry->name] = id; - } - else - { - styles += (*it); //Fall back to the key (but in original case) - nameToStyleKey[*it] = id; - } - } - - // Sort the style list, and add it to the combobox - styles.sort(); - styleConfig->comboStyle->addItems( styles ); - - // Find out which style is currently being used - KConfigGroup configGroup = config.group( "KDE" ); - QString defaultStyle = KCMStyle::defaultStyle(); - QString cfgStyle = configGroup.readEntry( "widgetStyle", defaultStyle ); - - // Select the current style - // Do not use comboStyle->listBox() as this may be NULL for some styles when - // they use QPopupMenus for the drop-down list! - - // ##### Since Trolltech likes to seemingly copy & paste code, - // QStringList::findItem() doesn't have a Qt::StringComparisonMode field. - // We roll our own (yuck) - cfgStyle = cfgStyle.toLower(); - int item = 0; - for( int i = 0; i < styleConfig->comboStyle->count(); i++ ) - { - QString id = nameToStyleKey[styleConfig->comboStyle->itemText(i)]; - item = i; - if ( id == cfgStyle ) // ExactMatch - break; - else if ( id.contains( cfgStyle ) ) - break; - else if ( id.contains( QApplication::style()->metaObject()->className() ) ) - break; - item = 0; - } - styleConfig->comboStyle->setCurrentIndex( item ); - m_bStyleDirty = false; - - switchStyle( currentStyle() ); // make resets visible -} - -QString KCMStyle::currentStyle() -{ - return nameToStyleKey[styleConfig->comboStyle->currentText()]; -} + // TODO the old code had a fallback chain but do we actually support not having Breeze for Plasma? + // defaultStyle() -> oxygen -> plastique -> windows -> platinum -> motif + m_model->setSelectedStyle(defaultStyle()); -void KCMStyle::styleChanged() -{ - switchStyle( currentStyle() ); -} - - -void KCMStyle::switchStyle(const QString& styleName, bool force) -{ - // Don't flicker the preview if the same style is chosen in the cb - if (!force && appliedStyle && appliedStyle->objectName() == styleName) - return; - - // Create an instance of the new style... - QStyle* style = QStyleFactory::create(styleName); - if (!style) - return; - - // Prevent Qt from wrongly caching radio button images - QPixmapCache::clear(); - - setStyleRecursive( stylePreview, style ); - - // this flickers, but reliably draws the widgets correctly. - stylePreview->resize( stylePreview->sizeHint() ); - - delete appliedStyle; - appliedStyle = style; -} - -void KCMStyle::setStyleRecursive(QWidget* w, QStyle* s) -{ - // Don't let broken styles kill the palette - // for other styles being previewed. (e.g SGI style) - w->setPalette(QPalette()); - - QPalette newPalette(KGlobalSettings::createApplicationPalette()); - s->polish( newPalette ); - w->setPalette(newPalette); - - // Apply the new style. - w->setStyle(s); - - // Recursively update all children. - const QObjectList children = w->children(); - - // Apply the style to each child widget. - foreach (QObject* child, children) - { - if (child->isWidgetType()) - setStyleRecursive((QWidget *) child, s); - } -} - -// ---------------------------------------------------------------- -// All the Effects stuff -// ---------------------------------------------------------------- -QString KCMStyle::toolbarButtonText(int index) -{ - switch (index) { - case 1: - return QStringLiteral("TextOnly"); - case 2: - return QStringLiteral("TextBesideIcon"); - case 3: - return QStringLiteral("TextUnderIcon"); - default: - break; - } - - return QStringLiteral("NoText"); -} - -int KCMStyle::toolbarButtonIndex(const QString &text) -{ - if (text == QLatin1String("TextOnly")) { - return 1; - } else if (text == QLatin1String("TextBesideIcon")) { - return 2; - } else if (text == QLatin1String("TextUnderIcon")) { - return 3; - } - - return 0; -} - -QString KCMStyle::menuBarStyleText(int index) -{ - switch (index) { - case 1: - return QStringLiteral("Decoration"); - case 2: - return QStringLiteral("Widget"); - } - - return QStringLiteral("InApplication"); -} - -int KCMStyle::menuBarStyleIndex(const QString &text) -{ - if (text == QLatin1String("Decoration")) { - return 1; - } else if (text == QLatin1String("Widget")) { - return 2; - } - - return 0; -} - -void KCMStyle::loadEffects( KConfig& config ) -{ - // KDE's Part via KConfig - KConfigGroup configGroup = config.group("Toolbar style"); - - QString tbIcon = configGroup.readEntry("ToolButtonStyle", "TextBesideIcon"); - styleConfig->comboToolbarIcons->setCurrentIndex(toolbarButtonIndex(tbIcon)); - tbIcon = configGroup.readEntry("ToolButtonStyleOtherToolbars", "TextBesideIcon"); - styleConfig->comboSecondaryToolbarIcons->setCurrentIndex(toolbarButtonIndex(tbIcon)); - - configGroup = config.group("KDE"); - styleConfig->cbIconsOnButtons->setChecked(configGroup.readEntry("ShowIconsOnPushButtons", true)); - styleConfig->cbIconsInMenus->setChecked(configGroup.readEntry("ShowIconsInMenuItems", true)); - - m_bEffectsDirty = false; -} - -void KCMStyle::addWhatsThis() -{ - stylePreview->setWhatsThis( i18n("This area shows a preview of the currently selected style " - "without having to apply it to the whole desktop.") ); - styleConfig->comboStyle->setWhatsThis( i18n("Here you can choose from a list of" - " predefined widget styles (e.g. the way buttons are drawn) which" - " may or may not be combined with a theme (additional information" - " like a marble texture or a gradient).") ); - styleConfig->cbIconsOnButtons->setWhatsThis( i18n( "If you enable this option, applications will " - "show small icons alongside some important buttons.") ); - styleConfig->cbIconsInMenus->setWhatsThis( i18n( "If you enable this option, applications will " - "show small icons alongside most menu items.") ); - styleConfig->comboToolbarIcons->setWhatsThis( i18n( "

No text: Shows only icons on toolbar buttons. " - "Best option for low resolutions.

" - "

Text only: Shows only text on toolbar buttons.

" - "

Text beside icons: Shows icons and text on toolbar buttons. " - "Text is aligned beside the icon.

" - "Text below icons: Shows icons and text on toolbar buttons. " - "Text is aligned below the icon.") ); + setIconsOnButtons(true); + setIconsInMenus(true); + setMainToolBarStyle(TextBesideIcon); + setOtherToolBarStyle(TextBesideIcon); } #include "kcmstyle.moc" diff --git a/kcms/style/package/contents/ui/EffectSettingsPopup.qml b/kcms/style/package/contents/ui/EffectSettingsPopup.qml new file mode 100644 --- /dev/null +++ b/kcms/style/package/contents/ui/EffectSettingsPopup.qml @@ -0,0 +1,81 @@ +/* + * Copyright 2018 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.7 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 as QtControls +import org.kde.kirigami 2.4 as Kirigami +import org.kde.private.kcms.style 1.0 as Private + +QtControls.Popup { + id: effectSettingsPopup + + modal: true + + onOpened: { + // can we do this automatically with "focus: true" somewhere? + iconsOnButtonsCheckBox.forceActiveFocus(); + } + + Kirigami.FormLayout { + // Popup's autosizing causes FormLayout to collapse when opening it a second time :( + wideMode: true + + QtControls.CheckBox { + id: iconsOnButtonsCheckBox + Kirigami.FormData.label: i18n("Show icons:") + text: i18n("On buttons") + checked: kcm.iconsOnButtons + onClicked: kcm.iconsOnButtons = checked + } + + QtControls.CheckBox { + text: i18n("In menus") + checked: kcm.iconsInMenus + onClicked: kcm.iconsInMenus = checked + } + + QtControls.ComboBox { + id: mainToolBarStyleCombo + Kirigami.FormData.label: i18n("Main toolbar label:") + model: [ + {text: i18n("None"), value: Private.KCM.NoText}, + {text: i18n("Text only"), value: Private.KCM.TextOnly}, + {text: i18n("Beside icons"), value: Private.KCM.TextBesideIcon}, + {text: i18n("Below icon"), value: Private.KCM.TextUnderIcon} + ] + textRole: "text" + currentIndex: model.findIndex(function (item) { + return item.value === kcm.mainToolBarStyle + }) + onActivated: kcm.mainToolBarStyle = model[currentIndex].value + } + + QtControls.ComboBox { + Kirigami.FormData.label: i18n("Secondary toolbar label:") + model: mainToolBarStyleCombo.model + textRole: "text" + currentIndex: model.findIndex(function (item) { + return item.value === kcm.otherToolBarStyle + }) + onActivated: kcm.otherToolBarStyle = model[currentIndex].value + } + } +} diff --git a/kcms/style/package/contents/ui/main.qml b/kcms/style/package/contents/ui/main.qml new file mode 100644 --- /dev/null +++ b/kcms/style/package/contents/ui/main.qml @@ -0,0 +1,122 @@ +/* + * 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 + + 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 * 14; + } + + // 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: Private.PreviewItem { + id: thumbnailItem + anchors.fill: parent + 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(); + } + } + } + + Loader { + id: effectSettingsPopupLoader + active: false + sourceComponent: EffectSettingsPopup { + parent: effectSettingsButton + y: -height + } + } +} diff --git a/kcms/style/package/metadata.desktop b/kcms/style/package/metadata.desktop new file mode 100644 --- /dev/null +++ b/kcms/style/package/metadata.desktop @@ -0,0 +1,16 @@ +[Desktop Entry] +Name=Application Style +Comment=Configure application style and behavior + +Icon=preferences-desktop-theme-applications +Type=Service +X-KDE-PluginInfo-Author=Kai Uwe Broulik +X-KDE-PluginInfo-Email=kde@privat.broulik.de +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-Name=kcm_style +X-KDE-PluginInfo-Version= +X-KDE-PluginInfo-Website= +X-KDE-ServiceTypes=Plasma/Generic +X-Plasma-API=declarativeappletscript + +X-Plasma-MainScript=ui/main.qml diff --git a/kcms/style/previewitem.h b/kcms/style/previewitem.h new file mode 100644 --- /dev/null +++ b/kcms/style/previewitem.h @@ -0,0 +1,76 @@ +/* + * Copyright © 2003-2007 Fredrik Höglund + * 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 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. + */ + +#pragma once + +#include +#include +#include + +#include "ui_stylepreview.h" + +class QWidget; +class QStyle; + +class PreviewItem : public QQuickPaintedItem +{ + Q_OBJECT + + Q_PROPERTY(QString styleName READ styleName WRITE setStyleName NOTIFY styleNameChanged) + + Q_PROPERTY(bool valid READ isValid NOTIFY validChanged) + +public: + explicit PreviewItem(QQuickItem *parent = nullptr); + ~PreviewItem() override; + + QString styleName() const; + void setStyleName(const QString &styleName); + Q_SIGNAL void styleNameChanged(); + + bool isValid() const; + Q_SIGNAL void validChanged(); + + Q_INVOKABLE void reload(); + + void componentComplete() override; + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + + void paint(QPainter *painter) override; + + void hoverMoveEvent(QHoverEvent *event) override; + void hoverLeaveEvent(QHoverEvent *event) override; + + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; + +private: + void sendHoverEvent(QHoverEvent *event); + void dispatchEnterLeave(QWidget *enter, QWidget *leave, const QPointF &globalPosF); + + QString m_styleName; + + Ui::StylePreview m_ui; + + QScopedPointer m_widget; + QPointer m_lastWidgetUnderMouse; + + QScopedPointer m_style; +}; diff --git a/kcms/style/previewitem.cpp b/kcms/style/previewitem.cpp new file mode 100644 --- /dev/null +++ b/kcms/style/previewitem.cpp @@ -0,0 +1,295 @@ +/* + * Copyright © 2003-2007 Fredrik Höglund + * 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 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 "previewitem.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +PreviewItem::PreviewItem(QQuickItem *parent) + : QQuickPaintedItem(parent) +{ + setAcceptHoverEvents(true); + + // HACK QtCurve deadlocks on application teardown when the Q_GLOBAL_STATIC QFactoryLoader + // in QStyleFactory is destroyed which destroys all loaded styles prompting QtCurve + // to disconnect from DBus stalling the application. + // This also happens before any of the KCM objects are destroyed, so our only chance + // is cleaning up in response to aboutToQuit + connect(qApp, &QApplication::aboutToQuit, this, [this] { + m_style.reset(); + }); +} + +PreviewItem::~PreviewItem() = default; + +void PreviewItem::componentComplete() +{ + QQuickPaintedItem::componentComplete(); + reload(); +} + +bool PreviewItem::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == m_widget.data()) { + switch (event->type()) { + case QEvent::Show: + case QEvent::UpdateRequest: + update(); + break; + default: + break; + } + } + + return QQuickPaintedItem::eventFilter(watched, event); +} + +QString PreviewItem::styleName() const +{ + return m_styleName; +} + +void PreviewItem::setStyleName(const QString &styleName) +{ + if (m_styleName == styleName) { + return; + } + + m_styleName = styleName; + reload(); + emit styleNameChanged(); +} + +bool PreviewItem::isValid() const +{ + return m_style && m_widget; +} + +void setStyleRecursively(QWidget *widget, QStyle *style) +{ + // Don't let styles kill the palette for other styles being previewed. + widget->setPalette(QPalette()); + + QPalette newPalette(KColorScheme::createApplicationPalette(KSharedConfig::openConfig())); + style->polish(newPalette); + + widget->setPalette(newPalette); + + widget->setStyle(style); + + const auto children = widget->children(); + for (QObject *child : children) { + if (child->isWidgetType()) { + setStyleRecursively(static_cast(child), style); + } + } +} + +void PreviewItem::reload() +{ + if (!isComponentComplete()) { + return; + } + + const bool oldValid = isValid(); + + m_style.reset(QStyleFactory::create(m_styleName)); + if (!m_style) { + qWarning() << "Failed to load style" << m_styleName; + if (oldValid != isValid()) { + emit validChanged(); + } + return; + } + + m_widget.reset(new QWidget); + // Don't actually show the widget as a separate window when calling show() + m_widget->setAttribute(Qt::WA_DontShowOnScreen); + // Do not wait for this widget to close before the app closes + m_widget->setAttribute(Qt::WA_QuitOnClose, false); + + m_ui.setupUi(m_widget.data()); + + // Prevent Qt from wrongly caching radio button images + QPixmapCache::clear(); + + setStyleRecursively(m_widget.data(), m_style.data()); + + m_widget->ensurePolished(); + + const auto sizeHint = m_widget->sizeHint(); + setImplicitSize(sizeHint.width(), sizeHint.height()); + + m_widget->resize(qRound(width()), qRound(height())); + + m_widget->installEventFilter(this); + + m_widget->show(); + + if (oldValid != isValid()) { + emit validChanged(); + } +} + +void PreviewItem::paint(QPainter *painter) +{ + if (m_widget && m_widget->isVisible()) { + m_widget->render(painter); + } +} + +void PreviewItem::hoverMoveEvent(QHoverEvent *event) +{ + sendHoverEvent(event); +} + +void PreviewItem::hoverLeaveEvent(QHoverEvent *event) +{ + if (m_lastWidgetUnderMouse) { + dispatchEnterLeave(nullptr, m_lastWidgetUnderMouse, mapToGlobal(event->pos())); + m_lastWidgetUnderMouse = nullptr; + } +} + +void PreviewItem::sendHoverEvent(QHoverEvent *event) +{ + if (!m_widget || !m_widget->isVisible()) { + return; + } + + QPointF pos = event->pos(); + + QWidget *child = m_widget->childAt(pos.toPoint()); + QWidget *receiver = child ? child : m_widget.data(); + + dispatchEnterLeave(receiver, m_lastWidgetUnderMouse, mapToGlobal(event->pos())); + + m_lastWidgetUnderMouse = receiver; + + pos = receiver->mapFrom(m_widget.data(), pos.toPoint()); + + QMouseEvent mouseEvent(QEvent::MouseMove, pos, receiver->mapTo(receiver->topLevelWidget(), pos.toPoint()), + receiver->mapToGlobal(pos.toPoint()), + Qt::NoButton, {} /*buttons*/, event->modifiers()); + + qApp->sendEvent(receiver, &mouseEvent); + + event->setAccepted(mouseEvent.isAccepted()); +} + +// Simplified copy of QApplicationPrivate::dispatchEnterLeave +void PreviewItem::dispatchEnterLeave(QWidget *enter, QWidget *leave, const QPointF &globalPosF) +{ + if ((!enter && !leave) || (enter == leave)) { + return; + } + + QWidgetList leaveList; + QWidgetList enterList; + + bool sameWindow = leave && enter && leave->window() == enter->window(); + if (leave && !sameWindow) { + auto *w = leave; + do { + leaveList.append(w); + } while (!w->isWindow() && (w = w->parentWidget())); + } + if (enter && !sameWindow) { + auto *w = enter; + do { + enterList.append(w); + } while (!w->isWindow() && (w = w->parentWidget())); + } + if (sameWindow) { + int enterDepth = 0; + int leaveDepth = 0; + auto *e = enter; + while (!e->isWindow() && (e = e->parentWidget())) + enterDepth++; + auto *l = leave; + while (!l->isWindow() && (l = l->parentWidget())) + leaveDepth++; + QWidget* wenter = enter; + QWidget* wleave = leave; + while (enterDepth > leaveDepth) { + wenter = wenter->parentWidget(); + enterDepth--; + } + while (leaveDepth > enterDepth) { + wleave = wleave->parentWidget(); + leaveDepth--; + } + while (!wenter->isWindow() && wenter != wleave) { + wenter = wenter->parentWidget(); + wleave = wleave->parentWidget(); + } + + for (auto *w = leave; w != wleave; w = w->parentWidget()) + leaveList.append(w); + + for (auto *w = enter; w != wenter; w = w->parentWidget()) + enterList.append(w); + } + + const QPoint globalPos = globalPosF.toPoint(); + + QEvent leaveEvent(QEvent::Leave); + for (int i = 0; i < leaveList.size(); ++i) { + auto *w = leaveList.at(i); + QApplication::sendEvent(w, &leaveEvent); + if (w->testAttribute(Qt::WA_Hover)) { + QHoverEvent he(QEvent::HoverLeave, QPoint(-1, -1), w->mapFromGlobal(globalPos), + QApplication::keyboardModifiers()); + QApplication::sendEvent(w, &he); + } + } + if (!enterList.isEmpty()) { + const QPoint windowPos = qAsConst(enterList).back()->window()->mapFromGlobal(globalPos); + for (auto it = enterList.crbegin(), end = enterList.crend(); it != end; ++it) { + auto *w = *it; + const QPointF localPos = w->mapFromGlobal(globalPos); + QEnterEvent enterEvent(localPos, windowPos, globalPosF); + QApplication::sendEvent(w, &enterEvent); + if (w->testAttribute(Qt::WA_Hover)) { + QHoverEvent he(QEvent::HoverEnter, localPos, QPoint(-1, -1), + QApplication::keyboardModifiers()); + QApplication::sendEvent(w, &he); + } + } + } +} + +void PreviewItem::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + if (!m_widget || newGeometry == oldGeometry) { + return; + } + + m_widget->resize(qRound(newGeometry.width()), qRound(newGeometry.height())); +} + diff --git a/kcms/style/styleconfig.ui b/kcms/style/styleconfig.ui deleted file mode 100644 --- a/kcms/style/styleconfig.ui +++ /dev/null @@ -1,157 +0,0 @@ - - - StyleConfig - - - - 0 - 0 - 378 - 204 - - - - - Qt::AlignHCenter|Qt::AlignTop - - - - - Application style: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - QComboBox::AdjustToContents - - - - - - - - 0 - 0 - - - - Configure Application Style... - - - - - - - .. - - - - - - - - - Show icons: - - - - - - - Main toolbar label: - - - - - - - 2 - - - - None - - - - - Text only - - - - - Beside icons - - - - - Below icons - - - - - - - - Secondary toolbar label: - - - - - - - 2 - - - - None - - - - - Text only - - - - - Beside icons - - - - - Below icons - - - - - - - - On buttons - - - true - - - - - - - In menus - - - true - - - - - - - - diff --git a/kcms/style/stylesmodel.h b/kcms/style/stylesmodel.h new file mode 100644 --- /dev/null +++ b/kcms/style/stylesmodel.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 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 . + */ + +#pragma once + +#include +#include +#include + +struct StylesModelData +{ + QString display; + QString styleName; + QString description; + QString configPage; +}; +Q_DECLARE_TYPEINFO(StylesModelData, Q_MOVABLE_TYPE); + +class StylesModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QString selectedStyle READ selectedStyle WRITE setSelectedStyle NOTIFY selectedStyleChanged) + Q_PROPERTY(int selectedStyleIndex READ selectedStyleIndex NOTIFY selectedStyleIndexChanged) + +public: + StylesModel(QObject *parent); + ~StylesModel() override; + + enum Roles { + StyleNameRole = Qt::UserRole + 1, + DescriptionRole, + ConfigurableRole + }; + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + QString selectedStyle() const; + void setSelectedStyle(const QString &style); + + int indexOfStyle(const QString &style) const; + int selectedStyleIndex() const; + + QString styleConfigPage(const QString &style) const; + + void load(); + +Q_SIGNALS: + void selectedStyleChanged(const QString &style); + void selectedStyleIndexChanged(); + +private: + QString m_selectedStyle; + + QVector m_data; + +}; diff --git a/kcms/style/stylesmodel.cpp b/kcms/style/stylesmodel.cpp new file mode 100644 --- /dev/null +++ b/kcms/style/stylesmodel.cpp @@ -0,0 +1,212 @@ +/* + * Copyright (C) 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 . + */ + +#include "stylesmodel.h" + +#include +#include +#include +#include + +#include +#include + +#include + +StylesModel::StylesModel(QObject *parent) : QAbstractListModel(parent) +{ + +} + +StylesModel::~StylesModel() = default; + +int StylesModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_data.count(); +} + +QVariant StylesModel::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index)) { + return QVariant(); + } + + const auto &item = m_data.at(index.row()); + + switch (role) { + case Qt::DisplayRole: + if (!item.display.isEmpty()) { + return item.display; + } + return item.styleName; + case StyleNameRole: return item.styleName; + case DescriptionRole: return item.description; + case ConfigurableRole: return !item.configPage.isEmpty(); + } + + return QVariant(); +} + +QHash StylesModel::roleNames() const +{ + return { + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {StyleNameRole, QByteArrayLiteral("styleName")}, + {DescriptionRole, QByteArrayLiteral("description")}, + {ConfigurableRole, QByteArrayLiteral("configurable")} + }; +} + +QString StylesModel::selectedStyle() const +{ + return m_selectedStyle; +} + +void StylesModel::setSelectedStyle(const QString &style) +{ + if (m_selectedStyle == style) { + return; + } + + const bool firstTime = m_selectedStyle.isNull(); + m_selectedStyle = style; + + if (!firstTime) { + emit selectedStyleChanged(style); + } + emit selectedStyleIndexChanged(); +} + +int StylesModel::indexOfStyle(const QString &style) const +{ + auto it = std::find_if(m_data.begin(), m_data.end(), [&style](const StylesModelData &item) { + return item.styleName == style; + }); + + if (it != m_data.end()) { + return std::distance(m_data.begin(), it); + } + + return -1; +} + +int StylesModel::selectedStyleIndex() const +{ + return indexOfStyle(m_selectedStyle); +} + +QString StylesModel::styleConfigPage(const QString &style) const +{ + const int idx = indexOfStyle(style); + if (idx == -1) { + return QString(); + } + + return m_data.at(idx).configPage; +} + +void StylesModel::load() +{ + beginResetModel(); + + const int oldCount = m_data.count(); + + m_data.clear(); + + // Combines the info we get from QStyleFactory and our themerc files + QHash styleData; + + const QStringList allStyles = QStyleFactory::keys(); + for (const QString &styleName : allStyles) { + auto &item = styleData[styleName]; + item.styleName = styleName; + } + + QStringList themeFiles; + + const QStringList themeDirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kstyle/themes"), QStandardPaths::LocateDirectory); + for (const QString &dir : themeDirs) { + const QStringList fileNames = QDir(dir).entryList(QStringList{QStringLiteral("*.themerc")}); + for (const QString &file : fileNames) { + const QString suffixedFileName = QLatin1String("kstyle/themes/") + file; + if (!themeFiles.contains(suffixedFileName)) { + themeFiles.append(suffixedFileName); + } + } + } + + std::transform(themeFiles.begin(), themeFiles.end(), themeFiles.begin(), [](const QString &item) { + return QStandardPaths::locate(QStandardPaths::GenericDataLocation, item); + }); + + for (const QString &file : themeFiles) { + KConfig config(file, KConfig::SimpleConfig); + if (!config.hasGroup("KDE") || !config.hasGroup("Misc")) { + continue; + } + + KConfigGroup kdeGroup = config.group("KDE"); + + const QString styleName = kdeGroup.readEntry("WidgetStyle", QString()); + if (styleName.isEmpty()) { + continue; + } + + auto it = styleData.find(styleName); + if (it == styleData.end()) { + continue; + } + + auto &item = *it; + + KConfigGroup desktopEntryGroup = config.group("Desktop Entry"); + if (desktopEntryGroup.readEntry("Hidden", false)) { + // Don't list hidden styles + styleData.remove(styleName); + continue; + } + + KConfigGroup miscGroup = config.group("Misc"); + + item.display = miscGroup.readEntry("Name"); + item.description = miscGroup.readEntry("Comment"); + item.configPage = miscGroup.readEntry("ConfigPage"); + } + + m_data = styleData.values().toVector(); + + QCollator collator; + std::sort(m_data.begin(), m_data.end(), [&collator](const StylesModelData &a, const StylesModelData &b) { + const QString aDisplay = !a.display.isEmpty() ? a.display : a.styleName; + const QString bDisplay = !b.display.isEmpty() ? b.display : b.styleName; + return collator.compare(aDisplay, bDisplay) < 0; + }); + + endResetModel(); + + // an item might have been added before the currently selected one + if (oldCount != m_data.count()) { + emit selectedStyleIndexChanged(); + } +}