novelWriter 2.6.2__py3-none-any.whl → 2.7b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- novelwriter/__init__.py +84 -74
- novelwriter/assets/i18n/nw_cs_CZ.qm +0 -0
- novelwriter/assets/i18n/nw_de_DE.qm +0 -0
- novelwriter/assets/i18n/nw_en_US.qm +0 -0
- novelwriter/assets/i18n/nw_es_419.qm +0 -0
- novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
- novelwriter/assets/i18n/nw_it_IT.qm +0 -0
- novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
- novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
- novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
- novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
- novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
- novelwriter/assets/i18n/nw_ru_RU.qm +0 -0
- novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
- novelwriter/assets/i18n/project_en_GB.json +1 -0
- novelwriter/assets/icons/font_awesome.icons +109 -0
- novelwriter/assets/icons/material_filled_bold.icons +109 -0
- novelwriter/assets/icons/material_filled_normal.icons +109 -0
- novelwriter/assets/icons/material_filled_thin.icons +109 -0
- novelwriter/assets/icons/material_rounded_bold.icons +109 -0
- novelwriter/assets/icons/material_rounded_normal.icons +109 -0
- novelwriter/assets/icons/material_rounded_thin.icons +109 -0
- novelwriter/assets/icons/remix_filled.icons +108 -0
- novelwriter/assets/icons/remix_outline.icons +108 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/{manual_fr_FR.pdf → manual_fr.pdf} +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/syntax/cyberpunk_night.conf +1 -1
- novelwriter/assets/text/credits_en.htm +6 -6
- novelwriter/assets/themes/cyberpunk_night.conf +23 -7
- novelwriter/assets/themes/default_dark.conf +20 -4
- novelwriter/assets/themes/default_light.conf +21 -5
- novelwriter/assets/themes/dracula.conf +20 -4
- novelwriter/assets/themes/solarized_dark.conf +24 -8
- novelwriter/assets/themes/solarized_light.conf +22 -6
- novelwriter/common.py +33 -26
- novelwriter/config.py +118 -127
- novelwriter/constants.py +75 -56
- novelwriter/core/buildsettings.py +23 -16
- novelwriter/core/coretools.py +11 -7
- novelwriter/core/docbuild.py +19 -13
- novelwriter/core/document.py +2 -2
- novelwriter/core/index.py +142 -432
- novelwriter/core/indexdata.py +403 -0
- novelwriter/core/item.py +35 -28
- novelwriter/core/itemmodel.py +27 -26
- novelwriter/core/novelmodel.py +223 -0
- novelwriter/core/options.py +1 -1
- novelwriter/core/project.py +10 -11
- novelwriter/core/projectdata.py +5 -5
- novelwriter/core/projectxml.py +1 -1
- novelwriter/core/sessions.py +3 -2
- novelwriter/core/spellcheck.py +4 -3
- novelwriter/core/status.py +12 -15
- novelwriter/core/storage.py +1 -1
- novelwriter/core/tree.py +46 -8
- novelwriter/dialogs/about.py +19 -22
- novelwriter/dialogs/docmerge.py +21 -23
- novelwriter/dialogs/docsplit.py +20 -23
- novelwriter/dialogs/editlabel.py +9 -13
- novelwriter/dialogs/preferences.py +111 -48
- novelwriter/dialogs/projectsettings.py +48 -54
- novelwriter/dialogs/quotes.py +14 -19
- novelwriter/dialogs/wordlist.py +25 -30
- novelwriter/enum.py +8 -0
- novelwriter/error.py +16 -16
- novelwriter/extensions/configlayout.py +18 -18
- novelwriter/extensions/eventfilters.py +9 -5
- novelwriter/extensions/modified.py +34 -15
- novelwriter/extensions/novelselector.py +18 -5
- novelwriter/extensions/pagedsidebar.py +39 -49
- novelwriter/extensions/progressbars.py +10 -8
- novelwriter/extensions/statusled.py +6 -13
- novelwriter/extensions/switch.py +19 -30
- novelwriter/extensions/switchbox.py +7 -3
- novelwriter/extensions/versioninfo.py +4 -4
- novelwriter/formats/shared.py +1 -1
- novelwriter/formats/todocx.py +14 -10
- novelwriter/formats/tohtml.py +7 -5
- novelwriter/formats/tokenizer.py +36 -33
- novelwriter/formats/tomarkdown.py +6 -2
- novelwriter/formats/toodt.py +27 -22
- novelwriter/formats/toqdoc.py +19 -14
- novelwriter/formats/toraw.py +6 -2
- novelwriter/gui/doceditor.py +216 -265
- novelwriter/gui/dochighlight.py +46 -45
- novelwriter/gui/docviewer.py +102 -104
- novelwriter/gui/docviewerpanel.py +47 -51
- novelwriter/gui/editordocument.py +8 -5
- novelwriter/gui/itemdetails.py +15 -18
- novelwriter/gui/mainmenu.py +147 -146
- novelwriter/gui/noveltree.py +239 -405
- novelwriter/gui/outline.py +137 -76
- novelwriter/gui/projtree.py +138 -132
- novelwriter/gui/search.py +33 -31
- novelwriter/gui/sidebar.py +13 -18
- novelwriter/gui/statusbar.py +13 -15
- novelwriter/gui/theme.py +533 -430
- novelwriter/guimain.py +27 -29
- novelwriter/shared.py +32 -23
- novelwriter/text/comments.py +70 -0
- novelwriter/text/patterns.py +4 -4
- novelwriter/tools/dictionaries.py +20 -29
- novelwriter/tools/lipsum.py +16 -17
- novelwriter/tools/manusbuild.py +39 -42
- novelwriter/tools/manuscript.py +113 -126
- novelwriter/tools/manussettings.py +78 -83
- novelwriter/tools/noveldetails.py +51 -64
- novelwriter/tools/welcome.py +56 -75
- novelwriter/tools/writingstats.py +44 -57
- novelwriter/types.py +5 -7
- {novelWriter-2.6.2.dist-info → novelwriter-2.7b1.dist-info}/METADATA +6 -6
- novelwriter-2.7b1.dist-info/RECORD +159 -0
- {novelWriter-2.6.2.dist-info → novelwriter-2.7b1.dist-info}/WHEEL +1 -1
- novelWriter-2.6.2.dist-info/RECORD +0 -363
- novelwriter/assets/icons/typicons_dark/README.md +0 -29
- novelwriter/assets/icons/typicons_dark/icons.conf +0 -134
- novelwriter/assets/icons/typicons_dark/mixed_copy.svg +0 -4
- novelwriter/assets/icons/typicons_dark/mixed_document-chapter.svg +0 -12
- novelwriter/assets/icons/typicons_dark/mixed_document-new.svg +0 -6
- novelwriter/assets/icons/typicons_dark/mixed_document-note.svg +0 -12
- novelwriter/assets/icons/typicons_dark/mixed_document-scene.svg +0 -12
- novelwriter/assets/icons/typicons_dark/mixed_document-section.svg +0 -12
- novelwriter/assets/icons/typicons_dark/mixed_document-title.svg +0 -12
- novelwriter/assets/icons/typicons_dark/mixed_edit.svg +0 -4
- novelwriter/assets/icons/typicons_dark/mixed_import.svg +0 -5
- novelwriter/assets/icons/typicons_dark/mixed_input-checked.svg +0 -5
- novelwriter/assets/icons/typicons_dark/mixed_input-none.svg +0 -5
- novelwriter/assets/icons/typicons_dark/mixed_input-unchecked.svg +0 -5
- novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +0 -6
- novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +0 -6
- novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +0 -6
- novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +0 -6
- novelwriter/assets/icons/typicons_dark/mixed_search-replace.svg +0 -6
- novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +0 -6
- novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +0 -6
- novelwriter/assets/icons/typicons_dark/nw_deco-h0.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_deco-h1.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_deco-h2.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_deco-h3.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_deco-h4.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_deco-noveltree-more.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_font.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_panel.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_quote.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_search-case.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_search-preserve.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_search-regex.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_search-word.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_tb-bold-md.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +0 -6
- novelwriter/assets/icons/typicons_dark/nw_tb-italic-md.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +0 -6
- novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +0 -7
- novelwriter/assets/icons/typicons_dark/nw_tb-strike-md.svg +0 -4
- novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +0 -6
- novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +0 -7
- novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +0 -7
- novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +0 -7
- novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +0 -5
- novelwriter/assets/icons/typicons_dark/typ_arrow-down-thick-grey.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_arrow-forward.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_arrow-maximise.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_arrow-minimise.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_arrow-repeat-grey.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_book-grey.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_book.svg +0 -6
- novelwriter/assets/icons/typicons_dark/typ_bookmark.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_calendar.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_cancel-grey.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_cancel.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_chart-bar-grey.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_chevron-down.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_chevron-left.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_chevron-right.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_chevron-up.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_cog.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_delete-full.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_delete.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_directions-full.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_document-add.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_document-text.svg +0 -8
- novelwriter/assets/icons/typicons_dark/typ_document.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_export-grey.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_export.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_eye.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_flag.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_folder-open.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_folder.svg +0 -5
- novelwriter/assets/icons/typicons_dark/typ_globe-grey.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_key.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_lightbulb-full.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_location.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_media-pause-grey.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_media-record-outline.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_media-record.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_minus.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_pencil.svg +0 -5
- novelwriter/assets/icons/typicons_dark/typ_pin-outline.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_pin.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_plus.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_puzzle-outline.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_puzzle.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_refresh.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_search.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_star.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_stopwatch-grey.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_th-dot-menu.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_th-list-grey.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_th-list.svg +0 -9
- novelwriter/assets/icons/typicons_dark/typ_times.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_trash.svg +0 -5
- novelwriter/assets/icons/typicons_dark/typ_unfold-hidden.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_unfold-visible.svg +0 -4
- novelwriter/assets/icons/typicons_dark/typ_user.svg +0 -5
- novelwriter/assets/icons/typicons_dark/typ_warning-full.svg +0 -4
- novelwriter/assets/icons/typicons_light/README.md +0 -29
- novelwriter/assets/icons/typicons_light/icons.conf +0 -134
- novelwriter/assets/icons/typicons_light/mixed_copy.svg +0 -4
- novelwriter/assets/icons/typicons_light/mixed_document-chapter.svg +0 -12
- novelwriter/assets/icons/typicons_light/mixed_document-new.svg +0 -6
- novelwriter/assets/icons/typicons_light/mixed_document-note.svg +0 -12
- novelwriter/assets/icons/typicons_light/mixed_document-scene.svg +0 -12
- novelwriter/assets/icons/typicons_light/mixed_document-section.svg +0 -12
- novelwriter/assets/icons/typicons_light/mixed_document-title.svg +0 -12
- novelwriter/assets/icons/typicons_light/mixed_edit.svg +0 -4
- novelwriter/assets/icons/typicons_light/mixed_import.svg +0 -5
- novelwriter/assets/icons/typicons_light/mixed_input-checked.svg +0 -5
- novelwriter/assets/icons/typicons_light/mixed_input-none.svg +0 -5
- novelwriter/assets/icons/typicons_light/mixed_input-unchecked.svg +0 -5
- novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +0 -6
- novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +0 -6
- novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +0 -6
- novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +0 -6
- novelwriter/assets/icons/typicons_light/mixed_search-replace.svg +0 -6
- novelwriter/assets/icons/typicons_light/mixed_size-height.svg +0 -6
- novelwriter/assets/icons/typicons_light/mixed_size-width.svg +0 -6
- novelwriter/assets/icons/typicons_light/nw_deco-h0.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_deco-h1.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_deco-h2.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_deco-h3.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_deco-h4.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_deco-note.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_deco-noveltree-more.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_font.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_panel.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_quote.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_search-case.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_search-preserve.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_search-regex.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_search-word.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_tb-bold-md.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +0 -6
- novelwriter/assets/icons/typicons_light/nw_tb-italic-md.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +0 -6
- novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +0 -7
- novelwriter/assets/icons/typicons_light/nw_tb-strike-md.svg +0 -4
- novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +0 -6
- novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +0 -7
- novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +0 -7
- novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +0 -7
- novelwriter/assets/icons/typicons_light/nw_toolbar.svg +0 -5
- novelwriter/assets/icons/typicons_light/typ_arrow-down-thick-grey.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_arrow-forward.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_arrow-maximise.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_arrow-minimise.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_arrow-repeat-grey.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_book-grey.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_book.svg +0 -6
- novelwriter/assets/icons/typicons_light/typ_bookmark.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_calendar.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_cancel-grey.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_cancel.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_chart-bar-grey.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_chevron-down.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_chevron-left.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_chevron-right.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_chevron-up.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_cog.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_delete-full.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_delete.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_directions-full.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_document-add.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_document-text.svg +0 -5
- novelwriter/assets/icons/typicons_light/typ_document.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_export-grey.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_export.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_eye.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_flag.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_folder-open.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_folder.svg +0 -5
- novelwriter/assets/icons/typicons_light/typ_globe-grey.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_key.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_lightbulb-full.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_location.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_media-pause-grey.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_media-record-outline.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_media-record.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_minus.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_pencil.svg +0 -5
- novelwriter/assets/icons/typicons_light/typ_pin-outline.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_pin.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_plus.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_puzzle-outline.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_puzzle.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_refresh.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_search-grey.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_search.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_star.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_stopwatch-grey.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_th-dot-menu.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_th-list-grey.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_th-list.svg +0 -9
- novelwriter/assets/icons/typicons_light/typ_times.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_trash.svg +0 -5
- novelwriter/assets/icons/typicons_light/typ_unfold-hidden.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_unfold-visible.svg +0 -4
- novelwriter/assets/icons/typicons_light/typ_user.svg +0 -5
- novelwriter/assets/icons/typicons_light/typ_warning-full.svg +0 -4
- {novelWriter-2.6.2.dist-info → novelwriter-2.7b1.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.6.2.dist-info → novelwriter-2.7b1.dist-info/licenses}/LICENSE.md +0 -0
- {novelWriter-2.6.2.dist-info → novelwriter-2.7b1.dist-info}/top_level.txt +0 -0
novelwriter/gui/theme.py
CHANGED
@@ -27,17 +27,25 @@ from __future__ import annotations
|
|
27
27
|
import logging
|
28
28
|
|
29
29
|
from math import ceil
|
30
|
-
from
|
30
|
+
from typing import TYPE_CHECKING, Final
|
31
31
|
|
32
|
-
from
|
33
|
-
from
|
34
|
-
|
32
|
+
from PyQt6.QtCore import QSize, Qt
|
33
|
+
from PyQt6.QtGui import (
|
34
|
+
QColor, QFont, QFontDatabase, QFontMetrics, QIcon, QPainter, QPainterPath,
|
35
|
+
QPalette, QPixmap
|
36
|
+
)
|
37
|
+
from PyQt6.QtWidgets import QApplication
|
35
38
|
|
36
39
|
from novelwriter import CONFIG
|
37
|
-
from novelwriter.common import NWConfigParser,
|
40
|
+
from novelwriter.common import NWConfigParser, minmax
|
41
|
+
from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX
|
38
42
|
from novelwriter.constants import nwLabels
|
39
43
|
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
40
44
|
from novelwriter.error import logException
|
45
|
+
from novelwriter.types import QtBlack, QtHexArgb, QtPaintAntiAlias, QtTransparent
|
46
|
+
|
47
|
+
if TYPE_CHECKING:
|
48
|
+
from pathlib import Path
|
41
49
|
|
42
50
|
logger = logging.getLogger(__name__)
|
43
51
|
|
@@ -46,74 +54,73 @@ STYLES_MIN_TOOLBUTTON = "minimalToolButton"
|
|
46
54
|
STYLES_BIG_TOOLBUTTON = "bigToolButton"
|
47
55
|
|
48
56
|
|
57
|
+
class ThemeMeta:
|
58
|
+
|
59
|
+
name: str = ""
|
60
|
+
description: str = ""
|
61
|
+
author: str = ""
|
62
|
+
credit: str = ""
|
63
|
+
url: str = ""
|
64
|
+
license: str = ""
|
65
|
+
licenseUrl: str = ""
|
66
|
+
|
67
|
+
|
68
|
+
class SyntaxColors:
|
69
|
+
|
70
|
+
back: QColor = QColor(255, 255, 255)
|
71
|
+
text: QColor = QColor(0, 0, 0)
|
72
|
+
link: QColor = QColor(0, 0, 0)
|
73
|
+
head: QColor = QColor(0, 0, 0)
|
74
|
+
headH: QColor = QColor(0, 0, 0)
|
75
|
+
emph: QColor = QColor(0, 0, 0)
|
76
|
+
dialN: QColor = QColor(0, 0, 0)
|
77
|
+
dialA: QColor = QColor(0, 0, 0)
|
78
|
+
hidden: QColor = QColor(0, 0, 0)
|
79
|
+
note: QColor = QColor(0, 0, 0)
|
80
|
+
code: QColor = QColor(0, 0, 0)
|
81
|
+
key: QColor = QColor(0, 0, 0)
|
82
|
+
tag: QColor = QColor(0, 0, 0)
|
83
|
+
val: QColor = QColor(0, 0, 0)
|
84
|
+
opt: QColor = QColor(0, 0, 0)
|
85
|
+
spell: QColor = QColor(0, 0, 0)
|
86
|
+
error: QColor = QColor(0, 0, 0)
|
87
|
+
repTag: QColor = QColor(0, 0, 0)
|
88
|
+
mod: QColor = QColor(0, 0, 0)
|
89
|
+
mark: QColor = QColor(255, 255, 255, 128)
|
90
|
+
|
91
|
+
|
49
92
|
class GuiTheme:
|
50
93
|
"""Gui Theme Class
|
51
94
|
|
52
95
|
Handles the look and feel of novelWriter.
|
53
96
|
"""
|
54
97
|
|
98
|
+
__slots__ = (
|
99
|
+
"_availSyntax", "_availThemes", "_guiPalette", "_styleSheets", "_syntaxList", "_themeList",
|
100
|
+
"baseButtonHeight", "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText",
|
101
|
+
"fadedText", "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration",
|
102
|
+
"getHeaderDecorationNarrow", "getIcon", "getIconColor", "getItemIcon", "getPixmap",
|
103
|
+
"getToggleIcon", "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall",
|
104
|
+
"helpText", "iconCache", "isDarkTheme", "syntaxMeta", "syntaxTheme", "textNHeight",
|
105
|
+
"textNWidth", "themeMeta",
|
106
|
+
)
|
107
|
+
|
55
108
|
def __init__(self) -> None:
|
56
109
|
|
57
110
|
self.iconCache = GuiIcons(self)
|
58
111
|
|
59
|
-
#
|
60
|
-
|
112
|
+
# GUI Theme
|
113
|
+
self.themeMeta = ThemeMeta()
|
114
|
+
self.isDarkTheme = False
|
61
115
|
|
62
|
-
#
|
63
|
-
self.
|
64
|
-
self.
|
65
|
-
self.
|
66
|
-
self.themeCredit = ""
|
67
|
-
self.themeUrl = ""
|
68
|
-
self.themeLicense = ""
|
69
|
-
self.themeLicenseUrl = ""
|
70
|
-
self.themeIcons = ""
|
71
|
-
self.isLightTheme = True
|
116
|
+
# Special Text Colours
|
117
|
+
self.helpText = QColor(0, 0, 0)
|
118
|
+
self.fadedText = QColor(0, 0, 0)
|
119
|
+
self.errorText = QColor(255, 0, 0)
|
72
120
|
|
73
|
-
#
|
74
|
-
self.
|
75
|
-
self.
|
76
|
-
self.statSaved = QColor(0, 0, 0)
|
77
|
-
self.helpText = QColor(0, 0, 0)
|
78
|
-
self.fadedText = QColor(0, 0, 0)
|
79
|
-
self.errorText = QColor(255, 0, 0)
|
80
|
-
|
81
|
-
# Loaded Syntax Settings
|
82
|
-
# ======================
|
83
|
-
|
84
|
-
# Main
|
85
|
-
self.syntaxName = ""
|
86
|
-
self.syntaxDescription = ""
|
87
|
-
self.syntaxAuthor = ""
|
88
|
-
self.syntaxCredit = ""
|
89
|
-
self.syntaxUrl = ""
|
90
|
-
self.syntaxLicense = ""
|
91
|
-
self.syntaxLicenseUrl = ""
|
92
|
-
|
93
|
-
# Colours
|
94
|
-
self.colBack = QColor(255, 255, 255)
|
95
|
-
self.colText = QColor(0, 0, 0)
|
96
|
-
self.colLink = QColor(0, 0, 0)
|
97
|
-
self.colHead = QColor(0, 0, 0)
|
98
|
-
self.colHeadH = QColor(0, 0, 0)
|
99
|
-
self.colEmph = QColor(0, 0, 0)
|
100
|
-
self.colDialN = QColor(0, 0, 0)
|
101
|
-
self.colDialA = QColor(0, 0, 0)
|
102
|
-
self.colHidden = QColor(0, 0, 0)
|
103
|
-
self.colNote = QColor(0, 0, 0)
|
104
|
-
self.colCode = QColor(0, 0, 0)
|
105
|
-
self.colKey = QColor(0, 0, 0)
|
106
|
-
self.colTag = QColor(0, 0, 0)
|
107
|
-
self.colVal = QColor(0, 0, 0)
|
108
|
-
self.colOpt = QColor(0, 0, 0)
|
109
|
-
self.colSpell = QColor(0, 0, 0)
|
110
|
-
self.colError = QColor(0, 0, 0)
|
111
|
-
self.colRepTag = QColor(0, 0, 0)
|
112
|
-
self.colMod = QColor(0, 0, 0)
|
113
|
-
self.colMark = QColor(255, 255, 255, 128)
|
114
|
-
|
115
|
-
# Class Setup
|
116
|
-
# ===========
|
121
|
+
# Syntax Theme
|
122
|
+
self.syntaxMeta = ThemeMeta()
|
123
|
+
self.syntaxTheme = SyntaxColors()
|
117
124
|
|
118
125
|
# Load Themes
|
119
126
|
self._guiPalette = QPalette()
|
@@ -123,10 +130,10 @@ class GuiTheme:
|
|
123
130
|
self._availSyntax: dict[str, Path] = {}
|
124
131
|
self._styleSheets: dict[str, str] = {}
|
125
132
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
133
|
+
_listConf(self._availSyntax, CONFIG.assetPath("syntax"), ".conf")
|
134
|
+
_listConf(self._availThemes, CONFIG.assetPath("themes"), ".conf")
|
135
|
+
_listConf(self._availSyntax, CONFIG.dataPath("syntax"), ".conf")
|
136
|
+
_listConf(self._availThemes, CONFIG.dataPath("themes"), ".conf")
|
130
137
|
|
131
138
|
self.loadTheme()
|
132
139
|
self.loadSyntax()
|
@@ -135,18 +142,12 @@ class GuiTheme:
|
|
135
142
|
self.getIcon = self.iconCache.getIcon
|
136
143
|
self.getPixmap = self.iconCache.getPixmap
|
137
144
|
self.getItemIcon = self.iconCache.getItemIcon
|
145
|
+
self.getIconColor = self.iconCache.getIconColor
|
138
146
|
self.getToggleIcon = self.iconCache.getToggleIcon
|
139
|
-
self.
|
147
|
+
self.getDecoration = self.iconCache.getDecoration
|
140
148
|
self.getHeaderDecoration = self.iconCache.getHeaderDecoration
|
141
149
|
self.getHeaderDecorationNarrow = self.iconCache.getHeaderDecorationNarrow
|
142
150
|
|
143
|
-
# Extract Other Info
|
144
|
-
self.guiDPI = QApplication.primaryScreen().logicalDotsPerInchX()
|
145
|
-
self.guiScale = QApplication.primaryScreen().logicalDotsPerInchX()/96.0
|
146
|
-
CONFIG.guiScale = self.guiScale
|
147
|
-
logger.debug("GUI DPI: %.1f", self.guiDPI)
|
148
|
-
logger.debug("GUI Scale: %.2f", self.guiScale)
|
149
|
-
|
150
151
|
# Fonts
|
151
152
|
self.guiFont = QApplication.font()
|
152
153
|
self.guiFontB = QApplication.font()
|
@@ -161,9 +162,9 @@ class GuiTheme:
|
|
161
162
|
fHeight = qMetric.height()
|
162
163
|
fAscent = qMetric.ascent()
|
163
164
|
self.fontPointSize = self.guiFont.pointSizeF()
|
164
|
-
self.fontPixelSize =
|
165
|
-
self.baseIconHeight =
|
166
|
-
self.baseButtonHeight =
|
165
|
+
self.fontPixelSize = round(fHeight)
|
166
|
+
self.baseIconHeight = round(fAscent)
|
167
|
+
self.baseButtonHeight = round(1.35*fAscent)
|
167
168
|
self.textNHeight = qMetric.boundingRect("N").height()
|
168
169
|
self.textNWidth = qMetric.boundingRect("N").width()
|
169
170
|
|
@@ -199,7 +200,7 @@ class GuiTheme:
|
|
199
200
|
qMetrics = QFontMetrics(font)
|
200
201
|
else:
|
201
202
|
qMetrics = QFontMetrics(self.guiFont)
|
202
|
-
return
|
203
|
+
return ceil(qMetrics.boundingRect(text).width())
|
203
204
|
|
204
205
|
##
|
205
206
|
# Theme Methods
|
@@ -207,43 +208,66 @@ class GuiTheme:
|
|
207
208
|
|
208
209
|
def loadTheme(self) -> bool:
|
209
210
|
"""Load the currently specified GUI theme."""
|
210
|
-
|
211
|
-
if
|
212
|
-
logger.error("Could not find GUI theme '%s'",
|
213
|
-
|
214
|
-
CONFIG.guiTheme =
|
215
|
-
|
216
|
-
|
217
|
-
if themeFile is None:
|
211
|
+
theme = CONFIG.guiTheme
|
212
|
+
if theme not in self._availThemes:
|
213
|
+
logger.error("Could not find GUI theme '%s'", theme)
|
214
|
+
theme = DEF_GUI
|
215
|
+
CONFIG.guiTheme = theme
|
216
|
+
|
217
|
+
if not (file := self._availThemes.get(theme)):
|
218
218
|
logger.error("Could not load GUI theme")
|
219
219
|
return False
|
220
220
|
|
221
|
-
|
222
|
-
logger.info("Loading GUI theme '%s'", guiTheme)
|
221
|
+
logger.info("Loading GUI theme '%s'", theme)
|
223
222
|
parser = NWConfigParser()
|
224
223
|
try:
|
225
|
-
with open(
|
226
|
-
parser.read_file(
|
224
|
+
with open(file, mode="r", encoding="utf-8") as fo:
|
225
|
+
parser.read_file(fo)
|
227
226
|
except Exception:
|
228
|
-
logger.error("Could not
|
227
|
+
logger.error("Could not read file: %s", file)
|
229
228
|
logException()
|
230
229
|
return False
|
231
230
|
|
232
231
|
# Reset Palette
|
233
|
-
self.
|
234
|
-
self._resetGuiColors()
|
232
|
+
self._resetTheme()
|
235
233
|
|
236
234
|
# Main
|
237
235
|
sec = "Main"
|
236
|
+
meta = ThemeMeta()
|
237
|
+
if parser.has_section(sec):
|
238
|
+
meta.name = parser.rdStr(sec, "name", "")
|
239
|
+
meta.description = parser.rdStr(sec, "description", "N/A")
|
240
|
+
meta.author = parser.rdStr(sec, "author", "N/A")
|
241
|
+
meta.credit = parser.rdStr(sec, "credit", "N/A")
|
242
|
+
meta.url = parser.rdStr(sec, "url", "")
|
243
|
+
meta.license = parser.rdStr(sec, "license", "N/A")
|
244
|
+
meta.licenseUrl = parser.rdStr(sec, "licenseurl", "")
|
245
|
+
|
246
|
+
self.themeMeta = meta
|
247
|
+
|
248
|
+
# Icons
|
249
|
+
sec = "Icons"
|
238
250
|
if parser.has_section(sec):
|
239
|
-
self.
|
240
|
-
self.
|
241
|
-
self.
|
242
|
-
self.
|
243
|
-
self.
|
244
|
-
self.
|
245
|
-
self.
|
246
|
-
self.
|
251
|
+
self.iconCache.setIconColor("default", self._parseColor(parser, sec, "default"))
|
252
|
+
self.iconCache.setIconColor("faded", self._parseColor(parser, sec, "faded"))
|
253
|
+
self.iconCache.setIconColor("red", self._parseColor(parser, sec, "red"))
|
254
|
+
self.iconCache.setIconColor("orange", self._parseColor(parser, sec, "orange"))
|
255
|
+
self.iconCache.setIconColor("yellow", self._parseColor(parser, sec, "yellow"))
|
256
|
+
self.iconCache.setIconColor("green", self._parseColor(parser, sec, "green"))
|
257
|
+
self.iconCache.setIconColor("aqua", self._parseColor(parser, sec, "aqua"))
|
258
|
+
self.iconCache.setIconColor("blue", self._parseColor(parser, sec, "blue"))
|
259
|
+
self.iconCache.setIconColor("purple", self._parseColor(parser, sec, "purple"))
|
260
|
+
|
261
|
+
# Project
|
262
|
+
sec = "Project"
|
263
|
+
if parser.has_section(sec):
|
264
|
+
self.iconCache.setIconColor("root", self._parseColor(parser, sec, "root"))
|
265
|
+
self.iconCache.setIconColor("folder", self._parseColor(parser, sec, "folder"))
|
266
|
+
self.iconCache.setIconColor("file", self._parseColor(parser, sec, "file"))
|
267
|
+
self.iconCache.setIconColor("title", self._parseColor(parser, sec, "title"))
|
268
|
+
self.iconCache.setIconColor("chapter", self._parseColor(parser, sec, "chapter"))
|
269
|
+
self.iconCache.setIconColor("scene", self._parseColor(parser, sec, "scene"))
|
270
|
+
self.iconCache.setIconColor("note", self._parseColor(parser, sec, "note"))
|
247
271
|
|
248
272
|
# Palette
|
249
273
|
sec = "Palette"
|
@@ -266,101 +290,136 @@ class GuiTheme:
|
|
266
290
|
# GUI
|
267
291
|
sec = "GUI"
|
268
292
|
if parser.has_section(sec):
|
269
|
-
self.helpText
|
270
|
-
self.fadedText
|
271
|
-
self.errorText
|
272
|
-
self.statNone = self._parseColour(parser, sec, "statusnone")
|
273
|
-
self.statUnsaved = self._parseColour(parser, sec, "statusunsaved")
|
274
|
-
self.statSaved = self._parseColour(parser, sec, "statussaved")
|
293
|
+
self.helpText = self._parseColor(parser, sec, "helptext")
|
294
|
+
self.fadedText = self._parseColor(parser, sec, "fadedtext")
|
295
|
+
self.errorText = self._parseColor(parser, sec, "errortext")
|
275
296
|
|
276
297
|
# Update Dependant Colours
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
298
|
+
# Based on: https://github.com/qt/qtbase/blob/dev/src/gui/kernel/qplatformtheme.cpp
|
299
|
+
text = self._guiPalette.text().color()
|
300
|
+
window = self._guiPalette.window().color()
|
301
|
+
highlight = self._guiPalette.highlight().color()
|
302
|
+
isDark = text.lightnessF() > window.lightnessF()
|
303
|
+
|
304
|
+
QtColActive = QPalette.ColorGroup.Active
|
305
|
+
QtColInactive = QPalette.ColorGroup.Inactive
|
306
|
+
QtColDisabled = QPalette.ColorGroup.Disabled
|
307
|
+
|
308
|
+
if window.lightnessF() < 0.15:
|
309
|
+
# If window is too dark, we need a lighter ref colour for shades
|
310
|
+
ref = QColor.fromHslF(window.hueF(), window.saturationF(), 0.15, window.alphaF())
|
311
|
+
else:
|
312
|
+
ref = window
|
313
|
+
|
314
|
+
light = ref.lighter(150)
|
315
|
+
mid = ref.darker(130)
|
316
|
+
midLight = mid.lighter(110)
|
317
|
+
dark = ref.darker(150)
|
318
|
+
shadow = dark.darker(135)
|
319
|
+
darkOff = dark.darker(150)
|
320
|
+
shadowOff = ref.darker(150)
|
321
|
+
|
322
|
+
grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140)
|
323
|
+
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190)
|
324
|
+
|
325
|
+
placeholder = QColor(text)
|
326
|
+
placeholder.setAlpha(128)
|
327
|
+
|
328
|
+
self._guiPalette.setBrush(QPalette.ColorRole.Light, light)
|
329
|
+
self._guiPalette.setBrush(QPalette.ColorRole.Mid, mid)
|
330
|
+
self._guiPalette.setBrush(QPalette.ColorRole.Midlight, midLight)
|
331
|
+
self._guiPalette.setBrush(QPalette.ColorRole.Dark, dark)
|
332
|
+
self._guiPalette.setBrush(QPalette.ColorRole.Shadow, shadow)
|
333
|
+
|
334
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Text, dimmed)
|
335
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.WindowText, dimmed)
|
336
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.ButtonText, dimmed)
|
337
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Base, window)
|
338
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Dark, darkOff)
|
339
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Shadow, shadowOff)
|
340
|
+
|
341
|
+
self._guiPalette.setBrush(QPalette.ColorRole.PlaceholderText, placeholder)
|
342
|
+
|
343
|
+
self._guiPalette.setBrush(QtColActive, QPalette.ColorRole.Highlight, highlight)
|
344
|
+
self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Highlight, highlight)
|
345
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Highlight, grey)
|
346
|
+
|
347
|
+
if CONFIG.verQtValue >= 0x060600:
|
348
|
+
self._guiPalette.setBrush(QtColActive, QPalette.ColorRole.Accent, highlight)
|
349
|
+
self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Accent, highlight)
|
350
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey)
|
351
|
+
|
352
|
+
# Load icons after the theme is parsed
|
353
|
+
self.iconCache.loadTheme(CONFIG.iconTheme)
|
354
|
+
|
355
|
+
# Finalise
|
356
|
+
self.isDarkTheme = isDark
|
299
357
|
QApplication.setPalette(self._guiPalette)
|
300
|
-
|
301
|
-
# Reset stylesheets so that they are regenerated
|
302
358
|
self._buildStyleSheets(self._guiPalette)
|
303
359
|
|
304
360
|
return True
|
305
361
|
|
306
362
|
def loadSyntax(self) -> bool:
|
307
363
|
"""Load the currently specified syntax highlighter theme."""
|
308
|
-
|
309
|
-
if
|
310
|
-
logger.error("Could not find syntax theme '%s'",
|
311
|
-
|
312
|
-
CONFIG.guiSyntax =
|
313
|
-
|
314
|
-
|
315
|
-
if syntaxFile is None:
|
364
|
+
theme = CONFIG.guiSyntax
|
365
|
+
if theme not in self._availSyntax:
|
366
|
+
logger.error("Could not find syntax theme '%s'", theme)
|
367
|
+
theme = DEF_SYNTAX
|
368
|
+
CONFIG.guiSyntax = theme
|
369
|
+
|
370
|
+
if not (file := self._availSyntax.get(theme)):
|
316
371
|
logger.error("Could not load syntax theme")
|
317
372
|
return False
|
318
373
|
|
319
|
-
logger.info("Loading syntax theme '%s'",
|
320
|
-
|
321
|
-
confParser = NWConfigParser()
|
374
|
+
logger.info("Loading syntax theme '%s'", theme)
|
375
|
+
parser = NWConfigParser()
|
322
376
|
try:
|
323
|
-
with open(
|
324
|
-
|
377
|
+
with open(file, mode="r", encoding="utf-8") as fo:
|
378
|
+
parser.read_file(fo)
|
325
379
|
except Exception:
|
326
|
-
logger.error("Could not
|
380
|
+
logger.error("Could not read file: %s", file)
|
327
381
|
logException()
|
328
382
|
return False
|
329
383
|
|
330
384
|
# Main
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
385
|
+
sec = "Main"
|
386
|
+
meta = ThemeMeta()
|
387
|
+
if parser.has_section(sec):
|
388
|
+
meta.name = parser.rdStr(sec, "name", "")
|
389
|
+
meta.description = parser.rdStr(sec, "description", "N/A")
|
390
|
+
meta.author = parser.rdStr(sec, "author", "N/A")
|
391
|
+
meta.credit = parser.rdStr(sec, "credit", "N/A")
|
392
|
+
meta.url = parser.rdStr(sec, "url", "")
|
393
|
+
meta.license = parser.rdStr(sec, "license", "N/A")
|
394
|
+
meta.licenseUrl = parser.rdStr(sec, "licenseurl", "")
|
340
395
|
|
341
396
|
# Syntax
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
397
|
+
sec = "Syntax"
|
398
|
+
syntax = SyntaxColors()
|
399
|
+
if parser.has_section(sec):
|
400
|
+
syntax.back = self._parseColor(parser, sec, "background")
|
401
|
+
syntax.text = self._parseColor(parser, sec, "text")
|
402
|
+
syntax.link = self._parseColor(parser, sec, "link")
|
403
|
+
syntax.head = self._parseColor(parser, sec, "headertext")
|
404
|
+
syntax.headH = self._parseColor(parser, sec, "headertag")
|
405
|
+
syntax.emph = self._parseColor(parser, sec, "emphasis")
|
406
|
+
syntax.dialN = self._parseColor(parser, sec, "dialog")
|
407
|
+
syntax.dialA = self._parseColor(parser, sec, "altdialog")
|
408
|
+
syntax.hidden = self._parseColor(parser, sec, "hidden")
|
409
|
+
syntax.note = self._parseColor(parser, sec, "note")
|
410
|
+
syntax.code = self._parseColor(parser, sec, "shortcode")
|
411
|
+
syntax.key = self._parseColor(parser, sec, "keyword")
|
412
|
+
syntax.tag = self._parseColor(parser, sec, "tag")
|
413
|
+
syntax.val = self._parseColor(parser, sec, "value")
|
414
|
+
syntax.opt = self._parseColor(parser, sec, "optional")
|
415
|
+
syntax.spell = self._parseColor(parser, sec, "spellcheckline")
|
416
|
+
syntax.error = self._parseColor(parser, sec, "errorline")
|
417
|
+
syntax.repTag = self._parseColor(parser, sec, "replacetag")
|
418
|
+
syntax.mod = self._parseColor(parser, sec, "modifier")
|
419
|
+
syntax.mark = self._parseColor(parser, sec, "texthighlight")
|
420
|
+
|
421
|
+
self.syntaxMeta = meta
|
422
|
+
self.syntaxTheme = syntax
|
364
423
|
|
365
424
|
return True
|
366
425
|
|
@@ -369,14 +428,14 @@ class GuiTheme:
|
|
369
428
|
if self._themeList:
|
370
429
|
return self._themeList
|
371
430
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
if
|
377
|
-
|
431
|
+
themes = []
|
432
|
+
parser = NWConfigParser()
|
433
|
+
for key, path in self._availThemes.items():
|
434
|
+
logger.debug("Checking theme config '%s'", key)
|
435
|
+
if name := _loadInternalName(parser, path):
|
436
|
+
themes.append((key, name))
|
378
437
|
|
379
|
-
self._themeList = sorted(
|
438
|
+
self._themeList = sorted(themes, key=_sortTheme)
|
380
439
|
|
381
440
|
return self._themeList
|
382
441
|
|
@@ -385,14 +444,14 @@ class GuiTheme:
|
|
385
444
|
if self._syntaxList:
|
386
445
|
return self._syntaxList
|
387
446
|
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
if
|
393
|
-
|
447
|
+
themes = []
|
448
|
+
parser = NWConfigParser()
|
449
|
+
for key, path in self._availSyntax.items():
|
450
|
+
logger.debug("Checking theme syntax '%s'", key)
|
451
|
+
if name := _loadInternalName(parser, path):
|
452
|
+
themes.append((key, name))
|
394
453
|
|
395
|
-
self._syntaxList = sorted(
|
454
|
+
self._syntaxList = sorted(themes, key=_sortTheme)
|
396
455
|
|
397
456
|
return self._syntaxList
|
398
457
|
|
@@ -404,67 +463,92 @@ class GuiTheme:
|
|
404
463
|
# Internal Functions
|
405
464
|
##
|
406
465
|
|
407
|
-
def
|
466
|
+
def _resetTheme(self) -> None:
|
408
467
|
"""Reset GUI colours to default values."""
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
if
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
468
|
+
palette = QPalette()
|
469
|
+
|
470
|
+
text = palette.color(QPalette.ColorRole.Text)
|
471
|
+
window = palette.color(QPalette.ColorRole.Window)
|
472
|
+
isDark = text.lightnessF() > window.lightnessF()
|
473
|
+
|
474
|
+
# Reset GUI Palette
|
475
|
+
faded = QColor(128, 128, 128)
|
476
|
+
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190)
|
477
|
+
red = QColor(242, 119, 122) if isDark else QColor(240, 40, 41)
|
478
|
+
orange = QColor(249, 145, 57) if isDark else QColor(245, 135, 31)
|
479
|
+
yellow = QColor(255, 204, 102) if isDark else QColor(234, 183, 0)
|
480
|
+
green = QColor(153, 204, 153) if isDark else QColor(113, 140, 0)
|
481
|
+
aqua = QColor(102, 204, 204) if isDark else QColor(62, 153, 159)
|
482
|
+
blue = QColor(102, 153, 204) if isDark else QColor(66, 113, 174)
|
483
|
+
purple = QColor(204, 153, 204) if isDark else QColor(137, 89, 168)
|
484
|
+
|
485
|
+
# Text Colours
|
486
|
+
self.helpText = dimmed
|
487
|
+
self.fadedText = faded
|
488
|
+
self.errorText = red
|
489
|
+
|
490
|
+
self._guiPalette = palette
|
491
|
+
|
492
|
+
# Reset Icons
|
493
|
+
icons = self.iconCache
|
494
|
+
icons.clear()
|
495
|
+
icons.setIconColor("default", text)
|
496
|
+
icons.setIconColor("faded", faded)
|
497
|
+
icons.setIconColor("red", red)
|
498
|
+
icons.setIconColor("orange", orange)
|
499
|
+
icons.setIconColor("yellow", yellow)
|
500
|
+
icons.setIconColor("green", green)
|
501
|
+
icons.setIconColor("aqua", aqua)
|
502
|
+
icons.setIconColor("blue", blue)
|
503
|
+
icons.setIconColor("purple", purple)
|
504
|
+
icons.setIconColor("root", blue)
|
505
|
+
icons.setIconColor("folder", yellow)
|
506
|
+
icons.setIconColor("file", text)
|
507
|
+
icons.setIconColor("title", green)
|
508
|
+
icons.setIconColor("chapter", red)
|
509
|
+
icons.setIconColor("scene", blue)
|
510
|
+
icons.setIconColor("note", yellow)
|
425
511
|
|
426
|
-
return
|
512
|
+
return
|
427
513
|
|
428
|
-
def
|
514
|
+
def _parseColor(self, parser: NWConfigParser, section: str, name: str) -> QColor:
|
429
515
|
"""Parse a colour value from a config string."""
|
430
516
|
return QColor(*parser.rdIntList(section, name, [0, 0, 0, 255]))
|
431
517
|
|
432
|
-
def _setPalette(
|
433
|
-
|
518
|
+
def _setPalette(
|
519
|
+
self, parser: NWConfigParser, section: str, name: str, value: QPalette.ColorRole
|
520
|
+
) -> None:
|
434
521
|
"""Set a palette colour value from a config string."""
|
435
|
-
self._guiPalette.
|
522
|
+
self._guiPalette.setBrush(value, self._parseColor(parser, section, name))
|
436
523
|
return
|
437
524
|
|
438
525
|
def _buildStyleSheets(self, palette: QPalette) -> None:
|
439
526
|
"""Build default style sheets."""
|
440
527
|
self._styleSheets = {}
|
441
528
|
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
tCol = palette.text().color()
|
448
|
-
hCol = palette.highlight().color()
|
529
|
+
text = palette.text().color()
|
530
|
+
text.setAlpha(48)
|
531
|
+
tCol = text.name(QtHexArgb)
|
532
|
+
hCol = palette.highlight().color().name(QtHexArgb)
|
449
533
|
|
450
534
|
# Flat Tab Widget and Tab Bar:
|
451
535
|
self._styleSheets[STYLES_FLAT_TABS] = (
|
452
536
|
"QTabWidget::pane {border: 0;} "
|
453
|
-
|
454
|
-
f"QTabWidget QTabBar::tab:selected {{color: {
|
537
|
+
"QTabWidget QTabBar::tab {border: 0; padding: 4px 8px;} "
|
538
|
+
f"QTabWidget QTabBar::tab:selected {{color: {hCol};}} "
|
455
539
|
)
|
456
540
|
|
457
541
|
# Minimal Tool Button
|
458
542
|
self._styleSheets[STYLES_MIN_TOOLBUTTON] = (
|
459
|
-
|
460
|
-
f"QToolButton:hover {{border: none; background: {
|
543
|
+
"QToolButton {padding: 2px; margin: 0; border: none; background: transparent;} "
|
544
|
+
f"QToolButton:hover {{border: none; background: {tCol};}} "
|
461
545
|
"QToolButton::menu-indicator {image: none;} "
|
462
546
|
)
|
463
547
|
|
464
548
|
# Big Tool Button
|
465
549
|
self._styleSheets[STYLES_BIG_TOOLBUTTON] = (
|
466
|
-
|
467
|
-
f"QToolButton:hover {{border: none; background: {
|
550
|
+
"QToolButton {padding: 6px; margin: 0; border: none; background: transparent;} "
|
551
|
+
f"QToolButton:hover {{border: none; background: {tCol};}} "
|
468
552
|
"QToolButton::menu-indicator {image: none;} "
|
469
553
|
)
|
470
554
|
|
@@ -473,64 +557,24 @@ class GuiTheme:
|
|
473
557
|
|
474
558
|
class GuiIcons:
|
475
559
|
"""The icon class manages the content of the assets/icons folder,
|
476
|
-
and provides a simple interface for requesting icons.
|
477
|
-
listed in the ICON_KEYS are handled.
|
478
|
-
|
479
|
-
Icons are loaded on first request, and then cached for further
|
480
|
-
requests. Each icon key in the ICON_KEYS set has standard icon set
|
481
|
-
in the icon theme conf file. The existence of the file, and the
|
482
|
-
definition of all keys are checked when the theme is loaded.
|
560
|
+
and provides a simple interface for requesting icons.
|
483
561
|
|
484
|
-
|
485
|
-
|
562
|
+
Icons are generated from SVG on first request, and then cached for
|
563
|
+
further requests. If the icon is not defined, a placeholder icon is
|
564
|
+
returned instead.
|
486
565
|
"""
|
487
566
|
|
488
|
-
|
489
|
-
|
490
|
-
"
|
491
|
-
"
|
492
|
-
|
493
|
-
"proj_stats", "proj_title", "status_idle", "status_lang", "status_lines", "status_stats",
|
494
|
-
"status_time", "view_build", "view_editor", "view_novel", "view_outline", "view_search",
|
495
|
-
|
496
|
-
# Class Icons
|
497
|
-
"cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none", "cls_novel",
|
498
|
-
"cls_object", "cls_plot", "cls_template", "cls_timeline", "cls_trash", "cls_world",
|
499
|
-
|
500
|
-
# Search Icons
|
501
|
-
"search_cancel", "search_case", "search_loop", "search_preserve", "search_project",
|
502
|
-
"search_regex", "search_word",
|
503
|
-
|
504
|
-
# Format Icons
|
505
|
-
"fmt_bold", "fmt_bold-md", "fmt_italic", "fmt_italic-md", "fmt_mark", "fmt_strike",
|
506
|
-
"fmt_strike-md", "fmt_subscript", "fmt_superscript", "fmt_underline", "margin_bottom",
|
507
|
-
"margin_left", "margin_right", "margin_top", "size_height", "size_width",
|
508
|
-
|
509
|
-
# General Button Icons
|
510
|
-
"add", "add_document", "backward", "bookmark", "browse", "checked", "close", "copy",
|
511
|
-
"cross", "document", "down", "edit", "export", "font", "forward", "import", "list",
|
512
|
-
"maximise", "menu", "minimise", "more", "noncheckable", "open", "panel", "quote",
|
513
|
-
"refresh", "remove", "revert", "search_replace", "search", "settings", "star", "toolbar",
|
514
|
-
"unchecked", "up", "view",
|
515
|
-
|
516
|
-
# Switches
|
517
|
-
"sticky-on", "sticky-off",
|
518
|
-
"bullet-on", "bullet-off",
|
519
|
-
"unfold-show", "unfold-hide",
|
520
|
-
|
521
|
-
# Decorations
|
522
|
-
"deco_doc_h0", "deco_doc_h1", "deco_doc_h2", "deco_doc_h3", "deco_doc_h4", "deco_doc_more",
|
523
|
-
"deco_doc_h0_n", "deco_doc_h1_n", "deco_doc_h2_n", "deco_doc_h3_n", "deco_doc_h4_n",
|
524
|
-
"deco_doc_nt_n",
|
525
|
-
}
|
567
|
+
__slots__ = (
|
568
|
+
"_availThemes", "_headerDec", "_headerDecNarrow", "_noIcon",
|
569
|
+
"_qColors", "_qIcons", "_svgColors", "_svgData", "_themeList",
|
570
|
+
"mainTheme", "themeMeta",
|
571
|
+
)
|
526
572
|
|
527
|
-
TOGGLE_ICON_KEYS: dict[str, tuple[str, str]] = {
|
528
|
-
"sticky": ("sticky-on", "sticky-off"),
|
573
|
+
TOGGLE_ICON_KEYS: Final[dict[str, tuple[str, str]]] = {
|
529
574
|
"bullet": ("bullet-on", "bullet-off"),
|
530
575
|
"unfold": ("unfold-show", "unfold-hide"),
|
531
576
|
}
|
532
|
-
|
533
|
-
IMAGE_MAP: dict[str, tuple[str, str]] = {
|
577
|
+
IMAGE_MAP: Final[dict[str, tuple[str, str]]] = {
|
534
578
|
"welcome": ("welcome-light.jpg", "welcome-dark.jpg"),
|
535
579
|
"nw-text": ("novelwriter-text-light.svg", "novelwriter-text-dark.svg"),
|
536
580
|
}
|
@@ -538,202 +582,209 @@ class GuiIcons:
|
|
538
582
|
def __init__(self, mainTheme: GuiTheme) -> None:
|
539
583
|
|
540
584
|
self.mainTheme = mainTheme
|
585
|
+
self.themeMeta = ThemeMeta()
|
541
586
|
|
542
587
|
# Storage
|
588
|
+
self._svgData: dict[str, bytes] = {}
|
589
|
+
self._svgColors: dict[str, bytes] = {}
|
590
|
+
self._qColors: dict[str, QColor] = {}
|
543
591
|
self._qIcons: dict[str, QIcon] = {}
|
544
|
-
self._themeMap: dict[str, Path] = {}
|
545
592
|
self._headerDec: list[QPixmap] = []
|
546
593
|
self._headerDecNarrow: list[QPixmap] = []
|
547
594
|
|
548
595
|
# Icon Theme Path
|
549
|
-
self.
|
550
|
-
self.
|
596
|
+
self._availThemes: dict[str, Path] = {}
|
597
|
+
self._themeList: list[tuple[str, str]] = []
|
551
598
|
|
552
599
|
# None Icon
|
553
|
-
self._noIcon = QIcon(str(
|
600
|
+
self._noIcon = QIcon(str(CONFIG.assetPath("icons") / "none.svg"))
|
554
601
|
|
555
|
-
|
556
|
-
self.
|
557
|
-
self.themeDescription = ""
|
558
|
-
self.themeAuthor = ""
|
559
|
-
self.themeCredit = ""
|
560
|
-
self.themeUrl = ""
|
561
|
-
self.themeLicense = ""
|
562
|
-
self.themeLicenseUrl = ""
|
602
|
+
_listConf(self._availThemes, CONFIG.assetPath("icons"), ".icons")
|
603
|
+
_listConf(self._availThemes, CONFIG.dataPath("icons"), ".icons")
|
563
604
|
|
564
605
|
return
|
565
606
|
|
607
|
+
def clear(self) -> None:
|
608
|
+
"""Clear the icon cache."""
|
609
|
+
self._svgData = {}
|
610
|
+
self._svgColors = {}
|
611
|
+
self._qColors = {}
|
612
|
+
self._qIcons = {}
|
613
|
+
self._headerDec = []
|
614
|
+
self._headerDecNarrow = []
|
615
|
+
self.themeMeta = ThemeMeta()
|
616
|
+
return
|
617
|
+
|
566
618
|
##
|
567
619
|
# Actions
|
568
620
|
##
|
569
621
|
|
570
|
-
def loadTheme(self,
|
622
|
+
def loadTheme(self, theme: str) -> bool:
|
571
623
|
"""Update the theme map. This is more of an init, since many of
|
572
624
|
the GUI icons cannot really be replaced without writing specific
|
573
625
|
update functions for the classes where they're used.
|
574
626
|
"""
|
575
|
-
self.
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
logger.info("Loading icon theme '%s'",
|
585
|
-
|
586
|
-
# Config File
|
587
|
-
confParser = NWConfigParser()
|
627
|
+
if theme not in self._availThemes:
|
628
|
+
logger.error("Could not find icon theme '%s'", theme)
|
629
|
+
theme = DEF_ICONS
|
630
|
+
CONFIG.iconTheme = theme
|
631
|
+
|
632
|
+
if not (file := self._availThemes.get(theme)):
|
633
|
+
logger.error("Could not load icon theme")
|
634
|
+
return False
|
635
|
+
|
636
|
+
logger.info("Loading icon theme '%s'", theme)
|
588
637
|
try:
|
589
|
-
|
590
|
-
|
638
|
+
meta = ThemeMeta()
|
639
|
+
with open(file, mode="r", encoding="utf-8") as icons:
|
640
|
+
for icon in icons:
|
641
|
+
bits = icon.partition("=")
|
642
|
+
key = bits[0].strip()
|
643
|
+
value = bits[2].strip()
|
644
|
+
if key and value:
|
645
|
+
if key.startswith("icon:"):
|
646
|
+
self._svgData[key[5:]] = value.encode("utf-8")
|
647
|
+
elif key == "meta:name":
|
648
|
+
meta.name = value
|
649
|
+
elif key == "meta:author":
|
650
|
+
meta.author = value
|
651
|
+
elif key == "meta:license":
|
652
|
+
meta.license = value
|
653
|
+
self.themeMeta = meta
|
591
654
|
except Exception:
|
592
|
-
logger.error("Could not
|
655
|
+
logger.error("Could not read file: %s", file)
|
593
656
|
logException()
|
594
657
|
return False
|
595
658
|
|
596
|
-
#
|
597
|
-
|
598
|
-
|
599
|
-
self.
|
600
|
-
self.
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
# Populate Icon Map
|
608
|
-
cnfSec = "Map"
|
609
|
-
if confParser.has_section(cnfSec):
|
610
|
-
for iconName, iconFile in confParser.items(cnfSec):
|
611
|
-
if iconName not in self.ICON_KEYS:
|
612
|
-
logger.error("Unknown icon name '%s' in config file", iconName)
|
613
|
-
else:
|
614
|
-
iconPath = themePath / iconFile
|
615
|
-
if iconPath.is_file():
|
616
|
-
self._themeMap[iconName] = iconPath
|
617
|
-
logger.debug("Icon slot '%s' using file '%s'", iconName, iconFile)
|
618
|
-
else:
|
619
|
-
logger.error("Icon file '%s' not in theme folder", iconFile)
|
620
|
-
|
621
|
-
# Check that icons have been defined
|
622
|
-
logger.debug("Scanning theme icons")
|
623
|
-
for iconKey in self.ICON_KEYS:
|
624
|
-
if iconKey in ("novelwriter", "proj_nwx"):
|
625
|
-
# These are not part of the theme itself
|
626
|
-
continue
|
627
|
-
if iconKey not in self._themeMap:
|
628
|
-
logger.error("No icon file specified for '%s'", iconKey)
|
629
|
-
|
630
|
-
# Refresh icons
|
631
|
-
for iconKey in self._qIcons:
|
632
|
-
logger.debug("Reloading icon: '%s'", iconKey)
|
633
|
-
qIcon = self._loadIcon(iconKey)
|
634
|
-
self._qIcons[iconKey] = qIcon
|
635
|
-
|
636
|
-
self._headerDec = []
|
637
|
-
self._headerDecNarrow = []
|
659
|
+
# Set colour overrides for project item icons
|
660
|
+
if (override := CONFIG.iconColTree) != "theme":
|
661
|
+
color = self._svgColors.get(override, b"#000000")
|
662
|
+
self._svgColors["root"] = color
|
663
|
+
self._svgColors["folder"] = color
|
664
|
+
if not CONFIG.iconColDocs:
|
665
|
+
self._svgColors["file"] = color
|
666
|
+
self._svgColors["title"] = color
|
667
|
+
self._svgColors["chapter"] = color
|
668
|
+
self._svgColors["scene"] = color
|
669
|
+
self._svgColors["note"] = color
|
638
670
|
|
639
671
|
return True
|
640
672
|
|
673
|
+
def setIconColor(self, key: str, color: QColor) -> None:
|
674
|
+
"""Set an icon colour for a named colour."""
|
675
|
+
self._qColors[key] = QColor(color)
|
676
|
+
self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8")
|
677
|
+
return
|
678
|
+
|
641
679
|
##
|
642
680
|
# Access Functions
|
643
681
|
##
|
644
682
|
|
645
|
-
def
|
646
|
-
"""
|
647
|
-
|
648
|
-
"""
|
649
|
-
if name in self._themeMap:
|
650
|
-
imgPath = self._themeMap[name]
|
651
|
-
elif name in self.IMAGE_MAP:
|
652
|
-
idx = 0 if self.mainTheme.isLightTheme else 1
|
653
|
-
imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx]
|
654
|
-
else:
|
655
|
-
logger.error("Decoration with name '%s' does not exist", name)
|
656
|
-
return QPixmap()
|
657
|
-
|
658
|
-
if not imgPath.is_file():
|
659
|
-
logger.error("Asset not found: %s", imgPath)
|
660
|
-
return QPixmap()
|
661
|
-
|
662
|
-
pixmap = QPixmap(str(imgPath))
|
663
|
-
tMode = Qt.TransformationMode.SmoothTransformation
|
664
|
-
if w is not None and h is not None:
|
665
|
-
return pixmap.scaled(w, h, Qt.AspectRatioMode.IgnoreAspectRatio, tMode)
|
666
|
-
elif w is None and h is not None:
|
667
|
-
return pixmap.scaledToHeight(h, tMode)
|
668
|
-
elif w is not None and h is None:
|
669
|
-
return pixmap.scaledToWidth(w, tMode)
|
670
|
-
|
671
|
-
return pixmap
|
683
|
+
def getIconColor(self, name: str) -> QColor:
|
684
|
+
"""Return an icon color."""
|
685
|
+
return QColor(self._qColors.get(name) or QtBlack)
|
672
686
|
|
673
|
-
def getIcon(self, name: str) -> QIcon:
|
687
|
+
def getIcon(self, name: str, color: str | None = None, w: int = 24, h: int = 24) -> QIcon:
|
674
688
|
"""Return an icon from the icon buffer, or load it."""
|
675
|
-
|
676
|
-
|
689
|
+
variant = f"{name}-{color}" if color else name
|
690
|
+
if (key := f"{variant}-{w}x{h}") in self._qIcons:
|
691
|
+
return self._qIcons[key]
|
677
692
|
else:
|
678
|
-
icon = self._loadIcon(name)
|
679
|
-
self._qIcons[
|
693
|
+
icon = self._loadIcon(name, color, w, h)
|
694
|
+
self._qIcons[key] = icon
|
695
|
+
logger.debug("Icon: %s", key)
|
680
696
|
return icon
|
681
697
|
|
682
|
-
def getToggleIcon(self, name: str, size: tuple[int, int]) -> QIcon:
|
683
|
-
"""Return a toggle icon from the icon buffer
|
698
|
+
def getToggleIcon(self, name: str, size: tuple[int, int], color: str | None = None) -> QIcon:
|
699
|
+
"""Return a toggle icon from the icon buffer, or load it."""
|
684
700
|
if name in self.TOGGLE_ICON_KEYS:
|
685
|
-
pOne = self.getPixmap(self.TOGGLE_ICON_KEYS[name][0], size)
|
686
|
-
pTwo = self.getPixmap(self.TOGGLE_ICON_KEYS[name][1], size)
|
701
|
+
pOne = self.getPixmap(self.TOGGLE_ICON_KEYS[name][0], size, color)
|
702
|
+
pTwo = self.getPixmap(self.TOGGLE_ICON_KEYS[name][1], size, color)
|
687
703
|
icon = QIcon()
|
688
704
|
icon.addPixmap(pOne, QIcon.Mode.Normal, QIcon.State.On)
|
689
705
|
icon.addPixmap(pTwo, QIcon.Mode.Normal, QIcon.State.Off)
|
690
706
|
return icon
|
691
707
|
return self._noIcon
|
692
708
|
|
693
|
-
def
|
694
|
-
|
695
|
-
|
696
|
-
"""
|
697
|
-
return self.getIcon(name).pixmap(size[0], size[1], QIcon.Mode.Normal)
|
698
|
-
|
699
|
-
def getItemIcon(self, tType: nwItemType, tClass: nwItemClass,
|
700
|
-
tLayout: nwItemLayout, hLevel: str = "H0") -> QIcon:
|
709
|
+
def getItemIcon(
|
710
|
+
self, tType: nwItemType, tClass: nwItemClass, tLayout: nwItemLayout, hLevel: str = "H0"
|
711
|
+
) -> QIcon:
|
701
712
|
"""Get the correct icon for a project item based on type, class
|
702
713
|
and heading level
|
703
714
|
"""
|
704
|
-
|
715
|
+
name = None
|
716
|
+
color = "default"
|
705
717
|
if tType == nwItemType.ROOT:
|
706
|
-
|
718
|
+
name = nwLabels.CLASS_ICON[tClass]
|
719
|
+
color = "root"
|
707
720
|
elif tType == nwItemType.FOLDER:
|
708
|
-
|
721
|
+
name = "prj_folder"
|
722
|
+
color = "folder"
|
709
723
|
elif tType == nwItemType.FILE:
|
710
|
-
iconName = "proj_document"
|
711
724
|
if tLayout == nwItemLayout.DOCUMENT:
|
712
725
|
if hLevel == "H1":
|
713
|
-
|
726
|
+
name = "prj_title"
|
727
|
+
color = "title"
|
714
728
|
elif hLevel == "H2":
|
715
|
-
|
729
|
+
name = "prj_chapter"
|
730
|
+
color = "chapter"
|
716
731
|
elif hLevel == "H3":
|
717
|
-
|
718
|
-
|
719
|
-
|
732
|
+
name = "prj_scene"
|
733
|
+
color = "scene"
|
734
|
+
else:
|
735
|
+
name = "prj_document"
|
736
|
+
color = "file"
|
720
737
|
elif tLayout == nwItemLayout.NOTE:
|
721
|
-
|
722
|
-
|
738
|
+
name = "prj_note"
|
739
|
+
color = "note"
|
740
|
+
if name is None:
|
723
741
|
return self._noIcon
|
724
742
|
|
725
|
-
return self.getIcon(
|
743
|
+
return self.getIcon(name, color)
|
744
|
+
|
745
|
+
def getPixmap(self, name: str, size: tuple[int, int], color: str | None = None) -> QPixmap:
|
746
|
+
"""Return an icon from the icon buffer as a QPixmap. If it
|
747
|
+
doesn't exist, return an empty QPixmap.
|
748
|
+
"""
|
749
|
+
w, h = size
|
750
|
+
return self.getIcon(name, color, w, h).pixmap(w, h, QIcon.Mode.Normal)
|
751
|
+
|
752
|
+
def getDecoration(self, name: str, w: int | None = None, h: int | None = None) -> QPixmap:
|
753
|
+
"""Load graphical decoration element based on the decoration
|
754
|
+
map or the icon map. This function always returns a QPixmap.
|
755
|
+
"""
|
756
|
+
if name in self.IMAGE_MAP:
|
757
|
+
idx = int(self.mainTheme.isDarkTheme)
|
758
|
+
imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx]
|
759
|
+
else:
|
760
|
+
logger.error("Decoration with name '%s' does not exist", name)
|
761
|
+
return QPixmap()
|
762
|
+
|
763
|
+
if not imgPath.is_file():
|
764
|
+
logger.error("Asset not found: %s", imgPath)
|
765
|
+
return QPixmap()
|
766
|
+
|
767
|
+
pixmap = QPixmap(str(imgPath))
|
768
|
+
tMode = Qt.TransformationMode.SmoothTransformation
|
769
|
+
if w is not None and h is not None:
|
770
|
+
return pixmap.scaled(w, h, Qt.AspectRatioMode.IgnoreAspectRatio, tMode)
|
771
|
+
elif w is None and h is not None:
|
772
|
+
return pixmap.scaledToHeight(h, tMode)
|
773
|
+
elif w is not None and h is None:
|
774
|
+
return pixmap.scaledToWidth(w, tMode)
|
775
|
+
|
776
|
+
return pixmap
|
726
777
|
|
727
778
|
def getHeaderDecoration(self, hLevel: int) -> QPixmap:
|
728
779
|
"""Get the decoration for a specific heading level."""
|
729
780
|
if not self._headerDec:
|
730
781
|
iPx = self.mainTheme.baseIconHeight
|
731
782
|
self._headerDec = [
|
732
|
-
self.
|
733
|
-
self.
|
734
|
-
self.
|
735
|
-
self.
|
736
|
-
self.
|
783
|
+
self._generateDecoration("file", iPx, 0),
|
784
|
+
self._generateDecoration("title", iPx, 0),
|
785
|
+
self._generateDecoration("chapter", iPx, 1),
|
786
|
+
self._generateDecoration("scene", iPx, 2),
|
787
|
+
self._generateDecoration("file", iPx, 3),
|
737
788
|
]
|
738
789
|
return self._headerDec[minmax(hLevel, 0, 4)]
|
739
790
|
|
@@ -742,47 +793,86 @@ class GuiIcons:
|
|
742
793
|
if not self._headerDecNarrow:
|
743
794
|
iPx = self.mainTheme.baseIconHeight
|
744
795
|
self._headerDecNarrow = [
|
745
|
-
self.
|
746
|
-
self.
|
747
|
-
self.
|
748
|
-
self.
|
749
|
-
self.
|
750
|
-
self.
|
796
|
+
self._generateDecoration("file", iPx, 0),
|
797
|
+
self._generateDecoration("title", iPx, 0),
|
798
|
+
self._generateDecoration("chapter", iPx, 0),
|
799
|
+
self._generateDecoration("scene", iPx, 0),
|
800
|
+
self._generateDecoration("file", iPx, 0),
|
801
|
+
self._generateDecoration("note", iPx, 0),
|
751
802
|
]
|
752
803
|
return self._headerDecNarrow[minmax(hLevel, 0, 5)]
|
753
804
|
|
805
|
+
def listThemes(self) -> list[tuple[str, str]]:
|
806
|
+
"""Scan the GUI icons folder and list all themes."""
|
807
|
+
if self._themeList:
|
808
|
+
return self._themeList
|
809
|
+
|
810
|
+
themes = []
|
811
|
+
for key, path in self._availThemes.items():
|
812
|
+
logger.debug("Checking icon theme '%s'", key)
|
813
|
+
if name := _loadIconName(path):
|
814
|
+
themes.append((key, name))
|
815
|
+
|
816
|
+
self._themeList = sorted(themes, key=_sortTheme)
|
817
|
+
|
818
|
+
return self._themeList
|
819
|
+
|
754
820
|
##
|
755
821
|
# Internal Functions
|
756
822
|
##
|
757
823
|
|
758
|
-
def _loadIcon(self, name: str) -> QIcon:
|
824
|
+
def _loadIcon(self, name: str, color: str | None = None, w: int = 24, h: int = 24) -> QIcon:
|
759
825
|
"""Load an icon from the assets themes folder. Is guaranteed to
|
760
826
|
return a QIcon.
|
761
827
|
"""
|
762
|
-
if name not in self.ICON_KEYS:
|
763
|
-
logger.error("Requested unknown icon name '%s'", name)
|
764
|
-
return self._noIcon
|
765
|
-
|
766
828
|
# If we just want the app icons, return right away
|
767
829
|
if name == "novelwriter":
|
768
|
-
return QIcon(str(
|
830
|
+
return QIcon(str(CONFIG.assetPath("icons") / "novelwriter.svg"))
|
769
831
|
elif name == "proj_nwx":
|
770
|
-
return QIcon(str(
|
832
|
+
return QIcon(str(CONFIG.assetPath("icons") / "x-novelwriter-project.svg"))
|
771
833
|
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
834
|
+
if svg := self._svgData.get(name, b""):
|
835
|
+
if fill := self._svgColors.get(color or "default"):
|
836
|
+
svg = svg.replace(b"#000000", fill)
|
837
|
+
pixmap = QPixmap(w, h)
|
838
|
+
pixmap.fill(QtTransparent)
|
839
|
+
pixmap.loadFromData(svg, "svg")
|
840
|
+
return QIcon(pixmap)
|
776
841
|
|
777
842
|
# If we didn't find one, give up and return an empty icon
|
778
843
|
logger.warning("Did not load an icon for '%s'", name)
|
779
844
|
|
780
845
|
return self._noIcon
|
781
846
|
|
847
|
+
def _generateDecoration(self, color: str, height: int, indent: int = 0) -> QPixmap:
|
848
|
+
"""Generate a decoration pixmap for novel headers."""
|
849
|
+
pixmap = QPixmap(48*indent + 12, 48)
|
850
|
+
pixmap.fill(QtTransparent)
|
851
|
+
|
852
|
+
path = QPainterPath()
|
853
|
+
path.addRoundedRect(48.0*indent, 2.0, 12.0, 44.0, 4.0, 4.0)
|
854
|
+
|
855
|
+
painter = QPainter(pixmap)
|
856
|
+
painter.setRenderHint(QtPaintAntiAlias)
|
857
|
+
if fill := self._svgColors.get(color or "default"):
|
858
|
+
painter.fillPath(path, QColor(fill.decode(encoding="utf-8")))
|
859
|
+
painter.end()
|
860
|
+
|
861
|
+
tMode = Qt.TransformationMode.SmoothTransformation
|
862
|
+
return pixmap.scaledToHeight(height, tMode)
|
863
|
+
|
782
864
|
|
783
865
|
# Module Functions
|
784
866
|
# ================
|
785
867
|
|
868
|
+
def _listConf(target: dict, path: Path, extension: str) -> None:
|
869
|
+
"""Scan for theme files and populate the dictionary."""
|
870
|
+
if path.is_dir():
|
871
|
+
for item in path.iterdir():
|
872
|
+
if item.is_file() and item.name.endswith(extension):
|
873
|
+
target[item.stem] = item
|
874
|
+
return
|
875
|
+
|
786
876
|
|
787
877
|
def _sortTheme(data: tuple[str, str]) -> str:
|
788
878
|
"""Key function for theme sorting."""
|
@@ -790,14 +880,27 @@ def _sortTheme(data: tuple[str, str]) -> str:
|
|
790
880
|
return f"*{name}" if key.startswith("default_") else name
|
791
881
|
|
792
882
|
|
793
|
-
def _loadInternalName(
|
883
|
+
def _loadInternalName(parser: NWConfigParser, path: str | Path) -> str:
|
794
884
|
"""Open a conf file and read the 'name' setting."""
|
795
885
|
try:
|
796
|
-
with open(
|
797
|
-
|
886
|
+
with open(path, mode="r", encoding="utf-8") as inFile:
|
887
|
+
parser.read_file(inFile)
|
888
|
+
return parser.rdStr("Main", "name", "")
|
798
889
|
except Exception:
|
799
|
-
logger.error("Could not
|
890
|
+
logger.error("Could not read file: %s", path)
|
800
891
|
logException()
|
801
|
-
|
892
|
+
return ""
|
802
893
|
|
803
|
-
|
894
|
+
|
895
|
+
def _loadIconName(path: Path) -> str:
|
896
|
+
"""Open an icons file and read the name setting."""
|
897
|
+
try:
|
898
|
+
with open(path, mode="r", encoding="utf-8") as icons:
|
899
|
+
for icon in icons:
|
900
|
+
key, _, value = icon.partition("=")
|
901
|
+
if key.strip() == "meta:name":
|
902
|
+
return value.strip()
|
903
|
+
except Exception:
|
904
|
+
logger.error("Could not read file: %s", path)
|
905
|
+
logException()
|
906
|
+
return ""
|