diff --git a/CREDITS.md b/CREDITS.md
index 9ac3b34a..b3c527a5 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -62,8 +62,8 @@ Some of the assets bundled with novelWriter were adapted from the following sour
* **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)
+* **Tomorrow** themes by Chris Kempson (MIT License)
+* **Owl** 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)
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..8f3d2f60 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,123 @@ 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
+ name = Default Light Theme
+ mode = light
+ 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/
- [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]
+ default = #303030
+ faded = #6c6c6c
+ red = #a62a2d
+ orange = #b36829
+ yellow = #a68542
+ 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
[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 = #efefef
+ windowtext = #000000
+ base = #ffffff
+ alternatebase = #e0e0e0
+ text = #000000
+ tooltipbase = #ffffc0
+ tooltiptext = #15150d
+ button = #efefef
+ buttontext = #000000
+ brighttext = #ffffff
+ highlight = #3087c6
+ highlightedtext = #ffffff
+ link = blue
+ linkvisited = blue
[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 = #ffffff
+ text = #000000
+ link = blue
+ headertext = green
+ headertag = green:160
+ emphasis = orange
+ dialog = blue
+ altdialog = blue
+ note = yellow
+ hidden = faded
+ shortcode = green
+ keyword = red
+ tag = green
+ value = blue
+ optional = green
+ spellcheckline = red
+ errorline = green
+ replacetag = green
+ modifier = green
+ texthighlight = #c8c80060
-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`` and ``mode``, 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"
+ :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:alpha``", "A name referring to one of the colours already specified under the ``[Base]`` section, with a transparency value added. The alpha value must be in the range ``0`` to ``255``, like ``red:127`` for half-transparent red."
+ "``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 +165,84 @@ 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]``.
-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/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/novelwriter/__init__.py b/novelwriter/__init__.py
index 4905f4b3..acc73ee6 100644
--- a/novelwriter/__init__.py
+++ b/novelwriter/__init__.py
@@ -49,7 +49,7 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
-__version__ = "2.8 Alpha 0"
+__version__ = "2.8a0"
__hexversion__ = "0x020800a0"
__date__ = "2025-06-01"
__status__ = "Stable"
diff --git a/novelwriter/assets/icons/font_awesome.icons b/novelwriter/assets/icons/font_awesome.icons
index 550fd857..75204942 100644
--- a/novelwriter/assets/icons/font_awesome.icons
+++ b/novelwriter/assets/icons/font_awesome.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_bold.icons b/novelwriter/assets/icons/material_filled_bold.icons
index bc902698..584a04bb 100644
--- a/novelwriter/assets/icons/material_filled_bold.icons
+++ b/novelwriter/assets/icons/material_filled_bold.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_normal.icons b/novelwriter/assets/icons/material_filled_normal.icons
index ce666d14..a6d7933e 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_bold.icons b/novelwriter/assets/icons/material_rounded_bold.icons
index 08564be2..02d83821 100644
--- a/novelwriter/assets/icons/material_rounded_bold.icons
+++ b/novelwriter/assets/icons/material_rounded_bold.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 3da2739e..06887aa7 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/remix_filled.icons b/novelwriter/assets/icons/remix_filled.icons
index b2305187..51e73a78 100644
--- a/novelwriter/assets/icons/remix_filled.icons
+++ b/novelwriter/assets/icons/remix_filled.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/remix_outline.icons b/novelwriter/assets/icons/remix_outline.icons
index 0fae7c91..02ce0243 100644
--- a/novelwriter/assets/icons/remix_outline.icons
+++ b/novelwriter/assets/icons/remix_outline.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/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..33903616 100644
--- a/novelwriter/assets/text/credits_en.htm
+++ b/novelwriter/assets/text/credits_en.htm
@@ -75,8 +75,8 @@ more contributions are listed on the project's Members page.
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)
+ Tomorrow themes by Chris Kempson (MIT License)
+ Owl 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)
diff --git a/novelwriter/assets/themes/cyberpunk_night.conf b/novelwriter/assets/themes/cyberpunk_night.conf
index 125cab45..40b5dd53 100644
--- a/novelwriter/assets/themes/cyberpunk_night.conf
+++ b/novelwriter/assets/themes/cyberpunk_night.conf
@@ -1,48 +1,71 @@
[Main]
name = Cyberpunk Night
+mode = dark
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/
-[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]
+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 = #4d4dff
+folder = #ffff00
+file = #888888
+title = #00ff00
+chapter = #f24817
+scene = #4d4dff
+note = #ffff00
[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 = #000000
+windowtext = #969696
+base = #000000
+alternatebase = #282828
+text = #969696
+tooltipbase = #281446
+tooltiptext = #ffffff
+button = #05000a
+buttontext = #969696
+brighttext = #ffffff
+highlight = #321e50
+highlightedtext = #ffffff
+link = #4d4dff
+linkvisited = #320050
[GUI]
-helptext = 97, 97, 97
-fadedtext = 97, 97, 97
-errortext = 242, 72, 23
+helptext = #616161
+fadedtext = #616161
+errortext = #f24817
+
+[Syntax]
+background = #000000
+text = #969696
+link = #4d4dff
+headertext = #ffffff
+headertag = #320064
+emphasis = #00ffff
+dialog = #00ff00
+altdialog = #008cff
+note = #969696
+hidden = #4d4d64
+shortcode = #ffff00
+keyword = #ff64ff
+tag = #ff960a
+value = #ff960a
+spellcheckline = #f24817
+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..e559de9e 100644
--- a/novelwriter/assets/themes/default_dark.conf
+++ b/novelwriter/assets/themes/default_dark.conf
@@ -1,5 +1,6 @@
[Main]
name = Default Dark Theme
+mode = dark
description = The novelWriter standard dark theme
author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen
@@ -7,43 +8,65 @@ url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
-[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]
+default = #cccccc
+faded = #949494
+red = #f2777a
+orange = #f99139
+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
[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 = #363636
+windowtext = default
+base = #3e3e3e
+alternatebase = #4e4e4e
+text = default
+tooltipbase = #ffffc0
+tooltiptext = #15150d
+button = #3e3e3e
+buttontext = default
+brighttext = #3e3e3e
+highlight = #2c98f7
+highlightedtext = #ffffff
+link = blue
+linkvisited = blue
[GUI]
-helptext = 164, 164, 164
-fadedtext = 148, 148, 148
-errortext = 255, 164, 164
+helptext = #a4a4a4
+fadedtext = faded
+errortext = red
+
+[Syntax]
+background = #363636
+text = default
+link = blue
+headertext = green
+headertag = green:160
+emphasis = orange
+dialog = blue
+altdialog = blue
+note = yellow
+hidden = faded
+shortcode = green
+keyword = red
+tag = green
+value = blue
+optional = green
+spellcheckline = red
+errorline = green
+replacetag = green
+modifier = green
+texthighlight = yellow:72
diff --git a/novelwriter/assets/themes/default_light.conf b/novelwriter/assets/themes/default_light.conf
index 29b4b98a..638d6879 100644
--- a/novelwriter/assets/themes/default_light.conf
+++ b/novelwriter/assets/themes/default_light.conf
@@ -1,5 +1,6 @@
[Main]
name = Default Light Theme
+mode = light
description = The novelWriter standard light theme
author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen
@@ -7,43 +8,65 @@ url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
-[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]
+default = #303030
+faded = #6c6c6c
+red = #a62a2d
+orange = #b36829
+yellow = #a68542
+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
[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 = #efefef
+windowtext = #000000
+base = #ffffff
+alternatebase = #e0e0e0
+text = #000000
+tooltipbase = #ffffc0
+tooltiptext = #15150d
+button = #efefef
+buttontext = #000000
+brighttext = #ffffff
+highlight = #3087c6
+highlightedtext = #ffffff
+link = blue
+linkvisited = blue
[GUI]
-helptext = 92, 92, 92
-fadedtext = 108, 108, 108
-errortext = 255, 92, 92
+helptext = #5c5c5c
+fadedtext = #6c6c6c
+errortext = red
+
+[Syntax]
+background = #ffffff
+text = #000000
+link = blue
+headertext = green
+headertag = green:160
+emphasis = orange
+dialog = blue
+altdialog = blue
+note = yellow
+hidden = faded
+shortcode = green
+keyword = red
+tag = green
+value = blue
+optional = green
+spellcheckline = red
+errorline = green
+replacetag = green
+modifier = green
+texthighlight = #c8c80060
diff --git a/novelwriter/assets/themes/dracula.conf b/novelwriter/assets/themes/dracula.conf
index 5805b84b..04e32450 100644
--- a/novelwriter/assets/themes/dracula.conf
+++ b/novelwriter/assets/themes/dracula.conf
@@ -1,5 +1,6 @@
[Main]
name = Dracula
+mode = dark
description = A dark theme with bright colours
author = Veronica Berglyd Olsen (adaptation)
credit = Zeno Rocha
@@ -9,9 +10,10 @@ licenseurl = https://github.com/dracula/dracula-theme/blob/main/LICENSE
##
# 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 +24,65 @@ 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]
+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 = #bd93f9
+folder = #f1fa8c
+file = #e6e6e0
+title = #50fa7b
+chapter = #ff5555
+scene = #8be9fd
+note = #f1fa8c
[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 = #282a36
+alternatebase = #333645
+text = #f8f8f2
+tooltipbase = #f1fa8c
+tooltiptext = #282a36
+button = #505369
+buttontext = #f8f8f2
+brighttext = #44475a
+highlight = #a681da
+highlightedtext = #f8f8f2
+link = #8be9fd
+linkvisited = #8be9fd
[GUI]
-helptext = 204, 172, 249
-fadedtext = 98, 114, 164
-errortext = 255, 85, 85
+helptext = #ccacf9
+fadedtext = #6272a4
+errortext = #ff5555
+
+[Syntax]
+background = #282a36
+text = #f8f8f2
+link = #ff79c6
+headertext = #bd93f9
+headertag = #bd93f9a0
+emphasis = #ffb86c
+dialog = #50fa7b
+altdialog = #f1fa8c
+note = #ffcce9
+hidden = #6272a4
+shortcode = #8be9fd
+keyword = #ff79c6
+tag = #ffb86c
+value = #ffb86c
+optional = #50fa7b
+spellcheckline = #ff5555
+errorline = #50fa7b
+replacetag = #f1fa8c
+modifier = #8be9fd
+texthighlight = #f1fa8c60
diff --git a/novelwriter/assets/themes/grey_dark.conf b/novelwriter/assets/themes/grey_dark.conf
new file mode 100644
index 00000000..21d95224
--- /dev/null
+++ b/novelwriter/assets/themes/grey_dark.conf
@@ -0,0 +1,71 @@
+[Main]
+name = Grey Dark
+mode = 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/
+
+[Base]
+default = #cccccc
+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
+
+[Palette]
+window = #363636
+windowtext = default
+base = #3e3e3e
+alternatebase = #4e4e4e
+text = default
+tooltipbase = #ffffc0
+tooltiptext = #15150d
+button = #3e3e3e
+buttontext = default
+brighttext = #3e3e3e
+highlight = #2c98f7
+highlightedtext = #ffffff
+link = blue
+linkvisited = blue
+
+[GUI]
+helptext = #a4a4a4
+fadedtext = faded
+errortext = red
+
+[Syntax]
+background = #363636
+text = #c8c8c8
+link = #c8c8c8
+headertext = #e1e1e1
+headertag = #e1e1e1a0
+emphasis = #c8c8c8
+dialog = #c8c8c8
+altdialog = #c8c8c8
+note = #c8c8c8
+hidden = faded
+shortcode = #e1e1e1
+keyword = #e1e1e1
+tag = #c8c8c8
+value = #c8c8c8
+optional = #e1e1e1
+spellcheckline = red
+errorline = green
+replacetag = #e1e1e1
+modifier = #e1e1e1
+texthighlight = #ffffff40
diff --git a/novelwriter/assets/themes/grey_light.conf b/novelwriter/assets/themes/grey_light.conf
new file mode 100644
index 00000000..651ab6d0
--- /dev/null
+++ b/novelwriter/assets/themes/grey_light.conf
@@ -0,0 +1,71 @@
+[Main]
+name = Grey Light
+mode = 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/
+
+[Base]
+default = #303030
+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
+
+[Palette]
+window = #efefef
+windowtext = default
+base = #ffffff
+alternatebase = #e0e0e0
+text = default
+tooltipbase = #ffffc0
+tooltiptext = #15150d
+button = #efefef
+buttontext = default
+brighttext = #ffffff
+highlight = #3087c6
+highlightedtext = #ffffff
+link = blue
+linkvisited = blue
+
+[GUI]
+helptext = #5c5c5c
+fadedtext = faded
+errortext = red
+
+[Syntax]
+background = #ffffff
+text = #141414
+link = #141414
+headertext = #000000
+headertag = #000000a0
+emphasis = #141414
+dialog = #141414
+altdialog = #141414
+note = #141414
+hidden = faded
+shortcode = #000000
+keyword = #000000
+tag = #141414
+value = #141414
+optional = #000000
+spellcheckline = red
+errorline = green
+replacetag = #000000
+modifier = #000000
+texthighlight = #00000040
diff --git a/novelwriter/assets/themes/light_owl.conf b/novelwriter/assets/themes/light_owl.conf
new file mode 100644
index 00000000..b18f5fb7
--- /dev/null
+++ b/novelwriter/assets/themes/light_owl.conf
@@ -0,0 +1,91 @@
+[Main]
+name = Light Owl
+mode = light
+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
+
+##
+# Theme: Light Owl
+# Source: https://github.com/sdras/night-owl-vscode-theme
+# Credit: Sarah Drasner
+##
+# Colours:
+# Background = #fbfbfb
+# Current Line = #e0e0e0
+# Selection = #e0e0e0
+# Foreground = #403f52
+# Comment = #989fb1
+# Red = #de3d3a
+# Orange = #e0af05
+# Yellow = #daaa01
+# Green = #08916a
+# Aqua = #2aa298
+# Blue = #288ed7
+# Purple = #964ac1
+##
+
+[Base]
+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
+
+[Palette]
+window = #e0e0e0
+windowtext = default
+base = #fbfbfb
+alternatebase = #e0e0e0
+text = default
+tooltipbase = #ffffc0
+tooltiptext = #15150d
+button = #e0e0e0
+buttontext = default
+brighttext = #e0e0e0
+highlight = #3087c6
+highlightedtext = #ffffff
+link = blue
+linkvisited = blue
+
+[GUI]
+helptext = blue
+fadedtext = faded
+errortext = red
+
+[Syntax]
+background = #fbfbfb
+text = default
+link = blue
+headertext = blue
+headertag = blue:160
+emphasis = orange
+dialog = green
+altdialog = yellow
+note = #80764b
+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/night_owl.conf b/novelwriter/assets/themes/night_owl.conf
new file mode 100644
index 00000000..8aae2b62
--- /dev/null
+++ b/novelwriter/assets/themes/night_owl.conf
@@ -0,0 +1,91 @@
+[Main]
+name = Night Owl
+mode = dark
+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
+
+##
+# Theme: Night Owl
+# Source: https://github.com/sdras/night-owl-vscode-theme
+# Credit: Sarah Drasner
+##
+# Colours:
+# Background = #011627
+# Current Line = #32374d
+# Selection = #32374d
+# Foreground = #d6deeb
+# Comment = #637777
+# Red = #f78c6c
+# Orange = #ecc48d
+# Yellow = #ffeb95
+# Green = #addb67
+# Aqua = #7fdbca
+# Blue = #82aaff
+# Purple = #c792ea
+##
+
+[Base]
+default = #d6deeb
+faded = #637777
+red = #f78c6c
+orange = #ecc48d
+yellow = #addb67
+green = #addb67
+cyan = #7fdbca
+blue = #82aaff
+purple = #c792ea
+
+[Project]
+root = blue
+folder = yellow
+file = default
+title = green
+chapter = red
+scene = blue
+note = yellow
+
+[Palette]
+window = #32374d
+windowtext = default
+base = #011627
+alternatebase = #32374d
+text = default
+tooltipbase = #ffffc0
+tooltiptext = #15150d
+button = #32374d
+buttontext = default
+brighttext = #32374d
+highlight = #2c98f7
+highlightedtext = #ffffff
+link = blue
+linkvisited = blue
+
+[GUI]
+helptext = blue
+fadedtext = faded
+errortext = red
+
+[Syntax]
+background = #011627
+text = default
+link = blue
+headertext = blue
+headertag = blue:160
+emphasis = orange
+dialog = green
+altdialog = yellow
+note = #fff9ca
+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/snazzy.conf b/novelwriter/assets/themes/snazzy.conf
index 39ec3c87..6220dcc3 100644
--- a/novelwriter/assets/themes/snazzy.conf
+++ b/novelwriter/assets/themes/snazzy.conf
@@ -1,48 +1,84 @@
[Main]
name = Snazzy Light
+mode = 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
-[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]
+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 = #09a1ed
+folder = #cf9c00
+file = #545554
+title = #2dae58
+chapter = #ff5c57
+scene = #09a1ed
+note = #cf9c00
[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 = #565869
+base = #fafbfc
+alternatebase = #eaeaeb
+text = #565869
+tooltipbase = #f5e9c2
+tooltiptext = #565869
+button = #fafbfc
+buttontext = #565869
+brighttext = #ffffff
+highlight = #09a1ed
+highlightedtext = #ffffff
+link = #09a1ed
+linkvisited = #09a1ed
[GUI]
-helptext = 9, 161, 237
-fadedtext = 84, 85, 84
-errortext = 255, 92, 87
+helptext = #09a1ed
+fadedtext = #545554
+errortext = #ff5c57
+
+[Syntax]
+background = #fafbfc
+text = #565869
+link = #09a1ed
+headertext = #2dae58
+headertag = #2dae58a0
+emphasis = #f767bb
+dialog = #09a1ed
+altdialog = #cf9c00
+note = #78bbb9
+hidden = #9194a2
+shortcode = #f767bb
+keyword = #09a1ed
+tag = #2dae58
+value = #cf9c00
+optional = #cf9c00
+spellcheckline = #ff5c57
+errorline = #2dae58
+replacetag = #13bbb7
+modifier = #f767bb
+texthighlight = #cf9c0060
diff --git a/novelwriter/assets/themes/solarized_dark.conf b/novelwriter/assets/themes/solarized_dark.conf
index 0f401eb9..5cf4e157 100644
--- a/novelwriter/assets/themes/solarized_dark.conf
+++ b/novelwriter/assets/themes/solarized_dark.conf
@@ -1,48 +1,90 @@
[Main]
name = Solarized Dark
+mode = dark
author = nullbasis
credit = Ethan Schoonover
url = https://ethanschoonover.com/solarized/
license = MIT
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
-[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]
+default = #e6dfce
+faded = #a6a195
+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 = #2aa198
+folder = #2aa198
+file = #e6dfce
+title = #859900
+chapter = #dc322f
+scene = #268bd2
+note = #b58900
[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 = #002b36
+windowtext = #fdf6e3
+base = #073642
+alternatebase = #586e75
+text = #fdf6e3
+tooltipbase = #859900
+tooltiptext = #002b36
+button = #073642
+buttontext = #fdf6e3
+brighttext = #073642
+highlight = #2aa198
+highlightedtext = #002b36
+link = #268bd2
+linkvisited = #268bd2
[GUI]
-helptext = 101, 123, 131
-fadedtext = 101, 123, 131
-errortext = 220, 50, 47
+helptext = #657b83
+fadedtext = #657b83
+errortext = #dc322f
+
+[Syntax]
+background = #073642
+text = #fdf6e3
+link = #268bd2
+headertext = #93a1a1
+headertag = #2aa198
+emphasis = #268bd2
+dialog = #2aa198
+altdialog = #2aa198
+note = #65a19c
+hidden = #93a1a1
+shortcode = #93a1a1
+keyword = #859900
+tag = #cb4b16
+value = #cb4b16
+optional = #93a1a1
+spellcheckline = #cb4b16
+errorline = #dc322f
+replacetag = #859900
+modifier = #b58900
+texthighlight = #b5890060
diff --git a/novelwriter/assets/themes/solarized_light.conf b/novelwriter/assets/themes/solarized_light.conf
index 06f4cb20..f185bd73 100644
--- a/novelwriter/assets/themes/solarized_light.conf
+++ b/novelwriter/assets/themes/solarized_light.conf
@@ -1,48 +1,90 @@
[Main]
name = Solarized Light
+mode = light
author = nullbasis
credit = Ethan Schoonover
url = https://ethanschoonover.com/solarized/
license = MIT
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
-[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]
+default = #3b4447
+faded = #4e5b5f
+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 = #2aa198
+folder = #2aa198
+file = #3b4447
+title = #859900
+chapter = #dc322f
+scene = #268bd2
+note = #b58900
[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 = #002b36
+base = #fdf6e3
+alternatebase = #93a1a1
+text = #002b36
+tooltipbase = #859900
+tooltiptext = #002b36
+button = #eee8d5
+buttontext = #002b36
+brighttext = #fdf6e3
+highlight = #2aa198
+highlightedtext = #fdf6e3
+link = #268bd2
+linkvisited = #268bd2
[GUI]
-helptext = 78, 91, 95
-fadedtext = 78, 91, 95
-errortext = 220, 50, 47
+helptext = #4e5b5f
+fadedtext = #4e5b5f
+errortext = #dc322f
+
+[Syntax]
+background = #fdf6e3
+text = #002b36
+link = #268bd2
+headertext = #586e75
+headertag = #2aa198
+emphasis = #268bd2
+dialog = #2aa198
+altdialog = #2aa198
+note = #1b6660
+hidden = #586e75
+shortcode = #586e75
+keyword = #859900
+tag = #cb4b16
+value = #cb4b16
+optional = #586e75
+spellcheckline = #cb4b16
+errorline = #dc322f
+replacetag = #859900
+modifier = #b58900
+texthighlight = #b5890060
diff --git a/novelwriter/assets/themes/tango_dark.conf b/novelwriter/assets/themes/tango_dark.conf
new file mode 100644
index 00000000..33e758f7
--- /dev/null
+++ b/novelwriter/assets/themes/tango_dark.conf
@@ -0,0 +1,81 @@
+[Main]
+name = Tango Dark
+mode = dark
+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
+##
+
+[Base]
+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
+
+[Palette]
+window = #555753
+windowtext = default
+base = #2e3436
+alternatebase = #888a85
+text = default
+tooltipbase = #ffffc0
+tooltiptext = #15150d
+button = #555753
+buttontext = default
+brighttext = #555753
+highlight = #2c98f7
+highlightedtext = #ffffff
+link = blue
+linkvisited = blue
+
+[GUI]
+helptext = #e9b96e
+fadedtext = faded
+errortext = red
+
+[Syntax]
+background = #2e3436
+text = default
+link = blue
+headertext = blue
+headertag = blue:160
+emphasis = orange
+dialog = green
+altdialog = yellow
+note = #fcf2a4
+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..877f71ac
--- /dev/null
+++ b/novelwriter/assets/themes/tango_light.conf
@@ -0,0 +1,81 @@
+[Main]
+name = Tango Light
+mode = light
+author = Veronica Berglyd Olsen (adaptation)
+
+##
+# Colours:
+# Background = #eeeeec
+# Foreground = #2e3436
+# Comment = 8f8f8d : 143, 143, 141
+# Red = #a40000
+# 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
+##
+
+[Base]
+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
+
+[Palette]
+window = #d3d7cf
+windowtext = default
+base = #eeeeec
+alternatebase = #e0e0e0
+text = default
+tooltipbase = #ffffc0
+tooltiptext = #15150d
+button = #d3d7cf
+buttontext = default
+brighttext = #d3d7cf
+highlight = #3087c6
+highlightedtext = #ffffff
+link = blue
+linkvisited = blue
+
+[GUI]
+helptext = #8f5902
+fadedtext = faded
+errortext = red
+
+[Syntax]
+background = #eeeeec
+text = default
+link = blue
+headertext = blue
+headertag = blue:160
+emphasis = orange
+dialog = green
+altdialog = yellow
+note = #a78800
+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..974816c1
--- /dev/null
+++ b/novelwriter/assets/themes/tomorrow.conf
@@ -0,0 +1,91 @@
+[Main]
+name = Tomorrow
+mode = light
+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
+
+##
+# 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]
+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
+
+[Palette]
+window = #efefef
+windowtext = #000000
+base = #ffffff
+alternatebase = #d6d6d6
+text = #000000
+tooltipbase = #ffffc0
+tooltiptext = #15150d
+button = #efefef
+buttontext = #000000
+brighttext = #efefef
+highlight = blue
+highlightedtext = #ffffff
+link = blue
+linkvisited = blue
+
+[GUI]
+helptext = #5c5c5c
+fadedtext = faded
+errortext = red
+
+[Syntax]
+background = #ffffff
+text = #4d4d4c
+link = blue
+headertext = blue
+headertag = blue:160
+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..94fecbc0
--- /dev/null
+++ b/novelwriter/assets/themes/tomorrow_night.conf
@@ -0,0 +1,91 @@
+[Main]
+name = Tomorrow Night
+mode = dark
+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
+
+##
+# 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]
+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
+
+[Palette]
+window = #282a2e
+windowtext = default
+base = #1d1f21
+alternatebase = #373b41
+text = default
+tooltipbase = #ffffc0
+tooltiptext = #15150d
+button = #1d1f21
+buttontext = default
+brighttext = #1d1f21
+highlight = #2c98f7
+highlightedtext = #ffffff
+link = blue
+linkvisited = blue
+
+[GUI]
+helptext = #a4a4a4
+fadedtext = #949494
+errortext = red
+
+[Syntax]
+background = #1d1f21
+text = default
+link = blue
+headertext = blue
+headertag = blue:160
+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..88aac2e3
--- /dev/null
+++ b/novelwriter/assets/themes/tomorrow_night_blue.conf
@@ -0,0 +1,91 @@
+[Main]
+name = Tomorrow Night Blue
+mode = dark
+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
+
+##
+# 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]
+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
+
+[Palette]
+window = #00346e
+windowtext = default
+base = #002451
+alternatebase = #003f8e
+text = default
+tooltipbase = #ffffc0
+tooltiptext = #15150d
+button = #002451
+buttontext = default
+brighttext = #002451
+highlight = #2c98f7
+highlightedtext = #ffffff
+link = blue
+linkvisited = blue
+
+[GUI]
+helptext = #a4a4a4
+fadedtext = #949494
+errortext = red
+
+[Syntax]
+background = #002451
+text = default
+link = blue
+headertext = blue
+headertag = blue:160
+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..ab3c4766
--- /dev/null
+++ b/novelwriter/assets/themes/tomorrow_night_bright.conf
@@ -0,0 +1,91 @@
+[Main]
+name = Tomorrow Night Bright
+mode = dark
+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
+
+##
+# 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]
+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
+
+[Palette]
+window = #2a2a2a
+windowtext = default
+base = #000000
+alternatebase = #424242
+text = default
+tooltipbase = #ffffc0
+tooltiptext = #15150d
+button = #000000
+buttontext = default
+brighttext = #000000
+highlight = #2c98f7
+highlightedtext = #ffffff
+link = blue
+linkvisited = blue
+
+[GUI]
+helptext = #a4a4a4
+fadedtext = #949494
+errortext = red
+
+[Syntax]
+background = #000000
+text = default
+link = blue
+headertext = blue
+headertag = blue:160
+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..ed67eb95
--- /dev/null
+++ b/novelwriter/assets/themes/tomorrow_night_eighties.conf
@@ -0,0 +1,91 @@
+[Main]
+name = Tomorrow Night Eighties
+mode = dark
+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
+
+##
+# 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]
+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
+
+[Palette]
+window = #393939
+windowtext = default
+base = #2d2d2d
+alternatebase = #515151
+text = default
+tooltipbase = #ffffc0
+tooltiptext = #15150d
+button = #2d2d2d
+buttontext = default
+brighttext = #2d2d2d
+highlight = #2c98f7
+highlightedtext = #ffffff
+link = blue
+linkvisited = blue
+
+[GUI]
+helptext = #a4a4a4
+fadedtext = #949494
+errortext = red
+
+[Syntax]
+background = #2d2d2d
+text = default
+link = blue
+headertext = blue
+headertag = blue:160
+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/common.py b/novelwriter/common.py
index a4e37f62..6d1e8519 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -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
@@ -676,6 +677,9 @@ def openExternalPath(path: Path) -> bool:
# Classes
##
+_T_Enum = TypeVar("_T_Enum", bound=Enum)
+
+
class NWConfigParser(ConfigParser):
"""Common: Adapted Config Parser
@@ -736,3 +740,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 ac403fdb..404391b1 100644
--- a/novelwriter/config.py
+++ b/novelwriter/config.py
@@ -45,6 +45,7 @@ from novelwriter.common import (
formatTimeStamp, processDialogSymbols, simplified
)
from novelwriter.constants import nwFiles, nwHtmlUnicode, nwQuotes, nwUnicode
+from novelwriter.enum import nwTheme
from novelwriter.error import formatException, logException
if TYPE_CHECKING:
@@ -55,8 +56,8 @@ 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"
@@ -69,22 +70,22 @@ 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",
+ "fmtSQuoteOpen", "focusWidth", "guiFont", "guiLocale", "hasEnchant", "hideFocusFooter",
+ "hideHScroll", "hideVScroll", "highlightEmph", "hostName", "iconColDocs", "iconColTree",
+ "iconTheme", "incNotesWCount", "isDebug", "kernelVer", "lastNotes", "lightTheme",
+ "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",
+ "tabWidth", "textFont", "textMargin", "textWidth", "themeMode", "useCharCount",
+ "userIdleTime", "verPyQtString", "verPyQtValue", "verPyString", "verQtString",
+ "verQtValue", "viewComments", "viewPanePos", "viewSynopsis", "welcomeWinSize",
)
LANG_NW = 1
@@ -153,14 +154,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
@@ -403,7 +405,7 @@ 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
@@ -551,11 +553,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()
@@ -626,8 +627,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)
@@ -721,7 +723,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 +753,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),
diff --git a/novelwriter/constants.py b/novelwriter/constants.py
index c20b9c47..5b7b260d 100644
--- a/novelwriter/constants.py
+++ b/novelwriter/constants.py
@@ -28,7 +28,8 @@ 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
)
@@ -449,10 +450,20 @@ class nwLabels:
"orange": QT_TRANSLATE_NOOP("Constant", "Orange"),
"yellow": QT_TRANSLATE_NOOP("Constant", "Yellow"),
"green": QT_TRANSLATE_NOOP("Constant", "Green"),
- "aqua": QT_TRANSLATE_NOOP("Constant", "Aqua"),
+ "cyan": QT_TRANSLATE_NOOP("Constant", "Cyan"),
"blue": QT_TRANSLATE_NOOP("Constant", "Blue"),
"purple": QT_TRANSLATE_NOOP("Constant", "Purple"),
}
+ THEME_MODE_ICON: Final[dict[nwTheme, str]] = {
+ nwTheme.AUTO: "theme_auto",
+ nwTheme.LIGHT: "theme_light",
+ nwTheme.DARK: "theme_dark",
+ }
+ THEME_MODE_LABEL: Final[dict[nwTheme, str]] = {
+ nwTheme.AUTO: QT_TRANSLATE_NOOP("Constant", "System Theme"),
+ nwTheme.LIGHT: QT_TRANSLATE_NOOP("Constant", "Light Theme"),
+ nwTheme.DARK: QT_TRANSLATE_NOOP("Constant", "Dark Theme"),
+ }
class nwHeadFmt:
diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py
index 281487b3..3d284c2e 100644
--- a/novelwriter/dialogs/preferences.py
+++ b/novelwriter/dialogs/preferences.py
@@ -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
@@ -165,22 +165,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("Light colour theme"), self.lightTheme,
+ self.tr("User interface colour theme."), stretch=(3, 2)
+ )
+ self.mainForm.addRow(
+ self.tr("Dark colour theme"), self.darkTheme,
self.tr("User interface colour theme."), 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 +254,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)
@@ -966,18 +966,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()
@@ -986,11 +991,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)
diff --git a/novelwriter/enum.py b/novelwriter/enum.py
index a837d824..18899295 100644
--- a/novelwriter/enum.py
+++ b/novelwriter/enum.py
@@ -156,6 +156,13 @@ class nwFocus(Enum):
OUTLINE = 3
+class nwTheme(Enum):
+
+ AUTO = 0
+ LIGHT = 1
+ DARK = 2
+
+
class nwOutline(Enum):
TITLE = 0
diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py
index f784623f..e0ccb9e5 100644
--- a/novelwriter/gui/sidebar.py
+++ b/novelwriter/gui/sidebar.py
@@ -27,12 +27,13 @@ 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
@@ -77,9 +78,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 +90,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 +116,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)
@@ -133,6 +139,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,6 +151,36 @@ class GuiSideBar(QWidget):
self.tbStats.setThemeIcon("sb_stats")
self.tbSettings.setThemeIcon("settings")
+ self._setThemeModeIcon()
+
+ return
+
+ ##
+ # 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()
+ return
+
+ ##
+ # 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]))
return
diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py
index 3239fd4a..315633da 100644
--- a/novelwriter/gui/statusbar.py
+++ b/novelwriter/gui/statusbar.py
@@ -143,9 +143,9 @@ 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)
diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py
index 1eb2e487..4ab4f7d8 100644
--- a/novelwriter/gui/theme.py
+++ b/novelwriter/gui/theme.py
@@ -26,21 +26,23 @@ 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
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,9 +56,18 @@ STYLES_MIN_TOOLBUTTON = "minimalToolButton"
STYLES_BIG_TOOLBUTTON = "bigToolButton"
+@dataclass
+class ThemeEntry:
+
+ name: str
+ dark: bool
+ path: Path
+
+
class ThemeMeta:
name: str = ""
+ mode: str = ""
description: str = ""
author: str = ""
credit: str = ""
@@ -96,21 +107,20 @@ class GuiTheme:
"""
__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", "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
@@ -118,23 +128,19 @@ class GuiTheme:
self.fadedText = QColor(0, 0, 0)
self.errorText = QColor(255, 0, 0)
- # 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,19 +184,19 @@ 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")
-
- self.loadTheme()
- self.loadSyntax()
-
return
##
- # Methods
+ # Properties
+ ##
+
+ @property
+ def colourThemes(self) -> dict[str, ThemeEntry]:
+ """Return a dictionary of all themes."""
+ return self._allThemes
+
+ ##
+ # Getters
##
def getTextWidth(self, text: str, font: QFont | None = None) -> int:
@@ -203,30 +209,105 @@ 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()
+
+ return
+
+ 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 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 alpha
+ name, _, alpha = value.partition(":")
+ color = QColor(self._qColors.get(name.strip(), default))
+ color.setAlpha(checkInt(alpha, 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
+
+ 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 +318,40 @@ 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.description = parser.get(sec, "description", fallback="N/A")
+ meta.author = parser.get(sec, "author", fallback="N/A")
+ meta.credit = parser.get(sec, "credit", fallback="N/A")
+ meta.url = parser.get(sec, "url", fallback="")
+ meta.license = parser.get(sec, "license", fallback="N/A")
+ meta.licenseUrl = parser.get(sec, "licenseurl", 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("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"))
# Palette
sec = "Palette"
@@ -292,16 +374,40 @@ class GuiTheme:
# 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.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 +427,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)
@@ -351,117 +457,30 @@ class GuiTheme:
self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Accent, highlight)
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey)
+ # Set project override colours
+ if (override := CONFIG.iconColTree) != "theme":
+ 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, "")
@@ -470,22 +489,26 @@ class GuiTheme:
# 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")
+ return
+
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
+ 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,37 +519,38 @@ 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("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)
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))
+ self._guiPalette.setBrush(value, self._readColor(parser, section, name))
return
def _buildStyleSheets(self, palette: QPalette) -> None:
@@ -561,6 +585,35 @@ class GuiTheme:
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="")
+ dark = parser.get("Main", "mode", fallback="light").lower() == "dark"
+ if name:
+ key = file.stem
+ prefix = "*" if key.startswith("default") else ""
+ lookup = f"{prefix}{name} {key}"
+ keys.append(lookup)
+ data[lookup] = (file.stem, name, 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)
+
+ return
+
class GuiIcons:
"""The icon class manages the content of the assets/icons folder,
@@ -572,9 +625,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]]] = {
@@ -588,63 +640,72 @@ class GuiIcons:
def __init__(self, mainTheme: GuiTheme) -> None:
- self.mainTheme = mainTheme
- self.themeMeta = ThemeMeta()
+ self._theme = mainTheme
+ self._meta = ThemeMeta()
# 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()
+ self._meta = ThemeMeta()
return
+ ##
+ # 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)
+ return
+
+ 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:
+ with open(entry.path, mode="r", encoding="utf-8") as icons:
for icon in icons:
bits = icon.partition("=")
key = bits[0].strip()
@@ -658,47 +719,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
@@ -769,7 +806,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 +830,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 +843,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 +854,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 +869,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 +891,46 @@ 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)
+
+ return
+
# 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
+ data.extend(n for n in path.iterdir() if n.is_file() and n.suffix == extension)
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 ""
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 8a895bfe..25f997f6 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -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,
@@ -902,10 +902,45 @@ 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()
+ return
+
+ def refreshThemeColors(self, syntax: bool = False, force: bool = False) -> None:
+ """Refresh the GUI theme."""
+ SHARED.theme.loadTheme(force=force)
+ 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()
+
+ return
+
##
# Events
##
+ def changeEvent(self, event: QEvent) -> None:
+ """Capture application change events."""
+ if int(event.type()) == 210: # ThemeChange
+ self.checkThemeUpdate()
+ return
+
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.
@@ -1054,23 +1089,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()
diff --git a/novelwriter/shared.py b/novelwriter/shared.py
index 228b9678..c5b7025c 100644
--- a/novelwriter/shared.py
+++ b/novelwriter/shared.py
@@ -175,6 +175,7 @@ class SharedData(QObject):
is created.
"""
self._theme = theme
+ self._theme.initThemes()
return
def initSharedData(self, gui: GuiMain) -> None:
diff --git a/tests/mocked.py b/tests/mocked.py
index d28cdbba..e1e4097c 100644
--- a/tests/mocked.py
+++ b/tests/mocked.py
@@ -65,6 +65,9 @@ class MockTheme:
self.guiFontBU = QFont()
return
+ def initThemes(self) -> None:
+ return
+
def getPixmap(self, *a) -> QPixmap:
return QPixmap()
diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf
index 78441636..8943a9b2 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-06-02 16:21:38
[Main]
font =
-theme = default
-syntax = default_light
+lighttheme = default_light
+darktheme = default_dark
+thememode = AUTO
icons = material_rounded_normal
iconcoltree = theme
iconcoldocs = False
diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py
index 006daa42..ef16baaa 100644
--- a/tests/test_base/test_base_common.py
+++ b/tests/test_base/test_base_common.py
@@ -41,6 +41,7 @@ from novelwriter.common import (
readTextFile, simplified, transferCase, uniqueCompact, xmlElement,
xmlIndent, xmlSubElem, yesNo
)
+from novelwriter.enum import nwItemClass
from tests.mocked import causeOSError
from tests.tools import writeFile
@@ -804,6 +805,7 @@ def testBaseCommon_NWConfigParser(fncPath):
"list1 = a, b, c\n"
"list2 = 17, 18, 19\n"
"float1 = 4.2\n"
+ "enum1 = NOVEL\n"
f"path1 = {fncPath}\n"
))
@@ -875,3 +877,7 @@ def testBaseCommon_NWConfigParser(fncPath):
assert cfgParser.rdIntList("nope", "list2", [1]) == [1]
assert cfgParser.rdIntList("main", "blabla", [1]) == [1]
+
+ # Read Enum
+ assert cfgParser.rdEnum("main", "enum1", nwItemClass.NO_CLASS) == nwItemClass.NOVEL
+ assert cfgParser.rdEnum("main", "blabla", nwItemClass.NO_CLASS) == nwItemClass.NO_CLASS
diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py
index 81b03831..98a1063d 100644
--- a/tests/test_base/test_base_config.py
+++ b/tests/test_base/test_base_config.py
@@ -131,15 +131,15 @@ def testBaseConfig_InitLoadSave(monkeypatch, fncPath, tstPaths):
assert tstConf.errorText().startswith("Could not load config file")
# Change a few settings, save, reset, and reload
- tstConf.guiTheme = "foo"
- tstConf.guiSyntax = "bar"
+ tstConf.lightTheme = "foo"
+ tstConf.darkTheme = "bar"
assert tstConf.saveConfig() is True
newConf = Config()
newConf.initConfig(confPath=fncPath, dataPath=fncPath)
newConf.loadConfig()
- assert newConf.guiTheme == "foo"
- assert newConf.guiSyntax == "bar"
+ assert newConf.lightTheme == "foo"
+ assert newConf.darkTheme == "bar"
# Test Correcting Quote Settings
tstConf.fmtDQuoteOpen = '"'
diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py
index 5572f430..1c54da98 100644
--- a/tests/test_dialogs/test_dlg_preferences.py
+++ b/tests/test_dialogs/test_dlg_preferences.py
@@ -27,10 +27,11 @@ from PyQt6.QtGui import QAction, QFont, QFontDatabase, QKeyEvent
from PyQt6.QtWidgets import QFileDialog, QFontDialog
from novelwriter import CONFIG, SHARED
-from novelwriter.config import DEF_GUI
+from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT
from novelwriter.constants import nwUnicode
from novelwriter.dialogs.preferences import GuiPreferences
from novelwriter.dialogs.quotes import GuiQuoteSelect
+from novelwriter.gui.theme import ThemeEntry
from novelwriter.types import QtDialogCancel, QtDialogSave, QtModNone
KEY_DELAY = 1
@@ -55,15 +56,11 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths):
assert "en_GB" in languages
# Check GUI Themes
- themes = [prefs.guiTheme.itemData(i) for i in range(prefs.guiTheme.count())]
- assert len(themes) >= 5
- assert DEF_GUI in themes
+ themes = [prefs.lightTheme.itemData(i) for i in range(prefs.lightTheme.count())]
+ assert DEF_GUI_LIGHT in themes
- # Check GUI Syntax
- syntax = [prefs.guiSyntax.itemData(i) for i in range(prefs.guiSyntax.count())]
- assert len(syntax) >= 10
- assert "default_dark" in syntax
- assert "default_light" in syntax
+ themes = [prefs.darkTheme.itemData(i) for i in range(prefs.darkTheme.count())]
+ assert DEF_GUI_DARK in themes
# Check Spell Checking
spelling = [prefs.spellLanguage.itemData(i) for i in range(prefs.spellLanguage.count())]
@@ -124,7 +121,7 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
button = prefs.buttonBox.button(QtDialogSave)
assert button is not None
button.click()
- assert signal.args == [False, False, False, False]
+ assert len(signal.args) == 4
# Check Close Button
prefs.show()
@@ -151,6 +148,12 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
(fncPath / "nw_en_US.qm").touch()
(fncPath / "project_en_US.json").touch()
CONFIG._nwLangPath = fncPath
+ SHARED.theme._allThemes = {
+ "theme1": ThemeEntry(name="Theme 1", dark=False, path=fncPath),
+ "theme2": ThemeEntry(name="Theme 2", dark=False, path=fncPath),
+ "theme3": ThemeEntry(name="Theme 3", dark=True, path=fncPath),
+ "theme4": ThemeEntry(name="Theme 4", dark=True, path=fncPath),
+ }
prefs = GuiPreferences(nwGUI)
with qtbot.waitExposed(prefs):
@@ -158,7 +161,8 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
# Appearance
prefs.guiLocale.setCurrentIndex(prefs.guiLocale.findData("en_US"))
- prefs.guiTheme.setCurrentIndex(prefs.guiTheme.findData("default_dark"))
+ prefs.lightTheme.setCurrentIndex(prefs.lightTheme.findData("theme1"))
+ prefs.darkTheme.setCurrentIndex(prefs.darkTheme.findData("theme3"))
with monkeypatch.context() as mp:
mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True))
prefs.nativeFont.setChecked(True) # Use OS font dialog
@@ -169,14 +173,14 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
prefs.useCharCount.setChecked(True)
assert CONFIG.guiLocale != "en_US"
- assert CONFIG.guiTheme != "default_dark"
+ assert CONFIG.lightTheme == "default_light"
+ assert CONFIG.darkTheme == "default_dark"
assert CONFIG.guiFont.family() != ""
assert CONFIG.hideVScroll is False
assert CONFIG.hideHScroll is False
assert CONFIG.useCharCount is False
# Document Style
- prefs.guiSyntax.setCurrentIndex(prefs.guiSyntax.findData("default_dark"))
with monkeypatch.context() as mp:
mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True))
prefs.nativeFont.setChecked(False) # Use Qt font dialog
@@ -185,7 +189,6 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
prefs.showFullPath.setChecked(False)
prefs.incNotesWCount.setChecked(False)
- assert CONFIG.guiSyntax != "default_dark"
assert CONFIG.textFont.family() != ""
assert CONFIG.showFullPath is True
assert CONFIG.incNotesWCount is True
@@ -344,14 +347,14 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
# Appearance
assert CONFIG.guiLocale == "en_US"
- assert CONFIG.guiTheme == "default_dark"
+ assert CONFIG.lightTheme == "theme1"
+ assert CONFIG.darkTheme == "theme3"
assert CONFIG.guiFont == QFont()
assert CONFIG.hideVScroll is True
assert CONFIG.hideHScroll is True
assert CONFIG.useCharCount is True
# Document Style
- assert CONFIG.guiSyntax == "default_dark"
assert CONFIG.textFont == QFont()
assert CONFIG.showFullPath is False
assert CONFIG.incNotesWCount is False
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index 6fda9b6a..5010094d 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -24,6 +24,7 @@ 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()
diff --git a/tests/test_gui/test_gui_sidebar.py b/tests/test_gui/test_gui_sidebar.py
new file mode 100644
index 00000000..45e372db
--- /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 .
+"""
+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..efb19f48 100644
--- a/tests/test_gui/test_gui_statusbar.py
+++ b/tests/test_gui/test_gui_statusbar.py
@@ -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 c64f1172..107fd125 100644
--- a/tests/test_gui/test_gui_theme.py
+++ b/tests/test_gui/test_gui_theme.py
@@ -20,344 +20,389 @@ along with this program. If not, see .
"""
from __future__ import annotations
-import sys
-
+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)
- 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")
-
- _listConf(result, tstPaths.cnfDir / "themes", ".conf")
- assert result["themeone"] == themeOne
- assert result["themetwo"] == themeTwo
-
- # 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"
)
- 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 Medium"
-
- # 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 Medium"
+ 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 Medium"
+
+ # 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 Medium"
+
+ # 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
# ==========
@@ -455,14 +500,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
# ================
@@ -519,5 +563,3 @@ def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI):
assert iconCache.getHeaderDecorationNarrow(4) == iconCache._headerDecNarrow[4]
assert iconCache.getHeaderDecorationNarrow(5) == iconCache._headerDecNarrow[5]
assert iconCache.getHeaderDecorationNarrow(6) == iconCache._headerDecNarrow[5]
-
- # qtbot.stop()
diff --git a/utils/icon_themes.py b/utils/icon_themes.py
index 00d75714..752b29e5 100644
--- a/utils/icon_themes.py
+++ b/utils/icon_themes.py
@@ -90,6 +90,10 @@ ICONS = [
"sb_search",
"sb_stats",
+ "theme_light",
+ "theme_dark",
+ "theme_auto",
+
"add",
"bookmarks",
"browse",
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",