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
@@ -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,55 +310,67 @@ 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")
368
+ projName = data.get("name", lblNewProject)
369
+ projAuthor = data.get("author", "")
370
+ projLang = data.get("language", "en_GB")
358
371
 
359
372
  project.data.setUuid(None)
360
373
  project.data.setName(projName)
361
- project.data.setTitle(projTitle)
362
374
  project.data.setAuthor(projAuthor)
363
375
  project.data.setLanguage(projLang)
364
376
  project.setDefaultStatusImport()
@@ -367,7 +379,7 @@ class ProjectBuilder:
367
379
  # Add Root Folders
368
380
  hNovelRoot = project.newRoot(nwItemClass.NOVEL)
369
381
  hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
370
- novelTitle = project.data.title if project.data.title else project.data.name
382
+ novelTitle = project.data.name
371
383
 
372
384
  titlePage = f"#! {novelTitle}\n\n"
373
385
  if project.data.author:
@@ -376,116 +388,142 @@ class ProjectBuilder:
376
388
  aDoc = project.storage.getDocument(hTitlePage)
377
389
  aDoc.writeDocument(titlePage)
378
390
 
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()
391
+ # Create a project structure based on selected root folders
392
+ # and a number of chapters and scenes selected in the
393
+ # wizard's custom page.
394
+
395
+ # Create chapters and scenes
396
+ numChapters = data.get("chapters", 0)
397
+ numScenes = data.get("scenes", 0)
398
+
399
+ chSynop = self.tr("Summary of the chapter.")
400
+ scSynop = self.tr("Summary of the scene.")
401
+ bfNote = self.tr("A short description.")
402
+
403
+ # Create chapters
404
+ if numChapters > 0:
405
+ for ch in range(numChapters):
406
+ chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
407
+ cHandle = project.newFile(chTitle, hNovelRoot)
408
+ aDoc = project.storage.getDocument(cHandle)
409
+ aDoc.writeDocument(f"## {chTitle}\n\n%Synopsis: {chSynop}\n\n")
410
+
411
+ # Create chapter scenes
412
+ if numScenes > 0 and cHandle:
413
+ for sc in range(numScenes):
414
+ scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
415
+ sHandle = project.newFile(scTitle, cHandle)
416
+ aDoc = project.storage.getDocument(sHandle)
417
+ aDoc.writeDocument(f"### {scTitle}\n\n%Synopsis: {scSynop}\n\n")
418
+
419
+ # Create scenes (no chapters)
420
+ elif numScenes > 0:
421
+ for sc in range(numScenes):
422
+ scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
423
+ sHandle = project.newFile(scTitle, hNovelRoot)
424
+ aDoc = project.storage.getDocument(sHandle)
425
+ aDoc.writeDocument(f"### {scTitle}\n\n%Synopsis: {scSynop}\n\n")
426
+
427
+ # Create notes folders
428
+ noteTitles = {
429
+ nwItemClass.PLOT: self.tr("Main Plot"),
430
+ nwItemClass.CHARACTER: self.tr("Protagonist"),
431
+ nwItemClass.WORLD: self.tr("Main Location"),
432
+ }
433
+
434
+ addNotes = data.get("notes", False)
435
+ for newRoot in data.get("roots", []):
436
+ if newRoot in nwItemClass:
437
+ rHandle = project.newRoot(newRoot)
438
+ if addNotes:
439
+ aHandle = project.newFile(noteTitles[newRoot], rHandle)
440
+ ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
441
+ aDoc = project.storage.getDocument(aHandle)
442
+ aDoc.writeDocument(
443
+ f"# {noteTitles[newRoot]}\n\n"
444
+ f"@tag: {ntTag}\n\n"
445
+ f"%Short: {bfNote}\n\n"
446
+ )
447
+
448
+ # Also add the archive and trash folders
449
+ project.newRoot(nwItemClass.ARCHIVE)
450
+ project.trashFolder()
451
+
452
+ project.saveProject()
453
+ project.closeProject()
463
454
 
464
455
  return True
465
456
 
466
- ##
467
- # Internal Functions
468
- ##
457
+ def _copyProject(self, path: Path, data: dict) -> bool:
458
+ """Copy an existing project content, but not the meta data, and
459
+ update new settings.
460
+ """
461
+ source = data.get("template")
462
+ if not (isinstance(source, Path) and source.is_file()
463
+ and (source.name == nwFiles.PROJ_FILE or is_zipfile(source))):
464
+ logger.error("Could not access source project: %s", source)
465
+ return False
469
466
 
470
- def _extractSampleProject(self, data: dict) -> bool:
467
+ logger.info("Copying project: %s", source)
468
+ if path.exists():
469
+ SHARED.error(self.tr(
470
+ "The target folder already exists. "
471
+ "Please choose another folder."
472
+ ))
473
+ return False
474
+
475
+ # Begin copying
476
+ srcPath = source.parent
477
+ dstPath = path.resolve()
478
+ srcCont = srcPath / "content"
479
+ dstCont = dstPath / "content"
480
+ try:
481
+ dstPath.mkdir(exist_ok=True)
482
+ dstCont.mkdir(exist_ok=True)
483
+ if is_zipfile(source):
484
+ with ZipFile(source) as zipObj:
485
+ for member in zipObj.namelist():
486
+ if member == nwFiles.PROJ_FILE:
487
+ zipObj.extract(member, dstPath)
488
+ elif member.startswith("content") and member.endswith(".nwd"):
489
+ zipObj.extract(member, dstPath)
490
+ else:
491
+ shutil.copy2(srcPath / nwFiles.PROJ_FILE, dstPath)
492
+ for item in srcCont.iterdir():
493
+ if item.is_file() and item.suffix == ".nwd" and isHandle(item.stem):
494
+ shutil.copy2(item, dstCont)
495
+ except Exception as exc:
496
+ SHARED.error(self.tr("Could not copy project files."), exc=exc)
497
+ return False
498
+
499
+ # Open the copied project and update settings
500
+ project = NWProject()
501
+ project.openProject(dstPath)
502
+ project.data.setUuid("") # Creates a fresh uuid
503
+ project.data.setName(data.get("name", "None"))
504
+ project.data.setAuthor(data.get("author", ""))
505
+ project.data.setLanguage(data.get("language", "en_GB"))
506
+ project.data.setSpellCheck(True)
507
+ project.data.setSpellLang(None)
508
+ project.data.setDoBackup(True)
509
+ project.data.setSaveCount(0)
510
+ project.data.setAutoCount(0)
511
+ project.data.setEditTime(0)
512
+ project.saveProject()
513
+ project.closeProject()
514
+
515
+ return True
516
+
517
+ def _extractSampleProject(self, path: Path) -> bool:
471
518
  """Make a copy of the sample project by extracting the
472
519
  sample.zip file to the new path.
473
520
  """
474
- projPath = data.get("projPath", None)
475
- if projPath is None:
476
- logger.error("No project path set for the example project")
477
- return False
478
-
479
- pkgSample = CONFIG.assetPath("sample.zip")
480
- if pkgSample.is_file():
521
+ if (sample := CONFIG.assetPath("sample.zip")).is_file():
481
522
  try:
482
- shutil.unpack_archive(pkgSample, projPath)
523
+ shutil.unpack_archive(sample, path)
483
524
  except Exception as exc:
484
- SHARED.error(self.tr(
485
- "Failed to create a new example project."
486
- ), exc=exc)
525
+ SHARED.error(self.tr("Failed to create a new example project."), exc=exc)
487
526
  return False
488
-
489
527
  else:
490
528
  SHARED.error(self.tr(
491
529
  "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."""