novelWriter 2.6.3__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.
Files changed (321) hide show
  1. novelwriter/__init__.py +84 -74
  2. novelwriter/assets/i18n/project_en_GB.json +1 -0
  3. novelwriter/assets/icons/font_awesome.icons +109 -0
  4. novelwriter/assets/icons/material_filled_bold.icons +109 -0
  5. novelwriter/assets/icons/material_filled_normal.icons +109 -0
  6. novelwriter/assets/icons/material_filled_thin.icons +109 -0
  7. novelwriter/assets/icons/material_rounded_bold.icons +109 -0
  8. novelwriter/assets/icons/material_rounded_normal.icons +109 -0
  9. novelwriter/assets/icons/material_rounded_thin.icons +109 -0
  10. novelwriter/assets/icons/remix_filled.icons +108 -0
  11. novelwriter/assets/icons/remix_outline.icons +108 -0
  12. novelwriter/assets/manual.pdf +0 -0
  13. novelwriter/assets/{manual_fr_FR.pdf → manual_fr.pdf} +0 -0
  14. novelwriter/assets/sample.zip +0 -0
  15. novelwriter/assets/syntax/cyberpunk_night.conf +1 -1
  16. novelwriter/assets/text/credits_en.htm +6 -6
  17. novelwriter/assets/themes/cyberpunk_night.conf +23 -7
  18. novelwriter/assets/themes/default_dark.conf +20 -4
  19. novelwriter/assets/themes/default_light.conf +21 -5
  20. novelwriter/assets/themes/dracula.conf +20 -4
  21. novelwriter/assets/themes/solarized_dark.conf +24 -8
  22. novelwriter/assets/themes/solarized_light.conf +22 -6
  23. novelwriter/common.py +33 -26
  24. novelwriter/config.py +118 -127
  25. novelwriter/constants.py +50 -36
  26. novelwriter/core/buildsettings.py +23 -16
  27. novelwriter/core/coretools.py +11 -7
  28. novelwriter/core/docbuild.py +19 -13
  29. novelwriter/core/document.py +2 -2
  30. novelwriter/core/index.py +142 -432
  31. novelwriter/core/indexdata.py +403 -0
  32. novelwriter/core/item.py +35 -28
  33. novelwriter/core/itemmodel.py +27 -26
  34. novelwriter/core/novelmodel.py +223 -0
  35. novelwriter/core/options.py +1 -1
  36. novelwriter/core/project.py +10 -11
  37. novelwriter/core/projectdata.py +5 -5
  38. novelwriter/core/projectxml.py +1 -1
  39. novelwriter/core/sessions.py +3 -2
  40. novelwriter/core/spellcheck.py +4 -3
  41. novelwriter/core/status.py +12 -15
  42. novelwriter/core/storage.py +1 -1
  43. novelwriter/core/tree.py +46 -8
  44. novelwriter/dialogs/about.py +19 -22
  45. novelwriter/dialogs/docmerge.py +21 -23
  46. novelwriter/dialogs/docsplit.py +20 -23
  47. novelwriter/dialogs/editlabel.py +9 -13
  48. novelwriter/dialogs/preferences.py +111 -48
  49. novelwriter/dialogs/projectsettings.py +48 -54
  50. novelwriter/dialogs/quotes.py +14 -19
  51. novelwriter/dialogs/wordlist.py +25 -30
  52. novelwriter/enum.py +8 -0
  53. novelwriter/error.py +16 -16
  54. novelwriter/extensions/configlayout.py +18 -18
  55. novelwriter/extensions/eventfilters.py +9 -5
  56. novelwriter/extensions/modified.py +34 -15
  57. novelwriter/extensions/novelselector.py +18 -5
  58. novelwriter/extensions/pagedsidebar.py +39 -49
  59. novelwriter/extensions/progressbars.py +10 -8
  60. novelwriter/extensions/statusled.py +6 -13
  61. novelwriter/extensions/switch.py +19 -30
  62. novelwriter/extensions/switchbox.py +7 -3
  63. novelwriter/extensions/versioninfo.py +4 -4
  64. novelwriter/formats/shared.py +1 -1
  65. novelwriter/formats/todocx.py +14 -10
  66. novelwriter/formats/tohtml.py +7 -5
  67. novelwriter/formats/tokenizer.py +36 -33
  68. novelwriter/formats/tomarkdown.py +6 -2
  69. novelwriter/formats/toodt.py +27 -22
  70. novelwriter/formats/toqdoc.py +19 -14
  71. novelwriter/formats/toraw.py +6 -2
  72. novelwriter/gui/doceditor.py +216 -265
  73. novelwriter/gui/dochighlight.py +46 -45
  74. novelwriter/gui/docviewer.py +102 -104
  75. novelwriter/gui/docviewerpanel.py +47 -51
  76. novelwriter/gui/editordocument.py +8 -5
  77. novelwriter/gui/itemdetails.py +11 -14
  78. novelwriter/gui/mainmenu.py +146 -145
  79. novelwriter/gui/noveltree.py +239 -405
  80. novelwriter/gui/outline.py +133 -72
  81. novelwriter/gui/projtree.py +138 -132
  82. novelwriter/gui/search.py +33 -31
  83. novelwriter/gui/sidebar.py +13 -18
  84. novelwriter/gui/statusbar.py +13 -15
  85. novelwriter/gui/theme.py +533 -430
  86. novelwriter/guimain.py +27 -29
  87. novelwriter/shared.py +32 -23
  88. novelwriter/text/comments.py +70 -0
  89. novelwriter/text/patterns.py +4 -4
  90. novelwriter/tools/dictionaries.py +20 -29
  91. novelwriter/tools/lipsum.py +16 -17
  92. novelwriter/tools/manusbuild.py +39 -42
  93. novelwriter/tools/manuscript.py +101 -114
  94. novelwriter/tools/manussettings.py +78 -83
  95. novelwriter/tools/noveldetails.py +51 -64
  96. novelwriter/tools/welcome.py +56 -75
  97. novelwriter/tools/writingstats.py +44 -57
  98. novelwriter/types.py +5 -7
  99. {novelWriter-2.6.3.dist-info → novelwriter-2.7b1.dist-info}/METADATA +6 -6
  100. novelwriter-2.7b1.dist-info/RECORD +159 -0
  101. {novelWriter-2.6.3.dist-info → novelwriter-2.7b1.dist-info}/WHEEL +1 -1
  102. novelWriter-2.6.3.dist-info/RECORD +0 -363
  103. novelwriter/assets/icons/typicons_dark/README.md +0 -29
  104. novelwriter/assets/icons/typicons_dark/icons.conf +0 -134
  105. novelwriter/assets/icons/typicons_dark/mixed_copy.svg +0 -4
  106. novelwriter/assets/icons/typicons_dark/mixed_document-chapter.svg +0 -12
  107. novelwriter/assets/icons/typicons_dark/mixed_document-new.svg +0 -6
  108. novelwriter/assets/icons/typicons_dark/mixed_document-note.svg +0 -12
  109. novelwriter/assets/icons/typicons_dark/mixed_document-scene.svg +0 -12
  110. novelwriter/assets/icons/typicons_dark/mixed_document-section.svg +0 -12
  111. novelwriter/assets/icons/typicons_dark/mixed_document-title.svg +0 -12
  112. novelwriter/assets/icons/typicons_dark/mixed_edit.svg +0 -4
  113. novelwriter/assets/icons/typicons_dark/mixed_import.svg +0 -5
  114. novelwriter/assets/icons/typicons_dark/mixed_input-checked.svg +0 -5
  115. novelwriter/assets/icons/typicons_dark/mixed_input-none.svg +0 -5
  116. novelwriter/assets/icons/typicons_dark/mixed_input-unchecked.svg +0 -5
  117. novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +0 -6
  118. novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +0 -6
  119. novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +0 -6
  120. novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +0 -6
  121. novelwriter/assets/icons/typicons_dark/mixed_search-replace.svg +0 -6
  122. novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +0 -6
  123. novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +0 -6
  124. novelwriter/assets/icons/typicons_dark/nw_deco-h0.svg +0 -4
  125. novelwriter/assets/icons/typicons_dark/nw_deco-h1.svg +0 -4
  126. novelwriter/assets/icons/typicons_dark/nw_deco-h2-narrow.svg +0 -4
  127. novelwriter/assets/icons/typicons_dark/nw_deco-h2.svg +0 -4
  128. novelwriter/assets/icons/typicons_dark/nw_deco-h3-narrow.svg +0 -4
  129. novelwriter/assets/icons/typicons_dark/nw_deco-h3.svg +0 -4
  130. novelwriter/assets/icons/typicons_dark/nw_deco-h4-narrow.svg +0 -4
  131. novelwriter/assets/icons/typicons_dark/nw_deco-h4.svg +0 -4
  132. novelwriter/assets/icons/typicons_dark/nw_deco-note.svg +0 -4
  133. novelwriter/assets/icons/typicons_dark/nw_deco-noveltree-more.svg +0 -4
  134. novelwriter/assets/icons/typicons_dark/nw_font.svg +0 -4
  135. novelwriter/assets/icons/typicons_dark/nw_panel.svg +0 -4
  136. novelwriter/assets/icons/typicons_dark/nw_quote.svg +0 -4
  137. novelwriter/assets/icons/typicons_dark/nw_search-case.svg +0 -4
  138. novelwriter/assets/icons/typicons_dark/nw_search-preserve.svg +0 -4
  139. novelwriter/assets/icons/typicons_dark/nw_search-regex.svg +0 -4
  140. novelwriter/assets/icons/typicons_dark/nw_search-word.svg +0 -4
  141. novelwriter/assets/icons/typicons_dark/nw_tb-bold-md.svg +0 -4
  142. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +0 -6
  143. novelwriter/assets/icons/typicons_dark/nw_tb-italic-md.svg +0 -4
  144. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +0 -6
  145. novelwriter/assets/icons/typicons_dark/nw_tb-mark.svg +0 -7
  146. novelwriter/assets/icons/typicons_dark/nw_tb-strike-md.svg +0 -4
  147. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +0 -6
  148. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +0 -7
  149. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +0 -7
  150. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +0 -7
  151. novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +0 -5
  152. novelwriter/assets/icons/typicons_dark/typ_arrow-down-thick-grey.svg +0 -4
  153. novelwriter/assets/icons/typicons_dark/typ_arrow-forward.svg +0 -4
  154. novelwriter/assets/icons/typicons_dark/typ_arrow-maximise.svg +0 -4
  155. novelwriter/assets/icons/typicons_dark/typ_arrow-minimise.svg +0 -4
  156. novelwriter/assets/icons/typicons_dark/typ_arrow-repeat-grey.svg +0 -4
  157. novelwriter/assets/icons/typicons_dark/typ_book-grey.svg +0 -4
  158. novelwriter/assets/icons/typicons_dark/typ_book.svg +0 -6
  159. novelwriter/assets/icons/typicons_dark/typ_bookmark.svg +0 -4
  160. novelwriter/assets/icons/typicons_dark/typ_calendar.svg +0 -4
  161. novelwriter/assets/icons/typicons_dark/typ_cancel-grey.svg +0 -4
  162. novelwriter/assets/icons/typicons_dark/typ_cancel.svg +0 -4
  163. novelwriter/assets/icons/typicons_dark/typ_chart-bar-grey.svg +0 -4
  164. novelwriter/assets/icons/typicons_dark/typ_chevron-down.svg +0 -4
  165. novelwriter/assets/icons/typicons_dark/typ_chevron-left.svg +0 -4
  166. novelwriter/assets/icons/typicons_dark/typ_chevron-right.svg +0 -4
  167. novelwriter/assets/icons/typicons_dark/typ_chevron-up.svg +0 -4
  168. novelwriter/assets/icons/typicons_dark/typ_cog.svg +0 -4
  169. novelwriter/assets/icons/typicons_dark/typ_delete-full.svg +0 -4
  170. novelwriter/assets/icons/typicons_dark/typ_delete.svg +0 -4
  171. novelwriter/assets/icons/typicons_dark/typ_directions-full.svg +0 -4
  172. novelwriter/assets/icons/typicons_dark/typ_document-add.svg +0 -4
  173. novelwriter/assets/icons/typicons_dark/typ_document-text.svg +0 -8
  174. novelwriter/assets/icons/typicons_dark/typ_document.svg +0 -4
  175. novelwriter/assets/icons/typicons_dark/typ_export-grey.svg +0 -4
  176. novelwriter/assets/icons/typicons_dark/typ_export.svg +0 -4
  177. novelwriter/assets/icons/typicons_dark/typ_eye.svg +0 -4
  178. novelwriter/assets/icons/typicons_dark/typ_flag.svg +0 -4
  179. novelwriter/assets/icons/typicons_dark/typ_folder-open.svg +0 -4
  180. novelwriter/assets/icons/typicons_dark/typ_folder.svg +0 -5
  181. novelwriter/assets/icons/typicons_dark/typ_globe-grey.svg +0 -4
  182. novelwriter/assets/icons/typicons_dark/typ_key.svg +0 -4
  183. novelwriter/assets/icons/typicons_dark/typ_lightbulb-full.svg +0 -4
  184. novelwriter/assets/icons/typicons_dark/typ_location.svg +0 -4
  185. novelwriter/assets/icons/typicons_dark/typ_media-pause-grey.svg +0 -4
  186. novelwriter/assets/icons/typicons_dark/typ_media-record-outline.svg +0 -4
  187. novelwriter/assets/icons/typicons_dark/typ_media-record.svg +0 -4
  188. novelwriter/assets/icons/typicons_dark/typ_minus.svg +0 -4
  189. novelwriter/assets/icons/typicons_dark/typ_pencil.svg +0 -5
  190. novelwriter/assets/icons/typicons_dark/typ_pin-outline.svg +0 -4
  191. novelwriter/assets/icons/typicons_dark/typ_pin.svg +0 -4
  192. novelwriter/assets/icons/typicons_dark/typ_plus.svg +0 -4
  193. novelwriter/assets/icons/typicons_dark/typ_puzzle-outline.svg +0 -4
  194. novelwriter/assets/icons/typicons_dark/typ_puzzle.svg +0 -4
  195. novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg +0 -4
  196. novelwriter/assets/icons/typicons_dark/typ_refresh.svg +0 -4
  197. novelwriter/assets/icons/typicons_dark/typ_search-grey.svg +0 -4
  198. novelwriter/assets/icons/typicons_dark/typ_search.svg +0 -4
  199. novelwriter/assets/icons/typicons_dark/typ_star.svg +0 -4
  200. novelwriter/assets/icons/typicons_dark/typ_stopwatch-grey.svg +0 -4
  201. novelwriter/assets/icons/typicons_dark/typ_th-dot-menu.svg +0 -4
  202. novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +0 -4
  203. novelwriter/assets/icons/typicons_dark/typ_th-list-grey.svg +0 -4
  204. novelwriter/assets/icons/typicons_dark/typ_th-list.svg +0 -9
  205. novelwriter/assets/icons/typicons_dark/typ_times.svg +0 -4
  206. novelwriter/assets/icons/typicons_dark/typ_trash.svg +0 -5
  207. novelwriter/assets/icons/typicons_dark/typ_unfold-hidden.svg +0 -4
  208. novelwriter/assets/icons/typicons_dark/typ_unfold-visible.svg +0 -4
  209. novelwriter/assets/icons/typicons_dark/typ_user.svg +0 -5
  210. novelwriter/assets/icons/typicons_dark/typ_warning-full.svg +0 -4
  211. novelwriter/assets/icons/typicons_light/README.md +0 -29
  212. novelwriter/assets/icons/typicons_light/icons.conf +0 -134
  213. novelwriter/assets/icons/typicons_light/mixed_copy.svg +0 -4
  214. novelwriter/assets/icons/typicons_light/mixed_document-chapter.svg +0 -12
  215. novelwriter/assets/icons/typicons_light/mixed_document-new.svg +0 -6
  216. novelwriter/assets/icons/typicons_light/mixed_document-note.svg +0 -12
  217. novelwriter/assets/icons/typicons_light/mixed_document-scene.svg +0 -12
  218. novelwriter/assets/icons/typicons_light/mixed_document-section.svg +0 -12
  219. novelwriter/assets/icons/typicons_light/mixed_document-title.svg +0 -12
  220. novelwriter/assets/icons/typicons_light/mixed_edit.svg +0 -4
  221. novelwriter/assets/icons/typicons_light/mixed_import.svg +0 -5
  222. novelwriter/assets/icons/typicons_light/mixed_input-checked.svg +0 -5
  223. novelwriter/assets/icons/typicons_light/mixed_input-none.svg +0 -5
  224. novelwriter/assets/icons/typicons_light/mixed_input-unchecked.svg +0 -5
  225. novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +0 -6
  226. novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +0 -6
  227. novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +0 -6
  228. novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +0 -6
  229. novelwriter/assets/icons/typicons_light/mixed_search-replace.svg +0 -6
  230. novelwriter/assets/icons/typicons_light/mixed_size-height.svg +0 -6
  231. novelwriter/assets/icons/typicons_light/mixed_size-width.svg +0 -6
  232. novelwriter/assets/icons/typicons_light/nw_deco-h0.svg +0 -4
  233. novelwriter/assets/icons/typicons_light/nw_deco-h1.svg +0 -4
  234. novelwriter/assets/icons/typicons_light/nw_deco-h2-narrow.svg +0 -4
  235. novelwriter/assets/icons/typicons_light/nw_deco-h2.svg +0 -4
  236. novelwriter/assets/icons/typicons_light/nw_deco-h3-narrow.svg +0 -4
  237. novelwriter/assets/icons/typicons_light/nw_deco-h3.svg +0 -4
  238. novelwriter/assets/icons/typicons_light/nw_deco-h4-narrow.svg +0 -4
  239. novelwriter/assets/icons/typicons_light/nw_deco-h4.svg +0 -4
  240. novelwriter/assets/icons/typicons_light/nw_deco-note.svg +0 -4
  241. novelwriter/assets/icons/typicons_light/nw_deco-noveltree-more.svg +0 -4
  242. novelwriter/assets/icons/typicons_light/nw_font.svg +0 -4
  243. novelwriter/assets/icons/typicons_light/nw_panel.svg +0 -4
  244. novelwriter/assets/icons/typicons_light/nw_quote.svg +0 -4
  245. novelwriter/assets/icons/typicons_light/nw_search-case.svg +0 -4
  246. novelwriter/assets/icons/typicons_light/nw_search-preserve.svg +0 -4
  247. novelwriter/assets/icons/typicons_light/nw_search-regex.svg +0 -4
  248. novelwriter/assets/icons/typicons_light/nw_search-word.svg +0 -4
  249. novelwriter/assets/icons/typicons_light/nw_tb-bold-md.svg +0 -4
  250. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +0 -6
  251. novelwriter/assets/icons/typicons_light/nw_tb-italic-md.svg +0 -4
  252. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +0 -6
  253. novelwriter/assets/icons/typicons_light/nw_tb-mark.svg +0 -7
  254. novelwriter/assets/icons/typicons_light/nw_tb-strike-md.svg +0 -4
  255. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +0 -6
  256. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +0 -7
  257. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +0 -7
  258. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +0 -7
  259. novelwriter/assets/icons/typicons_light/nw_toolbar.svg +0 -5
  260. novelwriter/assets/icons/typicons_light/typ_arrow-down-thick-grey.svg +0 -4
  261. novelwriter/assets/icons/typicons_light/typ_arrow-forward.svg +0 -4
  262. novelwriter/assets/icons/typicons_light/typ_arrow-maximise.svg +0 -4
  263. novelwriter/assets/icons/typicons_light/typ_arrow-minimise.svg +0 -4
  264. novelwriter/assets/icons/typicons_light/typ_arrow-repeat-grey.svg +0 -4
  265. novelwriter/assets/icons/typicons_light/typ_book-grey.svg +0 -4
  266. novelwriter/assets/icons/typicons_light/typ_book.svg +0 -6
  267. novelwriter/assets/icons/typicons_light/typ_bookmark.svg +0 -4
  268. novelwriter/assets/icons/typicons_light/typ_calendar.svg +0 -4
  269. novelwriter/assets/icons/typicons_light/typ_cancel-grey.svg +0 -4
  270. novelwriter/assets/icons/typicons_light/typ_cancel.svg +0 -4
  271. novelwriter/assets/icons/typicons_light/typ_chart-bar-grey.svg +0 -4
  272. novelwriter/assets/icons/typicons_light/typ_chevron-down.svg +0 -4
  273. novelwriter/assets/icons/typicons_light/typ_chevron-left.svg +0 -4
  274. novelwriter/assets/icons/typicons_light/typ_chevron-right.svg +0 -4
  275. novelwriter/assets/icons/typicons_light/typ_chevron-up.svg +0 -4
  276. novelwriter/assets/icons/typicons_light/typ_cog.svg +0 -4
  277. novelwriter/assets/icons/typicons_light/typ_delete-full.svg +0 -4
  278. novelwriter/assets/icons/typicons_light/typ_delete.svg +0 -4
  279. novelwriter/assets/icons/typicons_light/typ_directions-full.svg +0 -4
  280. novelwriter/assets/icons/typicons_light/typ_document-add.svg +0 -4
  281. novelwriter/assets/icons/typicons_light/typ_document-text.svg +0 -5
  282. novelwriter/assets/icons/typicons_light/typ_document.svg +0 -4
  283. novelwriter/assets/icons/typicons_light/typ_export-grey.svg +0 -4
  284. novelwriter/assets/icons/typicons_light/typ_export.svg +0 -4
  285. novelwriter/assets/icons/typicons_light/typ_eye.svg +0 -4
  286. novelwriter/assets/icons/typicons_light/typ_flag.svg +0 -4
  287. novelwriter/assets/icons/typicons_light/typ_folder-open.svg +0 -4
  288. novelwriter/assets/icons/typicons_light/typ_folder.svg +0 -5
  289. novelwriter/assets/icons/typicons_light/typ_globe-grey.svg +0 -4
  290. novelwriter/assets/icons/typicons_light/typ_key.svg +0 -4
  291. novelwriter/assets/icons/typicons_light/typ_lightbulb-full.svg +0 -4
  292. novelwriter/assets/icons/typicons_light/typ_location.svg +0 -4
  293. novelwriter/assets/icons/typicons_light/typ_media-pause-grey.svg +0 -4
  294. novelwriter/assets/icons/typicons_light/typ_media-record-outline.svg +0 -4
  295. novelwriter/assets/icons/typicons_light/typ_media-record.svg +0 -4
  296. novelwriter/assets/icons/typicons_light/typ_minus.svg +0 -4
  297. novelwriter/assets/icons/typicons_light/typ_pencil.svg +0 -5
  298. novelwriter/assets/icons/typicons_light/typ_pin-outline.svg +0 -4
  299. novelwriter/assets/icons/typicons_light/typ_pin.svg +0 -4
  300. novelwriter/assets/icons/typicons_light/typ_plus.svg +0 -4
  301. novelwriter/assets/icons/typicons_light/typ_puzzle-outline.svg +0 -4
  302. novelwriter/assets/icons/typicons_light/typ_puzzle.svg +0 -4
  303. novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg +0 -4
  304. novelwriter/assets/icons/typicons_light/typ_refresh.svg +0 -4
  305. novelwriter/assets/icons/typicons_light/typ_search-grey.svg +0 -4
  306. novelwriter/assets/icons/typicons_light/typ_search.svg +0 -4
  307. novelwriter/assets/icons/typicons_light/typ_star.svg +0 -4
  308. novelwriter/assets/icons/typicons_light/typ_stopwatch-grey.svg +0 -4
  309. novelwriter/assets/icons/typicons_light/typ_th-dot-menu.svg +0 -4
  310. novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +0 -4
  311. novelwriter/assets/icons/typicons_light/typ_th-list-grey.svg +0 -4
  312. novelwriter/assets/icons/typicons_light/typ_th-list.svg +0 -9
  313. novelwriter/assets/icons/typicons_light/typ_times.svg +0 -4
  314. novelwriter/assets/icons/typicons_light/typ_trash.svg +0 -5
  315. novelwriter/assets/icons/typicons_light/typ_unfold-hidden.svg +0 -4
  316. novelwriter/assets/icons/typicons_light/typ_unfold-visible.svg +0 -4
  317. novelwriter/assets/icons/typicons_light/typ_user.svg +0 -5
  318. novelwriter/assets/icons/typicons_light/typ_warning-full.svg +0 -4
  319. {novelWriter-2.6.3.dist-info → novelwriter-2.7b1.dist-info}/entry_points.txt +0 -0
  320. {novelWriter-2.6.3.dist-info → novelwriter-2.7b1.dist-info/licenses}/LICENSE.md +0 -0
  321. {novelWriter-2.6.3.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 pathlib import Path
30
+ from typing import TYPE_CHECKING, Final
31
31
 
32
- from PyQt5.QtCore import QSize, Qt
33
- from PyQt5.QtGui import QColor, QFont, QFontDatabase, QFontMetrics, QIcon, QPalette, QPixmap
34
- from PyQt5.QtWidgets import QApplication
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, cssCol, minmax
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
- # Loaded Theme Settings
60
- # =====================
112
+ # GUI Theme
113
+ self.themeMeta = ThemeMeta()
114
+ self.isDarkTheme = False
61
115
 
62
- # Theme
63
- self.themeName = ""
64
- self.themeDescription = ""
65
- self.themeAuthor = ""
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
- # GUI
74
- self.statNone = QColor(0, 0, 0)
75
- self.statUnsaved = QColor(0, 0, 0)
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
- 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"))
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.loadDecoration = self.iconCache.loadDecoration
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 = int(round(fHeight))
165
- self.baseIconHeight = int(round(fAscent))
166
- self.baseButtonHeight = int(round(1.35*fAscent))
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 int(ceil(qMetrics.boundingRect(text).width()))
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
- guiTheme = CONFIG.guiTheme
211
- if guiTheme not in self._availThemes:
212
- logger.error("Could not find GUI theme '%s'", guiTheme)
213
- guiTheme = "default"
214
- CONFIG.guiTheme = guiTheme
215
-
216
- themeFile = self._availThemes.get(guiTheme, None)
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
- # Config File
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(themeFile, mode="r", encoding="utf-8") as inFile:
226
- parser.read_file(inFile)
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 load theme settings from: %s", themeFile)
227
+ logger.error("Could not read file: %s", file)
229
228
  logException()
230
229
  return False
231
230
 
232
231
  # Reset Palette
233
- self._guiPalette = QApplication.style().standardPalette()
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.themeName = parser.rdStr(sec, "name", "")
240
- self.themeDescription = parser.rdStr(sec, "description", "N/A")
241
- self.themeAuthor = parser.rdStr(sec, "author", "N/A")
242
- self.themeCredit = parser.rdStr(sec, "credit", "N/A")
243
- self.themeUrl = parser.rdStr(sec, "url", "")
244
- self.themeLicense = parser.rdStr(sec, "license", "N/A")
245
- self.themeLicenseUrl = parser.rdStr(sec, "licenseurl", "")
246
- self.themeIcons = parser.rdStr(sec, "icontheme", "")
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 = self._parseColour(parser, sec, "helptext")
270
- self.fadedText = self._parseColour(parser, sec, "fadedtext")
271
- self.errorText = self._parseColour(parser, sec, "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
- backCol = self._guiPalette.window().color()
278
- textCol = self._guiPalette.windowText().color()
279
-
280
- backLNess = backCol.lightnessF()
281
- textLNess = textCol.lightnessF()
282
- self.isLightTheme = backLNess > textLNess
283
- if self.helpText == QColor(0, 0, 0):
284
- if self.isLightTheme:
285
- helpLCol = textLNess + 0.35*(backLNess - textLNess)
286
- else:
287
- helpLCol = backLNess + 0.65*(textLNess - backLNess)
288
- self.helpText = QColor.fromHsl(0, 0, int(255*helpLCol))
289
- logger.debug(
290
- "Computed help text colour: rgb(%d, %d, %d)",
291
- self.helpText.red(), self.helpText.green(), self.helpText.blue()
292
- )
293
-
294
- # Icons
295
- defaultIcons = "typicons_light" if backLNess >= 0.5 else "typicons_dark"
296
- self.iconCache.loadTheme(self.themeIcons or defaultIcons)
297
-
298
- # Apply Styles
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
- guiSyntax = CONFIG.guiSyntax
309
- if guiSyntax not in self._availSyntax:
310
- logger.error("Could not find syntax theme '%s'", guiSyntax)
311
- guiSyntax = "default_light"
312
- CONFIG.guiSyntax = guiSyntax
313
-
314
- syntaxFile = self._availSyntax.get(guiSyntax, None)
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'", guiSyntax)
320
-
321
- confParser = NWConfigParser()
374
+ logger.info("Loading syntax theme '%s'", theme)
375
+ parser = NWConfigParser()
322
376
  try:
323
- with open(syntaxFile, mode="r", encoding="utf-8") as inFile:
324
- confParser.read_file(inFile)
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 load syntax colours from: %s", syntaxFile)
380
+ logger.error("Could not read file: %s", file)
327
381
  logException()
328
382
  return False
329
383
 
330
384
  # Main
331
- cnfSec = "Main"
332
- if confParser.has_section(cnfSec):
333
- self.syntaxName = confParser.rdStr(cnfSec, "name", "")
334
- self.syntaxDescription = confParser.rdStr(cnfSec, "description", "N/A")
335
- self.syntaxAuthor = confParser.rdStr(cnfSec, "author", "N/A")
336
- self.syntaxCredit = confParser.rdStr(cnfSec, "credit", "N/A")
337
- self.syntaxUrl = confParser.rdStr(cnfSec, "url", "")
338
- self.syntaxLicense = confParser.rdStr(cnfSec, "license", "N/A")
339
- self.syntaxLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "")
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
- cnfSec = "Syntax"
343
- if confParser.has_section(cnfSec):
344
- self.colBack = self._parseColour(confParser, cnfSec, "background")
345
- self.colText = self._parseColour(confParser, cnfSec, "text")
346
- self.colLink = self._parseColour(confParser, cnfSec, "link")
347
- self.colHead = self._parseColour(confParser, cnfSec, "headertext")
348
- self.colHeadH = self._parseColour(confParser, cnfSec, "headertag")
349
- self.colEmph = self._parseColour(confParser, cnfSec, "emphasis")
350
- self.colDialN = self._parseColour(confParser, cnfSec, "dialog")
351
- self.colDialA = self._parseColour(confParser, cnfSec, "altdialog")
352
- self.colHidden = self._parseColour(confParser, cnfSec, "hidden")
353
- self.colNote = self._parseColour(confParser, cnfSec, "note")
354
- self.colCode = self._parseColour(confParser, cnfSec, "shortcode")
355
- self.colKey = self._parseColour(confParser, cnfSec, "keyword")
356
- self.colTag = self._parseColour(confParser, cnfSec, "tag")
357
- self.colVal = self._parseColour(confParser, cnfSec, "value")
358
- self.colOpt = self._parseColour(confParser, cnfSec, "optional")
359
- self.colSpell = self._parseColour(confParser, cnfSec, "spellcheckline")
360
- self.colError = self._parseColour(confParser, cnfSec, "errorline")
361
- self.colRepTag = self._parseColour(confParser, cnfSec, "replacetag")
362
- self.colMod = self._parseColour(confParser, cnfSec, "modifier")
363
- self.colMark = self._parseColour(confParser, cnfSec, "texthighlight")
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
- confParser = NWConfigParser()
373
- for themeKey, themePath in self._availThemes.items():
374
- logger.debug("Checking theme config for '%s'", themeKey)
375
- themeName = _loadInternalName(confParser, themePath)
376
- if themeName:
377
- self._themeList.append((themeKey, themeName))
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(self._themeList, key=_sortTheme)
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
- confParser = NWConfigParser()
389
- for syntaxKey, syntaxPath in self._availSyntax.items():
390
- logger.debug("Checking theme syntax for '%s'", syntaxKey)
391
- syntaxName = _loadInternalName(confParser, syntaxPath)
392
- if syntaxName:
393
- self._syntaxList.append((syntaxKey, syntaxName))
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(self._syntaxList, key=_sortTheme)
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 _resetGuiColors(self) -> None:
466
+ def _resetTheme(self) -> None:
408
467
  """Reset GUI colours to default values."""
409
- self.statNone = QColor(120, 120, 120)
410
- self.statUnsaved = QColor(200, 15, 39)
411
- self.statSaved = QColor(2, 133, 37)
412
- self.helpText = QColor(0, 0, 0)
413
- self.fadedText = QColor(128, 128, 128)
414
- self.errorText = QColor(255, 0, 0)
415
- return
416
-
417
- def _listConf(self, targetDict: dict, checkDir: Path) -> bool:
418
- """Scan for theme config files and populate the dictionary."""
419
- if not checkDir.is_dir():
420
- return False
421
-
422
- for checkFile in checkDir.iterdir():
423
- if checkFile.is_file() and checkFile.name.endswith(".conf"):
424
- targetDict[checkFile.name[:-5]] = checkFile
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 True
512
+ return
427
513
 
428
- def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> QColor:
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(self, parser: NWConfigParser, section: str,
433
- name: str, value: QPalette.ColorRole) -> None:
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.setColor(value, self._parseColour(parser, section, name))
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
- aPx = CONFIG.pxInt(2)
443
- bPx = CONFIG.pxInt(4)
444
- cPx = CONFIG.pxInt(6)
445
- dPx = CONFIG.pxInt(8)
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
- f"QTabWidget QTabBar::tab {{border: 0; padding: {bPx}px {dPx}px;}} "
454
- f"QTabWidget QTabBar::tab:selected {{color: {cssCol(hCol)};}} "
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
- f"QToolButton {{padding: {aPx}px; margin: 0; border: none; background: transparent;}} "
460
- f"QToolButton:hover {{border: none; background: {cssCol(tCol, 48)};}} "
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
- f"QToolButton {{padding: {cPx}px; margin: 0; border: none; background: transparent;}} "
467
- f"QToolButton:hover {{border: none; background: {cssCol(tCol, 48)};}} "
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. Only 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
- 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.
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
- ICON_KEYS: set[str] = {
489
- # Project and GUI Icons
490
- "novelwriter", "alert_error", "alert_info", "alert_question", "alert_warn",
491
- "build_excluded", "build_filtered", "build_included", "proj_chapter", "proj_details",
492
- "proj_document", "proj_folder", "proj_note", "proj_nwx", "proj_section", "proj_scene",
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._confName = "icons.conf"
550
- self._iconPath = CONFIG.assetPath("icons")
596
+ self._availThemes: dict[str, Path] = {}
597
+ self._themeList: list[tuple[str, str]] = []
551
598
 
552
599
  # None Icon
553
- self._noIcon = QIcon(str(self._iconPath / "none.svg"))
600
+ self._noIcon = QIcon(str(CONFIG.assetPath("icons") / "none.svg"))
554
601
 
555
- # Icon Theme Meta
556
- self.themeName = ""
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, iconTheme: str) -> bool:
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._themeMap = {}
576
- themePath = self._iconPath / iconTheme
577
- if not themePath.is_dir():
578
- themePath = CONFIG.dataPath("icons") / iconTheme
579
- if not themePath.is_dir():
580
- logger.warning("No icons loaded for '%s'", iconTheme)
581
- return False
582
-
583
- themeConf = themePath / self._confName
584
- logger.info("Loading icon theme '%s'", iconTheme)
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
- with open(themeConf, mode="r", encoding="utf-8") as inFile:
590
- confParser.read_file(inFile)
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 load icon theme settings from: %s", themeConf)
655
+ logger.error("Could not read file: %s", file)
593
656
  logException()
594
657
  return False
595
658
 
596
- # Main
597
- cnfSec = "Main"
598
- if confParser.has_section(cnfSec):
599
- self.themeName = confParser.rdStr(cnfSec, "name", "")
600
- self.themeDescription = confParser.rdStr(cnfSec, "description", "")
601
- self.themeAuthor = confParser.rdStr(cnfSec, "author", "N/A")
602
- self.themeCredit = confParser.rdStr(cnfSec, "credit", "N/A")
603
- self.themeUrl = confParser.rdStr(cnfSec, "url", "")
604
- self.themeLicense = confParser.rdStr(cnfSec, "license", "N/A")
605
- self.themeLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "")
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 loadDecoration(self, name: str, w: int | None = None, h: int | None = None) -> QPixmap:
646
- """Load graphical decoration element based on the decoration
647
- map or the icon map. This function always returns a QPixmap.
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
- if name in self._qIcons:
676
- return self._qIcons[name]
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[name] = icon
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. or load it."""
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 getPixmap(self, name: str, size: tuple[int, int]) -> QPixmap:
694
- """Return an icon from the icon buffer as a QPixmap. If it
695
- doesn't exist, return an empty QPixmap.
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
- iconName = None
715
+ name = None
716
+ color = "default"
705
717
  if tType == nwItemType.ROOT:
706
- iconName = nwLabels.CLASS_ICON[tClass]
718
+ name = nwLabels.CLASS_ICON[tClass]
719
+ color = "root"
707
720
  elif tType == nwItemType.FOLDER:
708
- iconName = "proj_folder"
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
- iconName = "proj_title"
726
+ name = "prj_title"
727
+ color = "title"
714
728
  elif hLevel == "H2":
715
- iconName = "proj_chapter"
729
+ name = "prj_chapter"
730
+ color = "chapter"
716
731
  elif hLevel == "H3":
717
- iconName = "proj_scene"
718
- elif hLevel == "H4":
719
- iconName = "proj_section"
732
+ name = "prj_scene"
733
+ color = "scene"
734
+ else:
735
+ name = "prj_document"
736
+ color = "file"
720
737
  elif tLayout == nwItemLayout.NOTE:
721
- iconName = "proj_note"
722
- if iconName is None:
738
+ name = "prj_note"
739
+ color = "note"
740
+ if name is None:
723
741
  return self._noIcon
724
742
 
725
- return self.getIcon(iconName)
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.loadDecoration("deco_doc_h0", h=iPx),
733
- self.loadDecoration("deco_doc_h1", h=iPx),
734
- self.loadDecoration("deco_doc_h2", h=iPx),
735
- self.loadDecoration("deco_doc_h3", h=iPx),
736
- self.loadDecoration("deco_doc_h4", h=iPx),
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.loadDecoration("deco_doc_h0_n", h=iPx),
746
- self.loadDecoration("deco_doc_h1_n", h=iPx),
747
- self.loadDecoration("deco_doc_h2_n", h=iPx),
748
- self.loadDecoration("deco_doc_h3_n", h=iPx),
749
- self.loadDecoration("deco_doc_h4_n", h=iPx),
750
- self.loadDecoration("deco_doc_nt_n", h=iPx),
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(self._iconPath / "novelwriter.svg"))
830
+ return QIcon(str(CONFIG.assetPath("icons") / "novelwriter.svg"))
769
831
  elif name == "proj_nwx":
770
- return QIcon(str(self._iconPath / "x-novelwriter-project.svg"))
832
+ return QIcon(str(CONFIG.assetPath("icons") / "x-novelwriter-project.svg"))
771
833
 
772
- # Otherwise, we load from the theme folder
773
- if name in self._themeMap:
774
- logger.debug("Loading: %s", self._themeMap[name].name)
775
- return QIcon(str(self._themeMap[name]))
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(confParser: NWConfigParser, confFile: str | Path) -> str:
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(confFile, mode="r", encoding="utf-8") as inFile:
797
- confParser.read_file(inFile)
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 load file: %s", confFile)
890
+ logger.error("Could not read file: %s", path)
800
891
  logException()
801
- return ""
892
+ return ""
802
893
 
803
- return confParser.rdStr("Main", "name", "")
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 ""