novelWriter 2.6.3__py3-none-any.whl → 2.7__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 +98 -75
- 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_de_DE.json +3 -1
- novelwriter/assets/i18n/project_en_GB.json +2 -0
- novelwriter/assets/i18n/project_en_US.json +2 -0
- novelwriter/assets/i18n/project_it_IT.json +2 -0
- novelwriter/assets/i18n/project_ja_JP.json +2 -0
- novelwriter/assets/i18n/project_nb_NO.json +2 -0
- novelwriter/assets/i18n/project_nn_NO.json +5 -0
- novelwriter/assets/i18n/project_pl_PL.json +2 -0
- novelwriter/assets/i18n/project_pt_BR.json +2 -0
- novelwriter/assets/i18n/project_ru_RU.json +2 -0
- novelwriter/assets/i18n/project_zh_CN.json +2 -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 +109 -0
- novelwriter/assets/icons/remix_outline.icons +109 -0
- novelwriter/assets/images/splash.png +0 -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/syntax/snazzy.conf +3 -3
- novelwriter/assets/text/credits_en.htm +12 -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/snazzy.conf +48 -0
- novelwriter/assets/themes/solarized_dark.conf +24 -8
- novelwriter/assets/themes/solarized_light.conf +22 -6
- novelwriter/common.py +43 -27
- novelwriter/config.py +201 -139
- novelwriter/constants.py +67 -36
- novelwriter/core/buildsettings.py +26 -17
- novelwriter/core/coretools.py +52 -41
- novelwriter/core/docbuild.py +20 -13
- novelwriter/core/document.py +2 -2
- novelwriter/core/index.py +166 -432
- novelwriter/core/indexdata.py +406 -0
- novelwriter/core/item.py +50 -32
- novelwriter/core/itemmodel.py +43 -38
- novelwriter/core/novelmodel.py +225 -0
- novelwriter/core/options.py +1 -1
- novelwriter/core/project.py +24 -25
- novelwriter/core/projectdata.py +47 -29
- novelwriter/core/projectxml.py +18 -8
- novelwriter/core/sessions.py +32 -15
- novelwriter/core/spellcheck.py +4 -3
- novelwriter/core/status.py +12 -15
- novelwriter/core/storage.py +1 -1
- novelwriter/core/tree.py +55 -13
- novelwriter/dialogs/about.py +19 -22
- novelwriter/dialogs/docmerge.py +23 -24
- novelwriter/dialogs/docsplit.py +26 -26
- novelwriter/dialogs/editlabel.py +19 -20
- novelwriter/dialogs/preferences.py +143 -69
- novelwriter/dialogs/projectsettings.py +51 -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 +24 -20
- 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 +31 -41
- novelwriter/extensions/switchbox.py +8 -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 +37 -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 +314 -305
- 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 +11 -14
- novelwriter/gui/mainmenu.py +146 -145
- novelwriter/gui/noveltree.py +246 -406
- novelwriter/gui/outline.py +141 -72
- novelwriter/gui/projtree.py +139 -132
- novelwriter/gui/search.py +34 -31
- novelwriter/gui/sidebar.py +13 -18
- novelwriter/gui/statusbar.py +27 -21
- novelwriter/gui/theme.py +554 -436
- novelwriter/guimain.py +56 -38
- novelwriter/shared.py +32 -23
- novelwriter/splash.py +74 -0
- novelwriter/text/comments.py +70 -0
- novelwriter/text/patterns.py +4 -4
- novelwriter/tools/dictionaries.py +20 -29
- novelwriter/tools/lipsum.py +18 -18
- novelwriter/tools/manusbuild.py +39 -42
- novelwriter/tools/manuscript.py +102 -115
- novelwriter/tools/manussettings.py +129 -102
- novelwriter/tools/noveldetails.py +60 -72
- novelwriter/tools/welcome.py +57 -75
- novelwriter/tools/writingstats.py +112 -102
- novelwriter/types.py +5 -7
- {novelWriter-2.6.3.dist-info → novelwriter-2.7.dist-info}/METADATA +6 -6
- novelwriter-2.7.dist-info/RECORD +162 -0
- {novelWriter-2.6.3.dist-info → novelwriter-2.7.dist-info}/WHEEL +1 -1
- novelWriter-2.6.3.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.3.dist-info → novelwriter-2.7.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.6.3.dist-info → novelwriter-2.7.dist-info/licenses}/LICENSE.md +0 -0
- {novelWriter-2.6.3.dist-info → novelwriter-2.7.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,30 +130,16 @@ class GuiTheme:
|
|
123
130
|
self._availSyntax: dict[str, Path] = {}
|
124
131
|
self._styleSheets: dict[str, str] = {}
|
125
132
|
|
126
|
-
self._listConf(self._availSyntax, CONFIG.assetPath("syntax"))
|
127
|
-
self._listConf(self._availThemes, CONFIG.assetPath("themes"))
|
128
|
-
self._listConf(self._availSyntax, CONFIG.dataPath("syntax"))
|
129
|
-
self._listConf(self._availThemes, CONFIG.dataPath("themes"))
|
130
|
-
|
131
|
-
self.loadTheme()
|
132
|
-
self.loadSyntax()
|
133
|
-
|
134
133
|
# Icon Functions
|
135
134
|
self.getIcon = self.iconCache.getIcon
|
136
135
|
self.getPixmap = self.iconCache.getPixmap
|
137
136
|
self.getItemIcon = self.iconCache.getItemIcon
|
137
|
+
self.getIconColor = self.iconCache.getIconColor
|
138
138
|
self.getToggleIcon = self.iconCache.getToggleIcon
|
139
|
-
self.
|
139
|
+
self.getDecoration = self.iconCache.getDecoration
|
140
140
|
self.getHeaderDecoration = self.iconCache.getHeaderDecoration
|
141
141
|
self.getHeaderDecorationNarrow = self.iconCache.getHeaderDecorationNarrow
|
142
142
|
|
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
143
|
# Fonts
|
151
144
|
self.guiFont = QApplication.font()
|
152
145
|
self.guiFontB = QApplication.font()
|
@@ -161,9 +154,9 @@ class GuiTheme:
|
|
161
154
|
fHeight = qMetric.height()
|
162
155
|
fAscent = qMetric.ascent()
|
163
156
|
self.fontPointSize = self.guiFont.pointSizeF()
|
164
|
-
self.fontPixelSize =
|
165
|
-
self.baseIconHeight =
|
166
|
-
self.baseButtonHeight =
|
157
|
+
self.fontPixelSize = round(fHeight)
|
158
|
+
self.baseIconHeight = round(fAscent)
|
159
|
+
self.baseButtonHeight = round(1.35*fAscent)
|
167
160
|
self.textNHeight = qMetric.boundingRect("N").height()
|
168
161
|
self.textNWidth = qMetric.boundingRect("N").width()
|
169
162
|
|
@@ -185,6 +178,15 @@ class GuiTheme:
|
|
185
178
|
logger.debug("Text 'N' Height: %d", self.textNHeight)
|
186
179
|
logger.debug("Text 'N' Width: %d", self.textNWidth)
|
187
180
|
|
181
|
+
# Process Themes
|
182
|
+
_listConf(self._availSyntax, CONFIG.assetPath("syntax"), ".conf")
|
183
|
+
_listConf(self._availThemes, CONFIG.assetPath("themes"), ".conf")
|
184
|
+
_listConf(self._availSyntax, CONFIG.dataPath("syntax"), ".conf")
|
185
|
+
_listConf(self._availThemes, CONFIG.dataPath("themes"), ".conf")
|
186
|
+
|
187
|
+
self.loadTheme()
|
188
|
+
self.loadSyntax()
|
189
|
+
|
188
190
|
return
|
189
191
|
|
190
192
|
##
|
@@ -199,7 +201,7 @@ class GuiTheme:
|
|
199
201
|
qMetrics = QFontMetrics(font)
|
200
202
|
else:
|
201
203
|
qMetrics = QFontMetrics(self.guiFont)
|
202
|
-
return
|
204
|
+
return ceil(qMetrics.boundingRect(text).width())
|
203
205
|
|
204
206
|
##
|
205
207
|
# Theme Methods
|
@@ -207,43 +209,67 @@ class GuiTheme:
|
|
207
209
|
|
208
210
|
def loadTheme(self) -> bool:
|
209
211
|
"""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:
|
212
|
+
theme = CONFIG.guiTheme
|
213
|
+
if theme not in self._availThemes:
|
214
|
+
logger.error("Could not find GUI theme '%s'", theme)
|
215
|
+
theme = DEF_GUI
|
216
|
+
CONFIG.guiTheme = theme
|
217
|
+
|
218
|
+
if not (file := self._availThemes.get(theme)):
|
218
219
|
logger.error("Could not load GUI theme")
|
219
220
|
return False
|
220
221
|
|
221
|
-
|
222
|
-
logger.info("Loading GUI theme '%s'",
|
222
|
+
CONFIG.splashMessage("Loading GUI theme ...")
|
223
|
+
logger.info("Loading GUI theme '%s'", theme)
|
223
224
|
parser = NWConfigParser()
|
224
225
|
try:
|
225
|
-
with open(
|
226
|
-
parser.read_file(
|
226
|
+
with open(file, mode="r", encoding="utf-8") as fo:
|
227
|
+
parser.read_file(fo)
|
227
228
|
except Exception:
|
228
|
-
logger.error("Could not
|
229
|
+
logger.error("Could not read file: %s", file)
|
229
230
|
logException()
|
230
231
|
return False
|
231
232
|
|
232
233
|
# Reset Palette
|
233
|
-
self.
|
234
|
-
self._resetGuiColors()
|
234
|
+
self._resetTheme()
|
235
235
|
|
236
236
|
# Main
|
237
237
|
sec = "Main"
|
238
|
+
meta = ThemeMeta()
|
238
239
|
if parser.has_section(sec):
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
240
|
+
meta.name = parser.rdStr(sec, "name", "")
|
241
|
+
meta.description = parser.rdStr(sec, "description", "N/A")
|
242
|
+
meta.author = parser.rdStr(sec, "author", "N/A")
|
243
|
+
meta.credit = parser.rdStr(sec, "credit", "N/A")
|
244
|
+
meta.url = parser.rdStr(sec, "url", "")
|
245
|
+
meta.license = parser.rdStr(sec, "license", "N/A")
|
246
|
+
meta.licenseUrl = parser.rdStr(sec, "licenseurl", "")
|
247
|
+
|
248
|
+
self.themeMeta = meta
|
249
|
+
|
250
|
+
# Icons
|
251
|
+
sec = "Icons"
|
252
|
+
if parser.has_section(sec):
|
253
|
+
self.iconCache.setIconColor("default", self._parseColor(parser, sec, "default"))
|
254
|
+
self.iconCache.setIconColor("faded", self._parseColor(parser, sec, "faded"))
|
255
|
+
self.iconCache.setIconColor("red", self._parseColor(parser, sec, "red"))
|
256
|
+
self.iconCache.setIconColor("orange", self._parseColor(parser, sec, "orange"))
|
257
|
+
self.iconCache.setIconColor("yellow", self._parseColor(parser, sec, "yellow"))
|
258
|
+
self.iconCache.setIconColor("green", self._parseColor(parser, sec, "green"))
|
259
|
+
self.iconCache.setIconColor("aqua", self._parseColor(parser, sec, "aqua"))
|
260
|
+
self.iconCache.setIconColor("blue", self._parseColor(parser, sec, "blue"))
|
261
|
+
self.iconCache.setIconColor("purple", self._parseColor(parser, sec, "purple"))
|
262
|
+
|
263
|
+
# Project
|
264
|
+
sec = "Project"
|
265
|
+
if parser.has_section(sec):
|
266
|
+
self.iconCache.setIconColor("root", self._parseColor(parser, sec, "root"))
|
267
|
+
self.iconCache.setIconColor("folder", self._parseColor(parser, sec, "folder"))
|
268
|
+
self.iconCache.setIconColor("file", self._parseColor(parser, sec, "file"))
|
269
|
+
self.iconCache.setIconColor("title", self._parseColor(parser, sec, "title"))
|
270
|
+
self.iconCache.setIconColor("chapter", self._parseColor(parser, sec, "chapter"))
|
271
|
+
self.iconCache.setIconColor("scene", self._parseColor(parser, sec, "scene"))
|
272
|
+
self.iconCache.setIconColor("note", self._parseColor(parser, sec, "note"))
|
247
273
|
|
248
274
|
# Palette
|
249
275
|
sec = "Palette"
|
@@ -266,101 +292,141 @@ class GuiTheme:
|
|
266
292
|
# GUI
|
267
293
|
sec = "GUI"
|
268
294
|
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")
|
295
|
+
self.helpText = self._parseColor(parser, sec, "helptext")
|
296
|
+
self.fadedText = self._parseColor(parser, sec, "fadedtext")
|
297
|
+
self.errorText = self._parseColor(parser, sec, "errortext")
|
275
298
|
|
276
299
|
# 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
|
-
|
300
|
+
# Based on: https://github.com/qt/qtbase/blob/dev/src/gui/kernel/qplatformtheme.cpp
|
301
|
+
text = self._guiPalette.text().color()
|
302
|
+
window = self._guiPalette.window().color()
|
303
|
+
highlight = self._guiPalette.highlight().color()
|
304
|
+
isDark = text.lightnessF() > window.lightnessF()
|
305
|
+
|
306
|
+
QtColActive = QPalette.ColorGroup.Active
|
307
|
+
QtColInactive = QPalette.ColorGroup.Inactive
|
308
|
+
QtColDisabled = QPalette.ColorGroup.Disabled
|
309
|
+
|
310
|
+
if window.lightnessF() < 0.15:
|
311
|
+
# If window is too dark, we need a lighter ref colour for shades
|
312
|
+
ref = QColor.fromHslF(window.hueF(), window.saturationF(), 0.15, window.alphaF())
|
313
|
+
else:
|
314
|
+
ref = window
|
315
|
+
|
316
|
+
light = ref.lighter(150)
|
317
|
+
mid = ref.darker(130)
|
318
|
+
midLight = mid.lighter(110)
|
319
|
+
dark = ref.darker(150)
|
320
|
+
shadow = dark.darker(135)
|
321
|
+
darkOff = dark.darker(150)
|
322
|
+
shadowOff = ref.darker(150)
|
323
|
+
|
324
|
+
grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140)
|
325
|
+
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190)
|
326
|
+
|
327
|
+
placeholder = QColor(text)
|
328
|
+
placeholder.setAlpha(128)
|
329
|
+
|
330
|
+
self._guiPalette.setBrush(QPalette.ColorRole.Light, light)
|
331
|
+
self._guiPalette.setBrush(QPalette.ColorRole.Mid, mid)
|
332
|
+
self._guiPalette.setBrush(QPalette.ColorRole.Midlight, midLight)
|
333
|
+
self._guiPalette.setBrush(QPalette.ColorRole.Dark, dark)
|
334
|
+
self._guiPalette.setBrush(QPalette.ColorRole.Shadow, shadow)
|
335
|
+
|
336
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Text, dimmed)
|
337
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.WindowText, dimmed)
|
338
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.ButtonText, dimmed)
|
339
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Base, window)
|
340
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Dark, darkOff)
|
341
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Shadow, shadowOff)
|
342
|
+
|
343
|
+
self._guiPalette.setBrush(QPalette.ColorRole.PlaceholderText, placeholder)
|
344
|
+
|
345
|
+
self._guiPalette.setBrush(QtColActive, QPalette.ColorRole.Highlight, highlight)
|
346
|
+
self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Highlight, highlight)
|
347
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Highlight, grey)
|
348
|
+
|
349
|
+
if CONFIG.verQtValue >= 0x060600:
|
350
|
+
self._guiPalette.setBrush(QtColActive, QPalette.ColorRole.Accent, highlight)
|
351
|
+
self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Accent, highlight)
|
352
|
+
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey)
|
353
|
+
|
354
|
+
# Load icons after the theme is parsed
|
355
|
+
self.iconCache.loadTheme(CONFIG.iconTheme)
|
356
|
+
|
357
|
+
# Finalise
|
358
|
+
self.isDarkTheme = isDark
|
299
359
|
QApplication.setPalette(self._guiPalette)
|
300
|
-
|
301
|
-
# Reset stylesheets so that they are regenerated
|
302
360
|
self._buildStyleSheets(self._guiPalette)
|
303
361
|
|
362
|
+
CONFIG.splashMessage(f"Loaded GUI theme: {meta.name}")
|
363
|
+
|
304
364
|
return True
|
305
365
|
|
306
366
|
def loadSyntax(self) -> bool:
|
307
367
|
"""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:
|
368
|
+
theme = CONFIG.guiSyntax
|
369
|
+
if theme not in self._availSyntax:
|
370
|
+
logger.error("Could not find syntax theme '%s'", theme)
|
371
|
+
theme = DEF_SYNTAX
|
372
|
+
CONFIG.guiSyntax = theme
|
373
|
+
|
374
|
+
if not (file := self._availSyntax.get(theme)):
|
316
375
|
logger.error("Could not load syntax theme")
|
317
376
|
return False
|
318
377
|
|
319
|
-
|
320
|
-
|
321
|
-
|
378
|
+
CONFIG.splashMessage("Loading syntax theme ...")
|
379
|
+
logger.info("Loading syntax theme '%s'", theme)
|
380
|
+
parser = NWConfigParser()
|
322
381
|
try:
|
323
|
-
with open(
|
324
|
-
|
382
|
+
with open(file, mode="r", encoding="utf-8") as fo:
|
383
|
+
parser.read_file(fo)
|
325
384
|
except Exception:
|
326
|
-
logger.error("Could not
|
385
|
+
logger.error("Could not read file: %s", file)
|
327
386
|
logException()
|
328
387
|
return False
|
329
388
|
|
330
389
|
# Main
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
390
|
+
sec = "Main"
|
391
|
+
meta = ThemeMeta()
|
392
|
+
if parser.has_section(sec):
|
393
|
+
meta.name = parser.rdStr(sec, "name", "")
|
394
|
+
meta.description = parser.rdStr(sec, "description", "N/A")
|
395
|
+
meta.author = parser.rdStr(sec, "author", "N/A")
|
396
|
+
meta.credit = parser.rdStr(sec, "credit", "N/A")
|
397
|
+
meta.url = parser.rdStr(sec, "url", "")
|
398
|
+
meta.license = parser.rdStr(sec, "license", "N/A")
|
399
|
+
meta.licenseUrl = parser.rdStr(sec, "licenseurl", "")
|
340
400
|
|
341
401
|
# Syntax
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
402
|
+
sec = "Syntax"
|
403
|
+
syntax = SyntaxColors()
|
404
|
+
if parser.has_section(sec):
|
405
|
+
syntax.back = self._parseColor(parser, sec, "background")
|
406
|
+
syntax.text = self._parseColor(parser, sec, "text")
|
407
|
+
syntax.link = self._parseColor(parser, sec, "link")
|
408
|
+
syntax.head = self._parseColor(parser, sec, "headertext")
|
409
|
+
syntax.headH = self._parseColor(parser, sec, "headertag")
|
410
|
+
syntax.emph = self._parseColor(parser, sec, "emphasis")
|
411
|
+
syntax.dialN = self._parseColor(parser, sec, "dialog")
|
412
|
+
syntax.dialA = self._parseColor(parser, sec, "altdialog")
|
413
|
+
syntax.hidden = self._parseColor(parser, sec, "hidden")
|
414
|
+
syntax.note = self._parseColor(parser, sec, "note")
|
415
|
+
syntax.code = self._parseColor(parser, sec, "shortcode")
|
416
|
+
syntax.key = self._parseColor(parser, sec, "keyword")
|
417
|
+
syntax.tag = self._parseColor(parser, sec, "tag")
|
418
|
+
syntax.val = self._parseColor(parser, sec, "value")
|
419
|
+
syntax.opt = self._parseColor(parser, sec, "optional")
|
420
|
+
syntax.spell = self._parseColor(parser, sec, "spellcheckline")
|
421
|
+
syntax.error = self._parseColor(parser, sec, "errorline")
|
422
|
+
syntax.repTag = self._parseColor(parser, sec, "replacetag")
|
423
|
+
syntax.mod = self._parseColor(parser, sec, "modifier")
|
424
|
+
syntax.mark = self._parseColor(parser, sec, "texthighlight")
|
425
|
+
|
426
|
+
CONFIG.splashMessage(f"Loaded syntax theme: {meta.name}")
|
427
|
+
|
428
|
+
self.syntaxMeta = meta
|
429
|
+
self.syntaxTheme = syntax
|
364
430
|
|
365
431
|
return True
|
366
432
|
|
@@ -369,14 +435,14 @@ class GuiTheme:
|
|
369
435
|
if self._themeList:
|
370
436
|
return self._themeList
|
371
437
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
if
|
377
|
-
|
438
|
+
themes = []
|
439
|
+
parser = NWConfigParser()
|
440
|
+
for key, path in self._availThemes.items():
|
441
|
+
logger.debug("Checking theme config '%s'", key)
|
442
|
+
if name := _loadInternalName(parser, path):
|
443
|
+
themes.append((key, name))
|
378
444
|
|
379
|
-
self._themeList = sorted(
|
445
|
+
self._themeList = sorted(themes, key=_sortTheme)
|
380
446
|
|
381
447
|
return self._themeList
|
382
448
|
|
@@ -385,14 +451,14 @@ class GuiTheme:
|
|
385
451
|
if self._syntaxList:
|
386
452
|
return self._syntaxList
|
387
453
|
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
if
|
393
|
-
|
454
|
+
themes = []
|
455
|
+
parser = NWConfigParser()
|
456
|
+
for key, path in self._availSyntax.items():
|
457
|
+
logger.debug("Checking theme syntax '%s'", key)
|
458
|
+
if name := _loadInternalName(parser, path):
|
459
|
+
themes.append((key, name))
|
394
460
|
|
395
|
-
self._syntaxList = sorted(
|
461
|
+
self._syntaxList = sorted(themes, key=_sortTheme)
|
396
462
|
|
397
463
|
return self._syntaxList
|
398
464
|
|
@@ -404,67 +470,92 @@ class GuiTheme:
|
|
404
470
|
# Internal Functions
|
405
471
|
##
|
406
472
|
|
407
|
-
def
|
473
|
+
def _resetTheme(self) -> None:
|
408
474
|
"""Reset GUI colours to default values."""
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
if
|
420
|
-
|
475
|
+
palette = QPalette()
|
476
|
+
|
477
|
+
text = palette.color(QPalette.ColorRole.Text)
|
478
|
+
window = palette.color(QPalette.ColorRole.Window)
|
479
|
+
isDark = text.lightnessF() > window.lightnessF()
|
480
|
+
|
481
|
+
# Reset GUI Palette
|
482
|
+
faded = QColor(128, 128, 128)
|
483
|
+
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190)
|
484
|
+
red = QColor(242, 119, 122) if isDark else QColor(240, 40, 41)
|
485
|
+
orange = QColor(249, 145, 57) if isDark else QColor(245, 135, 31)
|
486
|
+
yellow = QColor(255, 204, 102) if isDark else QColor(234, 183, 0)
|
487
|
+
green = QColor(153, 204, 153) if isDark else QColor(113, 140, 0)
|
488
|
+
aqua = QColor(102, 204, 204) if isDark else QColor(62, 153, 159)
|
489
|
+
blue = QColor(102, 153, 204) if isDark else QColor(66, 113, 174)
|
490
|
+
purple = QColor(204, 153, 204) if isDark else QColor(137, 89, 168)
|
491
|
+
|
492
|
+
# Text Colours
|
493
|
+
self.helpText = dimmed
|
494
|
+
self.fadedText = faded
|
495
|
+
self.errorText = red
|
496
|
+
|
497
|
+
self._guiPalette = palette
|
498
|
+
|
499
|
+
# Reset Icons
|
500
|
+
icons = self.iconCache
|
501
|
+
icons.clear()
|
502
|
+
icons.setIconColor("default", text)
|
503
|
+
icons.setIconColor("faded", faded)
|
504
|
+
icons.setIconColor("red", red)
|
505
|
+
icons.setIconColor("orange", orange)
|
506
|
+
icons.setIconColor("yellow", yellow)
|
507
|
+
icons.setIconColor("green", green)
|
508
|
+
icons.setIconColor("aqua", aqua)
|
509
|
+
icons.setIconColor("blue", blue)
|
510
|
+
icons.setIconColor("purple", purple)
|
511
|
+
icons.setIconColor("root", blue)
|
512
|
+
icons.setIconColor("folder", yellow)
|
513
|
+
icons.setIconColor("file", text)
|
514
|
+
icons.setIconColor("title", green)
|
515
|
+
icons.setIconColor("chapter", red)
|
516
|
+
icons.setIconColor("scene", blue)
|
517
|
+
icons.setIconColor("note", yellow)
|
421
518
|
|
422
|
-
|
423
|
-
if checkFile.is_file() and checkFile.name.endswith(".conf"):
|
424
|
-
targetDict[checkFile.name[:-5]] = checkFile
|
425
|
-
|
426
|
-
return True
|
519
|
+
return
|
427
520
|
|
428
|
-
def
|
521
|
+
def _parseColor(self, parser: NWConfigParser, section: str, name: str) -> QColor:
|
429
522
|
"""Parse a colour value from a config string."""
|
430
523
|
return QColor(*parser.rdIntList(section, name, [0, 0, 0, 255]))
|
431
524
|
|
432
|
-
def _setPalette(
|
433
|
-
|
525
|
+
def _setPalette(
|
526
|
+
self, parser: NWConfigParser, section: str, name: str, value: QPalette.ColorRole
|
527
|
+
) -> None:
|
434
528
|
"""Set a palette colour value from a config string."""
|
435
|
-
self._guiPalette.
|
529
|
+
self._guiPalette.setBrush(value, self._parseColor(parser, section, name))
|
436
530
|
return
|
437
531
|
|
438
532
|
def _buildStyleSheets(self, palette: QPalette) -> None:
|
439
533
|
"""Build default style sheets."""
|
440
534
|
self._styleSheets = {}
|
441
535
|
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
tCol = palette.text().color()
|
448
|
-
hCol = palette.highlight().color()
|
536
|
+
text = palette.text().color()
|
537
|
+
text.setAlpha(48)
|
538
|
+
tCol = text.name(QtHexArgb)
|
539
|
+
hCol = palette.highlight().color().name(QtHexArgb)
|
449
540
|
|
450
541
|
# Flat Tab Widget and Tab Bar:
|
451
542
|
self._styleSheets[STYLES_FLAT_TABS] = (
|
452
543
|
"QTabWidget::pane {border: 0;} "
|
453
|
-
|
454
|
-
f"QTabWidget QTabBar::tab:selected {{color: {
|
544
|
+
"QTabWidget QTabBar::tab {border: 0; padding: 4px 8px;} "
|
545
|
+
f"QTabWidget QTabBar::tab:selected {{color: {hCol};}} "
|
455
546
|
)
|
456
547
|
|
457
548
|
# Minimal Tool Button
|
458
549
|
self._styleSheets[STYLES_MIN_TOOLBUTTON] = (
|
459
|
-
|
460
|
-
f"QToolButton:hover {{border: none; background: {
|
550
|
+
"QToolButton {padding: 2px; margin: 0; border: none; background: transparent;} "
|
551
|
+
f"QToolButton:hover {{border: none; background: {tCol};}} "
|
461
552
|
"QToolButton::menu-indicator {image: none;} "
|
462
553
|
)
|
463
554
|
|
464
555
|
# Big Tool Button
|
465
556
|
self._styleSheets[STYLES_BIG_TOOLBUTTON] = (
|
466
|
-
|
467
|
-
f"QToolButton:hover {{border: none; background: {
|
557
|
+
"QToolButton {padding: 6px; margin: 0; border: none; background: transparent;} "
|
558
|
+
f"QToolButton:hover {{border: none; background: {tCol};}} "
|
468
559
|
"QToolButton::menu-indicator {image: none;} "
|
469
560
|
)
|
470
561
|
|
@@ -473,64 +564,24 @@ class GuiTheme:
|
|
473
564
|
|
474
565
|
class GuiIcons:
|
475
566
|
"""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.
|
567
|
+
and provides a simple interface for requesting icons.
|
478
568
|
|
479
|
-
Icons are
|
480
|
-
requests.
|
481
|
-
|
482
|
-
definition of all keys are checked when the theme is loaded.
|
483
|
-
|
484
|
-
When an icon is requested, the icon is loaded and cached. If it is
|
485
|
-
missing, a blank icon is returned and a warning issued.
|
569
|
+
Icons are generated from SVG on first request, and then cached for
|
570
|
+
further requests. If the icon is not defined, a placeholder icon is
|
571
|
+
returned instead.
|
486
572
|
"""
|
487
573
|
|
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
|
-
}
|
574
|
+
__slots__ = (
|
575
|
+
"_availThemes", "_headerDec", "_headerDecNarrow", "_noIcon",
|
576
|
+
"_qColors", "_qIcons", "_svgColors", "_svgData", "_themeList",
|
577
|
+
"mainTheme", "themeMeta",
|
578
|
+
)
|
526
579
|
|
527
|
-
TOGGLE_ICON_KEYS: dict[str, tuple[str, str]] = {
|
528
|
-
"sticky": ("sticky-on", "sticky-off"),
|
580
|
+
TOGGLE_ICON_KEYS: Final[dict[str, tuple[str, str]]] = {
|
529
581
|
"bullet": ("bullet-on", "bullet-off"),
|
530
582
|
"unfold": ("unfold-show", "unfold-hide"),
|
531
583
|
}
|
532
|
-
|
533
|
-
IMAGE_MAP: dict[str, tuple[str, str]] = {
|
584
|
+
IMAGE_MAP: Final[dict[str, tuple[str, str]]] = {
|
534
585
|
"welcome": ("welcome-light.jpg", "welcome-dark.jpg"),
|
535
586
|
"nw-text": ("novelwriter-text-light.svg", "novelwriter-text-dark.svg"),
|
536
587
|
}
|
@@ -538,202 +589,217 @@ class GuiIcons:
|
|
538
589
|
def __init__(self, mainTheme: GuiTheme) -> None:
|
539
590
|
|
540
591
|
self.mainTheme = mainTheme
|
592
|
+
self.themeMeta = ThemeMeta()
|
541
593
|
|
542
594
|
# Storage
|
595
|
+
self._svgData: dict[str, bytes] = {}
|
596
|
+
self._svgColors: dict[str, bytes] = {}
|
597
|
+
self._qColors: dict[str, QColor] = {}
|
543
598
|
self._qIcons: dict[str, QIcon] = {}
|
544
|
-
self._themeMap: dict[str, Path] = {}
|
545
599
|
self._headerDec: list[QPixmap] = []
|
546
600
|
self._headerDecNarrow: list[QPixmap] = []
|
547
601
|
|
548
602
|
# Icon Theme Path
|
549
|
-
self.
|
550
|
-
self.
|
603
|
+
self._availThemes: dict[str, Path] = {}
|
604
|
+
self._themeList: list[tuple[str, str]] = []
|
551
605
|
|
552
606
|
# None Icon
|
553
|
-
self._noIcon = QIcon(str(
|
607
|
+
self._noIcon = QIcon(str(CONFIG.assetPath("icons") / "none.svg"))
|
608
|
+
|
609
|
+
_listConf(self._availThemes, CONFIG.assetPath("icons"), ".icons")
|
610
|
+
_listConf(self._availThemes, CONFIG.dataPath("icons"), ".icons")
|
554
611
|
|
555
|
-
|
556
|
-
self.themeName = ""
|
557
|
-
self.themeDescription = ""
|
558
|
-
self.themeAuthor = ""
|
559
|
-
self.themeCredit = ""
|
560
|
-
self.themeUrl = ""
|
561
|
-
self.themeLicense = ""
|
562
|
-
self.themeLicenseUrl = ""
|
612
|
+
return
|
563
613
|
|
614
|
+
def clear(self) -> None:
|
615
|
+
"""Clear the icon cache."""
|
616
|
+
self._svgData = {}
|
617
|
+
self._svgColors = {}
|
618
|
+
self._qColors = {}
|
619
|
+
self._qIcons = {}
|
620
|
+
self._headerDec = []
|
621
|
+
self._headerDecNarrow = []
|
622
|
+
self.themeMeta = ThemeMeta()
|
564
623
|
return
|
565
624
|
|
566
625
|
##
|
567
626
|
# Actions
|
568
627
|
##
|
569
628
|
|
570
|
-
def loadTheme(self,
|
629
|
+
def loadTheme(self, theme: str) -> bool:
|
571
630
|
"""Update the theme map. This is more of an init, since many of
|
572
631
|
the GUI icons cannot really be replaced without writing specific
|
573
632
|
update functions for the classes where they're used.
|
574
633
|
"""
|
575
|
-
self.
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
# Config File
|
587
|
-
confParser = NWConfigParser()
|
634
|
+
if theme not in self._availThemes:
|
635
|
+
logger.error("Could not find icon theme '%s'", theme)
|
636
|
+
theme = DEF_ICONS
|
637
|
+
CONFIG.iconTheme = theme
|
638
|
+
|
639
|
+
if not (file := self._availThemes.get(theme)):
|
640
|
+
logger.error("Could not load icon theme")
|
641
|
+
return False
|
642
|
+
|
643
|
+
CONFIG.splashMessage("Loading icon theme ...")
|
644
|
+
logger.info("Loading icon theme '%s'", theme)
|
588
645
|
try:
|
589
|
-
|
590
|
-
|
646
|
+
meta = ThemeMeta()
|
647
|
+
with open(file, mode="r", encoding="utf-8") as icons:
|
648
|
+
for icon in icons:
|
649
|
+
bits = icon.partition("=")
|
650
|
+
key = bits[0].strip()
|
651
|
+
value = bits[2].strip()
|
652
|
+
if key and value:
|
653
|
+
if key.startswith("icon:"):
|
654
|
+
self._svgData[key[5:]] = value.encode("utf-8")
|
655
|
+
elif key == "meta:name":
|
656
|
+
meta.name = value
|
657
|
+
elif key == "meta:author":
|
658
|
+
meta.author = value
|
659
|
+
elif key == "meta:license":
|
660
|
+
meta.license = value
|
661
|
+
self.themeMeta = meta
|
591
662
|
except Exception:
|
592
|
-
logger.error("Could not
|
663
|
+
logger.error("Could not read file: %s", file)
|
593
664
|
logException()
|
594
665
|
return False
|
595
666
|
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
self.
|
603
|
-
self.
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
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 = []
|
667
|
+
CONFIG.splashMessage(f"Loaded icon theme: {meta.name}")
|
668
|
+
CONFIG.splashMessage("Generating additional icons ...")
|
669
|
+
|
670
|
+
# Set colour overrides for project item icons
|
671
|
+
if (override := CONFIG.iconColTree) != "theme":
|
672
|
+
color = self._svgColors.get(override, b"#000000")
|
673
|
+
self._svgColors["root"] = color
|
674
|
+
self._svgColors["folder"] = color
|
675
|
+
if not CONFIG.iconColDocs:
|
676
|
+
self._svgColors["file"] = color
|
677
|
+
self._svgColors["title"] = color
|
678
|
+
self._svgColors["chapter"] = color
|
679
|
+
self._svgColors["scene"] = color
|
680
|
+
self._svgColors["note"] = color
|
681
|
+
|
682
|
+
# Populate generated icons cache
|
683
|
+
self.getHeaderDecoration(0)
|
684
|
+
self.getHeaderDecorationNarrow(0)
|
638
685
|
|
639
686
|
return True
|
640
687
|
|
688
|
+
def setIconColor(self, key: str, color: QColor) -> None:
|
689
|
+
"""Set an icon colour for a named colour."""
|
690
|
+
self._qColors[key] = QColor(color)
|
691
|
+
self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8")
|
692
|
+
return
|
693
|
+
|
641
694
|
##
|
642
695
|
# Access Functions
|
643
696
|
##
|
644
697
|
|
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()
|
698
|
+
def getIconColor(self, name: str) -> QColor:
|
699
|
+
"""Return an icon color."""
|
700
|
+
return QColor(self._qColors.get(name) or QtBlack)
|
661
701
|
|
662
|
-
|
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
|
672
|
-
|
673
|
-
def getIcon(self, name: str) -> QIcon:
|
702
|
+
def getIcon(self, name: str, color: str | None = None, w: int = 24, h: int = 24) -> QIcon:
|
674
703
|
"""Return an icon from the icon buffer, or load it."""
|
675
|
-
|
676
|
-
|
704
|
+
variant = f"{name}-{color}" if color else name
|
705
|
+
if (key := f"{variant}-{w}x{h}") in self._qIcons:
|
706
|
+
return self._qIcons[key]
|
677
707
|
else:
|
678
|
-
icon = self._loadIcon(name)
|
679
|
-
self._qIcons[
|
708
|
+
icon = self._loadIcon(name, color, w, h)
|
709
|
+
self._qIcons[key] = icon
|
710
|
+
logger.debug("Icon: %s", key)
|
680
711
|
return icon
|
681
712
|
|
682
|
-
def getToggleIcon(self, name: str, size: tuple[int, int]) -> QIcon:
|
683
|
-
"""Return a toggle icon from the icon buffer
|
713
|
+
def getToggleIcon(self, name: str, size: tuple[int, int], color: str | None = None) -> QIcon:
|
714
|
+
"""Return a toggle icon from the icon buffer, or load it."""
|
684
715
|
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)
|
716
|
+
pOne = self.getPixmap(self.TOGGLE_ICON_KEYS[name][0], size, color)
|
717
|
+
pTwo = self.getPixmap(self.TOGGLE_ICON_KEYS[name][1], size, color)
|
687
718
|
icon = QIcon()
|
688
719
|
icon.addPixmap(pOne, QIcon.Mode.Normal, QIcon.State.On)
|
689
720
|
icon.addPixmap(pTwo, QIcon.Mode.Normal, QIcon.State.Off)
|
690
721
|
return icon
|
691
722
|
return self._noIcon
|
692
723
|
|
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:
|
724
|
+
def getItemIcon(
|
725
|
+
self, tType: nwItemType, tClass: nwItemClass, tLayout: nwItemLayout, hLevel: str = "H0"
|
726
|
+
) -> QIcon:
|
701
727
|
"""Get the correct icon for a project item based on type, class
|
702
728
|
and heading level
|
703
729
|
"""
|
704
|
-
|
730
|
+
name = None
|
731
|
+
color = "default"
|
705
732
|
if tType == nwItemType.ROOT:
|
706
|
-
|
733
|
+
name = nwLabels.CLASS_ICON[tClass]
|
734
|
+
color = "root"
|
707
735
|
elif tType == nwItemType.FOLDER:
|
708
|
-
|
736
|
+
name = "prj_folder"
|
737
|
+
color = "folder"
|
709
738
|
elif tType == nwItemType.FILE:
|
710
|
-
iconName = "proj_document"
|
711
739
|
if tLayout == nwItemLayout.DOCUMENT:
|
712
740
|
if hLevel == "H1":
|
713
|
-
|
741
|
+
name = "prj_title"
|
742
|
+
color = "title"
|
714
743
|
elif hLevel == "H2":
|
715
|
-
|
744
|
+
name = "prj_chapter"
|
745
|
+
color = "chapter"
|
716
746
|
elif hLevel == "H3":
|
717
|
-
|
718
|
-
|
719
|
-
|
747
|
+
name = "prj_scene"
|
748
|
+
color = "scene"
|
749
|
+
else:
|
750
|
+
name = "prj_document"
|
751
|
+
color = "file"
|
720
752
|
elif tLayout == nwItemLayout.NOTE:
|
721
|
-
|
722
|
-
|
753
|
+
name = "prj_note"
|
754
|
+
color = "note"
|
755
|
+
if name is None:
|
723
756
|
return self._noIcon
|
724
757
|
|
725
|
-
return self.getIcon(
|
758
|
+
return self.getIcon(name, color)
|
759
|
+
|
760
|
+
def getPixmap(self, name: str, size: tuple[int, int], color: str | None = None) -> QPixmap:
|
761
|
+
"""Return an icon from the icon buffer as a QPixmap. If it
|
762
|
+
doesn't exist, return an empty QPixmap.
|
763
|
+
"""
|
764
|
+
w, h = size
|
765
|
+
return self.getIcon(name, color, w, h).pixmap(w, h, QIcon.Mode.Normal)
|
766
|
+
|
767
|
+
def getDecoration(self, name: str, w: int | None = None, h: int | None = None) -> QPixmap:
|
768
|
+
"""Load graphical decoration element based on the decoration
|
769
|
+
map or the icon map. This function always returns a QPixmap.
|
770
|
+
"""
|
771
|
+
if name in self.IMAGE_MAP:
|
772
|
+
idx = int(self.mainTheme.isDarkTheme)
|
773
|
+
imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx]
|
774
|
+
else:
|
775
|
+
logger.error("Decoration with name '%s' does not exist", name)
|
776
|
+
return QPixmap()
|
777
|
+
|
778
|
+
if not imgPath.is_file():
|
779
|
+
logger.error("Asset not found: %s", imgPath)
|
780
|
+
return QPixmap()
|
781
|
+
|
782
|
+
pixmap = QPixmap(str(imgPath))
|
783
|
+
tMode = Qt.TransformationMode.SmoothTransformation
|
784
|
+
if w is not None and h is not None:
|
785
|
+
return pixmap.scaled(w, h, Qt.AspectRatioMode.IgnoreAspectRatio, tMode)
|
786
|
+
elif w is None and h is not None:
|
787
|
+
return pixmap.scaledToHeight(h, tMode)
|
788
|
+
elif w is not None and h is None:
|
789
|
+
return pixmap.scaledToWidth(w, tMode)
|
790
|
+
|
791
|
+
return pixmap
|
726
792
|
|
727
793
|
def getHeaderDecoration(self, hLevel: int) -> QPixmap:
|
728
794
|
"""Get the decoration for a specific heading level."""
|
729
795
|
if not self._headerDec:
|
730
796
|
iPx = self.mainTheme.baseIconHeight
|
731
797
|
self._headerDec = [
|
732
|
-
self.
|
733
|
-
self.
|
734
|
-
self.
|
735
|
-
self.
|
736
|
-
self.
|
798
|
+
self._generateDecoration("file", iPx, 0),
|
799
|
+
self._generateDecoration("title", iPx, 0),
|
800
|
+
self._generateDecoration("chapter", iPx, 1),
|
801
|
+
self._generateDecoration("scene", iPx, 2),
|
802
|
+
self._generateDecoration("file", iPx, 3),
|
737
803
|
]
|
738
804
|
return self._headerDec[minmax(hLevel, 0, 4)]
|
739
805
|
|
@@ -742,47 +808,86 @@ class GuiIcons:
|
|
742
808
|
if not self._headerDecNarrow:
|
743
809
|
iPx = self.mainTheme.baseIconHeight
|
744
810
|
self._headerDecNarrow = [
|
745
|
-
self.
|
746
|
-
self.
|
747
|
-
self.
|
748
|
-
self.
|
749
|
-
self.
|
750
|
-
self.
|
811
|
+
self._generateDecoration("file", iPx, 0),
|
812
|
+
self._generateDecoration("title", iPx, 0),
|
813
|
+
self._generateDecoration("chapter", iPx, 0),
|
814
|
+
self._generateDecoration("scene", iPx, 0),
|
815
|
+
self._generateDecoration("file", iPx, 0),
|
816
|
+
self._generateDecoration("note", iPx, 0),
|
751
817
|
]
|
752
818
|
return self._headerDecNarrow[minmax(hLevel, 0, 5)]
|
753
819
|
|
820
|
+
def listThemes(self) -> list[tuple[str, str]]:
|
821
|
+
"""Scan the GUI icons folder and list all themes."""
|
822
|
+
if self._themeList:
|
823
|
+
return self._themeList
|
824
|
+
|
825
|
+
themes = []
|
826
|
+
for key, path in self._availThemes.items():
|
827
|
+
logger.debug("Checking icon theme '%s'", key)
|
828
|
+
if name := _loadIconName(path):
|
829
|
+
themes.append((key, name))
|
830
|
+
|
831
|
+
self._themeList = sorted(themes, key=_sortTheme)
|
832
|
+
|
833
|
+
return self._themeList
|
834
|
+
|
754
835
|
##
|
755
836
|
# Internal Functions
|
756
837
|
##
|
757
838
|
|
758
|
-
def _loadIcon(self, name: str) -> QIcon:
|
759
|
-
"""Load an icon from the assets themes folder.
|
760
|
-
return a QIcon.
|
839
|
+
def _loadIcon(self, name: str, color: str | None = None, w: int = 24, h: int = 24) -> QIcon:
|
840
|
+
"""Load an icon from the assets themes folder. This function is
|
841
|
+
guaranteed to return a QIcon.
|
761
842
|
"""
|
762
|
-
if name not in self.ICON_KEYS:
|
763
|
-
logger.error("Requested unknown icon name '%s'", name)
|
764
|
-
return self._noIcon
|
765
|
-
|
766
843
|
# If we just want the app icons, return right away
|
767
844
|
if name == "novelwriter":
|
768
|
-
return QIcon(str(
|
845
|
+
return QIcon(str(CONFIG.assetPath("icons") / "novelwriter.svg"))
|
769
846
|
elif name == "proj_nwx":
|
770
|
-
return QIcon(str(
|
847
|
+
return QIcon(str(CONFIG.assetPath("icons") / "x-novelwriter-project.svg"))
|
771
848
|
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
849
|
+
if svg := self._svgData.get(name, b""):
|
850
|
+
if fill := self._svgColors.get(color or "default"):
|
851
|
+
svg = svg.replace(b"#000000", fill)
|
852
|
+
pixmap = QPixmap(w, h)
|
853
|
+
pixmap.fill(QtTransparent)
|
854
|
+
pixmap.loadFromData(svg, "svg")
|
855
|
+
return QIcon(pixmap)
|
776
856
|
|
777
857
|
# If we didn't find one, give up and return an empty icon
|
778
858
|
logger.warning("Did not load an icon for '%s'", name)
|
779
859
|
|
780
860
|
return self._noIcon
|
781
861
|
|
862
|
+
def _generateDecoration(self, color: str, height: int, indent: int = 0) -> QPixmap:
|
863
|
+
"""Generate a decoration pixmap for novel headers."""
|
864
|
+
pixmap = QPixmap(48*indent + 12, 48)
|
865
|
+
pixmap.fill(QtTransparent)
|
866
|
+
|
867
|
+
path = QPainterPath()
|
868
|
+
path.addRoundedRect(48.0*indent, 2.0, 12.0, 44.0, 4.0, 4.0)
|
869
|
+
|
870
|
+
painter = QPainter(pixmap)
|
871
|
+
painter.setRenderHint(QtPaintAntiAlias)
|
872
|
+
if fill := self._svgColors.get(color or "default"):
|
873
|
+
painter.fillPath(path, QColor(fill.decode(encoding="utf-8")))
|
874
|
+
painter.end()
|
875
|
+
|
876
|
+
tMode = Qt.TransformationMode.SmoothTransformation
|
877
|
+
return pixmap.scaledToHeight(height, tMode)
|
878
|
+
|
782
879
|
|
783
880
|
# Module Functions
|
784
881
|
# ================
|
785
882
|
|
883
|
+
def _listConf(target: dict, path: Path, extension: str) -> None:
|
884
|
+
"""Scan for theme files and populate the dictionary."""
|
885
|
+
if path.is_dir():
|
886
|
+
for item in path.iterdir():
|
887
|
+
if item.is_file() and item.name.endswith(extension):
|
888
|
+
target[item.stem] = item
|
889
|
+
return
|
890
|
+
|
786
891
|
|
787
892
|
def _sortTheme(data: tuple[str, str]) -> str:
|
788
893
|
"""Key function for theme sorting."""
|
@@ -790,14 +895,27 @@ def _sortTheme(data: tuple[str, str]) -> str:
|
|
790
895
|
return f"*{name}" if key.startswith("default_") else name
|
791
896
|
|
792
897
|
|
793
|
-
def _loadInternalName(
|
898
|
+
def _loadInternalName(parser: NWConfigParser, path: str | Path) -> str:
|
794
899
|
"""Open a conf file and read the 'name' setting."""
|
795
900
|
try:
|
796
|
-
with open(
|
797
|
-
|
901
|
+
with open(path, mode="r", encoding="utf-8") as inFile:
|
902
|
+
parser.read_file(inFile)
|
903
|
+
return parser.rdStr("Main", "name", "")
|
798
904
|
except Exception:
|
799
|
-
logger.error("Could not
|
905
|
+
logger.error("Could not read file: %s", path)
|
800
906
|
logException()
|
801
|
-
|
907
|
+
return ""
|
802
908
|
|
803
|
-
|
909
|
+
|
910
|
+
def _loadIconName(path: Path) -> str:
|
911
|
+
"""Open an icons file and read the name setting."""
|
912
|
+
try:
|
913
|
+
with open(path, mode="r", encoding="utf-8") as icons:
|
914
|
+
for icon in icons:
|
915
|
+
key, _, value = icon.partition("=")
|
916
|
+
if key.strip() == "meta:name":
|
917
|
+
return value.strip()
|
918
|
+
except Exception:
|
919
|
+
logger.error("Could not read file: %s", path)
|
920
|
+
logException()
|
921
|
+
return ""
|