diff --git a/.gitignore b/.gitignore index 66c21d61..3d1b337e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ __pycache__ /sample/meta *.bak *.lock +ToC.txt # PyTest /prof/ diff --git a/docs/source/int_interface.rst b/docs/source/int_interface.rst index cf6bc065..f5b8d68d 100644 --- a/docs/source/int_interface.rst +++ b/docs/source/int_interface.rst @@ -378,6 +378,8 @@ Most features are available as keyboard shortcuts. These are as follows: ":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking." ":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline." ":kbd:`Ctrl`:kbd:`Del`", "If in the project tree, move a document to trash, or delete a folder." + ":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:`'`", "Wrap selected text, or word under cursor, in single quotes." ":kbd:`Ctrl`:kbd:`""`", "Wrap selected text, or word under cursor, in double quotes." ":kbd:`Ctrl`:kbd:`Enter`", "Open the tag or reference under the cursor in the Viewer." @@ -392,9 +394,7 @@ Most features are available as keyboard shortcuts. These are as follows: ":kbd:`Ctrl`:kbd:`Shift`:kbd:`R`", "Close the document viewer." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`S`", "Save the current project." ":kbd:`Ctrl`:kbd:`Shift`:kbd:`W`", "Close the current project." - ":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Alternative sequence for redo last undo." - ":kbd:`Ctrl`:kbd:`Shift`:kbd:`Up`", "Move item one step up in the project tree." - ":kbd:`Ctrl`:kbd:`Shift`:kbd:`Down`", "Move item one step down in the project tree." + ":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Undo move of project tree item." ":kbd:`F1`", "Open the documentation. This will either open the Qt Assistant, if available, or send you to the documentation website." ":kbd:`F2`", "If in the project tree, edit a document or folder settings. (Same as :kbd:`Ctrl`:kbd:`E`)" ":kbd:`F3`", "Find next occurrence of search word in current document. (Same as :kbd:`Ctrl`:kbd:`G`)" diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 5b961e10..e748c0ff 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -49,13 +49,15 @@ class nwConst(): class nwLists(): """Lists used for grouping various other constants. """ - # Regular user-accessible item types REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE} # Item classes where the full list of novel layouts are allowed CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE} + # Item classes which do not require items to have same class + FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH} + # END Class nwLists class nwRegEx(): diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index bc83719b..e4a25e4e 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -646,6 +646,10 @@ class GuiDocEditor(QTextEdit): this class when calling these actions from other classes. """ logger.verbose("Requesting action: %s" % theAction.name) + if not self.hasFocus(): + logger.verbose("Editor does not have focus") + return False + if self.theHandle is None: logger.error("No document open") return False diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 1bdd1c32..38efa1d0 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -115,22 +115,25 @@ class GuiMainMenu(QMenuBar): ## def setSpellCheck(self, theMode): - """Set the spell check check box to theMode. This is controlled - by the document editor class, which holds the master spell check - flag. + """Forward spell check check state to its action. """ self.aSpellCheck.setChecked(theMode) return def setAutoOutline(self, theMode): - """Set the auto outline check box to theMode. Used during - initialisation. + """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. + """ + self.aFocusMode.setChecked(theMode) + return + ## - # Menu Action + # Slots ## def _toggleSpellCheck(self, isChecked=False): @@ -164,17 +167,11 @@ class GuiMainMenu(QMenuBar): return True def _openWebsite(self, theUrl): - """Open an URL in the system's default browser. + """Open a URL in the system's default browser. """ QDesktopServices.openUrl(QUrl(theUrl)) return True - def _openIssue(self): - """Open the issue tracker URL in the system's default browser. - """ - QDesktopServices.openUrl(QUrl(nw.__issuesurl__)) - return True - ## # Menu Builders ## @@ -263,19 +260,40 @@ class GuiMainMenu(QMenuBar): self.projMenu.addSeparator() # Project > Edit - self.aEditItem = QAction("Edit Project Item", self) - self.aEditItem.setStatusTip("Change item settings") + self.aEditItem = QAction("Edit Item", self) + self.aEditItem.setStatusTip("Change project item settings") self.aEditItem.setShortcuts(["Ctrl+E", "F2"]) self.aEditItem.triggered.connect(lambda: self.theParent.editItem(None)) self.projMenu.addAction(self.aEditItem) # Project > Delete - self.aDeleteItem = QAction("Delete Project Item", self) - self.aDeleteItem.setStatusTip("Delete selected item") + self.aDeleteItem = QAction("Delete Item", self) + self.aDeleteItem.setStatusTip("Delete selected project item") self.aDeleteItem.setShortcut("Ctrl+Del") self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None)) self.projMenu.addAction(self.aDeleteItem) + # Project > Move Up + self.aMoveUp = QAction("Move Item Up", self) + self.aMoveUp.setStatusTip("Move project item up") + self.aMoveUp.setShortcut("Ctrl+Up") + self.aMoveUp.triggered.connect(lambda: self._moveTreeItem(-1)) + self.projMenu.addAction(self.aMoveUp) + + # Project > Move Down + self.aMoveDown = QAction("Move Item Down", self) + self.aMoveDown.setStatusTip("Move project item down") + self.aMoveDown.setShortcut("Ctrl+Down") + self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1)) + self.projMenu.addAction(self.aMoveDown) + + # Project > Undo Last Action + self.aMoveUndo = QAction("Undo Last Move", self) + self.aMoveUndo.setStatusTip("Undo last item move") + self.aMoveUndo.setShortcut("Ctrl+Shift+Z") + self.aMoveUndo.triggered.connect(lambda: self.theParent.treeView.undoLastMove()) + self.projMenu.addAction(self.aMoveUndo) + # Project > Empty Trash self.aEmptyTrash = QAction("Empty Trash", self) self.aEmptyTrash.setStatusTip("Permanently delete all files in the Trash folder") @@ -891,23 +909,6 @@ class GuiMainMenu(QMenuBar): # Tools self.toolsMenu = self.addMenu("&Tools") - # Tools > Move Up - self.aMoveUp = QAction("Move Tree Item Up", self) - self.aMoveUp.setStatusTip("Move item up") - self.aMoveUp.setShortcut("Ctrl+Shift+Up") - self.aMoveUp.triggered.connect(lambda: self._moveTreeItem(-1)) - self.toolsMenu.addAction(self.aMoveUp) - - # Tools > Move Down - self.aMoveDown = QAction("Move Tree Item Down", self) - self.aMoveDown.setStatusTip("Move item down") - self.aMoveDown.setShortcut("Ctrl+Shift+Down") - self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1)) - self.toolsMenu.addAction(self.aMoveDown) - - # Tools > Separator - self.toolsMenu.addSeparator() - # Tools > Toggle Spell Check self.aSpellCheck = QAction("Check Spelling", self) self.aSpellCheck.setStatusTip("Toggle check spelling") diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 31cdb19a..a2e7c7b0 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -33,12 +33,12 @@ from time import time from PyQt5.QtCore import Qt, QSize, pyqtSignal from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import ( - qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction + QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction ) from nw.core import NWDoc from nw.constants import ( - nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst + nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst, nwLists ) logger = logging.getLogger(__name__) @@ -68,6 +68,7 @@ class GuiProjectTree(QTreeWidget): self._treeMap = {} self._treeChanged = False self._timeChanged = 0 + self._lastMove = {} ## # Build GUI @@ -297,10 +298,10 @@ class GuiProjectTree(QTreeWidget): # Save the text and index it newDoc.saveDocument(newText) - self.theParent.theIndex.scanText(tHandle, newText) + self.theIndex.scanText(tHandle, newText) # Get Word Counts - cC, wC, pC = self.theParent.theIndex.getCounts(tHandle) + cC, wC, pC = self.theIndex.getCounts(tHandle) nwItem.setCharCount(cC) nwItem.setWordCount(wC) nwItem.setParaCount(pC) @@ -335,7 +336,7 @@ class GuiProjectTree(QTreeWidget): logger.error("No project open") return False - if qApp.focusWidget() != self: + if not self.hasFocus(): return False tHandle = self.getSelectedHandle() @@ -361,6 +362,7 @@ class GuiProjectTree(QTreeWidget): return False cItem = pItem.takeChild(tIndex) pItem.insertChild(nIndex, cItem) + self._recordLastMove(cItem, pItem, tIndex) self.clearSelection() cItem.setSelected(True) @@ -517,8 +519,9 @@ class GuiProjectTree(QTreeWidget): theDoc = NWDoc(self.theProject, self.theParent) theDoc.deleteDocument(tHandle) - del self.theProject.projTree[tHandle] self.theIndex.deleteHandle(tHandle) + self._deleteTreeItem(tHandle) + self._setTreeChanged(True) else: # The file is not already in the trash folder, so we @@ -541,11 +544,12 @@ class GuiProjectTree(QTreeWidget): tIndex = trItemP.indexOfChild(trItemS) trItemC = trItemP.takeChild(tIndex) trItemT.addChild(trItemC) - nwItemS.setParent(self.theProject.projTree.trashRoot()) + self._updateItemParent(tHandle) self.propagateCount(tHandle, wCount) - self._setTreeChanged(True) self.theIndex.deleteHandle(tHandle) + self._recordLastMove(trItemS, trItemP, tIndex) + self._setTreeChanged(True) elif nwItemS.itemType == nwItemType.FOLDER: logger.debug("User requested folder %s deleted" % tHandle) @@ -556,7 +560,8 @@ class GuiProjectTree(QTreeWidget): tIndex = trItemP.indexOfChild(trItemS) if trItemS.childCount() == 0: trItemP.takeChild(tIndex) - del self.theProject.projTree[tHandle] + self._deleteTreeItem(tHandle) + self._setTreeChanged(True) else: self.makeAlert(( "Cannot delete folder. It is not empty. " @@ -570,7 +575,7 @@ class GuiProjectTree(QTreeWidget): tIndex = self.indexOfTopLevelItem(trItemS) if trItemS.childCount() == 0: self.takeTopLevelItem(tIndex) - del self.theProject.projTree[tHandle] + self._deleteTreeItem(tHandle) self.theParent.mainMenu.setAvailableRoot() self._setTreeChanged(True) else: @@ -679,6 +684,52 @@ class GuiProjectTree(QTreeWidget): logger.debug("%d items added to the project tree" % iCount) return True + def undoLastMove(self): + """Attempt to undo the last action. + """ + srcItem = self._lastMove.get("item", None) + dstItem = self._lastMove.get("parent", None) + dstIndex = self._lastMove.get("index", None) + + if not self.hasFocus(): + return False + + if srcItem is None or dstItem is None or dstIndex is None: + logger.verbose("No tree move to undo") + return False + + if srcItem not in self._treeMap.values(): + logger.warning("Source item no longer exists") + return False + + if dstItem not in self._treeMap.values(): + logger.warning("Previous parent item no longer exists") + return False + + dstIndex = min(max(0, dstIndex), dstItem.childCount()) + wCount = int(srcItem.data(self.C_COUNT, Qt.UserRole)) + sHandle = srcItem.data(self.C_NAME, Qt.UserRole) + dHandle = dstItem.data(self.C_NAME, Qt.UserRole) + logger.debug("Moving item %s back to %s, index %d" % ( + sHandle, dHandle, dstIndex + )) + + self.propagateCount(sHandle, 0) + parItem = srcItem.parent() + srcIndex = parItem.indexOfChild(srcItem) + movItem = parItem.takeChild(srcIndex) + dstItem.insertChild(dstIndex, movItem) + + snItem = self.theProject.projTree[sHandle] + dnItem = self.theProject.projTree[dHandle] + self._postItemMove(sHandle, snItem, dnItem, wCount) + + self.clearSelection() + movItem.setSelected(True) + self._lastMove = {} + + return True + def getSelectedHandle(self): """Get the currently selected handle. If multiple items are selected, return the first. @@ -787,46 +838,30 @@ class GuiProjectTree(QTreeWidget): self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR) return + pItem = sItem.parent() + pIndex = 0 + if pItem is not None: + pIndex = pItem.indexOfChild(sItem) + wCount = int(sItem.data(self.C_COUNT, Qt.UserRole)) + isFile = snItem.itemType == nwItemType.FILE + isRoot = snItem.itemType == nwItemType.ROOT + onFile = dnItem.itemType == nwItemType.FILE + isSame = snItem.itemClass == dnItem.itemClass isNone = snItem.itemClass == nwItemClass.NO_CLASS isNote = snItem.itemLayout == nwItemLayout.NOTE - onFile = dnItem.itemType == nwItemType.FILE - isRoot = snItem.itemType == nwItemType.ROOT - onFree = dnItem.itemClass == nwItemClass.ARCHIVE - onFree |= dnItem.itemClass == nwItemClass.TRASH - onFree &= snItem.itemType == nwItemType.FILE - isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem - if (isSame or isNone or isNote or onFree) and not (onFile and isOnTop) and not isRoot: + onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile + + allowDrop = isSame or isNone or isNote or onFree + allowDrop &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile) + + if allowDrop and not isRoot: logger.debug("Drag'n'drop of item %s accepted" % sHandle) self.propagateCount(sHandle, 0) QTreeWidget.dropEvent(self, theEvent) - self._updateItemParent(sHandle) - - # If the item does not have the same class as the target, - # and the target is not a free root folder, update its class - if not (isSame or onFree): - logger.debug("Item %s class has been changed from %s to %s" % ( - sHandle, - snItem.itemClass.name, - dnItem.itemClass.name - )) - snItem.setClass(dnItem.itemClass) - self.setTreeItemValues(sHandle) - - self.propagateCount(sHandle, wCount) - - # The items dropped into archive or trash should be removed - # from the project index, for all other items, we rescan the - # file to ensure the index is up to date. - if onFree: - self.theIndex.deleteHandle(sHandle) - else: - self.theIndex.reIndexHandle(sHandle) - - # Trigger dependent updates - self._setTreeChanged(True) - self._emitItemChange(sHandle) + self._postItemMove(sHandle, snItem, dnItem, wCount) + self._recordLastMove(sItem, pItem, pIndex) else: theEvent.ignore() @@ -839,22 +874,63 @@ class GuiProjectTree(QTreeWidget): # Internal Functions ## + def _postItemMove(self, sHandle, snItem, dnItem, wCount): + """Run various maintenance tasks for a moved item. + """ + isFile = snItem.itemType == nwItemType.FILE + isSame = snItem.itemClass == dnItem.itemClass + onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile + + self._updateItemParent(sHandle) + + # If the item does not have the same class as the target, + # and the target is not a free root folder, update its class + if not (isSame or onFree): + logger.debug("Item %s class has been changed from %s to %s" % ( + sHandle, snItem.itemClass.name, dnItem.itemClass.name + )) + snItem.setClass(dnItem.itemClass) + self.setTreeItemValues(sHandle) + + self.propagateCount(sHandle, wCount) + + # The items dropped into archive or trash should be removed + # from the project index, for all other items, we rescan the + # file to ensure the index is up to date. + if onFree: + self.theIndex.deleteHandle(sHandle) + else: + self.theIndex.reIndexHandle(sHandle) + + # Trigger dependent updates + self._setTreeChanged(True) + self._emitItemChange(sHandle) + + return + def _getTreeItem(self, tHandle): """Returns the QTreeWidgetItem of a given item handle. """ return self._treeMap.get(tHandle, None) - def _scanChildren(self, theList, theItem, theIndex): + def _deleteTreeItem(self, tHandle): + """Delete a tree item from the project and the map. + """ + del self.theProject.projTree[tHandle] + self._treeMap.pop(tHandle, None) + return + + def _scanChildren(self, theList, tItem, tIndex): """This is a recursive function returning all items in a tree starting at a given QTreeWidgetItem. """ - tHandle = theItem.data(self.C_NAME, Qt.UserRole) + tHandle = tItem.data(self.C_NAME, Qt.UserRole) nwItem = self.theProject.projTree[tHandle] - nwItem.setExpanded(theItem.isExpanded()) - nwItem.setOrder(theIndex) + nwItem.setExpanded(tItem.isExpanded()) + nwItem.setOrder(tIndex) theList.append(tHandle) - for i in range(theItem.childCount()): - self._scanChildren(theList, theItem.child(i), i) + for i in range(tItem.childCount()): + self._scanChildren(theList, tItem.child(i), i) return theList def _addTreeItem(self, nwItem, nHandle=None): @@ -984,6 +1060,19 @@ class GuiProjectTree(QTreeWidget): return + def _recordLastMove(self, srcItem, parItem, parIndex): + """Record the last action so that it can be undone. + """ + prevItem = self._lastMove.get("item", None) + if prevItem is None or srcItem != prevItem: + self._lastMove = { + "item": srcItem, + "parent": parItem, + "index": parIndex, + } + + return + # END Class GuiProjectTree class GuiProjectTreeMenu(QMenu): diff --git a/nw/guimain.py b/nw/guimain.py index f6538bf0..eff2f3d3 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -1167,11 +1167,11 @@ class GuiMain(QMainWindow): """ if self.docEditor.theHandle is None: logger.error("No document open, so not activating Focus Mode") - self.mainMenu.aFocusMode.setChecked(self.isFocusMode) + self.mainMenu.setFocusMode(self.isFocusMode) return False self.isFocusMode = not self.isFocusMode - self.mainMenu.aFocusMode.setChecked(self.isFocusMode) + self.mainMenu.setFocusMode(self.isFocusMode) if self.isFocusMode: logger.debug("Activating Focus Mode") self.mainTabs.setCurrentWidget(self.splitDocs) diff --git a/sample/ToC.txt b/sample/ToC.txt deleted file mode 100644 index b106e892..00000000 --- a/sample/ToC.txt +++ /dev/null @@ -1,23 +0,0 @@ - -Table of Contents -================= - -File Name Class Layout Document Label ---------------------------------------------------------------------- -content/53b69b83cdafc.nwd NOVEL TITLE Title Page -content/974e400180a99.nwd NOVEL PAGE Page -content/edca4be2fcaf8.nwd NOVEL PARTITION Part One -content/6a2d6d5f4f401.nwd NOVEL CHAPTER Chapter One -content/636b6aa9b697b.nwd NOVEL SCENE Making a Scene -content/bc0cbd2a407f3.nwd NOVEL SCENE Another Scene -content/ba8a28a246524.nwd NOVEL UNNUMBERED Interlude -content/96b68994dfa3d.nwd NOVEL NOTE A Note on Structure -content/88706ddc78b1b.nwd NOVEL CHAPTER Chapter Two -content/ae7339df26ded.nwd NOVEL SCENE We Found John! -content/14298de4d9524.nwd CHARACTER NOTE John Smith -content/bb2c23b3c42cc.nwd CHARACTER NOTE Jane Smith -content/b3e74dbc1f584.nwd WORLD NOTE Earth -content/f1471bef9f2ae.nwd WORLD NOTE Space -content/5eaea4e8cdee8.nwd WORLD NOTE Mars -content/8a5deb88c0e97.nwd NOVEL SCENE Old File -content/b8136a5a774a0.nwd NOVEL SCENE Delete Me! diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 69bd2eee..cbb940be 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 874 + 881 157 - 42533 + 42668 False @@ -117,7 +117,7 @@ 1st Draft True SCENE - 1811 + 1810 318 8 1880 diff --git a/tests/lipsum/ToC.txt b/tests/lipsum/ToC.txt deleted file mode 100644 index a499c6f8..00000000 --- a/tests/lipsum/ToC.txt +++ /dev/null @@ -1,22 +0,0 @@ - - Table of Contents -=================== - - File Name Class Document Label --------------------------------------------------------------------------------- - content/04468803b92e1.nwd WORLD Ancient Europe - content/2426c6f0ca922.nwd PLOT Main - content/441420a886d82.nwd NOVEL Chapter Two - content/47666c91c7ccf.nwd NOVEL Scene Five - content/4c4f28287af27.nwd CHARACTER Mr. Nobody - content/7a992350f3eb6.nwd NOVEL Lorem Ipsum - content/846352075de7d.nwd NOVEL Interlude - content/88243afbe5ed8.nwd NOVEL Scene One - content/88d59a277361b.nwd NOVEL Prologue - content/8c58a65414c23.nwd NOVEL Front Matter - content/db7e733775d4d.nwd NOVEL Act One - content/eb103bc70c90c.nwd NOVEL Scene Three - content/f8c0562e50f1b.nwd NOVEL Scene Four - content/f96ec11c6a3da.nwd NOVEL Scene Two - content/fb609cd8319dc.nwd NOVEL Chapter One - diff --git a/tests/minimal/ToC.txt b/tests/minimal/ToC.txt deleted file mode 100644 index f5b94b72..00000000 --- a/tests/minimal/ToC.txt +++ /dev/null @@ -1,10 +0,0 @@ - - Table of Contents -=================== - - File Name Class Document Label --------------------------------------------------------------------------------- - content/8c659a11cd429.nwd NOVEL New Scene - content/a35baf2e93843.nwd NOVEL Title Page - content/f5ab3e30151e1.nwd NOVEL New Chapter - diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index ba435ad0..b169e457 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -28,8 +28,10 @@ from tools import cmpFiles from PyQt5.QtCore import Qt from PyQt5.QtGui import QTextCursor -from PyQt5.QtWidgets import QAction, QMessageBox +from PyQt5.QtWidgets import QAction, QMessageBox, QDialog +from nw.gui.itemeditor import GuiItemEditor +from nw.gui.doceditor import GuiDocEditor from nw.gui.projtree import GuiProjectTree from nw.constants import nwItemType, nwDocAction @@ -43,7 +45,11 @@ def testGuiEditor_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, refDir, outDi """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(GuiItemEditor, "exec_", lambda *args: None) + monkeypatch.setattr(GuiItemEditor, "result", lambda *args: QDialog.Accepted) monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *args: True) + monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *args: True) # Create new, save, close project nwGUI.theProject.projTree.setSeed(42) @@ -353,6 +359,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, nwLipsum): """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *args: True) nwGUI.theProject.projTree.setSeed(42) assert nwGUI.openProject(nwLipsum) diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 8c928a97..3585c8c8 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -38,6 +38,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes) # Open project nwGUI.theProject.projTree.setSeed(42) diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index dcd32e91..f6504abf 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -27,6 +27,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QTextCursor, QTextBlock from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox +from nw.gui.doceditor import GuiDocEditor from nw.constants import nwUnicode, nwDocAction, nwDocInsert, nwKeyWords keyDelay = 2 @@ -39,6 +40,7 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, nwLipsum): """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *args: True) # Test Document Action with No Project assert not nwGUI.docEditor.docAction(nwDocAction.COPY) diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 125e59b4..bfeb2f96 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -37,6 +37,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum): """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes) assert nwGUI.openProject(nwLipsum) nwGUI.mainConf.lastPath = nwLipsum diff --git a/tests/test_gui/test_gui_projsettings.py b/tests/test_gui/test_gui_projsettings.py index 50a5fff1..9955aac8 100644 --- a/tests/test_gui/test_gui_projsettings.py +++ b/tests/test_gui/test_gui_projsettings.py @@ -48,6 +48,7 @@ def testGuiProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *args: QMessageBox.Yes) # Check that we cannot open when there is no project nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 317b8c58..b0fdaa89 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -38,6 +38,9 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "warning", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes) monkeypatch.setattr(GuiMain, "editItem", lambda *args: None) monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *args: True) @@ -96,12 +99,12 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): nwTree.setSelectedHandle("8c659a11cd429") # Shift focus and try to move item - monkeypatch.setattr("PyQt5.QtWidgets.qApp.focusWidget", lambda: None) + monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *args: False) assert not nwTree.moveTreeItem(1) assert nwTree.getTreeFromHandle("a6d311a93600a") == [ "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9" ] - monkeypatch.setattr("PyQt5.QtWidgets.qApp.focusWidget", lambda: nwTree) + monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *args: True) # Move second item up twice (should give same result) nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) @@ -131,6 +134,15 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429" ] + # Move up twice, and undo + nwTree._lastMove = {} + nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) + nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) + nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger) + assert nwTree.getTreeFromHandle("a6d311a93600a") == [ + "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429" + ] + # Move a root item (top level items are different) twice nwTree.flushTreeOrder() assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10 diff --git a/tests/test_gui/test_gui_projwizard.py b/tests/test_gui/test_gui_projwizard.py index f2a967fc..4208bc8f 100644 --- a/tests/test_gui/test_gui_projwizard.py +++ b/tests/test_gui/test_gui_projwizard.py @@ -46,6 +46,7 @@ def testGuiProjectWizard_Main(qtbot, monkeypatch, nwGUI, nwMinimal): """ # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *args: QMessageBox.Yes) if sys.platform.startswith("darwin"): # Disable for macOS because the test segfaults on QWizard.show()