333 lines
11 KiB
Python
333 lines
11 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 enum import Enum
|
||
|
||
from PyQt5.QtCore import Qt, QByteArray
|
||
from PyQt5.QtWidgets import (
|
||
QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QMenu, QAction,
|
||
QAbstractItemView
|
||
)
|
||
|
||
from nw.constants import nwKeyWords, nwLabels
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class HCols(Enum):
|
||
|
||
TITLE = 0
|
||
LEVEL = 1
|
||
LABEL = 2
|
||
LINE = 3
|
||
WCOUNT = 4
|
||
CCOUNT = 5
|
||
PCOUNT = 6
|
||
SYNOP = 7
|
||
POV = 8
|
||
CHAR = 9
|
||
PLOT = 10
|
||
TIME = 11
|
||
WORLD = 12
|
||
OBJECT = 13
|
||
ENTITY = 14
|
||
CUSTOM = 15
|
||
|
||
# END Enum HCols
|
||
|
||
class GuiProjectOutline(QTreeWidget):
|
||
|
||
COL_LABELS = {
|
||
HCols.TITLE : "Title",
|
||
HCols.LEVEL : "Level",
|
||
HCols.LABEL : "Document",
|
||
HCols.LINE : "Line",
|
||
HCols.WCOUNT : "Words",
|
||
HCols.CCOUNT : "Chars",
|
||
HCols.PCOUNT : "Pars",
|
||
HCols.POV : "POV",
|
||
HCols.CHAR : nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY],
|
||
HCols.PLOT : nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY],
|
||
HCols.TIME : nwLabels.KEY_NAME[nwKeyWords.TIME_KEY],
|
||
HCols.WORLD : nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY],
|
||
HCols.OBJECT : nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY],
|
||
HCols.ENTITY : nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY],
|
||
HCols.CUSTOM : nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY],
|
||
HCols.SYNOP : "Synopsis",
|
||
}
|
||
|
||
def __init__(self, theParent, theProject):
|
||
QTreeWidget.__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.headerMenu = GuiOutlineHeaderMenu(self)
|
||
|
||
self.firstView = True
|
||
self.lastBuild = 0
|
||
|
||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||
self.setExpandsOnDoubleClick(False)
|
||
self.setDragEnabled(False)
|
||
self.itemDoubleClicked.connect(self._treeDoubleClick)
|
||
|
||
self.treeHead = self.header()
|
||
self.treeHead.setContextMenuPolicy(Qt.CustomContextMenu)
|
||
self.treeHead.customContextMenuRequested.connect(self._headerRightClick)
|
||
self.treeHead.sectionMoved.connect(self._columnMoved)
|
||
|
||
self.treeMap = {}
|
||
self.treeOrder = self.COL_LABELS.keys()
|
||
self.treeNCols = len(self.treeOrder)
|
||
self.treeWidth = [150]*self.treeNCols
|
||
self.colIndex = {}
|
||
|
||
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.clear()
|
||
self.firstView = True
|
||
return
|
||
|
||
##
|
||
# Slots
|
||
##
|
||
|
||
def _treeDoubleClick(self, tItem, tCol):
|
||
print(tItem, tCol)
|
||
return
|
||
|
||
def _headerRightClick(self, clickPos):
|
||
print(clickPos)
|
||
globPos = self.mapToGlobal(clickPos)
|
||
print(globPos)
|
||
return
|
||
|
||
def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx):
|
||
"""Make sure the order and width read from settings file, or
|
||
with default values, is kept up-to-date when columns are moved
|
||
around. Otherwise, the original order will be restored on a tree
|
||
rebuild.
|
||
"""
|
||
self.treeOrder.insert(newVisualIdx, self.treeOrder.pop(oldVisualIdx))
|
||
self.treeWidth.insert(newVisualIdx, self.treeWidth.pop(oldVisualIdx))
|
||
return
|
||
|
||
##
|
||
# Internal Functions
|
||
##
|
||
|
||
def _loadHeaderState(self):
|
||
"""Load the state of the main tree header, that is, column order
|
||
and column width.
|
||
"""
|
||
|
||
# Load whatever we saved last time, regardless of wether it
|
||
# contains the correct names or number of columns.
|
||
keysOrder = self.COL_LABELS.keys()
|
||
tempOrder = self.optState.getValue("GuiProjectOutline", "headerOrder", keysOrder)
|
||
treeOrder = []
|
||
for hName in tempOrder:
|
||
for hItem in HCols:
|
||
if hItem.name == hName:
|
||
treeOrder.append(hItem)
|
||
|
||
# Add columns that were not in tempOrder to treeOrder, but in
|
||
# the default column order.
|
||
for cItem in keysOrder:
|
||
if cItem not in treeOrder:
|
||
treeOrder.append(cItem)
|
||
|
||
# Check that we now have a complete list, and only if so, save
|
||
# the order loaded from file. Otherwise, we keep the default.
|
||
if len(treeOrder) == self.treeNCols:
|
||
self.treeOrder = treeOrder
|
||
else:
|
||
logger.error("Failed to extract outline column order from previous session")
|
||
logger.error("Column count doesn't match %d != %d" % (len(treeOrder), self.treeNCols))
|
||
|
||
# The columns widths we just fill whatever we've got, and append
|
||
# the rest with defaults, and truncate to desired length.
|
||
tempWidth = self.optState.getValue("GuiProjectOutline", "headerWidth", [])
|
||
treeWidth = [int(w) for w in tempWidth]
|
||
self.treeWidth = (treeWidth + self.treeWidth)[0:self.treeNCols]
|
||
|
||
return
|
||
|
||
def _saveHeaderState(self):
|
||
"""Save the state of the main tree header, that is, column order
|
||
and column width.
|
||
"""
|
||
|
||
treeWidth = []
|
||
treeOrder = []
|
||
for iCol in range(self.columnCount()):
|
||
treeOrder.append(self.treeOrder[iCol].name)
|
||
iLog = self.treeHead.logicalIndex(iCol)
|
||
treeWidth.append(self.columnWidth(iLog))
|
||
|
||
self.optState.setValue("GuiProjectOutline", "headerOrder", treeOrder)
|
||
self.optState.setValue("GuiProjectOutline", "headerWidth", treeWidth)
|
||
self.optState.saveSettings()
|
||
|
||
return
|
||
|
||
def _populateTree(self):
|
||
"""Build the tree based on the project index.
|
||
"""
|
||
|
||
theLabels = []
|
||
for i, hItem in enumerate(self.treeOrder):
|
||
theLabels.append(self.COL_LABELS[hItem])
|
||
self.colIndex[hItem] = i
|
||
|
||
self.clear()
|
||
self.setHeaderLabels(theLabels)
|
||
for n, colW in enumerate(self.treeWidth):
|
||
self.setColumnWidth(n,colW)
|
||
|
||
headItem = self.headerItem()
|
||
headItem.setTextAlignment(self.colIndex[HCols.CCOUNT],Qt.AlignRight)
|
||
headItem.setTextAlignment(self.colIndex[HCols.WCOUNT],Qt.AlignRight)
|
||
headItem.setTextAlignment(self.colIndex[HCols.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.addTopLevelItem(tItem)
|
||
elif tLevel == "H2":
|
||
if currTitle is None:
|
||
self.addTopLevelItem(tItem)
|
||
else:
|
||
currTitle.addChild(tItem)
|
||
currChapter = tItem
|
||
elif tLevel == "H3":
|
||
if currChapter is None:
|
||
if currTitle is None:
|
||
self.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.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):
|
||
"""Populate a tree item with all the column values.
|
||
"""
|
||
|
||
nwItem = self.theProject.getItem(tHandle)
|
||
novIdx = self.theIndex.novelIndex[tHandle][sTitle]
|
||
|
||
newItem = QTreeWidgetItem()
|
||
|
||
newItem.setText(self.colIndex[HCols.TITLE], novIdx["title"])
|
||
newItem.setText(self.colIndex[HCols.LEVEL], novIdx["level"])
|
||
newItem.setText(self.colIndex[HCols.LABEL], nwItem.itemName)
|
||
newItem.setText(self.colIndex[HCols.LINE], sTitle[1:])
|
||
newItem.setText(self.colIndex[HCols.SYNOP], novIdx["synopsis"])
|
||
newItem.setText(self.colIndex[HCols.CCOUNT], str(novIdx["cCount"]))
|
||
newItem.setText(self.colIndex[HCols.WCOUNT], str(novIdx["wCount"]))
|
||
newItem.setText(self.colIndex[HCols.PCOUNT], str(novIdx["pCount"]))
|
||
newItem.setTextAlignment(self.colIndex[HCols.CCOUNT], Qt.AlignRight)
|
||
newItem.setTextAlignment(self.colIndex[HCols.WCOUNT], Qt.AlignRight)
|
||
newItem.setTextAlignment(self.colIndex[HCols.PCOUNT], Qt.AlignRight)
|
||
|
||
theRefs = self.theIndex.getReferences(tHandle, sTitle)
|
||
newItem.setText(self.colIndex[HCols.POV], ", ".join(theRefs[nwKeyWords.POV_KEY]))
|
||
newItem.setText(self.colIndex[HCols.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
|
||
newItem.setText(self.colIndex[HCols.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY]))
|
||
newItem.setText(self.colIndex[HCols.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY]))
|
||
newItem.setText(self.colIndex[HCols.WORLD], ", ".join(theRefs[nwKeyWords.WORLD_KEY]))
|
||
newItem.setText(self.colIndex[HCols.OBJECT], ", ".join(theRefs[nwKeyWords.OBJECT_KEY]))
|
||
newItem.setText(self.colIndex[HCols.ENTITY], ", ".join(theRefs[nwKeyWords.ENTITY_KEY]))
|
||
newItem.setText(self.colIndex[HCols.CUSTOM], ", ".join(theRefs[nwKeyWords.CUSTOM_KEY]))
|
||
|
||
return newItem
|
||
|
||
# END Class GuiProjectOutline
|
||
|
||
class GuiOutlineHeaderMenu(QMenu):
|
||
|
||
def __init__(self, theParent):
|
||
QMenu.__init__(self, theParent)
|
||
|
||
return
|
||
|
||
# END Class GuiOutlineHeaderMenu
|