diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index c2683cc7..43f3297e 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": @@ -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/noveltree.py b/novelwriter/gui/noveltree.py index 85773478..8cec9fdb 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -27,25 +27,34 @@ import logging import novelwriter from time import time +from enum import Enum -from PyQt5.QtCore import Qt, QSize +from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal +from PyQt5.QtGui import QPalette from PyQt5.QtWidgets import ( - QAbstractItemView, QFrame, QTreeWidget, QTreeWidgetItem, QVBoxLayout, + 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 nwKeyWords, nwLabels logger = logging.getLogger(__name__) 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.mainGui = mainGui + self.theProject = mainGui.theProject # Build GUI self.novelTree = GuiNovelTree(self) @@ -60,6 +69,11 @@ class GuiNovelView(QWidget): self.setLayout(self.outerBox) + # Connect Signals + self.novelBar.rootFolderSelectionChanged.connect( + lambda tHandle: self.novelTree.refreshTree(rootHandle=tHandle, overRide=True) + ) + # Function Mappings self.refreshTree = self.novelTree.refreshTree self.updateWordCounts = self.novelTree.updateWordCounts @@ -79,6 +93,21 @@ class GuiNovelView(QWidget): self.novelTree.clearTree() return + def openProjectTasks(self): + """Run tasks related to opening a project. + """ + lastNovel = self.theProject.lastNovel + if lastNovel is None: + lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL) + + logger.debug("Setting novel tree to root item '%s'", lastNovel) + + self.clearProject() + self.novelBar.rebuildNovelRootMenu(selHandle=lastNovel) + self.novelTree.refreshTree(rootHandle=lastNovel, overRide=True) + + return + def setFocus(self): """Forward the set focus call to the tree widget. """ @@ -90,16 +119,114 @@ class GuiNovelView(QWidget): """ return self.novelTree.hasFocus() + ## + # Public Slots + ## + + @pyqtSlot(str) + def updateRootItem(self, tHandle): + """Should be called whenever a root folders changes. + """ + self.novelBar.rebuildNovelRootMenu() + return + # END Class GuiNovelView class GuiNovelToolBar(QWidget): + rootFolderSelectionChanged = pyqtSignal(str) + def __init__(self, novelView): QTreeWidget.__init__(self, novelView) - self.mainConf = novelwriter.CONFIG - self.novelView = 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(4) + + 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) + + # Novel Root Menu + self.mRoot = QMenu() + + 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.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.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 rebuildNovelRootMenu(self, selHandle=None): + """Build the novel root menu. + """ + self.mRoot.clear() + + agRoot = QActionGroup(self.mRoot) + 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.rootFolderSelectionChanged.emit(tHandle) + ) + agRoot.addAction(aRoot) + + if n == 0: + aRoot.setChecked(True) + if selHandle == tHandle: + aRoot.setChecked(True) return @@ -128,41 +255,32 @@ class GuiNovelTree(QTreeWidget): self._lastBuild = 0 # Build GUI + # ========= + iPx = self.mainTheme.baseIconSize - self.setFrameStyle(QFrame.NoFrame) + cMg = self.mainConf.pxInt(6) + self.setIconSize(QSize(iPx, iPx)) + self.setFrameStyle(QFrame.NoFrame) + self.setHeaderHidden(True) self.setIndentation(iPx) 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_POV, 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) - - # The last column should just auto-scale - self.resizeColumnToContents(self.C_POV) + # Connect signals + self.itemDoubleClicked.connect(self._treeDoubleClick) + self.itemSelectionChanged.connect(self._treeSelectionChange) # Set custom settings self.initSettings() @@ -199,12 +317,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 @@ -214,7 +335,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) @@ -268,39 +390,39 @@ 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() @@ -309,7 +431,9 @@ class GuiNovelTree(QTreeWidget): currChapter = None currScene = None - for tKey, tHandle, sTitle, novIdx in self.theProject.index.novelStructure(skipExcl=True): + 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 diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index b9bfda0d..4f320bfc 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -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) @@ -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) @@ -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) @@ -972,7 +972,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 +1016,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 +1112,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/guimain.py b/novelwriter/guimain.py index 6fa52eb6..e06b9edd 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -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) @@ -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) @@ -509,6 +514,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 @@ -825,7 +831,7 @@ class GuiMain(QMainWindow): """Rebuild the project tree. """ self.projView.populateTree() - self.novelView.refreshTree() + # self.novelView.refreshTree() return def requestNovelTreeRefresh(self): @@ -1468,15 +1474,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) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index e8ee5c59..af509400 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1345 - 229 - 68286 + 1367 + 231 + 68784 False @@ -17,6 +17,8 @@ True 636b6aa9b697b 636b6aa9b697b + 7031beac91f75 + None 1363 954 409 @@ -70,11 +72,11 @@ Chapter One - + Making a Scene - + Another Scene @@ -86,7 +88,7 @@ A Note on Structure - + Chapter Two