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:`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."
+9
View File
@@ -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.
"""
-25
View File
@@ -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()
+150 -45
View File
@@ -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 20182022, 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("<a href='#%s=%s'>%s</a>" % (
theKey[1:], tTag, tTag
))
return ", ".join(refTags)
mKey = key[1:]
return ", ".join(
[f"<a href='#{mKey}={tag}'>{tag}</a>" for tag in refs.get(key, [])]
)
# END Class GuiOutlineDetails
+3 -17
View File
@@ -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
##
-1
View File
@@ -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
+8 -11
View File
@@ -21,10 +21,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
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)