Added project tree context menu
This commit is contained in:
+140
-67
@@ -6,7 +6,8 @@
|
|||||||
Class holding the left side document tree view
|
Class holding the left side document tree view
|
||||||
|
|
||||||
File History:
|
File History:
|
||||||
Created: 2018-09-29 [0.0.1]
|
Created: 2018-09-29 [0.0.1] GuiProjectTree
|
||||||
|
Created: 2020-06-04 [0.7.0] GuiProjectTreeMenu
|
||||||
|
|
||||||
This file is a part of novelWriter
|
This file is a part of novelWriter
|
||||||
Copyright 2020, Veronica Berglyd Olsen
|
Copyright 2020, Veronica Berglyd Olsen
|
||||||
@@ -32,7 +33,7 @@ from PyQt5.QtCore import Qt, QSize
|
|||||||
from PyQt5.QtGui import QFont, QColor, QIcon
|
from PyQt5.QtGui import QFont, QColor, QIcon
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMessageBox,
|
qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMessageBox,
|
||||||
QHeaderView
|
QHeaderView, QMenu, QAction
|
||||||
)
|
)
|
||||||
|
|
||||||
from nw.core import NWDoc
|
from nw.core import NWDoc
|
||||||
@@ -57,6 +58,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self.theParent = theParent
|
self.theParent = theParent
|
||||||
self.theTheme = theParent.theTheme
|
self.theTheme = theParent.theTheme
|
||||||
self.theProject = theProject
|
self.theProject = theProject
|
||||||
|
self.ctxMenu = GuiProjectTreeMenu(self)
|
||||||
|
|
||||||
# Tree Settings
|
# Tree Settings
|
||||||
self.theMap = None
|
self.theMap = None
|
||||||
@@ -71,6 +73,8 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self.setIndentation(iPx)
|
self.setIndentation(iPx)
|
||||||
self.setColumnCount(4)
|
self.setColumnCount(4)
|
||||||
self.setHeaderLabels(["Label", "Words", "Inc", "Flags"])
|
self.setHeaderLabels(["Label", "Words", "Inc", "Flags"])
|
||||||
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
|
self.customContextMenuRequested.connect(self._rightClickMenu)
|
||||||
|
|
||||||
treeHeadItem = self.headerItem()
|
treeHeadItem = self.headerItem()
|
||||||
treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
|
treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
|
||||||
@@ -556,6 +560,83 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
##
|
||||||
|
# Slots
|
||||||
|
##
|
||||||
|
|
||||||
|
def _rightClickMenu(self, clickPos):
|
||||||
|
"""The user right clicked an element in the project tree, so we
|
||||||
|
open a context menu in-place.
|
||||||
|
"""
|
||||||
|
selItem = self.itemAt(clickPos)
|
||||||
|
self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Events
|
||||||
|
##
|
||||||
|
|
||||||
|
def mousePressEvent(self, theEvent):
|
||||||
|
"""Overload mousePressEvent to clear selection if clicking the
|
||||||
|
mouse in a blank area of the tree view.
|
||||||
|
"""
|
||||||
|
QTreeWidget.mousePressEvent(self, theEvent)
|
||||||
|
selItem = self.indexAt(theEvent.pos())
|
||||||
|
if not selItem.isValid():
|
||||||
|
self.clearSelection()
|
||||||
|
return
|
||||||
|
|
||||||
|
def dropEvent(self, theEvent):
|
||||||
|
"""Overload the drop of dragged item event to check whether the
|
||||||
|
drop is allowed or not. Disallowed drops are cancelled.
|
||||||
|
"""
|
||||||
|
sHandle = self.getSelectedHandle()
|
||||||
|
if sHandle is None:
|
||||||
|
logger.error("No handle selected")
|
||||||
|
return
|
||||||
|
|
||||||
|
dIndex = self.indexAt(theEvent.pos())
|
||||||
|
if not dIndex.isValid():
|
||||||
|
logger.error("Invalid drop index")
|
||||||
|
return
|
||||||
|
|
||||||
|
dItem = self.itemFromIndex(dIndex)
|
||||||
|
dHandle = dItem.data(self.C_NAME, Qt.UserRole)
|
||||||
|
snItem = self.theProject.projTree[sHandle]
|
||||||
|
dnItem = self.theProject.projTree[dHandle]
|
||||||
|
if dnItem is None:
|
||||||
|
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
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
|
||||||
|
onRoot = dnItem.itemType == nwItemType.ROOT
|
||||||
|
isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem
|
||||||
|
if (isSame or isNone or isNote) and not (onFile and isOnTop) and not isRoot:
|
||||||
|
logger.debug("Drag'n'drop of item %s accepted" % sHandle)
|
||||||
|
QTreeWidget.dropEvent(self, theEvent)
|
||||||
|
if isNone:
|
||||||
|
self._moveOrphanedItem(sHandle, dHandle)
|
||||||
|
self._cleanOrphanedRoot()
|
||||||
|
else:
|
||||||
|
self._updateItemParent(sHandle)
|
||||||
|
if not isSame:
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
logger.debug("Drag'n'drop of item %s not accepted" % sHandle)
|
||||||
|
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
@@ -717,69 +798,61 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
self.theProject.setProjectChanged(True)
|
self.theProject.setProjectChanged(True)
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
|
||||||
# Event Overloading
|
|
||||||
##
|
|
||||||
|
|
||||||
def mousePressEvent(self, theEvent):
|
|
||||||
"""Overload mousePressEvent to clear selection if clicking the
|
|
||||||
mouse in a blank area of the tree view.
|
|
||||||
"""
|
|
||||||
QTreeWidget.mousePressEvent(self, theEvent)
|
|
||||||
selItem = self.indexAt(theEvent.pos())
|
|
||||||
if not selItem.isValid():
|
|
||||||
self.clearSelection()
|
|
||||||
return
|
|
||||||
|
|
||||||
def dropEvent(self, theEvent):
|
|
||||||
"""Overload the drop of dragged item event to check whether the
|
|
||||||
drop is allowed or not. Disallowed drops are cancelled.
|
|
||||||
"""
|
|
||||||
sHandle = self.getSelectedHandle()
|
|
||||||
if sHandle is None:
|
|
||||||
logger.error("No handle selected")
|
|
||||||
return
|
|
||||||
|
|
||||||
dIndex = self.indexAt(theEvent.pos())
|
|
||||||
if not dIndex.isValid():
|
|
||||||
logger.error("Invalid drop index")
|
|
||||||
return
|
|
||||||
|
|
||||||
dItem = self.itemFromIndex(dIndex)
|
|
||||||
dHandle = dItem.data(self.C_NAME, Qt.UserRole)
|
|
||||||
snItem = self.theProject.projTree[sHandle]
|
|
||||||
dnItem = self.theProject.projTree[dHandle]
|
|
||||||
if dnItem is None:
|
|
||||||
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
|
|
||||||
return
|
|
||||||
|
|
||||||
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
|
|
||||||
onRoot = dnItem.itemType == nwItemType.ROOT
|
|
||||||
isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem
|
|
||||||
if (isSame or isNone or isNote) and not (onFile and isOnTop) and not isRoot:
|
|
||||||
logger.debug("Drag'n'drop of item %s accepted" % sHandle)
|
|
||||||
QTreeWidget.dropEvent(self, theEvent)
|
|
||||||
if isNone:
|
|
||||||
self._moveOrphanedItem(sHandle, dHandle)
|
|
||||||
self._cleanOrphanedRoot()
|
|
||||||
else:
|
|
||||||
self._updateItemParent(sHandle)
|
|
||||||
if not isSame:
|
|
||||||
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)
|
|
||||||
else:
|
|
||||||
logger.debug("Drag'n'drop of item %s not accepted" % sHandle)
|
|
||||||
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
# END Class GuiProjectTree
|
# END Class GuiProjectTree
|
||||||
|
|
||||||
|
class GuiProjectTreeMenu(QMenu):
|
||||||
|
|
||||||
|
def __init__(self, theTree):
|
||||||
|
QMenu.__init__(self, theTree)
|
||||||
|
|
||||||
|
self.theTree = theTree
|
||||||
|
self.theItem = theItem
|
||||||
|
|
||||||
|
self.editItem = QAction("Edit Item", self)
|
||||||
|
self.editItem.triggered.connect(self._doEditItem)
|
||||||
|
self.addAction(self.editItem)
|
||||||
|
|
||||||
|
self.toggleExp = QAction("Toggle Exported", self)
|
||||||
|
self.toggleExp.triggered.connect(self._doToggleExported)
|
||||||
|
self.addAction(self.toggleExp)
|
||||||
|
|
||||||
|
self.addSeparator()
|
||||||
|
|
||||||
|
self.newFolder = QAction("New Folder", self)
|
||||||
|
self.newFolder.triggered.connect(self._doMakeFolder)
|
||||||
|
self.addAction(self.newFolder)
|
||||||
|
|
||||||
|
self.newFile = QAction("New File", self)
|
||||||
|
self.newFile.triggered.connect(self._doMakeFile)
|
||||||
|
self.addAction(self.newFile)
|
||||||
|
|
||||||
|
self.deleteItem = QAction("Delete Item", self)
|
||||||
|
self.deleteItem.triggered.connect(self._doDeleteItem)
|
||||||
|
self.addAction(self.deleteItem)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def updateFromItem(self, theItem):
|
||||||
|
self.theItem = theItem
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Slots
|
||||||
|
##
|
||||||
|
|
||||||
|
def _doEditItem(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
def _doDeleteItem(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
def _doMakeFolder(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
def _doMakeFile(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
def _doToggleExported(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
# END Class GuiProjectTreeMenu
|
||||||
|
|||||||
Reference in New Issue
Block a user