Improve macOS platform integration.
- 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 #4933 Fixes #5349
3
.gitattributes
vendored
@ -11,3 +11,6 @@ AppImage-Recipe.sh export-ignore
|
|||||||
# github-linguist language hints
|
# github-linguist language hints
|
||||||
*.h linguist-language=C++
|
*.h linguist-language=C++
|
||||||
*.cpp linguist-language=C++
|
*.cpp linguist-language=C++
|
||||||
|
|
||||||
|
# binary files
|
||||||
|
*.ai binary
|
||||||
|
BIN
share/macosx/keepassxc.ai
Executable file
BIN
share/macosx/keepassxc.iconset/icon_128x128.png
Executable file
After Width: | Height: | Size: 9.7 KiB |
BIN
share/macosx/keepassxc.iconset/icon_128x128@2x.png
Executable file
After Width: | Height: | Size: 23 KiB |
BIN
share/macosx/keepassxc.iconset/icon_16x16.png
Executable file
After Width: | Height: | Size: 783 B |
BIN
share/macosx/keepassxc.iconset/icon_16x16@2x.png
Executable file
After Width: | Height: | Size: 1.7 KiB |
BIN
share/macosx/keepassxc.iconset/icon_256x256.png
Executable file
After Width: | Height: | Size: 23 KiB |
BIN
share/macosx/keepassxc.iconset/icon_256x256@2x.png
Executable file
After Width: | Height: | Size: 58 KiB |
BIN
share/macosx/keepassxc.iconset/icon_32x32.png
Executable file
After Width: | Height: | Size: 1.7 KiB |
BIN
share/macosx/keepassxc.iconset/icon_32x32@2x.png
Executable file
After Width: | Height: | Size: 4.0 KiB |
BIN
share/macosx/keepassxc.iconset/icon_512x512.png
Executable file
After Width: | Height: | Size: 58 KiB |
BIN
share/macosx/keepassxc.iconset/icon_512x512@2x.png
Executable file
After Width: | Height: | Size: 152 KiB |
@ -23,6 +23,7 @@
|
|||||||
#include "core/Bootstrap.h"
|
#include "core/Bootstrap.h"
|
||||||
#include "core/Config.h"
|
#include "core/Config.h"
|
||||||
#include "core/Global.h"
|
#include "core/Global.h"
|
||||||
|
#include "gui/Icons.h"
|
||||||
#include "gui/MainWindow.h"
|
#include "gui/MainWindow.h"
|
||||||
#include "gui/MessageBox.h"
|
#include "gui/MessageBox.h"
|
||||||
#include "gui/osutils/OSUtils.h"
|
#include "gui/osutils/OSUtils.h"
|
||||||
@ -127,6 +128,12 @@ Application::Application(int& argc, char** argv)
|
|||||||
qWarning()
|
qWarning()
|
||||||
<< QObject::tr("The lock file could not be created. Single-instance mode disabled.").toUtf8().constData();
|
<< 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()
|
Application::~Application()
|
||||||
@ -174,15 +181,15 @@ void Application::applyTheme()
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
QPixmapCache::clear();
|
||||||
if (appTheme == "light") {
|
if (appTheme == "light") {
|
||||||
setStyle(new LightStyle);
|
auto* s = new LightStyle;
|
||||||
// Workaround Qt 5.15+ bug
|
setPalette(s->standardPalette());
|
||||||
setPalette(style()->standardPalette());
|
setStyle(s);
|
||||||
} else if (appTheme == "dark") {
|
} else if (appTheme == "dark") {
|
||||||
setStyle(new DarkStyle);
|
auto* s = new DarkStyle;
|
||||||
// Workaround Qt 5.15+ bug
|
setPalette(s->standardPalette());
|
||||||
setPalette(style()->standardPalette());
|
setStyle(s);
|
||||||
m_darkTheme = true;
|
m_darkTheme = true;
|
||||||
} else {
|
} else {
|
||||||
// Classic mode, don't check for dark theme on Windows
|
// Classic mode, don't check for dark theme on Windows
|
||||||
|
@ -251,8 +251,12 @@ void ApplicationSettingsWidget::loadSettings()
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_generalUi->trayIconAppearance->clear();
|
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 (light)"), "monochrome-light");
|
||||||
m_generalUi->trayIconAppearance->addItem(tr("Monochrome (dark)"), "monochrome-dark");
|
m_generalUi->trayIconAppearance->addItem(tr("Monochrome (dark)"), "monochrome-dark");
|
||||||
|
#endif
|
||||||
m_generalUi->trayIconAppearance->addItem(tr("Colorful"), "colorful");
|
m_generalUi->trayIconAppearance->addItem(tr("Colorful"), "colorful");
|
||||||
int trayIconIndex = m_generalUi->trayIconAppearance->findData(icons()->trayIconAppearance());
|
int trayIconIndex = m_generalUi->trayIconAppearance->findData(icons()->trayIconAppearance());
|
||||||
if (trayIconIndex > 0) {
|
if (trayIconIndex > 0) {
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
#include "Icons.h"
|
#include "Icons.h"
|
||||||
|
|
||||||
#include <QBitmap>
|
#include <QBitmap>
|
||||||
|
#include <QIconEngine>
|
||||||
|
#include <QPaintDevice>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
|
|
||||||
@ -27,8 +29,24 @@
|
|||||||
#include "gui/MainWindow.h"
|
#include "gui/MainWindow.h"
|
||||||
#include "gui/osutils/OSUtils.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::m_instance(nullptr);
|
||||||
|
|
||||||
|
Icons::Icons()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
QIcon Icons::applicationIcon()
|
QIcon Icons::applicationIcon()
|
||||||
{
|
{
|
||||||
return icon("keepassxc", false);
|
return icon("keepassxc", false);
|
||||||
@ -47,45 +65,102 @@ QString Icons::trayIconAppearance() const
|
|||||||
return iconAppearance;
|
return iconAppearance;
|
||||||
}
|
}
|
||||||
|
|
||||||
QIcon Icons::trayIcon()
|
QIcon Icons::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 Icons::trayIconLocked()
|
QIcon Icons::trayIconLocked()
|
||||||
{
|
{
|
||||||
auto iconApperance = trayIconAppearance();
|
return trayIcon("locked");
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QIcon Icons::trayIconUnlocked()
|
QIcon Icons::trayIconUnlocked()
|
||||||
{
|
{
|
||||||
auto iconApperance = trayIconAppearance();
|
return trayIcon("unlocked");
|
||||||
|
}
|
||||||
|
|
||||||
if (iconApperance == "monochrome-light") {
|
AdaptiveIconEngine::AdaptiveIconEngine(QIcon baseIcon)
|
||||||
return icon("keepassxc-monochrome-light", false);
|
: QIconEngine()
|
||||||
|
, m_baseIcon(std::move(baseIcon))
|
||||||
|
{
|
||||||
}
|
}
|
||||||
if (iconApperance == "monochrome-dark") {
|
|
||||||
return icon("keepassxc-monochrome-dark", false);
|
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));
|
||||||
}
|
}
|
||||||
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 Icons::icon(const QString& name, bool recolor, const QColor& overrideColor)
|
QIcon Icons::icon(const QString& name, bool recolor, const QColor& overrideColor)
|
||||||
{
|
{
|
||||||
QIcon icon = m_iconCache.value(name);
|
#ifdef Q_OS_LINUX
|
||||||
|
|
||||||
if (!icon.isNull() && !overrideColor.isValid()) {
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resetting the application theme name before calling QIcon::fromTheme() is required for hacky
|
// 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.
|
// 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
|
// If we do not reset the theme name here, it will become empty at some point, causing
|
||||||
@ -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
|
// See issue #4963: https://github.com/keepassxreboot/keepassxc/issues/4963
|
||||||
// and qt5ct issue #80: https://sourceforge.net/p/qt5ct/tickets/80/
|
// and qt5ct issue #80: https://sourceforge.net/p/qt5ct/tickets/80/
|
||||||
QIcon::setThemeName("application");
|
QIcon::setThemeName("application");
|
||||||
|
#endif
|
||||||
|
|
||||||
icon = QIcon::fromTheme(name);
|
QString cacheName =
|
||||||
if (getMainWindow() && recolor) {
|
QString("%1:%2:%3").arg(recolor ? "1" : "0", overrideColor.isValid() ? overrideColor.name() : "#", name);
|
||||||
const QRect rect(0, 0, 48, 48);
|
QIcon icon = m_iconCache.value(cacheName);
|
||||||
QImage img = icon.pixmap(rect.width(), rect.height()).toImage();
|
|
||||||
img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
|
||||||
icon = {};
|
|
||||||
|
|
||||||
QPainter painter(&img);
|
if (!icon.isNull() && !overrideColor.isValid()) {
|
||||||
painter.setCompositionMode(QPainter::CompositionMode_SourceAtop);
|
return icon;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
icon = QIcon::fromTheme(name);
|
||||||
|
if (recolor) {
|
||||||
|
icon = QIcon(new AdaptiveIconEngine(icon));
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
|
||||||
icon.setIsMask(true);
|
icon.setIsMask(true);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!overrideColor.isValid()) {
|
m_iconCache.insert(cacheName, icon);
|
||||||
m_iconCache.insert(name, icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,10 +217,6 @@ QIcon Icons::onOffIcon(const QString& name, bool recolor)
|
|||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
Icons::Icons()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Icons* Icons::instance()
|
Icons* Icons::instance()
|
||||||
{
|
{
|
||||||
if (!m_instance) {
|
if (!m_instance) {
|
||||||
|
@ -28,7 +28,7 @@ class Icons
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
QIcon applicationIcon();
|
QIcon applicationIcon();
|
||||||
QIcon trayIcon();
|
QIcon trayIcon(QString style = "unlocked");
|
||||||
QIcon trayIconLocked();
|
QIcon trayIconLocked();
|
||||||
QIcon trayIconUnlocked();
|
QIcon trayIconUnlocked();
|
||||||
QString trayIconAppearance() const;
|
QString trayIconAppearance() const;
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
#include "gui/Icons.h"
|
#include "gui/Icons.h"
|
||||||
#include "gui/MessageBox.h"
|
#include "gui/MessageBox.h"
|
||||||
#include "gui/SearchWidget.h"
|
#include "gui/SearchWidget.h"
|
||||||
|
#include "gui/osutils/OSUtils.h"
|
||||||
#include "keys/CompositeKey.h"
|
#include "keys/CompositeKey.h"
|
||||||
#include "keys/FileKey.h"
|
#include "keys/FileKey.h"
|
||||||
#include "keys/PasswordKey.h"
|
#include "keys/PasswordKey.h"
|
||||||
@ -503,6 +504,8 @@ MainWindow::MainWindow()
|
|||||||
connect(m_ui->actionOnlineHelp, SIGNAL(triggered()), SLOT(openOnlineHelp()));
|
connect(m_ui->actionOnlineHelp, SIGNAL(triggered()), SLOT(openOnlineHelp()));
|
||||||
connect(m_ui->actionKeyboardShortcuts, SIGNAL(triggered()), SLOT(openKeyboardShortcuts()));
|
connect(m_ui->actionKeyboardShortcuts, SIGNAL(triggered()), SLOT(openKeyboardShortcuts()));
|
||||||
|
|
||||||
|
connect(osUtils, &OSUtilsBase::statusbarThemeChanged, this, &MainWindow::updateTrayIcon);
|
||||||
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||||
// Install event filter for empty-area drag
|
// Install event filter for empty-area drag
|
||||||
auto* eventFilter = new MainWindowEventFilter(this);
|
auto* eventFilter = new MainWindowEventFilter(this);
|
||||||
@ -599,10 +602,10 @@ MainWindow::MainWindow()
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
QObject::connect(qApp, SIGNAL(anotherInstanceStarted()), this, SLOT(bringToFront()));
|
connect(qApp, SIGNAL(anotherInstanceStarted()), this, SLOT(bringToFront()));
|
||||||
QObject::connect(qApp, SIGNAL(applicationActivated()), this, SLOT(bringToFront()));
|
connect(qApp, SIGNAL(applicationActivated()), this, SLOT(bringToFront()));
|
||||||
QObject::connect(qApp, SIGNAL(openFile(QString)), this, SLOT(openDatabase(QString)));
|
connect(qApp, SIGNAL(openFile(QString)), this, SLOT(openDatabase(QString)));
|
||||||
QObject::connect(qApp, SIGNAL(quitSignalReceived()), this, SLOT(appExit()), Qt::DirectConnection);
|
connect(qApp, SIGNAL(quitSignalReceived()), this, SLOT(appExit()), Qt::DirectConnection);
|
||||||
|
|
||||||
restoreConfigState();
|
restoreConfigState();
|
||||||
}
|
}
|
||||||
@ -1757,8 +1760,10 @@ void MainWindow::initViewMenu()
|
|||||||
|
|
||||||
connect(themeActions, &QActionGroup::triggered, this, [this, theme](QAction* action) {
|
connect(themeActions, &QActionGroup::triggered, this, [this, theme](QAction* action) {
|
||||||
config()->set(Config::GUI_ApplicationTheme, action->data());
|
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?"));
|
restartApp(tr("You must restart the application to apply this setting. Would you like to restart now?"));
|
||||||
|
} else {
|
||||||
|
kpxcApp->applyTheme();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -35,6 +35,11 @@ public:
|
|||||||
*/
|
*/
|
||||||
virtual bool isDarkMode() const = 0;
|
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).
|
* @return KeePassXC set to launch at system startup (autostart).
|
||||||
*/
|
*/
|
||||||
@ -61,6 +66,16 @@ public:
|
|||||||
signals:
|
signals:
|
||||||
void globalShortcutTriggered(const QString& name);
|
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:
|
protected:
|
||||||
explicit OSUtilsBase(QObject* parent = nullptr);
|
explicit OSUtilsBase(QObject* parent = nullptr);
|
||||||
virtual ~OSUtilsBase();
|
virtual ~OSUtilsBase();
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
#define KEEPASSX_APPKIT_H
|
#define KEEPASSX_APPKIT_H
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QColor>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
class AppKit : public QObject
|
class AppKit : public QObject
|
||||||
@ -37,12 +38,14 @@ public:
|
|||||||
bool hideProcess(pid_t pid);
|
bool hideProcess(pid_t pid);
|
||||||
bool isHidden(pid_t pid);
|
bool isHidden(pid_t pid);
|
||||||
bool isDarkMode();
|
bool isDarkMode();
|
||||||
|
bool isStatusBarDark();
|
||||||
bool enableAccessibility();
|
bool enableAccessibility();
|
||||||
bool enableScreenRecording();
|
bool enableScreenRecording();
|
||||||
void toggleForegroundApp(bool foreground);
|
void toggleForegroundApp(bool foreground);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void lockDatabases();
|
void lockDatabases();
|
||||||
|
void interfaceThemeChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void* self;
|
void* self;
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
- (bool) hideProcess:(pid_t) pid;
|
- (bool) hideProcess:(pid_t) pid;
|
||||||
- (bool) isHidden:(pid_t) pid;
|
- (bool) isHidden:(pid_t) pid;
|
||||||
- (bool) isDarkMode;
|
- (bool) isDarkMode;
|
||||||
|
- (bool) isStatusBarDark;
|
||||||
- (void) userSwitchHandler:(NSNotification*) notification;
|
- (void) userSwitchHandler:(NSNotification*) notification;
|
||||||
- (bool) enableAccessibility;
|
- (bool) enableAccessibility;
|
||||||
- (bool) enableScreenRecording;
|
- (bool) enableScreenRecording;
|
||||||
|
@ -17,7 +17,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#import "AppKitImpl.h"
|
#import "AppKitImpl.h"
|
||||||
|
#include "AppKit.h"
|
||||||
|
|
||||||
|
#import <AppKit/NSStatusBar.h>
|
||||||
|
#import <AppKit/NSStatusItem.h>
|
||||||
|
#import <AppKit/NSStatusBarButton.h>
|
||||||
#import <AppKit/NSWorkspace.h>
|
#import <AppKit/NSWorkspace.h>
|
||||||
#import <CoreVideo/CVPixelBuffer.h>
|
#import <CoreVideo/CVPixelBuffer.h>
|
||||||
|
|
||||||
@ -26,17 +30,31 @@
|
|||||||
- (id) initWithObject:(AppKit*)appkit
|
- (id) initWithObject:(AppKit*)appkit
|
||||||
{
|
{
|
||||||
self = [super init];
|
self = [super init];
|
||||||
|
|
||||||
if (self) {
|
if (self) {
|
||||||
m_appkit = appkit;
|
m_appkit = appkit;
|
||||||
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:static_cast<id>(self)
|
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
|
||||||
selector:@selector(didDeactivateApplicationObserver:)
|
selector:@selector(didDeactivateApplicationObserver:)
|
||||||
name:NSWorkspaceDidDeactivateApplicationNotification
|
name:NSWorkspaceDidDeactivateApplicationNotification
|
||||||
object:nil];
|
object:nil];
|
||||||
|
|
||||||
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:static_cast<id>(self)
|
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
|
||||||
selector:@selector(userSwitchHandler:)
|
selector:@selector(userSwitchHandler:)
|
||||||
name:NSWorkspaceSessionDidResignActiveNotification
|
name:NSWorkspaceSessionDidResignActiveNotification
|
||||||
object:nil];
|
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;
|
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)
|
// Get process id of frontmost application (-> keyboard input)
|
||||||
//
|
//
|
||||||
@ -108,6 +138,23 @@
|
|||||||
&& NSOrderedSame == [style caseInsensitiveCompare:@"dark"] );
|
&& 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
|
// Notification for user switch
|
||||||
//
|
//
|
||||||
@ -170,7 +217,8 @@
|
|||||||
// ------------------------- C++ Trampolines -------------------------
|
// ------------------------- C++ Trampolines -------------------------
|
||||||
//
|
//
|
||||||
|
|
||||||
AppKit::AppKit(QObject* parent) : QObject(parent)
|
AppKit::AppKit(QObject* parent)
|
||||||
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
self = [[AppKitImpl alloc] initWithObject:this];
|
self = [[AppKitImpl alloc] initWithObject:this];
|
||||||
}
|
}
|
||||||
@ -178,6 +226,7 @@ AppKit::AppKit(QObject* parent) : QObject(parent)
|
|||||||
AppKit::~AppKit()
|
AppKit::~AppKit()
|
||||||
{
|
{
|
||||||
[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:static_cast<id>(self)];
|
[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:static_cast<id>(self)];
|
||||||
|
[[NSDistributedNotificationCenter defaultCenter] removeObserver:static_cast<id>(self)];
|
||||||
[static_cast<id>(self) dealloc];
|
[static_cast<id>(self) dealloc];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,6 +265,12 @@ bool AppKit::isDarkMode()
|
|||||||
return [static_cast<id>(self) isDarkMode];
|
return [static_cast<id>(self) isDarkMode];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AppKit::isStatusBarDark()
|
||||||
|
{
|
||||||
|
return [static_cast<id>(self) isStatusBarDark];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool AppKit::enableAccessibility()
|
bool AppKit::enableAccessibility()
|
||||||
{
|
{
|
||||||
return [static_cast<id>(self) enableAccessibility];
|
return [static_cast<id>(self) enableAccessibility];
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
#include <ApplicationServices/ApplicationServices.h>
|
#include <ApplicationServices/ApplicationServices.h>
|
||||||
#include <CoreGraphics/CGEventSource.h>
|
#include <CoreGraphics/CGEventSource.h>
|
||||||
@ -35,6 +36,14 @@ MacUtils::MacUtils(QObject* parent)
|
|||||||
, m_appkit(new AppKit())
|
, m_appkit(new AppKit())
|
||||||
{
|
{
|
||||||
connect(m_appkit.data(), SIGNAL(lockDatabases()), SIGNAL(lockDatabases()));
|
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()
|
MacUtils::~MacUtils()
|
||||||
@ -95,6 +104,11 @@ bool MacUtils::isDarkMode() const
|
|||||||
return m_appkit->isDarkMode();
|
return m_appkit->isDarkMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MacUtils::isStatusBarDark() const
|
||||||
|
{
|
||||||
|
return m_appkit->isStatusBarDark();
|
||||||
|
}
|
||||||
|
|
||||||
QString MacUtils::getLaunchAgentFilename() const
|
QString MacUtils::getLaunchAgentFilename() const
|
||||||
{
|
{
|
||||||
auto launchAgentDir =
|
auto launchAgentDir =
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
#include "gui/osutils/OSUtilsBase.h"
|
#include "gui/osutils/OSUtilsBase.h"
|
||||||
#include <Carbon/Carbon.h>
|
#include <Carbon/Carbon.h>
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include <QScopedPointer>
|
#include <QScopedPointer>
|
||||||
#include <qwindowdefs.h>
|
#include <qwindowdefs.h>
|
||||||
@ -35,6 +36,7 @@ public:
|
|||||||
static MacUtils* instance();
|
static MacUtils* instance();
|
||||||
|
|
||||||
bool isDarkMode() const override;
|
bool isDarkMode() const override;
|
||||||
|
bool isStatusBarDark() const override;
|
||||||
bool isLaunchAtStartupEnabled() const override;
|
bool isLaunchAtStartupEnabled() const override;
|
||||||
void setLaunchAtStartup(bool enable) override;
|
void setLaunchAtStartup(bool enable) override;
|
||||||
bool isCapslockEnabled() override;
|
bool isCapslockEnabled() override;
|
||||||
|
@ -79,6 +79,12 @@ bool NixUtils::isDarkMode() const
|
|||||||
return qApp->style()->standardPalette().color(QPalette::Window).toHsl().lightness() < 110;
|
return qApp->style()->standardPalette().color(QPalette::Window).toHsl().lightness() < 110;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NixUtils::isStatusBarDark() const
|
||||||
|
{
|
||||||
|
// TODO: implement
|
||||||
|
return isDarkMode();
|
||||||
|
}
|
||||||
|
|
||||||
QString NixUtils::getAutostartDesktopFilename(bool createDirs) const
|
QString NixUtils::getAutostartDesktopFilename(bool createDirs) const
|
||||||
{
|
{
|
||||||
QDir autostartDir;
|
QDir autostartDir;
|
||||||
|
@ -30,6 +30,7 @@ public:
|
|||||||
static NixUtils* instance();
|
static NixUtils* instance();
|
||||||
|
|
||||||
bool isDarkMode() const override;
|
bool isDarkMode() const override;
|
||||||
|
bool isStatusBarDark() const override;
|
||||||
bool isLaunchAtStartupEnabled() const override;
|
bool isLaunchAtStartupEnabled() const override;
|
||||||
void setLaunchAtStartup(bool enable) override;
|
void setLaunchAtStartup(bool enable) override;
|
||||||
bool isCapslockEnabled() override;
|
bool isCapslockEnabled() override;
|
||||||
|
@ -87,6 +87,12 @@ bool WinUtils::isDarkMode() const
|
|||||||
return settings.value("AppsUseLightTheme", 1).toInt() == 0;
|
return settings.value("AppsUseLightTheme", 1).toInt() == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool WinUtils::isStatusBarDark() const
|
||||||
|
{
|
||||||
|
// TODO: implement
|
||||||
|
return isDarkMode();
|
||||||
|
}
|
||||||
|
|
||||||
bool WinUtils::isLaunchAtStartupEnabled() const
|
bool WinUtils::isLaunchAtStartupEnabled() const
|
||||||
{
|
{
|
||||||
return QSettings(R"(HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run)", QSettings::NativeFormat)
|
return QSettings(R"(HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run)", QSettings::NativeFormat)
|
||||||
|
@ -35,6 +35,7 @@ public:
|
|||||||
static WinUtils* instance();
|
static WinUtils* instance();
|
||||||
|
|
||||||
bool isDarkMode() const override;
|
bool isDarkMode() const override;
|
||||||
|
bool isStatusBarDark() const override;
|
||||||
bool isLaunchAtStartupEnabled() const override;
|
bool isLaunchAtStartupEnabled() const override;
|
||||||
void setLaunchAtStartup(bool enable) override;
|
void setLaunchAtStartup(bool enable) override;
|
||||||
bool isCapslockEnabled() override;
|
bool isCapslockEnabled() override;
|
||||||
|
@ -52,6 +52,10 @@
|
|||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
#include <qdrawutil.h>
|
#include <qdrawutil.h>
|
||||||
|
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
|
#include <QOperatingSystemVersion>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
#include "gui/Icons.h"
|
#include "gui/Icons.h"
|
||||||
@ -288,10 +292,16 @@ namespace Phantom
|
|||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
QColor tabBarBase(const QPalette& pal)
|
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)
|
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);
|
return hack_isLightPalette(pal) ? QRgb(0xF4F4F4) : QRgb(0x282828);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -4571,27 +4581,6 @@ QStyle::SubControl BaseStyle::hitTestComplexControl(ComplexControl cc,
|
|||||||
return QCommonStyle::hitTestComplexControl(cc, opt, pt, w);
|
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,
|
int BaseStyle::styleHint(StyleHint hint,
|
||||||
const QStyleOption* option,
|
const QStyleOption* option,
|
||||||
const QWidget* widget,
|
const QWidget* widget,
|
||||||
|
@ -70,7 +70,6 @@ public:
|
|||||||
const QStyleOptionComplex* opt,
|
const QStyleOptionComplex* opt,
|
||||||
SubControl sc,
|
SubControl sc,
|
||||||
const QWidget* widget) const override;
|
const QWidget* widget) const override;
|
||||||
QPixmap generatedIconPixmap(QIcon::Mode iconMode, const QPixmap& pixmap, const QStyleOption* opt) const override;
|
|
||||||
int styleHint(StyleHint hint,
|
int styleHint(StyleHint hint,
|
||||||
const QStyleOption* option = nullptr,
|
const QStyleOption* option = nullptr,
|
||||||
const QWidget* widget = nullptr,
|
const QWidget* widget = nullptr,
|
||||||
|
@ -114,9 +114,9 @@ void DarkStyle::polish(QWidget* widget)
|
|||||||
auto palette = widget->palette();
|
auto palette = widget->palette();
|
||||||
#if defined(Q_OS_MACOS)
|
#if defined(Q_OS_MACOS)
|
||||||
if (!osUtils->isDarkMode()) {
|
if (!osUtils->isDarkMode()) {
|
||||||
palette.setColor(QPalette::Active, QPalette::Window, QRgb(0x252525));
|
palette.setColor(QPalette::Active, QPalette::Window, QRgb(0x2A2A2A));
|
||||||
palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0x282828));
|
palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0x2D2D2D));
|
||||||
palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0x252525));
|
palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0x2D2D2D));
|
||||||
}
|
}
|
||||||
#elif defined(Q_OS_WIN)
|
#elif defined(Q_OS_WIN)
|
||||||
palette.setColor(QPalette::All, QPalette::Window, QRgb(0x2F2F30));
|
palette.setColor(QPalette::All, QPalette::Window, QRgb(0x2F2F30));
|
||||||
|
@ -115,9 +115,9 @@ void LightStyle::polish(QWidget* widget)
|
|||||||
auto palette = widget->palette();
|
auto palette = widget->palette();
|
||||||
#if defined(Q_OS_MACOS)
|
#if defined(Q_OS_MACOS)
|
||||||
if (osUtils->isDarkMode()) {
|
if (osUtils->isDarkMode()) {
|
||||||
palette.setColor(QPalette::Active, QPalette::Window, QRgb(0xD1D1D1));
|
palette.setColor(QPalette::Active, QPalette::Window, QRgb(0xD4D4D4));
|
||||||
palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0xF4F4F4));
|
palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0xF5F5F5));
|
||||||
palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0xD1D1D1));
|
palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0xF5F5F5));
|
||||||
}
|
}
|
||||||
#elif defined(Q_OS_WIN)
|
#elif defined(Q_OS_WIN)
|
||||||
palette.setColor(QPalette::All, QPalette::Window, QRgb(0xFFFFFF));
|
palette.setColor(QPalette::All, QPalette::Window, QRgb(0xFFFFFF));
|
||||||
|