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)
### 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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
-5
View File
@@ -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
##
+2
View File
@@ -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",
+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
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"])
+4 -5
View File
@@ -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:
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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
+13
View File
@@ -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
View File
@@ -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)
+4 -4
View File
@@ -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")
+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.rebuildIndex()
nwGUI.tabWidget.setCurrentIndex(nwGUI.idxTabProj)
nwGUI.mainTabs.setCurrentIndex(nwGUI.idxTabProj)
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")
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)