Improve how items are added to project tree, allowing root folders to be added next to specified items
This commit is contained in:
@@ -81,6 +81,7 @@ class NWItem:
|
||||
return f"<NWItem handle={self._handle}, parent={self._parent}, name='{self._name}'>"
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""Evaluate to False if itemHandle is not set."""
|
||||
return self._handle is not None
|
||||
|
||||
def __copy__(self) -> NWItem:
|
||||
|
||||
+44
-46
@@ -658,11 +658,12 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
return True
|
||||
|
||||
def revealNewTreeItem(self, tHandle, nHandle=None, wordCount=False):
|
||||
"""Reveal a newly added project item in the project tree.
|
||||
"""
|
||||
def revealNewTreeItem(
|
||||
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]
|
||||
if nwItem is None:
|
||||
if not nwItem:
|
||||
return False
|
||||
|
||||
trItem = self._addTreeItem(nwItem, nHandle)
|
||||
@@ -683,9 +684,8 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
return True
|
||||
|
||||
def moveTreeItem(self, nStep):
|
||||
"""Move an item up or down in the tree.
|
||||
"""
|
||||
def moveTreeItem(self, nStep: int) -> bool:
|
||||
"""Move an item up or down in the tree."""
|
||||
tHandle = self.getSelectedHandle()
|
||||
trItem = self._getTreeItem(tHandle)
|
||||
if trItem is None:
|
||||
@@ -723,9 +723,8 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
return True
|
||||
|
||||
def renameTreeItem(self, tHandle):
|
||||
"""Open a dialog to edit the label of an item.
|
||||
"""
|
||||
def renameTreeItem(self, tHandle: str) -> bool:
|
||||
"""Open a dialog to edit the label of an item."""
|
||||
tItem = self.theProject.tree[tHandle]
|
||||
if tItem is None:
|
||||
return False
|
||||
@@ -738,7 +737,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
return True
|
||||
|
||||
def saveTreeOrder(self):
|
||||
def saveTreeOrder(self) -> None:
|
||||
"""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
|
||||
project structure, and must be called before any code that
|
||||
@@ -746,12 +745,14 @@ class GuiProjectTree(QTreeWidget):
|
||||
"""
|
||||
theList = []
|
||||
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")
|
||||
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
|
||||
item handle.
|
||||
"""
|
||||
@@ -761,7 +762,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
theList = self._scanChildren(theList, theItem, 0)
|
||||
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
|
||||
can be called on any item, and will check whether to attempt a
|
||||
permanent deletion or moving the item to Trash.
|
||||
@@ -998,7 +999,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
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,
|
||||
and propagating that count upwards in the tree until reaching a
|
||||
root item. This function is more efficient than recalculating
|
||||
@@ -1037,7 +1038,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def buildTree(self):
|
||||
def buildTree(self) -> None:
|
||||
"""Build the entire project tree from scratch. This depends on
|
||||
the save project item iterator in the project class which will
|
||||
always make sure items with a parent have had their parent item
|
||||
@@ -1052,11 +1053,10 @@ class GuiProjectTree(QTreeWidget):
|
||||
self._addTreeItem(nwItem)
|
||||
|
||||
logger.debug("%d item(s) added to the project tree", iCount)
|
||||
return True
|
||||
return
|
||||
|
||||
def undoLastMove(self):
|
||||
"""Attempt to undo the last action.
|
||||
"""
|
||||
"""Attempt to undo the last action."""
|
||||
srcItem = self._lastMove.get("item", None)
|
||||
dstItem = self._lastMove.get("parent", None)
|
||||
dstIndex = self._lastMove.get("index", None)
|
||||
@@ -1719,10 +1719,17 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
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
|
||||
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
|
||||
pHandle = nwItem.itemParent
|
||||
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_WORDS, 0)
|
||||
|
||||
self._treeMap[tHandle] = newItem
|
||||
if pHandle is None:
|
||||
if nwItem.isRootType():
|
||||
newItem.setFlags(newItem.flags() ^ Qt.ItemIsDragEnabled)
|
||||
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)
|
||||
|
||||
if pHandle is None and nwItem.isRootType():
|
||||
pItem = self.invisibleRootItem()
|
||||
elif pHandle and pHandle in self._treeMap:
|
||||
pItem = self._treeMap[pHandle]
|
||||
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
|
||||
|
||||
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)
|
||||
newItem.setExpanded(nwItem.isExpanded)
|
||||
|
||||
@@ -1808,7 +1806,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
return
|
||||
|
||||
tItem = self.theProject.tree[tHandle]
|
||||
if tItem.isRootType():
|
||||
if tItem and tItem.isRootType():
|
||||
self.projView.rootFolderChanged.emit(tHandle)
|
||||
|
||||
self.projView.treeItemChanged.emit(tHandle)
|
||||
|
||||
@@ -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/>.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import pytest
|
||||
|
||||
from PyQt5.QtGui import QIcon
|
||||
@@ -32,8 +33,7 @@ from novelwriter.core.project import NWProject
|
||||
|
||||
@pytest.mark.core
|
||||
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)
|
||||
mockRnd.reset()
|
||||
buildTestProject(theProject, fncPath)
|
||||
@@ -193,8 +193,7 @@ def testCoreItem_Setters(mockGUI, mockRnd, fncPath):
|
||||
|
||||
@pytest.mark.core
|
||||
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)
|
||||
mockRnd.reset()
|
||||
buildTestProject(theProject, fncPath)
|
||||
@@ -278,9 +277,75 @@ def testCoreItem_Methods(mockGUI, mockRnd, fncPath):
|
||||
# Truthiness
|
||||
# ==========
|
||||
|
||||
assert bool(theItem) is True
|
||||
theItem.setHandle(None)
|
||||
assert bool(theItem) is False
|
||||
bItem = NWItem(theProject)
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
@@ -37,8 +37,7 @@ from novelwriter.dialogs.editlabel import GuiEditLabel
|
||||
|
||||
@pytest.mark.gui
|
||||
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))
|
||||
|
||||
projView = nwGUI.projView
|
||||
@@ -159,6 +158,9 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn
|
||||
nHandle = theProject.newFile("Test", None)
|
||||
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
|
||||
# qtbot.stop()
|
||||
nwGUI.closeProject()
|
||||
|
||||
Reference in New Issue
Block a user