Add feature to undo last project tree move
This commit is contained in:
@@ -378,6 +378,8 @@ Most features are available as keyboard shortcuts. These are as follows:
|
||||
":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking."
|
||||
":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline."
|
||||
":kbd:`Ctrl`:kbd:`Del`", "If in the project tree, move a document to trash, or delete a folder."
|
||||
":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree."
|
||||
":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree."
|
||||
":kbd:`Ctrl`:kbd:`'`", "Wrap selected text, or word under cursor, in single quotes."
|
||||
":kbd:`Ctrl`:kbd:`""`", "Wrap selected text, or word under cursor, in double quotes."
|
||||
":kbd:`Ctrl`:kbd:`Enter`", "Open the tag or reference under the cursor in the Viewer."
|
||||
@@ -392,9 +394,7 @@ Most features are available as keyboard shortcuts. These are as follows:
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`R`", "Close the document viewer."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`S`", "Save the current project."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`W`", "Close the current project."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Alternative sequence for redo last undo."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Up`", "Move item one step up in the project tree."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Down`", "Move item one step down in the project tree."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Undo move of project tree item."
|
||||
":kbd:`F1`", "Open the documentation. This will either open the Qt Assistant, if available, or send you to the documentation website."
|
||||
":kbd:`F2`", "If in the project tree, edit a document or folder settings. (Same as :kbd:`Ctrl`:kbd:`E`)"
|
||||
":kbd:`F3`", "Find next occurrence of search word in current document. (Same as :kbd:`Ctrl`:kbd:`G`)"
|
||||
|
||||
@@ -56,6 +56,9 @@ class nwLists():
|
||||
# Item classes where the full list of novel layouts are allowed
|
||||
CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE}
|
||||
|
||||
# Item classes which do not require items to have same class
|
||||
FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH}
|
||||
|
||||
# END Class nwLists
|
||||
|
||||
class nwRegEx():
|
||||
|
||||
+11
-4
@@ -260,15 +260,15 @@ class GuiMainMenu(QMenuBar):
|
||||
self.projMenu.addSeparator()
|
||||
|
||||
# Project > Edit
|
||||
self.aEditItem = QAction("Edit Project Item", self)
|
||||
self.aEditItem.setStatusTip("Change item settings")
|
||||
self.aEditItem = QAction("Edit Item", self)
|
||||
self.aEditItem.setStatusTip("Change project item settings")
|
||||
self.aEditItem.setShortcuts(["Ctrl+E", "F2"])
|
||||
self.aEditItem.triggered.connect(lambda: self.theParent.editItem(None))
|
||||
self.projMenu.addAction(self.aEditItem)
|
||||
|
||||
# Project > Delete
|
||||
self.aDeleteItem = QAction("Delete Project Item", self)
|
||||
self.aDeleteItem.setStatusTip("Delete selected item")
|
||||
self.aDeleteItem = QAction("Delete Item", self)
|
||||
self.aDeleteItem.setStatusTip("Delete selected project item")
|
||||
self.aDeleteItem.setShortcut("Ctrl+Del")
|
||||
self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None))
|
||||
self.projMenu.addAction(self.aDeleteItem)
|
||||
@@ -287,6 +287,13 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1))
|
||||
self.projMenu.addAction(self.aMoveDown)
|
||||
|
||||
# Project > Undo Last Action
|
||||
self.aMoveUndo = QAction("Undo Last Move", self)
|
||||
self.aMoveUndo.setStatusTip("Undo last item move")
|
||||
self.aMoveUndo.setShortcut("Ctrl+Shift+Z")
|
||||
self.aMoveUndo.triggered.connect(lambda: self.theParent.treeView.undoLastMove())
|
||||
self.projMenu.addAction(self.aMoveUndo)
|
||||
|
||||
# Project > Empty Trash
|
||||
self.aEmptyTrash = QAction("Empty Trash", self)
|
||||
self.aEmptyTrash.setStatusTip("Permanently delete all files in the Trash folder")
|
||||
|
||||
+91
-14
@@ -33,12 +33,12 @@ from time import time
|
||||
from PyQt5.QtCore import Qt, QSize, pyqtSignal
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import (
|
||||
qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction
|
||||
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction
|
||||
)
|
||||
|
||||
from nw.core import NWDoc
|
||||
from nw.constants import (
|
||||
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst
|
||||
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst, nwLists
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -68,6 +68,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
self._treeMap = {}
|
||||
self._treeChanged = False
|
||||
self._timeChanged = 0
|
||||
self._lastMove = {}
|
||||
|
||||
##
|
||||
# Build GUI
|
||||
@@ -361,6 +362,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
return False
|
||||
cItem = pItem.takeChild(tIndex)
|
||||
pItem.insertChild(nIndex, cItem)
|
||||
self._recordLastMove(cItem, pItem, tIndex)
|
||||
|
||||
self.clearSelection()
|
||||
cItem.setSelected(True)
|
||||
@@ -517,8 +519,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
theDoc = NWDoc(self.theProject, self.theParent)
|
||||
theDoc.deleteDocument(tHandle)
|
||||
del self.theProject.projTree[tHandle]
|
||||
self.theIndex.deleteHandle(tHandle)
|
||||
self._deleteTreeItem(tHandle)
|
||||
self._setTreeChanged(True)
|
||||
|
||||
else:
|
||||
# The file is not already in the trash folder, so we
|
||||
@@ -541,11 +544,12 @@ class GuiProjectTree(QTreeWidget):
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
trItemC = trItemP.takeChild(tIndex)
|
||||
trItemT.addChild(trItemC)
|
||||
nwItemS.setParent(self.theProject.projTree.trashRoot())
|
||||
self._updateItemParent(tHandle)
|
||||
self.propagateCount(tHandle, wCount)
|
||||
|
||||
self._setTreeChanged(True)
|
||||
self.theIndex.deleteHandle(tHandle)
|
||||
self._recordLastMove(trItemS, trItemP, tIndex)
|
||||
self._setTreeChanged(True)
|
||||
|
||||
elif nwItemS.itemType == nwItemType.FOLDER:
|
||||
logger.debug("User requested folder %s deleted" % tHandle)
|
||||
@@ -556,7 +560,8 @@ class GuiProjectTree(QTreeWidget):
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
if trItemS.childCount() == 0:
|
||||
trItemP.takeChild(tIndex)
|
||||
del self.theProject.projTree[tHandle]
|
||||
self._deleteTreeItem(tHandle)
|
||||
self._setTreeChanged(True)
|
||||
else:
|
||||
self.makeAlert((
|
||||
"Cannot delete folder. It is not empty. "
|
||||
@@ -570,7 +575,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
tIndex = self.indexOfTopLevelItem(trItemS)
|
||||
if trItemS.childCount() == 0:
|
||||
self.takeTopLevelItem(tIndex)
|
||||
del self.theProject.projTree[tHandle]
|
||||
self._deleteTreeItem(tHandle)
|
||||
self.theParent.mainMenu.setAvailableRoot()
|
||||
self._setTreeChanged(True)
|
||||
else:
|
||||
@@ -679,6 +684,59 @@ class GuiProjectTree(QTreeWidget):
|
||||
logger.debug("%d items added to the project tree" % iCount)
|
||||
return True
|
||||
|
||||
def undoLastMove(self):
|
||||
"""Attempt to undo the last action.
|
||||
"""
|
||||
srcItem = self._lastMove.get("item", None)
|
||||
dstItem = self._lastMove.get("parent", None)
|
||||
dstIndex = self._lastMove.get("index", None)
|
||||
|
||||
if not self.hasFocus():
|
||||
return False
|
||||
|
||||
if srcItem is None or dstItem is None or dstIndex is None:
|
||||
logger.verbose("No tree move to undo")
|
||||
return False
|
||||
|
||||
if srcItem not in self._treeMap.values():
|
||||
logger.warning("Source item no longer exists")
|
||||
return False
|
||||
|
||||
if dstItem not in self._treeMap.values():
|
||||
logger.warning("Previous parent item no longer exists")
|
||||
return False
|
||||
|
||||
dstIndex = min(max(0, dstIndex), dstItem.childCount())
|
||||
wCount = int(srcItem.data(self.C_COUNT, Qt.UserRole))
|
||||
sHandle = srcItem.data(self.C_NAME, Qt.UserRole)
|
||||
dHandle = dstItem.data(self.C_NAME, Qt.UserRole)
|
||||
logger.debug("Moving item %s back to %s, index %d" % (
|
||||
sHandle, dHandle, dstIndex
|
||||
))
|
||||
|
||||
self.propagateCount(sHandle, 0)
|
||||
parItem = srcItem.parent()
|
||||
srcIndex = parItem.indexOfChild(srcItem)
|
||||
movItem = parItem.takeChild(srcIndex)
|
||||
dstItem.insertChild(dstIndex, movItem)
|
||||
self._updateItemParent(sHandle)
|
||||
self.propagateCount(sHandle, wCount)
|
||||
|
||||
snItem = self.theProject.projTree[sHandle]
|
||||
dnItem = self.theProject.projTree[dHandle]
|
||||
if dnItem.itemClass not in nwLists.FREE_CLASS:
|
||||
logger.debug("Item %s class has been changed from %s to %s" % (
|
||||
sHandle, snItem.itemClass.name, dnItem.itemClass.name
|
||||
))
|
||||
snItem.setClass(dnItem.itemClass)
|
||||
self.setTreeItemValues(sHandle)
|
||||
|
||||
self.clearSelection()
|
||||
movItem.setSelected(True)
|
||||
self._lastMove = {}
|
||||
|
||||
return True
|
||||
|
||||
def getSelectedHandle(self):
|
||||
"""Get the currently selected handle. If multiple items are
|
||||
selected, return the first.
|
||||
@@ -779,6 +837,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
return
|
||||
|
||||
sItem = self._getTreeItem(sHandle)
|
||||
pItem = sItem.parent()
|
||||
dItem = self.itemFromIndex(dIndex)
|
||||
dHandle = dItem.data(self.C_NAME, Qt.UserRole)
|
||||
snItem = self.theProject.projTree[sHandle]
|
||||
@@ -791,11 +850,10 @@ class GuiProjectTree(QTreeWidget):
|
||||
isSame = snItem.itemClass == dnItem.itemClass
|
||||
isNone = snItem.itemClass == nwItemClass.NO_CLASS
|
||||
isNote = snItem.itemLayout == nwItemLayout.NOTE
|
||||
onFile = dnItem.itemType == nwItemType.FILE
|
||||
isRoot = snItem.itemType == nwItemType.ROOT
|
||||
onFree = dnItem.itemClass == nwItemClass.ARCHIVE
|
||||
onFree |= dnItem.itemClass == nwItemClass.TRASH
|
||||
onFree &= snItem.itemType == nwItemType.FILE
|
||||
isFile = snItem.itemType == nwItemType.FILE
|
||||
onFile = dnItem.itemType == nwItemType.FILE
|
||||
onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile
|
||||
isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem
|
||||
if (isSame or isNone or isNote or onFree) and not (onFile and isOnTop) and not isRoot:
|
||||
logger.debug("Drag'n'drop of item %s accepted" % sHandle)
|
||||
@@ -807,14 +865,13 @@ class GuiProjectTree(QTreeWidget):
|
||||
# and the target is not a free root folder, update its class
|
||||
if not (isSame or onFree):
|
||||
logger.debug("Item %s class has been changed from %s to %s" % (
|
||||
sHandle,
|
||||
snItem.itemClass.name,
|
||||
dnItem.itemClass.name
|
||||
sHandle, snItem.itemClass.name, dnItem.itemClass.name
|
||||
))
|
||||
snItem.setClass(dnItem.itemClass)
|
||||
self.setTreeItemValues(sHandle)
|
||||
|
||||
self.propagateCount(sHandle, wCount)
|
||||
self._recordLastMove(sItem, pItem, pItem.indexOfChild(sItem))
|
||||
|
||||
# The items dropped into archive or trash should be removed
|
||||
# from the project index, for all other items, we rescan the
|
||||
@@ -844,6 +901,13 @@ class GuiProjectTree(QTreeWidget):
|
||||
"""
|
||||
return self._treeMap.get(tHandle, None)
|
||||
|
||||
def _deleteTreeItem(self, tHandle):
|
||||
"""Delete a tree item from the project and the map.
|
||||
"""
|
||||
del self.theProject.projTree[tHandle]
|
||||
self._treeMap.pop(tHandle, None)
|
||||
return
|
||||
|
||||
def _scanChildren(self, theList, theItem, theIndex):
|
||||
"""This is a recursive function returning all items in a tree
|
||||
starting at a given QTreeWidgetItem.
|
||||
@@ -984,6 +1048,19 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def _recordLastMove(self, srcItem, parItem, parIndex):
|
||||
"""Record the last action so that it can be undone.
|
||||
"""
|
||||
prevItem = self._lastMove.get("item", None)
|
||||
if prevItem is None or srcItem != prevItem:
|
||||
self._lastMove = {
|
||||
"item": srcItem,
|
||||
"parent": parItem,
|
||||
"index": parIndex,
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiProjectTree
|
||||
|
||||
class GuiProjectTreeMenu(QMenu):
|
||||
|
||||
Reference in New Issue
Block a user