diff --git a/docs/source/usage_shortcuts.rst b/docs/source/usage_shortcuts.rst index fea65c8c..69c2e773 100644 --- a/docs/source/usage_shortcuts.rst +++ b/docs/source/usage_shortcuts.rst @@ -59,7 +59,6 @@ The main shorcuts are as follows: ":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo." ":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes." ":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking." - ":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline." ":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree." ":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree." ":kbd:`Ctrl`:kbd:`Del`", "Delete next word in editor." @@ -88,7 +87,6 @@ The main shorcuts are as follows: ":kbd:`F7`", "Re-run spell checker." ":kbd:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer." ":kbd:`F9`", "Re-build the project index." - ":kbd:`F10`", "Re-build the project outline." ":kbd:`F11`", "Activate full screen mode." ":kbd:`Shift`:kbd:`F1`", "Open the local user manual (PDF) if it is available." ":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document." diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 11842a5a..d151f370 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -273,6 +273,15 @@ class NWTree(): rootClasses.add(nwItem.itemClass) return rootClasses + def novelRoots(self): + """Return a doctionary of all novel-like root items. + """ + novelItems = {} + for tHandle, nwItem in self._treeRoots.items(): + if nwItem.isNovelLike(): + novelItems[tHandle] = nwItem + return novelItems + def isRoot(self, tHandle): """Check if a handle is a root item. """ diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index f69d5a56..bd8d1edd 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -81,12 +81,6 @@ class GuiMainMenu(QMenuBar): self.aSpellCheck.setChecked(theMode) return - def setAutoOutline(self, theMode): - """Forward auto outline check state to its action. - """ - self.aAutoOutline.setChecked(theMode) - return - def setFocusMode(self, theMode): """Forward focus mode check state to its action. """ @@ -105,12 +99,6 @@ class GuiMainMenu(QMenuBar): self.theParent.docEditor.toggleSpellCheck(None) return True - def _toggleAutoOutline(self, theMode): - """Toggle auto outline when the menu entry is checked. - """ - self.theProject.setAutoOutline(theMode) - return True - def _openWebsite(self, theUrl): """Open a URL in the system's default browser. """ @@ -889,19 +877,6 @@ class GuiMainMenu(QMenuBar): self.aRebuildIndex.triggered.connect(lambda: self.theParent.rebuildIndex()) self.toolsMenu.addAction(self.aRebuildIndex) - # Tools > Rebuild Outline - self.aRebuildOutline = QAction(self.tr("Rebuild Outline"), self) - self.aRebuildOutline.setShortcut("F10") - self.aRebuildOutline.triggered.connect(lambda: self.theParent.rebuildOutline()) - self.toolsMenu.addAction(self.aRebuildOutline) - - # Tools > Toggle Auto Build Outline - self.aAutoOutline = QAction(self.tr("Auto-Update Outline"), self) - self.aAutoOutline.setCheckable(True) - self.aAutoOutline.toggled.connect(self._toggleAutoOutline) - self.aAutoOutline.setShortcut("Ctrl+F10") - self.toolsMenu.addAction(self.aAutoOutline) - # Tools > Separator self.toolsMenu.addSeparator() diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index c29b9ff9..6b50df5f 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -4,9 +4,11 @@ novelWriter – GUI Project Outline GUI class for the project outline view File History: -Created: 2019-11-16 [0.4.1] GuiOutlineView, GuiOutlineHeaderMenu -Created: 2020-06-02 [0.7.0] GuiOutlineDetails Created: 2022-05-15 [1.7b1] GuiOutline +Created: 2022-05-22 [1.7b1] GuiOutlineToolBar +Created: 2019-11-16 [0.4.1] GuiOutlineView +Created: 2019-11-16 [0.4.1] GuiOutlineHeaderMenu +Created: 2020-06-02 [0.7.0] GuiOutlineDetails This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen @@ -29,6 +31,7 @@ import logging import novelwriter from time import time +from enum import Enum from PyQt5.QtCore import ( Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP @@ -36,7 +39,7 @@ from PyQt5.QtCore import ( from PyQt5.QtWidgets import ( QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout, - QWidget, QFrame + QWidget, QFrame, QToolBar, QSizePolicy, QComboBox, QToolButton ) from novelwriter.enum import ( @@ -60,6 +63,7 @@ class GuiOutline(QWidget): self.theParent = theParent self.theProject = theParent.theProject + self.outlineBar = GuiOutlineToolBar(self) self.outlineView = GuiOutlineView(self) self.outlineData = GuiOutlineDetails(self) @@ -71,13 +75,20 @@ class GuiOutline(QWidget): # Assemble self.outerBox = QVBoxLayout() self.outerBox.setContentsMargins(0, 0, 0, 0) + self.outerBox.addWidget(self.outlineBar) self.outerBox.addWidget(self.splitOutline) self.setLayout(self.outerBox) + # Connect Signals + self.outlineView.hiddenStateChanged.connect(self._updateMenuColumns) + self.outlineBar.columnToggled.connect(self.outlineView.menuColumnToggled) + self.outlineBar.viewRefreshRequested.connect( + lambda: self.outlineView.refreshTree(overRide=True) + ) + # Function Mappings self.getSelectedHandle = self.outlineView.getSelectedHandle - self.updateClasses = self.outlineData.updateClasses return @@ -112,9 +123,112 @@ class GuiOutline(QWidget): def setTreeFocus(self): return self.outlineView.setFocus() + ## + # Slots + ## + + @pyqtSlot() + def projectUpdated(self): + """Should be called whenever the number of root folders change. + """ + self.outlineBar.populateNovelList() + self.outlineData.updateClasses() + return + + @pyqtSlot() + def _updateMenuColumns(self): + """Trigger an update of the toggled state of the column menu + checkboxes whenever a signal is received that the hidden state + of columns has changed. + """ + self.outlineBar.setColumnHiddenState(self.outlineView.hiddenColumns) + return + # END Class GuiOutline +class GuiOutlineToolBar(QToolBar): + + columnToggled = pyqtSignal(bool, Enum) + viewRefreshRequested = pyqtSignal() + + def __init__(self, theOutline): + QTreeWidget.__init__(self, theOutline) + + logger.debug("Initialising GuiOutlineToolBar ...") + + self.mainConf = novelwriter.CONFIG + self.theOutline = theOutline + self.theParent = theOutline.theParent + self.theProject = theOutline.theParent.theProject + self.theTheme = theOutline.theParent.theTheme + + iPx = self.mainConf.pxInt(22) + mPx = self.mainConf.pxInt(12) + + self.setMovable(False) + self.setIconSize(QSize(iPx, iPx)) + self.setContentsMargins(0, 0, 0, 0) + self.setStyleSheet("QToolBar {border: 0px;}") + + stretch = QWidget(self) + stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Novel Selector + self.novelLabel = QLabel(self.tr("Outline of")) + self.novelLabel.setContentsMargins(0, 0, mPx, 0) + + self.novelValue = QComboBox(self) + self.novelValue.setMinimumWidth(self.mainConf.pxInt(200)) + + # Actions + self.aRefresh = QAction(self.tr("Refresh"), self) + self.aRefresh.setIcon(self.theTheme.getIcon("refresh")) + self.aRefresh.triggered.connect( + lambda: self.viewRefreshRequested.emit() + ) + + # Column Menu + self.mColumns = GuiOutlineHeaderMenu(self) + self.mColumns.columnToggled.connect( + lambda isChecked, tItem: self.columnToggled.emit(isChecked, tItem) + ) + + self.tbColumns = QToolButton(self) + self.tbColumns.setIcon(self.theTheme.getIcon("menu")) + self.tbColumns.setMenu(self.mColumns) + self.tbColumns.setPopupMode(QToolButton.InstantPopup) + + # Assemble + self.addWidget(self.novelLabel) + self.addWidget(self.novelValue) + self.addSeparator() + self.addAction(self.aRefresh) + self.addWidget(self.tbColumns) + self.addWidget(stretch) + + self.populateNovelList() + + logger.debug("GuiOutlineToolBar initialisation complete") + + def populateNovelList(self): + """Fill the novel combo box. + """ + self.novelValue.clear() + for tHandle, nwItem in self.theProject.projTree.novelRoots().items(): + self.novelValue.addItem( + self.theTheme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]), + nwItem.itemName, tHandle + ) + return + + def setColumnHiddenState(self, hiddenState): + self.mColumns.setHiddenState(hiddenState) + return + +# END Class GuiOutlineToolBar + + class GuiOutlineView(QTreeWidget): DEF_WIDTH = { @@ -157,10 +271,12 @@ class GuiOutlineView(QTreeWidget): nwOutline.SYNOP: False, } + hiddenStateChanged = pyqtSignal() + def __init__(self, theOutline): QTreeWidget.__init__(self, theOutline) - logger.debug("Initialising GuiOutline ...") + logger.debug("Initialising GuiOutlineView ...") self.mainConf = novelwriter.CONFIG self.theOutline = theOutline @@ -169,7 +285,6 @@ class GuiOutlineView(QTreeWidget): self.theTheme = theOutline.theParent.theTheme self.theIndex = theOutline.theParent.theIndex self.optState = theOutline.theParent.theProject.optState - self.headerMenu = GuiOutlineHeaderMenu(self) self.setFrameStyle(QFrame.NoFrame) self.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -184,8 +299,6 @@ class GuiOutlineView(QTreeWidget): self.setIndentation(iPx) self.treeHead = self.header() - self.treeHead.setContextMenuPolicy(Qt.CustomContextMenu) - self.treeHead.customContextMenuRequested.connect(self._headerRightClick) self.treeHead.sectionMoved.connect(self._columnMoved) # Internals @@ -199,12 +312,25 @@ class GuiOutlineView(QTreeWidget): self.initOutline() self.clearOutline() - self.headerMenu.setHiddenState(self._colHidden) - logger.debug("GuiOutline initialisation complete") + self.hiddenStateChanged.emit() + + logger.debug("GuiOutlineView initialisation complete") return + ## + # Properties + ## + + @property + def hiddenColumns(self): + return self._colHidden + + ## + # Methods + ## + def initOutline(self): """Set or update outline settings. """ @@ -314,13 +440,6 @@ class GuiOutlineView(QTreeWidget): return - @pyqtSlot("QPoint") - def _headerRightClick(self, clickPos): - """Show the header column menu. - """ - self.headerMenu.exec_(self.mapToGlobal(clickPos)) - return - @pyqtSlot(int, int, int) def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx): """Make sure the order array is up to date with the actual order @@ -330,9 +449,10 @@ class GuiOutlineView(QTreeWidget): self._saveHeaderState() return - def _menuColumnToggled(self, isChecked, theItem): + @pyqtSlot(bool, Enum) + def menuColumnToggled(self, isChecked, theItem): """Receive the changes to column visibility forwarded by the - header context menu. + column selection menu. """ logger.verbose("User toggled Outline column '%s'", theItem.name) if theItem in self._colIdx: @@ -389,7 +509,7 @@ class GuiOutlineView(QTreeWidget): except Exception: logger.warning("Ignored unknown outline column '%s'", str(hName)) - self.headerMenu.setHiddenState(self._colHidden) + self.hiddenStateChanged.emit() return @@ -558,10 +678,11 @@ class GuiOutlineView(QTreeWidget): class GuiOutlineHeaderMenu(QMenu): - def __init__(self, theParent): - QMenu.__init__(self, theParent) + columnToggled = pyqtSignal(bool, Enum) + + def __init__(self, theOutline): + QMenu.__init__(self, theOutline) - self.theParent = theParent self.acceptToggle = True mnuHead = QAction(self.tr("Select Columns"), self) @@ -575,7 +696,7 @@ class GuiOutlineHeaderMenu(QMenu): self.actionMap[hItem] = QAction(trConst(nwLabels.OUTLINE_COLS[hItem]), self) self.actionMap[hItem].setCheckable(True) self.actionMap[hItem].toggled.connect( - lambda isChecked, tItem=hItem: self._columnToggled(isChecked, tItem) + lambda isChecked, tItem=hItem: self.columnToggled.emit(isChecked, tItem) ) self.addAction(self.actionMap[hItem]) @@ -596,18 +717,6 @@ class GuiOutlineHeaderMenu(QMenu): return - ## - # Slots - ## - - def _columnToggled(self, isChecked, theItem): - """The user has toggled the visibility of a column. Forward the - event to the parent class only if we're accepting changes. - """ - if self.acceptToggle: - self.theParent._menuColumnToggled(isChecked, theItem) - return - # END Class GuiOutlineHeaderMenu @@ -945,16 +1054,12 @@ class GuiOutlineDetails(QScrollArea): # Internal Functions ## - def _formatTags(self, theRefs, theKey): + def _formatTags(self, refs, key): """Format the tags as clickable links. """ - if theKey not in theRefs: - return "" - refTags = [] - for tTag in theRefs[theKey]: - refTags.append("%s" % ( - theKey[1:], tTag, tTag - )) - return ", ".join(refTags) + mKey = key[1:] + return ", ".join( + [f"{tag}" for tag in refs.get(key, [])] + ) # END Class GuiOutlineDetails diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 8f7814e0..80fd06e2 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -126,7 +126,7 @@ class GuiMain(QMainWindow): self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) - self.treeView.rootFoldersChanged.connect(self.projView.updateClasses) + self.treeView.rootFoldersChanged.connect(self.projView.projectUpdated) self.viewsBar.viewChangeRequested.connect(self._changeView) self.projView.viewChangeRequested.connect(self._changeView) @@ -353,7 +353,7 @@ class GuiMain(QMainWindow): self.rebuildTrees() self.saveProject() self.docEditor.setDictionaries() - self.projView.updateClasses() + self.projView.projectUpdated() self.rebuildIndex(beQuiet=True) self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setProjectStatus(nwState.GOOD) @@ -499,9 +499,8 @@ class GuiMain(QMainWindow): self.rebuildTrees() self.docEditor.setDictionaries() self.docEditor.toggleSpellCheck(self.theProject.spellCheck) - self.mainMenu.setAutoOutline(self.theProject.autoOutline) self.statusBar.setRefTime(self.theProject.projOpened) - self.projView.updateClasses() + self.projView.projectUpdated() self._updateStatusWordCount() # Restore previously open documents, if any @@ -895,19 +894,6 @@ class GuiMain(QMainWindow): return True - def rebuildOutline(self): - """Force a rebuild of the Outline view. - """ - if not self.hasProject: - logger.error("No project open") - return False - - logger.verbose("Forcing a rebuild of the Project Outline") - self._changeView(nwView.OUTLINE) - self.projView.refreshView(overRide=True) - - return True - ## # Main Dialogs ## diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index b420e4f3..c6647dbd 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -61,7 +61,6 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI): assert nwGUI.editItem() is False assert nwGUI.requestNovelTreeRefresh() is False assert nwGUI.rebuildIndex() is False - assert nwGUI.rebuildOutline() is False assert nwGUI.showProjectSettingsDialog() is False assert nwGUI.showProjectDetailsDialog() is False assert nwGUI.showBuildProjectDialog() is False diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 02dd1041..c1c258e6 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -21,10 +21,7 @@ along with this program. If not, see . import pytest -from PyQt5.QtCore import Qt, QPoint -from PyQt5.QtWidgets import QAction, QTreeWidgetItem, QMessageBox - -from novelwriter.enum import nwOutline +from PyQt5.QtWidgets import QTreeWidgetItem, QMessageBox keyDelay = 2 typeDelay = 1 @@ -51,16 +48,16 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum): assert outlineView.topLevelItemCount() > 0 # Context Menu - outlineView._headerRightClick(QPoint(1, 1)) - outlineView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger) - outlineView.headerMenu.close() - qtbot.mouseClick(outlineView, Qt.LeftButton) + # outlineView._headerRightClick(QPoint(1, 1)) + # outlineView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger) + # outlineView.headerMenu.close() + # qtbot.mouseClick(outlineView, Qt.LeftButton) - outlineView._loadHeaderState() - assert not outlineView._colHidden[nwOutline.CCOUNT] + # outlineView._loadHeaderState() + # assert not outlineView._colHidden[nwOutline.CCOUNT] # First Item - nwGUI.rebuildOutline() + outlineView.refreshTree() selItem = outlineView.topLevelItem(0) assert isinstance(selItem, QTreeWidgetItem)