From 3abd4b7d5ab303f8d990c104748e5d5aef4db355 Mon Sep 17 00:00:00 2001 From: Manuel Nickschas Date: Tue, 5 Jun 2018 00:07:01 +0200 Subject: [PATCH] qtui: Fix icon loading and improve icon theme support Quassel relies on quite a number of icons that are part of KDE/Plasma icon themes such as Breeze and Oxygen, but are not specified by freedesktop.org's Icon Naming Specification. As such, icon themes other than the afforementioned most likely don't contain all the icons Quassel needs. Additionally, Quassel ships several additional icons that are not part of any theme, but are made to fit in with Breeze/Oxygen. In order to ensure that all required icons are available, we now assume that at least one of Breeze, Breeze Dark or Oxygen themes is available as a fallback, either as a system-wide installation, or by having Quassel itself deploy the required subset in its data dir (related build options will be adapted in a follow-up commit). Allow the user to configure which fallback theme is to be used, and if the system icon theme should be completely overridden (as opposed to just filling in missing icons). If the system theme is Breeze/Oxygen, just inject the corresponding Quassel-specific icons. If the configured system theme doesn't match the selected fallback (and should not be overridden), create a proxy icon theme that inherits from first the system theme and then the selected fallback. That way, missing icons are found through the inheritance chain. Note that as of Qt 5.11.0, icons from the hicolor default theme are preferred over the ones from inherited themes, which violates the Icon Theme Specification (cf. QTBUG-68603). This affects Quassel's application icon, which will thus always use the Breeze variant if a non-supported system icon theme is used. Other icons will be picked from the selected fallback theme, as expected. --- src/qtui/qtui.cpp | 159 +++++++++++- src/qtui/qtui.h | 43 +++- src/qtui/qtuiapplication.cpp | 27 +- .../settingspages/appearancesettingspage.cpp | 73 +++--- .../settingspages/appearancesettingspage.ui | 243 ++++++++++-------- 5 files changed, 362 insertions(+), 183 deletions(-) diff --git a/src/qtui/qtui.cpp b/src/qtui/qtui.cpp index 3f04f8df..0082bd06 100644 --- a/src/qtui/qtui.cpp +++ b/src/qtui/qtui.cpp @@ -20,6 +20,12 @@ #include "qtui.h" +#include +#include +#include +#include +#include + #include "abstractnotificationbackend.h" #include "buffermodel.h" #include "chatlinemodel.h" @@ -33,22 +39,33 @@ #include "types.h" #include "util.h" -QtUi *QtUi::_instance = 0; -MainWin *QtUi::_mainWin = 0; +QtUi *QtUi::_instance = nullptr; +MainWin *QtUi::_mainWin = nullptr; QList QtUi::_notificationBackends; QList QtUi::_notifications; -QtUi::QtUi() : GraphicalUi() +QtUi::QtUi() + : GraphicalUi() + , _systemIconTheme{QIcon::themeName()} { - if (_instance != 0) { + if (_instance != nullptr) { qWarning() << "QtUi has been instantiated again!"; return; } _instance = this; + if (Quassel::isOptionSet("icontheme")) { + _systemIconTheme = Quassel::optionValue("icontheme"); + QIcon::setThemeName(_systemIconTheme); + } + QtUiSettings uiSettings; Quassel::loadTranslation(uiSettings.value("Locale", QLocale::system()).value()); + setupIconTheme(); + + QApplication::setWindowIcon(QIcon::fromTheme("quassel")); + setContextMenuActionProvider(new ContextMenuActionProvider(this)); setToolBarActionProvider(new ToolBarActionProvider(this)); @@ -67,8 +84,8 @@ QtUi::~QtUi() { unregisterAllNotificationBackends(); delete _mainWin; - _mainWin = 0; - _instance = 0; + _mainWin = nullptr; + _instance = nullptr; } @@ -248,3 +265,133 @@ void QtUi::bufferMarkedAsRead(BufferId bufferId) closeNotifications(bufferId); } } + + +std::vector> QtUi::availableIconThemes() const +{ + //: Supported icon theme names + static const std::vector> supported { + { "breeze", tr("Breeze") }, + { "breeze-dark", tr("Breeze Dark") }, + { "oxygen", tr("Oxygen") } + }; + + std::vector> result; + for (auto &&themePair : supported) { + for (auto &&dir : QIcon::themeSearchPaths()) { + if (QFileInfo{dir + "/" + themePair.first + "/index.theme"}.exists()) { + result.push_back(themePair); + break; + } + } + } + + return result; +} + + +void QtUi::setupIconTheme() +{ + // Add paths to our own icon sets to the theme search paths + QStringList themePaths = QIcon::themeSearchPaths(); + themePaths.removeAll(":/icons"); // this should come last + for (auto &&dataDir : Quassel::dataDirPaths()) { + QString iconDir{dataDir + "icons"}; + if (QFileInfo{iconDir}.isDir()) { + themePaths << iconDir; + } + } + themePaths << ":/icons"; + QIcon::setThemeSearchPaths(themePaths); + + refreshIconTheme(); +} + + +void QtUi::refreshIconTheme() +{ + // List of available fallback themes + QStringList availableThemes; + for (auto &&themePair : availableIconThemes()) { + availableThemes << themePair.first; + } + + if (availableThemes.isEmpty()) { + // We could probably introduce a more sophisticated fallback handling, such as putting the "most important" icons into hicolor, + // but this just gets complex for no good reason. We really rely on a supported theme to be installed, if not system-wide, then + // as part of the Quassel installation (which is enabled by default anyway). + qWarning() << tr("No supported icon theme installed, you'll lack icons! Supported are the KDE/Plasma themes Breeze, Breeze Dark and Oxygen."); + return; + } + + UiStyleSettings s; + QString fallbackTheme{s.value("Icons/FallbackTheme").toString()}; + + if (fallbackTheme.isEmpty() || !availableThemes.contains(fallbackTheme)) { + if (availableThemes.contains(_systemIconTheme)) { + fallbackTheme = _systemIconTheme; + } + else { + fallbackTheme = availableThemes.first(); + } + } + + if (_systemIconTheme.isEmpty() || _systemIconTheme == fallbackTheme || s.value("Icons/OverrideSystemTheme", false).toBool()) { + // We have a valid fallback theme and want to override the system theme (if it's even defined), so we're basically done + QIcon::setThemeName(fallbackTheme); + return; + } + +#if QT_VERSION >= 0x050000 + // At this point, we have a system theme that we don't want to override, but that may not contain all + // required icons. + // We create a dummy theme that inherits first from the system theme, then from the supported fallback. + // This rather ugly hack allows us to inject the fallback into the inheritance chain, so non-standard + // icons missing in the system theme will be filled in by the fallback. + // Since we can't get notified when the system theme changes, this means that a restart may be required + // to apply a theme change... but you can't have everything, I guess. + if (!_dummyThemeDir) { + _dummyThemeDir.reset(new QTemporaryDir{}); + if (!_dummyThemeDir->isValid() || !QDir{_dummyThemeDir->path()}.mkpath("icons/quassel-icon-proxy/apps/32")) { + qWarning() << "Could not create temporary directory for proxying the system icon theme, using fallback"; + QIcon::setThemeName(fallbackTheme); + return; + } + // Add this to XDG_DATA_DIRS, otherwise KIconLoader complains + auto xdgDataDirs = qgetenv("XDG_DATA_DIRS"); + if (!xdgDataDirs.isEmpty()) + xdgDataDirs += ":"; + xdgDataDirs += _dummyThemeDir->path(); + qputenv("XDG_DATA_DIRS", xdgDataDirs); + + QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() << _dummyThemeDir->path() + "/icons"); + } + + QFile indexFile{_dummyThemeDir->path() + "/icons/quassel-icon-proxy/index.theme"}; + if (!indexFile.open(QFile::WriteOnly|QFile::Truncate)) { + qWarning() << "Could not create index file for proxying the system icon theme, using fallback"; + QIcon::setThemeName(fallbackTheme); + return; + } + + // Write a dummy index file that is sufficient to make QIconLoader happy + auto indexContents = QString{ + "[Icon Theme]\n" + "Name=quassel-icon-proxy\n" + "Inherits=%1,%2\n" + "Directories=apps/32\n" + "[apps/32]\nSize=32\nType=Fixed\n" + }.arg(_systemIconTheme, fallbackTheme); + if (indexFile.write(indexContents.toLatin1()) < 0) { + qWarning() << "Could not write index file for proxying the system icon theme, using fallback"; + QIcon::setThemeName(fallbackTheme); + return; + } + indexFile.close(); + QIcon::setThemeName("quassel-icon-proxy"); +#else + // Qt4 doesn't support QTemporaryDir. Since it's deprecated and slated to be removed soon anyway, we don't bother + // writing a replacement and simply don't support not overriding the system theme. + QIcon::setThemeName(fallbackTheme); +#endif +} diff --git a/src/qtui/qtui.h b/src/qtui/qtui.h index 96e934f1..0d13e648 100644 --- a/src/qtui/qtui.h +++ b/src/qtui/qtui.h @@ -18,12 +18,20 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ -#ifndef QTUI_H -#define QTUI_H +#pragma once -#include "graphicalui.h" +#include +#include +#include + +#include + +#if QT_VERSION >= 0x050000 +# include +#endif #include "abstractnotificationbackend.h" +#include "graphicalui.h" #include "qtuistyle.h" class MainWin; @@ -59,6 +67,13 @@ public: static const QList ¬ificationBackends(); static const QList &activeNotifications(); + /** + * Determine available fallback icon themes. + * + * @returns The list of supported fallback themes (Breeze (Dark), Oxygen) that are available on the system + */ + std::vector> availableIconThemes() const; + public slots: void init() override; @@ -66,6 +81,14 @@ public slots: void closeNotification(uint notificationId); void closeNotifications(BufferId bufferId = BufferId()); + /** + * Refresh the current icon theme. + * + * @note This will not detect changes in the system icon theme, so if that changes, a client restart + * is required for icons to work correctly. + */ + void refreshIconTheme(); + protected slots: void connectedToCore() override; void disconnectedFromCore() override; @@ -79,12 +102,24 @@ protected: private slots: void useSystemTrayChanged(const QVariant &); +private: + /** + * Sets up icon theme handling. + */ + void setupIconTheme(); + private: static QtUi *_instance; static MainWin *_mainWin; static QList _notificationBackends; static QList _notifications; + QString _systemIconTheme; + +#if QT_VERSION >= 0x050000 + std::unique_ptr _dummyThemeDir; +#endif + bool _useSystemTray; }; @@ -92,5 +127,3 @@ private: QtUi *QtUi::instance() { return _instance ? _instance : new QtUi(); } QtUiStyle *QtUi::style() { return qobject_cast(uiStyle()); } MainWin *QtUi::mainWindow() { return _mainWin; } - -#endif diff --git a/src/qtui/qtuiapplication.cpp b/src/qtui/qtuiapplication.cpp index 73e67b29..6c744c27 100644 --- a/src/qtui/qtuiapplication.cpp +++ b/src/qtui/qtuiapplication.cpp @@ -20,8 +20,8 @@ #include "qtuiapplication.h" -#include #include +#include #include #ifdef HAVE_KDE4 @@ -109,30 +109,6 @@ bool QtUiApplication::init() return false; } - // Checking if settings Icon Theme is valid - QString savedIcontheme = QtUiSettings().value("IconTheme", QVariant("")).toString(); -#ifndef WITH_OXYGEN - if (savedIcontheme == "oxygen") - QtUiSettings().remove("IconTheme"); -#endif -#ifndef WITH_BREEZE - if (savedIcontheme == "breeze") - QtUiSettings().remove("IconTheme"); -#endif -#ifndef WITH_BREEZE_DARK - if (savedIcontheme == "breezedark") - QtUiSettings().remove("IconTheme"); -#endif - - // Set the icon theme - if (Quassel::isOptionSet("icontheme")) - QIcon::setThemeName(Quassel::optionValue("icontheme")); - else if (QtUiSettings().value("IconTheme", QVariant("")).toString() != "") - QIcon::setThemeName(QtUiSettings().value("IconTheme").toString()); - else if (QIcon::themeName().isEmpty()) - // Some platforms don't set a default icon theme; chances are we can find our bundled theme though - QIcon::setThemeName("breeze"); - Client::init(new QtUi()); // Init UI only after the event loop has started @@ -143,7 +119,6 @@ bool QtUiApplication::init() QtUi::mainWindow()->quit(); }); - return true; } return false; diff --git a/src/qtui/settingspages/appearancesettingspage.cpp b/src/qtui/settingspages/appearancesettingspage.cpp index 44c53fd7..31564334 100644 --- a/src/qtui/settingspages/appearancesettingspage.cpp +++ b/src/qtui/settingspages/appearancesettingspage.cpp @@ -20,16 +20,17 @@ #include "appearancesettingspage.h" +#include +#include +#include +#include +#include + #include "buffersettings.h" #include "qtui.h" #include "qtuisettings.h" #include "qtuistyle.h" -#include -#include -#include -#include -#include AppearanceSettingsPage::AppearanceSettingsPage(QWidget *parent) : SettingsPage(tr("Interface"), QString(), parent) @@ -39,6 +40,10 @@ AppearanceSettingsPage::AppearanceSettingsPage(QWidget *parent) #ifdef QT_NO_SYSTEMTRAYICON ui.useSystemTrayIcon->hide(); #endif +#if QT_VERSION < 0x050000 + // We don't support overriding the system icon theme with Qt4 + ui.overrideSystemIconTheme->hide(); +#endif initAutoWidgets(); initStyleComboBox(); @@ -98,21 +103,12 @@ void AppearanceSettingsPage::initLanguageComboBox() void AppearanceSettingsPage::initIconThemeComboBox() { - // TODO Replace by runtime detection -#if defined WITH_OXYGEN || defined WITH_BREEZE || defined WITH_BREEZE_DARK -# if defined WITH_BREEZE - ui.iconthemeComboBox->addItem(tr("Breeze Light"), QVariant("breeze")); -# endif -# if defined WITH_BREEZE_DARK - ui.iconthemeComboBox->addItem(tr("Breeze Dark"), QVariant("breezedark")); -# endif -# if defined WITH_OXYGEN - ui.iconthemeComboBox->addItem(tr("Oxygen"), QVariant("oxygen")); -# endif -#else - ui.iconthemeLabel->hide(); - ui.iconthemeComboBox->hide(); -#endif + auto availableThemes = QtUi::instance()->availableIconThemes(); + + ui.iconThemeComboBox->addItem(tr("Automatic"), QString{}); + for (auto &&p : QtUi::instance()->availableIconThemes()) { + ui.iconThemeComboBox->addItem(p.second, p.first); + } } @@ -152,12 +148,15 @@ void AppearanceSettingsPage::load() Quassel::loadTranslation(selectedLocale()); // IconTheme - QString icontheme = uiSettings.value("IconTheme", QVariant("")).toString(); - if (icontheme == "") - ui.iconthemeComboBox->setCurrentIndex(0); - else - ui.iconthemeComboBox->setCurrentIndex(ui.iconthemeComboBox->findData(icontheme)); - ui.iconthemeComboBox->setProperty("storedValue", ui.iconthemeComboBox->currentIndex()); + QString icontheme = UiStyleSettings{}.value("Icons/FallbackTheme", QString{}).toString(); + if (icontheme.isEmpty()) { + ui.iconThemeComboBox->setCurrentIndex(0); + } + else { + auto idx = ui.iconThemeComboBox->findData(icontheme); + ui.iconThemeComboBox->setCurrentIndex(idx > 0 ? idx : 0); + } + ui.iconThemeComboBox->setProperty("storedValue", ui.iconThemeComboBox->currentIndex()); // bufferSettings: BufferSettings bufferSettings; @@ -184,6 +183,7 @@ void AppearanceSettingsPage::load() void AppearanceSettingsPage::save() { QtUiSettings uiSettings; + UiStyleSettings styleSettings; if (ui.styleComboBox->currentIndex() < 1) { uiSettings.setValue("Style", QString("")); @@ -202,14 +202,17 @@ void AppearanceSettingsPage::save() } ui.languageComboBox->setProperty("storedValue", ui.languageComboBox->currentIndex()); - if (selectedIconTheme()=="") { - uiSettings.remove("IconTheme"); + bool needsIconThemeRefresh = ui.iconThemeComboBox->currentIndex() != ui.iconThemeComboBox->property("storedValue").toInt() + || ui.overrideSystemIconTheme->isChecked() != ui.overrideSystemIconTheme->property("storedValue").toBool(); + + auto iconTheme = selectedIconTheme(); + if (iconTheme.isEmpty()) { + styleSettings.remove("Icons/FallbackTheme"); } else { - uiSettings.setValue("IconTheme", selectedIconTheme()); - QIcon::setThemeName(selectedIconTheme()); + styleSettings.setValue("Icons/FallbackTheme", iconTheme); } - ui.iconthemeComboBox->setProperty("storedValue", ui.iconthemeComboBox->currentIndex()); + ui.iconThemeComboBox->setProperty("storedValue", ui.iconThemeComboBox->currentIndex()); bool needsStyleReload = ui.useCustomStyleSheet->isChecked() != ui.useCustomStyleSheet->property("storedValue").toBool() @@ -247,6 +250,8 @@ void AppearanceSettingsPage::save() setChangedState(false); if (needsStyleReload) QtUi::style()->reload(); + if (needsIconThemeRefresh) + QtUi::instance()->refreshIconTheme(); } @@ -264,11 +269,13 @@ QLocale AppearanceSettingsPage::selectedLocale() const return locale; } + QString AppearanceSettingsPage::selectedIconTheme() const { - return ui.iconthemeComboBox->itemData(ui.iconthemeComboBox->currentIndex()).toString(); + return ui.iconThemeComboBox->itemData(ui.iconThemeComboBox->currentIndex()).toString(); } + void AppearanceSettingsPage::chooseStyleSheet() { QString dir = ui.customStyleSheetPath->property("storedValue").toString(); @@ -293,7 +300,7 @@ bool AppearanceSettingsPage::testHasChanged() { if (ui.styleComboBox->currentIndex() != ui.styleComboBox->property("storedValue").toInt()) return true; if (ui.languageComboBox->currentIndex() != ui.languageComboBox->property("storedValue").toInt()) return true; - if (ui.iconthemeComboBox->currentIndex() != ui.iconthemeComboBox->property("storedValue").toInt()) return true; + if (ui.iconThemeComboBox->currentIndex() != ui.iconThemeComboBox->property("storedValue").toInt()) return true; if (SettingsPage::hasChanged(ui.userNoticesInStatusBuffer)) return true; if (SettingsPage::hasChanged(ui.userNoticesInDefaultBuffer)) return true; diff --git a/src/qtui/settingspages/appearancesettingspage.ui b/src/qtui/settingspages/appearancesettingspage.ui index abf67d5a..7b6af81f 100644 --- a/src/qtui/settingspages/appearancesettingspage.ui +++ b/src/qtui/settingspages/appearancesettingspage.ui @@ -6,8 +6,8 @@ 0 0 - 549 - 470 + 755 + 536 @@ -17,27 +17,13 @@ - - - Client style: - - - - - - - Set application style - - - - Language: - + Set the application language. Requires restart! @@ -54,7 +40,7 @@ - + Qt::Horizontal @@ -67,66 +53,68 @@ + + + + Widget style: + + + + + + + Set application style + + + - Icon theme: + Fallback icon theme: - + - Choose from the bundled icon themes! May need restart... + Icon theme to use for icons that are not found in the current system theme. Requires the selected theme to be installed either system-wide, or as part of the Quassel installation. Supported themes are Breeze, Breeze Dark and Oxygen, all of KDE fame. + + + + + + + <html><head/><body><p>If enabled, uses the selected fallback icon theme instead of the configured system theme for all icons. Recommended if you want Quassel to have a consistent look-and-feel.</p></body></html> + + + Override system theme + + + false + + + /UiStyle/Icons/OverrideSystemTheme + + + false - - - <System Default> - - - - - Use custom stylesheet - - - /UiStyle/UseCustomStyleSheet - - - false - - - - - + - - - Qt::Horizontal - - - QSizePolicy::Fixed + + + Use custom stylesheet - - - 20 - 20 - + + /UiStyle/UseCustomStyleSheet - - - - - + false - - Path: - @@ -163,26 +151,36 @@ - - - - Show system tray icon - - - true - - - UseSystemTrayIcon - - - true - - - - - + + + + Invert colors + + + /UiStyle/Icons/InvertTray + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + Qt::Horizontal @@ -197,7 +195,7 @@ - + Hide to tray on close button @@ -210,6 +208,38 @@ + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Show system tray icon + + + true + + + UseSystemTrayIcon + + + true + + + @@ -379,13 +409,16 @@ - styleComboBox languageComboBox + styleComboBox + iconThemeComboBox + overrideSystemIconTheme useCustomStyleSheet - customStyleSheetPath chooseStyleSheet + customStyleSheetPath useSystemTrayIcon minimizeOnClose + invertSystrayColors userNoticesInDefaultBuffer userNoticesInStatusBuffer userNoticesInCurrentBuffer @@ -405,28 +438,12 @@ setEnabled(bool) - 63 - 86 + 70 + 166 - 86 - 114 - - - - - useCustomStyleSheet - toggled(bool) - label - setEnabled(bool) - - - 45 - 80 - - - 38 - 113 + 321 + 171 @@ -437,12 +454,12 @@ setEnabled(bool) - 84 - 80 + 91 + 166 - 525 - 117 + 747 + 172 @@ -453,28 +470,28 @@ setEnabled(bool) - 91 - 143 + 98 + 206 - 92 - 174 + 125 + 238 useSystemTrayIcon toggled(bool) - animateSystrayIcon + invertSystrayColors setEnabled(bool) - 125 - 144 + 45 + 197 - 122 - 203 + 70 + 291 -- 2.20.1