Added code to build the tree context menu based on what element is selected.

This commit is contained in:
Veronica K. B. Olsen
2019-04-14 17:32:39 +02:00
parent 5609d16978
commit cb511b3c45
7 changed files with 444 additions and 45 deletions
+29 -6
View File
@@ -14,10 +14,12 @@ from enum import Enum
class nwItemType(Enum):
NONE = 0
ROOT = 1
FOLDER = 2
FILE = 3
NONE = 0
ROOT = 1
FOLDER = 2
FILE = 3
TRASHFOLDER = 4
TRASHFILE = 5
# END Enum nwItemType
@@ -27,7 +29,28 @@ class nwItemClass(Enum):
NOVEL = 1
CHAPTER = 2
SCENE = 3
CHARACTER = 4
WORLD = 5
PLOT = 4
CHARACTER = 6
WORLD = 6
TIMELINE = 7
OBJECT = 8
# END Enum nwItemClass
class nwItemAction(Enum):
NONE = 0
ADD_ROOT = 1
ADD_FOLDER = 2
ADD_FILE = 3
MOVE_UP = 4
MOVE_DOWN = 5
MOVE_TO = 6
MOVE_TRASH = 7
SPLIT = 8
MERGE = 9
DELETE = 10
DELETE_ROOT = 11
EMPTY_TRASH = 12
# END Enum nwItemAction
+16 -25
View File
@@ -42,10 +42,6 @@ class GuiDocTree(QTreeWidget):
if not self.debugGUI:
self.hideColumn(3)
# Context Menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._openContextMenu)
# Allow Move by Drag & Drop
self.setDragEnabled(True)
self.setDragDropMode(QAbstractItemView.InternalMove)
@@ -78,7 +74,7 @@ class GuiDocTree(QTreeWidget):
tHandle = self.theProject.newFile("New File", nwItemClass.NONE, pHandle)
elif itemType == nwItemType.FOLDER:
if pHandle is None:
logger.error("Failed to add new item.")
logger.error("Failed to add new item")
return
pItem = self.theProject.projTree[rHandle]
if pItem.itemClass == nwItemClass.NOVEL:
@@ -90,7 +86,7 @@ class GuiDocTree(QTreeWidget):
elif itemType == nwItemType.ROOT:
tHandle = self.theProject.newRoot("Root Folder", nwItemClass.NONE)
else:
logger.error("Failed to add new item.")
logger.error("Failed to add new item")
return
# Add the new item to the tree
@@ -114,25 +110,6 @@ class GuiDocTree(QTreeWidget):
]
return retVals
##
# Context Menu
##
def _openContextMenu(self, thePos):
sIDs = self.selectedIndexes()
print(sIDs)
ctxMenu = QMenu(self)
menuNewObject = QAction(QIcon.fromTheme("folder-new"), "New Object", ctxMenu)
# menuNewObject.triggered.connect(self.newProject)
ctxMenu.addAction(menuNewObject)
ctxMenu.exec_(self.viewport().mapToGlobal(thePos))
return
##
# Internal Functions
##
@@ -190,4 +167,18 @@ class GuiDocTree(QTreeWidget):
return selItem[0].text(3)
return None
##
# 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
# END Class GuiDocTree
+230
View File
@@ -0,0 +1,230 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Document Tree Context Menu
novelWriter GUI Document Tree Context Menu
==============================================
Class holding the context menu of the left side document tree view
File History:
Created: 2019-04-14 [0.0.1]
"""
import logging
import nw
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QMenu, QAction
from nw.enum import nwItemType, nwItemClass, nwItemAction
logger = logging.getLogger(__name__)
class GuiDocTreeCtx(QMenu):
def __init__(self, theDocTree, theProject, thePosition):
QMenu.__init__(self, theDocTree)
logger.debug("Initialising DocTree ContextMenu ...")
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theDocTree = theDocTree
self.selAction = None
self.selClass = None
self.selType = None
self.selTarget = None
selItems = self.theDocTree.selectedItems()
if len(selItems) == 0:
self.selHandle = None
else:
self.selHandle = selItems[0].text(3)
print(self.selHandle)
self._buildMenu()
logger.debug("DocTree ContextMenu initialisation complete")
self.exec_(self.theDocTree.viewport().mapToGlobal(thePosition))
return
def _buildMenu(self):
vActs = self.theProject.getActionList(self.selHandle)
if nwItemAction.ADD_ROOT in vActs.keys():
self._buildMenuAddRoot(vActs)
if nwItemAction.ADD_FOLDER in vActs.keys():
self._buildMenuAddFolder(vActs)
if nwItemAction.ADD_FILE in vActs.keys():
self._buildMenuAddFile(vActs)
if nwItemAction.MOVE_UP in vActs.keys():
self._buildMenuMoveUp(vActs)
if nwItemAction.MOVE_DOWN in vActs.keys():
self._buildMenuMoveDown(vActs)
if nwItemAction.MOVE_TRASH in vActs.keys():
self._buildMenuMoveTrash(vActs)
if nwItemAction.MOVE_TO in vActs.keys():
self._buildMenuMoveTo(vActs)
if nwItemAction.SPLIT in vActs.keys():
self._buildMenuSplit(vActs)
if nwItemAction.MERGE in vActs.keys():
self._buildMenuMerge(vActs)
if nwItemAction.DELETE in vActs.keys():
self._buildMenuDelete(vActs)
if nwItemAction.DELETE_ROOT in vActs.keys():
self._buildMenuDeleteRoot(vActs)
if nwItemAction.EMPTY_TRASH in vActs.keys():
self._buildMenuEmptyTrash(vActs)
##
# Build Sub Menus
##
def _buildMenuDelete(self, vActs):
mnuItem = QAction("Delete Permanently", self)
mnuItem.triggered.connect(lambda: self._ctxSignal(
nwItemAction.DELETE, None, None
))
self.addAction(mnuItem)
def _buildMenuDeleteRoot(self, vActs):
mnuItem = QAction("Remove Root", self)
mnuItem.triggered.connect(lambda: self._ctxSignal(
nwItemAction.DELETE_ROOT, None, None
))
self.addAction(mnuItem)
def _buildMenuEmptyTrash(self, vActs):
mnuItem = QAction("Empty Trash", self)
mnuItem.triggered.connect(lambda: self._ctxSignal(
nwItemAction.EMPTY_TRASH, None, None
))
self.addAction(mnuItem)
def _buildMenuSplit(self, vActs):
mnuItem = QAction("Split File", self)
mnuItem.triggered.connect(lambda: self._ctxSignal(
nwItemAction.SPLIT, None, None
))
self.addAction(mnuItem)
def _buildMenuMerge(self, vActs):
mnuItem = QAction("Merge Folder", self)
mnuItem.triggered.connect(lambda: self._ctxSignal(
nwItemAction.MERGE, None, None
))
self.addAction(mnuItem)
def _buildMenuMoveUp(self, vActs):
mnuItem = QAction("Move Up", self)
mnuItem.triggered.connect(lambda: self._ctxSignal(
nwItemAction.MOVE_UP, None, None
))
self.addAction(mnuItem)
def _buildMenuMoveDown(self, vActs):
mnuItem = QAction("Move Down", self)
mnuItem.triggered.connect(lambda: self._ctxSignal(
nwItemAction.MOVE_DOWN, None, None
))
self.addAction(mnuItem)
def _buildMenuMoveTrash(self, vActs):
mnuItem = QAction("Move to Trash", self)
mnuItem.triggered.connect(lambda: self._ctxSignal(
nwItemAction.MOVE_TRASH, None, None
))
self.addAction(mnuItem)
def _buildMenuMoveTo(self, vActs):
mnuItem = QAction("Move to", self)
mnuItem.triggered.connect(lambda: self._ctxSignal(
nwItemAction.MOVE_TO, None, None,
None
))
self.addAction(mnuItem)
def _buildMenuAddFile(self, vActs):
mnuItem = QAction("Add File", self)
mnuItem.triggered.connect(lambda: self._ctxSignal(
nwItemAction.ADD_FILE,
vActs[nwItemAction.ADD_FILE]["Class"],
nwItemType.FILE
))
self.addAction(mnuItem)
def _buildMenuAddFolder(self, vActs):
mnuItem = QAction("Add Folder", self)
mnuItem.triggered.connect(lambda: self._ctxSignal(
nwItemAction.ADD_FOLDER,
vActs[nwItemAction.ADD_FOLDER]["Class"],
nwItemType.FOLDER
))
self.addAction(mnuItem)
def _buildMenuAddRoot(self, vActs):
vClass = vActs[nwItemAction.ADD_ROOT]["Class"]
mnuItem = QMenu("Add Root Item", self)
if nwItemClass.NOVEL in vClass:
mnuSub = QAction("Novel", self)
mnuSub.triggered.connect(lambda: self._ctxSignal(
nwItemAction.ADD_ROOT,
nwItemClass.NOVEL,
nwItemType.ROOT
))
mnuItem.addAction(mnuSub)
if nwItemClass.CHARACTER in vClass:
mnuSub = QAction("Characters", self)
mnuSub.triggered.connect(lambda: self._ctxSignal(
nwItemAction.ADD_ROOT,
nwItemClass.CHARACTER,
nwItemType.ROOT
))
mnuItem.addAction(mnuSub)
if nwItemClass.WORLD in vClass:
mnuSub = QAction("Locations", self)
mnuSub.triggered.connect(lambda: self._ctxSignal(
nwItemAction.ADD_ROOT,
nwItemClass.WORLD,
nwItemType.ROOT
))
mnuItem.addAction(mnuSub)
if nwItemClass.TIMELINE in vClass:
mnuSub = QAction("Timeline", self)
mnuSub.triggered.connect(lambda: self._ctxSignal(
nwItemAction.ADD_ROOT,
nwItemClass.TIMELINE,
nwItemType.ROOT
))
mnuItem.addAction(mnuSub)
if nwItemClass.OBJECT in vClass:
mnuSub = QAction("Objects", self)
mnuSub.triggered.connect(lambda: self._ctxSignal(
nwItemAction.ADD_ROOT,
nwItemClass.OBJECT,
nwItemType.ROOT
))
mnuItem.addAction(mnuSub)
self.addMenu(mnuItem)
return
##
# Menu Signal
##
def _ctxSignal(self, theAction, theClass, theType, theTarget=None):
self.selAction = theAction
self.selClass = theClass
self.selType = theType
self.selTarget = theTarget
return
# END Class GuiDocTreeCtx
+21 -2
View File
@@ -20,6 +20,7 @@ from PyQt5.QtGui import QIcon, QStandardItemModel
from nw.enum import nwItemType
from nw.gui.doctree import GuiDocTree
from nw.gui.doctreectx import GuiDocTreeCtx
from nw.gui.doceditor import GuiDocEditor
from nw.gui.projecteditor import GuiProjectEditor
from nw.project.project import NWProject
@@ -66,6 +67,8 @@ class GuiMain(QMainWindow):
self.setCentralWidget(self.splitMain)
self._buildMenu()
self.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
self.treeView.customContextMenuRequested.connect(self._openDocTreeContextMenu)
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.treeView.buildTree()
@@ -184,9 +187,9 @@ class GuiMain(QMainWindow):
self.setWindowTitle(winTitle)
return True
#
##
# Menu Action
#
##
def _menuExit(self):
self._closeMain()
@@ -197,6 +200,22 @@ class GuiMain(QMainWindow):
self.docTabs.createTab(None,nw.DOCTYPE_ABOUT)
return True
##
# DocTree Context Menu
##
def _openDocTreeContextMenu(self, thePosition):
ctxMenu = GuiDocTreeCtx(self.treeView, self.theProject, thePosition)
selAction = ctxMenu.selAction
selClass = ctxMenu.selClass
selType = ctxMenu.selType
print(selAction, selClass, selType)
return
##
# Events
##
+22 -7
View File
@@ -23,15 +23,19 @@ logger = logging.getLogger(__name__)
class NWItem():
MAXDEPTH = 8
def __init__(self):
self.itemName = ""
self.itemHandle = None
self.parHandle = None
self.itemOrder = None
self.itemType = nwItemType.NONE
self.itemClass = nwItemClass.NONE
self.isExpanded = False
self.itemName = ""
self.itemHandle = None
self.parHandle = None
self.itemOrder = None
self.itemType = nwItemType.NONE
self.itemClass = nwItemClass.NONE
self.itemDepth = None
self.isExpanded = False
self.hasChildren = False
return
@@ -84,6 +88,13 @@ class NWItem():
self.itemClass = nwItemClass.NONE
return
def setDepth(self, theDepth):
if theDepth >= 0 and theDepth <= self.MAXDEPTH:
self.itemDepth = theDepth
else:
logger.error("Invalid item depth %d" % theDepth)
return
def setExpanded(self, expState):
if isinstance(expState, str):
self.isExpanded = expState == str(True)
@@ -91,4 +102,8 @@ class NWItem():
self.isExpanded = expState
return
def setHasChildren(self, hasChildren):
self.hasChildren = hasChildren
return
# END Class NWItem
+110 -1
View File
@@ -19,7 +19,7 @@ from hashlib import sha256
from datetime import datetime
from time import time
from nw.enum import nwItemType, nwItemClass
from nw.enum import nwItemType, nwItemClass, nwItemAction
from nw.project.item import NWItem
logger = logging.getLogger(__name__)
@@ -34,6 +34,7 @@ class NWProject():
# Project Settings
self.projTree = {}
self.treeOrder = []
self.treeRoots = []
self.projPath = None
self.projFile = "nwProject.nwx"
self.projName = ""
@@ -191,8 +192,12 @@ class NWProject():
xItemValue.text = str(nwItem.itemType.name)
xItemValue = etree.SubElement(xItem,"class")
xItemValue.text = str(nwItem.itemClass.name)
xItemValue = etree.SubElement(xItem,"depth")
xItemValue.text = str(nwItem.itemDepth)
xItemValue = etree.SubElement(xItem,"expanded")
xItemValue.text = str(nwItem.isExpanded)
xItemValue = etree.SubElement(xItem,"children")
xItemValue.text = str(nwItem.hasChildren)
# Write the xml tree to file
with open(path.join(self.projPath,self.projFile),"wb") as outFile:
@@ -248,18 +253,122 @@ class NWProject():
logger.error("No tree item with handle %s" % str(tHandle))
return None
def getActionList(self, tHandle):
"""Returns a dictionary of possible actions to perform on a give handle.
"""
validActions = {}
# If we're at root, or didn't select an utem, all we can add is root stuff
if tHandle is None:
validActions[nwItemAction.ADD_ROOT] = {
"Type" : nwItemType.ROOT,
"Class" : [x for x in nwItemClass if self._checkRootUnique(x)]
}
return validActions
# Otherwise, the options depend on the item we had selected
tItem = self.getItem(tHandle)
tType = tItem.itemType
tClass = tItem.itemClass
tDepth = tItem.itemDepth
# Trash items can only be moved or permanently deleted
if tType == nwItemType.TRASHFOLDER or tType == nwItemType.TRASHFILE:
validActions[nwItemAction.MOVE_TO] = {}
validActions[nwItemAction.DELETE] = {}
validActions[nwItemAction.EMPTY_TRASH] = {}
return validActions
# Adding folders or files
# - Novels can only have folders under root
# - Files can only be 1 or 2 levels deep
# - Non-novel roots have only have a restriction on max depth
if tClass == nwItemClass.NOVEL:
if tType == nwItemType.ROOT:
validActions[nwItemAction.ADD_FOLDER] = {
"Type" : nwItemType.FOLDER,
"Class" : nwItemClass.NOVEL
}
if tDepth < 2:
validActions[nwItemAction.ADD_FILE] = {
"Type" : nwItemType.FILE,
"Class" : nwItemClass.NOVEL
}
if tType == nwItemType.FOLDER:
validActions[nwItemAction.MERGE] = {}
if tType == nwItemType.FILE:
validActions[nwItemAction.SPLIT] = {}
else:
if tDepth < NWItem.MAXDEPTH-1 and (
tType == nwItemType.ROOT or tType == nwItemType.FOLDER
):
validActions[nwItemAction.ADD_FOLDER] = {
"Type" : nwItemType.FOLDER,
"Class" : tClass
}
if tDepth < NWItem.MAXDEPTH:
validActions[nwItemAction.ADD_FILE] = {
"Type" : nwItemType.FILE,
"Class" : tClass
}
# Other actions available to novel or plot items
validActions[nwItemAction.MOVE_UP] = {}
validActions[nwItemAction.MOVE_DOWN] = {}
if tType == nwItemType.ROOT:
validActions[nwItemAction.DELETE_ROOT] = {}
else:
validActions[nwItemAction.MOVE_TO] = {}
validActions[nwItemAction.MOVE_TRASH] = {}
return validActions
#
# Internal Functions
#
def _checkRootUnique(self, theClass):
"""Checks if there already is a root entry of class 'theClass' in the
root of the project tree.
"""
for aRoot in self.treeRoots:
if theClass == self.projTree[aRoot].itemClass:
return False
return True
def _countItemDepth(self, tHandle):
theDepth = 0
nwItem = self.getItem(tHandle)
while nwItem.parHandle is not None:
theDepth += 1
nwItem = self.getItem(nwItem.parHandle)
if theDepth > NWItem.MAXDEPTH:
return None
return theDepth
def _appendItem(self, tHandle, pHandle, nwItem):
tHandle = self._checkString(tHandle,self._makeHandle(),False)
pHandle = self._checkString(pHandle,None,True)
logger.verbose("Adding entry %s with parent %s" % (str(tHandle),str(pHandle)))
nwItem.setHandle(tHandle)
nwItem.setParent(pHandle)
self.projTree[tHandle] = nwItem
self.treeOrder.append(tHandle)
if pHandle is not None:
self.projTree[pHandle].setHasChildren(True)
if nwItem.itemType == nwItemType.ROOT:
logger.verbose("Entry %s is a root item" % str(tHandle))
self.treeRoots.append(tHandle)
itemDepth = self._countItemDepth(tHandle)
if itemDepth is None:
logger.error("The depth of entry %s could not be determined" % tHandle)
else:
nwItem.setDepth(itemDepth)
return
def _makeHandle(self, addSeed=""):
+16 -4
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.0.1" fileVersion="1.0" timeStamp="2019-04-13 21:07:45">
<novelWriterXML appVersion="0.0.1" fileVersion="1.0" timeStamp="2019-04-14 16:42:14">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
@@ -11,37 +11,49 @@
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<depth>0</depth>
<expanded>True</expanded>
<children>True</children>
</item>
<item handle="e7ded148d6e4a" order="0" parent="7031beac91f75">
<name>New Chapter</name>
<type>FOLDER</type>
<class>CHAPTER</class>
<class>NOVEL</class>
<depth>1</depth>
<expanded>True</expanded>
<children>True</children>
</item>
<item handle="96b68994dfa3d" order="0" parent="e7ded148d6e4a">
<name>New Scene</name>
<type>FILE</type>
<class>SCENE</class>
<class>NOVEL</class>
<depth>2</depth>
<expanded>False</expanded>
<children>False</children>
</item>
<item handle="636b6aa9b697b" order="1" parent="e7ded148d6e4a">
<name>New File</name>
<type>FILE</type>
<class>SCENE</class>
<class>NOVEL</class>
<depth>2</depth>
<expanded>False</expanded>
<children>False</children>
</item>
<item handle="f6622b4617424" order="1" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<depth>0</depth>
<expanded>False</expanded>
<children>False</children>
</item>
<item handle="73eee34351f85" order="2" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<depth>0</depth>
<expanded>False</expanded>
<children>False</children>
</item>
</content>
</novelWriterXML>