1066 lines
40 KiB
Python
1066 lines
40 KiB
Python
"""
|
||
novelWriter – GUI Project Outline
|
||
=================================
|
||
|
||
File History:
|
||
Created: 2022-05-15 [2.0rc1] GuiOutlineView
|
||
Created: 2022-05-22 [2.0rc1] GuiOutlineToolBar
|
||
Created: 2019-11-16 [0.4.1] GuiOutlineTree
|
||
Created: 2019-11-16 [0.4.1] GuiOutlineHeaderMenu
|
||
Created: 2020-06-02 [0.7] GuiOutlineDetails
|
||
|
||
This file is a part of novelWriter
|
||
Copyright (C) 2019 Veronica Berglyd Olsen and novelWriter contributors
|
||
|
||
This program is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation, either version 3 of the License, or
|
||
(at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful, but
|
||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
""" # noqa
|
||
from __future__ import annotations
|
||
|
||
import csv
|
||
import logging
|
||
|
||
from enum import Enum
|
||
from time import time
|
||
from typing import Final
|
||
|
||
from PyQt6.QtCore import QT_TRANSLATE_NOOP, Qt, pyqtSignal, pyqtSlot
|
||
from PyQt6.QtGui import QAction, QIcon
|
||
from PyQt6.QtWidgets import (
|
||
QAbstractItemView, QFileDialog, QFrame, QGridLayout, QGroupBox,
|
||
QHBoxLayout, QLabel, QMenu, QScrollArea, QSplitter, QToolBar, QToolButton,
|
||
QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
|
||
)
|
||
|
||
from novelwriter import CONFIG, SHARED
|
||
from novelwriter.common import checkInt, formatFileFilter
|
||
from novelwriter.constants import nwKeyWords, nwLabels, nwStats, nwStyles, trConst, trStats
|
||
from novelwriter.enum import nwChange, nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline
|
||
from novelwriter.error import logException
|
||
from novelwriter.extensions.configlayout import NColorLabel
|
||
from novelwriter.extensions.novelselector import NovelSelector
|
||
from novelwriter.types import (
|
||
QtAlignLeftTop, QtAlignRight, QtAlignRightTop, QtDecoration,
|
||
QtScrollAlwaysOff, QtScrollAsNeeded, QtSizeExpanding, QtUserRole
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class GuiOutlineView(QWidget):
|
||
"""GUI: Project Outline Panel."""
|
||
|
||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
|
||
|
||
def __init__(self, parent: QWidget) -> None:
|
||
super().__init__(parent=parent)
|
||
|
||
# Build GUI
|
||
self.outlineTree = GuiOutlineTree(self)
|
||
self.outlineData = GuiOutlineDetails(self)
|
||
self.outlineBar = GuiOutlineToolBar(self)
|
||
self.outlineBar.setEnabled(False)
|
||
|
||
self.splitOutline = QSplitter(Qt.Orientation.Vertical, self)
|
||
self.splitOutline.addWidget(self.outlineTree)
|
||
self.splitOutline.addWidget(self.outlineData)
|
||
self.splitOutline.setOpaqueResize(False)
|
||
self.splitOutline.setSizes(CONFIG.outlinePanePos)
|
||
|
||
# Assemble
|
||
self.outerBox = QVBoxLayout()
|
||
self.outerBox.setContentsMargins(0, 0, 4, 0)
|
||
self.outerBox.addWidget(self.outlineBar)
|
||
self.outerBox.addWidget(self.splitOutline)
|
||
|
||
self.setLayout(self.outerBox)
|
||
|
||
# Connect Signals
|
||
self.outlineTree.hiddenStateChanged.connect(self._updateMenuColumns)
|
||
self.outlineTree.activeItemChanged.connect(self.outlineData.showItem)
|
||
self.outlineData.itemTagClicked.connect(self._tagClicked)
|
||
self.outlineBar.loadNovelRootRequest.connect(self._rootItemChanged)
|
||
self.outlineBar.viewColumnToggled.connect(self.outlineTree.menuColumnToggled)
|
||
self.outlineBar.outlineExportRequest.connect(self.outlineTree.exportOutline)
|
||
|
||
# Function Mappings
|
||
self.getSelectedHandle = self.outlineTree.getSelectedHandle
|
||
|
||
##
|
||
# Methods
|
||
##
|
||
|
||
def updateTheme(self) -> None:
|
||
"""Update theme elements."""
|
||
self.outlineBar.updateTheme()
|
||
self.outlineTree.updateTheme()
|
||
self.outlineTree.refreshTree(
|
||
rootHandle=SHARED.project.data.getLastHandle("outline"), overRide=True
|
||
)
|
||
|
||
def initSettings(self) -> None:
|
||
"""Initialise GUI elements that depend on specific settings."""
|
||
self.outlineTree.initSettings()
|
||
self.outlineData.initSettings()
|
||
|
||
def refreshTree(self) -> None:
|
||
"""Refresh the current tree."""
|
||
self.outlineTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("outline"))
|
||
|
||
def clearOutline(self) -> None:
|
||
"""Clear project-related GUI content."""
|
||
self.outlineData.clearDetails()
|
||
self.outlineBar.setEnabled(False)
|
||
|
||
def openProjectTasks(self) -> None:
|
||
"""Run open project tasks."""
|
||
lastOutline = SHARED.project.data.getLastHandle("outline")
|
||
if not lastOutline or lastOutline not in SHARED.project.tree:
|
||
lastOutline = SHARED.project.tree.findRoot(nwItemClass.NOVEL)
|
||
|
||
logger.debug("Setting outline tree to root item '%s'", lastOutline)
|
||
|
||
self.clearOutline()
|
||
self.outlineBar.populateNovelList()
|
||
self.outlineBar.setCurrentRoot(lastOutline)
|
||
self.outlineBar.setEnabled(True)
|
||
self.outlineData.loadGuiSettings()
|
||
|
||
def closeProjectTasks(self) -> None:
|
||
"""Run closing project tasks."""
|
||
if self.outlineTree.wasRendered:
|
||
# If the panel hasn't been drawn yet, those values are incorrect
|
||
self.outlineData.saveGuiSettings()
|
||
self.outlineTree.closeProjectTasks()
|
||
self.outlineData.updateClasses()
|
||
self.clearOutline()
|
||
|
||
def splitSizes(self) -> list[int]:
|
||
"""Get the sizes of the splitter widget."""
|
||
return self.splitOutline.sizes()
|
||
|
||
def setTreeFocus(self) -> None:
|
||
"""Set the focus to the tree widget."""
|
||
return self.outlineTree.setFocus()
|
||
|
||
def treeHasFocus(self) -> bool:
|
||
"""Check if the outline tree has focus."""
|
||
return self.outlineTree.hasFocus()
|
||
|
||
##
|
||
# Public Slots
|
||
##
|
||
|
||
@pyqtSlot(str, Enum)
|
||
def updateRootItem(self, tHandle: str, change: nwChange) -> None:
|
||
"""Handle tasks whenever a root folders changes."""
|
||
self.outlineBar.populateNovelList()
|
||
self.outlineData.updateClasses()
|
||
|
||
##
|
||
# Private Slots
|
||
##
|
||
|
||
@pyqtSlot()
|
||
def _updateMenuColumns(self) -> None:
|
||
"""Trigger an update of the toggled state of the column menu
|
||
checkboxes whenever a signal is received that the hidden state
|
||
of columns has changed.
|
||
"""
|
||
self.outlineBar.setColumnHiddenState(self.outlineTree.hiddenColumns)
|
||
|
||
@pyqtSlot(str)
|
||
def _tagClicked(self, link: str) -> None:
|
||
"""Capture the click of a tag in the details panel."""
|
||
if link:
|
||
self.loadDocumentTagRequest.emit(link, nwDocMode.VIEW)
|
||
|
||
@pyqtSlot(str)
|
||
def _rootItemChanged(self, tHandle: str) -> None:
|
||
"""Handle root novel changed or needs to be refreshed."""
|
||
self.outlineTree.refreshTree(rootHandle=(tHandle or None), overRide=True)
|
||
|
||
|
||
class GuiOutlineToolBar(QToolBar):
|
||
"""GUI: Project Outline Panel ToolBar."""
|
||
|
||
loadNovelRootRequest = pyqtSignal(str)
|
||
outlineExportRequest = pyqtSignal()
|
||
viewColumnToggled = pyqtSignal(bool, Enum)
|
||
|
||
def __init__(self, outlineView: GuiOutlineView) -> None:
|
||
super().__init__(parent=outlineView)
|
||
|
||
logger.debug("Create: GuiOutlineToolBar")
|
||
|
||
self.setMovable(False)
|
||
self.setIconSize(1.4*SHARED.theme.baseIconSize)
|
||
self.setContentsMargins(0, 0, 0, 0)
|
||
|
||
stretch = QWidget(self)
|
||
stretch.setSizePolicy(QtSizeExpanding, QtSizeExpanding)
|
||
|
||
# Novel Selector
|
||
self.novelLabel = NColorLabel(
|
||
self.tr("Outline of"), self, scale=NColorLabel.HEADER_SCALE, bold=True
|
||
)
|
||
self.novelLabel.setContentsMargins(0, 0, 12, 0)
|
||
|
||
self.novelValue = NovelSelector(self)
|
||
self.novelValue.setIncludeAll(True)
|
||
self.novelValue.setMinimumWidth(200)
|
||
self.novelValue.novelSelectionChanged.connect(self._novelValueChanged)
|
||
|
||
# Actions
|
||
self.aRefresh = QAction(self.tr("Refresh"), self)
|
||
self.aRefresh.triggered.connect(self._refreshRequested)
|
||
|
||
self.aExport = QAction(self.tr("Export CSV"), self)
|
||
self.aExport.triggered.connect(self._exportRequested)
|
||
|
||
# Column Menu
|
||
self.mColumns = GuiOutlineHeaderMenu(self)
|
||
self.mColumns.columnToggled.connect(
|
||
lambda isChecked, tItem: self.viewColumnToggled.emit(isChecked, tItem)
|
||
)
|
||
|
||
self.tbColumns = QToolButton(self)
|
||
self.tbColumns.setMenu(self.mColumns)
|
||
self.tbColumns.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
||
|
||
# Assemble
|
||
self.addWidget(self.novelLabel)
|
||
self.addWidget(self.novelValue)
|
||
self.addSeparator()
|
||
self.addAction(self.aRefresh)
|
||
self.addAction(self.aExport)
|
||
self.addWidget(self.tbColumns)
|
||
self.addWidget(stretch)
|
||
|
||
self.updateTheme()
|
||
|
||
logger.debug("Ready: GuiOutlineToolBar")
|
||
|
||
##
|
||
# Methods
|
||
##
|
||
|
||
def updateTheme(self) -> None:
|
||
"""Update theme elements."""
|
||
self.setStyleSheet("QToolBar {border: 0px;}")
|
||
self.novelValue.refreshNovelList()
|
||
self.aRefresh.setIcon(SHARED.theme.getIcon("refresh", "green"))
|
||
self.aExport.setIcon(SHARED.theme.getIcon("export", "blue"))
|
||
self.tbColumns.setIcon(SHARED.theme.getIcon("more_vertical"))
|
||
self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}")
|
||
self.novelLabel.setTextColors(color=self.palette().windowText().color())
|
||
|
||
def populateNovelList(self) -> None:
|
||
"""Reload the content of the novel list."""
|
||
self.novelValue.refreshNovelList()
|
||
|
||
def setCurrentRoot(self, rootHandle: str | None) -> None:
|
||
"""Set the current active root handle."""
|
||
self.novelValue.setHandle(rootHandle)
|
||
|
||
def setColumnHiddenState(self, hiddenState: dict[nwOutline, bool]) -> None:
|
||
"""Forward the change of column hidden states to the menu."""
|
||
self.mColumns.setHiddenState(hiddenState)
|
||
|
||
##
|
||
# Private Slots
|
||
##
|
||
|
||
@pyqtSlot(str)
|
||
def _novelValueChanged(self, tHandle: str) -> None:
|
||
"""Emit a signal containing the handle of the selected item."""
|
||
self.loadNovelRootRequest.emit(tHandle)
|
||
|
||
@pyqtSlot()
|
||
def _refreshRequested(self) -> None:
|
||
"""Emit a signal containing the handle of the selected item."""
|
||
self.loadNovelRootRequest.emit(self.novelValue.handle)
|
||
|
||
@pyqtSlot()
|
||
def _exportRequested(self) -> None:
|
||
"""Emit a signal that an export of the outline was requested."""
|
||
self.outlineExportRequest.emit()
|
||
|
||
|
||
class GuiOutlineTree(QTreeWidget):
|
||
"""GUI: Project Outline Panel Tree."""
|
||
|
||
DEF_WIDTH: Final[dict[nwOutline, int]] = {
|
||
nwOutline.TITLE: 200,
|
||
nwOutline.LEVEL: 40,
|
||
nwOutline.LABEL: 150,
|
||
nwOutline.LINE: 40,
|
||
nwOutline.STATUS: 100,
|
||
nwOutline.CCOUNT: 50,
|
||
nwOutline.WCOUNT: 50,
|
||
nwOutline.PCOUNT: 50,
|
||
nwOutline.POV: 100,
|
||
nwOutline.FOCUS: 100,
|
||
nwOutline.CHAR: 100,
|
||
nwOutline.PLOT: 100,
|
||
nwOutline.TIME: 100,
|
||
nwOutline.WORLD: 100,
|
||
nwOutline.OBJECT: 100,
|
||
nwOutline.ENTITY: 100,
|
||
nwOutline.CUSTOM: 100,
|
||
nwOutline.STORY: 100,
|
||
nwOutline.MENTION: 100,
|
||
nwOutline.SYNOP: 200,
|
||
}
|
||
|
||
DEF_HIDDEN: Final[dict[nwOutline, bool]] = {
|
||
nwOutline.TITLE: False,
|
||
nwOutline.LEVEL: True,
|
||
nwOutline.LABEL: False,
|
||
nwOutline.LINE: True,
|
||
nwOutline.STATUS: True,
|
||
nwOutline.CCOUNT: True,
|
||
nwOutline.WCOUNT: False,
|
||
nwOutline.PCOUNT: False,
|
||
nwOutline.POV: False,
|
||
nwOutline.FOCUS: True,
|
||
nwOutline.CHAR: False,
|
||
nwOutline.PLOT: False,
|
||
nwOutline.TIME: True,
|
||
nwOutline.WORLD: False,
|
||
nwOutline.OBJECT: True,
|
||
nwOutline.ENTITY: True,
|
||
nwOutline.CUSTOM: True,
|
||
nwOutline.STORY: True,
|
||
nwOutline.MENTION: True,
|
||
nwOutline.SYNOP: False,
|
||
}
|
||
|
||
D_HANDLE = QtUserRole
|
||
D_TITLE = QtUserRole + 1
|
||
|
||
hiddenStateChanged = pyqtSignal()
|
||
activeItemChanged = pyqtSignal(str, str)
|
||
|
||
def __init__(self, outlineView: GuiOutlineView) -> None:
|
||
super().__init__(parent=outlineView)
|
||
|
||
logger.debug("Create: GuiOutlineTree")
|
||
|
||
self.outlineView = outlineView
|
||
|
||
self.setUniformRowHeights(True)
|
||
self.setFrameStyle(QFrame.Shape.NoFrame)
|
||
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||
self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||
self.setExpandsOnDoubleClick(False)
|
||
self.setDragEnabled(False)
|
||
self.itemDoubleClicked.connect(self._onItemDoubleClicked)
|
||
self.itemSelectionChanged.connect(self._onItemSelectionChanged)
|
||
|
||
self.setIconSize(SHARED.theme.baseIconSize)
|
||
self.setIndentation(0)
|
||
|
||
if header := self.header():
|
||
header.sectionMoved.connect(self._columnMoved)
|
||
|
||
# Pre-Generate Tree Formatting
|
||
fH1 = self.font()
|
||
fH1.setBold(True)
|
||
fH1.setUnderline(True)
|
||
|
||
fH2 = self.font()
|
||
fH2.setBold(True)
|
||
|
||
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
|
||
self._dIcon: dict[str, QIcon] = {}
|
||
|
||
# Internals
|
||
self._treeOrder = []
|
||
self._colWidth = {}
|
||
self._colHidden = {}
|
||
self._colIdx = {}
|
||
self._treeNCols = 0
|
||
self._firstView = True
|
||
self._lastBuild = 0
|
||
|
||
self.updateTheme()
|
||
self.initSettings()
|
||
self.clearContent()
|
||
|
||
self.hiddenStateChanged.emit()
|
||
|
||
logger.debug("Ready: GuiOutlineTree")
|
||
|
||
##
|
||
# Properties
|
||
##
|
||
|
||
@property
|
||
def hiddenColumns(self) -> dict[nwOutline, bool]:
|
||
return self._colHidden
|
||
|
||
@property
|
||
def wasRendered(self) -> bool:
|
||
"""Returns True after the Outline has been rendered once."""
|
||
return not self._firstView
|
||
|
||
##
|
||
# Methods
|
||
##
|
||
|
||
def initSettings(self) -> None:
|
||
"""Set or update outline settings."""
|
||
if CONFIG.hideVScroll:
|
||
self.setVerticalScrollBarPolicy(QtScrollAlwaysOff)
|
||
else:
|
||
self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
|
||
if CONFIG.hideHScroll:
|
||
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
|
||
else:
|
||
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
|
||
|
||
def clearContent(self) -> None:
|
||
"""Clear the tree and header and set the default values for the
|
||
columns arrays.
|
||
"""
|
||
self.clear()
|
||
self.setColumnCount(1)
|
||
self.setHeaderLabel(trConst(nwLabels.OUTLINE_COLS[nwOutline.TITLE]))
|
||
|
||
self._treeOrder: list[nwOutline] = []
|
||
self._colWidth: dict[nwOutline, int] = {}
|
||
self._colHidden: dict[nwOutline, bool] = {}
|
||
self._colIdx: dict[nwOutline, int] = {}
|
||
self._treeNCols = 0
|
||
|
||
for hItem in nwOutline:
|
||
self._treeOrder.append(hItem)
|
||
self._colWidth[hItem] = self.DEF_WIDTH[hItem]
|
||
self._colHidden[hItem] = self.DEF_HIDDEN[hItem]
|
||
|
||
self._treeNCols = len(self._treeOrder)
|
||
|
||
def updateTheme(self) -> None:
|
||
"""Update theme elements."""
|
||
iType = nwItemType.FILE
|
||
iClass = nwItemClass.NO_CLASS
|
||
iLayout = nwItemLayout.DOCUMENT
|
||
self._dIcon = {
|
||
"H0": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H0"),
|
||
"H1": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H1"),
|
||
"H2": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H2"),
|
||
"H3": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H3"),
|
||
"H4": SHARED.theme.getItemIcon(iType, iClass, iLayout, "H4"),
|
||
}
|
||
|
||
def refreshTree(
|
||
self, rootHandle: str | None = None,
|
||
overRide: bool = False, novelChanged: bool = False
|
||
) -> None:
|
||
"""Refresh the outline tree. Called whenever the Outline tab is
|
||
activated and controls what data to load, and if necessary,
|
||
force a rebuild of the tree.
|
||
"""
|
||
# If it's the first time, we always build
|
||
if self._firstView or (self._firstView and overRide):
|
||
self._loadHeaderState()
|
||
self._populateTree(rootHandle)
|
||
self._firstView = False
|
||
return
|
||
|
||
# If the novel index or novel tree has changed since the tree
|
||
# was last built, we rebuild the tree from the updated index.
|
||
indexChanged = SHARED.project.index.rootChangedSince(rootHandle, self._lastBuild)
|
||
if not (novelChanged or indexChanged or overRide):
|
||
logger.debug("No changes have been made to the novel index")
|
||
return
|
||
|
||
self._populateTree(rootHandle)
|
||
SHARED.project.data.setLastHandle(rootHandle or None, "outline")
|
||
|
||
return
|
||
|
||
def closeProjectTasks(self) -> None:
|
||
"""Call before a project is closed."""
|
||
self._saveHeaderState()
|
||
self.clearContent()
|
||
self._firstView = True
|
||
|
||
def getSelectedHandle(self) -> tuple[str | None, str | None]:
|
||
"""Get the currently selected handle. If multiple items are
|
||
selected, return the first.
|
||
"""
|
||
if item := self.selectedItems():
|
||
tHandle = item[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
|
||
sTitle = item[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
|
||
return tHandle, sTitle
|
||
return None, None
|
||
|
||
##
|
||
# Public Slots
|
||
##
|
||
|
||
@pyqtSlot(bool, Enum)
|
||
def menuColumnToggled(self, isChecked: bool, hItem: nwOutline) -> None:
|
||
"""Receive the changes to column visibility forwarded by the
|
||
column selection menu.
|
||
"""
|
||
if hItem in self._colIdx:
|
||
self.setColumnHidden(self._colIdx[hItem], not isChecked)
|
||
self._saveHeaderState()
|
||
|
||
@pyqtSlot()
|
||
def exportOutline(self) -> None:
|
||
"""Export the outline as a CSV file."""
|
||
name = CONFIG.lastPath("outline") / f"{SHARED.project.data.fileSafeName}.csv"
|
||
if path := QFileDialog.getSaveFileName(
|
||
self, self.tr("Save Outline As"), str(name), formatFileFilter(["*.csv", "*"])
|
||
)[0]:
|
||
CONFIG.setLastPath("outline", path)
|
||
logger.info("Writing CSV file: %s", path)
|
||
with open(path, mode="w", newline="", encoding="utf-8") as csvFile:
|
||
writer = csv.writer(csvFile, dialect="excel", quoting=csv.QUOTE_ALL)
|
||
writer.writerows(
|
||
self._dumpNovelData(self.outlineView.outlineBar.novelValue.handle)
|
||
)
|
||
|
||
##
|
||
# Private Slots
|
||
##
|
||
|
||
@pyqtSlot("QTreeWidgetItem*", int)
|
||
def _onItemDoubleClicked(self, tItem: QTreeWidgetItem, tCol: int) -> None:
|
||
"""Extract the handle and line number of the title double-
|
||
clicked, and send it to the main gui class for opening in the
|
||
document editor.
|
||
"""
|
||
tHandle, sTitle = self.getSelectedHandle()
|
||
if tHandle:
|
||
self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True)
|
||
|
||
@pyqtSlot()
|
||
def _onItemSelectionChanged(self) -> None:
|
||
"""Extract the handle and line number of the currently selected
|
||
title, and send it to the details panel.
|
||
"""
|
||
if items := self.selectedItems():
|
||
tHandle = items[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
|
||
sTitle = items[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
|
||
self.activeItemChanged.emit(tHandle, sTitle)
|
||
|
||
@pyqtSlot(int, int, int)
|
||
def _columnMoved(self, logIdx: int, oldVisualIdx: int, newVisualIdx: int) -> None:
|
||
"""Make sure the order array is up to date with the actual order
|
||
of the columns.
|
||
"""
|
||
self._treeOrder.insert(newVisualIdx, self._treeOrder.pop(oldVisualIdx))
|
||
self._saveHeaderState()
|
||
|
||
##
|
||
# Internal Functions
|
||
##
|
||
|
||
def _loadHeaderState(self) -> None:
|
||
"""Load the state of the main tree header, that is, column order
|
||
and column width.
|
||
"""
|
||
# Load whatever we saved last time, regardless of whether it
|
||
# contains the correct names or number of columns.
|
||
colState = SHARED.project.options.getValue("GuiOutline", "columnState", {})
|
||
|
||
tmpOrder = []
|
||
tmpHidden = {}
|
||
tmpWidth = {}
|
||
try:
|
||
for name, (hidden, width) in colState.items():
|
||
if name not in nwOutline.__members__:
|
||
logger.warning("Ignored unknown outline column '%s'", str(name))
|
||
continue
|
||
tmpOrder.append(nwOutline[name])
|
||
tmpHidden[nwOutline[name]] = hidden
|
||
tmpWidth[nwOutline[name]] = width
|
||
except Exception:
|
||
logger.error("Invalid column state")
|
||
logException()
|
||
|
||
# Add columns that was not in the file to the treeOrder array.
|
||
for hItem in nwOutline:
|
||
if hItem not in tmpOrder:
|
||
tmpOrder.append(hItem)
|
||
|
||
self._treeOrder = tmpOrder
|
||
self._colHidden.update(tmpHidden)
|
||
self._colWidth.update(tmpWidth)
|
||
|
||
self.hiddenStateChanged.emit()
|
||
|
||
def _saveHeaderState(self) -> None:
|
||
"""Save the state of the main tree header, that is, column
|
||
order, column width and column hidden state. We don't want to
|
||
save the current width of hidden columns though. This preserves
|
||
the last known width in case they're unhidden again.
|
||
"""
|
||
if self._lastBuild > 0 and (header := self.header()):
|
||
colState = {}
|
||
for iCol in range(self.columnCount()):
|
||
hItem = self._treeOrder[iCol]
|
||
iLog = header.logicalIndex(iCol)
|
||
logHidden = self.isColumnHidden(iLog)
|
||
orgWidth = self._colWidth[hItem]
|
||
logWidth = self.columnWidth(iLog)
|
||
colState[hItem.name] = [
|
||
logHidden, orgWidth if logHidden and logWidth == 0 else logWidth
|
||
]
|
||
|
||
logger.debug("Saving State: GuiOutline")
|
||
pOptions = SHARED.project.options
|
||
pOptions.setValue("GuiOutline", "columnState", colState)
|
||
pOptions.saveSettings()
|
||
|
||
def _populateTree(self, rootHandle: str | None) -> None:
|
||
"""Build the tree based on the project index, and the header
|
||
based on the defined constants, default values and user selected
|
||
width, order and hidden state. All columns are populated, even
|
||
if they are hidden. This ensures that showing and hiding columns
|
||
is fast and doesn't require a rebuild of the tree.
|
||
"""
|
||
logger.debug("Rebuilding Outline tree")
|
||
tStart = time()
|
||
self.clear()
|
||
|
||
if self._firstView:
|
||
labels = []
|
||
for i, hItem in enumerate(self._treeOrder):
|
||
labels.append(trConst(nwLabels.OUTLINE_COLS[hItem]))
|
||
self._colIdx[hItem] = i
|
||
|
||
self.setHeaderLabels(labels)
|
||
for hItem in self._treeOrder:
|
||
self.setColumnWidth(self._colIdx[hItem], self._colWidth[hItem])
|
||
self.setColumnHidden(self._colIdx[hItem], self._colHidden[hItem])
|
||
|
||
# Make sure title column is always visible
|
||
self.setColumnHidden(self._colIdx[nwOutline.TITLE], False)
|
||
|
||
headItem = self.headerItem()
|
||
if isinstance(headItem, QTreeWidgetItem):
|
||
headItem.setTextAlignment(
|
||
self._colIdx[nwOutline.CCOUNT], QtAlignRight)
|
||
headItem.setTextAlignment(
|
||
self._colIdx[nwOutline.WCOUNT], QtAlignRight)
|
||
headItem.setTextAlignment(
|
||
self._colIdx[nwOutline.PCOUNT], QtAlignRight)
|
||
|
||
novStruct = SHARED.project.index.novelStructure(rootHandle=rootHandle, activeOnly=True)
|
||
for _, tHandle, sTitle, novIdx in novStruct:
|
||
|
||
iLevel = nwStyles.H_LEVEL.get(novIdx.level, 0)
|
||
nwItem = SHARED.project.tree[tHandle]
|
||
if iLevel == 0 or nwItem is None:
|
||
continue
|
||
sLabel, sIcon = nwItem.getImportStatus()
|
||
|
||
item = QTreeWidgetItem()
|
||
hDec = SHARED.theme.getHeaderDecoration(iLevel)
|
||
|
||
item.setData(self._colIdx[nwOutline.TITLE], QtDecoration, hDec)
|
||
item.setText(self._colIdx[nwOutline.TITLE], novIdx.title)
|
||
item.setData(self._colIdx[nwOutline.TITLE], self.D_HANDLE, tHandle)
|
||
item.setData(self._colIdx[nwOutline.TITLE], self.D_TITLE, sTitle)
|
||
item.setFont(self._colIdx[nwOutline.TITLE], self._hFonts[iLevel])
|
||
item.setText(self._colIdx[nwOutline.LEVEL], novIdx.level)
|
||
item.setIcon(self._colIdx[nwOutline.LABEL], self._dIcon[nwItem.mainHeading])
|
||
item.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName)
|
||
item.setText(self._colIdx[nwOutline.LINE], f"{novIdx.line:n}")
|
||
item.setText(self._colIdx[nwOutline.STATUS], sLabel)
|
||
item.setIcon(self._colIdx[nwOutline.STATUS], sIcon)
|
||
item.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis)
|
||
item.setText(self._colIdx[nwOutline.CCOUNT], f"{novIdx.charCount:n}")
|
||
item.setText(self._colIdx[nwOutline.WCOUNT], f"{novIdx.wordCount:n}")
|
||
item.setText(self._colIdx[nwOutline.PCOUNT], f"{novIdx.paraCount:n}")
|
||
item.setTextAlignment(self._colIdx[nwOutline.CCOUNT], QtAlignRight)
|
||
item.setTextAlignment(self._colIdx[nwOutline.WCOUNT], QtAlignRight)
|
||
item.setTextAlignment(self._colIdx[nwOutline.PCOUNT], QtAlignRight)
|
||
|
||
refs = SHARED.project.index.getReferences(tHandle, sTitle)
|
||
item.setText(self._colIdx[nwOutline.POV], ", ".join(refs[nwKeyWords.POV_KEY]))
|
||
item.setText(self._colIdx[nwOutline.FOCUS], ", ".join(refs[nwKeyWords.FOCUS_KEY]))
|
||
item.setText(self._colIdx[nwOutline.CHAR], ", ".join(refs[nwKeyWords.CHAR_KEY]))
|
||
item.setText(self._colIdx[nwOutline.PLOT], ", ".join(refs[nwKeyWords.PLOT_KEY]))
|
||
item.setText(self._colIdx[nwOutline.TIME], ", ".join(refs[nwKeyWords.TIME_KEY]))
|
||
item.setText(self._colIdx[nwOutline.WORLD], ", ".join(refs[nwKeyWords.WORLD_KEY]))
|
||
item.setText(self._colIdx[nwOutline.OBJECT], ", ".join(refs[nwKeyWords.OBJECT_KEY]))
|
||
item.setText(self._colIdx[nwOutline.ENTITY], ", ".join(refs[nwKeyWords.ENTITY_KEY]))
|
||
item.setText(self._colIdx[nwOutline.CUSTOM], ", ".join(refs[nwKeyWords.CUSTOM_KEY]))
|
||
item.setText(self._colIdx[nwOutline.STORY], ", ".join(refs[nwKeyWords.STORY_KEY]))
|
||
item.setText(self._colIdx[nwOutline.MENTION], ", ".join(refs[nwKeyWords.MENTION_KEY]))
|
||
|
||
self.addTopLevelItem(item)
|
||
|
||
self._lastBuild = time()
|
||
logger.debug("Project outline built in %.3f ms", 1000.0*(time() - tStart))
|
||
|
||
def _dumpNovelData(self, rootHandle: str | None) -> list[list[str | int]]:
|
||
"""Dump all novel data into a table."""
|
||
sLabel = SHARED.project.localLookup("Story Structure")
|
||
nLabel = SHARED.project.localLookup("Note")
|
||
sKeys = sorted(SHARED.project.index.getStoryKeys())
|
||
nKeys = sorted(SHARED.project.index.getNoteKeys())
|
||
sMatch = [f"story.{k}" for k in sKeys]
|
||
nMatch = [f"note.{k}" for k in nKeys]
|
||
sHeaders = [f"{sLabel} ({k})" for k in sKeys]
|
||
nHeaders = [f"{nLabel} ({k})" for k in nKeys]
|
||
|
||
data: list[list[str | int]] = [[
|
||
"H",
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.TITLE]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.LABEL]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.LINE]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.STATUS]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.CCOUNT]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.WCOUNT]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.PCOUNT]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.POV]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.FOCUS]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.CHAR]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.PLOT]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.TIME]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.WORLD]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.OBJECT]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.ENTITY]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.CUSTOM]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.STORY]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.MENTION]),
|
||
trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP]),
|
||
*sHeaders,
|
||
*nHeaders,
|
||
]]
|
||
|
||
for _, tHandle, sTitle, novIdx in SHARED.project.index.novelStructure(
|
||
rootHandle=rootHandle, activeOnly=True
|
||
):
|
||
if novIdx.level != "H0" and (nwItem := SHARED.project.tree[tHandle]):
|
||
refs = SHARED.project.index.getReferences(tHandle, sTitle)
|
||
comments = dict(novIdx.comments.items())
|
||
story = [comments.get(k, "") for k in sMatch]
|
||
notes = [comments.get(k, "") for k in nMatch]
|
||
data.append([
|
||
novIdx.level,
|
||
novIdx.title,
|
||
nwItem.itemName,
|
||
novIdx.line,
|
||
nwItem.getImportStatus()[0],
|
||
novIdx.charCount,
|
||
novIdx.wordCount,
|
||
novIdx.paraCount,
|
||
", ".join(refs[nwKeyWords.POV_KEY]),
|
||
", ".join(refs[nwKeyWords.FOCUS_KEY]),
|
||
", ".join(refs[nwKeyWords.CHAR_KEY]),
|
||
", ".join(refs[nwKeyWords.PLOT_KEY]),
|
||
", ".join(refs[nwKeyWords.TIME_KEY]),
|
||
", ".join(refs[nwKeyWords.WORLD_KEY]),
|
||
", ".join(refs[nwKeyWords.OBJECT_KEY]),
|
||
", ".join(refs[nwKeyWords.ENTITY_KEY]),
|
||
", ".join(refs[nwKeyWords.CUSTOM_KEY]),
|
||
", ".join(refs[nwKeyWords.STORY_KEY]),
|
||
", ".join(refs[nwKeyWords.MENTION_KEY]),
|
||
novIdx.synopsis,
|
||
*story,
|
||
*notes,
|
||
])
|
||
|
||
return data
|
||
|
||
|
||
class GuiOutlineHeaderMenu(QMenu):
|
||
"""GUI: Project Outline Panel Header Selection Menu."""
|
||
|
||
columnToggled = pyqtSignal(bool, Enum)
|
||
|
||
def __init__(self, outlineToolBar: GuiOutlineToolBar) -> None:
|
||
super().__init__(parent=outlineToolBar)
|
||
|
||
self.acceptToggle = True
|
||
|
||
mnuHead = QAction(self.tr("Select Columns"), self)
|
||
self.addAction(mnuHead)
|
||
self.addSeparator()
|
||
|
||
self.actionMap = {}
|
||
for hItem in nwOutline:
|
||
if hItem == nwOutline.TITLE:
|
||
continue
|
||
self.actionMap[hItem] = QAction(trConst(nwLabels.OUTLINE_COLS[hItem]), self)
|
||
self.actionMap[hItem].setCheckable(True)
|
||
self.actionMap[hItem].toggled.connect(
|
||
lambda isChecked, tItem=hItem: self.columnToggled.emit(isChecked, tItem)
|
||
)
|
||
self.addAction(self.actionMap[hItem])
|
||
|
||
def setHiddenState(self, hiddenState: dict[nwOutline, bool]) -> None:
|
||
"""Overwrite the checked state of the columns as the inverse of
|
||
the hidden state. Skip the TITLE column as it cannot be hidden.
|
||
"""
|
||
self.acceptToggle = False
|
||
|
||
for hItem in nwOutline:
|
||
if hItem == nwOutline.TITLE or hItem not in hiddenState:
|
||
continue
|
||
self.actionMap[hItem].setChecked(not hiddenState[hItem])
|
||
|
||
self.acceptToggle = True
|
||
|
||
|
||
class GuiOutlineDetails(QScrollArea):
|
||
"""GUI: Project Outline Panel Details View."""
|
||
|
||
LVL_MAP: Final[dict[str, str]] = {
|
||
"H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"),
|
||
"H2": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Chapter"),
|
||
"H3": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Scene"),
|
||
"H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"),
|
||
}
|
||
|
||
itemTagClicked = pyqtSignal(str)
|
||
|
||
def __init__(self, outlineView: GuiOutlineView) -> None:
|
||
super().__init__(parent=outlineView)
|
||
|
||
logger.debug("Create: GuiOutlineDetails")
|
||
|
||
self.outlineView = outlineView
|
||
|
||
# Sizes
|
||
minTitle = 30*SHARED.theme.textNWidth
|
||
maxTitle = 40*SHARED.theme.textNWidth
|
||
wCount = SHARED.theme.getTextWidth("999,999")
|
||
|
||
bFont = SHARED.theme.guiFontB
|
||
|
||
trStats1 = trStats(nwLabels.STATS_NAME[nwStats.CHARS])
|
||
trStats2 = trStats(nwLabels.STATS_NAME[nwStats.WORDS])
|
||
trStats3 = trStats(nwLabels.STATS_NAME[nwStats.PARAGRAPHS])
|
||
|
||
# Details Area
|
||
self.titleLabel = QLabel(self.tr("Title"), self)
|
||
self.fileLabel = QLabel(self.tr("Document"), self)
|
||
self.itemLabel = QLabel(self.tr("Status"), self)
|
||
self.titleValue = QLabel("", self)
|
||
self.fileValue = QLabel("", self)
|
||
self.itemValue = QLabel("", self)
|
||
|
||
self.titleLabel.setFont(bFont)
|
||
self.fileLabel.setFont(bFont)
|
||
self.itemLabel.setFont(bFont)
|
||
|
||
self.titleValue.setMinimumWidth(minTitle)
|
||
self.titleValue.setMaximumWidth(maxTitle)
|
||
self.fileValue.setMinimumWidth(minTitle)
|
||
self.fileValue.setMaximumWidth(maxTitle)
|
||
self.itemValue.setMinimumWidth(minTitle)
|
||
self.itemValue.setMaximumWidth(maxTitle)
|
||
|
||
# Stats Area
|
||
self.cCLabel = QLabel(trStats1, self)
|
||
self.wCLabel = QLabel(trStats2, self)
|
||
self.pCLabel = QLabel(trStats3, self)
|
||
self.cCValue = QLabel("", self)
|
||
self.wCValue = QLabel("", self)
|
||
self.pCValue = QLabel("", self)
|
||
|
||
self.cCLabel.setFont(bFont)
|
||
self.wCLabel.setFont(bFont)
|
||
self.pCLabel.setFont(bFont)
|
||
|
||
self.cCValue.setMinimumWidth(wCount)
|
||
self.wCValue.setMinimumWidth(wCount)
|
||
self.pCValue.setMinimumWidth(wCount)
|
||
self.cCValue.setAlignment(QtAlignRight)
|
||
self.wCValue.setAlignment(QtAlignRight)
|
||
self.pCValue.setAlignment(QtAlignRight)
|
||
|
||
# Synopsis
|
||
self.synopLabel = QLabel(self.tr("Synopsis"), self)
|
||
self.synopLabel.setFont(bFont)
|
||
|
||
self.synopValue = QLabel("", self)
|
||
self.synopValue.setWordWrap(True)
|
||
self.synopValue.setAlignment(QtAlignLeftTop)
|
||
|
||
self.synopLWrap = QHBoxLayout()
|
||
self.synopLWrap.addWidget(self.synopValue, 1)
|
||
|
||
# Selected Item Details
|
||
self.mainForm = QGridLayout()
|
||
self.mainGroup = QGroupBox(self.tr("Title Details"), self)
|
||
self.mainGroup.setLayout(self.mainForm)
|
||
|
||
self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, QtAlignLeftTop)
|
||
self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, QtAlignLeftTop)
|
||
self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, QtAlignLeftTop)
|
||
self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, QtAlignRightTop)
|
||
self.mainForm.addWidget(self.fileLabel, 1, 0, 1, 1, QtAlignLeftTop)
|
||
self.mainForm.addWidget(self.fileValue, 1, 1, 1, 1, QtAlignLeftTop)
|
||
self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, QtAlignLeftTop)
|
||
self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, QtAlignRightTop)
|
||
self.mainForm.addWidget(self.itemLabel, 2, 0, 1, 1, QtAlignLeftTop)
|
||
self.mainForm.addWidget(self.itemValue, 2, 1, 1, 1, QtAlignLeftTop)
|
||
self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, QtAlignLeftTop)
|
||
self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, QtAlignRightTop)
|
||
self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, QtAlignLeftTop)
|
||
self.mainForm.addLayout(self.synopLWrap, 4, 0, 1, 4, QtAlignLeftTop)
|
||
|
||
self.mainForm.setColumnStretch(1, 1)
|
||
self.mainForm.setRowStretch(4, 1)
|
||
self.mainForm.setHorizontalSpacing(10)
|
||
self.mainForm.setVerticalSpacing(4)
|
||
|
||
# Selected Item Tags
|
||
self.tagsForm = QGridLayout()
|
||
self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self)
|
||
self.tagsGroup.setLayout(self.tagsForm)
|
||
|
||
self.tagValues: dict[str, tuple[QLabel, QLabel]] = {}
|
||
for key in nwKeyWords.ALL_KEYS:
|
||
label = QLabel(trConst(nwLabels.KEY_NAME[key]), self)
|
||
label.setFont(bFont)
|
||
value = QLabel("", self)
|
||
value.setWordWrap(True)
|
||
value.linkActivated.connect(lambda x: self.itemTagClicked.emit(x))
|
||
layout = QHBoxLayout()
|
||
layout.addWidget(value, 1)
|
||
n = len(self.tagValues)
|
||
self.tagsForm.addWidget(label, n, 0, QtAlignLeftTop)
|
||
self.tagsForm.addLayout(layout, n, 1, QtAlignLeftTop)
|
||
self.tagValues[key] = (label, value)
|
||
|
||
self.tagsForm.setColumnStretch(1, 1)
|
||
self.tagsForm.setRowStretch(len(self.tagValues), 1)
|
||
self.tagsForm.setHorizontalSpacing(10)
|
||
self.tagsForm.setVerticalSpacing(4)
|
||
|
||
# Assemble
|
||
self.mainSplit = QSplitter(Qt.Orientation.Horizontal)
|
||
self.mainSplit.addWidget(self.mainGroup)
|
||
self.mainSplit.addWidget(self.tagsGroup)
|
||
self.mainSplit.setCollapsible(0, False)
|
||
self.mainSplit.setCollapsible(1, False)
|
||
|
||
self.setWidget(self.mainSplit)
|
||
|
||
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
|
||
self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
|
||
self.setWidgetResizable(True)
|
||
self.setFrameStyle(QFrame.Shape.NoFrame)
|
||
|
||
self.initSettings()
|
||
|
||
logger.debug("Ready: GuiOutlineDetails")
|
||
|
||
def initSettings(self) -> None:
|
||
"""Set or update outline settings."""
|
||
if CONFIG.hideVScroll:
|
||
self.setVerticalScrollBarPolicy(QtScrollAlwaysOff)
|
||
else:
|
||
self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
|
||
if CONFIG.hideHScroll:
|
||
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
|
||
else:
|
||
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
|
||
self.updateClasses()
|
||
|
||
def loadGuiSettings(self) -> None:
|
||
"""Run open project tasks."""
|
||
parent = self.outlineView.parent() # This widget is rendered already
|
||
width = parent.width() if isinstance(parent, QWidget) else 1000
|
||
pOptions = SHARED.project.options
|
||
self.mainSplit.setSizes([
|
||
pOptions.getInt("GuiOutlineDetails", "detailsWidth", width//3),
|
||
pOptions.getInt("GuiOutlineDetails", "tagsWidth", 2*width//3),
|
||
])
|
||
|
||
def saveGuiSettings(self) -> None:
|
||
"""Run close project tasks."""
|
||
logger.debug("Saving State: GuiOutlineDetails")
|
||
mainSplit = self.mainSplit.sizes()
|
||
pOptions = SHARED.project.options
|
||
pOptions.setValue("GuiOutlineDetails", "detailsWidth", mainSplit[0])
|
||
pOptions.setValue("GuiOutlineDetails", "tagsWidth", mainSplit[1])
|
||
|
||
def clearDetails(self) -> None:
|
||
"""Clear all the data labels."""
|
||
self.titleLabel.setText(self.tr("Title"))
|
||
self.titleValue.clear()
|
||
self.fileValue.clear()
|
||
self.itemValue.clear()
|
||
self.cCValue.clear()
|
||
self.wCValue.clear()
|
||
self.pCValue.clear()
|
||
self.synopValue.clear()
|
||
|
||
for _, value in self.tagValues.values():
|
||
value.clear()
|
||
|
||
self.updateClasses()
|
||
|
||
##
|
||
# Slots
|
||
##
|
||
|
||
@pyqtSlot(str, str)
|
||
def showItem(self, tHandle: str, sTitle: str) -> None:
|
||
"""Update the content of the tree with the given handle and line
|
||
number pointing to a header.
|
||
"""
|
||
pIndex = SHARED.project.index
|
||
nwItem = SHARED.project.tree[tHandle]
|
||
novIdx = pIndex.getItemHeading(tHandle, sTitle)
|
||
novRefs = pIndex.getReferences(tHandle, sTitle)
|
||
if nwItem and novIdx:
|
||
self.titleLabel.setText(self.tr(self.LVL_MAP.get(novIdx.level, "H1")))
|
||
self.titleValue.setText(novIdx.title)
|
||
|
||
itemStatus, _ = nwItem.getImportStatus()
|
||
|
||
self.fileValue.setText(nwItem.itemName)
|
||
self.itemValue.setText(itemStatus)
|
||
|
||
self.cCValue.setText(f"{checkInt(novIdx.charCount, 0):n}")
|
||
self.wCValue.setText(f"{checkInt(novIdx.wordCount, 0):n}")
|
||
self.pCValue.setText(f"{checkInt(novIdx.paraCount, 0):n}")
|
||
|
||
self.synopValue.setText(novIdx.synopsis)
|
||
|
||
for key, (_, value) in self.tagValues.items():
|
||
value.setText(self._formatTags(novRefs, key))
|
||
|
||
@pyqtSlot()
|
||
def updateClasses(self) -> None:
|
||
"""Update the visibility status of class details."""
|
||
usedClasses = SHARED.project.tree.rootClasses()
|
||
for key, itemClass in nwKeyWords.KEY_CLASS.items():
|
||
visible = itemClass in usedClasses
|
||
if key in self.tagValues:
|
||
label, value = self.tagValues[key]
|
||
label.setVisible(visible)
|
||
value.setVisible(visible)
|
||
|
||
@staticmethod
|
||
def _formatTags(refs: dict[str, list[str]], key: str) -> str:
|
||
"""Convert a list of tags into a list of clickable tag links."""
|
||
return ", ".join(
|
||
[f"<a href='{tag}'>{tag}</a>" for tag in refs.get(key, [])]
|
||
)
|