Improve how items are added to project tree, allowing root folders to be added next to specified items

This commit is contained in:
Veronica Berglyd Olsen
2023-07-20 19:51:01 +02:00
parent 1559e3299a
commit 7a72634448
4 changed files with 121 additions and 55 deletions
+1
View File
@@ -81,6 +81,7 @@ class NWItem:
return f"<NWItem handle={self._handle}, parent={self._parent}, name='{self._name}'>" return f"<NWItem handle={self._handle}, parent={self._parent}, name='{self._name}'>"
def __bool__(self) -> bool: def __bool__(self) -> bool:
"""Evaluate to False if itemHandle is not set."""
return self._handle is not None return self._handle is not None
def __copy__(self) -> NWItem: def __copy__(self) -> NWItem:
+44 -46
View File
@@ -658,11 +658,12 @@ class GuiProjectTree(QTreeWidget):
return True return True
def revealNewTreeItem(self, tHandle, nHandle=None, wordCount=False): def revealNewTreeItem(
"""Reveal a newly added project item in the project tree. self, tHandle: str, nHandle: str | None = None, wordCount: bool = False
""" ) -> bool:
"""Reveal a newly added project item in the project tree."""
nwItem = self.theProject.tree[tHandle] nwItem = self.theProject.tree[tHandle]
if nwItem is None: if not nwItem:
return False return False
trItem = self._addTreeItem(nwItem, nHandle) trItem = self._addTreeItem(nwItem, nHandle)
@@ -683,9 +684,8 @@ class GuiProjectTree(QTreeWidget):
return True return True
def moveTreeItem(self, nStep): def moveTreeItem(self, nStep: int) -> bool:
"""Move an item up or down in the tree. """Move an item up or down in the tree."""
"""
tHandle = self.getSelectedHandle() tHandle = self.getSelectedHandle()
trItem = self._getTreeItem(tHandle) trItem = self._getTreeItem(tHandle)
if trItem is None: if trItem is None:
@@ -723,9 +723,8 @@ class GuiProjectTree(QTreeWidget):
return True return True
def renameTreeItem(self, tHandle): def renameTreeItem(self, tHandle: str) -> bool:
"""Open a dialog to edit the label of an item. """Open a dialog to edit the label of an item."""
"""
tItem = self.theProject.tree[tHandle] tItem = self.theProject.tree[tHandle]
if tItem is None: if tItem is None:
return False return False
@@ -738,7 +737,7 @@ class GuiProjectTree(QTreeWidget):
return True return True
def saveTreeOrder(self): def saveTreeOrder(self) -> None:
"""Build a list of the items in the project tree and send them """Build a list of the items in the project tree and send them
to the project class. This syncs up the two versions of the to the project class. This syncs up the two versions of the
project structure, and must be called before any code that project structure, and must be called before any code that
@@ -746,12 +745,14 @@ class GuiProjectTree(QTreeWidget):
""" """
theList = [] theList = []
for i in range(self.topLevelItemCount()): for i in range(self.topLevelItemCount()):
theList = self._scanChildren(theList, self.topLevelItem(i), i) item = self.topLevelItem(i)
if isinstance(item, QTreeWidgetItem):
theList = self._scanChildren(theList, item, i)
logger.debug("Saving project tree item order") logger.debug("Saving project tree item order")
self.theProject.setTreeOrder(theList) self.theProject.setTreeOrder(theList)
return True return
def getTreeFromHandle(self, tHandle): def getTreeFromHandle(self, tHandle: str) -> list[str]:
"""Recursively return all the child items starting from a given """Recursively return all the child items starting from a given
item handle. item handle.
""" """
@@ -761,7 +762,7 @@ class GuiProjectTree(QTreeWidget):
theList = self._scanChildren(theList, theItem, 0) theList = self._scanChildren(theList, theItem, 0)
return theList return theList
def requestDeleteItem(self, tHandle=None): def requestDeleteItem(self, tHandle: str | None = None) -> bool:
"""Request an item deleted from the project tree. This function """Request an item deleted from the project tree. This function
can be called on any item, and will check whether to attempt a can be called on any item, and will check whether to attempt a
permanent deletion or moving the item to Trash. permanent deletion or moving the item to Trash.
@@ -998,7 +999,7 @@ class GuiProjectTree(QTreeWidget):
return return
def propagateCount(self, tHandle, newCount, countChildren=False): def propagateCount(self, tHandle: str, newCount: int, countChildren: bool = False) -> None:
"""Recursive function setting the word count for a given item, """Recursive function setting the word count for a given item,
and propagating that count upwards in the tree until reaching a and propagating that count upwards in the tree until reaching a
root item. This function is more efficient than recalculating root item. This function is more efficient than recalculating
@@ -1037,7 +1038,7 @@ class GuiProjectTree(QTreeWidget):
return return
def buildTree(self): def buildTree(self) -> None:
"""Build the entire project tree from scratch. This depends on """Build the entire project tree from scratch. This depends on
the save project item iterator in the project class which will the save project item iterator in the project class which will
always make sure items with a parent have had their parent item always make sure items with a parent have had their parent item
@@ -1052,11 +1053,10 @@ class GuiProjectTree(QTreeWidget):
self._addTreeItem(nwItem) self._addTreeItem(nwItem)
logger.debug("%d item(s) added to the project tree", iCount) logger.debug("%d item(s) added to the project tree", iCount)
return True return
def undoLastMove(self): def undoLastMove(self):
"""Attempt to undo the last action. """Attempt to undo the last action."""
"""
srcItem = self._lastMove.get("item", None) srcItem = self._lastMove.get("item", None)
dstItem = self._lastMove.get("parent", None) dstItem = self._lastMove.get("parent", None)
dstIndex = self._lastMove.get("index", None) dstIndex = self._lastMove.get("index", None)
@@ -1719,10 +1719,17 @@ class GuiProjectTree(QTreeWidget):
return itemList return itemList
def _addTreeItem(self, nwItem, nHandle=None): def _addTreeItem(
self, nwItem: NWItem | None, nHandle: str | None = None
) -> QTreeWidgetItem | None:
"""Create a QTreeWidgetItem from an NWItem and add it to the """Create a QTreeWidgetItem from an NWItem and add it to the
project tree. project tree. Returns the widget if the item is valid, otherwise
a None is returned.
""" """
if not nwItem:
logger.error("Invalid item cannot be added to project tree")
return None
tHandle = nwItem.itemHandle tHandle = nwItem.itemHandle
pHandle = nwItem.itemParent pHandle = nwItem.itemParent
newItem = QTreeWidgetItem() newItem = QTreeWidgetItem()
@@ -1740,35 +1747,26 @@ class GuiProjectTree(QTreeWidget):
newItem.setData(self.C_DATA, self.D_HANDLE, tHandle) newItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
newItem.setData(self.C_DATA, self.D_WORDS, 0) newItem.setData(self.C_DATA, self.D_WORDS, 0)
self._treeMap[tHandle] = newItem if pHandle is None and nwItem.isRootType():
if pHandle is None: pItem = self.invisibleRootItem()
if nwItem.isRootType(): elif pHandle and pHandle in self._treeMap:
newItem.setFlags(newItem.flags() ^ Qt.ItemIsDragEnabled) pItem = self._treeMap[pHandle]
self.addTopLevelItem(newItem)
else:
self.mainGui.makeAlert(self.tr(
"There is nowhere to add item with name '{0}'."
).format(nwItem.itemName), nwAlert.ERROR)
del self._treeMap[tHandle]
return None
elif pHandle in self._treeMap:
byIndex = -1
if nHandle is not None and nHandle in self._treeMap:
byIndex = self._treeMap[pHandle].indexOfChild(self._treeMap[nHandle])
if byIndex >= 0:
self._treeMap[pHandle].insertChild(byIndex + 1, newItem)
else:
self._treeMap[pHandle].addChild(newItem)
self.propagateCount(tHandle, nwItem.wordCount, countChildren=True)
else: else:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
"There is nowhere to add item with name '{0}'." "There is nowhere to add item with name '{0}'."
).format(nwItem.itemName), nwAlert.ERROR) ).format(nwItem.itemName), nwAlert.ERROR)
del self._treeMap[tHandle]
return None return None
byIndex = -1
if nHandle is not None and nHandle in self._treeMap:
byIndex = pItem.indexOfChild(self._treeMap[nHandle])
if byIndex >= 0:
pItem.insertChild(byIndex + 1, newItem)
else:
pItem.addChild(newItem)
self._treeMap[tHandle] = newItem
self.propagateCount(tHandle, nwItem.wordCount, countChildren=True)
self.setTreeItemValues(tHandle) self.setTreeItemValues(tHandle)
newItem.setExpanded(nwItem.isExpanded) newItem.setExpanded(nwItem.isExpanded)
@@ -1808,7 +1806,7 @@ class GuiProjectTree(QTreeWidget):
return return
tItem = self.theProject.tree[tHandle] tItem = self.theProject.tree[tHandle]
if tItem.isRootType(): if tItem and tItem.isRootType():
self.projView.rootFolderChanged.emit(tHandle) self.projView.rootFolderChanged.emit(tHandle)
self.projView.treeItemChanged.emit(tHandle) self.projView.treeItemChanged.emit(tHandle)
+72 -7
View File
@@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import copy
import pytest import pytest
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
@@ -32,8 +33,7 @@ from novelwriter.core.project import NWProject
@pytest.mark.core @pytest.mark.core
def testCoreItem_Setters(mockGUI, mockRnd, fncPath): def testCoreItem_Setters(mockGUI, mockRnd, fncPath):
"""Test all the simple setters for the NWItem class. """Test all the simple setters for the NWItem class."""
"""
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
@@ -193,8 +193,7 @@ def testCoreItem_Setters(mockGUI, mockRnd, fncPath):
@pytest.mark.core @pytest.mark.core
def testCoreItem_Methods(mockGUI, mockRnd, fncPath): def testCoreItem_Methods(mockGUI, mockRnd, fncPath):
"""Test the simple methods of the NWItem class. """Test the simple methods of the NWItem class."""
"""
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
@@ -278,9 +277,75 @@ def testCoreItem_Methods(mockGUI, mockRnd, fncPath):
# Truthiness # Truthiness
# ========== # ==========
assert bool(theItem) is True bItem = NWItem(theProject)
theItem.setHandle(None)
assert bool(theItem) is False # An item with a handle is valid
bItem.setHandle(theProject.tree._makeHandle())
assert bool(bItem) is True
assert bItem
# An item without a handle is invalid
bItem.setHandle(None)
assert bool(bItem) is False
assert not bItem
# Copy an Item
# ============
scData = {
"name": "New Scene",
"itemAttr": {
"handle": "000000000000f",
"parent": "000000000000d",
"root": "0000000000008",
"order": "0",
"type": "FILE",
"class": "NOVEL",
"layout": "DOCUMENT"
},
"metaAttr": {
"expanded": "no",
"heading": "H3",
"charCount": "9",
"wordCount": "2",
"paraCount": "0",
"cursorPos": "0"
},
"nameAttr": {
"status": "s000000",
"import": "i000004",
"active": "yes"
}
}
scItem = theProject.tree[C.hSceneDoc]
cpItem = copy.copy(scItem)
# We should have two instances of NWItem
assert isinstance(scItem, NWItem)
assert isinstance(cpItem, NWItem)
assert scItem is not cpItem
# They should both point to the same project instance
assert scItem._project is cpItem._project
# They should contain the same data
assert scItem.pack() == scData
assert cpItem.pack() == scData
# Create a new handle for the copy
cpHandle = theProject.tree._makeHandle()
cpData = copy.deepcopy(scData)
cpData["itemAttr"]["handle"] = cpHandle
# Check that it is indeed changed
cpItem.setHandle(cpHandle)
assert cpItem.pack() != scData
assert cpItem.pack() == cpData
# Delete the original, and check that the copy remains
del scItem
assert cpItem.pack() == cpData
# END Test testCoreItem_Methods # END Test testCoreItem_Methods
+4 -2
View File
@@ -37,8 +37,7 @@ from novelwriter.dialogs.editlabel import GuiEditLabel
@pytest.mark.gui @pytest.mark.gui
def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd): def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
"""Test adding and removing items from the project tree. """Test adding and removing items from the project tree."""
"""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
projView = nwGUI.projView projView = nwGUI.projView
@@ -159,6 +158,9 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn
nHandle = theProject.newFile("Test", None) nHandle = theProject.newFile("Test", None)
assert projView.projTree.revealNewTreeItem(nHandle) is False assert projView.projTree.revealNewTreeItem(nHandle) is False
# Adding an invalid item directly to the tree should also fail
assert projView.projTree._addTreeItem(None) is None
# Clean up # Clean up
# qtbot.stop() # qtbot.stop()
nwGUI.closeProject() nwGUI.closeProject()