Skip to content

Commit

Permalink
Improve macOS platform integration.
Browse files Browse the repository at this point in the history
- 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)
- Update theme background colours for Big Sur
- Update application icon to match Big Sur HIG

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 keepassxreboot#4933
Fixes keepassxreboot#5349
  • Loading branch information
phoerious committed Jan 7, 2021
1 parent 37dab85 commit 80c1b9b
Show file tree
Hide file tree
Showing 32 changed files with 7,962 additions and 102 deletions.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
7,697 changes: 7,697 additions & 0 deletions share/macosx/keepassxc.ai

Large diffs are not rendered by default.

Binary file modified share/macosx/keepassxc.icns
Binary file not shown.
Binary file added share/macosx/keepassxc.iconset/icon_128x128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added share/macosx/keepassxc.iconset/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added share/macosx/keepassxc.iconset/icon_16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added share/macosx/keepassxc.iconset/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added share/macosx/keepassxc.iconset/icon_256x256.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added share/macosx/keepassxc.iconset/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added share/macosx/keepassxc.iconset/icon_32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added share/macosx/keepassxc.iconset/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added share/macosx/keepassxc.iconset/icon_512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 14 additions & 7 deletions src/gui/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "core/Bootstrap.h"
#include "core/Config.h"
#include "core/Global.h"
#include "gui/Icons.h"
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
#include "gui/osutils/OSUtils.h"
Expand Down Expand Up @@ -127,6 +128,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()
Expand Down Expand Up @@ -174,15 +181,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
Expand Down
4 changes: 4 additions & 0 deletions src/gui/ApplicationSettingsWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,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(icons()->trayIconAppearance());
if (trayIconIndex > 0) {
Expand Down
166 changes: 109 additions & 57 deletions src/gui/Icons.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
#include "Icons.h"

#include <QBitmap>
#include <QIconEngine>
#include <QPaintDevice>
#include <QPainter>
#include <QStyle>

Expand All @@ -27,8 +29,24 @@
#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;
};

Icons* Icons::m_instance(nullptr);

Icons::Icons()
{
}

QIcon Icons::applicationIcon()
{
return icon("keepassxc", false);
Expand All @@ -47,45 +65,102 @@ QString Icons::trayIconAppearance() const
return iconAppearance;
}

QIcon Icons::trayIcon()
QIcon Icons::trayIcon(QString style)
{
return trayIconUnlocked();
}
if (style == "unlocked") {
style.clear();
}
if (!style.isEmpty()) {
style = "-" + style;
}

QIcon Icons::trayIconLocked()
{
auto iconApperance = trayIconAppearance();

if (iconApperance == "monochrome-light") {
return icon("keepassxc-monochrome-light-locked", false);
if (!iconApperance.startsWith("monochrome")) {
return icon(QString("keepassxc%1").arg(style), false);
}
if (iconApperance == "monochrome-dark") {
return icon("keepassxc-monochrome-dark-locked", 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);
}
return icon("keepassxc-locked", 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 Icons::trayIconLocked()
{
return trayIcon("locked");
}

QIcon Icons::trayIconUnlocked()
{
auto iconApperance = trayIconAppearance();
return trayIcon("unlocked");
}

if (iconApperance == "monochrome-light") {
return icon("keepassxc-monochrome-light", false);
}
if (iconApperance == "monochrome-dark") {
return icon("keepassxc-monochrome-dark", false);
}
return icon("keepassxc", false);
AdaptiveIconEngine::AdaptiveIconEngine(QIcon baseIcon)
: QIconEngine()
, m_baseIcon(std::move(baseIcon))
{
}

QIcon Icons::icon(const QString& name, bool recolor, const QColor& overrideColor)
void AdaptiveIconEngine::paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state)
{
QIcon icon = m_iconCache.value(name);
#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;

if (!icon.isNull() && !overrideColor.isValid()) {
return icon;
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));
}
}
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 Icons::icon(const QString& name, bool recolor, const QColor& overrideColor)
{
#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
Expand All @@ -94,44 +169,25 @@ QIcon Icons::icon(const QString& name, bool recolor, const QColor& overrideColor
// 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

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);
QString cacheName =
QString("%1:%2:%3").arg(recolor ? "1" : "0", overrideColor.isValid() ? overrideColor.name() : "#", name);
QIcon icon = m_iconCache.value(cacheName);

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 (!icon.isNull() && !overrideColor.isValid()) {
return icon;
}

icon = QIcon::fromTheme(name);
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;
}

Expand Down Expand Up @@ -161,10 +217,6 @@ QIcon Icons::onOffIcon(const QString& name, bool recolor)
return icon;
}

Icons::Icons()
{
}

Icons* Icons::instance()
{
if (!m_instance) {
Expand Down
2 changes: 1 addition & 1 deletion src/gui/Icons.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class Icons
{
public:
QIcon applicationIcon();
QIcon trayIcon();
QIcon trayIcon(QString style = "unlocked");
QIcon trayIconLocked();
QIcon trayIconUnlocked();
QString trayIconAppearance() const;
Expand Down
15 changes: 10 additions & 5 deletions src/gui/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include "gui/Icons.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"
Expand Down Expand Up @@ -503,6 +504,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);
Expand Down Expand Up @@ -599,10 +602,10 @@ MainWindow::MainWindow()
}
#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);
connect(qApp, SIGNAL(anotherInstanceStarted()), this, SLOT(bringToFront()));
connect(qApp, SIGNAL(applicationActivated()), this, SLOT(bringToFront()));
connect(qApp, SIGNAL(openFile(QString)), this, SLOT(openDatabase(QString)));
connect(qApp, SIGNAL(quitSignalReceived()), this, SLOT(appExit()), Qt::DirectConnection);

restoreConfigState();
}
Expand Down Expand Up @@ -1757,8 +1760,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();
}
});

Expand Down
15 changes: 15 additions & 0 deletions src/gui/osutils/OSUtilsBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ class OSUtilsBase : public QObject
*/
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).
*/
Expand All @@ -61,6 +66,16 @@ class OSUtilsBase : public QObject
signals:
void globalShortcutTriggered(const QString& name);

/**
* 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();
Expand Down
3 changes: 3 additions & 0 deletions src/gui/osutils/macutils/AppKit.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#define KEEPASSX_APPKIT_H

#include <QObject>
#include <QColor>
#include <unistd.h>

class AppKit : public QObject
Expand All @@ -37,12 +38,14 @@ class AppKit : public QObject
bool hideProcess(pid_t pid);
bool isHidden(pid_t pid);
bool isDarkMode();
bool isStatusBarDark();
bool enableAccessibility();
bool enableScreenRecording();
void toggleForegroundApp(bool foreground);

signals:
void lockDatabases();
void interfaceThemeChanged();

private:
void* self;
Expand Down
Loading

0 comments on commit 80c1b9b

Please sign in to comment.