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.
- {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/METADATA +1 -1
- {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/RECORD +116 -101
- novelWriter-2.3.dist-info/entry_points.txt +2 -0
- novelwriter/__init__.py +4 -4
- novelwriter/assets/i18n/nw_de_DE.qm +0 -0
- novelwriter/assets/i18n/nw_en_US.qm +0 -0
- novelwriter/assets/i18n/nw_es_419.qm +0 -0
- novelwriter/assets/i18n/nw_it_IT.qm +0 -0
- novelwriter/assets/i18n/nw_ja_JP.qm +0 -0
- novelwriter/assets/i18n/nw_nb_NO.qm +0 -0
- novelwriter/assets/i18n/nw_nl_NL.qm +0 -0
- novelwriter/assets/i18n/project_nl_NL.json +11 -0
- novelwriter/assets/i18n/project_pt_BR.json +11 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
- novelwriter/assets/icons/typicons_dark/mixed_document-new.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
- novelwriter/assets/icons/typicons_dark/typ_document-add.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_document.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_th-dot-more.svg +4 -0
- novelwriter/assets/icons/typicons_dark/typ_th-list.svg +9 -0
- novelwriter/assets/icons/typicons_light/icons.conf +8 -0
- novelwriter/assets/icons/typicons_light/mixed_document-new.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
- novelwriter/assets/icons/typicons_light/typ_document-add.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_document.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_th-dot-more.svg +4 -0
- novelwriter/assets/icons/typicons_light/typ_th-list.svg +9 -0
- novelwriter/assets/images/novelwriter-text-dark.svg +4 -0
- novelwriter/assets/images/novelwriter-text-light.svg +4 -0
- novelwriter/assets/images/welcome-dark.jpg +0 -0
- novelwriter/assets/images/welcome-light.jpg +0 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/assets/syntax/cyberpunk_night.conf +26 -0
- novelwriter/assets/syntax/default_dark.conf +1 -0
- novelwriter/assets/syntax/default_light.conf +1 -0
- novelwriter/assets/syntax/grey_dark.conf +1 -0
- novelwriter/assets/syntax/grey_light.conf +1 -0
- novelwriter/assets/syntax/light_owl.conf +1 -0
- novelwriter/assets/syntax/night_owl.conf +1 -0
- novelwriter/assets/syntax/solarized_dark.conf +1 -0
- novelwriter/assets/syntax/solarized_light.conf +1 -0
- novelwriter/assets/syntax/tango.conf +23 -0
- novelwriter/assets/syntax/tomorrow.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_blue.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_bright.conf +1 -0
- novelwriter/assets/syntax/tomorrow_night_eighties.conf +1 -0
- novelwriter/assets/text/credits_en.htm +4 -2
- novelwriter/assets/themes/cyberpunk_night.conf +29 -0
- novelwriter/assets/themes/default_dark.conf +2 -2
- novelwriter/assets/themes/default_light.conf +2 -2
- novelwriter/common.py +48 -37
- novelwriter/config.py +36 -41
- novelwriter/constants.py +38 -16
- novelwriter/core/buildsettings.py +7 -7
- novelwriter/core/coretools.py +196 -156
- novelwriter/core/docbuild.py +6 -3
- novelwriter/core/document.py +6 -6
- novelwriter/core/index.py +89 -56
- novelwriter/core/item.py +21 -3
- novelwriter/core/options.py +8 -7
- novelwriter/core/project.py +70 -44
- novelwriter/core/projectdata.py +1 -14
- novelwriter/core/projectxml.py +13 -41
- novelwriter/core/sessions.py +2 -1
- novelwriter/core/spellcheck.py +2 -1
- novelwriter/core/status.py +2 -1
- novelwriter/core/storage.py +182 -140
- novelwriter/core/tohtml.py +4 -2
- novelwriter/core/tokenizer.py +109 -82
- novelwriter/core/toodt.py +40 -30
- novelwriter/core/tree.py +3 -2
- novelwriter/dialogs/about.py +70 -160
- novelwriter/dialogs/docmerge.py +6 -5
- novelwriter/dialogs/docsplit.py +6 -6
- novelwriter/dialogs/editlabel.py +1 -1
- novelwriter/dialogs/preferences.py +553 -703
- novelwriter/dialogs/{projsettings.py → projectsettings.py} +288 -262
- novelwriter/dialogs/quotes.py +27 -23
- novelwriter/dialogs/wordlist.py +96 -40
- novelwriter/enum.py +20 -18
- novelwriter/error.py +1 -1
- novelwriter/extensions/circularprogress.py +11 -11
- novelwriter/extensions/configlayout.py +185 -134
- novelwriter/extensions/modified.py +81 -0
- novelwriter/extensions/novelselector.py +26 -12
- novelwriter/extensions/pagedsidebar.py +14 -16
- novelwriter/extensions/simpleprogress.py +5 -5
- novelwriter/extensions/statusled.py +8 -8
- novelwriter/extensions/switch.py +31 -63
- novelwriter/extensions/switchbox.py +1 -1
- novelwriter/extensions/versioninfo.py +153 -0
- novelwriter/gui/doceditor.py +178 -150
- novelwriter/gui/dochighlight.py +63 -92
- novelwriter/gui/docviewer.py +49 -51
- novelwriter/gui/docviewerpanel.py +72 -24
- novelwriter/gui/itemdetails.py +7 -7
- novelwriter/gui/mainmenu.py +14 -19
- novelwriter/gui/noveltree.py +9 -8
- novelwriter/gui/outline.py +98 -75
- novelwriter/gui/projtree.py +241 -106
- novelwriter/gui/sidebar.py +3 -4
- novelwriter/gui/statusbar.py +3 -4
- novelwriter/gui/theme.py +69 -70
- novelwriter/guimain.py +51 -156
- novelwriter/shared.py +15 -1
- novelwriter/tools/dictionaries.py +5 -6
- novelwriter/tools/manuscript.py +6 -6
- novelwriter/tools/manussettings.py +192 -221
- novelwriter/tools/noveldetails.py +525 -0
- novelwriter/tools/welcome.py +819 -0
- novelwriter/tools/writingstats.py +9 -9
- novelWriter-2.2.1.dist-info/entry_points.txt +0 -5
- novelwriter/assets/images/wizard-back.jpg +0 -0
- novelwriter/assets/text/gplv3_en.htm +0 -641
- novelwriter/assets/text/release_notes.htm +0 -60
- novelwriter/dialogs/projdetails.py +0 -518
- novelwriter/dialogs/projload.py +0 -294
- novelwriter/dialogs/updates.py +0 -172
- novelwriter/extensions/pageddialog.py +0 -130
- novelwriter/tools/projwizard.py +0 -478
- {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.2.1.dist-info → novelWriter-2.3.dist-info}/WHEEL +0 -0
- {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
|
47
|
-
URL_DOCS
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
302
|
-
TITLE
|
303
|
-
CH_NUM
|
304
|
-
CH_WORD
|
305
|
-
CH_ROMU
|
306
|
-
CH_ROML
|
307
|
-
SC_NUM
|
308
|
-
SC_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
|
-
|
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
|
novelwriter/core/coretools.py
CHANGED
@@ -29,16 +29,19 @@ from __future__ import annotations
|
|
29
29
|
import shutil
|
30
30
|
import logging
|
31
31
|
|
32
|
-
from
|
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
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
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
|
-
|
299
|
-
|
300
|
-
|
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.
|
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
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
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
|
-
|
333
|
-
|
334
|
-
|
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
|
-
|
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(
|
361
|
-
project.data.
|
362
|
-
project.data.
|
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.
|
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
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
475
|
-
|
476
|
-
|
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
|
-
|
480
|
-
if pkgSample.is_file():
|
523
|
+
if (sample := CONFIG.assetPath("sample.zip")).is_file():
|
481
524
|
try:
|
482
|
-
shutil.unpack_archive(
|
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. "
|
novelwriter/core/docbuild.py
CHANGED
@@ -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
|
252
|
-
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))
|
novelwriter/core/document.py
CHANGED
@@ -52,7 +52,7 @@ class NWDocument:
|
|
52
52
|
|
53
53
|
def __init__(self, project: NWProject, tHandle: str | None) -> None:
|
54
54
|
|
55
|
-
self._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
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
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
|
292
|
+
return name, parent, itemClass, itemLayout
|
293
293
|
|
294
294
|
def getError(self) -> str:
|
295
295
|
"""Return the last recorded exception."""
|