diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bc63b5ee9..5214242b2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -87,6 +87,11 @@ set(keepassx_SOURCES format/OpVaultReaderAttachments.cpp format/OpVaultReaderBandEntry.cpp format/OpVaultReaderSections.cpp + gui/styles/styles.qrc + gui/styles/base/phantomcolor.cpp + gui/styles/base/BaseStyle.cpp + gui/styles/dark/DarkStyle.cpp + gui/styles/light/LightStyle.cpp gui/AboutDialog.cpp gui/Application.cpp gui/CategoryListWidget.cpp diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 445e587f8..41395214d 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -232,6 +232,7 @@ void Config::init(const QString& fileName) m_defaults.insert("GUI/HidePasswords", true); m_defaults.insert("GUI/AdvancedSettings", false); m_defaults.insert("GUI/MonospaceNotes", false); + m_defaults.insert("GUI/ApplicationTheme", "auto"); } Config* Config::instance() diff --git a/src/core/FilePath.cpp b/src/core/FilePath.cpp index 62db3929d..95e7fc0c2 100644 --- a/src/core/FilePath.cpp +++ b/src/core/FilePath.cpp @@ -18,13 +18,15 @@ #include "FilePath.h" -#include +#include #include #include +#include #include "config-keepassx.h" #include "core/Config.h" #include "core/Global.h" +#include "gui/MainWindow.h" FilePath* FilePath::m_instance(nullptr); @@ -98,7 +100,7 @@ QIcon FilePath::applicationIcon() #ifdef KEEPASSXC_DIST_SNAP return icon("apps", "keepassxc", false); #else - return icon("apps", "keepassxc"); + return icon("apps", "keepassxc", false); #endif } @@ -109,7 +111,7 @@ QIcon FilePath::trayIcon() #ifdef KEEPASSXC_DIST_SNAP return (darkIcon) ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc", false); #else - return (darkIcon) ? icon("apps", "keepassxc-dark") : icon("apps", "keepassxc"); + return (darkIcon) ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc", false); #endif } @@ -118,7 +120,7 @@ QIcon FilePath::trayIconLocked() #ifdef KEEPASSXC_DIST_SNAP return icon("apps", "keepassxc-locked", false); #else - return icon("apps", "keepassxc-locked"); + return icon("apps", "keepassxc-locked", false); #endif } @@ -129,14 +131,13 @@ QIcon FilePath::trayIconUnlocked() #ifdef KEEPASSXC_DIST_SNAP return darkIcon ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc-unlocked", false); #else - return darkIcon ? icon("apps", "keepassxc-dark") : icon("apps", "keepassxc-unlocked"); + return darkIcon ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc-unlocked", false); #endif } -QIcon FilePath::icon(const QString& category, const QString& name) +QIcon FilePath::icon(const QString& category, const QString& name, bool recolor) { QString combinedName = category + "/" + name; - QIcon icon = m_iconCache.value(combinedName); if (!icon.isNull()) { @@ -154,7 +155,30 @@ QIcon FilePath::icon(const QString& category, const QString& name) } } filename = QString("%1/icons/application/scalable/%2.svg").arg(m_dataPath, combinedName); - if (QFile::exists(filename)) { + if (QFile::exists(filename) && getMainWindow() && recolor) { + QPalette palette = getMainWindow()->palette(); + + QFile f(filename); + QIcon scalable(filename); + QPixmap pixmap = scalable.pixmap({128, 128}); + + auto mask = QBitmap::fromImage(pixmap.toImage().createAlphaMask()); + pixmap.fill(palette.color(QPalette::WindowText)); + pixmap.setMask(mask); + icon.addPixmap(pixmap, QIcon::Mode::Normal); + + pixmap.fill(palette.color(QPalette::HighlightedText)); + pixmap.setMask(mask); + icon.addPixmap(pixmap, QIcon::Mode::Selected); + + pixmap.fill(palette.color(QPalette::Disabled, QPalette::WindowText)); + pixmap.setMask(mask); + icon.addPixmap(pixmap, QIcon::Mode::Disabled); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) + icon.setIsMask(true); +#endif + } else if (QFile::exists(filename)) { icon.addFile(filename); } } @@ -164,7 +188,7 @@ QIcon FilePath::icon(const QString& category, const QString& name) return icon; } -QIcon FilePath::onOffIcon(const QString& category, const QString& name) +QIcon FilePath::onOffIcon(const QString& category, const QString& name, bool recolor) { QString combinedName = category + "/" + name; QString cacheName = "onoff/" + combinedName; @@ -175,31 +199,17 @@ QIcon FilePath::onOffIcon(const QString& category, const QString& name) return icon; } - for (int i = 0; i < 2; i++) { - QIcon::State state; - QString stateName; - - if (i == 0) { - state = QIcon::Off; - stateName = "off"; - } else { - state = QIcon::On; - stateName = "on"; - } - - const QList pngSizes = {16, 22, 24, 32, 48, 64, 128}; - QString filename; - for (int size : pngSizes) { - filename = QString("%1/icons/application/%2x%2/%3-%4.png") - .arg(m_dataPath, QString::number(size), combinedName, stateName); - if (QFile::exists(filename)) { - icon.addFile(filename, QSize(size, size), QIcon::Normal, state); - } - } - filename = QString("%1/icons/application/scalable/%2-%3.svg").arg(m_dataPath, combinedName, stateName); - if (QFile::exists(filename)) { - icon.addFile(filename, QSize(), QIcon::Normal, state); - } + QIcon on = FilePath::icon(category, name + "-on", recolor); + for (const auto& size : on.availableSizes()) { + 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 = FilePath::icon(category, name + "-off", recolor); + for (const auto& size : off.availableSizes()) { + 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); diff --git a/src/core/FilePath.h b/src/core/FilePath.h index ceb958237..008dfc33e 100644 --- a/src/core/FilePath.h +++ b/src/core/FilePath.h @@ -32,8 +32,8 @@ public: QIcon trayIcon(); QIcon trayIconLocked(); QIcon trayIconUnlocked(); - QIcon icon(const QString& category, const QString& name); - QIcon onOffIcon(const QString& category, const QString& name); + QIcon icon(const QString& category, const QString& name, bool recolor = true); + QIcon onOffIcon(const QString& category, const QString& name, bool recolor = true); static FilePath* instance(); diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index d3cc994f8..b17c44ecc 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -213,6 +213,14 @@ void ApplicationSettingsWidget::loadSettings() m_generalUi->toolbarMovableCheckBox->setChecked(config()->get("GUI/MovableToolbar").toBool()); m_generalUi->monospaceNotesCheckBox->setChecked(config()->get("GUI/MonospaceNotes").toBool()); + m_generalUi->appThemeSelection->clear(); + m_generalUi->appThemeSelection->addItem(tr("Automatic"), QStringLiteral("auto")); + m_generalUi->appThemeSelection->addItem(tr("Light"), QStringLiteral("light")); + m_generalUi->appThemeSelection->addItem(tr("Dark"), QStringLiteral("dark")); + m_generalUi->appThemeSelection->addItem(tr("Classic (Platform-native)"), QStringLiteral("classic")); + m_generalUi->appThemeSelection->setCurrentIndex( + m_generalUi->appThemeSelection->findData(config()->get("GUI/ApplicationTheme").toString())); + m_generalUi->toolButtonStyleComboBox->clear(); m_generalUi->toolButtonStyleComboBox->addItem(tr("Icon only"), Qt::ToolButtonIconOnly); m_generalUi->toolButtonStyleComboBox->addItem(tr("Text only"), Qt::ToolButtonTextOnly); @@ -303,19 +311,18 @@ void ApplicationSettingsWidget::saveSettings() config()->set("IgnoreGroupExpansion", m_generalUi->ignoreGroupExpansionCheckBox->isChecked()); config()->set("AutoTypeEntryTitleMatch", m_generalUi->autoTypeEntryTitleMatchCheckBox->isChecked()); config()->set("AutoTypeEntryURLMatch", m_generalUi->autoTypeEntryURLMatchCheckBox->isChecked()); - int currentLangIndex = m_generalUi->languageComboBox->currentIndex(); config()->set("FaviconDownloadTimeout", m_generalUi->faviconTimeoutSpinBox->value()); - config()->set("GUI/Language", m_generalUi->languageComboBox->itemData(currentLangIndex).toString()); - + config()->set("GUI/Language", m_generalUi->languageComboBox->currentData().toString()); config()->set("GUI/HidePreviewPanel", m_generalUi->previewHideCheckBox->isChecked()); config()->set("GUI/HideToolbar", m_generalUi->toolbarHideCheckBox->isChecked()); config()->set("GUI/MovableToolbar", m_generalUi->toolbarMovableCheckBox->isChecked()); config()->set("GUI/MonospaceNotes", m_generalUi->monospaceNotesCheckBox->isChecked()); - int currentToolButtonStyleIndex = m_generalUi->toolButtonStyleComboBox->currentIndex(); - config()->set("GUI/ToolButtonStyle", - m_generalUi->toolButtonStyleComboBox->itemData(currentToolButtonStyleIndex).toString()); + QString theme = m_generalUi->appThemeSelection->currentData().toString(); + config()->set("GUI/ApplicationTheme", theme); + + config()->set("GUI/ToolButtonStyle", m_generalUi->toolButtonStyleComboBox->currentData().toString()); config()->set("GUI/ShowTrayIcon", m_generalUi->systrayShowCheckBox->isChecked()); config()->set("GUI/DarkTrayIcon", m_generalUi->systrayDarkIconCheckBox->isChecked()); diff --git a/src/gui/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui index fa4da2acc..d286a8973 100644 --- a/src/gui/ApplicationSettingsWidgetGeneral.ui +++ b/src/gui/ApplicationSettingsWidgetGeneral.ui @@ -6,8 +6,8 @@ 0 0 - 684 - 951 + 499 + 1174 @@ -337,7 +337,7 @@ - + @@ -350,12 +350,6 @@ true - - - 0 - 0 - - Qt::StrongFocus @@ -400,6 +394,50 @@ General + + + + + + Application Theme: + + + + + + + + 0 + 0 + + + + Application Theme Selection + + + + + + + (restart program to activate) + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -491,9 +529,6 @@ - - true - 0 @@ -726,7 +761,7 @@ - Reset Settings to Default + Reset settings to default… diff --git a/src/gui/CategoryListWidget.cpp b/src/gui/CategoryListWidget.cpp index c57b19bc0..48ccb4783 100644 --- a/src/gui/CategoryListWidget.cpp +++ b/src/gui/CategoryListWidget.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -158,9 +159,7 @@ CategoryListWidgetDelegate::CategoryListWidgetDelegate(QListWidget* parent) } } -#ifdef Q_OS_WIN -#include -class WindowsCorrectedStyle : public QProxyStyle +class IconSelectionCorrectedStyle : public QProxyStyle { public: void drawPrimitive(PrimitiveElement element, @@ -171,8 +170,8 @@ public: painter->save(); if (PE_PanelItemViewItem == element) { - // Qt on Windows draws selection backgrounds only for the actual text/icon - // bounding box, not over the full width of a list item. + // Qt on Windows and the Fusion/Phantom base styles draw selection backgrounds only for + // the actual text/icon bounding box, not over the full width of a list item. // We therefore need to translate and stretch the painter before we can // tell Qt to draw its native styles. // Since we are scaling horizontally, we also need to move the right and left @@ -186,7 +185,6 @@ public: painter->restore(); } }; -#endif void CategoryListWidgetDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, @@ -203,12 +201,7 @@ void CategoryListWidgetDelegate::paint(QPainter* painter, opt.decorationAlignment = Qt::AlignHCenter | Qt::AlignVCenter; opt.decorationPosition = QStyleOptionViewItem::Top; -#ifdef Q_OS_WIN - QScopedPointer style(new WindowsCorrectedStyle()); -#else - QStyle* style = opt.widget ? opt.widget->style() : QApplication::style(); -#endif - + QScopedPointer style(new IconSelectionCorrectedStyle()); style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget); QRect fontRect = painter->fontMetrics().boundingRect( diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index c610a773d..9559047a9 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -90,12 +90,6 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) m_ui->yubikeyProgress->setVisible(false); #endif -#ifdef Q_OS_MACOS - // add random padding to layouts to align widgets properly - m_ui->dialogButtonsLayout->setContentsMargins(10, 0, 15, 0); - m_ui->gridLayout->setContentsMargins(10, 0, 0, 0); -#endif - #ifndef WITH_XC_TOUCHID m_ui->touchIDContainer->setVisible(false); #else diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui index 60b2feadc..f2cd96b6a 100644 --- a/src/gui/DatabaseOpenWidget.ui +++ b/src/gui/DatabaseOpenWidget.ui @@ -99,7 +99,7 @@ - + 550 @@ -235,142 +235,6 @@ 3 - - - - 0 - - - - - - 16777215 - 2 - - - - 0 - - - 0 - - - -1 - - - false - - - - - - - false - - - - 0 - 0 - - - - Hardware key slot selection - - - false - - - - - - - - - Browse for key file - - - Browse for key file - - - Browse... - - - - - - - 0 - - - - - true - - - - 0 - 0 - - - - Key file selection - - - true - - - - - - - - - 5 - - - - - Hardware Key: - - - comboChallengeResponse - - - - - - - PointingHandCursor - - - <p>You can use a hardware security key such as a <strong>YubiKey</strong> or <strong>OnlyKey</strong> with slots configured for HMAC-SHA1.</p> -<p>Click for more information...</p> - - - Hardware key help - - - QToolButton { - border: none; - background: none; -} - - - ? - - - - 12 - 12 - - - - QToolButton::InstantPopup - - - - - @@ -419,22 +283,204 @@ - - - - true + + + + 0 + + + + + 16777215 + 2 + + + + 0 + + + 0 + + + -1 + + + false + + + + + + + false + + + + 0 + 0 + + + + Hardware key slot selection + + + false + + + + + + + + + 2 + + + + + 5 + + + + + Hardware Key: + + + comboChallengeResponse + + + + + + + PointingHandCursor + + + <p>You can use a hardware security key such as a <strong>YubiKey</strong> or <strong>OnlyKey</strong> with slots configured for HMAC-SHA1.</p> +<p>Click for more information...</p> + + + Hardware key help + + + QToolButton { + border: none; + background: none; +} + + + ? + + + + 12 + 12 + + + + QToolButton::InstantPopup + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 2 + + + + + + + + + + 0 + + + + + true + + + + 0 + 0 + + + + Key file selection + + + true + + + + + + + - Refresh hardware tokens + Browse for key file - Refresh hardware tokens + Browse for key file - Refresh + Browse... + + + + 0 + + + + + true + + + Refresh hardware tokens + + + Refresh hardware tokens + + + Refresh + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 2 + + + + + + @@ -572,7 +618,6 @@ buttonBrowseFile hardwareKeyLabelHelp comboChallengeResponse - buttonRedetectYubikey checkTouchID diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index c867526ef..5523f7c62 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -80,8 +80,10 @@ void DatabaseTabWidget::toggleTabbar() { if (count() > 1) { tabBar()->show(); + emit tabVisibilityChanged(true); } else { tabBar()->hide(); + emit tabVisibilityChanged(false); } } diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index 29019a2d2..5b2d3f008 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -89,6 +89,7 @@ signals: void databaseLocked(DatabaseWidget* dbWidget); void activateDatabaseChanged(DatabaseWidget* dbWidget); void tabNameChanged(); + void tabVisibilityChanged(bool tabsVisible); void messageGlobal(const QString&, MessageWidget::MessageType type); void messageDismissGlobal(); void databaseUnlockDialogFinished(bool accepted, DatabaseWidget* dbWidget); diff --git a/src/gui/DatabaseWidgetStateSync.cpp b/src/gui/DatabaseWidgetStateSync.cpp index 5579b30cd..df0e43ebc 100644 --- a/src/gui/DatabaseWidgetStateSync.cpp +++ b/src/gui/DatabaseWidgetStateSync.cpp @@ -101,9 +101,7 @@ void DatabaseWidgetStateSync::setActive(DatabaseWidget* dbWidget) * * NOTE: * If m_listViewState is empty, the list view has been activated for the first - * time after starting with a clean (or invalid) config. Thus, save the current - * state. Without this, m_listViewState would remain empty until there is an - * actual view state change (e.g. column is resized) + * time after starting with a clean (or invalid) config. */ void DatabaseWidgetStateSync::restoreListView() { @@ -112,8 +110,6 @@ void DatabaseWidgetStateSync::restoreListView() if (!m_listViewState.isEmpty()) { m_activeDbWidget->setEntryViewState(m_listViewState); - } else { - m_listViewState = m_activeDbWidget->entryViewState(); } m_blockUpdates = false; diff --git a/src/gui/DialogyWidget.cpp b/src/gui/DialogyWidget.cpp index 597bcc59d..070393933 100644 --- a/src/gui/DialogyWidget.cpp +++ b/src/gui/DialogyWidget.cpp @@ -24,6 +24,7 @@ DialogyWidget::DialogyWidget(QWidget* parent) : QWidget(parent) { + setAutoFillBackground(true); } void DialogyWidget::keyPressEvent(QKeyEvent* e) diff --git a/src/gui/KMessageWidget.cpp b/src/gui/KMessageWidget.cpp index 8df7b6384..01925b7dd 100644 --- a/src/gui/KMessageWidget.cpp +++ b/src/gui/KMessageWidget.cpp @@ -102,12 +102,6 @@ void KMessageWidgetPrivate::init(KMessageWidget *q_ptr) closeButton->setAutoRaise(true); closeButton->setDefaultAction(closeAction); closeButtonPixmap = QPixmap(closeButton->icon().pixmap(closeButton->icon().actualSize(QSize(16, 16)))); -#ifdef Q_OS_MACOS - closeButton->setStyleSheet("QToolButton { background: transparent;" - "border-radius: 2px; padding: 3px; }" - "QToolButton::hover, QToolButton::focus {" - "border: 1px solid rgb(90, 200, 250); }"); -#endif q->setMessageType(KMessageWidget::Information); } @@ -263,7 +257,7 @@ void KMessageWidget::setMessageType(KMessageWidget::MessageType type) { d->messageType = type; QColor bg0, bg1, bg2, border; - QColor fg = palette().light().color(); + QColor fg = QColor(238, 238, 238); switch (type) { case Positive: bg1.setRgb(37, 163, 83); @@ -273,7 +267,7 @@ void KMessageWidget::setMessageType(KMessageWidget::MessageType type) break; case Warning: bg1.setRgb(252, 193, 57); - fg = palette().windowText().color(); + fg = QColor(48, 48, 48); break; case Error: bg1.setRgb(198, 69, 21); @@ -294,9 +288,15 @@ void KMessageWidget::setMessageType(KMessageWidget::MessageType type) painter.fillRect(QRect(0, 0, 16, 16), fg); painter.end(); d->closeButton->setIcon(closeButtonPixmap); + d->closeButton->setStyleSheet(QStringLiteral("QToolButton {" + " background: transparent;" + " border-radius: 2px;" + " border: none; }" + "QToolButton:hover, QToolButton:focus {" + " border: 1px solid %1; }").arg(fg.name())); d->content->setStyleSheet( - QString(QLatin1String(".QFrame {" + QStringLiteral(".QFrame {" "background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1," " stop: 0 %1," " stop: 0.1 %2," @@ -307,7 +307,7 @@ void KMessageWidget::setMessageType(KMessageWidget::MessageType type) " padding: 5px;" "}" ".QLabel { color: %6; }" - )) + ) .arg(bg0.name(), bg1.name(), bg2.name(), diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 07649d244..3db3e98f6 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -236,6 +236,9 @@ MainWindow::MainWindow() autoType()->registerGlobalShortcut(globalAutoTypeKey, globalAutoTypeModifiers); } + m_ui->toolbarSeparator->setVisible(false); + m_showToolbarSeparator = config()->get("GUI/ApplicationTheme").toString() != "classic"; + m_ui->actionEntryAutoType->setVisible(autoType()->isAvailable()); m_inactivityTimer = new InactivityTimer(this); @@ -389,6 +392,7 @@ MainWindow::MainWindow() connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), m_searchWidget, SLOT(databaseChanged())); connect(m_ui->tabWidget, SIGNAL(tabNameChanged()), SLOT(updateWindowTitle())); + connect(m_ui->tabWidget, SIGNAL(tabVisibilityChanged(bool)), SLOT(adjustToTabVisibilityChange(bool))); connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(updateWindowTitle())); connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(databaseTabChanged(int))); connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(setMenuActionState())); @@ -618,14 +622,18 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) bool inDatabaseTabWidgetOrWelcomeWidget = inDatabaseTabWidget || inWelcomeWidget; m_ui->actionDatabaseMerge->setEnabled(inDatabaseTabWidget); - m_ui->actionDatabaseNew->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->menuImport->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->actionLockDatabases->setEnabled(m_ui->tabWidget->hasLockableDatabases()); + if (m_showToolbarSeparator) { + m_ui->toolbarSeparator->setVisible( + (!inWelcomeWidget && inDatabaseTabWidget && !m_ui->tabWidget->tabBar()->isVisible()) + || currentIndex == SettingsScreen); + } + if (inDatabaseTabWidget && m_ui->tabWidget->currentIndex() != -1) { DatabaseWidget* dbWidget = m_ui->tabWidget->currentDatabaseWidget(); Q_ASSERT(dbWidget); @@ -776,6 +784,13 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) } } +void MainWindow::adjustToTabVisibilityChange(bool tabsVisible) +{ + if (m_showToolbarSeparator) { + m_ui->toolbarSeparator->setVisible(!tabsVisible && m_ui->stackedWidget->currentIndex() == DatabaseTabScreen); + } +} + void MainWindow::updateWindowTitle() { QString customWindowTitlePart; @@ -1125,7 +1140,7 @@ void MainWindow::updateTrayIcon() QAction* actionToggle = new QAction(tr("Toggle window"), menu); menu->addAction(actionToggle); - actionToggle->setIcon(filePath()->icon("apps", "keepassxc-dark")); + actionToggle->setIcon(filePath()->icon("apps", "keepassxc-dark", false)); menu->addAction(m_ui->actionLockDatabases); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 83a504f82..5e2bfef80 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -86,6 +86,7 @@ protected: private slots: void setMenuActionState(DatabaseWidget::Mode mode = DatabaseWidget::Mode::None); + void adjustToTabVisibilityChange(bool tabsVisible); void updateWindowTitle(); void showAboutDialog(); void showUpdateCheckStartup(); @@ -167,6 +168,7 @@ private: bool m_appExitCalled = false; bool m_appExiting = false; bool m_contextMenuFocusLock = false; + bool m_showToolbarSeparator = false; qint64 m_lastFocusOutTime = 0; qint64 m_lastShowTime = 0; QTimer m_trayIconTriggerTimer; diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index aec0efb37..41792986b 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -61,6 +61,16 @@ + + + + QFrame::Plain + + + Qt::Horizontal + + + @@ -195,7 +205,7 @@ 0 0 800 - 21 + 24 @@ -210,7 +220,7 @@ - &Recent databases + &Recent Databases @@ -249,7 +259,6 @@ &Help - @@ -259,10 +268,11 @@ + - E&ntries + &Entries @@ -272,7 +282,7 @@ - Copy att&ribute... + Copy Att&ribute @@ -284,7 +294,7 @@ false - TOTP... + TOTP @@ -311,7 +321,6 @@ &Groups - @@ -347,8 +356,8 @@ - 32 - 32 + 26 + 26 @@ -393,7 +402,7 @@ - &Check for Updates... + &Check for Updates QAction::ApplicationSpecificRole @@ -401,7 +410,7 @@ - &Open database... + &Open Database… @@ -409,7 +418,7 @@ false - &Save database + &Save Database @@ -417,12 +426,12 @@ false - &Close database + &Close Database - &New database... + &New Database… Create a new database @@ -430,7 +439,7 @@ - &Merge from database... + &Merge From Database… Merge from another KDBX database @@ -441,7 +450,7 @@ false - &New entry + &New Entry… Add a new entry @@ -452,7 +461,7 @@ false - &Edit entry + &Edit Entry… View or edit entry @@ -463,7 +472,7 @@ false - &Delete entry + &Delete Entry… @@ -471,7 +480,7 @@ false - &New group + &New Group… Add a new group @@ -482,7 +491,7 @@ false - &Edit group + &Edit Group… @@ -490,7 +499,7 @@ false - &Delete group + &Delete Group… @@ -498,7 +507,7 @@ false - Downlo&ad all favicons + Download All &Favicons… @@ -522,7 +531,7 @@ false - Sa&ve database as... + Sa&ve Database As… @@ -530,7 +539,7 @@ false - Change master &key... + Change Master &Key… @@ -552,7 +561,7 @@ false - &Database settings... + &Database Settings… Database settings @@ -566,7 +575,7 @@ false - &Clone entry + &Clone Entry… @@ -574,7 +583,7 @@ false - Copy &username + Copy &Username Copy username to clipboard @@ -585,7 +594,7 @@ false - Copy &password + Copy &Password Copy password to clipboard @@ -620,7 +629,7 @@ - Download favicon + Download &Favicon @@ -636,7 +645,7 @@ false - &Lock databases + &Lock Databases @@ -677,7 +686,7 @@ false - &Export to CSV file... + &Export to CSV File… @@ -685,12 +694,12 @@ false - &Export to HTML file... + &Export to HTML File… - KeePass 1 database... + KeePass 1 Database… Import a KeePass 1 database @@ -698,7 +707,7 @@ - 1Password Vault... + 1Password Vault… Import a 1Password Vault @@ -706,7 +715,7 @@ - CSV file... + CSV File… Import a CSV file @@ -714,17 +723,17 @@ - Show TOTP... + Show TOTP - Show TOTP QR Code... + Show QR Code - Set up TOTP... + Set up TOTP… @@ -747,7 +756,7 @@ - Report a &bug + Report a &Bug @@ -760,7 +769,7 @@ - &Online Help... + &Online Help Go to online documentation (opens browser) diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index c04487c0e..7d6f05d41 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -28,6 +28,7 @@ #include "core/PasswordGenerator.h" #include "core/PasswordHealth.h" #include "gui/Clipboard.h" +#include "gui/osutils/OSUtils.h" PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent) : QWidget(parent) @@ -390,27 +391,33 @@ void PasswordGeneratorWidget::colorStrengthIndicator(const PasswordHealth& healt style.replace(re, "\\1 %1;"); // Set the color and background based on entropy - // colors are taking from the KDE breeze palette - // + QList qualityColors; + if (osUtils->isDarkMode()) { + qualityColors << QStringLiteral("#C43F31") << QStringLiteral("#DB9837") << QStringLiteral("#608A22") + << QStringLiteral("#1F8023"); + } else { + qualityColors << QStringLiteral("#C43F31") << QStringLiteral("#E09932") << QStringLiteral("#5EA10E") + << QStringLiteral("#118f17"); + } switch (health.quality()) { case PasswordHealth::Quality::Bad: case PasswordHealth::Quality::Poor: - m_ui->entropyProgressBar->setStyleSheet(style.arg("#c0392b")); + m_ui->entropyProgressBar->setStyleSheet(style.arg(qualityColors[0])); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Poor", "Password quality"))); break; case PasswordHealth::Quality::Weak: - m_ui->entropyProgressBar->setStyleSheet(style.arg("#f39c1f")); + m_ui->entropyProgressBar->setStyleSheet(style.arg(qualityColors[1])); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Weak", "Password quality"))); break; case PasswordHealth::Quality::Good: - m_ui->entropyProgressBar->setStyleSheet(style.arg("#11d116")); + m_ui->entropyProgressBar->setStyleSheet(style.arg(qualityColors[2])); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Good", "Password quality"))); break; case PasswordHealth::Quality::Excellent: - m_ui->entropyProgressBar->setStyleSheet(style.arg("#27ae60")); + m_ui->entropyProgressBar->setStyleSheet(style.arg(qualityColors[3])); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Excellent", "Password quality"))); break; } diff --git a/src/gui/SearchHelpWidget.ui b/src/gui/SearchHelpWidget.ui index 45e0d0bc6..4668e409b 100644 --- a/src/gui/SearchHelpWidget.ui +++ b/src/gui/SearchHelpWidget.ui @@ -6,8 +6,8 @@ 0 0 - 334 - 249 + 487 + 326 @@ -17,10 +17,7 @@ false - QFrame::Box - - - QFrame::Plain + QFrame::StyledPanel @@ -58,6 +55,9 @@ Search terms are as follows: [modifiers][field:]["]term["] + + Qt::AlignCenter + @@ -77,6 +77,9 @@ Every search term must match (ie, logical AND) + + Qt::AlignCenter + diff --git a/src/gui/SearchWidget.ui b/src/gui/SearchWidget.ui index 93fbbdee5..b98f4aea7 100644 --- a/src/gui/SearchWidget.ui +++ b/src/gui/SearchWidget.ui @@ -16,7 +16,7 @@ 0 - + 3 @@ -29,22 +29,6 @@ 0 - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 30 - 20 - - - - diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index 17347e337..9bbf7d56d 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include #include @@ -27,6 +26,7 @@ #include "core/Config.h" #include "core/DatabaseIcons.h" #include "core/Entry.h" +#include "core/FilePath.h" #include "core/Global.h" #include "core/Group.h" #include "core/Metadata.h" @@ -218,9 +218,6 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const } return result; } - case Totp: - result = entry->hasTotp() ? tr("Yes") : ""; - return result; } } else if (role == Qt::UserRole) { // Qt::UserRole is used as sort role, see EntryView::EntryView() switch (index.column()) { @@ -240,7 +237,9 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const case Paperclip: // Display entries with attachments above those without when // sorting ascendingly (and vice versa when sorting descendingly) - return entry->attachments()->isEmpty() ? 1 : 0; + return !entry->attachments()->isEmpty(); + case Totp: + return entry->hasTotp(); default: // For all other columns, simply use data provided by Qt::Display- // Role for sorting @@ -260,7 +259,12 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const return entry->iconScaledPixmap(); case Paperclip: if (!entry->attachments()->isEmpty()) { - return m_paperClipPixmap; + return filePath()->icon("actions", "paperclip"); + } + break; + case Totp: + if (entry->hasTotp()) { + return filePath()->icon("actions", "chronometer"); } break; } @@ -327,16 +331,47 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro return tr("Accessed"); case Attachments: return tr("Attachments"); - case Totp: - return tr("TOTP"); } + } else if (role == Qt::DecorationRole) { - if (section == Paperclip) { - return m_paperClipPixmap; + switch (section) { + case Paperclip: + return filePath()->icon("actions", "paperclip"); + case Totp: + return filePath()->icon("actions", "chronometer"); + } + } else if (role == Qt::ToolTipRole) { + switch (section) { + case ParentGroup: + return tr("Group name"); + case Title: + return tr("Entry title"); + case Username: + return tr("Username"); + case Password: + return tr("Password"); + case Url: + return tr("URL"); + case Notes: + return tr("Entry notes"); + case Expires: + return tr("Entry expires at"); + case Created: + return tr("Creation date"); + case Modified: + return tr("Last modification date"); + case Accessed: + return tr("Last access date"); + case Attachments: + return tr("Attached files"); + case Paperclip: + return tr("Has attachments"); + case Totp: + return tr("Has TOTP one-time password"); } } - return QVariant(); + return {}; } Qt::DropActions EntryModel::supportedDropActions() const @@ -502,8 +537,3 @@ void EntryModel::setPasswordsHidden(bool hide) emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); emit passwordsHiddenChanged(); } - -void EntryModel::setPaperClipPixmap(const QPixmap& paperclip) -{ - m_paperClipPixmap = paperclip; -} diff --git a/src/gui/entry/EntryModel.h b/src/gui/entry/EntryModel.h index 5f405bd41..ea2c96d5a 100644 --- a/src/gui/entry/EntryModel.h +++ b/src/gui/entry/EntryModel.h @@ -68,8 +68,6 @@ public: bool isPasswordsHidden() const; void setPasswordsHidden(bool hide); - void setPaperClipPixmap(const QPixmap& paperclip); - signals: void usernamesHiddenChanged(); void passwordsHiddenChanged(); @@ -93,8 +91,6 @@ private: bool m_hideUsernames; bool m_hidePasswords; - QPixmap m_paperClipPixmap; - const QString HiddenContentDisplay; const Qt::DateFormat DateFormat; }; diff --git a/src/gui/entry/EntryView.cpp b/src/gui/entry/EntryView.cpp index bff11e124..ea41b9b81 100644 --- a/src/gui/entry/EntryView.cpp +++ b/src/gui/entry/EntryView.cpp @@ -24,7 +24,6 @@ #include #include -#include "core/FilePath.h" #include "gui/SortFilterHideProxyModel.h" EntryView::EntryView(QWidget* parent) @@ -70,22 +69,31 @@ EntryView::EntryView(QWidget* parent) m_hidePasswordsAction->setCheckable(true); m_headerMenu->addSeparator(); + resetViewToDefaults(); + // Actions to toggle column visibility, each carrying the corresponding // colummn index as data m_columnActions = new QActionGroup(this); m_columnActions->setExclusive(false); - for (int columnIndex = 1; columnIndex < header()->count(); ++columnIndex) { - QString caption = m_model->headerData(columnIndex, Qt::Horizontal, Qt::DisplayRole).toString(); - if (columnIndex == EntryModel::Paperclip) { - caption = tr("Attachments (icon)"); + for (int visualIndex = 1; visualIndex < header()->count(); ++visualIndex) { + int logicalIndex = header()->logicalIndex(visualIndex); + QString caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString(); + if (logicalIndex == EntryModel::Paperclip) { + caption = tr("Has attachments", "Entry attachment icon toggle"); + } else if (logicalIndex == EntryModel::Totp) { + caption = tr("Has TOTP", "Entry TOTP icon toggle"); } QAction* action = m_headerMenu->addAction(caption); action->setCheckable(true); - action->setData(columnIndex); + action->setData(logicalIndex); m_columnActions->addAction(action); } connect(m_columnActions, SIGNAL(triggered(QAction*)), this, SLOT(toggleColumnVisibility(QAction*))); + connect(header(), &QHeaderView::sortIndicatorChanged, [this](int index, Qt::SortOrder order) { + Q_UNUSED(order) + header()->setSortIndicatorShown(index != EntryModel::Paperclip && index != EntryModel::Totp); + }); m_headerMenu->addSeparator(); m_headerMenu->addAction(tr("Fit to window"), this, SLOT(fitColumnsToWindow())); @@ -114,22 +122,6 @@ EntryView::EntryView(QWidget* parent) // clang-format off connect(header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)), SIGNAL(viewStateChanged())); // clang-format on - - resetFixedColumns(); - - // Configure default search view state and save for later use - header()->showSection(EntryModel::ParentGroup); - m_sortModel->sort(EntryModel::ParentGroup, Qt::AscendingOrder); - sortByColumn(EntryModel::ParentGroup, Qt::AscendingOrder); - m_defaultSearchViewState = header()->saveState(); - - // Configure default list view state and save for later use - header()->hideSection(EntryModel::ParentGroup); - m_sortModel->sort(EntryModel::Title, Qt::AscendingOrder); - sortByColumn(EntryModel::Title, Qt::AscendingOrder); - m_defaultListViewState = header()->saveState(); - - m_model->setPaperClipPixmap(filePath()->icon("actions", "paperclip").pixmap(16)); } void EntryView::contextMenuShortcutPressed() @@ -325,6 +317,7 @@ bool EntryView::setViewState(const QByteArray& state) { bool status = header()->restoreState(state); resetFixedColumns(); + m_columnsNeedRelayout = state.isEmpty(); return status; } @@ -397,9 +390,11 @@ void EntryView::toggleColumnVisibility(QAction* action) */ void EntryView::fitColumnsToWindow() { - header()->resizeSections(QHeaderView::Stretch); + header()->setSectionResizeMode(QHeaderView::Stretch); + resetFixedColumns(); + QCoreApplication::processEvents(); + header()->setSectionResizeMode(QHeaderView::Interactive); resetFixedColumns(); - fillRemainingWidth(true); emit viewStateChanged(); } @@ -409,69 +404,88 @@ void EntryView::fitColumnsToWindow() */ void EntryView::fitColumnsToContents() { - // Resize columns to fit contents - header()->resizeSections(QHeaderView::ResizeToContents); + header()->setSectionResizeMode(QHeaderView::ResizeToContents); + resetFixedColumns(); + QCoreApplication::processEvents(); + header()->setSectionResizeMode(QHeaderView::Interactive); resetFixedColumns(); - fillRemainingWidth(false); emit viewStateChanged(); } /** - * Reset view to defaults + * Mark icon-only columns as fixed and resize them to their minimum section size. + */ +void EntryView::resetFixedColumns() +{ + header()->setSectionResizeMode(EntryModel::Paperclip, QHeaderView::Fixed); + header()->resizeSection(EntryModel::Paperclip, header()->minimumSectionSize()); + + header()->setSectionResizeMode(EntryModel::Totp, QHeaderView::Fixed); + header()->resizeSection(EntryModel::Totp, header()->minimumSectionSize()); +} + +/** + * Reset item view to defaults. */ void EntryView::resetViewToDefaults() { m_model->setUsernamesHidden(false); m_model->setPasswordsHidden(true); + // Reduce number of columns that are shown by default if (m_inSearchMode) { - header()->restoreState(m_defaultSearchViewState); + header()->showSection(EntryModel::ParentGroup); } else { - header()->restoreState(m_defaultListViewState); + header()->hideSection(EntryModel::ParentGroup); + } + header()->showSection(EntryModel::Title); + header()->showSection(EntryModel::Username); + header()->showSection(EntryModel::Url); + header()->showSection(EntryModel::Notes); + header()->showSection(EntryModel::Modified); + header()->showSection(EntryModel::Paperclip); + header()->showSection(EntryModel::Totp); + + header()->hideSection(EntryModel::Password); + header()->hideSection(EntryModel::Expires); + header()->hideSection(EntryModel::Created); + header()->hideSection(EntryModel::Accessed); + header()->hideSection(EntryModel::Attachments); + + // Reset column order to logical indices + for (int i = 0; i < header()->count(); ++i) { + header()->moveSection(header()->visualIndex(i), i); } - fitColumnsToWindow(); + // Reorder some columns + header()->moveSection(header()->visualIndex(EntryModel::Paperclip), 1); + header()->moveSection(header()->visualIndex(EntryModel::Totp), 2); + + // Sort by title or group (depending on the mode) + m_sortModel->sort(EntryModel::Title, Qt::AscendingOrder); + sortByColumn(EntryModel::Title, Qt::AscendingOrder); + + if (m_inSearchMode) { + m_sortModel->sort(EntryModel::ParentGroup, Qt::AscendingOrder); + sortByColumn(EntryModel::ParentGroup, Qt::AscendingOrder); + } + + // The following call only relayouts reliably if the widget has been shown + // already, so only do it if the widget is visible and let showEvent() handle + // the initial default layout. + if (isVisible()) { + fitColumnsToWindow(); + } } -void EntryView::fillRemainingWidth(bool lastColumnOnly) +void EntryView::showEvent(QShowEvent* event) { - // Determine total width of currently visible columns - int width = 0; - int lastColumnIndex = 0; - for (int columnIndex = 0; columnIndex < header()->count(); ++columnIndex) { - if (!header()->isSectionHidden(columnIndex)) { - width += header()->sectionSize(columnIndex); - } - if (header()->visualIndex(columnIndex) > lastColumnIndex) { - lastColumnIndex = header()->visualIndex(columnIndex); - } - } + QTreeView::showEvent(event); - int numColumns = header()->count() - header()->hiddenSectionCount(); - int availWidth = header()->width() - width; - if ((numColumns <= 0) || (availWidth <= 0)) { - return; + // Check if header columns need to be resized to sensible defaults. + // This is only needed if no previous view state has been loaded. + if (m_columnsNeedRelayout) { + fitColumnsToWindow(); + m_columnsNeedRelayout = false; } - - if (!lastColumnOnly) { - // Equally distribute remaining width to visible columns - int add = availWidth / numColumns; - width = 0; - for (int columnIndex = 0; columnIndex < header()->count(); ++columnIndex) { - if (!header()->isSectionHidden(columnIndex)) { - header()->resizeSection(columnIndex, header()->sectionSize(columnIndex) + add); - width += header()->sectionSize(columnIndex); - } - } - } - - // Add remaining width to last column - header()->resizeSection(header()->logicalIndex(lastColumnIndex), - header()->sectionSize(lastColumnIndex) + (header()->width() - width)); -} - -void EntryView::resetFixedColumns() -{ - header()->setSectionResizeMode(EntryModel::Paperclip, QHeaderView::Fixed); - header()->resizeSection(EntryModel::Paperclip, header()->minimumSectionSize()); } diff --git a/src/gui/entry/EntryView.h b/src/gui/entry/EntryView.h index 53de7aff5..f3786ed37 100644 --- a/src/gui/entry/EntryView.h +++ b/src/gui/entry/EntryView.h @@ -63,6 +63,7 @@ protected: void keyPressEvent(QKeyEvent* event) override; void focusInEvent(QFocusEvent* event) override; void focusOutEvent(QFocusEvent* event) override; + void showEvent(QShowEvent* event) override; private slots: void emitEntryActivated(const QModelIndex& index); @@ -75,15 +76,12 @@ private slots: void contextMenuShortcutPressed(); private: - void fillRemainingWidth(bool lastColumnOnly); void resetFixedColumns(); EntryModel* const m_model; SortFilterHideProxyModel* const m_sortModel; bool m_inSearchMode; - - QByteArray m_defaultListViewState; - QByteArray m_defaultSearchViewState; + bool m_columnsNeedRelayout = true; QMenu* m_headerMenu; QAction* m_hideUsernamesAction; diff --git a/src/gui/osutils/macutils/MacUtils.cpp b/src/gui/osutils/macutils/MacUtils.cpp index 498325868..44e5dbee4 100644 --- a/src/gui/osutils/macutils/MacUtils.cpp +++ b/src/gui/osutils/macutils/MacUtils.cpp @@ -22,7 +22,7 @@ QPointer MacUtils::m_instance = nullptr; MacUtils::MacUtils(QObject* parent) - : OSUtils(parent) + : OSUtilsBase(parent) , m_appkit(new AppKit()) { connect(m_appkit.data(), SIGNAL(lockDatabases()), SIGNAL(lockDatabases())); diff --git a/src/gui/osutils/macutils/MacUtils.h b/src/gui/osutils/macutils/MacUtils.h index 427c7230a..2146cdd3b 100644 --- a/src/gui/osutils/macutils/MacUtils.h +++ b/src/gui/osutils/macutils/MacUtils.h @@ -21,9 +21,12 @@ #include "gui/osutils/OSUtilsBase.h" #include "AppKit.h" -#include -class MacUtils : public OSUtils +#include +#include +#include + +class MacUtils : public OSUtilsBase { Q_OBJECT diff --git a/src/gui/styles/base/BaseStyle.cpp b/src/gui/styles/base/BaseStyle.cpp new file mode 100644 index 000000000..b3e22efc9 --- /dev/null +++ b/src/gui/styles/base/BaseStyle.cpp @@ -0,0 +1,4779 @@ +/* + * Copyright (C) 2020 KeePassXC Team + * Copyright (C) 2019 Andrew Richards + * + * Derived from Phantomstyle and relicensed under the GPLv2 or v3. + * https://github.com/randrew/phantomstyle + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "BaseStyle.h" +#include "phantomcolor.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +QT_BEGIN_NAMESPACE +Q_GUI_EXPORT int qt_defaultDpiX(); +QT_END_NAMESPACE + +// Redefine Q_FALLTHROUGH for older Qt versions +#ifndef Q_FALLTHROUGH +#if (defined(Q_CC_GNU) && Q_CC_GNU >= 700) && !defined(Q_CC_INTEL) +#define Q_FALLTHROUGH() __attribute__((fallthrough)) +#else +#define Q_FALLTHROUGH() (void)0 +#endif +#endif + +namespace Phantom +{ + namespace + { + constexpr qint16 DefaultFrameWidth = 6; + constexpr qint16 SplitterMaxLength = 25; // Length of splitter handle (not thickness) + constexpr qint16 MenuMinimumWidth = 20; // Smallest width that menu items can have + constexpr qint16 MenuBar_FrameWidth = 6; + constexpr qint16 SpinBox_ButtonWidth = 15; + + // These two are currently not based on font, but could be + constexpr qint16 LineEdit_ContentsHPad = 5; + constexpr qint16 ComboBox_NonEditable_ContentsHPad = 7; + constexpr qint16 HeaderSortIndicator_HOffset = 1; + constexpr qint16 HeaderSortIndicator_VOffset = 2; + constexpr qint16 TabBar_InctiveVShift = 0; + + constexpr qreal TabBarTab_Rounding = 1.0; + constexpr qreal SpinBox_Rounding = 1.0; + constexpr qreal LineEdit_Rounding = 1.0; + constexpr qreal FrameFocusRect_Rounding = 1.0; + constexpr qreal PushButton_Rounding = 1.0; + constexpr qreal ToolButton_Rounding = 1.0; + constexpr qreal ProgressBar_Rounding = 1.0; + constexpr qreal GroupBox_Rounding = 1.0; + constexpr qreal SliderGroove_Rounding = 1.0; + constexpr qreal SliderHandle_Rounding = 1.0; + + constexpr qreal CheckMark_WidthOfHeightScale = 0.8; + constexpr qreal PushButton_HorizontalPaddingFontHeightRatio = 1.0; + constexpr qreal TabBar_HPaddingFontRatio = 1.25; + constexpr qreal TabBar_VPaddingFontRatio = 1.0 / 1.25; + constexpr qreal GroupBox_LabelBottomMarginFontRatio = 1.0 / 4.0; + constexpr qreal ComboBox_ArrowMarginRatio = 1.0 / 3.25; + + constexpr qreal MenuBar_HorizontalPaddingFontRatio = 1.0 / 2.0; + constexpr qreal MenuBar_VerticalPaddingFontRatio = 1.0 / 3.0; + + constexpr qreal MenuItem_LeftMarginFontRatio = 1.0 / 2.0; + constexpr qreal MenuItem_RightMarginForTextFontRatio = 1.0 / 1.5; + constexpr qreal MenuItem_RightMarginForArrowFontRatio = 1.0 / 4.0; + constexpr qreal MenuItem_VerticalMarginsFontRatio = 1.0 / 8.0; + // Number that's multiplied with a font's height to get the space between a + // menu item's checkbox (or other sign) and its text (or icon). + constexpr qreal MenuItem_CheckRightSpaceFontRatio = 1.0 / 4.0; + constexpr qreal MenuItem_TextMnemonicSpaceFontRatio = 1.5; + constexpr qreal MenuItem_SubMenuArrowSpaceFontRatio = 1.0 / 1.5; + constexpr qreal MenuItem_SubMenuArrowWidthFontRatio = 1.0 / 2.75; + constexpr qreal MenuItem_SeparatorHeightFontRatio = 1.0 / 1.5; + constexpr qreal MenuItem_CheckMarkVerticalInsetFontRatio = 1.0 / 5.0; + constexpr qreal MenuItem_IconRightSpaceFontRatio = 1.0 / 3.0; + + constexpr bool BranchesOnEdge = false; + constexpr bool OverhangShadows = false; + constexpr bool IndicatorShadows = false; + constexpr bool MenuExtraBottomMargin = true; + constexpr bool MenuBarLeftMargin = false; + constexpr bool MenuBarDrawBorder = false; + constexpr bool AllowToolBarAutoRaise = true; + // Note that this only applies to the disclosure etc. decorators in tree views. + constexpr bool ShowItemViewDecorationSelected = false; + constexpr bool UseQMenuForComboBoxPopup = true; + constexpr bool ItemView_UseFontHeightForDecorationSize = true; + + // Whether or not the non-raised tabs in a tab bar have shininess/highlights to + // them. Setting this to false adds an extra visual hint for distinguishing + // between the current and non-current tabs, but makes the non-current tabs + // appear less clickable. Other ways to increase the visual differences could + // be to increase the color contrast for the background fill color, or increase + // the vertical offset. However, increasing the vertical offset comes with some + // layout challenges, and increasing the color contrast further may visually + // imply an incorrect layout structure. Not sure what's best. + // + // This doesn't disable creating the color/brush resource, even though it's + // currently a compile-time-only option, because it may be changed to be part + // of some dynamic config system for Phantom in the future, or have a + // per-widget style hint associated with it. + const bool TabBar_InactiveTabsHaveSpecular = false; + + struct Grad + { + Grad(const QColor& from, const QColor& to) + { + rgbA = Rgb::ofQColor(from); + rgbB = Rgb::ofQColor(to); + lA = rgbA.toHsl().l; + lB = rgbB.toHsl().l; + } + QColor sample(qreal alpha) const + { + Hsl hsl = Rgb::lerp(rgbA, rgbB, alpha).toHsl(); + hsl.l = Phantom::lerp(lA, lB, alpha); + return hsl.toQColor(); + } + Rgb rgbA, rgbB; + qreal lA, lB; + }; + + namespace DeriveColors + { + Q_NEVER_INLINE QColor adjustLightness(const QColor& qcolor, qreal ld) + { + Hsl hsl = Hsl::ofQColor(qcolor); + const qreal gamma = 3.0; + hsl.l = std::pow(Phantom::saturate(std::pow(hsl.l, 1.0 / gamma) + ld * 0.8), gamma); + return hsl.toQColor(); + } + QColor buttonColor(const QPalette& pal) + { + // temp hack + if (pal.color(QPalette::Button) == pal.color(QPalette::Window)) + return adjustLightness(pal.color(QPalette::Button), 0.01); + return pal.color(QPalette::Button); + } + QColor highlightedOutlineOf(const QPalette& pal) + { + return adjustLightness(pal.color(QPalette::Highlight), -0.08); + } + QColor dividerColor(const QColor& underlying) + { + return adjustLightness(underlying, -0.05); + } + QColor lightDividerColor(const QColor& underlying) + { + return adjustLightness(underlying, 0.02); + } + QColor outlineOf(const QPalette& pal) + { + return adjustLightness(pal.color(QPalette::Window), -0.1); + } + QColor gutterColorOf(const QPalette& pal) + { + return adjustLightness(pal.color(QPalette::Window), -0.05); + } + QColor darkGutterColorOf(const QPalette& pal) + { + return adjustLightness(pal.color(QPalette::Window), -0.08); + } + QColor lightShadeOf(const QColor& underlying) + { + return adjustLightness(underlying, 0.08); + } + QColor darkShadeOf(const QColor& underlying) + { + return adjustLightness(underlying, -0.08); + } + QColor overhangShadowOf(const QColor& underlying) + { + return adjustLightness(underlying, -0.05); + } + QColor sliderGutterShadowOf(const QColor& underlying) + { + return adjustLightness(underlying, -0.01); + } + QColor specularOf(const QColor& underlying) + { + return adjustLightness(underlying, 0.01); + } + QColor lightSpecularOf(const QColor& underlying) + { + return adjustLightness(underlying, 0.05); + } + QColor pressedOf(const QColor& color) + { + return adjustLightness(color, -0.05); + } + QColor darkPressedOf(const QColor& color) + { + return adjustLightness(color, -0.08); + } + QColor lightOnOf(const QColor& color) + { + return adjustLightness(color, -0.04); + } + QColor onOf(const QColor& color) + { + return adjustLightness(color, -0.08); + } + QColor indicatorColorOf(const QPalette& palette, QPalette::ColorGroup group = QPalette::Current) + { + return Grad(palette.color(group, QPalette::WindowText), palette.color(group, QPalette::Button)) + .sample(0.45); + } + QColor inactiveTabFillColorOf(const QColor& underlying) + { + // used to be -0.01 + return adjustLightness(underlying, -0.025); + } + QColor progressBarOutlineColorOf(const QPalette& pal) + { + // Pretty wasteful + Hsl hsl0 = Hsl::ofQColor(pal.color(QPalette::Window)); + Hsl hsl1 = Hsl::ofQColor(pal.color(QPalette::Highlight)); + hsl1.l = Phantom::saturate(qMin(hsl0.l - 0.1, hsl1.l - 0.2)); + return hsl1.toQColor(); + } + QColor itemViewMultiSelectionCurrentBorderOf(const QPalette& pal) + { + return adjustLightness(pal.color(QPalette::Highlight), -0.15); + } + bool hack_isLightPalette(const QPalette& pal) + { + Hsl hsl0 = Hsl::ofQColor(pal.color(QPalette::WindowText)); + Hsl hsl1 = Hsl::ofQColor(pal.color(QPalette::Window)); + return hsl0.l < hsl1.l; + } + QColor itemViewHeaderOnLineColorOf(const QPalette& pal) + { + return hack_isLightPalette(pal) + ? highlightedOutlineOf(pal) + : Grad(pal.color(QPalette::WindowText), pal.color(QPalette::Window)).sample(0.5); + } + } // namespace DeriveColors + + namespace SwatchColors + { + enum SwatchColor + { + S_none = 0, + S_window, + S_button, + S_base, + S_text, + S_windowText, + S_highlight, + S_highlightedText, + S_scrollbarGutter, + S_window_outline, + S_window_specular, + S_window_divider, + S_window_lighter, + S_window_darker, + S_frame_outline, + S_button_specular, + S_button_pressed, + S_button_on, + S_button_pressed_specular, + S_sliderHandle, + S_sliderHandle_pressed, + S_sliderHandle_specular, + S_sliderHandle_pressed_specular, + S_base_shadow, + S_base_divider, + S_windowText_disabled, + S_highlight_outline, + S_highlight_specular, + S_progressBar_outline, + S_inactiveTabYesFrame, + S_inactiveTabNoFrame, + S_inactiveTabYesFrame_specular, + S_inactiveTabNoFrame_specular, + S_indicator_current, + S_indicator_disabled, + S_itemView_multiSelection_currentBorder, + S_itemView_headerOnLine, + S_scrollbarGutter_disabled, + + // Aliases + S_progressBar = S_highlight, + S_progressBar_specular = S_highlight_specular, + S_tabFrame = S_window, + S_tabFrame_specular = S_window_specular, + }; + } + + using Swatchy = SwatchColors::SwatchColor; + + enum + { + Num_SwatchColors = SwatchColors::S_scrollbarGutter_disabled + 1, + Num_ShadowSteps = 3, + }; + + struct PhSwatch : public QSharedData + { + // The pens store the brushes within them, so storing the brushes here as + // well is redundant. However, QPen::brush() does not return its brush by + // reference, so we'd end up doing a bunch of inc/dec work every time we use + // one. Also, it saves us the indirection of chasing two pointers (Swatch -> + // QPen -> QBrush) every time we want to get a QColor. + QBrush brushes[Num_SwatchColors]; + QPen pens[Num_SwatchColors]; + QColor scrollbarShadowColors[Num_ShadowSteps]; + + // Note: the casts to int in the assert macros are to suppress a false + // positive warning for tautological comparison in the clang linter. + inline const QColor& color(Swatchy swatchValue) const + { + Q_ASSERT(swatchValue >= 0 && static_cast(swatchValue) < Num_SwatchColors); + return brushes[swatchValue].color(); + } + inline const QBrush& brush(Swatchy swatchValue) const + { + Q_ASSERT(swatchValue >= 0 && static_cast(swatchValue) < Num_SwatchColors); + return brushes[swatchValue]; + } + inline const QPen& pen(Swatchy swatchValue) const + { + Q_ASSERT(swatchValue >= 0 && static_cast(swatchValue) < Num_SwatchColors); + return pens[swatchValue]; + } + + void loadFromQPalette(const QPalette& pal); + }; + + using PhSwatchPtr = QExplicitlySharedDataPointer; + using PhCacheEntry = QPair; + enum : int + { + Num_ColorCacheEntries = 10, + }; + using PhSwatchCache = QVarLengthArray; + Q_NEVER_INLINE void PhSwatch::loadFromQPalette(const QPalette& pal) + { + using namespace SwatchColors; + namespace Dc = DeriveColors; + bool isLight = Dc::hack_isLightPalette(pal); + QColor colors[Num_SwatchColors]; + colors[S_none] = QColor(); + + colors[S_window] = pal.color(QPalette::Window); + colors[S_button] = pal.color(QPalette::Button); + if (colors[S_button] == colors[S_window]) + colors[S_button] = Dc::adjustLightness(colors[S_button], 0.01); + colors[S_base] = pal.color(QPalette::Base); + colors[S_text] = pal.color(QPalette::Text); + colors[S_text] = pal.color(QPalette::WindowText); + colors[S_windowText] = pal.color(QPalette::WindowText); + colors[S_highlight] = pal.color(QPalette::Highlight); + colors[S_highlightedText] = pal.color(QPalette::HighlightedText); + colors[S_scrollbarGutter] = isLight ? Dc::gutterColorOf(pal) : Dc::darkGutterColorOf(pal); + + colors[S_window_outline] = + isLight ? Dc::adjustLightness(colors[S_window], -0.1) : Dc::adjustLightness(colors[S_window], 0.03); + colors[S_window_specular] = Dc::specularOf(colors[S_window]); + colors[S_window_divider] = + isLight ? Dc::dividerColor(colors[S_window]) : Dc::lightDividerColor(colors[S_window]); + colors[S_window_lighter] = Dc::lightShadeOf(colors[S_window]); + colors[S_window_darker] = Dc::darkShadeOf(colors[S_window]); + colors[S_frame_outline] = isLight ? colors[S_window_outline] : Dc::adjustLightness(colors[S_window], 0.08); + colors[S_button_specular] = + isLight ? Dc::specularOf(colors[S_button]) : Dc::lightSpecularOf(colors[S_button]); + colors[S_button_pressed] = isLight ? Dc::pressedOf(colors[S_button]) : Dc::darkPressedOf(colors[S_button]); + colors[S_button_on] = isLight ? Dc::lightOnOf(colors[S_button]) : Dc::onOf(colors[S_button]); + colors[S_button_pressed_specular] = + isLight ? Dc::specularOf(colors[S_button_pressed]) : Dc::lightSpecularOf(colors[S_button_pressed]); + + colors[S_sliderHandle] = isLight ? colors[S_button] : Dc::adjustLightness(colors[S_button], -0.03); + colors[S_sliderHandle_specular] = + isLight ? Dc::specularOf(colors[S_sliderHandle]) : Dc::lightSpecularOf(colors[S_sliderHandle]); + colors[S_sliderHandle_pressed] = + isLight ? colors[S_button_pressed] : Dc::adjustLightness(colors[S_button_pressed], 0.03); + colors[S_sliderHandle_pressed_specular] = isLight ? Dc::specularOf(colors[S_sliderHandle_pressed]) + : Dc::lightSpecularOf(colors[S_sliderHandle_pressed]); + + colors[S_base_shadow] = Dc::overhangShadowOf(colors[S_base]); + colors[S_base_divider] = Dc::dividerColor(colors[S_base]); + colors[S_windowText_disabled] = pal.color(QPalette::Disabled, QPalette::WindowText); + colors[S_highlight_outline] = isLight ? Dc::adjustLightness(colors[S_highlight], -0.02) + : Dc::adjustLightness(colors[S_highlight], 0.02); + colors[S_highlight_specular] = Dc::specularOf(colors[S_highlight]); + colors[S_progressBar_outline] = Dc::progressBarOutlineColorOf(pal); + colors[S_inactiveTabYesFrame] = Dc::inactiveTabFillColorOf(colors[S_tabFrame]); + colors[S_inactiveTabNoFrame] = Dc::inactiveTabFillColorOf(colors[S_window]); + colors[S_inactiveTabYesFrame_specular] = Dc::specularOf(colors[S_inactiveTabYesFrame]); + colors[S_inactiveTabNoFrame_specular] = Dc::specularOf(colors[S_inactiveTabNoFrame]); + colors[S_indicator_current] = Dc::indicatorColorOf(pal, QPalette::Current); + colors[S_indicator_disabled] = Dc::indicatorColorOf(pal, QPalette::Disabled); + colors[S_itemView_multiSelection_currentBorder] = Dc::itemViewMultiSelectionCurrentBorderOf(pal); + colors[S_itemView_headerOnLine] = Dc::itemViewHeaderOnLineColorOf(pal); + colors[S_scrollbarGutter_disabled] = colors[S_window]; + + brushes[S_none] = Qt::NoBrush; + for (int i = S_none + 1; i < Num_SwatchColors; ++i) { + // todo try to reuse + brushes[i] = colors[i]; + } + pens[S_none] = Qt::NoPen; + // QPen::setColor constructs a QBrush behind the scenes, so better to just + // re-use the ones we already made. + for (int i = S_none + 1; i < Num_SwatchColors; ++i) { + pens[i].setBrush(brushes[i]); + // Width is already 1, don't need to set it. Caps and joins already fine at + // their defaults, too. + } + + Grad gutterGrad(Dc::sliderGutterShadowOf(colors[S_scrollbarGutter]), colors[S_scrollbarGutter]); + for (int i = 0; i < Num_ShadowSteps; ++i) { + scrollbarShadowColors[i] = gutterGrad.sample(i / static_cast(Num_ShadowSteps)); + } + } + + // This is the "hash" (not really a hash) function we'll use on the happy fast + // path when looking up a PhSwatch for a given QPalette. It's fragile, because + // it uses QPalette::cacheKey(), so it may not match even when the contents + // (currentColorGroup + the RGB colors) of the QPalette are actually a match. + // But it's cheaper to calculate, so we'll store a single one of these "hashes" + // for the head (most recently used) cached PhSwatch, and check to see if it + // matches. This is the most common case, so we can usually save some work by + // doing this. (The second most common case is probably having a different + // ColorGroup but the rest of the contents are the same, but we don't have a + // special path for that.) + inline quint64 fastfragile_hash_qpalette(const QPalette& p) + { + union + { + qint64 i; + quint64 u; + } x; + x.i = p.cacheKey(); + // QPalette::ColorGroup has range 0..5 (inclusive), so it only uses 3 bits. + // The high 32 bits in QPalette::cacheKey() are a global incrementing serial + // number for the QPalette creation. We don't store (2^29-1) things in our + // cache, and I doubt that many will ever be created in a real application + // while also retaining some of them across such a broad time range, so it's + // really unlikely that repurposing these top 3 bits to also include the + // QPalette::currentColorGroup() (which the cacheKey doesn't include for some + // reason...) will generate a collision. + // + // This may not be true in the future if the way the QPalette::cacheKey() is + // generated changes. If that happens, change to use the definition of + // `fastfragile_hash_qpalette` below, which is less likely to collide with an + // arbitrarily numbered key but also does more work. +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + x.u = x.u ^ (static_cast(p.currentColorGroup()) << (64 - 3)); + return x.u; +#else + // Use this definition here if the contents/layout of QPalette::cacheKey() + // (as in, the C++ code in qpalette.cpp) are changed. We'll also put a Qt6 + // guard for it, so that it will default to a more safe definition on the + // next guaranteed big breaking change for Qt. A warning will hopefully get + // someone to double-check it at some point in the future. +#warning "Verify contents and layout of QPalette::cacheKey() have not changed" + QtPrivate::QHashCombine c; + uint h = qHash(p.currentColorGroup()); + h = c(h, (uint)(x.u & 0xFFFFFFFFu)); + h = c(h, (uint)((x.u >> 32) & 0xFFFFFFFFu)); + return h; +#endif + } + + // This hash function is for when we want an actual accurate hash of a + // QPalette. QPalette's cacheKey() isn't very reliable -- it seems to change to + // a new random number whenever it's modified, with the exception of the + // currentColorGroup being changed. This kind of sucks for us, because it means + // two QPalette's can have the same contents but hash to different values. And + // this actually happens a lot! We'll do the hashing ourselves. Also, we're not + // interested in all of the colors, only some of them, and we ignore + // pens/brushes. + uint accurate_hash_qpalette(const QPalette& p) + { + // Probably shouldn't use this, could replace with our own guy. It's not a + // great hasher anyway. + QtPrivate::QHashCombine c; + uint h = qHash(p.currentColorGroup()); + QPalette::ColorRole const roles[] = {QPalette::Window, + QPalette::Button, + QPalette::Base, + QPalette::Text, + QPalette::WindowText, + QPalette::Highlight, + QPalette::HighlightedText}; + for (auto role : roles) { + h = c(h, p.color(role).rgb()); + } + return h; + } + + Q_NEVER_INLINE PhSwatchPtr + deep_getCachedSwatchOfQPalette(PhSwatchCache* cache, + int cacheCount, // Just saving a call to cache->count() + const QPalette& qpalette) + { + // Calculate our hash key from the QPalette's current ColorGroup and the + // actual RGBA values that we use. We have to mix the ColorGroup in + // ourselves, because QPalette does not account for it in the cache key. + uint key = accurate_hash_qpalette(qpalette); + int n = cacheCount; + int idx = -1; + for (int i = 0; i < n; ++i) { + const auto& x = cache->at(i); + if (x.first == key) { + idx = i; + break; + } + } + if (idx == -1) { + PhSwatchPtr ptr; + if (n < Num_ColorCacheEntries) { + ptr = new PhSwatch; + } else { + // Remove the oldest guy from the cache. Remember that because we may + // re-enter QStyle functions multiple times when drawing or calculating + // something, we may have to load several swaitches derived from + // different QPalettes on different stack frames at the same time. But as + // an extra cost-savings measure, we'll check and see if something else + // has a reference to the removed guy. If there aren't any references to + // it, then we'll re-use it directly instead of allocating a new one. (We + // will only ever run into the case where we can't re-use it directly if + // some other stack frame has a reference to it.) This is nice because + // then the QPens and QBrushes don't all also have to reallocate their d + // ptr stuff. + ptr = cache->last().second; + cache->removeLast(); + ptr.detach(); + } + ptr->loadFromQPalette(qpalette); + cache->prepend(PhCacheEntry(key, ptr)); + return ptr; + } else { + if (idx == 0) { + return cache->at(idx).second; + } + PhCacheEntry e = cache->at(idx); + // Using std::move from algorithm could be more efficient here, but I don't + // want to depend on algorithm or write this myself. Small N with a movable + // type means it doesn't really matter in this case. + cache->remove(idx); + cache->prepend(e); + return e.second; + } + } + + Q_NEVER_INLINE PhSwatchPtr + getCachedSwatchOfQPalette(PhSwatchCache* cache, + quint64* headSwatchFastKey, // Optimistic fast-path quick hash key + const QPalette& qpalette) + { + quint64 ck = fastfragile_hash_qpalette(qpalette); + int cacheCount = cache->count(); + // This hint is counter-productive if we're being called in a way that + // interleaves different QPalettes. But misses to this optimistic path were + // rare in my tests. (Probably not going to amount to any significant + // difference, anyway.) + if (Q_LIKELY(cacheCount > 0 && *headSwatchFastKey == ck)) { + return cache->at(0).second; + } + *headSwatchFastKey = ck; + return deep_getCachedSwatchOfQPalette(cache, cacheCount, qpalette); + } + + } // namespace +} // namespace Phantom + +class BaseStylePrivate +{ +public: + BaseStylePrivate(); + + // A fast'n'easy hash of QPalette::cacheKey()+QPalette::currentColorGroup() + // of only the head element of swatchCache list. The most common thing that + // happens when deriving a PhSwatch from a QPalette is that we just end up + // re-using the last one that we used. For that case, we can potentially save + // calling `accurate_hash_qpalette()` and instead use the value returned by + // QPalette::cacheKey() (and QPalette::currentColorGroup()) and compare it to + // the last one that we used. If it matches, then we know we can just use the + // head of the cache list without having to do any further checks, which + // saves a few hundred (!) nanoseconds. + // + // However, the `QPalette::cacheKey()` value is fragile and may change even + // if none of the colors in the QPalette have changed. In other words, all of + // the colors in a QPalette may match another QPalette (or a derived + // PhSwatch) even if the `QPalette::cacheKey()` value is different. + // + // So if `QPalette::cacheKey()+currentColorGroup()` doesn't match, then we'll + // use our more accurate `accurate_hash_qpalette()` to get a more accurate + // comparison key, and then search through the cache list to find a matching + // cached PhSwatch. (The more accurate cache key is what we store alongside + // each PhSwatch element, as the `.first` in each QPair. The + // QPalette::cacheKey() that we associate with the PhSwatch in the head + // position, `headSwatchFastKey`, is only stored for our single head element, + // as a special fast case.) If we find it, we'll move it to the head of the + // cache list. If not, we'll make a new one, and put it at the head. Either + // way, the `headSwatchFastKey` will be updated to the + // `fastfragile_qpalette_hash()` of the QPalette that we needed to derive a + // PhSwatch from, so that if we get called with the same QPalette again next + // time (which is probably going to be the case), it'll match and we can take + // the fast path. + quint64 headSwatchFastKey; + + Phantom::PhSwatchCache swatchCache; + QPen checkBox_pen_scratch; +}; + +namespace Phantom +{ + namespace + { + + // Minimal QPainter save/restore just for pen, brush, and AA render hint. If + // you touch more than that, this won't help you. But if you're only touching + // those things, this will save you some typing from manually storing/saving + // those properties each time. + struct PSave final + { + Q_DISABLE_COPY(PSave) + + explicit PSave(QPainter* painter_) + { + Q_ASSERT(painter_); + painter = painter_; + pen = painter_->pen(); + brush = painter_->brush(); + hintAA = painter_->testRenderHint(QPainter::Antialiasing); + } + Q_NEVER_INLINE void restore() + { + QPainter* p = painter; + if (!p) + return; + bool hintAA_ = hintAA; + // QPainter will check both pen and brush for equality when setting, so we + // should set it unconditionally here. + p->setPen(pen); + p->setBrush(brush); + // But it won't check the render hint to guard against doing extra work. + // We'll do that ourselves. (Though at least for the raster engine, this + // doesn't cause very much work to occur. But it still chases a few + // pointers.) + if (p->testRenderHint(QPainter::Antialiasing) != hintAA_) { + p->setRenderHint(QPainter::Antialiasing, hintAA_); + } + painter = nullptr; + pen = QPen(); + brush = QBrush(); + hintAA = false; + } + ~PSave() + { + restore(); + } + + private: + QPainter* painter; + QPen pen; + QBrush brush; + bool hintAA; + }; + + const qreal Pi = M_PI; + + qreal dpiScaled(qreal value) + { +#ifdef Q_OS_MAC + // On mac the DPI is always 72 so we should not scale it + return value; +#else + const qreal scale = qt_defaultDpiX() / 96.0; + return value * scale; +#endif + } + + struct MenuItemMetrics + { + int fontHeight; + int frameThickness; + int leftMargin; + int rightMarginForText; + int rightMarginForArrow; + int topMargin; + int bottomMargin; + int checkWidth; + int checkRightSpace; + int iconRightSpace; + int mnemonicSpace; + int arrowSpace; + int arrowWidth; + int separatorHeight; + int totalHeight; + + static MenuItemMetrics ofFontHeight(int fontHeight); + + private: + MenuItemMetrics() + { + } + }; + + MenuItemMetrics MenuItemMetrics::ofFontHeight(int fontHeight) + { + MenuItemMetrics m; + m.fontHeight = fontHeight; + m.frameThickness = dpiScaled(1.0); + m.leftMargin = static_cast(fontHeight * MenuItem_LeftMarginFontRatio); + m.rightMarginForText = static_cast(fontHeight * MenuItem_RightMarginForTextFontRatio); + m.rightMarginForArrow = static_cast(fontHeight * MenuItem_RightMarginForArrowFontRatio); + m.topMargin = static_cast(fontHeight * MenuItem_VerticalMarginsFontRatio); + m.bottomMargin = static_cast(fontHeight * MenuItem_VerticalMarginsFontRatio); + int checkVMargin = static_cast(fontHeight * MenuItem_CheckMarkVerticalInsetFontRatio); + int checkHeight = fontHeight - checkVMargin * 2; + if (checkHeight < 0) + checkHeight = 0; + m.checkWidth = static_cast(checkHeight * CheckMark_WidthOfHeightScale); + m.checkRightSpace = static_cast(fontHeight * MenuItem_CheckRightSpaceFontRatio); + m.iconRightSpace = static_cast(fontHeight * MenuItem_IconRightSpaceFontRatio); + m.mnemonicSpace = static_cast(fontHeight * MenuItem_TextMnemonicSpaceFontRatio); + m.arrowSpace = static_cast(fontHeight * MenuItem_SubMenuArrowSpaceFontRatio); + m.arrowWidth = static_cast(fontHeight * MenuItem_SubMenuArrowWidthFontRatio); + m.separatorHeight = static_cast(fontHeight * MenuItem_SeparatorHeightFontRatio); + // Odd numbers only + m.separatorHeight = (m.separatorHeight / 2) * 2 + 1; + m.totalHeight = fontHeight + m.frameThickness * 2 + m.topMargin + m.bottomMargin; + return m; + } + + QRect menuItemContentRect(const MenuItemMetrics& metrics, QRect itemRect, bool hasArrow) + { + QRect r = itemRect; + int ft = metrics.frameThickness; + int rm = hasArrow ? metrics.rightMarginForArrow : metrics.rightMarginForText; + r.adjust(ft + metrics.leftMargin, ft + metrics.topMargin, -(ft + rm), -(ft + metrics.bottomMargin)); + return r.isValid() ? r : QRect(); + } + QRect + menuItemCheckRect(const MenuItemMetrics& metrics, Qt::LayoutDirection direction, QRect itemRect, bool hasArrow) + { + QRect r = menuItemContentRect(metrics, itemRect, hasArrow); + int checkVMargin = static_cast(metrics.fontHeight * MenuItem_CheckMarkVerticalInsetFontRatio); + if (checkVMargin < 0) + checkVMargin = 0; + r.setSize(QSize(metrics.checkWidth, metrics.fontHeight)); + r.adjust(0, checkVMargin, 0, -checkVMargin); + return QStyle::visualRect(direction, itemRect, r) & itemRect; + } + QRect + menuItemIconRect(const MenuItemMetrics& metrics, Qt::LayoutDirection direction, QRect itemRect, bool hasArrow) + { + QRect r = menuItemContentRect(metrics, itemRect, hasArrow); + r.setX(r.x() + metrics.checkWidth + metrics.checkRightSpace); + r.setSize(QSize(metrics.fontHeight, metrics.fontHeight)); + return QStyle::visualRect(direction, itemRect, r) & itemRect; + } + QRect menuItemTextRect(const MenuItemMetrics& metrics, + Qt::LayoutDirection direction, + QRect itemRect, + bool hasArrow, + bool hasIcon, + int tabWidth) + { + QRect r = menuItemContentRect(metrics, itemRect, hasArrow); + r.setX(r.x() + metrics.checkWidth + metrics.checkRightSpace); + if (hasIcon) { + r.setX(r.x() + metrics.fontHeight + metrics.iconRightSpace); + } + r.setWidth(r.width() - tabWidth); + r.setHeight(metrics.fontHeight); + r &= itemRect; + return QStyle::visualRect(direction, itemRect, r); + } + QRect menuItemMnemonicRect(const MenuItemMetrics& metrics, + Qt::LayoutDirection direction, + QRect itemRect, + bool hasArrow, + int tabWidth) + { + QRect r = menuItemContentRect(metrics, itemRect, hasArrow); + int x = r.x() + r.width() - tabWidth; + if (hasArrow) + x -= metrics.arrowSpace + metrics.arrowWidth; + r.setX(x); + r.setHeight(metrics.fontHeight); + r &= itemRect; + return QStyle::visualRect(direction, itemRect, r); + } + QRect menuItemArrowRect(const MenuItemMetrics& metrics, Qt::LayoutDirection direction, QRect itemRect) + { + QRect r = menuItemContentRect(metrics, itemRect, true); + int x = r.x() + r.width() - metrics.arrowWidth; + r.setX(x); + r &= itemRect; + return QStyle::visualRect(direction, itemRect, r); + } + + Q_NEVER_INLINE + void progressBarFillRects(const QStyleOptionProgressBar* bar, + // The rect that represents the filled/completed region + QRect& outFilled, + // The rect that represents the incomplete region + QRect& outNonFilled, + // Whether or not the progress bar is indeterminate + bool& outIsIndeterminate) + { + QRect ra = bar->rect; + QRect rb = ra; + bool isHorizontal = bar->orientation != Qt::Vertical; + bool isInverted = bar->invertedAppearance; + bool isIndeterminate = bar->minimum == 0 && bar->maximum == 0; + bool isForward = !isHorizontal || bar->direction != Qt::RightToLeft; + if (isInverted) + isForward = !isForward; + int maxLen = isHorizontal ? ra.width() : ra.height(); + const auto availSteps = qMax(Q_INT64_C(1), qint64(bar->maximum) - bar->minimum); + const auto progress = qMax(bar->progress, bar->minimum); // workaround for bug in QProgressBar + const auto progressSteps = qint64(progress) - bar->minimum; + const auto progressBarWidth = progressSteps * maxLen / availSteps; + int barLen = isIndeterminate ? maxLen : progressBarWidth; + if (isHorizontal) { + if (isForward) { + ra.setWidth(barLen); + rb.setX(barLen); + } else { + ra.setX(ra.x() + ra.width() - barLen); + rb.setWidth(rb.width() - barLen); + } + } else { + if (isForward) { + ra.setY(ra.y() + ra.height() - barLen); + rb.setHeight(rb.height() - barLen); + } else { + ra.setHeight(barLen); + rb.setY(barLen); + } + } + outFilled = ra; + outNonFilled = rb; + outIsIndeterminate = isIndeterminate; + } + + int calcBigLineSize(int radius) + { + int bigLineSize = radius / 6; + if (bigLineSize < 4) + bigLineSize = 4; + if (bigLineSize > radius / 2) + bigLineSize = radius / 2; + return bigLineSize; + } + Q_NEVER_INLINE QPointF calcRadialPos(const QStyleOptionSlider* dial, qreal offset) + { + const int width = dial->rect.width(); + const int height = dial->rect.height(); + const int r = qMin(width, height) / 2; + const int currentSliderPosition = + dial->upsideDown ? dial->sliderPosition : (dial->maximum - dial->sliderPosition); + qreal a = 0; + if (dial->maximum == dial->minimum) + a = Pi / 2; + else if (dial->dialWrapping) + a = Pi * 3 / 2 - (currentSliderPosition - dial->minimum) * 2 * Pi / (dial->maximum - dial->minimum); + else + a = (Pi * 8 - (currentSliderPosition - dial->minimum) * 10 * Pi / (dial->maximum - dial->minimum)) / 6; + qreal xc = width / 2.0; + qreal yc = height / 2.0; + qreal len = r - calcBigLineSize(r) - 3; + qreal back = offset * len; + QPointF pos(QPointF(xc + back * qCos(a), yc - back * qSin(a))); + return pos; + } + Q_NEVER_INLINE QPolygonF calcLines(const QStyleOptionSlider* dial) + { + QPolygonF poly; + qreal width = dial->rect.width(); + qreal height = dial->rect.height(); + qreal r = qMin(width, height) / 2.0; + int bigLineSize = calcBigLineSize(r); + + qreal xc = width / 2.0 + 0.5; + qreal yc = height / 2.0 + 0.5; + const int ns = dial->tickInterval; + if (!ns) // Invalid values may be set by Qt Designer. + return poly; + int notches = (dial->maximum + ns - 1 - dial->minimum) / ns; + if (notches <= 0) + return poly; + if (dial->maximum < dial->minimum || dial->maximum - dial->minimum > 1000) { + int maximum = dial->minimum + 1000; + notches = (maximum + ns - 1 - dial->minimum) / ns; + } + poly.resize(2 + 2 * notches); + int smallLineSize = bigLineSize / 2; + for (int i = 0; i <= notches; ++i) { + qreal angle = + dial->dialWrapping ? Pi * 3 / 2 - i * 2 * Pi / notches : (Pi * 8 - i * 10 * Pi / notches) / 6; + qreal s = qSin(angle); + qreal c = qCos(angle); + if (i == 0 || (((ns * i) % (dial->pageStep ? dial->pageStep : 1)) == 0)) { + poly[2 * i] = QPointF(xc + (r - bigLineSize) * c, yc - (r - bigLineSize) * s); + poly[2 * i + 1] = QPointF(xc + r * c, yc - r * s); + } else { + poly[2 * i] = QPointF(xc + (r - 1 - smallLineSize) * c, yc - (r - 1 - smallLineSize) * s); + poly[2 * i + 1] = QPointF(xc + (r - 1) * c, yc - (r - 1) * s); + } + } + return poly; + } + // This will draw a nice and shiny QDial for us. We don't want + // all the shinyness in QWindowsStyle, hence we place it here + Q_NEVER_INLINE void drawDial(const QStyleOptionSlider* option, QPainter* painter) + { + namespace Dc = Phantom::DeriveColors; + const QPalette& pal = option->palette; + QColor buttonColor = Dc::buttonColor(option->palette); + const int width = option->rect.width(); + const int height = option->rect.height(); + const bool enabled = option->state & QStyle::State_Enabled; + qreal r = qMin(width, height) / 2.0; + r -= r / 50.0; + painter->save(); + painter->setRenderHint(QPainter::Antialiasing); + // Draw notches + if (option->subControls & QStyle::SC_DialTickmarks) { + painter->setPen(pal.color(QPalette::Disabled, QPalette::Text)); + painter->drawLines(calcLines(option)); + } + const qreal d_ = r / 6; + const qreal dx = option->rect.x() + d_ + (width - 2 * r) / 2 + 1; + const qreal dy = option->rect.y() + d_ + (height - 2 * r) / 2 + 1; + QRectF br = QRectF(dx + 0.5, dy + 0.5, int(r * 2 - 2 * d_ - 2), int(r * 2 - 2 * d_ - 2)); + if (enabled) { + painter->setBrush(buttonColor); + } else { + painter->setBrush(Qt::NoBrush); + } + painter->setPen(Dc::outlineOf(option->palette)); + painter->drawEllipse(br); + painter->setBrush(Qt::NoBrush); + painter->setPen(Dc::specularOf(buttonColor)); + painter->drawEllipse(br.adjusted(1, 1, -1, -1)); + if (option->state & QStyle::State_HasFocus) { + QColor highlight = pal.highlight().color(); + highlight.setHsv(highlight.hue(), qMin(160, highlight.saturation()), qMax(230, highlight.value())); + highlight.setAlpha(127); + painter->setPen(QPen(highlight, 2.0)); + painter->setBrush(Qt::NoBrush); + painter->drawEllipse(br.adjusted(-1, -1, 1, 1)); + } + QPointF dp = calcRadialPos(option, 0.70); + const qreal ds = r / 7.0; + QRectF dialRect(dp.x() - ds, dp.y() - ds, 2 * ds, 2 * ds); + painter->setBrush(option->palette.color(QPalette::Window)); + painter->setPen(Dc::outlineOf(option->palette)); + painter->drawEllipse(dialRect.adjusted(-1, -1, 1, 1)); + painter->restore(); + } + + int fontMetricsWidth(const QFontMetrics& fontMetrics, const QString& text) + { +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + return fontMetrics.width(text, text.size(), Qt::TextBypassShaping); +#else + return fontMetrics.horizontalAdvance(text); +#endif + } + + // This always draws the arrow with the correct aspect ratio, even if the + // provided bounding rect is non-square. The base edge of the triangle is + // snapped to a whole pixel to avoid anti-aliasing making it look soft. + // + // Expected time (release): 5usecs for regular-sized arrows + Q_NEVER_INLINE void drawArrow(QPainter* p, QRect rect, Qt::ArrowType arrowDirection, const QBrush& brush) + { + const qreal ArrowBaseRatio = 0.70; + qreal irx, iry, irw, irh; + QRectF(rect).getRect(&irx, &iry, &irw, &irh); + if (irw < 1.0 || irh < 1.0) + return; + qreal dw, dh; + if (arrowDirection == Qt::LeftArrow || arrowDirection == Qt::RightArrow) { + dw = ArrowBaseRatio; + dh = 1.0; + } else { + dw = 1.0; + dh = ArrowBaseRatio; + } + QSizeF sz = QSizeF(dw, dh).scaled(irw, irh, Qt::KeepAspectRatio); + qreal aw = sz.width(); + qreal ah = sz.height(); + qreal ax, ay; + ax = irx + (irw - aw) / 2; + ay = iry + (irh - ah) / 2; + QRectF arrowRect(ax, ay, aw, ah); + QPointF points[3]; + switch (arrowDirection) { + case Qt::DownArrow: + arrowRect.setTop(std::round(arrowRect.top())); + points[0] = arrowRect.topLeft(); + points[1] = arrowRect.topRight(); + points[2] = QPointF(arrowRect.center().x(), arrowRect.bottom()); + break; + case Qt::RightArrow: { + arrowRect.setLeft(std::round(arrowRect.left())); + points[0] = arrowRect.topLeft(); + points[1] = arrowRect.bottomLeft(); + points[2] = QPointF(arrowRect.right(), arrowRect.center().y()); + break; + } + case Qt::LeftArrow: + arrowRect.setRight(std::round(arrowRect.right())); + points[0] = arrowRect.topRight(); + points[1] = arrowRect.bottomRight(); + points[2] = QPointF(arrowRect.left(), arrowRect.center().y()); + break; + case Qt::UpArrow: + default: + arrowRect.setBottom(std::round(arrowRect.bottom())); + points[0] = arrowRect.bottomLeft(); + points[1] = arrowRect.bottomRight(); + points[2] = QPointF(arrowRect.center().x(), arrowRect.top()); + break; + } + auto oldPen = p->pen(); + auto oldBrush = p->brush(); + bool oldAA = p->testRenderHint(QPainter::Antialiasing); + p->setPen(Qt::NoPen); + p->setBrush(brush); + if (!oldAA) { + p->setRenderHint(QPainter::Antialiasing); + } + p->drawConvexPolygon(points, 3); + p->setPen(oldPen); + p->setBrush(oldBrush); + if (!oldAA) { + p->setRenderHint(QPainter::Antialiasing, false); + } + } + + // Pass allowEnabled as false to always draw the arrow with the disabled color, + // even if the underlying palette's current color group is not disabled. Useful + // for parts of widgets which may want to be drawn as disabled even if the + // actual widget is not set as disabled, such as scrollbar step buttons when + // the scrollbar has no movable range. + Q_NEVER_INLINE void + drawArrow(QPainter* painter, QRect rect, Qt::ArrowType type, const PhSwatch& swatch, bool allowEnabled = true) + { + if (rect.isEmpty()) + return; + using namespace SwatchColors; + Phantom::drawArrow( + painter, rect, type, swatch.brush(allowEnabled ? S_indicator_current : S_indicator_disabled)); + } + + // This draws exactly within the rect provided. If you provide a square rect, + // it will appear too wide -- you probably want to shrink the width of your + // square first by multiplying it with CheckMark_WidthOfHeightScale. + Q_NEVER_INLINE void + drawCheck(QPainter* painter, QPen& scratchPen, const QRectF& r, const PhSwatch& swatch, Swatchy color) + { + using namespace Phantom::SwatchColors; + qreal rx, ry, rw, rh; + QRectF(r).getRect(&rx, &ry, &rw, &rh); + qreal penWidth = 0.25 * qMin(rw, rh); + qreal dimx = rw - penWidth; + qreal dimy = rh - penWidth; + if (dimx < 0.5 || dimy < 0.5) + return; + qreal x = (rw - dimx) / 2 + rx; + qreal y = (rh - dimy) / 2 + ry; + QPointF points[3]; + points[0] = QPointF(0.0, 0.55); + points[1] = QPointF(0.4, 1.0); + points[2] = QPointF(1.0, 0); + for (int i = 0; i < 3; ++i) { + QPointF pnt = points[i]; + pnt.setX(pnt.x() * dimx + x); + pnt.setY(pnt.y() * dimy + y); + points[i] = pnt; + } + scratchPen.setBrush(swatch.brush(color)); + scratchPen.setCapStyle(Qt::RoundCap); + scratchPen.setJoinStyle(Qt::RoundJoin); + scratchPen.setWidthF(penWidth); + Phantom::PSave save(painter); + if (!painter->testRenderHint(QPainter::Antialiasing)) + painter->setRenderHint(QPainter::Antialiasing); + painter->setPen(scratchPen); + painter->setBrush(Qt::NoBrush); + painter->drawPolyline(points, 3); + } + + Q_NEVER_INLINE void + drawHyphen(QPainter* painter, QPen& scratchPen, const QRectF& r, const PhSwatch& swatch, Swatchy color) + { + using namespace Phantom::SwatchColors; + qreal rx, ry, rw, rh; + QRectF(r).getRect(&rx, &ry, &rw, &rh); + qreal penWidth = 0.25 * qMin(rw, rh); + qreal dimx = rw - penWidth; + qreal dimy = rh - penWidth; + if (dimx < 0.5 || dimy < 0.5) + return; + qreal x = (rw - dimx) / 2 + rx; + qreal y = (rh - dimy) / 2 + ry; + QPointF p0(0.0 * dimx + x, 0.5 * dimy + y); + QPointF p1(1.0 * dimx + x, 0.5 * dimy + y); + scratchPen.setBrush(swatch.brush(color)); + scratchPen.setCapStyle(Qt::RoundCap); + scratchPen.setWidthF(penWidth); + Phantom::PSave save(painter); + if (!painter->testRenderHint(QPainter::Antialiasing)) + painter->setRenderHint(QPainter::Antialiasing); + painter->setPen(scratchPen); + painter->setBrush(Qt::NoBrush); + painter->drawLine(p0, p1); + } + + Q_NEVER_INLINE void + drawMdiButton(QPainter* painter, const QStyleOptionTitleBar* option, QRect tmp, bool hover, bool sunken) + { + QColor dark; + dark.setHsv(option->palette.button().color().hue(), + qMin(255, (option->palette.button().color().saturation())), + qMin(255, option->palette.button().color().value() * 0.7)); + QColor highlight = option->palette.highlight().color(); + bool active = (option->titleBarState & QStyle::State_Active); + QColor titleBarHighlight(255, 255, 255, 60); + if (sunken) + painter->fillRect(tmp.adjusted(1, 1, -1, -1), option->palette.highlight().color().darker(120)); + else if (hover) + painter->fillRect(tmp.adjusted(1, 1, -1, -1), QColor(255, 255, 255, 20)); + if (sunken) + titleBarHighlight = highlight.darker(130); + QColor mdiButtonBorderColor(active ? option->palette.highlight().color().darker(180) : dark.darker(110)); + painter->setPen(QPen(mdiButtonBorderColor)); + const QLine lines[4] = {QLine(tmp.left() + 2, tmp.top(), tmp.right() - 2, tmp.top()), + QLine(tmp.left() + 2, tmp.bottom(), tmp.right() - 2, tmp.bottom()), + QLine(tmp.left(), tmp.top() + 2, tmp.left(), tmp.bottom() - 2), + QLine(tmp.right(), tmp.top() + 2, tmp.right(), tmp.bottom() - 2)}; + painter->drawLines(lines, 4); + const QPoint points[4] = {QPoint(tmp.left() + 1, tmp.top() + 1), + QPoint(tmp.right() - 1, tmp.top() + 1), + QPoint(tmp.left() + 1, tmp.bottom() - 1), + QPoint(tmp.right() - 1, tmp.bottom() - 1)}; + painter->drawPoints(points, 4); + painter->setPen(titleBarHighlight); + painter->drawLine(tmp.left() + 2, tmp.top() + 1, tmp.right() - 2, tmp.top() + 1); + painter->drawLine(tmp.left() + 1, tmp.top() + 2, tmp.left() + 1, tmp.bottom() - 2); + } + + Q_NEVER_INLINE void fillRectOutline(QPainter* p, QRect rect, QMargins margins, const QColor& brush) + { + int x, y, w, h; + rect.getRect(&x, &y, &w, &h); + int ml = margins.left(); + int mt = margins.top(); + int mr = margins.right(); + int mb = margins.bottom(); + QRect r0(x, y, w, mt); + QRect r1(x, y + mt, ml, h - (mt + mb)); + QRect r2((x + w) - mr, y + mt, mr, h - (mt + mb)); + QRect r3(x, (y + h) - mb, w, mb); + p->fillRect(r0 & rect, brush); + p->fillRect(r1 & rect, brush); + p->fillRect(r2 & rect, brush); + p->fillRect(r3 & rect, brush); + } + void fillRectOutline(QPainter* p, QRect rect, int thickness, const QColor& color) + { + fillRectOutline(p, rect, QMargins(thickness, thickness, thickness, thickness), color); + } + Q_NEVER_INLINE void + fillRectEdges(QPainter* p, QRect rect, Qt::Edges edges, QMargins margins, const QColor& color) + { + int x, y, w, h; + rect.getRect(&x, &y, &w, &h); + if (edges & Qt::LeftEdge) { + int ml = margins.left(); + QRect r0(x, y, ml, h); + p->fillRect(r0 & rect, color); + } + if (edges & Qt::TopEdge) { + int mt = margins.top(); + QRect r1(x, y, w, mt); + p->fillRect(r1 & rect, color); + } + if (edges & Qt::RightEdge) { + int mr = margins.right(); + QRect r2((x + w) - mr, y, mr, h); + p->fillRect(r2 & rect, color); + } + if (edges & Qt::BottomEdge) { + int mb = margins.bottom(); + QRect r3(x, (y + h) - mb, w, mb); + p->fillRect(r3 & rect, color); + } + } + void fillRectEdges(QPainter* p, QRect rect, Qt::Edges edges, int thickness, const QColor& color) + { + fillRectEdges(p, rect, edges, QMargins(thickness, thickness, thickness, thickness), color); + } + inline QRect expandRect(QRect rect, Qt::Edges edges, int delta) + { + int l = edges & Qt::LeftEdge ? -delta : 0; + int t = edges & Qt::TopEdge ? -delta : 0; + int r = edges & Qt::RightEdge ? delta : 0; + int b = edges & Qt::BottomEdge ? delta : 0; + return rect.adjusted(l, t, r, b); + } + inline Qt::Edge oppositeEdge(Qt::Edge edge) + { + switch (edge) { + case Qt::LeftEdge: + return Qt::RightEdge; + case Qt::TopEdge: + return Qt::BottomEdge; + case Qt::RightEdge: + return Qt::LeftEdge; + case Qt::BottomEdge: + return Qt::TopEdge; + } + return Qt::TopEdge; + } + inline QRect rectTranslatedTowardEdge(QRect rect, Qt::Edge edge, int delta) + { + switch (edge) { + case Qt::LeftEdge: + return rect.translated(-delta, 0); + case Qt::TopEdge: + return rect.translated(0, -delta); + case Qt::RightEdge: + return rect.translated(delta, 0); + case Qt::BottomEdge: + return rect.translated(0, delta); + } + return rect; + } + Q_NEVER_INLINE QRect rectFromInnerEdgeWithThickness(QRect rect, Qt::Edge edge, int thickness) + { + int x, y, w, h; + rect.getRect(&x, &y, &w, &h); + QRect r; + switch (edge) { + case Qt::LeftEdge: + r = QRect(x, y, thickness, h); + break; + case Qt::TopEdge: + r = QRect(x, y, w, thickness); + break; + case Qt::RightEdge: + r = QRect((x + w) - thickness, y, thickness, h); + break; + case Qt::BottomEdge: + r = QRect(x, (y + h) - thickness, w, thickness); + break; + } + return r & rect; + } + Q_NEVER_INLINE void + paintSolidRoundRect(QPainter* p, QRect rect, qreal radius, const PhSwatch& swatch, Swatchy fill) + { + if (!fill) + return; + bool aa = p->testRenderHint(QPainter::Antialiasing); + if (radius > 0.5) { + if (!aa) + p->setRenderHint(QPainter::Antialiasing); + p->setPen(swatch.pen(SwatchColors::S_none)); + p->setBrush(swatch.brush(fill)); + p->drawRoundedRect(rect, radius, radius); + } else { + if (aa) + p->setRenderHint(QPainter::Antialiasing, false); + p->fillRect(rect, swatch.color(fill)); + } + } + Q_NEVER_INLINE void paintBorderedRoundRect(QPainter* p, + QRect rect, + qreal radius, + const PhSwatch& swatch, + Swatchy stroke, + Swatchy fill) + { + if (rect.width() < 1 || rect.height() < 1) + return; + if (!stroke && !fill) + return; + bool aa = p->testRenderHint(QPainter::Antialiasing); + if (radius > 0.5) { + if (!aa) + p->setRenderHint(QPainter::Antialiasing); + p->setPen(swatch.pen(stroke)); + p->setBrush(swatch.brush(fill)); + QRectF rf(rect.x() + 0.5, rect.y() + 0.5, rect.width() - 1.0, rect.height() - 1.0); + p->drawRoundedRect(rf, radius, radius); + } else { + if (aa) + p->setRenderHint(QPainter::Antialiasing, false); + if (stroke) { + fillRectOutline(p, rect, 1, swatch.color(stroke)); + } + if (fill) { + p->fillRect(rect.adjusted(1, 1, -1, -1), swatch.color(fill)); + } + } + } + } // namespace +} // namespace Phantom + +BaseStylePrivate::BaseStylePrivate() + : headSwatchFastKey(0) +{ +} + +BaseStyle::BaseStyle() + : d(new BaseStylePrivate) +{ + setObjectName(QLatin1String("Phantom")); +} + +BaseStyle::~BaseStyle() +{ + delete d; +} + +// Draw text in a rectangle. The current pen set on the painter is used, unless +// an explicit textRole is set, in which case the palette will be used. The +// enabled bool indicates whether the text is enabled or not, and can influence +// how the text is drawn outside of just color. Wrapping and alignment flags +// can be passed in `alignment`. +void BaseStyle::drawItemText(QPainter* painter, + const QRect& rect, + int alignment, + const QPalette& pal, + bool enabled, + const QString& text, + QPalette::ColorRole textRole) const +{ + Q_UNUSED(enabled); + if (text.isEmpty()) + return; + if (textRole == QPalette::NoRole) { + painter->drawText(rect, alignment, text); + return; + } + QPen savedPen = painter->pen(); + const QBrush& newBrush = pal.brush(textRole); + bool changed = false; + if (savedPen.brush() != newBrush) { + changed = true; + painter->setPen(QPen(newBrush, savedPen.widthF())); + } + painter->drawText(rect, alignment, text); + if (changed) { + painter->setPen(savedPen); + } +} + +void BaseStyle::drawPrimitive(PrimitiveElement elem, + const QStyleOption* option, + QPainter* painter, + const QWidget* widget) const +{ + Q_ASSERT(option); + if (!option) + return; +#ifdef BUILD_WITH_EASY_PROFILER + EASY_BLOCK("drawPrimitive"); + const char* elemCString = QMetaEnum::fromType().valueToKey(elem); + EASY_TEXT("Element", elemCString); +#endif + using Swatchy = Phantom::Swatchy; + using namespace Phantom::SwatchColors; + namespace Ph = Phantom; + auto ph_swatchPtr = getCachedSwatchOfQPalette(&d->swatchCache, &d->headSwatchFastKey, option->palette); + const Ph::PhSwatch& swatch = *ph_swatchPtr.data(); + const int state = option->state; + // Cast to int here to suppress warnings about cases listed which are not in + // the original enum. This is for custom primitive elements. + switch (static_cast(elem)) { + case PE_Frame: { + if (widget && widget->inherits("QComboBoxPrivateContainer")) { + QStyleOption copy = *option; + copy.state |= State_Raised; + proxy()->drawPrimitive(PE_PanelMenu, ©, painter, widget); + break; + } + Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_frame_outline)); + break; + } + case PE_FrameMenu: { + break; + } + case PE_FrameDockWidget: { + painter->save(); + QColor softshadow = option->palette.background().color().darker(120); + QRect r = option->rect; + painter->setPen(softshadow); + painter->drawRect(r.adjusted(0, 0, -1, -1)); + painter->setPen(QPen(option->palette.light(), 1)); + painter->drawLine(QPoint(r.left() + 1, r.top() + 1), QPoint(r.left() + 1, r.bottom() - 1)); + painter->setPen(QPen(option->palette.background().color().darker(120))); + painter->drawLine(QPoint(r.left() + 1, r.bottom() - 1), QPoint(r.right() - 2, r.bottom() - 1)); + painter->drawLine(QPoint(r.right() - 1, r.top() + 1), QPoint(r.right() - 1, r.bottom() - 1)); + painter->restore(); + break; + } + case PE_FrameGroupBox: { + QRect frame = option->rect; + Ph::PSave save(painter); + bool isFlat = false; + if (auto groupBox = qstyleoption_cast(option)) { + isFlat = groupBox->features & QStyleOptionFrame::Flat; + } else if (auto frameOpt = qstyleoption_cast(option)) { + isFlat = frameOpt->features & QStyleOptionFrame::Flat; + } + if (isFlat) { + Ph::fillRectEdges(painter, frame, Qt::TopEdge, 1, swatch.color(S_window_divider)); + } else { + Ph::paintBorderedRoundRect(painter, frame, Ph::GroupBox_Rounding, swatch, S_frame_outline, S_none); + } + break; + } + case PE_IndicatorBranch: { + if (!(option->state & State_Children)) + break; + Qt::ArrowType arrow; + if (option->state & State_Open) { + arrow = Qt::DownArrow; + } else if (option->direction != Qt::RightToLeft) { + arrow = Qt::RightArrow; + } else { + arrow = Qt::LeftArrow; + } + bool useSelectionColor = false; + if (option->state & State_Selected) { + if (auto ivopt = qstyleoption_cast(option)) { + useSelectionColor = ivopt->showDecorationSelected; + } + } + Swatchy color = useSelectionColor ? S_highlightedText : S_indicator_current; + QRect r = option->rect; + if (Ph::BranchesOnEdge) { + // TODO RTL + r.moveLeft(0); + if (r.width() < r.height()) + r.setWidth(r.height()); + } + int adj = qMin(r.width(), r.height()) / 4; + r.adjust(adj, adj, -adj, -adj); + Ph::drawArrow(painter, r, arrow, swatch.brush(color)); + break; + } + case PE_IndicatorMenuCheckMark: { + // For this PE, QCommonStyle treats State_On as drawing the check with the + // highlighted text color, and otherwise with the regular text color. I + // guess we should match that behavior, even though it's not consistent + // with other check box/mark drawing in QStyle (buttons and item view + // items.) QCommonStyle also doesn't care about tri-state or unchecked + // states -- it seems that if you call this, you want a check, and nothing + // else. + // + // We'll also catch State_Selected and treat it equivalently (the way you'd + // expect.) We'll use windowText instead of text, though -- probably + // doesn't matter. + Swatchy fgColor = S_windowText; + bool isSelected = option->state & (State_Selected | State_On); + bool isEnabled = option->state & State_Enabled; + if (isSelected) { + fgColor = S_highlightedText; + } else if (!isEnabled) { + fgColor = S_windowText_disabled; + } + qreal rx, ry, rw, rh; + QRectF(option->rect).getRect(&rx, &ry, &rw, &rh); + qreal dim = qMin(rw, rh); + const qreal insetScale = 0.8; + qreal dimx = dim * insetScale * Ph::CheckMark_WidthOfHeightScale; + qreal dimy = dim * insetScale; + QRectF r_(rx + (rw - dimx) / 2, ry + (rh - dimy) / 2, dimx, dimy); + Ph::drawCheck(painter, d->checkBox_pen_scratch, r_, swatch, fgColor); + break; + } + // Called for the content area on tree view rows that are selected + case PE_PanelItemViewItem: { + QCommonStyle::drawPrimitive(elem, option, painter, widget); + break; + } + // Called for left-of-item-content-area on tree view rows that are selected + case PE_PanelItemViewRow: { + QCommonStyle::drawPrimitive(elem, option, painter, widget); + break; + } + case PE_FrameTabBarBase: { + auto tbb = qstyleoption_cast(option); + if (!tbb) + break; + Qt::Edge edge = Qt::TopEdge; + switch (tbb->shape) { + case QTabBar::RoundedNorth: + case QTabBar::TriangularNorth: + edge = Qt::TopEdge; + break; + case QTabBar::RoundedSouth: + case QTabBar::TriangularSouth: + edge = Qt::BottomEdge; + break; + case QTabBar::RoundedWest: + case QTabBar::TriangularWest: + edge = Qt::LeftEdge; + break; + case QTabBar::RoundedEast: + case QTabBar::TriangularEast: + edge = Qt::RightEdge; + break; + } + Ph::fillRectEdges(painter, option->rect, edge, 1, swatch.color(S_frame_outline)); + // TODO need to check here if we're drawing with window or button color as + // the frame fill. Assuming window right now, but could be wrong. + Ph::fillRectEdges(painter, Ph::expandRect(option->rect, edge, -1), edge, 1, swatch.color(S_tabFrame_specular)); + break; + } + case PE_PanelScrollAreaCorner: { + bool isLeftToRight = option->direction != Qt::RightToLeft; + Qt::Edges edges = Qt::TopEdge; + QRect bgRect = option->rect; + if (isLeftToRight) { + edges |= Qt::LeftEdge; + bgRect.setX(bgRect.x() + 1); + } else { + edges |= Qt::RightEdge; + bgRect.setWidth(bgRect.width() - 1); + } + painter->fillRect(bgRect, swatch.color(S_window)); + Ph::fillRectEdges(painter, option->rect, edges, 1, swatch.color(S_window_outline)); + break; + } + case PE_IndicatorArrowUp: + case PE_IndicatorArrowDown: + case PE_IndicatorArrowRight: + case PE_IndicatorArrowLeft: { + int rx, ry, rw, rh; + option->rect.getRect(&rx, &ry, &rw, &rh); + if (rw <= 1 || rh <= 1) + break; + Qt::ArrowType arrow = Qt::UpArrow; + switch (elem) { + case PE_IndicatorArrowUp: + arrow = Qt::UpArrow; + break; + case PE_IndicatorArrowDown: + arrow = Qt::DownArrow; + break; + case PE_IndicatorArrowRight: + arrow = Qt::RightArrow; + break; + case PE_IndicatorArrowLeft: + arrow = Qt::LeftArrow; + break; + default: + break; + } + // The caller may give us a huge rect and expect a normal-sized icon inside + // of it, so we don't want to fill the entire thing with an arrow, + // otherwise certain buttons will look weird, like the tab bar scroll + // buttons. Might want to break these out into editable parameters? + const int MaxArrowExt = Ph::dpiScaled(12); + const int MinMargin = qMin(rw, rh) / 4; + int aw, ah; + aw = qMin(MaxArrowExt, rw) - MinMargin; + ah = qMin(MaxArrowExt, rh) - MinMargin; + if (aw <= 2 || ah <= 2) + break; + // QCommonStyle's implementation of CC_ToolButton for non-instant popups + // gives us a pretty big rectangle to draw the arrow in -- shrink it. This + // is kind of a dirty temp hack thing until we do something smarter, like + // fully reimplement CC_ToolButton. Note that it passes us a regular + // QStyleOption and not a QStyleOptionToolButton in this case, so try to + // save some work before doing the inherits test. + if (arrow == Qt::DownArrow && !qstyleoption_cast(option) && widget) { + auto tbutton = qobject_cast(widget); + if (tbutton && tbutton->popupMode() != QToolButton::InstantPopup && tbutton->defaultAction()) { + int dim = static_cast(qMin(rw, rh) * 0.25); + aw -= dim; + ah -= dim; + // We have another hack in PE_IndicatorButtonDropDown where we shift + // the edge left or right by 1px to avoid having two borders touching + // (we make it overlap instead.) So we'll need to compensate for that + // in the arrow's position to avoid it looking off-center. + rw += 1; + if (option->direction != Qt::RightToLeft) { + rx -= 1; + } + } + } + aw += (rw - aw) % 2; + ah += (rh - ah) % 2; + int ax = (rw - aw) / 2 + rx; + int ay = (rh - ah) / 2 + ry; + Ph::drawArrow(painter, QRect(ax, ay, aw, ah), arrow, swatch); + break; + } + case PE_IndicatorItemViewItemCheck: { + QStyleOptionButton button; + button.QStyleOption::operator=(*option); + button.state &= ~State_MouseOver; + proxy()->drawPrimitive(PE_IndicatorCheckBox, &button, painter, widget); + return; + } + case PE_IndicatorHeaderArrow: { + auto header = qstyleoption_cast(option); + if (!header) + return; + QRect r = header->rect; + QPoint offset = QPoint(Phantom::HeaderSortIndicator_HOffset, Phantom::HeaderSortIndicator_VOffset); + if (header->sortIndicator & QStyleOptionHeader::SortUp) { + Ph::drawArrow(painter, r.translated(offset), Qt::DownArrow, swatch); + } else if (header->sortIndicator & QStyleOptionHeader::SortDown) { + Ph::drawArrow(painter, r.translated(offset), Qt::UpArrow, swatch); + } + break; + } + case PE_IndicatorButtonDropDown: { + // Temp hack until we implement CC_ToolButton: avoid double-stacked border + // by clipping off one edge slightly. + QStyleOption opt0 = *option; + if (opt0.direction != Qt::RightToLeft) { + opt0.rect.adjust(-1, 0, 0, 0); + } else { + opt0.rect.adjust(0, 0, 1, 0); + } + proxy()->drawPrimitive(PE_PanelButtonTool, &opt0, painter, widget); + break; + } + + case PE_IndicatorToolBarSeparator: { + QRect r = option->rect; + if (option->state & State_Horizontal) { + if (r.height() >= 10) + r.adjust(0, 3, 0, -3); + r.setWidth(r.width() / 2 + 1); + Ph::fillRectEdges(painter, r, Qt::RightEdge, 1, swatch.color(S_window_divider)); + } else { + // TODO replace with new code + const int margin = 6; + const int offset = r.height() / 2; + painter->setPen(QPen(option->palette.background().color().darker(110))); + painter->drawLine(r.topLeft().x() + margin, + r.topLeft().y() + offset, + r.topRight().x() - margin, + r.topRight().y() + offset); + painter->setPen(QPen(option->palette.background().color().lighter(110))); + painter->drawLine(r.topLeft().x() + margin, + r.topLeft().y() + offset + 1, + r.topRight().x() - margin, + r.topRight().y() + offset + 1); + } + break; + } + case PE_PanelButtonTool: { + bool isDown = option->state & State_Sunken; + bool isOn = option->state & State_On; + bool hasFocus = (option->state & State_HasFocus && option->state & State_KeyboardFocusChange); + const qreal rounding = Ph::ToolButton_Rounding; + Swatchy outline = S_window_outline; + Swatchy fill = S_button; + Swatchy specular = S_button_specular; + if (isDown) { + fill = S_button_pressed; + specular = S_button_pressed_specular; + } else if (isOn) { + fill = S_button_on; + specular = S_none; + } + if (hasFocus) { + outline = S_highlight_outline; + } + QRect r = option->rect; + Ph::PSave save(painter); + Ph::paintBorderedRoundRect(painter, r, rounding, swatch, outline, fill); + Ph::paintBorderedRoundRect(painter, r.adjusted(1, 1, -1, -1), rounding, swatch, specular, S_none); + break; + } + case PE_IndicatorDockWidgetResizeHandle: { + QStyleOption dockWidgetHandle = *option; + bool horizontal = option->state & State_Horizontal; + dockWidgetHandle.state = + !horizontal ? (dockWidgetHandle.state | State_Horizontal) : (dockWidgetHandle.state & ~State_Horizontal); + proxy()->drawControl(CE_Splitter, &dockWidgetHandle, painter, widget); + break; + } + case PE_FrameWindow: { + break; + } + case PE_FrameLineEdit: { + QRect r = option->rect; + bool hasFocus = option->state & State_HasFocus; + bool isEnabled = option->state & State_Enabled; + const qreal rounding = Ph::LineEdit_Rounding; + auto pen = hasFocus ? S_highlight_outline : S_window_outline; + Ph::PSave save(painter); + Ph::paintBorderedRoundRect(painter, r, rounding, swatch, pen, S_none); + save.restore(); + if (Ph::OverhangShadows && !hasFocus && isEnabled) { + // Imperfect when rounded, may leave a gap on left and right. Going + // closer would eat into the outline, though. + Ph::fillRectEdges(painter, + r.adjusted(qRound(rounding / 2) + 1, 1, -(qRound(rounding / 2) + 1), -1), + Qt::TopEdge, + 1, + swatch.color(S_base_shadow)); + } + break; + } + case PE_PanelLineEdit: { + auto panel = qstyleoption_cast(option); + if (!panel) + break; + Ph::PSave save(painter); + // We intentionally don't inset the fill rect, even if the frame will paint + // over the perimeter, because an inset with rounding enabled may cause + // some miscolored separated pixels between the fill and the border, since + // we're forced to paint them in two separate draw calls. + Ph::paintSolidRoundRect(painter, option->rect, Ph::LineEdit_Rounding, swatch, S_base); + save.restore(); + if (panel->lineWidth > 0) + proxy()->drawPrimitive(PE_FrameLineEdit, option, painter, widget); + break; + } + case PE_IndicatorCheckBox: { + auto checkbox = qstyleoption_cast(option); + if (!checkbox) + break; + QRect r = option->rect; + bool isHighlighted = option->state & State_HasFocus && option->state & State_KeyboardFocusChange; + bool isSelected = option->state & State_Selected; + bool isFlat = checkbox->features & QStyleOptionButton::Flat; + bool isEnabled = option->state & State_Enabled; + bool isPressed = state & State_Sunken; + Swatchy outlineColor = isHighlighted ? S_highlight_outline : S_window_outline; + Swatchy bgFillColor = isPressed ? S_highlight : S_base; + Swatchy fgColor = isFlat ? S_windowText : S_text; + if (isPressed && !isFlat) { + fgColor = S_highlightedText; + } + // Bare checkmarks that are selected should draw with the highlighted text + // color. + if (isSelected && isFlat) { + fgColor = S_highlightedText; + } + if (!isFlat) { + QRect fillR = r; + Ph::fillRectOutline(painter, fillR, 1, swatch.color(outlineColor)); + fillR.adjust(1, 1, -1, -1); + if (Ph::IndicatorShadows && !isPressed && isEnabled) { + Ph::fillRectEdges(painter, fillR, Qt::TopEdge, 1, swatch.color(S_base_shadow)); + fillR.adjust(0, 1, 0, 0); + } + painter->fillRect(fillR, swatch.color(bgFillColor)); + } + if (checkbox->state & State_NoChange) { + const qreal insetScale = 0.7; + qreal rx, ry, rw, rh; + QRectF(r.adjusted(1, 1, -1, -1)).getRect(&rx, &ry, &rw, &rh); + qreal dimx = rw * insetScale; + qreal dimy = rh * insetScale; + QRectF r_(rx + (rw - dimx) / 2, ry + (rh - dimy) / 2, dimx, dimy); + Ph::drawHyphen(painter, d->checkBox_pen_scratch, r_, swatch, fgColor); + } else if (checkbox->state & State_On) { + const qreal insetScale = 0.8; + qreal rx, ry, rw, rh; + QRectF(r.adjusted(1, 1, -1, -1)).getRect(&rx, &ry, &rw, &rh); + // kinda wrong, assumes we're already square, but we probably are + qreal dimx = rw * insetScale * Ph::CheckMark_WidthOfHeightScale; + qreal dimy = rh * insetScale; + QRectF r_(rx + (rw - dimx) / 2, ry + (rh - dimy) / 2, dimx, dimy); + Ph::drawCheck(painter, d->checkBox_pen_scratch, r_, swatch, fgColor); + } + break; + } + case PE_IndicatorRadioButton: { + qreal rx, ry, rw, rh; + QRectF(option->rect).getRect(&rx, &ry, &rw, &rh); + bool isHighlighted = option->state & State_HasFocus && option->state & State_KeyboardFocusChange; + bool isSunken = state & State_Sunken; + bool isEnabled = state & State_Enabled; + Swatchy outlineColor = isHighlighted ? S_highlight_outline : S_window_outline; + Swatchy bgFillColor = isSunken ? S_highlight : S_base; + QPointF circleCenter(rx + rw / 2.0, ry + rh / 2.0); + const qreal lineThickness = 1.0; + qreal outlineRadius = (qMin(rw, rh) - lineThickness) / 2.0; + qreal fillRadius = outlineRadius - lineThickness / 2.0; + Ph::PSave save(painter); + painter->setRenderHint(QPainter::Antialiasing); + painter->setBrush(swatch.brush(bgFillColor)); + painter->setPen(swatch.pen(outlineColor)); + painter->drawEllipse(circleCenter, outlineRadius, outlineRadius); + if (Ph::IndicatorShadows && !isSunken && isEnabled) { + // Really slow, just a temp demo test + painter->setPen(Qt::NoPen); + painter->setBrush(swatch.brush(S_base_shadow)); + QPainterPath path0, path1; + path0.addEllipse(circleCenter, fillRadius, fillRadius); + path1.addEllipse(circleCenter + QPointF(0, 1.25), fillRadius, fillRadius); + QPainterPath path2 = path0 - path1; + painter->drawPath(path2); + } + if (state & State_On) { + Swatchy fgColor = isSunken ? S_highlightedText : S_windowText; + qreal checkmarkRadius = outlineRadius / 2.32; + painter->setPen(Qt::NoPen); + painter->setBrush(swatch.brush(fgColor)); + painter->drawEllipse(circleCenter, checkmarkRadius, checkmarkRadius); + } + break; + } + case PE_IndicatorToolBarHandle: { + if (!option) + break; + QRect r = option->rect; + if (r.width() < 3 || r.height() < 3) + break; + int rows = 3; + int columns = 2; + if (option->state & State_Horizontal) { + } else { + qSwap(columns, rows); + } + int dotLen = Ph::dpiScaled(2); + QSize occupied(dotLen * (columns * 2 - 1), dotLen * (rows * 2 - 1)); + QRect rr = QStyle::alignedRect(option->direction, Qt::AlignCenter, QSize(occupied), r); + int x = rr.x(); + int y = rr.y(); + for (int row = 0; row < rows; ++row) { + for (int col = 0; col < columns; ++col) { + int x_ = x + col * 2 * dotLen; + int y_ = y + row * 2 * dotLen; + painter->fillRect(x_, y_, dotLen, dotLen, swatch.color(S_window_divider)); + } + } + break; + } + case PE_FrameDefaultButton: + break; + case PE_FrameFocusRect: { + auto fropt = qstyleoption_cast(option); + if (!fropt) + break; + //### check for d->alt_down + if (!(fropt->state & State_KeyboardFocusChange)) + return; + if (fropt->state & State_Item) { + if (auto itemView = qobject_cast(widget)) { + // TODO either our grid line hack is interfering, or Qt has a bug, but + // in RTL layout the grid borders can leave junk behind in the grid + // areas and the right edge of the focus rect may not get painted. + // (Sometimes it will, though.) To replicate, set to RTL mode, and move + // the current around in a table view without the selection being on + // the current. + if (option->state & QStyle::State_Selected) { + bool showCurrent = true; + bool hasTableGrid = false; + const auto selectionMode = itemView->selectionMode(); + if (selectionMode == QAbstractItemView::SingleSelection) { + showCurrent = false; + } else { + // Table views will can have a "current" frame drawn even if the + // "current" is within the selected range. Other item views won't, + // which means the "current" frame will be invisible if it's on a + // selected item. This is a compromise between the broken drawing + // behavior of Qt item views of drawing "current" frames when they + // don't make sense (like a tree view where you can only select + // entire rows, but Qt will the frame rect around whatever column + // was last clicked on by the mouse, but using keyboard navigation + // has no effect) and not drawing them at all. + bool isTableView = false; + if (auto tableView = qobject_cast(itemView)) { + hasTableGrid = tableView->showGrid(); + isTableView = true; + } + const auto selectionModel = itemView->selectionModel(); + if (selectionModel) { + const auto selection = selectionModel->selection(); + if (selection.count() == 1) { + const auto& range = selection.at(0); + if (isTableView) { + // For table views, we don't draw the "current" frame if + // there is exactly one cell selected and the "current" is + // that cell, or if there is exactly one row or one column + // selected with the behavior set to the corresponding + // selection, and the "current" is that one row or column. + const auto selectionBehavior = itemView->selectionBehavior(); + if ((range.width() == 1 && range.height() == 1) + || (selectionBehavior == QAbstractItemView::SelectRows && range.height() == 1) + || (selectionBehavior == QAbstractItemView::SelectColumns + && range.width() == 1)) { + showCurrent = false; + } + } else { + // For any other type of item view, don't draw the "current" + // frame if there is a single contiguous selection, and the + // "current" is within that selection. If there's a + // discontiguous selection, that means the user is probably + // doing something more advanced, and we should just draw the + // focus frame, even if Qt might be doing it badly in some + // cases. + showCurrent = false; + } + } + } + } + if (showCurrent) { + // TODO handle dark-highlight-light-text + const QColor& borderColor = swatch.color(S_itemView_multiSelection_currentBorder); + const int thickness = hasTableGrid ? 2 : 1; + Ph::fillRectOutline(painter, option->rect, thickness, borderColor); + } + } else { + Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_highlight_outline)); + } + break; + } + } + // It would be nice to also handle QTreeView's allColumnsShowFocus thing in + // the above code, in addition to the normal cases for focus rects in item + // views. Unfortunately, with allColumnsShowFocus set to true, + // QTreeView::drawRow() calls the style to paint with PE_FrameFocusRect for + // the row frame with the widget set to nullptr. This makes it basically + // impossible to figure out that we need to draw a special frame for it. + // So, if any application code is using that mode in a QTreeView, it won't + // get special item view frames. Too bad. + Ph::PSave save(painter); + Ph::paintBorderedRoundRect( + painter, option->rect, Ph::FrameFocusRect_Rounding, swatch, S_highlight_outline, S_none); + break; + } + case PE_PanelButtonCommand: + case PE_PanelButtonBevel: { + bool isDefault = false; + bool isFlat = false; + bool isDown = option->state & State_Sunken; + bool isOn = option->state & State_On; + if (auto button = qstyleoption_cast(option)) { + isDefault = (button->features & QStyleOptionButton::DefaultButton) && (button->state & State_Enabled); + isFlat = (button->features & QStyleOptionButton::Flat); + } + if (isFlat && !isDown && !isOn) + break; + bool isEnabled = option->state & State_Enabled; + Q_UNUSED(isEnabled); + bool hasFocus = (option->state & State_HasFocus && option->state & State_KeyboardFocusChange); + const qreal rounding = Ph::PushButton_Rounding; + Swatchy outline = S_window_outline; + Swatchy fill = S_button; + Swatchy specular = S_button_specular; + if (isDown) { + fill = S_button_pressed; + specular = S_button_pressed_specular; + } else if (isOn) { + // kinda repurposing this, hmm + fill = S_scrollbarGutter; + specular = S_button_pressed_specular; + } + if (hasFocus || isDefault) { + outline = S_highlight_outline; + } + QRect r = option->rect; + Ph::PSave save(painter); + Ph::paintBorderedRoundRect(painter, r, rounding, swatch, outline, fill); + Ph::paintBorderedRoundRect(painter, r.adjusted(1, 1, -1, -1), rounding, swatch, specular, S_none); + break; + } + case PE_FrameTabWidget: { + QRect bgRect = option->rect.adjusted(1, 1, -1, -1); + painter->fillRect(bgRect, swatch.color(S_tabFrame)); + auto twf = qstyleoption_cast(option); + if (!twf) + break; + Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_frame_outline)); + Ph::fillRectOutline(painter, bgRect, 1, swatch.color(S_tabFrame_specular)); + break; + } + case PE_FrameStatusBarItem: + break; + case PE_IndicatorTabClose: + case Phantom_PE_IndicatorTabNew: { + Swatchy fg = S_windowText; + Swatchy bg = S_none; + if ((option->state & State_Enabled) && (option->state & State_MouseOver)) { + fg = S_highlightedText; + bg = option->state & State_Sunken ? S_highlight_outline : S_highlight; + } + // temp code + Ph::PSave save(painter); + if (bg) { + Ph::paintSolidRoundRect(painter, option->rect, Ph::PushButton_Rounding, swatch, bg); + } + QPen pen = swatch.pen(fg); + pen.setCapStyle(Qt::RoundCap); + pen.setWidthF(1.5); + painter->setBrush(Qt::NoBrush); + painter->setPen(pen); + painter->setRenderHint(QPainter::Antialiasing); + QRect r = option->rect; + // int adj = (int)((qreal)qMin(r.width(), r.height()) * (1.0 / 2.5)); + int adj = Ph::dpiScaled(5.0); + r.adjust(adj, adj, -adj, -adj); + qreal x, y, w, h; + QRectF(r).getRect(&x, &y, &w, &h); + // painter->translate(-0.5, -0.5); + switch (static_cast(elem)) { + case PE_IndicatorTabClose: + painter->drawLine(QPointF(x - 0.5, y - 0.5), QPointF(x + 0.5 + w, y + 0.5 + h)); + painter->drawLine(QPointF(x - 0.5, y + h + 0.5), QPointF(x + 0.5 + w, y - 0.5)); + break; + case Phantom_PE_IndicatorTabNew: + // kinda hacky here on extra len + painter->drawLine(QPointF(x + w / 2, y - 1.0), QPointF(x + w / 2, y + h + 1.0)); + painter->drawLine(QPointF(x - 1.0, y + h / 2), QPointF(x + w + 1.0, y + h / 2)); + break; + } + save.restore(); + // painter->fillRect(option->rect, QColor(255, 0, 0, 30)); + break; + } + case PE_PanelMenu: { + bool isBelowMenuBar = false; + // works but currently unused + // QPoint gp = widget->mapToGlobal(widget->rect().topLeft()); + // gp.setY(gp.y() - 1); + // QWidget* bar = qApp->widgetAt(gp); + // if (bar && bar->inherits("QMenuBar")) { + // isBelowMenuBar = true; + // } + Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_window_divider)); + QRect bgRect = option->rect.adjusted(1, isBelowMenuBar ? 0 : 1, -1, -1); + painter->fillRect(bgRect, swatch.color(S_window)); + break; + } + case Phantom_PE_ScrollBarSliderVertical: { + bool isLeftToRight = option->direction != Qt::RightToLeft; + bool isSunken = option->state & State_Sunken; + Swatchy thumbFill, thumbSpecular; + if (isSunken) { + thumbFill = S_button_pressed; + thumbSpecular = S_button_pressed_specular; + } else { + thumbFill = S_button; + thumbSpecular = S_button_specular; + } + Qt::Edges edges; + QRect edgeRect = option->rect; + QRect mainRect = option->rect; + edgeRect.adjust(0, -1, 0, 1); + if (isLeftToRight) { + edges = Qt::LeftEdge | Qt::TopEdge | Qt::BottomEdge; + mainRect.setX(mainRect.x() + 1); + } else { + edges = Qt::TopEdge | Qt::BottomEdge | Qt::RightEdge; + mainRect.setWidth(mainRect.width() - 1); + } + Ph::fillRectEdges(painter, edgeRect, edges, 1, swatch.color(S_window_outline)); + painter->fillRect(mainRect, swatch.color(thumbFill)); + Ph::fillRectOutline(painter, mainRect, 1, swatch.color(thumbSpecular)); + break; + } + case Phantom_PE_WindowFrameColor: { + painter->fillRect(option->rect, swatch.color(S_window_outline)); + break; + } + default: + QCommonStyle::drawPrimitive(elem, option, painter, widget); + break; + } +} + +void BaseStyle::drawControl(ControlElement element, + const QStyleOption* option, + QPainter* painter, + const QWidget* widget) const +{ +#ifdef BUILD_WITH_EASY_PROFILER + EASY_BLOCK("drawControl"); + const char* elemCString = QMetaEnum::fromType().valueToKey(element); + EASY_TEXT("Element", elemCString); +#endif + using Swatchy = Phantom::Swatchy; + using namespace Phantom::SwatchColors; + namespace Ph = Phantom; + auto ph_swatchPtr = Ph::getCachedSwatchOfQPalette(&d->swatchCache, &d->headSwatchFastKey, option->palette); + const Ph::PhSwatch& swatch = *ph_swatchPtr.data(); + + switch (element) { + case CE_CheckBox: { + QCommonStyle::drawControl(element, option, painter, widget); + // painter->fillRect(option->rect, QColor(255, 0, 0, 90)); + break; + } + case CE_ComboBoxLabel: { + auto cb = qstyleoption_cast(option); + if (!cb) + break; + QRect editRect = proxy()->subControlRect(CC_ComboBox, cb, SC_ComboBoxEditField, widget); + painter->save(); + painter->setClipRect(editRect); + if (!cb->currentIcon.isNull()) { + QIcon::Mode mode = cb->state & State_Enabled ? QIcon::Normal : QIcon::Disabled; + QPixmap pixmap = cb->currentIcon.pixmap(cb->iconSize, mode); + QRect iconRect(editRect); + iconRect.setWidth(cb->iconSize.width() + 4); + iconRect = alignedRect(cb->direction, Qt::AlignLeft | Qt::AlignVCenter, iconRect.size(), editRect); + if (cb->editable) + painter->fillRect(iconRect, cb->palette.brush(QPalette::Base)); + proxy()->drawItemPixmap(painter, iconRect, Qt::AlignCenter, pixmap); + + if (cb->direction == Qt::RightToLeft) + editRect.translate(-4 - cb->iconSize.width(), 0); + else + editRect.translate(cb->iconSize.width() + 4, 0); + } + if (!cb->currentText.isEmpty() && !cb->editable) { + proxy()->drawItemText(painter, + editRect.adjusted(1, 0, -1, 0), + visualAlignment(cb->direction, Qt::AlignLeft | Qt::AlignVCenter), + cb->palette, + cb->state & State_Enabled, + cb->currentText, + cb->editable ? QPalette::Text : QPalette::ButtonText); + } + painter->restore(); + break; + } + case CE_Splitter: { + QRect r = option->rect; + // We don't have anything useful to draw if it's too thin + if (r.width() < 5 || r.height() < 5) + break; + int length = Ph::dpiScaled(Ph::SplitterMaxLength); + int thickness = Ph::dpiScaled(1); + QSize size; + if (option->state & State_Horizontal) { + if (r.height() < length) + length = r.height(); + size = QSize(thickness, length); + } else { + if (r.width() < length) + length = r.width(); + size = QSize(length, thickness); + } + QRect filledRect = QStyle::alignedRect(option->direction, Qt::AlignCenter, size, r); + painter->fillRect(filledRect, swatch.color(S_button_specular)); + Ph::fillRectOutline(painter, filledRect.adjusted(-1, 0, 1, 0), 1, swatch.color(S_window_divider)); + break; + } + // TODO update this for phantom + case CE_RubberBand: { + if (!qstyleoption_cast(option)) + break; + QColor highlight = option->palette.color(QPalette::Active, QPalette::Highlight); + painter->save(); + QColor penColor = highlight.darker(120); + penColor.setAlpha(180); + painter->setPen(penColor); + QColor dimHighlight(qMin(highlight.red() / 2 + 110, 255), + qMin(highlight.green() / 2 + 110, 255), + qMin(highlight.blue() / 2 + 110, 255)); + dimHighlight.setAlpha(widget && widget->isTopLevel() ? 255 : 80); + painter->setRenderHint(QPainter::Antialiasing, true); + painter->translate(0.5, 0.5); + painter->setBrush(dimHighlight); + painter->drawRoundedRect(option->rect.adjusted(0, 0, -1, -1), 1, 1); + QColor innerLine = Qt::white; + innerLine.setAlpha(40); + painter->setPen(innerLine); + painter->drawRoundedRect(option->rect.adjusted(1, 1, -2, -2), 1, 1); + painter->restore(); + break; + } + case CE_SizeGrip: { + Qt::LayoutDirection dir = option->direction; + QRect rect = option->rect; + int rcx = rect.center().x(); + int rcy = rect.center().y(); + // draw grips + for (int i = -6; i < 12; i += 3) { + for (int j = -6; j < 12; j += 3) { + if ((dir == Qt::LeftToRight && i > -j) || (dir == Qt::RightToLeft && j > i)) { + painter->fillRect(rcx + i, rcy + j, 2, 2, swatch.color(S_window_lighter)); + painter->fillRect(rcx + i, rcy + j, 1, 1, swatch.color(S_window_darker)); + } + } + } + break; + } + case CE_ToolBar: { + auto toolBar = qstyleoption_cast(option); + if (!toolBar) + break; + painter->fillRect(option->rect, option->palette.window().color()); + bool isFloating = false; + if (auto tb = qobject_cast(widget)) { + isFloating = tb->isFloating(); + } + if (isFloating) { + Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_window_outline)); + } + break; + } + case CE_DockWidgetTitle: { + auto dwOpt = qstyleoption_cast(option); + if (!dwOpt) + break; + painter->save(); + bool verticalTitleBar = dwOpt->verticalTitleBar; + + QRect titleRect = subElementRect(SE_DockWidgetTitleBarText, option, widget); + if (verticalTitleBar) { + QRect r = dwOpt->rect; + QRect rtrans = {r.x(), r.y(), r.height(), r.width()}; + titleRect = QRect(rtrans.left() + r.bottom() - titleRect.bottom(), + rtrans.top() + titleRect.left() - r.left(), + titleRect.height(), + titleRect.width()); + painter->translate(rtrans.left(), rtrans.top() + rtrans.width()); + painter->rotate(-90); + painter->translate(-rtrans.left(), -rtrans.top()); + } + if (!dwOpt->title.isEmpty()) { + QString titleText = painter->fontMetrics().elidedText(dwOpt->title, Qt::ElideRight, titleRect.width()); + proxy()->drawItemText(painter, + titleRect, + Qt::AlignLeft | Qt::AlignVCenter | Qt::TextShowMnemonic, + dwOpt->palette, + dwOpt->state & State_Enabled, + titleText, + QPalette::WindowText); + } + painter->restore(); + break; + } + case CE_HeaderSection: { + auto header = qstyleoption_cast(option); + if (!header) + break; + QRect rect = header->rect; + Qt::Orientation orientation = header->orientation; + QStyleOptionHeader::SectionPosition position = header->position; + // See the "Table header layout reference" comment block at the bottom of + // this file for more information to help understand what's going on. + bool isLeftToRight = header->direction != Qt::RightToLeft; + bool isHorizontal = orientation == Qt::Horizontal; + bool isVertical = orientation == Qt::Vertical; + bool isEnd = position == QStyleOptionHeader::End; + bool isBegin = position == QStyleOptionHeader::Beginning; + bool isOnlyOne = position == QStyleOptionHeader::OnlyOneSection; + Qt::Edges edges; + bool spansToEnd = false; + bool isSpecialCorner = false; + if ((isHorizontal && isLeftToRight && isEnd) || (isHorizontal && !isLeftToRight && isBegin) + || (isVertical && isEnd) || isOnlyOne) { + auto hv = qobject_cast(widget); + if (hv) { + spansToEnd = hv->stretchLastSection(); + // In the case where the header item is not stretched to the end, but + // could plausibly be in a position where it could happen to be exactly + // the right width or height to be appear to be stretched to the end, + // we'll check to see if it actually does exactly meet the right (or + // bottom in vertical, or left in RTL) edge, and omit drawing the edge + // if that's the case. This can commonly happen if you have a tree or + // list view and don't set it to stretch, but the widget is still sized + // exactly to hold the one column. (It could also happen if there's + // user code running to manually stretch the last section as + // necessary.) + if (!spansToEnd) { + QRect viewBound = hv->contentsRect(); + if (isHorizontal) { + if (isLeftToRight) { + spansToEnd = rect.right() == viewBound.right(); + } else { + spansToEnd = rect.left() == viewBound.left(); + } + } else if (isVertical) { + spansToEnd = rect.bottom() == viewBound.bottom(); + } + } + } else { + // We only need to do this check in RTL, because the corner button in + // RTL *doesn't* need hacks applied. In LTR, we can just treat the + // corner button like anything else on the horizontal header bar, and + // can skip doing this inherits check. + if (isOnlyOne && !isLeftToRight && widget && widget->inherits("QTableCornerButton")) { + isSpecialCorner = true; + } + } + } + + if (isSpecialCorner) { + // In RTL layout, the corner button in a table view doesn't have any + // offset problems. This branch we're on is only taken if we're in RTL + // layout and this is the corner button being drawn. + edges |= Qt::BottomEdge; + if (isLeftToRight) + edges |= Qt::RightEdge; + else + edges |= Qt::LeftEdge; + } else if (isHorizontal) { + // This branch is taken for horizontal headers in either layout direction + // or for the corner button in LTR. + edges |= Qt::BottomEdge; + if (isLeftToRight) { + // In LTR, this code path may be for both the corner button *and* the + // actual header item. It doesn't matter in this case, and we were able + // to avoid doing an extra inherits call earlier. + if (!spansToEnd) { + edges |= Qt::RightEdge; + } + } else { + // Note: in right-to-left layouts for horizontal headers, the header + // view will unfortunately be shifted to the right by 1 pixel, due to + // what appears to be a Qt bug. This causes the vertical lines we draw + // in the header view to misalign with the grid, and causes the + // rightmost section to have its right edge clipped off. Therefore, + // we'll draw the separator on the on the right edge instead of the + // left edge. (We would have expected to draw it on the left edge in + // RTL layout.) This makes it line up with the grid again, except for + // the last section. right by 1 pixel. + // + // In RTL, the "Begin" position is on the left side for some reason + // (the same as LTR.) So "End" is always on the right. Ok, whatever. + // See the table at the bottom of this file if you're confused. + if (!isOnlyOne && !isEnd) { + edges |= Qt::RightEdge; + } + // The leftmost section in RTL has to draw on both its right and left + // edges, instead of just 1 edge like every other configuration. The + // left edge will be offset by 1 pixel from the grid, but it's the best + // we can do. + if (isBegin && !spansToEnd) { + edges |= Qt::LeftEdge; + } + } + } else if (isVertical) { + if (isLeftToRight) { + edges |= Qt::RightEdge; + } else { + edges |= Qt::LeftEdge; + } + if (!spansToEnd) { + edges |= Qt::BottomEdge; + } + } + QRect bgRect = Ph::expandRect(rect, edges, -1); + painter->fillRect(bgRect, swatch.color(S_window)); + Ph::fillRectEdges(painter, rect, edges, 1, swatch.color(S_frame_outline)); + break; + } + case CE_HeaderLabel: { + auto header = qstyleoption_cast(option); + if (!header) + break; + QRect rect = header->rect; + if (!header->icon.isNull()) { + int iconExtent = qMin(qMin(rect.height(), rect.width()), option->fontMetrics.height()); + auto window = widget ? widget->window()->windowHandle() : nullptr; + QPixmap pixmap = header->icon.pixmap(window, + QSize(iconExtent, iconExtent), + (header->state & State_Enabled) ? QIcon::Normal : QIcon::Disabled); + int pixw = static_cast(pixmap.width() / pixmap.devicePixelRatio()); + QRect aligned = alignedRect( + header->direction, QFlag(header->iconAlignment), pixmap.size() / pixmap.devicePixelRatio(), rect); + QRect inter = aligned.intersected(rect); + painter->drawPixmap(inter.x(), + inter.y(), + pixmap, + inter.x() - aligned.x(), + inter.y() - aligned.y(), + static_cast(aligned.width() * pixmap.devicePixelRatio()), + static_cast(pixmap.height() * pixmap.devicePixelRatio())); + int margin = proxy()->pixelMetric(QStyle::PM_HeaderMargin, option, widget); + if (header->direction == Qt::LeftToRight) + rect.setLeft(rect.left() + pixw + margin); + else + rect.setRight(rect.right() - pixw - margin); + } + proxy()->drawItemText(painter, + rect, + header->textAlignment, + header->palette, + (header->state & State_Enabled), + header->text, + QPalette::ButtonText); + + // But we still need some kind of indicator, so draw a line + bool drawHighlightLine = option->state & State_On; + // Special logic: if the selection mode of the item view is to select every + // row or every column, there's no real need to draw special "this + // row/column is selected" highlight indicators in the header view. The + // application programmer can also disable this explicitly on the header + // view, but it's nice to have it done automatically, I think. + if (drawHighlightLine) { + const QAbstractItemView* itemview = nullptr; + // Header view itself is an item view, and we don't care about its + // selection behavior -- we care about the actual item view. So try to + // get the widget as the header first, then find the item view from + // there. + auto headerview = qobject_cast(widget); + if (headerview) { + // Also don't care about highlights if there's only one row or column. + drawHighlightLine = headerview->count() > 1; + itemview = qobject_cast(headerview->parentWidget()); + } + if (drawHighlightLine && itemview) { + auto selBehavior = itemview->selectionBehavior(); + if (selBehavior == QAbstractItemView::SelectRows && header->orientation == Qt::Horizontal) + drawHighlightLine = false; + else if (selBehavior == QAbstractItemView::SelectColumns && header->orientation == Qt::Vertical) + drawHighlightLine = false; + } + } + + if (drawHighlightLine) { + QRect r = option->rect; + Qt::Edge edge; + if (header->orientation == Qt::Horizontal) { + edge = Qt::BottomEdge; + r.adjust(-2, 1, 1, 1); + } else { + bool isLeftToRight = option->direction != Qt::RightToLeft; + if (isLeftToRight) { + edge = Qt::RightEdge; + r.adjust(1, -2, 1, 1); + } else { + edge = Qt::LeftEdge; + r.adjust(-1, -2, -1, 1); + } + } + Ph::fillRectEdges(painter, r, edge, 1, swatch.color(S_itemView_headerOnLine)); + } + break; + } + case CE_ProgressBarGroove: { + const qreal rounding = Ph::ProgressBar_Rounding; + QRect rect = option->rect; + Ph::PSave save(painter); + Ph::paintBorderedRoundRect(painter, rect, rounding, swatch, S_window_outline, S_base); + save.restore(); + if (Ph::OverhangShadows && option->state & State_Enabled) { + // Inner shadow + const QColor& shadowColor = swatch.color(S_base_shadow); + // We can either have the shadow cut into the rounded corners, or leave a + // 1px gap, due to AA. + Ph::fillRectEdges(painter, + rect.adjusted(qRound(rounding / 2) + 1, 1, -(qRound(rounding / 2) + 1), -1), + Qt::TopEdge, + 1, + shadowColor); + } + break; + } + case CE_ProgressBarContents: { + auto bar = qstyleoption_cast(option); + if (!bar) + break; + const qreal rounding = Ph::ProgressBar_Rounding; + QRect filled, nonFilled; + bool isIndeterminate = false; + Ph::progressBarFillRects(bar, filled, nonFilled, isIndeterminate); + if (isIndeterminate || bar->progress > bar->minimum) { + Ph::PSave save(painter); + Ph::paintBorderedRoundRect(painter, filled, rounding, swatch, S_progressBar_outline, S_progressBar); + Ph::paintBorderedRoundRect( + painter, filled.adjusted(1, 1, -1, -1), rounding, swatch, S_progressBar_specular, S_none); + if (isIndeterminate) { + // TODO paint indeterminate indicator + } + } + break; + } + case CE_ProgressBarLabel: { + auto bar = qstyleoption_cast(option); + if (!bar) + break; + if (bar->text.isEmpty()) + break; + QRect r = bar->rect.adjusted(2, 2, -2, -2); + if (r.isEmpty() || !r.isValid()) + break; + QSize textSize = option->fontMetrics.size(Qt::TextBypassShaping, bar->text); + QRect textRect = QStyle::alignedRect(option->direction, Qt::AlignCenter, textSize, option->rect); + textRect &= r; + if (textRect.isEmpty()) + break; + QRect filled, nonFilled; + bool isIndeterminate = false; + Ph::progressBarFillRects(bar, filled, nonFilled, isIndeterminate); + QRect textNonFilledR = textRect & nonFilled; + QRect textFilledR = textRect & filled; + bool needsNonFilled = !textNonFilledR.isEmpty(); + bool needsFilled = !textFilledR.isEmpty(); + bool needsMasking = needsNonFilled && needsFilled; + Ph::PSave save(painter); + if (needsNonFilled) { + if (needsMasking) { + painter->save(); + painter->setClipRect(textNonFilledR); + } + painter->setPen(swatch.pen(S_text)); + painter->setBrush(Qt::NoBrush); + painter->drawText(textRect, bar->text, Qt::AlignHCenter | Qt::AlignVCenter); + if (needsMasking) { + painter->restore(); + } + } + if (needsFilled) { + if (needsMasking) { + painter->save(); + painter->setClipRect(textFilledR); + } + painter->setPen(swatch.pen(S_highlightedText)); + painter->setBrush(Qt::NoBrush); + painter->drawText(textRect, bar->text, Qt::AlignHCenter | Qt::AlignVCenter); + if (needsMasking) { + painter->restore(); + } + } + break; + } + case CE_MenuBarItem: { + auto mbi = qstyleoption_cast(option); + if (!mbi) + break; + const QRect r = option->rect; + QRect textRect = r; + textRect.setY(textRect.y() + (r.height() - option->fontMetrics.height()) / 2); + int alignment = Qt::AlignHCenter | Qt::AlignTop | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine; + if (!proxy()->styleHint(SH_UnderlineShortcut, mbi, widget)) + alignment |= Qt::TextHideMnemonic; + const auto itemState = mbi->state; + bool maybeHasAltKeyNavFocus = itemState & State_Selected && itemState & State_HasFocus; + bool isSelected = itemState & State_Selected || itemState & State_Sunken; + if (!isSelected && maybeHasAltKeyNavFocus && widget) { + isSelected = widget->hasFocus(); + } + Swatchy fill = isSelected ? S_highlight : S_window; + painter->fillRect(r, swatch.color(fill)); + QPalette::ColorRole textRole = isSelected ? QPalette::HighlightedText : QPalette::Text; + proxy()->drawItemText( + painter, textRect, alignment, mbi->palette, mbi->state & State_Enabled, mbi->text, textRole); + if (Phantom::MenuBarDrawBorder && !isSelected) { + Ph::fillRectEdges(painter, r, Qt::BottomEdge, 1, swatch.color(S_window_divider)); + } + break; + } + + case CE_MenuItem: { + auto menuItem = qstyleoption_cast(option); + if (!menuItem) + break; + const auto metrics = Ph::MenuItemMetrics::ofFontHeight(option->fontMetrics.height()); + // Draws one item in a popup menu. + if (menuItem->menuItemType == QStyleOptionMenuItem::Separator) { + // Phantom ignores text and icons in menu separators, because + // 1) The text and icons for separators don't render on Mac native menus + // 2) There doesn't seem to be a way to account for the width of the text + // properly (Fusion will often draw separator text clipped off) + // 3) Setting text on separators also seems to mess up the metrics for + // menu items on Mac native menus + QRect r = option->rect; + r.setHeight(r.height() / 2 + 1); + Ph::fillRectEdges(painter, r, Qt::BottomEdge, 1, swatch.color(S_window_divider)); + break; + } + const QRect itemRect = option->rect; + painter->save(); + bool isSelected = menuItem->state & State_Selected && menuItem->state & State_Enabled; + bool isCheckable = menuItem->checkType != QStyleOptionMenuItem::NotCheckable; + bool isChecked = menuItem->checked; + bool isSunken = menuItem->state & State_Sunken; + bool isEnabled = menuItem->state & State_Enabled; + bool hasSubMenu = menuItem->menuItemType == QStyleOptionMenuItem::SubMenu; + if (isSelected) { + Swatchy fillColor = isSunken ? S_highlight_outline : S_highlight; + painter->fillRect(option->rect, swatch.color(fillColor)); + } + + if (isCheckable) { + // Note: check rect might be misaligned vertically if it's a menu from a + // combo box. Probably a bug in Qt code? + QRect checkRect = Ph::menuItemCheckRect(metrics, option->direction, itemRect, hasSubMenu); + Swatchy signColor = !isEnabled ? S_windowText : isSelected ? S_highlightedText : S_windowText; + if (menuItem->checkType & QStyleOptionMenuItem::Exclusive) { + // Radio button + if (isChecked) { + painter->setRenderHint(QPainter::Antialiasing); + painter->setPen(Qt::NoPen); + QPalette::ColorRole textRole = + !isEnabled ? QPalette::Text : isSelected ? QPalette::HighlightedText : QPalette::ButtonText; + painter->setBrush(option->palette.brush(option->palette.currentColorGroup(), textRole)); + qreal rx, ry, rw, rh; + QRectF(checkRect).getRect(&rx, &ry, &rw, &rh); + qreal dim = qMin(checkRect.width(), checkRect.height()) * 0.75; + QRectF rf(rx + rw / dim, ry + rh / dim, dim, dim); + painter->drawEllipse(rf); + } + } else { + // If we want mouse-down to immediately show the item as + // checked/unchecked (kinda bad if the user is click-holding on the + // menu instead of click-clicking.) + // + // if ((isChecked && !isSunken) || (!isChecked && isSunken)) { + if (isChecked) { + Ph::drawCheck(painter, d->checkBox_pen_scratch, checkRect, swatch, signColor); + } + } + } + + const bool hasIcon = !menuItem->icon.isNull(); + + if (hasIcon) { + QRect iconRect = Ph::menuItemIconRect(metrics, option->direction, itemRect, hasSubMenu); + QIcon::Mode mode = isEnabled ? QIcon::Normal : QIcon::Disabled; + if (isSelected && isEnabled) + mode = QIcon::Selected; + QIcon::State state = isChecked ? QIcon::On : QIcon::Off; + + // TODO hmm, we might be ending up with blurry icons at size 15 instead + // of 16 for example on Windows. + // + // int smallIconSize = + // proxy()->pixelMetric(PM_SmallIconSize, option, widget); + // QSize iconSize(smallIconSize, smallIconSize); + int iconExtent = qMin(iconRect.width(), iconRect.height()); + QSize iconSize(iconExtent, iconExtent); + if (auto combo = qobject_cast(widget)) { + iconSize = combo->iconSize(); + } + QWindow* window = widget ? widget->windowHandle() : nullptr; + QPixmap pixmap = menuItem->icon.pixmap(window, iconSize, mode, state); + const int pixw = static_cast(pixmap.width() / pixmap.devicePixelRatio()); + const int pixh = static_cast(pixmap.height() / pixmap.devicePixelRatio()); + QRect pixmapRect = QStyle::alignedRect(option->direction, Qt::AlignCenter, QSize(pixw, pixh), iconRect); + painter->drawPixmap(pixmapRect.topLeft(), pixmap); + } + + // Draw main text and mnemonic text + QStringRef s(&menuItem->text); + if (!s.isEmpty()) { + QRect textRect = + Ph::menuItemTextRect(metrics, option->direction, itemRect, hasSubMenu, hasIcon, menuItem->tabWidth); + int t = s.indexOf(QLatin1Char('\t')); + int text_flags = + Qt::AlignLeft | Qt::AlignTop | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine; + if (!styleHint(SH_UnderlineShortcut, menuItem, widget)) + text_flags |= Qt::TextHideMnemonic; +#if 0 + painter->save(); +#endif + painter->setPen(swatch.pen(isSelected ? S_highlightedText : S_text)); + + // Comment from original Qt code which did some dance with the font: + // + // font may not have any "hard" flags set. We override the point size so + // that when it is resolved against the device, this font will win. This + // is mainly to handle cases where someone sets the font on the window + // and then the combo inherits it and passes it onward. At that point the + // resolve mask is very, very weak. This makes it stonger. +#if 0 + QFont font = menuItem->font; + font.setPointSizeF(QFontInfo(menuItem->font).pointSizeF()); + painter->setFont(font); +#endif + + // My comment: + // + // What actually looks like is happening is that the qplatformtheme may + // have set a per-class font for menus. The QComboMenuDelegate sets the + // combo box's own font on the QStyleOptionMenuItem when passing it in + // here and when calling sizeFromContents with CT_MenuItem, but the + // QPainter we're called with hasn't had its font set to it -- it's still + // set to the QMenu/QMenuItem app fonts hash font. So if it's a menu + // coming from a combo box, let's just go ahead and set the font for it + // if it doesn't match, since that's probably what it wanted to do. I + // think. And as described above, we have to do the weird dance with the + // resolve mask... which is some internal Qt detail that we aren't + // supposed to have to deal with, but here we are. + // + // Ok, there's another problem, and QFusionStyle also suffers from it: in + // high DPI, setting the pointSizeF and setting the font again won't + // necessarily give us the right font (at least in Windows.) The font + // might have too thin of a weight, and probably other problems. So just + // forget about it: we'll have Phantom return 0 for the style hint that + // the combo box uses to determine if it should use a QMenu popup instead + // of a regular dropdown menu thing. The popup menu might actually be + // better for usability in some cases, and it's how combos work on Mac + // and BeOS, but it won't work anyway for editable combo boxes in Qt, and + // the font issues just make it not worth it. So we'll have a dropdown + // guy like a traditional Windows thing. + // + // If you want to try it out again, go to SH_ComboBox_Popup and have it + // return 1. + // + // Alternatively, we could instead have the CT_MenuItem handling code try + // to be aggressively clever and use the qt app font hash to look up the + // expected font for a QMenu and use that for calculating its metrics. + // Unfortunately, that probably won't work so great if the combo/menu + // actually wants to use custom fonts in its listing, since we'd be + // ignoring it. That's how UseQMenuForComboBoxPopup currently works, + // though it tests for Qt::WA_SetFont as an attempt at recognizing when + // it shouldn't use the qt font hash for QMenu. +#if 0 + if (qobject_cast(widget)) { + QFont font = menuItem->font; + font.setPointSizeF(QFontInfo(menuItem->font).pointSizeF()); + painter->setFont(font); + } +#endif + + // Draw mnemonic text + if (t >= 0) { + QRect mnemonicR = + Ph::menuItemMnemonicRect(metrics, option->direction, itemRect, hasSubMenu, menuItem->tabWidth); + const QStringRef textToDrawRef = s.mid(t + 1); + const QString unsafeTextToDraw = QString::fromRawData(textToDrawRef.constData(), textToDrawRef.size()); + painter->drawText(mnemonicR, text_flags, unsafeTextToDraw); + s = s.left(t); + } + const QStringRef textToDrawRef = s.left(t); + const QString unsafeTextToDraw = QString::fromRawData(textToDrawRef.constData(), textToDrawRef.size()); + painter->drawText(textRect, text_flags, unsafeTextToDraw); + +#if 0 + painter->restore(); +#endif + } + + // SubMenu Arrow + if (hasSubMenu) { + Qt::ArrowType arrow = option->direction == Qt::RightToLeft ? Qt::LeftArrow : Qt::RightArrow; + QRect arrowRect = Ph::menuItemArrowRect(metrics, option->direction, itemRect); + Swatchy arrowColor = isSelected ? S_highlightedText : S_indicator_current; + Ph::drawArrow(painter, arrowRect, arrow, swatch.brush(arrowColor)); + } + painter->restore(); + break; + } + case CE_MenuHMargin: + case CE_MenuVMargin: + case CE_MenuEmptyArea: + break; + case CE_PushButton: { + auto btn = qstyleoption_cast(option); + if (!btn) + break; + proxy()->drawControl(CE_PushButtonBevel, btn, painter, widget); + QStyleOptionButton subopt = *btn; + subopt.rect = subElementRect(SE_PushButtonContents, btn, widget); + proxy()->drawControl(CE_PushButtonLabel, &subopt, painter, widget); + break; + } + case CE_PushButtonLabel: { + auto button = qstyleoption_cast(option); + if (!button) + break; + // This code is very similar to QCommonStyle's implementation, but doesn't + // set the icon mode to active when focused. + QRect textRect = button->rect; + int tf = Qt::AlignVCenter | Qt::TextShowMnemonic; + if (!proxy()->styleHint(SH_UnderlineShortcut, button, widget)) + tf |= Qt::TextHideMnemonic; + if (!button->icon.isNull()) { + // Center both icon and text + QRect iconRect; + QIcon::Mode mode = button->state & State_Enabled ? QIcon::Normal : QIcon::Disabled; + QIcon::State state = button->state & State_On ? QIcon::On : QIcon::Off; + auto window = widget ? widget->window()->windowHandle() : nullptr; + QPixmap pixmap = button->icon.pixmap(window, button->iconSize, mode, state); + int pixmapWidth = static_cast(pixmap.width() / pixmap.devicePixelRatio()); + int pixmapHeight = static_cast(pixmap.height() / pixmap.devicePixelRatio()); + int labelWidth = pixmapWidth; + int labelHeight = pixmapHeight; + // 4 is hardcoded in QPushButton::sizeHint() + int iconSpacing = 4; + int textWidth = button->fontMetrics.boundingRect(option->rect, tf, button->text).width(); + if (!button->text.isEmpty()) + labelWidth += (textWidth + iconSpacing); + iconRect = QRect(textRect.x() + (textRect.width() - labelWidth) / 2, + textRect.y() + (textRect.height() - labelHeight) / 2, + pixmapWidth, + pixmapHeight); + iconRect = visualRect(button->direction, textRect, iconRect); + tf |= Qt::AlignLeft; // left align, we adjust the text-rect instead + if (button->direction == Qt::RightToLeft) + textRect.setRight(iconRect.left() - iconSpacing); + else + textRect.setLeft(iconRect.left() + iconRect.width() + iconSpacing); + if (button->state & (State_On | State_Sunken)) + iconRect.translate(proxy()->pixelMetric(PM_ButtonShiftHorizontal, option, widget), + proxy()->pixelMetric(PM_ButtonShiftVertical, option, widget)); + painter->drawPixmap(iconRect, pixmap); + } else { + tf |= Qt::AlignHCenter; + } + if (button->state & (State_On | State_Sunken)) + textRect.translate(proxy()->pixelMetric(PM_ButtonShiftHorizontal, option, widget), + proxy()->pixelMetric(PM_ButtonShiftVertical, option, widget)); + if (button->features & QStyleOptionButton::HasMenu) { + int indicatorSize = proxy()->pixelMetric(PM_MenuButtonIndicator, button, widget); + if (button->direction == Qt::LeftToRight) + textRect = textRect.adjusted(0, 0, -indicatorSize, 0); + else + textRect = textRect.adjusted(indicatorSize, 0, 0, 0); + } + proxy()->drawItemText(painter, + textRect, + tf, + button->palette, + (button->state & State_Enabled), + button->text, + QPalette::ButtonText); + break; + } + case CE_MenuBarEmptyArea: { + QRect rect = option->rect; + if (Phantom::MenuBarDrawBorder) { + Ph::fillRectEdges(painter, rect, Qt::BottomEdge, 1, swatch.color(S_window_divider)); + } + painter->fillRect(rect.adjusted(0, 0, 0, -1), swatch.color(S_window)); + break; + } + case CE_TabBarTabShape: { + auto tab = qstyleoption_cast(option); + if (!tab) + break; + bool rtlHorTabs = (tab->direction == Qt::RightToLeft + && (tab->shape == QTabBar::RoundedNorth || tab->shape == QTabBar::RoundedSouth)); + bool isSelected = tab->state & State_Selected; + bool lastTab = ((!rtlHorTabs && tab->position == QStyleOptionTab::End) + || (rtlHorTabs && tab->position == QStyleOptionTab::Beginning)); + bool onlyOne = tab->position == QStyleOptionTab::OnlyOneTab; + int tabOverlap = pixelMetric(PM_TabBarTabOverlap, option, widget); + const qreal rounding = Ph::TabBarTab_Rounding; + Qt::Edge outerEdge = Qt::TopEdge; + Qt::Edge edgeTowardNextTab = Qt::RightEdge; + switch (tab->shape) { + case QTabBar::RoundedNorth: + outerEdge = Qt::TopEdge; + edgeTowardNextTab = Qt::RightEdge; + break; + case QTabBar::RoundedSouth: + outerEdge = Qt::BottomEdge; + edgeTowardNextTab = Qt::RightEdge; + break; + case QTabBar::RoundedWest: + outerEdge = Qt::LeftEdge; + edgeTowardNextTab = Qt::BottomEdge; + break; + case QTabBar::RoundedEast: + outerEdge = Qt::RightEdge; + edgeTowardNextTab = Qt::BottomEdge; + break; + default: + QCommonStyle::drawControl(element, tab, painter, widget); + return; + } + Qt::Edge innerEdge = Ph::oppositeEdge(outerEdge); + Qt::Edge edgeAwayNextTab = Ph::oppositeEdge(edgeTowardNextTab); + QRect shapeClipRect = Ph::expandRect(option->rect, innerEdge, -2); + QRect drawRect = Ph::expandRect(shapeClipRect, innerEdge, 3 + 2 * rounding + 1); + if (!onlyOne && !lastTab) { + drawRect = Ph::expandRect(drawRect, edgeTowardNextTab, tabOverlap); + shapeClipRect = Ph::expandRect(shapeClipRect, edgeTowardNextTab, tabOverlap); + } + if (!isSelected) { + int offset = proxy()->pixelMetric(PM_TabBarTabShiftVertical, option, widget); + drawRect = Ph::expandRect(drawRect, outerEdge, -offset); + } + painter->save(); + painter->setClipRect(shapeClipRect); + bool hasFrame = tab->features & QStyleOptionTab::HasFrame && !tab->documentMode; + Swatchy tabFrameColor, thisFillColor, specular; + if (hasFrame) { + tabFrameColor = S_tabFrame; + if (isSelected) { + thisFillColor = S_tabFrame; + specular = S_tabFrame_specular; + } else { + thisFillColor = S_inactiveTabYesFrame; + specular = Ph::TabBar_InactiveTabsHaveSpecular ? S_inactiveTabYesFrame_specular : S_none; + } + } else { + tabFrameColor = S_window; + if (isSelected) { + thisFillColor = S_window; + specular = S_window_specular; + } else { + thisFillColor = S_inactiveTabNoFrame; + specular = Ph::TabBar_InactiveTabsHaveSpecular ? S_inactiveTabNoFrame_specular : S_none; + } + } + auto frameColor = isSelected ? S_frame_outline : S_window_outline; + Ph::paintBorderedRoundRect(painter, drawRect, rounding, swatch, frameColor, thisFillColor); + Ph::paintBorderedRoundRect(painter, drawRect.adjusted(1, 1, -1, -1), rounding, swatch, specular, S_none); + painter->restore(); + if (isSelected) { + QRect highlightRect = drawRect.adjusted(2, 1, -2, 0); + highlightRect.setHeight(Ph::dpiScaled(2.0)); + QRect highlightRectSpec = highlightRect.adjusted(-1, -1, 1, 0); + painter->fillRect(highlightRectSpec, Ph::DeriveColors::lightSpecularOf(swatch.color(S_highlight))); + painter->fillRect(highlightRect, swatch.color(S_highlight)); + + QRect refillRect = Ph::rectFromInnerEdgeWithThickness(shapeClipRect, innerEdge, 2); + refillRect = Ph::rectTranslatedTowardEdge(refillRect, innerEdge, 2); + refillRect = Ph::expandRect(refillRect, edgeAwayNextTab | edgeTowardNextTab, -1); + painter->fillRect(refillRect, swatch.color(tabFrameColor)); + Ph::fillRectEdges(painter, refillRect, edgeAwayNextTab | edgeTowardNextTab, 1, swatch.color(specular)); + } + break; + } + case CE_ItemViewItem: { + auto ivopt = qstyleoption_cast(option); + if (!ivopt) + break; + // Hack to work around broken grid line drawing in Qt's table view code: + // + // We tell it that the grid line color is a color via + // SH_Table_GridLineColor. It draws the grid lines, but it in high DPI it's + // broken because it uses a pen/path to draw the line, which makes it too + // narrow, subpixel-incorrectly-antialiased, and/or offset from its correct + // position. So when we draw the item view items in a table view, we'll + // also try to paint 1 pixel outside of our current rect to try to fill in + // the incorrectly painted areas where the grid lines are. + // + // Also note that the table views with the bad drawing code, when + // scrolling, will leave garbage behind in the incorrectly-drawn grid line + // areas. This will also paint over that. + bool overdrawGridHack = false; + if (auto tableWidget = qobject_cast(widget)) { + overdrawGridHack = tableWidget->showGrid() && tableWidget->gridStyle() == Qt::SolidLine; + } + if (overdrawGridHack) { + QRect r = option->rect.adjusted(-1, -1, 1, 1); + Ph::fillRectOutline(painter, r, 1, swatch.color(S_base_divider)); + } + QCommonStyle::drawControl(element, option, painter, widget); + break; + } + case CE_ShapedFrame: { + auto frameopt = qstyleoption_cast(option); + if (frameopt) { + if (frameopt->frameShape == QFrame::HLine) { + QRect r = option->rect; + r.setY(r.y() + r.height() / 2); + r.setHeight(2); + painter->fillRect(r, swatch.color(S_tabFrame_specular)); + r.setHeight(1); + painter->fillRect(r, swatch.color(S_frame_outline)); + break; + } else if (frameopt->frameShape == QFrame::VLine) { + QRect r = option->rect; + r.setX(r.x() + r.width() / 2); + r.setWidth(2); + painter->fillRect(r, swatch.color(S_tabFrame_specular)); + r.setWidth(1); + painter->fillRect(r, swatch.color(S_frame_outline)); + break; + } + } + QCommonStyle::drawControl(element, option, painter, widget); + break; + } + default: + QCommonStyle::drawControl(element, option, painter, widget); + break; + } +} + +QPalette BaseStyle::standardPalette() const +{ + return QCommonStyle::standardPalette(); +} + +void BaseStyle::drawComplexControl(ComplexControl control, + const QStyleOptionComplex* option, + QPainter* painter, + const QWidget* widget) const +{ +#ifdef BUILD_WITH_EASY_PROFILER + EASY_BLOCK("drawControl"); + const char* controlCString = QMetaEnum::fromType().valueToKey(control); + EASY_TEXT("ComplexControl", controlCString); +#endif + using Swatchy = Phantom::Swatchy; + using namespace Phantom::SwatchColors; + namespace Ph = Phantom; + auto ph_swatchPtr = Ph::getCachedSwatchOfQPalette(&d->swatchCache, &d->headSwatchFastKey, option->palette); + const Ph::PhSwatch& swatch = *ph_swatchPtr.data(); + + switch (control) { + case CC_GroupBox: { + auto groupBox = qstyleoption_cast(option); + if (!groupBox) + break; + painter->save(); + // Draw frame + QRect textRect = proxy()->subControlRect(CC_GroupBox, option, SC_GroupBoxLabel, widget); + QRect checkBoxRect = proxy()->subControlRect(CC_GroupBox, option, SC_GroupBoxCheckBox, widget); + + if (groupBox->subControls & QStyle::SC_GroupBoxFrame) { + QStyleOptionFrame frame; + frame.QStyleOption::operator=(*groupBox); + frame.features = groupBox->features; + frame.lineWidth = groupBox->lineWidth; + frame.midLineWidth = groupBox->midLineWidth; + frame.rect = proxy()->subControlRect(CC_GroupBox, option, SC_GroupBoxFrame, widget); + proxy()->drawPrimitive(PE_FrameGroupBox, &frame, painter, widget); + } + + // Draw title + if ((groupBox->subControls & QStyle::SC_GroupBoxLabel) && !groupBox->text.isEmpty()) { + // groupBox->textColor gets the incorrect palette here + painter->setPen(QPen(option->palette.windowText(), 1)); + unsigned alignment = groupBox->textAlignment; + if (!proxy()->styleHint(QStyle::SH_UnderlineShortcut, option, widget)) + alignment |= Qt::TextHideMnemonic; + + proxy()->drawItemText(painter, + textRect, + alignment | Qt::TextShowMnemonic | Qt::AlignLeft, + groupBox->palette, + groupBox->state & State_Enabled, + groupBox->text, + QPalette::NoRole); + + if (groupBox->state & State_HasFocus) { + QStyleOptionFocusRect fropt; + fropt.QStyleOption::operator=(*groupBox); + fropt.rect = textRect.adjusted(-1, 0, 1, 0); + proxy()->drawPrimitive(PE_FrameFocusRect, &fropt, painter, widget); + } + } + + // Draw checkbox + if (groupBox->subControls & SC_GroupBoxCheckBox) { + QStyleOptionButton box; + box.QStyleOption::operator=(*groupBox); + box.rect = checkBoxRect; + proxy()->drawPrimitive(PE_IndicatorCheckBox, &box, painter, widget); + } + painter->restore(); + break; + } + case CC_SpinBox: { + auto spinBox = qstyleoption_cast(option); + if (!spinBox) + break; + const qreal rounding = Ph::SpinBox_Rounding; + bool isLeftToRight = option->direction != Qt::RightToLeft; + const QRect rect = spinBox->rect; + bool sunken = spinBox->state & State_Sunken; + bool upIsActive = spinBox->activeSubControls == SC_SpinBoxUp; + bool downIsActive = spinBox->activeSubControls == SC_SpinBoxDown; + bool hasFocus = option->state & State_HasFocus; + bool isEnabled = option->state & State_Enabled; + QRect upRect = proxy()->subControlRect(CC_SpinBox, spinBox, SC_SpinBoxUp, widget); + QRect downRect = proxy()->subControlRect(CC_SpinBox, spinBox, SC_SpinBoxDown, widget); + if (spinBox->frame) { + QRect upDownRect = upRect | downRect; + upDownRect.adjust(0, -1, 0, 1); + painter->save(); // 0 + // Fill background + Ph::paintBorderedRoundRect(painter, rect, rounding, swatch, S_none, S_base); + // Draw button fill + painter->setClipRect(upDownRect); + // Side with the border + Qt::Edge edge = isLeftToRight ? Qt::LeftEdge : Qt::RightEdge; + Ph::paintBorderedRoundRect( + painter, Ph::expandRect(upDownRect, Ph::oppositeEdge(edge), -1), rounding, swatch, S_none, S_button); + painter->restore(); // 0 + if (Ph::OverhangShadows && !hasFocus && isEnabled) { + // Imperfect, leaves tiny gap on left and right. Going closer would eat + // into the outline, though. + QRect shadowRect = rect.adjusted(qRound(rounding / 2), 1, -qRound(rounding / 2), -1); + if (isLeftToRight) { + shadowRect.setRight(upDownRect.left()); + } else { + shadowRect.setLeft(upDownRect.right()); + } + Ph::fillRectEdges(painter, shadowRect, Qt::TopEdge, 1, swatch.color(S_base_shadow)); + } + if ((spinBox->stepEnabled & QAbstractSpinBox::StepUpEnabled) && upIsActive && sunken) { + painter->fillRect(upRect, swatch.color(S_button_pressed)); + } + if ((spinBox->stepEnabled & QAbstractSpinBox::StepDownEnabled) && downIsActive && sunken) { + painter->fillRect(downRect, swatch.color(S_button_pressed)); + } + // Left or right border line + Ph::fillRectEdges(painter, upDownRect, edge, 1, swatch.color(S_window_outline)); + Ph::PSave save(painter); + // Outline over entire frame + Swatchy outlineColor = hasFocus ? S_highlight_outline : S_window_outline; + Ph::paintBorderedRoundRect(painter, rect, rounding, swatch, outlineColor, S_none); + save.restore(); + } + + if (spinBox->buttonSymbols == QAbstractSpinBox::PlusMinus) { + Ph::PSave save(painter); + // TODO fix up old fusion code here + int centerX = upRect.center().x(); + int centerY = upRect.center().y(); + Swatchy arrowColorUp = + spinBox->stepEnabled & QAbstractSpinBox::StepUpEnabled ? S_indicator_current : S_indicator_disabled; + Swatchy arrowColorDown = + spinBox->stepEnabled & QAbstractSpinBox::StepDownEnabled ? S_indicator_current : S_indicator_disabled; + painter->setPen(swatch.pen(arrowColorUp)); + painter->drawLine(centerX - 1, centerY, centerX + 3, centerY); + painter->drawLine(centerX + 1, centerY - 2, centerX + 1, centerY + 2); + centerX = downRect.center().x(); + centerY = downRect.center().y(); + painter->setPen(arrowColorDown); + painter->drawLine(centerX - 1, centerY, centerX + 3, centerY); + } else if (spinBox->buttonSymbols == QAbstractSpinBox::UpDownArrows) { + int xoffs = isLeftToRight ? 0 : 1; + Ph::drawArrow(painter, + upRect.adjusted(4 + xoffs, 1, -5 + xoffs, 1), + Qt::UpArrow, + swatch, + spinBox->stepEnabled & QAbstractSpinBox::StepUpEnabled); + Ph::drawArrow(painter, + downRect.adjusted(4 + xoffs, 0, -5 + xoffs, -1), + Qt::DownArrow, + swatch, + spinBox->stepEnabled & QAbstractSpinBox::StepDownEnabled); + } + break; + } + case CC_TitleBar: { + auto titleBar = qstyleoption_cast(option); + if (!titleBar) + break; + painter->save(); + const int buttonMargin = 5; + bool active = (titleBar->titleBarState & State_Active); + QRect fullRect = titleBar->rect; + QPalette palette = option->palette; + QColor highlight = option->palette.highlight().color(); + QColor outline = option->palette.dark().color(); + + QColor titleBarFrameBorder(active ? highlight.darker(180) : outline.darker(110)); + QColor titleBarHighlight(active ? highlight.lighter(120) : palette.background().color().lighter(120)); + QColor textColor(active ? 0xffffff : 0xff000000); + QColor textAlphaColor(active ? 0xffffff : 0xff000000); + + { + // Fill title + QColor titlebarColor = QColor(active ? highlight : palette.background().color()); + painter->fillRect(option->rect.adjusted(1, 1, -1, 0), titlebarColor); + // Frame and rounded corners + painter->setPen(titleBarFrameBorder); + + // top outline + painter->drawLine(fullRect.left() + 5, fullRect.top(), fullRect.right() - 5, fullRect.top()); + painter->drawLine(fullRect.left(), fullRect.top() + 4, fullRect.left(), fullRect.bottom()); + const QPoint points[5] = {QPoint(fullRect.left() + 4, fullRect.top() + 1), + QPoint(fullRect.left() + 3, fullRect.top() + 1), + QPoint(fullRect.left() + 2, fullRect.top() + 2), + QPoint(fullRect.left() + 1, fullRect.top() + 3), + QPoint(fullRect.left() + 1, fullRect.top() + 4)}; + painter->drawPoints(points, 5); + + painter->drawLine(fullRect.right(), fullRect.top() + 4, fullRect.right(), fullRect.bottom()); + const QPoint points2[5] = {QPoint(fullRect.right() - 3, fullRect.top() + 1), + QPoint(fullRect.right() - 4, fullRect.top() + 1), + QPoint(fullRect.right() - 2, fullRect.top() + 2), + QPoint(fullRect.right() - 1, fullRect.top() + 3), + QPoint(fullRect.right() - 1, fullRect.top() + 4)}; + painter->drawPoints(points2, 5); + + // draw bottomline + painter->drawLine(fullRect.right(), fullRect.bottom(), fullRect.left(), fullRect.bottom()); + + // top highlight + painter->setPen(titleBarHighlight); + painter->drawLine(fullRect.left() + 6, fullRect.top() + 1, fullRect.right() - 6, fullRect.top() + 1); + } + // draw title + QRect textRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarLabel, widget); + painter->setPen(active ? (titleBar->palette.text().color().lighter(120)) : titleBar->palette.text().color()); + // Note workspace also does elliding but it does not use the correct font + QString title = painter->fontMetrics().elidedText(titleBar->text, Qt::ElideRight, textRect.width() - 14); + painter->drawText(textRect.adjusted(1, 1, 1, 1), title, QTextOption(Qt::AlignHCenter | Qt::AlignVCenter)); + painter->setPen(Qt::white); + if (active) + painter->drawText(textRect, title, QTextOption(Qt::AlignHCenter | Qt::AlignVCenter)); + // min button + if ((titleBar->subControls & SC_TitleBarMinButton) && (titleBar->titleBarFlags & Qt::WindowMinimizeButtonHint) + && !(titleBar->titleBarState & Qt::WindowMinimized)) { + QRect minButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarMinButton, widget); + if (minButtonRect.isValid()) { + bool hover = + (titleBar->activeSubControls & SC_TitleBarMinButton) && (titleBar->state & State_MouseOver); + bool sunken = (titleBar->activeSubControls & SC_TitleBarMinButton) && (titleBar->state & State_Sunken); + Ph::drawMdiButton(painter, titleBar, minButtonRect, hover, sunken); + QRect minButtonIconRect = + minButtonRect.adjusted(buttonMargin, buttonMargin, -buttonMargin, -buttonMargin); + painter->setPen(textColor); + painter->drawLine(minButtonIconRect.center().x() - 2, + minButtonIconRect.center().y() + 3, + minButtonIconRect.center().x() + 3, + minButtonIconRect.center().y() + 3); + painter->drawLine(minButtonIconRect.center().x() - 2, + minButtonIconRect.center().y() + 4, + minButtonIconRect.center().x() + 3, + minButtonIconRect.center().y() + 4); + painter->setPen(textAlphaColor); + painter->drawLine(minButtonIconRect.center().x() - 3, + minButtonIconRect.center().y() + 3, + minButtonIconRect.center().x() - 3, + minButtonIconRect.center().y() + 4); + painter->drawLine(minButtonIconRect.center().x() + 4, + minButtonIconRect.center().y() + 3, + minButtonIconRect.center().x() + 4, + minButtonIconRect.center().y() + 4); + } + } + // max button + if ((titleBar->subControls & SC_TitleBarMaxButton) && (titleBar->titleBarFlags & Qt::WindowMaximizeButtonHint) + && !(titleBar->titleBarState & Qt::WindowMaximized)) { + QRect maxButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarMaxButton, widget); + if (maxButtonRect.isValid()) { + bool hover = + (titleBar->activeSubControls & SC_TitleBarMaxButton) && (titleBar->state & State_MouseOver); + bool sunken = (titleBar->activeSubControls & SC_TitleBarMaxButton) && (titleBar->state & State_Sunken); + Ph::drawMdiButton(painter, titleBar, maxButtonRect, hover, sunken); + + QRect maxButtonIconRect = + maxButtonRect.adjusted(buttonMargin, buttonMargin, -buttonMargin, -buttonMargin); + + painter->setPen(textColor); + painter->drawRect(maxButtonIconRect.adjusted(0, 0, -1, -1)); + painter->drawLine(maxButtonIconRect.left() + 1, + maxButtonIconRect.top() + 1, + maxButtonIconRect.right() - 1, + maxButtonIconRect.top() + 1); + painter->setPen(textAlphaColor); + const QPoint points[4] = {maxButtonIconRect.topLeft(), + maxButtonIconRect.topRight(), + maxButtonIconRect.bottomLeft(), + maxButtonIconRect.bottomRight()}; + painter->drawPoints(points, 4); + } + } + + // close button + if ((titleBar->subControls & SC_TitleBarCloseButton) && (titleBar->titleBarFlags & Qt::WindowSystemMenuHint)) { + QRect closeButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarCloseButton, widget); + if (closeButtonRect.isValid()) { + bool hover = + (titleBar->activeSubControls & SC_TitleBarCloseButton) && (titleBar->state & State_MouseOver); + bool sunken = + (titleBar->activeSubControls & SC_TitleBarCloseButton) && (titleBar->state & State_Sunken); + Ph::drawMdiButton(painter, titleBar, closeButtonRect, hover, sunken); + QRect closeIconRect = + closeButtonRect.adjusted(buttonMargin, buttonMargin, -buttonMargin, -buttonMargin); + painter->setPen(textAlphaColor); + const QLine lines[4] = {QLine(closeIconRect.left() + 1, + closeIconRect.top(), + closeIconRect.right(), + closeIconRect.bottom() - 1), + QLine(closeIconRect.left(), + closeIconRect.top() + 1, + closeIconRect.right() - 1, + closeIconRect.bottom()), + QLine(closeIconRect.right() - 1, + closeIconRect.top(), + closeIconRect.left(), + closeIconRect.bottom() - 1), + QLine(closeIconRect.right(), + closeIconRect.top() + 1, + closeIconRect.left() + 1, + closeIconRect.bottom())}; + painter->drawLines(lines, 4); + const QPoint points[4] = {closeIconRect.topLeft(), + closeIconRect.topRight(), + closeIconRect.bottomLeft(), + closeIconRect.bottomRight()}; + painter->drawPoints(points, 4); + + painter->setPen(textColor); + painter->drawLine(closeIconRect.left() + 1, + closeIconRect.top() + 1, + closeIconRect.right() - 1, + closeIconRect.bottom() - 1); + painter->drawLine(closeIconRect.left() + 1, + closeIconRect.bottom() - 1, + closeIconRect.right() - 1, + closeIconRect.top() + 1); + } + } + + // normalize button + if ((titleBar->subControls & SC_TitleBarNormalButton) + && (((titleBar->titleBarFlags & Qt::WindowMinimizeButtonHint) + && (titleBar->titleBarState & Qt::WindowMinimized)) + || ((titleBar->titleBarFlags & Qt::WindowMaximizeButtonHint) + && (titleBar->titleBarState & Qt::WindowMaximized)))) { + QRect normalButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarNormalButton, widget); + if (normalButtonRect.isValid()) { + + bool hover = + (titleBar->activeSubControls & SC_TitleBarNormalButton) && (titleBar->state & State_MouseOver); + bool sunken = + (titleBar->activeSubControls & SC_TitleBarNormalButton) && (titleBar->state & State_Sunken); + QRect normalButtonIconRect = + normalButtonRect.adjusted(buttonMargin, buttonMargin, -buttonMargin, -buttonMargin); + Ph::drawMdiButton(painter, titleBar, normalButtonRect, hover, sunken); + + QRect frontWindowRect = normalButtonIconRect.adjusted(0, 3, -3, 0); + painter->setPen(textColor); + painter->drawRect(frontWindowRect.adjusted(0, 0, -1, -1)); + painter->drawLine(frontWindowRect.left() + 1, + frontWindowRect.top() + 1, + frontWindowRect.right() - 1, + frontWindowRect.top() + 1); + painter->setPen(textAlphaColor); + const QPoint points[4] = {frontWindowRect.topLeft(), + frontWindowRect.topRight(), + frontWindowRect.bottomLeft(), + frontWindowRect.bottomRight()}; + painter->drawPoints(points, 4); + + QRect backWindowRect = normalButtonIconRect.adjusted(3, 0, 0, -3); + QRegion clipRegion = backWindowRect; + clipRegion -= frontWindowRect; + painter->save(); + painter->setClipRegion(clipRegion); + painter->setPen(textColor); + painter->drawRect(backWindowRect.adjusted(0, 0, -1, -1)); + painter->drawLine(backWindowRect.left() + 1, + backWindowRect.top() + 1, + backWindowRect.right() - 1, + backWindowRect.top() + 1); + painter->setPen(textAlphaColor); + const QPoint points2[4] = {backWindowRect.topLeft(), + backWindowRect.topRight(), + backWindowRect.bottomLeft(), + backWindowRect.bottomRight()}; + painter->drawPoints(points2, 4); + painter->restore(); + } + } + + // context help button + if (titleBar->subControls & SC_TitleBarContextHelpButton + && (titleBar->titleBarFlags & Qt::WindowContextHelpButtonHint)) { + QRect contextHelpButtonRect = + proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarContextHelpButton, widget); + if (contextHelpButtonRect.isValid()) { + bool hover = + (titleBar->activeSubControls & SC_TitleBarContextHelpButton) && (titleBar->state & State_MouseOver); + bool sunken = + (titleBar->activeSubControls & SC_TitleBarContextHelpButton) && (titleBar->state & State_Sunken); + Ph::drawMdiButton(painter, titleBar, contextHelpButtonRect, hover, sunken); + // This is lame, but I doubt it will get used often. Previously, XPM + // icon was used here (very poorly, by re-allocating a QImage over and + // over and modifying/painting it) + QIcon helpIcon = QCommonStyle::standardIcon(QStyle::SP_DialogHelpButton); + helpIcon.paint(painter, contextHelpButtonRect.adjusted(4, 4, -4, -4)); + } + } + + // shade button + if (titleBar->subControls & SC_TitleBarShadeButton && (titleBar->titleBarFlags & Qt::WindowShadeButtonHint)) { + QRect shadeButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarShadeButton, widget); + if (shadeButtonRect.isValid()) { + bool hover = + (titleBar->activeSubControls & SC_TitleBarShadeButton) && (titleBar->state & State_MouseOver); + bool sunken = + (titleBar->activeSubControls & SC_TitleBarShadeButton) && (titleBar->state & State_Sunken); + Ph::drawMdiButton(painter, titleBar, shadeButtonRect, hover, sunken); + Ph::drawArrow(painter, shadeButtonRect.adjusted(5, 7, -5, -7), Qt::UpArrow, swatch); + } + } + + // unshade button + if (titleBar->subControls & SC_TitleBarUnshadeButton && (titleBar->titleBarFlags & Qt::WindowShadeButtonHint)) { + QRect unshadeButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarUnshadeButton, widget); + if (unshadeButtonRect.isValid()) { + bool hover = + (titleBar->activeSubControls & SC_TitleBarUnshadeButton) && (titleBar->state & State_MouseOver); + bool sunken = + (titleBar->activeSubControls & SC_TitleBarUnshadeButton) && (titleBar->state & State_Sunken); + Ph::drawMdiButton(painter, titleBar, unshadeButtonRect, hover, sunken); + Ph::drawArrow(painter, unshadeButtonRect.adjusted(5, 7, -5, -7), Qt::DownArrow, swatch); + } + } + + if ((titleBar->subControls & SC_TitleBarSysMenu) && (titleBar->titleBarFlags & Qt::WindowSystemMenuHint)) { + QRect iconRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarSysMenu, widget); + if (iconRect.isValid()) { + if (!titleBar->icon.isNull()) { + titleBar->icon.paint(painter, iconRect); + } else { + QStyleOption tool = *titleBar; + QPixmap pm = proxy()->standardIcon(SP_TitleBarMenuButton, &tool, widget).pixmap(16, 16); + tool.rect = iconRect; + painter->save(); + proxy()->drawItemPixmap(painter, iconRect, Qt::AlignCenter, pm); + painter->restore(); + } + } + } + painter->restore(); + break; + } + case CC_ScrollBar: { + auto scrollBar = qstyleoption_cast(option); + if (!scrollBar) + break; + auto pr = proxy(); + QRect scrollBarSubLine = pr->subControlRect(control, scrollBar, SC_ScrollBarSubLine, widget); + QRect scrollBarAddLine = pr->subControlRect(control, scrollBar, SC_ScrollBarAddLine, widget); + QRect scrollBarSlider = pr->subControlRect(control, scrollBar, SC_ScrollBarSlider, widget); + QRect scrollBarGroove = pr->subControlRect(control, scrollBar, SC_ScrollBarGroove, widget); + + int padding = Ph::dpiScaled(4); + scrollBarSlider.setX(scrollBarSlider.x() + padding); + scrollBarSlider.setY(scrollBarSlider.y() + padding); + // Width and height should be reduced by 2 * padding, but somehow padding is enough. + scrollBarSlider.setWidth(scrollBarSlider.width() - padding); + scrollBarSlider.setHeight(scrollBarSlider.height() - padding); + + // Groove/gutter/trench area + if (scrollBar->subControls & SC_ScrollBarGroove) { + painter->fillRect(scrollBarGroove, swatch.color(S_window)); + } + + // Slider thumb + if (scrollBar->subControls & SC_ScrollBarSlider) { + qreal radius = + (scrollBar->orientation == Qt::Horizontal ? scrollBarSlider.height() : scrollBarSlider.width()) / 2.0; + painter->fillRect(scrollBarSlider, swatch.color(S_window)); + Ph::paintSolidRoundRect(painter, scrollBarSlider, radius, swatch, S_button); + } + + // The SubLine (up/left) buttons + if (scrollBar->subControls & SC_ScrollBarSubLine) { + painter->fillRect(scrollBarSubLine, swatch.color(S_window)); + } + + // The AddLine (down/right) button + if (scrollBar->subControls & SC_ScrollBarAddLine) { + painter->fillRect(scrollBarAddLine, swatch.color(S_window)); + } + break; + } + case CC_ComboBox: { + auto comboBox = qstyleoption_cast(option); + if (!comboBox) + break; + painter->save(); + bool isLeftToRight = option->direction != Qt::RightToLeft; + bool hasFocus = option->state & State_HasFocus && option->state & State_KeyboardFocusChange; + bool isSunken = comboBox->state & State_Sunken; + QRect rect = comboBox->rect; + QRect downArrowRect = proxy()->subControlRect(CC_ComboBox, comboBox, SC_ComboBoxArrow, widget); + // Draw a line edit + if (comboBox->editable) { + Swatchy buttonFill = isSunken ? S_button_pressed : S_button; + // if (!hasOptions) + // buttonFill = S_window; + painter->fillRect(rect, swatch.color(buttonFill)); + if (comboBox->frame) { + QStyleOptionFrame buttonOption; + buttonOption.QStyleOption::operator=(*comboBox); + buttonOption.rect = rect; + buttonOption.state = + (comboBox->state & (State_Enabled | State_MouseOver | State_HasFocus)) | State_KeyboardFocusChange; + if (isSunken) { + buttonOption.state |= State_Sunken; + buttonOption.state &= ~State_MouseOver; + } + proxy()->drawPrimitive(PE_FrameLineEdit, &buttonOption, painter, widget); + QRect fr = proxy()->subControlRect(CC_ComboBox, option, SC_ComboBoxEditField, widget); + QRect br = rect; + if (isLeftToRight) { + br.setLeft(fr.x() + fr.width()); + } else { + br.setRight(fr.left() - 1); + } + Qt::Edge edge = isLeftToRight ? Qt::LeftEdge : Qt::RightEdge; + Swatchy color = hasFocus ? S_highlight_outline : S_window_outline; + br.adjust(0, 1, 0, -1); + Ph::fillRectEdges(painter, br, edge, 1, swatch.color(color)); + br.adjust(1, 0, -1, 0); + Swatchy specular = isSunken ? S_button_pressed_specular : S_button_specular; + Ph::fillRectOutline(painter, br, 1, swatch.color(specular)); + } + } else { + QStyleOptionButton buttonOption; + buttonOption.QStyleOption::operator=(*comboBox); + buttonOption.rect = rect; + buttonOption.state = + comboBox->state + & (State_Enabled | State_MouseOver | State_HasFocus | State_Active | State_KeyboardFocusChange); + // Combo boxes should be shown to be keyboard interactive if they're + // focused at all, not just if the user has pressed tab to enter keyboard + // focus change mode. This is because the up/down arrows can, regardless + // of having pressed tab, control the combo box selection. + if (comboBox->state & State_HasFocus) + buttonOption.state |= State_KeyboardFocusChange; + if (isSunken) { + buttonOption.state |= State_Sunken; + buttonOption.state &= ~State_MouseOver; + } + proxy()->drawPrimitive(PE_PanelButtonCommand, &buttonOption, painter, widget); + } + if (comboBox->subControls & SC_ComboBoxArrow) { + int margin = + static_cast(qMin(downArrowRect.width(), downArrowRect.height()) * Ph::ComboBox_ArrowMarginRatio); + QRect r = downArrowRect; + r.adjust(margin, margin, -margin, -margin); + // Draw the up/down arrow + Ph::drawArrow(painter, r, Qt::DownArrow, swatch); + } + painter->restore(); + break; + } + case CC_Slider: { + auto slider = qstyleoption_cast(option); + if (!slider) + break; + const QRect groove = proxy()->subControlRect(CC_Slider, option, SC_SliderGroove, widget); + const QRect handle = proxy()->subControlRect(CC_Slider, option, SC_SliderHandle, widget); + bool horizontal = slider->orientation == Qt::Horizontal; + bool ticksAbove = slider->tickPosition & QSlider::TicksAbove; + bool ticksBelow = slider->tickPosition & QSlider::TicksBelow; + Swatchy outlineColor = S_window_outline; + if (option->state & State_HasFocus && option->state & State_KeyboardFocusChange) + outlineColor = S_highlight_outline; + if ((option->subControls & SC_SliderGroove) && groove.isValid()) { + QRect g0 = groove; + if (g0.height() > 5) + g0.adjust(0, 1, 0, -1); + Ph::PSave saver(painter); + Swatchy gutterColor = option->state & State_Enabled ? S_scrollbarGutter : S_window; + Ph::paintBorderedRoundRect(painter, groove, Ph::SliderGroove_Rounding, swatch, outlineColor, gutterColor); + } + if (option->subControls & SC_SliderTickmarks) { + Ph::PSave save(painter); + painter->setPen(swatch.pen(S_window_outline)); + int tickSize = proxy()->pixelMetric(PM_SliderTickmarkOffset, option, widget); + int available = proxy()->pixelMetric(PM_SliderSpaceAvailable, slider, widget); + int interval = slider->tickInterval; + if (interval <= 0) { + interval = slider->singleStep; + if (QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, interval, available) + - QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, 0, available) + < 3) + interval = slider->pageStep; + } + if (interval <= 0) + interval = 1; + + int v = slider->minimum; + int len = proxy()->pixelMetric(PM_SliderLength, slider, widget); + while (v <= slider->maximum + 1) { + if (v == slider->maximum + 1 && interval == 1) + break; + const int v_ = qMin(v, slider->maximum); + int pos = sliderPositionFromValue(slider->minimum, + slider->maximum, + v_, + (horizontal ? slider->rect.width() : slider->rect.height()) - len, + slider->upsideDown) + + len / 2; + int extra = 2 - ((v_ == slider->minimum || v_ == slider->maximum) ? 1 : 0); + + if (horizontal) { + if (ticksAbove) { + painter->drawLine(pos, slider->rect.top() + extra, pos, slider->rect.top() + tickSize); + } + if (ticksBelow) { + painter->drawLine(pos, slider->rect.bottom() - extra, pos, slider->rect.bottom() - tickSize); + } + } else { + if (ticksAbove) { + painter->drawLine(slider->rect.left() + extra, pos, slider->rect.left() + tickSize, pos); + } + if (ticksBelow) { + painter->drawLine(slider->rect.right() - extra, pos, slider->rect.right() - tickSize, pos); + } + } + // in the case where maximum is max int + int nextInterval = v + interval; + if (nextInterval < v) + break; + v = nextInterval; + } + } + // draw handle + if ((option->subControls & SC_SliderHandle)) { + bool isPressed = option->state & QStyle::State_Sunken && option->activeSubControls & SC_SliderHandle; + QRect r = handle; + Swatchy handleOutline, handleFill, handleSpecular; + if (option->state & State_HasFocus && option->state & State_KeyboardFocusChange) { + handleOutline = S_highlight_outline; + } else { + handleOutline = S_window_outline; + } + if (isPressed) { + handleFill = S_sliderHandle_pressed; + handleSpecular = S_sliderHandle_pressed_specular; + } else { + handleFill = S_sliderHandle; + handleSpecular = S_sliderHandle_specular; + } + Ph::PSave save(painter); + Ph::paintBorderedRoundRect(painter, r, Ph::SliderHandle_Rounding, swatch, handleOutline, handleFill); + r.adjust(1, 1, -1, -1); + Ph::paintBorderedRoundRect(painter, r, Ph::SliderHandle_Rounding, swatch, handleSpecular, S_none); + } + break; + } + case CC_ToolButton: { + auto tbopt = qstyleoption_cast(option); + if (Ph::AllowToolBarAutoRaise || !tbopt || !widget || !widget->parent() + || !widget->parent()->inherits("QToolBar")) { + QCommonStyle::drawComplexControl(control, option, painter, widget); + break; + } + QStyleOptionToolButton opt_; + opt_.QStyleOptionToolButton::operator=(*tbopt); + opt_.state &= ~State_AutoRaise; + QCommonStyle::drawComplexControl(control, &opt_, painter, widget); + break; + } + case CC_Dial: + if (auto dial = qstyleoption_cast(option)) + Ph::drawDial(dial, painter); + break; + default: + QCommonStyle::drawComplexControl(control, option, painter, widget); + break; + } +} + +int BaseStyle::pixelMetric(PixelMetric metric, const QStyleOption* option, const QWidget* widget) const +{ + // Calculate pixel metrics. + // Use immediate return if value is not supposed to be dpi-scaled. + int val = -1; + switch (metric) { + case PM_SliderTickmarkOffset: + val = 6; + break; + case PM_ToolTipLabelFrameWidth: + case PM_HeaderMargin: + case PM_ButtonMargin: + case PM_SpinBoxFrameWidth: + val = Phantom::DefaultFrameWidth; + break; + case PM_ButtonDefaultIndicator: + case PM_ButtonShiftHorizontal: + val = 0; + break; + case PM_ButtonShiftVertical: + if (qobject_cast(widget)) { + return 0; + } + val = 1; + break; + case PM_ComboBoxFrameWidth: + return 1; + case PM_DefaultFrameWidth: + // Original comment from fusion: + // Do not dpi-scale because the drawn frame is always exactly 1 pixel thick + // My note: + // I seriously doubt, with all of the hacky add-or-remove-1 things + // everywhere in fusion (and still in phantom), and the fact that fusion is + // totally broken in high dpi, that this actually holds true. + if (qobject_cast(widget)) { + return 1; + } + val = qMax(1, Phantom::DefaultFrameWidth - 2); + break; + case PM_MessageBoxIconSize: + val = 48; + break; + case PM_DialogButtonsSeparator: + case PM_ScrollBarSliderMin: + val = 26; + break; + case PM_TitleBarHeight: + val = 24; + break; + case PM_ScrollBarExtent: + val = 12; + break; + case PM_SliderThickness: + case PM_SliderLength: + val = 15; + break; + case PM_DockWidgetTitleMargin: + val = 1; + break; + case PM_MenuVMargin: + case PM_MenuHMargin: + case PM_MenuPanelWidth: + val = 0; + break; + case PM_MenuBarItemSpacing: + val = 0; + break; + case PM_MenuBarHMargin: + // option is usually nullptr, use widget instead to get font metrics + if (!Phantom::MenuBarLeftMargin || !widget) { + val = 0; + break; + } + return widget->fontMetrics().height() * Phantom::MenuBar_HorizontalPaddingFontRatio; + case PM_MenuBarVMargin: + case PM_MenuBarPanelWidth: + val = 0; + break; + case PM_ToolBarSeparatorExtent: + val = 9; + break; + case PM_ToolBarHandleExtent: { + int dotLen = Phantom::dpiScaled(2); + return dotLen * (3 * 2 - 1); + } + case PM_ToolBarItemSpacing: + val = 1; + break; + case PM_ToolBarFrameWidth: + val = Phantom::MenuBar_FrameWidth; + break; + case PM_ToolBarItemMargin: + val = 1; + break; + case PM_ToolBarExtensionExtent: + val = 32; + break; + case PM_ListViewIconSize: + case PM_SmallIconSize: + if (Phantom::ItemView_UseFontHeightForDecorationSize && widget + && qobject_cast(widget)) { + // QAbstractItemView::viewOptions() always uses nullptr for the + // styleoption when querying for PM_SmallIconSize. The best we can do is + // use the font set on the widget itself, which is obviously going to be + // wrong if the row has a custom font set on it. Hmm. + return widget->fontMetrics().height(); + } + val = 16; + break; + case PM_ButtonIconSize: { + if (option) + return option->fontMetrics.height(); + if (widget) + return widget->fontMetrics().height(); + val = 16; + break; + } + case PM_DockWidgetTitleBarButtonMargin: + val = 2; + break; +#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) + case PM_TitleBarButtonSize: + val = 19; + break; +#endif + case PM_MaximumDragDistance: + return -1; // Do not dpi-scale because the value is magic + case PM_TabCloseIndicatorWidth: + case PM_TabCloseIndicatorHeight: + val = 16; + break; + case PM_TabBarTabHSpace: + // Contents may clip out horizontally if we don't some extra pixels here or + // in sizeFromContents for CT_TabBarTab. + if (!option) + break; + return static_cast(option->fontMetrics.height() * Phantom::TabBar_HPaddingFontRatio) + + static_cast(Phantom::dpiScaled(4)); + case PM_TabBarTabVSpace: + if (!option) + break; + return static_cast(option->fontMetrics.height() * Phantom::TabBar_VPaddingFontRatio) + + static_cast(Phantom::dpiScaled(2)); + case PM_TabBarTabOverlap: + val = 1; + break; + case PM_TabBarBaseOverlap: + val = 2; + break; + case PM_TabBarIconSize: { + if (!widget) + break; + return widget->fontMetrics().height(); + } + case PM_TabBarTabShiftVertical: { + val = Phantom::TabBar_InctiveVShift; + break; + } + case PM_SubMenuOverlap: + val = 0; + break; + case PM_DockWidgetHandleExtent: + case PM_SplitterWidth: + val = 5; + break; + case PM_IndicatorHeight: + case PM_IndicatorWidth: + case PM_ExclusiveIndicatorHeight: + case PM_ExclusiveIndicatorWidth: + if (option) + return option->fontMetrics.height(); + if (widget) + return widget->fontMetrics().height(); + val = 14; + break; + case PM_ScrollView_ScrollBarOverlap: + case PM_ScrollView_ScrollBarSpacing: + val = 0; + break; + case PM_TreeViewIndentation: { + if (widget) + return widget->fontMetrics().height(); + val = 12; + break; + } + default: + val = QCommonStyle::pixelMetric(metric, option, widget); + } + return Phantom::dpiScaled(val); +} + +QSize BaseStyle::sizeFromContents(ContentsType type, + const QStyleOption* option, + const QSize& size, + const QWidget* widget) const +{ + namespace Ph = Phantom; + // Cases which do not rely on the parent class to do any work + switch (type) { + case CT_RadioButton: + case CT_CheckBox: { + auto btn = qstyleoption_cast(option); + if (!btn) + break; + bool isRadio = type == CT_RadioButton; + int w = proxy()->pixelMetric(isRadio ? PM_ExclusiveIndicatorWidth : PM_IndicatorWidth, btn, widget); + int h = proxy()->pixelMetric(isRadio ? PM_ExclusiveIndicatorHeight : PM_IndicatorHeight, btn, widget); + int margins = 0; + if (!btn->icon.isNull() || !btn->text.isEmpty()) + margins = + proxy()->pixelMetric(isRadio ? PM_RadioButtonLabelSpacing : PM_CheckBoxLabelSpacing, option, widget); + return QSize(size.width() + w + margins, qMax(size.height(), h)); + } + case CT_MenuBarItem: { + int fontHeight = option ? option->fontMetrics.height() : size.height(); + int w = static_cast(fontHeight * Ph::MenuBar_HorizontalPaddingFontRatio); + int h = static_cast(fontHeight * Ph::MenuBar_VerticalPaddingFontRatio); + int line = Ph::dpiScaled(1); + return QSize(size.width() + w * 2, size.height() + h * 2 + line); + } + case CT_MenuItem: { + auto menuItem = qstyleoption_cast(option); + if (!menuItem) + return size; + bool hasTabChar = menuItem->text.contains(QLatin1Char('\t')); + bool hasSubMenu = menuItem->menuItemType == QStyleOptionMenuItem::SubMenu; + bool isSeparator = menuItem->menuItemType == QStyleOptionMenuItem::Separator; + int fontMetricsHeight = -1; + // See notes at CE_MenuItem and SH_ComboBox_Popup for more information + if (Ph::UseQMenuForComboBoxPopup && qobject_cast(widget)) { + if (!widget->testAttribute(Qt::WA_SetFont)) + fontMetricsHeight = QFontMetrics(qApp->font("QMenu")).height(); + } + if (fontMetricsHeight == -1) { + fontMetricsHeight = option->fontMetrics.height(); + } + auto metrics = Ph::MenuItemMetrics::ofFontHeight(fontMetricsHeight); + // Incoming width is the sum of the visual widths of the main item text and + // the mnemonic text (if any). To this width we will add the widths of the + // other features for this menu item -- the icon/checkbox, spacing between + // icon/text/mnemonic, etc. For cases like separators without any text, we + // may disregard the width. + // + // Height is the text height, probably. + int w = size.width(); + // Frame + w += metrics.frameThickness * 2; + // Left margins don't depend on whether or not we have a submenu arrow. + // Calculating the right margins requires knowing whether or not the menu + // item has a submenu arrow. + w += metrics.leftMargin; + // Phantom treats every menu item with the same space on the left for a + // check mark, even if it doesn't have the checkable property. + w += metrics.checkWidth + metrics.checkRightSpace; + + if (!menuItem->icon.isNull()) { + // Phantom disregards any user-specified icon sizing at the moment. + w += metrics.fontHeight; + w += metrics.iconRightSpace; + } + + // Tab character is used for separating the shortcut text + if (hasTabChar) + w += metrics.mnemonicSpace; + if (hasSubMenu) + w += metrics.arrowSpace + metrics.arrowWidth + metrics.rightMarginForArrow; + else + w += metrics.rightMarginForText; + int h; + if (isSeparator) { + h = metrics.separatorHeight; + } else { + h = metrics.totalHeight; + } + if (!menuItem->icon.isNull()) { + if (auto combo = qobject_cast(widget)) { + h = qMax(combo->iconSize().height() + 2, h); + } + } + QSize sz; + sz.setWidth(qMax(w, Ph::dpiScaled(Ph::MenuMinimumWidth))); + sz.setHeight(h); + return sz; + } + case CT_Menu: { + if (!Ph::MenuExtraBottomMargin || !option || !widget) + break; + // Trick the QMenu into putting a margin only at the bottom by adding extra + // height to the contents size. We only want to add this tricky space if + // there is at least more than 1 item in the menu. + const auto acts = widget->actions(); + if (acts.count() < 2) + break; + // We only want to add the tricky space if there's at least 1 separator, + // otherwise it looks weird. + bool anySeps = false; + for (auto act : acts) { + if (act->isSeparator()) { + anySeps = true; + break; + } + } + if (!anySeps) + break; + int fheight = option->fontMetrics.height(); + int vmargin = static_cast(fheight * Ph::MenuItem_SeparatorHeightFontRatio) / 2; + QSize sz = size; + sz.setHeight(sz.height() + vmargin); + return sz; + } + case CT_TabBarTab: { + // Placeholder in case we change this in the future + return size; + } + case CT_Slider: { + QSize sz = size; + if (qobject_cast(widget)->orientation() == Qt::Horizontal) { + sz.setHeight(sz.height() + PM_SliderTickmarkOffset); + } else { + sz.setWidth(sz.width() + PM_SliderTickmarkOffset); + } + return sz; + } + case CT_GroupBox: { + // This doesn't seem to get used except once by QGroupBox for + // minimumSizeHint(). After that, the sizing/layout calculations seem to + // use the rects given by subControlRect(). + auto opt = qstyleoption_cast(option); + if (!opt) + break; + // Checkbox and text height already accounted for, but margin between text + // and frame isn't. + int xadd = 0; + int yadd = 0; + if (opt->subControls & (SC_GroupBoxCheckBox | SC_GroupBoxLabel)) { + int fontHeight = option->fontMetrics.height(); + yadd += static_cast(fontHeight * Phantom::GroupBox_LabelBottomMarginFontRatio); + } + // We can test for the frame in general, but unfortunately testing to see + // if it's the 1-line "flat" style or 4-line box/rect "anything else" style + // doesn't seem to be possible here, only when painting. + if (opt->subControls & SC_GroupBoxFrame) { + xadd += 2; + yadd += 2; + } + return QSize(size.width() + xadd, size.height() + yadd); + } + case CT_ItemViewItem: { + auto vopt = qstyleoption_cast(option); + if (!vopt) + break; + QSize sz = QCommonStyle::sizeFromContents(type, option, size, widget); + sz += QSize(0, Phantom::DefaultFrameWidth); + // QCommonStyle has a bunch of complicated logic for laying out/calculating + // rects of view items, which is locked behind a private data guy. In + // sizeFromContents for CT_ItemViewItem, it unions all of the item row's + // rects together and then, if the decoration height is exactly the same as + // the row height, it adds 2 pixels (not dpi scaled) to the height. The + // comment says it's to prevent "icons from overlapping" but I have no idea + // how that's supposed to help. And we don't necessarily want those extra 2 + // pixels. Anyway, I don't want to copy and paste all of that code into + // Phantom and then maintain it. So when Phantom is in the mode where we're + // basing the item view decoration sizes off of the font size, we'll just + // take a guess when QCommonStyle has added 2 to the height (because the + // row height and decoration height are both the font height), and + // re-remove those two pixels. +#if 1 + if (Phantom::ItemView_UseFontHeightForDecorationSize) { + int fh = vopt->fontMetrics.height(); + if (sz.height() == fh + 2 && vopt->decorationSize.height() == fh) { + sz.setHeight(fh); + } + } +#endif + return sz; + } + case CT_HeaderSection: { + auto hdr = qstyleoption_cast(option); + if (!hdr) + break; + // This is pretty crummy. Should also check if we need multi-line support + // or not. + bool nullIcon = hdr->icon.isNull(); + int margin = proxy()->pixelMetric(QStyle::PM_HeaderMargin, hdr, widget); + int iconSize = nullIcon ? 0 : option->fontMetrics.height(); + QSize txt = hdr->fontMetrics.size(Qt::TextSingleLine | Qt::TextBypassShaping, hdr->text); + QSize sz; + sz.setHeight(margin + qMax(iconSize, txt.height()) + margin); + sz.setWidth((nullIcon ? 0 : margin) + iconSize + (hdr->text.isNull() ? 0 : margin) + txt.width() + margin); + if (hdr->sortIndicator != QStyleOptionHeader::None) { + if (hdr->orientation == Qt::Horizontal) + sz.rwidth() += sz.height() + margin; + else + sz.rheight() += sz.width() + margin; + } + return sz; + } + default: + break; + } + + // Cases which modify the size given by the parent class + QSize newSize = QCommonStyle::sizeFromContents(type, option, size, widget); + switch (type) { + case CT_PushButton: { + auto pbopt = qstyleoption_cast(option); + if (!pbopt || pbopt->text.isEmpty()) + break; + int hpad = static_cast(pbopt->fontMetrics.height() * Phantom::PushButton_HorizontalPaddingFontHeightRatio); + newSize.rwidth() += hpad * 2; + if (widget && qobject_cast(widget->parent())) { + int dialogButtonMinWidth = Phantom::dpiScaled(80); + newSize.rwidth() = qMax(newSize.width(), dialogButtonMinWidth); + } + break; + } + case CT_ToolButton: +#if defined(Q_OS_MACOS) + newSize += QSize(Ph::dpiScaled(6 + Phantom::DefaultFrameWidth), Ph::dpiScaled(6 + Phantom::DefaultFrameWidth)); +#elif defined(Q_OS_WIN) + newSize += QSize(Ph::dpiScaled(4 + Phantom::DefaultFrameWidth), Ph::dpiScaled(4 + Phantom::DefaultFrameWidth)); +#else + newSize += QSize(Ph::dpiScaled(3 + Phantom::DefaultFrameWidth), Ph::dpiScaled(3 + Phantom::DefaultFrameWidth)); +#endif + break; + case CT_ComboBox: { + newSize += QSize(0, Ph::dpiScaled(4 + Phantom::DefaultFrameWidth)); + auto cb = qstyleoption_cast(option); + // Non-editable combo boxes have some extra padding on the left side, + // similar to push buttons. We should account for that here to avoid text + // being clipped off. + if (cb) { + int pad = 0; + if (cb->editable) { + pad = Ph::dpiScaled(Ph::LineEdit_ContentsHPad); + } else { + pad = Ph::dpiScaled(Ph::ComboBox_NonEditable_ContentsHPad); + } + newSize.rwidth() += pad * 2; + } + break; + } + case CT_LineEdit: { + newSize += QSize(0, 4); + int pad = Ph::dpiScaled(Ph::LineEdit_ContentsHPad); + newSize.rwidth() += pad * 2; + break; + } + case CT_SpinBox: + // No changes needed + break; + case CT_SizeGrip: + newSize += QSize(4, 4); + break; + case CT_MdiControls: + newSize -= QSize(1, 0); + break; + default: + break; + } + return newSize; +} + +void BaseStyle::polish(QApplication* app) +{ + if (!app) { + return; + } + + Q_INIT_RESOURCE(styles); + QString stylesheet; + + QFile baseStylesheetFile(":/styles/base/basestyle.qss"); + if (baseStylesheetFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + stylesheet = baseStylesheetFile.readAll(); + baseStylesheetFile.close(); + } else { + qWarning("Failed to load base theme stylesheet."); + } + + stylesheet.append(getAppStyleSheet()); + app->setStyleSheet(stylesheet); + QCommonStyle::polish(app); +} + +QRect BaseStyle::subControlRect(ComplexControl control, + const QStyleOptionComplex* option, + SubControl subControl, + const QWidget* widget) const +{ + namespace Ph = Phantom; + QRect rect = QCommonStyle::subControlRect(control, option, subControl, widget); + switch (control) { + case CC_Slider: { + auto slider = qstyleoption_cast(option); + if (!slider) + break; + int tickSize = proxy()->pixelMetric(PM_SliderTickmarkOffset, option, widget); + switch (subControl) { + case SC_SliderHandle: { + if (slider->orientation == Qt::Horizontal) { + rect.setHeight(proxy()->pixelMetric(PM_SliderThickness)); + rect.setWidth(proxy()->pixelMetric(PM_SliderLength)); + int centerY = slider->rect.center().y() - rect.height() / 2; + if (slider->tickPosition & QSlider::TicksAbove) + centerY += tickSize; + if (slider->tickPosition & QSlider::TicksBelow) + centerY -= tickSize; + rect.moveTop(centerY); + } else { + rect.setWidth(proxy()->pixelMetric(PM_SliderThickness)); + rect.setHeight(proxy()->pixelMetric(PM_SliderLength)); + int centerX = slider->rect.center().x() - rect.width() / 2; + if (slider->tickPosition & QSlider::TicksAbove) + centerX += tickSize; + if (slider->tickPosition & QSlider::TicksBelow) + centerX -= tickSize; + rect.moveLeft(centerX); + } + break; + } + case SC_SliderGroove: { + QPoint grooveCenter = slider->rect.center(); + const int grooveThickness = Ph::dpiScaled(7); + if (slider->orientation == Qt::Horizontal) { + rect.setHeight(grooveThickness); + if (slider->tickPosition & QSlider::TicksAbove) + grooveCenter.ry() += tickSize; + if (slider->tickPosition & QSlider::TicksBelow) + grooveCenter.ry() -= tickSize; + } else { + rect.setWidth(grooveThickness); + if (slider->tickPosition & QSlider::TicksAbove) + grooveCenter.rx() += tickSize; + if (slider->tickPosition & QSlider::TicksBelow) + grooveCenter.rx() -= tickSize; + } + rect.moveCenter(grooveCenter); + break; + } + default: + break; + } + break; + } + case CC_SpinBox: { + auto spinbox = qstyleoption_cast(option); + if (!spinbox) + break; + // Some leftover Fusion code here. Should clean up this mess. + int center = spinbox->rect.height() / 2; + int fw = spinbox->frame ? 1 : 0; + int y = fw; + const int buttonWidth = static_cast(Ph::dpiScaled(Ph::SpinBox_ButtonWidth)) + 2; + int x, lx, rx; + x = spinbox->rect.width() - y - buttonWidth + 2; + lx = fw; + rx = x - fw; + switch (subControl) { + case SC_SpinBoxUp: + if (spinbox->buttonSymbols == QAbstractSpinBox::NoButtons) + return {}; + rect = QRect(x, fw, buttonWidth, center - fw); + break; + case SC_SpinBoxDown: + if (spinbox->buttonSymbols == QAbstractSpinBox::NoButtons) + return QRect(); + + rect = QRect(x, center, buttonWidth, spinbox->rect.bottom() - center - fw + 1); + break; + case SC_SpinBoxEditField: + if (spinbox->buttonSymbols == QAbstractSpinBox::NoButtons) { + rect = QRect(lx, fw, spinbox->rect.width() - 2 * fw, spinbox->rect.height() - 2 * fw); + } else { + rect = QRect(lx, fw, rx - qMax(fw - 1, 0), spinbox->rect.height() - 2 * fw); + } + break; + case SC_SpinBoxFrame: + rect = spinbox->rect; + break; + default: + break; + } + rect = visualRect(spinbox->direction, spinbox->rect, rect); + break; + } + case CC_GroupBox: { + auto groupBox = qstyleoption_cast(option); + if (!groupBox) + break; + switch (subControl) { + case SC_GroupBoxFrame: + case SC_GroupBoxContents: { + QRect r = option->rect; + if (groupBox->subControls & (SC_GroupBoxLabel | SC_GroupBoxCheckBox)) { + int fontHeight = option->fontMetrics.height(); + int topMargin = qMax(pixelMetric(PM_ExclusiveIndicatorHeight), fontHeight); + topMargin += static_cast(fontHeight * Ph::GroupBox_LabelBottomMarginFontRatio); + r.setTop(r.top() + topMargin); + } + if (subControl == SC_GroupBoxContents && groupBox->subControls & SC_GroupBoxFrame) { + // Testing against groupBox->features for the frame type doesn't seem + // to work here. + r.adjust(1, 1, -1, -1); + } + return r; + } + case SC_GroupBoxCheckBox: + case SC_GroupBoxLabel: { + // Accurate height doesn't matter -- the other group box style + // implementations also fail with multi-line or too-tall text. + int textHeight = option->fontMetrics.height(); + // width()/horizontalAdvance() is faster than size() and good enough for + // us, since we only support a single line of text here anyway. + int textWidth = Phantom::fontMetricsWidth(option->fontMetrics, groupBox->text); + int indicatorWidth = proxy()->pixelMetric(PM_IndicatorWidth, option, widget); + int indicatorHeight = proxy()->pixelMetric(PM_IndicatorHeight, option, widget); + int margin = 0; + int indicatorRightSpace = textHeight / 3; + int contentWidth = textWidth; + if (option->subControls & QStyle::SC_GroupBoxCheckBox) { + contentWidth += indicatorWidth + indicatorRightSpace; + } + int x = margin; + int y = 0; + switch (groupBox->textAlignment & Qt::AlignHorizontal_Mask) { + case Qt::AlignHCenter: + x += (option->rect.width() - contentWidth) / 2; + break; + case Qt::AlignRight: + x += option->rect.width() - contentWidth; + break; + default: + break; + } + int w, h; + if (subControl == SC_GroupBoxCheckBox) { + w = indicatorWidth; + h = indicatorHeight; + if (textHeight > indicatorHeight) { + y = (textHeight - indicatorHeight) / 2; + } + } else { + w = contentWidth; + h = textHeight; + if (option->subControls & QStyle::SC_GroupBoxCheckBox) { + x += indicatorWidth + indicatorRightSpace; + w -= indicatorWidth + indicatorRightSpace; + } + } + return visualRect(option->direction, option->rect, QRect(x, y, w, h)); + } + default: + break; + } + break; + } + case CC_ComboBox: { + auto cb = qstyleoption_cast(option); + if (!cb) + return QRect(); + int frame = cb->frame ? proxy()->pixelMetric(PM_ComboBoxFrameWidth, cb, widget) : 0; + QRect r = option->rect; + r.adjust(frame, frame, -frame, -frame); + int dim = qMin(r.width(), r.height()); + if (dim < 1) + return QRect(); + switch (subControl) { + case SC_ComboBoxFrame: + return cb->rect; + case SC_ComboBoxArrow: { + QRect r0 = r; + r0.setX((r0.x() + r0.width()) - dim + 1); + return visualRect(option->direction, option->rect, r0); + } + case SC_ComboBoxEditField: { + // Add extra padding if not editable + int pad = 0; + if (cb->editable) { + // Line edit padding already added + } else { + pad = Ph::dpiScaled(Ph::ComboBox_NonEditable_ContentsHPad); + } + r.adjust(pad, 0, -dim, 0); + return visualRect(option->direction, option->rect, r); + } + case SC_ComboBoxListBoxPopup: { + return cb->rect; + } + default: + break; + } + break; + } + case CC_TitleBar: { + auto tb = qstyleoption_cast(option); + if (!tb) + break; + SubControl sc = subControl; + QRect& ret = rect; + const int indent = 3; + const int controlTopMargin = 3; + const int controlBottomMargin = 3; + const int controlWidthMargin = 2; + const int controlHeight = tb->rect.height() - controlTopMargin - controlBottomMargin; + const int delta = controlHeight + controlWidthMargin; + int offset = 0; + bool isMinimized = tb->titleBarState & Qt::WindowMinimized; + bool isMaximized = tb->titleBarState & Qt::WindowMaximized; + switch (sc) { + case SC_TitleBarLabel: + if (tb->titleBarFlags & (Qt::WindowTitleHint | Qt::WindowSystemMenuHint)) { + ret = tb->rect; + if (tb->titleBarFlags & Qt::WindowSystemMenuHint) + ret.adjust(delta, 0, -delta, 0); + if (tb->titleBarFlags & Qt::WindowMinimizeButtonHint) + ret.adjust(0, 0, -delta, 0); + if (tb->titleBarFlags & Qt::WindowMaximizeButtonHint) + ret.adjust(0, 0, -delta, 0); + if (tb->titleBarFlags & Qt::WindowShadeButtonHint) + ret.adjust(0, 0, -delta, 0); + if (tb->titleBarFlags & Qt::WindowContextHelpButtonHint) + ret.adjust(0, 0, -delta, 0); + } + break; + case SC_TitleBarContextHelpButton: + if (tb->titleBarFlags & Qt::WindowContextHelpButtonHint) + offset += delta; + Q_FALLTHROUGH(); + case SC_TitleBarMinButton: + if (!isMinimized && (tb->titleBarFlags & Qt::WindowMinimizeButtonHint)) + offset += delta; + else if (sc == SC_TitleBarMinButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarNormalButton: + if (isMinimized && (tb->titleBarFlags & Qt::WindowMinimizeButtonHint)) + offset += delta; + else if (isMaximized && (tb->titleBarFlags & Qt::WindowMaximizeButtonHint)) + offset += delta; + else if (sc == SC_TitleBarNormalButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarMaxButton: + if (!isMaximized && (tb->titleBarFlags & Qt::WindowMaximizeButtonHint)) + offset += delta; + else if (sc == SC_TitleBarMaxButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarShadeButton: + if (!isMinimized && (tb->titleBarFlags & Qt::WindowShadeButtonHint)) + offset += delta; + else if (sc == SC_TitleBarShadeButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarUnshadeButton: + if (isMinimized && (tb->titleBarFlags & Qt::WindowShadeButtonHint)) + offset += delta; + else if (sc == SC_TitleBarUnshadeButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarCloseButton: + if (tb->titleBarFlags & Qt::WindowSystemMenuHint) + offset += delta; + else if (sc == SC_TitleBarCloseButton) + break; + ret.setRect( + tb->rect.right() - indent - offset, tb->rect.top() + controlTopMargin, controlHeight, controlHeight); + break; + case SC_TitleBarSysMenu: + if (tb->titleBarFlags & Qt::WindowSystemMenuHint) { + ret.setRect(tb->rect.left() + controlWidthMargin + indent, + tb->rect.top() + controlTopMargin, + controlHeight, + controlHeight); + } + break; + default: + break; + } + ret = visualRect(tb->direction, tb->rect, ret); + break; + } + default: + break; + } + + return rect; +} + +QRect BaseStyle::itemPixmapRect(const QRect& r, int flags, const QPixmap& pixmap) const +{ + return QCommonStyle::itemPixmapRect(r, flags, pixmap); +} + +void BaseStyle::drawItemPixmap(QPainter* painter, const QRect& rect, int alignment, const QPixmap& pixmap) const +{ + QCommonStyle::drawItemPixmap(painter, rect, alignment, pixmap); +} + +QStyle::SubControl BaseStyle::hitTestComplexControl(ComplexControl cc, + const QStyleOptionComplex* opt, + const QPoint& pt, + const QWidget* w) const +{ + return QCommonStyle::hitTestComplexControl(cc, opt, pt, w); +} + +QPixmap BaseStyle::generatedIconPixmap(QIcon::Mode iconMode, const QPixmap& pixmap, const QStyleOption* opt) const +{ + return QCommonStyle::generatedIconPixmap(iconMode, pixmap, opt); +} + +int BaseStyle::styleHint(StyleHint hint, + const QStyleOption* option, + const QWidget* widget, + QStyleHintReturn* returnData) const +{ + switch (hint) { + case SH_Slider_SnapToValue: + case SH_PrintDialog_RightAlignButtons: + case SH_FontDialog_SelectAssociatedText: + case SH_ComboBox_ListMouseTracking: + case SH_Slider_StopMouseOverSlider: + case SH_ScrollBar_MiddleClickAbsolutePosition: + case SH_TitleBar_AutoRaise: + case SH_TitleBar_NoBorder: + case SH_ItemView_ArrowKeysNavigateIntoChildren: + case SH_ItemView_ChangeHighlightOnFocus: + case SH_MenuBar_MouseTracking: + case SH_Menu_MouseTracking: + return 1; + case SH_Menu_SupportsSections: + return 0; +#ifndef Q_OS_MAC + case SH_MenuBar_AltKeyNavigation: + return 1; +#endif +#if defined(QT_PLATFORM_UIKIT) + case SH_ComboBox_UseNativePopup: + return 1; +#endif + case SH_ItemView_ShowDecorationSelected: + // QWindowsStyle does this as well -- QCommonStyle seems to have some + // internal confusion buried within its private implementation of laying + // out and drawing item views where it can't keep track of what's + // considered a decoration and what's not. For tree views, if you give 0 + // for ShowDecorationSelected, it applies only to the disclosure indicator + // and not to the QIcon/pixmap that might be present for the item. So + // selecting an item in a tree view will have the selection color drawn + // underneath the icon/pixmap, but not the disclosure indicator. However, + // in list views, if you give 0 for ShowDecorationSelected, it will *not* + // draw the selection color underneath the icon/pixmap. There's no way to + // access this internal logic in QCommonStyle without fully reimplementing + // the huge mass of stuff for item view layout and drawing. Therefore, the + // best we can do is at least try to get consistent behavior: if it's a + // list view, just always return 1 for ShowDecorationSelected. + if (!Phantom::ShowItemViewDecorationSelected && qobject_cast(widget)) + return 1; + return Phantom::ShowItemViewDecorationSelected; + case SH_ItemView_MovementWithoutUpdatingSelection: + return 1; +#if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)) + case SH_ItemView_ScrollMode: + return QAbstractItemView::ScrollPerPixel; +#endif + case SH_ScrollBar_ContextMenu: +#ifdef Q_OS_MAC + return 0; +#else + return 1; +#endif + // Some Linux distros might want to enable this, but it doesn't behave very + // consistently with varied QPalettes, depending on how the QPA and icons + // deal with both light and dark themes. It might seem weird to just disable + // this, but none of (Mac, Windows, BeOS/Haiku) show icons in dialog buttons, + // and the results on Linux are generally pretty messy -- not sure why it's + // historically been the default, especially when other button types + // generally don't have any icons. + case SH_DialogButtonBox_ButtonsHaveIcons: + return 0; + case SH_ScrollBar_Transient: + return 1; + case SH_EtchDisabledText: + case SH_DitherDisabledText: + case SH_ToolBox_SelectedPageTitleBold: + case SH_Menu_AllowActiveAndDisabled: + case SH_MainWindow_SpaceBelowMenuBar: + case SH_MessageBox_CenterButtons: + case SH_RubberBand_Mask: + case SH_ScrollView_FrameOnlyAroundContents: + return 0; + case SH_ComboBox_Popup: { + return Phantom::UseQMenuForComboBoxPopup; + // Fusion did this, but we don't because of font bugs (especially in high + // DPI) with the QMenu that the combo box will create instead of a dropdown + // view. See notes in CE_MenuItem for more details. + if (auto cmb = qstyleoption_cast(option)) + return !cmb->editable; + return 0; + } + case SH_Table_GridLineColor: { + using namespace Phantom::SwatchColors; + namespace Ph = Phantom; + auto ph_swatchPtr = Ph::getCachedSwatchOfQPalette(&d->swatchCache, &d->headSwatchFastKey, option->palette); + const Ph::PhSwatch& swatch = *ph_swatchPtr.data(); + // Qt code in table views for drawing grid lines is broken. See case for + // CE_ItemViewItem painting for more information. + return option ? static_cast(swatch.color(S_base_divider).rgb()) : 0; + } + case SH_MessageBox_TextInteractionFlags: + return Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse; + case SH_WizardStyle: + return QWizard::ClassicStyle; + case SH_Menu_SubMenuPopupDelay: + // Returning 0 will break sloppy submenus even if they're enabled + return 10; + case SH_Menu_SloppySubMenus: + return true; + case SH_Menu_SubMenuSloppyCloseTimeout: + return 500; + case SH_Menu_SubMenuDontStartSloppyOnLeave: + return 1; + case SH_Menu_SubMenuSloppySelectOtherActions: + return 1; + case SH_Menu_SubMenuUniDirection: + return 1; + case SH_Menu_SubMenuUniDirectionFailCount: + return 1; + case SH_Menu_SubMenuResetWhenReenteringParent: + return 0; +#ifdef Q_OS_MAC + case SH_Menu_FlashTriggeredItem: + return 1; + case SH_Menu_FadeOutOnHide: + return 0; +#endif + case SH_WindowFrame_Mask: + return 0; + case SH_UnderlineShortcut: { + return false; + } + case SH_Widget_Animate: + return 1; + default: + break; + } + return QCommonStyle::styleHint(hint, option, widget, returnData); +} + +QRect BaseStyle::subElementRect(SubElement sr, const QStyleOption* opt, const QWidget* w) const +{ + switch (sr) { + case SE_ProgressBarLabel: + case SE_ProgressBarContents: + case SE_ProgressBarGroove: + return opt->rect; + case SE_PushButtonFocusRect: { + QRect r = QCommonStyle::subElementRect(sr, opt, w); + r.adjust(0, 1, 0, -1); + return r; + } + case SE_DockWidgetTitleBarText: { + auto titlebar = qstyleoption_cast(opt); + if (!titlebar) + break; + QRect r = QCommonStyle::subElementRect(sr, opt, w); + bool verticalTitleBar = titlebar->verticalTitleBar; + if (verticalTitleBar) { + r.adjust(0, 0, 0, -4); + } else { + if (opt->direction == Qt::LeftToRight) + r.adjust(4, 0, 0, 0); + else + r.adjust(0, 0, -4, 0); + } + return r; + } + case SE_TreeViewDisclosureItem: { + if (Phantom::BranchesOnEdge) { + // Shove it all the way to the left (or right) side, probably outside of + // the rect it gave us. Old-school. + QRect rect = opt->rect; + if (opt->direction != Qt::RightToLeft) { + rect.moveLeft(0); + if (rect.width() < rect.height()) + rect.setWidth(rect.height()); + } else { + // todo + } + return rect; + } + break; + } + case SE_LineEditContents: { + QRect r = QCommonStyle::subElementRect(sr, opt, w); + int pad = Phantom::dpiScaled(Phantom::LineEdit_ContentsHPad); + return r.adjusted(pad, 0, -pad, 0); + } + default: + break; + } + return QCommonStyle::subElementRect(sr, opt, w); +} + +// Table header layout reference +// ----------------------------- +// +// begin: QStyleOptionHeader::Beginning; +// mid: QStyleOptionHeader::Middle; +// end: QStyleOptionHeader::End; +// one: QStyleOptionHeader::OnlyOneSection; +// one*: +// This is specified as QStyleOptionHeader::OnlyOneSection, but the call to +// drawControl(CE_HeaderSection...) is being performed by an instance of +// QTableCornerButton, defined in qtableview.cpp as a subclass of +// QAbstractButton. Only table views can have these corner buttons, and they +// only appear if there are both at least 1 column and 1 row visible. +// +// Configuration A: A table view with both columns and rows +// +// Configuration B: A list view, or a tree view, or a table view with no rows +// in the data or all rows hidden, such that the corner button is also made +// hidden. +// +// Configuration C: A table view with no columns in the data or all columns +// hidden, such that the corner button is also made hidden. +// +// Configuration A, Left-to-right, 4x4 +// [ one* ][ begin ][ mid ][ mid ][ end ] +// [ begin ] +// [ mid ] +// [ mid ] +// [ end ] +// +// Configuration A, Left-to-right, 2x2 +// [ one* ][ begin ][ end ] +// [ begin ] +// [ end ] +// +// Configuration A, Left-to-right, 1x1 +// [ one* ][ one ] +// [ one ] +// +// Configuration A, Right-to-left, 4x4 +// [ begin ][ mid ][ mid ][ end ][ one* ] +// [ begin ] +// [ mid ] +// [ mid ] +// [ end ] +// +// Configuration A, Right-to-left, 2x2 +// [ begin ][ end ][ one* ] +// [ begin ] +// [ end ] +// +// Configuration A, Right-to-left, 1x1 +// [ one ][ one* ] +// [ one ] +// +// Configuration B, Left-to-right and right-to-left, 4 columns (table view: +// 4 columns with 0 rows, list/tree view: 4 columns, rows count doesn't matter): +// [ begin ][ mid ][ mid ][ end ] +// +// Configuration B, Left-to-right and right-to-left, 2 columns (table view: +// 2 columns with 0 rows, list/tree view: 2 columns, rows count doesn't matter): +// [ begin ][ end ] +// +// Configuration B, Left-to-right and right-to-left, 1 column (table view: +// 1 column with 0 rows, list view: 1 column, rows count doesn't matter): +// [ one ] +// +// Configuration C, left-to-right and right-to-left, table view with no columns +// and 4 rows: +// [ begin ] +// [ mid ] +// [ mid ] +// [ end ] +// +// Configuration C, left-to-right and right-to-left, table view with no columns +// and 2 rows: +// [ begin ] +// [ end ] +// +// Configuration C, left-to-right and right-to-left, table view with no columns +// and 1 row: +// [ one ] diff --git a/src/gui/styles/base/BaseStyle.h b/src/gui/styles/base/BaseStyle.h new file mode 100644 index 000000000..d6269fad7 --- /dev/null +++ b/src/gui/styles/base/BaseStyle.h @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2020 KeePassXC Team + * Copyright (C) 2019 Andrew Richards + * + * Derived from Phantomstyle and relicensed under the GPLv2 or v3. + * https://github.com/randrew/phantomstyle + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_BASESTYLE_H +#define KEEPASSXC_BASESTYLE_H + +#include + +class BaseStylePrivate; + +class BaseStyle : public QCommonStyle +{ + Q_OBJECT + +public: + BaseStyle(); + ~BaseStyle() override; + + enum PhantomPrimitiveElement + { + Phantom_PE_IndicatorTabNew = PE_CustomBase + 1, + Phantom_PE_ScrollBarSliderVertical, + Phantom_PE_WindowFrameColor, + }; + + QPalette standardPalette() const override; + void drawPrimitive(PrimitiveElement elem, + const QStyleOption* option, + QPainter* painter, + const QWidget* widget = nullptr) const override; + void + drawControl(ControlElement ce, const QStyleOption* option, QPainter* painter, const QWidget* widget) const override; + int pixelMetric(PixelMetric metric, + const QStyleOption* option = nullptr, + const QWidget* widget = nullptr) const override; + void drawComplexControl(ComplexControl control, + const QStyleOptionComplex* option, + QPainter* painter, + const QWidget* widget) const override; + QRect subElementRect(SubElement r, const QStyleOption* opt, const QWidget* widget = nullptr) const override; + QSize sizeFromContents(ContentsType type, + const QStyleOption* option, + const QSize& size, + const QWidget* widget) const override; + SubControl hitTestComplexControl(ComplexControl cc, + const QStyleOptionComplex* opt, + const QPoint& pt, + const QWidget* w = nullptr) const override; + QRect subControlRect(ComplexControl cc, + 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, + QStyleHintReturn* returnData = nullptr) const override; + QRect itemPixmapRect(const QRect& r, int flags, const QPixmap& pixmap) const override; + void drawItemPixmap(QPainter* painter, const QRect& rect, int alignment, const QPixmap& pixmap) const override; + void drawItemText(QPainter* painter, + const QRect& rect, + int flags, + const QPalette& pal, + bool enabled, + const QString& text, + QPalette::ColorRole textRole = QPalette::NoRole) const override; + + using QCommonStyle::polish; + void polish(QApplication* app) override; + +protected: + /** + * @return Paths to application stylesheets + */ + virtual QString getAppStyleSheet() const + { + return {}; + } + + BaseStylePrivate* d; +}; + +#endif diff --git a/src/gui/styles/base/basestyle.qss b/src/gui/styles/base/basestyle.qss new file mode 100644 index 000000000..4219c4cfc --- /dev/null +++ b/src/gui/styles/base/basestyle.qss @@ -0,0 +1,48 @@ +QPushButton:default { + background: palette(highlight); + color: palette(highlighted-text); +} + +QSpinBox { + min-width: 90px; +} + +QDialogButtonBox QPushButton { + min-width: 55px; +} + +QCheckBox, QRadioButton { + spacing: 10px; +} + +DatabaseWidget, GroupView { + background-color: palette(window); + border: none; +} + +EntryPreviewWidget QLineEdit, EntryPreviewWidget QTextEdit { + background-color: palette(window); + border: none; +} + +DatabaseOpenWidget #loginFrame { + border: 2px groove palette(mid); + background: palette(light); +} + +QGroupBox { + margin-top: 1.4em; + margin-bottom: 1.4em; + font-weight: bold; +} + +QGroupBox::title { + margin-top: -3.4em; + margin-left: -.4em; + subcontrol-origin: padding; +} + +QToolTip { + border: none; + padding: 3px; +} diff --git a/src/gui/styles/base/phantomcolor.cpp b/src/gui/styles/base/phantomcolor.cpp new file mode 100644 index 000000000..3689cfc3f --- /dev/null +++ b/src/gui/styles/base/phantomcolor.cpp @@ -0,0 +1,423 @@ +/* + * HSLuv-C: Human-friendly HSL + * + * + * + * Copyright (c) 2015 Alexei Boronine (original idea, JavaScript implementation) + * Copyright (c) 2015 Roger Tallada (Obj-C implementation) + * Copyright (c) 2017 Martin Mitas (C implementation, based on Obj-C implementation) + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include "phantomcolor.h" +#include +#include + +namespace Phantom +{ + namespace + { + + // Th`ese declarations originate from hsluv.h, from the hsluv-c library. The + // hpluv functions have been removed, as they are unnecessary for Phantom. + /** + * Convert HSLuv to RGB. + * + * @param h Hue. Between 0.0 and 360.0. + * @param s Saturation. Between 0.0 and 100.0. + * @param l Lightness. Between 0.0 and 100.0. + * @param[out] pr Red component. Between 0.0 and 1.0. + * @param[out] pr Green component. Between 0.0 and 1.0. + * @param[out] pr Blue component. Between 0.0 and 1.0. + */ + void hsluv2rgb(double h, double s, double l, double* pr, double* pg, double* pb); + + /** + * Convert RGB to HSLuv. + * + * @param r Red component. Between 0.0 and 1.0. + * @param g Green component. Between 0.0 and 1.0. + * @param b Blue component. Between 0.0 and 1.0. + * @param[out] ph Hue. Between 0.0 and 360.0. + * @param[out] ps Saturation. Between 0.0 and 100.0. + * @param[out] pl Lightness. Between 0.0 and 100.0. + */ + void rgb2hsluv(double r, double g, double b, double* ph, double* ps, double* pl); + + // Contents below originate from hsluv.c from the hsluv-c library. They have + // been wrapped in a C++ namespace to avoid collisions and to reduce the + // translation unit count, and hsluv's own sRGB conversion code has been + // stripped out (sRGB conversion is now performed in the Phantom color code + // when going to/from the Rgb type.) + // + // If you need to update the hsluv-c code, be mindful of the removed sRGB + // conversions -- you will need to make similar modifications to the upstream + // hsluv-c code. Also note that that the hpluv (pastel) functions have been + // removed, as they are not used in Phantom. + typedef struct Triplet_tag Triplet; + struct Triplet_tag + { + double a; + double b; + double c; + }; + + /* for RGB */ + const Triplet m[3] = {{3.24096994190452134377, -1.53738317757009345794, -0.49861076029300328366}, + {-0.96924363628087982613, 1.87596750150772066772, 0.04155505740717561247}, + {0.05563007969699360846, -0.20397695888897656435, 1.05697151424287856072}}; + + /* for XYZ */ + const Triplet m_inv[3] = {{0.41239079926595948129, 0.35758433938387796373, 0.18048078840183428751}, + {0.21263900587151035754, 0.71516867876775592746, 0.07219231536073371500}, + {0.01933081871559185069, 0.11919477979462598791, 0.95053215224966058086}}; + + const double ref_u = 0.19783000664283680764; + const double ref_v = 0.46831999493879100370; + + const double kappa = 903.29629629629629629630; + const double epsilon = 0.00885645167903563082; + + typedef struct Bounds_tag Bounds; + struct Bounds_tag + { + double a; + double b; + }; + + void get_bounds(double l, Bounds bounds[6]) + { + double tl = l + 16.0; + double sub1 = (tl * tl * tl) / 1560896.0; + double sub2 = (sub1 > epsilon ? sub1 : (l / kappa)); + int channel; + int t; + + for (channel = 0; channel < 3; channel++) { + double m1 = m[channel].a; + double m2 = m[channel].b; + double m3 = m[channel].c; + + for (t = 0; t < 2; t++) { + double top1 = (284517.0 * m1 - 94839.0 * m3) * sub2; + double top2 = (838422.0 * m3 + 769860.0 * m2 + 731718.0 * m1) * l * sub2 - 769860.0 * t * l; + double bottom = (632260.0 * m3 - 126452.0 * m2) * sub2 + 126452.0 * t; + + bounds[channel * 2 + t].a = top1 / bottom; + bounds[channel * 2 + t].b = top2 / bottom; + } + } + } + + double ray_length_until_intersect(double theta, const Bounds* line) + { + return line->b / (sin(theta) - line->a * cos(theta)); + } + + double max_chroma_for_lh(double l, double h) + { + double min_len = DBL_MAX; + double hrad = h * 0.01745329251994329577; /* (2 * pi / 360) */ + Bounds bounds[6]; + int i; + + get_bounds(l, bounds); + for (i = 0; i < 6; i++) { + double len = ray_length_until_intersect(hrad, &bounds[i]); + + if (len >= 0 && len < min_len) + min_len = len; + } + return min_len; + } + + double dot_product(const Triplet* t1, const Triplet* t2) + { + return (t1->a * t2->a + t1->b * t2->b + t1->c * t2->c); + } + + void xyz2rgb(Triplet* in_out) + { + double r = dot_product(&m[0], in_out); + double g = dot_product(&m[1], in_out); + double b = dot_product(&m[2], in_out); + in_out->a = r; + in_out->b = g; + in_out->c = b; + } + + void rgb2xyz(Triplet* in_out) + { + Triplet rgbl = {in_out->a, in_out->b, in_out->c}; + double x = dot_product(&m_inv[0], &rgbl); + double y = dot_product(&m_inv[1], &rgbl); + double z = dot_product(&m_inv[2], &rgbl); + in_out->a = x; + in_out->b = y; + in_out->c = z; + } + + /* http://en.wikipedia.org/wiki/CIELUV + * In these formulas, Yn refers to the reference white point. We are using + * illuminant D65, so Yn (see refY in Maxima file) equals 1. The formula is + * simplified accordingly. + */ + double y2l(double y) + { + if (y <= epsilon) { + return y * kappa; + } else { + return 116.0 * cbrt(y) - 16.0; + } + } + + double l2y(double l) + { + if (l <= 8.0) { + return l / kappa; + } else { + double x = (l + 16.0) / 116.0; + return (x * x * x); + } + } + + void xyz2luv(Triplet* in_out) + { + double divisor = in_out->a + (15.0 * in_out->b) + (3.0 * in_out->c); + if (divisor <= 0.00000001) { + in_out->a = 0.0; + in_out->b = 0.0; + in_out->c = 0.0; + return; + } + + double var_u = (4.0 * in_out->a) / divisor; + double var_v = (9.0 * in_out->b) / divisor; + double l = y2l(in_out->b); + double u = 13.0 * l * (var_u - ref_u); + double v = 13.0 * l * (var_v - ref_v); + + in_out->a = l; + if (l < 0.00000001) { + in_out->b = 0.0; + in_out->c = 0.0; + } else { + in_out->b = u; + in_out->c = v; + } + } + + void luv2xyz(Triplet* in_out) + { + if (in_out->a <= 0.00000001) { + /* Black will create a divide-by-zero error. */ + in_out->a = 0.0; + in_out->b = 0.0; + in_out->c = 0.0; + return; + } + + double var_u = in_out->b / (13.0 * in_out->a) + ref_u; + double var_v = in_out->c / (13.0 * in_out->a) + ref_v; + double y = l2y(in_out->a); + double x = -(9.0 * y * var_u) / ((var_u - 4.0) * var_v - var_u * var_v); + double z = (9.0 * y - (15.0 * var_v * y) - (var_v * x)) / (3.0 * var_v); + in_out->a = x; + in_out->b = y; + in_out->c = z; + } + + void luv2lch(Triplet* in_out) + { + double l = in_out->a; + double u = in_out->b; + double v = in_out->c; + double h; + double c = sqrt(u * u + v * v); + + /* Grays: disambiguate hue */ + if (c < 0.00000001) { + h = 0; + } else { + h = atan2(v, u) * 57.29577951308232087680; /* (180 / pi) */ + if (h < 0.0) + h += 360.0; + } + + in_out->a = l; + in_out->b = c; + in_out->c = h; + } + + void lch2luv(Triplet* in_out) + { + double hrad = in_out->c * 0.01745329251994329577; /* (pi / 180.0) */ + double u = cos(hrad) * in_out->b; + double v = sin(hrad) * in_out->b; + + in_out->b = u; + in_out->c = v; + } + + void hsluv2lch(Triplet* in_out) + { + double h = in_out->a; + double s = in_out->b; + double l = in_out->c; + double c; + + /* White and black: disambiguate chroma */ + if (l > 99.9999999 || l < 0.00000001) { + c = 0.0; + } else { + c = max_chroma_for_lh(l, h) / 100.0 * s; + } + + /* Grays: disambiguate hue */ + if (s < 0.00000001) + h = 0.0; + + in_out->a = l; + in_out->b = c; + in_out->c = h; + } + + void lch2hsluv(Triplet* in_out) + { + double l = in_out->a; + double c = in_out->b; + double h = in_out->c; + double s; + + /* White and black: disambiguate saturation */ + if (l > 99.9999999 || l < 0.00000001) { + s = 0.0; + } else { + s = c / max_chroma_for_lh(l, h) * 100.0; + } + + /* Grays: disambiguate hue */ + if (c < 0.00000001) + h = 0.0; + + in_out->a = h; + in_out->b = s; + in_out->c = l; + } + + void hsluv2rgb(double h, double s, double l, double* pr, double* pg, double* pb) + { + Triplet tmp = {h, s, l}; + + hsluv2lch(&tmp); + lch2luv(&tmp); + luv2xyz(&tmp); + xyz2rgb(&tmp); + + *pr = tmp.a; + *pg = tmp.b; + *pb = tmp.c; + } + + void rgb2hsluv(double r, double g, double b, double* ph, double* ps, double* pl) + { + Triplet tmp = {r, g, b}; + + rgb2xyz(&tmp); + xyz2luv(&tmp); + luv2lch(&tmp); + lch2hsluv(&tmp); + + *ph = tmp.a; + *ps = tmp.b; + *pl = tmp.c; + } + + } // namespace +} // namespace Phantom + +// The code below is for Phantom, and is used for the Rgb/Hsl-based interface +// for color operations. +namespace Phantom +{ + namespace + { + // Note: these constants might be out of range when qreal is defined as float + // instead of double. + inline qreal linear_of_srgb(qreal x) + { + return x < 0.0404482362771082 ? x / 12.92 : std::pow((x + 0.055) / 1.055, 2.4f); + } + inline qreal srgb_of_linear(qreal x) + { + return x < 0.00313066844250063 ? x * 12.92 : std::pow(x, 1.0 / 2.4) * 1.055 - 0.055; + } + } // namespace + + Rgb rgb_of_qcolor(const QColor& color) + { + Rgb a; + a.r = linear_of_srgb(color.red() / 255.0); + a.g = linear_of_srgb(color.green() / 255.0); + a.b = linear_of_srgb(color.blue() / 255.0); + return a; + } + + Hsl hsl_of_rgb(qreal r, qreal g, qreal b) + { + double h, s, l; + rgb2hsluv(r, g, b, &h, &s, &l); + s /= 100.0; + l /= 100.0; + return {h, s, l}; + } + + Rgb rgb_of_hsl(qreal h, qreal s, qreal l) + { + double r, g, b; + hsluv2rgb(h, s * 100.0, l * 100.0, &r, &g, &b); + return {r, g, b}; + } + + QColor qcolor_of_rgb(qreal r, qreal g, qreal b) + { + int r_ = static_cast(std::lround(srgb_of_linear(r) * 255.0)); + int g_ = static_cast(std::lround(srgb_of_linear(g) * 255.0)); + int b_ = static_cast(std::lround(srgb_of_linear(b) * 255.0)); + return {r_, g_, b_}; + } + + QColor lerpQColor(const QColor& x, const QColor& y, qreal a) + { + Rgb x_ = rgb_of_qcolor(x); + Rgb y_ = rgb_of_qcolor(y); + Rgb z = Rgb::lerp(x_, y_, a); + return qcolor_of_rgb(z.r, z.g, z.b); + } + + Rgb Rgb::lerp(const Rgb& x, const Rgb& y, qreal a) + { + Rgb z; + z.r = (1.0 - a) * x.r + a * y.r; + z.g = (1.0 - a) * x.g + a * y.g; + z.b = (1.0 - a) * x.b + a * y.b; + return z; + } +} // namespace Phantom diff --git a/src/gui/styles/base/phantomcolor.h b/src/gui/styles/base/phantomcolor.h new file mode 100644 index 000000000..f9573ba65 --- /dev/null +++ b/src/gui/styles/base/phantomcolor.h @@ -0,0 +1,165 @@ +/* + * HSLuv-C: Human-friendly HSL + * + * + * + * Copyright (c) 2015 Alexei Boronine (original idea, JavaScript implementation) + * Copyright (c) 2015 Roger Tallada (Obj-C implementation) + * Copyright (c) 2017 Martin Mitas (C implementation, based on Obj-C implementation) + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#ifndef PHANTOMCOLOR_H +#define PHANTOMCOLOR_H + +#include + +namespace Phantom +{ + struct Rgb; + struct Hsl; + + // A color presumed to be in linear space, represented as RGB. Values are in + // the range 0.0 - 1.0. Conversions to and from QColor will assume the QColor + // is in sRGB space, and sRGB conversion will be performed. + struct Rgb + { + qreal r, g, b; + Rgb() + { + } + Rgb(qreal r, qreal g, qreal b) + : r(r) + , g(g) + , b(b) + { + } + + inline Hsl toHsl() const; + inline QColor toQColor() const; + static inline Rgb ofHsl(const Hsl&); + static inline Rgb ofQColor(const QColor&); + + static Rgb lerp(const Rgb& x, const Rgb& y, qreal a); + }; + + // A color represented as pseudo-CIE hue, saturation, and lightness. Hue is in + // the range 0.0 - 360.0 (degrees). Lightness and saturation are in the range + // 0.0 - 1.0. Using this and making adjustments to the L value will produce + // more consistent and predictable results than QColor's .darker()/.lighter(). + // Note that this is not strictly CIE -- some of the colorspace is distorted so + // that it can represented as a continuous coordinate space. Therefore not all + // adjustments to the parameters will produce perfectly linear results with + // regards to saturation and lightness. But it's still useful, and better than + // QColor's .darker()/.lighter(). Additionally, the L value is more useful for + // performing comparisons between two colors to measure relative and absolute + // brightness. + // + // See the documentation for the hsluv library for more information. (Note that + // for consistency we treat the S and L values in the range 0.0 - 1.0 instead + // of 0.0 - 100.0 like hsluv-c on its own does.) + struct Hsl + { + qreal h, s, l; + Hsl() + { + } + Hsl(qreal h, qreal s, qreal l) + : h(h) + , s(s) + , l(l) + { + } + + inline Rgb toRgb() const; + inline QColor toQColor() const; + static inline Hsl ofRgb(const Rgb&); + static inline Hsl ofQColor(const QColor&); + }; + Rgb rgb_of_qcolor(const QColor& color); + QColor qcolor_of_rgb(qreal r, qreal g, qreal b); + Hsl hsl_of_rgb(qreal r, qreal g, qreal b); + Rgb rgb_of_hsl(qreal h, qreal s, qreal l); + + // Clip a floating point value to the range 0.0 - 1.0. + inline qreal saturate(qreal x) + { + if (x < 0.0) + return 0.0; + if (x > 1.0) + return 1.0; + return x; + } + + inline qreal lerp(qreal x, qreal y, qreal a) + { + return (1.0 - a) * x + a * y; + } + + // Linearly interpolate two QColors after trasnforming them to linear color + // space, treating the QColor values as if they were in sRGB space. The + // returned QColor is converted back to sRGB space. + QColor lerpQColor(const QColor& x, const QColor& y, qreal a); + + Hsl Rgb::toHsl() const + { + return hsl_of_rgb(r, g, b); + } + + QColor Rgb::toQColor() const + { + return qcolor_of_rgb(r, g, b); + } + + Rgb Rgb::ofHsl(const Hsl& hsl) + { + return rgb_of_hsl(hsl.h, hsl.s, hsl.l); + } + + Rgb Rgb::ofQColor(const QColor& color) + { + return rgb_of_qcolor(color); + } + + Rgb Hsl::toRgb() const + { + return rgb_of_hsl(h, s, l); + } + + QColor Hsl::toQColor() const + { + Rgb rgb = rgb_of_hsl(h, s, l); + return qcolor_of_rgb(rgb.r, rgb.g, rgb.b); + } + + Hsl Hsl::ofRgb(const Rgb& rgb) + { + return hsl_of_rgb(rgb.r, rgb.g, rgb.b); + } + + Hsl Hsl::ofQColor(const QColor& color) + { + Rgb rgb = rgb_of_qcolor(color); + return hsl_of_rgb(rgb.r, rgb.g, rgb.b); + } + +} // namespace Phantom + +#endif diff --git a/src/gui/styles/dark/DarkStyle.cpp b/src/gui/styles/dark/DarkStyle.cpp new file mode 100644 index 000000000..db006dbfc --- /dev/null +++ b/src/gui/styles/dark/DarkStyle.cpp @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2020 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DarkStyle.h" +#include "gui/osutils/OSUtils.h" + +#include +#include +#include +#include + +void DarkStyle::polish(QPalette& palette) +{ + palette.setColor(QPalette::Active, QPalette::Window, QStringLiteral("#3B3B3D")); + palette.setColor(QPalette::Inactive, QPalette::Window, QStringLiteral("#404042")); + palette.setColor(QPalette::Disabled, QPalette::Window, QStringLiteral("#424242")); + + palette.setColor(QPalette::Active, QPalette::WindowText, QStringLiteral("#CACBCE")); + palette.setColor(QPalette::Inactive, QPalette::WindowText, QStringLiteral("#C8C8C6")); + palette.setColor(QPalette::Disabled, QPalette::WindowText, QStringLiteral("#707070")); + + palette.setColor(QPalette::Active, QPalette::Text, QStringLiteral("#CACBCE")); + palette.setColor(QPalette::Inactive, QPalette::Text, QStringLiteral("#C8C8C6")); + palette.setColor(QPalette::Disabled, QPalette::Text, QStringLiteral("#707070")); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) + palette.setColor(QPalette::Active, QPalette::PlaceholderText, QStringLiteral("#7D7D82")); + palette.setColor(QPalette::Inactive, QPalette::PlaceholderText, QStringLiteral("#87888C")); + palette.setColor(QPalette::Disabled, QPalette::PlaceholderText, QStringLiteral("#737373")); +#endif + + palette.setColor(QPalette::Active, QPalette::BrightText, QStringLiteral("#252627")); + palette.setColor(QPalette::Inactive, QPalette::BrightText, QStringLiteral("#2D2D2F")); + palette.setColor(QPalette::Disabled, QPalette::BrightText, QStringLiteral("#333333")); + + palette.setColor(QPalette::Active, QPalette::Base, QStringLiteral("#27272A")); + palette.setColor(QPalette::Inactive, QPalette::Base, QStringLiteral("#2A2A2D")); + palette.setColor(QPalette::Disabled, QPalette::Base, QStringLiteral("#343437")); + + palette.setColor(QPalette::Active, QPalette::AlternateBase, QStringLiteral("#303036")); + palette.setColor(QPalette::Inactive, QPalette::AlternateBase, QStringLiteral("#333338")); + palette.setColor(QPalette::Disabled, QPalette::AlternateBase, QStringLiteral("#36363A")); + + palette.setColor(QPalette::All, QPalette::ToolTipBase, QStringLiteral("#2D532D")); + palette.setColor(QPalette::All, QPalette::ToolTipText, QStringLiteral("#BFBFBF")); + + palette.setColor(QPalette::Active, QPalette::Button, QStringLiteral("#28282B")); + palette.setColor(QPalette::Inactive, QPalette::Button, QStringLiteral("#2B2B2E")); + palette.setColor(QPalette::Disabled, QPalette::Button, QStringLiteral("#2B2A2A")); + + palette.setColor(QPalette::Active, QPalette::ButtonText, QStringLiteral("#B9B9BE")); + palette.setColor(QPalette::Inactive, QPalette::ButtonText, QStringLiteral("#9E9FA5")); + palette.setColor(QPalette::Disabled, QPalette::ButtonText, QStringLiteral("#73747E")); + + palette.setColor(QPalette::Active, QPalette::Highlight, QStringLiteral("#2D532D")); + palette.setColor(QPalette::Inactive, QPalette::Highlight, QStringLiteral("#294C29")); + palette.setColor(QPalette::Disabled, QPalette::Highlight, QStringLiteral("#293D29")); + + palette.setColor(QPalette::Active, QPalette::HighlightedText, QStringLiteral("#CCCCCC")); + palette.setColor(QPalette::Inactive, QPalette::HighlightedText, QStringLiteral("#C7C7C7")); + palette.setColor(QPalette::Disabled, QPalette::HighlightedText, QStringLiteral("#707070")); + + palette.setColor(QPalette::All, QPalette::Light, QStringLiteral("#414145")); + palette.setColor(QPalette::All, QPalette::Midlight, QStringLiteral("#39393C")); + palette.setColor(QPalette::All, QPalette::Mid, QStringLiteral("#2F2F32")); + palette.setColor(QPalette::All, QPalette::Dark, QStringLiteral("#202022")); + palette.setColor(QPalette::All, QPalette::Shadow, QStringLiteral("#19191A")); + + palette.setColor(QPalette::All, QPalette::Link, QStringLiteral("#6BAE6B")); + palette.setColor(QPalette::Disabled, QPalette::Link, QStringLiteral("#9DE9D")); + palette.setColor(QPalette::All, QPalette::LinkVisited, QStringLiteral("#70A970")); + palette.setColor(QPalette::Disabled, QPalette::LinkVisited, QStringLiteral("#98A998")); +} + +QString DarkStyle::getAppStyleSheet() const +{ + QFile extStylesheetFile(QStringLiteral(":/styles/dark/darkstyle.qss")); + if (extStylesheetFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + return extStylesheetFile.readAll(); + } + qWarning("Failed to load dark theme stylesheet."); + return {}; +} + +void DarkStyle::polish(QWidget* widget) +{ + if (qobject_cast(widget) || qobject_cast(widget) || qobject_cast(widget) + || qobject_cast(widget)) { + auto palette = widget->palette(); +#if defined(Q_OS_MACOS) + if (osUtils->isDarkMode()) { + // Let the Cocoa platform plugin draw its own background + palette.setColor(QPalette::All, QPalette::Window, Qt::transparent); + } else { + palette.setColor(QPalette::Active, QPalette::Window, QStringLiteral("#2A2A2A")); + palette.setColor(QPalette::Inactive, QPalette::Window, QStringLiteral("#2D2D2D")); + palette.setColor(QPalette::Disabled, QPalette::Window, QStringLiteral("#2A2A2A")); + } +#elif defined(Q_OS_WIN) + // Register event filter for better dark mode support + WinUtils::registerEventFilters(); + palette.setColor(QPalette::All, QPalette::Window, QStringLiteral("#2F2F30")); +#else + palette.setColor(QPalette::Active, QPalette::Window, QStringLiteral("#2F2F30")); + palette.setColor(QPalette::Inactive, QPalette::Window, QStringLiteral("#313133")); + palette.setColor(QPalette::Disabled, QPalette::Window, QStringLiteral("#3A3A3B")); +#endif + + widget->setPalette(palette); + } +} diff --git a/src/gui/styles/dark/DarkStyle.h b/src/gui/styles/dark/DarkStyle.h new file mode 100644 index 000000000..aab949c3a --- /dev/null +++ b/src/gui/styles/dark/DarkStyle.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2020 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_DARKSTYLE_H +#define KEEPASSXC_DARKSTYLE_H + +#include "gui/styles/base/BaseStyle.h" +#include + +class DarkStyle : public BaseStyle +{ + Q_OBJECT + +public: + using BaseStyle::polish; + void polish(QPalette& palette) override; + void polish(QWidget* widget) override; + +protected: + QString getAppStyleSheet() const override; +}; + +#endif // KEEPASSXC_DARKSTYLE_H diff --git a/src/gui/styles/dark/darkstyle.qss b/src/gui/styles/dark/darkstyle.qss new file mode 100644 index 000000000..39ec32a2b --- /dev/null +++ b/src/gui/styles/dark/darkstyle.qss @@ -0,0 +1,18 @@ +DatabaseWidget:!active, GroupView:!active, +EntryPreviewWidget QLineEdit:!active, EntryPreviewWidget QTextEdit:!active { + background-color: #404042; +} + +DatabaseWidget:disabled, GroupView:disabled, +EntryPreviewWidget QLineEdit:disabled, EntryPreviewWidget QTextEdit:disabled { + background-color: #424242; +} + +QToolTip { + color: #BFBFBF; + background-color: #2D532D; +} + +QGroupBox { + background-color: palette(light); +} diff --git a/src/gui/styles/light/LightStyle.cpp b/src/gui/styles/light/LightStyle.cpp new file mode 100644 index 000000000..2f73c53b9 --- /dev/null +++ b/src/gui/styles/light/LightStyle.cpp @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2020 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "LightStyle.h" +#include "gui/ApplicationSettingsWidget.h" +#include "gui/osutils/OSUtils.h" + +#include +#include +#include +#include + +void LightStyle::polish(QPalette& palette) +{ + palette.setColor(QPalette::Active, QPalette::Window, QStringLiteral("#F7F7F7")); + palette.setColor(QPalette::Inactive, QPalette::Window, QStringLiteral("#FCFCFC")); + palette.setColor(QPalette::Disabled, QPalette::Window, QStringLiteral("#EDEDED")); + + palette.setColor(QPalette::Active, QPalette::WindowText, QStringLiteral("#1D1D20")); + palette.setColor(QPalette::Inactive, QPalette::WindowText, QStringLiteral("#252528")); + palette.setColor(QPalette::Disabled, QPalette::WindowText, QStringLiteral("#8C8C92")); + + palette.setColor(QPalette::Active, QPalette::Text, QStringLiteral("#1D1D20")); + palette.setColor(QPalette::Inactive, QPalette::Text, QStringLiteral("#252528")); + palette.setColor(QPalette::Disabled, QPalette::Text, QStringLiteral("#8C8C92")); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) + palette.setColor(QPalette::Active, QPalette::PlaceholderText, QStringLiteral("#71727D")); + palette.setColor(QPalette::Inactive, QPalette::PlaceholderText, QStringLiteral("#878893")); + palette.setColor(QPalette::Disabled, QPalette::PlaceholderText, QStringLiteral("#A3A4AC")); +#endif + + palette.setColor(QPalette::Active, QPalette::BrightText, QStringLiteral("#F3F3F4")); + palette.setColor(QPalette::Inactive, QPalette::BrightText, QStringLiteral("#EAEAEB")); + palette.setColor(QPalette::Disabled, QPalette::BrightText, QStringLiteral("#E4E5E7")); + + palette.setColor(QPalette::Active, QPalette::Base, QStringLiteral("#F9F9F9")); + palette.setColor(QPalette::Inactive, QPalette::Base, QStringLiteral("#F5F5F4")); + palette.setColor(QPalette::Disabled, QPalette::Base, QStringLiteral("#EFEFF2")); + + palette.setColor(QPalette::Active, QPalette::AlternateBase, QStringLiteral("#ECF3E8")); + palette.setColor(QPalette::Inactive, QPalette::AlternateBase, QStringLiteral("#EAF2E6")); + palette.setColor(QPalette::Disabled, QPalette::AlternateBase, QStringLiteral("#E1E9DD")); + + palette.setColor(QPalette::All, QPalette::ToolTipBase, QStringLiteral("#548C1D")); + palette.setColor(QPalette::All, QPalette::ToolTipText, QStringLiteral("#F7F7F7")); + + palette.setColor(QPalette::Active, QPalette::Button, QStringLiteral("#D4D5DD")); + palette.setColor(QPalette::Inactive, QPalette::Button, QStringLiteral("#DCDCE0")); + palette.setColor(QPalette::Disabled, QPalette::Button, QStringLiteral("#E5E5E6")); + + palette.setColor(QPalette::Active, QPalette::ButtonText, QStringLiteral("#181A18")); + palette.setColor(QPalette::Inactive, QPalette::ButtonText, QStringLiteral("#5F6671")); + palette.setColor(QPalette::Disabled, QPalette::ButtonText, QStringLiteral("#97979B")); + + palette.setColor(QPalette::Active, QPalette::Highlight, QStringLiteral("#549712")); + palette.setColor(QPalette::Inactive, QPalette::Highlight, QStringLiteral("#528D16")); + palette.setColor(QPalette::Disabled, QPalette::Highlight, QStringLiteral("#6F9847")); + + palette.setColor(QPalette::Active, QPalette::HighlightedText, QStringLiteral("#FCFCFC")); + palette.setColor(QPalette::Inactive, QPalette::HighlightedText, QStringLiteral("#F2F2F2")); + palette.setColor(QPalette::Disabled, QPalette::HighlightedText, QStringLiteral("#D9D9D9")); + + palette.setColor(QPalette::All, QPalette::Light, QStringLiteral("#F9F9F9")); + palette.setColor(QPalette::All, QPalette::Midlight, QStringLiteral("#E9E9EB")); + palette.setColor(QPalette::All, QPalette::Mid, QStringLiteral("#C9C9CF")); + palette.setColor(QPalette::All, QPalette::Dark, QStringLiteral("#BBBBC2")); + palette.setColor(QPalette::All, QPalette::Shadow, QStringLiteral("#6C6D79")); + + palette.setColor(QPalette::All, QPalette::Link, QStringLiteral("#429F14")); + palette.setColor(QPalette::Disabled, QPalette::Link, QStringLiteral("#949F8F")); + palette.setColor(QPalette::All, QPalette::LinkVisited, QStringLiteral("#3F8C17")); + palette.setColor(QPalette::Disabled, QPalette::LinkVisited, QStringLiteral("#838C7E")); +} + +QString LightStyle::getAppStyleSheet() const +{ + QFile extStylesheetFile(QStringLiteral(":/styles/light/lightstyle.qss")); + if (extStylesheetFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + return extStylesheetFile.readAll(); + } + qWarning("Failed to load light theme stylesheet."); + return {}; +} + +void LightStyle::polish(QWidget* widget) +{ + if (qobject_cast(widget) || qobject_cast(widget) || qobject_cast(widget) + || qobject_cast(widget)) { + auto palette = widget->palette(); +#if defined(Q_OS_MACOS) + if (!osUtils->isDarkMode()) { + // Let the Cocoa platform plugin draw its own background + palette.setColor(QPalette::All, QPalette::Window, Qt::transparent); + } else { + palette.setColor(QPalette::Active, QPalette::Window, QStringLiteral("#D6D6D6")); + palette.setColor(QPalette::Inactive, QPalette::Window, QStringLiteral("#F6F6F6")); + palette.setColor(QPalette::Disabled, QPalette::Window, QStringLiteral("#D4D4D4")); + } +#elif defined(Q_OS_WIN) + palette.setColor(QPalette::All, QPalette::Window, QStringLiteral("#FFFFFF")); +#else + palette.setColor(QPalette::Active, QPalette::Window, QStringLiteral("#EFF0F1")); + palette.setColor(QPalette::Inactive, QPalette::Window, QStringLiteral("#EFF0F1")); + palette.setColor(QPalette::Disabled, QPalette::Window, QStringLiteral("#E1E2E4")); +#endif + + widget->setPalette(palette); + } +} diff --git a/src/gui/styles/light/LightStyle.h b/src/gui/styles/light/LightStyle.h new file mode 100644 index 000000000..72153bd15 --- /dev/null +++ b/src/gui/styles/light/LightStyle.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2020 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_LIGHTSTYLE_H +#define KEEPASSXC_LIGHTSTYLE_H + +#include "gui/styles/base/BaseStyle.h" +#include + +class LightStyle : public BaseStyle +{ + Q_OBJECT + +public: + using BaseStyle::polish; + void polish(QPalette& palette) override; + void polish(QWidget* widget) override; + +protected: + QString getAppStyleSheet() const override; +}; + +#endif // KEEPASSXC_LIGHTSTYLE_H diff --git a/src/gui/styles/light/lightstyle.qss b/src/gui/styles/light/lightstyle.qss new file mode 100644 index 000000000..e6ea4d138 --- /dev/null +++ b/src/gui/styles/light/lightstyle.qss @@ -0,0 +1,18 @@ +DatabaseWidget:!active, GroupView:!active, +EntryPreviewWidget QLineEdit:!active, EntryPreviewWidget QTextEdit:!active { + background-color: #FCFCFC; +} + +DatabaseWidget:disabled, GroupView:disabled, +EntryPreviewWidget QLineEdit:disabled, EntryPreviewWidget QTextEdit:disabled { + background-color: #EDEDED; +} + +QGroupBox::title { + color: palette(highlight); +} + +QToolTip { + color: #F7F7F7; + background-color: #548C1D; +} diff --git a/src/gui/styles/styles.qrc b/src/gui/styles/styles.qrc new file mode 100644 index 000000000..c8e9057dc --- /dev/null +++ b/src/gui/styles/styles.qrc @@ -0,0 +1,8 @@ + + + + base/basestyle.qss + dark/darkstyle.qss + light/lightstyle.qss + + diff --git a/src/main.cpp b/src/main.cpp index 846baa67d..524f112b3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -29,6 +29,9 @@ #include "gui/Application.h" #include "gui/MainWindow.h" #include "gui/MessageBox.h" +#include "gui/osutils/OSUtils.h" +#include "gui/styles/dark/DarkStyle.h" +#include "gui/styles/light/LightStyle.h" #if defined(WITH_ASAN) && defined(WITH_LSAN) #include @@ -60,6 +63,20 @@ int main(int argc, char** argv) Application app(argc, argv); Application::setApplicationName("KeePassXC"); Application::setApplicationVersion(KEEPASSXC_VERSION); + + QString appTheme = config()->get("GUI/ApplicationTheme").toString(); + if (appTheme == "auto") { + if (osUtils->isDarkMode()) { + QApplication::setStyle(new DarkStyle); + } else { + QApplication::setStyle(new LightStyle); + } + } else if (appTheme == "light") { + QApplication::setStyle(new LightStyle); + } else if (appTheme == "dark") { + QApplication::setStyle(new DarkStyle); + } + // don't set organizationName as that changes the return value of // QStandardPaths::writableLocation(QDesktopServices::DataLocation) Bootstrap::bootstrapApplication();