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
@@ -3,9 +3,11 @@ novelWriter – GUI Novel Tree
3
3
  ============================
4
4
 
5
5
  File History:
6
- Created: 2020-12-20 [1.1rc1] GuiNovelTree
7
- Created: 2022-06-12 [2.0rc1] GuiNovelView
8
- Created: 2022-06-12 [2.0rc1] GuiNovelToolBar
6
+ Created: 2020-12-20 [1.1rc1] GuiNovelTree
7
+ Created: 2022-06-12 [2.0rc1] GuiNovelView
8
+ Created: 2022-06-12 [2.0rc1] GuiNovelToolBar
9
+ Rewritten: 2025-02-22 [2.7b1] GuiNovelView
10
+ Rewritten: 2025-02-22 [2.7b1] GuiNovelToolBar
9
11
 
10
12
  This file is a part of novelWriter
11
13
  Copyright (C) 2020 Veronica Berglyd Olsen and novelWriter contributors
@@ -28,40 +30,30 @@ from __future__ import annotations
28
30
  import logging
29
31
 
30
32
  from enum import Enum
31
- from time import time
32
33
 
33
- from PyQt5.QtCore import QModelIndex, QPoint, Qt, pyqtSignal, pyqtSlot
34
- from PyQt5.QtGui import QFocusEvent, QFont, QMouseEvent, QPalette, QResizeEvent
35
- from PyQt5.QtWidgets import (
36
- QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QInputDialog, QMenu,
37
- QToolTip, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
34
+ from PyQt6.QtCore import QModelIndex, QPoint, pyqtSignal, pyqtSlot
35
+ from PyQt6.QtGui import QActionGroup, QFont, QPainter, QPalette, QResizeEvent
36
+ from PyQt6.QtWidgets import (
37
+ QAbstractItemView, QFrame, QHBoxLayout, QInputDialog, QMenu,
38
+ QStyleOptionViewItem, QToolTip, QVBoxLayout, QWidget
38
39
  )
39
40
 
40
41
  from novelwriter import CONFIG, SHARED
41
- from novelwriter.common import minmax, qtLambda
42
- from novelwriter.constants import nwKeyWords, nwLabels, nwStyles, trConst
43
- from novelwriter.core.index import IndexHeading
44
- from novelwriter.enum import nwChange, nwDocMode, nwItemClass, nwOutline
45
- from novelwriter.extensions.modified import NIconToolButton
42
+ from novelwriter.common import minmax, qtAddAction, qtAddMenu, qtLambda
43
+ from novelwriter.constants import nwKeyWords, nwLabels, trConst
44
+ from novelwriter.core.novelmodel import NovelModel
45
+ from novelwriter.enum import nwChange, nwDocMode, nwNovelExtra, nwOutline
46
+ from novelwriter.extensions.modified import NIconToolButton, NTreeView
46
47
  from novelwriter.extensions.novelselector import NovelSelector
47
48
  from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
48
49
  from novelwriter.types import (
49
- QtAlignRight, QtDecoration, QtHeaderStretch, QtHeaderToContents,
50
- QtMouseLeft, QtMouseMiddle, QtScrollAlwaysOff, QtScrollAsNeeded,
51
- QtSizeExpanding, QtUserRole
50
+ QtHeaderStretch, QtHeaderToContents, QtScrollAlwaysOff, QtScrollAsNeeded,
51
+ QtSizeExpanding
52
52
  )
53
53
 
54
54
  logger = logging.getLogger(__name__)
55
55
 
56
56
 
57
- class NovelTreeColumn(Enum):
58
-
59
- HIDDEN = 0
60
- POV = 1
61
- FOCUS = 2
62
- PLOT = 3
63
-
64
-
65
57
  class GuiNovelView(QWidget):
66
58
 
67
59
  # Signals for user interaction with the novel tree
@@ -86,6 +78,7 @@ class GuiNovelView(QWidget):
86
78
  self.setLayout(self.outerBox)
87
79
 
88
80
  # Function Mappings
81
+ self.setActive = self.novelBar.setActive
89
82
  self.getSelectedHandle = self.novelTree.getSelectedHandle
90
83
 
91
84
  return
@@ -97,8 +90,6 @@ class GuiNovelView(QWidget):
97
90
  def updateTheme(self) -> None:
98
91
  """Update theme elements."""
99
92
  self.novelBar.updateTheme()
100
- self.novelTree.updateTheme()
101
- self.refreshTree()
102
93
  return
103
94
 
104
95
  def initSettings(self) -> None:
@@ -108,21 +99,18 @@ class GuiNovelView(QWidget):
108
99
 
109
100
  def clearNovelView(self) -> None:
110
101
  """Clear project-related GUI content."""
111
- self.novelTree.clearContent()
112
102
  self.novelBar.clearContent()
113
103
  self.novelBar.setEnabled(False)
104
+ self.novelTree.clearContent()
114
105
  return
115
106
 
116
107
  def openProjectTasks(self) -> None:
117
108
  """Run open project tasks."""
118
- lastNovel = SHARED.project.data.getLastHandle("novelTree")
119
- if lastNovel and lastNovel not in SHARED.project.tree:
120
- lastNovel = SHARED.project.tree.findRoot(nwItemClass.NOVEL)
121
-
109
+ lastNovel = SHARED.project.data.getLastHandle("novel")
122
110
  logger.debug("Setting novel tree to root item '%s'", lastNovel)
123
111
 
124
112
  lastCol = SHARED.project.options.getEnum(
125
- "GuiNovelView", "lastCol", NovelTreeColumn, NovelTreeColumn.HIDDEN
113
+ "GuiNovelView", "lastCol", nwNovelExtra, nwNovelExtra.HIDDEN
126
114
  )
127
115
  lastColSize = SHARED.project.options.getInt(
128
116
  "GuiNovelView", "lastColSize", 25
@@ -140,13 +128,17 @@ class GuiNovelView(QWidget):
140
128
 
141
129
  def closeProjectTasks(self) -> None:
142
130
  """Run closing project tasks."""
131
+ logger.debug("Saving State: GuiNovelView")
132
+
143
133
  lastColType = self.novelTree.lastColType
144
134
  lastColSize = self.novelTree.lastColSize
145
- logger.debug("Saving State: GuiNovelView")
146
- pOptions = SHARED.project.options
147
- pOptions.setValue("GuiNovelView", "lastCol", lastColType)
148
- pOptions.setValue("GuiNovelView", "lastColSize", lastColSize)
135
+
136
+ options = SHARED.project.options
137
+ options.setValue("GuiNovelView", "lastCol", lastColType)
138
+ options.setValue("GuiNovelView", "lastColSize", lastColSize)
139
+
149
140
  self.clearNovelView()
141
+
150
142
  return
151
143
 
152
144
  def setTreeFocus(self) -> None:
@@ -162,32 +154,24 @@ class GuiNovelView(QWidget):
162
154
  # Public Slots
163
155
  ##
164
156
 
157
+ @pyqtSlot(str)
158
+ def setCurrentNovel(self, rootHandle: str | None) -> None:
159
+ """Set the current novel to display."""
160
+ self.novelTree.setNovelModel(rootHandle)
161
+ return
162
+
165
163
  @pyqtSlot(str)
166
164
  def setActiveHandle(self, tHandle: str) -> None:
167
165
  """Highlight the rows associated with a given handle."""
168
166
  self.novelTree.setActiveHandle(tHandle)
169
167
  return
170
168
 
171
- @pyqtSlot()
172
- def refreshTree(self) -> None:
173
- """Refresh the current tree."""
174
- self.novelTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("novelTree"))
175
- return
176
-
177
169
  @pyqtSlot(str, Enum)
178
170
  def updateRootItem(self, tHandle: str, change: nwChange) -> None:
179
171
  """If any root item changes, rebuild the novel root menu."""
180
172
  self.novelBar.buildNovelRootMenu()
181
173
  return
182
174
 
183
- @pyqtSlot(str)
184
- def updateNovelItemMeta(self, tHandle: str) -> None:
185
- """The meta data of a novel item has changed, and the tree item
186
- needs to be refreshed.
187
- """
188
- self.novelTree.refreshHandle(tHandle)
189
- return
190
-
191
175
 
192
176
  class GuiNovelToolBar(QWidget):
193
177
 
@@ -198,10 +182,13 @@ class GuiNovelToolBar(QWidget):
198
182
 
199
183
  self.novelView = novelView
200
184
 
185
+ self._active = False
186
+ self._refresh: dict[str, bool] = {}
187
+
201
188
  iSz = SHARED.theme.baseIconSize
202
- mPx = CONFIG.pxInt(2)
203
189
 
204
190
  self.setContentsMargins(0, 0, 0, 0)
191
+ self.setBackgroundRole(QPalette.ColorRole.Base)
205
192
  self.setAutoFillBackground(True)
206
193
 
207
194
  # Novel Selector
@@ -211,7 +198,7 @@ class GuiNovelToolBar(QWidget):
211
198
  self.novelValue = NovelSelector(self)
212
199
  self.novelValue.setFont(selFont)
213
200
  self.novelValue.setListFormat(self.tr("Outline of {0}"))
214
- self.novelValue.setMinimumWidth(CONFIG.pxInt(150))
201
+ self.novelValue.setMinimumWidth(150)
215
202
  self.novelValue.setSizePolicy(QtSizeExpanding, QtSizeExpanding)
216
203
  self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot)
217
204
 
@@ -222,21 +209,21 @@ class GuiNovelToolBar(QWidget):
222
209
  # Refresh Button
223
210
  self.tbRefresh = NIconToolButton(self, iSz)
224
211
  self.tbRefresh.setToolTip(self.tr("Refresh"))
225
- self.tbRefresh.clicked.connect(self._refreshNovelTree)
212
+ self.tbRefresh.clicked.connect(self._forceRefreshNovelTree)
226
213
 
227
214
  # More Options Menu
228
215
  self.mMore = QMenu(self)
229
216
 
230
- self.mLastCol = self.mMore.addMenu(self.tr("Last Column"))
217
+ self.mLastCol = qtAddMenu(self.mMore, self.tr("Last Column"))
231
218
  self.gLastCol = QActionGroup(self.mMore)
232
219
  self.aLastCol = {}
233
- self._addLastColAction(NovelTreeColumn.HIDDEN, self.tr("Hidden"))
234
- self._addLastColAction(NovelTreeColumn.POV, self.tr("Point of View Character"))
235
- self._addLastColAction(NovelTreeColumn.FOCUS, self.tr("Focus Character"))
236
- self._addLastColAction(NovelTreeColumn.PLOT, self.tr("Novel Plot"))
220
+ self._addLastColAction(nwNovelExtra.HIDDEN, self.tr("Hidden"))
221
+ self._addLastColAction(nwNovelExtra.POV, self.tr("Point of View Character"))
222
+ self._addLastColAction(nwNovelExtra.FOCUS, self.tr("Focus Character"))
223
+ self._addLastColAction(nwNovelExtra.PLOT, self.tr("Novel Plot"))
237
224
 
238
225
  self.mLastCol.addSeparator()
239
- self.aLastColSize = self.mLastCol.addAction(self.tr("Column Size"))
226
+ self.aLastColSize = qtAddAction(self.mLastCol, self.tr("Column Size"))
240
227
  self.aLastColSize.triggered.connect(self._selectLastColumnSize)
241
228
 
242
229
  self.tbMore = NIconToolButton(self, iSz)
@@ -249,13 +236,16 @@ class GuiNovelToolBar(QWidget):
249
236
  self.outerBox.addWidget(self.tbNovel)
250
237
  self.outerBox.addWidget(self.tbRefresh)
251
238
  self.outerBox.addWidget(self.tbMore)
252
- self.outerBox.setContentsMargins(mPx, mPx, 0, mPx)
239
+ self.outerBox.setContentsMargins(4, 2, 0, 2)
253
240
  self.outerBox.setSpacing(0)
254
241
 
255
242
  self.setLayout(self.outerBox)
256
243
 
257
244
  self.updateTheme()
258
245
 
246
+ # Connect Signals
247
+ SHARED.novelStructureChanged.connect(self._refreshNovelTree)
248
+
259
249
  logger.debug("Ready: GuiNovelToolBar")
260
250
 
261
251
  return
@@ -267,13 +257,9 @@ class GuiNovelToolBar(QWidget):
267
257
  def updateTheme(self) -> None:
268
258
  """Update theme elements."""
269
259
  # Icons
270
- self.tbNovel.setThemeIcon("cls_novel")
271
- self.tbRefresh.setThemeIcon("refresh")
272
- self.tbMore.setThemeIcon("menu")
273
-
274
- qPalette = self.palette()
275
- qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
276
- self.setPalette(qPalette)
260
+ self.tbNovel.setThemeIcon("cls_novel", "red")
261
+ self.tbRefresh.setThemeIcon("refresh", "green")
262
+ self.tbMore.setThemeIcon("more_vertical")
277
263
 
278
264
  # StyleSheets
279
265
  buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
@@ -285,9 +271,11 @@ class GuiNovelToolBar(QWidget):
285
271
  "QComboBox {border-style: none; padding-left: 0;} "
286
272
  "QComboBox::drop-down {border-style: none}"
287
273
  )
288
- self.novelValue.refreshNovelList()
274
+ self.novelValue.updateTheme()
289
275
  self.tbNovel.setVisible(self.novelValue.count() > 1)
290
276
 
277
+ self._forceRefreshNovelTree()
278
+
291
279
  return
292
280
 
293
281
  def clearContent(self) -> None:
@@ -299,19 +287,39 @@ class GuiNovelToolBar(QWidget):
299
287
  def buildNovelRootMenu(self) -> None:
300
288
  """Build the novel root menu."""
301
289
  self.novelValue.refreshNovelList()
290
+ self.novelView.setCurrentNovel(self.novelValue.handle)
302
291
  self.tbNovel.setVisible(self.novelValue.count() > 1)
303
292
  return
304
293
 
305
294
  def setCurrentRoot(self, rootHandle: str | None) -> None:
306
295
  """Set the current active root handle."""
296
+ if rootHandle is None or rootHandle not in SHARED.project.tree:
297
+ rootHandle = self.novelValue.firstHandle
307
298
  self.novelValue.setHandle(rootHandle)
308
- self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True)
299
+ SHARED.project.data.setLastHandle(rootHandle, "novel")
300
+ self.novelView.setCurrentNovel(rootHandle)
309
301
  return
310
302
 
311
- def setLastColType(self, colType: NovelTreeColumn, doRefresh: bool = True) -> None:
303
+ def setLastColType(self, colType: nwNovelExtra, doRefresh: bool = True) -> None:
312
304
  """Set the last column type."""
313
305
  self.aLastCol[colType].setChecked(True)
314
- self.novelView.novelTree.setLastColType(colType, doRefresh=doRefresh)
306
+ self.novelView.novelTree.setLastColType(colType)
307
+ if doRefresh:
308
+ self._forceRefreshNovelTree()
309
+ self.novelView.novelTree.resizeColumns()
310
+ return
311
+
312
+ def setActive(self, state: bool) -> None:
313
+ """Set the widget active state, which enables automatic tree
314
+ refresh when content structure changes.
315
+ """
316
+ self._active = state
317
+ if (
318
+ self._active
319
+ and (handle := self.novelValue.handle)
320
+ and self._refresh.get(handle, False)
321
+ ):
322
+ self._refreshNovelTree(self.novelValue.handle)
315
323
  return
316
324
 
317
325
  ##
@@ -319,10 +327,22 @@ class GuiNovelToolBar(QWidget):
319
327
  ##
320
328
 
321
329
  @pyqtSlot()
322
- def _refreshNovelTree(self) -> None:
330
+ def _forceRefreshNovelTree(self) -> None:
323
331
  """Rebuild the current tree."""
324
- rootHandle = SHARED.project.data.getLastHandle("novelTree")
325
- self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True)
332
+ if tHandle := self.novelValue.handle:
333
+ self.novelView.setCurrentNovel(tHandle)
334
+ SHARED.project.index.refreshNovelModel(tHandle)
335
+ self._refresh[tHandle] = False
336
+ return
337
+
338
+ @pyqtSlot(str)
339
+ def _refreshNovelTree(self, tHandle: str) -> None:
340
+ """Refresh or schedule refresh of a novel tree."""
341
+ if self._active:
342
+ SHARED.project.index.refreshNovelModel(tHandle)
343
+ self._refresh[tHandle] = False
344
+ else:
345
+ self._refresh[tHandle] = True
326
346
  return
327
347
 
328
348
  @pyqtSlot()
@@ -334,16 +354,16 @@ class GuiNovelToolBar(QWidget):
334
354
  )
335
355
  if isOk:
336
356
  self.novelView.novelTree.setLastColSize(newSize)
337
- self._refreshNovelTree()
357
+ self.novelView.novelTree.resizeColumns()
338
358
  return
339
359
 
340
360
  ##
341
361
  # Internal Functions
342
362
  ##
343
363
 
344
- def _addLastColAction(self, colType: NovelTreeColumn, actionLabel: str) -> None:
364
+ def _addLastColAction(self, colType: nwNovelExtra, actionLabel: str) -> None:
345
365
  """Add a column selection entry to the last column menu."""
346
- aLast = self.mLastCol.addAction(actionLabel)
366
+ aLast = qtAddAction(self.mLastCol, actionLabel)
347
367
  aLast.setCheckable(True)
348
368
  aLast.setActionGroup(self.gLastCol)
349
369
  aLast.triggered.connect(qtLambda(self.setLastColType, colType))
@@ -351,18 +371,7 @@ class GuiNovelToolBar(QWidget):
351
371
  return
352
372
 
353
373
 
354
- class GuiNovelTree(QTreeWidget):
355
-
356
- C_DATA = 0
357
- C_TITLE = 0
358
- C_WORDS = 1
359
- C_EXTRA = 2
360
- C_MORE = 3
361
-
362
- D_HANDLE = QtUserRole
363
- D_TITLE = QtUserRole + 1
364
- D_KEY = QtUserRole + 2
365
- D_EXTRA = QtUserRole + 3
374
+ class GuiNovelTree(NTreeView):
366
375
 
367
376
  def __init__(self, novelView: GuiNovelView) -> None:
368
377
  super().__init__(parent=novelView)
@@ -372,63 +381,30 @@ class GuiNovelTree(QTreeWidget):
372
381
  self.novelView = novelView
373
382
 
374
383
  # Internal Variables
375
- self._lastBuild = 0
376
- self._lastCol = NovelTreeColumn.POV
377
- self._lastColSize = 0.25
378
384
  self._actHandle = None
379
- self._treeMap: dict[str, QTreeWidgetItem] = {}
380
-
381
- # Cached Strings
382
- self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
383
- self._focLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])
384
- self._pltLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])
385
-
386
- # Build GUI
387
- # =========
388
-
389
- iPx = SHARED.theme.baseIconHeight
390
- iSz = SHARED.theme.baseIconSize
391
- cMg = CONFIG.pxInt(6)
385
+ self._lastColType = nwNovelExtra.POV
386
+ self._lastColSize = 0.25
392
387
 
393
- self.setIconSize(iSz)
388
+ # Widget Setup
389
+ self.setIconSize(SHARED.theme.baseIconSize)
394
390
  self.setFrameStyle(QFrame.Shape.NoFrame)
395
391
  self.setUniformRowHeights(True)
396
392
  self.setAllColumnsShowFocus(True)
397
393
  self.setHeaderHidden(True)
398
- self.setIndentation(0)
399
- self.setColumnCount(4)
400
- self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
401
- self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
402
- self.setExpandsOnDoubleClick(False)
394
+ self.setIndentation(2)
403
395
  self.setDragEnabled(False)
404
396
 
405
- # Lock the column sizes
406
- treeHeader = self.header()
407
- treeHeader.setStretchLastSection(False)
408
- treeHeader.setMinimumSectionSize(iPx + cMg)
409
- treeHeader.setSectionResizeMode(self.C_TITLE, QtHeaderStretch)
410
- treeHeader.setSectionResizeMode(self.C_WORDS, QtHeaderToContents)
411
- treeHeader.setSectionResizeMode(self.C_EXTRA, QtHeaderToContents)
412
- treeHeader.setSectionResizeMode(self.C_MORE, QtHeaderToContents)
413
-
414
- # Pre-Generate Tree Formatting
415
- fH1 = self.font()
416
- fH1.setBold(True)
417
- fH1.setUnderline(True)
418
-
419
- fH2 = self.font()
420
- fH2.setBold(True)
421
-
422
- self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
397
+ # Set selection options
398
+ self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
399
+ self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
423
400
 
424
401
  # Connect signals
425
- self.clicked.connect(self._treeItemClicked)
426
- self.itemDoubleClicked.connect(self._treeDoubleClick)
427
- self.itemSelectionChanged.connect(self._treeSelectionChange)
402
+ self.clicked.connect(self._onSingleClick)
403
+ self.doubleClicked.connect(self._onDoubleClick)
404
+ self.middleClicked.connect(self._onMiddleClick)
428
405
 
429
406
  # Set custom settings
430
407
  self.initSettings()
431
- self.updateTheme()
432
408
 
433
409
  logger.debug("Ready: GuiNovelTree")
434
410
 
@@ -436,23 +412,14 @@ class GuiNovelTree(QTreeWidget):
436
412
 
437
413
  def initSettings(self) -> None:
438
414
  """Set or update tree widget settings."""
439
- # Scroll bars
440
415
  if CONFIG.hideVScroll:
441
416
  self.setVerticalScrollBarPolicy(QtScrollAlwaysOff)
442
417
  else:
443
418
  self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
444
-
445
419
  if CONFIG.hideHScroll:
446
420
  self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
447
421
  else:
448
422
  self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
449
-
450
- return
451
-
452
- def updateTheme(self) -> None:
453
- """Update theme elements."""
454
- iPx = SHARED.theme.baseIconHeight
455
- self._pMore = SHARED.theme.loadDecoration("deco_doc_more", h=iPx)
456
423
  return
457
424
 
458
425
  ##
@@ -460,313 +427,180 @@ class GuiNovelTree(QTreeWidget):
460
427
  ##
461
428
 
462
429
  @property
463
- def lastColType(self) -> NovelTreeColumn:
464
- return self._lastCol
430
+ def lastColType(self) -> nwNovelExtra:
431
+ """The data type of the extra column."""
432
+ return self._lastColType
465
433
 
466
434
  @property
467
435
  def lastColSize(self) -> int:
436
+ """Return the size of the extra column."""
468
437
  return int(self._lastColSize * 100)
469
438
 
470
439
  ##
471
- # Class Methods
440
+ # Getters
472
441
  ##
473
442
 
474
- def clearContent(self) -> None:
475
- """Clear the GUI content and the related maps."""
476
- self.clear()
477
- self._treeMap = {}
478
- self._lastBuild = 0
479
- return
480
-
481
- def refreshTree(self, rootHandle: str | None = None, overRide: bool = False) -> None:
482
- """Refresh the tree if it has been changed."""
483
- logger.debug("Requesting refresh of the novel tree")
484
- if rootHandle is None:
485
- rootHandle = SHARED.project.tree.findRoot(nwItemClass.NOVEL)
486
-
487
- titleKey = None
488
- if selItems := self.selectedItems():
489
- titleKey = selItems[0].data(self.C_DATA, self.D_KEY)
490
-
491
- self._populateTree(rootHandle)
492
- SHARED.project.data.setLastHandle(rootHandle, "novelTree")
493
-
494
- if titleKey is not None and titleKey in self._treeMap:
495
- self._treeMap[titleKey].setSelected(True)
496
-
497
- return
498
-
499
- def refreshHandle(self, tHandle: str) -> None:
500
- """Refresh the data for a given handle."""
501
- if idxData := SHARED.project.index.getItemData(tHandle):
502
- logger.debug("Refreshing meta data for item '%s'", tHandle)
503
- for sTitle, tHeading in idxData.items():
504
- sKey = f"{tHandle}:{sTitle}"
505
- if trItem := self._treeMap.get(sKey, None):
506
- self._updateTreeItemValues(trItem, tHeading, tHandle, sTitle)
507
- else:
508
- logger.debug("Heading '%s' not in novel tree", sKey)
509
- self.refreshTree()
510
- return
511
- return
512
-
513
443
  def getSelectedHandle(self) -> tuple[str | None, str | None]:
514
444
  """Get the currently selected or active handle. If multiple
515
445
  items are selected, return the first.
516
446
  """
517
- selList = self.selectedItems()
518
- trItem = selList[0] if selList else self.currentItem()
519
- if isinstance(trItem, QTreeWidgetItem):
520
- tHandle = trItem.data(self.C_DATA, self.D_HANDLE)
521
- sTitle = trItem.data(self.C_DATA, self.D_TITLE)
522
- return tHandle, sTitle
447
+ if (model := self._getModel()) and (index := self.currentIndex()).isValid():
448
+ return model.handle(index), model.key(index)
523
449
  return None, None
524
450
 
525
- def setLastColType(self, colType: NovelTreeColumn, doRefresh: bool = True) -> None:
526
- """Change the content type of the last column and rebuild."""
527
- if self._lastCol != colType:
528
- logger.debug("Changing last column to %s", colType.name)
529
- self._lastCol = colType
530
- self.setColumnHidden(self.C_EXTRA, colType == NovelTreeColumn.HIDDEN)
531
- if doRefresh:
532
- lastNovel = SHARED.project.data.getLastHandle("novelTree")
533
- self.refreshTree(rootHandle=lastNovel, overRide=True)
534
- return
451
+ ##
452
+ # Setters
453
+ ##
535
454
 
536
- def setLastColSize(self, colSize: int) -> None:
537
- """Set the column size in integer values between 15 and 75."""
538
- self._lastColSize = minmax(colSize, 15, 75)/100.0
455
+ def setNovelModel(self, tHandle: str | None) -> None:
456
+ """Set the current novel model."""
457
+ if tHandle and (model := SHARED.project.index.getNovelModel(tHandle)):
458
+ if model is not self.model():
459
+ self.setModel(model)
460
+ self.resizeColumns()
461
+ else:
462
+ self.clearContent()
539
463
  return
540
464
 
541
465
  def setActiveHandle(self, tHandle: str | None) -> None:
542
- """Highlight the rows associated with a given handle."""
543
- didScroll = False
544
- brushOn = self.palette().alternateBase()
545
- brushOff = self.palette().base()
546
- if pHandle := self._actHandle:
547
- for key, item in self._treeMap.items():
548
- if key.startswith(pHandle):
549
- for i in range(self.columnCount()):
550
- item.setBackground(i, brushOff)
551
- if tHandle:
552
- for key, item in self._treeMap.items():
553
- if key.startswith(tHandle):
554
- for i in range(self.columnCount()):
555
- item.setBackground(i, brushOn)
556
- if not didScroll:
557
- self.scrollToItem(item, QAbstractItemView.ScrollHint.PositionAtCenter)
558
- didScroll = True
559
- self._actHandle = tHandle or None
466
+ """Set the handle to be highlighted."""
467
+ self._actHandle = tHandle
468
+ if viewport := self.viewport():
469
+ viewport.repaint()
560
470
  return
561
471
 
562
- ##
563
- # Events
564
- ##
472
+ def setLastColType(self, colType: nwNovelExtra) -> None:
473
+ """Set the extra column type."""
474
+ self._lastColType = colType
475
+ SHARED.project.index.setNovelModelExtraColumn(colType)
476
+ return
565
477
 
566
- def mousePressEvent(self, event: QMouseEvent) -> None:
567
- """Overload mousePressEvent to clear selection if clicking the
568
- mouse in a blank area of the tree view, and to load a document
569
- for viewing if the user middle-clicked.
570
- """
571
- super().mousePressEvent(event)
478
+ def setLastColSize(self, colSize: int) -> None:
479
+ """Set the extra column size between 15% and 75%."""
480
+ self._lastColSize = minmax(colSize, 15, 75)/100.0
481
+ return
572
482
 
573
- if event.button() == QtMouseLeft:
574
- selItem = self.indexAt(event.pos())
575
- if not selItem.isValid():
576
- self.clearSelection()
483
+ ##
484
+ # Class Methods
485
+ ##
577
486
 
578
- elif event.button() == QtMouseMiddle:
579
- selItem = self.itemAt(event.pos())
580
- if not isinstance(selItem, QTreeWidgetItem):
581
- return
487
+ def clearContent(self) -> None:
488
+ """Clear the tree view."""
489
+ self.setModel(None)
490
+ return
582
491
 
583
- tHandle, sTitle = self.getSelectedHandle()
584
- if tHandle is None:
585
- return
492
+ def resizeColumns(self) -> None:
493
+ """Set the correct column sizes."""
494
+ if (header := self.header()) and (model := self._getModel()) and (vp := self.viewport()):
495
+ header.setStretchLastSection(False)
496
+ header.setMinimumSectionSize(SHARED.theme.baseIconHeight + 6)
497
+ header.setSectionResizeMode(0, QtHeaderStretch)
498
+ header.setSectionResizeMode(1, QtHeaderToContents)
499
+ header.setSectionResizeMode(2, QtHeaderToContents)
500
+ if model.columns == 4:
501
+ header.setSectionResizeMode(3, QtHeaderToContents)
502
+ header.setMaximumSectionSize(int(self._lastColSize * vp.width()))
503
+ return
586
504
 
587
- self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle or "", False)
505
+ ##
506
+ # Overloads
507
+ ##
588
508
 
509
+ def drawRow(self, painter: QPainter, opt: QStyleOptionViewItem, index: QModelIndex) -> None:
510
+ """Draw a box on the active row."""
511
+ if (model := self._getModel()) and model.handle(index) == self._actHandle:
512
+ painter.fillRect(opt.rect, self.palette().alternateBase())
513
+ super().drawRow(painter, opt, index)
589
514
  return
590
515
 
591
- def focusOutEvent(self, event: QFocusEvent) -> None:
592
- """Clear the selection when the tree no longer has focus."""
593
- super().focusOutEvent(event)
594
- self.clearSelection()
595
- return
516
+ ##
517
+ # Events
518
+ ##
596
519
 
597
520
  def resizeEvent(self, event: QResizeEvent) -> None:
598
- """Elide labels in the extra column."""
521
+ """Process size changed."""
599
522
  super().resizeEvent(event)
600
- newW = event.size().width()
601
- oldW = event.oldSize().width()
602
- if newW != oldW:
603
- eliW = int(self._lastColSize * newW)
604
- fMetric = self.fontMetrics()
605
- for i in range(self.topLevelItemCount()):
606
- trItem = self.topLevelItem(i)
607
- if isinstance(trItem, QTreeWidgetItem):
608
- lastText = trItem.data(self.C_DATA, self.D_EXTRA)
609
- trItem.setText(
610
- self.C_EXTRA,
611
- fMetric.elidedText(lastText, Qt.TextElideMode.ElideRight, eliW)
612
- )
523
+ self.resizeColumns()
613
524
  return
614
525
 
615
526
  ##
616
527
  # Private Slots
617
528
  ##
618
529
 
619
- @pyqtSlot("QModelIndex")
620
- def _treeItemClicked(self, index: QModelIndex) -> None:
621
- """The user clicked on an item in the tree."""
622
- if index.column() == self.C_MORE:
623
- tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE)
624
- sTitle = index.siblingAtColumn(self.C_DATA).data(self.D_TITLE)
625
- tipPos = self.mapToGlobal(self.visualRect(index).topRight())
626
- self._popMetaBox(tipPos, tHandle, sTitle)
627
- return
628
-
629
- @pyqtSlot()
630
- def _treeSelectionChange(self) -> None:
631
- """Extract the handle and line number of the currently selected
632
- title, and send it to the tree meta panel.
633
- """
634
- tHandle, _ = self.getSelectedHandle()
635
- if tHandle is not None:
636
- self.novelView.selectedItemChanged.emit(tHandle)
637
- return
638
-
639
- @pyqtSlot("QTreeWidgetItem*", int)
640
- def _treeDoubleClick(self, item: QTreeWidgetItem, column: int) -> None:
641
- """Extract the handle and line number of the title double-
642
- clicked, and send it to the main gui class for opening in the
643
- document editor.
644
- """
645
- tHandle, sTitle = self.getSelectedHandle()
646
- self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True)
530
+ @pyqtSlot(QModelIndex)
531
+ def _onSingleClick(self, index: QModelIndex) -> None:
532
+ """The user single-clicked an index."""
533
+ if index.isValid() and (model := self._getModel()):
534
+ if (tHandle := model.handle(index)) and (sTitle := model.key(index)):
535
+ self.novelView.selectedItemChanged.emit(tHandle)
536
+ if index.column() == model.columnCount(index) - 1:
537
+ pos = self.mapToGlobal(self.visualRect(index).topRight())
538
+ self._popMetaBox(pos, tHandle, sTitle)
539
+ return
540
+
541
+ @pyqtSlot(QModelIndex)
542
+ def _onDoubleClick(self, index: QModelIndex) -> None:
543
+ """The user double-clicked an index."""
544
+ if (
545
+ (model := self._getModel())
546
+ and (tHandle := model.handle(index))
547
+ and (sTitle := model.key(index))
548
+ ):
549
+ self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle, False)
550
+ return
551
+
552
+ @pyqtSlot(QModelIndex)
553
+ def _onMiddleClick(self, index: QModelIndex) -> None:
554
+ """The user middle-clicked an index."""
555
+ if (
556
+ (model := self._getModel())
557
+ and (tHandle := model.handle(index))
558
+ and (sTitle := model.key(index))
559
+ ):
560
+ self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle, False)
647
561
  return
648
562
 
649
563
  ##
650
564
  # Internal Functions
651
565
  ##
652
566
 
653
- def _populateTree(self, rootHandle: str | None) -> None:
654
- """Build the tree based on the project index."""
655
- self.clearContent()
656
- tStart = time()
657
- logger.debug("Building novel tree for root item '%s'", rootHandle)
658
-
659
- novStruct = SHARED.project.index.novelStructure(rootHandle=rootHandle, activeOnly=True)
660
- for tKey, tHandle, sTitle, novIdx in novStruct:
661
- if novIdx.level == "H0":
662
- continue
663
-
664
- newItem = QTreeWidgetItem()
665
- newItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
666
- newItem.setData(self.C_DATA, self.D_TITLE, sTitle)
667
- newItem.setData(self.C_DATA, self.D_KEY, tKey)
668
- newItem.setTextAlignment(self.C_WORDS, QtAlignRight)
669
-
670
- self._updateTreeItemValues(newItem, novIdx, tHandle, sTitle)
671
- self._treeMap[tKey] = newItem
672
- self.addTopLevelItem(newItem)
673
-
674
- self.setActiveHandle(self._actHandle)
675
-
676
- logger.debug("Novel Tree built in %.3f ms", (time() - tStart)*1000)
677
- self._lastBuild = time()
678
-
679
- return
680
-
681
- def _updateTreeItemValues(self, trItem: QTreeWidgetItem, idxItem: IndexHeading,
682
- tHandle: str, sTitle: str) -> None:
683
- """Set the tree item values from the index entry."""
684
- iLevel = nwStyles.H_LEVEL.get(idxItem.level, 0)
685
- hDec = SHARED.theme.getHeaderDecoration(iLevel)
686
-
687
- trItem.setData(self.C_TITLE, QtDecoration, hDec)
688
- trItem.setText(self.C_TITLE, idxItem.title)
689
- trItem.setFont(self.C_TITLE, self._hFonts[iLevel])
690
- trItem.setText(self.C_WORDS, f"{idxItem.wordCount:n}")
691
- trItem.setData(self.C_MORE, QtDecoration, self._pMore)
692
-
693
- # Custom column
694
- mW = int(self._lastColSize * self.viewport().width())
695
- lastText, toolTip = self._getLastColumnText(tHandle, sTitle)
696
- elideText = self.fontMetrics().elidedText(lastText, Qt.TextElideMode.ElideRight, mW)
697
- trItem.setText(self.C_EXTRA, elideText)
698
- trItem.setData(self.C_DATA, self.D_EXTRA, lastText)
699
- trItem.setToolTip(self.C_EXTRA, toolTip)
700
-
701
- return
702
-
703
- def _getLastColumnText(self, tHandle: str, sTitle: str) -> tuple[str, str]:
704
- """Generate text for the last column based on user settings."""
705
- if self._lastCol == NovelTreeColumn.HIDDEN:
706
- return "", ""
707
-
708
- refData = []
709
- refName = ""
710
- refs = SHARED.project.index.getReferences(tHandle, sTitle)
711
- if self._lastCol == NovelTreeColumn.POV:
712
- refData = refs[nwKeyWords.POV_KEY]
713
- refName = self._povLabel
714
-
715
- elif self._lastCol == NovelTreeColumn.FOCUS:
716
- refData = refs[nwKeyWords.FOCUS_KEY]
717
- refName = self._focLabel
718
-
719
- elif self._lastCol == NovelTreeColumn.PLOT:
720
- refData = refs[nwKeyWords.PLOT_KEY]
721
- refName = self._pltLabel
722
-
723
- if refData:
724
- toolText = ", ".join(refData)
725
- return toolText, f"{refName}: {toolText}"
726
-
727
- return "", ""
567
+ def _getModel(self) -> NovelModel | None:
568
+ """Return the model, if it exists."""
569
+ if isinstance(model := self.model(), NovelModel):
570
+ return model
571
+ return None
728
572
 
729
573
  def _popMetaBox(self, qPos: QPoint, tHandle: str, sTitle: str) -> None:
730
574
  """Show the novel meta data box."""
731
- logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
732
-
733
- pIndex = SHARED.project.index
734
- novIdx = pIndex.getItemHeading(tHandle, sTitle)
735
- refTags = pIndex.getReferences(tHandle, sTitle)
736
- if not novIdx:
575
+ def appendTags(refs: dict, key: str, lines: list[str]) -> None:
576
+ """Generate a reference list for a given reference key."""
577
+ if tags := ", ".join(refs.get(key, [])):
578
+ lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}:</b> {tags}")
737
579
  return
738
580
 
739
- synopText = novIdx.synopsis
740
- if synopText:
741
- synopLabel = trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP])
742
- synopText = f"<p><b>{synopLabel}</b>: {synopText}</p>"
743
-
744
- refLines = []
745
- refLines = self._appendMetaTag(refTags, nwKeyWords.POV_KEY, refLines)
746
- refLines = self._appendMetaTag(refTags, nwKeyWords.FOCUS_KEY, refLines)
747
- refLines = self._appendMetaTag(refTags, nwKeyWords.CHAR_KEY, refLines)
748
- refLines = self._appendMetaTag(refTags, nwKeyWords.PLOT_KEY, refLines)
749
- refLines = self._appendMetaTag(refTags, nwKeyWords.TIME_KEY, refLines)
750
- refLines = self._appendMetaTag(refTags, nwKeyWords.WORLD_KEY, refLines)
751
- refLines = self._appendMetaTag(refTags, nwKeyWords.OBJECT_KEY, refLines)
752
- refLines = self._appendMetaTag(refTags, nwKeyWords.ENTITY_KEY, refLines)
753
- refLines = self._appendMetaTag(refTags, nwKeyWords.CUSTOM_KEY, refLines)
754
-
755
- refText = ""
756
- if refLines:
757
- refList = "<br>".join(refLines)
758
- refText = f"<p>{refList}</p>"
759
-
760
- ttText = refText + synopText or self.tr("No meta data")
761
- if ttText:
762
- QToolTip.showText(qPos, ttText)
763
-
764
- return
765
-
766
- @staticmethod
767
- def _appendMetaTag(refs: dict, key: str, lines: list[str]) -> list[str]:
768
- """Generate a reference list for a given reference key."""
769
- tags = ", ".join(refs.get(key, []))
770
- if tags:
771
- lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}</b>: {tags}")
772
- return lines
581
+ if head := SHARED.project.index.getItemHeading(tHandle, sTitle):
582
+ logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
583
+ if synopsis := head.synopsis:
584
+ label = trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP])
585
+ synopsis = f"<p><b>{label}:</b> {synopsis}</p>"
586
+
587
+ lines = []
588
+ if head := SHARED.project.index.getItemHeading(tHandle, sTitle):
589
+ tags = head.getReferences()
590
+ appendTags(tags, nwKeyWords.POV_KEY, lines)
591
+ appendTags(tags, nwKeyWords.FOCUS_KEY, lines)
592
+ appendTags(tags, nwKeyWords.CHAR_KEY, lines)
593
+ appendTags(tags, nwKeyWords.PLOT_KEY, lines)
594
+ appendTags(tags, nwKeyWords.TIME_KEY, lines)
595
+ appendTags(tags, nwKeyWords.WORLD_KEY, lines)
596
+ appendTags(tags, nwKeyWords.OBJECT_KEY, lines)
597
+ appendTags(tags, nwKeyWords.ENTITY_KEY, lines)
598
+ appendTags(tags, nwKeyWords.CUSTOM_KEY, lines)
599
+
600
+ text = ""
601
+ if lines:
602
+ refs = "<br>".join(lines)
603
+ text = f"<p>{refs}</p>"
604
+ if tooltip := (text + synopsis or self.tr("No meta data")):
605
+ QToolTip.showText(qPos, tooltip)
606
+ return