From dad4ad68f36b558f4446f9bd39a572ef63da0b9e Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sat, 19 Dec 2020 00:20:24 +0100 Subject: [PATCH 1/5] Improve macOS platform integration (backport). - Allow switching between themes without restart (except classic) - Rework icon loading and recolouring logic to react to theme changes - Automatically react to light/dark theme change - Remove explicit selection of monochrome tray icon variant (selected automatically now) The tray icon doesn't respond perfectly to theme changes yet on Big Sur, since we need different icons for dark and light theme and cannot simply let the OS recolour the icon for us (we do that, too, but only as an additional fallback). At the moment, there is no signal to listen to that would allow this. This patch adds a few generic methods to OSUtils for detecting and communicating theme changes, which are only stubs for Windows and Linux at the moment and need to be implemented in future commits. Fixes #5349 --- .gitattributes | 3 + src/core/Resources.cpp | 162 ++++++++++++++++--------- src/core/Resources.h | 2 +- src/gui/Application.cpp | 21 ++-- src/gui/ApplicationSettingsWidget.cpp | 4 + src/gui/MainWindow.cpp | 12 +- src/gui/osutils/OSUtilsBase.h | 16 +++ src/gui/osutils/macutils/AppKit.h | 4 +- src/gui/osutils/macutils/AppKitImpl.h | 1 + src/gui/osutils/macutils/AppKitImpl.mm | 61 +++++++++- src/gui/osutils/macutils/MacUtils.cpp | 14 +++ src/gui/osutils/macutils/MacUtils.h | 2 + src/gui/osutils/nixutils/NixUtils.cpp | 6 + src/gui/osutils/nixutils/NixUtils.h | 1 + src/gui/osutils/winutils/WinUtils.cpp | 6 + src/gui/osutils/winutils/WinUtils.h | 1 + src/gui/styles/base/BaseStyle.cpp | 33 ++--- src/gui/styles/base/BaseStyle.h | 1 - src/gui/styles/dark/DarkStyle.cpp | 6 +- src/gui/styles/light/LightStyle.cpp | 6 +- 20 files changed, 265 insertions(+), 97 deletions(-) diff --git a/.gitattributes b/.gitattributes index 9df1af791..afc11a7bb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,6 @@ AppImage-Recipe.sh export-ignore # github-linguist language hints *.h linguist-language=C++ *.cpp linguist-language=C++ + +# binary files +*.ai binary diff --git a/src/core/Resources.cpp b/src/core/Resources.cpp index ae8c0d46a..26c63ab1c 100644 --- a/src/core/Resources.cpp +++ b/src/core/Resources.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -30,6 +31,18 @@ #include "gui/MainWindow.h" #include "gui/osutils/OSUtils.h" +class AdaptiveIconEngine : public QIconEngine +{ +public: + explicit AdaptiveIconEngine(QIcon baseIcon); + void paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) override; + QPixmap pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state) override; + QIconEngine* clone() const override; + +private: + QIcon m_baseIcon; +}; + Resources* Resources::m_instance(nullptr); QString Resources::dataPath(const QString& name) const @@ -114,45 +127,103 @@ QString Resources::trayIconAppearance() const return iconAppearance; } -QIcon Resources::trayIcon() +QIcon Resources::trayIcon(QString style) { - return trayIconUnlocked(); + if (style == "unlocked") { + style.clear(); + } + if (!style.isEmpty()) { + style = "-" + style; + } + + auto iconApperance = trayIconAppearance(); + if (!iconApperance.startsWith("monochrome")) { + return icon(QString("keepassxc%1").arg(style), false); + } + + QIcon i; +#ifdef Q_OS_MACOS + if (osUtils->isStatusBarDark()) { + i = icon(QString("keepassxc-monochrome-light%1").arg(style), false); + } else { + i = icon(QString("keepassxc-monochrome-dark%1").arg(style), false); + } +#else + i = icon(QString("keepassxc-%1%2").arg(iconApperance, style), false); +#endif +#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) + // Set as mask to allow the operating system to recolour the tray icon. This may look weird + // if we failed to detect the status bar background colour correctly, but it is certainly + // better than a barely visible icon and even if we did guess correctly, it allows for better + // integration should the system's preferred colours not be 100% black or white. + i.setIsMask(true); +#endif + return i; } QIcon Resources::trayIconLocked() { - auto iconApperance = trayIconAppearance(); - - if (iconApperance == "monochrome-light") { - return icon("keepassxc-monochrome-light-locked", false); - } - if (iconApperance == "monochrome-dark") { - return icon("keepassxc-monochrome-dark-locked", false); - } - return icon("keepassxc-locked", false); + return trayIcon("locked"); } QIcon Resources::trayIconUnlocked() { - auto iconApperance = trayIconAppearance(); + return trayIcon("unlocked"); +} - if (iconApperance == "monochrome-light") { - return icon("keepassxc-monochrome-light", false); + +AdaptiveIconEngine::AdaptiveIconEngine(QIcon baseIcon) + : QIconEngine() + , m_baseIcon(std::move(baseIcon)) +{ +} + +void AdaptiveIconEngine::paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) +{ +#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) + double dpr = !kpxcApp->testAttribute(Qt::AA_UseHighDpiPixmaps) ? 1.0 : painter->device()->devicePixelRatioF(); +#else + double dpr = !kpxcApp->testAttribute(Qt::AA_UseHighDpiPixmaps) ? 1.0 : painter->device()->devicePixelRatio(); +#endif + QSize pixmapSize = rect.size() * dpr; + + painter->save(); + painter->drawPixmap(rect, m_baseIcon.pixmap(pixmapSize, mode, state)); + + if (getMainWindow()) { + QPalette palette = getMainWindow()->palette(); + painter->setCompositionMode(QPainter::CompositionMode_SourceAtop); + + if (mode == QIcon::Active) { + painter->fillRect(rect, palette.color(QPalette::Active, QPalette::ButtonText)); + } else if (mode == QIcon::Selected) { + painter->fillRect(rect, palette.color(QPalette::Active, QPalette::HighlightedText)); + } else if (mode == QIcon::Disabled) { + painter->fillRect(rect, palette.color(QPalette::Disabled, QPalette::WindowText)); + } else { + painter->fillRect(rect, palette.color(QPalette::Normal, QPalette::WindowText)); + } } - if (iconApperance == "monochrome-dark") { - return icon("keepassxc-monochrome-dark", false); - } - return icon("keepassxc", false); + painter->restore(); +} + +QPixmap AdaptiveIconEngine::pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state) +{ + QImage img(size, QImage::Format_ARGB32_Premultiplied); + img.fill(0); + QPainter painter(&img); + paint(&painter, QRect(0, 0, size.width(), size.height()), mode, state); + return QPixmap::fromImage(img, Qt::NoFormatConversion); +} + +QIconEngine* AdaptiveIconEngine::clone() const +{ + return new AdaptiveIconEngine(m_baseIcon); } QIcon Resources::icon(const QString& name, bool recolor, const QColor& overrideColor) { - QIcon icon = m_iconCache.value(name); - - if (!icon.isNull() && !overrideColor.isValid()) { - return icon; - } - +#ifdef Q_OS_LINUX // Resetting the application theme name before calling QIcon::fromTheme() is required for hacky // QPA platform themes such as qt5ct, which randomly mess with the configured icon theme. // If we do not reset the theme name here, it will become empty at some point, causing @@ -161,44 +232,25 @@ QIcon Resources::icon(const QString& name, bool recolor, const QColor& overrideC // See issue #4963: https://github.com/keepassxreboot/keepassxc/issues/4963 // and qt5ct issue #80: https://sourceforge.net/p/qt5ct/tickets/80/ QIcon::setThemeName("application"); +#endif + + QString cacheName = + QString("%1:%2:%3").arg(recolor ? "1" : "0", overrideColor.isValid() ? overrideColor.name() : "#", name); + QIcon icon = m_iconCache.value(cacheName); + + if (!icon.isNull() && !overrideColor.isValid()) { + return icon; + } icon = QIcon::fromTheme(name); - if (getMainWindow() && recolor) { - const QRect rect(0, 0, 48, 48); - QImage img = icon.pixmap(rect.width(), rect.height()).toImage(); - img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied); - icon = {}; - - QPainter painter(&img); - painter.setCompositionMode(QPainter::CompositionMode_SourceAtop); - - if (!overrideColor.isValid()) { - QPalette palette = getMainWindow()->palette(); - painter.fillRect(rect, palette.color(QPalette::Normal, QPalette::WindowText)); - icon.addPixmap(QPixmap::fromImage(img), QIcon::Normal); - - painter.fillRect(rect, palette.color(QPalette::Active, QPalette::ButtonText)); - icon.addPixmap(QPixmap::fromImage(img), QIcon::Active); - - painter.fillRect(rect, palette.color(QPalette::Active, QPalette::HighlightedText)); - icon.addPixmap(QPixmap::fromImage(img), QIcon::Selected); - - painter.fillRect(rect, palette.color(QPalette::Disabled, QPalette::WindowText)); - icon.addPixmap(QPixmap::fromImage(img), QIcon::Disabled); - } else { - painter.fillRect(rect, overrideColor); - icon.addPixmap(QPixmap::fromImage(img), QIcon::Normal); - } - + if (recolor) { + icon = QIcon(new AdaptiveIconEngine(icon)); #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) icon.setIsMask(true); #endif } - if (!overrideColor.isValid()) { - m_iconCache.insert(name, icon); - } - + m_iconCache.insert(cacheName, icon); return icon; } diff --git a/src/core/Resources.h b/src/core/Resources.h index b76818150..6cadb5996 100644 --- a/src/core/Resources.h +++ b/src/core/Resources.h @@ -31,7 +31,7 @@ public: QString pluginPath(const QString& name) const; QString wordlistPath(const QString& name) const; QIcon applicationIcon(); - QIcon trayIcon(); + QIcon trayIcon(QString style = "unlocked"); QIcon trayIconLocked(); QIcon trayIconUnlocked(); QString trayIconAppearance() const; diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index 0c389706f..784528294 100644 --- a/src/gui/Application.cpp +++ b/src/gui/Application.cpp @@ -22,6 +22,7 @@ #include "autotype/AutoType.h" #include "core/Config.h" #include "core/Global.h" +#include "core/Resources.h" #include "gui/MainWindow.h" #include "gui/osutils/OSUtils.h" #include "gui/styles/dark/DarkStyle.h" @@ -131,6 +132,12 @@ Application::Application(int& argc, char** argv) qWarning() << QObject::tr("The lock file could not be created. Single-instance mode disabled.").toUtf8().constData(); } + + connect(osUtils, &OSUtilsBase::interfaceThemeChanged, this, [this]() { + if (config()->get(Config::GUI_ApplicationTheme).toString() != "classic") { + applyTheme(); + } + }); } Application::~Application() @@ -153,15 +160,15 @@ void Application::applyTheme() } #endif } - + QPixmapCache::clear(); if (appTheme == "light") { - setStyle(new LightStyle); - // Workaround Qt 5.15+ bug - setPalette(style()->standardPalette()); + auto* s = new LightStyle; + setPalette(s->standardPalette()); + setStyle(s); } else if (appTheme == "dark") { - setStyle(new DarkStyle); - // Workaround Qt 5.15+ bug - setPalette(style()->standardPalette()); + auto* s = new DarkStyle; + setPalette(s->standardPalette()); + setStyle(s); m_darkTheme = true; } else { // Classic mode, don't check for dark theme on Windows diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index cf09df39b..0d9829229 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -250,8 +250,12 @@ void ApplicationSettingsWidget::loadSettings() } m_generalUi->trayIconAppearance->clear(); +#ifdef Q_OS_MACOS + m_generalUi->trayIconAppearance->addItem(tr("Monochrome"), "monochrome"); +#else m_generalUi->trayIconAppearance->addItem(tr("Monochrome (light)"), "monochrome-light"); m_generalUi->trayIconAppearance->addItem(tr("Monochrome (dark)"), "monochrome-dark"); +#endif m_generalUi->trayIconAppearance->addItem(tr("Colorful"), "colorful"); int trayIconIndex = m_generalUi->trayIconAppearance->findData(resources()->trayIconAppearance()); if (trayIconIndex > 0) { diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 7f0f3bf6f..c08f6e677 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -39,6 +39,7 @@ #include "gui/DatabaseWidget.h" #include "gui/MessageBox.h" #include "gui/SearchWidget.h" +#include "gui/osutils/OSUtils.h" #include "keys/CompositeKey.h" #include "keys/FileKey.h" #include "keys/PasswordKey.h" @@ -496,6 +497,8 @@ MainWindow::MainWindow() connect(m_ui->actionOnlineHelp, SIGNAL(triggered()), SLOT(openOnlineHelp())); connect(m_ui->actionKeyboardShortcuts, SIGNAL(triggered()), SLOT(openKeyboardShortcuts())); + connect(osUtils, &OSUtilsBase::statusbarThemeChanged, this, &MainWindow::updateTrayIcon); + #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // Install event filter for empty-area drag auto* eventFilter = new MainWindowEventFilter(this); @@ -599,6 +602,11 @@ MainWindow::MainWindow() config()->set(Config::Messages_Qt55CompatibilityWarning, true); } #endif + + QObject::connect(qApp, SIGNAL(anotherInstanceStarted()), this, SLOT(bringToFront())); + QObject::connect(qApp, SIGNAL(applicationActivated()), this, SLOT(bringToFront())); + QObject::connect(qApp, SIGNAL(openFile(QString)), this, SLOT(openDatabase(QString))); + QObject::connect(qApp, SIGNAL(quitSignalReceived()), this, SLOT(appExit()), Qt::DirectConnection); } MainWindow::~MainWindow() @@ -1737,8 +1745,10 @@ void MainWindow::initViewMenu() connect(themeActions, &QActionGroup::triggered, this, [this, theme](QAction* action) { config()->set(Config::GUI_ApplicationTheme, action->data()); - if (action->data() != theme) { + if ((action->data() == "classic" || theme == "classic") && action->data() != theme) { restartApp(tr("You must restart the application to apply this setting. Would you like to restart now?")); + } else { + kpxcApp->applyTheme(); } }); diff --git a/src/gui/osutils/OSUtilsBase.h b/src/gui/osutils/OSUtilsBase.h index 340e9bf7e..0389b667c 100644 --- a/src/gui/osutils/OSUtilsBase.h +++ b/src/gui/osutils/OSUtilsBase.h @@ -35,6 +35,11 @@ public: */ virtual bool isDarkMode() const = 0; + /** + * @return OS task / menu bar is dark. + */ + virtual bool isStatusBarDark() const = 0; + /** * @return KeePassXC set to launch at system startup (autostart). */ @@ -50,6 +55,17 @@ public: */ virtual bool isCapslockEnabled() = 0; +signals: + /** + * Indicates platform UI theme change (light mode to dark mode). + */ + void interfaceThemeChanged(); + + /* + * Indicates a change in the tray / statusbar theme. + */ + void statusbarThemeChanged(); + protected: explicit OSUtilsBase(QObject* parent = nullptr); virtual ~OSUtilsBase(); diff --git a/src/gui/osutils/macutils/AppKit.h b/src/gui/osutils/macutils/AppKit.h index 02121683d..9ce4c01db 100644 --- a/src/gui/osutils/macutils/AppKit.h +++ b/src/gui/osutils/macutils/AppKit.h @@ -20,6 +20,7 @@ #define KEEPASSX_APPKIT_H #include +#include #include class AppKit : public QObject @@ -37,13 +38,14 @@ public: bool hideProcess(pid_t pid); bool isHidden(pid_t pid); bool isDarkMode(); - bool hasDarkMode(); + bool isStatusBarDark(); bool enableAccessibility(); bool enableScreenRecording(); void toggleForegroundApp(bool foreground); signals: void lockDatabases(); + void interfaceThemeChanged(); private: void* self; diff --git a/src/gui/osutils/macutils/AppKitImpl.h b/src/gui/osutils/macutils/AppKitImpl.h index 5dadc31dd..5e7a2fbae 100644 --- a/src/gui/osutils/macutils/AppKitImpl.h +++ b/src/gui/osutils/macutils/AppKitImpl.h @@ -35,6 +35,7 @@ - (bool) hideProcess:(pid_t) pid; - (bool) isHidden:(pid_t) pid; - (bool) isDarkMode; +- (bool) isStatusBarDark; - (void) userSwitchHandler:(NSNotification*) notification; - (bool) enableAccessibility; - (bool) enableScreenRecording; diff --git a/src/gui/osutils/macutils/AppKitImpl.mm b/src/gui/osutils/macutils/AppKitImpl.mm index 077dd71a6..faf061106 100644 --- a/src/gui/osutils/macutils/AppKitImpl.mm +++ b/src/gui/osutils/macutils/AppKitImpl.mm @@ -17,7 +17,11 @@ */ #import "AppKitImpl.h" +#include "AppKit.h" +#import +#import +#import #import #import @@ -26,17 +30,31 @@ - (id) initWithObject:(AppKit*)appkit { self = [super init]; + if (self) { m_appkit = appkit; - [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:static_cast(self) + [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self selector:@selector(didDeactivateApplicationObserver:) name:NSWorkspaceDidDeactivateApplicationNotification object:nil]; - [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:static_cast(self) + [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self selector:@selector(userSwitchHandler:) name:NSWorkspaceSessionDidResignActiveNotification object:nil]; + + [[NSDistributedNotificationCenter defaultCenter] addObserver:self + selector:@selector(interfaceThemeChanged:) + name:@"AppleInterfaceThemeChangedNotification" + object:nil]; + + // Unfortunately, there is no notification for a wallpaper change, which affects + // the status bar colour on macOS Big Sur, but we can at least subscribe to this. + [[NSDistributedNotificationCenter defaultCenter] addObserver:self + selector:@selector(interfaceThemeChanged:) + name:@"AppleColorPreferencesChangedNotification" + object:nil]; + } return self; } @@ -54,6 +72,18 @@ } } +// +// Light / dark theme toggled +// +- (void) interfaceThemeChanged:(NSNotification*) notification +{ + Q_UNUSED(notification); + if (m_appkit) { + emit m_appkit->interfaceThemeChanged(); + } +} + + // // Get process id of frontmost application (-> keyboard input) // @@ -108,6 +138,23 @@ && NSOrderedSame == [style caseInsensitiveCompare:@"dark"] ); } + +// +// Get global menu bar theme state +// +- (bool) isStatusBarDark +{ + if (@available(macOS 10.17, *)) { + // This is an ugly hack, but I couldn't find a way to access QTrayIcon's NSStatusItem. + NSStatusItem* dummy = [[NSStatusBar systemStatusBar] statusItemWithLength:0]; + NSString* appearance = [dummy.button.effectiveAppearance.name lowercaseString]; + [[NSStatusBar systemStatusBar] removeStatusItem:dummy]; + return [appearance containsString:@"dark"]; + } + + return [self isDarkMode]; +} + // // Notification for user switch // @@ -170,7 +217,8 @@ // ------------------------- C++ Trampolines ------------------------- // -AppKit::AppKit(QObject* parent) : QObject(parent) +AppKit::AppKit(QObject* parent) + : QObject(parent) { self = [[AppKitImpl alloc] initWithObject:this]; } @@ -178,6 +226,7 @@ AppKit::AppKit(QObject* parent) : QObject(parent) AppKit::~AppKit() { [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:static_cast(self)]; + [[NSDistributedNotificationCenter defaultCenter] removeObserver:static_cast(self)]; [static_cast(self) dealloc]; } @@ -216,6 +265,12 @@ bool AppKit::isDarkMode() return [static_cast(self) isDarkMode]; } +bool AppKit::isStatusBarDark() +{ + return [static_cast(self) isStatusBarDark]; +} + + bool AppKit::enableAccessibility() { return [static_cast(self) enableAccessibility]; diff --git a/src/gui/osutils/macutils/MacUtils.cpp b/src/gui/osutils/macutils/MacUtils.cpp index d32a15612..ebcc627e0 100644 --- a/src/gui/osutils/macutils/MacUtils.cpp +++ b/src/gui/osutils/macutils/MacUtils.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include @@ -33,6 +34,14 @@ MacUtils::MacUtils(QObject* parent) , m_appkit(new AppKit()) { connect(m_appkit.data(), SIGNAL(lockDatabases()), SIGNAL(lockDatabases())); + connect(m_appkit.data(), SIGNAL(interfaceThemeChanged()), SIGNAL(interfaceThemeChanged())); + connect(m_appkit.data(), &AppKit::interfaceThemeChanged, this, [this]() { + // Emit with delay, since isStatusBarDark() still returns the old value + // if we call it too fast after a theme change. + QTimer::singleShot(100, [this]() { + emit statusbarThemeChanged(); + }); + }); } MacUtils::~MacUtils() @@ -93,6 +102,11 @@ bool MacUtils::isDarkMode() const return m_appkit->isDarkMode(); } +bool MacUtils::isStatusBarDark() const +{ + return m_appkit->isStatusBarDark(); +} + QString MacUtils::getLaunchAgentFilename() const { auto launchAgentDir = QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/../LaunchAgents")); diff --git a/src/gui/osutils/macutils/MacUtils.h b/src/gui/osutils/macutils/MacUtils.h index 281c5438c..52270f6a7 100644 --- a/src/gui/osutils/macutils/MacUtils.h +++ b/src/gui/osutils/macutils/MacUtils.h @@ -22,6 +22,7 @@ #include "gui/osutils/OSUtilsBase.h" #include "AppKit.h" +#include #include #include #include @@ -34,6 +35,7 @@ public: static MacUtils* instance(); bool isDarkMode() const override; + bool isStatusBarDark() const override; bool isLaunchAtStartupEnabled() const override; void setLaunchAtStartup(bool enable) override; bool isCapslockEnabled() override; diff --git a/src/gui/osutils/nixutils/NixUtils.cpp b/src/gui/osutils/nixutils/NixUtils.cpp index d412aa813..eacb20060 100644 --- a/src/gui/osutils/nixutils/NixUtils.cpp +++ b/src/gui/osutils/nixutils/NixUtils.cpp @@ -62,6 +62,12 @@ bool NixUtils::isDarkMode() const return qApp->style()->standardPalette().color(QPalette::Window).toHsl().lightness() < 110; } +bool NixUtils::isStatusBarDark() const +{ + // TODO: implement + return isDarkMode(); +} + QString NixUtils::getAutostartDesktopFilename(bool createDirs) const { QDir autostartDir; diff --git a/src/gui/osutils/nixutils/NixUtils.h b/src/gui/osutils/nixutils/NixUtils.h index c91580796..bc1be9975 100644 --- a/src/gui/osutils/nixutils/NixUtils.h +++ b/src/gui/osutils/nixutils/NixUtils.h @@ -29,6 +29,7 @@ public: static NixUtils* instance(); bool isDarkMode() const override; + bool isStatusBarDark() const override; bool isLaunchAtStartupEnabled() const override; void setLaunchAtStartup(bool enable) override; bool isCapslockEnabled() override; diff --git a/src/gui/osutils/winutils/WinUtils.cpp b/src/gui/osutils/winutils/WinUtils.cpp index 61e913c93..ab5562b7b 100644 --- a/src/gui/osutils/winutils/WinUtils.cpp +++ b/src/gui/osutils/winutils/WinUtils.cpp @@ -85,6 +85,12 @@ bool WinUtils::isDarkMode() const return settings.value("AppsUseLightTheme", 1).toInt() == 0; } +bool WinUtils::isStatusBarDark() const +{ + // TODO: implement + return isDarkMode(); +} + bool WinUtils::isLaunchAtStartupEnabled() const { return QSettings(R"(HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run)", QSettings::NativeFormat) diff --git a/src/gui/osutils/winutils/WinUtils.h b/src/gui/osutils/winutils/WinUtils.h index c19040274..d8f9a10ea 100644 --- a/src/gui/osutils/winutils/WinUtils.h +++ b/src/gui/osutils/winutils/WinUtils.h @@ -33,6 +33,7 @@ public: static void registerEventFilters(); bool isDarkMode() const override; + bool isStatusBarDark() const override; bool isLaunchAtStartupEnabled() const override; void setLaunchAtStartup(bool enable) override; bool isCapslockEnabled() override; diff --git a/src/gui/styles/base/BaseStyle.cpp b/src/gui/styles/base/BaseStyle.cpp index 90c515871..26a251dc2 100644 --- a/src/gui/styles/base/BaseStyle.cpp +++ b/src/gui/styles/base/BaseStyle.cpp @@ -52,6 +52,10 @@ #include #include +#ifdef Q_OS_MACOS +#include +#endif + #include #include "core/Resources.h" @@ -288,10 +292,16 @@ namespace Phantom #ifdef Q_OS_MACOS QColor tabBarBase(const QPalette& pal) { - return hack_isLightPalette(pal) ? QRgb(0xD1D1D1) : QRgb(0x252525); + if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::MacOSBigSur) { + return hack_isLightPalette(pal) ? QRgb(0xD4D4D4) : QRgb(0x2A2A2A); + } + return hack_isLightPalette(pal) ? QRgb(0xDD1D1D1) : QRgb(0x252525); } QColor tabBarBaseInactive(const QPalette& pal) { + if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::MacOSBigSur) { + return hack_isLightPalette(pal) ? QRgb(0xF5F5F5) : QRgb(0x2D2D2D); + } return hack_isLightPalette(pal) ? QRgb(0xF4F4F4) : QRgb(0x282828); } #endif @@ -4571,27 +4581,6 @@ QStyle::SubControl BaseStyle::hitTestComplexControl(ComplexControl cc, return QCommonStyle::hitTestComplexControl(cc, opt, pt, w); } -QPixmap BaseStyle::generatedIconPixmap(QIcon::Mode iconMode, const QPixmap& pixmap, const QStyleOption* opt) const -{ - // Default icon highlight is way too subtle - if (iconMode == QIcon::Selected) { - QImage img = pixmap.toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); - QPainter painter(&img); - - painter.setCompositionMode(QPainter::CompositionMode_SourceAtop); - - QColor color = - Phantom::DeriveColors::adjustLightness(opt->palette.color(QPalette::Normal, QPalette::Highlight), .25); - color.setAlphaF(0.25); - painter.fillRect(0, 0, img.width(), img.height(), color); - - painter.end(); - - return QPixmap::fromImage(img); - } - return QCommonStyle::generatedIconPixmap(iconMode, pixmap, opt); -} - int BaseStyle::styleHint(StyleHint hint, const QStyleOption* option, const QWidget* widget, diff --git a/src/gui/styles/base/BaseStyle.h b/src/gui/styles/base/BaseStyle.h index d3c20915c..e9bdcbdc8 100644 --- a/src/gui/styles/base/BaseStyle.h +++ b/src/gui/styles/base/BaseStyle.h @@ -70,7 +70,6 @@ public: const QStyleOptionComplex* opt, SubControl sc, const QWidget* widget) const override; - QPixmap generatedIconPixmap(QIcon::Mode iconMode, const QPixmap& pixmap, const QStyleOption* opt) const override; int styleHint(StyleHint hint, const QStyleOption* option = nullptr, const QWidget* widget = nullptr, diff --git a/src/gui/styles/dark/DarkStyle.cpp b/src/gui/styles/dark/DarkStyle.cpp index 25f75e5ab..491f18d1f 100644 --- a/src/gui/styles/dark/DarkStyle.cpp +++ b/src/gui/styles/dark/DarkStyle.cpp @@ -114,9 +114,9 @@ void DarkStyle::polish(QWidget* widget) auto palette = widget->palette(); #if defined(Q_OS_MACOS) if (!osUtils->isDarkMode()) { - palette.setColor(QPalette::Active, QPalette::Window, QRgb(0x252525)); - palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0x282828)); - palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0x252525)); + palette.setColor(QPalette::Active, QPalette::Window, QRgb(0x2A2A2A)); + palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0x2D2D2D)); + palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0x2D2D2D)); } #elif defined(Q_OS_WIN) // Register event filter for better dark mode support diff --git a/src/gui/styles/light/LightStyle.cpp b/src/gui/styles/light/LightStyle.cpp index f1f0cb997..483e1323b 100644 --- a/src/gui/styles/light/LightStyle.cpp +++ b/src/gui/styles/light/LightStyle.cpp @@ -115,9 +115,9 @@ void LightStyle::polish(QWidget* widget) auto palette = widget->palette(); #if defined(Q_OS_MACOS) if (osUtils->isDarkMode()) { - palette.setColor(QPalette::Active, QPalette::Window, QRgb(0xD1D1D1)); - palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0xF4F4F4)); - palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0xD1D1D1)); + palette.setColor(QPalette::Active, QPalette::Window, QRgb(0xD4D4D4)); + palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0xF5F5F5)); + palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0xF5F5F5)); } #elif defined(Q_OS_WIN) palette.setColor(QPalette::All, QPalette::Window, QRgb(0xFFFFFF)); From f7d387d2335ee826e2c758c170734c884fa2b90d Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Thu, 24 Dec 2020 00:09:51 +0100 Subject: [PATCH 2/5] Add dynamic theme switching on Windows 10 (backport) --- src/core/Resources.cpp | 3 +-- src/gui/ApplicationSettingsWidget.cpp | 2 +- src/gui/osutils/winutils/WinUtils.cpp | 24 +++++++++++++++++------- src/gui/osutils/winutils/WinUtils.h | 3 +++ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/core/Resources.cpp b/src/core/Resources.cpp index 26c63ab1c..2976f9a25 100644 --- a/src/core/Resources.cpp +++ b/src/core/Resources.cpp @@ -142,7 +142,7 @@ QIcon Resources::trayIcon(QString style) } QIcon i; -#ifdef Q_OS_MACOS +#if defined(Q_OS_MACOS) || defined(Q_OS_WIN) if (osUtils->isStatusBarDark()) { i = icon(QString("keepassxc-monochrome-light%1").arg(style), false); } else { @@ -171,7 +171,6 @@ QIcon Resources::trayIconUnlocked() return trayIcon("unlocked"); } - AdaptiveIconEngine::AdaptiveIconEngine(QIcon baseIcon) : QIconEngine() , m_baseIcon(std::move(baseIcon)) diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index 0d9829229..8ec2d4730 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -250,7 +250,7 @@ void ApplicationSettingsWidget::loadSettings() } m_generalUi->trayIconAppearance->clear(); -#ifdef Q_OS_MACOS +#if defined(Q_OS_MACOS) || defined(Q_OS_WIN) m_generalUi->trayIconAppearance->addItem(tr("Monochrome"), "monochrome"); #else m_generalUi->trayIconAppearance->addItem(tr("Monochrome (light)"), "monochrome-light"); diff --git a/src/gui/osutils/winutils/WinUtils.cpp b/src/gui/osutils/winutils/WinUtils.cpp index ab5562b7b..9a08ef01f 100644 --- a/src/gui/osutils/winutils/WinUtils.cpp +++ b/src/gui/osutils/winutils/WinUtils.cpp @@ -30,6 +30,8 @@ WinUtils* WinUtils::instance() { if (!m_instance) { m_instance = new WinUtils(qApp); + m_instance->m_darkAppThemeActive = m_instance->isDarkMode(); + m_instance->m_darkSystemThemeActive = m_instance->isStatusBarDark(); } return m_instance; @@ -66,13 +68,20 @@ bool WinUtils::DWMEventFilter::nativeEventFilter(const QByteArray& eventType, vo return false; } switch (msg->message) { - case WM_CREATE: - case WM_INITDIALOG: { - if (winUtils()->isDarkMode()) { - // TODO: indicate dark mode support for black title bar + case WM_SETTINGCHANGE: + if (m_darkAppThemeActive != isDarkMode()) { + m_darkAppThemeActive = !m_darkAppThemeActive; + emit interfaceThemeChanged(); + } + + if (m_darkSystemThemeActive != isStatusBarDark()) { + m_darkSystemThemeActive = !m_darkSystemThemeActive; + emit statusbarThemeChanged(); } break; - } + case WM_HOTKEY: + triggerGlobalShortcut(msg->wParam); + break; } return false; @@ -87,8 +96,9 @@ bool WinUtils::isDarkMode() const bool WinUtils::isStatusBarDark() const { - // TODO: implement - return isDarkMode(); + QSettings settings(R"(HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)", + QSettings::NativeFormat); + return settings.value("SystemUsesLightTheme", 0).toInt() == 0; } bool WinUtils::isLaunchAtStartupEnabled() const diff --git a/src/gui/osutils/winutils/WinUtils.h b/src/gui/osutils/winutils/WinUtils.h index d8f9a10ea..19f43fd16 100644 --- a/src/gui/osutils/winutils/WinUtils.h +++ b/src/gui/osutils/winutils/WinUtils.h @@ -53,6 +53,9 @@ private: static QPointer m_instance; static QScopedPointer m_eventFilter; + bool m_darkAppThemeActive; + bool m_darkSystemThemeActive; + Q_DISABLE_COPY(WinUtils) }; From 48f2efbc921f0b9fce461dca4c2abf4643be5511 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 31 Jan 2021 12:52:45 +0100 Subject: [PATCH 3/5] Fix adaptive icon painting (backport) Fixes resolution problems on HighDPI. Backport of c7323accf2a004293a67bb06ff0eb9a692903f35 --- src/core/Resources.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/core/Resources.cpp b/src/core/Resources.cpp index 2976f9a25..66aa07f8e 100644 --- a/src/core/Resources.cpp +++ b/src/core/Resources.cpp @@ -179,15 +179,8 @@ AdaptiveIconEngine::AdaptiveIconEngine(QIcon baseIcon) void AdaptiveIconEngine::paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) - double dpr = !kpxcApp->testAttribute(Qt::AA_UseHighDpiPixmaps) ? 1.0 : painter->device()->devicePixelRatioF(); -#else - double dpr = !kpxcApp->testAttribute(Qt::AA_UseHighDpiPixmaps) ? 1.0 : painter->device()->devicePixelRatio(); -#endif - QSize pixmapSize = rect.size() * dpr; - painter->save(); - painter->drawPixmap(rect, m_baseIcon.pixmap(pixmapSize, mode, state)); + m_baseIcon.paint(painter, rect, Qt::AlignCenter, mode, state); if (getMainWindow()) { QPalette palette = getMainWindow()->palette(); From 874e9e10751d5d6cdf6d957f4d4eb60b70c86f2a Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 31 Jan 2021 14:25:42 +0100 Subject: [PATCH 4/5] Fix icon alpha blending in QTableView (backport) Some widgets such as QTableView do not call QIconEngine::pixmap(), but do the drawing immediately through QIconEngine::paint(). This breaks alpha blending for recolouring, since the underlying image canvas is not necessarily transparent and also not anchored at (0, 0). This results in a black box of the size of the icon bounding box. Icon recolouring is now always done on a temporary QImage with transparent background and only the finished end result is composed onto the original canvas. Fixes #6006 --- src/core/Resources.cpp | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/core/Resources.cpp b/src/core/Resources.cpp index 66aa07f8e..d602698d7 100644 --- a/src/core/Resources.cpp +++ b/src/core/Resources.cpp @@ -179,24 +179,29 @@ AdaptiveIconEngine::AdaptiveIconEngine(QIcon baseIcon) void AdaptiveIconEngine::paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) { - painter->save(); - m_baseIcon.paint(painter, rect, Qt::AlignCenter, mode, state); + // Temporary image canvas to ensure that the background is transparent and alpha blending works. + QImage img(rect.size(), QImage::Format_ARGB32_Premultiplied); + img.fill(0); + QPainter p(&img); + + m_baseIcon.paint(&p, img.rect(), Qt::AlignCenter, mode, state); if (getMainWindow()) { QPalette palette = getMainWindow()->palette(); - painter->setCompositionMode(QPainter::CompositionMode_SourceAtop); + p.setCompositionMode(QPainter::CompositionMode_SourceIn); if (mode == QIcon::Active) { - painter->fillRect(rect, palette.color(QPalette::Active, QPalette::ButtonText)); + p.fillRect(img.rect(), palette.color(QPalette::Active, QPalette::ButtonText)); } else if (mode == QIcon::Selected) { - painter->fillRect(rect, palette.color(QPalette::Active, QPalette::HighlightedText)); + p.fillRect(img.rect(), palette.color(QPalette::Active, QPalette::HighlightedText)); } else if (mode == QIcon::Disabled) { - painter->fillRect(rect, palette.color(QPalette::Disabled, QPalette::WindowText)); + p.fillRect(img.rect(), palette.color(QPalette::Disabled, QPalette::WindowText)); } else { - painter->fillRect(rect, palette.color(QPalette::Normal, QPalette::WindowText)); + p.fillRect(img.rect(), palette.color(QPalette::Normal, QPalette::WindowText)); } } - painter->restore(); + + painter->drawImage(rect, img); } QPixmap AdaptiveIconEngine::pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state) @@ -205,7 +210,7 @@ QPixmap AdaptiveIconEngine::pixmap(const QSize& size, QIcon::Mode mode, QIcon::S img.fill(0); QPainter painter(&img); paint(&painter, QRect(0, 0, size.width(), size.height()), mode, state); - return QPixmap::fromImage(img, Qt::NoFormatConversion); + return QPixmap::fromImage(img, Qt::ImageConversionFlag::NoFormatConversion); } QIconEngine* AdaptiveIconEngine::clone() const From 2889341acdafbe919d56bca76e8b256690feed94 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 31 Jan 2021 20:19:01 +0100 Subject: [PATCH 5/5] Fix on/off icons not being redrawn on theme change (backport) --- src/core/Resources.cpp | 32 +++++++-------------------- src/core/Resources.h | 2 +- src/gui/EntryPreviewWidget.cpp | 9 +++++--- src/gui/PasswordEdit.cpp | 4 ++-- src/gui/osutils/winutils/WinUtils.cpp | 15 +++++-------- 5 files changed, 23 insertions(+), 39 deletions(-) diff --git a/src/core/Resources.cpp b/src/core/Resources.cpp index d602698d7..df89a4f98 100644 --- a/src/core/Resources.cpp +++ b/src/core/Resources.cpp @@ -180,7 +180,12 @@ AdaptiveIconEngine::AdaptiveIconEngine(QIcon baseIcon) void AdaptiveIconEngine::paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) { // Temporary image canvas to ensure that the background is transparent and alpha blending works. - QImage img(rect.size(), QImage::Format_ARGB32_Premultiplied); +#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) + auto scale = painter->device()->devicePixelRatioF(); +#else + auto scale = painter->device()->devicePixelRatio(); +#endif + QImage img(rect.size() * scale, QImage::Format_ARGB32_Premultiplied); img.fill(0); QPainter p(&img); @@ -251,30 +256,9 @@ QIcon Resources::icon(const QString& name, bool recolor, const QColor& overrideC return icon; } -QIcon Resources::onOffIcon(const QString& name, bool recolor) +QIcon Resources::onOffIcon(const QString& name, bool on, bool recolor) { - QString cacheName = "onoff/" + name; - - QIcon icon = m_iconCache.value(cacheName); - - if (!icon.isNull()) { - return icon; - } - - const QSize size(48, 48); - QIcon on = Resources::icon(name + "-on", recolor); - icon.addPixmap(on.pixmap(size, QIcon::Mode::Normal), QIcon::Mode::Normal, QIcon::On); - icon.addPixmap(on.pixmap(size, QIcon::Mode::Selected), QIcon::Mode::Selected, QIcon::On); - icon.addPixmap(on.pixmap(size, QIcon::Mode::Disabled), QIcon::Mode::Disabled, QIcon::On); - - QIcon off = Resources::icon(name + "-off", recolor); - icon.addPixmap(off.pixmap(size, QIcon::Mode::Normal), QIcon::Mode::Normal, QIcon::Off); - icon.addPixmap(off.pixmap(size, QIcon::Mode::Selected), QIcon::Mode::Selected, QIcon::Off); - icon.addPixmap(off.pixmap(size, QIcon::Mode::Disabled), QIcon::Mode::Disabled, QIcon::Off); - - m_iconCache.insert(cacheName, icon); - - return icon; + return icon(name + (on ? "-on" : "-off"), recolor); } Resources::Resources() diff --git a/src/core/Resources.h b/src/core/Resources.h index 6cadb5996..1b9c9b91c 100644 --- a/src/core/Resources.h +++ b/src/core/Resources.h @@ -36,7 +36,7 @@ public: QIcon trayIconUnlocked(); QString trayIconAppearance() const; QIcon icon(const QString& name, bool recolor = true, const QColor& overrideColor = QColor::Invalid); - QIcon onOffIcon(const QString& name, bool recolor = true); + QIcon onOffIcon(const QString& name, bool on, bool recolor = true); static Resources* instance(); diff --git a/src/gui/EntryPreviewWidget.cpp b/src/gui/EntryPreviewWidget.cpp index 45a71a348..e240f7ae4 100644 --- a/src/gui/EntryPreviewWidget.cpp +++ b/src/gui/EntryPreviewWidget.cpp @@ -50,9 +50,9 @@ EntryPreviewWidget::EntryPreviewWidget(QWidget* parent) // Entry m_ui->entryTotpButton->setIcon(resources()->icon("chronometer")); m_ui->entryCloseButton->setIcon(resources()->icon("dialog-close")); - m_ui->togglePasswordButton->setIcon(resources()->onOffIcon("password-show")); - m_ui->toggleEntryNotesButton->setIcon(resources()->onOffIcon("password-show")); - m_ui->toggleGroupNotesButton->setIcon(resources()->onOffIcon("password-show")); + m_ui->togglePasswordButton->setIcon(resources()->onOffIcon("password-show", true)); + m_ui->toggleEntryNotesButton->setIcon(resources()->onOffIcon("password-show", true)); + m_ui->toggleGroupNotesButton->setIcon(resources()->onOffIcon("password-show", true)); m_ui->entryAttachmentsWidget->setReadOnly(true); m_ui->entryAttachmentsWidget->setButtonsVisible(false); @@ -199,16 +199,19 @@ void EntryPreviewWidget::setPasswordVisible(bool state) } else { m_ui->entryPasswordLabel->setText(QString("\u25cf").repeated(6)); } + m_ui->togglePasswordButton->setIcon(resources()->onOffIcon("password-show", state)); } void EntryPreviewWidget::setEntryNotesVisible(bool state) { setNotesVisible(m_ui->entryNotesTextEdit, m_currentEntry->notes(), state); + m_ui->toggleEntryNotesButton->setIcon(resources()->onOffIcon("password-show", state)); } void EntryPreviewWidget::setGroupNotesVisible(bool state) { setNotesVisible(m_ui->groupNotesTextEdit, m_currentGroup->notes(), state); + m_ui->toggleGroupNotesButton->setIcon(resources()->onOffIcon("password-show", state)); } void EntryPreviewWidget::setNotesVisible(QTextEdit* notesWidget, const QString& notes, bool state) diff --git a/src/gui/PasswordEdit.cpp b/src/gui/PasswordEdit.cpp index b43f5c623..73f8dd0f9 100644 --- a/src/gui/PasswordEdit.cpp +++ b/src/gui/PasswordEdit.cpp @@ -58,7 +58,7 @@ PasswordEdit::PasswordEdit(QWidget* parent) #endif m_toggleVisibleAction = new QAction( - resources()->icon("password-show-off"), + resources()->onOffIcon("password-show", false), tr("Toggle Password (%1)").arg(QKeySequence(modifier + Qt::Key_H).toString(QKeySequence::NativeText)), nullptr); m_toggleVisibleAction->setCheckable(true); @@ -113,7 +113,7 @@ void PasswordEdit::enablePasswordGenerator() void PasswordEdit::setShowPassword(bool show) { setEchoMode(show ? QLineEdit::Normal : QLineEdit::Password); - m_toggleVisibleAction->setIcon(resources()->icon(show ? "password-show-on" : "password-show-off")); + m_toggleVisibleAction->setIcon(resources()->onOffIcon("password-show", show)); m_toggleVisibleAction->setChecked(show); if (m_repeatPasswordEdit) { diff --git a/src/gui/osutils/winutils/WinUtils.cpp b/src/gui/osutils/winutils/WinUtils.cpp index 9a08ef01f..04216c54f 100644 --- a/src/gui/osutils/winutils/WinUtils.cpp +++ b/src/gui/osutils/winutils/WinUtils.cpp @@ -69,19 +69,16 @@ bool WinUtils::DWMEventFilter::nativeEventFilter(const QByteArray& eventType, vo } switch (msg->message) { case WM_SETTINGCHANGE: - if (m_darkAppThemeActive != isDarkMode()) { - m_darkAppThemeActive = !m_darkAppThemeActive; - emit interfaceThemeChanged(); + if (m_instance->m_darkAppThemeActive != m_instance->isDarkMode()) { + m_instance->m_darkAppThemeActive = !m_instance->m_darkAppThemeActive; + emit m_instance->interfaceThemeChanged(); } - if (m_darkSystemThemeActive != isStatusBarDark()) { - m_darkSystemThemeActive = !m_darkSystemThemeActive; - emit statusbarThemeChanged(); + if (m_instance->m_darkSystemThemeActive != m_instance->isStatusBarDark()) { + m_instance->m_darkSystemThemeActive = !m_instance->m_darkSystemThemeActive; + emit m_instance->statusbarThemeChanged(); } break; - case WM_HOTKEY: - triggerGlobalShortcut(msg->wParam); - break; } return false;