novelWriter 2.2.1__py3-none-any.whl → 2.3__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 (125) hide show
  1. {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/METADATA +1 -1
  2. {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/RECORD +116 -101
  3. novelWriter-2.3.dist-info/entry_points.txt +2 -0
  4. novelwriter/__init__.py +4 -4
  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_it_IT.qm +0 -0
  9. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  10. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  11. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  12. novelwriter/assets/i18n/project_nl_NL.json +11 -0
  13. novelwriter/assets/i18n/project_pt_BR.json +11 -0
  14. novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
  15. novelwriter/assets/icons/typicons_dark/mixed_document-new.svg +6 -0
  16. novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
  17. novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
  18. novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
  19. novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
  20. novelwriter/assets/icons/typicons_dark/typ_th-list.svg +9 -0
  21. novelwriter/assets/icons/typicons_light/icons.conf +8 -0
  22. novelwriter/assets/icons/typicons_light/mixed_document-new.svg +6 -0
  23. novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
  24. novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
  25. novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
  26. novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
  27. novelwriter/assets/icons/typicons_light/typ_th-list.svg +9 -0
  28. novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
  29. novelwriter/assets/images/novelwriter-text-light.svg +4 -0
  30. novelwriter/assets/images/welcome-dark.jpg +0 -0
  31. novelwriter/assets/images/welcome-light.jpg +0 -0
  32. novelwriter/assets/manual.pdf +0 -0
  33. novelwriter/assets/sample.zip +0 -0
  34. novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
  35. novelwriter/assets/syntax/default_dark.conf +1 -0
  36. novelwriter/assets/syntax/default_light.conf +1 -0
  37. novelwriter/assets/syntax/grey_dark.conf +1 -0
  38. novelwriter/assets/syntax/grey_light.conf +1 -0
  39. novelwriter/assets/syntax/light_owl.conf +1 -0
  40. novelwriter/assets/syntax/night_owl.conf +1 -0
  41. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  42. novelwriter/assets/syntax/solarized_light.conf +1 -0
  43. novelwriter/assets/syntax/tango.conf +23 -0
  44. novelwriter/assets/syntax/tomorrow.conf +1 -0
  45. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  46. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  47. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  48. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  49. novelwriter/assets/text/credits_en.htm +4 -2
  50. novelwriter/assets/themes/cyberpunk_night.conf +29 -0
  51. novelwriter/assets/themes/default_dark.conf +2 -2
  52. novelwriter/assets/themes/default_light.conf +2 -2
  53. novelwriter/common.py +48 -37
  54. novelwriter/config.py +36 -41
  55. novelwriter/constants.py +38 -16
  56. novelwriter/core/buildsettings.py +7 -7
  57. novelwriter/core/coretools.py +196 -156
  58. novelwriter/core/docbuild.py +6 -3
  59. novelwriter/core/document.py +6 -6
  60. novelwriter/core/index.py +89 -56
  61. novelwriter/core/item.py +21 -3
  62. novelwriter/core/options.py +8 -7
  63. novelwriter/core/project.py +70 -44
  64. novelwriter/core/projectdata.py +1 -14
  65. novelwriter/core/projectxml.py +13 -41
  66. novelwriter/core/sessions.py +2 -1
  67. novelwriter/core/spellcheck.py +2 -1
  68. novelwriter/core/status.py +2 -1
  69. novelwriter/core/storage.py +182 -140
  70. novelwriter/core/tohtml.py +4 -2
  71. novelwriter/core/tokenizer.py +109 -82
  72. novelwriter/core/toodt.py +40 -30
  73. novelwriter/core/tree.py +3 -2
  74. novelwriter/dialogs/about.py +70 -160
  75. novelwriter/dialogs/docmerge.py +6 -5
  76. novelwriter/dialogs/docsplit.py +6 -6
  77. novelwriter/dialogs/editlabel.py +1 -1
  78. novelwriter/dialogs/preferences.py +553 -703
  79. novelwriter/dialogs/{projsettings.py → projectsettings.py} +288 -262
  80. novelwriter/dialogs/quotes.py +27 -23
  81. novelwriter/dialogs/wordlist.py +96 -40
  82. novelwriter/enum.py +20 -18
  83. novelwriter/error.py +1 -1
  84. novelwriter/extensions/circularprogress.py +11 -11
  85. novelwriter/extensions/configlayout.py +185 -134
  86. novelwriter/extensions/modified.py +81 -0
  87. novelwriter/extensions/novelselector.py +26 -12
  88. novelwriter/extensions/pagedsidebar.py +14 -16
  89. novelwriter/extensions/simpleprogress.py +5 -5
  90. novelwriter/extensions/statusled.py +8 -8
  91. novelwriter/extensions/switch.py +31 -63
  92. novelwriter/extensions/switchbox.py +1 -1
  93. novelwriter/extensions/versioninfo.py +153 -0
  94. novelwriter/gui/doceditor.py +178 -150
  95. novelwriter/gui/dochighlight.py +63 -92
  96. novelwriter/gui/docviewer.py +49 -51
  97. novelwriter/gui/docviewerpanel.py +72 -24
  98. novelwriter/gui/itemdetails.py +7 -7
  99. novelwriter/gui/mainmenu.py +14 -19
  100. novelwriter/gui/noveltree.py +9 -8
  101. novelwriter/gui/outline.py +98 -75
  102. novelwriter/gui/projtree.py +241 -106
  103. novelwriter/gui/sidebar.py +3 -4
  104. novelwriter/gui/statusbar.py +3 -4
  105. novelwriter/gui/theme.py +69 -70
  106. novelwriter/guimain.py +51 -156
  107. novelwriter/shared.py +15 -1
  108. novelwriter/tools/dictionaries.py +5 -6
  109. novelwriter/tools/manuscript.py +6 -6
  110. novelwriter/tools/manussettings.py +192 -221
  111. novelwriter/tools/noveldetails.py +525 -0
  112. novelwriter/tools/welcome.py +819 -0
  113. novelwriter/tools/writingstats.py +9 -9
  114. novelWriter-2.2.1.dist-info/entry_points.txt +0 -5
  115. novelwriter/assets/images/wizard-back.jpg +0 -0
  116. novelwriter/assets/text/gplv3_en.htm +0 -641
  117. novelwriter/assets/text/release_notes.htm +0 -60
  118. novelwriter/dialogs/projdetails.py +0 -518
  119. novelwriter/dialogs/projload.py +0 -294
  120. novelwriter/dialogs/updates.py +0 -172
  121. novelwriter/extensions/pageddialog.py +0 -130
  122. novelwriter/tools/projwizard.py +0 -478
  123. {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/LICENSE.md +0 -0
  124. {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/WHEEL +0 -0
  125. {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/top_level.txt +0 -0
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
@@ -29,16 +29,19 @@ from __future__ import annotations
29
29
  import shutil
30
30
  import logging
31
31
 
32
- from typing import Iterable
32
+ from pathlib import Path
33
33
  from functools import partial
34
+ from zipfile import ZipFile, is_zipfile
35
+ from collections.abc import Iterable
34
36
 
35
37
  from PyQt5.QtCore import QCoreApplication
36
38
 
37
39
  from novelwriter import CONFIG, SHARED
38
- from novelwriter.common import minmax, simplified
39
- from novelwriter.constants import nwItemClass
40
+ from novelwriter.common import isHandle, minmax, simplified
41
+ from novelwriter.constants import nwFiles, nwItemClass
40
42
  from novelwriter.core.item import NWItem
41
43
  from novelwriter.core.project import NWProject
44
+ from novelwriter.core.storage import NWStorageCreate
42
45
 
43
46
  logger = logging.getLogger(__name__)
44
47
 
@@ -277,28 +280,25 @@ class DocDuplicator:
277
280
  """Run through a list of items, duplicate them, and copy the
278
281
  text content if they are documents.
279
282
  """
280
- if not items:
281
- return
282
-
283
- nHandle = items[0]
284
- hMap: dict[str, str | None] = {t: None for t in items}
285
- for tHandle in items:
286
- newItem = self._project.tree.duplicate(tHandle)
287
- if newItem is None:
288
- return
289
- hMap[tHandle] = newItem.itemHandle
290
- if newItem.itemParent in hMap:
291
- newItem.setParent(hMap[newItem.itemParent])
292
- self._project.tree.updateItemData(newItem.itemHandle)
293
- if newItem.isFileType():
294
- oldDoc = self._project.storage.getDocument(tHandle)
295
- newDoc = self._project.storage.getDocument(newItem.itemHandle)
296
- if newDoc.fileExists():
283
+ if items:
284
+ nHandle = items[0]
285
+ hMap: dict[str, str | None] = {t: None for t in items}
286
+ for tHandle in items:
287
+ newItem = self._project.tree.duplicate(tHandle)
288
+ if newItem is None:
297
289
  return
298
- newDoc.writeDocument(oldDoc.readDocument() or "")
299
- yield newItem.itemHandle, nHandle
300
- nHandle = None
301
-
290
+ hMap[tHandle] = newItem.itemHandle
291
+ if newItem.itemParent in hMap:
292
+ newItem.setParent(hMap[newItem.itemParent])
293
+ self._project.tree.updateItemData(newItem.itemHandle)
294
+ if newItem.isFileType():
295
+ oldDoc = self._project.storage.getDocument(tHandle)
296
+ newDoc = self._project.storage.getDocument(newItem.itemHandle)
297
+ if newDoc.fileExists():
298
+ return
299
+ newDoc.writeDocument(oldDoc.readDocument() or "")
300
+ yield newItem.itemHandle, nHandle
301
+ nHandle = None
302
302
  return
303
303
 
304
304
  # END Class DocDuplicator
@@ -310,64 +310,72 @@ class ProjectBuilder:
310
310
  """
311
311
 
312
312
  def __init__(self) -> None:
313
- self.tr = partial(QCoreApplication.translate, "NWProject")
313
+ self._path = None
314
+ self.tr = partial(QCoreApplication.translate, "ProjectBuilder")
314
315
  return
315
316
 
317
+ @property
318
+ def projPath(self) -> Path | None:
319
+ """The path of the newly created project."""
320
+ return self._path
321
+
316
322
  ##
317
323
  # Methods
318
324
  ##
319
325
 
320
326
  def buildProject(self, data: dict) -> bool:
321
- """Build a project from a data dictionary of specifications
322
- provided by the wizard.
323
- """
324
- if not isinstance(data, dict):
325
- logger.error("Invalid call to newProject function")
326
- return False
327
-
328
- popMinimal = data.get("popMinimal", True)
329
- popCustom = data.get("popCustom", False)
330
- popSample = data.get("popSample", False)
327
+ """Build or copy a project from a data dictionary."""
328
+ if isinstance(data, dict):
329
+ path = data.get("path", None) or None
330
+ if isinstance(path, (str, Path)):
331
+ self._path = Path(path).resolve()
332
+ if data.get("sample", False):
333
+ return self._extractSampleProject(self._path)
334
+ elif data.get("template"):
335
+ return self._copyProject(self._path, data)
336
+ else:
337
+ return self._buildAndPopulate(self._path, data)
338
+ SHARED.error("A project path is required.")
339
+ return False
331
340
 
332
- # Check if we're extracting the sample project. This is handled
333
- # differently as it isn't actually a new project, so we forward
334
- # this to another function and return here.
335
- if popSample:
336
- return self._extractSampleProject(data)
337
-
338
- projPath = data.get("projPath", None)
339
- if projPath is None:
340
- logger.error("No project path set for the new project")
341
- return False
341
+ ##
342
+ # Internal Functions
343
+ ##
342
344
 
345
+ def _buildAndPopulate(self, path: Path, data: dict) -> bool:
346
+ """Build a blank project from a data dictionary."""
343
347
  project = NWProject()
344
- if not project.storage.openProjectInPlace(projPath, newProject=True):
348
+ status = project.storage.createNewProject(path)
349
+ if status == NWStorageCreate.NOT_EMPTY:
350
+ SHARED.error(self.tr(
351
+ "The target folder is not empty. "
352
+ "Please choose another folder."
353
+ ))
354
+ return False
355
+ elif status == NWStorageCreate.OS_ERROR:
356
+ SHARED.error(self.tr(
357
+ "An error occurred while trying to create the project."
358
+ ), exc=project.storage.exc)
345
359
  return False
346
360
 
361
+ self._path = project.storage.storagePath
362
+
347
363
  lblNewProject = self.tr("New Project")
348
- lblNewChapter = self.tr("New Chapter")
349
- lblNewScene = self.tr("New Scene")
350
364
  lblTitlePage = self.tr("Title Page")
351
365
  lblByAuthors = self.tr("By")
352
366
 
353
367
  # Settings
354
- projName = data.get("projName", lblNewProject)
355
- projTitle = data.get("projTitle", lblNewProject)
356
- projAuthor = data.get("projAuthor", "")
357
- projLang = data.get("projLang", "en_GB")
358
-
359
368
  project.data.setUuid(None)
360
- project.data.setName(projName)
361
- project.data.setTitle(projTitle)
362
- project.data.setAuthor(projAuthor)
363
- project.data.setLanguage(projLang)
369
+ project.data.setName(data.get("name", lblNewProject))
370
+ project.data.setAuthor(data.get("author", ""))
371
+ project.data.setLanguage(CONFIG.guiLocale)
364
372
  project.setDefaultStatusImport()
365
373
  project.session.startSession()
366
374
 
367
375
  # Add Root Folders
368
376
  hNovelRoot = project.newRoot(nwItemClass.NOVEL)
369
377
  hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
370
- novelTitle = project.data.title if project.data.title else project.data.name
378
+ novelTitle = project.data.name
371
379
 
372
380
  titlePage = f"#! {novelTitle}\n\n"
373
381
  if project.data.author:
@@ -376,116 +384,148 @@ class ProjectBuilder:
376
384
  aDoc = project.storage.getDocument(hTitlePage)
377
385
  aDoc.writeDocument(titlePage)
378
386
 
379
- if popMinimal:
380
- # Creating a minimal project with a few root folders and a
381
- # single chapter with a single scene.
382
- hChapter = project.newFile(lblNewChapter, hNovelRoot)
383
- aDoc = project.storage.getDocument(hChapter)
384
- aDoc.writeDocument(f"## {lblNewChapter}\n\n")
385
-
386
- if hChapter:
387
- hScene = project.newFile(lblNewScene, hChapter)
388
- aDoc = project.storage.getDocument(hScene)
389
- aDoc.writeDocument(f"### {lblNewScene}\n\n")
390
-
391
- project.newRoot(nwItemClass.PLOT)
392
- project.newRoot(nwItemClass.CHARACTER)
393
- project.newRoot(nwItemClass.WORLD)
394
- project.newRoot(nwItemClass.ARCHIVE)
395
-
396
- project.saveProject()
397
- project.closeProject()
398
-
399
- elif popCustom:
400
- # Create a project structure based on selected root folders
401
- # and a number of chapters and scenes selected in the
402
- # wizard's custom page.
403
-
404
- # Create chapters and scenes
405
- numChapters = data.get("numChapters", 0)
406
- numScenes = data.get("numScenes", 0)
407
-
408
- chSynop = self.tr("Summary of the chapter.")
409
- scSynop = self.tr("Summary of the scene.")
410
- bfNote = self.tr("A short description.")
411
-
412
- # Create chapters
413
- if numChapters > 0:
414
- for ch in range(numChapters):
415
- chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
416
- cHandle = project.newFile(chTitle, hNovelRoot)
417
- aDoc = project.storage.getDocument(cHandle)
418
- aDoc.writeDocument(f"## {chTitle}\n\n% Synopsis: {chSynop}\n\n")
419
-
420
- # Create chapter scenes
421
- if numScenes > 0 and cHandle:
422
- for sc in range(numScenes):
423
- scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
424
- sHandle = project.newFile(scTitle, cHandle)
425
- aDoc = project.storage.getDocument(sHandle)
426
- aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
427
-
428
- # Create scenes (no chapters)
429
- elif numScenes > 0:
430
- for sc in range(numScenes):
431
- scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
432
- sHandle = project.newFile(scTitle, hNovelRoot)
433
- aDoc = project.storage.getDocument(sHandle)
434
- aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
435
-
436
- # Create notes folders
437
- noteTitles = {
438
- nwItemClass.PLOT: self.tr("Main Plot"),
439
- nwItemClass.CHARACTER: self.tr("Protagonist"),
440
- nwItemClass.WORLD: self.tr("Main Location"),
441
- }
442
-
443
- addNotes = data.get("addNotes", False)
444
- for newRoot in data.get("addRoots", []):
445
- if newRoot in nwItemClass:
446
- rHandle = project.newRoot(newRoot)
447
- if addNotes:
448
- aHandle = project.newFile(noteTitles[newRoot], rHandle)
449
- ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
450
- aDoc = project.storage.getDocument(aHandle)
451
- aDoc.writeDocument(
452
- f"# {noteTitles[newRoot]}\n\n"
453
- f"@tag: {ntTag}\n\n"
454
- f"% Short: {bfNote}\n\n"
455
- )
456
-
457
- # Also add the archive and trash folders
458
- project.newRoot(nwItemClass.ARCHIVE)
459
- project.trashFolder()
460
-
461
- project.saveProject()
462
- project.closeProject()
387
+ # Create a project structure based on selected root folders
388
+ # and a number of chapters and scenes selected in the
389
+ # wizard's custom page.
390
+
391
+ # Create chapters and scenes
392
+ numChapters = data.get("chapters", 0)
393
+ numScenes = data.get("scenes", 0)
394
+
395
+ chSynop = self.tr("Summary of the chapter.")
396
+ scSynop = self.tr("Summary of the scene.")
397
+ bfNote = self.tr("A short description.")
398
+
399
+ # Create chapters
400
+ if numChapters > 0:
401
+ for ch in range(numChapters):
402
+ chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
403
+ cHandle = project.newFile(chTitle, hNovelRoot)
404
+ aDoc = project.storage.getDocument(cHandle)
405
+ aDoc.writeDocument(f"## {chTitle}\n\n%Synopsis: {chSynop}\n\n")
406
+
407
+ # Create chapter scenes
408
+ if numScenes > 0 and cHandle:
409
+ for sc in range(numScenes):
410
+ scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
411
+ sHandle = project.newFile(scTitle, cHandle)
412
+ aDoc = project.storage.getDocument(sHandle)
413
+ aDoc.writeDocument(f"### {scTitle}\n\n%Synopsis: {scSynop}\n\n")
414
+
415
+ # Create scenes (no chapters)
416
+ elif numScenes > 0:
417
+ for sc in range(numScenes):
418
+ scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
419
+ sHandle = project.newFile(scTitle, hNovelRoot)
420
+ aDoc = project.storage.getDocument(sHandle)
421
+ aDoc.writeDocument(f"### {scTitle}\n\n%Synopsis: {scSynop}\n\n")
422
+
423
+ # Create notes folders
424
+ noteTitles = {
425
+ nwItemClass.PLOT: self.tr("Main Plot"),
426
+ nwItemClass.CHARACTER: self.tr("Protagonist"),
427
+ nwItemClass.WORLD: self.tr("Main Location"),
428
+ }
429
+
430
+ addNotes = data.get("notes", False)
431
+ for newRoot in data.get("roots", []):
432
+ if newRoot in nwItemClass:
433
+ rHandle = project.newRoot(newRoot)
434
+ if addNotes:
435
+ aHandle = project.newFile(noteTitles[newRoot], rHandle)
436
+ ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
437
+ aDoc = project.storage.getDocument(aHandle)
438
+ aDoc.writeDocument(
439
+ f"# {noteTitles[newRoot]}\n\n"
440
+ f"@tag: {ntTag}\n\n"
441
+ f"%Short: {bfNote}\n\n"
442
+ )
443
+
444
+ # Also add the archive and trash folders
445
+ project.newRoot(nwItemClass.ARCHIVE)
446
+ project.trashFolder()
447
+
448
+ project.saveProject()
449
+ project.closeProject()
463
450
 
464
451
  return True
465
452
 
466
- ##
467
- # Internal Functions
468
- ##
453
+ def _copyProject(self, path: Path, data: dict) -> bool:
454
+ """Copy an existing project content, but not the meta data, and
455
+ update new settings.
456
+ """
457
+ source = data.get("template")
458
+ if not (isinstance(source, Path) and source.is_file()
459
+ and (source.name == nwFiles.PROJ_FILE or is_zipfile(source))):
460
+ logger.error("Could not access source project: %s", source)
461
+ return False
462
+
463
+ logger.info("Copying project: %s", source)
464
+ if path.exists():
465
+ SHARED.error(self.tr(
466
+ "The target folder already exists. "
467
+ "Please choose another folder."
468
+ ))
469
+ return False
469
470
 
470
- def _extractSampleProject(self, data: dict) -> bool:
471
+ # Begin copying
472
+ srcPath = source.parent
473
+ dstPath = path.resolve()
474
+ srcCont = srcPath / "content"
475
+ dstCont = dstPath / "content"
476
+ try:
477
+ dstPath.mkdir(exist_ok=True)
478
+ dstCont.mkdir(exist_ok=True)
479
+ if is_zipfile(source):
480
+ with ZipFile(source) as zipObj:
481
+ for member in zipObj.namelist():
482
+ if member == nwFiles.PROJ_FILE:
483
+ zipObj.extract(member, dstPath)
484
+ elif member.startswith("content") and member.endswith(".nwd"):
485
+ zipObj.extract(member, dstPath)
486
+ else:
487
+ shutil.copy2(srcPath / nwFiles.PROJ_FILE, dstPath)
488
+ for item in srcCont.iterdir():
489
+ if item.is_file() and item.suffix == ".nwd" and isHandle(item.stem):
490
+ shutil.copy2(item, dstCont)
491
+ except Exception as exc:
492
+ SHARED.error(self.tr("Could not copy project files."), exc=exc)
493
+ return False
494
+
495
+ # Open the copied project and update settings
496
+ project = NWProject()
497
+ project.openProject(dstPath)
498
+ project.data.setUuid("") # Creates a fresh uuid
499
+ project.data.setName(data.get("name", "None"))
500
+ project.data.setAuthor(data.get("author", ""))
501
+ project.data.setSpellCheck(True)
502
+ project.data.setSpellLang(None)
503
+ project.data.setDoBackup(True)
504
+ project.data.setSaveCount(0)
505
+ project.data.setAutoCount(0)
506
+ project.data.setEditTime(0)
507
+ project.saveProject()
508
+ project.closeProject()
509
+
510
+ return True
511
+
512
+ def _extractSampleProject(self, path: Path) -> bool:
471
513
  """Make a copy of the sample project by extracting the
472
514
  sample.zip file to the new path.
473
515
  """
474
- projPath = data.get("projPath", None)
475
- if projPath is None:
476
- logger.error("No project path set for the example project")
516
+ if path.exists():
517
+ SHARED.error(self.tr(
518
+ "The target folder already exists. "
519
+ "Please choose another folder."
520
+ ))
477
521
  return False
478
522
 
479
- pkgSample = CONFIG.assetPath("sample.zip")
480
- if pkgSample.is_file():
523
+ if (sample := CONFIG.assetPath("sample.zip")).is_file():
481
524
  try:
482
- shutil.unpack_archive(pkgSample, projPath)
525
+ shutil.unpack_archive(sample, path)
483
526
  except Exception as exc:
484
- SHARED.error(self.tr(
485
- "Failed to create a new example project."
486
- ), exc=exc)
527
+ SHARED.error(self.tr("Failed to create a new example project."), exc=exc)
487
528
  return False
488
-
489
529
  else:
490
530
  SHARED.error(self.tr(
491
531
  "Failed to create a new example project. "
@@ -25,8 +25,8 @@ from __future__ import annotations
25
25
 
26
26
  import logging
27
27
 
28
- from typing import Iterable
29
28
  from pathlib import Path
29
+ from collections.abc import Iterable
30
30
 
31
31
  from PyQt5.QtGui import QFont, QFontInfo
32
32
 
@@ -248,8 +248,8 @@ class NWBuildDocument:
248
248
  def _setupBuild(self, bldObj: Tokenizer) -> dict:
249
249
  """Configure the build object."""
250
250
  # Get Settings
251
- textFont = self._build.getStr("format.textFont")
252
- textSize = self._build.getInt("format.textSize")
251
+ textFont = self._build.getStr("format.textFont")
252
+ textSize = self._build.getInt("format.textSize")
253
253
 
254
254
  fontFamily = textFont or CONFIG.textFont
255
255
  bldFont = QFont(fontFamily, textSize)
@@ -284,6 +284,9 @@ class NWBuildDocument:
284
284
  if isinstance(bldObj, ToOdt):
285
285
  bldObj.setColourHeaders(self._build.getBool("odt.addColours"))
286
286
  bldObj.setLanguage(self._project.data.language)
287
+ bldObj.setHeaderFormat(
288
+ self._build.getStr("odt.pageHeader"), self._build.getInt("odt.pageCountOffset")
289
+ )
287
290
 
288
291
  scale = nwLabels.UNIT_SCALE.get(self._build.getStr("format.pageUnit"), 1.0)
289
292
  pW, pH = nwLabels.PAPER_SIZE.get(self._build.getStr("format.pageSize"), (-1.0, -1.0))
@@ -52,7 +52,7 @@ class NWDocument:
52
52
 
53
53
  def __init__(self, project: NWProject, tHandle: str | None) -> None:
54
54
 
55
- self._project = project
55
+ self._project = project
56
56
 
57
57
  self._item = None # The currently open item
58
58
  self._handle = None # The handle of the currently open item
@@ -284,12 +284,12 @@ class NWDocument:
284
284
  """Parse the document meta tag and return the name, parent,
285
285
  class and layout meta values.
286
286
  """
287
- theName = self._docMeta.get("name", "")
288
- theParent = self._docMeta.get("parent", None)
289
- theClass = self._docMeta.get("class", None)
290
- theLayout = self._docMeta.get("layout", None)
287
+ name = self._docMeta.get("name", "")
288
+ parent = self._docMeta.get("parent", None)
289
+ itemClass = self._docMeta.get("class", None)
290
+ itemLayout = self._docMeta.get("layout", None)
291
291
 
292
- return theName, theParent, theClass, theLayout
292
+ return name, parent, itemClass, itemLayout
293
293
 
294
294
  def getError(self) -> str:
295
295
  """Return the last recorded exception."""