Simplify the novel outline tree

This commit is contained in:
Veronica Berglyd Olsen
2022-06-14 00:14:51 +02:00
parent 49ace054bd
commit ea61db9591
5 changed files with 101 additions and 114 deletions
+9
View File
@@ -57,6 +57,15 @@ class nwRegEx:
# END Class nwRegEx # END Class nwRegEx
class nwHeaders:
H_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
TT_NONE = "T000000"
# END Class nwHeaders
class nwFiles: class nwFiles:
PROJ_FILE = "nwProject.nwx" PROJ_FILE = "nwProject.nwx"
+8 -12
View File
@@ -34,7 +34,7 @@ from time import time
from novelwriter.enum import nwItemType, nwItemLayout from novelwriter.enum import nwItemType, nwItemLayout
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode, nwHeaders
from novelwriter.core.document import NWDoc from novelwriter.core.document import NWDoc
from novelwriter.common import ( from novelwriter.common import (
checkInt, isHandle, isItemClass, isTitleTag, jsonEncode checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
@@ -42,10 +42,6 @@ from novelwriter.common import (
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
H_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
TT_NONE = "T000000"
class NWIndex: class NWIndex:
"""This class holds the entire index for a given project. The index """This class holds the entire index for a given project. The index
@@ -477,7 +473,7 @@ class NWIndex:
""" """
hCount = [0, 0, 0, 0, 0] hCount = [0, 0, 0, 0, 0]
for _, _, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): for _, _, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl):
iLevel = H_LEVEL.get(hItem.level, 0) iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
hCount[iLevel] += 1 hCount[iLevel] += 1
return hCount return hCount
@@ -510,7 +506,7 @@ class NWIndex:
pKey = None pKey = None
for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl):
tKey = f"{tHandle}:{sTitle}" tKey = f"{tHandle}:{sTitle}"
iLevel = H_LEVEL.get(hItem.level, 0) iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
if iLevel > maxDepth: if iLevel > maxDepth:
if pKey in tData: if pKey in tData:
tData[pKey]["words"] += hItem.wordCount tData[pKey]["words"] += hItem.wordCount
@@ -660,7 +656,7 @@ class TagsIndex:
""" """
if tagKey in self._tags: if tagKey in self._tags:
return self._tags.get(tagKey).get("heading") return self._tags.get(tagKey).get("heading")
return TT_NONE return nwHeaders.TT_NONE
def tagClass(self, tagKey): def tagClass(self, tagKey):
"""Get the class of a given tag. """Get the class of a given tag.
@@ -906,7 +902,7 @@ class IndexItem:
self._index = 0 self._index = 0
# Add a placeholder heading # Add a placeholder heading
self._headings[TT_NONE] = IndexHeading(TT_NONE) self._headings[nwHeaders.TT_NONE] = IndexHeading(nwHeaders.TT_NONE)
return return
@@ -940,8 +936,8 @@ class IndexItem:
"""Add a heading to the item. Also remove the placeholder entry """Add a heading to the item. Also remove the placeholder entry
if it exists. if it exists.
""" """
if TT_NONE in self._headings: if nwHeaders.TT_NONE in self._headings:
self._headings.pop(TT_NONE) self._headings.pop(nwHeaders.TT_NONE)
self._headings[tHeading.key] = tHeading self._headings[tHeading.key] = tHeading
return return
@@ -1110,7 +1106,7 @@ class IndexHeading:
def setLevel(self, level): def setLevel(self, level):
"""Set the level of the header if it's a valid value. """Set the level of the header if it's a valid value.
""" """
if level in H_VALID: if level in nwHeaders.H_VALID:
self._level = level self._level = level
return return
+1 -1
View File
@@ -653,7 +653,7 @@ class NWProject():
self._legacyDataFolder(projItem) self._legacyDataFolder(projItem)
except Exception: except Exception:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"There was an error while converting project version 1.0. " "There was an error updating the project. "
"Some data may not have been preserved." "Some data may not have been preserved."
), nwAlert.ERROR) ), nwAlert.ERROR)
+73 -91
View File
@@ -4,7 +4,9 @@ novelWriter GUI Novel Tree
GUI classe for the main window novel tree GUI classe for the main window novel tree
File History: File History:
Created: 2020-12-20 [1.1a0] Created: 2020-12-20 [1.1a0] GuiNovelTree
Created: 2022-06-12 [1.7b1] GuiNovelView
Created: 2022-06-12 [1.7b1] GuiNovelToolBar
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182020, Veronica Berglyd Olsen Copyright 20182020, Veronica Berglyd Olsen
@@ -30,7 +32,7 @@ from time import time
from enum import Enum from enum import Enum
from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal
from PyQt5.QtGui import QPalette from PyQt5.QtGui import QPalette, QPixmap, QColor
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel, QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel,
QMenu, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QMenu, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
@@ -39,19 +41,19 @@ from PyQt5.QtWidgets import (
from novelwriter.enum import nwDocMode, nwItemClass from novelwriter.enum import nwDocMode, nwItemClass
from novelwriter.common import checkInt from novelwriter.common import checkInt
from novelwriter.constants import nwKeyWords, nwLabels, trConst from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NovelColumnType(Enum): class NovelTreeColumn(Enum):
HIDDEN = 0 HIDDEN = 0
POV = 1 POV = 1
FOCUS = 2 FOCUS = 2
PLOT = 3 PLOT = 3
# END Enum NovelColumnType # END Enum NovelTreeColumn
class GuiNovelView(QWidget): class GuiNovelView(QWidget):
@@ -209,16 +211,16 @@ class GuiNovelToolBar(QWidget):
self.mCol3 = self.mMore.addMenu(self.tr("Third Column")) self.mCol3 = self.mMore.addMenu(self.tr("Third Column"))
self.mCol3.addAction(self.tr("Hide Column")).triggered.connect( self.mCol3.addAction(self.tr("Hide Column")).triggered.connect(
lambda: self.novelView.novelTree.setLastColType(NovelColumnType.HIDDEN) lambda: self.novelView.novelTree.setLastColType(NovelTreeColumn.HIDDEN)
) )
self.mCol3.addAction(self.tr("Point of View Character")).triggered.connect( self.mCol3.addAction(self.tr("Point of View Character")).triggered.connect(
lambda: self.novelView.novelTree.setLastColType(NovelColumnType.POV) lambda: self.novelView.novelTree.setLastColType(NovelTreeColumn.POV)
) )
self.mCol3.addAction(self.tr("Focus Character")).triggered.connect( self.mCol3.addAction(self.tr("Focus Character")).triggered.connect(
lambda: self.novelView.novelTree.setLastColType(NovelColumnType.FOCUS) lambda: self.novelView.novelTree.setLastColType(NovelTreeColumn.FOCUS)
) )
self.mCol3.addAction(self.tr("Novel Plot")).triggered.connect( self.mCol3.addAction(self.tr("Novel Plot")).triggered.connect(
lambda: self.novelView.novelTree.setLastColType(NovelColumnType.PLOT) lambda: self.novelView.novelTree.setLastColType(NovelTreeColumn.PLOT)
) )
self.tbMore = QToolButton(self) self.tbMore = QToolButton(self)
@@ -305,9 +307,9 @@ class GuiNovelTree(QTreeWidget):
# Internal Variables # Internal Variables
self._treeMap = {} self._treeMap = {}
self._lastBuild = 0 self._lastBuild = 0
self._lastCol = NovelColumnType.POV self._lastCol = NovelTreeColumn.POV
# Cached i18n Strings # Cached Strings
self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]) self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
self._focLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]) self._focLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])
self._pltLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]) self._pltLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])
@@ -316,12 +318,15 @@ class GuiNovelTree(QTreeWidget):
# ========= # =========
iPx = self.mainTheme.baseIconSize iPx = self.mainTheme.baseIconSize
nPx = self.mainTheme.textNWidth
cMg = self.mainConf.pxInt(6) cMg = self.mainConf.pxInt(6)
mPx = self.mainConf.pxInt(4)
nMg = self.mainConf.pxInt(6)
self.setIconSize(QSize(iPx, iPx)) # self.setIconSize(QSize(iPx, iPx))
self.setFrameStyle(QFrame.NoFrame) self.setFrameStyle(QFrame.NoFrame)
self.setHeaderHidden(True) self.setHeaderHidden(True)
self.setIndentation(iPx) self.setIndentation(mPx)
self.setColumnCount(3) self.setColumnCount(3)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionMode(QAbstractItemView.SingleSelection)
@@ -336,6 +341,25 @@ class GuiNovelTree(QTreeWidget):
treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents) treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents)
treeHeader.setSectionResizeMode(self.C_LAST, QHeaderView.ResizeToContents) treeHeader.setSectionResizeMode(self.C_LAST, QHeaderView.ResizeToContents)
# Pre-Generate Tree Formatting
fH1 = self.font()
fH1.setBold(True)
fH1.setUnderline(True)
fH2 = self.font()
fH2.setBold(True)
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
self._hIndent = ["", "", "", "\u2022\u00a0", "\u00bb\u00a0"]
self._pIndent = [QPixmap(), QPixmap()]
hPix = QPixmap(QSize(iPx, iPx))
hPix.fill(QColor(0, 0, 0, 0))
for m in range(1, 4):
self._pIndent.append(hPix.scaled(
max(nPx*m - nMg, nMg), 2, Qt.IgnoreAspectRatio, Qt.FastTransformation
))
# Connect signals # Connect signals
self.itemDoubleClicked.connect(self._treeDoubleClick) self.itemDoubleClicked.connect(self._treeDoubleClick)
self.itemSelectionChanged.connect(self._treeSelectionChange) self.itemSelectionChanged.connect(self._treeSelectionChange)
@@ -379,9 +403,9 @@ class GuiNovelTree(QTreeWidget):
"""Load user options. """Load user options.
""" """
self._lastCol = self.theProject.options.getEnum( self._lastCol = self.theProject.options.getEnum(
"GuiNovelView", "lastCol", NovelColumnType, NovelColumnType.POV "GuiNovelView", "lastCol", NovelTreeColumn, NovelTreeColumn.POV
) )
self.setColumnHidden(self.C_LAST, self._lastCol == NovelColumnType.HIDDEN) self.setColumnHidden(self.C_LAST, self._lastCol == NovelTreeColumn.HIDDEN)
return True return True
def saveOptions(self): def saveOptions(self):
@@ -444,7 +468,7 @@ class GuiNovelTree(QTreeWidget):
if self._lastCol != colType: if self._lastCol != colType:
logger.debug("Changing last column to %s", colType.name) logger.debug("Changing last column to %s", colType.name)
self._lastCol = colType self._lastCol = colType
self.setColumnHidden(self.C_LAST, colType == NovelColumnType.HIDDEN) self.setColumnHidden(self.C_LAST, colType == NovelTreeColumn.HIDDEN)
self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True) self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True)
return return
@@ -509,100 +533,58 @@ class GuiNovelTree(QTreeWidget):
"""Build the tree based on the project index. """Build the tree based on the project index.
""" """
self.clearTree() self.clearTree()
currTitle = None
currChapter = None
currScene = None
tStart = time() tStart = time()
logger.verbose("Building novel tree for root item '%s'", rootHandle) logger.verbose("Building novel tree for root item '%s'", rootHandle)
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True) novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
for tKey, tHandle, sTitle, novIdx in novStruct: for tKey, tHandle, sTitle, novIdx in novStruct:
tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx) iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0)
self._treeMap[tKey] = tItem if iLevel == 0:
continue
tLevel = novIdx.level newItem = QTreeWidgetItem()
if tLevel == "H1": theData = (tHandle, sTitle[1:].lstrip("0"), tKey)
self.addTopLevelItem(tItem)
currTitle = tItem
currChapter = None
currScene = None
elif tLevel == "H2": newItem.setData(self.C_TITLE, Qt.DecorationRole, self._pIndent[iLevel])
if currTitle is None: newItem.setText(self.C_TITLE, self._hIndent[iLevel] + novIdx.title)
self.addTopLevelItem(tItem) newItem.setData(self.C_TITLE, Qt.UserRole, theData)
else: newItem.setFont(self.C_TITLE, self._hFonts[iLevel])
currTitle.addChild(tItem) newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}")
currChapter = tItem newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
currScene = None
elif tLevel == "H3": lastText, toolTip = self._getLastColumnText(tHandle, sTitle)
if currChapter is None: newItem.setText(self.C_LAST, lastText)
if currTitle is None: if lastText:
self.addTopLevelItem(tItem) newItem.setToolTip(self.C_LAST, toolTip)
else:
currTitle.addChild(tItem)
else:
currChapter.addChild(tItem)
currScene = tItem
elif tLevel == "H4": self._treeMap[tKey] = newItem
if currScene is None: self.addTopLevelItem(newItem)
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)
logger.verbose("Novel Tree built in %.3f ms", (time() - tStart)*1000) logger.verbose("Novel Tree built in %.3f ms", (time() - tStart)*1000)
self._lastBuild = time() self._lastBuild = time()
return return
def _createTreeItem(self, tHandle, sTitle, titleKey, novIdx): def _getLastColumnText(self, tHandle, sTitle):
"""Populate a tree item with all the column values. """Generate the text for the last column based on user settings.
""" """
newItem = QTreeWidgetItem() if self._lastCol == NovelTreeColumn.HIDDEN:
hIcon = "doc_%s" % novIdx.level.lower() return "", ""
theData = (tHandle, sTitle[1:].lstrip("0"), titleKey)
wC = int(novIdx.wordCount) theRefs = self.theProject.index.getReferences(tHandle, sTitle)
if self._lastCol == NovelTreeColumn.POV:
newText = ", ".join(theRefs[nwKeyWords.POV_KEY])
return newText, f"{self._povLabel}: {newText}"
newItem.setText(self.C_TITLE, novIdx.title) elif self._lastCol == NovelTreeColumn.FOCUS:
newItem.setData(self.C_TITLE, Qt.UserRole, theData) newText = ", ".join(theRefs[nwKeyWords.FOCUS_KEY])
newItem.setIcon(self.C_TITLE, self.mainTheme.getIcon(hIcon)) return newText, f"{self._focLabel}: {newText}"
newItem.setText(self.C_WORDS, f"{wC:n}")
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
if self._lastCol == NovelColumnType.HIDDEN: elif self._lastCol == NovelTreeColumn.PLOT:
newItem.setText(self.C_LAST, "") newText = ", ".join(theRefs[nwKeyWords.PLOT_KEY])
else: return newText, f"{self._pltLabel}: {newText}"
theRefs = self.theProject.index.getReferences(tHandle, sTitle)
if self._lastCol == NovelColumnType.POV:
newText = ", ".join(theRefs[nwKeyWords.POV_KEY])
newItem.setText(self.C_LAST, newText)
if newText:
newItem.setToolTip(self.C_LAST, f"{self._povLabel}: {newText}")
elif self._lastCol == NovelColumnType.FOCUS:
newText = ", ".join(theRefs[nwKeyWords.FOCUS_KEY])
newItem.setText(self.C_LAST, newText)
if newText:
newItem.setToolTip(self.C_LAST, f"{self._focLabel}: {newText}")
elif self._lastCol == NovelColumnType.PLOT:
newText = ", ".join(theRefs[nwKeyWords.PLOT_KEY])
newItem.setText(self.C_LAST, newText)
if newText:
newItem.setToolTip(self.C_LAST, f"{self._pltLabel}: {newText}")
return newItem return "", ""
# END Class GuiNovelTree # END Class GuiNovelTree
+10 -10
View File
@@ -1582,30 +1582,30 @@ class GuiMain(QMainWindow):
return return
@pyqtSlot(int) @pyqtSlot(int)
def _mainStackChanged(self, tabIndex): def _mainStackChanged(self, stIndex):
"""Activated when the main window tab is changed. """Activated when the main window tab is changed.
""" """
if tabIndex == self.idxEditorView: if stIndex == self.idxEditorView:
logger.verbose("Editor tab activated") logger.verbose("Editor View activated")
elif tabIndex == self.idxOutlineView: elif stIndex == self.idxOutlineView:
logger.verbose("Project outline tab activated") logger.verbose("Outline View activated")
if self.hasProject: if self.hasProject:
self.outlineView.refreshView() self.outlineView.refreshView()
return return
@pyqtSlot(int) @pyqtSlot(int)
def _projStackChanged(self, tabIndex): def _projStackChanged(self, stIndex):
"""Activated when the project view tab is changed. """Activated when the project view tab is changed.
""" """
sHandle = None sHandle = None
if tabIndex == self.idxProjView: if stIndex == self.idxProjView:
logger.verbose("Project tree tab activated") logger.verbose("Project Tree View activated")
sHandle = self.projView.getSelectedHandle() sHandle = self.projView.getSelectedHandle()
elif tabIndex == self.idxNovelView: elif stIndex == self.idxNovelView:
logger.verbose("Novel tree tab activated") logger.verbose("Novel Tree View activated")
if self.hasProject: if self.hasProject:
self.novelView.refreshTree() self.novelView.refreshTree()
sHandle, _ = self.novelView.getSelectedHandle() sHandle, _ = self.novelView.getSelectedHandle()