Update the Outline View panel (#1063)

Add a top level widget that contains the entire Outline View.
Move the details panel code to the outline.py file.
Add a toolbar to the top of the Outline view with a dropdown box to select which Novel folder to view.
Add a refresh button to the tool bar.
Move the context menu for changing columns to a menu button on the tool bar.
Remove the Rebuild Outline and Auto-Update Outline options from the main menu.
This commit is contained in:
Veronica Berglyd Olsen
2022-06-05 22:34:49 +02:00
committed by GitHub
31 changed files with 1150 additions and 739 deletions
-2
View File
@@ -59,7 +59,6 @@ The main shorcuts are as follows:
":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo." ":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo."
":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes." ":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes."
":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking." ":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking."
":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline."
":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree." ":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree."
":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree." ":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree."
":kbd:`Ctrl`:kbd:`Del`", "Delete next word in editor." ":kbd:`Ctrl`:kbd:`Del`", "Delete next word in editor."
@@ -88,7 +87,6 @@ The main shorcuts are as follows:
":kbd:`F7`", "Re-run spell checker." ":kbd:`F7`", "Re-run spell checker."
":kbd:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer." ":kbd:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer."
":kbd:`F9`", "Re-build the project index." ":kbd:`F9`", "Re-build the project index."
":kbd:`F10`", "Re-build the project outline."
":kbd:`F11`", "Activate full screen mode." ":kbd:`F11`", "Activate full screen mode."
":kbd:`Shift`:kbd:`F1`", "Open the local user manual (PDF) if it is available." ":kbd:`Shift`:kbd:`F1`", "Open the local user manual (PDF) if it is available."
":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document." ":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document."
@@ -47,6 +47,7 @@ edit = typ_pencil.svg
forward = typ_chevron-right.svg forward = typ_chevron-right.svg
hash = typ_hash.svg hash = typ_hash.svg
maximise = typ_arrow-maximise.svg maximise = typ_arrow-maximise.svg
menu = typ_th-menu.svg
minimise = typ_arrow-minimise.svg minimise = typ_arrow-minimise.svg
proj_chapter = mixed_document-chapter.svg proj_chapter = mixed_document-chapter.svg
proj_details = typ_th-list-grey.svg proj_details = typ_th-list-grey.svg
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg4455"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs4459" />
<path
d="M19 17h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2zM19 10h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2zM19 3h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2z"
id="path4453"
style="fill:#6699cc;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 571 B

@@ -47,6 +47,7 @@ edit = typ_pencil.svg
forward = typ_chevron-right.svg forward = typ_chevron-right.svg
hash = typ_hash.svg hash = typ_hash.svg
maximise = typ_arrow-maximise.svg maximise = typ_arrow-maximise.svg
menu = typ_th-menu.svg
minimise = typ_arrow-minimise.svg minimise = typ_arrow-minimise.svg
proj_chapter = mixed_document-chapter.svg proj_chapter = mixed_document-chapter.svg
proj_details = typ_th-list-grey.svg proj_details = typ_th-list-grey.svg
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg4455"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs4459" />
<path
d="M19 17h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2zM19 10h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2zM19 3h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2z"
id="path4453"
style="fill:#4271ae;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 571 B

+23 -30
View File
@@ -73,9 +73,8 @@ class NWIndex:
self._indexBroken = False self._indexBroken = False
# TimeStamps # TimeStamps
self._timeNovel = 0 self._indexChange = 0
self._timeNotes = 0 self._rootChange = {}
self._timeIndex = 0
return return
@@ -99,9 +98,8 @@ class NWIndex:
""" """
self._tagsIndex.clear() self._tagsIndex.clear()
self._itemIndex.clear() self._itemIndex.clear()
self._timeNovel = 0 self._indexChange = 0
self._timeNotes = 0 self._rootChange = {}
self._timeIndex = 0
return return
def deleteHandle(self, tHandle): def deleteHandle(self, tHandle):
@@ -129,20 +127,16 @@ class NWIndex:
return True return True
def novelChangedSince(self, checkTime):
"""Check if the novel index has changed since a given time.
"""
return self._timeNovel > checkTime
def notesChangedSince(self, checkTime):
"""Check if the notes index has changed since a given time.
"""
return self._timeNotes > checkTime
def indexChangedSince(self, checkTime): def indexChangedSince(self, checkTime):
"""Check if the index has changed since a given time. """Check if the index has changed since a given time.
""" """
return self._timeIndex > checkTime return self._indexChange > checkTime
def rootChangedSince(self, rootHandle, checkTime):
"""Check if the index has changed since a given time for a
given root item.
"""
return self._rootChange.get(rootHandle, self._indexChange) > checkTime
## ##
# Load and Save Index to/from File # Load and Save Index to/from File
@@ -184,10 +178,7 @@ class NWIndex:
logger.warning("Item '%s' is not in the index", fHandle) logger.warning("Item '%s' is not in the index", fHandle)
self.reIndexHandle(fHandle) self.reIndexHandle(fHandle)
nowTime = round(time()) self._indexChange = round(time())
self._timeNovel = nowTime
self._timeNotes = nowTime
self._timeIndex = nowTime
logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000) logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000)
@@ -307,11 +298,8 @@ class NWIndex:
# Update timestamps for index changes # Update timestamps for index changes
nowTime = round(time()) nowTime = round(time())
self._timeIndex = nowTime self._indexChange = nowTime
if theItem.itemLayout == nwItemLayout.NOTE: self._rootChange[theItem.itemRoot] = nowTime
self._timeNotes = nowTime
else:
self._timeNovel = nowTime
return True return True
@@ -466,14 +454,14 @@ class NWIndex:
# Extract Data # Extract Data
## ##
def novelStructure(self, skipExcl=True): def novelStructure(self, rootHandle=None, skipExcl=True):
"""Iterate over all titles in the novel, in the correct order as """Iterate over all titles in the novel, in the correct order as
they appear in the tree view and in the respective document they appear in the tree view and in the respective document
files, but skipping all note files. files, but skipping all note files.
""" """
for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl): novStruct = self._itemIndex.iterNovelStructure(rootHandle=rootHandle, skipExcl=skipExcl)
tKey = f"{tHandle}:{sTitle}" for tHandle, sTitle, hItem in novStruct:
yield tKey, tHandle, sTitle, hItem yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem
return return
def getNovelWordCount(self, skipExcl=True): def getNovelWordCount(self, skipExcl=True):
@@ -514,6 +502,11 @@ class NWIndex:
""" """
return self._itemIndex.mainItemHeader(tHandle) return self._itemIndex.mainItemHeader(tHandle)
def getHandleHeaderIntLevel(self, tHandle):
"""Get the integer header level of the first header of a handle.
"""
return H_LEVEL.get(self._itemIndex.mainItemHeader(tHandle), 0)
def getTableOfContents(self, maxDepth, skipExcl=True): def getTableOfContents(self, maxDepth, skipExcl=True):
"""Generate a table of contents up to a maximum depth. """Generate a table of contents up to a maximum depth.
""" """
+16
View File
@@ -265,6 +265,22 @@ class NWTree():
# Tree Root Methods # Tree Root Methods
## ##
def rootClasses(self):
"""Return a set of all root classes in use by the project.
"""
rootClasses = set()
for nwItem in self._treeRoots.values():
rootClasses.add(nwItem.itemClass)
return rootClasses
def iterRoots(self, itemClass):
"""Iterate over all items of a given class.
"""
for tHandle, nwItem in self._treeRoots.items():
if nwItem.itemClass == itemClass:
yield tHandle, nwItem
return
def isRoot(self, tHandle): def isRoot(self, tHandle):
"""Check if a handle is a root item. """Check if a handle is a root item.
""" """
+8
View File
@@ -62,6 +62,14 @@ class nwItemLayout(Enum):
# END Enum nwItemLayout # END Enum nwItemLayout
class nwDocMode(Enum):
VIEW = 0
EDIT = 1
# END Enum nwDocMode
class nwDocAction(Enum): class nwDocAction(Enum):
NO_ACTION = 0 NO_ACTION = 0
-2
View File
@@ -25,7 +25,6 @@ from novelwriter.gui.itemdetails import GuiItemDetails
from novelwriter.gui.mainmenu import GuiMainMenu from novelwriter.gui.mainmenu import GuiMainMenu
from novelwriter.gui.noveltree import GuiNovelTree from novelwriter.gui.noveltree import GuiNovelTree
from novelwriter.gui.outline import GuiOutline from novelwriter.gui.outline import GuiOutline
from novelwriter.gui.outlinedetails import GuiOutlineDetails
from novelwriter.gui.projtree import GuiProjectTree from novelwriter.gui.projtree import GuiProjectTree
from novelwriter.gui.statusbar import GuiMainStatus from novelwriter.gui.statusbar import GuiMainStatus
from novelwriter.gui.theme import GuiTheme from novelwriter.gui.theme import GuiTheme
@@ -40,7 +39,6 @@ __all__ = [
"GuiMainStatus", "GuiMainStatus",
"GuiNovelTree", "GuiNovelTree",
"GuiOutline", "GuiOutline",
"GuiOutlineDetails",
"GuiProjectTree", "GuiProjectTree",
"GuiTheme", "GuiTheme",
"GuiViewsBar", "GuiViewsBar",
+20 -13
View File
@@ -33,6 +33,7 @@ import bisect
import logging import logging
import novelwriter import novelwriter
from enum import Enum
from time import time from time import time
from PyQt5.QtCore import ( from PyQt5.QtCore import (
@@ -50,7 +51,7 @@ from PyQt5.QtWidgets import (
) )
from novelwriter.core import NWDoc, NWSpellEnchant, countWords from novelwriter.core import NWDoc, NWSpellEnchant, countWords
from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode
from novelwriter.common import transferCase from novelwriter.common import transferCase
from novelwriter.constants import nwConst, nwKeyWords, nwUnicode from novelwriter.constants import nwConst, nwKeyWords, nwUnicode
from novelwriter.gui.dochighlight import GuiDocHighlighter from novelwriter.gui.dochighlight import GuiDocHighlighter
@@ -69,6 +70,7 @@ class GuiDocEditor(QTextEdit):
spellDictionaryChanged = pyqtSignal(str, str) spellDictionaryChanged = pyqtSignal(str, str)
docEditedStatusChanged = pyqtSignal(bool) docEditedStatusChanged = pyqtSignal(bool)
docCountsChanged = pyqtSignal(str, int, int, int) docCountsChanged = pyqtSignal(str, int, int, int)
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, theParent): def __init__(self, theParent):
QTextEdit.__init__(self, theParent) QTextEdit.__init__(self, theParent)
@@ -567,16 +569,6 @@ class GuiDocEditor(QTextEdit):
return return
def updateDocInfo(self, tHandle):
"""Called when an item label is changed to check if the document
title bar needs updating,
"""
if tHandle == self._docHandle:
self.docHeader.setTitleFromHandle(self._docHandle)
self.docFooter.updateInfo()
self.updateDocMargins()
return
## ##
# Properties # Properties
## ##
@@ -1066,7 +1058,22 @@ class GuiDocEditor(QTextEdit):
return return
## ##
# Slots # Public Slots
##
@pyqtSlot(str)
def updateDocInfo(self, tHandle):
"""Called when an item label is changed to check if the document
title bar needs updating,
"""
if tHandle == self._docHandle:
self.docHeader.setTitleFromHandle(self._docHandle)
self.docFooter.updateInfo()
self.updateDocMargins()
return
##
# Private Slots
## ##
@pyqtSlot(int, int, int) @pyqtSlot(int, int, int)
@@ -1894,7 +1901,7 @@ class GuiDocEditor(QTextEdit):
if loadTag: if loadTag:
logger.verbose("Attempting to follow tag '%s'", theWord) logger.verbose("Attempting to follow tag '%s'", theWord)
self.theParent.docViewer.loadFromTag(theWord) self.loadDocumentTagRequest.emit(theWord, nwDocMode.VIEW)
else: else:
logger.verbose("Potential tag '%s'", theWord) logger.verbose("Potential tag '%s'", theWord)
+23 -38
View File
@@ -30,7 +30,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging import logging
import novelwriter import novelwriter
from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot from enum import Enum
from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot, pyqtSignal
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor
) )
@@ -40,7 +42,7 @@ from PyQt5.QtWidgets import (
) )
from novelwriter.core import ToHtml from novelwriter.core import ToHtml
from novelwriter.enum import nwAlert, nwItemType, nwDocAction from novelwriter.enum import nwItemType, nwDocAction, nwDocMode
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.constants import nwUnicode from novelwriter.constants import nwUnicode
@@ -49,6 +51,8 @@ logger = logging.getLogger(__name__)
class GuiDocViewer(QTextBrowser): class GuiDocViewer(QTextBrowser):
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, theParent): def __init__(self, theParent):
QTextBrowser.__init__(self, theParent) QTextBrowser.__init__(self, theParent)
@@ -239,30 +243,6 @@ class GuiDocViewer(QTextBrowser):
self.updateDocMargins() self.updateDocMargins()
return return
def loadFromTag(self, theTag):
"""Load text in the document from a reference given by a meta
tag rather than a known handle. This function depends on the
index being up to date.
"""
logger.debug("Loading document from tag '%s'", theTag)
tHandle, sTitle = self.theProject.index.getTagSource(theTag)
if tHandle is None:
self.theParent.makeAlert(self.tr(
"Could not find the reference for tag '{0}'. It either doesn't "
"exist, or the index is out of date. The index can be updated "
"from the Tools menu, or by pressing {1}."
).format(
theTag, "F9"
), nwAlert.ERROR)
return False
else:
# Let the parent handle the opening as it also ensures that
# the doc view panel is visible in case this request comes
# from outside this class.
logger.verbose("Tag points to '%s#%s'", tHandle, sTitle)
self.theParent.viewDocument(tHandle, "#%s" % sTitle)
return True
def docAction(self, theAction): def docAction(self, theAction):
"""Wrapper function for various document actions on the current """Wrapper function for various document actions on the current
document. document.
@@ -342,15 +322,6 @@ class GuiDocViewer(QTextBrowser):
return return
def updateDocInfo(self, tHandle):
"""Called when an item label is changed to check if the document
title bar needs updating,
"""
if tHandle == self._docHandle:
self.docHeader.setTitleFromHandle(self._docHandle)
self.updateDocMargins()
return
## ##
# Properties # Properties
## ##
@@ -409,19 +380,33 @@ class GuiDocViewer(QTextBrowser):
return 0 return 0
## ##
# Slots # Public Slots
##
@pyqtSlot(str)
def updateDocInfo(self, tHandle):
"""Called when an item label is changed to check if the document
title bar needs updating,
"""
if tHandle == self._docHandle:
self.docHeader.setTitleFromHandle(self._docHandle)
self.updateDocMargins()
return
##
# Private Slots
## ##
@pyqtSlot("QUrl") @pyqtSlot("QUrl")
def _linkClicked(self, theURL): def _linkClicked(self, theURL):
"""Slot for a link in the document being clicked. """Process a clicked link internally in the document.
""" """
theLink = theURL.url() theLink = theURL.url()
logger.verbose("Clicked link: '%s'", theLink) logger.verbose("Clicked link: '%s'", theLink)
if len(theLink) > 0: if len(theLink) > 0:
theBits = theLink.split("=") theBits = theLink.split("=")
if len(theBits) == 2: if len(theBits) == 2:
self.loadFromTag(theBits[1]) self.loadDocumentTagRequest.emit(theBits[1], nwDocMode.VIEW)
return return
@pyqtSlot("QPoint") @pyqtSlot("QPoint")
+5 -4
View File
@@ -220,6 +220,11 @@ class GuiItemDetails(QWidget):
""" """
self.updateViewBox(self._itemHandle) self.updateViewBox(self._itemHandle)
##
# Public Slots
##
@pyqtSlot(str)
def updateViewBox(self, tHandle): def updateViewBox(self, tHandle):
"""Populate the details box from a given handle. """Populate the details box from a given handle.
""" """
@@ -290,10 +295,6 @@ class GuiItemDetails(QWidget):
return return
##
# Slots
##
@pyqtSlot(str, int, int, int) @pyqtSlot(str, int, int, int)
def doUpdateCounts(self, tHandle, cC, wC, pC): def doUpdateCounts(self, tHandle, cC, wC, pC):
"""Update the counts if the handle is the same as the one we're """Update the counts if the handle is the same as the one we're
-25
View File
@@ -81,12 +81,6 @@ class GuiMainMenu(QMenuBar):
self.aSpellCheck.setChecked(theMode) self.aSpellCheck.setChecked(theMode)
return return
def setAutoOutline(self, theMode):
"""Forward auto outline check state to its action.
"""
self.aAutoOutline.setChecked(theMode)
return
def setFocusMode(self, theMode): def setFocusMode(self, theMode):
"""Forward focus mode check state to its action. """Forward focus mode check state to its action.
""" """
@@ -105,12 +99,6 @@ class GuiMainMenu(QMenuBar):
self.theParent.docEditor.toggleSpellCheck(None) self.theParent.docEditor.toggleSpellCheck(None)
return True return True
def _toggleAutoOutline(self, theMode):
"""Toggle auto outline when the menu entry is checked.
"""
self.theProject.setAutoOutline(theMode)
return True
def _openWebsite(self, theUrl): def _openWebsite(self, theUrl):
"""Open a URL in the system's default browser. """Open a URL in the system's default browser.
""" """
@@ -889,19 +877,6 @@ class GuiMainMenu(QMenuBar):
self.aRebuildIndex.triggered.connect(lambda: self.theParent.rebuildIndex()) self.aRebuildIndex.triggered.connect(lambda: self.theParent.rebuildIndex())
self.toolsMenu.addAction(self.aRebuildIndex) self.toolsMenu.addAction(self.aRebuildIndex)
# Tools > Rebuild Outline
self.aRebuildOutline = QAction(self.tr("Rebuild Outline"), self)
self.aRebuildOutline.setShortcut("F10")
self.aRebuildOutline.triggered.connect(lambda: self.theParent.rebuildOutline())
self.toolsMenu.addAction(self.aRebuildOutline)
# Tools > Toggle Auto Build Outline
self.aAutoOutline = QAction(self.tr("Auto-Update Outline"), self)
self.aAutoOutline.setCheckable(True)
self.aAutoOutline.toggled.connect(self._toggleAutoOutline)
self.aAutoOutline.setShortcut("Ctrl+F10")
self.toolsMenu.addAction(self.aAutoOutline)
# Tools > Separator # Tools > Separator
self.toolsMenu.addSeparator() self.toolsMenu.addSeparator()
+1 -2
View File
@@ -136,7 +136,7 @@ class GuiNovelTree(QTreeWidget):
""" """
logger.verbose("Requesting refresh of the novel tree") logger.verbose("Requesting refresh of the novel tree")
treeChanged = self.theParent.treeView.changedSince(self._lastBuild) treeChanged = self.theParent.treeView.changedSince(self._lastBuild)
indexChanged = self.theProject.index.novelChangedSince(self._lastBuild) indexChanged = self.theProject.index.indexChangedSince(self._lastBuild)
if not (treeChanged or indexChanged or overRide): if not (treeChanged or indexChanged or overRide):
logger.verbose("No changes have been made to the novel index") logger.verbose("No changes have been made to the novel index")
return return
@@ -146,7 +146,6 @@ class GuiNovelTree(QTreeWidget):
if selItem: if selItem:
titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2] titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2]
self.theParent.treeView.flushTreeOrder()
self._populateTree() self._populateTree()
if titleKey is not None and titleKey in self._treeMap: if titleKey is not None and titleKey in self._treeMap:
+604 -46
View File
@@ -4,7 +4,11 @@ novelWriter GUI Project Outline
GUI class for the project outline view GUI class for the project outline view
File History: File History:
Created: 2019-11-16 [0.4.1] Created: 2022-05-15 [1.7b1] GuiOutline
Created: 2022-05-22 [1.7b1] GuiOutlineToolBar
Created: 2019-11-16 [0.4.1] GuiOutlineView
Created: 2019-11-16 [0.4.1] GuiOutlineHeaderMenu
Created: 2020-06-02 [0.7.0] GuiOutlineDetails
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen Copyright 20182022, Veronica Berglyd Olsen
@@ -27,20 +31,244 @@ import logging
import novelwriter import novelwriter
from time import time from time import time
from enum import Enum
from PyQt5.QtCore import Qt, QSize, pyqtSlot from PyQt5.QtCore import (
Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP
)
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QMenu, QAction, QAbstractItemView, QFrame QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel,
QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
QWidget, QFrame, QToolBar, QSizePolicy, QComboBox, QToolButton
) )
from novelwriter.enum import nwItemLayout, nwItemType, nwOutline from novelwriter.enum import (
nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline
)
from novelwriter.common import checkInt from novelwriter.common import checkInt
from novelwriter.constants import trConst, nwKeyWords, nwLabels from novelwriter.constants import trConst, nwKeyWords, nwLabels
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GuiOutline(QTreeWidget): class GuiOutline(QWidget):
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, theParent):
QWidget.__init__(self, theParent)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.outlineBar = GuiOutlineToolBar(self)
self.outlineView = GuiOutlineView(self)
self.outlineData = GuiOutlineDetails(self)
self.splitOutline = QSplitter(Qt.Vertical)
self.splitOutline.addWidget(self.outlineView)
self.splitOutline.addWidget(self.outlineData)
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.setContentsMargins(0, 0, 0, 0)
self.outerBox.addWidget(self.outlineBar)
self.outerBox.addWidget(self.splitOutline)
self.setLayout(self.outerBox)
# Connect Signals
self.outlineView.hiddenStateChanged.connect(self._updateMenuColumns)
self.outlineView.activeItemChanged.connect(self.outlineData.showItem)
self.outlineData.itemTagClicked.connect(self._tagClicked)
self.outlineBar.loadNovelRootRequest.connect(self._rootItemChanged)
self.outlineBar.viewColumnToggled.connect(self.outlineView.menuColumnToggled)
# Function Mappings
self.getSelectedHandle = self.outlineView.getSelectedHandle
return
##
# Methods
##
def splitSizes(self):
return self.splitOutline.sizes()
def clearOutline(self):
self.outlineData.clearDetails()
return
def initOutline(self):
self.outlineView.initOutline()
self.outlineData.initDetails()
return
def closeOutline(self):
self.outlineView.closeOutline()
self.outlineData.updateClasses()
return
def refreshView(self, overRide=False, novelChanged=False):
self.outlineView.refreshTree(overRide=overRide, novelChanged=novelChanged)
return
def treeFocus(self):
return self.outlineView.hasFocus()
def setTreeFocus(self):
return self.outlineView.setFocus()
##
# Public Slots
##
@pyqtSlot(str)
def updateRootItem(self, tHandle):
"""Should be called whenever a root folders changes.
"""
self.outlineBar.populateNovelList()
self.outlineData.updateClasses()
return
##
# Private Slots
##
@pyqtSlot()
def _updateMenuColumns(self):
"""Trigger an update of the toggled state of the column menu
checkboxes whenever a signal is received that the hidden state
of columns has changed.
"""
self.outlineBar.setColumnHiddenState(self.outlineView.hiddenColumns)
return
@pyqtSlot(str)
def _tagClicked(self, link):
"""Capture the click of a tag in the details panel.
"""
if link:
self.loadDocumentTagRequest.emit(link, nwDocMode.VIEW)
return
@pyqtSlot(str)
def _rootItemChanged(self, handle):
"""The root novel handle has changed or needs to be refreshed.
"""
self.outlineView.refreshTree(rootHandle=(handle or None), overRide=True)
return
# END Class GuiOutline
class GuiOutlineToolBar(QToolBar):
loadNovelRootRequest = pyqtSignal(str)
viewColumnToggled = pyqtSignal(bool, Enum)
def __init__(self, theOutline):
QTreeWidget.__init__(self, theOutline)
logger.debug("Initialising GuiOutlineToolBar ...")
self.mainConf = novelwriter.CONFIG
self.theParent = theOutline.theParent
self.theProject = theOutline.theParent.theProject
self.theTheme = theOutline.theParent.theTheme
iPx = self.mainConf.pxInt(22)
mPx = self.mainConf.pxInt(12)
self.setMovable(False)
self.setIconSize(QSize(iPx, iPx))
self.setContentsMargins(0, 0, 0, 0)
self.setStyleSheet("QToolBar {border: 0px;}")
stretch = QWidget(self)
stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Novel Selector
self.novelLabel = QLabel(self.tr("Outline of"))
self.novelLabel.setContentsMargins(0, 0, mPx, 0)
self.novelValue = QComboBox(self)
self.novelValue.setMinimumWidth(self.mainConf.pxInt(200))
self.novelValue.currentIndexChanged.connect(self._novelValueChanged)
# Actions
self.aRefresh = QAction(self.tr("Refresh"), self)
self.aRefresh.setIcon(self.theTheme.getIcon("refresh"))
self.aRefresh.triggered.connect(self._refreshRequested)
# Column Menu
self.mColumns = GuiOutlineHeaderMenu(self)
self.mColumns.columnToggled.connect(
lambda isChecked, tItem: self.viewColumnToggled.emit(isChecked, tItem)
)
self.tbColumns = QToolButton(self)
self.tbColumns.setIcon(self.theTheme.getIcon("menu"))
self.tbColumns.setMenu(self.mColumns)
self.tbColumns.setPopupMode(QToolButton.InstantPopup)
# Assemble
self.addWidget(self.novelLabel)
self.addWidget(self.novelValue)
self.addSeparator()
self.addAction(self.aRefresh)
self.addWidget(self.tbColumns)
self.addWidget(stretch)
logger.debug("GuiOutlineToolBar initialisation complete")
##
# Methods
##
def populateNovelList(self):
"""Fill the novel combo box with a list of all novel folders.
"""
self.novelValue.clear()
tIcon = self.theTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])
for tHandle, nwItem in self.theProject.tree.iterRoots(nwItemClass.NOVEL):
self.novelValue.addItem(tIcon, nwItem.itemName, tHandle)
self.novelValue.insertSeparator(self.novelValue.count())
self.novelValue.addItem(tIcon, self.tr("All Novel Folders"), "")
return
def setColumnHiddenState(self, hiddenState):
"""Forward the change of column hidden states to the menu.
"""
self.mColumns.setHiddenState(hiddenState)
return
##
# Private Slots
##
@pyqtSlot(int)
def _novelValueChanged(self, index):
"""Emit a signal containing the handle of the selected item.
"""
if index >= 0:
self.loadNovelRootRequest.emit(self.novelValue.currentData())
return
@pyqtSlot()
def _refreshRequested(self):
"""Emit a signal containing the handle of the selected item.
"""
self.loadNovelRootRequest.emit(self.novelValue.currentData())
return
# END Class GuiOutlineToolBar
class GuiOutlineView(QTreeWidget):
DEF_WIDTH = { DEF_WIDTH = {
nwOutline.TITLE: 200, nwOutline.TITLE: 200,
@@ -82,16 +310,18 @@ class GuiOutline(QTreeWidget):
nwOutline.SYNOP: False, nwOutline.SYNOP: False,
} }
def __init__(self, theParent): hiddenStateChanged = pyqtSignal()
QTreeWidget.__init__(self, theParent) activeItemChanged = pyqtSignal(str, str)
logger.debug("Initialising GuiOutline ...") def __init__(self, theOutline):
QTreeWidget.__init__(self, theOutline)
logger.debug("Initialising GuiOutlineView ...")
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theParent = theParent self.theParent = theOutline.theParent
self.theProject = theParent.theProject self.theProject = theOutline.theParent.theProject
self.theTheme = theParent.theTheme self.theTheme = theOutline.theParent.theTheme
self.headerMenu = GuiOutlineHeaderMenu(self)
self.setFrameStyle(QFrame.NoFrame) self.setFrameStyle(QFrame.NoFrame)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
@@ -106,8 +336,6 @@ class GuiOutline(QTreeWidget):
self.setIndentation(iPx) self.setIndentation(iPx)
self.treeHead = self.header() self.treeHead = self.header()
self.treeHead.setContextMenuPolicy(Qt.CustomContextMenu)
self.treeHead.customContextMenuRequested.connect(self._headerRightClick)
self.treeHead.sectionMoved.connect(self._columnMoved) self.treeHead.sectionMoved.connect(self._columnMoved)
# Internals # Internals
@@ -121,12 +349,25 @@ class GuiOutline(QTreeWidget):
self.initOutline() self.initOutline()
self.clearOutline() self.clearOutline()
self.headerMenu.setHiddenState(self._colHidden)
logger.debug("GuiOutline initialisation complete") self.hiddenStateChanged.emit()
logger.debug("GuiOutlineView initialisation complete")
return return
##
# Properties
##
@property
def hiddenColumns(self):
return self._colHidden
##
# Methods
##
def initOutline(self): def initOutline(self):
"""Set or update outline settings. """Set or update outline settings.
""" """
@@ -166,7 +407,7 @@ class GuiOutline(QTreeWidget):
return return
def refreshTree(self, overRide=False, novelChanged=False): def refreshTree(self, rootHandle=None, overRide=False, novelChanged=False):
"""Called whenever the Outline tab is activated and controls """Called whenever the Outline tab is activated and controls
what data to load, and if necessary, force a rebuild of the what data to load, and if necessary, force a rebuild of the
tree. tree.
@@ -174,17 +415,17 @@ class GuiOutline(QTreeWidget):
# If it's the first time, we always build # If it's the first time, we always build
if self._firstView or self._firstView and overRide: if self._firstView or self._firstView and overRide:
self._loadHeaderState() self._loadHeaderState()
self._populateTree() self._populateTree(rootHandle)
self._firstView = False self._firstView = False
return return
# If the novel index or novel tree has changed since the tree # If the novel index or novel tree has changed since the tree
# was last built, we rebuild the tree from the updated index. # was last built, we rebuild the tree from the updated index.
indexChanged = self.theProject.index.novelChangedSince(self._lastBuild) indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline
if doBuild or overRide: if doBuild or overRide:
logger.debug("Rebuilding Project Outline") logger.debug("Rebuilding Project Outline")
self._populateTree() self._populateTree(rootHandle)
return return
@@ -220,7 +461,7 @@ class GuiOutline(QTreeWidget):
document editor. document editor.
""" """
tHandle, tLine = self.getSelectedHandle() tHandle, tLine = self.getSelectedHandle()
self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True) self.theParent.openDocument(tHandle, tLine=tLine - 1, doScroll=True)
return return
@pyqtSlot() @pyqtSlot()
@@ -232,18 +473,10 @@ class GuiOutline(QTreeWidget):
if selItems: if selItems:
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole) tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole) sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole)
self.theParent.projMeta.showItem(tHandle, sTitle) self.activeItemChanged.emit(tHandle, sTitle)
self.theParent.treeView.setSelectedHandle(tHandle)
return return
@pyqtSlot("QPoint")
def _headerRightClick(self, clickPos):
"""Show the header column menu.
"""
self.headerMenu.exec_(self.mapToGlobal(clickPos))
return
@pyqtSlot(int, int, int) @pyqtSlot(int, int, int)
def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx): def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx):
"""Make sure the order array is up to date with the actual order """Make sure the order array is up to date with the actual order
@@ -253,9 +486,10 @@ class GuiOutline(QTreeWidget):
self._saveHeaderState() self._saveHeaderState()
return return
def _menuColumnToggled(self, isChecked, theItem): @pyqtSlot(bool, Enum)
def menuColumnToggled(self, isChecked, theItem):
"""Receive the changes to column visibility forwarded by the """Receive the changes to column visibility forwarded by the
header context menu. column selection menu.
""" """
logger.verbose("User toggled Outline column '%s'", theItem.name) logger.verbose("User toggled Outline column '%s'", theItem.name)
if theItem in self._colIdx: if theItem in self._colIdx:
@@ -314,7 +548,7 @@ class GuiOutline(QTreeWidget):
except Exception: except Exception:
logger.warning("Ignored unknown outline column '%s'", str(hName)) logger.warning("Ignored unknown outline column '%s'", str(hName))
self.headerMenu.setHiddenState(self._colHidden) self.hiddenStateChanged.emit()
return return
@@ -356,7 +590,7 @@ class GuiOutline(QTreeWidget):
return return
def _populateTree(self): def _populateTree(self, rootHandle):
"""Build the tree based on the project index, and the header """Build the tree based on the project index, and the header
based on the defined constants, default values and user selected based on the defined constants, default values and user selected
width, order and hidden state. All columns are populated, even width, order and hidden state. All columns are populated, even
@@ -389,7 +623,8 @@ class GuiOutline(QTreeWidget):
currChapter = None currChapter = None
currScene = None currScene = None
for _, tHandle, sTitle, novIdx in self.theProject.index.novelStructure(skipExcl=True): novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
for _, tHandle, sTitle, novIdx in novStruct:
tItem = self._createTreeItem(tHandle, sTitle, novIdx) tItem = self._createTreeItem(tHandle, sTitle, novIdx)
@@ -479,15 +714,16 @@ class GuiOutline(QTreeWidget):
return newItem return newItem
# END Class GuiOutline # END Class GuiOutlineView
class GuiOutlineHeaderMenu(QMenu): class GuiOutlineHeaderMenu(QMenu):
def __init__(self, theParent): columnToggled = pyqtSignal(bool, Enum)
QMenu.__init__(self, theParent)
def __init__(self, theOutline):
QMenu.__init__(self, theOutline)
self.theParent = theParent
self.acceptToggle = True self.acceptToggle = True
mnuHead = QAction(self.tr("Select Columns"), self) mnuHead = QAction(self.tr("Select Columns"), self)
@@ -501,7 +737,7 @@ class GuiOutlineHeaderMenu(QMenu):
self.actionMap[hItem] = QAction(trConst(nwLabels.OUTLINE_COLS[hItem]), self) self.actionMap[hItem] = QAction(trConst(nwLabels.OUTLINE_COLS[hItem]), self)
self.actionMap[hItem].setCheckable(True) self.actionMap[hItem].setCheckable(True)
self.actionMap[hItem].toggled.connect( self.actionMap[hItem].toggled.connect(
lambda isChecked, tItem=hItem: self._columnToggled(isChecked, tItem) lambda isChecked, tItem=hItem: self.columnToggled.emit(isChecked, tItem)
) )
self.addAction(self.actionMap[hItem]) self.addAction(self.actionMap[hItem])
@@ -522,16 +758,338 @@ class GuiOutlineHeaderMenu(QMenu):
return return
# END Class GuiOutlineHeaderMenu
class GuiOutlineDetails(QScrollArea):
LVL_MAP = {
"H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"),
"H2": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Chapter"),
"H3": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Scene"),
"H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"),
}
itemTagClicked = pyqtSignal(str)
def __init__(self, theOutline):
QScrollArea.__init__(self, theOutline)
logger.debug("Initialising GuiOutlineDetails ...")
self.mainConf = novelwriter.CONFIG
self.theOutline = theOutline
self.theParent = theOutline.theParent
self.theProject = theOutline.theParent.theProject
self.theTheme = theOutline.theParent.theTheme
# Sizes
minTitle = 30*self.theTheme.textNWidth
maxTitle = 40*self.theTheme.textNWidth
wCount = self.theTheme.getTextWidth("999,999")
hSpace = int(self.mainConf.pxInt(10))
vSpace = int(self.mainConf.pxInt(4))
# Details Area
self.titleLabel = QLabel("<b>%s</b>" % self.tr("Title"))
self.fileLabel = QLabel("<b>%s</b>" % self.tr("Document"))
self.itemLabel = QLabel("<b>%s</b>" % self.tr("Status"))
self.titleValue = QLabel("")
self.fileValue = QLabel("")
self.itemValue = QLabel("")
self.titleValue.setMinimumWidth(minTitle)
self.titleValue.setMaximumWidth(maxTitle)
self.fileValue.setMinimumWidth(minTitle)
self.fileValue.setMaximumWidth(maxTitle)
self.itemValue.setMinimumWidth(minTitle)
self.itemValue.setMaximumWidth(maxTitle)
# Stats Area
self.cCLabel = QLabel("<b>%s</b>" % self.tr("Characters"))
self.wCLabel = QLabel("<b>%s</b>" % self.tr("Words"))
self.pCLabel = QLabel("<b>%s</b>" % self.tr("Paragraphs"))
self.cCValue = QLabel("")
self.wCValue = QLabel("")
self.pCValue = QLabel("")
self.cCValue.setMinimumWidth(wCount)
self.wCValue.setMinimumWidth(wCount)
self.pCValue.setMinimumWidth(wCount)
self.cCValue.setAlignment(Qt.AlignRight)
self.wCValue.setAlignment(Qt.AlignRight)
self.pCValue.setAlignment(Qt.AlignRight)
# Synopsis
self.synopLabel = QLabel("<b>%s</b>" % self.tr("Synopsis"))
self.synopValue = QLabel("")
self.synopLWrap = QHBoxLayout()
self.synopValue.setWordWrap(True)
self.synopValue.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self.synopLWrap.addWidget(self.synopValue, 1)
# Tags
self.povKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]))
self.focKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]))
self.chrKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY]))
self.pltKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]))
self.timKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.TIME_KEY]))
self.wldKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY]))
self.objKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY]))
self.entKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY]))
self.cstKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY]))
self.povKeyLWrap = QHBoxLayout()
self.focKeyLWrap = QHBoxLayout()
self.chrKeyLWrap = QHBoxLayout()
self.pltKeyLWrap = QHBoxLayout()
self.timKeyLWrap = QHBoxLayout()
self.wldKeyLWrap = QHBoxLayout()
self.objKeyLWrap = QHBoxLayout()
self.entKeyLWrap = QHBoxLayout()
self.cstKeyLWrap = QHBoxLayout()
self.povKeyValue = QLabel("")
self.focKeyValue = QLabel("")
self.chrKeyValue = QLabel("")
self.pltKeyValue = QLabel("")
self.timKeyValue = QLabel("")
self.wldKeyValue = QLabel("")
self.objKeyValue = QLabel("")
self.entKeyValue = QLabel("")
self.cstKeyValue = QLabel("")
self.povKeyValue.setWordWrap(True)
self.focKeyValue.setWordWrap(True)
self.chrKeyValue.setWordWrap(True)
self.pltKeyValue.setWordWrap(True)
self.timKeyValue.setWordWrap(True)
self.wldKeyValue.setWordWrap(True)
self.objKeyValue.setWordWrap(True)
self.entKeyValue.setWordWrap(True)
self.cstKeyValue.setWordWrap(True)
def tagClicked(link):
self.itemTagClicked.emit(link)
self.povKeyValue.linkActivated.connect(tagClicked)
self.focKeyValue.linkActivated.connect(tagClicked)
self.chrKeyValue.linkActivated.connect(tagClicked)
self.pltKeyValue.linkActivated.connect(tagClicked)
self.timKeyValue.linkActivated.connect(tagClicked)
self.wldKeyValue.linkActivated.connect(tagClicked)
self.objKeyValue.linkActivated.connect(tagClicked)
self.entKeyValue.linkActivated.connect(tagClicked)
self.cstKeyValue.linkActivated.connect(tagClicked)
self.povKeyLWrap.addWidget(self.povKeyValue, 1)
self.focKeyLWrap.addWidget(self.focKeyValue, 1)
self.chrKeyLWrap.addWidget(self.chrKeyValue, 1)
self.pltKeyLWrap.addWidget(self.pltKeyValue, 1)
self.timKeyLWrap.addWidget(self.timKeyValue, 1)
self.wldKeyLWrap.addWidget(self.wldKeyValue, 1)
self.objKeyLWrap.addWidget(self.objKeyValue, 1)
self.entKeyLWrap.addWidget(self.entKeyValue, 1)
self.cstKeyLWrap.addWidget(self.cstKeyValue, 1)
# Selected Item Details
self.mainGroup = QGroupBox(self.tr("Title Details"), self)
self.mainForm = QGridLayout()
self.mainGroup.setLayout(self.mainForm)
self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.fileLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.fileValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.itemLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.itemValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addLayout(self.synopLWrap, 4, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.setColumnStretch(1, 1)
self.mainForm.setRowStretch(4, 1)
self.mainForm.setHorizontalSpacing(hSpace)
self.mainForm.setVerticalSpacing(vSpace)
# Selected Item Tags
self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self)
self.tagsForm = QGridLayout()
self.tagsGroup.setLayout(self.tagsForm)
self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.focKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.focKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.chrKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.chrKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.pltKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.pltKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.timKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.timKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.wldKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.wldKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.objKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.objKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.entKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.entKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.cstKeyLabel, 8, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.cstKeyLWrap, 8, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.setColumnStretch(1, 1)
self.tagsForm.setRowStretch(8, 1)
self.tagsForm.setHorizontalSpacing(hSpace)
self.tagsForm.setVerticalSpacing(vSpace)
# Assemble
self.outerWidget = QWidget()
self.outerBox = QHBoxLayout()
self.outerBox.addWidget(self.mainGroup, 0)
self.outerBox.addWidget(self.tagsGroup, 1)
self.outerWidget.setLayout(self.outerBox)
self.setWidget(self.outerWidget)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setWidgetResizable(True)
self.setFrameStyle(QFrame.NoFrame)
self.initDetails()
logger.debug("GuiOutlineDetails initialisation complete")
return
def initDetails(self):
"""Set or update outline settings.
"""
# Scroll bars
if self.mainConf.hideVScroll:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
if self.mainConf.hideHScroll:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.updateClasses()
return
def clearDetails(self):
"""Clear all the data labels.
"""
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText("")
self.fileValue.setText("")
self.itemValue.setText("")
self.cCValue.setText("")
self.wCValue.setText("")
self.pCValue.setText("")
self.synopValue.setText("")
self.povKeyValue.setText("")
self.focKeyValue.setText("")
self.chrKeyValue.setText("")
self.pltKeyValue.setText("")
self.timKeyValue.setText("")
self.wldKeyValue.setText("")
self.objKeyValue.setText("")
self.entKeyValue.setText("")
self.cstKeyValue.setText("")
self.updateClasses()
return
## ##
# Slots # Slots
## ##
def _columnToggled(self, isChecked, theItem): @pyqtSlot(str, str)
"""The user has toggled the visibility of a column. Forward the def showItem(self, tHandle, sTitle):
event to the parent class only if we're accepting changes. """Update the content of the tree with the given handle and line
number pointing to a header.
""" """
if self.acceptToggle: pIndex = self.theProject.index
self.theParent._menuColumnToggled(isChecked, theItem) nwItem = self.theProject.tree[tHandle]
novIdx = pIndex.getNovelData(tHandle, sTitle)
theRefs = pIndex.getReferences(tHandle, sTitle)
if nwItem is None or novIdx is None:
return False
if novIdx.level in self.LVL_MAP:
self.titleLabel.setText("<b>%s</b>" % self.tr(self.LVL_MAP[novIdx.level]))
else:
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText(novIdx.title)
itemStatus, _ = nwItem.getImportStatus()
self.fileValue.setText(nwItem.itemName)
self.itemValue.setText(itemStatus)
cC = checkInt(novIdx.charCount, 0)
wC = checkInt(novIdx.wordCount, 0)
pC = checkInt(novIdx.paraCount, 0)
self.cCValue.setText(f"{cC:n}")
self.wCValue.setText(f"{wC:n}")
self.pCValue.setText(f"{pC:n}")
self.synopValue.setText(novIdx.synopsis)
self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY))
self.focKeyValue.setText(self._formatTags(theRefs, nwKeyWords.FOCUS_KEY))
self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY))
self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY))
self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY))
self.wldKeyValue.setText(self._formatTags(theRefs, nwKeyWords.WORLD_KEY))
self.objKeyValue.setText(self._formatTags(theRefs, nwKeyWords.OBJECT_KEY))
self.entKeyValue.setText(self._formatTags(theRefs, nwKeyWords.ENTITY_KEY))
self.cstKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CUSTOM_KEY))
return True
@pyqtSlot()
def updateClasses(self):
"""Update the visibility status of class details.
"""
usedClasses = self.theProject.tree.rootClasses()
pltVisible = nwItemClass.PLOT in usedClasses
timVisible = nwItemClass.TIMELINE in usedClasses
wldVisible = nwItemClass.WORLD in usedClasses
objVisible = nwItemClass.OBJECT in usedClasses
entVisible = nwItemClass.ENTITY in usedClasses
cstVisible = nwItemClass.CUSTOM in usedClasses
self.pltKeyLabel.setVisible(pltVisible)
self.pltKeyValue.setVisible(pltVisible)
self.timKeyLabel.setVisible(timVisible)
self.timKeyValue.setVisible(timVisible)
self.wldKeyLabel.setVisible(wldVisible)
self.wldKeyValue.setVisible(wldVisible)
self.objKeyLabel.setVisible(objVisible)
self.objKeyValue.setVisible(objVisible)
self.entKeyLabel.setVisible(entVisible)
self.entKeyValue.setVisible(entVisible)
self.cstKeyLabel.setVisible(cstVisible)
self.cstKeyValue.setVisible(cstVisible)
return return
# END Class GuiOutlineHeaderMenu @staticmethod
def _formatTags(refs, key):
"""Convert a list of tags into a list of clickable tag links.
"""
return ", ".join(
[f"<a href='{tag}'>{tag}</a>" for tag in refs.get(key, [])]
)
# END Class GuiOutlineDetails
-355
View File
@@ -1,355 +0,0 @@
"""
novelWriter GUI Project Outline Details
=========================================
GUI class for the project outline details panel
File History:
Created: 2020-06-02 [0.7.0]
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import novelwriter
from PyQt5.QtCore import Qt, QT_TRANSLATE_NOOP, pyqtSignal
from PyQt5.QtWidgets import (
QScrollArea, QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel, QFrame
)
from novelwriter.enum import nwView
from novelwriter.common import checkInt
from novelwriter.constants import trConst, nwKeyWords, nwLabels
logger = logging.getLogger(__name__)
class GuiOutlineDetails(QScrollArea):
LVL_MAP = {
"H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"),
"H2": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Chapter"),
"H3": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Scene"),
"H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"),
}
viewChangeRequested = pyqtSignal(nwView)
def __init__(self, theParent):
QScrollArea.__init__(self, theParent)
logger.debug("Initialising GuiOutlineDetails ...")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
self.theTheme = theParent.theTheme
# Sizes
minTitle = 30*self.theTheme.textNWidth
maxTitle = 40*self.theTheme.textNWidth
wCount = self.theTheme.getTextWidth("999,999")
hSpace = int(self.mainConf.pxInt(10))
vSpace = int(self.mainConf.pxInt(4))
# Details Area
self.titleLabel = QLabel("<b>%s</b>" % self.tr("Title"))
self.fileLabel = QLabel("<b>%s</b>" % self.tr("Document"))
self.itemLabel = QLabel("<b>%s</b>" % self.tr("Status"))
self.titleValue = QLabel("")
self.fileValue = QLabel("")
self.itemValue = QLabel("")
self.titleValue.setMinimumWidth(minTitle)
self.titleValue.setMaximumWidth(maxTitle)
self.fileValue.setMinimumWidth(minTitle)
self.fileValue.setMaximumWidth(maxTitle)
self.itemValue.setMinimumWidth(minTitle)
self.itemValue.setMaximumWidth(maxTitle)
# Stats Area
self.cCLabel = QLabel("<b>%s</b>" % self.tr("Characters"))
self.wCLabel = QLabel("<b>%s</b>" % self.tr("Words"))
self.pCLabel = QLabel("<b>%s</b>" % self.tr("Paragraphs"))
self.cCValue = QLabel("")
self.wCValue = QLabel("")
self.pCValue = QLabel("")
self.cCValue.setMinimumWidth(wCount)
self.wCValue.setMinimumWidth(wCount)
self.pCValue.setMinimumWidth(wCount)
self.cCValue.setAlignment(Qt.AlignRight)
self.wCValue.setAlignment(Qt.AlignRight)
self.pCValue.setAlignment(Qt.AlignRight)
# Synopsis
self.synopLabel = QLabel("<b>%s</b>" % self.tr("Synopsis"))
self.synopValue = QLabel("")
self.synopLWrap = QHBoxLayout()
self.synopValue.setWordWrap(True)
self.synopValue.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self.synopLWrap.addWidget(self.synopValue, 1)
# Tags
self.povKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]))
self.focKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]))
self.chrKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY]))
self.pltKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]))
self.timKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.TIME_KEY]))
self.wldKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY]))
self.objKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY]))
self.entKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY]))
self.cstKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY]))
self.povKeyLWrap = QHBoxLayout()
self.focKeyLWrap = QHBoxLayout()
self.chrKeyLWrap = QHBoxLayout()
self.pltKeyLWrap = QHBoxLayout()
self.timKeyLWrap = QHBoxLayout()
self.wldKeyLWrap = QHBoxLayout()
self.objKeyLWrap = QHBoxLayout()
self.entKeyLWrap = QHBoxLayout()
self.cstKeyLWrap = QHBoxLayout()
self.povKeyValue = QLabel("")
self.focKeyValue = QLabel("")
self.chrKeyValue = QLabel("")
self.pltKeyValue = QLabel("")
self.timKeyValue = QLabel("")
self.wldKeyValue = QLabel("")
self.objKeyValue = QLabel("")
self.entKeyValue = QLabel("")
self.cstKeyValue = QLabel("")
self.povKeyValue.setWordWrap(True)
self.focKeyValue.setWordWrap(True)
self.chrKeyValue.setWordWrap(True)
self.pltKeyValue.setWordWrap(True)
self.timKeyValue.setWordWrap(True)
self.wldKeyValue.setWordWrap(True)
self.objKeyValue.setWordWrap(True)
self.entKeyValue.setWordWrap(True)
self.cstKeyValue.setWordWrap(True)
self.povKeyValue.linkActivated.connect(self._tagClicked)
self.focKeyValue.linkActivated.connect(self._tagClicked)
self.chrKeyValue.linkActivated.connect(self._tagClicked)
self.pltKeyValue.linkActivated.connect(self._tagClicked)
self.timKeyValue.linkActivated.connect(self._tagClicked)
self.wldKeyValue.linkActivated.connect(self._tagClicked)
self.objKeyValue.linkActivated.connect(self._tagClicked)
self.entKeyValue.linkActivated.connect(self._tagClicked)
self.cstKeyValue.linkActivated.connect(self._tagClicked)
self.povKeyLWrap.addWidget(self.povKeyValue, 1)
self.focKeyLWrap.addWidget(self.focKeyValue, 1)
self.chrKeyLWrap.addWidget(self.chrKeyValue, 1)
self.pltKeyLWrap.addWidget(self.pltKeyValue, 1)
self.timKeyLWrap.addWidget(self.timKeyValue, 1)
self.wldKeyLWrap.addWidget(self.wldKeyValue, 1)
self.objKeyLWrap.addWidget(self.objKeyValue, 1)
self.entKeyLWrap.addWidget(self.entKeyValue, 1)
self.cstKeyLWrap.addWidget(self.cstKeyValue, 1)
# Selected Item Details
self.mainGroup = QGroupBox(self.tr("Title Details"), self)
self.mainForm = QGridLayout()
self.mainGroup.setLayout(self.mainForm)
self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.fileLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.fileValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.itemLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.itemValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addLayout(self.synopLWrap, 4, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.setColumnStretch(1, 1)
self.mainForm.setRowStretch(4, 1)
self.mainForm.setHorizontalSpacing(hSpace)
self.mainForm.setVerticalSpacing(vSpace)
# Selected Item Tags
self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self)
self.tagsForm = QGridLayout()
self.tagsGroup.setLayout(self.tagsForm)
self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.focKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.focKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.chrKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.chrKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.pltKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.pltKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.timKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.timKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.wldKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.wldKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.objKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.objKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.entKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.entKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.cstKeyLabel, 8, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.cstKeyLWrap, 8, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.setColumnStretch(1, 1)
self.tagsForm.setRowStretch(8, 1)
self.tagsForm.setHorizontalSpacing(hSpace)
self.tagsForm.setVerticalSpacing(vSpace)
# Assemble
self.outerWidget = QWidget()
self.outerBox = QHBoxLayout()
self.outerBox.addWidget(self.mainGroup, 0)
self.outerBox.addWidget(self.tagsGroup, 1)
self.outerWidget.setLayout(self.outerBox)
self.setWidget(self.outerWidget)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setWidgetResizable(True)
self.setFrameStyle(QFrame.NoFrame)
self.initDetails()
logger.debug("GuiOutlineDetails initialisation complete")
return
def initDetails(self):
"""Set or update outline settings.
"""
# Scroll bars
if self.mainConf.hideVScroll:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
if self.mainConf.hideHScroll:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
return
def clearDetails(self):
"""Clear all the data labels.
"""
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText("")
self.fileValue.setText("")
self.itemValue.setText("")
self.cCValue.setText("")
self.wCValue.setText("")
self.pCValue.setText("")
self.synopValue.setText("")
self.povKeyValue.setText("")
self.focKeyValue.setText("")
self.chrKeyValue.setText("")
self.pltKeyValue.setText("")
self.timKeyValue.setText("")
self.wldKeyValue.setText("")
self.objKeyValue.setText("")
self.entKeyValue.setText("")
self.cstKeyValue.setText("")
return
def showItem(self, tHandle, sTitle):
"""Update the content of the tree with the given handle and line
number pointing to a header.
"""
pIndex = self.theProject.index
nwItem = self.theProject.tree[tHandle]
novIdx = pIndex.getNovelData(tHandle, sTitle)
theRefs = pIndex.getReferences(tHandle, sTitle)
if nwItem is None or novIdx is None:
return False
if novIdx.level in self.LVL_MAP:
self.titleLabel.setText("<b>%s</b>" % self.tr(self.LVL_MAP[novIdx.level]))
else:
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText(novIdx.title)
itemStatus, _ = nwItem.getImportStatus()
self.fileValue.setText(nwItem.itemName)
self.itemValue.setText(itemStatus)
cC = checkInt(novIdx.charCount, 0)
wC = checkInt(novIdx.wordCount, 0)
pC = checkInt(novIdx.paraCount, 0)
self.cCValue.setText(f"{cC:n}")
self.wCValue.setText(f"{wC:n}")
self.pCValue.setText(f"{pC:n}")
self.synopValue.setText(novIdx.synopsis)
self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY))
self.focKeyValue.setText(self._formatTags(theRefs, nwKeyWords.FOCUS_KEY))
self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY))
self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY))
self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY))
self.wldKeyValue.setText(self._formatTags(theRefs, nwKeyWords.WORLD_KEY))
self.objKeyValue.setText(self._formatTags(theRefs, nwKeyWords.OBJECT_KEY))
self.entKeyValue.setText(self._formatTags(theRefs, nwKeyWords.ENTITY_KEY))
self.cstKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CUSTOM_KEY))
return True
##
# Slots
##
def _tagClicked(self, theLink):
"""Capture the click of a tag in the right-most column.
"""
logger.verbose("Clicked link: '%s'", theLink)
if len(theLink) > 0:
theBits = theLink.split("=")
if len(theBits) == 2:
self.viewChangeRequested.emit(nwView.PROJECT)
self.theParent.docViewer.loadFromTag(theBits[1])
return
##
# Internal Functions
##
def _formatTags(self, theRefs, theKey):
"""Format the tags as clickable links.
"""
if theKey not in theRefs:
return ""
refTags = []
for tTag in theRefs[theKey]:
refTags.append("<a href='#%s=%s'>%s</a>" % (
theKey[1:], tTag, tTag
))
return ", ".join(refTags)
# END Class GuiOutlineDetails
+78 -60
View File
@@ -32,11 +32,14 @@ from time import time
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame,
QDialog
) )
from novelwriter.core import NWDoc from novelwriter.core import NWDoc
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.common import minmax
from novelwriter.dialogs.itemeditor import GuiItemEditor
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -48,8 +51,9 @@ class GuiProjectTree(QTreeWidget):
C_EXPORT = 2 C_EXPORT = 2
C_STATUS = 3 C_STATUS = 3
novelItemChanged = pyqtSignal() treeItemChanged = pyqtSignal(str)
noteItemChanged = pyqtSignal() novelItemChanged = pyqtSignal(str)
rootFolderChanged = pyqtSignal(str)
wordCountsChanged = pyqtSignal() wordCountsChanged = pyqtSignal()
def __init__(self, theParent): def __init__(self, theParent):
@@ -63,10 +67,9 @@ class GuiProjectTree(QTreeWidget):
self.theProject = theParent.theProject self.theProject = theParent.theProject
# Internal Variables # Internal Variables
self._treeMap = {} self._treeMap = {}
self._treeChanged = False self._lastMove = {}
self._timeChanged = 0 self._timeChanged = 0
self._lastMove = {}
## ##
# Build GUI # Build GUI
@@ -156,15 +159,15 @@ class GuiProjectTree(QTreeWidget):
""" """
self.clear() self.clear()
self._treeMap = {} self._treeMap = {}
self._treeChanged = False self._lastMove = {}
self._timeChanged = 0 self._timeChanged = 0
return return
def newTreeItem(self, itemType, itemClass=None): def newTreeItem(self, itemType, itemClass=None):
"""Add new item to the tree, with a given itemType (and """Add new item to the tree, with a given itemType (and
itemClass if Root), and attach it to the selected handle. Also make itemClass if Root), and attach it to the selected handle. Also
sure the item is added in a place it can be added, and that other make sure the item is added in a place it can be added, and that
meta data is set correctly to ensure a valid project tree. other meta data is set correctly to ensure a valid project tree.
""" """
if not self.theParent.hasProject: if not self.theParent.hasProject:
logger.error("No project open") logger.error("No project open")
@@ -186,9 +189,11 @@ class GuiProjectTree(QTreeWidget):
), nwAlert.ERROR) ), nwAlert.ERROR)
return False return False
# If the selected item is a file, the new item will be a sibling # If the selected item is a file, the new item will be a
# sibling if the file has no children, otherwise a child
pItem = self.theProject.tree[sHandle] pItem = self.theProject.tree[sHandle]
if pItem.itemType == nwItemType.FILE: qItem = self._getTreeItem(sHandle)
if pItem.itemType == nwItemType.FILE and qItem.childCount() == 0:
nHandle = sHandle nHandle = sHandle
sHandle = pItem.itemParent sHandle = pItem.itemParent
if sHandle is None: if sHandle is None:
@@ -221,9 +226,9 @@ class GuiProjectTree(QTreeWidget):
# Add the new item to the tree # Add the new item to the tree
self.revealNewTreeItem(tHandle, nHandle) self.revealNewTreeItem(tHandle, nHandle)
self.theParent.editItem(tHandle) self.theParent.editItem(tHandle)
nwItem = self.theProject.tree[tHandle]
# If this is a folder, return here # Handle new file creation
nwItem = self.theProject.tree[tHandle]
if nwItem.itemType != nwItemType.FILE: if nwItem.itemType != nwItemType.FILE:
return True return True
@@ -231,7 +236,9 @@ class GuiProjectTree(QTreeWidget):
newDoc = NWDoc(self.theProject, tHandle) newDoc = NWDoc(self.theProject, tHandle)
if not newDoc.readDocument(): if not newDoc.readDocument():
if nwItem.itemLayout == nwItemLayout.DOCUMENT: if nwItem.itemLayout == nwItemLayout.DOCUMENT:
newText = f"### {nwItem.itemName}\n\n" iLvl = self.theProject.index.getHandleHeaderIntLevel(sHandle)
hLvl = "#"*minmax(iLvl + 1, 2, 4)
newText = f"{hLvl} {nwItem.itemName}\n\n"
else: else:
newText = f"# {nwItem.itemName}\n\n" newText = f"# {nwItem.itemName}\n\n"
@@ -266,7 +273,7 @@ class GuiProjectTree(QTreeWidget):
if pHandle is not None and pHandle in self._treeMap: if pHandle is not None and pHandle in self._treeMap:
self._treeMap[pHandle].setExpanded(True) self._treeMap[pHandle].setExpanded(True)
self._emitItemChange(tHandle) self._alertTreeChange(tHandle=tHandle, flush=True)
self.clearSelection() self.clearSelection()
trItem.setSelected(True) trItem.setSelected(True)
@@ -308,10 +315,31 @@ class GuiProjectTree(QTreeWidget):
pItem.insertChild(nIndex, cItem) pItem.insertChild(nIndex, cItem)
self._recordLastMove(cItem, pItem, tIndex) self._recordLastMove(cItem, pItem, tIndex)
self._alertTreeChange(tHandle=tHandle, flush=True)
self.clearSelection() self.clearSelection()
cItem.setSelected(True) cItem.setSelected(True)
self._setTreeChanged(True)
self._emitItemChange(tHandle) return True
def editTreeItem(self, tHandle=None):
"""Open the edit item dialog.
"""
if tHandle is None:
logger.warning("No item selected")
return False
tItem = self.theProject.tree[tHandle]
if tItem is None:
return False
if tItem.itemType == nwItemType.NO_TYPE:
return False
logger.verbose("Requesting change to item '%s'", tHandle)
dlgProj = GuiItemEditor(self, tHandle)
dlgProj.exec_()
if dlgProj.result() == QDialog.Accepted:
self.setTreeItemValues(tHandle)
self._alertTreeChange(tHandle=tHandle, flush=False)
return True return True
@@ -328,16 +356,6 @@ class GuiProjectTree(QTreeWidget):
self.theProject.setTreeOrder(theList) self.theProject.setTreeOrder(theList)
return True return True
def flushTreeOrder(self):
"""Calls saveTreeOrder if there are unsaved changes, otherwise
does nothing.
"""
if self._treeChanged:
logger.verbose("Flushing project tree to project class")
self.saveTreeOrder()
self._setTreeChanged(False)
return
def getTreeFromHandle(self, tHandle): def getTreeFromHandle(self, tHandle):
"""Recursively return all the children items starting from a """Recursively return all the children items starting from a
given item handle. given item handle.
@@ -409,7 +427,7 @@ class GuiProjectTree(QTreeWidget):
self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True) self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True)
if nTrash > 0: if nTrash > 0:
self._setTreeChanged(True) self._alertTreeChange(tHandle=trashHandle, flush=True)
return True return True
@@ -443,6 +461,7 @@ class GuiProjectTree(QTreeWidget):
return False return False
wCount = self._getItemWordCount(tHandle) wCount = self._getItemWordCount(tHandle)
autoFlush = not bulkAction
if nwItemS.itemType == nwItemType.ROOT: if nwItemS.itemType == nwItemType.ROOT:
# Only an empty ROOT folder can be deleted # Only an empty ROOT folder can be deleted
logger.debug("User requested a root folder '%s' deleted", tHandle) logger.debug("User requested a root folder '%s' deleted", tHandle)
@@ -450,7 +469,7 @@ class GuiProjectTree(QTreeWidget):
if trItemS.childCount() == 0: if trItemS.childCount() == 0:
self.takeTopLevelItem(tIndex) self.takeTopLevelItem(tIndex)
self._deleteTreeItem(tHandle) self._deleteTreeItem(tHandle)
self._setTreeChanged(True) self._alertTreeChange(tHandle=tHandle, flush=True)
else: else:
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
"Cannot delete root folder. It is not empty. " "Cannot delete root folder. It is not empty. "
@@ -466,7 +485,7 @@ class GuiProjectTree(QTreeWidget):
tIndex = trItemP.indexOfChild(trItemS) tIndex = trItemP.indexOfChild(trItemS)
trItemP.takeChild(tIndex) trItemP.takeChild(tIndex)
self._deleteTreeItem(tHandle) self._deleteTreeItem(tHandle)
self._setTreeChanged(True) self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
else: else:
# A populated FOLDER or a FILE requires confirmtation # A populated FOLDER or a FILE requires confirmtation
@@ -502,7 +521,7 @@ class GuiProjectTree(QTreeWidget):
self.theParent.closeDocument() self.theParent.closeDocument()
self._deleteTreeItem(dHandle) self._deleteTreeItem(dHandle)
self._setTreeChanged(True) self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
self.wordCountsChanged.emit() self.wordCountsChanged.emit()
else: else:
@@ -521,7 +540,7 @@ class GuiProjectTree(QTreeWidget):
trItemT.addChild(trItemC) trItemT.addChild(trItemC)
self._postItemMove(tHandle, wCount) self._postItemMove(tHandle, wCount)
self._recordLastMove(trItemS, trItemP, tIndex) self._recordLastMove(trItemS, trItemP, tIndex)
self._setTreeChanged(True) self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
return True return True
@@ -542,7 +561,7 @@ class GuiProjectTree(QTreeWidget):
else: else:
expIcon = self.theTheme.getIcon("cross") expIcon = self.theTheme.getIcon("cross")
itempStatus, statusIcon = nwItem.getImportStatus() itemStatus, statusIcon = nwItem.getImportStatus()
hLevel = self.theProject.index.getHandleHeaderLevel(tHandle) hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
itemIcon = self.theTheme.getItemIcon( itemIcon = self.theTheme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
@@ -552,7 +571,7 @@ class GuiProjectTree(QTreeWidget):
trItem.setText(self.C_NAME, nwItem.itemName) trItem.setText(self.C_NAME, nwItem.itemName)
trItem.setIcon(self.C_EXPORT, expIcon) trItem.setIcon(self.C_EXPORT, expIcon)
trItem.setIcon(self.C_STATUS, statusIcon) trItem.setIcon(self.C_STATUS, statusIcon)
trItem.setToolTip(self.C_STATUS, itempStatus) trItem.setToolTip(self.C_STATUS, itemStatus)
if self.mainConf.emphLabels and nwItem.itemLayout == nwItemLayout.DOCUMENT: if self.mainConf.emphLabels and nwItem.itemLayout == nwItemLayout.DOCUMENT:
trFont = trItem.font(self.C_NAME) trFont = trItem.font(self.C_NAME)
@@ -657,6 +676,7 @@ class GuiProjectTree(QTreeWidget):
dstItem.insertChild(dstIndex, movItem) dstItem.insertChild(dstIndex, movItem)
self._postItemMove(sHandle, wCount) self._postItemMove(sHandle, wCount)
self._alertTreeChange(tHandle=sHandle, flush=True)
self.clearSelection() self.clearSelection()
movItem.setSelected(True) movItem.setSelected(True)
@@ -785,6 +805,7 @@ class GuiProjectTree(QTreeWidget):
QTreeWidget.dropEvent(self, theEvent) QTreeWidget.dropEvent(self, theEvent)
self._postItemMove(sHandle, wCount) self._postItemMove(sHandle, wCount)
self._recordLastMove(sItem, pItem, pIndex) self._recordLastMove(sItem, pItem, pIndex)
self._alertTreeChange(tHandle=sHandle, flush=True)
sItem.setExpanded(isExpanded) sItem.setExpanded(isExpanded)
return return
@@ -826,8 +847,6 @@ class GuiProjectTree(QTreeWidget):
# Trigger dependent updates # Trigger dependent updates
self.propagateCount(tHandle, wCount) self.propagateCount(tHandle, wCount)
self._setTreeChanged(True)
self._emitItemChange(tHandle)
return True return True
@@ -920,7 +939,7 @@ class GuiProjectTree(QTreeWidget):
except Exception: except Exception:
logger.error("Failed to get index of item with handle '%s'", nHandle) logger.error("Failed to get index of item with handle '%s'", nHandle)
if byIndex >= 0: if byIndex >= 0:
self._treeMap[pHandle].insertChild(byIndex+1, newItem) self._treeMap[pHandle].insertChild(byIndex + 1, newItem)
else: else:
self._treeMap[pHandle].addChild(newItem) self._treeMap[pHandle].addChild(newItem)
self.propagateCount(tHandle, nwItem.wordCount, countChildren=True) self.propagateCount(tHandle, nwItem.wordCount, countChildren=True)
@@ -928,8 +947,6 @@ class GuiProjectTree(QTreeWidget):
self.setTreeItemValues(tHandle) self.setTreeItemValues(tHandle)
newItem.setExpanded(nwItem.isExpanded) newItem.setExpanded(nwItem.isExpanded)
self._setTreeChanged(True)
return newItem return newItem
def _addTrashRoot(self): def _addTrashRoot(self):
@@ -942,33 +959,34 @@ class GuiProjectTree(QTreeWidget):
trItem = self._getTreeItem(trashHandle) trItem = self._getTreeItem(trashHandle)
if trItem is None: if trItem is None:
trItem = self._addTreeItem( trItem = self._addTreeItem(self.theProject.tree[trashHandle])
self.theProject.tree[trashHandle]
)
if trItem is not None: if trItem is not None:
trItem.setExpanded(True) trItem.setExpanded(True)
self._setTreeChanged(True) self._alertTreeChange(tHandle=trashHandle, flush=True)
return trItem return trItem
def _setTreeChanged(self, theState): def _alertTreeChange(self, tHandle=None, flush=True):
"""Set the tree change flag, and propagate to the project. """Update information on tree change state, and emit necessary
signals.
""" """
self._treeChanged = theState self._timeChanged = time()
if theState: self.theProject.setProjectChanged(True)
self._timeChanged = time() if flush:
self.theProject.setProjectChanged(True) self.saveTreeOrder()
return
tItem = self.theProject.tree[tHandle]
if tItem is None:
return
itemType = tItem.itemType
if itemType == nwItemType.ROOT:
self.rootFolderChanged.emit(tHandle)
elif itemType == nwItemType.FILE and tItem.isNovelLike():
self.novelItemChanged.emit(tHandle)
self.treeItemChanged.emit(tHandle)
def _emitItemChange(self, tHandle):
"""Emit an item change signal for a given handle.
"""
if self.theProject.tree.checkType(tHandle, nwItemType.FILE):
nwItem = self.theProject.tree[tHandle]
if nwItem.isNovelLike():
self.novelItemChanged.emit()
else:
self.noteItemChanged.emit()
return return
def _recordLastMove(self, srcItem, parItem, parIndex): def _recordLastMove(self, srcItem, parItem, parIndex):
+2 -2
View File
@@ -466,8 +466,8 @@ class GuiIcons:
# General Button Icons # General Button Icons
"add", "backward", "check", "clear", "close", "cross", "delete", "done", "down", "edit", "add", "backward", "check", "clear", "close", "cross", "delete", "done", "down", "edit",
"forward", "hash", "maximise", "minimise", "reference", "refresh", "remove", "save", "forward", "hash", "maximise", "menu", "minimise", "reference", "refresh", "remove",
"search_replace", "search", "settings", "up", "save", "search_replace", "search", "settings", "up",
# Switches # Switches
"sticky-on", "sticky-off", "sticky-on", "sticky-off",
+64 -68
View File
@@ -27,6 +27,7 @@ import os
import logging import logging
import novelwriter import novelwriter
from enum import Enum
from time import time from time import time
from datetime import datetime from datetime import datetime
@@ -39,20 +40,19 @@ from PyQt5.QtWidgets import (
from novelwriter.gui import ( from novelwriter.gui import (
GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu, GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu,
GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails, GuiProjectTree, GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectTree, GuiTheme,
GuiTheme, GuiViewsBar GuiViewsBar
) )
from novelwriter.dialogs import ( from novelwriter.dialogs import (
GuiAbout, GuiDocMerge, GuiDocSplit, GuiItemEditor, GuiPreferences, GuiAbout, GuiDocMerge, GuiDocSplit, GuiPreferences, GuiProjectDetails,
GuiProjectDetails, GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiWordList
GuiWordList
) )
from novelwriter.tools import ( from novelwriter.tools import (
GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats
) )
from novelwriter.core import NWProject from novelwriter.core import NWProject
from novelwriter.enum import ( from novelwriter.enum import (
nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView
) )
from novelwriter.common import getGuiItem, hexToInt from novelwriter.common import getGuiItem, hexToInt
@@ -112,23 +112,30 @@ class GuiMain(QMainWindow):
self.docViewer = GuiDocViewer(self) self.docViewer = GuiDocViewer(self)
self.treeMeta = GuiItemDetails(self) self.treeMeta = GuiItemDetails(self)
self.projView = GuiOutline(self) self.projView = GuiOutline(self)
self.projMeta = GuiOutlineDetails(self)
self.mainMenu = GuiMainMenu(self) self.mainMenu = GuiMainMenu(self)
self.viewsBar = GuiViewsBar(self) self.viewsBar = GuiViewsBar(self)
# Connect Signals Between Main Elements # Connect Signals Between Main Elements
self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) self.viewsBar.viewChangeRequested.connect(self._changeView)
self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus)
self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts)
self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts)
self.treeView.itemSelectionChanged.connect(self._treeSingleClick) self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) self.treeView.novelItemChanged.connect(self._treeNovelItemChanged)
self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) self.treeView.wordCountsChanged.connect(self._updateStatusWordCount)
self.treeView.treeItemChanged.connect(self.docEditor.updateDocInfo)
self.treeView.treeItemChanged.connect(self.docViewer.updateDocInfo)
self.treeView.treeItemChanged.connect(self.treeMeta.updateViewBox)
self.treeView.rootFolderChanged.connect(self.projView.updateRootItem)
self.viewsBar.viewChangeRequested.connect(self._changeView) self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage)
self.projMeta.viewChangeRequested.connect(self._changeView) self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus)
self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts)
self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts)
self.docEditor.loadDocumentTagRequest.connect(self._followTag)
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
self.projView.loadDocumentTagRequest.connect(self._followTag)
# Project Tree Stack # Project Tree Stack
self.projStack = QStackedWidget() self.projStack = QStackedWidget()
@@ -158,12 +165,6 @@ class GuiMain(QMainWindow):
self.splitDocs.addWidget(self.splitView) self.splitDocs.addWidget(self.splitView)
self.splitDocs.setHandleWidth(hWd) self.splitDocs.setHandleWidth(hWd)
# Splitter : Project Outlie / Outline Details
self.splitOutline = QSplitter(Qt.Vertical)
self.splitOutline.addWidget(self.projView)
self.splitOutline.addWidget(self.projMeta)
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
# Splitter : Project Tree / Main Tabs # Splitter : Project Tree / Main Tabs
self.splitMain = QSplitter(Qt.Horizontal) self.splitMain = QSplitter(Qt.Horizontal)
self.splitMain.setContentsMargins(0, 0, 0, 0) self.splitMain.setContentsMargins(0, 0, 0, 0)
@@ -175,7 +176,7 @@ class GuiMain(QMainWindow):
# Main Stack : Editor / Outline # Main Stack : Editor / Outline
self.mainStack = QStackedWidget() self.mainStack = QStackedWidget()
self.mainStack.addWidget(self.splitMain) self.mainStack.addWidget(self.splitMain)
self.mainStack.addWidget(self.splitOutline) self.mainStack.addWidget(self.projView)
self.mainStack.currentChanged.connect(self._mainStackChanged) self.mainStack.currentChanged.connect(self._mainStackChanged)
# Indices of Splitter Widgets # Indices of Splitter Widgets
@@ -188,7 +189,7 @@ class GuiMain(QMainWindow):
# Indices of Tab Widgets # Indices of Tab Widgets
self.idxEditorView = self.mainStack.indexOf(self.splitMain) self.idxEditorView = self.mainStack.indexOf(self.splitMain)
self.idxOutlineView = self.mainStack.indexOf(self.splitOutline) self.idxOutlineView = self.mainStack.indexOf(self.projView)
self.idxTreeView = self.projStack.indexOf(self.treeView) self.idxTreeView = self.projStack.indexOf(self.treeView)
self.idxNovelView = self.projStack.indexOf(self.novelView) self.idxNovelView = self.projStack.indexOf(self.novelView)
@@ -296,7 +297,7 @@ class GuiMain(QMainWindow):
self.docEditor.clearEditor() self.docEditor.clearEditor()
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.closeDocViewer() self.closeDocViewer()
self.projMeta.clearDetails() self.projView.clearOutline()
# General # General
self.statusBar.clearStatus() self.statusBar.clearStatus()
@@ -358,6 +359,7 @@ class GuiMain(QMainWindow):
self.rebuildTrees() self.rebuildTrees()
self.saveProject() self.saveProject()
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.projView.updateRootItem(None)
self.rebuildIndex(beQuiet=True) self.rebuildIndex(beQuiet=True)
self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.setProjectStatus(nwState.GOOD) self.statusBar.setProjectStatus(nwState.GOOD)
@@ -503,8 +505,8 @@ class GuiMain(QMainWindow):
self.rebuildTrees() self.rebuildTrees()
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.docEditor.toggleSpellCheck(self.theProject.spellCheck) self.docEditor.toggleSpellCheck(self.theProject.spellCheck)
self.mainMenu.setAutoOutline(self.theProject.autoOutline)
self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setRefTime(self.theProject.projOpened)
self.projView.updateRootItem(None)
self._updateStatusWordCount() self._updateStatusWordCount()
# Restore previously open documents, if any # Restore previously open documents, if any
@@ -596,7 +598,6 @@ class GuiMain(QMainWindow):
logger.error("No project open") logger.error("No project open")
return False return False
self.treeView.flushTreeOrder()
nHandle = None # The next handle after tHandle nHandle = None # The next handle after tHandle
fHandle = None # The first file handle we encounter fHandle = None # The first file handle we encounter
foundIt = False # We've found tHandle, pick the next we see foundIt = False # We've found tHandle, pick the next we see
@@ -790,7 +791,7 @@ class GuiMain(QMainWindow):
tHandle = self.treeView.getSelectedHandle() tHandle = self.treeView.getSelectedHandle()
elif self.novelView.hasFocus(): elif self.novelView.hasFocus():
tHandle, tLine = self.novelView.getSelectedHandle() tHandle, tLine = self.novelView.getSelectedHandle()
elif self.projView.hasFocus(): elif self.projView.treeFocus():
tHandle, tLine = self.projView.getSelectedHandle() tHandle, tLine = self.projView.getSelectedHandle()
else: else:
logger.warning("No item selected") logger.warning("No item selected")
@@ -813,27 +814,10 @@ class GuiMain(QMainWindow):
tHandle = self.docEditor.docHandle() tHandle = self.docEditor.docHandle()
else: else:
tHandle = self.treeView.getSelectedHandle() tHandle = self.treeView.getSelectedHandle()
if tHandle:
return self.treeView.editTreeItem(tHandle)
if tHandle is None: return False
logger.warning("No item selected")
return False
tItem = self.theProject.tree[tHandle]
if tItem is None:
return False
if tItem.itemType == nwItemType.NO_TYPE:
return False
logger.verbose("Requesting change to item '%s'", tHandle)
dlgProj = GuiItemEditor(self, tHandle)
dlgProj.exec_()
if dlgProj.result() == QDialog.Accepted:
self.treeView.setTreeItemValues(tHandle)
self.treeMeta.updateViewBox(tHandle)
self.docEditor.updateDocInfo(tHandle)
self.docViewer.updateDocInfo(tHandle)
return True
def rebuildTrees(self): def rebuildTrees(self):
"""Rebuild the project tree. """Rebuild the project tree.
@@ -889,19 +873,6 @@ class GuiMain(QMainWindow):
return True return True
def rebuildOutline(self):
"""Force a rebuild of the Outline view.
"""
if not self.hasProject:
logger.error("No project open")
return False
logger.verbose("Forcing a rebuild of the Project Outline")
self._changeView(nwView.OUTLINE)
self.projView.refreshTree(overRide=True)
return True
## ##
# Main Dialogs # Main Dialogs
## ##
@@ -949,7 +920,6 @@ class GuiMain(QMainWindow):
self.treeView.initTree() self.treeView.initTree()
self.novelView.initTree() self.novelView.initTree()
self.projView.initOutline() self.projView.initOutline()
self.projMeta.initDetails()
self._updateStatusWordCount() self._updateStatusWordCount()
return return
@@ -980,8 +950,6 @@ class GuiMain(QMainWindow):
logger.error("No project open") logger.error("No project open")
return False return False
self.treeView.flushTreeOrder()
dlgDetails = getGuiItem("GuiProjectDetails") dlgDetails = getGuiItem("GuiProjectDetails")
if dlgDetails is None: if dlgDetails is None:
dlgDetails = GuiProjectDetails(self) dlgDetails = GuiProjectDetails(self)
@@ -1186,7 +1154,7 @@ class GuiMain(QMainWindow):
if not self.isFocusMode: if not self.isFocusMode:
self.mainConf.setMainPanePos(self.splitMain.sizes()) self.mainConf.setMainPanePos(self.splitMain.sizes())
self.mainConf.setDocPanePos(self.splitDocs.sizes()) self.mainConf.setDocPanePos(self.splitDocs.sizes())
self.mainConf.setOutlinePanePos(self.splitOutline.sizes()) self.mainConf.setOutlinePanePos(self.projView.splitSizes())
if self.viewMeta.isVisible(): if self.viewMeta.isVisible():
self.mainConf.setViewPanePos(self.splitView.sizes()) self.mainConf.setViewPanePos(self.splitView.sizes())
@@ -1223,7 +1191,7 @@ class GuiMain(QMainWindow):
self.docViewer.setFocus() self.docViewer.setFocus()
elif paneNo == nwWidget.OUTLINE: elif paneNo == nwWidget.OUTLINE:
self._changeView(nwView.OUTLINE) self._changeView(nwView.OUTLINE)
self.projView.setFocus() self.projView.setTreeFocus()
return return
def closeDocEditor(self): def closeDocEditor(self):
@@ -1453,6 +1421,23 @@ class GuiMain(QMainWindow):
return projData return projData
def _getTagSource(self, tTag):
"""A wrapper function for the index lookup of a tag that will
display an alert if the tag cannot be found.
"""
tHandle, sTitle = self.theProject.index.getTagSource(tTag)
if tHandle is None:
self.makeAlert(self.tr(
"Could not find the reference for tag '{0}'. It either doesn't "
"exist, or the index is out of date. The index can be updated "
"from the Tools menu, or by pressing {1}."
).format(
tTag, "F9"
), nwAlert.ERROR)
return None, None
return tHandle, sTitle
## ##
# Events # Events
## ##
@@ -1468,9 +1453,21 @@ class GuiMain(QMainWindow):
return return
## ##
# Slots # Private Slots
## ##
@pyqtSlot(str, Enum)
def _followTag(self, tTag, tMode):
"""Follow a tag after user interaction with a link.
"""
tHandle, sTitle = self._getTagSource(tTag)
if tHandle is not None:
if tMode == nwDocMode.EDIT:
self.openDocument(tHandle)
elif tMode == nwDocMode.VIEW:
self.viewDocument(tHandle=tHandle, tAnchor=f"#{sTitle}")
return
@pyqtSlot(nwView) @pyqtSlot(nwView)
def _changeView(self, view): def _changeView(self, view):
"""Handle the requested change of view from the GuiViewBar. """Handle the requested change of view from the GuiViewBar.
@@ -1488,7 +1485,7 @@ class GuiMain(QMainWindow):
self.projStack.setCurrentWidget(self.novelView) self.projStack.setCurrentWidget(self.novelView)
elif view == nwView.OUTLINE: elif view == nwView.OUTLINE:
self.mainStack.setCurrentWidget(self.splitOutline) self.mainStack.setCurrentWidget(self.projView)
return return
@@ -1568,8 +1565,7 @@ class GuiMain(QMainWindow):
if self.mainStack.currentIndex() == self.idxOutlineView: if self.mainStack.currentIndex() == self.idxOutlineView:
logger.verbose("Novel tree changed while Outline tab active") logger.verbose("Novel tree changed while Outline tab active")
if self.hasProject: if self.hasProject:
self.treeView.flushTreeOrder() self.projView.refreshView(novelChanged=True)
self.projView.refreshTree(novelChanged=True)
return return
@@ -1602,7 +1598,7 @@ class GuiMain(QMainWindow):
elif tabIndex == self.idxOutlineView: elif tabIndex == self.idxOutlineView:
logger.verbose("Project outline tab activated") logger.verbose("Project outline tab activated")
if self.hasProject: if self.hasProject:
self.projView.refreshTree() self.projView.refreshView()
return return
-1
View File
@@ -709,7 +709,6 @@ class GuiBuildNovel(QDialog):
bldObj.initDocument() bldObj.initDocument()
# Make sure the project and document is up to date # Make sure the project and document is up to date
self.theParent.treeView.flushTreeOrder()
self.theParent.saveDocument() self.theParent.saveDocument()
self.buildProgress.setMaximum(len(self.theProject.tree)) self.buildProgress.setMaximum(len(self.theProject.tree))
+17
View File
@@ -0,0 +1,17 @@
%%~name: Chapter One
%%~path: e5e47ebf63b1c/a520879ca0b45
%%~kind: NOVEL/DOCUMENT
### Chapter One
@pov: Jane
% Synopsis: Remember Jane and John?
### Scene One
@pov: Jane
@focus: John
A project can have multiple novel root folders for multiple novels. This is the first scene of a sequel to the first novel.
In this way, the writer can keep the same notes for multiple novels. This can be especially useful if the writer is planning a multi-novel story in advance.
+8
View File
@@ -0,0 +1,8 @@
%%~name: Title Page
%%~path: e5e47ebf63b1c/bacb7059e3083
%%~kind: NOVEL/DOCUMENT
#! Sequel Novel
>> **By Jane Doh** <<
% Synopsis: Jane and John are back in a sequel to My Novel!
+27 -15
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-05-17 18:36:56"> <novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-06-05 15:03:00">
<project> <project>
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
<author>Jane Smith</author> <author>Jane Smith</author>
<author>Jay Doh</author> <author>Jay Doh</author>
<saveCount>1329</saveCount> <saveCount>1334</saveCount>
<autoCount>220</autoCount> <autoCount>225</autoCount>
<editTime>67104</editTime> <editTime>67746</editTime>
</project> </project>
<settings> <settings>
<doBackup>False</doBackup> <doBackup>False</doBackup>
@@ -15,10 +15,10 @@
<spellCheck>True</spellCheck> <spellCheck>True</spellCheck>
<spellLang>None</spellLang> <spellLang>None</spellLang>
<autoOutline>True</autoOutline> <autoOutline>True</autoOutline>
<lastEdited>636b6aa9b697b</lastEdited> <lastEdited>a520879ca0b45</lastEdited>
<lastViewed>636b6aa9b697b</lastViewed> <lastViewed>636b6aa9b697b</lastViewed>
<lastWordCount>1303</lastWordCount> <lastWordCount>1363</lastWordCount>
<novelWordCount>894</novelWordCount> <novelWordCount>954</novelWordCount>
<notesWordCount>409</notesWordCount> <notesWordCount>409</notesWordCount>
<autoReplace> <autoReplace>
<entry key="A">B</entry> <entry key="A">B</entry>
@@ -33,10 +33,10 @@
<section></section> <section></section>
</titleFormat> </titleFormat>
<status> <status>
<entry key="sf12341" count="5" red="100" green="100" blue="100">New</entry> <entry key="sf12341" count="7" red="100" green="100" blue="100">New</entry>
<entry key="sf24ce6" count="1" red="200" green="50" blue="0">Notes</entry> <entry key="sf24ce6" count="1" red="200" green="50" blue="0">Notes</entry>
<entry key="sc24b8f" count="2" red="182" green="60" blue="0">Started</entry> <entry key="sc24b8f" count="2" red="182" green="60" blue="0">Started</entry>
<entry key="s90e6c9" count="5" red="193" green="129" blue="0">1st Draft</entry> <entry key="s90e6c9" count="6" red="193" green="129" blue="0">1st Draft</entry>
<entry key="sd51c5b" count="1" red="193" green="129" blue="0">2nd Draft</entry> <entry key="sd51c5b" count="1" red="193" green="129" blue="0">2nd Draft</entry>
<entry key="s8ae72a" count="0" red="193" green="129" blue="0">3rd Draft</entry> <entry key="s8ae72a" count="0" red="193" green="129" blue="0">3rd Draft</entry>
<entry key="s78ea90" count="0" red="58" green="180" blue="58">Finished</entry> <entry key="s78ea90" count="0" red="58" green="180" blue="58">Finished</entry>
@@ -48,13 +48,13 @@
<entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry> <entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="24"> <content count="27">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL"> <item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="True"/> <meta expanded="True"/>
<name status="sc24b8f" import="ia857f0">Novel</name> <name status="sc24b8f" import="ia857f0">Novel</name>
</item> </item>
<item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="93" wordCount="19" paraCount="2" cursorPos="2"/> <meta expanded="False" charCount="93" wordCount="19" paraCount="2" cursorPos="119"/>
<name status="sc24b8f" import="ia857f0" exported="True">Title Page</name> <name status="sc24b8f" import="ia857f0" exported="True">Title Page</name>
</item> </item>
<item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -93,7 +93,19 @@
<meta expanded="False" charCount="189" wordCount="37" paraCount="1" cursorPos="224"/> <meta expanded="False" charCount="189" wordCount="37" paraCount="1" cursorPos="224"/>
<name status="s90e6c9" import="ia857f0" exported="True">We Found John!</name> <name status="s90e6c9" import="ia857f0" exported="True">We Found John!</name>
</item> </item>
<item handle="f6622b4617424" parent="None" root="f6622b4617424" order="1" type="ROOT" class="CHARACTER"> <item handle="e5e47ebf63b1c" parent="None" root="e5e47ebf63b1c" order="1" type="ROOT" class="NOVEL">
<meta expanded="True"/>
<name status="sf12341" import="ia857f0">Sequel</name>
</item>
<item handle="bacb7059e3083" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="27" wordCount="5" paraCount="1" cursorPos="100"/>
<name status="sf12341" import="ia857f0" exported="True">Title Page</name>
</item>
<item handle="a520879ca0b45" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="299" wordCount="55" paraCount="2" cursorPos="104"/>
<name status="s90e6c9" import="ia857f0" exported="True">Chapter One</name>
</item>
<item handle="f6622b4617424" parent="None" root="f6622b4617424" order="2" type="ROOT" class="CHARACTER">
<meta expanded="True"/> <meta expanded="True"/>
<name status="sf12341" import="ia857f0">Characters</name> <name status="sf12341" import="ia857f0">Characters</name>
</item> </item>
@@ -109,7 +121,7 @@
<meta expanded="False" charCount="55" wordCount="9" paraCount="1" cursorPos="25"/> <meta expanded="False" charCount="55" wordCount="9" paraCount="1" cursorPos="25"/>
<name status="sf12341" import="i2d7a54" exported="True">Jane Smith</name> <name status="sf12341" import="i2d7a54" exported="True">Jane Smith</name>
</item> </item>
<item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="2" type="ROOT" class="WORLD"> <item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD">
<meta expanded="True"/> <meta expanded="True"/>
<name status="sf12341" import="ia857f0">Locations</name> <name status="sf12341" import="ia857f0">Locations</name>
</item> </item>
@@ -125,7 +137,7 @@
<meta expanded="False" charCount="28" wordCount="6" paraCount="1" cursorPos="45"/> <meta expanded="False" charCount="28" wordCount="6" paraCount="1" cursorPos="45"/>
<name status="sf12341" import="i2d7a54" exported="True">Mars</name> <name status="sf12341" import="i2d7a54" exported="True">Mars</name>
</item> </item>
<item handle="6827118336ac1" parent="None" root="6827118336ac1" order="3" type="ROOT" class="ARCHIVE"> <item handle="6827118336ac1" parent="None" root="6827118336ac1" order="4" type="ROOT" class="ARCHIVE">
<meta expanded="True"/> <meta expanded="True"/>
<name status="sf12341" import="ia857f0">Archive</name> <name status="sf12341" import="ia857f0">Archive</name>
</item> </item>
@@ -137,7 +149,7 @@
<meta expanded="False" charCount="232" wordCount="42" paraCount="1" cursorPos="239"/> <meta expanded="False" charCount="232" wordCount="42" paraCount="1" cursorPos="239"/>
<name status="s90e6c9" import="ia857f0" exported="True">Old File</name> <name status="s90e6c9" import="ia857f0" exported="True">Old File</name>
</item> </item>
<item handle="98acd8c76c93a" parent="None" root="98acd8c76c93a" order="4" type="ROOT" class="TRASH"> <item handle="98acd8c76c93a" parent="None" root="98acd8c76c93a" order="5" type="ROOT" class="TRASH">
<meta expanded="True"/> <meta expanded="True"/>
<name status="sf12341" import="ia857f0">Trash</name> <name status="sf12341" import="ia857f0">Trash</name>
</item> </item>
+5 -4
View File
@@ -197,8 +197,7 @@ def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd):
nItem = theProject.tree[nHandle] nItem = theProject.tree[nHandle]
cItem = theProject.tree[cHandle] cItem = theProject.tree[cHandle]
assert theIndex.novelChangedSince(0) is False assert theIndex.rootChangedSince("0000000000010", 0) is False
assert theIndex.notesChangedSince(0) is False
assert theIndex.indexChangedSince(0) is False assert theIndex.indexChangedSince(0) is False
assert theIndex.scanText(cHandle, ( assert theIndex.scanText(cHandle, (
@@ -228,12 +227,14 @@ def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd):
"@time": [] "@time": []
} }
assert theIndex.novelChangedSince(0) is True assert theIndex.rootChangedSince("0000000000010", 0) is True
assert theIndex.notesChangedSince(0) is True
assert theIndex.indexChangedSince(0) is True assert theIndex.indexChangedSince(0) is True
assert theIndex.getHandleHeaderLevel(cHandle) == "H1" assert theIndex.getHandleHeaderLevel(cHandle) == "H1"
assert theIndex.getHandleHeaderLevel(nHandle) == "H1" assert theIndex.getHandleHeaderLevel(nHandle) == "H1"
assert theIndex.getHandleHeaderIntLevel(cHandle) == 1
assert theIndex.getHandleHeaderIntLevel(nHandle) == 1
assert theIndex.getHandleHeaderIntLevel("stuff") == 0
# Zero Items # Zero Items
assert theIndex.checkThese([], cItem) == [] assert theIndex.checkThese([], cItem) == []
-4
View File
@@ -25,7 +25,6 @@ import pytest
from tools import readFile from tools import readFile
from novelwriter.core import NWProject, ToHtml from novelwriter.core import NWProject, ToHtml
from novelwriter.core.index import NWIndex
@pytest.mark.core @pytest.mark.core
@@ -33,7 +32,6 @@ def testCoreToHtml_ConvertFormat(mockGUI):
"""Test the tokenizer and converter chain using the ToHtml class. """Test the tokenizer and converter chain using the ToHtml class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theHtml = ToHtml(theProject) theHtml = ToHtml(theProject)
# Novel Files Headers # Novel Files Headers
@@ -236,7 +234,6 @@ def testCoreToHtml_ConvertDirect(mockGUI):
"""Test the converter directly using the ToHtml class. """Test the converter directly using the ToHtml class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theHtml = ToHtml(theProject) theHtml = ToHtml(theProject)
theHtml._isNovel = True theHtml._isNovel = True
@@ -607,7 +604,6 @@ def testCoreToHtml_Format(mockGUI):
"""Test all the formatters for the ToHtml class. """Test all the formatters for the ToHtml class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theHtml = ToHtml(theProject) theHtml = ToHtml(theProject)
# Export Mode # Export Mode
-4
View File
@@ -25,7 +25,6 @@ import pytest
from tools import readFile from tools import readFile
from novelwriter.core import NWProject, ToMarkdown from novelwriter.core import NWProject, ToMarkdown
from novelwriter.core.index import NWIndex
@pytest.mark.core @pytest.mark.core
@@ -33,7 +32,6 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
"""Test the tokenizer and converter chain using the ToMarkdown class. """Test the tokenizer and converter chain using the ToMarkdown class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theMD = ToMarkdown(theProject) theMD = ToMarkdown(theProject)
# Headers # Headers
@@ -162,7 +160,6 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
"""Test the converter directly using the ToMarkdown class. """Test the converter directly using the ToMarkdown class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theMD = ToMarkdown(theProject) theMD = ToMarkdown(theProject)
theMD._isNovel = True theMD._isNovel = True
@@ -267,7 +264,6 @@ def testCoreToMarkdown_Format(mockGUI):
"""Test all the formatters for the ToMarkdown class. """Test all the formatters for the ToMarkdown class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theMD = ToMarkdown(theProject) theMD = ToMarkdown(theProject)
assert theMD._formatKeywords("", theMD.A_NONE) == "" assert theMD._formatKeywords("", theMD.A_NONE) == ""
-8
View File
@@ -29,7 +29,6 @@ from shutil import copyfile
from tools import cmpFiles from tools import cmpFiles
from novelwriter.core import NWProject, ToOdt from novelwriter.core import NWProject, ToOdt
from novelwriter.core.index import NWIndex
from novelwriter.core.toodt import ODTParagraphStyle, ODTTextStyle, XMLParagraph, _mkTag from novelwriter.core.toodt import ODTParagraphStyle, ODTTextStyle, XMLParagraph, _mkTag
XML_NS = [ XML_NS = [
@@ -56,7 +55,6 @@ def testCoreToOdt_Init(mockGUI):
"""Test initialisation of the ODT document. """Test initialisation of the ODT document.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
# Flat Doc # Flat Doc
# ======== # ========
@@ -112,7 +110,6 @@ def testCoreToOdt_TextFormatting(mockGUI):
"""Test formatting of paragraphs. """Test formatting of paragraphs.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True) theDoc = ToOdt(theProject, isFlat=True)
theDoc.initDocument() theDoc.initDocument()
@@ -234,7 +231,6 @@ def testCoreToOdt_Convert(mockGUI):
"""Test the converter of the ToOdt class. """Test the converter of the ToOdt class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True) theDoc = ToOdt(theProject, isFlat=True)
theDoc._isNovel = True theDoc._isNovel = True
@@ -566,7 +562,6 @@ def testCoreToOdt_ConvertDirect(mockGUI):
otherwise hard to reach conditions. otherwise hard to reach conditions.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True) theDoc = ToOdt(theProject, isFlat=True)
theDoc._isNovel = True theDoc._isNovel = True
@@ -621,7 +616,6 @@ def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir):
"""Test the document save functions. """Test the document save functions.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True) theDoc = ToOdt(theProject, isFlat=True)
theDoc._isNovel = True theDoc._isNovel = True
@@ -658,7 +652,6 @@ def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir):
"""Test the document save functions. """Test the document save functions.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=False) theDoc = ToOdt(theProject, isFlat=False)
theDoc._isNovel = True theDoc._isNovel = True
@@ -738,7 +731,6 @@ def testCoreToOdt_Format(mockGUI):
"""Test the formatters for the ToOdt class. """Test the formatters for the ToOdt class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockGUI.theIndex = NWIndex(theProject)
theDoc = ToOdt(theProject, isFlat=True) theDoc = ToOdt(theProject, isFlat=True)
assert theDoc._formatSynopsis("synopsis text") == ( assert theDoc._formatSynopsis("synopsis text") == (
+5
View File
@@ -149,6 +149,11 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert theTree.isTrash("a000000000003") is True assert theTree.isTrash("a000000000003") is True
assert theTree.isRoot("a000000000002") is True assert theTree.isRoot("a000000000002") is True
# Check that we have the root classes
assert theTree.rootClasses() == {
nwItemClass.NOVEL, nwItemClass.CHARACTER, nwItemClass.ARCHIVE, nwItemClass.TRASH
}
# Check the isTrash function # Check the isTrash function
assert theTree.isTrash("0000000000000") is True # Doesn't exist assert theTree.isTrash("0000000000000") is True # Doesn't exist
assert theTree.isTrash("a000000000003") is True # This the trash folder assert theTree.isTrash("a000000000003") is True # This the trash folder
+3 -4
View File
@@ -61,7 +61,6 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI):
assert nwGUI.editItem() is False assert nwGUI.editItem() is False
assert nwGUI.requestNovelTreeRefresh() is False assert nwGUI.requestNovelTreeRefresh() is False
assert nwGUI.rebuildIndex() is False assert nwGUI.rebuildIndex() is False
assert nwGUI.rebuildOutline() is False
assert nwGUI.showProjectSettingsDialog() is False assert nwGUI.showProjectSettingsDialog() is False
assert nwGUI.showProjectDetailsDialog() is False assert nwGUI.showProjectDetailsDialog() is False
assert nwGUI.showBuildProjectDialog() is False assert nwGUI.showBuildProjectDialog() is False
@@ -149,12 +148,12 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
# Project Outline has focus # Project Outline has focus
nwGUI.switchFocus(nwWidget.OUTLINE) nwGUI.switchFocus(nwWidget.OUTLINE)
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(GuiOutline, "hasFocus", lambda *a: True) mp.setattr(GuiOutline, "treeFocus", lambda *a: True)
assert nwGUI.docEditor.docHandle() is None assert nwGUI.docEditor.docHandle() is None
actItem = nwGUI.projView.topLevelItem(0) actItem = nwGUI.projView.outlineView.topLevelItem(0)
chpItem = actItem.child(0) chpItem = actItem.child(0)
selItem = chpItem.child(0) selItem = chpItem.child(0)
nwGUI.projView.setCurrentItem(selItem) nwGUI.projView.outlineView.setCurrentItem(selItem)
nwGUI._keyPressReturn() nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.docEditor.docHandle() == sHandle
assert nwGUI.closeDocument() is True assert nwGUI.closeDocument() is True
+206 -46
View File
@@ -19,20 +19,144 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import time
import pytest import pytest
from PyQt5.QtCore import Qt, QPoint from tools import buildTestProject, writeFile
from PyQt5.QtWidgets import QAction, QTreeWidgetItem, QMessageBox
from novelwriter.enum import nwOutline from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QMessageBox, QAction
keyDelay = 2 from novelwriter.enum import nwItemClass, nwOutline, nwView
typeDelay = 1
stepDelay = 20
@pytest.mark.gui @pytest.mark.gui
def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum): def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir):
"""Test the outline view.
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
# Create a project
prjDir = os.path.join(fncDir, "project")
buildTestProject(nwGUI, prjDir)
nwGUI.rebuildIndex()
nwGUI._changeView(nwView.OUTLINE)
outlineMain = nwGUI.projView
outlineView = outlineMain.outlineView
outlineData = outlineMain.outlineData
outlineMenu = outlineMain.outlineBar.mColumns
# Toggle scrollbars
nwGUI.mainConf.hideVScroll = True
nwGUI.mainConf.hideHScroll = True
nwGUI.projView.initOutline()
assert outlineView.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
assert outlineView.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
assert outlineData.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
nwGUI.mainConf.hideVScroll = False
nwGUI.mainConf.hideHScroll = False
nwGUI.projView.initOutline()
assert outlineView.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded
assert outlineView.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded
assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded
assert outlineData.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded
# Check focus
with monkeypatch.context() as mp:
mp.setattr(QWidget, "hasFocus", lambda *a: True)
assert outlineMain.treeFocus() is True
outlineMain.setTreeFocus() # Can't check. just ensures that it doesn't error
# Option State
# ============
pOptions = nwGUI.theProject.options
colNames = [h.name for h in nwOutline]
colItems = [h for h in nwOutline]
colWidth = {h: outlineView.DEF_WIDTH[h] for h in nwOutline}
colHidden = {h: outlineView.DEF_HIDDEN[h] for h in nwOutline}
assert outlineView.topLevelItemCount() > 0
# Save header state not allowed
outlineView._lastBuild = 0
outlineView._saveHeaderState()
assert pOptions.getValue("GuiOutline", "headerOrder", []) == []
# Allow saving header state
outlineView._lastBuild = time.time()
outlineView._saveHeaderState()
assert pOptions.getValue("GuiOutline", "headerOrder", []) == colNames
assert outlineView._treeOrder == colItems
assert outlineView._colWidth == colWidth
assert outlineView._colHidden == colHidden
# Get default values
optItems = pOptions.getValue("GuiOutline", "headerOrder", [])
optWidth = pOptions.getValue("GuiOutline", "columnWidth", {})
optHidden = pOptions.getValue("GuiOutline", "columnHidden", {})
# Add invalid column name
pOptions.setValue("GuiOutline", "headerOrder", optItems + ["blabla"])
outlineView._loadHeaderState()
assert outlineView._treeOrder == colItems
assert outlineView._colHidden == colHidden
# Add duplicate column name
pOptions.setValue("GuiOutline", "headerOrder", optItems + [optItems[-1]])
outlineView._loadHeaderState()
assert outlineView._treeOrder == colItems
assert outlineView._colHidden == colHidden
# Invalid column width data
pOptions.setValue("GuiOutline", "headerOrder", optItems)
pOptions.setValue("GuiOutline", "columnWidth", {"blabla": None})
outlineView._loadHeaderState()
assert outlineView._treeOrder == colItems
assert outlineView._colHidden == colHidden
# Invalid column width data
pOptions.setValue("GuiOutline", "headerOrder", optItems)
pOptions.setValue("GuiOutline", "columnWidth", optWidth)
pOptions.setValue("GuiOutline", "columnHidden", {"bloabla": None})
outlineView._loadHeaderState()
assert outlineView._treeOrder == colItems
assert outlineView._colHidden == colHidden
# Valid settings
pOptions.setValue("GuiOutline", "headerOrder", optItems)
pOptions.setValue("GuiOutline", "columnWidth", optWidth)
pOptions.setValue("GuiOutline", "columnHidden", optHidden)
outlineView._loadHeaderState()
assert outlineView._treeOrder == colItems
assert outlineView._colHidden == colHidden
# Header Menu
# ===========
# Trigger the menu entry for all hidden columns
for hItem in nwOutline:
if outlineView.DEF_HIDDEN[hItem]:
outlineMenu.actionMap[hItem].activate(QAction.Trigger)
# Now no columns should be hidden
outlineView._saveHeaderState()
assert not any(pOptions.getValue("GuiOutline", "columnHidden", None).values())
# qtbot.stop()
# END Test testGuiOutline_Main
@pytest.mark.gui
def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum):
"""Test the outline view. """Test the outline view.
""" """
# Block message box # Block message box
@@ -43,73 +167,109 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
nwGUI.mainConf.lastPath = nwLipsum nwGUI.mainConf.lastPath = nwLipsum
nwGUI.rebuildIndex() nwGUI.rebuildIndex()
nwGUI.mainStack.setCurrentIndex(nwGUI.idxOutlineView) nwGUI._changeView(nwView.OUTLINE)
assert nwGUI.projView.topLevelItemCount() > 0 outlineMain = nwGUI.projView
outlineBar = outlineMain.outlineBar
outlineView = outlineMain.outlineView
outlineData = outlineMain.outlineData
# Context Menu lipHandle = "b3643d0f92e32"
nwGUI.projView._headerRightClick(QPoint(1, 1))
nwGUI.projView.headerMenu.actionMap[nwOutline.CCOUNT].activate(QAction.Trigger)
nwGUI.projView.headerMenu.close()
qtbot.mouseClick(nwGUI.projView, Qt.LeftButton)
nwGUI.projView._loadHeaderState() # Check defaults in dropdown list
assert not nwGUI.projView._colHidden[nwOutline.CCOUNT] assert outlineBar.novelValue.itemData(0) == lipHandle
assert outlineBar.novelValue.itemData(1) is None # Separator
assert outlineBar.novelValue.itemData(2) == "" # All novels
# Add a second novel folder
newHandle = nwGUI.theProject.newRoot(nwItemClass.NOVEL)
nwGUI.treeView.revealNewTreeItem(newHandle)
# Check new values in dropdown list
assert outlineBar.novelValue.itemData(0) == lipHandle
assert outlineBar.novelValue.itemData(1) == newHandle
assert outlineBar.novelValue.itemData(2) is None # Separator
assert outlineBar.novelValue.itemData(3) == "" # All novels
# Add a bunch of files in a header order that hits all tree combos
docList = [
("Section 1", 4), ("Scene 1", 3), ("Chapter 1", 2), ("Part 1", 1),
("Section 2", 4), ("Scene 2", 3), ("Chapter 2", 2),
("Section 3", 4), ("Scene 3", 3),
("Section 4", 4),
]
for dTitle, hLevel in docList:
aHandle = nwGUI.theProject.newFile(dTitle, newHandle)
hHash = "#"*hLevel
writeFile(os.path.join(nwLipsum, "content", f"{aHandle}.nwd"), f"{hHash} {dTitle}\n\n")
nwGUI.treeView.revealNewTreeItem(aHandle)
nwGUI.rebuildIndex()
# Build the second novel
outlineBar.novelValue.setCurrentIndex(1)
outlineBar._refreshRequested()
# Go back to Lipsum
outlineBar.novelValue.setCurrentIndex(0)
outlineBar._refreshRequested()
# Check Details
# =============
# First Item # First Item
nwGUI.rebuildOutline() outlineView.refreshTree()
selItem = nwGUI.projView.topLevelItem(0) selItem = outlineView.topLevelItem(0)
assert isinstance(selItem, QTreeWidgetItem)
nwGUI.projView.setCurrentItem(selItem) outlineView.setCurrentItem(selItem)
assert nwGUI.projMeta.titleLabel.text() == "<b>Title</b>" assert outlineData.titleLabel.text() == "<b>Title</b>"
assert nwGUI.projMeta.titleValue.text() == "Lorem Ipsum" assert outlineData.titleValue.text() == "Lorem Ipsum"
assert nwGUI.projMeta.fileValue.text() == "Lorem Ipsum" assert outlineData.fileValue.text() == "Lorem Ipsum"
assert nwGUI.projMeta.itemValue.text() == "Finished" assert outlineData.itemValue.text() == "Finished"
assert nwGUI.projMeta.cCValue.text() == "230" assert outlineData.cCValue.text() == "230"
assert nwGUI.projMeta.wCValue.text() == "40" assert outlineData.wCValue.text() == "40"
assert nwGUI.projMeta.pCValue.text() == "3" assert outlineData.pCValue.text() == "3"
# Scene One # Scene One
actItem = nwGUI.projView.topLevelItem(1) actItem = outlineView.topLevelItem(1)
chpItem = actItem.child(0) chpItem = actItem.child(0)
selItem = chpItem.child(0) selItem = chpItem.child(0)
nwGUI.projView.setCurrentItem(selItem) outlineView.setCurrentItem(selItem)
tHandle, tLine = nwGUI.projView.getSelectedHandle() tHandle, tLine = outlineView.getSelectedHandle()
assert tHandle == "88243afbe5ed8" assert tHandle == "88243afbe5ed8"
assert tLine == 0 assert tLine == 0
assert nwGUI.projMeta.titleLabel.text() == "<b>Scene</b>" assert outlineData.titleLabel.text() == "<b>Scene</b>"
assert nwGUI.projMeta.titleValue.text() == "Scene One" assert outlineData.titleValue.text() == "Scene One"
assert nwGUI.projMeta.fileValue.text() == "Scene One" assert outlineData.fileValue.text() == "Scene One"
assert nwGUI.projMeta.itemValue.text() == "Finished" assert outlineData.itemValue.text() == "Finished"
# Click POV Link # Click POV Link
assert nwGUI.projMeta.povKeyValue.text() == "<a href='#pov=Bod'>Bod</a>" assert outlineData.povKeyValue.text() == "<a href='Bod'>Bod</a>"
nwGUI.projMeta._tagClicked("#pov=Bod") nwGUI.projView._tagClicked("Bod")
assert nwGUI.docViewer.docHandle() == "4c4f28287af27" assert nwGUI.docViewer.docHandle() == "4c4f28287af27"
# Scene One, Section Two # Scene One, Section Two
actItem = nwGUI.projView.topLevelItem(1) actItem = outlineView.topLevelItem(1)
chpItem = actItem.child(0) chpItem = actItem.child(0)
scnItem = chpItem.child(0) scnItem = chpItem.child(0)
selItem = scnItem.child(0) selItem = scnItem.child(0)
nwGUI.projView.setCurrentItem(selItem) outlineView.setCurrentItem(selItem)
tHandle, tLine = nwGUI.projView.getSelectedHandle() tHandle, tLine = outlineView.getSelectedHandle()
assert tHandle == "88243afbe5ed8" assert tHandle == "88243afbe5ed8"
assert tLine == 12 assert tLine == 12
assert nwGUI.projMeta.titleLabel.text() == "<b>Section</b>" assert outlineData.titleLabel.text() == "<b>Section</b>"
assert nwGUI.projMeta.titleValue.text() == "Scene One, Section Two" assert outlineData.titleValue.text() == "Scene One, Section Two"
assert nwGUI.projMeta.fileValue.text() == "Scene One" assert outlineData.fileValue.text() == "Scene One"
assert nwGUI.projMeta.itemValue.text() == "Finished" assert outlineData.itemValue.text() == "Finished"
nwGUI.projView._treeDoubleClick(selItem, 0) outlineView._treeDoubleClick(selItem, 0)
assert nwGUI.docEditor.docHandle() == "88243afbe5ed8" assert nwGUI.docEditor.docHandle() == "88243afbe5ed8"
# qtbot.stopForInteraction() # qtbot.stop()
# END Test testGuiOutline_Main # END Test testGuiOutline_Content
+1 -6
View File
@@ -96,7 +96,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd)
assert nwGUI.theProject.tree["0000000000013"].itemRoot == "0000000000008" assert nwGUI.theProject.tree["0000000000013"].itemRoot == "0000000000008"
assert nwGUI.theProject.tree["0000000000013"].itemClass == nwItemClass.NOVEL assert nwGUI.theProject.tree["0000000000013"].itemClass == nwItemClass.NOVEL
assert nwGUI.openDocument("0000000000013") assert nwGUI.openDocument("0000000000013")
assert nwGUI.docEditor.getText() == "### New Document\n\n" assert nwGUI.docEditor.getText() == "## New Document\n\n"
# Add a new file to the characters folder # Add a new file to the characters folder
nwTree.setSelectedHandle("000000000000a") nwTree.setSelectedHandle("000000000000a")
@@ -246,17 +246,14 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
# Move novel folder up # Move novel folder up
assert nwTree.moveTreeItem(-1) is False assert nwTree.moveTreeItem(-1) is False
nwTree.flushTreeOrder()
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
# Move novel folder down # Move novel folder down
assert nwTree.moveTreeItem(1) is True assert nwTree.moveTreeItem(1) is True
nwTree.flushTreeOrder()
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 1 assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 1
# Move novel folder up again # Move novel folder up again
assert nwTree.moveTreeItem(-1) is True assert nwTree.moveTreeItem(-1) is True
nwTree.flushTreeOrder()
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0 assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
# Clean up # Clean up
@@ -432,10 +429,8 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR
assert nwTree.emptyTrash() is False assert nwTree.emptyTrash() is False
# Empty the trash proper # Empty the trash proper
nwTree._setTreeChanged(False)
assert nwTree.emptyTrash() is True assert nwTree.emptyTrash() is True
assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
assert nwTree._treeChanged is True
# Try to delete a file, but block the underlying deletion of the file on disk # Try to delete a file, but block the underlying deletion of the file on disk
assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd")) assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd"))