Merge branch 'main' into i18n-de_DE-created
This commit is contained in:
@@ -154,11 +154,11 @@ def isHandle(value):
|
||||
|
||||
|
||||
def isTitleTag(value):
|
||||
"""Check if a string is a valid title string.
|
||||
"""Check if a string is a valid title tag string.
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
if len(value) != 7:
|
||||
if len(value) != 5:
|
||||
return False
|
||||
if not value.startswith("T"):
|
||||
return False
|
||||
|
||||
@@ -61,7 +61,6 @@ class nwHeaders:
|
||||
|
||||
H_VALID = ("H0", "H1", "H2", "H3", "H4")
|
||||
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
|
||||
TT_NONE = "T000000"
|
||||
|
||||
# END Class nwHeaders
|
||||
|
||||
|
||||
+123
-115
@@ -41,6 +41,8 @@ from novelwriter.common import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TT_NONE = "T0000"
|
||||
|
||||
|
||||
class NWIndex:
|
||||
"""This class holds the entire index for a given project. The index
|
||||
@@ -102,7 +104,7 @@ class NWIndex:
|
||||
"""
|
||||
self.clearIndex()
|
||||
for nwItem in self._project.tree:
|
||||
if nwItem is not None and nwItem.isFileType():
|
||||
if nwItem.isFileType():
|
||||
tHandle = nwItem.itemHandle
|
||||
theDoc = self._project.storage.getDocument(tHandle)
|
||||
self.scanText(tHandle, theDoc.readDocument() or "")
|
||||
@@ -281,50 +283,58 @@ class NWIndex:
|
||||
def _scanActive(self, tHandle, theItem, theText, itemTags):
|
||||
"""Scan an active document for meta data.
|
||||
"""
|
||||
nTitle = 0
|
||||
findHeader = True
|
||||
theLines = theText.splitlines()
|
||||
nTitle = 0 # Line Number of the previous title
|
||||
cTitle = TT_NONE # Tag of the current title
|
||||
pTitle = TT_NONE # Tag of the previous title
|
||||
firstHeader = True # First header has been seen
|
||||
|
||||
theLines = theText.splitlines()
|
||||
for nLine, aLine in enumerate(theLines, start=1):
|
||||
|
||||
if len(aLine.strip()) == 0:
|
||||
if aLine.strip() == "":
|
||||
continue
|
||||
|
||||
if aLine.startswith("#"):
|
||||
if findHeader:
|
||||
hDepth, _ = self._splitHeading(aLine)
|
||||
if hDepth != "H0":
|
||||
theItem.setMainHeading(hDepth)
|
||||
findHeader = False
|
||||
hDepth, hText = self._splitHeading(aLine)
|
||||
if hDepth == "H0":
|
||||
continue
|
||||
|
||||
isTitle = self._indexTitle(tHandle, aLine, nLine)
|
||||
if isTitle and nLine > 0:
|
||||
if firstHeader:
|
||||
theItem.setMainHeading(hDepth)
|
||||
firstHeader = False
|
||||
|
||||
cTitle = self._itemIndex.addItemHeading(tHandle, nLine, hDepth, hText)
|
||||
if cTitle != TT_NONE:
|
||||
if nTitle > 0:
|
||||
# We have a new title, so we need to count the words of the previous one
|
||||
lastText = "\n".join(theLines[nTitle-1:nLine-1])
|
||||
self._indexWordCounts(tHandle, lastText, nTitle)
|
||||
self._indexWordCounts(tHandle, lastText, pTitle)
|
||||
nTitle = nLine
|
||||
pTitle = cTitle
|
||||
|
||||
elif aLine.startswith("@"):
|
||||
self._indexKeyword(tHandle, aLine, nTitle, theItem.itemClass, itemTags)
|
||||
if cTitle != TT_NONE:
|
||||
self._indexKeyword(tHandle, aLine, cTitle, theItem.itemClass, itemTags)
|
||||
|
||||
elif aLine.startswith("%"):
|
||||
if nTitle > 0:
|
||||
if cTitle != TT_NONE:
|
||||
toCheck = aLine[1:].lstrip()
|
||||
synTag = toCheck[:9].lower()
|
||||
tLen = len(aLine)
|
||||
cLen = len(toCheck)
|
||||
cOff = tLen - cLen
|
||||
if synTag == "synopsis:":
|
||||
self._indexSynopsis(tHandle, aLine[cOff+9:].strip(), nTitle)
|
||||
sText = aLine[cOff+9:].strip()
|
||||
self._itemIndex.setHeadingSynopsis(tHandle, cTitle, sText)
|
||||
|
||||
# Count words for remaining text after last heading
|
||||
if nTitle > 0:
|
||||
if pTitle != TT_NONE:
|
||||
lastText = "\n".join(theLines[nTitle-1:])
|
||||
self._indexWordCounts(tHandle, lastText, nTitle)
|
||||
self._indexWordCounts(tHandle, lastText, pTitle)
|
||||
|
||||
# Also count words on a page with no titles
|
||||
if nTitle == 0:
|
||||
self._indexWordCounts(tHandle, theText, nTitle)
|
||||
if cTitle == TT_NONE:
|
||||
self._indexWordCounts(tHandle, theText, cTitle)
|
||||
|
||||
# Prune no longer used tags
|
||||
for tTag, isActive in itemTags.items():
|
||||
@@ -362,34 +372,14 @@ class NWIndex:
|
||||
return "H2", aLine[4:].strip()
|
||||
return "H0", ""
|
||||
|
||||
def _indexTitle(self, tHandle, aLine, nTitle):
|
||||
"""Save information about the title and its location in the
|
||||
file to the index.
|
||||
"""
|
||||
hDepth, hText = self._splitHeading(aLine)
|
||||
if hDepth == "H0":
|
||||
return False
|
||||
|
||||
sTitle = f"T{nTitle:06d}"
|
||||
self._itemIndex.addItemHeading(tHandle, sTitle, hDepth, hText)
|
||||
return True
|
||||
|
||||
def _indexWordCounts(self, tHandle, theText, nTitle):
|
||||
def _indexWordCounts(self, tHandle, theText, sTitle):
|
||||
"""Count text stats and save the counts to the index.
|
||||
"""
|
||||
sTitle = f"T{nTitle:06d}"
|
||||
cC, wC, pC = countWords(theText)
|
||||
self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC)
|
||||
self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC)
|
||||
return
|
||||
|
||||
def _indexSynopsis(self, tHandle, theText, nTitle):
|
||||
"""Save the synopsis to the index.
|
||||
"""
|
||||
sTitle = f"T{nTitle:06d}"
|
||||
self._itemIndex.setHeadingSynopsis(tHandle, sTitle, theText)
|
||||
return
|
||||
|
||||
def _indexKeyword(self, tHandle, aLine, nTitle, itemClass, itemTags):
|
||||
def _indexKeyword(self, tHandle, aLine, sTitle, itemClass, itemTags):
|
||||
"""Validate and save the information about a reference to a tag
|
||||
in another file, or the setting of a tag in the file. A record
|
||||
of active tags is updated so that no longer used tags can be
|
||||
@@ -404,7 +394,6 @@ class NWIndex:
|
||||
logger.warning("Skipping invalid keyword '%s' in '%s'", theBits[0], tHandle)
|
||||
return
|
||||
|
||||
sTitle = f"T{nTitle:06d}"
|
||||
if theBits[0] == nwKeyWords.TAG_KEY:
|
||||
tagName = theBits[1]
|
||||
self._tagsIndex.add(tagName, tHandle, sTitle, itemClass)
|
||||
@@ -491,6 +480,19 @@ class NWIndex:
|
||||
# Extract Data
|
||||
##
|
||||
|
||||
def getItemData(self, tHandle):
|
||||
"""Get the index data for a given item.
|
||||
"""
|
||||
return self._itemIndex[tHandle]
|
||||
|
||||
def getItemHeader(self, tHandle, sTitle):
|
||||
"""Get the header entry for a specific item and heading.
|
||||
"""
|
||||
tItem = self._itemIndex[tHandle]
|
||||
if isinstance(tItem, IndexItem):
|
||||
return tItem[sTitle]
|
||||
return None
|
||||
|
||||
def novelStructure(self, rootHandle=None, skipExcl=True):
|
||||
"""Iterate over all titles in the novel, in the correct order as
|
||||
they appear in the tree view and in the respective document
|
||||
@@ -518,21 +520,13 @@ class NWIndex:
|
||||
hCount[iLevel] += 1
|
||||
return hCount
|
||||
|
||||
def getHandleWordCounts(self, tHandle):
|
||||
"""Get all header word counts for a specific handle.
|
||||
def getHandleHeaderCount(self, tHandle):
|
||||
"""Get the number of headers in an item.
|
||||
"""
|
||||
return [
|
||||
(f"{tHandle}:{sTitle}", hItem.wordCount)
|
||||
for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle)
|
||||
]
|
||||
|
||||
def getHandleHeaders(self, tHandle):
|
||||
"""Get all headers for a specific handle.
|
||||
"""
|
||||
return [
|
||||
(sTitle, hItem.level, hItem.title)
|
||||
for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle)
|
||||
]
|
||||
tItem = self._itemIndex[tHandle]
|
||||
if isinstance(tItem, IndexItem):
|
||||
return len(tItem)
|
||||
return 0
|
||||
|
||||
def getTableOfContents(self, rootHandle, maxDepth, skipExcl=True):
|
||||
"""Generate a table of contents up to a maximum depth.
|
||||
@@ -598,13 +592,6 @@ class NWIndex:
|
||||
|
||||
return theRefs
|
||||
|
||||
def getNovelData(self, tHandle, sTitle):
|
||||
"""Return the novel data of a given handle and title.
|
||||
"""
|
||||
if tHandle in self._itemIndex:
|
||||
return self._itemIndex[tHandle][sTitle]
|
||||
return None
|
||||
|
||||
def getBackReferenceList(self, tHandle):
|
||||
"""Build a list of files referring back to our file, specified
|
||||
by tHandle.
|
||||
@@ -644,10 +631,22 @@ class TagsIndex:
|
||||
control of the keys.
|
||||
"""
|
||||
|
||||
__slots__ = ("_tags")
|
||||
|
||||
def __init__(self):
|
||||
self._tags = {}
|
||||
return
|
||||
|
||||
def __contains__(self, tagKey):
|
||||
return tagKey in self._tags
|
||||
|
||||
def __delitem__(self, tagKey):
|
||||
self._tags.pop(tagKey, None)
|
||||
return
|
||||
|
||||
def __getitem__(self, tagKey):
|
||||
return self._tags.get(tagKey, None)
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -658,22 +657,6 @@ class TagsIndex:
|
||||
self._tags = {}
|
||||
return
|
||||
|
||||
def __contains__(self, tagKey):
|
||||
"""Check if a tag exists in the index,
|
||||
"""
|
||||
return tagKey in self._tags
|
||||
|
||||
def __delitem__(self, tagKey):
|
||||
"""Delete an entry in the index.
|
||||
"""
|
||||
self._tags.pop(tagKey, None)
|
||||
return
|
||||
|
||||
def __getitem__(self, tagKey):
|
||||
"""Return a tag, or return None if it isn't found.
|
||||
"""
|
||||
return self._tags.get(tagKey, None)
|
||||
|
||||
def add(self, tagKey, tHandle, sTitle, itemClass):
|
||||
"""Add a key to the index and set all values.
|
||||
"""
|
||||
@@ -690,7 +673,7 @@ class TagsIndex:
|
||||
def tagHeading(self, tagKey):
|
||||
"""Get the heading of a given tag.
|
||||
"""
|
||||
return self._tags.get(tagKey, {}).get("heading", nwHeaders.TT_NONE)
|
||||
return self._tags.get(tagKey, {}).get("heading", TT_NONE)
|
||||
|
||||
def tagClass(self, tagKey):
|
||||
"""Get the class of a given tag.
|
||||
@@ -749,11 +732,23 @@ class ItemIndex:
|
||||
IndexHeading object for each header of the text.
|
||||
"""
|
||||
|
||||
__slots__ = ("_project", "_items")
|
||||
|
||||
def __init__(self, project):
|
||||
self._project = project
|
||||
self._items = {}
|
||||
return
|
||||
|
||||
def __contains__(self, tHandle):
|
||||
return tHandle in self._items
|
||||
|
||||
def __delitem__(self, tHandle):
|
||||
self._items.pop(tHandle, None)
|
||||
return
|
||||
|
||||
def __getitem__(self, tHandle):
|
||||
return self._items.get(tHandle, None)
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
@@ -764,22 +759,6 @@ class ItemIndex:
|
||||
self._items = {}
|
||||
return
|
||||
|
||||
def __contains__(self, tHandle):
|
||||
"""Check if an item exists in the index,
|
||||
"""
|
||||
return tHandle in self._items
|
||||
|
||||
def __delitem__(self, tHandle):
|
||||
"""Delete an entry in the index.
|
||||
"""
|
||||
self._items.pop(tHandle, None)
|
||||
return
|
||||
|
||||
def __getitem__(self, tHandle):
|
||||
"""Return an item, or return None if it isn't found.
|
||||
"""
|
||||
return self._items.get(tHandle, None)
|
||||
|
||||
def add(self, tHandle, tItem):
|
||||
"""Add a new item to the index. This will overwrite the item if
|
||||
it already exists.
|
||||
@@ -815,8 +794,6 @@ class ItemIndex:
|
||||
a given root handle, or for all if root handle is None.
|
||||
"""
|
||||
for tItem in self._project.tree:
|
||||
if tItem is None:
|
||||
continue
|
||||
if tItem.isNoteLayout():
|
||||
continue
|
||||
if skipExcl and not tItem.isActive:
|
||||
@@ -839,13 +816,15 @@ class ItemIndex:
|
||||
# Setters
|
||||
##
|
||||
|
||||
def addItemHeading(self, tHandle, sTitle, hDepth, hText):
|
||||
"""Set the main heading level of an item.
|
||||
def addItemHeading(self, tHandle, lineNo, hDepth, hText):
|
||||
"""Add a heading to an item.
|
||||
"""
|
||||
if tHandle in self._items:
|
||||
tItem = self._items[tHandle]
|
||||
tItem.addHeading(IndexHeading(sTitle, hDepth, hText))
|
||||
return
|
||||
sTitle = tItem.nextHeading()
|
||||
tItem.addHeading(IndexHeading(sTitle, lineNo, hDepth, hText))
|
||||
return sTitle
|
||||
return TT_NONE
|
||||
|
||||
def setHeadingCounts(self, tHandle, sTitle, cC, wC, pC):
|
||||
"""Set the character, word and paragraph counts of a heading
|
||||
@@ -916,20 +895,31 @@ class IndexItem:
|
||||
must be reset each time the item is re-indexed.
|
||||
"""
|
||||
|
||||
__slots__ = ("_handle", "_item", "_headings", "_headings", "_count")
|
||||
|
||||
def __init__(self, tHandle, tItem):
|
||||
self._handle = tHandle
|
||||
self._item = tItem
|
||||
self._headings = {}
|
||||
self._index = 0
|
||||
self._count = 0
|
||||
|
||||
# Add a placeholder heading
|
||||
self._headings[nwHeaders.TT_NONE] = IndexHeading(nwHeaders.TT_NONE)
|
||||
self._headings[TT_NONE] = IndexHeading(TT_NONE)
|
||||
|
||||
return
|
||||
|
||||
def __repr__(self):
|
||||
return f"<IndexItem handle='{self._handle}'>"
|
||||
|
||||
def __len__(self):
|
||||
return len(self._headings)
|
||||
|
||||
def __getitem__(self, sTitle):
|
||||
return self._headings.get(sTitle, None)
|
||||
|
||||
def __contains__(self, sTitle):
|
||||
return sTitle in self._headings
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
@@ -946,8 +936,8 @@ class IndexItem:
|
||||
"""Add a heading to the item. Also remove the placeholder entry
|
||||
if it exists.
|
||||
"""
|
||||
if nwHeaders.TT_NONE in self._headings:
|
||||
self._headings.pop(nwHeaders.TT_NONE)
|
||||
if TT_NONE in self._headings:
|
||||
self._headings.pop(TT_NONE)
|
||||
self._headings[tHeading.key] = tHeading
|
||||
return
|
||||
|
||||
@@ -984,12 +974,6 @@ class IndexItem:
|
||||
# Data Methods
|
||||
##
|
||||
|
||||
def __getitem__(self, sTitle):
|
||||
return self._headings.get(sTitle, None)
|
||||
|
||||
def __contains__(self, sTitle):
|
||||
return sTitle in self._headings
|
||||
|
||||
def items(self):
|
||||
return self._headings.items()
|
||||
|
||||
@@ -1006,6 +990,12 @@ class IndexItem:
|
||||
tags.append(tag)
|
||||
return tags
|
||||
|
||||
def nextHeading(self):
|
||||
"""Return the next heading key to be used.
|
||||
"""
|
||||
self._count += 1
|
||||
return f"T{self._count:04d}"
|
||||
|
||||
##
|
||||
# Pack/Unpack
|
||||
##
|
||||
@@ -1051,8 +1041,14 @@ class IndexHeading:
|
||||
of all references made under each heading.
|
||||
"""
|
||||
|
||||
def __init__(self, key, level="H0", title=""):
|
||||
__slots__ = (
|
||||
"_key", "_line", "_level", "_title", "_charCount", "_wordCount",
|
||||
"_paraCount", "_synopsis", "_tag", "_refs",
|
||||
)
|
||||
|
||||
def __init__(self, key, line=0, level="H0", title=""):
|
||||
self._key = key
|
||||
self._line = line
|
||||
self._level = level
|
||||
self._title = title
|
||||
|
||||
@@ -1077,6 +1073,10 @@ class IndexHeading:
|
||||
def key(self):
|
||||
return self._key
|
||||
|
||||
@property
|
||||
def line(self):
|
||||
return self._line
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
return self._level
|
||||
@@ -1120,6 +1120,12 @@ class IndexHeading:
|
||||
self._level = level
|
||||
return
|
||||
|
||||
def setLine(self, line):
|
||||
"""Set the line number of a heading.
|
||||
"""
|
||||
self._line = max(0, checkInt(line, 0))
|
||||
return
|
||||
|
||||
def setCounts(self, charCount, wordCount, paraCount):
|
||||
"""Set the character, word and paragraph count. Make sure the
|
||||
value is an integer and is not smaller than 0.
|
||||
@@ -1161,6 +1167,7 @@ class IndexHeading:
|
||||
return {
|
||||
"level": self._level,
|
||||
"title": self._title,
|
||||
"line": self._line,
|
||||
"tag": self._tag,
|
||||
"cCount": self._charCount,
|
||||
"wCount": self._wordCount,
|
||||
@@ -1182,6 +1189,7 @@ class IndexHeading:
|
||||
self.setLevel(data.get("level", "H0"))
|
||||
self._title = str(data.get("title", ""))
|
||||
self._tag = str(data.get("tag", ""))
|
||||
self.setLine(data.get("line", 0))
|
||||
self.setCounts(
|
||||
data.get("cCount", 0),
|
||||
data.get("wCount", 0),
|
||||
|
||||
+16
-33
@@ -50,7 +50,6 @@ class NWTree:
|
||||
self._treeRoots = {} # The root items of the tree
|
||||
self._trashRoot = None # The handle of the trash root folder
|
||||
self._archRoot = None # The handle of the archive root folder
|
||||
self._theIndex = 0 # The current iterator index
|
||||
self._treeChanged = False # True if tree structure has changed
|
||||
|
||||
return
|
||||
@@ -62,12 +61,11 @@ class NWTree:
|
||||
def clear(self):
|
||||
"""Clear the item tree entirely.
|
||||
"""
|
||||
self._projTree = {}
|
||||
self._treeOrder = []
|
||||
self._treeRoots = {}
|
||||
self._trashRoot = None
|
||||
self._archRoot = None
|
||||
self._theIndex = 0
|
||||
self._projTree = {}
|
||||
self._treeOrder = []
|
||||
self._treeRoots = {}
|
||||
self._trashRoot = None
|
||||
self._archRoot = None
|
||||
self._treeChanged = False
|
||||
return
|
||||
|
||||
@@ -278,7 +276,7 @@ class NWTree:
|
||||
"""
|
||||
for tHandle in self._treeOrder:
|
||||
nwItem = self.__getitem__(tHandle)
|
||||
if nwItem is not None and nwItem.isRootType():
|
||||
if isinstance(nwItem, NWItem) and nwItem.isRootType():
|
||||
if itemClass is None or nwItem.itemClass == itemClass:
|
||||
yield tHandle, nwItem
|
||||
return
|
||||
@@ -365,22 +363,18 @@ class NWTree:
|
||||
return True
|
||||
|
||||
##
|
||||
# Meta Methods
|
||||
# Special Methods
|
||||
##
|
||||
|
||||
def __len__(self):
|
||||
"""Return the length counter. Does not check that it is correct!
|
||||
"""The number of items in the project.
|
||||
"""
|
||||
return len(self._treeOrder)
|
||||
|
||||
def __bool__(self):
|
||||
"""Returns True if the tree has any entries.
|
||||
"""True if there are any items in the project.
|
||||
"""
|
||||
return len(self._treeOrder) > 0
|
||||
|
||||
##
|
||||
# Item Access Methods
|
||||
##
|
||||
return bool(self._treeOrder)
|
||||
|
||||
def __getitem__(self, tHandle):
|
||||
"""Return a project item based on its handle. Returns None if
|
||||
@@ -417,25 +411,14 @@ class NWTree:
|
||||
"""
|
||||
return tHandle in self._treeOrder
|
||||
|
||||
##
|
||||
# Iterator Methods
|
||||
##
|
||||
|
||||
def __iter__(self):
|
||||
"""Initiates the iterator.
|
||||
"""Iterate through project items.
|
||||
"""
|
||||
self._theIndex = 0
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
"""Returns the item from the next entry in the _treeOrder list.
|
||||
"""
|
||||
if self._theIndex < len(self._treeOrder):
|
||||
theItem = self.__getitem__(self._treeOrder[self._theIndex])
|
||||
self._theIndex += 1
|
||||
return theItem
|
||||
else:
|
||||
raise StopIteration
|
||||
for tHandle in self._treeOrder:
|
||||
tItem = self._projTree.get(tHandle)
|
||||
if isinstance(tItem, NWItem):
|
||||
yield tItem
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
|
||||
@@ -130,15 +130,6 @@ class nwAlert(Enum):
|
||||
# END Enum nwAlert
|
||||
|
||||
|
||||
class nwState(Enum):
|
||||
|
||||
NONE = 0
|
||||
BAD = 1
|
||||
GOOD = 2
|
||||
|
||||
# END Enum nwState
|
||||
|
||||
|
||||
class nwView(Enum):
|
||||
|
||||
EDITOR = 0
|
||||
|
||||
@@ -28,7 +28,7 @@ from novelwriter.gui.outline import GuiOutlineView
|
||||
from novelwriter.gui.projtree import GuiProjectView
|
||||
from novelwriter.gui.statusbar import GuiMainStatus
|
||||
from novelwriter.gui.theme import GuiTheme
|
||||
from novelwriter.gui.viewsbar import GuiViewsBar
|
||||
from novelwriter.gui.sidebar import GuiSideBar
|
||||
|
||||
__all__ = [
|
||||
"GuiDocEditor",
|
||||
@@ -41,5 +41,5 @@ __all__ = [
|
||||
"GuiOutlineView",
|
||||
"GuiProjectView",
|
||||
"GuiTheme",
|
||||
"GuiViewsBar",
|
||||
"GuiSideBar",
|
||||
]
|
||||
|
||||
@@ -51,8 +51,8 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from novelwriter.core import NWSpellEnchant, countWords
|
||||
from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode
|
||||
from novelwriter.common import transferCase
|
||||
from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode, nwItemClass
|
||||
from novelwriter.common import minmax, transferCase
|
||||
from novelwriter.constants import nwConst, nwFiles, nwKeyWords, nwUnicode
|
||||
from novelwriter.gui.dochighlight import GuiDocHighlighter
|
||||
|
||||
@@ -71,6 +71,8 @@ class GuiDocEditor(QTextEdit):
|
||||
docEditedStatusChanged = pyqtSignal(bool)
|
||||
docCountsChanged = pyqtSignal(str, int, int, int)
|
||||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||||
novelStructureChanged = pyqtSignal()
|
||||
novelItemMetaChanged = pyqtSignal(str)
|
||||
|
||||
def __init__(self, mainGui):
|
||||
super().__init__(parent=mainGui)
|
||||
@@ -88,7 +90,6 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
self._docChanged = False # Flag for changed status of document
|
||||
self._docHandle = None # The handle of the open file
|
||||
self._docHeaders = [] # Record of headers in the file
|
||||
|
||||
self._spellCheck = False # Flag for spell checking enabled
|
||||
self._nonWord = "\"'" # Characters to not include in spell checking
|
||||
@@ -415,7 +416,7 @@ class GuiDocEditor(QTextEdit):
|
||||
self._queuePos = self._nwItem.cursorPos
|
||||
else:
|
||||
self.setCursorPosition(self._nwItem.cursorPos)
|
||||
else:
|
||||
elif isinstance(tLine, int):
|
||||
self.setCursorLine(tLine)
|
||||
|
||||
if self.mainConf.scrollPastEnd > 0:
|
||||
@@ -425,7 +426,6 @@ class GuiDocEditor(QTextEdit):
|
||||
self.document().rootFrame().setFrameFormat(docFrame)
|
||||
|
||||
self.docFooter.updateLineCount()
|
||||
self._docHeaders = self.theProject.index.getHandleHeaders(self._docHandle)
|
||||
|
||||
qApp.processEvents()
|
||||
self.document().clearUndoRedoStacks()
|
||||
@@ -531,14 +531,16 @@ class GuiDocEditor(QTextEdit):
|
||||
self.setDocumentChanged(False)
|
||||
|
||||
oldHeader = self._nwItem.mainHeading
|
||||
oldCount = self.theProject.index.getHandleHeaderCount(tHandle)
|
||||
self.theProject.index.scanText(tHandle, docText)
|
||||
newHeader = self._nwItem.mainHeading
|
||||
newCount = self.theProject.index.getHandleHeaderCount(tHandle)
|
||||
|
||||
# ToDo: This should be a signal
|
||||
if self._updateHeaders():
|
||||
self.mainGui.requestNovelTreeRefresh()
|
||||
else:
|
||||
self.mainGui.novelView.updateWordCounts(tHandle)
|
||||
if self._nwItem.itemClass == nwItemClass.NOVEL:
|
||||
if oldCount == newCount:
|
||||
self.novelItemMetaChanged.emit(tHandle)
|
||||
else:
|
||||
self.novelStructureChanged.emit()
|
||||
|
||||
# ToDo: This should be a signal
|
||||
if oldHeader != newHeader:
|
||||
@@ -652,17 +654,30 @@ class GuiDocEditor(QTextEdit):
|
||||
self.docEditedStatusChanged.emit(self._docChanged)
|
||||
return self._docChanged
|
||||
|
||||
def setCursorPosition(self, thePosition):
|
||||
def setCursorPosition(self, position):
|
||||
"""Move the cursor to a given position in the document.
|
||||
"""
|
||||
if not isinstance(thePosition, int):
|
||||
if not isinstance(position, int):
|
||||
return False
|
||||
|
||||
nChars = self.document().characterCount()
|
||||
if nChars > 1:
|
||||
theCursor = self.textCursor()
|
||||
theCursor.setPosition(min(max(thePosition, 0), nChars-1))
|
||||
theCursor.setPosition(minmax(position, 0, nChars-1))
|
||||
self.setTextCursor(theCursor)
|
||||
|
||||
# By default, the editor scrolls so the cursor is on the
|
||||
# last line, so we must correct it. The user setting for
|
||||
# auto-scroll is used to determine the scroll distance. This
|
||||
# makes it compatible with the typewriter scrolling feature
|
||||
# when it is enabled. By default, it's 30% of viewport.
|
||||
vPos = self.verticalScrollBar().value()
|
||||
cPos = self.cursorRect().topLeft().y()
|
||||
mPos = int(self.mainConf.autoScrollPos*0.01 * self.viewport().height())
|
||||
if cPos > mPos:
|
||||
# Only scroll if the cursor is past the auto-scroll limit
|
||||
self.verticalScrollBar().setValue(max(0, vPos + cPos - mPos))
|
||||
|
||||
self.docFooter.updateLineCount()
|
||||
|
||||
return True
|
||||
@@ -675,18 +690,18 @@ class GuiDocEditor(QTextEdit):
|
||||
self._nwItem.setCursorPos(cursPos)
|
||||
return
|
||||
|
||||
def setCursorLine(self, theLine):
|
||||
def setCursorLine(self, lineNo):
|
||||
"""Move the cursor to a given line in the document.
|
||||
"""
|
||||
if not isinstance(theLine, int):
|
||||
if not isinstance(lineNo, int):
|
||||
return False
|
||||
|
||||
if theLine >= 0:
|
||||
theBlock = self.document().findBlockByLineNumber(theLine)
|
||||
lineIdx = lineNo - 1 # Block index is 0 offset, lineNo is 1 offset
|
||||
if lineIdx >= 0:
|
||||
theBlock = self.document().findBlockByLineNumber(lineIdx)
|
||||
if theBlock:
|
||||
self.setCursorPosition(theBlock.position())
|
||||
self.docFooter.updateLineCount()
|
||||
logger.debug("Cursor moved to line %d", theLine)
|
||||
logger.debug("Cursor moved to line %d", lineNo)
|
||||
|
||||
return True
|
||||
|
||||
@@ -2065,21 +2080,6 @@ class GuiDocEditor(QTextEdit):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _updateHeaders(self):
|
||||
"""Update the headers record and return True if anything
|
||||
changed, if a check flag was provided.
|
||||
"""
|
||||
if self._docHandle is None:
|
||||
return False
|
||||
|
||||
newHeaders = self.theProject.index.getHandleHeaders(self._docHandle)
|
||||
newLev = [x[1] for x in newHeaders]
|
||||
oldLev = [x[1] for x in self._docHeaders]
|
||||
|
||||
self._docHeaders = newHeaders
|
||||
|
||||
return newLev != oldLev
|
||||
|
||||
def _checkDocSize(self, theSize):
|
||||
"""Check if document size crosses the big document limit set in
|
||||
config. If so, we will set the big document flag to True.
|
||||
|
||||
@@ -323,43 +323,10 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
|
||||
def docHandle(self):
|
||||
"""Return the handle of the currently open document. Returns
|
||||
None if no document is open.
|
||||
"""
|
||||
return self._docHandle
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
|
||||
def setCursorPosition(self, thePosition):
|
||||
"""Move the cursor to a given position in the document.
|
||||
"""
|
||||
if not isinstance(thePosition, int):
|
||||
return False
|
||||
if thePosition >= 0:
|
||||
theCursor = self.textCursor()
|
||||
theCursor.setPosition(thePosition)
|
||||
self.setTextCursor(theCursor)
|
||||
return True
|
||||
|
||||
def setCursorLine(self, theLine):
|
||||
"""Move the cursor to a given line in the document.
|
||||
"""
|
||||
if not isinstance(theLine, int):
|
||||
return False
|
||||
if theLine >= 0:
|
||||
theBlock = self.document().findBlockByLineNumber(theLine)
|
||||
if theBlock:
|
||||
self.setCursorPosition(theBlock.position())
|
||||
logger.debug("Cursor moved to line %d", theLine)
|
||||
return True
|
||||
|
||||
def setScrollPosition(self, thePos):
|
||||
"""Set the scrollbar position.
|
||||
"""
|
||||
@@ -372,6 +339,12 @@ class GuiDocViewer(QTextBrowser):
|
||||
# Getters
|
||||
##
|
||||
|
||||
def docHandle(self):
|
||||
"""Return the handle of the currently open document. Returns
|
||||
None if no document is open.
|
||||
"""
|
||||
return self._docHandle
|
||||
|
||||
def getScrollPosition(self):
|
||||
"""Get the scrollbar position. Returns 0 if no scrollbar.
|
||||
"""
|
||||
|
||||
@@ -40,7 +40,6 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from novelwriter.enum import nwDocMode, nwItemClass, nwOutline
|
||||
from novelwriter.common import checkInt
|
||||
from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -60,7 +59,7 @@ class GuiNovelView(QWidget):
|
||||
|
||||
# Signals for user interaction with the novel tree
|
||||
selectedItemChanged = pyqtSignal(str)
|
||||
openDocumentRequest = pyqtSignal(str, Enum, int, str)
|
||||
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
|
||||
|
||||
def __init__(self, mainGui):
|
||||
super().__init__(parent=mainGui)
|
||||
@@ -83,7 +82,6 @@ class GuiNovelView(QWidget):
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
# Function Mappings
|
||||
self.updateWordCounts = self.novelTree.updateWordCounts
|
||||
self.getSelectedHandle = self.novelTree.getSelectedHandle
|
||||
self.setActiveHandle = self.novelTree.setActiveHandle
|
||||
|
||||
@@ -107,12 +105,6 @@ class GuiNovelView(QWidget):
|
||||
self.novelTree.initSettings()
|
||||
return
|
||||
|
||||
def refreshTree(self):
|
||||
"""Refresh the current tree.
|
||||
"""
|
||||
self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("novelTree"))
|
||||
return
|
||||
|
||||
def clearProject(self):
|
||||
"""Clear project-related GUI content.
|
||||
"""
|
||||
@@ -164,6 +156,13 @@ class GuiNovelView(QWidget):
|
||||
# Public Slots
|
||||
##
|
||||
|
||||
@pyqtSlot()
|
||||
def refreshTree(self):
|
||||
"""Refresh the current tree.
|
||||
"""
|
||||
self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("novelTree"))
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def updateRootItem(self, tHandle):
|
||||
"""If any root item changes, rebuild the novel root menu.
|
||||
@@ -171,6 +170,14 @@ class GuiNovelView(QWidget):
|
||||
self.novelBar.buildNovelRootMenu()
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def updateNovelItemMeta(self, tHandle):
|
||||
"""The meta data of a novel item has changed, and the tree item
|
||||
needs to be refreshed.
|
||||
"""
|
||||
self.novelTree.refreshHandle(tHandle)
|
||||
return
|
||||
|
||||
# END Class GuiNovelView
|
||||
|
||||
|
||||
@@ -470,7 +477,7 @@ class GuiNovelTree(QTreeWidget):
|
||||
return
|
||||
|
||||
def refreshTree(self, rootHandle=None, overRide=False):
|
||||
"""Called whenever the Novel tab is activated.
|
||||
"""Refresh the tree if it has been changed.
|
||||
"""
|
||||
logger.debug("Requesting refresh of the novel tree")
|
||||
if rootHandle is None:
|
||||
@@ -495,13 +502,24 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def updateWordCounts(self, tHandle):
|
||||
"""Update the word count for a given handle.
|
||||
def refreshHandle(self, tHandle):
|
||||
"""Refresh the data for a given handle.
|
||||
"""
|
||||
tHeaders = self.theProject.index.getHandleWordCounts(tHandle)
|
||||
for titleKey, wCount in tHeaders:
|
||||
if titleKey in self._treeMap:
|
||||
self._treeMap[titleKey].setText(self.C_WORDS, f"{wCount:n}")
|
||||
idxData = self.theProject.index.getItemData(tHandle)
|
||||
if idxData is None:
|
||||
return
|
||||
|
||||
logger.debug("Refreshing meta data for item '%s'", tHandle)
|
||||
for sTitle, tHeading in idxData.items():
|
||||
sKey = f"{tHandle}:{sTitle}"
|
||||
trItem = self._treeMap.get(sKey, None)
|
||||
if trItem is None:
|
||||
logger.debug("Heading '%s' not in novel tree", sKey)
|
||||
self.refreshTree()
|
||||
return
|
||||
|
||||
self._updateTreeItemValues(trItem, tHeading, tHandle, sTitle)
|
||||
|
||||
return
|
||||
|
||||
def getSelectedHandle(self):
|
||||
@@ -509,14 +527,11 @@ class GuiNovelTree(QTreeWidget):
|
||||
selected, return the first.
|
||||
"""
|
||||
selItem = self.selectedItems()
|
||||
tHandle = None
|
||||
tLine = 0
|
||||
if selItem:
|
||||
tHandle = selItem[0].data(self.C_TITLE, self.D_HANDLE)
|
||||
sTitle = selItem[0].data(self.C_TITLE, self.D_TITLE)
|
||||
tLine = checkInt(sTitle[1:], 1) - 1
|
||||
|
||||
return tHandle, tLine
|
||||
return tHandle, sTitle
|
||||
return None, None
|
||||
|
||||
def setLastColType(self, colType, doRefresh=True):
|
||||
"""Change the content type of the last column and rebuild.
|
||||
@@ -575,11 +590,11 @@ class GuiNovelTree(QTreeWidget):
|
||||
if not isinstance(selItem, QTreeWidgetItem):
|
||||
return
|
||||
|
||||
tHandle, _ = self.getSelectedHandle()
|
||||
tHandle, sTitle = self.getSelectedHandle()
|
||||
if tHandle is None:
|
||||
return
|
||||
|
||||
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "")
|
||||
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle or "", False)
|
||||
|
||||
return
|
||||
|
||||
@@ -590,6 +605,22 @@ class GuiNovelTree(QTreeWidget):
|
||||
self.clearSelection()
|
||||
return
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""Elide labels in the extra column.
|
||||
"""
|
||||
super().resizeEvent(event)
|
||||
newW = event.size().width()
|
||||
oldW = event.oldSize().width()
|
||||
if newW != oldW:
|
||||
eliW = int(0.25 * newW)
|
||||
fMetric = self.fontMetrics()
|
||||
for i in range(self.topLevelItemCount()):
|
||||
trItem = self.topLevelItem(i)
|
||||
if isinstance(trItem, QTreeWidgetItem):
|
||||
lastText = trItem.data(self.C_EXTRA, Qt.UserRole)
|
||||
trItem.setText(self.C_EXTRA, fMetric.elidedText(lastText, Qt.ElideRight, eliW))
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
##
|
||||
@@ -621,8 +652,8 @@ class GuiNovelTree(QTreeWidget):
|
||||
clicked, and send it to the main gui class for opening in the
|
||||
document editor.
|
||||
"""
|
||||
tHandle, tLine = self.getSelectedHandle()
|
||||
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, tLine, "")
|
||||
tHandle, sTitle = self.getSelectedHandle()
|
||||
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True)
|
||||
return
|
||||
|
||||
##
|
||||
@@ -638,30 +669,16 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
|
||||
for tKey, tHandle, sTitle, novIdx in novStruct:
|
||||
|
||||
iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0)
|
||||
if iLevel == 0:
|
||||
if novIdx.level == "H0":
|
||||
continue
|
||||
|
||||
hDec = self.mainTheme.getHeaderDecoration(iLevel)
|
||||
|
||||
newItem = QTreeWidgetItem()
|
||||
newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
|
||||
newItem.setText(self.C_TITLE, novIdx.title)
|
||||
newItem.setData(self.C_TITLE, self.D_HANDLE, tHandle)
|
||||
newItem.setData(self.C_TITLE, self.D_TITLE, sTitle)
|
||||
newItem.setData(self.C_TITLE, self.D_KEY, tKey)
|
||||
newItem.setFont(self.C_TITLE, self._hFonts[iLevel])
|
||||
newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}")
|
||||
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
|
||||
newItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore)
|
||||
|
||||
# Custom column
|
||||
lastText, toolTip = self._getLastColumnText(tHandle, sTitle)
|
||||
newItem.setText(self.C_EXTRA, lastText)
|
||||
if lastText:
|
||||
newItem.setToolTip(self.C_EXTRA, toolTip)
|
||||
|
||||
self._updateTreeItemValues(newItem, novIdx, tHandle, sTitle)
|
||||
self._treeMap[tKey] = newItem
|
||||
self.addTopLevelItem(newItem)
|
||||
|
||||
@@ -672,24 +689,52 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def _updateTreeItemValues(self, trItem, idxItem, tHandle, sTitle):
|
||||
"""Set the tree item values from the index entry.
|
||||
"""
|
||||
iLevel = nwHeaders.H_LEVEL.get(idxItem.level, 0)
|
||||
hDec = self.mainTheme.getHeaderDecoration(iLevel)
|
||||
|
||||
trItem.setData(self.C_TITLE, Qt.DecorationRole, 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, Qt.DecorationRole, self._pMore)
|
||||
|
||||
# Custom column
|
||||
mW = int(0.25 * self.viewport().width())
|
||||
lastText, toolTip = self._getLastColumnText(tHandle, sTitle)
|
||||
elideText = self.fontMetrics().elidedText(lastText, Qt.ElideRight, mW)
|
||||
trItem.setText(self.C_EXTRA, elideText)
|
||||
trItem.setData(self.C_EXTRA, Qt.UserRole, lastText)
|
||||
trItem.setToolTip(self.C_EXTRA, toolTip)
|
||||
|
||||
return
|
||||
|
||||
def _getLastColumnText(self, tHandle, sTitle):
|
||||
"""Generate the text for the last column based on user settings.
|
||||
"""
|
||||
if self._lastCol == NovelTreeColumn.HIDDEN:
|
||||
return "", ""
|
||||
|
||||
refData = []
|
||||
refName = ""
|
||||
theRefs = self.theProject.index.getReferences(tHandle, sTitle)
|
||||
if self._lastCol == NovelTreeColumn.POV:
|
||||
newText = ", ".join(theRefs[nwKeyWords.POV_KEY])
|
||||
return newText, f"{self._povLabel}: {newText}"
|
||||
refData = theRefs[nwKeyWords.POV_KEY]
|
||||
refName = self._povLabel
|
||||
|
||||
elif self._lastCol == NovelTreeColumn.FOCUS:
|
||||
newText = ", ".join(theRefs[nwKeyWords.FOCUS_KEY])
|
||||
return newText, f"{self._focLabel}: {newText}"
|
||||
refData = theRefs[nwKeyWords.FOCUS_KEY]
|
||||
refName = self._focLabel
|
||||
|
||||
elif self._lastCol == NovelTreeColumn.PLOT:
|
||||
newText = ", ".join(theRefs[nwKeyWords.PLOT_KEY])
|
||||
return newText, f"{self._pltLabel}: {newText}"
|
||||
refData = theRefs[nwKeyWords.PLOT_KEY]
|
||||
refName = self._pltLabel
|
||||
|
||||
if refData:
|
||||
toolText = ", ".join(refData)
|
||||
return refData[0], f"{refName}: {toolText}"
|
||||
|
||||
return "", ""
|
||||
|
||||
@@ -699,7 +744,7 @@ class GuiNovelTree(QTreeWidget):
|
||||
logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
|
||||
|
||||
pIndex = self.theProject.index
|
||||
novIdx = pIndex.getNovelData(tHandle, sTitle)
|
||||
novIdx = pIndex.getItemHeader(tHandle, sTitle)
|
||||
refTags = pIndex.getReferences(tHandle, sTitle)
|
||||
|
||||
synopText = novIdx.synopsis
|
||||
|
||||
+19
-17
@@ -55,6 +55,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiOutlineView(QWidget):
|
||||
|
||||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||||
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
|
||||
|
||||
def __init__(self, mainGui):
|
||||
super().__init__(parent=mainGui)
|
||||
@@ -72,6 +73,7 @@ class GuiOutlineView(QWidget):
|
||||
self.splitOutline = QSplitter(Qt.Vertical)
|
||||
self.splitOutline.addWidget(self.outlineTree)
|
||||
self.splitOutline.addWidget(self.outlineData)
|
||||
self.splitOutline.setOpaqueResize(False)
|
||||
self.splitOutline.setSizes(self.mainConf.outlinePanePos)
|
||||
|
||||
# Assemble
|
||||
@@ -375,15 +377,16 @@ class GuiOutlineTree(QTreeWidget):
|
||||
hiddenStateChanged = pyqtSignal()
|
||||
activeItemChanged = pyqtSignal(str, str)
|
||||
|
||||
def __init__(self, theOutline):
|
||||
super().__init__(parent=theOutline)
|
||||
def __init__(self, outlineView):
|
||||
super().__init__(parent=outlineView)
|
||||
|
||||
logger.debug("Initialising GuiOutlineTree ...")
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = theOutline.mainGui
|
||||
self.theProject = theOutline.mainGui.theProject
|
||||
self.mainTheme = theOutline.mainGui.mainTheme
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.outlineView = outlineView
|
||||
self.mainGui = outlineView.mainGui
|
||||
self.theProject = outlineView.mainGui.theProject
|
||||
self.mainTheme = outlineView.mainGui.mainTheme
|
||||
|
||||
self.setUniformRowHeights(True)
|
||||
self.setFrameStyle(QFrame.NoFrame)
|
||||
@@ -524,13 +527,11 @@ class GuiOutlineTree(QTreeWidget):
|
||||
selected, return the first.
|
||||
"""
|
||||
selItem = self.selectedItems()
|
||||
tHandle = None
|
||||
tLine = 0
|
||||
if selItem:
|
||||
tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
|
||||
tLine = checkInt(selItem[0].text(self._colIdx[nwOutline.LINE]), 1) - 1
|
||||
|
||||
return tHandle, tLine
|
||||
sTitle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
|
||||
return tHandle, sTitle
|
||||
return None, None
|
||||
|
||||
##
|
||||
# Slots
|
||||
@@ -542,8 +543,10 @@ class GuiOutlineTree(QTreeWidget):
|
||||
clicked, and send it to the main gui class for opening in the
|
||||
document editor.
|
||||
"""
|
||||
tHandle, tLine = self.getSelectedHandle()
|
||||
self.mainGui.openDocument(tHandle, tLine=tLine - 1, doScroll=True)
|
||||
tHandle, sTitle = self.getSelectedHandle()
|
||||
if tHandle is None:
|
||||
return
|
||||
self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -554,9 +557,8 @@ class GuiOutlineTree(QTreeWidget):
|
||||
selItems = self.selectedItems()
|
||||
if selItems:
|
||||
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
|
||||
sTitle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
|
||||
sTitle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
|
||||
self.activeItemChanged.emit(tHandle, sTitle)
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot(int, int, int)
|
||||
@@ -718,7 +720,7 @@ class GuiOutlineTree(QTreeWidget):
|
||||
trItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level)
|
||||
trItem.setIcon(self._colIdx[nwOutline.LABEL], self._dIcon[nwItem.mainHeading])
|
||||
trItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName)
|
||||
trItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0"))
|
||||
trItem.setText(self._colIdx[nwOutline.LINE], f"{novIdx.line:n}")
|
||||
trItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis)
|
||||
trItem.setText(self._colIdx[nwOutline.CCOUNT], f"{novIdx.charCount:n}")
|
||||
trItem.setText(self._colIdx[nwOutline.WCOUNT], f"{novIdx.wordCount:n}")
|
||||
@@ -1048,7 +1050,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
"""
|
||||
pIndex = self.theProject.index
|
||||
nwItem = self.theProject.tree[tHandle]
|
||||
novIdx = pIndex.getNovelData(tHandle, sTitle)
|
||||
novIdx = pIndex.getItemHeader(tHandle, sTitle)
|
||||
theRefs = pIndex.getReferences(tHandle, sTitle)
|
||||
if nwItem is None or novIdx is None:
|
||||
return False
|
||||
|
||||
@@ -60,7 +60,7 @@ class GuiProjectView(QWidget):
|
||||
|
||||
# Signals for user interaction with the project tree
|
||||
selectedItemChanged = pyqtSignal(str)
|
||||
openDocumentRequest = pyqtSignal(str, Enum, int, str)
|
||||
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
|
||||
|
||||
# Requests for the main GUI
|
||||
projectSettingsRequest = pyqtSignal(int)
|
||||
@@ -1144,7 +1144,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
return
|
||||
|
||||
if tItem.isFileType():
|
||||
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "")
|
||||
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
|
||||
else:
|
||||
trItem.setExpanded(not trItem.isExpanded())
|
||||
|
||||
@@ -1190,11 +1190,11 @@ class GuiProjectTree(QTreeWidget):
|
||||
if isFile:
|
||||
aOpenDoc = ctxMenu.addAction(self.tr("Open Document"))
|
||||
aOpenDoc.triggered.connect(
|
||||
lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "")
|
||||
lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
|
||||
)
|
||||
aViewDoc = ctxMenu.addAction(self.tr("View Document"))
|
||||
aViewDoc.triggered.connect(
|
||||
lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "")
|
||||
lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False)
|
||||
)
|
||||
ctxMenu.addSeparator()
|
||||
|
||||
@@ -1324,7 +1324,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
return
|
||||
|
||||
if tItem.isFileType():
|
||||
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "")
|
||||
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False)
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
novelWriter – GUI Main Window Views ToolBar
|
||||
novelWriter – GUI Main Window SideBar
|
||||
===========================================
|
||||
GUI class for the main window "Views" toolbar
|
||||
GUI class for the main window side bar
|
||||
|
||||
File History:
|
||||
Created: 2022-05-10 [1.7b1]
|
||||
@@ -36,14 +36,14 @@ from novelwriter.enum import nwView
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiViewsBar(QToolBar):
|
||||
class GuiSideBar(QToolBar):
|
||||
|
||||
viewChangeRequested = pyqtSignal(nwView)
|
||||
|
||||
def __init__(self, mainGui):
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiViewsBar ...")
|
||||
logger.debug("Initialising GuiSideBar ...")
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
@@ -123,7 +123,7 @@ class GuiViewsBar(QToolBar):
|
||||
|
||||
self.updateTheme()
|
||||
|
||||
logger.debug("GuiViewsBar initialisation complete")
|
||||
logger.debug("GuiSideBar initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
@@ -142,4 +142,4 @@ class GuiViewsBar(QToolBar):
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiViewsBar
|
||||
# END Class GuiSideBar
|
||||
@@ -34,7 +34,6 @@ from PyQt5.QtGui import QColor, QPainter
|
||||
from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton
|
||||
|
||||
from novelwriter.common import formatTime
|
||||
from novelwriter.enum import nwState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -53,8 +52,8 @@ class GuiMainStatus(QStatusBar):
|
||||
self.userIdle = False
|
||||
|
||||
colNone = QColor(*self.mainTheme.statNone)
|
||||
colTrue = QColor(*self.mainTheme.statUnsaved)
|
||||
colFalse = QColor(*self.mainTheme.statSaved)
|
||||
colSaved = QColor(*self.mainTheme.statSaved)
|
||||
colUnsaved = QColor(*self.mainTheme.statUnsaved)
|
||||
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
|
||||
@@ -72,7 +71,7 @@ class GuiMainStatus(QStatusBar):
|
||||
self.addPermanentWidget(self.langText)
|
||||
|
||||
# The Editor Status
|
||||
self.docIcon = StatusLED(colNone, colTrue, colFalse, iPx, iPx, self)
|
||||
self.docIcon = StatusLED(colNone, colSaved, colUnsaved, iPx, iPx, self)
|
||||
self.docText = QLabel(self.tr("Editor"))
|
||||
self.docIcon.setContentsMargins(0, 0, 0, 0)
|
||||
self.docText.setContentsMargins(0, 0, xM, 0)
|
||||
@@ -80,7 +79,7 @@ class GuiMainStatus(QStatusBar):
|
||||
self.addPermanentWidget(self.docText)
|
||||
|
||||
# The Project Status
|
||||
self.projIcon = StatusLED(colNone, colTrue, colFalse, iPx, iPx, self)
|
||||
self.projIcon = StatusLED(colNone, colSaved, colUnsaved, iPx, iPx, self)
|
||||
self.projText = QLabel(self.tr("Project"))
|
||||
self.projIcon.setContentsMargins(0, 0, 0, 0)
|
||||
self.projText.setContentsMargins(0, 0, xM, 0)
|
||||
@@ -122,8 +121,8 @@ class GuiMainStatus(QStatusBar):
|
||||
self.setRefTime(None)
|
||||
self.setLanguage(None, "")
|
||||
self.setProjectStats(0, 0)
|
||||
self.setProjectStatus(nwState.NONE)
|
||||
self.setDocumentStatus(nwState.NONE)
|
||||
self.setProjectStatus(StatusLED.S_NONE)
|
||||
self.setDocumentStatus(StatusLED.S_NONE)
|
||||
self.updateTime()
|
||||
return True
|
||||
|
||||
@@ -236,14 +235,14 @@ class GuiMainStatus(QStatusBar):
|
||||
def doUpdateProjectStatus(self, isChanged):
|
||||
"""Slot for updating the project status.
|
||||
"""
|
||||
self.setProjectStatus(nwState.GOOD if isChanged else nwState.BAD)
|
||||
self.setProjectStatus(StatusLED.S_BAD if isChanged else StatusLED.S_GOOD)
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def doUpdateDocumentStatus(self, isChanged):
|
||||
"""Slot for updating the document status.
|
||||
"""
|
||||
self.setDocumentStatus(nwState.GOOD if isChanged else nwState.BAD)
|
||||
self.setDocumentStatus(StatusLED.S_BAD if isChanged else StatusLED.S_GOOD)
|
||||
return
|
||||
|
||||
# END Class GuiMainStatus
|
||||
@@ -251,6 +250,10 @@ class GuiMainStatus(QStatusBar):
|
||||
|
||||
class StatusLED(QAbstractButton):
|
||||
|
||||
S_NONE = 0
|
||||
S_BAD = 1
|
||||
S_GOOD = 2
|
||||
|
||||
def __init__(self, colNone, colGood, colBad, sW, sH, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
@@ -271,9 +274,9 @@ class StatusLED(QAbstractButton):
|
||||
def setState(self, theState):
|
||||
"""Set the colour state.
|
||||
"""
|
||||
if theState == nwState.GOOD:
|
||||
if theState == self.S_GOOD:
|
||||
self._theCol = self._colGood
|
||||
elif theState == nwState.BAD:
|
||||
elif theState == self.S_BAD:
|
||||
self._theCol = self._colBad
|
||||
else:
|
||||
self._theCol = self._colNone
|
||||
|
||||
+48
-34
@@ -41,7 +41,7 @@ from PyQt5.QtWidgets import (
|
||||
from novelwriter.gui import (
|
||||
GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu,
|
||||
GuiMainStatus, GuiNovelView, GuiOutlineView, GuiProjectView, GuiTheme,
|
||||
GuiViewsBar
|
||||
GuiSideBar
|
||||
)
|
||||
from novelwriter.dialogs import (
|
||||
GuiAbout, GuiPreferences, GuiProjectDetails, GuiProjectLoad,
|
||||
@@ -118,7 +118,7 @@ class GuiMain(QMainWindow):
|
||||
self.itemDetails = GuiItemDetails(self)
|
||||
self.outlineView = GuiOutlineView(self)
|
||||
self.mainMenu = GuiMainMenu(self)
|
||||
self.viewsBar = GuiViewsBar(self)
|
||||
self.viewsBar = GuiSideBar(self)
|
||||
|
||||
# Project Tree Stack
|
||||
self.projStack = QStackedWidget()
|
||||
@@ -140,12 +140,14 @@ class GuiMain(QMainWindow):
|
||||
self.splitView.addWidget(self.docViewer)
|
||||
self.splitView.addWidget(self.viewMeta)
|
||||
self.splitView.setHandleWidth(hWd)
|
||||
self.splitView.setOpaqueResize(False)
|
||||
self.splitView.setSizes(self.mainConf.viewPanePos)
|
||||
|
||||
# Splitter : Document Editor / Document Viewer
|
||||
self.splitDocs = QSplitter(Qt.Horizontal)
|
||||
self.splitDocs.addWidget(self.docEditor)
|
||||
self.splitDocs.addWidget(self.splitView)
|
||||
self.splitDocs.setOpaqueResize(False)
|
||||
self.splitDocs.setHandleWidth(hWd)
|
||||
|
||||
# Splitter : Project Tree / Main Tabs
|
||||
@@ -153,6 +155,7 @@ class GuiMain(QMainWindow):
|
||||
self.splitMain.setContentsMargins(0, 0, 0, 0)
|
||||
self.splitMain.addWidget(self.treePane)
|
||||
self.splitMain.addWidget(self.splitDocs)
|
||||
self.splitMain.setOpaqueResize(False)
|
||||
self.splitMain.setHandleWidth(hWd)
|
||||
self.splitMain.setSizes(self.mainConf.mainPanePos)
|
||||
|
||||
@@ -224,10 +227,13 @@ class GuiMain(QMainWindow):
|
||||
self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts)
|
||||
self.docEditor.docCountsChanged.connect(self.projView.updateCounts)
|
||||
self.docEditor.loadDocumentTagRequest.connect(self._followTag)
|
||||
self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
|
||||
self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
|
||||
|
||||
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
|
||||
|
||||
self.outlineView.loadDocumentTagRequest.connect(self._followTag)
|
||||
self.outlineView.openDocumentRequest.connect(self._openDocument)
|
||||
|
||||
# Finalise Initialisation
|
||||
# =======================
|
||||
@@ -306,7 +312,7 @@ class GuiMain(QMainWindow):
|
||||
# Work Area
|
||||
self.docEditor.clearEditor()
|
||||
self.docEditor.setDictionaries()
|
||||
self.closeDocViewer()
|
||||
self.closeDocViewer(byUser=False)
|
||||
self.outlineView.clearProject()
|
||||
|
||||
# General
|
||||
@@ -593,14 +599,21 @@ class GuiMain(QMainWindow):
|
||||
logger.debug("Requested item '%s' is not a document", tHandle)
|
||||
return False
|
||||
|
||||
cHandle = self.docEditor.docHandle()
|
||||
if cHandle == tHandle:
|
||||
self.docEditor.setCursorLine(tLine)
|
||||
if changeFocus:
|
||||
self.docEditor.setFocus()
|
||||
return True
|
||||
|
||||
self.closeDocument(beforeOpen=True)
|
||||
self._changeView(nwView.EDITOR)
|
||||
if self.docEditor.loadText(tHandle, tLine):
|
||||
if changeFocus:
|
||||
self.docEditor.setFocus()
|
||||
self.theProject.data.setLastHandle(tHandle, "editor")
|
||||
self.projView.setSelectedHandle(tHandle, doScroll=doScroll)
|
||||
self.novelView.setActiveHandle(tHandle)
|
||||
if changeFocus:
|
||||
self.docEditor.setFocus()
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -618,7 +631,7 @@ class GuiMain(QMainWindow):
|
||||
fHandle = None # The first file handle we encounter
|
||||
foundIt = False # We've found tHandle, pick the next we see
|
||||
for tItem in self.theProject.tree:
|
||||
if tItem is None or not tItem.isFileType():
|
||||
if not tItem.isFileType():
|
||||
continue
|
||||
if fHandle is None:
|
||||
fHandle = tItem.itemHandle
|
||||
@@ -648,7 +661,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return True
|
||||
|
||||
def viewDocument(self, tHandle=None, tAnchor=None):
|
||||
def viewDocument(self, tHandle=None, sTitle=None):
|
||||
"""Load a document for viewing in the view panel.
|
||||
"""
|
||||
if not self.hasProject:
|
||||
@@ -687,7 +700,8 @@ class GuiMain(QMainWindow):
|
||||
self.splitDocs.setSizes(vPos)
|
||||
self.viewMeta.setVisible(self.mainConf.showRefPanel)
|
||||
|
||||
self.docViewer.navigateTo(tAnchor)
|
||||
if sTitle:
|
||||
self.docViewer.navigateTo(f"#{sTitle}")
|
||||
|
||||
return True
|
||||
|
||||
@@ -775,17 +789,23 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
tHandle = None
|
||||
sTitle = None
|
||||
tLine = None
|
||||
if self.projView.treeHasFocus():
|
||||
tHandle = self.projView.getSelectedHandle()
|
||||
elif self.novelView.treeHasFocus():
|
||||
tHandle, tLine = self.novelView.getSelectedHandle()
|
||||
tHandle, sTitle = self.novelView.getSelectedHandle()
|
||||
elif self.outlineView.treeHasFocus():
|
||||
tHandle, tLine = self.outlineView.getSelectedHandle()
|
||||
tHandle, sTitle = self.outlineView.getSelectedHandle()
|
||||
else:
|
||||
logger.warning("No item selected")
|
||||
return False
|
||||
|
||||
if tHandle is not None and sTitle is not None:
|
||||
hItem = self.theProject.index.getItemHeader(tHandle, sTitle)
|
||||
if hItem is not None:
|
||||
tLine = hItem.line
|
||||
|
||||
if tHandle is not None:
|
||||
self.openDocument(tHandle, tLine=tLine, changeFocus=False, doScroll=False)
|
||||
|
||||
@@ -808,17 +828,8 @@ class GuiMain(QMainWindow):
|
||||
"""Rebuild the project tree.
|
||||
"""
|
||||
self.projView.populateTree()
|
||||
# self.novelView.refreshTree()
|
||||
return
|
||||
|
||||
def requestNovelTreeRefresh(self):
|
||||
"""Update the novel tree, but only if it is visible.
|
||||
"""
|
||||
if self.projStack.currentIndex() == self.idxNovelView and self.hasProject:
|
||||
self.novelView.refreshTree()
|
||||
return True
|
||||
return False
|
||||
|
||||
def rebuildIndex(self, beQuiet=False):
|
||||
"""Rebuild the entire index.
|
||||
"""
|
||||
@@ -833,6 +844,7 @@ class GuiMain(QMainWindow):
|
||||
self.projView.saveProjectTasks()
|
||||
self.theProject.index.rebuildIndex()
|
||||
self.projView.populateTree()
|
||||
self.novelView.refreshTree()
|
||||
|
||||
tEnd = time()
|
||||
self.setStatus(
|
||||
@@ -1208,14 +1220,19 @@ class GuiMain(QMainWindow):
|
||||
self.theProject.data.setLastHandle(None, "editor")
|
||||
return
|
||||
|
||||
def closeDocViewer(self):
|
||||
def closeDocViewer(self, byUser=True):
|
||||
"""Close the document view panel.
|
||||
"""
|
||||
self.docViewer.clearViewer()
|
||||
self.theProject.data.setLastHandle(None, "viewer")
|
||||
if byUser:
|
||||
# Only reset the last handle if the user called this
|
||||
self.theProject.data.setLastHandle(None, "viewer")
|
||||
|
||||
# Hide the panel
|
||||
bPos = self.splitMain.sizes()
|
||||
self.splitView.setVisible(False)
|
||||
self.splitDocs.setSizes([bPos[1], 0])
|
||||
|
||||
return not self.splitView.isVisible()
|
||||
|
||||
def toggleFocusMode(self):
|
||||
@@ -1471,18 +1488,22 @@ class GuiMain(QMainWindow):
|
||||
if tMode == nwDocMode.EDIT:
|
||||
self.openDocument(tHandle)
|
||||
elif tMode == nwDocMode.VIEW:
|
||||
self.viewDocument(tHandle=tHandle, tAnchor=f"#{sTitle}")
|
||||
self.viewDocument(tHandle=tHandle, sTitle=sTitle)
|
||||
return
|
||||
|
||||
@pyqtSlot(str, Enum, int, str)
|
||||
def _openDocument(self, tHandle, tMode, tLine, tAnchor):
|
||||
@pyqtSlot(str, Enum, str, bool)
|
||||
def _openDocument(self, tHandle, tMode, sTitle, setFocus):
|
||||
"""Handle an open document request from one of the tree views.
|
||||
"""
|
||||
if tHandle is not None:
|
||||
if tMode == nwDocMode.EDIT:
|
||||
self.openDocument(tHandle, tLine=tLine, changeFocus=False)
|
||||
tLine = None
|
||||
hItem = self.theProject.index.getItemHeader(tHandle, sTitle)
|
||||
if hItem is not None:
|
||||
tLine = hItem.line
|
||||
self.openDocument(tHandle, tLine=tLine, changeFocus=setFocus)
|
||||
elif tMode == nwDocMode.VIEW:
|
||||
self.viewDocument(tHandle=tHandle, tAnchor=(tAnchor or None))
|
||||
self.viewDocument(tHandle=tHandle, sTitle=sTitle)
|
||||
return
|
||||
|
||||
@pyqtSlot(nwView)
|
||||
@@ -1564,7 +1585,6 @@ class GuiMain(QMainWindow):
|
||||
self.docEditor.closeSearch()
|
||||
elif self.isFocusMode:
|
||||
self.toggleFocusMode()
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot(int)
|
||||
@@ -1581,17 +1601,11 @@ class GuiMain(QMainWindow):
|
||||
"""Activated when the project view tab is changed.
|
||||
"""
|
||||
sHandle = None
|
||||
|
||||
if stIndex == self.idxProjView:
|
||||
sHandle = self.projView.getSelectedHandle()
|
||||
|
||||
elif stIndex == self.idxNovelView:
|
||||
if self.hasProject:
|
||||
self.novelView.refreshTree()
|
||||
sHandle, _ = self.novelView.getSelectedHandle()
|
||||
|
||||
sHandle, _ = self.novelView.getSelectedHandle()
|
||||
self.itemDetails.updateViewBox(sHandle)
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiMain
|
||||
|
||||
Reference in New Issue
Block a user