novelWriter 2.2rc1__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 (153) hide show
  1. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/METADATA +1 -1
  2. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/RECORD +141 -129
  3. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/WHEEL +1 -1
  4. novelwriter/__init__.py +11 -6
  5. novelwriter/assets/i18n/nw_de_DE.qm +0 -0
  6. novelwriter/assets/i18n/nw_en_US.qm +0 -0
  7. novelwriter/assets/i18n/nw_es_419.qm +0 -0
  8. novelwriter/assets/i18n/nw_fr_FR.qm +0 -0
  9. novelwriter/assets/i18n/nw_it_IT.qm +0 -0
  10. novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
  11. novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
  12. novelwriter/assets/i18n/nw_zh_CN.qm +0 -0
  13. novelwriter/assets/i18n/project_de_DE.json +1 -0
  14. novelwriter/assets/i18n/project_en_US.json +1 -0
  15. novelwriter/assets/i18n/project_es_419.json +11 -0
  16. novelwriter/assets/i18n/project_fr_FR.json +11 -0
  17. novelwriter/assets/i18n/project_it_IT.json +11 -0
  18. novelwriter/assets/i18n/project_ja_JP.json +2 -1
  19. novelwriter/assets/i18n/project_nb_NO.json +1 -0
  20. novelwriter/assets/i18n/project_zh_CN.json +11 -0
  21. novelwriter/assets/icons/typicons_dark/icons.conf +9 -2
  22. novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
  23. novelwriter/assets/icons/typicons_dark/nw_tb-bold-md.svg +4 -0
  24. novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg +3 -1
  25. novelwriter/assets/icons/typicons_dark/nw_tb-italic-md.svg +4 -0
  26. novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg +3 -1
  27. novelwriter/assets/icons/typicons_dark/nw_tb-strike-md.svg +4 -0
  28. novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg +3 -1
  29. novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +4 -2
  30. novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +4 -2
  31. novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg +4 -2
  32. novelwriter/assets/icons/typicons_dark/typ_document-add-col.svg +8 -0
  33. novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
  34. novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
  35. novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
  36. novelwriter/assets/icons/typicons_light/icons.conf +9 -2
  37. novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
  38. novelwriter/assets/icons/typicons_light/nw_tb-bold-md.svg +4 -0
  39. novelwriter/assets/icons/typicons_light/nw_tb-bold.svg +3 -1
  40. novelwriter/assets/icons/typicons_light/nw_tb-italic-md.svg +4 -0
  41. novelwriter/assets/icons/typicons_light/nw_tb-italic.svg +3 -1
  42. novelwriter/assets/icons/typicons_light/nw_tb-strike-md.svg +4 -0
  43. novelwriter/assets/icons/typicons_light/nw_tb-strike.svg +3 -1
  44. novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +4 -2
  45. novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +4 -2
  46. novelwriter/assets/icons/typicons_light/nw_tb-underline.svg +4 -2
  47. novelwriter/assets/icons/typicons_light/typ_document-add-col.svg +8 -0
  48. novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
  49. novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
  50. novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
  51. novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
  52. novelwriter/assets/images/novelwriter-text-light.svg +4 -0
  53. novelwriter/assets/images/welcome-dark.jpg +0 -0
  54. novelwriter/assets/images/welcome-light.jpg +0 -0
  55. novelwriter/assets/manual.pdf +0 -0
  56. novelwriter/assets/sample.zip +0 -0
  57. novelwriter/assets/syntax/default_dark.conf +1 -0
  58. novelwriter/assets/syntax/default_light.conf +1 -0
  59. novelwriter/assets/syntax/grey_dark.conf +1 -0
  60. novelwriter/assets/syntax/grey_light.conf +1 -0
  61. novelwriter/assets/syntax/light_owl.conf +1 -0
  62. novelwriter/assets/syntax/night_owl.conf +1 -0
  63. novelwriter/assets/syntax/solarized_dark.conf +1 -0
  64. novelwriter/assets/syntax/solarized_light.conf +1 -0
  65. novelwriter/assets/syntax/tomorrow.conf +1 -0
  66. novelwriter/assets/syntax/tomorrow_night.conf +1 -0
  67. novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
  68. novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
  69. novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
  70. novelwriter/assets/text/credits_en.htm +4 -2
  71. novelwriter/assets/themes/default_dark.conf +2 -2
  72. novelwriter/assets/themes/default_light.conf +2 -2
  73. novelwriter/common.py +64 -66
  74. novelwriter/config.py +39 -44
  75. novelwriter/constants.py +39 -17
  76. novelwriter/core/buildsettings.py +8 -8
  77. novelwriter/core/coretools.py +194 -155
  78. novelwriter/core/docbuild.py +7 -4
  79. novelwriter/core/document.py +7 -7
  80. novelwriter/core/index.py +90 -57
  81. novelwriter/core/item.py +23 -5
  82. novelwriter/core/options.py +11 -10
  83. novelwriter/core/project.py +72 -47
  84. novelwriter/core/projectdata.py +3 -16
  85. novelwriter/core/projectxml.py +14 -42
  86. novelwriter/core/sessions.py +4 -3
  87. novelwriter/core/spellcheck.py +6 -4
  88. novelwriter/core/status.py +5 -4
  89. novelwriter/core/storage.py +179 -141
  90. novelwriter/core/tohtml.py +6 -4
  91. novelwriter/core/tokenizer.py +74 -46
  92. novelwriter/core/tomd.py +2 -2
  93. novelwriter/core/toodt.py +41 -31
  94. novelwriter/core/tree.py +5 -4
  95. novelwriter/dialogs/about.py +88 -179
  96. novelwriter/dialogs/docmerge.py +30 -20
  97. novelwriter/dialogs/docsplit.py +33 -22
  98. novelwriter/dialogs/editlabel.py +20 -8
  99. novelwriter/dialogs/preferences.py +562 -725
  100. novelwriter/dialogs/{projsettings.py → projectsettings.py} +301 -270
  101. novelwriter/dialogs/quotes.py +47 -36
  102. novelwriter/dialogs/wordlist.py +128 -59
  103. novelwriter/enum.py +25 -22
  104. novelwriter/error.py +2 -2
  105. novelwriter/extensions/circularprogress.py +12 -12
  106. novelwriter/extensions/configlayout.py +185 -146
  107. novelwriter/extensions/{wheeleventfilter.py → eventfilters.py} +15 -5
  108. novelwriter/extensions/modified.py +81 -0
  109. novelwriter/extensions/novelselector.py +27 -13
  110. novelwriter/extensions/pagedsidebar.py +15 -20
  111. novelwriter/extensions/simpleprogress.py +8 -9
  112. novelwriter/extensions/statusled.py +9 -9
  113. novelwriter/extensions/switch.py +32 -64
  114. novelwriter/extensions/switchbox.py +2 -7
  115. novelwriter/extensions/versioninfo.py +153 -0
  116. novelwriter/gui/doceditor.py +250 -214
  117. novelwriter/gui/dochighlight.py +66 -94
  118. novelwriter/gui/docviewer.py +71 -98
  119. novelwriter/gui/docviewerpanel.py +140 -47
  120. novelwriter/gui/editordocument.py +3 -3
  121. novelwriter/gui/itemdetails.py +9 -9
  122. novelwriter/gui/mainmenu.py +47 -46
  123. novelwriter/gui/noveltree.py +53 -61
  124. novelwriter/gui/outline.py +100 -76
  125. novelwriter/gui/projtree.py +193 -67
  126. novelwriter/gui/sidebar.py +9 -8
  127. novelwriter/gui/statusbar.py +49 -7
  128. novelwriter/gui/theme.py +65 -74
  129. novelwriter/guimain.py +173 -330
  130. novelwriter/shared.py +68 -30
  131. novelwriter/tools/dictionaries.py +7 -8
  132. novelwriter/tools/lipsum.py +34 -28
  133. novelwriter/tools/manusbuild.py +3 -4
  134. novelwriter/tools/manuscript.py +25 -32
  135. novelwriter/tools/manussettings.py +194 -225
  136. novelwriter/tools/noveldetails.py +525 -0
  137. novelwriter/tools/welcome.py +802 -0
  138. novelwriter/tools/writingstats.py +26 -13
  139. novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg +0 -8
  140. novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg +0 -8
  141. novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg +0 -8
  142. novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg +0 -8
  143. novelwriter/assets/images/wizard-back.jpg +0 -0
  144. novelwriter/assets/text/gplv3_en.htm +0 -641
  145. novelwriter/assets/text/release_notes.htm +0 -17
  146. novelwriter/dialogs/projdetails.py +0 -525
  147. novelwriter/dialogs/projload.py +0 -298
  148. novelwriter/dialogs/updates.py +0 -182
  149. novelwriter/extensions/pageddialog.py +0 -130
  150. novelwriter/tools/projwizard.py +0 -478
  151. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/LICENSE.md +0 -0
  152. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/entry_points.txt +0 -0
  153. {novelWriter-2.2rc1.dist-info → novelWriter-2.3b1.dist-info}/top_level.txt +0 -0
@@ -7,7 +7,7 @@ Created: 2023-02-14 [2.1b1] BuildSettings
7
7
  Created: 2023-05-22 [2.1b1] BuildCollection
8
8
 
9
9
  This file is a part of novelWriter
10
- Copyright 2018–2023, Veronica Berglyd Olsen
10
+ Copyright 2018–2024, Veronica Berglyd Olsen
11
11
 
12
12
  This program is free software: you can redistribute it and/or modify
13
13
  it under the terms of the GNU General Public License as published by
@@ -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
@@ -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,55 +310,67 @@ 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")
368
+ projName = data.get("name", lblNewProject)
369
+ projAuthor = data.get("author", "")
370
+ projLang = data.get("language", "en_GB")
357
371
 
358
372
  project.data.setUuid(None)
359
373
  project.data.setName(projName)
360
- project.data.setTitle(projTitle)
361
374
  project.data.setAuthor(projAuthor)
362
375
  project.data.setLanguage(projLang)
363
376
  project.setDefaultStatusImport()
@@ -366,7 +379,7 @@ class ProjectBuilder:
366
379
  # Add Root Folders
367
380
  hNovelRoot = project.newRoot(nwItemClass.NOVEL)
368
381
  hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
369
- novelTitle = project.data.title if project.data.title else project.data.name
382
+ novelTitle = project.data.name
370
383
 
371
384
  titlePage = f"#! {novelTitle}\n\n"
372
385
  if project.data.author:
@@ -375,116 +388,142 @@ class ProjectBuilder:
375
388
  aDoc = project.storage.getDocument(hTitlePage)
376
389
  aDoc.writeDocument(titlePage)
377
390
 
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()
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()
462
454
 
463
455
  return True
464
456
 
465
- ##
466
- # Internal Functions
467
- ##
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
468
466
 
469
- 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:
470
518
  """Make a copy of the sample project by extracting the
471
519
  sample.zip file to the new path.
472
520
  """
473
- projPath = data.get("projPath", None)
474
- if projPath is None:
475
- logger.error("No project path set for the example project")
476
- return False
477
-
478
- pkgSample = CONFIG.assetPath("sample.zip")
479
- if pkgSample.is_file():
521
+ if (sample := CONFIG.assetPath("sample.zip")).is_file():
480
522
  try:
481
- shutil.unpack_archive(pkgSample, projPath)
523
+ shutil.unpack_archive(sample, path)
482
524
  except Exception as exc:
483
- SHARED.error(self.tr(
484
- "Failed to create a new example project."
485
- ), exc=exc)
525
+ SHARED.error(self.tr("Failed to create a new example project."), exc=exc)
486
526
  return False
487
-
488
527
  else:
489
528
  SHARED.error(self.tr(
490
529
  "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."""