diff --git a/CREDITS.md b/CREDITS.md index 9ac3b34a..d036a4a4 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -55,19 +55,60 @@ The following libraries are dependencies of novelWriter: * **Enchant** by Dom Lachowicz * **PyEnchant** by Dimitri Merejkowsky -## Assets +## Icon Themes -Some of the assets bundled with novelWriter were adapted from the following sources: +The default icon theme is a subset of icons from: -* **Material Symbols** icons by Google Inc (Apache 2.0) -* **Remix** icons by RemixIcon (Apache 2.0) -* **Font Awesome** icons by Fonticons Inc (CC BY 4.0) -* **Tomorrow** syntax themes by Chris Kempson (MIT License) -* **Owl** syntax themes by Sarah Drasner (MIT License) -* **Solarized** themes by Ethan Schoonover (MIT License) -* **Cyberpunk Night** theme by Anders Lemvigh (CC BY-SA 4.0) -* **Dracula** theme by Zeno Rocha (MIT License) -* **Snazzy Light** theme by Florian Reuschel (MIT License) +* **Material Symbols** by Google Inc (Apache 2.0) + +Additional icon themes may or may not be bundled with novelWriter, depending on license +constraints. These themes include a subset of icons from: + +* **Font Awesome** by Fonticons Inc (CC BY 4.0) +* **Remix** by RemixIcon (Apache 2.0 w/non-free clause) + +## Colour Themes + +Colour themes included with novelWriter were adapted from the following sources: + +* **Aura** themes by Dalton Menezes +* **B2T** themes by Bram de Haan +* **B4T** themes by Bram de Haan +* **Dracula** theme by Zeno Rocha +* **Espresso** theme by Sainnhe Park and Antoine Cotten +* **Everforest** themes by Sainnhe Park +* **Horizon** themes by Jonathan Olaleye +* **New Moon** theme by Tania Rascia +* **Noctis** themes by Liviu Schera and contributors +* **Nord** theme by Sven Greb +* **Owl** themes by Sarah Drasner +* **Primer** themes by GitHub Inc. and Myian +* **Snazzy Light** theme by Florian Reuschel +* **Solarized** themes by Ethan Schoonover +* **Tango** themes by the Tango Desktop Project +* **Tomorrow** themes by Chris Kempson + +Other themes were created for novelWriter: + +* **Aura Bright** theme by Myian +* **Blue Streak** themes by Veronica Berglyd Olsen +* **Castle** themes by Myian +* **Chalky Soil** theme by Myian +* **Chernozem** theme by Myian +* **Cyberpunk Night** theme by Anders Lemvigh +* **Floral** themes by Myian +* **Full Moon** theme by Myian +* **LCARS** theme by Veronica Berglyd Olsen +* **Nordlicht** theme by Myian +* **Otium** theme by Myian +* **Paragon** theme by Myian +* **Ruby** themes by Myian +* **Selenium** themes by Myian +* **Sepia** themes by Myian +* **Sultana** themes by Myian +* **Vivid** themes by Myian +* **Warpgate** theme by Myian +* **Waterlily** themes by Myian ## Fonts diff --git a/docs/source/conf.py b/docs/source/conf.py index b3d3d41f..bfedb52a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,7 @@ """ Configuration file for the Sphinx documentation builder. Documentation: http://www.sphinx-doc.org/en/master/config -""" +""" # noqa # -- Imports ----------------------------------------------------------------- diff --git a/docs/source/features/shortcuts.rst b/docs/source/features/shortcuts.rst index 0a29927b..e5811c85 100644 --- a/docs/source/features/shortcuts.rst +++ b/docs/source/features/shortcuts.rst @@ -101,6 +101,7 @@ Text Formatting Shortcuts ":kbd:`Ctrl+B`", "Format selected text, or word under cursor, with bold" ":kbd:`Ctrl+D`", "Format selected text, or word under cursor, with strike through" ":kbd:`Ctrl+I`", "Format selected text, or word under cursor, with italic" + ":kbd:`Ctrl+M`", "Highlight selected text, or word under cursor" ":kbd:`Ctrl+Shift+/`", "Remove format for block or selected text" ":kbd:`Ctrl+Shift+D`", "Toggle ignored text format for block or selected text" diff --git a/docs/source/index.rst b/docs/source/index.rst index 8e863ff8..793d1d38 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -75,6 +75,7 @@ storage for robustness. more/counting more/typography + more/dictionaries more/customise more/handling_errors more/project_format diff --git a/docs/source/more/customise.rst b/docs/source/more/customise.rst index 80aa1201..89a74ba0 100644 --- a/docs/source/more/customise.rst +++ b/docs/source/more/customise.rst @@ -1,94 +1,35 @@ .. _docs_more_custom: -************** -Customisations -************** +************* +Custom Themes +************* -.. _Enchant: https://rrthomas.github.io/enchant/ -.. _Free Desktop: https://cgit.freedesktop.org/libreoffice/dictionaries/tree/ - -There are a few ways you can customise novelWriter yourself. Currently, you can add new GUI themes, -your own syntax themes, and install additional dictionaries. - - -.. _docs_more_custom_dict: - -Spell Check Dictionaries -======================== - -novelWriter uses Enchant_ as the spell checking tool. Depending on your operating system, it may or -may not load all installed spell check dictionaries automatically. - - -Linux and MacOS ---------------- - -On Linux and MacOS, you generally only have to install hunspell, aspell or myspell dictionaries on -your system like you do for other applications. See your distro or OS documentation for how to do -this. These dictionaries should show up as available spell check languages in novelWriter. - - -Windows -------- - -For Windows, English is included with the installation. For other languages you have to download -and add dictionaries yourself. - -**Install Tool** - -A small tool to assist with this can be found under **Tools > Add Dictionaries**. It will import -spell checking dictionaries from Free Office or Libre Office extensions. The dictionaries are then -installed in the install location for the Enchant library and should thus work for any application -that uses Enchant for spell checking. - -**Manual Install** - -If you prefer to do this manually or want to use a different source than the ones mentioned above, -You need to get compatible dictionary files for your language. You need two files files ending with -``.aff`` and ``.dic``. These files must then be copied to the following location: - -``C:\Users\\AppData\Local\enchant\hunspell`` - -This assumes your user profile is stored at ``C:\Users\``. The last one or two folders may -not exist, so you may need to create them. - -You can find the various dictionaries on the `Free Desktop`_ website. - -.. note:: - The Free Desktop link points to a repository, and what may look like file links inside the - dictionary folder are actually links to web pages. If you right-click and download those, you - get HTML files, not dictionaries! - - In order to download the actual dictionary files, right-click the "plain" label at the end of - each line and download that. +There are a few ways you can customise novelWriter yourself. Currently, you can relatively easily +add new GUI themes. You can also add new icon themes, although this is not as straightforward. .. _docs_more_custom_theme: -Syntax and GUI Themes -===================== +Colour Themes +============= -Adding your own GUI and syntax themes is relatively easy, although it requires that you manually -edit config files with colour values. The themes are defined by simple plain text config files with -meta data and colour settings. +Adding your own colour themes is relatively easy, although it requires that you manually edit +config files with colour values. The themes are defined by simple plain text config files with meta +data and colour settings. In order to make your own versions, first copy one of the existing files to your local computer and modify it as you like. -* The existing syntax themes are stored in - `novelwriter/assets/syntax `_. -* The existing GUI themes are stored in - `novelwriter/assets/themes `_. -* The existing icon themes are stored in - `novelwriter/assets/icons `_. +The existing colour themes are stored in +`novelwriter/assets/themes `_. Remember to also change the name of your theme by modifying the ``name`` setting at the top of the file, otherwise you may not be able to distinguish them in **Preferences**. For novelWriter to be able to locate the custom theme files, you must copy them to the -:ref:`docs_technical_locations_data` location in your home or user area. There should be a folder there named -``syntax`` for syntax themes, just ``themes`` for GUI themes, and ``icons`` for icon themes. These -folders are created the first time you start novelWriter. +:ref:`docs_technical_locations_data` location in your home or user area. There should be a folder +there named ``themes`` for colour themes. These folders are created the first time you start +novelWriter. Once the files are copied there, they should show up in **Preferences** with the label you set as ``name`` inside the file. @@ -99,71 +40,129 @@ set as ``name`` inside the file. files up to date. -Custom GUI and Icons Theme --------------------------- +The Theme File Format +--------------------- -A GUI theme ``.conf`` file consists of the following settings: +A colour theme ``.conf`` file consists of the following settings: .. code-block:: cfg + :caption: The theme file for the "Default Light Theme" [Main] - name = My Custom Theme - description = A description of my custom theme - author = Jane Doe - credit = John Doe - url = https://example.com - license = CC BY-SA 4.0 - licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ + name = Default Light Theme + mode = light + author = Veronica Berglyd Olsen + credit = Veronica Berglyd Olsen + url = https://github.com/vkbo/novelWriter - [Icons] - default = 100, 100, 100 - faded = 100, 100, 100 - red = 255, 0, 0 - orange = 255, 128, 0 - yellow = 255, 255, 0 - green = 0, 255, 0 - aqua = 0, 255, 255 - blue = 0, 0, 255 - purple = 255, 0, 255 + [Base] + base = #fcfcfc + default = #303030 + faded = #6c6c6c + red = #a62a2d + orange = #b36829 + yellow = #a39c34 + green = #296629 + cyan = #269999 + blue = #3a70a6 + purple = #b35ab3 [Project] - root = 0, 255, 255 - folder = 255, 255, 0 - file = 100, 100, 100 - title = 0, 255, 0 - chapter = 255, 0, 0 - scene = 0, 0, 255 - note = 255, 255, 0 + root = blue + folder = yellow + file = default + title = green + chapter = red + scene = blue + note = yellow + active = green + inactive = red + disabled = faded [Palette] - window = 100, 100, 100 - windowtext = 100, 100, 100 - base = 100, 100, 100 - alternatebase = 100, 100, 100 - text = 100, 100, 100 - tooltipbase = 100, 100, 100 - tooltiptext = 100, 100, 100 - button = 100, 100, 100 - buttontext = 100, 100, 100 - brighttext = 100, 100, 100 - highlight = 100, 100, 100 - highlightedtext = 100, 100, 100 - link = 100, 100, 100 - linkvisited = 100, 100, 100 + window = base:D105 + windowtext = default + base = base + alternatebase = #e0e0e0 + text = default + tooltipbase = #ffffc0 + tooltiptext = #15150d + button = #efefef + buttontext = default + brighttext = base + highlight = #3087c6 + highlightedtext = base + link = blue + linkvisited = blue + accent = #3087c6 [GUI] - helptext = 0, 0, 0 - fadedtext = 128, 128, 128 - errortext = 255, 0, 0 + helptext = #5c5c5c + fadedtext = #6c6c6c + errortext = red -In the Main section you must at least define the ``name`` settings. + [Syntax] + background = base + text = default + line = default:32 + link = blue + headertext = green + headertag = green:L135 + emphasis = orange + dialog = blue + altdialog = red + note = yellow:D125 + hidden = faded + shortcode = blue + keyword = red + tag = green + value = green + optional = blue + spellcheckline = red + errorline = green + replacetag = green + modifier = blue + texthighlight = yellow:72 -The Palette values correspond to the Qt enum values for ``QPalette::ColorRole``, see the -`Qt documentation `_ for more details. The -colour values are RGB numbers on the format ``r, g, b`` where each is an integer from ``0`` to -``255``. Omitted values are not loaded and will use default values. If the ``helptext`` colour is -not defined, it is computed as a colour between the ``window`` and ``windowtext`` colour. -Additional shades of some of the colours are also computed. These are mainly used for 3D effects. + +Theme Sections +-------------- + +.. _ColorRole: https://doc.qt.io/qt-6/qpalette.html#ColorRole-enum + +The theme file is made up of different sections depending on what part of novelWriter the theme +affects. + +.. csv-table:: Theme Sections Overview + :header: "Section", "Description" + :class: "tight-table" + + "``[Main]``", "Meta data about the theme, You must at least set ``name``, ``mode`` and ``author``, and ``mode`` must be either ``light`` or ``dark``." + "``[Base]``", "The base colours of the theme. These are also selectable colours in various places inside the app, like for icon colours in **Preferences**." + "``[Project]``", "The colours used for icons and markers for the different project item types." + "``[Palette]``", "The colours used for styling the user interface. The values correspond to the ColorRole_ values in the Qt library." + "``[GUI]``", "The colours used for styling additional elements of the user interface." + "``[Syntax]``", "The colours used for syntax highlighting in documents." + + +Colour Value Formats +-------------------- + +There are several ways to enter colour values: + +.. csv-table:: Colour Formats + :header: "Syntax", "Description" + :widths: 15, 85 + :class: "tight-table" + + "``#RRGGBB``", "A CSS style hexadecimal values, like ``#ff0000`` for red." + "``#RRGGBBAA``", "A CSS style hexadecimal values with transparency, like ``#ff00007f`` for half-transparent red." + "``name``", "A name referring to one of the colours already specified under the ``[Base]`` section, like ``red``. Note that you should not use named colours in the ``[Base]`` section itself as that may have unintended results." + "``name:255``", "A name referring to one of the colours already specified under the ``[Base]`` section, with a transparency value added. The value must be in the range ``0`` to ``255``, like ``red:127`` for half-transparent red." + "``name:L100``", "A name referring to one of the colours already specified under the ``[Base]`` section, where the L-number is a percentage value that makes it lighter. The value must be greater than ``0``. ``L100`` means no change." + "``name:D100``", "A name referring to one of the colours already specified under the ``[Base]`` section, where the D-number is a percentage value that makes it darker. The value must be greater than ``0``. ``D100`` means no change." + "``r, g, b``", "A set of red, green and blue numbers in the range ``0`` to ``255``, like ``255, 0, 0`` for red." + "``r, g, b, a``", "A set of red, green, blue and alpha numbers in the range ``0`` to ``255``, like ``255, 0, 0, 127`` for half-transparent red." .. versionadded:: 2.5 The ``fadedtext`` and ``errortext`` theme colour entries were added. @@ -172,60 +171,86 @@ Additional shades of some of the colours are also computed. These are mainly use The ``icontheme`` setting was dropped as the icon theme is now its own setting. The ``[Icons]`` and ``[Project]`` sections were added, and the ``status*`` settings removed. +.. 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. -Custom Syntax Theme -------------------- -A syntax theme ``.conf`` file consists of the following settings: +Icon Themes +=========== + +Icon themes are *not* straightforward to add, but if you want to make the effort, this section +describes how to do it. + +The existing icon themes are stored in +`novelwriter/assets/icons `_. + +As with colour themes, remember to change the name of your theme by modifying the ``name`` setting +at the top of the file, otherwise you may not be able to distinguish them in **Preferences**. + +For novelWriter to be able to locate the custom theme files, you must copy them to the +:ref:`docs_technical_locations_data` location in your home or user area. There should be a folder +there ``icons`` for icon themes. These folders are created the first time you start novelWriter. + + +The Icons File Format +--------------------- + +Icon themes are kept in files with the ``.icons`` file extension. The file format is a custom +format with entries on the form ``section:key = value``. .. code-block:: cfg + :caption: The icons file for "Material Symbols - Rounded Medium" (truncated) - [Main] - name = My Syntax Theme - author = Jane Doe - credit = John Doe - url = https://example.com - license = CC BY-SA 4.0 - licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ + # Meta + meta:name = Material Symbols - Rounded Medium + meta:author = Google Inc + meta:license = Apache 2.0 - [Syntax] - background = 255, 255, 255 - text = 0, 0, 0 - link = 0, 0, 0 - headertext = 0, 0, 0 - headertag = 0, 0, 0 - emphasis = 0, 0, 0 - dialog = 0, 0, 0 - altdialog = 0, 0, 0 - note = 0, 0, 0 - hidden = 0, 0, 0 - shortcode = 0, 0, 0 - keyword = 0, 0, 0 - tag = 0, 0, 0 - value = 0, 0, 0 - optional = 0, 0, 0 - spellcheckline = 0, 0, 0 - errorline = 0, 0, 0 - replacetag = 0, 0, 0 - modifier = 0, 0, 0 - texthighlight = 255, 255, 255, 128 + # Icons + icon:alert_error = + icon:alert_info = + icon:alert_question = + icon:alert_warn = + icon:cls_archive = + icon:cls_character = + icon:cls_custom = + icon:cls_entity = + icon:cls_none = + icon:cls_novel = + icon:cls_object = + icon:cls_plot = + icon:cls_template = + icon:cls_timeline = + icon:cls_trash = + icon:cls_world = -In the Main section, you must define at least the ``name`` setting. The Syntax colour values are -RGB(A) numbers of the format ``r, g, b, a`` where each is an integer from ``0`` to ``255``. The -fourth value is the alpha channel, which can be omitted. +The icon keys are associated with icon placement locations inside novelWriter, and the template for +them is defined in the script that generates the default icon themes. -Omitted syntax colours default to black, except ``background`` which defaults to white, and -``texthighlight`` which defaults to white with half transparency. +The script can be found under +`utils/icon_themes.py `__ +in the source code. -.. versionadded:: 2.2 - The ``shortcode`` syntax colour entry was added. +This file includes all the code needed to generate the themes that are included in novelWriter. The +icon keys are mapped to icon keys from the specific themes in JSON files in the ``icon_themes`` +folder next to the script. This is the recommended way to generate these themes. Doing it manually +is not advisable. -.. versionadded:: 2.3 - The ``optional`` syntax colour entry was added. -.. versionadded:: 2.4 - The ``texthighlight`` syntax colour entry was added. +Icon Value Format +----------------- -.. versionadded:: 2.5 - The ``dialog``, ``altdialog``, ``note`` and ``tag`` syntax colour entries were added. - ``straightquotes``, ``doublequotes`` and ``singlequotes`` were removed. +As can be seen from the example, an icon is defined in the ``icon`` section with a key and an +in-line SVG XML block. The XML must fit on one line and obey the following rules: + +#. It must be single colour, that is, the fill colour attribute must be able to colourise the + entire icon. +#. The fill colour attribute *must* be defined and must be set to: ``fill="#000000"``. This value + is replaced by the relevant theme colour when the icon is processed in novelWriter. + +.. versionadded:: 2.7 + The icon theme files were added. Previously, icons were stored as individual SVG files with a + config file mapping the file names to the internal icon keys. diff --git a/docs/source/more/dictionaries.rst b/docs/source/more/dictionaries.rst new file mode 100644 index 00000000..1ca896d5 --- /dev/null +++ b/docs/source/more/dictionaries.rst @@ -0,0 +1,55 @@ +.. _docs_more_dictionaries: + +************************ +Spell Check Dictionaries +************************ + +.. _Enchant: https://rrthomas.github.io/enchant/ +.. _Free Desktop: https://cgit.freedesktop.org/libreoffice/dictionaries/tree/ + +Spell checking is provided by the Enchant_ library. Depending on your operating system, it may or +may not load all installed spell check dictionaries automatically. + + +Linux and MacOS +=============== + +On Linux and MacOS, you generally only have to install hunspell, aspell or myspell dictionaries on +your system like you do for other applications. See your distro or OS documentation for how to do +this. These dictionaries should show up as available spell check languages in novelWriter. + + +Windows +======= + +For Windows, English is included with the installation. For other languages you have to download +and add dictionaries yourself. + +**Install Tool** + +A small tool to assist with this can be found under **Tools > Add Dictionaries**. It will import +spell checking dictionaries from Free Office or Libre Office extensions. The dictionaries are then +installed in the install location for the Enchant library and should thus work for any application +that uses Enchant for spell checking. + +**Manual Install** + +If you prefer to do this manually or want to use a different source than the ones mentioned above, +You need to get compatible dictionary files for your language. You need two files files ending with +``.aff`` and ``.dic``. These files must then be copied to the following location: + +``C:\Users\\AppData\Local\enchant\hunspell`` + +This assumes your user profile is stored at ``C:\Users\``. The last one or two folders may +not exist, so you may need to create them. + +You can find the various dictionaries on the `Free Desktop`_ website. + +.. note:: + + The Free Desktop link points to a repository, and what may look like file links inside the + dictionary folder are actually links to web pages. If you right-click and download those, you + get HTML files, not dictionaries! + + In order to download the actual dictionary files, right-click the "plain" label at the end of + each line and download that. diff --git a/docs/source/usage/organising_project.rst b/docs/source/usage/organising_project.rst index 4c8f1cf4..4f4e3c9b 100644 --- a/docs/source/usage/organising_project.rst +++ b/docs/source/usage/organising_project.rst @@ -166,6 +166,9 @@ a character note template, you can add a **Templates** root folder to your proje added to this root folder will show up in the **Add Item** menu in the project tree toolbar. When selected, a new document is created with its content copied from the chosen template. +If the first line of the template file is a title line, the title text will be replaced with the +text of the document label when it is first created. + .. versionadded:: 2.3 diff --git a/docs/source/user_interface/editor_viewer.rst b/docs/source/user_interface/editor_viewer.rst index 841be0b0..ee0c1eb3 100644 --- a/docs/source/user_interface/editor_viewer.rst +++ b/docs/source/user_interface/editor_viewer.rst @@ -55,7 +55,7 @@ edited from the **Project Word List** tool available from the **Tools** menu. .. note:: Generally, spell checking dictionaries are collected from your operating system, but on Windows - they are not. See :ref:`docs_more_custom_dict` for how to add spell checking dictionaries on + they are not. See :ref:`docs_more_dictionaries` for how to add spell checking dictionaries on Windows. diff --git a/i18n/qtbase.py b/i18n/qtbase.py index 3126bea3..e40279cd 100644 --- a/i18n/qtbase.py +++ b/i18n/qtbase.py @@ -8,7 +8,7 @@ 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 diff --git a/novelWriter.py b/novelWriter.py index 63d437e1..797dfe23 100755 --- a/novelWriter.py +++ b/novelWriter.py @@ -2,7 +2,7 @@ """ novelWriter – Start Script ========================== -""" +""" # noqa import os import sys diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 65a5c286..fa3dd30e 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import getopt @@ -49,9 +49,9 @@ __license__ = "GPLv3" __author__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" -__version__ = "2.7.5" -__hexversion__ = "0x020705f0" -__date__ = "2025-09-14" +__version__ = "2.8a2" +__hexversion__ = "0x020800a2" +__date__ = "2025-07-16" __status__ = "Stable" __domain__ = "novelwriter.io" @@ -296,7 +296,8 @@ def _createApp(style: str) -> QApplication: """Create the app. This is done in a function to make it easier to block app creation during testing. """ - app = QApplication([CONFIG.appName, f"-style={style}"]) + app = QApplication([CONFIG.appName]) + app.setStyle(style) app.setApplicationName(CONFIG.appName) app.setApplicationVersion(__version__) app.setOrganizationDomain(__domain__) diff --git a/novelwriter/assets/icons/material_filled_normal.icons b/novelwriter/assets/icons/material_filled_normal.icons index 04680813..36b8536f 100644 --- a/novelwriter/assets/icons/material_filled_normal.icons +++ b/novelwriter/assets/icons/material_filled_normal.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_filled_thin.icons b/novelwriter/assets/icons/material_filled_thin.icons index 70c4f364..a405ea3e 100644 --- a/novelwriter/assets/icons/material_filled_thin.icons +++ b/novelwriter/assets/icons/material_filled_thin.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_rounded_normal.icons b/novelwriter/assets/icons/material_rounded_normal.icons index 35c021c1..9c22a001 100644 --- a/novelwriter/assets/icons/material_rounded_normal.icons +++ b/novelwriter/assets/icons/material_rounded_normal.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_rounded_thin.icons b/novelwriter/assets/icons/material_rounded_thin.icons index 61fcd5fa..1002fcbf 100644 --- a/novelwriter/assets/icons/material_rounded_thin.icons +++ b/novelwriter/assets/icons/material_rounded_thin.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_sharp_normal.icons b/novelwriter/assets/icons/material_sharp_normal.icons index a5867da9..b8b8c0e6 100644 --- a/novelwriter/assets/icons/material_sharp_normal.icons +++ b/novelwriter/assets/icons/material_sharp_normal.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_sharp_thin.icons b/novelwriter/assets/icons/material_sharp_thin.icons index 485676b2..afe8528d 100644 --- a/novelwriter/assets/icons/material_sharp_thin.icons +++ b/novelwriter/assets/icons/material_sharp_thin.icons @@ -56,6 +56,9 @@ icon:sb_outline = icon:sb_search = icon:sb_stats = +icon:theme_light = +icon:theme_dark = +icon:theme_auto = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/images/welcome-dark.jpg b/novelwriter/assets/images/welcome-dark.jpg deleted file mode 100644 index 1b434912..00000000 Binary files a/novelwriter/assets/images/welcome-dark.jpg and /dev/null differ diff --git a/novelwriter/assets/images/welcome-light.jpg b/novelwriter/assets/images/welcome-light.jpg deleted file mode 100644 index 1a1b9f6a..00000000 Binary files a/novelwriter/assets/images/welcome-light.jpg and /dev/null differ diff --git a/novelwriter/assets/images/welcome.webp b/novelwriter/assets/images/welcome.webp new file mode 100644 index 00000000..3831aafe Binary files /dev/null and b/novelwriter/assets/images/welcome.webp differ diff --git a/novelwriter/assets/syntax/cyberpunk_night.conf b/novelwriter/assets/syntax/cyberpunk_night.conf deleted file mode 100644 index 57a14536..00000000 --- a/novelwriter/assets/syntax/cyberpunk_night.conf +++ /dev/null @@ -1,28 +0,0 @@ -[Main] -name = Cyberpunk Night -author = Anders Lemvigh -url = https://github.com/alemvigh -license = CC BY-SA 4.0 -licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ - -[Syntax] -background = 0, 0, 0 -text = 150, 150, 150 -link = 77, 77, 255 -headertext = 255, 255, 255 -headertag = 50, 0, 180 -emphasis = 0, 255, 255 -dialog = 0, 255, 0 -altdialog = 0, 140, 255 -note = 150, 150, 150 -hidden = 77, 77, 100 -shortcode = 255, 255, 0 -keyword = 255, 100, 255 -tag = 255, 150, 10 -value = 255, 150, 10 -spellcheckline = 242, 72, 23 -errorline = 186, 218, 4 -replacetag = 0, 0, 180 -modifier = 144, 142, 176 -optional = 180, 180, 180 -texthighlight = 255, 255, 132, 96 diff --git a/novelwriter/assets/syntax/default_dark.conf b/novelwriter/assets/syntax/default_dark.conf deleted file mode 100644 index 8e467189..00000000 --- a/novelwriter/assets/syntax/default_dark.conf +++ /dev/null @@ -1,42 +0,0 @@ -[Main] -name = Default Dark -author = Veronica Berglyd Olsen -credit = Veronica Berglyd Olsen -url = https://github.com/vkbo/novelWriter -license = CC BY-SA 4.0 -licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ - -## -# Colours: -# Background = 2a2a2a : 42, 42, 42 -# Foreground = cccccc : 204, 204, 204 -# Comment = 969696 : 150, 150, 150 -# Red = e64250 : 230, 66, 80 -# Orange = f99157 : 249, 145, 87 -# Yellow = ffff84 : 255, 255, 132 -# Green = 99cc99 : 153, 204, 153 -# Blue = 288ed7 : 102, 153, 204 -# Purple = 8c56d7 : 140, 86, 215 -## - -[Syntax] -background = 42, 42, 42 -text = 204, 204, 204 -link = 102, 153, 204 -headertext = 153, 204, 153 -headertag = 153, 204, 153, 160 -emphasis = 249, 145, 87 -dialog = 102, 153, 204 -altdialog = 102, 153, 204 -note = 255, 255, 216 -hidden = 150, 150, 150 -shortcode = 153, 204, 153 -keyword = 230, 66, 80 -tag = 153, 204, 153 -value = 102, 153, 204 -optional = 153, 204, 153 -spellcheckline = 230, 66, 80 -errorline = 153, 204, 153 -replacetag = 153, 204, 153 -modifier = 153, 204, 153 -texthighlight = 255, 255, 132, 96 diff --git a/novelwriter/assets/syntax/default_light.conf b/novelwriter/assets/syntax/default_light.conf deleted file mode 100644 index 2e5ae404..00000000 --- a/novelwriter/assets/syntax/default_light.conf +++ /dev/null @@ -1,42 +0,0 @@ -[Main] -name = Default Light -author = Veronica Berglyd Olsen -credit = Veronica Berglyd Olsen -url = https://github.com/vkbo/novelWriter -license = CC BY-SA 4.0 -licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ - -## -# Colours: -# Background = ffffff : 255, 255, 255 -# Foreground = 000000 : 0, 0, 0 -# Comment = 646464 : 100, 100, 100 -# Red = c83232 : 200, 50, 50 -# Orange = ae6e1e : 174, 110, 30 -# Yellow = 969600 : 150, 150, 0 -# Green = 006400 : 0, 100, 0 -# Blue = 0000c8 : 0, 0, 200 -# Purple = 6400ae : 100, 0, 174 -## - -[Syntax] -background = 255, 255, 255 -text = 0, 0, 0 -link = 0, 0, 200 -headertext = 0, 100, 0 -headertag = 0, 100, 0, 160 -emphasis = 174, 110, 30 -dialog = 0, 0, 200 -altdialog = 0, 0, 200 -note = 100, 100, 0 -hidden = 100, 100, 100 -shortcode = 0, 100, 0 -keyword = 200, 50, 50 -tag = 0, 100, 0 -value = 0, 0, 200 -optional = 0, 100, 0 -spellcheckline = 200, 50, 50 -errorline = 0, 100, 0 -replacetag = 0, 100, 0 -modifier = 0, 100, 0 -texthighlight = 150, 150, 0, 96 diff --git a/novelwriter/assets/syntax/dracula.conf b/novelwriter/assets/syntax/dracula.conf deleted file mode 100644 index fad01ea8..00000000 --- a/novelwriter/assets/syntax/dracula.conf +++ /dev/null @@ -1,44 +0,0 @@ -[Main] -name = Dracula -author = Veronica Berglyd Olsen (adaptation) -credit = Zeno Rocha -url = https://draculatheme.com -license = MIT -licenseurl = https://github.com/dracula/dracula-theme/blob/main/LICENSE - -## -# Colours: -# Background = 282a36 : 40, 42, 54 -# Foreground = f8f8f2 : 248, 248, 242 -# Comment = 6272a4 : 98, 114, 164 -# Cyan = 8be9fd : 139, 233, 253 -# Green = 50fa7b : 80, 250, 123 -# Orange = ffb86c : 255, 184, 108 -# Pink = ff79c6 : 255, 121, 198 -# Purple = bd93f9 : 189, 147, 249 -# Red = ff5555 : 255, 85, 85 -# Yellow = f1fa8c : 241, 250, 140 -## - -[Syntax] -# See: https://spec.draculatheme.com/ -background = 40, 42, 54 -text = 248, 248, 242 -link = 255, 121, 198 -headertext = 189, 147, 249 -headertag = 189, 147, 249, 160 -emphasis = 255, 184, 108 -dialog = 80, 250, 123 -altdialog = 241, 250, 140 -note = 255, 204, 233 -hidden = 98, 114, 164 -shortcode = 139, 233, 253 -keyword = 255, 121, 198 -tag = 255, 184, 108 -value = 255, 184, 108 -optional = 80, 250, 123 -spellcheckline = 255, 85, 85 -errorline = 80, 250, 123 -replacetag = 241, 250, 140 -modifier = 139, 233, 253 -texthighlight = 241, 250, 140, 96 diff --git a/novelwriter/assets/syntax/grey_dark.conf b/novelwriter/assets/syntax/grey_dark.conf deleted file mode 100644 index cc36478c..00000000 --- a/novelwriter/assets/syntax/grey_dark.conf +++ /dev/null @@ -1,29 +0,0 @@ -[Main] -name = Grey Dark -author = Veronica Berglyd Olsen -credit = Veronica Berglyd Olsen -url = https://github.com/vkbo/novelWriter -license = CC BY-SA 4.0 -licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ - -[Syntax] -background = 54, 54, 54 -text = 200, 200, 200 -link = 200, 200, 200 -headertext = 225, 225, 225 -headertag = 225, 225, 225, 160 -emphasis = 200, 200, 200 -dialog = 200, 200, 200 -altdialog = 200, 200, 200 -note = 200, 200, 200 -hidden = 150, 150, 150 -shortcode = 225, 225, 225 -keyword = 225, 225, 225 -tag = 200, 200, 200 -value = 200, 200, 200 -optional = 225, 225, 225 -spellcheckline = 200, 46, 0 -errorline = 46, 200, 0 -replacetag = 225, 225, 225 -modifier = 225, 225, 225 -texthighlight = 255, 255, 255, 64 diff --git a/novelwriter/assets/syntax/grey_light.conf b/novelwriter/assets/syntax/grey_light.conf deleted file mode 100644 index 80a8a01c..00000000 --- a/novelwriter/assets/syntax/grey_light.conf +++ /dev/null @@ -1,29 +0,0 @@ -[Main] -name = Grey Light -author = Veronica Berglyd Olsen -credit = Veronica Berglyd Olsen -url = https://github.com/vkbo/novelWriter -license = CC BY-SA 4.0 -licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ - -[Syntax] -background = 255, 255, 255 -text = 20, 20, 20 -link = 20, 20, 20 -headertext = 0, 0, 0 -headertag = 0, 0, 0, 160 -emphasis = 20, 20, 20 -dialog = 20, 20, 20 -altdialog = 20, 20, 20 -note = 20, 20, 20 -hidden = 100, 100, 100 -shortcode = 0, 0, 0 -keyword = 0, 0, 0 -tag = 20, 20, 20 -value = 20, 20, 20 -optional = 0, 0, 0 -spellcheckline = 200, 0, 0 -errorline = 0, 150, 0 -replacetag = 0, 0, 0 -modifier = 0, 0, 0 -texthighlight = 0, 0, 0, 64 diff --git a/novelwriter/assets/syntax/light_owl.conf b/novelwriter/assets/syntax/light_owl.conf deleted file mode 100644 index ce65e704..00000000 --- a/novelwriter/assets/syntax/light_owl.conf +++ /dev/null @@ -1,49 +0,0 @@ -## -# Theme: Light Owl -# Source: https://github.com/sdras/night-owl-vscode-theme -# Credit: Sarah Drasner -## -# Colours: -# Background = fbfbfb : 251, 251, 251 -# Current Line = e0e0e0 : 224, 224, 224 -# Selection = e0e0e0 : 224, 224, 224 -# Foreground = 403f52 : 64, 63, 82 -# Comment = 989fb1 : 152, 159, 177 -# Red = de3d3a : 222, 61, 58 -# Orange = e0af05 : 224, 175, 5 -# Yellow = daaa01 : 218, 170, 1 -# Green = 08916a : 8, 145, 106 -# Aqua = 2aa298 : 42, 162, 152 -# Blue = 288ed7 : 40, 142, 215 -# Purple = 964ac1 : 150, 74, 193 -## - -[Main] -name = Light Owl -author = Veronica Berglyd Olsen (adaptation) -credit = Sarah Drasner (color theme) -url = https://github.com/sdras/night-owl-vscode-theme -license = MIT License -licenseurl = https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE - -[Syntax] -background = 251, 251, 251 -text = 64, 63, 82 -link = 40, 142, 215 -headertext = 40, 142, 215 -headertag = 40, 142, 215, 160 -emphasis = 224, 175, 5 -dialog = 8, 145, 106 -altdialog = 218, 170, 1 -note = 128, 118, 75 -hidden = 152, 159, 177 -shortcode = 40, 142, 215 -keyword = 222, 61, 58 -tag = 150, 74, 193 -value = 224, 175, 5 -optional = 40, 142, 215 -spellcheckline = 222, 61, 58 -errorline = 8, 145, 106 -replacetag = 42, 162, 152 -modifier = 8, 145, 106 -texthighlight = 218, 170, 1, 96 diff --git a/novelwriter/assets/syntax/night_owl.conf b/novelwriter/assets/syntax/night_owl.conf deleted file mode 100644 index 337cdfb8..00000000 --- a/novelwriter/assets/syntax/night_owl.conf +++ /dev/null @@ -1,49 +0,0 @@ -## -# Theme: Night Owl -# Source: https://github.com/sdras/night-owl-vscode-theme -# Credit: Sarah Drasner -## -# Colours: -# Background = 011627 : 1, 22, 39 -# Current Line = 32374d : 50, 55, 77 -# Selection = 32374d : 50, 55, 77 -# Foreground = d6deeb : 214, 222, 235 -# Comment = 637777 : 99, 119, 119 -# Red = f78c6c : 247, 140, 108 -# Orange = ecc48d : 236, 196, 141 -# Yellow = ffeb95 : 255, 235, 149 -# Green = addb67 : 173, 219, 103 -# Aqua = 7fdbca : 127, 219, 202 -# Blue = 82aaff : 130, 170, 255 -# Purple = c792ea : 199, 146, 234 -## - -[Main] -name = Night Owl -author = Veronica Berglyd Olsen (adaptation) -credit = Sarah Drasner (color theme) -url = https://github.com/sdras/night-owl-vscode-theme -license = MIT License -licenseurl = https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE - -[Syntax] -background = 1, 22, 39 -text = 214, 222, 235 -link = 130, 170, 255 -headertext = 130, 170, 255 -headertag = 130, 170, 255, 160 -emphasis = 236, 196, 141 -dialog = 173, 219, 103 -altdialog = 255, 235, 149 -note = 255, 249, 202 -hidden = 99, 119, 119 -shortcode = 130, 170, 255 -keyword = 247, 140, 108 -tag = 199, 146, 234 -value = 255, 235, 149 -optional = 130, 170, 255 -spellcheckline = 247, 140, 108 -errorline = 173, 219, 103 -replacetag = 127, 219, 202 -modifier = 173, 219, 103 -texthighlight = 255, 235, 149, 96 diff --git a/novelwriter/assets/syntax/snazzy.conf b/novelwriter/assets/syntax/snazzy.conf deleted file mode 100644 index d948b770..00000000 --- a/novelwriter/assets/syntax/snazzy.conf +++ /dev/null @@ -1,42 +0,0 @@ -[Main] -name = Snazzy Light -author = Veronica Berglyd Olsen (adaptation) -credit = Florian Reuschel (color theme) -url = https://github.com/loilo/vscode-snazzy-light -license = MIT License -licenseurl = https://github.com/loilo/vscode-snazzy-light/blob/master/LICENSE - -## -# Colours: -# Background = fafbfc : 250, 251, 252 -# Foreground = 565869 : 86, 88, 105 -# Comment = 9194a2 : 145, 148, 162 -# Yellow = cf9c00 : 207, 156, 0 -# Red = ff5c57 : 255, 92, 87 -# Pink = f767bb : 247, 103, 187 -# Blue = 09a1ed : 9, 161, 237 -# Cyan = 13bbb7 : 19, 187, 183 -# Green = 2dae58 : 45, 174, 88 -## - -[Syntax] -background = 250, 251, 252 -text = 86, 88, 105 -link = 9, 161, 237 -headertext = 45, 174, 88 -headertag = 45, 174, 88, 160 -emphasis = 247, 103, 187 -dialog = 9, 161, 237 -altdialog = 207, 156, 0 -note = 120, 187, 185 -hidden = 145, 148, 162 -shortcode = 247, 103, 187 -keyword = 9, 161, 237 -tag = 45, 174, 88 -value = 207, 156, 0 -optional = 207, 156, 0 -spellcheckline = 255, 92, 87 -errorline = 45, 174, 88 -replacetag = 19, 187, 183 -modifier = 247, 103, 187 -texthighlight = 207, 156, 0, 96 diff --git a/novelwriter/assets/syntax/solarized_dark.conf b/novelwriter/assets/syntax/solarized_dark.conf deleted file mode 100644 index 0fbacdb7..00000000 --- a/novelwriter/assets/syntax/solarized_dark.conf +++ /dev/null @@ -1,29 +0,0 @@ -[Main] -name = Solarized Dark -author = nullbasis -credit = Ethan Schoonover -url = https://ethanschoonover.com/solarized/ -license = MIT -licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE - -[Syntax] -background = 7, 54, 66 -text = 253, 246, 227 -link = 38, 139, 210 -headertext = 147, 161, 161 -headertag = 42, 161, 152 -emphasis = 38, 139, 210 -dialog = 42, 161, 152 -altdialog = 42, 161, 152 -note = 101, 161, 156 -hidden = 147, 161, 161 -shortcode = 147, 161, 161 -keyword = 133, 153, 0 -tag = 203, 75, 22 -value = 203, 75, 22 -optional = 147, 161, 161 -spellcheckline = 203, 75, 22 -errorline = 220, 50, 47 -replacetag = 133, 153, 0 -modifier = 181, 137, 0 -texthighlight = 181, 137, 0, 96 diff --git a/novelwriter/assets/syntax/solarized_light.conf b/novelwriter/assets/syntax/solarized_light.conf deleted file mode 100644 index 525a59e5..00000000 --- a/novelwriter/assets/syntax/solarized_light.conf +++ /dev/null @@ -1,29 +0,0 @@ -[Main] -name = Solarized Light -author = nullbasis -credit = Ethan Schoonover -url = https://ethanschoonover.com/solarized/ -license = MIT -licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE - -[Syntax] -background = 253, 246, 227 -text = 0, 43, 54 -link = 38, 139, 210 -headertext = 88, 110, 117 -headertag = 42, 161, 152 -emphasis = 38, 139, 210 -dialog = 42, 161, 152 -altdialog = 42, 161, 152 -note = 27, 102, 96 -hidden = 88, 110, 117 -shortcode = 88, 110, 117 -keyword = 133, 153, 0 -tag = 203, 75, 22 -value = 203, 75, 22 -optional = 88, 110, 117 -spellcheckline = 203, 75, 22 -errorline = 220, 50, 47 -replacetag = 133, 153, 0 -modifier = 181, 137, 0 -texthighlight = 181, 137, 0, 96 diff --git a/novelwriter/assets/syntax/tango.conf b/novelwriter/assets/syntax/tango.conf deleted file mode 100644 index 7c5e9daa..00000000 --- a/novelwriter/assets/syntax/tango.conf +++ /dev/null @@ -1,39 +0,0 @@ -[Main] -name = Tango -author = Veronica Berglyd Olsen (adaptation) - -## -# Colours: -# Background = 30302f : 48, 48, 47 -# Foreground = eeeeec : 238, 238, 236 -# Comment = 8f8f8d : 143, 143, 141 -# Red = ef2929 : 239, 41, 41 -# Orange = c4a000 : 196, 160, 0 -# Yellow = fce94f : 252, 233, 79 -# Green = 8ae234 : 138, 226, 52 -# Cyan = 34e2e2 : 52, 226, 226 -# Blue = 3465a4 : 114, 159, 207 -# Purple = ad7fa8 : 173, 127, 168 -## - -[Syntax] -background = 48, 48, 47 -text = 238, 238, 236 -link = 114, 159, 207 -headertext = 114, 159, 207 -headertag = 114, 159, 207, 160 -emphasis = 196, 160, 0 -dialog = 138, 226, 52 -altdialog = 252, 233, 79 -note = 252, 242, 164 -hidden = 143, 143, 141 -shortcode = 114, 159, 207 -keyword = 239, 41, 41 -tag = 173, 127, 168 -value = 239, 41, 41 -optional = 114, 159, 207 -spellcheckline = 239, 41, 41 -errorline = 138, 226, 52 -replacetag = 52, 226, 226 -modifier = 114, 159, 207 -texthighlight = 252, 233, 79, 96 diff --git a/novelwriter/assets/syntax/tomorrow.conf b/novelwriter/assets/syntax/tomorrow.conf deleted file mode 100644 index d3d2b8e4..00000000 --- a/novelwriter/assets/syntax/tomorrow.conf +++ /dev/null @@ -1,49 +0,0 @@ -## -# Theme: Tomorrow -# Source: https://github.com/chriskempson/tomorrow-theme -# Credit: Chris Kempson -## -# Colours: -# Background = ffffff : 255, 255, 255 -# Current Line = efefef : 239, 239, 239 -# Selection = d6d6d6 : 214, 214, 214 -# Foreground = 4d4d4c : 77, 77, 76 -# Comment = 8e908c : 142, 144, 140 -# Red = c82829 : 240, 40, 41 -# Orange = f5871f : 245, 135, 31 -# Yellow = eab700 : 234, 183, 0 -# Green = 718c00 : 113, 140, 0 -# Aqua = 3e999f : 62, 153, 159 -# Blue = 4271ae : 66, 113, 174 -# Purple = 8959a8 : 137, 89, 168 -## - -[Main] -name = Tomorrow -author = Veronica Berglyd Olsen (adaptation) -credit = Chris Kempson (color theme) -url = https://github.com/chriskempson/tomorrow-theme -license = MIT License -licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md - -[Syntax] -background = 255, 255, 255 -text = 77, 77, 76 -link = 66, 113, 174 -headertext = 66, 113, 174 -headertag = 66, 113, 174, 160 -emphasis = 245, 135, 31 -dialog = 113, 140, 0 -altdialog = 234, 183, 0 -note = 115, 90, 0 -hidden = 142, 144, 140 -shortcode = 66, 113, 174 -keyword = 240, 40, 41 -tag = 137, 89, 168 -value = 234, 183, 0 -optional = 66, 113, 174 -spellcheckline = 240, 40, 41 -errorline = 113, 140, 0 -replacetag = 62, 153, 159 -modifier = 245, 135, 31 -texthighlight = 234, 183, 0, 96 diff --git a/novelwriter/assets/syntax/tomorrow_night.conf b/novelwriter/assets/syntax/tomorrow_night.conf deleted file mode 100644 index 15257945..00000000 --- a/novelwriter/assets/syntax/tomorrow_night.conf +++ /dev/null @@ -1,49 +0,0 @@ -## -# Theme: Tomorrow Night -# Source: https://github.com/chriskempson/tomorrow-theme -# Credit: Chris Kempson -## -# Colours: -# Background = 1d1f21 : 29, 31, 33 -# Current Line = 282a2e : 40, 42, 46 -# Selection = 373b41 : 55, 59. 65 -# Foreground = c5c8c6 : 197, 200, 198 -# Comment = 969896 : 150, 152, 150 -# Red = cc6666 : 204, 102, 102 -# Orange = de935f : 222, 147, 95 -# Yellow = f0c674 : 240, 198, 116 -# Green = b5bd68 : 181, 189, 104 -# Aqua = 8abeb7 : 138, 190, 183 -# Blue = 81a2be : 129, 162, 190 -# Purple = b294bb : 178, 148, 187 -## - -[Main] -name = Tomorrow Night -author = Veronica Berglyd Olsen (adaptation) -credit = Chris Kempson (color theme) -url = https://github.com/chriskempson/tomorrow-theme -license = MIT License -licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md - -[Syntax] -background = 29, 31, 33 -text = 197, 200, 198 -link = 197, 200, 198, 160 -headertext = 129, 162, 190 -headertag = 94, 119, 139 -emphasis = 222, 147, 95 -dialog = 181, 189, 104 -altdialog = 240, 198, 116 -note = 240, 219, 178 -hidden = 150, 152, 150 -shortcode = 129, 162, 190 -keyword = 204, 102, 102 -tag = 178, 148, 187 -value = 240, 198, 116 -optional = 129, 162, 190 -spellcheckline = 204, 102, 102 -errorline = 181, 189, 104 -replacetag = 138, 190, 183 -modifier = 222, 147, 95 -texthighlight = 240, 198, 116, 96 diff --git a/novelwriter/assets/syntax/tomorrow_night_blue.conf b/novelwriter/assets/syntax/tomorrow_night_blue.conf deleted file mode 100644 index 89067e4f..00000000 --- a/novelwriter/assets/syntax/tomorrow_night_blue.conf +++ /dev/null @@ -1,49 +0,0 @@ -## -# Theme: Tomorrow Night Blue -# Source: https://github.com/chriskempson/tomorrow-theme -# Credit: Chris Kempson -## -# Colours: -# Background = 002451 : 0, 36, 81 -# Current Line = 00346e : 0, 52, 110 -# Selection = 003f8e : 0, 63, 142 -# Foreground = ffffff : 255, 255, 255 -# Comment = 7285b7 : 114, 133, 183 -# Red = ff9da4 : 255, 157, 164 -# Orange = ffc58f : 255, 197, 143 -# Yellow = ffeead : 255, 238, 173 -# Green = d1f1a9 : 209, 241, 169 -# Aqua = 99ffff : 153, 255, 255 -# Blue = bbdaff : 187, 218, 255 -# Purple = ebbbff : 235, 187, 255 -## - -[Main] -name = Tomorrow Night Blue -author = Veronica Berglyd Olsen (adaptation) -credit = Chris Kempson (color theme) -url = https://github.com/chriskempson/tomorrow-theme -license = MIT License -licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md - -[Syntax] -background = 0, 36, 81 -text = 255, 255, 255 -link = 187, 218, 255 -headertext = 187, 218, 255 -headertag = 187, 218, 255, 160 -emphasis = 255, 197, 143 -dialog = 209, 241, 169 -altdialog = 255, 238, 173 -note = 255, 247, 214 -hidden = 114, 133, 183 -shortcode = 187, 218, 255 -keyword = 255, 157, 164 -tag = 235, 187, 255 -value = 255, 238, 173 -optional = 187, 218, 255 -spellcheckline = 255, 157, 164 -errorline = 209, 241, 169 -replacetag = 153, 255, 255 -modifier = 255, 197, 143 -texthighlight = 255, 238, 173, 96 diff --git a/novelwriter/assets/syntax/tomorrow_night_bright.conf b/novelwriter/assets/syntax/tomorrow_night_bright.conf deleted file mode 100644 index 0f1fd055..00000000 --- a/novelwriter/assets/syntax/tomorrow_night_bright.conf +++ /dev/null @@ -1,49 +0,0 @@ -## -# Theme: Tomorrow Night Bright -# Source: https://github.com/chriskempson/tomorrow-theme -# Credit: Chris Kempson -## -# Colours: -# Background = 000000 : 0, 0, 0 -# Current Line = 2a2a2a : 42, 42, 42 -# Selection = 424242 : 66, 66, 66 -# Foreground = eaeaea : 234, 234, 234 -# Comment = 969896 : 150, 152, 150 -# Red = d54e53 : 213, 78, 83 -# Orange = e78c45 : 231, 140, 69 -# Yellow = e7c547 : 231, 197, 71 -# Green = b9ca4a : 185, 202, 74 -# Aqua = 70c0b1 : 112, 192, 177 -# Blue = 7aa6da : 122, 166, 218 -# Purple = c397d8 : 195, 151, 216 -## - -[Main] -name = Tomorrow Night Bright -author = Veronica Berglyd Olsen (adaptation) -credit = Chris Kempson (color theme) -url = https://github.com/chriskempson/tomorrow-theme -license = MIT License -licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md - -[Syntax] -background = 0, 0, 0 -text = 234, 234, 234 -link = 122, 166, 218 -headertext = 122, 166, 218 -headertag = 122, 166, 218, 160 -emphasis = 231, 140, 69 -dialog = 185, 202, 74 -altdialog = 231, 197, 71 -note = 231, 214, 150 -hidden = 150, 152, 150 -shortcode = 122, 166, 218 -keyword = 213, 78, 83 -tag = 195, 151, 216 -value = 231, 197, 71 -optional = 122, 166, 218 -spellcheckline = 213, 78, 83 -errorline = 185, 202, 74 -replacetag = 112, 192, 177 -modifier = 231, 140, 69 -texthighlight = 231, 197, 71, 128 diff --git a/novelwriter/assets/syntax/tomorrow_night_eighties.conf b/novelwriter/assets/syntax/tomorrow_night_eighties.conf deleted file mode 100644 index a638a871..00000000 --- a/novelwriter/assets/syntax/tomorrow_night_eighties.conf +++ /dev/null @@ -1,49 +0,0 @@ -## -# Theme: Tomorrow Night Eighties -# Source: https://github.com/chriskempson/tomorrow-theme -# Credit: Chris Kempson -## -# Colours: -# Background = 2d2d2d : 45, 45, 45 -# Current Line = 393939 : 57, 57, 57 -# Selection = 515151 : 81, 81, 81 -# Foreground = cccccc : 204, 204, 204 -# Comment = 999999 : 153, 153, 153 -# Red = f2777a : 242, 119, 122 -# Orange = f99157 : 249, 145, 57 -# Yellow = ffcc66 : 255, 204, 102 -# Green = 99cc99 : 153, 204, 153 -# Aqua = 66cccc : 102, 204, 204 -# Blue = 6699cc : 102, 153, 204 -# Purple = cc99cc : 204, 153, 204 -## - -[Main] -name = Tomorrow Night Eighties -author = Veronica Berglyd Olsen (adaptation) -credit = Chris Kempson (color theme) -url = https://github.com/chriskempson/tomorrow-theme -license = MIT License -licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md - -[Syntax] -background = 45, 45, 45 -text = 204, 204, 204 -link = 102, 153, 204 -headertext = 102, 153, 204 -headertag = 102, 153, 204, 160 -emphasis = 249, 145, 57 -dialog = 153, 204, 153 -altdialog = 255, 204, 102 -note = 255, 230, 179 -hidden = 153, 153, 153 -shortcode = 102, 153, 204 -keyword = 242, 119, 122 -tag = 204, 153, 204 -value = 255, 204, 102 -optional = 102, 153, 204 -spellcheckline = 242, 119, 122 -errorline = 153, 204, 153 -replacetag = 102, 204, 204 -modifier = 249, 145, 57 -texthighlight = 255, 204, 102, 96 diff --git a/novelwriter/assets/text/credits_en.htm b/novelwriter/assets/text/credits_en.htm index 88b3887d..c200cf28 100644 --- a/novelwriter/assets/text/credits_en.htm +++ b/novelwriter/assets/text/credits_en.htm @@ -67,20 +67,67 @@ more contributions are listed on the project's Members page.

  • PyEnchant by Dimitri Merejkowsky
  • -

    Assets

    +

    Icon Themes

    -

    Some of the assets bundled with novelWriter were adapted from the following sources:

    +

    The default icon theme is a subset of icons from:

      -
    • Material Symbols icons by Google Inc (Apache 2.0)
    • -
    • Remix icons by RemixIcon (Apache 2.0)
    • -
    • Font Awesome icons by Fonticons Inc (CC BY 4.0)
    • -
    • Tomorrow syntax themes by Chris Kempson (MIT License)
    • -
    • Owl syntax themes by Sarah Drasner (MIT License)
    • -
    • Solarized themes by Ethan Schoonover (MIT License)
    • -
    • Cyberpunk Night theme by Anders Lemvigh (CC BY-SA 4.0)
    • -
    • Dracula theme by Zeno Rocha (MIT License)
    • -
    • Snazzy Light theme by Florian Reuschel (MIT License)
    • +
    • Material Symbols by Google Inc (Apache 2.0)
    • +
    + +

    Additional icon themes may or may not be bundled with novelWriter, depending on license +constraints. These themes include a subset of icons from:

    + +
      +
    • Font Awesome by Fonticons Inc (CC BY 4.0)
    • +
    • Remix by RemixIcon (Apache 2.0 w/non-free clause)
    • +
    + +

    Colour Themes

    + +

    Colour themes included with novelWriter were adapted from the following sources:

    + +
      +
    • Aura themes by Dalton Menezes
    • +
    • B2T themes by Bram de Haan
    • +
    • B4T themes by Bram de Haan
    • +
    • Dracula theme by Zeno Rocha
    • +
    • Espresso theme by Sainnhe Park and Antoine Cotten
    • +
    • Everforest themes by Sainnhe Park
    • +
    • Horizon themes by Jonathan Olaleye
    • +
    • New Moon theme by Tania Rascia
    • +
    • Noctis themes by Liviu Schera and contributors
    • +
    • Nord theme by Sven Greb
    • +
    • Owl themes by Sarah Drasner
    • +
    • Primer themes by GitHub Inc. and Myian
    • +
    • Snazzy Light theme by Florian Reuschel
    • +
    • Solarized themes by Ethan Schoonover
    • +
    • Tango themes by the Tango Desktop Project
    • +
    • Tomorrow themes by Chris Kempson
    • +
    + +

    Other themes were created for novelWriter:

    + +
      +
    • Aura Bright theme by Myian
    • +
    • Blue Streak themes by Veronica Berglyd Olsen
    • +
    • Castle themes by Myian
    • +
    • Chalky Soil theme by Myian
    • +
    • Chernozem theme by Myian
    • +
    • Cyberpunk Night theme by Anders Lemvigh
    • +
    • Floral themes by Myian
    • +
    • Full Moon theme by Myian
    • +
    • LCARS theme by Veronica Berglyd Olsen
    • +
    • Nordlicht theme by Myian
    • +
    • Otium themes by Myian
    • +
    • Paragon theme by Myian
    • +
    • Ruby themes by Myian
    • +
    • Selenium themes by Myian
    • +
    • Sepia themes by Myian
    • +
    • Sultana themes by Myian
    • +
    • Vivid themes by Myian
    • +
    • Warpgate theme by Myian
    • +
    • Waterlily themes by Myian

    Fonts

    diff --git a/novelwriter/assets/themes/aura.conf b/novelwriter/assets/themes/aura.conf new file mode 100644 index 00000000..a31ac7ba --- /dev/null +++ b/novelwriter/assets/themes/aura.conf @@ -0,0 +1,75 @@ +[Main] +name = Aura +mode = dark +author = Myian +credit = Dalton Menezes +url = https://github.com/daltonmenezes/aura-theme + +[Base] +base = #15141b +default = #edecee +faded = #888888 +red = #ff6767 +orange = #ff9341 +yellow = #ffca85 +green = #8ae986 +cyan = #61ffca +blue = #82e2ff +purple = #a277ff + +[Project] +root = default +folder = red +file = default +title = cyan +chapter = purple +scene = #f694ff +note = yellow +active = cyan +inactive = red +disabled = faded + +[Palette] +window = #0c0c11 +windowtext = default +base = base +alternatebase = #353249 +text = default +tooltipbase = yellow +tooltiptext = base +button = base:L125 +buttontext = default +brighttext = base +highlight = #603bb1 +highlightedtext = default +link = blue +linkvisited = blue +accent = #603bb1 + +[GUI] +helptext = faded:L125 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #29263c +link = blue +headertext = cyan +headertag = cyan:128 +emphasis = purple:L105 +dialog = cyan +altdialog = blue +note = yellow +hidden = faded +shortcode = yellow +keyword = #f694ff +tag = yellow +value = faded +optional = faded +spellcheckline = red +errorline = blue +replacetag = yellow +modifier = red +texthighlight = #3c3958 diff --git a/novelwriter/assets/themes/aura_bright.conf b/novelwriter/assets/themes/aura_bright.conf new file mode 100644 index 00000000..651e452c --- /dev/null +++ b/novelwriter/assets/themes/aura_bright.conf @@ -0,0 +1,73 @@ +[Main] +name = Aura Bright +mode = light +author = Myian + +[Base] +base = #f0e9f0 +default = #45425a +faded = #777777 +red = #ce4e4e +orange = #bd6b23 +yellow = #a97a20 +green = #378c1d +cyan = #098d82 +blue = #3371c2 +purple = #7752ce + +[Project] +root = default +folder = red +file = #7b788a +title = cyan +chapter = purple +scene = #c540cc +note = #c49134 +active = cyan +inactive = red +disabled = faded + +[Palette] +window = #e1dae2 +windowtext = default +base = base +alternatebase = #cec9d3 +text = default +tooltipbase = #ffecad +tooltiptext = default +button = base +buttontext = default +brighttext = base +highlight = purple +highlightedtext = base +link = blue +linkvisited = blue +accent = purple + +[GUI] +helptext = faded:D125 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #e1dae2 +link = blue +headertext = cyan +headertag = cyan:128 +emphasis = purple +dialog = cyan:D105 +altdialog = blue +note = yellow +hidden = faded +shortcode = yellow +keyword = #c540cc +tag = yellow +value = faded +optional = faded +spellcheckline = red +errorline = blue +replacetag = yellow +modifier = red +texthighlight = #c5bfca diff --git a/novelwriter/assets/themes/aura_soft.conf b/novelwriter/assets/themes/aura_soft.conf new file mode 100644 index 00000000..f8d58f70 --- /dev/null +++ b/novelwriter/assets/themes/aura_soft.conf @@ -0,0 +1,75 @@ +[Main] +name = Aura Soft +mode = dark +author = Myian +credit = Dalton Menezes +url = https://github.com/daltonmenezes/aura-theme + +[Base] +base = #21202e +default = #bdbdbd +faded = #6d6d6d +red = #c55858 +orange = #e0925d +yellow = #d8af79 +green = #6fb16d +cyan = #54c59f +blue = #6cb2c7 +purple = #8464c6 + +[Project] +root = default +folder = red +file = default +title = cyan +chapter = purple +scene = #f694ff +note = yellow +active = cyan +inactive = red +disabled = faded + +[Palette] +window = #191924 +windowtext = default +base = base +alternatebase = #353249 +text = default +tooltipbase = yellow +tooltiptext = base +button = base:L125 +buttontext = default +brighttext = base +highlight = #6a4da3 +highlightedtext = #edecee +link = blue +linkvisited = blue +accent = #6a4da3 + +[GUI] +helptext = faded:L125 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #29263c +link = blue +headertext = cyan +headertag = cyan:128 +emphasis = #a581ec +dialog = cyan +altdialog = blue +note = yellow +hidden = faded +shortcode = yellow +keyword = #c17ac8 +tag = yellow +value = faded +optional = faded +spellcheckline = #ff6767 +errorline = #82e2ff +replacetag = yellow +modifier = red +texthighlight = #3c3958 diff --git a/novelwriter/assets/themes/b2t_garden_dark.conf b/novelwriter/assets/themes/b2t_garden_dark.conf new file mode 100644 index 00000000..f5ecceba --- /dev/null +++ b/novelwriter/assets/themes/b2t_garden_dark.conf @@ -0,0 +1,75 @@ +[Main] +name = B2T Garden Dark +mode = dark +author = Myian +credit = Bram de Haan +url = https://base2t.one/demo/garden/ + +[Base] +base = #2b2c2a +default = #ece5df +faded = #828782 +red = #df6547 +orange = #db9257 +yellow = #e9e98e +green = #90d98c +cyan = #81c5de +blue = #9596ea +purple = #da72b2 + +[Project] +root = default +folder = orange +file = default +title = #1c8217 +chapter = #4cb946 +scene = #90d98c +note = faded +active = green +inactive = orange +disabled = #696d69 + +[Palette] +window = #1e1f1e +windowtext = default +base = base +alternatebase = #505350 +text = default +tooltipbase = #e0cab8 +tooltiptext = base +button = base +buttontext = default +brighttext = base +highlight = #1c8217 +highlightedtext = default +link = green +linkvisited = green +accent = #bd5d0f + +[GUI] +helptext = #aab1aa +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #3e403d +link = blue +headertext = #fbfaf8 +headertag = #828782 +emphasis = #3fac39 +dialog = #90d98c +altdialog = #4cb946 +note = #dd843c +hidden = faded +shortcode = #dd843c +keyword = #dd843c +tag = #dba070 +value = #dba070 +optional = faded +spellcheckline = #ff6f65 +errorline = green +replacetag = #dd843c +modifier = faded +texthighlight = green:80 diff --git a/novelwriter/assets/themes/b2t_garden_light.conf b/novelwriter/assets/themes/b2t_garden_light.conf new file mode 100644 index 00000000..ac2287b7 --- /dev/null +++ b/novelwriter/assets/themes/b2t_garden_light.conf @@ -0,0 +1,75 @@ +[Main] +name = B2T Garden Light +mode = light +author = Myian +credit = Bram de Haan +url = https://base2t.one/demo/garden/ + +[Base] +base = #fbfaf8 +default = #505350 +faded = #969c96 +red = #df6547 +orange = #db9257 +yellow = #cccc58 +green = #3fac39 +cyan = #5dacc9 +blue = #7678d4 +purple = #d362a8 + +[Project] +root = default +folder = orange +file = default +title = #6bcc66 +chapter = #3fac39 +scene = #1c8217 +note = faded +active = green +inactive = orange +disabled = #aab1aa + +[Palette] +window = #ece5df +windowtext = default +base = base +alternatebase = #dbd0c7 +text = default +tooltipbase = #dcf0db +tooltiptext = default +button = base +buttontext = default +brighttext = base +highlight = #4cb946 +highlightedtext = base +link = #1c8217 +linkvisited = #1c8217 +accent = #bd5d0f + +[GUI] +helptext = #696d69 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #f0ebe6 +link = blue +headertext = #2b2c2a +headertag = #828782 +emphasis = #4cb946 +dialog = #1c8217 +altdialog = #3fac39 +note = #d97726 +hidden = faded +shortcode = #d97726 +keyword = #d97726 +tag = #bd5d0f +value = #bd5d0f +optional = faded +spellcheckline = red +errorline = green +replacetag = #d97726 +modifier = faded +texthighlight = green:80 diff --git a/novelwriter/assets/themes/b2t_suburb_dark.conf b/novelwriter/assets/themes/b2t_suburb_dark.conf new file mode 100644 index 00000000..854808ca --- /dev/null +++ b/novelwriter/assets/themes/b2t_suburb_dark.conf @@ -0,0 +1,75 @@ +[Main] +name = B2T Suburb Dark +mode = dark +author = Myian +credit = Bram de Haan +url = https://base2t.one/demo/suburb/ + +[Base] +base = #25293d +default = #d7dbf3 +faded = #888eaf +red = #fb6fa9 +orange = #ff9b7d +yellow = #ffdf8f +green = #8bd59d +cyan = #76cde3 +blue = #8696fd +purple = #de95ee + +[Project] +root = default +folder = faded +file = #b0a6aa +title = #d14781 +chapter = #f764a1 +scene = #ff99c3 +note = #8696fd +active = red +inactive = faded +disabled = #575c79 + +[Palette] +window = #1e212f +windowtext = default +base = base +alternatebase = #444864 +text = default +tooltipbase = #d2d8fe +tooltiptext = base +button = base +buttontext = default +brighttext = base +highlight = red +highlightedtext = #1e202f +link = blue +linkvisited = blue +accent = red + +[GUI] +helptext = #9b9fb6 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = #b1b7d9 +line = #373a51 +link = blue +headertext = red +headertag = red:128 +emphasis = #ffffff +dialog = #fe81b5 +altdialog = #ffb3d2 +note = #a0acfe +hidden = blue +shortcode = #6375ee +keyword = faded +tag = #a0acfe +value = default +optional = faded +spellcheckline = #ff3b3b +errorline = #7083ff +replacetag = blue +modifier = faded +texthighlight = red:72 diff --git a/novelwriter/assets/themes/b2t_suburb_light.conf b/novelwriter/assets/themes/b2t_suburb_light.conf new file mode 100644 index 00000000..f31c8c29 --- /dev/null +++ b/novelwriter/assets/themes/b2t_suburb_light.conf @@ -0,0 +1,75 @@ +[Main] +name = B2T Suburb Light +mode = light +author = Myian +credit = Bram de Haan +url = https://base2t.one/demo/suburb/ + +[Base] +base = #fbf9fa +default = #444864 +faded = #878ba6 +red = #d14781 +orange = #d38152 +yellow = #cac63a +green = #378d60 +cyan = #308ab1 +blue = #5165e6 +purple = #9958be + +[Project] +root = default +folder = faded +file = #b0a5a9 +title = red +chapter = #f764a1 +scene = #ff99c3 +note = #8696fd +active = red +inactive = faded +disabled = #b6bad1 + +[Palette] +window = #e9e5e7 +windowtext = default +base = base +alternatebase = #d7cbd0 +text = default +tooltipbase = #d2d8fe +tooltiptext = default +button = #fbf9fa +buttontext = default +brighttext = base +highlight = #f764a1 +highlightedtext = base +link = blue +linkvisited = blue +accent = #fe81b5 + +[GUI] +helptext = #65697c +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = #6e7396 +line = #eae1e5 +link = blue +headertext = red +headertag = red:128 +emphasis = #1e202f +dialog = red +altdialog = #fb6fa9 +note = blue +hidden = blue +shortcode = #8696fd +keyword = faded +tag = blue +value = default +optional = faded +spellcheckline = #ff0000 +errorline = cyan +replacetag = blue +modifier = faded +texthighlight = #f25a9944 diff --git a/novelwriter/assets/themes/b4t_classic_o_dark.conf b/novelwriter/assets/themes/b4t_classic_o_dark.conf new file mode 100644 index 00000000..ce775630 --- /dev/null +++ b/novelwriter/assets/themes/b4t_classic_o_dark.conf @@ -0,0 +1,75 @@ +[Main] +name = B4T Classic-O Dark +mode = dark +author = Myian +credit = Bram de Haan +url = https://atelierbram.github.io/Base4Tone-prism/b4t_classic-o/dark/ + +[Base] +base = #22272f +default = #d0d5dd +faded = #707F98 +red = #f17eaa +orange = #eca277 +yellow = #f3cd91 +green = #7bd892 +cyan = #5ee3d1 +blue = #87b4fc +purple = #d19af4 + +[Project] +root = default +folder = #73a7fb +file = default +title = #039582 +chapter = #1BC3AD +scene = #93ebdf +note = #d19af4 +active = default +inactive = faded +disabled = #454f5f + +[Palette] +window = #191d23 +windowtext = default +base = base +alternatebase = #333b47 +text = default +tooltipbase = yellow +tooltiptext = base +button = #22272f +buttontext = default +brighttext = base +highlight = #1bbba5 +highlightedtext = base +link = blue +linkvisited = blue +accent = #1bbba5 + +[GUI] +helptext = default:192 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #333b47 +link = blue +headertext = cyan +headertag = cyan:128 +emphasis = #5fe2d1 +dialog = #87B4FC +altdialog = #5A96F6 +note = #d19af4 +hidden = faded +shortcode = #ba6cea +keyword = #877af0 +tag = #a095f3 +value = faded +optional = faded +spellcheckline = #ff688d +errorline = #5fe2d1 +replacetag = #877af0 +modifier = #ba6cea +texthighlight = cyan:56 diff --git a/novelwriter/assets/themes/b4t_classic_o_light.conf b/novelwriter/assets/themes/b4t_classic_o_light.conf new file mode 100644 index 00000000..0f9c94a1 --- /dev/null +++ b/novelwriter/assets/themes/b4t_classic_o_light.conf @@ -0,0 +1,75 @@ +[Main] +name = B4T Classic-O Light +mode = light +author = Myian +credit = Bram de Haan +url = https://atelierbram.github.io/Base4Tone-prism/b4t_classic-o/light/ + +[Base] +base = #f6f7f9 +default = #333b47 +faded = #718098 +red = #c03b6e +orange = #c46d26 +yellow = #bb8e46 +green = #3d9e29 +cyan = #039582 +blue = #4187f5 +purple = #aa52e0 + +[Project] +root = default +folder = blue +file = faded +title = #007061 +chapter = #039582 +scene = #1abba6 +note = #c17eed +active = default +inactive = faded +disabled = #acb5c3 + +[Palette] +window = #e7eaee +windowtext = default +base = base +alternatebase = #d0d5dd +text = default +tooltipbase = #ffe8c4 +tooltiptext = default +button = base +buttontext = default +brighttext = base +highlight = #0c9c89 +highlightedtext = base +link = blue +linkvisited = blue +accent = #0c9c89 + +[GUI] +helptext = default:192 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #dcdfe4 +link = blue +headertext = cyan +headertag = cyan:128 +emphasis = #008976 +dialog = #1a68e5 +altdialog = #1249a0 +note = #c17eed +hidden = faded +shortcode = purple +keyword = #6555e0 +tag = #9488f2 +value = faded +optional = faded +spellcheckline = #ff2259 +errorline = blue +replacetag = #7e6fe5 +modifier = purple +texthighlight = cyan:64 diff --git a/novelwriter/assets/themes/b4t_modern_c_dark.conf b/novelwriter/assets/themes/b4t_modern_c_dark.conf new file mode 100644 index 00000000..e348fbef --- /dev/null +++ b/novelwriter/assets/themes/b4t_modern_c_dark.conf @@ -0,0 +1,75 @@ +[Main] +name = B4T Modern-C Dark +mode = dark +author = Myian +credit = Bram de Haan +url = https://atelierbram.github.io/Base4Tone-prism/b4t_modern-c/dark/ + +[Base] +base = #25262c +default = #e7e7ed +faded = #9b9daa +red = #f17eaa +orange = #ffa978 +yellow = #f3cd91 +green = #46db6c +cyan = #5fe2d1 +blue = #8393f6 +purple = #d292f7 + +[Project] +root = default +folder = blue +file = default +title = #e64d87 +chapter = #f17ea9 +scene = #F6B1CB +note = #18ae9a +active = default +inactive = faded +disabled = #5d5f6f + +[Palette] +window = #1b1c20 +windowtext = default +base = base +alternatebase = #40424e +text = default +tooltipbase = yellow +tooltiptext = base +button = #2e2f37 +buttontext = default +brighttext = base +highlight = yellow +highlightedtext = base +link = blue +linkvisited = blue +accent = yellow + +[GUI] +helptext = default:192 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #373943 +link = blue +headertext = red +headertag = red:128 +emphasis = #f391b6 +dialog = yellow +altdialog = #e86296 +note = #929ff7 +hidden = faded +shortcode = #6f81f0 +keyword = #18ae9a +tag = #93ece0 +value = #c5cdfb +optional = faded +spellcheckline = #e64d87 +errorline = blue +replacetag = cyan +modifier = #6f81f0 +texthighlight = blue:72 diff --git a/novelwriter/assets/themes/b4t_modern_c_light.conf b/novelwriter/assets/themes/b4t_modern_c_light.conf new file mode 100644 index 00000000..b881167f --- /dev/null +++ b/novelwriter/assets/themes/b4t_modern_c_light.conf @@ -0,0 +1,75 @@ +[Main] +name = B4T Modern-C Light +mode = light +author = Myian +credit = Bram de Haan +url = https://atelierbram.github.io/Base4Tone-prism/b4t_modern-c/light/ + +[Base] +base = #f6f6f9 +default = #4a4c59 +faded = #848699 +red = #d22d6c +orange = #d55c0b +yellow = #d5880b +green = #269b43 +cyan = #17a592 +blue = #5b6feb +purple = #8e4fb3 + +[Project] +root = default +folder = blue +file = faded +title = #931f4c +chapter = #dd3f7c +scene = #eb75a2 +note = #18ae9a +active = default +inactive = faded +disabled = #BBBDC9 + +[Palette] +window = #e7e7ed +windowtext = default +base = base +alternatebase = #d1d2dc +text = default +tooltipbase = #f9e6c8 +tooltiptext = default +button = #ffffff +buttontext = default +brighttext = base +highlight = yellow +highlightedtext = base +link = blue +linkvisited = blue +accent = yellow + +[GUI] +helptext = default:192 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #e7e7ed +link = blue +headertext = red +headertag = red:128 +emphasis = #d53874 +dialog = #9f6303 +altdialog = #e86296 +note = blue +hidden = faded +shortcode = #354ddd +keyword = #007a6a +tag = #18ae9a +value = #8493F6 +optional = faded +spellcheckline = red +errorline = blue +replacetag = cyan +modifier = #354ddd +texthighlight = blue:64 diff --git a/novelwriter/assets/themes/blue_streak_dark.conf b/novelwriter/assets/themes/blue_streak_dark.conf new file mode 100644 index 00000000..49d19c42 --- /dev/null +++ b/novelwriter/assets/themes/blue_streak_dark.conf @@ -0,0 +1,75 @@ +[Main] +name = Blue Streak Dark +mode = dark +author = Veronica Berglyd Olsen +credit = Veronica Berglyd Olsen +url = https://github.com/vkbo/novelWriter + +[Base] +base = #2d2d2d +default = #cccccc +faded = #999999 +red = #ff6666 +orange = #ff9966 +yellow = #ffcc66 +green = #99cc99 +cyan = #66cccc +blue = #6699cc +purple = #cc99cc + +[Project] +root = blue +folder = default +file = default +title = blue:D150 +chapter = blue +scene = blue:L150 +note = yellow +active = blue:L150 +inactive = blue:D150 +disabled = faded + +[Palette] +window = base:L125 +windowtext = default +base = base +alternatebase = base:L150 +text = default +tooltipbase = yellow:L140 +tooltiptext = base:D150 +button = base +buttontext = default +brighttext = base +highlight = blue +highlightedtext = default:L120 +link = blue +linkvisited = blue +accent = blue + +[GUI] +helptext = blue:L125 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = blue:48 +link = blue +headertext = blue +headertag = blue:D150 +emphasis = blue:L150 +dialog = blue +altdialog = blue:L150 +note = blue:L150 +hidden = faded +shortcode = blue:D125 +keyword = blue:D125 +tag = blue +value = blue +optional = blue +spellcheckline = red +errorline = green +replacetag = blue:L150 +modifier = blue:D125 +texthighlight = blue:96 diff --git a/novelwriter/assets/themes/blue_streak_light.conf b/novelwriter/assets/themes/blue_streak_light.conf new file mode 100644 index 00000000..9f52d1a6 --- /dev/null +++ b/novelwriter/assets/themes/blue_streak_light.conf @@ -0,0 +1,75 @@ +[Main] +name = Blue Streak Light +mode = light +author = Veronica Berglyd Olsen +credit = Veronica Berglyd Olsen +url = https://github.com/vkbo/novelWriter + +[Base] +base = #fcfcfc +default = #303030 +faded = #6c6c6c +red = #cc3939 +orange = #cc7239 +yellow = #cca339 +green = #72a372 +cyan = #39a3a3 +blue = #3972a3 +purple = #a372a3 + +[Project] +root = blue +folder = faded +file = faded +title = blue:D150 +chapter = blue +scene = blue:L125 +note = yellow +active = blue:L115 +inactive = blue:D150 +disabled = faded + +[Palette] +window = base:D105 +windowtext = default +base = base +alternatebase = base:D110 +text = default +tooltipbase = yellow:L150 +tooltiptext = default:D150 +button = base +buttontext = default +brighttext = base +highlight = blue:L110 +highlightedtext = base +link = blue +linkvisited = blue +accent = blue + +[GUI] +helptext = blue +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = blue:32 +link = blue +headertext = blue +headertag = blue:D150 +emphasis = blue:L125 +dialog = blue +altdialog = blue:L125 +note = blue:L125 +hidden = faded +shortcode = blue:D125 +keyword = blue:D125 +tag = blue +value = blue +optional = blue +spellcheckline = red +errorline = green +replacetag = blue:L125 +modifier = blue:D125 +texthighlight = blue:96 diff --git a/novelwriter/assets/themes/castle_day.conf b/novelwriter/assets/themes/castle_day.conf new file mode 100644 index 00000000..ab57dcd8 --- /dev/null +++ b/novelwriter/assets/themes/castle_day.conf @@ -0,0 +1,73 @@ +[Main] +name = Castle by Day +mode = light +author = Myian + +[Base] +base = #f3eeec +default = #443d38 +faded = #8b827b +red = #c95e5e +orange = #d47643 +yellow = #b68730 +green = #4e8b30 +cyan = #539c9c +blue = #687fca +purple = #b261b4 + +[Project] +root = default +folder = blue +file = red +title = green +chapter = yellow +scene = orange +note = #9c9188 +active = default +inactive = faded +disabled = #b4aca5 + +[Palette] +window = base:D110 +windowtext = default +base = base +alternatebase = base:D118 +text = default +tooltipbase = yellow:L230 +tooltiptext = default +button = base +buttontext = default +brighttext = base +highlight = #cf7c6d +highlightedtext = base:L110 +link = #5068b6 +linkvisited = #5068b6 +accent = #cf7c6d + +[GUI] +helptext = default:L180 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = base:D110 +link = blue +headertext = yellow +headertag = yellow:160 +emphasis = #aa791e +dialog = green:D110 +altdialog = #378b8b +note = faded +hidden = faded +shortcode = faded +keyword = #ca4b4b +tag = orange +value = purple +optional = #dd8c60 +spellcheckline = #e94444 +errorline = #378b8b +replacetag = blue +modifier = #963399 +texthighlight = red:80 diff --git a/novelwriter/assets/themes/castle_night.conf b/novelwriter/assets/themes/castle_night.conf new file mode 100644 index 00000000..c4245236 --- /dev/null +++ b/novelwriter/assets/themes/castle_night.conf @@ -0,0 +1,73 @@ +[Main] +name = Castle by Night +mode = dark +author = Myian + +[Base] +base = #1c1f28 +default = #d6d6d6 +faded = #878a91 +red = #fa8080 +orange = #ee9c70 +yellow = #fadd8d +green = #afd49d +cyan = #a4dad7 +blue = #9cb2ec +purple = #d296d4 + +[Project] +root = default +folder = blue +file = red +title = green +chapter = yellow +scene = orange +note = #9b9ea5 +active = default +inactive = faded +disabled = #4b4f5c + +[Palette] +window = base:D130 +windowtext = default +base = base +alternatebase = base:L160 +text = default +tooltipbase = yellow:L120 +tooltiptext = base +button = base:L110 +buttontext = default +brighttext = base +highlight = red:D150 +highlightedtext = default:L110 +link = blue +linkvisited = blue +accent = red:D150 + +[GUI] +helptext = faded +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = base:L150 +link = blue +headertext = yellow +headertag = yellow:160 +emphasis = yellow +dialog = green +altdialog = cyan +note = faded +hidden = faded +shortcode = faded +keyword = red +tag = orange +value = #e4b2e6 +optional = orange:L120 +spellcheckline = #ff5d5d +errorline = cyan +replacetag = blue +modifier = purple +texthighlight = red:72 diff --git a/novelwriter/assets/themes/chalky_soil.conf b/novelwriter/assets/themes/chalky_soil.conf new file mode 100644 index 00000000..5e90b992 --- /dev/null +++ b/novelwriter/assets/themes/chalky_soil.conf @@ -0,0 +1,73 @@ +[Main] +name = Chalky Soil +mode = light +author = Myian + +[Base] +base = #f0f0f0 +default = #494949 +faded = #8a8a8a +red = #c45e6c +orange = #c9700b +yellow = #c09227 +green = #5f8f5a +cyan = #3f9b96 +blue = #417db6 +purple = #975ea8 + +[Project] +root = default +folder = green +file = faded:D105 +title = red +chapter = orange +scene = yellow +note = #6293c2 +active = green +inactive = faded +disabled = faded:L135 + +[Palette] +window = base:D108 +windowtext = default +base = base +alternatebase = base:D115 +text = default +tooltipbase = #f1dcab +tooltiptext = default +button = base:D110 +buttontext = default +brighttext = base +highlight = green +highlightedtext = base +link = blue +linkvisited = blue +accent = green + +[GUI] +helptext = default:L140 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = faded:48 +link = blue +headertext = red +headertag = red:128 +emphasis = #488843 +dialog = #b95a00 +altdialog = #b18010 +note = faded +hidden = faded +shortcode = blue +keyword = blue +tag = #276096 +value = #276096 +optional = faded +spellcheckline = #f0344d +errorline = cyan +replacetag = purple +modifier = blue +texthighlight = green:80 diff --git a/novelwriter/assets/themes/chernozem.conf b/novelwriter/assets/themes/chernozem.conf new file mode 100644 index 00000000..56f80f66 --- /dev/null +++ b/novelwriter/assets/themes/chernozem.conf @@ -0,0 +1,73 @@ +[Main] +name = Chernozem +mode = dark +author = Myian + +[Base] +base = #2c2525 +default = #dad0ca +faded = #857972 +red = #e06c6c +orange = #ec935f +yellow = #e5cc91 +green = #6e9e6a +cyan = #72b69f +blue = #7c9fbd +purple = #c899d6 + +[Project] +root = default +folder = green +file = default +title = red +chapter = orange +scene = yellow +note = blue +active = green +inactive = faded +disabled = #504742 + +[Palette] +window = #241d1d +windowtext = default +base = base +alternatebase = faded:80 +text = default +tooltipbase = yellow +tooltiptext = base +button = #362c2b +buttontext = default +brighttext = base +highlight = #6ca167 +highlightedtext = #241d1d +link = blue +linkvisited = blue +accent = #5a8d55 + +[GUI] +helptext = #a39891 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = faded:48 +link = blue +headertext = red +headertag = red:128 +emphasis = #85c47f +dialog = orange +altdialog = yellow +note = faded +hidden = faded +shortcode = blue +keyword = blue +tag = #a1c4e4 +value = #a1c4e4 +optional = faded +spellcheckline = #ff5555 +errorline = cyan +replacetag = purple +modifier = blue +texthighlight = green:80 diff --git a/novelwriter/assets/themes/cyberpunk_night.conf b/novelwriter/assets/themes/cyberpunk_night.conf index 125cab45..d433dabe 100644 --- a/novelwriter/assets/themes/cyberpunk_night.conf +++ b/novelwriter/assets/themes/cyberpunk_night.conf @@ -1,48 +1,74 @@ [Main] -name = Cyberpunk Night -description = A taste of the future 80s -author = Anders Lemvigh -url = https://github.com/alemvigh -license = CC BY-SA 4.0 -licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ +name = Cyberpunk Night +mode = dark +author = Anders Lemvigh +url = https://github.com/alemvigh -[Icons] -default = 136, 136, 136 -faded = 97, 97, 97 -red = 242, 72, 23 -orange = 255, 150, 10 -yellow = 255, 255, 0 -green = 0, 255, 0 -aqua = 0, 255, 255 -blue = 77, 77, 255 -purple = 50, 0, 180 +[Base] +base = #000000 +default = #888888 +faded = #616161 +red = #f24817 +orange = #ff960a +yellow = #ffff00 +green = #00ff00 +cyan = #00ffff +blue = #4d4dff +purple = #320064 [Project] -root = 77, 77, 255 -folder = 255, 255, 0 -file = 136, 136, 136 -title = 0, 255, 0 -chapter = 242, 72, 23 -scene = 77, 77, 255 -note = 255, 255, 0 +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded [Palette] -window = 0, 0, 0 -windowtext = 150, 150, 150 -base = 0, 0, 0 -alternatebase = 40, 40, 40 -text = 150, 150, 150 -tooltipbase = 40, 20, 70 -tooltiptext = 255, 255, 255 -button = 5, 0, 10 -buttontext = 150, 150, 150 -brighttext = 255, 255, 255 -highlight = 50, 30, 80 -highlightedtext = 255, 255, 255 -link = 77, 77, 255 -linkvisited = 50, 0, 80 +window = base +windowtext = #969696 +base = base +alternatebase = #282828 +text = #969696 +tooltipbase = #281446 +tooltiptext = #ffffff +button = #05000a +buttontext = #969696 +brighttext = #ffffff +highlight = #321e50 +highlightedtext = #ffffff +link = blue +linkvisited = #320050 +accent = #321e50 [GUI] -helptext = 97, 97, 97 -fadedtext = 97, 97, 97 -errortext = 242, 72, 23 +helptext = faded +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = #969696 +line = #282828 +link = blue +headertext = #ffffff +headertag = purple +emphasis = cyan +dialog = green +altdialog = #008cff +note = #969696 +hidden = #4d4d64 +shortcode = yellow +keyword = #ff64ff +tag = orange +value = orange +spellcheckline = red +errorline = #bada04 +replacetag = #0000b4 +modifier = #908eb0 +optional = #b4b4b4 +texthighlight = #ffff8460 diff --git a/novelwriter/assets/themes/default.conf b/novelwriter/assets/themes/default.conf deleted file mode 100644 index 9cd9cd5b..00000000 --- a/novelwriter/assets/themes/default.conf +++ /dev/null @@ -1,3 +0,0 @@ -[Main] -name = Qt Default Theme -description = Qt standard colours diff --git a/novelwriter/assets/themes/default_dark.conf b/novelwriter/assets/themes/default_dark.conf index 76916604..1f7359bb 100644 --- a/novelwriter/assets/themes/default_dark.conf +++ b/novelwriter/assets/themes/default_dark.conf @@ -1,49 +1,75 @@ [Main] -name = Default Dark Theme -description = The novelWriter standard dark theme -author = Veronica Berglyd Olsen -credit = Veronica Berglyd Olsen -url = https://github.com/vkbo/novelWriter -license = CC BY-SA 4.0 -licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ +name = Default Dark Theme +mode = dark +author = Veronica Berglyd Olsen +credit = Veronica Berglyd Olsen +url = https://github.com/vkbo/novelWriter -[Icons] -default = 184, 184, 184 -faded = 148, 148, 148 -red = 242, 119, 122 -orange = 249, 145, 57 -yellow = 255, 204, 102 -green = 153, 204, 153 -aqua = 102, 204, 204 -blue = 102, 153, 204 -purple = 204, 153, 204 +[Base] +base = #373737 +default = #cccccc +faded = #949494 +red = #ff6666 +orange = #ff9966 +yellow = #ffcc66 +green = #99cc99 +cyan = #66cccc +blue = #6699cc +purple = #cc99cc [Project] -root = 102, 153, 204 -folder = 255, 204, 102 -file = 184, 184, 184 -title = 153, 204, 153 -chapter = 242, 119, 122 -scene = 102, 153, 204 -note = 255, 204, 102 +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded [Palette] -window = 54, 54, 54 -windowtext = 204, 204, 204 -base = 62, 62, 62 -alternatebase = 78, 78, 78 -text = 204, 204, 204 -tooltipbase = 255, 255, 192 -tooltiptext = 21, 21, 13 -button = 62, 62, 62 -buttontext = 204, 204, 204 -brighttext = 62, 62, 62 -highlight = 44, 152, 247 -highlightedtext = 255, 255, 255 -link = 102, 153, 204 -linkvisited = 102, 153, 204 +window = base:L125 +windowtext = default +base = base +alternatebase = #4e4e4e +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = base +buttontext = default +brighttext = base +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue +accent = #2c98f7 [GUI] -helptext = 164, 164, 164 -fadedtext = 148, 148, 148 -errortext = 255, 164, 164 +helptext = #a4a4a4 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = default:32 +link = blue +headertext = green +headertag = green:D150 +emphasis = orange +dialog = blue +altdialog = red +note = yellow +hidden = faded +shortcode = blue +keyword = red +tag = green +value = green +optional = blue +spellcheckline = red +errorline = green +replacetag = green +modifier = blue +texthighlight = yellow:72 diff --git a/novelwriter/assets/themes/default_light.conf b/novelwriter/assets/themes/default_light.conf index 29b4b98a..da7784f5 100644 --- a/novelwriter/assets/themes/default_light.conf +++ b/novelwriter/assets/themes/default_light.conf @@ -1,49 +1,75 @@ [Main] -name = Default Light Theme -description = The novelWriter standard light theme -author = Veronica Berglyd Olsen -credit = Veronica Berglyd Olsen -url = https://github.com/vkbo/novelWriter -license = CC BY-SA 4.0 -licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ +name = Default Light Theme +mode = light +author = Veronica Berglyd Olsen +credit = Veronica Berglyd Olsen +url = https://github.com/vkbo/novelWriter -[Icons] -default = 72, 72, 72 -faded = 108, 108, 108 -red = 240, 40, 41 -orange = 245, 135, 31 -yellow = 234, 183, 0 -green = 113, 140, 0 -aqua = 62, 153, 159 -blue = 66, 113, 174 -purple = 137, 89, 168 +[Base] +base = #fcfcfc +default = #303030 +faded = #6c6c6c +red = #a62a2d +orange = #b36829 +yellow = #a39c34 +green = #296629 +cyan = #269999 +blue = #3a70a6 +purple = #b35ab3 [Project] -root = 66, 113, 174 -folder = 234, 183, 0 -file = 72, 72, 72 -title = 113, 140, 0 -chapter = 240, 40, 41 -scene = 66, 113, 174 -note = 234, 183, 0 +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded [Palette] -window = 239, 239, 239 -windowtext = 0, 0, 0 -base = 255, 255, 255 -alternatebase = 224, 224, 224 -text = 0, 0, 0 -tooltipbase = 255, 255, 220 -tooltiptext = 0, 0, 0 -button = 239, 239, 239 -buttontext = 0, 0, 0 -brighttext = 255, 255, 255 -highlight = 48, 135, 198 -highlightedtext = 255, 255, 255 -link = 66, 113, 174 -linkvisited = 66, 113, 174 +window = base:D105 +windowtext = default +base = base +alternatebase = #e0e0e0 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #efefef +buttontext = default +brighttext = base +highlight = #3087c6 +highlightedtext = base +link = blue +linkvisited = blue +accent = #3087c6 [GUI] -helptext = 92, 92, 92 -fadedtext = 108, 108, 108 -errortext = 255, 92, 92 +helptext = #5c5c5c +fadedtext = #6c6c6c +errortext = red + +[Syntax] +background = base +text = default +line = default:32 +link = blue +headertext = green +headertag = green:L135 +emphasis = orange +dialog = blue +altdialog = red +note = yellow:D125 +hidden = faded +shortcode = blue +keyword = red +tag = green +value = green +optional = blue +spellcheckline = red +errorline = green +replacetag = green +modifier = blue +texthighlight = yellow:72 diff --git a/novelwriter/assets/themes/dracula.conf b/novelwriter/assets/themes/dracula.conf index 5805b84b..04fe9949 100644 --- a/novelwriter/assets/themes/dracula.conf +++ b/novelwriter/assets/themes/dracula.conf @@ -1,17 +1,16 @@ [Main] -name = Dracula -description = A dark theme with bright colours -author = Veronica Berglyd Olsen (adaptation) -credit = Zeno Rocha -url = https://draculatheme.com -license = MIT -licenseurl = https://github.com/dracula/dracula-theme/blob/main/LICENSE +name = Dracula +mode = dark +author = Veronica Berglyd Olsen (adaptation) +credit = Zeno Rocha +url = https://draculatheme.com ## # Colours: +# See: https://spec.draculatheme.com/ # Background = 282a36 : 40, 42, 54 # Foreground = f8f8f2 : 248, 248, 242 -# Current Line = 44475A : 68, 71, 90 +# Current Line = 44475a : 68, 71, 90 # Comment = 6272a4 : 98, 114, 164 # Cyan = 8be9fd : 139, 233, 253 # Green = 50fa7b : 80, 250, 123 @@ -22,43 +21,71 @@ licenseurl = https://github.com/dracula/dracula-theme/blob/main/LICENSE # Yellow = f1fa8c : 241, 250, 140 ## -[Icons] -default = 230, 230, 224 -faded = 98, 114, 164 -red = 255, 85, 85 -orange = 255, 184, 108 -yellow = 241, 250, 140 -green = 80, 250, 123 -aqua = 139, 233, 253 -blue = 147, 207, 249 -purple = 189, 147, 249 +[Base] +base = #282a36 +default = #e6e6e0 +faded = #6272a4 +red = #ff5555 +orange = #ffb86c +yellow = #f1fa8c +green = #50fa7b +cyan = #8be9fd +blue = #93cff9 +purple = #bd93f9 [Project] -root = 189, 147, 249 -folder = 241, 250, 140 -file = 230, 230, 224 -title = 80, 250, 123 -chapter = 255, 85, 85 -scene = 139, 233, 253 -note = 241, 250, 140 +root = purple +folder = yellow +file = default +title = green +chapter = red +scene = cyan +note = yellow +active = green +inactive = red +disabled = faded [Palette] -window = 68, 71, 90 -windowtext = 248, 248, 242 -base = 40, 42, 54 -alternatebase = 51, 54, 69 -text = 248, 248, 242 -tooltipbase = 241, 250, 140 -tooltiptext = 40, 42, 54 -button = 80, 83, 105 -buttontext = 248, 248, 242 -brighttext = 68, 71, 90 -highlight = 166, 129, 218 -highlightedtext = 248, 248, 242 -link = 139, 233, 253 -linkvisited = 139, 233, 253 +window = #44475a +windowtext = #f8f8f2 +base = base +alternatebase = #333645 +text = #f8f8f2 +tooltipbase = yellow +tooltiptext = base +button = #505369 +buttontext = #f8f8f2 +brighttext = base +highlight = #a681da +highlightedtext = #f8f8f2 +link = cyan +linkvisited = cyan +accent = #a681da [GUI] -helptext = 204, 172, 249 -fadedtext = 98, 114, 164 -errortext = 255, 85, 85 +helptext = #ccacf9 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = #f8f8f2 +line = base:L150 +link = #ff79c6 +headertext = purple +headertag = purple:D150 +emphasis = orange +dialog = green +altdialog = yellow +note = #ffcce9 +hidden = faded +shortcode = cyan +keyword = #ff79c6 +tag = orange +value = orange +optional = green +spellcheckline = red +errorline = green +replacetag = yellow +modifier = cyan +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/espresso.conf b/novelwriter/assets/themes/espresso.conf new file mode 100644 index 00000000..3d222896 --- /dev/null +++ b/novelwriter/assets/themes/espresso.conf @@ -0,0 +1,75 @@ +[Main] +name = Espresso +mode = dark +author = Veronica Berglyd Olsen +credit = Sainnhe Park and Antoine Cotten +url = https://github.com/sainnhe/sonokai + +[Base] +base = #2f2726 +default = #e5e4e2 +faded = #96817b +red = #f86882 +orange = #f08d71 +yellow = #f0c66f +green = #a6cd77 +cyan = #81d0c9 +blue = #89a8e0 +purple = #9fa0e1 + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = base:L125 +windowtext = default +base = base +alternatebase = #51403c +text = default +tooltipbase = yellow:L125 +tooltiptext = base +button = base +buttontext = default +brighttext = base +highlight = #4e5f80 +highlightedtext = default +link = blue +linkvisited = blue +accent = blue + +[GUI] +helptext = blue +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #392e2c +link = blue +headertext = yellow:L125 +headertag = faded +emphasis = orange +dialog = yellow +altdialog = orange +note = yellow:L125 +hidden = faded +shortcode = green +keyword = red +tag = green +value = orange +optional = yellow +spellcheckline = red +errorline = green +replacetag = cyan +modifier = blue +texthighlight = yellow:108 diff --git a/novelwriter/assets/themes/everforest_dark.conf b/novelwriter/assets/themes/everforest_dark.conf new file mode 100644 index 00000000..ff10cf93 --- /dev/null +++ b/novelwriter/assets/themes/everforest_dark.conf @@ -0,0 +1,75 @@ +[Main] +name = Everforest Dark +mode = dark +author = Myian +credit = Sainnhe Park +url = https://github.com/sainnhe/everforest + +[Base] +base = #272e33 +default = #d3c6aa +faded = #859289 +red = #e67e80 +orange = #e69875 +yellow = #dbbc7f +green = #a7c080 +cyan = #83c092 +blue = #7fbbb3 +purple = #d699b6 + +[Project] +root = default +folder = orange +file = red +title = green +chapter = blue +scene = purple +note = faded +active = cyan +inactive = orange +disabled = faded + +[Palette] +window = #1e2326 +windowtext = default +base = base +alternatebase = #374145 +text = default +tooltipbase = yellow +tooltiptext = base +button = #2e383c +buttontext = default +brighttext = base +highlight = #a7c080 +highlightedtext = base +link = blue +linkvisited = blue +accent = #a7c080 + +[GUI] +helptext = #a9a49d +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #343f44 +link = blue +headertext = cyan +headertag = cyan:128 +emphasis = blue +dialog = green +altdialog = cyan +note = purple +hidden = faded +shortcode = orange +keyword = orange +tag = purple +value = faded +optional = faded +spellcheckline = red +errorline = cyan +replacetag = purple +modifier = red +texthighlight = cyan:72 diff --git a/novelwriter/assets/themes/everforest_light.conf b/novelwriter/assets/themes/everforest_light.conf new file mode 100644 index 00000000..901379ed --- /dev/null +++ b/novelwriter/assets/themes/everforest_light.conf @@ -0,0 +1,75 @@ +[Main] +name = Everforest Light +mode = light +author = Myian +credit = Sainnhe Park +url = https://github.com/sainnhe/everforest + +[Base] +base = #fffbef +default = #5c6a72 +faded = #939f91 +red = #f85552 +orange = #f57d26 +yellow = #dfa000 +green = #8da101 +cyan = #35a77c +blue = #3a94c5 +purple = #df69ba + +[Project] +root = default +folder = orange +file = red +title = green +chapter = blue +scene = purple +note = faded +active = cyan +inactive = orange +disabled = faded + +[Palette] +window = #f2efdf +windowtext = default +base = base +alternatebase = #e8e5d5 +text = default +tooltipbase = #fef2d5 +tooltiptext = default +button = #f8f5e4 +buttontext = default +brighttext = base +highlight = #93b259 +highlightedtext = base +link = blue +linkvisited = blue +accent = #93b259 + +[GUI] +helptext = #829181 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #edeada +link = blue +headertext = cyan +headertag = cyan:128 +emphasis = blue +dialog = green +altdialog = cyan +note = purple +hidden = faded +shortcode = orange +keyword = orange +tag = purple +value = faded +optional = faded +spellcheckline = red +errorline = cyan +replacetag = purple +modifier = red +texthighlight = cyan:64 diff --git a/novelwriter/assets/themes/floral_daydream.conf b/novelwriter/assets/themes/floral_daydream.conf new file mode 100644 index 00000000..ecee703f --- /dev/null +++ b/novelwriter/assets/themes/floral_daydream.conf @@ -0,0 +1,73 @@ +[Main] +name = Floral Daydream +mode = light +author = Myian + +[Base] +base = #fff4f7 +default = #ac3b6f +faded = #c285aa +red = #e76b6d +orange = #e08b46 +yellow = #cca333 +green = #59976b +cyan = #5d9ea3 +blue = #658cc5 +purple = #9d77c9 + +[Project] +root = default +folder = yellow +file = orange +title = blue +chapter = purple +scene = red +note = #6dad7f +active = green +inactive = faded +disabled = #e2bcce + +[Palette] +window = #ffe3ea +windowtext = default +base = base +alternatebase = #ffc7d8 +text = default +tooltipbase = #fff0ba +tooltiptext = #413309 +button = #ffffff +buttontext = default +brighttext = base +highlight = #e4688d +highlightedtext = #ffffff +link = #4781d8 +linkvisited = #4781d8 +accent = #e4688d + +[GUI] +helptext = #d14a73 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = #974169 +line = #ffdbe4 +link = #4781d8 +headertext = #4781d8 +headertag = #4781d880 +emphasis = #8152b8 +dialog = #eb4073 +altdialog = #4781d8 +note = #31924c +hidden = faded +shortcode = #31924c +keyword = #1a6d32 +tag = #31924c +value = #ce7b1d +optional = faded +spellcheckline = #ff0000 +errorline = #1a6d32 +replacetag = #31924c +modifier = #1a6d32 +texthighlight = #bae2c5 diff --git a/novelwriter/assets/themes/floral_midnight.conf b/novelwriter/assets/themes/floral_midnight.conf new file mode 100644 index 00000000..c05bb27d --- /dev/null +++ b/novelwriter/assets/themes/floral_midnight.conf @@ -0,0 +1,73 @@ +[Main] +name = Floral Midnight +mode = dark +author = Myian + +[Base] +base = #212130 +default = #d3cfe7 +faded = #948dac +red = #ff7b7d +orange = #ffad6e +yellow = #eedb94 +green = #79bd8b +cyan = #8bcac8 +blue = #91b7ff +purple = #b78ff0 + +[Project] +root = default +folder = yellow +file = orange +title = blue +chapter = purple +scene = red +note = green +active = green +inactive = faded +disabled = #55516d + +[Palette] +window = #181825 +windowtext = default +base = base +alternatebase = #3e395a +text = default +tooltipbase = yellow +tooltiptext = yellow:D400 +button = #1f1d35 +buttontext = default +brighttext = base +highlight = #705a94 +highlightedtext = #ffffff +link = blue +linkvisited = blue +accent = #b060c0 + +[GUI] +helptext = faded +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #3e395a +link = blue +headertext = blue +headertag = blue:128 +emphasis = #ff9dd9 +dialog = #d19df3 +altdialog = blue +note = #65ca80 +hidden = faded +shortcode = #65ca80 +keyword = #a6fdbd +tag = #65ca80 +value = yellow +optional = faded +spellcheckline = #ff4949 +errorline = #65ca80 +replacetag = #a6fdbd +modifier = #a6fdbd +texthighlight = green:96 diff --git a/novelwriter/assets/themes/full_moon.conf b/novelwriter/assets/themes/full_moon.conf new file mode 100644 index 00000000..c6e2c864 --- /dev/null +++ b/novelwriter/assets/themes/full_moon.conf @@ -0,0 +1,73 @@ +[Main] +name = Full Moon +mode = light +author = Myian + +[Base] +base = #ffffff +default = #37404e +faded = #93a2a8 +red = #d35654 +orange = #d17a09 +yellow = #caac00 +green = #579220 +cyan = #1b948a +blue = #2d78b9 +purple = #c45fb3 + +[Project] +root = default +folder = orange +file = purple +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = #e7ebee +windowtext = default +base = base +alternatebase = #d4d9dd +text = default +tooltipbase = #ffeea6 +tooltiptext = default +button = base +buttontext = default +brighttext = base +highlight = #5495ce +highlightedtext = base +link = blue +linkvisited = blue +accent = #5495ce + +[GUI] +helptext = #586a72 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = #586a72 +line = #e7ebee +link = blue +headertext = purple +headertag = purple:128 +emphasis = blue +dialog = #0e2a35 +altdialog = cyan +note = green +hidden = faded +shortcode = #c5931f +keyword = #c5931f +tag = red +value = orange +optional = #c5931f +spellcheckline = #f73c3f +errorline = cyan +replacetag = purple +modifier = #c5931f +texthighlight = blue:80 diff --git a/novelwriter/assets/themes/grey_dark.conf b/novelwriter/assets/themes/grey_dark.conf new file mode 100644 index 00000000..665c7a1f --- /dev/null +++ b/novelwriter/assets/themes/grey_dark.conf @@ -0,0 +1,75 @@ +[Main] +name = Grey Dark +mode = dark +author = Veronica Berglyd Olsen +credit = Veronica Berglyd Olsen +url = https://github.com/vkbo/novelWriter + +[Base] +base = #3e3e3e +default = #c8c8c8 +faded = #949494 +red = #f2777a +orange = #f99139 +yellow = #ffcc66 +green = #99cc99 +cyan = #66cccc +blue = #6699cc +purple = #cc99cc + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = #363636 +windowtext = default +base = base +alternatebase = #4e4e4e +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = base +buttontext = default +brighttext = base +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue +accent = #2c98f7 + +[GUI] +helptext = default:D120 +fadedtext = faded +errortext = red + +[Syntax] +background = #363636 +text = default +line = default:32 +link = default +headertext = default:L115 +headertag = default:D125 +emphasis = default +dialog = default +altdialog = default +note = default +hidden = faded +shortcode = default:L115 +keyword = default:L115 +tag = default +value = default +optional = default:L115 +spellcheckline = red +errorline = green +replacetag = default:L115 +modifier = default:L115 +texthighlight = #ffffff40 diff --git a/novelwriter/assets/themes/grey_light.conf b/novelwriter/assets/themes/grey_light.conf new file mode 100644 index 00000000..2106e5bf --- /dev/null +++ b/novelwriter/assets/themes/grey_light.conf @@ -0,0 +1,75 @@ +[Main] +name = Grey Light +mode = light +author = Veronica Berglyd Olsen +credit = Veronica Berglyd Olsen +url = https://github.com/vkbo/novelWriter + +[Base] +base = #ffffff +default = #282828 +faded = #6c6c6c +red = #a62a2d +orange = #b36829 +yellow = #a68542 +green = #296629 +cyan = #269999 +blue = #3a70a6 +purple = #b35ab3 + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = #efefef +windowtext = default +base = base +alternatebase = #e0e0e0 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #efefef +buttontext = default +brighttext = base +highlight = #3087c6 +highlightedtext = base +link = blue +linkvisited = blue +accent = #3087c6 + +[GUI] +helptext = default:L225 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = default:32 +link = default +headertext = default:D200 +headertag = default:L400 +emphasis = default +dialog = default +altdialog = default +note = default +hidden = faded +shortcode = default:D200 +keyword = default:D200 +tag = default +value = default +optional = default:D200 +spellcheckline = red +errorline = green +replacetag = default:D200 +modifier = default:D200 +texthighlight = #00000040 diff --git a/novelwriter/assets/themes/horizon_dark.conf b/novelwriter/assets/themes/horizon_dark.conf new file mode 100644 index 00000000..085e8c6d --- /dev/null +++ b/novelwriter/assets/themes/horizon_dark.conf @@ -0,0 +1,75 @@ +[Main] +name = Horizon Dark +mode = dark +author = Myian +credit = Jonathan Olaleye +url = https://github.com/jolaleye/horizon-theme-vscode + +[Base] +base = #1c1e26 +default = #d5d8da +faded = #6c6f93 +red = #e95678 +orange = #f09383 +yellow = #fac29a +green = #84c969 +cyan = #27d796 +blue = #25b2bc +purple = #b877db + +[Project] +root = default +folder = purple +file = yellow +title = blue +chapter = red +scene = orange +note = faded +active = yellow +inactive = faded +disabled = #404263 + +[Palette] +window = base +windowtext = default +base = base +alternatebase = #2e303e +text = default +tooltipbase = yellow +tooltiptext = base +button = #232530 +buttontext = default +brighttext = base +highlight = #fab28e +highlightedtext = base +link = #21bfc2 +linkvisited = #21bfc2 +accent = #FAB28E + +[GUI] +helptext = default:192 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #232530 +link = blue +headertext = red +headertag = red:128 +emphasis = yellow +dialog = orange +altdialog = red +note = blue +hidden = faded +shortcode = blue +keyword = purple +tag = blue +value = faded +optional = faded +spellcheckline = #ff3d5d +errorline = cyan +replacetag = blue +modifier = purple +texthighlight = orange:80 diff --git a/novelwriter/assets/themes/horizon_light.conf b/novelwriter/assets/themes/horizon_light.conf new file mode 100644 index 00000000..cee0a7f1 --- /dev/null +++ b/novelwriter/assets/themes/horizon_light.conf @@ -0,0 +1,75 @@ +[Main] +name = Horizon Bright +mode = light +author = Myian +credit = Jonathan Olaleye +url = https://github.com/jolaleye/horizon-theme-vscode + +[Base] +base = #fdf0ed +default = #1a1c23 +faded = #99827d +red = #da103f +orange = #dc3318 +yellow = #f77d26 +green = #5fa306 +cyan = #0fac72 +blue = #1d8991 +purple = #8931B9 + +[Project] +root = default +folder = purple +file = yellow +title = blue +chapter = red +scene = orange +note = faded +active = #eb834f +inactive = faded +disabled = faded:L125 + +[Palette] +window = base +windowtext = default +base = base +alternatebase = #fadad1 +text = default +tooltipbase = #f9cbbe +tooltiptext = default +button = #fadad1 +buttontext = default +brighttext = base +highlight = #e68656 +highlightedtext = base +link = #1eaeae +linkvisited = #1eaeae +accent = #f6661e + +[GUI] +helptext = default:192 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #ffe7e0 +link = blue +headertext = red +headertag = red:128 +emphasis = #f6661e +dialog = orange +altdialog = red +note = blue +hidden = faded +shortcode = blue +keyword = purple +tag = blue +value = faded +optional = faded +spellcheckline = #ff3d5d +errorline = #1eb980 +replacetag = blue +modifier = purple +texthighlight = orange:64 diff --git a/novelwriter/assets/themes/lcars.conf b/novelwriter/assets/themes/lcars.conf new file mode 100644 index 00000000..ee553602 --- /dev/null +++ b/novelwriter/assets/themes/lcars.conf @@ -0,0 +1,75 @@ +[Main] +name = LCARS +mode = dark +author = Veronica Berglyd Olsen +credit = Star Trek TNG +url = https://github.com/vkbo/novelWriter + +[Base] +base = #101010 +default = #c7c7c7 +faded = #757575 +red = #e24d5f +orange = #ff9902 +yellow = #ffdc59 +green = #54e985 +cyan = #7ffff2 +blue = #009cff +purple = #8aa5ff + +[Project] +root = orange +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = blue +inactive = red +disabled = faded + +[Palette] +window = base:L180 +windowtext = default +base = base +alternatebase = base:L300 +text = default +tooltipbase = yellow:L140 +tooltiptext = base +button = base:L140 +buttontext = default +brighttext = base +highlight = purple +highlightedtext = base +link = purple +linkvisited = purple +accent = orange + +[GUI] +helptext = blue +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = yellow:48 +link = purple +headertext = orange +headertag = red +emphasis = orange +dialog = yellow +altdialog = green +note = purple +hidden = faded +shortcode = purple +keyword = red +tag = orange +value = orange +optional = purple +spellcheckline = red +errorline = red +replacetag = purple +modifier = red +texthighlight = orange:96 diff --git a/novelwriter/assets/themes/light_owl.conf b/novelwriter/assets/themes/light_owl.conf new file mode 100644 index 00000000..6a43afc0 --- /dev/null +++ b/novelwriter/assets/themes/light_owl.conf @@ -0,0 +1,95 @@ +[Main] +name = Light Owl +mode = light +author = Veronica Berglyd Olsen +credit = Sarah Drasner +url = https://github.com/sdras/night-owl-vscode-theme + +## +# Theme: Light Owl +# Source: https://github.com/sdras/night-owl-vscode-theme +# Credit: Sarah Drasner +## +# Colours: +# Background = #fbfbfb +# Current Line = #eaeaea +# Selection = #e0e0e0 +# Foreground = #403f52 +# Comment = #989fb1 +# Red = #de3d3a +# Orange = #e0af05 +# Yellow = #daaa01 +# Green = #08916a +# Aqua = #2aa298 +# Blue = #288ed7 +# Purple = #964ac1 +## + +[Base] +base = #fbfbfb +default = #403f52 +faded = #989fb1 +red = #de3d3a +orange = #e0af05 +yellow = #daaa01 +green = #08916a +cyan = #2aa298 +blue = #288ed7 +purple = #964ac1 + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = #eaeaea +windowtext = default +base = base +alternatebase = #e0e0e0 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = base +buttontext = default +brighttext = base +highlight = #3087c6 +highlightedtext = #ffffff +link = blue +linkvisited = blue +accent = #3087c6 + +[GUI] +helptext = blue +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = default:32 +link = blue +headertext = blue +headertag = blue:160 +emphasis = orange +dialog = green +altdialog = yellow +note = yellow:D175 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = orange +optional = blue +spellcheckline = red +errorline = green +replacetag = cyan +modifier = green +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/new_moon.conf b/novelwriter/assets/themes/new_moon.conf new file mode 100644 index 00000000..4bbb2e58 --- /dev/null +++ b/novelwriter/assets/themes/new_moon.conf @@ -0,0 +1,75 @@ +[Main] +name = New Moon +mode = dark +author = Myian +credit = Tania Rascia +url = https://github.com/taniarascia/new-moon + +[Base] +base = #2d2d2d +default = #d1d7e0 +faded = #777c85 +red = #f2777a +orange = #fca369 +yellow = #ffeea6 +green = #92d192 +cyan = #76d4d6 +blue = #6ab0f3 +purple = #e1a6f2 + +[Project] +root = default +folder = orange +file = purple +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = #252525 +windowtext = default +base = base +alternatebase = #444444 +text = default +tooltipbase = yellow +tooltiptext = base +button = #2d2d2d +buttontext = default +brighttext = base +highlight = #4386c5 +highlightedtext = #ffffff +link = blue +linkvisited = blue +accent = #4386c5 + +[GUI] +helptext = #999eaa +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = #b3b9c5 +line = #444444 +link = blue +headertext = purple +headertag = purple:128 +emphasis = blue +dialog = #ffffff +altdialog = cyan +note = green +hidden = faded +shortcode = yellow +keyword = yellow +tag = red +value = #ffd479 +optional = yellow +spellcheckline = #ff6265 +errorline = green +replacetag = purple +modifier = yellow +texthighlight = blue:80 diff --git a/novelwriter/assets/themes/night_owl.conf b/novelwriter/assets/themes/night_owl.conf new file mode 100644 index 00000000..716f7eb8 --- /dev/null +++ b/novelwriter/assets/themes/night_owl.conf @@ -0,0 +1,95 @@ +[Main] +name = Night Owl +mode = dark +author = Veronica Berglyd Olsen +credit = Sarah Drasner +url = https://github.com/sdras/night-owl-vscode-theme + +## +# Theme: Night Owl +# Source: https://github.com/sdras/night-owl-vscode-theme +# Credit: Sarah Drasner +## +# Colours: +# Background = #011627 +# Current Line = #434652 +# Selection = #32374d +# Foreground = #d6deeb +# Comment = #637777 +# Red = #f78c6c +# Orange = #ecc48d +# Yellow = #ffeb95 +# Green = #addb67 +# Aqua = #7fdbca +# Blue = #82aaff +# Purple = #c792ea +## + +[Base] +base = #011627 +default = #d6deeb +faded = #637777 +red = #f78c6c +orange = #ecc48d +yellow = #ffeb95 +green = #addb67 +cyan = #7fdbca +blue = #82aaff +purple = #c792ea + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = base:L140 +windowtext = default +base = base +alternatebase = #32374d +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = base +buttontext = default +brighttext = base +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue +accent = #2c98f7 + +[GUI] +helptext = blue +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = default:48 +link = blue +headertext = blue +headertag = blue:160 +emphasis = orange +dialog = green +altdialog = yellow +note = yellow:L115 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = orange +optional = blue +spellcheckline = red +errorline = green +replacetag = cyan +modifier = green +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/noctis.conf b/novelwriter/assets/themes/noctis.conf new file mode 100644 index 00000000..235bb5dc --- /dev/null +++ b/novelwriter/assets/themes/noctis.conf @@ -0,0 +1,107 @@ +[Main] +name = Noctis +mode = dark +author = Veronica Berglyd Olsen +credit = Liviu Schera, and contributors +url = https://github.com/liviuschera/noctis + +## +# Noctis Lux : themes/noctis.json +## +# colors +# selection.background : #107684 -> Palette.highlight +# sideBar.background : #041d20 -> Palette.window +# editor.background : #052529 -> Base.base +# editor.foreground : #b2cacd -> Base.default +# editor.selectionBackground : #169fb144 -> Syntax.line, Syntax.texthighlight +# textLink.foreground : #40d4e7 -> Palette.link, Syntax.link +# input.background : #052529 -> Palette.button +# input.foreground : #cdd3de -> Palette.buttontext +# notifications.foreground : #cdd3de -> Palette.tooltiptext +# notifications.background : #073940 -> Palette.tooltipbase +## +# tokenColors +# COMMENT : #5b858b -> Base.faded +# TEXT : #b2cacd -> Syntax.text, Syntax.optional +# KEYWORD : #df769b -> Base.red +# VARIABLE : #e4b781 -> Palette.accent +# ANNOTATION : #d67e5c -> Syntax.note, Syntax.emphasis +# CONSTANT : #d5971a -> Base.yellow +# TAG : #e66533 -> Base.orange +# STRING : #49e9a6 -> Syntax.headertext, Palette.accent +# STRINGINTERPOLATED : #16b673 -> Base.green +# NUMBER : #7060eb -> Base.purple +# FUNCTION : #16a3b6 -> Base.cyan +# SUPPORT : #49d6e9 -> Syntax.replacetag +# MISC : #49ace9 -> Base.blue +# INVALID : #e3541c -> GUI.errortext +## + +[Base] +base = #052529 +default = #b2cacd +faded = #5b858b +red = #df769b +orange = #e66533 +yellow = #d5971a +green = #16b673 +cyan = #16a3b6 +blue = #49ace9 +purple = #7060eb + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = #041d20 +windowtext = default +base = base +alternatebase = base:L175 +text = default +tooltipbase = #073940 +tooltiptext = #cdd3de +button = base +buttontext = #cdd3de +brighttext = base +highlight = #107684 +highlightedtext = default +link = #40d4e7 +linkvisited = blue +accent = #49e9a6 + +[GUI] +helptext = #e4b781 +fadedtext = faded +errortext = #e3541c + +[Syntax] +background = base +text = #b2cacd +line = #169fb132 +link = #40d4e7 +headertext = #49e9a6 +headertag = green:D125 +emphasis = #d67e5c +dialog = green +altdialog = blue +note = #d67e5c +hidden = faded +shortcode = red +keyword = yellow +tag = blue +value = red:D125 +optional = #b2cacd +spellcheckline = red +errorline = green +replacetag = #49d6e9 +modifier = red +texthighlight = #169fb144 diff --git a/novelwriter/assets/themes/noctis_lux.conf b/novelwriter/assets/themes/noctis_lux.conf new file mode 100644 index 00000000..fa37b293 --- /dev/null +++ b/novelwriter/assets/themes/noctis_lux.conf @@ -0,0 +1,107 @@ +[Main] +name = Noctis Lux +mode = light +author = Veronica Berglyd Olsen +credit = Liviu Schera, and contributors +url = https://github.com/liviuschera/noctis + +## +# Noctis Lux : themes/lux.json +## +# colors +# selection.background : #169fb155 -> Palette.highlight +# sideBar.background : #f9f1e1 -> Palette.window +# editor.background : #fef8ec -> Base.base +# editor.foreground : #005661 -> Base.default +# editor.selectionBackground : #ade2eb77 -> Syntax.line, Syntax.texthighlight +# textLink.foreground : #00c6e0 -> Palette.link, Syntax.link +# input.background : #fef8ec -> Palette.button +# input.foreground : #6a7a7c -> Palette.buttontext +# notifications.foreground : #005661 -> Palette.tooltiptext +# notifications.background : #f2edde -> Palette.tooltipbase +## +# tokenColors +# COMMENT : #8ca6a6 -> Base.faded +# TEXT : #004d57 -> Syntax.text, Syntax.optional +# KEYWORD : #ff5792 -> Base.red +# VARIABLE : #fa8900 -> Palette.accent +# ANNOTATION : #b3694d -> Syntax.note, Syntax.emphasis +# CONSTANT : #a88c00 -> Base.yellow +# TAG : #e64100 -> Base.orange +# STRING : #00b368 -> Syntax.headertext, Palette.accent +# STRINGINTERPOLATED : #009456 -> Base.green +# NUMBER : #5842ff -> Base.purple +# FUNCTION : #0095a8 -> Base.cyan +# SUPPORT : #00bdd6 -> Syntax.replacetag +# MISC : #0094f0 -> Base.blue +# INVALID : #ff530f -> GUI.errortext +## + +[Base] +base = #fef8ec +default = #005661 +faded = #8ca6a6 +red = #ff5792 +orange = #e64100 +yellow = #a88c00 +green = #009456 +cyan = #0095a8 +blue = #0094f0 +purple = #5842ff + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = #f9f1e1 +windowtext = default +base = base +alternatebase = base:D115 +text = default +tooltipbase = #f2edde +tooltiptext = #005661 +button = base +buttontext = #6a7a7c +brighttext = base +highlight = #169fb155 +highlightedtext = default +link = #00c6e0 +linkvisited = blue +accent = #00b368 + +[GUI] +helptext = #fa8900 +fadedtext = faded +errortext = #ff530f + +[Syntax] +background = base +text = #004d57 +line = #ade2eb56 +link = #00c6e0 +headertext = #00b368 +headertag = green:D125 +emphasis = #b3694d +dialog = green +altdialog = blue +note = #b3694d +hidden = faded +shortcode = red +keyword = yellow +tag = blue +value = red:D125 +optional = #004d57 +spellcheckline = red +errorline = green +replacetag = #00bdd6 +modifier = red +texthighlight = #ade2eb77 diff --git a/novelwriter/assets/themes/nord.conf b/novelwriter/assets/themes/nord.conf new file mode 100644 index 00000000..e2a2faf9 --- /dev/null +++ b/novelwriter/assets/themes/nord.conf @@ -0,0 +1,75 @@ +[Main] +name = Nord +mode = dark +author = Myian +credit = Sven Greb +url = https://github.com/nordtheme/nord + +[Base] +base = #2e3440 +default = #d8dee9 +faded = #7b88a1 +red = #bf616a +orange = #d08770 +yellow = #ebcb8b +green = #a3be8c +cyan = #88c0d0 +blue = #81a1c1 +purple = #b48ead + +[Project] +root = default +folder = yellow +file = default +title = purple +chapter = blue +scene = #8fbcbb +note = #696f7e +active = green +inactive = faded +disabled = #576279 + +[Palette] +window = #242933 +windowtext = default +base = base +alternatebase = #434c5e +text = default +tooltipbase = yellow +tooltiptext = base +button = #2e3440 +buttontext = default +brighttext = base +highlight = cyan +highlightedtext = base +link = blue +linkvisited = blue +accent = cyan + +[GUI] +helptext = blue +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #3b4252 +link = purple +headertext = cyan +headertag = cyan:128 +emphasis = #8fbcbb +dialog = blue +altdialog = green +note = orange +hidden = faded +shortcode = orange +keyword = purple +tag = #8fbcbb +value = #8fbcbb +optional = faded +spellcheckline = #ff717f +errorline = #b2da8f +replacetag = orange +modifier = purple +texthighlight = cyan:64 diff --git a/novelwriter/assets/themes/nordlicht.conf b/novelwriter/assets/themes/nordlicht.conf new file mode 100644 index 00000000..f8d9a846 --- /dev/null +++ b/novelwriter/assets/themes/nordlicht.conf @@ -0,0 +1,73 @@ +[Main] +name = Nordlicht +mode = light +author = Myian + +[Base] +base = #ffffff +default = #4c566a +faded = #7b88a1 +red = #bf616a +orange = #c97c64 +yellow = #daba7a +green = #7cac78 +cyan = #6bacbe +blue = #6b8ab8 +purple = #a36f9a + +[Project] +root = default +folder = #d4b067 +file = #6d7a96 +title = purple +chapter = blue +scene = #68a593 +note = #a5b3ca +active = green +inactive = faded +disabled = #b2c0d6 + +[Palette] +window = #e5e9f0 +windowtext = default +base = base +alternatebase = #d8dee9 +text = default +tooltipbase = #fff0d3 +tooltiptext = default +button = #e5e9f0 +buttontext = default +brighttext = base +highlight = cyan +highlightedtext = base +link = blue +linkvisited = blue +accent = cyan + +[GUI] +helptext = blue +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #eceff4 +link = purple +headertext = #559db1 +headertag = #559db188 +emphasis = #549b86 +dialog = #5579a5 +altdialog = #619b5b +note = orange +hidden = faded +shortcode = orange +keyword = purple +tag = #549481 +value = #549481 +optional = faded +spellcheckline = #ff3b4f +errorline = #58aa51 +replacetag = orange +modifier = #a16e98 +texthighlight = cyan:64 diff --git a/novelwriter/assets/themes/otium_dark.conf b/novelwriter/assets/themes/otium_dark.conf new file mode 100644 index 00000000..7319180a --- /dev/null +++ b/novelwriter/assets/themes/otium_dark.conf @@ -0,0 +1,73 @@ +[Main] +name = Otium Dark +mode = dark +author = Myian + +[Base] +base = #23242b +default = #e9e7df +faded = #7f868f +red = #ff6e61 +orange = #ff9d66 +yellow = #ffcc6a +green = #70ad79 +cyan = #4fb8a8 +blue = #5e83b1 +purple = #8479b4 + +[Project] +root = default +folder = red +file = default +title = yellow +chapter = green +scene = blue +note = faded +active = default +inactive = faded +disabled = #4e545e + +[Palette] +window = base +windowtext = default +base = base +alternatebase = #3b3c47 +text = default +tooltipbase = #b2cab6 +tooltiptext = base +button = #262931 +buttontext = default +brighttext = base +highlight = #698a6e +highlightedtext = #1b1c22 +link = green +linkvisited = green +accent = #698a6e + +[GUI] +helptext = #a0a3a8 +fadedtext = faded +errortext = red + +[Syntax] +background = #1b1c22 +text = default +line = #2a2b35 +link = #a6d0ed +headertext = yellow +headertag = yellow:128 +emphasis = yellow +dialog = green +altdialog = orange +note = #a6d0ed +hidden = faded +shortcode = blue +keyword = purple +tag = #c5a9d7 +value = faded +spellcheckline = red +errorline = #a6d0ed +replacetag = purple +modifier = blue +optional = faded +texthighlight = #3b3c47 diff --git a/novelwriter/assets/themes/otium_light.conf b/novelwriter/assets/themes/otium_light.conf new file mode 100644 index 00000000..062c7879 --- /dev/null +++ b/novelwriter/assets/themes/otium_light.conf @@ -0,0 +1,73 @@ +[Main] +name = Otium Light +mode = light +author = Myian + +[Base] +base = #ebe9e4 +default = #3b3d42 +faded = #848691 +red = #c56961 +orange = #c98358 +yellow = #be9447 +green = #398144 +cyan = #3c918a +blue = #567bac +purple = #7a65ac + +[Project] +root = default +folder = red +file = default:L165 +title = yellow +chapter = #53945f +scene = blue +note = faded:L110 +active = default:L165 +inactive = faded:L110 +disabled = #adadad + +[Palette] +window = base +windowtext = default +base = base +alternatebase = base:D115 +text = default +tooltipbase = #cce0cf +tooltiptext = default +button = #f8f7f3 +buttontext = default +brighttext = base +highlight = #6c9173 +highlightedtext = #ffffff +link = green +linkvisited = green +accent = #6c9173 + +[GUI] +helptext = #57565e +fadedtext = faded +errortext = red + +[Syntax] +background = #f8f7f3 +text = default +line = #ebe9e4 +link = blue +headertext = yellow +headertag = yellow:128 +emphasis = #ad802a +dialog = green +altdialog = #c0652d +note = #668bbd +hidden = faded +shortcode = blue +keyword = purple +tag = #9e88b8 +value = faded +spellcheckline = #e43a2b +errorline = blue +replacetag = #8c77b2 +modifier = blue +optional = faded +texthighlight = base:D110 diff --git a/novelwriter/assets/themes/paragon.conf b/novelwriter/assets/themes/paragon.conf new file mode 100644 index 00000000..7b05bee4 --- /dev/null +++ b/novelwriter/assets/themes/paragon.conf @@ -0,0 +1,74 @@ +[Main] +name = Paragon +mode = dark +author = Myian +credit = Diablo III + +[Base] +base = #161d25 +default = #fdfdfd +faded = #727283 +red = #ff4800 +orange = #ff8000 +yellow = #ffff00 +green = #49e949 +cyan = #6bd8bb +blue = #99bbff +purple = #ca5dee + +[Project] +root = default +folder = #c7b377 +file = default +title = orange +chapter = yellow +scene = #6869ff +note = cyan +active = #00ffe1 +inactive = faded +disabled = faded:D150 + +[Palette] +window = #10151b +windowtext = default +base = base +alternatebase = #394452 +text = default +tooltipbase = cyan +tooltiptext = base +button = base +buttontext = default +brighttext = base +highlight = cyan:D125 +highlightedtext = base +link = cyan +linkvisited = cyan +accent = cyan + +[GUI] +helptext = #949baa +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #272d3a +link = green +headertext = green +headertag = green:128 +emphasis = #99bbff +dialog = cyan +altdialog = green +note = #c7b377 +hidden = faded +shortcode = orange +keyword = red +tag = #6869ff +value = #ffff64 +optional = faded +spellcheckline = #ff0000 +errorline = cyan +replacetag = purple +modifier = red +texthighlight = #99bbff60 diff --git a/novelwriter/assets/themes/primer_light.conf b/novelwriter/assets/themes/primer_light.conf new file mode 100644 index 00000000..49850873 --- /dev/null +++ b/novelwriter/assets/themes/primer_light.conf @@ -0,0 +1,75 @@ +[Main] +name = Primer Light +mode = light +author = Myian +credit = GitHub Inc. +url = https://github.com/primer + +[Base] +base = #ffffff +default = #1f2328 +faded = #8c959f +red = #cf222e +orange = #e16f24 +yellow = #d4a72c +green = #2da44e +cyan = #339d9b +blue = #0969da +purple = #8557db + +[Project] +root = #32383f +folder = orange +file = #49bcb7 +title = #116329 +chapter = #2e934a +scene = #4ac26b +note = yellow +active = #24292f +inactive = faded +disabled = #afb8c1 + +[Palette] +window = #eaeef2 +windowtext = default +base = base +alternatebase = #dde3e8 +text = default +tooltipbase = #fff8c5 +tooltiptext = default +button = base +buttontext = default +brighttext = base +highlight = green +highlightedtext = base +link = blue +linkvisited = blue +accent = green + +[GUI] +helptext = #424a53 +fadedtext = #8c959f +errortext = red + +[Syntax] +background = base +text = #424a53 +line = #eaeef2 +link = blue +headertext = default +headertag = #9ea7b0 +emphasis = #1a7f37 +dialog = #0d1117 +altdialog = cyan +note = #bf8700 +hidden = #8c959f +shortcode = #e85aad +keyword = blue +tag = #218bff +value = orange +optional = #54aeff +spellcheckline = #fa4549 +errorline = cyan +replacetag = purple +modifier = #bc4c00 +texthighlight = #a7fbbe diff --git a/novelwriter/assets/themes/primer_night.conf b/novelwriter/assets/themes/primer_night.conf new file mode 100644 index 00000000..2b4e4156 --- /dev/null +++ b/novelwriter/assets/themes/primer_night.conf @@ -0,0 +1,75 @@ +[Main] +name = Primer Night +mode = dark +author = Myian +credit = GitHub Inc. +url = https://github.com/primer + +[Base] +base = #0d1117 +default = #f0f6fc +faded = #6e7681 +red = #ff7b72 +orange = #ffa657 +yellow = #f8e3a1 +green = #56d364 +cyan = #57CCC5 +blue = #79c0ff +purple = #c398ff + +[Project] +root = default +folder = orange +file = #8adfd7 +title = #238636 +chapter = #56d364 +scene = #aff5b4 +note = yellow +active = default +inactive = faded +disabled = #484f58 + +[Palette] +window = #161b22 +windowtext = default +base = base +alternatebase = #30363d +text = default +tooltipbase = yellow +tooltiptext = base +button = #21262d +buttontext = default +brighttext = base +highlight = #207c59 +highlightedtext = default +link = blue +linkvisited = blue +accent = #207c59 + +[GUI] +helptext = #8b949e +fadedtext = #6e7681 +errortext = #f85149 + +[Syntax] +background = base +text = #b1bac4 +line = #21262d +link = blue +headertext = default +headertag = #b1bac4 +emphasis = #7ee787 +dialog = #ffffff +altdialog = #57ccc5 +note = #f8e3a1 +hidden = faded +shortcode = #ff9bce +keyword = #4493f8 +tag = blue +value = #ffc680 +optional = #a5d6ff +spellcheckline = #f85149 +errorline = cyan +replacetag = purple +modifier = orange +texthighlight = #104C35 diff --git a/novelwriter/assets/themes/ruby_day.conf b/novelwriter/assets/themes/ruby_day.conf new file mode 100644 index 00000000..5a56e472 --- /dev/null +++ b/novelwriter/assets/themes/ruby_day.conf @@ -0,0 +1,73 @@ +[Main] +name = Ruby Day +mode = light +author = Myian + +[Base] +base = #ffeeee +default = #444252 +faded = #8e8c9c +red = #e46969 +orange = #df9355 +yellow = #dfb362 +green = #62a369 +cyan = #5c9da3 +blue = #6e73bb +purple = #b67ac2 + +[Project] +root = default +folder = orange +file = #757285 +title = green +chapter = red +scene = blue +note = purple +active = default +inactive = red +disabled = faded + +[Palette] +window = #f1d9d9 +windowtext = default +base = base +alternatebase = #e9c1c1 +text = default +tooltipbase = #f0d0d0 +tooltiptext = default +button = base +buttontext = default +brighttext = red +highlight = #da3d3d +highlightedtext = base +link = #da3d3d +linkvisited = #da3d3d +accent = #c43939 + +[GUI] +helptext = #756777 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = #343242 +line = #f3dbdd +link = #5159cc +headertext = #343242 +headertag = #34324280 +emphasis = #c05858 +dialog = #ce1d1d +altdialog = #c45522 +note = #9f58ad +hidden = faded +shortcode = #2a9638 +keyword = #5159cc +tag = #9f58ad +value = #b98421 +optional = faded +spellcheckline = #ff3333 +errorline = cyan +replacetag = #5159cc +modifier = #2a9638 +texthighlight = red:80 diff --git a/novelwriter/assets/themes/ruby_night.conf b/novelwriter/assets/themes/ruby_night.conf new file mode 100644 index 00000000..37f60ac6 --- /dev/null +++ b/novelwriter/assets/themes/ruby_night.conf @@ -0,0 +1,73 @@ +[Main] +name = Ruby Night +mode = dark +author = Myian + +[Base] +base = #18181c +default = #dcd9e6 +faded = #817b94 +red = #ff6969 +orange = #f89d68 +yellow = #ebcf84 +green = #7fb88b +cyan = #75c5ca +blue = #7f8dcf +purple = #c37ac5 + +[Project] +root = default +folder = orange +file = default +title = green +chapter = red +scene = blue +note = purple +active = default +inactive = red +disabled = faded + +[Palette] +window = #0e0e11 +windowtext = default +base = base +alternatebase = #29292e +text = default +tooltipbase = #fa9b9b +tooltiptext = base +button = #1c1c21 +buttontext = default +brighttext = base +highlight = #a52a2c +highlightedtext = #ebd9d9 +link = red +linkvisited = red +accent = #a52a2c + +[GUI] +helptext = #a5a1b3 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #29292e +link = blue +headertext = default +headertag = default:128 +emphasis = #ff9d9d +dialog = red +altdialog = orange +note = purple +hidden = faded +shortcode = green +keyword = blue +tag = purple +value = yellow +optional = faded +spellcheckline = #ff2727 +errorline = cyan +replacetag = blue +modifier = green +texthighlight = red:80 \ No newline at end of file diff --git a/novelwriter/assets/themes/selenium_dark.conf b/novelwriter/assets/themes/selenium_dark.conf new file mode 100644 index 00000000..a5c5cc1b --- /dev/null +++ b/novelwriter/assets/themes/selenium_dark.conf @@ -0,0 +1,73 @@ +[Main] +name = Selenium Dark +mode = dark +author = Myian + +[Base] +base = #15141a +default = #a9a1b4 +faded = #786e87 +red = #c04358 +orange = #b1663a +yellow = #917a3e +green = #35834c +cyan = #1b8186 +blue = #3268cc +purple = #7d53b8 + +[Project] +root = default +folder = red +file = default +title = purple +chapter = blue +scene = cyan +note = #625b70 +active = #c7c0d2 +inactive = red +disabled = #625b70 + +[Palette] +window = #0f0e13 +windowtext = #c7c0d2 +base = base +alternatebase = #35303f +text = #c7c0d2 +tooltipbase = default +tooltiptext = base +button = #1e1c25 +buttontext = default +brighttext = base +highlight = #625b70 +highlightedtext = #e4e0ec +link = #9267d3 +linkvisited = #9267d3 +accent = red:D120 + +[GUI] +helptext = #888095 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #1e1c25 +link = blue +headertext = #e4e0ec +headertag = faded +emphasis = #9267d3 +dialog = #e4e0ec +altdialog = #c458ac +note = red +hidden = faded +shortcode = cyan +keyword = blue +tag = red +value = #c7c0d2 +optional = #c7c0d2 +spellcheckline = red:L120 +errorline = blue:L120 +replacetag = blue +modifier = cyan +texthighlight = #9267d370 diff --git a/novelwriter/assets/themes/selenium_light.conf b/novelwriter/assets/themes/selenium_light.conf new file mode 100644 index 00000000..d1eefa1f --- /dev/null +++ b/novelwriter/assets/themes/selenium_light.conf @@ -0,0 +1,73 @@ +[Main] +name = Selenium Light +mode = light +author = Myian + +[Base] +base = #e7e3eb +default = #463d52 +faded = #8a8197 +red = #c04358 +orange = #b1663a +yellow = #917a3e +green = #35834c +cyan = #1b8186 +blue = #3268cc +purple = #7d53b8 + +[Project] +root = default +folder = red +file = default +title = purple +chapter = blue +scene = cyan +note = faded +active = default +inactive = red +disabled = faded + +[Palette] +window = #d2cfda +windowtext = #29252f +base = base +alternatebase = #c1bccd +text = #29252f +tooltipbase = #ebebf1 +tooltiptext = default +button = #e6e3eb +buttontext = default +brighttext = base +highlight = #797082 +highlightedtext = #f0eff3 +link = #623a9b +linkvisited = #623a9b +accent = red + +[GUI] +helptext = default +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = #62576f +line = #d2cfda +link = blue +headertext = #29252f +headertag = faded +emphasis = purple +dialog = #29252f +altdialog = #c458ac +note = red +hidden = faded +shortcode = cyan +keyword = blue +tag = red +value = default +optional = default +spellcheckline = #e63653 +errorline = #3e77e2 +replacetag = blue +modifier = cyan +texthighlight = purple:64 diff --git a/novelwriter/assets/themes/sepia_dark.conf b/novelwriter/assets/themes/sepia_dark.conf new file mode 100644 index 00000000..49deeedd --- /dev/null +++ b/novelwriter/assets/themes/sepia_dark.conf @@ -0,0 +1,73 @@ +[Main] +name = Sepia Dark +mode = dark +author = Myian + +[Base] +base = #302622 +default = #d6c4b2 +faded = #8d7a71 +red = #d16b6b +orange = #d1886b +yellow = #c49e58 +green = #919e59 +cyan = #71a392 +blue = #718ea8 +purple = #b8759e + +[Project] +root = default +folder = cyan +file = default +title = red +chapter = yellow +scene = green +note = faded +active = default +inactive = red +disabled = faded + +[Palette] +window = #251b18 +windowtext = #ceb3a2 +base = base +alternatebase = #4b3d37 +text = #ceb3a2 +tooltipbase = #f1daca +tooltiptext = base +button = #362520 +buttontext = default +brighttext = base +highlight = #885b4d +highlightedtext = #e7d4c9 +link = orange +linkvisited = orange +accent = #885b4d + +[GUI] +helptext = faded +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = #c4a491 +line = #41342f +link = cyan +headertext = default +headertag = default:160 +emphasis = #f1daca +dialog = orange +altdialog = red +note = green +hidden = faded +shortcode = purple +keyword = cyan +tag = green +value = yellow +optional = yellow +spellcheckline = #ff5d5d +errorline = cyan +replacetag = green +modifier = purple +texthighlight = orange:72 diff --git a/novelwriter/assets/themes/sepia_light.conf b/novelwriter/assets/themes/sepia_light.conf new file mode 100644 index 00000000..a7c1d306 --- /dev/null +++ b/novelwriter/assets/themes/sepia_light.conf @@ -0,0 +1,73 @@ +[Main] +name = Sepia Light +mode = light +author = Myian + +[Base] +base = #f1e7dc +default = #635048 +faded = #a18375 +red = #d17a6b +orange = #d1906b +yellow = #c49e58 +green = #919e59 +cyan = #71a392 +blue = #718ea8 +purple = #b8759e + +[Project] +root = #86685b +folder = cyan +file = #86685b +title = red +chapter = yellow +scene = green +note = #b6a096 +active = #86685b +inactive = red +disabled = #b6a096 + +[Palette] +window = #e2d3c1 +windowtext = default +base = base +alternatebase = #d8c4b2 +text = default +tooltipbase = #e6dacd +tooltiptext = default +button = #f7efe6 +buttontext = default +brighttext = base +highlight = #a57f66 +highlightedtext = #f7efe6 +link = #ac5828 +linkvisited = #ac5828 +accent = #a57f66 + +[GUI] +helptext = #836050 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = #74584c +line = #e6dacd +link = #458871 +headertext = default +headertag = default:160 +emphasis = #6e2920 +dialog = #ac5828 +altdialog = #b44b4b +note = #6e8021 +hidden = faded +shortcode = #a75688 +keyword = #458871 +tag = #6e8021 +value = #af7f25 +optional = #af7f25 +spellcheckline = #ff5d5d +errorline = cyan +replacetag = #6e8021 +modifier = #a75688 +texthighlight = orange:96 diff --git a/novelwriter/assets/themes/snazzy.conf b/novelwriter/assets/themes/snazzy.conf index 39ec3c87..0c36d884 100644 --- a/novelwriter/assets/themes/snazzy.conf +++ b/novelwriter/assets/themes/snazzy.conf @@ -1,48 +1,88 @@ [Main] -name = Snazzy Light -author = Veronica Berglyd Olsen (adaptation) -credit = Florian Reuschel (color theme) -url = https://github.com/loilo/vscode-snazzy-light -license = MIT License -licenseurl = https://github.com/loilo/vscode-snazzy-light/blob/master/LICENSE +name = Snazzy Light +mode = light +author = Veronica Berglyd Olsen +credit = Florian Reuschel, Sindre Sorhus +url = https://github.com/loilo/vscode-snazzy-light -[Icons] -default = 86, 88, 105 -faded = 84, 85, 84 -red = 255, 92, 87 -orange = 245, 185, 0 -yellow = 207, 156, 0 -green = 45, 174, 88 -aqua = 19, 187, 183 -blue = 9, 161, 237 -purple = 247, 103, 187 +## +# Colours: +# Background = fafbfc : 250, 251, 252 +# Foreground = 565869 : 86, 88, 105 +# Comment = 9194a2 : 145, 148, 162 +# Yellow = cf9c00 : 207, 156, 0 +# Red = ff5c57 : 255, 92, 87 +# Pink = f767bb : 247, 103, 187 +# Blue = 09a1ed : 9, 161, 237 +# Cyan = 13bbb7 : 19, 187, 183 +# Green = 2dae58 : 45, 174, 88 +## + +[Base] +base = #fafbfc +default = #565869 +faded = #545554 +red = #ff5c57 +orange = #f5b900 +yellow = #cf9c00 +green = #2dae58 +cyan = #13bbb7 +blue = #09a1ed +purple = #f767bb [Project] -root = 9, 161, 237 -folder = 207, 156, 0 -file = 84, 85, 84 -title = 45, 174, 88 -chapter = 255, 92, 87 -scene = 9, 161, 237 -note = 207, 156, 0 +root = blue +folder = yellow +file = faded +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded [Palette] -window = 243, 244, 245 -windowtext = 86, 88, 105 -base = 250, 251, 252 -alternatebase = 234, 234, 235 -text = 86, 88, 105 -tooltipbase = 245, 233, 194 -tooltiptext = 86, 88, 105 -button = 250, 251, 252 -buttontext = 86, 88, 105 -brighttext = 255, 255, 255 -highlight = 9, 161, 237 -highlightedtext = 255, 255, 255 -link = 9, 161, 237 -linkvisited = 9, 161, 237 +window = #f3f4f5 +windowtext = default +base = base +alternatebase = #eaeaeb +text = default +tooltipbase = #f5e9c2 +tooltiptext = default +button = base +buttontext = default +brighttext = base +highlight = blue +highlightedtext = #ffffff +link = blue +linkvisited = blue +accent = blue [GUI] -helptext = 9, 161, 237 -fadedtext = 84, 85, 84 -errortext = 255, 92, 87 +helptext = blue +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = default:32 +link = blue +headertext = green +headertag = green:D125 +emphasis = purple +dialog = blue +altdialog = yellow +note = cyan +hidden = purple:D150 +shortcode = purple +keyword = blue +tag = green +value = yellow +optional = yellow +spellcheckline = red +errorline = green +replacetag = cyan +modifier = purple +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/solarized_dark.conf b/novelwriter/assets/themes/solarized_dark.conf index 0f401eb9..4c73412f 100644 --- a/novelwriter/assets/themes/solarized_dark.conf +++ b/novelwriter/assets/themes/solarized_dark.conf @@ -1,48 +1,94 @@ [Main] -name = Solarized Dark -author = nullbasis -credit = Ethan Schoonover -url = https://ethanschoonover.com/solarized/ -license = MIT -licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE +name = Solarized Dark +mode = dark +author = nullbasis and Veronica Berglyd Olsen +credit = Ethan Schoonover +url = https://ethanschoonover.com/solarized/ -[Icons] -default = 230, 223, 206 -faded = 166, 161, 149 -red = 220, 50, 47 -orange = 203, 75, 22 -yellow = 181, 137, 0 -green = 133, 153, 0 -aqua = 42, 161, 152 -blue = 38, 139, 210 -purple = 211, 54, 130 +## +# base03: #002b36 +# base02: #073642 +# base01: #586e75 +# base00: #657b83 +# base0: #839496 +# base1: #93a1a1 +# base2: #eee8d5 +# base3: #fdf6e3 +# yellow: #b58900 +# orange: #cb4b16 +# red: #dc322f +# magenta: #d33682 +# violet: #6c71c4 +# blue: #268bd2 +# cyan: #2aa198 +# green: #859900 +## + +[Base] +base = #002b36 +default = #fdf6e3 +faded = #eee8d5 +red = #dc322f +orange = #cb4b16 +yellow = #b58900 +green = #859900 +cyan = #2aa198 +blue = #268bd2 +purple = #6c71c4 [Project] -root = 42, 161, 152 -folder = 42, 161, 152 -file = 230, 223, 206 -title = 133, 153, 0 -chapter = 220, 50, 47 -scene = 38, 139, 210 -note = 181, 137, 0 +root = cyan +folder = cyan +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded [Palette] -window = 0, 43, 54 -windowtext = 253, 246, 227 -base = 7, 54, 66 -alternatebase = 88, 110, 117 -text = 253, 246, 227 -tooltipbase = 133, 153, 0 -tooltiptext = 0, 43, 54 -button = 7, 54, 66 -buttontext = 253, 246, 227 -brighttext = 7, 54, 66 -highlight = 42, 161, 152 -highlightedtext = 0, 43, 54 -link = 38, 139, 210 -linkvisited = 38, 139, 210 +window = #073642 +windowtext = default +base = base +alternatebase = #586e75 +text = default +tooltipbase = yellow:L150 +tooltiptext = #002b36 +button = base +buttontext = default +brighttext = base +highlight = cyan +highlightedtext = default +link = blue +linkvisited = blue +accent = cyan [GUI] -helptext = 101, 123, 131 -fadedtext = 101, 123, 131 -errortext = 220, 50, 47 +helptext = cyan +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #073642 +link = blue +headertext = blue +headertag = #657b83 +emphasis = blue +dialog = cyan +altdialog = red +note = cyan:D125 +hidden = default:L200 +shortcode = blue +keyword = green +tag = yellow +value = orange +optional = cyan:D125 +spellcheckline = red +errorline = red +replacetag = green +modifier = yellow +texthighlight = yellow:128 diff --git a/novelwriter/assets/themes/solarized_light.conf b/novelwriter/assets/themes/solarized_light.conf index 06f4cb20..61d701b7 100644 --- a/novelwriter/assets/themes/solarized_light.conf +++ b/novelwriter/assets/themes/solarized_light.conf @@ -1,48 +1,94 @@ [Main] -name = Solarized Light -author = nullbasis -credit = Ethan Schoonover -url = https://ethanschoonover.com/solarized/ -license = MIT -licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE +name = Solarized Light +mode = light +author = nullbasis and Veronica Berglyd Olsen +credit = Ethan Schoonover +url = https://ethanschoonover.com/solarized/ -[Icons] -default = 59, 68, 71 -faded = 78, 91, 95 -red = 220, 50, 47 -orange = 203, 75, 22 -yellow = 181, 137, 0 -green = 133, 153, 0 -aqua = 42, 161, 152 -blue = 38, 139, 210 -purple = 211, 54, 130 +## +# base03: #002b36 +# base02: #073642 +# base01: #586e75 +# base00: #657b83 +# base0: #839496 +# base1: #93a1a1 +# base2: #eee8d5 +# base3: #fdf6e3 +# yellow: #b58900 +# orange: #cb4b16 +# red: #dc322f +# magenta: #d33682 +# violet: #6c71c4 +# blue: #268bd2 +# cyan: #2aa198 +# green: #859900 +## + +[Base] +base = #fdf6e3 +default = #002b36 +faded = #073642 +red = #dc322f +orange = #cb4b16 +yellow = #b58900 +green = #859900 +cyan = #2aa198 +blue = #268bd2 +purple = #6c71c4 [Project] -root = 42, 161, 152 -folder = 42, 161, 152 -file = 59, 68, 71 -title = 133, 153, 0 -chapter = 220, 50, 47 -scene = 38, 139, 210 -note = 181, 137, 0 +root = cyan +folder = cyan +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded [Palette] -window = 238, 232, 213 -windowtext = 0, 43, 54 -base = 253, 246, 227 -alternatebase = 147, 161, 161 -text = 0, 43, 54 -tooltipbase = 133, 153, 0 -tooltiptext = 0, 43, 54 -button = 238, 232, 213 -buttontext = 0, 43, 54 -brighttext = 253, 246, 227 -highlight = 42, 161, 152 -highlightedtext = 253, 246, 227 -link = 38, 139, 210 -linkvisited = 38, 139, 210 +window = #eee8d5 +windowtext = default +base = base +alternatebase = #93a1a1 +text = default +tooltipbase = yellow:L150 +tooltiptext = #002b36 +button = base +buttontext = default +brighttext = base +highlight = cyan +highlightedtext = default +link = blue +linkvisited = blue +accent = cyan [GUI] -helptext = 78, 91, 95 -fadedtext = 78, 91, 95 -errortext = 220, 50, 47 +helptext = cyan +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #eee8d5 +link = blue +headertext = blue +headertag = #657b83 +emphasis = blue +dialog = cyan +altdialog = red +note = cyan:D125 +hidden = default:L200 +shortcode = blue +keyword = green +tag = yellow +value = orange +optional = cyan:D125 +spellcheckline = red +errorline = red +replacetag = green +modifier = yellow +texthighlight = yellow:128 diff --git a/novelwriter/assets/themes/sultana_light.conf b/novelwriter/assets/themes/sultana_light.conf new file mode 100644 index 00000000..1f4e5195 --- /dev/null +++ b/novelwriter/assets/themes/sultana_light.conf @@ -0,0 +1,73 @@ +[Main] +name = Sultana Light +mode = light +author = Myian + +[Base] +base = #fff6f6 +default = #494657 +faded = #a89d98 +red = #d3677e +orange = #d6805e +yellow = #cc9c49 +green = #819c42 +cyan = #5f9c88 +blue = #6591b6 +purple = #a279b9 + +[Project] +root = #5a5563 +folder = orange +file = blue +title = red +chapter = yellow +scene = green +note = faded +active = #77606e +inactive = red +disabled = faded + +[Palette] +window = #f0e4e4 +windowtext = default +base = base +alternatebase = #e4cccc +text = default +tooltipbase = #ffeac6 +tooltiptext = default +button = #fcf3f3 +buttontext = default +brighttext = base +highlight = #c57b85 +highlightedtext = #f9f4fc +link = #5577bb +linkvisited = #5577bb +accent = #ad6978 + +[GUI] +helptext = default:192 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = #77606e +line = #f0e4e4 +link = #5577bb +headertext = #66515e +headertag = #66515e80 +emphasis = #5577bb +dialog = #c54a5f +altdialog = #4d968a +note = #699128 +hidden = #aa9292 +shortcode = #4d968a +keyword = #b89126 +tag = #4d968a +value = #c77741 +optional = #aa9292 +spellcheckline = #ec5656 +errorline = #42a789 +replacetag = #ab69d1 +modifier = #b89126 +texthighlight = #b8788366 diff --git a/novelwriter/assets/themes/sultana_night.conf b/novelwriter/assets/themes/sultana_night.conf new file mode 100644 index 00000000..bd55dd84 --- /dev/null +++ b/novelwriter/assets/themes/sultana_night.conf @@ -0,0 +1,73 @@ +[Main] +name = Sultana Night +mode = dark +author = Myian + +[Base] +base = #2a222c +default = #e2cdc9 +faded = #97889b +red = #ec8ba0 +orange = #ffb29b +yellow = #ffdfa9 +green = #bdd49d +cyan = #9cd1bf +blue = #92b5d4 +purple = #d39fd3 + +[Project] +root = default +folder = orange +file = blue +title = red +chapter = yellow +scene = green +note = faded +active = default +inactive = red +disabled = #85748a + +[Palette] +window = #221a23 +windowtext = default +base = base +alternatebase = #4a3e4d +text = default +tooltipbase = yellow +tooltiptext = base +button = #2d2130 +buttontext = default +brighttext = base +highlight = #9e5c70 +highlightedtext = default +link = blue +linkvisited = blue +accent = #9e4c65 + +[GUI] +helptext = default:192 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #3e3341 +link = blue +headertext = #f3eae0 +headertag = #f3eae080 +emphasis = blue +dialog = red +altdialog = cyan +note = green +hidden = faded +shortcode = cyan +keyword = yellow +tag = cyan +value = orange +optional = faded +spellcheckline = #ff7070 +errorline = #88ecba +replacetag = purple +modifier = yellow +texthighlight = #9e5c7080 diff --git a/novelwriter/assets/themes/tango_dark.conf b/novelwriter/assets/themes/tango_dark.conf new file mode 100644 index 00000000..8661ce27 --- /dev/null +++ b/novelwriter/assets/themes/tango_dark.conf @@ -0,0 +1,89 @@ +[Main] +name = Tango Dark +mode = dark +author = Veronica Berglyd Olsen +credit = Tango Desktop Project +url = https://en.wikipedia.org/wiki/Tango_Desktop_Project + +## +# Colours: +# Background = #30302f +# Foreground = #eeeeec +# Comment = #8f8f8d +# Red = #ef2929 +# Orange = #c4a000 +# Yellow = #fce94f +# Green = #8ae234 +# Cyan = #34e2e2 +# Blue = #3465a4 +# Purple = #ad7fa8 +## + +[Base] +base = #2e3436 +default = #eeeeec +faded = #babdb6 +red = #ef2929 +orange = #fcaf3e +yellow = #fce94f +green = #8ae234 +cyan = #34e2e2 +blue = #729fcf +purple = #ad7fa8 + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = base:L140 +windowtext = default +base = base +alternatebase = #454e51 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #555753 +buttontext = default +brighttext = #555753 +highlight = blue +highlightedtext = #ffffff +link = blue +linkvisited = blue +accent = blue + +[GUI] +helptext = orange +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = default:32 +link = blue +headertext = blue +headertag = blue:160 +emphasis = orange +dialog = green +altdialog = yellow +note = yellow:L115 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = red +optional = blue +spellcheckline = red +errorline = green +replacetag = cyan +modifier = blue +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tango_light.conf b/novelwriter/assets/themes/tango_light.conf new file mode 100644 index 00000000..379b0a72 --- /dev/null +++ b/novelwriter/assets/themes/tango_light.conf @@ -0,0 +1,89 @@ +[Main] +name = Tango Light +mode = light +author = Veronica Berglyd Olsen +credit = Tango Desktop Project +url = https://en.wikipedia.org/wiki/Tango_Desktop_Project + +## +# Colours: +# Background = #eeeeec +# Foreground = #2e3436 +# Comment = #8f8f8d +# Red = #a40000 +# Orange = #c4a000 +# Yellow = #fce94f +# Green = #8ae234 +# Cyan = #34e2e2 +# Blue = #3465a4 +# Purple = #ad7fa8 +## + +[Base] +base = #eeeeec +default = #2e3436 +faded = #888a85 +red = #a40000 +orange = #ce5c00 +yellow = #c4a000 +green = #4e9a06 +cyan = #069a9a +blue = #204a87 +purple = #5c3566 + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = base:D110 +windowtext = default +base = base +alternatebase = #e0e0e0 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #d3d7cf +buttontext = default +brighttext = #d3d7cf +highlight = blue:L200 +highlightedtext = #ffffff +link = blue +linkvisited = blue +accent = blue:L150 + +[GUI] +helptext = orange +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = default:32 +link = blue +headertext = blue +headertag = blue:160 +emphasis = orange +dialog = green +altdialog = yellow +note = yellow:D125 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = red +optional = blue +spellcheckline = red +errorline = green +replacetag = cyan +modifier = blue +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow.conf b/novelwriter/assets/themes/tomorrow.conf new file mode 100644 index 00000000..15abcd32 --- /dev/null +++ b/novelwriter/assets/themes/tomorrow.conf @@ -0,0 +1,95 @@ +[Main] +name = Tomorrow +mode = light +author = Veronica Berglyd Olsen +credit = Chris Kempson +url = https://github.com/chriskempson/tomorrow-theme + +## +# Theme: Tomorrow +# Source: https://github.com/chriskempson/tomorrow-theme +# Credit: Chris Kempson +## +# Colours: +# Background = #ffffff +# Current Line = #efefef +# Selection = #d6d6d6 +# Foreground = #4d4d4c +# Comment = #8e908c +# Red = #c82829 +# Orange = #f5871f +# Yellow = #eab700 +# Green = #718c00 +# Aqua = #3e999f +# Blue = #4271ae +# Purple = #8959a8 +## + +[Base] +base = #ffffff +default = #484848 +faded = #6c6c6c +red = #c82829 +orange = #f5871f +yellow = #eab700 +green = #718c00 +cyan = #3e999f +blue = #4271ae +purple = #8959a8 + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = #efefef +windowtext = #000000 +base = base +alternatebase = #d6d6d6 +text = #000000 +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = #efefef +buttontext = #000000 +brighttext = #efefef +highlight = blue +highlightedtext = base +link = blue +linkvisited = blue +accent = blue + +[GUI] +helptext = #5c5c5c +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = #4d4d4c +line = default:32 +link = blue +headertext = blue +headertag = blue:L135 +emphasis = orange +dialog = green +altdialog = yellow +note = #b38c00 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = yellow +optional = blue +spellcheckline = red +errorline = green +replacetag = cyan +modifier = orange +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow_night.conf b/novelwriter/assets/themes/tomorrow_night.conf new file mode 100644 index 00000000..ac213a76 --- /dev/null +++ b/novelwriter/assets/themes/tomorrow_night.conf @@ -0,0 +1,95 @@ +[Main] +name = Tomorrow Night +mode = dark +author = Veronica Berglyd Olsen +credit = Chris Kempson +url = https://github.com/chriskempson/tomorrow-theme + +## +# Theme: Tomorrow Night +# Source: https://github.com/chriskempson/tomorrow-theme +# Credit: Chris Kempson +## +# Colours: +# Background = #1d1f21 +# Current Line = #282a2e +# Selection = #373b41 +# Foreground = #c5c8c6 +# Comment = #969896 +# Red = #cc6666 +# Orange = #de935f +# Yellow = #f0c674 +# Green = #b5bd68 +# Aqua = #8abeb7 +# Blue = #81a2be +# Purple = #b294bb +## + +[Base] +base = #1d1f21 +default = #c5c8c6 +faded = #969896 +red = #cc6666 +orange = #de935f +yellow = #f0c674 +green = #b5bd68 +cyan = #8abeb7 +blue = #81a2be +purple = #b294bb + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = #282a2e +windowtext = default +base = base +alternatebase = #373b41 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = base +buttontext = default +brighttext = base +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue +accent = #2c98f7 + +[GUI] +helptext = #a4a4a4 +fadedtext = #949494 +errortext = red + +[Syntax] +background = base +text = default +line = default:48 +link = blue +headertext = blue +headertag = blue:D150 +emphasis = orange +dialog = green +altdialog = yellow +note = #f0dbb2 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = yellow +optional = blue +spellcheckline = red +errorline = green +replacetag = cyan +modifier = orange +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow_night_blue.conf b/novelwriter/assets/themes/tomorrow_night_blue.conf new file mode 100644 index 00000000..05193e6f --- /dev/null +++ b/novelwriter/assets/themes/tomorrow_night_blue.conf @@ -0,0 +1,95 @@ +[Main] +name = Tomorrow Night Blue +mode = dark +author = Veronica Berglyd Olsen +credit = Chris Kempson +url = https://github.com/chriskempson/tomorrow-theme + +## +# Theme: Tomorrow Night Blue +# Source: https://github.com/chriskempson/tomorrow-theme +# Credit: Chris Kempson +## +# Colours: +# Background = #002451 +# Current Line = #00346e +# Selection = #003f8e +# Foreground = #ffffff +# Comment = #7285b7 +# Red = #ff9da4 +# Orange = #ffc58f +# Yellow = #ffeead +# Green = #d1f1a9 +# Aqua = #99ffff +# Blue = #bbdaff +# Purple = #ebbbff +## + +[Base] +base = #002451 +default = #ffffff +faded = #7285b7 +red = #ff9da4 +orange = #ffc58f +yellow = #ffeead +green = #d1f1a9 +cyan = #99ffff +blue = #bbdaff +purple = #ebbbff + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = base:L125 +windowtext = default +base = base +alternatebase = #003f8e +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = base +buttontext = default +brighttext = base +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue +accent = #2c98f7 + +[GUI] +helptext = #a4a4a4 +fadedtext = #949494 +errortext = red + +[Syntax] +background = base +text = default +line = default:32 +link = blue +headertext = blue +headertag = blue:D150 +emphasis = orange +dialog = green +altdialog = yellow +note = #fff4cc +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = yellow +optional = blue +spellcheckline = red +errorline = green +replacetag = cyan +modifier = orange +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow_night_bright.conf b/novelwriter/assets/themes/tomorrow_night_bright.conf new file mode 100644 index 00000000..514940fa --- /dev/null +++ b/novelwriter/assets/themes/tomorrow_night_bright.conf @@ -0,0 +1,95 @@ +[Main] +name = Tomorrow Night Bright +mode = dark +author = Veronica Berglyd Olsen +credit = Chris Kempson +url = https://github.com/chriskempson/tomorrow-theme + +## +# Theme: Tomorrow Night Bright +# Source: https://github.com/chriskempson/tomorrow-theme +# Credit: Chris Kempson +## +# Colours: +# Background = #000000 +# Current Line = #2a2a2a +# Selection = #424242 +# Foreground = #eaeaea +# Comment = #969896 +# Red = #d54e53 +# Orange = #e78c45 +# Yellow = #e7c547 +# Green = #b9ca4a +# Aqua = #70c0b1 +# Blue = #7aa6da +# Purple = #c397d8 +## + +[Base] +base = #000000 +default = #eaeaea +faded = #969896 +red = #d54e53 +orange = #e78c45 +yellow = #e7c547 +green = #b9ca4a +cyan = #70c0b1 +blue = #7aa6da +purple = #c397d8 + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = #181818 +windowtext = default +base = base +alternatebase = #424242 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = base +buttontext = default +brighttext = base +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue +accent = #2c98f7 + +[GUI] +helptext = #a4a4a4 +fadedtext = #949494 +errortext = red + +[Syntax] +background = base +text = default +line = default:48 +link = blue +headertext = blue +headertag = blue:D150 +emphasis = orange +dialog = green +altdialog = yellow +note = #e7d696 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = yellow +optional = blue +spellcheckline = red +errorline = green +replacetag = cyan +modifier = orange +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/tomorrow_night_eighties.conf b/novelwriter/assets/themes/tomorrow_night_eighties.conf new file mode 100644 index 00000000..154b6ef4 --- /dev/null +++ b/novelwriter/assets/themes/tomorrow_night_eighties.conf @@ -0,0 +1,95 @@ +[Main] +name = Tomorrow Night Eighties +mode = dark +author = Veronica Berglyd Olsen +credit = Chris Kempson +url = https://github.com/chriskempson/tomorrow-theme + +## +# Theme: Tomorrow Night Eighties +# Source: https://github.com/chriskempson/tomorrow-theme +# Credit: Chris Kempson +## +# Colours: +# Background = #2d2d2d +# Current Line = #393939 +# Selection = #515151 +# Foreground = #cccccc +# Comment = #999999 +# Red = #f2777a +# Orange = #f99157 +# Yellow = #ffcc66 +# Green = #99cc99 +# Aqua = #66cccc +# Blue = #6699cc +# Purple = #cc99cc +## + +[Base] +base = #2d2d2d +default = #cccccc +faded = #999999 +red = #f2777a +orange = #f99157 +yellow = #ffcc66 +green = #99cc99 +cyan = #66cccc +blue = #6699cc +purple = #cc99cc + +[Project] +root = blue +folder = yellow +file = default +title = green +chapter = red +scene = blue +note = yellow +active = green +inactive = red +disabled = faded + +[Palette] +window = #393939 +windowtext = default +base = base +alternatebase = #515151 +text = default +tooltipbase = #ffffc0 +tooltiptext = #15150d +button = base +buttontext = default +brighttext = base +highlight = #2c98f7 +highlightedtext = #ffffff +link = blue +linkvisited = blue +accent = #2c98f7 + +[GUI] +helptext = #a4a4a4 +fadedtext = #949494 +errortext = red + +[Syntax] +background = base +text = default +line = default:32 +link = blue +headertext = blue +headertag = blue:D150 +emphasis = orange +dialog = green +altdialog = yellow +note = #ffe6b3 +hidden = faded +shortcode = blue +keyword = red +tag = purple +value = yellow +optional = blue +spellcheckline = red +errorline = green +replacetag = cyan +modifier = orange +texthighlight = yellow:96 diff --git a/novelwriter/assets/themes/vivid_black_green.conf b/novelwriter/assets/themes/vivid_black_green.conf new file mode 100644 index 00000000..f2d17043 --- /dev/null +++ b/novelwriter/assets/themes/vivid_black_green.conf @@ -0,0 +1,75 @@ +[Main] +name = Vivid Black Green +mode = dark +author = Myian + +[Base] +base = #0c0f16 +default = #f1f1f1 +faded = #888e9e +red = #ff3a64 +orange = #ff823e +yellow = #fff461 +green = #3dff5d +cyan = #3ff3f0 +blue = #2f9aff +purple = #ae70ff + +#magenta = #ff61e5 + +[Project] +root = default +folder = yellow +file = default +title = blue +chapter = cyan +scene = green +note = red +active = green +inactive = faded +disabled = faded:D175 + +[Palette] +window = base +windowtext = default +base = base +alternatebase = #323742 +text = default +tooltipbase = green:L140 +tooltiptext = base +button = #131622 +buttontext = default +brighttext = base +highlight = green:D125 +highlightedtext = base +link = green +linkvisited = green +accent = green:D125 + +[GUI] +helptext = faded +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #1c1f27 +link = blue +headertext = cyan +headertag = cyan:128 +emphasis = blue +dialog = green +altdialog = cyan +note = red +hidden = faded +shortcode = #ff61e5 +keyword = yellow +tag = orange +value = purple +optional = faded +spellcheckline = #ff3737 +errorline = cyan +replacetag = orange +modifier = #ff61e5 +texthighlight = green:80 diff --git a/novelwriter/assets/themes/vivid_black_red.conf b/novelwriter/assets/themes/vivid_black_red.conf new file mode 100644 index 00000000..fd7d99f2 --- /dev/null +++ b/novelwriter/assets/themes/vivid_black_red.conf @@ -0,0 +1,75 @@ +[Main] +name = Vivid Black Red +mode = dark +author = Myian + +[Base] +base = #0c0f16 +default = #f1f1f1 +faded = #888e9e +red = #ff3a64 +orange = #ff823e +yellow = #fff461 +green = #3dff5d +cyan = #3ff3f0 +blue = #2f9aff +purple = #ae70ff + +#magenta = #ff61e5 + +[Project] +root = default +folder = yellow +file = default +title = purple +chapter = #ff61e5 +scene = red +note = green +active = red +inactive = faded +disabled = faded:D175 + +[Palette] +window = base +windowtext = default +base = base +alternatebase = #323742 +text = default +tooltipbase = red:L150 +tooltiptext = base +button = #131622 +buttontext = default +brighttext = base +highlight = red +highlightedtext = base +link = red +linkvisited = red +accent = red + +[GUI] +helptext = faded +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #1c1f27 +link = blue +headertext = yellow +headertag = yellow:128 +emphasis = orange +dialog = red +altdialog = yellow +note = green +hidden = faded +shortcode = cyan +keyword = #ff61e5 +tag = purple +value = blue +optional = faded +spellcheckline = #ff3737 +errorline = cyan +replacetag = #ff61e5 +modifier = cyan +texthighlight = red:80 \ No newline at end of file diff --git a/novelwriter/assets/themes/vivid_white_green.conf b/novelwriter/assets/themes/vivid_white_green.conf new file mode 100644 index 00000000..7c299bdc --- /dev/null +++ b/novelwriter/assets/themes/vivid_white_green.conf @@ -0,0 +1,75 @@ +[Main] +name = Vivid White Green +mode = light +author = Myian + +[Base] +base = #ffffff +default = #222127 +faded = #78748a +red = #e72b44 +orange = #e46802 +yellow = #bd9205 +green = #009e1a +cyan = #0093a7 +blue = #3d58f1 +purple = #7c17ff + +#magenta = #ed14f5 + +[Project] +root = default +folder = yellow +file = faded +title = blue +chapter = cyan +scene = green +note = red +active = green +inactive = faded +disabled = faded:L135 + +[Palette] +window = base +windowtext = default +base = base +alternatebase = #d8dce0 +text = default +tooltipbase = green:L275 +tooltiptext = default +button = base +buttontext = default +brighttext = base +highlight = green +highlightedtext = base +link = green +linkvisited = green +accent = green + +[GUI] +helptext = default:L250 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #f3f3f8 +link = blue +headertext = cyan +headertag = cyan:128 +emphasis = blue +dialog = green +altdialog = cyan +note = red +hidden = faded +shortcode = #ed14f5 +keyword = yellow +tag = orange +value = purple +optional = faded +spellcheckline = red +errorline = cyan +replacetag = orange +modifier = #ed14f5 +texthighlight = green:72 diff --git a/novelwriter/assets/themes/vivid_white_red.conf b/novelwriter/assets/themes/vivid_white_red.conf new file mode 100644 index 00000000..e77b42b3 --- /dev/null +++ b/novelwriter/assets/themes/vivid_white_red.conf @@ -0,0 +1,75 @@ +[Main] +name = Vivid White Red +mode = light +author = Myian + +[Base] +base = #ffffff +default = #222127 +faded = #78748a +red = #e72b44 +orange = #e46802 +yellow = #bd9205 +green = #009e1a +cyan = #0093a7 +blue = #3d58f1 +purple = #7c17ff + +#magenta = #ed14f5 + +[Project] +root = default +folder = yellow +file = faded +title = purple +chapter = #ed14f5 +scene = red +note = green +active = red +inactive = faded +disabled = faded:L135 + +[Palette] +window = base +windowtext = default +base = base +alternatebase = #d8dce0 +text = default +tooltipbase = red:L175 +tooltiptext = default +button = base +buttontext = default +brighttext = base +highlight = red +highlightedtext = base +link = red +linkvisited = red +accent = red + +[GUI] +helptext = default:L250 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #f3f3f8 +link = blue +headertext = yellow +headertag = yellow:128 +emphasis = orange +dialog = red +altdialog = #a37e03 +note = green +hidden = faded +shortcode = cyan +keyword = #ed14f5 +tag = purple +value = blue +optional = faded +spellcheckline = red +errorline = cyan +replacetag = #ed14f5 +modifier = cyan +texthighlight = red:72 diff --git a/novelwriter/assets/themes/warpgate.conf b/novelwriter/assets/themes/warpgate.conf new file mode 100644 index 00000000..1398732f --- /dev/null +++ b/novelwriter/assets/themes/warpgate.conf @@ -0,0 +1,74 @@ +[Main] +name = Warpgate +mode = dark +author = Myian +credit = PlanetSide 2 + +[Base] +base = #06232a +default = #f9fcfd +faded = #729faa +red = #fb564a +orange = #f67e0a +yellow = #fbcd42 +green = #5dff6a +cyan = #2ee7f8 +blue = #74a0f6 +purple = #c07efc + +[Project] +root = default +folder = yellow +file = default +title = red +chapter = purple +scene = blue +note = green +active = green +inactive = red +disabled = faded + +[Palette] +window = base:D150 +windowtext = default +base = base +alternatebase = #1a5463 +text = default +tooltipbase = #c2ffaa +tooltiptext = base +button = #00252e +buttontext = default +brighttext = base +highlight = #58d662 +highlightedtext = base +link = green +linkvisited = green +accent = green + +[GUI] +helptext = faded +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = base:L150 +link = green +headertext = green +headertag = green:128 +emphasis = #fff6aa +dialog = cyan +altdialog = #11bdc6 +note = green +hidden = faded +shortcode = orange +keyword = orange +tag = yellow +value = yellow +optional = faded +spellcheckline = #ff4444 +errorline = green +replacetag = green +modifier = orange +texthighlight = #1a5463 diff --git a/novelwriter/assets/themes/waterlily_dark.conf b/novelwriter/assets/themes/waterlily_dark.conf new file mode 100644 index 00000000..92395b9c --- /dev/null +++ b/novelwriter/assets/themes/waterlily_dark.conf @@ -0,0 +1,73 @@ +[Main] +name = Waterlily Dark +mode = dark +author = Myian + +[Base] +base = #1e2b33 +default = #d7dbdd +faded = #869aa5 +red = #ff7474 +orange = #fca469 +yellow = #f7eaa4 +green = #9ae37f +cyan = #8ef3d1 +blue = #71caeb +purple = #c89dfd + +[Project] +root = default +folder = blue +file = purple +title = green +chapter = yellow +scene = #ff9fbd +note = faded +active = #ff9fbd +inactive = faded +disabled = #374a5c + +[Palette] +window = #151f25 +windowtext = default +base = base +alternatebase = #3b515f +text = default +tooltipbase = yellow +tooltiptext = base +button = base +buttontext = default +brighttext = base +highlight = #87bcc0 +highlightedtext = base +link = blue +linkvisited = blue +accent = #ff9fbd + +[GUI] +helptext = faded +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = default +line = #283842 +link = blue +headertext = default +headertag = faded +emphasis = green +dialog = #ff9fbd +altdialog = orange +note = blue +hidden = faded +shortcode = purple +keyword = purple +tag = yellow +value = cyan +optional = faded +spellcheckline = #ff5d5d +errorline = #4ae698 +replacetag = blue +modifier = purple +texthighlight = blue:64 diff --git a/novelwriter/assets/themes/waterlily_light.conf b/novelwriter/assets/themes/waterlily_light.conf new file mode 100644 index 00000000..960baa67 --- /dev/null +++ b/novelwriter/assets/themes/waterlily_light.conf @@ -0,0 +1,73 @@ +[Main] +name = Waterlily Light +mode = light +author = Myian + +[Base] +base = #f5faf7 +default = #435c4e +faded = #91ac96 +red = #d36868 +orange = #e08b63 +yellow = #cab330 +green = #62a729 +cyan = #31b6a4 +blue = #589dca +purple = #bd7fe7 + +[Project] +root = #526b5d +folder = blue +file = purple +title = green +chapter = yellow +scene = #e6859a +note = faded +active = #da7b8f +inactive = faded +disabled = #b6c9ba + +[Palette] +window = #e2eee7 +windowtext = default +base = base +alternatebase = #c7e0d1 +text = default +tooltipbase = #f3ebbb +tooltiptext = default +button = base +buttontext = default +brighttext = base +highlight = #6bb3b3 +highlightedtext = base +link = blue +linkvisited = blue +accent = #e6859a + +[GUI] +helptext = default:192 +fadedtext = faded +errortext = red + +[Syntax] +background = base +text = #54615a +line = #e9f3ed +link = #0b9caf +headertext = #526b5d +headertag = #526b5d80 +emphasis = #1e920e +dialog = #d15574 +altdialog = #d86f3e +note = #0b9caf +hidden = #89a189 +shortcode = #0b9caf +keyword = #9b5cc5 +tag = #bd911b +value = #04a770 +optional = #89a189 +spellcheckline = #ff3e3e +errorline = #04a770 +replacetag = #0b9caf +modifier = #9b5cc5 +texthighlight = cyan:64 diff --git a/novelwriter/common.py b/novelwriter/common.py index 2ad42935..8fd9386a 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import json @@ -31,6 +31,7 @@ import xml.etree.ElementTree as ET from configparser import ConfigParser from datetime import datetime +from enum import Enum from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, TypeGuard, TypeVar from urllib.parse import urljoin @@ -134,7 +135,7 @@ def checkPath(value: Any, default: Path) -> Path: def isHandle(value: Any) -> TypeGuard[str]: """Check if a string is a valid novelWriter handle. - Note: This is case sensitive. Must be lower case! + Note: This is case sensitive. Must be lower case. """ if not isinstance(value, str): return False @@ -218,7 +219,7 @@ def firstFloat(*args: Any) -> float: ## def formatInt(value: int) -> str: - """Formats an integer with k, M, G etc.""" + """Format an integer with k, M, G etc.""" if not isinstance(value, int): return "ERR" @@ -444,6 +445,7 @@ def fontMatcher(font: QFont) -> QFont: default Qt font matching algorithm doesn't handle well changing application fonts at runtime. """ + font.setStyleName(None) # Make sure no font style name is set from config, see #2502 info = QFontInfo(font) if (famRequest := font.family()) != (famActual := info.family()): logger.warning("Font mismatch: Requested '%s', but got '%s'", famRequest, famActual) @@ -462,21 +464,21 @@ def fontMatcher(font: QFont) -> QFont: def qtLambda(func: Callable, *args: Any, **kwargs: Any) -> Callable: - """A replacement for Python lambdas that works for Qt slots.""" + """A replacement for Python lambdas that works for Qt slots.""" # noqa: D401 def wrapper(*a_: Any) -> None: func(*args, **kwargs) return wrapper def qtAddAction(parent: QWidget, label: str) -> QAction: - """Helper to add action to widget and always return the action.""" + """Helper to add action to widget and always return the action.""" # noqa: D401 action = QAction(label, parent) parent.addAction(action) return action def qtAddMenu(parent: QMenuBar | QMenu, label: str) -> QMenu: - """Helper to add menu to menu and always return the menu.""" + """Helper to add menu to menu and always return the menu.""" # noqa: D401 menu = QMenu(label, parent) parent.addMenu(menu) return menu @@ -485,7 +487,6 @@ def qtAddMenu(parent: QMenuBar | QMenu, label: str) -> QMenu: def encodeMimeHandles(mimeData: QMimeData, handles: list[str]) -> None: """Encode handles into a mime data object.""" mimeData.setData(nwConst.MIME_HANDLE, b"|".join(h.encode() for h in handles)) - return def decodeMimeHandles(mimeData: QMimeData) -> list[str]: @@ -493,6 +494,21 @@ def decodeMimeHandles(mimeData: QMimeData) -> list[str]: return mimeData.data(nwConst.MIME_HANDLE).data().decode().split("|") +def utf16CharMap(text: str) -> list[int]: + """Compute mapping from Python string index to QString index. + Python strings are always one character per position in either + ASCII, UCS-2 or UCS-4. QStrings are in UTF-16, so wide characters + use 2 indices, and thus create an offset. + """ + utf16Map = list(range(0, len(text) + 1)) + offset = 0 + for i, c in enumerate(text, 1): + if ord(c) > 0xffff: + offset += 1 + utf16Map[i] = i + offset + return utf16Map + + ## # Encoder Functions ## @@ -519,7 +535,7 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str: elif first in ("{", "["): n += 1 indent = "\n"+" "*n - if n > nmax and nmax > 0: + if n > nmax > 0: buffer.append(chunk) else: buffer.append(chunk[0] + indent + chunk[1:]) @@ -527,13 +543,13 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str: elif first in ("}", "]"): n -= 1 indent = "\n"+" "*n - if n >= nmax and nmax > 0: + if n >= nmax > 0: buffer.append(chunk) else: buffer.append(indent + chunk) elif first == ",": - if n > nmax and nmax > 0: + if n > nmax > 0: buffer.append(chunk) else: buffer.append(chunk[0] + indent + chunk[1:].lstrip()) @@ -551,7 +567,7 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str: def xmlIndent(xml: ET.Element | ET.ElementTree) -> None: """A modified version of the XML indent function in the standard library. It behaves more closely to how the one from lxml does. - """ + """ # noqa: D401 tree = xml.getroot() if isinstance(xml, ET.ElementTree) else xml if not isinstance(tree, ET.Element): return @@ -581,8 +597,6 @@ def xmlIndent(xml: ET.Element | ET.ElementTree) -> None: if last is not None: last.tail = indentations[level] - return - if len(tree): indentChildren(tree, 0) tree.tail = "\n" @@ -597,7 +611,7 @@ def xmlElement( attrib: dict | None = None, tail: str | None = None, ) -> ET.Element: - """A custom implementation of Element with more arguments.""" + """A custom implementation of Element with more arguments.""" # noqa: D401 xSub = ET.Element(tag, attrib=attrib or {}) if text is not None: if isinstance(text, bool): @@ -617,7 +631,7 @@ def xmlSubElem( attrib: dict | None = None, tail: str | None = None, ) -> ET.Element: - """A custom implementation of SubElement with more arguments.""" + """A custom implementation of SubElement with more arguments.""" # noqa: D401 xSub = ET.SubElement(parent, tag, attrib=attrib or {}) if text is not None: if isinstance(text, bool): @@ -648,6 +662,7 @@ def readTextFile(path: str | Path) -> str: def makeFileNameSafe(text: str) -> str: """Return a filename-safe string. + See: https://unicode.org/reports/tr15/#Norm_Forms """ text = unicodedata.normalize("NFKC", text).strip() @@ -676,8 +691,11 @@ def openExternalPath(path: Path) -> bool: # Classes ## +_T_Enum = TypeVar("_T_Enum", bound=Enum) + + class NWConfigParser(ConfigParser): - """Common: Adapted Config Parser + """Common: Adapted Config Parser. This is a subclass of the standard config parser that adds type safe helper functions, and support for lists. It also turns off @@ -686,7 +704,6 @@ class NWConfigParser(ConfigParser): def __init__(self) -> None: super().__init__(interpolation=None) - return def rdStr(self, section: str, option: str, default: str) -> str: """Read string value.""" @@ -737,3 +754,10 @@ class NWConfigParser(ConfigParser): for i in range(min(len(data), len(result))): result[i] = checkInt(data[i].strip(), result[i]) return result + + def rdEnum(self, section: str, option: str, default: _T_Enum) -> _T_Enum: + """Read enum value.""" + if self.has_option(section, option): + data = self.get(section, option, fallback="") + return type(default).__members__.get(data.upper(), default) + return default diff --git a/novelwriter/config.py b/novelwriter/config.py index 0c6d43bb..6ce9d434 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -22,7 +22,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import json @@ -45,6 +45,7 @@ from novelwriter.common import ( formatTimeStamp, processDialogSymbols, simplified ) from novelwriter.constants import nwFiles, nwQuotes, nwUnicode +from novelwriter.enum import nwTheme from novelwriter.error import formatException, logException if TYPE_CHECKING: @@ -55,13 +56,20 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) -DEF_GUI = "default" -DEF_SYNTAX = "default_light" +DEF_GUI_DARK = "default_dark" +DEF_GUI_LIGHT = "default_light" DEF_ICONS = "material_rounded_normal" DEF_TREECOL = "theme" class Config: + """User Config. + + The main user config. The state of the config is stored in the + novelwriter.conf file between sessions. Most of the settings can be + modified by the user in the Preferences dialog, but some just record + various states of the GUI. + """ __slots__ = ( "_appPath", "_appRoot", "_backPath", "_backupPath", "_confPath", "_dLocale", "_dShortDate", @@ -69,22 +77,23 @@ class Config: "_manuals", "_nwLangPath", "_qLocale", "_qtLangPath", "_qtTrans", "_recentPaths", "_recentProjects", "_splash", "allowOpenDial", "altDialogClose", "altDialogOpen", "appHandle", "appName", "askBeforeBackup", "askBeforeExit", "autoSaveDoc", "autoSaveProj", - "autoScroll", "autoScrollPos", "autoSelect", "backupOnClose", "cursorWidth", "dialogLine", - "dialogStyle", "doJustify", "doReplace", "doReplaceDQuote", "doReplaceDash", + "autoScroll", "autoScrollPos", "autoSelect", "backupOnClose", "cursorWidth", "darkTheme", + "dialogLine", "dialogStyle", "doJustify", "doReplace", "doReplaceDQuote", "doReplaceDash", "doReplaceDots", "doReplaceSQuote", "emphLabels", "fmtApostrophe", "fmtDQuoteClose", "fmtDQuoteOpen", "fmtPadAfter", "fmtPadBefore", "fmtPadThin", "fmtSQuoteClose", - "fmtSQuoteOpen", "focusWidth", "guiFont", "guiLocale", "guiSyntax", "guiTheme", - "hasEnchant", "hideFocusFooter", "hideHScroll", "hideVScroll", "highlightEmph", "hostName", - "iconColDocs", "iconColTree", "iconTheme", "incNotesWCount", "isDebug", "kernelVer", - "lastNotes", "mainPanePos", "mainWinSize", "memInfo", "narratorBreak", "narratorDialog", - "nativeFont", "osDarwin", "osLinux", "osType", "osUnknown", "osWindows", "outlinePanePos", - "prefsWinSize", "scrollPastEnd", "searchCase", "searchLoop", "searchMatchCap", - "searchNextFile", "searchProjCase", "searchProjRegEx", "searchProjWord", "searchRegEx", - "searchWord", "showEditToolBar", "showFullPath", "showLineEndings", "showMultiSpaces", - "showSessionTime", "showTabsNSpaces", "showViewerPanel", "spellLanguage", "stopWhenIdle", - "tabWidth", "textFont", "textMargin", "textWidth", "useCharCount", "userIdleTime", - "verPyQtString", "verPyQtValue", "verPyString", "verQtString", "verQtValue", - "viewComments", "viewPanePos", "viewSynopsis", "welcomeWinSize", + "fmtSQuoteOpen", "focusWidth", "guiFont", "guiLocale", "hasEnchant", "hideFocusFooter", + "hideHScroll", "hideVScroll", "highlightEmph", "hostName", "iconColDocs", "iconColTree", + "iconTheme", "incNotesWCount", "isDebug", "kernelVer", "lastNotes", "lightTheme", + "lineHighlight", "mainPanePos", "mainWinSize", "memInfo", "narratorBreak", + "narratorDialog", "nativeFont", "osDarwin", "osLinux", "osType", "osUnknown", "osWindows", + "outlinePanePos", "prefsWinSize", "scrollPastEnd", "searchCase", "searchLoop", + "searchMatchCap", "searchNextFile", "searchProjCase", "searchProjRegEx", "searchProjWord", + "searchRegEx", "searchWord", "showEditToolBar", "showFullPath", "showLineEndings", + "showMultiSpaces", "showSessionTime", "showTabsNSpaces", "showViewerPanel", + "spellLanguage", "stopWhenIdle", "tabWidth", "textFont", "textMargin", "textWidth", + "themeMode", "useCharCount", "userIdleTime", "verPyQtString", "verPyQtValue", + "verPyString", "verQtString", "verQtValue", "viewComments", "viewNotes", "viewPanePos", + "viewSynopsis", "welcomeWinSize", ) LANG_NW = 1 @@ -153,14 +162,15 @@ class Config: # General GUI Settings self.guiLocale = self._qLocale.name() - self.guiTheme = DEF_GUI # GUI theme - self.guiSyntax = DEF_SYNTAX # Syntax theme - self.guiFont = QFont() # Main GUI font - self.hideVScroll = False # Hide vertical scroll bars on main widgets - self.hideHScroll = False # Hide horizontal scroll bars on main widgets - self.lastNotes = "0x0" # The latest release notes that have been shown - self.nativeFont = True # Use native font dialog - self.useCharCount = False # Use character count as primary count + self.lightTheme = DEF_GUI_LIGHT # Light GUI theme + self.darkTheme = DEF_GUI_DARK # Dark GUI theme + self.themeMode = nwTheme.AUTO # Colour theme mode + self.guiFont = QFont() # Main GUI font + self.hideVScroll = False # Hide vertical scroll bars on main widgets + self.hideHScroll = False # Hide horizontal scroll bars on main widgets + self.lastNotes = "0x0" # The latest release notes that have been shown + self.nativeFont = True # Use native font dialog + self.useCharCount = False # Use character count as primary count # Icons self.iconTheme = DEF_ICONS # Icons theme @@ -178,8 +188,8 @@ class Config: # Project Settings self.autoSaveProj = 60 # Interval for auto-saving project, in seconds self.autoSaveDoc = 30 # Interval for auto-saving document, in seconds - self.emphLabels = True # Add emphasis to H1 and H2 item labels - self.backupOnClose = False # Flag for running automatic backups + self.emphLabels = False # Add emphasis to H1 and H2 item labels + self.backupOnClose = True # Flag for running automatic backups self.askBeforeBackup = True # Flag for asking before running automatic backup self.askBeforeExit = True # Flag for asking before exiting the app @@ -189,6 +199,7 @@ class Config: self.textMargin = 40 # Editor/viewer text margin self.tabWidth = 40 # Editor tabulator width self.cursorWidth = 1 # Editor cursor width + self.lineHighlight = False # Highlight current line in editor self.focusWidth = 800 # Focus Mode text width self.hideFocusFooter = False # Hide document footer in Focus Mode @@ -198,7 +209,7 @@ class Config: self.doJustify = False # Justify text self.showTabsNSpaces = False # Show tabs and spaces in editor self.showLineEndings = False # Show line endings in editor - self.showMultiSpaces = True # Highlight multiple spaces in the text + self.showMultiSpaces = False # Highlight multiple spaces in the text self.doReplace = True # Enable auto-replace as you type self.doReplaceSQuote = True # Smart single quotes @@ -206,7 +217,7 @@ class Config: self.doReplaceDash = True # Replace multiple hyphens with dashes self.doReplaceDots = True # Replace three dots with ellipsis - self.autoScroll = False # Typewriter-like scrolling + self.autoScroll = True # Typewriter-like scrolling self.autoScrollPos = 30 # Start point for typewriter-like scrolling self.scrollPastEnd = True # Scroll past end of document, and centre cursor @@ -245,6 +256,7 @@ class Config: self.showSessionTime = True # Show the session time in the status bar self.viewComments = True # Comments are shown in the viewer self.viewSynopsis = True # Synopsis is shown in the viewer + self.viewNotes = True # Notes are shown in the viewer # Search Box States self.searchCase = False @@ -295,8 +307,6 @@ class Config: # Packages self.hasEnchant = False # The pyenchant package - return - ## # Properties ## @@ -345,7 +355,6 @@ class Config: def setLastAuthor(self, value: str) -> None: """Set tle last used author name.""" self._lastAuthor = simplified(value) - return def setMainWinSize(self, width: int, height: int) -> None: """Set the size of the main window, but only if the change is @@ -357,17 +366,14 @@ class Config: self.mainWinSize[0] = width if abs(self.mainWinSize[1] - height) > 5: self.mainWinSize[1] = height - return def setWelcomeWinSize(self, width: int, height: int) -> None: """Set the size of the Preferences dialog window.""" self.welcomeWinSize = [width, height] - return def setPreferencesWinSize(self, width: int, height: int) -> None: """Set the size of the Preferences dialog window.""" self.prefsWinSize = [width, height] - return def setLastPath(self, key: str, path: str | Path) -> None: """Set the last used path. Only the folder is saved, so if the @@ -379,12 +385,10 @@ class Config: path = path.parent if path.is_dir(): self._recentPaths.setPath(key, path) - return def setBackupPath(self, path: Path | str) -> None: """Set the current backup path.""" self._backupPath = checkPath(path, self._backPath) - return def setGuiFont(self, value: QFont | str | None) -> None: """Update the GUI's font style from settings.""" @@ -403,9 +407,8 @@ class Config: else: font = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont) self.guiFont = fontMatcher(font) - logger.debug("GUI font set to: %s", describeFont(font)) + logger.debug("Main font set to: %s", describeFont(font)) QApplication.setFont(self.guiFont) - return def setTextFont(self, value: QFont | str | None) -> None: """Set the text font if it exists. If it doesn't, or is None, @@ -431,14 +434,13 @@ class Config: font = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont) self.textFont = fontMatcher(font) logger.debug("Text font set to: %s", describeFont(self.textFont)) - return ## # Methods ## def homePath(self) -> Path: - """The user's home folder.""" + """Return the user's home folder.""" return self._homePath def dataPath(self, target: str | None = None) -> Path: @@ -520,7 +522,6 @@ class Config: """Send a message to the splash screen.""" if self._splash: self._splash.showStatus(message) - return ## # Config Actions @@ -551,11 +552,10 @@ class Config: self._confPath.mkdir(exist_ok=True) self._dataPath.mkdir(exist_ok=True) - # Also create the syntax, themes and icons folders if possible + # Also create the themes and icons folders if possible if self._dataPath.is_dir(): (self._dataPath / "cache").mkdir(exist_ok=True) (self._dataPath / "icons").mkdir(exist_ok=True) - (self._dataPath / "syntax").mkdir(exist_ok=True) (self._dataPath / "themes").mkdir(exist_ok=True) self._recentPaths.loadCache() @@ -564,8 +564,6 @@ class Config: logger.debug("Config instance initialised") - return - def initLocalisation(self, nwApp: QApplication) -> None: """Initialise the localisation of the GUI.""" self.splashMessage("Loading localisation ...") @@ -593,8 +591,6 @@ class Config: nwApp.installTranslator(qTrans) self._qtTrans[lngFile] = qTrans - return - def loadConfig(self, splash: NSplashScreen | None = None) -> bool: """Load preferences from file and replace default settings.""" self._splash = splash @@ -626,8 +622,9 @@ class Config: # Main sec = "Main" self.setGuiFont(conf.rdStr(sec, "font", "")) - self.guiTheme = conf.rdStr(sec, "theme", self.guiTheme) - self.guiSyntax = conf.rdStr(sec, "syntax", self.guiSyntax) + self.lightTheme = conf.rdStr(sec, "lighttheme", self.lightTheme) + self.darkTheme = conf.rdStr(sec, "darktheme", self.darkTheme) + self.themeMode = conf.rdEnum(sec, "thememode", self.themeMode) self.iconTheme = conf.rdStr(sec, "icons", self.iconTheme) self.iconColTree = conf.rdStr(sec, "iconcoltree", self.iconColTree) self.iconColDocs = conf.rdBool(sec, "iconcoldocs", self.iconColDocs) @@ -665,6 +662,7 @@ class Config: self.textMargin = conf.rdInt(sec, "margin", self.textMargin) self.tabWidth = conf.rdInt(sec, "tabwidth", self.tabWidth) self.cursorWidth = conf.rdInt(sec, "cursorwidth", self.cursorWidth) + self.lineHighlight = conf.rdBool(sec, "linehighlight", self.lineHighlight) self.focusWidth = conf.rdInt(sec, "focuswidth", self.focusWidth) self.hideFocusFooter = conf.rdBool(sec, "hidefocusfooter", self.hideFocusFooter) self.doJustify = conf.rdBool(sec, "justify", self.doJustify) @@ -708,6 +706,7 @@ class Config: self.showSessionTime = conf.rdBool(sec, "showsessiontime", self.showSessionTime) self.viewComments = conf.rdBool(sec, "viewcomments", self.viewComments) self.viewSynopsis = conf.rdBool(sec, "viewsynopsis", self.viewSynopsis) + self.viewNotes = conf.rdBool(sec, "viewnotes", self.viewNotes) self.searchCase = conf.rdBool(sec, "searchcase", self.searchCase) self.searchWord = conf.rdBool(sec, "searchword", self.searchWord) self.searchRegEx = conf.rdBool(sec, "searchregex", self.searchRegEx) @@ -721,7 +720,7 @@ class Config: # Check Values # ============ - self._prepareFont(self.guiFont, "GUI") + self._prepareFont(self.guiFont, "main") self._prepareFont(self.textFont, "document") # If we're using straight quotes, disable auto-replace @@ -751,8 +750,9 @@ class Config: conf["Main"] = { "font": self.guiFont.toString(), - "theme": str(self.guiTheme), - "syntax": str(self.guiSyntax), + "lighttheme": str(self.lightTheme), + "darktheme": str(self.darkTheme), + "thememode": self.themeMode.name, "icons": str(self.iconTheme), "iconcoltree": str(self.iconColTree), "iconcoldocs": str(self.iconColDocs), @@ -790,6 +790,7 @@ class Config: "margin": str(self.textMargin), "tabwidth": str(self.tabWidth), "cursorwidth": str(self.cursorWidth), + "lineHighlight": str(self.lineHighlight), "focuswidth": str(self.focusWidth), "hidefocusfooter": str(self.hideFocusFooter), "justify": str(self.doJustify), @@ -833,6 +834,7 @@ class Config: "showsessiontime": str(self.showSessionTime), "viewcomments": str(self.viewComments), "viewsynopsis": str(self.viewSynopsis), + "viewnotes": str(self.viewNotes), "searchcase": str(self.searchCase), "searchword": str(self.searchWord), "searchregex": str(self.searchRegEx), @@ -862,7 +864,6 @@ class Config: def finishStartup(self) -> None: """Call after startup is complete.""" self._splash = None - return ## # Internal Functions @@ -884,7 +885,6 @@ class Config: else: self.hasEnchant = True logger.debug("Checking package 'pyenchant': OK") - return def _prepareFont(self, font: QFont, kind: str) -> None: """Check Unicode availability in font. This also initialises any @@ -895,16 +895,15 @@ class Config: for char in nwUnicode.UI_SYMBOLS: if not metrics.inFont(char): # type: ignore logger.warning("No glyph U+%04x in font", ord(char)) # pragma: no cover - return class RecentProjects: + """A record of recently opened projects.""" def __init__(self, config: Config) -> None: self._conf = config self._data: dict[str, dict[str, str | int]] = {} self._map: dict[str, str] = {} - return def loadCache(self) -> bool: """Load the cache file for recent projects.""" @@ -966,14 +965,12 @@ class RecentProjects: self.saveCache() except Exception: pass - return def remove(self, path: str | Path) -> None: """Try to remove a path from the recent projects cache.""" if self._data.pop(str(path), None) is not None: logger.debug("Removed recent: %s", path) self.saveCache() - return def _setEntry( self, puuid: str, path: str, title: str, words: int, chars: int, saved: int @@ -988,24 +985,22 @@ class RecentProjects: } if puuid: self._map[puuid] = path - return class RecentPaths: + """A record of recently used file paths.""" KEYS: Final[list[str]] = ["default", "project", "import", "outline", "stats"] def __init__(self, config: Config) -> None: self._conf = config self._data = {} - return def setPath(self, key: str, path: Path | str) -> None: """Set a path for a given key, and save the cache.""" if key in self.KEYS: self._data[key] = str(path) self.saveCache() - return def getPath(self, key: str) -> str | None: """Get a path for a given key, or return None.""" diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 191bcc1c..5ea3f8ff 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from typing import Final @@ -28,21 +28,23 @@ from typing import Final from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication from novelwriter.enum import ( - nwBuildFmt, nwComment, nwItemClass, nwItemLayout, nwOutline, nwStatusShape + nwBuildFmt, nwComment, nwItemClass, nwItemLayout, nwOutline, nwStatusShape, + nwTheme ) def trConst(text: str) -> str: - """Wrapper function for locally translating constants.""" + """Translate a constant.""" return QCoreApplication.translate("Constant", text) def trStats(text: str) -> str: - """Wrapper function for locally translating stats constants.""" + """Translate a stats constants.""" return QCoreApplication.translate("Stats", text) class nwConst: + """Various Constants.""" # Date and Time Formats FMT_TSTAMP = "%Y-%m-%d %H:%M:%S" # Default format @@ -69,6 +71,7 @@ class nwConst: class nwRegEx: + """Common RegExes.""" URL = r"https?://(?:www\.|(?!www))[\w/()@:%_\+-.~#?&=]+" WORDS = r"\b[^\s\-\+\/–—\[\]:]+\b" @@ -76,11 +79,13 @@ class nwRegEx: FMT_EI = r"(?. -""" +""" # noqa from __future__ import annotations import json @@ -211,7 +211,7 @@ class FilterMode(Enum): class BuildSettings: - """Core: Build Settings Class + """Core: Build Settings Class. This class manages the build settings for a Manuscript build job. The settings can be packed/unpacked to/from a dictionary for JSON. @@ -229,7 +229,6 @@ class BuildSettings: self._included = set() self._settings = {k: v[1] for k, v in SETTINGS_TEMPLATE.items()} self._changed = False - return @classmethod def fromDict(cls, data: dict) -> BuildSettings: @@ -315,7 +314,6 @@ class BuildSettings: def setName(self, name: str) -> None: """Set the build setting display name.""" self._name = str(name) - return def setBuildID(self, value: str | uuid.UUID) -> None: """Set a UUID build ID.""" @@ -324,13 +322,11 @@ class BuildSettings: self._uuid = str(uuid.uuid4()) elif value != self._uuid: self._uuid = value - return def setOrder(self, value: int) -> None: """Set the build order.""" if isinstance(value, int): self._order = value - return def setLastBuildPath(self, path: Path | str | None) -> None: """Set the last used build path.""" @@ -341,41 +337,35 @@ class BuildSettings: else: self._path = CONFIG.homePath() self._changed = True - return def setLastBuildName(self, name: str) -> None: """Set the last used build name.""" self._build = str(name).strip() self._changed = True - return def setLastFormat(self, value: nwBuildFmt) -> None: """Set the last used build format.""" if isinstance(value, nwBuildFmt): self._format = value self._changed = True - return def setFiltered(self, tHandle: str) -> None: """Set an item as filtered.""" self._excluded.discard(tHandle) self._included.discard(tHandle) self._changed = True - return def setIncluded(self, tHandle: str) -> None: """Set an item as explicitly included.""" self._excluded.discard(tHandle) self._included.add(tHandle) self._changed = True - return def setExcluded(self, tHandle: str) -> None: """Set an item as explicitly excluded.""" self._excluded.add(tHandle) self._included.discard(tHandle) self._changed = True - return def setAllowRoot(self, tHandle: str, state: bool) -> None: """Set a specific root folder as allowed or not.""" @@ -385,14 +375,12 @@ class BuildSettings: elif state is False: self._skipRoot.add(tHandle) self._changed = True - return def setValue(self, key: str, value: T_BuildValue) -> None: """Set a specific value for a build setting.""" if (d := SETTINGS_TEMPLATE.get(key)) and len(d) == 2 and isinstance(value, d[0]): self._changed |= (value != self._settings[key]) self._settings[key] = value - return ## # Methods @@ -463,7 +451,6 @@ class BuildSettings: called when the changes have been safely saved or passed on. """ self._changed = False - return def pack(self) -> dict: """Pack all content into a JSON compatible dictionary.""" @@ -516,8 +503,6 @@ class BuildSettings: self._changed = False - return - @classmethod def duplicate(cls, source: BuildSettings) -> BuildSettings: """Make a copy of another build.""" @@ -529,7 +514,7 @@ class BuildSettings: class BuildCollection: - """Core: Build Collection Class + """Core: Build Collection Class. This object holds all the build setting objects defined by the given project. The build settings are saved as a single JSON file in the @@ -542,7 +527,6 @@ class BuildCollection: self._defaultBuild = "" self._builds: dict[str, BuildSettings] = {} self._loadCollection() - return def __len__(self) -> int: """Return the number of builds.""" @@ -581,21 +565,18 @@ class BuildCollection: build.setOrder(i) self._lastBuild = lastBuild self._saveCollection() - return def setDefaultBuild(self, buildID: str) -> None: """Set the default build id.""" if buildID != self._defaultBuild: self._defaultBuild = buildID self._saveCollection() - return def setBuild(self, build: BuildSettings) -> None: """Set build settings data in the collection.""" if isinstance(build, BuildSettings): self._builds[build.buildID] = build self._saveCollection() - return ## # Methods @@ -605,7 +586,6 @@ class BuildCollection: """Remove a build from the collection.""" self._builds.pop(buildID, None) self._saveCollection() - return def builds(self) -> Iterable[tuple[str, str]]: """Iterate over all available builds.""" diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index a4b2a0b1..5ec81031 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -23,7 +23,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -52,7 +52,9 @@ logger = logging.getLogger(__name__) class DocMerger: - """Document tool for merging a set of documents into a single new + """Tool: Merge Documents. + + Document tool for merging a set of documents into a single new document. The parameters are defined by the user using the GuiDocMerge dialog. """ @@ -62,7 +64,6 @@ class DocMerger: self._error = "" self._target = None self._text = [] - return @property def targetHandle(self) -> str | None: @@ -85,7 +86,6 @@ class DocMerger: """ self._target = self._project.tree[tHandle] self._text = [] - return def newTargetDoc(self, sHandle: str, label: str) -> None: """Create a brand new target document based on a source handle @@ -101,7 +101,6 @@ class DocMerger: nwItem.notifyToRefresh() self._target = nwItem self._text = [] - return def appendText(self, sHandle: str, addComment: bool, cmtPrefix: str) -> None: """Append text from an existing document to the text buffer.""" @@ -112,7 +111,6 @@ class DocMerger: status, _ = item.getImportStatus() text = f"% {cmtPrefix} {info}: {item.itemName} [{status}]\n\n{text}" self._text.append(text) - return def writeTargetDoc(self) -> bool: """Write the accumulated text into the designated target @@ -158,7 +156,8 @@ class DocSplitter: self._srcHandle = sHandle self._srcItem = srcItem - return + def __len__(self) -> int: + return len(self._rawData) ## # Methods @@ -174,7 +173,6 @@ class DocSplitter: """ self._parHandle = pHandle self._inFolder = False - return def newParentFolder(self, pHandle: str, folderLabel: str) -> None: """Create a new folder that will be the top level parent item @@ -188,7 +186,6 @@ class DocSplitter: nwItem.notifyToRefresh() self._parHandle = nHandle self._inFolder = True - return def splitDocument(self, splitData: list, splitText: list[str]) -> None: """Loop through the split data record and perform the split job @@ -200,12 +197,9 @@ class DocSplitter: chunk = buffer[lineNo:] buffer = buffer[:lineNo] self._rawData.insert(0, (chunk, hLevel, hLabel)) - return def writeDocuments(self, docHierarchy: bool) -> Iterable[bool]: - """An iterator that will write each document in the buffer, and - return its new handle, parent handle, and sibling handle. - """ + """Write each document in the buffer and yield if successful.""" if self._srcHandle and self._srcItem and self._parHandle: pHandle = self._parHandle hHandle = [self._parHandle, None, None, None, None] @@ -256,7 +250,6 @@ class DocDuplicator: def __init__(self, project: NWProject) -> None: self._project = project - return ## # Methods @@ -270,7 +263,9 @@ class DocDuplicator: after = True if items: hMap: dict[str, str | None] = {t: None for t in items} + SHARED.initMainProgress(len(items)) for tHandle in items: + SHARED.incMainProgress() if oldItem := self._project.tree[tHandle]: pHandle = hMap.get(oldItem.itemParent or "") or oldItem.itemParent if newItem := self._project.tree.duplicate(tHandle, pHandle, after): @@ -282,17 +277,21 @@ class DocDuplicator: after = False else: break + SHARED.clearMainProgress() return result class DocSearch: + """Tool: Search Documents. + + A global document search class. + """ def __init__(self) -> None: self._regEx = re.compile(r"") - self._opts = re.UNICODE | re.IGNORECASE + self._opts = re.IGNORECASE self._words = False self._escape = True - return ## # Methods @@ -300,32 +299,30 @@ class DocSearch: def setCaseSensitive(self, state: bool) -> None: """Set the case sensitive search flag.""" - self._opts = re.UNICODE - if not state: - self._opts |= re.IGNORECASE - return + self._opts = 0 if state else re.IGNORECASE def setWholeWords(self, state: bool) -> None: """Set the whole words search flag.""" self._words = state - return def setUserRegEx(self, state: bool) -> None: """Set the escape flag to the opposite state.""" self._escape = not state - return def iterSearch( self, project: NWProject, search: str ) -> Iterable[tuple[NWItem, list[tuple[int, int, str]], bool]]: - """Iteratively search through documents in a project.""" + """Iterate through documents in a project and apply search.""" self._regEx = re.compile(self._buildPattern(search), self._opts) logger.debug("Searching with pattern '%s'", self._regEx.pattern) storage = project.storage + SHARED.initMainProgress(len(project.tree)) for item in project.tree: + SHARED.incMainProgress() if item.isFileType(): results, capped = self.searchText(storage.getDocumentText(item.itemHandle)) yield item, results, capped + SHARED.clearMainProgress() return def searchText(self, text: str) -> tuple[list[tuple[int, int, str]], bool]: @@ -368,7 +365,6 @@ class ProjectBuilder: def __init__(self) -> None: self._path = None self.tr = partial(QCoreApplication.translate, "ProjectBuilder") - return @property def projPath(self) -> Path | None: @@ -609,6 +605,6 @@ class ProjectBuilder: project.data.setSaveCount(0) project.data.setAutoCount(0) project.data.setEditTime(0) + project.index.rebuild() project.saveProject() project.closeProject() - return diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index 8e89095d..32bccb1d 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -53,7 +53,7 @@ logger = logging.getLogger(__name__) class NWBuildDocument: - """Core: Manuscript Document Build Class + """Core: Manuscript Document Build Class. This is the core tool that assembles a project and outputs a manuscript, based on a build definition object (BuildSettings). @@ -72,7 +72,6 @@ class NWBuildDocument: self._cache = None self._count = False self._outline = False - return ## # Properties @@ -106,7 +105,6 @@ class NWBuildDocument: def addDocument(self, tHandle: str) -> None: """Add a document to the build queue manually.""" self._queue.append(tHandle) - return def queueAll(self) -> None: """Queue all document as defined by the build settings.""" @@ -115,7 +113,6 @@ class NWBuildDocument: for item in self._project.tree: if filtered.get(item.itemHandle, False): self._queue.append(item.itemHandle) - return def iterBuildPreview(self, newPage: bool) -> Iterable[tuple[int, bool]]: """Build a preview QTextDocument.""" @@ -131,7 +128,7 @@ class NWBuildDocument: return def iterBuildDocument(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]: - """Wrapper for builders based on format.""" + """Select a builder based on format.""" self._error = None self._cache = None @@ -341,12 +338,10 @@ class NWBuildDocument: scale*self._build.getFloat("format.rightMargin"), ) - filtered = self._build.buildItemFilter( + return self._build.buildItemFilter( self._project, withRoots=self._build.getBool("text.addNoteHeadings") ) - return filtered - def _doBuild(self, bldObj: Tokenizer, tHandle: str, convert: bool = True) -> bool: """Build a single document and add it to the build object.""" tItem = self._project.tree[tHandle] diff --git a/novelwriter/core/document.py b/novelwriter/core/document.py index e0f469e9..1cb6809c 100644 --- a/novelwriter/core/document.py +++ b/novelwriter/core/document.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import hashlib @@ -42,7 +42,7 @@ logger = logging.getLogger(__name__) class NWDocument: - """Core: Document Class + """Core: Document Class. A Class wrapping a single novelWriter document file. It represents a project item of nwItemType FILE. The file is not guaranteed to @@ -68,8 +68,6 @@ class NWDocument: if self._handle is not None: self._item = self._project.tree[tHandle] - return - def __repr__(self) -> str: return f"" @@ -357,5 +355,3 @@ class NWDocument: else: logger.debug("Unknown meta data: '%s'", metaLine.strip()) - - return diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 07933690..2a9aba3a 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -22,7 +22,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import json @@ -56,7 +56,7 @@ KEY_SOURCE = "0123456789bcdfghjklmnpqrstvwxz" class Index: - """Core: Project Index + """Core: Project Index. This class holds the entire index for a given project. The index contains the data that isn't stored in the project items themselves. @@ -99,8 +99,6 @@ class Index: self._indexChange = 0.0 self._rootChange = {} - return - def __repr__(self) -> str: return f"" @@ -129,7 +127,6 @@ class Index: def setNovelModelExtraColumn(self, extra: nwNovelExtra) -> None: """Set the data content type of the novel model extra column.""" self._novelExtra = extra - return ## # Public Methods @@ -142,20 +139,21 @@ class Index: self._indexChange = 0.0 self._rootChange = {} SHARED.emitIndexCleared(self._project) - return def rebuild(self) -> None: """Rebuild the entire index from scratch.""" self.clear() + SHARED.initMainProgress(len(self._project.tree)) for nwItem in self._project.tree: if nwItem.isFileType(): text = self._project.storage.getDocumentText(nwItem.itemHandle) self.scanText(nwItem.itemHandle, text, blockSignal=True) + SHARED.incMainProgress() self._indexBroken = False SHARED.emitIndexAvailable(self._project) for tHandle in self._novelModels: self.refreshNovelModel(tHandle) - return + SHARED.clearMainProgress() def deleteHandle(self, tHandle: str) -> None: """Delete all entries of a given document handle.""" @@ -165,7 +163,6 @@ class Index: del self._tagsIndex[tTag] del self._itemIndex[tHandle] SHARED.emitIndexChangedTags(self._project, [], delTags) - return def reIndexHandle(self, tHandle: str | None) -> None: """Put a file back into the index. This is used when files are @@ -175,7 +172,6 @@ class Index: if tHandle and self._project.tree.checkType(tHandle, nwItemType.FILE): logger.debug("Re-indexing item '%s'", tHandle) self.scanText(tHandle, self._project.storage.getDocumentText(tHandle)) - return def refreshHandle(self, tHandle: str) -> None: """Update the class for all tags of a handle.""" @@ -185,7 +181,6 @@ class Index: self.deleteHandle(tHandle) else: self._tagsIndex.updateClass(tHandle, item.itemClass.name) - return def indexChangedSince(self, checkTime: int | float) -> bool: """Check if the index has changed since a given time.""" @@ -208,7 +203,6 @@ class Index: model.setExtraColumn(self._novelExtra) self._appendSubTreeToModel(tHandle, model) model.endResetModel() - return def updateNovelModelData(self, nwItem: NWItem) -> bool: """Refresh a novel model.""" @@ -425,8 +419,6 @@ class Index: if updated or deleted: SHARED.emitIndexChangedTags(self._project, updated, deleted) - return - def _scanInactive(self, nwItem: NWItem, text: str) -> None: """Scan an inactive document for meta data.""" for line in text.splitlines(): @@ -435,7 +427,6 @@ class Index: if hDepth != "H0": nwItem.setMainHeading(hDepth) break - return def _splitHeading(self, line: str) -> tuple[str, str]: """Split a heading into its heading level and text value.""" @@ -459,7 +450,6 @@ class Index: """Count text stats and save the counts to the index.""" cC, wC, pC = standardCounter(text) self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC) - return def _indexKeyword( self, tHandle: str, line: str, sTitle: str, itemClass: nwItemClass, tags: dict[str, bool] @@ -495,7 +485,6 @@ class Index: model.setExtraColumn(self._novelExtra) self._appendSubTreeToModel(tHandle, model) self._novelModels[tHandle] = model - return def _appendSubTreeToModel(self, tHandle: str, model: NovelModel) -> None: """Append all active novel documents to a novel model.""" @@ -506,7 +495,6 @@ class Index: and node.item.isActive ): model.append(node) - return ## # Check @ Lines @@ -680,15 +668,13 @@ class Index: "words": hItem.wordCount, } - result = [( + return [( tKey, tData[tKey]["level"], tData[tKey]["title"], tData[tKey]["words"] ) for tKey in tOrder] - return result - def getCounts(self, tHandle: str, sTitle: str | None = None) -> tuple[int, int, int]: """Return the counts for a file, or a section of a file, starting at title sTitle if it is provided. @@ -793,7 +779,7 @@ class Index: # ===================== class TagsIndex: - """Core: Tags Index Wrapper Class + """Core: Tags Index Wrapper Class. A wrapper class that holds the reverse lookup tags index. This is just a simple wrapper around a single dictionary to keep tighter @@ -804,14 +790,12 @@ class TagsIndex: def __init__(self) -> None: self._tags: dict[str, dict[str, str]] = {} - return def __contains__(self, tagKey: str) -> bool: return tagKey.lower() in self._tags def __delitem__(self, tagKey: str) -> None: self._tags.pop(tagKey.lower(), None) - return def __getitem__(self, tagKey: str) -> dict | None: return self._tags.get(tagKey.lower(), None) @@ -823,7 +807,6 @@ class TagsIndex: def clear(self) -> None: """Clear the index.""" self._tags = {} - return def items(self) -> ItemsView: """Return a dictionary view of all tags.""" @@ -839,7 +822,6 @@ class TagsIndex: "heading": sTitle, "class": className, } - return def tagName(self, tagKey: str, default: str = "") -> str: """Get the name of a given tag.""" @@ -879,7 +861,6 @@ class TagsIndex: for entry in self._tags.values(): if entry.get("handle") == tHandle: entry["class"] = className - return ## # Pack/Unpack @@ -922,11 +903,9 @@ class TagsIndex: self.add(name, display, handle, heading, className) - return - class IndexCache: - """Core: Item Index Lookup Data Class + """Core: Item Index Lookup Data Class. A small data class passed between all objects of the Item Index which provides lookup capabilities and caching for shared data. @@ -938,14 +917,13 @@ class IndexCache: self.tags: TagsIndex = tagsIndex self.story: set[str] = set() self.note: set[str] = set() - return # The Item Index Objects # ====================== class ItemIndex: - """Core: Item Index Wrapper Class + """Core: Item Index Wrapper Class. A wrapper object holding the indexed items. This is a wrapper class around a single storage dictionary with a set of utility @@ -960,14 +938,12 @@ class ItemIndex: self._project = project self._cache = IndexCache(tagsIndex) self._items: dict[str, IndexNode] = {} - return def __contains__(self, tHandle: str) -> bool: return tHandle in self._items def __delitem__(self, tHandle: str) -> None: self._items.pop(tHandle, None) - return def __getitem__(self, tHandle: str) -> IndexNode | None: return self._items.get(tHandle, None) @@ -979,14 +955,12 @@ class ItemIndex: def clear(self) -> None: """Clear the index.""" self._items = {} - return def add(self, tHandle: str, nwItem: NWItem) -> None: """Add a new item to the index. This will overwrite the item if it already exists. """ self._items[tHandle] = IndexNode(self._cache, tHandle, nwItem) - return def allStoryKeys(self) -> set[str]: """Return all story structure keys.""" @@ -1061,7 +1035,6 @@ class ItemIndex: """ if tHandle in self._items: self._items[tHandle].setHeadingCounts(sTitle, cC, wC, pC) - return def setHeadingComment( self, tHandle: str, sTitle: str, @@ -1070,25 +1043,21 @@ class ItemIndex: """Set a story comment for a heading on a given item.""" if tHandle in self._items: self._items[tHandle].setHeadingComment(sTitle, comment, key, text) - return def setHeadingTag(self, tHandle: str, sTitle: str, tagKey: str) -> None: """Set the main tag for a heading on a given item.""" if tHandle in self._items: self._items[tHandle].setHeadingTag(sTitle, tagKey) - return def addHeadingRef(self, tHandle: str, sTitle: str, tagKeys: list[str], refType: str) -> None: """Set the reference tags for a heading on a given item.""" if tHandle in self._items: self._items[tHandle].addHeadingRef(sTitle, tagKeys, refType) - return def addNoteKey(self, tHandle: str, style: T_NoteTypes, key: str) -> None: """Set notes key for a given item.""" if tHandle in self._items: self._items[tHandle].addNoteKey(style, key) - return def genNewNoteKey(self, tHandle: str, style: T_NoteTypes) -> str: """Set notes key for a given item.""" @@ -1128,5 +1097,3 @@ class ItemIndex: tItem = IndexNode(self._cache, tHandle, nwItem) tItem.unpackData(tData) self._items[tHandle] = tItem - - return diff --git a/novelwriter/core/indexdata.py b/novelwriter/core/indexdata.py index b74ce4c4..b7867bd3 100644 --- a/novelwriter/core/indexdata.py +++ b/novelwriter/core/indexdata.py @@ -23,7 +23,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -50,7 +50,7 @@ NOTE_TYPES: list[T_NoteTypes] = ["footnotes", "comments"] class IndexNode: - """Core: Single Index Item Node Class + """Core: Single Index Item Node Class. This object represents the index data of a project item (NWItem). It holds a record of all the headings in the text, and the meta data @@ -68,7 +68,6 @@ class IndexNode: self._headings: dict[str, IndexHeading] = {TT_NONE: IndexHeading(self._cache, TT_NONE)} self._notes: dict[str, set[str]] = {} self._count = 0 - return def __repr__(self) -> str: return f"" @@ -107,39 +106,33 @@ class IndexNode: if TT_NONE in self._headings: self._headings.pop(TT_NONE) self._headings[tHeading.key] = tHeading - return def setHeadingCounts(self, sTitle: str, cCount: int, wCount: int, pCount: int) -> None: """Set the character, word and paragraph count of a heading.""" if sTitle in self._headings: self._headings[sTitle].setCounts([cCount, wCount, pCount]) - return def setHeadingComment(self, sTitle: str, comment: nwComment, key: str, text: str) -> None: """Set the comment text of a heading.""" if sTitle in self._headings: self._headings[sTitle].setComment(comment.name, key, text) - return def setHeadingTag(self, sTitle: str, tag: str) -> None: """Set the tag of a heading.""" if sTitle in self._headings: self._headings[sTitle].setTag(tag) - return def addHeadingRef(self, sTitle: str, tags: list[str], keyword: str) -> None: """Add a reference key and all its types to a heading.""" if sTitle in self._headings: for tag in tags: self._headings[sTitle].addReference(tag, keyword) - return def addNoteKey(self, style: T_NoteTypes, key: str) -> None: """Add a note key to the index.""" if style not in self._notes: self._notes[style] = set() self._notes[style].add(key) - return ## # Data Methods @@ -195,11 +188,10 @@ class IndexNode: self._notes[style] = set(keys) else: raise KeyError("Index node contains an invalid key") - return class IndexHeading: - """Core: Single Index Heading Class + """Core: Single Index Heading Class. This object represents a section of text in a project item associated with a single (valid) heading. It holds a separate record @@ -224,7 +216,6 @@ class IndexHeading: self._tag = "" self._refs: dict[str, set[str]] = {} self._comments: dict[str, str] = {} - return def __repr__(self) -> str: return f"" @@ -289,12 +280,10 @@ class IndexHeading: """Set the level of the heading if it's a valid value.""" if level in nwStyles.H_VALID: self._level = level - return def setLine(self, line: int) -> None: """Set the line number of a heading.""" self._line = max(0, checkInt(line, 0)) - return def setCounts(self, counts: Sequence[int]) -> None: """Set the character, word and paragraph count. Make sure the @@ -306,7 +295,6 @@ class IndexHeading: max(0, checkInt(counts[1], 0)), max(0, checkInt(counts[2], 0)), ) - return def setComment(self, comment: str, key: str, text: str) -> None: """Set the text for a comment and make sure it is a string.""" @@ -319,12 +307,10 @@ class IndexHeading: case "note" if key: self._cache.note.add(key) self._comments[f"note.{key}"] = str(text) - return def setTag(self, tag: str) -> None: """Set the tag for references, and make sure it is a string.""" self._tag = str(tag).lower() - return def addReference(self, tag: str, keyword: str) -> None: """Add a record of a reference tag, and what keyword types it is @@ -335,7 +321,6 @@ class IndexHeading: if tag not in self._refs: self._refs[tag] = set() self._refs[tag].add(keyword) - return ## # Getters @@ -409,4 +394,3 @@ class IndexHeading: self.setComment(comment, compact(kind), str(entry)) else: raise KeyError("Unknown key in heading entry") - return diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index a6b92734..77b8d6c2 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -44,7 +44,7 @@ logger = logging.getLogger(__name__) class NWItem: - """Core: Item Data Class + """Core: Item Data Class. This class holds all the project information about a project item. Each item must be associated with a project and have a valid handle. @@ -84,16 +84,14 @@ class NWItem: self._wordInit = 0 # Initial character count self._charInit = 0 # Initial word count - return - def __repr__(self) -> str: return f"" def __bool__(self) -> bool: - """The truthiness of the class. The handle used to be initiated - to None, but this is no longer the case. It should always - evaluate to True since 2.1-beta1, although unpack and the NWTree - class can leave it as an empty string. + """Check the truthiness of the class. The handle used to be + initiated to None, but this is no longer the case. It should + always evaluate to True since 2.1-beta1, although unpack and the + NWTree class can leave it as an empty string. """ return bool(self._handle) @@ -206,15 +204,13 @@ class NWItem: meta["cursorPos"] = str(self._cursorPos) name["active"] = yesNo(self._active) - data = { + return { "name": str(self._name), "itemAttr": item, "metaAttr": meta, "nameAttr": name, } - return data - def unpack(self, data: dict) -> bool: """Set the values from a data dictionary.""" item = data.get("itemAttr", {}) @@ -298,13 +294,11 @@ class NWItem: def notifyToRefresh(self) -> None: """Notify GUI that item info needs to be refreshed.""" self._project.tree.refreshItems([self._handle]) - return def notifyNovelStructureChange(self) -> None: """Notify that the structure of a novel has changed.""" if self._root and self._class == nwItemClass.NOVEL: self._project.tree.novelStructureChanged(self._root) - return ## # Lookup Methods @@ -363,13 +357,13 @@ class NWItem: """ if self.isFileType(): key = "checked" if self._active else "unchecked" - color = "green" if self._active else "red" + color = "active" if self._active else "inactive" text = trConst(nwLabels.ACTIVE_NAME[key]) - icon = SHARED.theme.getIcon(key, color) else: + key = "noncheckable" + color = "disabled" text = "" - icon = SHARED.theme.getIcon("noncheckable", "faded") - return text, icon + return text, SHARED.theme.getIcon(key, color) ## # Checker Methods @@ -457,8 +451,6 @@ class NWItem: if self._import is None: self.setImport("New") # This forces a default value lookup - return - ## # Set Item Values ## @@ -469,7 +461,6 @@ class NWItem: self._name = simplified(name) else: self._name = "" - return def setParent(self, handle: Any) -> None: """Set the parent handle, and ensure it is valid.""" @@ -479,7 +470,6 @@ class NWItem: self._parent = handle else: self._parent = None - return def setRoot(self, handle: Any) -> None: """Set the root handle, and ensure it is valid.""" @@ -489,7 +479,6 @@ class NWItem: self._root = handle else: self._root = None - return def setOrder(self, order: Any) -> None: """Set the item order, and ensure that it is valid. This value @@ -497,7 +486,6 @@ class NWItem: the moment. """ self._order = checkInt(order, 0) - return def setType(self, value: Any) -> None: """Set the item type from either a proper nwItemType, or set it @@ -510,7 +498,6 @@ class NWItem: else: logger.error("Unrecognised item type '%s'", value) self._type = nwItemType.NO_TYPE - return def setClass(self, value: Any) -> None: """Set the item class from either a proper nwItemClass, or set @@ -523,7 +510,6 @@ class NWItem: else: logger.error("Unrecognised item class '%s'", value) self._class = nwItemClass.NO_CLASS - return def setLayout(self, value: Any) -> None: """Set the item layout from either a proper nwItemLayout, or set @@ -536,21 +522,18 @@ class NWItem: else: logger.error("Unrecognised item layout '%s'", value) self._layout = nwItemLayout.NO_LAYOUT - return def setStatus(self, value: Any) -> None: """Set the item status by looking it up in the valid status items of the current project. """ self._status = self._project.data.itemStatus.check(value) - return def setImport(self, value: Any) -> None: """Set the item importance by looking it up in the valid import items of the current project. """ self._import = self._project.data.itemImport.check(value) - return def setActive(self, state: Any) -> None: """Set the active flag.""" @@ -558,7 +541,6 @@ class NWItem: self._active = state else: self._active = False - return def setExpanded(self, state: Any) -> None: """Set the expanded status of an item in the project tree.""" @@ -566,7 +548,6 @@ class NWItem: self._expanded = state else: self._expanded = False - return ## # Set Document Meta Data @@ -576,7 +557,6 @@ class NWItem: """Set the main heading level.""" if value in nwStyles.H_LEVEL: self._heading = value - return def setCharCount(self, count: Any) -> None: """Set the character count, and ensure that it is an integer.""" @@ -584,7 +564,6 @@ class NWItem: self._charCount = max(0, count) else: self._charCount = 0 - return def setWordCount(self, count: Any) -> None: """Set the word count, and ensure that it is an integer.""" @@ -592,7 +571,6 @@ class NWItem: self._wordCount = max(0, count) else: self._wordCount = 0 - return def setParaCount(self, count: Any) -> None: """Set the paragraph count, and ensure that it is an integer.""" @@ -600,7 +578,6 @@ class NWItem: self._paraCount = max(0, count) else: self._paraCount = 0 - return def setCursorPos(self, position: Any) -> None: """Set the cursor position, and ensure that it is an integer.""" @@ -608,4 +585,3 @@ class NWItem: self._cursorPos = max(0, position) else: self._cursorPos = 0 - return diff --git a/novelwriter/core/itemmodel.py b/novelwriter/core/itemmodel.py index 89696997..10ebaf6d 100644 --- a/novelwriter/core/itemmodel.py +++ b/novelwriter/core/itemmodel.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -66,7 +66,7 @@ T_NodeData = str | QIcon | QFont | Qt.AlignmentFlag | None class ProjectNode: - """Core: Project Model Node Class + """Core: Project Model Node Class. The project tree structure is saved as nodes in a tree, starting from a root node. This class makes up these nodes. @@ -103,7 +103,6 @@ class ProjectNode: self._count = 0 self.refresh() self.updateCount() - return def __repr__(self) -> str: return ( @@ -114,7 +113,7 @@ class ProjectNode: ) def __bool__(self) -> bool: - """A node should always evaluate to True.""" + # A node should always evaluate to True. return True ## @@ -162,15 +161,12 @@ class ProjectNode: self._cache[C_STATUS_TIP] = sText self._cache[C_STATUS_ACCESS] = sText - return - def updateCount(self, propagate: bool = True) -> None: """Update counts, and propagate upwards in the tree.""" self._count = self._item.mainCount + sum(c._count for c in self._children) # noqa: SLF001 self._cache[C_COUNT_TEXT] = f"{self._count:n}" if propagate and (parent := self._parent): parent.updateCount() - return ## # Data Access @@ -223,7 +219,6 @@ class ProjectNode: self._children.append(child) self._refreshChildrenPos() self._item.notifyNovelStructureChange() - return def takeChild(self, pos: int) -> ProjectNode | None: """Remove a child item and return it.""" @@ -243,7 +238,6 @@ class ProjectNode: self._children.insert(target, node) self._refreshChildrenPos() self._item.notifyNovelStructureChange() - return def setExpanded(self, state: bool) -> None: """Set the node's expanded state.""" @@ -251,7 +245,6 @@ class ProjectNode: self._item.setExpanded(True) else: self._item.setExpanded(False) - return ## # Internal Functions @@ -262,14 +255,12 @@ class ProjectNode: for node in self._children: children.append(node) node._recursiveAppendChildren(children) # noqa: SLF001 - return def _refreshChildrenPos(self) -> None: """Update the row value on all children.""" for n, child in enumerate(self._children): child._row = n # noqa: SLF001 child.item.setOrder(n) - return def _updateRelationships(self, child: ProjectNode) -> None: """Update a child item's relationships.""" @@ -282,11 +273,10 @@ class ProjectNode: child.item.setParent(None) child.item.setRoot(child.item.itemHandle) child.item.setClassDefaults(child.item.itemClass) - return class ProjectModel(QAbstractItemModel): - """Core: Project Model Class + """Core: Project Model Class. This class provides the interface for the tree widget used on the GUI. It implements the QModelIndex based interface required, adds @@ -302,11 +292,9 @@ class ProjectModel(QAbstractItemModel): self._root = ProjectNode(NWItem(tree.project, INV_ROOT)) self._root.item.setName("Invisible Root") logger.debug("Ready: ProjectModel") - return def __del__(self) -> None: # pragma: no cover logger.debug("Delete: ProjectModel") - return ## # Properties @@ -363,7 +351,7 @@ class ProjectModel(QAbstractItemModel): ## def supportedDropActions(self) -> Qt.DropAction: - """Return supported drop actions""" + """Return supported drop actions.""" return Qt.DropAction.MoveAction def mimeTypes(self) -> list[str]: @@ -445,7 +433,6 @@ class ProjectModel(QAbstractItemModel): self.beginInsertRows(parent, row, row) node.addChild(child, row) self.endInsertRows() - return def removeChild(self, parent: QModelIndex, pos: int) -> ProjectNode | None: """Remove a node from the model and return it.""" @@ -469,7 +456,6 @@ class ProjectModel(QAbstractItemModel): self.beginMoveRows(index.parent(), pos, pos, index.parent(), end) parent.moveChild(pos, new) self.endMoveRows() - return def multiMove(self, indices: list[QModelIndex], target: QModelIndex, pos: int = -1) -> None: """Move multiple items to a new location.""" @@ -497,7 +483,6 @@ class ProjectModel(QAbstractItemModel): node._updateRelationships(child) # noqa: SLF001 child.item.notifyToRefresh() node.item.notifyToRefresh() - return ## # Other Methods @@ -506,7 +491,6 @@ class ProjectModel(QAbstractItemModel): def clear(self) -> None: """Clear the project model.""" self._root.children.clear() - return def allExpanded(self) -> list[QModelIndex]: """Return a list of all expanded items.""" diff --git a/novelwriter/core/novelmodel.py b/novelwriter/core/novelmodel.py index 0447f526..7b1cd426 100644 --- a/novelwriter/core/novelmodel.py +++ b/novelwriter/core/novelmodel.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -54,6 +54,7 @@ T_NodeData = str | QIcon | QPixmap | Qt.AlignmentFlag | None class NovelModel(QAbstractTableModel): + """Core: Novel Model CLass.""" __slots__ = ("_columns", "_extraKey", "_extraLabel", "_more", "_rows") @@ -64,11 +65,9 @@ class NovelModel(QAbstractTableModel): self._columns = 3 self._extraKey = "" self._extraLabel = "" - return def __del__(self) -> None: # pragma: no cover logger.debug("Delete: NovelModel") - return ## # Properties @@ -102,7 +101,6 @@ class NovelModel(QAbstractTableModel): self._columns = 4 self._extraKey = nwKeyWords.PLOT_KEY self._extraLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]) - return ## # Model Interface @@ -147,7 +145,6 @@ class NovelModel(QAbstractTableModel): def clear(self) -> None: """Clear the model.""" self._rows.clear() - return def append(self, node: IndexNode) -> None: """Append a node to the model.""" @@ -155,7 +152,6 @@ class NovelModel(QAbstractTableModel): for key, head in node.items(): if key != "T0000": self._rows.append(self._generateEntry(handle, key, head)) - return def refresh(self, node: IndexNode) -> bool: """Refresh an index node.""" diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py index 2753c086..de8c5759 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import json @@ -80,7 +80,7 @@ VALID_MAP: dict[str, set[str]] = { class OptionState: - """Core: GUI Options Storage + """Core: GUI Options Storage. A class for storing the state of the GUI. The data is stored per project. Settings that should be project-independent are stored in @@ -90,7 +90,6 @@ class OptionState: def __init__(self, project: NWProject) -> None: self._project = project self._state = {} - return ## # Load and Save Cache diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index ed5a8284..2a10fd92 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import json @@ -57,6 +57,7 @@ logger = logging.getLogger(__name__) class NWProjectState(Enum): + """The state of the loaded project.""" UNKNOWN = 0 LOCKED = 1 @@ -65,6 +66,11 @@ class NWProjectState(Enum): class NWProject: + """Core: novelWriter Project Class. + + This class is the parent class of the project, and holds instances + of project data, the project tree, and the project index. + """ __slots__ = ( "_changed", "_data", "_index", "_langData", "_options", "_session", @@ -92,17 +98,13 @@ class NWProject: logger.debug("Ready: NWProject") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: NWProject") - return def clear(self) -> None: """Clear the project.""" self._tree.clear() self._index.clear() - return ## # Properties @@ -224,7 +226,7 @@ class NWProject: return True - def copyFileContent(self, tHandle: str, sHandle: str) -> bool: + def copyFileContent(self, tHandle: str, sHandle: str, newTitle: str | None = None) -> bool: """Copy content to a new document after it is created. This will not run if the file exists and is not empty. """ @@ -239,6 +241,14 @@ class NWProject: logger.debug("Populating '%s' with text from '%s'", tHandle, sHandle) text = self._storage.getDocumentText(sHandle) + if ( + newTitle and (lines := text.split("\n")) and lines + and lines[0].startswith(("# ", "## ", "### ", "#### ", "#! ", "##! ", "###! ")) + ): + prefix, _, _ = lines[0].partition(" ") + lines[0] = f"{prefix} {newTitle}" + text = "\n".join(lines) + self._storage.getDocument(tHandle).writeDocument(text) sItem.setLayout(tItem.itemLayout) self._index.reIndexHandle(tHandle) @@ -255,7 +265,6 @@ class NWProject: if rHandle and (tHandle := SHARED.project.newFile(tag.title(), rHandle)): self.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n") self._tree.refreshItems([tHandle]) - return ## # Project Methods @@ -433,7 +442,6 @@ class NWProject: self._tree.writeToCFile() self._session.appendSession(idleTime) self._storage.closeSession() - return def backupProject(self, doNotify: bool) -> bool: """Create a zip file of the entire project.""" @@ -483,15 +491,14 @@ class NWProject: def setDefaultStatusImport(self) -> None: """Set the default status and importance values.""" - self._data.itemStatus.add(None, self.tr("New"), (120, 120, 120), "STAR", 0) - self._data.itemStatus.add(None, self.tr("Note"), (205, 171, 143), "TRIANGLE", 0) - self._data.itemStatus.add(None, self.tr("Draft"), (143, 240, 164), "CIRCLE_T", 0) - self._data.itemStatus.add(None, self.tr("Finished"), (249, 240, 107), "STAR", 0) - self._data.itemImport.add(None, self.tr("New"), (120, 120, 120), "SQUARE", 0) - self._data.itemImport.add(None, self.tr("Minor"), (220, 138, 221), "BLOCK_2", 0) - self._data.itemImport.add(None, self.tr("Major"), (220, 138, 221), "BLOCK_3", 0) - self._data.itemImport.add(None, self.tr("Main"), (220, 138, 221), "BLOCK_4", 0) - return + self._data.itemStatus.add(None, self.tr("New"), "faded", "STAR", 0) + self._data.itemStatus.add(None, self.tr("Note"), "red", "TRIANGLE", 0) + self._data.itemStatus.add(None, self.tr("Draft"), "yellow", "CIRCLE_T", 0) + self._data.itemStatus.add(None, self.tr("Finished"), "green", "STAR", 0) + self._data.itemImport.add(None, self.tr("New"), "purple", "SQUARE", 0) + self._data.itemImport.add(None, self.tr("Minor"), "purple", "BLOCK_2", 0) + self._data.itemImport.add(None, self.tr("Major"), "purple", "BLOCK_3", 0) + self._data.itemImport.add(None, self.tr("Main"), "purple", "BLOCK_4", 0) def setProjectLang(self, language: str | None) -> None: """Set the project-specific language.""" @@ -500,7 +507,6 @@ class NWProject: self._data.setLanguage(language) self._loadProjectLocalisation() self.setProjectChanged(True) - return def setProjectChanged(self, status: bool) -> bool: """Toggle the project changed flag, and propagate the @@ -519,7 +525,6 @@ class NWProject: """Update the total word and character count values.""" wNovel, wNotes, cNovel, cNotes = self._tree.sumCounts() self._data.setCurrCounts(wNovel=wNovel, wNotes=wNotes, cNovel=cNovel, cNotes=cNotes) - return def countStatus(self) -> None: """Count how many times the various status flags are used in the @@ -533,7 +538,6 @@ class NWProject: self._data.itemStatus.increment(nwItem.itemStatus) else: self._data.itemImport.increment(nwItem.itemImport) - return def updateStatus(self, kind: T_StatusKind, update: T_UpdateEntry) -> None: """Update status or import entries.""" @@ -545,7 +549,11 @@ class NWProject: self._data.itemImport.update(update) SHARED.emitStatusLabelsChanged(self, kind) self._tree.refreshAllItems() - return + + def updateTheme(self) -> None: + """Update theme elements.""" + self._data.itemStatus.refreshIcons() + self._data.itemImport.refreshIcons() def localLookup(self, word: str | int) -> str: """Look up a word or number in the translation map for the diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index 15201970..12471e11 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -41,7 +41,7 @@ logger = logging.getLogger(__name__) class NWProjectData: - """Core: Project Data Class + """Core: Project Data Class. The class holds all project data from the main XML file, aside from the list of project items. @@ -86,8 +86,6 @@ class NWProjectData: self._status = NWStatus(NWStatus.STATUS) self._import = NWStatus(NWStatus.IMPORT) - return - ## # Properties ## @@ -191,13 +189,11 @@ class NWProjectData: """Increment the save count by one.""" self._saveCount += 1 self._project.setProjectChanged(True) - return def incAutoCount(self) -> None: """Increment the auto save count by one.""" self._autoCount += 1 self._project.setProjectChanged(True) - return ## # Getters @@ -219,67 +215,57 @@ class NWProjectData: elif value != self._uuid: self._uuid = value self._project.setProjectChanged(True) - return def setName(self, value: str | None) -> None: """Set a new project name.""" if value != self._name: self._name = simplified(str(value or "")) self._project.setProjectChanged(True) - return def setAuthor(self, value: str | None) -> None: """Set the author value.""" if value != self._author: self._author = simplified(str(value or "")) self._project.setProjectChanged(True) - return def setSaveCount(self, value: Any) -> None: """Set the save count from last session.""" self._saveCount = checkInt(value, 0) self._project.setProjectChanged(True) - return def setAutoCount(self, value: Any) -> None: """Set the auto save count from last session.""" self._autoCount = checkInt(value, 0) self._project.setProjectChanged(True) - return def setEditTime(self, value: Any) -> None: """Set the edit time from last session.""" self._editTime = checkInt(value, 0) self._project.setProjectChanged(True) - return def setDoBackup(self, value: Any) -> None: """Set the do write backup flag.""" if value != self._doBackup: self._doBackup = checkBool(value, False) self._project.setProjectChanged(True) - return def setLanguage(self, value: str | None) -> None: """Set the project language.""" if value != self._language: self._language = checkStringNone(value, None) self._project.setProjectChanged(True) - return def setSpellCheck(self, value: Any) -> None: """Set the spell check flag.""" if value != self._spellCheck: self._spellCheck = checkBool(value, False) self._project.setProjectChanged(True) - return def setSpellLang(self, value: str | None) -> None: """Set the spell check language.""" if value != self._spellLang: self._spellLang = checkStringNone(value, None) self._project.setProjectChanged(True) - return def setLastHandle(self, value: str | None, component: str) -> None: """Set a last used handle into the handle registry for a given @@ -288,7 +274,6 @@ class NWProjectData: if isinstance(component, str): self._lastHandle[component] = checkStringNone(value, None) self._project.setProjectChanged(True) - return def setLastHandles(self, value: dict) -> None: """Set the full last handles dictionary to a new set of values. @@ -299,7 +284,6 @@ class NWProjectData: if key in self._lastHandle: self._lastHandle[key] = str(entry) if isHandle(entry) else None self._project.setProjectChanged(True) - return def setInitCounts( self, wNovel: Any = None, wNotes: Any = None, cNovel: Any = None, cNotes: Any = None @@ -321,7 +305,6 @@ class NWProjectData: count = checkInt(cNotes, 0) self._initCounts[3] = count self._currCounts[3] = count - return def setCurrCounts( self, wNovel: Any = None, wNotes: Any = None, cNovel: Any = None, cNotes: Any = None @@ -335,7 +318,6 @@ class NWProjectData: self._currCounts[2] = checkInt(cNovel, 0) if cNotes is not None: self._currCounts[3] = checkInt(cNotes, 0) - return def setAutoReplace(self, value: dict) -> None: """Set the auto-replace dictionary.""" @@ -345,4 +327,3 @@ class NWProjectData: if isinstance(entry, str): self._autoReplace[key] = simplified(entry) self._project.setProjectChanged(True) - return diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 1e0e146b..8434b0fa 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -22,7 +22,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -46,7 +46,7 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) FILE_VERSION = "1.5" # The current project file format version -FILE_REVISION = "5" # The current project file format revision +FILE_REVISION = "6" # The current project file format revision HEX_VERSION = 0x0105 NUM_VERSION = { @@ -72,7 +72,7 @@ class XMLReadState(Enum): class ProjectXMLReader: - """Core: Project XML Reader + """Core: Project XML Reader. All data is read into a NWProjectData instance, which must be provided. @@ -111,6 +111,8 @@ class ProjectXMLReader: nodes. 2.5. Rev 5: Added novelChars and notesChars attributes to content node. 2.7 RC 1. + Rev 6: Replaced red, green and blue attributes with a single + color attribute. 2.8 Beta 1. """ def __init__(self, path: str | Path) -> None: @@ -122,7 +124,6 @@ class ProjectXMLReader: self._appVersion = "" self._hexVersion = 0x0 self._timeStamp = "" - return ## # Properties @@ -252,8 +253,6 @@ class ProjectXMLReader: elif xItem.tag == "editTime": # Moved to attribute in 1.5 data.setEditTime(xItem.text) - return - def _parseProjectSettings(self, xSection: ET.Element, data: NWProjectData) -> None: """Parse the settings section of the XML file.""" logger.debug("Parsing section") @@ -292,8 +291,6 @@ class ProjectXMLReader: elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5 data.setInitCounts(wNotes=xItem.text) - return - def _parseProjectContent( self, xSection: ET.Element, data: NWProjectData, content: list ) -> None: @@ -354,8 +351,6 @@ class ProjectXMLReader: "nameAttr": name, }) - return - def _parseProjectContentLegacy( self, xSection: ET.Element, data: NWProjectData, content: list ) -> None: @@ -432,20 +427,20 @@ class ProjectXMLReader: "nameAttr": name, }) - return - def _parseStatusImport(self, xItem: ET.Element, sObject: NWStatus) -> None: """Parse a status or importance entry.""" for xEntry in xItem: if xEntry.tag == "entry": key = xEntry.attrib.get("key", None) - red = checkInt(xEntry.attrib.get("red", 0), 0) - green = checkInt(xEntry.attrib.get("green", 0), 0) - blue = checkInt(xEntry.attrib.get("blue", 0), 0) + red = checkInt(xEntry.attrib.get("red", 0), 0) # Removed in 1.5 R6 + green = checkInt(xEntry.attrib.get("green", 0), 0) # Removed in 1.5 R6 + blue = checkInt(xEntry.attrib.get("blue", 0), 0) # Removed in 1.5 R6 + color = xEntry.attrib.get("color") # Added in 1.5 R6 count = checkInt(xEntry.attrib.get("count", 0), 0) shape = xEntry.attrib.get("shape", "") - sObject.add(key, xEntry.text or "", (red, green, blue), shape, count) - return + if color is None: + color = f"{red}, {green}, {blue}" + sObject.add(key, xEntry.text or "", color, shape, count) def _parseDictKeyText(self, xItem: ET.Element) -> dict: """Parse a dictionary stored with key as an attribute and the @@ -465,7 +460,7 @@ class ProjectXMLReader: class ProjectXMLWriter: - """Core: Project XML Writer + """Core: Project XML Writer. The project writer class will only write a file according to the very latest spec. @@ -474,7 +469,6 @@ class ProjectXMLWriter: def __init__(self, path: str | Path) -> None: self._path = Path(path) self._error = None - return ## # Properties @@ -575,7 +569,6 @@ class ProjectXMLWriter: """Pack a single value into an XML element.""" xItem = ET.SubElement(xParent, name, attrib=attrib or {}) xItem.text = str(value) or "" - return def _packDictKeyValue(self, xParent: ET.Element, name: str, data: dict) -> None: """Pack the entries of a dictionary into an XML element.""" @@ -584,4 +577,3 @@ class ProjectXMLWriter: if len(key) > 0: xEntry = ET.SubElement(xItem, "entry", attrib={"key": key}) xEntry.text = str(value) or "" - return diff --git a/novelwriter/core/sessions.py b/novelwriter/core/sessions.py index 6936ed99..58625c81 100644 --- a/novelwriter/core/sessions.py +++ b/novelwriter/core/sessions.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import json @@ -43,7 +43,7 @@ logger = logging.getLogger(__name__) class NWSessionLog: - """Core: Session JSON Lines Log File + """Core: Session JSON Lines Log File. The class that wraps the session log file, which is in JSON Lines format. That is, one JSON object per line. @@ -52,7 +52,6 @@ class NWSessionLog: def __init__(self, project: NWProject) -> None: self._project = project self._start = 0.0 - return ## # Properties @@ -70,7 +69,6 @@ class NWSessionLog: def startSession(self) -> None: """Start the writing session.""" self._start = time() - return def appendSession(self, idleTime: float) -> bool: """Append session statistics to the sessions log file.""" diff --git a/novelwriter/core/spellcheck.py b/novelwriter/core/spellcheck.py index a9e8b443..e2b0dd56 100644 --- a/novelwriter/core/spellcheck.py +++ b/novelwriter/core/spellcheck.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import json @@ -44,7 +44,7 @@ logger = logging.getLogger(__name__) class NWSpellEnchant: - """Core: Enchant Spell Checking Wrapper + """Core: Enchant Spell Checking Wrapper. This is a rapper class for Enchant to keep the API consistent between spell check tools. @@ -57,11 +57,9 @@ class NWSpellEnchant: self._language = None self._broker = None logger.debug("Ready: NWSpellEnchant") - return def __del__(self) -> None: # pragma: no cover logger.debug("Delete: NWSpellEnchant") - return ## # Properties @@ -106,21 +104,19 @@ class NWSpellEnchant: for word in self._userDict: self._enchant.add_to_session(word) - return - ## # Methods ## def checkWord(self, word: str) -> bool: - """Wrapper function for pyenchant.""" + """Forward check to pyenchant.""" try: return bool(self._enchant.check(word)) except Exception: return True def suggestWords(self, word: str) -> list[str]: - """Wrapper function for pyenchant.""" + """Ask pyenchant for suggestions.""" try: return self._enchant.suggest(word) except Exception: @@ -172,24 +168,29 @@ class FakeEnchant: self.tag = "" self.provider = FakeProvider() - return - def check(self, word: str) -> bool: + """Return True for all words.""" return True def suggest(self, word: str) -> list[str]: + """Return an empty suggestion list.""" return [] def add_to_session(self, word: str) -> None: + """Do nothing.""" return class UserDictionary: + """Core: User Word Dictionary. + + This class holds all the user's own words for spell checking + purposes. The dictionary is per-project. + """ def __init__(self, project: NWProject) -> None: self._project = project self._words = set() - return def __contains__(self, word: str) -> bool: return word in self._words @@ -219,7 +220,6 @@ class UserDictionary: except Exception: logger.error("Failed to load user dictionary") logException() - return def save(self) -> None: """Save the user's dictionary.""" @@ -232,4 +232,3 @@ class UserDictionary: except Exception: logger.error("Failed to save user dictionary") logException() - return diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index f7d9b584..fc8bf85c 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import dataclasses @@ -35,6 +35,7 @@ from PyQt6.QtGui import QColor, QIcon, QPainter, QPainterPath, QPixmap, QPolygon from novelwriter import SHARED from novelwriter.common import simplified +from novelwriter.constants import nwLabels from novelwriter.enum import nwStatusShape from novelwriter.types import QtPaintAntiAlias, QtTransparent @@ -43,12 +44,16 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) +CUSTOM_COL = "custom" + @dataclasses.dataclass class StatusEntry: + """DataClass: Status Label Values.""" name: str color: QColor + theme: str shape: nwStatusShape icon: QIcon count: int = 0 @@ -62,13 +67,14 @@ class StatusEntry: return status -NO_ENTRY = StatusEntry("", QColor(0, 0, 0), nwStatusShape.SQUARE, QIcon(), 0) +NO_ENTRY = StatusEntry("", QColor(0, 0, 0), CUSTOM_COL, nwStatusShape.SQUARE, QIcon(), 0) T_UpdateEntry = list[tuple[str | None, StatusEntry]] T_StatusKind = Literal["s", "i"] class NWStatus: + """Core: Status/Importance Label Class.""" STATUS = "s" IMPORT = "i" @@ -80,7 +86,6 @@ class NWStatus: self._default = None self._prefix = prefix[:1] self._height = SHARED.theme.baseIconHeight - return def __len__(self) -> int: return len(self._store) @@ -97,15 +102,12 @@ class NWStatus: # Methods ## - def add(self, key: str | None, name: str, color: tuple[int, int, int], - shape: str, count: int) -> str: + def add(self, key: str | None, name: str, color: str, shape: str, count: int) -> str: """Add or update a status entry. If the key is invalid, a new key is generated. """ - if isinstance(color, tuple) and len(color) == 3: - qColor = QColor(*color) - else: - qColor = QColor(100, 100, 100) + qColor = SHARED.theme.parseColor(color) + theme = color if color in nwLabels.THEME_COLORS else CUSTOM_COL try: iShape = nwStatusShape[shape] @@ -115,7 +117,7 @@ class NWStatus: key = self._checkKey(key) name = simplified(name) icon = self.createIcon(self._height, qColor, iShape) - self._store[key] = StatusEntry(name, qColor, iShape, icon, count) + self._store[key] = StatusEntry(name, qColor, theme, iShape, icon, count) if self._default is None: self._default = key @@ -132,8 +134,6 @@ class NWStatus: if self._default not in self._store: self._default = next(iter(self._store)) if self._store else None - return - def check(self, value: str) -> str: """Check the key against the stored status names.""" if self._isKey(value) and value in self._store: @@ -146,23 +146,23 @@ class NWStatus: """Clear the counts of references to the status entries.""" for entry in self._store.values(): entry.count = 0 - return def increment(self, key: str | None) -> None: """Increment the counter for a given entry.""" if key and key in self._store: self._store[key].count += 1 - return def pack(self) -> Iterable[tuple[str, dict]]: """Pack the status entries into a dictionary.""" for key, entry in self._store.items(): + if entry.theme == CUSTOM_COL: + color = entry.color.name(QColor.NameFormat.HexRgb) + else: + color = entry.theme yield (entry.name, { "key": key, "count": str(entry.count), - "red": str(entry.color.red()), - "green": str(entry.color.green()), - "blue": str(entry.color.blue()), + "color": color, "shape": entry.shape.name, }) return @@ -179,12 +179,20 @@ class NWStatus: try: shape = nwStatusShape[str(data[0])] color = QColor(str(data[1])) + theme = CUSTOM_COL if data[1].startswith("#") else data[1] icon = NWStatus.createIcon(self._height, color, shape) - return StatusEntry(simplified(data[2]), color, shape, icon) + return StatusEntry(simplified(data[2]), color, theme, shape, icon) except Exception: logger.error("Could not parse entry %s", str(data)) return None + def refreshIcons(self) -> None: + """Refresh all icons.""" + for entry in self._store.values(): + if entry.theme != CUSTOM_COL: + entry.color = SHARED.theme.parseColor(entry.theme) + entry.icon = NWStatus.createIcon(self._height, entry.color, entry.shape) + @staticmethod def createIcon(height: int, color: QColor, shape: nwStatusShape) -> QIcon: """Generate an icon for a status label.""" @@ -240,7 +248,6 @@ class _ShapeCache: def __init__(self) -> None: self._cache: dict[nwStatusShape, QPainterPath] = {} - return def getShape(self, shape: nwStatusShape) -> QPainterPath: """Return a painter shape for an icon.""" diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py index b8c21948..dfa0269b 100644 --- a/novelwriter/core/storage.py +++ b/novelwriter/core/storage.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import json @@ -47,6 +47,7 @@ logger = logging.getLogger(__name__) class NWStorageOpen(Enum): + """The status of a storage location.""" UNKOWN = 0 NOT_FOUND = 1 @@ -56,6 +57,7 @@ class NWStorageOpen(Enum): class NWStorageCreate(Enum): + """The status of a new storage location.""" NOT_EMPTY = 0 OS_ERROR = 1 @@ -63,7 +65,7 @@ class NWStorageCreate(Enum): class NWStorage: - """Core: Project Storage Class + """Core: Project Storage Class. The class that handles all paths related to the project storage. """ @@ -81,7 +83,6 @@ class NWStorage: self._openMode = self.MODE_INACTIVE self._ready = False self._exception = None - return def clear(self) -> None: """Reset internal variables.""" @@ -90,7 +91,6 @@ class NWStorage: self._lockFilePath = None self._openMode = self.MODE_INACTIVE self._ready = False - return ## # Properties @@ -252,13 +252,11 @@ class NWStorage: """Lock the session when the project is successfully opened.""" if self._ready: self._writeLockFile() - return def closeSession(self) -> None: """Run tasks related to closing the session.""" self._clearLockFile() self.clear() - return ## # Content Access Methods @@ -394,7 +392,7 @@ class NWStorage: class _LegacyStorage: - """Core: Legacy Storage Converter Utils + """Core: Legacy Storage Converter Utils. A class with various functions to convert old file formats and file/folder layouts to the current project format. @@ -402,7 +400,6 @@ class _LegacyStorage: def __init__(self, project: NWProject) -> None: self._project = project - return def legacyDataFolder(self, path: Path, child: Path) -> None: """Handle the content of a legacy data folder from a version 1.0 @@ -484,8 +481,6 @@ class _LegacyStorage: except Exception as exc: logger.warning("Failed to delete: %s", item, exc_info=exc) - return - ## # Internal Functions ## diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 14158041..4ee4b626 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -50,7 +50,7 @@ MAX_DEPTH = 999 # Cap of tree traversing for loops (recursion limit) class NWTree: - """Core: Project Tree Data Class + """Core: Project Tree Data Class. Only one instance of this class should exist in the project class. This class holds all the project items of the project as instances @@ -71,18 +71,16 @@ class NWTree: self._trash = None self._ready = False logger.debug("Ready: NWTree") - return def __del__(self) -> None: # pragma: no cover logger.debug("Delete: NWTree") - return def __len__(self) -> int: - """The number of items in the project.""" + """Return the number of items in the project.""" return len(self._items) def __bool__(self) -> bool: - """True if there are any items in the project.""" + """Return True if there are any items in the project.""" return bool(self._items) def __getitem__(self, tHandle: str | None) -> NWItem | None: @@ -95,7 +93,7 @@ class NWTree: return None def __contains__(self, tHandle: str) -> bool: - """Checks if a handle exists in the tree.""" + """Check if a handle exists in the tree.""" return tHandle in self._items def __iter__(self) -> Iterator[NWItem]: @@ -142,7 +140,6 @@ class NWTree: self._trash = None oldModel.deleteLater() del oldModel - return def add(self, item: NWItem, pos: int = -1) -> bool: """Add a project item into the project tree.""" @@ -260,8 +257,6 @@ class NWTree: self._model.endInsertRows() self._model.layoutChanged.emit() - return - def pickParent(self, sNode: ProjectNode, hLevel: int, isNote: bool) -> tuple[str | None, int]: """Pick an appropriate parent handle for adding a new item.""" if sNode.item.isFolderType() or sNode.item.isRootType(): @@ -299,7 +294,6 @@ class NWTree: indexE = self._model.indexFromNode(node, 3) self._model.dataChanged.emit(indexS, indexE) self._itemChange(node.item, nwChange.UPDATE) - return def refreshAllItems(self) -> None: """Refresh all items in the tree.""" @@ -309,13 +303,11 @@ class NWTree: self._model.root.refresh() self._model.root.updateCount(propagate=False) self._model.layoutChanged.emit() - return def novelStructureChanged(self, tHandle: str) -> None: """Emit a novel structure change signal.""" if self._ready: SHARED.novelStructureChanged.emit(tHandle) - return def checkConsistency(self, prefix: str) -> tuple[int, int]: """Check the project tree consistency. Also check the content @@ -496,7 +488,6 @@ class NWTree: SHARED.emitProjectItemChanged(self._project, tHandle, change) if item.isRootType(): SHARED.emitRootFolderChanged(self._project, tHandle, change) - return def _getTrashNode(self) -> ProjectNode | None: """Get the trash node. If it doesn't exist, create it.""" diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index 3c7070df..59d4251f 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -45,6 +45,7 @@ logger = logging.getLogger(__name__) class GuiAbout(NDialog): + """GUI: About novelWriter Dialog.""" def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -109,11 +110,8 @@ class GuiAbout(NDialog): logger.debug("Ready: GuiAbout") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiAbout") - return ## # Events @@ -123,7 +121,6 @@ class GuiAbout(NDialog): """Capture the close event and perform cleanup.""" event.accept() self.softDelete() - return ## # Internal Functions @@ -135,7 +132,6 @@ class GuiAbout(NDialog): self.txtCredits.setHtml(html) else: self.txtCredits.setHtml("Error loading credits text ...") - return def _setStyleSheet(self) -> None: """Set stylesheet text document.""" @@ -143,4 +139,3 @@ class GuiAbout(NDialog): self.txtCredits.setStyleSheet( f"QTextBrowser {{border: none; background: {baseCol};}} " ) - return diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 9345efd6..2c392374 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -42,6 +42,7 @@ logger = logging.getLogger(__name__) class GuiDocMerge(NDialog): + """GUI: Document Merge Tool.""" D_HANDLE = QtUserRole @@ -110,11 +111,8 @@ class GuiDocMerge(NDialog): logger.debug("Ready: GuiDocMerge") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiDocMerge") - return def data(self) -> dict: """Return the user's choices.""" @@ -150,7 +148,6 @@ class GuiDocMerge(NDialog): if sHandle := self._data.get("sHandle"): itemList = self._data.get("origItems", []) self._loadContent(sHandle, itemList) - return ## # Internal Functions @@ -170,4 +167,3 @@ class GuiDocMerge(NDialog): item.setData(self.D_HANDLE, tHandle) item.setCheckState(Qt.CheckState.Checked) self.listBox.addItem(item) - return diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index a67aafa7..5850aff3 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -21,20 +21,20 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging from PyQt6.QtCore import pyqtSlot from PyQt6.QtWidgets import ( - QAbstractItemView, QComboBox, QDialogButtonBox, QGridLayout, QLabel, - QListWidget, QListWidgetItem, QVBoxLayout, QWidget + QAbstractItemView, QDialogButtonBox, QGridLayout, QLabel, QListWidget, + QListWidgetItem, QVBoxLayout, QWidget ) from novelwriter import SHARED from novelwriter.extensions.configlayout import NColorLabel -from novelwriter.extensions.modified import NDialog +from novelwriter.extensions.modified import NComboBox, NDialog from novelwriter.extensions.switch import NSwitch from novelwriter.types import QtAccepted, QtDialogCancel, QtDialogOk, QtUserRole @@ -42,6 +42,7 @@ logger = logging.getLogger(__name__) class GuiDocSplit(NDialog): + """GUI: Document Split Tool.""" LINE_ROLE = QtUserRole LEVEL_ROLE = QtUserRole + 1 @@ -79,7 +80,7 @@ class GuiDocSplit(NDialog): self.listBox.setMinimumWidth(400) self.listBox.setMinimumHeight(180) - self.splitLevel = QComboBox(self) + self.splitLevel = NComboBox(self) self.splitLevel.addItem(self.tr("Split on Heading Level 1 (Partition)"), 1) self.splitLevel.addItem(self.tr("Split up to Heading Level 2 (Chapter)"), 2) self.splitLevel.addItem(self.tr("Split up to Heading Level 3 (Scene)"), 3) @@ -139,11 +140,8 @@ class GuiDocSplit(NDialog): logger.debug("Ready: GuiDocSplit") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiDocSplit") - return def data(self) -> tuple[dict, list[str]]: """Return the user's choices. Also save the users options for @@ -197,7 +195,6 @@ class GuiDocSplit(NDialog): """Reload the content of the list box.""" if sHandle := self._data.get("sHandle"): self._loadContent(sHandle) - return ## # Internal Functions diff --git a/novelwriter/dialogs/editlabel.py b/novelwriter/dialogs/editlabel.py index 97bb269c..b8b4bae4 100644 --- a/novelwriter/dialogs/editlabel.py +++ b/novelwriter/dialogs/editlabel.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -34,6 +34,7 @@ logger = logging.getLogger(__name__) class GuiEditLabel(NDialog): + """GUI: Edit Item Label Dialog.""" def __init__(self, parent: QWidget, text: str = "") -> None: super().__init__(parent=parent) @@ -72,11 +73,8 @@ class GuiEditLabel(NDialog): logger.debug("Ready: GuiEditLabel") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiEditLabel") - return @property def itemLabel(self) -> str: diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index fbeb0523..758f7908 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -35,7 +35,7 @@ from PyQt6.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.common import compact, describeFont, processDialogSymbols, uniqueCompact -from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX, DEF_TREECOL +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.extensions.configlayout import NColorLabel, NScrollableForm @@ -50,6 +50,7 @@ logger = logging.getLogger(__name__) class GuiPreferences(NDialog): + """GUI: Preferences Dialog.""" newPreferencesReady = pyqtSignal(bool, bool, bool, bool) @@ -125,11 +126,8 @@ class GuiPreferences(NDialog): logger.debug("Ready: GuiPreferences") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiPreferences") - return def buildForm(self) -> None: """Build the settings form.""" @@ -165,22 +163,34 @@ class GuiPreferences(NDialog): ) # Colour Theme - self.guiTheme = NComboBox(self) - self.guiTheme.setMinimumWidth(200) - for theme, name in SHARED.theme.listThemes(): - self.guiTheme.addItem(name, theme) - self.guiTheme.setCurrentData(CONFIG.guiTheme, DEF_GUI) + self.lightTheme = NComboBox(self) + self.lightTheme.setMinimumWidth(200) + self.darkTheme = NComboBox(self) + self.darkTheme.setMinimumWidth(200) + for key, theme in SHARED.theme.colourThemes.items(): + if theme.dark: + self.darkTheme.addItem(theme.name, key) + else: + self.lightTheme.addItem(theme.name, key) + + self.lightTheme.setCurrentData(CONFIG.lightTheme, DEF_GUI_LIGHT) + self.darkTheme.setCurrentData(CONFIG.darkTheme, DEF_GUI_DARK) self.mainForm.addRow( - self.tr("Colour theme"), self.guiTheme, - self.tr("User interface colour theme."), stretch=(3, 2) + self.tr("Light colour theme"), self.lightTheme, + self.tr("You can change theme mode from the sidebar."), stretch=(3, 2) + ) + self.mainForm.addRow( + self.tr("Dark colour theme"), self.darkTheme, + self.tr("You can change theme mode from the sidebar."), stretch=(3, 2) ) # Icon Theme self.iconTheme = NComboBox(self) self.iconTheme.setMinimumWidth(200) - for theme, name in SHARED.theme.iconCache.listThemes(): - self.iconTheme.addItem(name, theme) + for key, theme in SHARED.theme.iconCache.iconThemes.items(): + self.iconTheme.addItem(theme.name, key) + self.iconTheme.setCurrentData(CONFIG.iconTheme, DEF_ICONS) self.mainForm.addRow( @@ -242,18 +252,6 @@ class GuiPreferences(NDialog): self.sidebar.addButton(title, section) self.mainForm.addGroupLabel(title, section) - # Document Colour Theme - self.guiSyntax = NComboBox(self) - self.guiSyntax.setMinimumWidth(200) - for syntax, name in SHARED.theme.listSyntax(): - self.guiSyntax.addItem(name, syntax) - self.guiSyntax.setCurrentData(CONFIG.guiSyntax, DEF_SYNTAX) - - self.mainForm.addRow( - self.tr("Document colour theme"), self.guiSyntax, - self.tr("Colour theme for the editor and viewer."), stretch=(3, 2) - ) - # Document Font Family self.textFont = QLineEdit(self) self.textFont.setReadOnly(True) @@ -294,6 +292,7 @@ class GuiPreferences(NDialog): # Tree Icon Colours self.iconColTree = NComboBox(self) self.iconColTree.setMinimumWidth(200) + self.iconColTree.addItem(self.tr("Theme Colours"), DEF_TREECOL) for key, label in nwLabels.THEME_COLORS.items(): self.iconColTree.addItem(trConst(label), key) self.iconColTree.setCurrentData(CONFIG.iconColTree, DEF_TREECOL) @@ -543,6 +542,13 @@ class GuiPreferences(NDialog): unit=self.tr("px") ) + # Highlight Current Line + self.lineHighlight = NSwitch(self) + self.lineHighlight.setChecked(CONFIG.lineHighlight) + self.mainForm.addRow( + self.tr("Highlight current line"), self.lineHighlight + ) + # Show Tabs and Spaces self.showTabsNSpaces = NSwitch(self) self.showTabsNSpaces.setChecked(CONFIG.showTabsNSpaces) @@ -693,7 +699,7 @@ class GuiPreferences(NDialog): self.showMultiSpaces = NSwitch(self) self.showMultiSpaces.setChecked(CONFIG.showMultiSpaces) self.mainForm.addRow( - self.tr("Highlight multiple or trailing spaces"), self.showMultiSpaces, + self.tr("Highlight multiple spaces between words"), self.showMultiSpaces, self.tr("Applies to the document editor only.") ) @@ -848,8 +854,6 @@ class GuiPreferences(NDialog): self.mainForm.finalise() self.sidebar.setSelected(1) - return - ## # Events ## @@ -860,7 +864,6 @@ class GuiPreferences(NDialog): self._saveWindowSize() event.accept() self.softDelete() - return def keyPressEvent(self, event: QKeyEvent) -> None: """Overload keyPressEvent and only accept escape. The main @@ -870,7 +873,6 @@ class GuiPreferences(NDialog): if event.matches(QKeySequence.StandardKey.Cancel): self.close() event.ignore() - return ## # Private Slots @@ -880,13 +882,11 @@ class GuiPreferences(NDialog): def _sidebarClicked(self, section: int) -> None: """Process a user request to switch page.""" self.mainForm.scrollToSection(section) - return @pyqtSlot() def _gotoSearch(self) -> None: """Go to the setting indicated by the search text.""" self.mainForm.scrollToLabel(self.searchText.text().strip()) - return @pyqtSlot() def _selectGuiFont(self) -> None: @@ -896,7 +896,6 @@ class GuiPreferences(NDialog): self.guiFont.setText(describeFont(font)) self.guiFont.setCursorPosition(0) self._guiFont = font - return @pyqtSlot() def _selectTextFont(self) -> None: @@ -906,7 +905,6 @@ class GuiPreferences(NDialog): self.textFont.setText(describeFont(font)) self.textFont.setCursorPosition(0) self._textFont = font - return @pyqtSlot() def _backupFolder(self) -> None: @@ -917,13 +915,11 @@ class GuiPreferences(NDialog): ): self.backupPath = path self.mainForm.setHelpText("backupPath", self.tr("Path: {0}").format(path)) - return @pyqtSlot(bool) def _toggledBackupOnClose(self, state: bool) -> None: """Toggle switch that depends on the backup on close switch.""" self.askBeforeBackup.setEnabled(state) - return @pyqtSlot(str) def _insertDialogLineSymbol(self, symbol: str) -> None: @@ -931,7 +927,6 @@ class GuiPreferences(NDialog): current = self.dialogLine.text() values = processDialogSymbols(f"{current} {symbol}") self.dialogLine.setText(" ".join(values)) - return @pyqtSlot(bool) def _toggleAutoReplaceMain(self, state: bool) -> None: @@ -941,7 +936,6 @@ class GuiPreferences(NDialog): self.doReplaceDash.setEnabled(state) self.doReplaceDots.setEnabled(state) self.fmtPadThin.setEnabled(state) - return @pyqtSlot() def _changeSingleQuoteOpen(self) -> None: @@ -949,7 +943,6 @@ class GuiPreferences(NDialog): quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtSQuoteOpen.text()) if status: self.fmtSQuoteOpen.setText(quote) - return @pyqtSlot() def _changeSingleQuoteClose(self) -> None: @@ -957,7 +950,6 @@ class GuiPreferences(NDialog): quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtSQuoteClose.text()) if status: self.fmtSQuoteClose.setText(quote) - return @pyqtSlot() def _changeDoubleQuoteOpen(self) -> None: @@ -965,7 +957,6 @@ class GuiPreferences(NDialog): quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtDQuoteOpen.text()) if status: self.fmtDQuoteOpen.setText(quote) - return @pyqtSlot() def _changeDoubleQuoteClose(self) -> None: @@ -973,7 +964,6 @@ class GuiPreferences(NDialog): quote, status = GuiQuoteSelect.getQuote(self, current=self.fmtDQuoteClose.text()) if status: self.fmtDQuoteClose.setText(quote) - return ## # Internal Functions @@ -982,7 +972,6 @@ class GuiPreferences(NDialog): def _saveWindowSize(self) -> None: """Save the dialog window size.""" CONFIG.setPreferencesWinSize(self.width(), self.height()) - return def _doSave(self) -> None: """Save the values set in the form.""" @@ -993,18 +982,23 @@ class GuiPreferences(NDialog): # Appearance guiLocale = self.guiLocale.currentData() - guiTheme = self.guiTheme.currentData() + lightTheme = self.lightTheme.currentData() + darkTheme = self.darkTheme.currentData() iconTheme = self.iconTheme.currentData() useCharCount = self.useCharCount.isChecked() - updateTheme |= CONFIG.guiTheme != guiTheme + updateTheme |= CONFIG.lightTheme != lightTheme + updateTheme |= CONFIG.darkTheme != darkTheme updateTheme |= CONFIG.iconTheme != iconTheme needsRestart |= CONFIG.guiLocale != guiLocale needsRestart |= CONFIG.guiFont != self._guiFont refreshTree |= CONFIG.useCharCount != useCharCount + updateSyntax |= CONFIG.lightTheme != lightTheme + updateSyntax |= CONFIG.darkTheme != darkTheme CONFIG.guiLocale = guiLocale - CONFIG.guiTheme = guiTheme + CONFIG.lightTheme = lightTheme + CONFIG.darkTheme = darkTheme CONFIG.iconTheme = iconTheme CONFIG.hideVScroll = self.hideVScroll.isChecked() CONFIG.hideHScroll = self.hideHScroll.isChecked() @@ -1013,11 +1007,6 @@ class GuiPreferences(NDialog): CONFIG.setGuiFont(self._guiFont) # Document Style - guiSyntax = self.guiSyntax.currentData() - - updateSyntax |= CONFIG.guiSyntax != guiSyntax - - CONFIG.guiSyntax = guiSyntax CONFIG.showFullPath = self.showFullPath.isChecked() CONFIG.incNotesWCount = self.incNotesWCount.isChecked() CONFIG.setTextFont(self._textFont) @@ -1058,9 +1047,14 @@ class GuiPreferences(NDialog): CONFIG.tabWidth = self.tabWidth.value() # Text Editing + lineHighlight = self.lineHighlight.isChecked() + + updateSyntax |= CONFIG.lineHighlight != lineHighlight + CONFIG.spellLanguage = self.spellLanguage.currentData() CONFIG.autoSelect = self.autoSelect.isChecked() CONFIG.cursorWidth = self.cursorWidth.value() + CONFIG.lineHighlight = lineHighlight CONFIG.showTabsNSpaces = self.showTabsNSpaces.isChecked() CONFIG.showLineEndings = self.showLineEndings.isChecked() @@ -1121,5 +1115,3 @@ class GuiPreferences(NDialog): self.newPreferencesReady.emit(needsRestart, refreshTree, updateTheme, updateSyntax) self.close() - - return diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index 623be345..5e16d52e 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import csv @@ -33,14 +33,14 @@ from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt6.QtGui import QCloseEvent, QColor from PyQt6.QtWidgets import ( QAbstractItemView, QApplication, QColorDialog, QDialogButtonBox, - QFileDialog, QGridLayout, QHBoxLayout, QLineEdit, QMenu, QStackedWidget, - QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget + QFileDialog, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu, + QStackedWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED from novelwriter.common import formatFileFilter, qtAddAction, qtLambda, simplified from novelwriter.constants import nwLabels, trConst -from novelwriter.core.status import NWStatus, StatusEntry +from novelwriter.core.status import CUSTOM_COL, NWStatus, StatusEntry from novelwriter.enum import nwStatusShape from novelwriter.extensions.configlayout import NColorLabel, NFixedPage, NScrollableForm from novelwriter.extensions.modified import NComboBox, NDialog, NIconToolButton @@ -55,6 +55,7 @@ logger = logging.getLogger(__name__) class GuiProjectSettings(NDialog): + """GUI: Project Settings DIalog.""" PAGE_SETTINGS = 0 PAGE_STATUS = 1 @@ -137,11 +138,8 @@ class GuiProjectSettings(NDialog): logger.debug("Ready: GuiProjectSettings") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiProjectSettings") - return ## # Events @@ -152,7 +150,6 @@ class GuiProjectSettings(NDialog): self._saveSettings() event.accept() self.softDelete() - return ## # Private Slots @@ -169,7 +166,6 @@ class GuiProjectSettings(NDialog): self.mainStack.setCurrentWidget(self.importPage) elif pageId == self.PAGE_REPLACE: self.mainStack.setCurrentWidget(self.replacePage) - return @pyqtSlot() def _doSave(self) -> None: @@ -203,8 +199,6 @@ class GuiProjectSettings(NDialog): QApplication.processEvents() self.close() - return - ## # Internal Functions ## @@ -223,8 +217,6 @@ class GuiProjectSettings(NDialog): options.setValue("GuiProjectSettings", "importColW", importColW) options.setValue("GuiProjectSettings", "replaceColW", replaceColW) - return - class _SettingsPage(NScrollableForm): @@ -295,8 +287,6 @@ class _SettingsPage(NScrollableForm): self.finalise() - return - class _StatusPage(NFixedPage): @@ -326,6 +316,7 @@ class _StatusPage(NFixedPage): self._changed = False self._color = QColor(100, 100, 100) self._shape = nwStatusShape.SQUARE + self._theme = CUSTOM_COL self._icons = {} self._iPx = SHARED.theme.baseIconHeight @@ -384,11 +375,26 @@ class _StatusPage(NFixedPage): self.exportButton.clicked.connect(self._exportLabels) # Edit Form - self.labelText = QLineEdit(self) - self.labelText.setMaxLength(40) - self.labelText.setPlaceholderText(self.tr("Select item to edit")) - self.labelText.setEnabled(False) - self.labelText.textEdited.connect(self._onNameEdit) + self.editName = QLineEdit(self) + self.editName.setMaxLength(40) + self.editName.setPlaceholderText(self.tr("Select item to edit")) + self.editName.setEnabled(False) + self.editName.textEdited.connect(self._onNameEdit) + + self.labelName = QLabel(self.tr("Label"), self) + self.labelName.setBuddy(self.editName) + + # Icon Colours + self.iconColor = NComboBox(self) + self.iconColor.setMinimumWidth(200) + self.iconColor.setEnabled(False) + self.iconColor.addItem(self.tr("Custom"), CUSTOM_COL) + for key, label in nwLabels.THEME_COLORS.items(): + self.iconColor.addItem(trConst(label), key) + self.iconColor.currentIndexChanged.connect(self._onThemeSelect) + + self.labelColor = QLabel(self.tr("Colour"), self) + self.labelColor.setBuddy(self.iconColor) buttonStyle = ( "QToolButton {padding: 0 4px;} " @@ -425,6 +431,9 @@ class _StatusPage(NFixedPage): self.shapeButton.setStyleSheet(buttonStyle) self.shapeButton.setEnabled(False) + self.labelShape = QLabel(self.tr("Shape"), self) + self.labelShape.setBuddy(self.iconColor) + # Assemble self.listControls = QVBoxLayout() self.listControls.addWidget(self.addButton) @@ -435,10 +444,15 @@ class _StatusPage(NFixedPage): self.listControls.addWidget(self.importButton) self.listControls.addWidget(self.exportButton) - self.editBox = QHBoxLayout() - self.editBox.addWidget(self.labelText, 1) - self.editBox.addWidget(self.colorButton, 0) - self.editBox.addWidget(self.shapeButton, 0) + self.editBox = QGridLayout() + self.editBox.addWidget(self.labelName, 0, 0) + self.editBox.addWidget(self.editName, 0, 1, 1, 5) + self.editBox.addWidget(self.labelColor, 1, 0) + self.editBox.addWidget(self.iconColor, 1, 1) + self.editBox.addWidget(self.colorButton, 1, 2) + self.editBox.addWidget(self.labelShape, 1, 3) + self.editBox.addWidget(self.shapeButton, 1, 4) + self.editBox.setColumnStretch(5, 1) self.innerBox = QGridLayout() self.innerBox.addWidget(self.listBox, 0, 0) @@ -454,8 +468,6 @@ class _StatusPage(NFixedPage): self.setCentralLayout(self.outerBox) self._setButtonIcons() - return - @property def changed(self) -> bool: """The user changed these settings.""" @@ -494,16 +506,22 @@ class _StatusPage(NFixedPage): entry.name = name item.setText(self.C_LABEL, name) self._changed = True - return + + @pyqtSlot(int) + def _onThemeSelect(self, index: int) -> None: + """Update the colour handling on theme selection change.""" + self._theme = str(self.iconColor.currentData()) + self._setButtonIcons() + self._updateIcon() @pyqtSlot() def _onColorSelect(self) -> None: """Open a dialog to select the status icon colour.""" if (color := QColorDialog.getColor(self._color, self, self.trSelColor)).isValid(): self._color = color + self._theme = CUSTOM_COL self._setButtonIcons() self._updateIcon() - return @pyqtSlot() def _onItemCreate(self) -> None: @@ -511,9 +529,9 @@ class _StatusPage(NFixedPage): color = QColor(100, 100, 100) shape = nwStatusShape.SQUARE icon = NWStatus.createIcon(self._iPx, color, shape) - self._addItem(None, StatusEntry(self.tr("New Item"), color, shape, icon, 0)) + theme = str(self.iconColor.currentData()) + self._addItem(None, StatusEntry(self.tr("New Item"), color, theme, shape, icon, 0)) self._changed = True - return @pyqtSlot() def _onItemDelete(self) -> None: @@ -526,7 +544,6 @@ class _StatusPage(NFixedPage): else: self.listBox.takeTopLevelItem(iRow) self._changed = True - return @pyqtSlot() def _onSelectionChanged(self) -> None: @@ -537,25 +554,28 @@ class _StatusPage(NFixedPage): entry: StatusEntry = item.data(self.C_DATA, self.D_ENTRY) self._color = entry.color self._shape = entry.shape + self._theme = entry.theme self._setButtonIcons() - self.labelText.setText(entry.name) - self.labelText.selectAll() - self.labelText.setFocus() + self.editName.setText(entry.name) + self.editName.selectAll() + self.editName.setFocus() - self.labelText.setEnabled(True) + self.editName.setEnabled(True) + self.iconColor.setEnabled(True) self.colorButton.setEnabled(True) self.shapeButton.setEnabled(True) else: self._color = QColor(100, 100, 100) self._shape = nwStatusShape.SQUARE + self._theme = CUSTOM_COL self._setButtonIcons() - self.labelText.setText("") + self.editName.setText("") - self.labelText.setEnabled(False) + self.editName.setEnabled(False) + self.iconColor.setEnabled(False) self.colorButton.setEnabled(False) self.shapeButton.setEnabled(False) - return @pyqtSlot() def _importLabels(self) -> None: @@ -592,7 +612,6 @@ class _StatusPage(NFixedPage): writer.writerow([entry.shape.name, entry.color.name(), entry.name]) except Exception as exc: SHARED.error("Could not write file.", exc=exc) - return ## # Internal Functions @@ -603,19 +622,18 @@ class _StatusPage(NFixedPage): self._shape = shape self._setButtonIcons() self._updateIcon() - return def _updateIcon(self) -> None: """Apply changes made to a status icon.""" if item := self._getSelectedItem(): - icon = NWStatus.createIcon(self._iPx, self._color, self._shape) + icon = NWStatus.createIcon(self._iPx, self._pickColor(), self._shape) entry: StatusEntry = item.data(self.C_DATA, self.D_ENTRY) entry.color = self._color entry.shape = self._shape + entry.theme = self._theme entry.icon = icon item.setIcon(self.C_LABEL, icon) self._changed = True - return def _addItem(self, key: str | None, entry: StatusEntry) -> None: """Add a status item to the list.""" @@ -626,7 +644,6 @@ class _StatusPage(NFixedPage): item.setData(self.C_DATA, self.D_KEY, key) item.setData(self.C_DATA, self.D_ENTRY, entry) self.listBox.addTopLevelItem(item) - return def _moveItem(self, step: int) -> None: """Move and item up or down step.""" @@ -639,7 +656,6 @@ class _StatusPage(NFixedPage): self.listBox.clearSelection() cItem.setSelected(True) self._changed = True - return def _getSelectedItem(self) -> QTreeWidgetItem | None: """Get the currently selected item.""" @@ -658,10 +674,16 @@ class _StatusPage(NFixedPage): def _setButtonIcons(self) -> None: """Set the colour of the colour button.""" - icon = NWStatus.createIcon(self._iPx, self._color, nwStatusShape.SQUARE) + icon = NWStatus.createIcon(self._iPx, self._pickColor(), nwStatusShape.SQUARE) + self.iconColor.setCurrentData(self._theme, CUSTOM_COL) self.colorButton.setIcon(icon) self.shapeButton.setIcon(self._icons[self._shape]) - return + + def _pickColor(self) -> QColor: + """Get the correct colour value based on selections.""" + if self._theme == CUSTOM_COL: + return self._color + return SHARED.theme.getBaseColor(self._theme) class _ReplacePage(NFixedPage): @@ -743,8 +765,6 @@ class _ReplacePage(NFixedPage): self.setCentralLayout(self.outerBox) - return - @property def changed(self) -> bool: """The user changed these settings.""" @@ -777,7 +797,6 @@ class _ReplacePage(NFixedPage): if (item := self._getSelectedItem()) and (key := self._stripKey(text)): item.setText(self.C_KEY, f"<{key}>") self._changed = True - return @pyqtSlot(str) def _onValueEdit(self, text: str) -> None: @@ -785,7 +804,6 @@ class _ReplacePage(NFixedPage): if item := self._getSelectedItem(): item.setText(self.C_REPL, text) self._changed = True - return @pyqtSlot() def _onSelectionChanged(self) -> None: @@ -804,14 +822,12 @@ class _ReplacePage(NFixedPage): self.editValue.setText("") self.editKey.setEnabled(False) self.editValue.setEnabled(False) - return @pyqtSlot() def _onEntryCreated(self) -> None: """Add a new list entry.""" key = f"" self.listBox.addTopLevelItem(QTreeWidgetItem([key, ""])) - return @pyqtSlot() def _onEntryDeleted(self) -> None: @@ -819,7 +835,6 @@ class _ReplacePage(NFixedPage): if item := self._getSelectedItem(): self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(item)) self._changed = True - return ## # Internal Functions diff --git a/novelwriter/dialogs/quotes.py b/novelwriter/dialogs/quotes.py index 931558a2..6030ecfe 100644 --- a/novelwriter/dialogs/quotes.py +++ b/novelwriter/dialogs/quotes.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -43,6 +43,7 @@ logger = logging.getLogger(__name__) class GuiQuoteSelect(NDialog): + """GUI: Quote Selector Dialog.""" _selected = "" @@ -108,11 +109,8 @@ class GuiQuoteSelect(NDialog): logger.debug("Ready: GuiQuoteSelect") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiQuoteSelect") - return @property def selectedQuote(self) -> str: @@ -140,4 +138,3 @@ class GuiQuoteSelect(NDialog): quote = items[0].data(self.D_KEY) self.previewLabel.setText(quote) self._selected = quote - return diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index a667738a..19e2da9d 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -48,6 +48,7 @@ logger = logging.getLogger(__name__) class GuiWordList(NDialog): + """GUI: User Dictionary Edit Tool.""" newWordListReady = pyqtSignal() @@ -128,11 +129,8 @@ class GuiWordList(NDialog): logger.debug("Ready: GuiWordList") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiWordList") - return ## # Events @@ -143,7 +141,6 @@ class GuiWordList(NDialog): self._saveGuiSettings() event.accept() self.softDelete() - return ## # Private Slots @@ -159,14 +156,12 @@ class GuiWordList(NDialog): if items := self.listBox.findItems(word, Qt.MatchFlag.MatchExactly): self.listBox.setCurrentItem(items[0]) self.listBox.scrollToItem(items[0], QAbstractItemView.ScrollHint.PositionAtCenter) - return @pyqtSlot() def _doDelete(self) -> None: """Delete the selected items.""" for item in self.listBox.selectedItems(): self.listBox.takeItem(self.listBox.row(item)) - return @pyqtSlot() def _doSave(self) -> None: @@ -178,7 +173,6 @@ class GuiWordList(NDialog): self.newWordListReady.emit() QApplication.processEvents() self.close() - return @pyqtSlot() def _importWords(self) -> None: @@ -213,7 +207,6 @@ class GuiWordList(NDialog): fo.write("\n".join(self._listWords())) except Exception as exc: SHARED.error("Could not write file.", exc=exc) - return ## # Internal Functions @@ -226,7 +219,6 @@ class GuiWordList(NDialog): self.listBox.clear() for word in userDict: self.listBox.addItem(word) - return def _saveGuiSettings(self) -> None: """Save GUI settings.""" @@ -234,14 +226,12 @@ class GuiWordList(NDialog): pOptions = SHARED.project.options pOptions.setValue("GuiWordList", "winWidth", self.width()) pOptions.setValue("GuiWordList", "winHeight", self.height()) - return def _addWord(self, word: str) -> None: """Add a single word to the list.""" if word and not self.listBox.findItems(word, Qt.MatchFlag.MatchExactly): self.listBox.addItem(word) self._changed = True - return def _listWords(self) -> list[str]: """List all words in the list box.""" diff --git a/novelwriter/enum.py b/novelwriter/enum.py index a837d824..7ea60294 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -20,12 +20,13 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from enum import Enum class nwItemType(Enum): + """Enum: Project Item Types.""" NO_TYPE = 0 ROOT = 1 @@ -34,6 +35,7 @@ class nwItemType(Enum): class nwItemClass(Enum): + """Enum: Project Item Classes.""" NO_CLASS = 0 NOVEL = 1 @@ -50,6 +52,7 @@ class nwItemClass(Enum): class nwItemLayout(Enum): + """A project item's layout.""" NO_LAYOUT = 0 DOCUMENT = 1 @@ -57,6 +60,7 @@ class nwItemLayout(Enum): class nwComment(Enum): + """Types of text comments.""" PLAIN = 0 IGNORE = 1 @@ -69,6 +73,7 @@ class nwComment(Enum): class nwChange(Enum): + """Change request modes.""" CREATE = 0 UPDATE = 1 @@ -76,12 +81,14 @@ class nwChange(Enum): class nwDocMode(Enum): + """Document open modes.""" VIEW = 0 EDIT = 1 class nwDocAction(Enum): + """Document actions.""" NO_ACTION = 0 UNDO = 1 @@ -92,38 +99,40 @@ class nwDocAction(Enum): MD_ITALIC = 6 MD_BOLD = 7 MD_STRIKE = 8 - S_QUOTE = 9 - D_QUOTE = 10 - SEL_ALL = 11 - SEL_PARA = 12 - BLOCK_H1 = 13 - BLOCK_H2 = 14 - BLOCK_H3 = 15 - BLOCK_H4 = 16 - BLOCK_COM = 17 - BLOCK_IGN = 18 - BLOCK_TXT = 19 - BLOCK_TTL = 20 - BLOCK_UNN = 21 - BLOCK_HSC = 22 - REPL_SNG = 23 - REPL_DBL = 24 - RM_BREAKS = 25 - ALIGN_L = 26 - ALIGN_C = 27 - ALIGN_R = 28 - INDENT_L = 29 - INDENT_R = 30 - SC_ITALIC = 31 - SC_BOLD = 32 - SC_STRIKE = 33 - SC_ULINE = 34 - SC_MARK = 35 - SC_SUP = 36 - SC_SUB = 37 + MD_MARK = 9 + S_QUOTE = 10 + D_QUOTE = 11 + SEL_ALL = 12 + SEL_PARA = 13 + BLOCK_H1 = 14 + BLOCK_H2 = 15 + BLOCK_H3 = 16 + BLOCK_H4 = 17 + BLOCK_COM = 18 + BLOCK_IGN = 19 + BLOCK_TXT = 20 + BLOCK_TTL = 21 + BLOCK_UNN = 22 + BLOCK_HSC = 23 + REPL_SNG = 24 + REPL_DBL = 25 + RM_BREAKS = 26 + ALIGN_L = 27 + ALIGN_C = 28 + ALIGN_R = 29 + INDENT_L = 30 + INDENT_R = 31 + SC_ITALIC = 32 + SC_BOLD = 33 + SC_STRIKE = 34 + SC_ULINE = 35 + SC_MARK = 36 + SC_SUP = 37 + SC_SUB = 38 class nwDocInsert(Enum): + """Document insert actions.""" NO_INSERT = 0 QUOTE_LS = 1 @@ -141,6 +150,7 @@ class nwDocInsert(Enum): class nwView(Enum): + """Main GUI view modes.""" EDITOR = 0 PROJECT = 1 @@ -150,13 +160,23 @@ class nwView(Enum): class nwFocus(Enum): + """Main GUI panel focus.""" TREE = 1 DOCUMENT = 2 OUTLINE = 3 +class nwTheme(Enum): + """GUI theme colour modes.""" + + AUTO = 0 + LIGHT = 1 + DARK = 2 + + class nwOutline(Enum): + """Enum: Project Outline Columns.""" TITLE = 0 LEVEL = 1 @@ -181,6 +201,7 @@ class nwOutline(Enum): class nwNovelExtra(Enum): + """Enum: Novel View Extra Columns.""" HIDDEN = 0 POV = 1 @@ -189,6 +210,7 @@ class nwNovelExtra(Enum): class nwBuildFmt(Enum): + """Enum: Manuscript Document Formats.""" ODT = 0 FODT = 1 @@ -203,6 +225,7 @@ class nwBuildFmt(Enum): class nwStatusShape(Enum): + """Enum: Status/Importance Icon Shapes.""" SQUARE = 0 TRIANGLE = 1 diff --git a/novelwriter/error.py b/novelwriter/error.py index 03f5d07a..59fafdff 100644 --- a/novelwriter/error.py +++ b/novelwriter/error.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -47,7 +47,6 @@ def logException() -> None: exType, exValue, _ = sys.exc_info() if exType is not None: logger.error(f"{exType.__name__}: {exValue!s}", stacklevel=2) - return def formatException(exc: BaseException) -> str: @@ -58,6 +57,7 @@ def formatException(exc: BaseException) -> str: class NWErrorMessage(QDialog): + """GUI: Error Dialog.""" def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -110,8 +110,6 @@ class NWErrorMessage(QDialog): self.setSizeGripEnabled(True) self.resize(800, 400) - return - def setMessage(self, exType: type, exValue: BaseException, exTrace: TracebackType) -> None: """Generate a message and append session data, error info and error traceback. @@ -143,7 +141,7 @@ class NWErrorMessage(QDialog): enchantVersion = "Unknown" try: - txtTrace = "\n".join(format_tb(exTrace)) + trace = "\n".join(format_tb(exTrace)) self.msgBody.setPlainText( "Environment:\n" f"novelWriter Version: {__version__}\n" @@ -152,13 +150,11 @@ class NWErrorMessage(QDialog): f"Qt: {QT_VERSION_STR}, PyQt: {PYQT_VERSION_STR}\n" f"enchant: {enchantVersion}\n\n" f"{exType.__name__}:\n{exValue!s}\n\n" - f"Traceback:\n{txtTrace}\n" + f"Traceback:\n{trace}\n" ) except Exception: self.msgBody.setPlainText("Failed to generate error report ...") - return - ## # Slots ## @@ -167,11 +163,10 @@ class NWErrorMessage(QDialog): def _doClose(self) -> None: """Close the dialog.""" self.close() - return def exceptionHandler(exType: type, exValue: BaseException, exTrace: TracebackType) -> None: - """Function to catch unhandled global exceptions.""" + """Catch unhandled global exceptions.""" from traceback import print_tb from PyQt6.QtWidgets import QApplication diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index b82a0997..14800a5c 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -24,7 +24,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from PyQt6.QtGui import QColor, QFont, QPalette, QPixmap @@ -39,7 +39,7 @@ DEFAULT_SCALE = 0.9 class NFixedPage(QFrame): - """Extension: Fixed Page Widget + """Extension: Fixed Page Widget. A custom widget that holds a layout. This is just a wrapper around a QFrame that sets the same frame style as the other Page widgets. @@ -49,23 +49,20 @@ class NFixedPage(QFrame): super().__init__(parent=parent) self.setFrameShadow(QFrame.Shadow.Sunken) self.setFrameShape(QFrame.Shape.StyledPanel) - return def setCentralLayout(self, layout: QLayout) -> None: """Set a layout as the central object.""" self.setLayout(layout) - return def setCentralWidget(self, widget: QWidget) -> None: """Set a layout as the central object.""" layout = QHBoxLayout() layout.addWidget(widget) self.setLayout(layout) - return class NScrollablePage(QScrollArea): - """Extension: Scrollable Page Widget + """Extension: Scrollable Page Widget. A custom widget that holds a layout within a scrollable area. """ @@ -79,16 +76,14 @@ class NScrollablePage(QScrollArea): self.setVerticalScrollBarPolicy(QtScrollAsNeeded) self.setFrameShadow(QFrame.Shadow.Sunken) self.setFrameShape(QFrame.Shape.StyledPanel) - return def setCentralLayout(self, layout: QLayout) -> None: """Set the central layout of the scroll page.""" self._widget.setLayout(layout) - return class NScrollableForm(QScrollArea): - """Extension: Scrollable Form Widget + """Extension: Scrollable Form Widget. A custom widget that creates a form within a scrollable area. """ @@ -117,8 +112,6 @@ class NScrollableForm(QScrollArea): self.setFrameShadow(QFrame.Shadow.Sunken) self.setFrameShape(QFrame.Shape.StyledPanel) - return - ## # Properties ## @@ -135,18 +128,15 @@ class NScrollableForm(QScrollArea): """Set the text color for the help text.""" self._helpCol = color self._fontScale = scale - return def setHelpText(self, key: str, text: str) -> None: """Set the text for the help label.""" if qHelp := self._editable.get(key): qHelp.setText(text) - return def setRowIndent(self, indent: int) -> None: """Set the indentation of each row.""" self._indent = max(indent, 0) - return ## # Methods @@ -158,7 +148,6 @@ class NScrollableForm(QScrollArea): yPos = self._sections[identifier].pos().y() - 8 if vBar := self.verticalScrollBar(): vBar.setValue(yPos) - return def scrollToLabel(self, label: str) -> None: """Scroll to the requested label.""" @@ -166,7 +155,6 @@ class NScrollableForm(QScrollArea): yPos = self._index[label].pos().y() - 8 if vBar := self.verticalScrollBar(): vBar.setValue(yPos) - return def addGroupLabel(self, label: str, identifier: int | None = None) -> None: """Add a text label to separate groups of settings.""" @@ -178,7 +166,6 @@ class NScrollableForm(QScrollArea): self._first = False if identifier is not None: self._sections[identifier] = qLabel - return def addRow( self, @@ -252,17 +239,14 @@ class NScrollableForm(QScrollArea): self._index[label.strip()] = qWidget qLabel.setAccessibleName(text) - return - def finalise(self) -> None: """Finalise the layout when the form is built.""" self._layout.addSpacing(20) self._layout.addStretch(1) - return class NColorLabel(QLabel): - """Extension: A Coloured Label + """Extension: A Coloured Label. A custom widget that draws a label in a specific colour, and optionally at a specific size, and word wrapped. @@ -298,21 +282,17 @@ class NColorLabel(QLabel): self.setWordWrap(wrap) self.setColorState(True) - return - def setTextColors(self, *, color: QColor | None = None, faded: QColor | None = None) -> None: """Set or update the text colours.""" self._color = color or self._color self._faded = faded or self._faded self._refeshTextColor() - return def setColorState(self, state: bool) -> None: """Change the colour state.""" if self._state is not state: self._state = state self._refeshTextColor() - return def _refeshTextColor(self) -> None: """Refresh the colour of the text on the label.""" @@ -322,11 +302,10 @@ class NColorLabel(QLabel): self._color if self._state else self._faded, ) self.setPalette(palette) - return class NWrappedWidgetBox(QHBoxLayout): - """Extension: A Text-Wrapped Widget Box + """Extension: A Text-Wrapped Widget Box. A custom layout box where a widget is wrapped in text labels on either side within a layout box. The widget is inserted at the {0} @@ -341,4 +320,3 @@ class NWrappedWidgetBox(QHBoxLayout): self.addWidget(widget) if after: self.addWidget(QLabel(after.lstrip())) - return diff --git a/novelwriter/extensions/eventfilters.py b/novelwriter/extensions/eventfilters.py index 09366590..844aacd6 100644 --- a/novelwriter/extensions/eventfilters.py +++ b/novelwriter/extensions/eventfilters.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from typing import TYPE_CHECKING @@ -34,7 +34,7 @@ if TYPE_CHECKING: class WheelEventFilter(QObject): - """Extensions: Wheel Event Filter + """Extensions: Wheel Event Filter. An event filter that filters mouse wheel events for a widget and forward them to the root widget. This solves the lack of mouse wheel @@ -50,7 +50,6 @@ class WheelEventFilter(QObject): super().__init__(parent=parent) self._parent = parent self._locked = False - return def eventFilter(self, obj: QObject, event: QEvent) -> bool: """Filter events of type QWheelEvent and forward them to the @@ -69,6 +68,7 @@ class WheelEventFilter(QObject): class StatusTipFilter(QObject): + """Filter: Remove StatusBar ToolTips.""" def eventFilter(self, obj: QObject, event: QEvent) -> bool: """Filter out status tip events on menus.""" diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index 9bea4c78..bb256ff1 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -24,7 +24,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from typing import TYPE_CHECKING @@ -47,6 +47,7 @@ if TYPE_CHECKING: class NDialog(QDialog): + """Custom: Modified QDialog.""" def softDelete(self) -> None: """Since calling deleteLater is sometimes not safe from Python, @@ -55,53 +56,54 @@ class NDialog(QDialog): so that it gets garbage collected when it runs out of scope. """ self.setParent(None) # type: ignore - return @pyqtSlot() def reject(self) -> None: """Overload the reject slot and also call close.""" super().reject() self.close() - return class NToolDialog(NDialog): + """Custom: Modified QDialog for Tools.""" def __init__(self, parent: GuiMain) -> None: super().__init__(parent=parent) self.setModal(False) if CONFIG.osDarwin: self.setWindowFlag(Qt.WindowType.Tool) - return def activateDialog(self) -> None: - """Helper function to activate dialog on various systems.""" + """Activate dialog on various operating systems.""" self.show() if CONFIG.osWindows: self.activateWindow() self.raise_() QApplication.processEvents() - return class NNonBlockingDialog(NDialog): + """Custom: Modified Non-Blocking QDialog.""" def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent=parent) self.setModal(True) - return def activateDialog(self) -> None: - """Helper function to activate dialog on various systems.""" + """Activate dialog on various operating systems.""" self.show() if CONFIG.osWindows: self.activateWindow() self.raise_() QApplication.processEvents() - return class NTreeView(QTreeView): + """Custom: Modified QTreeView. + + The main purpose is to provide the middleClicked signal that matches + clicked and doubleCLicked. + """ middleClicked = pyqtSignal(QModelIndex) @@ -116,42 +118,62 @@ class NTreeView(QTreeView): class NComboBox(QComboBox): + """Custom: Modified QComboBox. - def __init__(self, parent: QWidget | None = None) -> None: + The main purpose is to provide a combo box that doesn't scroll when + the mousewheel is active on it while scrolling through a scrollable + window of many widgets. + """ + + def __init__(self, parent: QWidget | None = None, maxItems: int = 15) -> None: super().__init__(parent=parent) self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) - return + 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;}") def wheelEvent(self, event: QWheelEvent) -> None: + """Only capture the mouse wheel if the widget has focus.""" if self.hasFocus(): super().wheelEvent(event) else: event.ignore() - return def setCurrentData(self, data: str | int | Enum, default: str | int | Enum) -> None: """Set the current index from data, with a fallback.""" idx = self.findData(data) self.setCurrentIndex(self.findData(default) if idx < 0 else idx) - return class NSpinBox(QSpinBox): + """Custom: Modified QSpinBox. + + The main purpose is to provide a spin box that doesn't scroll when + the mousewheel is active on it while scrolling through a scrollable + window of many widgets. + """ def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent=parent) self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) - return def wheelEvent(self, event: QWheelEvent) -> None: + """Only capture the mouse wheel if the widget has focus.""" if self.hasFocus(): super().wheelEvent(event) else: event.ignore() - return class NDoubleSpinBox(QDoubleSpinBox): + """Custom: Modified QDoubleSpinBox. + + The main purpose is to provide a float spin box that doesn't scroll + when the mousewheel is active on it while scrolling through a + scrollable window of many widgets. + """ def __init__( self, @@ -168,17 +190,20 @@ class NDoubleSpinBox(QDoubleSpinBox): self.setMaximum(maxVal) self.setSingleStep(step) self.setDecimals(prec) - return def wheelEvent(self, event: QWheelEvent) -> None: + """Only capture the mouse wheel if the widget has focus.""" if self.hasFocus(): super().wheelEvent(event) else: event.ignore() - return class NIconToolButton(QToolButton): + """Custom: Modified QToolButton. + + A quicker way to create a tool button using the app theme. + """ def __init__( self, parent: QWidget, iconSize: QSize, @@ -190,15 +215,17 @@ class NIconToolButton(QToolButton): self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) if icon: self.setThemeIcon(icon, color) - return def setThemeIcon(self, iconKey: str, color: str | None = None) -> None: """Set an icon from the current theme.""" self.setIcon(SHARED.theme.getIcon(iconKey, color)) - return class NIconToggleButton(QToolButton): + """Custom: Modified 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: super().__init__(parent=parent) @@ -209,16 +236,15 @@ class NIconToggleButton(QToolButton): self.setStyleSheet("border: none; background: transparent;") if icon: self.setThemeIcon(icon) - return def setThemeIcon(self, iconKey: str) -> None: """Set an icon from the current theme.""" iconSize = self.iconSize() self.setIcon(SHARED.theme.getToggleIcon(iconKey, (iconSize.width(), iconSize.height()))) - return class NClickableLabel(QLabel): + """Custom: Clickable QLabel.""" mouseClicked = pyqtSignal() diff --git a/novelwriter/extensions/novelselector.py b/novelwriter/extensions/novelselector.py index ee585a0d..27d217c6 100644 --- a/novelwriter/extensions/novelselector.py +++ b/novelwriter/extensions/novelselector.py @@ -20,23 +20,29 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging +from typing import TYPE_CHECKING + from PyQt6.QtCore import pyqtSignal, pyqtSlot from PyQt6.QtGui import QPalette -from PyQt6.QtWidgets import QComboBox, QWidget +from PyQt6.QtWidgets import QComboBox from novelwriter import SHARED from novelwriter.constants import nwLabels from novelwriter.enum import nwItemClass +if TYPE_CHECKING: + from PyQt6.QtWidgets import QWidget + logger = logging.getLogger(__name__) class NovelSelector(QComboBox): + """Custom: Novel Root Folder Selector.""" novelSelectionChanged = pyqtSignal(str) @@ -48,7 +54,6 @@ class NovelSelector(QComboBox): self._listFormat = None self.currentIndexChanged.connect(self._indexChanged) self.updateTheme() - return ## # Properties @@ -75,18 +80,15 @@ class NovelSelector(QComboBox): self._blockSignal = blockSignal self.setCurrentIndex(index) self._blockSignal = False - return def setIncludeAll(self, value: bool) -> None: """Set flag to add an "All Novel Folders" option.""" self._includeAll = value - return def setListFormat(self, value: str | None) -> None: """Set a format string for the list entries.""" if value is None or "{0}" in value: self._listFormat = value - return def updateTheme(self) -> None: """Update theme colours.""" @@ -94,7 +96,6 @@ class NovelSelector(QComboBox): palette.setBrush(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, palette.text()) self.setPalette(palette) self.refreshNovelList() - return ## # Public Slots @@ -128,8 +129,6 @@ class NovelSelector(QComboBox): self.setEnabled(self.count() > 1) self._blockSignal = False - return - ## # Private Slots ## @@ -139,4 +138,3 @@ class NovelSelector(QComboBox): """Re-emit the change of selection signal, unless blocked.""" if not self._blockSignal: self.novelSelectionChanged.emit(self.currentData()) - return diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py index b1745816..fd332495 100644 --- a/novelwriter/extensions/pagedsidebar.py +++ b/novelwriter/extensions/pagedsidebar.py @@ -22,7 +22,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from PyQt6.QtCore import QPoint, QRectF, QSize, Qt, pyqtSignal, pyqtSlot @@ -39,7 +39,7 @@ from novelwriter.types import ( class NPagedSideBar(QToolBar): - """Extensions: Paged Side Bar + """Extensions: Paged Side Bar. A side bar widget that holds buttons that mimic tabs. It is designed to be used in combination with a QStackedWidget for options panels. @@ -65,8 +65,6 @@ class NPagedSideBar(QToolBar): stretch.setSizePolicy(QtSizeExpanding, QtSizeExpanding) self._stretchAction = self.addWidget(stretch) - return - def button(self, buttonId: int) -> _PagedToolButton: """Return a specific button.""" return self._buttons[buttonId] @@ -74,14 +72,12 @@ class NPagedSideBar(QToolBar): def setLabelColor(self, color: QColor) -> None: """Set the text color for the labels.""" self._labelCol = color - return def addLabel(self, text: str) -> None: """Add a new label to the toolbar.""" label = _NPagedToolLabel(self, self._labelCol) label.setText(text) self.insertWidget(self._stretchAction, label) - return def addButton(self, text: str, buttonId: int = -1) -> None: """Add a new button to the toolbar.""" @@ -90,13 +86,11 @@ class NPagedSideBar(QToolBar): self.insertWidget(self._stretchAction, button) self._group.addButton(button, id=buttonId) self._buttons[buttonId] = button - return def setSelected(self, buttonId: int) -> None: """Set the selected button.""" if button := self._group.button(buttonId): button.setChecked(True) - return ## # Private Slots @@ -104,11 +98,10 @@ class NPagedSideBar(QToolBar): @pyqtSlot("QAbstractButton*") def _buttonClicked(self, button: QAbstractButton) -> None: - """A button was clicked in the group, emit its id.""" + """Handle a button click in the group and emit its id.""" buttonId = self._group.id(button) if buttonId != -1: self.buttonClicked.emit(buttonId) - return class _PagedToolButton(QToolButton): @@ -127,8 +120,6 @@ class _PagedToolButton(QToolButton): self._aH = 2*fH//7 self.setFixedHeight(self._bH) - return - def sizeHint(self) -> QSize: """Return a size hint that includes the arrow.""" return super().sizeHint() + QSize(4*self._aH, 0) @@ -156,7 +147,7 @@ class _PagedToolButton(QToolButton): if self.isChecked(): painter.setBrush(palette.highlight()) - painter.setOpacity(0.35) + painter.setOpacity(0.8) painter.drawRoundedRect(0, 0, width, height, 4, 4) textCol = palette.highlightedText().color() else: @@ -180,8 +171,6 @@ class _PagedToolButton(QToolButton): ])) painter.end() - return - class _NPagedToolLabel(QLabel): @@ -199,8 +188,6 @@ class _NPagedToolLabel(QLabel): self._textCol = textColor or self.palette().text().color() - return - def paintEvent(self, event: QPaintEvent) -> None: """Overload the paint event to draw a simple, left aligned text label that matches the button style. @@ -215,5 +202,3 @@ class _NPagedToolLabel(QLabel): painter.setOpacity(1.0) painter.drawText(QRectF(4, self._tM, tW, tH), QtAlignLeft, self.text()) painter.end() - - return diff --git a/novelwriter/extensions/progressbars.py b/novelwriter/extensions/progressbars.py index 0c92564e..8c0b1d1d 100644 --- a/novelwriter/extensions/progressbars.py +++ b/novelwriter/extensions/progressbars.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from math import ceil @@ -37,7 +37,7 @@ from novelwriter.types import ( class NProgressCircle(QProgressBar): - """Extension: Circular Progress Widget + """Extension: Circular Progress Widget. A custom widget that paints a circular progress indicator instead of a straight bar. It is also possible to set custom text for iṫ. @@ -64,7 +64,6 @@ class NProgressCircle(QProgressBar): self.setSizePolicy(QtSizeFixed, QtSizeFixed) self.setFixedWidth(size) self.setFixedHeight(size) - return def setColors( self, back: QColor | None = None, track: QColor | None = None, @@ -80,16 +79,14 @@ class NProgressCircle(QProgressBar): self._bPen = QPen(QBrush(track), self._point, QtSolidLine, QtRoundCap) if isinstance(text, QColor): self._tColor = text - return def setCentreText(self, text: str | None) -> None: """Replace the progress text with a custom string.""" self._text = text self.setValue(self.value()) # Triggers a redraw - return def paintEvent(self, event: QPaintEvent) -> None: - """Custom painter for the progress bar.""" + """Paint the progress bar.""" progress = 100.0*self.value()/self.maximum() angle = ceil(16*3.6*progress) painter = QPainter(self) @@ -103,21 +100,19 @@ class NProgressCircle(QProgressBar): painter.drawArc(self._cRect, 90*16, -angle) painter.setPen(self._tColor) painter.drawText(self._cRect, QtAlignCenter, self._text or f"{progress:.1f} %") - return class NProgressSimple(QProgressBar): - """Extension: Simple Progress Widget + """Extension: Simple Progress Widget. A custom widget that paints a plain bar with no other styling. """ def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) - return def paintEvent(self, event: QPaintEvent) -> None: - """Custom painter for the progress bar.""" + """Paint the progress bar.""" if (value := self.value()) > 0: progress = ceil(self.width()*float(value)/self.maximum()) painter = QPainter(self) @@ -125,4 +120,3 @@ class NProgressSimple(QProgressBar): painter.setPen(self.palette().highlight().color()) painter.setBrush(self.palette().highlight()) painter.drawRect(0, 0, progress, self.height()) - return diff --git a/novelwriter/extensions/statusled.py b/novelwriter/extensions/statusled.py index 5547a664..9152abb2 100644 --- a/novelwriter/extensions/statusled.py +++ b/novelwriter/extensions/statusled.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -34,6 +34,7 @@ logger = logging.getLogger(__name__) class StatusLED(QAbstractButton): + """Custom: LED Style Indicator.""" __slots__ = ("_color", "_negative", "_neutral", "_postitve", "_state") @@ -46,7 +47,6 @@ class StatusLED(QAbstractButton): self._state = None self.setFixedWidth(sW) self.setFixedHeight(sH) - return @property def state(self) -> bool | None: @@ -59,7 +59,6 @@ class StatusLED(QAbstractButton): self._postitve = positive self._negative = negative self.setState(self._state) - return def setState(self, state: bool | None) -> None: """Set the colour state.""" @@ -71,7 +70,6 @@ class StatusLED(QAbstractButton): self._color = self._neutral self._state = state self.update() - return def paintEvent(self, event: QPaintEvent) -> None: """Draw the LED.""" @@ -82,4 +80,3 @@ class StatusLED(QAbstractButton): painter.setOpacity(1.0) painter.drawEllipse(1, 1, self.width() - 2, self.height() - 2) painter.end() - return diff --git a/novelwriter/extensions/switch.py b/novelwriter/extensions/switch.py index 4699d50a..cc0d43a9 100644 --- a/novelwriter/extensions/switch.py +++ b/novelwriter/extensions/switch.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from PyQt6.QtCore import QPropertyAnimation, Qt, pyqtProperty, pyqtSlot # pyright: ignore @@ -32,8 +32,9 @@ from novelwriter.types import QtNoPen, QtPaintAntiAlias, QtSizeFixed class NSwitch(QAbstractButton): + """Custom: Toggle Switch.""" - __slots__ = ("_offset", "_rH", "_rR", "_xH", "_xR", "_xW") + __slots__ = ("_cOff", "_cOn", "_offset", "_rH", "_rR", "_xH", "_xR", "_xW") def __init__(self, parent: QWidget, height: int = 0) -> None: super().__init__(parent=parent) @@ -44,6 +45,9 @@ 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) @@ -52,8 +56,6 @@ class NSwitch(QAbstractButton): self.clicked.connect(self._onClick) - return - ## # Properties ## @@ -66,7 +68,6 @@ class NSwitch(QAbstractButton): def offset(self, offset: int) -> None: self._offset = offset self.update() - return ## # Getters and Setters @@ -76,7 +77,6 @@ class NSwitch(QAbstractButton): """Overload setChecked to also alter the offset.""" super().setChecked(checked) self._offset = (self._xW - self._xR) if checked else self._xR - return ## # Events @@ -86,7 +86,6 @@ class NSwitch(QAbstractButton): """Overload resize to ensure correct offset.""" super().resizeEvent(event) self._offset = (self._xW - self._xR) if self.isChecked() else self._xR - return def paintEvent(self, event: QPaintEvent) -> None: """Drawing the switch itself.""" @@ -97,7 +96,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(palette.highlight() if self.isChecked() else palette.alternateBase()) + painter.setBrush(self._cOn if self.isChecked() else self._cOff) painter.drawRoundedRect(0, 0, self._xW, self._xH, self._xR, self._xR) painter.setPen(QtNoPen) @@ -106,13 +105,10 @@ class NSwitch(QAbstractButton): painter.end() - return - def enterEvent(self, event: QEnterEvent) -> None: """Change the cursor when hovering the button.""" self.setCursor(Qt.CursorShape.PointingHandCursor) super().enterEvent(event) - return @pyqtSlot(bool) def _onClick(self, checked: bool) -> None: @@ -122,4 +118,3 @@ class NSwitch(QAbstractButton): anim.setStartValue(self._offset) anim.setEndValue((self._xW - self._xR) if checked else self._xR) anim.start() - return diff --git a/novelwriter/extensions/switchbox.py b/novelwriter/extensions/switchbox.py index e4732f97..e287cf25 100644 --- a/novelwriter/extensions/switchbox.py +++ b/novelwriter/extensions/switchbox.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from typing import TYPE_CHECKING @@ -39,7 +39,7 @@ if TYPE_CHECKING: class NSwitchBox(QScrollArea): - """Extension: Switch Box Widget + """Extension: Switch Box Widget. A widget that can hold a list of switches with labels and optional icons. The switch toggles emits a common signal with a switch key. @@ -55,7 +55,6 @@ class NSwitchBox(QScrollArea): self._sIcon = baseSize self._widgets = [] self.clear() - return def clear(self) -> None: """Rebuild the content of the core widget.""" @@ -72,8 +71,6 @@ class NSwitchBox(QScrollArea): self.setWidgetResizable(True) self.setWidget(self._widget) - return - def addLabel(self, text: str) -> None: """Add a header label to the content box.""" label = QLabel(text, self) @@ -83,7 +80,6 @@ class NSwitchBox(QScrollArea): self._content.addWidget(label, self._index, 0, 1, 3, QtAlignLeft) self._widgets.append(label) self._bumpIndex() - return def addItem(self, qIcon: QIcon, text: str, identifier: str, default: bool = False) -> None: """Add an item to the content box.""" @@ -104,8 +100,6 @@ class NSwitchBox(QScrollArea): self._widgets.append(switch) self._bumpIndex() - return - def addSeparator(self) -> None: """Add a blank entry in the content box.""" spacer = QWidget(self) @@ -113,7 +107,6 @@ class NSwitchBox(QScrollArea): self._content.addWidget(spacer, self._index, 0, 1, 3, QtAlignLeft) self._widgets.append(spacer) self._bumpIndex() - return ## # Internal Functions @@ -122,7 +115,6 @@ class NSwitchBox(QScrollArea): def _emitSwitchSignal(self, identifier: str, state: bool) -> None: """Emit a signal for a switch toggle.""" self.switchToggled.emit(identifier, state) - return def _bumpIndex(self) -> None: """Increase the index counter and make sure only the last @@ -131,4 +123,3 @@ class NSwitchBox(QScrollArea): self._content.setRowStretch(self._index, 0) self._content.setRowStretch(self._index + 1, 1) self._index += 1 - return diff --git a/novelwriter/extensions/versioninfo.py b/novelwriter/extensions/versioninfo.py index 2f13afaa..d92a71c8 100644 --- a/novelwriter/extensions/versioninfo.py +++ b/novelwriter/extensions/versioninfo.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import json @@ -45,6 +45,11 @@ API_URL = "https://api.github.com/repos/vkbo/novelwriter/releases/latest" class VersionInfoWidget(QWidget): + """Custom: version Info Label. + + A custom widget that will show a clickable area for contacting + GitHub and pulling the latest release version info. + """ def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -75,8 +80,6 @@ class VersionInfoWidget(QWidget): self.setLayout(self._layout) - return - ## # Private Slots ## @@ -93,7 +96,6 @@ class VersionInfoWidget(QWidget): lookup = _Retriever() lookup.signals.dataReady.connect(self._updateReleaseInfo) SHARED.runInThreadPool(lookup) - return ## # Private Slots @@ -109,7 +111,6 @@ class VersionInfoWidget(QWidget): )) else: self._lblRelease.setText(self._trLatest.format(reason or self.tr("Failed"))) - return class _Retriever(QRunnable): @@ -117,7 +118,6 @@ class _Retriever(QRunnable): def __init__(self) -> None: super().__init__() self.signals = _RetrieverSignal() - return @pyqtSlot() def run(self) -> None: @@ -140,7 +140,6 @@ class _Retriever(QRunnable): except Exception as e: logger.error("Failed to retrieve release info") self.signals.dataReady.emit("", str(e)) - return class _RetrieverSignal(QObject): diff --git a/novelwriter/formats/shared.py b/novelwriter/formats/shared.py index 8a2ab5e6..f6468125 100644 --- a/novelwriter/formats/shared.py +++ b/novelwriter/formats/shared.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import re @@ -29,7 +29,15 @@ from enum import Flag, IntEnum from PyQt6.QtGui import QColor -ESCAPES = {r"\*": "*", r"\~": "~", r"\_": "_", r"\[": "[", r"\]": "]", r"\ ": ""} +ESCAPES = { + r"\*": "*", + r"\~": "~", + r"\=": "=", + r"\_": "_", + r"\[": "[", + r"\]": "]", + r"\ ": "", +} RX_ESC = re.compile("|".join([re.escape(k) for k in ESCAPES.keys()]), flags=re.DOTALL) diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py index 366191ef..53d8884c 100644 --- a/novelwriter/formats/todocx.py +++ b/novelwriter/formats/todocx.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -38,7 +38,7 @@ from novelwriter import __version__ from novelwriter.common import firstFloat, xmlElement, xmlSubElem from novelwriter.constants import nwHeadFmt, nwStyles from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape -from novelwriter.formats.tokenizer import Tokenizer +from novelwriter.formats.tokenizer import COMMENT_BLOCKS, Tokenizer from novelwriter.types import QtHexRgb if TYPE_CHECKING: @@ -51,7 +51,7 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) # RegEx -RX_TEXT = re.compile(r"([\n\t])", re.UNICODE) +RX_TEXT = re.compile(r"([\n\t])") # Types and Relationships OOXML_SCM = "http://schemas.openxmlformats.org" @@ -100,7 +100,7 @@ def _wText(parent: ET.Element, text: str) -> ET.Element: def _mmToSz(value: float) -> int: - """Convert millimetres to internal margin size units""" + """Convert millimetres to internal margin size units.""" return int(value*20.0*72.0/25.4) @@ -143,6 +143,7 @@ S_FNOTE = "FootnoteText" class DocXXmlRel(NamedTuple): + """DocX XML Rel Data.""" rId: str relType: str @@ -150,6 +151,7 @@ class DocXXmlRel(NamedTuple): class DocXXmlFile(NamedTuple): + """DocX XML File Data.""" xml: ET.Element path: str @@ -157,6 +159,7 @@ class DocXXmlFile(NamedTuple): class DocXParStyle(NamedTuple): + """DocX XML Paragraph Style Data.""" name: str styleId: str @@ -176,7 +179,7 @@ class DocXParStyle(NamedTuple): class ToDocX(Tokenizer): - """Core: DocX Document Writer + """Core: DocX Document Writer. Extend the Tokenizer class to writer DocX Document files. """ @@ -202,8 +205,6 @@ class ToDocX(Tokenizer): self._usedNotes: dict[str, int] = {} self._usedFields: list[tuple[ET.Element, str]] = [] - return - ## # Setters ## @@ -214,25 +215,22 @@ class ToDocX(Tokenizer): """Set the document page size and margins in millimetres.""" self._pageSize = QSize(_mmToSz(width), _mmToSz(height)) self._pageMargins = QMargins(_mmToSz(left), _mmToSz(top), _mmToSz(right), _mmToSz(bottom)) - return def setHeaderFormat(self, value: str, offset: int) -> None: """Set the document header format.""" self._headerFormat = value.strip() self._pageOffset = offset - return ## # Class Methods ## def initDocument(self) -> None: - """Initialises the DocX document structure.""" + """Initialise the DocX document structure.""" super().initDocument() self._fontFamily = self._textFont.family() self._fontSize = self._textFont.pointSizeF() self._generateStyles() - return def doConvert(self) -> None: """Convert the list of text tokens into XML elements.""" @@ -296,14 +294,12 @@ class ToDocX(Tokenizer): elif tType == BlockTyp.SKIP: self._processFragments(par, S_NORM, "") - elif tType == BlockTyp.COMMENT: + elif tType in COMMENT_BLOCKS: self._processFragments(par, S_META, tText, tFormat) elif tType == BlockTyp.KEYWORD: self._processFragments(par, S_META, tText, tFormat) - return - def closeDocument(self) -> None: """Generate all the XML.""" self._coreXml() @@ -322,8 +318,6 @@ class ToDocX(Tokenizer): if self._usedNotes: self._footnotesXml() - return - def saveDocument(self, path: Path) -> None: """Save the data to a .docx file.""" # Content Lists @@ -373,8 +367,6 @@ class ToDocX(Tokenizer): xmlToZip(f"{rel.path}/{name}", rel.xml, outZip) xmlToZip("[Content_Types].xml", dTypes, outZip) - return - ## # Internal Functions ## @@ -454,8 +446,6 @@ class ToDocX(Tokenizer): if temp := text[fStart:]: par.addContent(self._textRunToXml(temp, xFmt, fClass, fLink)) - return - def _textRunToXml(self, text: str | None, fmt: int, fClass: str, fLink: str) -> ET.Element: """Encode the text run into XML.""" xR = xmlElement(_wTag("r")) @@ -668,8 +658,6 @@ class ToDocX(Tokenizer): for style in styles: self._styles[style.styleId] = style - return - def _nextRelId(self) -> str: """Generate the next unique rId.""" return f"rId{len(self._rels) + 1}" @@ -1054,6 +1042,10 @@ class ToDocX(Tokenizer): class DocXParagraph: + """DocX Text Paragraph. + + This class holds a single paragraph of a DocX document. + """ __slots__ = ( "_bottomMargin", "_breakAfter", "_breakBefore", "_content", @@ -1073,7 +1065,6 @@ class DocXParagraph: self._breakBefore = False self._breakAfter = False self._footnoteRef = False - return ## # Properties @@ -1091,53 +1082,43 @@ class DocXParagraph: def setStyle(self, style: DocXParStyle | None) -> None: """Set the paragraph style.""" self._style = style - return def setAlignment(self, value: str) -> None: """Set paragraph alignment.""" if value in ("left", "center", "right", "both"): self._textAlign = value - return def setMarginTop(self, value: float) -> None: """Set margin above in pt.""" self._topMargin = value - return def setMarginBottom(self, value: float) -> None: """Set margin below in pt.""" self._bottomMargin = value - return def setMarginLeft(self, value: float) -> None: """Set margin left in pt.""" self._leftMargin = value - return def setMarginRight(self, value: float) -> None: """Set margin right in pt.""" self._rightMargin = value - return def setIndentFirst(self, state: bool) -> None: """Set first line indent.""" self._indentFirst = state - return def setPageBreakBefore(self, state: bool) -> None: """Set page break before flag.""" self._breakBefore = state - return def setPageBreakAfter(self, state: bool) -> None: """Set page break after flag.""" self._breakAfter = state - return def setIsFootnote(self, state: bool) -> None: """Set is footnote flag.""" self._footnoteRef = state - return ## # Methods @@ -1146,10 +1127,9 @@ class DocXParagraph: def addContent(self, run: ET.Element) -> None: """Add a run segment to the paragraph.""" self._content.append(run) - return def toXml(self, body: ET.Element) -> None: - """Called after all content is set.""" + """Generate the XML. Call after all content is set.""" if style := self._style: xP = xmlSubElem(body, _wTag("p")) @@ -1191,5 +1171,3 @@ class DocXParagraph: if self._breakAfter: xR = xmlSubElem(xP, _wTag("r")) xmlSubElem(xR, _wTag("br"), attrib={_wTag("type"): "page"}) - - return diff --git a/novelwriter/formats/tohtml.py b/novelwriter/formats/tohtml.py index c7bf7b17..db9b0061 100644 --- a/novelwriter/formats/tohtml.py +++ b/novelwriter/formats/tohtml.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import json @@ -32,7 +32,7 @@ from typing import TYPE_CHECKING from novelwriter.common import formatTimeStamp from novelwriter.constants import nwHtmlUnicode, nwStyles from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape -from novelwriter.formats.tokenizer import Tokenizer +from novelwriter.formats.tokenizer import COMMENT_BLOCKS, Tokenizer from novelwriter.types import FONT_STYLE, FONT_WEIGHTS, QtHexRgb if TYPE_CHECKING: @@ -77,7 +77,7 @@ HTML_NONE = (0, "") class ToHtml(Tokenizer): - """Core: HTML Document Writer + """Core: HTML Document Writer. Extend the Tokenizer class to writer HTML output. This class is also used by the Document Viewer, and Manuscript Build Preview. @@ -90,7 +90,6 @@ class ToHtml(Tokenizer): self._usedNotes: dict[str, int] = {} self._usedFields: list[tuple[int, str]] = [] self.setReplaceUnicode(False) - return ## # Setters @@ -101,7 +100,6 @@ class ToHtml(Tokenizer): class tags. """ self._cssStyles = cssStyles - return def setReplaceUnicode(self, doReplace: bool) -> None: """Set the translation map to either minimal or full unicode for @@ -114,7 +112,6 @@ class ToHtml(Tokenizer): if doReplace: # Extend to all relevant Unicode characters self._trMap.update(str.maketrans(nwHtmlUnicode.U_TO_H)) - return ## # Class Methods @@ -130,7 +127,6 @@ class ToHtml(Tokenizer): """ super().doPreProcessing() self._text = self._text.translate(self._trMap) - return def doConvert(self) -> None: """Convert the list of text tokens into an HTML document.""" @@ -159,34 +155,33 @@ class ToHtml(Tokenizer): # If we don't have formatting, we can do a plain replace tText = tText.replace("<", "<").replace(">", ">") - # Styles + # Inline Styles aStyle = [] - if self._cssStyles: - if tStyle & BlockFmt.LEFT: - aStyle.append("text-align: left;") - elif tStyle & BlockFmt.RIGHT: - aStyle.append("text-align: right;") - elif tStyle & BlockFmt.CENTRE: - aStyle.append("text-align: center;") - elif tStyle & BlockFmt.JUSTIFY: - aStyle.append("text-align: justify;") + if tStyle & BlockFmt.LEFT: + aStyle.append("text-align: left;") + elif tStyle & BlockFmt.RIGHT: + aStyle.append("text-align: right;") + elif tStyle & BlockFmt.CENTRE: + aStyle.append("text-align: center;") + elif tStyle & BlockFmt.JUSTIFY: + aStyle.append("text-align: justify;") - if tStyle & BlockFmt.PBB: - aStyle.append("page-break-before: always;") - if tStyle & BlockFmt.PBA: - aStyle.append("page-break-after: always;") + if tStyle & BlockFmt.PBB: + aStyle.append("page-break-before: always;") + if tStyle & BlockFmt.PBA: + aStyle.append("page-break-after: always;") - if tStyle & BlockFmt.Z_BTM: - aStyle.append("margin-bottom: 0;") - if tStyle & BlockFmt.Z_TOP: - aStyle.append("margin-top: 0;") + if tStyle & BlockFmt.Z_BTM: + aStyle.append("margin-bottom: 0;") + if tStyle & BlockFmt.Z_TOP: + aStyle.append("margin-top: 0;") - if tStyle & BlockFmt.IND_L: - aStyle.append(f"margin-left: {self._blockIndent:.2f}em;") - if tStyle & BlockFmt.IND_R: - aStyle.append(f"margin-right: {self._blockIndent:.2f}em;") - if tStyle & BlockFmt.IND_T: - aStyle.append(f"text-indent: {self._firstWidth:.2f}em;") + if tStyle & BlockFmt.IND_L: + aStyle.append(f"margin-left: {self._blockIndent:.2f}em;") + if tStyle & BlockFmt.IND_R: + aStyle.append(f"margin-right: {self._blockIndent:.2f}em;") + if tStyle & BlockFmt.IND_T: + aStyle.append(f"text-indent: {self._firstWidth:.2f}em;") if aStyle: stVals = " ".join(aStyle) @@ -229,7 +224,7 @@ class ToHtml(Tokenizer): elif tType == BlockTyp.SKIP: lines.append(f" 

    \n") - elif tType == BlockTyp.COMMENT: + elif tType in COMMENT_BLOCKS: lines.append(f"

    {self._formatText(tText, tFmt)}

    \n") elif tType == BlockTyp.KEYWORD: @@ -238,8 +233,6 @@ class ToHtml(Tokenizer): self._pages.append("".join(lines)) - return - def closeDocument(self) -> None: """Run close document tasks.""" # Replace fields if there are stats available @@ -266,8 +259,6 @@ class ToHtml(Tokenizer): self._pages.append("".join(lines)) - return - def saveDocument(self, path: Path) -> None: """Save the data to an HTML file.""" if path.suffix.lower() == ".json": @@ -288,37 +279,33 @@ class ToHtml(Tokenizer): json.dump(data, fObj, indent=2) else: + html = [] + html.append("") + html.append("") + html.append("") + html.append(f"{self._project.data.name:s}") + html.append("") + if self._cssStyles: + html.append("") + html.append("") + html.append("") + html.append("") + html.append(("".join(self._pages)).replace("\t", " ").rstrip()) + html.append("") + html.append("\n") + with open(path, mode="w", encoding="utf-8") as fObj: - fObj.write(( - "\n" - "\n" - "\n" - "\n" - "{title:s}\n" - "\n" - "\n" - "\n" - "{body:s}\n" - "\n" - "\n" - ).format( - title=self._project.data.name, - style="\n".join(self.getStyleSheet()), - body=("".join(self._pages)).replace("\t", " ").rstrip(), - )) + fObj.write("\n".join(html)) logger.info("Wrote file: %s", path) - return - def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None: """Replace tabs with spaces in the html.""" tabSpace = spaceChar*nSpaces pages = [aLine.replace("\t", tabSpace) for aLine in self._pages] self._pages = pages - return def getStyleSheet(self) -> list[str]: """Generate a stylesheet for the current settings.""" @@ -410,7 +397,11 @@ class ToHtml(Tokenizer): # isn't already open, and only closed if it has previously been opened. tags: list[tuple[int, str]] = [] state = dict.fromkeys(HTML_OPENER, False) + plain = not self._cssStyles for pos, fmt, data in tFmt: + if plain and fmt in (TextFmt.COL_B, TextFmt.COL_E): + # We ignore colour tags if CSS is off + continue if m := HTML_OPENER.get(fmt): if not state.get(fmt, True): if fmt == TextFmt.COL_B and (color := self._classes.get(data)): diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index e0587002..a82105b4 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -55,6 +55,7 @@ logger = logging.getLogger(__name__) class ComStyle(NamedTuple): + """Comment style info.""" label: str = "" labelClass: str = "" @@ -71,16 +72,28 @@ COMMENT_STYLE = { nwComment.COMMENT: ComStyle(), nwComment.STORY: ComStyle("Story Structure", "modifier", "note"), } -HEADINGS = [ +COMMENT_TYPE = { + nwComment.PLAIN: BlockTyp.COMMENT, + nwComment.IGNORE: BlockTyp.COMMENT, + nwComment.SYNOPSIS: BlockTyp.SUMMARY, + nwComment.SHORT: BlockTyp.SUMMARY, + nwComment.NOTE: BlockTyp.NOTE, + nwComment.FOOTNOTE: BlockTyp.COMMENT, + nwComment.COMMENT: BlockTyp.COMMENT, + nwComment.STORY: BlockTyp.NOTE, +} +HEADING_BLOCKS = [ BlockTyp.TITLE, BlockTyp.PART, BlockTyp.HEAD1, BlockTyp.HEAD2, BlockTyp.HEAD3, BlockTyp.HEAD4, ] -SKIP_INDENT = [*HEADINGS, BlockTyp.SEP, BlockTyp.SKIP] +COMMENT_BLOCKS = (BlockTyp.COMMENT, BlockTyp.SUMMARY, BlockTyp.NOTE) +META_BLOCKS = (BlockTyp.COMMENT, BlockTyp.SUMMARY, BlockTyp.NOTE, BlockTyp.KEYWORD) +SKIP_INDENT = [*HEADING_BLOCKS, BlockTyp.SEP, BlockTyp.SKIP] B_EMPTY: T_Block = (BlockTyp.EMPTY, "", "", [], BlockFmt.NONE) class Tokenizer(ABC): - """Core: Text Tokenizer Abstract Base Class + """Core: Text Tokenizer Abstract Base Class. This is the base class for all document build classes. It parses the novelWriter markup format and generates a registry of tokens and @@ -189,6 +202,7 @@ class Tokenizer(ABC): (REGEX_PATTERNS.markdownItalic, [0, TextFmt.I_B, 0, TextFmt.I_E]), (REGEX_PATTERNS.markdownBold, [0, TextFmt.B_B, 0, TextFmt.B_E]), (REGEX_PATTERNS.markdownStrike, [0, TextFmt.D_B, 0, TextFmt.D_E]), + (REGEX_PATTERNS.markdownMark, [0, TextFmt.M_B, 0, TextFmt.M_E]), ] self._shortCodeFmt = { @@ -211,8 +225,6 @@ class Tokenizer(ABC): self._dialogParser = DialogParser() self._dialogParser.initParser() - return - ## # Properties ## @@ -240,94 +252,78 @@ class Tokenizer(ABC): """Set language for the document.""" if language: self._dLocale = QLocale(language) - return def setTheme(self, theme: TextDocumentTheme) -> None: """Set the document colour theme.""" self._theme = theme - return def setPartitionFormat(self, hFormat: str, hide: bool = False) -> None: """Set the partition format pattern.""" self._fmtPart = hFormat.strip() self._hidePart = hide - return def setChapterFormat(self, hFormat: str, hide: bool = False) -> None: """Set the chapter format pattern.""" self._fmtChapter = hFormat.strip() self._hideChapter = hide - return def setUnNumberedFormat(self, hFormat: str, hide: bool = False) -> None: """Set the unnumbered format pattern.""" self._fmtUnNum = hFormat.strip() self._hideUnNum = hide - return def setSceneFormat(self, hFormat: str, hide: bool = False) -> None: """Set the scene format pattern and hidden status.""" self._fmtScene = hFormat.strip() self._hideScene = hide - return def setHardSceneFormat(self, hFormat: str, hide: bool = False) -> None: """Set the hard scene format pattern and hidden status.""" self._fmtHScene = hFormat.strip() self._hideHScene = hide - return def setSectionFormat(self, hFormat: str, hide: bool = False) -> None: """Set the section format pattern and hidden status.""" self._fmtSection = hFormat.strip() self._hideSection = hide - return def setTitleStyle(self, center: bool, pageBreak: bool) -> None: """Set the title heading style.""" self._titleStyle = BlockFmt.CENTRE if center else BlockFmt.NONE self._titleStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE - return def setPartitionStyle(self, center: bool, pageBreak: bool) -> None: """Set the partition heading style.""" self._partStyle = BlockFmt.CENTRE if center else BlockFmt.NONE self._partStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE - return def setChapterStyle(self, center: bool, pageBreak: bool) -> None: """Set the chapter heading style.""" self._chapterStyle = BlockFmt.CENTRE if center else BlockFmt.NONE self._chapterStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE - return def setSceneStyle(self, center: bool, pageBreak: bool) -> None: """Set the scene heading style.""" self._sceneStyle = BlockFmt.CENTRE if center else BlockFmt.NONE self._sceneStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE - return def setTextFont(self, font: QFont) -> None: """Set the build font.""" self._textFont = fontMatcher(font) - return def setLineHeight(self, height: float) -> None: """Set the line height between 0.5 and 5.0.""" self._lineHeight = min(max(float(height), 0.5), 5.0) - return def setHeadingStyles(self, color: bool, scale: bool, bold: bool) -> None: """Set text style for headings.""" self._colorHeads = color self._scaleHeads = scale self._boldHeads = bold - return def setBlockIndent(self, indent: float) -> None: """Set the block indent between 0.0 and 10.0.""" self._blockIndent = min(max(float(indent), 0.0), 10.0) - return def setFirstLineIndent(self, state: bool, indent: float, first: bool) -> None: """Set first line indent and whether to also indent first @@ -336,67 +332,54 @@ class Tokenizer(ABC): self._firstIndent = state self._firstWidth = indent self._indentFirst = first - return def setJustify(self, state: bool) -> None: """Enable or disable text justification.""" self._doJustify = state - return def setDialogHighlight(self, state: bool) -> None: """Enable or disable dialogue highlighting.""" self._hlightDialog = state - return def setTitleMargins(self, upper: float, lower: float) -> None: """Set the upper and lower title margin.""" self._marginTitle = (float(upper), float(lower)) - return def setHead1Margins(self, upper: float, lower: float) -> None: """Set the upper and lower heading 1 margin.""" self._marginHead1 = (float(upper), float(lower)) - return def setHead2Margins(self, upper: float, lower: float) -> None: """Set the upper and lower heading 2 margin.""" self._marginHead2 = (float(upper), float(lower)) - return def setHead3Margins(self, upper: float, lower: float) -> None: """Set the upper and lower heading 3 margin.""" self._marginHead3 = (float(upper), float(lower)) - return def setHead4Margins(self, upper: float, lower: float) -> None: """Set the upper and lower heading 4 margin.""" self._marginHead4 = (float(upper), float(lower)) - return def setTextMargins(self, upper: float, lower: float) -> None: """Set the upper and lower text margin.""" self._marginText = (float(upper), float(lower)) - return def setMetaMargins(self, upper: float, lower: float) -> None: """Set the upper and lower meta text margin.""" self._marginMeta = (float(upper), float(lower)) - return def setSeparatorMargins(self, upper: float, lower: float) -> None: """Set the upper and lower meta text margin.""" self._marginSep = (float(upper), float(lower)) - return def setLinkHeadings(self, state: bool) -> None: """Enable or disable adding an anchor before headings.""" self._linkHeadings = state - return def setBodyText(self, state: bool) -> None: """Include body text in build.""" self._doBodyText = state - return def setCommentType(self, comment: nwComment, state: bool) -> None: """Toggle the inclusion og certain comment types.""" @@ -404,22 +387,18 @@ class Tokenizer(ABC): self._doComments.add(comment) else: self._doComments.discard(comment) - return def setKeywords(self, state: bool) -> None: """Include keywords in build.""" self._doKeywords = state - return def setIgnoredKeywords(self, keywords: str) -> None: """Comma separated string of keywords to ignore.""" self._skipKeywords = set(x.lower().strip() for x in keywords.split(",")) - return def setKeepLineBreaks(self, state: bool) -> None: """Keep line breaks in paragraphs.""" self._keepBreaks = state - return ## # Class Methods @@ -447,12 +426,10 @@ class Tokenizer(ABC): self._classes["tag"] = self._theme.tag self._classes["keyword"] = self._theme.keyword self._classes["optional"] = self._theme.optional - return def setBreakNext(self) -> None: """Set a page break for next block.""" self._breakNext = True - return def addRootHeading(self, tHandle: str) -> None: """Add a heading at the start of a new root folder.""" @@ -478,8 +455,6 @@ class Tokenizer(ABC): if self._keepRaw: self._raw.append(f"#! {title}\n\n") - return - def setText(self, tHandle: str, text: str | None = None) -> None: """Set the text for the tokenizer from a handle. If text is not set, it's is loaded from the file. @@ -490,7 +465,6 @@ class Tokenizer(ABC): self._text = text or self._project.storage.getDocumentText(tHandle) self._handle = tHandle self._isNovel = nwItem.itemLayout == nwItemLayout.DOCUMENT - return def doPreProcessing(self) -> None: """Run pre-processing jobs before the text is tokenized.""" @@ -499,7 +473,6 @@ class Tokenizer(ABC): replace = {f"<{k}>": v for k, v in entry.items()} rxRep = re.compile("|".join([re.escape(k) for k in replace]), flags=re.DOTALL) self._text = rxRep.sub(lambda x: replace[x.group(0)], self._text) - return def tokenizeText(self) -> None: """Scan the text for either lines starting with specific @@ -577,13 +550,13 @@ class Tokenizer(ABC): self._breakNext = True continue - elif sLine == "[vspace]": + if sLine == "[vspace]": tBlocks.append( (BlockTyp.SKIP, "", "", [], tStyle) ) continue - elif sLine.startswith("[vspace:") and sLine.endswith("]"): + if sLine.startswith("[vspace:") and sLine.endswith("]"): nSkip = checkInt(sLine[8:-1], 0) if nSkip >= 1: tBlocks.append( @@ -618,7 +591,7 @@ class Tokenizer(ABC): bStyle = COMMENT_STYLE[cStyle] tLine, tFmt = self._formatComment(bStyle, cKey, cText) tBlocks.append(( - BlockTyp.COMMENT, "", tLine, tFmt, tStyle + COMMENT_TYPE[cStyle], "", tLine, tFmt, tStyle )) elif cStyle == nwComment.FOOTNOTE: @@ -862,12 +835,12 @@ class Tokenizer(ABC): # We don't need to keep the empty lines after this pass pass - elif cBlock[0] == BlockTyp.KEYWORD: - # Adjust margins for lines in a list of keyword lines + elif cBlock[0] in (BlockTyp.KEYWORD, BlockTyp.NOTE): + # Adjust margins for lines in repeated meta blocks aStyle = cBlock[4] - if pBlock[0] == BlockTyp.KEYWORD: + if pBlock[0] == cBlock[0]: aStyle |= BlockFmt.Z_TOP - if nBlock[0] == BlockTyp.KEYWORD: + if nBlock[0] == cBlock[0]: aStyle |= BlockFmt.Z_BTM sBlocks.append(( cBlock[0], cBlock[1], cBlock[2], cBlock[3], aStyle @@ -949,8 +922,6 @@ class Tokenizer(ABC): text = tText.replace(nwHeadFmt.BR, " ").replace("&", "&") self._outline[tKey] = f"{prefix}|{text}" - return - def countStats(self) -> None: """Count stats on the tokenized text.""" titleCount = self._counts.get(nwStats.TITLES, 0) @@ -991,7 +962,7 @@ class Tokenizer(ABC): allWordChars += nPWChars textWordChars += nPWChars - elif tType in HEADINGS: + elif tType in HEADING_BLOCKS: titleCount += 1 allWords += nWords titleWords += nWords @@ -1005,7 +976,7 @@ class Tokenizer(ABC): allChars += nChars allWordChars += nWChars - elif tType in (BlockTyp.COMMENT, BlockTyp.KEYWORD): + elif tType in META_BLOCKS: words = tText.split() allWords += len(words) allChars += len(tText) @@ -1026,8 +997,6 @@ class Tokenizer(ABC): self._counts[nwStats.WCHARS_TEXT] = textWordChars self._counts[nwStats.WCHARS_TITLE] = titleWordChars - return - ## # Internal Functions ## @@ -1169,6 +1138,12 @@ class Tokenizer(ABC): class HeadingFormatter: + """Core: Format Text Headings. + + This class holds the various chapter and scene counters and can + apply the Build Settings header format settings based on internal + counter state. + """ def __init__( self, @@ -1182,35 +1157,29 @@ class HeadingFormatter: self._chapter = chapter self._scene = scene self._absolute = absolute - return def setHandle(self, tHandle: str | None) -> None: """Set the handle currently being processed.""" self._handle = tHandle - return def incChapter(self) -> None: """Increment the chapter counter.""" self._chapter += 1 - return def incScene(self) -> None: """Increment the scene counters.""" self._scene += 1 self._absolute += 1 - return def resetAll(self) -> None: """Reset all counters.""" self._chapter = 0 self._scene = 0 self._absolute = 0 - return def resetScene(self) -> None: """Reset the chapter scene counter.""" self._scene = 0 - return def apply(self, hFormat: str, text: str, nHead: int) -> str: """Apply formatting to a specific heading.""" diff --git a/novelwriter/formats/tomarkdown.py b/novelwriter/formats/tomarkdown.py index b87d0325..37391b4b 100644 --- a/novelwriter/formats/tomarkdown.py +++ b/novelwriter/formats/tomarkdown.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -29,7 +29,7 @@ from typing import TYPE_CHECKING from novelwriter.constants import nwUnicode from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt -from novelwriter.formats.tokenizer import Tokenizer +from novelwriter.formats.tokenizer import COMMENT_BLOCKS, Tokenizer if TYPE_CHECKING: from pathlib import Path @@ -79,7 +79,7 @@ EXT_MD = { class ToMarkdown(Tokenizer): - """Core: Markdown Document Writer + """Core: Markdown Document Writer. Extend the Tokenizer class to writer Markdown output. It supports both Standard Markdown and Extended Markdown. The class also @@ -91,7 +91,6 @@ class ToMarkdown(Tokenizer): self._extended = extended self._usedNotes: dict[str, int] = {} self._usedFields: list[tuple[int, str]] = [] - return ## # Class Methods @@ -144,7 +143,7 @@ class ToMarkdown(Tokenizer): elif tType == BlockTyp.SKIP: lines.append(f"{cSkip}\n\n") - elif tType == BlockTyp.COMMENT: + elif tType in COMMENT_BLOCKS: lines.append(f"{self._formatText(tText, tFormat, mTags)}\n\n") elif tType == BlockTyp.KEYWORD: @@ -153,8 +152,6 @@ class ToMarkdown(Tokenizer): self._pages.append("".join(lines)) - return - def closeDocument(self) -> None: """Run close document tasks.""" # Replace fields if there are stats available @@ -181,20 +178,16 @@ class ToMarkdown(Tokenizer): lines.append("\n") self._pages.append("".join(lines)) - return - def saveDocument(self, path: Path) -> None: """Save the data to a plain text file.""" with open(path, mode="w", encoding="utf-8") as outFile: outFile.write("".join(self._pages)) logger.info("Wrote file: %s", path) - return def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None: """Replace tabs with spaces.""" spaces = spaceChar*nSpaces self._pages = [p.replace("\t", spaces) for p in self._pages] - return ## # Internal Functions diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py index 15622845..0bd00189 100644 --- a/novelwriter/formats/toodt.py +++ b/novelwriter/formats/toodt.py @@ -23,7 +23,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -40,7 +40,7 @@ from novelwriter import __version__ from novelwriter.common import xmlElement, xmlIndent, xmlSubElem from novelwriter.constants import nwHeadFmt, nwStyles from novelwriter.formats.shared import BlockFmt, BlockTyp, TextFmt, stripEscape -from novelwriter.formats.tokenizer import Tokenizer +from novelwriter.formats.tokenizer import COMMENT_BLOCKS, Tokenizer from novelwriter.types import FONT_STYLE, QtHexRgb if TYPE_CHECKING: @@ -128,7 +128,7 @@ FONT_WEIGHT_MAP = {"400": "normal", "700": "bold"} class ToOdt(Tokenizer): - """Core: Open Document Writer + """Core: Open Document Writer. Extend the Tokenizer class to writer Open Document files. The output should conform to the 1.3 Extended standard. @@ -189,8 +189,6 @@ class ToOdt(Tokenizer): self._mDocLeft = "2.000cm" self._mDocRight = "2.000cm" - return - ## # Setters ## @@ -205,20 +203,18 @@ class ToOdt(Tokenizer): self._mDocBtm = f"{bottom/10.0:.3f}cm" self._mDocLeft = f"{left/10.0:.3f}cm" self._mDocRight = f"{right/10.0:.3f}cm" - return def setHeaderFormat(self, value: str, offset: int) -> None: """Set the document header format.""" self._headerFormat = value.strip() self._pageOffset = offset - return ## # Class Methods ## def initDocument(self) -> None: - """Initialises a new open document XML tree.""" + """Initialise a new open document XML tree.""" super().initDocument() # Initialise Variables @@ -325,8 +321,6 @@ class ToOdt(Tokenizer): self._useableStyles() self._writeHeader() - return - def doConvert(self) -> None: """Convert the list of text tokens into XML elements.""" xText = self._xText @@ -389,14 +383,12 @@ class ToOdt(Tokenizer): elif tType == BlockTyp.SKIP: self._addTextPar(xText, S_TEXT, oStyle, "") - elif tType == BlockTyp.COMMENT: + elif tType in COMMENT_BLOCKS: self._addTextPar(xText, S_META, oStyle, tText, tFmt=tFormat) elif tType == BlockTyp.KEYWORD: self._addTextPar(xText, S_META, oStyle, tText, tFmt=tFormat) - return - def closeDocument(self) -> None: """Add additional collected information to the XML.""" for style in self._autoPara.values(): @@ -412,7 +404,6 @@ class ToOdt(Tokenizer): _mkTag("text", "name"): f"Manuscript{key[:1].upper()}{key[1:]}", }) self._xText.insert(0, xFields) - return def saveDocument(self, path: Path) -> None: """Save the data to an .fodt or .odt file.""" @@ -456,8 +447,6 @@ class ToOdt(Tokenizer): logger.info("Wrote file: %s", path) - return - ## # Internal Functions ## @@ -670,7 +659,7 @@ class ToOdt(Tokenizer): return None def _emToCm(self, value: float) -> str: - """Converts an em value to centimetres.""" + """Convert an em value to centimetres.""" return f"{value*self._fontSize*2.54/72.0:.3f}cm" def _emToPt(self, scale: float) -> str: @@ -705,8 +694,6 @@ class ToOdt(Tokenizer): _mkTag("fo", "margin-bottom"): self._emToCm(0.5), }) - return - def _defaultStyles(self) -> None: """Set the default styles.""" hScale = self._scaleHeads @@ -783,8 +770,6 @@ class ToOdt(Tokenizer): _mkTag("number", "min-integer-digits"): "1", }) - return - def _useableStyles(self) -> None: """Set the usable styles.""" hScale = self._scaleHeads @@ -954,8 +939,6 @@ class ToOdt(Tokenizer): style.packXML(self._xStyl) self._mainPara[style.name] = style - return - def _writeHeader(self) -> None: """Write the header elements.""" xPage = ET.SubElement(self._xMast, _mkTag("style", "master-page"), attrib={ @@ -993,8 +976,6 @@ class ToOdt(Tokenizer): _mkTag("text", "style-name"): "Header" }) - return - # Auto-Style Classes # ================== @@ -1004,6 +985,7 @@ class ODTParagraphStyle: exporter. Only the used settings are exposed here to keep the class minimal and fast. """ + VALID_ALIGN: Final[list[str]] = ["start", "center", "end", "justify", "left", "right"] VALID_BREAK: Final[list[str]] = ["auto", "page", "even-page", "odd-page", "inherit"] VALID_LEVEL: Final[list[str]] = ["1", "2", "3", "4"] @@ -1046,8 +1028,6 @@ class ODTParagraphStyle: "opacity": ["loext", None], } - return - @property def name(self) -> str: return self._name @@ -1059,7 +1039,6 @@ class ODTParagraphStyle: def setName(self, name: str) -> None: """Set the paragraph style name.""" self._name = name - return ## # Attribute Setters @@ -1068,17 +1047,14 @@ class ODTParagraphStyle: def setDisplayName(self, value: str | None) -> None: """Set style display name.""" self._mAttr["display-name"][1] = value - return def setParentStyleName(self, value: str | None) -> None: """Set parent style name.""" self._mAttr["parent-style-name"][1] = value - return def setNextStyleName(self, value: str | None) -> None: """Set next style name.""" self._mAttr["next-style-name"][1] = value - return def setOutlineLevel(self, value: str | None) -> None: """Set paragraph outline level.""" @@ -1086,7 +1062,6 @@ class ODTParagraphStyle: self._mAttr["default-outline-level"][1] = value else: self._mAttr["default-outline-level"][1] = None - return def setClass(self, value: str | None) -> None: """Set paragraph class.""" @@ -1094,7 +1069,6 @@ class ODTParagraphStyle: self._mAttr["class"][1] = value else: self._mAttr["class"][1] = None - return ## # Paragraph Setters @@ -1103,32 +1077,26 @@ class ODTParagraphStyle: def setMarginTop(self, value: str | None) -> None: """Set paragraph top margin.""" self._pAttr["margin-top"][1] = value - return def setMarginBottom(self, value: str | None) -> None: """Set paragraph bottom margin.""" self._pAttr["margin-bottom"][1] = value - return def setMarginLeft(self, value: str | None) -> None: """Set paragraph left margin.""" self._pAttr["margin-left"][1] = value - return def setMarginRight(self, value: str | None) -> None: """Set paragraph right margin.""" self._pAttr["margin-right"][1] = value - return def setTextIndent(self, value: str | None) -> None: """Set text indentation.""" self._pAttr["text-indent"][1] = value - return def setLineHeight(self, value: str | None) -> None: """Set line height.""" self._pAttr["line-height"][1] = value - return def setTextAlign(self, value: str | None) -> None: """Set paragraph text alignment.""" @@ -1136,7 +1104,6 @@ class ODTParagraphStyle: self._pAttr["text-align"][1] = value else: self._pAttr["text-align"][1] = None - return def setBreakBefore(self, value: str | None) -> None: """Set page break before policy.""" @@ -1144,7 +1111,6 @@ class ODTParagraphStyle: self._pAttr["break-before"][1] = value else: self._pAttr["break-before"][1] = None - return def setBreakAfter(self, value: str | None) -> None: """Set page break after policy.""" @@ -1152,7 +1118,6 @@ class ODTParagraphStyle: self._pAttr["break-after"][1] = value else: self._pAttr["break-after"][1] = None - return ## # Text Setters @@ -1161,17 +1126,14 @@ class ODTParagraphStyle: def setFontName(self, value: str | None) -> None: """Set font name.""" self._tAttr["font-name"][1] = value - return def setFontFamily(self, value: str | None) -> None: """Set font family.""" self._tAttr["font-family"][1] = value - return def setFontSize(self, value: str | None) -> None: """Set font size.""" self._tAttr["font-size"][1] = value - return def setFontWeight(self, value: str | None) -> None: """Set font weight.""" @@ -1179,7 +1141,6 @@ class ODTParagraphStyle: self._tAttr["font-weight"][1] = value else: self._tAttr["font-weight"][1] = None - return def setColor(self, value: QColor | None) -> None: """Set text colour.""" @@ -1189,7 +1150,6 @@ class ODTParagraphStyle: else: self._tAttr["color"][1] = None self._tAttr["opacity"][1] = None - return ## # Methods @@ -1233,14 +1193,13 @@ class ODTParagraphStyle: if attr := {_mkTag(n, m): v for m, (n, v) in self._tAttr.items() if v}: ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr) - return - class ODTTextStyle: """Wrapper class for the text style setting used by the exporter. Only the used settings are exposed here to keep the class minimal and fast. """ + VALID_WEIGHT: Final[list[str]] = ["normal", "bold", *FONT_WEIGHT_NUM] VALID_STYLE: Final[list[str]] = ["normal", "italic", "oblique"] VALID_POS: Final[list[str]] = ["super", "sub"] @@ -1263,7 +1222,6 @@ class ODTTextStyle: "text-underline-width": ["style", None], "text-underline-color": ["style", None], } - return @property def name(self) -> str: @@ -1279,7 +1237,6 @@ class ODTTextStyle: self._tAttr["font-weight"][1] = value else: self._tAttr["font-weight"][1] = None - return def setFontStyle(self, value: str | None) -> None: """Set text font style.""" @@ -1287,7 +1244,6 @@ class ODTTextStyle: self._tAttr["font-style"][1] = value else: self._tAttr["font-style"][1] = None - return def setColor(self, value: QColor | None) -> None: """Set text colour.""" @@ -1295,7 +1251,6 @@ class ODTTextStyle: self._tAttr["color"][1] = value.name(QtHexRgb) else: self._tAttr["color"][1] = None - return def setBackgroundColor(self, value: QColor | None) -> None: """Set text background colour.""" @@ -1303,7 +1258,6 @@ class ODTTextStyle: self._tAttr["background-color"][1] = value.name(QtHexRgb) else: self._tAttr["background-color"][1] = None - return def setTextPosition(self, value: str | None) -> None: """Set text vertical position.""" @@ -1311,7 +1265,6 @@ class ODTTextStyle: self._tAttr["text-position"][1] = f"{value} 58%" else: self._tAttr["text-position"][1] = None - return def setStrikeStyle(self, value: str | None) -> None: """Set text line-trough style.""" @@ -1319,7 +1272,6 @@ class ODTTextStyle: self._tAttr["text-line-through-style"][1] = value else: self._tAttr["text-line-through-style"][1] = None - return def setStrikeType(self, value: str | None) -> None: """Set text line-through type.""" @@ -1327,7 +1279,6 @@ class ODTTextStyle: self._tAttr["text-line-through-type"][1] = value else: self._tAttr["text-line-through-type"][1] = None - return def setUnderlineStyle(self, value: str | None) -> None: """Set text underline style.""" @@ -1335,7 +1286,6 @@ class ODTTextStyle: self._tAttr["text-underline-style"][1] = value else: self._tAttr["text-underline-style"][1] = None - return def setUnderlineWidth(self, value: str | None) -> None: """Set text underline width.""" @@ -1343,7 +1293,6 @@ class ODTTextStyle: self._tAttr["text-underline-width"][1] = value else: self._tAttr["text-underline-width"][1] = None - return def setUnderlineColor(self, value: str | None) -> None: """Set text underline colour.""" @@ -1351,7 +1300,6 @@ class ODTTextStyle: self._tAttr["text-underline-color"][1] = value else: self._tAttr["text-underline-color"][1] = None - return ## # Methods @@ -1365,7 +1313,6 @@ class ODTTextStyle: }) if attr := {_mkTag(n, m): v for m, (n, v) in self._tAttr.items() if v}: ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr) - return # XML Complex Element Helper Class @@ -1378,7 +1325,9 @@ X_SPAN_SING = 3 class XMLParagraph: - """This is a helper class to manage the text content of a single + """ODT Text Paragraph. + + This is a helper class to manage the text content of a single XML element using mixed content tags. Rules: @@ -1408,8 +1357,6 @@ class XMLParagraph: self._rawTxt = "" self._xRoot.text = "" - return - def appendText(self, text: str) -> None: """Append text to the XML element. We do this one character at the time in order to be able to process line breaks, tabs and @@ -1424,7 +1371,7 @@ class XMLParagraph: if c == " ": nSpaces += 1 continue - elif nSpaces > 0: + if nSpaces > 0: self._processSpaces(nSpaces) nSpaces = 0 @@ -1468,8 +1415,6 @@ class XMLParagraph: # Handle trailing spaces self._processSpaces(nSpaces) - return - def appendSpan(self, text: str, style: str, link: str) -> None: """Append a text span to the XML element. The span is always closed since we do not produce nested spans (like Libre Office). @@ -1491,7 +1436,6 @@ class XMLParagraph: self._nState = X_SPAN_TEXT self.appendText(text) self._nState = X_ROOT_TAIL - return def appendNode(self, xNode: ET.Element | None) -> None: """Append an XML node to the paragraph. We only check for the @@ -1504,7 +1448,6 @@ class XMLParagraph: self._xTail = xNode self._xTail.tail = "" self._nState = X_ROOT_TAIL - return def checkError(self) -> tuple[int, str]: """Check that the number of characters written matches the @@ -1575,5 +1518,3 @@ class XMLParagraph: self._xSing.tail = "" self._nState = X_SPAN_SING self._chrPos += nSpaces - 1 - - return diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py index 69c25fa4..b53eb60c 100644 --- a/novelwriter/formats/toqdoc.py +++ b/novelwriter/formats/toqdoc.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -37,7 +37,7 @@ from PyQt6.QtPrintSupport import QPrinter from novelwriter import __version__ from novelwriter.constants import nwStyles, nwUnicode from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape -from novelwriter.formats.tokenizer import HEADINGS, Tokenizer +from novelwriter.formats.tokenizer import HEADING_BLOCKS, META_BLOCKS, Tokenizer from novelwriter.types import ( QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight, QtKeepAnchor, QtMoveAnchor, QtPageBreakAfter, QtPageBreakAuto, @@ -56,6 +56,7 @@ T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat] def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None: + """Insert a new block if not at the beginning of the document.""" if cursor.position() > 0: cursor.insertBlock(bFmt) else: @@ -63,7 +64,7 @@ def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None: class ToQTextDocument(Tokenizer): - """Core: QTextDocument Writer + """Core: QTextDocument Writer. Extend the Tokenizer class to generate a QTextDocument output. This is intended for usage in the document viewer and build tool preview. @@ -92,8 +93,6 @@ class ToQTextDocument(Tokenizer): self._pageSize = QPageSize(QPageSize.PageSizeId.A4) self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0) - return - ## # Properties ## @@ -113,17 +112,14 @@ class ToQTextDocument(Tokenizer): """Set the document page size and margins in millimetres.""" self._pageSize = QPageSize(QSizeF(width, height), QPageSize.Unit.Millimeter) self._pageMargins = QMarginsF(left, top, right, bottom) - return def setShowNewPage(self, state: bool) -> None: """Add markers for page breaks.""" self._newPage = state - return def disableAnchors(self) -> None: """Disable anchors for when writing to file.""" self._anchors = False - return ## # Class Methods @@ -200,8 +196,6 @@ class ToQTextDocument(Tokenizer): self._init = True - return - def doConvert(self) -> None: """Write text tokens into the document.""" if not self._init: @@ -214,7 +208,7 @@ class ToQTextDocument(Tokenizer): for tType, tMeta, tText, tFormat, tStyle in self._blocks: bFmt = QTextBlockFormat(self._blockFmt) - if tType in (BlockTyp.COMMENT, BlockTyp.KEYWORD): + if tType in META_BLOCKS: bFmt.setTopMargin(self._mMeta[0]) bFmt.setBottomMargin(self._mMeta[1]) elif tType == BlockTyp.SEP: @@ -248,17 +242,21 @@ class ToQTextDocument(Tokenizer): if tStyle & BlockFmt.IND_T: bFmt.setTextIndent(self._tIndent) - if tType in (BlockTyp.TEXT, BlockTyp.COMMENT, BlockTyp.KEYWORD): + if tType == BlockTyp.TEXT: newBlock(cursor, bFmt) self._insertFragments(tText, tFormat, cursor, self._charFmt) - elif tType in HEADINGS: + elif tType in HEADING_BLOCKS: bFmt, cFmt = self._genHeadStyle(tType, tMeta, bFmt) for tPart in tText.split("\n"): newBlock(cursor, bFmt) cursor.insertText(tPart, cFmt) bFmt.setPageBreakPolicy(QtPageBreakAuto) + elif tType in META_BLOCKS: + newBlock(cursor, bFmt) + self._insertFragments(tText, tFormat, cursor, self._charFmt) + elif tType == BlockTyp.SEP: newBlock(cursor, bFmt) cursor.insertText(tText, self._charFmt) @@ -293,8 +291,6 @@ class ToQTextDocument(Tokenizer): self._document.setPageSize(printer.pageRect(QPrinter.Unit.DevicePixel).size()) self._document.print(printer) - return - def closeDocument(self) -> None: """Run close document tasks.""" self._document.blockSignals(True) @@ -329,8 +325,6 @@ class ToQTextDocument(Tokenizer): self._document.blockSignals(False) - return - ## # Internal Functions ## @@ -435,8 +429,6 @@ class ToQTextDocument(Tokenizer): # Insert whatever is left in the buffer cursor.insertText(stripEscape(temp[start:]), cFmt) - return - def _insertNewPageMarker(self, cursor: QTextCursor) -> None: """Insert a new page marker.""" if self._newPage: @@ -471,8 +463,6 @@ class ToQTextDocument(Tokenizer): if root := self._document.rootFrame(): cursor.swap(root.lastCursorPosition()) - return - def _genHeadStyle(self, hType: BlockTyp, hKey: str, rFmt: QTextBlockFormat) -> T_TextStyle: """Generate a heading style set.""" mTop, mBottom = self._mHead.get(hType, (0.0, 0.0)) diff --git a/novelwriter/formats/toraw.py b/novelwriter/formats/toraw.py index c4ed979b..6eab8c52 100644 --- a/novelwriter/formats/toraw.py +++ b/novelwriter/formats/toraw.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import json @@ -41,7 +41,7 @@ logger = logging.getLogger(__name__) class ToRaw(Tokenizer): - """Core: Raw novelWriter Text Writer + """Core: Raw novelWriter Text Writer. A class that will collect the minimally altered original source text and write it to either a text or JSON file. @@ -51,7 +51,6 @@ class ToRaw(Tokenizer): super().__init__(project) self._keepRaw = True self._noTokens = True - return def doConvert(self) -> None: """No conversion to perform.""" @@ -86,10 +85,7 @@ class ToRaw(Tokenizer): logger.info("Wrote file: %s", path) - return - def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None: """Replace tabs with spaces.""" spaces = spaceChar*nSpaces self._raw = [p.replace("\t", spaces) for p in self._raw] - return diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index d93b9927..d35cb6b4 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -29,7 +29,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import bisect @@ -46,11 +46,11 @@ from PyQt6.QtGui import ( QAction, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QInputMethodEvent, QKeyEvent, QKeySequence, QMouseEvent, QPalette, QPixmap, QResizeEvent, QShortcut, QTextBlock, QTextCursor, QTextDocument, - QTextOption + QTextFormat, QTextOption ) from PyQt6.QtWidgets import ( QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu, - QPlainTextEdit, QToolBar, QVBoxLayout, QWidget + QPlainTextEdit, QTextEdit, QToolBar, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED @@ -77,7 +77,7 @@ from novelwriter.types import ( QtAlignCenterTop, QtAlignJustify, QtAlignLeft, QtAlignLeftTop, QtAlignRight, QtImCursorRectangle, QtKeepAnchor, QtModCtrl, QtModNone, QtModShift, QtMouseLeft, QtMoveAnchor, QtMoveLeft, QtMoveRight, - QtScrollAlwaysOff, QtScrollAsNeeded + QtScrollAlwaysOff, QtScrollAsNeeded, QtTransparent ) logger = logging.getLogger(__name__) @@ -99,7 +99,7 @@ class _TagAction(IntFlag): class GuiDocEditor(QPlainTextEdit): - """Gui Widget: Main Document Editor""" + """Gui Widget: Main Document Editor.""" __slots__ = ( "_autoReplace", "_completer", "_doReplace", "_docChanged", "_docHandle", "_followTag1", @@ -147,6 +147,8 @@ class GuiDocEditor(QPlainTextEdit): self._lastActive = 0.0 # Timestamp of last activity self._lastFind = None # Position of the last found search word self._doReplace = False # Switch to temporarily disable auto-replace + self._lineColor = QtTransparent + self._selection = QTextEdit.ExtraSelection() # Auto-Replace self._autoReplace = TextAutoReplace() @@ -235,8 +237,6 @@ class GuiDocEditor(QPlainTextEdit): logger.debug("Ready: GuiDocEditor") - return - ## # Properties ## @@ -285,18 +285,16 @@ class GuiDocEditor(QPlainTextEdit): self.docHeader.clearHeader() self.docFooter.setHandle(self._docHandle) self.docToolBar.setVisible(False) + self.setExtraSelections([]) self.itemHandleChanged.emit("") - return - def updateTheme(self) -> None: """Update theme elements.""" self.docSearch.updateTheme() self.docHeader.updateTheme() self.docFooter.updateTheme() self.docToolBar.updateTheme() - return def updateSyntaxColors(self) -> None: """Update the syntax highlighting theme.""" @@ -317,7 +315,9 @@ class GuiDocEditor(QPlainTextEdit): self.docHeader.matchColors() self.docFooter.matchColors() - return + self._lineColor = syntax.line + self._selection.format.setBackground(self._lineColor) + self._selection.format.setProperty(QTextFormat.Property.FullWidthSelection, True) def initEditor(self) -> None: """Initialise or re-initialise the editor with the user's @@ -374,6 +374,8 @@ class GuiDocEditor(QPlainTextEdit): # Refresh sizes self.setTabStopDistance(CONFIG.tabWidth) self.setCursorWidth(CONFIG.cursorWidth) + self.setExtraSelections([]) + self._cursorMoved() # If we have a document open, we should refresh it in case the # font changed, otherwise we just clear the editor entirely, @@ -384,8 +386,6 @@ class GuiDocEditor(QPlainTextEdit): else: self.clearEditor() - return - def loadText(self, tHandle: str, tLine: int | None = None) -> bool: """Load text from a document into the editor. If we have an I/O error, we must handle this and clear the editor so that we don't @@ -463,7 +463,6 @@ class GuiDocEditor(QPlainTextEdit): self.updateDocMargins() self.setDocumentChanged(True) QApplication.restoreOverrideCursor() - return def saveText(self) -> bool: """Save the text currently in the editor to the NWDocument @@ -531,7 +530,6 @@ class GuiDocEditor(QPlainTextEdit): vBar.setValue(vBar.value() + 1) count += 1 QApplication.processEvents() - return def updateDocMargins(self) -> None: """Automatically adjust the margins so the text is centred if @@ -572,8 +570,6 @@ class GuiDocEditor(QPlainTextEdit): lM = max(self._vpMargin, fH) self.setViewportMargins(tM, uM, tM, lM) - return - ## # Getters ## @@ -583,20 +579,19 @@ class GuiDocEditor(QPlainTextEdit): QTextDocument->toRawText instead of toPlainText. The former preserves non-breaking spaces, the latter does not. We still want to get rid of paragraph and line separators though. + See: https://doc.qt.io/qt-6/qtextdocument.html#toPlainText """ text = self._qDocument.toRawText() text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators - text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators - return text + return text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators def getSelectedText(self) -> str: """Get currently selected text.""" if (cursor := self.textCursor()).hasSelection(): text = cursor.selectedText() text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators - text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators - return text + return text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators return "" def getCursorPosition(self) -> int: @@ -617,7 +612,6 @@ class GuiDocEditor(QPlainTextEdit): logger.debug("Document changed status is '%s'", state) self._docChanged = state self.editedStatusChanged.emit(self._docChanged) - return def setCursorPosition(self, position: int) -> None: """Move the cursor to a given position in the document.""" @@ -626,14 +620,12 @@ class GuiDocEditor(QPlainTextEdit): cursor.setPosition(minmax(position, 0, chars-1)) self.setTextCursor(cursor) self.centerCursor() - return def saveCursorPosition(self) -> None: """Save the cursor position to the current project item.""" if self._nwItem is not None: cursPos = self.getCursorPosition() self._nwItem.setCursorPos(cursPos) - return def setCursorLine(self, line: int | None) -> None: """Move the cursor to a given line in the document.""" @@ -642,7 +634,6 @@ class GuiDocEditor(QPlainTextEdit): if block: self.setCursorPosition(block.position()) logger.debug("Cursor moved to line %d", line) - return def setCursorSelection(self, start: int, length: int) -> None: """Make a text selection.""" @@ -651,14 +642,15 @@ class GuiDocEditor(QPlainTextEdit): cursor.setPosition(start, QtMoveAnchor) cursor.setPosition(start + length, QtKeepAnchor) self.setTextCursor(cursor) - return ## # Spell Checking ## def toggleSpellCheck(self, state: bool | None) -> None: - """This is the main spell check setting function, and this one + """Toggle spell checking. + + This is the main spell check setting function, and this one should call all other setSpellCheck functions in other classes. If the spell check state is not defined (None), then toggle the current status saved in this class. @@ -682,8 +674,6 @@ class GuiDocEditor(QPlainTextEdit): logger.debug("Spell check is set to '%s'", str(state)) - return - def spellCheckDocument(self) -> None: """Rerun the highlighter to update spell checking status of the currently loaded text. @@ -695,7 +685,6 @@ class GuiDocEditor(QPlainTextEdit): QApplication.restoreOverrideCursor() logger.debug("Document highlighted in %.3f ms", 1000*(time() - start)) self.updateStatusMessage.emit(self.tr("Spell check complete")) - return ## # General Class Methods @@ -735,6 +724,8 @@ class GuiDocEditor(QPlainTextEdit): self._toggleFormat(2, "*") elif action == nwDocAction.MD_STRIKE: self._toggleFormat(2, "~") + elif action == nwDocAction.MD_MARK: + self._toggleFormat(2, "=") elif action == nwDocAction.S_QUOTE: self._wrapSelection(CONFIG.fmtSQuoteOpen, CONFIG.fmtSQuoteClose) elif action == nwDocAction.D_QUOTE: @@ -822,7 +813,6 @@ class GuiDocEditor(QPlainTextEdit): details=self.tr("File Location: {0}").format(self._nwDocument.fileLocation), log=False ) - return def insertText(self, insert: str | nwDocInsert) -> None: """Insert a specific type of text at the cursor position.""" @@ -964,7 +954,6 @@ class GuiDocEditor(QPlainTextEdit): event.acceptProposedAction() else: super().dragEnterEvent(event) - return def dragMoveEvent(self, event: QDragMoveEvent) -> None: """Overload drag move event to handle dragged items.""" @@ -972,7 +961,6 @@ class GuiDocEditor(QPlainTextEdit): event.acceptProposedAction() else: super().dragMoveEvent(event) - return def dropEvent(self, event: QDropEvent) -> None: """Overload drop event to handle dragged items.""" @@ -982,7 +970,6 @@ class GuiDocEditor(QPlainTextEdit): self.openDocumentRequest.emit(handles[0], nwDocMode.EDIT, "", True) else: super().dropEvent(event) - return def focusNextPrevChild(self, _next: bool) -> bool: """Capture the focus request from the tab key on the text @@ -1009,7 +996,6 @@ class GuiDocEditor(QPlainTextEdit): else: self._processTag(cursor) super().mouseReleaseEvent(event) - return def resizeEvent(self, event: QResizeEvent) -> None: """If the text editor is resized, we must make sure the document @@ -1017,7 +1003,6 @@ class GuiDocEditor(QPlainTextEdit): """ self.updateDocMargins() super().resizeEvent(event) - return def inputMethodEvent(self, event: QInputMethodEvent) -> None: """Handle text being input from CJK input methods.""" @@ -1045,14 +1030,13 @@ class GuiDocEditor(QPlainTextEdit): @pyqtSlot(str, Enum) def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None: - """Called when an item label is changed to check if the document - title bar needs updating, + """Process project item change. Called when an item label is + changed to check if the document title bar needs updating. """ if tHandle == self._docHandle and change == nwChange.UPDATE: self.docHeader.setHandle(tHandle) self.docFooter.updateInfo() self.updateDocMargins() - return @pyqtSlot(str) def insertKeyWord(self, keyword: str) -> bool: @@ -1063,8 +1047,7 @@ class GuiDocEditor(QPlainTextEdit): logger.error("Invalid keyword '%s'", keyword) return False logger.debug("Inserting keyword '%s'", keyword) - state = self.insertNewBlock(f"{keyword}: ") - return state + return self.insertNewBlock(f"{keyword}: ") @pyqtSlot() def toggleSearch(self) -> None: @@ -1073,14 +1056,12 @@ class GuiDocEditor(QPlainTextEdit): self.closeSearch() else: self.beginSearch() - return @pyqtSlot(list, list) def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None: """Tags have changed, so just in case we rehighlight them.""" if updated or deleted: self._qDocument.syntaxHighlighter.rehighlightByType(BLOCK_META) - return ## # Private Slots @@ -1122,13 +1103,14 @@ class GuiDocEditor(QPlainTextEdit): if self._autoReplace.process(text, cursor): self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block()) - return - @pyqtSlot() def _cursorMoved(self) -> None: """Triggered when the cursor moved in the editor.""" self.docFooter.updateLineCount(self.textCursor()) - return + if CONFIG.lineHighlight: + self._selection.cursor = self.textCursor() + self._selection.cursor.clearSelection() + self.setExtraSelections([self._selection]) @pyqtSlot(int, int, str) def _insertCompletion(self, pos: int, length: int, text: str) -> None: @@ -1140,13 +1122,11 @@ class GuiDocEditor(QPlainTextEdit): cursor.setPosition(check + length, QtKeepAnchor) cursor.insertText(text) self._completer.close() - return @pyqtSlot() def _openContextFromCursor(self) -> None: """Open the spell check context menu at the cursor.""" self._openContextMenu(self.cursorRect().center()) - return @pyqtSlot("QPoint") def _openContextMenu(self, pos: QPoint) -> None: @@ -1204,13 +1184,13 @@ class GuiDocEditor(QPlainTextEdit): # Spell Checking if SHARED.project.data.spellCheck: - word, cPos, cLen, suggest = self._qDocument.spellErrorAtPos(pCursor.position()) - if word and cPos >= 0 and cLen > 0: + word, offset, suggest = self._qDocument.spellErrorAtPos(pCursor.position()) + if word and offset >= 0: logger.debug("Word '%s' is misspelled", word) block = pCursor.block() sCursor = self.textCursor() - sCursor.setPosition(block.position() + cPos) - sCursor.movePosition(QtMoveRight, QtKeepAnchor, cLen) + sCursor.setPosition(block.position() + offset) + sCursor.movePosition(QtMoveRight, QtKeepAnchor, len(word)) if suggest: ctxMenu.addSeparator() qtAddAction(ctxMenu, self.tr("Spelling Suggestion(s)")) @@ -1233,8 +1213,6 @@ class GuiDocEditor(QPlainTextEdit): ctxMenu.setParent(None) - return - @pyqtSlot() def _runDocumentTasks(self) -> None: """Run timer document tasks.""" @@ -1271,11 +1249,10 @@ class GuiDocEditor(QPlainTextEdit): if not self.textCursor().hasSelection(): # Selection counter should take precedence (#2155) self.docFooter.updateMainCount(mCount, False) - return @pyqtSlot() def _updateSelectedStatus(self) -> None: - """The user made a change in text selection. Forward this + """Process user change in text selection. Forward this information to the footer, and start the selection word counter. """ if self.textCursor().hasSelection(): @@ -1284,7 +1261,6 @@ class GuiDocEditor(QPlainTextEdit): else: self._timerSel.stop() self.docFooter.updateMainCount(0, False) - return @pyqtSlot() def _runSelCounter(self) -> None: @@ -1302,14 +1278,12 @@ class GuiDocEditor(QPlainTextEdit): if self._docHandle and self._nwItem: self.docFooter.updateMainCount(cCount if CONFIG.useCharCount else wCount, True) self._timerSel.stop() - return @pyqtSlot() def _closeCurrentDocument(self) -> None: """Close the document. Forwarded to the main Gui.""" self.closeEditorRequest.emit() self.docToolBar.setVisible(False) - return @pyqtSlot() def _toggleToolBarVisibility(self) -> None: @@ -1317,7 +1291,6 @@ class GuiDocEditor(QPlainTextEdit): state = not self.docToolBar.isVisible() self.docToolBar.setVisible(state) CONFIG.showEditToolBar = state - return ## # Search & Replace @@ -1328,14 +1301,12 @@ class GuiDocEditor(QPlainTextEdit): self.docSearch.setSearchText(self.getSelectedText() or None) resS, _ = self.findAllOccurences() self.docSearch.setResultCount(None, len(resS)) - return def beginReplace(self) -> None: """Initialise the search box and reset the replace text box.""" self.beginSearch() self.docSearch.setReplaceText("") self.updateDocMargins() - return def findNext(self, goBack: bool = False) -> None: """Search for the next or previous occurrence of the search bar @@ -1623,8 +1594,6 @@ class GuiDocEditor(QPlainTextEdit): self.setTextCursor(cursor) - return - def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> None: """Replace all straight quotes in the selected text.""" cursor = self.textCursor() @@ -1884,8 +1853,6 @@ class GuiDocEditor(QPlainTextEdit): cursor.insertText(cleanText.rstrip() + "\n") cursor.endEditBlock() - return - def _insertCommentStructure(self, style: nwComment) -> None: """Insert a shortcut/comment combo.""" if self._docHandle and style == nwComment.FOOTNOTE: @@ -1933,7 +1900,6 @@ class GuiDocEditor(QPlainTextEdit): cursor.endEditBlock() cursor.setPosition(pos) self.setTextCursor(cursor) - return def _addWord(self, word: str, block: QTextBlock, save: bool) -> None: """Slot for the spell check context menu triggered when the user @@ -1942,7 +1908,6 @@ class GuiDocEditor(QPlainTextEdit): logger.debug("Added '%s' to project dictionary, %s", word, "saved" if save else "unsaved") SHARED.spelling.addWord(word, save=save) self._qDocument.syntaxHighlighter.rehighlightBlock(block) - return def _processTag( self, cursor: QTextCursor | None = None, follow: bool = True, create: bool = False @@ -2016,7 +1981,6 @@ class GuiDocEditor(QPlainTextEdit): if self._docHandle: text = block.text().lstrip("#").lstrip("!").strip() self.requestProjectItemRenamed.emit(self._docHandle, text) - return def _autoSelect(self) -> QTextCursor: """Return a cursor which may or may not have a selection based @@ -2086,14 +2050,11 @@ class GuiDocEditor(QPlainTextEdit): self.setTextCursor(cursor) - return - def _makePosSelection(self, mode: QTextCursor.SelectionType, pos: QPoint) -> None: """Select text based on selection mode, but first move cursor.""" cursor = self.cursorForPosition(pos) self.setTextCursor(cursor) self._makeSelection(mode) - return def _allowAutoReplace(self, state: bool) -> None: """Enable/disable the auto-replace feature temporarily.""" @@ -2101,11 +2062,10 @@ class GuiDocEditor(QPlainTextEdit): self._doReplace = CONFIG.doReplace else: self._doReplace = False - return class CommandCompleter(QMenu): - """GuiWidget: Command Completer Menu + """GuiWidget: Command Completer Menu. This is a context menu with options populated from the user's defined tags and keys. It also helps to type the meta data keyword @@ -2119,8 +2079,6 @@ class CommandCompleter(QMenu): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) - self._parent = parent - return def updateMetaText(self, text: str, pos: int) -> bool: """Update the menu options based on the line of text.""" @@ -2214,7 +2172,6 @@ class CommandCompleter(QMenu): else: self.close() # Close to release the event lock before forwarding the key press (#2510) self._parent.keyPressEvent(event) - return ## # Internal Functions @@ -2223,11 +2180,10 @@ class CommandCompleter(QMenu): def _emitComplete(self, pos: int, length: int, value: str) -> None: """Emit the signal to indicate a selection has been made.""" self.insertText.emit(pos, length, value) - return class BackgroundWordCounter(QRunnable): - """The Off-GUI Thread Word Counter + """The Off-GUI Thread Word Counter. A runnable for the word counter to be run in the thread pool off the main GUI thread. @@ -2239,9 +2195,9 @@ class BackgroundWordCounter(QRunnable): self._forSelection = forSelection self._isRunning = False self.signals = BackgroundWordCounterSignals() - return def isRunning(self) -> bool: + """Return True if the word counter is already running.""" return self._isRunning @pyqtSlot() @@ -2259,17 +2215,17 @@ class BackgroundWordCounter(QRunnable): self.signals.countsReady.emit(cC, wC, pC) self._isRunning = False - return - class BackgroundWordCounterSignals(QObject): """The QRunnable cannot emit a signal, so we need a simple QObject to hold the word counter signal. """ + countsReady = pyqtSignal(int, int, int) class TextAutoReplace: + """Encapsulates the editor auto replace feature.""" __slots__ = ( "_doPadAfter", "_doPadBefore", "_padAfter", "_padBefore", "_padChar", @@ -2279,7 +2235,6 @@ class TextAutoReplace: def __init__(self) -> None: self.initSettings() - return def initSettings(self) -> None: """Initialise the auto-replace settings from config.""" @@ -2298,27 +2253,28 @@ class TextAutoReplace: self._padAfter = CONFIG.fmtPadAfter self._doPadBefore = bool(CONFIG.fmtPadBefore) self._doPadAfter = bool(CONFIG.fmtPadAfter) - return def process(self, text: str, cursor: QTextCursor) -> bool: """Auto-replace text elements based on main configuration. Returns True if anything was changed. """ - pos = cursor.positionInBlock() - length = len(text) - if length < 1 or pos-1 > length: + aPos = cursor.position() + bPos = cursor.positionInBlock() + block = cursor.block() + length = block.length() - 1 + if length < 1 or bPos-1 > length: return False - delete, insert = self._determine(text, pos) - if insert == "": - return False + cursor.movePosition(QtMoveLeft, QtKeepAnchor, min(4, bPos)) + last = cursor.selectedText() + delete, insert = self._determine(last, bPos) check = insert if self._doPadBefore and check in self._padBefore: if not (check == ":" and length > 1 and text[0] == "@"): delete = max(delete, 1) - chkPos = pos - delete - 1 - if chkPos >= 0 and text[chkPos].isspace(): + chkPos = len(last) - delete - 1 + if chkPos >= 0 and last[chkPos].isspace(): # Strip existing space before inserting a new (#1061) delete += 1 insert = self._padChar + insert @@ -2329,6 +2285,7 @@ class TextAutoReplace: insert = insert + self._padChar if delete > 0: + cursor.setPosition(aPos) cursor.movePosition(QtMoveLeft, QtKeepAnchor, delete) cursor.insertText(insert) return True @@ -2337,42 +2294,55 @@ class TextAutoReplace: def _determine(self, text: str, pos: int) -> tuple[int, str]: """Determine what to replace, if anything.""" - t1 = text[pos-1:pos] - t2 = text[pos-2:pos] - t3 = text[pos-3:pos] - t4 = text[pos-4:pos] - if t1 == "": - # Return early if there is nothing to check - return 0, "" + t1 = text[-1:] + t2 = text[-2:] + t3 = text[-3:] + t4 = text[-4:] - leading = t2[:1].isspace() - if self._replaceDQuote: - if leading and t2.endswith('"'): + if self._replaceDQuote and t1 == '"': + # Process Double Quote + if pos == 1: return 1, self._quoteDO - elif t1 == '"': - if pos == 1: - return 1, self._quoteDO - elif pos == 2 and t2 == '>"': - return 1, self._quoteDO - elif pos == 3 and t3 == '>>"': - return 1, self._quoteDO - else: - return 1, self._quoteDC + elif t2[:1].isspace() and t2.endswith('"'): + return 1, self._quoteDO + elif pos == 2 and t2 == '>"': + return 1, self._quoteDO + elif pos == 3 and t3 == '>>"': + return 1, self._quoteDO + elif pos == 2 and t2 == '_"': + return 1, self._quoteDO + elif t3[:1].isspace() and t3.endswith('_"'): + return 1, self._quoteDO + elif pos == 3 and t3 in ('**"', '=="', '~~"'): + return 1, self._quoteDO + elif t4[:1].isspace() and t4.endswith(('**"', '=="', '~~"')): + return 1, self._quoteDO + else: + return 1, self._quoteDC - if self._replaceSQuote: - if leading and t2.endswith("'"): + if self._replaceSQuote and t1 == "'": + # Process Single Quote + if pos == 1: return 1, self._quoteSO - elif t1 == "'": - if pos == 1: - return 1, self._quoteSO - elif pos == 2 and t2 == ">'": - return 1, self._quoteSO - elif pos == 3 and t3 == ">>'": - return 1, self._quoteSO - else: - return 1, self._quoteSC + elif t2[:1].isspace() and t2.endswith("'"): + return 1, self._quoteSO + elif pos == 2 and t2 == ">'": + return 1, self._quoteSO + elif pos == 3 and t3 == ">>'": + return 1, self._quoteSO + elif pos == 2 and t2 == "_'": + return 1, self._quoteSO + elif t3[:1].isspace() and t3.endswith("_'"): + return 1, self._quoteSO + elif pos == 3 and t3 in ("**'", "=='", "~~'"): + return 1, self._quoteSO + elif t4[:1].isspace() and t4.endswith(("**'", "=='", "~~'")): + return 1, self._quoteSO + else: + return 1, self._quoteSC - if self._replaceDash: + if self._replaceDash and t1 == "-": + # Process Dashes if t4 == "----": return 4, "\u2015" # Horizontal bar elif t3 == "---": @@ -2385,6 +2355,7 @@ class TextAutoReplace: return 2, "\u2015" # Horizontal bar if self._replaceDots and t3 == "...": + # Process Dots return 3, "\u2026" # Ellipsis if t1 == "\u2028": # Line separator @@ -2395,7 +2366,7 @@ class TextAutoReplace: class GuiDocToolBar(QWidget): - """The Formatting and Options Fold Out Menu + """The Formatting and Options Fold Out Menu. Only used by DocEditor, and is opened by the first button in the header. @@ -2432,6 +2403,12 @@ class GuiDocToolBar(QWidget): qtLambda(self.requestDocAction.emit, nwDocAction.MD_STRIKE) ) + self.tbMarkMD = NIconToolButton(self, iSz) + self.tbMarkMD.setToolTip(self.tr("Markdown Highlight")) + self.tbMarkMD.clicked.connect( + qtLambda(self.requestDocAction.emit, nwDocAction.MD_MARK) + ) + self.tbBold = NIconToolButton(self, iSz) self.tbBold.setToolTip(self.tr("Shortcode Bold")) self.tbBold.clicked.connect( @@ -2481,6 +2458,7 @@ class GuiDocToolBar(QWidget): self.outerBox.addWidget(self.tbBoldMD) self.outerBox.addWidget(self.tbItalicMD) self.outerBox.addWidget(self.tbStrikeMD) + self.outerBox.addWidget(self.tbMarkMD) self.outerBox.addSpacing(4) self.outerBox.addWidget(self.tbBold) self.outerBox.addWidget(self.tbItalic) @@ -2500,8 +2478,6 @@ class GuiDocToolBar(QWidget): logger.debug("Ready: GuiDocToolBar") - return - def updateTheme(self) -> None: """Initialise GUI elements that depend on specific settings.""" syntax = SHARED.theme.syntaxTheme @@ -2515,6 +2491,7 @@ class GuiDocToolBar(QWidget): 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") @@ -2523,11 +2500,9 @@ class GuiDocToolBar(QWidget): self.tbSuperscript.setThemeIcon("fmt_superscript") self.tbSubscript.setThemeIcon("fmt_subscript") - return - class GuiDocEditSearch(QFrame): - """The Embedded Document Search/Replace Feature + """The Embedded Document Search/Replace Feature. Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport. @@ -2657,8 +2632,6 @@ class GuiDocEditSearch(QFrame): logger.debug("Ready: GuiDocEditSearch") - return - ## # Properties ## @@ -2707,14 +2680,12 @@ class GuiDocEditSearch(QFrame): self.searchBox.selectAll() if CONFIG.searchRegEx: self._alertSearchValid(True) - return def setReplaceText(self, text: str) -> None: """Set the replace text.""" self.showReplace.setChecked(True) self.replaceBox.setFocus() self.replaceBox.setText(text) - return def setResultCount(self, currRes: int | None, resCount: int | None) -> None: """Set the count values for the current search.""" @@ -2729,7 +2700,6 @@ class GuiDocEditSearch(QFrame): self.resultLabel.setMinimumWidth(minWidth) self.adjustSize() self.docEditor.updateDocMargins() - return ## # Methods @@ -2745,7 +2715,6 @@ class GuiDocEditSearch(QFrame): self.resultLabel.setMinimumWidth( SHARED.theme.getTextWidth("?/?", SHARED.theme.guiFontSmall) ) - return def updateTheme(self) -> None: """Update theme elements.""" @@ -2770,11 +2739,9 @@ class GuiDocEditSearch(QFrame): self.searchOpt.setStyleSheet("QToolBar {padding: 0;}") self.showReplace.setStyleSheet("QToolButton {border: none; background: transparent;}") - return - def cycleFocus(self) -> bool: - """The tab key just alternates focus between the two input - boxes, if the replace box is visible. + """Cycle focus on tab key press. This just alternates focus + between the two input boxes, if the replace box is visible. """ if self.searchBox.hasFocus(): self.replaceBox.setFocus() @@ -2799,7 +2766,6 @@ class GuiDocEditSearch(QFrame): self.setVisible(False) self.docEditor.updateDocMargins() self.docEditor.setFocus() - return ## # Private Slots @@ -2809,13 +2775,11 @@ class GuiDocEditSearch(QFrame): def _doSearch(self) -> None: """Call the search action function for the document editor.""" self.docEditor.findNext(goBack=(QApplication.keyboardModifiers() == QtModShift)) - return @pyqtSlot() def _doReplace(self) -> None: """Call the replace action function for the document editor.""" self.docEditor.replaceNext() - return @pyqtSlot(bool) def _doToggleReplace(self, state: bool) -> None: @@ -2824,43 +2788,36 @@ class GuiDocEditSearch(QFrame): self.replaceButton.setVisible(state) self.adjustSize() self.docEditor.updateDocMargins() - return @pyqtSlot(bool) def _doToggleCase(self, state: bool) -> None: """Enable/disable case sensitive mode.""" CONFIG.searchCase = state - return @pyqtSlot(bool) def _doToggleWord(self, state: bool) -> None: """Enable/disable whole word search mode.""" CONFIG.searchWord = state - return @pyqtSlot(bool) def _doToggleRegEx(self, state: bool) -> None: """Enable/disable regular expression search mode.""" CONFIG.searchRegEx = state - return @pyqtSlot(bool) def _doToggleLoop(self, state: bool) -> None: """Enable/disable looping the search.""" CONFIG.searchLoop = state - return @pyqtSlot(bool) def _doToggleProject(self, state: bool) -> None: """Enable/disable continuing search in next project file.""" CONFIG.searchNextFile = state - return @pyqtSlot(bool) def _doToggleMatchCap(self, state: bool) -> None: """Enable/disable preserving capitalisation when replacing.""" CONFIG.searchMatchCap = state - return ## # Internal Functions @@ -2876,11 +2833,10 @@ class GuiDocEditSearch(QFrame): palette.text().color() if isValid else SHARED.theme.errorText ) self.searchBox.setPalette(palette) - return class GuiDocEditHeader(QWidget): - """The Embedded Document Header + """The Embedded Document Header. Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport. @@ -2971,8 +2927,6 @@ class GuiDocEditHeader(QWidget): logger.debug("Ready: GuiDocEditHeader") - return - ## # Methods ## @@ -2989,7 +2943,6 @@ class GuiDocEditHeader(QWidget): self.searchButton.setVisible(False) self.closeButton.setVisible(False) self.minmaxButton.setVisible(False) - return def setOutline(self, data: dict[int, str]) -> None: """Set the document outline dataset.""" @@ -3001,13 +2954,11 @@ class GuiDocEditHeader(QWidget): action.triggered.connect(qtLambda(self._gotoBlock, number)) self._docOutline = data logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart)) - return def updateFont(self) -> None: """Update the font settings.""" self.setFont(SHARED.theme.guiFont) self.itemTitle.setFont(SHARED.theme.guiFontSmall) - return def updateTheme(self) -> None: """Update theme elements.""" @@ -3026,8 +2977,6 @@ class GuiDocEditHeader(QWidget): self.matchColors() - return - def matchColors(self) -> None: """Update the colours of the widget to match those of the syntax theme rather than the main GUI. @@ -3041,12 +2990,10 @@ class GuiDocEditHeader(QWidget): self.itemTitle.setTextColors( color=palette.windowText().color(), faded=SHARED.theme.fadedText ) - return def changeFocusState(self, state: bool) -> None: """Toggle focus state.""" self.itemTitle.setColorState(state) - return def setHandle(self, tHandle: str) -> None: """Set the document title from the handle, or alternatively, set @@ -3067,8 +3014,6 @@ class GuiDocEditHeader(QWidget): self.closeButton.setVisible(True) self.minmaxButton.setVisible(True) - return - ## # Private Slots ## @@ -3078,19 +3023,16 @@ class GuiDocEditHeader(QWidget): """Trigger the close editor on the main window.""" self.clearHeader() self.closeDocumentRequest.emit() - return @pyqtSlot(int) def _gotoBlock(self, blockNumber: int) -> None: """Move cursor to a specific heading.""" self.docEditor.setCursorLine(blockNumber + 1) - return @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") - return ## # Events @@ -3102,11 +3044,10 @@ class GuiDocEditHeader(QWidget): """ if event.button() == QtMouseLeft: self.docEditor.requestProjectItemSelected.emit(self._docHandle or "", True) - return class GuiDocEditFooter(QWidget): - """The Embedded Document Footer + """The Embedded Document Footer. Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport. @@ -3191,8 +3132,6 @@ class GuiDocEditFooter(QWidget): logger.debug("Ready: GuiDocEditFooter") - return - ## # Methods ## @@ -3202,7 +3141,6 @@ class GuiDocEditFooter(QWidget): self._trMainCount = trStats(nwLabels.STATS_DISPLAY[ nwStats.CHARS if CONFIG.useCharCount else nwStats.WORDS ]) - return def updateFont(self) -> None: """Update the font settings.""" @@ -3210,7 +3148,6 @@ class GuiDocEditFooter(QWidget): self.statusText.setFont(SHARED.theme.guiFontSmall) self.linesText.setFont(SHARED.theme.guiFontSmall) self.wordsText.setFont(SHARED.theme.guiFontSmall) - return def updateTheme(self) -> None: """Update theme elements.""" @@ -3218,7 +3155,6 @@ class GuiDocEditFooter(QWidget): self.linesIcon.setPixmap(SHARED.theme.getPixmap("lines", (iPx, iPx))) self.wordsIcon.setPixmap(SHARED.theme.getPixmap("stats", (iPx, iPx))) self.matchColors() - return def matchColors(self) -> None: """Update the colours of the widget to match those of the syntax @@ -3236,8 +3172,6 @@ class GuiDocEditFooter(QWidget): self.linesText.setPalette(palette) self.wordsText.setPalette(palette) - return - def setHandle(self, tHandle: str | None) -> None: """Set the handle that will populate the footer's data.""" self._docHandle = tHandle @@ -3250,8 +3184,6 @@ class GuiDocEditFooter(QWidget): self.updateInfo() self.updateMainCount(0, False) - return - def updateInfo(self) -> None: """Update the content of text labels.""" if self._tItem is None: @@ -3266,8 +3198,6 @@ class GuiDocEditFooter(QWidget): self.statusIcon.setPixmap(sIcon) self.statusText.setText(sText) - return - def updateLineCount(self, cursor: QTextCursor) -> None: """Update the line and document position counter.""" if document := cursor.document(): @@ -3277,7 +3207,6 @@ class GuiDocEditFooter(QWidget): self.linesText.setText( self._trLineCount.format(f"{cLine:n}", f"{100*cPos//cCount:d} %") ) - return def updateMainCount(self, count: int, selection: bool) -> None: """Update main counter information.""" @@ -3290,4 +3219,3 @@ class GuiDocEditFooter(QWidget): else: text = self._trMainCount.format("0", "+0") self.wordsText.setText(text) - return diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index 8103fd79..3cd1cb1d 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -36,11 +36,12 @@ from PyQt6.QtGui import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.common import checkInt +from novelwriter.common import checkInt, utf16CharMap from novelwriter.constants import nwStyles, nwUnicode from novelwriter.enum import nwComment from novelwriter.text.comments import processComment from novelwriter.text.patterns import REGEX_PATTERNS, DialogParser +from novelwriter.types import QtTextUserProperty logger = logging.getLogger(__name__) @@ -56,6 +57,7 @@ BLOCK_TITLE = 4 class GuiDocHighlighter(QSyntaxHighlighter): + """GUI: Editor Syntax Highlighter.""" __slots__ = ( "_cmnRules", "_dialogParser", "_hStyles", "_isInactive", "_isNovel", @@ -84,8 +86,6 @@ class GuiDocHighlighter(QSyntaxHighlighter): logger.debug("Ready: GuiDocHighlighter") - return - def initHighlighter(self) -> None: """Initialise the syntax highlighter, setting all the colour rules and building the RegExes. @@ -110,6 +110,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self._addCharFormat("bold", colEmph, "b") self._addCharFormat("italic", colEmph, "i") 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("altdialog", syntax.dialA) @@ -139,7 +140,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): # Multiple or Trailing Spaces if CONFIG.showMultiSpaces: - rxRule = re.compile(r"[ ]{2,}|[ ]*$", re.UNICODE) + rxRule = re.compile(r"\s{2,}") hlRule = { 0: self._hStyles["mspaces"], } @@ -148,7 +149,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self._cmnRules.append((rxRule, hlRule)) # Non-Breaking Spaces - rxRule = re.compile(f"[{nwUnicode.U_NBSP}{nwUnicode.U_THNBSP}]+", re.UNICODE) + rxRule = re.compile(f"[{nwUnicode.U_NBSP}{nwUnicode.U_THNBSP}]+") hlRule = { 0: self._hStyles["nobreak"], } @@ -196,6 +197,17 @@ class GuiDocHighlighter(QSyntaxHighlighter): self._txtRules.append((rxRule, hlRule)) self._cmnRules.append((rxRule, hlRule)) + # Markdown Highlight + rxRule = REGEX_PATTERNS.markdownMark + hlRule = { + 1: self._hStyles["markup"], + 2: self._hStyles["mark"], + 3: self._hStyles["markup"], + } + self._minRules.append((rxRule, hlRule)) + self._txtRules.append((rxRule, hlRule)) + self._cmnRules.append((rxRule, hlRule)) + # Shortcodes rxRule = REGEX_PATTERNS.shortcodePlain hlRule = { @@ -226,7 +238,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self._cmnRules.append((rxRule, hlRule)) # Alignment Tags - rxRule = re.compile(r"(^>{1,2}|<{1,2}$)", re.UNICODE) + rxRule = re.compile(r"(^>{1,2}|<{1,2}$)") hlRule = { 1: self._hStyles["markup"], } @@ -234,7 +246,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self._txtRules.append((rxRule, hlRule)) # Auto-Replace Tags - rxRule = re.compile(r"<(\S+?)>", re.UNICODE) + rxRule = re.compile(r"<(\S+?)>") hlRule = { 0: self._hStyles["replace"], } @@ -242,8 +254,6 @@ class GuiDocHighlighter(QSyntaxHighlighter): self._txtRules.append((rxRule, hlRule)) self._cmnRules.append((rxRule, hlRule)) - return - ## # Setters ## @@ -251,7 +261,6 @@ class GuiDocHighlighter(QSyntaxHighlighter): def setSpellCheck(self, state: bool) -> None: """Enable/disable the real time spell checker.""" self._spellCheck = state - return def setHandle(self, tHandle: str) -> None: """Set the handle of the currently highlighted document.""" @@ -262,7 +271,6 @@ class GuiDocHighlighter(QSyntaxHighlighter): self._isNovel = item.isDocumentLayout() self._isInactive = item.isInactiveClass() logger.debug("Syntax highlighter enabled for item '%s'", tHandle) - return ## # Methods @@ -280,7 +288,6 @@ class GuiDocHighlighter(QSyntaxHighlighter): if block.userState() & cType > 0: self.rehighlightBlock(block) logger.debug("Document highlighted in %.3f ms" % (1000*(time() - tStart))) - return ## # Highlight Block @@ -296,27 +303,37 @@ class GuiDocHighlighter(QSyntaxHighlighter): if self._tHandle is None or not text: return - xOff = 0 - hRules = None + blockLen = self.currentBlock().length() + utf16Map = None + if blockLen > len(text) + 1: + # If the lengths are different, the line contains 4 byte + # Unicode characters, and we must use a map between Python + # string indices and the UTF-16 indices used by Qt, where a + # 4 byte character occupies two slots. See #2449. + utf16Map = utf16CharMap(text) + + offset = 0 + rules = None if text.startswith("@"): # Keywords and commands self.setCurrentBlockState(BLOCK_META) index = SHARED.project.index - isValid, bits, pos = index.scanThis(text) + isValid, bits, loc = index.scanThis(text) isGood = index.checkThese(bits, self._tHandle) if isValid: for n, bit in enumerate(bits): - xPos = pos[n] - xLen = len(bit) + pos = utf16Map[loc[n]] if utf16Map else loc[n] + length = utf16Map[loc[n] + len(bit)] - pos if utf16Map else len(bit) if n == 0 and isGood[n]: - self.setFormat(xPos, xLen, self._hStyles["keyword"]) + self.setFormat(pos, length, self._hStyles["keyword"]) elif isGood[n] and not self._isInactive: - one, two = index.parseValue(bit) - self.setFormat(xPos, len(one), self._hStyles["tag"]) - if two: - yPos = xPos + len(bit) - len(two) - self.setFormat(yPos, len(two), self._hStyles["optional"]) + a, b = index.parseValue(bit) + aLen = utf16Map[loc[n] + len(a)] - pos if utf16Map else len(a) + self.setFormat(pos, aLen, self._hStyles["tag"]) + if b: + bLen = utf16Map[loc[n] + len(b)] - pos if utf16Map else len(b) + self.setFormat(pos + length - bLen, bLen, self._hStyles["optional"]) elif not self._isInactive: - self.setFormat(xPos, xLen, self._hStyles["invalid"]) + self.setFormat(pos, length, self._hStyles["invalid"]) # We never want to run the spell checker on keyword/values, # so we force a return here @@ -327,98 +344,118 @@ class GuiDocHighlighter(QSyntaxHighlighter): if text.startswith("# "): # Heading 1 self.setFormat(0, 1, self._hStyles["head1h"]) - self.setFormat(1, len(text), self._hStyles["header1"]) + self.setFormat(1, blockLen, self._hStyles["header1"]) elif text.startswith("## "): # Heading 2 self.setFormat(0, 2, self._hStyles["head2h"]) - self.setFormat(2, len(text), self._hStyles["header2"]) + self.setFormat(2, blockLen, self._hStyles["header2"]) elif text.startswith("### "): # Heading 3 self.setFormat(0, 3, self._hStyles["head3h"]) - self.setFormat(3, len(text), self._hStyles["header3"]) + self.setFormat(3, blockLen, self._hStyles["header3"]) elif text.startswith("#### "): # Heading 4 self.setFormat(0, 4, self._hStyles["head4h"]) - self.setFormat(4, len(text), self._hStyles["header4"]) + self.setFormat(4, blockLen, self._hStyles["header4"]) elif text.startswith("#! "): # Title self.setFormat(0, 2, self._hStyles["head1h"]) - self.setFormat(2, len(text), self._hStyles["header1"]) + self.setFormat(2, blockLen, self._hStyles["header1"]) elif text.startswith("##! "): # Unnumbered self.setFormat(0, 3, self._hStyles["head2h"]) - self.setFormat(3, len(text), self._hStyles["header2"]) + self.setFormat(3, blockLen, self._hStyles["header2"]) elif text.startswith("###! "): # Alternative Scene self.setFormat(0, 4, self._hStyles["head3h"]) - self.setFormat(4, len(text), self._hStyles["header3"]) + self.setFormat(4, blockLen, self._hStyles["header3"]) elif text.startswith("%"): # Comments self.setCurrentBlockState(BLOCK_TEXT) - hRules = self._cmnRules + rules = self._cmnRules - cStyle, cMod, _, cDot, cPos = processComment(text) - cLen = len(text) - cPos - xOff = cPos - if cStyle == nwComment.PLAIN: - self.setFormat(0, cLen, self._hStyles["hidden"]) - elif cStyle == nwComment.IGNORE: - self.setFormat(0, cLen, self._hStyles["strike"]) + style, mod, _, dot, pos = processComment(text) + offset = pos + if utf16Map: + dot = utf16Map[dot] + pos = utf16Map[pos] + length = blockLen - pos + if style == nwComment.PLAIN: + self.setFormat(0, length, self._hStyles["hidden"]) + elif style == nwComment.IGNORE: + self.setFormat(0, length, self._hStyles["strike"]) return # No more processing for these - elif cMod: - self.setFormat(0, cDot, self._hStyles["modifier"]) - self.setFormat(cDot, cPos - cDot, self._hStyles["value"]) - self.setFormat(cPos, cLen, self._hStyles["note"]) + elif mod: + self.setFormat(0, dot, self._hStyles["modifier"]) + self.setFormat(dot, pos - dot, self._hStyles["value"]) + self.setFormat(pos, length, self._hStyles["note"]) else: - self.setFormat(0, cPos, self._hStyles["modifier"]) - self.setFormat(cPos, cLen, self._hStyles["note"]) + self.setFormat(0, pos, self._hStyles["modifier"]) + self.setFormat(pos, length, self._hStyles["note"]) elif text.startswith("["): # Special Command self.setCurrentBlockState(BLOCK_TEXT) - hRules = self._txtRules if self._isNovel else self._minRules + rules = self._txtRules if self._isNovel else self._minRules - sText = text.rstrip().lower() - if sText in ("[newpage]", "[new page]", "[vspace]"): - self.setFormat(0, len(text), self._hStyles["code"]) + check = text.rstrip().lower() + if check in ("[newpage]", "[new page]", "[vspace]"): + self.setFormat(0, blockLen, self._hStyles["code"]) return - elif sText.startswith("[vspace:") and sText.endswith("]"): - tLen = len(sText) - tVal = checkInt(sText[8:-1], 0) - cVal = "value" if tVal > 0 else "invalid" + elif check.startswith("[vspace:") and check.endswith("]"): + value = checkInt(check[8:-1], 0) + style = "value" if value > 0 else "invalid" self.setFormat(0, 8, self._hStyles["code"]) - self.setFormat(8, tLen-9, self._hStyles[cVal]) - self.setFormat(tLen-1, tLen, self._hStyles["code"]) + self.setFormat(8, blockLen-10, self._hStyles[style]) + self.setFormat(blockLen-2, blockLen, self._hStyles["code"]) return else: # Text Paragraph self.setCurrentBlockState(BLOCK_TEXT) - hRules = self._txtRules if self._isNovel else self._minRules + rules = self._txtRules if self._isNovel else self._minRules if self._isNovel and self._dialogParser.enabled: - for pos, end in self._dialogParser(text): - length = end - pos - self.setFormat(pos, length, self._hStyles["dialog"]) + if utf16Map: + for pos, end in self._dialogParser(text): + pos = utf16Map[pos] + end = utf16Map[end] + self.setFormat(pos, end - pos, self._hStyles["dialog"]) + else: + for pos, end in self._dialogParser(text): + self.setFormat(pos, end - pos, self._hStyles["dialog"]) - if hRules: - for rX, hRule in hRules: - for res in re.finditer(rX, text[xOff:]): - for xM, hFmt in hRule.items(): - xPos = res.start(xM) + xOff - xEnd = res.end(xM) + xOff - for x in range(xPos, xEnd): - cFmt = self.format(x) - if cFmt.fontStyleName() != "markup": - cFmt.merge(hFmt) - self.setFormat(x, 1, cFmt) + if rules: + if utf16Map: + for rX, hRule in rules: + for res in re.finditer(rX, text[offset:]): + for x, hFmt in hRule.items(): + pos = res.start(x) + offset + end = res.end(x) + offset + for x in range(pos, end): + m = utf16Map[x] + cFmt = self.format(m) + if not cFmt.property(QtTextUserProperty): + cFmt.merge(hFmt) + self.setFormat(m, utf16Map[x+1] - m, cFmt) + else: + for rX, hRule in rules: + for res in re.finditer(rX, text[offset:]): + for x, hFmt in hRule.items(): + pos = res.start(x) + offset + end = res.end(x) + offset + for x in range(pos, end): + cFmt = self.format(x) + if not cFmt.property(QtTextUserProperty): + cFmt.merge(hFmt) + self.setFormat(x, 1, cFmt) data = self.currentBlockUserData() if not isinstance(data, TextBlockData): data = TextBlockData() self.setCurrentBlockUserData(data) - data.processText(text, xOff) + data.processText(text, offset) if self._spellCheck: - for xPos, xEnd in data.spellCheck(): - for x in range(xPos, xEnd): + for pos, end, _ in data.spellCheck(utf16Map): + for x in range(pos, end): cFmt = self.format(x) cFmt.merge(self._spellErr) self.setFormat(x, 1, cFmt) @@ -435,10 +472,8 @@ class GuiDocHighlighter(QSyntaxHighlighter): ) -> None: """Generate a highlighter character format.""" charFormat = QTextCharFormat() - charFormat.setFontStyleName(name) - - if color: - charFormat.setForeground(color) + blockMerge = name == "markup" + charFormat.setProperty(QtTextUserProperty, blockMerge) if style: styles = style.split(",") @@ -455,16 +490,23 @@ class GuiDocHighlighter(QSyntaxHighlighter): charFormat.setUnderlineStyle(QTextCharFormat.UnderlineStyle.SpellCheckUnderline) if "bg" in styles and color is not None: charFormat.setBackground(QBrush(color, Qt.BrushStyle.SolidPattern)) + color = None + + if color: + charFormat.setForeground(color) if size: charFormat.setFontPointSize(round(size*CONFIG.textFont.pointSize())) self._hStyles[name] = charFormat - return - class TextBlockData(QTextBlockUserData): + """Custom QTextBlock Data. + + Custom data stored in a single text block. The spell check state is + cached here and used when correcting misspelled text. + """ __slots__ = ("_metaData", "_offset", "_spellErrors", "_text") @@ -473,8 +515,7 @@ class TextBlockData(QTextBlockUserData): self._text = "" self._offset = 0 self._metaData: list[tuple[int, int, str, str]] = [] - self._spellErrors: list[tuple[int, int]] = [] - return + self._spellErrors: list[tuple[int, int, str]] = [] @property def metaData(self) -> list[tuple[int, int, str, str]]: @@ -482,7 +523,7 @@ class TextBlockData(QTextBlockUserData): return self._metaData @property - def spellErrors(self) -> list[tuple[int, int]]: + def spellErrors(self) -> list[tuple[int, int, str]]: """Return spell error data from last check.""" return self._spellErrors @@ -505,22 +546,24 @@ class TextBlockData(QTextBlockUserData): text = f"{text[:s]}{pad}{text[e:]}" self._metaData.append((s, e, res.group(0), "url")) - self._text = text.replace("\u02bc", "'") + self._text = text.replace("\u02bc", "'").replace("_", " ") self._offset = offset - return - - def spellCheck(self) -> list[tuple[int, int]]: + def spellCheck(self, utf16Map: list[int] | None) -> list[tuple[int, int, str]]: """Run the spell checker and cache the result, and return the list of spell check errors. """ - self._spellErrors = [] - checker = SHARED.spelling - for res in RX_WORDS.finditer(self._text.replace("_", " "), self._offset): - if ( - (word := res.group(0)) - and not (word.isnumeric() or word.isupper() or checker.checkWord(word)) - ): - self._spellErrors.append((res.start(0), res.end(0))) - + spell = SHARED.spelling + if utf16Map: + self._spellErrors = [ + (utf16Map[r.start(0)], utf16Map[r.end(0)], w) + for r in RX_WORDS.finditer(self._text, self._offset) + if (w := r.group(0)) and not (w.isnumeric() or w.isupper() or spell.checkWord(w)) + ] + else: + self._spellErrors = [ + (r.start(0), r.end(0), w) + for r in RX_WORDS.finditer(self._text, self._offset) + if (w := r.group(0)) and not (w.isnumeric() or w.isupper() or spell.checkWord(w)) + ] return self._spellErrors diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 4b43937b..10ef9c46 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -23,7 +23,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -60,6 +60,7 @@ logger = logging.getLogger(__name__) class GuiDocViewer(QTextBrowser): + """GUI: Document Viewer.""" closeDocumentRequest = pyqtSignal() documentLoaded = pyqtSignal(str) @@ -110,8 +111,6 @@ class GuiDocViewer(QTextBrowser): logger.debug("Ready: GuiDocViewer") - return - ## # Properties ## @@ -138,13 +137,11 @@ class GuiDocViewer(QTextBrowser): self.setSearchPaths([""]) self._docHandle = None self.docHeader.clearHeader() - return def updateTheme(self) -> None: """Update theme elements.""" self.docHeader.updateTheme() self.docFooter.updateTheme() - return def initViewer(self) -> None: """Set editor settings from main config.""" @@ -206,8 +203,6 @@ class GuiDocViewer(QTextBrowser): # If we have a document open, we should reload it in case the font changed self.reloadText() - return - def loadText(self, tHandle: str, updateHistory: bool = True) -> bool: """Load text into the viewer from an item handle.""" if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE): @@ -228,11 +223,11 @@ class GuiDocViewer(QTextBrowser): qDoc.setTheme(self._docTheme) qDoc.initDocument() qDoc.setKeywords(True) - qDoc.setCommentType(nwComment.NOTE, CONFIG.viewComments) - qDoc.setCommentType(nwComment.STORY, CONFIG.viewComments) qDoc.setCommentType(nwComment.PLAIN, CONFIG.viewComments) qDoc.setCommentType(nwComment.SYNOPSIS, CONFIG.viewSynopsis) qDoc.setCommentType(nwComment.SHORT, CONFIG.viewSynopsis) + qDoc.setCommentType(nwComment.STORY, CONFIG.viewNotes) + qDoc.setCommentType(nwComment.NOTE, CONFIG.viewNotes) # Be extra careful here to prevent crashes when first opening a # project as a crash here leaves no way of recovering. @@ -280,7 +275,6 @@ class GuiDocViewer(QTextBrowser): """Reload the text in the current document.""" if self._docHandle: self.loadText(self._docHandle, updateHistory=False) - return def docAction(self, action: nwDocAction) -> bool: """Process document actions on the current document.""" @@ -308,7 +302,6 @@ class GuiDocViewer(QTextBrowser): def clearNavHistory(self) -> None: """Clear the navigation history.""" self.docHistory.clear() - return def updateDocMargins(self) -> None: """Automatically adjust the margins so the text is centred.""" @@ -337,8 +330,6 @@ class GuiDocViewer(QTextBrowser): self.docFooter.setGeometry(tB, fY, tW, fH) self.setViewportMargins(tM, max(cM, tH), tM, max(cM, fH)) - return - ## # Setters ## @@ -347,7 +338,6 @@ class GuiDocViewer(QTextBrowser): """Set the scrollbar position.""" if (vBar := self.verticalScrollBar()) and vBar.isVisible(): vBar.setValue(pos) - return ## # Public Slots @@ -359,7 +349,6 @@ class GuiDocViewer(QTextBrowser): if tHandle == self._docHandle and change == nwChange.UPDATE: self.docHeader.setHandle(tHandle) self.updateDocMargins() - return @pyqtSlot(str) def navigateTo(self, anchor: str) -> None: @@ -367,7 +356,6 @@ class GuiDocViewer(QTextBrowser): if isinstance(anchor, str) and anchor.startswith("#"): logger.debug("Moving to anchor '%s'", anchor) self.setSource(QUrl(anchor)) - return ## # Private Slots @@ -377,13 +365,11 @@ class GuiDocViewer(QTextBrowser): def navBackward(self) -> None: """Navigate backwards in the document view history.""" self.docHistory.backward() - return @pyqtSlot() def navForward(self) -> None: """Navigate forwards in the document view history.""" self.docHistory.forward() - return @pyqtSlot("QUrl") def _linkClicked(self, url: QUrl) -> None: @@ -396,7 +382,6 @@ class GuiDocViewer(QTextBrowser): self.navigateTo(link) elif link.startswith("http"): QDesktopServices.openUrl(QUrl(url)) - return @pyqtSlot("QPoint") def _openContextMenu(self, point: QPoint) -> None: @@ -430,8 +415,6 @@ class GuiDocViewer(QTextBrowser): ctxMenu.setParent(None) - return - ## # Events ## @@ -440,7 +423,6 @@ class GuiDocViewer(QTextBrowser): """Update document margins when widget is resized.""" self.updateDocMargins() super().resizeEvent(event) - return def mouseReleaseEvent(self, event: QMouseEvent) -> None: """Capture mouse click events on the document.""" @@ -450,7 +432,6 @@ class GuiDocViewer(QTextBrowser): self.navForward() else: super().mouseReleaseEvent(event) - return def dragEnterEvent(self, event: QDragEnterEvent) -> None: """Overload drag enter event to handle dragged items.""" @@ -458,7 +439,6 @@ class GuiDocViewer(QTextBrowser): event.acceptProposedAction() else: super().dragEnterEvent(event) - return def dragMoveEvent(self, event: QDragMoveEvent) -> None: """Overload drag move event to handle dragged items.""" @@ -466,7 +446,6 @@ class GuiDocViewer(QTextBrowser): event.acceptProposedAction() else: super().dragMoveEvent(event) - return def dropEvent(self, event: QDropEvent) -> None: """Overload drop event to handle dragged items.""" @@ -476,7 +455,6 @@ class GuiDocViewer(QTextBrowser): self.openDocumentRequest.emit(handles[0], nwDocMode.VIEW, "", True) else: super().dropEvent(event) - return ## # Internal Functions @@ -500,16 +478,18 @@ class GuiDocViewer(QTextBrowser): self.setTextCursor(cursor) - return - def _makePosSelection(self, selType: QTextCursor.SelectionType, pos: QPoint) -> None: """Handle text selection at a given location.""" self.setTextCursor(self.cursorForPosition(pos)) self._makeSelection(selType) - return class GuiDocViewHistory: + """GUI: Document Viewer History. + + This class holds the navigation history for the viewer panel, which + is used for backward/forward navigation. + """ def __init__(self, docViewer: GuiDocViewer) -> None: self.docViewer = docViewer @@ -517,7 +497,6 @@ class GuiDocViewHistory: self._posHistory = [] self._currPos = -1 self._prevPos = -1 - return def clear(self) -> None: """Clear the view history.""" @@ -526,7 +505,6 @@ class GuiDocViewHistory: self._posHistory = [] self._currPos = -1 self._prevPos = -1 - return def append(self, tHandle: str) -> bool: """Append a document handle and its scroll bar position to the @@ -566,7 +544,6 @@ class GuiDocViewHistory: self._currPos = newPos self._updateNavButtons() self._dumpHistory() - return def backward(self) -> None: """Navigate to the previous entry in the view history.""" @@ -580,7 +557,6 @@ class GuiDocViewHistory: self._currPos = newPos self._updateNavButtons() self._dumpHistory() - return ## # Internal Functions @@ -590,12 +566,10 @@ class GuiDocViewHistory: """Update the scrollbar position of the previous entry.""" if self._prevPos >= 0 and self._prevPos < len(self._posHistory): self._posHistory[self._prevPos] = self.docViewer.scrollPosition - return def _updateNavButtons(self) -> None: """Update the navigation buttons in the document header.""" self.docViewer.docHeader.updateNavButtons(0, len(self._navHistory) - 1, self._currPos) - return def _truncateHistory(self, atPos: int) -> None: """Truncate the navigation history to the given position. Also @@ -606,7 +580,6 @@ class GuiDocViewHistory: self._posHistory = self._posHistory[nSkip:atPos + 1] self._currPos -= nSkip self._prevPos -= nSkip - return def _dumpHistory(self) -> None: """Debug function to dump history to the logger. Since it is a @@ -616,11 +589,10 @@ class GuiDocViewHistory: for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory, strict=False)): a = ">" if i == self._currPos else " " logger.debug(f"History {i + 1:02d}: {a} {h:13s} [x:{p}]") - return class GuiDocViewHeader(QWidget): - """The Embedded Document Header + """The Embedded Document Header. Only used by DocViewer, and is at a fixed position in the QTextBrowser's viewport. @@ -711,8 +683,6 @@ class GuiDocViewHeader(QWidget): logger.debug("Ready: GuiDocViewHeader") - return - ## # Methods ## @@ -730,7 +700,6 @@ class GuiDocViewHeader(QWidget): self.editButton.setVisible(False) self.refreshButton.setVisible(False) self.closeButton.setVisible(False) - return def setOutline(self, data: dict[str, tuple[str, int]]) -> None: """Set the document outline dataset.""" @@ -750,13 +719,11 @@ class GuiDocViewHeader(QWidget): lambda _, title=title: self.docViewer.navigateTo(f"#{tHandle}:{title}") ) self._docOutline = data - return def updateFont(self) -> None: """Update the font settings.""" self.setFont(SHARED.theme.guiFont) self.itemTitle.setFont(SHARED.theme.guiFontSmall) - return def updateTheme(self) -> None: """Update theme elements.""" @@ -777,8 +744,6 @@ class GuiDocViewHeader(QWidget): self.matchColors() - return - def matchColors(self) -> None: """Update the colours of the widget to match those of the syntax theme rather than the main GUI. @@ -792,15 +757,13 @@ class GuiDocViewHeader(QWidget): self.itemTitle.setTextColors( color=palette.windowText().color(), faded=SHARED.theme.fadedText ) - return def changeFocusState(self, state: bool) -> None: """Toggle focus state.""" self.itemTitle.setColorState(state) - return def setHandle(self, tHandle: str) -> None: - """Sets the document title from the handle, or alternatively, + """Set the document title from the handle, or alternatively, set the whole document path. """ self._docHandle = tHandle @@ -819,13 +782,10 @@ class GuiDocViewHeader(QWidget): self.refreshButton.setVisible(True) self.closeButton.setVisible(True) - return - def updateNavButtons(self, firstIdx: int, lastIdx: int, currIdx: int) -> None: """Enable and disable nav buttons based on index in history.""" self.backButton.setEnabled(currIdx > firstIdx) self.forwardButton.setEnabled(currIdx < lastIdx) - return ## # Private Slots @@ -836,20 +796,17 @@ class GuiDocViewHeader(QWidget): """Trigger the close editor/viewer on the main window.""" self.clearHeader() self.docViewer.closeDocumentRequest.emit() - return @pyqtSlot() def _refreshDocument(self) -> None: """Reload the content of the document.""" self.docViewer.reloadDocumentRequest.emit() - return @pyqtSlot() def _editDocument(self) -> None: """Open the document in the editor.""" if tHandle := self._docHandle: self.docViewer.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True) - return ## # Events @@ -861,11 +818,10 @@ class GuiDocViewHeader(QWidget): """ if event.button() == QtMouseLeft: self.docViewer.requestProjectItemSelected.emit(self._docHandle, True) - return class GuiDocViewFooter(QWidget): - """The Embedded Document Footer + """The Embedded Document Footer. Only used by DocViewer, and is at a fixed position in the QTextBrowser's viewport. @@ -913,12 +869,23 @@ class GuiDocViewFooter(QWidget): self.showSynopsis.toggled.connect(self._doToggleSynopsis) self.showSynopsis.setToolTip(self.tr("Show Synopsis Comments")) + # Show Notes + self.showNotes = QToolButton(self) + self.showNotes.setText(self.tr("Notes")) + self.showNotes.setCheckable(True) + self.showNotes.setChecked(CONFIG.viewNotes) + self.showNotes.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + self.showNotes.setIconSize(iSz) + self.showNotes.toggled.connect(self._doToggleNotes) + self.showNotes.setToolTip(self.tr("Show Notes")) + # Assemble Layout self.outerBox = QHBoxLayout() self.outerBox.addWidget(self.showHide, 0) self.outerBox.addStretch(1) self.outerBox.addWidget(self.showComments, 0) self.outerBox.addWidget(self.showSynopsis, 0) + self.outerBox.addWidget(self.showNotes, 0) self.outerBox.setSpacing(4) self.setLayout(self.outerBox) @@ -933,8 +900,6 @@ class GuiDocViewFooter(QWidget): logger.debug("Ready: GuiDocViewFooter") - return - ## # Methods ## @@ -944,7 +909,7 @@ class GuiDocViewFooter(QWidget): self.setFont(SHARED.theme.guiFont) self.showComments.setFont(SHARED.theme.guiFontSmall) self.showSynopsis.setFont(SHARED.theme.guiFontSmall) - return + self.showNotes.setFont(SHARED.theme.guiFontSmall) def updateTheme(self) -> None: """Update theme elements.""" @@ -955,16 +920,16 @@ class GuiDocViewFooter(QWidget): self.showHide.setThemeIcon("panel") self.showComments.setIcon(bulletIcon) self.showSynopsis.setIcon(bulletIcon) + self.showNotes.setIcon(bulletIcon) buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON) self.showHide.setStyleSheet(buttonStyle) self.showComments.setStyleSheet(buttonStyle) self.showSynopsis.setStyleSheet(buttonStyle) + self.showNotes.setStyleSheet(buttonStyle) self.matchColors() - return - def matchColors(self) -> None: """Update the colours of the widget to match those of the syntax theme rather than the main GUI. @@ -975,7 +940,6 @@ class GuiDocViewFooter(QWidget): palette.setColor(QPalette.ColorRole.WindowText, syntax.text) palette.setColor(QPalette.ColorRole.Text, syntax.text) self.setPalette(palette) - return ## # Private Slots @@ -986,11 +950,15 @@ class GuiDocViewFooter(QWidget): """Toggle the view comment button and reload the document.""" CONFIG.viewComments = state self.docViewer.reloadText() - return @pyqtSlot(bool) def _doToggleSynopsis(self, state: bool) -> None: """Toggle the view synopsis button and reload the document.""" CONFIG.viewSynopsis = state self.docViewer.reloadText() - return + + @pyqtSlot(bool) + def _doToggleNotes(self, state: bool) -> None: + """Toggle the view notes button and reload the document.""" + CONFIG.viewNotes = state + self.docViewer.reloadText() diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py index 7d996992..8ac9816a 100644 --- a/novelwriter/gui/docviewerpanel.py +++ b/novelwriter/gui/docviewerpanel.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -49,6 +49,10 @@ logger = logging.getLogger(__name__) class GuiDocViewerPanel(QWidget): + """GUI: Document Viewer Panel. + + The panel of project meta data below the viewer. + """ openDocumentRequest = pyqtSignal(str, Enum, str, bool) loadDocumentTagRequest = pyqtSignal(str, Enum) @@ -96,8 +100,6 @@ class GuiDocViewerPanel(QWidget): logger.debug("Ready: GuiDocViewerPanel") - return - ## # Methods ## @@ -113,7 +115,6 @@ class GuiDocViewerPanel(QWidget): for tab in self.kwTabs.values(): tab.updateTheme() self._loadAllTags() - return def openProjectTasks(self) -> None: """Run open project tasks.""" @@ -124,7 +125,6 @@ class GuiDocViewerPanel(QWidget): for key, value in colWidths.items(): if key in self.kwTabs and isinstance(value, list): self.kwTabs[key].setColumnWidths(value) - return def closeProjectTasks(self) -> None: """Run close project tasks.""" @@ -133,7 +133,6 @@ class GuiDocViewerPanel(QWidget): hideInactive = self.aInactive.isChecked() SHARED.project.options.setValue("GuiDocViewerPanel", "colWidths", colWidths) SHARED.project.options.setValue("GuiDocViewerPanel", "hideInactive", hideInactive) - return ## # Public Slots @@ -145,7 +144,6 @@ class GuiDocViewerPanel(QWidget): self.tabBackRefs.clearContent() for cTab in self.kwTabs.values(): cTab.clearContent() - return @pyqtSlot() def indexHasAppeared(self) -> None: @@ -153,7 +151,6 @@ class GuiDocViewerPanel(QWidget): self._loadAllTags() self._updateTabVisibility() self.updateHandle(self._lastHandle) - return @pyqtSlot(str, Enum) def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None: @@ -168,14 +165,12 @@ class GuiDocViewerPanel(QWidget): else: self.kwTabs[tClass].removeEntry(key) self._updateTabVisibility() - return @pyqtSlot(str) def updateHandle(self, tHandle: str | None) -> None: """Update the document handle.""" self._lastHandle = tHandle self.tabBackRefs.refreshContent(tHandle or None) - return @pyqtSlot(list, list) def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None: @@ -191,14 +186,12 @@ class GuiDocViewerPanel(QWidget): else: logger.warning("Could not remove tag '%s' from view panel", key) self._updateTabVisibility() - return @pyqtSlot(str) def updateStatusLabels(self, kind: str) -> None: """Update the importance labels.""" if kind == "i": self._loadAllTags() - return ## # Private Slots @@ -212,7 +205,6 @@ class GuiDocViewerPanel(QWidget): cTab.clearContent() self._loadAllTags() self._updateTabVisibility() - return ## # Internal Functions @@ -222,7 +214,6 @@ class GuiDocViewerPanel(QWidget): """Hide class tabs with no content.""" for tClass, cTab in self.kwTabs.items(): self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.countEntries() > 0) - return def _loadAllTags(self) -> None: """Load all tags into the tabs.""" @@ -230,7 +221,6 @@ class GuiDocViewerPanel(QWidget): for key, name, tClass, iItem, hItem in data: if tClass in self.kwTabs and iItem and hItem: self.kwTabs[tClass].addUpdateEntry(key, name, iItem, hItem) - return class _ViewPanelBackRefs(QTreeWidget): @@ -278,8 +268,6 @@ class _ViewPanelBackRefs(QTreeWidget): self.clicked.connect(self._treeItemClicked) self.doubleClicked.connect(self._treeItemDoubleClicked) - return - def updateTheme(self) -> None: """Update theme elements.""" self._editIcon = SHARED.theme.getIcon("edit", "green") @@ -288,13 +276,11 @@ class _ViewPanelBackRefs(QTreeWidget): if item := self.topLevelItem(i): item.setIcon(self.C_EDIT, self._editIcon) item.setIcon(self.C_VIEW, self._viewIcon) - return def clearContent(self) -> None: """Clear the widget.""" self.clear() self._treeMap = {} - return def refreshContent(self, dHandle: str | None) -> None: """Update the content.""" @@ -303,7 +289,6 @@ class _ViewPanelBackRefs(QTreeWidget): refs = SHARED.project.index.getBackReferenceList(dHandle) for tHandle, (sTitle, hItem) in refs.items(): self._setTreeItemValues(tHandle, sTitle, hItem) - return def refreshDocument(self, tHandle: str) -> None: """Refresh document meta data.""" @@ -311,7 +296,6 @@ class _ViewPanelBackRefs(QTreeWidget): for sTitle, hItem in iItem.items(): if f"{tHandle}:{sTitle}" in self._treeMap: self._setTreeItemValues(tHandle, sTitle, hItem) - return ## # Private Slots @@ -325,7 +309,6 @@ class _ViewPanelBackRefs(QTreeWidget): self._parent.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True) elif index.column() == self.C_VIEW: self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True) - return @pyqtSlot("QModelIndex") def _treeItemDoubleClicked(self, index: QModelIndex) -> None: @@ -333,7 +316,6 @@ class _ViewPanelBackRefs(QTreeWidget): tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE) if index.column() not in (self.C_EDIT, self.C_VIEW): self._parent.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", True) - return ## # Internal Functions @@ -362,8 +344,6 @@ class _ViewPanelBackRefs(QTreeWidget): self.addTopLevelItem(trItem) self._treeMap[tKey] = trItem - return - class _ViewPanelKeyWords(QTreeWidget): @@ -418,14 +398,11 @@ class _ViewPanelKeyWords(QTreeWidget): self.clicked.connect(self._treeItemClicked) self.doubleClicked.connect(self._treeItemDoubleClicked) - return - def updateTheme(self) -> None: """Update theme elements.""" 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") - return def countEntries(self) -> int: """Return the number of items in the list.""" @@ -435,7 +412,6 @@ class _ViewPanelKeyWords(QTreeWidget): """Clear the list.""" self._treeMap = {} self.clear() - return def addUpdateEntry(self, tag: str, name: str, iItem: IndexNode, hItem: IndexHeading) -> None: """Add a new entry, or update an existing one.""" @@ -470,8 +446,6 @@ class _ViewPanelKeyWords(QTreeWidget): self.addTopLevelItem(trItem) self._treeMap[tag] = trItem - return - def removeEntry(self, tag: str) -> bool: """Remove a tag from the list.""" if tag in self._treeMap: @@ -487,7 +461,6 @@ class _ViewPanelKeyWords(QTreeWidget): self.setColumnWidth(self.C_IMPORT, checkInt(widths[1], 100)) self.setColumnWidth(self.C_DOC, checkInt(widths[2], 100)) self.setColumnWidth(self.C_TITLE, checkInt(widths[3], 100)) - return def getColumnWidths(self) -> list[int]: """Get the widths of the user-adjustable columns.""" @@ -510,7 +483,6 @@ class _ViewPanelKeyWords(QTreeWidget): self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.EDIT) elif index.column() == self.C_VIEW: self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW) - return @pyqtSlot("QModelIndex") def _treeItemDoubleClicked(self, index: QModelIndex) -> None: @@ -518,4 +490,3 @@ class _ViewPanelKeyWords(QTreeWidget): tag = index.siblingAtColumn(self.C_DATA).data(self.D_TAG) if index.column() not in (self.C_EDIT, self.C_VIEW): self._parent.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW) - return diff --git a/novelwriter/gui/editordocument.py b/novelwriter/gui/editordocument.py index d42ffaf1..2f20c6d8 100644 --- a/novelwriter/gui/editordocument.py +++ b/novelwriter/gui/editordocument.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -42,6 +42,11 @@ logger = logging.getLogger(__name__) class GuiTextDocument(QTextDocument): + """Custom: Modified QTextDocument. + + A special text document format that incorporates a few additional + features including spell checking. + """ def __init__(self, parent: QObject) -> None: super().__init__(parent=parent) @@ -52,11 +57,8 @@ class GuiTextDocument(QTextDocument): logger.debug("Ready: GuiTextDocument") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiTextDocument") - return ## # Properties @@ -96,8 +98,6 @@ class GuiTextDocument(QTextDocument): logger.debug("Loaded %d text blocks in %.3f ms", count, 1000*(tMid - tStart)) logger.debug("Highlighted document in %.3f ms", 1000*(tEnd - tMid)) - return - def metaDataAtPos(self, pos: int) -> tuple[str, str]: """Check if there is meta data available at a given position in the document, and if so, return it. @@ -113,7 +113,7 @@ class GuiTextDocument(QTextDocument): return cData, cType return "", "" - def spellErrorAtPos(self, pos: int) -> tuple[str, int, int, list[str]]: + def spellErrorAtPos(self, pos: int) -> tuple[str, int, list[str]]: """Check if there is a misspelled word at a given position in the document, and if so, return it. """ @@ -122,15 +122,11 @@ class GuiTextDocument(QTextDocument): block = cursor.block() data = block.userData() if block.isValid() and isinstance(data, TextBlockData): - text = block.text() - check = pos - block.position() - if check >= 0: - for cPos, cEnd in data.spellErrors: - cLen = cEnd - cPos - if cPos <= check <= cEnd: - word = text[cPos:cEnd] - return word, cPos, cLen, SHARED.spelling.suggestWords(word) - return "", -1, -1, [] + if (check := pos - block.position()) >= 0: + for start, end, word in data.spellErrors: + if start <= check <= end: + return word, start, SHARED.spelling.suggestWords(word) + return "", -1, [] def iterBlockByType(self, cType: int, maxCount: int = 1000) -> Iterable[QTextBlock]: """Iterate over all text blocks of a given type.""" @@ -150,4 +146,3 @@ class GuiTextDocument(QTextDocument): def setSpellCheckState(self, state: bool) -> None: """Set the spell check state of the syntax highlighter.""" self._syntax.setSpellCheck(state) - return diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 9bece183..d087f742 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -43,6 +43,7 @@ logger = logging.getLogger(__name__) class GuiItemDetails(QWidget): + """GUI: Project Item Details Panel.""" def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -190,8 +191,6 @@ class GuiItemDetails(QWidget): logger.debug("Ready: GuiItemDetails") - return - ### # Class Methods ## @@ -210,7 +209,6 @@ class GuiItemDetails(QWidget): self.cCountData.clear() self.wCountData.clear() self.pCountData.clear() - return def refreshDetails(self) -> None: """Reload the content of the details panel.""" @@ -219,7 +217,6 @@ class GuiItemDetails(QWidget): def updateTheme(self) -> None: """Update theme elements.""" self.updateViewBox(self._handle) - return def updateViewBox(self, tHandle: str | None) -> None: """Populate the details box from a given handle.""" @@ -233,14 +230,8 @@ class GuiItemDetails(QWidget): # Label # ===== - if nwItem.isFileType(): - if nwItem.isActive: - self.labelIcon.setPixmap(SHARED.theme.getPixmap("checked", (iPx, iPx), "green")) - else: - self.labelIcon.setPixmap(SHARED.theme.getPixmap("unchecked", (iPx, iPx), "red")) - else: - self.labelIcon.setPixmap(SHARED.theme.getPixmap("noncheckable", (iPx, iPx), "faded")) - + _, icon = nwItem.getActiveStatus() + self.labelIcon.setPixmap(icon.pixmap(iPx, iPx)) self.labelData.setText(elide(nwItem.itemName, 100)) # Status @@ -289,4 +280,3 @@ class GuiItemDetails(QWidget): self.updateViewBox(tHandle) elif change == nwChange.DELETE: self.updateViewBox(None) - return diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 33f97dde..3509291e 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -82,8 +82,6 @@ class GuiMainMenu(QMenuBar): logger.debug("Ready: GuiMainMenu") - return - ## # Public Slots ## @@ -92,7 +90,6 @@ class GuiMainMenu(QMenuBar): def setSpellCheckState(self, state: bool) -> None: """Forward spell check check state to its action.""" self.aSpellCheck.setChecked(state) - return ## # Private Slots @@ -105,21 +102,18 @@ class GuiMainMenu(QMenuBar): decision, just pass a None to the function and let it decide. """ self.mainGui.docEditor.toggleSpellCheck(None) - return @pyqtSlot() def _openUserManualFile(self) -> None: """Open the documentation in PDF format.""" if isinstance(CONFIG.pdfDocs, Path): openExternalPath(CONFIG.pdfDocs) - return @pyqtSlot(str) def _changeSpelling(self, language: str) -> None: """Change the spell check language.""" SHARED.project.data.setSpellLang(language) SHARED.updateSpellCheckLanguage() - return ## # Internal Functions @@ -188,8 +182,6 @@ class GuiMainMenu(QMenuBar): self.aExitNW.triggered.connect(qtLambda(self.mainGui.closeMain)) self.mainGui.addAction(self.aExitNW) - return - def _buildDocumentMenu(self) -> None: """Assemble the Document menu.""" # Document @@ -236,8 +228,6 @@ class GuiMainMenu(QMenuBar): self.aImportFile = qtAddAction(self.docuMenu, self.tr("Import Text from File")) self.aImportFile.triggered.connect(qtLambda(self.mainGui.importDocument)) - return - def _buildEditMenu(self) -> None: """Assemble the Edit menu.""" # Edit @@ -305,8 +295,6 @@ class GuiMainMenu(QMenuBar): ) self.mainGui.addAction(self.aSelectPar) - return - def _buildViewMenu(self) -> None: """Assemble the View menu.""" # View @@ -318,6 +306,7 @@ class GuiMainMenu(QMenuBar): self.aFocusTree.triggered.connect( lambda: self.requestFocusChange.emit(nwFocus.TREE) ) + self.mainGui.addAction(self.aFocusTree) # View > Document Editor self.aFocusDocument = qtAddAction(self.viewMenu, self.tr("Go to Document")) @@ -332,6 +321,7 @@ class GuiMainMenu(QMenuBar): self.aFocusOutline.triggered.connect( lambda: self.requestFocusChange.emit(nwFocus.OUTLINE) ) + self.mainGui.addAction(self.aFocusOutline) # View > Separator self.viewMenu.addSeparator() @@ -365,8 +355,6 @@ class GuiMainMenu(QMenuBar): self.aFullScreen.triggered.connect(self.mainGui.toggleFullScreenMode) self.mainGui.addAction(self.aFullScreen) - return - def _buildInsertMenu(self) -> None: """Assemble the Insert menu.""" # Insert @@ -644,8 +632,6 @@ class GuiMainMenu(QMenuBar): lambda: self.requestDocInsert.emit(nwDocInsert.FOOTNOTE) ) - return - def _buildFormatMenu(self) -> None: """Assemble the Format menu.""" # Format @@ -675,6 +661,14 @@ class GuiMainMenu(QMenuBar): ) self.mainGui.addAction(self.aFmtStrike) + # Format > Highlight + self.aFmtMark = qtAddAction(self.fmtMenu, self.tr("Highlight")) + self.aFmtMark.setShortcut("Ctrl+M") + self.aFmtMark.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.MD_MARK) + ) + self.mainGui.addAction(self.aFmtStrike) + # Edit > Separator self.fmtMenu.addSeparator() @@ -891,8 +885,6 @@ class GuiMainMenu(QMenuBar): lambda: self.requestDocAction.emit(nwDocAction.RM_BREAKS) ) - return - def _buildSearchMenu(self) -> None: """Assemble the Search menu.""" # Search @@ -938,8 +930,6 @@ class GuiMainMenu(QMenuBar): self.aFindProj.setShortcut("Ctrl+Shift+F") self.aFindProj.triggered.connect(qtLambda(self.requestViewChange.emit, nwView.SEARCH)) - return - def _buildToolsMenu(self) -> None: """Assemble the Tools menu.""" # Tools @@ -1009,8 +999,6 @@ class GuiMainMenu(QMenuBar): self.aPreferences.triggered.connect(self.mainGui.showPreferencesDialog) self.mainGui.addAction(self.aPreferences) - return - def _buildHelpMenu(self) -> None: """Assemble the Help menu.""" # Help @@ -1056,5 +1044,3 @@ class GuiMainMenu(QMenuBar): # Document > Main Website self.aWebsite = qtAddAction(self.helpMenu, self.tr("The novelWriter Website")) self.aWebsite.triggered.connect(qtLambda(SHARED.openWebsite, nwConst.URL_WEB)) - - return diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 86913b61..a5521ef8 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -24,7 +24,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -55,6 +55,7 @@ logger = logging.getLogger(__name__) class GuiNovelView(QWidget): + """GUI: Novel View Panel.""" # Signals for user interaction with the novel tree selectedItemChanged = pyqtSignal(str) @@ -82,8 +83,6 @@ class GuiNovelView(QWidget): self.getSelectedHandle = self.novelTree.getSelectedHandle self.refreshCurrentTree = self.novelBar.forceRefreshNovelTree - return - ## # Methods ## @@ -91,19 +90,16 @@ class GuiNovelView(QWidget): def updateTheme(self) -> None: """Update theme elements.""" self.novelBar.updateTheme() - return def initSettings(self) -> None: """Initialise GUI elements that depend on specific settings.""" self.novelTree.initSettings() - return def clearNovelView(self) -> None: """Clear project-related GUI content.""" self.novelBar.clearContent() self.novelBar.setEnabled(False) self.novelTree.clearContent() - return def openProjectTasks(self) -> None: """Run open project tasks.""" @@ -125,8 +121,6 @@ class GuiNovelView(QWidget): self.novelTree.setLastColSize(lastColSize) - return - def closeProjectTasks(self) -> None: """Run closing project tasks.""" logger.debug("Saving State: GuiNovelView") @@ -140,12 +134,9 @@ class GuiNovelView(QWidget): self.clearNovelView() - return - def setTreeFocus(self) -> None: """Set the focus to the tree widget.""" self.novelTree.setFocus() - return def treeHasFocus(self) -> bool: """Check if the novel tree has focus.""" @@ -159,22 +150,20 @@ class GuiNovelView(QWidget): def setCurrentNovel(self, rootHandle: str | None) -> None: """Set the current novel to display.""" self.novelTree.setNovelModel(rootHandle) - return @pyqtSlot(str) def setActiveHandle(self, tHandle: str) -> None: """Highlight the rows associated with a given handle.""" self.novelTree.setActiveHandle(tHandle) - return @pyqtSlot(str, Enum) def updateRootItem(self, tHandle: str, change: nwChange) -> None: """If any root item changes, rebuild the novel root menu.""" self.novelBar.buildNovelRootMenu() - return class GuiNovelToolBar(QWidget): + """GUI: Novel View Panel ToolBar.""" def __init__(self, novelView: GuiNovelView) -> None: super().__init__(parent=novelView) @@ -249,8 +238,6 @@ class GuiNovelToolBar(QWidget): logger.debug("Ready: GuiNovelToolBar") - return - ## # Methods ## @@ -277,20 +264,16 @@ class GuiNovelToolBar(QWidget): self.forceRefreshNovelTree() - return - def clearContent(self) -> None: """Run clearing project tasks.""" self.novelValue.clear() self.novelValue.setToolTip("") - return def buildNovelRootMenu(self) -> None: """Build the novel root menu.""" self.novelValue.refreshNovelList() self.novelView.setCurrentNovel(self.novelValue.handle) self.tbNovel.setVisible(self.novelValue.count() > 1) - return def setCurrentRoot(self, rootHandle: str | None) -> None: """Set the current active root handle.""" @@ -300,7 +283,6 @@ class GuiNovelToolBar(QWidget): SHARED.project.data.setLastHandle(rootHandle, "novel") self.novelView.setCurrentNovel(rootHandle) self.novelView.novelTree.setAccessibleName(self.novelValue.currentText()) - return def setLastColType(self, colType: nwNovelExtra, doRefresh: bool = True) -> None: """Set the last column type.""" @@ -309,7 +291,6 @@ class GuiNovelToolBar(QWidget): if doRefresh: self.forceRefreshNovelTree() self.novelView.novelTree.resizeColumns() - return def setActive(self, state: bool) -> None: """Set the widget active state, which enables automatic tree @@ -322,7 +303,6 @@ class GuiNovelToolBar(QWidget): and self._refresh.get(handle, False) ): self._refreshNovelTree(self.novelValue.handle) - return ## # Public Slots @@ -335,7 +315,6 @@ class GuiNovelToolBar(QWidget): self.novelView.setCurrentNovel(tHandle) SHARED.project.index.refreshNovelModel(tHandle) self._refresh[tHandle] = False - return ## # Private Slots @@ -349,7 +328,6 @@ class GuiNovelToolBar(QWidget): self._refresh[tHandle] = False else: self._refresh[tHandle] = True - return @pyqtSlot() def _selectLastColumnSize(self) -> None: @@ -361,7 +339,6 @@ class GuiNovelToolBar(QWidget): if isOk: self.novelView.novelTree.setLastColSize(newSize) self.novelView.novelTree.resizeColumns() - return ## # Internal Functions @@ -374,10 +351,10 @@ class GuiNovelToolBar(QWidget): aLast.setActionGroup(self.gLastCol) aLast.triggered.connect(qtLambda(self.setLastColType, colType)) self.aLastCol[colType] = aLast - return class GuiNovelTree(NTreeView): + """GUI: Novel View Panel Tree.""" def __init__(self, novelView: GuiNovelView) -> None: super().__init__(parent=novelView) @@ -414,8 +391,6 @@ class GuiNovelTree(NTreeView): logger.debug("Ready: GuiNovelTree") - return - def initSettings(self) -> None: """Set or update tree widget settings.""" if CONFIG.hideVScroll: @@ -426,7 +401,6 @@ class GuiNovelTree(NTreeView): self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff) else: self.setHorizontalScrollBarPolicy(QtScrollAsNeeded) - return ## # Properties @@ -466,25 +440,21 @@ class GuiNovelTree(NTreeView): self.resizeColumns() else: self.clearContent() - return def setActiveHandle(self, tHandle: str | None) -> None: """Set the handle to be highlighted.""" self._actHandle = tHandle if viewport := self.viewport(): viewport.repaint() - return def setLastColType(self, colType: nwNovelExtra) -> None: """Set the extra column type.""" self._lastColType = colType SHARED.project.index.setNovelModelExtraColumn(colType) - return def setLastColSize(self, colSize: int) -> None: """Set the extra column size between 15% and 75%.""" self._lastColSize = minmax(colSize, 15, 75)/100.0 - return ## # Class Methods @@ -493,7 +463,6 @@ class GuiNovelTree(NTreeView): def clearContent(self) -> None: """Clear the tree view.""" self.setModel(None) - return def resizeColumns(self) -> None: """Set the correct column sizes.""" @@ -506,7 +475,6 @@ class GuiNovelTree(NTreeView): if model.columns == 4: header.setSectionResizeMode(3, QtHeaderToContents) header.setMaximumSectionSize(int(self._lastColSize * vp.width())) - return ## # Overloads @@ -517,7 +485,6 @@ class GuiNovelTree(NTreeView): if (model := self._getModel()) and model.handle(index) == self._actHandle: painter.fillRect(opt.rect, self.palette().alternateBase()) super().drawRow(painter, opt, index) - return ## # Events @@ -527,7 +494,6 @@ class GuiNovelTree(NTreeView): """Process size changed.""" super().resizeEvent(event) self.resizeColumns() - return ## # Private Slots @@ -535,36 +501,33 @@ class GuiNovelTree(NTreeView): @pyqtSlot(QModelIndex) def _onSingleClick(self, index: QModelIndex) -> None: - """The user single-clicked an index.""" + """Process user single-click on an index.""" if index.isValid() and (model := self._getModel()): if (tHandle := model.handle(index)) and (sTitle := model.key(index)): self.novelView.selectedItemChanged.emit(tHandle) if index.column() == model.columnCount(index) - 1: pos = self.mapToGlobal(self.visualRect(index).topRight()) self._popMetaBox(pos, tHandle, sTitle) - return @pyqtSlot(QModelIndex) def _onDoubleClick(self, index: QModelIndex) -> None: - """The user double-clicked an index.""" + """Process user double-click on an index.""" if ( (model := self._getModel()) and (tHandle := model.handle(index)) and (sTitle := model.key(index)) ): self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle, False) - return @pyqtSlot(QModelIndex) def _onMiddleClick(self, index: QModelIndex) -> None: - """The user middle-clicked an index.""" + """Process user middle-click on an index.""" if ( (model := self._getModel()) and (tHandle := model.handle(index)) and (sTitle := model.key(index)) ): self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle, False) - return ## # Internal Functions @@ -582,7 +545,6 @@ class GuiNovelTree(NTreeView): """Generate a reference list for a given reference key.""" if tags := ", ".join(refs.get(key, [])): lines.append(f"{trConst(nwLabels.KEY_NAME[key])}: {tags}") - return if head := SHARED.project.index.getItemHeading(tHandle, sTitle): logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle) @@ -610,4 +572,3 @@ class GuiNovelTree(NTreeView): text = f"

    {refs}

    " if tooltip := (text + synopsis or self.tr("No meta data")): QToolTip.showText(qPos, tooltip) - return diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 4ab18e50..3df01d3f 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -24,7 +24,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import csv @@ -58,6 +58,7 @@ logger = logging.getLogger(__name__) class GuiOutlineView(QWidget): + """GUI: Project Outline Panel.""" loadDocumentTagRequest = pyqtSignal(str, Enum) openDocumentRequest = pyqtSignal(str, Enum, str, bool) @@ -96,8 +97,6 @@ class GuiOutlineView(QWidget): # Function Mappings self.getSelectedHandle = self.outlineTree.getSelectedHandle - return - ## # Methods ## @@ -109,24 +108,20 @@ class GuiOutlineView(QWidget): self.outlineTree.refreshTree( rootHandle=SHARED.project.data.getLastHandle("outline"), overRide=True ) - return def initSettings(self) -> None: """Initialise GUI elements that depend on specific settings.""" self.outlineTree.initSettings() self.outlineData.initSettings() - return def refreshTree(self) -> None: """Refresh the current tree.""" self.outlineTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("outline")) - return def clearOutline(self) -> None: """Clear project-related GUI content.""" self.outlineData.clearDetails() self.outlineBar.setEnabled(False) - return def openProjectTasks(self) -> None: """Run open project tasks.""" @@ -142,8 +137,6 @@ class GuiOutlineView(QWidget): self.outlineBar.setEnabled(True) self.outlineData.loadGuiSettings() - return - def closeProjectTasks(self) -> None: """Run closing project tasks.""" if self.outlineTree.wasRendered: @@ -152,7 +145,6 @@ class GuiOutlineView(QWidget): self.outlineTree.closeProjectTasks() self.outlineData.updateClasses() self.clearOutline() - return def splitSizes(self) -> list[int]: """Get the sizes of the splitter widget.""" @@ -175,7 +167,6 @@ class GuiOutlineView(QWidget): """Handle tasks whenever a root folders changes.""" self.outlineBar.populateNovelList() self.outlineData.updateClasses() - return ## # Private Slots @@ -188,23 +179,21 @@ class GuiOutlineView(QWidget): of columns has changed. """ self.outlineBar.setColumnHiddenState(self.outlineTree.hiddenColumns) - return @pyqtSlot(str) def _tagClicked(self, link: str) -> None: """Capture the click of a tag in the details panel.""" if link: self.loadDocumentTagRequest.emit(link, nwDocMode.VIEW) - return @pyqtSlot(str) def _rootItemChanged(self, tHandle: str) -> None: """Handle root novel changed or needs to be refreshed.""" self.outlineTree.refreshTree(rootHandle=(tHandle or None), overRide=True) - return class GuiOutlineToolBar(QToolBar): + """GUI: Project Outline Panel ToolBar.""" loadNovelRootRequest = pyqtSignal(str) outlineExportRequest = pyqtSignal() @@ -263,8 +252,6 @@ class GuiOutlineToolBar(QToolBar): logger.debug("Ready: GuiOutlineToolBar") - return - ## # Methods ## @@ -278,22 +265,18 @@ class GuiOutlineToolBar(QToolBar): self.tbColumns.setIcon(SHARED.theme.getIcon("more_vertical")) self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}") self.novelLabel.setTextColors(color=self.palette().windowText().color()) - return def populateNovelList(self) -> None: """Reload the content of the novel list.""" self.novelValue.refreshNovelList() - return def setCurrentRoot(self, rootHandle: str | None) -> None: """Set the current active root handle.""" self.novelValue.setHandle(rootHandle) - return def setColumnHiddenState(self, hiddenState: dict[nwOutline, bool]) -> None: """Forward the change of column hidden states to the menu.""" self.mColumns.setHiddenState(hiddenState) - return ## # Private Slots @@ -303,22 +286,20 @@ class GuiOutlineToolBar(QToolBar): def _novelValueChanged(self, tHandle: str) -> None: """Emit a signal containing the handle of the selected item.""" self.loadNovelRootRequest.emit(tHandle) - return @pyqtSlot() def _refreshRequested(self) -> None: """Emit a signal containing the handle of the selected item.""" self.loadNovelRootRequest.emit(self.novelValue.handle) - return @pyqtSlot() def _exportRequested(self) -> None: """Emit a signal that an export of the outline was requested.""" self.outlineExportRequest.emit() - return class GuiOutlineTree(QTreeWidget): + """GUI: Project Outline Panel Tree.""" DEF_WIDTH: Final[dict[nwOutline, int]] = { nwOutline.TITLE: 200, @@ -422,8 +403,6 @@ class GuiOutlineTree(QTreeWidget): logger.debug("Ready: GuiOutlineTree") - return - ## # Properties ## @@ -451,7 +430,6 @@ class GuiOutlineTree(QTreeWidget): self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff) else: self.setHorizontalScrollBarPolicy(QtScrollAsNeeded) - return def clearContent(self) -> None: """Clear the tree and header and set the default values for the @@ -474,8 +452,6 @@ class GuiOutlineTree(QTreeWidget): self._treeNCols = len(self._treeOrder) - return - def updateTheme(self) -> None: """Update theme elements.""" iType = nwItemType.FILE @@ -488,15 +464,14 @@ class GuiOutlineTree(QTreeWidget): "H3": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H3"), "H4": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H4"), } - return def refreshTree( self, rootHandle: str | None = None, overRide: bool = False, novelChanged: bool = False ) -> None: - """Called whenever the Outline tab is activated and controls - what data to load, and if necessary, force a rebuild of the - tree. + """Refresh the outline tree. Called whenever the Outline tab is + activated and controls what data to load, and if necessary, + force a rebuild of the tree. """ # If it's the first time, we always build if self._firstView or (self._firstView and overRide): @@ -518,11 +493,10 @@ class GuiOutlineTree(QTreeWidget): return def closeProjectTasks(self) -> None: - """Called before a project is closed.""" + """Call before a project is closed.""" self._saveHeaderState() self.clearContent() self._firstView = True - return def getSelectedHandle(self) -> tuple[str | None, str | None]: """Get the currently selected handle. If multiple items are @@ -546,7 +520,6 @@ class GuiOutlineTree(QTreeWidget): if hItem in self._colIdx: self.setColumnHidden(self._colIdx[hItem], not isChecked) self._saveHeaderState() - return @pyqtSlot() def exportOutline(self) -> None: @@ -562,7 +535,6 @@ class GuiOutlineTree(QTreeWidget): writer.writerows( self._dumpNovelData(self.outlineView.outlineBar.novelValue.handle) ) - return ## # Private Slots @@ -577,7 +549,6 @@ class GuiOutlineTree(QTreeWidget): tHandle, sTitle = self.getSelectedHandle() if tHandle: self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True) - return @pyqtSlot() def _onItemSelectionChanged(self) -> None: @@ -588,7 +559,6 @@ class GuiOutlineTree(QTreeWidget): tHandle = items[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE) sTitle = items[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE) self.activeItemChanged.emit(tHandle, sTitle) - return @pyqtSlot(int, int, int) def _columnMoved(self, logIdx: int, oldVisualIdx: int, newVisualIdx: int) -> None: @@ -597,7 +567,6 @@ class GuiOutlineTree(QTreeWidget): """ self._treeOrder.insert(newVisualIdx, self._treeOrder.pop(oldVisualIdx)) self._saveHeaderState() - return ## # Internal Functions @@ -637,8 +606,6 @@ class GuiOutlineTree(QTreeWidget): self.hiddenStateChanged.emit() - return - def _saveHeaderState(self) -> None: """Save the state of the main tree header, that is, column order, column width and column hidden state. We don't want to @@ -661,7 +628,6 @@ class GuiOutlineTree(QTreeWidget): pOptions = SHARED.project.options pOptions.setValue("GuiOutline", "columnState", colState) pOptions.saveSettings() - return def _populateTree(self, rootHandle: str | None) -> None: """Build the tree based on the project index, and the header @@ -746,8 +712,6 @@ class GuiOutlineTree(QTreeWidget): self._lastBuild = time() logger.debug("Project outline built in %.3f ms", 1000.0*(time() - tStart)) - return - def _dumpNovelData(self, rootHandle: str | None) -> list[list[str | int]]: """Dump all novel data into a table.""" sLabel = SHARED.project.localLookup("Story Structure") @@ -821,6 +785,7 @@ class GuiOutlineTree(QTreeWidget): class GuiOutlineHeaderMenu(QMenu): + """GUI: Project Outline Panel Header Selection Menu.""" columnToggled = pyqtSignal(bool, Enum) @@ -844,8 +809,6 @@ class GuiOutlineHeaderMenu(QMenu): ) self.addAction(self.actionMap[hItem]) - return - def setHiddenState(self, hiddenState: dict[nwOutline, bool]) -> None: """Overwrite the checked state of the columns as the inverse of the hidden state. Skip the TITLE column as it cannot be hidden. @@ -859,10 +822,9 @@ class GuiOutlineHeaderMenu(QMenu): self.acceptToggle = True - return - class GuiOutlineDetails(QScrollArea): + """GUI: Project Outline Panel Details View.""" LVL_MAP: Final[dict[str, str]] = { "H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"), @@ -1007,8 +969,6 @@ class GuiOutlineDetails(QScrollArea): logger.debug("Ready: GuiOutlineDetails") - return - def initSettings(self) -> None: """Set or update outline settings.""" if CONFIG.hideVScroll: @@ -1020,7 +980,6 @@ class GuiOutlineDetails(QScrollArea): else: self.setHorizontalScrollBarPolicy(QtScrollAsNeeded) self.updateClasses() - return def loadGuiSettings(self) -> None: """Run open project tasks.""" @@ -1031,7 +990,6 @@ class GuiOutlineDetails(QScrollArea): pOptions.getInt("GuiOutlineDetails", "detailsWidth", width//3), pOptions.getInt("GuiOutlineDetails", "tagsWidth", 2*width//3), ]) - return def saveGuiSettings(self) -> None: """Run close project tasks.""" @@ -1040,7 +998,6 @@ class GuiOutlineDetails(QScrollArea): pOptions = SHARED.project.options pOptions.setValue("GuiOutlineDetails", "detailsWidth", mainSplit[0]) pOptions.setValue("GuiOutlineDetails", "tagsWidth", mainSplit[1]) - return def clearDetails(self) -> None: """Clear all the data labels.""" @@ -1057,7 +1014,6 @@ class GuiOutlineDetails(QScrollArea): value.clear() self.updateClasses() - return ## # Slots @@ -1090,8 +1046,6 @@ class GuiOutlineDetails(QScrollArea): for key, (_, value) in self.tagValues.items(): value.setText(self._formatTags(novRefs, key)) - return - @pyqtSlot() def updateClasses(self) -> None: """Update the visibility status of class details.""" @@ -1102,7 +1056,6 @@ class GuiOutlineDetails(QScrollArea): label, value = self.tagValues[key] label.setVisible(visible) value.setVisible(visible) - return @staticmethod def _formatTags(refs: dict[str, list[str]], key: str) -> str: diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 4565e315..ab79cfb6 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -25,7 +25,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -61,7 +61,9 @@ logger = logging.getLogger(__name__) class GuiProjectView(QWidget): - """This is a wrapper class holding all the elements of the project + """GUI: Project View. + + This is a wrapper class holding all the elements of the project tree. The core object is the project tree itself. Most methods available are mapped through to the project tree class. """ @@ -130,8 +132,6 @@ class GuiProjectView(QWidget): # Function Mappings self.getSelectedHandle = self.projTree.getSelectedHandle - return - ## # Methods ## @@ -139,19 +139,16 @@ class GuiProjectView(QWidget): def updateTheme(self) -> None: """Update theme elements.""" self.projBar.updateTheme() - return def initSettings(self) -> None: """Initialise GUI elements that depend on specific settings.""" self.projTree.initSettings() - return def closeProjectTasks(self) -> None: """Clear project-related GUI content.""" self.projBar.clearContent() self.projBar.setEnabled(False) self.projTree.clearTree() - return def openProjectTasks(self) -> None: """Run open project tasks.""" @@ -159,26 +156,23 @@ class GuiProjectView(QWidget): self.projBar.buildTemplatesMenu() self.projBar.buildQuickLinksMenu() self.projBar.setEnabled(True) - return def setTreeFocus(self) -> None: """Forward the set focus call to the tree widget.""" self.projTree.setFocus() - return def treeHasFocus(self) -> bool: """Check if the project tree has focus.""" return self.projTree.hasFocus() def connectMenuActions(self, rename: QAction, delete: QAction, trash: QAction) -> None: - """Main menu actions passed to the project tree.""" + """Connect main menu actions passed to the project tree.""" self.projTree.addAction(rename) self.projTree.addAction(delete) self.projTree.addAction(trash) rename.triggered.connect(self.renameTreeItem) delete.triggered.connect(self.projTree.processDeleteRequest) trash.triggered.connect(self.projTree.emptyTrash) - return ## # Public Slots @@ -197,41 +191,36 @@ class GuiProjectView(QWidget): if dlgOk: nwItem.setName(newLabel) nwItem.notifyToRefresh() - return @pyqtSlot(str, bool) def setSelectedHandle(self, tHandle: str, doScroll: bool = False) -> None: """Select an item and optionally scroll it into view.""" self.projTree.setSelectedHandle(tHandle, doScroll=doScroll) - return @pyqtSlot(str) def setActiveHandle(self, tHandle: str | None) -> None: """Highlight the active handle.""" self.projTree.setActiveHandle(tHandle) - return @pyqtSlot(str, Enum) def onProjectItemChanged(self, tHandle: str, change: nwChange) -> None: """Refresh other content when project item changed.""" self.projBar.processTemplateDocuments(tHandle) - return @pyqtSlot(str) def createFileFromTemplate(self, tHandle: str) -> None: """Create a new document from a template.""" logger.debug("Template selected: '%s'", tHandle) self.projTree.newTreeItem(nwItemType.FILE, copyDoc=tHandle) - return @pyqtSlot(str, Enum) def updateRootItem(self, tHandle: str, change: nwChange) -> None: """Process root item changes.""" self.projBar.buildQuickLinksMenu() - return class GuiProjectToolBar(QWidget): + """GUI> Project View ToolBar.""" newDocumentFromTemplate = pyqtSignal(str) @@ -351,8 +340,6 @@ class GuiProjectToolBar(QWidget): logger.debug("Ready: GuiProjectToolBar") - return - ## # Methods ## @@ -383,13 +370,10 @@ class GuiProjectToolBar(QWidget): self.buildQuickLinksMenu() self._buildRootMenu() - return - def clearContent(self) -> None: """Clear dynamic content on the tool bar.""" self.mQuick.clear() self.mTemplates.clearMenu() - return def buildQuickLinksMenu(self) -> None: """Build the quick link menu.""" @@ -402,14 +386,12 @@ class GuiProjectToolBar(QWidget): action.triggered.connect( qtLambda(self.projView.setSelectedHandle, tHandle, doScroll=True) ) - return def buildTemplatesMenu(self) -> None: """Build the templates menu.""" for tHandle, _ in SHARED.project.tree.iterRoots(nwItemClass.TEMPLATE): for dHandle in SHARED.project.tree.subTree(tHandle): self.processTemplateDocuments(dHandle) - return def processTemplateDocuments(self, tHandle: str) -> None: """Process change in tree items to update menu content.""" @@ -418,7 +400,6 @@ class GuiProjectToolBar(QWidget): self.mTemplates.addUpdate(tHandle, item.itemName, item.getMainIcon()) elif tHandle in self.mTemplates: self.mTemplates.remove(tHandle) - return ## # Public Slots @@ -436,7 +417,6 @@ class GuiProjectToolBar(QWidget): self.aAddChap.setVisible(allowDoc) self.aAddPart.setVisible(allowDoc) self.aAddEmpty.setVisible(allowDoc) - return ## # Internal Functions @@ -451,7 +431,6 @@ class GuiProjectToolBar(QWidget): qtLambda(self.projTree.newTreeItem, nwItemType.ROOT, itemClass) ) self.mAddRoot.addAction(aNew) - return self.mAddRoot.clear() addClass(nwItemClass.NOVEL) @@ -467,10 +446,9 @@ class GuiProjectToolBar(QWidget): addClass(nwItemClass.ARCHIVE) addClass(nwItemClass.TEMPLATE) - return - class GuiProjectTree(QTreeView): + """GUI: Project View Tree.""" def __init__(self, projView: GuiProjectView) -> None: super().__init__(parent=projView) @@ -520,8 +498,6 @@ class GuiProjectTree(QTreeView): logger.debug("Ready: GuiProjectTree") - return - def initSettings(self) -> None: """Set or update tree widget settings.""" if CONFIG.hideVScroll: @@ -532,7 +508,6 @@ class GuiProjectTree(QTreeView): self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff) else: self.setHorizontalScrollBarPolicy(QtScrollAsNeeded) - return ## # External Methods @@ -541,7 +516,6 @@ class GuiProjectTree(QTreeView): def setActiveHandle(self, tHandle: str | None) -> None: """Set the handle to be highlighted.""" self._actHandle = tHandle - return def getSelectedHandle(self) -> str | None: """Get the currently selected handle.""" @@ -556,7 +530,6 @@ class GuiProjectTree(QTreeView): def clearTree(self) -> None: """Clear the tree view.""" self.setModel(None) - return def loadModel(self) -> None: """Load and prepare a new project model.""" @@ -583,8 +556,6 @@ class GuiProjectTree(QTreeView): self.restoreExpandedState() - return - def restoreExpandedState(self) -> None: """Expand all nodes that were previously expanded.""" if model := self._getModel(): @@ -592,7 +563,6 @@ class GuiProjectTree(QTreeView): for index in model.allExpanded(): self.setExpanded(index, True) self.blockSignals(False) - return def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> None: """Set a specific handle as the selected item.""" @@ -601,7 +571,6 @@ class GuiProjectTree(QTreeView): if doScroll: self.scrollTo(index, QAbstractItemView.ScrollHint.PositionAtCenter) self.projView.selectedItemChanged.emit(tHandle) - return def newTreeItem( self, itemType: nwItemType, itemClass: nwItemClass | None = None, @@ -667,7 +636,7 @@ class GuiProjectTree(QTreeView): if itemType == nwItemType.FILE: if tHandle := SHARED.project.newFile(newLabel, sHandle, sPos): if copyDoc: - SHARED.project.copyFileContent(tHandle, copyDoc) + SHARED.project.copyFileContent(tHandle, copyDoc, newLabel) elif hLevel > 0: SHARED.project.writeNewFile(tHandle, hLevel, not nNote) SHARED.project.index.reIndexHandle(tHandle) @@ -718,9 +687,15 @@ class GuiProjectTree(QTreeView): else: return False + SHARED.initMainProgress(len(items)) + self.setEnabled(False) for sHandle in items: + SHARED.incMainProgress() docMerger.appendText(sHandle, True, mLabel) + self.setEnabled(True) + SHARED.clearMainProgress() + if not docMerger.writeTargetDoc(): SHARED.error( self.tr("Could not write document content."), @@ -764,7 +739,10 @@ class GuiProjectTree(QTreeView): docSplit.setParentItem(tItem.itemParent) docSplit.splitDocument(headerList, text) + SHARED.initMainProgress(len(docSplit)) + self.setEnabled(False) for writeOk in docSplit.writeDocuments(docHierarchy): + SHARED.incMainProgress() if not writeOk: SHARED.error( self.tr("Could not write document content."), @@ -774,6 +752,9 @@ class GuiProjectTree(QTreeView): if data.get("moveToTrash", False): self.processDeleteRequest([tHandle], False) + self.setEnabled(True) + SHARED.clearMainProgress() + return True def duplicateFromHandle(self, tHandle: str) -> None: @@ -786,12 +767,13 @@ class GuiProjectTree(QTreeView): else: question = self.tr("Do you want to duplicate this item and all child items?") if SHARED.question(question): + self.setEnabled(False) docDup = DocDuplicator(SHARED.project) dHandles = docDup.duplicate(itemTree) if len(dHandles) != len(itemTree): SHARED.warn(self.tr("Could not duplicate all items.")) + self.setEnabled(True) self.restoreExpandedState() - return ## # Events and Overloads @@ -811,14 +793,12 @@ class GuiProjectTree(QTreeView): self.projView.openDocumentRequest.emit( node.item.itemHandle, nwDocMode.VIEW, "", False ) - return def drawRow(self, painter: QPainter, opt: QStyleOptionViewItem, index: QModelIndex) -> None: """Draw a box on the active row.""" if (node := self._getNode(index)) and node.item.itemHandle == self._actHandle: painter.fillRect(opt.rect, self.palette().alternateBase()) super().drawRow(painter, opt, index) - return ## # Public Slots @@ -829,14 +809,12 @@ class GuiProjectTree(QTreeView): """Move an item up in the tree.""" if model := self._getModel(): model.internalMove(self.currentIndex(), -1) - return @pyqtSlot() def moveItemDown(self) -> None: """Move an item down in the tree.""" if model := self._getModel(): model.internalMove(self.currentIndex(), 1) - return @pyqtSlot() def goToSiblingUp(self) -> None: @@ -844,7 +822,6 @@ class GuiProjectTree(QTreeView): if (node := self._getNode(self.currentIndex())) and (parent := node.parent()): if (move := parent.child(node.row() - 1)) and (model := self._getModel()): self.setCurrentIndex(model.indexFromNode(move)) - return @pyqtSlot() def goToSiblingDown(self) -> None: @@ -852,7 +829,6 @@ class GuiProjectTree(QTreeView): if (node := self._getNode(self.currentIndex())) and (parent := node.parent()): if (move := parent.child(node.row() + 1)) and (model := self._getModel()): self.setCurrentIndex(model.indexFromNode(move)) - return @pyqtSlot() def goToParent(self) -> None: @@ -863,7 +839,6 @@ class GuiProjectTree(QTreeView): and (parent := node.parent()) ): self.setCurrentIndex(model.indexFromNode(parent)) - return @pyqtSlot() def goToFirstChild(self) -> None: @@ -874,13 +849,11 @@ class GuiProjectTree(QTreeView): and (child := node.child(0)) ): self.setCurrentIndex(model.indexFromNode(child)) - return @pyqtSlot(QModelIndex) def expandFromIndex(self, index: QModelIndex) -> None: """Expand all nodes from index.""" self.expandRecursively(index) - return @pyqtSlot(QModelIndex) def collapseFromIndex(self, index: QModelIndex) -> None: @@ -888,7 +861,6 @@ class GuiProjectTree(QTreeView): if (model := self._getModel()) and (node := model.node(index)): for child in node.allChildren(): self.setExpanded(model.indexFromNode(child), False) - return @pyqtSlot() def processDeleteRequest( @@ -912,17 +884,23 @@ class GuiProjectTree(QTreeView): if not SHARED.question(self.tr("Permanently delete selected item(s)?")): logger.info("Action cancelled by user") return + + self.setEnabled(False) for index in indices: if node := model.node(index): for child in reversed(node.allChildren()): SHARED.project.removeItem(child.item.itemHandle) SHARED.project.removeItem(node.item.itemHandle) + self.setEnabled(True) elif trashNode := SHARED.project.tree.trash: if askFirst and not SHARED.question(self.tr("Move selected item(s) to Trash?")): logger.info("Action cancelled by user") return + + self.setEnabled(False) model.multiMove(indices, model.indexFromNode(trashNode)) + self.setEnabled(True) return @@ -948,9 +926,7 @@ class GuiProjectTree(QTreeView): @pyqtSlot() @pyqtSlot("QPoint") def openContextMenu(self, point: QPoint | None = None) -> None: - """The user right clicked an element in the project tree, so we - open a context menu in-place. - """ + """Open a context menu in-place where the user clicked.""" if model := self._getModel(): if point is None: point = self.visualRect(self.currentIndex()).center() @@ -967,7 +943,6 @@ class GuiProjectTree(QTreeView): if viewport := self.viewport(): ctxMenu.exec(viewport.mapToGlobal(point)) ctxMenu.setParent(None) - return ## # Private Slots @@ -975,10 +950,9 @@ class GuiProjectTree(QTreeView): @pyqtSlot(QModelIndex, QModelIndex) def _onSelectionChange(self, current: QModelIndex, previous: QModelIndex) -> None: - """The user changed which item is selected.""" + """Process user changing which item is selected.""" if node := self._getNode(current): self.projView.selectedItemChanged.emit(node.item.itemHandle) - return @pyqtSlot(QModelIndex) def _onDoubleClick(self, index: QModelIndex) -> None: @@ -992,21 +966,18 @@ class GuiProjectTree(QTreeView): ) else: self.setExpanded(index, not self.isExpanded(index)) - return @pyqtSlot(QModelIndex) def _onNodeCollapsed(self, index: QModelIndex) -> None: """Capture a node collapse, and pass it to the model.""" if node := self._getNode(index): node.setExpanded(False) - return @pyqtSlot(QModelIndex) def _onNodeExpanded(self, index: QModelIndex) -> None: """Capture a node expand, and pass it to the model.""" if node := self._getNode(index): node.setExpanded(True) - return ## # Internal Functions @@ -1018,7 +989,6 @@ class GuiProjectTree(QTreeView): if model := self.selectionModel(): # Selection model can be None (#2173) model.clearCurrentIndex() - return def _selectedRows(self) -> list[QModelIndex]: """Return all column 0 indexes.""" @@ -1046,7 +1016,6 @@ class _UpdatableMenu(QMenu): self._map: dict[str, QAction] = {} self.setTitle(self.tr("From Template")) self.triggered.connect(self._actionTriggered) - return def __contains__(self, tHandle: str) -> bool: """Look up a handle in the menu.""" @@ -1068,7 +1037,6 @@ class _UpdatableMenu(QMenu): self.addAction(action) self._map[tHandle] = action self.setActionsVisible(True) - return def remove(self, tHandle: str) -> None: """Remove a template item.""" @@ -1076,19 +1044,16 @@ class _UpdatableMenu(QMenu): self.removeAction(action) if not self._map: self.setActionsVisible(False) - return def clearMenu(self) -> None: """Clear all menu content.""" self._map.clear() self.clear() - return def setActionsVisible(self, value: bool) -> None: """Set the visibility of root action.""" if action := self.menuAction(): action.setVisible(value) - return ## # Private Slots @@ -1098,7 +1063,6 @@ class _UpdatableMenu(QMenu): def _actionTriggered(self, action: QAction) -> None: """Translate the menu trigger into an item trigger.""" self.menuItemTriggered.emit(str(action.data())) - return class _TreeContextMenu(QMenu): @@ -1119,11 +1083,9 @@ class _TreeContextMenu(QMenu): self._indices = indices self._children = node.childCount() > 0 logger.debug("Ready: _TreeContextMenu") - return def __del__(self) -> None: # pragma: no cover logger.debug("Delete: _TreeContextMenu") - return ## # Methods @@ -1135,7 +1097,6 @@ class _TreeContextMenu(QMenu): action.triggered.connect(self._tree.emptyTrash) if self._children: self._expandCollapse() - return def buildSingleSelectMenu(self) -> None: """Build the single-select menu.""" @@ -1171,15 +1132,12 @@ class _TreeContextMenu(QMenu): action.triggered.connect(qtLambda(self._tree.duplicateFromHandle, self._handle)) self._deleteOrTrash() - return - def buildMultiSelectMenu(self) -> None: """Build the multi-select menu.""" self._itemActive() self._itemStatusImport(True) self.addSeparator() self._deleteOrTrash() - return ## # Menu Builders @@ -1197,7 +1155,6 @@ class _TreeContextMenu(QMenu): self._view.openDocumentRequest.emit, self._handle, nwDocMode.VIEW, "", False )) - return def _itemCreation(self) -> None: """Add create item actions.""" @@ -1208,7 +1165,6 @@ class _TreeContextMenu(QMenu): menu.addAction(self._view.projBar.aAddEmpty) menu.addAction(self._view.projBar.aAddNote) menu.addAction(self._view.projBar.aAddFolder) - return def _itemHeader(self) -> None: """Check if there is a header that can be used for rename.""" @@ -1218,7 +1174,6 @@ class _TreeContextMenu(QMenu): action.triggered.connect( qtLambda(self._view.renameTreeItem, self._handle, hItem.title) ) - return def _itemActive(self) -> None: """Add Active/Inactive actions.""" @@ -1233,7 +1188,6 @@ class _TreeContextMenu(QMenu): else: action = qtAddAction(self, self.tr("Toggle Active")) action.triggered.connect(self._toggleItemActive) - return def _itemStatusImport(self, multi: bool) -> None: """Add actions for changing status or importance.""" @@ -1275,7 +1229,6 @@ class _TreeContextMenu(QMenu): self._view.projectSettingsRequest.emit, GuiProjectSettings.PAGE_IMPORT )) - return def _itemTransform(self, isFile: bool, isFolder: bool) -> None: """Add actions for the Transform menu.""" @@ -1318,15 +1271,12 @@ class _TreeContextMenu(QMenu): action = qtAddAction(menu, self.tr("Split Document by Headings")) action.triggered.connect(qtLambda(self._tree.splitDocument, self._handle)) - return - def _expandCollapse(self) -> None: """Add actions for expand and collapse.""" action = qtAddAction(self, self.tr("Expand All")) action.triggered.connect(qtLambda(self._tree.expandFromIndex, self._indices[0])) action = qtAddAction(self, self.tr("Collapse All")) action.triggered.connect(qtLambda(self._tree.collapseFromIndex, self._indices[0])) - return def _deleteOrTrash(self) -> None: """Add move to Trash action.""" @@ -1339,7 +1289,6 @@ class _TreeContextMenu(QMenu): text = self.tr("Move to Trash") action = qtAddAction(self, text) action.triggered.connect(self._tree.processDeleteRequest) - return ## # Private Slots @@ -1351,7 +1300,6 @@ class _TreeContextMenu(QMenu): if self._item.isFileType(): self._item.setActive(not self._item.isActive) self._item.notifyToRefresh() - return ## # Internal Functions @@ -1365,13 +1313,11 @@ class _TreeContextMenu(QMenu): node.item.setActive(state) refresh.append(node.item.itemHandle) SHARED.project.tree.refreshItems(refresh) - return def _changeItemStatus(self, key: str) -> None: """Set a new status value of an item.""" self._item.setStatus(key) self._item.notifyToRefresh() - return def _iterSetItemStatus(self, key: str) -> None: """Change the status value for multiple items.""" @@ -1381,13 +1327,11 @@ class _TreeContextMenu(QMenu): node.item.setStatus(key) refresh.append(node.item.itemHandle) SHARED.project.tree.refreshItems(refresh) - return def _changeItemImport(self, key: str) -> None: """Set a new importance value of an item.""" self._item.setImport(key) self._item.notifyToRefresh() - return def _iterSetItemImport(self, key: str) -> None: """Change the status value for multiple items.""" @@ -1397,7 +1341,6 @@ class _TreeContextMenu(QMenu): node.item.setImport(key) refresh.append(node.item.itemHandle) SHARED.project.tree.refreshItems(refresh) - return def _changeItemLayout(self, itemLayout: nwItemLayout) -> None: """Set a new item layout value of an item.""" @@ -1408,7 +1351,6 @@ class _TreeContextMenu(QMenu): elif itemLayout == nwItemLayout.NOTE: self._item.setLayout(nwItemLayout.NOTE) self._item.notifyToRefresh() - return def _convertFolderToFile(self, itemLayout: nwItemLayout) -> None: """Convert a folder to a note or document.""" @@ -1428,4 +1370,3 @@ class _TreeContextMenu(QMenu): self._item.notifyToRefresh() else: logger.info("Folder conversion cancelled") - return diff --git a/novelwriter/gui/search.py b/novelwriter/gui/search.py index 51bf1a15..5313d676 100644 --- a/novelwriter/gui/search.py +++ b/novelwriter/gui/search.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -50,6 +50,7 @@ logger = logging.getLogger(__name__) class GuiProjectSearch(QWidget): + """GUI: Project Search Panel.""" C_NAME = 0 C_RESULT = 0 @@ -151,8 +152,6 @@ class GuiProjectSearch(QWidget): logger.debug("Ready: GuiProjectSearch") - return - ## # Methods ## @@ -174,8 +173,6 @@ class GuiProjectSearch(QWidget): self.toggleWord.setIcon(SHARED.theme.getIcon("search_word")) self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex")) - return - def processReturn(self) -> None: """Process a return keypress forwarded from the main GUI.""" if self.searchText.hasFocus(): @@ -189,7 +186,6 @@ class GuiProjectSearch(QWidget): self.openDocumentSelectRequest.emit( str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False ) - return def beginSearch(self, text: str = "") -> None: """Focus the search box and select its text, if any.""" @@ -198,20 +194,17 @@ class GuiProjectSearch(QWidget): if text: self.searchText.setText(text.partition("\n")[0]) self.searchText.selectAll() - return def closeProjectTasks(self) -> None: """Run close project tasks.""" self._map = {} self.searchText.clear() self.searchResult.clear() - return def refreshCurrentSearch(self) -> None: """Refresh the search if there is one.""" if self.searchResult.topLevelItemCount() > 0: self._processSearch() - return ## # Events @@ -238,7 +231,6 @@ class GuiProjectSearch(QWidget): self.searchText.setFocus() else: super().keyPressEvent(event) - return ## # Public Slots @@ -252,7 +244,6 @@ class GuiProjectSearch(QWidget): results, capped = self._search.searchText(SHARED.mainGui.docEditor.getText()) self._displayResultSet(SHARED.project.tree[tHandle], results, capped) logger.debug("Updated search for '%s' in %.3f ms", tHandle, 1000*(time() - start)) - return ## # Private Slots @@ -278,7 +269,6 @@ class GuiProjectSearch(QWidget): self._time = time() QApplication.restoreOverrideCursor() self._blocked = False - return @pyqtSlot() def _searchResultSelected(self) -> None: @@ -288,7 +278,6 @@ class GuiProjectSearch(QWidget): self.selectedItemChanged.emit(str(data[0])) elif data := items[0].data(0, self.D_HANDLE): self.selectedItemChanged.emit(str(data)) - return @pyqtSlot("QTreeWidgetItem*", int) def _searchResultDoubleClicked(self, item: QTreeWidgetItem, column: int) -> None: @@ -297,28 +286,24 @@ class GuiProjectSearch(QWidget): self.openDocumentSelectRequest.emit( str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), True ) - return @pyqtSlot(bool) def _toggleCase(self, state: bool) -> None: """Enable/disable case sensitive mode.""" CONFIG.searchProjCase = state self.refreshCurrentSearch() - return @pyqtSlot(bool) def _toggleWord(self, state: bool) -> None: """Enable/disable whole word search mode.""" CONFIG.searchProjWord = state self.refreshCurrentSearch() - return @pyqtSlot(bool) def _toggleRegEx(self, state: bool) -> None: """Enable/disable regular expression search mode.""" CONFIG.searchProjRegEx = state self.refreshCurrentSearch() - return ## # Internal Functions @@ -360,5 +345,3 @@ class GuiProjectSearch(QWidget): self.searchResult.setFirstColumnSpanned(i, parent, True) QApplication.processEvents() - - return diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index f784623f..6440fc15 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -20,19 +20,20 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging from typing import TYPE_CHECKING -from PyQt6.QtCore import QEvent, QPoint, QSize, pyqtSignal +from PyQt6.QtCore import QEvent, QPoint, QSize, pyqtSignal, pyqtSlot from PyQt6.QtWidgets import QMenu, QVBoxLayout, QWidget -from novelwriter import SHARED +from novelwriter import CONFIG, SHARED from novelwriter.common import qtLambda -from novelwriter.enum import nwView +from novelwriter.constants import nwLabels, trConst +from novelwriter.enum import nwTheme, nwView from novelwriter.extensions.eventfilters import StatusTipFilter from novelwriter.extensions.modified import NIconToolButton from novelwriter.gui.theme import STYLES_BIG_TOOLBUTTON @@ -44,6 +45,7 @@ logger = logging.getLogger(__name__) class GuiSideBar(QWidget): + """GUI: Main Window SideBar.""" requestViewChange = pyqtSignal(nwView) @@ -77,9 +79,9 @@ class GuiSideBar(QWidget): self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View"))) self.tbOutline.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.OUTLINE)) - self.tbBuild = NIconToolButton(self, iSz) - self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript"))) - self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog) + self.tbTheme = NIconToolButton(self, iSz) + self.tbTheme.setToolTip(self.tr("Switch Colour Theme")) + self.tbTheme.clicked.connect(self._cycleColurTheme) self.tbDetails = NIconToolButton(self, iSz) self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details"))) @@ -89,6 +91,10 @@ class GuiSideBar(QWidget): self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics"))) self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog) + self.tbBuild = NIconToolButton(self, iSz) + self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript"))) + self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog) + # Settings Menu self.tbSettings = NIconToolButton(self, iSz) self.tbSettings.setToolTip(self.tr("Settings")) @@ -111,6 +117,7 @@ class GuiSideBar(QWidget): self.outerBox.addStretch(1) self.outerBox.addWidget(self.tbDetails) self.outerBox.addWidget(self.tbStats) + self.outerBox.addWidget(self.tbTheme) self.outerBox.addWidget(self.tbSettings) self.outerBox.setContentsMargins(0, 0, 0, 0) self.outerBox.setSpacing(6) @@ -120,8 +127,6 @@ class GuiSideBar(QWidget): logger.debug("Ready: GuiSideBar") - return - def updateTheme(self) -> None: """Initialise GUI elements that depend on specific settings.""" buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON) @@ -133,6 +138,7 @@ class GuiSideBar(QWidget): self.tbBuild.setStyleSheet(buttonStyle) self.tbDetails.setStyleSheet(buttonStyle) self.tbStats.setStyleSheet(buttonStyle) + self.tbTheme.setStyleSheet(buttonStyle) self.tbSettings.setStyleSheet(buttonStyle) self.tbProject.setThemeIcon("sb_project") @@ -144,7 +150,33 @@ class GuiSideBar(QWidget): self.tbStats.setThemeIcon("sb_stats") self.tbSettings.setThemeIcon("settings") - return + self._setThemeModeIcon() + + ## + # Private Slots + ## + + @pyqtSlot() + def _cycleColurTheme(self) -> None: + """Go to nex colour theme.""" + match CONFIG.themeMode: + case nwTheme.AUTO: + CONFIG.themeMode = nwTheme.LIGHT + case nwTheme.LIGHT: + CONFIG.themeMode = nwTheme.DARK + case nwTheme.DARK: + CONFIG.themeMode = nwTheme.AUTO + self.mainGui.checkThemeUpdate() + self._setThemeModeIcon() + + ## + # Internal Functions + ## + + def _setThemeModeIcon(self) -> None: + """Set the theme button icon.""" + self.tbTheme.setThemeIcon(nwLabels.THEME_MODE_ICON[CONFIG.themeMode]) + self.tbTheme.setToolTip(trConst(nwLabels.THEME_MODE_LABEL[CONFIG.themeMode])) class _PopRightMenu(QMenu): diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index 3239fd4a..0c8288b5 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -41,6 +41,7 @@ logger = logging.getLogger(__name__) class GuiMainStatus(QStatusBar): + """GUI: Main Window Status Bar.""" def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -112,8 +113,6 @@ class GuiMainStatus(QStatusBar): self.updateTheme() self.clearStatus() - return - def initSettings(self) -> None: """Apply user settings.""" if CONFIG.useCharCount: @@ -122,7 +121,6 @@ class GuiMainStatus(QStatusBar): else: self._trStatsCount = trStats(nwLabels.STATS_DISPLAY[nwStats.WORDS]) self._trStatsTip = self.tr("Total word count (session change)") - return def clearStatus(self) -> None: """Reset all widgets on the status bar to default values.""" @@ -132,7 +130,6 @@ class GuiMainStatus(QStatusBar): self.setProjectStatus(None) self.setDocumentStatus(None) self.updateTime() - return def updateTheme(self) -> None: """Update theme elements.""" @@ -143,14 +140,12 @@ class GuiMainStatus(QStatusBar): self.idlePixmap = SHARED.theme.getPixmap("timer_off", (iPx, iPx)) self.timeIcon.setPixmap(self.timePixmap) - colNone = SHARED.theme.getIconColor("default").darker(150) - colSaved = SHARED.theme.getIconColor("green").darker(150) - colUnsaved = SHARED.theme.getIconColor("red").darker(150) + colNone = SHARED.theme.getBaseColor("default").darker(150) + colSaved = SHARED.theme.getBaseColor("green").darker(150) + colUnsaved = SHARED.theme.getBaseColor("red").darker(150) self.docIcon.setColors(colNone, colSaved, colUnsaved) self.projIcon.setColors(colNone, colSaved, colUnsaved) - return - ## # Setters ## @@ -158,17 +153,14 @@ class GuiMainStatus(QStatusBar): def setRefTime(self, refTime: float) -> None: """Set the reference time for the status bar clock.""" self._refTime = refTime - return def setProjectStatus(self, state: bool | None) -> None: """Set the project status colour icon.""" self.projIcon.setState(state) - return def setDocumentStatus(self, state: bool | None) -> None: """Set the document status colour icon.""" self.docIcon.setState(state) - return def setUserIdle(self, idle: bool) -> None: """Change the idle status icon.""" @@ -180,13 +172,11 @@ class GuiMainStatus(QStatusBar): else: self.timeIcon.setPixmap(self.timePixmap) self._userIdle = idle - return def setProjectStats(self, pWC: int, sWC: int) -> None: """Update the current project statistics.""" self.statsText.setText(self._trStatsCount.format(f"{pWC:n}", f"{sWC:+n}")) self.statsText.setToolTip(self._trStatsTip) - return def updateTime(self, idleTime: float = 0.0) -> None: """Update the session clock.""" @@ -198,7 +188,6 @@ class GuiMainStatus(QStatusBar): else: sessTime = round(time() - self._refTime) self.timeText.setText(formatTime(sessTime)) - return ## # Public Slots @@ -209,7 +198,6 @@ class GuiMainStatus(QStatusBar): """Set the status bar message to display.""" self.showMessage(message, nwConst.STATUS_MSG_TIMEOUT) QApplication.processEvents() - return @pyqtSlot(str, str) def setLanguage(self, language: str, provider: str) -> None: @@ -220,19 +208,16 @@ class GuiMainStatus(QStatusBar): else: self.langText.setText(QLocale(language).nativeLanguageName().title()) self.langText.setToolTip(f"{language} ({provider})" if provider else language) - return @pyqtSlot(bool) def updateProjectStatus(self, status: bool) -> None: """Update the project status.""" self.setProjectStatus(not status) - return @pyqtSlot(bool) def updateDocumentStatus(self, status: bool) -> None: """Update the document status.""" self.setDocumentStatus(not status) - return ## # Private Slots @@ -244,7 +229,6 @@ class GuiMainStatus(QStatusBar): state = not CONFIG.showSessionTime self.timeText.setVisible(state) CONFIG.showSessionTime = state - return ## # Debug @@ -279,4 +263,3 @@ class GuiMainStatus(QStatusBar): ) self.showMessage(f"Debug [{stamp}] {message}", 6000) logger.debug("[MEMINFO] %s", message) - return diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 1eb2e487..b83de9b7 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -21,26 +21,28 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging +from configparser import ConfigParser +from dataclasses import dataclass from math import ceil from typing import TYPE_CHECKING, Final from PyQt6.QtCore import QSize, Qt from PyQt6.QtGui import ( - QColor, QFont, QFontDatabase, QFontMetrics, QIcon, QPainter, QPainterPath, - QPalette, QPixmap + QColor, QFont, QFontDatabase, QFontMetrics, QGuiApplication, QIcon, + QPainter, QPainterPath, QPalette, QPixmap ) from PyQt6.QtWidgets import QApplication from novelwriter import CONFIG -from novelwriter.common import NWConfigParser, minmax -from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX +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 +from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme from novelwriter.error import logException from novelwriter.types import QtBlack, QtHexArgb, QtPaintAntiAlias, QtTransparent @@ -54,21 +56,39 @@ STYLES_MIN_TOOLBUTTON = "minimalToolButton" STYLES_BIG_TOOLBUTTON = "bigToolButton" -class ThemeMeta: +@dataclass +class ThemeEntry: + """Theme data.""" - name: str = "" - description: str = "" - author: str = "" - credit: str = "" - url: str = "" - license: str = "" - licenseUrl: str = "" + name: str + dark: bool + path: Path + + +class ThemeMeta: + """Theme meta data.""" + + name: str = "" + mode: str = "" + author: str = "" + credit: str = "" + url: str = "" + + +class IconsMeta: + """Icon theme meta data.""" + + name: str = "" + author: str = "" + license: str = "" class SyntaxColors: + """Colours for the syntax highlighter.""" back: QColor = QColor(255, 255, 255) text: QColor = QColor(0, 0, 0) + line: QColor = QColor(0, 0, 0) link: QColor = QColor(0, 0, 0) head: QColor = QColor(0, 0, 0) headH: QColor = QColor(0, 0, 0) @@ -90,51 +110,47 @@ class SyntaxColors: class GuiTheme: - """Gui Theme Class + """Gui Theme Class. Handles the look and feel of novelWriter. """ __slots__ = ( - "_availSyntax", "_availThemes", "_guiPalette", "_styleSheets", "_syntaxList", "_themeList", - "baseButtonHeight", "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", - "fadedText", "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration", - "getHeaderDecorationNarrow", "getIcon", "getIconColor", "getItemIcon", "getPixmap", - "getToggleIcon", "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", - "helpText", "iconCache", "isDarkTheme", "syntaxMeta", "syntaxTheme", "textNHeight", - "textNWidth", "themeMeta", + "_allThemes", "_currentTheme", "_darkThemes", "_guiPalette", "_lightThemes", "_meta", + "_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", ) def __init__(self) -> None: - self.iconCache = GuiIcons(self) - - # GUI Theme - self.themeMeta = ThemeMeta() + # Theme Objects + self.iconCache = GuiIcons(self) + self.syntaxTheme = SyntaxColors() self.isDarkTheme = False - # Special Text Colours + # Special Colours self.helpText = QColor(0, 0, 0) self.fadedText = QColor(0, 0, 0) self.errorText = QColor(255, 0, 0) + self.accentCol = QColor(255, 0, 255) # Needed until we move to Qt 6.6 - # Syntax Theme - self.syntaxMeta = ThemeMeta() - self.syntaxTheme = SyntaxColors() - - # Load Themes + # Theme Data + self._meta = ThemeMeta() + self._currentTheme = "" self._guiPalette = QPalette() - self._themeList: list[tuple[str, str]] = [] - self._syntaxList: list[tuple[str, str]] = [] - self._availThemes: dict[str, Path] = {} - self._availSyntax: dict[str, Path] = {} + self._allThemes: dict[str, ThemeEntry] = {} self._styleSheets: dict[str, str] = {} + self._svgColors: dict[str, bytes] = {} + self._qColors: dict[str, QColor] = {} # Icon Functions self.getIcon = self.iconCache.getIcon self.getPixmap = self.iconCache.getPixmap self.getItemIcon = self.iconCache.getItemIcon - self.getIconColor = self.iconCache.getIconColor self.getToggleIcon = self.iconCache.getToggleIcon self.getDecoration = self.iconCache.getDecoration self.getHeaderDecoration = self.iconCache.getHeaderDecoration @@ -178,23 +194,21 @@ class GuiTheme: logger.debug("Text 'N' Height: %d", self.textNHeight) logger.debug("Text 'N' Width: %d", self.textNWidth) - # Process Themes - _listConf(self._availSyntax, CONFIG.assetPath("syntax"), ".conf") - _listConf(self._availThemes, CONFIG.assetPath("themes"), ".conf") - _listConf(self._availSyntax, CONFIG.dataPath("syntax"), ".conf") - _listConf(self._availThemes, CONFIG.dataPath("themes"), ".conf") + ## + # Properties + ## - self.loadTheme() - self.loadSyntax() - - return + @property + def colourThemes(self) -> dict[str, ThemeEntry]: + """Return a dictionary of all themes.""" + return self._allThemes ## - # Methods + # Getters ## def getTextWidth(self, text: str, font: QFont | None = None) -> int: - """Returns the width needed to contain a given piece of text in + """Return the width needed to contain a given piece of text in pixels. """ if isinstance(font, QFont): @@ -203,30 +217,77 @@ class GuiTheme: qMetrics = QFontMetrics(self.guiFont) return ceil(qMetrics.boundingRect(text).width()) + def getBaseColor(self, name: str) -> QColor: + """Return a base color.""" + return QColor(self._qColors.get(name) or QtBlack) + + def getRawBaseColor(self, name: str) -> bytes: + """Return a base color.""" + return self._svgColors.get(name, self._svgColors.get("default", b"#000000")) + ## # Theme Methods ## - def loadTheme(self) -> bool: - """Load the currently specified GUI theme.""" - theme = CONFIG.guiTheme - if theme not in self._availThemes: - logger.error("Could not find GUI theme '%s'", theme) - theme = DEF_GUI - CONFIG.guiTheme = theme + def initThemes(self) -> None: + """Initialise themes.""" + CONFIG.splashMessage("Scanning for colour themes ...") + themes: list[Path] = [] + _listContent(themes, CONFIG.assetPath("themes"), ".conf") + _listContent(themes, CONFIG.dataPath("themes"), ".conf") + self._scanThemes(themes) - if not (file := self._availThemes.get(theme)): + self.iconCache.initIcons() + self.loadTheme() + + def isDesktopDarkMode(self) -> bool: + """Check if the desktop is in dark mode.""" + if CONFIG.verQtValue >= 0x060500 and (hint := QGuiApplication.styleHints()): + return hint.colorScheme() == Qt.ColorScheme.Dark + + palette = QPalette() + text = palette.windowText().color() + window = palette.window().color() + return text.lightnessF() > window.lightnessF() + + def loadTheme(self, force: bool = False) -> bool: + """Load the currently specified GUI theme. The boolean return + can be used to determine if the GUI needs refreshing. + """ + match CONFIG.themeMode: + case nwTheme.LIGHT: + darkMode = False + case nwTheme.DARK: + darkMode = True + case _: + darkMode = self.isDesktopDarkMode() + + theme = CONFIG.darkTheme if darkMode else CONFIG.lightTheme + if theme not in self._allThemes: + logger.error("Could not find theme '%s'", theme) + if darkMode: + theme = DEF_GUI_DARK + CONFIG.darkTheme = DEF_GUI_DARK + else: + theme = DEF_GUI_LIGHT + CONFIG.lightTheme = DEF_GUI_LIGHT + + if theme == self._currentTheme and not force: + logger.info("Theme '%s' is already loaded", theme) + return False + + entry = self._allThemes.get(theme) + if not entry: logger.error("Could not load GUI theme") return False - CONFIG.splashMessage("Loading GUI theme ...") + CONFIG.splashMessage(f"Loading colour theme: {entry.name}") logger.info("Loading GUI theme '%s'", theme) - parser = NWConfigParser() + parser = ConfigParser() try: - with open(file, mode="r", encoding="utf-8") as fo: - parser.read_file(fo) + parser.read(entry.path, encoding="utf-8") except Exception: - logger.error("Could not read file: %s", file) + logger.error("Could not read file: %s", entry.path) logException() return False @@ -237,39 +298,41 @@ class GuiTheme: sec = "Main" meta = ThemeMeta() if parser.has_section(sec): - meta.name = parser.rdStr(sec, "name", "") - meta.description = parser.rdStr(sec, "description", "N/A") - meta.author = parser.rdStr(sec, "author", "N/A") - meta.credit = parser.rdStr(sec, "credit", "N/A") - meta.url = parser.rdStr(sec, "url", "") - meta.license = parser.rdStr(sec, "license", "N/A") - meta.licenseUrl = parser.rdStr(sec, "licenseurl", "") + meta.name = parser.get(sec, "name", fallback="") + meta.mode = parser.get(sec, "mode", fallback="light") + meta.author = parser.get(sec, "author", fallback="") + meta.credit = parser.get(sec, "credit", fallback="") + meta.url = parser.get(sec, "url", fallback="") - self.themeMeta = meta + self._meta = meta - # Icons - sec = "Icons" + # Base + sec = "Base" if parser.has_section(sec): - self.iconCache.setIconColor("default", self._parseColor(parser, sec, "default")) - self.iconCache.setIconColor("faded", self._parseColor(parser, sec, "faded")) - self.iconCache.setIconColor("red", self._parseColor(parser, sec, "red")) - self.iconCache.setIconColor("orange", self._parseColor(parser, sec, "orange")) - self.iconCache.setIconColor("yellow", self._parseColor(parser, sec, "yellow")) - self.iconCache.setIconColor("green", self._parseColor(parser, sec, "green")) - self.iconCache.setIconColor("aqua", self._parseColor(parser, sec, "aqua")) - self.iconCache.setIconColor("blue", self._parseColor(parser, sec, "blue")) - self.iconCache.setIconColor("purple", self._parseColor(parser, sec, "purple")) + self._setBaseColor("base", self._readColor(parser, sec, "base")) + self._setBaseColor("default", self._readColor(parser, sec, "default")) + self._setBaseColor("faded", self._readColor(parser, sec, "faded")) + self._setBaseColor("red", self._readColor(parser, sec, "red")) + self._setBaseColor("orange", self._readColor(parser, sec, "orange")) + self._setBaseColor("yellow", self._readColor(parser, sec, "yellow")) + self._setBaseColor("green", self._readColor(parser, sec, "green")) + self._setBaseColor("cyan", self._readColor(parser, sec, "cyan")) + self._setBaseColor("blue", self._readColor(parser, sec, "blue")) + self._setBaseColor("purple", self._readColor(parser, sec, "purple")) # Project sec = "Project" if parser.has_section(sec): - self.iconCache.setIconColor("root", self._parseColor(parser, sec, "root")) - self.iconCache.setIconColor("folder", self._parseColor(parser, sec, "folder")) - self.iconCache.setIconColor("file", self._parseColor(parser, sec, "file")) - self.iconCache.setIconColor("title", self._parseColor(parser, sec, "title")) - self.iconCache.setIconColor("chapter", self._parseColor(parser, sec, "chapter")) - self.iconCache.setIconColor("scene", self._parseColor(parser, sec, "scene")) - self.iconCache.setIconColor("note", self._parseColor(parser, sec, "note")) + self._setBaseColor("root", self._readColor(parser, sec, "root")) + self._setBaseColor("folder", self._readColor(parser, sec, "folder")) + self._setBaseColor("file", self._readColor(parser, sec, "file")) + self._setBaseColor("title", self._readColor(parser, sec, "title")) + self._setBaseColor("chapter", self._readColor(parser, sec, "chapter")) + self._setBaseColor("scene", self._readColor(parser, sec, "scene")) + self._setBaseColor("note", self._readColor(parser, sec, "note")) + self._setBaseColor("active", self._readColor(parser, sec, "active")) + self._setBaseColor("inactive", self._readColor(parser, sec, "inactive")) + self._setBaseColor("disabled", self._readColor(parser, sec, "disabled")) # Palette sec = "Palette" @@ -288,20 +351,46 @@ class GuiTheme: self._setPalette(parser, sec, "highlightedtext", QPalette.ColorRole.HighlightedText) self._setPalette(parser, sec, "link", QPalette.ColorRole.Link) self._setPalette(parser, sec, "linkvisited", QPalette.ColorRole.LinkVisited) + self.accentCol = self._readColor(parser, sec, "accent") # Special handling 'til Qt 6.6 # GUI sec = "GUI" if parser.has_section(sec): - self.helpText = self._parseColor(parser, sec, "helptext") - self.fadedText = self._parseColor(parser, sec, "fadedtext") - self.errorText = self._parseColor(parser, sec, "errortext") + self.helpText = self._readColor(parser, sec, "helptext") + self.fadedText = self._readColor(parser, sec, "fadedtext") + self.errorText = self._readColor(parser, sec, "errortext") + + # Syntax + sec = "Syntax" + self.syntaxTheme = SyntaxColors() + if parser.has_section(sec): + self.syntaxTheme.back = self._readColor(parser, sec, "background") + self.syntaxTheme.text = self._readColor(parser, sec, "text") + self.syntaxTheme.line = self._readColor(parser, sec, "line") + self.syntaxTheme.link = self._readColor(parser, sec, "link") + 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.dialN = self._readColor(parser, sec, "dialog") + self.syntaxTheme.dialA = self._readColor(parser, sec, "altdialog") + self.syntaxTheme.hidden = self._readColor(parser, sec, "hidden") + self.syntaxTheme.note = self._readColor(parser, sec, "note") + self.syntaxTheme.code = self._readColor(parser, sec, "shortcode") + self.syntaxTheme.key = self._readColor(parser, sec, "keyword") + self.syntaxTheme.tag = self._readColor(parser, sec, "tag") + self.syntaxTheme.val = self._readColor(parser, sec, "value") + self.syntaxTheme.opt = self._readColor(parser, sec, "optional") + self.syntaxTheme.spell = self._readColor(parser, sec, "spellcheckline") + self.syntaxTheme.error = self._readColor(parser, sec, "errorline") + self.syntaxTheme.repTag = self._readColor(parser, sec, "replacetag") + self.syntaxTheme.mod = self._readColor(parser, sec, "modifier") + self.syntaxTheme.mark = self._readColor(parser, sec, "texthighlight") # Update Dependant Colours # Based on: https://github.com/qt/qtbase/blob/dev/src/gui/kernel/qplatformtheme.cpp text = self._guiPalette.text().color() window = self._guiPalette.window().color() highlight = self._guiPalette.highlight().color() - isDark = text.lightnessF() > window.lightnessF() QtColActive = QPalette.ColorGroup.Active QtColInactive = QPalette.ColorGroup.Inactive @@ -321,8 +410,8 @@ class GuiTheme: darkOff = dark.darker(150) shadowOff = ref.darker(150) - grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140) - dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190) + grey = QColor(120, 120, 120) if darkMode else QColor(140, 140, 140) + dimmed = QColor(130, 130, 130) if darkMode else QColor(190, 190, 190) placeholder = QColor(text) placeholder.setAlpha(128) @@ -347,145 +436,93 @@ class GuiTheme: self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Highlight, grey) if CONFIG.verQtValue >= 0x060600: - self._guiPalette.setBrush(QtColActive, QPalette.ColorRole.Accent, highlight) - self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Accent, highlight) + self._guiPalette.setBrush(QtColActive, QPalette.ColorRole.Accent, self.accentCol) + self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Accent, self.accentCol) self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey) + # Set project override colours + if (override := CONFIG.iconColTree) != DEF_TREECOL: + color = self._qColors.get(override, QtBlack) + self._setBaseColor("root", color) + self._setBaseColor("folder", color) + if not CONFIG.iconColDocs: + self._setBaseColor("file", color) + self._setBaseColor("title", color) + self._setBaseColor("chapter", color) + self._setBaseColor("scene", color) + self._setBaseColor("note", color) + + self.isDarkTheme = darkMode + self._currentTheme = theme + # Load icons after the theme is parsed self.iconCache.loadTheme(CONFIG.iconTheme) # Finalise - self.isDarkTheme = isDark QApplication.setPalette(self._guiPalette) self._buildStyleSheets(self._guiPalette) - CONFIG.splashMessage(f"Loaded GUI theme: {meta.name}") - return True - def loadSyntax(self) -> bool: - """Load the currently specified syntax highlighter theme.""" - theme = CONFIG.guiSyntax - if theme not in self._availSyntax: - logger.error("Could not find syntax theme '%s'", theme) - theme = DEF_SYNTAX - CONFIG.guiSyntax = theme - - if not (file := self._availSyntax.get(theme)): - logger.error("Could not load syntax theme") - return False - - CONFIG.splashMessage("Loading syntax theme ...") - logger.info("Loading syntax theme '%s'", theme) - parser = NWConfigParser() - try: - with open(file, mode="r", encoding="utf-8") as fo: - parser.read_file(fo) - except Exception: - logger.error("Could not read file: %s", file) - logException() - return False - - # Main - sec = "Main" - meta = ThemeMeta() - if parser.has_section(sec): - meta.name = parser.rdStr(sec, "name", "") - meta.description = parser.rdStr(sec, "description", "N/A") - meta.author = parser.rdStr(sec, "author", "N/A") - meta.credit = parser.rdStr(sec, "credit", "N/A") - meta.url = parser.rdStr(sec, "url", "") - meta.license = parser.rdStr(sec, "license", "N/A") - meta.licenseUrl = parser.rdStr(sec, "licenseurl", "") - - # Syntax - sec = "Syntax" - syntax = SyntaxColors() - if parser.has_section(sec): - syntax.back = self._parseColor(parser, sec, "background") - syntax.text = self._parseColor(parser, sec, "text") - syntax.link = self._parseColor(parser, sec, "link") - syntax.head = self._parseColor(parser, sec, "headertext") - syntax.headH = self._parseColor(parser, sec, "headertag") - syntax.emph = self._parseColor(parser, sec, "emphasis") - syntax.dialN = self._parseColor(parser, sec, "dialog") - syntax.dialA = self._parseColor(parser, sec, "altdialog") - syntax.hidden = self._parseColor(parser, sec, "hidden") - syntax.note = self._parseColor(parser, sec, "note") - syntax.code = self._parseColor(parser, sec, "shortcode") - syntax.key = self._parseColor(parser, sec, "keyword") - syntax.tag = self._parseColor(parser, sec, "tag") - syntax.val = self._parseColor(parser, sec, "value") - syntax.opt = self._parseColor(parser, sec, "optional") - syntax.spell = self._parseColor(parser, sec, "spellcheckline") - syntax.error = self._parseColor(parser, sec, "errorline") - syntax.repTag = self._parseColor(parser, sec, "replacetag") - syntax.mod = self._parseColor(parser, sec, "modifier") - syntax.mark = self._parseColor(parser, sec, "texthighlight") - - CONFIG.splashMessage(f"Loaded syntax theme: {meta.name}") - - self.syntaxMeta = meta - self.syntaxTheme = syntax - - return True - - def listThemes(self) -> list[tuple[str, str]]: - """Scan the GUI themes folder and list all themes.""" - if self._themeList: - return self._themeList - - themes = [] - parser = NWConfigParser() - for key, path in self._availThemes.items(): - logger.debug("Checking theme config '%s'", key) - if name := _loadInternalName(parser, path): - themes.append((key, name)) - - self._themeList = sorted(themes, key=_sortTheme) - - return self._themeList - - def listSyntax(self) -> list[tuple[str, str]]: - """Scan the syntax themes folder and list all themes.""" - if self._syntaxList: - return self._syntaxList - - themes = [] - parser = NWConfigParser() - for key, path in self._availSyntax.items(): - logger.debug("Checking theme syntax '%s'", key) - if name := _loadInternalName(parser, path): - themes.append((key, name)) - - self._syntaxList = sorted(themes, key=_sortTheme) - - return self._syntaxList - def getStyleSheet(self, name: str) -> str: """Load a standard style sheet.""" return self._styleSheets.get(name, "") + def parseColor(self, value: str, default: QColor = QtBlack) -> QColor: + """Parse a string as a colour value.""" + if value in self._qColors: + # Named colour + return self._qColors[value] + elif value.startswith("#") and len(value) == 7: + # Assume #RRGGBB + return QColor.fromString(value) + elif value.startswith("#") and len(value) == 9: + # Assume #RRGGBBAA and convert to #AARRGGBB + return QColor.fromString(f"#{value[7:9]}{value[1:7]}") + elif ":" in value: + # Colour name and lighter, darker or alpha + name, _, adjust = value.partition(":") + color = QColor(self._qColors.get(name.strip(), default)) + if adjust.startswith("L"): + color = color.lighter(checkInt(adjust[1:], 100)) + elif adjust.startswith("D"): + color = color.darker(checkInt(adjust[1:], 100)) + else: + color.setAlpha(checkInt(adjust, 255)) + return color + elif "," in value: + # Integer red, green, blue, alpha + data = value.split(",") + result = [0, 0, 0, 255] + for i in range(min(len(data), 4)): + result[i] = checkInt(data[i].strip(), result[i]) + return QColor(*result) + return default + ## # Internal Functions ## + def _setBaseColor(self, key: str, color: QColor) -> None: + """Set the colour for a named colour.""" + self._qColors[key] = QColor(color) + self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8") + def _resetTheme(self) -> None: """Reset GUI colours to default values.""" palette = QPalette() - - text = palette.color(QPalette.ColorRole.Text) - window = palette.color(QPalette.ColorRole.Window) - isDark = text.lightnessF() > window.lightnessF() + isDark = self.isDesktopDarkMode() # Reset GUI Palette + base = palette.color(QPalette.ColorRole.Base) + default = palette.color(QPalette.ColorRole.Text) faded = QColor(128, 128, 128) dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190) red = QColor(242, 119, 122) if isDark else QColor(240, 40, 41) orange = QColor(249, 145, 57) if isDark else QColor(245, 135, 31) yellow = QColor(255, 204, 102) if isDark else QColor(234, 183, 0) green = QColor(153, 204, 153) if isDark else QColor(113, 140, 0) - aqua = QColor(102, 204, 204) if isDark else QColor(62, 153, 159) + cyan = QColor(102, 204, 204) if isDark else QColor(62, 153, 159) blue = QColor(102, 153, 204) if isDark else QColor(66, 113, 174) purple = QColor(204, 153, 204) if isDark else QColor(137, 89, 168) @@ -496,38 +533,40 @@ class GuiTheme: self._guiPalette = palette - # Reset Icons - icons = self.iconCache - icons.clear() - icons.setIconColor("default", text) - icons.setIconColor("faded", faded) - icons.setIconColor("red", red) - icons.setIconColor("orange", orange) - icons.setIconColor("yellow", yellow) - icons.setIconColor("green", green) - icons.setIconColor("aqua", aqua) - icons.setIconColor("blue", blue) - icons.setIconColor("purple", purple) - icons.setIconColor("root", blue) - icons.setIconColor("folder", yellow) - icons.setIconColor("file", text) - icons.setIconColor("title", green) - icons.setIconColor("chapter", red) - icons.setIconColor("scene", blue) - icons.setIconColor("note", yellow) + # Reset Base Colours and Icons + self.iconCache.clear() + self._svgColors = {} + self._qColors = {} + self._setBaseColor("base", base) + self._setBaseColor("default", default) + self._setBaseColor("faded", faded) + self._setBaseColor("red", red) + self._setBaseColor("orange", orange) + self._setBaseColor("yellow", yellow) + self._setBaseColor("green", green) + self._setBaseColor("cyan", cyan) + self._setBaseColor("blue", blue) + self._setBaseColor("purple", purple) + self._setBaseColor("root", blue) + self._setBaseColor("folder", yellow) + self._setBaseColor("file", default) + self._setBaseColor("title", green) + self._setBaseColor("chapter", red) + self._setBaseColor("scene", blue) + self._setBaseColor("note", yellow) + self._setBaseColor("active", green) + self._setBaseColor("inactive", red) + self._setBaseColor("disabled", faded) - return - - def _parseColor(self, parser: NWConfigParser, section: str, name: str) -> QColor: + def _readColor(self, parser: ConfigParser, section: str, name: str) -> QColor: """Parse a colour value from a config string.""" - return QColor(*parser.rdIntList(section, name, [0, 0, 0, 255])) + return self.parseColor(parser.get(section, name, fallback="default")) def _setPalette( - self, parser: NWConfigParser, section: str, name: str, value: QPalette.ColorRole + self, parser: ConfigParser, section: str, name: str, value: QPalette.ColorRole ) -> None: """Set a palette colour value from a config string.""" - self._guiPalette.setBrush(value, self._parseColor(parser, section, name)) - return + self._guiPalette.setBrush(value, self._readColor(parser, section, name)) def _buildStyleSheets(self, palette: QPalette) -> None: """Build default style sheets.""" @@ -559,7 +598,32 @@ class GuiTheme: "QToolButton::menu-indicator {image: none;} " ) - return + def _scanThemes(self, files: list[Path]) -> None: + """Scan the GUI themes folder and list all themes.""" + parser = ConfigParser() + data: dict[str, tuple[str, str, bool, Path]] = {} + keys = [] + for file in files: + try: + parser.clear() + parser.read(file, encoding="utf-8") + name = parser.get("Main", "name", fallback="") + mode = parser.get("Main", "mode", fallback="").lower() + if name and mode in ("light", "dark"): + key = file.stem + prefix = "*" if key.startswith("default") else "" + lookup = f"{prefix}{name} {key}" + keys.append(lookup) + data[lookup] = (file.stem, name, mode == "dark", file) + except Exception: # noqa: PERF203 + logger.error("Could not read file: %s", file) + logException() + + self._allThemes = {} + for lookup in sorted(keys): + key, name, dark, item = data[lookup] + logger.debug("Checking theme config '%s'", key) + self._allThemes[key] = ThemeEntry(name, dark, item) class GuiIcons: @@ -572,9 +636,8 @@ class GuiIcons: """ __slots__ = ( - "_availThemes", "_headerDec", "_headerDecNarrow", "_noIcon", - "_qColors", "_qIcons", "_svgColors", "_svgData", "_themeList", - "mainTheme", "themeMeta", + "_allThemes", "_headerDec", "_headerDecNarrow", "_meta", + "_noIcon", "_qIcons", "_svgData", "_theme", ) TOGGLE_ICON_KEYS: Final[dict[str, tuple[str, str]]] = { @@ -582,69 +645,74 @@ class GuiIcons: "unfold": ("unfold-show", "unfold-hide"), } IMAGE_MAP: Final[dict[str, tuple[str, str]]] = { - "welcome": ("welcome-light.jpg", "welcome-dark.jpg"), + "welcome": ("welcome.webp", "welcome.webp"), "nw-text": ("novelwriter-text-light.svg", "novelwriter-text-dark.svg"), } def __init__(self, mainTheme: GuiTheme) -> None: - self.mainTheme = mainTheme - self.themeMeta = ThemeMeta() + self._theme = mainTheme + self._meta = IconsMeta() # Storage + self._allThemes: dict[str, ThemeEntry] = {} self._svgData: dict[str, bytes] = {} - self._svgColors: dict[str, bytes] = {} - self._qColors: dict[str, QColor] = {} self._qIcons: dict[str, QIcon] = {} self._headerDec: list[QPixmap] = [] self._headerDecNarrow: list[QPixmap] = [] - # Icon Theme Path - self._availThemes: dict[str, Path] = {} - self._themeList: list[tuple[str, str]] = [] - # None Icon self._noIcon = QIcon(str(CONFIG.assetPath("icons") / "none.svg")) - _listConf(self._availThemes, CONFIG.assetPath("icons"), ".icons") - _listConf(self._availThemes, CONFIG.dataPath("icons"), ".icons") - - return - def clear(self) -> None: """Clear the icon cache.""" self._svgData = {} - self._svgColors = {} - self._qColors = {} self._qIcons = {} self._headerDec = [] self._headerDecNarrow = [] - self.themeMeta = ThemeMeta() - return + self._meta = ThemeMeta() + + ## + # Properties + ## + + @property + def iconThemes(self) -> dict[str, ThemeEntry]: + """Return a dictionary of all icon themes.""" + return self._allThemes ## # Actions ## - def loadTheme(self, theme: str) -> bool: + def initIcons(self) -> None: + """Initialise icons.""" + CONFIG.splashMessage("Scanning for icon themes ...") + icons: list[Path] = [] + _listContent(icons, CONFIG.assetPath("icons"), ".icons") + _listContent(icons, CONFIG.dataPath("icons"), ".icons") + self._scanThemes(icons) + + def loadTheme(self, theme: str) -> None: """Update the theme map. This is more of an init, since many of the GUI icons cannot really be replaced without writing specific update functions for the classes where they're used. """ - if theme not in self._availThemes: + if theme not in self._allThemes: logger.error("Could not find icon theme '%s'", theme) theme = DEF_ICONS CONFIG.iconTheme = theme - if not (file := self._availThemes.get(theme)): + entry = self._allThemes.get(theme) + if not entry: logger.error("Could not load icon theme") - return False + return - CONFIG.splashMessage("Loading icon theme ...") + CONFIG.splashMessage(f"Loading icon theme: {entry.name}") logger.info("Loading icon theme '%s'", theme) try: - meta = ThemeMeta() - with open(file, mode="r", encoding="utf-8") as icons: + meta = IconsMeta() + with open(entry.path, mode="r", encoding="utf-8") as icons: for icon in icons: bits = icon.partition("=") key = bits[0].strip() @@ -658,47 +726,23 @@ class GuiIcons: meta.author = value elif key == "meta:license": meta.license = value - self.themeMeta = meta + self._meta = meta except Exception: - logger.error("Could not read file: %s", file) + logger.error("Could not read file: %s", entry.path) logException() - return False - - CONFIG.splashMessage(f"Loaded icon theme: {meta.name}") - CONFIG.splashMessage("Generating additional icons ...") - - # Set colour overrides for project item icons - if (override := CONFIG.iconColTree) != "theme": - color = self._svgColors.get(override, b"#000000") - self._svgColors["root"] = color - self._svgColors["folder"] = color - if not CONFIG.iconColDocs: - self._svgColors["file"] = color - self._svgColors["title"] = color - self._svgColors["chapter"] = color - self._svgColors["scene"] = color - self._svgColors["note"] = color + return # Populate generated icons cache + CONFIG.splashMessage("Generating additional icons ...") self.getHeaderDecoration(0) self.getHeaderDecorationNarrow(0) - return True - - def setIconColor(self, key: str, color: QColor) -> None: - """Set an icon colour for a named colour.""" - self._qColors[key] = QColor(color) - self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8") return ## # Access Functions ## - def getIconColor(self, name: str) -> QColor: - """Return an icon color.""" - return QColor(self._qColors.get(name) or QtBlack) - def getIcon(self, name: str, color: str | None = None, 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 @@ -725,7 +769,7 @@ class GuiIcons: self, tType: nwItemType, tClass: nwItemClass, tLayout: nwItemLayout, hLevel: str = "H0" ) -> QIcon: """Get the correct icon for a project item based on type, class - and heading level + and heading level. """ name = None color = "default" @@ -769,7 +813,7 @@ class GuiIcons: map or the icon map. This function always returns a QPixmap. """ if name in self.IMAGE_MAP: - idx = int(self.mainTheme.isDarkTheme) + idx = int(self._theme.isDarkTheme) imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx] else: logger.error("Decoration with name '%s' does not exist", name) @@ -793,7 +837,7 @@ class GuiIcons: def getHeaderDecoration(self, hLevel: int) -> QPixmap: """Get the decoration for a specific heading level.""" if not self._headerDec: - iPx = self.mainTheme.baseIconHeight + iPx = self._theme.baseIconHeight self._headerDec = [ self._generateDecoration("file", iPx, 0), self._generateDecoration("title", iPx, 0), @@ -806,7 +850,7 @@ class GuiIcons: def getHeaderDecorationNarrow(self, hLevel: int) -> QPixmap: """Get the narrow decoration for a specific heading level.""" if not self._headerDecNarrow: - iPx = self.mainTheme.baseIconHeight + iPx = self._theme.baseIconHeight self._headerDecNarrow = [ self._generateDecoration("file", iPx, 0), self._generateDecoration("title", iPx, 0), @@ -817,21 +861,6 @@ class GuiIcons: ] return self._headerDecNarrow[minmax(hLevel, 0, 5)] - def listThemes(self) -> list[tuple[str, str]]: - """Scan the GUI icons folder and list all themes.""" - if self._themeList: - return self._themeList - - themes = [] - for key, path in self._availThemes.items(): - logger.debug("Checking icon theme '%s'", key) - if name := _loadIconName(path): - themes.append((key, name)) - - self._themeList = sorted(themes, key=_sortTheme) - - return self._themeList - ## # Internal Functions ## @@ -847,7 +876,7 @@ class GuiIcons: return QIcon(str(CONFIG.assetPath("icons") / "x-novelwriter-project.svg")) if svg := self._svgData.get(name, b""): - if fill := self._svgColors.get(color or "default"): + if fill := self._theme.getRawBaseColor(color or "default"): svg = svg.replace(b"#000000", fill) pixmap = QPixmap(w, h) pixmap.fill(QtTransparent) @@ -869,53 +898,43 @@ class GuiIcons: painter = QPainter(pixmap) painter.setRenderHint(QtPaintAntiAlias) - if fill := self._svgColors.get(color or "default"): + if fill := self._theme.getRawBaseColor(color or "default"): painter.fillPath(path, QColor(fill.decode(encoding="utf-8"))) painter.end() tMode = Qt.TransformationMode.SmoothTransformation return pixmap.scaledToHeight(height, tMode) + def _scanThemes(self, entries: list[Path]) -> None: + """Scan the GUI themes folder and list all themes.""" + data: dict[str, tuple[str, str, Path]] = {} + keys = [] + for entry in entries: + try: + with open(entry, mode="r", encoding="utf-8") as fo: + for line in fo: + key, _, value = line.partition("=") + if key.strip() == "meta:name": + if name := value.strip(): + lookup = entry.stem + keys.append(lookup) + data[lookup] = (lookup, name, entry) + break + except Exception: + logger.error("Could not read file: %s", entry) + logException() + + self._allThemes = {} + for lookup in sorted(keys): + key, name, item = data[lookup] + logger.debug("Checking icon theme '%s'", key) + self._allThemes[key] = ThemeEntry(name, False, item) + # Module Functions # ================ -def _listConf(target: dict, path: Path, extension: str) -> None: - """Scan for theme files and populate the dictionary.""" +def _listContent(data: list[Path], path: Path, extension: str) -> None: + """List files of a specific type and extend the list.""" if path.is_dir(): - for item in path.iterdir(): - if item.is_file() and item.name.endswith(extension): - target[item.stem] = item - return - - -def _sortTheme(data: tuple[str, str]) -> str: - """Key function for theme sorting.""" - key, name = data - return f"*{name}" if key.startswith("default_") else name - - -def _loadInternalName(parser: NWConfigParser, path: str | Path) -> str: - """Open a conf file and read the 'name' setting.""" - try: - with open(path, mode="r", encoding="utf-8") as inFile: - parser.read_file(inFile) - return parser.rdStr("Main", "name", "") - except Exception: - logger.error("Could not read file: %s", path) - logException() - return "" - - -def _loadIconName(path: Path) -> str: - """Open an icons file and read the name setting.""" - try: - with open(path, mode="r", encoding="utf-8") as icons: - for icon in icons: - key, _, value = icon.partition("=") - if key.strip() == "meta:name": - return value.strip() - except Exception: - logger.error("Could not read file: %s", path) - logException() - return "" + data.extend(n for n in path.iterdir() if n.is_file() and n.suffix == extension) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index a12383fe..91d7ff59 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -30,7 +30,7 @@ from datetime import datetime from pathlib import Path from time import time -from PyQt6.QtCore import Qt, QTimer, pyqtSlot +from PyQt6.QtCore import QEvent, Qt, QTimer, pyqtSlot from PyQt6.QtGui import QCloseEvent, QCursor, QIcon, QShortcut from PyQt6.QtWidgets import ( QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, @@ -38,13 +38,14 @@ from PyQt6.QtWidgets import ( ) from novelwriter import CONFIG, SHARED, __hexversion__, __version__ -from novelwriter.common import formatFileFilter, formatVersion, hexToInt +from novelwriter.common import formatFileFilter, formatVersion, hexToInt, minmax from novelwriter.constants import nwConst from novelwriter.dialogs.about import GuiAbout from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.projectsettings import GuiProjectSettings from novelwriter.dialogs.wordlist import GuiWordList from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwFocus, nwItemType, nwView +from novelwriter.extensions.progressbars import NProgressSimple from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.docviewer import GuiDocViewer from novelwriter.gui.docviewerpanel import GuiDocViewerPanel @@ -67,16 +68,12 @@ logger = logging.getLogger(__name__) class GuiMain(QMainWindow): - """Main GUI Window + """Main GUI Window. - The Main GUI window class. It is the entry point of the - application, and holds all runtime objects aside from the main - Config instance, which is created before the Main GUI. - - The Main GUI is split up into GUI components, assembled in the init - function. Also, the project instance and theme instance are created - here. These should be passed around to all other objects who need - them and new instances of them should generally not be created. + The Main GUI window class is the entry point of the application. It + is split up into GUI components, assembled in the init function. + Tools and dialog windows are created on demand, but may be cached by + the Qt library and reused unless explicitly freed after use. """ def __init__(self) -> None: @@ -103,7 +100,7 @@ class GuiMain(QMainWindow): SHARED.initSharedData(self) # Prepare Main Window - self.resize(*CONFIG.mainWinSize) + self._setWindowSize(CONFIG.mainWinSize) self._updateWindowTitle() nwIcon = CONFIG.assetPath("icons") / "novelwriter.svg" @@ -169,9 +166,9 @@ class GuiMain(QMainWindow): self.splitMain.addWidget(self.splitDocs) self.splitMain.setOpaqueResize(False) self.splitMain.setHandleWidth(4) - self.splitMain.setSizes(CONFIG.mainPanePos) - self.splitMain.setCollapsible(0, False) + self.splitMain.setSizes([max(s, 100) for s in CONFIG.mainPanePos]) self.splitMain.setCollapsible(0, False) + self.splitMain.setCollapsible(1, False) self.splitMain.setStretchFactor(1, 0) self.splitMain.setStretchFactor(1, 1) @@ -185,6 +182,10 @@ class GuiMain(QMainWindow): self.splitView.setVisible(False) self.docEditor.closeSearch() + # Progress Bar + self.mainProgress = NProgressSimple(self) + self.mainProgress.setFixedHeight(2) + # Assemble Main Window Elements self.mainBox = QHBoxLayout() self.mainBox.addWidget(self.sideBar) @@ -192,8 +193,14 @@ class GuiMain(QMainWindow): self.mainBox.setContentsMargins(0, 0, 0, 0) self.mainBox.setSpacing(0) + self.outerBox = QVBoxLayout() + self.outerBox.addLayout(self.mainBox) + self.outerBox.addWidget(self.mainProgress) + self.outerBox.setContentsMargins(0, 0, 0, 0) + self.outerBox.setSpacing(0) + self.mainWidget = QWidget(self) - self.mainWidget.setLayout(self.mainBox) + self.mainWidget.setLayout(self.outerBox) self.setMenuBar(self.mainMenu) self.setCentralWidget(self.mainWidget) @@ -316,13 +323,10 @@ class GuiMain(QMainWindow): self.mainStatus.setStatusMessage(self.tr("novelWriter is ready ...")) CONFIG.splashMessage("novelWriter is ready ...") - return - def initMain(self) -> None: """Initialise elements that depend on user settings.""" self.asProjTimer.setInterval(int(CONFIG.autoSaveProj*1000)) self.asDocTimer.setInterval(int(CONFIG.autoSaveDoc*1000)) - return def postLaunchTasks(self, cmdOpen: str | None) -> None: """Process tasks after the main window has been created.""" @@ -344,8 +348,6 @@ class GuiMain(QMainWindow): # before showing any dialogs QTimer.singleShot(50, self.showPostLaunchDialogs) - return - @pyqtSlot() def showPostLaunchDialogs(self) -> None: """Show post launch dialogs.""" @@ -363,8 +365,6 @@ class GuiMain(QMainWindow): ).format(f"", "") SHARED.info(f"{trVersion}
    {trRelease}") - return - ## # Project Actions ## @@ -527,7 +527,6 @@ class GuiMain(QMainWindow): SHARED.setFocusMode(False) self.saveDocument() self.docEditor.clearEditor() - return def openDocument( self, @@ -584,7 +583,6 @@ class GuiMain(QMainWindow): self.openDocument(nHandle, tLine=1, doScroll=True) elif wrapAround: self.openDocument(fHandle, tLine=1, doScroll=True) - return def saveDocument(self, force: bool = False) -> None: """Save the current documents.""" @@ -592,13 +590,11 @@ class GuiMain(QMainWindow): self.docEditor.saveCursorPosition() if force or self.docEditor.docChanged: self.docEditor.saveText() - return @pyqtSlot() def forceSaveDocument(self) -> None: """Save document even of it has not changed.""" self.saveDocument(force=True) - return def viewDocument(self, tHandle: str | None = None, sTitle: str | None = None) -> bool: """Load a document for viewing in the view panel.""" @@ -753,8 +749,6 @@ class GuiMain(QMainWindow): if not beQuiet: SHARED.info(self.tr("The project index has been successfully rebuilt.")) - return - ## # Main Dialogs ## @@ -765,7 +759,6 @@ class GuiMain(QMainWindow): dialog = GuiWelcome(self) dialog.openProjectRequest.connect(self._openProjectFromWelcome) dialog.exec() - return @pyqtSlot() def showPreferencesDialog(self) -> None: @@ -773,7 +766,6 @@ class GuiMain(QMainWindow): dialog = GuiPreferences(self) dialog.newPreferencesReady.connect(self._processConfigChanges) dialog.exec() - return @pyqtSlot() @pyqtSlot(int) @@ -783,7 +775,6 @@ class GuiMain(QMainWindow): dialog = GuiProjectSettings(self, gotoPage=focusTab) dialog.newProjectSettingsReady.connect(self._processProjectSettingsChanges) dialog.exec() - return @pyqtSlot() def showNovelDetailsDialog(self) -> None: @@ -792,7 +783,6 @@ class GuiMain(QMainWindow): dialog = GuiNovelDetails(self) dialog.activateDialog() dialog.updateValues() - return @pyqtSlot() def showBuildManuscriptDialog(self) -> None: @@ -802,7 +792,6 @@ class GuiMain(QMainWindow): dialog = GuiManuscript(self) dialog.activateDialog() dialog.loadContent() - return @pyqtSlot() def showProjectWordListDialog(self) -> None: @@ -811,7 +800,6 @@ class GuiMain(QMainWindow): dialog = GuiWordList(self) dialog.newWordListReady.connect(self._processWordListChanges) dialog.exec() - return @pyqtSlot() def showWritingStatsDialog(self) -> None: @@ -821,21 +809,18 @@ class GuiMain(QMainWindow): dialog = GuiWritingStats(self) dialog.activateDialog() dialog.populateGUI() - return @pyqtSlot() def showAboutNWDialog(self) -> None: """Show the novelWriter about dialog.""" dialog = GuiAbout(self) dialog.exec() - return @pyqtSlot() def showAboutQtDialog(self) -> None: """Show the Qt about dialog.""" msgBox = QMessageBox(self) msgBox.aboutQt(self, "About Qt") - return @pyqtSlot() def showDictionariesDialog(self) -> None: @@ -845,7 +830,6 @@ class GuiMain(QMainWindow): if not dialog.initDialog(): dialog.close() SHARED.error(self.tr("Could not initialise the dialog.")) - return ## # Main Window Actions @@ -902,16 +886,47 @@ class GuiMain(QMainWindow): return not self.splitView.isVisible() + def checkThemeUpdate(self) -> None: + """Load theme if mode changed.""" + if SHARED.theme.loadTheme(): + self.refreshThemeColors(syntax=True) + self.docEditor.initEditor() + self.docViewer.initViewer() + + def refreshThemeColors(self, syntax: bool = False, force: bool = False) -> None: + """Refresh the GUI theme.""" + SHARED.theme.loadTheme(force=force) + SHARED.project.updateTheme() + self.setPalette(QApplication.palette()) + self.docEditor.updateTheme() + self.docViewer.updateTheme() + self.docViewerPanel.updateTheme() + self.sideBar.updateTheme() + self.projView.updateTheme() + self.novelView.updateTheme() + self.projSearch.updateTheme() + self.outlineView.updateTheme() + self.itemDetails.updateTheme() + self.mainStatus.updateTheme() + SHARED.project.tree.refreshAllItems() + + if syntax: + self.docEditor.updateSyntaxColors() + ## # Events ## + def changeEvent(self, event: QEvent) -> None: + """Capture application change events.""" + if int(event.type()) == 210: # ThemeChange + self.checkThemeUpdate() + def closeEvent(self, event: QCloseEvent) -> None: """Capture the closing event of the GUI and call the close function to handle all the close process steps. """ event.accept() if self.closeMain() else event.ignore() - return ## # Public Slots @@ -919,30 +934,26 @@ class GuiMain(QMainWindow): @pyqtSlot() def toggleFullScreenMode(self) -> None: - """Toggle full screen mode""" + """Toggle full screen mode.""" self.setWindowState(self.windowState() ^ Qt.WindowState.WindowFullScreen) - return @pyqtSlot() def closeDocEditor(self) -> None: """Close the document editor. This does not hide the editor.""" self.closeDocument() SHARED.project.data.setLastHandle(None, "editor") - return @pyqtSlot() def closeDocViewer(self) -> None: """Close the document viewer.""" self.closeViewerPanel() SHARED.project.data.setLastHandle(None, "viewer") - return @pyqtSlot() def toggleFocusMode(self) -> None: """Toggle focus mode.""" if self.docEditor.docHandle: SHARED.setFocusMode(not SHARED.focusMode) - return ## # Private Slots @@ -960,7 +971,6 @@ class GuiMain(QMainWindow): docViewer = True self.docEditor.changeFocusState(docEditor) self.docViewer.changeFocusState(docViewer) - return @pyqtSlot(bool) def _focusModeChanged(self, focusMode: bool) -> None: @@ -991,7 +1001,6 @@ class GuiMain(QMainWindow): if cursorVisible: self.docEditor.ensureCursorVisibleNoCentre() - return @pyqtSlot(nwFocus) def _switchFocus(self, paneNo: nwFocus) -> None: @@ -1017,16 +1026,16 @@ class GuiMain(QMainWindow): fP = self.projView.treeHasFocus() fN = self.novelView.treeHasFocus() - self._changeView(nwView.EDITOR) + self._changeView(nwView.EDITOR, exitFocus=True) if (vM and ((vP and fP) or (vN and not fN))) or (not vM and vN): - self._changeView(nwView.NOVEL) + self._changeView(nwView.NOVEL, exitFocus=True) self.novelView.setTreeFocus() else: - self._changeView(nwView.PROJECT) + self._changeView(nwView.PROJECT, exitFocus=True) self.projView.setTreeFocus() elif paneNo == nwFocus.DOCUMENT: - self._changeView(nwView.EDITOR) + self._changeView(nwView.EDITOR, exitFocus=True) hasViewer = self.splitView.isVisible() if hasViewer and self.docEditor.anyFocus(): self.docViewer.setFocus() @@ -1036,11 +1045,9 @@ class GuiMain(QMainWindow): self.docEditor.setFocus() elif paneNo == nwFocus.OUTLINE: - self._changeView(nwView.OUTLINE) + self._changeView(nwView.OUTLINE, exitFocus=True) self.outlineView.setTreeFocus() - return - @pyqtSlot(bool, bool, bool, bool) def _processConfigChanges(self, restart: bool, tree: bool, theme: bool, syntax: bool) -> None: """Refresh GUI based on flags from the Preferences dialog.""" @@ -1054,23 +1061,7 @@ class GuiMain(QMainWindow): self.novelView.refreshCurrentTree() if theme: - SHARED.theme.loadTheme() - self.setPalette(QApplication.palette()) - self.docEditor.updateTheme() - self.docViewer.updateTheme() - self.docViewerPanel.updateTheme() - self.sideBar.updateTheme() - self.projView.updateTheme() - self.novelView.updateTheme() - self.projSearch.updateTheme() - self.outlineView.updateTheme() - self.itemDetails.updateTheme() - self.mainStatus.updateTheme() - SHARED.project.tree.refreshAllItems() - - if syntax: - SHARED.theme.loadSyntax() - self.docEditor.updateSyntaxColors() + self.refreshThemeColors(syntax=syntax, force=True) self.docEditor.initEditor() self.docViewer.initViewer() @@ -1088,8 +1079,6 @@ class GuiMain(QMainWindow): "Some changes will not be applied until novelWriter has been restarted." )) - return - @pyqtSlot() def _processProjectSettingsChanges(self) -> None: """Refresh data dependent on project settings.""" @@ -1097,7 +1086,6 @@ class GuiMain(QMainWindow): SHARED.updateSpellCheckLanguage() self.itemDetails.refreshDetails() self._updateWindowTitle(SHARED.project.data.name) - return @pyqtSlot() def _processWordListChanges(self) -> None: @@ -1105,7 +1093,6 @@ class GuiMain(QMainWindow): logger.debug("Reloading word list") SHARED.updateSpellCheckLanguage(reload=True) self.docEditor.spellCheckDocument() - return @pyqtSlot(str, nwDocMode) def _followTag(self, tag: str, mode: nwDocMode) -> None: @@ -1124,7 +1111,6 @@ class GuiMain(QMainWindow): self.openDocument(tHandle, sTitle=sTitle) elif mode == nwDocMode.VIEW: self.viewDocument(tHandle=tHandle, sTitle=sTitle) - return @pyqtSlot(Path) def _openProjectFromWelcome(self, path: Path) -> None: @@ -1133,7 +1119,6 @@ class GuiMain(QMainWindow): self.openProject(path) if not SHARED.hasProject: self.showWelcomeDialog() - return @pyqtSlot(str, nwDocMode, str, bool) def _openDocument(self, tHandle: str, mode: nwDocMode, sTitle: str, setFocus: bool) -> None: @@ -1143,7 +1128,6 @@ class GuiMain(QMainWindow): self.openDocument(tHandle, sTitle=sTitle, changeFocus=setFocus) elif mode == nwDocMode.VIEW: self.viewDocument(tHandle=tHandle, sTitle=sTitle) - return @pyqtSlot(str, int, int, bool) def _openDocumentSelection( @@ -1152,7 +1136,6 @@ class GuiMain(QMainWindow): """Open a document and select a section of the text.""" if self.openDocument(tHandle, changeFocus=changeFocus): self.docEditor.setCursorSelection(selStart, selLength) - return @pyqtSlot() def _reloadViewer(self) -> None: @@ -1161,11 +1144,13 @@ class GuiMain(QMainWindow): # If the two panels have the same document, save any changes in the editor self.saveDocument() self.docViewer.reloadText() - return @pyqtSlot(nwView) - def _changeView(self, view: nwView) -> None: + def _changeView(self, view: nwView, exitFocus: bool = False) -> None: """Handle the requested change of view from the GuiViewBar.""" + if exitFocus: + SHARED.setFocusMode(False) + if view == nwView.EDITOR: # Only change the main stack, but not the project stack self.mainStack.setCurrentWidget(self.splitMain) @@ -1189,8 +1174,6 @@ class GuiMain(QMainWindow): isNovel = self.projStack.currentWidget() == self.novelView self.novelView.setActive(isMain and isNovel) - return - @pyqtSlot(nwDocAction) def _passDocumentAction(self, action: nwDocAction) -> None: """Pass on a document action to the editor or viewer based on @@ -1200,7 +1183,6 @@ class GuiMain(QMainWindow): self.docEditor.docAction(action) elif self.docViewer.hasFocus(): self.docViewer.docAction(action) - return @pyqtSlot(str) @pyqtSlot(nwDocInsert) @@ -1210,14 +1192,12 @@ class GuiMain(QMainWindow): """ if self.docEditor.hasFocus(): self.docEditor.insertText(content) - return @pyqtSlot() def _toggleViewerPanelVisibility(self) -> None: """Toggle the visibility of the document viewer panel.""" CONFIG.showViewerPanel = not CONFIG.showViewerPanel self.docViewerPanel.setVisible(CONFIG.showViewerPanel) - return @pyqtSlot() def _timeTick(self) -> None: @@ -1233,7 +1213,6 @@ class GuiMain(QMainWindow): self._updateStatusWordCount() if CONFIG.memInfo: # pragma: no cover self.mainStatus.memInfo() - return @pyqtSlot() def _autoSaveProject(self) -> None: @@ -1244,7 +1223,6 @@ class GuiMain(QMainWindow): if doSave: logger.debug("Auto-saving project") self.saveProject(autoSave=True) - return @pyqtSlot() def _autoSaveDocument(self) -> None: @@ -1252,7 +1230,6 @@ class GuiMain(QMainWindow): if SHARED.hasProject and self.docEditor.docChanged: logger.debug("Auto-saving document") self.saveDocument() - return @pyqtSlot() def _updateStatusWordCount(self) -> None: @@ -1282,8 +1259,6 @@ class GuiMain(QMainWindow): self.mainStatus.setProjectStats(cTotal, cTotal - iTotal) - return - @pyqtSlot() def _keyPressReturn(self) -> None: """Process a return or enter keypress in the main window.""" @@ -1291,7 +1266,6 @@ class GuiMain(QMainWindow): self.projSearch.processReturn() else: self.openSelectedItem() - return @pyqtSlot() def _keyPressEscape(self) -> None: @@ -1300,7 +1274,6 @@ class GuiMain(QMainWindow): self.docEditor.closeSearch() elif SHARED.focusMode: SHARED.setFocusMode(False) - return @pyqtSlot(int) def _mainStackChanged(self, index: int) -> None: @@ -1308,7 +1281,6 @@ class GuiMain(QMainWindow): if self.mainStack.widget(index) == self.outlineView: if SHARED.hasProject: self.outlineView.refreshTree() - return @pyqtSlot(int) def _projStackChanged(self, index: int) -> None: @@ -1320,13 +1292,19 @@ class GuiMain(QMainWindow): elif widget == self.novelView: sHandle, _ = self.novelView.getSelectedHandle() self.itemDetails.updateViewBox(sHandle) - return ## # Internal Functions ## + def _setWindowSize(self, size: list[int]) -> None: + """Set the main window size.""" + if len(size) == 2 and (screen := SHARED.mainScreen): + availSize = screen.availableSize() + width = minmax(size[0], 900, availSize.width()) + height = minmax(size[1], 500, availSize.height()) + self.resize(width, height) + def _updateWindowTitle(self, projName: str | None = None) -> None: """Set the window title and add the project's name.""" self.setWindowTitle(" - ".join(filter(None, [projName, CONFIG.appName]))) - return diff --git a/novelwriter/shared.py b/novelwriter/shared.py index 04add94c..7c971341 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -21,10 +21,11 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging +import re from enum import Enum from pathlib import Path @@ -32,8 +33,8 @@ from time import time from typing import TYPE_CHECKING, TypeVar from PyQt6.QtCore import QObject, QRunnable, QThreadPool, QTimer, QUrl, pyqtSignal, pyqtSlot -from PyQt6.QtGui import QDesktopServices, QFont -from PyQt6.QtWidgets import QFileDialog, QFontDialog, QMessageBox, QWidget +from PyQt6.QtGui import QDesktopServices, QFont, QScreen +from PyQt6.QtWidgets import QApplication, QFileDialog, QFontDialog, QMessageBox, QWidget from novelwriter.common import formatFileFilter from novelwriter.constants import nwFiles @@ -41,6 +42,8 @@ from novelwriter.core.spellcheck import NWSpellEnchant from novelwriter.enum import nwChange, nwItemClass if TYPE_CHECKING: + from collections.abc import Callable + from novelwriter.core.project import NWProject from novelwriter.core.status import T_StatusKind from novelwriter.gui.theme import GuiTheme @@ -50,8 +53,16 @@ logger = logging.getLogger(__name__) NWWidget = TypeVar("NWWidget", bound=QWidget) +RX_HTML = re.compile(r"<.*?>") + class SharedData(QObject): + """Shared Data Singleton. + + This is the class instantiated as the SHARED singleton. It holds + various globally needed data and pointers to important objects like + the main GUI, the current project, and the GUI theme. + """ __slots__ = ( "_gui", "_idleRefTime", "_idleTime", "_lastAlert", "_lockedBy", @@ -91,8 +102,6 @@ class SharedData(QObject): self._clock.setInterval(1000) self._clock.timeout.connect(lambda: self.mainClockTick.emit()) - return - ## # Properties ## @@ -150,6 +159,11 @@ class SharedData(QObject): """Return the last alert message.""" return self._lastAlert + @property + def mainScreen(self) -> QScreen | None: + """Return the screen of the main window.""" + return QApplication.screenAt(self.mainGui.rect().center()) + ## # Setters ## @@ -159,7 +173,6 @@ class SharedData(QObject): if state is not self._focusMode: self._focusMode = state self.focusModeChanged.emit(state) - return ## # Methods @@ -170,7 +183,7 @@ class SharedData(QObject): is created. """ self._theme = theme - return + self._theme.initThemes() def initSharedData(self, gui: GuiMain) -> None: """Initialise the SharedData instance. This must be called as @@ -183,7 +196,6 @@ class SharedData(QObject): logger.debug("Ready: SharedData") if pool := QThreadPool.globalInstance(): logger.debug("Thread Pool Max Count: %d", pool.maxThreadCount()) - return def closeDocument(self, tHandle: str | None = None) -> None: """Close the document editor, optionally a specific document.""" @@ -191,7 +203,6 @@ class SharedData(QObject): self.mainGui.closeDocument() if tHandle is None or tHandle == self.mainGui.docViewer.docHandle: self.mainGui.closeViewerPanel() - return def saveEditor(self, tHandle: str | None = None) -> None: """Save the editor content, optionally a specific document.""" @@ -202,7 +213,6 @@ class SharedData(QObject): ): logger.debug("Saving editor document before action") docEditor.saveText() - return def openProject(self, path: str | Path, clearLock: bool = False) -> bool: """Open a project.""" @@ -235,7 +245,6 @@ class SharedData(QObject): self.project.closeProject(self._idleTime) self._resetProject() self._resetIdleTimer() - return def updateSpellCheckLanguage(self, reload: bool = False) -> None: """Update the active spell check language from settings.""" @@ -245,7 +254,6 @@ class SharedData(QObject): self.spelling.setLanguage(language) _, provider = self.spelling.describeDict() self.spellLanguageChanged.emit(language, provider) - return def updateIdleTime(self, currTime: float, userIdle: bool) -> None: """Update the idle time record. If the userIdle flag is True, @@ -256,27 +264,40 @@ class SharedData(QObject): if userIdle: self._idleTime += currTime - self._idleRefTime self._idleRefTime = currTime - return + + def initMainProgress(self, maximum: int, inclusive: bool = False) -> None: + """Start a session for the main progress bar.""" + if gui := self._gui: + gui.mainProgress.setMaximum(maximum - (1 if inclusive else 0)) + gui.mainProgress.setValue(0) + + def incMainProgress(self) -> None: + """Increment the value for the main progress bar.""" + if gui := self._gui: + gui.mainProgress.setValue(gui.mainProgress.value() + 1) + QApplication.processEvents() + + def clearMainProgress(self, delay: float = 1.0) -> None: + """Clear the main progress bar.""" + if gui := self._gui: + QTimer.singleShot(int(delay*1000), gui.mainProgress.reset) def newStatusMessage(self, message: str) -> None: """Request a new status message. This is a callable function for core classes that cannot emit signals on their own. """ self.projectStatusMessage.emit(message) - return def setGlobalProjectState(self, state: bool) -> None: """Change the global project status. This is a callable function for core classes that cannot emit signals on their own. """ self.projectStatusChanged.emit(state) - return def runInThreadPool(self, runnable: QRunnable, priority: int = 0) -> None: """Queue a runnable in the application thread pool.""" if pool := QThreadPool.globalInstance(): pool.start(runnable, priority=priority) - return def getProjectPath( self, parent: QWidget, @@ -315,13 +336,11 @@ class SharedData(QObject): def openWebsite(self, url: str) -> None: """Open a URL in the system's default browser.""" QDesktopServices.openUrl(QUrl(url)) - return @pyqtSlot(str, nwItemClass) def createNewNote(self, tag: str, itemClass: nwItemClass) -> None: """Process new note request.""" self.project.createNewNote(tag, itemClass) - return ## # Signal Proxies @@ -333,37 +352,31 @@ class SharedData(QObject): """Emit the indexChangedTags signal.""" if self._project and self._project.data.uuid == project.data.uuid: self.indexChangedTags.emit(updated, deleted) - return def emitIndexCleared(self, project: NWProject) -> None: """Emit the indexCleared signal.""" if self._project and self._project.data.uuid == project.data.uuid: self.indexCleared.emit() - return def emitIndexAvailable(self, project: NWProject) -> None: """Emit the indexAvailable signal.""" if self._project and self._project.data.uuid == project.data.uuid: self.indexAvailable.emit() - return def emitStatusLabelsChanged(self, project: NWProject, kind: T_StatusKind) -> None: """Emit the statusLabelsChanged signal.""" if self._project and self._project.data.uuid == project.data.uuid: self.statusLabelsChanged.emit(kind) - return def emitProjectItemChanged(self, project: NWProject, handle: str, change: nwChange) -> None: """Emit the projectItemChanged signal.""" if self._project and self._project.data.uuid == project.data.uuid: self.projectItemChanged.emit(handle, change) - return def emitRootFolderChanged(self, project: NWProject, handle: str, change: nwChange) -> None: """Emit the rootFolderChanged signal.""" if self._project and self._project.data.uuid == project.data.uuid: self.rootFolderChanged.emit(handle, change) - return ## # Alert Boxes @@ -376,9 +389,8 @@ class SharedData(QObject): alert.setAlertType(_GuiAlert.INFO, False) self._lastAlert = alert.logMessage if log: - logger.info(self._lastAlert, stacklevel=2) + self._logMessage(self._lastAlert, logger.info) alert.exec() - return def warn(self, text: str, info: str = "", details: str = "", log: bool = True) -> None: """Open a warning alert box.""" @@ -387,9 +399,8 @@ class SharedData(QObject): alert.setAlertType(_GuiAlert.WARN, False) self._lastAlert = alert.logMessage if log: - logger.warning(self._lastAlert, stacklevel=2) + self._logMessage(self._lastAlert, logger.warning) alert.exec() - return def error(self, text: str, info: str = "", details: str = "", log: bool = True, exc: Exception | None = None) -> None: @@ -401,9 +412,8 @@ class SharedData(QObject): alert.setException(exc) self._lastAlert = alert.logMessage if log: - logger.error(self._lastAlert, stacklevel=2) + self._logMessage(self._lastAlert, logger.error) alert.exec() - return def question(self, text: str, info: str = "", details: str = "", warn: bool = False) -> bool: """Open a question box.""" @@ -412,13 +422,17 @@ class SharedData(QObject): alert.setAlertType(_GuiAlert.WARN if warn else _GuiAlert.ASK, True) self._lastAlert = alert.logMessage alert.exec() - isYes = alert.result() == QMessageBox.StandardButton.Yes - return isYes + return alert.result() == QMessageBox.StandardButton.Yes ## # Internal Functions ## + def _logMessage(self, message: str, log: Callable) -> None: + """Print message to log.""" + for text in message.split("
    "): + log(RX_HTML.sub("", text), stacklevel=3) + def _resetProject(self) -> None: """Create a new project and spell checking instance.""" from novelwriter.core.project import NWProject @@ -430,13 +444,11 @@ class SharedData(QObject): self._spelling = NWSpellEnchant(self._project) self.updateSpellCheckLanguage() self._focusMode = False - return def _resetIdleTimer(self) -> None: """Reset the timer data for the idle timer.""" self._idleRefTime = time() self._idleTime = 0.0 - return def _closeToolDialogs(self) -> None: """Close all open tool dialogs.""" @@ -444,7 +456,6 @@ class SharedData(QObject): for widget in self.mainGui.children(): if isinstance(widget, NToolDialog): widget.close() - return class _GuiAlert(QMessageBox): @@ -459,11 +470,9 @@ class _GuiAlert(QMessageBox): self._theme = theme self._message = "" logger.debug("Ready: _GuiAlert") - return def __del__(self) -> None: # pragma: no cover logger.debug("Delete: _GuiAlert") - return @property def logMessage(self) -> str: @@ -475,14 +484,12 @@ class _GuiAlert(QMessageBox): self.setText(text) self.setInformativeText(info) self.setDetailedText(details) - return def setException(self, exception: Exception) -> None: """Add exception details.""" info = self.informativeText() text = f"{type(exception).__name__}: {exception!s}" self.setInformativeText(f"{info}
    {text}" if info else text) - return def setAlertType(self, level: int, isYesNo: bool) -> None: """Set the type of alert and whether the dialog should have @@ -505,4 +512,3 @@ class _GuiAlert(QMessageBox): elif level == self.ASK: self.setIconPixmap(self._theme.getPixmap("alert_question", (pSz, pSz), "blue")) self.setWindowTitle(self.tr("Question")) - return diff --git a/novelwriter/splash.py b/novelwriter/splash.py index f1792a98..8de22019 100644 --- a/novelwriter/splash.py +++ b/novelwriter/splash.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -38,6 +38,14 @@ SPLASH_IMG = Path(__file__).parent / "assets" / "images" / "splash.png" class NSplashScreen(QSplashScreen): + """GUI: App Launch Splash Screen. + + A small splash screen that is shown as novelWriter starts up. Its + primary purpose is to provide user feedback that the app is being + initiated when there are delays in the process while Qt waits for + responses from the OS, or has to load particularly large data sets + like when the system has a lot of fonts installed. + """ __slots__ = ("_color", "_rect", "_text") @@ -52,17 +60,14 @@ class NSplashScreen(QSplashScreen): self._color = QColor(26, 52, 78) self._rect = QRect(144, 110, 440, 30) self._text = "" - return def __del__(self) -> None: # pragma: no cover logger.debug("Delete: NSplashScreen") - return def drawContents(self, painter: QPainter) -> None: """Draw the text message.""" painter.setPen(self._color) painter.drawText(self._rect, Qt.AlignmentFlag.AlignLeft, self._text) - return def showStatus(self, message: str) -> None: """Update the status message.""" @@ -71,4 +76,3 @@ class NSplashScreen(QSplashScreen): if message: logger.info("[Splash] %s", message) sleep(0.025) - return diff --git a/novelwriter/text/comments.py b/novelwriter/text/comments.py index d2a62738..5513430d 100644 --- a/novelwriter/text/comments.py +++ b/novelwriter/text/comments.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from novelwriter.enum import nwComment diff --git a/novelwriter/text/counting.py b/novelwriter/text/counting.py index fc3c749d..7b591884 100644 --- a/novelwriter/text/counting.py +++ b/novelwriter/text/counting.py @@ -22,7 +22,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import re @@ -74,9 +74,11 @@ def preProcessText(text: str, keepHeaders: bool = True) -> list[str]: def standardCounter(text: str) -> tuple[int, int, int]: - """A counter that counts paragraphs, words and characters. - This is the standard counter that includes headings in the word and - character counts. + """Return a standard count. + + A counter that counts paragraphs, words and characters. This is the + standard counter that includes headings in the word and character + counts. """ cCount = 0 wCount = 0 @@ -124,7 +126,9 @@ def standardCounter(text: str) -> tuple[int, int, int]: def bodyTextCounter(text: str) -> tuple[int, int, int]: - """A counter that counts body text words, characters, and characters + """Return a body text count. + + A counter that counts body text words, characters, and characters without white spaces. """ wCount = 0 diff --git a/novelwriter/text/patterns.py b/novelwriter/text/patterns.py index a5d2b3e9..3160eb0d 100644 --- a/novelwriter/text/patterns.py +++ b/novelwriter/text/patterns.py @@ -21,7 +21,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import re @@ -32,18 +32,20 @@ from novelwriter.constants import nwRegEx, nwUnicode class RegExPatterns: + """Compiled RegEx Patterns.""" AMBIGUOUS = (nwUnicode.U_APOS, nwUnicode.U_RSQUO) # Static RegExes _rxUrl = re.compile(nwRegEx.URL, re.ASCII) - _rxWords = re.compile(nwRegEx.WORDS, re.UNICODE) - _rxBreak = re.compile(nwRegEx.BREAK, re.UNICODE) - _rxItalic = re.compile(nwRegEx.FMT_EI, re.UNICODE) - _rxBold = re.compile(nwRegEx.FMT_EB, re.UNICODE) - _rxStrike = re.compile(nwRegEx.FMT_ST, re.UNICODE) - _rxSCPlain = re.compile(nwRegEx.FMT_SC, re.UNICODE) - _rxSCValue = re.compile(nwRegEx.FMT_SV, re.UNICODE) + _rxWords = re.compile(nwRegEx.WORDS) + _rxBreak = re.compile(nwRegEx.BREAK) + _rxItalic = re.compile(nwRegEx.FMT_EI) + _rxBold = re.compile(nwRegEx.FMT_EB) + _rxStrike = re.compile(nwRegEx.FMT_ST) + _rxMark = re.compile(nwRegEx.FMT_HL) + _rxSCPlain = re.compile(nwRegEx.FMT_SC) + _rxSCValue = re.compile(nwRegEx.FMT_SV) @property def url(self) -> re.Pattern: @@ -75,6 +77,11 @@ class RegExPatterns: """Markdown strikethrough style.""" return self._rxStrike + @property + def markdownMark(self) -> re.Pattern: + """Markdown highlight style.""" + return self._rxMark + @property def shortcodePlain(self) -> re.Pattern: """Plain shortcode style.""" @@ -108,7 +115,7 @@ class RegExPatterns: rx.append(f"(?:{qO}[^{qO}]+{qC})") if CONFIG.allowOpenDial: rx.append(f"(?:{qO}.+?$)") - return re.compile("|".join(rx), re.UNICODE) + return re.compile("|".join(rx)) return None @property @@ -118,7 +125,7 @@ class RegExPatterns: qO = re.escape(compact(CONFIG.altDialogOpen)) qC = re.escape(compact(CONFIG.altDialogClose)) qB = r"\B" if (qO == qC or qC in self.AMBIGUOUS) else "" - return re.compile(f"{qO}.*?{qC}{qB}", re.UNICODE) + return re.compile(f"{qO}.*?{qC}{qB}") return None @@ -126,6 +133,7 @@ REGEX_PATTERNS = RegExPatterns() class DialogParser: + """A callable parser for finding dialog regions in text.""" __slots__ = ( "_alternate", "_breakD", "_breakQ", "_dialog", "_enabled", "_mode", @@ -141,7 +149,6 @@ class DialogParser: self._breakD = None self._breakQ = None self._mode = "" - return @property def enabled(self) -> bool: @@ -163,13 +170,11 @@ class DialogParser: # Build narrator break RegExes if narrator := CONFIG.narratorBreak.strip()[:1]: punct = re.escape(".,:;!?") - self._breakD = re.compile(f"{narrator}.*?(?:{narrator}[{punct}]?|$)", re.UNICODE) - self._breakQ = re.compile(f"{narrator}.*?(?:{narrator}[{punct}]?)", re.UNICODE) + self._breakD = re.compile(f"{narrator}.*?(?:{narrator}[{punct}]?|$)") + self._breakQ = re.compile(f"{narrator}.*?(?:{narrator}[{punct}]?)") self._narrator = narrator self._mode = f" {narrator}" - return - def __call__(self, text: str) -> list[tuple[int, int]]: """Caller wrapper for dialogue processing.""" temp: list[int] = [] diff --git a/novelwriter/tools/dictionaries.py b/novelwriter/tools/dictionaries.py index 90f618bc..5cc97021 100644 --- a/novelwriter/tools/dictionaries.py +++ b/novelwriter/tools/dictionaries.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -45,6 +45,12 @@ logger = logging.getLogger(__name__) class GuiDictionaries(NNonBlockingDialog): + """GUI: Spell Check Dictionary Tool. + + A helper tool for downloading and extracting dictionaries to a + location where Enchant can find them. This tool is only needed on + Windows. + """ def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -123,11 +129,8 @@ class GuiDictionaries(NNonBlockingDialog): logger.debug("Ready: GuiDictionaries") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiDictionaries") - return def initDialog(self) -> bool: """Prepare and check that we can proceed.""" @@ -164,7 +167,6 @@ class GuiDictionaries(NNonBlockingDialog): """Capture the user closing the window.""" event.accept() self.softDelete() - return ## # Private Slots @@ -182,7 +184,6 @@ class GuiDictionaries(NNonBlockingDialog): if soxFile: path = Path(soxFile).absolute() self.huInput.setText(str(path)) - return @pyqtSlot() def _doImportHunspell(self) -> None: @@ -202,14 +203,12 @@ class GuiDictionaries(NNonBlockingDialog): self._appendLog(formatException(exc), err=True) else: self._appendLog(procErr, err=True) - return @pyqtSlot() def _doOpenInstallLocation(self) -> None: """Open the dictionary folder.""" if not openExternalPath(Path(self.inPath.text())): SHARED.error("Path not found.") - return ## # Internal Functions @@ -247,4 +246,3 @@ class GuiDictionaries(NNonBlockingDialog): cursor.movePosition(QTextCursor.MoveOperation.End) cursor.deleteChar() self.infoBox.setTextCursor(cursor) - return diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index 4e54d79e..d28fb845 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -42,6 +42,7 @@ logger = logging.getLogger(__name__) class GuiLipsum(NDialog): + """GUI: Lorem Ipsum Text Tool.""" def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -111,11 +112,8 @@ class GuiLipsum(NDialog): logger.debug("Ready: GuiLipsum") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiLipsum") - return @property def lipsumText(self) -> str: @@ -145,4 +143,3 @@ class GuiLipsum(NDialog): pCount = self.paraCount.value() self._lipsumText = "\n\n".join(lipsumText[0:pCount]) + "\n\n" self.close() - return diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py index 22628f43..19070b14 100644 --- a/novelwriter/tools/manusbuild.py +++ b/novelwriter/tools/manusbuild.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -54,7 +54,7 @@ logger = logging.getLogger(__name__) class GuiManuscriptBuild(NDialog): - """GUI Tools: Manuscript Build Dialog + """GUI Tools: Manuscript Build Dialog. This is the tool for running the build itself. It can be accessed independently of the Manuscript Build Tool. @@ -244,11 +244,8 @@ class GuiManuscriptBuild(NDialog): logger.debug("Ready: GuiManuscriptBuild") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiManuscriptBuild") - return ## # Events @@ -261,7 +258,6 @@ class GuiManuscriptBuild(NDialog): self._saveSettings() event.accept() self.softDelete() - return ## # Private Slots @@ -278,7 +274,6 @@ class GuiManuscriptBuild(NDialog): self._openOutputFolder() elif role == QtRoleReject: self.close() - return @pyqtSlot() def _doSelectPath(self) -> None: @@ -290,7 +285,6 @@ class GuiManuscriptBuild(NDialog): ) if savePath: self.buildPath.setText(savePath) - return @pyqtSlot() def _doResetBuildName(self) -> None: @@ -298,13 +292,11 @@ class GuiManuscriptBuild(NDialog): bName = f"{SHARED.project.data.name} - {self._build.name}" self.buildName.setText(bName) self._build.setLastBuildName(bName) - return @pyqtSlot() def _resetProgress(self) -> None: """Set the progress bar back to 0.""" self.buildProgress.setValue(0) - return ## # Internal Functions @@ -371,7 +363,6 @@ class GuiManuscriptBuild(NDialog): pOptions.setValue("GuiManuscriptBuild", "fmtWidth", mainSplit[0]) pOptions.setValue("GuiManuscriptBuild", "sumWidth", mainSplit[1]) pOptions.saveSettings() - return def _populateContentList(self) -> None: """Build the content list.""" @@ -396,9 +387,6 @@ class GuiManuscriptBuild(NDialog): item.setIcon(nwItem.getMainIcon()) self.listContent.addItem(item) - return - def _openOutputFolder(self) -> None: """Open the build folder in the system's file explorer.""" openExternalPath(Path(self.buildPath.text())) - return diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index f1bfb2dc..8c458db3 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -66,7 +66,7 @@ logger = logging.getLogger(__name__) class GuiManuscript(NToolDialog): - """GUI Tools: Manuscript Tool + """GUI Tools: Manuscript Tool. The dialog displays all the users build definitions, a preview panel for the manuscript, and can trigger the actual build dialog to build @@ -251,11 +251,8 @@ class GuiManuscript(NToolDialog): logger.debug("Ready: GuiManuscript") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiManuscript") - return def loadContent(self) -> None: """Load dialog content from project data.""" @@ -272,8 +269,6 @@ class GuiManuscript(NToolDialog): self.buildList.setCurrentItem(self._buildMap[selected]) QTimer.singleShot(200, self._generatePreview) - return - ## # Events ## @@ -290,7 +285,6 @@ class GuiManuscript(NToolDialog): obj.close() event.accept() self.softDelete() - return ## # Private Slots @@ -302,14 +296,12 @@ class GuiManuscript(NToolDialog): build = BuildSettings() build.setName(self.tr("My Manuscript")) self._openSettingsDialog(build) - return @pyqtSlot() def _editSelectedBuild(self) -> None: """Edit the currently selected build settings entry.""" if build := self._getSelectedBuild(): self._openSettingsDialog(build) - return @pyqtSlot() def _copySelectedBuild(self) -> None: @@ -320,14 +312,12 @@ class GuiManuscript(NToolDialog): self._updateBuildsList() if item := self._buildMap.get(new.buildID): item.setSelected(True) - return @pyqtSlot("QListWidgetItem*", "QListWidgetItem*") def _updateBuildDetails(self, current: QListWidgetItem, previous: QListWidgetItem) -> None: """Process change of build selection to update the details.""" if current and (build := self._builds.getBuild(current.data(self.D_KEY))): self.buildDetails.updateInfo(build) - return @pyqtSlot() def _deleteSelectedBuild(self) -> None: @@ -338,7 +328,6 @@ class GuiManuscript(NToolDialog): dialog.close() self._builds.removeBuild(build.buildID) self._updateBuildsList() - return @pyqtSlot(BuildSettings) def _processNewSettings(self, build: BuildSettings) -> None: @@ -347,7 +336,6 @@ class GuiManuscript(NToolDialog): self._updateBuildItem(build) if (current := self.buildList.currentItem()) and current.data(self.D_KEY) == build.buildID: self._updateBuildDetails(current, current) - return @pyqtSlot() def _generatePreview(self) -> None: @@ -399,15 +387,12 @@ class GuiManuscript(NToolDialog): if build.changed: self._builds.setBuild(build) - return - @pyqtSlot() def _printDocument(self) -> None: """Open the print preview dialog.""" preview = QPrintPreviewDialog(self) preview.paintRequested.connect(self.docPreview.printPreview) preview.exec() - return ## # Internal Functions @@ -456,8 +441,6 @@ class GuiManuscript(NToolDialog): pOptions.setValue("GuiManuscript", "showNewPage", showNewPage) pOptions.saveSettings() - return - def _openSettingsDialog(self, build: BuildSettings) -> None: """Open the build settings dialog.""" if dialog := self._findSettingsDialog(build.buildID): @@ -482,7 +465,6 @@ class GuiManuscript(NToolDialog): bItem.setData(self.D_KEY, key) self.buildList.addItem(bItem) self._buildMap[key] = bItem - return def _updateBuildItem(self, build: BuildSettings) -> None: """Update the entry of a specific build item.""" @@ -490,7 +472,6 @@ class GuiManuscript(NToolDialog): item.setText(build.name) else: # Probably a new item self._updateBuildsList() - return def _findSettingsDialog(self, buildID: str) -> GuiBuildSettings | None: """Return an open build settings dialog for a given build, if @@ -521,8 +502,6 @@ class _DetailsWidget(QWidget): self.outerBox.setContentsMargins(0, 0, 0, 0) self.setLayout(self.outerBox) - return - ## # Getters ## @@ -547,7 +526,6 @@ class _DetailsWidget(QWidget): def setColumnWidth(self, value: int) -> None: """Set the width of the first column.""" self.listView.setColumnWidth(0, value) - return def setExpandedState(self, state: list[bool]) -> None: """Set the expanded state of each top level item.""" @@ -556,7 +534,6 @@ class _DetailsWidget(QWidget): item = self.listView.topLevelItem(i) if isinstance(item, QTreeWidgetItem): item.setExpanded((state[i] if i < count else True) and item.childCount() > 0) - return ## # Methods @@ -637,8 +614,6 @@ class _DetailsWidget(QWidget): # Restore expanded state self.setExpandedState(expanded) - return - class _OutlineWidget(QWidget): @@ -663,8 +638,6 @@ class _OutlineWidget(QWidget): self.outerBox.setContentsMargins(0, 0, 0, 0) self.setLayout(self.outerBox) - return - def updateOutline(self, data: dict[str, str]) -> None: """Update the outline.""" if isinstance(data, dict) and data != self._outline: @@ -705,8 +678,6 @@ class _OutlineWidget(QWidget): self.listView.setIndentation(SHARED.theme.baseIconHeight if indent else 4) self._outline = data - return - ## # Private Slots ## @@ -714,7 +685,6 @@ class _OutlineWidget(QWidget): def _onItemClick(self, item: QTreeWidgetItem) -> None: """Process tree item click.""" self.outlineEntryClicked.emit(str(item.data(0, self.D_LINE))) - return class _PreviewWidget(QTextBrowser): @@ -787,8 +757,6 @@ class _PreviewWidget(QTextBrowser): self.ageTimer.timeout.connect(self._updateBuildAge) self.ageTimer.start() - return - ## # Setters ## @@ -797,7 +765,6 @@ class _PreviewWidget(QTextBrowser): """Set the build name for the document label.""" self._buildName = name self._updateBuildAge() - return def setTextFont(self, font: QFont) -> None: """Set the text font properties and then reset for sub-widgets. @@ -807,7 +774,6 @@ class _PreviewWidget(QTextBrowser): self.setFont(font) self.buildProgress.setFont(SHARED.theme.guiFont) self.ageLabel.setFont(SHARED.theme.guiFontSmall) - return ## # Methods @@ -823,13 +789,11 @@ class _PreviewWidget(QTextBrowser): self._scrollPos = vBar.value() self.setPlaceholderText("") self.clear() - return def buildStep(self, value: int) -> None: """Update the progress bar value.""" self.buildProgress.setValue(value) QApplication.processEvents() - return def setContent(self, document: QTextDocument) -> None: """Set the content of the preview widget.""" @@ -850,8 +814,6 @@ class _PreviewWidget(QTextBrowser): QApplication.processEvents() QTimer.singleShot(300, self._postUpdate) - return - ## # Events ## @@ -860,7 +822,6 @@ class _PreviewWidget(QTextBrowser): """Capture resize and update the document margins.""" super().resizeEvent(event) self._updateDocMargins() - return ## # Public Slots @@ -874,14 +835,12 @@ class _PreviewWidget(QTextBrowser): printer.setPageOrientation(QPageLayout.Orientation.Portrait) document.print(printer) QApplication.restoreOverrideCursor() - return @pyqtSlot(str) def navigateTo(self, anchor: str) -> None: """Go to a specific #link in the document.""" logger.debug("Moving to anchor '#%s'", anchor) self.setSource(QUrl(f"#{anchor}")) - return ## # Private Slots @@ -896,7 +855,6 @@ class _PreviewWidget(QTextBrowser): self.navigateTo(link.lstrip("#")) elif link.startswith("http"): QDesktopServices.openUrl(QUrl(url)) - return @pyqtSlot() def _updateBuildAge(self) -> None: @@ -909,7 +867,6 @@ class _PreviewWidget(QTextBrowser): )) else: self.ageLabel.setText("{0}".format(self.tr("No Preview"))) - return @pyqtSlot() def _postUpdate(self) -> None: @@ -917,7 +874,6 @@ class _PreviewWidget(QTextBrowser): self.buildProgress.setVisible(False) if vBar := self.verticalScrollBar(): vBar.setValue(self._scrollPos) - return ## # Internal Functions @@ -936,7 +892,6 @@ class _PreviewWidget(QTextBrowser): self.ageLabel.setGeometry(tB, tB, vW, tH) self.setViewportMargins(0, tH, 0, 0) self.buildProgress.move((vW-pS)//2, (vH-pS)//2) - return class _StatsWidget(QWidget): @@ -969,8 +924,6 @@ class _StatsWidget(QWidget): self._toggleView(False) - return - def updateStats(self, data: dict[str, int]) -> None: """Update the stats values from a Tokenizer stats dict.""" # Minimal @@ -992,8 +945,6 @@ 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}") - return - ## # Private Slots ## @@ -1013,7 +964,6 @@ class _StatsWidget(QWidget): self.minWidget.adjustSize() self.mainStack.adjustSize() self.adjustSize() - return ## # Internal Functions @@ -1105,5 +1055,3 @@ class _StatsWidget(QWidget): self.minWidget.setLayout(self.minLayout) self.maxWidget.setLayout(self.maxLayout) - - return diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 6fd4baa5..e8bf94b6 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -62,7 +62,7 @@ logger = logging.getLogger(__name__) class GuiBuildSettings(NToolDialog): - """GUI Tools: Manuscript Build Settings Dialog + """GUI Tools: Manuscript Build Settings Dialog. The main tool for configuring manuscript builds. It's a GUI tool for editing JSON build definitions, wrapped as a BuildSettings object. @@ -153,11 +153,8 @@ class GuiBuildSettings(NToolDialog): logger.debug("Ready: GuiBuildSettings") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiBuildSettings") - return def loadContent(self) -> None: """Populate the child widgets.""" @@ -165,7 +162,6 @@ class GuiBuildSettings(NToolDialog): self.optTabSelect.loadContent() self.optTabHeadings.loadContent() self.optTabFormatting.loadContent() - return ## # Properties @@ -190,7 +186,6 @@ class GuiBuildSettings(NToolDialog): self._saveSettings() event.accept() self.softDelete() - return ## # Private Slots @@ -206,7 +201,6 @@ class GuiBuildSettings(NToolDialog): elif pageId >= self.OPT_FORMATTING: self.toolStack.setCurrentWidget(self.optTabFormatting) self.optTabFormatting.scrollToSection(pageId) - return @pyqtSlot("QAbstractButton*") def _dialogButtonClicked(self, button: QAbstractButton) -> None: @@ -222,7 +216,6 @@ class GuiBuildSettings(NToolDialog): elif role == QtRoleReject: self._build.resetChangedState() self.close() - return ## # Internal Functions @@ -238,7 +231,6 @@ class GuiBuildSettings(NToolDialog): ).format(self._build.name)): self._emitBuildData() self._build.resetChangedState() - return def _saveSettings(self) -> None: """Save the various user settings.""" @@ -250,20 +242,17 @@ class GuiBuildSettings(NToolDialog): pOptions.setValue("GuiBuildSettings", "treeWidth", treeWidth) pOptions.setValue("GuiBuildSettings", "filterWidth", filterWidth) pOptions.saveSettings() - return def _applyChanges(self) -> None: """Apply all settings changes to the build object.""" self._build.setName(self.editBuildName.text()) self.optTabHeadings.saveContent() self.optTabFormatting.saveContent() - return def _emitBuildData(self) -> None: """Assemble the build data and emit the signal.""" self.newSettingsReady.emit(self._build) self._build.resetChangedState() - return class _FilterTab(NFixedPage): @@ -382,13 +371,10 @@ class _FilterTab(NFixedPage): self.setCentralWidget(self.mainSplit) - return - def loadContent(self) -> None: """Populate the widgets.""" self._populateTree() self._populateFilters() - return def mainSplitSizes(self) -> tuple[int, int]: """Extract the sizes of the main splitter.""" @@ -409,7 +395,6 @@ class _FilterTab(NFixedPage): elif key.startswith("root:"): self._build.setAllowRoot(key[5:], state) self._populateTree() - return ## # Internal Functions @@ -453,8 +438,6 @@ class _FilterTab(NFixedPage): self._setTreeItemMode() - return - def _populateFilters(self) -> None: """Populate the filter options switches.""" self.filterOpt.clear() @@ -489,8 +472,6 @@ class _FilterTab(NFixedPage): default=self._build.isRootAllowed(tHandle) ) - return - def _setSelectedMode(self, mode: int) -> None: """Set the mode for the selected items.""" items = self.optTree.selectedItems() @@ -511,8 +492,6 @@ class _FilterTab(NFixedPage): self._setTreeItemMode() - return - def _setTreeItemMode(self) -> None: """Update the filtered mode icon on all items.""" filtered = self._build.buildItemFilter(SHARED.project) @@ -529,11 +508,10 @@ class _FilterTab(NFixedPage): item.setToolTip(self.C_STATUS, self._trIncluded) else: item.setIcon(self.C_STATUS, self._statusFlags[self.F_NONE]) - return def _scanChildren(self, item: QTreeWidgetItem | None, items: list) -> list[QTreeWidgetItem]: - """This is a recursive function returning all items in a tree - starting at a given QTreeWidgetItem. + """Recursively return all items in a tree starting at a given + QTreeWidgetItem. """ if isinstance(item, QTreeWidgetItem): items.append(item) @@ -791,8 +769,6 @@ class _HeadingsTab(NScrollablePage): self.setCentralLayout(self.outerBox) - return - def loadContent(self) -> None: """Populate the widgets.""" def fmtBreak(text: str) -> str: @@ -821,7 +797,6 @@ class _HeadingsTab(NScrollablePage): self.breakPart.setChecked(self._build.getBool("headings.breakPart")) self.breakChapter.setChecked(self._build.getBool("headings.breakChapter")) self.breakScene.setChecked(self._build.getBool("headings.breakScene")) - return def saveContent(self) -> None: """Save choices back into build object.""" @@ -841,7 +816,6 @@ class _HeadingsTab(NScrollablePage): self._build.setValue("headings.breakPart", self.breakPart.isChecked()) self._build.setValue("headings.breakChapter", self.breakChapter.isChecked()) self._build.setValue("headings.breakScene", self.breakScene.isChecked()) - return ## # Internal Functions @@ -853,7 +827,6 @@ class _HeadingsTab(NScrollablePage): cursor = self.editTextBox.textCursor() cursor.insertText(text) self.editTextBox.setFocus() - return def _editHeading(self, heading: int) -> None: """Populate the form with a specific heading format.""" @@ -886,8 +859,6 @@ class _HeadingsTab(NScrollablePage): self.editTextBox.setPlainText(text.replace(nwUnicode.U_LBREAK, "\n")) self.lblEditForm.setText(self.tr("Editing: {0}").format(label)) - return - ## # Private Slots ## @@ -934,7 +905,6 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter): self._fmtSymbol.setForeground(syntax.head) self._fmtFormat = QTextCharFormat() self._fmtFormat.setForeground(syntax.emph) - return def highlightBlock(self, text: str) -> None: """Add syntax highlighting to the text block.""" @@ -947,7 +917,6 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter): ddots = heading.find(":") if ddots > 0: self.setFormat(pos + ddots, 1, self._fmtSymbol) - return class _FormattingTab(NScrollableForm): @@ -961,8 +930,6 @@ class _FormattingTab(NScrollableForm): self.setHelpTextStyle(SHARED.theme.helpText) self.buildForm() - return - def buildForm(self) -> None: """Build the formatting form.""" section = 10 @@ -1290,8 +1257,6 @@ class _FormattingTab(NScrollableForm): # Finalise self.finalise() - return - def loadContent(self) -> None: """Populate the widgets.""" # Text Content @@ -1391,8 +1356,6 @@ class _FormattingTab(NScrollableForm): self.htmlAddStyles.setChecked(self._build.getBool("html.addStyles")) self.htmlPreserveTabs.setChecked(self._build.getBool("html.preserveTabs")) - return - def saveContent(self) -> None: """Save choices back into build object.""" # Text Content @@ -1458,8 +1421,6 @@ class _FormattingTab(NScrollableForm): self._build.setValue("html.addStyles", self.htmlAddStyles.isChecked()) self._build.setValue("html.preserveTabs", self.htmlPreserveTabs.isChecked()) - return - ## # Private Slots ## @@ -1472,11 +1433,10 @@ class _FormattingTab(NScrollableForm): self._textFont = fontMatcher(font) self.textFont.setText(describeFont(self._textFont)) self.textFont.setCursorPosition(0) - return @pyqtSlot(int) def _changeUnit(self, index: int) -> None: - """The current unit change, so recalculate sizes.""" + """Process current unit change to recalculate sizes.""" newUnit = self.pageUnit.itemData(index) newScale = nwLabels.UNIT_SCALE.get(newUnit, 1.0) reScale = self._unitScale/newScale @@ -1531,11 +1491,9 @@ class _FormattingTab(NScrollableForm): self._unitScale = newScale self._changePageSize(self.pageSize.currentIndex()) - return - @pyqtSlot(int) def _changePageSize(self, index: int) -> None: - """The page size has changed.""" + """Process page size change.""" w, h = nwLabels.PAPER_SIZE[self.pageSize.itemData(index)] if index >= 0 else (-1.0, -1.0) if w > 0.0 and h > 0.0: self.pageWidth.blockSignals(True) @@ -1544,23 +1502,20 @@ class _FormattingTab(NScrollableForm): self.pageHeight.blockSignals(True) self.pageHeight.setValue(h/self._unitScale) self.pageHeight.blockSignals(False) - return @pyqtSlot() def _pageSizeValueChanged(self) -> None: - """The user has changed the page size spin boxes, so we flip - the page size box to Custom. + """Process that the user has changed the page size spin boxes, + so we flip the page size box to Custom. """ index = self.pageSize.findData("Custom") if index >= 0: self.pageSize.setCurrentIndex(index) - return def _resetPageHeader(self) -> None: """Reset the ODT header format to default.""" self.odtPageHeader.setText(nwHeadFmt.DOC_AUTO) self.odtPageHeader.setCursorPosition(0) - return ## # Internal Functions @@ -1573,4 +1528,3 @@ class _FormattingTab(NScrollableForm): current.append(keyword) verified = set(x for x in current if x in nwKeyWords.VALID_KEYS) self.ignoredKeywords.setText(", ".join(verified)) - return diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py index e264df62..7447b553 100644 --- a/novelwriter/tools/noveldetails.py +++ b/novelwriter/tools/noveldetails.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -52,6 +52,7 @@ logger = logging.getLogger(__name__) class GuiNovelDetails(NNonBlockingDialog): + """GUI: Novel Details Tool.""" PAGE_OVERVIEW = 1 PAGE_CONTENTS = 2 @@ -130,11 +131,8 @@ class GuiNovelDetails(NNonBlockingDialog): logger.debug("Ready: GuiNovelDetails") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiNovelDetails") - return ## # Methods @@ -146,7 +144,6 @@ class GuiNovelDetails(NNonBlockingDialog): self.overviewPage.updateProjectData() self.overviewPage.novelValueChanged(handle) self.contentsPage.novelValueChanged(handle) - return ## # Events @@ -157,7 +154,6 @@ class GuiNovelDetails(NNonBlockingDialog): self._saveSettings() event.accept() self.softDelete() - return ## # Private Slots @@ -170,7 +166,6 @@ class GuiNovelDetails(NNonBlockingDialog): self.mainStack.setCurrentWidget(self.overviewPage) elif pageId == self.PAGE_CONTENTS: self.mainStack.setCurrentWidget(self.contentsPage) - return ## # Internal Functions @@ -185,7 +180,6 @@ class GuiNovelDetails(NNonBlockingDialog): options.setValue("GuiNovelDetails", "winHeight", self.height()) options.setValue("GuiNovelDetails", "novelRoot", novelRoot) self.contentsPage.saveSettings() - return class _OverviewPage(NScrollablePage): @@ -248,8 +242,6 @@ class _OverviewPage(NScrollablePage): self.setCentralLayout(self.outerBox) - return - ## # Methods ## @@ -266,7 +258,6 @@ class _OverviewPage(NScrollablePage): self.projWords.setText(f"{wcNovel + wcNotes:n}") self.projNovels.setText(f"{wcNovel:n}") self.projNotes.setText(f"{wcNotes:n}") - return ## # Public Slots @@ -286,8 +277,6 @@ class _OverviewPage(NScrollablePage): self.novelChapters.setText(f"{hCounts[2]:n}") self.novelScenes.setText(f"{hCounts[3]:n}") - return - class _ContentsPage(NFixedPage): @@ -400,8 +389,6 @@ class _ContentsPage(NFixedPage): self.setCentralLayout(self.outerBox) - return - def saveSettings(self) -> None: """Save the user GUI settings.""" options = SHARED.project.options @@ -413,7 +400,6 @@ class _ContentsPage(NFixedPage): options.setValue("GuiNovelDetails", "wordsPerPage", self.wpValue.value()) options.setValue("GuiNovelDetails", "countFrom", self.poValue.value()) options.setValue("GuiNovelDetails", "clearDouble", self.dblValue.isChecked()) - return ## # Public Slots @@ -426,7 +412,6 @@ class _ContentsPage(NFixedPage): self._prepareData(tHandle) self._populateTree() self._currentRoot = tHandle - return ## # Private Slots @@ -496,8 +481,6 @@ class _ContentsPage(NFixedPage): self.tocTree.addTopLevelItem(newItem) - return - ## # Internal Functions ## @@ -507,4 +490,3 @@ class _ContentsPage(NFixedPage): logger.debug("Populating ToC from handle '%s'", rootHandle) self._data = SHARED.project.index.getTableOfContents(rootHandle, 2) self._data.append(("", 0, self.tr("END"), 0)) - return diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index 9b0abe33..69d2e090 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -32,7 +32,7 @@ from PyQt6.QtCore import ( QAbstractListModel, QModelIndex, QObject, QPoint, QSize, Qt, pyqtSignal, pyqtSlot ) -from PyQt6.QtGui import QAction, QCloseEvent, QColor, QFont, QPainter, QPaintEvent, QPen, QShortcut +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, @@ -56,6 +56,11 @@ PANEL_ALPHA = 178 class GuiWelcome(NDialog): + """GUI: Welcome Dialog. + + This is the main dialog shown when novelWriter launches or when the + user wants to create or open another project. + """ openProjectRequest = pyqtSignal(Path) @@ -77,7 +82,6 @@ class GuiWelcome(NDialog): self.bgImage = SHARED.theme.getDecoration("welcome") self.nwImage = SHARED.theme.getDecoration("nw-text", h=36) - self.bgColor = QColor(54, 54, 54) if SHARED.theme.isDarkTheme else QColor(255, 255, 255) self.nwLogo = QLabel(self) self.nwLogo.setPixmap(SHARED.theme.getPixmap("novelwriter", (128, 128))) @@ -162,11 +166,8 @@ class GuiWelcome(NDialog): logger.debug("Ready: GuiWelcome") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiWelcome") - return ## # Events @@ -178,17 +179,15 @@ class GuiWelcome(NDialog): hPix = min(hWin, 600) tMode = Qt.TransformationMode.SmoothTransformation painter = QPainter(self) - painter.fillRect(self.rect(), self.bgColor) painter.drawPixmap(0, hWin - hPix, self.bgImage.scaledToHeight(hPix, tMode)) + painter.end() super().paintEvent(event) - return def closeEvent(self, event: QCloseEvent) -> None: """Capture the user closing the window and save settings.""" self._saveSettings() event.accept() self.softDelete() - return ## # Private Slots @@ -200,28 +199,24 @@ class GuiWelcome(NDialog): self.mainStack.setCurrentWidget(self.tabNew) self._setButtonVisibility() self.tabNew.enterForm() - return @pyqtSlot() def _showOpenProjectPage(self) -> None: """Show the open exiting project page.""" self.mainStack.setCurrentWidget(self.tabOpen) self._setButtonVisibility() - return @pyqtSlot() def _browseForProject(self) -> None: """Browse for a project to open.""" if path := SHARED.getProjectPath(self, path=CONFIG.homePath(), allowZip=False): self._openProjectPath(path) - return @pyqtSlot() def _openSelectedItem(self) -> None: """Open the currently selected project item.""" if self.mainStack.currentWidget() == self.tabOpen: self.tabOpen.openSelectedItem() - return @pyqtSlot(Path) def _openProjectPath(self, path: Path) -> None: @@ -232,7 +227,6 @@ class GuiWelcome(NDialog): self.hide() self.openProjectRequest.emit(path) self.close() - return ## # Internal Functions @@ -242,7 +236,6 @@ class GuiWelcome(NDialog): """Save the user GUI settings.""" logger.debug("Saving State: GuiWelcome") CONFIG.setWelcomeWinSize(self.width(), self.height()) - return def _setButtonVisibility(self) -> None: """Change the visibility of the dialog buttons.""" @@ -256,7 +249,6 @@ class GuiWelcome(NDialog): self.btnOpen.setFocus() else: self.btnCreate.setFocus() - return class _OpenProjectPage(QWidget): @@ -309,8 +301,6 @@ class _OpenProjectPage(QWidget): f"QLineEdit {{border: none; background: {baseCol}; padding: 4px;}} " ) - return - ## # Public Slots ## @@ -320,7 +310,6 @@ class _OpenProjectPage(QWidget): """Open the currently selected project item.""" if (selection := self.listWidget.selectedIndexes()) and (index := selection[0]).isValid(): self.openProjectRequest.emit(Path(str(index.data()[1]))) - return ## # Private Slots @@ -336,14 +325,12 @@ class _OpenProjectPage(QWidget): self.selectedPath.setToolTip(text) self.selectedPath.setCursorPosition(0) self.aMissing.setVisible(not (Path(value) / nwFiles.PROJ_FILE).is_file()) - return @pyqtSlot(QModelIndex) def _projectDoubleClicked(self, index: QModelIndex) -> None: """Process double click on project item.""" if index.isValid(): self.openProjectRequest.emit(Path(str(index.data()[1]))) - return @pyqtSlot() def _deleteSelectedItem(self) -> None: @@ -356,7 +343,6 @@ class _OpenProjectPage(QWidget): if SHARED.question(text): self.listModel.removeEntry(index) self._selectFirstItem() - return @pyqtSlot("QPoint") def _openContextMenu(self, pos: QPoint) -> None: @@ -369,7 +355,6 @@ class _OpenProjectPage(QWidget): action.triggered.connect(self._deleteSelectedItem) ctxMenu.exec(self.mapToGlobal(pos)) ctxMenu.setParent(None) - return ## # Internal Functions @@ -380,7 +365,6 @@ class _OpenProjectPage(QWidget): index = self.listModel.index(0) self.listWidget.setCurrentIndex(index) self._projectClicked(index) - return class _ProjectListItem(QStyledItemDelegate): @@ -408,8 +392,6 @@ class _ProjectListItem(QStyledItemDelegate): self._icon = SHARED.theme.getPixmap("proj_nwx", (iPx, iPx)) - return - def paint(self, painter: QPainter, opt: QStyleOptionViewItem, index: QModelIndex) -> None: """Paint a project entry on the canvas.""" rect = opt.rect @@ -420,7 +402,7 @@ class _ProjectListItem(QStyledItemDelegate): painter.save() if opt.state & QtSelected == QtSelected: painter.setOpacity(0.25) - painter.fillRect(rect, QApplication.palette().highlight()) + painter.fillRect(rect, QApplication.palette().text()) painter.setOpacity(1.0) painter.drawPixmap(2, rect.top() + 6, self._icon) @@ -431,8 +413,6 @@ class _ProjectListItem(QStyledItemDelegate): painter.drawText(rect.adjusted(x, y, 0, 0), tFlag, details) painter.restore() - return - def sizeHint(self, opt: QStyleOptionViewItem, index: QModelIndex) -> QSize: """Set the size hint to fixed height.""" return QSize(opt.rect.width(), self._hPx) @@ -450,7 +430,6 @@ class _ProjectListModel(QAbstractListModel): when = CONFIG.localDate(datetime.fromtimestamp(time)) data.append((title, path, f"{opened}: {when}, {words}: {formatInt(count)}")) self._data = data - return def rowCount(self, parent: QModelIndex | None = None) -> int: """Return the size of the model.""" @@ -517,8 +496,6 @@ class _NewProjectPage(QWidget): f"_NewProjectForm {{border: none; background: {baseCol};}} " ) - return - ## # Public Slots ## @@ -690,13 +667,10 @@ class _NewProjectForm(QWidget): self._updateProjPath() self._updateFillInfo() - return - def enterForm(self) -> None: """Focus the project name field when entering the form.""" self.projName.setFocus() self.projName.selectAll() - return def getProjectData(self) -> dict: """Collect form data and return it as a dictionary.""" @@ -734,14 +708,12 @@ class _NewProjectForm(QWidget): self._basePath = Path(path) self._updateProjPath() CONFIG.setLastPath("project", path) - return @pyqtSlot() def _updateProjPath(self) -> None: """Update the path box to show the full project path.""" projName = makeFileNameSafe(self.projName.text().strip()) self.projPath.setText(str(self._basePath / projName)) - return @pyqtSlot() def _syncSwitches(self) -> None: @@ -751,21 +723,18 @@ class _NewProjectForm(QWidget): addWorld = self.addWorld.isChecked() if not (addPlot or addChar or addWorld): self.addNotes.setChecked(False) - return @pyqtSlot() def _setFillBlank(self) -> None: """Set fill mode to blank project.""" self._fillMode = self.FILL_BLANK self._updateFillInfo() - return @pyqtSlot() def _setFillSample(self) -> None: """Set fill mode to sample project.""" self._fillMode = self.FILL_SAMPLE self._updateFillInfo() - return @pyqtSlot() def _setFillCopy(self) -> None: @@ -774,7 +743,6 @@ class _NewProjectForm(QWidget): self._fillMode = self.FILL_COPY self._copyPath = copyPath self._updateFillInfo() - return ## # Internal Functions @@ -794,5 +762,3 @@ class _NewProjectForm(QWidget): self.projFill.setToolTip(text) self.projFill.setCursorPosition(0) self.extraWidget.setVisible(self._fillMode == self.FILL_BLANK) - - return diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index 0cbb9124..4494da8f 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import json @@ -54,7 +54,7 @@ logger = logging.getLogger(__name__) class GuiWritingStats(NToolDialog): - """GUI Tools: Writing Statistics + """GUI Tools: Writing Statistics. Displays data from the NWSessionLog object. """ @@ -311,11 +311,8 @@ class GuiWritingStats(NToolDialog): logger.debug("Ready: GuiWritingStats") - return - def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiWritingStats") - return def populateGUI(self) -> None: """Populate list box with data from the log file.""" @@ -323,7 +320,6 @@ class GuiWritingStats(NToolDialog): self._loadLogFile() self._updateListBox() QApplication.restoreOverrideCursor() - return ## # Events @@ -333,7 +329,6 @@ class GuiWritingStats(NToolDialog): """Capture the user closing the window.""" event.accept() self.softDelete() - return ## # Private Slots @@ -377,8 +372,6 @@ class GuiWritingStats(NToolDialog): self.close() - return - def _saveData(self, dataFmt: int) -> bool: """Save the content of the list box to a file.""" fileExt = "" @@ -498,8 +491,6 @@ class GuiWritingStats(NToolDialog): self.notesWords.setText(f"{ttNotes:n}") self.totalWords.setText(f"{ttWords:n}") - return - ## # Private Slots ## @@ -622,5 +613,3 @@ class GuiWritingStats(NToolDialog): self.timeFilter += sDiff self.labelFilter.setText(formatTime(round(self.timeFilter))) - - return diff --git a/novelwriter/types.py b/novelwriter/types.py index f7f7a36d..0d5e64d8 100644 --- a/novelwriter/types.py +++ b/novelwriter/types.py @@ -20,7 +20,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from PyQt6.QtCore import Qt @@ -54,6 +54,8 @@ QtPageBreakBefore = QTextFormat.PageBreakFlag.PageBreak_AlwaysBefore QtPageBreakAfter = QTextFormat.PageBreakFlag.PageBreak_AlwaysAfter QtPageBreakAuto = QTextFormat.PageBreakFlag.PageBreak_Auto +QtTextUserProperty = QTextFormat.Property.UserProperty + QtPropLineHeight = 1 # QTextBlockFormat.LineHeightTypes.ProportionalHeight # Qt Painter Types @@ -128,6 +130,7 @@ QtHeaderFixed = QHeaderView.ResizeMode.Fixed # Scroll Bar Policy +QtScrollAlwaysOn = Qt.ScrollBarPolicy.ScrollBarAlwaysOn QtScrollAlwaysOff = Qt.ScrollBarPolicy.ScrollBarAlwaysOff QtScrollAsNeeded = Qt.ScrollBarPolicy.ScrollBarAsNeeded diff --git a/pkgutils.py b/pkgutils.py index f71ea2f2..29d6983f 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -23,7 +23,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import argparse @@ -50,7 +50,6 @@ OS_WIN = sys.platform.startswith("win32") def printVersion(args: argparse.Namespace) -> None: """Print the novelWriter version and exit.""" print(extractVersion(beQuiet=True)[0], end=None) - return def installPackages(args: argparse.Namespace) -> None: @@ -79,8 +78,6 @@ def installPackages(args: argparse.Namespace) -> None: print(str(exc)) sys.exit(1) - return - def cleanBuildDirs(args: argparse.Namespace) -> None: """Recursively delete the 'build' and 'dist' folders.""" @@ -111,8 +108,6 @@ def cleanBuildDirs(args: argparse.Namespace) -> None: print("") - return - def genMacOSPlist(args: argparse.Namespace) -> None: """Set necessary values for .plist file for MacOS build.""" @@ -134,8 +129,6 @@ def genMacOSPlist(args: argparse.Namespace) -> None: print(f"Writing Info.plist to {outDir}/Info.plist") writeFile(outDir / "Info.plist", plistXML) - return - if __name__ == "__main__": """Parse command line options and run the commands.""" diff --git a/pyproject.toml b/pyproject.toml index 15736ca4..ce9fe191 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,26 +63,34 @@ preview = true # Rules: https://docs.astral.sh/ruff/rules select = [ - "A", # flake8-builtins (A) - "ANN", # flake8-annotations (ANN) - "B", # flake8-bugbear (B) - "E", # pycodestyle (E) - "F", # Pyflakes (F) - "FA", # flake8-future-annotations (FA) - "PERF", # Perflint (PERF) - "PLC", # Pylint Convention (PLC) - "PLE", # Pylint Error (PLE) - "PLW", # Pylint Warning (PLW) - "Q", # flake8-quotes (Q) - "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 + "D105", # undocumented-magic-method + "D107", # undocumented-public-init + "D203", # incorrect-blank-line-before-class + "D205", # missing-blank-line-after-summary + "D213", # multi-line-summary-second-line "E221", # multiple-spaces-before-operator "E226", # missing-whitespace-around-arithmetic-operator "E228", # missing-whitespace-around-modulo-operator @@ -92,6 +100,7 @@ ignore = [ "PLC1901", # compare-to-empty-string "PLW0108", # unnecessary-lambda "PLW2901", # redefined-loop-name + "RET505", # superfluous-else-return "RUF001", # ambiguous-unicode-character-string "RUF002", # ambiguous-unicode-character-docstring "RUF015", # unnecessary-iterable-allocation-for-first-element @@ -100,9 +109,21 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] -"tests/*" = ["ANN", "SLF", "TC", "PLC2701"] +"tests/*" = ["ANN", "SLF", "TC", "PLC2701", "D101", "D102"] "utils/*" = ["ANN", "SLF", "TC"] +[tool.ruff.lint.pydocstyle] +ignore-decorators = [ + "abc.abstractmethod", + "property", + "PyQt6.QtCore.pyqtProperty", + "typing.overload", + "pytest.fixture", +] + +[tool.ruff.lint.pylint] +max-nested-blocks = 10 + [tool.ruff.format] quote-style = "double" @@ -111,7 +132,6 @@ include = ["novelwriter"] exclude = ["**/__pycache__"] reportIncompatibleMethodOverride = false - pythonVersion = "3.10" [tool.pytest.ini_options] @@ -121,7 +141,6 @@ markers = [ "base: Base classes tests", "core: Core classes tests", "gui: GUI classes tests", - "serial", ] [tool.coverage.run] diff --git a/requirements-all.txt b/requirements-all.txt new file mode 100644 index 00000000..d140ead6 --- /dev/null +++ b/requirements-all.txt @@ -0,0 +1,5 @@ +-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 index 18c97a30..794644e0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ +build isort pyright ruff diff --git a/run_tests.py b/run_tests.py new file mode 100755 index 00000000..b9687aa9 --- /dev/null +++ b/run_tests.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Run novelWriter Tests.""" + +import argparse +import os +import subprocess +import sys + +if __name__ == "__main__": + """Parse command line options and run the commands.""" + parser = argparse.ArgumentParser(usage="run_tests.py [--flags]") + 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") + + args = parser.parse_args() + + env = os.environ.copy() + env["QT_SCALE_FACTOR"] = "1.0" + cmd = [sys.executable, "-m", "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.m: + cmd += ["-m", args.m] + if args.k: + cmd += ["-k", args.k] + + print("Calling:", " ".join(cmd)) + subprocess.call(cmd, env=env) diff --git a/sample/content/14298de4d9524.nwd b/sample/content/14298de4d9524.nwd index b37ae5a8..bf0d1e32 100644 --- a/sample/content/14298de4d9524.nwd +++ b/sample/content/14298de4d9524.nwd @@ -1,12 +1,12 @@ %%~name: John Smith %%~path: f7e2d9f330615/14298de4d9524 %%~kind: CHARACTER/NOTE -%%~hash: f85b4da6ba4a557db0c4d5f1b59f33ba01542606 -%%~date: Unknown/2024-01-30 10:58:18 +%%~hash: d344894f612ee7b6f75e60a8d2cfdf32c78d1b0a +%%~date: Unknown/2025-07-16 12:09:00 # John Smith @tag: John | John Smith -% Short: The sidekick +%Short: The sidekick He’s pretty cool. Not Brad Pitt though. diff --git a/sample/content/5aec885635c85.nwd b/sample/content/5aec885635c85.nwd index 376e4f9a..e024df62 100644 --- a/sample/content/5aec885635c85.nwd +++ b/sample/content/5aec885635c85.nwd @@ -1,7 +1,7 @@ %%~name: Scene -%%~path: 09f22ae8369f7/5aec885635c85 +%%~path: f4ed1ae756a1f/5aec885635c85 %%~kind: TEMPLATE/DOCUMENT -%%~hash: 05c9750b00020f8cf9a7bc7362bf19681022cc03 +%%~hash: 537fb0132a80101060b40b42ad8875ebbcb40a5b %%~date: 2024-02-02 10:58:47/2024-02-02 10:59:32 ### Scene diff --git a/sample/content/5ee8aebcdebc9.nwd b/sample/content/5ee8aebcdebc9.nwd index d31c4d96..442156da 100644 --- a/sample/content/5ee8aebcdebc9.nwd +++ b/sample/content/5ee8aebcdebc9.nwd @@ -1,7 +1,7 @@ %%~name: Character Note -%%~path: 09f22ae8369f7/5ee8aebcdebc9 +%%~path: f4ed1ae756a1f/5ee8aebcdebc9 %%~kind: TEMPLATE/NOTE -%%~hash: 6230bb6f796e708a72f4c29dc79d63dc90a47ba5 +%%~hash: 0d33e49259d13a6be92b493d2122dcacc677eb0b %%~date: 2024-02-02 10:54:52/2024-02-02 10:56:05 # Character Name diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 416efc4e..11e59489 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -1,8 +1,8 @@ %%~name: Making a Scene %%~path: 6a2d6d5f4f401/636b6aa9b697b %%~kind: NOVEL/DOCUMENT -%%~hash: b39201b02e3db63493d61d09e5fec5765a937255 -%%~date: Unknown/2025-06-24 09:09:56 +%%~hash: c1a75b18145e49e5e71b5284f66a261207da37d0 +%%~date: Unknown/2025-06-16 12:49:10 ### Making a Scene @tag: Scene @@ -16,7 +16,7 @@ A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree. The scene document can be sorted after the chapter document, or as a child of the chapter. Both result in the same output in the end, so it is a matter of preference. -Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but there are some known limitations. If the syntax highlighter doesn’t show it correctly, the export tool will not either. +Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but there are some known limitations. If the ==syntax highlighter== doesn’t show it correctly, the export tool will not either. For special formatting aside from standard emphasis, you can use “shortcodes”. Here we have super[sup]script[/sup] and sub[sub]script[/sub], and [b]part[/b]ial bold too, which ends in the middle of a word. The shortcodes also support [u]underline[/u], and you can [m]highlight text as well[/m]. diff --git a/sample/content/6a2d6d5f4f401.nwd b/sample/content/6a2d6d5f4f401.nwd index d9a6bb24..479f6117 100644 --- a/sample/content/6a2d6d5f4f401.nwd +++ b/sample/content/6a2d6d5f4f401.nwd @@ -1,13 +1,13 @@ %%~name: Chapter One %%~path: 7031beac91f75/6a2d6d5f4f401 %%~kind: NOVEL/DOCUMENT -%%~hash: 89687756c204742d17f1d37a0ba7fa3aadaf00f2 -%%~date: Unknown/2023-08-25 16:51:56 +%%~hash: 4310d3f9d399b5cf1eef66d7d7caa8b022fc6663 +%%~date: Unknown/2025-07-16 12:09:14 ## So it Begins @pov: Jane @location: Earth -% Synopsis: We can add a chapter document, but keep the scene files separate. In the chapter document we can set the meta data that applies to the whole chapter if we wish to. You can add the scenes as child documents directly under the chapter. +%Synopsis: We can add a chapter document, but keep the scene files separate. In the chapter document we can set the meta data that applies to the whole chapter if we wish to. You can add the scenes as child documents directly under the chapter. A chapter can contain leading text before the first scene, like this piece of text. diff --git a/sample/content/a520879ca0b45.nwd b/sample/content/a520879ca0b45.nwd index a8afd6ce..010f77fd 100644 --- a/sample/content/a520879ca0b45.nwd +++ b/sample/content/a520879ca0b45.nwd @@ -1,13 +1,13 @@ %%~name: Chapter One %%~path: e5e47ebf63b1c/a520879ca0b45 %%~kind: NOVEL/DOCUMENT -%%~hash: 2285256b970dbb3ccf833d9275cd5f8935b6aff1 -%%~date: Unknown/2023-08-25 16:52:02 +%%~hash: 223ba31c0cd6c59ebb14df7f26e722c3ef3eea42 +%%~date: Unknown/2025-07-16 12:09:42 ## Chapter One @pov: Jane -% Synopsis: Remember Jane and John? +%Synopsis: Remember Jane and John? ### Scene One diff --git a/sample/content/bacb7059e3083.nwd b/sample/content/bacb7059e3083.nwd index 309b3d3c..bd47eae1 100644 --- a/sample/content/bacb7059e3083.nwd +++ b/sample/content/bacb7059e3083.nwd @@ -1,10 +1,10 @@ %%~name: Title Page %%~path: e5e47ebf63b1c/bacb7059e3083 %%~kind: NOVEL/DOCUMENT -%%~hash: 16b55dd0f8dc6bbba266384930a87ec211d471df -%%~date: Unknown/2023-08-25 16:52:01 +%%~hash: 4805628d99711129c7296b01e5059410993ebd02 +%%~date: Unknown/2025-07-16 12:09:35 #! Sequel Novel >> **By Jane Doh** << -% Synopsis: Jane and John are back in a sequel to My Novel! +%Synopsis: Jane and John are back in a sequel to My Novel! diff --git a/sample/content/bb2c23b3c42cc.nwd b/sample/content/bb2c23b3c42cc.nwd index 9c18a616..0b2350fa 100644 --- a/sample/content/bb2c23b3c42cc.nwd +++ b/sample/content/bb2c23b3c42cc.nwd @@ -1,12 +1,12 @@ %%~name: Jane Smith %%~path: f7e2d9f330615/bb2c23b3c42cc %%~kind: CHARACTER/NOTE -%%~hash: 10f072a4bf7389b0c178a4181e50833d39b945af -%%~date: Unknown/2024-01-30 11:05:37 +%%~hash: d3911172cd384130e57b3754ea8a3ff30633f825 +%%~date: Unknown/2025-07-16 12:09:50 # Jane Smith @tag: Jane | Jane Smith -% Short: The heroine +%Short: The heroine She’s pretty cool. Not Angelina Jolie though. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index a4871a50..b01942bb 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Jane Smith @@ -20,61 +20,61 @@ D - New - Notes - Started - 1st Draft - 2nd Draft - 3rd Draft - Finished + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished - None - Background - Minor - Major - Main + None + Background + Minor + Major + Main
    - + Novel - + Title Page - + Page - + Part One - + Chapter One - + Making a Scene - + Another Scene - + Interlude - + A Note on Structure - + Chapter Two @@ -86,11 +86,11 @@ Sequel - + Title Page - + Chapter One @@ -102,11 +102,11 @@ Main Characters - + John Smith - + Jane Smith @@ -118,7 +118,7 @@ Earth - + Space diff --git a/setup/debian/control b/setup/debian/control index 75e94a20..24f0cd25 100644 --- a/setup/debian/control +++ b/setup/debian/control @@ -2,12 +2,28 @@ Source: novelwriter Maintainer: Veronica Berglyd Olsen Section: text Priority: optional -Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9), python3 (>=3.10), python3-pyqt6 (>= 6.4), python3-pyqt6.qtsvg (>= 6.4), python3-enchant (>= 2.0) +Build-Depends: + dh-python, + python3-setuptools, + python3-all, + debhelper (>= 9), + python3 (>=3.10), + 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 Package: novelwriter Architecture: all -Depends: ${misc:Depends}, ${python3:Depends}, python3 (>=3.10), python3-pyqt6 (>= 6.4), python3-pyqt6.qtsvg (>= 6.4), python3-enchant (>= 2.0) +Depends: + ${misc:Depends}, + ${python3:Depends}, + python3 (>=3.10), + python3-pyqt6 (>= 6.4), + python3-pyqt6.qtsvg (>= 6.4), + python3-enchant (>= 2.0), + qt6-image-formats-plugins (>= 6.4) Description: A plain text editor for planning and writing novels diff --git a/setup/description_pypi.md b/setup/description_pypi.md index 0221db8d..23c85909 100644 --- a/setup/description_pypi.md +++ b/setup/description_pypi.md @@ -10,9 +10,9 @@ synchronisation tools. All text is saved as plain text files with a meta data he project structure is stored in a single project XML file, and other meta data is primarily saved as JSON files. -The application is written with Python 3 (3.10+) using Qt6 and PyQt6 (5.10+). It is developed on -Linux, but should in principle work fine on other operating systems as well as long as dependencies -are met. It is regularly tested on Debian and Ubuntu Linux, Windows, and MacOS. +The application is written with Python 3 using Qt6 and PyQt6. It is developed on Linux, but should +in principle work fine on other operating systems as well as long as dependencies are met. It is +regularly tested on Debian and Ubuntu Linux, Windows, and MacOS. novelWriter is developed and maintained by [Veronica Berglyd Olsen](https://github.com/vkbo). diff --git a/setup/iss_license.txt b/setup/iss_license.txt index fe023cb4..dbcf6b7d 100644 --- a/setup/iss_license.txt +++ b/setup/iss_license.txt @@ -1,6 +1,6 @@ novelWriter License -Copyright (C) 2018-2025 Veronica Berglyd Olsen +Copyright (C) Veronica Berglyd Olsen and novelWriter contributors License: GPL v3+ This program is free software: you can redistribute it and/or modify diff --git a/setup/requirements.txt b/setup/requirements.txt new file mode 100644 index 00000000..120a3f4f --- /dev/null +++ b/setup/requirements.txt @@ -0,0 +1,2 @@ +setuptools>=77.0.3 +twine diff --git a/tests/conftest.py b/tests/conftest.py index 33baa870..765755bf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import logging @@ -34,11 +34,14 @@ from PyQt6.QtWidgets import QMessageBox sys.path.insert(1, str(Path(__file__).parent.parent.absolute())) from novelwriter import CONFIG, SHARED +from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT +from novelwriter.enum import nwTheme -from tests.mocked import MockGuiMain, MockTheme +from tests.mocked import MockGuiMain from tests.tools import cleanProject _TST_ROOT = Path(__file__).parent +_SRC_ROOT = _TST_ROOT.parent _TMP_ROOT = _TST_ROOT / "temp" _TMP_CONF = _TMP_ROOT / "conf" @@ -59,7 +62,13 @@ def resetConfigVars(): CONFIG._dLocale = QLocale("en_GB") CONFIG._manuals = {"manual": _TMP_ROOT / "manual.pdf"} CONFIG.guiLocale = "en_GB" - return + CONFIG.darkTheme = DEF_GUI_DARK + CONFIG.lightTheme = DEF_GUI_LIGHT + CONFIG.themeMode = nwTheme.LIGHT + + # Enable a few settings to ensure better coverage + CONFIG.emphLabels = True + CONFIG.lineHighlight = True ## @@ -75,7 +84,8 @@ def sessionFixture(): _TMP_ROOT.mkdir() _TMP_CONF.mkdir() (_TMP_ROOT / "manual.pdf").touch() - return + (_SRC_ROOT / "novelwriter" / "assets"/ "manual.pdf").touch() + (_SRC_ROOT / "novelwriter" / "assets"/ "manual_fr.pdf").touch() @pytest.fixture(scope="function", autouse=True) @@ -94,8 +104,6 @@ def functionFixture(qtbot): resetConfigVars() logging.getLogger("novelwriter").setLevel(logging.INFO) - return - ## # Core Test Folders @@ -148,15 +156,27 @@ def projPath(fncPath): @pytest.fixture(scope="function") def mockGUI(qtbot, monkeypatch): """Create a mock instance of novelWriter's main GUI class.""" + from novelwriter.gui.theme import GuiTheme + monkeypatch.setattr(QMessageBox, "exec", lambda *a: None) monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes) gui = MockGuiMain() - theme = MockTheme() + theme = GuiTheme() monkeypatch.setattr(SHARED, "_gui", gui) monkeypatch.setattr(SHARED, "_theme", theme) + return gui +@pytest.fixture(scope="function") +def mockGUIwithTheme(mockGUI): + """Create a mock instance of novelWriter's main GUI class with the + theme instance initialised. + """ + SHARED.theme.initThemes() + return mockGUI + + @pytest.fixture(scope="function") def nwGUI(qtbot, monkeypatch, functionFixture): """Create an instance of the novelWriter GUI.""" @@ -226,13 +246,11 @@ def prjLipsum(): if dstDir.exists(): shutil.rmtree(dstDir) - return - @pytest.fixture(scope="session") def ipsumText(): """Return five paragraphs of Lorem Ipsum text.""" - thatIpsum = [( + return [( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum co" "mmodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, e" "get euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, ve" @@ -280,4 +298,3 @@ def ipsumText(): " a nisl. Etiam eget tristique dui. Nulla sed mi finibus, venenatis tellus non, maximus en" "im." )] - return thatIpsum diff --git a/tests/files/all_icons.json b/tests/files/all_icons.json new file mode 100644 index 00000000..3d85f818 --- /dev/null +++ b/tests/files/all_icons.json @@ -0,0 +1,114 @@ +[ + "alert_error", + "alert_info", + "alert_question", + "alert_warn", + + "cls_archive", + "cls_character", + "cls_custom", + "cls_entity", + "cls_none", + "cls_novel", + "cls_object", + "cls_plot", + "cls_template", + "cls_timeline", + "cls_trash", + "cls_world", + + "prj_folder", + "prj_document", + "prj_title", + "prj_chapter", + "prj_scene", + "prj_note", + + "fmt_bold", + "fmt_italic", + "fmt_mark", + "fmt_strike", + "fmt_subscript", + "fmt_superscript", + "fmt_underline", + "fmt_toolbar", + + "search", + "search_cancel", + "search_case", + "search_loop", + "search_preserve", + "search_project", + "search_regex", + "search_replace", + "search_word", + + "bullet-off", + "bullet-on", + "unfold-hide", + "unfold-show", + + "sb_build", + "sb_details", + "sb_novel", + "sb_outline", + "sb_project", + "sb_search", + "sb_stats", + + "theme_light", + "theme_dark", + "theme_auto", + + "add", + "bookmarks", + "browse", + "build_settings", + "cancel", + "checked", + "chevron_down", + "chevron_left", + "chevron_right", + "chevron_up", + "close", + "copy", + "document_add", + "document", + "edit", + "exclude", + "export", + "filter", + "fit_height", + "fit_width", + "folder", + "font", + "import", + "language", + "lines", + "list", + "margin_bottom", + "margin_left", + "margin_right", + "margin_top", + "maximise", + "minimise", + "more_arrow", + "more_vertical", + "noncheckable", + "open", + "panel", + "pin", + "project_copy", + "quote", + "refresh", + "remove", + "revert", + "settings", + "star", + "stats", + "text", + "timer_off", + "timer", + "unchecked", + "view" +] diff --git a/tests/files/nwProject-1.5.nwx b/tests/files/nwProject-1.5.nwx index cd103d11..839b0876 100644 --- a/tests/files/nwProject-1.5.nwx +++ b/tests/files/nwProject-1.5.nwx @@ -1,5 +1,5 @@ - + Sample Project Jane Smith @@ -20,20 +20,20 @@ D - New - Notes - Started - 1st Draft - 2nd Draft - 3rd Draft - Finished + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished - None - Background - Minor - Major - Main + None + Background + Minor + Major + Main diff --git a/tests/lipsum/content/04468803b92e1.nwd b/tests/lipsum/content/04468803b92e1.nwd index 35a2dad3..c1a7ac41 100644 --- a/tests/lipsum/content/04468803b92e1.nwd +++ b/tests/lipsum/content/04468803b92e1.nwd @@ -1,12 +1,14 @@ %%~name: Ancient Europe %%~path: 60bdf227455cc/04468803b92e1 %%~kind: WORLD/NOTE -%%~hash: 7b746485a1d06c64a5e0bf671e3141806ed51a54 -%%~date: Unknown/2024-01-30 21:36:44 +%%~hash: 9a0a98cda777f557ea10670ea1618cde253084b7 +%%~date: Unknown/2025-06-19 21:02:43 # Ancient Europe @tag: Europe | Ancient Europe +%Short: A strange place + Vivamus sodales risus ac accumsan posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc vel enim felis. Vestibulum dignissim massa nunc, a auctor magna eleifend et. Proin dignissim sodales erat vitae convallis. Aliquam id tellus dui. Curabitur sollicitudin scelerisque ex sit amet posuere. Nam rutrum felis id rhoncus feugiat. Duis sagittis quam quis purus efficitur, quis rutrum odio iaculis. Maecenas semper ante turpis, at vulputate mi consectetur non. Sed rutrum nibh turpis, quis rhoncus purus ornare quis. Vestibulum at rutrum mauris. Integer dolor nisi, tincidunt eget vehicula ac, ultricies at ligula. Aenean semper turpis quis varius rhoncus. Vivamus ac mi eget felis euismod vulputate. Nam eu tempus velit. Etiam ut est porta, finibus erat sit amet, consectetur felis. Nullam consequat felis ut lacus pharetra, in lobortis urna mollis. Nulla varius eros nec lorem rhoncus, sed venenatis risus ultrices. Phasellus pellentesque laoreet neque, ut ultricies lacus vulputate quis. In malesuada dui sit amet est interdum, eget consectetur mi gravida. Cras vel bibendum purus. Quisque commodo tempor arcu, non lacinia sem blandit eleifend. Quisque at neque gravida, porttitor metus a, suscipit diam. Quisque convallis sodales lacus et condimentum. Donec a suscipit diam. Pellentesque eget cursus neque. diff --git a/tests/lipsum/content/2426c6f0ca922.nwd b/tests/lipsum/content/2426c6f0ca922.nwd index e6d5dd54..47db3a77 100644 --- a/tests/lipsum/content/2426c6f0ca922.nwd +++ b/tests/lipsum/content/2426c6f0ca922.nwd @@ -1,12 +1,14 @@ %%~name: Main %%~path: 6c6afb1247750/2426c6f0ca922 %%~kind: PLOT/NOTE -%%~hash: db3897d166e246acdb5e25e9bd98c5a40a699ed0 -%%~date: Unknown/Unknown +%%~hash: ffc82e98d37621d83a808a75f06bd36923356706 +%%~date: Unknown/2025-06-19 21:02:33 # Main Plot @tag: Main +%Short: Where it all happens + Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vitae ultricies felis ullamcorper eu. Duis luctus risus mi, in accumsan velit cursus ut. Vestibulum eleifend leo in magna eleifend fermentum. Proin nec ornare elit. Phasellus nec interdum risus. In a volutpat augue, quis egestas justo. Morbi porta mauris mattis bibendum imperdiet. Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur. \ No newline at end of file diff --git a/tests/lipsum/content/441420a886d82.nwd b/tests/lipsum/content/441420a886d82.nwd index 05881160..b31445e3 100644 --- a/tests/lipsum/content/441420a886d82.nwd +++ b/tests/lipsum/content/441420a886d82.nwd @@ -1,14 +1,14 @@ %%~name: Chapter Two %%~path: 6bd935d2490cd/441420a886d82 %%~kind: NOVEL/DOCUMENT -%%~hash: fd6d46708faa1333f8f7ba0442fc5b35bf1e3f85 -%%~date: Unknown/Unknown +%%~hash: 81d30d3448202c782a6e252c5f7ef030bca3bf7f +%%~date: Unknown/2025-06-19 21:01:48 ## Chapter Two @pov: Bod @plot: Main @location: Europe -% Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue. +%Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue. Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue. Vestibulum pharetra fermentum leo, sed faucibus eros placerat quis. In hac habitasse platea dictumst. Donec metus massa, rutrum quis consequat et, tincidunt ac felis. Duis mollis metus ac nunc tincidunt blandit. Ut aliquet velit eu odio pharetra condimentum. Integer rutrum lacus orci, id venenatis libero accumsan at. diff --git a/tests/lipsum/content/47666c91c7ccf.nwd b/tests/lipsum/content/47666c91c7ccf.nwd index e551a0e3..5f6011ef 100644 --- a/tests/lipsum/content/47666c91c7ccf.nwd +++ b/tests/lipsum/content/47666c91c7ccf.nwd @@ -1,15 +1,15 @@ %%~name: Scene Five %%~path: 6bd935d2490cd/47666c91c7ccf %%~kind: NOVEL/DOCUMENT -%%~hash: d210c26966da6f9edea8726567abf4860b7bb9b7 -%%~date: Unknown/Unknown +%%~hash: 680e26046056165f1560ed1468cf316a3c51f90e +%%~date: Unknown/2025-06-19 21:01:59 ### Scene Five @pov: Bod @plot: Main @location: Europe -% Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. +%Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In sed felis auctor, rhoncus dui ac, consequat dolor. Integer volutpat libero sed nisl aliquet varius. Suspendisse et lorem sapien. Proin id ultrices nibh, ac suscipit diam. Suspendisse placerat varius porttitor. Curabitur elementum sed enim ultrices imperdiet. diff --git a/tests/lipsum/content/4c4f28287af27.nwd b/tests/lipsum/content/4c4f28287af27.nwd index 4bf57987..7ee071db 100644 --- a/tests/lipsum/content/4c4f28287af27.nwd +++ b/tests/lipsum/content/4c4f28287af27.nwd @@ -1,13 +1,15 @@ %%~name: Mr. Nobody %%~path: 67a8707f2f249/4c4f28287af27 %%~kind: CHARACTER/NOTE -%%~hash: 2a9b9751e8207b719bfa6233ebe04e24928ff8a8 -%%~date: Unknown/2024-01-30 21:36:26 +%%~hash: 84639e78d265b3a1f2fb8e3211a08c223d95500a +%%~date: Unknown/2025-06-19 21:02:21 # Nobody Owens @tag: Bod | Nobody Owens @plot: Main +%Short: The Hero + Pellentesque nec erat ut nulla posuere commodo. Curabitur nisi augue, imperdiet et porta imperdiet, efficitur id leo. Cras finibus arcu at nibh commodo congue. Proin suscipit placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin nunc lacus, imperdiet nec posuere ac, interdum non lectus. Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec sodales rutrum mattis. In in sem ornare, consequat nulla ac, convallis arcu. Duis ac metus id felis commodo commodo sit amet eget diam. Curabitur rhoncus lacinia leo at sodales. Etiam finibus porta diam a viverra. Praesent nisi urna, volutpat sit amet odio at, vehicula vehicula leo. In non enim eget nisl luctus commodo. Pellentesque pellentesque at lectus at luctus. Quisque nec felis bibendum, lacinia libero ut, lacinia eros. Integer finibus ultricies nibh sit amet placerat. diff --git a/tests/lipsum/content/88243afbe5ed8.nwd b/tests/lipsum/content/88243afbe5ed8.nwd index 3f40135a..c8276545 100644 --- a/tests/lipsum/content/88243afbe5ed8.nwd +++ b/tests/lipsum/content/88243afbe5ed8.nwd @@ -1,15 +1,15 @@ %%~name: Scene One %%~path: 45e6b01ca35c1/88243afbe5ed8 %%~kind: NOVEL/DOCUMENT -%%~hash: a09245a7a772bbe02850b5db109977e336cd9cc1 -%%~date: Unknown/Unknown +%%~hash: d660202d320b3c99dbcc3bd9dd39d464d4f422ae +%%~date: Unknown/2025-06-19 21:01:38 ### Scene One @pov: Bod @plot: Main @location: Europe -% Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur. +%Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur. Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur. Donec quis ante nunc. Mauris ut leo ipsum. Vestibulum est neque, hendrerit nec neque a, ullamcorper lobortis tellus. Fusce sollicitudin purus quis congue bibendum. Aliquam condimentum ipsum tristique blandit tristique. Donec pulvinar neque ac suscipit malesuada. diff --git a/tests/lipsum/content/88d59a277361b.nwd b/tests/lipsum/content/88d59a277361b.nwd index a05c84e6..367ea3e1 100644 --- a/tests/lipsum/content/88d59a277361b.nwd +++ b/tests/lipsum/content/88d59a277361b.nwd @@ -1,11 +1,12 @@ %%~name: Prologue %%~path: b3643d0f92e32/88d59a277361b %%~kind: NOVEL/DOCUMENT -%%~hash: 377a72ba340ff458a00d98af745e9fa0c79fb26c -%%~date: Unknown/2024-10-25 19:16:49 +%%~hash: d43324a30d3f1e5608711ca71515dee429b30925 +%%~date: Unknown/2025-06-19 21:01:07 ##! Prologue -% Synopsis: Explanation from the lipsum.com website. +%Synopsis: Explanation from the lipsum.com website. +%Note.Purpose: Introduction _Lorem Ipsum_ is simply dummy text[footnote:f9kgf] of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. diff --git a/tests/lipsum/content/eb103bc70c90c.nwd b/tests/lipsum/content/eb103bc70c90c.nwd index bdfac437..3c126692 100644 --- a/tests/lipsum/content/eb103bc70c90c.nwd +++ b/tests/lipsum/content/eb103bc70c90c.nwd @@ -1,15 +1,15 @@ %%~name: Scene Three %%~path: 6bd935d2490cd/eb103bc70c90c %%~kind: NOVEL/DOCUMENT -%%~hash: c4eda49e4fe81dc450d547eee0bdabe77fdaaa98 -%%~date: Unknown/Unknown +%%~hash: 59eeb405f5b26cbe1056d1f7684f7ed40ed6685a +%%~date: Unknown/2025-06-19 21:01:52 ### Scene Three @pov: Bod @plot: Main @location: Europe -% Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. +%Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean tincidunt lacus vitae nibh elementum eleifend. Sed rutrum condimentum sem quis blandit. Duis imperdiet libero metus, quis convallis quam faucibus a. Nulla ligula est, semper quis sollicitudin et, pretium id justo. Curabitur pharetra risus eget consectetur commodo. Duis mattis arcu non est condimentum, id venenatis risus volutpat. Pellentesque aliquet mauris non mauris porttitor ultrices. Phasellus ut vestibulum mi. Suspendisse malesuada metus lorem, a malesuada orci rhoncus a. Praesent euismod convallis ante, lacinia tincidunt ex egestas id. Praesent sit amet efficitur sapien. Morbi tincidunt volutpat nunc sed dictum. Aliquam ultrices metus id fermentum lobortis. diff --git a/tests/lipsum/content/f8c0562e50f1b.nwd b/tests/lipsum/content/f8c0562e50f1b.nwd index 73bedd5f..890a7e06 100644 --- a/tests/lipsum/content/f8c0562e50f1b.nwd +++ b/tests/lipsum/content/f8c0562e50f1b.nwd @@ -1,15 +1,15 @@ %%~name: Scene Four %%~path: 6bd935d2490cd/f8c0562e50f1b %%~kind: NOVEL/DOCUMENT -%%~hash: 9461a279b9fb6ef005ee4d432fcda77ff5bfbd42 -%%~date: Unknown/Unknown +%%~hash: f4848b56d1f41a04de1579d772a4d1ddd3e28e55 +%%~date: Unknown/2025-06-19 21:01:55 ### Scene Four @pov: Bod @plot: Main @location: Europe -% Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo. +%Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo. Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo. Nullam viverra dui et auctor pretium. Ut ullamcorper velit urna, sed imperdiet massa convallis a. Suspendisse efficitur, ipsum nec cursus pulvinar, eros urna posuere diam, nec elementum mi felis vitae sapien. diff --git a/tests/lipsum/content/f96ec11c6a3da.nwd b/tests/lipsum/content/f96ec11c6a3da.nwd index b6434395..a71ad88a 100644 --- a/tests/lipsum/content/f96ec11c6a3da.nwd +++ b/tests/lipsum/content/f96ec11c6a3da.nwd @@ -1,15 +1,15 @@ %%~name: Scene Two %%~path: 45e6b01ca35c1/f96ec11c6a3da %%~kind: NOVEL/DOCUMENT -%%~hash: ebe3fbaa16d9d81bc1a139822e3bf39bb357866d -%%~date: Unknown/Unknown +%%~hash: e95ff5b633e74c9cab8dc32b6a77b3e40d265f2b +%%~date: Unknown/2025-06-19 21:01:41 ### Scene Two @pov: Bod @plot: Main @location: Europe -% Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. +%Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. Vestibulum facilisis bibendum aliquam. Aliquam posuere, turpis ac bibendum varius, sem tellus venenatis risus, in elementum massa enim ac lorem. Integer in sem ac diam blandit ultricies ut in nulla. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam sit amet erat est. Curabitur vitae cursus justo, sit amet placerat dolor. Vivamus eu felis hendrerit, tincidunt massa rutrum, maximus arcu. Pellentesque commodo justo odio, vel rutrum nulla tincidunt eu. Integer non neque condimentum, convallis diam non, varius ligula. Aliquam eget sapien mauris. Aenean pharetra nunc nisi, vel maximus ante tristique sit amet. Aliquam risus metus, interdum non odio eu, consectetur lacinia sapien. diff --git a/tests/lipsum/content/fb609cd8319dc.nwd b/tests/lipsum/content/fb609cd8319dc.nwd index 10e465ae..ff643fb1 100644 --- a/tests/lipsum/content/fb609cd8319dc.nwd +++ b/tests/lipsum/content/fb609cd8319dc.nwd @@ -1,14 +1,14 @@ %%~name: Chapter One %%~path: 45e6b01ca35c1/fb609cd8319dc %%~kind: NOVEL/DOCUMENT -%%~hash: 5dabeaa7a58238a6ad99ce73176b1bdfad1a71b7 -%%~date: Unknown/2024-01-30 21:35:57 +%%~hash: 73a1008ed17a84a48b14b41109a842a6ed300eec +%%~date: Unknown/2025-06-19 21:01:19 ## Chapter One @pov: Bod @plot: Main @location: Europe -% Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam. +%Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam. Praesent magna nunc, lacinia sit amet quam eget, aliquet ultrices justo. Morbi ornare enim et lorem rutrum finibus ut eu dolor. Aliquam a orci odio. Ut ultrices sem quis massa placerat, eget mollis nisl cursus. Cras vel sagittis justo. Ut non ultricies leo. Maecenas rutrum velit in est varius, et egestas massa pulvinar. diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index de32f1f6..a403da4a 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Lorem Ipsum lipsum.com @@ -9,7 +9,7 @@ en_GB None - 7a992350f3eb6 + 04468803b92e1 None b3643d0f92e32 None @@ -19,16 +19,16 @@ Replace Text 2 - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main @@ -45,7 +45,7 @@ Front Matter - + Prologue @@ -57,7 +57,7 @@ Chapter One - + Chapter One @@ -65,7 +65,7 @@ Scene One - + Scene Two @@ -77,19 +77,19 @@ Chapter Two - + Chapter Two - + Scene Three - + Scene Four - + Scene Five @@ -97,24 +97,24 @@ Characters - - Mr. Nobody + + Mr. Nobody Plot - - Main + + Main World - - Ancient Europe + + Ancient Europe diff --git a/tests/mocked.py b/tests/mocked.py index d28cdbba..f86d5be0 100644 --- a/tests/mocked.py +++ b/tests/mocked.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from unittest.mock import MagicMock @@ -33,8 +33,8 @@ class MockGuiMain(QWidget): self.mainStatus = MagicMock() self.docEditor = MagicMock() self.docViewer = MagicMock() + self.mainProgress = MagicMock() self.projPath = "" - return def postLaunchTasks(self, cmdOpen): return @@ -44,7 +44,6 @@ class MockGuiMain(QWidget): def openProject(self, projPath): self.projPath = projPath - return def rebuildIndex(self): return @@ -63,6 +62,8 @@ class MockTheme: self.guiFont = QFont() self.guiFontB = QFont() self.guiFontBU = QFont() + + def initThemes(self) -> None: return def getPixmap(self, *a) -> QPixmap: @@ -92,8 +93,10 @@ class MockApp: # Mock functions that will raise errors instead. def causeOSError(*args, **kwargs): + """Raise an OSError.""" raise OSError("Mock OSError") def causeException(*args, **kwargs): + """Raise an Exception.""" raise Exception("Mock Exception") diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf index 78441636..7a281145 100644 --- a/tests/reference/baseConfig_novelwriter.conf +++ b/tests/reference/baseConfig_novelwriter.conf @@ -1,10 +1,11 @@ [Meta] -timestamp = 2025-05-11 13:28:11 +timestamp = 2025-08-24 15:30:16 [Main] font = -theme = default -syntax = default_light +lighttheme = default_light +darktheme = default_dark +thememode = AUTO icons = material_rounded_normal iconcoltree = theme iconcoldocs = False @@ -26,9 +27,9 @@ outlinepane = 500, 150 [Project] autosaveproject = 60 autosavedoc = 30 -emphlabels = True +emphlabels = False backuppath = -backuponclose = False +backuponclose = True askbeforebackup = True askbeforeexit = True lastauthor = @@ -39,6 +40,7 @@ width = 700 margin = 40 tabwidth = 40 cursorwidth = 1 +linehighlight = False focuswidth = 800 hidefocusfooter = False justify = False @@ -48,7 +50,7 @@ repsquotes = True repdquotes = True repdash = True repdots = True -autoscroll = False +autoscroll = True autoscrollpos = 30 scrollpastend = True fmtsquoteopen = ‘ @@ -61,7 +63,7 @@ fmtpadthin = False spellcheck = en showtabsnspaces = False showlineendings = False -showmultispaces = True +showmultispaces = False incnoteswcount = True showfullpath = True dialogstyle = 2 @@ -81,6 +83,7 @@ showedittoolbar = False showsessiontime = True viewcomments = True viewsynopsis = True +viewnotes = True searchcase = False searchword = False searchregex = False @@ -90,3 +93,4 @@ searchmatchcap = False searchprojcase = False searchprojword = False searchprojregex = False + diff --git a/tests/reference/coreIndex_LoadSave_tagsIndex.json b/tests/reference/coreIndex_LoadSave_tagsIndex.json index 99201a13..c8365601 100644 --- a/tests/reference/coreIndex_LoadSave_tagsIndex.json +++ b/tests/reference/coreIndex_LoadSave_tagsIndex.json @@ -18,7 +18,8 @@ "88d59a277361b": { "T0001": { "meta": {"level": "H2", "title": "Prologue", "line": 1, "tag": "", "counts": [605, 94, 2]}, - "summary": "Explanation from the lipsum.com website." + "summary": "Explanation from the lipsum.com website.", + "note.Purpose": "Introduction" }, "document": { "footnotes": ["f9kgf"] @@ -92,17 +93,20 @@ "4c4f28287af27": { "T0001": { "meta": {"level": "H1", "title": "Nobody Owens", "line": 1, "tag": "bod", "counts": [1864, 284, 3]}, - "refs": {"main": "@plot"} + "refs": {"main": "@plot"}, + "summary": "The Hero" } }, "2426c6f0ca922": { "T0001": { - "meta": {"level": "H1", "title": "Main Plot", "line": 1, "tag": "main", "counts": [1369, 195, 2]} + "meta": {"level": "H1", "title": "Main Plot", "line": 1, "tag": "main", "counts": [1369, 195, 2]}, + "summary": "Where it all happens" } }, "04468803b92e1": { "T0001": { - "meta": {"level": "H1", "title": "Ancient Europe", "line": 1, "tag": "europe", "counts": [1770, 259, 3]} + "meta": {"level": "H1", "title": "Ancient Europe", "line": 1, "tag": "europe", "counts": [1770, 259, 3]}, + "summary": "A strange place" } } } diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index b244eeb6..af93504f 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project Jane Doe @@ -16,19 +16,19 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main - + Novel @@ -73,6 +73,10 @@ John + + + Mike + Locations diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index abcfdfb7..4c253097 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project Jane Doe @@ -16,16 +16,16 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main diff --git a/tests/reference/coreTools_DocDuplicator_nwProject.nwx b/tests/reference/coreTools_DocDuplicator_nwProject.nwx index 9b0d2071..2fb52c05 100644 --- a/tests/reference/coreTools_DocDuplicator_nwProject.nwx +++ b/tests/reference/coreTools_DocDuplicator_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project Jane Doe @@ -16,16 +16,16 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main diff --git a/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx b/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx index 296b8172..5b7cbbab 100644 --- a/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx +++ b/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Project A Jane Doe @@ -16,16 +16,16 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main diff --git a/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx b/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx index 531aea22..83362eb3 100644 --- a/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx +++ b/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Project B Jane Doe @@ -16,16 +16,16 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main diff --git a/tests/reference/fmtToDocX_SaveDocument_app.xml b/tests/reference/fmtToDocX_SaveDocument_app.xml index 43e25401..0e99320a 100644 --- a/tests/reference/fmtToDocX_SaveDocument_app.xml +++ b/tests/reference/fmtToDocX_SaveDocument_app.xml @@ -1,8 +1,8 @@ - 41 - novelWriter/2.7b1 - 4035 + 45 + novelWriter/2.8a1 + 4050 21296 24964 45 diff --git a/tests/reference/fmtToDocX_SaveDocument_core.xml b/tests/reference/fmtToDocX_SaveDocument_core.xml index 3a0d34b6..35597dc5 100644 --- a/tests/reference/fmtToDocX_SaveDocument_core.xml +++ b/tests/reference/fmtToDocX_SaveDocument_core.xml @@ -1,10 +1,10 @@ - 2025-04-29T22:46:36 - 2025-04-29T22:46:36 + 2025-06-19T21:08:59 + 2025-06-19T21:08:59 lipsum.com Lorem Ipsum en_GB - 52 + 55 lipsum.com diff --git a/tests/reference/fmtToDocX_SaveDocument_document.xml b/tests/reference/fmtToDocX_SaveDocument_document.xml index 68bd89f3..3e631895 100644 --- a/tests/reference/fmtToDocX_SaveDocument_document.xml +++ b/tests/reference/fmtToDocX_SaveDocument_document.xml @@ -34,7 +34,7 @@ - 4,035 + 4,050 @@ -48,7 +48,7 @@ - 27,064 + 27,164 @@ -1250,6 +1250,28 @@ Main + + + + + + + + + + Short Description: + + + + + + + + + + The Hero + + @@ -1323,6 +1345,28 @@ Main + + + + + + + + + + Short Description: + + + + + + + + + + Where it all happens + + @@ -1396,6 +1440,28 @@ Ancient Europe + + + + + + + + + + Short Description: + + + + + + + + + + A strange place + + diff --git a/tests/reference/fmtToDocX_SaveDocument_settings.xml b/tests/reference/fmtToDocX_SaveDocument_settings.xml index 10913023..c1622522 100644 --- a/tests/reference/fmtToDocX_SaveDocument_settings.xml +++ b/tests/reference/fmtToDocX_SaveDocument_settings.xml @@ -9,13 +9,13 @@ - + - + - + diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 21078178..d415b195 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project Jane Doe @@ -16,16 +16,16 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 1f3260da..bb69bd02 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project Jane Doe @@ -16,16 +16,16 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main diff --git a/tests/reference/guiOutline_Content_outline.csv b/tests/reference/guiOutline_Content_outline.csv index 62edb467..22bc5537 100644 --- a/tests/reference/guiOutline_Content_outline.csv +++ b/tests/reference/guiOutline_Content_outline.csv @@ -1,13 +1,13 @@ -"H","Title","Document","Line","Status","Chars","Words","Pars","POV","Focus","Characters","Plot","Timeline","Locations","Objects","Entities","Custom","Story","Mentions","Synopsis" -"H1","Lorem Ipsum","Lorem Ipsum","1","Finished","259","44","5","","","","","","","","","","","","" -"H2","Prologue","Prologue","1","Draft","605","94","2","","","","","","","","","","","","Explanation from the lipsum.com website." -"H1","Act One","Act One","1","New","35","6","1","","","","","","","","","","","","" -"H2","Chapter One","Chapter One","1","Draft","419","67","1","Bod","","","Main","","Europe","","","","","","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam." -"H3","Scene One","Scene One","1","Finished","1197","174","2","Bod","","","Main","","Europe","","","","","","Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur." -"H4","Scene One, Section Two","Scene One","13","Finished","1561","230","2","","","","","","","","","","","","" -"H3","Scene Two","Scene Two","1","Finished","2034","299","3","Bod","","","Main","","Europe","","","","","","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci." -"H4","Scene Two, Section Two","Scene Two","15","Finished","2009","301","3","","","","","","","","","","","","" -"H2","Chapter Two","Chapter Two","1","Draft","477","70","1","Bod","","","Main","","Europe","","","","","","Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue." -"H3","Scene Three","Scene Three","1","Finished","3006","439","4","Bod","","","Main","","Europe","","","","","","Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos." -"H3","Scene Four","Scene Four","1","Finished","3839","563","6","Bod","","","Main","","Europe","","","","","","Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo." -"H3","Scene Five","Scene Five","1","Finished","3644","543","5","Bod","","","Main","","Europe","","","","","","Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus." +"H","Title","Document","Line","Status","Chars","Words","Pars","POV","Focus","Characters","Plot","Timeline","Locations","Objects","Entities","Custom","Story","Mentions","Synopsis","Note (Purpose)" +"H1","Lorem Ipsum","Lorem Ipsum","1","Finished","259","44","5","","","","","","","","","","","","","" +"H2","Prologue","Prologue","1","Draft","605","94","2","","","","","","","","","","","","Explanation from the lipsum.com website.","Introduction" +"H1","Act One","Act One","1","New","35","6","1","","","","","","","","","","","","","" +"H2","Chapter One","Chapter One","1","Draft","419","67","1","Bod","","","Main","","Europe","","","","","","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.","" +"H3","Scene One","Scene One","1","Finished","1197","174","2","Bod","","","Main","","Europe","","","","","","Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.","" +"H4","Scene One, Section Two","Scene One","13","Finished","1561","230","2","","","","","","","","","","","","","" +"H3","Scene Two","Scene Two","1","Finished","2034","299","3","Bod","","","Main","","Europe","","","","","","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.","" +"H4","Scene Two, Section Two","Scene Two","15","Finished","2009","301","3","","","","","","","","","","","","","" +"H2","Chapter Two","Chapter Two","1","Draft","477","70","1","Bod","","","Main","","Europe","","","","","","Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.","" +"H3","Scene Three","Scene Three","1","Finished","3006","439","4","Bod","","","Main","","Europe","","","","","","Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.","" +"H3","Scene Four","Scene Four","1","Finished","3839","563","6","Bod","","","Main","","Europe","","","","","","Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.","" +"H3","Scene Five","Scene Five","1","Finished","3644","543","5","Bod","","","Main","","Europe","","","","","","Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.","" diff --git a/tests/reference/mBuildDocBuild_Extended_Markdown_Lorem_Ipsum.md b/tests/reference/mBuildDocBuild_Extended_Markdown_Lorem_Ipsum.md index 96b1fc07..329eeaea 100644 --- a/tests/reference/mBuildDocBuild_Extended_Markdown_Lorem_Ipsum.md +++ b/tests/reference/mBuildDocBuild_Extended_Markdown_Lorem_Ipsum.md @@ -3,9 +3,9 @@ Lorem Ipsum **By lipsum.com** -Word Count: 4,169 +Word Count: 4,184 -Character Count: 27,898 +Character Count: 27,998 “Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” @@ -162,6 +162,8 @@ Notes: Characters **Tag:** Bod | Nobody Owens **Plot:** Main +**Short Description:** The Hero + Pellentesque nec erat ut nulla posuere commodo. Curabitur nisi augue, imperdiet et porta imperdiet, efficitur id leo. Cras finibus arcu at nibh commodo congue. Proin suscipit placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin nunc lacus, imperdiet nec posuere ac, interdum non lectus. Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec sodales rutrum mattis. In in sem ornare, consequat nulla ac, convallis arcu. Duis ac metus id felis commodo commodo sit amet eget diam. Curabitur rhoncus lacinia leo at sodales. Etiam finibus porta diam a viverra. Praesent nisi urna, volutpat sit amet odio at, vehicula vehicula leo. In non enim eget nisl luctus commodo. Pellentesque pellentesque at lectus at luctus. Quisque nec felis bibendum, lacinia libero ut, lacinia eros. Integer finibus ultricies nibh sit amet placerat. @@ -175,6 +177,8 @@ Notes: Plot **Tag:** Main +**Short Description:** Where it all happens + Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vitae ultricies felis ullamcorper eu. Duis luctus risus mi, in accumsan velit cursus ut. Vestibulum eleifend leo in magna eleifend fermentum. Proin nec ornare elit. Phasellus nec interdum risus. In a volutpat augue, quis egestas justo. Morbi porta mauris mattis bibendum imperdiet. Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur. @@ -186,6 +190,8 @@ Notes: World **Tag:** Europe | Ancient Europe +**Short Description:** A strange place + Vivamus sodales risus ac accumsan posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc vel enim felis. Vestibulum dignissim massa nunc, a auctor magna eleifend et. Proin dignissim sodales erat vitae convallis. Aliquam id tellus dui. Curabitur sollicitudin scelerisque ex sit amet posuere. Nam rutrum felis id rhoncus feugiat. Duis sagittis quam quis purus efficitur, quis rutrum odio iaculis. Maecenas semper ante turpis, at vulputate mi consectetur non. Sed rutrum nibh turpis, quis rhoncus purus ornare quis. Vestibulum at rutrum mauris. Integer dolor nisi, tincidunt eget vehicula ac, ultricies at ligula. Aenean semper turpis quis varius rhoncus. Vivamus ac mi eget felis euismod vulputate. Nam eu tempus velit. Etiam ut est porta, finibus erat sit amet, consectetur felis. Nullam consequat felis ut lacus pharetra, in lobortis urna mollis. Nulla varius eros nec lorem rhoncus, sed venenatis risus ultrices. Phasellus pellentesque laoreet neque, ut ultricies lacus vulputate quis. In malesuada dui sit amet est interdum, eget consectetur mi gravida. Cras vel bibendum purus. Quisque commodo tempor arcu, non lacinia sem blandit eleifend. Quisque at neque gravida, porttitor metus a, suscipit diam. Quisque convallis sodales lacus et condimentum. Donec a suscipit diam. Pellentesque eget cursus neque. diff --git a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm index f605b0fa..646a7c05 100644 --- a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm +++ b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm @@ -1,8 +1,9 @@ - Lorem Ipsum + + \n" "\n" "\n" - "{bodyText:s}\n" + "{body}\n" "\n" "\n" ).format( - htmlStyle="\n".join(hStyle), - bodyText="".join(resText).rstrip() + style="\n".join(hStyle), + body="".join(resText).rstrip() ) saveFile = fncPath / "outFile.htm" html.saveDocument(saveFile) - assert saveFile.read_text(encoding="utf-8") == htmlDoc + assert saveFile.read_text(encoding="utf-8") == htmlDocCSS - # JSON + HTML + # HTML wo/Styles + html.setStyles(False) + htmlDocCSS = ( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "{body}\n" + "\n" + "\n" + ).format( + body="".join(resText).rstrip() + ) + + saveFile = fncPath / "outFile.htm" + html.saveDocument(saveFile) + assert saveFile.read_text(encoding="utf-8") == htmlDocCSS + + # JSON + HTML w/Styles + html.setStyles(True) saveFile = fncPath / "outFile.json" html.saveDocument(saveFile) data = json.loads(saveFile.read_text(encoding="utf-8")) diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py index 58106cdd..dd61b3ea 100644 --- a/tests/test_formats/test_fmt_tokenizer.py +++ b/tests/test_formats/test_fmt_tokenizer.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import pytest @@ -783,7 +783,7 @@ def testFmtToken_MetaFormat(mockGUI): tokens._text = "% synopsis: The synopsis\n" tokens.tokenizeText() assert tokens._blocks == [( - BlockTyp.COMMENT, "", "Synopsis: The synopsis", [ + BlockTyp.SUMMARY, "", "Synopsis: The synopsis", [ (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"), (9, TextFmt.COL_E, ""), (9, TextFmt.B_E, ""), (10, TextFmt.COL_B, "note"), (22, TextFmt.COL_E, "") @@ -800,13 +800,52 @@ def testFmtToken_MetaFormat(mockGUI): tokens._text = "% short: A short description\n" tokens.tokenizeText() assert tokens._blocks == [( - BlockTyp.COMMENT, "", "Short Description: A short description", [ + BlockTyp.SUMMARY, "", "Short Description: A short description", [ (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"), (18, TextFmt.COL_E, ""), (18, TextFmt.B_E, ""), (19, TextFmt.COL_B, "note"), (38, TextFmt.COL_E, ""), ], BlockFmt.NONE )] + # Story Comments + tokens.setCommentType(nwComment.STORY, False) + tokens.setCommentType(nwComment.NOTE, False) + tokens._text = ( + "%Story.Stuff: Stuff happens\n" + "%Note.More: That stuff that happened\n" + "%Note.Other: That other stuff that happened\n" + ) + tokens.tokenizeText() + assert tokens._blocks == [] + + tokens.setCommentType(nwComment.STORY, True) + tokens.setCommentType(nwComment.NOTE, True) + tokens._text = ( + "%Story.Stuff: Stuff happens\n" + "%Note.More: That stuff that happened\n" + "%Note.Other: That other stuff that happened\n" + ) + tokens.tokenizeText() + assert tokens._blocks == [( + BlockTyp.NOTE, "", "Story Structure (Stuff): Stuff happens", [ + (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"), + (24, TextFmt.COL_E, ""), (24, TextFmt.B_E, ""), + (25, TextFmt.COL_B, "note"), (38, TextFmt.COL_E, "") + ], BlockFmt.Z_BTM + ), ( + BlockTyp.NOTE, "", "Note (More): That stuff that happened", [ + (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"), + (12, TextFmt.COL_E, ""), (12, TextFmt.B_E, ""), + (13, TextFmt.COL_B, "note"), (37, TextFmt.COL_E, "") + ], BlockFmt.Z_TOP | BlockFmt.Z_BTM + ), ( + BlockTyp.NOTE, "", "Note (Other): That other stuff that happened", [ + (0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"), + (13, TextFmt.COL_E, ""), (13, TextFmt.B_E, ""), + (14, TextFmt.COL_B, "note"), (44, TextFmt.COL_E, "") + ], BlockFmt.Z_TOP + )] + # Keyword tokens.setKeywords(False) tokens._text = "@char: Bod\n" @@ -919,6 +958,11 @@ def testFmtToken_ExtractFormats(mockGUI): assert text == "Text with strikethrough in it." assert fmt == [(10, TextFmt.D_B, ""), (23, TextFmt.D_E, "")] + # Plain mark + text, fmt = tokens._extractFormats("Text with ==a highlight== in it.") + assert text == "Text with a highlight in it." + assert fmt == [(10, TextFmt.M_B, ""), (21, TextFmt.M_E, "")] + # Nested bold/italics text, fmt = tokens._extractFormats("Text with **bold and _italics_** in it.") assert text == "Text with bold and italics in it." @@ -1632,7 +1676,7 @@ def testFmtToken_TextIndent(mockGUI): ] assert tokens._blocks == [ (BlockTyp.HEAD3, TM1, "Scene Two", [], BlockFmt.NONE), - (BlockTyp.COMMENT, "", "Synopsis: Stuff happens.", tFmt, BlockFmt.NONE), + (BlockTyp.SUMMARY, "", "Synopsis: Stuff happens.", tFmt, BlockFmt.NONE), ] assert tokens._noIndent is True diff --git a/tests/test_formats/test_fmt_tomarkdown.py b/tests/test_formats/test_fmt_tomarkdown.py index 90f26ff7..84c0dbc9 100644 --- a/tests/test_formats/test_fmt_tomarkdown.py +++ b/tests/test_formats/test_fmt_tomarkdown.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import pytest diff --git a/tests/test_formats/test_fmt_toodt.py b/tests/test_formats/test_fmt_toodt.py index eded1eeb..a859f6b9 100644 --- a/tests/test_formats/test_fmt_toodt.py +++ b/tests/test_formats/test_fmt_toodt.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import xml.etree.ElementTree as ET diff --git a/tests/test_formats/test_fmt_toqdoc.py b/tests/test_formats/test_fmt_toqdoc.py index 697d4833..04d51fdd 100644 --- a/tests/test_formats/test_fmt_toqdoc.py +++ b/tests/test_formats/test_fmt_toqdoc.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import pytest diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 4e240295..ba03b103 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from unittest.mock import MagicMock @@ -28,7 +28,7 @@ from PyQt6.QtCore import QEvent, QMimeData, QPointF, Qt, QThreadPool, QUrl from PyQt6.QtGui import ( QAction, QClipboard, QDesktopServices, QDragEnterEvent, QDragMoveEvent, QDropEvent, QFont, QInputMethodEvent, QMouseEvent, QTextBlock, QTextCursor, - QTextOption + QTextDocument, QTextOption ) from PyQt6.QtWidgets import QApplication, QMenu, QPlainTextEdit @@ -37,7 +37,7 @@ from novelwriter.common import decodeMimeHandles from novelwriter.constants import nwKeyWords, nwUnicode from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout -from novelwriter.gui.doceditor import GuiDocEditor, _TagAction +from novelwriter.gui.doceditor import GuiDocEditor, TextAutoReplace, _TagAction from novelwriter.gui.dochighlight import TextBlockData from novelwriter.text.counting import standardCounter from novelwriter.types import ( @@ -515,32 +515,36 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText, # Run SpellCheck # ============== SHARED.project.data.setSpellCheck(True) + LORAX = "Lorax\U0001F03A" cursor = docEditor.textCursor() cursor.setPosition(16) data = cursor.block().userData() assert cursor.block().text().startswith("Lorem") assert isinstance(data, TextBlockData) - data._spellErrors = [(0, 5)] + data._spellErrors = [(0, 5, "Lorem")] # No known position - assert docEditor._qDocument.spellErrorAtPos(-1) == ("", -1, -1, []) + assert docEditor._qDocument.spellErrorAtPos(-1) == ("", -1, []) # With Suggestion with monkeypatch.context() as mp: - mp.setattr(SHARED.spelling, "suggestWords", lambda *a: ["Lorax"]) + mp.setattr(SHARED.spelling, "suggestWords", lambda *a: [LORAX]) ctxMenu = getMenuForPos(docEditor, 16) assert ctxMenu is not None actions = [x.text() for x in ctxMenu.actions() if x.text()] assert "Spelling Suggestion(s)" in actions - assert f"{nwUnicode.U_ENDASH} Lorax" in actions + assert f"{nwUnicode.U_ENDASH} {LORAX}" in actions ctxMenu.actions()[7].trigger() QApplication.processEvents() - assert docEditor.getText() == text.replace("Lorem", "Lorax", 1) + assert docEditor.getText() == text.replace("Lorem", LORAX, 1) ctxMenu.setObjectName("") ctxMenu.deleteLater() + # Update Entry + data._spellErrors = [(0, 7, LORAX)] + # Without Suggestion with monkeypatch.context() as mp: mp.setattr(SHARED.spelling, "suggestWords", lambda *a: []) @@ -549,7 +553,7 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText, assert ctxMenu is not None actions = [x.text() for x in ctxMenu.actions() if x.text()] assert f"{nwUnicode.U_ENDASH} No Suggestions" in actions - assert docEditor.getText() == text.replace("Lorem", "Lorax", 1) + assert docEditor.getText() == text.replace("Lorem", LORAX, 1) ctxMenu.setObjectName("") ctxMenu.deleteLater() @@ -563,11 +567,11 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText, assert "Ignore Word" in actions assert "Add Word to Dictionary" in actions - assert "Lorax" not in SHARED.spelling._userDict + assert LORAX not in SHARED.spelling._userDict ctxMenu.actions()[7].trigger() # Ignore - assert "Lorax" not in SHARED.spelling._userDict + assert LORAX not in SHARED.spelling._userDict ctxMenu.actions()[8].trigger() # Add - assert "Lorax" in SHARED.spelling._userDict + assert LORAX in SHARED.spelling._userDict ctxMenu.setObjectName("") ctxMenu.deleteLater() @@ -675,9 +679,16 @@ def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd): assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text + # Mark + docEditor.setCursorPosition(50) + assert docEditor.docAction(nwDocAction.MD_MARK) is True + assert docEditor.getText() == text.replace("consectetur", "==consectetur==") + assert docEditor.docAction(nwDocAction.UNDO) is True + assert docEditor.getText() == text + # Redo assert docEditor.docAction(nwDocAction.REDO) is True - assert docEditor.getText() == text.replace("consectetur", "~~consectetur~~") + assert docEditor.getText() == text.replace("consectetur", "==consectetur==") assert docEditor.docAction(nwDocAction.UNDO) is True assert docEditor.getText() == text @@ -2081,7 +2092,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): origText = docEditor.getText() # Select the Word "est" - docEditor.setCursorPosition(645) + docEditor.setCursorPosition(663) docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor) cursor = docEditor.textCursor() assert cursor.selectedText() == "est" @@ -2094,11 +2105,11 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): # Find next by enter key monkeypatch.setattr(docSearch.searchBox, "hasFocus", lambda: True) qtbot.keyClick(docSearch.searchBox, Qt.Key.Key_Return, delay=KEY_DELAY) - assert abs(docEditor.getCursorPosition() - 1299) < 3 + assert abs(docEditor.getCursorPosition() - 1317) < 3 # Find next by button qtbot.mouseClick(docSearch.searchButton, QtMouseLeft, delay=KEY_DELAY) - assert abs(docEditor.getCursorPosition() - 1513) < 3 + assert abs(docEditor.getCursorPosition() - 1531) < 3 # Activate loop search docSearch.toggleLoop.activate(QAction.ActionEvent.Trigger) @@ -2107,7 +2118,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): # Find next by menu Search > Find Next nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger) - assert abs(docEditor.getCursorPosition() - 647) < 3 + assert abs(docEditor.getCursorPosition() - 665) < 3 # Close search docSearch.cancelSearch.activate(QAction.ActionEvent.Trigger) @@ -2146,13 +2157,13 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): # Set valid RegEx docSearch.setSearchText(r"\bSus") qtbot.mouseClick(docSearch.searchButton, QtMouseLeft, delay=KEY_DELAY) - assert abs(docEditor.getCursorPosition() - 223) < 3 + assert abs(docEditor.getCursorPosition() - 241) < 3 # Find next and then prev nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger) - assert abs(docEditor.getCursorPosition() - 324) < 3 + assert abs(docEditor.getCursorPosition() - 342) < 3 nwGUI.mainMenu.aFindPrev.activate(QAction.ActionEvent.Trigger) - assert abs(docEditor.getCursorPosition() - 223) < 3 + assert abs(docEditor.getCursorPosition() - 241) < 3 # Make RegEx case sensitive docSearch.toggleCase.activate(QAction.ActionEvent.Trigger) @@ -2161,11 +2172,11 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): # Find next/prev (one result) nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger) - assert abs(docEditor.getCursorPosition() - 626) < 3 + assert abs(docEditor.getCursorPosition() - 644) < 3 nwGUI.mainMenu.aFindPrev.activate(QAction.ActionEvent.Trigger) - assert abs(docEditor.getCursorPosition() - 626) < 3 + assert abs(docEditor.getCursorPosition() - 644) < 3 nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger) - assert abs(docEditor.getCursorPosition() - 626) < 3 + assert abs(docEditor.getCursorPosition() - 644) < 3 # Trigger replace nwGUI.mainMenu.aReplace.activate(QAction.ActionEvent.Trigger) @@ -2182,22 +2193,22 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): assert CONFIG.searchMatchCap is True # Replace "Sus" with "Foo" via menu - docEditor.setCursorPosition(605) + docEditor.setCursorPosition(623) nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger) nwGUI.mainMenu.aReplaceNext.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[623:634] == "Foopendisse" + assert docEditor.getText()[641:652] == "Foopendisse" # Find next/prev to loop file nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger) - assert abs(docEditor.getCursorPosition() - 223) < 3 + assert abs(docEditor.getCursorPosition() - 241) < 3 nwGUI.mainMenu.aFindPrev.activate(QAction.ActionEvent.Trigger) - assert abs(docEditor.getCursorPosition() - 1805) < 3 + assert abs(docEditor.getCursorPosition() - 1823) < 3 nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger) - assert abs(docEditor.getCursorPosition() - 223) < 3 + assert abs(docEditor.getCursorPosition() - 241) < 3 # Replace "sus" with "foo" via replace button qtbot.mouseClick(docSearch.replaceButton, QtMouseLeft, delay=KEY_DELAY) - assert docEditor.getText()[220:228] == "foocipit" + assert docEditor.getText()[238:246] == "foocipit" # Revert last two replaces assert docEditor.docAction(nwDocAction.UNDO) @@ -2211,7 +2222,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): # Close search and select "est" again docSearch.cancelSearch.activate(QAction.ActionEvent.Trigger) - docEditor.setCursorPosition(645) + docEditor.setCursorPosition(663) docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor) cursor = docEditor.textCursor() assert cursor.selectedText() == "est" @@ -2228,9 +2239,9 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): # Only one match nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger) - assert abs(docEditor.getCursorPosition() - 647) < 3 + assert abs(docEditor.getCursorPosition() - 665) < 3 nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger) - assert abs(docEditor.getCursorPosition() - 647) < 3 + assert abs(docEditor.getCursorPosition() - 665) < 3 # Enable next doc search docSearch.toggleProject.activate(QAction.ActionEvent.Trigger) @@ -2241,9 +2252,9 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger) assert docEditor.docHandle == "2426c6f0ca922" # Next document nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger) - assert abs(docEditor.getCursorPosition() - 620) < 3 + assert abs(docEditor.getCursorPosition() - 651) < 3 nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger) - assert abs(docEditor.getCursorPosition() - 1127) < 3 + assert abs(docEditor.getCursorPosition() - 1157) < 3 # Next doc, no match assert CONFIG.searchNextFile is True @@ -2345,3 +2356,142 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): assert docEditor.textCursor().selectedText() == "" # qtbot.stop() + + +@pytest.mark.gui +def testGuiEditor_TextAutoReplaceSymbols(): + """Test the editor auto-replace functionality.""" + CONFIG.fmtSQuoteOpen = nwUnicode.U_LSQUO + CONFIG.fmtSQuoteClose = nwUnicode.U_RSQUO + CONFIG.fmtDQuoteOpen = nwUnicode.U_LDQUO + CONFIG.fmtDQuoteClose = nwUnicode.U_RDQUO + + CONFIG.doReplaceSQuote = True + CONFIG.doReplaceDQuote = True + CONFIG.doReplaceDash = True + CONFIG.doReplaceDots = True + + ar = TextAutoReplace() + + def prep(text: str) -> tuple[str, int]: + return text, len(text) + + # Double Quote Open + assert ar._determine(*prep('"')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep('Stuff "')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep('>"')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep('>>"')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep('_"')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep(' _"')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep('\u00a0_"')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep('**"')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep(' **"')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep('\u00a0**"')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep('=="')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep(' =="')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep('\u00a0=="')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep('~~"')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep(' ~~"')) == (1, nwUnicode.U_LDQUO) + assert ar._determine(*prep('\u00a0~~"')) == (1, nwUnicode.U_LDQUO) + + # Double Quote Close + assert ar._determine(*prep('Stuff"')) == (1, nwUnicode.U_RDQUO) + + # Single Quote Open + assert ar._determine(*prep("'")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep("Stuff '")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep(">'")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep(">>'")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep("_'")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep(" _'")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep("\u00a0_'")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep("**'")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep(" **'")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep("\u00a0**'")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep("=='")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep(" =='")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep("\u00a0=='")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep("~~'")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep(" ~~'")) == (1, nwUnicode.U_LSQUO) + assert ar._determine(*prep("\u00a0~~'")) == (1, nwUnicode.U_LSQUO) + + # Single Quote Close + assert ar._determine(*prep("Stuff'")) == (1, nwUnicode.U_RSQUO) + + # Dashes + assert ar._determine(*prep("-")) == (0, "-") + assert ar._determine(*prep("--")) == (2, nwUnicode.U_ENDASH) + assert ar._determine(*prep("---")) == (3, nwUnicode.U_EMDASH) + assert ar._determine(*prep("----")) == (4, nwUnicode.U_HBAR) + assert ar._determine(*prep("\u2013-")) == (2, nwUnicode.U_EMDASH) + assert ar._determine(*prep("\u2014-")) == (2, nwUnicode.U_HBAR) + + # Ellipsis + assert ar._determine(*prep(".")) == (0, ".") + assert ar._determine(*prep("..")) == (0, ".") + assert ar._determine(*prep("...")) == (3, nwUnicode.U_HELLIP) + + # Block Typed Line Separator (#1150) + assert ar._determine(*prep("Text\u2028")) == (1, nwUnicode.U_PSEP) + + +@pytest.mark.gui +def testGuiEditor_TextAutoReplaceProcess(): + """Test the editor auto-replace functionality.""" + CONFIG.fmtDQuoteOpen = nwUnicode.U_LAQUO + CONFIG.fmtDQuoteClose = nwUnicode.U_RAQUO + + CONFIG.doReplaceDQuote = True + CONFIG.doReplaceDots = True + + ar = TextAutoReplace() + doc = QTextDocument() + + def prep(text: str) -> tuple[str, QTextCursor]: + doc.setPlainText(text) + cursor = QTextCursor(doc) + cursor.setPosition(len(text)) + return text, cursor + + # Nothing to Process + assert ar.process(*prep("")) is False + + # Standard Auto-Replace + assert ar.process(*prep("Text ...")) is True + assert doc.toRawText() == "Text \u2026" + + # Pad Before, Normal + CONFIG.fmtPadBefore = ":\u00bb" + CONFIG.fmtPadThin = False + ar.initSettings() + assert ar.process(*prep("Text:")) is True + assert doc.toRawText() == "Text\u00a0:" + assert ar.process(*prep("Text :")) is True # See #1061 + assert doc.toRawText() == "Text\u00a0:" + assert ar.process(*prep('Text"')) is True + assert doc.toRawText() == "Text\u00a0»" + assert ar.process(*prep("@Synopsis:")) is False + assert doc.toRawText() == "@Synopsis:" + + # Pad Before, Thin + CONFIG.fmtPadBefore = ":\u00bb" + CONFIG.fmtPadThin = True + ar.initSettings() + assert ar.process(*prep("Text:")) is True + assert doc.toRawText() == "Text\u202f:" + assert ar.process(*prep("Text :")) is True # See #1061 + assert doc.toRawText() == "Text\u202f:" + + # Pad After, Normal + CONFIG.fmtPadAfter = "\u00ab" + CONFIG.fmtPadThin = False + ar.initSettings() + assert ar.process(*prep('Text "')) is True + assert doc.toRawText() == "Text «\u00a0" + + # Pad After, Thin + CONFIG.fmtPadAfter = "\u00ab" + CONFIG.fmtPadThin = True + ar.initSettings() + assert ar.process(*prep('Text "')) is True + assert doc.toRawText() == "Text «\u202f" diff --git a/tests/test_gui/test_gui_dochighlighter.py b/tests/test_gui/test_gui_dochighlighter.py new file mode 100644 index 00000000..dfdb1208 --- /dev/null +++ b/tests/test_gui/test_gui_dochighlighter.py @@ -0,0 +1,632 @@ +""" +novelWriter – GUI Syntax Highlighter Tester +=========================================== + +This file is a part of novelWriter +Copyright (C) 2025 Veronica Berglyd Olsen and novelWriter contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" # noqa +from __future__ import annotations + +import pytest + +from PyQt6.QtGui import QTextCharFormat, QTextCursor, QTextDocument + +from novelwriter import CONFIG, SHARED +from novelwriter.core.item import NWItem +from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme +from novelwriter.gui.dochighlight import BLOCK_META, BLOCK_TITLE, GuiDocHighlighter, TextBlockData +from novelwriter.types import QtKeepAnchor + +R_HANDLE = "3456789abcdef" +T_HANDLE = "0123456789abc" + + +@pytest.fixture() +def syntax(nwGUI): + """Create a syntax object for use with testing.""" + CONFIG.lightTheme = "default_light" + CONFIG.themeMode = nwTheme.LIGHT + + CONFIG.dialogStyle = 3 + CONFIG.fmtDQuoteOpen = "\u201c" + CONFIG.fmtDQuoteClose = "\u201d" + CONFIG.altDialogOpen = "::" + CONFIG.altDialogClose = "::" + CONFIG.showMultiSpaces = True + + theme = SHARED.theme + theme.loadTheme(force=True) + assert theme._guiPalette.base().color().getRgb() == (0xfc, 0xfc, 0xfc, 0xff) + assert theme.syntaxTheme.text.getRgb() == (0x30, 0x30, 0x30, 0xff) + + doc = QTextDocument() + syntax = GuiDocHighlighter(doc) + + # Add a Mock Item + tRoot = NWItem(SHARED.project, R_HANDLE) + tRoot.setClass(nwItemClass.NOVEL) + tRoot.setType(nwItemType.ROOT) + + tItem = NWItem(SHARED.project, T_HANDLE) + tItem.setParent(R_HANDLE) + tItem.setLayout(nwItemLayout.NOTE) + tItem.setClass(nwItemClass.NOVEL) + tItem.setType(nwItemType.FILE) + + SHARED.project.tree.add(tRoot) + SHARED.project.tree.add(tItem) + + yield syntax + + +def getFragments( + syntax: GuiDocHighlighter +) -> tuple[list[tuple[int, str]], list[QTextCharFormat]]: + """Extract all syntax highlighter fragments from a document.""" + pieces = [] + formats = [] + doc = syntax.document() + cursor = QTextCursor(doc) + assert doc is not None + for b in range(doc.blockCount()): + block = doc.findBlockByNumber(b) + first = block.position() + syntax.rehighlightBlock(block) + if layout := block.layout(): + for fmt in layout.formats(): + cursor.setPosition(first + fmt.start) + cursor.setPosition(first + fmt.start + fmt.length, QtKeepAnchor) + pieces.append((b, fmt.start, fmt.length, cursor.selectedText())) + formats.append(fmt.format) + return pieces, formats + + +def maxOrd(text: str) -> int: + """Get the max character value of a string.""" + return max(ord(c) for c in text) + + +@pytest.mark.gui +def testGuiDocHighlighter_Basic(syntax): + """Test the basic functionality of the syntax highlighter.""" + # Alternate Spell Check + assert syntax._spellCheck is False + syntax.setSpellCheck(True) + assert syntax._spellCheck is True + + # Check Handle + assert syntax._tHandle is None + syntax.setHandle(T_HANDLE) + assert syntax._tHandle == T_HANDLE + assert syntax._isNovel is False + assert syntax._isInactive is False + + tItem = SHARED.project.tree[T_HANDLE] + assert tItem is not None + tItem.setLayout(nwItemLayout.DOCUMENT) + tItem.setClass(nwItemClass.ARCHIVE) + syntax.setHandle(T_HANDLE) + assert syntax._tHandle == T_HANDLE + assert syntax._isNovel is True + assert syntax._isInactive is True + + +@pytest.mark.gui +def testGuiDocHighlighter_Keywords(syntax): + """Test highlighting of keywords.""" + theme = SHARED.theme + doc = syntax.document() + assert doc is not None + + # Settings + syntax._tHandle = T_HANDLE + + colKey = theme.syntaxTheme.key.getRgb() + colTag = theme.syntaxTheme.tag.getRgb() + colOpt = theme.syntaxTheme.opt.getRgb() + colErr = theme.syntaxTheme.error.getRgb() + + # Ascii + doc.setPlainText( + "@tag: Bob | Robert\n" + "@char: Someone\n" + ) + syntax.rehighlightByType(BLOCK_META) + assert maxOrd(doc.toPlainText()) <= 0x7f + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 4, "@tag"), (0, 6, 3, "Bob"), (0, 12, 6, "Robert"), + (1, 0, 5, "@char"), (1, 7, 7, "Someone"), + ] + assert formats[0].foreground().color().getRgb() == colKey + assert formats[1].foreground().color().getRgb() == colTag + assert formats[2].foreground().color().getRgb() == colOpt + assert formats[3].foreground().color().getRgb() == colKey + assert formats[4].underlineColor().getRgb() == colErr + + # # Unicode <= 0xFFFF + doc.setPlainText( + "@tag: Zoë | Zoë Smith\n" + "@char: Олексій\n" + ) + syntax.rehighlightByType(BLOCK_META) + assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 4, "@tag"), (0, 6, 3, "Zoë"), (0, 12, 9, "Zoë Smith"), + (1, 0, 5, "@char"), (1, 7, 7, "Олексій"), + ] + assert formats[0].foreground().color().getRgb() == colKey + assert formats[1].foreground().color().getRgb() == colTag + assert formats[2].foreground().color().getRgb() == colOpt + assert formats[3].foreground().color().getRgb() == colKey + assert formats[4].underlineColor().getRgb() == colErr + + # # Unicode > 0xFFFF + doc.setPlainText( + "@tag: 😄 | Smiley 😄\n" + "@char: 😎😎😎\n" + ) + syntax.rehighlightByType(BLOCK_META) + assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 4, "@tag"), (0, 6, 2, "😄"), (0, 11, 9, "Smiley 😄"), + (1, 0, 5, "@char"), (1, 7, 6, "😎😎😎"), + ] + assert formats[0].foreground().color().getRgb() == colKey + assert formats[1].foreground().color().getRgb() == colTag + assert formats[2].foreground().color().getRgb() == colOpt + assert formats[3].foreground().color().getRgb() == colKey + assert formats[4].underlineColor().getRgb() == colErr + + +@pytest.mark.gui +def testGuiDocHighlighter_Titles(syntax): + """Test highlighting of titles.""" + theme = SHARED.theme + doc = syntax.document() + assert doc is not None + + # Settings + syntax._tHandle = T_HANDLE + + colHeadMark = theme.syntaxTheme.headH.getRgb() + colHeadText = theme.syntaxTheme.head.getRgb() + + # Ascii + doc.setPlainText( + "# Heading 1\n\n" + "## Heading 2\n\n" + "### Heading 3\n\n" + "#### Heading 4\n\n" + "#! Heading A1\n\n" + "##! Heading A2\n\n" + "###! Heading A3\n\n" + ) + syntax.rehighlightByType(BLOCK_TITLE) + assert maxOrd(doc.toPlainText()) <= 0x7f + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 1, "#"), (0, 1, 10, " Heading 1"), + (2, 0, 2, "##"), (2, 2, 10, " Heading 2"), + (4, 0, 3, "###"), (4, 3, 10, " Heading 3"), + (6, 0, 4, "####"), (6, 4, 10, " Heading 4"), + (8, 0, 2, "#!"), (8, 2, 11, " Heading A1"), + (10, 0, 3, "##!"), (10, 3, 11, " Heading A2"), + (12, 0, 4, "###!"), (12, 4, 11, " Heading A3"), + ] + for i in range(0, len(formats), 2): + assert formats[i].foreground().color().getRgb() == colHeadMark + assert formats[i+1].foreground().color().getRgb() == colHeadText + + # Unicode <= 0xFFFF + doc.setPlainText( + "# Ȟǣđ 1\n\n" + "## Ȟǣđ 2\n\n" + "### Ȟǣđ 3\n\n" + "#### Ȟǣđ 4\n\n" + "#! Ȟǣđ A1\n\n" + "##! Ȟǣđ A2\n\n" + "###! Ȟǣđ A3\n\n" + ) + syntax.rehighlightByType(BLOCK_TITLE) + assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 1, "#"), (0, 1, 6, " Ȟǣđ 1"), + (2, 0, 2, "##"), (2, 2, 6, " Ȟǣđ 2"), + (4, 0, 3, "###"), (4, 3, 6, " Ȟǣđ 3"), + (6, 0, 4, "####"), (6, 4, 6, " Ȟǣđ 4"), + (8, 0, 2, "#!"), (8, 2, 7, " Ȟǣđ A1"), + (10, 0, 3, "##!"), (10, 3, 7, " Ȟǣđ A2"), + (12, 0, 4, "###!"), (12, 4, 7, " Ȟǣđ A3"), + ] + for i in range(0, len(formats), 2): + assert formats[i].foreground().color().getRgb() == colHeadMark + assert formats[i+1].foreground().color().getRgb() == colHeadText + + # Unicode > 0xFFFF + doc.setPlainText( + "# 😇😎 1\n\n" + "## 😇😎 2\n\n" + "### 😇😎 3\n\n" + "#### 😇😎 4\n\n" + "#! 😇😎 A1\n\n" + "##! 😇😎 A2\n\n" + "###! 😇😎 A3\n\n" + ) + syntax.rehighlightByType(BLOCK_TITLE) + assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 1, "#"), (0, 1, 7, " 😇😎 1"), + (2, 0, 2, "##"), (2, 2, 7, " 😇😎 2"), + (4, 0, 3, "###"), (4, 3, 7, " 😇😎 3"), + (6, 0, 4, "####"), (6, 4, 7, " 😇😎 4"), + (8, 0, 2, "#!"), (8, 2, 8, " 😇😎 A1"), + (10, 0, 3, "##!"), (10, 3, 8, " 😇😎 A2"), + (12, 0, 4, "###!"), (12, 4, 8, " 😇😎 A3"), + ] + for i in range(0, len(formats), 2): + assert formats[i].foreground().color().getRgb() == colHeadMark + assert formats[i+1].foreground().color().getRgb() == colHeadText + + +@pytest.mark.gui +def testGuiDocHighlighter_Comments(syntax): + """Test highlighting of comments.""" + theme = SHARED.theme + doc = syntax.document() + assert doc is not None + + # Settings + syntax._tHandle = T_HANDLE + + colHidden = theme.syntaxTheme.hidden.getRgb() + colMod = theme.syntaxTheme.mod.getRgb() + colValue = theme.syntaxTheme.val.getRgb() + colNote = theme.syntaxTheme.note.getRgb() + + # Ascii + doc.setPlainText( + "% Plain\n" + "%~ Ignored\n" + "%Synopsis: Synopsis\n" + "%Note.Stuff: Note\n" + ) + syntax.rehighlight() + assert maxOrd(doc.toPlainText()) <= 0x7f + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 7, "% Plain"), + (1, 0, 10, "%~ Ignored"), + (2, 0, 10, "%Synopsis:"), (2, 10, 9, " Synopsis"), + (3, 0, 6, "%Note."), (3, 6, 6, "Stuff:"), (3, 12, 5, " Note"), + ] + assert formats[0].foreground().color().getRgb() == colHidden + assert formats[1].foreground().color().getRgb() == colHidden + assert formats[1].fontStrikeOut() is True + assert formats[2].foreground().color().getRgb() == colMod + assert formats[3].foreground().color().getRgb() == colNote + assert formats[4].foreground().color().getRgb() == colMod + assert formats[5].foreground().color().getRgb() == colValue + assert formats[6].foreground().color().getRgb() == colNote + + # Unicode <= 0xFFFF + doc.setPlainText( + "% Рівнина\n" + "%~ Ігноровано\n" + "%Synopsis: Синопсис\n" + "%Note.Stuff: Примітка\n" + ) + syntax.rehighlight() + assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 9, "% Рівнина"), + (1, 0, 13, "%~ Ігноровано"), + (2, 0, 10, "%Synopsis:"), (2, 10, 9, " Синопсис"), + (3, 0, 6, "%Note."), (3, 6, 6, "Stuff:"), (3, 12, 9, " Примітка"), + ] + assert formats[0].foreground().color().getRgb() == colHidden + assert formats[1].foreground().color().getRgb() == colHidden + assert formats[1].fontStrikeOut() is True + assert formats[2].foreground().color().getRgb() == colMod + assert formats[3].foreground().color().getRgb() == colNote + assert formats[4].foreground().color().getRgb() == colMod + assert formats[5].foreground().color().getRgb() == colValue + assert formats[6].foreground().color().getRgb() == colNote + + # Unicode > 0xFFFF + doc.setPlainText( + "% 😎😎\n" + "%~ 🙈🙈🙈\n" + "%Synopsis: 😍😍😍😍\n" + "%Note.Stuff: 😡😡😡😡😡\n" + ) + syntax.rehighlight() + assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 6, "% 😎😎"), + (1, 0, 9, "%~ 🙈🙈🙈"), + (2, 0, 10, "%Synopsis:"), (2, 10, 9, " 😍😍😍😍"), + (3, 0, 6, "%Note."), (3, 6, 6, "Stuff:"), (3, 12, 11, " 😡😡😡😡😡"), + ] + assert formats[0].foreground().color().getRgb() == colHidden + assert formats[1].foreground().color().getRgb() == colHidden + assert formats[1].fontStrikeOut() is True + assert formats[2].foreground().color().getRgb() == colMod + assert formats[3].foreground().color().getRgb() == colNote + assert formats[4].foreground().color().getRgb() == colMod + assert formats[5].foreground().color().getRgb() == colValue + assert formats[6].foreground().color().getRgb() == colNote + + +@pytest.mark.gui +def testGuiDocHighlighter_Special(syntax): + """Test highlighting of special commands.""" + theme = SHARED.theme + doc = syntax.document() + assert doc is not None + + # Settings + syntax._tHandle = T_HANDLE + + colErr = theme.syntaxTheme.error.getRgb() + colCode = theme.syntaxTheme.code.getRgb() + colValue = theme.syntaxTheme.val.getRgb() + + # Ascii + doc.setPlainText( + "[NewPage]\n" + "[New Page]\n" + "[VSpace]\n" + "[VSpace:123]\n" + "[VSpace:Meh]\n" + ) + syntax.rehighlight() + assert maxOrd(doc.toPlainText()) <= 0x7f + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 9, "[NewPage]"), + (1, 0, 10, "[New Page]"), + (2, 0, 8, "[VSpace]"), + (3, 0, 8, "[VSpace:"), (3, 8, 3, "123"), (3, 11, 1, "]"), + (4, 0, 8, "[VSpace:"), (4, 8, 3, "Meh"), (4, 11, 1, "]"), + ] + assert formats[0].foreground().color().getRgb() == colCode + assert formats[1].foreground().color().getRgb() == colCode + assert formats[2].foreground().color().getRgb() == colCode + assert formats[3].foreground().color().getRgb() == colCode + assert formats[4].foreground().color().getRgb() == colValue + assert formats[5].foreground().color().getRgb() == colCode + assert formats[6].foreground().color().getRgb() == colCode + assert formats[7].underlineColor().getRgb() == colErr + assert formats[8].foreground().color().getRgb() == colCode + + # Unicode <= 0xFFFF + doc.setPlainText( + "[NewPage]\n" + "[New Page]\n" + "[VSpace]\n" + "[VSpace:123]\n" + "[VSpace:⅘]\n" + ) + syntax.rehighlight() + assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 9, "[NewPage]"), + (1, 0, 10, "[New Page]"), + (2, 0, 8, "[VSpace]"), + (3, 0, 8, "[VSpace:"), (3, 8, 3, "123"), (3, 11, 1, "]"), + (4, 0, 8, "[VSpace:"), (4, 8, 1, "⅘"), (4, 9, 1, "]"), + ] + assert formats[0].foreground().color().getRgb() == colCode + assert formats[1].foreground().color().getRgb() == colCode + assert formats[2].foreground().color().getRgb() == colCode + assert formats[3].foreground().color().getRgb() == colCode + assert formats[4].foreground().color().getRgb() == colValue + assert formats[5].foreground().color().getRgb() == colCode + assert formats[6].foreground().color().getRgb() == colCode + assert formats[7].underlineColor().getRgb() == colErr + assert formats[8].foreground().color().getRgb() == colCode + + # Unicode > 0xFFFF + doc.setPlainText( + "[NewPage]\n" + "[New Page]\n" + "[VSpace]\n" + "[VSpace:123]\n" + "[VSpace:🙈🙈]\n" + ) + syntax.rehighlight() + assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 9, "[NewPage]"), + (1, 0, 10, "[New Page]"), + (2, 0, 8, "[VSpace]"), + (3, 0, 8, "[VSpace:"), (3, 8, 3, "123"), (3, 11, 1, "]"), + (4, 0, 8, "[VSpace:"), (4, 8, 4, "🙈🙈"), (4, 12, 1, "]"), + ] + assert formats[0].foreground().color().getRgb() == colCode + assert formats[1].foreground().color().getRgb() == colCode + assert formats[2].foreground().color().getRgb() == colCode + assert formats[3].foreground().color().getRgb() == colCode + assert formats[4].foreground().color().getRgb() == colValue + assert formats[5].foreground().color().getRgb() == colCode + assert formats[6].foreground().color().getRgb() == colCode + assert formats[7].underlineColor().getRgb() == colErr + assert formats[8].foreground().color().getRgb() == colCode + + +@pytest.mark.gui +def testGuiDocHighlighter_Text(monkeypatch, syntax): + """Test highlighting of text.""" + theme = SHARED.theme + doc = syntax.document() + assert doc is not None + + # Settings + syntax._tHandle = T_HANDLE + syntax._isNovel = True + syntax.setSpellCheck(True) + monkeypatch.setattr(SHARED.spelling, "checkWord", lambda *a: False) + + colHidden = theme.syntaxTheme.hidden.getRgb() + colEmph = theme.syntaxTheme.emph.getRgb() + colLink = theme.syntaxTheme.link.getRgb() + colSpell = theme.syntaxTheme.spell.getRgb() + colCode = theme.syntaxTheme.code.getRgb() + colDialogue = theme.syntaxTheme.dialN.getRgb() + colAltDialogue = theme.syntaxTheme.dialA.getRgb() + + # Ascii + doc.setPlainText( + "Text **bold** text _italic_ text ~~strike~~ text [b]bold[/b], http://example.com\n\n" + ) + syntax.rehighlight() + assert maxOrd(doc.toPlainText()) <= 0x7f + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 4, "Text"), + (0, 5, 2, "**"), (0, 7, 4, "bold"), (0, 11, 2, "**"), + (0, 14, 4, "text"), + (0, 19, 1, "_"), (0, 20, 6, "italic"), (0, 26, 1, "_"), + (0, 28, 4, "text"), + (0, 33, 2, "~~"), (0, 35, 6, "strike"), (0, 41, 2, "~~"), + (0, 44, 4, "text"), + (0, 49, 3, "[b]"), (0, 52, 4, "bold"), (0, 56, 4, "[/b]"), + (0, 62, 18, "http://example.com"), + ] + assert formats[0].underlineColor().getRgb() == colSpell # Text + assert formats[1].foreground().color().getRgb() == colHidden # ** + assert formats[2].foreground().color().getRgb() == colEmph # bold + assert formats[2].underlineColor().getRgb() == colSpell + assert formats[3].foreground().color().getRgb() == colHidden # ** + assert formats[4].underlineColor().getRgb() == colSpell # text + assert formats[5].foreground().color().getRgb() == colHidden # _ + assert formats[6].foreground().color().getRgb() == colEmph # italic + assert formats[6].underlineColor().getRgb() == colSpell + assert formats[7].foreground().color().getRgb() == colHidden # _ + assert formats[8].underlineColor().getRgb() == colSpell # text + assert formats[9].foreground().color().getRgb() == colHidden # ~~ + assert formats[10].foreground().color().getRgb() == colHidden # strike + assert formats[10].fontStrikeOut() is True + assert formats[10].underlineColor().getRgb() == colSpell + assert formats[11].foreground().color().getRgb() == colHidden # ~~ + assert formats[12].underlineColor().getRgb() == colSpell # text + assert formats[13].foreground().color().getRgb() == colCode # [b] + assert formats[14].underlineColor().getRgb() == colSpell # bold + assert formats[15].foreground().color().getRgb() == colCode # [/b] + assert formats[16].foreground().color().getRgb() == colLink # http://example.com + + # Spell Check + data = doc.findBlockByNumber(0).userData() + assert isinstance(data, TextBlockData) + assert data.metaData == [(62, 80, "http://example.com", "url")] + assert data.spellErrors == [ + (0, 4, "Text"), (7, 11, "bold"), + (14, 18, "text"), (20, 26, "italic"), + (28, 32, "text"), (35, 41, "strike"), + (44, 48, "text"), (52, 56, "bold"), + ] + + # Unicode <= 0xFFFF + doc.setPlainText( + "\u201cDialogue,\u201d and then ::dialogue::, http://example.com\n\n" + ) + syntax.rehighlight() + assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 1, "\u201c"), (0, 1, 8, "Dialogue"), (0, 9, 2, ",\u201d"), + (0, 12, 3, "and"), (0, 16, 4, "then"), + (0, 21, 2, "::"), (0, 23, 8, "dialogue"), (0, 31, 2, "::"), + (0, 35, 18, "http://example.com"), + ] + assert formats[0].foreground().color().getRgb() == colDialogue # Quote + assert formats[1].foreground().color().getRgb() == colDialogue # Dialogue + assert formats[1].underlineColor().getRgb() == colSpell + assert formats[2].foreground().color().getRgb() == colDialogue # Quote + assert formats[3].underlineColor().getRgb() == colSpell # and + assert formats[4].underlineColor().getRgb() == colSpell # then + assert formats[5].foreground().color().getRgb() == colAltDialogue # :: + assert formats[6].foreground().color().getRgb() == colAltDialogue # dialogue + assert formats[6].underlineColor().getRgb() == colSpell + assert formats[7].foreground().color().getRgb() == colAltDialogue # :: + assert formats[8].foreground().color().getRgb() == colLink # http://example.com + + # Spell Check + data = doc.findBlockByNumber(0).userData() + assert isinstance(data, TextBlockData) + assert data.metaData == [(35, 53, "http://example.com", "url")] + assert data.spellErrors == [ + (1, 9, "Dialogue"), (12, 15, "and"), + (16, 20, "then"), (23, 31, "dialogue"), + ] + + # Unicode > 0xFFFF + doc.setPlainText( + "\u201c😁 Grinning 😁,\u201d and then ::🙊 shush 🙊::, http://example.com\n\n" + ) + syntax.rehighlight() + assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff + + pieces, formats = getFragments(syntax) + assert pieces == [ + (0, 0, 4, "\u201c😁 "), (0, 4, 8, "Grinning"), (0, 12, 5, " 😁,\u201d"), + (0, 18, 3, "and"), (0, 22, 4, "then"), + (0, 27, 5, "::🙊 "), (0, 32, 5, "shush"), (0, 37, 5, " 🙊::"), + (0, 44, 18, "http://example.com"), + ] + assert formats[0].foreground().color().getRgb() == colDialogue # Quote😁 + assert formats[1].foreground().color().getRgb() == colDialogue # Grinning + assert formats[1].underlineColor().getRgb() == colSpell + assert formats[2].foreground().color().getRgb() == colDialogue # 😁Quote + assert formats[3].underlineColor().getRgb() == colSpell # and + assert formats[4].underlineColor().getRgb() == colSpell # then + assert formats[5].foreground().color().getRgb() == colAltDialogue # ::🙊 + assert formats[6].foreground().color().getRgb() == colAltDialogue # shush + assert formats[6].underlineColor().getRgb() == colSpell + assert formats[7].foreground().color().getRgb() == colAltDialogue # 🙊:: + assert formats[8].foreground().color().getRgb() == colLink # http://example.com + + # Spell Check + data = doc.findBlockByNumber(0).userData() + assert isinstance(data, TextBlockData) + assert data.metaData == [(40, 58, "http://example.com", "url")] + assert data.spellErrors == [ + (4, 12, "Grinning"), (18, 21, "and"), + (22, 26, "then"), (32, 37, "shush"), + ] diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 65c31a24..ac16173b 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from unittest.mock import MagicMock @@ -223,17 +223,23 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): CONFIG.showFullPath = True # Document footer show/hide synopsis - assert nwGUI.viewDocument("f96ec11c6a3da") is True + nwGUI.viewDocument("f96ec11c6a3da") assert len(docViewer.toPlainText()) == 4314 docViewer.docFooter._doToggleSynopsis(False) assert len(docViewer.toPlainText()) == 4098 # Document footer show/hide comments - assert nwGUI.viewDocument("846352075de7d") is True + nwGUI.viewDocument("846352075de7d") assert len(docViewer.toPlainText()) == 683 docViewer.docFooter._doToggleComments(False) assert len(docViewer.toPlainText()) == 634 + # Document footer show/hide notes + nwGUI.viewDocument("88d59a277361b") + assert len(docViewer.toPlainText()) == 900 + docViewer.docFooter._doToggleNotes(False) + assert len(docViewer.toPlainText()) == 871 + # Crash the HTML rendering with monkeypatch.context() as mp: mp.setattr(ToQTextDocument, "doConvert", causeException) diff --git a/tests/test_gui/test_gui_docviewerpanel.py b/tests/test_gui/test_gui_docviewerpanel.py index b0d02493..32c3c54e 100644 --- a/tests/test_gui/test_gui_docviewerpanel.py +++ b/tests/test_gui/test_gui_docviewerpanel.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import pytest @@ -224,7 +224,7 @@ def testGuiViewerPanel_Tags(qtbot, monkeypatch, caplog, nwGUI, projPath, mockRnd # Update Labels assert charTab.topLevelItem(0).text(charTab.C_IMPORT) == "New" - SHARED.project.data.itemImport.add(C.iNew, "Stuff", (100, 100, 100), "SQUARE", 0) + SHARED.project.data.itemImport.add(C.iNew, "Stuff", "#646464", "SQUARE", 0) viewPanel.updateStatusLabels("i") assert charTab.topLevelItem(0).text(charTab.C_IMPORT) == "Stuff" diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 69c4da26..258d5e0a 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -17,13 +17,14 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import shutil from pathlib import Path from shutil import copyfile +from unittest.mock import Mock import pytest @@ -32,9 +33,10 @@ from PyQt6.QtGui import QPalette from PyQt6.QtWidgets import QInputDialog, QMessageBox from novelwriter import CONFIG, SHARED +from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT from novelwriter.constants import nwFiles from novelwriter.dialogs.editlabel import GuiEditLabel -from novelwriter.enum import nwDocAction, nwDocMode, nwFocus, nwItemType, nwView +from novelwriter.enum import nwDocAction, nwDocMode, nwFocus, nwItemType, nwTheme, nwView from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.noveltree import GuiNovelView from novelwriter.gui.outline import GuiOutlineView @@ -179,21 +181,35 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): @pytest.mark.gui def testGuiMain_UpdateTheme(qtbot, nwGUI): """Test updating the theme in the GUI.""" - mainTheme = SHARED.theme - CONFIG.guiTheme = "default_dark" - CONFIG.guiSyntax = "default_dark" - mainTheme.loadTheme() - mainTheme.loadSyntax() + theme = SHARED.theme + CONFIG.themeMode = nwTheme.DARK + CONFIG.darkTheme = DEF_GUI_DARK + CONFIG.lightTheme = DEF_GUI_LIGHT + theme.loadTheme() + assert theme.isDarkTheme is True + nwGUI._processConfigChanges(False, True, False, False) nwGUI._processConfigChanges(True, True, True, True) + # Check editor syntax syntax = SHARED.theme.syntaxTheme - assert nwGUI.docEditor.palette().color(QPalette.ColorRole.Window) == syntax.back assert nwGUI.docEditor.docHeader.palette().color(QPalette.ColorRole.Window) == syntax.back assert nwGUI.docViewer.palette().color(QPalette.ColorRole.Window) == syntax.back assert nwGUI.docViewer.docHeader.palette().color(QPalette.ColorRole.Window) == syntax.back + # Update by check + CONFIG.themeMode = nwTheme.LIGHT + nwGUI.checkThemeUpdate() + assert theme.isDarkTheme is False + + # Through change event + event = Mock() + event.type.return_value = 210 + CONFIG.themeMode = nwTheme.DARK + nwGUI.changeEvent(event) + assert theme.isDarkTheme is True + # qtbot.stop() @@ -251,8 +267,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): # Change some settings CONFIG.hideHScroll = True CONFIG.hideVScroll = True - CONFIG.autoScrollPos = 80 - CONFIG.autoScroll = True + CONFIG.autoScroll = False # Add a Character File nwGUI._changeView(nwView.PROJECT) diff --git a/tests/test_gui/test_gui_i18n.py b/tests/test_gui/test_gui_i18n.py index aab521ce..915d83a4 100644 --- a/tests/test_gui/test_gui_i18n.py +++ b/tests/test_gui/test_gui_i18n.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import sys diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index f63f588a..9c327cda 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from unittest.mock import MagicMock @@ -68,154 +68,155 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): assert docEditor.docAction(nwDocAction.COPY) is False assert nwGUI.openProject(prjLipsum) is True + x = 72 # Split By Chapter assert nwGUI.openDocument("4c4f28287af27") is True - docEditor.setCursorPosition(57) - cleanText = docEditor.getText()[54:101] + docEditor.setCursorPosition(x+3) + cleanText = docEditor.getText()[x:x+47] # Bold mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger) fmtStr = "**Pellentesque** nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:105] == fmtStr + assert docEditor.getText()[x:x+51] == fmtStr mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[x:x+47] == cleanText # Italic mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger) fmtStr = "_Pellentesque_ nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:103] == fmtStr + assert docEditor.getText()[x:x+49] == fmtStr mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[x:x+47] == cleanText # Strikethrough mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger) fmtStr = "~~Pellentesque~~ nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:105] == fmtStr + assert docEditor.getText()[x:x+51] == fmtStr mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[x:x+47] == cleanText # Should get us back to plain mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger) mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger) mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger) mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[x:x+47] == cleanText # Double Quotes mainMenu.aFmtDQuote.activate(QAction.ActionEvent.Trigger) fmtStr = "“Pellentesque” nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:103] == fmtStr + assert docEditor.getText()[x:x+49] == fmtStr mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[x:x+47] == cleanText # Single Quotes mainMenu.aFmtSQuote.activate(QAction.ActionEvent.Trigger) fmtStr = "‘Pellentesque’ nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:103] == fmtStr + assert docEditor.getText()[x:x+49] == fmtStr mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[x:x+47] == cleanText # Block Formats # ============= # cSpell:ignore Pellentesque erat nulla posuere commodo - docEditor.setCursorPosition(57) + docEditor.setCursorPosition(x+3) # Header 1 mainMenu.aFmtHead1.activate(QAction.ActionEvent.Trigger) fmtStr = "# Pellentesque nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:103] == fmtStr + assert docEditor.getText()[x:x+49] == fmtStr # Header 2 mainMenu.aFmtHead2.activate(QAction.ActionEvent.Trigger) fmtStr = "## Pellentesque nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:104] == fmtStr + assert docEditor.getText()[x:x+50] == fmtStr # Header 3 mainMenu.aFmtHead3.activate(QAction.ActionEvent.Trigger) fmtStr = "### Pellentesque nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:105] == fmtStr + assert docEditor.getText()[x:x+51] == fmtStr # Header 4 mainMenu.aFmtHead4.activate(QAction.ActionEvent.Trigger) fmtStr = "#### Pellentesque nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:106] == fmtStr + assert docEditor.getText()[x:x+52] == fmtStr # Title Format mainMenu.aFmtTitle.activate(QAction.ActionEvent.Trigger) fmtStr = "#! Pellentesque nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:104] == fmtStr + assert docEditor.getText()[x:x+50] == fmtStr # Unnumbered Chapter mainMenu.aFmtUnNum.activate(QAction.ActionEvent.Trigger) fmtStr = "##! Pellentesque nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:105] == fmtStr + assert docEditor.getText()[x:x+51] == fmtStr # Hard Scene mainMenu.aFmtHardSc.activate(QAction.ActionEvent.Trigger) fmtStr = "###! Pellentesque nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:106] == fmtStr + assert docEditor.getText()[x:x+52] == fmtStr # Clear Format mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[x:x+47] == cleanText # Comment On mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger) fmtStr = "% Pellentesque nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:103] == fmtStr + assert docEditor.getText()[x:x+49] == fmtStr # Comment Off mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[x:x+47] == cleanText # Check comment with no space before text - docEditor.setCursorPosition(54) + docEditor.setCursorPosition(x) docEditor.insertText("%") fmtStr = "%Pellentesque nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:102] == fmtStr + assert docEditor.getText()[x:x+48] == fmtStr mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[x:x+47] == cleanText # Undo/Redo mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger) fmtStr = "%Pellentesque nec erat ut nulla posuere commodo." - assert docEditor.getText()[54:102] == fmtStr + assert docEditor.getText()[x:x+48] == fmtStr mainMenu.aEditRedo.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[54:101] == cleanText + assert docEditor.getText()[x:x+47] == cleanText # Cut, Copy and Paste - docEditor.setCursorPosition(54) + docEditor.setCursorPosition(x) docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor) mainMenu.aEditCut.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[54:104] == ( + assert docEditor.getText()[x:x+50] == ( " nec erat ut nulla posuere commodo. Curabitur nisi" ) mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[54:104] == ( + assert docEditor.getText()[x:x+50] == ( "Pellentesque nec erat ut nulla posuere commodo. Cu" ) - docEditor.setCursorPosition(54) + docEditor.setCursorPosition(x) docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor) mainMenu.aEditCopy.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[54:104] == ( + assert docEditor.getText()[x:x+50] == ( "Pellentesque nec erat ut nulla posuere commodo. Cu" ) - docEditor.setCursorPosition(54) + docEditor.setCursorPosition(x) mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger) - assert docEditor.getText()[54:104] == ( + assert docEditor.getText()[x:x+50] == ( "PellentesquePellentesque nec erat ut nulla posuere" ) mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger) # Select Paragraph/All - docEditor.setCursorPosition(57) + docEditor.setCursorPosition(x+3) mainMenu.aSelectPar.activate(QAction.ActionEvent.Trigger) cursor = docEditor.textCursor() assert cursor.selectedText() == ( @@ -228,10 +229,10 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): "nunc lacus, imperdiet nec posuere ac, interdum non lectus." ) - docEditor.setCursorPosition(57) + docEditor.setCursorPosition(x+3) mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger) cursor = docEditor.textCursor() - assert len(cursor.selectedText()) == 1910 + assert len(cursor.selectedText()) == 1928 # Clear the Text docEditor.clear() diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index cd726e0a..39938a3e 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from pathlib import Path diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 8ee650df..f93171dc 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import time diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index ae8e9af0..f6430f55 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from unittest.mock import MagicMock @@ -1454,7 +1454,7 @@ def testGuiProjTree_Templates(qtbot, monkeypatch, nwGUI, projPath, mockRnd): nwNewCharacter = project.tree[hNewCharacter] assert nwNewCharacter is not None assert nwNewCharacter.itemName == "Note" - assert project.storage.getDocument(hNewCharacter).readDocument() == "# Jane\n\n@tag: Jane\n\n" + assert project.storage.getDocument(hNewCharacter).readDocument() == "# Note\n\n@tag: Jane\n\n" # Clearing the menu and rebuilding it should work projBar.mTemplates.clearMenu() diff --git a/tests/test_gui/test_gui_search.py b/tests/test_gui/test_gui_search.py index 3110fa3e..a4065dbf 100644 --- a/tests/test_gui/test_gui_search.py +++ b/tests/test_gui/test_gui_search.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from time import time diff --git a/tests/test_gui/test_gui_sidebar.py b/tests/test_gui/test_gui_sidebar.py new file mode 100644 index 00000000..e4ec983e --- /dev/null +++ b/tests/test_gui/test_gui_sidebar.py @@ -0,0 +1,53 @@ +""" +novelWriter – Side Bar Class Tester +=================================== + +This file is a part of novelWriter +Copyright (C) 2025 Veronica Berglyd Olsen and novelWriter contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" # noqa +from __future__ import annotations + +import pytest + +from novelwriter import CONFIG +from novelwriter.constants import nwLabels +from novelwriter.enum import nwTheme + + +@pytest.mark.gui +def testGuiSideBar_CycleColourTheme(nwGUI): + """Test theme cycle feature on the side bar.""" + CONFIG.themeMode = nwTheme.AUTO + sidebar = nwGUI.sideBar + sidebar.mainGui.checkThemeUpdate = lambda *a: None + + # Run 3 Cycles + for _ in range(3): + + # Cycle Light + sidebar._cycleColurTheme() + assert CONFIG.themeMode == nwTheme.LIGHT + assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.LIGHT] + + # Cycle Dark + sidebar._cycleColurTheme() + assert CONFIG.themeMode == nwTheme.DARK + assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.DARK] + + # Cycle Auto + sidebar._cycleColurTheme() + assert CONFIG.themeMode == nwTheme.AUTO + assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.AUTO] diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index a4d5d91f..19c3f277 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import time @@ -31,7 +31,7 @@ from tests.tools import C, buildTestProject @pytest.mark.gui def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd): - """Test the the various features of the status bar.""" + """Test the various features of the status bar.""" buildTestProject(nwGUI, projPath) cHandle = SHARED.project.newFile("A Note", C.hCharRoot) newDoc = SHARED.project.storage.getDocument(cHandle) diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 2deb3464..aa613bd5 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -17,347 +17,405 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations -import sys +import json +from configparser import ConfigParser from pathlib import Path +from unittest.mock import MagicMock, Mock import pytest -from PyQt6.QtGui import QColor, QIcon, QPalette, QPixmap +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QColor, QFont, QFontDatabase, QIcon, QPalette, QPixmap, QStyleHints -from novelwriter import CONFIG, SHARED -from novelwriter.common import NWConfigParser -from novelwriter.config import DEF_GUI +from novelwriter import CONFIG +from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS from novelwriter.constants import nwLabels -from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType -from novelwriter.gui.theme import _listConf +from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme +from novelwriter.gui.theme import ( + STYLES_BIG_TOOLBUTTON, STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON, GuiTheme, + ThemeMeta, _listContent +) from tests.mocked import causeOSError -from tests.tools import writeFile @pytest.mark.gui -def testGuiTheme_Main(qtbot, nwGUI, tstPaths): - """Test the theme class init.""" - mainTheme = SHARED.theme +def testGuiTheme_ParseColor(): + """Test the colour parsing.""" + theme = GuiTheme() - # Methods - # ======= + # Pre-Populate + theme._qColors["red"] = QColor(255, 0, 0) + theme._qColors["green"] = QColor(0, 255, 0) + theme._qColors["blue"] = QColor(0, 0, 255) + theme._qColors["grey"] = QColor(127, 127, 127) - mSize = mainTheme.getTextWidth("m") - assert mSize > 0 - assert mainTheme.getTextWidth("m", mainTheme.guiFont) == mSize + # By Name + assert theme.parseColor("red").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("green").getRgb() == (0, 255, 0, 255) + assert theme.parseColor("blue").getRgb() == (0, 0, 255, 255) + assert theme.parseColor("bob").getRgb() == (0, 0, 0, 255) - # Scan for Themes - # =============== + # CSS Format + assert theme.parseColor("#ff0000").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("#ff00007f").getRgb() == (255, 0, 0, 127) + assert theme.parseColor("#ff00").getRgb() == (0, 0, 0, 255) # Too short -> ignored + assert theme.parseColor("#ff00007f15").getRgb() == (0, 0, 0, 255) # Too long -> ignored - result = {} - _listConf({}, Path("not_a_path"), ".conf") - assert result == {} + # Name + Alpha + assert theme.parseColor("red:255").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("red:127").getRgb() == (255, 0, 0, 127) + assert theme.parseColor("red:512").getRgb() == (255, 0, 0, 255) # Value truncated - themeOne = tstPaths.cnfDir / "themes" / "themeone.conf" - themeTwo = tstPaths.cnfDir / "themes" / "themetwo.conf" - writeFile(themeOne, "# Stuff") - writeFile(themeTwo, "# Stuff") + # Name + Lighter + assert theme.parseColor("grey:L100").getRgb() == (127, 127, 127, 255) + assert theme.parseColor("grey:L150").getRgb() == (190, 190, 190, 255) + assert theme.parseColor("grey:L50").getRgb() == (63, 63, 63, 255) - _listConf(result, tstPaths.cnfDir / "themes", ".conf") - assert result["themeone"] == themeOne - assert result["themetwo"] == themeTwo + # Name + Darker + assert theme.parseColor("grey:D100").getRgb() == (127, 127, 127, 255) + assert theme.parseColor("grey:D150").getRgb() == (85, 85, 85, 255) + assert theme.parseColor("grey:D50").getRgb() == (254, 254, 254, 255) - # Parse Colours - # ============= - - parser = NWConfigParser() - parser["Palette"] = { - "colour1": "100, 150, 200", # Valid - "colour2": "100, 150, 200, 250", # With alpha - "colour3": "100, 150, 200, 250, 300", # Too many values - "colour4": "250, 250", # Missing blue - "colour5": "-10, 127, 300", # Invalid red and blue - "colour6": "bob, 127, 255", # Invalid red - } - - # Test the parser for several valid and invalid values - assert mainTheme._parseColor(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255) - assert mainTheme._parseColor(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250) - assert mainTheme._parseColor(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250) - assert mainTheme._parseColor(parser, "Palette", "colour4").getRgb() == (250, 250, 0, 255) - assert mainTheme._parseColor(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0) - assert mainTheme._parseColor(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255) - - # The palette should load with the parsed values - mainTheme._setPalette(parser, "Palette", "colour1", QPalette.ColorRole.Window) - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 255) - mainTheme._setPalette(parser, "Palette", "colour2", QPalette.ColorRole.Window) - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250) - mainTheme._setPalette(parser, "Palette", "colour3", QPalette.ColorRole.Window) - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250) - mainTheme._setPalette(parser, "Palette", "colour4", QPalette.ColorRole.Window) - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (250, 250, 0, 255) - mainTheme._setPalette(parser, "Palette", "colour5", QPalette.ColorRole.Window) - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 0) - mainTheme._setPalette(parser, "Palette", "colour6", QPalette.ColorRole.Window) - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 127, 255, 255) - - # Non-existing value should return default colour - mainTheme._setPalette(parser, "Palette", "stuff", QPalette.ColorRole.Window) - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 255) - - # qtbot.stop() + # Values + assert theme.parseColor("255, 0, 0").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("255, 0, 0, 255").getRgb() == (255, 0, 0, 255) + assert theme.parseColor("255, 0, 0, 127").getRgb() == (255, 0, 0, 127) + assert theme.parseColor("255, 0, 0, 127, 42").getRgb() == (255, 0, 0, 127) # Truncated @pytest.mark.gui -def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): - """Test the theme part of the class.""" - mainTheme = SHARED.theme +def testGuiTheme_ScanThemes(monkeypatch): + """Test the themes scanning.""" + theme = GuiTheme() - # List Themes + # Load built-in themes + files = [] + _listContent(files, CONFIG.assetPath("themes"), ".conf") + assert len(files) > 0 + + # Block reading theme files + with monkeypatch.context() as mp: + mp.setattr(ConfigParser, "read", causeOSError) + theme._scanThemes(files) + assert theme.colourThemes == {} + + # Read all themes correctly + theme._scanThemes(files) + assert len(theme.colourThemes) > 0 + + dark = theme.colourThemes[DEF_GUI_DARK] + light = theme.colourThemes[DEF_GUI_LIGHT] + + assert dark.name == "Default Dark Theme" + assert dark.dark is True + assert light.name == "Default Light Theme" + assert light.dark is False + + +@pytest.mark.gui +def testGuiTheme_LoadThemes(monkeypatch): + """Test loading themes.""" + theme = GuiTheme() + theme.iconCache = MagicMock() + CONFIG.lightTheme = DEF_GUI_LIGHT + CONFIG.darkTheme = DEF_GUI_DARK + + # Load built-in themes + files = [] + _listContent(files, CONFIG.assetPath("themes"), ".conf") + theme._scanThemes(files) + assert DEF_GUI_LIGHT in theme._allThemes + assert DEF_GUI_DARK in theme._allThemes + assert theme._currentTheme == "" + + # Load light theme + CONFIG.themeMode = nwTheme.LIGHT + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_LIGHT + + # Load dark theme + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_DARK + + # Let auto switch back to light, then dark + with monkeypatch.context() as mp: + mp.setattr(CONFIG, "verQtValue", 0x060500) + mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Light) + CONFIG.themeMode = nwTheme.AUTO + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_LIGHT + + with monkeypatch.context() as mp: + mp.setattr(CONFIG, "verQtValue", 0x060500) + mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Dark) + CONFIG.themeMode = nwTheme.AUTO + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_DARK + + # Error Cases # =========== - # Block the reading of the files + # Invalid light theme + CONFIG.lightTheme = "not_a_theme" + CONFIG.themeMode = nwTheme.LIGHT + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_LIGHT + + # Invalid dark theme + CONFIG.darkTheme = "not_a_theme" + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme() + assert theme._currentTheme == DEF_GUI_DARK + + # Clear meta and check exit early cases + theme._meta = ThemeMeta() + assert theme._meta.name == "" + + # Reload dark should not load anything + CONFIG.darkTheme = DEF_GUI_DARK + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme() + assert theme._meta.name == "" + + # Force reload, but fail parsing with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert mainTheme.listThemes() == [] + mp.setattr(ConfigParser, "read", causeOSError) + CONFIG.darkTheme = DEF_GUI_DARK + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme(force=True) + assert theme._meta.name == "" - # Load the theme info, default themes first - themesList = mainTheme.listThemes() - assert themesList[0] == ("default_dark", "Default Dark Theme") - assert themesList[1] == ("default_light", "Default Light Theme") - assert themesList[2] == ("cyberpunk_night", "Cyberpunk Night") - assert themesList[3] == ("dracula", "Dracula") + # Invalid theme, and defaults are missing + del theme._allThemes[DEF_GUI_DARK] + del theme._allThemes[DEF_GUI_LIGHT] - # A second call should returned the cached list - assert mainTheme.listThemes() == mainTheme._themeList + CONFIG.lightTheme = "not_a_theme" + CONFIG.themeMode = nwTheme.LIGHT + theme.loadTheme(force=True) + assert theme._meta.name == "" - # Check handling of broken theme settings - CONFIG.guiTheme = "not_a_theme" - availThemes = mainTheme._availThemes - mainTheme._availThemes = {} - assert mainTheme.loadTheme() is False - mainTheme._availThemes = availThemes + CONFIG.darkTheme = "not_a_theme" + CONFIG.themeMode = nwTheme.DARK + theme.loadTheme(force=True) + assert theme._meta.name == "" - # Check handling of unreadable file - CONFIG.guiTheme = DEF_GUI - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert mainTheme.loadTheme() is False - # Load Default Theme - # ================== +@pytest.mark.gui +def testGuiTheme_SpecialColors(tstPaths): + """Test handling special cases for colours.""" + theme = GuiTheme() + theme.iconCache = MagicMock() - if sys.platform != "win32": - # Set a mock colour for the window background - mainTheme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0) - - # Load the default theme - CONFIG.guiTheme = DEF_GUI - assert mainTheme.loadTheme() is True - - # This should load a standard palette - wCol = QPalette().color(QPalette.ColorRole.Window).getRgb() - assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == wCol - - # Mock Dark Theme - # =============== - - mockTheme: Path = tstPaths.cnfDir / "themes" / "test.conf" - mockTheme.write_text(( + testTheme: Path = tstPaths.cnfDir / "themes" / "test.conf" + testTheme.write_text(( "[Main]\n" "name = Test\n" + "mode = light\n" + "\n" + "[Base]\n" + "default = #cccccc\n" + "faded = #949494\n" + "red = #ff0000\n" + "orange = #ff7f00\n" + "yellow = #ffff00\n" + "green = #00ff00\n" + "cyan = #00ffff\n" + "blue = #0000ff\n" + "purple = #ff00ff\n" + "\n" + "[Project]\n" + "root = blue\n" + "folder = yellow\n" + "file = default\n" + "title = green\n" + "chapter = red\n" + "scene = blue\n" + "note = yellow\n" "\n" "[Palette]\n" - "window = 0, 0, 0\n" - "text = 255, 255, 255\n" + "window = #000000\n" + "text = #ffffff\n" ), encoding="utf-8") - mainTheme._availThemes["test"] = mockTheme + theme._scanThemes([testTheme]) + assert len(theme.colourThemes) == 1 + CONFIG.themeMode = nwTheme.LIGHT + CONFIG.lightTheme = "test" - CONFIG.guiTheme = "test" - assert mainTheme.loadTheme() is True - assert mainTheme._guiPalette.window().color().getRgb() == (0, 0, 0, 255) - assert mainTheme._guiPalette.text().color().getRgb() == (255, 255, 255, 255) - assert mainTheme._guiPalette.light().color().getRgb() == (57, 57, 57, 255) - assert mainTheme.isDarkTheme is True + # Load theme + theme.loadTheme() - # Load Default Light Theme - # ======================== + # Since window is black, a lighter version should be generated + assert theme._guiPalette.light().color().getRgb() == (57, 57, 57, 255) - CONFIG.guiTheme = "default_light" - assert mainTheme.loadTheme() is True + # Reload with project override to red + CONFIG.iconColTree = "red" + CONFIG.iconColDocs = True + theme.loadTheme(force=True) - # Check a few values - assert mainTheme._guiPalette.color( - QPalette.ColorRole.Window - ).getRgb() == (239, 239, 239, 255) - assert mainTheme._guiPalette.color( - QPalette.ColorRole.WindowText - ).getRgb() == (0, 0, 0, 255) - assert mainTheme._guiPalette.color( - QPalette.ColorRole.Base - ).getRgb() == (255, 255, 255, 255) - assert mainTheme._guiPalette.color( - QPalette.ColorRole.AlternateBase - ).getRgb() == (224, 224, 224, 255) + assert theme.getBaseColor("root").getRgb() == (255, 0, 0, 255) + assert theme.getBaseColor("folder").getRgb() == (255, 0, 0, 255) + assert theme.getBaseColor("file").getRgb() == (204, 204, 204, 255) + assert theme.getBaseColor("title").getRgb() == (0, 255, 0, 255) + assert theme.getBaseColor("chapter").getRgb() == (255, 0, 0, 255) + assert theme.getBaseColor("scene").getRgb() == (0, 0, 255, 255) + assert theme.getBaseColor("note").getRgb() == (255, 255, 0, 255) - # Load Default Dark Theme - # ======================= - - CONFIG.guiTheme = "default_dark" - assert mainTheme.loadTheme() is True - - # Check a few values - assert mainTheme._guiPalette.color( - QPalette.ColorRole.Window).getRgb() == (54, 54, 54, 255) - assert mainTheme._guiPalette.color( - QPalette.ColorRole.WindowText).getRgb() == (204, 204, 204, 255) - assert mainTheme._guiPalette.color( - QPalette.ColorRole.Base).getRgb() == (62, 62, 62, 255) - assert mainTheme._guiPalette.color( - QPalette.ColorRole.AlternateBase).getRgb() == (78, 78, 78, 255) - - # qtbot.stop() - - -@pytest.mark.gui -def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI): - """Test the syntax part of the class.""" - mainTheme = SHARED.theme - - # List Themes - # =========== - - # Block the reading of the files - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert mainTheme.listThemes() == [] - - # Load the syntax info - syntaxList = mainTheme.listSyntax() - assert syntaxList[0] == ("default_dark", "Default Dark") - assert syntaxList[1] == ("default_light", "Default Light") - - # A second call should returned the cached list - assert mainTheme.listSyntax() == mainTheme._syntaxList - - # Check handling of broken theme settings - availSyntax = mainTheme._availSyntax - mainTheme._availSyntax = {} - CONFIG.guiSyntax = "not_a_syntax" - assert mainTheme.loadSyntax() is False - mainTheme._availSyntax = availSyntax - - # Check handling of unreadable file - CONFIG.guiSyntax = "default_light" - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert mainTheme.loadSyntax() is False - - # Load Default Light Syntax - # ========================= - - # Load the default syntax - CONFIG.guiSyntax = "default_light" - assert mainTheme.loadSyntax() is True - - # Check some values - assert mainTheme.syntaxMeta.name == "Default Light" - assert mainTheme.syntaxTheme.back == QColor(255, 255, 255) - assert mainTheme.syntaxTheme.text == QColor(0, 0, 0) - assert mainTheme.syntaxTheme.link == QColor(0, 0, 200) - - # Load Default Dark Theme - # ======================= - - # Load the default syntax - CONFIG.guiSyntax = "default_dark" - assert mainTheme.loadSyntax() is True - - # Check some values - assert mainTheme.syntaxMeta.name == "Default Dark" - assert mainTheme.syntaxTheme.back == QColor(42, 42, 42) - assert mainTheme.syntaxTheme.text == QColor(204, 204, 204) - assert mainTheme.syntaxTheme.link == QColor(102, 153, 204) - - # qtbot.stop() - - -@pytest.mark.gui -def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths): - """Test the icon cache class.""" - iconCache = SHARED.theme.iconCache - - # Load Theme - # ========== - - # Invalid theme name - availThemes = iconCache._availThemes - iconCache._availThemes = {} - assert iconCache.loadTheme("not_a_theme") is False - iconCache._availThemes = availThemes - - # Check handling of unreadable file - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert iconCache.loadTheme("material_rounded_normal") is False - - # Load working theme file - assert iconCache.loadTheme("material_rounded_normal") is True - assert iconCache.themeMeta.name == "Material Symbols - Rounded" - - # Load with project colour override - purple = iconCache._svgColors["purple"] - assert iconCache._svgColors["root"] != purple - assert iconCache._svgColors["folder"] != purple - assert iconCache._svgColors["file"] != purple - assert iconCache._svgColors["title"] != purple - assert iconCache._svgColors["chapter"] != purple - assert iconCache._svgColors["scene"] != purple - assert iconCache._svgColors["note"] != purple + assert theme.getRawBaseColor("root") == b"#ff0000" + assert theme.getRawBaseColor("folder") == b"#ff0000" + assert theme.getRawBaseColor("file") == b"#cccccc" + assert theme.getRawBaseColor("title") == b"#00ff00" + assert theme.getRawBaseColor("chapter") == b"#ff0000" + assert theme.getRawBaseColor("scene") == b"#0000ff" + assert theme.getRawBaseColor("note") == b"#ffff00" + # Reload with project override to purple, also for docs CONFIG.iconColTree = "purple" - assert iconCache.loadTheme("material_rounded_normal") is True - assert iconCache._svgColors["root"] == purple - assert iconCache._svgColors["folder"] == purple - assert iconCache._svgColors["file"] == purple - assert iconCache._svgColors["title"] == purple - assert iconCache._svgColors["chapter"] == purple - assert iconCache._svgColors["scene"] == purple - assert iconCache._svgColors["note"] == purple + CONFIG.iconColDocs = False + theme.loadTheme(force=True) - # Change some colours - iconCache.setIconColor("root", QColor(255, 255, 255)) - assert iconCache._svgColors["root"] != purple - assert iconCache._svgColors["root"] == b"#ffffff" + assert theme.getBaseColor("root").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("folder").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("file").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("title").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("chapter").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("scene").getRgb() == (255, 0, 255, 255) + assert theme.getBaseColor("note").getRgb() == (255, 0, 255, 255) - # List Themes - # =========== - - # Load error returns empty list - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - themes = iconCache.listThemes() - assert themes == [] - - # Successful read - themes = iconCache.listThemes() - assert len(themes) > 1 - assert "material_rounded_normal" in dict(themes) - - # Load error doesn't matter on second read since list is cached - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert iconCache.listThemes() == themes - - # qtbot.stop() + assert theme.getRawBaseColor("root") == b"#ff00ff" + assert theme.getRawBaseColor("folder") == b"#ff00ff" + assert theme.getRawBaseColor("file") == b"#ff00ff" + assert theme.getRawBaseColor("title") == b"#ff00ff" + assert theme.getRawBaseColor("chapter") == b"#ff00ff" + assert theme.getRawBaseColor("scene") == b"#ff00ff" + assert theme.getRawBaseColor("note") == b"#ff00ff" @pytest.mark.gui -def testGuiTheme_LoadIcons(qtbot, nwGUI): +def testGuiTheme_Methods(monkeypatch): + """Test other themes methods.""" + theme = GuiTheme() + theme.iconCache = MagicMock() + CONFIG.darkTheme = DEF_GUI_DARK + CONFIG.themeMode = nwTheme.DARK + + # Init theme + assert theme.colourThemes == {} + theme.initThemes() + assert theme._meta.name == "Default Dark Theme" + + # Text width + theme.guiFont = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont) + assert theme.getTextWidth("MMMMM") > theme.getTextWidth("MMM") + font = QFont(theme.guiFont) + font.setPointSizeF(0.5*font.pointSizeF()) + assert theme.getTextWidth("MMMMM", font) < theme.getTextWidth("MMMMM") + + # Detect desktop mode Qt 6.5+ + with monkeypatch.context() as mp: + mp.setattr(CONFIG, "verQtValue", 0x060500) + + mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Light) + assert theme.isDesktopDarkMode() is False + + mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Dark) + assert theme.isDesktopDarkMode() is True + + # Detect desktop mode Qt 6.4 + mockWhite = Mock() + mockWhite.color.return_value = QColor(255, 255, 255) + + mockBlack = Mock() + mockBlack.color.return_value = QColor(0, 0, 0) + + with monkeypatch.context() as mp: + mp.setattr(CONFIG, "verQtValue", 0x060400) + + mp.setattr(QPalette, "window", lambda *a: mockWhite) + mp.setattr(QPalette, "windowText", lambda *a: mockBlack) + assert theme.isDesktopDarkMode() is False + + mp.setattr(QPalette, "window", lambda *a: mockBlack) + mp.setattr(QPalette, "windowText", lambda *a: mockWhite) + assert theme.isDesktopDarkMode() is True + + # Stylesheets + assert theme.getStyleSheet(STYLES_FLAT_TABS) != "" + assert theme.getStyleSheet(STYLES_MIN_TOOLBUTTON) != "" + assert theme.getStyleSheet(STYLES_BIG_TOOLBUTTON) != "" + assert theme.getStyleSheet("stuff") == "" + + +@pytest.mark.gui +def testGuiTheme_ScanIcons(monkeypatch): + """Test the icon theme scanning.""" + theme = GuiTheme() + + # Load built-in themes + files = [] + _listContent(files, CONFIG.assetPath("icons"), ".icons") + assert len(files) > 0 + + # Block reading theme files + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + theme.iconCache._scanThemes(files) + assert theme.iconCache.iconThemes == {} + + # Read all themes correctly + theme.iconCache._scanThemes(files) + assert len(theme.iconCache.iconThemes) > 0 + + +@pytest.mark.gui +def testGuiTheme_IconThemes(monkeypatch): + """Test loading icon theme.""" + CONFIG.lightTheme = DEF_GUI_LIGHT + CONFIG.themeMode = nwTheme.LIGHT + CONFIG.iconTheme = DEF_ICONS + + theme = GuiTheme() + + # Init should load default theme + theme.initThemes() + assert theme.iconCache._meta.name == "Material Symbols - Rounded" + assert DEF_ICONS in theme.iconCache.iconThemes + + # Load default theme directly + theme.iconCache._meta = ThemeMeta() + theme.iconCache.loadTheme("DEF_ICONS") + assert theme.iconCache._meta.name == "Material Symbols - Rounded" + + # Failed loading should load nothing + theme.iconCache._meta = ThemeMeta() + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + theme.iconCache.loadTheme("DEF_ICONS") + assert theme.iconCache._meta.name == "" + + # Reload with non-existent theme should reload default + theme.iconCache._meta = ThemeMeta() + theme.iconCache.loadTheme("not_a_theme") + assert theme.iconCache._meta.name == "Material Symbols - Rounded" + + # If default theme is missing, load nothing + del theme.iconCache._allThemes[DEF_ICONS] + theme.iconCache._meta = ThemeMeta() + theme.iconCache.loadTheme("not_a_theme") + assert theme.iconCache._meta.name == "" + + +@pytest.mark.gui +def testGuiTheme_LoadIcons(): """Test the icon cache class.""" - iconCache = SHARED.theme.iconCache - assert iconCache.loadTheme("material_rounded_normal") is True + theme = GuiTheme() + theme.initThemes() + iconCache = theme.iconCache # Load Icons # ========== @@ -373,11 +431,13 @@ def testGuiTheme_LoadIcons(qtbot, nwGUI): 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)) assert isinstance(qPix, QPixmap) assert qPix.isNull() is False - assert qPix.width() == 50 - assert qPix.height() == 50 + 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") @@ -455,14 +515,13 @@ def testGuiTheme_LoadIcons(qtbot, nwGUI): nwItemType.NO_TYPE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H0" ) == iconCache._noIcon - # qtbot.stop() - @pytest.mark.gui -def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI): +def testGuiTheme_LoadDecorations(monkeypatch): """Test the icon cache class.""" - iconCache = SHARED.theme.iconCache - assert iconCache.loadTheme("material_rounded_normal") is True + theme = GuiTheme() + theme.initThemes() + iconCache = theme.iconCache # Load Decorations # ================ @@ -520,4 +579,114 @@ def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI): assert iconCache.getHeaderDecorationNarrow(5) == iconCache._headerDecNarrow[5] assert iconCache.getHeaderDecorationNarrow(6) == iconCache._headerDecNarrow[5] - # qtbot.stop() + +THEMES = [] +_listContent(THEMES, CONFIG.assetPath("themes"), ".conf") + + +@pytest.mark.gui +@pytest.mark.parametrize("theme", [a.stem for a in THEMES]) +def testGuiTheme_CheckTheme(theme): + """Test loading all themes.""" + themes = GuiTheme() + themes.iconCache = MagicMock() + themes._scanThemes(THEMES) + + assert theme in themes.colourThemes + current = themes.colourThemes[theme] + if current.dark: + CONFIG.darkTheme = theme + CONFIG.themeMode = nwTheme.DARK + else: + CONFIG.lightTheme = theme + CONFIG.themeMode = nwTheme.LIGHT + + # Check loading + themes.loadTheme() + assert themes._meta.name == current.name + assert themes.isDarkTheme == current.dark + + # Check completeness + parser = ConfigParser() + parser.read(current.path, encoding="utf-8") + + sections = ["Main", "Base", "Project", "Palette", "GUI", "Syntax"] + assert sorted(parser.sections()) == sorted(sections) + + structure = { + "Main": [ + "name", "mode", "author", # The rest are not required + ], + "Base": [ + "base", "default", "faded", "red", "orange", "yellow", "green", + "cyan", "blue", "purple", + ], + "Project": [ + "root", "folder", "file", "title", "chapter", "scene", "note", + "active", "inactive", "disabled", + ], + "Palette": [ + "window", "windowtext", "base", "alternatebase", "text", + "tooltipbase", "tooltiptext", "button", "buttontext", "brighttext", + "highlight", "highlightedtext", "link", "linkvisited", "accent", + ], + "GUI": [ + "helptext", "fadedtext", "errortext", + ], + "Syntax": [ + "background", "text", "line", "link", "headertext", "headertag", + "emphasis", "dialog", "altdialog", "hidden", "note", "shortcode", + "keyword", "tag", "value", "optional", "spellcheckline", + "errorline", "replacetag", "modifier", "texthighlight", + ], + } + optional = ["credit", "url"] + missing = [] + for section, options in structure.items(): + missing.extend(opt for opt in options if opt not in parser[section]) + assert missing == [], "Missing options in theme file" + + # Check deprecated + deprecated = [] + for section in sections: + deprecated.extend( + opt for opt in parser[section] + if opt not in structure[section] and opt not in optional + ) + assert deprecated == [], "Deprecated options in theme file" + + +ICONS = [] +_listContent(ICONS, CONFIG.assetPath("icons"), ".icons") + + +@pytest.mark.gui +@pytest.mark.parametrize("icons", [a.stem for a in ICONS]) +def testGuiTheme_CheckIcons(icons, tstPaths): + """Test loading all icons.""" + keysFile: Path = tstPaths.filesDir / "all_icons.json" + iconKeys = json.loads(keysFile.read_text(encoding="utf-8")) + assert isinstance(iconKeys, list) + + CONFIG.lightTheme = DEF_GUI_LIGHT + CONFIG.themeMode = nwTheme.LIGHT + + themes = GuiTheme() + themes.initThemes() + iconCache = themes.iconCache + + assert icons in iconCache.iconThemes + current = iconCache.iconThemes[icons] + CONFIG.iconTheme = icons + + # Check loading + themes.loadTheme(force=True) + assert iconCache._meta.name == current.name + + # Check completeness + missing = [key for key in iconKeys if key not in iconCache._svgData] + assert missing == [], "Missing keys in icons file" + + # Check deprecated + deprecated = [key for key in iconCache._svgData if key not in iconKeys] + assert deprecated == [], "Deprecated keys in icons file" diff --git a/tests/test_text/test_text_comments.py b/tests/test_text/test_text_comments.py index 30be4e37..9b42294e 100644 --- a/tests/test_text/test_text_comments.py +++ b/tests/test_text/test_text_comments.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import pytest diff --git a/tests/test_text/test_text_counting.py b/tests/test_text/test_text_counting.py index 8accab95..d7661e18 100644 --- a/tests/test_text/test_text_counting.py +++ b/tests/test_text/test_text_counting.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import pytest diff --git a/tests/test_text/test_text_patterns.py b/tests/test_text/test_text_patterns.py index e6a8b599..1325b1d6 100644 --- a/tests/test_text/test_text_patterns.py +++ b/tests/test_text/test_text_patterns.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import re @@ -157,6 +157,15 @@ def testTextPatterns_Markdown(): assert allMatches(regEx, "one ~two~~ three") == [] assert allMatches(regEx, "one~~two~~three") == [] + # Mark + regEx = REGEX_PATTERNS.markdownMark + assert allMatches(regEx, "one ==two== three") == [ + [("==two==", 4, 11), ("==", 4, 6), ("two", 6, 9), ("==", 9, 11)] + ] + assert allMatches(regEx, "one ==two= three") == [] + assert allMatches(regEx, "one =two== three") == [] + assert allMatches(regEx, "one==two==three") == [] + @pytest.mark.core def testTextPatterns_ShortcodesPlain(): diff --git a/tests/test_tools/test_tools_dictionaries.py b/tests/test_tools/test_tools_dictionaries.py index e8a23c7b..b22ec074 100644 --- a/tests/test_tools/test_tools_dictionaries.py +++ b/tests/test_tools/test_tools_dictionaries.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from zipfile import ZipFile diff --git a/tests/test_tools/test_tools_lipsum.py b/tests/test_tools/test_tools_lipsum.py index 27098b5c..b73a78f3 100644 --- a/tests/test_tools/test_tools_lipsum.py +++ b/tests/test_tools/test_tools_lipsum.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import pytest diff --git a/tests/test_tools/test_tools_manusbuild.py b/tests/test_tools/test_tools_manusbuild.py index 50d04391..91924120 100644 --- a/tests/test_tools/test_tools_manusbuild.py +++ b/tests/test_tools/test_tools_manusbuild.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from pathlib import Path @@ -143,7 +143,6 @@ def testToolManuscriptBuild_Main( def mockOpenUrl(url: QUrl) -> None: nonlocal lastUrl lastUrl = url.toString() - return with monkeypatch.context() as mp: mp.setattr(QDesktopServices, "openUrl", mockOpenUrl) diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py index 070b5db6..0e498ea1 100644 --- a/tests/test_tools/test_tools_manuscript.py +++ b/tests/test_tools/test_tools_manuscript.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import sys diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py index 91a9f4f9..b5ce7880 100644 --- a/tests/test_tools/test_tools_manussettings.py +++ b/tests/test_tools/test_tools_manussettings.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import pytest diff --git a/tests/test_tools/test_tools_noveldetails.py b/tests/test_tools/test_tools_noveldetails.py index 067cbc08..64428cdf 100644 --- a/tests/test_tools/test_tools_noveldetails.py +++ b/tests/test_tools/test_tools_noveldetails.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import pytest diff --git a/tests/test_tools/test_tools_welcome.py b/tests/test_tools/test_tools_welcome.py index 42157b2d..846e3900 100644 --- a/tests/test_tools/test_tools_welcome.py +++ b/tests/test_tools/test_tools_welcome.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations from datetime import datetime diff --git a/tests/test_tools/test_tools_writingstats.py b/tests/test_tools/test_tools_writingstats.py index 0e2bc4ee..1992096a 100644 --- a/tests/test_tools/test_tools_writingstats.py +++ b/tests/test_tools/test_tools_writingstats.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import json diff --git a/tests/tools.py b/tests/tools.py index 7acc598f..9eec20d0 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import shutil @@ -124,7 +124,7 @@ def xmlToText(xElem): def readFile(fileName: str | Path): - """Returns the content of a file as a string.""" + """Return the content of a file as a string.""" with open(fileName, mode="r", encoding="utf-8") as inFile: return inFile.read() @@ -154,8 +154,6 @@ def cleanProject(path: str | Path): if tocFile.is_file(): tocFile.unlink() - return - def clearLogHandlers(): """Clear all log handlers.""" @@ -235,7 +233,6 @@ class SimpleDialog(QDialog): self.setLayout(layout) if widget: layout.addWidget(widget) - return @property def widget(self) -> QWidget | None: @@ -246,4 +243,3 @@ class SimpleDialog(QDialog): layout = self.layout() assert layout is not None layout.addWidget(widget) - return diff --git a/utils/assets.py b/utils/assets.py index 1849c696..7f0222cd 100644 --- a/utils/assets.py +++ b/utils/assets.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import argparse @@ -62,8 +62,6 @@ def buildSampleZip(args: argparse.Namespace | None = None) -> None: print(f"Built file: {dstSample}") print("") - return - def importI18nUpdates(args: argparse.Namespace) -> None: """Import new translation files from a zip file.""" @@ -94,8 +92,6 @@ def importI18nUpdates(args: argparse.Namespace) -> None: print("") - return - def updateTranslationSources(args: argparse.Namespace) -> None: """Build the lang.ts files for Qt Linguist.""" @@ -157,8 +153,6 @@ def updateTranslationSources(args: argparse.Namespace) -> None: print("") - return - def buildTranslationAssets(args: argparse.Namespace | None = None) -> None: """Build the lang.qm files for Qt Linguist.""" @@ -203,8 +197,6 @@ def buildTranslationAssets(args: argparse.Namespace | None = None) -> None: print("") - return - def cleanBuiltAssets(args: argparse.Namespace | None = None) -> None: """Remove assets built by this script.""" @@ -223,8 +215,6 @@ def cleanBuiltAssets(args: argparse.Namespace | None = None) -> None: print("") - return - def buildAllAssets(args: argparse.Namespace) -> None: """Build all assets.""" @@ -232,4 +222,3 @@ def buildAllAssets(args: argparse.Namespace) -> None: buildSampleZip() buildTranslationAssets() buildPdfDocAssets() - return diff --git a/utils/build_appimage.py b/utils/build_appimage.py index d32a377e..ff52cd3f 100644 --- a/utils/build_appimage.py +++ b/utils/build_appimage.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import argparse @@ -134,5 +134,3 @@ def appImage(args: argparse.Namespace) -> None: toUpload(bldFile) toUpload(updFile) toUpload(shaFile) - - return diff --git a/utils/build_binary.py b/utils/build_binary.py index 1ffa4797..e58321e5 100644 --- a/utils/build_binary.py +++ b/utils/build_binary.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import argparse @@ -25,7 +25,7 @@ import argparse def runPyinstaller() -> None: """Run the pyinstaller.""" - import PyInstaller.__main__ + import PyInstaller.__main__ # type: ignore build = ["novelWriter.py", "--clean", "--windowed", "--onedir", "--noconfirm"] build += ["--name", "novelwriter"] @@ -35,10 +35,7 @@ def runPyinstaller() -> None: build += ["--add-data", "novelwriter/assets:assets"] PyInstaller.__main__.run(build) - return - def main(args: argparse.Namespace) -> None: """Entry point function.""" runPyinstaller() - return diff --git a/utils/build_debian.py b/utils/build_debian.py index a8cf9a05..9ac0bd2e 100644 --- a/utils/build_debian.py +++ b/utils/build_debian.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import argparse @@ -155,17 +155,16 @@ def makeDebianPackage( def debian(args: argparse.Namespace) -> None: - """Build a .deb package""" + """Build a .deb package.""" if sys.platform != "linux": print("ERROR: Command 'build-deb' can only be used on Linux") sys.exit(1) signKey = SIGN_KEY if args.sign else None makeDebianPackage(signKey) - return def launchpad(args: argparse.Namespace) -> None: - """Wrapper for building Debian packages for Launchpad.""" + """Build Debian packages for Launchpad.""" if sys.platform != "linux": print("ERROR: Command 'build-ubuntu' can only be used on Linux") sys.exit(1) @@ -216,5 +215,3 @@ def launchpad(args: argparse.Namespace) -> None: for dCmd in dputCmd: print(f" > {dCmd}") print("") - - return diff --git a/utils/build_windows.py b/utils/build_windows.py index 76fdc281..523797da 100644 --- a/utils/build_windows.py +++ b/utils/build_windows.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import argparse @@ -60,8 +60,6 @@ def prepareCode(outDir: Path) -> None: print("Done") print("") - return - def embedPython(bldDir: Path, outDir: Path) -> None: """Embed Python library.""" @@ -82,8 +80,6 @@ def embedPython(bldDir: Path, outDir: Path) -> None: print("Done") print("") - return - def installRequirements(libDir: Path) -> None: """Install dependencies.""" @@ -93,7 +89,6 @@ def installRequirements(libDir: Path) -> None: ]) print("Done") print("") - return def main(args: argparse.Namespace) -> None: @@ -157,5 +152,3 @@ def main(args: argparse.Namespace) -> None: print("") print("Done") print("") - - return diff --git a/utils/common.py b/utils/common.py index 09d937b1..54528ba2 100644 --- a/utils/common.py +++ b/utils/common.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import shutil @@ -88,7 +88,6 @@ def copySourceCode(dst: Path) -> None: if item.is_file(): shutil.copyfile(item, dst / relSrc) print("Copied:", relSrc, flush=True) - return def copyPackageFiles(dst: Path, oldLicense: bool = False) -> None: @@ -118,8 +117,6 @@ def copyPackageFiles(dst: Path, oldLicense: bool = False) -> None: text = "\n".join(new) writeFile(dst / "pyproject.toml", text) - return - def toUpload(srcPath: str | Path, dstName: str | None = None) -> None: """Copy a file produced by one of the build functions to the upload @@ -129,7 +126,6 @@ def toUpload(srcPath: str | Path, dstName: str | None = None) -> None: uplDir.mkdir(exist_ok=True) srcPath = Path(srcPath) shutil.copyfile(srcPath, uplDir / (dstName or srcPath.name)) - return def makeCheckSum(sumFile: str, cwd: Path | None = None) -> str: @@ -178,9 +174,7 @@ def appdataXml() -> str: """Generate the appdata XML content.""" raw = readFile(SETUP_DIR / "description_short.txt") desc = " ".join(raw.strip().splitlines()).strip() - xml = readFile(SETUP_DIR / "novelwriter.appdata.xml") - xml = xml.format(description=desc) - return xml + return readFile(SETUP_DIR / "novelwriter.appdata.xml").format(description=desc) def readFile(file: Path) -> str: @@ -201,7 +195,6 @@ def freshFolder(path: Path) -> None: print("Removing:", str(path), flush=True) shutil.rmtree(path) path.mkdir() - return def systemCall(cmd: list, cwd: Path | str | None = None, env: dict | None = None) -> int: @@ -217,7 +210,7 @@ def systemCall(cmd: list, cwd: Path | str | None = None, env: dict | None = None def removeRedundantQt(qtBase: Path) -> None: - """Delete Qt files that are not needed""" + """Delete Qt files that are not needed.""" def unlinkIfFound(file: Path) -> None: if file.is_file(): @@ -278,5 +271,3 @@ def removeRedundantQt(qtBase: Path) -> None: deleteFolder(qt6Dir / "qml") deleteFolder(plugDir / "qmlls") deleteFolder(plugDir / "qmllint") - - return diff --git a/utils/docs.py b/utils/docs.py index 7a4de94b..856abcfb 100644 --- a/utils/docs.py +++ b/utils/docs.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import argparse @@ -61,12 +61,9 @@ def updateDocsTranslationSources(args: argparse.Namespace) -> None: print("Done") print("") - return - def buildHtmlDocs(args: argparse.Namespace | None = None) -> None: """Build the documentation files.""" - print("") print("Building HTML Docs") print("==================") @@ -105,12 +102,9 @@ def buildHtmlDocs(args: argparse.Namespace | None = None) -> None: print("") - return - def buildPdfDocAssets(args: argparse.Namespace | None = None) -> None: """Build the documentation PDF files.""" - print("") print("Building Docs Manuals") print("=====================") @@ -146,5 +140,3 @@ def buildPdfDocAssets(args: argparse.Namespace | None = None) -> None: raise Exception(f"Build returned error code {ex}") print("") - - return diff --git a/utils/icon_themes.py b/utils/icon_themes.py index b9ddf16e..cb906b01 100644 --- a/utils/icon_themes.py +++ b/utils/icon_themes.py @@ -17,7 +17,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -""" +""" # noqa from __future__ import annotations import argparse @@ -45,6 +45,7 @@ ICON_EXTRACT = { "remix": "RemixIcon-4.6.0", } ICONS = [ + # Remember to also update tests/files/all_icons.json for test coverage "alert_error", "alert_info", "alert_question", @@ -102,6 +103,10 @@ ICONS = [ "sb_search", "sb_stats", + "theme_light", + "theme_dark", + "theme_auto", + "add", "bookmarks", "browse", @@ -192,7 +197,6 @@ def _writeThemeFile( out.write(f"icon:{key:<15s} = {_fixXml(svg)}\n") print(f"- Wrote: {len(icons)} icons") print(f"- Target: {path.relative_to(UTILS.parent)}") - return def _updateRepo(path: Path, name: str) -> None: @@ -203,7 +207,6 @@ def _updateRepo(path: Path, name: str) -> None: else: subprocess.call(["git", "pull"], cwd=path) print("") - return def _downloadIconPack(path: Path, name: str) -> None: @@ -215,7 +218,6 @@ def _downloadIconPack(path: Path, name: str) -> None: with zipfile.ZipFile(zipFile, "r") as inFile: inFile.extractall(path) print("") - return def processMaterialIcons(workDir: Path, iconsDir: Path, jobs: dict) -> None: @@ -252,8 +254,6 @@ def processMaterialIcons(workDir: Path, iconsDir: Path, jobs: dict) -> None: print("") - return - def processFontAwesome(workDir: Path, iconsDir: Path, jobs: dict) -> None: """Process Font Awesome icons of a given spec and write output file.""" @@ -298,8 +298,6 @@ def processFontAwesome(workDir: Path, iconsDir: Path, jobs: dict) -> None: print("") - return - def processRemix(workDir: Path, iconsDir: Path, jobs: dict) -> None: """Process Remix icons of a given spec and write output file.""" @@ -342,8 +340,6 @@ def processRemix(workDir: Path, iconsDir: Path, jobs: dict) -> None: print("") - return - def main(args: argparse.Namespace) -> None: """Build icon themes entry point.""" @@ -418,5 +414,3 @@ def main(args: argparse.Namespace) -> None: print("Done") print("") - - return diff --git a/utils/icon_themes/font_awesome.json b/utils/icon_themes/font_awesome.json index 5ed09376..d98dc9e2 100644 --- a/utils/icon_themes/font_awesome.json +++ b/utils/icon_themes/font_awesome.json @@ -56,6 +56,10 @@ "sb_search": "magnifying-glass", "sb_stats": "chart-simple", + "theme_light": "sun", + "theme_dark": "moon", + "theme_auto": "circle-half-stroke", + "add": "plus", "bookmarks": "bookmark", "browse": "folder-open", diff --git a/utils/icon_themes/material_symbols.json b/utils/icon_themes/material_symbols.json index 230f5c60..61c1975a 100644 --- a/utils/icon_themes/material_symbols.json +++ b/utils/icon_themes/material_symbols.json @@ -56,6 +56,10 @@ "sb_search": "search", "sb_stats": "bar_chart", + "theme_light": "light_mode", + "theme_dark": "dark_mode", + "theme_auto": "contrast", + "add": "add", "bookmarks": "bookmarks", "browse": "folder_open", diff --git a/utils/icon_themes/remix.json b/utils/icon_themes/remix.json index 300b8bbe..0ac173ec 100644 --- a/utils/icon_themes/remix.json +++ b/utils/icon_themes/remix.json @@ -56,6 +56,10 @@ "sb_search": "file-search", "sb_stats": "bar-chart-fill", + "theme_light": "sun", + "theme_dark": "moon", + "theme_auto": "contrast", + "add": "add", "bookmarks": "bookmark", "browse": "folder-2",