Re-implement basic novel view functionality

This commit is contained in:
Veronica Berglyd Olsen
2025-02-22 21:54:24 +01:00
parent 9a63a84fc1
commit 71ad7c3229
5 changed files with 153 additions and 117 deletions
+1 -1
View File
@@ -123,7 +123,7 @@ class Index:
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)
model = NovelModel()
for handle in SHARED.project.tree.subTree(tHandle):
if node := self._itemIndex[handle]:
model.append(node)
+40 -19
View File
@@ -31,37 +31,45 @@ 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
R_TEXT = Qt.ItemDataRole.DisplayRole
R_ICON = Qt.ItemDataRole.DecorationRole
R_ALIGN = Qt.ItemDataRole.TextAlignmentRole
R_TIP = Qt.ItemDataRole.ToolTipRole
T_NodeData = str | QIcon | QPixmap | Qt.AlignmentFlag | None
class NovelModel(QAbstractTableModel):
def __init__(self, rootItem: NWItem) -> None:
__slots__ = ("_meta", "_rows", "_more", "_columns")
def __init__(self) -> None:
super().__init__()
self._root = rootItem
self._rows: list[tuple[str, str, dict]] = []
self._meta: list[tuple[str, str]] = []
self._rows: list[dict[int, T_NodeData]] = []
self._more = SHARED.theme.getIcon("more_arrow")
self._columns = 3
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NovelModel")
return
##
# Setters
##
def setExtraColumn(self, state: bool) -> None:
"""Set extra data column settings."""
self._columns = 4 if state else 3
return
##
# Model Interface
##
@@ -72,29 +80,42 @@ class NovelModel(QAbstractTableModel):
def columnCount(self, index: QModelIndex) -> int:
"""Return the number of columns for an entry."""
return 4
return self._columns
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 self._rows[row].get(C_FACTOR*index.column() | role)
return None
def keys(self, index: QModelIndex) -> tuple[str | None, str | None]:
"""Return display data for a node."""
if index.isValid() and (row := index.row()) < len(self._rows):
return self._meta[row]
return None, None
##
# Data Methods
##
def clear(self) -> None:
"""Clear the model."""
self._rows.clear()
return
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)
more = self._columns - 1
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))
data[C_FACTOR*0 | R_TEXT] = head.title
data[C_FACTOR*0 | R_ICON] = SHARED.theme.getHeaderDecoration(iLevel)
data[C_FACTOR*1 | R_TEXT] = f"{head.mainCount:n}"
data[C_FACTOR*1 | R_ALIGN] = QtAlignRight
data[C_FACTOR*more | R_ICON] = self._more
self._meta.append((handle, key))
self._rows.append(data)
return
+14 -3
View File
@@ -30,15 +30,15 @@ from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING
from PyQt6.QtCore import QSize, Qt, pyqtSignal, pyqtSlot
from PyQt6.QtCore import QModelIndex, QSize, Qt, pyqtSignal, pyqtSlot
from PyQt6.QtGui import QMouseEvent, QWheelEvent
from PyQt6.QtWidgets import (
QApplication, QComboBox, QDialog, QDoubleSpinBox, QLabel, QSpinBox,
QToolButton, QWidget
QToolButton, QTreeView, QWidget
)
from novelwriter import CONFIG, SHARED
from novelwriter.types import QtMouseLeft
from novelwriter.types import QtMouseLeft, QtMouseMiddle
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
@@ -99,6 +99,17 @@ class NNonBlockingDialog(NDialog):
return
class NTreeView(QTreeView):
middleClicked = pyqtSignal(QModelIndex)
def mousePressEvent(self, event: QMouseEvent | None) -> None:
"""Emit a signal on mouse middle click."""
if event and event.button() == QtMouseMiddle:
self.middleClicked.emit(self.indexAt(event.pos()))
return super().mousePressEvent(event)
class NComboBox(QComboBox):
def __init__(self, parent: QWidget | None = None) -> None:
+97 -93
View File
@@ -29,17 +29,15 @@ import logging
from enum import Enum
from PyQt6.QtCore import pyqtSignal, pyqtSlot
from PyQt6.QtCore import QModelIndex, pyqtSignal, pyqtSlot
from PyQt6.QtGui import QActionGroup, QFont, QPalette
from PyQt6.QtWidgets import (
QFrame, QHBoxLayout, QMenu, QTreeView, QTreeWidgetItem, QVBoxLayout,
QWidget
)
from PyQt6.QtWidgets import QAbstractItemView, QFrame, QHBoxLayout, QMenu, 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.core.novelmodel import NovelModel
from novelwriter.enum import nwChange, nwDocMode, nwItemClass
from novelwriter.extensions.modified import NIconToolButton, NTreeView
from novelwriter.extensions.novelselector import NovelSelector
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
from novelwriter.types import (
@@ -93,7 +91,6 @@ class GuiNovelView(QWidget):
def updateTheme(self) -> None:
"""Update theme elements."""
self.novelBar.updateTheme()
self.novelTree.updateTheme()
self.refreshTree()
return
@@ -104,7 +101,7 @@ class GuiNovelView(QWidget):
def clearNovelView(self) -> None:
"""Clear project-related GUI content."""
self.novelTree.clearContent()
# self.novelTree.clearContent()
self.novelBar.clearContent()
self.novelBar.setEnabled(False)
return
@@ -351,7 +348,7 @@ class GuiNovelToolBar(QWidget):
return
class GuiNovelTree(QTreeView):
class GuiNovelTree(NTreeView):
C_DATA = 0
C_TITLE = 0
@@ -372,68 +369,36 @@ class GuiNovelTree(QTreeView):
self.novelView = novelView
# Internal Variables
self._lastBuild = 0
self._lastCol = NovelTreeColumn.POV
self._lastColSize = 0.25
self._actHandle = None
self._treeMap: dict[str, QTreeWidgetItem] = {}
# 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.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)
# 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()]
# Set selection options
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
# Connect signals
# self.clicked.connect(self._treeItemClicked)
# self.itemDoubleClicked.connect(self._treeDoubleClick)
# self.itemSelectionChanged.connect(self._treeSelectionChange)
self.clicked.connect(self._onSingleClick)
self.doubleClicked.connect(self._onDoubleClick)
self.middleClicked.connect(self._onMiddleClick)
# Set custom settings
self.initSettings()
# self.updateTheme()
logger.debug("Ready: GuiNovelTree")
@@ -441,25 +406,87 @@ class GuiNovelTree(QTreeView):
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))
##
# Getters
##
def getSelectedHandle(self) -> tuple[str | None, str | None]:
"""Get the currently selected or active handle. If multiple
items are selected, return the first.
"""
if model := self._getModel():
return model.keys(self.currentIndex())
return None, None
##
# Class Methods
##
def resizeColumsn(self) -> None:
"""Set the correct column sizes."""
if (header := self.header()) and (model := self._getModel()):
header.setStretchLastSection(False)
header.setMinimumSectionSize(SHARED.theme.baseIconHeight + 6)
header.setSectionResizeMode(0, QtHeaderStretch)
for i in range(1, model.columnCount(QModelIndex())):
header.setSectionResizeMode(i, QtHeaderToContents)
return
##
# Private Slots
##
@pyqtSlot(QModelIndex)
def _onSingleClick(self, index: QModelIndex) -> None:
"""The user single-clicked an index."""
if (model := self._getModel()) and (keys := model.keys(index)) and (tHandle := keys[0]):
self.novelView.selectedItemChanged.emit(tHandle)
return
@pyqtSlot(QModelIndex)
def _onDoubleClick(self, index: QModelIndex) -> None:
"""The user double-clicked an index."""
if (
(model := self._getModel()) and (keys := model.keys(index))
and (tHandle := keys[0]) and (sTitle := keys[1])
):
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", False)
return
@pyqtSlot(QModelIndex)
def _onMiddleClick(self, index: QModelIndex) -> None:
"""The user middle-clicked an index."""
if (
(model := self._getModel()) and (keys := model.keys(index))
and (tHandle := keys[0]) and (sTitle := keys[1])
):
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle or "", False)
return
##
# Internal Functions
##
def _getModel(self) -> NovelModel | None:
"""Return the model, if it exists."""
if isinstance(model := self.model(), NovelModel):
return model
return None
##
# Old Code
##
##
# Properties
##
@@ -476,23 +503,12 @@ class GuiNovelTree(QTreeView):
# 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 clearContent(self) -> None:
# """Clear the GUI content and the related maps."""
# 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."""
@@ -526,18 +542,6 @@ class GuiNovelTree(QTreeView):
# 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:
@@ -575,9 +579,9 @@ class GuiNovelTree(QTreeView):
# self._actHandle = tHandle or None
# return
# ##
# # Events
# ##
##
# Events
##
# def mousePressEvent(self, event: QMouseEvent) -> None:
# """Overload mousePressEvent to clear selection if clicking the
+1 -1
View File
@@ -1022,7 +1022,7 @@ class GuiProjectTree(QTreeView):
return [i for i in self.selectedIndexes() if i.column() == 0]
def _getModel(self) -> ProjectModel | None:
"""Return a project node corresponding to a model index."""
"""Return the model, if it exists."""
if isinstance(model := self.model(), ProjectModel):
return model
return None