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:
@@ -59,7 +59,6 @@ The main shorcuts are as follows:
|
||||
":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo."
|
||||
":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes."
|
||||
":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:`Down`", "Move item one step down in the project tree."
|
||||
":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:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer."
|
||||
":kbd:`F9`", "Re-build the project index."
|
||||
":kbd:`F10`", "Re-build the project outline."
|
||||
":kbd:`F11`", "Activate full screen mode."
|
||||
":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."
|
||||
|
||||
@@ -47,6 +47,7 @@ edit = typ_pencil.svg
|
||||
forward = typ_chevron-right.svg
|
||||
hash = typ_hash.svg
|
||||
maximise = typ_arrow-maximise.svg
|
||||
menu = typ_th-menu.svg
|
||||
minimise = typ_arrow-minimise.svg
|
||||
proj_chapter = mixed_document-chapter.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
|
||||
hash = typ_hash.svg
|
||||
maximise = typ_arrow-maximise.svg
|
||||
menu = typ_th-menu.svg
|
||||
minimise = typ_arrow-minimise.svg
|
||||
proj_chapter = mixed_document-chapter.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
@@ -73,9 +73,8 @@ class NWIndex:
|
||||
self._indexBroken = False
|
||||
|
||||
# TimeStamps
|
||||
self._timeNovel = 0
|
||||
self._timeNotes = 0
|
||||
self._timeIndex = 0
|
||||
self._indexChange = 0
|
||||
self._rootChange = {}
|
||||
|
||||
return
|
||||
|
||||
@@ -99,9 +98,8 @@ class NWIndex:
|
||||
"""
|
||||
self._tagsIndex.clear()
|
||||
self._itemIndex.clear()
|
||||
self._timeNovel = 0
|
||||
self._timeNotes = 0
|
||||
self._timeIndex = 0
|
||||
self._indexChange = 0
|
||||
self._rootChange = {}
|
||||
return
|
||||
|
||||
def deleteHandle(self, tHandle):
|
||||
@@ -129,20 +127,16 @@ class NWIndex:
|
||||
|
||||
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):
|
||||
"""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
|
||||
@@ -184,10 +178,7 @@ class NWIndex:
|
||||
logger.warning("Item '%s' is not in the index", fHandle)
|
||||
self.reIndexHandle(fHandle)
|
||||
|
||||
nowTime = round(time())
|
||||
self._timeNovel = nowTime
|
||||
self._timeNotes = nowTime
|
||||
self._timeIndex = nowTime
|
||||
self._indexChange = round(time())
|
||||
|
||||
logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000)
|
||||
|
||||
@@ -307,11 +298,8 @@ class NWIndex:
|
||||
|
||||
# Update timestamps for index changes
|
||||
nowTime = round(time())
|
||||
self._timeIndex = nowTime
|
||||
if theItem.itemLayout == nwItemLayout.NOTE:
|
||||
self._timeNotes = nowTime
|
||||
else:
|
||||
self._timeNovel = nowTime
|
||||
self._indexChange = nowTime
|
||||
self._rootChange[theItem.itemRoot] = nowTime
|
||||
|
||||
return True
|
||||
|
||||
@@ -466,14 +454,14 @@ class NWIndex:
|
||||
# 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
|
||||
they appear in the tree view and in the respective document
|
||||
files, but skipping all note files.
|
||||
"""
|
||||
for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl):
|
||||
tKey = f"{tHandle}:{sTitle}"
|
||||
yield tKey, tHandle, sTitle, hItem
|
||||
novStruct = self._itemIndex.iterNovelStructure(rootHandle=rootHandle, skipExcl=skipExcl)
|
||||
for tHandle, sTitle, hItem in novStruct:
|
||||
yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem
|
||||
return
|
||||
|
||||
def getNovelWordCount(self, skipExcl=True):
|
||||
@@ -514,6 +502,11 @@ class NWIndex:
|
||||
"""
|
||||
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):
|
||||
"""Generate a table of contents up to a maximum depth.
|
||||
"""
|
||||
|
||||
@@ -265,6 +265,22 @@ class NWTree():
|
||||
# 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):
|
||||
"""Check if a handle is a root item.
|
||||
"""
|
||||
|
||||
@@ -62,6 +62,14 @@ class nwItemLayout(Enum):
|
||||
# END Enum nwItemLayout
|
||||
|
||||
|
||||
class nwDocMode(Enum):
|
||||
|
||||
VIEW = 0
|
||||
EDIT = 1
|
||||
|
||||
# END Enum nwDocMode
|
||||
|
||||
|
||||
class nwDocAction(Enum):
|
||||
|
||||
NO_ACTION = 0
|
||||
|
||||
@@ -25,7 +25,6 @@ from novelwriter.gui.itemdetails import GuiItemDetails
|
||||
from novelwriter.gui.mainmenu import GuiMainMenu
|
||||
from novelwriter.gui.noveltree import GuiNovelTree
|
||||
from novelwriter.gui.outline import GuiOutline
|
||||
from novelwriter.gui.outlinedetails import GuiOutlineDetails
|
||||
from novelwriter.gui.projtree import GuiProjectTree
|
||||
from novelwriter.gui.statusbar import GuiMainStatus
|
||||
from novelwriter.gui.theme import GuiTheme
|
||||
@@ -40,7 +39,6 @@ __all__ = [
|
||||
"GuiMainStatus",
|
||||
"GuiNovelTree",
|
||||
"GuiOutline",
|
||||
"GuiOutlineDetails",
|
||||
"GuiProjectTree",
|
||||
"GuiTheme",
|
||||
"GuiViewsBar",
|
||||
|
||||
@@ -33,6 +33,7 @@ import bisect
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from enum import Enum
|
||||
from time import time
|
||||
|
||||
from PyQt5.QtCore import (
|
||||
@@ -50,7 +51,7 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
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.constants import nwConst, nwKeyWords, nwUnicode
|
||||
from novelwriter.gui.dochighlight import GuiDocHighlighter
|
||||
@@ -69,6 +70,7 @@ class GuiDocEditor(QTextEdit):
|
||||
spellDictionaryChanged = pyqtSignal(str, str)
|
||||
docEditedStatusChanged = pyqtSignal(bool)
|
||||
docCountsChanged = pyqtSignal(str, int, int, int)
|
||||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||||
|
||||
def __init__(self, theParent):
|
||||
QTextEdit.__init__(self, theParent)
|
||||
@@ -567,16 +569,6 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
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
|
||||
##
|
||||
@@ -1066,7 +1058,22 @@ class GuiDocEditor(QTextEdit):
|
||||
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)
|
||||
@@ -1894,7 +1901,7 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
if loadTag:
|
||||
logger.verbose("Attempting to follow tag '%s'", theWord)
|
||||
self.theParent.docViewer.loadFromTag(theWord)
|
||||
self.loadDocumentTagRequest.emit(theWord, nwDocMode.VIEW)
|
||||
else:
|
||||
logger.verbose("Potential tag '%s'", theWord)
|
||||
|
||||
|
||||
@@ -30,7 +30,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
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 (
|
||||
QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor
|
||||
)
|
||||
@@ -40,7 +42,7 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
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.constants import nwUnicode
|
||||
|
||||
@@ -49,6 +51,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiDocViewer(QTextBrowser):
|
||||
|
||||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||||
|
||||
def __init__(self, theParent):
|
||||
QTextBrowser.__init__(self, theParent)
|
||||
|
||||
@@ -239,30 +243,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.updateDocMargins()
|
||||
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):
|
||||
"""Wrapper function for various document actions on the current
|
||||
document.
|
||||
@@ -342,15 +322,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
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
|
||||
##
|
||||
@@ -409,19 +380,33 @@ class GuiDocViewer(QTextBrowser):
|
||||
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")
|
||||
def _linkClicked(self, theURL):
|
||||
"""Slot for a link in the document being clicked.
|
||||
"""Process a clicked link internally in the document.
|
||||
"""
|
||||
theLink = theURL.url()
|
||||
logger.verbose("Clicked link: '%s'", theLink)
|
||||
if len(theLink) > 0:
|
||||
theBits = theLink.split("=")
|
||||
if len(theBits) == 2:
|
||||
self.loadFromTag(theBits[1])
|
||||
self.loadDocumentTagRequest.emit(theBits[1], nwDocMode.VIEW)
|
||||
return
|
||||
|
||||
@pyqtSlot("QPoint")
|
||||
|
||||
@@ -220,6 +220,11 @@ class GuiItemDetails(QWidget):
|
||||
"""
|
||||
self.updateViewBox(self._itemHandle)
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
##
|
||||
|
||||
@pyqtSlot(str)
|
||||
def updateViewBox(self, tHandle):
|
||||
"""Populate the details box from a given handle.
|
||||
"""
|
||||
@@ -290,10 +295,6 @@ class GuiItemDetails(QWidget):
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
@pyqtSlot(str, int, int, int)
|
||||
def doUpdateCounts(self, tHandle, cC, wC, pC):
|
||||
"""Update the counts if the handle is the same as the one we're
|
||||
|
||||
@@ -81,12 +81,6 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aSpellCheck.setChecked(theMode)
|
||||
return
|
||||
|
||||
def setAutoOutline(self, theMode):
|
||||
"""Forward auto outline check state to its action.
|
||||
"""
|
||||
self.aAutoOutline.setChecked(theMode)
|
||||
return
|
||||
|
||||
def setFocusMode(self, theMode):
|
||||
"""Forward focus mode check state to its action.
|
||||
"""
|
||||
@@ -105,12 +99,6 @@ class GuiMainMenu(QMenuBar):
|
||||
self.theParent.docEditor.toggleSpellCheck(None)
|
||||
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):
|
||||
"""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.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
|
||||
self.toolsMenu.addSeparator()
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ class GuiNovelTree(QTreeWidget):
|
||||
"""
|
||||
logger.verbose("Requesting refresh of the novel tree")
|
||||
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):
|
||||
logger.verbose("No changes have been made to the novel index")
|
||||
return
|
||||
@@ -146,7 +146,6 @@ class GuiNovelTree(QTreeWidget):
|
||||
if selItem:
|
||||
titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2]
|
||||
|
||||
self.theParent.treeView.flushTreeOrder()
|
||||
self._populateTree()
|
||||
|
||||
if titleKey is not None and titleKey in self._treeMap:
|
||||
|
||||
+604
-46
@@ -4,7 +4,11 @@ novelWriter – GUI Project Outline
|
||||
GUI class for the project outline view
|
||||
|
||||
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
|
||||
Copyright 2018–2022, Veronica Berglyd Olsen
|
||||
@@ -27,20 +31,244 @@ import logging
|
||||
import novelwriter
|
||||
|
||||
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 (
|
||||
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.constants import trConst, nwKeyWords, nwLabels
|
||||
|
||||
|
||||
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 = {
|
||||
nwOutline.TITLE: 200,
|
||||
@@ -82,16 +310,18 @@ class GuiOutline(QTreeWidget):
|
||||
nwOutline.SYNOP: False,
|
||||
}
|
||||
|
||||
def __init__(self, theParent):
|
||||
QTreeWidget.__init__(self, theParent)
|
||||
hiddenStateChanged = pyqtSignal()
|
||||
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.theParent = theParent
|
||||
self.theProject = theParent.theProject
|
||||
self.theTheme = theParent.theTheme
|
||||
self.headerMenu = GuiOutlineHeaderMenu(self)
|
||||
self.theParent = theOutline.theParent
|
||||
self.theProject = theOutline.theParent.theProject
|
||||
self.theTheme = theOutline.theParent.theTheme
|
||||
|
||||
self.setFrameStyle(QFrame.NoFrame)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
@@ -106,8 +336,6 @@ class GuiOutline(QTreeWidget):
|
||||
self.setIndentation(iPx)
|
||||
|
||||
self.treeHead = self.header()
|
||||
self.treeHead.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.treeHead.customContextMenuRequested.connect(self._headerRightClick)
|
||||
self.treeHead.sectionMoved.connect(self._columnMoved)
|
||||
|
||||
# Internals
|
||||
@@ -121,12 +349,25 @@ class GuiOutline(QTreeWidget):
|
||||
|
||||
self.initOutline()
|
||||
self.clearOutline()
|
||||
self.headerMenu.setHiddenState(self._colHidden)
|
||||
|
||||
logger.debug("GuiOutline initialisation complete")
|
||||
self.hiddenStateChanged.emit()
|
||||
|
||||
logger.debug("GuiOutlineView initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def hiddenColumns(self):
|
||||
return self._colHidden
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
|
||||
def initOutline(self):
|
||||
"""Set or update outline settings.
|
||||
"""
|
||||
@@ -166,7 +407,7 @@ class GuiOutline(QTreeWidget):
|
||||
|
||||
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
|
||||
what data to load, and if necessary, force a rebuild of the
|
||||
tree.
|
||||
@@ -174,17 +415,17 @@ class GuiOutline(QTreeWidget):
|
||||
# If it's the first time, we always build
|
||||
if self._firstView or self._firstView and overRide:
|
||||
self._loadHeaderState()
|
||||
self._populateTree()
|
||||
self._populateTree(rootHandle)
|
||||
self._firstView = False
|
||||
return
|
||||
|
||||
# If the novel index or novel tree has changed since the tree
|
||||
# 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
|
||||
if doBuild or overRide:
|
||||
logger.debug("Rebuilding Project Outline")
|
||||
self._populateTree()
|
||||
self._populateTree(rootHandle)
|
||||
|
||||
return
|
||||
|
||||
@@ -220,7 +461,7 @@ class GuiOutline(QTreeWidget):
|
||||
document editor.
|
||||
"""
|
||||
tHandle, tLine = self.getSelectedHandle()
|
||||
self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True)
|
||||
self.theParent.openDocument(tHandle, tLine=tLine - 1, doScroll=True)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -232,18 +473,10 @@ class GuiOutline(QTreeWidget):
|
||||
if selItems:
|
||||
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
|
||||
sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole)
|
||||
self.theParent.projMeta.showItem(tHandle, sTitle)
|
||||
self.theParent.treeView.setSelectedHandle(tHandle)
|
||||
self.activeItemChanged.emit(tHandle, sTitle)
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot("QPoint")
|
||||
def _headerRightClick(self, clickPos):
|
||||
"""Show the header column menu.
|
||||
"""
|
||||
self.headerMenu.exec_(self.mapToGlobal(clickPos))
|
||||
return
|
||||
|
||||
@pyqtSlot(int, int, int)
|
||||
def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx):
|
||||
"""Make sure the order array is up to date with the actual order
|
||||
@@ -253,9 +486,10 @@ class GuiOutline(QTreeWidget):
|
||||
self._saveHeaderState()
|
||||
return
|
||||
|
||||
def _menuColumnToggled(self, isChecked, theItem):
|
||||
@pyqtSlot(bool, Enum)
|
||||
def menuColumnToggled(self, isChecked, theItem):
|
||||
"""Receive the changes to column visibility forwarded by the
|
||||
header context menu.
|
||||
column selection menu.
|
||||
"""
|
||||
logger.verbose("User toggled Outline column '%s'", theItem.name)
|
||||
if theItem in self._colIdx:
|
||||
@@ -314,7 +548,7 @@ class GuiOutline(QTreeWidget):
|
||||
except Exception:
|
||||
logger.warning("Ignored unknown outline column '%s'", str(hName))
|
||||
|
||||
self.headerMenu.setHiddenState(self._colHidden)
|
||||
self.hiddenStateChanged.emit()
|
||||
|
||||
return
|
||||
|
||||
@@ -356,7 +590,7 @@ class GuiOutline(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def _populateTree(self):
|
||||
def _populateTree(self, rootHandle):
|
||||
"""Build the tree based on the project index, and the header
|
||||
based on the defined constants, default values and user selected
|
||||
width, order and hidden state. All columns are populated, even
|
||||
@@ -389,7 +623,8 @@ class GuiOutline(QTreeWidget):
|
||||
currChapter = 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)
|
||||
|
||||
@@ -479,15 +714,16 @@ class GuiOutline(QTreeWidget):
|
||||
|
||||
return newItem
|
||||
|
||||
# END Class GuiOutline
|
||||
# END Class GuiOutlineView
|
||||
|
||||
|
||||
class GuiOutlineHeaderMenu(QMenu):
|
||||
|
||||
def __init__(self, theParent):
|
||||
QMenu.__init__(self, theParent)
|
||||
columnToggled = pyqtSignal(bool, Enum)
|
||||
|
||||
def __init__(self, theOutline):
|
||||
QMenu.__init__(self, theOutline)
|
||||
|
||||
self.theParent = theParent
|
||||
self.acceptToggle = True
|
||||
|
||||
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].setCheckable(True)
|
||||
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])
|
||||
|
||||
@@ -522,16 +758,338 @@ class GuiOutlineHeaderMenu(QMenu):
|
||||
|
||||
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
|
||||
##
|
||||
|
||||
def _columnToggled(self, isChecked, theItem):
|
||||
"""The user has toggled the visibility of a column. Forward the
|
||||
event to the parent class only if we're accepting changes.
|
||||
@pyqtSlot(str, str)
|
||||
def showItem(self, tHandle, sTitle):
|
||||
"""Update the content of the tree with the given handle and line
|
||||
number pointing to a header.
|
||||
"""
|
||||
if self.acceptToggle:
|
||||
self.theParent._menuColumnToggled(isChecked, theItem)
|
||||
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
|
||||
|
||||
@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
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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 2018–2022, 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
@@ -32,11 +32,14 @@ from time import time
|
||||
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import (
|
||||
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame
|
||||
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame,
|
||||
QDialog
|
||||
)
|
||||
|
||||
from novelwriter.core import NWDoc
|
||||
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||
from novelwriter.common import minmax
|
||||
from novelwriter.dialogs.itemeditor import GuiItemEditor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -48,8 +51,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
C_EXPORT = 2
|
||||
C_STATUS = 3
|
||||
|
||||
novelItemChanged = pyqtSignal()
|
||||
noteItemChanged = pyqtSignal()
|
||||
treeItemChanged = pyqtSignal(str)
|
||||
novelItemChanged = pyqtSignal(str)
|
||||
rootFolderChanged = pyqtSignal(str)
|
||||
wordCountsChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, theParent):
|
||||
@@ -63,10 +67,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.theProject = theParent.theProject
|
||||
|
||||
# Internal Variables
|
||||
self._treeMap = {}
|
||||
self._treeChanged = False
|
||||
self._treeMap = {}
|
||||
self._lastMove = {}
|
||||
self._timeChanged = 0
|
||||
self._lastMove = {}
|
||||
|
||||
##
|
||||
# Build GUI
|
||||
@@ -156,15 +159,15 @@ class GuiProjectTree(QTreeWidget):
|
||||
"""
|
||||
self.clear()
|
||||
self._treeMap = {}
|
||||
self._treeChanged = False
|
||||
self._lastMove = {}
|
||||
self._timeChanged = 0
|
||||
return
|
||||
|
||||
def newTreeItem(self, itemType, itemClass=None):
|
||||
"""Add new item to the tree, with a given itemType (and
|
||||
itemClass if Root), and attach it to the selected handle. Also make
|
||||
sure the item is added in a place it can be added, and that other
|
||||
meta data is set correctly to ensure a valid project tree.
|
||||
itemClass if Root), and attach it to the selected handle. Also
|
||||
make sure the item is added in a place it can be added, and that
|
||||
other meta data is set correctly to ensure a valid project tree.
|
||||
"""
|
||||
if not self.theParent.hasProject:
|
||||
logger.error("No project open")
|
||||
@@ -186,9 +189,11 @@ class GuiProjectTree(QTreeWidget):
|
||||
), nwAlert.ERROR)
|
||||
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]
|
||||
if pItem.itemType == nwItemType.FILE:
|
||||
qItem = self._getTreeItem(sHandle)
|
||||
if pItem.itemType == nwItemType.FILE and qItem.childCount() == 0:
|
||||
nHandle = sHandle
|
||||
sHandle = pItem.itemParent
|
||||
if sHandle is None:
|
||||
@@ -221,9 +226,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
# Add the new item to the tree
|
||||
self.revealNewTreeItem(tHandle, nHandle)
|
||||
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:
|
||||
return True
|
||||
|
||||
@@ -231,7 +236,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
newDoc = NWDoc(self.theProject, tHandle)
|
||||
if not newDoc.readDocument():
|
||||
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:
|
||||
newText = f"# {nwItem.itemName}\n\n"
|
||||
|
||||
@@ -266,7 +273,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
if pHandle is not None and pHandle in self._treeMap:
|
||||
self._treeMap[pHandle].setExpanded(True)
|
||||
|
||||
self._emitItemChange(tHandle)
|
||||
self._alertTreeChange(tHandle=tHandle, flush=True)
|
||||
self.clearSelection()
|
||||
trItem.setSelected(True)
|
||||
|
||||
@@ -308,10 +315,31 @@ class GuiProjectTree(QTreeWidget):
|
||||
pItem.insertChild(nIndex, cItem)
|
||||
self._recordLastMove(cItem, pItem, tIndex)
|
||||
|
||||
self._alertTreeChange(tHandle=tHandle, flush=True)
|
||||
self.clearSelection()
|
||||
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
|
||||
|
||||
@@ -328,16 +356,6 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.theProject.setTreeOrder(theList)
|
||||
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):
|
||||
"""Recursively return all the children items starting from a
|
||||
given item handle.
|
||||
@@ -409,7 +427,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True)
|
||||
|
||||
if nTrash > 0:
|
||||
self._setTreeChanged(True)
|
||||
self._alertTreeChange(tHandle=trashHandle, flush=True)
|
||||
|
||||
return True
|
||||
|
||||
@@ -443,6 +461,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
return False
|
||||
|
||||
wCount = self._getItemWordCount(tHandle)
|
||||
autoFlush = not bulkAction
|
||||
if nwItemS.itemType == nwItemType.ROOT:
|
||||
# Only an empty ROOT folder can be deleted
|
||||
logger.debug("User requested a root folder '%s' deleted", tHandle)
|
||||
@@ -450,7 +469,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
if trItemS.childCount() == 0:
|
||||
self.takeTopLevelItem(tIndex)
|
||||
self._deleteTreeItem(tHandle)
|
||||
self._setTreeChanged(True)
|
||||
self._alertTreeChange(tHandle=tHandle, flush=True)
|
||||
else:
|
||||
self.theParent.makeAlert(self.tr(
|
||||
"Cannot delete root folder. It is not empty. "
|
||||
@@ -466,7 +485,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
trItemP.takeChild(tIndex)
|
||||
self._deleteTreeItem(tHandle)
|
||||
self._setTreeChanged(True)
|
||||
self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
|
||||
|
||||
else:
|
||||
# A populated FOLDER or a FILE requires confirmtation
|
||||
@@ -502,7 +521,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.theParent.closeDocument()
|
||||
self._deleteTreeItem(dHandle)
|
||||
|
||||
self._setTreeChanged(True)
|
||||
self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
|
||||
self.wordCountsChanged.emit()
|
||||
|
||||
else:
|
||||
@@ -521,7 +540,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
trItemT.addChild(trItemC)
|
||||
self._postItemMove(tHandle, wCount)
|
||||
self._recordLastMove(trItemS, trItemP, tIndex)
|
||||
self._setTreeChanged(True)
|
||||
self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
|
||||
|
||||
return True
|
||||
|
||||
@@ -542,7 +561,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
else:
|
||||
expIcon = self.theTheme.getIcon("cross")
|
||||
|
||||
itempStatus, statusIcon = nwItem.getImportStatus()
|
||||
itemStatus, statusIcon = nwItem.getImportStatus()
|
||||
hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
|
||||
itemIcon = self.theTheme.getItemIcon(
|
||||
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
|
||||
@@ -552,7 +571,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
trItem.setText(self.C_NAME, nwItem.itemName)
|
||||
trItem.setIcon(self.C_EXPORT, expIcon)
|
||||
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:
|
||||
trFont = trItem.font(self.C_NAME)
|
||||
@@ -657,6 +676,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
dstItem.insertChild(dstIndex, movItem)
|
||||
|
||||
self._postItemMove(sHandle, wCount)
|
||||
self._alertTreeChange(tHandle=sHandle, flush=True)
|
||||
|
||||
self.clearSelection()
|
||||
movItem.setSelected(True)
|
||||
@@ -785,6 +805,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
QTreeWidget.dropEvent(self, theEvent)
|
||||
self._postItemMove(sHandle, wCount)
|
||||
self._recordLastMove(sItem, pItem, pIndex)
|
||||
self._alertTreeChange(tHandle=sHandle, flush=True)
|
||||
sItem.setExpanded(isExpanded)
|
||||
|
||||
return
|
||||
@@ -826,8 +847,6 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
# Trigger dependent updates
|
||||
self.propagateCount(tHandle, wCount)
|
||||
self._setTreeChanged(True)
|
||||
self._emitItemChange(tHandle)
|
||||
|
||||
return True
|
||||
|
||||
@@ -920,7 +939,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
except Exception:
|
||||
logger.error("Failed to get index of item with handle '%s'", nHandle)
|
||||
if byIndex >= 0:
|
||||
self._treeMap[pHandle].insertChild(byIndex+1, newItem)
|
||||
self._treeMap[pHandle].insertChild(byIndex + 1, newItem)
|
||||
else:
|
||||
self._treeMap[pHandle].addChild(newItem)
|
||||
self.propagateCount(tHandle, nwItem.wordCount, countChildren=True)
|
||||
@@ -928,8 +947,6 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.setTreeItemValues(tHandle)
|
||||
newItem.setExpanded(nwItem.isExpanded)
|
||||
|
||||
self._setTreeChanged(True)
|
||||
|
||||
return newItem
|
||||
|
||||
def _addTrashRoot(self):
|
||||
@@ -942,33 +959,34 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
trItem = self._getTreeItem(trashHandle)
|
||||
if trItem is None:
|
||||
trItem = self._addTreeItem(
|
||||
self.theProject.tree[trashHandle]
|
||||
)
|
||||
trItem = self._addTreeItem(self.theProject.tree[trashHandle])
|
||||
if trItem is not None:
|
||||
trItem.setExpanded(True)
|
||||
self._setTreeChanged(True)
|
||||
self._alertTreeChange(tHandle=trashHandle, flush=True)
|
||||
|
||||
return trItem
|
||||
|
||||
def _setTreeChanged(self, theState):
|
||||
"""Set the tree change flag, and propagate to the project.
|
||||
def _alertTreeChange(self, tHandle=None, flush=True):
|
||||
"""Update information on tree change state, and emit necessary
|
||||
signals.
|
||||
"""
|
||||
self._treeChanged = theState
|
||||
if theState:
|
||||
self._timeChanged = time()
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
self._timeChanged = time()
|
||||
self.theProject.setProjectChanged(True)
|
||||
if flush:
|
||||
self.saveTreeOrder()
|
||||
|
||||
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
|
||||
|
||||
def _recordLastMove(self, srcItem, parItem, parIndex):
|
||||
|
||||
@@ -466,8 +466,8 @@ class GuiIcons:
|
||||
|
||||
# General Button Icons
|
||||
"add", "backward", "check", "clear", "close", "cross", "delete", "done", "down", "edit",
|
||||
"forward", "hash", "maximise", "minimise", "reference", "refresh", "remove", "save",
|
||||
"search_replace", "search", "settings", "up",
|
||||
"forward", "hash", "maximise", "menu", "minimise", "reference", "refresh", "remove",
|
||||
"save", "search_replace", "search", "settings", "up",
|
||||
|
||||
# Switches
|
||||
"sticky-on", "sticky-off",
|
||||
|
||||
+64
-68
@@ -27,6 +27,7 @@ import os
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from enum import Enum
|
||||
from time import time
|
||||
from datetime import datetime
|
||||
|
||||
@@ -39,20 +40,19 @@ from PyQt5.QtWidgets import (
|
||||
|
||||
from novelwriter.gui import (
|
||||
GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu,
|
||||
GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails, GuiProjectTree,
|
||||
GuiTheme, GuiViewsBar
|
||||
GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectTree, GuiTheme,
|
||||
GuiViewsBar
|
||||
)
|
||||
from novelwriter.dialogs import (
|
||||
GuiAbout, GuiDocMerge, GuiDocSplit, GuiItemEditor, GuiPreferences,
|
||||
GuiProjectDetails, GuiProjectLoad, GuiProjectSettings, GuiUpdates,
|
||||
GuiWordList
|
||||
GuiAbout, GuiDocMerge, GuiDocSplit, GuiPreferences, GuiProjectDetails,
|
||||
GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiWordList
|
||||
)
|
||||
from novelwriter.tools import (
|
||||
GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats
|
||||
)
|
||||
from novelwriter.core import NWProject
|
||||
from novelwriter.enum import (
|
||||
nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView
|
||||
nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView
|
||||
)
|
||||
from novelwriter.common import getGuiItem, hexToInt
|
||||
|
||||
@@ -112,23 +112,30 @@ class GuiMain(QMainWindow):
|
||||
self.docViewer = GuiDocViewer(self)
|
||||
self.treeMeta = GuiItemDetails(self)
|
||||
self.projView = GuiOutline(self)
|
||||
self.projMeta = GuiOutlineDetails(self)
|
||||
self.mainMenu = GuiMainMenu(self)
|
||||
self.viewsBar = GuiViewsBar(self)
|
||||
|
||||
# Connect Signals Between Main Elements
|
||||
self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage)
|
||||
self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus)
|
||||
self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts)
|
||||
self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts)
|
||||
self.viewsBar.viewChangeRequested.connect(self._changeView)
|
||||
|
||||
self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
|
||||
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
|
||||
self.treeView.novelItemChanged.connect(self._treeNovelItemChanged)
|
||||
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.projMeta.viewChangeRequested.connect(self._changeView)
|
||||
self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage)
|
||||
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
|
||||
self.projStack = QStackedWidget()
|
||||
@@ -158,12 +165,6 @@ class GuiMain(QMainWindow):
|
||||
self.splitDocs.addWidget(self.splitView)
|
||||
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
|
||||
self.splitMain = QSplitter(Qt.Horizontal)
|
||||
self.splitMain.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -175,7 +176,7 @@ class GuiMain(QMainWindow):
|
||||
# Main Stack : Editor / Outline
|
||||
self.mainStack = QStackedWidget()
|
||||
self.mainStack.addWidget(self.splitMain)
|
||||
self.mainStack.addWidget(self.splitOutline)
|
||||
self.mainStack.addWidget(self.projView)
|
||||
self.mainStack.currentChanged.connect(self._mainStackChanged)
|
||||
|
||||
# Indices of Splitter Widgets
|
||||
@@ -188,7 +189,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
# Indices of Tab Widgets
|
||||
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.idxNovelView = self.projStack.indexOf(self.novelView)
|
||||
|
||||
@@ -296,7 +297,7 @@ class GuiMain(QMainWindow):
|
||||
self.docEditor.clearEditor()
|
||||
self.docEditor.setDictionaries()
|
||||
self.closeDocViewer()
|
||||
self.projMeta.clearDetails()
|
||||
self.projView.clearOutline()
|
||||
|
||||
# General
|
||||
self.statusBar.clearStatus()
|
||||
@@ -358,6 +359,7 @@ class GuiMain(QMainWindow):
|
||||
self.rebuildTrees()
|
||||
self.saveProject()
|
||||
self.docEditor.setDictionaries()
|
||||
self.projView.updateRootItem(None)
|
||||
self.rebuildIndex(beQuiet=True)
|
||||
self.statusBar.setRefTime(self.theProject.projOpened)
|
||||
self.statusBar.setProjectStatus(nwState.GOOD)
|
||||
@@ -503,8 +505,8 @@ class GuiMain(QMainWindow):
|
||||
self.rebuildTrees()
|
||||
self.docEditor.setDictionaries()
|
||||
self.docEditor.toggleSpellCheck(self.theProject.spellCheck)
|
||||
self.mainMenu.setAutoOutline(self.theProject.autoOutline)
|
||||
self.statusBar.setRefTime(self.theProject.projOpened)
|
||||
self.projView.updateRootItem(None)
|
||||
self._updateStatusWordCount()
|
||||
|
||||
# Restore previously open documents, if any
|
||||
@@ -596,7 +598,6 @@ class GuiMain(QMainWindow):
|
||||
logger.error("No project open")
|
||||
return False
|
||||
|
||||
self.treeView.flushTreeOrder()
|
||||
nHandle = None # The next handle after tHandle
|
||||
fHandle = None # The first file handle we encounter
|
||||
foundIt = False # We've found tHandle, pick the next we see
|
||||
@@ -790,7 +791,7 @@ class GuiMain(QMainWindow):
|
||||
tHandle = self.treeView.getSelectedHandle()
|
||||
elif self.novelView.hasFocus():
|
||||
tHandle, tLine = self.novelView.getSelectedHandle()
|
||||
elif self.projView.hasFocus():
|
||||
elif self.projView.treeFocus():
|
||||
tHandle, tLine = self.projView.getSelectedHandle()
|
||||
else:
|
||||
logger.warning("No item selected")
|
||||
@@ -813,27 +814,10 @@ class GuiMain(QMainWindow):
|
||||
tHandle = self.docEditor.docHandle()
|
||||
else:
|
||||
tHandle = self.treeView.getSelectedHandle()
|
||||
if tHandle:
|
||||
return self.treeView.editTreeItem(tHandle)
|
||||
|
||||
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.treeView.setTreeItemValues(tHandle)
|
||||
self.treeMeta.updateViewBox(tHandle)
|
||||
self.docEditor.updateDocInfo(tHandle)
|
||||
self.docViewer.updateDocInfo(tHandle)
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def rebuildTrees(self):
|
||||
"""Rebuild the project tree.
|
||||
@@ -889,19 +873,6 @@ class GuiMain(QMainWindow):
|
||||
|
||||
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
|
||||
##
|
||||
@@ -949,7 +920,6 @@ class GuiMain(QMainWindow):
|
||||
self.treeView.initTree()
|
||||
self.novelView.initTree()
|
||||
self.projView.initOutline()
|
||||
self.projMeta.initDetails()
|
||||
self._updateStatusWordCount()
|
||||
|
||||
return
|
||||
@@ -980,8 +950,6 @@ class GuiMain(QMainWindow):
|
||||
logger.error("No project open")
|
||||
return False
|
||||
|
||||
self.treeView.flushTreeOrder()
|
||||
|
||||
dlgDetails = getGuiItem("GuiProjectDetails")
|
||||
if dlgDetails is None:
|
||||
dlgDetails = GuiProjectDetails(self)
|
||||
@@ -1186,7 +1154,7 @@ class GuiMain(QMainWindow):
|
||||
if not self.isFocusMode:
|
||||
self.mainConf.setMainPanePos(self.splitMain.sizes())
|
||||
self.mainConf.setDocPanePos(self.splitDocs.sizes())
|
||||
self.mainConf.setOutlinePanePos(self.splitOutline.sizes())
|
||||
self.mainConf.setOutlinePanePos(self.projView.splitSizes())
|
||||
if self.viewMeta.isVisible():
|
||||
self.mainConf.setViewPanePos(self.splitView.sizes())
|
||||
|
||||
@@ -1223,7 +1191,7 @@ class GuiMain(QMainWindow):
|
||||
self.docViewer.setFocus()
|
||||
elif paneNo == nwWidget.OUTLINE:
|
||||
self._changeView(nwView.OUTLINE)
|
||||
self.projView.setFocus()
|
||||
self.projView.setTreeFocus()
|
||||
return
|
||||
|
||||
def closeDocEditor(self):
|
||||
@@ -1453,6 +1421,23 @@ class GuiMain(QMainWindow):
|
||||
|
||||
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
|
||||
##
|
||||
@@ -1468,9 +1453,21 @@ class GuiMain(QMainWindow):
|
||||
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)
|
||||
def _changeView(self, view):
|
||||
"""Handle the requested change of view from the GuiViewBar.
|
||||
@@ -1488,7 +1485,7 @@ class GuiMain(QMainWindow):
|
||||
self.projStack.setCurrentWidget(self.novelView)
|
||||
|
||||
elif view == nwView.OUTLINE:
|
||||
self.mainStack.setCurrentWidget(self.splitOutline)
|
||||
self.mainStack.setCurrentWidget(self.projView)
|
||||
|
||||
return
|
||||
|
||||
@@ -1568,8 +1565,7 @@ class GuiMain(QMainWindow):
|
||||
if self.mainStack.currentIndex() == self.idxOutlineView:
|
||||
logger.verbose("Novel tree changed while Outline tab active")
|
||||
if self.hasProject:
|
||||
self.treeView.flushTreeOrder()
|
||||
self.projView.refreshTree(novelChanged=True)
|
||||
self.projView.refreshView(novelChanged=True)
|
||||
|
||||
return
|
||||
|
||||
@@ -1602,7 +1598,7 @@ class GuiMain(QMainWindow):
|
||||
elif tabIndex == self.idxOutlineView:
|
||||
logger.verbose("Project outline tab activated")
|
||||
if self.hasProject:
|
||||
self.projView.refreshTree()
|
||||
self.projView.refreshView()
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -709,7 +709,6 @@ class GuiBuildNovel(QDialog):
|
||||
bldObj.initDocument()
|
||||
|
||||
# Make sure the project and document is up to date
|
||||
self.theParent.treeView.flushTreeOrder()
|
||||
self.theParent.saveDocument()
|
||||
|
||||
self.buildProgress.setMaximum(len(self.theProject.tree))
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -1,13 +1,13 @@
|
||||
<?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>
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jane Smith</author>
|
||||
<author>Jay Doh</author>
|
||||
<saveCount>1329</saveCount>
|
||||
<autoCount>220</autoCount>
|
||||
<editTime>67104</editTime>
|
||||
<saveCount>1334</saveCount>
|
||||
<autoCount>225</autoCount>
|
||||
<editTime>67746</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>False</doBackup>
|
||||
@@ -15,10 +15,10 @@
|
||||
<spellCheck>True</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>636b6aa9b697b</lastEdited>
|
||||
<lastEdited>a520879ca0b45</lastEdited>
|
||||
<lastViewed>636b6aa9b697b</lastViewed>
|
||||
<lastWordCount>1303</lastWordCount>
|
||||
<novelWordCount>894</novelWordCount>
|
||||
<lastWordCount>1363</lastWordCount>
|
||||
<novelWordCount>954</novelWordCount>
|
||||
<notesWordCount>409</notesWordCount>
|
||||
<autoReplace>
|
||||
<entry key="A">B</entry>
|
||||
@@ -33,10 +33,10 @@
|
||||
<section></section>
|
||||
</titleFormat>
|
||||
<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="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="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>
|
||||
@@ -48,13 +48,13 @@
|
||||
<entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="24">
|
||||
<content count="27">
|
||||
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
|
||||
<meta expanded="True"/>
|
||||
<name status="sc24b8f" import="ia857f0">Novel</name>
|
||||
</item>
|
||||
<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>
|
||||
</item>
|
||||
<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"/>
|
||||
<name status="s90e6c9" import="ia857f0" exported="True">We Found John!</name>
|
||||
</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"/>
|
||||
<name status="sf12341" import="ia857f0">Characters</name>
|
||||
</item>
|
||||
@@ -109,7 +121,7 @@
|
||||
<meta expanded="False" charCount="55" wordCount="9" paraCount="1" cursorPos="25"/>
|
||||
<name status="sf12341" import="i2d7a54" exported="True">Jane Smith</name>
|
||||
</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"/>
|
||||
<name status="sf12341" import="ia857f0">Locations</name>
|
||||
</item>
|
||||
@@ -125,7 +137,7 @@
|
||||
<meta expanded="False" charCount="28" wordCount="6" paraCount="1" cursorPos="45"/>
|
||||
<name status="sf12341" import="i2d7a54" exported="True">Mars</name>
|
||||
</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"/>
|
||||
<name status="sf12341" import="ia857f0">Archive</name>
|
||||
</item>
|
||||
@@ -137,7 +149,7 @@
|
||||
<meta expanded="False" charCount="232" wordCount="42" paraCount="1" cursorPos="239"/>
|
||||
<name status="s90e6c9" import="ia857f0" exported="True">Old File</name>
|
||||
</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"/>
|
||||
<name status="sf12341" import="ia857f0">Trash</name>
|
||||
</item>
|
||||
|
||||
@@ -197,8 +197,7 @@ def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd):
|
||||
nItem = theProject.tree[nHandle]
|
||||
cItem = theProject.tree[cHandle]
|
||||
|
||||
assert theIndex.novelChangedSince(0) is False
|
||||
assert theIndex.notesChangedSince(0) is False
|
||||
assert theIndex.rootChangedSince("0000000000010", 0) is False
|
||||
assert theIndex.indexChangedSince(0) is False
|
||||
|
||||
assert theIndex.scanText(cHandle, (
|
||||
@@ -228,12 +227,14 @@ def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd):
|
||||
"@time": []
|
||||
}
|
||||
|
||||
assert theIndex.novelChangedSince(0) is True
|
||||
assert theIndex.notesChangedSince(0) is True
|
||||
assert theIndex.rootChangedSince("0000000000010", 0) is True
|
||||
assert theIndex.indexChangedSince(0) is True
|
||||
|
||||
assert theIndex.getHandleHeaderLevel(cHandle) == "H1"
|
||||
assert theIndex.getHandleHeaderLevel(nHandle) == "H1"
|
||||
assert theIndex.getHandleHeaderIntLevel(cHandle) == 1
|
||||
assert theIndex.getHandleHeaderIntLevel(nHandle) == 1
|
||||
assert theIndex.getHandleHeaderIntLevel("stuff") == 0
|
||||
|
||||
# Zero Items
|
||||
assert theIndex.checkThese([], cItem) == []
|
||||
|
||||
@@ -25,7 +25,6 @@ import pytest
|
||||
from tools import readFile
|
||||
|
||||
from novelwriter.core import NWProject, ToHtml
|
||||
from novelwriter.core.index import NWIndex
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
@@ -33,7 +32,6 @@ def testCoreToHtml_ConvertFormat(mockGUI):
|
||||
"""Test the tokenizer and converter chain using the ToHtml class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theHtml = ToHtml(theProject)
|
||||
|
||||
# Novel Files Headers
|
||||
@@ -236,7 +234,6 @@ def testCoreToHtml_ConvertDirect(mockGUI):
|
||||
"""Test the converter directly using the ToHtml class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theHtml = ToHtml(theProject)
|
||||
|
||||
theHtml._isNovel = True
|
||||
@@ -607,7 +604,6 @@ def testCoreToHtml_Format(mockGUI):
|
||||
"""Test all the formatters for the ToHtml class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theHtml = ToHtml(theProject)
|
||||
|
||||
# Export Mode
|
||||
|
||||
@@ -25,7 +25,6 @@ import pytest
|
||||
from tools import readFile
|
||||
|
||||
from novelwriter.core import NWProject, ToMarkdown
|
||||
from novelwriter.core.index import NWIndex
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
@@ -33,7 +32,6 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
|
||||
"""Test the tokenizer and converter chain using the ToMarkdown class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theMD = ToMarkdown(theProject)
|
||||
|
||||
# Headers
|
||||
@@ -162,7 +160,6 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
|
||||
"""Test the converter directly using the ToMarkdown class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theMD = ToMarkdown(theProject)
|
||||
|
||||
theMD._isNovel = True
|
||||
@@ -267,7 +264,6 @@ def testCoreToMarkdown_Format(mockGUI):
|
||||
"""Test all the formatters for the ToMarkdown class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theMD = ToMarkdown(theProject)
|
||||
|
||||
assert theMD._formatKeywords("", theMD.A_NONE) == ""
|
||||
|
||||
@@ -29,7 +29,6 @@ from shutil import copyfile
|
||||
from tools import cmpFiles
|
||||
|
||||
from novelwriter.core import NWProject, ToOdt
|
||||
from novelwriter.core.index import NWIndex
|
||||
from novelwriter.core.toodt import ODTParagraphStyle, ODTTextStyle, XMLParagraph, _mkTag
|
||||
|
||||
XML_NS = [
|
||||
@@ -56,7 +55,6 @@ def testCoreToOdt_Init(mockGUI):
|
||||
"""Test initialisation of the ODT document.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
|
||||
# Flat Doc
|
||||
# ========
|
||||
@@ -112,7 +110,6 @@ def testCoreToOdt_TextFormatting(mockGUI):
|
||||
"""Test formatting of paragraphs.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theDoc = ToOdt(theProject, isFlat=True)
|
||||
|
||||
theDoc.initDocument()
|
||||
@@ -234,7 +231,6 @@ def testCoreToOdt_Convert(mockGUI):
|
||||
"""Test the converter of the ToOdt class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theDoc = ToOdt(theProject, isFlat=True)
|
||||
|
||||
theDoc._isNovel = True
|
||||
@@ -566,7 +562,6 @@ def testCoreToOdt_ConvertDirect(mockGUI):
|
||||
otherwise hard to reach conditions.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theDoc = ToOdt(theProject, isFlat=True)
|
||||
|
||||
theDoc._isNovel = True
|
||||
@@ -621,7 +616,6 @@ def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir):
|
||||
"""Test the document save functions.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
|
||||
theDoc = ToOdt(theProject, isFlat=True)
|
||||
theDoc._isNovel = True
|
||||
@@ -658,7 +652,6 @@ def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir):
|
||||
"""Test the document save functions.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
|
||||
theDoc = ToOdt(theProject, isFlat=False)
|
||||
theDoc._isNovel = True
|
||||
@@ -738,7 +731,6 @@ def testCoreToOdt_Format(mockGUI):
|
||||
"""Test the formatters for the ToOdt class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
mockGUI.theIndex = NWIndex(theProject)
|
||||
theDoc = ToOdt(theProject, isFlat=True)
|
||||
|
||||
assert theDoc._formatSynopsis("synopsis text") == (
|
||||
|
||||
@@ -149,6 +149,11 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
||||
assert theTree.isTrash("a000000000003") 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
|
||||
assert theTree.isTrash("0000000000000") is True # Doesn't exist
|
||||
assert theTree.isTrash("a000000000003") is True # This the trash folder
|
||||
|
||||
@@ -61,7 +61,6 @@ def testGuiMain_ProjectBlocker(monkeypatch, nwGUI):
|
||||
assert nwGUI.editItem() is False
|
||||
assert nwGUI.requestNovelTreeRefresh() is False
|
||||
assert nwGUI.rebuildIndex() is False
|
||||
assert nwGUI.rebuildOutline() is False
|
||||
assert nwGUI.showProjectSettingsDialog() is False
|
||||
assert nwGUI.showProjectDetailsDialog() is False
|
||||
assert nwGUI.showBuildProjectDialog() is False
|
||||
@@ -149,12 +148,12 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
|
||||
# Project Outline has focus
|
||||
nwGUI.switchFocus(nwWidget.OUTLINE)
|
||||
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
|
||||
actItem = nwGUI.projView.topLevelItem(0)
|
||||
actItem = nwGUI.projView.outlineView.topLevelItem(0)
|
||||
chpItem = actItem.child(0)
|
||||
selItem = chpItem.child(0)
|
||||
nwGUI.projView.setCurrentItem(selItem)
|
||||
nwGUI.projView.outlineView.setCurrentItem(selItem)
|
||||
nwGUI._keyPressReturn()
|
||||
assert nwGUI.docEditor.docHandle() == sHandle
|
||||
assert nwGUI.closeDocument() is True
|
||||
|
||||
@@ -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/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import pytest
|
||||
|
||||
from PyQt5.QtCore import Qt, QPoint
|
||||
from PyQt5.QtWidgets import QAction, QTreeWidgetItem, QMessageBox
|
||||
from tools import buildTestProject, writeFile
|
||||
|
||||
from novelwriter.enum import nwOutline
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QWidget, QMessageBox, QAction
|
||||
|
||||
keyDelay = 2
|
||||
typeDelay = 1
|
||||
stepDelay = 20
|
||||
from novelwriter.enum import nwItemClass, nwOutline, nwView
|
||||
|
||||
|
||||
@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.
|
||||
"""
|
||||
# Block message box
|
||||
@@ -43,73 +167,109 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
|
||||
nwGUI.mainConf.lastPath = nwLipsum
|
||||
|
||||
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
|
||||
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)
|
||||
lipHandle = "b3643d0f92e32"
|
||||
|
||||
nwGUI.projView._loadHeaderState()
|
||||
assert not nwGUI.projView._colHidden[nwOutline.CCOUNT]
|
||||
# Check defaults in dropdown list
|
||||
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
|
||||
nwGUI.rebuildOutline()
|
||||
selItem = nwGUI.projView.topLevelItem(0)
|
||||
assert isinstance(selItem, QTreeWidgetItem)
|
||||
outlineView.refreshTree()
|
||||
selItem = outlineView.topLevelItem(0)
|
||||
|
||||
nwGUI.projView.setCurrentItem(selItem)
|
||||
assert nwGUI.projMeta.titleLabel.text() == "<b>Title</b>"
|
||||
assert nwGUI.projMeta.titleValue.text() == "Lorem Ipsum"
|
||||
assert nwGUI.projMeta.fileValue.text() == "Lorem Ipsum"
|
||||
assert nwGUI.projMeta.itemValue.text() == "Finished"
|
||||
outlineView.setCurrentItem(selItem)
|
||||
assert outlineData.titleLabel.text() == "<b>Title</b>"
|
||||
assert outlineData.titleValue.text() == "Lorem Ipsum"
|
||||
assert outlineData.fileValue.text() == "Lorem Ipsum"
|
||||
assert outlineData.itemValue.text() == "Finished"
|
||||
|
||||
assert nwGUI.projMeta.cCValue.text() == "230"
|
||||
assert nwGUI.projMeta.wCValue.text() == "40"
|
||||
assert nwGUI.projMeta.pCValue.text() == "3"
|
||||
assert outlineData.cCValue.text() == "230"
|
||||
assert outlineData.wCValue.text() == "40"
|
||||
assert outlineData.pCValue.text() == "3"
|
||||
|
||||
# Scene One
|
||||
actItem = nwGUI.projView.topLevelItem(1)
|
||||
actItem = outlineView.topLevelItem(1)
|
||||
chpItem = actItem.child(0)
|
||||
selItem = chpItem.child(0)
|
||||
|
||||
nwGUI.projView.setCurrentItem(selItem)
|
||||
tHandle, tLine = nwGUI.projView.getSelectedHandle()
|
||||
outlineView.setCurrentItem(selItem)
|
||||
tHandle, tLine = outlineView.getSelectedHandle()
|
||||
assert tHandle == "88243afbe5ed8"
|
||||
assert tLine == 0
|
||||
|
||||
assert nwGUI.projMeta.titleLabel.text() == "<b>Scene</b>"
|
||||
assert nwGUI.projMeta.titleValue.text() == "Scene One"
|
||||
assert nwGUI.projMeta.fileValue.text() == "Scene One"
|
||||
assert nwGUI.projMeta.itemValue.text() == "Finished"
|
||||
assert outlineData.titleLabel.text() == "<b>Scene</b>"
|
||||
assert outlineData.titleValue.text() == "Scene One"
|
||||
assert outlineData.fileValue.text() == "Scene One"
|
||||
assert outlineData.itemValue.text() == "Finished"
|
||||
|
||||
# Click POV Link
|
||||
assert nwGUI.projMeta.povKeyValue.text() == "<a href='#pov=Bod'>Bod</a>"
|
||||
nwGUI.projMeta._tagClicked("#pov=Bod")
|
||||
assert outlineData.povKeyValue.text() == "<a href='Bod'>Bod</a>"
|
||||
nwGUI.projView._tagClicked("Bod")
|
||||
assert nwGUI.docViewer.docHandle() == "4c4f28287af27"
|
||||
|
||||
# Scene One, Section Two
|
||||
actItem = nwGUI.projView.topLevelItem(1)
|
||||
actItem = outlineView.topLevelItem(1)
|
||||
chpItem = actItem.child(0)
|
||||
scnItem = chpItem.child(0)
|
||||
selItem = scnItem.child(0)
|
||||
|
||||
nwGUI.projView.setCurrentItem(selItem)
|
||||
tHandle, tLine = nwGUI.projView.getSelectedHandle()
|
||||
outlineView.setCurrentItem(selItem)
|
||||
tHandle, tLine = outlineView.getSelectedHandle()
|
||||
assert tHandle == "88243afbe5ed8"
|
||||
assert tLine == 12
|
||||
|
||||
assert nwGUI.projMeta.titleLabel.text() == "<b>Section</b>"
|
||||
assert nwGUI.projMeta.titleValue.text() == "Scene One, Section Two"
|
||||
assert nwGUI.projMeta.fileValue.text() == "Scene One"
|
||||
assert nwGUI.projMeta.itemValue.text() == "Finished"
|
||||
assert outlineData.titleLabel.text() == "<b>Section</b>"
|
||||
assert outlineData.titleValue.text() == "Scene One, Section Two"
|
||||
assert outlineData.fileValue.text() == "Scene One"
|
||||
assert outlineData.itemValue.text() == "Finished"
|
||||
|
||||
nwGUI.projView._treeDoubleClick(selItem, 0)
|
||||
outlineView._treeDoubleClick(selItem, 0)
|
||||
assert nwGUI.docEditor.docHandle() == "88243afbe5ed8"
|
||||
|
||||
# qtbot.stopForInteraction()
|
||||
# qtbot.stop()
|
||||
|
||||
# END Test testGuiOutline_Main
|
||||
# END Test testGuiOutline_Content
|
||||
|
||||
@@ -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"].itemClass == nwItemClass.NOVEL
|
||||
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
|
||||
nwTree.setSelectedHandle("000000000000a")
|
||||
@@ -246,17 +246,14 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
|
||||
|
||||
# Move novel folder up
|
||||
assert nwTree.moveTreeItem(-1) is False
|
||||
nwTree.flushTreeOrder()
|
||||
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
|
||||
|
||||
# Move novel folder down
|
||||
assert nwTree.moveTreeItem(1) is True
|
||||
nwTree.flushTreeOrder()
|
||||
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 1
|
||||
|
||||
# Move novel folder up again
|
||||
assert nwTree.moveTreeItem(-1) is True
|
||||
nwTree.flushTreeOrder()
|
||||
assert nwGUI.theProject.tree._treeOrder.index("0000000000008") == 0
|
||||
|
||||
# Clean up
|
||||
@@ -432,10 +429,8 @@ def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockR
|
||||
assert nwTree.emptyTrash() is False
|
||||
|
||||
# Empty the trash proper
|
||||
nwTree._setTreeChanged(False)
|
||||
assert nwTree.emptyTrash() is True
|
||||
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
|
||||
assert os.path.isfile(os.path.join(fncDir, "project", "content", "000000000000e.nwd"))
|
||||
|
||||
Reference in New Issue
Block a user