diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index ae157e2c..d4106803 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -83,3 +83,8 @@ view_build = typ_export.svg view_editor = mixed_edit.svg view_novel = typ_book-grey.svg view_outline = typ_puzzle-outline.svg + +deco_doc_h1 = nw_deco-h1.svg +deco_doc_h2 = nw_deco-h2.svg +deco_doc_h3 = nw_deco-h3.svg +deco_doc_h4 = nw_deco-h4.svg diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h0.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h0.svg new file mode 100644 index 00000000..3c1618c9 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h0.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h1.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h1.svg new file mode 100644 index 00000000..1c0dec9b --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h1.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h2.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h2.svg new file mode 100644 index 00000000..0f86e5bb --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h2.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h3.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h3.svg new file mode 100644 index 00000000..f05e46e6 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h3.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_deco-h4.svg b/novelwriter/assets/icons/typicons_dark/nw_deco-h4.svg new file mode 100644 index 00000000..aa74e6f3 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_deco-h4.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index 4683bb19..8d267a47 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -83,3 +83,8 @@ view_build = typ_export.svg view_editor = mixed_edit.svg view_novel = typ_book-grey.svg view_outline = typ_puzzle-outline.svg + +deco_doc_h1 = nw_deco-h1.svg +deco_doc_h2 = nw_deco-h2.svg +deco_doc_h3 = nw_deco-h3.svg +deco_doc_h4 = nw_deco-h4.svg diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h0.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h0.svg new file mode 100644 index 00000000..3c1618c9 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h0.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h1.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h1.svg new file mode 100644 index 00000000..e6c8efdc --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h1.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h2.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h2.svg new file mode 100644 index 00000000..7caa4203 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h2.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h3.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h3.svg new file mode 100644 index 00000000..61feca1b --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h3.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_deco-h4.svg b/novelwriter/assets/icons/typicons_light/nw_deco-h4.svg new file mode 100644 index 00000000..b76fd7da --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_deco-h4.svg @@ -0,0 +1,35 @@ + + + + + + image/svg+xml + + + + + + + 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/options.py b/novelwriter/core/options.py index 144ba4ac..d64970ae 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -28,6 +28,8 @@ import os import json import logging +from enum import Enum + from novelwriter.error import logException from novelwriter.common import checkBool, checkFloat, checkInt, checkString from novelwriter.constants import nwFiles @@ -56,7 +58,8 @@ VALID_MAP = { "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2", "widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble" }, - "GuiWordList": {"winWidth", "winHeight"} + "GuiWordList": {"winWidth", "winHeight"}, + "GuiNovelView": {"lastCol"}, } @@ -137,7 +140,10 @@ class OptionState(): if group not in self._theState: self._theState[group] = {} - self._theState[group][name] = value + if isinstance(value, Enum): + self._theState[group][name] = value.name + else: + self._theState[group][name] = value return True @@ -186,4 +192,16 @@ class OptionState(): return checkBool(self._theState[group].get(name, default), default) return default + def getEnum(self, group, name, lookup, default): + """Return the value mapped to an enum. Otherwise return the + default value + """ + if issubclass(lookup, Enum): + if group in self._theState: + if name in self._theState[group]: + value = self._theState[group][name] + if value in lookup.__members__: + return lookup[value] + return default + # END Class OptionState diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index c2683cc7..fb81f033 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -102,6 +102,8 @@ class NWProject(): self.importItems = None # Note file importance values self.lastEdited = None # The handle of the last file to be edited self.lastViewed = None # The handle of the last file to be viewed + self.lastNovel = None # The handle of the last novel root viewed + self.lastOutline = None # The handle of the last outline root viewed self.lastWCount = 0 # The project word count from last session self.lastNovelWC = 0 # The novel files word count from last session self.lastNotesWC = 0 # The note files word count from last session @@ -612,6 +614,10 @@ class NWProject(): self.lastEdited = checkString(xItem.text, None, True) elif xItem.tag == "lastViewed": self.lastViewed = checkString(xItem.text, None, True) + elif xItem.tag == "lastNovel": + self.lastNovel = checkString(xItem.text, None, True) + elif xItem.tag == "lastOutline": + self.lastOutline = checkString(xItem.text, None, True) elif xItem.tag == "lastWordCount": self.lastWCount = checkInt(xItem.text, 0, False) elif xItem.tag == "novelWordCount": @@ -647,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) @@ -732,6 +738,8 @@ class NWProject(): self._packProjectValue(xSettings, "autoOutline", self.autoOutline) self._packProjectValue(xSettings, "lastEdited", self.lastEdited) self._packProjectValue(xSettings, "lastViewed", self.lastViewed) + self._packProjectValue(xSettings, "lastNovel", self.lastNovel) + self._packProjectValue(xSettings, "lastOutline", self.lastOutline) self._packProjectValue(xSettings, "lastWordCount", self.currWCount) self._packProjectValue(xSettings, "novelWordCount", self.currNovelWC) self._packProjectValue(xSettings, "notesWordCount", self.currNotesWC) @@ -1123,6 +1131,14 @@ class NWProject(): self.setProjectChanged(True) return True + def setLastNovelViewed(self, tHandle): + """Set last viewed novel root in the novel tree. + """ + if self.lastNovel != tHandle: + self.lastNovel = tHandle + self.setProjectChanged(True) + return True + def setStatusColours(self, newCols, delCols): """Update the list of novel file status flags. """ diff --git a/novelwriter/gui/__init__.py b/novelwriter/gui/__init__.py index 1892e83d..17699942 100644 --- a/novelwriter/gui/__init__.py +++ b/novelwriter/gui/__init__.py @@ -23,7 +23,7 @@ from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.docviewer import GuiDocViewer, GuiDocViewDetails from novelwriter.gui.itemdetails import GuiItemDetails from novelwriter.gui.mainmenu import GuiMainMenu -from novelwriter.gui.noveltree import GuiNovelTree +from novelwriter.gui.noveltree import GuiNovelView from novelwriter.gui.outline import GuiOutlineView from novelwriter.gui.projtree import GuiProjectView from novelwriter.gui.statusbar import GuiMainStatus @@ -37,7 +37,7 @@ __all__ = [ "GuiItemDetails", "GuiMainMenu", "GuiMainStatus", - "GuiNovelTree", + "GuiNovelView", "GuiOutlineView", "GuiProjectView", "GuiTheme", diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 25552037..0d32689e 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -511,11 +511,13 @@ class GuiDocEditor(QTextEdit): self.theProject.index.scanText(tHandle, docText) newHeader = self.theProject.index.getHandleHeaderLevel(tHandle) + # ToDo: This should be a signal if self._updateHeaders(checkLevel=True): self.mainGui.requestNovelTreeRefresh() else: self.mainGui.novelView.updateWordCounts(tHandle) + # ToDo: This should be a signal if oldHeader != newHeader: self.mainGui.projView.setTreeItemValues(tHandle) self.mainGui.itemDetails.updateViewBox(tHandle) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index f634ce23..8711dc8d 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 @@ -26,84 +28,376 @@ along with this program. If not, see . import logging import novelwriter +from enum import Enum from time import time -from PyQt5.QtCore import Qt, QSize +from PyQt5.QtGui import QPalette +from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal from PyQt5.QtWidgets import ( - QTreeWidget, QTreeWidgetItem, QAbstractItemView, QFrame + QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel, + QMenu, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, + QWidget ) +from novelwriter.enum import nwDocMode, nwItemClass from novelwriter.common import checkInt -from novelwriter.constants import nwKeyWords +from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst logger = logging.getLogger(__name__) +class NovelTreeColumn(Enum): + + HIDDEN = 0 + POV = 1 + FOCUS = 2 + PLOT = 3 + +# END Enum NovelTreeColumn + + +class GuiNovelView(QWidget): + + # Signals for user interaction with the novel tree + selectedItemChanged = pyqtSignal(str) + openDocumentRequest = pyqtSignal(str, Enum, int, str) + + def __init__(self, mainGui): + QWidget.__init__(self, mainGui) + + self.mainGui = mainGui + self.theProject = mainGui.theProject + + # Build GUI + self.novelTree = GuiNovelTree(self) + self.novelBar = GuiNovelToolBar(self) + + # Assemble + self.outerBox = QVBoxLayout() + self.outerBox.addWidget(self.novelBar, 0) + self.outerBox.addWidget(self.novelTree, 1) + self.outerBox.setContentsMargins(0, 0, 0, 0) + self.outerBox.setSpacing(0) + + self.setLayout(self.outerBox) + + # Function Mappings + self.updateWordCounts = self.novelTree.updateWordCounts + self.getSelectedHandle = self.novelTree.getSelectedHandle + + return + + ## + # Methods + ## + + def initSettings(self): + self.novelTree.initSettings() + return + + def refreshTree(self): + """Refresh the current tree. + """ + self.novelTree.refreshTree(rootHandle=self.theProject.lastNovel) + return + + def clearProject(self): + """Clear project-related GUI content. + """ + self.novelTree.clearContent() + self.novelBar.clearContent() + return + + def openProjectTasks(self): + """Run opening project tasks. + """ + lastNovel = self.theProject.lastNovel + if lastNovel not in self.theProject.tree: + lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL) + + logger.debug("Setting novel tree to root item '%s'", lastNovel) + + lastCol = self.theProject.options.getEnum( + "GuiNovelView", "lastCol", NovelTreeColumn, NovelTreeColumn.HIDDEN + ) + + self.clearProject() + self.novelBar.buildNovelRootMenu() + self.novelBar.setLastColType(lastCol, doRefresh=False) + self.novelBar.setCurrentRoot(lastNovel) + + return + + def closeProjectTasks(self): + """Run closing project tasks. + """ + lastColType = self.novelTree.lastColType + self.theProject.options.setValue("GuiNovelView", "lastCol", lastColType) + return + + def setFocus(self): + """Forward the set focus call to the tree widget. + """ + self.novelTree.setFocus() + return + + def treeHasFocus(self): + """Check if the novel tree has focus. + """ + return self.novelTree.hasFocus() + + ## + # Public Slots + ## + + @pyqtSlot(str) + def updateRootItem(self, tHandle): + """If any root item changes, rebuild the novel root menu. + """ + self.novelBar.buildNovelRootMenu() + return + +# END Class GuiNovelView + + +class GuiNovelToolBar(QWidget): + + def __init__(self, novelView): + QTreeWidget.__init__(self, novelView) + + logger.debug("Initialising GuiNovelToolBar ...") + + self.mainConf = novelwriter.CONFIG + self.novelView = novelView + self.theProject = novelView.mainGui.theProject + self.mainTheme = novelView.mainGui.mainTheme + + iPx = self.mainTheme.baseIconSize + mPx = self.mainConf.pxInt(3) + + self.setContentsMargins(0, 0, 0, 0) + self.setAutoFillBackground(True) + + qPalette = self.palette() + qPalette.setBrush(QPalette.Window, qPalette.base()) + self.setPalette(qPalette) + + fadeCol = qPalette.text().color() + buttonStyle = ( + "QToolButton {{padding: {0}px; border: none; background: transparent;}} " + "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" + ).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue()) + + # Widget Label + self.viewLabel = QLabel("%s" % self.tr("Novel Outline")) + self.viewLabel.setContentsMargins(0, 0, 0, 0) + self.viewLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Refresh Button + self.tbRefresh = QToolButton(self) + self.tbRefresh.setToolTip(self.tr("Refresh")) + self.tbRefresh.setIcon(self.mainTheme.getIcon("refresh")) + self.tbRefresh.setIconSize(QSize(iPx, iPx)) + self.tbRefresh.setStyleSheet(buttonStyle) + self.tbRefresh.clicked.connect(self._refreshNovelTree) + + # Novel Root Menu + self.mRoot = QMenu() + self.gRoot = QActionGroup(self.mRoot) + self.aRoot = {} + + self.tbRoot = QToolButton(self) + self.tbRoot.setToolTip(self.tr("Novel Root")) + self.tbRoot.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])) + self.tbRoot.setIconSize(QSize(iPx, iPx)) + self.tbRoot.setStyleSheet(buttonStyle) + self.tbRoot.setMenu(self.mRoot) + self.tbRoot.setPopupMode(QToolButton.InstantPopup) + + # More Options Menu + self.mMore = QMenu() + + self.mLastCol = self.mMore.addMenu(self.tr("Last Column")) + self.gLastCol = QActionGroup(self.mMore) + self.aLastCol = {} + self._addLastColAction(NovelTreeColumn.HIDDEN, self.tr("Hidden")) + self._addLastColAction(NovelTreeColumn.POV, self.tr("Point of View Character")) + self._addLastColAction(NovelTreeColumn.FOCUS, self.tr("Focus Character")) + self._addLastColAction(NovelTreeColumn.PLOT, self.tr("Novel Plot")) + + self.tbMore = QToolButton(self) + self.tbMore.setToolTip(self.tr("More Options")) + self.tbMore.setIcon(self.mainTheme.getIcon("menu")) + self.tbMore.setIconSize(QSize(iPx, iPx)) + self.tbMore.setStyleSheet(buttonStyle) + self.tbMore.setMenu(self.mMore) + self.tbMore.setPopupMode(QToolButton.InstantPopup) + + # Assemble + self.outerBox = QHBoxLayout() + self.outerBox.addWidget(self.viewLabel) + self.outerBox.addWidget(self.tbRefresh) + self.outerBox.addWidget(self.tbRoot) + self.outerBox.addWidget(self.tbMore) + self.outerBox.setContentsMargins(mPx, mPx, 0, mPx) + self.outerBox.setSpacing(0) + + self.setLayout(self.outerBox) + + logger.debug("GuiNovelToolBar initialisation complete") + + return + + ## + # Methods + ## + + def clearContent(self): + """Run clearing project tasks. + """ + self.mRoot.clear() + self.aRoot = {} + return + + def buildNovelRootMenu(self): + """Build the novel root menu. + """ + self.mRoot.clear() + self.aRoot = {} + for n, (tHandle, nwItem) in enumerate(self.theProject.tree.iterRoots(nwItemClass.NOVEL)): + aRoot = self.mRoot.addAction(nwItem.itemName) + aRoot.setData(tHandle) + aRoot.setCheckable(True) + aRoot.triggered.connect(lambda n, tHandle=tHandle: self.setCurrentRoot(tHandle)) + self.gRoot.addAction(aRoot) + self.aRoot[tHandle] = aRoot + + return + + def setCurrentRoot(self, rootHandle): + """Set the current active root handle. + """ + if rootHandle in self.aRoot: + self.aRoot[rootHandle].setChecked(True) + self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) + return + + def setLastColType(self, colType, doRefresh=True): + """Set the last column type. + """ + self.aLastCol[colType].setChecked(True) + self.novelView.novelTree.setLastColType(colType, doRefresh=doRefresh) + return + + ## + # Private Slots + ## + + @pyqtSlot() + def _refreshNovelTree(self): + """Rebuild the current tree. + """ + rootHandle = self.theProject.lastNovel + self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) + return + + ## + # Internal Functions + ## + + def _addLastColAction(self, colType, actionLabel): + """Add a column selection entry to the last column menu. + """ + aLast = self.mLastCol.addAction(actionLabel) + aLast.setCheckable(True) + aLast.setActionGroup(self.gLastCol) + aLast.triggered.connect(lambda: self.setLastColType(colType)) + self.aLastCol[colType] = aLast + return + +# END Class GuiNovelToolBar + + class GuiNovelTree(QTreeWidget): C_TITLE = 0 C_WORDS = 1 - C_POV = 2 + C_LAST = 2 - def __init__(self, mainGui): - QTreeWidget.__init__(self, mainGui) + def __init__(self, novelView): + QTreeWidget.__init__(self, novelView) logger.debug("Initialising GuiNovelTree ...") self.mainConf = novelwriter.CONFIG - self.mainGui = mainGui - self.mainTheme = mainGui.mainTheme - self.theProject = mainGui.theProject + self.novelView = novelView + self.mainGui = novelView.mainGui + self.mainTheme = novelView.mainGui.mainTheme + self.theProject = novelView.mainGui.theProject # Internal Variables self._treeMap = {} self._lastBuild = 0 + self._lastCol = NovelTreeColumn.POV + + # 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]) # Build GUI + # ========= + iPx = self.mainTheme.baseIconSize - self.setFrameStyle(QFrame.NoFrame) + cMg = self.mainConf.pxInt(6) + self.setIconSize(QSize(iPx, iPx)) - self.setIndentation(iPx) + self.setFrameStyle(QFrame.NoFrame) + self.setHeaderHidden(True) + self.setIndentation(0) self.setColumnCount(3) - self.setHeaderLabels([ - self.tr("Novel Outline"), - self.tr("Words"), - self.tr("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, self.tr("Section title")) - treeHeadItem.setToolTip(self.C_WORDS, self.tr("Word count")) - treeHeadItem.setToolTip(self.C_POV, self.tr("Point-of-view character")) - + # Lock the column sizes treeHeader = self.header() - treeHeader.setStretchLastSection(True) - treeHeader.setMinimumSectionSize(iPx + 6) + treeHeader.setStretchLastSection(False) + treeHeader.setMinimumSectionSize(iPx + cMg) + treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.Stretch) + treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents) + treeHeader.setSectionResizeMode(self.C_LAST, QHeaderView.ResizeToContents) - # 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) + # Pre-Generate Tree Formatting + fH1 = self.font() + fH1.setBold(True) + fH1.setUnderline(True) - # The last column should just auto-scale - self.resizeColumnToContents(self.C_POV) + fH2 = self.font() + fH2.setBold(True) + + self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()] + self._pIndent = [ + self.mainTheme.loadDecoration("deco_doc_h0", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h1", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h2", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h3", pxH=iPx), + self.mainTheme.loadDecoration("deco_doc_h4", pxH=iPx), + ] + + # Connect signals + self.itemDoubleClicked.connect(self._treeDoubleClick) + self.itemSelectionChanged.connect(self._treeSelectionChange) # Set custom settings - self.initTree() + self.initSettings() logger.debug("GuiNovelTree initialisation complete") return - def initTree(self): + def initSettings(self): """Set or update tree widget settings. """ # Scroll bars @@ -119,11 +413,19 @@ class GuiNovelTree(QTreeWidget): return + ## + # Properties + ## + + @property + def lastColType(self): + return self._lastCol + ## # Class Methods ## - def clearTree(self): + def clearContent(self): """Clear the GUI content and the related maps. """ self.clear() @@ -131,12 +433,15 @@ class GuiNovelTree(QTreeWidget): self._lastBuild = 0 return - def refreshTree(self, overRide=False): + def refreshTree(self, rootHandle=None, overRide=False): """Called whenever the Novel tab is activated. """ logger.verbose("Requesting refresh of the novel tree") + if rootHandle is None: + rootHandle = self.theProject.tree.findRoot(nwItemClass.NOVEL) + treeChanged = self.mainGui.projView.changedSince(self._lastBuild) - indexChanged = self.theProject.index.indexChangedSince(self._lastBuild) + indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild) if not (treeChanged or indexChanged or overRide): logger.verbose("No changes have been made to the novel index") return @@ -146,7 +451,8 @@ class GuiNovelTree(QTreeWidget): if selItem: titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2] - self._populateTree() + self._populateTree(rootHandle) + self.theProject.setLastNovelViewed(rootHandle) if titleKey is not None and titleKey in self._treeMap: self._treeMap[titleKey].setSelected(True) @@ -162,15 +468,6 @@ class GuiNovelTree(QTreeWidget): self._treeMap[titleKey].setText(self.C_WORDS, f"{wCount:n}") 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. @@ -184,6 +481,17 @@ class GuiNovelTree(QTreeWidget): return tHandle, tLine + def setLastColType(self, colType, doRefresh=True): + """Change the content type of the last column and rebuild. + """ + if self._lastCol != colType: + logger.debug("Changing last column to %s", colType.name) + self._lastCol = colType + self.setColumnHidden(self.C_LAST, colType == NovelTreeColumn.HIDDEN) + if doRefresh: + self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True) + return + ## # Events ## @@ -209,113 +517,94 @@ class GuiNovelTree(QTreeWidget): if tHandle is None: return - self.mainGui.viewDocument(tHandle) + self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") return ## - # Slots + # Private Slots ## - def _treeDoubleClick(self, tItem, tCol): + @pyqtSlot() + def _treeSelectionChange(self): + """Extract the handle and line number of the currently selected + title, and send it to the tree meta panel. + """ + tHandle, _ = self.getSelectedHandle() + if tHandle is not None: + self.novelView.selectedItemChanged.emit(tHandle) + return + + @pyqtSlot("QTreeWidgetItem*", int) + def _treeDoubleClick(self, tItem, colNo): """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. """ tHandle, tLine = self.getSelectedHandle() - self.mainGui.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.mainGui.itemDetails.updateViewBox(tHandle) - + self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, tLine, "") return ## # Internal Functions ## - def _populateTree(self): + def _populateTree(self, rootHandle): """Build the tree based on the project index. """ - self.clearTree() + self.clearContent() + tStart = time() + logger.verbose("Building novel tree for root item '%s'", rootHandle) - currTitle = None - currChapter = None - currScene = None + novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True) + for tKey, tHandle, sTitle, novIdx in novStruct: - for tKey, tHandle, sTitle, novIdx in self.theProject.index.novelStructure(skipExcl=True): + iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0) + if iLevel == 0: + continue - tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx) - self._treeMap[tKey] = tItem + newItem = QTreeWidgetItem() + theData = (tHandle, sTitle[1:].lstrip("0"), tKey) - tLevel = novIdx.level - if tLevel == "H1": - self.addTopLevelItem(tItem) - currTitle = tItem - currChapter = None - currScene = None + newItem.setData(self.C_TITLE, Qt.DecorationRole, self._pIndent[iLevel]) + newItem.setText(self.C_TITLE, 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 == "H2": - if currTitle is None: - self.addTopLevelItem(tItem) - else: - currTitle.addChild(tItem) - currChapter = tItem - currScene = None + lastText, toolTip = self._getLastColumnText(tHandle, sTitle) + newItem.setText(self.C_LAST, lastText) + if lastText: + newItem.setToolTip(self.C_LAST, toolTip) - 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._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) - - wC = int(novIdx.wordCount) - - 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) + if self._lastCol == NovelTreeColumn.HIDDEN: + return "", "" theRefs = self.theProject.index.getReferences(tHandle, sTitle) - newItem.setText(self.C_POV, ", ".join(theRefs[nwKeyWords.POV_KEY])) + if self._lastCol == NovelTreeColumn.POV: + newText = ", ".join(theRefs[nwKeyWords.POV_KEY]) + return newText, f"{self._povLabel}: {newText}" - return newItem + elif self._lastCol == NovelTreeColumn.FOCUS: + newText = ", ".join(theRefs[nwKeyWords.FOCUS_KEY]) + return newText, f"{self._focLabel}: {newText}" + + elif self._lastCol == NovelTreeColumn.PLOT: + newText = ", ".join(theRefs[nwKeyWords.PLOT_KEY]) + return newText, f"{self._pltLabel}: {newText}" + + return "", "" # END Class GuiNovelTree diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index c858c480..4b2077f6 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -117,7 +117,7 @@ class GuiOutlineView(QWidget): self.outlineTree.refreshTree(overRide=overRide, novelChanged=novelChanged) return - def treeFocus(self): + def treeHasFocus(self): return self.outlineTree.hasFocus() def setTreeFocus(self): diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index b9bfda0d..08b24232 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -31,8 +31,8 @@ import novelwriter from enum import Enum from time import time +from PyQt5.QtGui import QPalette from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QIcon, QPalette from PyQt5.QtWidgets import ( QAbstractItemView, QFrame, QHBoxLayout, QHeaderView, QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, @@ -61,7 +61,7 @@ class GuiProjectView(QWidget): # Signals for user interaction with the project tree selectedItemChanged = pyqtSignal(str) - openDocumentRequest = pyqtSignal(str, Enum) + openDocumentRequest = pyqtSignal(str, Enum, int, str) def __init__(self, mainGui): QWidget.__init__(self, mainGui) @@ -142,7 +142,7 @@ class GuiProjectView(QWidget): self.projTree.setFocus() return - def treeFocus(self): + def treeHasFocus(self): """Check if the project tree has focus. """ return self.projTree.hasFocus() @@ -177,7 +177,7 @@ class GuiProjectToolBar(QWidget): self.mainTheme = projView.mainGui.mainTheme iPx = self.mainTheme.baseIconSize - mPx = self.mainConf.pxInt(4) + mPx = self.mainConf.pxInt(3) self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) @@ -192,10 +192,10 @@ class GuiProjectToolBar(QWidget): "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" ).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue()) - # Tree Label - self.projLabel = QLabel("%s" % self.tr("Project Content")) - self.projLabel.setContentsMargins(0, 0, 0, 0) - self.projLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + # Widget Label + self.viewLabel = QLabel("%s" % self.tr("Project Content")) + self.viewLabel.setContentsMargins(0, 0, 0, 0) + self.viewLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # Move Buttons self.tbMoveU = QToolButton(self) @@ -252,7 +252,7 @@ class GuiProjectToolBar(QWidget): self._addRootFolderEntry(nwItemClass.PLOT) self._addRootFolderEntry(nwItemClass.CHARACTER) self._addRootFolderEntry(nwItemClass.WORLD) - self._addRootFolderEntry(nwItemClass.ARCHIVE) + self._addRootFolderEntry(nwItemClass.TIMELINE) self._addRootFolderEntry(nwItemClass.OBJECT) self._addRootFolderEntry(nwItemClass.ENTITY) self._addRootFolderEntry(nwItemClass.CUSTOM) @@ -285,7 +285,7 @@ class GuiProjectToolBar(QWidget): # Assemble self.outerBox = QHBoxLayout() - self.outerBox.addWidget(self.projLabel) + self.outerBox.addWidget(self.viewLabel) self.outerBox.addWidget(self.tbMoveU) self.outerBox.addWidget(self.tbMoveD) self.outerBox.addWidget(self.tbAdd) @@ -337,9 +337,8 @@ class GuiProjectTree(QTreeWidget): self._lastMove = {} self._timeChanged = 0 - ## - # Build GUI - ## + # Build GUI + # ========= # Context Menu self.setContextMenuPolicy(Qt.CustomContextMenu) @@ -348,6 +347,7 @@ class GuiProjectTree(QTreeWidget): # Tree Settings iPx = self.mainTheme.baseIconSize cMg = self.mainConf.pxInt(6) + self.setIconSize(QSize(iPx, iPx)) self.setFrameStyle(QFrame.NoFrame) self.setExpandsOnDoubleClick(False) @@ -778,13 +778,6 @@ class GuiProjectTree(QTreeWidget): if trItem is None or nwItem is None: return - expIcon = QIcon() - if nwItem.itemType == nwItemType.FILE: - if nwItem.isExported: - expIcon = self.mainTheme.getIcon("check") - else: - expIcon = self.mainTheme.getIcon("cross") - itemStatus, statusIcon = nwItem.getImportStatus() hLevel = self.theProject.index.getHandleHeaderLevel(tHandle) itemIcon = self.mainTheme.getItemIcon( @@ -793,18 +786,18 @@ class GuiProjectTree(QTreeWidget): trItem.setIcon(self.C_NAME, itemIcon) trItem.setText(self.C_NAME, nwItem.itemName) - trItem.setIcon(self.C_EXPORT, expIcon) trItem.setIcon(self.C_STATUS, statusIcon) trItem.setToolTip(self.C_STATUS, itemStatus) + if nwItem.itemType == nwItemType.FILE: + trItem.setIcon( + self.C_EXPORT, self.mainTheme.getIcon("check" if nwItem.isExported else "cross") + ) + if self.mainConf.emphLabels and nwItem.itemLayout == nwItemLayout.DOCUMENT: trFont = trItem.font(self.C_NAME) - if hLevel in ("H1", "H2"): - trFont.setBold(True) - trFont.setUnderline(True) - else: - trFont.setBold(False) - trFont.setUnderline(False) + trFont.setBold(hLevel == "H1" or hLevel == "H2") + trFont.setUnderline(hLevel == "H1") trItem.setFont(self.C_NAME, trFont) return @@ -972,7 +965,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "") else: trItem = self._getTreeItem(tHandle) if trItem is not None: @@ -1016,11 +1009,11 @@ class GuiProjectTree(QTreeWidget): if isFile: ctxMenu.addAction( self.tr("Open Document"), - lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT) + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "") ) ctxMenu.addAction( self.tr("View Document"), - lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") ) ctxMenu.addSeparator() @@ -1112,7 +1105,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.itemType == nwItemType.FILE: - self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW) + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") return diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 3a59e7e2..6c4ff50e 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -472,9 +472,12 @@ class GuiIcons: # Switches "sticky-on", "sticky-off", "bullet-on", "bullet-off", + + # Decorations + "deco_doc_h0", "deco_doc_h1", "deco_doc_h2", "deco_doc_h3", "deco_doc_h4", } - DECO_MAP = { + IMAGE_MAP = { "wiz-back": "wizard-back.jpg", } @@ -573,19 +576,22 @@ class GuiIcons: # Access Functions ## - def loadDecoration(self, decoKey, pxW, pxH): + def loadDecoration(self, decoKey, pxW=None, pxH=None): """Load graphical decoration element based on the decoration - map. This function always returns a QSwgWidget. + map or the icon map. This function always returns a QPixmap. """ - if decoKey not in self.DECO_MAP: + if decoKey in self._themeMap: + imgPath = self._themeMap[decoKey] + elif decoKey in self.IMAGE_MAP: + imgPath = os.path.join( + self.mainConf.assetPath, "images", self.IMAGE_MAP[decoKey] + ) + else: logger.error("Decoration with name '%s' does not exist", decoKey) return QPixmap() - imgPath = os.path.join( - self.mainConf.assetPath, "images", self.DECO_MAP[decoKey] - ) if not os.path.isfile(imgPath): - logger.error("Decoration file '%s' not in assets folder", self.DECO_MAP[decoKey]) + logger.error("Asset '%s' not found", self.IMAGE_MAP[decoKey]) return QPixmap() theDeco = QPixmap(imgPath) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index e15a82b0..5030347b 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -40,7 +40,7 @@ from PyQt5.QtWidgets import ( from novelwriter.gui import ( GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu, - GuiMainStatus, GuiNovelTree, GuiOutlineView, GuiProjectView, GuiTheme, + GuiMainStatus, GuiNovelView, GuiOutlineView, GuiProjectView, GuiTheme, GuiViewsBar ) from novelwriter.dialogs import ( @@ -106,7 +106,7 @@ class GuiMain(QMainWindow): # Main GUI Elements self.statusBar = GuiMainStatus(self) self.projView = GuiProjectView(self) - self.novelView = GuiNovelTree(self) + self.novelView = GuiNovelView(self) self.docEditor = GuiDocEditor(self) self.viewMeta = GuiDocViewDetails(self) self.docViewer = GuiDocViewer(self) @@ -205,6 +205,10 @@ class GuiMain(QMainWindow): self.projView.treeItemChanged.connect(self.docViewer.updateDocInfo) self.projView.treeItemChanged.connect(self.itemDetails.updateViewBox) self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem) + self.projView.rootFolderChanged.connect(self.novelView.updateRootItem) + + self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox) + self.novelView.openDocumentRequest.connect(self._openDocument) self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus) @@ -292,7 +296,7 @@ class GuiMain(QMainWindow): """ # Project Area self.projView.clearProject() - self.novelView.clearTree() + self.novelView.clearProject() self.itemDetails.clearDetails() # Work Area @@ -362,6 +366,7 @@ class GuiMain(QMainWindow): self.saveProject() self.docEditor.setDictionaries() self.outlineView.updateRootItem(None) + self.novelView.openProjectTasks() self.rebuildIndex(beQuiet=True) self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setProjectStatus(nwState.GOOD) @@ -418,6 +423,7 @@ class GuiMain(QMainWindow): self.closeDocument() self.docViewer.clearNavHistory() self.outlineView.closeOutline() + self.novelView.closeProjectTasks() self.theProject.closeProject(self.idleTime) self.idleRefTime = time() @@ -509,6 +515,7 @@ class GuiMain(QMainWindow): self.docEditor.toggleSpellCheck(self.theProject.spellCheck) self.statusBar.setRefTime(self.theProject.projOpened) self.outlineView.updateRootItem(None) + self.novelView.openProjectTasks() self._updateStatusWordCount() # Restore previously open documents, if any @@ -789,11 +796,11 @@ class GuiMain(QMainWindow): tHandle = None tLine = None - if self.projView.treeFocus(): + if self.projView.treeHasFocus(): tHandle = self.projView.getSelectedHandle() - elif self.novelView.hasFocus(): + elif self.novelView.treeHasFocus(): tHandle, tLine = self.novelView.getSelectedHandle() - elif self.outlineView.treeFocus(): + elif self.outlineView.treeHasFocus(): tHandle, tLine = self.outlineView.getSelectedHandle() else: logger.warning("No item selected") @@ -825,7 +832,7 @@ class GuiMain(QMainWindow): """Rebuild the project tree. """ self.projView.populateTree() - self.novelView.refreshTree() + # self.novelView.refreshTree() return def requestNovelTreeRefresh(self): @@ -920,7 +927,7 @@ class GuiMain(QMainWindow): self.docEditor.initEditor() self.docViewer.initViewer() self.projView.initSettings() - self.novelView.initTree() + self.novelView.initSettings() self.outlineView.initOutline() self._updateStatusWordCount() @@ -1161,7 +1168,6 @@ class GuiMain(QMainWindow): self.mainConf.setViewPanePos(self.splitView.sizes()) self.mainConf.setShowRefPanel(self.viewMeta.isVisible()) - self.mainConf.setNovelColWidths(self.novelView.getColumnSizes()) if not self.mainConf.isFullScreen: self.mainConf.setWinSize(self.width(), self.height()) @@ -1469,15 +1475,15 @@ class GuiMain(QMainWindow): self.viewDocument(tHandle=tHandle, tAnchor=f"#{sTitle}") return - @pyqtSlot(str, Enum) - def _openDocument(self, tHandle, tMode): - """Handle an open document request. + @pyqtSlot(str, Enum, int, str) + def _openDocument(self, tHandle, tMode, tLine, tAnchor): + """Handle an open document request from one of the tree views. """ if tHandle is not None: if tMode == nwDocMode.EDIT: - self.openDocument(tHandle, changeFocus=False) + self.openDocument(tHandle, tLine=tLine, changeFocus=False) elif tMode == nwDocMode.VIEW: - self.viewDocument(tHandle=tHandle) + self.viewDocument(tHandle=tHandle, tAnchor=(tAnchor or None)) return @pyqtSlot(nwView) @@ -1576,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() diff --git a/sample/content/a520879ca0b45.nwd b/sample/content/a520879ca0b45.nwd index 12d7a62a..75ba39c8 100644 --- a/sample/content/a520879ca0b45.nwd +++ b/sample/content/a520879ca0b45.nwd @@ -1,7 +1,7 @@ %%~name: Chapter One %%~path: e5e47ebf63b1c/a520879ca0b45 %%~kind: NOVEL/DOCUMENT -### Chapter One +## Chapter One @pov: Jane diff --git a/sample/content/edca4be2fcaf8.nwd b/sample/content/edca4be2fcaf8.nwd index 3028b3fd..8b3efc78 100644 --- a/sample/content/edca4be2fcaf8.nwd +++ b/sample/content/edca4be2fcaf8.nwd @@ -3,4 +3,4 @@ %%~kind: NOVEL/DOCUMENT # Part One ->> In the beginning … << \ No newline at end of file +>> In the beginning … << diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index e8ee5c59..6ba4b5f1 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1345 - 229 - 68286 + 1371 + 236 + 69222 False @@ -17,6 +17,8 @@ True 636b6aa9b697b 636b6aa9b697b + 7031beac91f75 + None 1363 954 409 @@ -33,9 +35,9 @@
- New + New Notes - Started + Started 1st Draft 2nd Draft 3rd Draft @@ -70,11 +72,11 @@ Chapter One - + Making a Scene - + Another Scene @@ -86,11 +88,11 @@ A Note on Structure - + Chapter Two - + We Found John! @@ -99,7 +101,7 @@ - Title Page + Title Page diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index 02e3df49..a8cf6393 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -16,6 +16,8 @@ True 7a992350f3eb6 None + None + None 3847 3109 738 diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index af7595a4..430ecf17 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -17,6 +17,8 @@ True None None + None + None 10 10 0 diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index be4845bf..f046fceb 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -17,6 +17,8 @@ True None None + None + None 0 0 0 diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 12f911cd..5d02172c 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -17,6 +17,8 @@ True None None + None + None 0 0 0 diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index 4235e9bb..20aeb027 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -16,6 +16,8 @@ True None None + None + None 2 1 1 diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 422bb6b2..ba08600f 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -15,6 +15,8 @@ True None None + None + None 0 0 0 diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index cd25c1cf..0a606137 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -16,6 +16,8 @@ True None None + None + None 0 0 0 diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index a47d454d..ecc96604 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -16,6 +16,8 @@ True 000000000000f None + 0000000000008 + None 129 102 27 diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 03313570..1a79d2c2 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -16,6 +16,8 @@ True None None + None + None 9 9 0 diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index f12c1dc2..883cb26d 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -17,6 +17,8 @@ True None None + None + None 9 9 0 diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index bdc27771..a6409b98 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -470,7 +470,7 @@ def testCoreProject_Open(monkeypatch, nwMinimal, mockGUI): writeFile(os.path.join(nwMinimal, "data_0", "123456789abc_main.bak"), "stuff") mockGUI.clear() assert theProject.openProject(nwMinimal) is True - assert "version 1.0" in mockGUI.lastAlert + assert "There was an error updating the project." in mockGUI.lastAlert assert theProject.closeProject() diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 5fa6951b..6407b84b 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -28,8 +28,8 @@ from tools import cmpFiles, buildTestProject, XML_IGNORE, writeFile from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox, QInputDialog -from novelwriter.gui import GuiDocEditor, GuiNovelTree, GuiOutlineView -from novelwriter.enum import nwItemType, nwWidget +from novelwriter.gui import GuiDocEditor, GuiNovelView, GuiOutlineView +from novelwriter.enum import nwItemType, nwView, nwWidget from novelwriter.tools import GuiProjectWizard from novelwriter.gui.projtree import GuiProjectTree from novelwriter.dialogs import GuiEditLabel @@ -120,6 +120,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwGUI.openSelectedItem() is False # Project Tree has focus + nwGUI._changeView(nwView.PROJECT) nwGUI.switchFocus(nwWidget.TREE) nwGUI.projStack.setCurrentIndex(0) with monkeypatch.context() as mp: @@ -131,23 +132,22 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwGUI.closeDocument() is True # Novel Tree has focus - nwGUI.projStack.setCurrentIndex(1) - nwGUI.novelView.refreshTree(True) + nwGUI._changeView(nwView.NOVEL) + nwGUI.novelView.novelTree.refreshTree(rootHandle=None, overRide=True) with monkeypatch.context() as mp: - mp.setattr(GuiNovelTree, "hasFocus", lambda *a: True) + mp.setattr(GuiNovelView, "treeHasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None - actItem = nwGUI.novelView.topLevelItem(0) - chpItem = actItem.child(0) - selItem = chpItem.child(0) - nwGUI.novelView.setCurrentItem(selItem) + selItem = nwGUI.novelView.novelTree.topLevelItem(2) + nwGUI.novelView.novelTree.setCurrentItem(selItem) nwGUI._keyPressReturn() assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.closeDocument() is True # Project Outline has focus + nwGUI._changeView(nwView.OUTLINE) nwGUI.switchFocus(nwWidget.OUTLINE) with monkeypatch.context() as mp: - mp.setattr(GuiOutlineView, "treeFocus", lambda *a: True) + mp.setattr(GuiOutlineView, "treeHasFocus", lambda *a: True) assert nwGUI.docEditor.docHandle() is None actItem = nwGUI.outlineView.outlineTree.topLevelItem(0) chpItem = actItem.child(0) @@ -157,7 +157,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.closeDocument() is True - # qtbot.stopForInteraction() + # qtbot.stop() # END Test testGuiMain_ProjectTreeItems diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 2bf2df8a..102b03d2 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -19,73 +19,93 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import pytest import os +import pytest -from tools import writeFile +from tools import buildTestProject, writeFile from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox +from novelwriter.enum import nwWidget, nwItemType +from novelwriter.dialogs import GuiEditLabel +from novelwriter.gui.noveltree import NovelTreeColumn + @pytest.mark.gui -def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): +def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): """Test navigating the novel tree. """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - nwGUI.openProject(nwMinimal) - nwTree = nwGUI.novelView + buildTestProject(nwGUI, fncProj) - ## - # Show/Hide Scrollbars - ## + nwGUI.switchFocus(nwWidget.TREE) + nwGUI.projView.projTree.clearSelection() + nwGUI.projView.projTree._getTreeItem("000000000000a").setSelected(True) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE) - nwTree.mainConf.hideVScroll = True - nwTree.mainConf.hideHScroll = True - nwTree.initTree() - assert not nwTree.verticalScrollBar().isVisible() - assert not nwTree.horizontalScrollBar().isVisible() + writeFile( + os.path.join(nwGUI.theProject.projContent, "0000000000010.nwd"), + "# Jane Doe\n\n@tag: Jane\n\n" + ) + writeFile( + os.path.join(nwGUI.theProject.projContent, "000000000000f.nwd"), + "### Scene One\n\n@pov: Jane\n@focus: Jane\n\n" + ) - nwTree.mainConf.hideVScroll = False - nwTree.mainConf.hideHScroll = False - nwTree.initTree() - assert nwTree.verticalScrollBar().isEnabled() - assert nwTree.horizontalScrollBar().isEnabled() + novelView = nwGUI.novelView + novelTree = novelView.novelTree + novelBar = novelView.novelBar - ## - # Populate Tree - ## + # Show/Hide Scrollbars + # ==================== + + nwGUI.mainConf.hideVScroll = True + nwGUI.mainConf.hideHScroll = True + novelView.initSettings() + assert not novelTree.verticalScrollBar().isVisible() + assert not novelTree.horizontalScrollBar().isVisible() + + nwGUI.mainConf.hideVScroll = False + nwGUI.mainConf.hideHScroll = False + novelView.initSettings() + assert novelTree.verticalScrollBar().isEnabled() + assert novelTree.horizontalScrollBar().isEnabled() + + # Populate Tree + # ============= nwGUI.projStack.setCurrentIndex(nwGUI.idxNovelView) nwGUI.rebuildIndex() - nwTree._populateTree() - assert nwTree.topLevelItemCount() == 1 + novelTree._populateTree(rootHandle=None) + assert novelTree.topLevelItemCount() == 3 # Rebuild should preserve selection - topItem = nwTree.topLevelItem(0) + topItem = novelTree.topLevelItem(0) assert not topItem.isSelected() topItem.setSelected(True) - assert nwTree.selectedItems()[0] == topItem - assert nwTree.getSelectedHandle() == ("a35baf2e93843", 0) + assert novelTree.selectedItems()[0] == topItem + assert novelView.getSelectedHandle() == ("000000000000c", 0) - nwTree.refreshTree() - assert nwTree.topLevelItem(0).isSelected() + # Refresh using the slot for the butoom + novelBar._refreshNovelTree() + assert novelTree.topLevelItem(0).isSelected() - ## - # Open Items - ## + # Open Items + # ========== # Clear selection - nwTree.clearSelection() - scItem = nwTree.topLevelItem(0).child(0).child(0) + novelTree.clearSelection() + scItem = novelTree.topLevelItem(2) scItem.setSelected(True) assert scItem.isSelected() # Clear selection with mouse - vPort = nwTree.viewport() + vPort = novelTree.viewport() qtbot.mouseClick(vPort, Qt.LeftButton, pos=vPort.rect().center(), delay=10) assert not scItem.isSelected() @@ -93,8 +113,8 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): scItem.setSelected(True) assert scItem.isSelected() assert nwGUI.docEditor.docHandle() is None - nwTree._treeDoubleClick(scItem, 0) - assert nwGUI.docEditor.docHandle() == "8c659a11cd429" + novelTree._treeDoubleClick(scItem, 0) + assert nwGUI.docEditor.docHandle() == "000000000000f" # Open item with middle mouse button scItem.setSelected(True) @@ -103,58 +123,52 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal): qtbot.mouseClick(vPort, Qt.MiddleButton, pos=vPort.rect().center(), delay=10) assert nwGUI.docViewer.docHandle() is None - scRect = nwTree.visualItemRect(scItem) - oldData = scItem.data(nwTree.C_TITLE, Qt.UserRole) - scItem.setData(nwTree.C_TITLE, Qt.UserRole, (None, "", "")) + scRect = novelTree.visualItemRect(scItem) + oldData = scItem.data(novelTree.C_TITLE, Qt.UserRole) + scItem.setData(novelTree.C_TITLE, Qt.UserRole, (None, "", "")) qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) assert nwGUI.docViewer.docHandle() is None - scItem.setData(nwTree.C_TITLE, Qt.UserRole, oldData) + scItem.setData(novelTree.C_TITLE, Qt.UserRole, oldData) qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) - assert nwGUI.docViewer.docHandle() == "8c659a11cd429" + assert nwGUI.docViewer.docHandle() == "000000000000f" - ## - # Populate Tree - ## + # Last Column + # =========== - # 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" + novelBar.setLastColType(NovelTreeColumn.HIDDEN) + assert novelTree.isColumnHidden(novelTree.C_LAST) is True + assert novelTree.lastColType == NovelTreeColumn.HIDDEN + assert novelTree._getLastColumnText("000000000000f", "T000001") == ("", "") - 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" + novelBar.setLastColType(NovelTreeColumn.POV) + assert novelTree.isColumnHidden(novelTree.C_LAST) is False + assert novelTree.lastColType == NovelTreeColumn.POV + assert novelTree._getLastColumnText("000000000000f", "T000001") == ( + "Jane", "Point of View: Jane" + ) - 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" + novelBar.setLastColType(NovelTreeColumn.FOCUS) + assert novelTree.isColumnHidden(novelTree.C_LAST) is False + assert novelTree.lastColType == NovelTreeColumn.FOCUS + assert novelTree._getLastColumnText("000000000000f", "T000001") == ( + "Jane", "Focus: Jane" + ) - tScene = tChap.child(1) - assert tScene.child(0).text(nwTree.C_TITLE) == "Section" + novelBar.setLastColType(NovelTreeColumn.PLOT) + assert novelTree.isColumnHidden(novelTree.C_LAST) is False + assert novelTree.lastColType == NovelTreeColumn.PLOT + assert novelTree._getLastColumnText("000000000000f", "T000001") == ( + "", "Plot: " + ) - ## - # Close - ## + novelTree._lastCol = None + assert novelTree._getLastColumnText("0000000000000", "T000000") == ("", "") - # qtbot.stopForInteraction() + # Close + # ===== + + # qtbot.stop() nwGUI.closeProject() # END Test testGuiNovelTree_TreeItems diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 1038404a..26ca9803 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -71,7 +71,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir): # Check focus with monkeypatch.context() as mp: mp.setattr(QWidget, "hasFocus", lambda *a: True) - assert outlineView.treeFocus() is True + assert outlineView.treeHasFocus() is True outlineView.setTreeFocus() # Can't check. just ensures that it doesn't error diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 756b2a81..8c7b934a 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -127,7 +127,7 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir): assert anImg.isNull() # Add a non-existent file and request it - iconCache.DECO_MAP["nonsense"] = "nofile.jpg" + iconCache.IMAGE_MAP["nonsense"] = "nofile.jpg" anImg = iconCache.loadDecoration("nonsense", 20, 20) assert isinstance(anImg, QPixmap) assert anImg.isNull()