Add a toolbar to the Outline view

This commit is contained in:
Veronica Berglyd Olsen
2022-05-22 23:03:30 +02:00
parent 007f2fbff4
commit ecc0585c87
7 changed files with 170 additions and 101 deletions
-2
View File
@@ -59,7 +59,6 @@ The main shorcuts are as follows:
":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo." ":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo."
":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes." ":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes."
":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking." ":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:`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:`Down`", "Move item one step down in the project tree."
":kbd:`Ctrl`:kbd:`Del`", "Delete next word in editor." ":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:`F7`", "Re-run spell checker."
":kbd:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer." ":kbd:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer."
":kbd:`F9`", "Re-build the project index." ":kbd:`F9`", "Re-build the project index."
":kbd:`F10`", "Re-build the project outline."
":kbd:`F11`", "Activate full screen mode." ":kbd:`F11`", "Activate full screen mode."
":kbd:`Shift`:kbd:`F1`", "Open the local user manual (PDF) if it is available." ":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." ":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document."
+9
View File
@@ -273,6 +273,15 @@ class NWTree():
rootClasses.add(nwItem.itemClass) rootClasses.add(nwItem.itemClass)
return rootClasses 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): def isRoot(self, tHandle):
"""Check if a handle is a root item. """Check if a handle is a root item.
""" """
-25
View File
@@ -81,12 +81,6 @@ class GuiMainMenu(QMenuBar):
self.aSpellCheck.setChecked(theMode) self.aSpellCheck.setChecked(theMode)
return return
def setAutoOutline(self, theMode):
"""Forward auto outline check state to its action.
"""
self.aAutoOutline.setChecked(theMode)
return
def setFocusMode(self, theMode): def setFocusMode(self, theMode):
"""Forward focus mode check state to its action. """Forward focus mode check state to its action.
""" """
@@ -105,12 +99,6 @@ class GuiMainMenu(QMenuBar):
self.theParent.docEditor.toggleSpellCheck(None) self.theParent.docEditor.toggleSpellCheck(None)
return True 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): def _openWebsite(self, theUrl):
"""Open a URL in the system's default browser. """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.aRebuildIndex.triggered.connect(lambda: self.theParent.rebuildIndex())
self.toolsMenu.addAction(self.aRebuildIndex) 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 # Tools > Separator
self.toolsMenu.addSeparator() self.toolsMenu.addSeparator()
+150 -45
View File
@@ -4,9 +4,11 @@ novelWriter GUI Project Outline
GUI class for the project outline view GUI class for the project outline view
File History: 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-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 This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen Copyright 20182022, Veronica Berglyd Olsen
@@ -29,6 +31,7 @@ import logging
import novelwriter import novelwriter
from time import time from time import time
from enum import Enum
from PyQt5.QtCore import ( from PyQt5.QtCore import (
Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP
@@ -36,7 +39,7 @@ from PyQt5.QtCore import (
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel,
QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
QWidget, QFrame QWidget, QFrame, QToolBar, QSizePolicy, QComboBox, QToolButton
) )
from novelwriter.enum import ( from novelwriter.enum import (
@@ -60,6 +63,7 @@ class GuiOutline(QWidget):
self.theParent = theParent self.theParent = theParent
self.theProject = theParent.theProject self.theProject = theParent.theProject
self.outlineBar = GuiOutlineToolBar(self)
self.outlineView = GuiOutlineView(self) self.outlineView = GuiOutlineView(self)
self.outlineData = GuiOutlineDetails(self) self.outlineData = GuiOutlineDetails(self)
@@ -71,13 +75,20 @@ class GuiOutline(QWidget):
# Assemble # Assemble
self.outerBox = QVBoxLayout() self.outerBox = QVBoxLayout()
self.outerBox.setContentsMargins(0, 0, 0, 0) self.outerBox.setContentsMargins(0, 0, 0, 0)
self.outerBox.addWidget(self.outlineBar)
self.outerBox.addWidget(self.splitOutline) self.outerBox.addWidget(self.splitOutline)
self.setLayout(self.outerBox) 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 # Function Mappings
self.getSelectedHandle = self.outlineView.getSelectedHandle self.getSelectedHandle = self.outlineView.getSelectedHandle
self.updateClasses = self.outlineData.updateClasses
return return
@@ -112,9 +123,112 @@ class GuiOutline(QWidget):
def setTreeFocus(self): def setTreeFocus(self):
return self.outlineView.setFocus() 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 # 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): class GuiOutlineView(QTreeWidget):
DEF_WIDTH = { DEF_WIDTH = {
@@ -157,10 +271,12 @@ class GuiOutlineView(QTreeWidget):
nwOutline.SYNOP: False, nwOutline.SYNOP: False,
} }
hiddenStateChanged = pyqtSignal()
def __init__(self, theOutline): def __init__(self, theOutline):
QTreeWidget.__init__(self, theOutline) QTreeWidget.__init__(self, theOutline)
logger.debug("Initialising GuiOutline ...") logger.debug("Initialising GuiOutlineView ...")
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theOutline = theOutline self.theOutline = theOutline
@@ -169,7 +285,6 @@ class GuiOutlineView(QTreeWidget):
self.theTheme = theOutline.theParent.theTheme self.theTheme = theOutline.theParent.theTheme
self.theIndex = theOutline.theParent.theIndex self.theIndex = theOutline.theParent.theIndex
self.optState = theOutline.theParent.theProject.optState self.optState = theOutline.theParent.theProject.optState
self.headerMenu = GuiOutlineHeaderMenu(self)
self.setFrameStyle(QFrame.NoFrame) self.setFrameStyle(QFrame.NoFrame)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
@@ -184,8 +299,6 @@ class GuiOutlineView(QTreeWidget):
self.setIndentation(iPx) self.setIndentation(iPx)
self.treeHead = self.header() self.treeHead = self.header()
self.treeHead.setContextMenuPolicy(Qt.CustomContextMenu)
self.treeHead.customContextMenuRequested.connect(self._headerRightClick)
self.treeHead.sectionMoved.connect(self._columnMoved) self.treeHead.sectionMoved.connect(self._columnMoved)
# Internals # Internals
@@ -199,12 +312,25 @@ class GuiOutlineView(QTreeWidget):
self.initOutline() self.initOutline()
self.clearOutline() self.clearOutline()
self.headerMenu.setHiddenState(self._colHidden)
logger.debug("GuiOutline initialisation complete") self.hiddenStateChanged.emit()
logger.debug("GuiOutlineView initialisation complete")
return return
##
# Properties
##
@property
def hiddenColumns(self):
return self._colHidden
##
# Methods
##
def initOutline(self): def initOutline(self):
"""Set or update outline settings. """Set or update outline settings.
""" """
@@ -314,13 +440,6 @@ class GuiOutlineView(QTreeWidget):
return return
@pyqtSlot("QPoint")
def _headerRightClick(self, clickPos):
"""Show the header column menu.
"""
self.headerMenu.exec_(self.mapToGlobal(clickPos))
return
@pyqtSlot(int, int, int) @pyqtSlot(int, int, int)
def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx): def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx):
"""Make sure the order array is up to date with the actual order """Make sure the order array is up to date with the actual order
@@ -330,9 +449,10 @@ class GuiOutlineView(QTreeWidget):
self._saveHeaderState() self._saveHeaderState()
return return
def _menuColumnToggled(self, isChecked, theItem): @pyqtSlot(bool, Enum)
def menuColumnToggled(self, isChecked, theItem):
"""Receive the changes to column visibility forwarded by the """Receive the changes to column visibility forwarded by the
header context menu. column selection menu.
""" """
logger.verbose("User toggled Outline column '%s'", theItem.name) logger.verbose("User toggled Outline column '%s'", theItem.name)
if theItem in self._colIdx: if theItem in self._colIdx:
@@ -389,7 +509,7 @@ class GuiOutlineView(QTreeWidget):
except Exception: except Exception:
logger.warning("Ignored unknown outline column '%s'", str(hName)) logger.warning("Ignored unknown outline column '%s'", str(hName))
self.headerMenu.setHiddenState(self._colHidden) self.hiddenStateChanged.emit()
return return
@@ -558,10 +678,11 @@ class GuiOutlineView(QTreeWidget):
class GuiOutlineHeaderMenu(QMenu): class GuiOutlineHeaderMenu(QMenu):
def __init__(self, theParent): columnToggled = pyqtSignal(bool, Enum)
QMenu.__init__(self, theParent)
def __init__(self, theOutline):
QMenu.__init__(self, theOutline)
self.theParent = theParent
self.acceptToggle = True self.acceptToggle = True
mnuHead = QAction(self.tr("Select Columns"), self) 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] = QAction(trConst(nwLabels.OUTLINE_COLS[hItem]), self)
self.actionMap[hItem].setCheckable(True) self.actionMap[hItem].setCheckable(True)
self.actionMap[hItem].toggled.connect( 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]) self.addAction(self.actionMap[hItem])
@@ -596,18 +717,6 @@ class GuiOutlineHeaderMenu(QMenu):
return 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 # END Class GuiOutlineHeaderMenu
@@ -945,16 +1054,12 @@ class GuiOutlineDetails(QScrollArea):
# Internal Functions # Internal Functions
## ##
def _formatTags(self, theRefs, theKey): def _formatTags(self, refs, key):
"""Format the tags as clickable links. """Format the tags as clickable links.
""" """
if theKey not in theRefs: mKey = key[1:]
return "" return ", ".join(
refTags = [] [f"<a href='#{mKey}={tag}'>{tag}</a>" for tag in refs.get(key, [])]
for tTag in theRefs[theKey]: )
refTags.append("<a href='#%s=%s'>%s</a>" % (
theKey[1:], tTag, tTag
))
return ", ".join(refTags)
# END Class GuiOutlineDetails # END Class GuiOutlineDetails
+3 -17
View File
@@ -126,7 +126,7 @@ class GuiMain(QMainWindow):
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) self.treeView.novelItemChanged.connect(self._treeNovelItemChanged)
self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) 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.viewsBar.viewChangeRequested.connect(self._changeView)
self.projView.viewChangeRequested.connect(self._changeView) self.projView.viewChangeRequested.connect(self._changeView)
@@ -353,7 +353,7 @@ class GuiMain(QMainWindow):
self.rebuildTrees() self.rebuildTrees()
self.saveProject() self.saveProject()
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.projView.updateClasses() self.projView.projectUpdated()
self.rebuildIndex(beQuiet=True) self.rebuildIndex(beQuiet=True)
self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.setProjectStatus(nwState.GOOD) self.statusBar.setProjectStatus(nwState.GOOD)
@@ -499,9 +499,8 @@ class GuiMain(QMainWindow):
self.rebuildTrees() self.rebuildTrees()
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.docEditor.toggleSpellCheck(self.theProject.spellCheck) self.docEditor.toggleSpellCheck(self.theProject.spellCheck)
self.mainMenu.setAutoOutline(self.theProject.autoOutline)
self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setRefTime(self.theProject.projOpened)
self.projView.updateClasses() self.projView.projectUpdated()
self._updateStatusWordCount() self._updateStatusWordCount()
# Restore previously open documents, if any # Restore previously open documents, if any
@@ -895,19 +894,6 @@ class GuiMain(QMainWindow):
return True 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 # Main Dialogs
## ##
-1
View File
@@ -61,7 +61,6 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI):
assert nwGUI.editItem() is False assert nwGUI.editItem() is False
assert nwGUI.requestNovelTreeRefresh() is False assert nwGUI.requestNovelTreeRefresh() is False
assert nwGUI.rebuildIndex() is False assert nwGUI.rebuildIndex() is False
assert nwGUI.rebuildOutline() is False
assert nwGUI.showProjectSettingsDialog() is False assert nwGUI.showProjectSettingsDialog() is False
assert nwGUI.showProjectDetailsDialog() is False assert nwGUI.showProjectDetailsDialog() is False
assert nwGUI.showBuildProjectDialog() is False assert nwGUI.showBuildProjectDialog() is False
+8 -11
View File
@@ -21,10 +21,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import pytest import pytest
from PyQt5.QtCore import Qt, QPoint from PyQt5.QtWidgets import QTreeWidgetItem, QMessageBox
from PyQt5.QtWidgets import QAction, QTreeWidgetItem, QMessageBox
from novelwriter.enum import nwOutline
keyDelay = 2 keyDelay = 2
typeDelay = 1 typeDelay = 1
@@ -51,16 +48,16 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
assert outlineView.topLevelItemCount() > 0 assert outlineView.topLevelItemCount() > 0
# Context Menu # Context Menu
outlineView._headerRightClick(QPoint(1, 1)) # outlineView._headerRightClick(QPoint(1, 1))
outlineView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger) # outlineView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger)
outlineView.headerMenu.close() # outlineView.headerMenu.close()
qtbot.mouseClick(outlineView, Qt.LeftButton) # qtbot.mouseClick(outlineView, Qt.LeftButton)
outlineView._loadHeaderState() # outlineView._loadHeaderState()
assert not outlineView._colHidden[nwOutline.CCOUNT] # assert not outlineView._colHidden[nwOutline.CCOUNT]
# First Item # First Item
nwGUI.rebuildOutline() outlineView.refreshTree()
selItem = outlineView.topLevelItem(0) selItem = outlineView.topLevelItem(0)
assert isinstance(selItem, QTreeWidgetItem) assert isinstance(selItem, QTreeWidgetItem)