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}'>"
|
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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user