diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f56136038c..d504d89232 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -86,8 +86,12 @@ set(keepassx_SOURCES format/OpVaultReaderAttachments.cpp format/OpVaultReaderBandEntry.cpp format/OpVaultReaderSections.cpp - gui/styles/dark/darkstyle.qrc - gui/styles/dark/KpxcDarkStyle.cpp + gui/styles/styles.qrc + gui/styles/base/BaseStyle.cpp + gui/styles/dark/DarkStyle.cpp + gui/styles/light/LightStyle.cpp + gui/styles/phantomstyle/phantomcolor.cpp + gui/styles/phantomstyle/phantomstyle.cpp gui/AboutDialog.cpp gui/Application.cpp gui/CategoryListWidget.cpp diff --git a/src/gui/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui index 3c6de499ab..c2a5f715de 100644 --- a/src/gui/ApplicationSettingsWidgetGeneral.ui +++ b/src/gui/ApplicationSettingsWidgetGeneral.ui @@ -7,7 +7,7 @@ 0 0 684 - 951 + 1206 @@ -57,11 +57,11 @@ - - - Minimize window after unlocking database - - + + + Minimize window after unlocking database + + @@ -337,7 +337,7 @@ - + @@ -350,12 +350,6 @@ true - - - 0 - 0 - - Qt::StrongFocus diff --git a/src/gui/CategoryListWidget.cpp b/src/gui/CategoryListWidget.cpp index c57b19bc03..ef95f8c738 100644 --- a/src/gui/CategoryListWidget.cpp +++ b/src/gui/CategoryListWidget.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -158,9 +159,8 @@ CategoryListWidgetDelegate::CategoryListWidgetDelegate(QListWidget* parent) } } -#ifdef Q_OS_WIN -#include -class WindowsCorrectedStyle : public QProxyStyle + +class IconSelectionCorrectedStyle : public QProxyStyle { public: void drawPrimitive(PrimitiveElement element, @@ -171,8 +171,8 @@ class WindowsCorrectedStyle : public QProxyStyle 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 +186,6 @@ class WindowsCorrectedStyle : public QProxyStyle painter->restore(); } }; -#endif void CategoryListWidgetDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, @@ -203,12 +202,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.ui b/src/gui/DatabaseOpenWidget.ui index 60b2feadc2..0acbfdc162 100644 --- a/src/gui/DatabaseOpenWidget.ui +++ b/src/gui/DatabaseOpenWidget.ui @@ -2,6 +2,14 @@ DatabaseOpenWidget + + + 0 + 0 + 576 + 505 + + Unlock KeePassXC Database @@ -99,7 +107,7 @@ - + 550 diff --git a/src/gui/SearchHelpWidget.ui b/src/gui/SearchHelpWidget.ui index 45e0d0bc6e..4668e409ba 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/styles/base/BaseStyle.cpp b/src/gui/styles/base/BaseStyle.cpp new file mode 100644 index 0000000000..f8baee506b --- /dev/null +++ b/src/gui/styles/base/BaseStyle.cpp @@ -0,0 +1,56 @@ +/* + * 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 +#include + +#include "gui/styles/phantomstyle/phantomstyle.h" +#include "BaseStyle.h" + +BaseStyle::BaseStyle() +{ + setBaseStyle(new PhantomStyle); +} + +BaseStyle::BaseStyle(QProxyStyle* style) + : QProxyStyle(style) +{ +} + +void BaseStyle::polish(QApplication* app) +{ + 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 theme base stylesheet."); + } + + QFile extensionStyleSheetFile(getStylesheetPath()); + if (extensionStyleSheetFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + stylesheet.append(extensionStyleSheetFile.readAll()); + extensionStyleSheetFile.close(); + } else { + qWarning("Failed to load theme extension stylesheet."); + } + + app->setStyleSheet(stylesheet); +} diff --git a/src/gui/styles/base/BaseStyle.h b/src/gui/styles/base/BaseStyle.h new file mode 100644 index 0000000000..eea98ab401 --- /dev/null +++ b/src/gui/styles/base/BaseStyle.h @@ -0,0 +1,43 @@ +/* + * 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_BASESTYLE_H +#define KEEPASSXC_BASESTYLE_H + +#include +#include + +class BaseStyle : public QProxyStyle +{ + Q_OBJECT + +public: + BaseStyle(); + explicit BaseStyle(QProxyStyle* style); + + using QProxyStyle::polish; + void polish(QApplication* app) override; + +protected: + /** + * @return Path to stylesheet which extends basestyle.qss + */ + virtual QString getStylesheetPath() const = 0; +}; + + +#endif //KEEPASSXC_BASESTYLE_H diff --git a/src/gui/styles/base/basestyle.qss b/src/gui/styles/base/basestyle.qss new file mode 100644 index 0000000000..e2e06993a8 --- /dev/null +++ b/src/gui/styles/base/basestyle.qss @@ -0,0 +1,186 @@ +QPushButton, QLineEdit, QTextEdit, QSpinBox { + padding: 5px; +} + +QToolButton { + padding: 4px; +} + +QComboBox:!editable { + padding: 4px 5px; +} + +QComboBox:editable, QDateTimeEdit { + /* With Phantom as the base style, this must be adjusted in + C++, because the QLineEdit doesn't scale with padding set here. */ + /*padding: 5px;*/ +} + +QPushButton:default { + background: palette(highlight); + color: palette(highlighted-text); +} + +QSpinBox { + min-width: 90px; +} + +QPushButton { + padding: 5px 30px; +} + +QDialogButtonBox QPushButton { + min-width: 55px; +} + +QCheckBox, QRadioButton { + spacing: 10px; +} + +QToolTip { + border: 1px solid palette(mid); + border-radius: 2px; +} + +QMenuBar { + padding: 0 0 0 10px; + margin: 0; + border: none; +} + +QMenuBar::item { + padding: 5px; + border-radius: 2px; + color: palette(window-text); +} + +QMenuBar::item:selected { + color: palette(highlighted-text); + background-color: palette(highlight); +} + +QMenuBar::item:pressed { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +QToolBar { + border: none; + margin: 0; + padding: 0 10px 0 10px; +} + +/* Fusion adjustments */ + +/*QTabBar {*/ +/* background: none;*/ +/* border-bottom: 2px groove palette(midlight);*/ +/*}*/ + +/*QTabBar::tab {*/ +/* background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(mid), stop: 1 palette(base));*/ +/* border-top-left-radius: 4px;*/ +/* border-top-right-radius: 4px;*/ +/* border: 2px groove palette(mid);*/ +/* border-bottom-color: palette(midlight);*/ +/* padding: 5px 10px 5px 12px;*/ +/*}*/ + +/*QTabBar::tab:hover, QTabBar::tab::focus {*/ +/* background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(midlight), stop: 1 palette(base));*/ +/*}*/ + +/*QTabBar::tab:selected {*/ +/* background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(light), stop: 1 palette(window));*/ +/* border-color: palette(midlight);*/ +/* border-top: 2px outset palette(highlight);*/ +/* border-bottom: 2px solid palette(window);*/ +/*}*/ + +/*QTabWidget::tab-bar {*/ +/* alignment: center;*/ +/*}*/ + +/*QTabWidget::pane {*/ +/* background-color: palette(window);*/ +/* border: 2px groove palette(midlight);*/ +/* border-radius: 4px;*/ +/* top: -2px;*/ +/*}*/ + +/* Phantomstyle adjustments */ + +QTabBar::tab { + background-color: palette(button); + border-top-left-radius: 2px; + border-top-right-radius: 2px; + border: 2px groove palette(mid); + border-bottom: 1px solid palette(mid); + padding: 7px 12px; +} + +QTabBar::tab:hover, QTabBar::tab:focus { + background-color: palette(base); +} + +QTabBar::tab:selected { + background-color: palette(window); + border-color: palette(mid); + border-top: 2px outset palette(highlight); + border-bottom-color: palette(window); +} + +QTabBar::tab:!selected { + bottom: 1px; + padding-bottom: 6px; + margin-top: 1px; +} + +QTabWidget::tab-bar { + alignment: center; +} + +QGroupBox * { + background: none; +} + +QGroupBox { + background-color: palette(light); + border: 2px groove palette(mid); + border-radius: 4px; + margin-top: 2.5em; + font-weight: bold; +} + +QGroupBox::title { + background: none; + border-bottom: 2px groove palette(highlight); + padding: 5px; + subcontrol-origin: margin; + subcontrol-position: top center; + top: .5em; +} + +QTreeView::item, QHeaderView::section { + padding: 4px 5px; +} + +DatabaseWidget, GroupView { + background-color: palette(window); + border: none; +} + +EntryPreviewWidget QTabWidget > QWidget { + background-color: palette(window); +} + +EntryPreviewWidget QLineEdit, EntryPreviewWidget QTextEdit { + background-color: palette(window); + border: none; +} + +DatabaseOpenWidget #loginFrame { + border: 2px groove palette(mid); + background: palette(light); + border-radius: 2px; +} diff --git a/src/gui/styles/dark/KpxcDarkStyle.cpp b/src/gui/styles/dark/DarkStyle.cpp similarity index 87% rename from src/gui/styles/dark/KpxcDarkStyle.cpp rename to src/gui/styles/dark/DarkStyle.cpp index 04c44cfcb9..3d98abc7e2 100644 --- a/src/gui/styles/dark/KpxcDarkStyle.cpp +++ b/src/gui/styles/dark/DarkStyle.cpp @@ -18,24 +18,20 @@ #include #include -#include "KpxcDarkStyle.h" +#include "gui/styles/phantomstyle/phantomstyle.h" +#include "DarkStyle.h" -KpxcDarkStyle::KpxcDarkStyle() +DarkStyle::DarkStyle() { - setBaseStyle(QStyleFactory::create(QStringLiteral("Fusion"))); + setBaseStyle(new PhantomStyle); } -KpxcDarkStyle::KpxcDarkStyle(KpxcDarkStyle* style) - : QProxyStyle(style) +DarkStyle::DarkStyle(QProxyStyle* style) + : BaseStyle(style) { } -void KpxcDarkStyle::polish(QWidget* widget) -{ - QProxyStyle::polish(widget); -} - -void KpxcDarkStyle::polish(QPalette& palette) +void DarkStyle::polish(QPalette& palette) { palette.setColor(QPalette::Active, QPalette::Window, QStringLiteral("#3B3D3C")); palette.setColor(QPalette::Inactive, QPalette::Window, QStringLiteral("#363736")); @@ -94,15 +90,7 @@ void KpxcDarkStyle::polish(QPalette& palette) palette.setColor(QPalette::All, QPalette::LinkVisited, QStringLiteral("#5B8048")); } -void KpxcDarkStyle::polish(QApplication* app) +QString DarkStyle::getStylesheetPath() const { - Q_INIT_RESOURCE(darkstyle); - QFile stylesheet(":/styles/darkstyle.qss"); - if (stylesheet.open(QIODevice::ReadOnly | QIODevice::Text)) { - QString qsStylesheet = stylesheet.readAll(); - app->setStyleSheet(qsStylesheet); - stylesheet.close(); - } else { - qWarning("Failed to load theme stylesheet."); - } + return QStringLiteral(":/styles/dark/darkstyle.qss"); } diff --git a/src/gui/styles/dark/KpxcDarkStyle.h b/src/gui/styles/dark/DarkStyle.h similarity index 71% rename from src/gui/styles/dark/KpxcDarkStyle.h rename to src/gui/styles/dark/DarkStyle.h index 3e590d77a3..ab589c77a2 100644 --- a/src/gui/styles/dark/KpxcDarkStyle.h +++ b/src/gui/styles/dark/DarkStyle.h @@ -15,24 +15,26 @@ * along with this program. If not, see . */ -#ifndef KEEPASSXC_KPXCDARKSTYLE_H -#define KEEPASSXC_KPXCDARKSTYLE_H +#ifndef KEEPASSXC_DARKSTYLE_H +#define KEEPASSXC_DARKSTYLE_H +#include "gui/styles/base/BaseStyle.h" #include -#include -class KpxcDarkStyle : public QProxyStyle +class DarkStyle : public BaseStyle { Q_OBJECT public: - KpxcDarkStyle(); - explicit KpxcDarkStyle(KpxcDarkStyle* style); + DarkStyle(); + explicit DarkStyle(QProxyStyle* style); - void polish(QWidget* widget) override; + using BaseStyle::polish; void polish(QPalette& palette) override; - void polish(QApplication* app) override; + +protected: + QString getStylesheetPath() const override; }; -#endif //KEEPASSXC_KPXCDARKSTYLE_H +#endif //KEEPASSXC_DARKSTYLE_H diff --git a/src/gui/styles/dark/darkstyle.qrc b/src/gui/styles/dark/darkstyle.qrc deleted file mode 100644 index 00165eec61..0000000000 --- a/src/gui/styles/dark/darkstyle.qrc +++ /dev/null @@ -1,6 +0,0 @@ - - - - darkstyle.qss - - diff --git a/src/gui/styles/dark/darkstyle.qss b/src/gui/styles/dark/darkstyle.qss index 9df679b42b..e69de29bb2 100644 --- a/src/gui/styles/dark/darkstyle.qss +++ b/src/gui/styles/dark/darkstyle.qss @@ -1,55 +0,0 @@ -QPushButton { - padding: 5px 20px; -} - -QMenuBar{ - border: none; - padding: 0; - margin: 5px 5px 0 5px; -} - -QMenuBar::item { - margin: 0; - padding: 5px; - background: none; - border: none; - border-radius: 2px; -} - -QMenuBar::item:selected { - color: palette(highlight-text); - background-color: palette(highlight); -} - -QMenuBar::item:pressed { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -QToolBar { - border: none; - padding: 0; - margin: 10px; -} - -QTabBar::tab { - background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(mid), stop: 1 palette(dark)); - border-top-left-radius: 3px; - border-top-right-radius: 3px; - border: 1px solid palette(mid); - border-bottom: none; - padding: 5px 10px 5px 12px; -} - -QTabBar::tab:selected { - background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(light), stop: 1 palette(window)); -} - -QTreeView::item, QHeaderView::section { - padding: 4px 5px; -} - -DatabaseWidget, GroupView { - background-color: palette(window); - border: none; -} diff --git a/src/gui/styles/light/LightStyle.cpp b/src/gui/styles/light/LightStyle.cpp new file mode 100644 index 0000000000..c4a9c01620 --- /dev/null +++ b/src/gui/styles/light/LightStyle.cpp @@ -0,0 +1,96 @@ +/* + * 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 +#include + +#include "gui/styles/phantomstyle/phantomstyle.h" +#include "LightStyle.h" + +LightStyle::LightStyle() +{ + setBaseStyle(new PhantomStyle); +} + +LightStyle::LightStyle(QProxyStyle* style) + : BaseStyle(style) +{ +} + +void LightStyle::polish(QPalette& palette) +{ + palette.setColor(QPalette::Active, QPalette::Window, QStringLiteral("#EFEFEF")); + palette.setColor(QPalette::Inactive, QPalette::Window, QStringLiteral("#E7E8E8")); + palette.setColor(QPalette::Disabled, QPalette::Window, QStringLiteral("#CECFCF")); + + palette.setColor(QPalette::Active, QPalette::WindowText, QStringLiteral("#080908")); + palette.setColor(QPalette::Inactive, QPalette::WindowText, QStringLiteral("#0E0F0E")); + palette.setColor(QPalette::Disabled, QPalette::WindowText, QStringLiteral("#616761")); + + palette.setColor(QPalette::Active, QPalette::Base, QStringLiteral("#F9F9F9")); + palette.setColor(QPalette::Inactive, QPalette::Base, QStringLiteral("#F0F1F0")); + palette.setColor(QPalette::Disabled, QPalette::Base, QStringLiteral("#DADCD9")); + + palette.setColor(QPalette::Active, QPalette::AlternateBase, QStringLiteral("#E5EFE1")); + palette.setColor(QPalette::Inactive, QPalette::AlternateBase, QStringLiteral("#DFEAD9")); + palette.setColor(QPalette::Disabled, QPalette::AlternateBase, QStringLiteral("#D5E0CF")); + + palette.setColor(QPalette::All, QPalette::ToolTipBase, QStringLiteral("#4E9E26")); + palette.setColor(QPalette::All, QPalette::ToolTipText, QStringLiteral("#EAEBEA")); + + palette.setColor(QPalette::Active, QPalette::PlaceholderText, QStringLiteral("#181A18")); + palette.setColor(QPalette::Inactive, QPalette::PlaceholderText, QStringLiteral("#454B45")); + palette.setColor(QPalette::Disabled, QPalette::PlaceholderText, QStringLiteral("#8F998F")); + + palette.setColor(QPalette::Active, QPalette::Text, QStringLiteral("#080908")); + palette.setColor(QPalette::Inactive, QPalette::Text, QStringLiteral("#0E0F0E")); + palette.setColor(QPalette::Disabled, QPalette::Text, QStringLiteral("#616761")); + + palette.setColor(QPalette::Active, QPalette::Button, QStringLiteral("#CFD1D0")); + palette.setColor(QPalette::Inactive, QPalette::Button, QStringLiteral("#DADBDA")); + palette.setColor(QPalette::Disabled, QPalette::Button, QStringLiteral("#E7E8E8")); + + palette.setColor(QPalette::Active, QPalette::ButtonText, QStringLiteral("#181A18")); + palette.setColor(QPalette::Inactive, QPalette::ButtonText, QStringLiteral("#454B45")); + palette.setColor(QPalette::Disabled, QPalette::ButtonText, QStringLiteral("#8F998F")); + + palette.setColor(QPalette::Active, QPalette::BrightText, QStringLiteral("#E0E3E0")); + palette.setColor(QPalette::Inactive, QPalette::BrightText, QStringLiteral("#D6DBD6")); + palette.setColor(QPalette::Disabled, QPalette::BrightText, QStringLiteral("#B2B9B2")); + + palette.setColor(QPalette::All, QPalette::Light, QStringLiteral("#F6F6F6")); + palette.setColor(QPalette::All, QPalette::Midlight, QStringLiteral("#D4D5D4")); + palette.setColor(QPalette::All, QPalette::Dark, QStringLiteral("#929694")); + palette.setColor(QPalette::All, QPalette::Mid, QStringLiteral("#B3B6B4")); + palette.setColor(QPalette::All, QPalette::Shadow, QStringLiteral("#636564")); + + palette.setColor(QPalette::Active, QPalette::Highlight, QStringLiteral("#44941C")); + palette.setColor(QPalette::Inactive, QPalette::Highlight, QStringLiteral("#4A812E")); + palette.setColor(QPalette::Disabled, QPalette::Highlight, QStringLiteral("#597E46")); + + palette.setColor(QPalette::Active, QPalette::HighlightedText, QStringLiteral("#E0E3E0")); + palette.setColor(QPalette::Inactive, QPalette::HighlightedText, QStringLiteral("#D6DBD6")); + palette.setColor(QPalette::Disabled, QPalette::HighlightedText, QStringLiteral("#B2B9B2")); + + palette.setColor(QPalette::All, QPalette::Link, QStringLiteral("#368E0B")); + palette.setColor(QPalette::All, QPalette::LinkVisited, QStringLiteral("#346B19")); +} + +QString LightStyle::getStylesheetPath() const +{ + return QStringLiteral(":/styles/light/lightstyle.qss"); +} diff --git a/src/gui/styles/light/LightStyle.h b/src/gui/styles/light/LightStyle.h new file mode 100644 index 0000000000..46e782da44 --- /dev/null +++ b/src/gui/styles/light/LightStyle.h @@ -0,0 +1,40 @@ +/* + * 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: + LightStyle(); + explicit LightStyle(QProxyStyle* style); + + using BaseStyle::polish; + void polish(QPalette& palette) override; + +protected: + QString getStylesheetPath() 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 0000000000..9c406e8de6 --- /dev/null +++ b/src/gui/styles/light/lightstyle.qss @@ -0,0 +1,3 @@ +QTabBar::tab:selected { + border-top-style: solid; +} diff --git a/src/gui/styles/phantomstyle/phantomcolor.cpp b/src/gui/styles/phantomstyle/phantomcolor.cpp new file mode 100644 index 0000000000..d434047476 --- /dev/null +++ b/src/gui/styles/phantomstyle/phantomcolor.cpp @@ -0,0 +1,433 @@ +/* + * 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. + */ + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" + +#include "phantomcolor.h" +#include +#include +#include + +namespace Phantom { +namespace { +// clang-format off + +// These 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 */ +static const Triplet m[3] = { + { 3.24096994190452134377, -1.53738317757009345794, -0.49861076029300328366 }, + { -0.96924363628087982613, 1.87596750150772066772, 0.04155505740717561247 }, + { 0.05563007969699360846, -0.20397695888897656435, 1.05697151424287856072 } +}; + +/* for XYZ */ +static const Triplet m_inv[3] = { + { 0.41239079926595948129, 0.35758433938387796373, 0.18048078840183428751 }, + { 0.21263900587151035754, 0.71516867876775592746, 0.07219231536073371500 }, + { 0.01933081871559185069, 0.11919477979462598791, 0.95053215224966058086 } +}; + +static const double ref_u = 0.19783000664283680764; +static const double ref_v = 0.46831999493879100370; + +static const double kappa = 903.29629629629629629630; +static const double epsilon = 0.00885645167903563082; + + +typedef struct Bounds_tag Bounds; +struct Bounds_tag { + double a; + double b; +}; + + +static 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; + } + } +} + +static double +ray_length_until_intersect(double theta, const Bounds* line) +{ + return line->b / (sin(theta) - line->a * cos(theta)); +} + +static 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; +} + +static double +dot_product(const Triplet* t1, const Triplet* t2) +{ + return (t1->a * t2->a + t1->b * t2->b + t1->c * t2->c); +} + +static 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; +} + +static 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. + */ +static double +y2l(double y) +{ + if(y <= epsilon) + return y * kappa; + else + return 116.0 * cbrt(y) - 16.0; +} + +static double +l2y(double l) +{ + if(l <= 8.0) { + return l / kappa; + } else { + double x = (l + 16.0) / 116.0; + return (x * x * x); + } +} + +static 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; + } +} + +static 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; +} + +static 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; +} + +static 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; +} + +static 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; +} + +static 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; +} + +// clang-format on +} // 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((qreal)color.red() / 255.0); + a.g = linear_of_srgb((qreal)color.green() / 255.0); + a.b = linear_of_srgb((qreal)color.blue() / 255.0); + return a; +} +Hsl hsl_of_rgb(qreal r, qreal g, qreal b) { + double h, s, l; + rgb2hsluv((double)r, (double)g, (double)b, &h, &s, &l); + s /= 100.0; + l /= 100.0; + return Hsl((qreal)h, (qreal)s, (qreal)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 Rgb((qreal)r, (qreal)g, (qreal)b); +} +QColor qcolor_of_rgb(qreal r, qreal g, qreal b) { + int r_ = (int)std::lround(srgb_of_linear(r) * 255.0); + int g_ = (int)std::lround(srgb_of_linear(g) * 255.0); + int b_ = (int)std::lround(srgb_of_linear(b) * 255.0); + return QColor(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 + +#pragma GCC diagnostic pop diff --git a/src/gui/styles/phantomstyle/phantomcolor.h b/src/gui/styles/phantomstyle/phantomcolor.h new file mode 100644 index 0000000000..e6e401db48 --- /dev/null +++ b/src/gui/styles/phantomstyle/phantomcolor.h @@ -0,0 +1,111 @@ +/* + * 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/phantomstyle/phantomstyle.cpp b/src/gui/styles/phantomstyle/phantomstyle.cpp new file mode 100644 index 0000000000..c9fec813d1 --- /dev/null +++ b/src/gui/styles/phantomstyle/phantomstyle.cpp @@ -0,0 +1,5262 @@ +/* + * Phantom Style + * Copyright (C) 2019 Andrew Richards + * https://github.com/randrew/phantomstyle + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" + +#include "phantomstyle.h" +#include "phantomcolor.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if QT_CONFIG(combobox) +#include +#endif +#if QT_CONFIG(pushbutton) +#include +#endif +#if QT_CONFIG(abstractbutton) +#include +#endif +#if QT_CONFIG(mainwindow) +#include +#endif +#if QT_CONFIG(groupbox) +#include +#endif +#if QT_CONFIG(scrollbar) +#include +#endif +#if QT_CONFIG(spinbox) +#include +#endif +#if QT_CONFIG(abstractslider) +#include +#endif +#if QT_CONFIG(slider) +#include +#endif +#if QT_CONFIG(splitter) +#include +#endif +#if QT_CONFIG(progressbar) +#include +#endif +#if QT_CONFIG(wizard) +#include +#endif +#if QT_CONFIG(scrollbar) +#include +#endif +#if QT_CONFIG(menu) +#include +#endif +#if QT_CONFIG(toolbar) +#include +#endif +#if QT_CONFIG(toolbutton) +#include +#endif +#if QT_CONFIG(dialogbuttonbox) +#include +#endif +#if QT_CONFIG(itemviews) +#include +#include +#include +#include +#include +#endif + +#ifdef BUILD_WITH_EASY_PROFILER +#include +#include +#include +#endif + +QT_BEGIN_NAMESPACE +Q_GUI_EXPORT int qt_defaultDpiX(); +QT_END_NAMESPACE + +namespace Phantom { +namespace Tweak { +const char* const menubar_no_ruler = "_phantom_menubar_no_ruler"; +} +namespace { +Q_NEVER_INLINE bool hasTweakTrue(const QObject* object, const char* tweakName) { + if (!object) + return false; + QVariant value = object->property(tweakName); + return value.toBool(); +} +} // namespace +} // namespace Phantom + +namespace Phantom { +namespace { +enum { + MenuMinimumWidth = 10, // Smallest width that menu items can have + SplitterMaxLength = 25, // Length of splitter handle (not thickness) + SpinBox_ButtonWidth = 14, + + // These two are currently not based on font, but could be + LineEdit_ContentsHPad = 4, + ComboBox_NonEditable_ContentsHPad = 4, +}; + +static const qreal TabBarTab_Rounding = 2.0; +static const qreal SpinBox_Rounding = 2.0; +static const qreal LineEdit_Rounding = 2.0; +static const qreal FrameFocusRect_Rounding = 1.0; +static const qreal PushButton_Rounding = 2.0; +static const qreal ToolButton_Rounding = 1.25; +static const qreal ProgressBar_Rounding = 2.0; +static const qreal GroupBox_Rounding = 2.0; +static const qreal SliderGroove_Rounding = 2.0; +static const qreal SliderHandle_Rounding = 0.0; + +static const qreal CheckMark_WidthOfHeightScale = 0.8; +static const qreal PushButton_HorizontalPaddingFontHeightRatio = 1.0; +static const qreal TabBar_HPaddingFontRatio = 1.25; +static const qreal TabBar_VPaddingFontRatio = 1.0; +static const qreal GroupBox_LabelBottomMarginFontRatio = 1.0 / 4.0; +static const qreal ComboBox_ArrowMarginRatio = 1.0 / 3.25; + +static const qreal MenuBar_HorizontalPaddingFontRatio = 1.0 / 2.0; +static const qreal MenuBar_VerticalPaddingFontRatio = 1.0 / 6.0; + +static const qreal MenuItem_LeftMarginFontRatio = 1.0 / 2.0; +static const qreal MenuItem_RightMarginForTextFontRatio = 1.0 / 1.5; +static const qreal MenuItem_RightMarginForArrowFontRatio = 1.0 / 4.0; +static const 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). +static const qreal MenuItem_CheckRightSpaceFontRatio = 1.0 / 2.0; +static const qreal MenuItem_TextMnemonicSpaceFontRatio = 1.5; +static const qreal MenuItem_SubMenuArrowSpaceFontRatio = 1.0 / 1.5; +static const qreal MenuItem_SubMenuArrowWidthFontRatio = 1.0 / 2.75; +static const qreal MenuItem_SeparatorHeightFontRatio = 1.0 / 1.5; +static const qreal MenuItem_CheckMarkVerticalInsetFontRatio = 1.0 / 5.0; +static const qreal MenuItem_IconRightSpaceFontRatio = 1.0 / 3.0; + +static const bool BranchesOnEdge = false; +static const bool OverhangShadows = false; +static const bool IndicatorShadows = false; +static const bool ScrollbarShadows = true; +static const bool MenuExtraBottomMargin = true; +static const bool MenuBarLeftMargin = false; +static const bool AllowToolBarAutoRaise = true; +// Note that this only applies to the disclosure etc. decorators in tree views. +static const bool ShowItemViewDecorationSelected = false; +static const bool UseQMenuForComboBoxPopup = true; +static const 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. +static 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.05); +} +QColor dividerColor(const QColor& underlying) { + return adjustLightness(underlying, -0.05); +} +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.03); +} +QColor lightShadeOf(const QColor& underlying) { + return adjustLightness(underlying, 0.07); +} +QColor darkShadeOf(const QColor& underlying) { + return adjustLightness(underlying, -0.07); +} +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.03); +} +QColor pressedOf(const QColor& color) { return adjustLightness(color, -0.02); } +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_button_specular, + S_button_pressed, + S_button_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. + Q_ALWAYS_INLINE const QColor& color(Swatchy swatchValue) const { + Q_ASSERT((int)swatchValue >= 0 && (int)swatchValue < Num_SwatchColors); + return brushes[swatchValue].color(); + } + Q_ALWAYS_INLINE const QBrush& brush(Swatchy swatchValue) const { + Q_ASSERT((int)swatchValue >= 0 && (int)swatchValue < Num_SwatchColors); + return brushes[swatchValue]; + } + Q_ALWAYS_INLINE const QPen& pen(Swatchy swatchValue) const { + Q_ASSERT((int)swatchValue >= 0 && (int)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; + 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] = Dc::gutterColorOf(pal); + + colors[S_window_outline] = Dc::adjustLightness(colors[S_window], -0.1); + colors[S_window_specular] = Dc::specularOf(colors[S_window]); + colors[S_window_divider] = Dc::dividerColor(colors[S_window]); + colors[S_window_lighter] = Dc::lightShadeOf(colors[S_window]); + colors[S_window_darker] = Dc::darkShadeOf(colors[S_window]); + colors[S_button_specular] = Dc::specularOf(colors[S_button]); + colors[S_button_pressed] = Dc::pressedOf(colors[S_button]); + colors[S_button_pressed_specular] = Dc::specularOf(colors[S_button_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] = Dc::adjustLightness(colors[S_highlight], -0.05); + 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((qreal)i / (qreal)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.) +Q_ALWAYS_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 ^ ((quint64)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 (int i = 0; i < (int)(sizeof(roles) / sizeof(roles[0])); ++i) { + h = c(h, p.color(roles[i]).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 PhantomStylePrivate { +public: + PhantomStylePrivate(); + + // 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; +}; + +static const qreal Pi = qreal(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 + static const qreal scale = qreal(qt_defaultDpiX()) / 96.0; + return value * scale; +#endif +} + +#if QT_CONFIG(menu) +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 = (int)dpiScaled(1.0); + m.leftMargin = (int)((qreal)fontHeight * MenuItem_LeftMarginFontRatio); + m.rightMarginForText = + (int)((qreal)fontHeight * MenuItem_RightMarginForTextFontRatio); + m.rightMarginForArrow = + (int)((qreal)fontHeight * MenuItem_RightMarginForArrowFontRatio); + m.topMargin = (int)((qreal)fontHeight * MenuItem_VerticalMarginsFontRatio); + m.bottomMargin = (int)((qreal)fontHeight * MenuItem_VerticalMarginsFontRatio); + int checkVMargin = + (int)((qreal)fontHeight * MenuItem_CheckMarkVerticalInsetFontRatio); + int checkHeight = fontHeight - checkVMargin * 2; + if (checkHeight < 0) + checkHeight = 0; + m.checkWidth = (int)((qreal)checkHeight * CheckMark_WidthOfHeightScale); + m.checkRightSpace = + (int)((qreal)fontHeight * MenuItem_CheckRightSpaceFontRatio); + m.iconRightSpace = + (int)((qreal)fontHeight * MenuItem_IconRightSpaceFontRatio); + m.mnemonicSpace = + (int)((qreal)fontHeight * MenuItem_TextMnemonicSpaceFontRatio); + m.arrowSpace = (int)((qreal)fontHeight * MenuItem_SubMenuArrowSpaceFontRatio); + m.arrowWidth = (int)((qreal)fontHeight * MenuItem_SubMenuArrowWidthFontRatio); + m.separatorHeight = + (int)((qreal)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 = (int)((qreal)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); +} +#endif + +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 : (int)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; +} + +#if QT_CONFIG(dial) +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 = (qreal)dial->rect.width(); + qreal height = (qreal)dial->rect.height(); + qreal r = qMin(width, height) / 2.0; + int bigLineSize = calcBigLineSize(int(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 = (qreal)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, qreal(0.70)); + const qreal ds = r / qreal(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(); +} +#endif //QT_CONFIG(dial) + +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, (int)(option->palette.button().color().saturation())), + qMin(255, (int)(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); +} +Q_ALWAYS_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); +} +Q_ALWAYS_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; +} +Q_ALWAYS_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((qreal)rect.x() + 0.5, (qreal)rect.y() + 0.5, + (qreal)rect.width() - 1.0, (qreal)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 + + +PhantomStylePrivate::PhantomStylePrivate() : headSwatchFastKey(0) {} + +PhantomStyle::PhantomStyle() : d(new PhantomStylePrivate) { + setObjectName(QLatin1String("Phantom")); +} + +PhantomStyle::~PhantomStyle() { 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 PhantomStyle::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 PhantomStyle::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 ((int)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_window_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_window_divider, 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; + } +#if QT_CONFIG(itemviews) + // 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; + } +#endif +#if QT_CONFIG(tabbar) + 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_window_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; + } +#endif // QT_CONFIG(tabbar) + 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 = (int)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; +#if QT_CONFIG(toolbutton) + // 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 && + !(bool)qstyleoption_cast(option) && + widget) { + auto tbutton = qobject_cast(widget); + if (tbutton && tbutton->popupMode() != QToolButton::InstantPopup && + tbutton->defaultAction()) { + int dim = (int)((qreal)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; + } + } + } +#endif + 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(0, -1); + 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) { + // kinda repurposing this, hmm + fill = S_scrollbarGutter; + 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.setFlag(State_Horizontal, !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 = (int)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 QT_CONFIG(itemviews) + 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. +#endif + 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)); +#if QT_CONFIG(tabwidget) + auto twf = qstyleoption_cast(option); + if (!twf) + break; + Ph::fillRectOutline(painter, option->rect, 1, + swatch.color(S_window_outline)); + Ph::fillRectOutline(painter, bgRect, 1, swatch.color(S_tabFrame_specular)); +#endif // QT_CONFIG(tabwidget) + 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 = (int)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 ((int)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 PhantomStyle::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; + } +#if QT_CONFIG(splitter) + 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 = (int)Ph::dpiScaled(Ph::SplitterMaxLength); + int thickness = (int)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; + } +#endif // QT_CONFIG(splitter) +#if QT_CONFIG(rubberband) + // 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; + } +#endif //QT_CONFIG(rubberband) + 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; + } +#if QT_CONFIG(toolbar) + 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; + } +#endif // QT_CONFIG(toolbar) + 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.transposed(); + 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 QT_CONFIG(itemviews) + 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; + } + } + } +#endif + + 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_window_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 = (int)((qreal)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(), + (int)(aligned.width() * pixmap.devicePixelRatio()), + (int)(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; +#if QT_CONFIG(itemviews) + // 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; + } + } +#endif + 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 +#if QT_CONFIG(animation) + // old code started or stepped animation here +#endif + } else { +#if QT_CONFIG(animation) + // old code stopped animation here +#endif + } + } + 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; + } +#if QT_CONFIG(menubar) + 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 (isSelected) + break; + if (Phantom::hasTweakTrue(widget, Phantom::Tweak::menubar_no_ruler)) + break; + if (!isSelected) { + Ph::fillRectEdges(painter, r, Qt::BottomEdge, 1, + swatch.color(S_window_divider)); + } + break; + } +#endif +#if QT_CONFIG(menu) + 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 = (qreal)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 QT_CONFIG(combobox) + if (auto combo = qobject_cast(widget)) { + iconSize = combo->iconSize(); + } +#endif + QWindow* window = widget ? widget->windowHandle() : nullptr; + QPixmap pixmap = menuItem->icon.pixmap(window, iconSize, mode, state); + const int pixw = (int)(pixmap.width() / pixmap.devicePixelRatio()); + const int pixh = (int)(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 QT_CONFIG(combobox) && 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; + } +#endif // QT_CONFIG(menu) + 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 = + (int)((qreal)pixmap.width() / pixmap.devicePixelRatio()); + int pixmapHeight = + (int)((qreal)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: { + if (Phantom::hasTweakTrue(widget, Phantom::Tweak::menubar_no_ruler)) + break; + QRect rect = option->rect; + 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; + } +#if QT_CONFIG(tabbar) + 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 * (int)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; + } + } + Ph::paintBorderedRoundRect(painter, drawRect, rounding, swatch, + S_window_outline, thisFillColor); + Ph::paintBorderedRoundRect(painter, drawRect.adjusted(1, 1, -1, -1), + rounding, swatch, specular, S_none); + painter->restore(); + if (isSelected) { + 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; + } +#endif //QT_CONFIG(tabbar) +#if QT_CONFIG(itemviews) + 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; + } +#endif + 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(1); + painter->fillRect(r, swatch.color(S_window_outline)); + break; + } else if (frameopt->frameShape == QFrame::VLine) { + QRect r = option->rect; + r.setX(r.x() + r.width() / 2); + r.setWidth(1); + painter->fillRect(r, swatch.color(S_window_outline)); + break; + } + } + QCommonStyle::drawControl(element, option, painter, widget); + break; + } + default: + QCommonStyle::drawControl(element, option, painter, widget); + break; + } +} + +QPalette PhantomStyle::standardPalette() const { + return QCommonStyle::standardPalette(); +} + +void PhantomStyle::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)); + int alignment = int(groupBox->textAlignment); + if (!proxy()->styleHint(QStyle::SH_UnderlineShortcut, option, widget)) + alignment |= Qt::TextHideMnemonic; + + proxy()->drawItemText(painter, textRect, + // int cast to suppress clang analyzer warning + (int)Qt::TextShowMnemonic | (int)Qt::AlignLeft | + alignment, + 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; + } +#if QT_CONFIG(spinbox) + 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; + } +#endif // QT_CONFIG(spinbox) + 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; + } +#if QT_CONFIG(slider) + case CC_ScrollBar: { + auto scrollBar = qstyleoption_cast(option); + if (!scrollBar) + break; + bool isHorizontal = scrollBar->orientation == Qt::Horizontal; + bool isLeftToRight = option->direction != Qt::RightToLeft; + auto pr = proxy(); + bool isSunken = scrollBar->state & State_Sunken; + 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); + + bool scrollBarGrooveShown = scrollBar->subControls & SC_ScrollBarGroove; + bool isEnabled = scrollBar->state & State_Enabled; + bool hasRange = scrollBar->minimum != scrollBar->maximum; + + // Groove/gutter/trench area + if (scrollBarGrooveShown) { + QRect r = scrollBarGroove; + Qt::Edges edges; + if (isHorizontal) { + edges = Qt::TopEdge; + r.setY(r.y() + 1); + } else { + if (isLeftToRight) { + edges = Qt::LeftEdge; + r.setX(r.x() + 1); + } else { + edges = Qt::RightEdge; + r.setWidth(r.width() - 1); + } + } + Swatchy grooveColor = + isEnabled ? S_scrollbarGutter : S_scrollbarGutter_disabled; + // Top or left dark edge + Ph::fillRectEdges(painter, scrollBarGroove, edges, 1, + swatch.color(S_window_outline)); + // Ring shadow + if (Ph::ScrollbarShadows && isEnabled) { + for (int i = 0; i < Ph::Num_ShadowSteps; ++i) { + Ph::fillRectOutline(painter, r, 1, swatch.scrollbarShadowColors[i]); + r.adjust(1, 1, -1, -1); + } + } + // General BG fill + painter->fillRect(r, swatch.color(grooveColor)); + + // Bonus fun: also draw a shadow cast by the scrollbar slider + if (Ph::ScrollbarShadows && scrollBar->subControls & SC_ScrollBarSlider && + isEnabled && hasRange) { + if (isHorizontal) { + r = scrollBarSlider; + int leftwardEnd = scrollBarGroove.left() + 1; + int availWidth = r.left() - leftwardEnd; + int steps = qMin(availWidth / 2, (int)Ph::Num_ShadowSteps); + if (steps < 0) + steps = 0; + r.adjust(-2, 2, 0, -1); + r.setWidth(1); + for (int i = 0; i < steps; ++i) { + painter->fillRect(r, swatch.scrollbarShadowColors[i]); + r.adjust(-1, 1, -1, -1); + } + r = scrollBarSlider; + int rightwardEnd = + scrollBarGroove.left() + scrollBarGroove.width() - 2; + availWidth = rightwardEnd - r.right(); + steps = qMin(availWidth / 2, (int)Ph::Num_ShadowSteps); + if (steps < 0) + steps = 0; + r.moveLeft(r.right() + 1); + r.adjust(1, 2, 0, -1); + r.setWidth(1); + for (int i = 0; i < steps; ++i) { + painter->fillRect(r, swatch.scrollbarShadowColors[i]); + r.adjust(1, 1, 1, -1); + } + } else { + r = scrollBarSlider; + int topEnd = scrollBarGroove.top() + 1; + int availWidth = r.top() - topEnd; + int steps = qMin(availWidth / 2, (int)Ph::Num_ShadowSteps); + if (steps < 0) + steps = 0; + r.adjust(2, -2, -1, 0); + r.setHeight(1); + if (!isLeftToRight) + r.translate(-1, 0); + for (int i = 0; i < steps; ++i) { + painter->fillRect(r, swatch.scrollbarShadowColors[i]); + r.adjust(1, -1, -1, -1); + } + r = scrollBarSlider; + int botEnd = scrollBarGroove.bottom() - 1; + availWidth = botEnd - r.bottom(); + steps = qMin(availWidth / 2, (int)Ph::Num_ShadowSteps); + if (steps < 0) + steps = 0; + r.setTop(r.bottom() + 1); + r.setHeight(1); + r.adjust(2, 2, -1, 0); + if (!isLeftToRight) + r.translate(-1, 0); + for (int i = 0; i < steps; ++i) { + painter->fillRect(r, swatch.scrollbarShadowColors[i]); + r.adjust(1, 1, -1, 1); + } + } + } + } + + // Slider thumb + if (scrollBar->subControls & SC_ScrollBarSlider) { + Swatchy thumbFill, thumbSpecular; + if (isSunken && scrollBar->activeSubControls & SC_ScrollBarSlider) { + thumbFill = S_button_pressed; + thumbSpecular = S_button_pressed_specular; + } else if (hasRange) { + thumbFill = S_button; + thumbSpecular = S_button_specular; + } else { + thumbFill = S_window; + thumbSpecular = S_none; + } + Qt::Edges edges; + QRect edgeRect = scrollBarSlider; + QRect mainRect = scrollBarSlider; + if (isHorizontal) { + edges = Qt::LeftEdge | Qt::TopEdge | Qt::RightEdge; + edgeRect.adjust(-1, 0, 1, 0); + mainRect.setY(mainRect.y() + 1); + } else { + 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)); + if (thumbSpecular) { + Ph::fillRectOutline(painter, mainRect, 1, swatch.color(thumbSpecular)); + } + } + + // The SubLine (up/left) buttons + if (scrollBar->subControls & SC_ScrollBarSubLine) { + Swatchy fill, specular; + if (isSunken && scrollBar->activeSubControls & SC_ScrollBarSubLine) { + fill = S_button_pressed; + specular = S_button_pressed_specular; + } else if (hasRange) { + fill = S_button; + specular = S_button_specular; + } else { + fill = S_window; + specular = S_none; + } + + QRect btnRect = scrollBarSubLine; + QRect bgRect = btnRect; + Qt::Edges edges; + if (isHorizontal) { + if (isLeftToRight) { + edges = Qt::TopEdge | Qt::RightEdge; + bgRect.adjust(0, 1, -1, 0); + } else { + edges = Qt::LeftEdge | Qt::TopEdge; + bgRect.adjust(1, 1, 0, 0); + } + } else { + if (isLeftToRight) { + edges = Qt::LeftEdge | Qt::BottomEdge; + bgRect.adjust(1, 0, 0, -1); + } else { + edges = Qt::RightEdge | Qt::BottomEdge; + bgRect.adjust(0, 0, -1, -1); + } + } + // Outline, fill, specular + Ph::fillRectEdges(painter, btnRect, edges, 1, + swatch.color(S_window_outline)); + painter->fillRect(bgRect, swatch.color(fill)); + if (specular) { + Ph::fillRectOutline(painter, bgRect, 1, swatch.color(specular)); + } + + // Arrows + Qt::ArrowType arrowType; + if (isHorizontal) { + arrowType = isLeftToRight ? Qt::LeftArrow : Qt::RightArrow; + } else { + arrowType = Qt::UpArrow; + } + int adj = qMin(bgRect.width(), bgRect.height()) / 4; + Ph::drawArrow(painter, bgRect.adjusted(adj, adj, -adj, -adj), arrowType, + swatch, hasRange); + } + + // The AddLine (down/right) button + if (scrollBar->subControls & SC_ScrollBarAddLine) { + Swatchy fill, specular; + if (isSunken && scrollBar->activeSubControls & SC_ScrollBarAddLine) { + fill = S_button_pressed; + specular = S_button_pressed_specular; + } else if (hasRange) { + fill = S_button; + specular = S_button_specular; + } else { + fill = S_window; + specular = S_none; + } + QRect btnRect = scrollBarAddLine; + QRect bgRect = btnRect; + Qt::Edges edges; + if (isLeftToRight) { + edges = Qt::LeftEdge | Qt::TopEdge; + bgRect.adjust(1, 1, 0, 0); + } else { + edges = Qt::TopEdge | Qt::RightEdge; + bgRect.adjust(0, 1, -1, 0); + } + // Outline, fill, specular + Ph::fillRectEdges(painter, btnRect, edges, 1, + swatch.color(S_window_outline)); + painter->fillRect(bgRect, swatch.color(fill)); + if (specular) { + Ph::fillRectOutline(painter, bgRect, 1, swatch.color(specular)); + } + + // Arrows + Qt::ArrowType arrowType; + if (isHorizontal) { + arrowType = isLeftToRight ? Qt::RightArrow : Qt::LeftArrow; + } else { + arrowType = Qt::DownArrow; + } + int adj = qMin(bgRect.width(), bgRect.height()) / 4; + Ph::drawArrow(painter, bgRect.adjusted(adj, adj, -adj, -adj), arrowType, + swatch, hasRange); + } + break; + } +#endif // QT_CONFIG(slider) + 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 = + (int)((qreal)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; + } +#if QT_CONFIG(slider) + 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_button_pressed; + handleSpecular = S_button_pressed_specular; + } else { + handleFill = S_button; + handleSpecular = S_button_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; + } +#endif // QT_CONFIG(slider) +#if QT_CONFIG(toolbutton) + 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; + } +#endif +#if QT_CONFIG(dial) + case CC_Dial: + if (auto dial = qstyleoption_cast(option)) + Ph::drawDial(dial, painter); + break; +#endif + default: + QCommonStyle::drawComplexControl(control, option, painter, widget); + break; + } +} + +int PhantomStyle::pixelMetric(PixelMetric metric, const QStyleOption* option, + const QWidget* widget) const { + int val = -1; + switch (metric) { + case PM_SliderTickmarkOffset: + val = 4; + break; + case PM_HeaderMargin: + case PM_ToolTipLabelFrameWidth: + val = 2; + break; + case PM_ButtonMargin: + val = 6; + break; + case PM_ComboBoxFrameWidth: + val = 1; + break; + case PM_ButtonDefaultIndicator: + case PM_ButtonShiftHorizontal: + val = 0; + break; + case PM_ButtonShiftVertical: +#if QT_CONFIG(toolbutton) + if (qobject_cast(widget)) { + return 0; + } +#endif + val = 1; + break; + 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. + return 1; + case PM_SpinBoxFrameWidth: + return 1; + case PM_MessageBoxIconSize: + val = 48; + break; + case PM_DialogButtonsSeparator: + case PM_ScrollBarSliderMin: + val = 26; + break; + case PM_TitleBarHeight: + val = 24; + break; + case PM_ScrollBarExtent: + // Classic Mac would have an extent of 15 (14 visible clickable, need +1 in + // Qt for frame), so we're 1px thinner. + val = 14; + 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 (int)((qreal)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 = (int)Phantom::dpiScaled(2); + return dotLen * (3 * 2 - 1); + } + case PM_ToolBarItemSpacing: + val = 1; + break; + case PM_ToolBarFrameWidth: + val = 0; + break; + case PM_ToolBarItemMargin: + val = 1; + break; + case PM_ListViewIconSize: + case PM_SmallIconSize: +#if QT_CONFIG(itemviews) + 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(); + } +#endif + 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; + case PM_TitleBarButtonSize: + val = 19; + break; + 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 (int)((qreal)option->fontMetrics.height() * + Phantom::TabBar_HPaddingFontRatio) + + (int)Phantom::dpiScaled(4); + case PM_TabBarTabVSpace: + if (!option) + break; + return (int)((qreal)option->fontMetrics.height() * + Phantom::TabBar_VPaddingFontRatio) + + (int)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_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_ScrollBarSpacing: + val = 0; + break; + case PM_ScrollView_ScrollBarOverlap: + val = 0; + break; + case PM_TreeViewIndentation: { + if (widget) + return widget->fontMetrics().height(); + val = 12; + break; + } + default: + return QCommonStyle::pixelMetric(metric, option, widget); + } + return (int)Phantom::dpiScaled(val); +} + +QSize PhantomStyle::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)); + } +#if QT_CONFIG(menubar) + case CT_MenuBarItem: { + int fontHeight = option ? option->fontMetrics.height() : size.height(); + int w = (int)((qreal)fontHeight * Ph::MenuBar_HorizontalPaddingFontRatio); + int h = (int)((qreal)fontHeight * Ph::MenuBar_VerticalPaddingFontRatio); + int line = (int)Ph::dpiScaled(1); + return QSize(size.width() + w * 2, size.height() + h * 2 + line); + } +#endif +#if QT_CONFIG(menu) + 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 QT_CONFIG(combobox) + if (Ph::UseQMenuForComboBoxPopup && + qobject_cast(widget)) { + if (!widget->testAttribute(Qt::WA_SetFont)) + fontMetricsHeight = QFontMetrics(qApp->font("QMenu")).height(); + } +#endif + 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 QT_CONFIG(combobox) + if (auto combo = qobject_cast(widget)) { + h = qMax(combo->iconSize().height() + 2, h); + } +#endif // QT_CONFIG(combobox) + } + QSize sz; + sz.setWidth(qMax(w, (int)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 = + (int)((qreal)fheight * Ph::MenuItem_SeparatorHeightFontRatio) / 2; + QSize sz = size; + sz.setHeight(sz.height() + vmargin); + return sz; + } +#endif // QT_CONFIG(menu) +#if QT_CONFIG(tabbar) + case CT_TabBarTab: { + // Placeholder in case we change this in the future + return size; + } +#endif +#if QT_CONFIG(groupbox) + 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 += (int)((qreal)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); + } +#endif +#if QT_CONFIG(itemviews) + case CT_ItemViewItem: { + auto vopt = qstyleoption_cast(option); + if (!vopt) + break; + QSize sz = QCommonStyle::sizeFromContents(type, option, size, widget); + // 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); + } + } +#else +#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; + } +#endif + 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 = (int)((qreal)pbopt->fontMetrics.height() * + Phantom::PushButton_HorizontalPaddingFontHeightRatio); + newSize.rwidth() += hpad * 2; +#if QT_CONFIG(dialogbuttonbox) + if (widget && qobject_cast(widget->parent())) { + int dialogButtonMinWidth = (int)Phantom::dpiScaled(80); + newSize.rwidth() = qMax(newSize.width(), dialogButtonMinWidth); + } +#endif + break; + } + case CT_ToolButton: + newSize += QSize(2, 2); + break; + case CT_ComboBox: { + newSize += QSize(0, 3); +#if QT_CONFIG(combobox) + 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 = (int)Ph::dpiScaled(Ph::LineEdit_ContentsHPad); + newSize += QSize(0, 4); + } else { + pad = (int)Ph::dpiScaled(Ph::ComboBox_NonEditable_ContentsHPad); + } + newSize.rwidth() += pad * 2; + } +#endif + break; + } + case CT_LineEdit: { + newSize += QSize(0, 3); + int pad = (int)Ph::dpiScaled(Ph::LineEdit_ContentsHPad); + newSize.rwidth() += pad * 2; + break; + } + case CT_SpinBox: + // No adjustment necessary + break; + case CT_SizeGrip: + newSize += QSize(4, 4); + break; + case CT_MdiControls: + newSize -= QSize(1, 0); + break; + default: + break; + } + return newSize; +} + +void PhantomStyle::polish(QApplication* app) { QCommonStyle::polish(app); } + +void PhantomStyle::polish(QWidget* widget) { + QCommonStyle::polish(widget); + // Leaving this code here to debug/remove hover stuff if necessary +#if 0 + if (false +#if QT_CONFIG(abstractbutton) + || qobject_cast(widget) +#endif +#if QT_CONFIG(combobox) + || qobject_cast(widget) +#endif +#if QT_CONFIG(progressbar) + || qobject_cast(widget) +#endif +#if QT_CONFIG(scrollbar) + || qobject_cast(widget) +#endif +#if QT_CONFIG(splitter) + || qobject_cast(widget) +#endif +#if QT_CONFIG(abstractslider) + || qobject_cast(widget) +#endif +#if QT_CONFIG(spinbox) + || qobject_cast(widget) +#endif + || (widget->inherits("QDockSeparator")) || + (widget->inherits("QDockWidgetSeparator"))) { + widget->setAttribute(Qt::WA_Hover, true); + widget->setAttribute(Qt::WA_OpaquePaintEvent, false); + } +#endif +} + +void PhantomStyle::polish(QPalette& pal) { QCommonStyle::polish(pal); } + +void PhantomStyle::unpolish(QWidget* widget) { + QCommonStyle::unpolish(widget); + // Leaving this code here to debug/remove hover stuff if necessary +#if 0 + if (false +#if QT_CONFIG(abstractbutton) + || qobject_cast(widget) +#endif +#if QT_CONFIG(combobox) + || qobject_cast(widget) +#endif +#if QT_CONFIG(progressbar) + || qobject_cast(widget) +#endif +#if QT_CONFIG(scrollbar) + || qobject_cast(widget) +#endif +#if QT_CONFIG(splitter) + || qobject_cast(widget) +#endif +#if QT_CONFIG(abstractslider) + || qobject_cast(widget) +#endif +#if QT_CONFIG(spinbox) + || qobject_cast(widget) +#endif + || (widget->inherits("QDockSeparator")) || + (widget->inherits("QDockWidgetSeparator"))) { + widget->setAttribute(Qt::WA_Hover, false); + } +#endif +} + +void PhantomStyle::unpolish(QApplication* app) { QCommonStyle::unpolish(app); } + +QRect PhantomStyle::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) { +#if QT_CONFIG(slider) + 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 = (int)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; + } +#endif // QT_CONFIG(slider) +#if QT_CONFIG(spinbox) + 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 = (int)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 QRect(); + 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; + } +#endif // QT_CONFIG(spinbox) +#if QT_CONFIG(groupbox) + 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 += + (int)((qreal)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; + } +#endif // QT_CONFIG(groupbox) +#if QT_CONFIG(combobox) + 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 = (int)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; + } +#endif + 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 PhantomStyle::itemPixmapRect(const QRect& r, int flags, + const QPixmap& pixmap) const { + return QCommonStyle::itemPixmapRect(r, flags, pixmap); +} +void PhantomStyle::drawItemPixmap(QPainter* painter, const QRect& rect, + int alignment, const QPixmap& pixmap) const { + QCommonStyle::drawItemPixmap(painter, rect, alignment, pixmap); +} +QStyle::SubControl +PhantomStyle::hitTestComplexControl(ComplexControl cc, + const QStyleOptionComplex* opt, + const QPoint& pt, const QWidget* w) const { + return QCommonStyle::hitTestComplexControl(cc, opt, pt, w); +} +QPixmap PhantomStyle::generatedIconPixmap(QIcon::Mode iconMode, + const QPixmap& pixmap, + const QStyleOption* opt) const { + return QCommonStyle::generatedIconPixmap(iconMode, pixmap, opt); +} + +int PhantomStyle::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_ScrollBar_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: +#if QT_CONFIG(itemviews) + // 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; +#endif + return (int)Phantom::ShowItemViewDecorationSelected; + case SH_ItemView_MovementWithoutUpdatingSelection: + return 1; +#if QT_CONFIG(itemviews) + 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: + case SH_EtchDisabledText: + case SH_DitherDisabledText: + case SH_ToolBox_SelectedPageTitleBold: + case SH_ScrollView_FrameOnlyAroundContents: + case SH_Menu_AllowActiveAndDisabled: + case SH_MainWindow_SpaceBelowMenuBar: + case SH_MessageBox_CenterButtons: + case SH_RubberBand_Mask: + return 0; + case SH_ComboBox_Popup: { + if (!Phantom::UseQMenuForComboBoxPopup) + return 0; +#if QT_CONFIG(combobox) + // 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; +#endif + 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 ? (int)swatch.color(S_base_divider).rgb() : 0; + } + case SH_MessageBox_TextInteractionFlags: + return Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse; +#if QT_CONFIG(wizard) + case SH_WizardStyle: + return QWizard::ClassicStyle; +#endif + 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: { +#if defined(Q_OS_MAC) + return false; +#else + return true; +#endif + } + case SH_Widget_Animate: + return 0; + default: + break; + } + return QCommonStyle::styleHint(hint, option, widget, returnData); +} + +QRect PhantomStyle::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; + } +#if QT_CONFIG(itemviews) + 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; + } +#endif +#if QT_CONFIG(lineedit) + case SE_LineEditContents: { + QRect r = QCommonStyle::subElementRect(sr, opt, w); + int pad = (int)Phantom::dpiScaled(Phantom::LineEdit_ContentsHPad); + return r.adjusted(pad, 0, -pad, 0); + } +#endif + default: + break; + } + return QCommonStyle::subElementRect(sr, opt, w); +} + +// Projects which don't use moc can define PHANTOM_NO_MOC to skip this include. +// However, they will still need to deal with the Q_OBJECT macro in the header. +// Easiest way is to probably keep your own copy of the header without the +// macro in it. (If there's a smarter way to do this, please let me know.) +#ifndef PHANTOM_NO_MOC +#include "moc_phantomstyle.cpp" +#endif + +// 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 ] + +#pragma GCC diagnostic pop diff --git a/src/gui/styles/phantomstyle/phantomstyle.h b/src/gui/styles/phantomstyle/phantomstyle.h new file mode 100644 index 0000000000..f136695865 --- /dev/null +++ b/src/gui/styles/phantomstyle/phantomstyle.h @@ -0,0 +1,83 @@ +/* + * Phantom Style + * Copyright (C) 2019 Andrew Richards + * https://github.com/randrew/phantomstyle + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef PHANTOMSTYLE_H +#define PHANTOMSTYLE_H +#include + +class PhantomStylePrivate; +class PhantomStyle : public QCommonStyle { +Q_OBJECT +public: + PhantomStyle(); + ~PhantomStyle(); + + 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; + void polish(QWidget* widget) override; + void polish(QApplication* app) override; + void polish(QPalette& pal) override; + void unpolish(QWidget* widget) override; + void unpolish(QApplication* app) override; + +protected: + PhantomStylePrivate* d; +}; +#endif diff --git a/src/gui/styles/styles.qrc b/src/gui/styles/styles.qrc new file mode 100644 index 0000000000..c8e9057dc9 --- /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 6f84ef8b16..dc002d7027 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,7 +26,8 @@ #include "core/Config.h" #include "core/Tools.h" #include "crypto/Crypto.h" -#include "gui/styles/dark/KpxcDarkStyle.h" +#include "gui/styles/dark/DarkStyle.h" +#include "gui/styles/light/LightStyle.h" #include "gui/Application.h" #include "gui/MainWindow.h" #include "gui/MessageBox.h" @@ -63,7 +64,8 @@ int main(int argc, char** argv) Application app(argc, argv); Application::setApplicationName("KeePassXC"); Application::setApplicationVersion(KEEPASSXC_VERSION); - QApplication::setStyle(new KpxcDarkStyle); +// QApplication::setStyle(new LightStyle); + QApplication::setStyle(new DarkStyle); // don't set organizationName as that changes the return value of // QStandardPaths::writableLocation(QDesktopServices::DataLocation) Bootstrap::bootstrapApplication();