Handle item consistency in the nodes tree

This commit is contained in:
Veronica Berglyd Olsen
2024-11-19 17:47:21 +01:00
parent 53e70c118c
commit 442b065834
3 changed files with 352 additions and 414 deletions
+31 -15
View File
@@ -45,7 +45,6 @@ COL_MASK = 0x0100
C_LABEL_TEXT = 0x0000 | Qt.ItemDataRole.DisplayRole C_LABEL_TEXT = 0x0000 | Qt.ItemDataRole.DisplayRole
C_LABEL_ICON = 0x0000 | Qt.ItemDataRole.DecorationRole C_LABEL_ICON = 0x0000 | Qt.ItemDataRole.DecorationRole
C_LABEL_TIP = 0x0000 | Qt.ItemDataRole.ToolTipRole
C_COUNT_TEXT = 0x0100 | Qt.ItemDataRole.DisplayRole C_COUNT_TEXT = 0x0100 | Qt.ItemDataRole.DisplayRole
C_COUNT_ICON = 0x0100 | Qt.ItemDataRole.DecorationRole C_COUNT_ICON = 0x0100 | Qt.ItemDataRole.DecorationRole
C_COUNT_ALIGN = 0x0100 | Qt.ItemDataRole.TextAlignmentRole C_COUNT_ALIGN = 0x0100 | Qt.ItemDataRole.TextAlignmentRole
@@ -95,34 +94,31 @@ class ProjectNode:
## ##
def refresh(self) -> None: def refresh(self) -> None:
cache: dict[int, str | QIcon | Qt.AlignmentFlag] = {} """Refresh data values."""
# Label # Label
cache[C_LABEL_ICON] = SHARED.theme.getItemIcon( self._cache[C_LABEL_ICON] = SHARED.theme.getItemIcon(
self._item.itemType, self._item.itemClass, self._item.itemType, self._item.itemClass,
self._item.itemLayout, self._item.mainHeading self._item.itemLayout, self._item.mainHeading
) )
cache[C_LABEL_TEXT] = self._item.itemName self._cache[C_LABEL_TEXT] = self._item.itemName
cache[C_LABEL_TIP] = self._item.itemName
# Count # Count
cache[C_COUNT_ALIGN] = QtAlignRight self._cache[C_COUNT_ALIGN] = QtAlignRight
# Active # Active
if self._item.isFileType(): if self._item.isFileType():
if self._item.isActive: if self._item.isActive:
cache[C_ACTIVE_ICON] = SHARED.theme.getIcon("checked") self._cache[C_ACTIVE_ICON] = SHARED.theme.getIcon("checked")
else: else:
cache[C_ACTIVE_ICON] = SHARED.theme.getIcon("unchecked") self._cache[C_ACTIVE_ICON] = SHARED.theme.getIcon("unchecked")
else: else:
cache[C_ACTIVE_ICON] = SHARED.theme.getIcon("noncheckable") self._cache[C_ACTIVE_ICON] = SHARED.theme.getIcon("noncheckable")
# Status # Status
sText, sIcon = self._item.getImportStatus() sText, sIcon = self._item.getImportStatus()
cache[C_STATUS_ICON] = sIcon self._cache[C_STATUS_ICON] = sIcon
cache[C_STATUS_TIP] = sText self._cache[C_STATUS_TIP] = sText
self._cache = cache
self.updateCount() self.updateCount()
return return
@@ -145,7 +141,7 @@ class ProjectNode:
return len(self._children) return len(self._children)
def data(self, column: int, role: Qt.ItemDataRole) -> T_NodeData: def data(self, column: int, role: Qt.ItemDataRole) -> T_NodeData:
"""""" """Return cached node data."""
return self._cache.get(COL_MASK*column | role) return self._cache.get(COL_MASK*column | role)
def parent(self) -> ProjectNode | None: def parent(self) -> ProjectNode | None:
@@ -157,6 +153,7 @@ class ProjectNode:
return None return None
def allChildren(self) -> list[ProjectNode]: def allChildren(self) -> list[ProjectNode]:
"""Return a recursive list of all children."""
nodes: list[ProjectNode] = [] nodes: list[ProjectNode] = []
self._recursiveAppendChildren(nodes) self._recursiveAppendChildren(nodes)
return nodes return nodes
@@ -168,15 +165,24 @@ class ProjectNode:
def addChild(self, child: ProjectNode, pos: int = -1) -> None: def addChild(self, child: ProjectNode, pos: int = -1) -> None:
"""Add a child item to this item.""" """Add a child item to this item."""
child._parent = self child._parent = self
self._updateRelationships(child)
if 0 <= pos < len(self._children): if 0 <= pos < len(self._children):
self._children.insert(pos, child) self._children.insert(pos, child)
self._refreshChildrenPos() self._refreshChildrenPos()
else: else:
child._row = len(self._children) child._row = len(self._children)
self._children.append(child) self._children.append(child)
self.refresh() self.updateCount()
return return
def takeChild(self, pos: int) -> ProjectNode | None:
"""Remove a child item and return it."""
if 0 <= pos < len(self._children):
node = self._children.pop()
self._refreshChildrenPos()
return node
return None
def moveChild(self, source: int, target: int) -> None: def moveChild(self, source: int, target: int) -> None:
"""Move a child internally.""" """Move a child internally."""
count = len(self._children) count = len(self._children)
@@ -203,6 +209,16 @@ class ProjectNode:
child._row = n child._row = n
return return
def _updateRelationships(self, child: ProjectNode) -> None:
"""Update a child item's relationships."""
if self._parent:
child.item.setParent(self.item.itemHandle)
child.item.setRoot(self.item.itemRoot)
else:
child.item.setParent(None)
child.item.setRoot(child.item.itemHandle)
return
class ProjectModel(QAbstractItemModel): class ProjectModel(QAbstractItemModel):
+7 -38
View File
@@ -87,10 +87,10 @@ class NWTree:
# Properties # Properties
## ##
# @property @property
# def trashRoot(self) -> str | None: def trash(self) -> ProjectNode | None:
# """Return the handle of the trash folder, or None.""" """Return trash node, if it exists."""
# return self._trash return self._getTrashNode()
@property @property
def model(self) -> ProjectModel: def model(self) -> ProjectModel:
@@ -158,7 +158,6 @@ class NWTree:
nwItem.setType(itemType) nwItem.setType(itemType)
nwItem.setClass(itemClass) nwItem.setClass(itemClass)
if self.add(nwItem, pos): if self.add(nwItem, pos):
self.updateItemData(tHandle)
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return tHandle return tHandle
return None return None
@@ -238,23 +237,15 @@ class NWTree:
mark recovered files. mark recovered files.
""" """
storage = self._project.storage storage = self._project.storage
files = set(storage.scanContent()) remains = set(storage.scanContent()).difference(set(self._nodes.keys()))
for tHandle in self._nodes: orphans = len(remains)
if self.updateItemData(tHandle):
logger.debug("Checking item '%s' ... OK", tHandle)
files.discard(tHandle) # Remove it from the record
else:
logger.error("Checking item '%s' ... ERROR", tHandle)
self.__delitem__(tHandle) # The file will be re-added as orphaned
orphans = len(files)
if orphans == 0: if orphans == 0:
logger.info("Checked project files: OK") logger.info("Checked project files: OK")
return 0, 0 return 0, 0
logger.warning("Found %d file(s) not tracked in project", orphans) logger.warning("Found %d file(s) not tracked in project", orphans)
recovered = 0 recovered = 0
for cHandle in files: for cHandle in remains:
aDoc = storage.getDocument(cHandle) aDoc = storage.getDocument(cHandle)
aDoc.readDocument(isOrphan=True) aDoc.readDocument(isOrphan=True)
oName, oParent, oClass, oLayout = aDoc.getMeta() oName, oParent, oClass, oLayout = aDoc.getMeta()
@@ -282,7 +273,6 @@ class NWTree:
newItem.setClass(oClass) newItem.setClass(oClass)
newItem.setLayout(oLayout) newItem.setLayout(oLayout)
if self.add(newItem): if self.add(newItem):
self.updateItemData(cHandle)
recovered += 1 recovered += 1
return orphans, recovered return orphans, recovered
@@ -346,25 +336,6 @@ class NWTree:
# Tree Item Methods # Tree Item Methods
## ##
def updateItemData(self, tHandle: str) -> bool:
"""Update the root item handle of a given item. Returns True if
a root was found and data updated, otherwise False.
"""
if tItem := self._items.get(tHandle):
iItem = tItem
for _ in range(MAX_DEPTH):
if iItem.itemParent is None:
tItem.setRoot(iItem.itemHandle)
tItem.setClassDefaults(iItem.itemClass)
return True
else:
iItem = self.__getitem__(iItem.itemParent)
if iItem is None:
return False
else:
raise RecursionError("Critical internal error")
return False
def checkType(self, tHandle: str, itemType: nwItemType) -> bool: def checkType(self, tHandle: str, itemType: nwItemType) -> bool:
"""Check if item exists and is of the specified item type.""" """Check if item exists and is of the specified item type."""
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
@@ -515,8 +486,6 @@ class NWTree:
elif pHandle in items: elif pHandle in items:
remains[handle] = item remains[handle] = item
logger.warning("Item '%s' found before its parent", handle) logger.warning("Item '%s' found before its parent", handle)
else:
logger.error("Item '%s' has no parent in current tree", handle)
elif item.isRootType(): elif item.isRootType():
node = ProjectNode(item) node = ProjectNode(item)
self._model.root.addChild(node) self._model.root.addChild(node)
+314 -361
View File
@@ -39,12 +39,11 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import qtLambda from novelwriter.common import qtLambda
from novelwriter.constants import nwLabels, nwStyles, nwUnicode, trConst from novelwriter.constants import nwLabels, nwStyles, trConst
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from novelwriter.core.itemmodel import ProjectModel, ProjectNode from novelwriter.core.itemmodel import ProjectModel, ProjectNode
from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.dialogs.projectsettings import GuiProjectSettings from novelwriter.enum import nwDocMode, nwItemClass, nwItemType
from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType
from novelwriter.extensions.modified import NIconToolButton from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
from novelwriter.types import ( from novelwriter.types import (
@@ -526,7 +525,6 @@ class GuiProjectTree(QTreeView):
# Tree Settings # Tree Settings
iPx = SHARED.theme.baseIconHeight iPx = SHARED.theme.baseIconHeight
# cMg = CONFIG.pxInt(6)
self.setIconSize(SHARED.theme.baseIconSize) self.setIconSize(SHARED.theme.baseIconSize)
self.setFrameStyle(QFrame.Shape.NoFrame) self.setFrameStyle(QFrame.Shape.NoFrame)
@@ -536,18 +534,6 @@ class GuiProjectTree(QTreeView):
self.setAutoExpandDelay(1000) self.setAutoExpandDelay(1000)
self.setHeaderHidden(True) self.setHeaderHidden(True)
self.setIndentation(iPx) self.setIndentation(iPx)
# self.setColumnCount(4)
# Lock the column sizes
# treeHeader = self.header()
# treeHeader.setStretchLastSection(False)
# treeHeader.setMinimumSectionSize(iPx + cMg)
# treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.ResizeMode.Stretch)
# treeHeader.setSectionResizeMode(self.C_COUNT, QHeaderView.ResizeMode.ResizeToContents)
# treeHeader.setSectionResizeMode(self.C_ACTIVE, QHeaderView.ResizeMode.Fixed)
# treeHeader.setSectionResizeMode(self.C_STATUS, QHeaderView.ResizeMode.Fixed)
# treeHeader.resizeSection(self.C_ACTIVE, iPx + cMg)
# treeHeader.resizeSection(self.C_STATUS, iPx + cMg)
# Allow Move by Drag & Drop # Allow Move by Drag & Drop
# self.setDragEnabled(True) # self.setDragEnabled(True)
@@ -732,13 +718,15 @@ class GuiProjectTree(QTreeView):
if dlgOk: if dlgOk:
# Add the file or folder # Add the file or folder
if itemType == nwItemType.FILE: if itemType == nwItemType.FILE:
tHandle = SHARED.project.newFile(newLabel, sHandle, pos) if tHandle := SHARED.project.newFile(newLabel, sHandle, pos):
if tHandle and copyDoc: if copyDoc:
SHARED.project.copyFileContent(tHandle, copyDoc) SHARED.project.copyFileContent(tHandle, copyDoc)
elif tHandle and hLevel > 0: elif hLevel > 0:
SHARED.project.writeNewFile(tHandle, hLevel, not isNote) SHARED.project.writeNewFile(tHandle, hLevel, not isNote)
SHARED.project.index.reIndexHandle(tHandle)
SHARED.project.tree.refreshItems([tHandle])
else: else:
tHandle = SHARED.project.newFolder(newLabel, sHandle, pos) SHARED.project.newFolder(newLabel, sHandle, pos)
return return
@@ -876,41 +864,6 @@ class GuiProjectTree(QTreeView):
# self.revealNewTreeItem(tHandle, wordCount=True) # self.revealNewTreeItem(tHandle, wordCount=True)
return return
def revealNewTreeItem(self, tHandle: str | None, nHandle: str | None = None,
wordCount: bool = False) -> bool:
"""Reveal a newly added project item in the project tree."""
# nwItem = SHARED.project.tree[tHandle] if tHandle else None
# if tHandle is None or nwItem is None:
# return False
# trItem = self._addTreeItem(nwItem, nHandle)
# if trItem is None:
# return False
# if nwItem.isFileType() and wordCount:
# wC = SHARED.project.index.getCounts(tHandle)[1]
# self.propagateCount(tHandle, wC)
# self.projView.wordCountsChanged.emit()
# pHandle = nwItem.itemParent
# if pHandle is not None and pHandle in self._treeMap:
# self._treeMap[pHandle].setExpanded(True)
# self._alertTreeChange(tHandle, flush=True)
# self.setCurrentItem(trItem)
return True
def getTreeFromHandle(self, tHandle: str) -> list[str]:
"""Recursively return all the child items starting from a given
item handle.
"""
result = []
# tIten = self._getTreeItem(tHandle)
# if tIten is not None:
# result = self._scanChildren(result, tIten, 0)
return result
def requestDeleteItem(self, tHandle: str | None = None) -> bool: 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
@@ -1768,358 +1721,358 @@ class _UpdatableMenu(QMenu):
return return
class _TreeContextMenu(QMenu): # class _TreeContextMenu(QMenu):
def __init__(self, projTree: GuiProjectTree, nwItem: NWItem) -> None: # def __init__(self, projTree: GuiProjectTree, nwItem: NWItem) -> None:
super().__init__(parent=projTree) # super().__init__(parent=projTree)
self.projTree = projTree # self.projTree = projTree
self.projView = projTree.projView # self.projView = projTree.projView
self._item = nwItem # self._item = nwItem
self._handle = nwItem.itemHandle # self._handle = nwItem.itemHandle
self._items: list[NWItem] = [] # self._items: list[NWItem] = []
logger.debug("Ready: _TreeContextMenu") # logger.debug("Ready: _TreeContextMenu")
return # return
def __del__(self) -> None: # pragma: no cover # def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: _TreeContextMenu") # logger.debug("Delete: _TreeContextMenu")
return # return
## # ##
# Methods # # Methods
## # ##
def buildTrashMenu(self) -> None: # def buildTrashMenu(self) -> None:
"""Build the special menu for the Trash folder.""" # """Build the special menu for the Trash folder."""
action = self.addAction(self.tr("Empty Trash")) # action = self.addAction(self.tr("Empty Trash"))
action.triggered.connect(self.projTree.emptyTrash) # action.triggered.connect(self.projTree.emptyTrash)
return # return
def buildSingleSelectMenu(self, hasChild: bool) -> None: # def buildSingleSelectMenu(self, hasChild: bool) -> None:
"""Build the single-select menu.""" # """Build the single-select menu."""
isFile = self._item.isFileType() # isFile = self._item.isFileType()
isFolder = self._item.isFolderType() # isFolder = self._item.isFolderType()
isRoot = self._item.isRootType() # isRoot = self._item.isRootType()
# Document Actions # # Document Actions
if isFile: # if isFile:
self._docActions() # self._docActions()
self.addSeparator() # self.addSeparator()
# Create New Items # # Create New Items
self._itemCreation() # self._itemCreation()
self.addSeparator() # self.addSeparator()
# Edit Item Settings # # Edit Item Settings
action = self.addAction(self.tr("Rename")) # action = self.addAction(self.tr("Rename"))
action.triggered.connect(qtLambda(self.projView.renameTreeItem, self._handle)) # action.triggered.connect(qtLambda(self.projView.renameTreeItem, self._handle))
if isFile: # if isFile:
self._itemHeader() # self._itemHeader()
self._itemActive(False) # self._itemActive(False)
self._itemStatusImport(False) # self._itemStatusImport(False)
# Transform Item # # Transform Item
if isFile or isFolder: # if isFile or isFolder:
self._itemTransform(isFile, isFolder, hasChild) # self._itemTransform(isFile, isFolder, hasChild)
self.addSeparator() # self.addSeparator()
# Process Item # # Process Item
self._itemProcess(isFile, isFolder, isRoot, hasChild) # self._itemProcess(isFile, isFolder, isRoot, hasChild)
return # return
def buildMultiSelectMenu(self, handles: list[str]) -> None: # def buildMultiSelectMenu(self, handles: list[str]) -> None:
"""Build the multi-select menu.""" # """Build the multi-select menu."""
self._items = [] # self._items = []
for tHandle in handles: # for tHandle in handles:
if (tItem := SHARED.project.tree[tHandle]): # if (tItem := SHARED.project.tree[tHandle]):
self._items.append(tItem) # self._items.append(tItem)
self._itemActive(True) # self._itemActive(True)
self._itemStatusImport(True) # self._itemStatusImport(True)
self.addSeparator() # self.addSeparator()
self._multiMoveToTrash() # self._multiMoveToTrash()
return # return
## # ##
# Menu Builders # # Menu Builders
## # ##
def _docActions(self) -> None: # def _docActions(self) -> None:
"""Add document actions.""" # """Add document actions."""
action = self.addAction(self.tr("Open Document")) # action = self.addAction(self.tr("Open Document"))
action.triggered.connect(qtLambda( # action.triggered.connect(qtLambda(
self.projView.openDocumentRequest.emit, # self.projView.openDocumentRequest.emit,
self._handle, nwDocMode.EDIT, "", True # self._handle, nwDocMode.EDIT, "", True
)) # ))
action = self.addAction(self.tr("View Document")) # action = self.addAction(self.tr("View Document"))
action.triggered.connect(qtLambda( # action.triggered.connect(qtLambda(
self.projView.openDocumentRequest.emit, # self.projView.openDocumentRequest.emit,
self._handle, nwDocMode.VIEW, "", False # self._handle, nwDocMode.VIEW, "", False
)) # ))
return # return
def _itemCreation(self) -> None: # def _itemCreation(self) -> None:
"""Add create item actions.""" # """Add create item actions."""
menu = self.addMenu(self.tr("Create New ...")) # menu = self.addMenu(self.tr("Create New ..."))
menu.addAction(self.projView.projBar.aAddEmpty) # menu.addAction(self.projView.projBar.aAddEmpty)
menu.addAction(self.projView.projBar.aAddChap) # menu.addAction(self.projView.projBar.aAddChap)
menu.addAction(self.projView.projBar.aAddScene) # menu.addAction(self.projView.projBar.aAddScene)
menu.addAction(self.projView.projBar.aAddNote) # menu.addAction(self.projView.projBar.aAddNote)
menu.addAction(self.projView.projBar.aAddFolder) # menu.addAction(self.projView.projBar.aAddFolder)
return # return
def _itemHeader(self) -> None: # def _itemHeader(self) -> None:
"""Check if there is a header that can be used for rename.""" # """Check if there is a header that can be used for rename."""
SHARED.saveEditor() # SHARED.saveEditor()
if hItem := SHARED.project.index.getItemHeading(self._handle, "T0001"): # if hItem := SHARED.project.index.getItemHeading(self._handle, "T0001"):
action = self.addAction(self.tr("Rename to Heading")) # action = self.addAction(self.tr("Rename to Heading"))
action.triggered.connect( # action.triggered.connect(
qtLambda(self.projView.renameTreeItem, self._handle, hItem.title) # qtLambda(self.projView.renameTreeItem, self._handle, hItem.title)
) # )
return # return
def _itemActive(self, multi: bool) -> None: # def _itemActive(self, multi: bool) -> None:
"""Add Active/Inactive actions.""" # """Add Active/Inactive actions."""
if multi: # if multi:
mSub = self.addMenu(self.tr("Set Active to ...")) # mSub = self.addMenu(self.tr("Set Active to ..."))
aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.projTree.trActive) # aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.projTree.trActive)
aOne.triggered.connect(qtLambda(self._iterItemActive, True)) # aOne.triggered.connect(qtLambda(self._iterItemActive, True))
aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.projTree.trInactive) # aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.projTree.trInactive)
aTwo.triggered.connect(qtLambda(self._iterItemActive, False)) # aTwo.triggered.connect(qtLambda(self._iterItemActive, False))
else: # else:
action = self.addAction(self.tr("Toggle Active")) # action = self.addAction(self.tr("Toggle Active"))
action.triggered.connect(self._toggleItemActive) # action.triggered.connect(self._toggleItemActive)
return # return
def _itemStatusImport(self, multi: bool) -> None: # def _itemStatusImport(self, multi: bool) -> None:
"""Add actions for changing status or importance.""" # """Add actions for changing status or importance."""
if self._item.isNovelLike(): # if self._item.isNovelLike():
menu = self.addMenu(self.tr("Set Status to ...")) # menu = self.addMenu(self.tr("Set Status to ..."))
current = self._item.itemStatus # current = self._item.itemStatus
for key, entry in SHARED.project.data.itemStatus.iterItems(): # for key, entry in SHARED.project.data.itemStatus.iterItems():
name = entry.name # name = entry.name
if not multi and current == key: # if not multi and current == key:
name += f" ({nwUnicode.U_CHECK})" # name += f" ({nwUnicode.U_CHECK})"
action = menu.addAction(entry.icon, name) # action = menu.addAction(entry.icon, name)
if multi: # if multi:
action.triggered.connect(qtLambda(self._iterSetItemStatus, key)) # action.triggered.connect(qtLambda(self._iterSetItemStatus, key))
else: # else:
action.triggered.connect(qtLambda(self._changeItemStatus, key)) # action.triggered.connect(qtLambda(self._changeItemStatus, key))
menu.addSeparator() # menu.addSeparator()
action = menu.addAction(self.tr("Manage Labels ...")) # action = menu.addAction(self.tr("Manage Labels ..."))
action.triggered.connect(qtLambda( # action.triggered.connect(qtLambda(
self.projView.projectSettingsRequest.emit, # self.projView.projectSettingsRequest.emit,
GuiProjectSettings.PAGE_STATUS # GuiProjectSettings.PAGE_STATUS
)) # ))
else: # else:
menu = self.addMenu(self.tr("Set Importance to ...")) # menu = self.addMenu(self.tr("Set Importance to ..."))
current = self._item.itemImport # current = self._item.itemImport
for key, entry in SHARED.project.data.itemImport.iterItems(): # for key, entry in SHARED.project.data.itemImport.iterItems():
name = entry.name # name = entry.name
if not multi and current == key: # if not multi and current == key:
name += f" ({nwUnicode.U_CHECK})" # name += f" ({nwUnicode.U_CHECK})"
action = menu.addAction(entry.icon, name) # action = menu.addAction(entry.icon, name)
if multi: # if multi:
action.triggered.connect(qtLambda(self._iterSetItemImport, key)) # action.triggered.connect(qtLambda(self._iterSetItemImport, key))
else: # else:
action.triggered.connect(qtLambda(self._changeItemImport, key)) # action.triggered.connect(qtLambda(self._changeItemImport, key))
menu.addSeparator() # menu.addSeparator()
action = menu.addAction(self.tr("Manage Labels ...")) # action = menu.addAction(self.tr("Manage Labels ..."))
action.triggered.connect(qtLambda( # action.triggered.connect(qtLambda(
self.projView.projectSettingsRequest.emit, # self.projView.projectSettingsRequest.emit,
GuiProjectSettings.PAGE_IMPORT # GuiProjectSettings.PAGE_IMPORT
)) # ))
return # return
def _itemTransform(self, isFile: bool, isFolder: bool, hasChild: bool) -> None: # def _itemTransform(self, isFile: bool, isFolder: bool, hasChild: bool) -> None:
"""Add actions for the Transform menu.""" # """Add actions for the Transform menu."""
menu = self.addMenu(self.tr("Transform ...")) # menu = self.addMenu(self.tr("Transform ..."))
tree = self.projTree # tree = self.projTree
tHandle = self._handle # tHandle = self._handle
trDoc = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT]) # trDoc = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
trNote = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE]) # trNote = trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
loDoc = nwItemLayout.DOCUMENT # loDoc = nwItemLayout.DOCUMENT
loNote = nwItemLayout.NOTE # loNote = nwItemLayout.NOTE
isDocFile = isFile and self._item.isDocumentLayout() # isDocFile = isFile and self._item.isDocumentLayout()
isNoteFile = isFile and self._item.isNoteLayout() # isNoteFile = isFile and self._item.isNoteLayout()
if isNoteFile and self._item.documentAllowed(): # if isNoteFile and self._item.documentAllowed():
action = menu.addAction(self.tr("Convert to {0}").format(trDoc)) # action = menu.addAction(self.tr("Convert to {0}").format(trDoc))
action.triggered.connect(qtLambda(self._changeItemLayout, loDoc)) # action.triggered.connect(qtLambda(self._changeItemLayout, loDoc))
if isDocFile: # if isDocFile:
action = menu.addAction(self.tr("Convert to {0}").format(trNote)) # action = menu.addAction(self.tr("Convert to {0}").format(trNote))
action.triggered.connect(qtLambda(self._changeItemLayout, loNote)) # action.triggered.connect(qtLambda(self._changeItemLayout, loNote))
if isFolder and self._item.documentAllowed(): # if isFolder and self._item.documentAllowed():
action = menu.addAction(self.tr("Convert to {0}").format(trDoc)) # action = menu.addAction(self.tr("Convert to {0}").format(trDoc))
action.triggered.connect(qtLambda(self._covertFolderToFile, loDoc)) # action.triggered.connect(qtLambda(self._covertFolderToFile, loDoc))
if isFolder: # if isFolder:
action = menu.addAction(self.tr("Convert to {0}").format(trNote)) # action = menu.addAction(self.tr("Convert to {0}").format(trNote))
action.triggered.connect(qtLambda(self._covertFolderToFile, loNote)) # action.triggered.connect(qtLambda(self._covertFolderToFile, loNote))
if hasChild and isFile: # if hasChild and isFile:
action = menu.addAction(self.tr("Merge Child Items into Self")) # action = menu.addAction(self.tr("Merge Child Items into Self"))
action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, False)) # action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, False))
action = menu.addAction(self.tr("Merge Child Items into New")) # action = menu.addAction(self.tr("Merge Child Items into New"))
action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, True)) # action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, True))
if hasChild and isFolder: # if hasChild and isFolder:
action = menu.addAction(self.tr("Merge Documents in Folder")) # action = menu.addAction(self.tr("Merge Documents in Folder"))
action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, True)) # action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, True))
if isFile: # if isFile:
action = menu.addAction(self.tr("Split Document by Headings")) # action = menu.addAction(self.tr("Split Document by Headings"))
action.triggered.connect(qtLambda(tree._splitDocument, tHandle)) # action.triggered.connect(qtLambda(tree._splitDocument, tHandle))
return # return
def _itemProcess(self, isFile: bool, isFolder: bool, isRoot: bool, hasChild: bool) -> None: # def _itemProcess(self, isFile: bool, isFolder: bool, isRoot: bool, hasChild: bool) -> None:
"""Add actions for item processing.""" # """Add actions for item processing."""
tree = self.projTree # tree = self.projTree
tHandle = self._handle # tHandle = self._handle
if hasChild: # if hasChild:
action = self.addAction(self.tr("Expand All")) # action = self.addAction(self.tr("Expand All"))
action.triggered.connect(qtLambda(tree.setExpandedFromHandle, tHandle, True)) # action.triggered.connect(qtLambda(tree.setExpandedFromHandle, tHandle, True))
action = self.addAction(self.tr("Collapse All")) # action = self.addAction(self.tr("Collapse All"))
action.triggered.connect(qtLambda(tree.setExpandedFromHandle, tHandle, False)) # action.triggered.connect(qtLambda(tree.setExpandedFromHandle, tHandle, False))
action = self.addAction(self.tr("Duplicate")) # action = self.addAction(self.tr("Duplicate"))
action.triggered.connect(qtLambda(tree._duplicateFromHandle, tHandle)) # action.triggered.connect(qtLambda(tree._duplicateFromHandle, tHandle))
if self._item.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild): # if self._item.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
action = self.addAction(self.tr("Delete Permanently")) # action = self.addAction(self.tr("Delete Permanently"))
action.triggered.connect(qtLambda(tree.permDeleteItem, tHandle)) # action.triggered.connect(qtLambda(tree.permDeleteItem, tHandle))
else: # else:
action = self.addAction(self.tr("Move to Trash")) # action = self.addAction(self.tr("Move to Trash"))
action.triggered.connect(qtLambda(tree.moveItemToTrash, tHandle)) # action.triggered.connect(qtLambda(tree.moveItemToTrash, tHandle))
return # return
def _multiMoveToTrash(self) -> None: # def _multiMoveToTrash(self) -> None:
"""Add move to Trash action.""" # """Add move to Trash action."""
areTrash = [i.itemClass == nwItemClass.TRASH for i in self._items] # areTrash = [i.itemClass == nwItemClass.TRASH for i in self._items]
if all(areTrash): # if all(areTrash):
action = self.addAction(self.tr("Delete Permanently")) # action = self.addAction(self.tr("Delete Permanently"))
action.triggered.connect(self._iterPermDelete) # action.triggered.connect(self._iterPermDelete)
elif not any(areTrash): # elif not any(areTrash):
action = self.addAction(self.tr("Move to Trash")) # action = self.addAction(self.tr("Move to Trash"))
action.triggered.connect(self._iterMoveToTrash) # action.triggered.connect(self._iterMoveToTrash)
return # return
## # ##
# Private Slots # # Private Slots
## # ##
@pyqtSlot() # @pyqtSlot()
def _iterMoveToTrash(self) -> None: # def _iterMoveToTrash(self) -> None:
"""Iterate through files and move them to Trash.""" # """Iterate through files and move them to Trash."""
if SHARED.question(self.tr("Move {0} items to Trash?").format(len(self._items))): # if SHARED.question(self.tr("Move {0} items to Trash?").format(len(self._items))):
for tItem in self._items: # for tItem in self._items:
if tItem.isFileType() and tItem.itemClass != nwItemClass.TRASH: # if tItem.isFileType() and tItem.itemClass != nwItemClass.TRASH:
self.projTree.moveItemToTrash(tItem.itemHandle, askFirst=False, flush=False) # self.projTree.moveItemToTrash(tItem.itemHandle, askFirst=False, flush=False)
# self.projTree.saveTreeOrder() # # self.projTree.saveTreeOrder()
return # return
@pyqtSlot() # @pyqtSlot()
def _iterPermDelete(self) -> None: # def _iterPermDelete(self) -> None:
"""Iterate through files and delete them.""" # """Iterate through files and delete them."""
if SHARED.question(self.projTree.trPermDelete.format(len(self._items))): # if SHARED.question(self.projTree.trPermDelete.format(len(self._items))):
for tItem in self._items: # for tItem in self._items:
if tItem.isFileType() and tItem.itemClass == nwItemClass.TRASH: # if tItem.isFileType() and tItem.itemClass == nwItemClass.TRASH:
self.projTree.permDeleteItem(tItem.itemHandle, askFirst=False, flush=False) # self.projTree.permDeleteItem(tItem.itemHandle, askFirst=False, flush=False)
# self.projTree.saveTreeOrder() # # self.projTree.saveTreeOrder()
return # return
@pyqtSlot() # @pyqtSlot()
def _toggleItemActive(self) -> None: # def _toggleItemActive(self) -> None:
"""Toggle the active status of an item.""" # """Toggle the active status of an item."""
self._item.setActive(not self._item.isActive) # self._item.setActive(not self._item.isActive)
self.projTree.setTreeItemValues(self._item) # self.projTree.setTreeItemValues(self._item)
self.projTree._alertTreeChange(self._handle, flush=False) # self.projTree._alertTreeChange(self._handle, flush=False)
return # return
## # ##
# Internal Functions # # Internal Functions
## # ##
def _iterItemActive(self, isActive: bool) -> None: # def _iterItemActive(self, isActive: bool) -> None:
"""Set the active status of multiple items.""" # """Set the active status of multiple items."""
for tItem in self._items: # for tItem in self._items:
if tItem and tItem.isFileType(): # if tItem and tItem.isFileType():
tItem.setActive(isActive) # tItem.setActive(isActive)
self.projTree.setTreeItemValues(tItem) # self.projTree.setTreeItemValues(tItem)
self.projTree._alertTreeChange(tItem.itemHandle, flush=False) # self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
return # return
def _changeItemStatus(self, key: str) -> None: # def _changeItemStatus(self, key: str) -> None:
"""Set a new status value of an item.""" # """Set a new status value of an item."""
self._item.setStatus(key) # self._item.setStatus(key)
self.projTree.setTreeItemValues(self._item) # self.projTree.setTreeItemValues(self._item)
self.projTree._alertTreeChange(self._handle, flush=False) # self.projTree._alertTreeChange(self._handle, flush=False)
return # return
def _iterSetItemStatus(self, key: str) -> None: # def _iterSetItemStatus(self, key: str) -> None:
"""Change the status value for multiple items.""" # """Change the status value for multiple items."""
for tItem in self._items: # for tItem in self._items:
if tItem and tItem.isNovelLike(): # if tItem and tItem.isNovelLike():
tItem.setStatus(key) # tItem.setStatus(key)
self.projTree.setTreeItemValues(tItem) # self.projTree.setTreeItemValues(tItem)
self.projTree._alertTreeChange(tItem.itemHandle, flush=False) # self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
return # return
def _changeItemImport(self, key: str) -> None: # def _changeItemImport(self, key: str) -> None:
"""Set a new importance value of an item.""" # """Set a new importance value of an item."""
self._item.setImport(key) # self._item.setImport(key)
self.projTree.setTreeItemValues(self._item) # self.projTree.setTreeItemValues(self._item)
self.projTree._alertTreeChange(self._handle, flush=False) # self.projTree._alertTreeChange(self._handle, flush=False)
return # return
def _iterSetItemImport(self, key: str) -> None: # def _iterSetItemImport(self, key: str) -> None:
"""Change the status value for multiple items.""" # """Change the status value for multiple items."""
for tItem in self._items: # for tItem in self._items:
if tItem and not tItem.isNovelLike(): # if tItem and not tItem.isNovelLike():
tItem.setImport(key) # tItem.setImport(key)
self.projTree.setTreeItemValues(tItem) # self.projTree.setTreeItemValues(tItem)
self.projTree._alertTreeChange(tItem.itemHandle, flush=False) # self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
return # return
def _changeItemLayout(self, itemLayout: nwItemLayout) -> None: # def _changeItemLayout(self, itemLayout: nwItemLayout) -> None:
"""Set a new item layout value of an item.""" # """Set a new item layout value of an item."""
if itemLayout == nwItemLayout.DOCUMENT and self._item.documentAllowed(): # if itemLayout == nwItemLayout.DOCUMENT and self._item.documentAllowed():
self._item.setLayout(nwItemLayout.DOCUMENT) # self._item.setLayout(nwItemLayout.DOCUMENT)
self.projTree.setTreeItemValues(self._item) # self.projTree.setTreeItemValues(self._item)
self.projTree._alertTreeChange(self._handle, flush=False) # self.projTree._alertTreeChange(self._handle, flush=False)
elif itemLayout == nwItemLayout.NOTE: # elif itemLayout == nwItemLayout.NOTE:
self._item.setLayout(nwItemLayout.NOTE) # self._item.setLayout(nwItemLayout.NOTE)
self.projTree.setTreeItemValues(self._item) # self.projTree.setTreeItemValues(self._item)
self.projTree._alertTreeChange(self._handle, flush=False) # self.projTree._alertTreeChange(self._handle, flush=False)
return # return
def _covertFolderToFile(self, itemLayout: nwItemLayout) -> None: # def _covertFolderToFile(self, itemLayout: nwItemLayout) -> None:
"""Convert a folder to a note or document.""" # """Convert a folder to a note or document."""
if self._item.isFolderType(): # if self._item.isFolderType():
msgYes = SHARED.question(self.tr( # msgYes = SHARED.question(self.tr(
"Do you want to convert the folder to a {0}? " # "Do you want to convert the folder to a {0}? "
"This action cannot be reversed." # "This action cannot be reversed."
).format(trConst(nwLabels.LAYOUT_NAME[itemLayout]))) # ).format(trConst(nwLabels.LAYOUT_NAME[itemLayout])))
if msgYes and itemLayout == nwItemLayout.DOCUMENT and self._item.documentAllowed(): # if msgYes and itemLayout == nwItemLayout.DOCUMENT and self._item.documentAllowed():
self._item.setType(nwItemType.FILE) # self._item.setType(nwItemType.FILE)
self._item.setLayout(nwItemLayout.DOCUMENT) # self._item.setLayout(nwItemLayout.DOCUMENT)
self.projTree.setTreeItemValues(self._item) # self.projTree.setTreeItemValues(self._item)
self.projTree._alertTreeChange(self._handle, flush=False) # self.projTree._alertTreeChange(self._handle, flush=False)
elif msgYes and itemLayout == nwItemLayout.NOTE: # elif msgYes and itemLayout == nwItemLayout.NOTE:
self._item.setType(nwItemType.FILE) # self._item.setType(nwItemType.FILE)
self._item.setLayout(nwItemLayout.NOTE) # self._item.setLayout(nwItemLayout.NOTE)
self.projTree.setTreeItemValues(self._item) # self.projTree.setTreeItemValues(self._item)
self.projTree._alertTreeChange(self._handle, flush=False) # self.projTree._alertTreeChange(self._handle, flush=False)
else: # else:
logger.info("Folder conversion cancelled") # logger.info("Folder conversion cancelled")
return # return