Files
novelWriter/novelwriter/gui/noveltree.py
T
2025-02-22 20:12:50 +01:00

791 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
novelWriter GUI Novel Tree
============================
File History:
Created: 2020-12-20 [1.1rc1] GuiNovelTree
Created: 2022-06-12 [2.0rc1] GuiNovelView
Created: 2022-06-12 [2.0rc1] GuiNovelToolBar
This file is a part of novelWriter
Copyright (C) 2020 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/>.
"""
from __future__ import annotations
import logging
from enum import Enum
from PyQt6.QtCore import pyqtSignal, pyqtSlot
from PyQt6.QtGui import QActionGroup, QFont, QPalette
from PyQt6.QtWidgets import (
QFrame, QHBoxLayout, QMenu, QTreeView, QTreeWidgetItem, QVBoxLayout,
QWidget
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import qtAddAction, qtAddMenu, qtLambda
from novelwriter.enum import nwChange, nwItemClass
from novelwriter.extensions.modified import NIconToolButton
from novelwriter.extensions.novelselector import NovelSelector
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
from novelwriter.types import (
QtHeaderStretch, QtHeaderToContents, QtScrollAlwaysOff, QtScrollAsNeeded,
QtSizeExpanding, QtUserRole
)
logger = logging.getLogger(__name__)
class NovelTreeColumn(Enum):
HIDDEN = 0
POV = 1
FOCUS = 2
PLOT = 3
class GuiNovelView(QWidget):
# Signals for user interaction with the novel tree
selectedItemChanged = pyqtSignal(str)
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
# Build GUI
self.novelTree = GuiNovelTree(self)
self.novelBar = GuiNovelToolBar(self)
self.novelBar.setEnabled(False)
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.novelBar, 0)
self.outerBox.addWidget(self.novelTree, 1)
self.outerBox.setContentsMargins(0, 0, 0, 0)
self.outerBox.setSpacing(0)
self.setLayout(self.outerBox)
# Function Mappings
self.getSelectedHandle = self.novelTree.getSelectedHandle
return
##
# Methods
##
def updateTheme(self) -> None:
"""Update theme elements."""
self.novelBar.updateTheme()
self.novelTree.updateTheme()
self.refreshTree()
return
def initSettings(self) -> None:
"""Initialise GUI elements that depend on specific settings."""
self.novelTree.initSettings()
return
def clearNovelView(self) -> None:
"""Clear project-related GUI content."""
self.novelTree.clearContent()
self.novelBar.clearContent()
self.novelBar.setEnabled(False)
return
def openProjectTasks(self) -> None:
"""Run open project tasks."""
lastNovel = SHARED.project.data.getLastHandle("novelTree")
if lastNovel and lastNovel not in SHARED.project.tree:
lastNovel = SHARED.project.tree.findRoot(nwItemClass.NOVEL)
logger.debug("Setting novel tree to root item '%s'", lastNovel)
lastCol = SHARED.project.options.getEnum(
"GuiNovelView", "lastCol", NovelTreeColumn, NovelTreeColumn.HIDDEN
)
# lastColSize = SHARED.project.options.getInt(
# "GuiNovelView", "lastColSize", 25
# )
self.clearNovelView()
self.novelBar.buildNovelRootMenu()
self.novelBar.setLastColType(lastCol, doRefresh=False)
self.novelBar.setCurrentRoot(lastNovel)
self.novelBar.setEnabled(True)
# self.novelTree.setLastColSize(lastColSize)
return
def closeProjectTasks(self) -> None:
"""Run closing project tasks."""
# lastColType = self.novelTree.lastColType
# lastColSize = self.novelTree.lastColSize
# logger.debug("Saving State: GuiNovelView")
# pOptions = SHARED.project.options
# pOptions.setValue("GuiNovelView", "lastCol", lastColType)
# pOptions.setValue("GuiNovelView", "lastColSize", lastColSize)
self.clearNovelView()
return
def setTreeFocus(self) -> None:
"""Set the focus to the tree widget."""
self.novelTree.setFocus()
return
def treeHasFocus(self) -> bool:
"""Check if the novel tree has focus."""
return self.novelTree.hasFocus()
##
# Public Slots
##
@pyqtSlot(str)
def setCurrentNovel(self, rootHandle: str | None) -> None:
"""Set the current novel to display."""
if rootHandle and (model := SHARED.project.index.getNovelModel(rootHandle)):
self.novelTree.setModel(model)
self.novelTree.resizeColumsn()
return
@pyqtSlot(str)
def setActiveHandle(self, tHandle: str) -> None:
"""Highlight the rows associated with a given handle."""
# self.novelTree.setActiveHandle(tHandle)
return
@pyqtSlot()
def refreshTree(self) -> None:
"""Refresh the current tree."""
# self.novelTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("novelTree"))
return
@pyqtSlot(str, Enum)
def updateRootItem(self, tHandle: str, change: nwChange) -> None:
"""If any root item changes, rebuild the novel root menu."""
self.novelBar.buildNovelRootMenu()
return
@pyqtSlot(str)
def updateNovelItemMeta(self, tHandle: str) -> None:
"""The meta data of a novel item has changed, and the tree item
needs to be refreshed.
"""
# self.novelTree.refreshHandle(tHandle)
return
class GuiNovelToolBar(QWidget):
def __init__(self, novelView: GuiNovelView) -> None:
super().__init__(parent=novelView)
logger.debug("Create: GuiNovelToolBar")
self.novelView = novelView
iSz = SHARED.theme.baseIconSize
self.setContentsMargins(0, 0, 0, 0)
self.setBackgroundRole(QPalette.ColorRole.Base)
self.setAutoFillBackground(True)
# Novel Selector
selFont = self.font()
selFont.setWeight(QFont.Weight.Bold)
self.novelValue = NovelSelector(self)
self.novelValue.setFont(selFont)
self.novelValue.setListFormat(self.tr("Outline of {0}"))
self.novelValue.setMinimumWidth(150)
self.novelValue.setSizePolicy(QtSizeExpanding, QtSizeExpanding)
self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot)
self.tbNovel = NIconToolButton(self, iSz)
self.tbNovel.setToolTip(self.tr("Novel Root"))
self.tbNovel.clicked.connect(self.novelValue.showPopup)
# Refresh Button
self.tbRefresh = NIconToolButton(self, iSz)
self.tbRefresh.setToolTip(self.tr("Refresh"))
self.tbRefresh.clicked.connect(self._refreshNovelTree)
# More Options Menu
self.mMore = QMenu(self)
self.mLastCol = qtAddMenu(self.mMore, self.tr("Last Column"))
self.gLastCol = QActionGroup(self.mMore)
self.aLastCol = {}
self._addLastColAction(NovelTreeColumn.HIDDEN, self.tr("Hidden"))
self._addLastColAction(NovelTreeColumn.POV, self.tr("Point of View Character"))
self._addLastColAction(NovelTreeColumn.FOCUS, self.tr("Focus Character"))
self._addLastColAction(NovelTreeColumn.PLOT, self.tr("Novel Plot"))
self.mLastCol.addSeparator()
self.aLastColSize = qtAddAction(self.mLastCol, self.tr("Column Size"))
self.aLastColSize.triggered.connect(self._selectLastColumnSize)
self.tbMore = NIconToolButton(self, iSz)
self.tbMore.setToolTip(self.tr("More Options"))
self.tbMore.setMenu(self.mMore)
# Assemble
self.outerBox = QHBoxLayout()
self.outerBox.addWidget(self.novelValue)
self.outerBox.addWidget(self.tbNovel)
self.outerBox.addWidget(self.tbRefresh)
self.outerBox.addWidget(self.tbMore)
self.outerBox.setContentsMargins(4, 2, 0, 2)
self.outerBox.setSpacing(0)
self.setLayout(self.outerBox)
self.updateTheme()
logger.debug("Ready: GuiNovelToolBar")
return
##
# Methods
##
def updateTheme(self) -> None:
"""Update theme elements."""
# Icons
self.tbNovel.setThemeIcon("cls_novel", "red")
self.tbRefresh.setThemeIcon("refresh", "green")
self.tbMore.setThemeIcon("more_vertical")
# StyleSheets
buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
self.tbNovel.setStyleSheet(buttonStyle)
self.tbRefresh.setStyleSheet(buttonStyle)
self.tbMore.setStyleSheet(buttonStyle)
self.novelValue.setStyleSheet(
"QComboBox {border-style: none; padding-left: 0;} "
"QComboBox::drop-down {border-style: none}"
)
self.novelValue.refreshNovelList()
self.tbNovel.setVisible(self.novelValue.count() > 1)
return
def clearContent(self) -> None:
"""Run clearing project tasks."""
self.novelValue.clear()
self.novelValue.setToolTip("")
return
def buildNovelRootMenu(self) -> None:
"""Build the novel root menu."""
self.novelValue.refreshNovelList()
self.tbNovel.setVisible(self.novelValue.count() > 1)
return
def setCurrentRoot(self, rootHandle: str | None) -> None:
"""Set the current active root handle."""
self.novelValue.setHandle(rootHandle)
SHARED.project.data.setLastHandle(rootHandle, "novelTree")
self.novelView.setCurrentNovel(rootHandle)
return
def setLastColType(self, colType: NovelTreeColumn, doRefresh: bool = True) -> None:
"""Set the last column type."""
# self.aLastCol[colType].setChecked(True)
# self.novelView.novelTree.setLastColType(colType, doRefresh=doRefresh)
return
##
# Private Slots
##
@pyqtSlot()
def _refreshNovelTree(self) -> None:
"""Rebuild the current tree."""
self.novelView.setCurrentNovel(SHARED.project.data.getLastHandle("novelTree"))
return
@pyqtSlot()
def _selectLastColumnSize(self) -> None:
"""Set the maximum width for the last column."""
# oldSize = self.novelView.novelTree.lastColSize
# newSize, isOk = QInputDialog.getInt(
# self, self.tr("Column Size"), self.tr("Maximum column size in %"), oldSize, 15, 75, 5
# )
# if isOk:
# self.novelView.novelTree.setLastColSize(newSize)
# self._refreshNovelTree()
return
##
# Internal Functions
##
def _addLastColAction(self, colType: NovelTreeColumn, actionLabel: str) -> None:
"""Add a column selection entry to the last column menu."""
aLast = qtAddAction(self.mLastCol, actionLabel)
aLast.setCheckable(True)
aLast.setActionGroup(self.gLastCol)
aLast.triggered.connect(qtLambda(self.setLastColType, colType))
self.aLastCol[colType] = aLast
return
class GuiNovelTree(QTreeView):
C_DATA = 0
C_TITLE = 0
C_WORDS = 1
C_EXTRA = 2
C_MORE = 3
D_HANDLE = QtUserRole
D_TITLE = QtUserRole + 1
D_KEY = QtUserRole + 2
D_EXTRA = QtUserRole + 3
def __init__(self, novelView: GuiNovelView) -> None:
super().__init__(parent=novelView)
logger.debug("Create: GuiNovelTree")
self.novelView = novelView
# Internal Variables
self._lastBuild = 0
self._lastCol = NovelTreeColumn.POV
self._lastColSize = 0.25
self._actHandle = None
self._treeMap: dict[str, QTreeWidgetItem] = {}
# Cached Strings
# self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
# self._focLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])
# self._pltLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])
# Build GUI
# =========
# iPx = SHARED.theme.baseIconHeight
self.setIconSize(SHARED.theme.baseIconSize)
self.setFrameStyle(QFrame.Shape.NoFrame)
self.setUniformRowHeights(True)
self.setAllColumnsShowFocus(True)
self.setHeaderHidden(True)
self.setIndentation(2)
# self.setIconSize(iSz)
# self.setFrameStyle(QFrame.Shape.NoFrame)
# self.setUniformRowHeights(True)
# self.setAllColumnsShowFocus(True)
# self.setHeaderHidden(True)
# self.setIndentation(2)
# self.setColumnCount(4)
# self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
# self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
# self.setExpandsOnDoubleClick(False)
# self.setDragEnabled(False)
# Lock the column sizes
# if header := self.header():
# header.setStretchLastSection(False)
# header.setMinimumSectionSize(iPx + 6)
# header.setSectionResizeMode(self.C_TITLE, QtHeaderStretch)
# header.setSectionResizeMode(self.C_WORDS, QtHeaderToContents)
# header.setSectionResizeMode(self.C_EXTRA, QtHeaderToContents)
# header.setSectionResizeMode(self.C_MORE, QtHeaderToContents)
# 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()]
# Connect signals
# self.clicked.connect(self._treeItemClicked)
# self.itemDoubleClicked.connect(self._treeDoubleClick)
# self.itemSelectionChanged.connect(self._treeSelectionChange)
# Set custom settings
self.initSettings()
# self.updateTheme()
logger.debug("Ready: GuiNovelTree")
return
def initSettings(self) -> None:
"""Set or update tree widget settings."""
# Scroll bars
if CONFIG.hideVScroll:
self.setVerticalScrollBarPolicy(QtScrollAlwaysOff)
else:
self.setVerticalScrollBarPolicy(QtScrollAsNeeded)
if CONFIG.hideHScroll:
self.setHorizontalScrollBarPolicy(QtScrollAlwaysOff)
else:
self.setHorizontalScrollBarPolicy(QtScrollAsNeeded)
return
def updateTheme(self) -> None:
"""Update theme elements."""
# iPx = SHARED.theme.baseIconHeight
# self._pMore = SHARED.theme.getPixmap("more_arrow", (iPx, iPx))
return
##
# Properties
##
# @property
# def lastColType(self) -> NovelTreeColumn:
# return self._lastCol
# @property
# def lastColSize(self) -> int:
# return int(self._lastColSize * 100)
##
# Class Methods
##
def clearContent(self) -> None:
"""Clear the GUI content and the related maps."""
# self.clear()
# self._treeMap = {}
# self._lastBuild = 0
return
def resizeColumsn(self) -> None:
"""Set the correct column sizes."""
if header := self.header():
header.setStretchLastSection(False)
header.setMinimumSectionSize(SHARED.theme.baseIconHeight + 6)
header.setSectionResizeMode(self.C_TITLE, QtHeaderStretch)
header.setSectionResizeMode(self.C_WORDS, QtHeaderToContents)
header.setSectionResizeMode(self.C_EXTRA, QtHeaderToContents)
header.setSectionResizeMode(self.C_MORE, QtHeaderToContents)
return
# def refreshTree(self, rootHandle: str | None = None, overRide: bool = False) -> None:
# """Refresh the tree if it has been changed."""
# logger.debug("Requesting refresh of the novel tree")
# if rootHandle is None:
# rootHandle = SHARED.project.tree.findRoot(nwItemClass.NOVEL)
# titleKey = None
# if selItems := self.selectedItems():
# titleKey = selItems[0].data(self.C_DATA, self.D_KEY)
# self._populateTree(rootHandle)
# SHARED.project.data.setLastHandle(rootHandle, "novelTree")
# if titleKey is not None and titleKey in self._treeMap:
# self._treeMap[titleKey].setSelected(True)
# return
# def refreshHandle(self, tHandle: str) -> None:
# """Refresh the data for a given handle."""
# if idxData := SHARED.project.index.getItemData(tHandle):
# logger.debug("Refreshing meta data for item '%s'", tHandle)
# for sTitle, tHeading in idxData.items():
# sKey = f"{tHandle}:{sTitle}"
# if trItem := self._treeMap.get(sKey, None):
# self._updateTreeItemValues(trItem, tHeading, tHandle, sTitle)
# else:
# logger.debug("Heading '%s' not in novel tree", sKey)
# self.refreshTree()
# return
# return
def getSelectedHandle(self) -> tuple[str | None, str | None]:
"""Get the currently selected or active handle. If multiple
items are selected, return the first.
"""
# selList = self.selectedItems()
# trItem = selList[0] if selList else self.currentItem()
# if isinstance(trItem, QTreeWidgetItem):
# tHandle = trItem.data(self.C_DATA, self.D_HANDLE)
# sTitle = trItem.data(self.C_DATA, self.D_TITLE)
# return tHandle, sTitle
return None, None
# def setLastColType(self, colType: NovelTreeColumn, doRefresh: bool = True) -> None:
# """Change the content type of the last column and rebuild."""
# if self._lastCol != colType:
# logger.debug("Changing last column to %s", colType.name)
# self._lastCol = colType
# self.setColumnHidden(self.C_EXTRA, colType == NovelTreeColumn.HIDDEN)
# if doRefresh:
# lastNovel = SHARED.project.data.getLastHandle("novelTree")
# self.refreshTree(rootHandle=lastNovel, overRide=True)
# return
# def setLastColSize(self, colSize: int) -> None:
# """Set the column size in integer values between 15 and 75."""
# self._lastColSize = minmax(colSize, 15, 75)/100.0
# return
# def setActiveHandle(self, tHandle: str | None) -> None:
# """Highlight the rows associated with a given handle."""
# didScroll = False
# brushOn = self.palette().alternateBase()
# brushOff = self.palette().base()
# if pHandle := self._actHandle:
# for key, item in self._treeMap.items():
# if key.startswith(pHandle):
# for i in range(self.columnCount()):
# item.setBackground(i, brushOff)
# if tHandle:
# for key, item in self._treeMap.items():
# if key.startswith(tHandle):
# for i in range(self.columnCount()):
# item.setBackground(i, brushOn)
# if not didScroll:
# self.scrollToItem(item, QAbstractItemView.ScrollHint.PositionAtCenter)
# didScroll = True
# self._actHandle = tHandle or None
# return
# ##
# # Events
# ##
# def mousePressEvent(self, event: QMouseEvent) -> None:
# """Overload mousePressEvent to clear selection if clicking the
# mouse in a blank area of the tree view, and to load a document
# for viewing if the user middle-clicked.
# """
# super().mousePressEvent(event)
# if event.button() == QtMouseLeft:
# selItem = self.indexAt(event.pos())
# if not selItem.isValid():
# self.clearSelection()
# elif event.button() == QtMouseMiddle:
# selItem = self.itemAt(event.pos())
# if not isinstance(selItem, QTreeWidgetItem):
# return
# tHandle, sTitle = self.getSelectedHandle()
# if tHandle is None:
# return
# self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle or "", False)
# return
# def focusOutEvent(self, event: QFocusEvent) -> None:
# """Clear the selection when the tree no longer has focus."""
# super().focusOutEvent(event)
# self.clearSelection()
# return
# def resizeEvent(self, event: QResizeEvent) -> None:
# """Elide labels in the extra column."""
# super().resizeEvent(event)
# newW = event.size().width()
# oldW = event.oldSize().width()
# if newW != oldW:
# eliW = int(self._lastColSize * newW)
# fMetric = self.fontMetrics()
# for i in range(self.topLevelItemCount()):
# trItem = self.topLevelItem(i)
# if isinstance(trItem, QTreeWidgetItem):
# lastText = trItem.data(self.C_DATA, self.D_EXTRA)
# trItem.setText(
# self.C_EXTRA,
# fMetric.elidedText(lastText, Qt.TextElideMode.ElideRight, eliW)
# )
# return
##
# Private Slots
##
# @pyqtSlot("QModelIndex")
# def _treeItemClicked(self, index: QModelIndex) -> None:
# """The user clicked on an item in the tree."""
# if index.column() == self.C_MORE:
# tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE)
# sTitle = index.siblingAtColumn(self.C_DATA).data(self.D_TITLE)
# tipPos = self.mapToGlobal(self.visualRect(index).topRight())
# self._popMetaBox(tipPos, tHandle, sTitle)
# return
# @pyqtSlot()
# def _treeSelectionChange(self) -> None:
# """Extract the handle and line number of the currently selected
# title, and send it to the tree meta panel.
# """
# tHandle, _ = self.getSelectedHandle()
# if tHandle is not None:
# self.novelView.selectedItemChanged.emit(tHandle)
# return
# @pyqtSlot("QTreeWidgetItem*", int)
# def _treeDoubleClick(self, item: QTreeWidgetItem, column: 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()
# self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True)
# return
##
# Internal Functions
##
# def _populateTree(self, rootHandle: str | None) -> None:
# """Build the tree based on the project index."""
# self.clearContent()
# tStart = time()
# logger.debug("Building novel tree for root item '%s'", rootHandle)
# novStruct = SHARED.project.index.novelStructure(rootHandle=rootHandle, activeOnly=True)
# for tKey, tHandle, sTitle, novIdx in novStruct:
# if novIdx.level == "H0":
# continue
# newItem = QTreeWidgetItem()
# newItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
# newItem.setData(self.C_DATA, self.D_TITLE, sTitle)
# newItem.setData(self.C_DATA, self.D_KEY, tKey)
# newItem.setTextAlignment(self.C_WORDS, QtAlignRight)
# self._updateTreeItemValues(newItem, novIdx, tHandle, sTitle)
# self._treeMap[tKey] = newItem
# self.addTopLevelItem(newItem)
# self.setActiveHandle(self._actHandle)
# logger.debug("Novel Tree built in %.3f ms", (time() - tStart)*1000)
# self._lastBuild = time()
# return
# def _updateTreeItemValues(
# self, trItem: QTreeWidgetItem, idxItem: IndexHeading, tHandle: str, sTitle: str
# ) -> None:
# """Set the tree item values from the index entry."""
# iLevel = nwStyles.H_LEVEL.get(idxItem.level, 0)
# hDec = SHARED.theme.getHeaderDecoration(iLevel)
# trItem.setData(self.C_TITLE, QtDecoration, hDec)
# trItem.setText(self.C_TITLE, idxItem.title)
# trItem.setFont(self.C_TITLE, self._hFonts[iLevel])
# trItem.setText(self.C_WORDS, f"{idxItem.wordCount:n}")
# trItem.setData(self.C_MORE, QtDecoration, self._pMore)
# # Custom column
# viewport = self.viewport()
# mW = int(self._lastColSize * (viewport.width() if viewport else 100))
# lastText, toolTip = self._getLastColumnText(tHandle, sTitle)
# elideText = self.fontMetrics().elidedText(lastText, Qt.TextElideMode.ElideRight, mW)
# trItem.setText(self.C_EXTRA, elideText)
# trItem.setData(self.C_DATA, self.D_EXTRA, lastText)
# trItem.setToolTip(self.C_EXTRA, toolTip)
# return
# def _getLastColumnText(self, tHandle: str, sTitle: str) -> tuple[str, str]:
# """Generate text for the last column based on user settings."""
# if self._lastCol == NovelTreeColumn.HIDDEN:
# return "", ""
# refData = []
# refName = ""
# refs = SHARED.project.index.getReferences(tHandle, sTitle)
# if self._lastCol == NovelTreeColumn.POV:
# refData = refs[nwKeyWords.POV_KEY]
# refName = self._povLabel
# elif self._lastCol == NovelTreeColumn.FOCUS:
# refData = refs[nwKeyWords.FOCUS_KEY]
# refName = self._focLabel
# elif self._lastCol == NovelTreeColumn.PLOT:
# refData = refs[nwKeyWords.PLOT_KEY]
# refName = self._pltLabel
# if refData:
# toolText = ", ".join(refData)
# return toolText, f"{refName}: {toolText}"
# return "", ""
# def _popMetaBox(self, qPos: QPoint, tHandle: str, sTitle: str) -> None:
# """Show the novel meta data box."""
# logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
# pIndex = SHARED.project.index
# novIdx = pIndex.getItemHeading(tHandle, sTitle)
# refTags = pIndex.getReferences(tHandle, sTitle)
# if not novIdx:
# return
# synopText = novIdx.synopsis
# if synopText:
# synopLabel = trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP])
# synopText = f"<p><b>{synopLabel}</b>: {synopText}</p>"
# refLines = []
# refLines = self._appendMetaTag(refTags, nwKeyWords.POV_KEY, refLines)
# refLines = self._appendMetaTag(refTags, nwKeyWords.FOCUS_KEY, refLines)
# refLines = self._appendMetaTag(refTags, nwKeyWords.CHAR_KEY, refLines)
# refLines = self._appendMetaTag(refTags, nwKeyWords.PLOT_KEY, refLines)
# refLines = self._appendMetaTag(refTags, nwKeyWords.TIME_KEY, refLines)
# refLines = self._appendMetaTag(refTags, nwKeyWords.WORLD_KEY, refLines)
# refLines = self._appendMetaTag(refTags, nwKeyWords.OBJECT_KEY, refLines)
# refLines = self._appendMetaTag(refTags, nwKeyWords.ENTITY_KEY, refLines)
# refLines = self._appendMetaTag(refTags, nwKeyWords.CUSTOM_KEY, refLines)
# refText = ""
# if refLines:
# refList = "<br>".join(refLines)
# refText = f"<p>{refList}</p>"
# ttText = refText + synopText or self.tr("No meta data")
# if ttText:
# QToolTip.showText(qPos, ttText)
# return
# @staticmethod
# def _appendMetaTag(refs: dict, key: str, lines: list[str]) -> list[str]:
# """Generate a reference list for a given reference key."""
# tags = ", ".join(refs.get(key, []))
# if tags:
# lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}</b>: {tags}")
# return lines