From bcabaa61492f3dc7bfcd3564cd00c0bc47d765a5 Mon Sep 17 00:00:00 2001 From: cutefishd Date: Tue, 16 Mar 2021 14:43:20 +0800 Subject: [PATCH] init --- .gitignore | 55 + CMakeLists.txt | 8 + README.md | 21 + cmake/FindQt5PlatformSupport.cmake | 121 + cmake/FindQt5ThemeSupport.cmake | 122 + platformtheme/CMakeLists.txt | 93 + platformtheme/cutefish-platformtheme.json | 3 + platformtheme/hintsettings.cpp | 126 + platformtheme/hintsettings.h | 57 + platformtheme/main.cpp | 32 + .../org.kde.StatusNotifierWatcher.xml | 42 + platformtheme/platformtheme.cpp | 202 + platformtheme/platformtheme.h | 48 + platformtheme/qdbusmenubar.cpp | 156 + platformtheme/qdbusmenubar_p.h | 101 + platformtheme/statusnotifier/dbustypes.cpp | 47 + platformtheme/statusnotifier/dbustypes.h | 32 + .../org.kde.StatusNotifierItem.xml | 69 + .../statusnotifier/statusnotifieritem.cpp | 313 + .../statusnotifier/statusnotifieritem.h | 163 + platformtheme/systemtrayicon.cpp | 355 ++ platformtheme/systemtrayicon.h | 124 + platformtheme/x11integration.cpp | 135 + platformtheme/x11integration.h | 47 + widgetstyle/CMakeLists.txt | 48 + widgetstyle/basestyle.cpp | 5094 +++++++++++++++++ widgetstyle/basestyle.h | 114 + widgetstyle/blurhelper.cpp | 105 + widgetstyle/blurhelper.h | 57 + widgetstyle/boxshadowrenderer.cpp | 348 ++ widgetstyle/boxshadowrenderer.h | 97 + widgetstyle/cutefishstyle.json | 4 + widgetstyle/phantomcolor.cpp | 423 ++ widgetstyle/phantomcolor.h | 165 + widgetstyle/pstyleplugin.cpp | 20 + widgetstyle/pstyleplugin.h | 18 + widgetstyle/shadowhelper.cpp | 414 ++ widgetstyle/shadowhelper.h | 148 + widgetstyle/tileset.cpp | 183 + widgetstyle/tileset.h | 121 + 40 files changed, 9831 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 cmake/FindQt5PlatformSupport.cmake create mode 100644 cmake/FindQt5ThemeSupport.cmake create mode 100644 platformtheme/CMakeLists.txt create mode 100644 platformtheme/cutefish-platformtheme.json create mode 100644 platformtheme/hintsettings.cpp create mode 100644 platformtheme/hintsettings.h create mode 100644 platformtheme/main.cpp create mode 100644 platformtheme/org.kde.StatusNotifierWatcher.xml create mode 100644 platformtheme/platformtheme.cpp create mode 100644 platformtheme/platformtheme.h create mode 100644 platformtheme/qdbusmenubar.cpp create mode 100644 platformtheme/qdbusmenubar_p.h create mode 100644 platformtheme/statusnotifier/dbustypes.cpp create mode 100644 platformtheme/statusnotifier/dbustypes.h create mode 100644 platformtheme/statusnotifier/org.kde.StatusNotifierItem.xml create mode 100644 platformtheme/statusnotifier/statusnotifieritem.cpp create mode 100644 platformtheme/statusnotifier/statusnotifieritem.h create mode 100644 platformtheme/systemtrayicon.cpp create mode 100644 platformtheme/systemtrayicon.h create mode 100644 platformtheme/x11integration.cpp create mode 100644 platformtheme/x11integration.h create mode 100644 widgetstyle/CMakeLists.txt create mode 100644 widgetstyle/basestyle.cpp create mode 100644 widgetstyle/basestyle.h create mode 100644 widgetstyle/blurhelper.cpp create mode 100644 widgetstyle/blurhelper.h create mode 100644 widgetstyle/boxshadowrenderer.cpp create mode 100644 widgetstyle/boxshadowrenderer.h create mode 100644 widgetstyle/cutefishstyle.json create mode 100644 widgetstyle/phantomcolor.cpp create mode 100644 widgetstyle/phantomcolor.h create mode 100644 widgetstyle/pstyleplugin.cpp create mode 100644 widgetstyle/pstyleplugin.h create mode 100644 widgetstyle/shadowhelper.cpp create mode 100644 widgetstyle/shadowhelper.h create mode 100644 widgetstyle/tileset.cpp create mode 100644 widgetstyle/tileset.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..830955d --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +# C++ objects and libs +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so +*.so.* +*.dll +*.dylib + +# Qt-es +object_script.*.Release +object_script.*.Debug +*_plugin_import.cpp +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +moc_*.h +qrc_*.cpp +ui_*.h +*.qmlc +*.jsc +Makefile* +*build-* +*.qm +*.prl + +# Qt unit tests +target_wrapper.* + +# QtCreator +*.autosave + +# QtCreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCreator CMake +CMakeLists.txt.user* + +# QtCreator 4.8< compilation database +compile_commands.json + +# QtCreator local machine specific files for imported projects +*creator.user* + +build/* +.vscode/* diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..05f6d40 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.0) + +project(cutefish-qt-plugins) + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +add_subdirectory(platformtheme) +add_subdirectory(widgetstyle) diff --git a/README.md b/README.md new file mode 100644 index 0000000..b499af5 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Qt Plugins + +Unify Qt application style of CutefishOS. + +## Dependencies + +`sudo pacman -S gcc extra-cmake-modules qt5-base qt5-tools qt5-x11extras libqtxdg libdbusmenu-qt5 libxcb` + +## Build + +```shell +mkdir build +cd build +cmake .. +make +sudo make install +``` + +## License + +cutefish-qt-plugins is licensed under GPLv3. diff --git a/cmake/FindQt5PlatformSupport.cmake b/cmake/FindQt5PlatformSupport.cmake new file mode 100644 index 0000000..da6a1c1 --- /dev/null +++ b/cmake/FindQt5PlatformSupport.cmake @@ -0,0 +1,121 @@ +#.rst: +# FindQt5PlatformSupport +# ------- +# +# Try to find Qt5PlatformSupport on a Unix system. +# +# This will define the following variables: +# +# ``Qt5PlatformSupport_FOUND`` +# True if (the requested version of) Qt5PlatformSupport is available +# ``Qt5PlatformSupport_VERSION`` +# The version of Qt5PlatformSupport +# ``Qt5PlatformSupport_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``Qt5PlatformSupport::Qt5PlatformSupport`` +# target +# ``Qt5PlatformSupport_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``Qt5PlatformSupport_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``Qt5PlatformSupport_FOUND`` is TRUE, it will also define the following imported target: +# +# ``Qt5PlatformSupport::Qt5PlatformSupport`` +# The Qt5PlatformSupport library +# +# In general we recommend using the imported target, as it is easier to use. +# Bear in mind, however, that if the target is in the link interface of an +# exported library, it must be made available by the package config file. + +#============================================================================= +# Copyright 2014 Alex Merry +# Copyright 2014 Martin Gräßlin +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + +if(CMAKE_VERSION VERSION_LESS 2.8.12) + message(FATAL_ERROR "CMake 2.8.12 is required by FindQt5PlatformSupport.cmake") +endif() +if(CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.12) + message(AUTHOR_WARNING "Your project should require at least CMake 2.8.12 to use FindQt5PlatformSupport.cmake") +endif() + +# Use pkg-config to get the directories and then use these values +# in the FIND_PATH() and FIND_LIBRARY() calls +find_package(PkgConfig) +pkg_check_modules(PKG_Qt5PlatformSupport QUIET Qt5Gui) + +set(Qt5PlatformSupport_DEFINITIONS ${PKG_Qt5PlatformSupport_CFLAGS_OTHER}) +set(Qt5PlatformSupport_VERSION ${PKG_Qt5PlatformSupport_VERSION}) + +find_path(Qt5PlatformSupport_INCLUDE_DIR + NAMES + QtPlatformSupport/private/qfontconfigdatabase_p.h + HINTS + ${PKG_Qt5PlatformSupport_INCLUDEDIR}/QtPlatformSupport/${PKG_Qt5PlatformSupport_VERSION}/ +) +find_library(Qt5PlatformSupport_LIBRARY + NAMES + Qt5PlatformSupport + HINTS + ${PKG_Qt5PlatformSupport_LIBRARY_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Qt5PlatformSupport + FOUND_VAR + Qt5PlatformSupport_FOUND + REQUIRED_VARS + Qt5PlatformSupport_LIBRARY + Qt5PlatformSupport_INCLUDE_DIR + VERSION_VAR + Qt5PlatformSupport_VERSION +) + +if(Qt5PlatformSupport_FOUND AND NOT TARGET Qt5PlatformSupport::Qt5PlatformSupport) + add_library(Qt5PlatformSupport::Qt5PlatformSupport UNKNOWN IMPORTED) + set_target_properties(Qt5PlatformSupport::Qt5PlatformSupport PROPERTIES + IMPORTED_LOCATION "${Qt5PlatformSupport_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${Qt5PlatformSupport_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${Qt5PlatformSupport_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced(Qt5PlatformSupport_LIBRARY Qt5PlatformSupport_INCLUDE_DIR) + +# compatibility variables +set(Qt5PlatformSupport_LIBRARIES ${Qt5PlatformSupport_LIBRARY}) +set(Qt5PlatformSupport_INCLUDE_DIRS ${Qt5PlatformSupport_INCLUDE_DIR}) +set(Qt5PlatformSupport_VERSION_STRING ${Qt5PlatformSupport_VERSION}) + + +include(FeatureSummary) +set_package_properties(Qt5PlatformSupport PROPERTIES + URL "http://www.qt.io" + DESCRIPTION "Qt PlatformSupport module." +) + diff --git a/cmake/FindQt5ThemeSupport.cmake b/cmake/FindQt5ThemeSupport.cmake new file mode 100644 index 0000000..5588a76 --- /dev/null +++ b/cmake/FindQt5ThemeSupport.cmake @@ -0,0 +1,122 @@ +#.rst: +# FindQt5ThemeSupport +# ------- +# +# Try to find Qt5ThemeSupport on a Unix system. +# +# This will define the following variables: +# +# ``Qt5ThemeSupport_FOUND`` +# True if (the requested version of) Qt5ThemeSupport is available +# ``Qt5ThemeSupport_VERSION`` +# The version of Qt5ThemeSupport +# ``Qt5ThemeSupport_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``Qt5ThemeSupport::Qt5ThemeSupport`` +# target +# ``Qt5ThemeSupport_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``Qt5ThemeSupport_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``Qt5ThemeSupport_FOUND`` is TRUE, it will also define the following imported target: +# +# ``Qt5ThemeSupport::Qt5ThemeSupport`` +# The Qt5ThemeSupport library +# +# In general we recommend using the imported target, as it is easier to use. +# Bear in mind, however, that if the target is in the link interface of an +# exported library, it must be made available by the package config file. + +#============================================================================= +# Copyright 2014 Alex Merry +# Copyright 2014 Martin Gräßlin +# Copyright 2016 Takahiro Hashimoto +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + +if(CMAKE_VERSION VERSION_LESS 2.8.12) + message(FATAL_ERROR "CMake 2.8.12 is required by FindQt5ThemeSupport.cmake") +endif() +if(CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.12) + message(AUTHOR_WARNING "Your project should require at least CMake 2.8.12 to use FindQt5ThemeSupport.cmake") +endif() + +# Use pkg-config to get the directories and then use these values +# in the FIND_PATH() and FIND_LIBRARY() calls +find_package(PkgConfig) +pkg_check_modules(PKG_Qt5ThemeSupport QUIET Qt5Gui) + +set(Qt5ThemeSupport_DEFINITIONS ${PKG_Qt5ThemeSupport_CFLAGS_OTHER}) +set(Qt5ThemeSupport_VERSION ${PKG_Qt5ThemeSupport_VERSION}) + +find_path(Qt5ThemeSupport_INCLUDE_DIR + NAMES + QtThemeSupport/private/qgenericunixthemes_p.h + HINTS + ${PKG_Qt5ThemeSupport_INCLUDEDIR}/QtThemeSupport/${PKG_Qt5ThemeSupport_VERSION}/ +) +find_library(Qt5ThemeSupport_LIBRARY + NAMES + Qt5ThemeSupport + HINTS + ${PKG_Qt5ThemeSupport_LIBRARY_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Qt5ThemeSupport + FOUND_VAR + Qt5ThemeSupport_FOUND + REQUIRED_VARS + Qt5ThemeSupport_LIBRARY + Qt5ThemeSupport_INCLUDE_DIR + VERSION_VAR + Qt5ThemeSupport_VERSION +) + +if(Qt5ThemeSupport_FOUND AND NOT TARGET Qt5ThemeSupport::Qt5ThemeSupport) + add_library(Qt5ThemeSupport::Qt5ThemeSupport UNKNOWN IMPORTED) + set_target_properties(Qt5ThemeSupport::Qt5ThemeSupport PROPERTIES + IMPORTED_LOCATION "${Qt5ThemeSupport_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${Qt5ThemeSupport_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${Qt5ThemeSupport_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced(Qt5ThemeSupport_LIBRARY Qt5ThemeSupport_INCLUDE_DIR) + +# compatibility variables +set(Qt5ThemeSupport_LIBRARIES ${Qt5ThemeSupport_LIBRARY}) +set(Qt5ThemeSupport_INCLUDE_DIRS ${Qt5ThemeSupport_INCLUDE_DIR}) +set(Qt5ThemeSupport_VERSION_STRING ${Qt5ThemeSupport_VERSION}) + + +include(FeatureSummary) +set_package_properties(Qt5ThemeSupport PROPERTIES + URL "http://www.qt.io" + DESCRIPTION "Qt ThemeSupport module." +) + diff --git a/platformtheme/CMakeLists.txt b/platformtheme/CMakeLists.txt new file mode 100644 index 0000000..cc55f01 --- /dev/null +++ b/platformtheme/CMakeLists.txt @@ -0,0 +1,93 @@ +project(platformthemeplugin) +include(GNUInstallDirs) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +find_package(Qt5Core REQUIRED) +find_package(Qt5Widgets REQUIRED) +find_package(Qt5QuickControls2 REQUIRED) +find_package(Qt5DBus REQUIRED) +find_package(Qt5X11Extras REQUIRED) +find_package(Qt5Gui CONFIG REQUIRED Private) +find_package(Qt5XdgIconLoader REQUIRED) +find_package(dbusmenu-qt5 REQUIRED) +find_package(KF5WindowSystem REQUIRED) + +# dependencies for QPA plugin +find_package(Qt5ThemeSupport REQUIRED) +set(QT5PLATFORMSUPPORT_LIBS Qt5ThemeSupport::Qt5ThemeSupport) + +# qdbusmenubar uses them +remove_definitions(-DQT_NO_SIGNALS_SLOTS_KEYWORDS) + +pkg_check_modules(XCB_EWMH REQUIRED xcb xcb-ewmh x11) + +set (SRCS + main.cpp + platformtheme.h + platformtheme.cpp + hintsettings.h + hintsettings.cpp + systemtrayicon.h + systemtrayicon.cpp + qdbusmenubar_p.h + qdbusmenubar.cpp + x11integration.h + x11integration.cpp + statusnotifier/dbustypes.h + statusnotifier/dbustypes.cpp + statusnotifier/statusnotifieritem.h + statusnotifier/statusnotifieritem.cpp +) + +qt5_add_dbus_interface(SRCS org.kde.StatusNotifierWatcher.xml statusnotifierwatcher_interface) + +qt5_add_dbus_adaptor(SRCS + statusnotifier/org.kde.StatusNotifierItem.xml + statusnotifier/statusnotifieritem.h + StatusNotifierItem +) + +add_library(cutefishplatformtheme MODULE ${SRCS}) + +target_compile_definitions(cutefishplatformtheme + PRIVATE + "QT_NO_FOREACH" + "LIB_FM_QT_SONAME=\"${LIB_FM_QT_SONAME}\"" +) + +target_link_libraries(cutefishplatformtheme PRIVATE + Qt5::GuiPrivate + Qt5::X11Extras + Qt5::Widgets + Qt5::QuickControls2 + Qt5::Core + Qt5::DBus + dbusmenu-qt5 + + Qt5XdgIconLoader + + KF5::WindowSystem + ${XCB_LIBRARIES} + ${QT5PLATFORMSUPPORT_LIBS} +) + +get_target_property(QT_QMAKE_EXECUTABLE ${Qt5Core_QMAKE_EXECUTABLE} IMPORTED_LOCATION) +if(NOT QT_QMAKE_EXECUTABLE) + message(FATAL_ERROR "qmake is not found.") +endif() + +# execute the command "qmake -query QT_INSTALL_PLUGINS" to get the path of plugins dir. +execute_process(COMMAND ${QT_QMAKE_EXECUTABLE} -query QT_INSTALL_PLUGINS + OUTPUT_VARIABLE QT_PLUGINS_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) +if(QT_PLUGINS_DIR) + message(STATUS "Qt5 plugin directory:" "${QT_PLUGINS_DIR}") +else() + message(FATAL_ERROR "Qt5 plugin directory cannot be detected.") +endif() + +install(TARGETS cutefishplatformtheme LIBRARY DESTINATION "${QT_PLUGINS_DIR}/platformthemes") diff --git a/platformtheme/cutefish-platformtheme.json b/platformtheme/cutefish-platformtheme.json new file mode 100644 index 0000000..37ab5e2 --- /dev/null +++ b/platformtheme/cutefish-platformtheme.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "cutefish" ] +} diff --git a/platformtheme/hintsettings.cpp b/platformtheme/hintsettings.cpp new file mode 100644 index 0000000..683f3f2 --- /dev/null +++ b/platformtheme/hintsettings.cpp @@ -0,0 +1,126 @@ +#include "hintsettings.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static const QByteArray s_systemFontName = QByteArrayLiteral("Font"); +static const QByteArray s_systemFixedFontName = QByteArrayLiteral("FixedFont"); +static const QByteArray s_systemPointFontSize = QByteArrayLiteral("FontSize"); +static const QByteArray s_darkModeName = QByteArrayLiteral("DarkMode"); +static const QByteArray s_lightIconName = QByteArrayLiteral("Crule"); +static const QByteArray s_darkIconName = QByteArrayLiteral("Crule-dark"); + +HintsSettings::HintsSettings(QObject *parent) + : QObject(parent), + m_settings(new QSettings(QSettings::UserScope, "cutefishos", "theme")) +{ + m_hints[QPlatformTheme::SystemIconThemeName] = darkMode() ? s_darkIconName : s_lightIconName; + m_hints[QPlatformTheme::StyleNames] = "cutefish"; + m_hints[QPlatformTheme::SystemIconFallbackThemeName] = QStringLiteral("hicolor"); + m_hints[QPlatformTheme::IconThemeSearchPaths] = xdgIconThemePaths(); + m_hints[QPlatformTheme::UseFullScreenForPopupMenu] = false; + m_hints[QPlatformTheme::DialogButtonBoxLayout] = QPlatformDialogHelper::MacLayout; + + m_settingsFile = m_settings->fileName(); + + QMetaObject::invokeMethod(this, "lazyInit", Qt::QueuedConnection); +} + +HintsSettings::~HintsSettings() +{ +} + +void HintsSettings::lazyInit() +{ + m_fileWatcher = new QFileSystemWatcher(); + m_fileWatcher->addPath(m_settingsFile); + connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &HintsSettings::onFileChanged); +} + +QStringList HintsSettings::xdgIconThemePaths() const +{ + QStringList paths; + + // make sure we have ~/.local/share/icons in paths if it exists + paths << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("icons"), QStandardPaths::LocateDirectory); + + const QFileInfo homeIconDir(QDir::homePath() + QStringLiteral("/.icons")); + if (homeIconDir.isDir()) { + paths << homeIconDir.absoluteFilePath(); + } + + return paths; +} + +QString HintsSettings::systemFont() const +{ + return m_settings->value(s_systemFontName, "Noto Sans").toString(); +} + +QString HintsSettings::systemFixedFont() const +{ + return m_settings->value(s_systemFixedFontName, "Monospace").toString(); +} + +qreal HintsSettings::systemFontPointSize() const +{ + return m_settings->value(s_systemPointFontSize, 10.5).toDouble(); +} + +bool HintsSettings::darkMode() +{ + return m_settings->value(s_darkModeName, false).toBool(); +} + +void HintsSettings::onFileChanged(const QString &path) +{ + Q_UNUSED(path); + + QVariantMap map; + for (const QString &value : m_settings->allKeys()) { + map[value] = m_settings->value(value); + } + + m_settings->sync(); + + for (const QString &value : m_settings->allKeys()) { + const QVariant &oldValue = map.value(value); + const QVariant &newValue = m_settings->value(value); + + if (oldValue != newValue) { + if (value == s_systemFontName) + emit systemFontChanged(newValue.toString()); + else if (value == s_systemFixedFontName) + emit systemFixedFontChanged(newValue.toString()); + else if (value == s_systemPointFontSize) + emit systemFontPointSizeChanged(newValue.toDouble()); + else if (value == s_darkModeName) { + emit darkModeChanged(newValue.toBool()); + // Need to update the icon to dark + m_hints[QPlatformTheme::SystemIconThemeName] = darkMode() ? s_darkIconName : s_lightIconName; + emit iconThemeChanged(); + } + } + } + + bool fileDeleted = !m_fileWatcher->files().contains(m_settingsFile); + if (fileDeleted) + m_fileWatcher->addPath(m_settingsFile); +} diff --git a/platformtheme/hintsettings.h b/platformtheme/hintsettings.h new file mode 100644 index 0000000..8c72f3b --- /dev/null +++ b/platformtheme/hintsettings.h @@ -0,0 +1,57 @@ +#ifndef HINTSSETTINGS_H +#define HINTSSETTINGS_H + +#include +#include +#include +#include +#include + +#include + +class QPalette; +class HintsSettings : public QObject +{ + Q_OBJECT + +public: + explicit HintsSettings(QObject *parent = nullptr); + ~HintsSettings() override; + + QStringList xdgIconThemePaths() const; + + inline QVariant hint(QPlatformTheme::ThemeHint hint) const { + return m_hints[hint]; + } + + QString systemFont() const; + QString systemFixedFont() const; + qreal systemFontPointSize() const; + + bool darkMode(); + +public Q_SLOTS: + void lazyInit(); + +Q_SIGNALS: + void systemFontChanged(QString font); + void systemFixedFontChanged(QString fixedFont); + void systemFontPointSizeChanged(qreal systemFontPointSize); + void iconThemeChanged(); + void darkModeChanged(bool darkMode); + +private: + void onFileChanged(const QString &path); + +private: + QHash m_hints; + QSettings *m_settings; + QString m_settingsFile; + QFileSystemWatcher *m_fileWatcher; + + QString m_systemFont; + QString m_systemFixedFont; + qreal m_systemFontPointSize; +}; + +#endif //HINTSSETTINGS_H diff --git a/platformtheme/main.cpp b/platformtheme/main.cpp new file mode 100644 index 0000000..1ef7183 --- /dev/null +++ b/platformtheme/main.cpp @@ -0,0 +1,32 @@ +#include +#include "platformtheme.h" + +#include + +QT_BEGIN_NAMESPACE + +void updateXdgIconSystemTheme() +{ + XdgIconLoader::instance()->updateSystemTheme(); +} + +class PlatformThemePlugin : public QPlatformThemePlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QPlatformThemeFactoryInterface_iid FILE "cutefish-platformtheme.json") + +public: + PlatformThemePlugin(QObject *parent = nullptr) + : QPlatformThemePlugin(parent) {} + + QPlatformTheme *create(const QString &key, const QStringList ¶mList) override + { + Q_UNUSED(key) + Q_UNUSED(paramList) + return new PlatformTheme; + } +}; + +QT_END_NAMESPACE + +#include "main.moc" diff --git a/platformtheme/org.kde.StatusNotifierWatcher.xml b/platformtheme/org.kde.StatusNotifierWatcher.xml new file mode 100644 index 0000000..2eb1a7a --- /dev/null +++ b/platformtheme/org.kde.StatusNotifierWatcher.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platformtheme/platformtheme.cpp b/platformtheme/platformtheme.cpp new file mode 100644 index 0000000..efb6a7b --- /dev/null +++ b/platformtheme/platformtheme.cpp @@ -0,0 +1,202 @@ +#include "platformtheme.h" +#include "x11integration.h" +#include "qdbusmenubar_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Qt Private +#include +#include +#include +#include + +// Qt DBus +#include +#include + +#include + +static const QByteArray s_x11AppMenuServiceNamePropertyName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME"); +static const QByteArray s_x11AppMenuObjectPathPropertyName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH"); + +static bool checkDBusGlobalMenuAvailable() +{ + QDBusConnection connection = QDBusConnection::sessionBus(); + QString registrarService = QStringLiteral("com.canonical.AppMenu.Registrar"); + return connection.interface()->isServiceRegistered(registrarService); +} + +static bool isDBusGlobalMenuAvailable() +{ + static bool dbusGlobalMenuAvailable = checkDBusGlobalMenuAvailable(); + return dbusGlobalMenuAvailable; +} + +extern void updateXdgIconSystemTheme(); + +void onDarkModeChanged() +{ + QStyle *style = QStyleFactory::create("cutefish"); + if (style) { + qApp->setStyle(style); + } +} + +PlatformTheme::PlatformTheme() + : m_hints(new HintsSettings) +{ + // qApp->setProperty("_hints_settings_object", (quintptr)m_hints); + + if (KWindowSystem::isPlatformX11()) { + m_x11Integration.reset(new X11Integration()); + m_x11Integration->init(); + } + + connect(m_hints, &HintsSettings::systemFontChanged, this, &PlatformTheme::onFontChanged); + connect(m_hints, &HintsSettings::systemFontPointSizeChanged, this, &PlatformTheme::onFontChanged); + connect(m_hints, &HintsSettings::iconThemeChanged, this, &PlatformTheme::onIconThemeChanged); + connect(m_hints, &HintsSettings::darkModeChanged, &onDarkModeChanged); + + QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, false); + setQtQuickControlsTheme(); +} + +PlatformTheme::~PlatformTheme() +{ +} + +QVariant PlatformTheme::themeHint(QPlatformTheme::ThemeHint hintType) const +{ + QVariant hint = m_hints->hint(hintType); + if (hint.isValid()) { + return hint; + } else { + return QPlatformTheme::themeHint(hintType); + } +} + +const QFont* PlatformTheme::font(Font type) const +{ + switch (type) { + case SystemFont: + case MessageBoxFont: + case LabelFont: + case TipLabelFont: + case StatusBarFont: + case PushButtonFont: + case ItemViewFont: + case ListViewFont: + case HeaderViewFont: + case ListBoxFont: + case ComboMenuItemFont: + case ComboLineEditFont: { + const QString &fontName = m_hints->systemFont(); + qreal fontSize = m_hints->systemFontPointSize(); + static QFont font = QFont(QString()); + font.setFamily(fontName); + font.setPointSizeF(fontSize); + return &font; + } + case FixedFont: { + const QString &fontName = m_hints->systemFixedFont(); + qreal fontSize = m_hints->systemFontPointSize(); + static QFont font = QFont(QString()); + font.setFamily(fontName); + font.setPointSizeF(fontSize); + return &font; + } + default: { + const QString &fontName = m_hints->systemFont(); + qreal fontSize = m_hints->systemFontPointSize(); + static QFont font = QFont(QString()); + font.setFamily(fontName); + font.setPointSizeF(fontSize); + return &font; + } + } + + return QPlatformTheme::font(type); +} + +QPlatformMenuBar *PlatformTheme::createPlatformMenuBar() const +{ +// if (isDBusGlobalMenuAvailable()) { +// auto *menu = new QDBusMenuBar(); + +// QObject::connect(menu, &QDBusMenuBar::windowChanged, menu, [this, menu](QWindow *newWindow, QWindow *oldWindow) { +// const QString &serviceName = QDBusConnection::sessionBus().baseService(); +// const QString &objectPath = menu->objectPath(); + +// if (m_x11Integration) { +// if (oldWindow) { +// m_x11Integration->setWindowProperty(oldWindow, s_x11AppMenuServiceNamePropertyName, {}); +// m_x11Integration->setWindowProperty(oldWindow, s_x11AppMenuObjectPathPropertyName, {}); +// } + +// if (newWindow) { +// m_x11Integration->setWindowProperty(newWindow, s_x11AppMenuServiceNamePropertyName, serviceName.toUtf8()); +// m_x11Integration->setWindowProperty(newWindow, s_x11AppMenuObjectPathPropertyName, objectPath.toUtf8()); +// } +// } + +// // if (m_kwaylandIntegration) { +// // if (oldWindow) { +// // m_kwaylandIntegration->setAppMenu(oldWindow, QString(), QString()); +// // } +// // +// // if (newWindow) { +// // m_kwaylandIntegration->setAppMenu(newWindow, serviceName, objectPath); +// // } +// // } +// }); + +// return menu; +// } + + return nullptr; +} + +void PlatformTheme::onFontChanged() +{ + QFont font; + font.setFamily(m_hints->systemFont()); + font.setPointSizeF(m_hints->systemFontPointSize()); + + // Change font + if (qobject_cast(QCoreApplication::instance())) + QApplication::setFont(font); + else if (qobject_cast(QCoreApplication::instance())) + QGuiApplication::setFont(font); +} + +void PlatformTheme::onIconThemeChanged() +{ + QIconLoader::instance()->updateSystemTheme(); + updateXdgIconSystemTheme(); + + QEvent update(QEvent::UpdateRequest); + for (QWindow *window : qGuiApp->allWindows()) { + if (window->type() == Qt::Desktop) + continue; + + qApp->sendEvent(window, &update); + } +} + +void PlatformTheme::setQtQuickControlsTheme() +{ + //if the user has explicitly set something else, don't meddle + if (!QQuickStyle::name().isEmpty()) { + return; + } + + QQuickStyle::setStyle(QLatin1String("meui-style")); +} \ No newline at end of file diff --git a/platformtheme/platformtheme.h b/platformtheme/platformtheme.h new file mode 100644 index 0000000..dbaef46 --- /dev/null +++ b/platformtheme/platformtheme.h @@ -0,0 +1,48 @@ +#ifndef PLATFORMTHEME_H +#define PLATFORMTHEME_H + +#include +#include "hintsettings.h" +#include "systemtrayicon.h" + +#include +#include + +class QIconEngine; +class QWindow; +class X11Integration; + +class PlatformTheme : public QObject, public QPlatformTheme +{ + Q_OBJECT + +public: + PlatformTheme(); + ~PlatformTheme() override; + + QVariant themeHint(ThemeHint hint) const override; + const QFont *font(Font type) const override; + + QPlatformMenuBar *createPlatformMenuBar() const override; + + QPlatformSystemTrayIcon *createPlatformSystemTrayIcon() const override { + auto trayIcon = new SystemTrayIcon; + if (trayIcon->isSystemTrayAvailable()) + return trayIcon; + else { + delete trayIcon; + return nullptr; + } + } + +private: + void onFontChanged(); + void onIconThemeChanged(); + void setQtQuickControlsTheme(); + +private: + HintsSettings *m_hints; + QScopedPointer m_x11Integration; +}; + +#endif // PLATFORMTHEME_H diff --git a/platformtheme/qdbusmenubar.cpp b/platformtheme/qdbusmenubar.cpp new file mode 100644 index 0000000..b7b5de3 --- /dev/null +++ b/platformtheme/qdbusmenubar.cpp @@ -0,0 +1,156 @@ +#include "qdbusmenubar_p.h" + + +QT_BEGIN_NAMESPACE + +/* note: do not change these to QStringLiteral; + we are unloaded before QtDBus is done using the strings. + */ +#define REGISTRAR_SERVICE QLatin1String("com.canonical.AppMenu.Registrar") +#define REGISTRAR_PATH QLatin1String("/com/canonical/AppMenu/Registrar") + +QDBusMenuBar::QDBusMenuBar() + : QPlatformMenuBar() + , m_menu(new QDBusPlatformMenu()) + , m_menuAdaptor(new QDBusMenuAdaptor(m_menu)) +{ + QDBusMenuItem::registerDBusTypes(); + connect(m_menu, &QDBusPlatformMenu::propertiesUpdated, + m_menuAdaptor, &QDBusMenuAdaptor::ItemsPropertiesUpdated); + connect(m_menu, &QDBusPlatformMenu::updated, + m_menuAdaptor, &QDBusMenuAdaptor::LayoutUpdated); + + // This signal is new in Qt 5.8 but distros might have backported it, hence a runtime look-up + if (m_menu->metaObject()->indexOfSignal("popupRequested(int,uint)") != -1) { + connect(m_menu, SIGNAL(popupRequested(int,uint)), m_menuAdaptor, SIGNAL(ItemActivationRequested(int,uint))); + } +} + +QDBusMenuBar::~QDBusMenuBar() +{ + unregisterMenuBar(); + delete m_menuAdaptor; + delete m_menu; + qDeleteAll(m_menuItems); +} + +QDBusPlatformMenuItem *QDBusMenuBar::menuItemForMenu(QPlatformMenu *menu) +{ + if (!menu) + return nullptr; + quintptr tag = menu->tag(); + const auto it = m_menuItems.constFind(tag); + if (it != m_menuItems.cend()) { + return *it; + } else { + QDBusPlatformMenuItem *item = new QDBusPlatformMenuItem; + updateMenuItem(item, menu); + m_menuItems.insert(tag, item); + return item; + } +} + +void QDBusMenuBar::updateMenuItem(QDBusPlatformMenuItem *item, QPlatformMenu *menu) +{ + const QDBusPlatformMenu *ourMenu = qobject_cast(menu); + item->setText(ourMenu->text()); + item->setIcon(ourMenu->icon()); + item->setEnabled(ourMenu->isEnabled()); + item->setVisible(ourMenu->isVisible()); + item->setMenu(menu); +} + +void QDBusMenuBar::insertMenu(QPlatformMenu *menu, QPlatformMenu *before) +{ + QDBusPlatformMenuItem *menuItem = menuItemForMenu(menu); + QDBusPlatformMenuItem *beforeItem = menuItemForMenu(before); + m_menu->insertMenuItem(menuItem, beforeItem); + m_menu->emitUpdated(); +} + +void QDBusMenuBar::removeMenu(QPlatformMenu *menu) +{ + QDBusPlatformMenuItem *menuItem = menuItemForMenu(menu); + m_menu->removeMenuItem(menuItem); + m_menu->emitUpdated(); +} + +void QDBusMenuBar::syncMenu(QPlatformMenu *menu) +{ + QDBusPlatformMenuItem *menuItem = menuItemForMenu(menu); + updateMenuItem(menuItem, menu); +} + +void QDBusMenuBar::handleReparent(QWindow *newParentWindow) +{ + if (newParentWindow == m_window) { + return; + } + + QWindow *oldWindow = m_window; + + unregisterMenuBar(); + m_window = newParentWindow; + + if (newParentWindow) { + registerMenuBar(); + } + + emit windowChanged(newParentWindow, oldWindow); +} + +QPlatformMenu *QDBusMenuBar::menuForTag(quintptr tag) const +{ + QDBusPlatformMenuItem *menuItem = m_menuItems.value(tag); + if (menuItem) + return const_cast(menuItem->menu()); + return nullptr; +} + +QPlatformMenu *QDBusMenuBar::createMenu() const +{ + return new QDBusPlatformMenu; +} + +void QDBusMenuBar::registerMenuBar() +{ + static uint menuBarId = 0; + + if (!m_window) { + qWarning("Cannot register window menu without window"); + return; + } + + QDBusConnection connection = QDBusConnection::sessionBus(); + m_objectPath = QStringLiteral("/MenuBar/%1").arg(++menuBarId); + if (!connection.registerObject(m_objectPath, m_menu)) + return; + + QDBusMenuRegistrarInterface registrar(REGISTRAR_SERVICE, REGISTRAR_PATH, connection, this); + QDBusPendingReply<> r = registrar.RegisterWindow(static_cast(window()->winId()), QDBusObjectPath(m_objectPath)); + r.waitForFinished(); + if (r.isError()) { + qWarning("Failed to register window menu, reason: %s (\"%s\")", + qUtf8Printable(r.error().name()), qUtf8Printable(r.error().message())); + connection.unregisterObject(m_objectPath); + } +} + +void QDBusMenuBar::unregisterMenuBar() +{ + QDBusConnection connection = QDBusConnection::sessionBus(); + + if (m_window) { + QDBusMenuRegistrarInterface registrar(REGISTRAR_SERVICE, REGISTRAR_PATH, connection, this); + QDBusPendingReply<> r = registrar.UnregisterWindow(static_cast(window()->winId())); + r.waitForFinished(); + if (r.isError()) + qWarning("Failed to unregister window menu, reason: %s (\"%s\")", + qUtf8Printable(r.error().name()), qUtf8Printable(r.error().message())); + } + + if (!m_objectPath.isEmpty()) + connection.unregisterObject(m_objectPath); +} + +QT_END_NAMESPACE diff --git a/platformtheme/qdbusmenubar_p.h b/platformtheme/qdbusmenubar_p.h new file mode 100644 index 0000000..1b1278e --- /dev/null +++ b/platformtheme/qdbusmenubar_p.h @@ -0,0 +1,101 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Dmitry Shachnev +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QDBUSMENUBAR_P_H +#define QDBUSMENUBAR_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QDBusMenuBar : public QPlatformMenuBar +{ + Q_OBJECT + +public: + QDBusMenuBar(); + ~QDBusMenuBar() override; + + void insertMenu(QPlatformMenu *menu, QPlatformMenu *before) override; + void removeMenu(QPlatformMenu *menu) override; + void syncMenu(QPlatformMenu *menu) override; + void handleReparent(QWindow *newParentWindow) override; + QPlatformMenu *menuForTag(quintptr tag) const override; + QPlatformMenu *createMenu() const override; + + QWindow *window() const { return m_window; } + QString objectPath() const { return m_objectPath; } + +Q_SIGNALS: + void windowChanged(QWindow *newWindow, QWindow *oldWindow); + +private: + QDBusPlatformMenu *m_menu; + QDBusMenuAdaptor *m_menuAdaptor; + QHash m_menuItems; + QPointer m_window; + QString m_objectPath; + + QDBusPlatformMenuItem *menuItemForMenu(QPlatformMenu *menu); + static void updateMenuItem(QDBusPlatformMenuItem *item, QPlatformMenu *menu); + void registerMenuBar(); + void unregisterMenuBar(); +}; + +QT_END_NAMESPACE + +#endif // QDBUSMENUBAR_P_H diff --git a/platformtheme/statusnotifier/dbustypes.cpp b/platformtheme/statusnotifier/dbustypes.cpp new file mode 100644 index 0000000..7eabe5c --- /dev/null +++ b/platformtheme/statusnotifier/dbustypes.cpp @@ -0,0 +1,47 @@ +#include "dbustypes.h" + +// Marshall the IconPixmap data into a D-Bus argument +QDBusArgument &operator<<(QDBusArgument &argument, const IconPixmap &icon) +{ + argument.beginStructure(); + argument << icon.width; + argument << icon.height; + argument << icon.bytes; + argument.endStructure(); + return argument; +} + +// Retrieve the ImageStruct data from the D-Bus argument +const QDBusArgument &operator>>(const QDBusArgument &argument, IconPixmap &icon) +{ + argument.beginStructure(); + argument >> icon.width; + argument >> icon.height; + argument >> icon.bytes; + argument.endStructure(); + return argument; +} + +// Marshall the ToolTip data into a D-Bus argument +QDBusArgument &operator<<(QDBusArgument &argument, const ToolTip &toolTip) +{ + argument.beginStructure(); + argument << toolTip.iconName; + argument << toolTip.iconPixmap; + argument << toolTip.title; + argument << toolTip.description; + argument.endStructure(); + return argument; +} + +// Retrieve the ToolTip data from the D-Bus argument +const QDBusArgument &operator>>(const QDBusArgument &argument, ToolTip &toolTip) +{ + argument.beginStructure(); + argument >> toolTip.iconName; + argument >> toolTip.iconPixmap; + argument >> toolTip.title; + argument >> toolTip.description; + argument.endStructure(); + return argument; +} \ No newline at end of file diff --git a/platformtheme/statusnotifier/dbustypes.h b/platformtheme/statusnotifier/dbustypes.h new file mode 100644 index 0000000..8f87710 --- /dev/null +++ b/platformtheme/statusnotifier/dbustypes.h @@ -0,0 +1,32 @@ +#include + +#ifndef DBUSTYPES_H +#define DBUSTYPES_H + +struct IconPixmap { + int width; + int height; + QByteArray bytes; +}; + +typedef QList IconPixmapList; + +Q_DECLARE_METATYPE(IconPixmap) +Q_DECLARE_METATYPE(IconPixmapList) + +struct ToolTip { + QString iconName; + QList iconPixmap; + QString title; + QString description; +}; + +Q_DECLARE_METATYPE(ToolTip) + +QDBusArgument &operator<<(QDBusArgument &argument, const IconPixmap &icon); +const QDBusArgument &operator>>(const QDBusArgument &argument, IconPixmap &icon); + +QDBusArgument &operator<<(QDBusArgument &argument, const ToolTip &toolTip); +const QDBusArgument &operator>>(const QDBusArgument &argument, ToolTip &toolTip); + +#endif // DBUSTYPES_H \ No newline at end of file diff --git a/platformtheme/statusnotifier/org.kde.StatusNotifierItem.xml b/platformtheme/statusnotifier/org.kde.StatusNotifierItem.xml new file mode 100644 index 0000000..d95e740 --- /dev/null +++ b/platformtheme/statusnotifier/org.kde.StatusNotifierItem.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platformtheme/statusnotifier/statusnotifieritem.cpp b/platformtheme/statusnotifier/statusnotifieritem.cpp new file mode 100644 index 0000000..94596f5 --- /dev/null +++ b/platformtheme/statusnotifier/statusnotifieritem.cpp @@ -0,0 +1,313 @@ +#include "statusnotifieritem.h" +#include "statusnotifieritemadaptor.h" +#include +#include +#include + +int StatusNotifierItem::mServiceCounter = 0; + +StatusNotifierItem::StatusNotifierItem(QString id, QObject *parent) + : QObject(parent), + mAdaptor(new StatusNotifierItemAdaptor(this)), + mService(QString::fromLatin1("org.freedesktop.StatusNotifierItem-%1-%2") + .arg(QCoreApplication::applicationPid()) + .arg(++mServiceCounter)), + mId(id), + mTitle(QLatin1String("Test")), + mStatus(QLatin1String("Active")), + mCategory(QLatin1String("ApplicationStatus")), + mMenu(nullptr), + mMenuPath(QLatin1String("/NO_DBUSMENU")), + mMenuExporter(nullptr), + mSessionBus(QDBusConnection::connectToBus(QDBusConnection::SessionBus, mService)) +{ + // Separate DBus connection to the session bus is created, because QDbus does not provide + // a way to register different objects for different services with the same paths. + // For status notifiers we need different /StatusNotifierItem for each service. + + // register service + + mSessionBus.registerObject(QLatin1String("/StatusNotifierItem"), this); + + registerToHost(); + + // monitor the watcher service in case the host restarts + QDBusServiceWatcher *watcher = new QDBusServiceWatcher(QLatin1String("org.kde.StatusNotifierWatcher"), + mSessionBus, + QDBusServiceWatcher::WatchForOwnerChange, + this); + connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, + this, &StatusNotifierItem::onServiceOwnerChanged); +} + +StatusNotifierItem::~StatusNotifierItem() +{ + mSessionBus.unregisterObject(QLatin1String("/StatusNotifierItem")); + QDBusConnection::disconnectFromBus(mService); +} + +void StatusNotifierItem::registerToHost() +{ + QDBusInterface interface(QLatin1String("org.kde.StatusNotifierWatcher"), + QLatin1String("/StatusNotifierWatcher"), + QLatin1String("org.kde.StatusNotifierWatcher"), + mSessionBus); + interface.asyncCall(QLatin1String("RegisterStatusNotifierItem"), mSessionBus.baseService()); +} + +void StatusNotifierItem::onServiceOwnerChanged(const QString& service, const QString& oldOwner, + const QString& newOwner) +{ + Q_UNUSED(service); + Q_UNUSED(oldOwner); + + if (!newOwner.isEmpty()) + registerToHost(); +} + +void StatusNotifierItem::onMenuDestroyed() +{ + mMenu = nullptr; + setMenuPath(QLatin1String("/NO_DBUSMENU")); + mMenuExporter = nullptr; //mMenu is a QObject parent of the mMenuExporter +} + +void StatusNotifierItem::setTitle(const QString &title) +{ + if (mTitle == title) + return; + + mTitle = title; + Q_EMIT mAdaptor->NewTitle(); +} + +void StatusNotifierItem::setStatus(const QString &status) +{ + if (mStatus == status) + return; + + mStatus = status; + Q_EMIT mAdaptor->NewStatus(mStatus); +} + +void StatusNotifierItem::setCategory(const QString &category) +{ + if (mCategory == category) + return; + + mCategory = category; +} + +void StatusNotifierItem::setMenuPath(const QString& path) +{ + mMenuPath.setPath(path); +} + +void StatusNotifierItem::setIconByName(const QString &name) +{ + if (mIconName == name) + return; + + mIconName = name; + Q_EMIT mAdaptor->NewIcon(); +} + +void StatusNotifierItem::setIconByPixmap(const QIcon &icon) +{ + if (mIconCacheKey == icon.cacheKey()) + return; + + mIconCacheKey = icon.cacheKey(); + mIcon = iconToPixmapList(icon); + mIconName.clear(); + Q_EMIT mAdaptor->NewIcon(); +} + +void StatusNotifierItem::setOverlayIconByName(const QString &name) +{ + if (mOverlayIconName == name) + return; + + mOverlayIconName = name; + Q_EMIT mAdaptor->NewOverlayIcon(); +} + +void StatusNotifierItem::setOverlayIconByPixmap(const QIcon &icon) +{ + if (mOverlayIconCacheKey == icon.cacheKey()) + return; + + mOverlayIconCacheKey = icon.cacheKey(); + mOverlayIcon = iconToPixmapList(icon); + mOverlayIconName.clear(); + Q_EMIT mAdaptor->NewOverlayIcon(); +} + +void StatusNotifierItem::setAttentionIconByName(const QString &name) +{ + if (mAttentionIconName == name) + return; + + mAttentionIconName = name; + Q_EMIT mAdaptor->NewAttentionIcon(); +} + +void StatusNotifierItem::setAttentionIconByPixmap(const QIcon &icon) +{ + if (mAttentionIconCacheKey == icon.cacheKey()) + return; + + mAttentionIconCacheKey = icon.cacheKey(); + mAttentionIcon = iconToPixmapList(icon); + mAttentionIconName.clear(); + Q_EMIT mAdaptor->NewAttentionIcon(); +} + +void StatusNotifierItem::setToolTipTitle(const QString &title) +{ + if (mTooltipTitle == title) + return; + + mTooltipTitle = title; + Q_EMIT mAdaptor->NewToolTip(); +} + +void StatusNotifierItem::setToolTipSubTitle(const QString &subTitle) +{ + if (mTooltipSubtitle == subTitle) + return; + + mTooltipSubtitle = subTitle; + Q_EMIT mAdaptor->NewToolTip(); +} + +void StatusNotifierItem::setToolTipIconByName(const QString &name) +{ + if (mTooltipIconName == name) + return; + + mTooltipIconName = name; + Q_EMIT mAdaptor->NewToolTip(); +} + +void StatusNotifierItem::setToolTipIconByPixmap(const QIcon &icon) +{ + if (mTooltipIconCacheKey == icon.cacheKey()) + return; + + mTooltipIconCacheKey = icon.cacheKey(); + mTooltipIcon = iconToPixmapList(icon); + mTooltipIconName.clear(); + Q_EMIT mAdaptor->NewToolTip(); +} + +void StatusNotifierItem::setContextMenu(QMenu* menu) +{ + if (mMenu == menu) + return; + + if (nullptr != mMenu) + { + disconnect(mMenu, &QObject::destroyed, this, &StatusNotifierItem::onMenuDestroyed); + } + mMenu = menu; + + if (nullptr != mMenu) + setMenuPath(QLatin1String("/MenuBar")); + else + setMenuPath(QLatin1String("/NO_DBUSMENU")); + + //Note: we need to destroy menu exporter before creating new one -> to free the DBus object path for new menu + delete mMenuExporter; + if (nullptr != mMenu) + { + connect(mMenu, &QObject::destroyed, this, &StatusNotifierItem::onMenuDestroyed); + mMenuExporter = new DBusMenuExporter{this->menu().path(), mMenu, mSessionBus}; + } +} + +void StatusNotifierItem::Activate(int x, int y) +{ + if (mStatus == QLatin1String("NeedsAttention")) + mStatus = QLatin1String("Active"); + + Q_EMIT activateRequested(QPoint(x, y)); +} + +void StatusNotifierItem::SecondaryActivate(int x, int y) +{ + if (mStatus == QLatin1String("NeedsAttention")) + mStatus = QLatin1String("Active"); + + Q_EMIT secondaryActivateRequested(QPoint(x, y)); +} + +void StatusNotifierItem::ContextMenu(int x, int y) +{ + if (mMenu) + { + if (mMenu->isVisible()) + mMenu->popup(QPoint(x, y)); + else + mMenu->hide(); + } +} + +void StatusNotifierItem::Scroll(int delta, const QString &orientation) +{ + Qt::Orientation orient = Qt::Vertical; + if (orientation.toLower() == QLatin1String("horizontal")) + orient = Qt::Horizontal; + + Q_EMIT scrollRequested(delta, orient); +} + +void StatusNotifierItem::showMessage(const QString& title, const QString& msg, + const QString& iconName, int secs) +{ + QDBusInterface interface(QLatin1String("org.freedesktop.Notifications"), QLatin1String("/org/freedesktop/Notifications"), + QLatin1String("org.freedesktop.Notifications"), mSessionBus); + interface.call(QLatin1String("Notify"), mTitle, (uint) 0, iconName, title, + msg, QStringList(), QVariantMap(), secs); +} + +IconPixmapList StatusNotifierItem::iconToPixmapList(const QIcon& icon) +{ + IconPixmapList pixmapList; + + // long live KDE! + const QList sizes = icon.availableSizes(); + for (const QSize &size : sizes) + { + QImage image = icon.pixmap(size).toImage(); + + IconPixmap pix; + pix.height = image.height(); + pix.width = image.width(); + + if (image.format() != QImage::Format_ARGB32) + image = image.convertToFormat(QImage::Format_ARGB32); + + pix.bytes = QByteArray((char *) image.bits(), +#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) + image.byteCount()); +#else + image.sizeInBytes()); +#endif + + // swap to network byte order if we are little endian + if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) + { + quint32 *uintBuf = (quint32 *) pix.bytes.data(); + for (uint i = 0; i < pix.bytes.size() / sizeof(quint32); ++i) + { + *uintBuf = qToBigEndian(*uintBuf); + ++uintBuf; + } + } + + pixmapList.append(pix); + } + + return pixmapList; +} \ No newline at end of file diff --git a/platformtheme/statusnotifier/statusnotifieritem.h b/platformtheme/statusnotifier/statusnotifieritem.h new file mode 100644 index 0000000..d312401 --- /dev/null +++ b/platformtheme/statusnotifier/statusnotifieritem.h @@ -0,0 +1,163 @@ +#ifndef STATUS_NOTIFIER_ITEM_H +#define STATUS_NOTIFIER_ITEM_H + +#include +#include +#include +#include + +#include "dbustypes.h" + +class StatusNotifierItemAdaptor; +class DBusMenuExporter; + +class StatusNotifierItem : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString Category READ category) + Q_PROPERTY(QString Title READ title) + Q_PROPERTY(QString Id READ id) + Q_PROPERTY(QString Status READ status) + Q_PROPERTY(QDBusObjectPath Menu READ menu) + + Q_PROPERTY(QString IconName READ iconName) + Q_PROPERTY(IconPixmapList IconPixmap READ iconPixmap) + + Q_PROPERTY(QString OverlayIconName READ overlayIconName) + Q_PROPERTY(IconPixmapList OverlayIconPixmap READ overlayIconPixmap) + + Q_PROPERTY(QString AttentionIconName READ attentionIconName) + Q_PROPERTY(IconPixmapList AttentionIconPixmap READ attentionIconPixmap) + + Q_PROPERTY(ToolTip ToolTip READ toolTip) + +public: + StatusNotifierItem(QString id, QObject *parent = nullptr); + ~StatusNotifierItem() override; + + QString id() const + { return mId; } + + QString title() const + { return mTitle; } + void setTitle(const QString &title); + + QString status() const + { return mStatus; } + void setStatus(const QString &status); + + QString category() const + { return mCategory; } + void setCategory(const QString &category); + + QDBusObjectPath menu() const + { return mMenuPath; } + void setMenuPath(const QString &path); + + QString iconName() const + { return mIconName; } + void setIconByName(const QString &name); + + IconPixmapList iconPixmap() const + { return mIcon; } + void setIconByPixmap(const QIcon &icon); + + QString overlayIconName() const + { return mOverlayIconName; } + void setOverlayIconByName(const QString &name); + + IconPixmapList overlayIconPixmap() const + { return mOverlayIcon; } + void setOverlayIconByPixmap(const QIcon &icon); + + QString attentionIconName() const + { return mAttentionIconName; } + void setAttentionIconByName(const QString &name); + + IconPixmapList attentionIconPixmap() const + { return mAttentionIcon; } + void setAttentionIconByPixmap(const QIcon &icon); + + QString toolTipTitle() const + { return mTooltipTitle; } + void setToolTipTitle(const QString &title); + + QString toolTipSubTitle() const + { return mTooltipSubtitle; } + void setToolTipSubTitle(const QString &subTitle); + + QString toolTipIconName() const + { return mTooltipIconName; } + void setToolTipIconByName(const QString &name); + + IconPixmapList toolTipIconPixmap() const + { return mTooltipIcon; } + void setToolTipIconByPixmap(const QIcon &icon); + + ToolTip toolTip() const + { + ToolTip tt; + tt.title = mTooltipTitle; + tt.description = mTooltipSubtitle; + tt.iconName = mTooltipIconName; + tt.iconPixmap = mTooltipIcon; + return tt; + } + + /*! + * \Note: we don't take ownership for the \param menu + */ + void setContextMenu(QMenu *menu); + +public Q_SLOTS: + void Activate(int x, int y); + void SecondaryActivate(int x, int y); + void ContextMenu(int x, int y); + void Scroll(int delta, const QString &orientation); + + void showMessage(const QString &title, const QString &msg, const QString &iconName, int secs); + +private: + void registerToHost(); + IconPixmapList iconToPixmapList(const QIcon &icon); + +private Q_SLOTS: + void onServiceOwnerChanged(const QString &service, const QString &oldOwner, + const QString &newOwner); + void onMenuDestroyed(); + +Q_SIGNALS: + void activateRequested(const QPoint &pos); + void secondaryActivateRequested(const QPoint &pos); + void scrollRequested(int delta, Qt::Orientation orientation); + +private: + StatusNotifierItemAdaptor *mAdaptor; + + QString mService; + QString mId; + QString mTitle; + QString mStatus; + QString mCategory; + + // icons + QString mIconName, mOverlayIconName, mAttentionIconName; + IconPixmapList mIcon, mOverlayIcon, mAttentionIcon; + qint64 mIconCacheKey, mOverlayIconCacheKey, mAttentionIconCacheKey; + + // tooltip + QString mTooltipTitle, mTooltipSubtitle, mTooltipIconName; + IconPixmapList mTooltipIcon; + qint64 mTooltipIconCacheKey; + + // menu + QMenu *mMenu; + QDBusObjectPath mMenuPath; + DBusMenuExporter *mMenuExporter; + QDBusConnection mSessionBus; + + static int mServiceCounter; +}; + +#endif \ No newline at end of file diff --git a/platformtheme/systemtrayicon.cpp b/platformtheme/systemtrayicon.cpp new file mode 100644 index 0000000..686689e --- /dev/null +++ b/platformtheme/systemtrayicon.cpp @@ -0,0 +1,355 @@ +#include "systemtrayicon.h" +#include +#include +#include +#include +#include +#include +#include + +SystemTrayMenu::SystemTrayMenu() + : QPlatformMenu(), + m_tag(0), + m_menu(new QMenu()) +{ + connect(m_menu.data(), &QMenu::aboutToShow, this, &QPlatformMenu::aboutToShow); + connect(m_menu.data(), &QMenu::aboutToHide, this, &QPlatformMenu::aboutToHide); +} + +SystemTrayMenu::~SystemTrayMenu() +{ + if (m_menu) + m_menu->deleteLater(); +} + +QPlatformMenuItem *SystemTrayMenu::createMenuItem() const +{ + return new SystemTrayMenuItem(); +} + +void SystemTrayMenu::insertMenuItem(QPlatformMenuItem *menuItem, QPlatformMenuItem *before) +{ + if (SystemTrayMenuItem *ours = qobject_cast(menuItem)) + { + bool inserted = false; + + if (SystemTrayMenuItem *oursBefore = qobject_cast(before)) + { + for (auto it = m_items.begin(); it != m_items.end(); ++it) + { + if (*it == oursBefore) + { + m_items.insert(it, ours); + if (m_menu) + m_menu->insertAction(oursBefore->action(), ours->action()); + + inserted = true; + break; + } + } + } + + if (!inserted) + { + m_items.append(ours); + if (m_menu) + m_menu->addAction(ours->action()); + } + } +} + +QPlatformMenuItem *SystemTrayMenu::menuItemAt(int position) const +{ + if (position < m_items.size()) + return m_items.at(position); + + return nullptr; +} + +QPlatformMenuItem *SystemTrayMenu::menuItemForTag(quintptr tag) const +{ + auto it = std::find_if(m_items.constBegin(), m_items.constEnd(), [tag] (SystemTrayMenuItem *item) + { + return item->tag() == tag; + }); + + if (it != m_items.constEnd()) + return *it; + + return nullptr; +} + +void SystemTrayMenu::removeMenuItem(QPlatformMenuItem *menuItem) +{ + if (SystemTrayMenuItem *ours = qobject_cast(menuItem)) + { + m_items.removeOne(ours); + if (ours->action() && m_menu) + m_menu->removeAction(ours->action()); + } +} + +void SystemTrayMenu::setEnabled(bool enabled) +{ + if (!m_menu) + return; + + m_menu->setEnabled(enabled); +} + +void SystemTrayMenu::setIcon(const QIcon &icon) +{ + if (!m_menu) + return; + + m_menu->setIcon(icon); +} + +void SystemTrayMenu::setTag(quintptr tag) +{ + m_tag = tag; +} + +void SystemTrayMenu::setText(const QString &text) +{ + if (!m_menu) + return; + + m_menu->setTitle(text); +} + +void SystemTrayMenu::setVisible(bool visible) +{ + if (!m_menu) + return; + + m_menu->setVisible(visible); +} + +void SystemTrayMenu::syncMenuItem(QPlatformMenuItem *) +{ + // Nothing to do +} + +void SystemTrayMenu::syncSeparatorsCollapsible(bool enable) +{ + if (!m_menu) + return; + + m_menu->setSeparatorsCollapsible(enable); +} + +quintptr SystemTrayMenu::tag() const +{ + return m_tag; +} + +QMenu *SystemTrayMenu::menu() const +{ + return m_menu.data(); +} + +SystemTrayMenuItem::SystemTrayMenuItem() + : QPlatformMenuItem(), + m_tag(0), + m_action(new QAction(this)) +{ + connect(m_action, &QAction::triggered, this, &QPlatformMenuItem::activated); + connect(m_action, &QAction::hovered, this, &QPlatformMenuItem::hovered); +} + +SystemTrayMenuItem::~SystemTrayMenuItem() +{ +} + +void SystemTrayMenuItem::setCheckable(bool checkable) +{ + m_action->setCheckable(checkable); +} + +void SystemTrayMenuItem::setChecked(bool isChecked) +{ + m_action->setChecked(isChecked); +} + +void SystemTrayMenuItem::setEnabled(bool enabled) +{ + m_action->setEnabled(enabled); +} + +void SystemTrayMenuItem::setFont(const QFont &font) +{ + m_action->setFont(font); +} + +void SystemTrayMenuItem::setIcon(const QIcon &icon) +{ + m_action->setIcon(icon); +} + +void SystemTrayMenuItem::setIsSeparator(bool isSeparator) +{ + m_action->setSeparator(isSeparator); +} + +void SystemTrayMenuItem::setMenu(QPlatformMenu *menu) +{ + if (SystemTrayMenu *ourMenu = qobject_cast(menu)) + m_action->setMenu(ourMenu->menu()); +} + +void SystemTrayMenuItem::setRole(QPlatformMenuItem::MenuRole) +{ +} + +void SystemTrayMenuItem::setShortcut(const QKeySequence &shortcut) +{ + m_action->setShortcut(shortcut); +} + +void SystemTrayMenuItem::setTag(quintptr tag) +{ + m_tag = tag; +} + +void SystemTrayMenuItem::setText(const QString &text) +{ + m_action->setText(text); +} + +void SystemTrayMenuItem::setVisible(bool isVisible) +{ + m_action->setVisible(isVisible); +} + +void SystemTrayMenuItem::setIconSize(int) +{ +} + +quintptr SystemTrayMenuItem::tag() const +{ + return m_tag; +} + +QAction *SystemTrayMenuItem::action() const +{ + return m_action; +} + +SystemTrayIcon::SystemTrayIcon() + : QPlatformSystemTrayIcon(), + mSni(nullptr) +{ + // register types + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); +} + +SystemTrayIcon::~SystemTrayIcon() +{ +} + +void SystemTrayIcon::init() +{ + if (!mSni) + { + mSni = new StatusNotifierItem(QString::number(QCoreApplication::applicationPid()), this); + mSni->setTitle(QApplication::applicationDisplayName()); + + // default menu + QPlatformMenu *menu = createMenu(); + menu->setParent(mSni); + QPlatformMenuItem *menuItem = menu->createMenuItem(); + menuItem->setParent(menu); + menuItem->setText(tr("Quit")); + menuItem->setIcon(QIcon::fromTheme(QLatin1String("application-exit"))); + connect(menuItem, &QPlatformMenuItem::activated, qApp, &QApplication::quit); + menu->insertMenuItem(menuItem, nullptr); + updateMenu(menu); + + connect(mSni, &StatusNotifierItem::activateRequested, [this](const QPoint &) + { + Q_EMIT activated(QPlatformSystemTrayIcon::Trigger); + }); + + connect(mSni, &StatusNotifierItem::secondaryActivateRequested, [this](const QPoint &) + { + Q_EMIT activated(QPlatformSystemTrayIcon::MiddleClick); + }); + } +} + +void SystemTrayIcon::cleanup() +{ + delete mSni; + mSni = nullptr; +} + +void SystemTrayIcon::updateIcon(const QIcon &icon) +{ + if (!mSni) + return; + + if (icon.name().isEmpty()) + { + mSni->setIconByPixmap(icon); + mSni->setToolTipIconByPixmap(icon); + } + else + { + mSni->setIconByName(icon.name()); + mSni->setToolTipIconByName(icon.name()); + } +} + +void SystemTrayIcon::updateToolTip(const QString &tooltip) +{ + if (!mSni) + return; + + mSni->setToolTipTitle(tooltip); +} + +void SystemTrayIcon::updateMenu(QPlatformMenu *menu) +{ + if (!mSni) + return; + + if (SystemTrayMenu *ourMenu = qobject_cast(menu)) + mSni->setContextMenu(ourMenu->menu()); +} + +QPlatformMenu *SystemTrayIcon::createMenu() const +{ + return new SystemTrayMenu(); +} + +QRect SystemTrayIcon::geometry() const +{ + // StatusNotifierItem doesn't provide the geometry + return {}; +} + +void SystemTrayIcon::showMessage(const QString &title, const QString &msg, + const QIcon &icon, MessageIcon, int secs) +{ + if (!mSni) + return; + + mSni->showMessage(title, msg, icon.name(), secs); +} + +bool SystemTrayIcon::isSystemTrayAvailable() const +{ + QDBusInterface systrayHost(QLatin1String("org.kde.StatusNotifierWatcher"), + QLatin1String("/StatusNotifierWatcher"), + QLatin1String("org.kde.StatusNotifierWatcher")); + + return systrayHost.isValid() && systrayHost.property("IsStatusNotifierHostRegistered").toBool(); +} + +bool SystemTrayIcon::supportsMessages() const +{ + return true; +} \ No newline at end of file diff --git a/platformtheme/systemtrayicon.h b/platformtheme/systemtrayicon.h new file mode 100644 index 0000000..9c62baf --- /dev/null +++ b/platformtheme/systemtrayicon.h @@ -0,0 +1,124 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org/ + * + * Copyright: 2015 LXQt team + * Authors: + * Paulo Lieuthier + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#ifndef SYSTEMTRAYICON_H +#define SYSTEMTRAYICON_H + +#include +#include + +#include "statusnotifier/statusnotifieritem.h" + +class SystemTrayMenuItem; +class QAction; +class QMenu; + +class SystemTrayMenu : public QPlatformMenu +{ + Q_OBJECT +public: + SystemTrayMenu(); + ~SystemTrayMenu() Q_DECL_OVERRIDE; + void insertMenuItem(QPlatformMenuItem *menuItem, QPlatformMenuItem *before) Q_DECL_OVERRIDE; + QPlatformMenuItem *menuItemAt(int position) const Q_DECL_OVERRIDE; + QPlatformMenuItem *menuItemForTag(quintptr tag) const Q_DECL_OVERRIDE; + void removeMenuItem(QPlatformMenuItem *menuItem) Q_DECL_OVERRIDE; + void setEnabled(bool enabled) Q_DECL_OVERRIDE; + void setIcon(const QIcon &icon) Q_DECL_OVERRIDE; + void setTag(quintptr tag) Q_DECL_OVERRIDE; + void setText(const QString &text) Q_DECL_OVERRIDE; + void setVisible(bool visible) Q_DECL_OVERRIDE; + void syncMenuItem(QPlatformMenuItem *menuItem) Q_DECL_OVERRIDE; + void syncSeparatorsCollapsible(bool enable) Q_DECL_OVERRIDE; + quintptr tag() const Q_DECL_OVERRIDE; + QPlatformMenuItem *createMenuItem() const Q_DECL_OVERRIDE; + + QMenu *menu() const; + +private: + quintptr m_tag; + QPointer m_menu; + QList m_items; +}; + +class SystemTrayMenuItem : public QPlatformMenuItem +{ + Q_OBJECT +public: + SystemTrayMenuItem(); + ~SystemTrayMenuItem() Q_DECL_OVERRIDE; + void setCheckable(bool checkable) Q_DECL_OVERRIDE; + void setChecked(bool isChecked) Q_DECL_OVERRIDE; + void setEnabled(bool enabled) Q_DECL_OVERRIDE; + void setFont(const QFont &font) Q_DECL_OVERRIDE; + void setIcon(const QIcon &icon) Q_DECL_OVERRIDE; + void setIsSeparator(bool isSeparator) Q_DECL_OVERRIDE; + void setMenu(QPlatformMenu *menu) Q_DECL_OVERRIDE; + void setRole(MenuRole role) Q_DECL_OVERRIDE; + void setShortcut(const QKeySequence &shortcut) Q_DECL_OVERRIDE; + void setTag(quintptr tag) Q_DECL_OVERRIDE; + void setText(const QString &text) Q_DECL_OVERRIDE; + void setVisible(bool isVisible) Q_DECL_OVERRIDE; + quintptr tag() const Q_DECL_OVERRIDE; + void setIconSize(int size) + #if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) + Q_DECL_OVERRIDE + #endif + ; + + QAction *action() const; + +private: + quintptr m_tag; + QAction *m_action; +}; + +class SystemTrayIcon : public QPlatformSystemTrayIcon +{ +public: + SystemTrayIcon(); + ~SystemTrayIcon() Q_DECL_OVERRIDE; + + void init() Q_DECL_OVERRIDE; + void cleanup() Q_DECL_OVERRIDE; + void updateIcon(const QIcon &icon) Q_DECL_OVERRIDE; + void updateToolTip(const QString &tooltip) Q_DECL_OVERRIDE; + void updateMenu(QPlatformMenu *menu) Q_DECL_OVERRIDE; + QRect geometry() const Q_DECL_OVERRIDE; + void showMessage(const QString &title, const QString &msg, + const QIcon &icon, MessageIcon iconType, int secs) Q_DECL_OVERRIDE; + + bool isSystemTrayAvailable() const Q_DECL_OVERRIDE; + bool supportsMessages() const Q_DECL_OVERRIDE; + + QPlatformMenu *createMenu() const Q_DECL_OVERRIDE; + +private: + StatusNotifierItem *mSni; +}; + +#endif \ No newline at end of file diff --git a/platformtheme/x11integration.cpp b/platformtheme/x11integration.cpp new file mode 100644 index 0000000..7b10cc3 --- /dev/null +++ b/platformtheme/x11integration.cpp @@ -0,0 +1,135 @@ +/* This file is part of the KDE libraries + * Copyright 2015 Martin Gräßlin + * Copyright 2016 Marco Martin + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2 of the License or ( at + * your option ) version 3 or, at the discretion of KDE e.V. ( which shall + * act as a proxy as in section 14 of the GPLv3 ), 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 Lesser General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#include "x11integration.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +static const char s_schemePropertyName[] = "KDE_COLOR_SCHEME_PATH"; +static const QByteArray s_blurBehindPropertyName = QByteArrayLiteral("ENABLE_BLUR_BEHIND_HINT"); +static const QByteArray s_blurRegionPropertyName = QByteArrayLiteral("BLUR_REGION"); + +X11Integration::X11Integration() + : QObject() +{ +} + +X11Integration::~X11Integration() = default; + +void X11Integration::init() +{ + QCoreApplication::instance()->installEventFilter(this); +} + +bool X11Integration::eventFilter(QObject *watched, QEvent *event) +{ + //the drag and drop window should NOT be a tooltip + //https://bugreports.qt.io/browse/QTBUG-52560 + if (event->type() == QEvent::Show && watched->inherits("QShapedPixmapWindow")) { + //static cast should be safe there + QWindow *w = static_cast(watched); + NETWinInfo info(QX11Info::connection(), w->winId(), QX11Info::appRootWindow(), NET::WMWindowType, NET::Properties2()); + info.setWindowType(NET::DNDIcon); + // TODO: does this flash the xcb connection? + } + + // if (event->type() == QEvent::PlatformSurface) { + // if (QWindow *w = qobject_cast(watched)) { + // QPlatformSurfaceEvent *pe = static_cast(event); + // if (!w->flags().testFlag(Qt::ForeignWindow)) { + // if (pe->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) { + // const auto blurBehindProperty = w->property(s_blurBehindPropertyName.constData()); + // if (blurBehindProperty.isValid()) { + // KWindowEffects::enableBlurBehind(w->winId(), blurBehindProperty.toBool()); + // } + // installDesktopFileName(w); + // } + // } + // } + // } + + // if (event->type() == QEvent::ApplicationPaletteChange) { + // const auto topLevelWindows = QGuiApplication::topLevelWindows(); + // for (QWindow *w : topLevelWindows) { + // installColorScheme(w); + // } + // } + + return false; +} + +void X11Integration::installDesktopFileName(QWindow *w) +{ + if (!w->isTopLevel()) { + return; + } + + QString desktopFileName = QGuiApplication::desktopFileName(); + if (desktopFileName.isEmpty()) { + return; + } + // handle apps which set the desktopFileName property with filename suffix, + // due to unclear API dox (https://bugreports.qt.io/browse/QTBUG-75521) + if (desktopFileName.endsWith(QLatin1String(".desktop"))) { + desktopFileName.chop(8); + } + NETWinInfo info(QX11Info::connection(), w->winId(), QX11Info::appRootWindow(), NET::Properties(), NET::Properties2()); + info.setDesktopFileName(desktopFileName.toUtf8().constData()); +} + +void X11Integration::setWindowProperty(QWindow *window, const QByteArray &name, const QByteArray &value) +{ + auto *c = QX11Info::connection(); + + xcb_atom_t atom; + auto it = m_atoms.find(name); + if (it == m_atoms.end()) { + const xcb_intern_atom_cookie_t cookie = xcb_intern_atom(c, false, name.length(), name.constData()); + QScopedPointer reply(xcb_intern_atom_reply(c, cookie, nullptr)); + if (!reply.isNull()) { + atom = reply->atom; + m_atoms[name] = atom; + } else { + return; + } + } else { + atom = *it; + } + + if (value.isEmpty()) { + xcb_delete_property(c, window->winId(), atom); + } else { + xcb_change_property(c, XCB_PROP_MODE_REPLACE, window->winId(), atom, XCB_ATOM_STRING, + 8, value.length(), value.constData()); + } +} diff --git a/platformtheme/x11integration.h b/platformtheme/x11integration.h new file mode 100644 index 0000000..a216789 --- /dev/null +++ b/platformtheme/x11integration.h @@ -0,0 +1,47 @@ +/* This file is part of the KDE libraries + * Copyright 2015 Martin Gräßlin + * Copyright 2016 Marco Martin + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2 of the License or ( at + * your option ) version 3 or, at the discretion of KDE e.V. ( which shall + * act as a proxy as in section 14 of the GPLv3 ), 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 Lesser General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#ifndef X11INTEGRATION_H +#define X11INTEGRATION_H + +#include +#include +#include + +class QWindow; + +class X11Integration : public QObject +{ + Q_OBJECT +public: + explicit X11Integration(); + ~X11Integration() override; + void init(); + + void setWindowProperty(QWindow *window, const QByteArray &name, const QByteArray &value); + + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + void installDesktopFileName(QWindow *w); + QHash m_atoms; +}; + +#endif diff --git a/widgetstyle/CMakeLists.txt b/widgetstyle/CMakeLists.txt new file mode 100644 index 0000000..218f64b --- /dev/null +++ b/widgetstyle/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.5) +project(cutefishstyle) + +set(TARGET cutefishstyle) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +find_package(ECM REQUIRED NO_MODULE) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) + +set(QT Core Gui Widgets DBus) +find_package(Qt5 REQUIRED ${QT}) +find_package(PkgConfig REQUIRED) +find_package(KF5 REQUIRED WindowSystem) + +include(ECMQueryQmake) + +set (SRCS + blurhelper.cpp + blurhelper.h + pstyleplugin.cpp + pstyleplugin.h + basestyle.h + basestyle.cpp + phantomcolor.h + phantomcolor.cpp + shadowhelper.h + shadowhelper.cpp + tileset.h + tileset.cpp + boxshadowrenderer.h + boxshadowrenderer.cpp +) + +add_library(${TARGET} MODULE ${SRCS}) +target_link_libraries(${TARGET} + Qt5::GuiPrivate + Qt5::Core + Qt5::Gui + Qt5::Widgets + Qt5::DBus + KF5::WindowSystem + ) + +query_qmake(CMAKE_INSTALL_QTPLUGINDIR QT_INSTALL_PLUGINS) + +install(TARGETS ${TARGET} DESTINATION ${CMAKE_INSTALL_QTPLUGINDIR}/styles/) diff --git a/widgetstyle/basestyle.cpp b/widgetstyle/basestyle.cpp new file mode 100644 index 0000000..4139add --- /dev/null +++ b/widgetstyle/basestyle.cpp @@ -0,0 +1,5094 @@ +/* + * Copyright (C) 2020 Reven Martin + * Copyright (C) 2020 KeePassXC Team + * Copyright (C) 2019 Andrew Richards + * + * Derived from Phantomstyle and relicensed under the GPLv2 or v3. + * https://github.com/randrew/phantomstyle + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "basestyle.h" +#include "phantomcolor.h" +#include "shadowhelper.h" +#include "blurhelper.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +QT_BEGIN_NAMESPACE +Q_GUI_EXPORT int qt_defaultDpiX(); +QT_END_NAMESPACE + +static QObject *hintsSettings() +{ + return reinterpret_cast(qvariant_cast(qApp->property("_hints_settings_object"))); +} + +QRectF strokedRect( const QRectF &rect, const int penWidth ) +{ + /* With a pen stroke width of 1, the rectangle should have each of its + * sides moved inwards by half a pixel. This allows the stroke to be + * pixel perfect instead of blurry from sitting between pixels and + * prevents the rectangle with a stroke from becoming larger than the + * original size of the rectangle. + */ + qreal adjustment = 0.5 * penWidth; + return QRectF( rect ).adjusted( adjustment, adjustment, -adjustment, -adjustment ); +} + +QRectF strokedRect( const QRect &rect, const int penWidth ) +{ + return strokedRect(QRectF(rect), penWidth); +} + +extern void qt_blurImage(QImage &blurImage, qreal radius, bool quality, int transposed); + +// Redefine Q_FALLTHROUGH for older Qt versions +#ifndef Q_FALLTHROUGH +#if (defined(Q_CC_GNU) && Q_CC_GNU >= 700) && !defined(Q_CC_INTEL) +#define Q_FALLTHROUGH() __attribute__((fallthrough)) +#else +#define Q_FALLTHROUGH() (void)0 +#endif +#endif + +namespace Phantom +{ + namespace + { + constexpr qint16 DefaultFrameWidth = 6; + constexpr qint16 SplitterMaxLength = 100; // Length of splitter handle (not thickness) + constexpr qint16 MenuMinimumWidth = 20; // Smallest width that menu items can have + constexpr qint16 MenuBar_FrameWidth = 6; + constexpr qint16 MenuBar_ItemSpacing = 10; + constexpr qint16 SpinBox_ButtonWidth = 15; + + // These two are currently not based on font, but could be + constexpr qint16 LineEdit_ContentsHPad = 5; + constexpr qint16 ComboBox_NonEditable_ContentsHPad = 7; + constexpr qint16 HeaderSortIndicator_HOffset = 1; + constexpr qint16 HeaderSortIndicator_VOffset = 2; + constexpr qint16 TabBar_InctiveVShift = 0; + + constexpr qreal DefaultFrame_Radius = 5.0; + constexpr qreal TabBarTab_Rounding = 1.0; + constexpr qreal SpinBox_Rounding = 5.0; + constexpr qreal LineEdit_Rounding = 5.0; + constexpr qreal FrameFocusRect_Rounding = 5.0; + constexpr qreal PushButton_Rounding = 5.0; + constexpr qreal ToolButton_Rounding = 5.0; + constexpr qreal ProgressBar_Rounding = 5.0; + constexpr qreal GroupBox_Rounding = 5.0; + constexpr qreal SliderGroove_Rounding = 5.0; + constexpr qreal SliderHandle_Rounding = 5.0; + + constexpr qreal CheckMark_WidthOfHeightScale = 0.8; + constexpr qreal PushButton_HorizontalPaddingFontHeightRatio = 1.0; + constexpr qreal TabBar_HPaddingFontRatio = 1.25; + constexpr qreal TabBar_VPaddingFontRatio = 1.0 / 1.25; + constexpr qreal GroupBox_LabelBottomMarginFontRatio = 1.0 / 4.0; + constexpr qreal ComboBox_ArrowMarginRatio = 1.0 / 3.25; + + constexpr qreal MenuBar_HorizontalPaddingFontRatio = 1.0 / 2.0; + constexpr qreal MenuBar_VerticalPaddingFontRatio = 1.0 / 3.0; + + constexpr qreal MenuItem_LeftMarginFontRatio = 1.0 / 2.0; + constexpr qreal MenuItem_RightMarginForTextFontRatio = 1.0 / 1.5; + constexpr qreal MenuItem_RightMarginForArrowFontRatio = 1.0 / 4.0; + constexpr qreal MenuItem_VerticalMarginsFontRatio = 1.0 / 5.0; + // Number that's multiplied with a font's height to get the space between a + // menu item's checkbox (or other sign) and its text (or icon). + constexpr qreal MenuItem_CheckRightSpaceFontRatio = 1.0 / 4.0; + constexpr qreal MenuItem_TextMnemonicSpaceFontRatio = 1.5; + constexpr qreal MenuItem_SubMenuArrowSpaceFontRatio = 1.0 / 1.5; + constexpr qreal MenuItem_SubMenuArrowWidthFontRatio = 1.0 / 2.75; + constexpr qreal MenuItem_SeparatorHeightFontRatio = 1.0 / 1.5; + constexpr qreal MenuItem_CheckMarkVerticalInsetFontRatio = 1.0 / 5.0; + constexpr qreal MenuItem_IconRightSpaceFontRatio = 1.0 / 3.0; + + constexpr bool BranchesOnEdge = false; + constexpr bool OverhangShadows = false; + constexpr bool IndicatorShadows = false; + constexpr bool MenuExtraBottomMargin = true; + constexpr bool MenuBarLeftMargin = false; + constexpr bool MenuBarDrawBorder = false; + constexpr bool AllowToolBarAutoRaise = true; + // Note that this only applies to the disclosure etc. decorators in tree views. + constexpr bool ShowItemViewDecorationSelected = false; + constexpr bool UseQMenuForComboBoxPopup = true; + constexpr bool ItemView_UseFontHeightForDecorationSize = true; + + // Whether or not the non-raised tabs in a tab bar have shininess/highlights to + // them. Setting this to false adds an extra visual hint for distinguishing + // between the current and non-current tabs, but makes the non-current tabs + // appear less clickable. Other ways to increase the visual differences could + // be to increase the color contrast for the background fill color, or increase + // the vertical offset. However, increasing the vertical offset comes with some + // layout challenges, and increasing the color contrast further may visually + // imply an incorrect layout structure. Not sure what's best. + // + // This doesn't disable creating the color/brush resource, even though it's + // currently a compile-time-only option, because it may be changed to be part + // of some dynamic config system for Phantom in the future, or have a + // per-widget style hint associated with it. + const bool TabBar_InactiveTabsHaveSpecular = false; + + struct Grad + { + Grad(const QColor& from, const QColor& to) + { + rgbA = Rgb::ofQColor(from); + rgbB = Rgb::ofQColor(to); + lA = rgbA.toHsl().l; + lB = rgbB.toHsl().l; + } + QColor sample(qreal alpha) const + { + Hsl hsl = Rgb::lerp(rgbA, rgbB, alpha).toHsl(); + hsl.l = Phantom::lerp(lA, lB, alpha); + return hsl.toQColor(); + } + Rgb rgbA, rgbB; + qreal lA, lB; + }; + + namespace DeriveColors + { + Q_NEVER_INLINE QColor adjustLightness(const QColor& qcolor, qreal ld) + { + Hsl hsl = Hsl::ofQColor(qcolor); + const qreal gamma = 3.0; + hsl.l = std::pow(Phantom::saturate(std::pow(hsl.l, 1.0 / gamma) + ld * 0.8), gamma); + return hsl.toQColor(); + } + 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 buttonColor(const QPalette& pal) + { + // temp hack + if (pal.color(QPalette::Button) == pal.color(QPalette::Window)) + return adjustLightness(pal.color(QPalette::Button), 0.01); + return pal.color(QPalette::Button); + } + QColor highlightedOutlineOf(const QPalette& pal) + { + return adjustLightness(pal.color(QPalette::Highlight), -0.08); + } + QColor dividerColor(const QColor& underlying) + { + return adjustLightness(underlying, -0.05); + } + QColor lightDividerColor(const QColor& underlying) + { + return adjustLightness(underlying, 0.02); + } + QColor outlineOf(const QPalette& pal) + { + return adjustLightness(pal.color(QPalette::Window), -0.1); + } + QColor gutterColorOf(const QPalette& pal) + { + return adjustLightness(pal.color(QPalette::Window), -0.05); + } + QColor darkGutterColorOf(const QPalette& pal) + { + return adjustLightness(pal.color(QPalette::Window), -0.08); + } + QColor lightShadeOf(const QColor& underlying) + { + return adjustLightness(underlying, 0.08); + } + QColor darkShadeOf(const QColor& underlying) + { + return adjustLightness(underlying, -0.08); + } + QColor overhangShadowOf(const QColor& underlying) + { + return adjustLightness(underlying, -0.05); + } + QColor sliderGutterShadowOf(const QColor& underlying) + { + return adjustLightness(underlying, -0.01); + } + QColor specularOf(const QColor& underlying) + { + return adjustLightness(underlying, 0.01); + } + QColor lightSpecularOf(const QColor& underlying) + { + return adjustLightness(underlying, 0.05); + } + QColor pressedOf(const QColor& color) + { + return adjustLightness(color, -0.05); + } + QColor darkPressedOf(const QColor& color) + { + return adjustLightness(color, -0.08); + } + QColor lightOnOf(const QColor& color) + { + return adjustLightness(color, -0.04); + } + QColor onOf(const QColor& color) + { + return adjustLightness(color, -0.08); + } + QColor indicatorColorOf(const QPalette& palette, QPalette::ColorGroup group = QPalette::Current) + { + if (hack_isLightPalette(palette)) { + qreal adjust = (palette.currentColorGroup() == QPalette::Disabled) ? 0.09 : 0.32; + return adjustLightness(palette.color(group, QPalette::WindowText), adjust); + } + return adjustLightness(palette.color(group, QPalette::WindowText), -0.05); + } + 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); + } + 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_scrollbarSlider, + S_scrollbarSlider_hover, + S_scrollbarSlider_pressed, + S_window_outline, + S_window_specular, + S_window_divider, + S_window_lighter, + S_window_darker, + S_frame_outline, + S_button_specular, + S_button_pressed, + S_button_on, + S_button_pressed_specular, + S_sliderHandle, + S_sliderHandle_pressed, + S_sliderHandle_specular, + S_sliderHandle_pressed_specular, + S_base_shadow, + S_base_divider, + S_windowText_disabled, + S_highlight_outline, + S_highlight_specular, + S_progressBar_outline, + S_inactiveTabYesFrame, + S_inactiveTabNoFrame, + S_inactiveTabYesFrame_specular, + S_inactiveTabNoFrame_specular, + S_indicator_current, + S_indicator_disabled, + S_itemView_multiSelection_currentBorder, + S_itemView_headerOnLine, + S_scrollbarGutter_disabled, + + // Aliases + S_progressBar = S_highlight, + S_progressBar_specular = S_highlight_specular, + S_tabFrame = S_window, + S_tabFrame_specular = S_window_specular, + }; + } + + using Swatchy = SwatchColors::SwatchColor; + + enum + { + Num_SwatchColors = SwatchColors::S_scrollbarGutter_disabled + 1, + Num_ShadowSteps = 3, + }; + + struct PhSwatch : public QSharedData + { + // The pens store the brushes within them, so storing the brushes here as + // well is redundant. However, QPen::brush() does not return its brush by + // reference, so we'd end up doing a bunch of inc/dec work every time we use + // one. Also, it saves us the indirection of chasing two pointers (Swatch -> + // QPen -> QBrush) every time we want to get a QColor. + QBrush brushes[Num_SwatchColors]; + QPen pens[Num_SwatchColors]; + QColor scrollbarShadowColors[Num_ShadowSteps]; + + // Note: the casts to int in the assert macros are to suppress a false + // positive warning for tautological comparison in the clang linter. + inline const QColor& color(Swatchy swatchValue) const + { + Q_ASSERT(swatchValue >= 0 && static_cast(swatchValue) < Num_SwatchColors); + return brushes[swatchValue].color(); + } + inline const QBrush& brush(Swatchy swatchValue) const + { + Q_ASSERT(swatchValue >= 0 && static_cast(swatchValue) < Num_SwatchColors); + return brushes[swatchValue]; + } + inline const QPen& pen(Swatchy swatchValue) const + { + Q_ASSERT(swatchValue >= 0 && static_cast(swatchValue) < Num_SwatchColors); + return pens[swatchValue]; + } + + void loadFromQPalette(const QPalette& pal); + }; + + using PhSwatchPtr = QExplicitlySharedDataPointer; + using PhCacheEntry = QPair; + enum : int + { + Num_ColorCacheEntries = 10, + }; + using PhSwatchCache = QVarLengthArray; + Q_NEVER_INLINE void PhSwatch::loadFromQPalette(const QPalette& pal) + { + using namespace SwatchColors; + namespace Dc = DeriveColors; + bool isLight = Dc::hack_isLightPalette(pal); + QColor colors[Num_SwatchColors]; + colors[S_none] = QColor(); + + colors[S_window] = pal.color(QPalette::Window); + colors[S_button] = pal.color(QPalette::Button); + if (colors[S_button] == colors[S_window]) + colors[S_button] = Dc::adjustLightness(colors[S_button], 0.01); + colors[S_base] = pal.color(QPalette::Base); + colors[S_text] = pal.color(QPalette::Text); + colors[S_windowText] = pal.color(QPalette::WindowText); + colors[S_highlight] = pal.color(QPalette::Highlight); + colors[S_highlightedText] = pal.color(QPalette::HighlightedText); + colors[S_scrollbarGutter] = isLight ? Dc::gutterColorOf(pal) : Dc::darkGutterColorOf(pal); + colors[S_scrollbarSlider] = isLight ? colors[S_button] : Dc::adjustLightness(colors[S_window], 0.1); + colors[S_scrollbarSlider_hover] = isLight ? Dc::adjustLightness(colors[S_button], -0.1) : Dc::adjustLightness(colors[S_window], 0.2); + colors[S_scrollbarSlider_pressed] = isLight ? Dc::adjustLightness(colors[S_button], -0.2) : Dc::adjustLightness(colors[S_window], 0.25); + + colors[S_window_outline] = + isLight ? Dc::adjustLightness(colors[S_window], -0.1) : Dc::adjustLightness(colors[S_window], 0.03); + colors[S_window_specular] = Dc::specularOf(colors[S_window]); + colors[S_window_divider] = + isLight ? Dc::dividerColor(colors[S_window]) : Dc::lightDividerColor(colors[S_window]); + colors[S_window_lighter] = Dc::lightShadeOf(colors[S_window]); + colors[S_window_darker] = Dc::darkShadeOf(colors[S_window]); + colors[S_frame_outline] = isLight ? colors[S_window_outline] : Dc::adjustLightness(colors[S_window], 0.08); + colors[S_button_specular] = + isLight ? Dc::specularOf(colors[S_button]) : Dc::lightSpecularOf(colors[S_button]); + colors[S_button_pressed] = isLight ? Dc::pressedOf(colors[S_button]) : Dc::darkPressedOf(colors[S_button]); + colors[S_button_on] = isLight ? Dc::lightOnOf(colors[S_button]) : Dc::onOf(colors[S_button]); + colors[S_button_pressed_specular] = + isLight ? Dc::specularOf(colors[S_button_pressed]) : Dc::lightSpecularOf(colors[S_button_pressed]); + + colors[S_sliderHandle] = isLight ? colors[S_button] : Dc::adjustLightness(colors[S_button], -0.03); + colors[S_sliderHandle_specular] = + isLight ? Dc::specularOf(colors[S_sliderHandle]) : Dc::lightSpecularOf(colors[S_sliderHandle]); + colors[S_sliderHandle_pressed] = + isLight ? colors[S_button_pressed] : Dc::adjustLightness(colors[S_button_pressed], 0.03); + colors[S_sliderHandle_pressed_specular] = isLight ? Dc::specularOf(colors[S_sliderHandle_pressed]) + : Dc::lightSpecularOf(colors[S_sliderHandle_pressed]); + + colors[S_base_shadow] = Dc::overhangShadowOf(colors[S_base]); + colors[S_base_divider] = colors[S_window_divider]; + colors[S_windowText_disabled] = pal.color(QPalette::Disabled, QPalette::WindowText); + colors[S_highlight_outline] = isLight ? Dc::adjustLightness(colors[S_highlight], -0.02) + : Dc::adjustLightness(colors[S_highlight], 0.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(i / static_cast(Num_ShadowSteps)); + } + } + + // This is the "hash" (not really a hash) function we'll use on the happy fast + // path when looking up a PhSwatch for a given QPalette. It's fragile, because + // it uses QPalette::cacheKey(), so it may not match even when the contents + // (currentColorGroup + the RGB colors) of the QPalette are actually a match. + // But it's cheaper to calculate, so we'll store a single one of these "hashes" + // for the head (most recently used) cached PhSwatch, and check to see if it + // matches. This is the most common case, so we can usually save some work by + // doing this. (The second most common case is probably having a different + // ColorGroup but the rest of the contents are the same, but we don't have a + // special path for that.) + inline quint64 fastfragile_hash_qpalette(const QPalette& p) + { + union + { + qint64 i; + quint64 u; + } x; + x.i = p.cacheKey(); + // QPalette::ColorGroup has range 0..5 (inclusive), so it only uses 3 bits. + // The high 32 bits in QPalette::cacheKey() are a global incrementing serial + // number for the QPalette creation. We don't store (2^29-1) things in our + // cache, and I doubt that many will ever be created in a real application + // while also retaining some of them across such a broad time range, so it's + // really unlikely that repurposing these top 3 bits to also include the + // QPalette::currentColorGroup() (which the cacheKey doesn't include for some + // reason...) will generate a collision. + // + // This may not be true in the future if the way the QPalette::cacheKey() is + // generated changes. If that happens, change to use the definition of + // `fastfragile_hash_qpalette` below, which is less likely to collide with an + // arbitrarily numbered key but also does more work. +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + x.u = x.u ^ (static_cast(p.currentColorGroup()) << (64 - 3)); + return x.u; +#else + // Use this definition here if the contents/layout of QPalette::cacheKey() + // (as in, the C++ code in qpalette.cpp) are changed. We'll also put a Qt6 + // guard for it, so that it will default to a more safe definition on the + // next guaranteed big breaking change for Qt. A warning will hopefully get + // someone to double-check it at some point in the future. +#warning "Verify contents and layout of QPalette::cacheKey() have not changed" + QtPrivate::QHashCombine c; + uint h = qHash(p.currentColorGroup()); + h = c(h, (uint)(x.u & 0xFFFFFFFFu)); + h = c(h, (uint)((x.u >> 32) & 0xFFFFFFFFu)); + return h; +#endif + } + + // This hash function is for when we want an actual accurate hash of a + // QPalette. QPalette's cacheKey() isn't very reliable -- it seems to change to + // a new random number whenever it's modified, with the exception of the + // currentColorGroup being changed. This kind of sucks for us, because it means + // two QPalette's can have the same contents but hash to different values. And + // this actually happens a lot! We'll do the hashing ourselves. Also, we're not + // interested in all of the colors, only some of them, and we ignore + // pens/brushes. + uint accurate_hash_qpalette(const QPalette& p) + { + // Probably shouldn't use this, could replace with our own guy. It's not a + // great hasher anyway. + QtPrivate::QHashCombine c; + uint h = qHash(p.currentColorGroup()); + QPalette::ColorRole const roles[] = {QPalette::Window, + QPalette::Button, + QPalette::Base, + QPalette::Text, + QPalette::WindowText, + QPalette::Highlight, + QPalette::HighlightedText}; + for (auto role : roles) { + h = c(h, p.color(role).rgb()); + } + return h; + } + + Q_NEVER_INLINE PhSwatchPtr + deep_getCachedSwatchOfQPalette(PhSwatchCache* cache, + int cacheCount, // Just saving a call to cache->count() + const QPalette& qpalette) + { + // Calculate our hash key from the QPalette's current ColorGroup and the + // actual RGBA values that we use. We have to mix the ColorGroup in + // ourselves, because QPalette does not account for it in the cache key. + uint key = accurate_hash_qpalette(qpalette); + int n = cacheCount; + int idx = -1; + for (int i = 0; i < n; ++i) { + const auto& x = cache->at(i); + if (x.first == key) { + idx = i; + break; + } + } + if (idx == -1) { + PhSwatchPtr ptr; + if (n < Num_ColorCacheEntries) { + ptr = new PhSwatch; + } else { + // Remove the oldest guy from the cache. Remember that because we may + // re-enter QStyle functions multiple times when drawing or calculating + // something, we may have to load several swaitches derived from + // different QPalettes on different stack frames at the same time. But as + // an extra cost-savings measure, we'll check and see if something else + // has a reference to the removed guy. If there aren't any references to + // it, then we'll re-use it directly instead of allocating a new one. (We + // will only ever run into the case where we can't re-use it directly if + // some other stack frame has a reference to it.) This is nice because + // then the QPens and QBrushes don't all also have to reallocate their d + // ptr stuff. + ptr = cache->last().second; + cache->removeLast(); + ptr.detach(); + } + ptr->loadFromQPalette(qpalette); + cache->prepend(PhCacheEntry(key, ptr)); + return ptr; + } else { + if (idx == 0) { + return cache->at(idx).second; + } + PhCacheEntry e = cache->at(idx); + // Using std::move from algorithm could be more efficient here, but I don't + // want to depend on algorithm or write this myself. Small N with a movable + // type means it doesn't really matter in this case. + cache->remove(idx); + cache->prepend(e); + return e.second; + } + } + + Q_NEVER_INLINE PhSwatchPtr + getCachedSwatchOfQPalette(PhSwatchCache* cache, + quint64* headSwatchFastKey, // Optimistic fast-path quick hash key + const QPalette& qpalette) + { + quint64 ck = fastfragile_hash_qpalette(qpalette); + int cacheCount = cache->count(); + // This hint is counter-productive if we're being called in a way that + // interleaves different QPalettes. But misses to this optimistic path were + // rare in my tests. (Probably not going to amount to any significant + // difference, anyway.) + if (Q_LIKELY(cacheCount > 0 && *headSwatchFastKey == ck)) { + return cache->at(0).second; + } + *headSwatchFastKey = ck; + return deep_getCachedSwatchOfQPalette(cache, cacheCount, qpalette); + } + + } // namespace +} // namespace Phantom + +class BaseStylePrivate +{ +public: + BaseStylePrivate(); + + // A fast'n'easy hash of QPalette::cacheKey()+QPalette::currentColorGroup() + // of only the head element of swatchCache list. The most common thing that + // happens when deriving a PhSwatch from a QPalette is that we just end up + // re-using the last one that we used. For that case, we can potentially save + // calling `accurate_hash_qpalette()` and instead use the value returned by + // QPalette::cacheKey() (and QPalette::currentColorGroup()) and compare it to + // the last one that we used. If it matches, then we know we can just use the + // head of the cache list without having to do any further checks, which + // saves a few hundred (!) nanoseconds. + // + // However, the `QPalette::cacheKey()` value is fragile and may change even + // if none of the colors in the QPalette have changed. In other words, all of + // the colors in a QPalette may match another QPalette (or a derived + // PhSwatch) even if the `QPalette::cacheKey()` value is different. + // + // So if `QPalette::cacheKey()+currentColorGroup()` doesn't match, then we'll + // use our more accurate `accurate_hash_qpalette()` to get a more accurate + // comparison key, and then search through the cache list to find a matching + // cached PhSwatch. (The more accurate cache key is what we store alongside + // each PhSwatch element, as the `.first` in each QPair. The + // QPalette::cacheKey() that we associate with the PhSwatch in the head + // position, `headSwatchFastKey`, is only stored for our single head element, + // as a special fast case.) If we find it, we'll move it to the head of the + // cache list. If not, we'll make a new one, and put it at the head. Either + // way, the `headSwatchFastKey` will be updated to the + // `fastfragile_qpalette_hash()` of the QPalette that we needed to derive a + // PhSwatch from, so that if we get called with the same QPalette again next + // time (which is probably going to be the case), it'll match and we can take + // the fast path. + quint64 headSwatchFastKey; + + Phantom::PhSwatchCache swatchCache; + QPen checkBox_pen_scratch; +}; + +namespace Phantom +{ + namespace + { + + // Minimal QPainter save/restore just for pen, brush, and AA render hint. If + // you touch more than that, this won't help you. But if you're only touching + // those things, this will save you some typing from manually storing/saving + // those properties each time. + struct PSave final + { + Q_DISABLE_COPY(PSave) + + explicit PSave(QPainter* painter_) + { + Q_ASSERT(painter_); + painter = painter_; + pen = painter_->pen(); + brush = painter_->brush(); + hintAA = painter_->testRenderHint(QPainter::Antialiasing); + } + Q_NEVER_INLINE void restore() + { + QPainter* p = painter; + if (!p) + return; + bool hintAA_ = hintAA; + // QPainter will check both pen and brush for equality when setting, so we + // should set it unconditionally here. + p->setPen(pen); + p->setBrush(brush); + // But it won't check the render hint to guard against doing extra work. + // We'll do that ourselves. (Though at least for the raster engine, this + // doesn't cause very much work to occur. But it still chases a few + // pointers.) + if (p->testRenderHint(QPainter::Antialiasing) != hintAA_) { + p->setRenderHint(QPainter::Antialiasing, hintAA_); + } + painter = nullptr; + pen = QPen(); + brush = QBrush(); + hintAA = false; + } + ~PSave() + { + restore(); + } + + private: + QPainter* painter; + QPen pen; + QBrush brush; + bool hintAA; + }; + + const qreal Pi = M_PI; + + qreal dpiScaled(qreal value) + { +#ifdef Q_OS_MAC + // On mac the DPI is always 72 so we should not scale it + return value; +#else + const qreal scale = qt_defaultDpiX() / 96.0; + return value * scale; +#endif + } + + struct MenuItemMetrics + { + int fontHeight; + int frameThickness; + int leftMargin; + int rightMarginForText; + int rightMarginForArrow; + int topMargin; + int bottomMargin; + int checkWidth; + int checkRightSpace; + int iconRightSpace; + int mnemonicSpace; + int arrowSpace; + int arrowWidth; + int separatorHeight; + int totalHeight; + + static MenuItemMetrics ofFontHeight(int fontHeight); + + private: + MenuItemMetrics() + { + } + }; + + MenuItemMetrics MenuItemMetrics::ofFontHeight(int fontHeight) + { + MenuItemMetrics m; + m.fontHeight = fontHeight; + m.frameThickness = dpiScaled(1.0); + m.leftMargin = static_cast(fontHeight * MenuItem_LeftMarginFontRatio); + m.rightMarginForText = static_cast(fontHeight * MenuItem_RightMarginForTextFontRatio); + m.rightMarginForArrow = static_cast(fontHeight * MenuItem_RightMarginForArrowFontRatio); + m.topMargin = static_cast(fontHeight * MenuItem_VerticalMarginsFontRatio); + m.bottomMargin = static_cast(fontHeight * MenuItem_VerticalMarginsFontRatio); + int checkVMargin = static_cast(fontHeight * MenuItem_CheckMarkVerticalInsetFontRatio); + int checkHeight = fontHeight - checkVMargin * 2; + if (checkHeight < 0) + checkHeight = 0; + m.checkWidth = static_cast(checkHeight * CheckMark_WidthOfHeightScale); + m.checkRightSpace = static_cast(fontHeight * MenuItem_CheckRightSpaceFontRatio); + m.iconRightSpace = static_cast(fontHeight * MenuItem_IconRightSpaceFontRatio); + m.mnemonicSpace = static_cast(fontHeight * MenuItem_TextMnemonicSpaceFontRatio); + m.arrowSpace = static_cast(fontHeight * MenuItem_SubMenuArrowSpaceFontRatio); + m.arrowWidth = static_cast(fontHeight * MenuItem_SubMenuArrowWidthFontRatio); + m.separatorHeight = static_cast(fontHeight * MenuItem_SeparatorHeightFontRatio); + // Odd numbers only + m.separatorHeight = (m.separatorHeight / 2) * 2 + 1; + m.totalHeight = fontHeight + m.frameThickness * 2 + m.topMargin + m.bottomMargin; + return m; + } + + QRect menuItemContentRect(const MenuItemMetrics& metrics, QRect itemRect, bool hasArrow) + { + QRect r = itemRect; + int ft = metrics.frameThickness; + int rm = hasArrow ? metrics.rightMarginForArrow : metrics.rightMarginForText; + r.adjust(ft + metrics.leftMargin, ft + metrics.topMargin, -(ft + rm), -(ft + metrics.bottomMargin)); + return r.isValid() ? r : QRect(); + } + QRect + menuItemCheckRect(const MenuItemMetrics& metrics, Qt::LayoutDirection direction, QRect itemRect, bool hasArrow) + { + QRect r = menuItemContentRect(metrics, itemRect, hasArrow); + int checkVMargin = static_cast(metrics.fontHeight * MenuItem_CheckMarkVerticalInsetFontRatio); + if (checkVMargin < 0) + checkVMargin = 0; + r.setSize(QSize(metrics.checkWidth, metrics.fontHeight)); + r.adjust(0, checkVMargin, 0, -checkVMargin); + return QStyle::visualRect(direction, itemRect, r) & itemRect; + } + QRect + menuItemIconRect(const MenuItemMetrics& metrics, Qt::LayoutDirection direction, QRect itemRect, bool hasArrow) + { + QRect r = menuItemContentRect(metrics, itemRect, hasArrow); + r.setX(r.x() + metrics.checkWidth + metrics.checkRightSpace); + r.setSize(QSize(metrics.fontHeight, metrics.fontHeight)); + return QStyle::visualRect(direction, itemRect, r) & itemRect; + } + QRect menuItemTextRect(const MenuItemMetrics& metrics, + Qt::LayoutDirection direction, + QRect itemRect, + bool hasArrow, + bool hasIcon, + int tabWidth) + { + QRect r = menuItemContentRect(metrics, itemRect, hasArrow); + r.setX(r.x() + metrics.checkWidth + metrics.checkRightSpace); + if (hasIcon) { + r.setX(r.x() + metrics.fontHeight + metrics.iconRightSpace); + } + r.setWidth(r.width() - tabWidth); + r.setHeight(metrics.fontHeight); + r &= itemRect; + return QStyle::visualRect(direction, itemRect, r); + } + QRect menuItemMnemonicRect(const MenuItemMetrics& metrics, + Qt::LayoutDirection direction, + QRect itemRect, + bool hasArrow, + int tabWidth) + { + QRect r = menuItemContentRect(metrics, itemRect, hasArrow); + int x = r.x() + r.width() - tabWidth; + if (hasArrow) + x -= metrics.arrowSpace + metrics.arrowWidth; + r.setX(x); + r.setHeight(metrics.fontHeight); + r &= itemRect; + return QStyle::visualRect(direction, itemRect, r); + } + QRect menuItemArrowRect(const MenuItemMetrics& metrics, Qt::LayoutDirection direction, QRect itemRect) + { + QRect r = menuItemContentRect(metrics, itemRect, true); + int x = r.x() + r.width() - metrics.arrowWidth; + r.setX(x); + r &= itemRect; + return QStyle::visualRect(direction, itemRect, r); + } + + Q_NEVER_INLINE + void progressBarFillRects(const QStyleOptionProgressBar* bar, + // The rect that represents the filled/completed region + QRect& outFilled, + // The rect that represents the incomplete region + QRect& outNonFilled, + // Whether or not the progress bar is indeterminate + bool& outIsIndeterminate) + { + QRect ra = bar->rect; + QRect rb = ra; + bool isHorizontal = bar->orientation != Qt::Vertical; + bool isInverted = bar->invertedAppearance; + bool isIndeterminate = bar->minimum == 0 && bar->maximum == 0; + bool isForward = !isHorizontal || bar->direction != Qt::RightToLeft; + if (isInverted) + isForward = !isForward; + int maxLen = isHorizontal ? ra.width() : ra.height(); + const auto availSteps = qMax(Q_INT64_C(1), qint64(bar->maximum) - bar->minimum); + const auto progress = qMax(bar->progress, bar->minimum); // workaround for bug in QProgressBar + const auto progressSteps = qint64(progress) - bar->minimum; + const auto progressBarWidth = progressSteps * maxLen / availSteps; + int barLen = isIndeterminate ? maxLen : progressBarWidth; + if (isHorizontal) { + if (isForward) { + ra.setWidth(barLen); + rb.setX(barLen); + } else { + ra.setX(ra.x() + ra.width() - barLen); + rb.setWidth(rb.width() - barLen); + } + } else { + if (isForward) { + ra.setY(ra.y() + ra.height() - barLen); + rb.setHeight(rb.height() - barLen); + } else { + ra.setHeight(barLen); + rb.setY(barLen); + } + } + outFilled = ra; + outNonFilled = rb; + outIsIndeterminate = isIndeterminate; + } + + int calcBigLineSize(int radius) + { + int bigLineSize = radius / 6; + if (bigLineSize < 4) + bigLineSize = 4; + if (bigLineSize > radius / 2) + bigLineSize = radius / 2; + return bigLineSize; + } + Q_NEVER_INLINE QPointF calcRadialPos(const QStyleOptionSlider* dial, qreal offset) + { + const int width = dial->rect.width(); + const int height = dial->rect.height(); + const int r = qMin(width, height) / 2; + const int currentSliderPosition = + dial->upsideDown ? dial->sliderPosition : (dial->maximum - dial->sliderPosition); + qreal a = 0; + if (dial->maximum == dial->minimum) + a = Pi / 2; + else if (dial->dialWrapping) + a = Pi * 3 / 2 - (currentSliderPosition - dial->minimum) * 2 * Pi / (dial->maximum - dial->minimum); + else + a = (Pi * 8 - (currentSliderPosition - dial->minimum) * 10 * Pi / (dial->maximum - dial->minimum)) / 6; + qreal xc = width / 2.0; + qreal yc = height / 2.0; + qreal len = r - calcBigLineSize(r) - 3; + qreal back = offset * len; + QPointF pos(QPointF(xc + back * qCos(a), yc - back * qSin(a))); + return pos; + } + Q_NEVER_INLINE QPolygonF calcLines(const QStyleOptionSlider* dial) + { + QPolygonF poly; + qreal width = dial->rect.width(); + qreal height = dial->rect.height(); + qreal r = qMin(width, height) / 2.0; + int bigLineSize = calcBigLineSize(r); + + qreal xc = width / 2.0 + 0.5; + qreal yc = height / 2.0 + 0.5; + const int ns = dial->tickInterval; + if (!ns) // Invalid values may be set by Qt Designer. + return poly; + int notches = (dial->maximum + ns - 1 - dial->minimum) / ns; + if (notches <= 0) + return poly; + if (dial->maximum < dial->minimum || dial->maximum - dial->minimum > 1000) { + int maximum = dial->minimum + 1000; + notches = (maximum + ns - 1 - dial->minimum) / ns; + } + poly.resize(2 + 2 * notches); + int smallLineSize = bigLineSize / 2; + for (int i = 0; i <= notches; ++i) { + qreal angle = + dial->dialWrapping ? Pi * 3 / 2 - i * 2 * Pi / notches : (Pi * 8 - i * 10 * Pi / notches) / 6; + qreal s = qSin(angle); + qreal c = qCos(angle); + if (i == 0 || (((ns * i) % (dial->pageStep ? dial->pageStep : 1)) == 0)) { + poly[2 * i] = QPointF(xc + (r - bigLineSize) * c, yc - (r - bigLineSize) * s); + poly[2 * i + 1] = QPointF(xc + r * c, yc - r * s); + } else { + poly[2 * i] = QPointF(xc + (r - 1 - smallLineSize) * c, yc - (r - 1 - smallLineSize) * s); + poly[2 * i + 1] = QPointF(xc + (r - 1) * c, yc - (r - 1) * s); + } + } + return poly; + } + // This will draw a nice and shiny QDial for us. We don't want + // all the shinyness in QWindowsStyle, hence we place it here + Q_NEVER_INLINE void drawDial(const QStyleOptionSlider* option, QPainter* painter) + { + namespace Dc = Phantom::DeriveColors; + const QPalette& pal = option->palette; + QColor buttonColor = Dc::buttonColor(option->palette); + const int width = option->rect.width(); + const int height = option->rect.height(); + const bool enabled = option->state & QStyle::State_Enabled; + qreal r = qMin(width, height) / 2.0; + r -= r / 50.0; + painter->save(); + painter->setRenderHint(QPainter::Antialiasing); + // Draw notches + if (option->subControls & QStyle::SC_DialTickmarks) { + painter->setPen(pal.color(QPalette::Disabled, QPalette::Text)); + painter->drawLines(calcLines(option)); + } + const qreal d_ = r / 6; + const qreal dx = option->rect.x() + d_ + (width - 2 * r) / 2 + 1; + const qreal dy = option->rect.y() + d_ + (height - 2 * r) / 2 + 1; + QRectF br = QRectF(dx + 0.5, dy + 0.5, int(r * 2 - 2 * d_ - 2), int(r * 2 - 2 * d_ - 2)); + if (enabled) { + painter->setBrush(buttonColor); + } else { + painter->setBrush(Qt::NoBrush); + } + painter->setPen(Dc::outlineOf(option->palette)); + painter->drawEllipse(br); + painter->setBrush(Qt::NoBrush); + painter->setPen(Dc::specularOf(buttonColor)); + painter->drawEllipse(br.adjusted(1, 1, -1, -1)); + if (option->state & QStyle::State_HasFocus) { + QColor highlight = pal.highlight().color(); + highlight.setHsv(highlight.hue(), qMin(160, highlight.saturation()), qMax(230, highlight.value())); + highlight.setAlpha(127); + painter->setPen(QPen(highlight, 2.0)); + painter->setBrush(Qt::NoBrush); + painter->drawEllipse(br.adjusted(-1, -1, 1, 1)); + } + QPointF dp = calcRadialPos(option, 0.70); + const qreal ds = r / 7.0; + QRectF dialRect(dp.x() - ds, dp.y() - ds, 2 * ds, 2 * ds); + painter->setBrush(option->palette.color(QPalette::Window)); + painter->setPen(Dc::outlineOf(option->palette)); + painter->drawEllipse(dialRect.adjusted(-1, -1, 1, 1)); + painter->restore(); + } + + int fontMetricsWidth(const QFontMetrics& fontMetrics, const QString& text) + { +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + return fontMetrics.width(text, text.size(), Qt::TextBypassShaping); +#else + return fontMetrics.horizontalAdvance(text); +#endif + } + + // This always draws the arrow with the correct aspect ratio, even if the + // provided bounding rect is non-square. The base edge of the triangle is + // snapped to a whole pixel to avoid anti-aliasing making it look soft. + // + // Expected time (release): 5usecs for regular-sized arrows + Q_NEVER_INLINE void drawArrow(QPainter* p, QRect rect, Qt::ArrowType arrowDirection, const QBrush& brush) + { + const qreal ArrowBaseRatio = 0.70; + qreal irx, iry, irw, irh; + QRectF(rect).getRect(&irx, &iry, &irw, &irh); + if (irw < 1.0 || irh < 1.0) + return; + qreal dw, dh; + if (arrowDirection == Qt::LeftArrow || arrowDirection == Qt::RightArrow) { + dw = ArrowBaseRatio; + dh = 1.0; + } else { + dw = 1.0; + dh = ArrowBaseRatio; + } + QSizeF sz = QSizeF(dw, dh).scaled(irw, irh, Qt::KeepAspectRatio); + qreal aw = sz.width(); + qreal ah = sz.height(); + qreal ax, ay; + ax = irx + (irw - aw) / 2; + ay = iry + (irh - ah) / 2; + QRectF arrowRect(ax, ay, aw, ah); + QPointF points[3]; + switch (arrowDirection) { + case Qt::DownArrow: + arrowRect.setTop(std::round(arrowRect.top())); + points[0] = arrowRect.topLeft(); + points[1] = arrowRect.topRight(); + points[2] = QPointF(arrowRect.center().x(), arrowRect.bottom()); + break; + case Qt::RightArrow: { + arrowRect.setLeft(std::round(arrowRect.left())); + points[0] = arrowRect.topLeft(); + points[1] = arrowRect.bottomLeft(); + points[2] = QPointF(arrowRect.right(), arrowRect.center().y()); + break; + } + case Qt::LeftArrow: + arrowRect.setRight(std::round(arrowRect.right())); + points[0] = arrowRect.topRight(); + points[1] = arrowRect.bottomRight(); + points[2] = QPointF(arrowRect.left(), arrowRect.center().y()); + break; + case Qt::UpArrow: + default: + arrowRect.setBottom(std::round(arrowRect.bottom())); + points[0] = arrowRect.bottomLeft(); + points[1] = arrowRect.bottomRight(); + points[2] = QPointF(arrowRect.center().x(), arrowRect.top()); + break; + } + auto oldPen = p->pen(); + auto oldBrush = p->brush(); + bool oldAA = p->testRenderHint(QPainter::Antialiasing); + p->setPen(Qt::NoPen); + p->setBrush(brush); + if (!oldAA) { + p->setRenderHint(QPainter::Antialiasing); + } + p->drawConvexPolygon(points, 3); + p->setPen(oldPen); + p->setBrush(oldBrush); + if (!oldAA) { + p->setRenderHint(QPainter::Antialiasing, false); + } + } + + // Pass allowEnabled as false to always draw the arrow with the disabled color, + // even if the underlying palette's current color group is not disabled. Useful + // for parts of widgets which may want to be drawn as disabled even if the + // actual widget is not set as disabled, such as scrollbar step buttons when + // the scrollbar has no movable range. + Q_NEVER_INLINE void drawArrow(QPainter* painter, + QRect rect, + Qt::ArrowType type, + const PhSwatch& swatch, + bool allowEnabled = true, + qreal lightnessAdjustment = 0.0) + { + if (rect.isEmpty()) + return; + using namespace SwatchColors; + auto brush = swatch.brush(allowEnabled ? S_indicator_current : S_indicator_disabled); + brush.setColor(DeriveColors::adjustLightness(brush.color(), lightnessAdjustment)); + Phantom::drawArrow(painter, rect, type, brush); + } + + // This draws exactly within the rect provided. If you provide a square rect, + // it will appear too wide -- you probably want to shrink the width of your + // square first by multiplying it with CheckMark_WidthOfHeightScale. + Q_NEVER_INLINE void + drawCheck(QPainter* painter, QPen& scratchPen, const QRectF& r, const PhSwatch& swatch, Swatchy color) + { + using namespace Phantom::SwatchColors; + qreal rx, ry, rw, rh; + QRectF(r).getRect(&rx, &ry, &rw, &rh); + qreal penWidth = 0.25 * qMin(rw, rh); + qreal dimx = rw - penWidth; + qreal dimy = rh - penWidth; + if (dimx < 0.5 || dimy < 0.5) + return; + qreal x = (rw - dimx) / 2 + rx; + qreal y = (rh - dimy) / 2 + ry; + QPointF points[3]; + points[0] = QPointF(0.0, 0.55); + points[1] = QPointF(0.4, 1.0); + points[2] = QPointF(1.0, 0); + for (int i = 0; i < 3; ++i) { + QPointF pnt = points[i]; + pnt.setX(pnt.x() * dimx + x); + pnt.setY(pnt.y() * dimy + y); + points[i] = pnt; + } + scratchPen.setBrush(swatch.brush(color)); + scratchPen.setCapStyle(Qt::RoundCap); + scratchPen.setJoinStyle(Qt::RoundJoin); + scratchPen.setWidthF(penWidth); + Phantom::PSave save(painter); + if (!painter->testRenderHint(QPainter::Antialiasing)) + painter->setRenderHint(QPainter::Antialiasing); + painter->setPen(scratchPen); + painter->setBrush(Qt::NoBrush); + painter->drawPolyline(points, 3); + } + + Q_NEVER_INLINE void + drawHyphen(QPainter* painter, QPen& scratchPen, const QRectF& r, const PhSwatch& swatch, Swatchy color) + { + using namespace Phantom::SwatchColors; + qreal rx, ry, rw, rh; + QRectF(r).getRect(&rx, &ry, &rw, &rh); + qreal penWidth = 0.25 * qMin(rw, rh); + qreal dimx = rw - penWidth; + qreal dimy = rh - penWidth; + if (dimx < 0.5 || dimy < 0.5) + return; + qreal x = (rw - dimx) / 2 + rx; + qreal y = (rh - dimy) / 2 + ry; + QPointF p0(0.0 * dimx + x, 0.5 * dimy + y); + QPointF p1(1.0 * dimx + x, 0.5 * dimy + y); + scratchPen.setBrush(swatch.brush(color)); + scratchPen.setCapStyle(Qt::RoundCap); + scratchPen.setWidthF(penWidth); + Phantom::PSave save(painter); + if (!painter->testRenderHint(QPainter::Antialiasing)) + painter->setRenderHint(QPainter::Antialiasing); + painter->setPen(scratchPen); + painter->setBrush(Qt::NoBrush); + painter->drawLine(p0, p1); + } + + Q_NEVER_INLINE void + drawMdiButton(QPainter* painter, const QStyleOptionTitleBar* option, QRect tmp, bool hover, bool sunken) + { + QColor dark; + dark.setHsv(option->palette.button().color().hue(), + qMin(255, (option->palette.button().color().saturation())), + qMin(255, option->palette.button().color().value() * 0.7)); + QColor highlight = option->palette.highlight().color(); + bool active = (option->titleBarState & QStyle::State_Active); + QColor titleBarHighlight(255, 255, 255, 60); + if (sunken) + painter->fillRect(tmp.adjusted(1, 1, -1, -1), option->palette.highlight().color().darker(120)); + else if (hover) + painter->fillRect(tmp.adjusted(1, 1, -1, -1), QColor(255, 255, 255, 20)); + if (sunken) + titleBarHighlight = highlight.darker(130); + QColor mdiButtonBorderColor(active ? option->palette.highlight().color().darker(180) : dark.darker(110)); + painter->setPen(QPen(mdiButtonBorderColor)); + const QLine lines[4] = {QLine(tmp.left() + 2, tmp.top(), tmp.right() - 2, tmp.top()), + QLine(tmp.left() + 2, tmp.bottom(), tmp.right() - 2, tmp.bottom()), + QLine(tmp.left(), tmp.top() + 2, tmp.left(), tmp.bottom() - 2), + QLine(tmp.right(), tmp.top() + 2, tmp.right(), tmp.bottom() - 2)}; + painter->drawLines(lines, 4); + const QPoint points[4] = {QPoint(tmp.left() + 1, tmp.top() + 1), + QPoint(tmp.right() - 1, tmp.top() + 1), + QPoint(tmp.left() + 1, tmp.bottom() - 1), + QPoint(tmp.right() - 1, tmp.bottom() - 1)}; + painter->drawPoints(points, 4); + painter->setPen(titleBarHighlight); + painter->drawLine(tmp.left() + 2, tmp.top() + 1, tmp.right() - 2, tmp.top() + 1); + painter->drawLine(tmp.left() + 1, tmp.top() + 2, tmp.left() + 1, tmp.bottom() - 2); + } + + Q_NEVER_INLINE void fillRectOutline(QPainter* p, QRect rect, QMargins margins, const QColor& brush) + { + int x, y, w, h; + rect.getRect(&x, &y, &w, &h); + int ml = margins.left(); + int mt = margins.top(); + int mr = margins.right(); + int mb = margins.bottom(); + QRect r0(x, y, w, mt); + QRect r1(x, y + mt, ml, h - (mt + mb)); + QRect r2((x + w) - mr, y + mt, mr, h - (mt + mb)); + QRect r3(x, (y + h) - mb, w, mb); + p->fillRect(r0 & rect, brush); + p->fillRect(r1 & rect, brush); + p->fillRect(r2 & rect, brush); + p->fillRect(r3 & rect, brush); + } + void fillRectOutline(QPainter* p, QRect rect, int thickness, const QColor& color) + { + fillRectOutline(p, rect, QMargins(thickness, thickness, thickness, thickness), color); + } + Q_NEVER_INLINE void + fillRectEdges(QPainter* p, QRect rect, Qt::Edges edges, QMargins margins, const QColor& color) + { + int x, y, w, h; + rect.getRect(&x, &y, &w, &h); + if (edges & Qt::LeftEdge) { + int ml = margins.left(); + QRect r0(x, y, ml, h); + p->fillRect(r0 & rect, color); + } + if (edges & Qt::TopEdge) { + int mt = margins.top(); + QRect r1(x, y, w, mt); + p->fillRect(r1 & rect, color); + } + if (edges & Qt::RightEdge) { + int mr = margins.right(); + QRect r2((x + w) - mr, y, mr, h); + p->fillRect(r2 & rect, color); + } + if (edges & Qt::BottomEdge) { + int mb = margins.bottom(); + QRect r3(x, (y + h) - mb, w, mb); + p->fillRect(r3 & rect, color); + } + } + void fillRectEdges(QPainter* p, QRect rect, Qt::Edges edges, int thickness, const QColor& color) + { + fillRectEdges(p, rect, edges, QMargins(thickness, thickness, thickness, thickness), color); + } + inline QRect expandRect(QRect rect, Qt::Edges edges, int delta) + { + int l = edges & Qt::LeftEdge ? -delta : 0; + int t = edges & Qt::TopEdge ? -delta : 0; + int r = edges & Qt::RightEdge ? delta : 0; + int b = edges & Qt::BottomEdge ? delta : 0; + return rect.adjusted(l, t, r, b); + } + inline Qt::Edge oppositeEdge(Qt::Edge edge) + { + switch (edge) { + case Qt::LeftEdge: + return Qt::RightEdge; + case Qt::TopEdge: + return Qt::BottomEdge; + case Qt::RightEdge: + return Qt::LeftEdge; + case Qt::BottomEdge: + return Qt::TopEdge; + } + return Qt::TopEdge; + } + inline QRect rectTranslatedTowardEdge(QRect rect, Qt::Edge edge, int delta) + { + switch (edge) { + case Qt::LeftEdge: + return rect.translated(-delta, 0); + case Qt::TopEdge: + return rect.translated(0, -delta); + case Qt::RightEdge: + return rect.translated(delta, 0); + case Qt::BottomEdge: + return rect.translated(0, delta); + } + return rect; + } + Q_NEVER_INLINE QRect rectFromInnerEdgeWithThickness(QRect rect, Qt::Edge edge, int thickness) + { + int x, y, w, h; + rect.getRect(&x, &y, &w, &h); + QRect r; + switch (edge) { + case Qt::LeftEdge: + r = QRect(x, y, thickness, h); + break; + case Qt::TopEdge: + r = QRect(x, y, w, thickness); + break; + case Qt::RightEdge: + r = QRect((x + w) - thickness, y, thickness, h); + break; + case Qt::BottomEdge: + r = QRect(x, (y + h) - thickness, w, thickness); + break; + } + return r & rect; + } + Q_NEVER_INLINE void + paintSolidRoundRect(QPainter* p, QRect rect, qreal radius, const PhSwatch& swatch, Swatchy fill) + { + if (!fill) + return; + bool aa = p->testRenderHint(QPainter::Antialiasing); + if (radius > 0.5) { + if (!aa) + p->setRenderHint(QPainter::Antialiasing); + p->setPen(swatch.pen(SwatchColors::S_none)); + p->setBrush(swatch.brush(fill)); + p->drawRoundedRect(rect, radius, radius); + } else { + if (aa) + p->setRenderHint(QPainter::Antialiasing, false); + p->fillRect(rect, swatch.color(fill)); + } + } + Q_NEVER_INLINE void paintBorderedRoundRect(QPainter* p, + QRect rect, + qreal radius, + const PhSwatch& swatch, + Swatchy stroke, + Swatchy fill) + { + if (rect.width() < 1 || rect.height() < 1) + return; + if (!stroke && !fill) + return; + bool aa = p->testRenderHint(QPainter::Antialiasing); + if (radius > 0.5) { + if (!aa) + p->setRenderHint(QPainter::Antialiasing); + p->setPen(swatch.pen(stroke)); + p->setBrush(swatch.brush(fill)); + QRectF rf(rect.x() + 0.5, rect.y() + 0.5, rect.width() - 1.0, rect.height() - 1.0); + p->drawRoundedRect(rf, radius, radius); + } else { + if (aa) + p->setRenderHint(QPainter::Antialiasing, false); + if (stroke) { + fillRectOutline(p, rect, 1, swatch.color(stroke)); + } + if (fill) { + p->fillRect(rect.adjusted(1, 1, -1, -1), swatch.color(fill)); + } + } + } + } // namespace +} // namespace Phantom + +BaseStylePrivate::BaseStylePrivate() + : headSwatchFastKey(0) +{ +} + +BaseStyle::BaseStyle() + : d(new BaseStylePrivate), + m_shadowHelper(new ShadowHelper(this)), + m_blurHelper(new BlurHelper(this)) +{ + setObjectName(QLatin1String("Phantom")); + + m_shadowHelper->setFrameRadius(Phantom::DefaultFrame_Radius); +} + +BaseStyle::~BaseStyle() +{ + delete d; +} + +// Draw text in a rectangle. The current pen set on the painter is used, unless +// an explicit textRole is set, in which case the palette will be used. The +// enabled bool indicates whether the text is enabled or not, and can influence +// how the text is drawn outside of just color. Wrapping and alignment flags +// can be passed in `alignment`. +void BaseStyle::drawItemText(QPainter* painter, + const QRect& rect, + int alignment, + const QPalette& pal, + bool enabled, + const QString& text, + QPalette::ColorRole textRole) const +{ + Q_UNUSED(enabled); + if (text.isEmpty()) + return; + if (textRole == QPalette::NoRole) { + painter->drawText(rect, alignment, text); + return; + } + QPen savedPen = painter->pen(); + const QBrush& newBrush = pal.brush(textRole); + bool changed = false; + if (savedPen.brush() != newBrush) { + changed = true; + painter->setPen(QPen(newBrush, savedPen.widthF())); + } + painter->drawText(rect, alignment, text); + if (changed) { + painter->setPen(savedPen); + } +} + +void BaseStyle::drawPrimitive(PrimitiveElement elem, + const QStyleOption* option, + QPainter* painter, + const QWidget* widget) const +{ + Q_ASSERT(option); + if (!option) + return; +#ifdef BUILD_WITH_EASY_PROFILER + EASY_BLOCK("drawPrimitive"); + const char* elemCString = QMetaEnum::fromType().valueToKey(elem); + EASY_TEXT("Element", elemCString); +#endif + using Swatchy = Phantom::Swatchy; + using namespace Phantom::SwatchColors; + namespace Ph = Phantom; + auto ph_swatchPtr = getCachedSwatchOfQPalette(&d->swatchCache, &d->headSwatchFastKey, option->palette); + const Ph::PhSwatch& swatch = *ph_swatchPtr.data(); + const int state = option->state; + // Cast to int here to suppress warnings about cases listed which are not in + // the original enum. This is for custom primitive elements. + switch (static_cast(elem)) { + case PE_Frame: { + if (widget && widget->inherits("QComboBoxPrivateContainer")) { + QStyleOption copy = *option; + copy.state |= State_Raised; + proxy()->drawPrimitive(PE_PanelMenu, ©, painter, widget); + break; + } + Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_frame_outline)); + break; + } + case PE_FrameMenu: { + break; + } + case PE_FrameDockWidget: { + painter->save(); + QColor softshadow = option->palette.window().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.window().color().darker(120))); + painter->drawLine(QPoint(r.left() + 1, r.bottom() - 1), QPoint(r.right() - 2, r.bottom() - 1)); + painter->drawLine(QPoint(r.right() - 1, r.top() + 1), QPoint(r.right() - 1, r.bottom() - 1)); + painter->restore(); + break; + } + case PE_FrameGroupBox: { + QRect frame = option->rect; + Ph::PSave save(painter); + bool isFlat = false; + if (auto groupBox = qstyleoption_cast(option)) { + isFlat = groupBox->features & QStyleOptionFrame::Flat; + } else if (auto frameOpt = qstyleoption_cast(option)) { + isFlat = frameOpt->features & QStyleOptionFrame::Flat; + } + if (isFlat) { + Ph::fillRectEdges(painter, frame, Qt::TopEdge, 1, swatch.color(S_window_divider)); + } else { + Ph::paintBorderedRoundRect(painter, frame, Ph::GroupBox_Rounding, swatch, S_frame_outline, S_none); + } + break; + } + case PE_IndicatorBranch: { + if (!(option->state & State_Children)) + break; + Qt::ArrowType arrow; + if (option->state & State_Open) { + arrow = Qt::DownArrow; + } else if (option->direction != Qt::RightToLeft) { + arrow = Qt::RightArrow; + } else { + arrow = Qt::LeftArrow; + } + bool useSelectionColor = false; + if (option->state & State_Selected) { + if (auto ivopt = qstyleoption_cast(option)) { + useSelectionColor = ivopt->showDecorationSelected; + } + } + Swatchy color = useSelectionColor ? S_highlightedText : S_indicator_current; + QRect r = option->rect; + if (Ph::BranchesOnEdge) { + // TODO RTL + r.moveLeft(0); + if (r.width() < r.height()) + r.setWidth(r.height()); + } + int adj = qMin(r.width(), r.height()) / 4; + r.adjust(adj, adj, -adj, -adj); + Ph::drawArrow(painter, r, arrow, swatch.brush(color)); + break; + } + case PE_IndicatorMenuCheckMark: { + // For this PE, QCommonStyle treats State_On as drawing the check with the + // highlighted text color, and otherwise with the regular text color. I + // guess we should match that behavior, even though it's not consistent + // with other check box/mark drawing in QStyle (buttons and item view + // items.) QCommonStyle also doesn't care about tri-state or unchecked + // states -- it seems that if you call this, you want a check, and nothing + // else. + // + // We'll also catch State_Selected and treat it equivalently (the way you'd + // expect.) We'll use windowText instead of text, though -- probably + // doesn't matter. + Swatchy fgColor = S_windowText; + bool isSelected = option->state & (State_Selected | State_On); + bool isEnabled = option->state & State_Enabled; + if (isSelected) { + fgColor = S_highlightedText; + } else if (!isEnabled) { + fgColor = S_windowText_disabled; + } + qreal rx, ry, rw, rh; + QRectF(option->rect).getRect(&rx, &ry, &rw, &rh); + qreal dim = qMin(rw, rh); + const qreal insetScale = 0.8; + qreal dimx = dim * insetScale * Ph::CheckMark_WidthOfHeightScale; + qreal dimy = dim * insetScale; + QRectF r_(rx + (rw - dimx) / 2, ry + (rh - dimy) / 2, dimx, dimy); + Ph::drawCheck(painter, d->checkBox_pen_scratch, r_, swatch, fgColor); + break; + } + case PE_PanelTipLabel: { + // force registration of widget + if (widget && widget->window()) { + m_shadowHelper->registerWidget(widget->window(), true); + } + + const auto &palette = option->palette; + const auto &background = palette.color(QPalette::ToolTipBase); + const qreal radius = Phantom::DefaultFrame_Radius; + painter->save(); + painter->setRenderHint(QPainter::Antialiasing); + painter->setPen(Qt::NoPen); + painter->setBrush(background); + painter->drawRoundedRect(option->rect, radius, radius); + painter->restore(); + break; + } + // Called for the content area on tree view rows that are selected + case PE_PanelItemViewItem: { + QCommonStyle::drawPrimitive(elem, option, painter, widget); + break; + } + // Called for left-of-item-content-area on tree view rows that are selected + case PE_PanelItemViewRow: { + QCommonStyle::drawPrimitive(elem, option, painter, widget); + break; + } + case PE_FrameTabBarBase: { + auto tbb = qstyleoption_cast(option); + if (!tbb) + break; + Qt::Edge edge = Qt::TopEdge; + switch (tbb->shape) { + case QTabBar::RoundedNorth: + case QTabBar::TriangularNorth: + edge = Qt::TopEdge; + break; + case QTabBar::RoundedSouth: + case QTabBar::TriangularSouth: + edge = Qt::BottomEdge; + break; + case QTabBar::RoundedWest: + case QTabBar::TriangularWest: + edge = Qt::LeftEdge; + break; + case QTabBar::RoundedEast: + case QTabBar::TriangularEast: + edge = Qt::RightEdge; + break; + } + Ph::fillRectEdges(painter, option->rect, edge, 1, swatch.color(S_frame_outline)); + // TODO need to check here if we're drawing with window or button color as + // the frame fill. Assuming window right now, but could be wrong. + Ph::fillRectEdges(painter, Ph::expandRect(option->rect, edge, -1), edge, 1, swatch.color(S_tabFrame_specular)); + break; + } + case PE_PanelScrollAreaCorner: { + bool isLeftToRight = option->direction != Qt::RightToLeft; + Qt::Edges edges = Qt::TopEdge; + QRect bgRect = option->rect; + if (isLeftToRight) { + edges |= Qt::LeftEdge; + bgRect.setX(bgRect.x() + 1); + } else { + edges |= Qt::RightEdge; + bgRect.setWidth(bgRect.width() - 1); + } + painter->fillRect(bgRect, swatch.color(S_window)); + Ph::fillRectEdges(painter, option->rect, edges, 1, swatch.color(S_window_outline)); + break; + } + case PE_IndicatorArrowUp: + case PE_IndicatorArrowDown: + case PE_IndicatorArrowRight: + case PE_IndicatorArrowLeft: { + int rx, ry, rw, rh; + option->rect.getRect(&rx, &ry, &rw, &rh); + if (rw <= 1 || rh <= 1) + break; + Qt::ArrowType arrow = Qt::UpArrow; + switch (elem) { + case PE_IndicatorArrowUp: + arrow = Qt::UpArrow; + break; + case PE_IndicatorArrowDown: + arrow = Qt::DownArrow; + break; + case PE_IndicatorArrowRight: + arrow = Qt::RightArrow; + break; + case PE_IndicatorArrowLeft: + arrow = Qt::LeftArrow; + break; + default: + break; + } + // The caller may give us a huge rect and expect a normal-sized icon inside + // of it, so we don't want to fill the entire thing with an arrow, + // otherwise certain buttons will look weird, like the tab bar scroll + // buttons. Might want to break these out into editable parameters? + const int MaxArrowExt = Ph::dpiScaled(12); + const int MinMargin = qMin(rw, rh) / 4; + int aw, ah; + aw = qMin(MaxArrowExt, rw) - MinMargin; + ah = qMin(MaxArrowExt, rh) - MinMargin; + if (aw <= 2 || ah <= 2) + break; + // QCommonStyle's implementation of CC_ToolButton for non-instant popups + // gives us a pretty big rectangle to draw the arrow in -- shrink it. This + // is kind of a dirty temp hack thing until we do something smarter, like + // fully reimplement CC_ToolButton. Note that it passes us a regular + // QStyleOption and not a QStyleOptionToolButton in this case, so try to + // save some work before doing the inherits test. + if (arrow == Qt::DownArrow && !qstyleoption_cast(option) && widget) { + auto tbutton = qobject_cast(widget); + if (tbutton && tbutton->popupMode() != QToolButton::InstantPopup && tbutton->defaultAction()) { + int dim = static_cast(qMin(rw, rh) * 0.25); + aw -= dim; + ah -= dim; + // We have another hack in PE_IndicatorButtonDropDown where we shift + // the edge left or right by 1px to avoid having two borders touching + // (we make it overlap instead.) So we'll need to compensate for that + // in the arrow's position to avoid it looking off-center. + rw += 1; + if (option->direction != Qt::RightToLeft) { + rx -= 1; + } + } + } + aw += (rw - aw) % 2; + ah += (rh - ah) % 2; + int ax = (rw - aw) / 2 + rx; + int ay = (rh - ah) / 2 + ry; + Ph::drawArrow(painter, QRect(ax, ay, aw, ah), arrow, swatch); + break; + } + case PE_IndicatorItemViewItemCheck: { + QStyleOptionButton button; + button.QStyleOption::operator=(*option); + button.state &= ~State_MouseOver; + proxy()->drawPrimitive(PE_IndicatorCheckBox, &button, painter, widget); + return; + } + case PE_IndicatorHeaderArrow: { + auto header = qstyleoption_cast(option); + if (!header) + return; + QRect r = header->rect; + QPoint offset = QPoint(Phantom::HeaderSortIndicator_HOffset, Phantom::HeaderSortIndicator_VOffset); + qreal lightness = Phantom::DeriveColors::hack_isLightPalette(widget->palette()) ? 0.03 : 0.0; + if (header->sortIndicator & QStyleOptionHeader::SortUp) { + Ph::drawArrow(painter, r.translated(offset), Qt::DownArrow, swatch, true, lightness); + } else if (header->sortIndicator & QStyleOptionHeader::SortDown) { + Ph::drawArrow(painter, r.translated(offset), Qt::UpArrow, swatch, true, lightness); + } + 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.window().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.window().color().lighter(110))); + painter->drawLine(r.topLeft().x() + margin, + r.topLeft().y() + offset + 1, + r.topRight().x() - margin, + r.topRight().y() + offset + 1); + } + break; + } + case PE_PanelButtonTool: { + bool isDown = option->state & State_Sunken; + bool isOn = option->state & State_On; + bool hasFocus = (option->state & State_HasFocus && option->state & State_KeyboardFocusChange); + const qreal rounding = Ph::ToolButton_Rounding; + Swatchy outline = S_window_outline; + Swatchy fill = S_button; + Swatchy specular = S_button_specular; + if (isDown) { + fill = S_button_pressed; + specular = S_button_pressed_specular; + } else if (isOn) { + fill = S_button_on; + specular = S_none; + } + if (hasFocus) { + outline = S_highlight_outline; + } + QRect r = option->rect; + Ph::PSave save(painter); + Ph::paintBorderedRoundRect(painter, r, rounding, swatch, outline, fill); + Ph::paintBorderedRoundRect(painter, r.adjusted(1, 1, -1, -1), rounding, swatch, specular, S_none); + break; + } + case PE_IndicatorDockWidgetResizeHandle: { + QStyleOption dockWidgetHandle = *option; + bool horizontal = option->state & State_Horizontal; + dockWidgetHandle.state = + !horizontal ? (dockWidgetHandle.state | State_Horizontal) : (dockWidgetHandle.state & ~State_Horizontal); + proxy()->drawControl(CE_Splitter, &dockWidgetHandle, painter, widget); + break; + } + case PE_FrameWindow: { + break; + } + case PE_FrameLineEdit: { + QRect r = option->rect; + bool hasFocus = option->state & State_HasFocus; + bool isEnabled = option->state & State_Enabled; + const qreal rounding = Ph::LineEdit_Rounding; + auto pen = hasFocus ? S_highlight_outline : S_window_outline; + Ph::PSave save(painter); + Ph::paintBorderedRoundRect(painter, r, rounding, swatch, pen, S_none); + save.restore(); + if (Ph::OverhangShadows && !hasFocus && isEnabled) { + // Imperfect when rounded, may leave a gap on left and right. Going + // closer would eat into the outline, though. + Ph::fillRectEdges(painter, + r.adjusted(qRound(rounding / 2) + 1, 1, -(qRound(rounding / 2) + 1), -1), + Qt::TopEdge, + 1, + swatch.color(S_base_shadow)); + } + break; + } + case PE_PanelLineEdit: { + auto panel = qstyleoption_cast(option); + if (!panel) + break; + Ph::PSave save(painter); + // We intentionally don't inset the fill rect, even if the frame will paint + // over the perimeter, because an inset with rounding enabled may cause + // some miscolored separated pixels between the fill and the border, since + // we're forced to paint them in two separate draw calls. + + painter->setRenderHint(QPainter::Antialiasing); + painter->setPen(swatch.pen(Ph::SwatchColors::S_none)); + QColor bgColor = swatch.color(S_base); + bgColor.setAlpha(100); + painter->setBrush(bgColor); + painter->drawRoundedRect(option->rect, Ph::LineEdit_Rounding, Ph::LineEdit_Rounding); + + // Ph::paintSolidRoundRect(painter, option->rect, Ph::LineEdit_Rounding, swatch, S_base); + save.restore(); + if (panel->lineWidth > 0) + proxy()->drawPrimitive(PE_FrameLineEdit, option, painter, widget); + break; + } + case PE_IndicatorCheckBox: { + auto checkbox = qstyleoption_cast(option); + if (!checkbox) + break; + QRect r = option->rect; + bool isHighlighted = option->state & State_HasFocus && option->state & State_KeyboardFocusChange; + bool isSelected = option->state & State_Selected; + bool isFlat = checkbox->features & QStyleOptionButton::Flat; + bool isEnabled = option->state & State_Enabled; + bool isPressed = state & State_Sunken; + Swatchy outlineColor = isHighlighted ? S_highlight_outline : S_window_outline; + Swatchy bgFillColor = isPressed ? S_highlight : S_base; + Swatchy fgColor = isFlat ? S_windowText : S_text; + if (isPressed && !isFlat) { + fgColor = S_highlightedText; + } + // Bare checkmarks that are selected should draw with the highlighted text + // color. + if (isSelected && isFlat) { + fgColor = S_highlightedText; + } + if (!isFlat) { + QRect fillR = r; + Ph::fillRectOutline(painter, fillR, 1, swatch.color(outlineColor)); + fillR.adjust(1, 1, -1, -1); + if (Ph::IndicatorShadows && !isPressed && isEnabled) { + Ph::fillRectEdges(painter, fillR, Qt::TopEdge, 1, swatch.color(S_base_shadow)); + fillR.adjust(0, 1, 0, 0); + } + painter->fillRect(fillR, swatch.color(bgFillColor)); + } + if (checkbox->state & State_NoChange) { + const qreal insetScale = 0.7; + qreal rx, ry, rw, rh; + QRectF(r.adjusted(1, 1, -1, -1)).getRect(&rx, &ry, &rw, &rh); + qreal dimx = rw * insetScale; + qreal dimy = rh * insetScale; + QRectF r_(rx + (rw - dimx) / 2, ry + (rh - dimy) / 2, dimx, dimy); + Ph::drawHyphen(painter, d->checkBox_pen_scratch, r_, swatch, fgColor); + } else if (checkbox->state & State_On) { + const qreal insetScale = 0.8; + qreal rx, ry, rw, rh; + QRectF(r.adjusted(1, 1, -1, -1)).getRect(&rx, &ry, &rw, &rh); + // kinda wrong, assumes we're already square, but we probably are + qreal dimx = rw * insetScale * Ph::CheckMark_WidthOfHeightScale; + qreal dimy = rh * insetScale; + QRectF r_(rx + (rw - dimx) / 2, ry + (rh - dimy) / 2, dimx, dimy); + Ph::drawCheck(painter, d->checkBox_pen_scratch, r_, swatch, fgColor); + } + break; + } + case PE_IndicatorRadioButton: { + qreal rx, ry, rw, rh; + QRectF(option->rect).getRect(&rx, &ry, &rw, &rh); + bool isHighlighted = option->state & State_HasFocus && option->state & State_KeyboardFocusChange; + bool isSunken = state & State_Sunken; + bool isEnabled = state & State_Enabled; + Swatchy outlineColor = isHighlighted ? S_highlight_outline : S_window_outline; + Swatchy bgFillColor = isSunken ? S_highlight : S_base; + QPointF circleCenter(rx + rw / 2.0, ry + rh / 2.0); + const qreal lineThickness = 1.0; + qreal outlineRadius = (qMin(rw, rh) - lineThickness) / 2.0; + qreal fillRadius = outlineRadius - lineThickness / 2.0; + Ph::PSave save(painter); + painter->setRenderHint(QPainter::Antialiasing); + painter->setBrush(swatch.brush(bgFillColor)); + painter->setPen(swatch.pen(outlineColor)); + painter->drawEllipse(circleCenter, outlineRadius, outlineRadius); + if (Ph::IndicatorShadows && !isSunken && isEnabled) { + // Really slow, just a temp demo test + painter->setPen(Qt::NoPen); + painter->setBrush(swatch.brush(S_base_shadow)); + QPainterPath path0, path1; + path0.addEllipse(circleCenter, fillRadius, fillRadius); + path1.addEllipse(circleCenter + QPointF(0, 1.25), fillRadius, fillRadius); + QPainterPath path2 = path0 - path1; + painter->drawPath(path2); + } + if (state & State_On) { + Swatchy fgColor = isSunken ? S_highlightedText : S_windowText; + qreal checkmarkRadius = outlineRadius / 2.32; + painter->setPen(Qt::NoPen); + painter->setBrush(swatch.brush(fgColor)); + painter->drawEllipse(circleCenter, checkmarkRadius, checkmarkRadius); + } + break; + } + case PE_IndicatorToolBarHandle: { + if (!option) + break; + QRect r = option->rect; + if (r.width() < 3 || r.height() < 3) + break; + int rows = 3; + int columns = 2; + if (option->state & State_Horizontal) { + } else { + qSwap(columns, rows); + } + int dotLen = Ph::dpiScaled(2); + QSize occupied(dotLen * (columns * 2 - 1), dotLen * (rows * 2 - 1)); + QRect rr = QStyle::alignedRect(option->direction, Qt::AlignCenter, QSize(occupied), r); + int x = rr.x(); + int y = rr.y(); + for (int row = 0; row < rows; ++row) { + for (int col = 0; col < columns; ++col) { + int x_ = x + col * 2 * dotLen; + int y_ = y + row * 2 * dotLen; + painter->fillRect(x_, y_, dotLen, dotLen, swatch.color(S_window_divider)); + } + } + break; + } + case PE_FrameDefaultButton: + break; + case PE_FrameFocusRect: { + auto fropt = qstyleoption_cast(option); + if (!fropt) + break; + //### check for d->alt_down + if (!(fropt->state & State_KeyboardFocusChange)) + return; + if (fropt->state & State_Item) { + if (auto itemView = qobject_cast(widget)) { + // TODO either our grid line hack is interfering, or Qt has a bug, but + // in RTL layout the grid borders can leave junk behind in the grid + // areas and the right edge of the focus rect may not get painted. + // (Sometimes it will, though.) To replicate, set to RTL mode, and move + // the current around in a table view without the selection being on + // the current. + if (option->state & QStyle::State_Selected) { + bool showCurrent = true; + bool hasTableGrid = false; + const auto selectionMode = itemView->selectionMode(); + if (selectionMode == QAbstractItemView::SingleSelection) { + showCurrent = false; + } else { + // Table views will can have a "current" frame drawn even if the + // "current" is within the selected range. Other item views won't, + // which means the "current" frame will be invisible if it's on a + // selected item. This is a compromise between the broken drawing + // behavior of Qt item views of drawing "current" frames when they + // don't make sense (like a tree view where you can only select + // entire rows, but Qt will the frame rect around whatever column + // was last clicked on by the mouse, but using keyboard navigation + // has no effect) and not drawing them at all. + bool isTableView = false; + if (auto tableView = qobject_cast(itemView)) { + hasTableGrid = tableView->showGrid(); + isTableView = true; + } + const auto selectionModel = itemView->selectionModel(); + if (selectionModel) { + const auto selection = selectionModel->selection(); + if (selection.count() == 1) { + const auto& range = selection.at(0); + if (isTableView) { + // For table views, we don't draw the "current" frame if + // there is exactly one cell selected and the "current" is + // that cell, or if there is exactly one row or one column + // selected with the behavior set to the corresponding + // selection, and the "current" is that one row or column. + const auto selectionBehavior = itemView->selectionBehavior(); + if ((range.width() == 1 && range.height() == 1) + || (selectionBehavior == QAbstractItemView::SelectRows && range.height() == 1) + || (selectionBehavior == QAbstractItemView::SelectColumns + && range.width() == 1)) { + showCurrent = false; + } + } else { + // For any other type of item view, don't draw the "current" + // frame if there is a single contiguous selection, and the + // "current" is within that selection. If there's a + // discontiguous selection, that means the user is probably + // doing something more advanced, and we should just draw the + // focus frame, even if Qt might be doing it badly in some + // cases. + showCurrent = false; + } + } + } + } + if (showCurrent) { + // TODO handle dark-highlight-light-text + const QColor& borderColor = swatch.color(S_itemView_multiSelection_currentBorder); + const int thickness = hasTableGrid ? 2 : 1; + Ph::fillRectOutline(painter, option->rect, thickness, borderColor); + } + } else { + Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_highlight_outline)); + } + break; + } + } + // It would be nice to also handle QTreeView's allColumnsShowFocus thing in + // the above code, in addition to the normal cases for focus rects in item + // views. Unfortunately, with allColumnsShowFocus set to true, + // QTreeView::drawRow() calls the style to paint with PE_FrameFocusRect for + // the row frame with the widget set to nullptr. This makes it basically + // impossible to figure out that we need to draw a special frame for it. + // So, if any application code is using that mode in a QTreeView, it won't + // get special item view frames. Too bad. + Ph::PSave save(painter); + Ph::paintBorderedRoundRect( + painter, option->rect, Ph::FrameFocusRect_Rounding, swatch, S_highlight_outline, S_none); + break; + } + case PE_PanelButtonCommand: + case PE_PanelButtonBevel: { + bool isDefault = false; + bool isFlat = false; + bool isDown = option->state & State_Sunken; + bool isOn = option->state & State_On; + if (auto button = qstyleoption_cast(option)) { + isDefault = (button->features & QStyleOptionButton::DefaultButton) && (button->state & State_Enabled); + isFlat = (button->features & QStyleOptionButton::Flat); + } + if (isFlat && !isDown && !isOn) + break; + bool isEnabled = option->state & State_Enabled; + Q_UNUSED(isEnabled); + bool hasFocus = (option->state & State_HasFocus && option->state & State_KeyboardFocusChange); + const qreal rounding = Ph::PushButton_Rounding; + Swatchy outline = S_window_outline; + Swatchy fill = S_button; + Swatchy specular = S_button_specular; + if (isDown) { + fill = S_button_pressed; + specular = S_button_pressed_specular; + } else if (isOn) { + // kinda repurposing this, hmm + fill = S_scrollbarGutter; + specular = S_button_pressed_specular; + } + if (hasFocus || isDefault) { + outline = S_highlight_outline; + } + QRect r = option->rect; + Ph::PSave save(painter); + Ph::paintBorderedRoundRect(painter, r, rounding, swatch, outline, fill); + Ph::paintBorderedRoundRect(painter, r.adjusted(1, 1, -1, -1), rounding, swatch, specular, S_none); + break; + } + case PE_FrameTabWidget: { + QRect bgRect = option->rect.adjusted(1, 1, -1, -1); + painter->fillRect(bgRect, swatch.color(S_tabFrame)); + auto twf = qstyleoption_cast(option); + if (!twf) + break; + Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_frame_outline)); + Ph::fillRectOutline(painter, bgRect, 1, swatch.color(S_tabFrame_specular)); + break; + } + case PE_FrameStatusBarItem: + break; + case PE_IndicatorTabClose: + case Phantom_PE_IndicatorTabNew: { + Swatchy fg = S_windowText; + Swatchy bg = S_none; + if ((option->state & State_Enabled) && (option->state & State_MouseOver)) { + fg = S_highlightedText; + bg = option->state & State_Sunken ? S_highlight_outline : S_highlight; + } + // temp code + Ph::PSave save(painter); + if (bg) { + Ph::paintSolidRoundRect(painter, option->rect, Ph::PushButton_Rounding, swatch, bg); + } + QPen pen = swatch.pen(fg); + pen.setCapStyle(Qt::RoundCap); + pen.setWidthF(1.5); + painter->setBrush(Qt::NoBrush); + painter->setPen(pen); + painter->setRenderHint(QPainter::Antialiasing); + QRect r = option->rect; + // int adj = (int)((qreal)qMin(r.width(), r.height()) * (1.0 / 2.5)); + int adj = Ph::dpiScaled(5.0); + r.adjust(adj, adj, -adj, -adj); + qreal x, y, w, h; + QRectF(r).getRect(&x, &y, &w, &h); + // painter->translate(-0.5, -0.5); + switch (static_cast(elem)) { + case PE_IndicatorTabClose: + painter->drawLine(QPointF(x - 0.5, y - 0.5), QPointF(x + 0.5 + w, y + 0.5 + h)); + painter->drawLine(QPointF(x - 0.5, y + h + 0.5), QPointF(x + 0.5 + w, y - 0.5)); + break; + case Phantom_PE_IndicatorTabNew: + // kinda hacky here on extra len + painter->drawLine(QPointF(x + w / 2, y - 1.0), QPointF(x + w / 2, y + h + 1.0)); + painter->drawLine(QPointF(x - 1.0, y + h / 2), QPointF(x + w + 1.0, y + h / 2)); + break; + } + save.restore(); + // painter->fillRect(option->rect, QColor(255, 0, 0, 30)); + break; + } + case PE_PanelMenu: { + // bool isBelowMenuBar = false; + // works but currently unused + // QPoint gp = widget->mapToGlobal(widget->rect().topLeft()); + // gp.setY(gp.y() - 1); + // QWidget* bar = qApp->widgetAt(gp); + // if (bar && bar->inherits("QMenuBar")) { + // isBelowMenuBar = true; + // } + + // Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_window_divider)); + // QRect bgRect = option->rect.adjusted(1, isBelowMenuBar ? 0 : 1, -1, -1); + // painter->fillRect(bgRect, swatch.color(S_window)); + + const int radius = Phantom::DefaultFrame_Radius; + painter->save(); + painter->setRenderHint(QPainter::Antialiasing); + painter->setPen(swatch.color(S_frame_outline)); + QColor background(swatch.color(S_window)); + background.setAlpha(150); + painter->setBrush(background); + QRectF frameRect = strokedRect(option->rect, 1); + painter->drawRoundedRect(frameRect, radius, radius); + + // blur + if (widget && widget->window()) { + QPainterPath path; + path.addRoundedRect(option->rect, radius, radius); + const_cast(widget)->setMask(path.toFillPolygon().toPolygon()); + m_blurHelper->update(const_cast(widget)); + } + + painter->restore(); + 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_scrollbarSlider; + thumbSpecular = S_button_specular; + } + Qt::Edges edges; + QRect edgeRect = option->rect; + QRect mainRect = option->rect; + edgeRect.adjust(0, -1, 0, 1); + if (isLeftToRight) { + edges = Qt::LeftEdge | Qt::TopEdge | Qt::BottomEdge; + mainRect.setX(mainRect.x() + 1); + } else { + edges = Qt::TopEdge | Qt::BottomEdge | Qt::RightEdge; + mainRect.setWidth(mainRect.width() - 1); + } + Ph::fillRectEdges(painter, edgeRect, edges, 1, swatch.color(S_window_outline)); + painter->fillRect(mainRect, swatch.color(thumbFill)); + Ph::fillRectOutline(painter, mainRect, 1, swatch.color(thumbSpecular)); + break; + } + case Phantom_PE_WindowFrameColor: { + painter->fillRect(option->rect, swatch.color(S_window_outline)); + break; + } + default: + QCommonStyle::drawPrimitive(elem, option, painter, widget); + break; + } +} + +void BaseStyle::drawControl(ControlElement element, + const QStyleOption* option, + QPainter* painter, + const QWidget* widget) const +{ +#ifdef BUILD_WITH_EASY_PROFILER + EASY_BLOCK("drawControl"); + const char* elemCString = QMetaEnum::fromType().valueToKey(element); + EASY_TEXT("Element", elemCString); +#endif + using Swatchy = Phantom::Swatchy; + using namespace Phantom::SwatchColors; + namespace Ph = Phantom; + auto ph_swatchPtr = Ph::getCachedSwatchOfQPalette(&d->swatchCache, &d->headSwatchFastKey, option->palette); + const Ph::PhSwatch& swatch = *ph_swatchPtr.data(); + + switch (element) { + case CE_CheckBox: { + QCommonStyle::drawControl(element, option, painter, widget); + // painter->fillRect(option->rect, QColor(255, 0, 0, 90)); + break; + } + case CE_ComboBoxLabel: { + auto cb = qstyleoption_cast(option); + if (!cb) + break; + QRect editRect = proxy()->subControlRect(CC_ComboBox, cb, SC_ComboBoxEditField, widget); + painter->save(); + painter->setClipRect(editRect); + if (!cb->currentIcon.isNull()) { + QIcon::Mode mode = cb->state & State_Enabled ? QIcon::Normal : QIcon::Disabled; + QPixmap pixmap = cb->currentIcon.pixmap(cb->iconSize, mode); + QRect iconRect(editRect); + iconRect.setWidth(cb->iconSize.width() + 4); + iconRect = alignedRect(cb->direction, Qt::AlignLeft | Qt::AlignVCenter, iconRect.size(), editRect); + if (cb->editable) + painter->fillRect(iconRect, cb->palette.brush(QPalette::Base)); + proxy()->drawItemPixmap(painter, iconRect, Qt::AlignCenter, pixmap); + + if (cb->direction == Qt::RightToLeft) + editRect.translate(-4 - cb->iconSize.width(), 0); + else + editRect.translate(cb->iconSize.width() + 4, 0); + } + if (!cb->currentText.isEmpty() && !cb->editable) { + proxy()->drawItemText(painter, + editRect.adjusted(1, 0, -1, 0), + visualAlignment(cb->direction, Qt::AlignLeft | Qt::AlignVCenter), + cb->palette, + cb->state & State_Enabled, + cb->currentText, + cb->editable ? QPalette::Text : QPalette::ButtonText); + } + painter->restore(); + break; + } + case CE_Splitter: { + QRect r = option->rect; + // We don't have anything useful to draw if it's too thin + if (r.width() < 5 || r.height() < 5) + break; + int length = Ph::dpiScaled(Ph::SplitterMaxLength); + // int thickness = Ph::dpiScaled(1); + qreal radius = 0; + QSize size; + if (option->state & State_Horizontal) { + if (r.height() < length) + length = r.height(); + // size = QSize(thickness, length); + size = QSize(r.width() / 2, length); + radius = r.width() * 0.3; + } else { + if (r.width() < length) + length = r.width(); + // size = QSize(length, thickness); + size = QSize(length, r.height() / 2); + radius = r.height() * 0.3; + } + QRect filledRect = QStyle::alignedRect(option->direction, Qt::AlignCenter, size, r); + Ph::paintSolidRoundRect(painter, filledRect, radius, swatch, S_button_specular); + // Ph::fillRectOutline(painter, filledRect.adjusted(-1, 0, 1, 0), 1, swatch.color(S_window_divider)); + break; + } + // TODO update this for phantom + case CE_RubberBand: { + if (!qstyleoption_cast(option)) + break; + QColor highlight = option->palette.color(QPalette::Active, QPalette::Highlight); + painter->save(); + QColor penColor = highlight.darker(120); + penColor.setAlpha(180); + painter->setPen(penColor); + QColor dimHighlight(qMin(highlight.red() / 2 + 110, 255), + qMin(highlight.green() / 2 + 110, 255), + qMin(highlight.blue() / 2 + 110, 255)); + dimHighlight.setAlpha(widget && widget->isTopLevel() ? 255 : 80); + painter->setRenderHint(QPainter::Antialiasing, true); + painter->translate(0.5, 0.5); + painter->setBrush(dimHighlight); + painter->drawRoundedRect(option->rect.adjusted(0, 0, -1, -1), 1, 1); + QColor innerLine = Qt::white; + innerLine.setAlpha(40); + painter->setPen(innerLine); + painter->drawRoundedRect(option->rect.adjusted(1, 1, -2, -2), 1, 1); + painter->restore(); + break; + } + case CE_SizeGrip: { + Qt::LayoutDirection dir = option->direction; + QRect rect = option->rect; + int rcx = rect.center().x(); + int rcy = rect.center().y(); + // draw grips + for (int i = -6; i < 12; i += 3) { + for (int j = -6; j < 12; j += 3) { + if ((dir == Qt::LeftToRight && i > -j) || (dir == Qt::RightToLeft && j > i)) { + painter->fillRect(rcx + i, rcy + j, 2, 2, swatch.color(S_window_lighter)); + painter->fillRect(rcx + i, rcy + j, 1, 1, swatch.color(S_window_darker)); + } + } + } + break; + } + case CE_ToolBar: { + auto toolBar = qstyleoption_cast(option); + if (!toolBar) + break; + painter->fillRect(option->rect, option->palette.window().color()); + bool isFloating = false; + if (auto tb = qobject_cast(widget)) { + isFloating = tb->isFloating(); + } + if (isFloating) { + Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_window_outline)); + } + break; + } + case CE_DockWidgetTitle: { + auto dwOpt = qstyleoption_cast(option); + if (!dwOpt) + break; + painter->save(); + bool verticalTitleBar = dwOpt->verticalTitleBar; + + QRect titleRect = subElementRect(SE_DockWidgetTitleBarText, option, widget); + if (verticalTitleBar) { + QRect r = dwOpt->rect; + QRect rtrans = {r.x(), r.y(), r.height(), r.width()}; + titleRect = QRect(rtrans.left() + r.bottom() - titleRect.bottom(), + rtrans.top() + titleRect.left() - r.left(), + titleRect.height(), + titleRect.width()); + painter->translate(rtrans.left(), rtrans.top() + rtrans.width()); + painter->rotate(-90); + painter->translate(-rtrans.left(), -rtrans.top()); + } + if (!dwOpt->title.isEmpty()) { + QString titleText = painter->fontMetrics().elidedText(dwOpt->title, Qt::ElideRight, titleRect.width()); + proxy()->drawItemText(painter, + titleRect, + Qt::AlignLeft | Qt::AlignVCenter | Qt::TextShowMnemonic, + dwOpt->palette, + dwOpt->state & State_Enabled, + titleText, + QPalette::WindowText); + } + painter->restore(); + break; + } + case CE_HeaderSection: { + auto header = qstyleoption_cast(option); + if (!header) + break; + QRect rect = header->rect; + Qt::Orientation orientation = header->orientation; + QStyleOptionHeader::SectionPosition position = header->position; + // See the "Table header layout reference" comment block at the bottom of + // this file for more information to help understand what's going on. + bool isLeftToRight = header->direction != Qt::RightToLeft; + bool isHorizontal = orientation == Qt::Horizontal; + bool isVertical = orientation == Qt::Vertical; + bool isEnd = position == QStyleOptionHeader::End; + bool isBegin = position == QStyleOptionHeader::Beginning; + bool isOnlyOne = position == QStyleOptionHeader::OnlyOneSection; + Qt::Edges edges; + bool spansToEnd = false; + bool isSpecialCorner = false; + if ((isHorizontal && isLeftToRight && isEnd) || (isHorizontal && !isLeftToRight && isBegin) + || (isVertical && isEnd) || isOnlyOne) { + auto hv = qobject_cast(widget); + if (hv) { + spansToEnd = hv->stretchLastSection(); + // In the case where the header item is not stretched to the end, but + // could plausibly be in a position where it could happen to be exactly + // the right width or height to be appear to be stretched to the end, + // we'll check to see if it actually does exactly meet the right (or + // bottom in vertical, or left in RTL) edge, and omit drawing the edge + // if that's the case. This can commonly happen if you have a tree or + // list view and don't set it to stretch, but the widget is still sized + // exactly to hold the one column. (It could also happen if there's + // user code running to manually stretch the last section as + // necessary.) + if (!spansToEnd) { + QRect viewBound = hv->contentsRect(); + if (isHorizontal) { + if (isLeftToRight) { + spansToEnd = rect.right() == viewBound.right(); + } else { + spansToEnd = rect.left() == viewBound.left(); + } + } else if (isVertical) { + spansToEnd = rect.bottom() == viewBound.bottom(); + } + } + } else { + // We only need to do this check in RTL, because the corner button in + // RTL *doesn't* need hacks applied. In LTR, we can just treat the + // corner button like anything else on the horizontal header bar, and + // can skip doing this inherits check. + if (isOnlyOne && !isLeftToRight && widget && widget->inherits("QTableCornerButton")) { + isSpecialCorner = true; + } + } + } + + if (isSpecialCorner) { + // In RTL layout, the corner button in a table view doesn't have any + // offset problems. This branch we're on is only taken if we're in RTL + // layout and this is the corner button being drawn. + edges |= Qt::BottomEdge; + if (isLeftToRight) + edges |= Qt::RightEdge; + else + edges |= Qt::LeftEdge; + } else if (isHorizontal) { + // This branch is taken for horizontal headers in either layout direction + // or for the corner button in LTR. + edges |= Qt::BottomEdge; + if (isLeftToRight) { + // In LTR, this code path may be for both the corner button *and* the + // actual header item. It doesn't matter in this case, and we were able + // to avoid doing an extra inherits call earlier. + if (!spansToEnd) { + edges |= Qt::RightEdge; + } + } else { + // Note: in right-to-left layouts for horizontal headers, the header + // view will unfortunately be shifted to the right by 1 pixel, due to + // what appears to be a Qt bug. This causes the vertical lines we draw + // in the header view to misalign with the grid, and causes the + // rightmost section to have its right edge clipped off. Therefore, + // we'll draw the separator on the on the right edge instead of the + // left edge. (We would have expected to draw it on the left edge in + // RTL layout.) This makes it line up with the grid again, except for + // the last section. right by 1 pixel. + // + // In RTL, the "Begin" position is on the left side for some reason + // (the same as LTR.) So "End" is always on the right. Ok, whatever. + // See the table at the bottom of this file if you're confused. + if (!isOnlyOne && !isEnd) { + edges |= Qt::RightEdge; + } + // The leftmost section in RTL has to draw on both its right and left + // edges, instead of just 1 edge like every other configuration. The + // left edge will be offset by 1 pixel from the grid, but it's the best + // we can do. + if (isBegin && !spansToEnd) { + edges |= Qt::LeftEdge; + } + } + } else if (isVertical) { + if (isLeftToRight) { + edges |= Qt::RightEdge; + } else { + edges |= Qt::LeftEdge; + } + if (!spansToEnd) { + edges |= Qt::BottomEdge; + } + } + QRect bgRect = Ph::expandRect(rect, edges, -1); + painter->fillRect(bgRect, swatch.color(S_window)); + Ph::fillRectEdges(painter, rect, edges, 1, swatch.color(S_frame_outline)); + break; + } + case CE_HeaderLabel: { + auto header = qstyleoption_cast(option); + if (!header) + break; + QRect rect = header->rect; + if (!header->icon.isNull()) { + int iconExtent = qMin(qMin(rect.height(), rect.width()), option->fontMetrics.height()); + auto window = widget ? widget->window()->windowHandle() : nullptr; + QPixmap pixmap = header->icon.pixmap(window, + QSize(iconExtent, iconExtent), + (header->state & State_Enabled) ? QIcon::Normal : QIcon::Disabled); + int pixw = static_cast(pixmap.width() / pixmap.devicePixelRatio()); + QRect aligned = alignedRect( + header->direction, QFlag(header->iconAlignment), pixmap.size() / pixmap.devicePixelRatio(), rect); + QRect inter = aligned.intersected(rect); + painter->drawPixmap(inter.x(), + inter.y(), + pixmap, + inter.x() - aligned.x(), + inter.y() - aligned.y(), + static_cast(aligned.width() * pixmap.devicePixelRatio()), + static_cast(pixmap.height() * pixmap.devicePixelRatio())); + int margin = proxy()->pixelMetric(QStyle::PM_HeaderMargin, option, widget); + if (header->direction == Qt::LeftToRight) + rect.setLeft(rect.left() + pixw + margin); + else + rect.setRight(rect.right() - pixw - margin); + } + proxy()->drawItemText(painter, + rect, + header->textAlignment, + header->palette, + (header->state & State_Enabled), + header->text, + QPalette::ButtonText); + + // But we still need some kind of indicator, so draw a line + bool drawHighlightLine = option->state & State_On; + // Special logic: if the selection mode of the item view is to select every + // row or every column, there's no real need to draw special "this + // row/column is selected" highlight indicators in the header view. The + // application programmer can also disable this explicitly on the header + // view, but it's nice to have it done automatically, I think. + if (drawHighlightLine) { + const QAbstractItemView* itemview = nullptr; + // Header view itself is an item view, and we don't care about its + // selection behavior -- we care about the actual item view. So try to + // get the widget as the header first, then find the item view from + // there. + auto headerview = qobject_cast(widget); + if (headerview) { + // Also don't care about highlights if there's only one row or column. + drawHighlightLine = headerview->count() > 1; + itemview = qobject_cast(headerview->parentWidget()); + } + if (drawHighlightLine && itemview) { + auto selBehavior = itemview->selectionBehavior(); + if (selBehavior == QAbstractItemView::SelectRows && header->orientation == Qt::Horizontal) + drawHighlightLine = false; + else if (selBehavior == QAbstractItemView::SelectColumns && header->orientation == Qt::Vertical) + drawHighlightLine = false; + } + } + + if (drawHighlightLine) { + QRect r = option->rect; + Qt::Edge edge; + if (header->orientation == Qt::Horizontal) { + edge = Qt::BottomEdge; + r.adjust(-2, 1, 1, 1); + } else { + bool isLeftToRight = option->direction != Qt::RightToLeft; + if (isLeftToRight) { + edge = Qt::RightEdge; + r.adjust(1, -2, 1, 1); + } else { + edge = Qt::LeftEdge; + r.adjust(-1, -2, -1, 1); + } + } + Ph::fillRectEdges(painter, r, edge, 1, swatch.color(S_itemView_headerOnLine)); + } + break; + } + case CE_ProgressBarGroove: { + const qreal rounding = Ph::ProgressBar_Rounding; + QRect rect = option->rect; + Ph::PSave save(painter); + Ph::paintBorderedRoundRect(painter, rect, rounding, swatch, S_window_outline, S_base); + save.restore(); + if (Ph::OverhangShadows && option->state & State_Enabled) { + // Inner shadow + const QColor& shadowColor = swatch.color(S_base_shadow); + // We can either have the shadow cut into the rounded corners, or leave a + // 1px gap, due to AA. + Ph::fillRectEdges(painter, + rect.adjusted(qRound(rounding / 2) + 1, 1, -(qRound(rounding / 2) + 1), -1), + Qt::TopEdge, + 1, + shadowColor); + } + break; + } + case CE_ProgressBarContents: { + auto bar = qstyleoption_cast(option); + if (!bar) + break; + const qreal rounding = Ph::ProgressBar_Rounding; + QRect filled, nonFilled; + bool isIndeterminate = false; + Ph::progressBarFillRects(bar, filled, nonFilled, isIndeterminate); + if (isIndeterminate || bar->progress > bar->minimum) { + Ph::PSave save(painter); + Ph::paintBorderedRoundRect(painter, filled, rounding, swatch, S_progressBar_outline, S_progressBar); + Ph::paintBorderedRoundRect( + painter, filled.adjusted(1, 1, -1, -1), rounding, swatch, S_progressBar_specular, S_none); + if (isIndeterminate) { + // TODO paint indeterminate indicator + } + } + break; + } + case CE_ProgressBarLabel: { + auto bar = qstyleoption_cast(option); + if (!bar) + break; + if (bar->text.isEmpty()) + break; + QRect r = bar->rect.adjusted(2, 2, -2, -2); + if (r.isEmpty() || !r.isValid()) + break; + QSize textSize = option->fontMetrics.size(Qt::TextBypassShaping, bar->text); + QRect textRect = QStyle::alignedRect(option->direction, Qt::AlignCenter, textSize, option->rect); + textRect &= r; + if (textRect.isEmpty()) + break; + QRect filled, nonFilled; + bool isIndeterminate = false; + Ph::progressBarFillRects(bar, filled, nonFilled, isIndeterminate); + QRect textNonFilledR = textRect & nonFilled; + QRect textFilledR = textRect & filled; + bool needsNonFilled = !textNonFilledR.isEmpty(); + bool needsFilled = !textFilledR.isEmpty(); + bool needsMasking = needsNonFilled && needsFilled; + Ph::PSave save(painter); + if (needsNonFilled) { + if (needsMasking) { + painter->save(); + painter->setClipRect(textNonFilledR); + } + painter->setPen(swatch.pen(S_text)); + painter->setBrush(Qt::NoBrush); + painter->drawText(textRect, bar->text, Qt::AlignHCenter | Qt::AlignVCenter); + if (needsMasking) { + painter->restore(); + } + } + if (needsFilled) { + if (needsMasking) { + painter->save(); + painter->setClipRect(textFilledR); + } + painter->setPen(swatch.pen(S_highlightedText)); + painter->setBrush(Qt::NoBrush); + painter->drawText(textRect, bar->text, Qt::AlignHCenter | Qt::AlignVCenter); + if (needsMasking) { + painter->restore(); + } + } + break; + } + case CE_MenuBarItem: { + auto mbi = qstyleoption_cast(option); + if (!mbi) + break; + const QRect r = option->rect; + QRect textRect = r; + textRect.setY(textRect.y() + (r.height() - option->fontMetrics.height()) / 2); + int alignment = Qt::AlignHCenter | Qt::AlignTop | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine; + if (!proxy()->styleHint(SH_UnderlineShortcut, mbi, widget)) + alignment |= Qt::TextHideMnemonic; + const auto itemState = mbi->state; + bool maybeHasAltKeyNavFocus = itemState & State_Selected && itemState & State_HasFocus; + bool isSelected = itemState & State_Selected || itemState & State_Sunken; + if (!isSelected && maybeHasAltKeyNavFocus && widget) { + isSelected = widget->hasFocus(); + } + Swatchy fill = isSelected ? S_highlight : S_window; + painter->fillRect(r, swatch.color(fill)); + QPalette::ColorRole textRole = isSelected ? QPalette::HighlightedText : QPalette::Text; + proxy()->drawItemText( + painter, textRect, alignment, mbi->palette, mbi->state & State_Enabled, mbi->text, textRole); + if (Phantom::MenuBarDrawBorder && !isSelected) { + Ph::fillRectEdges(painter, r, Qt::BottomEdge, 1, swatch.color(S_window_divider)); + } + break; + } + + case CE_MenuItem: { + auto menuItem = qstyleoption_cast(option); + if (!menuItem) + break; + const auto metrics = Ph::MenuItemMetrics::ofFontHeight(option->fontMetrics.height()); + // Draws one item in a popup menu. + if (menuItem->menuItemType == QStyleOptionMenuItem::Separator) { + // Phantom ignores text and icons in menu separators, because + // 1) The text and icons for separators don't render on Mac native menus + // 2) There doesn't seem to be a way to account for the width of the text + // properly (Fusion will often draw separator text clipped off) + // 3) Setting text on separators also seems to mess up the metrics for + // menu items on Mac native menus + QRect r = option->rect; + r.setHeight(r.height() / 2 + 1); + Ph::fillRectEdges(painter, r, Qt::BottomEdge, 1, swatch.color(S_window_divider)); + break; + } + const QRect itemRect = option->rect; + painter->save(); + bool isSelected = menuItem->state & State_Selected && menuItem->state & State_Enabled; + bool isCheckable = menuItem->checkType != QStyleOptionMenuItem::NotCheckable; + bool isChecked = menuItem->checked; + bool isSunken = menuItem->state & State_Sunken; + bool isEnabled = menuItem->state & State_Enabled; + bool hasSubMenu = menuItem->menuItemType == QStyleOptionMenuItem::SubMenu; + if (isSelected) { + Swatchy fillColor = isSunken ? S_highlight_outline : S_highlight; + // painter->fillRect(option->rect, swatch.color(fillColor)); + // rekols: Add rounded rectangle. + painter->save(); + painter->setPen(Qt::NoPen); + painter->setBrush(swatch.color(fillColor)); + painter->setRenderHint(QPainter::Antialiasing); + painter->drawRoundedRect(option->rect, Phantom::DefaultFrame_Radius, Phantom::DefaultFrame_Radius); + painter->restore(); + } + + if (isCheckable) { + // Note: check rect might be misaligned vertically if it's a menu from a + // combo box. Probably a bug in Qt code? + QRect checkRect = Ph::menuItemCheckRect(metrics, option->direction, itemRect, hasSubMenu); + Swatchy signColor = !isEnabled ? S_windowText : isSelected ? S_highlightedText : S_windowText; + if (menuItem->checkType & QStyleOptionMenuItem::Exclusive) { + // Radio button + if (isChecked) { + painter->setRenderHint(QPainter::Antialiasing); + painter->setPen(Qt::NoPen); + QPalette::ColorRole textRole = + !isEnabled ? QPalette::Text : isSelected ? QPalette::HighlightedText : QPalette::ButtonText; + painter->setBrush(option->palette.brush(option->palette.currentColorGroup(), textRole)); + qreal rx, ry, rw, rh; + QRectF(checkRect).getRect(&rx, &ry, &rw, &rh); + qreal dim = qMin(checkRect.width(), checkRect.height()) * 0.75; + QRectF rf(rx + rw / dim, ry + rh / dim, dim, dim); + painter->drawEllipse(rf); + } + } else { + // If we want mouse-down to immediately show the item as + // checked/unchecked (kinda bad if the user is click-holding on the + // menu instead of click-clicking.) + // + // if ((isChecked && !isSunken) || (!isChecked && isSunken)) { + if (isChecked) { + Ph::drawCheck(painter, d->checkBox_pen_scratch, checkRect, swatch, signColor); + } + } + } + + const bool hasIcon = !menuItem->icon.isNull(); + + if (hasIcon) { + QRect iconRect = Ph::menuItemIconRect(metrics, option->direction, itemRect, hasSubMenu); + QIcon::Mode mode = isEnabled ? QIcon::Normal : QIcon::Disabled; + if (isSelected && isEnabled) + mode = QIcon::Selected; + QIcon::State state = isChecked ? QIcon::On : QIcon::Off; + + // TODO hmm, we might be ending up with blurry icons at size 15 instead + // of 16 for example on Windows. + // + // int smallIconSize = + // proxy()->pixelMetric(PM_SmallIconSize, option, widget); + // QSize iconSize(smallIconSize, smallIconSize); + int iconExtent = qMin(iconRect.width(), iconRect.height()); + QSize iconSize(iconExtent, iconExtent); + if (auto combo = qobject_cast(widget)) { + iconSize = combo->iconSize(); + } + QWindow* window = widget ? widget->windowHandle() : nullptr; + QPixmap pixmap = menuItem->icon.pixmap(window, iconSize, mode, state); + const int pixw = static_cast(pixmap.width() / pixmap.devicePixelRatio()); + const int pixh = static_cast(pixmap.height() / pixmap.devicePixelRatio()); + QRect pixmapRect = QStyle::alignedRect(option->direction, Qt::AlignCenter, QSize(pixw, pixh), iconRect); + painter->drawPixmap(pixmapRect.topLeft(), pixmap); + } + + // Draw main text and mnemonic text + QStringRef s(&menuItem->text); + if (!s.isEmpty()) { + QRect textRect = + Ph::menuItemTextRect(metrics, option->direction, itemRect, hasSubMenu, hasIcon, menuItem->tabWidth); + int t = s.indexOf(QLatin1Char('\t')); + int text_flags = + Qt::AlignLeft | Qt::AlignTop | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine; + if (!styleHint(SH_UnderlineShortcut, menuItem, widget)) + text_flags |= Qt::TextHideMnemonic; +#if 0 + painter->save(); +#endif + painter->setPen(swatch.pen(isSelected ? S_highlightedText : S_text)); + + // Comment from original Qt code which did some dance with the font: + // + // font may not have any "hard" flags set. We override the point size so + // that when it is resolved against the device, this font will win. This + // is mainly to handle cases where someone sets the font on the window + // and then the combo inherits it and passes it onward. At that point the + // resolve mask is very, very weak. This makes it stonger. +#if 0 + QFont font = menuItem->font; + font.setPointSizeF(QFontInfo(menuItem->font).pointSizeF()); + painter->setFont(font); +#endif + + // My comment: + // + // What actually looks like is happening is that the qplatformtheme may + // have set a per-class font for menus. The QComboMenuDelegate sets the + // combo box's own font on the QStyleOptionMenuItem when passing it in + // here and when calling sizeFromContents with CT_MenuItem, but the + // QPainter we're called with hasn't had its font set to it -- it's still + // set to the QMenu/QMenuItem app fonts hash font. So if it's a menu + // coming from a combo box, let's just go ahead and set the font for it + // if it doesn't match, since that's probably what it wanted to do. I + // think. And as described above, we have to do the weird dance with the + // resolve mask... which is some internal Qt detail that we aren't + // supposed to have to deal with, but here we are. + // + // Ok, there's another problem, and QFusionStyle also suffers from it: in + // high DPI, setting the pointSizeF and setting the font again won't + // necessarily give us the right font (at least in Windows.) The font + // might have too thin of a weight, and probably other problems. So just + // forget about it: we'll have Phantom return 0 for the style hint that + // the combo box uses to determine if it should use a QMenu popup instead + // of a regular dropdown menu thing. The popup menu might actually be + // better for usability in some cases, and it's how combos work on Mac + // and BeOS, but it won't work anyway for editable combo boxes in Qt, and + // the font issues just make it not worth it. So we'll have a dropdown + // guy like a traditional Windows thing. + // + // If you want to try it out again, go to SH_ComboBox_Popup and have it + // return 1. + // + // Alternatively, we could instead have the CT_MenuItem handling code try + // to be aggressively clever and use the qt app font hash to look up the + // expected font for a QMenu and use that for calculating its metrics. + // Unfortunately, that probably won't work so great if the combo/menu + // actually wants to use custom fonts in its listing, since we'd be + // ignoring it. That's how UseQMenuForComboBoxPopup currently works, + // though it tests for Qt::WA_SetFont as an attempt at recognizing when + // it shouldn't use the qt font hash for QMenu. +#if 0 + if (qobject_cast(widget)) { + QFont font = menuItem->font; + font.setPointSizeF(QFontInfo(menuItem->font).pointSizeF()); + painter->setFont(font); + } +#endif + + // Draw mnemonic text + if (t >= 0) { + QRect mnemonicR = + Ph::menuItemMnemonicRect(metrics, option->direction, itemRect, hasSubMenu, menuItem->tabWidth); + const QStringRef textToDrawRef = s.mid(t + 1); + const QString unsafeTextToDraw = QString::fromRawData(textToDrawRef.constData(), textToDrawRef.size()); + painter->drawText(mnemonicR, text_flags, unsafeTextToDraw); + s = s.left(t); + } + const QStringRef textToDrawRef = s.left(t); + const QString unsafeTextToDraw = QString::fromRawData(textToDrawRef.constData(), textToDrawRef.size()); + painter->drawText(textRect, text_flags, unsafeTextToDraw); + +#if 0 + painter->restore(); +#endif + } + + // SubMenu Arrow + if (hasSubMenu) { + Qt::ArrowType arrow = option->direction == Qt::RightToLeft ? Qt::LeftArrow : Qt::RightArrow; + QRect arrowRect = Ph::menuItemArrowRect(metrics, option->direction, itemRect); + Swatchy arrowColor = isSelected ? S_highlightedText : S_indicator_current; + Ph::drawArrow(painter, arrowRect, arrow, swatch.brush(arrowColor)); + } + painter->restore(); + break; + } + case CE_MenuHMargin: + case CE_MenuVMargin: + case CE_MenuEmptyArea: + break; + case CE_PushButton: { + auto btn = qstyleoption_cast(option); + if (!btn) + break; + proxy()->drawControl(CE_PushButtonBevel, btn, painter, widget); + QStyleOptionButton subopt = *btn; + subopt.rect = subElementRect(SE_PushButtonContents, btn, widget); + proxy()->drawControl(CE_PushButtonLabel, &subopt, painter, widget); + break; + } + case CE_PushButtonLabel: { + auto button = qstyleoption_cast(option); + if (!button) + break; + // This code is very similar to QCommonStyle's implementation, but doesn't + // set the icon mode to active when focused. + QRect textRect = button->rect; + int tf = Qt::AlignVCenter | Qt::TextShowMnemonic; + if (!proxy()->styleHint(SH_UnderlineShortcut, button, widget)) + tf |= Qt::TextHideMnemonic; + if (!button->icon.isNull()) { + // Center both icon and text + QRect iconRect; + QIcon::Mode mode = button->state & State_Enabled ? QIcon::Normal : QIcon::Disabled; + QIcon::State state = button->state & State_On ? QIcon::On : QIcon::Off; + auto window = widget ? widget->window()->windowHandle() : nullptr; + QPixmap pixmap = button->icon.pixmap(window, button->iconSize, mode, state); + int pixmapWidth = static_cast(pixmap.width() / pixmap.devicePixelRatio()); + int pixmapHeight = static_cast(pixmap.height() / pixmap.devicePixelRatio()); + int labelWidth = pixmapWidth; + int labelHeight = pixmapHeight; + // 4 is hardcoded in QPushButton::sizeHint() + int iconSpacing = 4; + int textWidth = button->fontMetrics.boundingRect(option->rect, tf, button->text).width(); + if (!button->text.isEmpty()) + labelWidth += (textWidth + iconSpacing); + iconRect = QRect(textRect.x() + (textRect.width() - labelWidth) / 2, + textRect.y() + (textRect.height() - labelHeight) / 2, + pixmapWidth, + pixmapHeight); + iconRect = visualRect(button->direction, textRect, iconRect); + tf |= Qt::AlignLeft; // left align, we adjust the text-rect instead + if (button->direction == Qt::RightToLeft) + textRect.setRight(iconRect.left() - iconSpacing); + else + textRect.setLeft(iconRect.left() + iconRect.width() + iconSpacing); + if (button->state & (State_On | State_Sunken)) + iconRect.translate(proxy()->pixelMetric(PM_ButtonShiftHorizontal, option, widget), + proxy()->pixelMetric(PM_ButtonShiftVertical, option, widget)); + painter->drawPixmap(iconRect, pixmap); + } else { + tf |= Qt::AlignHCenter; + } + if (button->state & (State_On | State_Sunken)) + textRect.translate(proxy()->pixelMetric(PM_ButtonShiftHorizontal, option, widget), + proxy()->pixelMetric(PM_ButtonShiftVertical, option, widget)); + if (button->features & QStyleOptionButton::HasMenu) { + int indicatorSize = proxy()->pixelMetric(PM_MenuButtonIndicator, button, widget); + if (button->direction == Qt::LeftToRight) + textRect = textRect.adjusted(0, 0, -indicatorSize, 0); + else + textRect = textRect.adjusted(indicatorSize, 0, 0, 0); + } + proxy()->drawItemText(painter, + textRect, + tf, + button->palette, + (button->state & State_Enabled), + button->text, + QPalette::ButtonText); + break; + } + case CE_MenuBarEmptyArea: { + QRect rect = option->rect; + if (Phantom::MenuBarDrawBorder) { + Ph::fillRectEdges(painter, rect, Qt::BottomEdge, 1, swatch.color(S_window_divider)); + } + painter->fillRect(rect.adjusted(0, 0, 0, -1), swatch.color(S_window)); + break; + } + case CE_TabBarTabShape: { + auto tab = qstyleoption_cast(option); + if (!tab) + break; + bool rtlHorTabs = (tab->direction == Qt::RightToLeft + && (tab->shape == QTabBar::RoundedNorth || tab->shape == QTabBar::RoundedSouth)); + bool isSelected = tab->state & State_Selected; + bool lastTab = ((!rtlHorTabs && tab->position == QStyleOptionTab::End) + || (rtlHorTabs && tab->position == QStyleOptionTab::Beginning)); + bool onlyOne = tab->position == QStyleOptionTab::OnlyOneTab; + int tabOverlap = pixelMetric(PM_TabBarTabOverlap, option, widget); + const qreal rounding = Ph::TabBarTab_Rounding; + Qt::Edge outerEdge = Qt::TopEdge; + Qt::Edge edgeTowardNextTab = Qt::RightEdge; + switch (tab->shape) { + case QTabBar::RoundedNorth: + outerEdge = Qt::TopEdge; + edgeTowardNextTab = Qt::RightEdge; + break; + case QTabBar::RoundedSouth: + outerEdge = Qt::BottomEdge; + edgeTowardNextTab = Qt::RightEdge; + break; + case QTabBar::RoundedWest: + outerEdge = Qt::LeftEdge; + edgeTowardNextTab = Qt::BottomEdge; + break; + case QTabBar::RoundedEast: + outerEdge = Qt::RightEdge; + edgeTowardNextTab = Qt::BottomEdge; + break; + default: + QCommonStyle::drawControl(element, tab, painter, widget); + return; + } + Qt::Edge innerEdge = Ph::oppositeEdge(outerEdge); + Qt::Edge edgeAwayNextTab = Ph::oppositeEdge(edgeTowardNextTab); + QRect shapeClipRect = Ph::expandRect(option->rect, innerEdge, -2); + QRect drawRect = Ph::expandRect(shapeClipRect, innerEdge, 3 + 2 * rounding + 1); + if (!onlyOne && !lastTab) { + drawRect = Ph::expandRect(drawRect, edgeTowardNextTab, tabOverlap); + shapeClipRect = Ph::expandRect(shapeClipRect, edgeTowardNextTab, tabOverlap); + } + if (!isSelected) { + int offset = proxy()->pixelMetric(PM_TabBarTabShiftVertical, option, widget); + drawRect = Ph::expandRect(drawRect, outerEdge, -offset); + } + painter->save(); + painter->setClipRect(shapeClipRect); + bool hasFrame = tab->features & QStyleOptionTab::HasFrame && !tab->documentMode; + Swatchy tabFrameColor, thisFillColor, specular; + if (hasFrame) { + tabFrameColor = S_tabFrame; + if (isSelected) { + thisFillColor = S_tabFrame; + specular = S_tabFrame_specular; + } else { + thisFillColor = S_inactiveTabYesFrame; + specular = Ph::TabBar_InactiveTabsHaveSpecular ? S_inactiveTabYesFrame_specular : S_none; + } + } else { + tabFrameColor = S_window; + if (isSelected) { + thisFillColor = S_window; + specular = S_window_specular; + } else { + thisFillColor = S_inactiveTabNoFrame; + specular = Ph::TabBar_InactiveTabsHaveSpecular ? S_inactiveTabNoFrame_specular : S_none; + } + } + auto frameColor = isSelected ? S_frame_outline : S_window_outline; + Ph::paintBorderedRoundRect(painter, drawRect, rounding, swatch, frameColor, thisFillColor); + Ph::paintBorderedRoundRect(painter, drawRect.adjusted(1, 1, -1, -1), rounding, swatch, specular, S_none); + painter->restore(); + if (isSelected) { + QRect highlightRect = drawRect.adjusted(2, 1, -2, 0); + highlightRect.setHeight(Ph::dpiScaled(2.0)); + QRect highlightRectSpec = highlightRect.adjusted(-1, -1, 1, 0); + painter->fillRect(highlightRectSpec, Ph::DeriveColors::lightSpecularOf(swatch.color(S_highlight))); + painter->fillRect(highlightRect, swatch.color(S_highlight)); + + QRect refillRect = Ph::rectFromInnerEdgeWithThickness(shapeClipRect, innerEdge, 2); + refillRect = Ph::rectTranslatedTowardEdge(refillRect, innerEdge, 2); + refillRect = Ph::expandRect(refillRect, edgeAwayNextTab | edgeTowardNextTab, -1); + painter->fillRect(refillRect, swatch.color(tabFrameColor)); + Ph::fillRectEdges(painter, refillRect, edgeAwayNextTab | edgeTowardNextTab, 1, swatch.color(specular)); + } + break; + } + case CE_ItemViewItem: { + auto ivopt = qstyleoption_cast(option); + if (!ivopt) + break; + // Hack to work around broken grid line drawing in Qt's table view code: + // + // We tell it that the grid line color is a color via + // SH_Table_GridLineColor. It draws the grid lines, but it in high DPI it's + // broken because it uses a pen/path to draw the line, which makes it too + // narrow, subpixel-incorrectly-antialiased, and/or offset from its correct + // position. So when we draw the item view items in a table view, we'll + // also try to paint 1 pixel outside of our current rect to try to fill in + // the incorrectly painted areas where the grid lines are. + // + // Also note that the table views with the bad drawing code, when + // scrolling, will leave garbage behind in the incorrectly-drawn grid line + // areas. This will also paint over that. + bool overdrawGridHack = false; + if (auto tableWidget = qobject_cast(widget)) { + overdrawGridHack = tableWidget->showGrid() && tableWidget->gridStyle() == Qt::SolidLine; + } + if (overdrawGridHack) { + QRect r = option->rect.adjusted(-1, -1, 1, 1); + Ph::fillRectOutline(painter, r, 1, swatch.color(S_base_divider)); + } + QCommonStyle::drawControl(element, option, painter, widget); + break; + } + case CE_ShapedFrame: { + auto frameopt = qstyleoption_cast(option); + if (frameopt) { + if (frameopt->frameShape == QFrame::HLine) { + QRect r = option->rect; + r.setY(r.y() + r.height() / 2); + r.setHeight(2); + painter->fillRect(r, swatch.color(S_tabFrame_specular)); + r.setHeight(1); + painter->fillRect(r, swatch.color(S_frame_outline)); + break; + } else if (frameopt->frameShape == QFrame::VLine) { + QRect r = option->rect; + r.setX(r.x() + r.width() / 2); + r.setWidth(2); + painter->fillRect(r, swatch.color(S_tabFrame_specular)); + r.setWidth(1); + painter->fillRect(r, swatch.color(S_frame_outline)); + break; + } + } + QCommonStyle::drawControl(element, option, painter, widget); + break; + } + default: + QCommonStyle::drawControl(element, option, painter, widget); + break; + } +} + +QPalette BaseStyle::lightModePalette() +{ + QPalette palette; + palette.setColor(QPalette::Active, QPalette::Window, QRgb(0xF7F7F7)); + palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0xFCFCFC)); + palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0xEDEDED)); + + palette.setColor(QPalette::Active, QPalette::WindowText, QRgb(0x1D1D20)); + palette.setColor(QPalette::Inactive, QPalette::WindowText, QRgb(0x252528)); + palette.setColor(QPalette::Disabled, QPalette::WindowText, QRgb(0x8C8C92)); + + palette.setColor(QPalette::Active, QPalette::Text, QRgb(0x1D1D20)); + palette.setColor(QPalette::Inactive, QPalette::Text, QRgb(0x252528)); + palette.setColor(QPalette::Disabled, QPalette::Text, QRgb(0x8C8C92)); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) + palette.setColor(QPalette::Active, QPalette::PlaceholderText, QRgb(0x71727D)); + palette.setColor(QPalette::Inactive, QPalette::PlaceholderText, QRgb(0x878893)); + palette.setColor(QPalette::Disabled, QPalette::PlaceholderText, QRgb(0xA3A4AC)); +#endif + + palette.setColor(QPalette::Active, QPalette::BrightText, QRgb(0xF3F3F4)); + palette.setColor(QPalette::Inactive, QPalette::BrightText, QRgb(0xEAEAEB)); + palette.setColor(QPalette::Disabled, QPalette::BrightText, QRgb(0xE4E5E7)); + + palette.setColor(QPalette::Active, QPalette::Base, QRgb(0xF9F9F9)); + palette.setColor(QPalette::Inactive, QPalette::Base, QRgb(0xFCFCFC)); + palette.setColor(QPalette::Disabled, QPalette::Base, QRgb(0xEFEFF2)); + + palette.setColor(QPalette::Active, QPalette::AlternateBase, QRgb(0xECF3E8)); + palette.setColor(QPalette::Inactive, QPalette::AlternateBase, QRgb(0xF1F6EE)); + palette.setColor(QPalette::Disabled, QPalette::AlternateBase, QRgb(0xE1E9DD)); + + palette.setColor(QPalette::All, QPalette::ToolTipBase, QRgb(0xF7F7F7)); + palette.setColor(QPalette::All, QPalette::ToolTipText, QRgb(0x1D1D20)); + + palette.setColor(QPalette::Active, QPalette::Button, QRgb(0xD4D5DD)); + palette.setColor(QPalette::Inactive, QPalette::Button, QRgb(0xDCDCE0)); + palette.setColor(QPalette::Disabled, QPalette::Button, QRgb(0xE5E5E6)); + + palette.setColor(QPalette::Active, QPalette::ButtonText, QRgb(0x181A18)); + palette.setColor(QPalette::Inactive, QPalette::ButtonText, QRgb(0x454A54)); + palette.setColor(QPalette::Disabled, QPalette::ButtonText, QRgb(0x97979B)); + + palette.setColor(QPalette::Active, QPalette::Highlight, QRgb(0x549CFF)); + palette.setColor(QPalette::Inactive, QPalette::Highlight, QRgb(0x96C2FF)); + palette.setColor(QPalette::Disabled, QPalette::Highlight, QRgb(0xBFDAFF)); + + palette.setColor(QPalette::Active, QPalette::HighlightedText, QRgb(0xFFFFFF)); + palette.setColor(QPalette::Inactive, QPalette::HighlightedText, QRgb(0x252528)); + palette.setColor(QPalette::Disabled, QPalette::HighlightedText, QRgb(0x8C8C92)); + + palette.setColor(QPalette::All, QPalette::Light, QRgb(0xF9F9F9)); + palette.setColor(QPalette::All, QPalette::Midlight, QRgb(0xE9E9EB)); + palette.setColor(QPalette::All, QPalette::Mid, QRgb(0xC9C9CF)); + palette.setColor(QPalette::All, QPalette::Dark, QRgb(0xBBBBC2)); + palette.setColor(QPalette::All, QPalette::Shadow, QRgb(0x6C6D79)); + + palette.setColor(QPalette::All, QPalette::Link, QRgb(0x4090FF)); + palette.setColor(QPalette::Disabled, QPalette::Link, QRgb(0x3388FF)); + palette.setColor(QPalette::All, QPalette::LinkVisited, QRgb(0x4090FF)); + palette.setColor(QPalette::Disabled, QPalette::LinkVisited, QRgb(0x3388FF)); + + return palette; +} + +QPalette BaseStyle::darkModePalette() +{ + QPalette palette; + palette.setColor(QPalette::Active, QPalette::Window, QRgb(0x3B3B3D)); + palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0x404042)); + palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0x424242)); + + palette.setColor(QPalette::Active, QPalette::WindowText, QRgb(0xCACBCE)); + palette.setColor(QPalette::Inactive, QPalette::WindowText, QRgb(0xC8C8C6)); + palette.setColor(QPalette::Disabled, QPalette::WindowText, QRgb(0x707070)); + + palette.setColor(QPalette::Active, QPalette::Text, QRgb(0xCACBCE)); + palette.setColor(QPalette::Inactive, QPalette::Text, QRgb(0xC8C8C6)); + palette.setColor(QPalette::Disabled, QPalette::Text, QRgb(0x707070)); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) + palette.setColor(QPalette::Active, QPalette::PlaceholderText, QRgb(0x7D7D82)); + palette.setColor(QPalette::Inactive, QPalette::PlaceholderText, QRgb(0x87888C)); + palette.setColor(QPalette::Disabled, QPalette::PlaceholderText, QRgb(0x737373)); +#endif + + palette.setColor(QPalette::Active, QPalette::BrightText, QRgb(0x252627)); + palette.setColor(QPalette::Inactive, QPalette::BrightText, QRgb(0x2D2D2F)); + palette.setColor(QPalette::Disabled, QPalette::BrightText, QRgb(0x333333)); + + palette.setColor(QPalette::Active, QPalette::Base, QRgb(0x27272A)); + palette.setColor(QPalette::Inactive, QPalette::Base, QRgb(0x2A2A2D)); + palette.setColor(QPalette::Disabled, QPalette::Base, QRgb(0x343437)); + + palette.setColor(QPalette::Active, QPalette::AlternateBase, QRgb(0x2C2C30)); + palette.setColor(QPalette::Inactive, QPalette::AlternateBase, QRgb(0x2B2B2F)); + palette.setColor(QPalette::Disabled, QPalette::AlternateBase, QRgb(0x36363A)); + + palette.setColor(QPalette::All, QPalette::ToolTipBase, QRgb(0x3B3B3D)); + palette.setColor(QPalette::All, QPalette::ToolTipText, QRgb(0xCACBCE)); + + palette.setColor(QPalette::Active, QPalette::Button, QRgb(0x28282B)); + palette.setColor(QPalette::Inactive, QPalette::Button, QRgb(0x28282B)); + palette.setColor(QPalette::Disabled, QPalette::Button, QRgb(0x2B2A2A)); + + palette.setColor(QPalette::Active, QPalette::ButtonText, QRgb(0xB9B9BE)); + palette.setColor(QPalette::Inactive, QPalette::ButtonText, QRgb(0x9E9FA5)); + palette.setColor(QPalette::Disabled, QPalette::ButtonText, QRgb(0x73747E)); + + palette.setColor(QPalette::Active, QPalette::Highlight, QRgb(0x447FCF)); + palette.setColor(QPalette::Inactive, QPalette::Highlight, QRgb(0x3B6EB3)); + palette.setColor(QPalette::Disabled, QPalette::Highlight, QRgb(0x315B94)); + + palette.setColor(QPalette::Active, QPalette::HighlightedText, QRgb(0xCCCCCC)); + palette.setColor(QPalette::Inactive, QPalette::HighlightedText, QRgb(0xCECECE)); + palette.setColor(QPalette::Disabled, QPalette::HighlightedText, QRgb(0x707070)); + + palette.setColor(QPalette::All, QPalette::Light, QRgb(0x414145)); + palette.setColor(QPalette::All, QPalette::Midlight, QRgb(0x39393C)); + palette.setColor(QPalette::All, QPalette::Mid, QRgb(0x2F2F32)); + palette.setColor(QPalette::All, QPalette::Dark, QRgb(0x202022)); + palette.setColor(QPalette::All, QPalette::Shadow, QRgb(0x19191A)); + + palette.setColor(QPalette::All, QPalette::Link, QRgb(0x68B668)); + palette.setColor(QPalette::Disabled, QPalette::Link, QRgb(0x74A474)); + palette.setColor(QPalette::All, QPalette::LinkVisited, QRgb(0x75B875)); + palette.setColor(QPalette::Disabled, QPalette::LinkVisited, QRgb(0x77A677)); + + return palette; +} + +QPalette BaseStyle::standardPalette() const +{ + return isDarkMode() ? darkModePalette() : lightModePalette(); + + // QColor backGround(251, 251, 251); + // QColor light = backGround.lighter(150); + // QColor mid(backGround.darker(130)); + // QColor midLight = mid.lighter(110); + // QColor base = Qt::white; + // QColor disabledBase(backGround); + // QColor dark = backGround.darker(150); + // QColor darkDisabled = QColor(209, 209, 209).darker(110); + // QColor text = Qt::black; + // QColor hightlightedText = Qt::white; + // QColor disabledText = QColor(190, 190, 190); + // QColor button = QColor(242, 242, 242); + // QColor shadow = dark.darker(135); + // QColor disabledShadow = shadow.lighter(150); + // QColor placeholder = text; + // QColor highlightColor(84, 156, 255); + // placeholder.setAlpha(128); + + // QPalette fusionPalette(Qt::black, backGround, light, dark, mid, text, base); + // fusionPalette.setBrush(QPalette::Midlight, midLight); + // fusionPalette.setBrush(QPalette::Button, button); + // fusionPalette.setBrush(QPalette::Shadow, shadow); + // fusionPalette.setBrush(QPalette::HighlightedText, hightlightedText); + + // fusionPalette.setBrush(QPalette::Disabled, QPalette::Text, disabledText); + // fusionPalette.setBrush(QPalette::Disabled, QPalette::WindowText, disabledText); + // fusionPalette.setBrush(QPalette::Disabled, QPalette::ButtonText, disabledText); + // fusionPalette.setBrush(QPalette::Disabled, QPalette::Base, disabledBase); + // fusionPalette.setBrush(QPalette::Disabled, QPalette::Dark, darkDisabled); + // fusionPalette.setBrush(QPalette::Disabled, QPalette::Shadow, disabledShadow); + + // fusionPalette.setBrush(QPalette::Highlight, highlightColor); + // fusionPalette.setBrush(QPalette::Active, QPalette::Highlight, highlightColor); + // fusionPalette.setBrush(QPalette::Inactive, QPalette::Highlight, highlightColor); + // fusionPalette.setBrush(QPalette::Disabled, QPalette::Highlight, QColor(145, 145, 145)); + + // fusionPalette.setBrush(QPalette::Base, base); + // fusionPalette.setBrush(QPalette::Window, base); + + // fusionPalette.setBrush(QPalette::PlaceholderText, placeholder); + + // return fusionPalette; +} + +void BaseStyle::drawComplexControl(ComplexControl control, + const QStyleOptionComplex* option, + QPainter* painter, + const QWidget* widget) const +{ +#ifdef BUILD_WITH_EASY_PROFILER + EASY_BLOCK("drawControl"); + const char* controlCString = QMetaEnum::fromType().valueToKey(control); + EASY_TEXT("ComplexControl", controlCString); +#endif + using Swatchy = Phantom::Swatchy; + using namespace Phantom::SwatchColors; + namespace Ph = Phantom; + auto ph_swatchPtr = Ph::getCachedSwatchOfQPalette(&d->swatchCache, &d->headSwatchFastKey, option->palette); + const Ph::PhSwatch& swatch = *ph_swatchPtr.data(); + + switch (control) { + case CC_GroupBox: { + auto groupBox = qstyleoption_cast(option); + if (!groupBox) + break; + painter->save(); + // Draw frame + QRect textRect = proxy()->subControlRect(CC_GroupBox, option, SC_GroupBoxLabel, widget); + QRect checkBoxRect = proxy()->subControlRect(CC_GroupBox, option, SC_GroupBoxCheckBox, widget); + + if (groupBox->subControls & QStyle::SC_GroupBoxFrame) { + QStyleOptionFrame frame; + frame.QStyleOption::operator=(*groupBox); + frame.features = groupBox->features; + frame.lineWidth = groupBox->lineWidth; + frame.midLineWidth = groupBox->midLineWidth; + frame.rect = proxy()->subControlRect(CC_GroupBox, option, SC_GroupBoxFrame, widget); + proxy()->drawPrimitive(PE_FrameGroupBox, &frame, painter, widget); + } + + // Draw title + if ((groupBox->subControls & QStyle::SC_GroupBoxLabel) && !groupBox->text.isEmpty()) { + // groupBox->textColor gets the incorrect palette here + painter->setPen(QPen(option->palette.windowText(), 1)); + unsigned alignment = groupBox->textAlignment; + if (!proxy()->styleHint(QStyle::SH_UnderlineShortcut, option, widget)) + alignment |= Qt::TextHideMnemonic; + + proxy()->drawItemText(painter, + textRect, + alignment | Qt::TextShowMnemonic | Qt::AlignLeft, + groupBox->palette, + groupBox->state & State_Enabled, + groupBox->text, + QPalette::NoRole); + + if (groupBox->state & State_HasFocus) { + QStyleOptionFocusRect fropt; + fropt.QStyleOption::operator=(*groupBox); + fropt.rect = textRect.adjusted(-1, 0, 1, 0); + proxy()->drawPrimitive(PE_FrameFocusRect, &fropt, painter, widget); + } + } + + // Draw checkbox + if (groupBox->subControls & SC_GroupBoxCheckBox) { + QStyleOptionButton box; + box.QStyleOption::operator=(*groupBox); + box.rect = checkBoxRect; + proxy()->drawPrimitive(PE_IndicatorCheckBox, &box, painter, widget); + } + painter->restore(); + break; + } + case CC_SpinBox: { + auto spinBox = qstyleoption_cast(option); + if (!spinBox) + break; + const qreal rounding = Ph::SpinBox_Rounding; + bool isLeftToRight = option->direction != Qt::RightToLeft; + const QRect rect = spinBox->rect; + bool sunken = spinBox->state & State_Sunken; + bool upIsActive = spinBox->activeSubControls == SC_SpinBoxUp; + bool downIsActive = spinBox->activeSubControls == SC_SpinBoxDown; + bool hasFocus = option->state & State_HasFocus; + bool isEnabled = option->state & State_Enabled; + QRect upRect = proxy()->subControlRect(CC_SpinBox, spinBox, SC_SpinBoxUp, widget); + QRect downRect = proxy()->subControlRect(CC_SpinBox, spinBox, SC_SpinBoxDown, widget); + if (spinBox->frame) { + QRect upDownRect = upRect | downRect; + upDownRect.adjust(0, -1, 0, 1); + painter->save(); // 0 + // Fill background + Ph::paintBorderedRoundRect(painter, rect, rounding, swatch, S_none, S_base); + // Draw button fill + painter->setClipRect(upDownRect); + // Side with the border + Qt::Edge edge = isLeftToRight ? Qt::LeftEdge : Qt::RightEdge; + Ph::paintBorderedRoundRect( + painter, Ph::expandRect(upDownRect, Ph::oppositeEdge(edge), -1), rounding, swatch, S_none, S_button); + painter->restore(); // 0 + if (Ph::OverhangShadows && !hasFocus && isEnabled) { + // Imperfect, leaves tiny gap on left and right. Going closer would eat + // into the outline, though. + QRect shadowRect = rect.adjusted(qRound(rounding / 2), 1, -qRound(rounding / 2), -1); + if (isLeftToRight) { + shadowRect.setRight(upDownRect.left()); + } else { + shadowRect.setLeft(upDownRect.right()); + } + Ph::fillRectEdges(painter, shadowRect, Qt::TopEdge, 1, swatch.color(S_base_shadow)); + } + if ((spinBox->stepEnabled & QAbstractSpinBox::StepUpEnabled) && upIsActive && sunken) { + painter->fillRect(upRect, swatch.color(S_button_pressed)); + } + if ((spinBox->stepEnabled & QAbstractSpinBox::StepDownEnabled) && downIsActive && sunken) { + painter->fillRect(downRect, swatch.color(S_button_pressed)); + } + // Left or right border line + Ph::fillRectEdges(painter, upDownRect, edge, 1, swatch.color(S_window_outline)); + Ph::PSave save(painter); + // Outline over entire frame + Swatchy outlineColor = hasFocus ? S_highlight_outline : S_window_outline; + Ph::paintBorderedRoundRect(painter, rect, rounding, swatch, outlineColor, S_none); + save.restore(); + } + + if (spinBox->buttonSymbols == QAbstractSpinBox::PlusMinus) { + Ph::PSave save(painter); + // TODO fix up old fusion code here + int centerX = upRect.center().x(); + int centerY = upRect.center().y(); + Swatchy arrowColorUp = + spinBox->stepEnabled & QAbstractSpinBox::StepUpEnabled ? S_indicator_current : S_indicator_disabled; + Swatchy arrowColorDown = + spinBox->stepEnabled & QAbstractSpinBox::StepDownEnabled ? S_indicator_current : S_indicator_disabled; + painter->setPen(swatch.pen(arrowColorUp)); + painter->drawLine(centerX - 1, centerY, centerX + 3, centerY); + painter->drawLine(centerX + 1, centerY - 2, centerX + 1, centerY + 2); + centerX = downRect.center().x(); + centerY = downRect.center().y(); + painter->setPen(arrowColorDown); + painter->drawLine(centerX - 1, centerY, centerX + 3, centerY); + } else if (spinBox->buttonSymbols == QAbstractSpinBox::UpDownArrows) { + int xoffs = isLeftToRight ? 0 : 1; + Ph::drawArrow(painter, + upRect.adjusted(4 + xoffs, 1, -5 + xoffs, 1), + Qt::UpArrow, + swatch, + spinBox->stepEnabled & QAbstractSpinBox::StepUpEnabled); + Ph::drawArrow(painter, + downRect.adjusted(4 + xoffs, 0, -5 + xoffs, -1), + Qt::DownArrow, + swatch, + spinBox->stepEnabled & QAbstractSpinBox::StepDownEnabled); + } + break; + } + case CC_TitleBar: { + auto titleBar = qstyleoption_cast(option); + if (!titleBar) + break; + painter->save(); + const int buttonMargin = 5; + bool active = (titleBar->titleBarState & State_Active); + QRect fullRect = titleBar->rect; + QPalette palette = option->palette; + QColor highlight = option->palette.highlight().color(); + QColor outline = option->palette.dark().color(); + + QColor titleBarFrameBorder(active ? highlight.darker(180) : outline.darker(110)); + QColor titleBarHighlight(active ? highlight.lighter(120) : palette.window().color().lighter(120)); + QColor textColor(active ? 0xffffff : 0xff000000); + QColor textAlphaColor(active ? 0xffffff : 0xff000000); + + { + // Fill title + QColor titlebarColor = QColor(active ? highlight : palette.window().color()); + painter->fillRect(option->rect.adjusted(1, 1, -1, 0), titlebarColor); + // Frame and rounded corners + painter->setPen(titleBarFrameBorder); + + // top outline + painter->drawLine(fullRect.left() + 5, fullRect.top(), fullRect.right() - 5, fullRect.top()); + painter->drawLine(fullRect.left(), fullRect.top() + 4, fullRect.left(), fullRect.bottom()); + const QPoint points[5] = {QPoint(fullRect.left() + 4, fullRect.top() + 1), + QPoint(fullRect.left() + 3, fullRect.top() + 1), + QPoint(fullRect.left() + 2, fullRect.top() + 2), + QPoint(fullRect.left() + 1, fullRect.top() + 3), + QPoint(fullRect.left() + 1, fullRect.top() + 4)}; + painter->drawPoints(points, 5); + + painter->drawLine(fullRect.right(), fullRect.top() + 4, fullRect.right(), fullRect.bottom()); + const QPoint points2[5] = {QPoint(fullRect.right() - 3, fullRect.top() + 1), + QPoint(fullRect.right() - 4, fullRect.top() + 1), + QPoint(fullRect.right() - 2, fullRect.top() + 2), + QPoint(fullRect.right() - 1, fullRect.top() + 3), + QPoint(fullRect.right() - 1, fullRect.top() + 4)}; + painter->drawPoints(points2, 5); + + // draw bottomline + painter->drawLine(fullRect.right(), fullRect.bottom(), fullRect.left(), fullRect.bottom()); + + // top highlight + painter->setPen(titleBarHighlight); + painter->drawLine(fullRect.left() + 6, fullRect.top() + 1, fullRect.right() - 6, fullRect.top() + 1); + } + // draw title + QRect textRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarLabel, widget); + painter->setPen(active ? (titleBar->palette.text().color().lighter(120)) : titleBar->palette.text().color()); + // Note workspace also does elliding but it does not use the correct font + QString title = painter->fontMetrics().elidedText(titleBar->text, Qt::ElideRight, textRect.width() - 14); + painter->drawText(textRect.adjusted(1, 1, 1, 1), title, QTextOption(Qt::AlignHCenter | Qt::AlignVCenter)); + painter->setPen(Qt::white); + if (active) + painter->drawText(textRect, title, QTextOption(Qt::AlignHCenter | Qt::AlignVCenter)); + // min button + if ((titleBar->subControls & SC_TitleBarMinButton) && (titleBar->titleBarFlags & Qt::WindowMinimizeButtonHint) + && !(titleBar->titleBarState & Qt::WindowMinimized)) { + QRect minButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarMinButton, widget); + if (minButtonRect.isValid()) { + bool hover = + (titleBar->activeSubControls & SC_TitleBarMinButton) && (titleBar->state & State_MouseOver); + bool sunken = (titleBar->activeSubControls & SC_TitleBarMinButton) && (titleBar->state & State_Sunken); + Ph::drawMdiButton(painter, titleBar, minButtonRect, hover, sunken); + QRect minButtonIconRect = + minButtonRect.adjusted(buttonMargin, buttonMargin, -buttonMargin, -buttonMargin); + painter->setPen(textColor); + painter->drawLine(minButtonIconRect.center().x() - 2, + minButtonIconRect.center().y() + 3, + minButtonIconRect.center().x() + 3, + minButtonIconRect.center().y() + 3); + painter->drawLine(minButtonIconRect.center().x() - 2, + minButtonIconRect.center().y() + 4, + minButtonIconRect.center().x() + 3, + minButtonIconRect.center().y() + 4); + painter->setPen(textAlphaColor); + painter->drawLine(minButtonIconRect.center().x() - 3, + minButtonIconRect.center().y() + 3, + minButtonIconRect.center().x() - 3, + minButtonIconRect.center().y() + 4); + painter->drawLine(minButtonIconRect.center().x() + 4, + minButtonIconRect.center().y() + 3, + minButtonIconRect.center().x() + 4, + minButtonIconRect.center().y() + 4); + } + } + // max button + if ((titleBar->subControls & SC_TitleBarMaxButton) && (titleBar->titleBarFlags & Qt::WindowMaximizeButtonHint) + && !(titleBar->titleBarState & Qt::WindowMaximized)) { + QRect maxButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarMaxButton, widget); + if (maxButtonRect.isValid()) { + bool hover = + (titleBar->activeSubControls & SC_TitleBarMaxButton) && (titleBar->state & State_MouseOver); + bool sunken = (titleBar->activeSubControls & SC_TitleBarMaxButton) && (titleBar->state & State_Sunken); + Ph::drawMdiButton(painter, titleBar, maxButtonRect, hover, sunken); + + QRect maxButtonIconRect = + maxButtonRect.adjusted(buttonMargin, buttonMargin, -buttonMargin, -buttonMargin); + + painter->setPen(textColor); + painter->drawRect(maxButtonIconRect.adjusted(0, 0, -1, -1)); + painter->drawLine(maxButtonIconRect.left() + 1, + maxButtonIconRect.top() + 1, + maxButtonIconRect.right() - 1, + maxButtonIconRect.top() + 1); + painter->setPen(textAlphaColor); + const QPoint points[4] = {maxButtonIconRect.topLeft(), + maxButtonIconRect.topRight(), + maxButtonIconRect.bottomLeft(), + maxButtonIconRect.bottomRight()}; + painter->drawPoints(points, 4); + } + } + + // close button + if ((titleBar->subControls & SC_TitleBarCloseButton) && (titleBar->titleBarFlags & Qt::WindowSystemMenuHint)) { + QRect closeButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarCloseButton, widget); + if (closeButtonRect.isValid()) { + bool hover = + (titleBar->activeSubControls & SC_TitleBarCloseButton) && (titleBar->state & State_MouseOver); + bool sunken = + (titleBar->activeSubControls & SC_TitleBarCloseButton) && (titleBar->state & State_Sunken); + Ph::drawMdiButton(painter, titleBar, closeButtonRect, hover, sunken); + QRect closeIconRect = + closeButtonRect.adjusted(buttonMargin, buttonMargin, -buttonMargin, -buttonMargin); + painter->setPen(textAlphaColor); + const QLine lines[4] = {QLine(closeIconRect.left() + 1, + closeIconRect.top(), + closeIconRect.right(), + closeIconRect.bottom() - 1), + QLine(closeIconRect.left(), + closeIconRect.top() + 1, + closeIconRect.right() - 1, + closeIconRect.bottom()), + QLine(closeIconRect.right() - 1, + closeIconRect.top(), + closeIconRect.left(), + closeIconRect.bottom() - 1), + QLine(closeIconRect.right(), + closeIconRect.top() + 1, + closeIconRect.left() + 1, + closeIconRect.bottom())}; + painter->drawLines(lines, 4); + const QPoint points[4] = {closeIconRect.topLeft(), + closeIconRect.topRight(), + closeIconRect.bottomLeft(), + closeIconRect.bottomRight()}; + painter->drawPoints(points, 4); + + painter->setPen(textColor); + painter->drawLine(closeIconRect.left() + 1, + closeIconRect.top() + 1, + closeIconRect.right() - 1, + closeIconRect.bottom() - 1); + painter->drawLine(closeIconRect.left() + 1, + closeIconRect.bottom() - 1, + closeIconRect.right() - 1, + closeIconRect.top() + 1); + } + } + + // normalize button + if ((titleBar->subControls & SC_TitleBarNormalButton) + && (((titleBar->titleBarFlags & Qt::WindowMinimizeButtonHint) + && (titleBar->titleBarState & Qt::WindowMinimized)) + || ((titleBar->titleBarFlags & Qt::WindowMaximizeButtonHint) + && (titleBar->titleBarState & Qt::WindowMaximized)))) { + QRect normalButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarNormalButton, widget); + if (normalButtonRect.isValid()) { + + bool hover = + (titleBar->activeSubControls & SC_TitleBarNormalButton) && (titleBar->state & State_MouseOver); + bool sunken = + (titleBar->activeSubControls & SC_TitleBarNormalButton) && (titleBar->state & State_Sunken); + QRect normalButtonIconRect = + normalButtonRect.adjusted(buttonMargin, buttonMargin, -buttonMargin, -buttonMargin); + Ph::drawMdiButton(painter, titleBar, normalButtonRect, hover, sunken); + + QRect frontWindowRect = normalButtonIconRect.adjusted(0, 3, -3, 0); + painter->setPen(textColor); + painter->drawRect(frontWindowRect.adjusted(0, 0, -1, -1)); + painter->drawLine(frontWindowRect.left() + 1, + frontWindowRect.top() + 1, + frontWindowRect.right() - 1, + frontWindowRect.top() + 1); + painter->setPen(textAlphaColor); + const QPoint points[4] = {frontWindowRect.topLeft(), + frontWindowRect.topRight(), + frontWindowRect.bottomLeft(), + frontWindowRect.bottomRight()}; + painter->drawPoints(points, 4); + + QRect backWindowRect = normalButtonIconRect.adjusted(3, 0, 0, -3); + QRegion clipRegion = backWindowRect; + clipRegion -= frontWindowRect; + painter->save(); + painter->setClipRegion(clipRegion); + painter->setPen(textColor); + painter->drawRect(backWindowRect.adjusted(0, 0, -1, -1)); + painter->drawLine(backWindowRect.left() + 1, + backWindowRect.top() + 1, + backWindowRect.right() - 1, + backWindowRect.top() + 1); + painter->setPen(textAlphaColor); + const QPoint points2[4] = {backWindowRect.topLeft(), + backWindowRect.topRight(), + backWindowRect.bottomLeft(), + backWindowRect.bottomRight()}; + painter->drawPoints(points2, 4); + painter->restore(); + } + } + + // context help button + if (titleBar->subControls & SC_TitleBarContextHelpButton + && (titleBar->titleBarFlags & Qt::WindowContextHelpButtonHint)) { + QRect contextHelpButtonRect = + proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarContextHelpButton, widget); + if (contextHelpButtonRect.isValid()) { + bool hover = + (titleBar->activeSubControls & SC_TitleBarContextHelpButton) && (titleBar->state & State_MouseOver); + bool sunken = + (titleBar->activeSubControls & SC_TitleBarContextHelpButton) && (titleBar->state & State_Sunken); + Ph::drawMdiButton(painter, titleBar, contextHelpButtonRect, hover, sunken); + // This is lame, but I doubt it will get used often. Previously, XPM + // icon was used here (very poorly, by re-allocating a QImage over and + // over and modifying/painting it) + QIcon helpIcon = QCommonStyle::standardIcon(QStyle::SP_DialogHelpButton); + helpIcon.paint(painter, contextHelpButtonRect.adjusted(4, 4, -4, -4)); + } + } + + // shade button + if (titleBar->subControls & SC_TitleBarShadeButton && (titleBar->titleBarFlags & Qt::WindowShadeButtonHint)) { + QRect shadeButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarShadeButton, widget); + if (shadeButtonRect.isValid()) { + bool hover = + (titleBar->activeSubControls & SC_TitleBarShadeButton) && (titleBar->state & State_MouseOver); + bool sunken = + (titleBar->activeSubControls & SC_TitleBarShadeButton) && (titleBar->state & State_Sunken); + Ph::drawMdiButton(painter, titleBar, shadeButtonRect, hover, sunken); + Ph::drawArrow(painter, shadeButtonRect.adjusted(5, 7, -5, -7), Qt::UpArrow, swatch); + } + } + + // unshade button + if (titleBar->subControls & SC_TitleBarUnshadeButton && (titleBar->titleBarFlags & Qt::WindowShadeButtonHint)) { + QRect unshadeButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarUnshadeButton, widget); + if (unshadeButtonRect.isValid()) { + bool hover = + (titleBar->activeSubControls & SC_TitleBarUnshadeButton) && (titleBar->state & State_MouseOver); + bool sunken = + (titleBar->activeSubControls & SC_TitleBarUnshadeButton) && (titleBar->state & State_Sunken); + Ph::drawMdiButton(painter, titleBar, unshadeButtonRect, hover, sunken); + Ph::drawArrow(painter, unshadeButtonRect.adjusted(5, 7, -5, -7), Qt::DownArrow, swatch); + } + } + + if ((titleBar->subControls & SC_TitleBarSysMenu) && (titleBar->titleBarFlags & Qt::WindowSystemMenuHint)) { + QRect iconRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarSysMenu, widget); + if (iconRect.isValid()) { + if (!titleBar->icon.isNull()) { + titleBar->icon.paint(painter, iconRect); + } else { + QStyleOption tool = *titleBar; + QPixmap pm = proxy()->standardIcon(SP_TitleBarMenuButton, &tool, widget).pixmap(16, 16); + tool.rect = iconRect; + painter->save(); + proxy()->drawItemPixmap(painter, iconRect, Qt::AlignCenter, pm); + painter->restore(); + } + } + } + painter->restore(); + break; + } + case CC_ScrollBar: { + auto scrollBar = qstyleoption_cast(option); + if (!scrollBar) + break; + auto pr = proxy(); + QRect scrollBarSubLine = pr->subControlRect(control, scrollBar, SC_ScrollBarSubLine, widget); + QRect scrollBarAddLine = pr->subControlRect(control, scrollBar, SC_ScrollBarAddLine, widget); + QRect scrollBarSlider = pr->subControlRect(control, scrollBar, SC_ScrollBarSlider, widget); + QRect scrollBarGroove = pr->subControlRect(control, scrollBar, SC_ScrollBarGroove, widget); + + int padding = Ph::dpiScaled(4); + scrollBarSlider.setX(scrollBarSlider.x() + padding); + scrollBarSlider.setY(scrollBarSlider.y() + padding); + // Width and height should be reduced by 2 * padding, but somehow padding is enough. + scrollBarSlider.setWidth(scrollBarSlider.width() - padding); + scrollBarSlider.setHeight(scrollBarSlider.height() - padding); + + // Groove/gutter/trench area + if (scrollBar->subControls & SC_ScrollBarGroove) { + painter->fillRect(scrollBarGroove, swatch.color(S_window)); + } + + // Slider thumb + if (scrollBar->subControls & SC_ScrollBarSlider) { + qreal radius = + (scrollBar->orientation == Qt::Horizontal ? scrollBarSlider.height() : scrollBarSlider.width()) / 2.0; + bool mouseOver((option->state & State_Active) && option->state & State_MouseOver); + bool mousePress(option->state & State_Sunken); + painter->fillRect(scrollBarSlider, swatch.color(S_window)); + // Ph::paintSolidRoundRect(painter, scrollBarSlider, radius, swatch, S_scrollbarSlider); + painter->save(); + painter->setRenderHint(QPainter::Antialiasing); + painter->setPen(Qt::NoPen); + painter->setBrush(mouseOver ? swatch.brush(S_scrollbarSlider_hover) : swatch.brush(S_scrollbarSlider)); + if (mousePress) + painter->setBrush(swatch.brush(S_scrollbarSlider_pressed)); + painter->drawRoundedRect(scrollBarSlider, radius, radius); + painter->restore(); + } + + // The SubLine (up/left) buttons + if (scrollBar->subControls & SC_ScrollBarSubLine) { + painter->fillRect(scrollBarSubLine, swatch.color(S_window)); + } + + // The AddLine (down/right) button + if (scrollBar->subControls & SC_ScrollBarAddLine) { + painter->fillRect(scrollBarAddLine, swatch.color(S_window)); + } + break; + } + case CC_ComboBox: { + auto comboBox = qstyleoption_cast(option); + if (!comboBox) + break; + painter->save(); + bool isLeftToRight = option->direction != Qt::RightToLeft; + bool hasFocus = option->state & State_HasFocus && option->state & State_KeyboardFocusChange; + bool isSunken = comboBox->state & State_Sunken; + QRect rect = comboBox->rect; + QRect downArrowRect = proxy()->subControlRect(CC_ComboBox, comboBox, SC_ComboBoxArrow, widget); + // Draw a line edit + if (comboBox->editable) { + Swatchy buttonFill = isSunken ? S_button_pressed : S_button; + // if (!hasOptions) + // buttonFill = S_window; + painter->fillRect(rect, swatch.color(buttonFill)); + if (comboBox->frame) { + QStyleOptionFrame buttonOption; + buttonOption.QStyleOption::operator=(*comboBox); + buttonOption.rect = rect; + buttonOption.state = + (comboBox->state & (State_Enabled | State_MouseOver | State_HasFocus)) | State_KeyboardFocusChange; + if (isSunken) { + buttonOption.state |= State_Sunken; + buttonOption.state &= ~State_MouseOver; + } + proxy()->drawPrimitive(PE_FrameLineEdit, &buttonOption, painter, widget); + QRect fr = proxy()->subControlRect(CC_ComboBox, option, SC_ComboBoxEditField, widget); + QRect br = rect; + if (isLeftToRight) { + br.setLeft(fr.x() + fr.width()); + } else { + br.setRight(fr.left() - 1); + } + Qt::Edge edge = isLeftToRight ? Qt::LeftEdge : Qt::RightEdge; + Swatchy color = hasFocus ? S_highlight_outline : S_window_outline; + br.adjust(0, 1, 0, -1); + Ph::fillRectEdges(painter, br, edge, 1, swatch.color(color)); + br.adjust(1, 0, -1, 0); + Swatchy specular = isSunken ? S_button_pressed_specular : S_button_specular; + Ph::fillRectOutline(painter, br, 1, swatch.color(specular)); + } + } else { + QStyleOptionButton buttonOption; + buttonOption.QStyleOption::operator=(*comboBox); + buttonOption.rect = rect; + buttonOption.state = + comboBox->state + & (State_Enabled | State_MouseOver | State_HasFocus | State_Active | State_KeyboardFocusChange); + // Combo boxes should be shown to be keyboard interactive if they're + // focused at all, not just if the user has pressed tab to enter keyboard + // focus change mode. This is because the up/down arrows can, regardless + // of having pressed tab, control the combo box selection. + if (comboBox->state & State_HasFocus) + buttonOption.state |= State_KeyboardFocusChange; + if (isSunken) { + buttonOption.state |= State_Sunken; + buttonOption.state &= ~State_MouseOver; + } + proxy()->drawPrimitive(PE_PanelButtonCommand, &buttonOption, painter, widget); + } + if (comboBox->subControls & SC_ComboBoxArrow) { + int margin = + static_cast(qMin(downArrowRect.width(), downArrowRect.height()) * Ph::ComboBox_ArrowMarginRatio); + QRect r = downArrowRect; + r.adjust(margin, margin, -margin, -margin); + // Draw the up/down arrow + Ph::drawArrow(painter, r, Qt::DownArrow, swatch); + } + painter->restore(); + break; + } + case CC_Slider: { + auto slider = qstyleoption_cast(option); + if (!slider) + break; + const QRect groove = proxy()->subControlRect(CC_Slider, option, SC_SliderGroove, widget); + const QRect handle = proxy()->subControlRect(CC_Slider, option, SC_SliderHandle, widget); + bool horizontal = slider->orientation == Qt::Horizontal; + bool ticksAbove = slider->tickPosition & QSlider::TicksAbove; + bool ticksBelow = slider->tickPosition & QSlider::TicksBelow; + Swatchy outlineColor = S_window_outline; + if (option->state & State_HasFocus && option->state & State_KeyboardFocusChange) + outlineColor = S_highlight_outline; + if ((option->subControls & SC_SliderGroove) && groove.isValid()) { + QRect g0 = groove; + if (g0.height() > 5) + g0.adjust(0, 1, 0, -1); + Ph::PSave saver(painter); + Swatchy gutterColor = option->state & State_Enabled ? S_scrollbarGutter : S_window; + Ph::paintBorderedRoundRect(painter, groove, Ph::SliderGroove_Rounding, swatch, outlineColor, gutterColor); + } + if (option->subControls & SC_SliderTickmarks) { + Ph::PSave save(painter); + painter->setPen(swatch.pen(S_window_outline)); + int tickSize = proxy()->pixelMetric(PM_SliderTickmarkOffset, option, widget); + int available = proxy()->pixelMetric(PM_SliderSpaceAvailable, slider, widget); + int interval = slider->tickInterval; + if (interval <= 0) { + interval = slider->singleStep; + if (QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, interval, available) + - QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, 0, available) + < 3) + interval = slider->pageStep; + } + if (interval <= 0) + interval = 1; + + int v = slider->minimum; + int len = proxy()->pixelMetric(PM_SliderLength, slider, widget); + while (v <= slider->maximum + 1) { + if (v == slider->maximum + 1 && interval == 1) + break; + const int v_ = qMin(v, slider->maximum); + int pos = sliderPositionFromValue(slider->minimum, + slider->maximum, + v_, + (horizontal ? slider->rect.width() : slider->rect.height()) - len, + slider->upsideDown) + + len / 2; + int extra = 2 - ((v_ == slider->minimum || v_ == slider->maximum) ? 1 : 0); + + if (horizontal) { + if (ticksAbove) { + painter->drawLine(pos, slider->rect.top() + extra, pos, slider->rect.top() + tickSize); + } + if (ticksBelow) { + painter->drawLine(pos, slider->rect.bottom() - extra, pos, slider->rect.bottom() - tickSize); + } + } else { + if (ticksAbove) { + painter->drawLine(slider->rect.left() + extra, pos, slider->rect.left() + tickSize, pos); + } + if (ticksBelow) { + painter->drawLine(slider->rect.right() - extra, pos, slider->rect.right() - tickSize, pos); + } + } + // in the case where maximum is max int + int nextInterval = v + interval; + if (nextInterval < v) + break; + v = nextInterval; + } + } + // draw handle + if ((option->subControls & SC_SliderHandle)) { + bool isPressed = option->state & QStyle::State_Sunken && option->activeSubControls & SC_SliderHandle; + QRect r = handle; + Swatchy handleOutline, handleFill, handleSpecular; + if (option->state & State_HasFocus && option->state & State_KeyboardFocusChange) { + handleOutline = S_highlight_outline; + } else { + handleOutline = S_window_outline; + } + if (isPressed) { + handleFill = S_sliderHandle_pressed; + handleSpecular = S_sliderHandle_pressed_specular; + } else { + handleFill = S_sliderHandle; + handleSpecular = S_sliderHandle_specular; + } + Ph::PSave save(painter); + Ph::paintBorderedRoundRect(painter, r, Ph::SliderHandle_Rounding, swatch, handleOutline, handleFill); + r.adjust(1, 1, -1, -1); + Ph::paintBorderedRoundRect(painter, r, Ph::SliderHandle_Rounding, swatch, handleSpecular, S_none); + } + break; + } + case CC_ToolButton: { + auto tbopt = qstyleoption_cast(option); + if (Ph::AllowToolBarAutoRaise || !tbopt || !widget || !widget->parent() + || !widget->parent()->inherits("QToolBar")) { + QCommonStyle::drawComplexControl(control, option, painter, widget); + break; + } + QStyleOptionToolButton opt_; + opt_.QStyleOptionToolButton::operator=(*tbopt); + opt_.state &= ~State_AutoRaise; + QCommonStyle::drawComplexControl(control, &opt_, painter, widget); + break; + } + case CC_Dial: + if (auto dial = qstyleoption_cast(option)) + Ph::drawDial(dial, painter); + break; + default: + QCommonStyle::drawComplexControl(control, option, painter, widget); + break; + } +} + +int BaseStyle::pixelMetric(PixelMetric metric, const QStyleOption* option, const QWidget* widget) const +{ + // Calculate pixel metrics. + // Use immediate return if value is not supposed to be dpi-scaled. + int val = -1; + switch (metric) { + case PM_SliderTickmarkOffset: + val = 6; + break; + case PM_ToolTipLabelFrameWidth: + case PM_HeaderMargin: + case PM_ButtonMargin: + case PM_SpinBoxFrameWidth: + val = Phantom::DefaultFrameWidth; + break; + case PM_ButtonDefaultIndicator: + case PM_ButtonShiftHorizontal: + val = 0; + break; + case PM_ButtonShiftVertical: + if (qobject_cast(widget)) { + return 0; + } + val = 1; + break; + case PM_ComboBoxFrameWidth: + return 1; + case PM_DefaultFrameWidth: + // Original comment from fusion: + // Do not dpi-scale because the drawn frame is always exactly 1 pixel thick + // My note: + // I seriously doubt, with all of the hacky add-or-remove-1 things + // everywhere in fusion (and still in phantom), and the fact that fusion is + // totally broken in high dpi, that this actually holds true. + if (qobject_cast(widget)) { + return 1; + } + val = qMax(1, Phantom::DefaultFrameWidth - 2); + break; + case PM_MessageBoxIconSize: + val = 48; + break; + case PM_DialogButtonsSeparator: + case PM_ScrollBarSliderMin: + val = 26; + break; + case PM_TitleBarHeight: + val = 24; + break; + case PM_ScrollBarExtent: + val = 14; + break; + case PM_SliderThickness: + case PM_SliderLength: + val = 15; + break; + case PM_DockWidgetTitleMargin: + val = 1; + break; + case PM_MenuVMargin: + val = Phantom::DefaultFrame_Radius; + break; + case PM_MenuHMargin: + val = Phantom::DefaultFrame_Radius; + break; + case PM_MenuPanelWidth: + val = 0; + break; + case PM_MenuBarItemSpacing: + val = Phantom::MenuBar_ItemSpacing; + break; + case PM_MenuBarHMargin: + // option is usually nullptr, use widget instead to get font metrics + if (!Phantom::MenuBarLeftMargin || !widget) { + val = 0; + break; + } + return widget->fontMetrics().height() * Phantom::MenuBar_HorizontalPaddingFontRatio; + case PM_MenuBarVMargin: + case PM_MenuBarPanelWidth: + val = 0; + break; + case PM_ToolBarSeparatorExtent: + val = 9; + break; + case PM_ToolBarHandleExtent: { + int dotLen = Phantom::dpiScaled(2); + return dotLen * (3 * 2 - 1); + } + case PM_ToolBarItemSpacing: + val = 1; + break; + case PM_ToolBarFrameWidth: + val = Phantom::MenuBar_FrameWidth; + break; + case PM_ToolBarItemMargin: + val = 1; + break; + case PM_ToolBarExtensionExtent: + val = 32; + break; + case PM_ListViewIconSize: + case PM_SmallIconSize: + if (Phantom::ItemView_UseFontHeightForDecorationSize && widget + && qobject_cast(widget)) { + // QAbstractItemView::viewOptions() always uses nullptr for the + // styleoption when querying for PM_SmallIconSize. The best we can do is + // use the font set on the widget itself, which is obviously going to be + // wrong if the row has a custom font set on it. Hmm. + return widget->fontMetrics().height(); + } + val = 16; + break; + case PM_ButtonIconSize: { + if (option) + return option->fontMetrics.height(); + if (widget) + return widget->fontMetrics().height(); + val = 16; + break; + } + case PM_DockWidgetTitleBarButtonMargin: + val = 2; + break; +#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) + case PM_TitleBarButtonSize: + val = 19; + break; +#endif + case PM_MaximumDragDistance: + return -1; // Do not dpi-scale because the value is magic + case PM_TabCloseIndicatorWidth: + case PM_TabCloseIndicatorHeight: + val = 16; + break; + case PM_TabBarTabHSpace: + // Contents may clip out horizontally if we don't some extra pixels here or + // in sizeFromContents for CT_TabBarTab. + if (!option) + break; + return static_cast(option->fontMetrics.height() * Phantom::TabBar_HPaddingFontRatio) + + static_cast(Phantom::dpiScaled(4)); + case PM_TabBarTabVSpace: + if (!option) + break; + return static_cast(option->fontMetrics.height() * Phantom::TabBar_VPaddingFontRatio) + + static_cast(Phantom::dpiScaled(2)); + case PM_TabBarTabOverlap: + val = 1; + break; + case PM_TabBarBaseOverlap: + val = 2; + break; + case PM_TabBarIconSize: { + if (!widget) + break; + return widget->fontMetrics().height(); + } + case PM_TabBarTabShiftVertical: { + val = Phantom::TabBar_InctiveVShift; + break; + } + case PM_SubMenuOverlap: + val = 0; + break; + case PM_DockWidgetHandleExtent: + case PM_SplitterWidth: + val = 5; + break; + case PM_IndicatorHeight: + case PM_IndicatorWidth: + case PM_ExclusiveIndicatorHeight: + case PM_ExclusiveIndicatorWidth: + if (option) + return option->fontMetrics.height(); + if (widget) + return widget->fontMetrics().height(); + val = 14; + break; + case PM_ScrollView_ScrollBarOverlap: + case PM_ScrollView_ScrollBarSpacing: + val = 0; + break; + case PM_TreeViewIndentation: { + if (widget) + return widget->fontMetrics().height(); + val = 12; + break; + } + default: + val = QCommonStyle::pixelMetric(metric, option, widget); + } + return Phantom::dpiScaled(val); +} + +QSize BaseStyle::sizeFromContents(ContentsType type, + const QStyleOption* option, + const QSize& size, + const QWidget* widget) const +{ + namespace Ph = Phantom; + // Cases which do not rely on the parent class to do any work + switch (type) { + case CT_RadioButton: + case CT_CheckBox: { + auto btn = qstyleoption_cast(option); + if (!btn) + break; + bool isRadio = type == CT_RadioButton; + int w = proxy()->pixelMetric(isRadio ? PM_ExclusiveIndicatorWidth : PM_IndicatorWidth, btn, widget); + int h = proxy()->pixelMetric(isRadio ? PM_ExclusiveIndicatorHeight : PM_IndicatorHeight, btn, widget); + int margins = 0; + if (!btn->icon.isNull() || !btn->text.isEmpty()) + margins = + proxy()->pixelMetric(isRadio ? PM_RadioButtonLabelSpacing : PM_CheckBoxLabelSpacing, option, widget); + return QSize(size.width() + w + margins, qMax(size.height(), h)); + } + case CT_MenuBarItem: { + return QSize(QCommonStyle::sizeFromContents(type, option, size, widget) + QSize(8, 5)); + + // int fontHeight = option ? option->fontMetrics.height() : size.height(); + // int w = static_cast(fontHeight * Ph::MenuBar_HorizontalPaddingFontRatio); + // int h = static_cast(fontHeight * Ph::MenuBar_VerticalPaddingFontRatio); + // int line = Ph::dpiScaled(1); + // return QSize(size.width() + w * 2, size.height() + h * 2 + line); + } + case CT_MenuItem: { + auto menuItem = qstyleoption_cast(option); + if (!menuItem) + return size; + bool hasTabChar = menuItem->text.contains(QLatin1Char('\t')); + bool hasSubMenu = menuItem->menuItemType == QStyleOptionMenuItem::SubMenu; + bool isSeparator = menuItem->menuItemType == QStyleOptionMenuItem::Separator; + int fontMetricsHeight = -1; + // See notes at CE_MenuItem and SH_ComboBox_Popup for more information + if (Ph::UseQMenuForComboBoxPopup && qobject_cast(widget)) { + if (!widget->testAttribute(Qt::WA_SetFont)) + fontMetricsHeight = QFontMetrics(qApp->font("QMenu")).height(); + } + if (fontMetricsHeight == -1) { + fontMetricsHeight = option->fontMetrics.height(); + } + auto metrics = Ph::MenuItemMetrics::ofFontHeight(fontMetricsHeight); + // Incoming width is the sum of the visual widths of the main item text and + // the mnemonic text (if any). To this width we will add the widths of the + // other features for this menu item -- the icon/checkbox, spacing between + // icon/text/mnemonic, etc. For cases like separators without any text, we + // may disregard the width. + // + // Height is the text height, probably. + int w = size.width(); + // Frame + w += metrics.frameThickness * 2; + // Left margins don't depend on whether or not we have a submenu arrow. + // Calculating the right margins requires knowing whether or not the menu + // item has a submenu arrow. + w += metrics.leftMargin; + // Phantom treats every menu item with the same space on the left for a + // check mark, even if it doesn't have the checkable property. + w += metrics.checkWidth + metrics.checkRightSpace; + + if (!menuItem->icon.isNull()) { + // Phantom disregards any user-specified icon sizing at the moment. + w += metrics.fontHeight; + w += metrics.iconRightSpace; + } + + // Tab character is used for separating the shortcut text + if (hasTabChar) + w += metrics.mnemonicSpace; + if (hasSubMenu) + w += metrics.arrowSpace + metrics.arrowWidth + metrics.rightMarginForArrow; + else + w += metrics.rightMarginForText; + int h; + if (isSeparator) { + h = metrics.separatorHeight; + } else { + h = metrics.totalHeight; + } + if (!menuItem->icon.isNull()) { + if (auto combo = qobject_cast(widget)) { + h = qMax(combo->iconSize().height() + 2, h); + } + } + QSize sz; + sz.setWidth(qMax(w, Ph::dpiScaled(Ph::MenuMinimumWidth))); + sz.setHeight(h); + return sz; + } + case CT_Menu: { + if (!Ph::MenuExtraBottomMargin || !option || !widget) + break; + // Trick the QMenu into putting a margin only at the bottom by adding extra + // height to the contents size. We only want to add this tricky space if + // there is at least more than 1 item in the menu. + const auto acts = widget->actions(); + if (acts.count() < 2) + break; + // We only want to add the tricky space if there's at least 1 separator, + // otherwise it looks weird. + bool anySeps = false; + for (auto act : acts) { + if (act->isSeparator()) { + anySeps = true; + break; + } + } + if (!anySeps) + break; + // int fheight = option->fontMetrics.height(); + // int vmargin = static_cast(fheight * Ph::MenuItem_SeparatorHeightFontRatio) / 2; + // QSize sz = size; + // sz.setHeight(sz.height() + vmargin); + return size; + } + case CT_TabBarTab: { + // Placeholder in case we change this in the future + return size; + } + case CT_Slider: { + QSize sz = size; + if (qobject_cast(widget)->orientation() == Qt::Horizontal) { + sz.setHeight(sz.height() + PM_SliderTickmarkOffset); + } else { + sz.setWidth(sz.width() + PM_SliderTickmarkOffset); + } + return sz; + } + case CT_GroupBox: { + // This doesn't seem to get used except once by QGroupBox for + // minimumSizeHint(). After that, the sizing/layout calculations seem to + // use the rects given by subControlRect(). + auto opt = qstyleoption_cast(option); + if (!opt) + break; + // Checkbox and text height already accounted for, but margin between text + // and frame isn't. + int xadd = 0; + int yadd = 0; + if (opt->subControls & (SC_GroupBoxCheckBox | SC_GroupBoxLabel)) { + int fontHeight = option->fontMetrics.height(); + yadd += static_cast(fontHeight * Phantom::GroupBox_LabelBottomMarginFontRatio); + } + // We can test for the frame in general, but unfortunately testing to see + // if it's the 1-line "flat" style or 4-line box/rect "anything else" style + // doesn't seem to be possible here, only when painting. + if (opt->subControls & SC_GroupBoxFrame) { + xadd += 2; + yadd += 2; + } + return QSize(size.width() + xadd, size.height() + yadd); + } + case CT_ItemViewItem: { + auto vopt = qstyleoption_cast(option); + if (!vopt) + break; + QSize sz = QCommonStyle::sizeFromContents(type, option, size, widget); + sz += QSize(0, Phantom::DefaultFrameWidth); + // QCommonStyle has a bunch of complicated logic for laying out/calculating + // rects of view items, which is locked behind a private data guy. In + // sizeFromContents for CT_ItemViewItem, it unions all of the item row's + // rects together and then, if the decoration height is exactly the same as + // the row height, it adds 2 pixels (not dpi scaled) to the height. The + // comment says it's to prevent "icons from overlapping" but I have no idea + // how that's supposed to help. And we don't necessarily want those extra 2 + // pixels. Anyway, I don't want to copy and paste all of that code into + // Phantom and then maintain it. So when Phantom is in the mode where we're + // basing the item view decoration sizes off of the font size, we'll just + // take a guess when QCommonStyle has added 2 to the height (because the + // row height and decoration height are both the font height), and + // re-remove those two pixels. +#if 1 + if (Phantom::ItemView_UseFontHeightForDecorationSize) { + int fh = vopt->fontMetrics.height(); + if (sz.height() == fh + 2 && vopt->decorationSize.height() == fh) { + sz.setHeight(fh); + } + } +#endif + return sz; + } + case CT_HeaderSection: { + auto hdr = qstyleoption_cast(option); + if (!hdr) + break; + // This is pretty crummy. Should also check if we need multi-line support + // or not. + bool nullIcon = hdr->icon.isNull(); + int margin = proxy()->pixelMetric(QStyle::PM_HeaderMargin, hdr, widget); + int iconSize = nullIcon ? 0 : option->fontMetrics.height(); + QSize txt = hdr->fontMetrics.size(Qt::TextSingleLine | Qt::TextBypassShaping, hdr->text); + QSize sz; + sz.setHeight(margin + qMax(iconSize, txt.height()) + margin); + sz.setWidth((nullIcon ? 0 : margin) + iconSize + (hdr->text.isNull() ? 0 : margin) + txt.width() + margin); + if (hdr->sortIndicator != QStyleOptionHeader::None) { + if (hdr->orientation == Qt::Horizontal) + sz.rwidth() += sz.height() + margin; + else + sz.rheight() += sz.width() + margin; + } + return sz; + } + default: + break; + } + + // Cases which modify the size given by the parent class + QSize newSize = QCommonStyle::sizeFromContents(type, option, size, widget); + switch (type) { + case CT_PushButton: { + auto pbopt = qstyleoption_cast(option); + if (!pbopt || pbopt->text.isEmpty()) + break; + int hpad = static_cast(pbopt->fontMetrics.height() * Phantom::PushButton_HorizontalPaddingFontHeightRatio); + newSize.rwidth() += hpad * 2; + if (widget && qobject_cast(widget->parent())) { + int dialogButtonMinWidth = Phantom::dpiScaled(80); + newSize.rwidth() = qMax(newSize.width(), dialogButtonMinWidth); + } + break; + } + case CT_ToolButton: +#if defined(Q_OS_MACOS) + newSize += QSize(Ph::dpiScaled(6 + Phantom::DefaultFrameWidth), Ph::dpiScaled(6 + Phantom::DefaultFrameWidth)); +#elif defined(Q_OS_WIN) + newSize += QSize(Ph::dpiScaled(4 + Phantom::DefaultFrameWidth), Ph::dpiScaled(4 + Phantom::DefaultFrameWidth)); +#else + newSize += QSize(Ph::dpiScaled(3 + Phantom::DefaultFrameWidth), Ph::dpiScaled(3 + Phantom::DefaultFrameWidth)); +#endif + break; + case CT_ComboBox: { + newSize += QSize(0, Ph::dpiScaled(4 + Phantom::DefaultFrameWidth)); + auto cb = qstyleoption_cast(option); + // Non-editable combo boxes have some extra padding on the left side, + // similar to push buttons. We should account for that here to avoid text + // being clipped off. + if (cb) { + int pad = 0; + if (cb->editable) { + pad = Ph::dpiScaled(Ph::LineEdit_ContentsHPad); + } else { + pad = Ph::dpiScaled(Ph::ComboBox_NonEditable_ContentsHPad); + } + newSize.rwidth() += pad * 2; + } + break; + } + case CT_LineEdit: { + newSize += QSize(0, 4); + int pad = Ph::dpiScaled(Ph::LineEdit_ContentsHPad); + newSize.rwidth() += pad * 2; + break; + } + case CT_SpinBox: + // No changes needed + break; + case CT_SizeGrip: + newSize += QSize(4, 4); + break; + case CT_MdiControls: + newSize -= QSize(1, 0); + break; + default: + break; + } + return newSize; +} + +void BaseStyle::polish(QApplication* app) +{ + QCommonStyle::polish(app); + + app->setPalette(standardPalette()); + + // if (QObject *obj = hintsSettings()) { + // connect(obj, SIGNAL(systemFontChanged(QString)), this, SLOT(updateAppFont())); + // connect(obj, SIGNAL(systemFontPointSizeChanged(qreal)), this, SLOT(updateAppFont())); + // } +} + +void BaseStyle::unpolish(QApplication* app) +{ + QCommonStyle::unpolish(app); + + // if (QObject *obj = hintsSettings()) { + // disconnect(obj, SIGNAL(systemFontChanged(QString)), this, SLOT(updateAppFont())); + // disconnect(obj, SIGNAL(systemFontPointSizeChanged(qreal)), this, SLOT(updateAppFont())); + // } +} + +void BaseStyle::polish(QWidget *widget) +{ + QCommonStyle::polish(widget); + if (false + || qobject_cast(widget) + || qobject_cast(widget) + || qobject_cast(widget) + || qobject_cast(widget) + || qobject_cast(widget) + || qobject_cast(widget) + || qobject_cast(widget) + || (widget->inherits("QDockSeparator")) + || (widget->inherits("QDockWidgetSeparator")) + ) { + widget->setAttribute(Qt::WA_Hover, true); + widget->setAttribute(Qt::WA_OpaquePaintEvent, false); + } + + if (qobject_cast(widget)) { + widget->setAttribute(Qt::WA_TranslucentBackground, true); + m_blurHelper->registerWidget(widget); + } + + if (widget->inherits("QTipLabel") || widget->inherits("QComboBoxPrivateContainer")) { + widget->setAttribute(Qt::WA_TranslucentBackground, true); + } + + m_shadowHelper->registerWidget(widget); +} + +void BaseStyle::unpolish(QWidget *widget) +{ + QCommonStyle::unpolish(widget); + if (false + || qobject_cast(widget) + || qobject_cast(widget) + || qobject_cast(widget) + || qobject_cast(widget) + || qobject_cast(widget) + || qobject_cast(widget) + || qobject_cast(widget) + || (widget->inherits("QDockSeparator")) + || (widget->inherits("QDockWidgetSeparator")) + ) { + widget->setAttribute(Qt::WA_Hover, false); + } + + if (qobject_cast(widget)) { + widget->setAttribute(Qt::WA_TranslucentBackground, false); + m_blurHelper->unregisterWidget(widget); + } + + if (widget->inherits("QTipLabel")) { + widget->setAttribute(Qt::WA_TranslucentBackground, false); + } + + m_shadowHelper->unregisterWidget(widget); +} + +bool BaseStyle::isDarkMode() const +{ + QSettings settings(QSettings::UserScope, "cutefishos", "theme"); + return settings.value("DarkMode", false).toBool(); +} + +QRect BaseStyle::subControlRect(ComplexControl control, + const QStyleOptionComplex* option, + SubControl subControl, + const QWidget* widget) const +{ + namespace Ph = Phantom; + QRect rect = QCommonStyle::subControlRect(control, option, subControl, widget); + switch (control) { + case CC_Slider: { + auto slider = qstyleoption_cast(option); + if (!slider) + break; + int tickSize = proxy()->pixelMetric(PM_SliderTickmarkOffset, option, widget); + switch (subControl) { + case SC_SliderHandle: { + if (slider->orientation == Qt::Horizontal) { + rect.setHeight(proxy()->pixelMetric(PM_SliderThickness)); + rect.setWidth(proxy()->pixelMetric(PM_SliderLength)); + int centerY = slider->rect.center().y() - rect.height() / 2; + if (slider->tickPosition & QSlider::TicksAbove) + centerY += tickSize; + if (slider->tickPosition & QSlider::TicksBelow) + centerY -= tickSize; + rect.moveTop(centerY); + } else { + rect.setWidth(proxy()->pixelMetric(PM_SliderThickness)); + rect.setHeight(proxy()->pixelMetric(PM_SliderLength)); + int centerX = slider->rect.center().x() - rect.width() / 2; + if (slider->tickPosition & QSlider::TicksAbove) + centerX += tickSize; + if (slider->tickPosition & QSlider::TicksBelow) + centerX -= tickSize; + rect.moveLeft(centerX); + } + break; + } + case SC_SliderGroove: { + QPoint grooveCenter = slider->rect.center(); + const int grooveThickness = Ph::dpiScaled(7); + if (slider->orientation == Qt::Horizontal) { + rect.setHeight(grooveThickness); + if (slider->tickPosition & QSlider::TicksAbove) + grooveCenter.ry() += tickSize; + if (slider->tickPosition & QSlider::TicksBelow) + grooveCenter.ry() -= tickSize; + } else { + rect.setWidth(grooveThickness); + if (slider->tickPosition & QSlider::TicksAbove) + grooveCenter.rx() += tickSize; + if (slider->tickPosition & QSlider::TicksBelow) + grooveCenter.rx() -= tickSize; + } + rect.moveCenter(grooveCenter); + break; + } + default: + break; + } + break; + } + case CC_SpinBox: { + auto spinbox = qstyleoption_cast(option); + if (!spinbox) + break; + // Some leftover Fusion code here. Should clean up this mess. + int center = spinbox->rect.height() / 2; + int fw = spinbox->frame ? 1 : 0; + int y = fw; + const int buttonWidth = static_cast(Ph::dpiScaled(Ph::SpinBox_ButtonWidth)) + 2; + int x, lx, rx; + x = spinbox->rect.width() - y - buttonWidth + 2; + lx = fw; + rx = x - fw; + switch (subControl) { + case SC_SpinBoxUp: + if (spinbox->buttonSymbols == QAbstractSpinBox::NoButtons) + return {}; + rect = QRect(x, fw, buttonWidth, center - fw); + break; + case SC_SpinBoxDown: + if (spinbox->buttonSymbols == QAbstractSpinBox::NoButtons) + return QRect(); + + rect = QRect(x, center, buttonWidth, spinbox->rect.bottom() - center - fw + 1); + break; + case SC_SpinBoxEditField: + if (spinbox->buttonSymbols == QAbstractSpinBox::NoButtons) { + rect = QRect(lx, fw, spinbox->rect.width() - 2 * fw, spinbox->rect.height() - 2 * fw); + } else { + rect = QRect(lx, fw, rx - qMax(fw - 1, 0), spinbox->rect.height() - 2 * fw); + } + break; + case SC_SpinBoxFrame: + rect = spinbox->rect; + break; + default: + break; + } + rect = visualRect(spinbox->direction, spinbox->rect, rect); + break; + } + case CC_GroupBox: { + auto groupBox = qstyleoption_cast(option); + if (!groupBox) + break; + switch (subControl) { + case SC_GroupBoxFrame: + case SC_GroupBoxContents: { + QRect r = option->rect; + if (groupBox->subControls & (SC_GroupBoxLabel | SC_GroupBoxCheckBox)) { + int fontHeight = option->fontMetrics.height(); + int topMargin = qMax(pixelMetric(PM_ExclusiveIndicatorHeight), fontHeight); + topMargin += static_cast(fontHeight * Ph::GroupBox_LabelBottomMarginFontRatio); + r.setTop(r.top() + topMargin); + } + if (subControl == SC_GroupBoxContents && groupBox->subControls & SC_GroupBoxFrame) { + // Testing against groupBox->features for the frame type doesn't seem + // to work here. + r.adjust(1, 1, -1, -1); + } + return r; + } + case SC_GroupBoxCheckBox: + case SC_GroupBoxLabel: { + // Accurate height doesn't matter -- the other group box style + // implementations also fail with multi-line or too-tall text. + int textHeight = option->fontMetrics.height(); + // width()/horizontalAdvance() is faster than size() and good enough for + // us, since we only support a single line of text here anyway. + int textWidth = Phantom::fontMetricsWidth(option->fontMetrics, groupBox->text); + int indicatorWidth = proxy()->pixelMetric(PM_IndicatorWidth, option, widget); + int indicatorHeight = proxy()->pixelMetric(PM_IndicatorHeight, option, widget); + int margin = 0; + int indicatorRightSpace = textHeight / 3; + int contentWidth = textWidth; + if (option->subControls & QStyle::SC_GroupBoxCheckBox) { + contentWidth += indicatorWidth + indicatorRightSpace; + } + int x = margin; + int y = 0; + switch (groupBox->textAlignment & Qt::AlignHorizontal_Mask) { + case Qt::AlignHCenter: + x += (option->rect.width() - contentWidth) / 2; + break; + case Qt::AlignRight: + x += option->rect.width() - contentWidth; + break; + default: + break; + } + int w, h; + if (subControl == SC_GroupBoxCheckBox) { + w = indicatorWidth; + h = indicatorHeight; + if (textHeight > indicatorHeight) { + y = (textHeight - indicatorHeight) / 2; + } + } else { + w = contentWidth; + h = textHeight; + if (option->subControls & QStyle::SC_GroupBoxCheckBox) { + x += indicatorWidth + indicatorRightSpace; + w -= indicatorWidth + indicatorRightSpace; + } + } + return visualRect(option->direction, option->rect, QRect(x, y, w, h)); + } + default: + break; + } + break; + } + case CC_ComboBox: { + auto cb = qstyleoption_cast(option); + if (!cb) + return QRect(); + int frame = cb->frame ? proxy()->pixelMetric(PM_ComboBoxFrameWidth, cb, widget) : 0; + QRect r = option->rect; + r.adjust(frame, frame, -frame, -frame); + int dim = qMin(r.width(), r.height()); + if (dim < 1) + return QRect(); + switch (subControl) { + case SC_ComboBoxFrame: + return cb->rect; + case SC_ComboBoxArrow: { + QRect r0 = r; + r0.setX((r0.x() + r0.width()) - dim + 1); + return visualRect(option->direction, option->rect, r0); + } + case SC_ComboBoxEditField: { + // Add extra padding if not editable + int pad = 0; + if (cb->editable) { + // Line edit padding already added + } else { + pad = Ph::dpiScaled(Ph::ComboBox_NonEditable_ContentsHPad); + } + r.adjust(pad, 0, -dim, 0); + return visualRect(option->direction, option->rect, r); + } + case SC_ComboBoxListBoxPopup: { + //return cb->rect; + //rekols + QSize size = proxy()->sizeFromContents(CT_Menu, option, option->rect.size(), widget); + QRect rect = option->rect; + if (auto styleOption = static_cast(option)) { + if (auto menuItem = static_cast(styleOption)) { + if (menuItem->icon.isNull()) { + rect.setWidth(size.width()); + } + } + } + return rect; + } + default: + break; + } + break; + } + case CC_TitleBar: { + auto tb = qstyleoption_cast(option); + if (!tb) + break; + SubControl sc = subControl; + QRect& ret = rect; + const int indent = 3; + const int controlTopMargin = 3; + const int controlBottomMargin = 3; + const int controlWidthMargin = 2; + const int controlHeight = tb->rect.height() - controlTopMargin - controlBottomMargin; + const int delta = controlHeight + controlWidthMargin; + int offset = 0; + bool isMinimized = tb->titleBarState & Qt::WindowMinimized; + bool isMaximized = tb->titleBarState & Qt::WindowMaximized; + switch (sc) { + case SC_TitleBarLabel: + if (tb->titleBarFlags & (Qt::WindowTitleHint | Qt::WindowSystemMenuHint)) { + ret = tb->rect; + if (tb->titleBarFlags & Qt::WindowSystemMenuHint) + ret.adjust(delta, 0, -delta, 0); + if (tb->titleBarFlags & Qt::WindowMinimizeButtonHint) + ret.adjust(0, 0, -delta, 0); + if (tb->titleBarFlags & Qt::WindowMaximizeButtonHint) + ret.adjust(0, 0, -delta, 0); + if (tb->titleBarFlags & Qt::WindowShadeButtonHint) + ret.adjust(0, 0, -delta, 0); + if (tb->titleBarFlags & Qt::WindowContextHelpButtonHint) + ret.adjust(0, 0, -delta, 0); + } + break; + case SC_TitleBarContextHelpButton: + if (tb->titleBarFlags & Qt::WindowContextHelpButtonHint) + offset += delta; + Q_FALLTHROUGH(); + case SC_TitleBarMinButton: + if (!isMinimized && (tb->titleBarFlags & Qt::WindowMinimizeButtonHint)) + offset += delta; + else if (sc == SC_TitleBarMinButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarNormalButton: + if (isMinimized && (tb->titleBarFlags & Qt::WindowMinimizeButtonHint)) + offset += delta; + else if (isMaximized && (tb->titleBarFlags & Qt::WindowMaximizeButtonHint)) + offset += delta; + else if (sc == SC_TitleBarNormalButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarMaxButton: + if (!isMaximized && (tb->titleBarFlags & Qt::WindowMaximizeButtonHint)) + offset += delta; + else if (sc == SC_TitleBarMaxButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarShadeButton: + if (!isMinimized && (tb->titleBarFlags & Qt::WindowShadeButtonHint)) + offset += delta; + else if (sc == SC_TitleBarShadeButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarUnshadeButton: + if (isMinimized && (tb->titleBarFlags & Qt::WindowShadeButtonHint)) + offset += delta; + else if (sc == SC_TitleBarUnshadeButton) + break; + Q_FALLTHROUGH(); + case SC_TitleBarCloseButton: + if (tb->titleBarFlags & Qt::WindowSystemMenuHint) + offset += delta; + else if (sc == SC_TitleBarCloseButton) + break; + ret.setRect( + tb->rect.right() - indent - offset, tb->rect.top() + controlTopMargin, controlHeight, controlHeight); + break; + case SC_TitleBarSysMenu: + if (tb->titleBarFlags & Qt::WindowSystemMenuHint) { + ret.setRect(tb->rect.left() + controlWidthMargin + indent, + tb->rect.top() + controlTopMargin, + controlHeight, + controlHeight); + } + break; + default: + break; + } + ret = visualRect(tb->direction, tb->rect, ret); + break; + } + default: + break; + } + + return rect; +} + +QRect BaseStyle::itemPixmapRect(const QRect& r, int flags, const QPixmap& pixmap) const +{ + return QCommonStyle::itemPixmapRect(r, flags, pixmap); +} + +void BaseStyle::drawItemPixmap(QPainter* painter, const QRect& rect, int alignment, const QPixmap& pixmap) const +{ + QCommonStyle::drawItemPixmap(painter, rect, alignment, pixmap); +} + +QStyle::SubControl BaseStyle::hitTestComplexControl(ComplexControl cc, + const QStyleOptionComplex* opt, + const QPoint& pt, + const QWidget* w) const +{ + return QCommonStyle::hitTestComplexControl(cc, opt, pt, w); +} + +QPixmap BaseStyle::generatedIconPixmap(QIcon::Mode iconMode, const QPixmap& pixmap, const QStyleOption* opt) const +{ + // Default icon highlight is way too subtle + if (iconMode == QIcon::Selected) { + QImage img = pixmap.toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + QPainter painter(&img); + + painter.setCompositionMode(QPainter::CompositionMode_SourceAtop); + + QColor color = + Phantom::DeriveColors::adjustLightness(opt->palette.color(QPalette::Normal, QPalette::Highlight), .25); + color.setAlphaF(0.25); + painter.fillRect(0, 0, img.width(), img.height(), color); + + painter.end(); + + return QPixmap::fromImage(img); + } + return QCommonStyle::generatedIconPixmap(iconMode, pixmap, opt); +} + +int BaseStyle::styleHint(StyleHint hint, + const QStyleOption* option, + const QWidget* widget, + QStyleHintReturn* returnData) const +{ + if (auto menu = qobject_cast(widget)) { + const_cast(widget)->setAttribute(Qt::WA_TranslucentBackground); + } + + switch (hint) { + case SH_Slider_SnapToValue: + case SH_PrintDialog_RightAlignButtons: + case SH_FontDialog_SelectAssociatedText: + case SH_ComboBox_ListMouseTracking: + case SH_Slider_StopMouseOverSlider: + case SH_ScrollBar_MiddleClickAbsolutePosition: + case SH_TitleBar_AutoRaise: + case SH_TitleBar_NoBorder: + case SH_ItemView_ArrowKeysNavigateIntoChildren: + case SH_ItemView_ChangeHighlightOnFocus: + case SH_MenuBar_MouseTracking: + case SH_Menu_MouseTracking: + return 1; + case SH_Menu_SupportsSections: + return 0; +#ifndef Q_OS_MAC + case SH_MenuBar_AltKeyNavigation: + return 1; +#endif +#if defined(QT_PLATFORM_UIKIT) + case SH_ComboBox_UseNativePopup: + return 1; +#endif + case SH_ItemView_ShowDecorationSelected: + // QWindowsStyle does this as well -- QCommonStyle seems to have some + // internal confusion buried within its private implementation of laying + // out and drawing item views where it can't keep track of what's + // considered a decoration and what's not. For tree views, if you give 0 + // for ShowDecorationSelected, it applies only to the disclosure indicator + // and not to the QIcon/pixmap that might be present for the item. So + // selecting an item in a tree view will have the selection color drawn + // underneath the icon/pixmap, but not the disclosure indicator. However, + // in list views, if you give 0 for ShowDecorationSelected, it will *not* + // draw the selection color underneath the icon/pixmap. There's no way to + // access this internal logic in QCommonStyle without fully reimplementing + // the huge mass of stuff for item view layout and drawing. Therefore, the + // best we can do is at least try to get consistent behavior: if it's a + // list view, just always return 1 for ShowDecorationSelected. + if (!Phantom::ShowItemViewDecorationSelected && qobject_cast(widget)) + return 1; + return Phantom::ShowItemViewDecorationSelected; + case SH_ItemView_MovementWithoutUpdatingSelection: + return 1; +#if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)) + case SH_ItemView_ScrollMode: + return QAbstractItemView::ScrollPerPixel; +#endif + case SH_ScrollBar_ContextMenu: +#ifdef Q_OS_MAC + return 0; +#else + return 1; +#endif + // Some Linux distros might want to enable this, but it doesn't behave very + // consistently with varied QPalettes, depending on how the QPA and icons + // deal with both light and dark themes. It might seem weird to just disable + // this, but none of (Mac, Windows, BeOS/Haiku) show icons in dialog buttons, + // and the results on Linux are generally pretty messy -- not sure why it's + // historically been the default, especially when other button types + // generally don't have any icons. + case SH_DialogButtonBox_ButtonsHaveIcons: + return 0; + case SH_ScrollBar_Transient: + return 1; + case SH_EtchDisabledText: + case SH_DitherDisabledText: + case SH_ToolBox_SelectedPageTitleBold: + case SH_Menu_AllowActiveAndDisabled: + case SH_MainWindow_SpaceBelowMenuBar: + case SH_MessageBox_CenterButtons: + case SH_RubberBand_Mask: + case SH_ScrollView_FrameOnlyAroundContents: + return 0; + case SH_ComboBox_Popup: { + return Phantom::UseQMenuForComboBoxPopup; + // Fusion did this, but we don't because of font bugs (especially in high + // DPI) with the QMenu that the combo box will create instead of a dropdown + // view. See notes in CE_MenuItem for more details. + if (auto cmb = qstyleoption_cast(option)) + return !cmb->editable; + return 0; + } + case SH_Table_GridLineColor: { + using namespace Phantom::SwatchColors; + namespace Ph = Phantom; + if (!option) + return 0; + 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 static_cast(swatch.color(S_base_divider).rgb()); + } + case SH_MessageBox_TextInteractionFlags: + return Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse; + case SH_WizardStyle: + return QWizard::ClassicStyle; + case SH_Menu_SubMenuPopupDelay: + // Returning 0 will break sloppy submenus even if they're enabled + return 10; + case SH_Menu_SloppySubMenus: + return true; + case SH_Menu_SubMenuSloppyCloseTimeout: + return 500; + case SH_Menu_SubMenuDontStartSloppyOnLeave: + return 1; + case SH_Menu_SubMenuSloppySelectOtherActions: + return 1; + case SH_Menu_SubMenuUniDirection: + return 1; + case SH_Menu_SubMenuUniDirectionFailCount: + return 1; + case SH_Menu_SubMenuResetWhenReenteringParent: + return 0; +#ifdef Q_OS_MAC + case SH_Menu_FlashTriggeredItem: + return 1; + case SH_Menu_FadeOutOnHide: + return 0; +#endif + case SH_WindowFrame_Mask: + return 0; + case SH_UnderlineShortcut: { + return false; + } + case SH_Widget_Animate: + return 1; + default: + break; + } + return QCommonStyle::styleHint(hint, option, widget, returnData); +} + +QRect BaseStyle::subElementRect(SubElement sr, const QStyleOption* opt, const QWidget* w) const +{ + switch (sr) { + case SE_ProgressBarLabel: + case SE_ProgressBarContents: + case SE_ProgressBarGroove: + return opt->rect; + case SE_PushButtonFocusRect: { + QRect r = QCommonStyle::subElementRect(sr, opt, w); + r.adjust(0, 1, 0, -1); + return r; + } + case SE_DockWidgetTitleBarText: { + auto titlebar = qstyleoption_cast(opt); + if (!titlebar) + break; + QRect r = QCommonStyle::subElementRect(sr, opt, w); + bool verticalTitleBar = titlebar->verticalTitleBar; + if (verticalTitleBar) { + r.adjust(0, 0, 0, -4); + } else { + if (opt->direction == Qt::LeftToRight) + r.adjust(4, 0, 0, 0); + else + r.adjust(0, 0, -4, 0); + } + return r; + } + case SE_TreeViewDisclosureItem: { + if (Phantom::BranchesOnEdge) { + // Shove it all the way to the left (or right) side, probably outside of + // the rect it gave us. Old-school. + QRect rect = opt->rect; + if (opt->direction != Qt::RightToLeft) { + rect.moveLeft(0); + if (rect.width() < rect.height()) + rect.setWidth(rect.height()); + } else { + // todo + } + return rect; + } + break; + } + case SE_LineEditContents: { + QRect r = QCommonStyle::subElementRect(sr, opt, w); + int pad = Phantom::dpiScaled(Phantom::LineEdit_ContentsHPad); + return r.adjusted(pad, 0, -pad, 0); + } + default: + break; + } + return QCommonStyle::subElementRect(sr, opt, w); +} diff --git a/widgetstyle/basestyle.h b/widgetstyle/basestyle.h new file mode 100644 index 0000000..42d0722 --- /dev/null +++ b/widgetstyle/basestyle.h @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2020 Reven Martin + * Copyright (C) 2020 KeePassXC Team + * Copyright (C) 2019 Andrew Richards + * + * Derived from Phantomstyle and relicensed under the GPLv2 or v3. + * https://github.com/randrew/phantomstyle + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef BASESTYLE_H +#define BASESTYLE_H + +#include + +class BaseStylePrivate; +class ShadowHelper; +class BlurHelper; +class BaseStyle : public QCommonStyle +{ + Q_OBJECT + +public: + BaseStyle(); + ~BaseStyle() override; + + enum PhantomPrimitiveElement + { + Phantom_PE_IndicatorTabNew = PE_CustomBase + 1, + Phantom_PE_ScrollBarSliderVertical, + Phantom_PE_WindowFrameColor, + }; + + static QPalette lightModePalette(); + static QPalette darkModePalette(); + QPalette standardPalette() const override; + void drawPrimitive(PrimitiveElement elem, + const QStyleOption* option, + QPainter* painter, + const QWidget* widget = nullptr) const override; + void + drawControl(ControlElement ce, const QStyleOption* option, QPainter* painter, const QWidget* widget) const override; + int pixelMetric(PixelMetric metric, + const QStyleOption* option = nullptr, + const QWidget* widget = nullptr) const override; + void drawComplexControl(ComplexControl control, + const QStyleOptionComplex* option, + QPainter* painter, + const QWidget* widget) const override; + QRect subElementRect(SubElement r, const QStyleOption* opt, const QWidget* widget = nullptr) const override; + QSize sizeFromContents(ContentsType type, + const QStyleOption* option, + const QSize& size, + const QWidget* widget) const override; + SubControl hitTestComplexControl(ComplexControl cc, + const QStyleOptionComplex* opt, + const QPoint& pt, + const QWidget* w = nullptr) const override; + QRect subControlRect(ComplexControl cc, + const QStyleOptionComplex* opt, + SubControl sc, + const QWidget* widget) const override; + QPixmap generatedIconPixmap(QIcon::Mode iconMode, const QPixmap& pixmap, const QStyleOption* opt) const override; + int styleHint(StyleHint hint, + const QStyleOption* option = nullptr, + const QWidget* widget = nullptr, + QStyleHintReturn* returnData = nullptr) const override; + QRect itemPixmapRect(const QRect& r, int flags, const QPixmap& pixmap) const override; + void drawItemPixmap(QPainter* painter, const QRect& rect, int alignment, const QPixmap& pixmap) const override; + void drawItemText(QPainter* painter, + const QRect& rect, + int flags, + const QPalette& pal, + bool enabled, + const QString& text, + QPalette::ColorRole textRole = QPalette::NoRole) const override; + + using QCommonStyle::polish; + void polish(QApplication* app) override; + void unpolish(QApplication* app) override; + void polish(QWidget *widget) override; + void unpolish(QWidget *widget) override; + + bool isDarkMode() const; + +protected: + /** + * @return Paths to application stylesheets + */ + virtual QString getAppStyleSheet() const + { + return {}; + } + + BaseStylePrivate* d; + +private: + ShadowHelper *m_shadowHelper; + BlurHelper *m_blurHelper; +}; + +#endif diff --git a/widgetstyle/blurhelper.cpp b/widgetstyle/blurhelper.cpp new file mode 100644 index 0000000..4f511f3 --- /dev/null +++ b/widgetstyle/blurhelper.cpp @@ -0,0 +1,105 @@ +////////////////////////////////////////////////////////////////////////////// +// breezeblurhelper.cpp +// handle regions passed to kwin for blurring +// ------------------- +// +// Copyright (C) 2018 Alex Nemeth +// +// Largely rewritten from Oxygen widget style +// Copyright (C) 2007 Thomas Luebking +// Copyright (c) 2010 Hugo Pereira Da Costa +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +////////////////////////////////////////////////////////////////////////////// + +#include "blurhelper.h" + +// KF5 +#include + +// Qt +#include +#include +#include +#include + +BlurHelper::BlurHelper(QObject *parent) + : QObject(parent) +{ +} + +void BlurHelper::registerWidget(QWidget *widget) +{ + // install event filter + addEventFilter(widget); + + // schedule shadow area repaint + update(widget); +} + +void BlurHelper::unregisterWidget(QWidget *widget) +{ + // remove event filter + widget->removeEventFilter(this); +} + +bool BlurHelper::eventFilter(QObject *object, QEvent *event) +{ + switch (event->type()) { + case QEvent::Hide: + case QEvent::Show: + case QEvent::Resize: { + // cast to widget and check + QWidget *widget(qobject_cast(object)); + + if (!widget) + break; + + update(widget); + break; + } + + default: break; + } + + // never eat events + return false; +} + + +void BlurHelper::update(QWidget *widget) const +{ + /* + directly from bespin code. Supposedly prevent playing with some 'pseudo-widgets' + that have winId matching some other -random- window + */ + if (!(widget->testAttribute(Qt::WA_WState_Created) || widget->internalWinId())) + return; + + if (widget->mask().isEmpty()) { + KWindowEffects::enableBlurBehind(widget->winId(), true); + } else { + KWindowEffects::enableBlurBehind(widget->winId(), true, widget->mask()); + } + + // force update + if (widget->isVisible()) { + widget->update(); + } +} diff --git a/widgetstyle/blurhelper.h b/widgetstyle/blurhelper.h new file mode 100644 index 0000000..fe69749 --- /dev/null +++ b/widgetstyle/blurhelper.h @@ -0,0 +1,57 @@ +////////////////////////////////////////////////////////////////////////////// +// breezeblurhelper.h +// handle regions passed to kwin for blurring +// ------------------- +// +// Copyright (C) 2018 Alex Nemeth +// +// Largely rewritten from Oxygen widget style +// Copyright (C) 2007 Thomas Luebking +// Copyright (c) 2010 Hugo Pereira Da Costa +// +// 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 BLURHELPER_H +#define BLURHELPER_H + +#include + +class BlurHelper : public QObject +{ + Q_OBJECT + +public: + explicit BlurHelper(QObject *parent = nullptr); + + void registerWidget(QWidget *); + void unregisterWidget(QWidget *); + bool eventFilter(QObject *, QEvent *) override; + + void update(QWidget *) const; + +protected: + void addEventFilter(QObject *object) { + object->removeEventFilter(this); + object->installEventFilter(this); + } + +}; + +#endif // BLURHELPER_H diff --git a/widgetstyle/boxshadowrenderer.cpp b/widgetstyle/boxshadowrenderer.cpp new file mode 100644 index 0000000..fa91853 --- /dev/null +++ b/widgetstyle/boxshadowrenderer.cpp @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2018 Vlad Zahorodnii + * + * The box blur implementation is based on AlphaBoxBlur from Firefox. + * + * 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 of the License, or + * (at your option) any later version. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +// own +#include "boxshadowrenderer.h" + +// Qt +#include +#include + +static inline int calculateBlurRadius(qreal stdDev) +{ + // See https://www.w3.org/TR/SVG11/filters.html#feGaussianBlurElement + const qreal gaussianScaleFactor = (3.0 * qSqrt(2.0 * M_PI) / 4.0) * 1.5; + return qMax(2, qFloor(stdDev * gaussianScaleFactor + 0.5)); +} + +static inline qreal calculateBlurStdDev(int radius) +{ + // See https://www.w3.org/TR/css-backgrounds-3/#shadow-blur + return radius * 0.5; +} + +static inline QSize calculateBlurExtent(int radius) +{ + const int blurRadius = calculateBlurRadius(calculateBlurStdDev(radius)); + return QSize(blurRadius, blurRadius); +} + +struct BoxLobes +{ + int left; ///< how many pixels sample to the left + int right; ///< how many pixels sample to the right +}; + +/** + * Compute box filter parameters. + * + * @param radius The blur radius. + * @returns Parameters for three box filters. + **/ +static QVector computeLobes(int radius) +{ + const int blurRadius = calculateBlurRadius(calculateBlurStdDev(radius)); + const int z = blurRadius / 3; + + int major; + int minor; + int final; + + switch (blurRadius % 3) { + case 0: + major = z; + minor = z; + final = z; + break; + + case 1: + major = z + 1; + minor = z; + final = z; + break; + + case 2: + major = z + 1; + minor = z; + final = z + 1; + break; + + default: + Q_UNREACHABLE(); + } + + Q_ASSERT(major + minor + final == blurRadius); + + return { + {major, minor}, + {minor, major}, + {final, final} + }; +} + +/** + * Process a row with a box filter. + * + * @param src The start of the row. + * @param dst The destination. + * @param width The width of the row, in pixels. + * @param horizontalStride The number of bytes from one alpha value to the + * next alpha value. + * @param verticalStride The number of bytes from one row to the next row. + * @param lobes Params of the box filter. + * @param transposeInput Whether the input is transposed. + * @param transposeOutput Whether the output should be transposed. + **/ +static inline void boxBlurRowAlpha(const uint8_t *src, uint8_t *dst, int width, int horizontalStride, + int verticalStride, const BoxLobes &lobes, bool transposeInput, + bool transposeOutput) +{ + const int inputStep = transposeInput ? verticalStride : horizontalStride; + const int outputStep = transposeOutput ? verticalStride : horizontalStride; + + const int boxSize = lobes.left + 1 + lobes.right; + const int reciprocal = (1 << 24) / boxSize; + + uint32_t alphaSum = (boxSize + 1) / 2; + + const uint8_t *left = src; + const uint8_t *right = src; + uint8_t *out = dst; + + const uint8_t firstValue = src[0]; + const uint8_t lastValue = src[(width - 1) * inputStep]; + + alphaSum += firstValue * lobes.left; + + const uint8_t *initEnd = src + (boxSize - lobes.left) * inputStep; + while (right < initEnd) { + alphaSum += *right; + right += inputStep; + } + + const uint8_t *leftEnd = src + boxSize * inputStep; + while (right < leftEnd) { + *out = (alphaSum * reciprocal) >> 24; + alphaSum += *right - firstValue; + right += inputStep; + out += outputStep; + } + + const uint8_t *centerEnd = src + width * inputStep; + while (right < centerEnd) { + *out = (alphaSum * reciprocal) >> 24; + alphaSum += *right - *left; + left += inputStep; + right += inputStep; + out += outputStep; + } + + const uint8_t *rightEnd = dst + width * outputStep; + while (out < rightEnd) { + *out = (alphaSum * reciprocal) >> 24; + alphaSum += lastValue - *left; + left += inputStep; + out += outputStep; + } +} + +/** + * Blur the alpha channel of a given image. + * + * @param image The input image. + * @param radius The blur radius. + * @param rect Specifies what part of the image to blur. If nothing is provided, then + * the whole alpha channel of the input image will be blurred. + **/ +static inline void boxBlurAlpha(QImage &image, int radius, const QRect &rect = {}) +{ + if (radius < 2) { + return; + } + + const QVector lobes = computeLobes(radius); + + const QRect blurRect = rect.isNull() ? image.rect() : rect; + + const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3; + const int width = blurRect.width(); + const int height = blurRect.height(); + const int rowStride = image.bytesPerLine(); + const int pixelStride = image.depth() >> 3; + + const int bufferStride = qMax(width, height) * pixelStride; + QScopedPointer > buf(new uint8_t[2 * bufferStride]); + uint8_t *buf1 = buf.data(); + uint8_t *buf2 = buf1 + bufferStride; + + // Blur the image in horizontal direction. + for (int i = 0; i < height; ++i) { + uint8_t *row = image.scanLine(blurRect.y() + i) + blurRect.x() * pixelStride + alphaOffset; + boxBlurRowAlpha(row, buf1, width, pixelStride, rowStride, lobes[0], false, false); + boxBlurRowAlpha(buf1, buf2, width, pixelStride, rowStride, lobes[1], false, false); + boxBlurRowAlpha(buf2, row, width, pixelStride, rowStride, lobes[2], false, false); + } + + // Blur the image in vertical direction. + for (int i = 0; i < width; ++i) { + uint8_t *column = image.scanLine(blurRect.y()) + (blurRect.x() + i) * pixelStride + alphaOffset; + boxBlurRowAlpha(column, buf1, height, pixelStride, rowStride, lobes[0], true, false); + boxBlurRowAlpha(buf1, buf2, height, pixelStride, rowStride, lobes[1], false, false); + boxBlurRowAlpha(buf2, column, height, pixelStride, rowStride, lobes[2], false, true); + } +} + +static inline void mirrorTopLeftQuadrant(QImage &image) +{ + const int width = image.width(); + const int height = image.height(); + + const int centerX = qCeil(width * 0.5); + const int centerY = qCeil(height * 0.5); + + const int alphaOffset = QSysInfo::ByteOrder == QSysInfo::BigEndian ? 0 : 3; + const int stride = image.depth() >> 3; + + for (int y = 0; y < centerY; ++y) { + uint8_t *in = image.scanLine(y) + alphaOffset; + uint8_t *out = in + (width - 1) * stride; + + for (int x = 0; x < centerX; ++x, in += stride, out -= stride) { + *out = *in; + } + } + + for (int y = 0; y < centerY; ++y) { + const uint8_t *in = image.scanLine(y) + alphaOffset; + uint8_t *out = image.scanLine(width - y - 1) + alphaOffset; + + for (int x = 0; x < width; ++x, in += stride, out += stride) { + *out = *in; + } + } +} + +static void renderShadow(QPainter *painter, const QRect &rect, qreal borderRadius, const QPoint &offset, int radius, const QColor &color) +{ + const QSize inflation = calculateBlurExtent(radius); + const QSize size = rect.size() + 2 * inflation; + + const qreal dpr = painter->device()->devicePixelRatioF(); + + QImage shadow(size * dpr, QImage::Format_ARGB32_Premultiplied); + shadow.setDevicePixelRatio(dpr); + shadow.fill(Qt::transparent); + + QRect boxRect(QPoint(0, 0), rect.size()); + boxRect.moveCenter(QRect(QPoint(0, 0), size).center()); + + const qreal xRadius = 2.0 * borderRadius / boxRect.width(); + const qreal yRadius = 2.0 * borderRadius / boxRect.height(); + + QPainter shadowPainter; + shadowPainter.begin(&shadow); + shadowPainter.setRenderHint(QPainter::Antialiasing); + shadowPainter.setPen(Qt::NoPen); + shadowPainter.setBrush(Qt::black); + shadowPainter.drawRoundedRect(boxRect, xRadius, yRadius); + shadowPainter.end(); + + // Because the shadow texture is symmetrical, that's enough to blur + // only the top-left quadrant and then mirror it. + const QRect blurRect(0, 0, qCeil(shadow.width() * 0.5), qCeil(shadow.height() * 0.5)); + const int scaledRadius = qRound(radius * dpr); + boxBlurAlpha(shadow, scaledRadius, blurRect); + mirrorTopLeftQuadrant(shadow); + + // Give the shadow a tint of the desired color. + shadowPainter.begin(&shadow); + shadowPainter.setCompositionMode(QPainter::CompositionMode_SourceIn); + shadowPainter.fillRect(shadow.rect(), color); + shadowPainter.end(); + + // Actually, present the shadow. + QRect shadowRect = shadow.rect(); + shadowRect.setSize(shadowRect.size() / dpr); + shadowRect.moveCenter(rect.center() + offset); + painter->drawImage(shadowRect, shadow); +} + +void BoxShadowRenderer::setBoxSize(const QSize &size) +{ + m_boxSize = size; +} + +void BoxShadowRenderer::setBorderRadius(qreal radius) +{ + m_borderRadius = radius; +} + +void BoxShadowRenderer::setDevicePixelRatio(qreal dpr) +{ + m_dpr = dpr; +} + +void BoxShadowRenderer::addShadow(const QPoint &offset, int radius, const QColor &color) +{ + Shadow shadow = {}; + shadow.offset = offset; + shadow.radius = radius; + shadow.color = color; + m_shadows.append(shadow); +} + +QImage BoxShadowRenderer::render() const +{ + if (m_shadows.isEmpty()) { + return {}; + } + + QSize canvasSize; + for (const Shadow &shadow : qAsConst(m_shadows)) { + canvasSize = canvasSize.expandedTo( + calculateMinimumShadowTextureSize(m_boxSize, shadow.radius, shadow.offset)); + } + + QImage canvas(canvasSize * m_dpr, QImage::Format_ARGB32_Premultiplied); + canvas.setDevicePixelRatio(m_dpr); + canvas.fill(Qt::transparent); + + QRect boxRect(QPoint(0, 0), m_boxSize); + boxRect.moveCenter(QRect(QPoint(0, 0), canvasSize).center()); + + QPainter painter(&canvas); + for (const Shadow &shadow : qAsConst(m_shadows)) { + renderShadow(&painter, boxRect, m_borderRadius, shadow.offset, shadow.radius, shadow.color); + } + painter.end(); + + return canvas; +} + +QSize BoxShadowRenderer::calculateMinimumBoxSize(int radius) +{ + const QSize blurExtent = calculateBlurExtent(radius); + return 2 * blurExtent + QSize(1, 1); +} + +QSize BoxShadowRenderer::calculateMinimumShadowTextureSize(const QSize &boxSize, int radius, const QPoint &offset) +{ + return boxSize + 2 * calculateBlurExtent(radius) + QSize(qAbs(offset.x()), qAbs(offset.y())); +} \ No newline at end of file diff --git a/widgetstyle/boxshadowrenderer.h b/widgetstyle/boxshadowrenderer.h new file mode 100644 index 0000000..6211a73 --- /dev/null +++ b/widgetstyle/boxshadowrenderer.h @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2018 Vlad Zahorodnii + * + * 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 of the License, or + * (at your option) any later version. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +// Qt +#include +#include +#include +#include + +class BoxShadowRenderer +{ +public: + // Compiler generated constructors & destructor are fine. + + /** + * Set the size of the box. + * @param size The size of the box. + **/ + void setBoxSize(const QSize &size); + + /** + * Set the radius of box' corners. + * @param radius The border radius, in pixels. + **/ + void setBorderRadius(qreal radius); + + /** + * Set the device pixel ratio of the resulting shadow texture. + * @param dpr The device pixel ratio. + **/ + void setDevicePixelRatio(qreal dpr); + + /** + * Add a shadow. + * @param offset The offset of the shadow. + * @param radius The blur radius. + * @param color The color of the shadow. + **/ + void addShadow(const QPoint &offset, int radius, const QColor &color); + + /** + * Render the shadow. + **/ + QImage render() const; + + /** + * Calculate the minimum size of the box. + * + * This helper computes the minimum size of the box so the shadow behind it has + * full its strength. + * + * @param radius The blur radius of the shadow. + **/ + static QSize calculateMinimumBoxSize(int radius); + + /** + * Calculate the minimum size of the shadow texture. + * + * This helper computes the minimum size of the resulting texture so the shadow + * is not clipped. + * + * @param boxSize The size of the box. + * @param radius The blur radius. + * @param offset The offset of the shadow. + **/ + static QSize calculateMinimumShadowTextureSize(const QSize &boxSize, int radius, const QPoint &offset); + +private: + QSize m_boxSize; + qreal m_borderRadius = 0.0; + qreal m_dpr = 1.0; + + struct Shadow { + QPoint offset; + int radius; + QColor color; + }; + + QVector m_shadows; +}; diff --git a/widgetstyle/cutefishstyle.json b/widgetstyle/cutefishstyle.json new file mode 100644 index 0000000..5c01a56 --- /dev/null +++ b/widgetstyle/cutefishstyle.json @@ -0,0 +1,4 @@ + +{ + "Keys": ["cutefish"] +} diff --git a/widgetstyle/phantomcolor.cpp b/widgetstyle/phantomcolor.cpp new file mode 100644 index 0000000..3689cfc --- /dev/null +++ b/widgetstyle/phantomcolor.cpp @@ -0,0 +1,423 @@ +/* + * HSLuv-C: Human-friendly HSL + * + * + * + * Copyright (c) 2015 Alexei Boronine (original idea, JavaScript implementation) + * Copyright (c) 2015 Roger Tallada (Obj-C implementation) + * Copyright (c) 2017 Martin Mitas (C implementation, based on Obj-C implementation) + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include "phantomcolor.h" +#include +#include + +namespace Phantom +{ + namespace + { + + // Th`ese declarations originate from hsluv.h, from the hsluv-c library. The + // hpluv functions have been removed, as they are unnecessary for Phantom. + /** + * Convert HSLuv to RGB. + * + * @param h Hue. Between 0.0 and 360.0. + * @param s Saturation. Between 0.0 and 100.0. + * @param l Lightness. Between 0.0 and 100.0. + * @param[out] pr Red component. Between 0.0 and 1.0. + * @param[out] pr Green component. Between 0.0 and 1.0. + * @param[out] pr Blue component. Between 0.0 and 1.0. + */ + void hsluv2rgb(double h, double s, double l, double* pr, double* pg, double* pb); + + /** + * Convert RGB to HSLuv. + * + * @param r Red component. Between 0.0 and 1.0. + * @param g Green component. Between 0.0 and 1.0. + * @param b Blue component. Between 0.0 and 1.0. + * @param[out] ph Hue. Between 0.0 and 360.0. + * @param[out] ps Saturation. Between 0.0 and 100.0. + * @param[out] pl Lightness. Between 0.0 and 100.0. + */ + void rgb2hsluv(double r, double g, double b, double* ph, double* ps, double* pl); + + // Contents below originate from hsluv.c from the hsluv-c library. They have + // been wrapped in a C++ namespace to avoid collisions and to reduce the + // translation unit count, and hsluv's own sRGB conversion code has been + // stripped out (sRGB conversion is now performed in the Phantom color code + // when going to/from the Rgb type.) + // + // If you need to update the hsluv-c code, be mindful of the removed sRGB + // conversions -- you will need to make similar modifications to the upstream + // hsluv-c code. Also note that that the hpluv (pastel) functions have been + // removed, as they are not used in Phantom. + typedef struct Triplet_tag Triplet; + struct Triplet_tag + { + double a; + double b; + double c; + }; + + /* for RGB */ + const Triplet m[3] = {{3.24096994190452134377, -1.53738317757009345794, -0.49861076029300328366}, + {-0.96924363628087982613, 1.87596750150772066772, 0.04155505740717561247}, + {0.05563007969699360846, -0.20397695888897656435, 1.05697151424287856072}}; + + /* for XYZ */ + const Triplet m_inv[3] = {{0.41239079926595948129, 0.35758433938387796373, 0.18048078840183428751}, + {0.21263900587151035754, 0.71516867876775592746, 0.07219231536073371500}, + {0.01933081871559185069, 0.11919477979462598791, 0.95053215224966058086}}; + + const double ref_u = 0.19783000664283680764; + const double ref_v = 0.46831999493879100370; + + const double kappa = 903.29629629629629629630; + const double epsilon = 0.00885645167903563082; + + typedef struct Bounds_tag Bounds; + struct Bounds_tag + { + double a; + double b; + }; + + void get_bounds(double l, Bounds bounds[6]) + { + double tl = l + 16.0; + double sub1 = (tl * tl * tl) / 1560896.0; + double sub2 = (sub1 > epsilon ? sub1 : (l / kappa)); + int channel; + int t; + + for (channel = 0; channel < 3; channel++) { + double m1 = m[channel].a; + double m2 = m[channel].b; + double m3 = m[channel].c; + + for (t = 0; t < 2; t++) { + double top1 = (284517.0 * m1 - 94839.0 * m3) * sub2; + double top2 = (838422.0 * m3 + 769860.0 * m2 + 731718.0 * m1) * l * sub2 - 769860.0 * t * l; + double bottom = (632260.0 * m3 - 126452.0 * m2) * sub2 + 126452.0 * t; + + bounds[channel * 2 + t].a = top1 / bottom; + bounds[channel * 2 + t].b = top2 / bottom; + } + } + } + + double ray_length_until_intersect(double theta, const Bounds* line) + { + return line->b / (sin(theta) - line->a * cos(theta)); + } + + double max_chroma_for_lh(double l, double h) + { + double min_len = DBL_MAX; + double hrad = h * 0.01745329251994329577; /* (2 * pi / 360) */ + Bounds bounds[6]; + int i; + + get_bounds(l, bounds); + for (i = 0; i < 6; i++) { + double len = ray_length_until_intersect(hrad, &bounds[i]); + + if (len >= 0 && len < min_len) + min_len = len; + } + return min_len; + } + + double dot_product(const Triplet* t1, const Triplet* t2) + { + return (t1->a * t2->a + t1->b * t2->b + t1->c * t2->c); + } + + void xyz2rgb(Triplet* in_out) + { + double r = dot_product(&m[0], in_out); + double g = dot_product(&m[1], in_out); + double b = dot_product(&m[2], in_out); + in_out->a = r; + in_out->b = g; + in_out->c = b; + } + + void rgb2xyz(Triplet* in_out) + { + Triplet rgbl = {in_out->a, in_out->b, in_out->c}; + double x = dot_product(&m_inv[0], &rgbl); + double y = dot_product(&m_inv[1], &rgbl); + double z = dot_product(&m_inv[2], &rgbl); + in_out->a = x; + in_out->b = y; + in_out->c = z; + } + + /* http://en.wikipedia.org/wiki/CIELUV + * In these formulas, Yn refers to the reference white point. We are using + * illuminant D65, so Yn (see refY in Maxima file) equals 1. The formula is + * simplified accordingly. + */ + double y2l(double y) + { + if (y <= epsilon) { + return y * kappa; + } else { + return 116.0 * cbrt(y) - 16.0; + } + } + + double l2y(double l) + { + if (l <= 8.0) { + return l / kappa; + } else { + double x = (l + 16.0) / 116.0; + return (x * x * x); + } + } + + void xyz2luv(Triplet* in_out) + { + double divisor = in_out->a + (15.0 * in_out->b) + (3.0 * in_out->c); + if (divisor <= 0.00000001) { + in_out->a = 0.0; + in_out->b = 0.0; + in_out->c = 0.0; + return; + } + + double var_u = (4.0 * in_out->a) / divisor; + double var_v = (9.0 * in_out->b) / divisor; + double l = y2l(in_out->b); + double u = 13.0 * l * (var_u - ref_u); + double v = 13.0 * l * (var_v - ref_v); + + in_out->a = l; + if (l < 0.00000001) { + in_out->b = 0.0; + in_out->c = 0.0; + } else { + in_out->b = u; + in_out->c = v; + } + } + + void luv2xyz(Triplet* in_out) + { + if (in_out->a <= 0.00000001) { + /* Black will create a divide-by-zero error. */ + in_out->a = 0.0; + in_out->b = 0.0; + in_out->c = 0.0; + return; + } + + double var_u = in_out->b / (13.0 * in_out->a) + ref_u; + double var_v = in_out->c / (13.0 * in_out->a) + ref_v; + double y = l2y(in_out->a); + double x = -(9.0 * y * var_u) / ((var_u - 4.0) * var_v - var_u * var_v); + double z = (9.0 * y - (15.0 * var_v * y) - (var_v * x)) / (3.0 * var_v); + in_out->a = x; + in_out->b = y; + in_out->c = z; + } + + void luv2lch(Triplet* in_out) + { + double l = in_out->a; + double u = in_out->b; + double v = in_out->c; + double h; + double c = sqrt(u * u + v * v); + + /* Grays: disambiguate hue */ + if (c < 0.00000001) { + h = 0; + } else { + h = atan2(v, u) * 57.29577951308232087680; /* (180 / pi) */ + if (h < 0.0) + h += 360.0; + } + + in_out->a = l; + in_out->b = c; + in_out->c = h; + } + + void lch2luv(Triplet* in_out) + { + double hrad = in_out->c * 0.01745329251994329577; /* (pi / 180.0) */ + double u = cos(hrad) * in_out->b; + double v = sin(hrad) * in_out->b; + + in_out->b = u; + in_out->c = v; + } + + void hsluv2lch(Triplet* in_out) + { + double h = in_out->a; + double s = in_out->b; + double l = in_out->c; + double c; + + /* White and black: disambiguate chroma */ + if (l > 99.9999999 || l < 0.00000001) { + c = 0.0; + } else { + c = max_chroma_for_lh(l, h) / 100.0 * s; + } + + /* Grays: disambiguate hue */ + if (s < 0.00000001) + h = 0.0; + + in_out->a = l; + in_out->b = c; + in_out->c = h; + } + + void lch2hsluv(Triplet* in_out) + { + double l = in_out->a; + double c = in_out->b; + double h = in_out->c; + double s; + + /* White and black: disambiguate saturation */ + if (l > 99.9999999 || l < 0.00000001) { + s = 0.0; + } else { + s = c / max_chroma_for_lh(l, h) * 100.0; + } + + /* Grays: disambiguate hue */ + if (c < 0.00000001) + h = 0.0; + + in_out->a = h; + in_out->b = s; + in_out->c = l; + } + + void hsluv2rgb(double h, double s, double l, double* pr, double* pg, double* pb) + { + Triplet tmp = {h, s, l}; + + hsluv2lch(&tmp); + lch2luv(&tmp); + luv2xyz(&tmp); + xyz2rgb(&tmp); + + *pr = tmp.a; + *pg = tmp.b; + *pb = tmp.c; + } + + void rgb2hsluv(double r, double g, double b, double* ph, double* ps, double* pl) + { + Triplet tmp = {r, g, b}; + + rgb2xyz(&tmp); + xyz2luv(&tmp); + luv2lch(&tmp); + lch2hsluv(&tmp); + + *ph = tmp.a; + *ps = tmp.b; + *pl = tmp.c; + } + + } // namespace +} // namespace Phantom + +// The code below is for Phantom, and is used for the Rgb/Hsl-based interface +// for color operations. +namespace Phantom +{ + namespace + { + // Note: these constants might be out of range when qreal is defined as float + // instead of double. + inline qreal linear_of_srgb(qreal x) + { + return x < 0.0404482362771082 ? x / 12.92 : std::pow((x + 0.055) / 1.055, 2.4f); + } + inline qreal srgb_of_linear(qreal x) + { + return x < 0.00313066844250063 ? x * 12.92 : std::pow(x, 1.0 / 2.4) * 1.055 - 0.055; + } + } // namespace + + Rgb rgb_of_qcolor(const QColor& color) + { + Rgb a; + a.r = linear_of_srgb(color.red() / 255.0); + a.g = linear_of_srgb(color.green() / 255.0); + a.b = linear_of_srgb(color.blue() / 255.0); + return a; + } + + Hsl hsl_of_rgb(qreal r, qreal g, qreal b) + { + double h, s, l; + rgb2hsluv(r, g, b, &h, &s, &l); + s /= 100.0; + l /= 100.0; + return {h, s, l}; + } + + Rgb rgb_of_hsl(qreal h, qreal s, qreal l) + { + double r, g, b; + hsluv2rgb(h, s * 100.0, l * 100.0, &r, &g, &b); + return {r, g, b}; + } + + QColor qcolor_of_rgb(qreal r, qreal g, qreal b) + { + int r_ = static_cast(std::lround(srgb_of_linear(r) * 255.0)); + int g_ = static_cast(std::lround(srgb_of_linear(g) * 255.0)); + int b_ = static_cast(std::lround(srgb_of_linear(b) * 255.0)); + return {r_, g_, b_}; + } + + QColor lerpQColor(const QColor& x, const QColor& y, qreal a) + { + Rgb x_ = rgb_of_qcolor(x); + Rgb y_ = rgb_of_qcolor(y); + Rgb z = Rgb::lerp(x_, y_, a); + return qcolor_of_rgb(z.r, z.g, z.b); + } + + Rgb Rgb::lerp(const Rgb& x, const Rgb& y, qreal a) + { + Rgb z; + z.r = (1.0 - a) * x.r + a * y.r; + z.g = (1.0 - a) * x.g + a * y.g; + z.b = (1.0 - a) * x.b + a * y.b; + return z; + } +} // namespace Phantom diff --git a/widgetstyle/phantomcolor.h b/widgetstyle/phantomcolor.h new file mode 100644 index 0000000..f9573ba --- /dev/null +++ b/widgetstyle/phantomcolor.h @@ -0,0 +1,165 @@ +/* + * HSLuv-C: Human-friendly HSL + * + * + * + * Copyright (c) 2015 Alexei Boronine (original idea, JavaScript implementation) + * Copyright (c) 2015 Roger Tallada (Obj-C implementation) + * Copyright (c) 2017 Martin Mitas (C implementation, based on Obj-C implementation) + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#ifndef PHANTOMCOLOR_H +#define PHANTOMCOLOR_H + +#include + +namespace Phantom +{ + struct Rgb; + struct Hsl; + + // A color presumed to be in linear space, represented as RGB. Values are in + // the range 0.0 - 1.0. Conversions to and from QColor will assume the QColor + // is in sRGB space, and sRGB conversion will be performed. + struct Rgb + { + qreal r, g, b; + Rgb() + { + } + Rgb(qreal r, qreal g, qreal b) + : r(r) + , g(g) + , b(b) + { + } + + inline Hsl toHsl() const; + inline QColor toQColor() const; + static inline Rgb ofHsl(const Hsl&); + static inline Rgb ofQColor(const QColor&); + + static Rgb lerp(const Rgb& x, const Rgb& y, qreal a); + }; + + // A color represented as pseudo-CIE hue, saturation, and lightness. Hue is in + // the range 0.0 - 360.0 (degrees). Lightness and saturation are in the range + // 0.0 - 1.0. Using this and making adjustments to the L value will produce + // more consistent and predictable results than QColor's .darker()/.lighter(). + // Note that this is not strictly CIE -- some of the colorspace is distorted so + // that it can represented as a continuous coordinate space. Therefore not all + // adjustments to the parameters will produce perfectly linear results with + // regards to saturation and lightness. But it's still useful, and better than + // QColor's .darker()/.lighter(). Additionally, the L value is more useful for + // performing comparisons between two colors to measure relative and absolute + // brightness. + // + // See the documentation for the hsluv library for more information. (Note that + // for consistency we treat the S and L values in the range 0.0 - 1.0 instead + // of 0.0 - 100.0 like hsluv-c on its own does.) + struct Hsl + { + qreal h, s, l; + Hsl() + { + } + Hsl(qreal h, qreal s, qreal l) + : h(h) + , s(s) + , l(l) + { + } + + inline Rgb toRgb() const; + inline QColor toQColor() const; + static inline Hsl ofRgb(const Rgb&); + static inline Hsl ofQColor(const QColor&); + }; + Rgb rgb_of_qcolor(const QColor& color); + QColor qcolor_of_rgb(qreal r, qreal g, qreal b); + Hsl hsl_of_rgb(qreal r, qreal g, qreal b); + Rgb rgb_of_hsl(qreal h, qreal s, qreal l); + + // Clip a floating point value to the range 0.0 - 1.0. + inline qreal saturate(qreal x) + { + if (x < 0.0) + return 0.0; + if (x > 1.0) + return 1.0; + return x; + } + + inline qreal lerp(qreal x, qreal y, qreal a) + { + return (1.0 - a) * x + a * y; + } + + // Linearly interpolate two QColors after trasnforming them to linear color + // space, treating the QColor values as if they were in sRGB space. The + // returned QColor is converted back to sRGB space. + QColor lerpQColor(const QColor& x, const QColor& y, qreal a); + + Hsl Rgb::toHsl() const + { + return hsl_of_rgb(r, g, b); + } + + QColor Rgb::toQColor() const + { + return qcolor_of_rgb(r, g, b); + } + + Rgb Rgb::ofHsl(const Hsl& hsl) + { + return rgb_of_hsl(hsl.h, hsl.s, hsl.l); + } + + Rgb Rgb::ofQColor(const QColor& color) + { + return rgb_of_qcolor(color); + } + + Rgb Hsl::toRgb() const + { + return rgb_of_hsl(h, s, l); + } + + QColor Hsl::toQColor() const + { + Rgb rgb = rgb_of_hsl(h, s, l); + return qcolor_of_rgb(rgb.r, rgb.g, rgb.b); + } + + Hsl Hsl::ofRgb(const Rgb& rgb) + { + return hsl_of_rgb(rgb.r, rgb.g, rgb.b); + } + + Hsl Hsl::ofQColor(const QColor& color) + { + Rgb rgb = rgb_of_qcolor(color); + return hsl_of_rgb(rgb.r, rgb.g, rgb.b); + } + +} // namespace Phantom + +#endif diff --git a/widgetstyle/pstyleplugin.cpp b/widgetstyle/pstyleplugin.cpp new file mode 100644 index 0000000..ce5c034 --- /dev/null +++ b/widgetstyle/pstyleplugin.cpp @@ -0,0 +1,20 @@ +#include "pstyleplugin.h" +#include "basestyle.h" + +#include +#include +#include + +QStringList ProxyStylePlugin::keys() const +{ + return {"cutefish"}; +} + +QStyle *ProxyStylePlugin::create(const QString &key) +{ + if (key != QStringLiteral("cutefish")) { + return nullptr; + } + + return new BaseStyle; +} diff --git a/widgetstyle/pstyleplugin.h b/widgetstyle/pstyleplugin.h new file mode 100644 index 0000000..5d54dba --- /dev/null +++ b/widgetstyle/pstyleplugin.h @@ -0,0 +1,18 @@ +#ifndef PSTYLEPLUGIN_H +#define PSTYLEPLUGIN_H + +#include + +class ProxyStylePlugin : public QStylePlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QStyleFactoryInterface" FILE "cutefishstyle.json") + +public: + ProxyStylePlugin() = default; + + QStringList keys() const; + QStyle *create(const QString &key) override; +}; + +#endif // PSTYLEPLUGIN_H diff --git a/widgetstyle/shadowhelper.cpp b/widgetstyle/shadowhelper.cpp new file mode 100644 index 0000000..d0dea5a --- /dev/null +++ b/widgetstyle/shadowhelper.cpp @@ -0,0 +1,414 @@ +/************************************************************************* + * Copyright (C) 2014 by Hugo Pereira Da Costa * + * Copyright (C) 2018, 2020 by Vlad Zahorodnii * + * Copyright (C) 2020, 2020 by Reven Martin * + * * + * 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 of the License, or * + * (at your option) any later version. * + * * + * 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, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + *************************************************************************/ + +#include "shadowhelper.h" +#include "boxshadowrenderer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +const char netWMSkipShadow[] = "_CYBER_NET_WM_SKIP_SHADOW"; +const char netWMForceShadow[] = "_CYBER_NET_WM_FORCE_SHADOW"; +const char netWMFrameRadius[] = "_CYBER_NET_WM_FRAME_RADIUS"; + +enum { + ShadowNone, + ShadowSmall, + ShadowMedium, + ShadowLarge, + ShadowVeryLarge +}; + +const CompositeShadowParams s_shadowParams[] = { + // None + CompositeShadowParams(), + // Small + CompositeShadowParams( + QPoint(0, 3), + ShadowParams(QPoint(0, 0), 16, 0.26), + ShadowParams(QPoint(0, -2), 8, 0.16)), + // Medium + CompositeShadowParams( + QPoint(0, 4), + ShadowParams(QPoint(0, 0), 20, 0.24), + ShadowParams(QPoint(0, -2), 10, 0.14)), + // Large + CompositeShadowParams( + QPoint(0, 5), + ShadowParams(QPoint(0, 0), 24, 0.22), + ShadowParams(QPoint(0, -3), 12, 0.12)), + // Very Large + CompositeShadowParams( + QPoint(0, 6), + ShadowParams(QPoint(0, 0), 32, 0.2), + ShadowParams(QPoint(0, -3), 16, 0.1)) +}; + + +ShadowHelper::ShadowHelper(QObject * parent) + : QObject(parent), + m_frameRadius(5) +{ +} + +ShadowHelper::~ShadowHelper() +{ +} + +CompositeShadowParams ShadowHelper::lookupShadowParams(int shadowSizeEnum) +{ + switch (shadowSizeEnum) { + case ShadowNone: + return s_shadowParams[0]; + case ShadowSmall: + return s_shadowParams[1]; + case ShadowMedium: + return s_shadowParams[2]; + case ShadowLarge: + return s_shadowParams[3]; + case ShadowVeryLarge: + return s_shadowParams[4]; + default: + // Fallback to the Large size. + return s_shadowParams[3]; + } +} + +bool ShadowHelper::registerWidget(QWidget *widget, bool force) +{ + // make sure widget is not already registered + if (m_widgets.contains(widget)) + return false; + + // check if widget qualifies + if (!(force || acceptWidget(widget))) + return false; + + qreal frameRadius = m_frameRadius; + const auto frameRadiusProperty = widget->property(netWMFrameRadius); + if (frameRadiusProperty.isValid()) + frameRadius = frameRadiusProperty.toReal(); + + installShadows(widget, shadowTiles(frameRadius)); + m_widgets.insert(widget); + + // install event filter + widget->removeEventFilter(this); + widget->installEventFilter(this); + + // connect destroy signal + connect(widget, &QObject::destroyed, this, &ShadowHelper::objectDeleted); + + return true; +} + +void ShadowHelper::unregisterWidget(QWidget *widget) +{ + if (m_widgets.remove(widget)) { + // uninstall the event filter + widget->removeEventFilter(this); + + // disconnect all signals + disconnect(widget, nullptr, this, nullptr); + + // uninstall the shadow + uninstallShadows(widget); + } +} + +bool ShadowHelper::eventFilter(QObject *object, QEvent *event) +{ + if (KWindowSystem::isPlatformX11()) { + // check event type + if (event->type() == QEvent::WinIdChange) { + QWidget *widget = static_cast(object); + + qreal frameRadius = m_frameRadius; + const auto frameRadiusProperty = widget->property(netWMFrameRadius); + if (frameRadiusProperty.isValid()) + frameRadius = frameRadiusProperty.toReal(); + TileSet shadowTileSet = shadowTiles(frameRadius); + installShadows(widget, shadowTileSet); + } + } else { + if (event->type() != QEvent::PlatformSurface) + return false; + + QWidget *widget(static_cast(object)); + QPlatformSurfaceEvent* surfaceEvent(static_cast(event)); + + switch (surfaceEvent->surfaceEventType()) { + case QPlatformSurfaceEvent::SurfaceCreated: + //installShadows(widget); + break; + case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed: + // Don't care. + break; + } + } + + return false; +} + +TileSet ShadowHelper::shadowTiles(const qreal frameRadius) +{ + const CompositeShadowParams params = lookupShadowParams(ShadowVeryLarge); + + if (params.isNone()) + return TileSet(); + // } else if (_shadowTiles.isValid()) { + // return _shadowTiles; + // } + + auto withOpacity = [](const QColor &color, qreal opacity) -> QColor { + QColor c(color); + c.setAlphaF(opacity); + return c; + }; + + const QColor color = Qt::black; + // const qreal strength = static_cast(255) / 255.0; + const qreal strength = 1.5; + + const QSize boxSize = BoxShadowRenderer::calculateMinimumBoxSize(params.shadow1.radius) + .expandedTo(BoxShadowRenderer::calculateMinimumBoxSize(params.shadow2.radius)); + + const qreal dpr = qApp->devicePixelRatio(); + + BoxShadowRenderer shadowRenderer; + shadowRenderer.setBorderRadius(frameRadius); + shadowRenderer.setBoxSize(boxSize); + shadowRenderer.setDevicePixelRatio(dpr); + + shadowRenderer.addShadow(params.shadow1.offset, params.shadow1.radius, + withOpacity(color, params.shadow1.opacity * strength)); + shadowRenderer.addShadow(params.shadow2.offset, params.shadow2.radius, + withOpacity(color, params.shadow2.opacity * strength)); + + QImage shadowTexture = shadowRenderer.render(); + + const QRect outerRect(QPoint(0, 0), shadowTexture.size() / dpr); + + QRect boxRect(QPoint(0, 0), boxSize); + boxRect.moveCenter(outerRect.center()); + + // Mask out inner rect. + QPainter painter(&shadowTexture); + painter.setRenderHint(QPainter::Antialiasing); + + int Shadow_Overlap = 3; + const QMargins margins = QMargins( + boxRect.left() - outerRect.left() - Shadow_Overlap - params.offset.x(), + boxRect.top() - outerRect.top() - Shadow_Overlap - params.offset.y(), + outerRect.right() - boxRect.right() - Shadow_Overlap + params.offset.x(), + outerRect.bottom() - boxRect.bottom() - Shadow_Overlap + params.offset.y()); + + painter.setPen(Qt::NoPen); + painter.setBrush(Qt::black); + painter.setCompositionMode(QPainter::CompositionMode_DestinationOut); + painter.drawRoundedRect( + outerRect - margins, + frameRadius, + frameRadius); + + // We're done. + painter.end(); + + const QPoint innerRectTopLeft = outerRect.center(); + TileSet tiles = TileSet( + QPixmap::fromImage(shadowTexture), + innerRectTopLeft.x(), + innerRectTopLeft.y(), + 1, 1); + + return tiles; +} + +void ShadowHelper::objectDeleted(QObject *object) +{ + QWidget *widget(static_cast(object)); + m_widgets.remove(widget); + m_shadows.remove(widget); +} + +bool ShadowHelper::isMenu(QWidget *widget) const +{ + return qobject_cast(widget); +} + +bool ShadowHelper::isToolTip(QWidget *widget) const +{ + return widget->inherits("QTipLabel") || (widget->windowFlags() & Qt::WindowType_Mask) == Qt::ToolTip; +} + +bool ShadowHelper::isDockWidget(QWidget *widget) const +{ + return qobject_cast(widget); +} + +bool ShadowHelper::isToolBar(QWidget *widget) const +{ + return qobject_cast(widget); +} + +bool ShadowHelper::acceptWidget(QWidget *widget) const +{ + // flags + if (widget->property(netWMSkipShadow).toBool()) + return false; + if (widget->property(netWMForceShadow).toBool()) + return true; + + // menus + if (isMenu(widget)) + return true; + + // combobox dropdown lists + if (widget->inherits("QComboBoxPrivateContainer")) + return true; + + // tooltips + if (isToolTip(widget) && !widget->inherits("Plasma::ToolTip")) + return true; + + // detached widgets + if (isDockWidget(widget) || isToolBar(widget)) + return true; + + // reject + return false; +} + +KWindowShadowTile::Ptr ShadowHelper::createTile(const QPixmap& source) +{ + KWindowShadowTile::Ptr tile = KWindowShadowTile::Ptr::create(); + tile->setImage(source.toImage()); + return tile; +} + +void ShadowHelper::installShadows(QWidget *widget, TileSet shadowTiles) +{ + if (!widget) + return; + + // only toplevel widgets can cast drop-shadows + if (!widget->isWindow()) + return; + + // widget must have valid native window + if (!widget->testAttribute(Qt::WA_WState_Created)) + return; + + // create platform shadow tiles + QVector tiles = { + createTile(shadowTiles.pixmap(1)), + createTile(shadowTiles.pixmap(2)), + createTile(shadowTiles.pixmap(5)), + createTile(shadowTiles.pixmap(8)), + createTile(shadowTiles.pixmap(7)), + createTile(shadowTiles.pixmap(6)), + createTile(shadowTiles.pixmap(3)), + createTile(shadowTiles.pixmap(0)) + }; + if (tiles.count() != numTiles) + return; + + // find a shadow associated with the widget + KWindowShadow*& shadow = m_shadows[ widget ]; + + if (!shadow) + shadow = new KWindowShadow(widget); + + if (shadow->isCreated()) + shadow->destroy(); + + shadow->setTopTile(tiles[ 0 ]); + shadow->setTopRightTile(tiles[ 1 ]); + shadow->setRightTile(tiles[ 2 ]); + shadow->setBottomRightTile(tiles[ 3 ]); + shadow->setBottomTile(tiles[ 4 ]); + shadow->setBottomLeftTile(tiles[ 5 ]); + shadow->setLeftTile(tiles[ 6 ]); + shadow->setTopLeftTile(tiles[ 7 ]); + shadow->setPadding(shadowMargins(widget, shadowTiles)); + shadow->setWindow(widget->windowHandle()); + shadow->create(); +} + +QMargins ShadowHelper::shadowMargins(QWidget *widget, TileSet shadowTiles) const +{ + const CompositeShadowParams params = lookupShadowParams(ShadowVeryLarge); + if (params.isNone()) + return QMargins(); + + const QSize boxSize = BoxShadowRenderer::calculateMinimumBoxSize(params.shadow1.radius) + .expandedTo(BoxShadowRenderer::calculateMinimumBoxSize(params.shadow2.radius)); + + const QSize shadowSize = BoxShadowRenderer::calculateMinimumShadowTextureSize(boxSize, params.shadow1.radius, params.shadow1.offset) + .expandedTo(BoxShadowRenderer::calculateMinimumShadowTextureSize(boxSize, params.shadow2.radius, params.shadow2.offset)); + + const QRect shadowRect(QPoint(0, 0), shadowSize); + + QRect boxRect(QPoint(0, 0), boxSize); + boxRect.moveCenter(shadowRect.center()); + + int Shadow_Overlap = 3; + QMargins margins( + boxRect.left() - shadowRect.left() - Shadow_Overlap - params.offset.x(), + boxRect.top() - shadowRect.top() - Shadow_Overlap - params.offset.y(), + shadowRect.right() - boxRect.right() - Shadow_Overlap + params.offset.x(), + shadowRect.bottom() - boxRect.bottom() - Shadow_Overlap + params.offset.y()); + + if (widget->inherits("QBalloonTip")) { + // Balloon tip needs special margins to deal with the arrow. + int top = widget->contentsMargins().top(); + int bottom = widget->contentsMargins().bottom(); + + // Need to decrement default size further due to extra hard coded round corner. + margins -= 1; + + // Arrow can be either to the top or the bottom. Adjust margins accordingly. + const int diff = qAbs(top - bottom); + if (top > bottom) { + margins.setTop(margins.top() - diff); + } else { + margins.setBottom(margins.bottom() - diff); + } + } + + margins *= shadowTiles.pixmap(0).devicePixelRatio(); + + return margins; +} + +void ShadowHelper::uninstallShadows(QWidget *widget) +{ + delete m_shadows.take(widget); +} diff --git a/widgetstyle/shadowhelper.h b/widgetstyle/shadowhelper.h new file mode 100644 index 0000000..6bc123c --- /dev/null +++ b/widgetstyle/shadowhelper.h @@ -0,0 +1,148 @@ +/************************************************************************* + * Copyright (C) 2014 by Hugo Pereira Da Costa * + * Copyright (C) 2020 by Vlad Zahorodnii * + * Copyright (C) 2020, 2020 by Reven Martin * + * * + * 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 of the License, or * + * (at your option) any later version. * + * * + * 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, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + *************************************************************************/ + +#ifndef SHADOWHELPER_H +#define SHADOWHELPER_H + +#include "tileset.h" +#include + +#include +#include +#include +#include +#include + +struct ShadowParams +{ + ShadowParams() = default; + + ShadowParams(const QPoint &offset, int radius, qreal opacity): + offset(offset), + radius(radius), + opacity(opacity) + {} + + QPoint offset; + int radius = 0; + qreal opacity = 0; +}; + +struct CompositeShadowParams +{ + CompositeShadowParams() = default; + + CompositeShadowParams( + const QPoint &offset, + const ShadowParams &shadow1, + const ShadowParams &shadow2) + : offset(offset) + , shadow1(shadow1) + , shadow2(shadow2) {} + + bool isNone() const + { return qMax(shadow1.radius, shadow2.radius) == 0; } + + QPoint offset; + ShadowParams shadow1; + ShadowParams shadow2; +}; + +//* handle shadow pixmaps passed to window manager via X property +class ShadowHelper: public QObject +{ + Q_OBJECT + +public: + //* constructor + ShadowHelper(QObject *); + + //* destructor + ~ShadowHelper() override; + + //* shadow params from size enum + static CompositeShadowParams lookupShadowParams(int shadowSizeEnum); + + //* register widget + bool registerWidget(QWidget *, bool force = false); + + //* unregister widget + void unregisterWidget(QWidget *); + + //* event filter + bool eventFilter(QObject *, QEvent *) override; + + void setFrameRadius(qreal radius) { m_frameRadius = radius; } + + //* shadow tiles + /** is public because it is also needed for mdi windows */ + // TileSet shadowTiles(); + + TileSet shadowTiles(const qreal frameRadius); + +protected Q_SLOTS: + //* unregister widget + void objectDeleted(QObject *); + +protected: + //* true if widget is a menu + bool isMenu(QWidget *) const; + + //* true if widget is a tooltip + bool isToolTip(QWidget *) const; + + //* dock widget + bool isDockWidget(QWidget *) const; + + //* toolbar + bool isToolBar(QWidget *) const; + + //* accept widget + bool acceptWidget(QWidget *) const; + + // create shadow tile from pixmap + KWindowShadowTile::Ptr createTile(const QPixmap &); + + //* installs shadow on given widget in a platform independent way + // void installShadows( QWidget * ); + + void installShadows(QWidget *widget, TileSet shadowTiles); + + //* uninstalls shadow on given widget in a platform independent way + void uninstallShadows(QWidget *); + + //* gets the shadow margins for the given widget + QMargins shadowMargins(QWidget*, TileSet) const; + +private: + //* registered widgets + QSet m_widgets; + + //* managed shadows + QMap m_shadows; + + qreal m_frameRadius; + + //* number of tiles + enum { numTiles = 8 }; +}; + +#endif diff --git a/widgetstyle/tileset.cpp b/widgetstyle/tileset.cpp new file mode 100644 index 0000000..b53f7c8 --- /dev/null +++ b/widgetstyle/tileset.cpp @@ -0,0 +1,183 @@ +/************************************************************************* + * Copyright (C) 2014 by Hugo Pereira Da Costa * + * * + * 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 of the License, or * + * (at your option) any later version. * + * * + * 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, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + *************************************************************************/ + +#include "tileset.h" + +#include + +//___________________________________________________________ +inline bool bits(TileSet::Tiles flags, TileSet::Tiles testFlags) +{ return (flags & testFlags) == testFlags; } + +//______________________________________________________________________________________ +inline qreal devicePixelRatio( const QPixmap& pixmap ) +{ + return pixmap.devicePixelRatio(); +} + +//______________________________________________________________________________________ +inline void setDevicePixelRatio( QPixmap& pixmap, qreal value ) +{ + return pixmap.setDevicePixelRatio( value ); +} + +//______________________________________________________________ +void TileSet::initPixmap( PixmapList& pixmaps, const QPixmap &source, int width, int height, const QRect &rect) +{ + QSize size( width, height ); + if( !( size.isValid() && rect.isValid() ) ) + { + pixmaps.append( QPixmap() ); + + } else if( size != rect.size() ) { + + const qreal dpiRatio( devicePixelRatio( source ) ); + const QRect scaledRect( rect.topLeft()*dpiRatio, rect.size()*dpiRatio ); + const QSize scaledSize( size*dpiRatio ); + const QPixmap tile( source.copy(scaledRect) ); + QPixmap pixmap( scaledSize ); + + pixmap.fill(Qt::transparent); + QPainter painter(&pixmap); + painter.drawTiledPixmap(0, 0, scaledSize.width(), scaledSize.height(), tile); + setDevicePixelRatio( pixmap, dpiRatio ); + pixmaps.append( pixmap ); + + } else { + + const qreal dpiRatio( devicePixelRatio( source ) ); + const QRect scaledRect( rect.topLeft()*dpiRatio, rect.size()*dpiRatio ); + QPixmap pixmap( source.copy( scaledRect ) ); + setDevicePixelRatio( pixmap, dpiRatio ); + pixmaps.append( pixmap ); + + } + +} + +//______________________________________________________________ +TileSet::TileSet(): + _w1(0), + _h1(0), + _w3(0), + _h3(0) +{ _pixmaps.reserve(9); } + +//______________________________________________________________ +TileSet::TileSet(const QPixmap &source, int w1, int h1, int w2, int h2 ): + _w1(w1), + _h1(h1), + _w3(0), + _h3(0) +{ + _pixmaps.reserve(9); + if( source.isNull() ) return; + + _w3 = source.width()/devicePixelRatio( source ) - (w1 + w2); + _h3 = source.height()/devicePixelRatio( source ) - (h1 + h2); + int w = w2; + int h = h2; + + // initialise pixmap array + initPixmap( _pixmaps, source, _w1, _h1, QRect(0, 0, _w1, _h1) ); + initPixmap( _pixmaps, source, w, _h1, QRect(_w1, 0, w2, _h1) ); + initPixmap( _pixmaps, source, _w3, _h1, QRect(_w1+w2, 0, _w3, _h1) ); + initPixmap( _pixmaps, source, _w1, h, QRect(0, _h1, _w1, h2) ); + initPixmap( _pixmaps, source, w, h, QRect(_w1, _h1, w2, h2) ); + initPixmap( _pixmaps, source, _w3, h, QRect(_w1+w2, _h1, _w3, h2) ); + initPixmap( _pixmaps, source, _w1, _h3, QRect(0, _h1+h2, _w1, _h3) ); + initPixmap( _pixmaps, source, w, _h3, QRect(_w1, _h1+h2, w2, _h3) ); + initPixmap( _pixmaps, source, _w3, _h3, QRect(_w1+w2, _h1+h2, _w3, _h3) ); +} + +//___________________________________________________________ +void TileSet::render(const QRect &constRect, QPainter *painter, Tiles tiles) const +{ + + const bool oldHint( painter->testRenderHint( QPainter::SmoothPixmapTransform ) ); + painter->setRenderHint( QPainter::SmoothPixmapTransform, true ); + + // check initialization + if( _pixmaps.size() < 9 ) return; + + // copy source rect + QRect rect( constRect ); + + // get rect dimensions + int x0, y0, w, h; + rect.getRect(&x0, &y0, &w, &h); + + // calculate pixmaps widths + int wLeft(0); + int wRight(0); + if( _w1+_w3 > 0 ) + { + qreal wRatio( qreal( _w1 )/qreal( _w1 + _w3 ) ); + wLeft = (tiles&Right) ? qMin( _w1, int(w*wRatio) ):_w1; + wRight = (tiles&Left) ? qMin( _w3, int(w*(1.0-wRatio)) ):_w3; + } + + // calculate pixmap heights + int hTop(0); + int hBottom(0); + if( _h1+_h3 > 0 ) + { + qreal hRatio( qreal( _h1 )/qreal( _h1 + _h3 ) ); + hTop = (tiles&Bottom) ? qMin( _h1, int(h*hRatio) ):_h1; + hBottom = (tiles&Top) ? qMin( _h3, int(h*(1.0-hRatio)) ):_h3; + } + + // calculate corner locations + w -= wLeft + wRight; + h -= hTop + hBottom; + const int x1 = x0 + wLeft; + const int x2 = x1 + w; + const int y1 = y0 + hTop; + const int y2 = y1 + h; + + const int w2 = _pixmaps.at(7).width()/devicePixelRatio( _pixmaps.at(7) ); + const int h2 = _pixmaps.at(5).height()/devicePixelRatio( _pixmaps.at(5) ); + + // corner + if( bits( tiles, Top|Left) ) painter->drawPixmap(x0, y0, _pixmaps.at(0), 0, 0, wLeft*devicePixelRatio( _pixmaps.at(0) ), hTop*devicePixelRatio( _pixmaps.at(0) )); + if( bits( tiles, Top|Right) ) painter->drawPixmap(x2, y0, _pixmaps.at(2), (_w3-wRight)*devicePixelRatio( _pixmaps.at(2) ), 0, wRight*devicePixelRatio( _pixmaps.at(2) ), hTop*devicePixelRatio( _pixmaps.at(2) ) ); + if( bits( tiles, Bottom|Left) ) painter->drawPixmap(x0, y2, _pixmaps.at(6), 0, (_h3-hBottom)*devicePixelRatio( _pixmaps.at(6) ), wLeft*devicePixelRatio( _pixmaps.at(6) ), hBottom*devicePixelRatio( _pixmaps.at(6) )); + if( bits( tiles, Bottom|Right) ) painter->drawPixmap(x2, y2, _pixmaps.at(8), (_w3-wRight)*devicePixelRatio( _pixmaps.at(8) ), (_h3-hBottom)*devicePixelRatio( _pixmaps.at(8) ), wRight*devicePixelRatio( _pixmaps.at(8) ), hBottom*devicePixelRatio( _pixmaps.at(8) ) ); + + // top and bottom + if( w > 0 ) + { + if( tiles&Top ) painter->drawPixmap(x1, y0, w, hTop, _pixmaps.at(1), 0, 0, w2*devicePixelRatio( _pixmaps.at(1) ), hTop*devicePixelRatio( _pixmaps.at(1) ) ); + if( tiles&Bottom ) painter->drawPixmap(x1, y2, w, hBottom, _pixmaps.at(7), 0, (_h3-hBottom)*devicePixelRatio( _pixmaps.at(7) ), w2*devicePixelRatio( _pixmaps.at(7) ), hBottom*devicePixelRatio( _pixmaps.at(7) ) ); + } + + // left and right + if( h > 0 ) + { + if( tiles&Left ) painter->drawPixmap(x0, y1, wLeft, h, _pixmaps.at(3), 0, 0, wLeft*devicePixelRatio( _pixmaps.at(3) ), h2*devicePixelRatio( _pixmaps.at(3) ) ); + if( tiles&Right ) painter->drawPixmap(x2, y1, wRight, h, _pixmaps.at(5), (_w3-wRight)*devicePixelRatio( _pixmaps.at(5) ), 0, wRight*devicePixelRatio( _pixmaps.at(5) ), h2*devicePixelRatio( _pixmaps.at(5) ) ); + } + + // center + if( (tiles&Center) && h > 0 && w > 0 ) painter->drawPixmap(x1, y1, w, h, _pixmaps.at(4)); + + // restore + painter->setRenderHint( QPainter::SmoothPixmapTransform, oldHint ); + +} diff --git a/widgetstyle/tileset.h b/widgetstyle/tileset.h new file mode 100644 index 0000000..4f16f8c --- /dev/null +++ b/widgetstyle/tileset.h @@ -0,0 +1,121 @@ +#ifndef TILESET_H +#define TILESET_H + +/************************************************************************* + * Copyright (C) 2014 by Hugo Pereira Da Costa * + * * + * 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 of the License, or * + * (at your option) any later version. * + * * + * 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, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + *************************************************************************/ + + +#include +#include +#include + +//* handles proper scaling of pixmap to match widget rect. +/** +tilesets are collections of stretchable pixmaps corresponding to a given widget corners, sides, and center. +corner pixmaps are never stretched. center pixmaps are +*/ +class TileSet +{ + public: + /** + Create a TileSet from a pixmap. The size of the bottom/right chunks is + whatever is left over from the other chunks, whose size is specified + in the required parameters. + + @param w1 width of the left chunks + @param h1 height of the top chunks + @param w2 width of the not-left-or-right chunks + @param h2 height of the not-top-or-bottom chunks + */ + TileSet(const QPixmap&, int w1, int h1, int w2, int h2 ); + + //* empty constructor + TileSet(); + + //* destructor + virtual ~TileSet() + {} + + /** + Flags specifying what sides to draw in ::render. Corners are drawn when + the sides forming that corner are drawn, e.g. Top|Left draws the + top-center, center-left, and top-left chunks. The center-center chunk is + only drawn when Center is requested. + */ + enum Tile { + Top = 0x1, + Left = 0x2, + Bottom = 0x4, + Right = 0x8, + Center = 0x10, + TopLeft = Top|Left, + TopRight = Top|Right, + BottomLeft = Bottom|Left, + BottomRight = Bottom|Right, + Ring = Top|Left|Bottom|Right, + Horizontal = Left|Right|Center, + Vertical = Top|Bottom|Center, + Full = Ring|Center + }; + Q_DECLARE_FLAGS(Tiles, Tile) + + /** + Fills the specified rect with tiled chunks. Corners are never tiled, + edges are tiled in one direction, and the center chunk is tiled in both + directions. Partial tiles are used as needed so that the entire rect is + perfectly filled. Filling is performed as if all chunks are being drawn. + */ + void render(const QRect&, QPainter*, Tiles = Ring) const; + + //* return size associated to this tileset + QSize size() const + { return QSize( _w1 + _w3, _h1 + _h3 ); } + + //* is valid + bool isValid() const + { return _pixmaps.size() == 9; } + + //* returns pixmap for given index + QPixmap pixmap( int index ) const + { return _pixmaps[index]; } + + protected: + + //* shortcut to pixmap list + using PixmapList = QVector; + + //* initialize pixmap + void initPixmap( PixmapList&, const QPixmap&, int w, int h, const QRect& ); + + private: + + //* pixmap arry + PixmapList _pixmaps; + + // dimensions + int _w1; + int _h1; + int _w3; + int _h3; + +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(TileSet::Tiles) + +#endif //TILESET_H