Add back extra column content
This commit is contained in:
@@ -87,7 +87,7 @@ class Index:
|
|||||||
|
|
||||||
# Storage and State
|
# Storage and State
|
||||||
self._tagsIndex = TagsIndex()
|
self._tagsIndex = TagsIndex()
|
||||||
self._itemIndex = ItemIndex(project)
|
self._itemIndex = ItemIndex(project, self._tagsIndex)
|
||||||
self._indexBroken = False
|
self._indexBroken = False
|
||||||
|
|
||||||
# Models
|
# Models
|
||||||
@@ -906,10 +906,11 @@ class ItemIndex:
|
|||||||
IndexHeading object for each heading of the text.
|
IndexHeading object for each heading of the text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_project", "_items")
|
__slots__ = ("_project", "_tags", "_items")
|
||||||
|
|
||||||
def __init__(self, project: NWProject) -> None:
|
def __init__(self, project: NWProject, tagsIndex: TagsIndex) -> None:
|
||||||
self._project = project
|
self._project = project
|
||||||
|
self._tags = tagsIndex
|
||||||
self._items: dict[str, IndexNode] = {}
|
self._items: dict[str, IndexNode] = {}
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -936,7 +937,7 @@ class ItemIndex:
|
|||||||
"""Add a new item to the index. This will overwrite the item if
|
"""Add a new item to the index. This will overwrite the item if
|
||||||
it already exists.
|
it already exists.
|
||||||
"""
|
"""
|
||||||
self._items[tHandle] = IndexNode(tHandle, nwItem)
|
self._items[tHandle] = IndexNode(self._tags, tHandle, nwItem)
|
||||||
return
|
return
|
||||||
|
|
||||||
def allItemTags(self, tHandle: str) -> list[str]:
|
def allItemTags(self, tHandle: str) -> list[str]:
|
||||||
@@ -994,7 +995,7 @@ class ItemIndex:
|
|||||||
if tHandle in self._items:
|
if tHandle in self._items:
|
||||||
tItem = self._items[tHandle]
|
tItem = self._items[tHandle]
|
||||||
sTitle = tItem.nextHeading()
|
sTitle = tItem.nextHeading()
|
||||||
tItem.addHeading(IndexHeading(sTitle, lineNo, level, text))
|
tItem.addHeading(IndexHeading(self._tags, sTitle, lineNo, level, text))
|
||||||
return sTitle
|
return sTitle
|
||||||
return TT_NONE
|
return TT_NONE
|
||||||
|
|
||||||
@@ -1065,7 +1066,7 @@ class ItemIndex:
|
|||||||
|
|
||||||
nwItem = self._project.tree[tHandle]
|
nwItem = self._project.tree[tHandle]
|
||||||
if nwItem is not None:
|
if nwItem is not None:
|
||||||
tItem = IndexNode(tHandle, nwItem)
|
tItem = IndexNode(self._tags, tHandle, nwItem)
|
||||||
tItem.unpackData(tData)
|
tItem.unpackData(tData)
|
||||||
self._items[tHandle] = tItem
|
self._items[tHandle] = tItem
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from novelwriter.common import checkInt, isListInstance, isTitleTag
|
|||||||
from novelwriter.constants import nwKeyWords, nwStyles
|
from novelwriter.constants import nwKeyWords, nwStyles
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from novelwriter.core.index import TagsIndex
|
||||||
from novelwriter.core.item import NWItem
|
from novelwriter.core.item import NWItem
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -56,12 +57,13 @@ class IndexNode:
|
|||||||
must be reset each time the item is re-indexed.
|
must be reset each time the item is re-indexed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_handle", "_item", "_headings", "_count", "_notes")
|
__slots__ = ("_tags", "_handle", "_item", "_headings", "_notes", "_count")
|
||||||
|
|
||||||
def __init__(self, tHandle: str, nwItem: NWItem) -> None:
|
def __init__(self, tagsIndex: TagsIndex, tHandle: str, nwItem: NWItem) -> None:
|
||||||
|
self._tags = tagsIndex
|
||||||
self._handle = tHandle
|
self._handle = tHandle
|
||||||
self._item = nwItem
|
self._item = nwItem
|
||||||
self._headings: dict[str, IndexHeading] = {TT_NONE: IndexHeading(TT_NONE)}
|
self._headings: dict[str, IndexHeading] = {TT_NONE: IndexHeading(self._tags, TT_NONE)}
|
||||||
self._notes: dict[str, set[str]] = {}
|
self._notes: dict[str, set[str]] = {}
|
||||||
self._count = 0
|
self._count = 0
|
||||||
return
|
return
|
||||||
@@ -179,7 +181,7 @@ class IndexNode:
|
|||||||
"""Unpack an item entry from the data."""
|
"""Unpack an item entry from the data."""
|
||||||
for key, entry in data.items():
|
for key, entry in data.items():
|
||||||
if isTitleTag(key):
|
if isTitleTag(key):
|
||||||
heading = IndexHeading(key)
|
heading = IndexHeading(self._tags, key)
|
||||||
heading.unpackData(entry)
|
heading.unpackData(entry)
|
||||||
self.addHeading(heading)
|
self.addHeading(heading)
|
||||||
elif key == "document":
|
elif key == "document":
|
||||||
@@ -202,9 +204,16 @@ class IndexHeading:
|
|||||||
of all references made under the heading.
|
of all references made under the heading.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_key", "_line", "_level", "_title", "_counts", "_tag", "_refs", "_comments")
|
__slots__ = (
|
||||||
|
"_tags", "_key", "_line", "_level", "_title",
|
||||||
|
"_counts", "_tag", "_refs", "_comments",
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, key: str, line: int = 0, level: str = "H0", title: str = "") -> None:
|
def __init__(
|
||||||
|
self, tagsIndex: TagsIndex, key: str, line: int = 0,
|
||||||
|
level: str = "H0", title: str = "",
|
||||||
|
) -> None:
|
||||||
|
self._tags = tagsIndex
|
||||||
self._key = key
|
self._key = key
|
||||||
self._line = line
|
self._line = line
|
||||||
self._level = level
|
self._level = level
|
||||||
@@ -314,6 +323,27 @@ class IndexHeading:
|
|||||||
self._refs[tag].add(keyword)
|
self._refs[tag].add(keyword)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Getters
|
||||||
|
##
|
||||||
|
|
||||||
|
def getReferences(self) -> dict[str, list[str]]:
|
||||||
|
"""Extract all references for this heading."""
|
||||||
|
refs = {x: [] for x in nwKeyWords.VALID_KEYS}
|
||||||
|
for tag, types in self._refs.items():
|
||||||
|
for keyword in types:
|
||||||
|
if keyword in refs:
|
||||||
|
refs[keyword].append(self._tags.tagName(tag))
|
||||||
|
return refs
|
||||||
|
|
||||||
|
def getReferencesByKeyword(self, keyword: str) -> list[str]:
|
||||||
|
"""Extract all references for this heading."""
|
||||||
|
refs = []
|
||||||
|
for tag, types in self._refs.items():
|
||||||
|
if keyword in types:
|
||||||
|
refs.append(self._tags.tagName(tag))
|
||||||
|
return refs
|
||||||
|
|
||||||
##
|
##
|
||||||
# Data Methods
|
# Data Methods
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from PyQt6.QtCore import QAbstractTableModel, QModelIndex, Qt
|
|||||||
from PyQt6.QtGui import QIcon, QPixmap
|
from PyQt6.QtGui import QIcon, QPixmap
|
||||||
|
|
||||||
from novelwriter import SHARED
|
from novelwriter import SHARED
|
||||||
from novelwriter.constants import nwKeyWords, nwStyles
|
from novelwriter.constants import nwKeyWords, nwLabels, nwStyles, trConst
|
||||||
from novelwriter.core.indexdata import IndexHeading, IndexNode
|
from novelwriter.core.indexdata import IndexHeading, IndexNode
|
||||||
from novelwriter.enum import nwNovelExtra
|
from novelwriter.enum import nwNovelExtra
|
||||||
from novelwriter.error import logException
|
from novelwriter.error import logException
|
||||||
@@ -52,7 +52,7 @@ T_NodeData = str | tuple[str, str] | QIcon | QPixmap | Qt.AlignmentFlag | None
|
|||||||
|
|
||||||
class NovelModel(QAbstractTableModel):
|
class NovelModel(QAbstractTableModel):
|
||||||
|
|
||||||
__slots__ = ("_rows", "_header", "_more", "_columns", "_extra")
|
__slots__ = ("_rows", "_header", "_more", "_columns", "_extraKey", "_extraLabel")
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -60,7 +60,8 @@ class NovelModel(QAbstractTableModel):
|
|||||||
self._header: list[QSize] = []
|
self._header: list[QSize] = []
|
||||||
self._more = SHARED.theme.getIcon("more_arrow")
|
self._more = SHARED.theme.getIcon("more_arrow")
|
||||||
self._columns = 3
|
self._columns = 3
|
||||||
self._extra = ""
|
self._extraKey = ""
|
||||||
|
self._extraLabel = ""
|
||||||
return
|
return
|
||||||
|
|
||||||
def __del__(self) -> None: # pragma: no cover
|
def __del__(self) -> None: # pragma: no cover
|
||||||
@@ -85,16 +86,20 @@ class NovelModel(QAbstractTableModel):
|
|||||||
match extra:
|
match extra:
|
||||||
case nwNovelExtra.HIDDEN:
|
case nwNovelExtra.HIDDEN:
|
||||||
self._columns = 3
|
self._columns = 3
|
||||||
self._extra = ""
|
self._extraKey = ""
|
||||||
|
self._extraLabel = ""
|
||||||
case nwNovelExtra.POV:
|
case nwNovelExtra.POV:
|
||||||
self._columns = 4
|
self._columns = 4
|
||||||
self._extra = nwKeyWords.POV_KEY
|
self._extraKey = nwKeyWords.POV_KEY
|
||||||
|
self._extraLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
|
||||||
case nwNovelExtra.FOCUS:
|
case nwNovelExtra.FOCUS:
|
||||||
self._columns = 4
|
self._columns = 4
|
||||||
self._extra = nwKeyWords.FOCUS_KEY
|
self._extraKey = nwKeyWords.FOCUS_KEY
|
||||||
|
self._extraLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])
|
||||||
case nwNovelExtra.PLOT:
|
case nwNovelExtra.PLOT:
|
||||||
self._columns = 4
|
self._columns = 4
|
||||||
self._extra = nwKeyWords.PLOT_KEY
|
self._extraKey = nwKeyWords.PLOT_KEY
|
||||||
|
self._extraLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -209,6 +214,7 @@ class NovelModel(QAbstractTableModel):
|
|||||||
"""Generate a cache entry."""
|
"""Generate a cache entry."""
|
||||||
iLevel = nwStyles.H_LEVEL.get(heading.level, 0)
|
iLevel = nwStyles.H_LEVEL.get(heading.level, 0)
|
||||||
data = {}
|
data = {}
|
||||||
|
data[C_FACTOR*0 | R_TIP] = heading.title
|
||||||
data[C_FACTOR*0 | R_TEXT] = heading.title
|
data[C_FACTOR*0 | R_TEXT] = heading.title
|
||||||
data[C_FACTOR*0 | R_ICON] = SHARED.theme.getHeaderDecoration(iLevel)
|
data[C_FACTOR*0 | R_ICON] = SHARED.theme.getHeaderDecoration(iLevel)
|
||||||
data[C_FACTOR*1 | R_TEXT] = f"{heading.mainCount:n}"
|
data[C_FACTOR*1 | R_TEXT] = f"{heading.mainCount:n}"
|
||||||
@@ -216,8 +222,10 @@ class NovelModel(QAbstractTableModel):
|
|||||||
if self._columns == 3:
|
if self._columns == 3:
|
||||||
data[C_FACTOR*2 | R_ICON] = self._more
|
data[C_FACTOR*2 | R_ICON] = self._more
|
||||||
else:
|
else:
|
||||||
data[C_FACTOR*2 | R_TIP] = "Hello World"
|
if self._extraKey and (refs := heading.getReferencesByKeyword(self._extraKey)):
|
||||||
data[C_FACTOR*2 | R_TEXT] = "Hello World"
|
text = ", ".join(refs)
|
||||||
|
data[C_FACTOR*2 | R_TEXT] = text
|
||||||
|
data[C_FACTOR*2 | R_TIP] = f"<b>{self._extraLabel}:</b> {text}"
|
||||||
data[C_FACTOR*3 | R_ICON] = self._more
|
data[C_FACTOR*3 | R_ICON] = self._more
|
||||||
data[R_HANDLE] = handle
|
data[R_HANDLE] = handle
|
||||||
data[R_KEY] = key
|
data[R_KEY] = key
|
||||||
|
|||||||
+16
-251
@@ -383,14 +383,8 @@ class GuiNovelTree(NTreeView):
|
|||||||
self._actHandle = None
|
self._actHandle = None
|
||||||
self._lastColType = nwNovelExtra.POV
|
self._lastColType = nwNovelExtra.POV
|
||||||
self._lastColSize = 0.25
|
self._lastColSize = 0.25
|
||||||
# self._lastBuild = 0
|
|
||||||
# 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])
|
|
||||||
|
|
||||||
|
# Widget Setup
|
||||||
self.setIconSize(SHARED.theme.baseIconSize)
|
self.setIconSize(SHARED.theme.baseIconSize)
|
||||||
self.setFrameStyle(QFrame.Shape.NoFrame)
|
self.setFrameStyle(QFrame.Shape.NoFrame)
|
||||||
self.setUniformRowHeights(True)
|
self.setUniformRowHeights(True)
|
||||||
@@ -569,25 +563,26 @@ class GuiNovelTree(NTreeView):
|
|||||||
logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
|
logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
|
||||||
if synopsis := head.synopsis:
|
if synopsis := head.synopsis:
|
||||||
label = trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP])
|
label = trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP])
|
||||||
synopsis = f"<p><b>{label}</b>: {synopsis}</p>"
|
synopsis = f"<p><b>{label}:</b> {synopsis}</p>"
|
||||||
|
|
||||||
def appendTags(refs: dict, key: str, lines: list[str]) -> list[str]:
|
def appendTags(refs: dict, key: str, lines: list[str]) -> None:
|
||||||
"""Generate a reference list for a given reference key."""
|
"""Generate a reference list for a given reference key."""
|
||||||
if tags := ", ".join(refs.get(key, [])):
|
if tags := ", ".join(refs.get(key, [])):
|
||||||
lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}</b>: {tags}")
|
lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}:</b> {tags}")
|
||||||
return lines
|
return
|
||||||
|
|
||||||
tags = SHARED.project.index.getReferences(tHandle, sTitle)
|
|
||||||
lines = []
|
lines = []
|
||||||
lines = appendTags(tags, nwKeyWords.POV_KEY, lines)
|
if head := SHARED.project.index.getItemHeading(tHandle, sTitle):
|
||||||
lines = appendTags(tags, nwKeyWords.FOCUS_KEY, lines)
|
tags = head.getReferences()
|
||||||
lines = appendTags(tags, nwKeyWords.CHAR_KEY, lines)
|
appendTags(tags, nwKeyWords.POV_KEY, lines)
|
||||||
lines = appendTags(tags, nwKeyWords.PLOT_KEY, lines)
|
appendTags(tags, nwKeyWords.FOCUS_KEY, lines)
|
||||||
lines = appendTags(tags, nwKeyWords.TIME_KEY, lines)
|
appendTags(tags, nwKeyWords.CHAR_KEY, lines)
|
||||||
lines = appendTags(tags, nwKeyWords.WORLD_KEY, lines)
|
appendTags(tags, nwKeyWords.PLOT_KEY, lines)
|
||||||
lines = appendTags(tags, nwKeyWords.OBJECT_KEY, lines)
|
appendTags(tags, nwKeyWords.TIME_KEY, lines)
|
||||||
lines = appendTags(tags, nwKeyWords.ENTITY_KEY, lines)
|
appendTags(tags, nwKeyWords.WORLD_KEY, lines)
|
||||||
lines = appendTags(tags, nwKeyWords.CUSTOM_KEY, lines)
|
appendTags(tags, nwKeyWords.OBJECT_KEY, lines)
|
||||||
|
appendTags(tags, nwKeyWords.ENTITY_KEY, lines)
|
||||||
|
appendTags(tags, nwKeyWords.CUSTOM_KEY, lines)
|
||||||
|
|
||||||
text = ""
|
text = ""
|
||||||
if lines:
|
if lines:
|
||||||
@@ -596,233 +591,3 @@ class GuiNovelTree(NTreeView):
|
|||||||
if tooltip := (text + synopsis or self.tr("No meta data")):
|
if tooltip := (text + synopsis or self.tr("No meta data")):
|
||||||
QToolTip.showText(qPos, tooltip)
|
QToolTip.showText(qPos, tooltip)
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
|
||||||
# Old Code
|
|
||||||
##
|
|
||||||
|
|
||||||
##
|
|
||||||
# Class Methods
|
|
||||||
##
|
|
||||||
|
|
||||||
# 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."""
|
|
||||||
# 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 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 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
|
|
||||||
|
|
||||||
##
|
|
||||||
# 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 "", ""
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from novelwriter.core.index import TagsIndex
|
||||||
from novelwriter.core.indexdata import IndexHeading, IndexNode
|
from novelwriter.core.indexdata import IndexHeading, IndexNode
|
||||||
from novelwriter.core.item import NWItem
|
from novelwriter.core.item import NWItem
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
@@ -33,9 +34,10 @@ def testCoreIndexData_IndexNode(mockGUI):
|
|||||||
handle = "0123456789abc"
|
handle = "0123456789abc"
|
||||||
project = NWProject()
|
project = NWProject()
|
||||||
item = NWItem(project, handle)
|
item = NWItem(project, handle)
|
||||||
|
tags = TagsIndex()
|
||||||
|
|
||||||
# Defaults
|
# Defaults
|
||||||
node = IndexNode(handle, item)
|
node = IndexNode(tags, handle, item)
|
||||||
assert node.handle == handle
|
assert node.handle == handle
|
||||||
assert node.item is item
|
assert node.item is item
|
||||||
assert str(node) == f"<IndexNode handle='{handle}'>"
|
assert str(node) == f"<IndexNode handle='{handle}'>"
|
||||||
@@ -44,8 +46,8 @@ def testCoreIndexData_IndexNode(mockGUI):
|
|||||||
assert "T0000" in node # Placeholder heading
|
assert "T0000" in node # Placeholder heading
|
||||||
|
|
||||||
# Add a heading
|
# Add a heading
|
||||||
head1 = IndexHeading(node.nextHeading(), line=1, level="H1", title="Heading 1")
|
head1 = IndexHeading(tags, node.nextHeading(), line=1, level="H1", title="Heading 1")
|
||||||
head2 = IndexHeading(node.nextHeading(), line=10, level="H2", title="Heading 2")
|
head2 = IndexHeading(tags, node.nextHeading(), line=10, level="H2", title="Heading 2")
|
||||||
node.addHeading(head1)
|
node.addHeading(head1)
|
||||||
node.addHeading(head2)
|
node.addHeading(head2)
|
||||||
assert len(node) == 2
|
assert len(node) == 2
|
||||||
@@ -105,11 +107,12 @@ def testCoreIndexData_IndexNodePackUnpack(mockGUI):
|
|||||||
handle = "0123456789abc"
|
handle = "0123456789abc"
|
||||||
project = NWProject()
|
project = NWProject()
|
||||||
item = NWItem(project, handle)
|
item = NWItem(project, handle)
|
||||||
node = IndexNode(handle, item)
|
tags = TagsIndex()
|
||||||
|
node = IndexNode(tags, handle, item)
|
||||||
|
|
||||||
# Add some headings and notes
|
# Add some headings and notes
|
||||||
head1 = IndexHeading(node.nextHeading(), line=1, level="H1", title="Heading 1")
|
head1 = IndexHeading(tags, node.nextHeading(), line=1, level="H1", title="Heading 1")
|
||||||
head2 = IndexHeading(node.nextHeading(), line=10, level="H2", title="Heading 2")
|
head2 = IndexHeading(tags, node.nextHeading(), line=10, level="H2", title="Heading 2")
|
||||||
node.addHeading(head1)
|
node.addHeading(head1)
|
||||||
node.addHeading(head2)
|
node.addHeading(head2)
|
||||||
node.setHeadingCounts("T0001", 42, 13, 3)
|
node.setHeadingCounts("T0001", 42, 13, 3)
|
||||||
@@ -128,7 +131,7 @@ def testCoreIndexData_IndexNodePackUnpack(mockGUI):
|
|||||||
assert set(data["document"]["footnotes"]) == {"key1", "key2"}
|
assert set(data["document"]["footnotes"]) == {"key1", "key2"}
|
||||||
|
|
||||||
# Create a new node
|
# Create a new node
|
||||||
new = IndexNode(handle, item)
|
new = IndexNode(tags, handle, item)
|
||||||
|
|
||||||
# Unpack heading one
|
# Unpack heading one
|
||||||
data = {"T0001": {"meta": {
|
data = {"T0001": {"meta": {
|
||||||
@@ -165,7 +168,8 @@ def testCoreIndexData_IndexNodePackUnpack(mockGUI):
|
|||||||
def testCoreIndexData_IndexHeading():
|
def testCoreIndexData_IndexHeading():
|
||||||
"""Test the IndexHeading class."""
|
"""Test the IndexHeading class."""
|
||||||
# Defaults
|
# Defaults
|
||||||
head = IndexHeading("T0001")
|
tags = TagsIndex()
|
||||||
|
head = IndexHeading(tags, "T0001")
|
||||||
assert str(head) == "<IndexHeading key='T0001'>"
|
assert str(head) == "<IndexHeading key='T0001'>"
|
||||||
assert repr(head) == "<IndexHeading key='T0001'>"
|
assert repr(head) == "<IndexHeading key='T0001'>"
|
||||||
assert head.key == "T0001"
|
assert head.key == "T0001"
|
||||||
@@ -234,11 +238,13 @@ def testCoreIndexData_IndexHeading():
|
|||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreIndexData_IndexHeadingUnpackMeta():
|
def testCoreIndexData_IndexHeadingUnpackMeta():
|
||||||
"""Test IndexHeading class meta unpacking."""
|
"""Test IndexHeading class meta unpacking."""
|
||||||
|
tags = TagsIndex()
|
||||||
|
|
||||||
# Valid
|
# Valid
|
||||||
data = {"meta": {
|
data = {"meta": {
|
||||||
"level": "H1", "title": "So it Begins", "line": 1, "tag": "begins", "counts": [95, 18, 1]
|
"level": "H1", "title": "So it Begins", "line": 1, "tag": "begins", "counts": [95, 18, 1]
|
||||||
}}
|
}}
|
||||||
head = IndexHeading("T0001")
|
head = IndexHeading(tags, "T0001")
|
||||||
head.unpackData(data)
|
head.unpackData(data)
|
||||||
assert head.level == "H1"
|
assert head.level == "H1"
|
||||||
assert head.title == "So it Begins"
|
assert head.title == "So it Begins"
|
||||||
@@ -252,7 +258,7 @@ def testCoreIndexData_IndexHeadingUnpackMeta():
|
|||||||
data = {"meta": {
|
data = {"meta": {
|
||||||
"level": "H9", "title": None, "line": None, "tag": None, "counts": [42]
|
"level": "H9", "title": None, "line": None, "tag": None, "counts": [42]
|
||||||
}}
|
}}
|
||||||
head = IndexHeading("T0001")
|
head = IndexHeading(tags, "T0001")
|
||||||
head.unpackData(data)
|
head.unpackData(data)
|
||||||
assert head.level == "H0"
|
assert head.level == "H0"
|
||||||
assert head.title == "None"
|
assert head.title == "None"
|
||||||
@@ -264,7 +270,7 @@ def testCoreIndexData_IndexHeadingUnpackMeta():
|
|||||||
|
|
||||||
# Empty
|
# Empty
|
||||||
data = {"meta": {}}
|
data = {"meta": {}}
|
||||||
head = IndexHeading("T0001")
|
head = IndexHeading(tags, "T0001")
|
||||||
head.unpackData(data)
|
head.unpackData(data)
|
||||||
assert head.level == "H0"
|
assert head.level == "H0"
|
||||||
assert head.title == ""
|
assert head.title == ""
|
||||||
@@ -278,11 +284,13 @@ def testCoreIndexData_IndexHeadingUnpackMeta():
|
|||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreIndexData_IndexHeadingUnpackRefs():
|
def testCoreIndexData_IndexHeadingUnpackRefs():
|
||||||
"""Test IndexHeading class refs unpacking."""
|
"""Test IndexHeading class refs unpacking."""
|
||||||
|
tags = TagsIndex()
|
||||||
|
|
||||||
# Valid
|
# Valid
|
||||||
data = {"refs": {
|
data = {"refs": {
|
||||||
"jane": "@char,@pov", "john": "@char", "earth": "@location", "space": "@mention,@location"
|
"jane": "@char,@pov", "john": "@char", "earth": "@location", "space": "@mention,@location"
|
||||||
}}
|
}}
|
||||||
head = IndexHeading("T0001")
|
head = IndexHeading(tags, "T0001")
|
||||||
head.unpackData(data)
|
head.unpackData(data)
|
||||||
assert head.references["jane"] == {"@char", "@pov"}
|
assert head.references["jane"] == {"@char", "@pov"}
|
||||||
assert head.references["john"] == {"@char"}
|
assert head.references["john"] == {"@char"}
|
||||||
@@ -291,18 +299,18 @@ def testCoreIndexData_IndexHeadingUnpackRefs():
|
|||||||
|
|
||||||
# Invalid key
|
# Invalid key
|
||||||
data = {"refs": {0: "@char,@pov"}}
|
data = {"refs": {0: "@char,@pov"}}
|
||||||
head = IndexHeading("T0001")
|
head = IndexHeading(tags, "T0001")
|
||||||
with pytest.raises(ValueError, match="Heading reference key must be a string"):
|
with pytest.raises(ValueError, match="Heading reference key must be a string"):
|
||||||
head.unpackData(data)
|
head.unpackData(data)
|
||||||
|
|
||||||
# Invalid value
|
# Invalid value
|
||||||
data = {"refs": {"jane": None}}
|
data = {"refs": {"jane": None}}
|
||||||
head = IndexHeading("T0001")
|
head = IndexHeading(tags, "T0001")
|
||||||
with pytest.raises(ValueError, match="Heading reference value must be a string"):
|
with pytest.raises(ValueError, match="Heading reference value must be a string"):
|
||||||
head.unpackData(data)
|
head.unpackData(data)
|
||||||
|
|
||||||
# Invalid keyword
|
# Invalid keyword
|
||||||
data = {"refs": {"jane": "@char,@pov,@stuff"}}
|
data = {"refs": {"jane": "@char,@pov,@stuff"}}
|
||||||
head = IndexHeading("T0001")
|
head = IndexHeading(tags, "T0001")
|
||||||
with pytest.raises(ValueError, match="Heading reference contains an invalid keyword"):
|
with pytest.raises(ValueError, match="Heading reference contains an invalid keyword"):
|
||||||
head.unpackData(data)
|
head.unpackData(data)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import pytest
|
|||||||
from novelwriter.constants import nwFiles
|
from novelwriter.constants import nwFiles
|
||||||
from novelwriter.core.options import OptionState
|
from novelwriter.core.options import OptionState
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
from novelwriter.gui.noveltree import NovelTreeColumn
|
from novelwriter.enum import nwNovelExtra
|
||||||
|
|
||||||
from tests.mocked import causeOSError
|
from tests.mocked import causeOSError
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ def testCoreOptions_SetGet(mockGUI):
|
|||||||
project = NWProject()
|
project = NWProject()
|
||||||
options = OptionState(project)
|
options = OptionState(project)
|
||||||
|
|
||||||
nwColHidden = NovelTreeColumn.HIDDEN
|
nwColHidden = nwNovelExtra.HIDDEN
|
||||||
|
|
||||||
# Set invalid values
|
# Set invalid values
|
||||||
assert options.setValue("MockGroup", "mockItem", None) is False
|
assert options.setValue("MockGroup", "mockItem", None) is False
|
||||||
@@ -141,7 +141,7 @@ def testCoreOptions_SetGet(mockGUI):
|
|||||||
assert options.getFloat("GuiNovelDetails", "mockItem", None) is None # type: ignore
|
assert options.getFloat("GuiNovelDetails", "mockItem", None) is None # type: ignore
|
||||||
assert options.getBool("GuiNovelDetails", "clearDouble", None) is True # type: ignore
|
assert options.getBool("GuiNovelDetails", "clearDouble", None) is True # type: ignore
|
||||||
assert options.getBool("GuiNovelDetails", "mockItem", None) is None # type: ignore
|
assert options.getBool("GuiNovelDetails", "mockItem", None) is None # type: ignore
|
||||||
assert options.getEnum("GuiNovelView", "lastCol", NovelTreeColumn, nwColHidden) == nwColHidden
|
assert options.getEnum("GuiNovelView", "lastCol", nwNovelExtra, nwColHidden) == nwColHidden
|
||||||
|
|
||||||
# Get from non-existent groups
|
# Get from non-existent groups
|
||||||
assert options.getValue("SomeGroup", "mockItem", None) is None
|
assert options.getValue("SomeGroup", "mockItem", None) is None
|
||||||
@@ -149,4 +149,4 @@ def testCoreOptions_SetGet(mockGUI):
|
|||||||
assert options.getInt("SomeGroup", "mockItem", None) is None # type: ignore
|
assert options.getInt("SomeGroup", "mockItem", None) is None # type: ignore
|
||||||
assert options.getFloat("SomeGroup", "mockItem", None) is None # type: ignore
|
assert options.getFloat("SomeGroup", "mockItem", None) is None # type: ignore
|
||||||
assert options.getBool("SomeGroup", "mockItem", None) is None # type: ignore
|
assert options.getBool("SomeGroup", "mockItem", None) is None # type: ignore
|
||||||
assert options.getEnum("SomeGroup", "mockItem", NovelTreeColumn, None) is None # type: ignore
|
assert options.getEnum("SomeGroup", "mockItem", nwNovelExtra, None) is None # type: ignore
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ from PyQt6.QtWidgets import QInputDialog, QToolTip
|
|||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
from novelwriter.dialogs.editlabel import GuiEditLabel
|
from novelwriter.dialogs.editlabel import GuiEditLabel
|
||||||
from novelwriter.enum import nwFocus, nwItemType
|
from novelwriter.enum import nwFocus, nwItemType, nwNovelExtra
|
||||||
from novelwriter.gui.noveltree import GuiNovelTree, NovelTreeColumn
|
from novelwriter.gui.noveltree import GuiNovelTree
|
||||||
from novelwriter.types import QtMouseLeft, QtMouseMiddle
|
from novelwriter.types import QtMouseLeft, QtMouseMiddle
|
||||||
|
|
||||||
from tests.tools import C, buildTestProject
|
from tests.tools import C, buildTestProject
|
||||||
@@ -143,28 +143,28 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
|||||||
# Last Column
|
# Last Column
|
||||||
# ===========
|
# ===========
|
||||||
|
|
||||||
novelBar.setLastColType(NovelTreeColumn.HIDDEN)
|
novelBar.setLastColType(nwNovelExtra.HIDDEN)
|
||||||
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is True
|
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is True
|
||||||
assert novelTree.lastColType == NovelTreeColumn.HIDDEN
|
assert novelTree.lastColType == nwNovelExtra.HIDDEN
|
||||||
assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ("", "")
|
assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ("", "")
|
||||||
|
|
||||||
novelBar.setLastColType(NovelTreeColumn.PLOT)
|
novelBar.setLastColType(nwNovelExtra.PLOT)
|
||||||
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
||||||
assert novelTree.lastColType == NovelTreeColumn.PLOT
|
assert novelTree.lastColType == nwNovelExtra.PLOT
|
||||||
assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == (
|
assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == (
|
||||||
"", ""
|
"", ""
|
||||||
)
|
)
|
||||||
|
|
||||||
novelBar.setLastColType(NovelTreeColumn.FOCUS)
|
novelBar.setLastColType(nwNovelExtra.FOCUS)
|
||||||
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
||||||
assert novelTree.lastColType == NovelTreeColumn.FOCUS
|
assert novelTree.lastColType == nwNovelExtra.FOCUS
|
||||||
assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == (
|
assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == (
|
||||||
"Jane", "Focus: Jane"
|
"Jane", "Focus: Jane"
|
||||||
)
|
)
|
||||||
|
|
||||||
novelBar.setLastColType(NovelTreeColumn.POV)
|
novelBar.setLastColType(nwNovelExtra.POV)
|
||||||
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
|
||||||
assert novelTree.lastColType == NovelTreeColumn.POV
|
assert novelTree.lastColType == nwNovelExtra.POV
|
||||||
assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == (
|
assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == (
|
||||||
"Jane", "Point of View: Jane"
|
"Jane", "Point of View: Jane"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user