diff --git a/.github/workflows/build_assets.yml b/.github/workflows/build_assets.yml
index d9959e37..945b0634 100644
--- a/.github/workflows/build_assets.yml
+++ b/.github/workflows/build_assets.yml
@@ -6,27 +6,28 @@ jobs:
buildAssets:
runs-on: ubuntu-latest
steps:
- - name: Python Setup
- uses: actions/setup-python@v5
- with:
- python-version: "3.13"
- architecture: x64
+ - name: Checkout Source
+ uses: actions/checkout@v5
- - name: Install Packages (apt)
+ - name: Install System Packages
run: |
sudo apt update
sudo apt install qttools5-dev-tools latexmk texlive texlive-latex-extra
- - name: Checkout Source
- uses: actions/checkout@v4
+ - name: Install UV and Python
+ uses: astral-sh/setup-uv@v6
+ with:
+ python-version: "3.13"
+ enable-cache: true
- - name: Install Packages (pip)
- run: pip install -U -r requirements.txt -r docs/requirements.txt
+ - name: Sync UV
+ run: |
+ uv sync --no-dev --group docs
- name: Build Assets
run: |
- python pkgutils.py build-assets
- python pkgutils.py icons optional
+ uv run --no-sync pkgutils.py build-assets
+ uv run --no-sync pkgutils.py icons optional
- name: Upload Artifacts
uses: actions/upload-artifact@v4
diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml
index 64f13d21..b1e62800 100644
--- a/.github/workflows/build_linux.yml
+++ b/.github/workflows/build_linux.yml
@@ -17,7 +17,7 @@ jobs:
LINUX_ARCH: "x86_64"
steps:
- name: Python Setup
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: "3.13"
architecture: x64
@@ -31,7 +31,7 @@ jobs:
run: pip install python-appimage setuptools
- name: Checkout Source
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Download Artifacts
uses: actions/download-artifact@v4
diff --git a/.github/workflows/build_mac.yml b/.github/workflows/build_mac.yml
index 0b08977c..1d708561 100644
--- a/.github/workflows/build_mac.yml
+++ b/.github/workflows/build_mac.yml
@@ -15,7 +15,7 @@ jobs:
MINICONDA_ARCH: x86_64
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Download Artifacts
uses: actions/download-artifact@v4
@@ -46,7 +46,7 @@ jobs:
MINICONDA_ARCH: arm64
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Download Artifacts
uses: actions/download-artifact@v4
diff --git a/.github/workflows/build_win.yml b/.github/workflows/build_win.yml
index b9e96e9d..5c7bc616 100644
--- a/.github/workflows/build_win.yml
+++ b/.github/workflows/build_win.yml
@@ -8,16 +8,16 @@ jobs:
buildWin64:
needs: buildAssets
- runs-on: windows-latest
+ runs-on: windows-2022
steps:
- name: Python Setup
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: "3.13"
architecture: x64
- name: Checkout Source
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Download Assets
uses: actions/download-artifact@v4
diff --git a/.github/workflows/build_win_launcher.yml b/.github/workflows/build_win_launcher.yml
index 901f8a0d..59314e25 100644
--- a/.github/workflows/build_win_launcher.yml
+++ b/.github/workflows/build_win_launcher.yml
@@ -7,7 +7,7 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout Source
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Build Launcher
id: build
diff --git a/.github/workflows/i18n.yml b/.github/workflows/i18n.yml
index 45d91b07..238ae27a 100644
--- a/.github/workflows/i18n.yml
+++ b/.github/workflows/i18n.yml
@@ -10,23 +10,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Python Setup
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: "3.13"
architecture: x64
- - name: Install Packages (apt)
+
+ - name: Checkout Source
+ uses: actions/checkout@v5
+
+ - name: Install System Packages
run: |
sudo apt update
sudo apt install qttools5-dev-tools
- - name: Checkout Source
- uses: actions/checkout@v4
+
- name: Build Assets
run: python pkgutils.py qtlrelease
+
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: nw-i18n
- path: |
- novelwriter/assets/i18n/*.qm
+ path: novelwriter/assets/i18n/*.qm
if-no-files-found: error
retention-days: 7
diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml
index e66f3104..f3d723f9 100644
--- a/.github/workflows/syntax.yml
+++ b/.github/workflows/syntax.yml
@@ -14,24 +14,29 @@ jobs:
checkSyntax:
runs-on: ubuntu-latest
steps:
- - name: Python Setup
- uses: actions/setup-python@v5
- with:
- python-version: 3
- architecture: x64
- name: Checkout Source
- uses: actions/checkout@v4
- - name: Install Dependencies
- run: pip install -r requirements.txt -r requirements-dev.txt
+ uses: actions/checkout@v5
+
+ - name: Install UV and Python
+ uses: astral-sh/setup-uv@v6
+ with:
+ enable-cache: true
+
+ - name: Sync UV
+ run: |
+ uv sync --no-dev --group lint
+
- name: Ruff Check
run: |
- ruff --version
- ruff check
+ uv run --no-sync ruff --version
+ uv run --no-sync ruff check
+
- name: Pyright Check
run: |
- pyright --version
- pyright
+ uv run --no-sync pyright --version
+ uv run --no-sync pyright
+
- name: Isort Check
run: |
- isort --version
- isort --check .
+ uv run --no-sync isort --version
+ uv run --no-sync isort --check .
diff --git a/.github/workflows/test_linux.yml b/.github/workflows/test_linux.yml
index b13e19c1..1f84fa95 100644
--- a/.github/workflows/test_linux.yml
+++ b/.github/workflows/test_linux.yml
@@ -14,33 +14,44 @@ jobs:
testLinux:
strategy:
matrix:
- python-version: ["3.10", "3.11", "3.12", "3.13"]
+ python-version:
+ - "3.11"
+ - "3.12"
+ - "3.13"
+ - "3.14"
fail-fast: false
runs-on: ubuntu-latest
steps:
- - name: Python Setup
- uses: actions/setup-python@v5
- with:
- python-version: ${{ matrix.python-version }}
- architecture: x64
- - name: Install Packages (apt)
+ - name: Checkout Source
+ uses: actions/checkout@v5
+
+ - name: Install System Packages
run: |
sudo apt update
- sudo apt install libenchant-2-dev qttools5-dev-tools
- - name: Checkout Source
- uses: actions/checkout@v4
- - name: Install Dependencies (pip)
+ sudo apt install qttools5-dev-tools
+
+ - name: Install UV and Python
+ uses: astral-sh/setup-uv@v6
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Sync UV
run: |
- pip install -U -r requirements.txt -r tests/requirements.txt
- - name: Run Build Commands
+ uv sync --no-dev --group test
+
+ - name: Build Assets
run: |
- python pkgutils.py qtlrelease
- python pkgutils.py sample
+ uv run --no-sync pkgutils.py qtlrelease
+ uv run --no-sync pkgutils.py sample
+
- name: Run Tests
run: |
export QT_QPA_PLATFORM=offscreen
- python -m pytest -v --cov=novelwriter --timeout=60
+ uv run --no-sync coverage run -m pytest -v --timeout=60
+ uv run --no-sync coverage xml
+
- name: Upload to Codecov
uses: codecov/codecov-action@v5
- env:
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ fail_ci_if_error: true
diff --git a/.github/workflows/test_mac.yml b/.github/workflows/test_mac.yml
index 8465f75b..d191ba4b 100644
--- a/.github/workflows/test_mac.yml
+++ b/.github/workflows/test_mac.yml
@@ -12,26 +12,32 @@ on:
jobs:
testMac:
- runs-on: macos-13
+ runs-on: macos-latest
steps:
- - name: Python Setup
- uses: actions/setup-python@v5
- with:
- python-version: "3.13"
- architecture: x64
- - name: Install Packages (brew)
+ - name: Checkout Source
+ uses: actions/checkout@v5
+
+ - name: Install System Packages
run: |
brew install enchant
- - name: Checkout Source
- uses: actions/checkout@v4
- - name: Install Dependencies (pip)
+
+ - name: Install UV and Python
+ uses: astral-sh/setup-uv@v6
+ with:
+ python-version: "3.13"
+
+ - name: Sync UV
run: |
- pip install -U pyobjc -r requirements.txt -r tests/requirements.txt
+ uv sync --no-dev --group test --group macos
+
- name: Run Tests
run: |
export QT_QPA_PLATFORM=offscreen
- python -m pytest -v --cov=novelwriter --timeout=60
+ uv run --no-sync coverage run -m pytest -v --timeout=60
+ uv run --no-sync coverage xml
+
- name: Upload to Codecov
uses: codecov/codecov-action@v5
- env:
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ fail_ci_if_error: true
diff --git a/.github/workflows/test_win.yml b/.github/workflows/test_win.yml
index f2887216..1cb2dbdd 100644
--- a/.github/workflows/test_win.yml
+++ b/.github/workflows/test_win.yml
@@ -14,20 +14,25 @@ jobs:
testWin:
runs-on: windows-latest
steps:
- - name: Python Setup
- uses: actions/setup-python@v5
+ - name: Checkout Source
+ uses: actions/checkout@v5
+
+ - name: Install UV and Python
+ uses: astral-sh/setup-uv@v6
with:
python-version: "3.13"
- architecture: x64
- - name: Checkout Source
- uses: actions/checkout@v4
- - name: Install Dependencies (pip)
+
+ - name: Sync UV
run: |
- pip install -U -r requirements.txt -r tests/requirements.txt
+ uv sync --no-dev --group test
+
- name: Run Tests
run: |
- python -m pytest -v --cov=novelwriter --timeout=60
+ uv run --no-sync coverage run -m pytest -v --timeout=60
+ uv run --no-sync coverage xml
+
- name: Upload to Codecov
uses: codecov/codecov-action@v5
- env:
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ fail_ci_if_error: true
diff --git a/.gitignore b/.gitignore
index 833d4182..5b91bed7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ setup.iss
/setup/macos/Info.plist
/setup/windows/build
.venv
+/uv.lock
# Translations
/novelwriter/assets/i18n/*.qm
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1dc17163..d68344bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,28 @@
# novelWriter Changelog
+## Version 2.7.5 [2025-09-14]
+
+### Release Notes
+
+This is a patch release that fixes an issue related to crashes when using the completer menu under
+certain conditions, and improves positioning of the input box for CJK languages.
+
+### Detailed Changelog
+
+**Bugfixes**
+
+* Fixes an issue where the app would crash of deleting the `@` character with the completer menu
+ visible and the text margins of the editor set to "justified". This is likely crashing due to
+ some unhandled corner case in the Qt library, but the implementation of the completer menu in
+ novelWriter uses a small hack to bypass some intended behaviour of the menu. Extra steps have
+ been added to the implementation that seems to avoid the crash. Issue #2510. PR #2511.
+* Fixes an issue where the input box that shows up when typing CJK languages were covering the text
+ due to an incorrect offset of the box location. The incorrect offset is caused by the text
+ margins not being taken into account. Fix by @Euophrys based on solution by @Jack-name.
+ Issues #2267 and #2517. PR #2518.
+
+----
+
## Version 2.7.4 [2025-07-15]
### Release Notes
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index abd8b319..1ddeaed1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,6 +12,8 @@ just make a pull request directly.
* Bugfixes for new or existing bugs. Please also report new bugs in the issue tracker even if you
also provide a fix. It makes it easier to keep track of what has been fixed and when.
* Translations made via the [Crowdin project page](https://crowdin.com/project/novelwriter).
+* Translations of the documentation. These need to use Sphinx i18n tooling. Please start a
+ discussion before beginning such work as it requires some coordination.
* Improvements to the documentation. Particularly if the documentation is unclear. Please don't
make any larger changes to the documentation without discussing them with the maintainer first.
* Adaptations, installation or packaging features targeting specific operating systems.
@@ -21,6 +23,34 @@ just make a pull request directly.
* Make a pull request that restructures or reformats existing code. If you think some part of the
code could be improved, please make an issue thread or start a discussion. The same applies to
any text document in the repository.
+* Make pull requests with AI generated code. This is not a project suitable for vibe coding.
+ Outright slop will result in the account being blocked.
+
+This project uses [uv](https://docs.astral.sh/uv/) as its main developer tool. In order to run
+novelWriter directly from checked out source, simply call from the root folder:
+
+```bash
+uv run novelwriter
+```
+
+Many tasks like building assets from source are handled by the `pkgutils.py` helper tool.
+
+```bash
+uv run pkgutils.py --help
+```
+
+The translation files needed at runtime can be built with:
+
+```bash
+uv run pkgutils.py qtlrelease
+```
+
+Material design icons are included with the source. Optional icon themes can be built with:
+
+```bash
+uv run pkgutils.py icons optional
+```
+
## Picking the Correct Branch for a Pull Request
@@ -33,6 +63,11 @@ New features are only accepted on full releases, so a feature pull request must
`main` branch. However, if the `main` branch is very close to a new full release, pull requests may
not be merged until the release is completed.
+This project uses GitHub milestones to plan releases, and only pull requests included in the
+current release cycle will be merged to `main`. Milestone tickets are not set in stone and are
+often moved between them.
+
+
## Pull Request Check List
Make sure the pull request follows these rules:
@@ -41,32 +76,45 @@ Make sure the pull request follows these rules:
own fork from the current `main` branch. Do not make pull requests from your copy of the `main`
branch.
* Please provide a description of the changes in the pull request under the summary section of the
- pull request template, and reference any related issues by providing the issue number.
+ pull request template, and reference any related issues by providing the issue number. Do not
+ post links to issue numbers as that breaks the integration. Stating the issue number is enough.
* Do not change the version number.
* Do not submit files that were not actively changed but have otherwise been modified. This is
- mostly an issue with translation files. The language tool may update all files in the `i18n`
- folder.
+ particularly an issue with autoformatting.
+
## General Rules
These are the guidelines for the project. The source code of novelWriter broadly follows the
[PEP8](https://www.python.org/dev/peps/pep-0008) style guide, but with a few exceptions.
+The project uses [ruff](https://docs.astral.sh/ruff/) for linting, but the auto-formatter should
+not be used at this point. It also uses [isort](https://pycqa.github.io/isort) for import sorting.
+The latter can be auto-formatted and the settings are defined in ``pyproject.toml`.
+
+
### Tests
* New code must not break any existing tests.
* New code must come with tests that cover the code in full. If the code has branches that only
- runs on some OSes, they must be covered when test are run on that OS. The test suite runs on
- Linux, Windows and MacOS.
+ runs on some OSes, they only need to be covered when test are run on that OS. The test suite runs
+ on Linux, Windows and MacOS.
+
+A helper script is provided for running tests. It simplifies coverage reporting and a few other
+things. Run the following to see all details:
+
+```bash
+uv run run_tests.py --help
+```
+
### Code Formatting
-* Do not run automatic formatting tools like `black` or `ruff` on the code. Auto-formatting using
- `ruff` is planned, but there are a couple of features missing in it, so it is currently only used
- for linting. Auto-formatting with `isort` is configured in `pyproject.toml` and can be used.
-* The pull request code *must* pass the `ruff` linting rules specified in `pyproject.toml`.
+* The pull request code *must* pass the `ruff` and `isort` linting rules specified in
+ `pyproject.toml`.
* In general, do not make large scale formatting changes to the code.
+
### Type Annotations
* All functions and parameters must be type annotated, and so must variables and attributes if the
@@ -77,6 +125,7 @@ These are the guidelines for the project. The source code of novelWriter broadly
* Do not use deprecated capitalised annotations like `Dict`, `List`, `Tuple`, etc.
* Type annotated code must be runnable on all supported Python versions.
+
### Internationalisation
* All comments and docstrings in the code must be in English.
@@ -84,6 +133,7 @@ These are the guidelines for the project. The source code of novelWriter broadly
spelling of this text *must* be UK English. US English spelling is not allowed for these strings.
* Commit descriptions and pull requests must also be in English.
+
### Line Length
* Source code lines can extend to the upper limit of 99 characters. Generally, if a code statement
@@ -92,6 +142,7 @@ These are the guidelines for the project. The source code of novelWriter broadly
* For text files, the text should be wrapped at 99 character. The exception is Markdown image tags
and URLs which can run past that limit.
+
### Spaces, Indentation and Alignment
* Only indentation by multiples of 4 spaces is allowed.
@@ -101,9 +152,11 @@ These are the guidelines for the project. The source code of novelWriter broadly
rule is relaxed a bit here. Alignment is allowed when populating large dictionaries or setting
many class attributes. It does improve readability in such cases, but should not be overused.
+
### General Code Rules
* Use f-string style for string formatting as the first choice, and `.format` functions if there is
a good reason for it. Do not use `%` style formatting except for logging output. For logging, `%`
must be used (it's a limitation in the logging library unfortunately).
-* Functions should be on camelCase form for consistency with the Qt library code.
+* Functions should be on camelCase form for consistency with the Qt library code. This also goes
+ for variable names for the sake of internal consistency.
diff --git a/README.md b/README.md
index bb2cd3e9..ada066c1 100644
--- a/README.md
+++ b/README.md
@@ -12,11 +12,7 @@ novelWriter is a plain text editor designed for writing novels assembled from ma
documents. It uses a minimal formatting syntax inspired by Markdown, and adds a meta data syntax
for comments, synopsis, and cross-referencing. It's designed to be a simple text editor that allows
for easy organisation of text and notes, using human readable text files as storage for robustness.
-
-The project storage is suitable for version control software, and also well suited for file
-synchronisation tools. All text is saved as plain text files with a meta data header. The core
-project structure is stored in a single project XML file. Other meta data is primarily saved as
-JSON files.
+The project format is well suited both for version control software and file synchronisation tools.
For more details, and how to install and use novelWriter, please see the main website and
documentation.
@@ -29,6 +25,7 @@ documentation.
* PyPi Project: [pypi.org/project/novelWriter](https://pypi.org/project/novelWriter)
* Social Media: [fosstodon.org/@novelwriter](https://fosstodon.org/@novelwriter)
+
## Sponsors
@@ -38,22 +35,25 @@ documentation.
+
## Implementation
-novelWriter is written with Python and Qt6 with PyQt6 Python binding. It is released on Linux,
-Windows and MacOS. It can in principle run on any Operating System that also supports Qt, PyQt and
-Python.
+novelWriter is written in Python and uses Qt6 with PyQt6 Python binding as the UI framework. It is
+released on Linux, Windows and MacOS. It can in principle run on any Operating System that also
+supports Qt, PyQt and Python.
+
## Project Contributions
Please don't make feature pull requests without first having discussed them with the maintainer.
You can make a feature request in the [issues tracker](https://github.com/vkbo/novelWriter/issues),
or if the idea isn't fully formed, start a [discussion](https://github.com/vkbo/novelWriter/discussions).
-Please also don't make pull requests to reformat or rewrite existing code unless there is a very good reason for doing so.
+Please also don't make pull requests to reformat or rewrite existing code unless there is a very
+good reason for doing so. Please do not submit AI generated content.
Fixes and patches are welcome. Contributions related to packaging and installing novelWriter will
also be appreciated, but please make an issue or a discussion topic first. Before contributing any
@@ -66,6 +66,7 @@ Project credits are available in [CREDITS.md](https://github.com/vkbo/novelWrite
the `release` branch. So if you're submitting a fix to a current release, **including changes to
documentation**, they must be made to the `release` branch.
+
### Translations
New translations are always welcome. This project uses Crowdin to maintain translations, and you
@@ -73,6 +74,7 @@ can contribute translations at the [Crowdin project page](https://crowdin.com/pr
If you have any questions, feel free to post them to the
[Translations of novelWriter](https://github.com/vkbo/novelWriter/issues/93) issue thread.
+
## Licence
This is Open Source software, and novelWriter is licenced under GPLv3. See the
diff --git a/docs/requirements.txt b/docs/requirements.txt
deleted file mode 100644
index 767647e7..00000000
--- a/docs/requirements.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-docutils>=0.17.1
-pygments>=2.7
-sphinx-book-theme
-sphinx-copybutton
-sphinx-design
-sphinx-favicon
-sphinx-intl
-sphinx>=5.0
diff --git a/docs/source/more/customise.rst b/docs/source/more/customise.rst
index 89a74ba0..ba016d30 100644
--- a/docs/source/more/customise.rst
+++ b/docs/source/more/customise.rst
@@ -109,6 +109,7 @@ A colour theme ``.conf`` file consists of the following settings:
headertext = green
headertag = green:L135
emphasis = orange
+ whitespace = orange:64
dialog = blue
altdialog = red
note = yellow:D125
@@ -173,9 +174,10 @@ There are several ways to enter colour values:
.. versionadded:: 2.8
The ``[Syntax]`` section was moved into the main theme file. Previously, these settings were in
- their own file. The ``[Icons]`` section was renamed to ``[Base]``. Added the ``line`` setting.
- Dropped the ``license``, ``licenseurl``, and ``description`` settings. The ``author`` field
- is now required if the theme is included in the app, but not for user themes.
+ their own file. The ``[Icons]`` section was renamed to ``[Base]``. Added the ``line`` and
+ ``whitespace`` settings. Dropped the ``license``, ``licenseurl``, and ``description`` settings.
+ The ``author`` field is now required if the theme is included in the app, but not for user
+ themes.
Icon Themes
diff --git a/docs/source/technical/source.rst b/docs/source/technical/source.rst
index 6b69b491..c5bce486 100644
--- a/docs/source/technical/source.rst
+++ b/docs/source/technical/source.rst
@@ -7,6 +7,7 @@ Running from Source
.. _GitHub: https://github.com/vkbo/novelWriter/releases
.. _PyPi: https://pypi.org/project/novelWriter/
.. _Sphinx Docs: https://www.sphinx-doc.org/
+.. _uv: https://docs.astral.sh/uv/
This chapter describes various ways of running novelWriter directly from the source code, and how
to build the various components like the translation files and documentation.
@@ -28,6 +29,7 @@ by running:
.. _docs_technical_source_depend:
+
Dependencies
============
@@ -43,11 +45,20 @@ The following Python packages are needed to run all features of novelWriter:
If you want spell checking, you must install the ``PyEnchant`` package. The spell check library
must be at least 3.0 to work with Windows. On Linux, 2.0 also works fine.
-If you install from PyPi, these dependencies should be installed automatically. If you install from
-source, dependencies can still be installed from PyPi with:
+If you install novelWriter from PyPi, these dependencies should be installed automatically.
+
+You can run novelWriter directly from source with uv_:
.. code-block:: bash
+ uv run novelwriter
+
+If you prefer to install dependencies using ``pip``, you must first generate the
+``requirements.txt`` file:
+
+.. code-block:: bash
+
+ python pkgutils.py gen-req
pip install -r requirements.txt
.. note::
@@ -136,12 +147,14 @@ running:
Building the Documentation
==========================
-A local copy of this documentation can be generated as HTML. This requires installing some Python
-packages from PyPi:
+A local copy of this documentation can be generated as HTML.
+
+If you're using ``pip``, you must first generate the ``requirements.txt`` file:
.. code-block:: bash
- pip install -r docs/requirements.txt
+ python pkgutils.py gen-req docs
+ pip install -r requirements.txt
The documentation can then be built from the root folder in the source code by running:
@@ -149,6 +162,12 @@ The documentation can then be built from the root folder in the source code by r
make -C docs html
+Or you can run directly with uv_:
+
+.. code-block:: bash
+
+ uv run make -C docs html
+
If successful, the documentation should be available in the ``docs/build/html`` folder and you can
open the ``index.html`` file in your browser.
diff --git a/docs/source/technical/tests.rst b/docs/source/technical/tests.rst
index 56623ff4..532bd750 100644
--- a/docs/source/technical/tests.rst
+++ b/docs/source/technical/tests.rst
@@ -4,23 +4,12 @@
Running Tests
*************
+.. _uv: https://docs.astral.sh/uv/
+
The novelWriter source code is well covered by tests. The test framework used for the development
is ``pytest`` with the use of an extension for Qt.
-Dependencies
-============
-
-The dependencies for running the tests can be installed with:
-
-.. code-block:: bash
-
- pip install -r tests/requirements.txt
-
-This will install a couple of extra packages for coverage and test management. The minimum
-requirement is ``pytest`` and ``pytest-qt``.
-
-
Simple Test Run
===============
@@ -28,19 +17,35 @@ To run the tests, you simply need to execute the following from the root of the
.. code-block:: bash
- pytest
+ uv run pytest
+
+This uses uv_. See below for manually installing dependencies using ``pip``.
Since several of the tests involve opening up the novelWriter GUI, you may want to disable the GUI
for the duration of the test run. Moving your mouse while the tests are running may otherwise
interfere with the execution of some tests.
-You can disable the renderring of the GUI by setting the flag ``QT_QPA_PLATFORM=offscreen``:
+You can disable the rendering of the GUI by setting the flag ``QT_QPA_PLATFORM=offscreen``:
.. code-block:: bash
export QT_QPA_PLATFORM=offscreen pytest
+Dependencies
+------------
+
+To generate the requirements file and install dependencies using ``pip``, run:
+
+.. code-block:: bash
+
+ python pkgutils.py gen-req app test
+ pip install -r tests/requirements.txt
+
+This will install a couple of extra packages for coverage and test management. The minimum
+requirement is ``pytest`` and ``pytest-qt``.
+
+
Advanced Options
================
diff --git a/docs/source/usage/introduction.rst b/docs/source/usage/introduction.rst
index 47dceb98..0dc59f96 100644
--- a/docs/source/usage/introduction.rst
+++ b/docs/source/usage/introduction.rst
@@ -6,8 +6,8 @@ Introduction
.. _Markdown: https://en.wikipedia.org/wiki/Markdown
-In a nutshell, novelWriter is a plain text editor that lets you organise one or more novels and
-associated notes as many smaller documents. You can at any time generate standard document formats
+In a nutshell, novelWriter is a plain text editor that lets you organise one or more novels, and
+associated notes, as many smaller documents. You can at any time generate standard document formats
from these plain text documents. Whether it is an outline of your story, a draft, a complete
manuscript, or even a collection of your character notes or other notes.
@@ -51,7 +51,7 @@ comments, and an auto-complete menu can help you here too. More about this later
writing. It is also *not* a full-featured Markdown editor.
In addition, novelWriter is not intended as a tool for organising research for writing, and
- therefore lacks formatting features you may need for this purpose. The notes feature in is
+ therefore lacks formatting features you may need for this purpose. The notes feature is
mainly intended for character profiles and plot outlines. It is recommended to use a proper
note-taking tool for research. This is anyway more practical as you may use the same research
for multiple projects.
diff --git a/docs/source/usage/organising_project.rst b/docs/source/usage/organising_project.rst
index 4f4e3c9b..a3809d7a 100644
--- a/docs/source/usage/organising_project.rst
+++ b/docs/source/usage/organising_project.rst
@@ -15,7 +15,7 @@ side of the main window.
Each line in the project tree shows the name of each item, its word count (or alternatively
character count), an icon for :ref:`docs_usage_project_active`, and a custom icon for
-:ref:`docs_usage_project_status` of each item. These latter two are covered alter in this section.
+:ref:`docs_usage_project_status` of each item. These latter two are covered later in this section.
You can add, view and edit documents in the project tree by right-clicking on them. Some features
are also located in the buttons along the top, next to the **Project Content** label.
@@ -47,8 +47,8 @@ Root Folder Types
**Novel** (Story)
This is where you put the documents that are part of your story. You can create multiple Novel
- folders if you wish, but various parts of the application assumes each Novel folder belong to
- one novel.
+ folders if you wish, but various parts of the application assumes each Novel folder belongs to
+ only one novel.
The Novel folder is somewhat special in that it can contain documents for chapters, scenes and
story partitions. How this is indicated is covered in the section :ref:`docs_usage_headings`.
diff --git a/i18n/README.md b/i18n/README.md
index b2df46f0..6b8ce27f 100644
--- a/i18n/README.md
+++ b/i18n/README.md
@@ -114,23 +114,6 @@ You can now test the translation in novelWriter. The Preferences dialog should l
language, so go ahead and select it.
-### Missing QtBase Translations
-
-The default Qt dialogs also have translations, for instance for standard buttons like "Yes", "No",
-"Ok", "Cancel", etc. Generally, these translation files are installed with the Qt libraries on your
-system, and novelWriter will collect those translations from there. However, these translations are
-missing for many languages.
-
-As a starting point, there is no need to translate any entries in the `.ts` files that are under
-elements starting with the letter "Q", like "QPlatformTheme", "QWizard", etc. If these turn up in
-English in novelWriter after activating a translation, it means they are probably missing in the Qt
-library, and you may also need to translate these.
-
-These additional translation entries are generated from a file named `i18n/qtbase.py`, which is not
-a file that novelWriter uses. It is there only to generate these additional entries for the `.ts`
-files.
-
-
## Project Localisation
Projects can have a different language setting than the GUI itself. The files with format
diff --git a/i18n/qtbase.py b/i18n/qtbase.py
deleted file mode 100644
index e40279cd..00000000
--- a/i18n/qtbase.py
+++ /dev/null
@@ -1,49 +0,0 @@
-"""
-Qt Base Translation File
-========================
-
-This file causes Qt Linguist to generate translation entries for the Qt
-elements that need translation in novelWriter for those languages who do
-not yet have a qtbase_xx.qm file shipped with Qt.
-
-If a qtbase_xx.qm file already exists, do not add a translation for the
-entries generated from this file.
-""" # noqa
-
-from PyQt6.QtCore import QT_TRANSLATE_NOOP
-
-# QDialogButtonBox
-# ================
-
-QT_TRANSLATE_NOOP("QDialogButtonBox", "OK")
-
-# QGnomeTheme
-# ===========
-
-QT_TRANSLATE_NOOP("QGnomeTheme", "&OK")
-QT_TRANSLATE_NOOP("QGnomeTheme", "&Save")
-QT_TRANSLATE_NOOP("QGnomeTheme", "&Cancel")
-QT_TRANSLATE_NOOP("QGnomeTheme", "&Close")
-QT_TRANSLATE_NOOP("QGnomeTheme", "Close without Saving")
-
-# QPlatformTheme
-# ==============
-
-QT_TRANSLATE_NOOP("QPlatformTheme", "OK")
-QT_TRANSLATE_NOOP("QPlatformTheme", "Save")
-QT_TRANSLATE_NOOP("QPlatformTheme", "Save All")
-QT_TRANSLATE_NOOP("QPlatformTheme", "Open")
-QT_TRANSLATE_NOOP("QPlatformTheme", "&Yes")
-QT_TRANSLATE_NOOP("QPlatformTheme", "Yes to &All")
-QT_TRANSLATE_NOOP("QPlatformTheme", "&No")
-QT_TRANSLATE_NOOP("QPlatformTheme", "N&o to All")
-QT_TRANSLATE_NOOP("QPlatformTheme", "Abort")
-QT_TRANSLATE_NOOP("QPlatformTheme", "Retry")
-QT_TRANSLATE_NOOP("QPlatformTheme", "Ignore")
-QT_TRANSLATE_NOOP("QPlatformTheme", "Close")
-QT_TRANSLATE_NOOP("QPlatformTheme", "Cancel")
-QT_TRANSLATE_NOOP("QPlatformTheme", "Discard")
-QT_TRANSLATE_NOOP("QPlatformTheme", "Help")
-QT_TRANSLATE_NOOP("QPlatformTheme", "Apply")
-QT_TRANSLATE_NOOP("QPlatformTheme", "Reset")
-QT_TRANSLATE_NOOP("QPlatformTheme", "Restore Defaults")
diff --git a/novelWriter.py b/novelWriter.py
index 797dfe23..9758edee 100755
--- a/novelWriter.py
+++ b/novelWriter.py
@@ -6,14 +6,6 @@ novelWriter – Start Script
import os
import sys
-try:
- import PyQt6.QtCore
- import PyQt6.QtGui
- import PyQt6.QtWidgets # noqa: F401
-except Exception:
- print("ERROR: Failed to load dependency PyQt6")
- sys.exit(1)
-
os.curdir = os.path.abspath(os.path.dirname(__file__))
if __name__ == "__main__":
diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py
index fa3dd30e..17c0298e 100644
--- a/novelwriter/__init__.py
+++ b/novelwriter/__init__.py
@@ -49,9 +49,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
-__version__ = "2.8a2"
-__hexversion__ = "0x020800a2"
-__date__ = "2025-07-16"
+__version__ = "2.8a3"
+__hexversion__ = "0x020800a3"
+__date__ = "2025-10-18"
__status__ = "Stable"
__domain__ = "novelwriter.io"
@@ -206,9 +206,9 @@ def main(sysArgs: list | None = None) -> GuiMain | None:
# Check Packages and Versions
errorData = []
errorCode = 0
- if sys.hexversion < 0x030a00f0:
+ if sys.hexversion < 0x030b00f0:
errorData.append(
- f"At least Python 3.10 is required, found {CONFIG.verPyString}"
+ f"At least Python 3.11 is required, found {CONFIG.verPyString}"
)
errorCode |= 0x04
if CONFIG.verQtValue < 0x060400:
diff --git a/novelwriter/assets/icons/material_filled_normal.icons b/novelwriter/assets/icons/material_filled_normal.icons
index 36b8536f..7b58f1a4 100644
--- a/novelwriter/assets/icons/material_filled_normal.icons
+++ b/novelwriter/assets/icons/material_filled_normal.icons
@@ -59,6 +59,23 @@ icon:sb_stats =
icon:theme_dark =
icon:theme_auto =
+icon:btn_ok =
+icon:btn_cancel =
+icon:btn_yes =
+icon:btn_no =
+icon:btn_open =
+icon:btn_close =
+icon:btn_save =
+icon:btn_browse =
+icon:btn_list =
+icon:btn_new =
+icon:btn_create =
+icon:btn_reset =
+icon:btn_insert =
+icon:btn_apply =
+icon:btn_build =
+icon:btn_print =
+icon:btn_preview =
icon:add =
icon:bookmarks =
icon:browse =
@@ -94,7 +111,6 @@ icon:minimise =
icon:more_vertical =
icon:noncheckable =
-icon:open =
icon:panel =
icon:pin =
icon:project_copy =
@@ -103,7 +119,6 @@ icon:refresh =
icon:revert =
icon:settings =
-icon:star =
icon:stats =
icon:text =
icon:timer_off =
diff --git a/novelwriter/assets/icons/material_filled_thin.icons b/novelwriter/assets/icons/material_filled_thin.icons
index a405ea3e..0bf9758b 100644
--- a/novelwriter/assets/icons/material_filled_thin.icons
+++ b/novelwriter/assets/icons/material_filled_thin.icons
@@ -59,6 +59,23 @@ icon:sb_stats =
icon:theme_dark =
icon:theme_auto =
+icon:btn_ok =
+icon:btn_cancel =
+icon:btn_yes =
+icon:btn_no =
+icon:btn_open =
+icon:btn_close =
+icon:btn_save =
+icon:btn_browse =
+icon:btn_list =
+icon:btn_new =
+icon:btn_create =
+icon:btn_reset =
+icon:btn_insert =
+icon:btn_apply =
+icon:btn_build =
+icon:btn_print =
+icon:btn_preview =
icon:add =
icon:bookmarks =
icon:browse =
@@ -94,7 +111,6 @@ icon:minimise =
icon:more_vertical =
icon:noncheckable =
-icon:open =
icon:panel =
icon:pin =
icon:project_copy =
@@ -103,7 +119,6 @@ icon:refresh =
icon:revert =
icon:settings =
-icon:star =
icon:stats =
icon:text =
icon:timer_off =
diff --git a/novelwriter/assets/icons/material_rounded_normal.icons b/novelwriter/assets/icons/material_rounded_normal.icons
index 9c22a001..0ff44cbd 100644
--- a/novelwriter/assets/icons/material_rounded_normal.icons
+++ b/novelwriter/assets/icons/material_rounded_normal.icons
@@ -59,6 +59,23 @@ icon:sb_stats =
icon:theme_dark =
icon:theme_auto =
+icon:btn_ok =
+icon:btn_cancel =
+icon:btn_yes =
+icon:btn_no =
+icon:btn_open =
+icon:btn_close =
+icon:btn_save =
+icon:btn_browse =
+icon:btn_list =
+icon:btn_new =
+icon:btn_create =
+icon:btn_reset =
+icon:btn_insert =
+icon:btn_apply =
+icon:btn_build =
+icon:btn_print =
+icon:btn_preview =
icon:add =
icon:bookmarks =
icon:browse =
@@ -94,7 +111,6 @@ icon:minimise =
icon:more_vertical =
icon:noncheckable =
-icon:open =
icon:panel =
icon:pin =
icon:project_copy =
@@ -103,7 +119,6 @@ icon:refresh =
icon:revert =
icon:settings =
-icon:star =
icon:stats =
icon:text =
icon:timer_off =
diff --git a/novelwriter/assets/icons/material_rounded_thin.icons b/novelwriter/assets/icons/material_rounded_thin.icons
index 1002fcbf..b5606512 100644
--- a/novelwriter/assets/icons/material_rounded_thin.icons
+++ b/novelwriter/assets/icons/material_rounded_thin.icons
@@ -59,6 +59,23 @@ icon:sb_stats =
icon:theme_dark =
icon:theme_auto =
+icon:btn_ok =
+icon:btn_cancel =
+icon:btn_yes =
+icon:btn_no =
+icon:btn_open =
+icon:btn_close =
+icon:btn_save =
+icon:btn_browse =
+icon:btn_list =
+icon:btn_new =
+icon:btn_create =
+icon:btn_reset =
+icon:btn_insert =
+icon:btn_apply =
+icon:btn_build =
+icon:btn_print =
+icon:btn_preview =
icon:add =
icon:bookmarks =
icon:browse =
@@ -94,7 +111,6 @@ icon:minimise =
icon:more_vertical =
icon:noncheckable =
-icon:open =
icon:panel =
icon:pin =
icon:project_copy =
@@ -103,7 +119,6 @@ icon:refresh =
icon:revert =
icon:settings =
-icon:star =
icon:stats =
icon:text =
icon:timer_off =
diff --git a/novelwriter/assets/icons/material_sharp_normal.icons b/novelwriter/assets/icons/material_sharp_normal.icons
index b8b8c0e6..a9b6ff25 100644
--- a/novelwriter/assets/icons/material_sharp_normal.icons
+++ b/novelwriter/assets/icons/material_sharp_normal.icons
@@ -59,6 +59,23 @@ icon:sb_stats =
icon:theme_dark =
icon:theme_auto =
+icon:btn_ok =
+icon:btn_cancel =
+icon:btn_yes =
+icon:btn_no =
+icon:btn_open =
+icon:btn_close =
+icon:btn_save =
+icon:btn_browse =
+icon:btn_list =
+icon:btn_new =
+icon:btn_create =
+icon:btn_reset =
+icon:btn_insert =
+icon:btn_apply =
+icon:btn_build =
+icon:btn_print =
+icon:btn_preview =
icon:add =
icon:bookmarks =
icon:browse =
@@ -94,7 +111,6 @@ icon:minimise =
icon:more_vertical =
icon:noncheckable =
-icon:open =
icon:panel =
icon:pin =
icon:project_copy =
@@ -103,7 +119,6 @@ icon:refresh =
icon:revert =
icon:settings =
-icon:star =
icon:stats =
icon:text =
icon:timer_off =
diff --git a/novelwriter/assets/icons/material_sharp_thin.icons b/novelwriter/assets/icons/material_sharp_thin.icons
index afe8528d..11e04deb 100644
--- a/novelwriter/assets/icons/material_sharp_thin.icons
+++ b/novelwriter/assets/icons/material_sharp_thin.icons
@@ -59,6 +59,23 @@ icon:sb_stats =
icon:theme_dark =
icon:theme_auto =
+icon:btn_ok =
+icon:btn_cancel =
+icon:btn_yes =
+icon:btn_no =
+icon:btn_open =
+icon:btn_close =
+icon:btn_save =
+icon:btn_browse =
+icon:btn_list =
+icon:btn_new =
+icon:btn_create =
+icon:btn_reset =
+icon:btn_insert =
+icon:btn_apply =
+icon:btn_build =
+icon:btn_print =
+icon:btn_preview =
icon:add =
icon:bookmarks =
icon:browse =
@@ -94,7 +111,6 @@ icon:minimise =
icon:more_vertical =
icon:noncheckable =
-icon:open =
icon:panel =
icon:pin =
icon:project_copy =
@@ -103,7 +119,6 @@ icon:refresh =
icon:revert =
icon:settings =
-icon:star =
icon:stats =
icon:text =
icon:timer_off =
diff --git a/novelwriter/assets/themes/aura.conf b/novelwriter/assets/themes/aura.conf
index a31ac7ba..5a62ffb8 100644
--- a/novelwriter/assets/themes/aura.conf
+++ b/novelwriter/assets/themes/aura.conf
@@ -29,6 +29,27 @@ active = cyan
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #0c0c11
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = cyan
headertag = cyan:128
emphasis = purple:L105
+whitespace = purple:64
dialog = cyan
altdialog = blue
note = yellow
diff --git a/novelwriter/assets/themes/aura_bright.conf b/novelwriter/assets/themes/aura_bright.conf
index 651e452c..0a4881b3 100644
--- a/novelwriter/assets/themes/aura_bright.conf
+++ b/novelwriter/assets/themes/aura_bright.conf
@@ -27,6 +27,27 @@ active = cyan
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #e1dae2
windowtext = default
@@ -57,6 +78,7 @@ link = blue
headertext = cyan
headertag = cyan:128
emphasis = purple
+whitespace = purple:64
dialog = cyan:D105
altdialog = blue
note = yellow
diff --git a/novelwriter/assets/themes/aura_soft.conf b/novelwriter/assets/themes/aura_soft.conf
index f8d58f70..a3c3ed00 100644
--- a/novelwriter/assets/themes/aura_soft.conf
+++ b/novelwriter/assets/themes/aura_soft.conf
@@ -29,6 +29,27 @@ active = cyan
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #191924
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = cyan
headertag = cyan:128
emphasis = #a581ec
+whitespace = #a581ec64
dialog = cyan
altdialog = blue
note = yellow
diff --git a/novelwriter/assets/themes/b2t_garden_dark.conf b/novelwriter/assets/themes/b2t_garden_dark.conf
index f5ecceba..c55a0987 100644
--- a/novelwriter/assets/themes/b2t_garden_dark.conf
+++ b/novelwriter/assets/themes/b2t_garden_dark.conf
@@ -29,6 +29,27 @@ active = green
inactive = orange
disabled = #696d69
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #1e1f1e
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = #fbfaf8
headertag = #828782
emphasis = #3fac39
+whitespace = #3fac3964
dialog = #90d98c
altdialog = #4cb946
note = #dd843c
diff --git a/novelwriter/assets/themes/b2t_garden_light.conf b/novelwriter/assets/themes/b2t_garden_light.conf
index ac2287b7..c5a33fed 100644
--- a/novelwriter/assets/themes/b2t_garden_light.conf
+++ b/novelwriter/assets/themes/b2t_garden_light.conf
@@ -29,6 +29,27 @@ active = green
inactive = orange
disabled = #aab1aa
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #ece5df
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = #2b2c2a
headertag = #828782
emphasis = #4cb946
+whitespace = #4cb94664
dialog = #1c8217
altdialog = #3fac39
note = #d97726
diff --git a/novelwriter/assets/themes/b2t_suburb_dark.conf b/novelwriter/assets/themes/b2t_suburb_dark.conf
index 854808ca..a3050b8d 100644
--- a/novelwriter/assets/themes/b2t_suburb_dark.conf
+++ b/novelwriter/assets/themes/b2t_suburb_dark.conf
@@ -29,6 +29,27 @@ active = red
inactive = faded
disabled = #575c79
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #1e212f
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = red
headertag = red:128
emphasis = #ffffff
+whitespace = #ffffff64
dialog = #fe81b5
altdialog = #ffb3d2
note = #a0acfe
diff --git a/novelwriter/assets/themes/b2t_suburb_light.conf b/novelwriter/assets/themes/b2t_suburb_light.conf
index f31c8c29..8c1f7de3 100644
--- a/novelwriter/assets/themes/b2t_suburb_light.conf
+++ b/novelwriter/assets/themes/b2t_suburb_light.conf
@@ -29,6 +29,27 @@ active = red
inactive = faded
disabled = #b6bad1
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #e9e5e7
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = red
headertag = red:128
emphasis = #1e202f
+whitespace = #1e202f64
dialog = red
altdialog = #fb6fa9
note = blue
diff --git a/novelwriter/assets/themes/b4t_classic_o_dark.conf b/novelwriter/assets/themes/b4t_classic_o_dark.conf
index ce775630..1b7e4e6f 100644
--- a/novelwriter/assets/themes/b4t_classic_o_dark.conf
+++ b/novelwriter/assets/themes/b4t_classic_o_dark.conf
@@ -29,6 +29,27 @@ active = default
inactive = faded
disabled = #454f5f
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #191d23
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = cyan
headertag = cyan:128
emphasis = #5fe2d1
+whitespace = #5fe2d164
dialog = #87B4FC
altdialog = #5A96F6
note = #d19af4
diff --git a/novelwriter/assets/themes/b4t_classic_o_light.conf b/novelwriter/assets/themes/b4t_classic_o_light.conf
index 0f9c94a1..60a06c42 100644
--- a/novelwriter/assets/themes/b4t_classic_o_light.conf
+++ b/novelwriter/assets/themes/b4t_classic_o_light.conf
@@ -29,6 +29,27 @@ active = default
inactive = faded
disabled = #acb5c3
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #e7eaee
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = cyan
headertag = cyan:128
emphasis = #008976
+whitespace = #00897664
dialog = #1a68e5
altdialog = #1249a0
note = #c17eed
diff --git a/novelwriter/assets/themes/b4t_modern_c_dark.conf b/novelwriter/assets/themes/b4t_modern_c_dark.conf
index e348fbef..844bdf83 100644
--- a/novelwriter/assets/themes/b4t_modern_c_dark.conf
+++ b/novelwriter/assets/themes/b4t_modern_c_dark.conf
@@ -29,6 +29,27 @@ active = default
inactive = faded
disabled = #5d5f6f
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #1b1c20
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = red
headertag = red:128
emphasis = #f391b6
+whitespace = #f391b664
dialog = yellow
altdialog = #e86296
note = #929ff7
diff --git a/novelwriter/assets/themes/b4t_modern_c_light.conf b/novelwriter/assets/themes/b4t_modern_c_light.conf
index b881167f..c5358a2c 100644
--- a/novelwriter/assets/themes/b4t_modern_c_light.conf
+++ b/novelwriter/assets/themes/b4t_modern_c_light.conf
@@ -29,6 +29,27 @@ active = default
inactive = faded
disabled = #BBBDC9
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #e7e7ed
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = red
headertag = red:128
emphasis = #d53874
+whitespace = #d5387464
dialog = #9f6303
altdialog = #e86296
note = blue
diff --git a/novelwriter/assets/themes/blue_streak_dark.conf b/novelwriter/assets/themes/blue_streak_dark.conf
index 49d19c42..fc14f4be 100644
--- a/novelwriter/assets/themes/blue_streak_dark.conf
+++ b/novelwriter/assets/themes/blue_streak_dark.conf
@@ -29,6 +29,27 @@ active = blue:L150
inactive = blue:D150
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base:L125
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = blue
headertag = blue:D150
emphasis = blue:L150
+whitespace = blue:64
dialog = blue
altdialog = blue:L150
note = blue:L150
diff --git a/novelwriter/assets/themes/blue_streak_light.conf b/novelwriter/assets/themes/blue_streak_light.conf
index 9f52d1a6..52e8ce73 100644
--- a/novelwriter/assets/themes/blue_streak_light.conf
+++ b/novelwriter/assets/themes/blue_streak_light.conf
@@ -29,6 +29,27 @@ active = blue:L115
inactive = blue:D150
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base:D105
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = blue
headertag = blue:D150
emphasis = blue:L125
+whitespace = blue:64
dialog = blue
altdialog = blue:L125
note = blue:L125
diff --git a/novelwriter/assets/themes/castle_day.conf b/novelwriter/assets/themes/castle_day.conf
index ab57dcd8..a860caa5 100644
--- a/novelwriter/assets/themes/castle_day.conf
+++ b/novelwriter/assets/themes/castle_day.conf
@@ -27,6 +27,27 @@ active = default
inactive = faded
disabled = #b4aca5
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base:D110
windowtext = default
@@ -57,6 +78,7 @@ link = blue
headertext = yellow
headertag = yellow:160
emphasis = #aa791e
+whitespace = #aa791e64
dialog = green:D110
altdialog = #378b8b
note = faded
diff --git a/novelwriter/assets/themes/castle_night.conf b/novelwriter/assets/themes/castle_night.conf
index c4245236..90532b3b 100644
--- a/novelwriter/assets/themes/castle_night.conf
+++ b/novelwriter/assets/themes/castle_night.conf
@@ -27,6 +27,27 @@ active = default
inactive = faded
disabled = #4b4f5c
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base:D130
windowtext = default
@@ -57,6 +78,7 @@ link = blue
headertext = yellow
headertag = yellow:160
emphasis = yellow
+whitespace = yellow:64
dialog = green
altdialog = cyan
note = faded
diff --git a/novelwriter/assets/themes/chalky_soil.conf b/novelwriter/assets/themes/chalky_soil.conf
index 5e90b992..16e89018 100644
--- a/novelwriter/assets/themes/chalky_soil.conf
+++ b/novelwriter/assets/themes/chalky_soil.conf
@@ -27,6 +27,27 @@ active = green
inactive = faded
disabled = faded:L135
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base:D108
windowtext = default
@@ -57,6 +78,7 @@ link = blue
headertext = red
headertag = red:128
emphasis = #488843
+whitespace = #48884364
dialog = #b95a00
altdialog = #b18010
note = faded
diff --git a/novelwriter/assets/themes/chernozem.conf b/novelwriter/assets/themes/chernozem.conf
index 56f80f66..ea6a74a8 100644
--- a/novelwriter/assets/themes/chernozem.conf
+++ b/novelwriter/assets/themes/chernozem.conf
@@ -27,6 +27,27 @@ active = green
inactive = faded
disabled = #504742
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #241d1d
windowtext = default
@@ -57,6 +78,7 @@ link = blue
headertext = red
headertag = red:128
emphasis = #85c47f
+whitespace = #85c47f64
dialog = orange
altdialog = yellow
note = faded
diff --git a/novelwriter/assets/themes/cyberpunk_night.conf b/novelwriter/assets/themes/cyberpunk_night.conf
index d433dabe..1d5c4ce0 100644
--- a/novelwriter/assets/themes/cyberpunk_night.conf
+++ b/novelwriter/assets/themes/cyberpunk_night.conf
@@ -28,6 +28,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base
windowtext = #969696
@@ -58,6 +79,7 @@ link = blue
headertext = #ffffff
headertag = purple
emphasis = cyan
+whitespace = cyan:64
dialog = green
altdialog = #008cff
note = #969696
diff --git a/novelwriter/assets/themes/default_dark.conf b/novelwriter/assets/themes/default_dark.conf
index 1f7359bb..6db770f1 100644
--- a/novelwriter/assets/themes/default_dark.conf
+++ b/novelwriter/assets/themes/default_dark.conf
@@ -29,6 +29,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base:L125
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = green
headertag = green:D150
emphasis = orange
+whitespace = orange:64
dialog = blue
altdialog = red
note = yellow
diff --git a/novelwriter/assets/themes/default_light.conf b/novelwriter/assets/themes/default_light.conf
index da7784f5..c9ef8dc7 100644
--- a/novelwriter/assets/themes/default_light.conf
+++ b/novelwriter/assets/themes/default_light.conf
@@ -29,6 +29,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base:D105
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = green
headertag = green:L135
emphasis = orange
+whitespace = orange:64
dialog = blue
altdialog = red
note = yellow:D125
diff --git a/novelwriter/assets/themes/dracula.conf b/novelwriter/assets/themes/dracula.conf
index 04fe9949..080e6f96 100644
--- a/novelwriter/assets/themes/dracula.conf
+++ b/novelwriter/assets/themes/dracula.conf
@@ -45,6 +45,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #44475a
windowtext = #f8f8f2
@@ -75,6 +96,7 @@ link = #ff79c6
headertext = purple
headertag = purple:D150
emphasis = orange
+whitespace = orange:64
dialog = green
altdialog = yellow
note = #ffcce9
diff --git a/novelwriter/assets/themes/espresso.conf b/novelwriter/assets/themes/espresso.conf
index 3d222896..54a166f7 100644
--- a/novelwriter/assets/themes/espresso.conf
+++ b/novelwriter/assets/themes/espresso.conf
@@ -29,6 +29,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base:L125
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = yellow:L125
headertag = faded
emphasis = orange
+whitespace = orange:64
dialog = yellow
altdialog = orange
note = yellow:L125
diff --git a/novelwriter/assets/themes/everforest_dark.conf b/novelwriter/assets/themes/everforest_dark.conf
index ff10cf93..36204052 100644
--- a/novelwriter/assets/themes/everforest_dark.conf
+++ b/novelwriter/assets/themes/everforest_dark.conf
@@ -29,6 +29,27 @@ active = cyan
inactive = orange
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #1e2326
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = cyan
headertag = cyan:128
emphasis = blue
+whitespace = blue:64
dialog = green
altdialog = cyan
note = purple
diff --git a/novelwriter/assets/themes/everforest_light.conf b/novelwriter/assets/themes/everforest_light.conf
index 901379ed..f4b4b5e1 100644
--- a/novelwriter/assets/themes/everforest_light.conf
+++ b/novelwriter/assets/themes/everforest_light.conf
@@ -29,6 +29,27 @@ active = cyan
inactive = orange
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #f2efdf
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = cyan
headertag = cyan:128
emphasis = blue
+whitespace = blue:64
dialog = green
altdialog = cyan
note = purple
diff --git a/novelwriter/assets/themes/floral_daydream.conf b/novelwriter/assets/themes/floral_daydream.conf
index ecee703f..ed74dfd2 100644
--- a/novelwriter/assets/themes/floral_daydream.conf
+++ b/novelwriter/assets/themes/floral_daydream.conf
@@ -27,6 +27,27 @@ active = green
inactive = faded
disabled = #e2bcce
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #ffe3ea
windowtext = default
@@ -57,6 +78,7 @@ link = #4781d8
headertext = #4781d8
headertag = #4781d880
emphasis = #8152b8
+whitespace = #8152b864
dialog = #eb4073
altdialog = #4781d8
note = #31924c
diff --git a/novelwriter/assets/themes/floral_midnight.conf b/novelwriter/assets/themes/floral_midnight.conf
index e08eaa3a..510dc185 100644
--- a/novelwriter/assets/themes/floral_midnight.conf
+++ b/novelwriter/assets/themes/floral_midnight.conf
@@ -27,6 +27,27 @@ active = green
inactive = faded
disabled = #55516d
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #181825
windowtext = default
@@ -57,6 +78,7 @@ link = blue
headertext = blue
headertag = blue:128
emphasis = #d19df3
+whitespace = #d19df364
dialog = #ff9dd9
altdialog = blue
note = #65ca80
diff --git a/novelwriter/assets/themes/full_moon.conf b/novelwriter/assets/themes/full_moon.conf
index c6e2c864..67a19c96 100644
--- a/novelwriter/assets/themes/full_moon.conf
+++ b/novelwriter/assets/themes/full_moon.conf
@@ -27,6 +27,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #e7ebee
windowtext = default
@@ -57,6 +78,7 @@ link = blue
headertext = purple
headertag = purple:128
emphasis = blue
+whitespace = blue:64
dialog = #0e2a35
altdialog = cyan
note = green
diff --git a/novelwriter/assets/themes/grey_dark.conf b/novelwriter/assets/themes/grey_dark.conf
index 665c7a1f..45cd4137 100644
--- a/novelwriter/assets/themes/grey_dark.conf
+++ b/novelwriter/assets/themes/grey_dark.conf
@@ -29,6 +29,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #363636
windowtext = default
@@ -59,6 +80,7 @@ link = default
headertext = default:L115
headertag = default:D125
emphasis = default
+whitespace = default:64
dialog = default
altdialog = default
note = default
diff --git a/novelwriter/assets/themes/grey_light.conf b/novelwriter/assets/themes/grey_light.conf
index 2106e5bf..bd341eff 100644
--- a/novelwriter/assets/themes/grey_light.conf
+++ b/novelwriter/assets/themes/grey_light.conf
@@ -29,6 +29,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #efefef
windowtext = default
@@ -59,6 +80,7 @@ link = default
headertext = default:D200
headertag = default:L400
emphasis = default
+whitespace = default:64
dialog = default
altdialog = default
note = default
diff --git a/novelwriter/assets/themes/horizon_dark.conf b/novelwriter/assets/themes/horizon_dark.conf
index 085e8c6d..6e2077fa 100644
--- a/novelwriter/assets/themes/horizon_dark.conf
+++ b/novelwriter/assets/themes/horizon_dark.conf
@@ -29,6 +29,27 @@ active = yellow
inactive = faded
disabled = #404263
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = red
headertag = red:128
emphasis = yellow
+whitespace = yellow:64
dialog = orange
altdialog = red
note = blue
diff --git a/novelwriter/assets/themes/horizon_light.conf b/novelwriter/assets/themes/horizon_light.conf
index cee0a7f1..128f90d6 100644
--- a/novelwriter/assets/themes/horizon_light.conf
+++ b/novelwriter/assets/themes/horizon_light.conf
@@ -29,6 +29,27 @@ active = #eb834f
inactive = faded
disabled = faded:L125
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = red
headertag = red:128
emphasis = #f6661e
+whitespace = #f6661e64
dialog = orange
altdialog = red
note = blue
diff --git a/novelwriter/assets/themes/lcars.conf b/novelwriter/assets/themes/lcars.conf
index ee553602..2587c4cd 100644
--- a/novelwriter/assets/themes/lcars.conf
+++ b/novelwriter/assets/themes/lcars.conf
@@ -29,6 +29,27 @@ active = blue
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base:L180
windowtext = default
@@ -59,6 +80,7 @@ link = purple
headertext = orange
headertag = red
emphasis = orange
+whitespace = orange:64
dialog = yellow
altdialog = green
note = purple
diff --git a/novelwriter/assets/themes/light_owl.conf b/novelwriter/assets/themes/light_owl.conf
index 6a43afc0..c1e48f6d 100644
--- a/novelwriter/assets/themes/light_owl.conf
+++ b/novelwriter/assets/themes/light_owl.conf
@@ -49,6 +49,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #eaeaea
windowtext = default
@@ -79,6 +100,7 @@ link = blue
headertext = blue
headertag = blue:160
emphasis = orange
+whitespace = orange:64
dialog = green
altdialog = yellow
note = yellow:D175
diff --git a/novelwriter/assets/themes/new_moon.conf b/novelwriter/assets/themes/new_moon.conf
index 4bbb2e58..a69da8ad 100644
--- a/novelwriter/assets/themes/new_moon.conf
+++ b/novelwriter/assets/themes/new_moon.conf
@@ -29,6 +29,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #252525
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = purple
headertag = purple:128
emphasis = blue
+whitespace = blue:64
dialog = #ffffff
altdialog = cyan
note = green
diff --git a/novelwriter/assets/themes/night_owl.conf b/novelwriter/assets/themes/night_owl.conf
index 716f7eb8..d49de77b 100644
--- a/novelwriter/assets/themes/night_owl.conf
+++ b/novelwriter/assets/themes/night_owl.conf
@@ -37,6 +37,27 @@ cyan = #7fdbca
blue = #82aaff
purple = #c792ea
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Project]
root = blue
folder = yellow
@@ -79,6 +100,7 @@ link = blue
headertext = blue
headertag = blue:160
emphasis = orange
+whitespace = orange:64
dialog = green
altdialog = yellow
note = yellow:L115
diff --git a/novelwriter/assets/themes/noctis.conf b/novelwriter/assets/themes/noctis.conf
index 235bb5dc..09bf4080 100644
--- a/novelwriter/assets/themes/noctis.conf
+++ b/novelwriter/assets/themes/noctis.conf
@@ -61,6 +61,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #041d20
windowtext = default
@@ -91,6 +112,7 @@ link = #40d4e7
headertext = #49e9a6
headertag = green:D125
emphasis = #d67e5c
+whitespace = #d67e5c64
dialog = green
altdialog = blue
note = #d67e5c
diff --git a/novelwriter/assets/themes/noctis_lux.conf b/novelwriter/assets/themes/noctis_lux.conf
index fa37b293..cc08f4e9 100644
--- a/novelwriter/assets/themes/noctis_lux.conf
+++ b/novelwriter/assets/themes/noctis_lux.conf
@@ -61,6 +61,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #f9f1e1
windowtext = default
@@ -91,6 +112,7 @@ link = #00c6e0
headertext = #00b368
headertag = green:D125
emphasis = #b3694d
+whitespace = #b3694d64
dialog = green
altdialog = blue
note = #b3694d
diff --git a/novelwriter/assets/themes/nord.conf b/novelwriter/assets/themes/nord.conf
index e2a2faf9..ac29ff76 100644
--- a/novelwriter/assets/themes/nord.conf
+++ b/novelwriter/assets/themes/nord.conf
@@ -29,6 +29,27 @@ active = green
inactive = faded
disabled = #576279
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #242933
windowtext = default
@@ -59,6 +80,7 @@ link = purple
headertext = cyan
headertag = cyan:128
emphasis = #8fbcbb
+whitespace = #8fbcbb64
dialog = blue
altdialog = green
note = orange
diff --git a/novelwriter/assets/themes/nordlicht.conf b/novelwriter/assets/themes/nordlicht.conf
index f8d9a846..31235ce0 100644
--- a/novelwriter/assets/themes/nordlicht.conf
+++ b/novelwriter/assets/themes/nordlicht.conf
@@ -27,6 +27,27 @@ active = green
inactive = faded
disabled = #b2c0d6
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #e5e9f0
windowtext = default
@@ -57,6 +78,7 @@ link = purple
headertext = #559db1
headertag = #559db188
emphasis = #549b86
+whitespace = #549b8664
dialog = #5579a5
altdialog = #619b5b
note = orange
diff --git a/novelwriter/assets/themes/otium_dark.conf b/novelwriter/assets/themes/otium_dark.conf
index 7319180a..d34fdd9d 100644
--- a/novelwriter/assets/themes/otium_dark.conf
+++ b/novelwriter/assets/themes/otium_dark.conf
@@ -27,6 +27,27 @@ active = default
inactive = faded
disabled = #4e545e
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base
windowtext = default
@@ -57,6 +78,7 @@ link = #a6d0ed
headertext = yellow
headertag = yellow:128
emphasis = yellow
+whitespace = yellow:64
dialog = green
altdialog = orange
note = #a6d0ed
diff --git a/novelwriter/assets/themes/otium_light.conf b/novelwriter/assets/themes/otium_light.conf
index 062c7879..40493c3b 100644
--- a/novelwriter/assets/themes/otium_light.conf
+++ b/novelwriter/assets/themes/otium_light.conf
@@ -27,6 +27,27 @@ active = default:L165
inactive = faded:L110
disabled = #adadad
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base
windowtext = default
@@ -57,6 +78,7 @@ link = blue
headertext = yellow
headertag = yellow:128
emphasis = #ad802a
+whitespace = #ad802a64
dialog = green
altdialog = #c0652d
note = #668bbd
diff --git a/novelwriter/assets/themes/paragon.conf b/novelwriter/assets/themes/paragon.conf
index ff48674c..bd9eab36 100644
--- a/novelwriter/assets/themes/paragon.conf
+++ b/novelwriter/assets/themes/paragon.conf
@@ -28,6 +28,27 @@ active = green
inactive = faded
disabled = faded:D150
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #10151b
windowtext = default
@@ -58,6 +79,7 @@ link = green
headertext = green
headertag = green:128
emphasis = blue
+whitespace = blue:100
dialog = cyan
altdialog = green
note = #c7b377
diff --git a/novelwriter/assets/themes/primer_light.conf b/novelwriter/assets/themes/primer_light.conf
index e21bb642..72b69533 100644
--- a/novelwriter/assets/themes/primer_light.conf
+++ b/novelwriter/assets/themes/primer_light.conf
@@ -29,6 +29,27 @@ active = #24292f
inactive = faded
disabled = #afb8c1
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #eaeef2
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = default
headertag = #9ea7b0
emphasis = #1a7f37
+whitespace = #1a7f3764
dialog = #0d1117
altdialog = cyan
note = #bf8700
diff --git a/novelwriter/assets/themes/primer_night.conf b/novelwriter/assets/themes/primer_night.conf
index cf677a88..a62dab67 100644
--- a/novelwriter/assets/themes/primer_night.conf
+++ b/novelwriter/assets/themes/primer_night.conf
@@ -29,6 +29,27 @@ active = default
inactive = faded
disabled = #484f58
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #161b22
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = default
headertag = #b1bac4
emphasis = #7ee787
+whitespace = #7ee78764
dialog = #ffffff
altdialog = #57ccc5
note = #f8e3a1
diff --git a/novelwriter/assets/themes/ruby_day.conf b/novelwriter/assets/themes/ruby_day.conf
index 5a56e472..411302c8 100644
--- a/novelwriter/assets/themes/ruby_day.conf
+++ b/novelwriter/assets/themes/ruby_day.conf
@@ -27,6 +27,27 @@ active = default
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #f1d9d9
windowtext = default
@@ -57,6 +78,7 @@ link = #5159cc
headertext = #343242
headertag = #34324280
emphasis = #c05858
+whitespace = #c0585864
dialog = #ce1d1d
altdialog = #c45522
note = #9f58ad
diff --git a/novelwriter/assets/themes/ruby_night.conf b/novelwriter/assets/themes/ruby_night.conf
index 37f60ac6..2ae79c59 100644
--- a/novelwriter/assets/themes/ruby_night.conf
+++ b/novelwriter/assets/themes/ruby_night.conf
@@ -27,6 +27,27 @@ active = default
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #0e0e11
windowtext = default
@@ -57,6 +78,7 @@ link = blue
headertext = default
headertag = default:128
emphasis = #ff9d9d
+whitespace = #ff9d9d64
dialog = red
altdialog = orange
note = purple
@@ -70,4 +92,4 @@ spellcheckline = #ff2727
errorline = cyan
replacetag = blue
modifier = green
-texthighlight = red:80
\ No newline at end of file
+texthighlight = red:80
diff --git a/novelwriter/assets/themes/selenium_dark.conf b/novelwriter/assets/themes/selenium_dark.conf
index a5c5cc1b..95b23535 100644
--- a/novelwriter/assets/themes/selenium_dark.conf
+++ b/novelwriter/assets/themes/selenium_dark.conf
@@ -27,6 +27,27 @@ active = #c7c0d2
inactive = red
disabled = #625b70
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #0f0e13
windowtext = #c7c0d2
@@ -57,6 +78,7 @@ link = blue
headertext = #e4e0ec
headertag = faded
emphasis = #9267d3
+whitespace = #9267d364
dialog = #e4e0ec
altdialog = #c458ac
note = red
diff --git a/novelwriter/assets/themes/selenium_light.conf b/novelwriter/assets/themes/selenium_light.conf
index d1eefa1f..64f448e6 100644
--- a/novelwriter/assets/themes/selenium_light.conf
+++ b/novelwriter/assets/themes/selenium_light.conf
@@ -27,6 +27,27 @@ active = default
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #d2cfda
windowtext = #29252f
@@ -57,6 +78,7 @@ link = blue
headertext = #29252f
headertag = faded
emphasis = purple
+whitespace = purple:64
dialog = #29252f
altdialog = #c458ac
note = red
diff --git a/novelwriter/assets/themes/sepia_dark.conf b/novelwriter/assets/themes/sepia_dark.conf
index 49deeedd..fd530359 100644
--- a/novelwriter/assets/themes/sepia_dark.conf
+++ b/novelwriter/assets/themes/sepia_dark.conf
@@ -27,6 +27,27 @@ active = default
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #251b18
windowtext = #ceb3a2
@@ -57,6 +78,7 @@ link = cyan
headertext = default
headertag = default:160
emphasis = #f1daca
+whitespace = #f1daca64
dialog = orange
altdialog = red
note = green
diff --git a/novelwriter/assets/themes/sepia_light.conf b/novelwriter/assets/themes/sepia_light.conf
index a7c1d306..35452652 100644
--- a/novelwriter/assets/themes/sepia_light.conf
+++ b/novelwriter/assets/themes/sepia_light.conf
@@ -27,6 +27,27 @@ active = #86685b
inactive = red
disabled = #b6a096
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #e2d3c1
windowtext = default
@@ -57,6 +78,7 @@ link = #458871
headertext = default
headertag = default:160
emphasis = #6e2920
+whitespace = #6e292064
dialog = #ac5828
altdialog = #b44b4b
note = #6e8021
diff --git a/novelwriter/assets/themes/snazzy.conf b/novelwriter/assets/themes/snazzy.conf
index 0c36d884..a4e5935d 100644
--- a/novelwriter/assets/themes/snazzy.conf
+++ b/novelwriter/assets/themes/snazzy.conf
@@ -42,6 +42,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #f3f4f5
windowtext = default
@@ -72,6 +93,7 @@ link = blue
headertext = green
headertag = green:D125
emphasis = purple
+whitespace = purple:64
dialog = blue
altdialog = yellow
note = cyan
diff --git a/novelwriter/assets/themes/solarized_dark.conf b/novelwriter/assets/themes/solarized_dark.conf
index 4c73412f..26963c0f 100644
--- a/novelwriter/assets/themes/solarized_dark.conf
+++ b/novelwriter/assets/themes/solarized_dark.conf
@@ -48,6 +48,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #073642
windowtext = default
@@ -78,6 +99,7 @@ link = blue
headertext = blue
headertag = #657b83
emphasis = blue
+whitespace = blue:64
dialog = cyan
altdialog = red
note = cyan:D125
diff --git a/novelwriter/assets/themes/solarized_light.conf b/novelwriter/assets/themes/solarized_light.conf
index 61d701b7..a05c5beb 100644
--- a/novelwriter/assets/themes/solarized_light.conf
+++ b/novelwriter/assets/themes/solarized_light.conf
@@ -48,6 +48,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #eee8d5
windowtext = default
@@ -78,6 +99,7 @@ link = blue
headertext = blue
headertag = #657b83
emphasis = blue
+whitespace = blue:64
dialog = cyan
altdialog = red
note = cyan:D125
diff --git a/novelwriter/assets/themes/sultana_light.conf b/novelwriter/assets/themes/sultana_light.conf
index 1f4e5195..3f09904a 100644
--- a/novelwriter/assets/themes/sultana_light.conf
+++ b/novelwriter/assets/themes/sultana_light.conf
@@ -27,6 +27,27 @@ active = #77606e
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #f0e4e4
windowtext = default
@@ -57,6 +78,7 @@ link = #5577bb
headertext = #66515e
headertag = #66515e80
emphasis = #5577bb
+whitespace = #5577bb64
dialog = #c54a5f
altdialog = #4d968a
note = #699128
diff --git a/novelwriter/assets/themes/sultana_night.conf b/novelwriter/assets/themes/sultana_night.conf
index bd55dd84..c27b2ac3 100644
--- a/novelwriter/assets/themes/sultana_night.conf
+++ b/novelwriter/assets/themes/sultana_night.conf
@@ -27,6 +27,27 @@ active = default
inactive = red
disabled = #85748a
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #221a23
windowtext = default
@@ -57,6 +78,7 @@ link = blue
headertext = #f3eae0
headertag = #f3eae080
emphasis = blue
+whitespace = blue:64
dialog = red
altdialog = cyan
note = green
diff --git a/novelwriter/assets/themes/tango_dark.conf b/novelwriter/assets/themes/tango_dark.conf
index 8661ce27..0b1884bc 100644
--- a/novelwriter/assets/themes/tango_dark.conf
+++ b/novelwriter/assets/themes/tango_dark.conf
@@ -43,6 +43,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base:L140
windowtext = default
@@ -73,6 +94,7 @@ link = blue
headertext = blue
headertag = blue:160
emphasis = orange
+whitespace = orange:64
dialog = green
altdialog = yellow
note = yellow:L115
diff --git a/novelwriter/assets/themes/tango_light.conf b/novelwriter/assets/themes/tango_light.conf
index 379b0a72..01486203 100644
--- a/novelwriter/assets/themes/tango_light.conf
+++ b/novelwriter/assets/themes/tango_light.conf
@@ -43,6 +43,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base:D110
windowtext = default
@@ -73,6 +94,7 @@ link = blue
headertext = blue
headertag = blue:160
emphasis = orange
+whitespace = orange:64
dialog = green
altdialog = yellow
note = yellow:D125
diff --git a/novelwriter/assets/themes/tomorrow.conf b/novelwriter/assets/themes/tomorrow.conf
index 15abcd32..80823828 100644
--- a/novelwriter/assets/themes/tomorrow.conf
+++ b/novelwriter/assets/themes/tomorrow.conf
@@ -49,6 +49,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #efefef
windowtext = #000000
@@ -79,6 +100,7 @@ link = blue
headertext = blue
headertag = blue:L135
emphasis = orange
+whitespace = orange:64
dialog = green
altdialog = yellow
note = #b38c00
diff --git a/novelwriter/assets/themes/tomorrow_night.conf b/novelwriter/assets/themes/tomorrow_night.conf
index ac213a76..b0346d0f 100644
--- a/novelwriter/assets/themes/tomorrow_night.conf
+++ b/novelwriter/assets/themes/tomorrow_night.conf
@@ -49,6 +49,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #282a2e
windowtext = default
@@ -79,6 +100,7 @@ link = blue
headertext = blue
headertag = blue:D150
emphasis = orange
+whitespace = orange:64
dialog = green
altdialog = yellow
note = #f0dbb2
diff --git a/novelwriter/assets/themes/tomorrow_night_blue.conf b/novelwriter/assets/themes/tomorrow_night_blue.conf
index 05193e6f..678185a3 100644
--- a/novelwriter/assets/themes/tomorrow_night_blue.conf
+++ b/novelwriter/assets/themes/tomorrow_night_blue.conf
@@ -49,6 +49,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base:L125
windowtext = default
@@ -79,6 +100,7 @@ link = blue
headertext = blue
headertag = blue:D150
emphasis = orange
+whitespace = orange:64
dialog = green
altdialog = yellow
note = #fff4cc
diff --git a/novelwriter/assets/themes/tomorrow_night_bright.conf b/novelwriter/assets/themes/tomorrow_night_bright.conf
index 514940fa..a28657e7 100644
--- a/novelwriter/assets/themes/tomorrow_night_bright.conf
+++ b/novelwriter/assets/themes/tomorrow_night_bright.conf
@@ -49,6 +49,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #181818
windowtext = default
@@ -79,6 +100,7 @@ link = blue
headertext = blue
headertag = blue:D150
emphasis = orange
+whitespace = orange:64
dialog = green
altdialog = yellow
note = #e7d696
diff --git a/novelwriter/assets/themes/tomorrow_night_eighties.conf b/novelwriter/assets/themes/tomorrow_night_eighties.conf
index 154b6ef4..651c764d 100644
--- a/novelwriter/assets/themes/tomorrow_night_eighties.conf
+++ b/novelwriter/assets/themes/tomorrow_night_eighties.conf
@@ -49,6 +49,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #393939
windowtext = default
@@ -79,6 +100,7 @@ link = blue
headertext = blue
headertag = blue:D150
emphasis = orange
+whitespace = orange:64
dialog = green
altdialog = yellow
note = #ffe6b3
diff --git a/novelwriter/assets/themes/vivid_black_green.conf b/novelwriter/assets/themes/vivid_black_green.conf
index f2d17043..c9e3af02 100644
--- a/novelwriter/assets/themes/vivid_black_green.conf
+++ b/novelwriter/assets/themes/vivid_black_green.conf
@@ -29,6 +29,27 @@ active = green
inactive = faded
disabled = faded:D175
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = cyan
headertag = cyan:128
emphasis = blue
+whitespace = blue:64
dialog = green
altdialog = cyan
note = red
diff --git a/novelwriter/assets/themes/vivid_black_red.conf b/novelwriter/assets/themes/vivid_black_red.conf
index fd7d99f2..45fdf500 100644
--- a/novelwriter/assets/themes/vivid_black_red.conf
+++ b/novelwriter/assets/themes/vivid_black_red.conf
@@ -29,6 +29,27 @@ active = red
inactive = faded
disabled = faded:D175
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = yellow
headertag = yellow:128
emphasis = orange
+whitespace = orange:64
dialog = red
altdialog = yellow
note = green
@@ -72,4 +94,4 @@ spellcheckline = #ff3737
errorline = cyan
replacetag = #ff61e5
modifier = cyan
-texthighlight = red:80
\ No newline at end of file
+texthighlight = red:80
diff --git a/novelwriter/assets/themes/vivid_white_green.conf b/novelwriter/assets/themes/vivid_white_green.conf
index 7c299bdc..7d10036d 100644
--- a/novelwriter/assets/themes/vivid_white_green.conf
+++ b/novelwriter/assets/themes/vivid_white_green.conf
@@ -29,6 +29,27 @@ active = green
inactive = faded
disabled = faded:L135
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = cyan
headertag = cyan:128
emphasis = blue
+whitespace = blue:64
dialog = green
altdialog = cyan
note = red
diff --git a/novelwriter/assets/themes/vivid_white_red.conf b/novelwriter/assets/themes/vivid_white_red.conf
index e77b42b3..e8f6956c 100644
--- a/novelwriter/assets/themes/vivid_white_red.conf
+++ b/novelwriter/assets/themes/vivid_white_red.conf
@@ -29,6 +29,27 @@ active = red
inactive = faded
disabled = faded:L135
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base
windowtext = default
@@ -59,6 +80,7 @@ link = blue
headertext = yellow
headertag = yellow:128
emphasis = orange
+whitespace = orange:64
dialog = red
altdialog = #a37e03
note = green
diff --git a/novelwriter/assets/themes/warpgate.conf b/novelwriter/assets/themes/warpgate.conf
index 1398732f..f4f016cf 100644
--- a/novelwriter/assets/themes/warpgate.conf
+++ b/novelwriter/assets/themes/warpgate.conf
@@ -28,6 +28,27 @@ active = green
inactive = red
disabled = faded
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = base:D150
windowtext = default
@@ -58,6 +79,7 @@ link = green
headertext = green
headertag = green:128
emphasis = #fff6aa
+whitespace = #fff6aa64
dialog = cyan
altdialog = #11bdc6
note = green
diff --git a/novelwriter/assets/themes/waterlily_dark.conf b/novelwriter/assets/themes/waterlily_dark.conf
index 92395b9c..fd19a045 100644
--- a/novelwriter/assets/themes/waterlily_dark.conf
+++ b/novelwriter/assets/themes/waterlily_dark.conf
@@ -27,6 +27,27 @@ active = #ff9fbd
inactive = faded
disabled = #374a5c
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #151f25
windowtext = default
@@ -57,6 +78,7 @@ link = blue
headertext = default
headertag = faded
emphasis = green
+whitespace = green:64
dialog = #ff9fbd
altdialog = orange
note = blue
diff --git a/novelwriter/assets/themes/waterlily_light.conf b/novelwriter/assets/themes/waterlily_light.conf
index 960baa67..7d1ad410 100644
--- a/novelwriter/assets/themes/waterlily_light.conf
+++ b/novelwriter/assets/themes/waterlily_light.conf
@@ -27,6 +27,27 @@ active = #da7b8f
inactive = faded
disabled = #b6c9ba
+[Icon]
+tool = default
+sidebar = default
+accept = green
+reject = red
+action = blue
+altaction = orange
+apply = green
+create = yellow
+destroy = faded
+reset = green
+add = green
+change = green
+remove = red
+shortcode = default
+markdown = orange
+systemio = yellow
+info = blue
+warning = orange
+error = red
+
[Palette]
window = #e2eee7
windowtext = default
@@ -57,6 +78,7 @@ link = #0b9caf
headertext = #526b5d
headertag = #526b5d80
emphasis = #1e920e
+whitespace = #1e920e64
dialog = #d15574
altdialog = #d86f3e
note = #0b9caf
diff --git a/novelwriter/common.py b/novelwriter/common.py
index 8fd9386a..5bd35d50 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -560,6 +560,12 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
return "".join(buffer)
+def jsonCombine(data: dict[str, str]) -> str:
+ """Combine multiple already packed JSON strings."""
+ payload = ",\n".join(f' "{k}": {v}' for k, v in data.items())
+ return f"{{\n{payload}\n}}\n"
+
+
##
# XML Helpers
##
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index 2a9aba3a..d1955c45 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -33,8 +33,10 @@ from pathlib import Path
from time import time
from typing import TYPE_CHECKING
-from novelwriter import SHARED
-from novelwriter.common import isHandle, isItemClass, isTitleTag, jsonEncode
+from novelwriter import SHARED, __hexversion__
+from novelwriter.common import (
+ formatTimeStamp, isHandle, isItemClass, isTitleTag, jsonCombine, jsonEncode
+)
from novelwriter.constants import nwFiles, nwKeyWords, nwStyles
from novelwriter.core.indexdata import NOTE_TYPES, TT_NONE, IndexHeading, IndexNode, T_NoteTypes
from novelwriter.core.novelmodel import NovelModel
@@ -82,6 +84,11 @@ class Index:
a rebuild of the index data.
"""
+ __slots__ = (
+ "_indexBroken", "_indexChange", "_indexUpgrade", "_itemIndex", "_novelExtra",
+ "_novelModels", "_project", "_rootChange", "_tagsIndex",
+ )
+
def __init__(self, project: NWProject) -> None:
self._project = project
@@ -90,6 +97,7 @@ class Index:
self._tagsIndex = TagsIndex()
self._itemIndex = ItemIndex(project, self._tagsIndex)
self._indexBroken = False
+ self._indexUpgrade = False
# Models
self._novelModels: dict[str, NovelModel] = {}
@@ -110,6 +118,10 @@ class Index:
def indexBroken(self) -> bool:
return self._indexBroken
+ @property
+ def indexUpgrade(self) -> bool:
+ return self._indexUpgrade
+
##
# Getters
##
@@ -241,6 +253,8 @@ class Index:
return False
try:
+ meta = data.get("novelWriter.meta", {})
+ self._indexUpgrade = meta.get("version") != __hexversion__
self._tagsIndex.unpackData(data["novelWriter.tagsIndex"])
self._itemIndex.unpackData(data["novelWriter.itemIndex"])
except Exception:
@@ -273,23 +287,22 @@ class Index:
return False
logger.debug("Saving index file")
- tStart = time()
+ start = time()
try:
- tagsIndex = jsonEncode(self._tagsIndex.packData(), n=1, nmax=2)
- itemIndex = jsonEncode(self._itemIndex.packData(), n=1, nmax=4)
+ meta = {"version": __hexversion__, "timestamp": formatTimeStamp(start)}
with open(indexFile, mode="w+", encoding="utf-8") as outFile:
- outFile.write("{\n")
- outFile.write(f' "novelWriter.tagsIndex": {tagsIndex},\n')
- outFile.write(f' "novelWriter.itemIndex": {itemIndex}\n')
- outFile.write("}\n")
-
+ outFile.write(jsonCombine({
+ "novelWriter.meta": jsonEncode(meta, n=1),
+ "novelWriter.tagsIndex": jsonEncode(self._tagsIndex.packData(), n=1, nmax=2),
+ "novelWriter.itemIndex": jsonEncode(self._itemIndex.packData(), n=1, nmax=4),
+ }))
except Exception:
logger.error("Failed to save index file")
logException()
return False
- logger.debug("Index saved in %.3f ms", (time() - tStart)*1000)
+ logger.debug("Index saved in %.3f ms", (time() - start)*1000)
return True
diff --git a/novelwriter/core/indexdata.py b/novelwriter/core/indexdata.py
index b7867bd3..78477312 100644
--- a/novelwriter/core/indexdata.py
+++ b/novelwriter/core/indexdata.py
@@ -300,13 +300,13 @@ class IndexHeading:
"""Set the text for a comment and make sure it is a string."""
match comment.lower():
case "short" | "synopsis" | "summary":
- self._comments["summary"] = str(text)
+ self._appendCommentText("summary", text)
case "story" if key:
self._cache.story.add(key)
- self._comments[f"story.{key}"] = str(text)
+ self._appendCommentText(f"story.{key}", text)
case "note" if key:
self._cache.note.add(key)
- self._comments[f"note.{key}"] = str(text)
+ self._appendCommentText(f"note.{key}", text)
def setTag(self, tag: str) -> None:
"""Set the tag for references, and make sure it is a string."""
@@ -371,6 +371,7 @@ class IndexHeading:
def unpackData(self, data: dict) -> None:
"""Unpack a heading entry from a dictionary."""
+ self._comments = {} # These are accumulative and should be reset here
for key, entry in data.items():
if key == "meta":
self.setLevel(entry.get("level", "H0"))
@@ -394,3 +395,13 @@ class IndexHeading:
self.setComment(comment, compact(kind), str(entry))
else:
raise KeyError("Unknown key in heading entry")
+
+ ##
+ # Internal Functions
+ ##
+
+ def _appendCommentText(self, key: str, text: str) -> None:
+ """Append text to a comment."""
+ if current := self._comments.get(key):
+ text = f"{current:s}\n\n{text:s}"
+ self._comments[key] = str(text)
diff --git a/novelwriter/core/novelmodel.py b/novelwriter/core/novelmodel.py
index 7b1cd426..6418b1bc 100644
--- a/novelwriter/core/novelmodel.py
+++ b/novelwriter/core/novelmodel.py
@@ -61,7 +61,7 @@ class NovelModel(QAbstractTableModel):
def __init__(self) -> None:
super().__init__()
self._rows: list[dict[int, T_NodeData]] = []
- self._more = SHARED.theme.getIcon("more_arrow")
+ self._more = SHARED.theme.getIcon("more_arrow", "tool")
self._columns = 3
self._extraKey = ""
self._extraLabel = ""
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 2a10fd92..44bf3590 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -552,6 +552,8 @@ class NWProject:
def updateTheme(self) -> None:
"""Update theme elements."""
+ logger.debug("Theme Update: NWProject")
+
self._data.itemStatus.refreshIcons()
self._data.itemImport.refreshIcons()
diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py
index fc8bf85c..9a3d3194 100644
--- a/novelwriter/core/status.py
+++ b/novelwriter/core/status.py
@@ -183,7 +183,7 @@ class NWStatus:
icon = NWStatus.createIcon(self._height, color, shape)
return StatusEntry(simplified(data[2]), color, theme, shape, icon)
except Exception:
- logger.error("Could not parse entry %s", str(data))
+ logger.error("Could not parse entry %s", data)
return None
def refreshIcons(self) -> None:
diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py
index 4ee4b626..2f151cc4 100644
--- a/novelwriter/core/tree.py
+++ b/novelwriter/core/tree.py
@@ -89,7 +89,7 @@ class NWTree:
"""
if tHandle and tHandle in self._items:
return self._items[tHandle]
- logger.error("No tree item with handle '%s'", str(tHandle))
+ logger.error("No tree item with handle '%s'", tHandle)
return None
def __contains__(self, tHandle: str) -> bool:
diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py
index 59d4251f..a8898fa0 100644
--- a/novelwriter/dialogs/about.py
+++ b/novelwriter/dialogs/about.py
@@ -33,10 +33,11 @@ from PyQt6.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.common import readTextFile
+from novelwriter.enum import nwStandardButton
from novelwriter.extensions.configlayout import NColorLabel
from novelwriter.extensions.modified import NDialog
from novelwriter.extensions.versioninfo import VersionInfoWidget
-from novelwriter.types import QtAlignRightTop, QtDialogClose, QtHexArgb
+from novelwriter.types import QtAlignRightTop, QtHexArgb, QtRoleDestruct
if TYPE_CHECKING:
from PyQt6.QtGui import QCloseEvent
@@ -82,8 +83,11 @@ class GuiAbout(NDialog):
self.txtCredits.setViewportMargins(0, 8, 8, 0)
# Buttons
- self.btnBox = QDialogButtonBox(QtDialogClose, self)
- self.btnBox.rejected.connect(self.reject)
+ self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self)
+ self.btnClose.clicked.connect(self.reject)
+
+ self.btnBox = QDialogButtonBox(self)
+ self.btnBox.addButton(self.btnClose, QtRoleDestruct)
# Assemble
self.innerBox = QVBoxLayout()
diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py
index 2c392374..9f3d6184 100644
--- a/novelwriter/dialogs/docmerge.py
+++ b/novelwriter/dialogs/docmerge.py
@@ -33,10 +33,11 @@ from PyQt6.QtWidgets import (
)
from novelwriter import SHARED
+from novelwriter.enum import nwStandardButton
from novelwriter.extensions.configlayout import NColorLabel
from novelwriter.extensions.modified import NDialog
from novelwriter.extensions.switch import NSwitch
-from novelwriter.types import QtAccepted, QtDialogCancel, QtDialogOk, QtDialogReset, QtUserRole
+from novelwriter.types import QtAccepted, QtRoleAccept, QtRoleReject, QtRoleReset, QtUserRole
logger = logging.getLogger(__name__)
@@ -85,13 +86,19 @@ class GuiDocMerge(NDialog):
self.optBox.setColumnStretch(2, 1)
# Buttons
- self.buttonBox = QDialogButtonBox(QtDialogOk | QtDialogCancel, self)
- self.buttonBox.accepted.connect(self.accept)
- self.buttonBox.rejected.connect(self.reject)
+ self.btnOk = SHARED.theme.getStandardButton(nwStandardButton.OK, self)
+ self.btnOk.clicked.connect(self.accept)
- self.resetButton = self.buttonBox.addButton(QtDialogReset)
- if self.resetButton:
- self.resetButton.clicked.connect(self._resetList)
+ self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self)
+ self.btnCancel.clicked.connect(self.reject)
+
+ self.btnReset = SHARED.theme.getStandardButton(nwStandardButton.RESET, self)
+ self.btnReset.clicked.connect(self._resetList)
+
+ self.btnBox = QDialogButtonBox(self)
+ self.btnBox.addButton(self.btnOk, QtRoleAccept)
+ self.btnBox.addButton(self.btnCancel, QtRoleReject)
+ self.btnBox.addButton(self.btnReset, QtRoleReset)
# Assemble
self.outerBox = QVBoxLayout()
@@ -103,7 +110,7 @@ class GuiDocMerge(NDialog):
self.outerBox.addSpacing(8)
self.outerBox.addLayout(self.optBox)
self.outerBox.addSpacing(12)
- self.outerBox.addWidget(self.buttonBox)
+ self.outerBox.addWidget(self.btnBox)
self.setLayout(self.outerBox)
# Load Content
diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py
index 5850aff3..059a1678 100644
--- a/novelwriter/dialogs/docsplit.py
+++ b/novelwriter/dialogs/docsplit.py
@@ -33,10 +33,11 @@ from PyQt6.QtWidgets import (
)
from novelwriter import SHARED
+from novelwriter.enum import nwStandardButton
from novelwriter.extensions.configlayout import NColorLabel
from novelwriter.extensions.modified import NComboBox, NDialog
from novelwriter.extensions.switch import NSwitch
-from novelwriter.types import QtAccepted, QtDialogCancel, QtDialogOk, QtUserRole
+from novelwriter.types import QtAccepted, QtRoleAccept, QtRoleReject, QtUserRole
logger = logging.getLogger(__name__)
@@ -117,9 +118,15 @@ class GuiDocSplit(NDialog):
self.optBox.setColumnStretch(3, 1)
# Buttons
- self.buttonBox = QDialogButtonBox(QtDialogOk | QtDialogCancel, self)
- self.buttonBox.accepted.connect(self.accept)
- self.buttonBox.rejected.connect(self.reject)
+ self.btnOk = SHARED.theme.getStandardButton(nwStandardButton.OK, self)
+ self.btnOk.clicked.connect(self.accept)
+
+ self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self)
+ self.btnCancel.clicked.connect(self.reject)
+
+ self.btnBox = QDialogButtonBox(self)
+ self.btnBox.addButton(self.btnOk, QtRoleAccept)
+ self.btnBox.addButton(self.btnCancel, QtRoleReject)
# Assemble
self.outerBox = QVBoxLayout()
@@ -132,7 +139,7 @@ class GuiDocSplit(NDialog):
self.outerBox.addSpacing(8)
self.outerBox.addLayout(self.optBox)
self.outerBox.addSpacing(12)
- self.outerBox.addWidget(self.buttonBox)
+ self.outerBox.addWidget(self.btnBox)
self.setLayout(self.outerBox)
# Load Content
diff --git a/novelwriter/dialogs/editlabel.py b/novelwriter/dialogs/editlabel.py
index b8b4bae4..4e998aa8 100644
--- a/novelwriter/dialogs/editlabel.py
+++ b/novelwriter/dialogs/editlabel.py
@@ -27,8 +27,10 @@ import logging
from PyQt6.QtWidgets import QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, QWidget
+from novelwriter import SHARED
+from novelwriter.enum import nwStandardButton
from novelwriter.extensions.modified import NDialog
-from novelwriter.types import QtAccepted, QtDialogCancel, QtDialogOk
+from novelwriter.types import QtAccepted, QtRoleAccept, QtRoleReject
logger = logging.getLogger(__name__)
@@ -54,9 +56,15 @@ class GuiEditLabel(NDialog):
self.lblValue.setBuddy(self.lblValue)
# Buttons
- self.buttonBox = QDialogButtonBox(QtDialogOk | QtDialogCancel, self)
- self.buttonBox.accepted.connect(self.accept)
- self.buttonBox.rejected.connect(self.reject)
+ self.btnOk = SHARED.theme.getStandardButton(nwStandardButton.OK, self)
+ self.btnOk.clicked.connect(self.accept)
+
+ self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self)
+ self.btnCancel.clicked.connect(self.reject)
+
+ self.btnBox = QDialogButtonBox(self)
+ self.btnBox.addButton(self.btnOk, QtRoleAccept)
+ self.btnBox.addButton(self.btnCancel, QtRoleReject)
# Assemble
self.innerBox = QHBoxLayout()
@@ -67,7 +75,7 @@ class GuiEditLabel(NDialog):
self.outerBox = QVBoxLayout()
self.outerBox.setSpacing(12)
self.outerBox.addLayout(self.innerBox, 1)
- self.outerBox.addWidget(self.buttonBox, 0)
+ self.outerBox.addWidget(self.btnBox, 0)
self.setLayout(self.outerBox)
diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py
index 758f7908..df976d21 100644
--- a/novelwriter/dialogs/preferences.py
+++ b/novelwriter/dialogs/preferences.py
@@ -38,13 +38,14 @@ from novelwriter.common import compact, describeFont, processDialogSymbols, uniq
from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS, DEF_TREECOL
from novelwriter.constants import nwLabels, nwQuotes, nwUnicode, trConst
from novelwriter.dialogs.quotes import GuiQuoteSelect
+from novelwriter.enum import nwStandardButton
from novelwriter.extensions.configlayout import NColorLabel, NScrollableForm
from novelwriter.extensions.modified import (
NComboBox, NDialog, NDoubleSpinBox, NIconToolButton, NSpinBox
)
from novelwriter.extensions.pagedsidebar import NPagedSideBar
from novelwriter.extensions.switch import NSwitch
-from novelwriter.types import QtAlignCenter, QtDialogCancel, QtDialogSave
+from novelwriter.types import QtAlignCenter, QtRoleAccept, QtRoleReject
logger = logging.getLogger(__name__)
@@ -70,7 +71,7 @@ class GuiPreferences(NDialog):
)
# Search Box
- self.searchAction = QAction(SHARED.theme.getIcon("search"), "")
+ self.searchAction = QAction(SHARED.theme.getIcon("search", "apply"), "")
self.searchAction.triggered.connect(self._gotoSearch)
self.searchText = QLineEdit(self)
@@ -89,9 +90,15 @@ class GuiPreferences(NDialog):
self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
# Buttons
- self.buttonBox = QDialogButtonBox(QtDialogSave | QtDialogCancel, self)
- self.buttonBox.accepted.connect(self._doSave)
- self.buttonBox.rejected.connect(self.reject)
+ self.btnSave = SHARED.theme.getStandardButton(nwStandardButton.SAVE, self)
+ self.btnSave.clicked.connect(self._doSave)
+
+ self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self)
+ self.btnCancel.clicked.connect(self.reject)
+
+ self.btnBox = QDialogButtonBox(self)
+ self.btnBox.addButton(self.btnSave, QtRoleAccept)
+ self.btnBox.addButton(self.btnCancel, QtRoleReject)
# Assemble
self.searchBox = QHBoxLayout()
@@ -107,7 +114,7 @@ class GuiPreferences(NDialog):
self.outerBox = QVBoxLayout()
self.outerBox.addLayout(self.searchBox)
self.outerBox.addLayout(self.mainBox)
- self.outerBox.addWidget(self.buttonBox)
+ self.outerBox.addWidget(self.btnBox)
self.outerBox.setSpacing(8)
self.setLayout(self.outerBox)
@@ -151,7 +158,7 @@ class GuiPreferences(NDialog):
self.mainForm.addGroupLabel(title, section)
# Display Language
- self.guiLocale = NComboBox(self)
+ self.guiLocale = NComboBox(self, scrollable=True)
self.guiLocale.setMinimumWidth(200)
for lang, name in CONFIG.listLanguages(CONFIG.LANG_NW):
self.guiLocale.addItem(name, lang)
@@ -163,9 +170,9 @@ class GuiPreferences(NDialog):
)
# Colour Theme
- self.lightTheme = NComboBox(self)
+ self.lightTheme = NComboBox(self, scrollable=True)
self.lightTheme.setMinimumWidth(200)
- self.darkTheme = NComboBox(self)
+ self.darkTheme = NComboBox(self, scrollable=True)
self.darkTheme.setMinimumWidth(200)
for key, theme in SHARED.theme.colourThemes.items():
if theme.dark:
@@ -186,7 +193,7 @@ class GuiPreferences(NDialog):
)
# Icon Theme
- self.iconTheme = NComboBox(self)
+ self.iconTheme = NComboBox(self, scrollable=True)
self.iconTheme.setMinimumWidth(200)
for key, theme in SHARED.theme.iconCache.iconThemes.items():
self.iconTheme.addItem(theme.name, key)
@@ -204,7 +211,7 @@ class GuiPreferences(NDialog):
self.guiFont.setMinimumWidth(162)
self.guiFont.setText(describeFont(self._guiFont))
self.guiFont.setCursorPosition(0)
- self.guiFontButton = NIconToolButton(self, iSz, "font")
+ self.guiFontButton = NIconToolButton(self, iSz, "font", "tool")
self.guiFontButton.clicked.connect(self._selectGuiFont)
self.mainForm.addRow(
self.tr("Application font"), self.guiFont,
@@ -258,7 +265,7 @@ class GuiPreferences(NDialog):
self.textFont.setMinimumWidth(162)
self.textFont.setText(describeFont(CONFIG.textFont))
self.textFont.setCursorPosition(0)
- self.textFontButton = NIconToolButton(self, iSz, "font")
+ self.textFontButton = NIconToolButton(self, iSz, "font", "tool")
self.textFontButton.clicked.connect(self._selectTextFont)
self.mainForm.addRow(
self.tr("Document font"), self.textFont,
@@ -366,7 +373,9 @@ class GuiPreferences(NDialog):
# Backup Path
self.backupPath = CONFIG.backupPath()
- self.backupGetPath = QPushButton(SHARED.theme.getIcon("browse"), self.tr("Browse"), self)
+ self.backupGetPath = QPushButton(
+ SHARED.theme.getIcon("browse", "systemio"), self.tr("Browse"), self
+ )
self.backupGetPath.setIconSize(iSz)
self.backupGetPath.clicked.connect(self._backupFolder)
self.mainForm.addRow(
@@ -504,7 +513,7 @@ class GuiPreferences(NDialog):
self.mainForm.addGroupLabel(title, section)
# Spell Checking
- self.spellLanguage = NComboBox(self)
+ self.spellLanguage = NComboBox(self, scrollable=True)
self.spellLanguage.setMinimumWidth(200)
if CONFIG.hasEnchant:
@@ -658,7 +667,7 @@ class GuiPreferences(NDialog):
self.dialogLine.setAlignment(QtAlignCenter)
self.dialogLine.setText(" ".join(CONFIG.dialogLine))
- self.dialogLineButton = NIconToolButton(self, iSz, "add", "green")
+ self.dialogLineButton = NIconToolButton(self, iSz, "add", "add")
self.dialogLineButton.setMenu(self.mnLineSymbols)
self.mainForm.addRow(
@@ -800,7 +809,7 @@ class GuiPreferences(NDialog):
self.fmtSQuoteOpen.setFixedWidth(boxFixed)
self.fmtSQuoteOpen.setAlignment(QtAlignCenter)
self.fmtSQuoteOpen.setText(CONFIG.fmtSQuoteOpen)
- self.btnSQuoteOpen = NIconToolButton(self, iSz, "quote")
+ self.btnSQuoteOpen = NIconToolButton(self, iSz, "quote", "tool")
self.btnSQuoteOpen.clicked.connect(self._changeSingleQuoteOpen)
self.mainForm.addRow(
self.tr("Single quote open style"), self.fmtSQuoteOpen,
@@ -814,7 +823,7 @@ class GuiPreferences(NDialog):
self.fmtSQuoteClose.setFixedWidth(boxFixed)
self.fmtSQuoteClose.setAlignment(QtAlignCenter)
self.fmtSQuoteClose.setText(CONFIG.fmtSQuoteClose)
- self.btnSQuoteClose = NIconToolButton(self, iSz, "quote")
+ self.btnSQuoteClose = NIconToolButton(self, iSz, "quote", "tool")
self.btnSQuoteClose.clicked.connect(self._changeSingleQuoteClose)
self.mainForm.addRow(
self.tr("Single quote close style"), self.fmtSQuoteClose,
@@ -829,7 +838,7 @@ class GuiPreferences(NDialog):
self.fmtDQuoteOpen.setFixedWidth(boxFixed)
self.fmtDQuoteOpen.setAlignment(QtAlignCenter)
self.fmtDQuoteOpen.setText(CONFIG.fmtDQuoteOpen)
- self.btnDQuoteOpen = NIconToolButton(self, iSz, "quote")
+ self.btnDQuoteOpen = NIconToolButton(self, iSz, "quote", "tool")
self.btnDQuoteOpen.clicked.connect(self._changeDoubleQuoteOpen)
self.mainForm.addRow(
self.tr("Double quote open style"), self.fmtDQuoteOpen,
@@ -843,7 +852,7 @@ class GuiPreferences(NDialog):
self.fmtDQuoteClose.setFixedWidth(boxFixed)
self.fmtDQuoteClose.setAlignment(QtAlignCenter)
self.fmtDQuoteClose.setText(CONFIG.fmtDQuoteClose)
- self.btnDQuoteClose = NIconToolButton(self, iSz, "quote")
+ self.btnDQuoteClose = NIconToolButton(self, iSz, "quote", "tool")
self.btnDQuoteClose.clicked.connect(self._changeDoubleQuoteClose)
self.mainForm.addRow(
self.tr("Double quote close style"), self.fmtDQuoteClose,
diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py
index 5e16d52e..16a4a865 100644
--- a/novelwriter/dialogs/projectsettings.py
+++ b/novelwriter/dialogs/projectsettings.py
@@ -41,13 +41,13 @@ from novelwriter import CONFIG, SHARED
from novelwriter.common import formatFileFilter, qtAddAction, qtLambda, simplified
from novelwriter.constants import nwLabels, trConst
from novelwriter.core.status import CUSTOM_COL, NWStatus, StatusEntry
-from novelwriter.enum import nwStatusShape
+from novelwriter.enum import nwStandardButton, nwStatusShape
from novelwriter.extensions.configlayout import NColorLabel, NFixedPage, NScrollableForm
from novelwriter.extensions.modified import NComboBox, NDialog, NIconToolButton
from novelwriter.extensions.pagedsidebar import NPagedSideBar
from novelwriter.extensions.switch import NSwitch
from novelwriter.types import (
- QtDialogCancel, QtDialogSave, QtSizeMinimum, QtSizeMinimumExpanding,
+ QtRoleAccept, QtRoleReject, QtSizeMinimum, QtSizeMinimumExpanding,
QtUserRole
)
@@ -95,9 +95,15 @@ class GuiProjectSettings(NDialog):
self.sidebar.buttonClicked.connect(self._sidebarClicked)
# Buttons
- self.buttonBox = QDialogButtonBox(QtDialogSave | QtDialogCancel, self)
- self.buttonBox.accepted.connect(self._doSave)
- self.buttonBox.rejected.connect(self.reject)
+ self.btnSave = SHARED.theme.getStandardButton(nwStandardButton.SAVE, self)
+ self.btnSave.clicked.connect(self._doSave)
+
+ self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self)
+ self.btnCancel.clicked.connect(self.reject)
+
+ self.btnBox = QDialogButtonBox(self)
+ self.btnBox.addButton(self.btnSave, QtRoleAccept)
+ self.btnBox.addButton(self.btnCancel, QtRoleReject)
# Content
SHARED.project.countStatus()
@@ -126,7 +132,7 @@ class GuiProjectSettings(NDialog):
self.outerBox = QVBoxLayout()
self.outerBox.addLayout(self.topBox)
self.outerBox.addLayout(self.mainBox)
- self.outerBox.addWidget(self.buttonBox)
+ self.outerBox.addWidget(self.btnBox)
self.outerBox.setSpacing(8)
self.setLayout(self.outerBox)
@@ -251,7 +257,7 @@ class _SettingsPage(NScrollableForm):
# Project Language
projLang = data.language or CONFIG.guiLocale
- self.projLang = NComboBox(self)
+ self.projLang = NComboBox(self, scrollable=True)
self.projLang.setMinimumWidth(200)
for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ):
self.projLang.addItem(language, tag)
@@ -263,7 +269,7 @@ class _SettingsPage(NScrollableForm):
)
# Spell Check Language
- self.spellLang = NComboBox(self)
+ self.spellLang = NComboBox(self, scrollable=True)
self.spellLang.setMinimumWidth(200)
self.spellLang.addItem(self.tr("Default"), "None")
if CONFIG.hasEnchant:
@@ -350,27 +356,27 @@ class _StatusPage(NFixedPage):
self._addItem(key, StatusEntry.duplicate(entry))
# List Controls
- self.addButton = NIconToolButton(self, iSz, "add", "green")
+ self.addButton = NIconToolButton(self, iSz, "add", "add")
self.addButton.setToolTip(self.tr("Add Label"))
self.addButton.clicked.connect(self._onItemCreate)
- self.delButton = NIconToolButton(self, iSz, "remove", "red")
+ self.delButton = NIconToolButton(self, iSz, "remove", "remove")
self.delButton.setToolTip(self.tr("Delete Label"))
self.delButton.clicked.connect(self._onItemDelete)
- self.upButton = NIconToolButton(self, iSz, "chevron_up", "blue")
+ self.upButton = NIconToolButton(self, iSz, "chevron_up", "action")
self.upButton.setToolTip(self.tr("Move Up"))
self.upButton.clicked.connect(qtLambda(self._moveItem, -1))
- self.downButton = NIconToolButton(self, iSz, "chevron_down", "blue")
+ self.downButton = NIconToolButton(self, iSz, "chevron_down", "action")
self.downButton.setToolTip(self.tr("Move Down"))
self.downButton.clicked.connect(qtLambda(self._moveItem, 1))
- self.importButton = NIconToolButton(self, iSz, "import", "green")
+ self.importButton = NIconToolButton(self, iSz, "import", "apply")
self.importButton.setToolTip(self.tr("Import Labels"))
self.importButton.clicked.connect(self._importLabels)
- self.exportButton = NIconToolButton(self, iSz, "export", "blue")
+ self.exportButton = NIconToolButton(self, iSz, "export", "action")
self.exportButton.setToolTip(self.tr("Export Labels"))
self.exportButton.clicked.connect(self._exportLabels)
@@ -723,10 +729,10 @@ class _ReplacePage(NFixedPage):
self.listBox.setSortingEnabled(True)
# List Controls
- self.addButton = NIconToolButton(self, iSz, "add", "green")
+ self.addButton = NIconToolButton(self, iSz, "add", "add")
self.addButton.clicked.connect(self._onEntryCreated)
- self.delButton = NIconToolButton(self, iSz, "remove", "red")
+ self.delButton = NIconToolButton(self, iSz, "remove", "remove")
self.delButton.clicked.connect(self._onEntryDeleted)
# Edit Form
diff --git a/novelwriter/dialogs/quotes.py b/novelwriter/dialogs/quotes.py
index 6030ecfe..2e625ba0 100644
--- a/novelwriter/dialogs/quotes.py
+++ b/novelwriter/dialogs/quotes.py
@@ -32,10 +32,12 @@ from PyQt6.QtWidgets import (
QListWidgetItem, QVBoxLayout, QWidget
)
+from novelwriter import SHARED
from novelwriter.constants import nwQuotes, trConst
+from novelwriter.enum import nwStandardButton
from novelwriter.extensions.modified import NDialog
from novelwriter.types import (
- QtAccepted, QtAlignCenter, QtAlignTop, QtDialogCancel, QtDialogOk,
+ QtAccepted, QtAlignCenter, QtAlignTop, QtRoleAccept, QtRoleReject,
QtUserRole
)
@@ -91,9 +93,15 @@ class GuiQuoteSelect(NDialog):
self.listBox.setMinimumHeight(150)
# Buttons
- self.buttonBox = QDialogButtonBox(QtDialogOk | QtDialogCancel, self)
- self.buttonBox.accepted.connect(self.accept)
- self.buttonBox.rejected.connect(self.reject)
+ self.btnOk = SHARED.theme.getStandardButton(nwStandardButton.OK, self)
+ self.btnOk.clicked.connect(self.accept)
+
+ self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self)
+ self.btnCancel.clicked.connect(self.reject)
+
+ self.btnBox = QDialogButtonBox(self)
+ self.btnBox.addButton(self.btnOk, QtRoleAccept)
+ self.btnBox.addButton(self.btnCancel, QtRoleReject)
# Assemble
self.labelBox.addWidget(self.previewLabel, 0, QtAlignTop)
@@ -103,7 +111,7 @@ class GuiQuoteSelect(NDialog):
self.innerBox.addWidget(self.listBox)
self.outerBox.addLayout(self.innerBox)
- self.outerBox.addWidget(self.buttonBox)
+ self.outerBox.addWidget(self.btnBox)
self.setLayout(self.outerBox)
diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py
index 19e2da9d..927d6f21 100644
--- a/novelwriter/dialogs/wordlist.py
+++ b/novelwriter/dialogs/wordlist.py
@@ -37,9 +37,10 @@ from PyQt6.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.common import formatFileFilter
from novelwriter.core.spellcheck import UserDictionary
+from novelwriter.enum import nwStandardButton
from novelwriter.extensions.configlayout import NColorLabel
from novelwriter.extensions.modified import NDialog, NIconToolButton
-from novelwriter.types import QtDialogClose, QtDialogSave
+from novelwriter.types import QtRoleAccept, QtRoleDestruct
if TYPE_CHECKING:
from PyQt6.QtGui import QCloseEvent
@@ -74,11 +75,11 @@ class GuiWordList(NDialog):
scale=NColorLabel.HEADER_SCALE
)
- self.importButton = NIconToolButton(self, iSz, "import", "green")
+ self.importButton = NIconToolButton(self, iSz, "import", "apply")
self.importButton.setToolTip(self.tr("Import words from text file"))
self.importButton.clicked.connect(self._importWords)
- self.exportButton = NIconToolButton(self, iSz, "export", "blue")
+ self.exportButton = NIconToolButton(self, iSz, "export", "action")
self.exportButton.setToolTip(self.tr("Export words to text file"))
self.exportButton.clicked.connect(self._exportWords)
@@ -96,11 +97,11 @@ class GuiWordList(NDialog):
# Add/Remove Form
self.newEntry = QLineEdit(self)
- self.addButton = NIconToolButton(self, iSz, "add", "green")
+ self.addButton = NIconToolButton(self, iSz, "add", "add")
self.addButton.setToolTip(self.tr("Add Word"))
self.addButton.clicked.connect(self._doAdd)
- self.delButton = NIconToolButton(self, iSz, "remove", "red")
+ self.delButton = NIconToolButton(self, iSz, "remove", "remove")
self.delButton.setToolTip(self.tr("Remove Word"))
self.delButton.clicked.connect(self._doDelete)
@@ -110,9 +111,15 @@ class GuiWordList(NDialog):
self.editBox.addWidget(self.delButton, 0)
# Buttons
- self.buttonBox = QDialogButtonBox(QtDialogSave | QtDialogClose, self)
- self.buttonBox.accepted.connect(self._doSave)
- self.buttonBox.rejected.connect(self.reject)
+ self.btnSave = SHARED.theme.getStandardButton(nwStandardButton.SAVE, self)
+ self.btnSave.clicked.connect(self._doSave)
+
+ self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self)
+ self.btnClose.clicked.connect(self.reject)
+
+ self.btnBox = QDialogButtonBox(self)
+ self.btnBox.addButton(self.btnSave, QtRoleAccept)
+ self.btnBox.addButton(self.btnClose, QtRoleDestruct)
# Assemble
self.outerBox = QVBoxLayout()
@@ -120,7 +127,7 @@ class GuiWordList(NDialog):
self.outerBox.addWidget(self.listBox, 1)
self.outerBox.addLayout(self.editBox, 0)
self.outerBox.addSpacing(12)
- self.outerBox.addWidget(self.buttonBox, 0)
+ self.outerBox.addWidget(self.btnBox, 0)
self.outerBox.setSpacing(4)
self.setLayout(self.outerBox)
diff --git a/novelwriter/enum.py b/novelwriter/enum.py
index 7ea60294..0b88f8ab 100644
--- a/novelwriter/enum.py
+++ b/novelwriter/enum.py
@@ -247,3 +247,25 @@ class nwStatusShape(Enum):
BLOCK_2 = 17
BLOCK_3 = 18
BLOCK_4 = 19
+
+
+class nwStandardButton(Enum):
+ """Enum: Standard Dialog Buttons."""
+
+ OK = 0
+ CANCEL = 1
+ YES = 2
+ NO = 3
+ OPEN = 4
+ CLOSE = 5
+ SAVE = 6
+ BROWSE = 7
+ LIST = 8
+ NEW = 9
+ CREATE = 10
+ RESET = 11
+ INSERT = 12
+ APPLY = 13
+ BUILD = 14
+ PRINT = 15
+ PREVIEW = 16
diff --git a/novelwriter/error.py b/novelwriter/error.py
index 59fafdff..0f1ff8fd 100644
--- a/novelwriter/error.py
+++ b/novelwriter/error.py
@@ -171,7 +171,7 @@ def exceptionHandler(exType: type, exValue: BaseException, exTrace: TracebackTyp
from PyQt6.QtWidgets import QApplication
- logger.critical("%s: %s", exType.__name__, str(exValue))
+ logger.critical("%s: %s", exType.__name__, exValue)
print_tb(exTrace)
try:
diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py
index 14800a5c..2c11ad8e 100644
--- a/novelwriter/extensions/configlayout.py
+++ b/novelwriter/extensions/configlayout.py
@@ -27,7 +27,7 @@ along with this program. If not, see .
""" # noqa
from __future__ import annotations
-from PyQt6.QtGui import QColor, QFont, QPalette, QPixmap
+from PyQt6.QtGui import QColor, QFont, QPalette
from PyQt6.QtWidgets import (
QAbstractButton, QFrame, QHBoxLayout, QLabel, QLayout, QScrollArea,
QVBoxLayout, QWidget
@@ -170,7 +170,7 @@ class NScrollableForm(QScrollArea):
def addRow(
self,
label: str | None,
- widget: QWidget | list[QWidget | QPixmap | int],
+ widget: QWidget | list[QWidget | int],
helpText: str = "",
unit: str | None = None,
button: QWidget | None = None,
@@ -187,10 +187,6 @@ class NScrollableForm(QScrollArea):
for item in widget:
if isinstance(item, QWidget):
wBox.addWidget(item)
- elif isinstance(item, QPixmap):
- icon = QLabel(self)
- icon.setPixmap(item)
- wBox.addWidget(icon)
elif isinstance(item, int):
wBox.addSpacing(item)
qWidget = QWidget(self)
diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py
index bb256ff1..d6a01b44 100644
--- a/novelwriter/extensions/modified.py
+++ b/novelwriter/extensions/modified.py
@@ -31,8 +31,8 @@ from typing import TYPE_CHECKING
from PyQt6.QtCore import QModelIndex, QSize, Qt, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import (
- QApplication, QComboBox, QDialog, QDoubleSpinBox, QLabel, QSpinBox,
- QToolButton, QTreeView, QWidget
+ QApplication, QComboBox, QDialog, QDoubleSpinBox, QLabel, QPushButton,
+ QSpinBox, QToolButton, QTreeView, QWidget
)
from novelwriter import CONFIG, SHARED
@@ -125,14 +125,16 @@ class NComboBox(QComboBox):
window of many widgets.
"""
- def __init__(self, parent: QWidget | None = None, maxItems: int = 15) -> None:
+ def __init__(
+ self, parent: QWidget | None = None, maxItems: int = 15, scrollable: bool = False
+ ) -> None:
super().__init__(parent=parent)
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.setMaxVisibleItems(maxItems)
-
- # The style sheet disables Fusion style pop-up mode on some platforms
- # and allows for scrolling of long lists of items
- self.setStyleSheet("QComboBox {combobox-popup: 0;}")
+ if scrollable:
+ # The style sheet disables Fusion style pop-up mode on some
+ # platforms and allows for scrolling of long lists of items
+ self.setStyleSheet("QComboBox {combobox-popup: 0;}")
def wheelEvent(self, event: QWheelEvent) -> None:
"""Only capture the mouse wheel if the widget has focus."""
@@ -199,6 +201,29 @@ class NDoubleSpinBox(QDoubleSpinBox):
event.ignore()
+class NPushButton(QPushButton):
+ """Custom: Modified QPushButton.
+
+ A quicker way to create a push button using the app theme.
+ """
+
+ def __init__(
+ self, parent: QWidget, text: str, iconSize: QSize,
+ icon: str | None = None, color: str | None = None
+ ) -> None:
+ super().__init__(parent=parent)
+ self._icon = icon
+ self._color = color
+ self.setText(text)
+ self.setIconSize(iconSize)
+ self.updateIcon()
+
+ def updateIcon(self) -> None:
+ """Update the theme icon."""
+ if self._icon and self._color:
+ self.setIcon(SHARED.theme.getIcon(self._icon, self._color))
+
+
class NIconToolButton(QToolButton):
"""Custom: Modified QToolButton.
@@ -213,12 +238,12 @@ class NIconToolButton(QToolButton):
self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.setIconSize(iconSize)
self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
- if icon:
+ if icon and color:
self.setThemeIcon(icon, color)
- def setThemeIcon(self, iconKey: str, color: str | None = None) -> None:
+ def setThemeIcon(self, icon: str, color: str) -> None:
"""Set an icon from the current theme."""
- self.setIcon(SHARED.theme.getIcon(iconKey, color))
+ self.setIcon(SHARED.theme.getIcon(icon, color))
class NIconToggleButton(QToolButton):
@@ -227,20 +252,23 @@ class NIconToggleButton(QToolButton):
A quicker way to create a toggle button using the app theme.
"""
- def __init__(self, parent: QWidget, iconSize: QSize, icon: str | None = None) -> None:
+ def __init__(
+ self, parent: QWidget, iconSize: QSize,
+ icon: str | None = None, color: str | None = None
+ ) -> None:
super().__init__(parent=parent)
self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.setIconSize(iconSize)
self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
self.setCheckable(True)
self.setStyleSheet("border: none; background: transparent;")
- if icon:
- self.setThemeIcon(icon)
+ if icon and color:
+ self.setThemeIcon(icon, color)
- def setThemeIcon(self, iconKey: str) -> None:
+ def setThemeIcon(self, icon: str, color: str) -> None:
"""Set an icon from the current theme."""
- iconSize = self.iconSize()
- self.setIcon(SHARED.theme.getToggleIcon(iconKey, (iconSize.width(), iconSize.height())))
+ size = self.iconSize()
+ self.setIcon(SHARED.theme.getToggleIcon(icon, (size.width(), size.height()), color))
class NClickableLabel(QLabel):
diff --git a/novelwriter/extensions/novelselector.py b/novelwriter/extensions/novelselector.py
index 27d217c6..64513378 100644
--- a/novelwriter/extensions/novelselector.py
+++ b/novelwriter/extensions/novelselector.py
@@ -110,7 +110,7 @@ class NovelSelector(QComboBox):
self._firstHandle = None
self.clear()
- icon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL], "blue")
+ icon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL], "root")
for tHandle, nwItem in SHARED.project.tree.iterRoots(nwItemClass.NOVEL):
if self._listFormat:
name = self._listFormat.format(nwItem.itemName)
diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py
index fd332495..04c284ed 100644
--- a/novelwriter/extensions/pagedsidebar.py
+++ b/novelwriter/extensions/pagedsidebar.py
@@ -72,6 +72,9 @@ class NPagedSideBar(QToolBar):
def setLabelColor(self, color: QColor) -> None:
"""Set the text color for the labels."""
self._labelCol = color
+ for widget in self.children():
+ if isinstance(widget, _NPagedToolLabel):
+ widget.setTextColor(color)
def addLabel(self, text: str) -> None:
"""Add a new label to the toolbar."""
@@ -188,6 +191,10 @@ class _NPagedToolLabel(QLabel):
self._textCol = textColor or self.palette().text().color()
+ def setTextColor(self, textColor: QColor | None = None) -> None:
+ """Set a new text colour."""
+ self._textCol = textColor or self.palette().text().color()
+
def paintEvent(self, event: QPaintEvent) -> None:
"""Overload the paint event to draw a simple, left aligned text
label that matches the button style.
diff --git a/novelwriter/extensions/switch.py b/novelwriter/extensions/switch.py
index cc0d43a9..983d6294 100644
--- a/novelwriter/extensions/switch.py
+++ b/novelwriter/extensions/switch.py
@@ -34,7 +34,7 @@ from novelwriter.types import QtNoPen, QtPaintAntiAlias, QtSizeFixed
class NSwitch(QAbstractButton):
"""Custom: Toggle Switch."""
- __slots__ = ("_cOff", "_cOn", "_offset", "_rH", "_rR", "_xH", "_xR", "_xW")
+ __slots__ = ("_offset", "_rH", "_rR", "_xH", "_xR", "_xW")
def __init__(self, parent: QWidget, height: int = 0) -> None:
super().__init__(parent=parent)
@@ -45,13 +45,11 @@ class NSwitch(QAbstractButton):
self._rH = self._xH - 4
self._rR = self._xR - 2
- self._cOn = SHARED.theme.accentCol
- self._cOff = self.palette().alternateBase()
-
self.setCheckable(True)
self.setSizePolicy(QtSizeFixed, QtSizeFixed)
self.setFixedWidth(self._xW)
self.setFixedHeight(self._xH)
+ self.setUpdatesEnabled(True)
self._offset = self._xR
self.clicked.connect(self._onClick)
@@ -96,7 +94,7 @@ class NSwitch(QAbstractButton):
painter.setOpacity(1.0 if self.isEnabled() else 0.5)
painter.setPen(palette.highlight().color() if self.hasFocus() else palette.mid().color())
- painter.setBrush(self._cOn if self.isChecked() else self._cOff)
+ painter.setBrush(SHARED.theme.accentCol if self.isChecked() else palette.alternateBase())
painter.drawRoundedRect(0, 0, self._xW, self._xH, self._xR, self._xR)
painter.setPen(QtNoPen)
@@ -110,6 +108,10 @@ class NSwitch(QAbstractButton):
self.setCursor(Qt.CursorShape.PointingHandCursor)
super().enterEvent(event)
+ ##
+ # Internal Functions
+ ##
+
@pyqtSlot(bool)
def _onClick(self, checked: bool) -> None:
"""Animate the toggle action."""
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index a7349699..fc05f063 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -39,13 +39,14 @@ from enum import Enum, IntFlag
from time import time
from PyQt6.QtCore import (
- QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal,
- pyqtSlot
+ QObject, QPoint, QRect, QRegularExpression, QRunnable, Qt, QTimer,
+ QVariant, pyqtSignal, pyqtSlot
)
from PyQt6.QtGui import (
- QAction, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeyEvent,
- QKeySequence, QMouseEvent, QPalette, QPixmap, QResizeEvent, QShortcut,
- QTextBlock, QTextCursor, QTextDocument, QTextFormat, QTextOption
+ QAction, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent,
+ QInputMethodEvent, QKeyEvent, QKeySequence, QMouseEvent, QPalette, QPixmap,
+ QResizeEvent, QShortcut, QTextBlock, QTextCursor, QTextDocument,
+ QTextFormat, QTextOption
)
from PyQt6.QtWidgets import (
QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu,
@@ -74,9 +75,10 @@ from novelwriter.text.counting import standardCounter
from novelwriter.tools.lipsum import GuiLipsum
from novelwriter.types import (
QtAlignCenterTop, QtAlignJustify, QtAlignLeft, QtAlignLeftTop,
- QtAlignRight, QtKeepAnchor, QtModCtrl, QtModNone, QtModShift, QtMouseLeft,
- QtMoveAnchor, QtMoveLeft, QtMoveRight, QtScrollAlwaysOff, QtScrollAsNeeded,
- QtTransparent
+ QtAlignRight, QtImCursorRectangle, QtKeepAnchor, QtModCtrl, QtModNone,
+ QtModShift, QtMouseLeft, QtMoveAnchor, QtMoveLeft, QtMoveRight,
+ QtScrollAlwaysOff, QtScrollAsNeeded, QtSelectBlock, QtSelectDocument,
+ QtSelectWord, QtTransparent
)
logger = logging.getLogger(__name__)
@@ -154,7 +156,7 @@ class GuiDocEditor(QPlainTextEdit):
# Completer
self._completer = CommandCompleter(self)
- self._completer.complete.connect(self._insertCompletion)
+ self._completer.insertText.connect(self._insertCompletion)
# Create Custom Document
self._qDocument = GuiTextDocument(self)
@@ -290,6 +292,8 @@ class GuiDocEditor(QPlainTextEdit):
def updateTheme(self) -> None:
"""Update theme elements."""
+ logger.debug("Theme Update: GuiDocEditor")
+
self.docSearch.updateTheme()
self.docHeader.updateTheme()
self.docFooter.updateTheme()
@@ -671,7 +675,7 @@ class GuiDocEditor(QPlainTextEdit):
self.spellCheckStateChanged.emit(state)
self.spellCheckDocument()
- logger.debug("Spell check is set to '%s'", str(state))
+ logger.debug("Spell check is set to '%s'", state)
def spellCheckDocument(self) -> None:
"""Rerun the highlighter to update spell checking status of the
@@ -730,9 +734,9 @@ class GuiDocEditor(QPlainTextEdit):
elif action == nwDocAction.D_QUOTE:
self._wrapSelection(CONFIG.fmtDQuoteOpen, CONFIG.fmtDQuoteClose)
elif action == nwDocAction.SEL_ALL:
- self._makeSelection(QTextCursor.SelectionType.Document)
+ self._makeSelection(QtSelectDocument)
elif action == nwDocAction.SEL_PARA:
- self._makeSelection(QTextCursor.SelectionType.BlockUnderCursor)
+ self._makeSelection(QtSelectBlock)
elif action == nwDocAction.BLOCK_H1:
self._formatBlock(nwDocAction.BLOCK_H1)
elif action == nwDocAction.BLOCK_H2:
@@ -784,7 +788,7 @@ class GuiDocEditor(QPlainTextEdit):
elif action == nwDocAction.SC_SUB:
self._wrapSelection(nwShortcode.SUB_O, nwShortcode.SUB_C)
else:
- logger.debug("Unknown or unsupported document action '%s'", str(action))
+ logger.debug("Unknown or unsupported document action '%s'", action)
self._allowAutoReplace(True)
return False
@@ -905,7 +909,7 @@ class GuiDocEditor(QPlainTextEdit):
return True
##
- # Document Events and Maintenance
+ # Events and Overloads
##
def keyPressEvent(self, event: QKeyEvent) -> None:
@@ -1003,6 +1007,26 @@ class GuiDocEditor(QPlainTextEdit):
self.updateDocMargins()
super().resizeEvent(event)
+ def inputMethodEvent(self, event: QInputMethodEvent) -> None:
+ """Handle text being input from CJK input methods."""
+ super().inputMethodEvent(event)
+ if event.commitString():
+ # See issues #2267 and #2517
+ self.ensureCursorVisible()
+ self._completerToCursor()
+
+ def inputMethodQuery(self, query: Qt.InputMethodQuery) -> QRect | QVariant:
+ """Adjust completion windows for CJK input methods to consider
+ the viewport margins.
+ """
+ if query == QtImCursorRectangle:
+ # See issues #2267 and #2517
+ vM = self.viewportMargins()
+ rect = self.cursorRect()
+ rect.translate(vM.left(), vM.top())
+ return rect
+ return super().inputMethodQuery(query)
+
##
# Public Slots
##
@@ -1062,24 +1086,20 @@ class GuiDocEditor(QPlainTextEdit):
if (block := self._qDocument.findBlock(pos)).isValid():
text = block.text()
+
if text and text[0] in "@%" and added + removed == 1:
# Only run on single character changes, or it will trigger
# at unwanted times when other changes are made to the document
cursor = self.textCursor()
bPos = cursor.positionInBlock()
- if bPos > 0 and (viewport := self.viewport()):
+ if bPos > 0:
if text[0] == "@":
show = self._completer.updateMetaText(text, bPos)
else:
show = self._completer.updateCommentText(text, bPos)
if show:
- point = self.cursorRect().bottomRight()
- self._completer.move(viewport.mapToGlobal(point))
self._completer.show()
- else:
- self._completer.close()
- else:
- self._completer.close()
+ self._completerToCursor()
if self._doReplace and added == 1:
cursor = self.textCursor()
@@ -1104,7 +1124,7 @@ class GuiDocEditor(QPlainTextEdit):
cursor.setPosition(check, QtMoveAnchor)
cursor.setPosition(check + length, QtKeepAnchor)
cursor.insertText(text)
- self._completer.hide()
+ self._completer.close()
@pyqtSlot()
def _openContextFromCursor(self) -> None:
@@ -1157,13 +1177,9 @@ class GuiDocEditor(QPlainTextEdit):
action = qtAddAction(ctxMenu, self.tr("Select All"))
action.triggered.connect(qtLambda(self.docAction, nwDocAction.SEL_ALL))
action = qtAddAction(ctxMenu, self.tr("Select Word"))
- action.triggered.connect(qtLambda(
- self._makePosSelection, QTextCursor.SelectionType.WordUnderCursor, pos,
- ))
+ action.triggered.connect(qtLambda(self._makePosSelection, QtSelectWord, pos))
action = qtAddAction(ctxMenu, self.tr("Select Paragraph"))
- action.triggered.connect(qtLambda(
- self._makePosSelection, QTextCursor.SelectionType.BlockUnderCursor, pos
- ))
+ action.triggered.connect(qtLambda(self._makePosSelection, QtSelectBlock, pos))
# Spell Checking
if SHARED.project.data.spellCheck:
@@ -1733,7 +1749,7 @@ class GuiDocEditor(QPlainTextEdit):
elif action == nwDocAction.BLOCK_TXT:
text = temp
else:
- logger.error("Unknown or unsupported block format requested: '%s'", str(action))
+ logger.error("Unknown or unsupported block format requested: '%s'", action)
return nwDocAction.NO_ACTION, "", 0
return action, text, offset
@@ -1743,7 +1759,7 @@ class GuiDocEditor(QPlainTextEdit):
cursor = self.textCursor()
block = cursor.block()
if not block.isValid():
- logger.debug("Invalid block selected for action '%s'", str(action))
+ logger.debug("Invalid block selected for action '%s'", action)
return False
action, text, offset = self._processBlockFormat(action, block.text())
@@ -1753,7 +1769,7 @@ class GuiDocEditor(QPlainTextEdit):
pos = cursor.position()
cursor.beginEditBlock()
- self._makeSelection(QTextCursor.SelectionType.BlockUnderCursor, cursor)
+ self._makeSelection(QtSelectBlock, cursor)
cursor.insertText(text)
cursor.endEditBlock()
@@ -1781,7 +1797,7 @@ class GuiDocEditor(QPlainTextEdit):
if pAction != nwDocAction.NO_ACTION and blockText.strip():
action = pAction # First block decides further actions
cursor.setPosition(block.position())
- self._makeSelection(QTextCursor.SelectionType.BlockUnderCursor, cursor)
+ self._makeSelection(QtSelectBlock, cursor)
cursor.insertText(text)
toggle = False
@@ -1801,7 +1817,7 @@ class GuiDocEditor(QPlainTextEdit):
"""Strip line breaks within paragraphs in the selected text."""
cursor = self.textCursor()
if not cursor.hasSelection():
- cursor.select(QTextCursor.SelectionType.Document)
+ cursor.select(QtSelectDocument)
rS = 0
rE = self._qDocument.characterCount()
@@ -1866,6 +1882,12 @@ class GuiDocEditor(QPlainTextEdit):
# Internal Functions
##
+ def _completerToCursor(self) -> None:
+ """Make sure the completer menu is positioned by the cursor."""
+ if self._completer.isVisible() and (viewport := self.viewport()):
+ point = self.cursorRect().bottomLeft()
+ self._completer.move(viewport.mapToGlobal(point))
+
def _correctWord(self, cursor: QTextCursor, word: str) -> None:
"""Slot for the spell check context menu triggering the
replacement of a word with the word from the dictionary.
@@ -2012,10 +2034,10 @@ class GuiDocEditor(QPlainTextEdit):
cursor.clearSelection()
cursor.select(mode)
- if mode == QTextCursor.SelectionType.WordUnderCursor:
+ if mode == QtSelectWord:
cursor = self._autoSelect()
- elif mode == QTextCursor.SelectionType.BlockUnderCursor:
+ elif mode == QtSelectBlock:
# This selection mode also selects the preceding paragraph
# separator, which we want to avoid.
posS = cursor.selectionStart()
@@ -2050,10 +2072,13 @@ class CommandCompleter(QMenu):
called on every keystroke on a line starting with @ or %.
"""
- complete = pyqtSignal(int, int, str)
+ __slots__ = ("_parent",)
+
+ insertText = pyqtSignal(int, int, str)
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
+ self._parent = parent
def updateMetaText(self, text: str, pos: int) -> bool:
"""Update the menu options based on the line of text."""
@@ -2139,14 +2164,14 @@ class CommandCompleter(QMenu):
def keyPressEvent(self, event: QKeyEvent) -> None:
"""Capture keypresses and forward most of them to the editor."""
- parent = self.parent()
if event.key() in (
Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Return,
Qt.Key.Key_Enter, Qt.Key.Key_Escape
):
super().keyPressEvent(event)
- elif isinstance(parent, GuiDocEditor):
- parent.keyPressEvent(event)
+ else:
+ self.close() # Close to release the event lock before forwarding the key press (#2510)
+ self._parent.keyPressEvent(event)
##
# Internal Functions
@@ -2154,7 +2179,7 @@ class CommandCompleter(QMenu):
def _emitComplete(self, pos: int, length: int, value: str) -> None:
"""Emit the signal to indicate a selection has been made."""
- self.complete.emit(pos, length, value)
+ self.insertText.emit(pos, length, value)
class BackgroundWordCounter(QRunnable):
@@ -2455,25 +2480,26 @@ class GuiDocToolBar(QWidget):
def updateTheme(self) -> None:
"""Initialise GUI elements that depend on specific settings."""
- syntax = SHARED.theme.syntaxTheme
+ logger.debug("Theme Update: GuiDocToolBar")
+ syntax = SHARED.theme.syntaxTheme
palette = self.palette()
palette.setColor(QPalette.ColorRole.Window, syntax.back)
palette.setColor(QPalette.ColorRole.WindowText, syntax.text)
palette.setColor(QPalette.ColorRole.Text, syntax.text)
self.setPalette(palette)
- self.tbBoldMD.setThemeIcon("fmt_bold", "orange")
- self.tbItalicMD.setThemeIcon("fmt_italic", "orange")
- self.tbStrikeMD.setThemeIcon("fmt_strike", "orange")
- self.tbMarkMD.setThemeIcon("fmt_mark", "orange")
- self.tbBold.setThemeIcon("fmt_bold")
- self.tbItalic.setThemeIcon("fmt_italic")
- self.tbStrike.setThemeIcon("fmt_strike")
- self.tbUnderline.setThemeIcon("fmt_underline")
- self.tbMark.setThemeIcon("fmt_mark")
- self.tbSuperscript.setThemeIcon("fmt_superscript")
- self.tbSubscript.setThemeIcon("fmt_subscript")
+ self.tbBoldMD.setThemeIcon("fmt_bold", "mdformat")
+ self.tbItalicMD.setThemeIcon("fmt_italic", "mdformat")
+ self.tbStrikeMD.setThemeIcon("fmt_strike", "mdformat")
+ self.tbMarkMD.setThemeIcon("fmt_mark", "mdformat")
+ self.tbBold.setThemeIcon("fmt_bold", "scformat")
+ self.tbItalic.setThemeIcon("fmt_italic", "scformat")
+ self.tbStrike.setThemeIcon("fmt_strike", "scformat")
+ self.tbUnderline.setThemeIcon("fmt_underline", "scformat")
+ self.tbMark.setThemeIcon("fmt_mark", "scformat")
+ self.tbSuperscript.setThemeIcon("fmt_superscript", "scformat")
+ self.tbSubscript.setThemeIcon("fmt_subscript", "scformat")
class GuiDocEditSearch(QFrame):
@@ -2567,7 +2593,7 @@ class GuiDocEditSearch(QFrame):
# Buttons
# =======
- self.showReplace = NIconToggleButton(self, iSz, "unfold")
+ self.showReplace = NIconToggleButton(self, iSz)
self.showReplace.toggled.connect(self._doToggleReplace)
self.searchButton = NIconToolButton(self, iSz)
@@ -2693,22 +2719,24 @@ class GuiDocEditSearch(QFrame):
def updateTheme(self) -> None:
"""Update theme elements."""
- palette = QApplication.palette()
+ logger.debug("Theme Update: GuiDocEditSearch")
+ palette = QApplication.palette()
self.setPalette(palette)
self.searchBox.setPalette(palette)
self.replaceBox.setPalette(palette)
# Set icons
- self.toggleCase.setIcon(SHARED.theme.getIcon("search_case"))
- self.toggleWord.setIcon(SHARED.theme.getIcon("search_word"))
- self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex"))
- self.toggleLoop.setIcon(SHARED.theme.getIcon("search_loop"))
- self.toggleProject.setIcon(SHARED.theme.getIcon("search_project"))
- self.toggleMatchCap.setIcon(SHARED.theme.getIcon("search_preserve"))
- self.cancelSearch.setIcon(SHARED.theme.getIcon("search_cancel"))
- self.searchButton.setThemeIcon("search", "green")
- self.replaceButton.setThemeIcon("search_replace", "green")
+ self.toggleCase.setIcon(SHARED.theme.getIcon("search_case", "tool"))
+ self.toggleWord.setIcon(SHARED.theme.getIcon("search_word", "tool"))
+ self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex", "tool"))
+ self.toggleLoop.setIcon(SHARED.theme.getIcon("search_loop", "tool"))
+ self.toggleProject.setIcon(SHARED.theme.getIcon("search_project", "tool"))
+ self.toggleMatchCap.setIcon(SHARED.theme.getIcon("search_preserve", "tool"))
+ self.cancelSearch.setIcon(SHARED.theme.getIcon("search_cancel", "tool"))
+ self.searchButton.setThemeIcon("search", "action")
+ self.replaceButton.setThemeIcon("search_replace", "apply")
+ self.showReplace.setThemeIcon("unfold", "default")
# Set stylesheets
self.searchOpt.setStyleSheet("QToolBar {padding: 0;}")
@@ -2937,11 +2965,13 @@ class GuiDocEditHeader(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
- self.tbButton.setThemeIcon("fmt_toolbar", "blue")
- self.outlineButton.setThemeIcon("list", "blue")
- self.searchButton.setThemeIcon("search", "blue")
- self.minmaxButton.setThemeIcon("maximise", "blue")
- self.closeButton.setThemeIcon("close", "red")
+ logger.debug("Theme Update: GuiDocEditHeader")
+
+ self.tbButton.setThemeIcon("fmt_toolbar", "action")
+ self.outlineButton.setThemeIcon("list", "action")
+ self.searchButton.setThemeIcon("search", "action")
+ self.minmaxButton.setThemeIcon("maximise", "action")
+ self.closeButton.setThemeIcon("close", "reject")
buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
self.tbButton.setStyleSheet(buttonStyle)
@@ -3007,7 +3037,7 @@ class GuiDocEditHeader(QWidget):
@pyqtSlot(bool)
def _focusModeChanged(self, focusMode: bool) -> None:
"""Update minimise/maximise icon of the Focus Mode button."""
- self.minmaxButton.setThemeIcon("minimise" if focusMode else "maximise", "blue")
+ self.minmaxButton.setThemeIcon("minimise" if focusMode else "maximise", "action")
##
# Events
@@ -3126,6 +3156,8 @@ class GuiDocEditFooter(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
+ logger.debug("Theme Update: GuiDocEditFooter")
+
iPx = round(0.9*SHARED.theme.baseIconHeight)
self.linesIcon.setPixmap(SHARED.theme.getPixmap("lines", (iPx, iPx)))
self.wordsIcon.setPixmap(SHARED.theme.getPixmap("stats", (iPx, iPx)))
diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py
index 3cd1cb1d..f20f5f5e 100644
--- a/novelwriter/gui/dochighlight.py
+++ b/novelwriter/gui/dochighlight.py
@@ -94,8 +94,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
syntax = SHARED.theme.syntaxTheme
colEmph = syntax.emph if CONFIG.highlightEmph else None
- colBreak = QColor(syntax.emph)
- colBreak.setAlpha(64)
# Create Character Formats
self._addCharFormat("text", syntax.text)
@@ -112,7 +110,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self._addCharFormat("strike", syntax.hidden, "s")
self._addCharFormat("mark", syntax.mark, "bg")
self._addCharFormat("mspaces", syntax.error, "err")
- self._addCharFormat("nobreak", colBreak, "bg")
+ self._addCharFormat("nobreak", syntax.space, "bg")
self._addCharFormat("altdialog", syntax.dialA)
self._addCharFormat("dialog", syntax.dialN)
self._addCharFormat("replace", syntax.repTag)
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 10ef9c46..b9dcb3d6 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -53,7 +53,8 @@ from novelwriter.formats.toqdoc import ToQTextDocument
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
from novelwriter.types import (
QtAlignCenterTop, QtKeepAnchor, QtMouseLeft, QtMoveAnchor,
- QtScrollAlwaysOff, QtScrollAsNeeded
+ QtScrollAlwaysOff, QtScrollAsNeeded, QtSelectBlock, QtSelectDocument,
+ QtSelectWord
)
logger = logging.getLogger(__name__)
@@ -140,6 +141,7 @@ class GuiDocViewer(QTextBrowser):
def updateTheme(self) -> None:
"""Update theme elements."""
+ logger.debug("Theme Update: GuiDocViewer")
self.docHeader.updateTheme()
self.docFooter.updateTheme()
@@ -287,11 +289,11 @@ class GuiDocViewer(QTextBrowser):
elif action == nwDocAction.COPY:
self.copy()
elif action == nwDocAction.SEL_ALL:
- self._makeSelection(QTextCursor.SelectionType.Document)
+ self._makeSelection(QtSelectDocument)
elif action == nwDocAction.SEL_PARA:
- self._makeSelection(QTextCursor.SelectionType.BlockUnderCursor)
+ self._makeSelection(QtSelectBlock)
else:
- logger.debug("Unknown or unsupported document action '%s'", str(action))
+ logger.debug("Unknown or unsupported document action '%s'", action)
return False
return True
@@ -400,14 +402,10 @@ class GuiDocViewer(QTextBrowser):
action.triggered.connect(qtLambda(self.docAction, nwDocAction.SEL_ALL))
action = qtAddAction(ctxMenu, self.tr("Select Word"))
- action.triggered.connect(qtLambda(
- self._makePosSelection, QTextCursor.SelectionType.WordUnderCursor, point
- ))
+ action.triggered.connect(qtLambda(self._makePosSelection, QtSelectWord, point))
action = qtAddAction(ctxMenu, self.tr("Select Paragraph"))
- action.triggered.connect(qtLambda(
- self._makePosSelection, QTextCursor.SelectionType.BlockUnderCursor, point
- ))
+ action.triggered.connect(qtLambda(self._makePosSelection, QtSelectBlock, point))
# Open the context menu
if viewport := self.viewport():
@@ -466,7 +464,7 @@ class GuiDocViewer(QTextBrowser):
cursor.clearSelection()
cursor.select(selType)
- if selType == QTextCursor.SelectionType.BlockUnderCursor:
+ if selType == QtSelectBlock:
# This selection mode also selects the preceding paragraph
# separator, which we want to avoid.
posS = cursor.selectionStart()
@@ -727,12 +725,14 @@ class GuiDocViewHeader(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
- self.outlineButton.setThemeIcon("list", "blue")
- self.backButton.setThemeIcon("chevron_left", "blue")
- self.forwardButton.setThemeIcon("chevron_right", "blue")
- self.editButton.setThemeIcon("edit", "green")
- self.refreshButton.setThemeIcon("refresh", "green")
- self.closeButton.setThemeIcon("close", "red")
+ logger.debug("Theme Update: GuiDocViewHeader")
+
+ self.outlineButton.setThemeIcon("list", "action")
+ self.backButton.setThemeIcon("chevron_left", "action")
+ self.forwardButton.setThemeIcon("chevron_right", "action")
+ self.editButton.setThemeIcon("edit", "change")
+ self.refreshButton.setThemeIcon("refresh", "change")
+ self.closeButton.setThemeIcon("close", "reject")
buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
self.outlineButton.setStyleSheet(buttonStyle)
@@ -913,11 +913,12 @@ class GuiDocViewFooter(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
- # Icons
- fPx = int(0.9*SHARED.theme.fontPixelSize)
- bulletIcon = SHARED.theme.getToggleIcon("bullet", (fPx, fPx), "blue")
+ logger.debug("Theme Update: GuiDocViewFooter")
- self.showHide.setThemeIcon("panel")
+ fPx = int(0.9*SHARED.theme.fontPixelSize)
+ bulletIcon = SHARED.theme.getToggleIcon("bullet", (fPx, fPx), "action")
+
+ self.showHide.setThemeIcon("panel", "default")
self.showComments.setIcon(bulletIcon)
self.showSynopsis.setIcon(bulletIcon)
self.showNotes.setIcon(bulletIcon)
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index 8ac9816a..fed0e4a3 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -106,7 +106,9 @@ class GuiDocViewerPanel(QWidget):
def updateTheme(self, updateTabs: bool = True) -> None:
"""Update theme elements."""
- self.optsButton.setThemeIcon("more_vertical")
+ logger.debug("Theme Update: GuiDocViewerPanel")
+
+ self.optsButton.setThemeIcon("more_vertical", "default")
self.optsButton.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON))
self.mainTabs.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_FLAT_TABS))
if updateTabs:
@@ -261,8 +263,8 @@ class _ViewPanelBackRefs(QTreeWidget):
header.setSectionsMovable(False)
# Cache Icons Locally
- self._editIcon = SHARED.theme.getIcon("edit", "green")
- self._viewIcon = SHARED.theme.getIcon("view", "blue")
+ self._editIcon = SHARED.theme.getIcon("edit", "change")
+ self._viewIcon = SHARED.theme.getIcon("view", "action")
# Signals
self.clicked.connect(self._treeItemClicked)
@@ -270,8 +272,10 @@ class _ViewPanelBackRefs(QTreeWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
- self._editIcon = SHARED.theme.getIcon("edit", "green")
- self._viewIcon = SHARED.theme.getIcon("view", "blue")
+ logger.debug("Theme Update: _ViewPanelBackRefs")
+
+ self._editIcon = SHARED.theme.getIcon("edit", "change")
+ self._viewIcon = SHARED.theme.getIcon("view", "action")
for i in range(self.topLevelItemCount()):
if item := self.topLevelItem(i):
item.setIcon(self.C_EDIT, self._editIcon)
@@ -400,9 +404,11 @@ class _ViewPanelKeyWords(QTreeWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
+ logger.debug("Theme Update: _ViewPanelKeyWords")
+
self._classIcon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[self._class], "root")
- self._editIcon = SHARED.theme.getIcon("edit", "green")
- self._viewIcon = SHARED.theme.getIcon("view", "blue")
+ self._editIcon = SHARED.theme.getIcon("edit", "change")
+ self._viewIcon = SHARED.theme.getIcon("view", "action")
def countEntries(self) -> int:
"""Return the number of items in the list."""
diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py
index d087f742..697d8b8c 100644
--- a/novelwriter/gui/itemdetails.py
+++ b/novelwriter/gui/itemdetails.py
@@ -216,6 +216,7 @@ class GuiItemDetails(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
+ logger.debug("Theme Update: GuiItemDetails")
self.updateViewBox(self._handle)
def updateViewBox(self, tHandle: str | None) -> None:
diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py
index a5521ef8..25b9cb9e 100644
--- a/novelwriter/gui/noveltree.py
+++ b/novelwriter/gui/noveltree.py
@@ -89,6 +89,7 @@ class GuiNovelView(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
+ logger.debug("Theme Update: GuiNovelView")
self.novelBar.updateTheme()
def initSettings(self) -> None:
@@ -244,12 +245,12 @@ class GuiNovelToolBar(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
- # Icons
- self.tbNovel.setThemeIcon("cls_novel", "red")
- self.tbRefresh.setThemeIcon("refresh", "green")
- self.tbMore.setThemeIcon("more_vertical")
+ logger.debug("Theme Update: GuiNovelToolBar")
+
+ self.tbNovel.setThemeIcon("cls_novel", "root")
+ self.tbRefresh.setThemeIcon("refresh", "change")
+ self.tbMore.setThemeIcon("more_vertical", "default")
- # StyleSheets
buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
self.tbNovel.setStyleSheet(buttonStyle)
self.tbRefresh.setStyleSheet(buttonStyle)
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index 3df01d3f..1bc185a7 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -103,6 +103,8 @@ class GuiOutlineView(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
+ logger.debug("Theme Update: GuiOutlineView")
+
self.outlineBar.updateTheme()
self.outlineTree.updateTheme()
self.outlineTree.refreshTree(
@@ -258,11 +260,13 @@ class GuiOutlineToolBar(QToolBar):
def updateTheme(self) -> None:
"""Update theme elements."""
+ logger.debug("Theme Update: GuiOutlineToolBar")
+
self.setStyleSheet("QToolBar {border: 0px;}")
self.novelValue.refreshNovelList()
- self.aRefresh.setIcon(SHARED.theme.getIcon("refresh", "green"))
- self.aExport.setIcon(SHARED.theme.getIcon("export", "blue"))
- self.tbColumns.setIcon(SHARED.theme.getIcon("more_vertical"))
+ self.aRefresh.setIcon(SHARED.theme.getIcon("refresh", "change"))
+ self.aExport.setIcon(SHARED.theme.getIcon("export", "action"))
+ self.tbColumns.setIcon(SHARED.theme.getIcon("more_vertical", "default"))
self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}")
self.novelLabel.setTextColors(color=self.palette().windowText().color())
@@ -454,6 +458,8 @@ class GuiOutlineTree(QTreeWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
+ logger.debug("Theme Update: GuiOutlineTree")
+
iType = nwItemType.FILE
iClass = nwItemClass.NO_CLASS
iLayout = nwItemLayout.DOCUMENT
@@ -586,7 +592,7 @@ class GuiOutlineTree(QTreeWidget):
try:
for name, (hidden, width) in colState.items():
if name not in nwOutline.__members__:
- logger.warning("Ignored unknown outline column '%s'", str(name))
+ logger.warning("Ignored unknown outline column '%s'", name)
continue
tmpOrder.append(nwOutline[name])
tmpHidden[nwOutline[name]] = hidden
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index ab79cfb6..de427a1d 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -138,6 +138,7 @@ class GuiProjectView(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
+ logger.debug("Theme Update: GuiProjectView")
self.projBar.updateTheme()
def initSettings(self) -> None:
@@ -346,6 +347,8 @@ class GuiProjectToolBar(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
+ logger.debug("Theme Update: GuiProjectToolBar")
+
buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
self.tbQuick.setStyleSheet(buttonStyle)
self.tbMoveU.setStyleSheet(buttonStyle)
@@ -353,11 +356,11 @@ class GuiProjectToolBar(QWidget):
self.tbAdd.setStyleSheet(buttonStyle)
self.tbMore.setStyleSheet(buttonStyle)
- self.tbQuick.setThemeIcon("bookmarks", "blue")
- self.tbMoveU.setThemeIcon("chevron_up", "blue")
- self.tbMoveD.setThemeIcon("chevron_down", "blue")
- self.tbAdd.setThemeIcon("add", "green")
- self.tbMore.setThemeIcon("more_vertical")
+ self.tbQuick.setThemeIcon("bookmarks", "action")
+ self.tbMoveU.setThemeIcon("chevron_up", "action")
+ self.tbMoveD.setThemeIcon("chevron_down", "action")
+ self.tbAdd.setThemeIcon("add", "add")
+ self.tbMore.setThemeIcon("more_vertical", "default")
self.aAddScene.setIcon(SHARED.theme.getIcon("prj_scene", "scene"))
self.aAddChap.setIcon(SHARED.theme.getIcon("prj_chapter", "chapter"))
@@ -1180,10 +1183,10 @@ class _TreeContextMenu(QMenu):
if len(self._indices) > 1:
mSub = qtAddMenu(self, self.tr("Set Active to ..."))
aOne = qtAddAction(mSub, self._tree.trActive)
- aOne.setIcon(SHARED.theme.getIcon("checked", "green"))
+ aOne.setIcon(SHARED.theme.getIcon("checked", "accept"))
aOne.triggered.connect(qtLambda(self._iterItemActive, True))
aTwo = qtAddAction(mSub, self._tree.trInactive)
- aTwo.setIcon(SHARED.theme.getIcon("unchecked", "red"))
+ aTwo.setIcon(SHARED.theme.getIcon("unchecked", "reject"))
aTwo.triggered.connect(qtLambda(self._iterItemActive, False))
else:
action = qtAddAction(self, self.tr("Toggle Active"))
diff --git a/novelwriter/gui/search.py b/novelwriter/gui/search.py
index 5313d676..1571591b 100644
--- a/novelwriter/gui/search.py
+++ b/novelwriter/gui/search.py
@@ -107,7 +107,7 @@ class GuiProjectSearch(QWidget):
# Search Box
self.searchAction = QAction("", self)
- self.searchAction.setIcon(SHARED.theme.getIcon("search", "blue"))
+ self.searchAction.setIcon(SHARED.theme.getIcon("search", "apply"))
self.searchAction.triggered.connect(self._processSearch)
self.searchText = QLineEdit(self)
@@ -158,6 +158,8 @@ class GuiProjectSearch(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
+ logger.debug("Theme Update: GuiProjectSearch")
+
palette = QApplication.palette()
colBase = palette.base().color().name(QtHexArgb)
colFocus = palette.highlight().color().name(QtHexArgb)
@@ -168,10 +170,10 @@ class GuiProjectSearch(QWidget):
f"QLineEdit:focus {{border: 1px solid {colFocus};}} "
)
- self.searchAction.setIcon(SHARED.theme.getIcon("search", "blue"))
- self.toggleCase.setIcon(SHARED.theme.getIcon("search_case"))
- self.toggleWord.setIcon(SHARED.theme.getIcon("search_word"))
- self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex"))
+ self.searchAction.setIcon(SHARED.theme.getIcon("search", "apply"))
+ self.toggleCase.setIcon(SHARED.theme.getIcon("search_case", "tool"))
+ self.toggleWord.setIcon(SHARED.theme.getIcon("search_word", "tool"))
+ self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex", "tool"))
def processReturn(self) -> None:
"""Process a return keypress forwarded from the main GUI."""
diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py
index 6440fc15..b03302ac 100644
--- a/novelwriter/gui/sidebar.py
+++ b/novelwriter/gui/sidebar.py
@@ -129,8 +129,9 @@ class GuiSideBar(QWidget):
def updateTheme(self) -> None:
"""Initialise GUI elements that depend on specific settings."""
- buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON)
+ logger.debug("Theme Update: GuiSideBar")
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON)
self.tbProject.setStyleSheet(buttonStyle)
self.tbNovel.setStyleSheet(buttonStyle)
self.tbSearch.setStyleSheet(buttonStyle)
@@ -141,14 +142,14 @@ class GuiSideBar(QWidget):
self.tbTheme.setStyleSheet(buttonStyle)
self.tbSettings.setStyleSheet(buttonStyle)
- self.tbProject.setThemeIcon("sb_project")
- self.tbNovel.setThemeIcon("sb_novel")
- self.tbSearch.setThemeIcon("sb_search")
- self.tbOutline.setThemeIcon("sb_outline")
- self.tbBuild.setThemeIcon("sb_build")
- self.tbDetails.setThemeIcon("sb_details")
- self.tbStats.setThemeIcon("sb_stats")
- self.tbSettings.setThemeIcon("settings")
+ self.tbProject.setThemeIcon("sb_project", "sidebar")
+ self.tbNovel.setThemeIcon("sb_novel", "sidebar")
+ self.tbSearch.setThemeIcon("sb_search", "sidebar")
+ self.tbOutline.setThemeIcon("sb_outline", "sidebar")
+ self.tbBuild.setThemeIcon("sb_build", "sidebar")
+ self.tbDetails.setThemeIcon("sb_details", "sidebar")
+ self.tbStats.setThemeIcon("sb_stats", "sidebar")
+ self.tbSettings.setThemeIcon("settings", "sidebar")
self._setThemeModeIcon()
@@ -175,7 +176,7 @@ class GuiSideBar(QWidget):
def _setThemeModeIcon(self) -> None:
"""Set the theme button icon."""
- self.tbTheme.setThemeIcon(nwLabels.THEME_MODE_ICON[CONFIG.themeMode])
+ self.tbTheme.setThemeIcon(nwLabels.THEME_MODE_ICON[CONFIG.themeMode], "sidebar")
self.tbTheme.setToolTip(trConst(nwLabels.THEME_MODE_LABEL[CONFIG.themeMode]))
diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py
index 0c8288b5..3578e3e0 100644
--- a/novelwriter/gui/statusbar.py
+++ b/novelwriter/gui/statusbar.py
@@ -133,6 +133,8 @@ class GuiMainStatus(QStatusBar):
def updateTheme(self) -> None:
"""Update theme elements."""
+ logger.debug("Theme Update: GuiMainStatus")
+
iPx = SHARED.theme.baseIconHeight
self.langIcon.setPixmap(SHARED.theme.getPixmap("language", (iPx, iPx)))
self.statsIcon.setPixmap(SHARED.theme.getPixmap("stats", (iPx, iPx)))
diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py
index b83de9b7..fb0935ab 100644
--- a/novelwriter/gui/theme.py
+++ b/novelwriter/gui/theme.py
@@ -31,19 +31,20 @@ from dataclasses import dataclass
from math import ceil
from typing import TYPE_CHECKING, Final
-from PyQt6.QtCore import QSize, Qt
+from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication, QSize, Qt
from PyQt6.QtGui import (
QColor, QFont, QFontDatabase, QFontMetrics, QGuiApplication, QIcon,
QPainter, QPainterPath, QPalette, QPixmap
)
-from PyQt6.QtWidgets import QApplication
+from PyQt6.QtWidgets import QApplication, QWidget
from novelwriter import CONFIG
from novelwriter.common import checkInt, minmax
from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS, DEF_TREECOL
from novelwriter.constants import nwLabels
-from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme
+from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwStandardButton, nwTheme
from novelwriter.error import logException
+from novelwriter.extensions.modified import NPushButton
from novelwriter.types import QtBlack, QtHexArgb, QtPaintAntiAlias, QtTransparent
if TYPE_CHECKING:
@@ -55,6 +56,26 @@ STYLES_FLAT_TABS = "flatTabWidget"
STYLES_MIN_TOOLBUTTON = "minimalToolButton"
STYLES_BIG_TOOLBUTTON = "bigToolButton"
+STANDARD_BUTTONS = {
+ nwStandardButton.OK: (QT_TRANSLATE_NOOP("Button", "OK"), "btn_ok", "action"),
+ nwStandardButton.CANCEL: (QT_TRANSLATE_NOOP("Button", "Cancel"), "btn_cancel", "reject"),
+ nwStandardButton.YES: (QT_TRANSLATE_NOOP("Button", "&Yes"), "btn_yes", "accept"),
+ nwStandardButton.NO: (QT_TRANSLATE_NOOP("Button", "&No"), "btn_no", "reject"),
+ nwStandardButton.OPEN: (QT_TRANSLATE_NOOP("Button", "Open"), "btn_open", "action"),
+ nwStandardButton.CLOSE: (QT_TRANSLATE_NOOP("Button", "Close"), "btn_close", "destroy"),
+ nwStandardButton.SAVE: (QT_TRANSLATE_NOOP("Button", "Save"), "btn_save", "action"),
+ nwStandardButton.BROWSE: (QT_TRANSLATE_NOOP("Button", "Browse"), "btn_browse", "systemio"),
+ nwStandardButton.LIST: (QT_TRANSLATE_NOOP("Button", "List"), "btn_list", "action"),
+ nwStandardButton.NEW: (QT_TRANSLATE_NOOP("Button", "New"), "btn_new", "apply"),
+ nwStandardButton.CREATE: (QT_TRANSLATE_NOOP("Button", "Create"), "btn_create", "create"),
+ nwStandardButton.RESET: (QT_TRANSLATE_NOOP("Button", "Reset"), "btn_reset", "reset"),
+ nwStandardButton.INSERT: (QT_TRANSLATE_NOOP("Button", "Insert"), "btn_insert", "action"),
+ nwStandardButton.APPLY: (QT_TRANSLATE_NOOP("Button", "Apply"), "btn_apply", "apply"),
+ nwStandardButton.BUILD: (QT_TRANSLATE_NOOP("Button", "Build"), "btn_build", "action"),
+ nwStandardButton.PRINT: (QT_TRANSLATE_NOOP("Button", "Print"), "btn_print", "action"),
+ nwStandardButton.PREVIEW: (QT_TRANSLATE_NOOP("Button", "Preview"), "btn_preview", "action"),
+}
+
@dataclass
class ThemeEntry:
@@ -93,6 +114,7 @@ class SyntaxColors:
head: QColor = QColor(0, 0, 0)
headH: QColor = QColor(0, 0, 0)
emph: QColor = QColor(0, 0, 0)
+ space: QColor = QColor(0, 0, 0)
dialN: QColor = QColor(0, 0, 0)
dialA: QColor = QColor(0, 0, 0)
hidden: QColor = QColor(0, 0, 0)
@@ -120,9 +142,9 @@ class GuiTheme:
"_qColors", "_styleSheets", "_svgColors", "_syntaxList", "accentCol", "baseButtonHeight",
"baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", "fadedText",
"fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration",
- "getHeaderDecorationNarrow", "getIcon", "getItemIcon", "getPixmap", "getToggleIcon",
- "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText",
- "iconCache", "isDarkTheme", "syntaxTheme", "textNHeight", "textNWidth",
+ "getHeaderDecorationNarrow", "getIcon", "getItemIcon", "getPixmap", "getStandardButton",
+ "getToggleIcon", "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall",
+ "helpText", "iconCache", "isDarkTheme", "syntaxTheme", "textNHeight", "textNWidth",
)
def __init__(self) -> None:
@@ -153,6 +175,7 @@ class GuiTheme:
self.getItemIcon = self.iconCache.getItemIcon
self.getToggleIcon = self.iconCache.getToggleIcon
self.getDecoration = self.iconCache.getDecoration
+ self.getStandardButton = self.iconCache.getStandardButton
self.getHeaderDecoration = self.iconCache.getHeaderDecoration
self.getHeaderDecorationNarrow = self.iconCache.getHeaderDecorationNarrow
@@ -334,6 +357,29 @@ class GuiTheme:
self._setBaseColor("inactive", self._readColor(parser, sec, "inactive"))
self._setBaseColor("disabled", self._readColor(parser, sec, "disabled"))
+ # Icon
+ sec = "Icon"
+ if parser.has_section(sec):
+ self._setBaseColor("tool", self._readColor(parser, sec, "tool"))
+ self._setBaseColor("sidebar", self._readColor(parser, sec, "sidebar"))
+ self._setBaseColor("accept", self._readColor(parser, sec, "accept"))
+ self._setBaseColor("reject", self._readColor(parser, sec, "reject"))
+ self._setBaseColor("action", self._readColor(parser, sec, "action"))
+ self._setBaseColor("altaction", self._readColor(parser, sec, "altaction"))
+ self._setBaseColor("apply", self._readColor(parser, sec, "apply"))
+ self._setBaseColor("create", self._readColor(parser, sec, "create"))
+ self._setBaseColor("destroy", self._readColor(parser, sec, "destroy"))
+ self._setBaseColor("reset", self._readColor(parser, sec, "reset"))
+ self._setBaseColor("add", self._readColor(parser, sec, "add"))
+ self._setBaseColor("change", self._readColor(parser, sec, "change"))
+ self._setBaseColor("remove", self._readColor(parser, sec, "remove"))
+ self._setBaseColor("shortcode", self._readColor(parser, sec, "shortcode"))
+ self._setBaseColor("markdown", self._readColor(parser, sec, "markdown"))
+ self._setBaseColor("systemio", self._readColor(parser, sec, "systemio"))
+ self._setBaseColor("info", self._readColor(parser, sec, "info"))
+ self._setBaseColor("warning", self._readColor(parser, sec, "warning"))
+ self._setBaseColor("error", self._readColor(parser, sec, "error"))
+
# Palette
sec = "Palette"
if parser.has_section(sec):
@@ -371,6 +417,7 @@ class GuiTheme:
self.syntaxTheme.head = self._readColor(parser, sec, "headertext")
self.syntaxTheme.headH = self._readColor(parser, sec, "headertag")
self.syntaxTheme.emph = self._readColor(parser, sec, "emphasis")
+ self.syntaxTheme.space = self._readColor(parser, sec, "whitespace")
self.syntaxTheme.dialN = self._readColor(parser, sec, "dialog")
self.syntaxTheme.dialA = self._readColor(parser, sec, "altdialog")
self.syntaxTheme.hidden = self._readColor(parser, sec, "hidden")
@@ -615,7 +662,7 @@ class GuiTheme:
lookup = f"{prefix}{name} {key}"
keys.append(lookup)
data[lookup] = (file.stem, name, mode == "dark", file)
- except Exception: # noqa: PERF203
+ except Exception:
logger.error("Could not read file: %s", file)
logException()
@@ -743,7 +790,7 @@ class GuiIcons:
# Access Functions
##
- def getIcon(self, name: str, color: str | None = None, w: int = 24, h: int = 24) -> QIcon:
+ def getIcon(self, name: str, color: str, w: int = 24, h: int = 24) -> QIcon:
"""Return an icon from the icon buffer, or load it."""
variant = f"{name}-{color}" if color else name
if (key := f"{variant}-{w}x{h}") in self._qIcons:
@@ -754,7 +801,7 @@ class GuiIcons:
logger.debug("Icon: %s", key)
return icon
- def getToggleIcon(self, name: str, size: tuple[int, int], color: str | None = None) -> QIcon:
+ def getToggleIcon(self, name: str, size: tuple[int, int], color: str) -> QIcon:
"""Return a toggle icon from the icon buffer, or load it."""
if name in self.TOGGLE_ICON_KEYS:
pOne = self.getPixmap(self.TOGGLE_ICON_KEYS[name][0], size, color)
@@ -806,7 +853,15 @@ class GuiIcons:
doesn't exist, return an empty QPixmap.
"""
w, h = size
- return self.getIcon(name, color, w, h).pixmap(w, h, QIcon.Mode.Normal)
+ return self.getIcon(name, color or "default", w, h).pixmap(w, h, QIcon.Mode.Normal)
+
+ def getStandardButton(self, button: nwStandardButton, parent: QWidget) -> NPushButton:
+ """Return a standard button with icon and text."""
+ text, icon, color = STANDARD_BUTTONS.get(button, ("", "", ""))
+ return NPushButton(
+ parent, QCoreApplication.translate("Button", text),
+ self._theme.buttonIconSize, icon, color
+ )
def getDecoration(self, name: str, w: int | None = None, h: int | None = None) -> QPixmap:
"""Load graphical decoration element based on the decoration
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 91d7ff59..20e86ad8 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -497,7 +497,8 @@ class GuiMain(QMainWindow):
# Check if we need to rebuild the index
if SHARED.project.index.indexBroken:
- SHARED.info(self.tr("The project index is outdated or broken. Rebuilding index."))
+ if not SHARED.project.index.indexUpgrade:
+ SHARED.warn(self.tr("The project index is broken. Rebuilding index."))
self.rebuildIndex()
# Make sure the changed status is set to false on things opened
@@ -729,7 +730,7 @@ class GuiMain(QMainWindow):
return
- def rebuildIndex(self, beQuiet: bool = False) -> None:
+ def rebuildIndex(self) -> None:
"""Rebuild the entire index."""
if SHARED.hasProject:
logger.info("Rebuilding index ...")
@@ -746,8 +747,7 @@ class GuiMain(QMainWindow):
self._updateStatusWordCount()
QApplication.restoreOverrideCursor()
- if not beQuiet:
- SHARED.info(self.tr("The project index has been successfully rebuilt."))
+ SHARED.info(self.tr("The project index has been successfully rebuilt."))
##
# Main Dialogs
@@ -910,6 +910,9 @@ class GuiMain(QMainWindow):
self.mainStatus.updateTheme()
SHARED.project.tree.refreshAllItems()
+ if dialog := SHARED.findTopLevelWidget(GuiManuscript):
+ dialog.updateTheme()
+
if syntax:
self.docEditor.updateSyntaxColors()
diff --git a/novelwriter/shared.py b/novelwriter/shared.py
index 7c971341..eacf2298 100644
--- a/novelwriter/shared.py
+++ b/novelwriter/shared.py
@@ -39,7 +39,7 @@ from PyQt6.QtWidgets import QApplication, QFileDialog, QFontDialog, QMessageBox,
from novelwriter.common import formatFileFilter
from novelwriter.constants import nwFiles
from novelwriter.core.spellcheck import NWSpellEnchant
-from novelwriter.enum import nwChange, nwItemClass
+from novelwriter.enum import nwChange, nwItemClass, nwStandardButton
if TYPE_CHECKING:
from collections.abc import Callable
@@ -422,7 +422,7 @@ class SharedData(QObject):
alert.setAlertType(_GuiAlert.WARN if warn else _GuiAlert.ASK, True)
self._lastAlert = alert.logMessage
alert.exec()
- return alert.result() == QMessageBox.StandardButton.Yes
+ return alert.finalState
##
# Internal Functions
@@ -469,6 +469,7 @@ class _GuiAlert(QMessageBox):
super().__init__(parent=parent)
self._theme = theme
self._message = ""
+ self._state = False
logger.debug("Ready: _GuiAlert")
def __del__(self) -> None: # pragma: no cover
@@ -478,6 +479,10 @@ class _GuiAlert(QMessageBox):
def logMessage(self) -> str:
return self._message
+ @property
+ def finalState(self) -> bool:
+ return self._state
+
def setMessage(self, text: str, info: str, details: str) -> None:
"""Set the alert box message."""
self._message = " ".join(filter(None, [text, info, details]))
@@ -496,19 +501,39 @@ class _GuiAlert(QMessageBox):
Yes/No buttons or just an Ok button.
"""
if isYesNo:
- self.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
+ self._btnYes = self._theme.getStandardButton(nwStandardButton.YES, self)
+ self._btnYes.clicked.connect(self._onAccept)
+ self._btnNo = self._theme.getStandardButton(nwStandardButton.NO, self)
+ self._btnNo.clicked.connect(self._onReject)
+ self.addButton(self._btnYes, QMessageBox.ButtonRole.YesRole)
+ self.addButton(self._btnNo, QMessageBox.ButtonRole.NoRole)
else:
- self.setStandardButtons(QMessageBox.StandardButton.Ok)
+ self._btnOk = self._theme.getStandardButton(nwStandardButton.OK, self)
+ self._btnOk.clicked.connect(self._onAccept)
+ self.addButton(self._btnOk, QMessageBox.ButtonRole.AcceptRole)
+
pSz = 2*self._theme.baseIconHeight
if level == self.INFO:
- self.setIconPixmap(self._theme.getPixmap("alert_info", (pSz, pSz), "blue"))
+ self.setIconPixmap(self._theme.getPixmap("alert_info", (pSz, pSz), "info"))
self.setWindowTitle(self.tr("Information"))
elif level == self.WARN:
- self.setIconPixmap(self._theme.getPixmap("alert_warn", (pSz, pSz), "orange"))
+ self.setIconPixmap(self._theme.getPixmap("alert_warn", (pSz, pSz), "warning"))
self.setWindowTitle(self.tr("Warning"))
elif level == self.ERROR:
- self.setIconPixmap(self._theme.getPixmap("alert_error", (pSz, pSz), "red"))
+ self.setIconPixmap(self._theme.getPixmap("alert_error", (pSz, pSz), "error"))
self.setWindowTitle(self.tr("Error"))
elif level == self.ASK:
- self.setIconPixmap(self._theme.getPixmap("alert_question", (pSz, pSz), "blue"))
+ self.setIconPixmap(self._theme.getPixmap("alert_question", (pSz, pSz), "info"))
self.setWindowTitle(self.tr("Question"))
+
+ @pyqtSlot()
+ def _onAccept(self) -> None:
+ """Process accepted state."""
+ self._state = True
+ self.close()
+
+ @pyqtSlot()
+ def _onReject(self) -> None:
+ """Process rejected state."""
+ self._state = False
+ self.close()
diff --git a/novelwriter/tools/dictionaries.py b/novelwriter/tools/dictionaries.py
index 5cc97021..9195b49a 100644
--- a/novelwriter/tools/dictionaries.py
+++ b/novelwriter/tools/dictionaries.py
@@ -37,9 +37,10 @@ from PyQt6.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.common import formatFileFilter, formatInt, getFileSize, openExternalPath
+from novelwriter.enum import nwStandardButton
from novelwriter.error import formatException
from novelwriter.extensions.modified import NIconToolButton, NNonBlockingDialog
-from novelwriter.types import QtDialogClose, QtHexArgb
+from novelwriter.types import QtHexArgb, QtRoleDestruct
logger = logging.getLogger(__name__)
@@ -78,10 +79,10 @@ class GuiDictionaries(NNonBlockingDialog):
self.huInfo.setOpenExternalLinks(True)
self.huInfo.setWordWrap(True)
self.huInput = QLineEdit(self)
- self.huBrowse = NIconToolButton(self, iSz, "browse")
+ self.huBrowse = NIconToolButton(self, iSz, "browse", "systemio")
self.huBrowse.clicked.connect(self._doBrowseHunspell)
self.huImport = QPushButton(self.tr("Add Dictionary"), self)
- self.huImport.setIcon(SHARED.theme.getIcon("add", "green"))
+ self.huImport.setIcon(SHARED.theme.getIcon("add", "add"))
self.huImport.clicked.connect(self._doImportHunspell)
self.huPathBox = QHBoxLayout()
@@ -96,7 +97,7 @@ class GuiDictionaries(NNonBlockingDialog):
self.inInfo = QLabel(self.tr("Dictionary install location"), self)
self.inPath = QLineEdit(self)
self.inPath.setReadOnly(True)
- self.inBrowse = NIconToolButton(self, iSz, "browse")
+ self.inBrowse = NIconToolButton(self, iSz, "browse", "systemio")
self.inBrowse.clicked.connect(self._doOpenInstallLocation)
self.inBox = QHBoxLayout()
@@ -110,8 +111,11 @@ class GuiDictionaries(NNonBlockingDialog):
self.infoBox.setFrameStyle(QFrame.Shape.NoFrame)
# Buttons
- self.buttonBox = QDialogButtonBox(QtDialogClose, self)
- self.buttonBox.rejected.connect(self.reject)
+ self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self)
+ self.btnClose.clicked.connect(self.reject)
+
+ self.btnBox = QDialogButtonBox(self)
+ self.btnBox.addButton(self.btnClose, QtRoleDestruct)
# Assemble
self.outerBox = QVBoxLayout()
@@ -123,7 +127,7 @@ class GuiDictionaries(NNonBlockingDialog):
self.outerBox.addLayout(self.inBox, 0)
self.outerBox.addWidget(self.infoBox, 1)
self.outerBox.addSpacing(8)
- self.outerBox.addWidget(self.buttonBox, 0)
+ self.outerBox.addWidget(self.btnBox, 0)
self.setLayout(self.outerBox)
diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py
index d28fb845..55119317 100644
--- a/novelwriter/tools/lipsum.py
+++ b/novelwriter/tools/lipsum.py
@@ -34,9 +34,10 @@ from PyQt6.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.common import readTextFile
+from novelwriter.enum import nwStandardButton
from novelwriter.extensions.modified import NDialog
from novelwriter.extensions.switch import NSwitch
-from novelwriter.types import QtAlignLeft, QtAlignRight, QtDialogClose, QtRoleAction
+from novelwriter.types import QtAlignLeft, QtAlignRight, QtRoleApply, QtRoleDestruct
logger = logging.getLogger(__name__)
@@ -58,7 +59,7 @@ class GuiLipsum(NDialog):
# Icon
self.docIcon = QLabel(self)
- self.docIcon.setPixmap(SHARED.theme.getPixmap("text", (64, 64), "blue"))
+ self.docIcon.setPixmap(SHARED.theme.getPixmap("text", (64, 64), "info"))
self.leftBox = QVBoxLayout()
self.leftBox.setSpacing(4)
@@ -91,22 +92,22 @@ class GuiLipsum(NDialog):
self.innerBox.addLayout(self.formBox)
# Buttons
- self.buttonBox = QDialogButtonBox(self)
- self.buttonBox.rejected.connect(self.reject)
+ self.btnInsert = SHARED.theme.getStandardButton(nwStandardButton.INSERT, self)
+ self.btnInsert.clicked.connect(self._doInsert)
+ self.btnInsert.setAutoDefault(False)
- self.btnClose = self.buttonBox.addButton(QtDialogClose)
- if self.btnClose:
- self.btnClose.setAutoDefault(False)
+ self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self)
+ self.btnClose.clicked.connect(self.reject)
+ self.btnClose.setAutoDefault(False)
- self.btnInsert = self.buttonBox.addButton(self.tr("Insert"), QtRoleAction)
- if self.btnInsert:
- self.btnInsert.clicked.connect(self._doInsert)
- self.btnInsert.setAutoDefault(False)
+ self.btnBox = QDialogButtonBox(self)
+ self.btnBox.addButton(self.btnInsert, QtRoleApply)
+ self.btnBox.addButton(self.btnClose, QtRoleDestruct)
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.addLayout(self.innerBox)
- self.outerBox.addWidget(self.buttonBox)
+ self.outerBox.addWidget(self.btnBox)
self.outerBox.setSpacing(16)
self.setLayout(self.outerBox)
diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py
index 19070b14..5520982d 100644
--- a/novelwriter/tools/manusbuild.py
+++ b/novelwriter/tools/manusbuild.py
@@ -32,7 +32,7 @@ from PyQt6.QtCore import QTimer, pyqtSlot
from PyQt6.QtWidgets import (
QAbstractButton, QAbstractItemView, QDialogButtonBox, QFileDialog,
QGridLayout, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem,
- QPushButton, QSplitter, QVBoxLayout, QWidget
+ QSplitter, QVBoxLayout, QWidget
)
from novelwriter import SHARED
@@ -40,10 +40,10 @@ from novelwriter.common import makeFileNameSafe, openExternalPath
from novelwriter.constants import nwLabels
from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.item import NWItem
-from novelwriter.enum import nwBuildFmt
-from novelwriter.extensions.modified import NDialog, NIconToolButton
+from novelwriter.enum import nwBuildFmt, nwStandardButton
+from novelwriter.extensions.modified import NDialog, NIconToolButton, NPushButton
from novelwriter.extensions.progressbars import NProgressSimple
-from novelwriter.types import QtAlignCenter, QtDialogClose, QtRoleAction, QtRoleReject, QtUserRole
+from novelwriter.types import QtAlignCenter, QtRoleAction, QtRoleDestruct, QtUserRole
if TYPE_CHECKING:
from PyQt6.QtGui import QCloseEvent
@@ -143,7 +143,7 @@ class GuiManuscriptBuild(NDialog):
# Build Path
self.lblPath = QLabel(self.tr("Path"), self)
self.buildPath = QLineEdit(self)
- self.btnBrowse = NIconToolButton(self, iSz, "browse")
+ self.btnBrowse = NIconToolButton(self, iSz, "browse", "systemio")
self.pathBox = QHBoxLayout()
self.pathBox.addWidget(self.buildPath)
@@ -153,7 +153,7 @@ class GuiManuscriptBuild(NDialog):
# Build Name
self.lblName = QLabel(self.tr("File Name"), self)
self.buildName = QLineEdit(self)
- self.btnReset = NIconToolButton(self, iSz, "revert", "green")
+ self.btnReset = NIconToolButton(self, iSz, "revert", "reset")
self.btnReset.setToolTip(self.tr("Reset file name to default"))
self.nameBox = QHBoxLayout()
@@ -178,25 +178,19 @@ class GuiManuscriptBuild(NDialog):
self.buildBox.setVerticalSpacing(4)
# Dialog Buttons
- self.buttonBox = QDialogButtonBox(self)
-
- self.btnOpen = QPushButton(
- SHARED.theme.getIcon("browse", "yellow"), self.tr("Open Folder"), self
- )
- self.btnOpen.setIconSize(bSz)
+ self.btnOpen = NPushButton(self, self.tr("Open Folder"), bSz, "browse", "systemio")
self.btnOpen.setAutoDefault(False)
- self.buttonBox.addButton(self.btnOpen, QtRoleAction)
- self.btnBuild = QPushButton(
- SHARED.theme.getIcon("sb_build", "blue"), self.tr("&Build"), self
- )
- self.btnBuild.setIconSize(bSz)
+ self.btnBuild = SHARED.theme.getStandardButton(nwStandardButton.BUILD, self)
self.btnBuild.setAutoDefault(True)
- self.buttonBox.addButton(self.btnBuild, QtRoleAction)
- self.btnClose = self.buttonBox.addButton(QtDialogClose)
- if self.btnClose:
- self.btnClose.setAutoDefault(False)
+ self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self)
+ self.btnClose.setAutoDefault(False)
+
+ self.btnBox = QDialogButtonBox(self)
+ self.btnBox.addButton(self.btnOpen, QtRoleAction)
+ self.btnBox.addButton(self.btnBuild, QtRoleAction)
+ self.btnBox.addButton(self.btnClose, QtRoleDestruct)
# Assemble GUI
# ============
@@ -223,7 +217,7 @@ class GuiManuscriptBuild(NDialog):
self.outerBox.addSpacing(4)
self.outerBox.addLayout(self.buildBox, 0)
self.outerBox.addSpacing(16)
- self.outerBox.addWidget(self.buttonBox, 0)
+ self.outerBox.addWidget(self.btnBox, 0)
self.outerBox.setSpacing(0)
self.setLayout(self.outerBox)
@@ -239,7 +233,7 @@ class GuiManuscriptBuild(NDialog):
# Signals
self.btnReset.clicked.connect(self._doResetBuildName)
self.btnBrowse.clicked.connect(self._doSelectPath)
- self.buttonBox.clicked.connect(self._dialogButtonClicked)
+ self.btnBox.clicked.connect(self._dialogButtonClicked)
self.listFormats.itemSelectionChanged.connect(self._resetProgress)
logger.debug("Ready: GuiManuscriptBuild")
@@ -266,13 +260,11 @@ class GuiManuscriptBuild(NDialog):
@pyqtSlot("QAbstractButton*")
def _dialogButtonClicked(self, button: QAbstractButton) -> None:
"""Handle button clicks from the dialog button box."""
- role = self.buttonBox.buttonRole(button)
- if role == QtRoleAction:
- if button == self.btnBuild:
- self._runBuild()
- elif button == self.btnOpen:
- self._openOutputFolder()
- elif role == QtRoleReject:
+ if button == self.btnBuild:
+ self._runBuild()
+ elif button == self.btnOpen:
+ self._openOutputFolder()
+ elif button == self.btnClose:
self.close()
@pyqtSlot()
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index 8c458db3..5bf30983 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -36,9 +36,9 @@ from PyQt6.QtGui import (
from PyQt6.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt6.QtWidgets import (
QAbstractItemView, QApplication, QFormLayout, QGridLayout, QHBoxLayout,
- QLabel, QListWidget, QListWidgetItem, QPushButton, QSplitter,
- QStackedWidget, QTabWidget, QTextBrowser, QTreeWidget, QTreeWidgetItem,
- QVBoxLayout, QWidget
+ QLabel, QListWidget, QListWidgetItem, QSplitter, QStackedWidget,
+ QTabWidget, QTextBrowser, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
+ QWidget
)
from novelwriter import CONFIG, SHARED
@@ -46,6 +46,7 @@ from novelwriter.common import fuzzyTime, qtLambda
from novelwriter.constants import nwHeadFmt, nwLabels, nwStats, nwUnicode, trStats
from novelwriter.core.buildsettings import BuildCollection, BuildSettings
from novelwriter.core.docbuild import NWBuildDocument
+from novelwriter.enum import nwStandardButton
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog
from novelwriter.extensions.progressbars import NProgressCircle
from novelwriter.extensions.switch import NSwitch
@@ -100,30 +101,20 @@ class GuiManuscript(NToolDialog):
# Build Controls
# ==============
- qPalette = self.palette()
- qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
- self.setPalette(qPalette)
-
- buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
-
- self.tbAdd = NIconToolButton(self, iSz, "add", "green")
+ self.tbAdd = NIconToolButton(self, iSz)
self.tbAdd.setToolTip(self.tr("Add New Build"))
- self.tbAdd.setStyleSheet(buttonStyle)
self.tbAdd.clicked.connect(self._createNewBuild)
- self.tbDel = NIconToolButton(self, iSz, "remove", "red")
+ self.tbDel = NIconToolButton(self, iSz)
self.tbDel.setToolTip(self.tr("Delete Selected Build"))
- self.tbDel.setStyleSheet(buttonStyle)
self.tbDel.clicked.connect(self._deleteSelectedBuild)
- self.tbCopy = NIconToolButton(self, iSz, "copy", "blue")
+ self.tbCopy = NIconToolButton(self, iSz)
self.tbCopy.setToolTip(self.tr("Duplicate Selected Build"))
- self.tbCopy.setStyleSheet(buttonStyle)
self.tbCopy.clicked.connect(self._copySelectedBuild)
- self.tbEdit = NIconToolButton(self, iSz, "edit", "green")
+ self.tbEdit = NIconToolButton(self, iSz)
self.tbEdit.setToolTip(self.tr("Edit Selected Build"))
- self.tbEdit.setStyleSheet(buttonStyle)
self.tbEdit.clicked.connect(self._editSelectedBuild)
self.lblBuilds = QLabel("{0}".format(self.tr("Builds")), self)
@@ -158,7 +149,6 @@ class GuiManuscript(NToolDialog):
self.detailsTabs = QTabWidget(self)
self.detailsTabs.addTab(self.buildDetails, self.tr("Details"))
self.detailsTabs.addTab(self.buildOutline, self.tr("Outline"))
- self.detailsTabs.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_FLAT_TABS))
self.buildSplit = QSplitter(Qt.Orientation.Vertical, self)
self.buildSplit.addWidget(self.buildList)
@@ -171,16 +161,16 @@ class GuiManuscript(NToolDialog):
# Process Controls
# ================
- self.btnPreview = QPushButton(self.tr("Preview"), self)
+ self.btnPreview = SHARED.theme.getStandardButton(nwStandardButton.PREVIEW, self)
self.btnPreview.clicked.connect(self._generatePreview)
- self.btnPrint = QPushButton(self.tr("Print"), self)
+ self.btnPrint = SHARED.theme.getStandardButton(nwStandardButton.PRINT, self)
self.btnPrint.clicked.connect(self._printDocument)
- self.btnBuild = QPushButton(self.tr("Build"), self)
+ self.btnBuild = SHARED.theme.getStandardButton(nwStandardButton.BUILD, self)
self.btnBuild.clicked.connect(self._buildManuscript)
- self.btnClose = QPushButton(self.tr("Close"), self)
+ self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self)
self.btnClose.clicked.connect(qtLambda(self.close))
self.processBox = QGridLayout()
@@ -246,6 +236,8 @@ class GuiManuscript(NToolDialog):
self.setLayout(self.outerBox)
self.setSizeGripEnabled(True)
+ self.updateTheme(init=True)
+
# Signals
self.buildOutline.outlineEntryClicked.connect(self.docPreview.navigateTo)
@@ -269,6 +261,37 @@ class GuiManuscript(NToolDialog):
self.buildList.setCurrentItem(self._buildMap[selected])
QTimer.singleShot(200, self._generatePreview)
+ def updateTheme(self, *, init: bool = False) -> None:
+ """Update theme elements."""
+ logger.debug("Theme Update: GuiManuscript, init=%s", init)
+
+ if not init:
+ self.btnPreview.updateIcon()
+ self.btnPrint.updateIcon()
+ self.btnBuild.updateIcon()
+ self.btnClose.updateIcon()
+
+ self.tbAdd.setThemeIcon("add", "add")
+ self.tbDel.setThemeIcon("remove", "remove")
+ self.tbCopy.setThemeIcon("copy", "accept")
+ self.tbEdit.setThemeIcon("edit", "change")
+
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
+ self.tbAdd.setStyleSheet(buttonStyle)
+ self.tbDel.setStyleSheet(buttonStyle)
+ self.tbCopy.setStyleSheet(buttonStyle)
+ self.tbEdit.setStyleSheet(buttonStyle)
+
+ self.detailsTabs.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_FLAT_TABS))
+
+ self.buildDetails.updateTheme()
+ self.buildOutline.updateTheme()
+ self.docPreview.updateTheme()
+
+ for obj in SHARED.mainGui.children():
+ if isinstance(obj, GuiBuildSettings):
+ obj.updateTheme()
+
##
# Events
##
@@ -461,7 +484,7 @@ class GuiManuscript(NToolDialog):
for key, name in self._builds.builds():
bItem = QListWidgetItem()
bItem.setText(name)
- bItem.setIcon(SHARED.theme.getIcon("build_settings", "blue"))
+ bItem.setIcon(SHARED.theme.getIcon("build_settings", "action"))
bItem.setData(self.D_KEY, key)
self.buildList.addItem(bItem)
self._buildMap[key] = bItem
@@ -489,6 +512,7 @@ class _DetailsWidget(QWidget):
super().__init__(parent=parent)
self._initExpanded = True
+ self._build = None
# Tree Widget
self.listView = QTreeWidget(self)
@@ -550,8 +574,8 @@ class _DetailsWidget(QWidget):
self.listView.clear()
- on = SHARED.theme.getIcon("bullet-on", "blue")
- off = SHARED.theme.getIcon("bullet-off", "blue")
+ on = SHARED.theme.getIcon("bullet-on", "action")
+ off = SHARED.theme.getIcon("bullet-off", "action")
# Name
item = QTreeWidgetItem()
@@ -611,9 +635,17 @@ class _DetailsWidget(QWidget):
sub.setIcon(1, on if build.getBool(key) else off)
item.addChild(sub)
+ self._build = build
+
# Restore expanded state
self.setExpandedState(expanded)
+ def updateTheme(self) -> None:
+ """Update theme elements."""
+ if self._build:
+ logger.debug("Theme Update: _DetailsWidget")
+ self.updateInfo(self._build)
+
class _OutlineWidget(QWidget):
@@ -638,9 +670,9 @@ class _OutlineWidget(QWidget):
self.outerBox.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.outerBox)
- def updateOutline(self, data: dict[str, str]) -> None:
+ def updateOutline(self, data: dict[str, str], *, force: bool = False) -> None:
"""Update the outline."""
- if isinstance(data, dict) and data != self._outline:
+ if isinstance(data, dict) and (data != self._outline or force):
self.listView.clear()
tFont = self.font()
@@ -678,6 +710,11 @@ class _OutlineWidget(QWidget):
self.listView.setIndentation(SHARED.theme.baseIconHeight if indent else 4)
self._outline = data
+ def updateTheme(self) -> None:
+ """Update theme elements."""
+ logger.debug("Theme Update: _OutlineWidget")
+ self.updateOutline(self._outline, force=True)
+
##
# Private Slots
##
@@ -720,17 +757,12 @@ class _PreviewWidget(QTextBrowser):
self.anchorClicked.connect(self._linkClicked)
# Document Age
- aPalette = self.palette()
- aPalette.setColor(QPalette.ColorRole.Window, aPalette.toolTipBase().color())
- aPalette.setColor(QPalette.ColorRole.WindowText, aPalette.toolTipText().color())
-
aFont = self.font()
aFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
self.ageLabel = QLabel("", self)
self.ageLabel.setIndent(0)
self.ageLabel.setFont(aFont)
- self.ageLabel.setPalette(aPalette)
self.ageLabel.setAutoFillBackground(True)
self.ageLabel.setAlignment(QtAlignCenter)
self.ageLabel.setFixedHeight(int(2.1*SHARED.theme.fontPixelSize))
@@ -749,6 +781,7 @@ class _PreviewWidget(QTextBrowser):
self._updateDocMargins()
self._updateBuildAge()
+ self.updateTheme()
self.setTextFont(CONFIG.textFont)
# Age Timer
@@ -814,6 +847,15 @@ class _PreviewWidget(QTextBrowser):
QApplication.processEvents()
QTimer.singleShot(300, self._postUpdate)
+ def updateTheme(self) -> None:
+ """Update theme elements."""
+ logger.debug("Theme Update: _PreviewWidget")
+
+ palette = QApplication.palette()
+ palette.setColor(QPalette.ColorRole.Window, palette.toolTipBase().color())
+ palette.setColor(QPalette.ColorRole.WindowText, palette.toolTipText().color())
+ self.ageLabel.setPalette(palette)
+
##
# Events
##
@@ -906,7 +948,7 @@ class _StatsWidget(QWidget):
self.minWidget = QWidget(self)
self.maxWidget = QWidget(self)
- self.toggleButton = NIconToggleButton(self, SHARED.theme.baseIconSize, "unfold")
+ self.toggleButton = NIconToggleButton(self, SHARED.theme.baseIconSize)
self.toggleButton.toggled.connect(self._toggleView)
self._buildBottomPanel()
@@ -921,6 +963,7 @@ class _StatsWidget(QWidget):
self.outerBox.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.outerBox)
+ self.updateTheme()
self._toggleView(False)
@@ -945,6 +988,11 @@ class _StatsWidget(QWidget):
self.maxHeadWordChars.setText(f"{data.get(nwStats.WCHARS_TITLE, 0):n}")
self.maxTextWordChars.setText(f"{data.get(nwStats.WCHARS_TEXT, 0):n}")
+ def updateTheme(self) -> None:
+ """Update theme elements."""
+ logger.debug("Theme Update: _StatsWidget")
+ self.toggleButton.setThemeIcon("unfold", "default")
+
##
# Private Slots
##
diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py
index e8bf94b6..35fe49f6 100644
--- a/novelwriter/tools/manussettings.py
+++ b/novelwriter/tools/manussettings.py
@@ -40,6 +40,7 @@ from novelwriter import CONFIG, SHARED
from novelwriter.common import describeFont, fontMatcher, qtAddAction, qtLambda
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwUnicode, trConst
from novelwriter.core.buildsettings import BuildSettings, FilterMode
+from novelwriter.enum import nwStandardButton
from novelwriter.extensions.configlayout import (
NColorLabel, NFixedPage, NScrollableForm, NScrollablePage
)
@@ -50,9 +51,8 @@ from novelwriter.extensions.pagedsidebar import NPagedSideBar
from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.switchbox import NSwitchBox
from novelwriter.types import (
- QtAlignCenter, QtAlignLeft, QtDialogApply, QtDialogClose, QtDialogSave,
- QtHeaderFixed, QtHeaderStretch, QtRoleAccept, QtRoleApply, QtRoleReject,
- QtUserRole
+ QtAlignCenter, QtAlignLeft, QtHeaderFixed, QtHeaderStretch, QtRoleAccept,
+ QtRoleApply, QtRoleDestruct, QtUserRole
)
if TYPE_CHECKING:
@@ -125,8 +125,15 @@ class GuiBuildSettings(NToolDialog):
self.toolStack.addWidget(self.optTabFormatting)
# Buttons
- self.buttonBox = QDialogButtonBox(QtDialogApply | QtDialogSave | QtDialogClose, self)
- self.buttonBox.clicked.connect(self._dialogButtonClicked)
+ self.btnApply = SHARED.theme.getStandardButton(nwStandardButton.APPLY, self)
+ self.btnSave = SHARED.theme.getStandardButton(nwStandardButton.SAVE, self)
+ self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self)
+
+ self.btnBox = QDialogButtonBox(self)
+ self.btnBox.addButton(self.btnApply, QtRoleApply)
+ self.btnBox.addButton(self.btnSave, QtRoleAccept)
+ self.btnBox.addButton(self.btnClose, QtRoleDestruct)
+ self.btnBox.clicked.connect(self._dialogButtonClicked)
# Assemble
self.topBox = QHBoxLayout()
@@ -143,10 +150,11 @@ class GuiBuildSettings(NToolDialog):
self.outerBox = QVBoxLayout()
self.outerBox.addLayout(self.topBox)
self.outerBox.addLayout(self.mainBox)
- self.outerBox.addWidget(self.buttonBox)
+ self.outerBox.addWidget(self.btnBox)
self.outerBox.setSpacing(12)
self.setLayout(self.outerBox)
+ self.updateTheme(init=True)
# Set Default Tab
self.sidebar.setSelected(self.OPT_FILTERS)
@@ -163,6 +171,22 @@ class GuiBuildSettings(NToolDialog):
self.optTabHeadings.loadContent()
self.optTabFormatting.loadContent()
+ def updateTheme(self, *, init: bool = False) -> None:
+ """Update theme elements."""
+ logger.debug("Theme Update: GuiBuildSettings, init=%s", init)
+
+ if not init:
+ self.btnApply.updateIcon()
+ self.btnSave.updateIcon()
+ self.btnClose.updateIcon()
+
+ self.optTabSelect.updateTheme()
+ self.optTabHeadings.updateTheme()
+ self.optTabFormatting.updateTheme()
+
+ self.titleLabel.setTextColors(color=SHARED.theme.helpText)
+ self.sidebar.setLabelColor(SHARED.theme.helpText)
+
##
# Properties
##
@@ -205,15 +229,14 @@ class GuiBuildSettings(NToolDialog):
@pyqtSlot("QAbstractButton*")
def _dialogButtonClicked(self, button: QAbstractButton) -> None:
"""Handle button clicks from the dialog button box."""
- role = self.buttonBox.buttonRole(button)
- if role == QtRoleApply:
+ if button == self.btnApply:
self._applyChanges()
self._emitBuildData()
- elif role == QtRoleAccept:
+ elif button == self.btnSave:
self._applyChanges()
self._emitBuildData()
self.close()
- elif role == QtRoleReject:
+ elif button == self.btnClose:
self._build.resetChangedState()
self.close()
@@ -278,9 +301,9 @@ class _FilterTab(NFixedPage):
self._statusFlags: dict[int, QIcon] = {
self.F_NONE: QIcon(),
- self.F_FILTERED: SHARED.theme.getIcon("filter", "orange"),
- self.F_INCLUDED: SHARED.theme.getIcon("pin", "blue"),
- self.F_EXCLUDED: SHARED.theme.getIcon("exclude", "red"),
+ self.F_FILTERED: SHARED.theme.getIcon("filter", "altaction"),
+ self.F_INCLUDED: SHARED.theme.getIcon("pin", "action"),
+ self.F_EXCLUDED: SHARED.theme.getIcon("exclude", "reject"),
}
self._trIncluded = self.tr("Included in manuscript")
@@ -319,15 +342,13 @@ class _FilterTab(NFixedPage):
self.includedButton = NIconToolButton(self, iSz)
self.includedButton.setToolTip(self.tr("Always included"))
- self.includedButton.setIcon(self._statusFlags[self.F_INCLUDED])
self.includedButton.clicked.connect(qtLambda(self._setSelectedMode, self.F_INCLUDED))
self.excludedButton = NIconToolButton(self, iSz)
self.excludedButton.setToolTip(self.tr("Always excluded"))
- self.excludedButton.setIcon(self._statusFlags[self.F_EXCLUDED])
self.excludedButton.clicked.connect(qtLambda(self._setSelectedMode, self.F_EXCLUDED))
- self.resetButton = NIconToolButton(self, iSz, "revert", "green")
+ self.resetButton = NIconToolButton(self, iSz)
self.resetButton.setToolTip(self.tr("Reset to default"))
self.resetButton.clicked.connect(qtLambda(self._setSelectedMode, self.F_FILTERED))
@@ -369,6 +390,7 @@ class _FilterTab(NFixedPage):
pOptions.getInt("GuiBuildSettings", "filterWidth", 300),
])
+ self.updateTheme(init=True)
self.setCentralWidget(self.mainSplit)
def loadContent(self) -> None:
@@ -382,6 +404,20 @@ class _FilterTab(NFixedPage):
m, n = (sizes[0], sizes[1]) if len(sizes) >= 2 else (0, 0)
return m, n
+ def updateTheme(self, *, init: bool = False) -> None:
+ """Update theme elements."""
+ logger.debug("Theme Update: _FilterTab, init=%s", init)
+
+ if not init:
+ self._statusFlags[self.F_FILTERED] = SHARED.theme.getIcon("filter", "altaction")
+ self._statusFlags[self.F_INCLUDED] = SHARED.theme.getIcon("pin", "action")
+ self._statusFlags[self.F_EXCLUDED] = SHARED.theme.getIcon("exclude", "reject")
+ self.loadContent()
+
+ self.includedButton.setIcon(self._statusFlags[self.F_INCLUDED])
+ self.excludedButton.setIcon(self._statusFlags[self.F_EXCLUDED])
+ self.resetButton.setThemeIcon("revert", "reset")
+
##
# Slots
##
@@ -455,7 +491,7 @@ class _FilterTab(NFixedPage):
default=self._build.getBool("filter.includeNotes")
)
self.filterOpt.addItem(
- SHARED.theme.getIcon("unchecked", "red"),
+ SHARED.theme.getIcon("unchecked", "reject"),
self._build.getLabel("filter.includeInactive"),
"doc:filter.includeInactive",
default=self._build.getBool("filter.includeInactive")
@@ -548,7 +584,7 @@ class _HeadingsTab(NScrollablePage):
self.lblPart = QLabel(self._build.getLabel("headings.fmtPart"), self)
self.fmtPart = QLineEdit("", self)
self.fmtPart.setReadOnly(True)
- self.btnPart = NIconToolButton(self, iSz, "edit", "green")
+ self.btnPart = NIconToolButton(self, iSz)
self.btnPart.clicked.connect(qtLambda(self._editHeading, self.EDIT_TITLE))
self.swtPart = NSwitch(self, height=iPx)
self.hdePart = QLabel(trHide, self)
@@ -565,7 +601,7 @@ class _HeadingsTab(NScrollablePage):
self.lblChapter = QLabel(self._build.getLabel("headings.fmtChapter"), self)
self.fmtChapter = QLineEdit("", self)
self.fmtChapter.setReadOnly(True)
- self.btnChapter = NIconToolButton(self, iSz, "edit", "green")
+ self.btnChapter = NIconToolButton(self, iSz)
self.btnChapter.clicked.connect(qtLambda(self._editHeading, self.EDIT_CHAPTER))
self.swtChapter = NSwitch(self, height=iPx)
self.hdeChapter = QLabel(trHide, self)
@@ -582,7 +618,7 @@ class _HeadingsTab(NScrollablePage):
self.lblUnnumbered = QLabel(self._build.getLabel("headings.fmtUnnumbered"), self)
self.fmtUnnumbered = QLineEdit("", self)
self.fmtUnnumbered.setReadOnly(True)
- self.btnUnnumbered = NIconToolButton(self, iSz, "edit", "green")
+ self.btnUnnumbered = NIconToolButton(self, iSz)
self.btnUnnumbered.clicked.connect(qtLambda(self._editHeading, self.EDIT_UNNUM))
self.swtUnnumbered = NSwitch(self, height=iPx)
self.hdeUnnumbered = QLabel(trHide, self)
@@ -599,7 +635,7 @@ class _HeadingsTab(NScrollablePage):
self.lblScene = QLabel(self._build.getLabel("headings.fmtScene"), self)
self.fmtScene = QLineEdit("", self)
self.fmtScene.setReadOnly(True)
- self.btnScene = NIconToolButton(self, iSz, "edit", "green")
+ self.btnScene = NIconToolButton(self, iSz)
self.btnScene.clicked.connect(qtLambda(self._editHeading, self.EDIT_SCENE))
self.swtScene = NSwitch(self, height=iPx)
self.hdeScene = QLabel(trHide, self)
@@ -616,7 +652,7 @@ class _HeadingsTab(NScrollablePage):
self.lblAScene = QLabel(self._build.getLabel("headings.fmtAltScene"), self)
self.fmtAScene = QLineEdit("", self)
self.fmtAScene.setReadOnly(True)
- self.btnAScene = NIconToolButton(self, iSz, "edit", "green")
+ self.btnAScene = NIconToolButton(self, iSz)
self.btnAScene.clicked.connect(qtLambda(self._editHeading, self.EDIT_HSCENE))
self.swtAScene = NSwitch(self, height=iPx)
self.hdeAScene = QLabel(trHide, self)
@@ -633,7 +669,7 @@ class _HeadingsTab(NScrollablePage):
self.lblSection = QLabel(self._build.getLabel("headings.fmtSection"), self)
self.fmtSection = QLineEdit("", self)
self.fmtSection.setReadOnly(True)
- self.btnSection = NIconToolButton(self, iSz, "edit", "green")
+ self.btnSection = NIconToolButton(self, iSz)
self.btnSection.clicked.connect(qtLambda(self._editHeading, self.EDIT_SECTION))
self.swtSection = NSwitch(self, height=iPx)
self.hdeSection = QLabel(trHide, self)
@@ -767,8 +803,23 @@ class _HeadingsTab(NScrollablePage):
self.outerBox.addLayout(self.layoutMatrix)
self.outerBox.addStretch(1)
+ self.updateTheme()
self.setCentralLayout(self.outerBox)
+ def updateTheme(self) -> None:
+ """Update theme elements."""
+ logger.debug("Theme Update: _HeadingsTab")
+
+ self.btnPart.setThemeIcon("edit", "change")
+ self.btnChapter.setThemeIcon("edit", "change")
+ self.btnUnnumbered.setThemeIcon("edit", "change")
+ self.btnScene.setThemeIcon("edit", "change")
+ self.btnAScene.setThemeIcon("edit", "change")
+ self.btnSection.setThemeIcon("edit", "change")
+
+ self.formSyntax.initHighlighter()
+ self.formSyntax.rehighlight()
+
def loadContent(self) -> None:
"""Populate the widgets."""
def fmtBreak(text: str) -> str:
@@ -900,10 +951,14 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter):
def __init__(self, document: QTextDocument | None) -> None:
super().__init__(document)
- syntax = SHARED.theme.syntaxTheme
self._fmtSymbol = QTextCharFormat()
- self._fmtSymbol.setForeground(syntax.head)
self._fmtFormat = QTextCharFormat()
+ self.initHighlighter()
+
+ def initHighlighter(self) -> None:
+ """Update theme elements."""
+ syntax = SHARED.theme.syntaxTheme
+ self._fmtSymbol.setForeground(syntax.head)
self._fmtFormat.setForeground(syntax.emph)
def highlightBlock(self, text: str) -> None:
@@ -929,6 +984,7 @@ class _FormattingTab(NScrollableForm):
self.setHelpTextStyle(SHARED.theme.helpText)
self.buildForm()
+ self.updateTheme()
def buildForm(self) -> None:
"""Build the formatting form."""
@@ -972,7 +1028,7 @@ class _FormattingTab(NScrollableForm):
lambda keyword=keyword: self._updateIgnoredKeywords(keyword)
)
- self.ignoredKeywordsButton = NIconToolButton(self, iSz, "add", "green")
+ self.ignoredKeywordsButton = NIconToolButton(self, iSz)
self.ignoredKeywordsButton.setMenu(self.mnKeywords)
self.addRow(
self._build.getLabel("text.ignoredKeywords"), self.ignoredKeywords,
@@ -994,7 +1050,7 @@ class _FormattingTab(NScrollableForm):
# Text Font
self.textFont = QLineEdit(self)
self.textFont.setReadOnly(True)
- self.btnTextFont = NIconToolButton(self, iSz, "font")
+ self.btnTextFont = NIconToolButton(self, iSz)
self.btnTextFont.clicked.connect(self._selectFont)
self.addRow(
self._build.getLabel("format.textFont"), self.textFont,
@@ -1053,12 +1109,12 @@ class _FormattingTab(NScrollableForm):
self._sidebar.addButton(title, section)
self.addGroupLabel(title, section)
- pixT = SHARED.theme.getPixmap("margin_top", (iPx, iPx))
- pixB = SHARED.theme.getPixmap("margin_bottom", (iPx, iPx))
- pixL = SHARED.theme.getPixmap("margin_left", (iPx, iPx))
- pixR = SHARED.theme.getPixmap("margin_right", (iPx, iPx))
- pixH = SHARED.theme.getPixmap("fit_height", (iPx, iPx))
- pixW = SHARED.theme.getPixmap("fit_width", (iPx, iPx))
+ self.pixT = QLabel(self)
+ self.pixB = QLabel(self)
+ self.pixL = QLabel(self)
+ self.pixR = QLabel(self)
+ self.pixH = QLabel(self)
+ self.pixW = QLabel(self)
# Title
self.titleMarginT = NDoubleSpinBox(self)
@@ -1069,7 +1125,7 @@ class _FormattingTab(NScrollableForm):
self.addRow(
self._build.getLabel("format.titleMargin"),
- [pixT, self.titleMarginT, 6, pixB, self.titleMarginB],
+ [self.pixT, self.titleMarginT, 6, self.pixB, self.titleMarginB],
unit="em",
)
@@ -1082,7 +1138,7 @@ class _FormattingTab(NScrollableForm):
self.addRow(
self._build.getLabel("format.h1Margin"),
- [pixT, self.h1MarginT, 6, pixB, self.h1MarginB],
+ [self.pixT, self.h1MarginT, 6, self.pixB, self.h1MarginB],
unit="em",
)
@@ -1095,7 +1151,7 @@ class _FormattingTab(NScrollableForm):
self.addRow(
self._build.getLabel("format.h2Margin"),
- [pixT, self.h2MarginT, 6, pixB, self.h2MarginB],
+ [self.pixT, self.h2MarginT, 6, self.pixB, self.h2MarginB],
unit="em",
)
@@ -1108,7 +1164,7 @@ class _FormattingTab(NScrollableForm):
self.addRow(
self._build.getLabel("format.h3Margin"),
- [pixT, self.h3MarginT, 6, pixB, self.h3MarginB],
+ [self.pixT, self.h3MarginT, 6, self.pixB, self.h3MarginB],
unit="em",
)
@@ -1121,7 +1177,7 @@ class _FormattingTab(NScrollableForm):
self.addRow(
self._build.getLabel("format.h4Margin"),
- [pixT, self.h4MarginT, 6, pixB, self.h4MarginB],
+ [self.pixT, self.h4MarginT, 6, self.pixB, self.h4MarginB],
unit="em",
)
@@ -1134,7 +1190,7 @@ class _FormattingTab(NScrollableForm):
self.addRow(
self._build.getLabel("format.textMargin"),
- [pixT, self.textMarginT, 6, pixB, self.textMarginB],
+ [self.pixT, self.textMarginT, 6, self.pixB, self.textMarginB],
unit="em",
)
@@ -1147,7 +1203,7 @@ class _FormattingTab(NScrollableForm):
self.addRow(
self._build.getLabel("format.sepMargin"),
- [pixT, self.sepMarginT, 6, pixB, self.sepMarginB],
+ [self.pixT, self.sepMarginT, 6, self.pixB, self.sepMarginB],
unit="em",
)
@@ -1181,7 +1237,7 @@ class _FormattingTab(NScrollableForm):
self.addRow(
self._build.getLabel("format.pageSize"),
- [self.pageSize, 6, pixW, self.pageWidth, 6, pixH, self.pageHeight],
+ [self.pageSize, 6, self.pixW, self.pageWidth, 6, self.pixH, self.pageHeight],
)
# Page Margins
@@ -1199,11 +1255,11 @@ class _FormattingTab(NScrollableForm):
self.addRow(
self._build.getLabel("format.pageMargins"),
- [pixT, self.topMargin, 6, pixB, self.bottomMargin],
+ [self.pixT, self.topMargin, 6, self.pixB, self.bottomMargin],
)
self.addRow(
"",
- [pixL, self.leftMargin, 6, pixR, self.rightMargin],
+ [self.pixL, self.leftMargin, 6, self.pixR, self.rightMargin],
)
# Open Document
@@ -1217,7 +1273,7 @@ class _FormattingTab(NScrollableForm):
# Header
self.odtPageHeader = QLineEdit(self)
self.odtPageHeader.setMinimumWidth(200)
- self.btnPageHeader = NIconToolButton(self, iSz, "revert", "green")
+ self.btnPageHeader = NIconToolButton(self, iSz)
self.btnPageHeader.clicked.connect(self._resetPageHeader)
self.addRow(
self._build.getLabel("doc.pageHeader"), self.odtPageHeader,
@@ -1257,6 +1313,22 @@ class _FormattingTab(NScrollableForm):
# Finalise
self.finalise()
+ def updateTheme(self) -> None:
+ """Update theme elements."""
+ logger.debug("Theme Update: _FormattingTab")
+
+ self.ignoredKeywordsButton.setThemeIcon("add", "add")
+ self.btnTextFont.setThemeIcon("font", "tool")
+ self.btnPageHeader.setThemeIcon("revert", "reset")
+
+ iPx = SHARED.theme.baseIconHeight
+ self.pixT.setPixmap(SHARED.theme.getPixmap("margin_top", (iPx, iPx)))
+ self.pixB.setPixmap(SHARED.theme.getPixmap("margin_bottom", (iPx, iPx)))
+ self.pixL.setPixmap(SHARED.theme.getPixmap("margin_left", (iPx, iPx)))
+ self.pixR.setPixmap(SHARED.theme.getPixmap("margin_right", (iPx, iPx)))
+ self.pixH.setPixmap(SHARED.theme.getPixmap("fit_height", (iPx, iPx)))
+ self.pixW.setPixmap(SHARED.theme.getPixmap("fit_width", (iPx, iPx)))
+
def loadContent(self) -> None:
"""Populate the widgets."""
# Text Content
diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py
index 7447b553..24b7a9d4 100644
--- a/novelwriter/tools/noveldetails.py
+++ b/novelwriter/tools/noveldetails.py
@@ -38,12 +38,13 @@ from PyQt6.QtWidgets import (
from novelwriter import SHARED
from novelwriter.common import formatTime, numberToRoman
from novelwriter.constants import nwUnicode
+from novelwriter.enum import nwStandardButton
from novelwriter.extensions.configlayout import NColorLabel, NFixedPage, NScrollablePage
from novelwriter.extensions.modified import NNonBlockingDialog
from novelwriter.extensions.novelselector import NovelSelector
from novelwriter.extensions.pagedsidebar import NPagedSideBar
from novelwriter.extensions.switch import NSwitch
-from novelwriter.types import QtAlignRight, QtDecoration, QtDialogClose
+from novelwriter.types import QtAlignRight, QtDecoration, QtRoleDestruct
if TYPE_CHECKING:
from PyQt6.QtGui import QCloseEvent
@@ -102,8 +103,11 @@ class GuiNovelDetails(NNonBlockingDialog):
self.mainStack.addWidget(self.contentsPage)
# Buttons
- self.buttonBox = QDialogButtonBox(QtDialogClose, self)
- self.buttonBox.rejected.connect(self.reject)
+ self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self)
+ self.btnClose.clicked.connect(self.reject)
+
+ self.btnBox = QDialogButtonBox(self)
+ self.btnBox.addButton(self.btnClose, QtRoleDestruct)
# Assemble
self.topBox = QHBoxLayout()
@@ -119,7 +123,7 @@ class GuiNovelDetails(NNonBlockingDialog):
self.outerBox = QVBoxLayout()
self.outerBox.addLayout(self.topBox)
self.outerBox.addLayout(self.mainBox)
- self.outerBox.addWidget(self.buttonBox)
+ self.outerBox.addWidget(self.btnBox)
self.outerBox.setSpacing(8)
self.setLayout(self.outerBox)
diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py
index 69d2e090..933ecec1 100644
--- a/novelwriter/tools/welcome.py
+++ b/novelwriter/tools/welcome.py
@@ -35,15 +35,15 @@ from PyQt6.QtCore import (
from PyQt6.QtGui import QAction, QCloseEvent, QFont, QPainter, QPaintEvent, QPen, QShortcut
from PyQt6.QtWidgets import (
QApplication, QFileDialog, QFormLayout, QHBoxLayout, QLabel, QLineEdit,
- QListView, QMenu, QPushButton, QScrollArea, QStackedWidget,
- QStyledItemDelegate, QStyleOptionViewItem, QVBoxLayout, QWidget
+ QListView, QMenu, QScrollArea, QStackedWidget, QStyledItemDelegate,
+ QStyleOptionViewItem, QVBoxLayout, QWidget
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import formatInt, makeFileNameSafe, qtAddAction, qtLambda
from novelwriter.constants import nwFiles
from novelwriter.core.coretools import ProjectBuilder
-from novelwriter.enum import nwItemClass
+from novelwriter.enum import nwItemClass, nwStandardButton
from novelwriter.extensions.configlayout import NWrappedWidgetBox
from novelwriter.extensions.modified import NDialog, NIconToolButton, NSpinBox
from novelwriter.extensions.switch import NSwitch
@@ -75,8 +75,6 @@ class GuiWelcome(NDialog):
self.setMinimumHeight(450)
self.resize(*CONFIG.welcomeWinSize)
- btnIconSize = SHARED.theme.buttonIconSize
-
# Elements
# ========
@@ -104,34 +102,22 @@ class GuiWelcome(NDialog):
# Buttons
# =======
- self.btnList = QPushButton(self.tr("List"), self)
- self.btnList.setIcon(SHARED.theme.getIcon("list", "blue"))
- self.btnList.setIconSize(btnIconSize)
+ self.btnList = SHARED.theme.getStandardButton(nwStandardButton.LIST, self)
self.btnList.clicked.connect(self._showOpenProjectPage)
- self.btnNew = QPushButton(self.tr("New"), self)
- self.btnNew.setIcon(SHARED.theme.getIcon("add", "green"))
- self.btnNew.setIconSize(btnIconSize)
+ self.btnNew = SHARED.theme.getStandardButton(nwStandardButton.NEW, self)
self.btnNew.clicked.connect(self._showNewProjectPage)
- self.btnBrowse = QPushButton(self.tr("Browse"), self)
- self.btnBrowse.setIcon(SHARED.theme.getIcon("browse", "yellow"))
- self.btnBrowse.setIconSize(btnIconSize)
+ self.btnBrowse = SHARED.theme.getStandardButton(nwStandardButton.BROWSE, self)
self.btnBrowse.clicked.connect(self._browseForProject)
- self.btnCancel = QPushButton(self.tr("Cancel"), self)
- self.btnCancel.setIcon(SHARED.theme.getIcon("cancel", "red"))
- self.btnCancel.setIconSize(btnIconSize)
+ self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self)
self.btnCancel.clicked.connect(qtLambda(self.close))
- self.btnCreate = QPushButton(self.tr("Create"), self)
- self.btnCreate.setIcon(SHARED.theme.getIcon("star", "yellow"))
- self.btnCreate.setIconSize(btnIconSize)
+ self.btnCreate = SHARED.theme.getStandardButton(nwStandardButton.CREATE, self)
self.btnCreate.clicked.connect(self.tabNew.createNewProject)
- self.btnOpen = QPushButton(self.tr("Open"), self)
- self.btnOpen.setIcon(SHARED.theme.getIcon("open", "blue"))
- self.btnOpen.setIconSize(btnIconSize)
+ self.btnOpen = SHARED.theme.getStandardButton(nwStandardButton.OPEN, self)
self.btnOpen.clicked.connect(self._openSelectedItem)
self.btnBox = QHBoxLayout()
@@ -272,7 +258,7 @@ class _OpenProjectPage(QWidget):
# Info / Tool
self.aMissing = QAction(self)
- self.aMissing.setIcon(SHARED.theme.getIcon("alert_warn", "orange"))
+ self.aMissing.setIcon(SHARED.theme.getIcon("alert_warn", "warning"))
self.aMissing.setToolTip(self.tr("The project path is not reachable."))
self.selectedPath = QLineEdit(self)
@@ -548,7 +534,7 @@ class _NewProjectForm(QWidget):
self.projPath = QLineEdit(self)
self.projPath.setReadOnly(True)
- self.browsePath = NIconToolButton(self, iSz, "browse")
+ self.browsePath = NIconToolButton(self, iSz, "browse", "systemio")
self.browsePath.clicked.connect(self._doBrowse)
self.pathBox = QHBoxLayout()
@@ -559,20 +545,20 @@ class _NewProjectForm(QWidget):
self.projFill = QLineEdit(self)
self.projFill.setReadOnly(True)
- self.browseFill = NIconToolButton(self, iSz, "document_add", "blue")
+ self.browseFill = NIconToolButton(self, iSz, "document_add", "add")
self.fillMenu = QMenu(self.browseFill)
self.fillBlank = qtAddAction(self.fillMenu, self.tr("Create a fresh project"))
- self.fillBlank.setIcon(SHARED.theme.getIcon("document"))
+ self.fillBlank.setIcon(SHARED.theme.getIcon("document", "file"))
self.fillBlank.triggered.connect(self._setFillBlank)
self.fillSample = qtAddAction(self.fillMenu, self.tr("Create an example project"))
- self.fillSample.setIcon(SHARED.theme.getIcon("document_add", "blue"))
+ self.fillSample.setIcon(SHARED.theme.getIcon("document_add", "add"))
self.fillSample.triggered.connect(self._setFillSample)
self.fillCopy = qtAddAction(self.fillMenu, self.tr("Copy an existing project"))
- self.fillCopy.setIcon(SHARED.theme.getIcon("project_copy", "green"))
+ self.fillCopy.setIcon(SHARED.theme.getIcon("project_copy", "action"))
self.fillCopy.triggered.connect(self._setFillCopy)
self.browseFill.setMenu(self.fillMenu)
diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py
index 4494da8f..c8034487 100644
--- a/novelwriter/tools/writingstats.py
+++ b/novelwriter/tools/writingstats.py
@@ -39,12 +39,13 @@ from PyQt6.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.common import checkInt, checkIntTuple, formatTime, minmax, qtLambda
from novelwriter.constants import nwConst
+from novelwriter.enum import nwStandardButton
from novelwriter.error import formatException
-from novelwriter.extensions.modified import NToolDialog
+from novelwriter.extensions.modified import NPushButton, NToolDialog
from novelwriter.extensions.switch import NSwitch
from novelwriter.types import (
QtAlignLeftMiddle, QtAlignRight, QtAlignRightMiddle, QtDecoration,
- QtDialogClose, QtRoleAction
+ QtRoleAction, QtRoleDestruct
)
if TYPE_CHECKING:
@@ -182,6 +183,7 @@ class GuiWritingStats(NToolDialog):
# Filter Options
iPx = SHARED.theme.baseIconHeight
+ bSz = SHARED.theme.buttonIconSize
self.filterForm = QGridLayout(self)
self.filterForm.setRowStretch(6, 1)
@@ -276,6 +278,10 @@ class GuiWritingStats(NToolDialog):
self.optsBox.addWidget(self.histMax, 0)
# Buttons
+ self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self)
+ self.btnClose.clicked.connect(self._doClose)
+ self.btnClose.setAutoDefault(False)
+
self.saveJSON = QAction(self.tr("JSON Data File (.json)"), self)
self.saveJSON.triggered.connect(qtLambda(self._saveData, self.FMT_JSON))
@@ -286,17 +292,13 @@ class GuiWritingStats(NToolDialog):
self.saveMenu.addAction(self.saveJSON)
self.saveMenu.addAction(self.saveCSV)
- self.buttonBox = QDialogButtonBox(self)
- self.buttonBox.rejected.connect(self._doClose)
+ self.btnSave = NPushButton(self, self.tr("Save As"), bSz, "btn_save", "action")
+ self.btnSave.setAutoDefault(False)
+ self.btnSave.setMenu(self.saveMenu)
- self.btnClose = self.buttonBox.addButton(QtDialogClose)
- if self.btnClose:
- self.btnClose.setAutoDefault(False)
-
- self.btnSave = self.buttonBox.addButton(self.tr("Save As"), QtRoleAction)
- if self.btnSave:
- self.btnSave.setAutoDefault(False)
- self.btnSave.setMenu(self.saveMenu)
+ self.btnBox = QDialogButtonBox(self)
+ self.btnBox.addButton(self.btnSave, QtRoleAction)
+ self.btnBox.addButton(self.btnClose, QtRoleDestruct)
# Assemble
self.outerBox = QGridLayout()
@@ -304,7 +306,7 @@ class GuiWritingStats(NToolDialog):
self.outerBox.addLayout(self.optsBox, 1, 0, 1, 2)
self.outerBox.addWidget(self.infoBox, 2, 0)
self.outerBox.addWidget(self.filterBox, 2, 1)
- self.outerBox.addWidget(self.buttonBox, 3, 0, 1, 2)
+ self.outerBox.addWidget(self.btnBox, 3, 0, 1, 2)
self.outerBox.setRowStretch(0, 1)
self.setLayout(self.outerBox)
diff --git a/novelwriter/types.py b/novelwriter/types.py
index ba803715..658e4ed0 100644
--- a/novelwriter/types.py
+++ b/novelwriter/types.py
@@ -93,25 +93,27 @@ QtMouseMiddle = Qt.MouseButton.MiddleButton
QtAccepted = QDialog.DialogCode.Accepted
QtRejected = QDialog.DialogCode.Rejected
-QtDialogApply = QDialogButtonBox.StandardButton.Apply
-QtDialogCancel = QDialogButtonBox.StandardButton.Cancel
-QtDialogClose = QDialogButtonBox.StandardButton.Close
-QtDialogOk = QDialogButtonBox.StandardButton.Ok
-QtDialogReset = QDialogButtonBox.StandardButton.Reset
-QtDialogSave = QDialogButtonBox.StandardButton.Save
-
QtRoleAccept = QDialogButtonBox.ButtonRole.AcceptRole
QtRoleAction = QDialogButtonBox.ButtonRole.ActionRole
QtRoleApply = QDialogButtonBox.ButtonRole.ApplyRole
+QtRoleDestruct = QDialogButtonBox.ButtonRole.DestructiveRole
QtRoleReject = QDialogButtonBox.ButtonRole.RejectRole
+QtRoleReset = QDialogButtonBox.ButtonRole.ResetRole
# Cursor Types
QtKeepAnchor = QTextCursor.MoveMode.KeepAnchor
QtMoveAnchor = QTextCursor.MoveMode.MoveAnchor
+
QtMoveLeft = QTextCursor.MoveOperation.Left
QtMoveRight = QTextCursor.MoveOperation.Right
+QtSelectWord = QTextCursor.SelectionType.WordUnderCursor
+QtSelectBlock = QTextCursor.SelectionType.BlockUnderCursor
+QtSelectDocument = QTextCursor.SelectionType.Document
+
+QtImCursorRectangle = Qt.InputMethodQuery.ImCursorRectangle
+
# Size Policy
QtSizeExpanding = QSizePolicy.Policy.Expanding
diff --git a/pkgutils.py b/pkgutils.py
index 4ef16fd5..1c17e960 100755
--- a/pkgutils.py
+++ b/pkgutils.py
@@ -29,7 +29,6 @@ from __future__ import annotations
import argparse
import datetime
import shutil
-import subprocess
import sys
import utils.assets
@@ -40,7 +39,10 @@ import utils.build_windows
import utils.docs
import utils.icon_themes
-from utils.common import ROOT_DIR, SETUP_DIR, extractVersion, readFile, stripVersion, writeFile
+from utils.common import (
+ ROOT_DIR, SETUP_DIR, extractReqs, extractVersion, readFile, stripVersion,
+ writeFile
+)
OS_LINUX = sys.platform.startswith("linux")
OS_DARWIN = sys.platform.startswith("darwin")
@@ -52,33 +54,6 @@ def printVersion(args: argparse.Namespace) -> None:
print(extractVersion(beQuiet=True)[0], end=None)
-def installPackages(args: argparse.Namespace) -> None:
- """Install package dependencies both for this script and for running
- novelWriter itself.
- """
- print("")
- print("Installing Dependencies")
- print("=======================")
- print("")
-
- installQueue = ["pip", "-r requirements.txt"]
- if args.mac:
- installQueue.append("pyobjc")
- elif args.win:
- installQueue.append("pywin32")
-
- pyCmd = [sys.executable, "-m"]
- pipCmd = ["pip", "install", "--user", "--upgrade"]
- for stepCmd in installQueue:
- pkgCmd = stepCmd.split(" ")
- try:
- subprocess.call(pyCmd + pipCmd + pkgCmd)
- except Exception as exc:
- print("Failed with error:")
- print(str(exc))
- sys.exit(1)
-
-
def cleanBuildDirs(args: argparse.Namespace) -> None:
"""Recursively delete the 'build' and 'dist' folders."""
print("")
@@ -130,6 +105,15 @@ def genMacOSPlist(args: argparse.Namespace) -> None:
writeFile(outDir / "Info.plist", plistXML)
+def genReqFiles(args: argparse.Namespace) -> None:
+ """Generate requirements.txt file from pyproject.toml."""
+ select = [s.strip().lower() for s in args.groups] if args.groups else ["app"]
+ (ROOT_DIR / "requirements.txt").write_text(
+ "\n".join(extractReqs(select)),
+ encoding="utf-8"
+ )
+
+
if __name__ == "__main__":
"""Parse command line options and run the commands."""
parser = argparse.ArgumentParser(
@@ -148,18 +132,6 @@ if __name__ == "__main__":
)
cmdVersion.set_defaults(func=printVersion)
- # General
- # =======
-
- # Pip Install
- cmdPipInstall = parsers.add_parser(
- "pip", help="Install all package dependencies for novelWriter using pip."
- )
- cmdPipInstall.add_argument("--linux", action="store_true", help="For Linux.", default=OS_LINUX)
- cmdPipInstall.add_argument("--mac", action="store_true", help="For MacOS.", default=OS_DARWIN)
- cmdPipInstall.add_argument("--win", action="store_true", help="For Windows.", default=OS_WIN)
- cmdPipInstall.set_defaults(func=installPackages)
-
# Additional Builds
# =================
@@ -222,7 +194,7 @@ if __name__ == "__main__":
cmdBuildHtmlDocs = parsers.add_parser(
"docs-html", help="Build the HTML docs."
)
- cmdBuildHtmlDocs.add_argument("lang", nargs="+")
+ cmdBuildHtmlDocs.add_argument("lang", nargs="+", help="Language codes to generate docs for.")
cmdBuildHtmlDocs.set_defaults(func=utils.docs.buildHtmlDocs)
# Build Sample
@@ -260,8 +232,7 @@ if __name__ == "__main__":
cmdBuildUbuntu = parsers.add_parser(
"build-ubuntu", help=(
"Build a .deb package for Debian and Ubuntu. "
- "Add --sign to sign package. "
- "Add --first to set build number to 0."
+ "Add --sign to sign package."
)
)
cmdBuildUbuntu.add_argument("--sign", action="store_true", help="Sign the package.")
@@ -296,10 +267,23 @@ if __name__ == "__main__":
cmdBuildClean.set_defaults(func=cleanBuildDirs)
# Generate MacOS PList File
- cmdBuildMacOSPlist = parsers.add_parser(
+ cmdGenMacOSPlist = parsers.add_parser(
"gen-plist", help="Generate an Info.plist for use in a MacOS Bundle."
)
- cmdBuildMacOSPlist.set_defaults(func=genMacOSPlist)
+ cmdGenMacOSPlist.set_defaults(func=genMacOSPlist)
+
+ # Generate Requirement File
+ cmdGenReq = parsers.add_parser(
+ "gen-req", help="Generate a requirements.txt file for pip."
+ )
+ cmdGenReq.add_argument(
+ "groups", nargs="*", help=(
+ "Groups to generate for, or 'all' to generate for all groups. "
+ "Use 'app' to generate for just the core application. "
+ "Defaults to 'app' if none are specified."
+ )
+ )
+ cmdGenReq.set_defaults(func=genReqFiles)
args = parser.parse_args()
args.func(args)
diff --git a/pyproject.toml b/pyproject.toml
index 920d7d6d..17599847 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,25 +1,22 @@
[build-system]
-requires = ["setuptools >= 77.0.3"]
+requires = ["setuptools>=77.0.3"]
build-backend = "setuptools.build_meta"
[project]
name = "novelWriter"
-authors = [
- {name = "Veronica Berglyd Olsen", email = "code@vkbo.net"},
-]
+authors = [{ name = "Veronica Berglyd Olsen", email = "code@vkbo.net" }]
description = "A plain text editor for planning and writing novels"
-readme = {file = "setup/description_pypi.md", content-type = "text/markdown"}
+readme = { file = "setup/description_pypi.md", content-type = "text/markdown" }
license = "GPL-3.0-or-later AND Apache-2.0 AND CC-BY-4.0"
-license-files = [
- "LICENSE.md",
- "setup/LICENSE-Apache-2.0.txt",
-]
+license-files = ["LICENSE.md", "setup/LICENSE-Apache-2.0.txt"]
+dynamic = ["version"]
+requires-python = ">=3.11"
classifiers = [
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: CPython",
"Development Status :: 5 - Production/Stable",
"Operating System :: OS Independent",
@@ -27,12 +24,32 @@ classifiers = [
"Natural Language :: English",
"Topic :: Text Editors",
]
-requires-python = ">=3.10"
dependencies = [
"pyqt6>=6.4",
- "pyenchant>=3.0.0",
+ "pyenchant>=3.3.0", # 3.3 is needed for MacOS AARCH64 builds
]
-dynamic = ["version"]
+
+[dependency-groups]
+dev = [
+ { include-group = "build" },
+ { include-group = "docs" },
+ { include-group = "test" },
+ { include-group = "lint" },
+]
+build = ["build", "setuptools>=77.0.3"]
+docs = [
+ "docutils>=0.17.1",
+ "pygments>=2.7",
+ "sphinx-book-theme",
+ "sphinx-copybutton",
+ "sphinx-design",
+ "sphinx-favicon",
+ "sphinx-intl",
+ "sphinx>=5.0",
+]
+test = ["coverage>=7.2.0", "pytest-qt", "pytest-timeout", "pytest>=6.0.0"]
+lint = ["isort", "pyright", "ruff"]
+macos = ["pyobjc"]
[project.urls]
Homepage = "https://novelwriter.io"
@@ -44,13 +61,13 @@ Issues = "https://github.com/vkbo/novelWriter/issues"
novelwriter = "novelwriter:main"
[tool.setuptools.dynamic]
-version = {attr = "novelwriter.__version__"}
+version = { attr = "novelwriter.__init__.__version__" }
[tool.setuptools.packages.find]
include = ["novelwriter*"]
[tool.isort]
-py_version="310"
+py_version = "311"
line_length = 99
wrap_length = 79
multi_line_output = 5
@@ -66,26 +83,26 @@ preview = true
# Rules: https://docs.astral.sh/ruff/rules
select = [
- "A", # flake8-builtins (A)
- "ANN", # flake8-annotations (ANN)
- "B", # flake8-bugbear (B)
- "D", # pydocstyle (D)
- "E", # pycodestyle (E)
- "F", # Pyflakes (F)
- "FA", # flake8-future-annotations (FA)
- "PERF", # Perflint (PERF)
- "PLC", # Pylint Convention (PLC)
- "PLE", # Pylint Error (PLE)
- "PLR17", # Refactor (PLR) - Only PLR17xx
- "PLW", # Pylint Warning (PLW)
- "Q", # flake8-quotes (Q)
- "RET", # flake8-return (RET)
- "RUF", # Ruff-specific rules (RUF)
- "SLF", # flake8-self (SLF)
- "SLOT", # flake8-slots (SLOT)
- "TC", # flake8-type-checking (TC)
- "UP", # pyupgrade (UP)
- "W", # pycodestyle (W)
+ "A", # flake8-builtins (A)
+ "ANN", # flake8-annotations (ANN)
+ "B", # flake8-bugbear (B)
+ "D", # pydocstyle (D)
+ "E", # pycodestyle (E)
+ "F", # Pyflakes (F)
+ "FA", # flake8-future-annotations (FA)
+ "PERF", # Perflint (PERF)
+ "PLC", # Pylint Convention (PLC)
+ "PLE", # Pylint Error (PLE)
+ "PLR17", # Refactor (PLR) - Only PLR17xx
+ "PLW", # Pylint Warning (PLW)
+ "Q", # flake8-quotes (Q)
+ "RET", # flake8-return (RET)
+ "RUF", # Ruff-specific rules (RUF)
+ "SLF", # flake8-self (SLF)
+ "SLOT", # flake8-slots (SLOT)
+ "TC", # flake8-type-checking (TC)
+ "UP", # pyupgrade (UP)
+ "W", # pycodestyle (W)
]
ignore = [
"ANN401", # any-type
@@ -135,7 +152,7 @@ include = ["novelwriter"]
exclude = ["**/__pycache__"]
reportIncompatibleMethodOverride = false
-pythonVersion = "3.10"
+pythonVersion = "3.11"
[tool.pytest.ini_options]
log_level = "DEBUG"
@@ -148,9 +165,8 @@ markers = [
[tool.coverage.run]
branch = false
+source = ["novelwriter"]
[tool.coverage.report]
precision = 2
-exclude_also = [
- "if TYPE_CHECKING:"
-]
+exclude_also = ["if TYPE_CHECKING:"]
diff --git a/requirements-all.txt b/requirements-all.txt
deleted file mode 100644
index d140ead6..00000000
--- a/requirements-all.txt
+++ /dev/null
@@ -1,5 +0,0 @@
--r requirements.txt
--r requirements-dev.txt
--r docs/requirements.txt
--r setup/requirements.txt
--r tests/requirements.txt
diff --git a/requirements-dev.txt b/requirements-dev.txt
deleted file mode 100644
index 794644e0..00000000
--- a/requirements-dev.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-build
-isort
-pyright
-ruff
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index fa1cf957..00000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-pyqt6>=6.4
-pyenchant>=3.0.0
diff --git a/run_tests.py b/run_tests.py
index b9687aa9..6f593127 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -3,6 +3,7 @@
import argparse
import os
+import shlex
import subprocess
import sys
@@ -12,27 +13,44 @@ if __name__ == "__main__":
parser.add_argument("-o", action="store_true", help="Run off screen")
parser.add_argument("-r", action="store_true", help="Generate reports")
parser.add_argument("-t", action="store_true", help="Generate terminal report")
- parser.add_argument("-m", help="Test modules")
- parser.add_argument("-k", help="Test filters")
+ parser.add_argument("-u", action="store_true", help="Generate uncovered terminal report")
+ parser.add_argument("-lf", action="store_true", help="Re-run failed tests")
+ parser.add_argument("-sw", action="store_true", help="Run tests stepwise")
+ parser.add_argument("-m", help="Test modules", metavar="MARKEXPR")
+ parser.add_argument("-k", help="Test filters", metavar="EXPRESSION")
args = parser.parse_args()
env = os.environ.copy()
env["QT_SCALE_FACTOR"] = "1.0"
- cmd = [sys.executable, "-m", "pytest", "-vv"]
+ if args.r or args.t or args.u:
+ cmd = ["coverage", "run"]
+ if args.lf or args.sw:
+ cmd += ["--append"]
+ cmd += ["-m"]
+ else:
+ cmd = [sys.executable, "-m"]
+
+ cmd += ["pytest", "-vv"]
if args.o:
env["QT_QPA_PLATFORM"] = "offscreen"
- if args.r or args.t:
- cmd += ["--cov=novelwriter"]
- if args.r:
- cmd += ["--cov-report=xml", "--cov-report=html"]
- if args.t:
- cmd += ["--cov-report=term"]
+ if args.lf:
+ cmd += ["--last-failed"]
+ if args.sw:
+ cmd += ["--stepwise"]
if args.m:
cmd += ["-m", args.m]
if args.k:
cmd += ["-k", args.k]
- print("Calling:", " ".join(cmd))
+ print("Calling:", shlex.join(cmd))
subprocess.call(cmd, env=env)
+
+ if args.r:
+ subprocess.call(["coverage", "xml"])
+ subprocess.call(["coverage", "html"])
+ if args.t and not args.u:
+ subprocess.call(["coverage", "report"])
+ if args.u:
+ subprocess.call(["coverage", "report", "--skip-covered "])
diff --git a/setup/debian/control b/setup/debian/control
index 24f0cd25..06d63fa8 100644
--- a/setup/debian/control
+++ b/setup/debian/control
@@ -4,24 +4,26 @@ Section: text
Priority: optional
Build-Depends:
dh-python,
+ pybuild-plugin-pyproject,
+ python3-build,
python3-setuptools,
python3-all,
debhelper (>= 9),
- python3 (>=3.10),
+ python3 (>=3.11),
python3-pyqt6 (>= 6.4),
python3-pyqt6.qtsvg (>= 6.4),
python3-enchant (>= 2.0),
qt6-image-formats-plugins (>= 6.4)
Standards-Version: 4.5.1
Homepage: https://novelwriter.io
-X-Python3-Version: >= 3.10
+X-Python3-Version: >= 3.11
Package: novelwriter
Architecture: all
Depends:
${misc:Depends},
${python3:Depends},
- python3 (>=3.10),
+ python3 (>=3.11),
python3-pyqt6 (>= 6.4),
python3-pyqt6.qtsvg (>= 6.4),
python3-enchant (>= 2.0),
diff --git a/setup/macos/build.sh b/setup/macos/build.sh
index ca475fa1..cafa196a 100755
--- a/setup/macos/build.sh
+++ b/setup/macos/build.sh
@@ -1,7 +1,7 @@
#! /bin/bash
if [[ -z "$1" || -z "$2" || -z "$3" ]]; then
- echo "Not enouch input arguments"
+ echo "Not enough input arguments"
exit 1
fi
@@ -108,8 +108,8 @@ conda install -c conda-forge enchant hunspell-en --yes
# Install dependencies
echo "Installing Python dependencies ..."
+python3 pkgutils.py gen-req
pip install -r "$SRC_DIR/requirements.txt"
-pip install pyenchant==3.3.0rc1
# Leave conda env
conda deactivate
diff --git a/setup/make_pip.sh b/setup/make_pip.sh
index 681a3cb5..9d3c61b2 100755
--- a/setup/make_pip.sh
+++ b/setup/make_pip.sh
@@ -1,36 +1,23 @@
#!/bin/bash
set -e
-ENVPATH=/tmp/nwBuild
-
if [ ! -f pkgutils.py ]; then
echo "Must be called from the root folder of the source"
exit 1
fi
-echo ""
-echo " Create Python Env"
-echo "================================================================================"
-echo ""
-
-if [ ! -d $ENVPATH ]; then
- python3 -m venv $ENVPATH
-fi
-source $ENVPATH/bin/activate
-pip3 install -U build twine -r requirements.txt -r docs/requirements.txt
-
echo ""
echo " Building Dependencies"
echo "================================================================================"
echo ""
-python3 pkgutils.py build-assets
-python3 pkgutils.py icons optional
+uv run pkgutils.py build-assets
+uv run pkgutils.py icons optional
echo ""
echo " Building Packages"
echo "================================================================================"
echo ""
-python3 -m build
+uv build
mkdir -pv dist_upload
cp -v dist/novelwriter-*.whl dist_upload/
cd dist_upload
@@ -38,13 +25,6 @@ FILE=$(ls -t | head -1)
shasum -a 256 $FILE | tee $FILE.sha256
cd ..
-echo ""
-echo " Checking Packages"
-echo "================================================================================"
-echo ""
-twine check dist/*
-deactivate
-
echo ""
echo " Done!"
echo "================================================================================"
diff --git a/setup/make_release.sh b/setup/make_release.sh
index 97438bd9..8c59a933 100755
--- a/setup/make_release.sh
+++ b/setup/make_release.sh
@@ -1,8 +1,6 @@
#!/bin/bash
set -e
-ENVPATH=/tmp/nwBuild
-
if [ ! -f pkgutils.py ]; then
echo "Must be called from the root folder of the source"
exit 1
@@ -12,18 +10,12 @@ echo ""
echo " Building Dependencies"
echo "================================================================================"
echo ""
-if [ ! -d $ENVPATH ]; then
- python3 -m venv $ENVPATH
-fi
-source $ENVPATH/bin/activate
-pip3 install -r requirements.txt -r docs/requirements.txt
-python3 pkgutils.py build-assets
-python3 pkgutils.py icons optional
-deactivate
+uv run pkgutils.py build-assets
+uv run pkgutils.py icons optional
echo ""
echo " Building Linux Packages"
echo "================================================================================"
echo ""
-python3 pkgutils.py build-deb --sign
-python3 pkgutils.py build-ubuntu --sign
+uv run pkgutils.py build-deb --sign
+uv run pkgutils.py build-ubuntu --sign
diff --git a/setup/requirements.txt b/setup/requirements.txt
deleted file mode 100644
index 120a3f4f..00000000
--- a/setup/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-setuptools>=77.0.3
-twine
diff --git a/tests/conftest.py b/tests/conftest.py
index 765755bf..f8e3a5eb 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -157,9 +157,11 @@ def projPath(fncPath):
def mockGUI(qtbot, monkeypatch):
"""Create a mock instance of novelWriter's main GUI class."""
from novelwriter.gui.theme import GuiTheme
+ from novelwriter.shared import _GuiAlert
monkeypatch.setattr(QMessageBox, "exec", lambda *a: None)
- monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes)
+ monkeypatch.setattr(_GuiAlert, "exec", lambda *a: None)
+ monkeypatch.setattr(_GuiAlert, "finalState", True)
gui = MockGuiMain()
theme = GuiTheme()
monkeypatch.setattr(SHARED, "_gui", gui)
@@ -182,9 +184,11 @@ def nwGUI(qtbot, monkeypatch, functionFixture):
"""Create an instance of the novelWriter GUI."""
from novelwriter.gui.theme import GuiTheme
from novelwriter.guimain import GuiMain
+ from novelwriter.shared import _GuiAlert
monkeypatch.setattr(QMessageBox, "exec", lambda *a: None)
- monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes)
+ monkeypatch.setattr(_GuiAlert, "exec", lambda *a: None)
+ monkeypatch.setattr(_GuiAlert, "finalState", True)
CONFIG.loadConfig()
SHARED.initTheme(GuiTheme())
diff --git a/tests/files/all_icons.json b/tests/files/all_icons.json
index 3d85f818..0579a3df 100644
--- a/tests/files/all_icons.json
+++ b/tests/files/all_icons.json
@@ -60,6 +60,24 @@
"theme_dark",
"theme_auto",
+ "btn_ok",
+ "btn_cancel",
+ "btn_yes",
+ "btn_no",
+ "btn_open",
+ "btn_close",
+ "btn_save",
+ "btn_browse",
+ "btn_list",
+ "btn_new",
+ "btn_create",
+ "btn_reset",
+ "btn_insert",
+ "btn_apply",
+ "btn_build",
+ "btn_print",
+ "btn_preview",
+
"add",
"bookmarks",
"browse",
@@ -95,7 +113,6 @@
"more_arrow",
"more_vertical",
"noncheckable",
- "open",
"panel",
"pin",
"project_copy",
@@ -104,7 +121,6 @@
"remove",
"revert",
"settings",
- "star",
"stats",
"text",
"timer_off",
diff --git a/tests/mocked.py b/tests/mocked.py
index f86d5be0..92b56abe 100644
--- a/tests/mocked.py
+++ b/tests/mocked.py
@@ -22,9 +22,12 @@ from __future__ import annotations
from unittest.mock import MagicMock
+from PyQt6.QtCore import QSize
from PyQt6.QtGui import QFont, QIcon, QPixmap
from PyQt6.QtWidgets import QWidget
+from novelwriter.extensions.modified import NPushButton
+
class MockGuiMain(QWidget):
@@ -72,6 +75,9 @@ class MockTheme:
def getHeaderDecoration(self, *a) -> QPixmap:
return QPixmap()
+ def getStandardButton(self, *a) -> NPushButton:
+ return NPushButton(None, "", QSize(1, 1)) # type: ignore
+
def getIcon(self, *a) -> QIcon:
return QIcon()
diff --git a/tests/reference/coreIndex_LoadSave_tagsIndex.json b/tests/reference/coreIndex_LoadSave_tagsIndex.json
index c8365601..af9474f0 100644
--- a/tests/reference/coreIndex_LoadSave_tagsIndex.json
+++ b/tests/reference/coreIndex_LoadSave_tagsIndex.json
@@ -1,4 +1,8 @@
{
+ "novelWriter.meta": {
+ "version": "0x020800a2",
+ "timestamp": "2025-10-05 17:06:58"
+ },
"novelWriter.tagsIndex": {
"bod": {"name": "Bod", "display": "Nobody Owens", "handle": "4c4f28287af27", "heading": "T0001", "class": "CHARACTER"},
"main": {"name": "Main", "display": "Main", "handle": "2426c6f0ca922", "heading": "T0001", "class": "PLOT"},
diff --git a/tests/requirements.txt b/tests/requirements.txt
deleted file mode 100644
index 39f96e3c..00000000
--- a/tests/requirements.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-coverage>=7.2.0
-pytest-cov
-pytest-qt
-pytest-timeout
-pytest>=6.0.0
diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py
index 95d339df..7ba2ddb0 100644
--- a/tests/test_base/test_base_common.py
+++ b/tests/test_base/test_base_common.py
@@ -36,10 +36,10 @@ from novelwriter.common import (
describeFont, elide, encodeMimeHandles, firstFloat, fontMatcher,
formatFileFilter, formatInt, formatTime, formatTimeStamp, formatVersion,
fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, isItemLayout,
- isItemType, isListInstance, isTitleTag, jsonEncode, makeFileNameSafe,
- minmax, numberToRoman, openExternalPath, processDialogSymbols,
- readTextFile, simplified, transferCase, uniqueCompact, utf16CharMap,
- xmlElement, xmlIndent, xmlSubElem, yesNo
+ isItemType, isListInstance, isTitleTag, jsonCombine, jsonEncode,
+ makeFileNameSafe, minmax, numberToRoman, openExternalPath,
+ processDialogSymbols, readTextFile, simplified, transferCase,
+ uniqueCompact, utf16CharMap, xmlElement, xmlIndent, xmlSubElem, yesNo
)
from novelwriter.enum import nwItemClass
@@ -651,6 +651,17 @@ def testBaseCommon_jsonEncode():
)
+@pytest.mark.base
+def testBaseCommon_jsonCombine():
+ """Test the jsonCombine function."""
+ assert jsonCombine({"a": "[1, 2]", "b": "[3, 4]"}) == (
+ '{\n'
+ ' "a": [1, 2],\n'
+ ' "b": [3, 4]\n'
+ '}\n'
+ )
+
+
@pytest.mark.base
def testBaseCommon_xmlIndent():
"""Test the xmlIndent function."""
diff --git a/tests/test_base/test_base_shared.py b/tests/test_base/test_base_shared.py
index 64cc306b..0a37b624 100644
--- a/tests/test_base/test_base_shared.py
+++ b/tests/test_base/test_base_shared.py
@@ -20,16 +20,18 @@ along with this program. If not, see .
""" # noqa
from __future__ import annotations
+import sys
+
from unittest.mock import MagicMock
import pytest
from PyQt6.QtCore import QUrl
from PyQt6.QtGui import QDesktopServices
-from PyQt6.QtWidgets import QFileDialog, QMessageBox, QWidget
+from PyQt6.QtWidgets import QFileDialog, QWidget
from novelwriter.core.project import NWProject
-from novelwriter.shared import SharedData
+from novelwriter.shared import SharedData, _GuiAlert
from tests.mocked import MockGuiMain, MockTheme
from tests.tools import buildTestProject
@@ -143,10 +145,10 @@ def testBaseSharedData_Projects(monkeypatch, caplog, fncPath):
@pytest.mark.base
-def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog):
+def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog, mockGUI):
"""Test SharedData class alert helper functions."""
- monkeypatch.setattr(QMessageBox, "exec", lambda *a: None)
- monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes)
+ monkeypatch.setattr(_GuiAlert, "exec", lambda *a: None)
+ monkeypatch.setattr(_GuiAlert, "finalState", True)
shared = SharedData()
@@ -188,3 +190,67 @@ def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog):
# Question box
assert shared.question("Why?") is True
assert shared.lastAlert == "Why?"
+
+
+@pytest.mark.base
+def testBaseSharedData_GuiAlert():
+ """Test the _GuiAlert class."""
+ alert = _GuiAlert(None, MockTheme()) # type: ignore
+
+ # Default states
+ assert alert.logMessage == ""
+ assert alert.finalState is False
+
+ # Populate message
+ text = "one"
+ info = "two"
+ details = "three"
+ alert.setMessage(text, info, details)
+ assert alert.logMessage == f"{text} {info} {details}"
+ assert alert.text() == text
+ assert alert.informativeText() == info
+ assert alert.detailedText() == details
+
+ # Populate exception
+ exc = Exception("oops")
+ alert.setException(exc)
+ assert alert.logMessage == f"{text} {info} {details}"
+ assert alert.informativeText() == f"{info} Exception: {exc!s}"
+
+ # Alert: Info
+ alert.setAlertType(_GuiAlert.INFO, False)
+ assert hasattr(alert, "_btnOk")
+ if sys.platform != "darwin": # Not set on MacOS
+ assert alert.windowTitle() == "Information"
+ alert._btnOk.click()
+ assert alert.finalState is True
+ alert._state = False
+
+ # Alert: Warning
+ alert.setAlertType(_GuiAlert.WARN, False)
+ assert hasattr(alert, "_btnOk")
+ if sys.platform != "darwin": # Not set on MacOS
+ assert alert.windowTitle() == "Warning"
+ alert._btnOk.click()
+ assert alert.finalState is True
+ alert._state = False
+
+ # Alert: Error
+ alert.setAlertType(_GuiAlert.ERROR, False)
+ assert hasattr(alert, "_btnOk")
+ if sys.platform != "darwin": # Not set on MacOS
+ assert alert.windowTitle() == "Error"
+ alert._btnOk.click()
+ assert alert.finalState is True
+ alert._state = False
+
+ # Alert: Question
+ alert.setAlertType(_GuiAlert.ASK, True)
+ assert hasattr(alert, "_btnYes")
+ assert hasattr(alert, "_btnNo")
+ if sys.platform != "darwin": # Not set on MacOS
+ assert alert.windowTitle() == "Question"
+ alert._btnYes.click()
+ assert alert.finalState is True
+ alert._btnNo.click()
+ assert alert.finalState is False
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py
index 332c27c3..9032aed8 100644
--- a/tests/test_core/test_core_index.py
+++ b/tests/test_core/test_core_index.py
@@ -157,7 +157,7 @@ def testCoreIndex_LoadSave(qtbot, monkeypatch, prjLipsum, nwGUI, tstPaths):
# Check File
copyfile(projFile, testFile)
- assert cmpFiles(testFile, compFile)
+ assert cmpFiles(testFile, compFile, ignoreLines=[3, 4])
# Write an empty index file and load it
projFile.write_text("{}", encoding="utf-8")
diff --git a/tests/test_core/test_core_indexdata.py b/tests/test_core/test_core_indexdata.py
index faef5d51..cb749be7 100644
--- a/tests/test_core/test_core_indexdata.py
+++ b/tests/test_core/test_core_indexdata.py
@@ -251,13 +251,17 @@ def testCoreIndexData_IndexHeading():
"note.consitency": "Only explode once",
}
+ # Append Synopsis
+ head.setComment(nwComment.SYNOPSIS.name, "", "How it started ...")
+ assert head.synopsis == "In the beginning ...\n\nHow it started ..."
+
# Unpack KeyError
with pytest.raises(KeyError, match="Unknown key in heading entry"):
head.unpackData({"stuff": "more stuff"})
# Unpack Comments
head.unpackData({"summary": "How it started ..."})
- assert head.synopsis == "How it started ..."
+ assert head.synopsis == "How it started ..." # This resets the comments dictionary
@pytest.mark.core
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index cdb202c1..799cd423 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -25,13 +25,12 @@ from zipfile import ZipFile
import pytest
-from PyQt6.QtWidgets import QMessageBox
-
from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwFiles
from novelwriter.core.project import NWProject, NWProjectState
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
from novelwriter.enum import nwItemClass
+from novelwriter.shared import _GuiAlert
from tests.mocked import causeOSError
from tests.tools import XML_IGNORE, C, buildTestProject, cmpFiles
@@ -274,14 +273,14 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
# Won't convert legacy file
with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY))
- mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
+ mp.setattr(_GuiAlert, "finalState", False)
assert project.openProject(fncPath, clearLock=True) is False
assert "The file format of your project is about to be" in SHARED.lastAlert
# Won't open project from newer version
with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999))
- mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
+ mp.setattr(_GuiAlert, "finalState", False)
assert project.openProject(fncPath, clearLock=True) is False
assert "This project was saved by a newer version" in SHARED.lastAlert
diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py
index dd749aa5..f9509528 100644
--- a/tests/test_dialogs/test_dlg_preferences.py
+++ b/tests/test_dialogs/test_dlg_preferences.py
@@ -32,7 +32,7 @@ from novelwriter.constants import nwUnicode
from novelwriter.dialogs.preferences import GuiPreferences
from novelwriter.dialogs.quotes import GuiQuoteSelect
from novelwriter.gui.theme import ThemeEntry
-from novelwriter.types import QtDialogCancel, QtDialogSave, QtModNone
+from novelwriter.types import QtModNone
KEY_DELAY = 1
@@ -118,16 +118,12 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
# Check Save Button
prefs.show()
with qtbot.waitSignal(prefs.newPreferencesReady) as signal:
- button = prefs.buttonBox.button(QtDialogSave)
- assert button is not None
- button.click()
+ prefs.btnSave.click()
assert len(signal.args) == 4
# Check Close Button
prefs.show()
- button = prefs.buttonBox.button(QtDialogCancel)
- assert button is not None
- button.click()
+ prefs.btnCancel.click()
assert prefs.isHidden() is True
# Close Using Escape Key
@@ -342,9 +338,7 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
with monkeypatch.context() as mp:
mp.setattr(QFontDatabase, "families", lambda *a: ["TestFont"])
with qtbot.waitSignal(prefs.newPreferencesReady) as signal:
- button = prefs.buttonBox.button(QtDialogSave)
- assert button is not None
- button.click()
+ prefs.btnSave.click()
assert signal.args == [True, True, True, True]
# Check Settings
diff --git a/tests/test_ext/test_ext_modified.py b/tests/test_ext/test_ext_modified.py
index 7e65de1a..d7c2ae09 100644
--- a/tests/test_ext/test_ext_modified.py
+++ b/tests/test_ext/test_ext_modified.py
@@ -22,12 +22,13 @@ from __future__ import annotations
import pytest
-from PyQt6.QtCore import QEvent, QPoint, QPointF, Qt
+from PyQt6.QtCore import QEvent, QPoint, QPointF, QSize, Qt
from PyQt6.QtGui import QKeyEvent, QMouseEvent, QStandardItem, QStandardItemModel, QWheelEvent
from PyQt6.QtWidgets import QWidget
from novelwriter.extensions.modified import (
- NClickableLabel, NComboBox, NDialog, NDoubleSpinBox, NSpinBox, NTreeView
+ NClickableLabel, NComboBox, NDialog, NDoubleSpinBox, NIconToggleButton,
+ NIconToolButton, NSpinBox, NTreeView
)
from novelwriter.types import QtModNone, QtMouseLeft, QtMouseMiddle, QtRejected
@@ -168,7 +169,7 @@ def testExtModified_NDoubleSpinBox(qtbot, monkeypatch):
@pytest.mark.gui
-def testExtModified_NClickableLabel(qtbot, monkeypatch):
+def testExtModified_NClickableLabel(qtbot):
"""Test the NClickableLabel class."""
widget = NClickableLabel()
dialog = SimpleDialog(widget)
@@ -181,3 +182,23 @@ def testExtModified_NClickableLabel(qtbot, monkeypatch):
with qtbot.waitSignal(widget.mouseClicked):
widget.mousePressEvent(event)
+
+
+@pytest.mark.gui
+def testExtModified_ToolButtons(qtbot, mockGUI):
+ """Test the NIconToolButton and NIconToggleButton classes."""
+ dialog = SimpleDialog(None)
+
+ size = QSize(16, 16)
+ button1 = NIconToolButton(dialog, size, "add", "add")
+ button2 = NIconToggleButton(dialog, size, "bullet", "action")
+
+ assert button1.iconSize() == size
+ assert button2.iconSize() == size
+
+ assert button1.icon().isNull() is False
+ assert button2.icon().isNull() is False
+
+ dialog.addWidget(button1)
+ dialog.addWidget(button2)
+ dialog.show()
diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py
index 13589957..4a3c97d9 100644
--- a/tests/test_gui/test_gui_doceditor.py
+++ b/tests/test_gui/test_gui_doceditor.py
@@ -27,8 +27,8 @@ import pytest
from PyQt6.QtCore import QEvent, QMimeData, QPointF, Qt, QThreadPool, QUrl
from PyQt6.QtGui import (
QAction, QClipboard, QDesktopServices, QDragEnterEvent, QDragMoveEvent,
- QDropEvent, QFont, QMouseEvent, QTextBlock, QTextCursor, QTextDocument,
- QTextOption
+ QDropEvent, QFont, QInputMethodEvent, QMouseEvent, QTextBlock, QTextCursor,
+ QTextDocument, QTextOption
)
from PyQt6.QtWidgets import QApplication, QMenu, QPlainTextEdit
@@ -42,7 +42,8 @@ from novelwriter.gui.dochighlight import TextBlockData
from novelwriter.text.counting import standardCounter
from novelwriter.types import (
QtAlignJustify, QtAlignLeft, QtKeepAnchor, QtModCtrl, QtModNone,
- QtMouseLeft, QtMoveAnchor, QtMoveRight, QtScrollAlwaysOff, QtScrollAsNeeded
+ QtMouseLeft, QtMoveAnchor, QtMoveRight, QtScrollAlwaysOff,
+ QtScrollAsNeeded, QtSelectDocument, QtSelectWord
)
from tests.mocked import causeOSError
@@ -58,7 +59,7 @@ def getMenuForPos(editor: GuiDocEditor, pos: int, select: bool = False) -> QMenu
cursor = editor.textCursor()
cursor.setPosition(pos)
if select:
- cursor.select(QTextCursor.SelectionType.WordUnderCursor)
+ cursor.select(QtSelectWord)
editor.setTextCursor(cursor)
editor._openContextFromCursor()
for obj in editor.children():
@@ -1239,7 +1240,7 @@ def testGuiEditor_TextManipulation(qtbot, nwGUI, projPath, ipsumText, mockRnd):
docEditor.setCursorPosition(45)
assert len(docEditor._selectedBlocks(cursor)) == 0
- cursor.select(QTextCursor.SelectionType.Document)
+ cursor.select(QtSelectDocument)
assert len(docEditor._selectedBlocks(cursor)) == 15
# Remove All
@@ -1953,6 +1954,19 @@ def testGuiEditor_Completer(qtbot, nwGUI, projPath, mockRnd):
"%Note.Consistency: \n"
)
+ # CJK completer reposition (#2267 and #2517)
+ qtbot.keyClick(docEditor, "%", delay=KEY_DELAY)
+ assert completer.isVisible() is True
+ completer.move(0, 0)
+ assert completer.pos().x() == 0 # Completer menu at 0
+ assert completer.pos().y() == 0 # Completer menu at 0
+
+ event = QInputMethodEvent()
+ event.setCommitString("Text")
+ docEditor.inputMethodEvent(event)
+ assert completer.pos().x() > 0 # Completer should have moved
+ assert completer.pos().y() > 0 # Completer should have moved
+
# qtbot.stop()
@@ -2080,7 +2094,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
# Select the Word "est"
docEditor.setCursorPosition(663)
- docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
+ docEditor._makeSelection(QtSelectWord)
cursor = docEditor.textCursor()
assert cursor.selectedText() == "est"
@@ -2210,7 +2224,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
# Close search and select "est" again
docSearch.cancelSearch.activate(QAction.ActionEvent.Trigger)
docEditor.setCursorPosition(663)
- docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
+ docEditor._makeSelection(QtSelectWord)
cursor = docEditor.textCursor()
assert cursor.selectedText() == "est"
diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py
index ac16173b..06d97a80 100644
--- a/tests/test_gui/test_gui_docviewer.py
+++ b/tests/test_gui/test_gui_docviewer.py
@@ -27,7 +27,7 @@ import pytest
from PyQt6.QtCore import QEvent, QMimeData, QPointF, Qt, QUrl
from PyQt6.QtGui import (
QAction, QDesktopServices, QDragEnterEvent, QDragMoveEvent, QDropEvent,
- QMouseEvent, QTextCursor
+ QMouseEvent
)
from PyQt6.QtWidgets import QApplication, QMenu, QTextBrowser
@@ -35,7 +35,7 @@ from novelwriter import CONFIG, SHARED
from novelwriter.common import decodeMimeHandles
from novelwriter.enum import nwChange, nwDocAction
from novelwriter.formats.toqdoc import ToQTextDocument
-from novelwriter.types import QtModNone, QtMouseLeft, QtMouseMiddle
+from novelwriter.types import QtModNone, QtMouseLeft, QtMouseMiddle, QtSelectBlock, QtSelectWord
from tests.mocked import causeException
from tests.tools import C, buildTestProject
@@ -89,7 +89,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
cursor = docViewer.textCursor()
cursor.setPosition(100)
docViewer.setTextCursor(cursor)
- docViewer._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
+ docViewer._makeSelection(QtSelectWord)
clipboard = QApplication.clipboard()
assert clipboard is not None
@@ -117,9 +117,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
cursor.clearSelection()
docViewer.setTextCursor(cursor)
- docViewer._makePosSelection(
- QTextCursor.SelectionType.BlockUnderCursor, docViewer.cursorRect().center()
- )
+ docViewer._makePosSelection(QtSelectBlock, docViewer.cursorRect().center())
cursor = docViewer.textCursor()
assert cursor.selectedText() == (
"Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, "
@@ -159,7 +157,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
cursor = docViewer.textCursor()
cursor.setPosition(27)
docViewer.setTextCursor(cursor)
- docViewer._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
+ docViewer._makeSelection(QtSelectWord)
with monkeypatch.context() as mp:
mp.setattr(QMenu, "exec", mockExec)
docViewer._openContextMenu(docViewer.cursorRect().center())
@@ -169,7 +167,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
cursor = docViewer.textCursor()
cursor.setPosition(27)
docViewer.setTextCursor(cursor)
- docViewer._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
+ docViewer._makeSelection(QtSelectWord)
rect = docViewer.cursorRect()
docViewer._linkClicked(QUrl("#tag_bod"))
assert docViewer.docHandle == "4c4f28287af27"
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index 258d5e0a..4cfa3a91 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -30,9 +30,10 @@ import pytest
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPalette
-from PyQt6.QtWidgets import QInputDialog, QMessageBox
+from PyQt6.QtWidgets import QInputDialog
-from novelwriter import CONFIG, SHARED
+from novelwriter import CONFIG, SHARED, __hexversion__
+from novelwriter.common import jsonEncode
from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT
from novelwriter.constants import nwFiles
from novelwriter.dialogs.editlabel import GuiEditLabel
@@ -41,6 +42,7 @@ from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.gui.noveltree import GuiNovelView
from novelwriter.gui.outline import GuiOutlineView
from novelwriter.gui.projtree import GuiProjectTree
+from novelwriter.shared import _GuiAlert
from novelwriter.tools.welcome import GuiWelcome
from novelwriter.types import QtModCtrl, QtModShift
@@ -103,7 +105,7 @@ def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, projPath):
# Check that closes can be blocked
with monkeypatch.context() as mp:
- mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
+ mp.setattr(_GuiAlert, "finalState", False)
assert nwGUI.openProject(projPath) is True
assert nwGUI.closeMain() is False
nwGUI.closeProject()
@@ -713,7 +715,7 @@ def testGuiMain_Features(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
cHandle = SHARED.project.newFile("Jane", C.hCharRoot)
newDoc = SHARED.project.storage.getDocument(cHandle)
newDoc.writeDocument("# Jane\n\n@tag: Jane\n\n")
- nwGUI.rebuildIndex(beQuiet=True)
+ nwGUI.rebuildIndex()
assert SHARED.focusMode is False
@@ -825,11 +827,11 @@ def testGuiMain_OpenClose(qtbot, monkeypatch, nwGUI, projPath, fncPath, mockRnd)
nwGUI.viewDocument(C.hTitlePage)
# Handle broken index on project open
+ idxData = jsonEncode({"novelWriter.meta": {"version": __hexversion__}})
nwGUI.closeProject()
idxPath: Path = projPath / "meta" / nwFiles.INDEX_FILE
assert idxPath.read_text(encoding="utf-8") != "{}"
- idxPath.write_text("{}", encoding="utf-8")
- assert idxPath.read_text(encoding="utf-8") == "{}"
+ idxPath.write_text(idxData, encoding="utf-8")
nwGUI.openProject(projPath)
nwGUI.saveProject()
@@ -840,7 +842,7 @@ def testGuiMain_OpenClose(qtbot, monkeypatch, nwGUI, projPath, fncPath, mockRnd)
# Block closing
assert SHARED.hasProject is True
with monkeypatch.context() as mp:
- mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
+ mp.setattr(_GuiAlert, "finalState", False)
assert nwGUI.openProject(projPath) is False
assert SHARED.hasProject is True
@@ -853,7 +855,7 @@ def testGuiMain_OpenClose(qtbot, monkeypatch, nwGUI, projPath, fncPath, mockRnd)
shutil.copyfile(lockBack, lockPath)
with monkeypatch.context() as mp:
- mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
+ mp.setattr(_GuiAlert, "finalState", False)
assert nwGUI.openProject(projPath) is False
assert nwGUI.openProject(projPath) is True
diff --git a/tests/test_gui/test_gui_i18n.py b/tests/test_gui/test_gui_i18n.py
index 915d83a4..e482d85f 100644
--- a/tests/test_gui/test_gui_i18n.py
+++ b/tests/test_gui/test_gui_i18n.py
@@ -24,7 +24,7 @@ import sys
import pytest
-from PyQt6.QtWidgets import QApplication, QDialog, QMessageBox
+from PyQt6.QtWidgets import QApplication, QDialog
from novelwriter import CONFIG, SHARED
from novelwriter.dialogs.about import GuiAbout
@@ -49,8 +49,6 @@ LANG_DATA = CONFIG.listLanguages(CONFIG.LANG_NW)
def testGuiI18n_Localisation(qtbot, monkeypatch, language, nwGUI, projPath):
"""Test loading the gui with a specific language."""
monkeypatch.setattr(QDialog, "exec", lambda *a: None)
- monkeypatch.setattr(QMessageBox, "exec", lambda *a: None)
- monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes)
# Set the test language
CONFIG.guiLocale = language
diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py
index 9c327cda..e599e9f9 100644
--- a/tests/test_gui/test_gui_mainmenu.py
+++ b/tests/test_gui/test_gui_mainmenu.py
@@ -24,14 +24,15 @@ from unittest.mock import MagicMock
import pytest
-from PyQt6.QtGui import QAction, QDesktopServices, QTextBlock, QTextCursor
-from PyQt6.QtWidgets import QFileDialog, QMessageBox
+from PyQt6.QtGui import QAction, QDesktopServices, QTextBlock
+from PyQt6.QtWidgets import QFileDialog
from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwKeyWords, nwShortcode, nwStats, nwUnicode
from novelwriter.enum import nwDocAction, nwDocInsert
from novelwriter.gui.doceditor import GuiDocEditor
-from novelwriter.types import QtKeepAnchor, QtMoveRight
+from novelwriter.shared import _GuiAlert
+from novelwriter.types import QtKeepAnchor, QtMoveRight, QtSelectWord
from tests.tools import C, buildTestProject, writeFile
@@ -188,7 +189,7 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
# Cut, Copy and Paste
docEditor.setCursorPosition(x)
- docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
+ docEditor._makeSelection(QtSelectWord)
mainMenu.aEditCut.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[x:x+50] == (
@@ -201,7 +202,7 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
)
docEditor.setCursorPosition(x)
- docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
+ docEditor._makeSelection(QtSelectWord)
mainMenu.aEditCopy.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[x:x+50] == (
@@ -575,7 +576,7 @@ def testGuiMainMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd
# The document isn't empty, so the message box should pop
with monkeypatch.context() as mp:
- mp.setattr(QMessageBox, "result", lambda *a, **k: QMessageBox.StandardButton.No)
+ mp.setattr(_GuiAlert, "finalState", False)
assert not nwGUI.importDocument()
assert docEditor.getText() == "Bar"
diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py
index f6430f55..bdd50ad0 100644
--- a/tests/test_gui/test_gui_projtree.py
+++ b/tests/test_gui/test_gui_projtree.py
@@ -26,7 +26,7 @@ import pytest
from PyQt6.QtCore import QEvent, QItemSelectionModel, QModelIndex, QPointF
from PyQt6.QtGui import QMouseEvent
-from PyQt6.QtWidgets import QMenu, QMessageBox
+from PyQt6.QtWidgets import QMenu
from novelwriter import CONFIG, SHARED
from novelwriter.dialogs.docmerge import GuiDocMerge
@@ -34,6 +34,7 @@ from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType
from novelwriter.gui.projtree import _TreeContextMenu
+from novelwriter.shared import _GuiAlert
from novelwriter.types import (
QtAccepted, QtModNone, QtMouseLeft, QtMouseMiddle, QtRejected,
QtScrollAlwaysOff, QtScrollAsNeeded
@@ -627,7 +628,7 @@ def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, m
# User can cancel move to trash
with monkeypatch.context() as mp:
- mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
+ mp.setattr(_GuiAlert, "finalState", False)
projTree.processDeleteRequest(hScenes, askFirst=True)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
@@ -645,7 +646,7 @@ def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, m
# User can block permanent deletion
with monkeypatch.context() as mp:
- mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
+ mp.setattr(_GuiAlert, "finalState", False)
projTree.processDeleteRequest(hScenes[0:2], askFirst=True)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
@@ -677,7 +678,7 @@ def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, m
# Trash can be completely emptied, but user can block it
with monkeypatch.context() as mp:
- mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
+ mp.setattr(_GuiAlert, "finalState", False)
projTree.emptyTrash()
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
@@ -995,7 +996,7 @@ def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# Duplicate title page, but select no
with monkeypatch.context() as mp:
- mp.setattr(QMessageBox, "result", lambda *a: QtRejected)
+ mp.setattr(_GuiAlert, "finalState", False)
projTree.duplicateFromHandle(C.hTitlePage)
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
@@ -1302,7 +1303,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# Click no on the dialog
with monkeypatch.context() as mp:
- mp.setattr(QMessageBox, "result", lambda *a: QtRejected)
+ mp.setattr(_GuiAlert, "finalState", False)
ctxMenu._convertFolderToFile(nwItemLayout.DOCUMENT)
assert nodeOne.item.isFolderType()
diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py
index 19c3f277..8bbb6fbe 100644
--- a/tests/test_gui/test_gui_statusbar.py
+++ b/tests/test_gui/test_gui_statusbar.py
@@ -36,7 +36,7 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
cHandle = SHARED.project.newFile("A Note", C.hCharRoot)
newDoc = SHARED.project.storage.getDocument(cHandle)
newDoc.writeDocument("# A Note\n\n")
- nwGUI.rebuildIndex(beQuiet=True)
+ nwGUI.rebuildIndex()
status = nwGUI.mainStatus
diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py
index aa613bd5..cd25dfe9 100644
--- a/tests/test_gui/test_gui_theme.py
+++ b/tests/test_gui/test_gui_theme.py
@@ -421,36 +421,36 @@ def testGuiTheme_LoadIcons():
# ==========
# Load an unknown icon
- qIcon = iconCache.getIcon("stuff")
+ qIcon = iconCache.getIcon("stuff", "tool")
assert isinstance(qIcon, QIcon)
assert qIcon == iconCache._noIcon
# Load an icon, it is likely already cached
- qIcon = iconCache.getIcon("add")
+ qIcon = iconCache.getIcon("add", "tool")
assert isinstance(qIcon, QIcon)
assert qIcon.isNull() is False
# Load it as a pixmap with a size
# If this part of the test fails, you may need to set the
# environment variable: QT_SCALE_FACTOR=1
- qPix = iconCache.getPixmap("add", (50, 50))
+ qPix = iconCache.getPixmap("add", (50, 50), "tool")
assert isinstance(qPix, QPixmap)
assert qPix.isNull() is False
assert qPix.width() == 50, "If this fails, make sure QT_SCALE_FACTOR=1"
assert qPix.height() == 50, "If this fails, make sure QT_SCALE_FACTOR=1"
# Load app icon
- qIcon = iconCache.getIcon("novelwriter")
+ qIcon = iconCache.getIcon("novelwriter", "tool")
assert isinstance(qIcon, QIcon)
assert qIcon != iconCache._noIcon
# Load mime icon
- qIcon = iconCache.getIcon("proj_nwx")
+ qIcon = iconCache.getIcon("proj_nwx", "tool")
assert isinstance(qIcon, QIcon)
assert qIcon != iconCache._noIcon
# Toggle icon
- qIcon = iconCache.getToggleIcon("bullet", (24, 24))
+ qIcon = iconCache.getToggleIcon("bullet", (24, 24), "tool")
assert isinstance(qIcon, QIcon)
assert qIcon != iconCache._noIcon
pOn = qIcon.pixmap(24, 24, QIcon.Mode.Normal, QIcon.State.On)
@@ -458,7 +458,7 @@ def testGuiTheme_LoadIcons():
assert pOn != pOff
# Unknown toggle icon
- qIcon = iconCache.getToggleIcon("stuff", (24, 24))
+ qIcon = iconCache.getToggleIcon("stuff", (24, 24), "tool")
assert isinstance(qIcon, QIcon)
assert qIcon == iconCache._noIcon
@@ -610,7 +610,7 @@ def testGuiTheme_CheckTheme(theme):
parser = ConfigParser()
parser.read(current.path, encoding="utf-8")
- sections = ["Main", "Base", "Project", "Palette", "GUI", "Syntax"]
+ sections = ["Main", "Base", "Project", "Icon", "Palette", "GUI", "Syntax"]
assert sorted(parser.sections()) == sorted(sections)
structure = {
@@ -625,6 +625,11 @@ def testGuiTheme_CheckTheme(theme):
"root", "folder", "file", "title", "chapter", "scene", "note",
"active", "inactive", "disabled",
],
+ "Icon": [
+ "tool", "sidebar", "accept", "reject", "action", "altaction",
+ "apply", "create", "destroy", "reset", "add", "change", "remove",
+ "shortcode", "markdown", "systemio", "info", "warning", "error",
+ ],
"Palette": [
"window", "windowtext", "base", "alternatebase", "text",
"tooltipbase", "tooltiptext", "button", "buttontext", "brighttext",
@@ -635,9 +640,10 @@ def testGuiTheme_CheckTheme(theme):
],
"Syntax": [
"background", "text", "line", "link", "headertext", "headertag",
- "emphasis", "dialog", "altdialog", "hidden", "note", "shortcode",
- "keyword", "tag", "value", "optional", "spellcheckline",
- "errorline", "replacetag", "modifier", "texthighlight",
+ "emphasis", "whitespace", "dialog", "altdialog", "hidden", "note",
+ "shortcode", "keyword", "tag", "value", "optional",
+ "spellcheckline", "errorline", "replacetag", "modifier",
+ "texthighlight",
],
}
optional = ["credit", "url"]
diff --git a/tests/test_tools/test_tools_manusbuild.py b/tests/test_tools/test_tools_manusbuild.py
index 91924120..39875483 100644
--- a/tests/test_tools/test_tools_manusbuild.py
+++ b/tests/test_tools/test_tools_manusbuild.py
@@ -26,15 +26,15 @@ import pytest
from PyQt6.QtCore import QUrl
from PyQt6.QtGui import QDesktopServices
-from PyQt6.QtWidgets import QFileDialog, QListWidgetItem, QMessageBox
+from PyQt6.QtWidgets import QFileDialog, QListWidgetItem
from pytestqt.qtbot import QtBot
from novelwriter.constants import nwLabels
from novelwriter.core.buildsettings import BuildSettings
from novelwriter.enum import nwBuildFmt
from novelwriter.guimain import GuiMain
+from novelwriter.shared import _GuiAlert
from novelwriter.tools.manusbuild import GuiManuscriptBuild
-from novelwriter.types import QtDialogClose
from tests.tools import buildTestProject
@@ -94,9 +94,7 @@ def testToolManuscriptBuild_Main(
assert (fncPath / "TestBuild").with_suffix(nwLabels.BUILD_EXT[fmt]).exists()
lastFmt = fmt
- button = manus.buttonBox.button(QtDialogClose)
- assert button is not None
- manus._dialogButtonClicked(button)
+ manus._dialogButtonClicked(manus.btnClose)
manus.deleteLater()
assert build.lastBuildName == "TestBuild"
@@ -134,7 +132,7 @@ def testToolManuscriptBuild_Main(
manus.buildPath.setText(str(fncPath))
manus.buildName.setText("TestBuild")
with monkeypatch.context() as mp:
- mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
+ mp.setattr(_GuiAlert, "finalState", False)
assert manus._runBuild() is False
# Test that the open button works
@@ -150,7 +148,5 @@ def testToolManuscriptBuild_Main(
assert lastUrl.startswith("file://")
# Finish
- button = manus.buttonBox.button(QtDialogClose)
- assert button is not None
- manus._dialogButtonClicked(button)
+ manus._dialogButtonClicked(manus.btnClose)
# qtbot.stop()
diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py
index 0e498ea1..3d8b86cb 100644
--- a/tests/test_tools/test_tools_manuscript.py
+++ b/tests/test_tools/test_tools_manuscript.py
@@ -37,7 +37,6 @@ from novelwriter.core.buildsettings import BuildSettings
from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manuscript import GuiManuscript
from novelwriter.tools.manussettings import GuiBuildSettings
-from novelwriter.types import QtDialogApply, QtDialogSave
from tests.tools import C, buildTestProject
@@ -73,6 +72,9 @@ def testToolManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
manus.btnPreview.click()
assert manus.docPreview.toPlainText().strip() == allText
+ # Trigger a theme update, which is only a visual refresh, but it shouldn't crash
+ manus.updateTheme()
+
nwGUI.closeProject() # This should auto-close the manuscript tool
# qtbot.stop()
@@ -115,9 +117,7 @@ def testToolManuscript_Builds(qtbot, nwGUI, projPath):
with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000):
bSettings.newSettingsReady.connect(_testNewSettingsReady)
- button = bSettings.buttonBox.button(QtDialogSave)
- assert button is not None
- button.click()
+ bSettings.btnSave.click()
assert isinstance(build, BuildSettings)
assert build.name == "Test Build"
@@ -136,9 +136,7 @@ def testToolManuscript_Builds(qtbot, nwGUI, projPath):
with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000):
bSettings.newSettingsReady.connect(_testNewSettingsReady)
- button = bSettings.buttonBox.button(QtDialogApply)
- assert button is not None
- button.click() # Should leave the dialog open
+ bSettings.btnApply.click() # Should leave the dialog open
assert isinstance(build, BuildSettings)
assert build.name == "Test Build"
@@ -153,6 +151,9 @@ def testToolManuscript_Builds(qtbot, nwGUI, projPath):
assert new is not None
assert new.name == "Test Build 2"
+ # Trigger a theme update, which should propagate to settings
+ nwGUI.refreshThemeColors()
+
# Close the dialog should also close the child dialogs
manus.btnClose.click()
if isinstance(bSettings, GuiBuildSettings):
diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py
index b5ce7880..853fe6cb 100644
--- a/tests/test_tools/test_tools_manussettings.py
+++ b/tests/test_tools/test_tools_manussettings.py
@@ -33,7 +33,6 @@ from novelwriter.core.buildsettings import BuildSettings, FilterMode
from novelwriter.tools.manussettings import (
GuiBuildSettings, _FilterTab, _FormattingTab, _HeadingsTab
)
-from novelwriter.types import QtDialogApply, QtDialogClose, QtDialogSave
from tests.tools import C, buildTestProject
@@ -78,9 +77,7 @@ def testToolBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
# Capture Apply button
with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000):
bSettings.newSettingsReady.connect(_testNewSettingsReady)
- button = bSettings.buttonBox.button(QtDialogApply)
- assert button is not None
- bSettings._dialogButtonClicked(button)
+ bSettings._dialogButtonClicked(bSettings.btnApply)
assert triggered
@@ -89,9 +86,7 @@ def testToolBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000):
bSettings.newSettingsReady.connect(_testNewSettingsReady)
- button = bSettings.buttonBox.button(QtDialogSave)
- assert button is not None
- bSettings._dialogButtonClicked(button)
+ bSettings._dialogButtonClicked(bSettings.btnSave)
assert triggered
@@ -109,9 +104,7 @@ def testToolBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
assert triggered
# Finish
- button = bSettings.buttonBox.button(QtDialogClose)
- assert button is not None
- bSettings._dialogButtonClicked(button)
+ bSettings._dialogButtonClicked(bSettings.btnClose)
# qtbot.stop()
@@ -326,9 +319,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
]
# Finish
- button = bSettings.buttonBox.button(QtDialogClose)
- assert button is not None
- bSettings._dialogButtonClicked(button)
+ bSettings._dialogButtonClicked(bSettings.btnClose)
# qtbot.stop()
@@ -505,9 +496,7 @@ def testToolBuildSettings_Headings(qtbot, nwGUI):
assert sBuild.getBool("headings.hideSection") is True
# Finish
- button = bSettings.buttonBox.button(QtDialogClose)
- assert button is not None
- bSettings._dialogButtonClicked(button)
+ bSettings._dialogButtonClicked(bSettings.btnClose)
# qtbot.stop()
@@ -579,9 +568,7 @@ def testToolBuildSettings_FormatTextContent(qtbot, nwGUI):
assert sBuild.getBool("text.addNoteHeadings") is True
# Finish
- button = bSettings.buttonBox.button(QtDialogClose)
- assert button is not None
- bSettings._dialogButtonClicked(button)
+ bSettings._dialogButtonClicked(bSettings.btnClose)
# qtbot.stop()
@@ -657,9 +644,7 @@ def testToolBuildSettings_FormatTextFormat(monkeypatch, qtbot, nwGUI):
assert fmtTab._textFont == font
# Finish
- button = bSettings.buttonBox.button(QtDialogClose)
- assert button is not None
- bSettings._dialogButtonClicked(button)
+ bSettings._dialogButtonClicked(bSettings.btnClose)
# qtbot.stop()
@@ -703,9 +688,7 @@ def testToolBuildSettings_FormatFirstLineIndent(monkeypatch, qtbot, nwGUI):
assert sBuild.getBool("format.indentFirstPar") is True
# Finish
- button = bSettings.buttonBox.button(QtDialogClose)
- assert button is not None
- bSettings._dialogButtonClicked(button)
+ bSettings._dialogButtonClicked(bSettings.btnClose)
# qtbot.stop()
@@ -761,9 +744,7 @@ def testToolBuildSettings_FormatPageLayout(monkeypatch, qtbot, nwGUI):
assert fmtTab.rightMargin.value() == 1.5
# Finish
- button = bSettings.buttonBox.button(QtDialogClose)
- assert button is not None
- bSettings._dialogButtonClicked(button)
+ bSettings._dialogButtonClicked(bSettings.btnClose)
# qtbot.stop()
@@ -832,7 +813,5 @@ def testToolBuildSettings_FormatOutput(qtbot, nwGUI):
assert fmtTab.odtPageHeader.text() == nwHeadFmt.DOC_AUTO
# Finish
- button = bSettings.buttonBox.button(QtDialogClose)
- assert button is not None
- bSettings._dialogButtonClicked(button)
+ bSettings._dialogButtonClicked(bSettings.btnClose)
# qtbot.stop()
diff --git a/tests/tools.py b/tests/tools.py
index 9eec20d0..bca68899 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -66,7 +66,7 @@ class C:
def cmpFiles(
fileOne: str | Path,
fileTwo: str | Path,
- ignoreLines: list | None = None,
+ ignoreLines: list[int] | None = None,
ignoreStart: tuple | None = None
) -> bool:
"""Compare two files, with optional line ignore."""
diff --git a/utils/assets.py b/utils/assets.py
index 7f0222cd..5822cc96 100644
--- a/utils/assets.py
+++ b/utils/assets.py
@@ -111,7 +111,6 @@ def updateTranslationSources(args: argparse.Namespace) -> None:
print("")
sources = list((ROOT_DIR / "novelwriter").glob("**/*.py"))
- sources.insert(0, ROOT_DIR / "i18n" / "qtbase.py")
for source in sources:
print(source.relative_to(ROOT_DIR))
diff --git a/utils/build_debian.py b/utils/build_debian.py
index 6feac9de..892ee718 100644
--- a/utils/build_debian.py
+++ b/utils/build_debian.py
@@ -36,7 +36,7 @@ SIGN_KEY = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08"
def makeDebianPackage(
signKey: str | None = None, sourceBuild: bool = False, distName: str = "unstable",
- buildName: str = "", forLaunchpad: bool = False
+ buildName: str = "", forLaunchpad: bool = False, oldLicense: bool = False,
) -> str:
"""Build a Debian package."""
print("")
@@ -96,7 +96,7 @@ def makeDebianPackage(
print("Copying or generating additional files ...")
print("")
- copyPackageFiles(outDir, setupPy=True)
+ copyPackageFiles(outDir, oldLicense=oldLicense)
# Copy/Write Debian Files
# =======================
@@ -180,14 +180,15 @@ def launchpad(args: argparse.Namespace) -> None:
bldNum = "0"
distLoop = [
- ("24.04", "noble"),
- ("25.04", "plucky"),
- ("25.10", "questing"),
+ ("24.04", "noble", True),
+ ("25.04", "plucky", True),
+ ("25.10", "questing", False),
+ ("26.04", "resolute", False),
]
print("Building Ubuntu packages for:")
print("")
- for distNum, codeName in distLoop:
+ for distNum, codeName, _ in distLoop:
print(f" * Ubuntu {distNum} {codeName.title()}")
print("")
@@ -197,7 +198,7 @@ def launchpad(args: argparse.Namespace) -> None:
print("")
dputCmd = []
- for distNum, codeName in distLoop:
+ for distNum, codeName, oldLicense in distLoop:
buildName = f"ubuntu{distNum}.{bldNum}"
dCmd = makeDebianPackage(
signKey=signKey,
@@ -205,6 +206,7 @@ def launchpad(args: argparse.Namespace) -> None:
distName=codeName,
buildName=buildName,
forLaunchpad=True,
+ oldLicense=oldLicense,
)
dputCmd.append(dCmd)
diff --git a/utils/build_windows.py b/utils/build_windows.py
index 523797da..debae0ef 100644
--- a/utils/build_windows.py
+++ b/utils/build_windows.py
@@ -30,7 +30,7 @@ import zipfile
from pathlib import Path
from utils.common import (
- ROOT_DIR, SETUP_DIR, copySourceCode, extractVersion, readFile,
+ ROOT_DIR, SETUP_DIR, copySourceCode, extractReqs, extractVersion, readFile,
removeRedundantQt, systemCall, writeFile
)
@@ -45,7 +45,6 @@ def prepareCode(outDir: Path) -> None:
files = [
ROOT_DIR / "CREDITS.md",
ROOT_DIR / "LICENSE.md",
- ROOT_DIR / "requirements.txt",
SETUP_DIR / "iss_license.txt",
SETUP_DIR / "windows" / "novelWriter.ico",
SETUP_DIR / "windows" / "novelWriter.exe",
@@ -85,7 +84,7 @@ def installRequirements(libDir: Path) -> None:
"""Install dependencies."""
print("Install dependencies ...")
systemCall([
- sys.executable, "-m", "pip", "install", "-r", "requirements.txt", "--target", libDir
+ sys.executable, "-m", "pip", "install", *extractReqs(["app"]), "--target", libDir
])
print("Done")
print("")
diff --git a/utils/common.py b/utils/common.py
index d8590225..f5637285 100644
--- a/utils/common.py
+++ b/utils/common.py
@@ -23,6 +23,7 @@ from __future__ import annotations
import shutil
import subprocess
import sys
+import tomllib
from pathlib import Path
@@ -30,6 +31,18 @@ ROOT_DIR = Path(__file__).parent.parent
SETUP_DIR = ROOT_DIR / "setup"
+def extractReqs(groups: list[str]) -> list[str]:
+ """Extract dependency groups from pyproject.toml."""
+ data = tomllib.loads((ROOT_DIR / "pyproject.toml").read_text(encoding="utf-8"))
+ reqs = []
+ if "app" in groups or "all" in groups:
+ reqs += data["project"]["dependencies"]
+ for group in data["dependency-groups"]:
+ if group in groups or "all" in groups:
+ reqs += [d for d in data["dependency-groups"][group] if isinstance(d, str)]
+ return reqs
+
+
def extractVersion(beQuiet: bool = False) -> tuple[str, str, str]:
"""Extract the novelWriter version number without having to import
anything else from the main package.
@@ -90,27 +103,36 @@ def copySourceCode(dst: Path) -> None:
print("Copied:", relSrc, flush=True)
-def copyPackageFiles(dst: Path, setupPy: bool = False) -> None:
+def copyPackageFiles(dst: Path, oldLicense: bool = False) -> None:
"""Copy files needed for packaging."""
- copyFiles = ["LICENSE.md", "CREDITS.md", "pyproject.toml"]
+ copyFiles = [
+ ROOT_DIR / "LICENSE.md",
+ SETUP_DIR / "LICENSE-Apache-2.0.txt",
+ ROOT_DIR / "CREDITS.md",
+ ROOT_DIR / "pyproject.toml",
+ ]
for copyFile in copyFiles:
- shutil.copyfile(copyFile, dst / copyFile)
+ shutil.copyfile(copyFile, dst / copyFile.name)
print("Copied:", copyFile, flush=True)
writeFile(dst / "MANIFEST.in", (
"include LICENSE.md\n"
+ "include LICENSE-Apache-2.0.txt\n"
"include CREDITS.md\n"
"recursive-include novelwriter/assets *\n"
))
- if setupPy:
- writeFile(dst / "setup.py", (
- "import setuptools\n"
- "setuptools.setup()\n"
- ))
-
text = readFile(ROOT_DIR / "pyproject.toml")
text = text.replace("setup/description_pypi.md", "data/description_short.txt")
+ if oldLicense:
+ new = []
+ for line in text.splitlines():
+ if line.startswith("license = "):
+ line = 'license = {text = "GPL-3.0-or-later AND Apache-2.0 AND CC-BY-4.0"}'
+ if line.startswith("license-files = "):
+ continue
+ new.append(line)
+ text = "\n".join(new)
writeFile(dst / "pyproject.toml", text)
diff --git a/utils/icon_themes.py b/utils/icon_themes.py
index cb906b01..cd133dbc 100644
--- a/utils/icon_themes.py
+++ b/utils/icon_themes.py
@@ -107,6 +107,24 @@ ICONS = [
"theme_dark",
"theme_auto",
+ "btn_ok",
+ "btn_cancel",
+ "btn_yes",
+ "btn_no",
+ "btn_open",
+ "btn_close",
+ "btn_save",
+ "btn_browse",
+ "btn_list",
+ "btn_new",
+ "btn_create",
+ "btn_reset",
+ "btn_insert",
+ "btn_apply",
+ "btn_build",
+ "btn_print",
+ "btn_preview",
+
"add",
"bookmarks",
"browse",
@@ -142,7 +160,6 @@ ICONS = [
"more_arrow",
"more_vertical",
"noncheckable",
- "open",
"panel",
"pin",
"project_copy",
@@ -151,7 +168,6 @@ ICONS = [
"remove",
"revert",
"settings",
- "star",
"stats",
"text",
"timer_off",
@@ -288,6 +304,8 @@ def processFontAwesome(workDir: Path, iconsDir: Path, jobs: dict) -> None:
viewbox = [int(x) for x in svg.get("viewBox", "").split()]
viewbox = [viewbox[2]//2 - 256, 0, 512, 512]
svg.set("viewBox", " ".join(str(x) for x in viewbox))
+ for elem in svg.iter():
+ elem.attrib.pop("fill", None)
icons[key] = svg
else:
print(f"Not Found: {icon}.svg")
diff --git a/utils/icon_themes/font_awesome.json b/utils/icon_themes/font_awesome.json
index d98dc9e2..82f295fc 100644
--- a/utils/icon_themes/font_awesome.json
+++ b/utils/icon_themes/font_awesome.json
@@ -60,6 +60,24 @@
"theme_dark": "moon",
"theme_auto": "circle-half-stroke",
+ "btn_ok": "circle-check",
+ "btn_cancel": "ban",
+ "btn_yes": "circle-check",
+ "btn_no": "circle-xmark",
+ "btn_open": "file-arrow-up",
+ "btn_close": "circle-xmark",
+ "btn_save": "floppy-disk",
+ "btn_browse": "folder-open",
+ "btn_list": "list",
+ "btn_new": "plus",
+ "btn_create": "star",
+ "btn_reset": "rotate-left",
+ "btn_insert": "i-cursor",
+ "btn_apply": "square-check",
+ "btn_build": "up-right-from-square",
+ "btn_print": "print",
+ "btn_preview": "eye",
+
"add": "plus",
"bookmarks": "bookmark",
"browse": "folder-open",
@@ -95,16 +113,14 @@
"more_arrow": "caret-right",
"more_vertical": "ellipsis-vertical",
"noncheckable": "square-minus",
- "open": "file-arrow-up",
"panel": "table-list",
"pin": "thumbtack",
"project_copy": "copy",
"quote": "quote-right",
- "refresh": "arrow-rotate-right",
+ "refresh": "rotate-right",
"remove": "minus",
- "revert": "arrow-rotate-left",
+ "revert": "rotate-left",
"settings": "gear",
- "star": "star",
"stats": "chart-line",
"text": "file-lines",
"timer_off": "pause",
diff --git a/utils/icon_themes/material_symbols.json b/utils/icon_themes/material_symbols.json
index 61c1975a..0727a21c 100644
--- a/utils/icon_themes/material_symbols.json
+++ b/utils/icon_themes/material_symbols.json
@@ -60,6 +60,24 @@
"theme_dark": "dark_mode",
"theme_auto": "contrast",
+ "btn_ok": "check_circle",
+ "btn_cancel": "cancel",
+ "btn_yes": "check_circle",
+ "btn_no": "do_not_disturb_on",
+ "btn_open": "open_in_new",
+ "btn_close": "cancel",
+ "btn_save": "file_save",
+ "btn_browse": "folder_open",
+ "btn_list": "format_list_bulleted",
+ "btn_new": "new_window",
+ "btn_create": "star",
+ "btn_reset": "undo",
+ "btn_insert": "insert_text",
+ "btn_apply": "check_box",
+ "btn_build": "export_notes",
+ "btn_print": "print",
+ "btn_preview": "preview",
+
"add": "add",
"bookmarks": "bookmarks",
"browse": "folder_open",
@@ -95,7 +113,6 @@
"more_arrow": "arrow_right",
"more_vertical": "more_vert",
"noncheckable": "indeterminate_check_box",
- "open": "open_in_new",
"panel": "dock_to_bottom",
"pin": "keep",
"project_copy": "folder_copy",
@@ -104,7 +121,6 @@
"remove": "remove",
"revert": "settings_backup_restore",
"settings": "settings",
- "star": "star",
"stats": "stacked_line_chart",
"text": "subject",
"timer_off": "timer_off",
diff --git a/utils/icon_themes/remix.json b/utils/icon_themes/remix.json
index 0ac173ec..9e4bd207 100644
--- a/utils/icon_themes/remix.json
+++ b/utils/icon_themes/remix.json
@@ -60,6 +60,24 @@
"theme_dark": "moon",
"theme_auto": "contrast",
+ "btn_ok": "checkbox-circle",
+ "btn_cancel": "indeterminate-circle",
+ "btn_yes": "checkbox-circle",
+ "btn_no": "close-circle",
+ "btn_open": "file-upload",
+ "btn_close": "close-circle",
+ "btn_save": "save-3",
+ "btn_browse": "folder-2",
+ "btn_list": "list-unordered",
+ "btn_new": "add",
+ "btn_create": "star-fill",
+ "btn_reset": "reset-left",
+ "btn_insert": "add-box",
+ "btn_apply": "checkbox",
+ "btn_build": "stack",
+ "btn_print": "printer",
+ "btn_preview": "eye",
+
"add": "add",
"bookmarks": "bookmark",
"browse": "folder-2",
@@ -95,7 +113,6 @@
"more_arrow": "arrow-right-s-fill",
"more_vertical": "more-2-fill",
"noncheckable": "checkbox-indeterminate",
- "open": "file-upload",
"panel": "layout-bottom",
"pin": "pushpin",
"project_copy": "file-copy-2",
@@ -104,7 +121,6 @@
"remove": "subtract",
"revert": "reset-left",
"settings": "settings-2",
- "star": "star-fill",
"stats": "line-chart",
"text": "file-text",
"timer_off": "zzz",