"""
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 .
"""
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"
{synopLabel}: {synopText}
"
# 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 = "
".join(refLines)
# refText = f"{refList}
"
# 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"{trConst(nwLabels.KEY_NAME[key])}: {tags}")
# return lines