Update the Outline View panel (#1063)

Add a top level widget that contains the entire Outline View.
Move the details panel code to the outline.py file.
Add a toolbar to the top of the Outline view with a dropdown box to select which Novel folder to view.
Add a refresh button to the tool bar.
Move the context menu for changing columns to a menu button on the tool bar.
Remove the Rebuild Outline and Auto-Update Outline options from the main menu.
This commit is contained in:
Veronica Berglyd Olsen
2022-06-05 22:34:49 +02:00
committed by GitHub
31 changed files with 1150 additions and 739 deletions
-2
View File
@@ -59,7 +59,6 @@ The main shorcuts are as follows:
":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo."
":kbd:`Ctrl`:kbd:`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
View File
@@ -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.
"""
+16
View File
@@ -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.
"""
+8
View File
@@ -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
-2
View File
@@ -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",
+20 -13
View File
@@ -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)
+23 -38
View File
@@ -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")
+5 -4
View File
@@ -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
-25
View File
@@ -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()
+1 -2
View File
@@ -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
View File
@@ -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 20182022, 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
-355
View File
@@ -1,355 +0,0 @@
"""
novelWriter GUI Project Outline Details
=========================================
GUI class for the project outline details panel
File History:
Created: 2020-06-02 [0.7.0]
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import novelwriter
from PyQt5.QtCore import Qt, QT_TRANSLATE_NOOP, pyqtSignal
from PyQt5.QtWidgets import (
QScrollArea, QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel, QFrame
)
from novelwriter.enum import nwView
from novelwriter.common import checkInt
from novelwriter.constants import trConst, nwKeyWords, nwLabels
logger = logging.getLogger(__name__)
class GuiOutlineDetails(QScrollArea):
LVL_MAP = {
"H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"),
"H2": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Chapter"),
"H3": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Scene"),
"H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"),
}
viewChangeRequested = pyqtSignal(nwView)
def __init__(self, theParent):
QScrollArea.__init__(self, theParent)
logger.debug("Initialising GuiOutlineDetails ...")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
self.theTheme = theParent.theTheme
# Sizes
minTitle = 30*self.theTheme.textNWidth
maxTitle = 40*self.theTheme.textNWidth
wCount = self.theTheme.getTextWidth("999,999")
hSpace = int(self.mainConf.pxInt(10))
vSpace = int(self.mainConf.pxInt(4))
# Details Area
self.titleLabel = QLabel("<b>%s</b>" % self.tr("Title"))
self.fileLabel = QLabel("<b>%s</b>" % self.tr("Document"))
self.itemLabel = QLabel("<b>%s</b>" % self.tr("Status"))
self.titleValue = QLabel("")
self.fileValue = QLabel("")
self.itemValue = QLabel("")
self.titleValue.setMinimumWidth(minTitle)
self.titleValue.setMaximumWidth(maxTitle)
self.fileValue.setMinimumWidth(minTitle)
self.fileValue.setMaximumWidth(maxTitle)
self.itemValue.setMinimumWidth(minTitle)
self.itemValue.setMaximumWidth(maxTitle)
# Stats Area
self.cCLabel = QLabel("<b>%s</b>" % self.tr("Characters"))
self.wCLabel = QLabel("<b>%s</b>" % self.tr("Words"))
self.pCLabel = QLabel("<b>%s</b>" % self.tr("Paragraphs"))
self.cCValue = QLabel("")
self.wCValue = QLabel("")
self.pCValue = QLabel("")
self.cCValue.setMinimumWidth(wCount)
self.wCValue.setMinimumWidth(wCount)
self.pCValue.setMinimumWidth(wCount)
self.cCValue.setAlignment(Qt.AlignRight)
self.wCValue.setAlignment(Qt.AlignRight)
self.pCValue.setAlignment(Qt.AlignRight)
# Synopsis
self.synopLabel = QLabel("<b>%s</b>" % self.tr("Synopsis"))
self.synopValue = QLabel("")
self.synopLWrap = QHBoxLayout()
self.synopValue.setWordWrap(True)
self.synopValue.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self.synopLWrap.addWidget(self.synopValue, 1)
# Tags
self.povKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]))
self.focKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]))
self.chrKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY]))
self.pltKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]))
self.timKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.TIME_KEY]))
self.wldKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY]))
self.objKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY]))
self.entKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY]))
self.cstKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY]))
self.povKeyLWrap = QHBoxLayout()
self.focKeyLWrap = QHBoxLayout()
self.chrKeyLWrap = QHBoxLayout()
self.pltKeyLWrap = QHBoxLayout()
self.timKeyLWrap = QHBoxLayout()
self.wldKeyLWrap = QHBoxLayout()
self.objKeyLWrap = QHBoxLayout()
self.entKeyLWrap = QHBoxLayout()
self.cstKeyLWrap = QHBoxLayout()
self.povKeyValue = QLabel("")
self.focKeyValue = QLabel("")
self.chrKeyValue = QLabel("")
self.pltKeyValue = QLabel("")
self.timKeyValue = QLabel("")
self.wldKeyValue = QLabel("")
self.objKeyValue = QLabel("")
self.entKeyValue = QLabel("")
self.cstKeyValue = QLabel("")
self.povKeyValue.setWordWrap(True)
self.focKeyValue.setWordWrap(True)
self.chrKeyValue.setWordWrap(True)
self.pltKeyValue.setWordWrap(True)
self.timKeyValue.setWordWrap(True)
self.wldKeyValue.setWordWrap(True)
self.objKeyValue.setWordWrap(True)
self.entKeyValue.setWordWrap(True)
self.cstKeyValue.setWordWrap(True)
self.povKeyValue.linkActivated.connect(self._tagClicked)
self.focKeyValue.linkActivated.connect(self._tagClicked)
self.chrKeyValue.linkActivated.connect(self._tagClicked)
self.pltKeyValue.linkActivated.connect(self._tagClicked)
self.timKeyValue.linkActivated.connect(self._tagClicked)
self.wldKeyValue.linkActivated.connect(self._tagClicked)
self.objKeyValue.linkActivated.connect(self._tagClicked)
self.entKeyValue.linkActivated.connect(self._tagClicked)
self.cstKeyValue.linkActivated.connect(self._tagClicked)
self.povKeyLWrap.addWidget(self.povKeyValue, 1)
self.focKeyLWrap.addWidget(self.focKeyValue, 1)
self.chrKeyLWrap.addWidget(self.chrKeyValue, 1)
self.pltKeyLWrap.addWidget(self.pltKeyValue, 1)
self.timKeyLWrap.addWidget(self.timKeyValue, 1)
self.wldKeyLWrap.addWidget(self.wldKeyValue, 1)
self.objKeyLWrap.addWidget(self.objKeyValue, 1)
self.entKeyLWrap.addWidget(self.entKeyValue, 1)
self.cstKeyLWrap.addWidget(self.cstKeyValue, 1)
# Selected Item Details
self.mainGroup = QGroupBox(self.tr("Title Details"), self)
self.mainForm = QGridLayout()
self.mainGroup.setLayout(self.mainForm)
self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.fileLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.fileValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.itemLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.itemValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addLayout(self.synopLWrap, 4, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.setColumnStretch(1, 1)
self.mainForm.setRowStretch(4, 1)
self.mainForm.setHorizontalSpacing(hSpace)
self.mainForm.setVerticalSpacing(vSpace)
# Selected Item Tags
self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self)
self.tagsForm = QGridLayout()
self.tagsGroup.setLayout(self.tagsForm)
self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.focKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.focKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.chrKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.chrKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.pltKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.pltKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.timKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.timKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.wldKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.wldKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.objKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.objKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.entKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.entKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.cstKeyLabel, 8, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.cstKeyLWrap, 8, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.setColumnStretch(1, 1)
self.tagsForm.setRowStretch(8, 1)
self.tagsForm.setHorizontalSpacing(hSpace)
self.tagsForm.setVerticalSpacing(vSpace)
# Assemble
self.outerWidget = QWidget()
self.outerBox = QHBoxLayout()
self.outerBox.addWidget(self.mainGroup, 0)
self.outerBox.addWidget(self.tagsGroup, 1)
self.outerWidget.setLayout(self.outerBox)
self.setWidget(self.outerWidget)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setWidgetResizable(True)
self.setFrameStyle(QFrame.NoFrame)
self.initDetails()
logger.debug("GuiOutlineDetails initialisation complete")
return
def initDetails(self):
"""Set or update outline settings.
"""
# Scroll bars
if self.mainConf.hideVScroll:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
if self.mainConf.hideHScroll:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
return
def clearDetails(self):
"""Clear all the data labels.
"""
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText("")
self.fileValue.setText("")
self.itemValue.setText("")
self.cCValue.setText("")
self.wCValue.setText("")
self.pCValue.setText("")
self.synopValue.setText("")
self.povKeyValue.setText("")
self.focKeyValue.setText("")
self.chrKeyValue.setText("")
self.pltKeyValue.setText("")
self.timKeyValue.setText("")
self.wldKeyValue.setText("")
self.objKeyValue.setText("")
self.entKeyValue.setText("")
self.cstKeyValue.setText("")
return
def showItem(self, tHandle, sTitle):
"""Update the content of the tree with the given handle and line
number pointing to a header.
"""
pIndex = self.theProject.index
nwItem = self.theProject.tree[tHandle]
novIdx = pIndex.getNovelData(tHandle, sTitle)
theRefs = pIndex.getReferences(tHandle, sTitle)
if nwItem is None or novIdx is None:
return False
if novIdx.level in self.LVL_MAP:
self.titleLabel.setText("<b>%s</b>" % self.tr(self.LVL_MAP[novIdx.level]))
else:
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText(novIdx.title)
itemStatus, _ = nwItem.getImportStatus()
self.fileValue.setText(nwItem.itemName)
self.itemValue.setText(itemStatus)
cC = checkInt(novIdx.charCount, 0)
wC = checkInt(novIdx.wordCount, 0)
pC = checkInt(novIdx.paraCount, 0)
self.cCValue.setText(f"{cC:n}")
self.wCValue.setText(f"{wC:n}")
self.pCValue.setText(f"{pC:n}")
self.synopValue.setText(novIdx.synopsis)
self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY))
self.focKeyValue.setText(self._formatTags(theRefs, nwKeyWords.FOCUS_KEY))
self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY))
self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY))
self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY))
self.wldKeyValue.setText(self._formatTags(theRefs, nwKeyWords.WORLD_KEY))
self.objKeyValue.setText(self._formatTags(theRefs, nwKeyWords.OBJECT_KEY))
self.entKeyValue.setText(self._formatTags(theRefs, nwKeyWords.ENTITY_KEY))
self.cstKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CUSTOM_KEY))
return True
##
# Slots
##
def _tagClicked(self, theLink):
"""Capture the click of a tag in the right-most column.
"""
logger.verbose("Clicked link: '%s'", theLink)
if len(theLink) > 0:
theBits = theLink.split("=")
if len(theBits) == 2:
self.viewChangeRequested.emit(nwView.PROJECT)
self.theParent.docViewer.loadFromTag(theBits[1])
return
##
# Internal Functions
##
def _formatTags(self, theRefs, theKey):
"""Format the tags as clickable links.
"""
if theKey not in theRefs:
return ""
refTags = []
for tTag in theRefs[theKey]:
refTags.append("<a href='#%s=%s'>%s</a>" % (
theKey[1:], tTag, tTag
))
return ", ".join(refTags)
# END Class GuiOutlineDetails
+78 -60
View File
@@ -32,11 +32,14 @@ from time import time
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.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):
+2 -2
View File
@@ -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
View File
@@ -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
-1
View File
@@ -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))
+17
View File
@@ -0,0 +1,17 @@
%%~name: Chapter One
%%~path: e5e47ebf63b1c/a520879ca0b45
%%~kind: NOVEL/DOCUMENT
### Chapter One
@pov: Jane
% Synopsis: Remember Jane and John?
### Scene One
@pov: Jane
@focus: John
A project can have multiple novel root folders for multiple novels. This is the first scene of a sequel to the first novel.
In this way, the writer can keep the same notes for multiple novels. This can be especially useful if the writer is planning a multi-novel story in advance.
+8
View File
@@ -0,0 +1,8 @@
%%~name: Title Page
%%~path: e5e47ebf63b1c/bacb7059e3083
%%~kind: NOVEL/DOCUMENT
#! Sequel Novel
>> **By Jane Doh** <<
% Synopsis: Jane and John are back in a sequel to My Novel!
+27 -15
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<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>
+5 -4
View File
@@ -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) == []
-4
View File
@@ -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
-4
View File
@@ -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) == ""
-8
View File
@@ -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") == (
+5
View File
@@ -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
+3 -4
View File
@@ -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
+206 -46
View File
@@ -19,20 +19,144 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
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
+1 -6
View File
@@ -96,7 +96,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd)
assert nwGUI.theProject.tree["0000000000013"].itemRoot == "0000000000008"
assert nwGUI.theProject.tree["0000000000013"].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"))