From ea61db9591316db21774840411e7fd4a021ec608 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 14 Jun 2022 00:14:51 +0200 Subject: [PATCH] Simplify the novel outline tree --- novelwriter/constants.py | 9 ++ novelwriter/core/index.py | 20 ++--- novelwriter/core/project.py | 2 +- novelwriter/gui/noveltree.py | 164 ++++++++++++++++------------------- novelwriter/guimain.py | 20 ++--- 5 files changed, 101 insertions(+), 114 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index dc62dbd3..3d1b5c13 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -57,6 +57,15 @@ 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: PROJ_FILE = "nwProject.nwx" diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index ee9ea089..0b3afdc5 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -34,7 +34,7 @@ from time import time from novelwriter.enum import nwItemType, nwItemLayout 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.common import ( checkInt, isHandle, isItemClass, isTitleTag, jsonEncode @@ -42,10 +42,6 @@ from novelwriter.common import ( 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: """This class holds the entire index for a given project. The index @@ -477,7 +473,7 @@ class NWIndex: """ hCount = [0, 0, 0, 0, 0] 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 return hCount @@ -510,7 +506,7 @@ class NWIndex: pKey = None for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): tKey = f"{tHandle}:{sTitle}" - iLevel = H_LEVEL.get(hItem.level, 0) + iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) if iLevel > maxDepth: if pKey in tData: tData[pKey]["words"] += hItem.wordCount @@ -660,7 +656,7 @@ class TagsIndex: """ if tagKey in self._tags: return self._tags.get(tagKey).get("heading") - return TT_NONE + return nwHeaders.TT_NONE def tagClass(self, tagKey): """Get the class of a given tag. @@ -906,7 +902,7 @@ class IndexItem: self._index = 0 # Add a placeholder heading - self._headings[TT_NONE] = IndexHeading(TT_NONE) + self._headings[nwHeaders.TT_NONE] = IndexHeading(nwHeaders.TT_NONE) return @@ -940,8 +936,8 @@ class IndexItem: """Add a heading to the item. Also remove the placeholder entry if it exists. """ - if TT_NONE in self._headings: - self._headings.pop(TT_NONE) + if nwHeaders.TT_NONE in self._headings: + self._headings.pop(nwHeaders.TT_NONE) self._headings[tHeading.key] = tHeading return @@ -1110,7 +1106,7 @@ class IndexHeading: def setLevel(self, level): """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 return diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 43f3297e..fb81f033 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -653,7 +653,7 @@ class NWProject(): self._legacyDataFolder(projItem) except Exception: 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." ), nwAlert.ERROR) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 8bfd207a..33981243 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -4,7 +4,9 @@ novelWriter – GUI Novel Tree GUI classe for the main window novel tree 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 Copyright 2018–2020, Veronica Berglyd Olsen @@ -30,7 +32,7 @@ from time import time from enum import Enum from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal -from PyQt5.QtGui import QPalette +from PyQt5.QtGui import QPalette, QPixmap, QColor from PyQt5.QtWidgets import ( QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel, QMenu, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, @@ -39,19 +41,19 @@ from PyQt5.QtWidgets import ( from novelwriter.enum import nwDocMode, nwItemClass from novelwriter.common import checkInt -from novelwriter.constants import nwKeyWords, nwLabels, trConst +from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst logger = logging.getLogger(__name__) -class NovelColumnType(Enum): +class NovelTreeColumn(Enum): HIDDEN = 0 POV = 1 FOCUS = 2 PLOT = 3 -# END Enum NovelColumnType +# END Enum NovelTreeColumn class GuiNovelView(QWidget): @@ -209,16 +211,16 @@ class GuiNovelToolBar(QWidget): self.mCol3 = self.mMore.addMenu(self.tr("Third Column")) 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( - lambda: self.novelView.novelTree.setLastColType(NovelColumnType.POV) + lambda: self.novelView.novelTree.setLastColType(NovelTreeColumn.POV) ) 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( - lambda: self.novelView.novelTree.setLastColType(NovelColumnType.PLOT) + lambda: self.novelView.novelTree.setLastColType(NovelTreeColumn.PLOT) ) self.tbMore = QToolButton(self) @@ -305,9 +307,9 @@ class GuiNovelTree(QTreeWidget): # Internal Variables self._treeMap = {} 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._focLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]) self._pltLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]) @@ -316,12 +318,15 @@ class GuiNovelTree(QTreeWidget): # ========= iPx = self.mainTheme.baseIconSize + nPx = self.mainTheme.textNWidth 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.setHeaderHidden(True) - self.setIndentation(iPx) + self.setIndentation(mPx) self.setColumnCount(3) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionMode(QAbstractItemView.SingleSelection) @@ -336,6 +341,25 @@ class GuiNovelTree(QTreeWidget): treeHeader.setSectionResizeMode(self.C_WORDS, 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 self.itemDoubleClicked.connect(self._treeDoubleClick) self.itemSelectionChanged.connect(self._treeSelectionChange) @@ -379,9 +403,9 @@ class GuiNovelTree(QTreeWidget): """Load user options. """ 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 def saveOptions(self): @@ -444,7 +468,7 @@ class GuiNovelTree(QTreeWidget): if self._lastCol != colType: logger.debug("Changing last column to %s", colType.name) 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) return @@ -509,100 +533,58 @@ class GuiNovelTree(QTreeWidget): """Build the tree based on the project index. """ self.clearTree() - - currTitle = None - currChapter = None - currScene = None - tStart = time() - logger.verbose("Building novel tree for root item '%s'", rootHandle) + novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True) for tKey, tHandle, sTitle, novIdx in novStruct: - tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx) - self._treeMap[tKey] = tItem + iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0) + if iLevel == 0: + continue - tLevel = novIdx.level - if tLevel == "H1": - self.addTopLevelItem(tItem) - currTitle = tItem - currChapter = None - currScene = None + newItem = QTreeWidgetItem() + theData = (tHandle, sTitle[1:].lstrip("0"), tKey) - elif tLevel == "H2": - if currTitle is None: - self.addTopLevelItem(tItem) - else: - currTitle.addChild(tItem) - currChapter = tItem - currScene = None + newItem.setData(self.C_TITLE, Qt.DecorationRole, self._pIndent[iLevel]) + newItem.setText(self.C_TITLE, self._hIndent[iLevel] + novIdx.title) + newItem.setData(self.C_TITLE, Qt.UserRole, theData) + newItem.setFont(self.C_TITLE, self._hFonts[iLevel]) + newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}") + newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) - elif tLevel == "H3": - if currChapter is None: - if currTitle is None: - self.addTopLevelItem(tItem) - else: - currTitle.addChild(tItem) - else: - currChapter.addChild(tItem) - currScene = tItem + lastText, toolTip = self._getLastColumnText(tHandle, sTitle) + newItem.setText(self.C_LAST, lastText) + if lastText: + newItem.setToolTip(self.C_LAST, toolTip) - 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._treeMap[tKey] = newItem + self.addTopLevelItem(newItem) logger.verbose("Novel Tree built in %.3f ms", (time() - tStart)*1000) - self._lastBuild = time() return - def _createTreeItem(self, tHandle, sTitle, titleKey, novIdx): - """Populate a tree item with all the column values. + def _getLastColumnText(self, tHandle, sTitle): + """Generate the text for the last column based on user settings. """ - newItem = QTreeWidgetItem() - hIcon = "doc_%s" % novIdx.level.lower() - theData = (tHandle, sTitle[1:].lstrip("0"), titleKey) + if self._lastCol == NovelTreeColumn.HIDDEN: + return "", "" - 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) - newItem.setData(self.C_TITLE, Qt.UserRole, theData) - newItem.setIcon(self.C_TITLE, self.mainTheme.getIcon(hIcon)) - newItem.setText(self.C_WORDS, f"{wC:n}") - newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) + elif self._lastCol == NovelTreeColumn.FOCUS: + newText = ", ".join(theRefs[nwKeyWords.FOCUS_KEY]) + return newText, f"{self._focLabel}: {newText}" - if self._lastCol == NovelColumnType.HIDDEN: - newItem.setText(self.C_LAST, "") - else: - 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}") + elif self._lastCol == NovelTreeColumn.PLOT: + newText = ", ".join(theRefs[nwKeyWords.PLOT_KEY]) + return newText, f"{self._pltLabel}: {newText}" - return newItem + return "", "" # END Class GuiNovelTree diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index d3e36e90..71d99e33 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1582,30 +1582,30 @@ class GuiMain(QMainWindow): return @pyqtSlot(int) - def _mainStackChanged(self, tabIndex): + def _mainStackChanged(self, stIndex): """Activated when the main window tab is changed. """ - if tabIndex == self.idxEditorView: - logger.verbose("Editor tab activated") - elif tabIndex == self.idxOutlineView: - logger.verbose("Project outline tab activated") + if stIndex == self.idxEditorView: + logger.verbose("Editor View activated") + elif stIndex == self.idxOutlineView: + logger.verbose("Outline View activated") if self.hasProject: self.outlineView.refreshView() return @pyqtSlot(int) - def _projStackChanged(self, tabIndex): + def _projStackChanged(self, stIndex): """Activated when the project view tab is changed. """ sHandle = None - if tabIndex == self.idxProjView: - logger.verbose("Project tree tab activated") + if stIndex == self.idxProjView: + logger.verbose("Project Tree View activated") sHandle = self.projView.getSelectedHandle() - elif tabIndex == self.idxNovelView: - logger.verbose("Novel tree tab activated") + elif stIndex == self.idxNovelView: + logger.verbose("Novel Tree View activated") if self.hasProject: self.novelView.refreshTree() sHandle, _ = self.novelView.getSelectedHandle()