Apply open document shortcuts to the tree that has focus (#945)

* Better handling of Enter/Return on GUI trees, resolves #913
* Update test
* Generalise open selected handle functions
* Improve test coverage
* Update docstring
This commit is contained in:
Veronica Berglyd Olsen
2021-12-31 19:51:26 +01:00
committed by GitHub
parent 7024254960
commit b0f939af32
7 changed files with 188 additions and 74 deletions
+8 -10
View File
@@ -135,7 +135,7 @@ class GuiNovelTree(QTreeWidget):
logger.verbose("Requesting refresh of the novel tree")
treeChanged = self.theParent.treeView.changedSince(self._lastBuild)
indexChanged = self.theIndex.novelChangedSince(self._lastBuild)
if not (treeChanged or indexChanged):
if not (treeChanged or indexChanged or overRide):
logger.verbose("No changes have been made to the novel index")
return
@@ -175,10 +175,13 @@ class GuiNovelTree(QTreeWidget):
selected, return the first.
"""
selItem = self.selectedItems()
tHandle = None
tLine = 0
if selItem:
return selItem[0].data(self.C_TITLE, Qt.UserRole)[0]
tHandle = selItem[0].data(self.C_TITLE, Qt.UserRole)[0]
tLine = checkInt(selItem[0].data(self.C_TITLE, Qt.UserRole)[1], 1) - 1
return None
return tHandle, tLine
##
# Events
@@ -201,7 +204,7 @@ class GuiNovelTree(QTreeWidget):
if not isinstance(selItem, QTreeWidgetItem):
return
tHandle = self.getSelectedHandle()
tHandle, _ = self.getSelectedHandle()
if tHandle is None:
return
@@ -218,13 +221,8 @@ class GuiNovelTree(QTreeWidget):
clicked, and send it to the main gui class for opening in the
document editor.
"""
theData = tItem.data(self.C_TITLE, Qt.UserRole)
tHandle = theData[0]
tLine = checkInt(theData[1], 1)
logger.verbose("User selected entry with handle '%s' on line %s", tHandle, tLine)
tHandle, tLine = self.getSelectedHandle()
self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True)
return
def _itemSelected(self):
+15 -8
View File
@@ -34,6 +34,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.enum import nwItemLayout, nwItemType, nwOutline
from novelwriter.common import checkInt
from novelwriter.constants import trConst, nwKeyWords, nwLabels
logger = logging.getLogger(__name__)
@@ -196,6 +197,19 @@ class GuiOutline(QTreeWidget):
self._firstView = True
return
def getSelectedHandle(self):
"""Get the currently selected handle. If multiple items are
selected, return the first.
"""
selItem = self.selectedItems()
tHandle = None
tLine = 0
if selItem:
tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
tLine = checkInt(selItem[0].text(self._colIdx[nwOutline.LINE]), 1) - 1
return tHandle, tLine
##
# Slots
##
@@ -206,15 +220,8 @@ class GuiOutline(QTreeWidget):
clicked, and send it to the main gui class for opening in the
document editor.
"""
tHandle = tItem.data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
try:
tLine = int(tItem.text(self._colIdx[nwOutline.LINE]))
except Exception:
tLine = 1
logger.verbose("User selected entry with handle '%s' on line %s", tHandle, tLine)
tHandle, tLine = self.getSelectedHandle()
self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True)
return
@pyqtSlot()
+48 -46
View File
@@ -262,9 +262,13 @@ class GuiMain(QMainWindow):
# Shortcuts and Actions
self._connectMenuActions()
keyReturn = QShortcut(self.treeView)
keyReturn = QShortcut(self)
keyReturn.setKey(QKeySequence(Qt.Key_Return))
keyReturn.activated.connect(self._treeKeyPressReturn)
keyReturn.activated.connect(self._keyPressReturn)
keyEnter = QShortcut(self)
keyEnter.setKey(QKeySequence(Qt.Key_Enter))
keyEnter.activated.connect(self._keyPressReturn)
keyEscape = QShortcut(self)
keyEscape.setKey(QKeySequence(Qt.Key_Escape))
@@ -350,7 +354,7 @@ class GuiMain(QMainWindow):
##
def newProject(self, projData=None):
"""Create new project via the new project wizard.
"""Create a new project via the new project wizard.
"""
if self.hasProject:
if not self.closeProject():
@@ -396,9 +400,9 @@ class GuiMain(QMainWindow):
return True
def closeProject(self, isYes=False):
"""Closes the project if one is open. isYes is passed on from
the close application event so the user doesn't get prompted
twice to confirm.
"""Close the project if one is open. isYes is passed on from the
close application event so the user doesn't get prompted twice
to confirm.
"""
if not self.hasProject:
# There is no project loaded, everything OK
@@ -598,6 +602,10 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return False
if not self.theProject.projTree.checkType(tHandle, nwItemType.FILE):
logger.debug("Requested item '%s' is not a document", tHandle)
return False
self.closeDocument()
self.mainTabs.setCurrentWidget(self.splitDocs)
if self.docEditor.loadText(tHandle, tLine):
@@ -798,22 +806,28 @@ class GuiMain(QMainWindow):
##
def openSelectedItem(self):
"""Open the selected documents.
"""Open the selected item from the tree that is currently
active. It is not checked that the item is actually a document.
That should be handled by the openDocument function.
"""
if not self.hasProject:
logger.error("No project open")
return False
tHandle = self.treeView.getSelectedHandle()
if tHandle is None:
tHandle = None
tLine = None
if self.treeView.hasFocus():
tHandle = self.treeView.getSelectedHandle()
elif self.novelView.hasFocus():
tHandle, tLine = self.novelView.getSelectedHandle()
elif self.projView.hasFocus():
tHandle, tLine = self.projView.getSelectedHandle()
else:
logger.warning("No item selected")
return False
logger.verbose("Opening item '%s'", tHandle)
if self.theProject.projTree.checkType(tHandle, nwItemType.FILE):
self.openDocument(tHandle, doScroll=False)
else:
logger.verbose("Requested item '%s' is not a file", tHandle)
if tHandle is not None:
self.openDocument(tHandle, tLine=tLine, changeFocus=False, doScroll=False)
return True
@@ -986,7 +1000,7 @@ class GuiMain(QMainWindow):
"""
if not self.hasProject:
logger.error("No project open")
return
return False
dlgProj = GuiProjectSettings(self)
dlgProj.exec_()
@@ -996,14 +1010,14 @@ class GuiMain(QMainWindow):
self.docEditor.setDictionaries()
self._updateWindowTitle(self.theProject.projName)
return
return True
def showProjectDetailsDialog(self):
"""Open the project details dialog.
"""
if not self.hasProject:
logger.error("No project open")
return
return False
self.treeView.flushTreeOrder()
@@ -1016,14 +1030,14 @@ class GuiMain(QMainWindow):
dlgDetails.raise_()
dlgDetails.updateValues()
return
return True
def showBuildProjectDialog(self):
"""Open the build project dialog.
"""
if not self.hasProject:
logger.error("No project open")
return
return False
dlgBuild = getGuiItem("GuiBuildNovel")
if dlgBuild is None:
@@ -1035,14 +1049,14 @@ class GuiMain(QMainWindow):
qApp.processEvents()
dlgBuild.viewCachedDoc()
return
return True
def showProjectWordListDialog(self):
"""Open the project word list dialog.
"""
if not self.hasProject:
logger.error("No project open")
return
return False
dlgWords = GuiWordList(self)
dlgWords.exec_()
@@ -1051,14 +1065,14 @@ class GuiMain(QMainWindow):
logger.debug("Reloading word list")
self.docEditor.setDictionaries()
return
return True
def showWritingStatsDialog(self):
"""Open the session stats dialog.
"""
if not self.hasProject:
logger.error("No project open")
return
return False
dlgStats = getGuiItem("GuiWritingStats")
if dlgStats is None:
@@ -1070,7 +1084,7 @@ class GuiMain(QMainWindow):
qApp.processEvents()
dlgStats.populateGUI()
return
return True
def showAboutNWDialog(self, showNotes=False):
"""Show the about dialog for novelWriter.
@@ -1555,9 +1569,9 @@ class GuiMain(QMainWindow):
"""Single click on a project tree item just updates the details
panel below the tree.
"""
sHandle = self.treeView.getSelectedHandle()
if sHandle is not None:
self.treeMeta.updateViewBox(sHandle)
tHandle = self.treeView.getSelectedHandle()
if tHandle is not None:
self.treeMeta.updateViewBox(tHandle)
return
@pyqtSlot("QTreeWidgetItem*", int)
@@ -1565,14 +1579,9 @@ class GuiMain(QMainWindow):
"""The user double-clicked an item in the tree. If it is a file,
we open it. Otherwise, we do nothing.
"""
tHandle = tItem.data(self.treeView.C_NAME, Qt.UserRole)
logger.verbose("User double clicked tree item with handle '%s'", tHandle)
if self.theProject.projTree.checkType(tHandle, nwItemType.FILE):
tHandle = self.treeView.getSelectedHandle()
if tHandle is not None:
self.openDocument(tHandle, changeFocus=False, doScroll=False)
else:
logger.verbose("Requested item '%s' is a folder", tHandle)
return
@pyqtSlot()
@@ -1589,18 +1598,11 @@ class GuiMain(QMainWindow):
return
@pyqtSlot()
def _treeKeyPressReturn(self):
"""The user pressed return on an item in the tree. If it is a
file, we open it. Otherwise, we do nothing. Pressing return does
not change focus to the editor as double click does.
def _keyPressReturn(self):
"""Forward the return/enter keypress to the function that opens
the currently selected item.
"""
tHandle = self.treeView.getSelectedHandle()
logger.verbose("User pressed return on tree item with handle '%s'", tHandle)
if self.theProject.projTree.checkType(tHandle, nwItemType.FILE):
self.openDocument(tHandle, changeFocus=False, doScroll=False)
else:
logger.verbose("Requested item '%s' is a folder", tHandle)
self.openSelectedItem()
return
@pyqtSlot()
@@ -1642,7 +1644,7 @@ class GuiMain(QMainWindow):
logger.verbose("Novel tree tab activated")
if self.hasProject:
self.novelView.refreshTree()
sHandle = self.novelView.getSelectedHandle()
sHandle, _ = self.novelView.getSelectedHandle()
self.treeMeta.updateViewBox(sHandle)
+91 -3
View File
@@ -28,16 +28,104 @@ from tools import cmpFiles
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox, QDialog
from novelwriter.dialogs.itemeditor import GuiItemEditor
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.gui.projtree import GuiProjectTree
from novelwriter.gui import (
GuiDocEditor, GuiProjectTree, GuiNovelTree, GuiOutline
)
from novelwriter.enum import nwItemType, nwWidget
from novelwriter.dialogs.itemeditor import GuiItemEditor
keyDelay = 2
typeDelay = 1
stepDelay = 20
@pytest.mark.gui
def testGuiMain_ProjectBlocker(monkeypatch, nwGUI):
"""Test the blocking of features when there's no project open.
"""
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
# Test no-project blocking
assert nwGUI.closeProject() is True
assert nwGUI.saveProject() is False
assert nwGUI.closeDocument() is False
assert nwGUI.openDocument(None) is False
assert nwGUI.openNextDocument(None) is False
assert nwGUI.saveDocument() is False
assert nwGUI.viewDocument(None) is False
assert nwGUI.importDocument() is False
assert nwGUI.mergeDocuments() is False
assert nwGUI.splitDocument() is False
assert nwGUI.openSelectedItem() is False
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
assert nwGUI.showProjectWordListDialog() is False
assert nwGUI.showWritingStatsDialog() is False
# END Test testGuiMain_NoProject
@pytest.mark.gui
def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj):
"""Test handling of project tree items based on GUI focus states.
"""
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj}) is True
assert nwGUI.saveProject() is True
sHandle = "0e17daca5f3e1"
assert nwGUI.openSelectedItem() is False
# Project Tree has focus
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projTabs.setCurrentIndex(0)
with monkeypatch.context() as mp:
mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
assert nwGUI.docEditor.docHandle() is None
nwGUI.treeView._getTreeItem(sHandle).setSelected(True)
nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle
assert nwGUI.closeDocument() is True
# Novel Tree has focus
nwGUI.projTabs.setCurrentIndex(1)
nwGUI.novelView.refreshTree(True)
with monkeypatch.context() as mp:
mp.setattr(GuiNovelTree, "hasFocus", lambda *a: True)
assert nwGUI.docEditor.docHandle() is None
actItem = nwGUI.novelView.topLevelItem(0)
chpItem = actItem.child(0)
selItem = chpItem.child(0)
nwGUI.novelView.setCurrentItem(selItem)
nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle
assert nwGUI.closeDocument() is True
# Project Outline has focus
nwGUI.switchFocus(nwWidget.OUTLINE)
with monkeypatch.context() as mp:
mp.setattr(GuiOutline, "hasFocus", lambda *a: True)
assert nwGUI.docEditor.docHandle() is None
actItem = nwGUI.projView.topLevelItem(0)
chpItem = actItem.child(0)
selItem = chpItem.child(0)
nwGUI.projView.setCurrentItem(selItem)
nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle
assert nwGUI.closeDocument() is True
# qtbot.stopForInteraction()
# END Test testGuiMain_ProjectTreeItems
@pytest.mark.gui
def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
"""Test the document editor.
+2 -6
View File
@@ -29,7 +29,7 @@ from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox
from tools import writeFile
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget
from novelwriter.enum import nwDocAction, nwDocInsert
from novelwriter.constants import nwKeyWords, nwUnicode
keyDelay = 2
@@ -471,11 +471,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert nwGUI.newProject({"projPath": fncProj})
assert nwGUI.treeView._getTreeItem("0e17daca5f3e1") is not None
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem("0e17daca5f3e1").setSelected(True)
assert nwGUI.openSelectedItem()
assert nwGUI.openDocument("0e17daca5f3e1") is True
nwGUI.docEditor.clear()
# Test Faulty Inserts
+1 -1
View File
@@ -74,7 +74,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, nwMinimal):
assert not topItem.isSelected()
topItem.setSelected(True)
assert nwTree.selectedItems()[0] == topItem
assert nwTree.getSelectedHandle() == "a35baf2e93843"
assert nwTree.getSelectedHandle() == ("a35baf2e93843", 0)
nwTree.refreshTree()
assert nwTree.topLevelItem(0).isSelected()
+23
View File
@@ -77,6 +77,10 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
selItem = chpItem.child(0)
nwGUI.projView.setCurrentItem(selItem)
tHandle, tLine = nwGUI.projView.getSelectedHandle()
assert tHandle == "88243afbe5ed8"
assert tLine == 0
assert nwGUI.projMeta.titleLabel.text() == "<b>Scene</b>"
assert nwGUI.projMeta.titleValue.text() == "Scene One"
assert nwGUI.projMeta.fileValue.text() == "Scene One"
@@ -87,6 +91,25 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
nwGUI.projMeta._tagClicked("#pov=Bod")
assert nwGUI.docViewer.docHandle() == "4c4f28287af27"
# Scene One, Section Two
actItem = nwGUI.projView.topLevelItem(1)
chpItem = actItem.child(0)
scnItem = chpItem.child(0)
selItem = scnItem.child(0)
nwGUI.projView.setCurrentItem(selItem)
tHandle, tLine = nwGUI.projView.getSelectedHandle()
assert tHandle == "88243afbe5ed8"
assert tLine == 12
assert nwGUI.projMeta.titleLabel.text() == "<b>Section</b>"
assert nwGUI.projMeta.titleValue.text() == "Scene One, Section Two"
assert nwGUI.projMeta.fileValue.text() == "Scene One"
assert nwGUI.projMeta.itemValue.text() == "Finished"
nwGUI.projView._treeDoubleClick(selItem, 0)
assert nwGUI.docEditor.docHandle() == "88243afbe5ed8"
# qtbot.stopForInteraction()
# END Test testGuiOutline_Main