@@ -2,6 +2,17 @@
|
||||
|
||||
## 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]
|
||||
|
||||
+21
-8
@@ -95,14 +95,15 @@ class Config:
|
||||
self.lastNotes = "" # The latest release notes that have been shown
|
||||
|
||||
## Sizes
|
||||
self.winGeometry = [1200, 650]
|
||||
self.treeColWidth = [200, 50, 30]
|
||||
self.projColWidth = [200, 60, 140]
|
||||
self.mainPanePos = [300, 800]
|
||||
self.docPanePos = [400, 400]
|
||||
self.viewPanePos = [500, 150]
|
||||
self.outlnPanePos = [500, 150]
|
||||
self.isFullScreen = False
|
||||
self.winGeometry = [1200, 650]
|
||||
self.treeColWidth = [200, 50, 30]
|
||||
self.novelColWidth = [200, 50]
|
||||
self.projColWidth = [200, 60, 140]
|
||||
self.mainPanePos = [300, 800]
|
||||
self.docPanePos = [400, 400]
|
||||
self.viewPanePos = [500, 150]
|
||||
self.outlnPanePos = [500, 150]
|
||||
self.isFullScreen = False
|
||||
|
||||
## Features
|
||||
self.hideVScroll = False # Hide vertical scroll bars on main widgets
|
||||
@@ -395,6 +396,9 @@ class Config:
|
||||
self.treeColWidth = self._parseLine(
|
||||
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(
|
||||
cnfParse, cnfSec, "projcols", self.CNF_I_LST, self.projColWidth
|
||||
)
|
||||
@@ -597,6 +601,7 @@ class Config:
|
||||
cnfParse.add_section(cnfSec)
|
||||
cnfParse.set(cnfSec, "geometry", self._packList(self.winGeometry))
|
||||
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, "mainpane", self._packList(self.mainPanePos))
|
||||
cnfParse.set(cnfSec, "docpane", self._packList(self.docPanePos))
|
||||
@@ -816,6 +821,11 @@ class Config:
|
||||
self.confChanged = 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):
|
||||
self.projColWidth = [int(x/self.guiScale) for x in colWidths]
|
||||
self.confChanged = True
|
||||
@@ -872,6 +882,9 @@ class Config:
|
||||
def getTreeColWidths(self):
|
||||
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):
|
||||
return [int(x*self.guiScale) for x in self.projColWidth]
|
||||
|
||||
|
||||
+147
-128
@@ -51,18 +51,16 @@ class NWIndex():
|
||||
self.indexBroken = False
|
||||
|
||||
# Indices
|
||||
self.tagIndex = None
|
||||
self.refIndex = None
|
||||
self.novelIndex = None
|
||||
self.noteIndex = None
|
||||
self.textCounts = None
|
||||
self._tagIndex = {}
|
||||
self._refIndex = {}
|
||||
self._novelIndex = {}
|
||||
self._noteIndex = {}
|
||||
self._textCounts = {}
|
||||
|
||||
# TimeStamps
|
||||
self.timeNovel = 0
|
||||
self.timeNote = 0
|
||||
self.timeIndex = 0
|
||||
|
||||
self.clearIndex()
|
||||
self._timeNovel = 0
|
||||
self._timeNotes = 0
|
||||
self._timeIndex = 0
|
||||
|
||||
return
|
||||
|
||||
@@ -73,14 +71,14 @@ class NWIndex():
|
||||
def clearIndex(self):
|
||||
"""Clear the index dictionaries and time stamps.
|
||||
"""
|
||||
self.tagIndex = {}
|
||||
self.refIndex = {}
|
||||
self.novelIndex = {}
|
||||
self.noteIndex = {}
|
||||
self.textCounts = {}
|
||||
self.timeNovel = 0
|
||||
self.timeNote = 0
|
||||
self.timeIndex = 0
|
||||
self._tagIndex = {}
|
||||
self._refIndex = {}
|
||||
self._novelIndex = {}
|
||||
self._noteIndex = {}
|
||||
self._textCounts = {}
|
||||
self._timeNovel = 0
|
||||
self._timeNotes = 0
|
||||
self._timeIndex = 0
|
||||
return
|
||||
|
||||
def deleteHandle(self, tHandle):
|
||||
@@ -89,17 +87,17 @@ class NWIndex():
|
||||
logger.debug("Removing item %s from the index" % tHandle)
|
||||
|
||||
delTags = []
|
||||
for tTag in self.tagIndex:
|
||||
if self.tagIndex[tTag][1] == tHandle:
|
||||
for tTag in self._tagIndex:
|
||||
if self._tagIndex[tTag][1] == tHandle:
|
||||
delTags.append(tTag)
|
||||
|
||||
for tTag in delTags:
|
||||
self.tagIndex.pop(tTag, None)
|
||||
self._tagIndex.pop(tTag, None)
|
||||
|
||||
self.refIndex.pop(tHandle, None)
|
||||
self.novelIndex.pop(tHandle, None)
|
||||
self.noteIndex.pop(tHandle, None)
|
||||
self.textCounts.pop(tHandle, None)
|
||||
self._refIndex.pop(tHandle, None)
|
||||
self._novelIndex.pop(tHandle, None)
|
||||
self._noteIndex.pop(tHandle, None)
|
||||
self._textCounts.pop(tHandle, None)
|
||||
|
||||
return
|
||||
|
||||
@@ -123,6 +121,21 @@ class NWIndex():
|
||||
|
||||
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
|
||||
##
|
||||
@@ -143,16 +156,16 @@ class NWIndex():
|
||||
logger.error(str(e))
|
||||
return False
|
||||
|
||||
self.tagIndex = theData.get("tagIndex", {})
|
||||
self.refIndex = theData.get("refIndex", {})
|
||||
self.novelIndex = theData.get("novelIndex", {})
|
||||
self.noteIndex = theData.get("noteIndex", {})
|
||||
self.textCounts = theData.get("textCounts", {})
|
||||
self._tagIndex = theData.get("tagIndex", {})
|
||||
self._refIndex = theData.get("refIndex", {})
|
||||
self._novelIndex = theData.get("novelIndex", {})
|
||||
self._noteIndex = theData.get("noteIndex", {})
|
||||
self._textCounts = theData.get("textCounts", {})
|
||||
|
||||
nowTime = round(time())
|
||||
self.timeNovel = nowTime
|
||||
self.timeNote = nowTime
|
||||
self.timeIndex = nowTime
|
||||
self._timeNovel = nowTime
|
||||
self._timeNotes = nowTime
|
||||
self._timeIndex = nowTime
|
||||
|
||||
self.checkIndex()
|
||||
|
||||
@@ -168,11 +181,11 @@ class NWIndex():
|
||||
try:
|
||||
with open(indexFile, mode="w+", encoding="utf8") as outFile:
|
||||
json.dump({
|
||||
"tagIndex" : self.tagIndex,
|
||||
"refIndex" : self.refIndex,
|
||||
"novelIndex" : self.novelIndex,
|
||||
"noteIndex" : self.noteIndex,
|
||||
"textCounts" : self.textCounts,
|
||||
"tagIndex" : self._tagIndex,
|
||||
"refIndex" : self._refIndex,
|
||||
"novelIndex" : self._novelIndex,
|
||||
"noteIndex" : self._noteIndex,
|
||||
"textCounts" : self._textCounts,
|
||||
}, outFile, indent=2)
|
||||
except Exception as e:
|
||||
logger.error("Failed to save index file")
|
||||
@@ -189,28 +202,28 @@ class NWIndex():
|
||||
self.indexBroken = False
|
||||
|
||||
try:
|
||||
for tTag in self.tagIndex:
|
||||
if len(self.tagIndex[tTag]) != 4:
|
||||
for tTag in self._tagIndex:
|
||||
if len(self._tagIndex[tTag]) != 4:
|
||||
self.indexBroken = True
|
||||
|
||||
for tHandle in self.refIndex:
|
||||
for sTitle in self.refIndex[tHandle]:
|
||||
for tEntry in self.refIndex[tHandle][sTitle]["tags"]:
|
||||
for tHandle in self._refIndex:
|
||||
for sTitle in self._refIndex[tHandle]:
|
||||
for tEntry in self._refIndex[tHandle][sTitle]["tags"]:
|
||||
if len(tEntry) != 3:
|
||||
self.indexBroken = True
|
||||
|
||||
for tHandle in self.novelIndex:
|
||||
for sLine in self.novelIndex[tHandle]:
|
||||
if len(self.novelIndex[tHandle][sLine].keys()) != 8:
|
||||
for tHandle in self._novelIndex:
|
||||
for sLine in self._novelIndex[tHandle]:
|
||||
if len(self._novelIndex[tHandle][sLine].keys()) != 8:
|
||||
self.indexBroken = True
|
||||
|
||||
for tHandle in self.noteIndex:
|
||||
for sLine in self.noteIndex[tHandle]:
|
||||
if len(self.noteIndex[tHandle][sLine].keys()) != 8:
|
||||
for tHandle in self._noteIndex:
|
||||
for sLine in self._noteIndex[tHandle]:
|
||||
if len(self._noteIndex[tHandle][sLine].keys()) != 8:
|
||||
self.indexBroken = True
|
||||
|
||||
for tHandle in self.textCounts:
|
||||
if len(self.textCounts[tHandle]) != 3:
|
||||
for tHandle in self._textCounts:
|
||||
if len(self._textCounts[tHandle]) != 3:
|
||||
self.indexBroken = True
|
||||
|
||||
except Exception:
|
||||
@@ -254,7 +267,7 @@ class NWIndex():
|
||||
|
||||
# Run word counter for the whole text
|
||||
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 self.theProject.projTree.isTrashRoot(theItem.itemParent):
|
||||
@@ -271,25 +284,25 @@ class NWIndex():
|
||||
|
||||
# Check file type, and reset its old index
|
||||
# Also add a dummy entry T000000 in case the file has no title
|
||||
self.refIndex[tHandle] = {}
|
||||
self.refIndex[tHandle]["T000000"] = {
|
||||
self._refIndex[tHandle] = {}
|
||||
self._refIndex[tHandle]["T000000"] = {
|
||||
"tags" : [],
|
||||
"updated" : round(time()),
|
||||
}
|
||||
if itemLayout == nwItemLayout.NOTE:
|
||||
self.noteIndex[tHandle] = {}
|
||||
self._noteIndex[tHandle] = {}
|
||||
isNovel = False
|
||||
else:
|
||||
self.novelIndex[tHandle] = {}
|
||||
self._novelIndex[tHandle] = {}
|
||||
isNovel = True
|
||||
|
||||
# Also clear references to file in tag index
|
||||
clearTags = []
|
||||
for aTag in self.tagIndex:
|
||||
if self.tagIndex[aTag][1] == tHandle:
|
||||
for aTag in self._tagIndex:
|
||||
if self._tagIndex[aTag][1] == tHandle:
|
||||
clearTags.append(aTag)
|
||||
for aTag in clearTags:
|
||||
self.tagIndex.pop(aTag)
|
||||
self._tagIndex.pop(aTag)
|
||||
|
||||
nLine = 0
|
||||
nTitle = 0
|
||||
@@ -330,11 +343,11 @@ class NWIndex():
|
||||
|
||||
# Update timestamps for index changes
|
||||
nowTime = round(time())
|
||||
self.timeIndex = nowTime
|
||||
self._timeIndex = nowTime
|
||||
if isNovel:
|
||||
self.timeNovel = nowTime
|
||||
self._timeNovel = nowTime
|
||||
else:
|
||||
self.timeNote = nowTime
|
||||
self._timeNotes = nowTime
|
||||
|
||||
return True
|
||||
|
||||
@@ -362,7 +375,7 @@ class NWIndex():
|
||||
return False
|
||||
|
||||
sTitle = "T%06d" % nLine
|
||||
self.refIndex[tHandle][sTitle] = {
|
||||
self._refIndex[tHandle][sTitle] = {
|
||||
"tags" : [],
|
||||
"updated" : round(time()),
|
||||
}
|
||||
@@ -379,11 +392,11 @@ class NWIndex():
|
||||
|
||||
if hText != "":
|
||||
if isNovel:
|
||||
if tHandle in self.novelIndex:
|
||||
self.novelIndex[tHandle][sTitle] = theData
|
||||
if tHandle in self._novelIndex:
|
||||
self._novelIndex[tHandle][sTitle] = theData
|
||||
else:
|
||||
if tHandle in self.noteIndex:
|
||||
self.noteIndex[tHandle][sTitle] = theData
|
||||
if tHandle in self._noteIndex:
|
||||
self._noteIndex[tHandle][sTitle] = theData
|
||||
|
||||
return True
|
||||
|
||||
@@ -393,19 +406,19 @@ class NWIndex():
|
||||
cC, wC, pC = countWords(theText)
|
||||
sTitle = "T%06d" % nTitle
|
||||
if isNovel:
|
||||
if tHandle in self.novelIndex:
|
||||
if sTitle in self.novelIndex[tHandle]:
|
||||
self.novelIndex[tHandle][sTitle]["cCount"] = cC
|
||||
self.novelIndex[tHandle][sTitle]["wCount"] = wC
|
||||
self.novelIndex[tHandle][sTitle]["pCount"] = pC
|
||||
self.novelIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
if tHandle in self._novelIndex:
|
||||
if sTitle in self._novelIndex[tHandle]:
|
||||
self._novelIndex[tHandle][sTitle]["cCount"] = cC
|
||||
self._novelIndex[tHandle][sTitle]["wCount"] = wC
|
||||
self._novelIndex[tHandle][sTitle]["pCount"] = pC
|
||||
self._novelIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
else:
|
||||
if tHandle in self.noteIndex:
|
||||
if sTitle in self.noteIndex[tHandle]:
|
||||
self.noteIndex[tHandle][sTitle]["cCount"] = cC
|
||||
self.noteIndex[tHandle][sTitle]["wCount"] = wC
|
||||
self.noteIndex[tHandle][sTitle]["pCount"] = pC
|
||||
self.noteIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
if tHandle in self._noteIndex:
|
||||
if sTitle in self._noteIndex[tHandle]:
|
||||
self._noteIndex[tHandle][sTitle]["cCount"] = cC
|
||||
self._noteIndex[tHandle][sTitle]["wCount"] = wC
|
||||
self._noteIndex[tHandle][sTitle]["pCount"] = pC
|
||||
self._noteIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
return
|
||||
|
||||
def _indexSynopsis(self, tHandle, isNovel, theText, nTitle):
|
||||
@@ -413,15 +426,15 @@ class NWIndex():
|
||||
"""
|
||||
sTitle = "T%06d" % nTitle
|
||||
if isNovel:
|
||||
if tHandle in self.novelIndex:
|
||||
if sTitle in self.novelIndex[tHandle]:
|
||||
self.novelIndex[tHandle][sTitle]["synopsis"] = theText
|
||||
self.novelIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
if tHandle in self._novelIndex:
|
||||
if sTitle in self._novelIndex[tHandle]:
|
||||
self._novelIndex[tHandle][sTitle]["synopsis"] = theText
|
||||
self._novelIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
else:
|
||||
if tHandle in self.noteIndex:
|
||||
if sTitle in self.noteIndex[tHandle]:
|
||||
self.noteIndex[tHandle][sTitle]["synopsis"] = theText
|
||||
self.noteIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
if tHandle in self._noteIndex:
|
||||
if sTitle in self._noteIndex[tHandle]:
|
||||
self._noteIndex[tHandle][sTitle]["synopsis"] = theText
|
||||
self._noteIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
return
|
||||
|
||||
def _indexNoteRef(self, tHandle, aLine, nLine, nTitle):
|
||||
@@ -433,9 +446,9 @@ class NWIndex():
|
||||
return False
|
||||
|
||||
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:]:
|
||||
self.refIndex[tHandle][sTitle]["tags"].append([nLine, theBits[0], aVal])
|
||||
self._refIndex[tHandle][sTitle]["tags"].append([nLine, theBits[0], aVal])
|
||||
|
||||
return True
|
||||
|
||||
@@ -448,7 +461,7 @@ class NWIndex():
|
||||
|
||||
if theBits[0] == nwKeyWords.TAG_KEY:
|
||||
sTitle = "T%06d" % nTitle
|
||||
self.tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle]
|
||||
self._tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle]
|
||||
|
||||
return True
|
||||
|
||||
@@ -512,8 +525,8 @@ class NWIndex():
|
||||
# is ignored
|
||||
if theBits[0] == nwKeyWords.TAG_KEY and nBits > 1:
|
||||
isGood[0] = True
|
||||
if theBits[1] in self.tagIndex:
|
||||
if self.tagIndex[theBits[1]][1] == tItem.itemHandle:
|
||||
if theBits[1] in self._tagIndex:
|
||||
if self._tagIndex[theBits[1]][1] == tItem.itemHandle:
|
||||
isGood[1] = True
|
||||
else:
|
||||
isGood[1] = False
|
||||
@@ -523,8 +536,8 @@ class NWIndex():
|
||||
|
||||
# If we're still here, we better check that the references exist
|
||||
for n in range(1, nBits):
|
||||
if theBits[n] in self.tagIndex:
|
||||
isGood[n] = nwKeyWords.KEY_CLASS[theBits[0]].name == self.tagIndex[theBits[n]][2]
|
||||
if theBits[n] in self._tagIndex:
|
||||
isGood[n] = nwKeyWords.KEY_CLASS[theBits[0]].name == self._tagIndex[theBits[n]][2]
|
||||
|
||||
return isGood
|
||||
|
||||
@@ -532,23 +545,21 @@ class NWIndex():
|
||||
# Extract Data
|
||||
##
|
||||
|
||||
def getNovelStructure(self, skipExcluded=True):
|
||||
"""Builds a list of all titles in the novel, in the correct
|
||||
order as they appear in the tree view and in the respective
|
||||
document files, but skipping all note files.
|
||||
def novelStructure(self, skipExcluded=True):
|
||||
"""Iterate over all titles in the novel, in the correct order as
|
||||
they appear in the tree view and in the respective document
|
||||
files, but skipping all note files.
|
||||
"""
|
||||
theStructure = []
|
||||
for tItem in self.theProject.projTree:
|
||||
if tItem is not None:
|
||||
if not tItem.isExported and skipExcluded:
|
||||
continue
|
||||
tHandle = tItem.itemHandle
|
||||
if tHandle not in self.novelIndex:
|
||||
if tHandle not in self._novelIndex:
|
||||
continue
|
||||
for sTitle in sorted(self.novelIndex[tHandle].keys()):
|
||||
theStructure.append("%s:%s" % (tHandle, sTitle))
|
||||
|
||||
return theStructure
|
||||
for sTitle in sorted(self._novelIndex[tHandle]):
|
||||
tKey = "%s:%s" % (tHandle, sTitle)
|
||||
yield tKey, tHandle, sTitle, self._novelIndex[tHandle][sTitle]
|
||||
|
||||
def getCounts(self, tHandle, sTitle=None):
|
||||
"""Returns the counts for a file, or a section of a file
|
||||
@@ -559,21 +570,21 @@ class NWIndex():
|
||||
pC = 0
|
||||
|
||||
if sTitle is None:
|
||||
if tHandle in self.textCounts:
|
||||
cC = self.textCounts[tHandle][0]
|
||||
wC = self.textCounts[tHandle][1]
|
||||
pC = self.textCounts[tHandle][2]
|
||||
if tHandle in self._textCounts:
|
||||
cC = self._textCounts[tHandle][0]
|
||||
wC = self._textCounts[tHandle][1]
|
||||
pC = self._textCounts[tHandle][2]
|
||||
else:
|
||||
if tHandle in self.novelIndex:
|
||||
if sTitle in self.novelIndex[tHandle]:
|
||||
cC = self.novelIndex[tHandle][sTitle]["cCount"]
|
||||
wC = self.novelIndex[tHandle][sTitle]["wCount"]
|
||||
pC = self.novelIndex[tHandle][sTitle]["pCount"]
|
||||
elif tHandle in self.noteIndex:
|
||||
if sTitle in self.noteIndex[tHandle]:
|
||||
cC = self.noteIndex[tHandle][sTitle]["cCount"]
|
||||
wC = self.noteIndex[tHandle][sTitle]["wCount"]
|
||||
pC = self.noteIndex[tHandle][sTitle]["pCount"]
|
||||
if tHandle in self._novelIndex:
|
||||
if sTitle in self._novelIndex[tHandle]:
|
||||
cC = self._novelIndex[tHandle][sTitle]["cCount"]
|
||||
wC = self._novelIndex[tHandle][sTitle]["wCount"]
|
||||
pC = self._novelIndex[tHandle][sTitle]["pCount"]
|
||||
elif tHandle in self._noteIndex:
|
||||
if sTitle in self._noteIndex[tHandle]:
|
||||
cC = self._noteIndex[tHandle][sTitle]["cCount"]
|
||||
wC = self._noteIndex[tHandle][sTitle]["wCount"]
|
||||
pC = self._noteIndex[tHandle][sTitle]["pCount"]
|
||||
|
||||
return cC, wC, pC
|
||||
|
||||
@@ -585,16 +596,24 @@ class NWIndex():
|
||||
for tKey in nwKeyWords.KEY_CLASS:
|
||||
theRefs[tKey] = []
|
||||
|
||||
if tHandle not in self.refIndex:
|
||||
if tHandle not in self._refIndex:
|
||||
return theRefs
|
||||
|
||||
for refTitle in self.refIndex[tHandle]:
|
||||
for aTag in self.refIndex[tHandle][refTitle].get("tags", []):
|
||||
for refTitle in self._refIndex[tHandle]:
|
||||
for aTag in self._refIndex[tHandle][refTitle].get("tags", []):
|
||||
if len(aTag) == 3 and (sTitle is None or sTitle == refTitle):
|
||||
theRefs[aTag[1]].append(aTag[2])
|
||||
|
||||
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):
|
||||
"""Build a list of files referring back to our file, specified
|
||||
by tHandle.
|
||||
@@ -604,14 +623,14 @@ class NWIndex():
|
||||
return theRefs
|
||||
|
||||
theTags = set()
|
||||
for tTag in self.tagIndex:
|
||||
if tHandle == self.tagIndex[tTag][1]:
|
||||
for tTag in self._tagIndex:
|
||||
if tHandle == self._tagIndex[tTag][1]:
|
||||
theTags.add(tTag)
|
||||
|
||||
if theTags:
|
||||
for tHandle in self.refIndex:
|
||||
for sTitle in self.refIndex[tHandle]:
|
||||
for _, _, tTag in self.refIndex[tHandle][sTitle]["tags"]:
|
||||
for tHandle in self._refIndex:
|
||||
for sTitle in self._refIndex[tHandle]:
|
||||
for _, _, tTag in self._refIndex[tHandle][sTitle]["tags"]:
|
||||
if tTag in theTags and tHandle not in theRefs:
|
||||
theRefs[tHandle] = sTitle
|
||||
|
||||
@@ -620,8 +639,8 @@ class NWIndex():
|
||||
def getTagSource(self, theTag):
|
||||
"""Return the source location of a given tag.
|
||||
"""
|
||||
if theTag in self.tagIndex:
|
||||
theRef = self.tagIndex[theTag]
|
||||
if theTag in self._tagIndex:
|
||||
theRef = self._tagIndex[theTag]
|
||||
if len(theRef) == 4:
|
||||
return theRef[1], theRef[0], theRef[3]
|
||||
return None, 0, "T000000"
|
||||
|
||||
+2
-2
@@ -1025,7 +1025,7 @@ class NWProject():
|
||||
by drag-and-drop. Forwarded to the NWTree class.
|
||||
"""
|
||||
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.setProjectChanged(True)
|
||||
return True
|
||||
@@ -1341,7 +1341,7 @@ class NWProject():
|
||||
if oLayout is None:
|
||||
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)
|
||||
if oParent is None:
|
||||
oParent = self.projTree.findRoot(nwItemClass.NOVEL)
|
||||
|
||||
@@ -294,11 +294,6 @@ class NWTree():
|
||||
tTree.append(tHandle)
|
||||
return tTree
|
||||
|
||||
def handleExists(self, tHandle):
|
||||
"""Check if a handle exists in the project.
|
||||
"""
|
||||
return tHandle in self._treeOrder
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
|
||||
@@ -9,6 +9,7 @@ from nw.gui.docviewer import GuiDocViewer, GuiDocViewDetails
|
||||
from nw.gui.itemdetails import GuiItemDetails
|
||||
from nw.gui.itemeditor import GuiItemEditor
|
||||
from nw.gui.mainmenu import GuiMainMenu
|
||||
from nw.gui.noveltree import GuiNovelTree
|
||||
from nw.gui.outline import GuiOutline
|
||||
from nw.gui.outlinedetails import GuiOutlineDetails
|
||||
from nw.gui.preferences import GuiPreferences
|
||||
@@ -31,6 +32,7 @@ __all__ = [
|
||||
"GuiItemDetails",
|
||||
"GuiItemEditor",
|
||||
"GuiMainMenu",
|
||||
"GuiNovelTree",
|
||||
"GuiMainStatus",
|
||||
"GuiOutline",
|
||||
"GuiOutlineDetails",
|
||||
|
||||
@@ -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 2018–2020, 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
@@ -165,7 +165,7 @@ class GuiOutline(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def refreshTree(self, overRide=False):
|
||||
def refreshTree(self, overRide=False, novelChanged=False):
|
||||
"""Called whenever the Outline tab is activated and controls
|
||||
what data to load, and if necessary, force a rebuild of the
|
||||
tree.
|
||||
@@ -177,13 +177,10 @@ class GuiOutline(QTreeWidget):
|
||||
self.firstView = False
|
||||
return
|
||||
|
||||
# If the novel index has changed since the tree was last built,
|
||||
# we rebuild the tree from the updated index.
|
||||
lastChange = self.theParent.theIndex.timeNovel
|
||||
logger.verbose("Last outline build: %.3f" % self.lastBuild)
|
||||
logger.verbose("Novel index change: %.3f" % lastChange)
|
||||
|
||||
doBuild = lastChange > self.lastBuild and self.theProject.autoOutline
|
||||
# If the novel index or novel tree has changed since the tree
|
||||
# was last built, we rebuild the tree from the updated index.
|
||||
indexChanged = self.theIndex.novelChangedSince(self.lastBuild)
|
||||
doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline
|
||||
if doBuild or overRide:
|
||||
logger.debug("Rebuilding Project Outline")
|
||||
self._populateTree()
|
||||
@@ -227,6 +224,7 @@ class GuiOutline(QTreeWidget):
|
||||
tHandle = selItems[0].data(self.colIndex[nwOutline.TITLE], Qt.UserRole)
|
||||
sTitle = selItems[0].data(self.colIndex[nwOutline.LINE], Qt.UserRole)
|
||||
self.theParent.projMeta.showItem(tHandle, sTitle)
|
||||
self.theParent.treeView.setSelectedHandle(tHandle)
|
||||
|
||||
return
|
||||
|
||||
@@ -377,23 +375,12 @@ class GuiOutline(QTreeWidget):
|
||||
currChapter = 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:
|
||||
continue
|
||||
|
||||
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
|
||||
tItem = self._createTreeItem(tHandle, sTitle, novIdx)
|
||||
self.treeMap[tKey] = tItem
|
||||
|
||||
tLevel = novIdx["level"]
|
||||
if tLevel == "H1":
|
||||
self.addTopLevelItem(tItem)
|
||||
currTitle = tItem
|
||||
@@ -436,14 +423,12 @@ class GuiOutline(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def _createTreeItem(self, tHandle, sTitle, tLevel):
|
||||
def _createTreeItem(self, tHandle, sTitle, novIdx):
|
||||
"""Populate a tree item with all the column values.
|
||||
"""
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
novIdx = self.theIndex.novelIndex[tHandle][sTitle]
|
||||
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
newItem = QTreeWidgetItem()
|
||||
hIcon = "doc_%s" % tLevel.lower()
|
||||
hIcon = "doc_%s" % novIdx["level"].lower()
|
||||
|
||||
cC = int(novIdx["cCount"])
|
||||
wC = int(novIdx["wCount"])
|
||||
|
||||
@@ -271,11 +271,10 @@ class GuiOutlineDetails(QScrollArea):
|
||||
"""Update the content of the tree with the given handle and line
|
||||
number pointing to a header.
|
||||
"""
|
||||
try:
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
novIdx = self.theIndex.novelIndex[tHandle][sTitle]
|
||||
theRefs = self.theIndex.getReferences(tHandle, sTitle)
|
||||
except Exception:
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
novIdx = self.theIndex.getNovelData(tHandle, sTitle)
|
||||
theRefs = self.theIndex.getReferences(tHandle, sTitle)
|
||||
if nwItem is None or novIdx is None:
|
||||
return False
|
||||
|
||||
if novIdx["level"] in self.LVL_MAP:
|
||||
|
||||
@@ -118,7 +118,7 @@ class GuiProjectSettings(PagedDialog):
|
||||
self.theProject.setImportColours(importCol)
|
||||
|
||||
if self.tabStatus.colChanged or self.tabImport.colChanged:
|
||||
self.theParent.rebuildTree()
|
||||
self.theParent.rebuildTrees()
|
||||
|
||||
if self.tabReplace.arChanged:
|
||||
newList = self.tabReplace.getNewList()
|
||||
|
||||
+77
-46
@@ -29,7 +29,9 @@
|
||||
import nw
|
||||
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.QtWidgets import (
|
||||
qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction
|
||||
@@ -49,6 +51,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
C_EXPORT = 2
|
||||
C_FLAGS = 3
|
||||
|
||||
novelItemChanged = pyqtSignal()
|
||||
noteItemChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, theParent):
|
||||
QTreeWidget.__init__(self, theParent)
|
||||
|
||||
@@ -60,22 +65,27 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.theProject = theParent.theProject
|
||||
self.theIndex = theParent.theIndex
|
||||
|
||||
# Tree Settings
|
||||
self.theMap = {}
|
||||
self.treeChanged = False
|
||||
# Internal Variables
|
||||
self._treeMap = {}
|
||||
self._treeChanged = False
|
||||
self._timeChanged = 0
|
||||
|
||||
##
|
||||
# Build GUI
|
||||
##
|
||||
|
||||
# Context Menu
|
||||
self.ctxMenu = GuiProjectTreeMenu(self)
|
||||
self.clearTree()
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self._rightClickMenu)
|
||||
|
||||
# Build GUI
|
||||
# Tree Settings
|
||||
iPx = self.theTheme.baseIconSize
|
||||
self.setIconSize(QSize(iPx, iPx))
|
||||
self.setExpandsOnDoubleClick(True)
|
||||
self.setIndentation(iPx)
|
||||
self.setColumnCount(4)
|
||||
self.setHeaderLabels(["Label", "Words", "Inc", "Flags"])
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self._rightClickMenu)
|
||||
|
||||
treeHeadItem = self.headerItem()
|
||||
treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
|
||||
@@ -102,7 +112,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
# Set Multiple Selection by CTRL
|
||||
# Disabled for now, until the merge files option has been added
|
||||
# self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||
# self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
|
||||
# Get user's column width preferences for NAME and COUNT
|
||||
treeColWidth = self.mainConf.getTreeColWidths()
|
||||
@@ -116,10 +126,11 @@ class GuiProjectTree(QTreeWidget):
|
||||
# Set custom settings
|
||||
self.initTree()
|
||||
|
||||
logger.debug("GuiProjectTree initialisation complete")
|
||||
# Internal Function Mapping
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
self.askQuestion = self.theParent.askQuestion
|
||||
|
||||
# Internal Mapping
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
logger.debug("GuiProjectTree initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
@@ -147,8 +158,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
"""Clear the GUI content and the related map.
|
||||
"""
|
||||
self.clear()
|
||||
self.theMap = {}
|
||||
self.treeChanged = False
|
||||
self._treeMap = {}
|
||||
self._treeChanged = False
|
||||
self._timeChanged = 0
|
||||
return
|
||||
|
||||
def newTreeItem(self, itemType, itemClass):
|
||||
@@ -274,10 +286,13 @@ class GuiProjectTree(QTreeWidget):
|
||||
return False
|
||||
|
||||
pHandle = nwItem.itemParent
|
||||
if pHandle is not None and pHandle in self.theMap:
|
||||
self.theMap[pHandle].setExpanded(True)
|
||||
if pHandle is not None and pHandle in self._treeMap:
|
||||
self._treeMap[pHandle].setExpanded(True)
|
||||
|
||||
self._emitItemChange(tHandle)
|
||||
self.clearSelection()
|
||||
trItem.setSelected(True)
|
||||
|
||||
return True
|
||||
|
||||
def moveTreeItem(self, nStep):
|
||||
@@ -318,6 +333,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.clearSelection()
|
||||
cItem.setSelected(True)
|
||||
self._setTreeChanged(True)
|
||||
self._emitItemChange(tHandle)
|
||||
|
||||
return True
|
||||
|
||||
@@ -338,7 +354,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
"""Calls saveTreeOrder if there are unsaved changes, otherwise
|
||||
does nothing.
|
||||
"""
|
||||
if self.treeChanged:
|
||||
if self._treeChanged:
|
||||
logger.verbose("Flushing project tree to project class")
|
||||
self.saveTreeOrder()
|
||||
self._setTreeChanged(False)
|
||||
@@ -391,7 +407,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.makeAlert("The Trash folder is already empty.", nwAlert.INFO)
|
||||
return False
|
||||
|
||||
msgYes = self.theParent.askQuestion(
|
||||
msgYes = self.askQuestion(
|
||||
"Empty Trash", "Permanently delete %d file(s) from Trash?" % nTrash
|
||||
)
|
||||
if not msgYes:
|
||||
@@ -446,7 +462,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
# user if they want to permanently delete the file.
|
||||
doPermanent = False
|
||||
if not alreadyAsked:
|
||||
msgYes = self.theParent.askQuestion(
|
||||
msgYes = self.askQuestion(
|
||||
"Delete File", "Permanently delete file '%s'?" % nwItemS.itemName
|
||||
)
|
||||
if msgYes:
|
||||
@@ -474,7 +490,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
# move it there.
|
||||
doTrash = False
|
||||
if askForTrash:
|
||||
msgYes = self.theParent.askQuestion(
|
||||
msgYes = self.askQuestion(
|
||||
"Delete File", "Move file '%s' to Trash?" % nwItemS.itemName
|
||||
)
|
||||
if msgYes:
|
||||
@@ -533,7 +549,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
return True
|
||||
|
||||
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)
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
@@ -622,9 +640,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
sent first.
|
||||
"""
|
||||
logger.debug("Building the project tree ...")
|
||||
self.clear()
|
||||
iCount = 0
|
||||
self.clearTree()
|
||||
|
||||
iCount = 0
|
||||
for nwItem in self.theProject.getProjectItems():
|
||||
iCount += 1
|
||||
self._addTreeItem(nwItem)
|
||||
@@ -642,21 +660,10 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
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):
|
||||
"""Set a specific handle as the selected item.
|
||||
"""
|
||||
if tHandle not in self.theMap:
|
||||
if tHandle not in self._treeMap:
|
||||
return False
|
||||
|
||||
tItem = self._getTreeItem(tHandle)
|
||||
@@ -664,7 +671,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
return False
|
||||
|
||||
self.clearSelection()
|
||||
self.theMap[tHandle].setSelected(True)
|
||||
self._treeMap[tHandle].setSelected(True)
|
||||
|
||||
selItems = self.selectedIndexes()
|
||||
if selItems and doScroll:
|
||||
@@ -672,6 +679,11 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
return True
|
||||
|
||||
def changedSince(self, checkTime):
|
||||
"""Check if the tree has changed since a given time.
|
||||
"""
|
||||
return self._timeChanged > checkTime
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
@@ -699,7 +711,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
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 suer middle clicked.
|
||||
for viewing if the user middle-clicked.
|
||||
"""
|
||||
QTreeWidget.mousePressEvent(self, theEvent)
|
||||
|
||||
@@ -783,6 +795,10 @@ class GuiProjectTree(QTreeWidget):
|
||||
else:
|
||||
self.theIndex.reIndexHandle(sHandle)
|
||||
|
||||
# Trigger dependent updates
|
||||
self._setTreeChanged(True)
|
||||
self._emitItemChange(sHandle)
|
||||
|
||||
else:
|
||||
theEvent.ignore()
|
||||
logger.debug("Drag'n'drop of item %s not accepted" % sHandle)
|
||||
@@ -797,7 +813,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
def _getTreeItem(self, tHandle):
|
||||
"""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):
|
||||
"""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_COUNT, Qt.UserRole, 0)
|
||||
|
||||
self.theMap[tHandle] = newItem
|
||||
self._treeMap[tHandle] = newItem
|
||||
if pHandle is None:
|
||||
if nwItem.itemType == nwItemType.ROOT:
|
||||
self.addTopLevelItem(newItem)
|
||||
@@ -845,20 +861,20 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.makeAlert(
|
||||
"There is nowhere to add item with name '%s'" % nwItem.itemName, nwAlert.ERROR
|
||||
)
|
||||
del self.theMap[tHandle]
|
||||
del self._treeMap[tHandle]
|
||||
return None
|
||||
|
||||
else:
|
||||
byIndex = -1
|
||||
if nHandle is not None and nHandle in self.theMap:
|
||||
if nHandle is not None and nHandle in self._treeMap:
|
||||
try:
|
||||
byIndex = self.theMap[pHandle].indexOfChild(self.theMap[nHandle])
|
||||
byIndex = self._treeMap[pHandle].indexOfChild(self._treeMap[nHandle])
|
||||
except Exception:
|
||||
logger.error("Failed to get index of item with handle %s" % nHandle)
|
||||
if byIndex >= 0:
|
||||
self.theMap[pHandle].insertChild(byIndex+1, newItem)
|
||||
self._treeMap[pHandle].insertChild(byIndex+1, newItem)
|
||||
else:
|
||||
self.theMap[pHandle].addChild(newItem)
|
||||
self._treeMap[pHandle].addChild(newItem)
|
||||
self.propagateCount(tHandle, nwItem.wordCount)
|
||||
|
||||
self.setTreeItemValues(tHandle)
|
||||
@@ -910,7 +926,6 @@ class GuiProjectTree(QTreeWidget):
|
||||
pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
|
||||
nwItemS.setParent(pHandle)
|
||||
self.setTreeItemValues(tHandle)
|
||||
self._setTreeChanged(True)
|
||||
|
||||
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):
|
||||
"""Set the tree change flag, and propagate to the project.
|
||||
"""
|
||||
self.treeChanged = theState
|
||||
self._treeChanged = theState
|
||||
if theState:
|
||||
self._timeChanged = time()
|
||||
self.theProject.setProjectChanged(True)
|
||||
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
|
||||
|
||||
class GuiProjectTreeMenu(QMenu):
|
||||
|
||||
+97
-34
@@ -32,7 +32,7 @@ import os
|
||||
from datetime import datetime
|
||||
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.QtWidgets import (
|
||||
qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut,
|
||||
@@ -42,9 +42,9 @@ from PyQt5.QtWidgets import (
|
||||
from nw.gui import (
|
||||
GuiAbout, GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit,
|
||||
GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor,
|
||||
GuiMainMenu, GuiMainStatus, GuiOutline, GuiOutlineDetails, GuiPreferences,
|
||||
GuiProjectLoad, GuiProjectSettings, GuiProjectTree, GuiProjectWizard,
|
||||
GuiTheme, GuiWritingStats
|
||||
GuiMainMenu, GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails,
|
||||
GuiPreferences, GuiProjectLoad, GuiProjectSettings, GuiProjectTree,
|
||||
GuiProjectWizard, GuiTheme, GuiWritingStats
|
||||
)
|
||||
from nw.core import NWProject, NWDoc, NWIndex
|
||||
from nw.constants import nwItemType, nwItemClass, nwAlert, nwConst
|
||||
@@ -99,6 +99,7 @@ class GuiMain(QMainWindow):
|
||||
# Main GUI Elements
|
||||
self.statusBar = GuiMainStatus(self)
|
||||
self.treeView = GuiProjectTree(self)
|
||||
self.novelView = GuiNovelTree(self)
|
||||
self.docEditor = GuiDocEditor(self)
|
||||
self.viewMeta = GuiDocViewDetails(self)
|
||||
self.docViewer = GuiDocViewer(self)
|
||||
@@ -111,11 +112,24 @@ class GuiMain(QMainWindow):
|
||||
self.statusIcons = []
|
||||
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
|
||||
self.treePane = QWidget()
|
||||
self.treeBox = QVBoxLayout()
|
||||
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.treePane.setLayout(self.treeBox)
|
||||
|
||||
@@ -136,31 +150,33 @@ class GuiMain(QMainWindow):
|
||||
self.splitOutline.addWidget(self.projMeta)
|
||||
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
|
||||
|
||||
# Main Tabs : Edirot / Outline
|
||||
self.tabWidget = QTabWidget()
|
||||
self.tabWidget.setTabPosition(QTabWidget.East)
|
||||
self.tabWidget.setStyleSheet("QTabWidget::pane {border: 0;}")
|
||||
self.tabWidget.addTab(self.splitDocs, "Editor")
|
||||
self.tabWidget.addTab(self.splitOutline, "Outline")
|
||||
self.tabWidget.currentChanged.connect(self._mainTabChanged)
|
||||
# Main Tabs : Editor / Outline
|
||||
self.mainTabs = QTabWidget()
|
||||
self.mainTabs.setTabPosition(QTabWidget.East)
|
||||
self.mainTabs.setStyleSheet("QTabWidget::pane {border: 0;}")
|
||||
self.mainTabs.addTab(self.splitDocs, "Editor")
|
||||
self.mainTabs.addTab(self.splitOutline, "Outline")
|
||||
self.mainTabs.currentChanged.connect(self._mainTabChanged)
|
||||
|
||||
# Splitter : Project Tree / Main Tabs
|
||||
xCM = self.mainConf.pxInt(4)
|
||||
self.splitMain = QSplitter(Qt.Horizontal)
|
||||
self.splitMain.setContentsMargins(xCM, xCM, xCM, xCM)
|
||||
self.splitMain.addWidget(self.treePane)
|
||||
self.splitMain.addWidget(self.tabWidget)
|
||||
self.splitMain.addWidget(self.mainTabs)
|
||||
self.splitMain.setSizes(self.mainConf.getMainPanePos())
|
||||
|
||||
# Indices of All Splitter Widgets
|
||||
self.idxTree = self.splitMain.indexOf(self.treePane)
|
||||
self.idxMain = self.splitMain.indexOf(self.tabWidget)
|
||||
self.idxEditor = self.splitDocs.indexOf(self.docEditor)
|
||||
self.idxViewer = self.splitDocs.indexOf(self.splitView)
|
||||
self.idxViewDoc = self.splitView.indexOf(self.docViewer)
|
||||
self.idxViewMeta = self.splitView.indexOf(self.viewMeta)
|
||||
self.idxTabEdit = self.tabWidget.indexOf(self.splitDocs)
|
||||
self.idxTabProj = self.tabWidget.indexOf(self.splitOutline)
|
||||
self.idxTree = self.splitMain.indexOf(self.treePane)
|
||||
self.idxMain = self.splitMain.indexOf(self.mainTabs)
|
||||
self.idxEditor = self.splitDocs.indexOf(self.docEditor)
|
||||
self.idxViewer = self.splitDocs.indexOf(self.splitView)
|
||||
self.idxViewDoc = self.splitView.indexOf(self.docViewer)
|
||||
self.idxViewMeta = self.splitView.indexOf(self.viewMeta)
|
||||
self.idxTabEdit = self.mainTabs.indexOf(self.splitDocs)
|
||||
self.idxTabProj = self.mainTabs.indexOf(self.splitOutline)
|
||||
self.idxTreeView = self.projTabs.indexOf(self.treeView)
|
||||
self.idxNovelView = self.projTabs.indexOf(self.novelView)
|
||||
|
||||
# Splitter Behaviour
|
||||
self.splitMain.setCollapsible(self.idxTree, False)
|
||||
@@ -177,7 +193,8 @@ class GuiMain(QMainWindow):
|
||||
# Initialise the Project Tree
|
||||
self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
|
||||
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
|
||||
self.rebuildTree()
|
||||
self.treeView.novelItemChanged.connect(self._treeNovelItemChanged)
|
||||
self.rebuildTrees()
|
||||
|
||||
# Set Main Window Elements
|
||||
self.setMenuBar(self.mainMenu)
|
||||
@@ -253,6 +270,7 @@ class GuiMain(QMainWindow):
|
||||
"""Wrapper function to clear all sub-elements of the main GUI.
|
||||
"""
|
||||
self.treeView.clearTree()
|
||||
self.novelView.clearTree()
|
||||
self.docEditor.clearEditor()
|
||||
self.closeDocViewer()
|
||||
self.statusBar.clearStatus()
|
||||
@@ -299,7 +317,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
logger.info("Creating new project")
|
||||
if self.theProject.newProject(projData):
|
||||
self.rebuildTree()
|
||||
self.rebuildTrees()
|
||||
self.saveProject()
|
||||
self.hasProject = True
|
||||
self.statusBar.setRefTime(self.theProject.projOpened)
|
||||
@@ -355,7 +373,7 @@ class GuiMain(QMainWindow):
|
||||
self.theIndex.clearIndex()
|
||||
self.clearGUI()
|
||||
self.hasProject = False
|
||||
self.tabWidget.setCurrentWidget(self.splitDocs)
|
||||
self.mainTabs.setCurrentWidget(self.splitDocs)
|
||||
|
||||
return saveOK
|
||||
|
||||
@@ -371,7 +389,7 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
# Switch main tab to editor view
|
||||
self.tabWidget.setCurrentWidget(self.splitDocs)
|
||||
self.mainTabs.setCurrentWidget(self.splitDocs)
|
||||
|
||||
# Try to open the project
|
||||
if not self.theProject.openProject(projFile):
|
||||
@@ -423,7 +441,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
# Update GUI
|
||||
self._setWindowTitle(self.theProject.projName)
|
||||
self.rebuildTree()
|
||||
self.rebuildTrees()
|
||||
self.docEditor.setDictionaries()
|
||||
self.docEditor.setSpellCheck(self.theProject.spellCheck)
|
||||
self.mainMenu.setAutoOutline(self.theProject.autoOutline)
|
||||
@@ -490,7 +508,7 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
self.closeDocument()
|
||||
self.tabWidget.setCurrentWidget(self.splitDocs)
|
||||
self.mainTabs.setCurrentWidget(self.splitDocs)
|
||||
if self.docEditor.loadText(tHandle, tLine):
|
||||
if changeFocus:
|
||||
self.docEditor.setFocus()
|
||||
@@ -575,7 +593,7 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
# 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)
|
||||
if self.docViewer.loadText(tHandle):
|
||||
@@ -739,13 +757,13 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return
|
||||
|
||||
def rebuildTree(self):
|
||||
def rebuildTrees(self):
|
||||
"""Rebuild the project tree.
|
||||
"""
|
||||
self._makeStatusIcons()
|
||||
self._makeImportIcons()
|
||||
self.treeView.clearTree()
|
||||
self.treeView.buildTree()
|
||||
self.novelView.refreshTree()
|
||||
return
|
||||
|
||||
def rebuildIndex(self, beQuiet=False):
|
||||
@@ -803,7 +821,7 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
logger.verbose("Forcing a rebuild of the Project Outline")
|
||||
self.tabWidget.setCurrentWidget(self.splitOutline)
|
||||
self.mainTabs.setCurrentWidget(self.splitOutline)
|
||||
self.projView.refreshTree(overRide=True)
|
||||
|
||||
return True
|
||||
@@ -866,6 +884,7 @@ class GuiMain(QMainWindow):
|
||||
self.docEditor.initEditor()
|
||||
self.docViewer.initViewer()
|
||||
self.treeView.initTree()
|
||||
self.novelView.initTree()
|
||||
self.projView.initOutline()
|
||||
self.projMeta.initDetails()
|
||||
|
||||
@@ -1027,6 +1046,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
self.mainConf.setShowRefPanel(self.viewMeta.isVisible())
|
||||
self.mainConf.setTreeColWidths(self.treeView.getColumnSizes())
|
||||
self.mainConf.setNovelColWidths(self.novelView.getColumnSizes())
|
||||
if not self.mainConf.isFullScreen:
|
||||
self.mainConf.setWinSize(self.width(), self.height())
|
||||
|
||||
@@ -1083,7 +1103,7 @@ class GuiMain(QMainWindow):
|
||||
self.mainMenu.aFocusMode.setChecked(self.isFocusMode)
|
||||
if self.isFocusMode:
|
||||
logger.debug("Activating Focus Mode")
|
||||
self.tabWidget.setCurrentWidget(self.splitDocs)
|
||||
self.mainTabs.setCurrentWidget(self.splitDocs)
|
||||
else:
|
||||
logger.debug("Deactivating Focus Mode")
|
||||
|
||||
@@ -1091,7 +1111,7 @@ class GuiMain(QMainWindow):
|
||||
self.treePane.setVisible(isVisible)
|
||||
self.statusBar.setVisible(isVisible)
|
||||
self.mainMenu.setVisible(isVisible)
|
||||
self.tabWidget.tabBar().setVisible(isVisible)
|
||||
self.mainTabs.tabBar().setVisible(isVisible)
|
||||
|
||||
hideDocFooter = self.isFocusMode and self.mainConf.hideFocusFooter
|
||||
self.docEditor.docFooter.setVisible(not hideDocFooter)
|
||||
@@ -1300,9 +1320,10 @@ class GuiMain(QMainWindow):
|
||||
return
|
||||
|
||||
##
|
||||
# Signal Handlers
|
||||
# Slots
|
||||
##
|
||||
|
||||
@pyqtSlot()
|
||||
def _treeSingleClick(self):
|
||||
"""Single click on a project tree item just updates the details
|
||||
panel below the tree.
|
||||
@@ -1312,12 +1333,14 @@ class GuiMain(QMainWindow):
|
||||
self.treeMeta.updateViewBox(sHandle)
|
||||
return
|
||||
|
||||
@pyqtSlot("QTreeWidgetItem*", int)
|
||||
def _treeDoubleClick(self, tItem, colNo):
|
||||
"""The user double-clicked an item in the tree. If it is a file,
|
||||
we open it. Otherwise, we do nothing.
|
||||
"""
|
||||
tHandle = tItem.data(self.treeView.C_NAME, Qt.UserRole)
|
||||
logger.verbose("User double clicked tree item with handle %s" % tHandle)
|
||||
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
if nwItem is not None:
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
@@ -1328,6 +1351,20 @@ class GuiMain(QMainWindow):
|
||||
|
||||
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):
|
||||
"""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
|
||||
@@ -1335,6 +1372,7 @@ class GuiMain(QMainWindow):
|
||||
"""
|
||||
tHandle = self.treeView.getSelectedHandle()
|
||||
logger.verbose("User pressed return on tree item with handle %s" % tHandle)
|
||||
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
if nwItem is not None:
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
@@ -1342,8 +1380,10 @@ class GuiMain(QMainWindow):
|
||||
self.openDocument(tHandle, changeFocus=False, doScroll=False)
|
||||
else:
|
||||
logger.verbose("Requested item %s is a folder" % tHandle)
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _keyPressEscape(self):
|
||||
"""When the escape key is pressed somewhere in the main window,
|
||||
do the following, in order:
|
||||
@@ -1352,8 +1392,10 @@ class GuiMain(QMainWindow):
|
||||
self.docEditor.closeSearch()
|
||||
elif self.isFocusMode:
|
||||
self.toggleFocusMode()
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot(int)
|
||||
def _mainTabChanged(self, tabIndex):
|
||||
"""Activated when the main window tab is changed.
|
||||
"""
|
||||
@@ -1363,6 +1405,27 @@ class GuiMain(QMainWindow):
|
||||
logger.verbose("Project outline tab activated")
|
||||
if self.hasProject:
|
||||
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
|
||||
|
||||
# END Class GuiMain
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<?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>
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jane Smith</author>
|
||||
<author>Jay Doh</author>
|
||||
<saveCount>823</saveCount>
|
||||
<saveCount>824</saveCount>
|
||||
<autoCount>153</autoCount>
|
||||
<editTime>39446</editTime>
|
||||
<editTime>39448</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>False</doBackup>
|
||||
|
||||
@@ -11,6 +11,7 @@ lastnotes = 1.0
|
||||
[Sizes]
|
||||
geometry = 1200, 650
|
||||
treecols = 200, 50, 30
|
||||
novelcols = 200, 50
|
||||
projcols = 200, 60, 140
|
||||
mainpane = 300, 800
|
||||
docpane = 400, 400
|
||||
|
||||
@@ -11,6 +11,7 @@ lastnotes = 1.0
|
||||
[Sizes]
|
||||
geometry = 1100, 650
|
||||
treecols = 120, 30, 50
|
||||
novelcols = 200, 50
|
||||
projcols = 140, 55, 140
|
||||
mainpane = 300, 800
|
||||
docpane = 400, 400
|
||||
|
||||
@@ -304,6 +304,19 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir):
|
||||
|
||||
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
|
||||
tmpConf.guiScale = 2.0
|
||||
assert tmpConf.setProjColWidths([10, 20, 30])
|
||||
|
||||
+138
-108
@@ -56,30 +56,30 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, dummyGUI, outDir, refDir):
|
||||
assert theIndex.saveIndex()
|
||||
|
||||
# Take a copy of the index
|
||||
tagIndex = str(theIndex.tagIndex)
|
||||
refIndex = str(theIndex.refIndex)
|
||||
novelIndex = str(theIndex.novelIndex)
|
||||
noteIndex = str(theIndex.noteIndex)
|
||||
textCounts = str(theIndex.textCounts)
|
||||
tagIndex = str(theIndex._tagIndex)
|
||||
refIndex = str(theIndex._refIndex)
|
||||
novelIndex = str(theIndex._novelIndex)
|
||||
noteIndex = str(theIndex._noteIndex)
|
||||
textCounts = str(theIndex._textCounts)
|
||||
|
||||
# Delete a handle
|
||||
assert theIndex.tagIndex.get("Bod", None) is not None
|
||||
assert theIndex.refIndex.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._tagIndex.get("Bod", None) is not None
|
||||
assert theIndex._refIndex.get("4c4f28287af27", None) is not None
|
||||
assert theIndex._noteIndex.get("4c4f28287af27", None) is not None
|
||||
assert theIndex._textCounts.get("4c4f28287af27", None) is not None
|
||||
theIndex.deleteHandle("4c4f28287af27")
|
||||
assert theIndex.tagIndex.get("Bod", None) is None
|
||||
assert theIndex.refIndex.get("4c4f28287af27", None) is None
|
||||
assert theIndex.noteIndex.get("4c4f28287af27", None) is None
|
||||
assert theIndex.textCounts.get("4c4f28287af27", None) is None
|
||||
assert theIndex._tagIndex.get("Bod", None) is None
|
||||
assert theIndex._refIndex.get("4c4f28287af27", None) is None
|
||||
assert theIndex._noteIndex.get("4c4f28287af27", None) is None
|
||||
assert theIndex._textCounts.get("4c4f28287af27", None) is None
|
||||
|
||||
# Clear the index
|
||||
theIndex.clearIndex()
|
||||
assert not theIndex.tagIndex
|
||||
assert not theIndex.refIndex
|
||||
assert not theIndex.novelIndex
|
||||
assert not theIndex.noteIndex
|
||||
assert not theIndex.textCounts
|
||||
assert not theIndex._tagIndex
|
||||
assert not theIndex._refIndex
|
||||
assert not theIndex._novelIndex
|
||||
assert not theIndex._noteIndex
|
||||
assert not theIndex._textCounts
|
||||
|
||||
# Make the load fail
|
||||
monkeypatch.setattr(json, "load", doPanic)
|
||||
@@ -89,46 +89,46 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, dummyGUI, outDir, refDir):
|
||||
monkeypatch.undo()
|
||||
assert theIndex.loadIndex()
|
||||
|
||||
assert str(theIndex.tagIndex) == tagIndex
|
||||
assert str(theIndex.refIndex) == refIndex
|
||||
assert str(theIndex.novelIndex) == novelIndex
|
||||
assert str(theIndex.noteIndex) == noteIndex
|
||||
assert str(theIndex.textCounts) == textCounts
|
||||
assert str(theIndex._tagIndex) == tagIndex
|
||||
assert str(theIndex._refIndex) == refIndex
|
||||
assert str(theIndex._novelIndex) == novelIndex
|
||||
assert str(theIndex._noteIndex) == noteIndex
|
||||
assert str(theIndex._textCounts) == textCounts
|
||||
|
||||
# Break the index and check that we notice
|
||||
assert not theIndex.indexBroken
|
||||
theIndex.tagIndex["Bod"].append("Stuff") # No longer len() == 4
|
||||
theIndex._tagIndex["Bod"].append("Stuff") # No longer len() == 4
|
||||
theIndex.checkIndex()
|
||||
assert theIndex.indexBroken
|
||||
|
||||
assert theIndex.loadIndex()
|
||||
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()
|
||||
assert theIndex.indexBroken
|
||||
|
||||
assert theIndex.loadIndex()
|
||||
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()
|
||||
assert theIndex.indexBroken
|
||||
|
||||
assert theIndex.loadIndex()
|
||||
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()
|
||||
assert theIndex.indexBroken
|
||||
|
||||
assert theIndex.loadIndex()
|
||||
assert not theIndex.indexBroken
|
||||
theIndex.textCounts["7a992350f3eb6"].append("Stuff") # No longer len() == 3
|
||||
theIndex._textCounts["7a992350f3eb6"].append("Stuff") # No longer len() == 3
|
||||
theIndex.checkIndex()
|
||||
assert theIndex.indexBroken
|
||||
|
||||
# Make the try/except trigger as well
|
||||
assert theIndex.loadIndex()
|
||||
assert not theIndex.indexBroken
|
||||
theIndex.refIndex["fb609cd8319dc"]["T000001"] = {"tagssss": []} # Wrong key name
|
||||
theIndex._refIndex["fb609cd8319dc"]["T000001"] = {"tagssss": []} # Wrong key name
|
||||
theIndex.checkIndex()
|
||||
assert theIndex.indexBroken
|
||||
|
||||
@@ -205,6 +205,10 @@ def testCoreIndex_CheckThese(nwMinimal, dummyGUI):
|
||||
nItem = theProject.projTree[nHandle]
|
||||
cItem = theProject.projTree[cHandle]
|
||||
|
||||
assert not theIndex.novelChangedSince(0)
|
||||
assert not theIndex.notesChangedSince(0)
|
||||
assert not theIndex.indexChangedSince(0)
|
||||
|
||||
assert theIndex.scanText(cHandle, (
|
||||
"# Jane Smith\n"
|
||||
"@tag: Jane"
|
||||
@@ -213,8 +217,12 @@ def testCoreIndex_CheckThese(nwMinimal, dummyGUI):
|
||||
"# Hello World!\n"
|
||||
"@pov: Jane"
|
||||
))
|
||||
assert theIndex.tagIndex == {"Jane": [2, cHandle, "CHARACTER", "T000001"]}
|
||||
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
|
||||
assert theIndex._tagIndex == {"Jane": [2, cHandle, "CHARACTER", "T000001"]}
|
||||
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(["@tag", "Jane"], cItem) == [True, True]
|
||||
@@ -285,8 +293,8 @@ def testCoreIndex_ScanText(nwMinimal, dummyGUI):
|
||||
"This is a story about Jane Smith.\n\n"
|
||||
"Well, not really.\n"
|
||||
))
|
||||
assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle
|
||||
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
|
||||
assert str(theIndex._tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle
|
||||
assert theIndex.getNovelData(nHandle, "T000001")["title"] == "Hello World!"
|
||||
|
||||
# Check that title sections are indexed properly
|
||||
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
|
||||
"Paragraph Five.\n\n"
|
||||
))
|
||||
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("T000002", 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("T000005", 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("T000008", 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("T000011", 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("T000014", 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("T000017", 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("T000020", 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("T000023", 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("T000026", None) is None
|
||||
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("T000002", 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("T000005", 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("T000008", 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("T000011", 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("T000014", 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("T000017", 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("T000020", 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("T000023", 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("T000026", None) is None
|
||||
|
||||
assert theIndex.novelIndex[nHandle]["T000001"]["level"] == "H1"
|
||||
assert theIndex.novelIndex[nHandle]["T000007"]["level"] == "H2"
|
||||
assert theIndex.novelIndex[nHandle]["T000013"]["level"] == "H3"
|
||||
assert theIndex.novelIndex[nHandle]["T000019"]["level"] == "H4"
|
||||
assert theIndex._novelIndex[nHandle]["T000001"]["level"] == "H1"
|
||||
assert theIndex._novelIndex[nHandle]["T000007"]["level"] == "H2"
|
||||
assert theIndex._novelIndex[nHandle]["T000013"]["level"] == "H3"
|
||||
assert theIndex._novelIndex[nHandle]["T000019"]["level"] == "H4"
|
||||
|
||||
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Title One"
|
||||
assert theIndex.novelIndex[nHandle]["T000007"]["title"] == "Title Two"
|
||||
assert theIndex.novelIndex[nHandle]["T000013"]["title"] == "Title Three"
|
||||
assert theIndex.novelIndex[nHandle]["T000019"]["title"] == "Title Four"
|
||||
assert theIndex._novelIndex[nHandle]["T000001"]["title"] == "Title One"
|
||||
assert theIndex._novelIndex[nHandle]["T000007"]["title"] == "Title Two"
|
||||
assert theIndex._novelIndex[nHandle]["T000013"]["title"] == "Title Three"
|
||||
assert theIndex._novelIndex[nHandle]["T000019"]["title"] == "Title Four"
|
||||
|
||||
assert theIndex.novelIndex[nHandle]["T000001"]["layout"] == "SCENE"
|
||||
assert theIndex.novelIndex[nHandle]["T000007"]["layout"] == "SCENE"
|
||||
assert theIndex.novelIndex[nHandle]["T000013"]["layout"] == "SCENE"
|
||||
assert theIndex.novelIndex[nHandle]["T000019"]["layout"] == "SCENE"
|
||||
assert theIndex._novelIndex[nHandle]["T000001"]["layout"] == "SCENE"
|
||||
assert theIndex._novelIndex[nHandle]["T000007"]["layout"] == "SCENE"
|
||||
assert theIndex._novelIndex[nHandle]["T000013"]["layout"] == "SCENE"
|
||||
assert theIndex._novelIndex[nHandle]["T000019"]["layout"] == "SCENE"
|
||||
|
||||
assert theIndex.novelIndex[nHandle]["T000001"]["synopsis"] == "Synopsis One."
|
||||
assert theIndex.novelIndex[nHandle]["T000007"]["synopsis"] == "Synopsis Two."
|
||||
assert theIndex.novelIndex[nHandle]["T000013"]["synopsis"] == "Synopsis Three."
|
||||
assert theIndex.novelIndex[nHandle]["T000019"]["synopsis"] == "Synopsis Four."
|
||||
assert theIndex._novelIndex[nHandle]["T000001"]["synopsis"] == "Synopsis One."
|
||||
assert theIndex._novelIndex[nHandle]["T000007"]["synopsis"] == "Synopsis Two."
|
||||
assert theIndex._novelIndex[nHandle]["T000013"]["synopsis"] == "Synopsis Three."
|
||||
assert theIndex._novelIndex[nHandle]["T000019"]["synopsis"] == "Synopsis Four."
|
||||
|
||||
assert theIndex.novelIndex[nHandle]["T000001"]["cCount"] == 23
|
||||
assert theIndex.novelIndex[nHandle]["T000007"]["cCount"] == 23
|
||||
assert theIndex.novelIndex[nHandle]["T000013"]["cCount"] == 27
|
||||
assert theIndex.novelIndex[nHandle]["T000019"]["cCount"] == 56
|
||||
assert theIndex._novelIndex[nHandle]["T000001"]["cCount"] == 23
|
||||
assert theIndex._novelIndex[nHandle]["T000007"]["cCount"] == 23
|
||||
assert theIndex._novelIndex[nHandle]["T000013"]["cCount"] == 27
|
||||
assert theIndex._novelIndex[nHandle]["T000019"]["cCount"] == 56
|
||||
|
||||
assert theIndex.novelIndex[nHandle]["T000001"]["wCount"] == 4
|
||||
assert theIndex.novelIndex[nHandle]["T000007"]["wCount"] == 4
|
||||
assert theIndex.novelIndex[nHandle]["T000013"]["wCount"] == 4
|
||||
assert theIndex.novelIndex[nHandle]["T000019"]["wCount"] == 9
|
||||
assert theIndex._novelIndex[nHandle]["T000001"]["wCount"] == 4
|
||||
assert theIndex._novelIndex[nHandle]["T000007"]["wCount"] == 4
|
||||
assert theIndex._novelIndex[nHandle]["T000013"]["wCount"] == 4
|
||||
assert theIndex._novelIndex[nHandle]["T000019"]["wCount"] == 9
|
||||
|
||||
assert theIndex.novelIndex[nHandle]["T000001"]["pCount"] == 1
|
||||
assert theIndex.novelIndex[nHandle]["T000007"]["pCount"] == 1
|
||||
assert theIndex.novelIndex[nHandle]["T000013"]["pCount"] == 1
|
||||
assert theIndex.novelIndex[nHandle]["T000019"]["pCount"] == 3
|
||||
assert theIndex._novelIndex[nHandle]["T000001"]["pCount"] == 1
|
||||
assert theIndex._novelIndex[nHandle]["T000007"]["pCount"] == 1
|
||||
assert theIndex._novelIndex[nHandle]["T000013"]["pCount"] == 1
|
||||
assert theIndex._novelIndex[nHandle]["T000019"]["pCount"] == 3
|
||||
|
||||
assert theIndex.scanText(cHandle, (
|
||||
"# Title One\n\n"
|
||||
@@ -374,22 +382,22 @@ def testCoreIndex_ScanText(nwMinimal, dummyGUI):
|
||||
"% synopsis: Synopsis One.\n\n"
|
||||
"Paragraph One.\n\n"
|
||||
))
|
||||
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("T000002", 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("T000005", 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("T000000", 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("T000003", 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("T000006", None) is None
|
||||
assert theIndex._refIndex[cHandle].get("T000007", None) is None
|
||||
|
||||
assert theIndex.noteIndex[cHandle]["T000001"]["level"] == "H1"
|
||||
assert theIndex.noteIndex[cHandle]["T000001"]["title"] == "Title One"
|
||||
assert theIndex.noteIndex[cHandle]["T000001"]["layout"] == "NOTE"
|
||||
assert theIndex.noteIndex[cHandle]["T000001"]["synopsis"] == "Synopsis One."
|
||||
assert theIndex.noteIndex[cHandle]["T000001"]["cCount"] == 23
|
||||
assert theIndex.noteIndex[cHandle]["T000001"]["wCount"] == 4
|
||||
assert theIndex.noteIndex[cHandle]["T000001"]["pCount"] == 1
|
||||
assert theIndex._noteIndex[cHandle]["T000001"]["level"] == "H1"
|
||||
assert theIndex._noteIndex[cHandle]["T000001"]["title"] == "Title One"
|
||||
assert theIndex._noteIndex[cHandle]["T000001"]["layout"] == "NOTE"
|
||||
assert theIndex._noteIndex[cHandle]["T000001"]["synopsis"] == "Synopsis One."
|
||||
assert theIndex._noteIndex[cHandle]["T000001"]["cCount"] == 23
|
||||
assert theIndex._noteIndex[cHandle]["T000001"]["wCount"] == 4
|
||||
assert theIndex._noteIndex[cHandle]["T000001"]["pCount"] == 1
|
||||
|
||||
assert theIndex.scanText(sHandle, (
|
||||
"# Title One\n\n"
|
||||
@@ -399,7 +407,7 @@ def testCoreIndex_ScanText(nwMinimal, dummyGUI):
|
||||
"% synopsis: Synopsis 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"]]
|
||||
)
|
||||
|
||||
@@ -419,6 +427,9 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
|
||||
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
|
||||
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
|
||||
|
||||
assert theIndex.getNovelData("", "") is None
|
||||
assert theIndex.getNovelData("a508bb932959c", "") is None
|
||||
|
||||
assert theIndex.scanText(cHandle, (
|
||||
"# Jane Smith\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
|
||||
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
|
||||
theProject.projTree[nHandle].setExported(False)
|
||||
assert theIndex.getNovelStructure(skipExcluded=False) == ["%s:T000001" % nHandle]
|
||||
assert theIndex.getNovelStructure(skipExcluded=True) == []
|
||||
assert theIndex.getNovelStructure() == []
|
||||
|
||||
theKeys = []
|
||||
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
|
||||
cC, wC, pC = theIndex.getCounts(nHandle)
|
||||
|
||||
@@ -26,11 +26,11 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
|
||||
assert nwGUI.openProject(nwLipsum)
|
||||
|
||||
# Rebuild the index as it isn't automatically copied
|
||||
assert nwGUI.theIndex.tagIndex == {}
|
||||
assert nwGUI.theIndex.refIndex == {}
|
||||
assert nwGUI.theIndex._tagIndex == {}
|
||||
assert nwGUI.theIndex._refIndex == {}
|
||||
nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger)
|
||||
assert nwGUI.theIndex.tagIndex != {}
|
||||
assert nwGUI.theIndex.refIndex != {}
|
||||
assert nwGUI.theIndex._tagIndex != {}
|
||||
assert nwGUI.theIndex._refIndex != {}
|
||||
|
||||
# Select a document in the project tree
|
||||
assert nwGUI.treeView.setSelectedHandle("88243afbe5ed8")
|
||||
|
||||
@@ -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
|
||||
@@ -24,7 +24,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
|
||||
nwGUI.mainConf.lastPath = nwLipsum
|
||||
|
||||
nwGUI.rebuildIndex()
|
||||
nwGUI.tabWidget.setCurrentIndex(nwGUI.idxTabProj)
|
||||
nwGUI.mainTabs.setCurrentIndex(nwGUI.idxTabProj)
|
||||
|
||||
assert nwGUI.projView.topLevelItemCount() > 0
|
||||
|
||||
|
||||
@@ -220,10 +220,11 @@ def testGuiPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir):
|
||||
compFile = os.path.join(refDir, "guiPreferences_novelwriter.conf")
|
||||
copyfile(projFile, testFile)
|
||||
ignoreLines = [
|
||||
2, # Timestamp
|
||||
9, # Release Notes
|
||||
12, 13, 14, 15, 16, 17, 18, # Window sizes
|
||||
7, 28, # Fonts (depends on system default)
|
||||
2, # Timestamp
|
||||
9, # Release Notes
|
||||
12, 13, 14, 15, # Window sizes
|
||||
16, 17, 18, 19, # Window sizes
|
||||
7, 29, # Fonts (depends on system default)
|
||||
]
|
||||
assert cmpFiles(testFile, compFile, ignoreLines)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user