Merge pull request #538 from vkbo/novel_tab

Novel Tree Tab [v1.1]
This commit is contained in:
Veronica K. Berglyd Olsen
2021-01-03 17:23:25 +01:00
committed by GitHub
21 changed files with 1000 additions and 377 deletions
+11
View File
@@ -2,6 +2,17 @@
## Version 1.1 Dev (Alpha) ## Version 1.1 Dev (Alpha)
### Release Notes
### Detailed Changelog
**User Interface**
* Added a Novel tab under the project tree where the user can navigate the novel's layout of
chapters and scenes, similar to the Outline view, but next to the document editor. The Outline
view and Novel/Project trees now also behave more in cooperation. When files on one are selected
or moved, the other will follow and update. PR #537.
---- ----
## Version 1.0 [2021-01-03] ## Version 1.0 [2021-01-03]
+21 -8
View File
@@ -95,14 +95,15 @@ class Config:
self.lastNotes = "" # The latest release notes that have been shown self.lastNotes = "" # The latest release notes that have been shown
## Sizes ## Sizes
self.winGeometry = [1200, 650] self.winGeometry = [1200, 650]
self.treeColWidth = [200, 50, 30] self.treeColWidth = [200, 50, 30]
self.projColWidth = [200, 60, 140] self.novelColWidth = [200, 50]
self.mainPanePos = [300, 800] self.projColWidth = [200, 60, 140]
self.docPanePos = [400, 400] self.mainPanePos = [300, 800]
self.viewPanePos = [500, 150] self.docPanePos = [400, 400]
self.outlnPanePos = [500, 150] self.viewPanePos = [500, 150]
self.isFullScreen = False self.outlnPanePos = [500, 150]
self.isFullScreen = False
## Features ## Features
self.hideVScroll = False # Hide vertical scroll bars on main widgets self.hideVScroll = False # Hide vertical scroll bars on main widgets
@@ -395,6 +396,9 @@ class Config:
self.treeColWidth = self._parseLine( self.treeColWidth = self._parseLine(
cnfParse, cnfSec, "treecols", self.CNF_I_LST, self.treeColWidth cnfParse, cnfSec, "treecols", self.CNF_I_LST, self.treeColWidth
) )
self.novelColWidth = self._parseLine(
cnfParse, cnfSec, "novelcols", self.CNF_I_LST, self.novelColWidth
)
self.projColWidth = self._parseLine( self.projColWidth = self._parseLine(
cnfParse, cnfSec, "projcols", self.CNF_I_LST, self.projColWidth cnfParse, cnfSec, "projcols", self.CNF_I_LST, self.projColWidth
) )
@@ -597,6 +601,7 @@ class Config:
cnfParse.add_section(cnfSec) cnfParse.add_section(cnfSec)
cnfParse.set(cnfSec, "geometry", self._packList(self.winGeometry)) cnfParse.set(cnfSec, "geometry", self._packList(self.winGeometry))
cnfParse.set(cnfSec, "treecols", self._packList(self.treeColWidth)) cnfParse.set(cnfSec, "treecols", self._packList(self.treeColWidth))
cnfParse.set(cnfSec, "novelcols", self._packList(self.novelColWidth))
cnfParse.set(cnfSec, "projcols", self._packList(self.projColWidth)) cnfParse.set(cnfSec, "projcols", self._packList(self.projColWidth))
cnfParse.set(cnfSec, "mainpane", self._packList(self.mainPanePos)) cnfParse.set(cnfSec, "mainpane", self._packList(self.mainPanePos))
cnfParse.set(cnfSec, "docpane", self._packList(self.docPanePos)) cnfParse.set(cnfSec, "docpane", self._packList(self.docPanePos))
@@ -816,6 +821,11 @@ class Config:
self.confChanged = True self.confChanged = True
return True return True
def setNovelColWidths(self, colWidths):
self.novelColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True
return True
def setProjColWidths(self, colWidths): def setProjColWidths(self, colWidths):
self.projColWidth = [int(x/self.guiScale) for x in colWidths] self.projColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True self.confChanged = True
@@ -872,6 +882,9 @@ class Config:
def getTreeColWidths(self): def getTreeColWidths(self):
return [int(x*self.guiScale) for x in self.treeColWidth] return [int(x*self.guiScale) for x in self.treeColWidth]
def getNovelColWidths(self):
return [int(x*self.guiScale) for x in self.novelColWidth]
def getProjColWidths(self): def getProjColWidths(self):
return [int(x*self.guiScale) for x in self.projColWidth] return [int(x*self.guiScale) for x in self.projColWidth]
+147 -128
View File
@@ -51,18 +51,16 @@ class NWIndex():
self.indexBroken = False self.indexBroken = False
# Indices # Indices
self.tagIndex = None self._tagIndex = {}
self.refIndex = None self._refIndex = {}
self.novelIndex = None self._novelIndex = {}
self.noteIndex = None self._noteIndex = {}
self.textCounts = None self._textCounts = {}
# TimeStamps # TimeStamps
self.timeNovel = 0 self._timeNovel = 0
self.timeNote = 0 self._timeNotes = 0
self.timeIndex = 0 self._timeIndex = 0
self.clearIndex()
return return
@@ -73,14 +71,14 @@ class NWIndex():
def clearIndex(self): def clearIndex(self):
"""Clear the index dictionaries and time stamps. """Clear the index dictionaries and time stamps.
""" """
self.tagIndex = {} self._tagIndex = {}
self.refIndex = {} self._refIndex = {}
self.novelIndex = {} self._novelIndex = {}
self.noteIndex = {} self._noteIndex = {}
self.textCounts = {} self._textCounts = {}
self.timeNovel = 0 self._timeNovel = 0
self.timeNote = 0 self._timeNotes = 0
self.timeIndex = 0 self._timeIndex = 0
return return
def deleteHandle(self, tHandle): def deleteHandle(self, tHandle):
@@ -89,17 +87,17 @@ class NWIndex():
logger.debug("Removing item %s from the index" % tHandle) logger.debug("Removing item %s from the index" % tHandle)
delTags = [] delTags = []
for tTag in self.tagIndex: for tTag in self._tagIndex:
if self.tagIndex[tTag][1] == tHandle: if self._tagIndex[tTag][1] == tHandle:
delTags.append(tTag) delTags.append(tTag)
for tTag in delTags: for tTag in delTags:
self.tagIndex.pop(tTag, None) self._tagIndex.pop(tTag, None)
self.refIndex.pop(tHandle, None) self._refIndex.pop(tHandle, None)
self.novelIndex.pop(tHandle, None) self._novelIndex.pop(tHandle, None)
self.noteIndex.pop(tHandle, None) self._noteIndex.pop(tHandle, None)
self.textCounts.pop(tHandle, None) self._textCounts.pop(tHandle, None)
return return
@@ -123,6 +121,21 @@ class NWIndex():
return True return True
def novelChangedSince(self, checkTime):
"""Check if the novel index has changed since a given time.
"""
return self._timeNovel > checkTime
def notesChangedSince(self, checkTime):
"""Check if the notes index has changed since a given time.
"""
return self._timeNotes > checkTime
def indexChangedSince(self, checkTime):
"""Check if the index has changed since a given time.
"""
return self._timeIndex > checkTime
## ##
# Load and Save Index to/from File # Load and Save Index to/from File
## ##
@@ -143,16 +156,16 @@ class NWIndex():
logger.error(str(e)) logger.error(str(e))
return False return False
self.tagIndex = theData.get("tagIndex", {}) self._tagIndex = theData.get("tagIndex", {})
self.refIndex = theData.get("refIndex", {}) self._refIndex = theData.get("refIndex", {})
self.novelIndex = theData.get("novelIndex", {}) self._novelIndex = theData.get("novelIndex", {})
self.noteIndex = theData.get("noteIndex", {}) self._noteIndex = theData.get("noteIndex", {})
self.textCounts = theData.get("textCounts", {}) self._textCounts = theData.get("textCounts", {})
nowTime = round(time()) nowTime = round(time())
self.timeNovel = nowTime self._timeNovel = nowTime
self.timeNote = nowTime self._timeNotes = nowTime
self.timeIndex = nowTime self._timeIndex = nowTime
self.checkIndex() self.checkIndex()
@@ -168,11 +181,11 @@ class NWIndex():
try: try:
with open(indexFile, mode="w+", encoding="utf8") as outFile: with open(indexFile, mode="w+", encoding="utf8") as outFile:
json.dump({ json.dump({
"tagIndex" : self.tagIndex, "tagIndex" : self._tagIndex,
"refIndex" : self.refIndex, "refIndex" : self._refIndex,
"novelIndex" : self.novelIndex, "novelIndex" : self._novelIndex,
"noteIndex" : self.noteIndex, "noteIndex" : self._noteIndex,
"textCounts" : self.textCounts, "textCounts" : self._textCounts,
}, outFile, indent=2) }, outFile, indent=2)
except Exception as e: except Exception as e:
logger.error("Failed to save index file") logger.error("Failed to save index file")
@@ -189,28 +202,28 @@ class NWIndex():
self.indexBroken = False self.indexBroken = False
try: try:
for tTag in self.tagIndex: for tTag in self._tagIndex:
if len(self.tagIndex[tTag]) != 4: if len(self._tagIndex[tTag]) != 4:
self.indexBroken = True self.indexBroken = True
for tHandle in self.refIndex: for tHandle in self._refIndex:
for sTitle in self.refIndex[tHandle]: for sTitle in self._refIndex[tHandle]:
for tEntry in self.refIndex[tHandle][sTitle]["tags"]: for tEntry in self._refIndex[tHandle][sTitle]["tags"]:
if len(tEntry) != 3: if len(tEntry) != 3:
self.indexBroken = True self.indexBroken = True
for tHandle in self.novelIndex: for tHandle in self._novelIndex:
for sLine in self.novelIndex[tHandle]: for sLine in self._novelIndex[tHandle]:
if len(self.novelIndex[tHandle][sLine].keys()) != 8: if len(self._novelIndex[tHandle][sLine].keys()) != 8:
self.indexBroken = True self.indexBroken = True
for tHandle in self.noteIndex: for tHandle in self._noteIndex:
for sLine in self.noteIndex[tHandle]: for sLine in self._noteIndex[tHandle]:
if len(self.noteIndex[tHandle][sLine].keys()) != 8: if len(self._noteIndex[tHandle][sLine].keys()) != 8:
self.indexBroken = True self.indexBroken = True
for tHandle in self.textCounts: for tHandle in self._textCounts:
if len(self.textCounts[tHandle]) != 3: if len(self._textCounts[tHandle]) != 3:
self.indexBroken = True self.indexBroken = True
except Exception: except Exception:
@@ -254,7 +267,7 @@ class NWIndex():
# Run word counter for the whole text # Run word counter for the whole text
cC, wC, pC = countWords(theText) cC, wC, pC = countWords(theText)
self.textCounts[tHandle] = [cC, wC, pC] self._textCounts[tHandle] = [cC, wC, pC]
# If the file is archived or trashed, we don't index the file itself # If the file is archived or trashed, we don't index the file itself
if self.theProject.projTree.isTrashRoot(theItem.itemParent): if self.theProject.projTree.isTrashRoot(theItem.itemParent):
@@ -271,25 +284,25 @@ class NWIndex():
# Check file type, and reset its old index # Check file type, and reset its old index
# Also add a dummy entry T000000 in case the file has no title # Also add a dummy entry T000000 in case the file has no title
self.refIndex[tHandle] = {} self._refIndex[tHandle] = {}
self.refIndex[tHandle]["T000000"] = { self._refIndex[tHandle]["T000000"] = {
"tags" : [], "tags" : [],
"updated" : round(time()), "updated" : round(time()),
} }
if itemLayout == nwItemLayout.NOTE: if itemLayout == nwItemLayout.NOTE:
self.noteIndex[tHandle] = {} self._noteIndex[tHandle] = {}
isNovel = False isNovel = False
else: else:
self.novelIndex[tHandle] = {} self._novelIndex[tHandle] = {}
isNovel = True isNovel = True
# Also clear references to file in tag index # Also clear references to file in tag index
clearTags = [] clearTags = []
for aTag in self.tagIndex: for aTag in self._tagIndex:
if self.tagIndex[aTag][1] == tHandle: if self._tagIndex[aTag][1] == tHandle:
clearTags.append(aTag) clearTags.append(aTag)
for aTag in clearTags: for aTag in clearTags:
self.tagIndex.pop(aTag) self._tagIndex.pop(aTag)
nLine = 0 nLine = 0
nTitle = 0 nTitle = 0
@@ -330,11 +343,11 @@ class NWIndex():
# Update timestamps for index changes # Update timestamps for index changes
nowTime = round(time()) nowTime = round(time())
self.timeIndex = nowTime self._timeIndex = nowTime
if isNovel: if isNovel:
self.timeNovel = nowTime self._timeNovel = nowTime
else: else:
self.timeNote = nowTime self._timeNotes = nowTime
return True return True
@@ -362,7 +375,7 @@ class NWIndex():
return False return False
sTitle = "T%06d" % nLine sTitle = "T%06d" % nLine
self.refIndex[tHandle][sTitle] = { self._refIndex[tHandle][sTitle] = {
"tags" : [], "tags" : [],
"updated" : round(time()), "updated" : round(time()),
} }
@@ -379,11 +392,11 @@ class NWIndex():
if hText != "": if hText != "":
if isNovel: if isNovel:
if tHandle in self.novelIndex: if tHandle in self._novelIndex:
self.novelIndex[tHandle][sTitle] = theData self._novelIndex[tHandle][sTitle] = theData
else: else:
if tHandle in self.noteIndex: if tHandle in self._noteIndex:
self.noteIndex[tHandle][sTitle] = theData self._noteIndex[tHandle][sTitle] = theData
return True return True
@@ -393,19 +406,19 @@ class NWIndex():
cC, wC, pC = countWords(theText) cC, wC, pC = countWords(theText)
sTitle = "T%06d" % nTitle sTitle = "T%06d" % nTitle
if isNovel: if isNovel:
if tHandle in self.novelIndex: if tHandle in self._novelIndex:
if sTitle in self.novelIndex[tHandle]: if sTitle in self._novelIndex[tHandle]:
self.novelIndex[tHandle][sTitle]["cCount"] = cC self._novelIndex[tHandle][sTitle]["cCount"] = cC
self.novelIndex[tHandle][sTitle]["wCount"] = wC self._novelIndex[tHandle][sTitle]["wCount"] = wC
self.novelIndex[tHandle][sTitle]["pCount"] = pC self._novelIndex[tHandle][sTitle]["pCount"] = pC
self.novelIndex[tHandle][sTitle]["updated"] = round(time()) self._novelIndex[tHandle][sTitle]["updated"] = round(time())
else: else:
if tHandle in self.noteIndex: if tHandle in self._noteIndex:
if sTitle in self.noteIndex[tHandle]: if sTitle in self._noteIndex[tHandle]:
self.noteIndex[tHandle][sTitle]["cCount"] = cC self._noteIndex[tHandle][sTitle]["cCount"] = cC
self.noteIndex[tHandle][sTitle]["wCount"] = wC self._noteIndex[tHandle][sTitle]["wCount"] = wC
self.noteIndex[tHandle][sTitle]["pCount"] = pC self._noteIndex[tHandle][sTitle]["pCount"] = pC
self.noteIndex[tHandle][sTitle]["updated"] = round(time()) self._noteIndex[tHandle][sTitle]["updated"] = round(time())
return return
def _indexSynopsis(self, tHandle, isNovel, theText, nTitle): def _indexSynopsis(self, tHandle, isNovel, theText, nTitle):
@@ -413,15 +426,15 @@ class NWIndex():
""" """
sTitle = "T%06d" % nTitle sTitle = "T%06d" % nTitle
if isNovel: if isNovel:
if tHandle in self.novelIndex: if tHandle in self._novelIndex:
if sTitle in self.novelIndex[tHandle]: if sTitle in self._novelIndex[tHandle]:
self.novelIndex[tHandle][sTitle]["synopsis"] = theText self._novelIndex[tHandle][sTitle]["synopsis"] = theText
self.novelIndex[tHandle][sTitle]["updated"] = round(time()) self._novelIndex[tHandle][sTitle]["updated"] = round(time())
else: else:
if tHandle in self.noteIndex: if tHandle in self._noteIndex:
if sTitle in self.noteIndex[tHandle]: if sTitle in self._noteIndex[tHandle]:
self.noteIndex[tHandle][sTitle]["synopsis"] = theText self._noteIndex[tHandle][sTitle]["synopsis"] = theText
self.noteIndex[tHandle][sTitle]["updated"] = round(time()) self._noteIndex[tHandle][sTitle]["updated"] = round(time())
return return
def _indexNoteRef(self, tHandle, aLine, nLine, nTitle): def _indexNoteRef(self, tHandle, aLine, nLine, nTitle):
@@ -433,9 +446,9 @@ class NWIndex():
return False return False
sTitle = "T%06d" % nTitle sTitle = "T%06d" % nTitle
if sTitle in self.refIndex[tHandle] and theBits[0] != nwKeyWords.TAG_KEY: if sTitle in self._refIndex[tHandle] and theBits[0] != nwKeyWords.TAG_KEY:
for aVal in theBits[1:]: for aVal in theBits[1:]:
self.refIndex[tHandle][sTitle]["tags"].append([nLine, theBits[0], aVal]) self._refIndex[tHandle][sTitle]["tags"].append([nLine, theBits[0], aVal])
return True return True
@@ -448,7 +461,7 @@ class NWIndex():
if theBits[0] == nwKeyWords.TAG_KEY: if theBits[0] == nwKeyWords.TAG_KEY:
sTitle = "T%06d" % nTitle sTitle = "T%06d" % nTitle
self.tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle] self._tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle]
return True return True
@@ -512,8 +525,8 @@ class NWIndex():
# is ignored # is ignored
if theBits[0] == nwKeyWords.TAG_KEY and nBits > 1: if theBits[0] == nwKeyWords.TAG_KEY and nBits > 1:
isGood[0] = True isGood[0] = True
if theBits[1] in self.tagIndex: if theBits[1] in self._tagIndex:
if self.tagIndex[theBits[1]][1] == tItem.itemHandle: if self._tagIndex[theBits[1]][1] == tItem.itemHandle:
isGood[1] = True isGood[1] = True
else: else:
isGood[1] = False isGood[1] = False
@@ -523,8 +536,8 @@ class NWIndex():
# If we're still here, we better check that the references exist # If we're still here, we better check that the references exist
for n in range(1, nBits): for n in range(1, nBits):
if theBits[n] in self.tagIndex: if theBits[n] in self._tagIndex:
isGood[n] = nwKeyWords.KEY_CLASS[theBits[0]].name == self.tagIndex[theBits[n]][2] isGood[n] = nwKeyWords.KEY_CLASS[theBits[0]].name == self._tagIndex[theBits[n]][2]
return isGood return isGood
@@ -532,23 +545,21 @@ class NWIndex():
# Extract Data # Extract Data
## ##
def getNovelStructure(self, skipExcluded=True): def novelStructure(self, skipExcluded=True):
"""Builds a list of all titles in the novel, in the correct """Iterate over all titles in the novel, in the correct order as
order as they appear in the tree view and in the respective they appear in the tree view and in the respective document
document files, but skipping all note files. files, but skipping all note files.
""" """
theStructure = []
for tItem in self.theProject.projTree: for tItem in self.theProject.projTree:
if tItem is not None: if tItem is not None:
if not tItem.isExported and skipExcluded: if not tItem.isExported and skipExcluded:
continue continue
tHandle = tItem.itemHandle tHandle = tItem.itemHandle
if tHandle not in self.novelIndex: if tHandle not in self._novelIndex:
continue continue
for sTitle in sorted(self.novelIndex[tHandle].keys()): for sTitle in sorted(self._novelIndex[tHandle]):
theStructure.append("%s:%s" % (tHandle, sTitle)) tKey = "%s:%s" % (tHandle, sTitle)
yield tKey, tHandle, sTitle, self._novelIndex[tHandle][sTitle]
return theStructure
def getCounts(self, tHandle, sTitle=None): def getCounts(self, tHandle, sTitle=None):
"""Returns the counts for a file, or a section of a file """Returns the counts for a file, or a section of a file
@@ -559,21 +570,21 @@ class NWIndex():
pC = 0 pC = 0
if sTitle is None: if sTitle is None:
if tHandle in self.textCounts: if tHandle in self._textCounts:
cC = self.textCounts[tHandle][0] cC = self._textCounts[tHandle][0]
wC = self.textCounts[tHandle][1] wC = self._textCounts[tHandle][1]
pC = self.textCounts[tHandle][2] pC = self._textCounts[tHandle][2]
else: else:
if tHandle in self.novelIndex: if tHandle in self._novelIndex:
if sTitle in self.novelIndex[tHandle]: if sTitle in self._novelIndex[tHandle]:
cC = self.novelIndex[tHandle][sTitle]["cCount"] cC = self._novelIndex[tHandle][sTitle]["cCount"]
wC = self.novelIndex[tHandle][sTitle]["wCount"] wC = self._novelIndex[tHandle][sTitle]["wCount"]
pC = self.novelIndex[tHandle][sTitle]["pCount"] pC = self._novelIndex[tHandle][sTitle]["pCount"]
elif tHandle in self.noteIndex: elif tHandle in self._noteIndex:
if sTitle in self.noteIndex[tHandle]: if sTitle in self._noteIndex[tHandle]:
cC = self.noteIndex[tHandle][sTitle]["cCount"] cC = self._noteIndex[tHandle][sTitle]["cCount"]
wC = self.noteIndex[tHandle][sTitle]["wCount"] wC = self._noteIndex[tHandle][sTitle]["wCount"]
pC = self.noteIndex[tHandle][sTitle]["pCount"] pC = self._noteIndex[tHandle][sTitle]["pCount"]
return cC, wC, pC return cC, wC, pC
@@ -585,16 +596,24 @@ class NWIndex():
for tKey in nwKeyWords.KEY_CLASS: for tKey in nwKeyWords.KEY_CLASS:
theRefs[tKey] = [] theRefs[tKey] = []
if tHandle not in self.refIndex: if tHandle not in self._refIndex:
return theRefs return theRefs
for refTitle in self.refIndex[tHandle]: for refTitle in self._refIndex[tHandle]:
for aTag in self.refIndex[tHandle][refTitle].get("tags", []): for aTag in self._refIndex[tHandle][refTitle].get("tags", []):
if len(aTag) == 3 and (sTitle is None or sTitle == refTitle): if len(aTag) == 3 and (sTitle is None or sTitle == refTitle):
theRefs[aTag[1]].append(aTag[2]) theRefs[aTag[1]].append(aTag[2])
return theRefs return theRefs
def getNovelData(self, tHandle, sTitle):
"""Return the novel data of a given handle and title.
"""
if tHandle in self._novelIndex:
if sTitle in self._novelIndex[tHandle]:
return self._novelIndex[tHandle][sTitle]
return None
def getBackReferenceList(self, tHandle): def getBackReferenceList(self, tHandle):
"""Build a list of files referring back to our file, specified """Build a list of files referring back to our file, specified
by tHandle. by tHandle.
@@ -604,14 +623,14 @@ class NWIndex():
return theRefs return theRefs
theTags = set() theTags = set()
for tTag in self.tagIndex: for tTag in self._tagIndex:
if tHandle == self.tagIndex[tTag][1]: if tHandle == self._tagIndex[tTag][1]:
theTags.add(tTag) theTags.add(tTag)
if theTags: if theTags:
for tHandle in self.refIndex: for tHandle in self._refIndex:
for sTitle in self.refIndex[tHandle]: for sTitle in self._refIndex[tHandle]:
for _, _, tTag in self.refIndex[tHandle][sTitle]["tags"]: for _, _, tTag in self._refIndex[tHandle][sTitle]["tags"]:
if tTag in theTags and tHandle not in theRefs: if tTag in theTags and tHandle not in theRefs:
theRefs[tHandle] = sTitle theRefs[tHandle] = sTitle
@@ -620,8 +639,8 @@ class NWIndex():
def getTagSource(self, theTag): def getTagSource(self, theTag):
"""Return the source location of a given tag. """Return the source location of a given tag.
""" """
if theTag in self.tagIndex: if theTag in self._tagIndex:
theRef = self.tagIndex[theTag] theRef = self._tagIndex[theTag]
if len(theRef) == 4: if len(theRef) == 4:
return theRef[1], theRef[0], theRef[3] return theRef[1], theRef[0], theRef[3]
return None, 0, "T000000" return None, 0, "T000000"
+2 -2
View File
@@ -1025,7 +1025,7 @@ class NWProject():
by drag-and-drop. Forwarded to the NWTree class. by drag-and-drop. Forwarded to the NWTree class.
""" """
if len(self.projTree) != len(newOrder): if len(self.projTree) != len(newOrder):
logger.warning("Size of new and old tree order do not match") logger.warning("Sizes of new and old tree order do not match")
self.projTree.setOrder(newOrder) self.projTree.setOrder(newOrder)
self.setProjectChanged(True) self.setProjectChanged(True)
return True return True
@@ -1341,7 +1341,7 @@ class NWProject():
if oLayout is None: if oLayout is None:
oLayout = nwItemLayout.NOTE oLayout = nwItemLayout.NOTE
if oParent is None or not self.projTree.handleExists(oParent): if oParent is None or oParent not in self.projTree:
oParent = self.projTree.findRoot(oClass) oParent = self.projTree.findRoot(oClass)
if oParent is None: if oParent is None:
oParent = self.projTree.findRoot(nwItemClass.NOVEL) oParent = self.projTree.findRoot(nwItemClass.NOVEL)
-5
View File
@@ -294,11 +294,6 @@ class NWTree():
tTree.append(tHandle) tTree.append(tHandle)
return tTree return tTree
def handleExists(self, tHandle):
"""Check if a handle exists in the project.
"""
return tHandle in self._treeOrder
## ##
# Setters # Setters
## ##
+2
View File
@@ -9,6 +9,7 @@ from nw.gui.docviewer import GuiDocViewer, GuiDocViewDetails
from nw.gui.itemdetails import GuiItemDetails from nw.gui.itemdetails import GuiItemDetails
from nw.gui.itemeditor import GuiItemEditor from nw.gui.itemeditor import GuiItemEditor
from nw.gui.mainmenu import GuiMainMenu from nw.gui.mainmenu import GuiMainMenu
from nw.gui.noveltree import GuiNovelTree
from nw.gui.outline import GuiOutline from nw.gui.outline import GuiOutline
from nw.gui.outlinedetails import GuiOutlineDetails from nw.gui.outlinedetails import GuiOutlineDetails
from nw.gui.preferences import GuiPreferences from nw.gui.preferences import GuiPreferences
@@ -31,6 +32,7 @@ __all__ = [
"GuiItemDetails", "GuiItemDetails",
"GuiItemEditor", "GuiItemEditor",
"GuiMainMenu", "GuiMainMenu",
"GuiNovelTree",
"GuiMainStatus", "GuiMainStatus",
"GuiOutline", "GuiOutline",
"GuiOutlineDetails", "GuiOutlineDetails",
+312
View File
@@ -0,0 +1,312 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Novel Tree
novelWriter GUI Novel Tree
==============================
Class holding the project's novel files tree view
File History:
Created: 2020-12-20 [1.1a0]
This file is a part of novelWriter
Copyright 20182020, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
import logging
from time import time
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView
from nw.constants import nwKeyWords
from nw.common import checkInt
logger = logging.getLogger(__name__)
class GuiNovelTree(QTreeWidget):
C_TITLE = 0
C_WORDS = 1
C_POV = 2
def __init__(self, theParent):
QTreeWidget.__init__(self, theParent)
logger.debug("Initialising GuiNovelTree ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theParent.theProject
self.theIndex = theParent.theIndex
# Internal Variables
self._treeMap = {}
self._lastBuild = 0
# Build GUI
iPx = self.theTheme.baseIconSize
self.setIconSize(QSize(iPx, iPx))
self.setIndentation(iPx)
self.setColumnCount(3)
self.setHeaderLabels(["Title", "Words", "POV"])
self.itemDoubleClicked.connect(self._treeDoubleClick)
self.itemSelectionChanged.connect(self._itemSelected)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setExpandsOnDoubleClick(False)
self.setDragEnabled(False)
treeHeadItem = self.headerItem()
treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
treeHeadItem.setToolTip(self.C_TITLE, "Section title")
treeHeadItem.setToolTip(self.C_WORDS, "Word count")
treeHeadItem.setToolTip(self.C_POV, "Point-of-view character")
treeHeader = self.header()
treeHeader.setStretchLastSection(True)
treeHeader.setMinimumSectionSize(iPx + 6)
# Get user's column width preferences for NAME and COUNT
treeColWidth = self.mainConf.getNovelColWidths()
if len(treeColWidth) <= 3:
for colN, colW in enumerate(treeColWidth):
self.setColumnWidth(colN, colW)
# The last column should just auto-scale
self.resizeColumnToContents(self.C_POV)
# Set custom settings
self.initTree()
logger.debug("GuiNovelTree initialisation complete")
# Internal Mapping
self.makeAlert = self.theParent.makeAlert
return
def initTree(self):
"""Set or update tree widget settings.
"""
# Scroll bars
if self.mainConf.hideVScroll:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
if self.mainConf.hideHScroll:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
return
##
# Class Methods
##
def clearTree(self):
"""Clear the GUI content and the related maps.
"""
self.clear()
self._treeMap = {}
self._lastBuild = 0
return
def refreshTree(self, overRide=False):
"""Called whenever the Novel tab is activated.
"""
treeChanged = self.theParent.treeView.changedSince(self._lastBuild)
indexChanged = self.theIndex.novelChangedSince(self._lastBuild)
if not (treeChanged or indexChanged):
logger.verbose("No changes made to the novel")
return
selItem = self.selectedItems()
titleKey = None
if selItem:
titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2]
self.theParent.treeView.flushTreeOrder()
self._populateTree()
if titleKey is not None and titleKey in self._treeMap:
self._treeMap[titleKey].setSelected(True)
return
def getColumnSizes(self):
"""Return the column widths for the tree columns.
"""
retVals = [
self.columnWidth(0),
self.columnWidth(1),
]
return retVals
def getSelectedHandle(self):
"""Get the currently selected handle. If multiple items are
selected, return the first.
"""
selItem = self.selectedItems()
if selItem:
return selItem[0].data(self.C_TITLE, Qt.UserRole)[0]
return None
##
# Events
##
def mousePressEvent(self, theEvent):
"""Overload mousePressEvent to clear selection if clicking the
mouse in a blank area of the tree view, and to load a document
for viewing if the user middle-clicked.
"""
QTreeWidget.mousePressEvent(self, theEvent)
if theEvent.button() == Qt.LeftButton:
selItem = self.indexAt(theEvent.pos())
if not selItem.isValid():
self.clearSelection()
elif theEvent.button() == Qt.MiddleButton:
selItem = self.itemAt(theEvent.pos())
if not isinstance(selItem, QTreeWidgetItem):
return
tHandle = self.getSelectedHandle()
if tHandle is None:
return
self.theParent.viewDocument(tHandle)
return
##
# Slots
##
def _treeDoubleClick(self, tItem, tCol):
"""Extract the handle and line number of the title double-
clicked, and send it to the main gui class for opening in the
document editor.
"""
theData = tItem.data(self.C_TITLE, Qt.UserRole)
tHandle = theData[0]
tLine = checkInt(theData[1], 1)
logger.verbose("User selected entry with handle %s on line %s" % (tHandle, tLine))
self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True)
return
def _itemSelected(self):
"""Extract the handle and line number of the currently selected
title, and send it to the tree meta panel.
"""
selItems = self.selectedItems()
if selItems:
tHandle = selItems[0].data(self.C_TITLE, Qt.UserRole)[0]
self.theParent.treeMeta.updateViewBox(tHandle)
return
##
# Internal Functions
##
def _populateTree(self):
"""Build the tree based on the project index.
"""
self.clearTree()
currTitle = None
currChapter = None
currScene = None
for tKey, tHandle, sTitle, novIdx in self.theIndex.novelStructure(skipExcluded=True):
tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx)
self._treeMap[tKey] = tItem
tLevel = novIdx["level"]
if tLevel == "H1":
self.addTopLevelItem(tItem)
currTitle = tItem
currChapter = None
currScene = None
elif tLevel == "H2":
if currTitle is None:
self.addTopLevelItem(tItem)
else:
currTitle.addChild(tItem)
currChapter = tItem
currScene = None
elif tLevel == "H3":
if currChapter is None:
if currTitle is None:
self.addTopLevelItem(tItem)
else:
currTitle.addChild(tItem)
else:
currChapter.addChild(tItem)
currScene = tItem
elif tLevel == "H4":
if currScene is None:
if currChapter is None:
if currTitle is None:
self.addTopLevelItem(tItem)
else:
currTitle.addChild(tItem)
else:
currChapter.addChild(tItem)
else:
currScene.addChild(tItem)
tItem.setExpanded(True)
self._lastBuild = time()
return
def _createTreeItem(self, tHandle, sTitle, titleKey, novIdx):
"""Populate a tree item with all the column values.
"""
newItem = QTreeWidgetItem()
hIcon = "doc_%s" % novIdx["level"].lower()
theData = (tHandle, sTitle[1:].lstrip("0"), titleKey)
wC = int(novIdx["wCount"])
newItem.setText(self.C_TITLE, novIdx["title"])
newItem.setData(self.C_TITLE, Qt.UserRole, theData)
newItem.setIcon(self.C_TITLE, self.theTheme.getIcon(hIcon))
newItem.setText(self.C_WORDS, f"{wC:n}")
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
theRefs = self.theIndex.getReferences(tHandle, sTitle)
newItem.setText(self.C_POV, ", ".join(theRefs[nwKeyWords.POV_KEY]))
return newItem
# END Class GuiNovelTree
+13 -28
View File
@@ -165,7 +165,7 @@ class GuiOutline(QTreeWidget):
return return
def refreshTree(self, overRide=False): def refreshTree(self, overRide=False, novelChanged=False):
"""Called whenever the Outline tab is activated and controls """Called whenever the Outline tab is activated and controls
what data to load, and if necessary, force a rebuild of the what data to load, and if necessary, force a rebuild of the
tree. tree.
@@ -177,13 +177,10 @@ class GuiOutline(QTreeWidget):
self.firstView = False self.firstView = False
return return
# If the novel index has changed since the tree was last built, # If the novel index or novel tree has changed since the tree
# we rebuild the tree from the updated index. # was last built, we rebuild the tree from the updated index.
lastChange = self.theParent.theIndex.timeNovel indexChanged = self.theIndex.novelChangedSince(self.lastBuild)
logger.verbose("Last outline build: %.3f" % self.lastBuild) doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline
logger.verbose("Novel index change: %.3f" % lastChange)
doBuild = lastChange > self.lastBuild and self.theProject.autoOutline
if doBuild or overRide: if doBuild or overRide:
logger.debug("Rebuilding Project Outline") logger.debug("Rebuilding Project Outline")
self._populateTree() self._populateTree()
@@ -227,6 +224,7 @@ class GuiOutline(QTreeWidget):
tHandle = selItems[0].data(self.colIndex[nwOutline.TITLE], Qt.UserRole) tHandle = selItems[0].data(self.colIndex[nwOutline.TITLE], Qt.UserRole)
sTitle = selItems[0].data(self.colIndex[nwOutline.LINE], Qt.UserRole) sTitle = selItems[0].data(self.colIndex[nwOutline.LINE], Qt.UserRole)
self.theParent.projMeta.showItem(tHandle, sTitle) self.theParent.projMeta.showItem(tHandle, sTitle)
self.theParent.treeView.setSelectedHandle(tHandle)
return return
@@ -377,23 +375,12 @@ class GuiOutline(QTreeWidget):
currChapter = None currChapter = None
currScene = None currScene = None
for titleKey in self.theIndex.getNovelStructure(skipExcluded=True): for tKey, tHandle, sTitle, novIdx in self.theIndex.novelStructure(skipExcluded=True):
if len(titleKey) < 16: tItem = self._createTreeItem(tHandle, sTitle, novIdx)
continue self.treeMap[tKey] = tItem
tHandle = titleKey[:13]
sTitle = titleKey[14:]
if tHandle not in self.theIndex.novelIndex:
continue
if sTitle not in self.theIndex.novelIndex[tHandle]:
continue
tLevel = self.theIndex.novelIndex[tHandle][sTitle]["level"]
tItem = self._createTreeItem(tHandle, sTitle, tLevel)
self.treeMap[titleKey] = tItem
tLevel = novIdx["level"]
if tLevel == "H1": if tLevel == "H1":
self.addTopLevelItem(tItem) self.addTopLevelItem(tItem)
currTitle = tItem currTitle = tItem
@@ -436,14 +423,12 @@ class GuiOutline(QTreeWidget):
return return
def _createTreeItem(self, tHandle, sTitle, tLevel): def _createTreeItem(self, tHandle, sTitle, novIdx):
"""Populate a tree item with all the column values. """Populate a tree item with all the column values.
""" """
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.projTree[tHandle]
novIdx = self.theIndex.novelIndex[tHandle][sTitle]
newItem = QTreeWidgetItem() newItem = QTreeWidgetItem()
hIcon = "doc_%s" % tLevel.lower() hIcon = "doc_%s" % novIdx["level"].lower()
cC = int(novIdx["cCount"]) cC = int(novIdx["cCount"])
wC = int(novIdx["wCount"]) wC = int(novIdx["wCount"])
+4 -5
View File
@@ -271,11 +271,10 @@ class GuiOutlineDetails(QScrollArea):
"""Update the content of the tree with the given handle and line """Update the content of the tree with the given handle and line
number pointing to a header. number pointing to a header.
""" """
try: nwItem = self.theProject.projTree[tHandle]
nwItem = self.theProject.projTree[tHandle] novIdx = self.theIndex.getNovelData(tHandle, sTitle)
novIdx = self.theIndex.novelIndex[tHandle][sTitle] theRefs = self.theIndex.getReferences(tHandle, sTitle)
theRefs = self.theIndex.getReferences(tHandle, sTitle) if nwItem is None or novIdx is None:
except Exception:
return False return False
if novIdx["level"] in self.LVL_MAP: if novIdx["level"] in self.LVL_MAP:
+1 -1
View File
@@ -118,7 +118,7 @@ class GuiProjectSettings(PagedDialog):
self.theProject.setImportColours(importCol) self.theProject.setImportColours(importCol)
if self.tabStatus.colChanged or self.tabImport.colChanged: if self.tabStatus.colChanged or self.tabImport.colChanged:
self.theParent.rebuildTree() self.theParent.rebuildTrees()
if self.tabReplace.arChanged: if self.tabReplace.arChanged:
newList = self.tabReplace.getNewList() newList = self.tabReplace.getNewList()
+77 -46
View File
@@ -29,7 +29,9 @@
import nw import nw
import logging import logging
from PyQt5.QtCore import Qt, QSize from time import time
from PyQt5.QtCore import Qt, QSize, pyqtSignal
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction
@@ -49,6 +51,9 @@ class GuiProjectTree(QTreeWidget):
C_EXPORT = 2 C_EXPORT = 2
C_FLAGS = 3 C_FLAGS = 3
novelItemChanged = pyqtSignal()
noteItemChanged = pyqtSignal()
def __init__(self, theParent): def __init__(self, theParent):
QTreeWidget.__init__(self, theParent) QTreeWidget.__init__(self, theParent)
@@ -60,22 +65,27 @@ class GuiProjectTree(QTreeWidget):
self.theProject = theParent.theProject self.theProject = theParent.theProject
self.theIndex = theParent.theIndex self.theIndex = theParent.theIndex
# Tree Settings # Internal Variables
self.theMap = {} self._treeMap = {}
self.treeChanged = False self._treeChanged = False
self._timeChanged = 0
##
# Build GUI
##
# Context Menu
self.ctxMenu = GuiProjectTreeMenu(self) self.ctxMenu = GuiProjectTreeMenu(self)
self.clearTree() self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._rightClickMenu)
# Build GUI # Tree Settings
iPx = self.theTheme.baseIconSize iPx = self.theTheme.baseIconSize
self.setIconSize(QSize(iPx, iPx)) self.setIconSize(QSize(iPx, iPx))
self.setExpandsOnDoubleClick(True) self.setExpandsOnDoubleClick(True)
self.setIndentation(iPx) self.setIndentation(iPx)
self.setColumnCount(4) self.setColumnCount(4)
self.setHeaderLabels(["Label", "Words", "Inc", "Flags"]) self.setHeaderLabels(["Label", "Words", "Inc", "Flags"])
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._rightClickMenu)
treeHeadItem = self.headerItem() treeHeadItem = self.headerItem()
treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
@@ -102,7 +112,7 @@ class GuiProjectTree(QTreeWidget):
# Set Multiple Selection by CTRL # Set Multiple Selection by CTRL
# Disabled for now, until the merge files option has been added # Disabled for now, until the merge files option has been added
# self.setSelectionMode(QAbstractItemView.ExtendedSelection) # self.setSelectionMode(QAbstractItemView.ExtendedSelection)
# self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
# Get user's column width preferences for NAME and COUNT # Get user's column width preferences for NAME and COUNT
treeColWidth = self.mainConf.getTreeColWidths() treeColWidth = self.mainConf.getTreeColWidths()
@@ -116,10 +126,11 @@ class GuiProjectTree(QTreeWidget):
# Set custom settings # Set custom settings
self.initTree() self.initTree()
logger.debug("GuiProjectTree initialisation complete") # Internal Function Mapping
self.makeAlert = self.theParent.makeAlert
self.askQuestion = self.theParent.askQuestion
# Internal Mapping logger.debug("GuiProjectTree initialisation complete")
self.makeAlert = self.theParent.makeAlert
return return
@@ -147,8 +158,9 @@ class GuiProjectTree(QTreeWidget):
"""Clear the GUI content and the related map. """Clear the GUI content and the related map.
""" """
self.clear() self.clear()
self.theMap = {} self._treeMap = {}
self.treeChanged = False self._treeChanged = False
self._timeChanged = 0
return return
def newTreeItem(self, itemType, itemClass): def newTreeItem(self, itemType, itemClass):
@@ -274,10 +286,13 @@ class GuiProjectTree(QTreeWidget):
return False return False
pHandle = nwItem.itemParent pHandle = nwItem.itemParent
if pHandle is not None and pHandle in self.theMap: if pHandle is not None and pHandle in self._treeMap:
self.theMap[pHandle].setExpanded(True) self._treeMap[pHandle].setExpanded(True)
self._emitItemChange(tHandle)
self.clearSelection() self.clearSelection()
trItem.setSelected(True) trItem.setSelected(True)
return True return True
def moveTreeItem(self, nStep): def moveTreeItem(self, nStep):
@@ -318,6 +333,7 @@ class GuiProjectTree(QTreeWidget):
self.clearSelection() self.clearSelection()
cItem.setSelected(True) cItem.setSelected(True)
self._setTreeChanged(True) self._setTreeChanged(True)
self._emitItemChange(tHandle)
return True return True
@@ -338,7 +354,7 @@ class GuiProjectTree(QTreeWidget):
"""Calls saveTreeOrder if there are unsaved changes, otherwise """Calls saveTreeOrder if there are unsaved changes, otherwise
does nothing. does nothing.
""" """
if self.treeChanged: if self._treeChanged:
logger.verbose("Flushing project tree to project class") logger.verbose("Flushing project tree to project class")
self.saveTreeOrder() self.saveTreeOrder()
self._setTreeChanged(False) self._setTreeChanged(False)
@@ -391,7 +407,7 @@ class GuiProjectTree(QTreeWidget):
self.makeAlert("The Trash folder is already empty.", nwAlert.INFO) self.makeAlert("The Trash folder is already empty.", nwAlert.INFO)
return False return False
msgYes = self.theParent.askQuestion( msgYes = self.askQuestion(
"Empty Trash", "Permanently delete %d file(s) from Trash?" % nTrash "Empty Trash", "Permanently delete %d file(s) from Trash?" % nTrash
) )
if not msgYes: if not msgYes:
@@ -446,7 +462,7 @@ class GuiProjectTree(QTreeWidget):
# user if they want to permanently delete the file. # user if they want to permanently delete the file.
doPermanent = False doPermanent = False
if not alreadyAsked: if not alreadyAsked:
msgYes = self.theParent.askQuestion( msgYes = self.askQuestion(
"Delete File", "Permanently delete file '%s'?" % nwItemS.itemName "Delete File", "Permanently delete file '%s'?" % nwItemS.itemName
) )
if msgYes: if msgYes:
@@ -474,7 +490,7 @@ class GuiProjectTree(QTreeWidget):
# move it there. # move it there.
doTrash = False doTrash = False
if askForTrash: if askForTrash:
msgYes = self.theParent.askQuestion( msgYes = self.askQuestion(
"Delete File", "Move file '%s' to Trash?" % nwItemS.itemName "Delete File", "Move file '%s' to Trash?" % nwItemS.itemName
) )
if msgYes: if msgYes:
@@ -533,7 +549,9 @@ class GuiProjectTree(QTreeWidget):
return True return True
def setTreeItemValues(self, tHandle): def setTreeItemValues(self, tHandle):
"""Set the name and flag values for a tree item. """Set the name and flag values for a tree item from a handle in
the project tree. Does not trigger a tree change as the data is
already coming from the project tree.
""" """
trItem = self._getTreeItem(tHandle) trItem = self._getTreeItem(tHandle)
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.projTree[tHandle]
@@ -622,9 +640,9 @@ class GuiProjectTree(QTreeWidget):
sent first. sent first.
""" """
logger.debug("Building the project tree ...") logger.debug("Building the project tree ...")
self.clear() self.clearTree()
iCount = 0
iCount = 0
for nwItem in self.theProject.getProjectItems(): for nwItem in self.theProject.getProjectItems():
iCount += 1 iCount += 1
self._addTreeItem(nwItem) self._addTreeItem(nwItem)
@@ -642,21 +660,10 @@ class GuiProjectTree(QTreeWidget):
return None return None
def getSelectedHandles(self):
"""Return a list of all currently selected item handles.
"""
selItems = self.selectedItems()
selHandles = []
for n in range(len(selItems)):
if isinstance(selItems[n], QTreeWidgetItem):
selHandles.append(selItems[n].data(self.C_NAME, Qt.UserRole))
return selHandles
def setSelectedHandle(self, tHandle, doScroll=False): def setSelectedHandle(self, tHandle, doScroll=False):
"""Set a specific handle as the selected item. """Set a specific handle as the selected item.
""" """
if tHandle not in self.theMap: if tHandle not in self._treeMap:
return False return False
tItem = self._getTreeItem(tHandle) tItem = self._getTreeItem(tHandle)
@@ -664,7 +671,7 @@ class GuiProjectTree(QTreeWidget):
return False return False
self.clearSelection() self.clearSelection()
self.theMap[tHandle].setSelected(True) self._treeMap[tHandle].setSelected(True)
selItems = self.selectedIndexes() selItems = self.selectedIndexes()
if selItems and doScroll: if selItems and doScroll:
@@ -672,6 +679,11 @@ class GuiProjectTree(QTreeWidget):
return True return True
def changedSince(self, checkTime):
"""Check if the tree has changed since a given time.
"""
return self._timeChanged > checkTime
## ##
# Slots # Slots
## ##
@@ -699,7 +711,7 @@ class GuiProjectTree(QTreeWidget):
def mousePressEvent(self, theEvent): def mousePressEvent(self, theEvent):
"""Overload mousePressEvent to clear selection if clicking the """Overload mousePressEvent to clear selection if clicking the
mouse in a blank area of the tree view, and to load a document mouse in a blank area of the tree view, and to load a document
for viewing if the suer middle clicked. for viewing if the user middle-clicked.
""" """
QTreeWidget.mousePressEvent(self, theEvent) QTreeWidget.mousePressEvent(self, theEvent)
@@ -783,6 +795,10 @@ class GuiProjectTree(QTreeWidget):
else: else:
self.theIndex.reIndexHandle(sHandle) self.theIndex.reIndexHandle(sHandle)
# Trigger dependent updates
self._setTreeChanged(True)
self._emitItemChange(sHandle)
else: else:
theEvent.ignore() theEvent.ignore()
logger.debug("Drag'n'drop of item %s not accepted" % sHandle) logger.debug("Drag'n'drop of item %s not accepted" % sHandle)
@@ -797,7 +813,7 @@ class GuiProjectTree(QTreeWidget):
def _getTreeItem(self, tHandle): def _getTreeItem(self, tHandle):
"""Returns the QTreeWidgetItem of a given item handle. """Returns the QTreeWidgetItem of a given item handle.
""" """
return self.theMap.get(tHandle, None) return self._treeMap.get(tHandle, None)
def _scanChildren(self, theList, theItem, theIndex): def _scanChildren(self, theList, theItem, theIndex):
"""This is a recursive function returning all items in a tree """This is a recursive function returning all items in a tree
@@ -834,7 +850,7 @@ class GuiProjectTree(QTreeWidget):
newItem.setData(self.C_NAME, Qt.UserRole, tHandle) newItem.setData(self.C_NAME, Qt.UserRole, tHandle)
newItem.setData(self.C_COUNT, Qt.UserRole, 0) newItem.setData(self.C_COUNT, Qt.UserRole, 0)
self.theMap[tHandle] = newItem self._treeMap[tHandle] = newItem
if pHandle is None: if pHandle is None:
if nwItem.itemType == nwItemType.ROOT: if nwItem.itemType == nwItemType.ROOT:
self.addTopLevelItem(newItem) self.addTopLevelItem(newItem)
@@ -845,20 +861,20 @@ class GuiProjectTree(QTreeWidget):
self.makeAlert( self.makeAlert(
"There is nowhere to add item with name '%s'" % nwItem.itemName, nwAlert.ERROR "There is nowhere to add item with name '%s'" % nwItem.itemName, nwAlert.ERROR
) )
del self.theMap[tHandle] del self._treeMap[tHandle]
return None return None
else: else:
byIndex = -1 byIndex = -1
if nHandle is not None and nHandle in self.theMap: if nHandle is not None and nHandle in self._treeMap:
try: try:
byIndex = self.theMap[pHandle].indexOfChild(self.theMap[nHandle]) byIndex = self._treeMap[pHandle].indexOfChild(self._treeMap[nHandle])
except Exception: except Exception:
logger.error("Failed to get index of item with handle %s" % nHandle) logger.error("Failed to get index of item with handle %s" % nHandle)
if byIndex >= 0: if byIndex >= 0:
self.theMap[pHandle].insertChild(byIndex+1, newItem) self._treeMap[pHandle].insertChild(byIndex+1, newItem)
else: else:
self.theMap[pHandle].addChild(newItem) self._treeMap[pHandle].addChild(newItem)
self.propagateCount(tHandle, nwItem.wordCount) self.propagateCount(tHandle, nwItem.wordCount)
self.setTreeItemValues(tHandle) self.setTreeItemValues(tHandle)
@@ -910,7 +926,6 @@ class GuiProjectTree(QTreeWidget):
pHandle = trItemP.data(self.C_NAME, Qt.UserRole) pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
nwItemS.setParent(pHandle) nwItemS.setParent(pHandle)
self.setTreeItemValues(tHandle) self.setTreeItemValues(tHandle)
self._setTreeChanged(True)
logger.debug("The parent of item %s has been changed to %s" % (tHandle, pHandle)) logger.debug("The parent of item %s has been changed to %s" % (tHandle, pHandle))
@@ -919,11 +934,27 @@ class GuiProjectTree(QTreeWidget):
def _setTreeChanged(self, theState): def _setTreeChanged(self, theState):
"""Set the tree change flag, and propagate to the project. """Set the tree change flag, and propagate to the project.
""" """
self.treeChanged = theState self._treeChanged = theState
if theState: if theState:
self._timeChanged = time()
self.theProject.setProjectChanged(True) self.theProject.setProjectChanged(True)
return return
def _emitItemChange(self, tHandle):
"""Emit an item change signal for a given handle.
"""
nwItem = self.theProject.projTree[tHandle]
if nwItem is None:
return
if nwItem.itemType == nwItemType.FILE:
if nwItem.itemClass == nwItemClass.NOVEL:
self.novelItemChanged.emit()
else:
self.noteItemChanged.emit()
return
# END Class GuiProjectTree # END Class GuiProjectTree
class GuiProjectTreeMenu(QMenu): class GuiProjectTreeMenu(QMenu):
+97 -34
View File
@@ -32,7 +32,7 @@ import os
from datetime import datetime from datetime import datetime
from time import time from time import time
from PyQt5.QtCore import Qt, QTimer, QThreadPool from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot
from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence, QCursor from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence, QCursor
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut, qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut,
@@ -42,9 +42,9 @@ from PyQt5.QtWidgets import (
from nw.gui import ( from nw.gui import (
GuiAbout, GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiAbout, GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit,
GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor,
GuiMainMenu, GuiMainStatus, GuiOutline, GuiOutlineDetails, GuiPreferences, GuiMainMenu, GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails,
GuiProjectLoad, GuiProjectSettings, GuiProjectTree, GuiProjectWizard, GuiPreferences, GuiProjectLoad, GuiProjectSettings, GuiProjectTree,
GuiTheme, GuiWritingStats GuiProjectWizard, GuiTheme, GuiWritingStats
) )
from nw.core import NWProject, NWDoc, NWIndex from nw.core import NWProject, NWDoc, NWIndex
from nw.constants import nwItemType, nwItemClass, nwAlert, nwConst from nw.constants import nwItemType, nwItemClass, nwAlert, nwConst
@@ -99,6 +99,7 @@ class GuiMain(QMainWindow):
# Main GUI Elements # Main GUI Elements
self.statusBar = GuiMainStatus(self) self.statusBar = GuiMainStatus(self)
self.treeView = GuiProjectTree(self) self.treeView = GuiProjectTree(self)
self.novelView = GuiNovelTree(self)
self.docEditor = GuiDocEditor(self) self.docEditor = GuiDocEditor(self)
self.viewMeta = GuiDocViewDetails(self) self.viewMeta = GuiDocViewDetails(self)
self.docViewer = GuiDocViewer(self) self.docViewer = GuiDocViewer(self)
@@ -111,11 +112,24 @@ class GuiMain(QMainWindow):
self.statusIcons = [] self.statusIcons = []
self.importIcons = [] self.importIcons = []
# Project Tabs : Project / Novel
self.projTabs = QTabWidget()
self.projTabs.setTabPosition(QTabWidget.South)
self.projTabs.setStyleSheet("QTabWidget::pane {border: 0;};")
self.projTabs.addTab(self.treeView, "Project")
self.projTabs.addTab(self.novelView, "Novel")
self.projTabs.currentChanged.connect(self._projTabsChanged)
tabFont = self.projTabs.tabBar().font()
tabFont.setPointSize(round(0.9*self.theTheme.fontPointSize))
self.projTabs.tabBar().setFont(tabFont)
# Project Tree View # Project Tree View
self.treePane = QWidget() self.treePane = QWidget()
self.treeBox = QVBoxLayout() self.treeBox = QVBoxLayout()
self.treeBox.setContentsMargins(0, 0, 0, 0) self.treeBox.setContentsMargins(0, 0, 0, 0)
self.treeBox.addWidget(self.treeView) self.treeBox.setSpacing(0)
self.treeBox.addWidget(self.projTabs)
self.treeBox.addWidget(self.treeMeta) self.treeBox.addWidget(self.treeMeta)
self.treePane.setLayout(self.treeBox) self.treePane.setLayout(self.treeBox)
@@ -136,31 +150,33 @@ class GuiMain(QMainWindow):
self.splitOutline.addWidget(self.projMeta) self.splitOutline.addWidget(self.projMeta)
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos()) self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
# Main Tabs : Edirot / Outline # Main Tabs : Editor / Outline
self.tabWidget = QTabWidget() self.mainTabs = QTabWidget()
self.tabWidget.setTabPosition(QTabWidget.East) self.mainTabs.setTabPosition(QTabWidget.East)
self.tabWidget.setStyleSheet("QTabWidget::pane {border: 0;}") self.mainTabs.setStyleSheet("QTabWidget::pane {border: 0;}")
self.tabWidget.addTab(self.splitDocs, "Editor") self.mainTabs.addTab(self.splitDocs, "Editor")
self.tabWidget.addTab(self.splitOutline, "Outline") self.mainTabs.addTab(self.splitOutline, "Outline")
self.tabWidget.currentChanged.connect(self._mainTabChanged) self.mainTabs.currentChanged.connect(self._mainTabChanged)
# Splitter : Project Tree / Main Tabs # Splitter : Project Tree / Main Tabs
xCM = self.mainConf.pxInt(4) xCM = self.mainConf.pxInt(4)
self.splitMain = QSplitter(Qt.Horizontal) self.splitMain = QSplitter(Qt.Horizontal)
self.splitMain.setContentsMargins(xCM, xCM, xCM, xCM) self.splitMain.setContentsMargins(xCM, xCM, xCM, xCM)
self.splitMain.addWidget(self.treePane) self.splitMain.addWidget(self.treePane)
self.splitMain.addWidget(self.tabWidget) self.splitMain.addWidget(self.mainTabs)
self.splitMain.setSizes(self.mainConf.getMainPanePos()) self.splitMain.setSizes(self.mainConf.getMainPanePos())
# Indices of All Splitter Widgets # Indices of All Splitter Widgets
self.idxTree = self.splitMain.indexOf(self.treePane) self.idxTree = self.splitMain.indexOf(self.treePane)
self.idxMain = self.splitMain.indexOf(self.tabWidget) self.idxMain = self.splitMain.indexOf(self.mainTabs)
self.idxEditor = self.splitDocs.indexOf(self.docEditor) self.idxEditor = self.splitDocs.indexOf(self.docEditor)
self.idxViewer = self.splitDocs.indexOf(self.splitView) self.idxViewer = self.splitDocs.indexOf(self.splitView)
self.idxViewDoc = self.splitView.indexOf(self.docViewer) self.idxViewDoc = self.splitView.indexOf(self.docViewer)
self.idxViewMeta = self.splitView.indexOf(self.viewMeta) self.idxViewMeta = self.splitView.indexOf(self.viewMeta)
self.idxTabEdit = self.tabWidget.indexOf(self.splitDocs) self.idxTabEdit = self.mainTabs.indexOf(self.splitDocs)
self.idxTabProj = self.tabWidget.indexOf(self.splitOutline) self.idxTabProj = self.mainTabs.indexOf(self.splitOutline)
self.idxTreeView = self.projTabs.indexOf(self.treeView)
self.idxNovelView = self.projTabs.indexOf(self.novelView)
# Splitter Behaviour # Splitter Behaviour
self.splitMain.setCollapsible(self.idxTree, False) self.splitMain.setCollapsible(self.idxTree, False)
@@ -177,7 +193,8 @@ class GuiMain(QMainWindow):
# Initialise the Project Tree # Initialise the Project Tree
self.treeView.itemSelectionChanged.connect(self._treeSingleClick) self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.rebuildTree() self.treeView.novelItemChanged.connect(self._treeNovelItemChanged)
self.rebuildTrees()
# Set Main Window Elements # Set Main Window Elements
self.setMenuBar(self.mainMenu) self.setMenuBar(self.mainMenu)
@@ -253,6 +270,7 @@ class GuiMain(QMainWindow):
"""Wrapper function to clear all sub-elements of the main GUI. """Wrapper function to clear all sub-elements of the main GUI.
""" """
self.treeView.clearTree() self.treeView.clearTree()
self.novelView.clearTree()
self.docEditor.clearEditor() self.docEditor.clearEditor()
self.closeDocViewer() self.closeDocViewer()
self.statusBar.clearStatus() self.statusBar.clearStatus()
@@ -299,7 +317,7 @@ class GuiMain(QMainWindow):
logger.info("Creating new project") logger.info("Creating new project")
if self.theProject.newProject(projData): if self.theProject.newProject(projData):
self.rebuildTree() self.rebuildTrees()
self.saveProject() self.saveProject()
self.hasProject = True self.hasProject = True
self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setRefTime(self.theProject.projOpened)
@@ -355,7 +373,7 @@ class GuiMain(QMainWindow):
self.theIndex.clearIndex() self.theIndex.clearIndex()
self.clearGUI() self.clearGUI()
self.hasProject = False self.hasProject = False
self.tabWidget.setCurrentWidget(self.splitDocs) self.mainTabs.setCurrentWidget(self.splitDocs)
return saveOK return saveOK
@@ -371,7 +389,7 @@ class GuiMain(QMainWindow):
return False return False
# Switch main tab to editor view # Switch main tab to editor view
self.tabWidget.setCurrentWidget(self.splitDocs) self.mainTabs.setCurrentWidget(self.splitDocs)
# Try to open the project # Try to open the project
if not self.theProject.openProject(projFile): if not self.theProject.openProject(projFile):
@@ -423,7 +441,7 @@ class GuiMain(QMainWindow):
# Update GUI # Update GUI
self._setWindowTitle(self.theProject.projName) self._setWindowTitle(self.theProject.projName)
self.rebuildTree() self.rebuildTrees()
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.docEditor.setSpellCheck(self.theProject.spellCheck) self.docEditor.setSpellCheck(self.theProject.spellCheck)
self.mainMenu.setAutoOutline(self.theProject.autoOutline) self.mainMenu.setAutoOutline(self.theProject.autoOutline)
@@ -490,7 +508,7 @@ class GuiMain(QMainWindow):
return False return False
self.closeDocument() self.closeDocument()
self.tabWidget.setCurrentWidget(self.splitDocs) self.mainTabs.setCurrentWidget(self.splitDocs)
if self.docEditor.loadText(tHandle, tLine): if self.docEditor.loadText(tHandle, tLine):
if changeFocus: if changeFocus:
self.docEditor.setFocus() self.docEditor.setFocus()
@@ -575,7 +593,7 @@ class GuiMain(QMainWindow):
return False return False
# Make sure main tab is in Editor view # Make sure main tab is in Editor view
self.tabWidget.setCurrentWidget(self.splitDocs) self.mainTabs.setCurrentWidget(self.splitDocs)
logger.debug("Viewing document with handle %s" % tHandle) logger.debug("Viewing document with handle %s" % tHandle)
if self.docViewer.loadText(tHandle): if self.docViewer.loadText(tHandle):
@@ -739,13 +757,13 @@ class GuiMain(QMainWindow):
return return
def rebuildTree(self): def rebuildTrees(self):
"""Rebuild the project tree. """Rebuild the project tree.
""" """
self._makeStatusIcons() self._makeStatusIcons()
self._makeImportIcons() self._makeImportIcons()
self.treeView.clearTree()
self.treeView.buildTree() self.treeView.buildTree()
self.novelView.refreshTree()
return return
def rebuildIndex(self, beQuiet=False): def rebuildIndex(self, beQuiet=False):
@@ -803,7 +821,7 @@ class GuiMain(QMainWindow):
return False return False
logger.verbose("Forcing a rebuild of the Project Outline") logger.verbose("Forcing a rebuild of the Project Outline")
self.tabWidget.setCurrentWidget(self.splitOutline) self.mainTabs.setCurrentWidget(self.splitOutline)
self.projView.refreshTree(overRide=True) self.projView.refreshTree(overRide=True)
return True return True
@@ -866,6 +884,7 @@ class GuiMain(QMainWindow):
self.docEditor.initEditor() self.docEditor.initEditor()
self.docViewer.initViewer() self.docViewer.initViewer()
self.treeView.initTree() self.treeView.initTree()
self.novelView.initTree()
self.projView.initOutline() self.projView.initOutline()
self.projMeta.initDetails() self.projMeta.initDetails()
@@ -1027,6 +1046,7 @@ class GuiMain(QMainWindow):
self.mainConf.setShowRefPanel(self.viewMeta.isVisible()) self.mainConf.setShowRefPanel(self.viewMeta.isVisible())
self.mainConf.setTreeColWidths(self.treeView.getColumnSizes()) self.mainConf.setTreeColWidths(self.treeView.getColumnSizes())
self.mainConf.setNovelColWidths(self.novelView.getColumnSizes())
if not self.mainConf.isFullScreen: if not self.mainConf.isFullScreen:
self.mainConf.setWinSize(self.width(), self.height()) self.mainConf.setWinSize(self.width(), self.height())
@@ -1083,7 +1103,7 @@ class GuiMain(QMainWindow):
self.mainMenu.aFocusMode.setChecked(self.isFocusMode) self.mainMenu.aFocusMode.setChecked(self.isFocusMode)
if self.isFocusMode: if self.isFocusMode:
logger.debug("Activating Focus Mode") logger.debug("Activating Focus Mode")
self.tabWidget.setCurrentWidget(self.splitDocs) self.mainTabs.setCurrentWidget(self.splitDocs)
else: else:
logger.debug("Deactivating Focus Mode") logger.debug("Deactivating Focus Mode")
@@ -1091,7 +1111,7 @@ class GuiMain(QMainWindow):
self.treePane.setVisible(isVisible) self.treePane.setVisible(isVisible)
self.statusBar.setVisible(isVisible) self.statusBar.setVisible(isVisible)
self.mainMenu.setVisible(isVisible) self.mainMenu.setVisible(isVisible)
self.tabWidget.tabBar().setVisible(isVisible) self.mainTabs.tabBar().setVisible(isVisible)
hideDocFooter = self.isFocusMode and self.mainConf.hideFocusFooter hideDocFooter = self.isFocusMode and self.mainConf.hideFocusFooter
self.docEditor.docFooter.setVisible(not hideDocFooter) self.docEditor.docFooter.setVisible(not hideDocFooter)
@@ -1300,9 +1320,10 @@ class GuiMain(QMainWindow):
return return
## ##
# Signal Handlers # Slots
## ##
@pyqtSlot()
def _treeSingleClick(self): def _treeSingleClick(self):
"""Single click on a project tree item just updates the details """Single click on a project tree item just updates the details
panel below the tree. panel below the tree.
@@ -1312,12 +1333,14 @@ class GuiMain(QMainWindow):
self.treeMeta.updateViewBox(sHandle) self.treeMeta.updateViewBox(sHandle)
return return
@pyqtSlot("QTreeWidgetItem*", int)
def _treeDoubleClick(self, tItem, colNo): def _treeDoubleClick(self, tItem, colNo):
"""The user double-clicked an item in the tree. If it is a file, """The user double-clicked an item in the tree. If it is a file,
we open it. Otherwise, we do nothing. we open it. Otherwise, we do nothing.
""" """
tHandle = tItem.data(self.treeView.C_NAME, Qt.UserRole) tHandle = tItem.data(self.treeView.C_NAME, Qt.UserRole)
logger.verbose("User double clicked tree item with handle %s" % tHandle) logger.verbose("User double clicked tree item with handle %s" % tHandle)
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.projTree[tHandle]
if nwItem is not None: if nwItem is not None:
if nwItem.itemType == nwItemType.FILE: if nwItem.itemType == nwItemType.FILE:
@@ -1328,6 +1351,20 @@ class GuiMain(QMainWindow):
return return
@pyqtSlot()
def _treeNovelItemChanged(self):
"""Triggered when there is a change to a novel item in the
project tree.
"""
if self.mainTabs.currentIndex() == self.idxTabProj:
logger.verbose("Novel tree changed while Outline tab active")
if self.hasProject:
self.treeView.flushTreeOrder()
self.projView.refreshTree(novelChanged=True)
return
@pyqtSlot()
def _treeKeyPressReturn(self): def _treeKeyPressReturn(self):
"""The user pressed return on an item in the tree. If it is a """The user pressed return on an item in the tree. If it is a
file, we open it. Otherwise, we do nothing. Pressing return does file, we open it. Otherwise, we do nothing. Pressing return does
@@ -1335,6 +1372,7 @@ class GuiMain(QMainWindow):
""" """
tHandle = self.treeView.getSelectedHandle() tHandle = self.treeView.getSelectedHandle()
logger.verbose("User pressed return on tree item with handle %s" % tHandle) logger.verbose("User pressed return on tree item with handle %s" % tHandle)
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.projTree[tHandle]
if nwItem is not None: if nwItem is not None:
if nwItem.itemType == nwItemType.FILE: if nwItem.itemType == nwItemType.FILE:
@@ -1342,8 +1380,10 @@ class GuiMain(QMainWindow):
self.openDocument(tHandle, changeFocus=False, doScroll=False) self.openDocument(tHandle, changeFocus=False, doScroll=False)
else: else:
logger.verbose("Requested item %s is a folder" % tHandle) logger.verbose("Requested item %s is a folder" % tHandle)
return return
@pyqtSlot()
def _keyPressEscape(self): def _keyPressEscape(self):
"""When the escape key is pressed somewhere in the main window, """When the escape key is pressed somewhere in the main window,
do the following, in order: do the following, in order:
@@ -1352,8 +1392,10 @@ class GuiMain(QMainWindow):
self.docEditor.closeSearch() self.docEditor.closeSearch()
elif self.isFocusMode: elif self.isFocusMode:
self.toggleFocusMode() self.toggleFocusMode()
return return
@pyqtSlot(int)
def _mainTabChanged(self, tabIndex): def _mainTabChanged(self, tabIndex):
"""Activated when the main window tab is changed. """Activated when the main window tab is changed.
""" """
@@ -1363,6 +1405,27 @@ class GuiMain(QMainWindow):
logger.verbose("Project outline tab activated") logger.verbose("Project outline tab activated")
if self.hasProject: if self.hasProject:
self.projView.refreshTree() self.projView.refreshTree()
return
@pyqtSlot(int)
def _projTabsChanged(self, tabIndex):
"""Activated when the project view tab is changed.
"""
sHandle = None
if tabIndex == self.idxTreeView:
logger.verbose("Project tree tab activated")
sHandle = self.treeView.getSelectedHandle()
elif tabIndex == self.idxNovelView:
logger.verbose("Novel tree tab activated")
if self.hasProject:
self.novelView.refreshTree()
sHandle = self.novelView.getSelectedHandle()
self.treeMeta.updateViewBox(sHandle)
return return
# END Class GuiMain # END Class GuiMain
+3 -3
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.0" hexVersion="0x010000f0" fileVersion="1.2" timeStamp="2021-01-03 16:47:07"> <novelWriterXML appVersion="1.1a0" hexVersion="0x010100a0" fileVersion="1.2" timeStamp="2021-01-03 17:17:01">
<project> <project>
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
<author>Jane Smith</author> <author>Jane Smith</author>
<author>Jay Doh</author> <author>Jay Doh</author>
<saveCount>823</saveCount> <saveCount>824</saveCount>
<autoCount>153</autoCount> <autoCount>153</autoCount>
<editTime>39446</editTime> <editTime>39448</editTime>
</project> </project>
<settings> <settings>
<doBackup>False</doBackup> <doBackup>False</doBackup>
@@ -11,6 +11,7 @@ lastnotes = 1.0
[Sizes] [Sizes]
geometry = 1200, 650 geometry = 1200, 650
treecols = 200, 50, 30 treecols = 200, 50, 30
novelcols = 200, 50
projcols = 200, 60, 140 projcols = 200, 60, 140
mainpane = 300, 800 mainpane = 300, 800
docpane = 400, 400 docpane = 400, 400
@@ -11,6 +11,7 @@ lastnotes = 1.0
[Sizes] [Sizes]
geometry = 1100, 650 geometry = 1100, 650
treecols = 120, 30, 50 treecols = 120, 30, 50
novelcols = 200, 50
projcols = 140, 55, 140 projcols = 140, 55, 140
mainpane = 300, 800 mainpane = 300, 800
docpane = 400, 400 docpane = 400, 400
+13
View File
@@ -304,6 +304,19 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir):
assert tmpConf.setTreeColWidths([200, 50, 30]) assert tmpConf.setTreeColWidths([200, 50, 30])
# Novel Tree Columns
tmpConf.guiScale = 2.0
assert tmpConf.setNovelColWidths([10, 20])
assert tmpConf.getNovelColWidths() == [10, 20]
assert tmpConf.novelColWidth == [5, 10]
tmpConf.guiScale = 1.0
assert tmpConf.setNovelColWidths([10, 20])
assert tmpConf.getNovelColWidths() == [10, 20]
assert tmpConf.novelColWidth == [10, 20]
assert tmpConf.setNovelColWidths([200, 50])
# Project Settings Tree Columns # Project Settings Tree Columns
tmpConf.guiScale = 2.0 tmpConf.guiScale = 2.0
assert tmpConf.setProjColWidths([10, 20, 30]) assert tmpConf.setProjColWidths([10, 20, 30])
+138 -108
View File
@@ -56,30 +56,30 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, dummyGUI, outDir, refDir):
assert theIndex.saveIndex() assert theIndex.saveIndex()
# Take a copy of the index # Take a copy of the index
tagIndex = str(theIndex.tagIndex) tagIndex = str(theIndex._tagIndex)
refIndex = str(theIndex.refIndex) refIndex = str(theIndex._refIndex)
novelIndex = str(theIndex.novelIndex) novelIndex = str(theIndex._novelIndex)
noteIndex = str(theIndex.noteIndex) noteIndex = str(theIndex._noteIndex)
textCounts = str(theIndex.textCounts) textCounts = str(theIndex._textCounts)
# Delete a handle # Delete a handle
assert theIndex.tagIndex.get("Bod", None) is not None assert theIndex._tagIndex.get("Bod", None) is not None
assert theIndex.refIndex.get("4c4f28287af27", None) is not None assert theIndex._refIndex.get("4c4f28287af27", None) is not None
assert theIndex.noteIndex.get("4c4f28287af27", None) is not None assert theIndex._noteIndex.get("4c4f28287af27", None) is not None
assert theIndex.textCounts.get("4c4f28287af27", None) is not None assert theIndex._textCounts.get("4c4f28287af27", None) is not None
theIndex.deleteHandle("4c4f28287af27") theIndex.deleteHandle("4c4f28287af27")
assert theIndex.tagIndex.get("Bod", None) is None assert theIndex._tagIndex.get("Bod", None) is None
assert theIndex.refIndex.get("4c4f28287af27", None) is None assert theIndex._refIndex.get("4c4f28287af27", None) is None
assert theIndex.noteIndex.get("4c4f28287af27", None) is None assert theIndex._noteIndex.get("4c4f28287af27", None) is None
assert theIndex.textCounts.get("4c4f28287af27", None) is None assert theIndex._textCounts.get("4c4f28287af27", None) is None
# Clear the index # Clear the index
theIndex.clearIndex() theIndex.clearIndex()
assert not theIndex.tagIndex assert not theIndex._tagIndex
assert not theIndex.refIndex assert not theIndex._refIndex
assert not theIndex.novelIndex assert not theIndex._novelIndex
assert not theIndex.noteIndex assert not theIndex._noteIndex
assert not theIndex.textCounts assert not theIndex._textCounts
# Make the load fail # Make the load fail
monkeypatch.setattr(json, "load", doPanic) monkeypatch.setattr(json, "load", doPanic)
@@ -89,46 +89,46 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, dummyGUI, outDir, refDir):
monkeypatch.undo() monkeypatch.undo()
assert theIndex.loadIndex() assert theIndex.loadIndex()
assert str(theIndex.tagIndex) == tagIndex assert str(theIndex._tagIndex) == tagIndex
assert str(theIndex.refIndex) == refIndex assert str(theIndex._refIndex) == refIndex
assert str(theIndex.novelIndex) == novelIndex assert str(theIndex._novelIndex) == novelIndex
assert str(theIndex.noteIndex) == noteIndex assert str(theIndex._noteIndex) == noteIndex
assert str(theIndex.textCounts) == textCounts assert str(theIndex._textCounts) == textCounts
# Break the index and check that we notice # Break the index and check that we notice
assert not theIndex.indexBroken assert not theIndex.indexBroken
theIndex.tagIndex["Bod"].append("Stuff") # No longer len() == 4 theIndex._tagIndex["Bod"].append("Stuff") # No longer len() == 4
theIndex.checkIndex() theIndex.checkIndex()
assert theIndex.indexBroken assert theIndex.indexBroken
assert theIndex.loadIndex() assert theIndex.loadIndex()
assert not theIndex.indexBroken assert not theIndex.indexBroken
theIndex.refIndex["fb609cd8319dc"]["T000001"]["tags"].append("Stuff") # No longer len() == 3 theIndex._refIndex["fb609cd8319dc"]["T000001"]["tags"].append("Stuff") # No longer len() == 3
theIndex.checkIndex() theIndex.checkIndex()
assert theIndex.indexBroken assert theIndex.indexBroken
assert theIndex.loadIndex() assert theIndex.loadIndex()
assert not theIndex.indexBroken assert not theIndex.indexBroken
theIndex.novelIndex["7a992350f3eb6"]["T000001"]["Stuff"] = "" # No longer len(keys()) == 8 theIndex._novelIndex["7a992350f3eb6"]["T000001"]["Stuff"] = "" # No longer len(keys()) == 8
theIndex.checkIndex() theIndex.checkIndex()
assert theIndex.indexBroken assert theIndex.indexBroken
assert theIndex.loadIndex() assert theIndex.loadIndex()
assert not theIndex.indexBroken assert not theIndex.indexBroken
theIndex.noteIndex["4c4f28287af27"]["T000001"]["Stuff"] = "" # No longer len(keys()) == 8 theIndex._noteIndex["4c4f28287af27"]["T000001"]["Stuff"] = "" # No longer len(keys()) == 8
theIndex.checkIndex() theIndex.checkIndex()
assert theIndex.indexBroken assert theIndex.indexBroken
assert theIndex.loadIndex() assert theIndex.loadIndex()
assert not theIndex.indexBroken assert not theIndex.indexBroken
theIndex.textCounts["7a992350f3eb6"].append("Stuff") # No longer len() == 3 theIndex._textCounts["7a992350f3eb6"].append("Stuff") # No longer len() == 3
theIndex.checkIndex() theIndex.checkIndex()
assert theIndex.indexBroken assert theIndex.indexBroken
# Make the try/except trigger as well # Make the try/except trigger as well
assert theIndex.loadIndex() assert theIndex.loadIndex()
assert not theIndex.indexBroken assert not theIndex.indexBroken
theIndex.refIndex["fb609cd8319dc"]["T000001"] = {"tagssss": []} # Wrong key name theIndex._refIndex["fb609cd8319dc"]["T000001"] = {"tagssss": []} # Wrong key name
theIndex.checkIndex() theIndex.checkIndex()
assert theIndex.indexBroken assert theIndex.indexBroken
@@ -205,6 +205,10 @@ def testCoreIndex_CheckThese(nwMinimal, dummyGUI):
nItem = theProject.projTree[nHandle] nItem = theProject.projTree[nHandle]
cItem = theProject.projTree[cHandle] cItem = theProject.projTree[cHandle]
assert not theIndex.novelChangedSince(0)
assert not theIndex.notesChangedSince(0)
assert not theIndex.indexChangedSince(0)
assert theIndex.scanText(cHandle, ( assert theIndex.scanText(cHandle, (
"# Jane Smith\n" "# Jane Smith\n"
"@tag: Jane" "@tag: Jane"
@@ -213,8 +217,12 @@ def testCoreIndex_CheckThese(nwMinimal, dummyGUI):
"# Hello World!\n" "# Hello World!\n"
"@pov: Jane" "@pov: Jane"
)) ))
assert theIndex.tagIndex == {"Jane": [2, cHandle, "CHARACTER", "T000001"]} assert theIndex._tagIndex == {"Jane": [2, cHandle, "CHARACTER", "T000001"]}
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!" assert theIndex.getNovelData(nHandle, "T000001")["title"] == "Hello World!"
assert theIndex.novelChangedSince(0)
assert theIndex.notesChangedSince(0)
assert theIndex.indexChangedSince(0)
assert theIndex.checkThese([], cItem) == [] assert theIndex.checkThese([], cItem) == []
assert theIndex.checkThese(["@tag", "Jane"], cItem) == [True, True] assert theIndex.checkThese(["@tag", "Jane"], cItem) == [True, True]
@@ -285,8 +293,8 @@ def testCoreIndex_ScanText(nwMinimal, dummyGUI):
"This is a story about Jane Smith.\n\n" "This is a story about Jane Smith.\n\n"
"Well, not really.\n" "Well, not really.\n"
)) ))
assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle assert str(theIndex._tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!" assert theIndex.getNovelData(nHandle, "T000001")["title"] == "Hello World!"
# Check that title sections are indexed properly # Check that title sections are indexed properly
assert theIndex.scanText(nHandle, ( assert theIndex.scanText(nHandle, (
@@ -305,68 +313,68 @@ def testCoreIndex_ScanText(nwMinimal, dummyGUI):
"##### Title Five\n\n" # Not interpreted as a title, the hashes is counted as a word "##### Title Five\n\n" # Not interpreted as a title, the hashes is counted as a word
"Paragraph Five.\n\n" "Paragraph Five.\n\n"
)) ))
assert theIndex.refIndex[nHandle].get("T000000", None) is not None # Always there assert theIndex._refIndex[nHandle].get("T000000", None) is not None # Always there
assert theIndex.refIndex[nHandle].get("T000001", None) is not None # Heading 1 assert theIndex._refIndex[nHandle].get("T000001", None) is not None # Heading 1
assert theIndex.refIndex[nHandle].get("T000002", None) is None assert theIndex._refIndex[nHandle].get("T000002", None) is None
assert theIndex.refIndex[nHandle].get("T000003", None) is None assert theIndex._refIndex[nHandle].get("T000003", None) is None
assert theIndex.refIndex[nHandle].get("T000004", None) is None assert theIndex._refIndex[nHandle].get("T000004", None) is None
assert theIndex.refIndex[nHandle].get("T000005", None) is None assert theIndex._refIndex[nHandle].get("T000005", None) is None
assert theIndex.refIndex[nHandle].get("T000006", None) is None assert theIndex._refIndex[nHandle].get("T000006", None) is None
assert theIndex.refIndex[nHandle].get("T000007", None) is not None # Heading 2 assert theIndex._refIndex[nHandle].get("T000007", None) is not None # Heading 2
assert theIndex.refIndex[nHandle].get("T000008", None) is None assert theIndex._refIndex[nHandle].get("T000008", None) is None
assert theIndex.refIndex[nHandle].get("T000009", None) is None assert theIndex._refIndex[nHandle].get("T000009", None) is None
assert theIndex.refIndex[nHandle].get("T000010", None) is None assert theIndex._refIndex[nHandle].get("T000010", None) is None
assert theIndex.refIndex[nHandle].get("T000011", None) is None assert theIndex._refIndex[nHandle].get("T000011", None) is None
assert theIndex.refIndex[nHandle].get("T000012", None) is None assert theIndex._refIndex[nHandle].get("T000012", None) is None
assert theIndex.refIndex[nHandle].get("T000013", None) is not None # Heading 3 assert theIndex._refIndex[nHandle].get("T000013", None) is not None # Heading 3
assert theIndex.refIndex[nHandle].get("T000014", None) is None assert theIndex._refIndex[nHandle].get("T000014", None) is None
assert theIndex.refIndex[nHandle].get("T000015", None) is None assert theIndex._refIndex[nHandle].get("T000015", None) is None
assert theIndex.refIndex[nHandle].get("T000016", None) is None assert theIndex._refIndex[nHandle].get("T000016", None) is None
assert theIndex.refIndex[nHandle].get("T000017", None) is None assert theIndex._refIndex[nHandle].get("T000017", None) is None
assert theIndex.refIndex[nHandle].get("T000018", None) is None assert theIndex._refIndex[nHandle].get("T000018", None) is None
assert theIndex.refIndex[nHandle].get("T000019", None) is not None # Heading 4 assert theIndex._refIndex[nHandle].get("T000019", None) is not None # Heading 4
assert theIndex.refIndex[nHandle].get("T000020", None) is None assert theIndex._refIndex[nHandle].get("T000020", None) is None
assert theIndex.refIndex[nHandle].get("T000021", None) is None assert theIndex._refIndex[nHandle].get("T000021", None) is None
assert theIndex.refIndex[nHandle].get("T000022", None) is None assert theIndex._refIndex[nHandle].get("T000022", None) is None
assert theIndex.refIndex[nHandle].get("T000023", None) is None assert theIndex._refIndex[nHandle].get("T000023", None) is None
assert theIndex.refIndex[nHandle].get("T000024", None) is None assert theIndex._refIndex[nHandle].get("T000024", None) is None
assert theIndex.refIndex[nHandle].get("T000025", None) is None assert theIndex._refIndex[nHandle].get("T000025", None) is None
assert theIndex.refIndex[nHandle].get("T000026", None) is None assert theIndex._refIndex[nHandle].get("T000026", None) is None
assert theIndex.novelIndex[nHandle]["T000001"]["level"] == "H1" assert theIndex._novelIndex[nHandle]["T000001"]["level"] == "H1"
assert theIndex.novelIndex[nHandle]["T000007"]["level"] == "H2" assert theIndex._novelIndex[nHandle]["T000007"]["level"] == "H2"
assert theIndex.novelIndex[nHandle]["T000013"]["level"] == "H3" assert theIndex._novelIndex[nHandle]["T000013"]["level"] == "H3"
assert theIndex.novelIndex[nHandle]["T000019"]["level"] == "H4" assert theIndex._novelIndex[nHandle]["T000019"]["level"] == "H4"
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Title One" assert theIndex._novelIndex[nHandle]["T000001"]["title"] == "Title One"
assert theIndex.novelIndex[nHandle]["T000007"]["title"] == "Title Two" assert theIndex._novelIndex[nHandle]["T000007"]["title"] == "Title Two"
assert theIndex.novelIndex[nHandle]["T000013"]["title"] == "Title Three" assert theIndex._novelIndex[nHandle]["T000013"]["title"] == "Title Three"
assert theIndex.novelIndex[nHandle]["T000019"]["title"] == "Title Four" assert theIndex._novelIndex[nHandle]["T000019"]["title"] == "Title Four"
assert theIndex.novelIndex[nHandle]["T000001"]["layout"] == "SCENE" assert theIndex._novelIndex[nHandle]["T000001"]["layout"] == "SCENE"
assert theIndex.novelIndex[nHandle]["T000007"]["layout"] == "SCENE" assert theIndex._novelIndex[nHandle]["T000007"]["layout"] == "SCENE"
assert theIndex.novelIndex[nHandle]["T000013"]["layout"] == "SCENE" assert theIndex._novelIndex[nHandle]["T000013"]["layout"] == "SCENE"
assert theIndex.novelIndex[nHandle]["T000019"]["layout"] == "SCENE" assert theIndex._novelIndex[nHandle]["T000019"]["layout"] == "SCENE"
assert theIndex.novelIndex[nHandle]["T000001"]["synopsis"] == "Synopsis One." assert theIndex._novelIndex[nHandle]["T000001"]["synopsis"] == "Synopsis One."
assert theIndex.novelIndex[nHandle]["T000007"]["synopsis"] == "Synopsis Two." assert theIndex._novelIndex[nHandle]["T000007"]["synopsis"] == "Synopsis Two."
assert theIndex.novelIndex[nHandle]["T000013"]["synopsis"] == "Synopsis Three." assert theIndex._novelIndex[nHandle]["T000013"]["synopsis"] == "Synopsis Three."
assert theIndex.novelIndex[nHandle]["T000019"]["synopsis"] == "Synopsis Four." assert theIndex._novelIndex[nHandle]["T000019"]["synopsis"] == "Synopsis Four."
assert theIndex.novelIndex[nHandle]["T000001"]["cCount"] == 23 assert theIndex._novelIndex[nHandle]["T000001"]["cCount"] == 23
assert theIndex.novelIndex[nHandle]["T000007"]["cCount"] == 23 assert theIndex._novelIndex[nHandle]["T000007"]["cCount"] == 23
assert theIndex.novelIndex[nHandle]["T000013"]["cCount"] == 27 assert theIndex._novelIndex[nHandle]["T000013"]["cCount"] == 27
assert theIndex.novelIndex[nHandle]["T000019"]["cCount"] == 56 assert theIndex._novelIndex[nHandle]["T000019"]["cCount"] == 56
assert theIndex.novelIndex[nHandle]["T000001"]["wCount"] == 4 assert theIndex._novelIndex[nHandle]["T000001"]["wCount"] == 4
assert theIndex.novelIndex[nHandle]["T000007"]["wCount"] == 4 assert theIndex._novelIndex[nHandle]["T000007"]["wCount"] == 4
assert theIndex.novelIndex[nHandle]["T000013"]["wCount"] == 4 assert theIndex._novelIndex[nHandle]["T000013"]["wCount"] == 4
assert theIndex.novelIndex[nHandle]["T000019"]["wCount"] == 9 assert theIndex._novelIndex[nHandle]["T000019"]["wCount"] == 9
assert theIndex.novelIndex[nHandle]["T000001"]["pCount"] == 1 assert theIndex._novelIndex[nHandle]["T000001"]["pCount"] == 1
assert theIndex.novelIndex[nHandle]["T000007"]["pCount"] == 1 assert theIndex._novelIndex[nHandle]["T000007"]["pCount"] == 1
assert theIndex.novelIndex[nHandle]["T000013"]["pCount"] == 1 assert theIndex._novelIndex[nHandle]["T000013"]["pCount"] == 1
assert theIndex.novelIndex[nHandle]["T000019"]["pCount"] == 3 assert theIndex._novelIndex[nHandle]["T000019"]["pCount"] == 3
assert theIndex.scanText(cHandle, ( assert theIndex.scanText(cHandle, (
"# Title One\n\n" "# Title One\n\n"
@@ -374,22 +382,22 @@ def testCoreIndex_ScanText(nwMinimal, dummyGUI):
"% synopsis: Synopsis One.\n\n" "% synopsis: Synopsis One.\n\n"
"Paragraph One.\n\n" "Paragraph One.\n\n"
)) ))
assert theIndex.refIndex[cHandle].get("T000000", None) is not None assert theIndex._refIndex[cHandle].get("T000000", None) is not None
assert theIndex.refIndex[cHandle].get("T000001", None) is not None assert theIndex._refIndex[cHandle].get("T000001", None) is not None
assert theIndex.refIndex[cHandle].get("T000002", None) is None assert theIndex._refIndex[cHandle].get("T000002", None) is None
assert theIndex.refIndex[cHandle].get("T000003", None) is None assert theIndex._refIndex[cHandle].get("T000003", None) is None
assert theIndex.refIndex[cHandle].get("T000004", None) is None assert theIndex._refIndex[cHandle].get("T000004", None) is None
assert theIndex.refIndex[cHandle].get("T000005", None) is None assert theIndex._refIndex[cHandle].get("T000005", None) is None
assert theIndex.refIndex[cHandle].get("T000006", None) is None assert theIndex._refIndex[cHandle].get("T000006", None) is None
assert theIndex.refIndex[cHandle].get("T000007", None) is None assert theIndex._refIndex[cHandle].get("T000007", None) is None
assert theIndex.noteIndex[cHandle]["T000001"]["level"] == "H1" assert theIndex._noteIndex[cHandle]["T000001"]["level"] == "H1"
assert theIndex.noteIndex[cHandle]["T000001"]["title"] == "Title One" assert theIndex._noteIndex[cHandle]["T000001"]["title"] == "Title One"
assert theIndex.noteIndex[cHandle]["T000001"]["layout"] == "NOTE" assert theIndex._noteIndex[cHandle]["T000001"]["layout"] == "NOTE"
assert theIndex.noteIndex[cHandle]["T000001"]["synopsis"] == "Synopsis One." assert theIndex._noteIndex[cHandle]["T000001"]["synopsis"] == "Synopsis One."
assert theIndex.noteIndex[cHandle]["T000001"]["cCount"] == 23 assert theIndex._noteIndex[cHandle]["T000001"]["cCount"] == 23
assert theIndex.noteIndex[cHandle]["T000001"]["wCount"] == 4 assert theIndex._noteIndex[cHandle]["T000001"]["wCount"] == 4
assert theIndex.noteIndex[cHandle]["T000001"]["pCount"] == 1 assert theIndex._noteIndex[cHandle]["T000001"]["pCount"] == 1
assert theIndex.scanText(sHandle, ( assert theIndex.scanText(sHandle, (
"# Title One\n\n" "# Title One\n\n"
@@ -399,7 +407,7 @@ def testCoreIndex_ScanText(nwMinimal, dummyGUI):
"% synopsis: Synopsis One.\n\n" "% synopsis: Synopsis One.\n\n"
"Paragraph One.\n\n" "Paragraph One.\n\n"
)) ))
assert theIndex.refIndex[sHandle]["T000001"]["tags"] == ( assert theIndex._refIndex[sHandle]["T000001"]["tags"] == (
[[3, "@pov", "One"], [5, "@char", "Two"]] [[3, "@pov", "One"], [5, "@char", "Two"]]
) )
@@ -419,6 +427,9 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
assert theIndex.getNovelData("", "") is None
assert theIndex.getNovelData("a508bb932959c", "") is None
assert theIndex.scanText(cHandle, ( assert theIndex.scanText(cHandle, (
"# Jane Smith\n" "# Jane Smith\n"
"@tag: Jane\n" "@tag: Jane\n"
@@ -433,13 +444,32 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
)) ))
# The novel structure should contain the pointer to the novel file header # The novel structure should contain the pointer to the novel file header
assert theIndex.getNovelStructure() == ["%s:T000001" % nHandle] theKeys = []
for aKey, _, _, _ in theIndex.novelStructure():
theKeys.append(aKey)
assert theKeys == ["%s:T000001" % nHandle]
# Check that excluded files can be skipped # Check that excluded files can be skipped
theProject.projTree[nHandle].setExported(False) theProject.projTree[nHandle].setExported(False)
assert theIndex.getNovelStructure(skipExcluded=False) == ["%s:T000001" % nHandle]
assert theIndex.getNovelStructure(skipExcluded=True) == [] theKeys = []
assert theIndex.getNovelStructure() == [] for aKey, _, _, _ in theIndex.novelStructure(skipExcluded=False):
theKeys.append(aKey)
assert theKeys == ["%s:T000001" % nHandle]
theKeys = []
for aKey, _, _, _ in theIndex.novelStructure(skipExcluded=True):
theKeys.append(aKey)
assert theKeys == []
theKeys = []
for aKey, _, _, _ in theIndex.novelStructure():
theKeys.append(aKey)
assert theKeys == []
# The novel file should have the correct counts # The novel file should have the correct counts
cC, wC, pC = theIndex.getCounts(nHandle) cC, wC, pC = theIndex.getCounts(nHandle)
+4 -4
View File
@@ -26,11 +26,11 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
assert nwGUI.openProject(nwLipsum) assert nwGUI.openProject(nwLipsum)
# Rebuild the index as it isn't automatically copied # Rebuild the index as it isn't automatically copied
assert nwGUI.theIndex.tagIndex == {} assert nwGUI.theIndex._tagIndex == {}
assert nwGUI.theIndex.refIndex == {} assert nwGUI.theIndex._refIndex == {}
nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger) nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger)
assert nwGUI.theIndex.tagIndex != {} assert nwGUI.theIndex._tagIndex != {}
assert nwGUI.theIndex.refIndex != {} assert nwGUI.theIndex._refIndex != {}
# Select a document in the project tree # Select a document in the project tree
assert nwGUI.treeView.setSelectedHandle("88243afbe5ed8") assert nwGUI.treeView.setSelectedHandle("88243afbe5ed8")
+147
View File
@@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-
"""novelWriter Main GUI Project Tree Class Tester
"""
import pytest
import os
from tools import writeFile
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox
@pytest.mark.gui
def testGuiNovelTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal):
"""Test navigating the novel tree.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes)
nwGUI.openProject(nwMinimal)
nwGUI.theProject.projTree.setSeed(42)
nwTree = nwGUI.novelView
##
# Show/Hide Scrollbars
##
nwTree.mainConf.hideVScroll = True
nwTree.mainConf.hideHScroll = True
nwTree.initTree()
assert not nwTree.verticalScrollBar().isVisible()
assert not nwTree.horizontalScrollBar().isVisible()
nwTree.mainConf.hideVScroll = False
nwTree.mainConf.hideHScroll = False
nwTree.initTree()
assert nwTree.verticalScrollBar().isEnabled()
assert nwTree.horizontalScrollBar().isEnabled()
##
# Populate Tree
##
nwGUI.projTabs.setCurrentIndex(nwGUI.idxNovelView)
# The tree should be empty as there is no index
assert nwTree.topLevelItemCount() == 0
nwGUI.rebuildIndex()
nwTree._populateTree()
assert nwTree.topLevelItemCount() == 1
# Rebuild should preserve selection
topItem = nwTree.topLevelItem(0)
assert not topItem.isSelected()
topItem.setSelected(True)
assert nwTree.selectedItems()[0] == topItem
assert nwTree.getSelectedHandle() == "a35baf2e93843"
nwTree.refreshTree()
assert nwTree.topLevelItem(0).isSelected()
##
# Open Items
##
# Clear selection
nwTree.clearSelection()
scItem = nwTree.topLevelItem(0).child(0).child(0)
scItem.setSelected(True)
assert scItem.isSelected()
# Clear selection with mouse
vPort = nwTree.viewport()
qtbot.mouseClick(vPort, Qt.LeftButton, pos=vPort.rect().center(), delay=10)
assert not scItem.isSelected()
# Double-click item
scItem.setSelected(True)
assert scItem.isSelected()
assert nwGUI.docEditor.theHandle is None
nwTree._treeDoubleClick(scItem, 0)
assert nwGUI.docEditor.theHandle == "8c659a11cd429"
# Open item with middle mouse button
scItem.setSelected(True)
assert scItem.isSelected()
assert nwGUI.docViewer.theHandle is None
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=vPort.rect().center(), delay=10)
assert nwGUI.docViewer.theHandle is None
scRect = nwTree.visualItemRect(scItem)
oldData = scItem.data(nwTree.C_TITLE, Qt.UserRole)
scItem.setData(nwTree.C_TITLE, Qt.UserRole, (None, "", ""))
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10)
assert nwGUI.docViewer.theHandle is None
scItem.setData(nwTree.C_TITLE, Qt.UserRole, oldData)
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10)
assert nwGUI.docViewer.theHandle == "8c659a11cd429"
##
# Populate Tree
##
# Add weird titles to first file to check hnadling of non-standard
# order of title levels.
writeFile(os.path.join(nwMinimal, "content", "a35baf2e93843.nwd"), (
"#### Section wo/Scene\n\n"
"### Scene wo/Chapter\n\n"
"## Chapter wo/Title\n\n"
"# Title\n\n"
"#### Section w/Title, wo/Scene\n\n"
"### Scene w/Title, wo/Chapter\n\n"
"## Chapter\n\n"
"#### Section w/Chapter, wo/Scene\n\n"
"### Scene\n\n"
"#### Section\n\n"
))
nwGUI.rebuildIndex()
nwTree._populateTree()
assert nwTree.topLevelItem(0).text(nwTree.C_TITLE) == "Section wo/Scene"
assert nwTree.topLevelItem(1).text(nwTree.C_TITLE) == "Scene wo/Chapter"
assert nwTree.topLevelItem(2).text(nwTree.C_TITLE) == "Chapter wo/Title"
assert nwTree.topLevelItem(3).text(nwTree.C_TITLE) == "Title"
tTitle = nwTree.topLevelItem(3)
assert tTitle.child(0).text(nwTree.C_TITLE) == "Section w/Title, wo/Scene"
assert tTitle.child(1).text(nwTree.C_TITLE) == "Scene w/Title, wo/Chapter"
assert tTitle.child(2).text(nwTree.C_TITLE) == "Chapter"
tChap = tTitle.child(2)
assert tChap.child(0).text(nwTree.C_TITLE) == "Section w/Chapter, wo/Scene"
assert tChap.child(1).text(nwTree.C_TITLE) == "Scene"
tScene = tChap.child(1)
assert tScene.child(0).text(nwTree.C_TITLE) == "Section"
##
# Close
##
# qtbot.stopForInteraction()
nwGUI.closeProject()
# END Test testGuiNovelTree_TreeItems
+1 -1
View File
@@ -24,7 +24,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
nwGUI.mainConf.lastPath = nwLipsum nwGUI.mainConf.lastPath = nwLipsum
nwGUI.rebuildIndex() nwGUI.rebuildIndex()
nwGUI.tabWidget.setCurrentIndex(nwGUI.idxTabProj) nwGUI.mainTabs.setCurrentIndex(nwGUI.idxTabProj)
assert nwGUI.projView.topLevelItemCount() > 0 assert nwGUI.projView.topLevelItemCount() > 0
+5 -4
View File
@@ -220,10 +220,11 @@ def testGuiPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir):
compFile = os.path.join(refDir, "guiPreferences_novelwriter.conf") compFile = os.path.join(refDir, "guiPreferences_novelwriter.conf")
copyfile(projFile, testFile) copyfile(projFile, testFile)
ignoreLines = [ ignoreLines = [
2, # Timestamp 2, # Timestamp
9, # Release Notes 9, # Release Notes
12, 13, 14, 15, 16, 17, 18, # Window sizes 12, 13, 14, 15, # Window sizes
7, 28, # Fonts (depends on system default) 16, 17, 18, 19, # Window sizes
7, 29, # Fonts (depends on system default)
] ]
assert cmpFiles(testFile, compFile, ignoreLines) assert cmpFiles(testFile, compFile, ignoreLines)