novelWriter 2.2rc1__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 (162) hide show
  1. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/METADATA +1 -1
  2. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/RECORD +149 -132
  3. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/WHEEL +1 -1
  4. novelWriter-2.3.dist-info/entry_points.txt +2 -0
  5. novelwriter/__init__.py +11 -6
  6. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  7. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  8. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  9. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  10. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  11. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  12. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  13. novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
  14. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  15. novelwriter/assets/i18n/project_de_DE.json +1 -0
  16. novelwriter/assets/i18n/project_en_US.json +1 -0
  17. novelwriter/assets/i18n/project_es_419.json +11 -0
  18. novelwriter/assets/i18n/project_fr_FR.json +11 -0
  19. novelwriter/assets/i18n/project_it_IT.json +11 -0
  20. novelwriter/assets/i18n/project_ja_JP.json +2 -1
  21. novelwriter/assets/i18n/project_nb_NO.json +1 -0
  22. novelwriter/assets/i18n/project_nl_NL.json +11 -0
  23. novelwriter/assets/i18n/project_pt_BR.json +11 -0
  24. novelwriter/assets/i18n/project_zh_CN.json +11 -0
  25. novelwriter/assets/icons/typicons_dark/icons.conf +11 -2
  26. novelwriter/assets/icons/typicons_dark/mixed_document-new.svg +6 -0
  27. novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
  28. novelwriter/assets/icons/typicons_dark/nw_tb-bold-md.svg +4 -0
  29. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +3 -1
  30. novelwriter/assets/icons/typicons_dark/nw_tb-italic-md.svg +4 -0
  31. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +3 -1
  32. novelwriter/assets/icons/typicons_dark/nw_tb-strike-md.svg +4 -0
  33. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +3 -1
  34. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +4 -2
  35. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +4 -2
  36. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +4 -2
  37. novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
  38. novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
  39. novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
  40. novelwriter/assets/icons/typicons_dark/typ_th-list.svg +9 -0
  41. novelwriter/assets/icons/typicons_light/icons.conf +11 -2
  42. novelwriter/assets/icons/typicons_light/mixed_document-new.svg +6 -0
  43. novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
  44. novelwriter/assets/icons/typicons_light/nw_tb-bold-md.svg +4 -0
  45. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +3 -1
  46. novelwriter/assets/icons/typicons_light/nw_tb-italic-md.svg +4 -0
  47. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +3 -1
  48. novelwriter/assets/icons/typicons_light/nw_tb-strike-md.svg +4 -0
  49. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +3 -1
  50. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +4 -2
  51. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +4 -2
  52. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +4 -2
  53. novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
  54. novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
  55. novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
  56. novelwriter/assets/icons/typicons_light/typ_th-list.svg +9 -0
  57. novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
  58. novelwriter/assets/images/novelwriter-text-light.svg +4 -0
  59. novelwriter/assets/images/welcome-dark.jpg +0 -0
  60. novelwriter/assets/images/welcome-light.jpg +0 -0
  61. novelwriter/assets/manual.pdf +0 -0
  62. novelwriter/assets/sample.zip +0 -0
  63. novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
  64. novelwriter/assets/syntax/default_dark.conf +1 -0
  65. novelwriter/assets/syntax/default_light.conf +1 -0
  66. novelwriter/assets/syntax/grey_dark.conf +1 -0
  67. novelwriter/assets/syntax/grey_light.conf +1 -0
  68. novelwriter/assets/syntax/light_owl.conf +1 -0
  69. novelwriter/assets/syntax/night_owl.conf +1 -0
  70. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  71. novelwriter/assets/syntax/solarized_light.conf +1 -0
  72. novelwriter/assets/syntax/tango.conf +23 -0
  73. novelwriter/assets/syntax/tomorrow.conf +1 -0
  74. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  75. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  76. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  77. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  78. novelwriter/assets/text/credits_en.htm +4 -2
  79. novelwriter/assets/themes/cyberpunk_night.conf +29 -0
  80. novelwriter/assets/themes/default_dark.conf +2 -2
  81. novelwriter/assets/themes/default_light.conf +2 -2
  82. novelwriter/common.py +64 -66
  83. novelwriter/config.py +39 -44
  84. novelwriter/constants.py +39 -17
  85. novelwriter/core/buildsettings.py +8 -8
  86. novelwriter/core/coretools.py +198 -157
  87. novelwriter/core/docbuild.py +7 -4
  88. novelwriter/core/document.py +7 -7
  89. novelwriter/core/index.py +90 -57
  90. novelwriter/core/item.py +23 -5
  91. novelwriter/core/options.py +11 -10
  92. novelwriter/core/project.py +73 -47
  93. novelwriter/core/projectdata.py +3 -16
  94. novelwriter/core/projectxml.py +14 -42
  95. novelwriter/core/sessions.py +4 -3
  96. novelwriter/core/spellcheck.py +6 -4
  97. novelwriter/core/status.py +5 -4
  98. novelwriter/core/storage.py +183 -141
  99. novelwriter/core/tohtml.py +6 -4
  100. novelwriter/core/tokenizer.py +110 -83
  101. novelwriter/core/tomd.py +2 -2
  102. novelwriter/core/toodt.py +41 -31
  103. novelwriter/core/tree.py +5 -4
  104. novelwriter/dialogs/about.py +88 -179
  105. novelwriter/dialogs/docmerge.py +30 -20
  106. novelwriter/dialogs/docsplit.py +33 -22
  107. novelwriter/dialogs/editlabel.py +20 -8
  108. novelwriter/dialogs/preferences.py +562 -725
  109. novelwriter/dialogs/{projsettings.py → projectsettings.py} +301 -270
  110. novelwriter/dialogs/quotes.py +47 -36
  111. novelwriter/dialogs/wordlist.py +128 -59
  112. novelwriter/enum.py +25 -22
  113. novelwriter/error.py +2 -2
  114. novelwriter/extensions/circularprogress.py +12 -12
  115. novelwriter/extensions/configlayout.py +185 -146
  116. novelwriter/extensions/{wheeleventfilter.py → eventfilters.py} +15 -5
  117. novelwriter/extensions/modified.py +81 -0
  118. novelwriter/extensions/novelselector.py +27 -13
  119. novelwriter/extensions/pagedsidebar.py +15 -20
  120. novelwriter/extensions/simpleprogress.py +8 -9
  121. novelwriter/extensions/statusled.py +9 -9
  122. novelwriter/extensions/switch.py +32 -64
  123. novelwriter/extensions/switchbox.py +2 -7
  124. novelwriter/extensions/versioninfo.py +153 -0
  125. novelwriter/gui/doceditor.py +250 -214
  126. novelwriter/gui/dochighlight.py +66 -94
  127. novelwriter/gui/docviewer.py +71 -98
  128. novelwriter/gui/docviewerpanel.py +140 -47
  129. novelwriter/gui/editordocument.py +3 -3
  130. novelwriter/gui/itemdetails.py +9 -9
  131. novelwriter/gui/mainmenu.py +47 -47
  132. novelwriter/gui/noveltree.py +53 -61
  133. novelwriter/gui/outline.py +100 -76
  134. novelwriter/gui/projtree.py +246 -112
  135. novelwriter/gui/sidebar.py +9 -8
  136. novelwriter/gui/statusbar.py +49 -7
  137. novelwriter/gui/theme.py +74 -76
  138. novelwriter/guimain.py +175 -330
  139. novelwriter/shared.py +68 -30
  140. novelwriter/tools/dictionaries.py +7 -8
  141. novelwriter/tools/lipsum.py +34 -28
  142. novelwriter/tools/manusbuild.py +3 -4
  143. novelwriter/tools/manuscript.py +25 -32
  144. novelwriter/tools/manussettings.py +194 -225
  145. novelwriter/tools/noveldetails.py +525 -0
  146. novelwriter/tools/welcome.py +819 -0
  147. novelwriter/tools/writingstats.py +26 -13
  148. novelWriter-2.2rc1.dist-info/entry_points.txt +0 -5
  149. novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +0 -8
  150. novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +0 -8
  151. novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +0 -8
  152. novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +0 -8
  153. novelwriter/assets/images/wizard-back.jpg +0 -0
  154. novelwriter/assets/text/gplv3_en.htm +0 -641
  155. novelwriter/assets/text/release_notes.htm +0 -17
  156. novelwriter/dialogs/projdetails.py +0 -525
  157. novelwriter/dialogs/projload.py +0 -298
  158. novelwriter/dialogs/updates.py +0 -182
  159. novelwriter/extensions/pageddialog.py +0 -130
  160. novelwriter/tools/projwizard.py +0 -478
  161. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/LICENSE.md +0 -0
  162. {novelWriter-2.2rc1.dist-info → novelWriter-2.3.dist-info}/top_level.txt +0 -0
@@ -6,9 +6,10 @@ File History:
6
6
  Created: 2022-10-02 [2.0rc1] DocMerger
7
7
  Created: 2022-10-11 [2.0rc1] DocSplitter
8
8
  Created: 2022-11-03 [2.0rc2] ProjectBuilder
9
+ Created: 2023-07-20 [2.1b1] DocDuplicator
9
10
 
10
11
  This file is a part of novelWriter
11
- Copyright 2018–2023, Veronica Berglyd Olsen
12
+ Copyright 2018–2024, Veronica Berglyd Olsen
12
13
 
13
14
  This program is free software: you can redistribute it and/or modify
14
15
  it under the terms of the GNU General Public License as published by
@@ -28,16 +29,19 @@ from __future__ import annotations
28
29
  import shutil
29
30
  import logging
30
31
 
31
- from typing import Iterable
32
+ from pathlib import Path
32
33
  from functools import partial
34
+ from zipfile import ZipFile, is_zipfile
35
+ from collections.abc import Iterable
33
36
 
34
37
  from PyQt5.QtCore import QCoreApplication
35
38
 
36
39
  from novelwriter import CONFIG, SHARED
37
- from novelwriter.common import minmax, simplified
38
- from novelwriter.constants import nwItemClass
40
+ from novelwriter.common import isHandle, minmax, simplified
41
+ from novelwriter.constants import nwFiles, nwItemClass
39
42
  from novelwriter.core.item import NWItem
40
43
  from novelwriter.core.project import NWProject
44
+ from novelwriter.core.storage import NWStorageCreate
41
45
 
42
46
  logger = logging.getLogger(__name__)
43
47
 
@@ -276,28 +280,25 @@ class DocDuplicator:
276
280
  """Run through a list of items, duplicate them, and copy the
277
281
  text content if they are documents.
278
282
  """
279
- if not items:
280
- return
281
-
282
- nHandle = items[0]
283
- hMap: dict[str, str | None] = {t: None for t in items}
284
- for tHandle in items:
285
- newItem = self._project.tree.duplicate(tHandle)
286
- if newItem is None:
287
- return
288
- hMap[tHandle] = newItem.itemHandle
289
- if newItem.itemParent in hMap:
290
- newItem.setParent(hMap[newItem.itemParent])
291
- self._project.tree.updateItemData(newItem.itemHandle)
292
- if newItem.isFileType():
293
- oldDoc = self._project.storage.getDocument(tHandle)
294
- newDoc = self._project.storage.getDocument(newItem.itemHandle)
295
- 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:
296
289
  return
297
- newDoc.writeDocument(oldDoc.readDocument() or "")
298
- yield newItem.itemHandle, nHandle
299
- nHandle = None
300
-
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
301
302
  return
302
303
 
303
304
  # END Class DocDuplicator
@@ -309,64 +310,72 @@ class ProjectBuilder:
309
310
  """
310
311
 
311
312
  def __init__(self) -> None:
312
- self.tr = partial(QCoreApplication.translate, "NWProject")
313
+ self._path = None
314
+ self.tr = partial(QCoreApplication.translate, "ProjectBuilder")
313
315
  return
314
316
 
317
+ @property
318
+ def projPath(self) -> Path | None:
319
+ """The path of the newly created project."""
320
+ return self._path
321
+
315
322
  ##
316
323
  # Methods
317
324
  ##
318
325
 
319
326
  def buildProject(self, data: dict) -> bool:
320
- """Build a project from a data dictionary of specifications
321
- provided by the wizard.
322
- """
323
- if not isinstance(data, dict):
324
- logger.error("Invalid call to newProject function")
325
- return False
326
-
327
- popMinimal = data.get("popMinimal", True)
328
- popCustom = data.get("popCustom", False)
329
- 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
330
340
 
331
- # Check if we're extracting the sample project. This is handled
332
- # differently as it isn't actually a new project, so we forward
333
- # this to another function and return here.
334
- if popSample:
335
- return self._extractSampleProject(data)
336
-
337
- projPath = data.get("projPath", None)
338
- if projPath is None:
339
- logger.error("No project path set for the new project")
340
- return False
341
+ ##
342
+ # Internal Functions
343
+ ##
341
344
 
345
+ def _buildAndPopulate(self, path: Path, data: dict) -> bool:
346
+ """Build a blank project from a data dictionary."""
342
347
  project = NWProject()
343
- 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)
344
359
  return False
345
360
 
361
+ self._path = project.storage.storagePath
362
+
346
363
  lblNewProject = self.tr("New Project")
347
- lblNewChapter = self.tr("New Chapter")
348
- lblNewScene = self.tr("New Scene")
349
364
  lblTitlePage = self.tr("Title Page")
350
365
  lblByAuthors = self.tr("By")
351
366
 
352
367
  # Settings
353
- projName = data.get("projName", lblNewProject)
354
- projTitle = data.get("projTitle", lblNewProject)
355
- projAuthor = data.get("projAuthor", "")
356
- projLang = data.get("projLang", "en_GB")
357
-
358
368
  project.data.setUuid(None)
359
- project.data.setName(projName)
360
- project.data.setTitle(projTitle)
361
- project.data.setAuthor(projAuthor)
362
- 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)
363
372
  project.setDefaultStatusImport()
364
373
  project.session.startSession()
365
374
 
366
375
  # Add Root Folders
367
376
  hNovelRoot = project.newRoot(nwItemClass.NOVEL)
368
377
  hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
369
- novelTitle = project.data.title if project.data.title else project.data.name
378
+ novelTitle = project.data.name
370
379
 
371
380
  titlePage = f"#! {novelTitle}\n\n"
372
381
  if project.data.author:
@@ -375,116 +384,148 @@ class ProjectBuilder:
375
384
  aDoc = project.storage.getDocument(hTitlePage)
376
385
  aDoc.writeDocument(titlePage)
377
386
 
378
- if popMinimal:
379
- # Creating a minimal project with a few root folders and a
380
- # single chapter with a single scene.
381
- hChapter = project.newFile(lblNewChapter, hNovelRoot)
382
- aDoc = project.storage.getDocument(hChapter)
383
- aDoc.writeDocument(f"## {lblNewChapter}\n\n")
384
-
385
- if hChapter:
386
- hScene = project.newFile(lblNewScene, hChapter)
387
- aDoc = project.storage.getDocument(hScene)
388
- aDoc.writeDocument(f"### {lblNewScene}\n\n")
389
-
390
- project.newRoot(nwItemClass.PLOT)
391
- project.newRoot(nwItemClass.CHARACTER)
392
- project.newRoot(nwItemClass.WORLD)
393
- project.newRoot(nwItemClass.ARCHIVE)
394
-
395
- project.saveProject()
396
- project.closeProject()
397
-
398
- elif popCustom:
399
- # Create a project structure based on selected root folders
400
- # and a number of chapters and scenes selected in the
401
- # wizard's custom page.
402
-
403
- # Create chapters and scenes
404
- numChapters = data.get("numChapters", 0)
405
- numScenes = data.get("numScenes", 0)
406
-
407
- chSynop = self.tr("Summary of the chapter.")
408
- scSynop = self.tr("Summary of the scene.")
409
- bfNote = self.tr("A short description.")
410
-
411
- # Create chapters
412
- if numChapters > 0:
413
- for ch in range(numChapters):
414
- chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
415
- cHandle = project.newFile(chTitle, hNovelRoot)
416
- aDoc = project.storage.getDocument(cHandle)
417
- aDoc.writeDocument(f"## {chTitle}\n\n% Synopsis: {chSynop}\n\n")
418
-
419
- # Create chapter scenes
420
- if numScenes > 0 and cHandle:
421
- for sc in range(numScenes):
422
- scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
423
- sHandle = project.newFile(scTitle, cHandle)
424
- aDoc = project.storage.getDocument(sHandle)
425
- aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
426
-
427
- # Create scenes (no chapters)
428
- elif numScenes > 0:
429
- for sc in range(numScenes):
430
- scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
431
- sHandle = project.newFile(scTitle, hNovelRoot)
432
- aDoc = project.storage.getDocument(sHandle)
433
- aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
434
-
435
- # Create notes folders
436
- noteTitles = {
437
- nwItemClass.PLOT: self.tr("Main Plot"),
438
- nwItemClass.CHARACTER: self.tr("Protagonist"),
439
- nwItemClass.WORLD: self.tr("Main Location"),
440
- }
441
-
442
- addNotes = data.get("addNotes", False)
443
- for newRoot in data.get("addRoots", []):
444
- if newRoot in nwItemClass:
445
- rHandle = project.newRoot(newRoot)
446
- if addNotes:
447
- aHandle = project.newFile(noteTitles[newRoot], rHandle)
448
- ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
449
- aDoc = project.storage.getDocument(aHandle)
450
- aDoc.writeDocument(
451
- f"# {noteTitles[newRoot]}\n\n"
452
- f"@tag: {ntTag}\n\n"
453
- f"% Short: {bfNote}\n\n"
454
- )
455
-
456
- # Also add the archive and trash folders
457
- project.newRoot(nwItemClass.ARCHIVE)
458
- project.trashFolder()
459
-
460
- project.saveProject()
461
- 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()
462
450
 
463
451
  return True
464
452
 
465
- ##
466
- # Internal Functions
467
- ##
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
468
470
 
469
- 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:
470
513
  """Make a copy of the sample project by extracting the
471
514
  sample.zip file to the new path.
472
515
  """
473
- projPath = data.get("projPath", None)
474
- if projPath is None:
475
- 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
+ ))
476
521
  return False
477
522
 
478
- pkgSample = CONFIG.assetPath("sample.zip")
479
- if pkgSample.is_file():
523
+ if (sample := CONFIG.assetPath("sample.zip")).is_file():
480
524
  try:
481
- shutil.unpack_archive(pkgSample, projPath)
525
+ shutil.unpack_archive(sample, path)
482
526
  except Exception as exc:
483
- SHARED.error(self.tr(
484
- "Failed to create a new example project."
485
- ), exc=exc)
527
+ SHARED.error(self.tr("Failed to create a new example project."), exc=exc)
486
528
  return False
487
-
488
529
  else:
489
530
  SHARED.error(self.tr(
490
531
  "Failed to create a new example project. "
@@ -6,7 +6,7 @@ File History:
6
6
  Created: 2022-12-01 [2.1b1] NWBuildDocument
7
7
 
8
8
  This file is a part of novelWriter
9
- Copyright 2018–2023, Veronica Berglyd Olsen
9
+ Copyright 2018–2024, Veronica Berglyd Olsen
10
10
 
11
11
  This program is free software: you can redistribute it and/or modify
12
12
  it under the terms of the GNU General Public License as published by
@@ -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))
@@ -6,7 +6,7 @@ File History:
6
6
  Created: 2018-09-29 [0.0.1]
7
7
 
8
8
  This file is a part of novelWriter
9
- Copyright 2018–2023, Veronica Berglyd Olsen
9
+ Copyright 2018–2024, Veronica Berglyd Olsen
10
10
 
11
11
  This program is free software: you can redistribute it and/or modify
12
12
  it under the terms of the GNU General Public License as published by
@@ -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."""