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)