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 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/.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/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7e6583c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,68 @@ +# 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]). +- 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 + +### 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/ +[ruilvo]: https://github.com/ruilvo/ diff --git a/README.md b/README.md index a271735..7015ea0 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,34 @@ 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. [Known Issues and Workarounds](#known-issues-and-workarounds) +10. [License](#license) +11. [Contributing](#contributing) +12. [Acknowledgements](#acknowledgements) +13. [Contact](#contact) + +## Gallery + +### Breeze/BreezeDark Example user interface using the Breeze and BreezeDark stylesheets side-by-side. @@ -59,210 +59,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-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). - -# 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`. @@ -292,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) @@ -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. 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 + +- 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 + +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`: + +```c++ +class ApplicationStyle: public QCommonStyle +{ + ... +} +``` -# Debugging +The limitations of stylesheets include: -Have an issue with the styles? Here's a few suggestions, prior to filing a bug report: +- 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,28 @@ 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. - - ```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 +## 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: - -```bash -python vcs.py --install-hooks -``` - -You can also manually run each check independently: - -```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 -``` - -# 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. +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 +## 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 +Major contributions to the project have made by: +- [Inverted-E](https://github.com/Inverted-E/) + +## Contact -Email: ahuszagh@gmail.com +Email: [ahuszagh@gmail.com](mailto:ahuszagh@gmail.com) Twitter: KardOnIce diff --git a/assets/advanced_docking_system_example.png b/assets/advanced_docking_system_example.png new file mode 100644 index 0000000..627242a Binary files /dev/null and b/assets/advanced_docking_system_example.png differ diff --git a/assets/custom-standard-icons.png b/assets/custom-standard-icons.png new file mode 100644 index 0000000..2e08917 Binary files /dev/null and b/assets/custom-standard-icons.png differ diff --git a/assets/custom_placeholder_text.png b/assets/custom_placeholder_text.png new file mode 100644 index 0000000..f582ae6 Binary files /dev/null and b/assets/custom_placeholder_text.png differ diff --git a/assets/custom_slider.png b/assets/custom_slider.png new file mode 100644 index 0000000..e1ca17b Binary files /dev/null and b/assets/custom_slider.png differ diff --git a/assets/custom_url.png b/assets/custom_url.png new file mode 100644 index 0000000..96ea64e Binary files /dev/null and b/assets/custom_url.png differ diff --git a/assets/default-standard-icons.png b/assets/default-standard-icons.png new file mode 100644 index 0000000..73e9668 Binary files /dev/null and b/assets/default-standard-icons.png differ 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 0000000..193f779 Binary files /dev/null and b/assets/mismatched_titlebar.png differ 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/README.md b/example/README.md new file mode 100644 index 0000000..3127046 --- /dev/null +++ b/example/README.md @@ -0,0 +1,327 @@ +# Examples + +Here are numerous examples, including recipes, including auto-detecting the system theme, support for the Advanced Docking System, and overriding standard icons. Most of the widgets are drop-in replacements, with modifying `compat` in [shared.py](/example/shared.py) with the correct Qt imports and any other helper functions. + +**Table of Contents** + +1. [Advanced Docking System](#advanced-docking-system) +2. [System Theme Detection](#system-theme-detection) +3. [System Icons](#system-icons) +4. [Custom Titlebars](#custom-titlebars) +5. [Dial Widgets](#dial-widgets) +6. [Branchless QTreeView](#branchless-qtreeview) +7. [LCD](#lcd) +8. [Slider](#slider) +9. [Non-Stylesheet Styling](#non-stylesheet-styling) +10. [CMake](#cmake) +11. [Example Widgets](#example-widgets) + +## Advanced Docking System + +Shows use of the [Advanced Docking System](https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System) in [Python](/example/advanced-dock.py). This requires `PySide6-QtAds` (for PySide6) or `PyQtAds` (for PyQt5) (`PyQt6` and `PySide2` are not supported). This requires configuring with the `advanced-docking-system` extension. + +![Advanced Docking System](/assets/advanced_docking_system_example.png) + +## System Theme Detection + +To provide a consistent application style matching the system theme, you can detect it via using Qt for versions 6.5+, or using the [system theme](/example/detect/) Python and C++ utilities. Both are stand-alone libraries that can be dropped in any project. A cross-platform, version-agnostic approach to detect if light or dark mode is: + +```python +import system_theme + + +def get_theme() -> 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..bcc46c8 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,20 +82,21 @@ 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.') 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 @@ -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 6da781d..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.''' @@ -589,8 +584,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() @@ -2086,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.''' @@ -2318,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..fb63059 100755 --- a/scripts/theme.sh +++ b/scripts/theme.sh @@ -6,7 +6,7 @@ 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" @@ -14,11 +14,11 @@ 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 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