novelWriter 2.2.1__py3-none-any.whl → 2.3b1__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 (110) hide show
  1. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/RECORD +102 -92
  3. novelwriter/__init__.py +4 -4
  4. novelwriter/assets/icons/typicons_dark/icons.conf +6 -0
  5. novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
  6. novelwriter/assets/icons/typicons_dark/typ_document-add-col.svg +8 -0
  7. novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
  8. novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
  9. novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
  10. novelwriter/assets/icons/typicons_light/icons.conf +6 -0
  11. novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
  12. novelwriter/assets/icons/typicons_light/typ_document-add-col.svg +8 -0
  13. novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
  14. novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
  15. novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
  16. novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
  17. novelwriter/assets/images/novelwriter-text-light.svg +4 -0
  18. novelwriter/assets/images/welcome-dark.jpg +0 -0
  19. novelwriter/assets/images/welcome-light.jpg +0 -0
  20. novelwriter/assets/manual.pdf +0 -0
  21. novelwriter/assets/sample.zip +0 -0
  22. novelwriter/assets/syntax/default_dark.conf +1 -0
  23. novelwriter/assets/syntax/default_light.conf +1 -0
  24. novelwriter/assets/syntax/grey_dark.conf +1 -0
  25. novelwriter/assets/syntax/grey_light.conf +1 -0
  26. novelwriter/assets/syntax/light_owl.conf +1 -0
  27. novelwriter/assets/syntax/night_owl.conf +1 -0
  28. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  29. novelwriter/assets/syntax/solarized_light.conf +1 -0
  30. novelwriter/assets/syntax/tomorrow.conf +1 -0
  31. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  32. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  33. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  34. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  35. novelwriter/assets/text/credits_en.htm +4 -2
  36. novelwriter/assets/themes/default_dark.conf +2 -2
  37. novelwriter/assets/themes/default_light.conf +2 -2
  38. novelwriter/common.py +48 -37
  39. novelwriter/config.py +36 -41
  40. novelwriter/constants.py +38 -16
  41. novelwriter/core/buildsettings.py +7 -7
  42. novelwriter/core/coretools.py +192 -154
  43. novelwriter/core/docbuild.py +6 -3
  44. novelwriter/core/document.py +6 -6
  45. novelwriter/core/index.py +89 -56
  46. novelwriter/core/item.py +21 -3
  47. novelwriter/core/options.py +8 -7
  48. novelwriter/core/project.py +69 -44
  49. novelwriter/core/projectdata.py +1 -14
  50. novelwriter/core/projectxml.py +13 -41
  51. novelwriter/core/sessions.py +2 -1
  52. novelwriter/core/spellcheck.py +2 -1
  53. novelwriter/core/status.py +2 -1
  54. novelwriter/core/storage.py +178 -140
  55. novelwriter/core/tohtml.py +4 -2
  56. novelwriter/core/tokenizer.py +73 -45
  57. novelwriter/core/toodt.py +40 -30
  58. novelwriter/core/tree.py +3 -2
  59. novelwriter/dialogs/about.py +70 -160
  60. novelwriter/dialogs/docmerge.py +6 -5
  61. novelwriter/dialogs/docsplit.py +6 -6
  62. novelwriter/dialogs/editlabel.py +1 -1
  63. novelwriter/dialogs/preferences.py +553 -703
  64. novelwriter/dialogs/{projsettings.py → projectsettings.py} +288 -262
  65. novelwriter/dialogs/quotes.py +27 -23
  66. novelwriter/dialogs/wordlist.py +96 -40
  67. novelwriter/enum.py +20 -18
  68. novelwriter/error.py +1 -1
  69. novelwriter/extensions/circularprogress.py +11 -11
  70. novelwriter/extensions/configlayout.py +185 -134
  71. novelwriter/extensions/modified.py +81 -0
  72. novelwriter/extensions/novelselector.py +26 -12
  73. novelwriter/extensions/pagedsidebar.py +14 -16
  74. novelwriter/extensions/simpleprogress.py +5 -5
  75. novelwriter/extensions/statusled.py +8 -8
  76. novelwriter/extensions/switch.py +31 -63
  77. novelwriter/extensions/switchbox.py +1 -1
  78. novelwriter/extensions/versioninfo.py +153 -0
  79. novelwriter/gui/doceditor.py +178 -150
  80. novelwriter/gui/dochighlight.py +63 -92
  81. novelwriter/gui/docviewer.py +49 -51
  82. novelwriter/gui/docviewerpanel.py +72 -24
  83. novelwriter/gui/itemdetails.py +7 -7
  84. novelwriter/gui/mainmenu.py +14 -18
  85. novelwriter/gui/noveltree.py +9 -8
  86. novelwriter/gui/outline.py +98 -75
  87. novelwriter/gui/projtree.py +188 -61
  88. novelwriter/gui/sidebar.py +3 -4
  89. novelwriter/gui/statusbar.py +3 -4
  90. novelwriter/gui/theme.py +60 -68
  91. novelwriter/guimain.py +49 -156
  92. novelwriter/shared.py +15 -1
  93. novelwriter/tools/dictionaries.py +5 -6
  94. novelwriter/tools/manuscript.py +6 -6
  95. novelwriter/tools/manussettings.py +192 -221
  96. novelwriter/tools/noveldetails.py +525 -0
  97. novelwriter/tools/welcome.py +802 -0
  98. novelwriter/tools/writingstats.py +9 -9
  99. novelwriter/assets/images/wizard-back.jpg +0 -0
  100. novelwriter/assets/text/gplv3_en.htm +0 -641
  101. novelwriter/assets/text/release_notes.htm +0 -60
  102. novelwriter/dialogs/projdetails.py +0 -518
  103. novelwriter/dialogs/projload.py +0 -294
  104. novelwriter/dialogs/updates.py +0 -172
  105. novelwriter/extensions/pageddialog.py +0 -130
  106. novelwriter/tools/projwizard.py +0 -478
  107. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/LICENSE.md +0 -0
  108. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/WHEEL +0 -0
  109. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/entry_points.txt +0 -0
  110. {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/top_level.txt +0 -0
@@ -20,6 +20,7 @@ hidden = 88, 110, 117
20
20
  shortcode = 88, 110, 117
21
21
  keyword = 133, 153, 0
22
22
  value = 203, 75, 22
23
+ optional = 88, 110, 117
23
24
  spellcheckline = 203, 75, 22
24
25
  errorline = 220, 50, 47
25
26
  replacetag = 133, 153, 0
@@ -40,6 +40,7 @@ hidden = 142, 144, 140
40
40
  shortcode = 66, 113, 174
41
41
  keyword = 240, 40, 41
42
42
  value = 137, 89, 168
43
+ optional = 66, 113, 174
43
44
  spellcheckline = 240, 40, 41
44
45
  errorline = 113, 140, 0
45
46
  replacetag = 62, 153, 159
@@ -40,6 +40,7 @@ hidden = 150, 152, 150
40
40
  shortcode = 129, 162, 190
41
41
  keyword = 204, 102, 102
42
42
  value = 178, 148, 187
43
+ optional = 129, 162, 190
43
44
  spellcheckline = 204, 102, 102
44
45
  errorline = 181, 189, 104
45
46
  replacetag = 138, 190, 183
@@ -40,6 +40,7 @@ hidden = 114, 133, 183
40
40
  shortcode = 187, 218, 255
41
41
  keyword = 255, 157, 164
42
42
  value = 235, 187, 255
43
+ optional = 187, 218, 255
43
44
  spellcheckline = 255, 157, 164
44
45
  errorline = 209, 241, 169
45
46
  replacetag = 153, 255, 255
@@ -40,6 +40,7 @@ hidden = 150, 152, 150
40
40
  shortcode = 122, 166, 218
41
41
  keyword = 213, 78, 83
42
42
  value = 195, 151, 216
43
+ optional = 122, 166, 218
43
44
  spellcheckline = 213, 78, 83
44
45
  errorline = 185, 202, 74
45
46
  replacetag = 112, 192, 177
@@ -40,6 +40,7 @@ hidden = 153, 153, 153
40
40
  shortcode = 102, 153, 204
41
41
  keyword = 242, 119, 122
42
42
  value = 204, 153, 204
43
+ optional = 102, 153, 204
43
44
  spellcheckline = 242, 119, 122
44
45
  errorline = 153, 204, 153
45
46
  replacetag = 102, 204, 204
@@ -2,8 +2,6 @@
2
2
  <html>
3
3
  <body>
4
4
 
5
- <h2>Credits</h2>
6
-
7
5
  <h3>Main Developer</h3>
8
6
  <p>Veronica Berglyd Olsen (<a href="https://github.com/vkbo">@vkbo</a>)</p>
9
7
 
@@ -17,6 +15,10 @@
17
15
  <p>For other contributions, see the project's
18
16
  <a href="https://github.com/vkbo/novelWriter/graphs/contributors">Contributors</a> page.</p>
19
17
 
18
+ <h3>Artwork</h3>
19
+
20
+ <p>The artwork on the Welcome dialog was created by <a href="https://louisdurrant.art">Louis Durrant</a>.</p>
21
+
20
22
  <h3>Translations</h3>
21
23
 
22
24
  <p>The default language is English (UK) with English (US) as an option. These are the original
@@ -21,8 +21,8 @@ buttontext = 204, 204, 204
21
21
  brighttext = 62, 62, 62
22
22
  highlight = 44, 152, 247
23
23
  highlightedtext = 255, 255, 255
24
- link = 44, 152, 247
25
- linkvisited = 44, 152, 247
24
+ link = 102, 153, 204
25
+ linkvisited = 102, 153, 204
26
26
 
27
27
  [GUI]
28
28
  helptext = 164, 164, 164
@@ -21,8 +21,8 @@ buttontext = 0, 0, 0
21
21
  brighttext = 255, 255, 255
22
22
  highlight = 48, 135, 198
23
23
  highlightedtext = 255, 255, 255
24
- link = 0, 84, 255
25
- linkvisited = 0, 84, 255
24
+ link = 66, 113, 174
25
+ linkvisited = 66, 113, 174
26
26
 
27
27
  [GUI]
28
28
  helptext = 92, 92, 92
novelwriter/common.py CHANGED
@@ -29,7 +29,7 @@ import logging
29
29
  import unicodedata
30
30
  import xml.etree.ElementTree as ET
31
31
 
32
- from typing import Any, Literal
32
+ from typing import TYPE_CHECKING, Any, Literal
33
33
  from pathlib import Path
34
34
  from datetime import datetime
35
35
  from configparser import ConfigParser
@@ -41,7 +41,10 @@ from PyQt5.QtCore import QCoreApplication, QUrl
41
41
 
42
42
  from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
43
43
  from novelwriter.error import logException
44
- from novelwriter.constants import nwConst, nwUnicode
44
+ from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst
45
+
46
+ if TYPE_CHECKING: # pragma: no cover
47
+ from typing import TypeGuard # Requires Python 3.10
45
48
 
46
49
  logger = logging.getLogger(__name__)
47
50
 
@@ -104,15 +107,6 @@ def checkBool(value: Any, default: bool) -> bool:
104
107
  return default
105
108
 
106
109
 
107
- def checkHandle(value, default, allowNone=False):
108
- """Check if a value is a handle."""
109
- if allowNone and (value is None or value == "None"):
110
- return None
111
- if isHandle(value):
112
- return str(value)
113
- return default
114
-
115
-
116
110
  def checkUuid(value: Any, default: str) -> str:
117
111
  """Try to process a value as an UUID, or return a default."""
118
112
  try:
@@ -135,7 +129,7 @@ def checkPath(value: Any, default: Path) -> Path:
135
129
  # Validator Functions
136
130
  ##
137
131
 
138
- def isHandle(value: Any) -> bool:
132
+ def isHandle(value: Any) -> TypeGuard[str]:
139
133
  """Check if a string is a valid novelWriter handle.
140
134
  Note: This is case sensitive. Must be lower case!
141
135
  """
@@ -149,7 +143,7 @@ def isHandle(value: Any) -> bool:
149
143
  return True
150
144
 
151
145
 
152
- def isTitleTag(value: Any) -> bool:
146
+ def isTitleTag(value: Any) -> TypeGuard[str]:
153
147
  """Check if a string is a valid title tag string."""
154
148
  if not isinstance(value, str):
155
149
  return False
@@ -163,19 +157,19 @@ def isTitleTag(value: Any) -> bool:
163
157
  return True
164
158
 
165
159
 
166
- def isItemClass(value: str) -> bool:
160
+ def isItemClass(value: Any) -> TypeGuard[str]:
167
161
  """Check if a string is a valid nwItemClass identifier."""
168
- return value in nwItemClass.__members__
162
+ return isinstance(value, str) and value in nwItemClass.__members__
169
163
 
170
164
 
171
- def isItemType(value: str) -> bool:
165
+ def isItemType(value: Any) -> TypeGuard[str]:
172
166
  """Check if a string is a valid nwItemType identifier."""
173
- return value in nwItemType.__members__
167
+ return isinstance(value, str) and value in nwItemType.__members__
174
168
 
175
169
 
176
- def isItemLayout(value: str) -> bool:
170
+ def isItemLayout(value: Any) -> TypeGuard[str]:
177
171
  """Check if a string is a valid nwItemLayout identifier."""
178
- return value in nwItemLayout.__members__
172
+ return isinstance(value, str) and value in nwItemLayout.__members__
179
173
 
180
174
 
181
175
  def hexToInt(value: Any, default: int = 0) -> int:
@@ -189,8 +183,7 @@ def hexToInt(value: Any, default: int = 0) -> int:
189
183
 
190
184
 
191
185
  def minmax(value: int, minVal: int, maxVal: int) -> int:
192
- """Make sure an integer is between min and max value (inclusive).
193
- """
186
+ """Check that an value is between min and max value (inclusive)."""
194
187
  return min(maxVal, max(minVal, value))
195
188
 
196
189
 
@@ -213,17 +206,17 @@ def formatInt(value: int) -> str:
213
206
  if not isinstance(value, int):
214
207
  return "ERR"
215
208
 
216
- theVal = float(value)
217
- if theVal > 1000.0:
209
+ fVal = float(value)
210
+ if fVal > 1000.0:
218
211
  for pF in ["k", "M", "G", "T", "P", "E"]:
219
- theVal /= 1000.0
220
- if theVal < 1000.0:
221
- if theVal < 10.0:
222
- return f"{theVal:4.2f}{nwUnicode.U_THSP}{pF}"
223
- elif theVal < 100.0:
224
- return f"{theVal:4.1f}{nwUnicode.U_THSP}{pF}"
212
+ fVal /= 1000.0
213
+ if fVal < 1000.0:
214
+ if fVal < 10.0:
215
+ return f"{fVal:4.2f}{nwUnicode.U_THSP}{pF}"
216
+ elif fVal < 100.0:
217
+ return f"{fVal:4.1f}{nwUnicode.U_THSP}{pF}"
225
218
  else:
226
- return f"{theVal:3.0f}{nwUnicode.U_THSP}{pF}"
219
+ return f"{fVal:3.0f}{nwUnicode.U_THSP}{pF}"
227
220
 
228
221
  return str(value)
229
222
 
@@ -250,6 +243,24 @@ def formatTime(t: int) -> str:
250
243
  return "ERROR"
251
244
 
252
245
 
246
+ def formatVersion(value: str) -> str:
247
+ """Format a version number into a more human readable form."""
248
+ return value.lower().replace("a", " Alpha ").replace("b", " Beta ").replace("rc", " RC ")
249
+
250
+
251
+ def formatFileFilter(extensions: list[str | tuple[str, str]]) -> str:
252
+ """Format a list of extensions, or extension + label pairs into a
253
+ QFileDialog extensions filter.
254
+ """
255
+ result = []
256
+ for ext in extensions:
257
+ if isinstance(ext, str):
258
+ result.append(f"{trConst(nwLabels.FILE_FILTERS.get(ext))} ({ext})")
259
+ elif isinstance(ext, tuple) and len(ext) == 2:
260
+ result.append(f"{ext[0]} ({ext[1]})")
261
+ return ";;".join(result)
262
+
263
+
253
264
  ##
254
265
  # String Functions
255
266
  ##
@@ -270,22 +281,22 @@ def transferCase(source: str, target: str) -> str:
270
281
  """Transfers the case of the source word to the target word. This
271
282
  will consider all upper or lower, and first char capitalisation.
272
283
  """
273
- theResult = target
284
+ result = target
274
285
 
275
286
  if not isinstance(source, str) or not isinstance(target, str):
276
- return theResult
287
+ return result
277
288
  if len(target) < 1 or len(source) < 1:
278
- return theResult
289
+ return result
279
290
 
280
291
  if source.istitle():
281
- theResult = target.title()
292
+ result = target.title()
282
293
 
283
294
  if source.isupper():
284
- theResult = target.upper()
295
+ result = target.upper()
285
296
  elif source.islower():
286
- theResult = target.lower()
297
+ result = target.lower()
287
298
 
288
- return theResult
299
+ return result
289
300
 
290
301
 
291
302
  def fuzzyTime(seconds: int) -> str:
novelwriter/config.py CHANGED
@@ -110,8 +110,8 @@ class Config:
110
110
 
111
111
  # Size Settings
112
112
  self._mainWinSize = [1200, 650] # Last size of the main GUI window
113
+ self._welcomeSize = [800, 550] # Last size of the welcome window
113
114
  self._prefsWinSize = [700, 615] # Last size of the Preferences dialog
114
- self._projLoadCols = [280, 60, 160] # Last columns widths of the Project Load dialog
115
115
  self._mainPanePos = [300, 800] # Last position of the main window splitter
116
116
  self._viewPanePos = [500, 150] # Last position of the document viewer splitter
117
117
  self._outlnPanePos = [500, 150] # Last position of the outline panel splitter
@@ -150,9 +150,6 @@ class Config:
150
150
  self.autoScrollPos = 30 # Start point for typewriter-like scrolling
151
151
  self.scrollPastEnd = True # Scroll past end of document, and centre cursor
152
152
 
153
- self.wordCountTimer = 5.0 # Interval for word count update in seconds
154
- self.incNotesWCount = True # The status bar word count includes notes
155
-
156
153
  self.highlightQuotes = True # Highlight text in quotes
157
154
  self.allowOpenSQuote = False # Allow open-ended single quotes
158
155
  self.allowOpenDQuote = True # Allow open-ended double quotes
@@ -160,6 +157,7 @@ class Config:
160
157
 
161
158
  self.stopWhenIdle = True # Stop the status bar clock when the user is idle
162
159
  self.userIdleTime = 300 # Time of inactivity to consider user idle
160
+ self.incNotesWCount = True # The status bar word count includes notes
163
161
 
164
162
  # User-Selected Symbol Settings
165
163
  self.fmtApostrophe = nwUnicode.U_RSQUO
@@ -250,12 +248,12 @@ class Config:
250
248
  return [int(x*self.guiScale) for x in self._mainWinSize]
251
249
 
252
250
  @property
253
- def preferencesWinSize(self) -> list[int]:
254
- return [int(x*self.guiScale) for x in self._prefsWinSize]
251
+ def welcomeWinSize(self) -> list[int]:
252
+ return [int(x*self.guiScale) for x in self._welcomeSize]
255
253
 
256
254
  @property
257
- def projLoadColWidths(self) -> list[int]:
258
- return [int(x*self.guiScale) for x in self._projLoadCols]
255
+ def preferencesWinSize(self) -> list[int]:
256
+ return [int(x*self.guiScale) for x in self._prefsWinSize]
259
257
 
260
258
  @property
261
259
  def mainPanePos(self) -> list[int]:
@@ -306,17 +304,18 @@ class Config:
306
304
  self._mainWinSize[1] = height
307
305
  return
308
306
 
307
+ def setWelcomeWinSize(self, width: int, height: int) -> None:
308
+ """Set the size of the Preferences dialog window."""
309
+ self._welcomeSize[0] = int(width/self.guiScale)
310
+ self._welcomeSize[1] = int(height/self.guiScale)
311
+ return
312
+
309
313
  def setPreferencesWinSize(self, width: int, height: int) -> None:
310
314
  """Set the size of the Preferences dialog window."""
311
315
  self._prefsWinSize[0] = int(width/self.guiScale)
312
316
  self._prefsWinSize[1] = int(height/self.guiScale)
313
317
  return
314
318
 
315
- def setProjLoadColWidths(self, widths: list[int]) -> None:
316
- """Set the column widths of the Load Project dialog."""
317
- self._projLoadCols = [int(x/self.guiScale) for x in widths]
318
- return
319
-
320
319
  def setMainPanePos(self, pos: list[int]) -> None:
321
320
  """Set the position of the main GUI splitter."""
322
321
  self._mainPanePos = [int(x/self.guiScale) for x in pos]
@@ -410,10 +409,10 @@ class Config:
410
409
  """Compile and return error messages from the initialisation of
411
410
  the Config class, and clear the error buffer.
412
411
  """
413
- errMessage = "<br>".join(self._errData)
412
+ message = "<br>".join(self._errData)
414
413
  self._hasError = False
415
414
  self._errData = []
416
- return errMessage
415
+ return message
417
416
 
418
417
  def listLanguages(self, lngSet: int) -> list[tuple[str, str]]:
419
418
  """List localisation files in the i18n folder. The default GUI
@@ -545,8 +544,8 @@ class Config:
545
544
  # Sizes
546
545
  sec = "Sizes"
547
546
  self._mainWinSize = conf.rdIntList(sec, "mainwindow", self._mainWinSize)
547
+ self._welcomeSize = conf.rdIntList(sec, "welcome", self._welcomeSize)
548
548
  self._prefsWinSize = conf.rdIntList(sec, "preferences", self._prefsWinSize)
549
- self._projLoadCols = conf.rdIntList(sec, "projloadcols", self._projLoadCols)
550
549
  self._mainPanePos = conf.rdIntList(sec, "mainpane", self._mainPanePos)
551
550
  self._viewPanePos = conf.rdIntList(sec, "viewpane", self._viewPanePos)
552
551
  self._outlnPanePos = conf.rdIntList(sec, "outlinepane", self._outlnPanePos)
@@ -590,7 +589,6 @@ class Config:
590
589
  self.showTabsNSpaces = conf.rdBool(sec, "showtabsnspaces", self.showTabsNSpaces)
591
590
  self.showLineEndings = conf.rdBool(sec, "showlineendings", self.showLineEndings)
592
591
  self.showMultiSpaces = conf.rdBool(sec, "showmultispaces", self.showMultiSpaces)
593
- self.wordCountTimer = conf.rdFlt(sec, "wordcounttimer", self.wordCountTimer)
594
592
  self.incNotesWCount = conf.rdBool(sec, "incnoteswcount", self.incNotesWCount)
595
593
  self.showFullPath = conf.rdBool(sec, "showfullpath", self.showFullPath)
596
594
  self.highlightQuotes = conf.rdBool(sec, "highlightquotes", self.highlightQuotes)
@@ -635,7 +633,7 @@ class Config:
635
633
  conf = NWConfigParser()
636
634
 
637
635
  conf["Meta"] = {
638
- "timestamp": formatTimeStamp(time()),
636
+ "timestamp": formatTimeStamp(time()),
639
637
  }
640
638
 
641
639
  conf["Main"] = {
@@ -651,12 +649,12 @@ class Config:
651
649
  }
652
650
 
653
651
  conf["Sizes"] = {
654
- "mainwindow": self._packList(self._mainWinSize),
655
- "preferences": self._packList(self._prefsWinSize),
656
- "projloadcols": self._packList(self._projLoadCols),
657
- "mainpane": self._packList(self._mainPanePos),
658
- "viewpane": self._packList(self._viewPanePos),
659
- "outlinepane": self._packList(self._outlnPanePos),
652
+ "mainwindow": self._packList(self._mainWinSize),
653
+ "welcome": self._packList(self._welcomeSize),
654
+ "preferences": self._packList(self._prefsWinSize),
655
+ "mainpane": self._packList(self._mainPanePos),
656
+ "viewpane": self._packList(self._viewPanePos),
657
+ "outlinepane": self._packList(self._outlnPanePos),
660
658
  }
661
659
 
662
660
  conf["Project"] = {
@@ -697,7 +695,6 @@ class Config:
697
695
  "showtabsnspaces": str(self.showTabsNSpaces),
698
696
  "showlineendings": str(self.showLineEndings),
699
697
  "showmultispaces": str(self.showMultiSpaces),
700
- "wordcounttimer": str(self.wordCountTimer),
701
698
  "incnoteswcount": str(self.incNotesWCount),
702
699
  "showfullpath": str(self.showFullPath),
703
700
  "highlightquotes": str(self.highlightQuotes),
@@ -774,22 +771,20 @@ class RecentProjects:
774
771
  self._data = {}
775
772
 
776
773
  cacheFile = self._conf.dataPath(nwFiles.RECENT_FILE)
777
- if not cacheFile.is_file():
778
- return True
779
-
780
- try:
781
- with open(cacheFile, mode="r", encoding="utf-8") as inFile:
782
- theData = json.load(inFile)
783
- for projPath, theEntry in theData.items():
784
- self._data[projPath] = {
785
- "title": theEntry.get("title", ""),
786
- "words": theEntry.get("words", 0),
787
- "time": theEntry.get("time", 0),
788
- }
789
- except Exception:
790
- logger.error("Could not load recent project cache")
791
- logException()
792
- return False
774
+ if cacheFile.is_file():
775
+ try:
776
+ with open(cacheFile, mode="r", encoding="utf-8") as inFile:
777
+ data = json.load(inFile)
778
+ for path, entry in data.items():
779
+ self._data[path] = {
780
+ "title": entry.get("title", ""),
781
+ "words": entry.get("words", 0),
782
+ "time": entry.get("time", 0),
783
+ }
784
+ except Exception:
785
+ logger.error("Could not load recent project cache")
786
+ logException()
787
+ return False
793
788
 
794
789
  return True
795
790
 
novelwriter/constants.py CHANGED
@@ -43,12 +43,12 @@ class nwConst:
43
43
  FMT_DSTAMP = "%Y-%m-%d" # Date only format
44
44
 
45
45
  # URLs
46
- URL_WEB = "https://novelwriter.io"
47
- URL_DOCS = "https://docs.novelwriter.io"
48
- URL_CODE = "https://github.com/vkbo/novelWriter"
49
- URL_REPORT = "https://github.com/vkbo/novelWriter/issues"
50
- URL_HELP = "https://github.com/vkbo/novelWriter/discussions"
51
- URL_RELEASE = "https://github.com/vkbo/novelWriter/releases/latest"
46
+ URL_WEB = "https://novelwriter.io"
47
+ URL_DOCS = "https://docs.novelwriter.io"
48
+ URL_RELEASES = "https://releases.novelwriter.io"
49
+ URL_CODE = "https://github.com/vkbo/novelWriter"
50
+ URL_REPORT = "https://github.com/vkbo/novelWriter/issues"
51
+ URL_HELP = "https://github.com/vkbo/novelWriter/discussions"
52
52
 
53
53
  # Requests
54
54
  USER_AGENT = "Mozilla/5.0 (compatible; novelWriter (Python))"
@@ -56,6 +56,9 @@ class nwConst:
56
56
  # Gui Settings
57
57
  STATUS_MSG_TIMEOUT = 15000 # milliseconds
58
58
 
59
+ # Dialogs
60
+ DLG_FINISHED = 2
61
+
59
62
  # END Class nwConst
60
63
 
61
64
 
@@ -107,7 +110,6 @@ class nwFiles:
107
110
 
108
111
  # Project Root Files
109
112
  PROJ_FILE = "nwProject.nwx"
110
- PROJ_BACKUP = "nwProject.bak"
111
113
  PROJ_LOCK = "nwProject.lock"
112
114
  TOC_TXT = "ToC.txt"
113
115
 
@@ -184,6 +186,7 @@ class nwLabels:
184
186
  nwItemClass.ENTITY: QT_TRANSLATE_NOOP("Constant", "Entities"),
185
187
  nwItemClass.CUSTOM: QT_TRANSLATE_NOOP("Constant", "Custom"),
186
188
  nwItemClass.ARCHIVE: QT_TRANSLATE_NOOP("Constant", "Archive"),
189
+ nwItemClass.TEMPLATE: QT_TRANSLATE_NOOP("Constant", "Templates"),
187
190
  nwItemClass.TRASH: QT_TRANSLATE_NOOP("Constant", "Trash"),
188
191
  }
189
192
  CLASS_ICON = {
@@ -197,6 +200,7 @@ class nwLabels:
197
200
  nwItemClass.ENTITY: "cls_entity",
198
201
  nwItemClass.CUSTOM: "cls_custom",
199
202
  nwItemClass.ARCHIVE: "cls_archive",
203
+ nwItemClass.TEMPLATE: "cls_template",
200
204
  nwItemClass.TRASH: "cls_trash",
201
205
  }
202
206
  LAYOUT_NAME = {
@@ -266,6 +270,13 @@ class nwLabels:
266
270
  nwBuildFmt.J_HTML: ".json",
267
271
  nwBuildFmt.J_NWD: ".json",
268
272
  }
273
+ FILE_FILTERS = {
274
+ "*.txt": QT_TRANSLATE_NOOP("Constant", "Text files"),
275
+ "*.md": QT_TRANSLATE_NOOP("Constant", "Markdown files"),
276
+ "*.nwd": QT_TRANSLATE_NOOP("Constant", "novelWriter files"),
277
+ "*.csv": QT_TRANSLATE_NOOP("Constant", "CSV files"),
278
+ "*": QT_TRANSLATE_NOOP("Constant", "All files"),
279
+ }
269
280
  UNIT_NAME = {
270
281
  "mm": QT_TRANSLATE_NOOP("Constant", "Millimetres"),
271
282
  "cm": QT_TRANSLATE_NOOP("Constant", "Centimetres"),
@@ -298,16 +309,27 @@ class nwLabels:
298
309
 
299
310
  class nwHeadFmt:
300
311
 
301
- BR = "{BR}"
302
- TITLE = "{Title}"
303
- CH_NUM = "{Chapter}"
304
- CH_WORD = "{Chapter:Word}"
305
- CH_ROMU = "{Chapter:URoman}"
306
- CH_ROML = "{Chapter:LRoman}"
307
- SC_NUM = "{Scene}"
308
- SC_ABS = "{Scene:Abs}"
312
+ BR = "{BR}"
313
+ TITLE = "{Title}"
314
+ CH_NUM = "{Chapter}"
315
+ CH_WORD = "{Chapter:Word}"
316
+ CH_ROMU = "{Chapter:URoman}"
317
+ CH_ROML = "{Chapter:LRoman}"
318
+ SC_NUM = "{Scene}"
319
+ SC_ABS = "{Scene:Abs}"
320
+ CHAR_POV = "{Char:POV}"
321
+ CHAR_FOCUS = "{Char:Focus}"
322
+
323
+ PAGE_HEADERS = [
324
+ TITLE, CH_NUM, CH_WORD, CH_ROMU, CH_ROML, SC_NUM, SC_ABS,
325
+ CHAR_POV, CHAR_FOCUS
326
+ ]
309
327
 
310
- ALL = [TITLE, CH_NUM, CH_WORD, CH_ROMU, CH_ROML, SC_NUM, SC_ABS]
328
+ # ODT Document Page Header
329
+ ODT_PROJECT = "{Project}"
330
+ ODT_AUTHOR = "{Author}"
331
+ ODT_PAGE = "{Page}"
332
+ ODT_AUTO = "{Project} / {Author} / {Page}"
311
333
 
312
334
  # END Class nwHeadFmt
313
335
 
@@ -29,8 +29,8 @@ import uuid
29
29
  import logging
30
30
 
31
31
  from enum import Enum
32
- from typing import Iterable
33
32
  from pathlib import Path
33
+ from collections.abc import Iterable
34
34
 
35
35
  from PyQt5.QtCore import QT_TRANSLATE_NOOP, QCoreApplication
36
36
 
@@ -79,6 +79,8 @@ SETTINGS_TEMPLATE = {
79
79
  "format.leftMargin": (float, 2.0),
80
80
  "format.rightMargin": (float, 2.0),
81
81
  "odt.addColours": (bool, True),
82
+ "odt.pageHeader": (str, nwHeadFmt.ODT_AUTO),
83
+ "odt.pageCountOffset": (int, 0),
82
84
  "html.addStyles": (bool, True),
83
85
  }
84
86
 
@@ -125,6 +127,8 @@ SETTINGS_LABELS = {
125
127
 
126
128
  "odt": QT_TRANSLATE_NOOP("Builds", "Open Document (.odt)"),
127
129
  "odt.addColours": QT_TRANSLATE_NOOP("Builds", "Add Highlight Colours"),
130
+ "odt.pageHeader": QT_TRANSLATE_NOOP("Builds", "Page Header"),
131
+ "odt.pageCountOffset": QT_TRANSLATE_NOOP("Builds", "Page Counter Offset"),
128
132
 
129
133
  "html": QT_TRANSLATE_NOOP("Builds", "HTML (.html)"),
130
134
  "html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"),
@@ -235,16 +239,12 @@ class BuildSettings:
235
239
  def getInt(self, key: str) -> int:
236
240
  """Type safe value access for integers."""
237
241
  value = self._settings.get(key, SETTINGS_TEMPLATE.get(key, (None, None))[1])
238
- if isinstance(value, (int, float)):
239
- return int(value)
240
- return 0
242
+ return int(value) if isinstance(value, (int, float)) else 0
241
243
 
242
244
  def getFloat(self, key: str) -> float:
243
245
  """Type safe value access for floats."""
244
246
  value = self._settings.get(key, SETTINGS_TEMPLATE.get(key, (None, None))[1])
245
- if isinstance(value, (int, float)):
246
- return float(value)
247
- return 0.0
247
+ return float(value) if isinstance(value, (int, float)) else 0.0
248
248
 
249
249
  ##
250
250
  # Setters