275 lines
8.6 KiB
Python
275 lines
8.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""novelWriter GUI Project Outline
|
||
|
||
novelWriter – GUI Project Outline
|
||
===================================
|
||
Class holding the project outline view
|
||
|
||
File History:
|
||
Created: 2019-11-16 [0.4.1]
|
||
|
||
"""
|
||
|
||
import logging
|
||
import nw
|
||
|
||
from os import path
|
||
from time import time
|
||
|
||
from PyQt5.QtCore import Qt, QByteArray
|
||
from PyQt5.QtWidgets import (
|
||
QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem
|
||
)
|
||
|
||
from nw.constants import nwKeyWords, nwLabels
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class GuiProjectOutline(QWidget):
|
||
|
||
I_TITLE = 0
|
||
I_LEVEL = 1
|
||
I_LABEL = 2
|
||
I_LINE = 3
|
||
I_WCOUNT = 4
|
||
I_CCOUNT = 5
|
||
I_PCOUNT = 6
|
||
I_SYNOP = 7
|
||
I_POV = 8
|
||
I_CHAR = 9
|
||
I_PLOT = 10
|
||
I_TIME = 11
|
||
I_WORLD = 12
|
||
I_OBJECT = 13
|
||
I_ENTITY = 14
|
||
I_CUSTOM = 15
|
||
|
||
COL_MAX = 15
|
||
|
||
COL_LABELS = {
|
||
I_TITLE : "Title",
|
||
I_LEVEL : "Level",
|
||
I_LABEL : "Document",
|
||
I_LINE : "Line",
|
||
I_WCOUNT : "Words",
|
||
I_CCOUNT : "Chars",
|
||
I_PCOUNT : "Pars",
|
||
I_SYNOP : "Synopsis",
|
||
I_POV : "POV",
|
||
I_CHAR : nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY],
|
||
I_PLOT : nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY],
|
||
I_TIME : nwLabels.KEY_NAME[nwKeyWords.TIME_KEY],
|
||
I_WORLD : nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY],
|
||
I_OBJECT : nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY],
|
||
I_ENTITY : nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY],
|
||
I_CUSTOM : nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY],
|
||
}
|
||
|
||
def __init__(self, theParent, theProject):
|
||
QWidget.__init__(self, theParent)
|
||
|
||
logger.debug("Initialising ProjectOutline ...")
|
||
|
||
self.mainConf = nw.CONFIG
|
||
self.theParent = theParent
|
||
self.theProject = theProject
|
||
self.theIndex = self.theParent.theIndex
|
||
self.optState = self.theProject.optState
|
||
|
||
self.firstView = True
|
||
|
||
self.outerBox = QVBoxLayout()
|
||
self.mainTree = QTreeWidget()
|
||
self.lastBuild = 0
|
||
self.treeMap = {}
|
||
self.treeCols = {
|
||
"order" : [
|
||
self.I_TITLE, self.I_LABEL,
|
||
self.I_WCOUNT, self.I_POV,
|
||
self.I_CHAR, self.I_PLOT,
|
||
self.I_WORLD, self.I_SYNOP
|
||
],
|
||
"width" : [150, 100, 80, 100, 100, 100, 100, 300],
|
||
}
|
||
self.colIndex = {}
|
||
|
||
self.outerBox.addWidget(self.mainTree)
|
||
self.outerBox.setContentsMargins(0,0,0,0)
|
||
self.setLayout(self.outerBox)
|
||
|
||
logger.debug("ProjectOutline initialisation complete")
|
||
|
||
return
|
||
|
||
def refreshTree(self):
|
||
"""Called whenever the Outline tab is activated and controls
|
||
what data to load, and if necessary, force a rebuild of the
|
||
tree.
|
||
"""
|
||
|
||
if self.firstView:
|
||
self._loadHeaderState()
|
||
self._populateTree()
|
||
|
||
self.firstView = False
|
||
|
||
return
|
||
|
||
def closeOutline(self):
|
||
"""Called before a project is closed.
|
||
"""
|
||
|
||
self._saveHeaderState()
|
||
self.mainTree.clear()
|
||
self.firstView = True
|
||
|
||
return
|
||
|
||
##
|
||
# Internal Functions
|
||
##
|
||
|
||
def _saveHeaderState(self):
|
||
|
||
colW = []
|
||
for iCol in range(self.mainTree.columnCount()):
|
||
colW.append(self.mainTree.columnWidth(iCol))
|
||
|
||
self.treeCols["width"] = colW
|
||
self.optState.setValue("GuiProjectOutline", "headState", self.treeCols)
|
||
self.optState.saveSettings()
|
||
return
|
||
|
||
def _loadHeaderState(self):
|
||
|
||
treeCols = self.optState.getValue("GuiProjectOutline", "headState", {})
|
||
|
||
if "order" not in treeCols.keys(): return
|
||
if not isinstance(treeCols["order"], list): return
|
||
if len(treeCols["order"]) == 0: return
|
||
|
||
self.treeCols["order"] = []
|
||
for colID in treeCols["order"]:
|
||
if colID >= 0 and colID <= self.COL_MAX:
|
||
self.treeCols["order"].append(colID)
|
||
|
||
if "width" in treeCols.keys():
|
||
if isinstance(treeCols["width"],list):
|
||
self.treeCols["width"] = treeCols["width"]
|
||
|
||
return
|
||
|
||
def _populateTree(self):
|
||
|
||
theLabels = []
|
||
for i, n in enumerate(self.treeCols["order"]):
|
||
theLabels.append(self.COL_LABELS[n])
|
||
self.colIndex[n] = i
|
||
|
||
self.mainTree.clear()
|
||
self.mainTree.setHeaderLabels(theLabels)
|
||
for n, colW in enumerate(self.treeCols["width"]):
|
||
self.mainTree.setColumnWidth(n,colW)
|
||
|
||
treeHead = self.mainTree.headerItem()
|
||
if self.I_CCOUNT in self.colIndex:
|
||
treeHead.setTextAlignment(self.colIndex[self.I_CCOUNT],Qt.AlignRight)
|
||
if self.I_WCOUNT in self.colIndex:
|
||
treeHead.setTextAlignment(self.colIndex[self.I_WCOUNT],Qt.AlignRight)
|
||
if self.I_PCOUNT in self.colIndex:
|
||
treeHead.setTextAlignment(self.colIndex[self.I_PCOUNT],Qt.AlignRight)
|
||
|
||
currTitle = None
|
||
currChapter = None
|
||
currScene = None
|
||
|
||
for titleKey in self.theIndex.getNovelStructure():
|
||
|
||
if len(titleKey) < 16:
|
||
continue
|
||
|
||
tHandle = titleKey[:13]
|
||
sTitle = titleKey[14:]
|
||
|
||
if tHandle not in self.theIndex.novelIndex:
|
||
continue
|
||
if sTitle not in self.theIndex.novelIndex[tHandle]:
|
||
continue
|
||
|
||
tLevel = self.theIndex.novelIndex[tHandle][sTitle]["level"]
|
||
tTime = self.theIndex.novelIndex[tHandle][sTitle]["updated"]
|
||
tItem = self._createTreeItem(tHandle, sTitle)
|
||
self.treeMap[titleKey] = tItem
|
||
|
||
if tLevel == "H1":
|
||
currTitle = tItem
|
||
self.mainTree.addTopLevelItem(tItem)
|
||
elif tLevel == "H2":
|
||
if currTitle is None:
|
||
self.mainTree.addTopLevelItem(tItem)
|
||
else:
|
||
currTitle.addChild(tItem)
|
||
currChapter = tItem
|
||
elif tLevel == "H3":
|
||
if currChapter is None:
|
||
if currTitle is None:
|
||
self.mainTree.addTopLevelItem(tItem)
|
||
else:
|
||
currTitle.addChild(tItem)
|
||
else:
|
||
currChapter.addChild(tItem)
|
||
currScene = tItem
|
||
elif tLevel == "H4":
|
||
if currScene is None:
|
||
if currChapter is None:
|
||
if currTitle is None:
|
||
self.mainTree.addTopLevelItem(tItem)
|
||
else:
|
||
currTitle.addChild(tItem)
|
||
else:
|
||
currChapter.addChild(tItem)
|
||
else:
|
||
currScene.addChild(tItem)
|
||
|
||
tItem.setExpanded(True)
|
||
|
||
self.lastBuild = time()
|
||
|
||
return
|
||
|
||
def _createTreeItem(self, tHandle, sTitle):
|
||
|
||
nwItem = self.theProject.getItem(tHandle)
|
||
novIdx = self.theIndex.novelIndex[tHandle][sTitle]
|
||
|
||
newItem = QTreeWidgetItem()
|
||
self._setItemText(newItem, self.I_TITLE, novIdx["title"])
|
||
self._setItemText(newItem, self.I_LEVEL, novIdx["level"])
|
||
self._setItemText(newItem, self.I_LABEL, nwItem.itemName)
|
||
self._setItemText(newItem, self.I_LINE, sTitle[1:])
|
||
self._setItemText(newItem, self.I_SYNOP, novIdx["synopsis"])
|
||
self._setItemText(newItem, self.I_CCOUNT, str(novIdx["cCount"]), True)
|
||
self._setItemText(newItem, self.I_WCOUNT, str(novIdx["wCount"]), True)
|
||
self._setItemText(newItem, self.I_PCOUNT, str(novIdx["pCount"]), True)
|
||
|
||
theRefs = self.theIndex.getReferences(tHandle, sTitle)
|
||
self._setItemText(newItem, self.I_POV, ", ".join(theRefs[nwKeyWords.POV_KEY]))
|
||
self._setItemText(newItem, self.I_CHAR, ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
|
||
self._setItemText(newItem, self.I_PLOT, ", ".join(theRefs[nwKeyWords.PLOT_KEY]))
|
||
self._setItemText(newItem, self.I_TIME, ", ".join(theRefs[nwKeyWords.TIME_KEY]))
|
||
self._setItemText(newItem, self.I_WORLD, ", ".join(theRefs[nwKeyWords.WORLD_KEY]))
|
||
self._setItemText(newItem, self.I_OBJECT, ", ".join(theRefs[nwKeyWords.OBJECT_KEY]))
|
||
self._setItemText(newItem, self.I_ENTITY, ", ".join(theRefs[nwKeyWords.ENTITY_KEY]))
|
||
self._setItemText(newItem, self.I_CUSTOM, ", ".join(theRefs[nwKeyWords.CUSTOM_KEY]))
|
||
|
||
return newItem
|
||
|
||
def _setItemText(self, tItem, colID, theText, rAlign=False):
|
||
if colID in self.colIndex:
|
||
tItem.setText(self.colIndex[colID], theText)
|
||
if rAlign:
|
||
tItem.setTextAlignment(self.colIndex[colID],Qt.AlignRight)
|
||
return
|
||
|
||
# END Class GuiProjectOutline
|