From 624787994aa503b1f3eb3eec672fa99d741c1a11 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 25 Oct 2022 08:17:18 +0200 Subject: [PATCH 1/5] Refactor the projhect tree context menu code --- novelwriter/gui/projtree.py | 77 +++++++++++++++---------------------- 1 file changed, 30 insertions(+), 47 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 96dc98f1..202efee0 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -1128,9 +1128,8 @@ class GuiProjectTree(QTreeWidget): trashHandle = self.theProject.tree.trashRoot() if tItem.itemHandle == trashHandle and trashHandle is not None: # The trash folder only has one option - ctxMenu.addAction( - self.tr("Empty Trash"), lambda: self.emptyTrash() - ) + aEmptyTrash = ctxMenu.addAction(self.tr("Empty Trash")) + aEmptyTrash.triggered.connect(lambda: self.emptyTrash()) ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) return True @@ -1143,12 +1142,12 @@ class GuiProjectTree(QTreeWidget): hasChild = selItem.childCount() > 0 if isFile: - ctxMenu.addAction( - self.tr("Open Document"), + aOpenDoc = ctxMenu.addAction(self.tr("Open Document")) + aOpenDoc.triggered.connect( lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "") ) - ctxMenu.addAction( - self.tr("View Document"), + aViewDoc = ctxMenu.addAction(self.tr("View Document")) + aViewDoc.triggered.connect( lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") ) ctxMenu.addSeparator() @@ -1156,14 +1155,12 @@ class GuiProjectTree(QTreeWidget): # Edit Item Settings # ================== - ctxMenu.addAction( - self.tr("Change Label"), lambda: self.renameTreeItem(tHandle) - ) + aLabel = ctxMenu.addAction(self.tr("Change Label")) + aLabel.triggered.connect(lambda: self.renameTreeItem(tHandle)) if isFile: - ctxMenu.addAction( - self.tr("Toggle Active"), lambda: self._toggleItemActive(tHandle) - ) + aActive = ctxMenu.addAction(self.tr("Toggle Active")) + aActive.triggered.connect(lambda: self._toggleItemActive(tHandle)) if tItem.isNovelLike(): mStatus = ctxMenu.addMenu(self.tr("Set Status to ...")) @@ -1193,38 +1190,30 @@ class GuiProjectTree(QTreeWidget): isNoteFile = isFile and tItem.isNoteLayout() if (isNoteFile or isFolder) and tItem.documentAllowed(): - mTrans.addAction( - self.tr("Convert to {0}").format(trDoc), + aConvert1 = mTrans.addAction(self.tr("Convert to {0}").format(trDoc)) + aConvert1.triggered.connect( lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT) ) if isDocFile or isFolder: - mTrans.addAction( - self.tr("Convert to {0}").format(trNote), + aConvert2 = mTrans.addAction(self.tr("Convert to {0}").format(trNote)) + aConvert2.triggered.connect( lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE) ) if hasChild and isFile: - mTrans.addAction( - self.tr("Merge Child Items into Self"), - lambda: self._mergeDocuments(tHandle, False) - ) - mTrans.addAction( - self.tr("Merge Child Items into New"), - lambda: self._mergeDocuments(tHandle, True) - ) + aMerge1 = mTrans.addAction(self.tr("Merge Child Items into Self")) + aMerge1.triggered.connect(lambda: self._mergeDocuments(tHandle, False)) + aMerge2 = mTrans.addAction(self.tr("Merge Child Items into New")) + aMerge2.triggered.connect(lambda: self._mergeDocuments(tHandle, True)) if hasChild and isFolder: - mTrans.addAction( - self.tr("Merge Documents in Folder"), - lambda: self._mergeDocuments(tHandle, True) - ) + aMerge3 = mTrans.addAction(self.tr("Merge Documents in Folder")) + aMerge3.triggered.connect(lambda: self._mergeDocuments(tHandle, True)) if isFile: - mTrans.addAction( - self.tr("Split Document by Headers"), - lambda: self._splitDocument(tHandle) - ) + aSplit1 = mTrans.addAction(self.tr("Split Document by Headers")) + aSplit1.triggered.connect(lambda: self._splitDocument(tHandle)) # Expand/Collapse/Delete # ====================== @@ -1232,23 +1221,17 @@ class GuiProjectTree(QTreeWidget): ctxMenu.addSeparator() if hasChild: - ctxMenu.addAction( - self.tr("Expand All"), - lambda: self.setExpandedFromHandle(tHandle, True) - ) - ctxMenu.addAction( - self.tr("Collapse All"), - lambda: self.setExpandedFromHandle(tHandle, False) - ) + aExpand = ctxMenu.addAction(self.tr("Expand All")) + aExpand.triggered.connect(lambda: self.setExpandedFromHandle(tHandle, True)) + aCollapse = ctxMenu.addAction(self.tr("Collapse All")) + aCollapse.triggered.connect(lambda: self.setExpandedFromHandle(tHandle, False)) if tItem.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild): - ctxMenu.addAction( - self.tr("Delete Permanently"), lambda: self.permanentlyDeleteItem(tHandle) - ) + aDelete = ctxMenu.addAction(self.tr("Delete Permanently")) + aDelete.triggered.connect(lambda: self.permanentlyDeleteItem(tHandle)) else: - ctxMenu.addAction( - self.tr("Move to Trash"), lambda: self.moveItemToTrash(tHandle) - ) + aMoveTrash = ctxMenu.addAction(self.tr("Move to Trash")) + aMoveTrash.triggered.connect(lambda: self.moveItemToTrash(tHandle)) # Show Context Menu ctxMenu.exec_(self.viewport().mapToGlobal(clickPos)) From e89f00453294a9ff16cc044a6553d5ed3713dba4 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 25 Oct 2022 12:20:18 +0200 Subject: [PATCH 2/5] Add a manage labels option to project tree context menu (#1203) --- novelwriter/custom.py | 6 ++++++ novelwriter/dialogs/projsettings.py | 23 ++++++++++++++++++++++- novelwriter/gui/projtree.py | 15 ++++++++++++++- novelwriter/guimain.py | 6 ++++-- 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/novelwriter/custom.py b/novelwriter/custom.py index d516e2dd..5d9d8122 100644 --- a/novelwriter/custom.py +++ b/novelwriter/custom.py @@ -421,6 +421,12 @@ class PagedDialog(QDialog): self._buttonBox.addWidget(buttonBar) return + def setCurrentWidget(self, widget): + """Forward the changing of tab to the QTabWidget. + """ + self._tabBox.setCurrentWidget(widget) + return + # END Class PagedDialog diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 86c268d6..5760d982 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -43,7 +43,12 @@ logger = logging.getLogger(__name__) class GuiProjectSettings(PagedDialog): - def __init__(self, mainGui): + TAB_MAIN = 0 + TAB_STATUS = 1 + TAB_IMPORT = 2 + TAB_REPLACE = 3 + + def __init__(self, mainGui, focusTab=TAB_MAIN): super().__init__(parent=mainGui) logger.debug("Initialising GuiProjectSettings ...") @@ -85,6 +90,9 @@ class GuiProjectSettings(PagedDialog): # Flags self.spellChanged = False + # Focus Tab + self._focusTab(focusTab) + logger.debug("GuiProjectSettings initialisation complete") return @@ -141,6 +149,19 @@ class GuiProjectSettings(PagedDialog): # Internal Functions ## + def _focusTab(self, tab): + """Change which is the focused tab. + """ + if tab == self.TAB_MAIN: + self.setCurrentWidget(self.tabMain) + elif tab == self.TAB_STATUS: + self.setCurrentWidget(self.tabStatus) + elif tab == self.TAB_IMPORT: + self.setCurrentWidget(self.tabImport) + elif tab == self.TAB_REPLACE: + self.setCurrentWidget(self.tabReplace) + return + def _saveGuiSettings(self): """Save GUI settings. """ diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 202efee0..f4b60161 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -41,7 +41,7 @@ from PyQt5.QtWidgets import ( from novelwriter.core import DocMerger, DocSplitter from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert -from novelwriter.dialogs import GuiDocMerge, GuiDocSplit, GuiEditLabel +from novelwriter.dialogs import GuiDocMerge, GuiDocSplit, GuiEditLabel, GuiProjectSettings from novelwriter.constants import nwHeaders, trConst, nwLabels logger = logging.getLogger(__name__) @@ -62,6 +62,9 @@ class GuiProjectView(QWidget): selectedItemChanged = pyqtSignal(str) openDocumentRequest = pyqtSignal(str, Enum, int, str) + # Requests for the main GUI + projectSettingsRequest = pyqtSignal(int) + def __init__(self, mainGui): super().__init__(parent=mainGui) @@ -1169,6 +1172,11 @@ class GuiProjectTree(QTreeWidget): aStatus.triggered.connect( lambda n, key=key: self._changeItemStatus(tHandle, key) ) + mStatus.addSeparator() + aManage1 = mStatus.addAction("Manage Labels ...") + aManage1.triggered.connect( + lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_STATUS) + ) else: mImport = ctxMenu.addMenu(self.tr("Set Importance to ...")) for n, (key, entry) in enumerate(self.theProject.importItems.items()): @@ -1176,6 +1184,11 @@ class GuiProjectTree(QTreeWidget): aImport.triggered.connect( lambda n, key=key: self._changeItemImport(tHandle, key) ) + mImport.addSeparator() + aManage2 = mImport.addAction("Manage Labels ...") + aManage2.triggered.connect( + lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_IMPORT) + ) # Transform Item # ============== diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index fee7c7d6..4025dd05 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -208,6 +208,7 @@ class GuiMain(QMainWindow): self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem) self.projView.rootFolderChanged.connect(self.novelView.updateRootItem) self.projView.rootFolderChanged.connect(self.projView.updateRootItem) + self.projView.projectSettingsRequest.connect(self.showProjectSettingsDialog) self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox) self.novelView.openDocumentRequest.connect(self._openDocument) @@ -921,14 +922,15 @@ class GuiMain(QMainWindow): return - def showProjectSettingsDialog(self): + @pyqtSlot(int) + def showProjectSettingsDialog(self, focusTab=GuiProjectSettings.TAB_MAIN): """Open the project settings dialog. """ if not self.hasProject: logger.error("No project open") return False - dlgProj = GuiProjectSettings(self) + dlgProj = GuiProjectSettings(self, focusTab=focusTab) dlgProj.exec_() if dlgProj.result() == QDialog.Accepted: From 2ee6adcf352de52113806753a767a9189a2f6c54 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 25 Oct 2022 15:47:40 +0200 Subject: [PATCH 3/5] Clean up function mappings in project tree class --- novelwriter/gui/projtree.py | 13 ++- novelwriter/guimain.py | 12 +-- tests/test_gui/test_gui_doceditor.py | 2 +- tests/test_gui/test_gui_outline.py | 4 +- tests/test_gui/test_gui_projtree.py | 120 ++++++++++++++------------- tests/test_gui/test_gui_statusbar.py | 2 +- 6 files changed, 79 insertions(+), 74 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index f4b60161..bd77a57a 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -105,9 +105,6 @@ class GuiProjectView(QWidget): self.keyContext.activated.connect(lambda: self.projTree.openContextOnSelected()) # Function Mappings - self.revealNewTreeItem = self.projTree.revealNewTreeItem - self.renameTreeItem = self.projTree.renameTreeItem - self.getTreeFromHandle = self.projTree.getTreeFromHandle self.emptyTrash = self.projTree.emptyTrash self.requestDeleteItem = self.projTree.requestDeleteItem self.setTreeItemValues = self.projTree.setTreeItemValues @@ -164,6 +161,16 @@ class GuiProjectView(QWidget): """ return self.projTree.hasFocus() + def renameTreeItem(self, tHandle=None): + """External request to rename an item or the currently selected + item. This is triggered by the global menu or keyboard shortcut. + """ + if tHandle is None: + tHandle = self.projTree.getSelectedHandle() + if tHandle: + return self.projTree.renameTreeItem(tHandle) + return + ## # Public Slots ## diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 4025dd05..cf0a9dff 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -807,15 +807,11 @@ class GuiMain(QMainWindow): logger.error("No project open") return False - if tHandle is None: - if self.docEditor.anyFocus() or self.isFocusMode: - tHandle = self.docEditor.docHandle() - else: - tHandle = self.projView.getSelectedHandle() - if tHandle: - return self.projView.renameTreeItem(tHandle) + if tHandle is None and (self.docEditor.anyFocus() or self.isFocusMode): + tHandle = self.docEditor.docHandle() + self.projView.renameTreeItem(tHandle) - return False + return True def rebuildTrees(self): """Rebuild the project tree. diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index b3bc49b1..68f02c71 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -1144,7 +1144,7 @@ def testGuiEditor_Tags(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): assert nwGUI.openDocument(cHandle) is True assert nwGUI.docEditor.replaceText(theText) is True assert nwGUI.saveDocument() is True - assert nwGUI.projView.revealNewTreeItem(cHandle) + assert nwGUI.projView.projTree.revealNewTreeItem(cHandle) nwGUI.docEditor.updateTagHighLighting() # Follow Tag diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 2f33cf7a..457be313 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -183,7 +183,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): # Add a second novel folder newHandle = nwGUI.theProject.newRoot(nwItemClass.NOVEL) - nwGUI.projView.revealNewTreeItem(newHandle) + nwGUI.projView.projTree.revealNewTreeItem(newHandle) # Check new values in dropdown list assert outlineBar.novelValue.itemData(0) == lipHandle @@ -202,7 +202,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum): aHandle = nwGUI.theProject.newFile(dTitle, newHandle) hHash = "#"*hLevel writeFile(os.path.join(nwLipsum, "content", f"{aHandle}.nwd"), f"{hHash} {dTitle}\n\n") - nwGUI.projView.revealNewTreeItem(aHandle) + nwGUI.projView.projTree.revealNewTreeItem(aHandle) nwGUI.rebuildIndex() diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index b5201846..ecf78341 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -160,11 +160,11 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) # ============ # Also check error handling in reveal function - assert projView.revealNewTreeItem("abc") is False + assert projView.projTree.revealNewTreeItem("abc") is False # Add an item that cannot be displayed in the tree nHandle = theProject.newFile("Test", None) - assert projView.revealNewTreeItem(nHandle) is False + assert projView.projTree.revealNewTreeItem(nHandle) is False # Clean up # qtbot.stop() @@ -184,10 +184,11 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - nwTree = nwGUI.projView + projView = nwGUI.projView + projTree = nwGUI.projView.projTree # Try to move item with no project - assert nwTree.projTree.moveTreeItem(1) is False + assert projView.projTree.moveTreeItem(1) is False # Create a project prjDir = os.path.join(fncDir, "project") @@ -197,68 +198,68 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # ============== # Add some files - nwTree.setSelectedHandle(C.hChapterDir) - assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True - assert nwTree.getTreeFromHandle(C.hChapterDir) == [ + projView.setSelectedHandle(C.hChapterDir) + assert projTree.newTreeItem(nwItemType.FILE) is True + assert projTree.newTreeItem(nwItemType.FILE) is True + assert projTree.newTreeItem(nwItemType.FILE) is True + assert projTree.getTreeFromHandle(C.hChapterDir) == [ C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000010", "0000000000011", "0000000000012", ] # Move with no selections - nwTree.projTree.clearSelection() - assert nwTree.projTree.moveTreeItem(1) is False + projTree.clearSelection() + assert projTree.moveTreeItem(1) is False # Move second item up twice (should give same result) - nwTree.setSelectedHandle(C.hSceneDoc) - assert nwTree.projTree.moveTreeItem(-1) is True - assert nwTree.getTreeFromHandle(C.hChapterDir) == [ + projView.setSelectedHandle(C.hSceneDoc) + assert projTree.moveTreeItem(-1) is True + assert projTree.getTreeFromHandle(C.hChapterDir) == [ C.hChapterDir, C.hSceneDoc, C.hChapterDoc, "0000000000010", "0000000000011", "0000000000012", ] - assert nwTree.projTree.moveTreeItem(-1) is False - assert nwTree.getTreeFromHandle(C.hChapterDir) == [ + assert projTree.moveTreeItem(-1) is False + assert projTree.getTreeFromHandle(C.hChapterDir) == [ C.hChapterDir, C.hSceneDoc, C.hChapterDoc, "0000000000010", "0000000000011", "0000000000012", ] # Restore - assert nwTree.projTree.moveTreeItem(1) is True - assert nwTree.getTreeFromHandle(C.hChapterDir) == [ + assert projTree.moveTreeItem(1) is True + assert projTree.getTreeFromHandle(C.hChapterDir) == [ C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000010", "0000000000011", "0000000000012", ] # Move fifth item down twice (should give same result) - nwTree.setSelectedHandle("0000000000011") - assert nwTree.projTree.moveTreeItem(1) is True - assert nwTree.getTreeFromHandle(C.hChapterDir) == [ + projView.setSelectedHandle("0000000000011") + assert projTree.moveTreeItem(1) is True + assert projTree.getTreeFromHandle(C.hChapterDir) == [ C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000010", "0000000000012", "0000000000011", ] - assert nwTree.projTree.moveTreeItem(1) is False - assert nwTree.getTreeFromHandle(C.hChapterDir) == [ + assert projTree.moveTreeItem(1) is False + assert projTree.getTreeFromHandle(C.hChapterDir) == [ C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000010", "0000000000012", "0000000000011", ] # Restore - assert nwTree.projTree.moveTreeItem(-1) is True - assert nwTree.getTreeFromHandle(C.hChapterDir) == [ + assert projTree.moveTreeItem(-1) is True + assert projTree.getTreeFromHandle(C.hChapterDir) == [ C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000010", "0000000000011", "0000000000012", ] # Move down again, and restore via undo - nwTree.setSelectedHandle("0000000000011") - assert nwTree.projTree.moveTreeItem(1) is True - assert nwTree.getTreeFromHandle(C.hChapterDir) == [ + projView.setSelectedHandle("0000000000011") + assert projTree.moveTreeItem(1) is True + assert projTree.getTreeFromHandle(C.hChapterDir) == [ C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000010", "0000000000012", "0000000000011", ] - assert nwTree.projTree.undoLastMove() is True - assert nwTree.getTreeFromHandle(C.hChapterDir) == [ + assert projTree.undoLastMove() is True + assert projTree.getTreeFromHandle(C.hChapterDir) == [ C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000010", "0000000000011", "0000000000012", ] @@ -266,19 +267,19 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): # Root Folder # =========== - nwTree.setSelectedHandle(C.hNovelRoot) + projView.setSelectedHandle(C.hNovelRoot) assert nwGUI.theProject.tree._treeOrder.index(C.hNovelRoot) == 0 # Move novel folder up - assert nwTree.projTree.moveTreeItem(-1) is False + assert projTree.moveTreeItem(-1) is False assert nwGUI.theProject.tree._treeOrder.index(C.hNovelRoot) == 0 # Move novel folder down - assert nwTree.projTree.moveTreeItem(1) is True + assert projTree.moveTreeItem(1) is True assert nwGUI.theProject.tree._treeOrder.index(C.hNovelRoot) == 1 # Move novel folder up again - assert nwTree.projTree.moveTreeItem(-1) is True + assert projTree.moveTreeItem(-1) is True assert nwGUI.theProject.tree._treeOrder.index(C.hNovelRoot) == 0 # Clean up @@ -299,76 +300,77 @@ def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fncDir, monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - nwView = nwGUI.projView + projView = nwGUI.projView + projTree = nwGUI.projView.projTree # Try to run with no project - assert nwView.requestDeleteItem() is False + assert projView.requestDeleteItem() is False # Create a project prjDir = os.path.join(fncDir, "project") buildTestProject(nwGUI, prjDir) # Try emptying the trash already now, when there is no trash folder - assert nwView.emptyTrash() is False + assert projView.emptyTrash() is False # Add some files - nwView.setSelectedHandle(C.hChapterDir) - assert nwView.projTree.newTreeItem(nwItemType.FILE) is True - assert nwView.projTree.newTreeItem(nwItemType.FILE) is True - assert nwView.projTree.newTreeItem(nwItemType.FILE) is True - assert nwView.getTreeFromHandle(C.hChapterDir) == [ + projView.setSelectedHandle(C.hChapterDir) + assert projTree.newTreeItem(nwItemType.FILE) is True + assert projTree.newTreeItem(nwItemType.FILE) is True + assert projTree.newTreeItem(nwItemType.FILE) is True + assert projTree.getTreeFromHandle(C.hChapterDir) == [ C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000010", "0000000000011", "0000000000012", ] # Delete item without focus -> blocked monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) - nwView.setSelectedHandle("0000000000012") - assert nwView.requestDeleteItem() is False + projView.setSelectedHandle("0000000000012") + assert projView.requestDeleteItem() is False monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) # No selection made - nwView.projTree.clearSelection() + projTree.clearSelection() caplog.clear() - assert nwView.requestDeleteItem() is False + assert projView.requestDeleteItem() is False assert "no item to delete" in caplog.text # Not a valid handle - nwView.projTree.clearSelection() + projTree.clearSelection() caplog.clear() - assert nwView.requestDeleteItem("0000000000000") is False + assert projView.requestDeleteItem("0000000000000") is False assert "No tree item with handle '0000000000000'" in caplog.text # Delete Root Folders # =================== - assert nwView.requestDeleteItem(C.hNovelRoot) is False # Novel Root is blocked - assert nwView.requestDeleteItem(C.hCharRoot) is True # Character Root + assert projView.requestDeleteItem(C.hNovelRoot) is False # Novel Root is blocked + assert projView.requestDeleteItem(C.hCharRoot) is True # Character Root # Delete File # =========== # Block adding trash folder - funcPointer = nwView.projTree._addTrashRoot - nwView.projTree._addTrashRoot = lambda *a: None - assert nwView.requestDeleteItem("0000000000012") is False - nwView.projTree._addTrashRoot = funcPointer + funcPointer = projTree._addTrashRoot + projTree._addTrashRoot = lambda *a: None + assert projView.requestDeleteItem("0000000000012") is False + projTree._addTrashRoot = funcPointer # Delete last two documents, which also adds the trash folder - assert nwView.requestDeleteItem("0000000000012") is True - assert nwView.requestDeleteItem("0000000000011") is True - assert nwView.getTreeFromHandle(C.hChapterDir) == [ + assert projView.requestDeleteItem("0000000000012") is True + assert projView.requestDeleteItem("0000000000011") is True + assert projTree.getTreeFromHandle(C.hChapterDir) == [ C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000010" ] trashHandle = nwGUI.theProject.tree.trashRoot() - assert nwView.getTreeFromHandle(trashHandle) == [ + assert projTree.getTreeFromHandle(trashHandle) == [ trashHandle, "0000000000012", "0000000000011" ] # Try to delete the trash folder caplog.clear() - assert nwView.requestDeleteItem("0000000000013") is False + assert projView.requestDeleteItem("0000000000013") is False assert "Cannot delete the Trash folder" in caplog.text nwGUI.closeProject() diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index 6c99642f..e2e9a5bb 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -40,7 +40,7 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): cHandle = nwGUI.theProject.newFile("A Note", C.hCharRoot) newDoc = NWDoc(nwGUI.theProject, cHandle) newDoc.writeDocument("# A Note\n\n") - nwGUI.projView.revealNewTreeItem(cHandle) + nwGUI.projView.projTree.revealNewTreeItem(cHandle) nwGUI.rebuildIndex(beQuiet=True) # Reference Time From 7b04d576056469f004a7c04362c493d70f51bbf4 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 25 Oct 2022 15:48:29 +0200 Subject: [PATCH 4/5] Refactor project settings test --- novelwriter/dialogs/projsettings.py | 8 +- .../guiProjSettings_Dialog_nwProject.nwx | 83 --- tests/test_dialogs/test_dlg_projsettings.py | 492 ++++++++++++------ tests/tools.py | 11 + 4 files changed, 351 insertions(+), 243 deletions(-) delete mode 100644 tests/reference/guiProjSettings_Dialog_nwProject.nwx diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 5760d982..eb96ffb2 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -88,7 +88,7 @@ class GuiProjectSettings(PagedDialog): self.addControls(self.buttonBox) # Flags - self.spellChanged = False + self._spellChanged = False # Focus Tab self._focusTab(focusTab) @@ -97,6 +97,10 @@ class GuiProjectSettings(PagedDialog): return + @property + def spellChanged(self): + return self._spellChanged + ## # Slots ## @@ -116,7 +120,7 @@ class GuiProjectSettings(PagedDialog): self.theProject.setProjBackup(doBackup) # Remember this as updating spell dictionary can be expensive - self.spellChanged = self.theProject.setSpellLang(spellLang) + self._spellChanged = self.theProject.setSpellLang(spellLang) if self.tabStatus.colChanged: newList, delList = self.tabStatus.getNewList() diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx deleted file mode 100644 index 307aae55..00000000 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ /dev/null @@ -1,83 +0,0 @@ - - - - Project Name - Project Title - Jane Doe - John Doh - 1 - 1 - 0 - - - True - None - False - en - None - None - None - None - 9 - 9 - 0 - - B - D - With This Stuff - - - %title% - %title% - %title% - * * * -
-
- - New - Note - Finished - Final - - - New - Minor - Major - Final - -
- - - - Novel - - - - Title Page - - - - New Chapter - - - - New Chapter - - - - New Scene - - - - Plot - - - - Characters - - - - World - - -
diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index d194229c..48e896bf 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -19,56 +19,46 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import pytest -from shutil import copyfile -from tools import cmpFiles, getGuiItem, buildTestProject +from novelwriter.enum import nwItemType +from tools import C, getGuiItem, buildTestProject from PyQt5.QtGui import QColor from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QDialog, QAction, QMessageBox, QColorDialog +from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.projsettings import GuiProjectSettings keyDelay = 2 typeDelay = 1 stepDelay = 20 -statusKeys = ["s000000", "s000001", "s000002", "s000003"] -importKeys = ["i000004", "i000005", "i000006", "i000007"] @pytest.mark.gui -def testDlgProjSettings_Dialog( - qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDir, refDir, mockRnd -): - """Test the full project settings dialog. +def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI): + """Test the main dialog class. Saving settings is not tested in this + test, but are instead tested in the individual tab tests. """ - projFile = os.path.join(fncProj, "nwProject.nwx") - testFile = os.path.join(outDir, "guiProjSettings_Dialog_nwProject.nwx") - compFile = os.path.join(refDir, "guiProjSettings_Dialog_nwProject.nwx") - # Block message box monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + # Block the GUI blocking thread + monkeypatch.setattr(GuiProjectSettings, "exec_", lambda *a: None) + monkeypatch.setattr(GuiProjectSettings, "result", lambda *a: QDialog.Accepted) + monkeypatch.setattr(GuiProjectSettings, "spellChanged", lambda *a: True) + # Check that we cannot open when there is no project nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) assert getGuiItem("GuiProjectSettings") is None - # Create new project - buildTestProject(nwGUI, fncProj) - nwGUI.mainConf.backupPath = fncDir - + # Pretend we have a project + nwGUI.hasProject = True nwGUI.theProject.setSpellLang("en") - nwGUI.theProject.setBookAuthors("Jane Smith\nJohn Smith") - nwGUI.theProject.setAutoReplace({"A": "B", "C": "D"}) # Get the dialog object - monkeypatch.setattr(GuiProjectSettings, "exec_", lambda *a: None) - monkeypatch.setattr(GuiProjectSettings, "result", lambda *a: QDialog.Accepted) - monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")]) - nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) qtbot.waitUntil(lambda: getGuiItem("GuiProjectSettings") is not None, timeout=1000) @@ -77,83 +67,189 @@ def testDlgProjSettings_Dialog( projEdit.show() qtbot.addWidget(projEdit) + # Switch Tabs + projEdit._focusTab(GuiProjectSettings.TAB_REPLACE) + assert projEdit._tabBox.currentWidget() == projEdit.tabReplace + + projEdit._focusTab(GuiProjectSettings.TAB_IMPORT) + assert projEdit._tabBox.currentWidget() == projEdit.tabImport + + projEdit._focusTab(GuiProjectSettings.TAB_STATUS) + assert projEdit._tabBox.currentWidget() == projEdit.tabStatus + + projEdit._focusTab(GuiProjectSettings.TAB_MAIN) + assert projEdit._tabBox.currentWidget() == projEdit.tabMain + + # Clean Up + projEdit._doClose() + # qtbot.stop() + +# END Test testDlgProjSettings_Dialog + + +@pytest.mark.gui +def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): + """Test the main tab of the project settings dialog. + """ + # Block message box + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + + # Mock components + monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")]) + + # Create new project + buildTestProject(nwGUI, fncProj) + mockRnd.reset() + nwGUI.mainConf.backupPath = fncDir + + # Set some values + theProject = nwGUI.theProject + theProject.setSpellLang("en") + theProject.setBookAuthors("Jane Smith\nJohn Smith") + theProject.setAutoReplace({"A": "B", "C": "D"}) + + # Create Dialog + projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_MAIN) + projSettings.show() + qtbot.addWidget(projSettings) + # Settings Tab # ============ - assert projEdit.tabMain.editName.text() == "New Project" - assert projEdit.tabMain.editTitle.text() == "New Novel" - assert projEdit.tabMain.editAuthors.toPlainText() == "Jane Smith\nJohn Smith" - assert projEdit.tabMain.spellLang.currentData() == "en" - assert projEdit.tabMain.doBackup.isChecked() is False + tabMain = projSettings.tabMain + + assert tabMain.editName.text() == "New Project" + assert tabMain.editTitle.text() == "New Novel" + assert tabMain.editAuthors.toPlainText() == "Jane Smith\nJohn Smith" + assert tabMain.spellLang.currentData() == "en" + assert tabMain.doBackup.isChecked() is False qtbot.wait(stepDelay) - projEdit.tabMain.editName.setText("") + tabMain.editName.setText("") for c in "Project Name": - qtbot.keyClick(projEdit.tabMain.editName, c, delay=typeDelay) - projEdit.tabMain.editTitle.setText("") + qtbot.keyClick(tabMain.editName, c, delay=typeDelay) + tabMain.editTitle.setText("") for c in "Project Title": - qtbot.keyClick(projEdit.tabMain.editTitle, c, delay=typeDelay) + qtbot.keyClick(tabMain.editTitle, c, delay=typeDelay) - projEdit.tabMain.editAuthors.clear() + tabMain.editAuthors.clear() for c in "Jane Doe": - qtbot.keyClick(projEdit.tabMain.editAuthors, c, delay=typeDelay) - qtbot.keyClick(projEdit.tabMain.editAuthors, Qt.Key_Return, delay=keyDelay) + qtbot.keyClick(tabMain.editAuthors, c, delay=typeDelay) + qtbot.keyClick(tabMain.editAuthors, Qt.Key_Return, delay=keyDelay) for c in "John Doh": - qtbot.keyClick(projEdit.tabMain.editAuthors, c, delay=typeDelay) - qtbot.keyClick(projEdit.tabMain.editAuthors, Qt.Key_Return, delay=keyDelay) + qtbot.keyClick(tabMain.editAuthors, c, delay=typeDelay) + qtbot.keyClick(tabMain.editAuthors, Qt.Key_Return, delay=keyDelay) qtbot.wait(stepDelay) - assert projEdit.tabMain.editName.text() == "Project Name" - assert projEdit.tabMain.editTitle.text() == "Project Title" - assert projEdit.tabMain.editAuthors.toPlainText() == "Jane Doe\nJohn Doh\n" + assert tabMain.editName.text() == "Project Name" + assert tabMain.editTitle.text() == "Project Title" + assert tabMain.editAuthors.toPlainText() == "Jane Doe\nJohn Doh\n" + assert projSettings.spellChanged is False + + projSettings._doSave() + assert theProject.projName == "Project Name" + assert theProject.bookTitle == "Project Title" + assert theProject.bookAuthors == ["Jane Doe", "John Doh"] + + # Clean up + projSettings._doClose() + # qtbot.stop() + +# END Test testDlgProjSettings_Main + + +@pytest.mark.gui +def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): + """Test the status and importance tabs of the project settings + dialog. + """ + # Block message box + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) + + # Mock components + monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")]) + + # Create new project + mockRnd.reset() + buildTestProject(nwGUI, fncProj) + nwGUI.mainConf.backupPath = fncDir + + # Set some values + theProject = nwGUI.theProject + theProject.tree[C.hTitlePage].setStatus(C.sFinished) + theProject.tree[C.hChapterDoc].setStatus(C.sDraft) + theProject.tree[C.hSceneDoc].setStatus(C.sDraft) + + nwGUI.projView.projTree.setSelectedHandle(C.hPlotRoot) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) + nwGUI.projView.projTree.setSelectedHandle(C.hCharRoot) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) + nwGUI.projView.projTree.setSelectedHandle(C.hWorldRoot) + nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) + + hPlotNote = "0000000000010" + hCharNote = "0000000000011" + hWorldNote = "0000000000012" + + theProject.tree[hPlotNote].setImport(C.iMajor) + theProject.tree[hCharNote].setImport(C.iMajor) + theProject.tree[hWorldNote].setImport(C.iMain) + + # Create Dialog + projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_STATUS) + projSettings.show() + qtbot.addWidget(projSettings) # Status Tab # ========== - projEdit._tabBox.setCurrentWidget(projEdit.tabStatus) + tabStatus = projSettings.tabStatus - assert projEdit.tabStatus.colChanged is False - assert projEdit.tabStatus.getNewList() == ([], []) - assert projEdit.tabStatus.listBox.topLevelItemCount() == 4 + assert tabStatus.colChanged is False + assert tabStatus.getNewList() == ([], []) + assert tabStatus.listBox.topLevelItemCount() == 4 # Can't delete the first item (it's in use) - projEdit.tabStatus.listBox.clearSelection() - projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True) - qtbot.mouseClick(projEdit.tabStatus.delButton, Qt.LeftButton) - assert projEdit.tabStatus.listBox.topLevelItemCount() == 4 + tabStatus.listBox.clearSelection() + tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(0)) + qtbot.mouseClick(tabStatus.delButton, Qt.LeftButton) + assert tabStatus.listBox.topLevelItemCount() == 4 - # Can delete the third item - projEdit.tabStatus.listBox.clearSelection() - projEdit.tabStatus.listBox.topLevelItem(2).setSelected(True) - qtbot.mouseClick(projEdit.tabStatus.delButton, Qt.LeftButton) - assert projEdit.tabStatus.listBox.topLevelItemCount() == 3 + # Can delete the second item + tabStatus.listBox.clearSelection() + tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(1)) + qtbot.mouseClick(tabStatus.delButton, Qt.LeftButton) + assert tabStatus.listBox.topLevelItemCount() == 3 # Add a new item - monkeypatch.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40)) - qtbot.mouseClick(projEdit.tabStatus.addButton, Qt.LeftButton) - projEdit.tabStatus.listBox.topLevelItem(3).setSelected(True) - for n in range(8): - qtbot.keyClick(projEdit.tabStatus.editName, Qt.Key_Backspace, delay=typeDelay) - for c in "Final": - qtbot.keyClick(projEdit.tabStatus.editName, c, delay=typeDelay) - qtbot.mouseClick(projEdit.tabStatus.colButton, Qt.LeftButton) - qtbot.mouseClick(projEdit.tabStatus.saveButton, Qt.LeftButton) - assert projEdit.tabStatus.listBox.topLevelItemCount() == 4 - qtbot.wait(stepDelay) + with monkeypatch.context() as mp: + mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40)) + qtbot.mouseClick(tabStatus.addButton, Qt.LeftButton) + tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(3)) + for _ in range(8): + qtbot.keyClick(tabStatus.editName, Qt.Key_Backspace, delay=typeDelay) + for c in "Final": + qtbot.keyClick(tabStatus.editName, c, delay=typeDelay) + qtbot.mouseClick(tabStatus.colButton, Qt.LeftButton) + qtbot.mouseClick(tabStatus.saveButton, Qt.LeftButton) + assert tabStatus.listBox.topLevelItemCount() == 4 - assert projEdit.tabStatus.colChanged is True - assert projEdit.tabStatus.getNewList() == ( + assert tabStatus.colChanged is True + assert tabStatus.getNewList() == ( [ { - "key": statusKeys[0], + "key": C.sNew, "name": "New", "cols": (100, 100, 100) }, { - "key": statusKeys[1], - "name": "Note", - "cols": (200, 50, 0) + "key": C.sDraft, + "name": "Draft", + "cols": (200, 150, 0) }, { - "key": statusKeys[3], + "key": C.sFinished, "name": "Finished", "cols": (50, 200, 0) }, { @@ -162,121 +258,201 @@ def testDlgProjSettings_Dialog( "cols": (20, 30, 40) } ], [ - statusKeys[2] # Deleted item + C.sNote # Deleted item ] ) - # Move items - projEdit.tabStatus.listBox.clearSelection() - projEdit.tabStatus._moveItem(1) - assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [ - statusKeys[0], statusKeys[1], statusKeys[3], None + # Move items, none selected -> no change + tabStatus.listBox.clearSelection() + tabStatus._moveItem(1) + assert [x["key"] for x in tabStatus.getNewList()[0]] == [ + C.sNew, C.sDraft, C.sFinished, None ] - projEdit.tabStatus.listBox.clearSelection() - projEdit.tabStatus.listBox.topLevelItem(0).setSelected(True) - projEdit.tabStatus._moveItem(-1) - assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [ - statusKeys[0], statusKeys[1], statusKeys[3], None + # Move items, first selected, move up -> no change + tabStatus.listBox.clearSelection() + tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(0)) + tabStatus._moveItem(-1) + assert [x["key"] for x in tabStatus.getNewList()[0]] == [ + C.sNew, C.sDraft, C.sFinished, None ] - projEdit.tabStatus.listBox.clearSelection() - projEdit.tabStatus.listBox.topLevelItem(3).setSelected(True) - projEdit.tabStatus._moveItem(-1) - assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [ - statusKeys[0], statusKeys[1], None, statusKeys[3] + # Move items, last selected, move up -> allowed + tabStatus.listBox.clearSelection() + tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(3)) + tabStatus._moveItem(-1) + assert [x["key"] for x in tabStatus.getNewList()[0]] == [ + C.sNew, C.sDraft, None, C.sFinished ] - projEdit.tabStatus._moveItem(1) - assert [x["key"] for x in projEdit.tabStatus.getNewList()[0]] == [ - statusKeys[0], statusKeys[1], statusKeys[3], None + + # Move items, same selected, move down -> allowed + tabStatus._moveItem(1) + assert [x["key"] for x in tabStatus.getNewList()[0]] == [ + C.sNew, C.sDraft, C.sFinished, None ] # Importance Tab # ============== - projEdit._tabBox.setCurrentWidget(projEdit.tabImport) - projEdit.tabStatus.listBox.clearSelection() - projEdit.tabImport.listBox.topLevelItem(3).setSelected(True) - qtbot.mouseClick(projEdit.tabImport.delButton, Qt.LeftButton) - qtbot.mouseClick(projEdit.tabImport.addButton, Qt.LeftButton) - projEdit.tabStatus.listBox.clearSelection() - projEdit.tabImport.listBox.topLevelItem(3).setSelected(True) - for n in range(8): - qtbot.keyClick(projEdit.tabImport.editName, Qt.Key_Backspace, delay=typeDelay) - for c in "Final": - qtbot.keyClick(projEdit.tabImport.editName, c, delay=typeDelay) - qtbot.mouseClick(projEdit.tabImport.saveButton, Qt.LeftButton) - qtbot.wait(stepDelay) + tabImport = projSettings.tabImport + projSettings._focusTab(GuiProjectSettings.TAB_IMPORT) + + # Delete unused entry + tabImport.listBox.clearSelection() + tabImport.listBox.setCurrentItem(tabImport.listBox.topLevelItem(1)) + qtbot.mouseClick(tabImport.delButton, Qt.LeftButton) + assert tabImport.listBox.topLevelItemCount() == 3 + + # Add a new entry + with monkeypatch.context() as mp: + mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40)) + qtbot.mouseClick(tabImport.addButton, Qt.LeftButton) + tabImport.listBox.clearSelection() + tabImport.listBox.setCurrentItem(tabImport.listBox.topLevelItem(3)) + for _ in range(8): + qtbot.keyClick(tabImport.editName, Qt.Key_Backspace, delay=typeDelay) + for c in "Final": + qtbot.keyClick(tabImport.editName, c, delay=typeDelay) + qtbot.mouseClick(tabImport.colButton, Qt.LeftButton) + qtbot.mouseClick(tabImport.saveButton, Qt.LeftButton) + assert tabImport.listBox.topLevelItemCount() == 4 + + assert tabImport.colChanged is True + assert tabImport.getNewList() == ( + [ + { + "key": C.iNew, + "name": "New", + "cols": (100, 100, 100) + }, { + "key": C.iMajor, + "name": "Major", + "cols": (200, 150, 0) + }, { + "key": C.iMain, + "name": "Main", + "cols": (50, 200, 0) + }, { + "key": None, + "name": "Final", + "cols": (20, 30, 40) + } + ], [ + C.iMinor # Deleted item + ] + ) + + # Check Project + projSettings._doSave() + + statusItems = dict(theProject.statusItems.items()) + assert statusItems[C.sNew]["name"] == "New" + assert statusItems[C.sDraft]["name"] == "Draft" + assert statusItems[C.sFinished]["name"] == "Finished" + assert statusItems["s000013"]["name"] == "Final" + + importItems = dict(theProject.importItems.items()) + assert importItems[C.iNew]["name"] == "New" + assert importItems[C.iMajor]["name"] == "Major" + assert importItems[C.iMain]["name"] == "Main" + assert importItems["i000014"]["name"] == "Final" + + # Clean up + # qtbot.stop() + projSettings._doClose() + +# END Test testDlgProjSettings_StatusImport + + +@pytest.mark.gui +def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): + """Test the auto-replace tab of the project settings dialog. + """ + # Block message box + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) + + # Mock components + monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")]) + + # Create new project + mockRnd.reset() + buildTestProject(nwGUI, fncProj) + nwGUI.mainConf.backupPath = fncDir + + # Set some values + theProject = nwGUI.theProject + theProject.autoReplace = { + "A": "B", "C": "D" + } + + # Create Dialog + projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_REPLACE) + projSettings.show() + qtbot.addWidget(projSettings) # Auto-Replace Tab # ================ - qtbot.wait(stepDelay) - projEdit._tabBox.setCurrentWidget(projEdit.tabReplace) + tabReplace = projSettings.tabReplace - assert projEdit.tabReplace.listBox.topLevelItem(0).text(0) == "" - assert projEdit.tabReplace.listBox.topLevelItem(0).text(1) == "B" - assert projEdit.tabReplace.listBox.topLevelItem(1).text(0) == "" - assert projEdit.tabReplace.listBox.topLevelItem(1).text(1) == "D" + assert tabReplace.listBox.topLevelItem(0).text(0) == "" + assert tabReplace.listBox.topLevelItem(0).text(1) == "B" + assert tabReplace.listBox.topLevelItem(1).text(0) == "" + assert tabReplace.listBox.topLevelItem(1).text(1) == "D" + assert tabReplace.listBox.topLevelItemCount() == 2 - qtbot.mouseClick(projEdit.tabReplace.addButton, Qt.LeftButton) - projEdit.tabReplace.listBox.topLevelItem(2).setSelected(True) - projEdit.tabReplace.editKey.setText("") + # Nothing to save or delete + tabReplace.listBox.clearSelection() + assert tabReplace._saveEntry() is False + assert tabReplace._delEntry() is False + assert tabReplace.listBox.topLevelItemCount() == 2 + + # Create a new entry + qtbot.mouseClick(tabReplace.addButton, Qt.LeftButton) + assert tabReplace.listBox.topLevelItemCount() == 3 + assert tabReplace.listBox.topLevelItem(2).text(0) == "" + assert tabReplace.listBox.topLevelItem(2).text(1) == "" + + # Edit the entry + tabReplace.listBox.setCurrentItem(tabReplace.listBox.topLevelItem(2)) + tabReplace.editKey.setText("") for c in "Th is ": - qtbot.keyClick(projEdit.tabReplace.editKey, c, delay=typeDelay) - projEdit.tabReplace.editValue.setText("") + qtbot.keyClick(tabReplace.editKey, c, delay=typeDelay) + tabReplace.editValue.setText("") for c in "With This Stuff ": - qtbot.keyClick(projEdit.tabReplace.editValue, c, delay=typeDelay) - qtbot.mouseClick(projEdit.tabReplace.saveButton, Qt.LeftButton) + qtbot.keyClick(tabReplace.editValue, c, delay=typeDelay) + qtbot.mouseClick(tabReplace.saveButton, Qt.LeftButton) + assert tabReplace.listBox.topLevelItem(2).text(0) == "" + assert tabReplace.listBox.topLevelItem(2).text(1) == "With This Stuff " - qtbot.wait(stepDelay) - projEdit.tabReplace.listBox.clearSelection() - assert not projEdit.tabReplace._saveEntry() - assert not projEdit.tabReplace._delEntry() - qtbot.mouseClick(projEdit.tabReplace.addButton, Qt.LeftButton) + # Create a new entry again + tabReplace.listBox.clearSelection() + qtbot.mouseClick(tabReplace.addButton, Qt.LeftButton) + assert tabReplace.listBox.topLevelItemCount() == 4 + # The list is sorted, so we must find it newIdx = -1 - for i in range(projEdit.tabReplace.listBox.topLevelItemCount()): - if projEdit.tabReplace.listBox.topLevelItem(i).text(0) == "": + for i in range(tabReplace.listBox.topLevelItemCount()): + if tabReplace.listBox.topLevelItem(i).text(0) == "": newIdx = i break - assert newIdx >= 0 - newItem = projEdit.tabReplace.listBox.topLevelItem(newIdx) - projEdit.tabReplace.listBox.setCurrentItem(newItem) - qtbot.mouseClick(projEdit.tabReplace.delButton, Qt.LeftButton) - qtbot.wait(stepDelay) - # Save & Check - # ============ + # Then delete the new item + tabReplace.listBox.setCurrentItem(tabReplace.listBox.topLevelItem(newIdx)) + qtbot.mouseClick(tabReplace.delButton, Qt.LeftButton) + assert tabReplace.listBox.topLevelItemCount() == 3 - projEdit._doSave() + # Check Project + projSettings._doSave() + assert theProject.autoReplace == { + "A": "B", "C": "D", "This": "With This Stuff" + } - # Open again, and check project settings - nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) - qtbot.waitUntil(lambda: getGuiItem("GuiProjectSettings") is not None, timeout=1000) + # Clean up + # qtbot.stop() + projSettings._doClose() - projEdit = getGuiItem("GuiProjectSettings") - assert isinstance(projEdit, GuiProjectSettings) - - qtbot.addWidget(projEdit) - assert projEdit.tabMain.editName.text() == "Project Name" - assert projEdit.tabMain.editTitle.text() == "Project Title" - theAuth = projEdit.tabMain.editAuthors.toPlainText().strip().splitlines() - assert len(theAuth) == 2 - assert theAuth[0] == "Jane Doe" - assert theAuth[1] == "John Doh" - - projEdit._doClose() - qtbot.wait(stepDelay) - - assert nwGUI.saveProject() - qtbot.wait(stepDelay) - - # Check the files - copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile, [2, 8, 9, 10]) - - # qtbot.stopForInteraction() - -# END Test testDlgProjSettings_Dialog +# END Test testDlgProjSettings_Replace diff --git a/tests/tools.py b/tests/tools.py index 4744d472..4a97c930 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -30,6 +30,17 @@ XML_IGNORE = (" Date: Tue, 25 Oct 2022 16:19:25 +0200 Subject: [PATCH 5/5] Add checkmark for current active status or iumportance value (#1202) --- novelwriter/gui/projtree.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index bd77a57a..e34419d1 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -42,7 +42,7 @@ from PyQt5.QtWidgets import ( from novelwriter.core import DocMerger, DocSplitter from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.dialogs import GuiDocMerge, GuiDocSplit, GuiEditLabel, GuiProjectSettings -from novelwriter.constants import nwHeaders, trConst, nwLabels +from novelwriter.constants import nwHeaders, nwUnicode, trConst, nwLabels logger = logging.getLogger(__name__) @@ -1121,10 +1121,12 @@ class GuiProjectTree(QTreeWidget): open a context menu in-place. """ tItem = None + hasChild = False selItem = self.itemAt(clickPos) if isinstance(selItem, QTreeWidgetItem): tHandle = selItem.data(self.C_NAME, Qt.UserRole) tItem = self.theProject.tree[tHandle] + hasChild = selItem.childCount() > 0 if tItem is None: logger.debug("No item found") @@ -1149,7 +1151,6 @@ class GuiProjectTree(QTreeWidget): isRoot = tItem.isRootType() isFolder = tItem.isFolderType() isFile = tItem.isFileType() - hasChild = selItem.childCount() > 0 if isFile: aOpenDoc = ctxMenu.addAction(self.tr("Open Document")) @@ -1172,10 +1173,12 @@ class GuiProjectTree(QTreeWidget): aActive = ctxMenu.addAction(self.tr("Toggle Active")) aActive.triggered.connect(lambda: self._toggleItemActive(tHandle)) + checkMark = f" ({nwUnicode.U_CHECK})" if tItem.isNovelLike(): mStatus = ctxMenu.addMenu(self.tr("Set Status to ...")) for n, (key, entry) in enumerate(self.theProject.statusItems.items()): - aStatus = mStatus.addAction(entry["icon"], entry["name"]) + entryName = entry["name"] + (checkMark if tItem.itemStatus == key else "") + aStatus = mStatus.addAction(entry["icon"], entryName) aStatus.triggered.connect( lambda n, key=key: self._changeItemStatus(tHandle, key) ) @@ -1187,7 +1190,8 @@ class GuiProjectTree(QTreeWidget): else: mImport = ctxMenu.addMenu(self.tr("Set Importance to ...")) for n, (key, entry) in enumerate(self.theProject.importItems.items()): - aImport = mImport.addAction(entry["icon"], entry["name"]) + entryName = entry["name"] + (checkMark if tItem.itemImport == key else "") + aImport = mImport.addAction(entry["icon"], entryName) aImport.triggered.connect( lambda n, key=key: self._changeItemImport(tHandle, key) )