From eab3cfd863593d736802250631c1a0c441400e71 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sun, 8 Sep 2024 07:12:45 -0500 Subject: [PATCH 1/7] Update docs to use Qt6 and not Qt5. --- README.md | 2 +- example/titlebar.py | 5 +++-- test/ui.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a271735..f839b16 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ The limitations of stylesheets include: - QToolButton cannot control the icon size without also affecting the arrow size. - Close and dock float icon sizes scale poorly with font size. -For an example of using QCommonStyle to override standard icons in a PyQt application, see [standard_icons.py](/example/standard_icons.py). An extensive reference can be found [here](https://doc.qt.io/qt-5/style-reference.html). A reference of QStyle, and the default styles Qt provides can be found [here](https://doc.qt.io/qt-5/qstyle.html). +For an example of using QCommonStyle to override standard icons in a PyQt application, see [standard_icons.py](/example/standard_icons.py). An extensive reference can be found [here](https://doc.qt.io/qt-6/style-reference.html). A reference of QStyle, and the default styles Qt provides can be found [here](https://doc.qt.io/qt-6/qstyle.html). # Installing diff --git a/example/titlebar.py b/example/titlebar.py index 6da781d..bbfbd76 100644 --- a/example/titlebar.py +++ b/example/titlebar.py @@ -589,8 +589,9 @@ def start_resize(self, window, window_type): # Grab the mouse so we can intercept the click event, # and track hover events outside the app. This doesn't - # work on Wayland or on macOS. - # https://doc.qt.io/qt-5/qwidget.html#grabMouse + # work on Wayland or on macOS. On Windows, it only works + # within the window owned by the process. + # https://doc.qt.io/qt-6/qwidget.html#grabMouse if not IS_TRUE_WAYLAND and sys.platform != 'darwin': self.window().grabMouse() diff --git a/test/ui.py b/test/ui.py index df876eb..59e4c0a 100644 --- a/test/ui.py +++ b/test/ui.py @@ -760,7 +760,7 @@ def test_button_position_tabwidget(widget, *_): def test_text_browser(widget, *_): child = QtWidgets.QTextBrowser(widget) child.setOpenExternalLinks(True) - child.setMarkdown('[QTextBrowser](https://doc.qt.io/qt-5/qtextbrowser.html)') + child.setMarkdown('[QTextBrowser](https://doc.qt.io/qt-6/qtextbrowser.html)') return child From 637ced53c5ac1ce0c71885a21996179f7a907f78 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sun, 8 Sep 2024 08:35:26 -0500 Subject: [PATCH 2/7] Refactor README for better visiibility. --- README.md | 592 ++++++++++++++++++++++++++---------------------------- 1 file changed, 290 insertions(+), 302 deletions(-) diff --git a/README.md b/README.md index f839b16..44bb212 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -BreezeStyleSheets -================= +# BreezeStyleSheets Configurable Breeze and BreezeDark-like stylesheets for Qt Applications. @@ -7,33 +6,35 @@ BreezeStyleSheets is a set of beautiful light and dark stylesheets that render c **Table of Contents** -- [Gallery](#gallery) -- [Customization](#customization) -- [Examples](#examples) -- [Features](#features) - - [Extensions](#extensions) -- [Extending Stylesheets](#extending-stylesheets) -- [Installing](#installing) - - [CMake Installation](#cmake-installation) - - [QMake Installation](#qmake-installation) - - [PyQt5/6 & PySide2/6 Installation](#pyqt56--pyside26-installation) -- [Debugging](#debugging) -- [Development Guide](#development-guide) - - [Configuring](#configuring) - - [Testing](#testing) - - [Distribution Files](#distribution-files) - - [Git Ignore](#git-ignore) -- [What's changed in this fork?](#whats-changed-in-this-fork) -- [Known Issues and Workarounds](#known-issues-and-workarounds) -- [Developing](#developing) -- [License](#license) -- [Contributing](#contributing) -- [Acknowledgements](#acknowledgements) -- [Contact](#contact) - -# Gallery - -**Breeze/BreezeDark** +1. [Gallery](#gallery) +2. [Getting Started](#getting-started) + - [Building Styles](#building-styles) + - [Python Installation](#python-installation) + - [CMake Installation](#cmake-installation) + - [QMake Installation](#qmake-installation) +3. [Examples](#examples) +4. [Features](#features) + - [Extensions](#extensions) +5. [Customization](#customization) +6. [Extending Stylesheets](#extending-stylesheets) +7. [Debugging](#debugging) +8. [Development Guide](#development-guide) + - [Git Hooks](#git-hooks) + - [Configuring Styles](#configuring-styles) + - [Testing](#testing) + - [Linting and Type Checks](#linting-and-type-checks) + - [Distribution Files](#distribution-files) + - [Git Ignore](#git-ignore) +9. [What's changed in this fork?](#whats-changed-in-this-fork) +10. [Known Issues and Workarounds](#known-issues-and-workarounds) +11. [License](#license) +12. [Contributing](#contributing) +13. [Acknowledgements](#acknowledgements) +14. [Contact](#contact) + +## Gallery + +### Breeze/BreezeDark Example user interface using the Breeze and BreezeDark stylesheets side-by-side. @@ -59,210 +60,66 @@ Alternative themes - Change QTableWidget hover behavior to highlight whole row, For an extensive view of screenshots of the theme, see the [gallery](assets/gallery.md). -# Customization +## Getting Started -It's easy to design your own themes using `configure.py`. First, add the styles you want into [theme](/theme/), then run configure with a list of styles you want to include. - -**Theme** - -Here is a sample theme, with the color descriptions annotated. Please note that although there are nearly 40 possibilities, for most applications, you should use less than 20, and ~10 different hues. +Here are detailed instructions on how to install Breeze Style Sheets for a variety of build systems and programming languages. This will require a Qt installation with QtCore, QtGui, QtWidgets, and QtSvg installed. -```jsonc -// NOTE: This is a custom JSON file, where lines leading -// with `//` are removed. No other comments are valid. -{ - // Main foreground color. - "foreground": "#eff0f1", - // Lighter foreground color for selected items. - "foreground-light": "#ffffff", - // Main background color. - "background": "#31363b", - // Alternate background color for styles. - "background:alternate": "#31363b", - // Main color to highlight widgets, such as on hover events. - "highlight": "#3daee9", - // Color for selected widgets so hover events can change widget color. - "highlight:dark": "#2a79a3", - // Alternate highlight color for hovered widgets in QAbstractItemViews. - "highlight:alternate": "#369cd1", - // Main midtone color, such as for borders. - "midtone": "#76797c", - // Lighter color for midtones, such as for certain disabled widgets. - "midtone:light": "#b0b0b0", - // Darker midtone, such as for the background of QPushButton and QSlider. - "midtone:dark": "#626568", - // Lighter midtone for separator hover events. - "midtone:hover": "#8a8d8f", - // Color for checked widgets in QAbstractItemViews. - "view:checked": "#334e5e", - // Hover background color in QAbstractItemViews. - // This should be fairly transparent. - "view:hover": "rgba(61, 173, 232, 0.1)", - // Background for a horizontal QToolBar. - "toolbar:horizontal:background": "#31363b", - // Background for a vertical QToolBar. - "toolbar:vertical:background": "#31363b", - // Background color for the corner widget in a QAbstractItemView. - "view:corner": "#31363b", - // Border color between items in a QHeaderView. - "view:header:border": "#76797c", - // Background color for a QHeaderView. - "view:header": "#31363b", - // Border color Between items in a QAbstractItemView. - "view:border": "#31363b", - // Background for QAbstractItemViews. - "view:background": "#1d2023", - // Background for widgets with text input. - "text:background": "#1d2023", - // Background for the currently selected tab. - "tab:background:selected": "#31363b", - // Background for non-selected tabs. - "tab:background": "#2c3034", - // Color for the branch/arrow icons in a QTreeView. - "tree": "#afafaf", - // Color for the chunk of a QProgressBar, the active groove - // of a QSlider, and the border of a hovered QSlider handle. - "slider:foreground": "#3daee9", - // Background color for the handle of a QSlider. - "slider:handle:background": "#1d2023", - // Color for a disabled menubar/menu item. - "menu:disabled": "#76797c", - // Color for a checked/hovered QCheckBox or QRadioButton. - "checkbox:light": "#58d3ff", - // Color for a disabled or unchecked/unhovered QCheckBox or QRadioButton. - "checkbox:disabled": "#c8c9ca", - // Color for the handle of a scrollbar. Due to limitations of - // Qt stylesheets, any handle of a scrollbar must be treated - // like it's hovered. - "scrollbar:hover": "#3daee9", - // Background for a non-hovered scrollbar. - "scrollbar:background": "#1d2023", - // Background for a hovered scrollbar. - "scrollbar:background:hover": "#76797c", - // Default background for a QPushButton. - "button:background": "#31363b", - // Background for a pressed QPushButton. - "button:background:pressed": "#454a4f", - // Border for a non-hovered QPushButton. - "button:border": "#76797c", - // Background for a disabled QPushButton, or fallthrough - // for disabled QWidgets. - "button:disabled": "#454545", - // Color of a dock/tab close icon when hovered. - "close:hover": "#b37979", - // Color of a dock/tab close icon when pressed. - "close:pressed": "#b33e3e", - // Default background color for QDockWidget and title. - "dock:background": "#31363b", - // Color for the float icon for QDockWidgets. - "dock:float": "#a2a2a2", - // Background color for the QMessageBox critical icon. - "critical": "#80404a", - // Background color for the QMessageBox information icon. - "information": "#406880", - // Background color for the QMessageBox question icon. - "question": "#634d80", - // Background color for the QMessageBox warning icon. - "warning": "#99995C", - // These are extension-specific - // The background color for an Advanced Docking System Tab - "ads-tab:focused": "rgba(61, 173, 232, 0.1)", - "ads-border:focused": "rgba(61, 173, 232, 0.15)" -} -``` +### Building Styles -Once you've saved your custom theme, you can then build the stylesheet, icons, and resource file with: +By default, BreezeStyleSheets comes with the `dark` and `light` themes pre-built in the [resources](/resources/) directory. In order to build all pre-packaged themes including PyQt5 and PyQt6 support, run: ```bash -python configure.py --styles=dark,light, --resource custom.qrc +# choose only the frameworks you want +frameworks=("pyqt5" "pyqt6" "pyside2" "pyside6") +for framework in "${frameworks[@]}"; do + python configure.py --styles=all --extensions=all --qt-framework "${framework}" \ + --resource breeze.qrc --compiled-resource "breeze_${framework}.py" +done ``` -Then, you can use `custom.qrc`, along with the generated icons and stylesheets in each folder, in place of `breeze.qrc` for any style. - -The `--styles` command flag takes a comma-separated list of values, or `all`, which will configure every theme present in the [themes](/theme) directory. - -**Generating Colors** - -As a reference point, see the pre-generated [themes](/theme). In general, to create a good theme, modify only the highlight colors (blues, greens, purples) to a new color, such that the saturation and lightness stay the same (only the hue changes). For example, the color `rgba(51, 164, 223, 0.5)` becomes `rgba(164, 51, 223, 0.5)`. +All generated themes will be in the [dist](/dist) subdirectory, and the compiled Python resource(s) will be in `resources/breeze_{framework}.py` (for example, `resources/breeze_pyqt5.py`). Note that using the `--compiled-resource` flag requires the correct RCC to be installed for the Qt framework (see [Python Installation](#python-installation) for the required RCC). -**Extensions** +### Python Installation -We also allow customizable extensions to extend the default stylesheets with additional style rules, using the colors defined in your theme. This also enables the integration of third-party Qt extensions/widgets into the generated stylesheets. - -For example, to configure with extensions for the [Advanced Docking System](https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System), run: +To compile the stylesheet for use with PyQt5, PyQt6, PySide2 or PySide6, ensure you configure with the `--compiled-resource` flag (which requires the rcc executable for your chosen framework to be installed - see below for details). The compiled resource Python file now contains all the stylesheet data. To load and set the stylesheet in a PyQt5/6 or PySide2/6 application, import that file, load the contents using QFile and read the data. For example, to load BreezeDark, first configure using: ```bash -python configure.py --extensions=advanced-docking-system --resource custom.qrc +python configure.py --compiled-resource breeze_resources.py ``` -Like with styles, `--extensions` takes a comma-separated list of values, or `all`, which will add every extension present in the [extensions](/extension) directory. For a detailed introduction to creating your own extensions, see the extensions [tutorial](/extension/README.md). - -# Examples - -Many examples of widgets using [custom themes](/example/widgets.py), including with the [Advanced Docking System](/example/advanced-dock.py), [custom icons](/example/standard_icons.py), and [titlebars](/example/titlebar.py) can be found in the [example](/example/) directory. - -The support stylesheets include: -- `dark` -- `light` -- `auto` -- `native` (the system native theme) - -And any `-purple`, `-green`, etc. variants can also be used. `auto` will automatically detect if the system theme is light or dark and select the correct theme accordingly. The cross-platform way to detect the correct theme is using `get_theme` in either [Python](/example/breeze_theme.py) or [C++](/example/breeze_theme.hpp). Just include those stand-alone files in your project and you can select the desired theme based on the user's profile settings at startup. - -# Features +Then load the stylesheet and run the application using: -- Complete stylesheet for all Qt widgets, including esoteric widgets like `QCalendarWidget`. -- Customizable, beautiful light and dark themes. -- Cross-platform icon packs for standard icons. -- Extensible stylesheets: add your own extensions or rules and automatically configure them using the same configuration syntax. +```python +from PyQt5 import QtWidgets +from PyQt5.QtCore import QFile, QTextStream +# This must match the name of the file and be in the Python search path. +# To modify the search path, add the directory containing the file to `sys.path`. +import breeze_resources -## Extensions -The supported extensions can be found in the [extensions](/extension/README.md) directory and include theme support for: -- [Advanced Docking System](/extension/README.md#advanced-docking-system) -- [QDockWidget Tooltips](/extension/README.md#qdockwidget-tooltips) -- [Complete Standard Icon Set](/extension/README.md#standard-icons) +def main(): + app = QtWidgets.QApplication(sys.argv) -# Extending Stylesheets + # set stylesheet + file = QFile(":/dark/stylesheet.qss") + file.open(QFile.ReadOnly | QFile.Text) + stream = QTextStream(file) + app.setStyleSheet(stream.readAll()) -There are some limitations of using Qt stylesheets in general, which cannot be solved by stylesheets. To get more fine-grained style control, you should subclass `QCommonStyle`: + # code goes here -```c++ -class ApplicationStyle: public QCommonStyle -{ - ... -} + app.exec_() ``` -The limitations of stylesheets include: - -- Using custom standard icons. -- Scaling icons with the theme size. -- QToolButton cannot control the icon size without also affecting the arrow size. -- Close and dock float icon sizes scale poorly with font size. - -For an example of using QCommonStyle to override standard icons in a PyQt application, see [standard_icons.py](/example/standard_icons.py). An extensive reference can be found [here](https://doc.qt.io/qt-6/style-reference.html). A reference of QStyle, and the default styles Qt provides can be found [here](https://doc.qt.io/qt-6/qstyle.html). - -# Installing - -Here are detailed instructions on how to install Breeze Style Sheets for a variety of build systems and programming languages. This will require a Qt installation with QtCore, QtGui, QtWidgets, and QtSvg installed. - -## Configuring - -By default, BreezeStyleSheets comes with the `dark` and `light` themes pre-built in the [resources](/resources/) directory. In order to build all pre-packaged themes including PyQt5 and PyQt6 support, run: - -```bash -# choose only the frameworks you want -frameworks=("pyqt5" "pyqt6" "pyside2" "pyside6") -for framework in "${frameworks[@]}"; do - python configure.py --styles=all --extensions=all --qt-framework "${framework}" \ - --resource breeze.qrc --compiled-resource "breeze_${framework}.py" -done -``` +The required Qt resource compilers (RCC) for each framework are: +- PyQt5: `pyrcc5` +- PyQt6: `pyside6-rcc` (requires `PySide6` installed) +- PySide2: `pyside2-rcc` (requires Python.10 or earlier) +- PySide6: `pyside6-rcc` -All generated themes will be in the [dist](/dist) subdirectory, and the compiled Python resource(s) will be in `resources/breeze_{framework}.py` (for example, `resources/breeze_pyqt5.py`). Note that using the `--compiled-resource` flag requires the correct RCC to be installed for the Qt framework (see [PyQt5/6 & PySide2/6 Installation](#pyqt56--pyside26-installation) for the required RCC). +You can also use the pre-compiled resources in the [resources](/resources/) directory. -## CMake Installation +### CMake Installation Using CMake, you can download, configure, and compile the resources as part part of the build process. The following configurations are provided by @ruilvo. You can see a full example in [example](/example/cmake/). First, save the following as `breeze.cmake`. @@ -350,7 +207,7 @@ int main(int argc, char *argv[]) } ``` -## QMake Installation +### QMake Installation Copy the contents of the `dist` subdirectory into your project directory and add the qrc file to your project file. @@ -386,64 +243,215 @@ int main(int argc, char *argv[]) } ``` -## PyQt5/6 & PySide2/6 Installation +## Examples -To compile the stylesheet for use with PyQt5, PyQt6, PySide2 or PySide6, ensure you configure with the `--compiled-resource` flag (which requires the rcc executable for your chosen framework to be installed - see below for details). The compiled resource Python file now contains all the stylesheet data. To load and set the stylesheet in a PyQt5/6 or PySide2/6 application, import that file, load the contents using QFile and read the data. For example, to load BreezeDark, first configure using: +Many examples of widgets using [custom themes](/example/widgets.py), including with the [Advanced Docking System](/example/advanced-dock.py), [custom icons](/example/standard_icons.py), and [titlebars](/example/titlebar.py) can be found in the [example](/example/) directory. + +The support stylesheets include: +- `dark` +- `light` +- `auto` +- `native` (the system native theme) + +And any `-purple`, `-green`, etc. variants can also be used. `auto` will automatically detect if the system theme is light or dark and select the correct theme accordingly. The cross-platform way to detect the correct theme is using `get_theme` in either [Python](/example/breeze_theme.py) or [C++](/example/breeze_theme.hpp). Just include those stand-alone files in your project and you can select the desired theme based on the user's profile settings at startup. + +## Features + +- Complete stylesheet for all Qt widgets, including esoteric widgets like `QCalendarWidget`. +- Customizable, beautiful light and dark themes. +- Cross-platform icon packs for standard icons. +- Extensible stylesheets: add your own extensions or rules and automatically configure them using the same configuration syntax. + +### Extensions + +The supported extensions can be found in the [extensions](/extension/README.md) directory and include theme support for: +- [Advanced Docking System](/extension/README.md#advanced-docking-system) +- [QDockWidget Tooltips](/extension/README.md#qdockwidget-tooltips) +- [Complete Standard Icon Set](/extension/README.md#standard-icons) + +## Customization + +It's easy to design your own themes using `configure.py`. First, add the styles you want into [theme](/theme/), then run configure with a list of styles you want to include. + +### Theme + +Here is a sample theme, with the color descriptions annotated. Please note that although there are nearly 40 possibilities, for most applications, you should use less than 20, and ~10 different hues. + +```jsonc +// NOTE: This is a custom JSON file, where lines leading +// with `//` are removed. No other comments are valid. +{ + // Main foreground color. + "foreground": "#eff0f1", + // Lighter foreground color for selected items. + "foreground-light": "#ffffff", + // Main background color. + "background": "#31363b", + // Alternate background color for styles. + "background:alternate": "#31363b", + // Main color to highlight widgets, such as on hover events. + "highlight": "#3daee9", + // Color for selected widgets so hover events can change widget color. + "highlight:dark": "#2a79a3", + // Alternate highlight color for hovered widgets in QAbstractItemViews. + "highlight:alternate": "#369cd1", + // Main midtone color, such as for borders. + "midtone": "#76797c", + // Lighter color for midtones, such as for certain disabled widgets. + "midtone:light": "#b0b0b0", + // Darker midtone, such as for the background of QPushButton and QSlider. + "midtone:dark": "#626568", + // Lighter midtone for separator hover events. + "midtone:hover": "#8a8d8f", + // Color for checked widgets in QAbstractItemViews. + "view:checked": "#334e5e", + // Hover background color in QAbstractItemViews. + // This should be fairly transparent. + "view:hover": "rgba(61, 173, 232, 0.1)", + // Background for a horizontal QToolBar. + "toolbar:horizontal:background": "#31363b", + // Background for a vertical QToolBar. + "toolbar:vertical:background": "#31363b", + // Background color for the corner widget in a QAbstractItemView. + "view:corner": "#31363b", + // Border color between items in a QHeaderView. + "view:header:border": "#76797c", + // Background color for a QHeaderView. + "view:header": "#31363b", + // Border color Between items in a QAbstractItemView. + "view:border": "#31363b", + // Background for QAbstractItemViews. + "view:background": "#1d2023", + // Background for widgets with text input. + "text:background": "#1d2023", + // Background for the currently selected tab. + "tab:background:selected": "#31363b", + // Background for non-selected tabs. + "tab:background": "#2c3034", + // Color for the branch/arrow icons in a QTreeView. + "tree": "#afafaf", + // Color for the chunk of a QProgressBar, the active groove + // of a QSlider, and the border of a hovered QSlider handle. + "slider:foreground": "#3daee9", + // Background color for the handle of a QSlider. + "slider:handle:background": "#1d2023", + // Color for a disabled menubar/menu item. + "menu:disabled": "#76797c", + // Color for a checked/hovered QCheckBox or QRadioButton. + "checkbox:light": "#58d3ff", + // Color for a disabled or unchecked/unhovered QCheckBox or QRadioButton. + "checkbox:disabled": "#c8c9ca", + // Color for the handle of a scrollbar. Due to limitations of + // Qt stylesheets, any handle of a scrollbar must be treated + // like it's hovered. + "scrollbar:hover": "#3daee9", + // Background for a non-hovered scrollbar. + "scrollbar:background": "#1d2023", + // Background for a hovered scrollbar. + "scrollbar:background:hover": "#76797c", + // Default background for a QPushButton. + "button:background": "#31363b", + // Background for a pressed QPushButton. + "button:background:pressed": "#454a4f", + // Border for a non-hovered QPushButton. + "button:border": "#76797c", + // Background for a disabled QPushButton, or fallthrough + // for disabled QWidgets. + "button:disabled": "#454545", + // Color of a dock/tab close icon when hovered. + "close:hover": "#b37979", + // Color of a dock/tab close icon when pressed. + "close:pressed": "#b33e3e", + // Default background color for QDockWidget and title. + "dock:background": "#31363b", + // Color for the float icon for QDockWidgets. + "dock:float": "#a2a2a2", + // Background color for the QMessageBox critical icon. + "critical": "#80404a", + // Background color for the QMessageBox information icon. + "information": "#406880", + // Background color for the QMessageBox question icon. + "question": "#634d80", + // Background color for the QMessageBox warning icon. + "warning": "#99995C", + // These are extension-specific + // The background color for an Advanced Docking System Tab + "ads-tab:focused": "rgba(61, 173, 232, 0.1)", + "ads-border:focused": "rgba(61, 173, 232, 0.15)" +} +``` + +Once you've saved your custom theme, you can then build the stylesheet, icons, and resource file with: ```bash -python configure.py --compiled-resource breeze_resources.py +python configure.py --styles=dark,light, --resource custom.qrc ``` -Then load the stylesheet and run the application using: +Then, you can use `custom.qrc`, along with the generated icons and stylesheets in each folder, in place of `breeze.qrc` for any style. -```python -from PyQt5 import QtWidgets -from PyQt5.QtCore import QFile, QTextStream -# This must match the name of the file and be in the Python search path. -# To modify the search path, add the directory containing the file to `sys.path`. -import breeze_resources +The `--styles` command flag takes a comma-separated list of values, or `all`, which will configure every theme present in the [themes](/theme) directory. +#### Generating Colors -def main(): - app = QtWidgets.QApplication(sys.argv) +As a reference point, see the pre-generated [themes](/theme). In general, to create a good theme, modify only the highlight colors (blues, greens, purples) to a new color, such that the saturation and lightness stay the same (only the hue changes). For example, the color `rgba(51, 164, 223, 0.5)` becomes `rgba(164, 51, 223, 0.5)`. - # set stylesheet - file = QFile(":/dark/stylesheet.qss") - file.open(QFile.ReadOnly | QFile.Text) - stream = QTextStream(file) - app.setStyleSheet(stream.readAll()) +#### Adding Extensions - # code goes here +We also allow customizable extensions to extend the default stylesheets with additional style rules, using the colors defined in your theme. This also enables the integration of third-party Qt extensions/widgets into the generated stylesheets. - app.exec_() +For example, to configure with extensions for the [Advanced Docking System](https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System), run: + +```bash +python configure.py --extensions=advanced-docking-system --resource custom.qrc ``` -Required rcc for each framework: -- PyQt5: `pyrcc5` -- PyQt6: `pyside6-rcc` (requires `PySide6` installed) -- PySide2: `pyside2-rcc` (requires Python.10 or earlier) -- PySide6: `pyside6-rcc` +Like with styles, `--extensions` takes a comma-separated list of values, or `all`, which will add every extension present in the [extensions](/extension) directory. For a detailed introduction to creating your own extensions, see the extensions [tutorial](/extension/README.md). -You can also use the pre-compiled resources in the [resources](/resources/) directory. +## Extending Stylesheets -# Debugging +There are some limitations of using Qt stylesheets in general, which cannot be solved by stylesheets. To get more fine-grained style control, you should subclass `QCommonStyle`: -Have an issue with the styles? Here's a few suggestions, prior to filing a bug report: +```c++ +class ApplicationStyle: public QCommonStyle +{ + ... +} +``` + +The limitations of stylesheets include: + +- Using custom standard icons. +- Scaling icons with the theme size. +- QToolButton cannot control the icon size without also affecting the arrow size. +- Close and dock float icon sizes scale poorly with font size. +For an example of using QCommonStyle to override standard icons in a PyQt application, see [standard_icons.py](/example/standard_icons.py). An extensive reference can be found [here](https://doc.qt.io/qt-6/style-reference.html). A reference of QStyle, and the default styles Qt provides can be found [here](https://doc.qt.io/qt-6/qstyle.html). + +## Debugging + +Have an issue with the styles? Here's a few suggestions, prior to filing a bug report: - Modified the application font? Make sure you do **before** setting the application stylesheet. - Modified the application style? Make sure you do **after** you creating a `QApplication instance` but **before** you show the window or add widgets. -# Development Guide +## Development Guide + +### Git Hooks + +Contributors to BreezeStylesheets should make use of [vcs](/vcs.py) and [scripts](/scripts/) to both install Git hooks and run local tests and typechecking. After cloning the repository, developers should first install a pre-commit hook, to ensure their code is formatted and linted prior to commiting: + +```bash +python vcs.py --install-hooks +``` -## Configuring +### Configuring Styles -To configure the assets and the stylesheets, run `python configure.py`. To compile the assets and stylesheets for PyQt5, ensure `pyrcc5` is installed (for other frameworks, see [PyQt5/6 & PySide2/6 Installation](#pyqt56--pyside26-installation) for the correct RCC) and run: +To configure the assets and the stylesheets, run `python configure.py`. To compile the assets and stylesheets for PyQt5, ensure `pyrcc5` is installed (for other frameworks, see [Python Installation](#python-installation) for the correct RCC) and run: ```bash python configure.py --compiled-resource breeze_resources.py ``` -## Testing +### Testing The unittest suite is [ui.py](test/ui.py). By default, the suite runs every test, so to test changes to a specific widget, pass the `--widget $widget` flag. To test other configurations, see the options for `--stylesheet`, `--widget`, `--font-size`, and `--font-family`, and then run the tests with the complete UI in [widgets.py](/example/widgets.py). If the widget you fixed the style for does not exist in the test suite or [widgets.py](/example/widgets.py), please add it. @@ -494,7 +502,28 @@ yes_button To see the complete list of Qt widgets covered by the unittests, see [Test Coverage](Test%20Coverage.md). -## Distribution Files +### Linting and Type Checks + +You can check code quality using static typecheckers and code linters. + +```bash +# format python code to a standard style. +# requires `black` and `isort` to be installed. +scripts/fmt.sh +# run linters and static typecheckers +# requires `pylint`, `pyright`, and `flake8` to be installed +scripts/lint.sh +# check if the system can automatically determine the theme +# on windows, this requires `winrt-Windows.UI.ViewManagement` +# and `winrt-Windows.UI` to be installed. +scripts/theme.sh +# run more involved, comprehensive tests. these assume a Linux +# environment and detail the install scripts to use them. +scripts/cmake.sh +scripts/headless.sh +``` + +### Distribution Files When pushing changes, only the `light` and `dark` themes should be configured, without any extensions. To reset the built resource files to the defaults (this requires the correct RCC to be installed), run: @@ -515,7 +544,7 @@ To turn back on tracking, run: python vcs.py --track-dist ``` -## Git Ignore +### Git Ignore Note that the `.gitignore` is auto-generated via `vcs.py`, and the scripts to track or untrack distribution files turn off `.gitignore` tracking. Any changes should be made in `vcs.py`, and ensure that `.gitignore` is tracked, and commit any changes: @@ -525,81 +554,40 @@ git add .gitignore git commit -m "..." ``` -# What's changed in this fork? - -* Added support for PySide2 and PySide6. - -* Removed old PyQt6 packaging system and replaced with an identical process for the four most common Python Qt frameworks. - * This is achieved by using PySide6-rcc. New function was added to change import from 'PySide6' to 'PyQt6' when building for PyQt6. +## What's changed in this fork? - ```bash - python configure.py --compiled-resource=breeze_resources.py --qt-framework=pyqt6 - ``` - -* Error message added if required rcc executable is not found. - -* Altered '--no-qrc' option. Compiled resources will now still build if this option is selected as long as a qrc file already exists. - * Error message presented if no qrc file is present and '--no-qrc' option is selected. - -* Removed 'qrc dist' as no longer needed to separate PyQt6 files. All files now built in 'dist'. - -* Changed function spacing to align with PEP-8 (two new lines between functions). - -# Known Issues and Workarounds - -For known issues and workarounds, see [issues](/ISSUES.md). - -# Developing - -Contributors to BreezeStylesheets should make use of [vcs](/vcs.py) and [scripts](/scripts/) to both install Git hooks and run local tests and typechecking. After cloning the repository, developers should first install a pre-commit hook, to ensure their code is formatted and linted prior to commiting: +- Added support for PySide2 and PySide6. +- Removed old PyQt6 packaging system and replaced with an identical process for the four most common Python Qt frameworks. + - This is achieved by using PySide6-rcc. New function was added to change import from 'PySide6' to 'PyQt6' when building for PyQt6. +- Error message added if required rcc executable is not found. +- Altered '--no-qrc' option. Compiled resources will now still build if this option is selected as long as a qrc file already exists. + - Error message presented if no qrc file is present and '--no-qrc' option is selected. +- Removed 'qrc dist' as no longer needed to separate PyQt6 files. All files now built in 'dist'. +- Changed function spacing to align with PEP-8 (two new lines between functions). ```bash -python vcs.py --install-hooks +python configure.py --compiled-resource=breeze_resources.py --qt-framework=pyqt6 ``` -You can also manually run each check independently: +## Known Issues and Workarounds -```bash -# format python code to a standard style. -# requires `black` and `isort` to be installed. -scripts/fmt.sh -# run linters and static typecheckers -# requires `pylint`, `pyright`, and `flake8` to be installed -scripts/lint.sh -# check if the system can automatically determine the theme -# on windows, this requires `winrt-Windows.UI.ViewManagement` -# and `winrt-Windows.UI` to be installed. -scripts/theme.sh -# run more involved, comprehensive tests. these assume a Linux -# environment and detail the install scripts to use them. -scripts/cmake.sh -scripts/headless.sh -``` - -You should also stop tracking changes to generated files from source control until desired. This avoids large commits for minor changes that are reverted later. - -```bash -# don't track changes to generated file, like in dist -python vcs.py --no-track-dist -# retrack changes to these files. -python vcs.py --track-dist -``` +For known issues and workarounds, see [issues](/ISSUES.md). -# License +## License MIT, see [license](/LICENSE.md). -# Contributing +## Contributing Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in BreezeStyleSheets by you shall be licensed under the MIT license without any additional terms or conditions. -# Acknowledgements +## Acknowledgements BreezeStyleSheets is a fork of [QDarkStyleSheet](https://github.com/ColinDuquesnoy/QDarkStyleSheet). Some of the icons are modified from [Material UI](https://github.com/google/material-design-icons) and [Material Design Icons](https://materialdesignicons.com/) (both of which use an Apache 2.0 [license](/MaterialUi.LICENSE)), and are redistributed under the MIT license. PyQtBreezeStyleSheets is a further fork of [BreezeStyleSheets](https://github.com/Alexhuszagh/BreezeStyleSheets). -# Contact +## Contact -Email: ahuszagh@gmail.com +Email: [ahuszagh@gmail.com](mailto:ahuszagh@gmail.com) Twitter: KardOnIce From 00feeda9059f6e5727554f8f71db111cca707f9f Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sun, 8 Sep 2024 08:35:43 -0500 Subject: [PATCH 3/7] Use 2 spaces for markdown. --- .editorconfig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.editorconfig b/.editorconfig index fbbc827..1038f84 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,9 @@ trim_trailing_whitespace = true [*.{cpp,hpp,sh,in,svg,qss,txt,cmake,py,json}] charset = utf-8 +[*.md] +indent_size = 2 + [{package.json,.github/workflows/*.yml}] indent_style = space indent_size = 2 From 8460d78d5cc683cb0cf17d394e00bd010b3a1b40 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sun, 8 Sep 2024 09:11:54 -0500 Subject: [PATCH 4/7] Add a changelog. --- CHANGELOG.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 31 ++++++++------------------ 2 files changed, 72 insertions(+), 22 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9786864 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,63 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog], +and this project adheres to [Semantic Versioning]. + +## [Unreleased] + +### Added + +- Detection of system theme (light or dark mode). +- Support for PySide2 and PySide6 frameworks (from [Inverted-E]). +- Support for flat groupboxes. +- Advanced Docking System styling. +- Examples for title bars, standard icon overrides, LCD displays, and more. +- Configurable stylesheets via themes. +- Custom extension support, such as the advanced docking system. +- Compile Qt resource files (from [chaosink]). + +### Changed + +- Stylesheets to match KDE-like Breeze and Breeze dark themes. +- Icons to match KDE-like Breeze and Breeze dark themes. + +### Deprecated + +### Removed + +- Old PyQt6 packaging system to match the standard Qt5 and Qt6 approach using resource compilers (from [Inverted-E]). +- The `--no-qrc` flag when configuring stylesheets due to the new RCC system (from [Inverted-E]). +- The QRC dist files due to the new RCC system (from [Inverted-E]). + +### Fixed + +- Documentation for CMake installation. +- QTableWidget::indicator size to match other checkboxes (from [Inverted-E]). +- Menu bar hover styling. +- Qt6 support. +- Branch indicators for QTreeView and QTreeWidget (from [eblade]). + + + + + + + +[Keep A Changelog]: https://keepachangelog.com/en/1.0.0/ +[Semantic Versioning]: https://semver.org/spec/v2.0.0.html + + +[Unreleased]: https://github.com/Author/Repository/compare/v0.0.2...HEAD + + + +[Inverted-E]: https://github.com/Inverted-E/ +[eblade]: https://github.com/eblade/ +[chaosink]: https://github.com/chaosink/ diff --git a/README.md b/README.md index 44bb212..4fbebb9 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,11 @@ BreezeStyleSheets is a set of beautiful light and dark stylesheets that render c - [Linting and Type Checks](#linting-and-type-checks) - [Distribution Files](#distribution-files) - [Git Ignore](#git-ignore) -9. [What's changed in this fork?](#whats-changed-in-this-fork) -10. [Known Issues and Workarounds](#known-issues-and-workarounds) -11. [License](#license) -12. [Contributing](#contributing) -13. [Acknowledgements](#acknowledgements) -14. [Contact](#contact) +9. [Known Issues and Workarounds](#known-issues-and-workarounds) +10. [License](#license) +11. [Contributing](#contributing) +12. [Acknowledgements](#acknowledgements) +13. [Contact](#contact) ## Gallery @@ -554,21 +553,6 @@ git add .gitignore git commit -m "..." ``` -## What's changed in this fork? - -- Added support for PySide2 and PySide6. -- Removed old PyQt6 packaging system and replaced with an identical process for the four most common Python Qt frameworks. - - This is achieved by using PySide6-rcc. New function was added to change import from 'PySide6' to 'PyQt6' when building for PyQt6. -- Error message added if required rcc executable is not found. -- Altered '--no-qrc' option. Compiled resources will now still build if this option is selected as long as a qrc file already exists. - - Error message presented if no qrc file is present and '--no-qrc' option is selected. -- Removed 'qrc dist' as no longer needed to separate PyQt6 files. All files now built in 'dist'. -- Changed function spacing to align with PEP-8 (two new lines between functions). - -```bash -python configure.py --compiled-resource=breeze_resources.py --qt-framework=pyqt6 -``` - ## Known Issues and Workarounds For known issues and workarounds, see [issues](/ISSUES.md). @@ -579,7 +563,7 @@ MIT, see [license](/LICENSE.md). ## Contributing -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in BreezeStyleSheets by you shall be licensed under the MIT license without any additional terms or conditions. +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in BreezeStyleSheets by you shall be licensed under the MIT license without any additional terms or conditions. See the [changelog](/CHANGELOG.md) for changes and contributors to the project. ## Acknowledgements @@ -587,6 +571,9 @@ BreezeStyleSheets is a fork of [QDarkStyleSheet](https://github.com/ColinDuquesn PyQtBreezeStyleSheets is a further fork of [BreezeStyleSheets](https://github.com/Alexhuszagh/BreezeStyleSheets). +Major contributions to the project have made by: +- [Inverted-E](https://github.com/Inverted-E/) + ## Contact Email: [ahuszagh@gmail.com](mailto:ahuszagh@gmail.com) From e2330df40dc5fbf112275511bdf48a74c9400cee Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sun, 8 Sep 2024 12:37:09 -0500 Subject: [PATCH 5/7] Add documentation for examples and additional enhancements. - Document the titlebar, custom styles, and other examples. - Document custom widget overrides like sliders, dials, and LCD numbers. - Convert the Qt5 refs to Qt6. - Add in better links to examples in the README. - Add a changelog. - Make our custom widgets, like titlebars and sliders, to use a general-purpose class and then reusable components. - Ensure we use a shallow clone for our CMake builds. --- .github/workflows/lint.yml | 152 ++++---- CHANGELOG.md | 2 + README.md | 3 +- assets/advanced_docking_system_example.png | Bin 0 -> 45538 bytes assets/custom-standard-icons.png | Bin 0 -> 25917 bytes assets/custom_placeholder_text.png | Bin 0 -> 4085 bytes assets/custom_slider.png | Bin 0 -> 4703 bytes assets/custom_url.png | Bin 0 -> 5057 bytes assets/default-standard-icons.png | Bin 0 -> 37262 bytes assets/gallery.md | 68 ++-- assets/mismatched_titlebar.png | Bin 0 -> 5940 bytes example/README.md | 327 ++++++++++++++++++ example/branchless/README.md | 31 -- .../branchless/{application.py => main.py} | 5 +- example/cmake/breeze.cmake | 1 + .../system_theme.hpp} | 2 +- .../system_theme.py} | 2 +- example/{ => dial}/dial.py | 54 +-- example/dial/main.py | 58 ++++ example/{standard_icons.py => icons/main.py} | 149 ++------ example/icons/standard.py | 87 +++++ example/{ => lcd}/lcd.py | 78 +---- example/lcd/main.py | 74 ++++ example/placeholder_text.py | 1 + example/shared.py | 42 ++- example/slider/main.py | 55 +++ example/{ => slider}/slider.py | 49 +-- example/titlebar/main.py | 140 ++++++++ example/{ => titlebar}/titlebar.py | 56 +-- example/url.py | 1 + example/whatsthis.py | 1 + extension/README.md | 39 +-- scripts/cmake.sh | 17 +- scripts/lint.sh | 4 +- scripts/test_theme.cpp | 2 +- scripts/theme.sh | 12 +- setup.cfg | 4 +- 37 files changed, 992 insertions(+), 524 deletions(-) create mode 100644 assets/advanced_docking_system_example.png create mode 100644 assets/custom-standard-icons.png create mode 100644 assets/custom_placeholder_text.png create mode 100644 assets/custom_slider.png create mode 100644 assets/custom_url.png create mode 100644 assets/default-standard-icons.png create mode 100644 assets/mismatched_titlebar.png create mode 100644 example/README.md delete mode 100644 example/branchless/README.md rename example/branchless/{application.py => main.py} (96%) rename example/{breeze_theme.hpp => detect/system_theme.hpp} (99%) rename example/{breeze_theme.py => detect/system_theme.py} (99%) rename example/{ => dial}/dial.py (88%) create mode 100644 example/dial/main.py rename example/{standard_icons.py => icons/main.py} (55%) create mode 100644 example/icons/standard.py rename example/{ => lcd}/lcd.py (54%) create mode 100644 example/lcd/main.py create mode 100644 example/slider/main.py rename example/{ => slider}/slider.py (73%) create mode 100644 example/titlebar/main.py rename example/{ => titlebar}/titlebar.py (97%) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e1b52c8..6b34dc6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,76 +1,76 @@ -name: Linters - -on: [push, pull_request] - -jobs: - lint-version-python: - strategy: - matrix: - os: [ubuntu-latest] - python-version: ["3.11", "3.12"] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pylint flake8 pyright - - name: Analysing the code with pylint - run: | - scripts/lint.sh - - lint-os-python: - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.10"] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pylint flake8 pyright - - name: Analysing the code with pylint - run: | - scripts/lint.sh - - lint-cpp: - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install clang-tidy - - name: Analysing the code with clang-tidy - shell: bash - run: | - set -eux pipefail - - # Windows oddly requires C++20 support due to internal bugs. - if [[ "${RUNNER_OS}" == "Windows" ]]; then - extra_args="-extra-arg=-std=c++20" - passthrough="" - elif [[ "${RUNNER_OS}" == "macOS" ]]; then - # NOTE: The search paths aren't added by default, and we need C then C++ by default - # for our search. This makes the process easier. - extra_args="-extra-arg=-std=c++17 -extra-arg=--stdlib=libc++" - location="$(xcrun --show-sdk-path)" - passthrough="-I${location}/usr/include/c++/v1 -I${location}/usr/include" - else - extra_args="-extra-arg=-std=c++17" - passthrough="" - fi - clang-tidy -checks=-*,clang-analyzer-*,-clang-analyzer-cplusplus* ${extra_args} example/breeze_theme.hpp -- ${passthrough} +name: Linters + +on: [push, pull_request] + +jobs: + lint-version-python: + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.11", "3.12"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint flake8 pyright + - name: Analysing the code with pylint + run: | + scripts/lint.sh + + lint-os-python: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint flake8 pyright + - name: Analysing the code with pylint + run: | + scripts/lint.sh + + lint-cpp: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install clang-tidy + - name: Analysing the code with clang-tidy + shell: bash + run: | + set -eux pipefail + + # Windows oddly requires C++20 support due to internal bugs. + if [[ "${RUNNER_OS}" == "Windows" ]]; then + extra_args="-extra-arg=-std=c++20" + passthrough="" + elif [[ "${RUNNER_OS}" == "macOS" ]]; then + # NOTE: The search paths aren't added by default, and we need C then C++ by default + # for our search. This makes the process easier. + extra_args="-extra-arg=-std=c++17 -extra-arg=--stdlib=libc++" + location="$(xcrun --show-sdk-path)" + passthrough="-I${location}/usr/include/c++/v1 -I${location}/usr/include" + else + extra_args="-extra-arg=-std=c++17" + passthrough="" + fi + clang-tidy -checks=-*,clang-analyzer-*,-clang-analyzer-cplusplus* ${extra_args} example/detect/system_theme.hpp -- ${passthrough} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9786864..4166622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning]. - Configurable stylesheets via themes. - Custom extension support, such as the advanced docking system. - Compile Qt resource files (from [chaosink]). +- Documented support for CMake builds (from [ruilvo]). ### Changed @@ -61,3 +62,4 @@ and this project adheres to [Semantic Versioning]. [Inverted-E]: https://github.com/Inverted-E/ [eblade]: https://github.com/eblade/ [chaosink]: https://github.com/chaosink/ +[ruilvo]: https://github.com/ruilvo/ diff --git a/README.md b/README.md index 4fbebb9..7015ea0 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ FetchContent_Declare( GIT_REPOSITORY https://github.com/Alexhuszagh/BreezeStyleSheets.git GIT_TAG origin/main GIT_PROGRESS ON + GIT_SHALLOW 1 USES_TERMINAL_DOWNLOAD TRUE) FetchContent_GetProperties(breeze_stylesheets) @@ -252,7 +253,7 @@ The support stylesheets include: - `auto` - `native` (the system native theme) -And any `-purple`, `-green`, etc. variants can also be used. `auto` will automatically detect if the system theme is light or dark and select the correct theme accordingly. The cross-platform way to detect the correct theme is using `get_theme` in either [Python](/example/breeze_theme.py) or [C++](/example/breeze_theme.hpp). Just include those stand-alone files in your project and you can select the desired theme based on the user's profile settings at startup. +And any `-purple`, `-green`, etc. variants can also be used. `auto` will automatically detect if the system theme is light or dark and select the correct theme accordingly. A recipe for a cross-platform way to detect the correct theme in Python or C++ is shown in our [System Theme Detection](/example/README.md#system-theme-detection). ## Features diff --git a/assets/advanced_docking_system_example.png b/assets/advanced_docking_system_example.png new file mode 100644 index 0000000000000000000000000000000000000000..627242abbda6536e5b1bca6d3fe54899124997b8 GIT binary patch literal 45538 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

9_)(yniW@#o*~#6YZy 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 Ykx6Qr9(&m<5co4$xz0H8+bxIw2W8F7ivR!s literal 0 HcmV?d00001 diff --git a/assets/custom-standard-icons.png b/assets/custom-standard-icons.png new file mode 100644 index 0000000000000000000000000000000000000000..2e0891761b99eb57d04c0aa7ab22fcd19eb03654 GIT binary patch literal 25917 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 literal 0 HcmV?d00001 diff --git a/assets/custom_slider.png b/assets/custom_slider.png new file mode 100644 index 0000000000000000000000000000000000000000..e1ca17b4838f2c69567c2b20a492bede9bee401b GIT binary patch literal 4703 zcmcgw2~<*P`@iZ}mVV|;rcLECS&c1jxuhj#Wn_M(wkd)Onu?YS;-0xwW{ZwGPMMOX zqotOBrih}sHEx->WneB~O0I;6B!~k3uQqkg%$Yg=Z@&L>&*8v*-{*av-?RSiKIVmsX7JT{KNaG%~1d-O_3M5 zuL1AZ2cCon1HgvHm5)>_`l%^du9?Sp%9aa)ZaWe6yQc{*3rd) zI}NR5Cevbn=R{#jQE)adi|J8$NJtB_9Ty+#?S$|sYvfhRD=Wjp!oo7K*nU-dfsIF8 zoJ@S~_1s)i{0ONr{^j!km+uhaBm13Cc0k=De0<#Ir)P?bd0qZUa$wIv_ny&Ty9{sy z0s)8foqCgh>VY<#9O(Y(0T#R-1tzNKOgj$r^jB%hg+eFh;uFWyOR2B$Cvju#>9~D{ zhK9sTXznN!=O8GaVu|4>5{U%$Fb*(T*-=r+C*|>K;^im0%y<;T7uD5K8toJc#qUH# z1UD~~wHAb6s#mP-Qe6(Yi~=u036XcBvzRwQs;tz5mp#FN!W0U^)yk^1h)U-<;2lS_ zZmkF!lA(zz3LECTSTi#Lsu|C{CUolEpc5CGut`f9AnyJfYc{)@8o^P~+*CbrqeV0? zL&A+m$G7H)UV9*QMg5H_ojDfum{pj)F{~fD@LD;C<{oP-d+yCXG?!LUQPDzQv0d<7 z$EiXU5;5C-v-u_Y8Rm^ynT%|}G@I@3dnuVPf%c5qb3cvoc&rlZ(#p9Xi;xyWRMcZhtS9tGah@fOx$cYX& zuUi(>f7&OoSiCN)Q6)4)MvcUCYUbty$SCfxB^IuDm@ByF>G1Z%CTr&Fo|uH@_=q=D zF4>vi`(e9r9M+y7yaakMR*#BS>tdJJ35EKzbji|J}a9FFRA-Pq%2Z+NU^9dMOBYD+GZm%fYO{pLX6xXQNqHOqFOi1|lIsHfogCkctk0h20y) zZ5_qG#yg5q)vi+`J(_8RZCRB8^droHn`y%x1A!B@9s&kQC~1U=dN={mlZ{YR?I-bc zII(WtV)&n6U_-#L4&pZMbU0wM#j{R8yRMk*TJRF~2Ny znG!_1y?@gU_KqkYERsdXbb^gh7{+a-?exvM zU%%iO3?YUkAsOTY?p`RDu;EtS*!PcNVlKyT=_7h(;a4^~)&!Rdw-9nuezo?ZHQl(Y z=hd%k-*kWTs3G29mxG$NM60{pG>D>jfo`6#cXW^KogH2-YI?*X*@NRUEw3lK$#eYW zbw6aQ=D0y(*lp=r-~fVGE!@NCE_dr2u#yJ_M#VOcSh&;S`Kz$l#mW-lkXoOGDAy$= z2HYM?uM_YU6_AlEf(bH~O}Zzh;|F-fd4&lXbH}nA)b(I$ex2z-N|uGrL$#5!WLTDi z3d8ItIQKcmmu;A|Ln)^QEWD|z1rN}koCqv6tam-PgQG4RFD6g^H-bEMp<x%2)(eIZ@)XA@NRT*-1^CywKNiUTxDLV>s2;Y-x}4dX_|MS$Uf9Qre4cI*-m*sHNNb^MNCkr{^5~1 zH+&6R)dHoQ7Oq!+1Y@fl)qz#sJen2pbBAJ3jVwOfvWo0+B{wp=bRp1cA8w%no~3H_ zTG54pP)~=00Ev@qYxE(>;63$>nZbJ}P_?nk`(t`u3_S{`Zxb8!7L;yJVN*lPg35s$FYTHG63;^X%U*Wf* zYf8g50oPQNv7zr9t`D3A0L0F(vN3Daf$yQIa7MQj;MUT%a=dXxW!&#o{)v_U<0U9c zK~eE-vNX`0H!||_ido6v1v5F|NMZtIF)8lFWh*JbC-p1FO;{3|WB;4S+OfUgfW9bW z$D%!-X|_|nqS)zcqd zzyV;-*PUZ1Dd5RZ{|`&IR!pv40rMv&L3|^*l2v>cJZYh3x;V8pz%NiJN^S9d=O3Jg zqS&<~3BSocsK2xh&}Sx4=ITHn3~m3nz1eNx8xm5!T9v|pFVXxH2mYHSO0;=&+zysi zrE_icQs|4~;o9)N>}{6LcW@Z}fCQz>;ZnbZ8pw~A&D6at?SMUSF}};)#&5o4?%Sl& zaQbJszPCb>-@7VbJNVgcCrm<1rhPf4oLc$xavjI-Mvpls#bM58eN))n*0^KaWku^%nCocYfRzvo6?R`jLP!UIObiY(egPF*@K-6WT<1;1b5Fiv)WPBAKsKA&e~|& z>T--jxQhca()tP@l7}FYIq&#%=R*kjXG)0J^77G{7(4k@ln>{^878h_S9Ed ziIePjbyNFdNw*7Ex~I0N#KBf;*9=1xBojDL9ki5uCm$g??c$52n*CDh=1|2SA7qB+ z)-r1l2Or(%Cy#ZM7yyOD-GocAcDQ`XGzewj6eRF6yM`;n%{x;oJN2oFo{78&*am}% z@?CEP)$wxRE;%Z>j$J?hGz$9EtJmm)o-W{5%U?uD+nhax9Sdi;SL{K4d%43%ftn}} zFik$?Xp{#&UjCy5U)ywuRBn{sK^g@n;QL4>h(fDo14ai{u~;KbjfW zOLG*|+$y-YHUNgK`?a~2v-~IexQMIgu5B@rLE9ZNdi!Qu+veqWos8kR6n2B(dyPRU zANpyDhnZRN7O+tm`H_ojnbhqIg^#6dt2Bu*;)+WW2}EB$$-mA(Qt~d}Bhy!G1O;=k z^d(ndrushNw+w+L_2kX2^knMB>6+>T=hn_88!(U}sYPWx2UT zFE-LNrm;@pab`7ao)oTK!l*LRZK9ffXVSbLgUEtF%If&v1E-x*;e;BrihgFOKp{Dx zt_c6sx@kS;;c%;T1{^HNx;uo_`NvMhj5={Omle*@>D~KT$raApYybR!!MXr(jL`&Ri$lPI#W1g>p?c|H(yNCO#|e=j0`AqA50jvYI`us4^>{W8C^ z&9DA}_7;>QR_XqVr7UP(>no}Mh35Y#Z~uv|O;}@7M~lyFU5y&_VebY2oxKJunp4;+ zqicTUE075KI@`a5TvD(dX!voTLHiPzJ&Ng9slS=WFWj7(n))Sn3kwmyf$-%pYQF^6 z{3<`hmMjHayTqDib2vzIv|xt*h>r8Elmhhq>9|E$SV=}-Z*Olu-RXq_DCR`%i)G9u zU-8buoPfb#-dzs|Iso9bB@8`0KoWDrV?rK-mo3PO=J&hM%}Ivf1KKTPvAF#~E$3qe zF@cf*zPDRln&&IfI%NR2gzCt|mg$+P%f4pddxngR%#wg(5i$E-Eqn+gOoN4omGHwA zrGSE`xXSvg_DD2xxM83!zkOiG^J9ae$5>MEL}%k|#o@3)f0#3-StR6jnG1&Gb;)jH zuhU%kyunhBRzf9Xt97;iU`5Mz@NqMI+Bn`gp6a_O7o9IL7Pr#+^aG`3 zh_r#JDT{296=BMWN)3Hk=EZTEWmF&!Ep6@!koP8aSg@9*9Q*VD zx;h%B0Kk-Zgy&g~9Z5Z@t@@*h31zCS4pjDB{B@+9aJg@A9{}FJX5W9pe5AAfrDKT# zfYVKX52iNH5~rg@Zg0)U-j6(;z5Ske!2oS97z~MW_4KwiXE}1=xudIb-^|~3IRgzbMef9J$BICoJbf7G{@bufj_5JDTJliXu6H9S2G>M z40?^EDqJy0ve!Fqu4(<{nnp@lVG5hrFa2|+c+>J$Y5Uygx9oOv6kVKqvQV?*Bxw}= zV7S-lwS`|#mf~FZQdXPG>`Hs@9n#9DMdZhyk>t$Vo)h)KZf*-dqeN7F7LtR`mt9o! znx5`R@+~OOy7M%@jgLk2csmjCT|?W^(J}uTsPt9@Q2{yn(XlNa3?;_Cem&to6*noZ z&SKg0@}-8h7$#fEFFror%6x{F82n*85JVIU=Xj!FAXbuz{PZisz4ARR(I@rX2qXNg zY%-TYy8Q4<FMeA!NFuOOZ8SNRQv-1_hJy= zc2->{i3>*Prw-fgwyNz9JTp01%U)&0z1yLJCLaEY;kOeO5+X!#sp~lE;t6=X(}%FE z0LD^r4x{iiE{8#Fjk~b?B>as$xjuL=UP#GT`Q^DzDJc9BX|=wo)_1x3f??tB`lhI5 zttegmRTqa3VRcWpvFM!-ht>fHOLK=y^mU;KJZjj+Z-i5wDkfVhe(hR_LOT@;g?7`giOKD@CtQ9<%8%F@m=#?k*LX6H z(ra<0*!DC1O#RdO!7jSmVSj>jFeBS9^K5gXYBuZh=2IwvHLT^DcIImMR_sVuA`OZ96((R;0m6zZJqosCTNW`Cng z!yUhM5^3(h81M5dC=VVNR_nJr)f#A%q9#&Fi(Cm|$vPSWiTjl9 zZeK-_8_DER`D*xr{=?|O_uof$+p*MaVqyF*i+(F;5r1^)Wk=$7%gjn**|wgYvyN{L zN-TU+m{bs{2jwO;X+Hj}q;Kj@kl$#QYuN05)DX0=#z3#nKj#m*O`-0H%&ZdY=@YB0 zSi6p{b15+?wU_8&N=+E!9&LwBQQq$6o+S6H4J)Q2g9$mLGAB=MT4FEvhp?cYs0B^8 zPql>w`}Y+2<-YD09tFQF>$M{}#N=9Wz>jUm)oLvPOt=?ni_2_EvIiI{Wj(;yN;>`;c5tKJTOT@O$|GgemR)3=u zq#z>)37Hj4q;%DhRm3u9Q7bGvKgcN3O6BP*UOMBsOI!G>PeD5F0n9ggrE`K5$bp}p zPWgbB-`>|N4BoI*earM=e6iK~P2FixmDz9a9ibnPgbRkq`kmRW!&7G`f*7Ml=v{Q0 zTHU5eXPbykDrQJKO`7ost+ul?Ac9`+Xq&7!qx$g*{xKr@v=dQ(R8cb16sdqw>F zR%E?&X{H(;FW=!evOLvBevXt+h`0hTzgSzlFQ_lrjms8D33|g;RMh+^_Ituwn%T|F zonJaiJ0|k<4ADGrKaPWvlPO$zfEw5f{DT_cqXkZux-@Hxde(0(qdX7CF3gSh=-2<= ztU-uympg1a-DVLjgwb5xJv<&dnaP=s2vc>t%Q}%qRjn3JfegsJc9bNrh;G%i(!|2w z7``B~h<1kkkHUdKSSIG20*skx#{gI^7%)Ehe+snnu`Ml{xO0HL>rwr!t6NG@PwMWG zdZ${4mt9VyM`&F*b)JnIA*Afz2UbD4#9eV%@NK!p%9Gi=nOaX+($% z{Z2$j&~{PMHQ!Y`gSJ(pkoS=&tqu*LLgGT48v+jtdPTIqd4+b@d;X0Dja3E#Lp_`; za3H6v*&~LO*$|Dp+7PSDXOw@39_>Cwq%)@(B8E08p#<|9B|P(Djs}#0*1^-CPIt)g zrYkn**vwLUOWyj=@b|;Ovt+H`Cur5u-wn5}+&a1TIGyMi6F0h4z1jmJ_kSvu=Yf~R?(79=LIsg8DQ+b2-BYQsWUgV1_CXh-6#Ffre_xZ6 zgO$;rs=ajRTY%L}|1#d(jzrEy5^*^~V@}eAW2wu}!9`E+mWoB;SIVzHe3`8u1}_Ve z(mHX_z3CQ7TbL?&I91NRUgHXOS1amPr)ON*y|7WAq>KLzd< zi6Sumw@VleV>zA)nMxRsi*jpWB)1~tmWMaNwWv~CJH-)?*S^bLC7=5I(s9`Oc!@n9 zf2`&3xoknoACQBmN23YfhMMsYB_k+BJ#_q68nJFC$pGxP-fSUeo>ZfJTViV(lzZu| zl{&UQh|M4wq(R_)3hOIXn>{*ecafzFgoDEOQ;zFA^P?h79#{%#qm=-azBfdNnKecG zGINK;;x-76AqUzPGq?zPW#jizPOcLBrRt}SGHlb&%y!B);9&831%Jz}w*kdrzO>FR z&Y!lSE3SsnMy&E31mWGM6g+pheXDWz5E~+Z|B4rN{P(U>8Ffz86@mFEex76m+=c1j zhgOgZNpkCN&Wi>;>?#?yxcaWR3_tU(^cSUkig(-3j3T_TUYEAesPq;z(wcb#Oo07N z=i1do(PVj=ZoGCcpB*icCv!^#$_(a2$@I(M?GUV%cU&-w55(Mj z&<(o|VVaMP#KVfb16m=hSD)RVT20(-bkw;inlv6W((juww4i^|4)%n84>h?)%82*% zLD~kLQ-GcJHY_|WaKnK&7FpVI?%@AmM^GG6rE2GE6D3%!; z+)i%vO*h1z3RMjy3g(U9b_e=6UCWueO+nkoSb)G&h%K%?!FR*GKwKDcSEmx3#oTwv zl^sZaP6X}={J#d2TB5THzmbToRrc6a%5mV~mWp*(W&}3aF`u8M@{fLw z>8h=<8Q9S67>#{Ig6*s@$~BdqK9lO^XbDSbQ}LfpTpXeiN^aVAM2LhG)&Fo>SL*)!PJq8!ui4A!M+HU;LAX>3M>kTtsqcoZ{# zsB>y+>TU&iE#9T1zf;Hik#C$dVd*EuQkQqNSS*685w==2UE@-+;&cjVYIc0BLPh`m zFzs6jmZheV@LUQi?a7X4ES;{V3VRv;R9SJ5B649*Ln?@ zfRlQw=&NUdo5p>7!1dz@rcl{`0=N-mGC2SP01x!@u~EW-?I330ycIV>Wxf5f?G4~d zU)bM?9ZeJt`hEZ3eb?v57p8y1tA}-_KxiKGKaKWJZWL(@Wk=~3pOKbY?a_CJoR-eE z{ie1!$P@}n8z;ys)%siECn+yrp9ZO$b!w5xDZg^B0VPE@5E$>`*aJ7v6J#r{;;l*- zyMB4ivoNnvP-FfV8~Hu^g`&b`0jEoi zWdoj$s1F7>_1*2xQpYxka8(OpqCAvlKPFo>M74s06zcGefTxFvK}Gj=KDbvq)ik^M z`WPE!&8e-`%2t~FA&}FGhl!i6gvnD<;dMbC3Qc3`wa2{b@3m7kRswh@=k=hzH<8j1 z)0U2myBKQGT$9Y7v2;qRA<7xE%zNBePmb1^D%aD^x@BhmF4emndv^6pJBQItmjuox zKV*FAtgtX$=P{C#H;9lCFJA7v>(e_XPO}+04^YJ!{gBxHb2dB(^N}kTBZpquR0u4$ z9t0LF`a!R%kdk|PtmF8^FRb>wdj^_dst^(EhX|>dr_c+rlcJQ8ww#|-cJsGQ`fQ*9 zEHU4rG}pB+Sf7_-ytu+~)aiUQ8}LKEWgOPfFnNnUe|dKt_=Qk_9-K0~>sIT+Z|fH7 z^Ys%I0-3)Em5=3qo@uqTpaVtV5~GHU<5u*3ce5;JK8JUiZ4ewJ`?84D>2@5re5>?M zi}gHhb-AK%Go}mg>xcdj*Ja8mS<~C0y`)&nx^nL() zy6Z7(ShE(WAmg<1HFN!^8Imm&a9s zP*Dvhc~wJ$BO&EFh?45GF-taW&R_>3xh!1_#;#?2+FdATN9ZQXrzwLCmuad# zN)zB?UotL>>3QEG=npzkEw4ooD?cqr1h`%l5*j<=aE>UOo@^zIglKrh5ktnq!!tEC mb>TMhoP2a{MVW%fAG8EXNt>Xi(tO0Q0lJ!o8kOqyVgCYTjGpZP literal 0 HcmV?d00001 diff --git a/assets/default-standard-icons.png b/assets/default-standard-icons.png new file mode 100644 index 0000000000000000000000000000000000000000..73e96681ff7917786ad8bce7754c2ac7ed15cf7d GIT binary patch literal 37262 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 QN4{P`MnyV9((vK`0^nI%rvLx| literal 0 HcmV?d00001 diff --git a/assets/gallery.md b/assets/gallery.md index 0dac19b..b317cfd 100644 --- a/assets/gallery.md +++ b/assets/gallery.md @@ -1,13 +1,17 @@ -# Dark Style Sheets +# Gallery -## Breeze Dark +A gallery of various themes and images of the widgets. + +## Dark Style Sheets + +### Breeze Dark

Linux

- Breeze Dark theme for Linux
@@ -15,19 +19,19 @@
Breeze Dark theme for Windows
-## Breeze Dark Green +### Breeze Dark Green

Linux

- Breeze Dark-Green theme for Linux
@@ -35,19 +39,19 @@
Breeze Dark-Green theme for Windows
-## Breeze Dark Purple +### Breeze Dark Purple

Linux

- Breeze Dark-Purple theme for Linux
@@ -55,21 +59,21 @@
Breeze Dark-Purple theme for Windows
-# Light Style Sheets +## Light Style Sheets -## Breeze Light +### Breeze Light

Linux

- Breeze Light theme for Linux
@@ -77,19 +81,19 @@
Breeze Light theme for Windows
-## Breeze Light Green +### Breeze Light Green

Linux

- Breeze Light-Green theme for Linux
@@ -97,19 +101,19 @@
Breeze Light-Green theme for Windows
-## Breeze Light Purple +### Breeze Light Purple

Linux

- Breeze Light-Purple theme for Linux
@@ -117,7 +121,7 @@
Breeze Light-Purple theme for Windows
diff --git a/assets/mismatched_titlebar.png b/assets/mismatched_titlebar.png new file mode 100644 index 0000000000000000000000000000000000000000..193f7790cc89a452c8b0c2e4e5cc0aa6058954ef GIT binary patch literal 5940 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 system_theme.Theme: + '''Detect the system theme.''' + + if QT_VERSION >= (6, 5, 0): + color_scheme = app.styleHints().colorScheme() + if color_schema == ColorScheme.Unknown: + return system_theme.UNKNOWN + elif color_schema == ColorScheme.Light: + return system_theme.LIGHT + return system_theme.DARK + return system_theme.get_theme() +``` + +## System Icons + +To provide consistent themes, you can override the default system icons. An [example](/example/icons/standard.py) comparing the icons between Windows and our overrides is below. This requires configuring with the `standard-icons` extension. + + + + + + + + + + + + + + +
Default Icons
Custom Icons
The dark stylesheet demo.The light stylesheet demo.
+ +Then, create a style factory and register the style with your style: + +```python +# where the style name is a Qt style, like Windows or Fusion, +style = QtWidgets.QStyleFactory.create('Windows') +style = StandardIconStyle(style) +app.setStyle(style) +``` + +## Custom Titlebars + +When the stylesheet color does not match the system theme, the titlebar can look out of place with the application. + +![Mismatched Titlebar](/assets/mismatched_titlebar.png) + +Removing the application titlebar and using a custom [titlebar](/example/titlebar/titlebar.py) can create a consistent look anbd feel. + +First, ensure the main window (and any subwindows) remove the titlebar, optionally removing hints: + +```python +# this removes the title bar, and optionally the help and shade button hints +flags = titlebar.QtCore.Qt.WindowType(0) +flags |= titlebar.compat.WindowContextHelpButtonHint +flags |= titlebar.compat.WindowShadeButtonHint +``` + +Next, initialize the application, load your stylesheet, and install an event filter for the window to track move, drag, and other events: + +```python +app = QtWidgets.QApplication.instance() +app.installEventFilter(window) +``` + +This custom titlebar supports the following: +- Title text +- Title bar with menu, help, min, max, restore, close, shade, and unshade. + - Help, shade, and unshade are optional. + - Menu contains restore, min, max, move, resize, stay on top, and close. +- Custom window minimization. + - Minimized windows can be placed in any corner. + - Windows reposition on resize events to avoid truncating windows. +- Dynamically toggle window state to keep windows above others. +- Drag titlebar to move window +- Double click titlebar to change window state. + - Restores if maximized or minimized. + - Shades or unshades if in normal state and applicable. + - Otherwise, maximizes window. +- Context menu move and resize events. + - Click "Size" to resize from the bottom right based on cursor. + - Click "Move" to move bottom-center of titlebar to cursor. +- Drag to resize on window border with or without size grips. + - If the window contains size grips, use the default behavior. + - Otherwise, monitor mouse and hover events on window border. + - If hovering over window border, draw appropriate resize cursor. + - If clicked on window border, enter resize mode. + - Click again to exit resize mode. +- Custom border width for a window outline. + +The following Qt properties ensure proper styling of the UI: +- `isTitlebar`: should be set on the title bar. ensures all widgets in the title bar have the correct background. +- `isWindow`: set on the window to ensure there is no default border. +- `hasWindowFrame`: set on a window with a border to draw the frame. + +![Custom Titlebar](/assets/custom_titlebar.png) + +The custom titlebar mas **MAJOR** limitations and if possible, it's better to change the look and feel using your OS's API. + +- **Linux - Wayland** + - Cannot move the window position. This cannot be done even if you know the compositor (such as kwin). + - Cannot use the menu resize due to `QWidget::mouseGrab()`. + - This plugin supports grabbing the mouse only for popup windows + - The window stops tracking mouse movements past a certain distance. + - Attempting to move the window position causes global position to be wrong. + - Wayland does not support `Stay on Top` directive. + - qt.qpa.wayland: Wayland does not support QWindow::requestActivate() + - The menu resize has to guess the mouse position outside of the window bounds. + - This cannot be fixed since we cannot use mouse events if the user is outside the main window, nor do hover events trigger. We cannot guess where the user left the main window, since `QCursor::pos` will not be updated until the user moves the mouse within the application, so merely resizing until the actual cursor is within the window won't work. + - We cannot intercept mouse events for the menu resize outside the window (this even occurs when forcing X11 on Wayland). +- **Windows** + - Cannot resize the menu. + - Subwindows and windows cannot track outside the main window boundaries. + +**Customizing Windows Title Bars via the Win32 API:** + +On Windows, you can use the [Desktop Window Manager](https://learn.microsoft.com/en-us/windows/win32/api/_dwm/) Win32 API on Windows 10+. + +First, get the HDNL for the [window](https://doc.qt.io/qt-6/qwidget.html#winId). + +```cpp +auto window_handle = reinterpret_cast(window.winId()); +``` + +To toggle dark mode on or off, use: + +```cpp +#include +#include + +// set to true or false +auto use_dark_mode = true; +auto success = SUCCEEDED(DwmSetWindowAttribute( + window_handle, + DWMWINDOWATTRIBUTE::DWMWA_USE_IMMERSIVE_DARK_MODE, + &use_dark_mode, + sizeof(use_dark_mode))); +``` + +You can set specific [colors](https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute) as well to mimic the stylesheet theme, such as the border and caption colors, and then redraw the window to set the colors. Colors are provided in the `0x00BBGGRR` format. The supported colors are: + +- Border: `DWMWA_BORDER_COLOR` +- Caption: `DWMWA_CAPTION_COLOR` +- Text: `DWMWA_TEXT_COLOR` + +```cpp +#include +#include + +COLORREF color = 0x00505050; +auto success = SUCCEEDED(DwmSetWindowAttribute( + window_handle, + DWMWINDOWATTRIBUTE::DWMWA_BORDER_COLOR, + &color, + sizeof(color))); + +// you must redraw the window to change the titlebar colors +ShowWindow(window_handle, SW_MINIMIZE); +ShowWindow(window_handle, SW_RESTORE); +``` + +## Dial Widgets + +The standard [QDial] widget cannot be stylized via stylesheets and is quite aesthetically unappealing. However, subclassing the paint events in `QDial` can apply the stylesheet colors to restyle the [dial](/example/dial/dial.py). + +![Custom Dial](/assets/custom_dial.png) + +## Branchless QTreeView + +This is an example widget for a `QTreeView` where the branch indicators are hidden. In order to add these branchless indicators to your project, copy the branchless [directory](/example/branchless/) into the [extension](/extension) folder, and then configure with (adding any additional resources or styles as you see fit): + +```bash +# choose the desired framework from pyqt5, pyqt6, pyside2, pyside6 +framework=pyqt5 +python configure.py \ + --styles=all \ + --extensions=all \ + --qt-framework \ + "${framework}" \ + --resource breeze.qrc \ + --compiled-resource \ + "breeze_${framework}.py" +``` + +Then, to remove the branch indicators, you must also set the object name for each `QTreeView` or `QTreeWidget` to `"branchless"`, for example, in Python, `tree.setObjectName("branchless")`. + + + + + + + + + + + + + + +
Dark
Light
+ Breeze Dark theme using branchless indicators for Windows + + Breeze Light theme using branchless indicators for Windows +
+ +## LCD + +Similar to [QDial], the standard [QLCDNumber] widget cannot be stylized via stylesheets and is quite aesthetically unappealing. However, subclassing the paint events in `QLCDNumber` can apply the stylesheet colors to restyle the [LCD display](/example/lcd/lcd.py). + +![Custom LCD](/assets/custom_lcd.png) + +## Slider + +Similar to [QDial], the standard [QSlider] widget cannot be stylized via stylesheets and is quite aesthetically unappealing. However, subclassing the paint events in `QSlider` can apply the stylesheet colors to restyle the [slider](/example/slider/slider.py). + +![Custom Slider](/assets/custom_slider.png) + +## Non-Stylesheet Styling + +Stylesheets have limitations, as does subclassing individual widgets to override paint events. You can provide more general styling by overriding [QCommonStyle]. See [System Icons](#system-icons) for a simple example implementing a general look and feel of the UI. + +First, create a custom subclass of `QCommonStyle`: + +```python +class CustomStyle(QtWidgets.QCommonStyle): + '''A custom application style.''' + + # implementation goes here +``` + +Then, create a style factory and register the style with your style: + +```python +# where the style name is a Qt style, like Windows or Fusion, +style = QtWidgets.QStyleFactory.create('Windows') +style = CustomStyle(style) +app.setStyle(style) +``` + +## CMake + +Using CMake, you can download, configure, and compile the resources as part part of the build process. The following configurations are provided by [ruilvo](https://github.com/ruilvo/). You can see a full example in [example](/example/cmake/). First, use the CMake module [breeze.cmake](/example/cmake/breeze.cmake) and create a [CMakeLists](/example/cmake/CMakeLists.txt). + +Add in cached variables necessary to configure the `breeze.cmake` module: + +```cmake +set(QT_VERSION Qt5 CACHE STRING "The Qt version framework to use (Qt5 or Qt6).") +set(BREEZE_EXTENSIONS all CACHE STRING "The extensions to include in our stylesheets.") +set(BREEZE_STYLES all CACHE STRING "The styles to include in our stylesheets.") +``` + +Then, include the module and link the libraries to our executable: + +```cmake +include(${CMAKE_CURRENT_SOURCE_DIR}/breeze.cmake) +set(SOURCE_FILES ...) +add_executable(testing ${SOURCE_FILES}) +target_link_libraries(executable PRIVATE Qt${QT_VERSION_MAJOR}::Widgets breeze) +``` + +## Example Widgets + +Examples of some simple UIs with our stylesheets include: + +[**Widgets**](/example/widgets.py) + +![Widgets](/assets/Breeze%20Dark.gif) + +[**Placeholder Text**](/example/placeholder_text.py) + +This only works on Qt5. + +![Custom Placeholder Text](/assets/custom_placeholder_text.png) + +[**What's This**](/example/whatsthis.py) + +![What's This](/assets/custom_whatsthis.png) + +[**URL**](/example/url.py) + +![URL](/assets/custom_url.png) + + +[QDial]: https://doc.qt.io/qt-6/qdial.html +[QLCDNumber]: https://doc.qt.io/qt-6/qlcdnumber.html +[QSlider]: https://doc.qt.io/qt-6/qslider.html +[QCommonStyle]: https://doc.qt.io/qt-6/qcommonstyle.html diff --git a/example/branchless/README.md b/example/branchless/README.md deleted file mode 100644 index 2588d37..0000000 --- a/example/branchless/README.md +++ /dev/null @@ -1,31 +0,0 @@ -branchless -========== - -This contains an example widget for a `QTreeView` where the branch indicators are hidden. In order to add these branchless indicators to your project, copy this directory into the [extension](/extension) folder, and then configure with (adding any additional resources or styles as you see fit): - -```bash -python configure.py --extensions=branchless --resource custom.qrc -``` - -To remove the branch indicators, you must also set the object name for each `QTreeView` or `QTreeWidget` to `"branchless"`, for example, in Python, `tree.setObjectName("branchless")`. - -## Example - -

Dark

-
- Breeze Dark theme using branchless indicators for Windows -
- - -

Light

-
- Breeze Light theme using branchless indicators for Windows -
diff --git a/example/branchless/application.py b/example/branchless/main.py similarity index 96% rename from example/branchless/application.py rename to example/branchless/main.py index 94fbeef..1153a8a 100644 --- a/example/branchless/application.py +++ b/example/branchless/main.py @@ -32,8 +32,9 @@ import os import sys -HOME = os.path.dirname(os.path.realpath(__file__)) -sys.path.insert(0, os.path.dirname(HOME)) +EXAMPLE = os.path.dirname(os.path.realpath(__file__)) +HOME = os.path.dirname(EXAMPLE) +sys.path.insert(0, HOME) import shared # noqa # pylint: disable=wrong-import-position,import-error import widgets # noqa # pylint: disable=wrong-import-position,import-error diff --git a/example/cmake/breeze.cmake b/example/cmake/breeze.cmake index 0fa23d7..f3ff6d4 100644 --- a/example/cmake/breeze.cmake +++ b/example/cmake/breeze.cmake @@ -23,6 +23,7 @@ FetchContent_Declare( GIT_REPOSITORY https://github.com/Alexhuszagh/BreezeStyleSheets.git GIT_TAG origin/main GIT_PROGRESS ON + GIT_SHALLOW 1 USES_TERMINAL_DOWNLOAD TRUE) FetchContent_GetProperties(breeze_stylesheets) diff --git a/example/breeze_theme.hpp b/example/detect/system_theme.hpp similarity index 99% rename from example/breeze_theme.hpp rename to example/detect/system_theme.hpp index 04603db..8db8d84 100644 --- a/example/breeze_theme.hpp +++ b/example/detect/system_theme.hpp @@ -1,5 +1,5 @@ /** - * breeze_theme + * system_theme * ============ * * Determine if the system theme is light or dark, supporting many platforms. diff --git a/example/breeze_theme.py b/example/detect/system_theme.py similarity index 99% rename from example/breeze_theme.py rename to example/detect/system_theme.py index 653c41e..a02864a 100644 --- a/example/breeze_theme.py +++ b/example/detect/system_theme.py @@ -1,5 +1,5 @@ ''' - breeze_theme + system_theme ============ Get the current system them information. This is adapted from darkdetect diff --git a/example/dial.py b/example/dial/dial.py similarity index 88% rename from example/dial.py rename to example/dial/dial.py index 3a385cb..1d0fdd3 100644 --- a/example/dial.py +++ b/example/dial/dial.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python -# # The MIT License (MIT) # # Copyright (c) <2022-Present> @@ -32,9 +30,13 @@ ''' import math +import os import sys -import shared +EXAMPLE = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, os.path.dirname(EXAMPLE)) + +import shared # noqa # pylint: disable=wrong-import-position,import-error parser = shared.create_parser() parser.add_argument( @@ -270,49 +272,3 @@ def eventFilter(self, obj, event): self.repaint() return super().eventFilter(obj, event) - - -class Ui: - '''Main class for the user interface.''' - - def setup(self, MainWindow): - '''Setup our main window for the UI.''' - - MainWindow.setObjectName('MainWindow') - MainWindow.resize(1068, 824) - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.centralwidget.setObjectName('centralwidget') - self.layout = QtWidgets.QHBoxLayout(self.centralwidget) - self.layout.setObjectName('layout') - if not args.no_align: - self.layout.setAlignment(compat.AlignVCenter) - MainWindow.setCentralWidget(self.centralwidget) - - self.dial1 = Dial(self.centralwidget) - self.layout.addWidget(self.dial1) - - self.dial2 = Dial(self.centralwidget) - self.dial2.setNotchesVisible(True) - self.layout.addWidget(self.dial2) - - self.dial3 = Dial(self.centralwidget) - self.dial3.setWrapping(True) - self.layout.addWidget(self.dial3) - - -def main(): - 'Application entry point' - - app, window = shared.setup_app(args, unknown, compat) - - # setup ui - ui = Ui() - ui.setup(window) - window.setWindowTitle('QDial') - - shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window) - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/example/dial/main.py b/example/dial/main.py new file mode 100644 index 0000000..3212c8f --- /dev/null +++ b/example/dial/main.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +''' + dial + ==== + + Sample UI widget with our dial. +''' + +import sys + +import dial + + +class Ui: + '''Main class for the user interface.''' + + def setup(self, MainWindow): + '''Setup our main window for the UI.''' + + MainWindow.setObjectName('MainWindow') + MainWindow.resize(1068, 824) + self.centralwidget = dial.compat.QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName('centralwidget') + self.layout = dial.compat.QtWidgets.QHBoxLayout(self.centralwidget) + self.layout.setObjectName('layout') + if not dial.args.no_align: + self.layout.setAlignment(dial.compat.AlignVCenter) + MainWindow.setCentralWidget(self.centralwidget) + + self.dial1 = dial.Dial(self.centralwidget) + self.layout.addWidget(self.dial1) + + self.dial2 = dial.Dial(self.centralwidget) + self.dial2.setNotchesVisible(True) + self.layout.addWidget(self.dial2) + + self.dial3 = dial.Dial(self.centralwidget) + self.dial3.setWrapping(True) + self.layout.addWidget(self.dial3) + + +def main(): + 'Application entry point' + + app, window = dial.shared.setup_app(dial.args, dial.unknown, dial.compat) + + # setup ui + ui = Ui() + ui.setup(window) + window.setWindowTitle('QDial') + window.resize(400, 150) + + dial.shared.set_stylesheet(dial.args, app, dial.compat) + return dial.shared.exec_app(dial.args, app, window) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/example/standard_icons.py b/example/icons/main.py similarity index 55% rename from example/standard_icons.py rename to example/icons/main.py index b00db10..21d9d8a 100644 --- a/example/standard_icons.py +++ b/example/icons/main.py @@ -1,27 +1,4 @@ #!/usr/bin/env python -# -# The MIT License (MIT) -# -# Copyright (c) <2022-Present> -# -# 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. - ''' standard_icons ============== @@ -31,59 +8,7 @@ import sys -import shared - -parser = shared.create_parser() -args, unknown = shared.parse_args(parser) -QtCore, QtGui, QtWidgets = shared.import_qt(args) -compat = shared.get_compat_definitions(args) -ICON_MAP = shared.get_icon_map(compat) - - -def style_icon(style, icon, option=None, widget=None): - '''Helper to provide arguments for setting a style icon.''' - return shared.style_icon(args, style, icon, ICON_MAP, option, widget) - - -class ApplicationStyle(QtWidgets.QCommonStyle): - '''A custom application style overriding standard icons.''' - - def __init__(self, style): - super().__init__() - self.style = style - - def __getattribute__(self, item): - ''' - Override for standardIcon. Everything else should default to the - system default. We cannot have `style_icon` be a member of - `ApplicationStyle`, since this will cause an infinite recursive loop. - ''' - - if item == 'standardIcon': - return lambda *x: style_icon(self, *x) - return getattr(self.style, item) - - -def add_standard_button(ui, layout, icon, index): - '''Create and add a QToolButton with a standard icon.''' - - button = QtWidgets.QToolButton(ui.centralwidget) - setattr(ui, f'button{index}', button) - button.setAutoRaise(True) - button.setIcon(style_icon(button.style(), icon, widget=button)) - button.setObjectName(f'button{index}') - layout.addWidget(button) - - -def add_standard_buttons(ui, page, icons): - '''Create and add QToolButtons with standard icons to the UI.''' - - _ = ui - for icon_name in icons: - icon_enum = getattr(compat, icon_name) - icon = style_icon(page.style(), icon_enum, widget=page) - item = QtWidgets.QListWidgetItem(icon, icon_name) - page.addItem(item) +import standard class Ui: @@ -94,19 +19,19 @@ def setup(self, MainWindow): # pylint: disable=too-many-statements MainWindow.setObjectName('MainWindow') MainWindow.resize(1068, 824) - self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget = standard.compat.QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName('centralwidget') - self.layout = QtWidgets.QVBoxLayout(self.centralwidget) + self.layout = standard.compat.QtWidgets.QVBoxLayout(self.centralwidget) self.layout.setObjectName('layout') - self.layout.setAlignment(compat.AlignHCenter) + self.layout.setAlignment(standard.compat.AlignHCenter) MainWindow.setCentralWidget(self.centralwidget) - self.tool_box = QtWidgets.QToolBox(self.centralwidget) - self.page1 = QtWidgets.QListWidget() + self.tool_box = standard.compat.QtWidgets.QToolBox(self.centralwidget) + self.page1 = standard.compat.QtWidgets.QListWidget() self.tool_box.addItem(self.page1, 'Overwritten Icons') self.layout.addWidget(self.tool_box) - add_standard_buttons( + standard.add_standard_buttons( self, self.page1, [ @@ -127,7 +52,7 @@ def setup(self, MainWindow): # pylint: disable=too-many-statements ], ) - self.page2 = QtWidgets.QListWidget() + self.page2 = standard.QtWidgets.QListWidget() self.tool_box.addItem(self.page2, 'Default Icons') self.layout.addWidget(self.tool_box) @@ -197,71 +122,71 @@ def setup(self, MainWindow): # pylint: disable=too-many-statements 'SP_DialogIgnoreButton', 'SP_RestoreDefaultsButton', ] - if compat.QT_VERSION >= (6, 3, 0): + if standard.compat.QT_VERSION >= (6, 3, 0): default_icons.append('SP_TabCloseButton') - add_standard_buttons(self, self.page2, default_icons) + standard.add_standard_buttons(self, self.page2, default_icons) - self.dockWidget1 = QtWidgets.QDockWidget(MainWindow) + self.dockWidget1 = standard.compat.QtWidgets.QDockWidget(MainWindow) self.dockWidget1.setObjectName('dockWidget1') - self.dockWidgetContents = QtWidgets.QWidget() + self.dockWidgetContents = standard.compat.QtWidgets.QWidget() self.dockWidgetContents.setObjectName('dockWidgetContents') self.dockWidget1.setWidget(self.dockWidgetContents) - MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(1), self.dockWidget1) + MainWindow.addDockWidget(standard.compat.QtCore.Qt.DockWidgetArea(1), self.dockWidget1) - self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.dockWidgetContents) + self.verticalLayout_2 = standard.compat.QtWidgets.QVBoxLayout(self.dockWidgetContents) self.verticalLayout_2.setObjectName('verticalLayout_2') - self.verticalLayout = QtWidgets.QVBoxLayout() + self.verticalLayout = standard.compat.QtWidgets.QVBoxLayout() self.verticalLayout.setObjectName('verticalLayout') - self.comboBox = QtWidgets.QComboBox(self.dockWidgetContents) + self.comboBox = standard.compat.QtWidgets.QComboBox(self.dockWidgetContents) self.comboBox.setObjectName('comboBox') self.comboBox.setEditable(True) self.comboBox.addItem('First') self.comboBox.addItem('Second') self.verticalLayout.addWidget(self.comboBox) - self.horizontalSlider = QtWidgets.QSlider(self.dockWidgetContents) - self.horizontalSlider.setOrientation(compat.Horizontal) + self.horizontalSlider = standard.compat.QtWidgets.QSlider(self.dockWidgetContents) + self.horizontalSlider.setOrientation(standard.compat.Horizontal) self.horizontalSlider.setObjectName('horizontalSlider') self.verticalLayout.addWidget(self.horizontalSlider) - self.textEdit = QtWidgets.QTextEdit(self.dockWidgetContents) + self.textEdit = standard.compat.QtWidgets.QTextEdit(self.dockWidgetContents) self.textEdit.setObjectName('textEdit') self.verticalLayout.addWidget(self.textEdit) - self.line = QtWidgets.QFrame(self.dockWidgetContents) - self.line.setFrameShape(compat.HLine) - self.line.setFrameShadow(compat.Sunken) + self.line = standard.compat.QtWidgets.QFrame(self.dockWidgetContents) + self.line.setFrameShape(standard.compat.HLine) + self.line.setFrameShadow(standard.compat.Sunken) self.line.setObjectName('line') self.verticalLayout.addWidget(self.line) - self.progressBar = QtWidgets.QProgressBar(self.dockWidgetContents) + self.progressBar = standard.compat.QtWidgets.QProgressBar(self.dockWidgetContents) self.progressBar.setProperty('value', 24) self.progressBar.setObjectName('progressBar') self.verticalLayout.addWidget(self.progressBar) self.verticalLayout_2.addLayout(self.verticalLayout) - self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1068, 29)) + self.menubar = standard.compat.QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(standard.compat.QtCore.QRect(0, 0, 1068, 29)) self.menubar.setObjectName('menubar') - self.menuMenu = QtWidgets.QMenu(self.menubar) + self.menuMenu = standard.compat.QtWidgets.QMenu(self.menubar) self.menuMenu.setObjectName('menuMenu') MainWindow.setMenuBar(self.menubar) - self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar = standard.compat.QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName('statusbar') MainWindow.setStatusBar(self.statusbar) - self.actionAction = compat.QAction(MainWindow) + self.actionAction = standard.compat.QAction(MainWindow) self.actionAction.setObjectName('actionAction') - self.actionAction_C = compat.QAction(MainWindow) + self.actionAction_C = standard.compat.QAction(MainWindow) self.actionAction_C.setObjectName('actionAction_C') self.menuMenu.addAction(self.actionAction) self.menuMenu.addAction(self.actionAction_C) self.menubar.addAction(self.menuMenu.menuAction()) - QtCore.QMetaObject.connectSlotsByName(MainWindow) + standard.compat.QtCore.QMetaObject.connectSlotsByName(MainWindow) self.retranslateUi(MainWindow) def retranslateUi(self, MainWindow): '''Retranslate our UI after initializing some of our base modules.''' - _translate = QtCore.QCoreApplication.translate + _translate = standard.compat.QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate('MainWindow', 'MainWindow')) self.menuMenu.setTitle(_translate('MainWindow', '&Menu')) self.actionAction.setText(_translate('MainWindow', '&Action')) @@ -269,17 +194,19 @@ def retranslateUi(self, MainWindow): def about(self): '''Load our Qt about window.''' - QtWidgets.QMessageBox.aboutQt(self.centralwidget, 'About Menu') + standard.compat.QtWidgets.QMessageBox.aboutQt(self.centralwidget, 'About Menu') def critical(self): '''Launch a critical message box.''' - QtWidgets.QMessageBox.critical(self.centralwidget, 'Error', 'Critical Error') + standard.compat.QtWidgets.QMessageBox.critical(self.centralwidget, 'Error', 'Critical Error') def main(): 'Application entry point' - app, window = shared.setup_app(args, unknown, compat, style_class=ApplicationStyle) + app, window = standard.shared.setup_app( + standard.args, standard.unknown, standard.compat, style_class=standard.StandardIconStyle + ) # setup ui ui = Ui() @@ -290,8 +217,8 @@ def main(): ui.actionAction.triggered.connect(ui.about) ui.actionAction_C.triggered.connect(ui.critical) - shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window) + standard.shared.set_stylesheet(standard.args, app, standard.compat) + return standard.shared.exec_app(standard.args, app, window) if __name__ == '__main__': diff --git a/example/icons/standard.py b/example/icons/standard.py new file mode 100644 index 0000000..b33120b --- /dev/null +++ b/example/icons/standard.py @@ -0,0 +1,87 @@ +# The MIT License (MIT) +# +# Copyright (c) <2022-Present> +# +# 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. + +''' + standard_icons + ============== + + Example overriding QCommonStyle for custom standard icons. +''' + +import os +import sys + +HOME = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, os.path.dirname(HOME)) + +import shared # noqa # pylint: disable=wrong-import-position,import-error + +parser = shared.create_parser() +args, unknown = shared.parse_args(parser) +QtCore, QtGui, QtWidgets = shared.import_qt(args) +compat = shared.get_compat_definitions(args) +ICON_MAP = shared.get_icon_map(compat) + + +def style_icon(style, icon, option=None, widget=None): + '''Helper to provide arguments for setting a style icon.''' + return shared.style_icon(args, style, icon, ICON_MAP, option, widget) + + +class StandardIconStyle(QtWidgets.QCommonStyle): + '''A custom application style overriding standard icons.''' + + def __init__(self, style): + super().__init__() + self.style = style + + def __getattribute__(self, item): + ''' + Override for standardIcon. Everything else should default to the + system default. We cannot have `style_icon` be a member of + `StandardIconStyle`, since this will cause an infinite recursive loop. + ''' + if item == 'standardIcon': + return lambda *x: style_icon(self, *x) + return getattr(object.__getattribute__(self, 'style'), item) + + +def add_standard_button(ui, layout, icon, index): + '''Create and add a QToolButton with a standard icon.''' + + button = QtWidgets.QToolButton(ui.centralwidget) + setattr(ui, f'button{index}', button) + button.setAutoRaise(True) + button.setIcon(style_icon(button.style(), icon, widget=button)) + button.setObjectName(f'button{index}') + layout.addWidget(button) + + +def add_standard_buttons(ui, page, icons): + '''Create and add QToolButtons with standard icons to the UI.''' + + _ = ui + for icon_name in icons: + icon_enum = getattr(compat, icon_name) + icon = style_icon(page.style(), icon_enum, widget=page) + item = QtWidgets.QListWidgetItem(icon, icon_name) + page.addItem(item) diff --git a/example/lcd.py b/example/lcd/lcd.py similarity index 54% rename from example/lcd.py rename to example/lcd/lcd.py index 98a2ad7..c0e07ab 100644 --- a/example/lcd.py +++ b/example/lcd/lcd.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python -# # The MIT License (MIT) # # Copyright (c) <2022-Present> @@ -23,24 +21,29 @@ # THE SOFTWARE. ''' - dial - ==== + lcd + === Example showing how to override the `paintEvent` and `eventFilter` - for a `QDial`, creating a visually consistent, stylish `QDial` that - supports highlighting the handle on the active or hovered dial. + for a `QLCDNumber`, creating a visually consistent, stylish + `QLCDNumber` that supports highlighting the handle on the active + or hovered number. ''' +import os import sys -import shared +EXAMPLE = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, os.path.dirname(EXAMPLE)) + +import shared # noqa # pylint: disable=wrong-import-position,import-error parser = shared.create_parser() parser.add_argument( '--no-align', help='''allow larger widgets without forcing alignment.''', action='store_true' ) args, unknown = shared.parse_args(parser) -QtCore, QtGui, QtWidgets = shared.import_qt(args) +_, __, QtWidgets = shared.import_qt(args) compat = shared.get_compat_definitions(args) colors = shared.get_colors(args, compat) @@ -65,62 +68,3 @@ def __init__(self, widget=None): palette.setColor(compat.LightPalette, colors.Selected) palette.setColor(compat.DarkPalette, colors.Notch) self.setPalette(palette) - - -class Ui: - '''Main class for the user interface.''' - - def setup(self, MainWindow): - '''Setup our main window for the UI.''' - - MainWindow.setObjectName('MainWindow') - MainWindow.resize(1068, 824) - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.layout = QtWidgets.QHBoxLayout(self.centralwidget) - self.layout.setSpacing(0) - if not args.no_align: - self.layout.setAlignment(compat.AlignVCenter) - MainWindow.setCentralWidget(self.centralwidget) - - self.lcd1 = LCD(self.centralwidget) - self.lcd1.display(15) - self.lcd1.setDigitCount(2) - self.layout.addWidget(self.lcd1) - - self.lcd2 = LCD(self.centralwidget) - self.lcd2.display(31) - self.lcd2.setHexMode() - self.lcd2.setDigitCount(2) - self.layout.addWidget(self.lcd2) - - self.lcd3 = LCD(self.centralwidget) - self.lcd3.display(15) - self.lcd3.setSegmentStyle(compat.LCDOutline) - self.lcd3.setFrameShape(compat.NoFrame) - self.lcd3.setDigitCount(2) - self.layout.addWidget(self.lcd3) - - self.lcd4 = LCD(self.centralwidget) - self.lcd4.display(15) - self.lcd4.setSegmentStyle(compat.LCDFlat) - self.lcd4.setFrameShape(compat.NoFrame) - self.lcd4.setDigitCount(2) - self.layout.addWidget(self.lcd4) - - -def main(): - 'Application entry point' - - app, window = shared.setup_app(args, unknown, compat) - - # setup ui - ui = Ui() - ui.setup(window) - window.setWindowTitle('QLCDNumber') - - shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window) - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/example/lcd/main.py b/example/lcd/main.py new file mode 100644 index 0000000..a73e29d --- /dev/null +++ b/example/lcd/main.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +''' + lcd + === + + Example showing how to override the `paintEvent` and `eventFilter` + for a `QLCDNumber`, creating a visually consistent, stylish + `QLCDNumber` that supports highlighting the handle on the active + or hovered number. +''' + +import sys + +import lcd + + +class Ui: + '''Main class for the user interface.''' + + def setup(self, MainWindow): + '''Setup our main window for the UI.''' + + MainWindow.setObjectName('MainWindow') + MainWindow.resize(1068, 824) + self.centralwidget = lcd.QtWidgets.QWidget(MainWindow) + self.layout = lcd.QtWidgets.QHBoxLayout(self.centralwidget) + self.layout.setSpacing(0) + if not lcd.args.no_align: + self.layout.setAlignment(lcd.compat.AlignVCenter) + MainWindow.setCentralWidget(self.centralwidget) + + self.lcd1 = lcd.LCD(self.centralwidget) + self.lcd1.display(15) + self.lcd1.setDigitCount(2) + self.layout.addWidget(self.lcd1) + + self.lcd2 = lcd.LCD(self.centralwidget) + self.lcd2.display(31) + self.lcd2.setHexMode() + self.lcd2.setDigitCount(2) + self.layout.addWidget(self.lcd2) + + self.lcd3 = lcd.LCD(self.centralwidget) + self.lcd3.display(15) + self.lcd3.setSegmentStyle(lcd.compat.LCDOutline) + self.lcd3.setFrameShape(lcd.compat.NoFrame) + self.lcd3.setDigitCount(2) + self.layout.addWidget(self.lcd3) + + self.lcd4 = lcd.LCD(self.centralwidget) + self.lcd4.display(15) + self.lcd4.setSegmentStyle(lcd.compat.LCDFlat) + self.lcd4.setFrameShape(lcd.compat.NoFrame) + self.lcd4.setDigitCount(2) + self.layout.addWidget(self.lcd4) + + +def main(): + 'Application entry point' + + app, window = lcd.shared.setup_app(lcd.args, lcd.unknown, lcd.compat) + + # setup ui + ui = Ui() + ui.setup(window) + window.setWindowTitle('QLCDNumber') + window.resize(400, 150) + + lcd.shared.set_stylesheet(lcd.args, app, lcd.compat) + return lcd.shared.exec_app(lcd.args, app, window) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/example/placeholder_text.py b/example/placeholder_text.py index a1cec31..fb3210b 100644 --- a/example/placeholder_text.py +++ b/example/placeholder_text.py @@ -114,6 +114,7 @@ def main(): ui = Ui() ui.setup(window) window.setWindowTitle('Stylized Placeholder Text.') + window.resize(400, 150) shared.set_stylesheet(args, app, compat) return shared.exec_app(args, app, window) diff --git a/example/shared.py b/example/shared.py index c6c2324..0fa0987 100644 --- a/example/shared.py +++ b/example/shared.py @@ -13,12 +13,13 @@ import os import sys -import breeze_theme - example_dir = os.path.dirname(os.path.realpath(__file__)) home = os.path.dirname(example_dir) dist = os.path.join(home, 'dist') -IS_DARK = None +sys.path.append(home) +THEME = None + +from example.detect import system_theme # noqa # pylint: disable=wrong-import-position,import-error def create_parser(): @@ -81,12 +82,13 @@ def parse_args(parser): def normalize_stylesheet(stylesheet): '''Normalize the stylesheet, removing and normalizing any aliases.''' - # now we need to normalize our theme + # now we need to normalize our theme. we don't use Qt6 features + # so we can differentiat between light/dark/unknown. if stylesheet.startswith('auto'): - theme = breeze_theme.get_theme() - if theme == breeze_theme.Theme.DARK: + theme = system_theme.get_theme() + if theme == system_theme.Theme.DARK: stylesheet = stylesheet.replace('auto', 'dark', 1) - elif theme == breeze_theme.Theme.LIGHT: + elif theme == system_theme.Theme.LIGHT: stylesheet = stylesheet.replace('auto', 'light', 1) else: logging.warning('Unknown an unknown system theme, falling back to the system native theme.') @@ -956,7 +958,7 @@ def setup_app(args, unknown, compat, style_class=None, window_class=None): if app is None: app = compat.QtWidgets.QApplication(sys.argv[:1] + unknown) # NOTE: Need to detect if the style is dark mode here - _ = is_dark_mode(compat) + _ = get_theme(compat) if args.style != 'native': style = compat.QtWidgets.QStyleFactory.create(args.style) if style_class is not None: @@ -980,13 +982,13 @@ def setup_app(args, unknown, compat, style_class=None, window_class=None): return app, window -def is_dark_mode(compat, reinitialize=False): +def get_theme(compat, reinitialize=False): '''Determine if the system theme is in dark mode.''' - global IS_DARK + global THEME - if IS_DARK is not None and not reinitialize: - return IS_DARK + if THEME is not None and not reinitialize: + return THEME app = compat.QtWidgets.QApplication.instance() if app is None: @@ -994,15 +996,17 @@ def is_dark_mode(compat, reinitialize=False): if compat.QT_VERSION >= (6, 5, 0): color_scheme = app.styleHints().colorScheme() - IS_DARK = color_scheme == color_scheme.__class__.Dark + theme_cls = color_scheme.__class__ + if color_scheme == theme_cls.Unknown: + THEME = system_theme.Theme.UNKNOWN + elif color_scheme == theme_cls.Light: + THEME = system_theme.Theme.LIGHT + else: + THEME = system_theme.Theme.DARK else: - # NOTE: This does not work, it only gives the default app color - # which on early versions of Qt defaults to the application style. - text = app.palette().windowText().color() - window = app.palette().window().color() - IS_DARK = window.lightness() > text.lightness() + THEME = system_theme.get_theme() - return IS_DARK + return THEME def read_qtext_file(path, compat): diff --git a/example/slider/main.py b/example/slider/main.py new file mode 100644 index 0000000..29da8bc --- /dev/null +++ b/example/slider/main.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +''' + slider + ====== + + Example showing how to add ticks to a QSlider. Note that this does + not work with stylesheets, so it's merely an example of how to + get customized styling behavior with a QSlider. +''' + +import sys + +import slider + + +class Ui: + '''Main class for the user interface.''' + + def setup(self, MainWindow): + '''Setup our main window for the UI.''' + + MainWindow.setObjectName('MainWindow') + MainWindow.resize(1068, 824) + self.centralwidget = slider.compat.QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName('centralwidget') + self.layout = slider.compat.QtWidgets.QVBoxLayout(self.centralwidget) + self.layout.setObjectName('layout') + self.layout.setAlignment(slider.compat.AlignHCenter) + MainWindow.setCentralWidget(self.centralwidget) + + self.slider = slider.Slider(self.centralwidget) + self.slider.setOrientation(slider.compat.Horizontal) + self.slider.setTickInterval(5) + self.slider.setTickPosition(slider.compat.TicksAbove) + self.slider.setObjectName('slider') + self.layout.addWidget(self.slider) + + +def main(): + 'Application entry point' + + app, window = slider.shared.setup_app(slider.args, slider.unknown, slider.compat) + + # setup ui + ui = Ui() + ui.setup(window) + window.setWindowTitle('QSlider with Ticks.') + window.resize(400, 150) + + slider.shared.set_stylesheet(slider.args, app, slider.compat) + return slider.shared.exec_app(slider.args, app, window) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/example/slider.py b/example/slider/slider.py similarity index 73% rename from example/slider.py rename to example/slider/slider.py index 33c9aa3..076289a 100644 --- a/example/slider.py +++ b/example/slider/slider.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python -# # The MIT License (MIT) # # Copyright (c) <2022-Present> @@ -31,9 +29,13 @@ get customized styling behavior with a QSlider. ''' +import os import sys -import shared +EXAMPLE = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, os.path.dirname(EXAMPLE)) + +import shared # noqa # pylint: disable=wrong-import-position,import-error parser = shared.create_parser() args, unknown = shared.parse_args(parser) @@ -86,44 +88,3 @@ def paintEvent(self, event): # pylint: disable=unused-argument,(too-many-locals options.subControls = compat.SC_SliderHandle painter.drawComplexControl(compat.CC_Slider, options) - - -class Ui: - '''Main class for the user interface.''' - - def setup(self, MainWindow): - '''Setup our main window for the UI.''' - - MainWindow.setObjectName('MainWindow') - MainWindow.resize(1068, 824) - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.centralwidget.setObjectName('centralwidget') - self.layout = QtWidgets.QVBoxLayout(self.centralwidget) - self.layout.setObjectName('layout') - self.layout.setAlignment(compat.AlignHCenter) - MainWindow.setCentralWidget(self.centralwidget) - - self.slider = Slider(self.centralwidget) - self.slider.setOrientation(compat.Horizontal) - self.slider.setTickInterval(5) - self.slider.setTickPosition(compat.TicksAbove) - self.slider.setObjectName('slider') - self.layout.addWidget(self.slider) - - -def main(): - 'Application entry point' - - app, window = shared.setup_app(args, unknown, compat) - - # setup ui - ui = Ui() - ui.setup(window) - window.setWindowTitle('QSlider with Ticks.') - - shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window) - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/example/titlebar/main.py b/example/titlebar/main.py new file mode 100644 index 0000000..ddf1cfe --- /dev/null +++ b/example/titlebar/main.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +''' + titlebar + ======== + + A full-featured, custom titlebar for a subwindow in an MDI area. This + uses a frameless window hint with a custom titlebar, and event filter + to capture titlebar and frame events. This example can also be easily + applied to a top-level window. + + The custom titlebar supports the following: + - Title text + - Title bar with menu, help, min, max, restore, close, shade, and unshade. + - Help, shade, and unshade are optional. + - Menu contains restore, min, max, move, resize, stay on top, and close. + - Custom window minimization. + - Minimized windows can be placed in any corner. + - Windows reposition on resize events to avoid truncating windows. + - Dynamically toggle window state to keep windows above others. + - Drag titlebar to move window + - Double click titlebar to change window state. + - Restores if maximized or minimized. + - Shades or unshades if in normal state and applicable. + - Otherwise, maximizes window. + - Context menu move and resize events. + - Click "Size" to resize from the bottom right based on cursor. + - Click "Move" to move bottom-center of titlebar to cursor. + - Drag to resize on window border with or without size grips. + - If the window contains size grips, use the default behavior. + - Otherwise, monitor mouse and hover events on window border. + - If hovering over window border, draw appropriate resize cursor. + - If clicked on window border, enter resize mode. + - Click again to exit resize mode. + - Custom border width for a window outline. + + The following Qt properties ensure proper styling of the UI: + - `isTitlebar`: should be set on the title bar. ensures all widgets + in the title bar have the correct background. + - `isWindow`: set on the window to ensure there is no default border. + - `hasWindowFrame`: set on a window with a border to draw the frame. + + The widget choice is very deliberate: any modifications can cause + unexpected changes. `TitleBar` must be a `QFrame` so the background + is filled, but must have a `NoFrame` shape. The window frame should + have `NoFrame` without a border, but should be a `Box` with a border. + Any other more elaborate style, like a `Panel`, won't be rendered + correctly. + + NOTE: you cannot correctly emulate a title bar if the desktop environment + is Wayland, even if the app is running in X11 mode. This mostly affects + just the top-level title bar (and subwindows almost entirely work), + but there are a few small issues for subwindows. + + The top-level title bar can have a few issues on Wayland. + - Cannot move the window position. This cannot be done even if you know + the compositor (such as kwin). + - Cannot use the menu resize due to `QWidget::mouseGrab()`. + - This plugin supports grabbing the mouse only for popup windows + - The window stops tracking mouse movements past a certain distance. + - Attempting to move the window position causes global position to be wrong. + - Wayland does not support `Stay on Top` directive. + - qt.qpa.wayland: Wayland does not support QWindow::requestActivate() + + A few other issues exist on Wayland. + - The menu resize has to guess the mouse position outside of the window bounds. + - This cannot be fixed since we cannot use mouse events if the user + is outside the main window, nor do hover events trigger. + We cannot guess where the user left the main window, since + `QCursor::pos` will not be updated until the user moves the + mouse within the application, so merely resizing until the + actual cursor is within the window won't work. + - We cannot intercept mouse events for the menu resize outside the window. + - This even occurs when forcing X11 on Wayland. + + On Windows, only the menu resize event fails. For the subwindow, it stops + tracking outside of the window boundaries, and for the main window, it does + the same, making it practically useless. + + # Testing + + The current platforms/desktop environments have been tested: + - Gnome (X11, Wayland) + - KDE Plasma (X11, Wayland) + - Windows 10 +''' + +import sys + +import titlebar + +# Add a warning if we're using Wayland with a custom titlebar. +if not titlebar.args.default_window_frame and titlebar.USE_WAYLAND_FRAME: + print('WARNING: Wayland does not support custom title bars.', file=sys.stderr) + print('Applications in Wayland cannot set their own position.', file=sys.stderr) + print('Defaulting to the system title bar instead.', file=sys.stderr) + + +class DefaultWindow(titlebar.Window): + '''Default main window with a window frame.''' + + def __init__(self, parent=None, flags=titlebar.QtCore.Qt.WindowType(0)): + if titlebar.args.window_help: + flags |= titlebar.compat.WindowContextHelpButtonHint + if titlebar.args.window_shade: + flags |= titlebar.compat.WindowShadeButtonHint + super().__init__(parent, flags) + + self._central = titlebar.QtWidgets.QFrame(self) + self._layout = titlebar.QtWidgets.QVBoxLayout(self._central) + self.setCentralWidget(self._central) + self._widget = titlebar.QtWidgets.QWidget(self._central) + self._widget.setLayout(titlebar.QtWidgets.QVBoxLayout()) + self._central.layout().addWidget(self._widget, 10) + + if titlebar.args.status_bar: + self._statusbar = titlebar.QtWidgets.QStatusBar(self._central) + self.setStatusBar(self._statusbar) + + self.setup() + + +def main(): + 'Application entry point' + + window_class = titlebar.FramelessWindow + # Wayland does not allow windows to reposition themselves: therefore, + # we cannot use the custom titlebar at the application level. + if titlebar.args.default_window_frame or titlebar.USE_WAYLAND_FRAME: + window_class = DefaultWindow + app, window = titlebar.shared.setup_app( + titlebar.args, titlebar.unknown, titlebar.compat, window_class=window_class + ) + app.installEventFilter(window) + + titlebar.shared.set_stylesheet(titlebar.args, app, titlebar.compat) + return titlebar.shared.exec_app(titlebar.args, app, window) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/example/titlebar.py b/example/titlebar/titlebar.py similarity index 97% rename from example/titlebar.py rename to example/titlebar/titlebar.py index bbfbd76..f23b172 100644 --- a/example/titlebar.py +++ b/example/titlebar/titlebar.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python -# # The MIT License (MIT) # # Copyright (c) <2022-Present> @@ -114,7 +112,10 @@ import sys from pathlib import Path -import shared +HOME = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, os.path.dirname(HOME)) + +import shared # noqa # pylint: disable=wrong-import-position,import-error parser = shared.create_parser() parser.add_argument( @@ -180,12 +181,6 @@ IS_TRUE_WAYLAND = 'WAYLAND_DISPLAY' in os.environ USE_WAYLAND_FRAME = IS_WAYLAND and not args.wayland_testing -# Add a warning if we're using Wayland with a custom titlebar. -if not args.default_window_frame and USE_WAYLAND_FRAME: - print('WARNING: Wayland does not support custom title bars.', file=sys.stderr) - print('Applications in Wayland cannot set their own position.', file=sys.stderr) - print('Defaulting to the system title bar instead.', file=sys.stderr) - class MinimizeLocation(enum.IntEnum): '''Location where to place minimized widgets.''' @@ -2087,30 +2082,6 @@ def eventFilter(self, obj, event): return super().eventFilter(obj, event) -class DefaultWindow(Window): - '''Default main window with a window frame.''' - - def __init__(self, parent=None, flags=QtCore.Qt.WindowType(0)): - if args.window_help: - flags |= compat.WindowContextHelpButtonHint - if args.window_shade: - flags |= compat.WindowShadeButtonHint - super().__init__(parent, flags) - - self._central = QtWidgets.QFrame(self) - self._layout = QtWidgets.QVBoxLayout(self._central) - self.setCentralWidget(self._central) - self._widget = QtWidgets.QWidget(self._central) - self._widget.setLayout(QtWidgets.QVBoxLayout()) - self._central.layout().addWidget(self._widget, 10) - - if args.status_bar: - self._statusbar = QtWidgets.QStatusBar(self._central) - self.setStatusBar(self._statusbar) - - self.setup() - - class FramelessWindow(Window): '''Main window with a custom event filter for all events.''' @@ -2319,22 +2290,3 @@ def changeEvent(self, event): self._titlebar.maximize() else: self._titlebar.restore() - - -def main(): - 'Application entry point' - - window_class = FramelessWindow - # Wayland does not allow windows to reposition themselves: therefore, - # we cannot use the custom titlebar at the application level. - if args.default_window_frame or USE_WAYLAND_FRAME: - window_class = DefaultWindow - app, window = shared.setup_app(args, unknown, compat, window_class=window_class) - app.installEventFilter(window) - - shared.set_stylesheet(args, app, compat) - return shared.exec_app(args, app, window) - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/example/url.py b/example/url.py index 4256744..5025d44 100644 --- a/example/url.py +++ b/example/url.py @@ -123,6 +123,7 @@ def main(): ui = Ui() ui.setup(window) window.setWindowTitle('Stylized URL colors.') + window.resize(200, 100) shared.set_stylesheet(args, app, compat) return shared.exec_app(args, app, window) diff --git a/example/whatsthis.py b/example/whatsthis.py index 1d7c78e..312f580 100644 --- a/example/whatsthis.py +++ b/example/whatsthis.py @@ -79,6 +79,7 @@ def main(): ui = Ui() ui.setup(window) window.setWindowTitle('Stylized QWhatsThis.') + window.resize(200, 100) shared.set_stylesheet(args, app, compat) return shared.exec_app(args, app, window) diff --git a/extension/README.md b/extension/README.md index 0816213..4bd5cdc 100644 --- a/extension/README.md +++ b/extension/README.md @@ -1,5 +1,4 @@ -Extensions -========== +# Extensions Extensions enable the creation of stylesheets using the same, customizable themes of the original stylesheet. This both allows refining the generated stylesheet and supporting third-party Qt extensions/widgets. @@ -7,7 +6,7 @@ Extensions are optionally added to the generated stylesheets, allowing you to ex Furthermore, this simplifies making local, application-specific changes, without having to deal with merge conflicts when fetching updates. -# Pre-Packaged Extensions +## Pre-Packaged Extensions ### Advanced Docking System @@ -22,34 +21,34 @@ python configure.py --extensions=advanced-docking-system And make sure to [disable](https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/blob/master/doc/user-guide.md#disabling-the-internal-style-sheet) the internal stylesheet in the dock manager.
- Advanced Docking System View 1
- Advanced Docking System View 2
- Advanced Docking System View 3
- Advanced Docking System View 4
@@ -64,10 +63,10 @@ python configure.py --extensions=dock-tooltips ```
- Dock Tooltips
@@ -168,7 +167,7 @@ The following is a 1:1 mapping of the standard icon enumerated name and the icon } ``` -# Creating Extensions +## Creating Extensions Creating extensions extends the existing stylesheet configurations, and adds custom rules to the stylesheet, which can then be configured for all themes. This supports custom icons, rules, and more. @@ -211,7 +210,7 @@ Next, let's create an SVG template for the icon, at `icon.svg.in`: ``` -Here, `^0^` signifies index-based replacement, so we must define an entry in +Here, `^0^` signifies index-based replacement, so we must define an entry in `icons.json` to specify how we should do the replacement. ```json diff --git a/scripts/cmake.sh b/scripts/cmake.sh index 79b3933..6eac38c 100755 --- a/scripts/cmake.sh +++ b/scripts/cmake.sh @@ -21,7 +21,6 @@ mkdir -p "${build_dir}/"{qt5,qt6} # we xcb installed for our headless running, so exit if we don't have it if ! hash xvfb-run &>/dev/null; then >&2 echo "Do not have xvfb installed..." - exit 1 fi # first, try Qt5 @@ -31,16 +30,20 @@ export QT_QPA_PLATFORM=offscreen cd "${build_dir}/qt5" cmake "${project_home}/example/cmake" -D QT_VERSION=Qt5 make -j -timeout 1 xvfb-run -a ./testing || error_code=$? -if [[ "${error_code}" != 124 ]]; then - exit "${error_code}" +if hash xvfb-run &>/dev/null; then + timeout 1 xvfb-run -a ./testing || error_code=$? + if [[ "${error_code}" != 124 ]]; then + exit "${error_code}" + fi fi # first, try Qt6 cd "${build_dir}/qt6" cmake "${project_home}/example/cmake" -D QT_VERSION=Qt6 make -j -timeout 1 xvfb-run -a ./testing || error_code=$? -if [[ "${error_code}" != 124 ]]; then - exit "${error_code}" +if hash xvfb-run &>/dev/null; then + timeout 1 xvfb-run -a ./testing || error_code=$? + if [[ "${error_code}" != 124 ]]; then + exit "${error_code}" + fi fi diff --git a/scripts/lint.sh b/scripts/lint.sh index 892d686..430e693 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -16,10 +16,10 @@ cd "${project_home}" # on the path by default. if ! is-set PYTHON; then pylint ./*.py example/*.py example/**/*.py - pyright example/breeze_theme.py + pyright example/detect/system_theme.py flake8 else ${PYTHON} -m pylint ./*.py example/*.py example/**/*.py - ${PYTHON} -m pyright example/breeze_theme.py + ${PYTHON} -m pyright example/detect/system_theme.py ${PYTHON} -m flake8 fi diff --git a/scripts/test_theme.cpp b/scripts/test_theme.cpp index a9f2fae..93acdaf 100644 --- a/scripts/test_theme.cpp +++ b/scripts/test_theme.cpp @@ -3,7 +3,7 @@ */ #include -#include "../example/breeze_theme.hpp" +#include "../example/detect/system_theme.hpp" int main() { diff --git a/scripts/theme.sh b/scripts/theme.sh index a3794da..0c9e480 100755 --- a/scripts/theme.sh +++ b/scripts/theme.sh @@ -6,19 +6,19 @@ set -eux pipefail scripts_home="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" project_home="$(dirname "${scripts_home}")" -cd "${project_home}/example" +cd "${project_home}/example/detect" # shellcheck source=/dev/null -. "${scripts_home}/shared.sh" +. "${scripts_home}/../shared.sh" if ! is-set PYTHON; then PYTHON=python fi # Check the import first, then calling the function for easier debugging. -${PYTHON} -c "import breeze_theme" -theme=$(${PYTHON} -c "import breeze_theme; print(breeze_theme.get_theme())") +${PYTHON} -c "import system_theme" +theme=$(${PYTHON} -c "import system_theme; print(system_theme.get_theme())") if [[ "${theme}" != Theme.* ]]; then >&2 echo "Unable to get the correct theme." exit 1 fi -${PYTHON} -c "import breeze_theme; print(breeze_theme.is_light())" -${PYTHON} -c "import breeze_theme; print(breeze_theme.is_dark())" +${PYTHON} -c "import system_theme; print(system_theme.is_light())" +${PYTHON} -c "import system_theme; print(system_theme.is_dark())" diff --git a/setup.cfg b/setup.cfg index c7c01a4..499c9d4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,6 +6,6 @@ per-file-ignores = example/widgets.py: F841 test/ui.py: F841 # lambdas are way cleaner here - example/titlebar.py: E731 + example/titlebar/titlebar.py: E731 # these are auto-generated files - resources/*.py: E302 E305 \ No newline at end of file + resources/*.py: E302 E305 From 5b9625bb808b83225efad384e67bea4ce1760a58 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sun, 8 Sep 2024 12:48:18 -0500 Subject: [PATCH 6/7] Improve stylesheet aliases for examples and configuration. --- .gitignore | 2 +- configure.py | 4 ++-- example/shared.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index ac06a4b..fda11c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# NOTE: this file is auto-generated via `git.py` +# NOTE: this file is auto-generated via `vcs.py` # DO NOT MANUALLY EDIT THIS FILE. TODO.md dist/ diff --git a/configure.py b/configure.py index 5d864c6..b516aad 100644 --- a/configure.py +++ b/configure.py @@ -417,8 +417,8 @@ def configure(args): # Create aliases for our light-blue and dark-blue styles to light and dark. # Only create aliases if light-blue and/or dark-blue are to be built. - themes = [theme for theme in args.styles if theme in ('dark-blue', 'light-blue')] - for theme in themes: + aliases = set(args.styles) & {'dark-blue', 'light-blue'} + for theme in aliases: source = args.output_dir / theme / 'stylesheet.qss' destination = args.output_dir / theme.split('-')[0] / 'stylesheet.qss' destination.parent.mkdir(exist_ok=True) diff --git a/example/shared.py b/example/shared.py index 0fa0987..bcc46c8 100644 --- a/example/shared.py +++ b/example/shared.py @@ -95,8 +95,8 @@ def normalize_stylesheet(stylesheet): stylesheet = 'native' # Needed so we remove any aliases. See #106. - if stylesheet in ('dark-blue', 'light-blue'): - stylesheet = stylesheet[: len('-blue') - 1] + if stylesheet in ('dark', 'light'): + stylesheet += '-blue' return stylesheet From e8c2378b99e55b2164f102d436f6eac55440ca00 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sun, 8 Sep 2024 12:51:12 -0500 Subject: [PATCH 7/7] Fix our CI with the theme checks. --- CHANGELOG.md | 3 +++ scripts/theme.sh | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4166622..7e6583c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,11 +18,14 @@ and this project adheres to [Semantic Versioning]. - Custom extension support, such as the advanced docking system. - Compile Qt resource files (from [chaosink]). - Documented support for CMake builds (from [ruilvo]). +- Add additional alternate themes for common styles (from [Inverted-E]). +- Add additional a red theme (from [Inverted-E]). ### Changed - Stylesheets to match KDE-like Breeze and Breeze dark themes. - Icons to match KDE-like Breeze and Breeze dark themes. +- Make `dark` and `light` aliases for `dark-blue` and `light-blue`, respectively. ### Deprecated diff --git a/scripts/theme.sh b/scripts/theme.sh index 0c9e480..fb63059 100755 --- a/scripts/theme.sh +++ b/scripts/theme.sh @@ -8,7 +8,7 @@ scripts_home="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" project_home="$(dirname "${scripts_home}")" cd "${project_home}/example/detect" # shellcheck source=/dev/null -. "${scripts_home}/../shared.sh" +. "${scripts_home}/shared.sh" if ! is-set PYTHON; then PYTHON=python