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:`F7`", "Toggle spell checking."
|
||||||
":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline."
|
":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:`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 single quotes."
|
||||||
":kbd:`Ctrl`:kbd:`""`", "Wrap selected text, or word under cursor, in double 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."
|
":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:`R`", "Close the document viewer."
|
||||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`S`", "Save the current project."
|
":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:`W`", "Close the current project."
|
||||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Alternative sequence for redo last undo."
|
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Undo move of project tree item."
|
||||||
":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:`F1`", "Open the documentation. This will either open the Qt Assistant, if available, or send you to the documentation website."
|
":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:`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`)"
|
":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
|
# Item classes where the full list of novel layouts are allowed
|
||||||
CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE}
|
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
|
# END Class nwLists
|
||||||
|
|
||||||
class nwRegEx():
|
class nwRegEx():
|
||||||
|
|||||||
+11
-4
@@ -260,15 +260,15 @@ class GuiMainMenu(QMenuBar):
|
|||||||
self.projMenu.addSeparator()
|
self.projMenu.addSeparator()
|
||||||
|
|
||||||
# Project > Edit
|
# Project > Edit
|
||||||
self.aEditItem = QAction("Edit Project Item", self)
|
self.aEditItem = QAction("Edit Item", self)
|
||||||
self.aEditItem.setStatusTip("Change item settings")
|
self.aEditItem.setStatusTip("Change project item settings")
|
||||||
self.aEditItem.setShortcuts(["Ctrl+E", "F2"])
|
self.aEditItem.setShortcuts(["Ctrl+E", "F2"])
|
||||||
self.aEditItem.triggered.connect(lambda: self.theParent.editItem(None))
|
self.aEditItem.triggered.connect(lambda: self.theParent.editItem(None))
|
||||||
self.projMenu.addAction(self.aEditItem)
|
self.projMenu.addAction(self.aEditItem)
|
||||||
|
|
||||||
# Project > Delete
|
# Project > Delete
|
||||||
self.aDeleteItem = QAction("Delete Project Item", self)
|
self.aDeleteItem = QAction("Delete Item", self)
|
||||||
self.aDeleteItem.setStatusTip("Delete selected item")
|
self.aDeleteItem.setStatusTip("Delete selected project item")
|
||||||
self.aDeleteItem.setShortcut("Ctrl+Del")
|
self.aDeleteItem.setShortcut("Ctrl+Del")
|
||||||
self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None))
|
self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None))
|
||||||
self.projMenu.addAction(self.aDeleteItem)
|
self.projMenu.addAction(self.aDeleteItem)
|
||||||
@@ -287,6 +287,13 @@ class GuiMainMenu(QMenuBar):
|
|||||||
self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1))
|
self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1))
|
||||||
self.projMenu.addAction(self.aMoveDown)
|
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
|
# Project > Empty Trash
|
||||||
self.aEmptyTrash = QAction("Empty Trash", self)
|
self.aEmptyTrash = QAction("Empty Trash", self)
|
||||||
self.aEmptyTrash.setStatusTip("Permanently delete all files in the Trash folder")
|
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.QtCore import Qt, QSize, pyqtSignal
|
||||||
from PyQt5.QtGui import QIcon
|
from PyQt5.QtGui import QIcon
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction
|
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction
|
||||||
)
|
)
|
||||||
|
|
||||||
from nw.core import NWDoc
|
from nw.core import NWDoc
|
||||||
from nw.constants import (
|
from nw.constants import (
|
||||||
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst
|
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst, nwLists
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -68,6 +68,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self._treeMap = {}
|
self._treeMap = {}
|
||||||
self._treeChanged = False
|
self._treeChanged = False
|
||||||
self._timeChanged = 0
|
self._timeChanged = 0
|
||||||
|
self._lastMove = {}
|
||||||
|
|
||||||
##
|
##
|
||||||
# Build GUI
|
# Build GUI
|
||||||
@@ -361,6 +362,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
return False
|
return False
|
||||||
cItem = pItem.takeChild(tIndex)
|
cItem = pItem.takeChild(tIndex)
|
||||||
pItem.insertChild(nIndex, cItem)
|
pItem.insertChild(nIndex, cItem)
|
||||||
|
self._recordLastMove(cItem, pItem, tIndex)
|
||||||
|
|
||||||
self.clearSelection()
|
self.clearSelection()
|
||||||
cItem.setSelected(True)
|
cItem.setSelected(True)
|
||||||
@@ -517,8 +519,9 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
theDoc = NWDoc(self.theProject, self.theParent)
|
theDoc = NWDoc(self.theProject, self.theParent)
|
||||||
theDoc.deleteDocument(tHandle)
|
theDoc.deleteDocument(tHandle)
|
||||||
del self.theProject.projTree[tHandle]
|
|
||||||
self.theIndex.deleteHandle(tHandle)
|
self.theIndex.deleteHandle(tHandle)
|
||||||
|
self._deleteTreeItem(tHandle)
|
||||||
|
self._setTreeChanged(True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# The file is not already in the trash folder, so we
|
# The file is not already in the trash folder, so we
|
||||||
@@ -541,11 +544,12 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
tIndex = trItemP.indexOfChild(trItemS)
|
tIndex = trItemP.indexOfChild(trItemS)
|
||||||
trItemC = trItemP.takeChild(tIndex)
|
trItemC = trItemP.takeChild(tIndex)
|
||||||
trItemT.addChild(trItemC)
|
trItemT.addChild(trItemC)
|
||||||
nwItemS.setParent(self.theProject.projTree.trashRoot())
|
self._updateItemParent(tHandle)
|
||||||
self.propagateCount(tHandle, wCount)
|
self.propagateCount(tHandle, wCount)
|
||||||
|
|
||||||
self._setTreeChanged(True)
|
|
||||||
self.theIndex.deleteHandle(tHandle)
|
self.theIndex.deleteHandle(tHandle)
|
||||||
|
self._recordLastMove(trItemS, trItemP, tIndex)
|
||||||
|
self._setTreeChanged(True)
|
||||||
|
|
||||||
elif nwItemS.itemType == nwItemType.FOLDER:
|
elif nwItemS.itemType == nwItemType.FOLDER:
|
||||||
logger.debug("User requested folder %s deleted" % tHandle)
|
logger.debug("User requested folder %s deleted" % tHandle)
|
||||||
@@ -556,7 +560,8 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
tIndex = trItemP.indexOfChild(trItemS)
|
tIndex = trItemP.indexOfChild(trItemS)
|
||||||
if trItemS.childCount() == 0:
|
if trItemS.childCount() == 0:
|
||||||
trItemP.takeChild(tIndex)
|
trItemP.takeChild(tIndex)
|
||||||
del self.theProject.projTree[tHandle]
|
self._deleteTreeItem(tHandle)
|
||||||
|
self._setTreeChanged(True)
|
||||||
else:
|
else:
|
||||||
self.makeAlert((
|
self.makeAlert((
|
||||||
"Cannot delete folder. It is not empty. "
|
"Cannot delete folder. It is not empty. "
|
||||||
@@ -570,7 +575,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
tIndex = self.indexOfTopLevelItem(trItemS)
|
tIndex = self.indexOfTopLevelItem(trItemS)
|
||||||
if trItemS.childCount() == 0:
|
if trItemS.childCount() == 0:
|
||||||
self.takeTopLevelItem(tIndex)
|
self.takeTopLevelItem(tIndex)
|
||||||
del self.theProject.projTree[tHandle]
|
self._deleteTreeItem(tHandle)
|
||||||
self.theParent.mainMenu.setAvailableRoot()
|
self.theParent.mainMenu.setAvailableRoot()
|
||||||
self._setTreeChanged(True)
|
self._setTreeChanged(True)
|
||||||
else:
|
else:
|
||||||
@@ -679,6 +684,59 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
logger.debug("%d items added to the project tree" % iCount)
|
logger.debug("%d items added to the project tree" % iCount)
|
||||||
return True
|
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):
|
def getSelectedHandle(self):
|
||||||
"""Get the currently selected handle. If multiple items are
|
"""Get the currently selected handle. If multiple items are
|
||||||
selected, return the first.
|
selected, return the first.
|
||||||
@@ -779,6 +837,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
return
|
return
|
||||||
|
|
||||||
sItem = self._getTreeItem(sHandle)
|
sItem = self._getTreeItem(sHandle)
|
||||||
|
pItem = sItem.parent()
|
||||||
dItem = self.itemFromIndex(dIndex)
|
dItem = self.itemFromIndex(dIndex)
|
||||||
dHandle = dItem.data(self.C_NAME, Qt.UserRole)
|
dHandle = dItem.data(self.C_NAME, Qt.UserRole)
|
||||||
snItem = self.theProject.projTree[sHandle]
|
snItem = self.theProject.projTree[sHandle]
|
||||||
@@ -791,11 +850,10 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
isSame = snItem.itemClass == dnItem.itemClass
|
isSame = snItem.itemClass == dnItem.itemClass
|
||||||
isNone = snItem.itemClass == nwItemClass.NO_CLASS
|
isNone = snItem.itemClass == nwItemClass.NO_CLASS
|
||||||
isNote = snItem.itemLayout == nwItemLayout.NOTE
|
isNote = snItem.itemLayout == nwItemLayout.NOTE
|
||||||
onFile = dnItem.itemType == nwItemType.FILE
|
|
||||||
isRoot = snItem.itemType == nwItemType.ROOT
|
isRoot = snItem.itemType == nwItemType.ROOT
|
||||||
onFree = dnItem.itemClass == nwItemClass.ARCHIVE
|
isFile = snItem.itemType == nwItemType.FILE
|
||||||
onFree |= dnItem.itemClass == nwItemClass.TRASH
|
onFile = dnItem.itemType == nwItemType.FILE
|
||||||
onFree &= snItem.itemType == nwItemType.FILE
|
onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile
|
||||||
isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem
|
isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem
|
||||||
if (isSame or isNone or isNote or onFree) and not (onFile and isOnTop) and not isRoot:
|
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)
|
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
|
# and the target is not a free root folder, update its class
|
||||||
if not (isSame or onFree):
|
if not (isSame or onFree):
|
||||||
logger.debug("Item %s class has been changed from %s to %s" % (
|
logger.debug("Item %s class has been changed from %s to %s" % (
|
||||||
sHandle,
|
sHandle, snItem.itemClass.name, dnItem.itemClass.name
|
||||||
snItem.itemClass.name,
|
|
||||||
dnItem.itemClass.name
|
|
||||||
))
|
))
|
||||||
snItem.setClass(dnItem.itemClass)
|
snItem.setClass(dnItem.itemClass)
|
||||||
self.setTreeItemValues(sHandle)
|
self.setTreeItemValues(sHandle)
|
||||||
|
|
||||||
self.propagateCount(sHandle, wCount)
|
self.propagateCount(sHandle, wCount)
|
||||||
|
self._recordLastMove(sItem, pItem, pItem.indexOfChild(sItem))
|
||||||
|
|
||||||
# The items dropped into archive or trash should be removed
|
# The items dropped into archive or trash should be removed
|
||||||
# from the project index, for all other items, we rescan the
|
# from the project index, for all other items, we rescan the
|
||||||
@@ -844,6 +901,13 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
"""
|
"""
|
||||||
return self._treeMap.get(tHandle, None)
|
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):
|
def _scanChildren(self, theList, theItem, theIndex):
|
||||||
"""This is a recursive function returning all items in a tree
|
"""This is a recursive function returning all items in a tree
|
||||||
starting at a given QTreeWidgetItem.
|
starting at a given QTreeWidgetItem.
|
||||||
@@ -984,6 +1048,19 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
|
|
||||||
return
|
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
|
# END Class GuiProjectTree
|
||||||
|
|
||||||
class GuiProjectTreeMenu(QMenu):
|
class GuiProjectTreeMenu(QMenu):
|
||||||
|
|||||||
Reference in New Issue
Block a user