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): class nwItemType(Enum):
NONE = 0 NONE = 0
ROOT = 1 ROOT = 1
FOLDER = 2 FOLDER = 2
FILE = 3 FILE = 3
TRASHFOLDER = 4
TRASHFILE = 5
# END Enum nwItemType # END Enum nwItemType
@@ -27,7 +29,28 @@ class nwItemClass(Enum):
NOVEL = 1 NOVEL = 1
CHAPTER = 2 CHAPTER = 2
SCENE = 3 SCENE = 3
CHARACTER = 4 PLOT = 4
WORLD = 5 CHARACTER = 6
WORLD = 6
TIMELINE = 7
OBJECT = 8
# END Enum nwItemClass # 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: if not self.debugGUI:
self.hideColumn(3) self.hideColumn(3)
# Context Menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._openContextMenu)
# Allow Move by Drag & Drop # Allow Move by Drag & Drop
self.setDragEnabled(True) self.setDragEnabled(True)
self.setDragDropMode(QAbstractItemView.InternalMove) self.setDragDropMode(QAbstractItemView.InternalMove)
@@ -78,7 +74,7 @@ class GuiDocTree(QTreeWidget):
tHandle = self.theProject.newFile("New File", nwItemClass.NONE, pHandle) tHandle = self.theProject.newFile("New File", nwItemClass.NONE, pHandle)
elif itemType == nwItemType.FOLDER: elif itemType == nwItemType.FOLDER:
if pHandle is None: if pHandle is None:
logger.error("Failed to add new item.") logger.error("Failed to add new item")
return return
pItem = self.theProject.projTree[rHandle] pItem = self.theProject.projTree[rHandle]
if pItem.itemClass == nwItemClass.NOVEL: if pItem.itemClass == nwItemClass.NOVEL:
@@ -90,7 +86,7 @@ class GuiDocTree(QTreeWidget):
elif itemType == nwItemType.ROOT: elif itemType == nwItemType.ROOT:
tHandle = self.theProject.newRoot("Root Folder", nwItemClass.NONE) tHandle = self.theProject.newRoot("Root Folder", nwItemClass.NONE)
else: else:
logger.error("Failed to add new item.") logger.error("Failed to add new item")
return return
# Add the new item to the tree # Add the new item to the tree
@@ -114,25 +110,6 @@ class GuiDocTree(QTreeWidget):
] ]
return retVals 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 # Internal Functions
## ##
@@ -190,4 +167,18 @@ class GuiDocTree(QTreeWidget):
return selItem[0].text(3) return selItem[0].text(3)
return None 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 # 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.enum import nwItemType
from nw.gui.doctree import GuiDocTree from nw.gui.doctree import GuiDocTree
from nw.gui.doctreectx import GuiDocTreeCtx
from nw.gui.doceditor import GuiDocEditor from nw.gui.doceditor import GuiDocEditor
from nw.gui.projecteditor import GuiProjectEditor from nw.gui.projecteditor import GuiProjectEditor
from nw.project.project import NWProject from nw.project.project import NWProject
@@ -66,6 +67,8 @@ class GuiMain(QMainWindow):
self.setCentralWidget(self.splitMain) self.setCentralWidget(self.splitMain)
self._buildMenu() self._buildMenu()
self.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
self.treeView.customContextMenuRequested.connect(self._openDocTreeContextMenu)
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.treeView.buildTree() self.treeView.buildTree()
@@ -184,9 +187,9 @@ class GuiMain(QMainWindow):
self.setWindowTitle(winTitle) self.setWindowTitle(winTitle)
return True return True
# ##
# Menu Action # Menu Action
# ##
def _menuExit(self): def _menuExit(self):
self._closeMain() self._closeMain()
@@ -197,6 +200,22 @@ class GuiMain(QMainWindow):
self.docTabs.createTab(None,nw.DOCTYPE_ABOUT) self.docTabs.createTab(None,nw.DOCTYPE_ABOUT)
return True 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 # Events
## ##
+22 -7
View File
@@ -23,15 +23,19 @@ logger = logging.getLogger(__name__)
class NWItem(): class NWItem():
MAXDEPTH = 8
def __init__(self): def __init__(self):
self.itemName = "" self.itemName = ""
self.itemHandle = None self.itemHandle = None
self.parHandle = None self.parHandle = None
self.itemOrder = None self.itemOrder = None
self.itemType = nwItemType.NONE self.itemType = nwItemType.NONE
self.itemClass = nwItemClass.NONE self.itemClass = nwItemClass.NONE
self.isExpanded = False self.itemDepth = None
self.isExpanded = False
self.hasChildren = False
return return
@@ -84,6 +88,13 @@ class NWItem():
self.itemClass = nwItemClass.NONE self.itemClass = nwItemClass.NONE
return 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): def setExpanded(self, expState):
if isinstance(expState, str): if isinstance(expState, str):
self.isExpanded = expState == str(True) self.isExpanded = expState == str(True)
@@ -91,4 +102,8 @@ class NWItem():
self.isExpanded = expState self.isExpanded = expState
return return
def setHasChildren(self, hasChildren):
self.hasChildren = hasChildren
return
# END Class NWItem # END Class NWItem
+110 -1
View File
@@ -19,7 +19,7 @@ from hashlib import sha256
from datetime import datetime from datetime import datetime
from time import time from time import time
from nw.enum import nwItemType, nwItemClass from nw.enum import nwItemType, nwItemClass, nwItemAction
from nw.project.item import NWItem from nw.project.item import NWItem
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -34,6 +34,7 @@ class NWProject():
# Project Settings # Project Settings
self.projTree = {} self.projTree = {}
self.treeOrder = [] self.treeOrder = []
self.treeRoots = []
self.projPath = None self.projPath = None
self.projFile = "nwProject.nwx" self.projFile = "nwProject.nwx"
self.projName = "" self.projName = ""
@@ -191,8 +192,12 @@ class NWProject():
xItemValue.text = str(nwItem.itemType.name) xItemValue.text = str(nwItem.itemType.name)
xItemValue = etree.SubElement(xItem,"class") xItemValue = etree.SubElement(xItem,"class")
xItemValue.text = str(nwItem.itemClass.name) xItemValue.text = str(nwItem.itemClass.name)
xItemValue = etree.SubElement(xItem,"depth")
xItemValue.text = str(nwItem.itemDepth)
xItemValue = etree.SubElement(xItem,"expanded") xItemValue = etree.SubElement(xItem,"expanded")
xItemValue.text = str(nwItem.isExpanded) xItemValue.text = str(nwItem.isExpanded)
xItemValue = etree.SubElement(xItem,"children")
xItemValue.text = str(nwItem.hasChildren)
# Write the xml tree to file # Write the xml tree to file
with open(path.join(self.projPath,self.projFile),"wb") as outFile: 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)) logger.error("No tree item with handle %s" % str(tHandle))
return None 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 # 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): def _appendItem(self, tHandle, pHandle, nwItem):
tHandle = self._checkString(tHandle,self._makeHandle(),False) tHandle = self._checkString(tHandle,self._makeHandle(),False)
pHandle = self._checkString(pHandle,None,True) pHandle = self._checkString(pHandle,None,True)
logger.verbose("Adding entry %s with parent %s" % (str(tHandle),str(pHandle))) logger.verbose("Adding entry %s with parent %s" % (str(tHandle),str(pHandle)))
nwItem.setHandle(tHandle) nwItem.setHandle(tHandle)
nwItem.setParent(pHandle) nwItem.setParent(pHandle)
self.projTree[tHandle] = nwItem self.projTree[tHandle] = nwItem
self.treeOrder.append(tHandle) 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 return
def _makeHandle(self, addSeed=""): def _makeHandle(self, addSeed=""):
+16 -4
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?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> <project>
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
@@ -11,37 +11,49 @@
<name>Novel</name> <name>Novel</name>
<type>ROOT</type> <type>ROOT</type>
<class>NOVEL</class> <class>NOVEL</class>
<depth>0</depth>
<expanded>True</expanded> <expanded>True</expanded>
<children>True</children>
</item> </item>
<item handle="e7ded148d6e4a" order="0" parent="7031beac91f75"> <item handle="e7ded148d6e4a" order="0" parent="7031beac91f75">
<name>New Chapter</name> <name>New Chapter</name>
<type>FOLDER</type> <type>FOLDER</type>
<class>CHAPTER</class> <class>NOVEL</class>
<depth>1</depth>
<expanded>True</expanded> <expanded>True</expanded>
<children>True</children>
</item> </item>
<item handle="96b68994dfa3d" order="0" parent="e7ded148d6e4a"> <item handle="96b68994dfa3d" order="0" parent="e7ded148d6e4a">
<name>New Scene</name> <name>New Scene</name>
<type>FILE</type> <type>FILE</type>
<class>SCENE</class> <class>NOVEL</class>
<depth>2</depth>
<expanded>False</expanded> <expanded>False</expanded>
<children>False</children>
</item> </item>
<item handle="636b6aa9b697b" order="1" parent="e7ded148d6e4a"> <item handle="636b6aa9b697b" order="1" parent="e7ded148d6e4a">
<name>New File</name> <name>New File</name>
<type>FILE</type> <type>FILE</type>
<class>SCENE</class> <class>NOVEL</class>
<depth>2</depth>
<expanded>False</expanded> <expanded>False</expanded>
<children>False</children>
</item> </item>
<item handle="f6622b4617424" order="1" parent="None"> <item handle="f6622b4617424" order="1" parent="None">
<name>Characters</name> <name>Characters</name>
<type>ROOT</type> <type>ROOT</type>
<class>CHARACTER</class> <class>CHARACTER</class>
<depth>0</depth>
<expanded>False</expanded> <expanded>False</expanded>
<children>False</children>
</item> </item>
<item handle="73eee34351f85" order="2" parent="None"> <item handle="73eee34351f85" order="2" parent="None">
<name>World</name> <name>World</name>
<type>ROOT</type> <type>ROOT</type>
<class>WORLD</class> <class>WORLD</class>
<depth>0</depth>
<expanded>False</expanded> <expanded>False</expanded>
<children>False</children>
</item> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>