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.
- {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/METADATA +1 -1
- {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/RECORD +102 -92
- novelwriter/__init__.py +4 -4
- novelwriter/assets/icons/typicons_dark/icons.conf +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_import.svg +5 -0
- novelwriter/assets/icons/typicons_dark/typ_document-add-col.svg +8 -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_light/icons.conf +6 -0
- novelwriter/assets/icons/typicons_light/mixed_import.svg +5 -0
- novelwriter/assets/icons/typicons_light/typ_document-add-col.svg +8 -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/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/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/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/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 +192 -154
- 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 +69 -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 +178 -140
- novelwriter/core/tohtml.py +4 -2
- novelwriter/core/tokenizer.py +73 -45
- 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 -18
- novelwriter/gui/noveltree.py +9 -8
- novelwriter/gui/outline.py +98 -75
- novelwriter/gui/projtree.py +188 -61
- novelwriter/gui/sidebar.py +3 -4
- novelwriter/gui/statusbar.py +3 -4
- novelwriter/gui/theme.py +60 -68
- novelwriter/guimain.py +49 -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 +802 -0
- novelwriter/tools/writingstats.py +9 -9
- 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.3b1.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/WHEEL +0 -0
- {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.2.1.dist-info → novelWriter-2.3b1.dist-info}/top_level.txt +0 -0
novelwriter/core/index.py
CHANGED
@@ -32,8 +32,9 @@ import json
|
|
32
32
|
import logging
|
33
33
|
|
34
34
|
from time import time
|
35
|
-
from typing import TYPE_CHECKING
|
35
|
+
from typing import TYPE_CHECKING
|
36
36
|
from pathlib import Path
|
37
|
+
from collections.abc import ItemsView, Iterable, Iterator
|
37
38
|
|
38
39
|
from novelwriter import SHARED
|
39
40
|
from novelwriter.enum import nwComment, nwItemClass, nwItemType, nwItemLayout
|
@@ -122,8 +123,8 @@ class NWIndex:
|
|
122
123
|
for nwItem in self._project.tree:
|
123
124
|
if nwItem.isFileType():
|
124
125
|
tHandle = nwItem.itemHandle
|
125
|
-
|
126
|
-
self.scanText(tHandle,
|
126
|
+
doc = self._project.storage.getDocument(tHandle)
|
127
|
+
self.scanText(tHandle, doc.readDocument() or "", blockSignal=True)
|
127
128
|
self._indexBroken = False
|
128
129
|
SHARED.indexSignalProxy({"event": "buildIndex"})
|
129
130
|
return
|
@@ -148,8 +149,8 @@ class NWIndex:
|
|
148
149
|
"""
|
149
150
|
if tHandle and self._project.tree.checkType(tHandle, nwItemType.FILE):
|
150
151
|
logger.debug("Re-indexing item '%s'", tHandle)
|
151
|
-
|
152
|
-
self.scanText(tHandle,
|
152
|
+
doc = self._project.storage.getDocument(tHandle)
|
153
|
+
self.scanText(tHandle, doc.readDocument() or "")
|
153
154
|
return True
|
154
155
|
return False
|
155
156
|
|
@@ -420,10 +421,10 @@ class NWIndex:
|
|
420
421
|
return
|
421
422
|
|
422
423
|
if tBits[0] == nwKeyWords.TAG_KEY:
|
423
|
-
|
424
|
-
self._tagsIndex.add(
|
425
|
-
self._itemIndex.setHeadingTag(tHandle, sTitle,
|
426
|
-
tags[
|
424
|
+
tagKey, displayName = self.parseValue(tBits[1])
|
425
|
+
self._tagsIndex.add(tagKey, displayName, tHandle, sTitle, itemClass.name)
|
426
|
+
self._itemIndex.setHeadingTag(tHandle, sTitle, tagKey)
|
427
|
+
tags[tagKey.lower()] = True
|
427
428
|
else:
|
428
429
|
self._itemIndex.addHeadingRef(tHandle, sTitle, tBits[1:], tBits[0])
|
429
430
|
|
@@ -471,10 +472,8 @@ class NWIndex:
|
|
471
472
|
|
472
473
|
return True, tBits, tPos
|
473
474
|
|
474
|
-
def checkThese(self, tBits: list[str],
|
475
|
-
"""Check
|
476
|
-
tags. This is needed for syntax highlighting.
|
477
|
-
"""
|
475
|
+
def checkThese(self, tBits: list[str], tHandle: str) -> list[bool]:
|
476
|
+
"""Check tags against the index to see if they are valid."""
|
478
477
|
nBits = len(tBits)
|
479
478
|
isGood = [False]*nBits
|
480
479
|
if nBits == 0:
|
@@ -487,20 +486,27 @@ class NWIndex:
|
|
487
486
|
|
488
487
|
# For a tag, only the first value is accepted, the rest are ignored
|
489
488
|
if tBits[0] == nwKeyWords.TAG_KEY and nBits > 1:
|
490
|
-
|
491
|
-
|
489
|
+
check, _ = self.parseValue(tBits[1])
|
490
|
+
if check in self._tagsIndex:
|
491
|
+
isGood[1] = self._tagsIndex.tagHandle(check) == tHandle
|
492
492
|
else:
|
493
493
|
isGood[1] = True
|
494
494
|
return isGood
|
495
495
|
|
496
496
|
# If we're still here, we check that the references exist
|
497
|
+
# Class references cannot have the | symbol in them
|
497
498
|
refKey = nwKeyWords.KEY_CLASS[tBits[0]].name
|
498
499
|
for n in range(1, nBits):
|
499
|
-
if tBits[n] in self._tagsIndex:
|
500
|
-
isGood[n] = self._tagsIndex.tagClass(
|
500
|
+
if (aBit := tBits[n]) in self._tagsIndex:
|
501
|
+
isGood[n] = self._tagsIndex.tagClass(aBit) == refKey and "|" not in aBit
|
501
502
|
|
502
503
|
return isGood
|
503
504
|
|
505
|
+
def parseValue(self, text: str) -> tuple[str, str]:
|
506
|
+
"""Parse a single value into a name and display part."""
|
507
|
+
name, _, display = text.partition("|")
|
508
|
+
return name.rstrip(), display.lstrip()
|
509
|
+
|
504
510
|
##
|
505
511
|
# Extract Data
|
506
512
|
##
|
@@ -517,28 +523,31 @@ class NWIndex:
|
|
517
523
|
return None
|
518
524
|
|
519
525
|
def novelStructure(
|
520
|
-
self, rootHandle: str | None = None,
|
526
|
+
self, rootHandle: str | None = None, activeOnly: bool = True
|
521
527
|
) -> Iterator[tuple[str, str, str, IndexHeading]]:
|
522
528
|
"""Iterate over all titles in the novel, in the correct order as
|
523
529
|
they appear in the tree view and in the respective document
|
524
530
|
files, but skipping all note files.
|
525
531
|
"""
|
526
|
-
structure = self._itemIndex.iterNovelStructure(rHandle=rootHandle,
|
532
|
+
structure = self._itemIndex.iterNovelStructure(rHandle=rootHandle, activeOnly=activeOnly)
|
527
533
|
for tHandle, sTitle, hItem in structure:
|
528
534
|
yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem
|
529
535
|
return
|
530
536
|
|
531
|
-
def getNovelWordCount(self,
|
532
|
-
"""Count the number of words in
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
return wCount
|
537
|
+
def getNovelWordCount(self, rootHandle: str | None = None, activeOnly: bool = True) -> int:
|
538
|
+
"""Count the number of words in one or all novel roots."""
|
539
|
+
return sum(hItem.wordCount for _, _, hItem in self._itemIndex.iterNovelStructure(
|
540
|
+
rHandle=rootHandle, activeOnly=activeOnly
|
541
|
+
))
|
537
542
|
|
538
|
-
def getNovelTitleCounts(
|
539
|
-
|
543
|
+
def getNovelTitleCounts(
|
544
|
+
self, rootHandle: str | None = None, activeOnly: bool = True
|
545
|
+
) -> list[int]:
|
546
|
+
"""Count the number of titles in one or all novel roots."""
|
540
547
|
hCount = [0, 0, 0, 0, 0]
|
541
|
-
for _, _, hItem in self._itemIndex.iterNovelStructure(
|
548
|
+
for _, _, hItem in self._itemIndex.iterNovelStructure(
|
549
|
+
rHandle=rootHandle, activeOnly=activeOnly
|
550
|
+
):
|
542
551
|
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
|
543
552
|
hCount[iLevel] += 1
|
544
553
|
return hCount
|
@@ -551,14 +560,14 @@ class NWIndex:
|
|
551
560
|
return 0
|
552
561
|
|
553
562
|
def getTableOfContents(
|
554
|
-
self, rHandle: str | None, maxDepth: int,
|
563
|
+
self, rHandle: str | None, maxDepth: int, activeOnly: bool = True
|
555
564
|
) -> list[tuple[str, int, str, int]]:
|
556
565
|
"""Generate a table of contents up to a maximum depth."""
|
557
566
|
tOrder = []
|
558
567
|
tData = {}
|
559
568
|
pKey = None
|
560
569
|
for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(
|
561
|
-
rHandle=rHandle,
|
570
|
+
rHandle=rHandle, activeOnly=activeOnly
|
562
571
|
):
|
563
572
|
tKey = f"{tHandle}:{sTitle}"
|
564
573
|
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
|
@@ -612,9 +621,18 @@ class NWIndex:
|
|
612
621
|
for refType in refTypes:
|
613
622
|
if refType in tRefs:
|
614
623
|
tRefs[refType].append(self._tagsIndex.tagName(aTag))
|
615
|
-
|
616
624
|
return tRefs
|
617
625
|
|
626
|
+
def getReferenceForHeader(self, tHandle: str, nHead: int, keyClass: str) -> list[str]:
|
627
|
+
"""Get the display names for a tags class for insertion into a
|
628
|
+
heading by one of the build classes.
|
629
|
+
"""
|
630
|
+
if iItem := self._itemIndex[tHandle]:
|
631
|
+
if hItem := iItem[f"T{nHead:04d}"]:
|
632
|
+
hRefs = [k for k, v in hItem.references.items() if keyClass in v]
|
633
|
+
return [self._tagsIndex.tagDisplay(k) for k in hRefs]
|
634
|
+
return []
|
635
|
+
|
618
636
|
def getBackReferenceList(self, tHandle: str) -> dict[str, tuple[str, IndexHeading]]:
|
619
637
|
"""Build a dict of files referring back to our file."""
|
620
638
|
if tHandle is None or tHandle not in self._itemIndex:
|
@@ -646,12 +664,15 @@ class NWIndex:
|
|
646
664
|
"""Return all tags based on itemClass."""
|
647
665
|
return self._tagsIndex.filterTagNames(itemClass.name)
|
648
666
|
|
649
|
-
def getTagsData(
|
667
|
+
def getTagsData(
|
668
|
+
self, activeOnly: bool = True
|
669
|
+
) -> Iterator[tuple[str, str, str, IndexItem | None, IndexHeading | None]]:
|
650
670
|
"""Return all known tags."""
|
651
671
|
for tag, data in self._tagsIndex.items():
|
652
672
|
iItem = self._itemIndex[data.get("handle")]
|
653
673
|
hItem = None if iItem is None else iItem[data.get("heading")]
|
654
|
-
|
674
|
+
if not activeOnly or (iItem and iItem.item.isActive):
|
675
|
+
yield tag, data.get("name", ""), data.get("class", ""), iItem, hItem
|
655
676
|
return
|
656
677
|
|
657
678
|
def getSingleTag(self, tagKey: str) -> tuple[str, str, IndexItem | None, IndexHeading | None]:
|
@@ -709,17 +730,26 @@ class TagsIndex:
|
|
709
730
|
"""Return a dictionary view of all tags."""
|
710
731
|
return self._tags.items()
|
711
732
|
|
712
|
-
def add(self, tagKey: str,
|
733
|
+
def add(self, tagKey: str, displayName: str, tHandle: str,
|
734
|
+
sTitle: str, className: str) -> None:
|
713
735
|
"""Add a key to the index and set all values."""
|
714
736
|
self._tags[tagKey.lower()] = {
|
715
|
-
"name": tagKey,
|
737
|
+
"name": tagKey,
|
738
|
+
"display": displayName or tagKey,
|
739
|
+
"handle": tHandle,
|
740
|
+
"heading": sTitle,
|
741
|
+
"class": className,
|
716
742
|
}
|
717
743
|
return
|
718
744
|
|
719
745
|
def tagName(self, tagKey: str) -> str:
|
720
|
-
"""Get the
|
746
|
+
"""Get the name of a given tag."""
|
721
747
|
return self._tags.get(tagKey.lower(), {}).get("name", "")
|
722
748
|
|
749
|
+
def tagDisplay(self, tagKey: str) -> str:
|
750
|
+
"""Get the display name of a given tag."""
|
751
|
+
return self._tags.get(tagKey.lower(), {}).get("display", "")
|
752
|
+
|
723
753
|
def tagHandle(self, tagKey: str) -> str | None:
|
724
754
|
"""Get the handle of a given tag."""
|
725
755
|
return self._tags.get(tagKey.lower(), {}).get("handle", None)
|
@@ -754,27 +784,30 @@ class TagsIndex:
|
|
754
784
|
if not isinstance(data, dict):
|
755
785
|
raise ValueError("tagsIndex is not a dict")
|
756
786
|
|
757
|
-
for
|
758
|
-
if not isinstance(
|
759
|
-
raise ValueError("tagsIndex keys must be a
|
760
|
-
if
|
761
|
-
raise
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
787
|
+
for key, entry in data.items():
|
788
|
+
if not isinstance(key, str):
|
789
|
+
raise ValueError("tagsIndex keys must be a string")
|
790
|
+
if not isinstance(entry, dict):
|
791
|
+
raise ValueError("tagsIndex entry is not a dict")
|
792
|
+
|
793
|
+
name = entry.get("name")
|
794
|
+
display = entry.get("display")
|
795
|
+
handle = entry.get("handle")
|
796
|
+
heading = entry.get("heading")
|
797
|
+
className = entry.get("class")
|
798
|
+
|
799
|
+
if not isinstance(name, str):
|
800
|
+
raise ValueError("tagsIndex name is not a string")
|
801
|
+
if not isinstance(display, str):
|
802
|
+
raise ValueError("tagsIndex display is not a string")
|
803
|
+
if not isHandle(handle):
|
771
804
|
raise ValueError("tagsIndex handle must be a handle")
|
772
|
-
if not isTitleTag(
|
805
|
+
if not isTitleTag(heading):
|
773
806
|
raise ValueError("tagsIndex heading must be a title tag")
|
774
|
-
if not isItemClass(
|
807
|
+
if not isItemClass(className):
|
775
808
|
raise ValueError("tagsIndex handle must be an nwItemClass")
|
776
809
|
|
777
|
-
|
810
|
+
self.add(name, display, handle, heading, className)
|
778
811
|
|
779
812
|
return
|
780
813
|
|
@@ -848,7 +881,7 @@ class ItemIndex:
|
|
848
881
|
return
|
849
882
|
|
850
883
|
def iterNovelStructure(
|
851
|
-
self, rHandle: str | None = None,
|
884
|
+
self, rHandle: str | None = None, activeOnly: bool = False
|
852
885
|
) -> Iterable[tuple[str, str, IndexHeading]]:
|
853
886
|
"""Iterate over all items and headers in the novel structure for
|
854
887
|
a given root handle, or for all if root handle is None.
|
@@ -856,7 +889,7 @@ class ItemIndex:
|
|
856
889
|
for tItem in self._project.tree:
|
857
890
|
if tItem.isNoteLayout():
|
858
891
|
continue
|
859
|
-
if
|
892
|
+
if activeOnly and not tItem.isActive:
|
860
893
|
continue
|
861
894
|
|
862
895
|
tHandle = tItem.itemHandle
|
@@ -1159,7 +1192,7 @@ class IndexHeading:
|
|
1159
1192
|
return self._tag
|
1160
1193
|
|
1161
1194
|
@property
|
1162
|
-
def references(self) -> dict:
|
1195
|
+
def references(self) -> dict[str, set[str]]:
|
1163
1196
|
return self._refs
|
1164
1197
|
|
1165
1198
|
##
|
novelwriter/core/item.py
CHANGED
@@ -334,15 +334,33 @@ class NWItem:
|
|
334
334
|
|
335
335
|
def isNovelLike(self) -> bool:
|
336
336
|
"""Check if the item is of a novel-like class."""
|
337
|
-
return self._class in (
|
337
|
+
return self._class in (
|
338
|
+
nwItemClass.NOVEL,
|
339
|
+
nwItemClass.ARCHIVE,
|
340
|
+
nwItemClass.TEMPLATE,
|
341
|
+
)
|
342
|
+
|
343
|
+
def isTemplateFile(self) -> bool:
|
344
|
+
"""Check if the item is a template file."""
|
345
|
+
return self._type == nwItemType.FILE and self._class == nwItemClass.TEMPLATE
|
338
346
|
|
339
347
|
def documentAllowed(self) -> bool:
|
340
348
|
"""Check if the item is allowed to be of document layout."""
|
341
|
-
return self._class in (
|
349
|
+
return self._class in (
|
350
|
+
nwItemClass.NOVEL,
|
351
|
+
nwItemClass.ARCHIVE,
|
352
|
+
nwItemClass.TEMPLATE,
|
353
|
+
nwItemClass.TRASH,
|
354
|
+
)
|
342
355
|
|
343
356
|
def isInactiveClass(self) -> bool:
|
344
357
|
"""Check if the item is in an inactive class."""
|
345
|
-
return self._class in (
|
358
|
+
return self._class in (
|
359
|
+
nwItemClass.NO_CLASS,
|
360
|
+
nwItemClass.ARCHIVE,
|
361
|
+
nwItemClass.TEMPLATE,
|
362
|
+
nwItemClass.TRASH,
|
363
|
+
)
|
346
364
|
|
347
365
|
def isRootType(self) -> bool:
|
348
366
|
"""Check if item is a root item."""
|
novelwriter/core/options.py
CHANGED
@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
|
|
42
42
|
|
43
43
|
NWEnum = TypeVar("NWEnum", bound=Enum)
|
44
44
|
|
45
|
-
VALID_MAP = {
|
45
|
+
VALID_MAP: dict[str, set[str]] = {
|
46
46
|
"GuiWritingStats": {
|
47
47
|
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
|
48
48
|
"widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes",
|
@@ -53,10 +53,6 @@ VALID_MAP = {
|
|
53
53
|
"GuiProjectSettings": {
|
54
54
|
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW",
|
55
55
|
},
|
56
|
-
"GuiProjectDetails": {
|
57
|
-
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
|
58
|
-
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
|
59
|
-
},
|
60
56
|
"GuiWordList": {"winWidth", "winHeight"},
|
61
57
|
"GuiNovelView": {"lastCol", "lastColSize"},
|
62
58
|
"GuiBuildSettings": {
|
@@ -70,8 +66,13 @@ VALID_MAP = {
|
|
70
66
|
"winWidth", "winHeight", "fmtWidth", "sumWidth",
|
71
67
|
},
|
72
68
|
"GuiDocViewerPanel": {
|
73
|
-
"colWidths",
|
74
|
-
}
|
69
|
+
"colWidths", "hideInactive",
|
70
|
+
},
|
71
|
+
"GuiNovelDetails": {
|
72
|
+
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
|
73
|
+
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
|
74
|
+
"novelRoot",
|
75
|
+
},
|
75
76
|
}
|
76
77
|
|
77
78
|
|
novelwriter/core/project.py
CHANGED
@@ -26,10 +26,12 @@ from __future__ import annotations
|
|
26
26
|
import json
|
27
27
|
import logging
|
28
28
|
|
29
|
+
from enum import Enum
|
29
30
|
from time import time
|
30
|
-
from typing import TYPE_CHECKING
|
31
|
+
from typing import TYPE_CHECKING
|
31
32
|
from pathlib import Path
|
32
33
|
from functools import partial
|
34
|
+
from collections.abc import Iterator
|
33
35
|
|
34
36
|
from PyQt5.QtCore import QCoreApplication
|
35
37
|
|
@@ -40,7 +42,7 @@ from novelwriter.constants import trConst, nwLabels
|
|
40
42
|
from novelwriter.core.tree import NWTree
|
41
43
|
from novelwriter.core.index import NWIndex
|
42
44
|
from novelwriter.core.options import OptionState
|
43
|
-
from novelwriter.core.storage import NWStorage
|
45
|
+
from novelwriter.core.storage import NWStorage, NWStorageOpen
|
44
46
|
from novelwriter.core.sessions import NWSessionLog
|
45
47
|
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
|
46
48
|
from novelwriter.core.projectdata import NWProjectData
|
@@ -55,6 +57,16 @@ if TYPE_CHECKING: # pragma: no cover
|
|
55
57
|
logger = logging.getLogger(__name__)
|
56
58
|
|
57
59
|
|
60
|
+
class NWProjectState(Enum):
|
61
|
+
|
62
|
+
UNKNOWN = 0
|
63
|
+
LOCKED = 1
|
64
|
+
RECOVERY = 2
|
65
|
+
READY = 3
|
66
|
+
|
67
|
+
# END Enum NWProjectState
|
68
|
+
|
69
|
+
|
58
70
|
class NWProject:
|
59
71
|
|
60
72
|
def __init__(self) -> None:
|
@@ -69,9 +81,9 @@ class NWProject:
|
|
69
81
|
|
70
82
|
# Project Status
|
71
83
|
self._langData = {} # Localisation data
|
72
|
-
self._lockedBy = None # Data on which computer has the project open
|
73
84
|
self._changed = False # The project has unsaved changes
|
74
85
|
self._valid = False # The project was successfully loaded
|
86
|
+
self._state = NWProjectState.UNKNOWN
|
75
87
|
|
76
88
|
# Internal Mapping
|
77
89
|
self.tr = partial(QCoreApplication.translate, "NWProject")
|
@@ -125,12 +137,15 @@ class NWProject:
|
|
125
137
|
"""Return True if a project is loaded."""
|
126
138
|
return self._valid
|
127
139
|
|
140
|
+
@property
|
141
|
+
def state(self) -> NWProjectState:
|
142
|
+
"""Return the current project state."""
|
143
|
+
return self._state
|
144
|
+
|
128
145
|
@property
|
129
146
|
def lockStatus(self) -> list | None:
|
130
147
|
"""Return the project lock information."""
|
131
|
-
|
132
|
-
return self._lockedBy
|
133
|
-
return None
|
148
|
+
return self._storage.lockStatus
|
134
149
|
|
135
150
|
@property
|
136
151
|
def currentEditTime(self) -> int:
|
@@ -161,24 +176,47 @@ class NWProject:
|
|
161
176
|
will not run if the file exists and is not empty.
|
162
177
|
"""
|
163
178
|
tItem = self._tree[tHandle]
|
164
|
-
if tItem
|
165
|
-
return False
|
166
|
-
if not tItem.isFileType():
|
179
|
+
if not (tItem and tItem.isFileType()):
|
167
180
|
return False
|
168
181
|
|
169
182
|
newDoc = self._storage.getDocument(tHandle)
|
170
183
|
if (newDoc.readDocument() or "").strip():
|
171
184
|
return False
|
172
185
|
|
173
|
-
|
174
|
-
|
186
|
+
indent = "#"*minmax(hLevel, 1, 4)
|
187
|
+
text = f"{indent} {tItem.itemName}\n\n{text}"
|
188
|
+
|
175
189
|
if tItem.isNovelLike() and isDocument:
|
176
190
|
tItem.setLayout(nwItemLayout.DOCUMENT)
|
177
191
|
else:
|
178
192
|
tItem.setLayout(nwItemLayout.NOTE)
|
179
193
|
|
180
|
-
newDoc.writeDocument(
|
181
|
-
self._index.scanText(tHandle,
|
194
|
+
newDoc.writeDocument(text)
|
195
|
+
self._index.scanText(tHandle, text)
|
196
|
+
|
197
|
+
return True
|
198
|
+
|
199
|
+
def copyFileContent(self, tHandle: str, sHandle: str) -> bool:
|
200
|
+
"""Copy content to a new document after it is created. This
|
201
|
+
will not run if the file exists and is not empty.
|
202
|
+
"""
|
203
|
+
tItem = self._tree[tHandle]
|
204
|
+
if not (tItem and tItem.isFileType()):
|
205
|
+
return False
|
206
|
+
|
207
|
+
sItem = self._tree[sHandle]
|
208
|
+
if not (sItem and sItem.isFileType()):
|
209
|
+
return False
|
210
|
+
|
211
|
+
newDoc = self._storage.getDocument(tHandle)
|
212
|
+
if (newDoc.readDocument() or "").strip():
|
213
|
+
return False
|
214
|
+
|
215
|
+
logger.debug("Populating '%s' with text from '%s'", tHandle, sHandle)
|
216
|
+
text = self._storage.getDocument(sHandle).readDocument() or ""
|
217
|
+
newDoc.writeDocument(text)
|
218
|
+
sItem.setLayout(tItem.itemLayout)
|
219
|
+
self._index.scanText(tHandle, text)
|
182
220
|
|
183
221
|
return True
|
184
222
|
|
@@ -219,29 +257,21 @@ class NWProject:
|
|
219
257
|
build the tree of project items.
|
220
258
|
"""
|
221
259
|
logger.info("Opening project: %s", projPath)
|
222
|
-
if not self._storage.openProjectInPlace(projPath):
|
223
|
-
SHARED.error(self.tr("Could not open project with path: {0}").format(projPath))
|
224
|
-
return False
|
225
|
-
|
226
|
-
# Project Lock
|
227
|
-
# ============
|
228
260
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
self.
|
239
|
-
|
240
|
-
else:
|
241
|
-
logger.debug("Project is not locked")
|
261
|
+
status = self._storage.initProjectStorage(projPath, clearLock)
|
262
|
+
if status != NWStorageOpen.READY:
|
263
|
+
if status == NWStorageOpen.UNKOWN:
|
264
|
+
SHARED.error(self.tr("Not a known project file format."))
|
265
|
+
elif status == NWStorageOpen.NOT_FOUND:
|
266
|
+
SHARED.error(self.tr("Project file not found."))
|
267
|
+
elif status == NWStorageOpen.LOCKED:
|
268
|
+
self._state = NWProjectState.LOCKED
|
269
|
+
elif status == NWStorageOpen.FAILED:
|
270
|
+
SHARED.error(self.tr("Failed to open project."), exc=self._storage.exc)
|
271
|
+
return False
|
242
272
|
|
243
|
-
#
|
244
|
-
#
|
273
|
+
# Read Project XML
|
274
|
+
# ================
|
245
275
|
|
246
276
|
xmlReader = self._storage.getXmlReader()
|
247
277
|
if not isinstance(xmlReader, ProjectXMLReader):
|
@@ -250,9 +280,7 @@ class NWProject:
|
|
250
280
|
self._data = NWProjectData(self)
|
251
281
|
projContent = []
|
252
282
|
xmlParsed = xmlReader.read(self._data, projContent)
|
253
|
-
|
254
283
|
appVersion = xmlReader.appVersion or self.tr("Unknown")
|
255
|
-
|
256
284
|
if not xmlParsed:
|
257
285
|
if xmlReader.state == XMLReadState.NOT_NWX_FILE:
|
258
286
|
SHARED.error(self.tr(
|
@@ -302,8 +330,7 @@ class NWProject:
|
|
302
330
|
self._loadProjectLocalisation()
|
303
331
|
|
304
332
|
# Update recent projects
|
305
|
-
storePath
|
306
|
-
if storePath:
|
333
|
+
if storePath := self._storage.storagePath:
|
307
334
|
CONFIG.recentProjects.update(
|
308
335
|
storePath, self._data.name, sum(self._data.initCounts), time()
|
309
336
|
)
|
@@ -323,9 +350,9 @@ class NWProject:
|
|
323
350
|
|
324
351
|
self.updateWordCounts()
|
325
352
|
self._session.startSession()
|
326
|
-
self._storage.writeLockFile()
|
327
353
|
self.setProjectChanged(False)
|
328
354
|
self._valid = True
|
355
|
+
self._state = NWProjectState.READY
|
329
356
|
|
330
357
|
SHARED.newStatusMessage(self.tr("Opened Project: {0}").format(self._data.name))
|
331
358
|
|
@@ -370,13 +397,11 @@ class NWProject:
|
|
370
397
|
self._storage.runPostSaveTasks(autoSave=autoSave)
|
371
398
|
|
372
399
|
# Update recent projects
|
373
|
-
|
374
|
-
if storePath:
|
400
|
+
if storagePath := self._storage.storagePath:
|
375
401
|
CONFIG.recentProjects.update(
|
376
|
-
|
402
|
+
storagePath, self._data.name, sum(self._data.currCounts), saveTime
|
377
403
|
)
|
378
404
|
|
379
|
-
self._storage.writeLockFile()
|
380
405
|
SHARED.newStatusMessage(self.tr("Saved Project: {0}").format(self._data.name))
|
381
406
|
self.setProjectChanged(False)
|
382
407
|
|
@@ -420,8 +445,8 @@ class NWProject:
|
|
420
445
|
timeStamp = formatTimeStamp(time(), fileSafe=True)
|
421
446
|
archName = baseDir / f"{cleanName} {timeStamp}.zip"
|
422
447
|
if self._storage.zipIt(archName, compression=2):
|
423
|
-
size = formatInt(getFileSize(archName))
|
424
448
|
if doNotify:
|
449
|
+
size = formatInt(getFileSize(archName))
|
425
450
|
SHARED.info(
|
426
451
|
self.tr("Created a backup of your project of size {0}B.").format(size),
|
427
452
|
info=self.tr("Path: {0}").format(str(backupPath))
|
novelwriter/core/projectdata.py
CHANGED
@@ -53,7 +53,6 @@ class NWProjectData:
|
|
53
53
|
# Project Meta
|
54
54
|
self._uuid = ""
|
55
55
|
self._name = ""
|
56
|
-
self._title = ""
|
57
56
|
self._author = ""
|
58
57
|
self._saveCount = 0
|
59
58
|
self._autoCount = 0
|
@@ -102,11 +101,6 @@ class NWProjectData:
|
|
102
101
|
"""Return the project name."""
|
103
102
|
return self._name
|
104
103
|
|
105
|
-
@property
|
106
|
-
def title(self) -> str:
|
107
|
-
"""Return the project title."""
|
108
|
-
return self._title
|
109
|
-
|
110
104
|
@property
|
111
105
|
def author(self) -> str:
|
112
106
|
"""Return the project author."""
|
@@ -228,16 +222,9 @@ class NWProjectData:
|
|
228
222
|
self._project.setProjectChanged(True)
|
229
223
|
return
|
230
224
|
|
231
|
-
def setTitle(self, value: str | None) -> None:
|
232
|
-
"""Set a new novel title."""
|
233
|
-
if value != self._title:
|
234
|
-
self._title = simplified(str(value or ""))
|
235
|
-
self._project.setProjectChanged(True)
|
236
|
-
return
|
237
|
-
|
238
225
|
def setAuthor(self, value: str | None) -> None:
|
239
226
|
"""Set the author value."""
|
240
|
-
if value != self.
|
227
|
+
if value != self._author:
|
241
228
|
self._author = simplified(str(value or ""))
|
242
229
|
self._project.setProjectChanged(True)
|
243
230
|
return
|