diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index e6acd81d3..fd331c6a8 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -537,7 +537,7 @@ def revealNewTreeItem(self, tHandle, nHandle=None): if pHandle is not None and pHandle in self._treeMap: self._treeMap[pHandle].setExpanded(True) - self._alertTreeChange(tHandle=tHandle, flush=True) + self._alertTreeChange(tHandle, flush=True) self.clearSelection() trItem.setSelected(True) @@ -577,7 +577,7 @@ def moveTreeItem(self, nStep): pItem.insertChild(nIndex, cItem) self._recordLastMove(cItem, pItem, tIndex) - self._alertTreeChange(tHandle=tHandle, flush=True) + self._alertTreeChange(tHandle, flush=True) self.clearSelection() trItem.setSelected(True) trItem.setExpanded(isExp) @@ -595,7 +595,7 @@ def renameTreeItem(self, tHandle): if dlgOk: tItem.setName(newLabel) self.setTreeItemValues(tHandle) - self._alertTreeChange(tHandle=tHandle, flush=False) + self._alertTreeChange(tHandle, flush=False) return @@ -665,7 +665,7 @@ def emptyTrash(self): self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True) if nTrash > 0: - self._alertTreeChange(tHandle=trashHandle, flush=True) + self._alertTreeChange(trashHandle, flush=True) return True @@ -707,7 +707,7 @@ def deleteItem(self, tHandle=None, alreadyAsked=False, bulkAction=False): if trItemS.childCount() == 0: self.takeTopLevelItem(tIndex) self._deleteTreeItem(tHandle) - self._alertTreeChange(tHandle=tHandle, flush=True) + self._alertTreeChange(tHandle, flush=True) else: self.mainGui.makeAlert(self.tr( "Cannot delete root folder. It is not empty. " @@ -723,7 +723,7 @@ def deleteItem(self, tHandle=None, alreadyAsked=False, bulkAction=False): tIndex = trItemP.indexOfChild(trItemS) trItemP.takeChild(tIndex) self._deleteTreeItem(tHandle) - self._alertTreeChange(tHandle=tHandle, flush=autoFlush) + self._alertTreeChange(tHandle, flush=autoFlush) else: # A populated FOLDER or a FILE requires confirmtation @@ -759,7 +759,7 @@ def deleteItem(self, tHandle=None, alreadyAsked=False, bulkAction=False): self.mainGui.closeDocument() self._deleteTreeItem(dHandle) - self._alertTreeChange(tHandle=tHandle, flush=autoFlush) + self._alertTreeChange(tHandle, flush=autoFlush) self.projView.wordCountsChanged.emit() else: @@ -778,7 +778,7 @@ def deleteItem(self, tHandle=None, alreadyAsked=False, bulkAction=False): trItemT.addChild(trItemC) self._postItemMove(tHandle, wCount) self._recordLastMove(trItemS, trItemP, tIndex) - self._alertTreeChange(tHandle=tHandle, flush=autoFlush) + self._alertTreeChange(tHandle, flush=autoFlush) return True @@ -904,7 +904,7 @@ def undoLastMove(self): dstItem.insertChild(dstIndex, movItem) self._postItemMove(sHandle, wCount) - self._alertTreeChange(tHandle=sHandle, flush=True) + self._alertTreeChange(sHandle, flush=True) self.clearSelection() movItem.setSelected(True) @@ -1077,6 +1077,20 @@ def _openContextMenu(self, clickPos): ), lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE) ) + elif isFolder: + if tItem.documentAllowed(): + ctxMenu.addAction( + self.tr("Convert to {0}").format( + trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT]) + ), + lambda: self._covertFolderToFile(tHandle, nwItemLayout.DOCUMENT) + ) + ctxMenu.addAction( + self.tr("Convert to {0}").format( + trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE]) + ), + lambda: self._covertFolderToFile(tHandle, nwItemLayout.NOTE) + ) ctxMenu.addSeparator() @@ -1154,7 +1168,7 @@ def dropEvent(self, theEvent): QTreeWidget.dropEvent(self, theEvent) self._postItemMove(sHandle, wCount) self._recordLastMove(sItem, pItem, pIndex) - self._alertTreeChange(tHandle=sHandle, flush=True) + self._alertTreeChange(sHandle, flush=True) sItem.setExpanded(isExpanded) return @@ -1236,6 +1250,7 @@ def _toggleItemExported(self, tHandle): if tItem is not None: tItem.setExported(not tItem.isExported) self.setTreeItemValues(tItem.itemHandle) + self._alertTreeChange(tHandle, flush=False) return def _changeItemStatus(self, tHandle, tStatus): @@ -1245,6 +1260,7 @@ def _changeItemStatus(self, tHandle, tStatus): if tItem is not None: tItem.setStatus(tStatus) self.setTreeItemValues(tItem.itemHandle) + self._alertTreeChange(tHandle, flush=False) return def _changeItemImport(self, tHandle, tImport): @@ -1254,6 +1270,7 @@ def _changeItemImport(self, tHandle, tImport): if tItem is not None: tItem.setImport(tImport) self.setTreeItemValues(tItem.itemHandle) + self._alertTreeChange(tHandle, flush=False) return def _changeItemLayout(self, tHandle, itemLayout): @@ -1263,10 +1280,38 @@ def _changeItemLayout(self, tHandle, itemLayout): if tItem is not None: if itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed(): tItem.setLayout(nwItemLayout.DOCUMENT) - self.setTreeItemValues(tItem.itemHandle) + self.setTreeItemValues(tHandle) + self._alertTreeChange(tHandle, flush=False) elif itemLayout == nwItemLayout.NOTE: tItem.setLayout(nwItemLayout.NOTE) - self.setTreeItemValues(tItem.itemHandle) + self.setTreeItemValues(tHandle) + self._alertTreeChange(tHandle, flush=False) + return + + def _covertFolderToFile(self, tHandle, itemLayout): + """Convert a folder to a note or document. + """ + tItem = self.theProject.tree[tHandle] + if tItem is not None and tItem.isFolderType(): + msgYes = self.mainGui.askQuestion( + self.tr("Convert Folder"), + self.tr( + "Do you want to convert the folder to a {0}? " + "This action cannot be reversed." + ).format(trConst(nwLabels.LAYOUT_NAME[itemLayout])) + ) + if msgYes and itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed(): + tItem.setType(nwItemType.FILE) + tItem.setLayout(nwItemLayout.DOCUMENT) + self.setTreeItemValues(tHandle) + self._alertTreeChange(tHandle, flush=False) + elif msgYes and itemLayout == nwItemLayout.NOTE: + tItem.setType(nwItemType.FILE) + tItem.setLayout(nwItemLayout.NOTE) + self.setTreeItemValues(tHandle) + self._alertTreeChange(tHandle, flush=False) + else: + logger.info("Folder conversion cancelled") return def _scanChildren(self, theList, tItem, tIndex): @@ -1351,25 +1396,28 @@ def _addTrashRoot(self): trItem = self._addTreeItem(self.theProject.tree[trashHandle]) if trItem is not None: trItem.setExpanded(True) - self._alertTreeChange(tHandle=trashHandle, flush=True) + self._alertTreeChange(trashHandle, flush=True) return trItem - def _alertTreeChange(self, tHandle=None, flush=True): + def _alertTreeChange(self, tHandle, flush=False): """Update information on tree change state, and emit necessary - signals. + signals. A flush is only needed if an item is moved, created or + deleted. """ self._timeChanged = time() self.theProject.setProjectChanged(True) if flush: self.saveTreeOrder() + if tHandle is None: + return + tItem = self.theProject.tree[tHandle] if tItem is None: return - itemType = tItem.itemType - if itemType == nwItemType.ROOT: + if tItem.isRootType(): self.projView.rootFolderChanged.emit(tHandle) self.projView.treeItemChanged.emit(tHandle) diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index feef859d3..4e381614e 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -458,7 +458,7 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR @pytest.mark.gui -def testGuiProjTree_ContextMenu(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd): +def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): """Test the building of the project tree context menu. All this does is test that the menu builds. It doesn't open the actual menu, """ @@ -470,6 +470,8 @@ def testGuiProjTree_ContextMenu(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) monkeypatch.setattr(QMenu, "exec_", lambda *a: None) + nwTree = nwGUI.projView + # Create a project prjDir = os.path.join(fncDir, "project") buildTestProject(nwGUI, prjDir) @@ -544,6 +546,38 @@ def itemPos(tHandle): projTree._changeItemLayout(hNovelNote, nwItemLayout.NOTE) assert nwItem.itemLayout == nwItemLayout.NOTE + # Convert Folders to Documents + # ============================ + + hNewFolderOne = "0000000000013" + hNewFolderTwo = "0000000000015" + + nwTree.setSelectedHandle(hNovelNote) + assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True + nwTree.setSelectedHandle(hNewFolderOne) + assert nwTree.projTree.newTreeItem(nwItemType.FILE) is True + + nwTree.setSelectedHandle(hNovelNote) + assert nwTree.projTree.newTreeItem(nwItemType.FOLDER) is True + nwTree.setSelectedHandle(hNewFolderTwo) + assert nwTree.projTree.newTreeItem(nwItemType.FILE, isNote=True) is True + + # Click no on the dialog + with monkeypatch.context() as mp: + mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) + projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT) + assert nwGUI.theProject.tree[hNewFolderOne].isFolderType() + + # Convert the first folder to a document + projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT) + assert nwGUI.theProject.tree[hNewFolderOne].isFileType() + assert nwGUI.theProject.tree[hNewFolderOne].isDocumentLayout() + + # Convert the second folder to a note + projTree._covertFolderToFile(hNewFolderTwo, nwItemLayout.NOTE) + assert nwGUI.theProject.tree[hNewFolderTwo].isFileType() + assert nwGUI.theProject.tree[hNewFolderTwo].isNoteLayout() + # qtbot.stop() # END Test testGuiProjTree_ContextMenu