Add a basic model for the novel view

This commit is contained in:
Veronica Berglyd Olsen
2025-02-22 20:12:50 +01:00
parent bc26d2250e
commit 9a63a84fc1
7 changed files with 497 additions and 343 deletions
+20 -17
View File
@@ -68,23 +68,23 @@ class Config:
"_backupPath",
"appName", "appHandle", "guiLocale", "guiTheme", "guiSyntax", "guiFont", "hideVScroll",
"hideHScroll", "lastNotes", "nativeFont", "iconTheme", "iconColTree", "iconColDocs",
"mainWinSize", "welcomeWinSize", "prefsWinSize", "mainPanePos", "viewPanePos",
"outlinePanePos", "autoSaveProj", "autoSaveDoc", "emphLabels", "backupOnClose",
"askBeforeBackup", "askBeforeExit", "textFont", "textWidth", "textMargin", "tabWidth",
"cursorWidth", "focusWidth", "hideFocusFooter", "showFullPath", "autoSelect", "doJustify",
"showTabsNSpaces", "showLineEndings", "showMultiSpaces", "doReplace", "doReplaceSQuote",
"doReplaceDQuote", "doReplaceDash", "doReplaceDots", "autoScroll", "autoScrollPos",
"scrollPastEnd", "dialogStyle", "allowOpenDial", "dialogLine", "narratorBreak",
"narratorDialog", "altDialogOpen", "altDialogClose", "highlightEmph", "stopWhenIdle",
"userIdleTime", "incNotesWCount", "fmtApostrophe", "fmtSQuoteOpen", "fmtSQuoteClose",
"fmtDQuoteOpen", "fmtDQuoteClose", "fmtPadBefore", "fmtPadAfter", "fmtPadThin",
"spellLanguage", "showViewerPanel", "showEditToolBar", "showSessionTime", "viewComments",
"viewSynopsis", "searchCase", "searchWord", "searchRegEx", "searchLoop", "searchNextFile",
"searchMatchCap", "searchProjCase", "searchProjWord", "searchProjRegEx", "verQtString",
"verQtValue", "verPyQtString", "verPyQtValue", "verPyString", "osType", "osLinux",
"osWindows", "osDarwin", "osUnknown", "hostName", "kernelVer", "isDebug", "memInfo",
"hasEnchant",
"hideHScroll", "lastNotes", "nativeFont", "useCharCount", "iconTheme", "iconColTree",
"iconColDocs", "mainWinSize", "welcomeWinSize", "prefsWinSize", "mainPanePos",
"viewPanePos", "outlinePanePos", "autoSaveProj", "autoSaveDoc", "emphLabels",
"backupOnClose", "askBeforeBackup", "askBeforeExit", "textFont", "textWidth", "textMargin",
"tabWidth", "cursorWidth", "focusWidth", "hideFocusFooter", "showFullPath", "autoSelect",
"doJustify", "showTabsNSpaces", "showLineEndings", "showMultiSpaces", "doReplace",
"doReplaceSQuote", "doReplaceDQuote", "doReplaceDash", "doReplaceDots", "autoScroll",
"autoScrollPos", "scrollPastEnd", "dialogStyle", "allowOpenDial", "dialogLine",
"narratorBreak", "narratorDialog", "altDialogOpen", "altDialogClose", "highlightEmph",
"stopWhenIdle", "userIdleTime", "incNotesWCount", "fmtApostrophe", "fmtSQuoteOpen",
"fmtSQuoteClose", "fmtDQuoteOpen", "fmtDQuoteClose", "fmtPadBefore", "fmtPadAfter",
"fmtPadThin", "spellLanguage", "showViewerPanel", "showEditToolBar", "showSessionTime",
"viewComments", "viewSynopsis", "searchCase", "searchWord", "searchRegEx", "searchLoop",
"searchNextFile", "searchMatchCap", "searchProjCase", "searchProjWord", "searchProjRegEx",
"verQtString", "verQtValue", "verPyQtString", "verPyQtValue", "verPyString", "osType",
"osLinux", "osWindows", "osDarwin", "osUnknown", "hostName", "kernelVer", "isDebug",
"memInfo", "hasEnchant",
)
LANG_NW = 1
@@ -157,6 +157,7 @@ class Config:
self.hideHScroll = False # Hide horizontal scroll bars on main widgets
self.lastNotes = "0x0" # The latest release notes that have been shown
self.nativeFont = True # Use native font dialog
self.useCharCount = False # Use character count as primary count
# Icons
self.iconTheme = DEF_ICONS # Icons theme
@@ -601,6 +602,7 @@ class Config:
self.hideHScroll = conf.rdBool(sec, "hidehscroll", self.hideHScroll)
self.lastNotes = conf.rdStr(sec, "lastnotes", self.lastNotes)
self.nativeFont = conf.rdBool(sec, "nativefont", self.nativeFont)
self.useCharCount = conf.rdBool(sec, "usecharcount", self.useCharCount)
# Sizes
sec = "Sizes"
@@ -717,6 +719,7 @@ class Config:
"hidehscroll": str(self.hideHScroll),
"lastnotes": str(self.lastNotes),
"nativefont": str(self.nativeFont),
"usecharcount": str(self.useCharCount),
}
conf["Sizes"] = {
+24
View File
@@ -38,6 +38,7 @@ from novelwriter import SHARED
from novelwriter.common import isHandle, isItemClass, isTitleTag, jsonEncode
from novelwriter.constants import nwFiles, nwKeyWords, nwStyles
from novelwriter.core.indexdata import NOTE_TYPES, TT_NONE, IndexHeading, IndexNode, T_NoteTypes
from novelwriter.core.novelmodel import NovelModel
from novelwriter.enum import nwComment, nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException
from novelwriter.text.comments import processComment
@@ -89,6 +90,9 @@ class Index:
self._itemIndex = ItemIndex(project)
self._indexBroken = False
# Models
self._novelModels: dict[str, NovelModel] = {}
# TimeStamps
self._indexChange = 0.0
self._rootChange = {}
@@ -106,6 +110,26 @@ class Index:
def indexBroken(self) -> bool:
return self._indexBroken
##
# Getters
##
def getNovelModel(self, tHandle: str) -> NovelModel | None:
"""Get the model for a specific novel root."""
if tHandle not in self._novelModels:
self._generateNovelModel(tHandle)
return self._novelModels.get(tHandle)
def _generateNovelModel(self, tHandle: str) -> None:
"""Generate a novel model for a specific handle."""
if (item := SHARED.project.tree[tHandle]) and item.isRootType() and item.isNovelLike():
model = NovelModel(item)
for handle in SHARED.project.tree.subTree(tHandle):
if node := self._itemIndex[handle]:
model.append(node)
self._novelModels[tHandle] = model
return
##
# Public Methods
##
+5
View File
@@ -31,6 +31,7 @@ import logging
from collections.abc import ItemsView, Sequence
from typing import TYPE_CHECKING, Literal
from novelwriter import CONFIG
from novelwriter.common import checkInt, isListInstance, isTitleTag
from novelwriter.constants import nwKeyWords, nwStyles
@@ -237,6 +238,10 @@ class IndexHeading:
def title(self) -> str:
return self._title
@property
def mainCount(self) -> int:
return self._counts[0 if CONFIG.useCharCount else 1]
@property
def charCount(self) -> int:
return self._counts[0]
+1 -1
View File
@@ -331,7 +331,7 @@ class ProjectModel(QAbstractItemModel):
return QModelIndex()
def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex:
"""get the index of a child item of a parent."""
"""Get the index of a child item of a parent."""
if self.hasIndex(row, column, parent):
node: ProjectNode = parent.internalPointer() if parent.isValid() else self._root
if child := node.child(row):
+100
View File
@@ -0,0 +1,100 @@
"""
novelWriter Novel Model
=========================
File History:
Created: 2025-02-22 [2.7b1] NovelModel
This file is a part of novelWriter
Copyright (C) 2025 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 PyQt6.QtCore import QAbstractTableModel, QModelIndex, Qt
from PyQt6.QtGui import QIcon, QPixmap
from novelwriter import SHARED
from novelwriter.constants import nwStyles
from novelwriter.core.indexdata import IndexNode
from novelwriter.core.item import NWItem
from novelwriter.types import QtAlignRight
logger = logging.getLogger(__name__)
C_FACTOR = 0x0100
C_TITLE_TEXT = 0x0000 | Qt.ItemDataRole.DisplayRole
C_TITLE_ICON = 0x0000 | Qt.ItemDataRole.DecorationRole
C_COUNT_TEXT = 0x0100 | Qt.ItemDataRole.DisplayRole
C_COUNT_ALIGN = 0x0100 | Qt.ItemDataRole.TextAlignmentRole
C_EXTRA_TEXT = 0x0200 | Qt.ItemDataRole.DisplayRole
C_EXTRA_TIP = 0x0200 | Qt.ItemDataRole.ToolTipRole
C_MORE_ICON = 0x0300 | Qt.ItemDataRole.DecorationRole
T_NodeData = str | QIcon | QPixmap | Qt.AlignmentFlag | None
class NovelModel(QAbstractTableModel):
def __init__(self, rootItem: NWItem) -> None:
super().__init__()
self._root = rootItem
self._rows: list[tuple[str, str, dict]] = []
self._more = SHARED.theme.getIcon("more_arrow")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NovelModel")
return
##
# Model Interface
##
def rowCount(self, index: QModelIndex) -> int:
"""Return the number of rows for an entry."""
return len(self._rows)
def columnCount(self, index: QModelIndex) -> int:
"""Return the number of columns for an entry."""
return 4
def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> T_NodeData:
"""Return display data for a node."""
if index.isValid() and (row := index.row()) < len(self._rows):
return self._rows[row][2].get(C_FACTOR*index.column() | role)
return None
##
# Data Methods
##
def append(self, node: IndexNode) -> None:
"""Append a node to the model."""
handle = node.handle
for key, head in node.items():
if key != "T0000":
iLevel = nwStyles.H_LEVEL.get(head.level, 0)
data = {}
data[C_TITLE_TEXT] = head.title
data[C_TITLE_ICON] = SHARED.theme.getHeaderDecoration(iLevel)
data[C_COUNT_TEXT] = f"{head.mainCount:n}"
data[C_COUNT_ALIGN] = QtAlignRight
data[C_MORE_ICON] = self._more
self._rows.append((handle, key, data))
return
+335 -314
View File
@@ -28,26 +28,22 @@ from __future__ import annotations
import logging
from enum import Enum
from time import time
from PyQt6.QtCore import QModelIndex, QPoint, Qt, pyqtSignal, pyqtSlot
from PyQt6.QtGui import QActionGroup, QFocusEvent, QFont, QMouseEvent, QPalette, QResizeEvent
from PyQt6.QtCore import pyqtSignal, pyqtSlot
from PyQt6.QtGui import QActionGroup, QFont, QPalette
from PyQt6.QtWidgets import (
QAbstractItemView, QFrame, QHBoxLayout, QInputDialog, QMenu, QToolTip,
QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
QFrame, QHBoxLayout, QMenu, QTreeView, QTreeWidgetItem, QVBoxLayout,
QWidget
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import minmax, qtAddAction, qtAddMenu, qtLambda
from novelwriter.constants import nwKeyWords, nwLabels, nwStyles, trConst
from novelwriter.core.indexdata import IndexHeading
from novelwriter.enum import nwChange, nwDocMode, nwItemClass, nwOutline
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 (
QtAlignRight, QtDecoration, QtHeaderStretch, QtHeaderToContents,
QtMouseLeft, QtMouseMiddle, QtScrollAlwaysOff, QtScrollAsNeeded,
QtHeaderStretch, QtHeaderToContents, QtScrollAlwaysOff, QtScrollAsNeeded,
QtSizeExpanding, QtUserRole
)
@@ -124,9 +120,9 @@ class GuiNovelView(QWidget):
lastCol = SHARED.project.options.getEnum(
"GuiNovelView", "lastCol", NovelTreeColumn, NovelTreeColumn.HIDDEN
)
lastColSize = SHARED.project.options.getInt(
"GuiNovelView", "lastColSize", 25
)
# lastColSize = SHARED.project.options.getInt(
# "GuiNovelView", "lastColSize", 25
# )
self.clearNovelView()
self.novelBar.buildNovelRootMenu()
@@ -134,18 +130,18 @@ class GuiNovelView(QWidget):
self.novelBar.setCurrentRoot(lastNovel)
self.novelBar.setEnabled(True)
self.novelTree.setLastColSize(lastColSize)
# 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)
# 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
@@ -162,16 +158,24 @@ class GuiNovelView(QWidget):
# 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)
# self.novelTree.setActiveHandle(tHandle)
return
@pyqtSlot()
def refreshTree(self) -> None:
"""Refresh the current tree."""
self.novelTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("novelTree"))
# self.novelTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("novelTree"))
return
@pyqtSlot(str, Enum)
@@ -185,7 +189,7 @@ class GuiNovelView(QWidget):
"""The meta data of a novel item has changed, and the tree item
needs to be refreshed.
"""
self.novelTree.refreshHandle(tHandle)
# self.novelTree.refreshHandle(tHandle)
return
@@ -301,13 +305,14 @@ class GuiNovelToolBar(QWidget):
def setCurrentRoot(self, rootHandle: str | None) -> None:
"""Set the current active root handle."""
self.novelValue.setHandle(rootHandle)
self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True)
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)
# self.aLastCol[colType].setChecked(True)
# self.novelView.novelTree.setLastColType(colType, doRefresh=doRefresh)
return
##
@@ -317,20 +322,19 @@ class GuiNovelToolBar(QWidget):
@pyqtSlot()
def _refreshNovelTree(self) -> None:
"""Rebuild the current tree."""
rootHandle = SHARED.project.data.getLastHandle("novelTree")
self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True)
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()
# 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
##
@@ -347,7 +351,7 @@ class GuiNovelToolBar(QWidget):
return
class GuiNovelTree(QTreeWidget):
class GuiNovelTree(QTreeView):
C_DATA = 0
C_TITLE = 0
@@ -375,55 +379,61 @@ class GuiNovelTree(QTreeWidget):
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])
# 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
iSz = SHARED.theme.baseIconSize
# iPx = SHARED.theme.baseIconHeight
self.setIconSize(iSz)
self.setIconSize(SHARED.theme.baseIconSize)
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)
# 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)
# 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)
# fH1 = self.font()
# fH1.setBold(True)
# fH1.setUnderline(True)
fH2 = self.font()
fH2.setBold(True)
# fH2 = self.font()
# fH2.setBold(True)
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
# 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)
# self.clicked.connect(self._treeItemClicked)
# self.itemDoubleClicked.connect(self._treeDoubleClick)
# self.itemSelectionChanged.connect(self._treeSelectionChange)
# Set custom settings
self.initSettings()
self.updateTheme()
# self.updateTheme()
logger.debug("Ready: GuiNovelTree")
@@ -446,21 +456,21 @@ class GuiNovelTree(QTreeWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
iPx = SHARED.theme.baseIconHeight
self._pMore = SHARED.theme.getPixmap("more_arrow", (iPx, iPx))
# 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 lastColType(self) -> NovelTreeColumn:
# return self._lastCol
@property
def lastColSize(self) -> int:
return int(self._lastColSize * 100)
# @property
# def lastColSize(self) -> int:
# return int(self._lastColSize * 100)
##
# Class Methods
@@ -468,302 +478,313 @@ class GuiNovelTree(QTreeWidget):
def clearContent(self) -> None:
"""Clear the GUI content and the related maps."""
self.clear()
self._treeMap = {}
self._lastBuild = 0
# self.clear()
# self._treeMap = {}
# self._lastBuild = 0
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)
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 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 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
# 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 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 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
# 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
##
# ##
# # 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)
# 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()
# 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
# elif event.button() == QtMouseMiddle:
# selItem = self.itemAt(event.pos())
# if not isinstance(selItem, QTreeWidgetItem):
# return
tHandle, sTitle = self.getSelectedHandle()
if tHandle is None:
return
# tHandle, sTitle = self.getSelectedHandle()
# if tHandle is None:
# return
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle or "", False)
# self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle or "", False)
return
# return
def focusOutEvent(self, event: QFocusEvent) -> None:
"""Clear the selection when the tree no longer has focus."""
super().focusOutEvent(event)
self.clearSelection()
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
# 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("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()
# 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
# @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)
# 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
# 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)
# 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._updateTreeItemValues(newItem, novIdx, tHandle, sTitle)
# self._treeMap[tKey] = newItem
# self.addTopLevelItem(newItem)
self.setActiveHandle(self._actHandle)
# self.setActiveHandle(self._actHandle)
logger.debug("Novel Tree built in %.3f ms", (time() - tStart)*1000)
self._lastBuild = time()
# logger.debug("Novel Tree built in %.3f ms", (time() - tStart)*1000)
# self._lastBuild = time()
return
# 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)
# 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)
# 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)
# # 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
# 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 "", ""
# 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
# 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.FOCUS:
# refData = refs[nwKeyWords.FOCUS_KEY]
# refName = self._focLabel
elif self._lastCol == NovelTreeColumn.PLOT:
refData = refs[nwKeyWords.PLOT_KEY]
refName = self._pltLabel
# elif self._lastCol == NovelTreeColumn.PLOT:
# refData = refs[nwKeyWords.PLOT_KEY]
# refName = self._pltLabel
if refData:
toolText = ", ".join(refData)
return toolText, f"{refName}: {toolText}"
# if refData:
# toolText = ", ".join(refData)
# return toolText, f"{refName}: {toolText}"
return "", ""
# 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)
# 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
# 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>"
# 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)
# 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>"
# 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)
# ttText = refText + synopText or self.tr("No meta data")
# if ttText:
# QToolTip.showText(qPos, ttText)
return
# 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
# @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
+2 -1
View File
@@ -1,5 +1,5 @@
[Meta]
timestamp = 2025-02-06 11:05:16
timestamp = 2025-02-22 19:57:47
[Main]
font =
@@ -13,6 +13,7 @@ hidevscroll = False
hidehscroll = False
lastnotes = 0x0
nativefont = True
usecharcount = False
[Sizes]
mainwindow = 1200, 650