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",