novelWriter 2.4.3__py3-none-any.whl → 2.5__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 (123) hide show
  1. {novelWriter-2.4.3.dist-info → novelWriter-2.5.dist-info}/METADATA +4 -5
  2. {novelWriter-2.4.3.dist-info → novelWriter-2.5.dist-info}/RECORD +122 -112
  3. {novelWriter-2.4.3.dist-info → novelWriter-2.5.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +33 -39
  5. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  6. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  7. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  8. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  9. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  10. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  11. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  12. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  13. novelwriter/assets/i18n/nw_pl_PL.qm +0 -0
  14. novelwriter/assets/i18n/nw_pt_BR.qm +0 -0
  15. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  16. novelwriter/assets/i18n/project_en_GB.json +1 -0
  17. novelwriter/assets/i18n/project_pl_PL.json +116 -0
  18. novelwriter/assets/i18n/project_pt_BR.json +74 -74
  19. novelwriter/assets/icons/typicons_dark/icons.conf +2 -0
  20. novelwriter/assets/icons/typicons_dark/nw_font.svg +4 -0
  21. novelwriter/assets/icons/typicons_dark/nw_quote.svg +4 -0
  22. novelwriter/assets/icons/typicons_light/icons.conf +2 -0
  23. novelwriter/assets/icons/typicons_light/nw_font.svg +4 -0
  24. novelwriter/assets/icons/typicons_light/nw_quote.svg +4 -0
  25. novelwriter/assets/manual.pdf +0 -0
  26. novelwriter/assets/sample.zip +0 -0
  27. novelwriter/assets/syntax/cyberpunk_night.conf +5 -3
  28. novelwriter/assets/syntax/default_dark.conf +32 -18
  29. novelwriter/assets/syntax/default_light.conf +24 -10
  30. novelwriter/assets/syntax/dracula.conf +44 -0
  31. novelwriter/assets/syntax/grey_dark.conf +5 -4
  32. novelwriter/assets/syntax/grey_light.conf +5 -4
  33. novelwriter/assets/syntax/light_owl.conf +7 -6
  34. novelwriter/assets/syntax/night_owl.conf +7 -6
  35. novelwriter/assets/syntax/snazzy.conf +42 -0
  36. novelwriter/assets/syntax/solarized_dark.conf +4 -3
  37. novelwriter/assets/syntax/solarized_light.conf +4 -3
  38. novelwriter/assets/syntax/tango.conf +27 -11
  39. novelwriter/assets/syntax/tomorrow.conf +6 -5
  40. novelwriter/assets/syntax/tomorrow_night.conf +7 -6
  41. novelwriter/assets/syntax/tomorrow_night_blue.conf +6 -5
  42. novelwriter/assets/syntax/tomorrow_night_bright.conf +6 -5
  43. novelwriter/assets/syntax/tomorrow_night_eighties.conf +6 -5
  44. novelwriter/assets/text/credits_en.htm +52 -41
  45. novelwriter/assets/themes/cyberpunk_night.conf +3 -0
  46. novelwriter/assets/themes/default_dark.conf +2 -0
  47. novelwriter/assets/themes/default_light.conf +2 -0
  48. novelwriter/assets/themes/dracula.conf +48 -0
  49. novelwriter/assets/themes/solarized_dark.conf +2 -0
  50. novelwriter/assets/themes/solarized_light.conf +2 -0
  51. novelwriter/common.py +33 -12
  52. novelwriter/config.py +184 -98
  53. novelwriter/constants.py +47 -35
  54. novelwriter/core/buildsettings.py +68 -69
  55. novelwriter/core/coretools.py +5 -23
  56. novelwriter/core/docbuild.py +52 -40
  57. novelwriter/core/document.py +3 -5
  58. novelwriter/core/index.py +115 -45
  59. novelwriter/core/item.py +8 -19
  60. novelwriter/core/options.py +2 -4
  61. novelwriter/core/project.py +37 -61
  62. novelwriter/core/projectdata.py +1 -3
  63. novelwriter/core/projectxml.py +12 -15
  64. novelwriter/core/sessions.py +3 -5
  65. novelwriter/core/spellcheck.py +4 -9
  66. novelwriter/core/status.py +211 -164
  67. novelwriter/core/storage.py +0 -8
  68. novelwriter/core/tohtml.py +139 -105
  69. novelwriter/core/tokenizer.py +278 -122
  70. novelwriter/core/{tomd.py → tomarkdown.py} +97 -78
  71. novelwriter/core/toodt.py +257 -166
  72. novelwriter/core/toqdoc.py +419 -0
  73. novelwriter/core/tree.py +5 -7
  74. novelwriter/dialogs/about.py +11 -18
  75. novelwriter/dialogs/docmerge.py +17 -19
  76. novelwriter/dialogs/docsplit.py +17 -19
  77. novelwriter/dialogs/editlabel.py +6 -10
  78. novelwriter/dialogs/preferences.py +200 -164
  79. novelwriter/dialogs/projectsettings.py +225 -189
  80. novelwriter/dialogs/quotes.py +12 -9
  81. novelwriter/dialogs/wordlist.py +9 -15
  82. novelwriter/enum.py +35 -30
  83. novelwriter/error.py +8 -15
  84. novelwriter/extensions/configlayout.py +55 -21
  85. novelwriter/extensions/eventfilters.py +1 -5
  86. novelwriter/extensions/modified.py +70 -14
  87. novelwriter/extensions/novelselector.py +1 -3
  88. novelwriter/extensions/pagedsidebar.py +9 -12
  89. novelwriter/extensions/{circularprogress.py → progressbars.py} +30 -8
  90. novelwriter/extensions/statusled.py +40 -26
  91. novelwriter/extensions/switch.py +4 -6
  92. novelwriter/extensions/switchbox.py +7 -6
  93. novelwriter/extensions/versioninfo.py +3 -9
  94. novelwriter/gui/doceditor.py +120 -139
  95. novelwriter/gui/dochighlight.py +231 -186
  96. novelwriter/gui/docviewer.py +69 -108
  97. novelwriter/gui/docviewerpanel.py +3 -10
  98. novelwriter/gui/editordocument.py +1 -3
  99. novelwriter/gui/itemdetails.py +7 -11
  100. novelwriter/gui/mainmenu.py +22 -18
  101. novelwriter/gui/noveltree.py +11 -24
  102. novelwriter/gui/outline.py +15 -26
  103. novelwriter/gui/projtree.py +39 -65
  104. novelwriter/gui/search.py +10 -3
  105. novelwriter/gui/sidebar.py +2 -6
  106. novelwriter/gui/statusbar.py +29 -37
  107. novelwriter/gui/theme.py +26 -48
  108. novelwriter/guimain.py +162 -160
  109. novelwriter/shared.py +36 -19
  110. novelwriter/text/patterns.py +113 -0
  111. novelwriter/tools/dictionaries.py +10 -20
  112. novelwriter/tools/lipsum.py +10 -16
  113. novelwriter/tools/manusbuild.py +9 -11
  114. novelwriter/tools/manuscript.py +75 -149
  115. novelwriter/tools/manussettings.py +74 -76
  116. novelwriter/tools/noveldetails.py +16 -21
  117. novelwriter/tools/welcome.py +21 -26
  118. novelwriter/tools/writingstats.py +9 -12
  119. novelwriter/types.py +49 -4
  120. novelwriter/extensions/simpleprogress.py +0 -55
  121. {novelWriter-2.4.3.dist-info → novelWriter-2.5.dist-info}/LICENSE.md +0 -0
  122. {novelWriter-2.4.3.dist-info → novelWriter-2.5.dist-info}/entry_points.txt +0 -0
  123. {novelWriter-2.4.3.dist-info → novelWriter-2.5.dist-info}/top_level.txt +0 -0
novelwriter/core/index.py CHANGED
@@ -29,17 +29,20 @@ from __future__ import annotations
29
29
 
30
30
  import json
31
31
  import logging
32
+ import random
32
33
 
33
- from time import time
34
- from typing import TYPE_CHECKING
35
- from pathlib import Path
36
34
  from collections.abc import ItemsView, Iterable
35
+ from pathlib import Path
36
+ from time import time
37
+ from typing import TYPE_CHECKING, Literal
37
38
 
38
39
  from novelwriter import SHARED
39
- from novelwriter.enum import nwComment, nwItemClass, nwItemType, nwItemLayout
40
+ from novelwriter.common import (
41
+ checkInt, isHandle, isItemClass, isListInstance, isTitleTag, jsonEncode
42
+ )
43
+ from novelwriter.constants import nwFiles, nwHeaders, nwKeyWords
44
+ from novelwriter.enum import nwComment, nwItemClass, nwItemLayout, nwItemType
40
45
  from novelwriter.error import logException
41
- from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
42
- from novelwriter.constants import nwFiles, nwKeyWords, nwHeaders
43
46
  from novelwriter.text.counting import standardCounter
44
47
 
45
48
  if TYPE_CHECKING: # pragma: no cover
@@ -48,7 +51,12 @@ if TYPE_CHECKING: # pragma: no cover
48
51
 
49
52
  logger = logging.getLogger(__name__)
50
53
 
51
- TT_NONE = "T0000"
54
+ T_NoteTypes = Literal["footnotes", "comments"]
55
+
56
+ TT_NONE = "T0000" # Default title key
57
+ MAX_RETRY = 1000 # Key generator recursion limit
58
+ KEY_SOURCE = "0123456789bcdfghjklmnpqrstvwxz"
59
+ NOTE_TYPES: list[T_NoteTypes] = ["footnotes", "comments"]
52
60
 
53
61
 
54
62
  class NWIndex:
@@ -301,9 +309,9 @@ class NWIndex:
301
309
 
302
310
  def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, tags: dict[str, bool]) -> None:
303
311
  """Scan an active document for meta data."""
304
- nTitle = 0 # Line Number of the previous title
305
- cTitle = TT_NONE # Tag of the current title
306
- pTitle = TT_NONE # Tag of the previous title
312
+ nTitle = 0 # Line Number of the previous title
313
+ cTitle = TT_NONE # Tag of the current title
314
+ pTitle = TT_NONE # Tag of the previous title
307
315
  canSetHead = True # First heading has not yet been set
308
316
 
309
317
  lines = text.splitlines()
@@ -335,10 +343,11 @@ class NWIndex:
335
343
  self._indexKeyword(tHandle, line, cTitle, nwItem.itemClass, tags)
336
344
 
337
345
  elif line.startswith("%"):
338
- if cTitle != TT_NONE:
339
- cStyle, cText, _ = processComment(line)
340
- if cStyle in (nwComment.SYNOPSIS, nwComment.SHORT):
341
- self._itemIndex.setHeadingSynopsis(tHandle, cTitle, cText)
346
+ cStyle, cKey, cText, _, _ = processComment(line)
347
+ if cStyle in (nwComment.SYNOPSIS, nwComment.SHORT):
348
+ self._itemIndex.setHeadingSynopsis(tHandle, cTitle, cText)
349
+ elif cStyle == nwComment.FOOTNOTE:
350
+ self._itemIndex.addNoteKey(tHandle, "footnotes", cKey)
342
351
 
343
352
  # Count words for remaining text after last heading
344
353
  if pTitle != TT_NONE:
@@ -506,6 +515,14 @@ class NWIndex:
506
515
  name, _, display = text.partition("|")
507
516
  return name.rstrip(), display.lstrip()
508
517
 
518
+ def newCommentKey(self, tHandle: str, style: nwComment) -> str:
519
+ """Generate a new key for a comment style."""
520
+ if style == nwComment.FOOTNOTE:
521
+ return self._itemIndex.genNewNoteKey(tHandle, "footnotes")
522
+ elif style == nwComment.COMMENT:
523
+ return self._itemIndex.genNewNoteKey(tHandle, "comments")
524
+ return "err"
525
+
509
526
  ##
510
527
  # Extract Data
511
528
  ##
@@ -690,12 +707,9 @@ class NWIndex:
690
707
  return tName, tClass, iItem, None if iItem is None else iItem[tHeading]
691
708
  return "", "", None, None
692
709
 
693
- # END Class NWIndex
694
-
695
710
 
696
- # =============================================================================================== #
697
- # The Tags Index Object
698
- # =============================================================================================== #
711
+ # The Tags Index Object
712
+ # =====================
699
713
 
700
714
  class TagsIndex:
701
715
  """Core: Tags Index Wrapper Class
@@ -790,7 +804,7 @@ class TagsIndex:
790
804
 
791
805
  for key, entry in data.items():
792
806
  if not isinstance(key, str):
793
- raise ValueError("tagsIndex keys must be a string")
807
+ raise ValueError("tagsIndex key must be a string")
794
808
  if not isinstance(entry, dict):
795
809
  raise ValueError("tagsIndex entry is not a dict")
796
810
 
@@ -815,12 +829,9 @@ class TagsIndex:
815
829
 
816
830
  return
817
831
 
818
- # END Class TagsIndex
819
-
820
832
 
821
- # =============================================================================================== #
822
- # The Item Index Objects
823
- # =============================================================================================== #
833
+ # The Item Index Objects
834
+ # ======================
824
835
 
825
836
  class ItemIndex:
826
837
  """Core: Item Index Wrapper Class
@@ -950,6 +961,25 @@ class ItemIndex:
950
961
  self._items[tHandle].addHeadingRef(sTitle, tagKeys, refType)
951
962
  return
952
963
 
964
+ def addNoteKey(self, tHandle: str, style: T_NoteTypes, key: str) -> None:
965
+ """Set notes key for a given item."""
966
+ if tHandle in self._items:
967
+ self._items[tHandle].addNoteKey(style, key)
968
+ return
969
+
970
+ def genNewNoteKey(self, tHandle: str, style: T_NoteTypes) -> str:
971
+ """Set notes key for a given item."""
972
+ if style in NOTE_TYPES and (item := self._items.get(tHandle)):
973
+ keys = set()
974
+ for entry in self._items.values():
975
+ keys.update(entry.noteKeys(style))
976
+ for _ in range(MAX_RETRY):
977
+ key = style[:1] + "".join(random.choices(KEY_SOURCE, k=4))
978
+ if key not in keys:
979
+ item.addNoteKey(style, key)
980
+ return key
981
+ return "err"
982
+
953
983
  ##
954
984
  # Pack/Unpack
955
985
  ##
@@ -978,8 +1008,6 @@ class ItemIndex:
978
1008
 
979
1009
  return
980
1010
 
981
- # END Class ItemIndex
982
-
983
1011
 
984
1012
  class IndexItem:
985
1013
  """Core: Single Index Item Class
@@ -991,12 +1019,13 @@ class IndexItem:
991
1019
  must be reset each time the item is re-indexed.
992
1020
  """
993
1021
 
994
- __slots__ = ("_handle", "_item", "_headings", "_count")
1022
+ __slots__ = ("_handle", "_item", "_headings", "_count", "_notes")
995
1023
 
996
1024
  def __init__(self, tHandle: str, nwItem: NWItem) -> None:
997
1025
  self._handle = tHandle
998
1026
  self._item = nwItem
999
1027
  self._headings: dict[str, IndexHeading] = {TT_NONE: IndexHeading(TT_NONE)}
1028
+ self._notes: dict[str, set[str]] = {}
1000
1029
  self._count = 0
1001
1030
  return
1002
1031
 
@@ -1064,6 +1093,13 @@ class IndexItem:
1064
1093
  self._headings[sTitle].addReference(tagKey, refType)
1065
1094
  return
1066
1095
 
1096
+ def addNoteKey(self, style: T_NoteTypes, key: str) -> None:
1097
+ """Add a note key to the index."""
1098
+ if style not in self._notes:
1099
+ self._notes[style] = set()
1100
+ self._notes[style].add(key)
1101
+ return
1102
+
1067
1103
  ##
1068
1104
  # Data Methods
1069
1105
  ##
@@ -1085,6 +1121,10 @@ class IndexItem:
1085
1121
  self._count += 1
1086
1122
  return f"T{self._count:04d}"
1087
1123
 
1124
+ def noteKeys(self, style: T_NoteTypes) -> set[str]:
1125
+ """Return a set of all note keys."""
1126
+ return self._notes.get(style, set())
1127
+
1088
1128
  ##
1089
1129
  # Pack/Unpack
1090
1130
  ##
@@ -1103,6 +1143,8 @@ class IndexItem:
1103
1143
  data["headings"] = heads
1104
1144
  if refs:
1105
1145
  data["references"] = refs
1146
+ if self._notes:
1147
+ data["notes"] = {style: list(keys) for style, keys in self._notes.items()}
1106
1148
 
1107
1149
  return data
1108
1150
 
@@ -1116,9 +1158,15 @@ class IndexItem:
1116
1158
  tHeading.unpackData(hData)
1117
1159
  tHeading.unpackReferences(references.get(sTitle, {}))
1118
1160
  self.addHeading(tHeading)
1119
- return
1120
1161
 
1121
- # END Class IndexItem
1162
+ for style, keys in data.get("notes", {}).items():
1163
+ if style not in NOTE_TYPES:
1164
+ raise ValueError("The notes style is invalid")
1165
+ if not isListInstance(keys, str):
1166
+ raise ValueError("The notes keys must be a list of strings")
1167
+ self._notes[style] = set(keys)
1168
+
1169
+ return
1122
1170
 
1123
1171
 
1124
1172
  class IndexHeading:
@@ -1295,25 +1343,47 @@ class IndexHeading:
1295
1343
  raise ValueError("The itemIndex contains an invalid reference type")
1296
1344
  return
1297
1345
 
1298
- # END Class IndexHeading
1299
-
1300
1346
 
1301
- # =============================================================================================== #
1302
- # Text Processing Functions
1303
- # =============================================================================================== #
1347
+ # Text Processing Functions
1348
+ # =========================
1304
1349
 
1305
- CLASSIFIERS = {
1306
- "short": nwComment.SHORT,
1350
+ MODIFIERS = {
1307
1351
  "synopsis": nwComment.SYNOPSIS,
1352
+ "short": nwComment.SHORT,
1353
+ "note": nwComment.NOTE,
1354
+ "footnote": nwComment.FOOTNOTE,
1308
1355
  }
1356
+ KEY_REQ = {
1357
+ "synopsis": 0, # Key not allowed
1358
+ "short": 0, # Key not allowed
1359
+ "note": 1, # Key optional
1360
+ "footnote": 2, # Key required
1361
+ }
1362
+
1363
+
1364
+ def _checkModKey(modifier: str, key: str) -> bool:
1365
+ """Check if a modifier and key set are ok."""
1366
+ if modifier in MODIFIERS:
1367
+ if key == "":
1368
+ return KEY_REQ[modifier] < 2
1369
+ elif key.replace("_", "").isalnum():
1370
+ return KEY_REQ[modifier] > 0
1371
+ return False
1309
1372
 
1310
1373
 
1311
- def processComment(text: str) -> tuple[nwComment, str, int]:
1312
- """Extract comment style and text. Should only be called on text
1313
- starting with a %.
1374
+ def processComment(text: str) -> tuple[nwComment, str, str, int, int]:
1375
+ """Extract comment style, key and text. Should only be called on
1376
+ text starting with a %.
1314
1377
  """
1315
- check = text[1:].lstrip()
1316
- classifier, _, content = check.partition(":")
1317
- if content and (clean := classifier.strip().lower()) in CLASSIFIERS:
1318
- return CLASSIFIERS[clean], content.strip(), text.find(":") + 1
1319
- return nwComment.PLAIN, check, 0
1378
+ if text[:2] == "%~":
1379
+ return nwComment.IGNORE, "", text[2:].lstrip(), 0, 0
1380
+
1381
+ check = text[1:].strip()
1382
+ start, _, content = check.partition(":")
1383
+ modifier, _, key = start.rstrip().partition(".")
1384
+ if content and (clean := modifier.lower()) and _checkModKey(clean, key):
1385
+ col = text.find(":") + 1
1386
+ dot = text.find(".", 0, col) + 1
1387
+ return MODIFIERS[clean], key, content.lstrip(), dot, col
1388
+
1389
+ return nwComment.PLAIN, "", check, 0, 0
novelwriter/core/item.py CHANGED
@@ -25,15 +25,16 @@ from __future__ import annotations
25
25
 
26
26
  import logging
27
27
 
28
- from typing import TYPE_CHECKING, Any, Literal, overload
28
+ from typing import TYPE_CHECKING, Any
29
29
 
30
30
  from PyQt5.QtGui import QIcon
31
31
 
32
- from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
33
32
  from novelwriter.common import (
34
- checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified, yesNo
33
+ checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified,
34
+ yesNo
35
35
  )
36
36
  from novelwriter.constants import nwHeaders, nwLabels, trConst
37
+ from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
37
38
 
38
39
  if TYPE_CHECKING: # pragma: no cover
39
40
  from novelwriter.core.project import NWProject
@@ -308,25 +309,15 @@ class NWItem:
308
309
 
309
310
  return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
310
311
 
311
- @overload # pragma: no cover
312
- def getImportStatus(self, incIcon: Literal[True] = True) -> tuple[str, QIcon]:
313
- pass
314
-
315
- @overload # pragma: no cover
316
- def getImportStatus(self, incIcon: Literal[False]) -> tuple[str, None]:
317
- pass
318
-
319
- def getImportStatus(self, incIcon=True):
312
+ def getImportStatus(self) -> tuple[str, QIcon]:
320
313
  """Return the relevant importance or status label and icon for
321
314
  the current item based on its class.
322
315
  """
323
316
  if self.isNovelLike():
324
- stName = self._project.data.itemStatus.name(self._status)
325
- stIcon = self._project.data.itemStatus.icon(self._status) if incIcon else None
317
+ entry = self._project.data.itemStatus[self._status]
326
318
  else:
327
- stName = self._project.data.itemImport.name(self._import)
328
- stIcon = self._project.data.itemImport.icon(self._import) if incIcon else None
329
- return stName, stIcon
319
+ entry = self._project.data.itemImport[self._import]
320
+ return entry.name, entry.icon
330
321
 
331
322
  ##
332
323
  # Checker Methods
@@ -567,5 +558,3 @@ class NWItem:
567
558
  """Save the initial word count."""
568
559
  self._initCount = self._wordCount
569
560
  return
570
-
571
- # END Class NWItem
@@ -28,12 +28,12 @@ import json
28
28
  import logging
29
29
 
30
30
  from enum import Enum
31
- from typing import TYPE_CHECKING, Any, TypeVar
32
31
  from pathlib import Path
32
+ from typing import TYPE_CHECKING, Any, TypeVar
33
33
 
34
- from novelwriter.error import logException
35
34
  from novelwriter.common import checkBool, checkFloat, checkInt, checkString, jsonEncode
36
35
  from novelwriter.constants import nwFiles
36
+ from novelwriter.error import logException
37
37
 
38
38
  if TYPE_CHECKING: # pragma: no cover
39
39
  from novelwriter.core.project import NWProject
@@ -218,5 +218,3 @@ class OptionState:
218
218
  if value in lookup.__members__:
219
219
  return lookup[value]
220
220
  return default
221
-
222
- # END Class OptionState
@@ -26,33 +26,33 @@ from __future__ import annotations
26
26
  import json
27
27
  import logging
28
28
 
29
+ from collections.abc import Iterable
29
30
  from enum import Enum
31
+ from functools import partial
32
+ from pathlib import Path
30
33
  from time import time
31
34
  from typing import TYPE_CHECKING
32
- from pathlib import Path
33
- from functools import partial
34
- from collections.abc import Iterable
35
35
 
36
36
  from PyQt5.QtCore import QCoreApplication
37
37
 
38
- from novelwriter import CONFIG, SHARED, __version__, __hexversion__
39
- from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
40
- from novelwriter.error import logException
41
- from novelwriter.constants import trConst, nwLabels
42
- from novelwriter.core.tree import NWTree
38
+ from novelwriter import CONFIG, SHARED, __hexversion__, __version__
39
+ from novelwriter.common import (
40
+ checkStringNone, formatInt, formatTimeStamp, getFileSize, hexToInt,
41
+ makeFileNameSafe, minmax
42
+ )
43
+ from novelwriter.constants import nwLabels, trConst
43
44
  from novelwriter.core.index import NWIndex
44
45
  from novelwriter.core.options import OptionState
45
- from novelwriter.core.storage import NWStorage, NWStorageOpen
46
- from novelwriter.core.sessions import NWSessionLog
47
- from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
48
46
  from novelwriter.core.projectdata import NWProjectData
49
- from novelwriter.common import (
50
- checkStringNone, formatInt, formatTimeStamp, getFileSize, hexToInt, makeFileNameSafe, minmax
51
- )
47
+ from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
48
+ from novelwriter.core.sessions import NWSessionLog
49
+ from novelwriter.core.storage import NWStorage, NWStorageOpen
50
+ from novelwriter.core.tree import NWTree
51
+ from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
52
+ from novelwriter.error import logException
52
53
 
53
54
  if TYPE_CHECKING: # pragma: no cover
54
55
  from novelwriter.core.item import NWItem
55
- from novelwriter.core.status import NWStatus
56
56
 
57
57
  logger = logging.getLogger(__name__)
58
58
 
@@ -64,8 +64,6 @@ class NWProjectState(Enum):
64
64
  RECOVERY = 2
65
65
  READY = 3
66
66
 
67
- # END Enum NWProjectState
68
-
69
67
 
70
68
  class NWProject:
71
69
 
@@ -256,13 +254,23 @@ class NWProject:
256
254
  status = self._storage.initProjectStorage(projPath, clearLock)
257
255
  if status != NWStorageOpen.READY:
258
256
  if status == NWStorageOpen.UNKOWN:
259
- SHARED.error(self.tr("Not a known project file format."))
257
+ SHARED.error(
258
+ self.tr("Not a known project file format."),
259
+ info=self.tr("Path: {0}").format(str(projPath))
260
+ )
260
261
  elif status == NWStorageOpen.NOT_FOUND:
261
- SHARED.error(self.tr("Project file not found."))
262
+ SHARED.error(
263
+ self.tr("Project file not found."),
264
+ info=self.tr("Path: {0}").format(str(projPath))
265
+ )
266
+ elif status == NWStorageOpen.FAILED:
267
+ SHARED.error(
268
+ self.tr("Failed to open project."),
269
+ info=self.tr("Path: {0}").format(str(projPath)),
270
+ exc=self._storage.exc
271
+ )
262
272
  elif status == NWStorageOpen.LOCKED:
263
273
  self._state = NWProjectState.LOCKED
264
- elif status == NWStorageOpen.FAILED:
265
- SHARED.error(self.tr("Failed to open project."), exc=self._storage.exc)
266
274
  return False
267
275
 
268
276
  # Read Project XML
@@ -461,14 +469,14 @@ class NWProject:
461
469
 
462
470
  def setDefaultStatusImport(self) -> None:
463
471
  """Set the default status and importance values."""
464
- self._data.itemStatus.write(None, self.tr("New"), (100, 100, 100))
465
- self._data.itemStatus.write(None, self.tr("Note"), (200, 50, 0))
466
- self._data.itemStatus.write(None, self.tr("Draft"), (200, 150, 0))
467
- self._data.itemStatus.write(None, self.tr("Finished"), (50, 200, 0))
468
- self._data.itemImport.write(None, self.tr("New"), (100, 100, 100))
469
- self._data.itemImport.write(None, self.tr("Minor"), (200, 50, 0))
470
- self._data.itemImport.write(None, self.tr("Major"), (200, 150, 0))
471
- self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0))
472
+ self._data.itemStatus.add(None, self.tr("New"), (100, 100, 100), "SQUARE", 0)
473
+ self._data.itemStatus.add(None, self.tr("Note"), (200, 50, 0), "SQUARE", 0)
474
+ self._data.itemStatus.add(None, self.tr("Draft"), (200, 150, 0), "SQUARE", 0)
475
+ self._data.itemStatus.add(None, self.tr("Finished"), (50, 200, 0), "SQUARE", 0)
476
+ self._data.itemImport.add(None, self.tr("New"), (100, 100, 100), "SQUARE", 0)
477
+ self._data.itemImport.add(None, self.tr("Minor"), (200, 50, 0), "SQUARE", 0)
478
+ self._data.itemImport.add(None, self.tr("Major"), (200, 150, 0), "SQUARE", 0)
479
+ self._data.itemImport.add(None, self.tr("Main"), (50, 200, 0), "SQUARE", 0)
472
480
  return
473
481
 
474
482
  def setProjectLang(self, language: str | None) -> None:
@@ -491,14 +499,6 @@ class NWProject:
491
499
  self.setProjectChanged(True)
492
500
  return
493
501
 
494
- def setStatusColours(self, new: list[dict], deleted: list[str]) -> bool:
495
- """Update the list of novel file status flags."""
496
- return self._setStatusImport(new, deleted, self._data.itemStatus)
497
-
498
- def setImportColours(self, new: list[dict], deleted: list[str]) -> bool:
499
- """Update the list of note file importance flags."""
500
- return self._setStatusImport(new, deleted, self._data.itemImport)
501
-
502
502
  def setProjectChanged(self, status: bool) -> bool:
503
503
  """Toggle the project changed flag, and propagate the
504
504
  information to the GUI statusbar.
@@ -584,28 +584,6 @@ class NWProject:
584
584
  # Internal Functions
585
585
  ##
586
586
 
587
- def _setStatusImport(self, new: list[dict], delete: list[str], target: NWStatus) -> bool:
588
- """Update the list of novel file status or importance flags, and
589
- delete those that have been requested deleted.
590
- """
591
- if not (new or delete):
592
- return False
593
-
594
- order = []
595
- for entry in new:
596
- key = entry.get("key", None)
597
- name = entry.get("name", "")
598
- cols = entry.get("cols", (100, 100, 100))
599
- if name:
600
- order.append(target.write(key, name, cols))
601
-
602
- for key in delete:
603
- target.remove(key)
604
-
605
- target.reorder(order)
606
-
607
- return True
608
-
609
587
  def _loadProjectLocalisation(self) -> bool:
610
588
  """Load the language data for the current project language."""
611
589
  if self._data.language is None or CONFIG._nwLangPath is None:
@@ -626,5 +604,3 @@ class NWProject:
626
604
  return False
627
605
 
628
606
  return True
629
-
630
- # END Class NWProject
@@ -23,8 +23,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
23
23
  """
24
24
  from __future__ import annotations
25
25
 
26
- import uuid
27
26
  import logging
27
+ import uuid
28
28
 
29
29
  from typing import TYPE_CHECKING, Any
30
30
 
@@ -322,5 +322,3 @@ class NWProjectData:
322
322
  self._autoReplace[key] = simplified(entry)
323
323
  self._project.setProjectChanged(True)
324
324
  return
325
-
326
- # END Class NWProjectData
@@ -29,24 +29,24 @@ import logging
29
29
  import xml.etree.ElementTree as ET
30
30
 
31
31
  from enum import Enum
32
+ from pathlib import Path
32
33
  from time import time
33
34
  from typing import TYPE_CHECKING
34
- from pathlib import Path
35
35
 
36
- from novelwriter import __version__, __hexversion__
36
+ from novelwriter import __hexversion__, __version__
37
37
  from novelwriter.common import (
38
38
  checkBool, checkInt, checkString, checkStringNone, formatTimeStamp,
39
39
  hexToInt, simplified, xmlIndent, yesNo
40
40
  )
41
41
 
42
42
  if TYPE_CHECKING: # pragma: no cover
43
- from novelwriter.core.status import NWStatus
44
43
  from novelwriter.core.projectdata import NWProjectData
44
+ from novelwriter.core.status import NWStatus
45
45
 
46
46
  logger = logging.getLogger(__name__)
47
47
 
48
48
  FILE_VERSION = "1.5" # The current project file format version
49
- FILE_REVISION = "3" # The current project file format revision
49
+ FILE_REVISION = "4" # The current project file format revision
50
50
  HEX_VERSION = 0x0105
51
51
 
52
52
  NUM_VERSION = {
@@ -70,8 +70,6 @@ class XMLReadState(Enum):
70
70
  PARSED_OK = 5
71
71
  WAS_LEGACY = 6
72
72
 
73
- # END Class XMLReadState
74
-
75
73
 
76
74
  class ProjectXMLReader:
77
75
  """Core: Project XML Reader
@@ -109,6 +107,8 @@ class ProjectXMLReader:
109
107
  Rev 2: Drops the title node from project and adds the TEMPLATE
110
108
  class for items. 2.3 Beta 1.
111
109
  Rev 3: Added TEMPLATE class. 2.3.
110
+ Rev 4: Added shape attribute to status and importance entry
111
+ nodes. 2.5.
112
112
  """
113
113
 
114
114
  def __init__(self, path: str | Path) -> None:
@@ -356,8 +356,8 @@ class ProjectXMLReader:
356
356
  logger.debug("Parsing <content> section (legacy format)")
357
357
 
358
358
  # Create maps to look up name -> key for status and importance
359
- statusMap = {entry.get("name"): key for key, entry in data.itemStatus.items()}
360
- importMap = {entry.get("name"): key for key, entry in data.itemImport.items()}
359
+ sMap: dict[str | None, str] = {e.name: k for k, e in data.itemStatus.iterItems()}
360
+ iMap: dict[str | None, str] = {e.name: k for k, e in data.itemImport.iterItems()}
361
361
 
362
362
  for xItem in xSection:
363
363
  if xItem.tag != "item":
@@ -404,9 +404,9 @@ class ProjectXMLReader:
404
404
 
405
405
  # Status was split into separate status/import with a key in 1.4
406
406
  if item.get("class", "") in ("NOVEL", "ARCHIVE"):
407
- name["status"] = statusMap.get(tmpStatus, None)
407
+ name["status"] = sMap.get(tmpStatus, None)
408
408
  else:
409
- name["import"] = importMap.get(tmpStatus, None)
409
+ name["import"] = iMap.get(tmpStatus, None)
410
410
 
411
411
  # A number of layouts were removed in 1.3
412
412
  if item.get("layout", "") in (
@@ -436,7 +436,8 @@ class ProjectXMLReader:
436
436
  green = checkInt(xEntry.attrib.get("green", 0), 0)
437
437
  blue = checkInt(xEntry.attrib.get("blue", 0), 0)
438
438
  count = checkInt(xEntry.attrib.get("count", 0), 0)
439
- sObject.write(key, xEntry.text or "", (red, green, blue), count)
439
+ shape = xEntry.attrib.get("shape", "")
440
+ sObject.add(key, xEntry.text or "", (red, green, blue), shape, count)
440
441
  return
441
442
 
442
443
  def _parseDictKeyText(self, xItem: ET.Element) -> dict:
@@ -455,8 +456,6 @@ class ProjectXMLReader:
455
456
  """
456
457
  return {xNode.tag: checkString(xNode.text, "") for xNode in xItem}
457
458
 
458
- # END Class ProjectXMLReader
459
-
460
459
 
461
460
  class ProjectXMLWriter:
462
461
  """Core: Project XML Writer
@@ -576,5 +575,3 @@ class ProjectXMLWriter:
576
575
  xEntry = ET.SubElement(xItem, "entry", attrib={"key": key})
577
576
  xEntry.text = str(value) or ""
578
577
  return
579
-
580
- # END Class ProjectXMLWriter
@@ -26,14 +26,14 @@ from __future__ import annotations
26
26
  import json
27
27
  import logging
28
28
 
29
+ from collections.abc import Iterable
30
+ from pathlib import Path
29
31
  from time import time
30
32
  from typing import TYPE_CHECKING
31
- from pathlib import Path
32
- from collections.abc import Iterable
33
33
 
34
- from novelwriter.error import logException
35
34
  from novelwriter.common import formatTimeStamp
36
35
  from novelwriter.constants import nwFiles
36
+ from novelwriter.error import logException
37
37
 
38
38
  if TYPE_CHECKING: # pragma: no cover
39
39
  from novelwriter.core.project import NWProject
@@ -135,5 +135,3 @@ class NWSessionLog:
135
135
  "novel": novel, "notes": notes, "idle": idle,
136
136
  })
137
137
  return f"{data}\n"
138
-
139
- # END Class NWSessionLog