Merge pull request #140 from vkbo/project_outline

Synopsis and Outline Feature
This commit is contained in:
Veronica K. Berglyd Olsen
2020-04-13 16:07:10 +02:00
committed by GitHub
38 changed files with 897 additions and 478 deletions
@@ -17,3 +17,4 @@ value = 184, 200, 0
spellcheckline = 200, 46, 0
tagerror = 46, 200, 0
replacetag = 0, 184, 46
modifier = 200, 120, 0
@@ -17,3 +17,4 @@ value = 50, 150, 50
spellcheckline = 200, 0, 0
tagerror = 0, 150, 0
replacetag = 0, 150, 0
modifier = 150, 110, 30
+1
View File
@@ -40,3 +40,4 @@ value = 150, 74, 193
spellcheckline = 222, 61, 58
tagerror = 8, 145, 106
replacetag = 42, 162, 152
modifier = 224, 175, 5
+1
View File
@@ -40,3 +40,4 @@ value = 199, 146, 234
spellcheckline = 247, 140, 108
tagerror = 173, 219, 103
replacetag = 127, 219, 202
modifier = 236, 196, 141
+1
View File
@@ -40,3 +40,4 @@ value = 137, 89, 168
spellcheckline = 240, 40, 41
tagerror = 113, 140, 0
replacetag = 62, 153, 159
modifier = 245, 135, 31
@@ -40,3 +40,4 @@ value = 178, 148, 187
spellcheckline = 204, 102, 102
tagerror = 181, 189, 104
replacetag = 138, 190, 183
modifier = 222, 147, 95
@@ -40,3 +40,4 @@ value = 235, 187, 255
spellcheckline = 255, 157, 164
tagerror = 209, 241, 169
replacetag = 153, 255, 255
modifier = 255, 197, 143
@@ -40,3 +40,4 @@ value = 195, 151, 216
spellcheckline = 213, 78, 83
tagerror = 185, 202, 74
replacetag = 112, 192, 177
modifier = 231, 140, 69
@@ -40,3 +40,4 @@ value = 204, 153, 204
spellcheckline = 242, 119, 122
tagerror = 153, 204, 153
replacetag = 102, 204, 204
modifier = 249, 145, 57
+3 -2
View File
@@ -4,7 +4,7 @@ from nw.constants.constants import (
nwConst, nwFiles, nwKeyWords, nwLabels, nwDependencies, nwQuotes, nwUnicode
)
from nw.constants.enum import (
nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType
nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline
)
__all__ = [
@@ -22,4 +22,5 @@ __all__ = [
"nwItemClass",
"nwItemLayout",
"nwItemType",
]
"nwOutline",
]
+19 -1
View File
@@ -10,7 +10,7 @@
"""
from nw.constants.enum import nwItemClass, nwItemLayout
from nw.constants.enum import nwItemClass, nwItemLayout, nwOutline
class nwConst():
@@ -104,6 +104,24 @@ class nwLabels():
nwKeyWords.ENTITY_KEY : "Entities",
nwKeyWords.CUSTOM_KEY : "Custom",
}
OUTLINE_COLS = {
nwOutline.TITLE : "Title",
nwOutline.LEVEL : "Level",
nwOutline.LABEL : "Document",
nwOutline.LINE : "Line",
nwOutline.CCOUNT : "Chars",
nwOutline.WCOUNT : "Words",
nwOutline.PCOUNT : "Pars",
nwOutline.POV : "POV",
nwOutline.CHAR : KEY_NAME[nwKeyWords.CHAR_KEY],
nwOutline.PLOT : KEY_NAME[nwKeyWords.PLOT_KEY],
nwOutline.TIME : KEY_NAME[nwKeyWords.TIME_KEY],
nwOutline.WORLD : KEY_NAME[nwKeyWords.WORLD_KEY],
nwOutline.OBJECT : KEY_NAME[nwKeyWords.OBJECT_KEY],
nwOutline.ENTITY : KEY_NAME[nwKeyWords.ENTITY_KEY],
nwOutline.CUSTOM : KEY_NAME[nwKeyWords.CUSTOM_KEY],
nwOutline.SYNOP : "Synopsis",
}
# END Class nwLabels
+21
View File
@@ -88,3 +88,24 @@ class nwAlert(Enum):
BUG = 3
# END Enum nwAlert
class nwOutline(Enum):
TITLE = 0
LEVEL = 1
LABEL = 2
LINE = 3
CCOUNT = 4
WCOUNT = 5
PCOUNT = 6
POV = 7
CHAR = 8
PLOT = 9
TIME = 10
WORLD = 11
OBJECT = 12
ENTITY = 13
CUSTOM = 14
SYNOP = 15
# END Enum nwOutline
+2 -2
View File
@@ -15,7 +15,6 @@ from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor
from nw.gui.dialogs.projectload import GuiProjectLoad
from nw.gui.dialogs.sessionlog import GuiSessionLogView
from nw.gui.dialogs.timelineview import GuiTimeLineView
# GUI Elements
from nw.gui.elements.docdetails import GuiDocDetails
@@ -23,6 +22,7 @@ from nw.gui.elements.doceditor import GuiDocEditor
from nw.gui.elements.doctree import GuiDocTree
from nw.gui.elements.docviewer import GuiDocViewer
from nw.gui.elements.noticebar import GuiNoticeBar
from nw.gui.elements.outline import GuiProjectOutline
from nw.gui.elements.searchbar import GuiSearchBar
from nw.gui.elements.viewdetails import GuiDocViewDetails
@@ -43,12 +43,12 @@ __all__ = [
"GuiProjectEditor",
"GuiProjectLoad",
"GuiSessionLogView",
"GuiTimeLineView",
"GuiDocDetails",
"GuiDocEditor",
"GuiDocTree",
"GuiDocViewer",
"GuiNoticeBar",
"GuiProjectOutline",
"GuiSearchBar",
"GuiDocViewDetails",
"GuiDocHighlighter",
-2
View File
@@ -8,7 +8,6 @@ from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor
from nw.gui.dialogs.projectload import GuiProjectLoad
from nw.gui.dialogs.sessionlog import GuiSessionLogView
from nw.gui.dialogs.timelineview import GuiTimeLineView
__all__ = [
"GuiConfigEditor",
@@ -19,5 +18,4 @@ __all__ = [
"GuiProjectEditor",
"GuiProjectLoad",
"GuiSessionLogView",
"GuiTimeLineView",
]
+5 -5
View File
@@ -44,11 +44,11 @@ class GuiConfigEditor(QDialog):
self.setWindowTitle("Preferences")
self.guiDeco = self.theParent.theTheme.loadDecoration("settings",(64,64))
self.tabMain = GuiConfigEditGeneral(self.theParent)
self.tabEditor = GuiConfigEditEditor(self.theParent)
self.tabMain = GuiConfigEditGeneral(self.theParent)
self.tabEditor = GuiConfigEditEditor(self.theParent)
self.tabWidget = QTabWidget()
self.tabWidget.addTab(self.tabMain, "General")
self.tabWidget.addTab(self.tabMain, "General")
self.tabWidget.addTab(self.tabEditor, "Editor")
self.setLayout(self.outerBox)
@@ -79,11 +79,11 @@ class GuiConfigEditor(QDialog):
validEntries = True
needsRestart = False
retA, retB = self.tabMain.saveValues()
retA, retB = self.tabMain.saveValues()
validEntries &= retA
needsRestart |= retB
retA, retB = self.tabEditor.saveValues()
retA, retB = self.tabEditor.saveValues()
validEntries &= retA
needsRestart |= retB
+3 -3
View File
@@ -162,10 +162,10 @@ class GuiProjectEditStatus(QWidget):
self.colChanged = False
self.selColour = None
self.mainBox = QHBoxLayout()
self.mainForm = QVBoxLayout()
self.mainBox = QHBoxLayout()
self.mainForm = QVBoxLayout()
self.listBox = QListWidget()
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
self.listBox.itemSelectionChanged.connect(self._selectedItem)
self.listBox.model().rowsMoved.connect(self._rowsMoved)
-261
View File
@@ -1,261 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Timeline View
novelWriter GUI Timeline View
=================================
Class holding the timeline view window
File History:
Created: 2019-05-30 [0.1.4]
"""
import logging
import nw
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QPixmap
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QLabel,
QDialogButtonBox, QPushButton, QHeaderView, QGridLayout, QGroupBox,
QCheckBox
)
from nw.constants import nwFiles, nwItemClass
logger = logging.getLogger(__name__)
class GuiTimeLineView(QDialog):
def __init__(self, theParent, theProject, theIndex):
QDialog.__init__(self, theParent)
logger.debug("Initialising TimeLineView ...")
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.theIndex = theIndex
self.optState = self.theProject.optState
self.theMatrix = {}
self.numRows = 0
self.numCols = 0
self.outerBox = QVBoxLayout()
self.filterBox = QVBoxLayout()
self.centreBox = QHBoxLayout()
self.bottomBox = QHBoxLayout()
self.setWindowTitle("Timeline View")
self.setMinimumWidth(700)
self.setMinimumHeight(400)
winWidth = self.optState.validIntRange(
self.optState.getInt("GuiTimeLine", "winWidth", 700), 700, 10000, 700
)
winHeight = self.optState.validIntRange(
self.optState.getInt("GuiTimeLine", "winHeight", 400), 400, 10000, 400
)
self.resize(winWidth,winHeight)
# TimeLine Table
self.mainTable = QTableWidget()
self.mainTable.setGridStyle(Qt.NoPen)
self.hHeader = self.mainTable.horizontalHeader()
self.hHeader.setSectionResizeMode(QHeaderView.ResizeToContents)
self.mainTable.setHorizontalHeader(self.hHeader)
self.vHeader = self.mainTable.verticalHeader()
self.vHeader.setSectionResizeMode(QHeaderView.ResizeToContents)
self.mainTable.setVerticalHeader(self.vHeader)
# Option Box
self.optFilter = QGroupBox("Include Tags", self)
self.optFilterGrid = QGridLayout(self)
self.optFilter.setLayout(self.optFilterGrid)
self.filterPlot = QCheckBox("Plot tags", self)
self.filterPlot.setChecked(
self.optState.getBool("GuiTimeLine", "fPlot", True)
)
self.filterPlot.stateChanged.connect(self._filterChange)
self.filterChar = QCheckBox("Character tags", self)
self.filterChar.setChecked(
self.optState.getBool("GuiTimeLine", "fChar", True)
)
self.filterChar.stateChanged.connect(self._filterChange)
self.filterWorld = QCheckBox("Location tags", self)
self.filterWorld.setChecked(
self.optState.getBool("GuiTimeLine", "fWorld", True)
)
self.filterWorld.stateChanged.connect(self._filterChange)
self.filterTime = QCheckBox("Timeline tags", self)
self.filterTime.setChecked(
self.optState.getBool("GuiTimeLine", "fTime", True)
)
self.filterTime.stateChanged.connect(self._filterChange)
self.filterObject = QCheckBox("Object tags", self)
self.filterObject.setChecked(
self.optState.getBool("GuiTimeLine", "fObject", True)
)
self.filterObject.stateChanged.connect(self._filterChange)
self.filterCustom = QCheckBox("Custom tags", self)
self.filterCustom.setChecked(
self.optState.getBool("GuiTimeLine", "fCustom", True)
)
self.filterCustom.stateChanged.connect(self._filterChange)
self.optFilterGrid.addWidget(self.filterPlot, 0, 1)
self.optFilterGrid.addWidget(self.filterChar, 1, 1)
self.optFilterGrid.addWidget(self.filterWorld, 2, 1)
self.optFilterGrid.addWidget(self.filterTime, 3, 1)
self.optFilterGrid.addWidget(self.filterObject, 4, 1)
self.optFilterGrid.addWidget(self.filterCustom, 5, 1)
self.optHide = QGroupBox("Filters", self)
self.optHideGrid = QGridLayout(self)
self.optHide.setLayout(self.optHideGrid)
self.hideUnused = QCheckBox("Hide unused", self)
self.hideUnused.setChecked(
self.optState.getBool("GuiTimeLine", "hUnused", True)
)
self.hideUnused.stateChanged.connect(self._filterChange)
self.optHideGrid.addWidget(self.hideUnused, 0, 1)
# Button Box
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
self.buttonBox.rejected.connect(self._doClose)
self.btnRebuild = QPushButton("Rebuild Index")
self.btnRebuild.clicked.connect(self.theParent.rebuildIndex)
self.btnRefresh = QPushButton("Refresh Table")
self.btnRefresh.clicked.connect(self._buildNovelList)
self.bottomBox.addWidget(self.btnRebuild)
self.bottomBox.addWidget(self.btnRefresh)
self.bottomBox.addStretch()
self.bottomBox.addWidget(self.buttonBox)
# Assemble
self.filterBox.addWidget(self.optFilter)
self.filterBox.addWidget(self.optHide)
self.filterBox.addStretch()
self.centreBox.addWidget(self.mainTable)
self.centreBox.addLayout(self.filterBox)
self.outerBox.addLayout(self.centreBox)
self.outerBox.addLayout(self.bottomBox)
self.setLayout(self.outerBox)
self._buildNovelList()
self.buttonBox.setFocus()
self.show()
logger.debug("TimeLineView initialisation complete")
return
def _buildNovelList(self):
self.mainTable.clear()
self.theIndex.buildNovelList()
self.numRows = len(self.theIndex.novelList)
self.mainTable.setRowCount(self.numRows)
theFilters = {}
theFilters["exClass"] = []
theFilters["hUnused"] = self.hideUnused.isChecked()
if not self.filterPlot.isChecked():
theFilters["exClass"].append(nwItemClass.PLOT)
if not self.filterChar.isChecked():
theFilters["exClass"].append(nwItemClass.CHARACTER)
if not self.filterWorld.isChecked():
theFilters["exClass"].append(nwItemClass.WORLD)
if not self.filterTime.isChecked():
theFilters["exClass"].append(nwItemClass.TIMELINE)
if not self.filterObject.isChecked():
theFilters["exClass"].append(nwItemClass.OBJECT)
if not self.filterCustom.isChecked():
theFilters["exClass"].append(nwItemClass.CUSTOM)
for n in range(len(self.theIndex.novelList)):
iDepth = self.theIndex.novelList[n][1]
iTitle = self.theIndex.novelList[n][2]
newItem = QTableWidgetItem("%s%s " % (" "*iDepth,iTitle))
self.mainTable.setVerticalHeaderItem(n, newItem)
theMap = self.theIndex.buildTagNovelMap(self.theIndex.tagIndex.keys(), theFilters)
self.numCols = len(theMap.keys())
self.mainTable.setColumnCount(self.numCols)
nCol = 0
for theTag, theCols in theMap.items():
newItem = QTableWidgetItem(" %s " % theTag)
self.mainTable.setHorizontalHeaderItem(nCol, newItem)
for n in range(len(theCols)):
if theCols[n] == 1:
pxNew = QPixmap(10,10)
pxNew.fill(QColor(0,120,0))
lblNew = QLabel()
lblNew.setPixmap(pxNew)
lblNew.setAlignment(Qt.AlignCenter)
lblNew.setAttribute(Qt.WA_TranslucentBackground)
self.mainTable.setCellWidget(n, nCol, lblNew)
elif theCols[n] == 2:
pxNew = QPixmap(10,10)
pxNew.fill(QColor(0,0,120))
lblNew = QLabel()
lblNew.setPixmap(pxNew)
lblNew.setAlignment(Qt.AlignCenter)
lblNew.setAttribute(Qt.WA_TranslucentBackground)
self.mainTable.setCellWidget(n, nCol, lblNew)
nCol += 1
return
def _doClose(self):
logger.verbose("GuiTimeLineView close button clicked")
winWidth = self.width()
winHeight = self.height()
fPlot = self.filterPlot.isChecked()
fChar = self.filterChar.isChecked()
fWorld = self.filterWorld.isChecked()
fTime = self.filterTime.isChecked()
fObject = self.filterObject.isChecked()
fCustom = self.filterCustom.isChecked()
hUnused = self.hideUnused.isChecked()
self.optState.setValue("GuiTimeLine", "winWidth", winWidth)
self.optState.setValue("GuiTimeLine", "winHeight", winHeight)
self.optState.setValue("GuiTimeLine", "fPlot", fPlot)
self.optState.setValue("GuiTimeLine", "fChar", fChar)
self.optState.setValue("GuiTimeLine", "fWorld", fWorld)
self.optState.setValue("GuiTimeLine", "fTime", fTime)
self.optState.setValue("GuiTimeLine", "fObject", fObject)
self.optState.setValue("GuiTimeLine", "fCustom", fCustom)
self.optState.setValue("GuiTimeLine", "hUnused", hUnused)
self.optState.saveSettings()
self.close()
return
def _filterChange(self, checkState):
self._buildNovelList()
return
# END Class GuiTimeLineView
+2
View File
@@ -5,6 +5,7 @@ from nw.gui.elements.doceditor import GuiDocEditor
from nw.gui.elements.doctree import GuiDocTree
from nw.gui.elements.docviewer import GuiDocViewer
from nw.gui.elements.noticebar import GuiNoticeBar
from nw.gui.elements.outline import GuiProjectOutline
from nw.gui.elements.searchbar import GuiSearchBar
from nw.gui.elements.viewdetails import GuiDocViewDetails
@@ -14,6 +15,7 @@ __all__ = [
"GuiDocTree",
"GuiDocViewer",
"GuiNoticeBar",
"GuiProjectOutline",
"GuiSearchBar",
"GuiDocViewDetails",
]
+446
View File
@@ -0,0 +1,446 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Project Outline
novelWriter GUI Project Outline
===================================
Class holding the project outline view
File History:
Created: 2019-11-16 [0.4.1]
"""
import logging
import nw
from time import time
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QMenu, QAction, QAbstractItemView
)
from nw.constants import nwKeyWords, nwLabels, nwOutline
logger = logging.getLogger(__name__)
class GuiProjectOutline(QTreeWidget):
DEF_WIDTH = {
nwOutline.TITLE : 200,
nwOutline.LEVEL : 40,
nwOutline.LABEL : 150,
nwOutline.LINE : 40,
nwOutline.CCOUNT : 50,
nwOutline.WCOUNT : 50,
nwOutline.PCOUNT : 50,
nwOutline.POV : 100,
nwOutline.CHAR : 100,
nwOutline.PLOT : 100,
nwOutline.TIME : 100,
nwOutline.WORLD : 100,
nwOutline.OBJECT : 100,
nwOutline.ENTITY : 100,
nwOutline.CUSTOM : 100,
nwOutline.SYNOP : 200,
}
DEF_HIDDEN = {
nwOutline.TITLE : False,
nwOutline.LEVEL : True,
nwOutline.LABEL : False,
nwOutline.LINE : True,
nwOutline.CCOUNT : True,
nwOutline.WCOUNT : False,
nwOutline.PCOUNT : False,
nwOutline.POV : False,
nwOutline.CHAR : False,
nwOutline.PLOT : False,
nwOutline.TIME : True,
nwOutline.WORLD : False,
nwOutline.OBJECT : True,
nwOutline.ENTITY : True,
nwOutline.CUSTOM : True,
nwOutline.SYNOP : False,
}
def __init__(self, theParent, theProject):
QTreeWidget.__init__(self, theParent)
logger.debug("Initialising ProjectOutline ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.theIndex = self.theParent.theIndex
self.optState = self.theProject.optState
self.headerMenu = GuiOutlineHeaderMenu(self)
self.firstView = True
self.lastBuild = 0
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setExpandsOnDoubleClick(False)
self.setDragEnabled(False)
self.itemDoubleClicked.connect(self._treeDoubleClick)
self.treeHead = self.header()
self.treeHead.setContextMenuPolicy(Qt.CustomContextMenu)
self.treeHead.customContextMenuRequested.connect(self._headerRightClick)
self.treeHead.sectionMoved.connect(self._columnMoved)
self.treeMap = {}
self.treeOrder = []
self.colWidth = {}
self.colHidden = {}
self.colIndex = {}
self.treeNCols = 0
self.initOutline()
self.headerMenu.setHiddenState(self.colHidden)
logger.debug("ProjectOutline initialisation complete")
return
def initOutline(self):
"""Set the default values for the Outline tree.
"""
self.treeOrder = []
self.colWidth = {}
self.colHidden = {}
self.colIndex = {}
self.treeNCols = 0
for hItem in nwOutline:
self.treeOrder.append(hItem)
self.colWidth[hItem] = self.DEF_WIDTH[hItem]
self.colHidden[hItem] = self.DEF_HIDDEN[hItem]
self.treeNCols = len(self.treeOrder)
return
def refreshTree(self, overRide=False):
"""Called whenever the Outline tab is activated and controls
what data to load, and if necessary, force a rebuild of the
tree.
"""
# If it's the first time, we always build
if self.firstView or overRide:
self._loadHeaderState()
self._populateTree()
self.firstView = False
return
# If the novel index has changed since the tree was last built,
# we rebuild the tree from the updated index.
lastChange = self.theParent.theIndex.timeNovel
logger.verbose("Last outline build: %.3f" % self.lastBuild)
logger.verbose("Novel index change: %.3f" % lastChange)
doBuild = lastChange > self.lastBuild and self.theProject.autoOutline
if doBuild or overRide:
logger.debug("Rebuilding Project Outline")
self._populateTree()
return
def closeOutline(self):
"""Called before a project is closed.
"""
self._saveHeaderState()
self.clear()
self.firstView = True
return
##
# Slots
##
def _treeDoubleClick(self, tItem, tCol):
print(tItem, tCol)
return
def _headerRightClick(self, clickPos):
"""Show the header column menu.
"""
self.headerMenu.exec_(self.mapToGlobal(clickPos))
return
def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx):
"""Make sure the order array is up to date with the actual order
of the columns.
"""
self.treeOrder.insert(newVisualIdx, self.treeOrder.pop(oldVisualIdx))
return
def _menuColumnToggled(self, isChecked, theItem):
"""Receive the changes to column visibility forwarded by the
header context menu.
"""
logger.verbose("User toggled Outline column '%s'" % theItem.name)
if theItem in self.colIndex:
self.setColumnHidden(self.colIndex[theItem], not isChecked)
return
##
# Internal Functions
##
def _loadHeaderState(self):
"""Load the state of the main tree header, that is, column order
and column width.
"""
# Load whatever we saved last time, regardless of wether it
# contains the correct names or number of columns. The names
# must be valid though.
tempOrder = self.optState.getValue("GuiProjectOutline", "headerOrder", [])
treeOrder = []
for hName in tempOrder:
try:
treeOrder.append(nwOutline[hName])
except:
logger.warning("Ignored unknown outline column '%s'" % str(hName))
# Add columns that was not in the file to the treeOrder array.
for hItem in nwOutline:
if hItem not in treeOrder:
treeOrder.append(hItem)
# Check that we now have a complete list, and only if so, save
# the order loaded from file. Otherwise, we keep the default.
if len(treeOrder) == self.treeNCols:
self.treeOrder = treeOrder
else:
logger.error("Failed to extract outline column order from previous session")
logger.error("Column count doesn't match %d != %d" % (len(treeOrder), self.treeNCols))
# We load whatever column widths and hidden states we find in
# the file, and leave the rest in their default state.
tmpWidth = self.optState.getValue("GuiProjectOutline", "columnWidth", {})
for hName in tmpWidth:
try:
self.colWidth[nwOutline[hName]] = tmpWidth[hName]
except:
logger.warning("Ignored unknown outline column '%s'" % str(hName))
tmpHidden = self.optState.getValue("GuiProjectOutline", "columnHidden", {})
for hName in tmpHidden:
try:
self.colHidden[nwOutline[hName]] = tmpHidden[hName]
except:
logger.warning("Ignored unknown outline column '%s'" % str(hName))
self.headerMenu.setHiddenState(self.colHidden)
return
def _saveHeaderState(self):
"""Save the state of the main tree header, that is, column
order, column width and column hidden state. We don't want to
save the current width of hidden columns though. This preserves
the last known width in case they're unhidden again.
"""
# If we haven't built the tree, there is nothing to save.
if self.lastBuild == 0:
return
treeOrder = []
colWidth = {}
colHidden = {}
for hItem in nwOutline:
colWidth[hItem.name] = self.colWidth[hItem]
colHidden[hItem.name] = self.colHidden[hItem]
for iCol in range(self.columnCount()):
hName = self.treeOrder[iCol].name
treeOrder.append(hName)
iLog = self.treeHead.logicalIndex(iCol)
logWidth = self.columnWidth(iLog)
logHidden = self.isColumnHidden(iLog)
colHidden[hName] = logHidden
if not logHidden and logWidth > 0:
colWidth[hName] = logWidth
self.optState.setValue("GuiProjectOutline", "headerOrder", treeOrder)
self.optState.setValue("GuiProjectOutline", "columnWidth", colWidth)
self.optState.setValue("GuiProjectOutline", "columnHidden", colHidden)
self.optState.saveSettings()
return
def _populateTree(self):
"""Build the tree based on the project index.
"""
theLabels = []
for i, hItem in enumerate(self.treeOrder):
theLabels.append(nwLabels.OUTLINE_COLS[hItem])
self.colIndex[hItem] = i
self.clear()
self.setHeaderLabels(theLabels)
for hItem in self.treeOrder:
self.setColumnWidth(self.colIndex[hItem], self.colWidth[hItem])
self.setColumnHidden(self.colIndex[hItem], self.colHidden[hItem])
headItem = self.headerItem()
headItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight)
currTitle = None
currChapter = None
currScene = None
for titleKey in self.theIndex.getNovelStructure():
if len(titleKey) < 16:
continue
tHandle = titleKey[:13]
sTitle = titleKey[14:]
if tHandle not in self.theIndex.novelIndex:
continue
if sTitle not in self.theIndex.novelIndex[tHandle]:
continue
tLevel = self.theIndex.novelIndex[tHandle][sTitle]["level"]
tTime = self.theIndex.novelIndex[tHandle][sTitle]["updated"]
tItem = self._createTreeItem(tHandle, sTitle)
self.treeMap[titleKey] = tItem
if tLevel == "H1":
currTitle = tItem
self.addTopLevelItem(tItem)
elif tLevel == "H2":
if currTitle is None:
self.addTopLevelItem(tItem)
else:
currTitle.addChild(tItem)
currChapter = tItem
elif tLevel == "H3":
if currChapter is None:
if currTitle is None:
self.addTopLevelItem(tItem)
else:
currTitle.addChild(tItem)
else:
currChapter.addChild(tItem)
currScene = tItem
elif tLevel == "H4":
if currScene is None:
if currChapter is None:
if currTitle is None:
self.addTopLevelItem(tItem)
else:
currTitle.addChild(tItem)
else:
currChapter.addChild(tItem)
else:
currScene.addChild(tItem)
tItem.setExpanded(True)
self.lastBuild = time()
return
def _createTreeItem(self, tHandle, sTitle):
"""Populate a tree item with all the column values.
"""
nwItem = self.theProject.getItem(tHandle)
novIdx = self.theIndex.novelIndex[tHandle][sTitle]
newItem = QTreeWidgetItem()
newItem.setText(self.colIndex[nwOutline.TITLE], novIdx["title"])
newItem.setText(self.colIndex[nwOutline.LEVEL], novIdx["level"])
newItem.setText(self.colIndex[nwOutline.LABEL], nwItem.itemName)
newItem.setText(self.colIndex[nwOutline.LINE], sTitle[1:])
newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"])
newItem.setText(self.colIndex[nwOutline.CCOUNT], str(novIdx["cCount"]))
newItem.setText(self.colIndex[nwOutline.WCOUNT], str(novIdx["wCount"]))
newItem.setText(self.colIndex[nwOutline.PCOUNT], str(novIdx["pCount"]))
newItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight)
theRefs = self.theIndex.getReferences(tHandle, sTitle)
newItem.setText(self.colIndex[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY]))
newItem.setText(self.colIndex[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
newItem.setText(self.colIndex[nwOutline.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY]))
newItem.setText(self.colIndex[nwOutline.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY]))
newItem.setText(self.colIndex[nwOutline.WORLD], ", ".join(theRefs[nwKeyWords.WORLD_KEY]))
newItem.setText(self.colIndex[nwOutline.OBJECT], ", ".join(theRefs[nwKeyWords.OBJECT_KEY]))
newItem.setText(self.colIndex[nwOutline.ENTITY], ", ".join(theRefs[nwKeyWords.ENTITY_KEY]))
newItem.setText(self.colIndex[nwOutline.CUSTOM], ", ".join(theRefs[nwKeyWords.CUSTOM_KEY]))
return newItem
# END Class GuiProjectOutline
class GuiOutlineHeaderMenu(QMenu):
def __init__(self, theParent):
QMenu.__init__(self, theParent)
self.theParent = theParent
self.acceptToggle = True
mnuHead = QAction("Select Columns", self)
self.addAction(mnuHead)
self.addSeparator()
self.actionMap = {}
for hItem in nwOutline:
if hItem == nwOutline.TITLE:
continue
self.actionMap[hItem] = QAction(nwLabels.OUTLINE_COLS[hItem], self)
self.actionMap[hItem].setCheckable(True)
self.actionMap[hItem].toggled.connect(
lambda isChecked, tItem=hItem : self._columnToggled(isChecked, tItem)
)
self.addAction(self.actionMap[hItem])
return
def setHiddenState(self, hiddenState):
"""Overwrite the checked state of the columns as the inverse of
the hidden state. Skip the TITLE column as it cannot be hidden.
"""
self.acceptToggle = False
for hItem in nwOutline:
if hItem == nwOutline.TITLE or hItem not in hiddenState:
continue
self.actionMap[hItem].setChecked(not hiddenState[hItem])
self.acceptToggle = True
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.
"""
if self.acceptToggle:
self.theParent._menuColumnToggled(isChecked, theItem)
return
# END Class GuiOutlineHeaderMenu
+1 -1
View File
@@ -85,7 +85,7 @@ class GuiDocViewDetails(QWidget):
if self.isSticky.isChecked():
return
theRefs = self.theParent.theIndex.buildReferenceList(tHandle)
theRefs = self.theParent.theIndex.getBackReferenceList(tHandle)
theList = []
for tHandle in theRefs:
tItem = self.theProject.getItem(tHandle)
+31 -10
View File
@@ -69,6 +69,13 @@ class GuiMainMenu(QMenuBar):
self.aSpellCheck.setChecked(theMode)
return
def setAutoOutline(self, theMode):
"""Set the auto outline check box to theMode. Used during
initialisation.
"""
self.aAutoOutline.setChecked(theMode)
return
##
# Menu Action
##
@@ -85,6 +92,12 @@ class GuiMainMenu(QMenuBar):
self.theParent.docEditor.setSpellCheck(None)
return True
def _toggleAutoOutline(self, theMode):
"""Toggle auto outline when the menu entry is checked.
"""
self.theProject.setAutoOutline(theMode)
return True
def _toggleViewComments(self):
self.mainConf.setViewComments(self.aViewDocComments.isChecked())
self.theParent.docViewer.reloadText()
@@ -396,16 +409,6 @@ class GuiMainMenu(QMenuBar):
self.aFullScreen.triggered.connect(self.theParent.toggleFullScreenMode)
self.viewMenu.addAction(self.aFullScreen)
# View > Separator
self.viewMenu.addSeparator()
# View > Project Timeline
self.aViewTimeLine = QAction("Show Project Timeline", self)
self.aViewTimeLine.setStatusTip("Open the project timeline window")
self.aViewTimeLine.setShortcut("Ctrl+T")
self.aViewTimeLine.triggered.connect(self.theParent.showTimeLineDialog)
self.viewMenu.addAction(self.aViewTimeLine)
return
def _buildEditMenu(self):
@@ -656,6 +659,24 @@ class GuiMainMenu(QMenuBar):
self.aRebuildIndex.triggered.connect(self.theParent.rebuildIndex)
self.toolsMenu.addAction(self.aRebuildIndex)
# Tools > Rebuild Outline
self.aRebuildOutline = QAction("Rebuild Outline", self)
self.aRebuildOutline.setStatusTip("Rebuild the novel outline tree")
self.aRebuildOutline.setShortcut("F10")
self.aRebuildOutline.triggered.connect(self.theParent.rebuildOutline)
self.toolsMenu.addAction(self.aRebuildOutline)
# Tools > Toggle Auto Build Outline
self.aAutoOutline = QAction("Auto-Update Outline", self)
self.aAutoOutline.setStatusTip("Update project outline when a novel file is changed")
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()
# Tools > Backup
self.aBackupProject = QAction("Backup Project", self)
self.aBackupProject.setStatusTip("Backup Project")
+2
View File
@@ -78,6 +78,7 @@ class GuiTheme:
self.colSpell = [ 0, 0, 0]
self.colTagErr = [ 0, 0, 0]
self.colRepTag = [ 0, 0, 0]
self.colMod = [ 0, 0, 0]
# Changeable Settings
self.guiTheme = None
@@ -217,6 +218,7 @@ class GuiTheme:
self.colSpell = self._loadColour(confParser, cnfSec, "spellcheckline")
self.colTagErr = self._loadColour(confParser, cnfSec, "tagerror")
self.colRepTag = self._loadColour(confParser, cnfSec, "replacetag")
self.colMod = self._loadColour(confParser, cnfSec, "modifier")
logger.info("Loaded syntax theme '%s'" % self.guiSyntax)
+7 -1
View File
@@ -75,6 +75,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.colSpell = QColor(*self.theTheme.colSpell)
self.colTagErr = QColor(*self.theTheme.colTagErr)
self.colRepTag = QColor(*self.theTheme.colRepTag)
self.colMod = QColor(*self.theTheme.colMod)
self.colTrail = QColor(*self.theTheme.colEmph,64)
self.hStyles = {
@@ -98,6 +99,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
"replace" : self._makeFormat(self.colRepTag),
"hidden" : self._makeFormat(self.colComm),
"keyword" : self._makeFormat(self.colKey),
"modifier" : self._makeFormat(self.colMod),
"value" : self._makeFormat(self.colVal),
}
@@ -250,7 +252,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setFormat(4, len(theText), self.hStyles["header4"])
elif theText.startswith("%"): # Comments
self.setFormat(0, len(theText), self.hStyles["hidden"])
if theText.startswith("%synopsis:"):
self.setFormat(0, 10, self.hStyles["modifier"])
self.setFormat(10, len(theText), self.hStyles["hidden"])
else:
self.setFormat(0, len(theText), self.hStyles["hidden"])
else: # Text Paragraph
for rX, xFmt in self.rxRules:
+56 -18
View File
@@ -21,13 +21,13 @@ from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence
from PyQt5.QtWidgets import (
qApp, QMainWindow, QVBoxLayout, QFrame, QSplitter, QFileDialog, QShortcut,
QMessageBox, QProgressDialog, QDialog
QMessageBox, QProgressDialog, QDialog, QTabWidget
)
from nw.gui import (
GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiExport,
GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails,
GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiTimeLineView,
GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiProjectOutline,
GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad
)
from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup
@@ -76,6 +76,7 @@ class GuiMain(QMainWindow):
self.searchBar = GuiSearchBar(self)
self.treeMeta = GuiDocDetails(self, self.theProject)
self.treeView = GuiDocTree(self, self.theProject)
self.projView = GuiProjectOutline(self, self.theProject)
self.mainMenu = GuiMainMenu(self, self.theProject)
# Minor Gui Elements
@@ -111,20 +112,33 @@ class GuiMain(QMainWindow):
self.splitView.addWidget(self.editPane)
self.splitView.addWidget(self.viewPane)
self.splitOutline = QSplitter(Qt.Vertical)
self.splitOutline.addWidget(self.projView)
self.tabWidget = QTabWidget()
self.tabWidget.setTabPosition(QTabWidget.East)
self.tabWidget.setStyleSheet("QTabWidget::pane {border: 0;}")
self.tabWidget.addTab(self.splitView, "Editor")
self.tabWidget.addTab(self.splitOutline, "Outline")
self.tabWidget.currentChanged.connect(self._mainTabChanged)
self.splitMain = QSplitter(Qt.Horizontal)
self.splitMain.setContentsMargins(4,4,4,4)
self.splitMain.setOpaqueResize(False)
self.splitMain.addWidget(self.treePane)
self.splitMain.addWidget(self.splitView)
self.splitMain.addWidget(self.tabWidget)
self.splitMain.setSizes(self.mainConf.mainPanePos)
self.setCentralWidget(self.splitMain)
self.idxTree = self.splitMain.indexOf(self.treePane)
self.idxMain = self.splitMain.indexOf(self.splitView)
self.idxMain = self.splitMain.indexOf(self.tabWidget)
self.idxEditor = self.splitView.indexOf(self.editPane)
self.idxViewer = self.splitView.indexOf(self.viewPane)
self.idxTabEdit = self.tabWidget.indexOf(self.splitView)
self.idxTabProj = self.tabWidget.indexOf(self.splitOutline)
self.splitMain.setCollapsible(self.idxTree, False)
self.splitMain.setCollapsible(self.idxMain, False)
self.splitView.setCollapsible(self.idxEditor, False)
@@ -296,6 +310,7 @@ class GuiMain(QMainWindow):
if saveOK:
self.closeDocument()
self.projView.closeOutline()
self.theProject.closeProject()
self.theIndex.clearIndex()
self.clearGUI()
@@ -318,6 +333,9 @@ class GuiMain(QMainWindow):
if not self.closeProject():
return False
# Switch main tab to editor view
self.tabWidget.setCurrentWidget(self.splitView)
# Try to open the project
if not self.theProject.openProject(projFile):
if self.theProject.lockedBy is not None:
@@ -368,6 +386,7 @@ class GuiMain(QMainWindow):
self.rebuildTree()
self.docEditor.setDictionaries()
self.docEditor.setSpellCheck(self.theProject.spellCheck)
self.mainMenu.setAutoOutline(self.theProject.autoOutline)
self.statusBar.setRefTime(self.theProject.projOpened)
# Restore previously open documents, if any
@@ -420,6 +439,7 @@ class GuiMain(QMainWindow):
def openDocument(self, tHandle):
if self.hasProject:
self.closeDocument()
self.tabWidget.setCurrentWidget(self.splitView)
if self.docEditor.loadText(tHandle):
self.docEditor.setFocus()
self.theProject.setLastEdited(tHandle)
@@ -446,6 +466,9 @@ class GuiMain(QMainWindow):
logger.debug("No document selected, giving up")
return False
# Make sure main tab is in Editor view
self.tabWidget.setCurrentWidget(self.splitView)
if self.docViewer.loadText(tHandle) and not self.viewPane.isVisible():
bPos = self.splitMain.sizes()
self.viewPane.setVisible(True)
@@ -615,17 +638,17 @@ class GuiMain(QMainWindow):
theDoc = NWDoc(self.theProject, self)
theText = theDoc.openDocument(tHandle, False)
# Run Word Count
cC, wC, pC = countWords(theText)
# Build tag index
self.theIndex.scanText(tHandle, theText)
# Get Word Counts
cC, wC, pC = self.theIndex.getCounts(tHandle)
tItem.setCharCount(cC)
tItem.setWordCount(wC)
tItem.setParaCount(pC)
self.treeView.propagateCount(tHandle, wC)
self.treeView.projectWordCount()
# Build tag index
self.theIndex.scanText(tHandle, theText)
nDone += 1
if dlgProg.wasCanceled():
break
@@ -634,6 +657,14 @@ class GuiMain(QMainWindow):
return True
def rebuildOutline(self):
"""Force a rebuild of the Outline view.
"""
logger.verbose("Forcing a rebuild of the Project Outline")
self.tabWidget.setCurrentWidget(self.splitOutline)
self.projView.refreshTree(overRide=True)
return True
##
# Main Dialogs
##
@@ -696,12 +727,6 @@ class GuiMain(QMainWindow):
dlgExport.exec_()
return True
def showTimeLineDialog(self):
if self.hasProject:
dlgTLine = GuiTimeLineView(self, self.theProject, self.theIndex)
dlgTLine.exec_()
return True
def showSessionLogDialog(self):
if self.hasProject:
dlgTLine = GuiSessionLogView(self, self.theProject)
@@ -767,7 +792,8 @@ class GuiMain(QMainWindow):
return False
logger.info("Exiting %s" % nw.__package__)
self.closeProject(True)
if self.hasProject:
self.closeProject(True)
self.mainConf.setTreeColWidths(self.treeView.getColumnSizes())
if not self.mainConf.isFullScreen:
@@ -817,6 +843,7 @@ class GuiMain(QMainWindow):
self.isZenMode = not self.isZenMode
if self.isZenMode:
logger.debug("Activating Zen mode")
self.tabWidget.setCurrentWidget(self.splitView)
else:
logger.debug("Deactivating Zen mode")
@@ -824,6 +851,7 @@ class GuiMain(QMainWindow):
self.treePane.setVisible(isVisible)
self.statusBar.setVisible(isVisible)
self.mainMenu.setVisible(isVisible)
self.tabWidget.tabBar().setVisible(isVisible)
if self.viewPane.isVisible():
self.viewPane.setVisible(False)
@@ -866,7 +894,6 @@ class GuiMain(QMainWindow):
self.addAction(self.mainMenu.aFileDetails)
self.addAction(self.mainMenu.aZenMode)
self.addAction(self.mainMenu.aFullScreen)
self.addAction(self.mainMenu.aViewTimeLine)
self.addAction(self.mainMenu.aEditUndo)
self.addAction(self.mainMenu.aEditRedo)
self.addAction(self.mainMenu.aEditCut)
@@ -972,7 +999,7 @@ class GuiMain(QMainWindow):
def _keyPressEscape(self):
"""When the escape key is pressed somewhere in the main window,
do the following, in order.
do the following, in order:
"""
if self.searchBar.isVisible():
self.searchBar.setVisible(False)
@@ -981,4 +1008,15 @@ class GuiMain(QMainWindow):
self.toggleZenMode()
return
def _mainTabChanged(self, tabIndex):
"""Activated when the main window tab is changed.
"""
if tabIndex == self.idxTabEdit:
logger.verbose("Editor tab activated")
elif tabIndex == self.idxTabProj:
logger.verbose("Project outline tab activated")
if self.hasProject:
self.projView.refreshTree()
return
# END Class GuiMain
+252 -114
View File
@@ -15,10 +15,12 @@ import json
import nw
from os import path
from time import time
from nw.constants import (
nwFiles, nwKeyWords, nwItemType, nwItemClass, nwAlert
nwFiles, nwKeyWords, nwItemType, nwItemClass, nwItemLayout, nwAlert
)
from nw.tools import countWords
logger = logging.getLogger(__name__)
@@ -36,14 +38,14 @@ class NWIndex():
nwKeyWords.CUSTOM_KEY
]
TAG_CLASS = {
nwKeyWords.CHAR_KEY : [nwItemClass.CHARACTER, 1],
nwKeyWords.POV_KEY : [nwItemClass.CHARACTER, 2],
nwKeyWords.PLOT_KEY : [nwItemClass.PLOT, 1],
nwKeyWords.TIME_KEY : [nwItemClass.TIMELINE, 1],
nwKeyWords.WORLD_KEY : [nwItemClass.WORLD, 1],
nwKeyWords.OBJECT_KEY : [nwItemClass.OBJECT, 1],
nwKeyWords.ENTITY_KEY : [nwItemClass.ENTITY, 1],
nwKeyWords.CUSTOM_KEY : [nwItemClass.CUSTOM, 1],
nwKeyWords.CHAR_KEY : nwItemClass.CHARACTER,
nwKeyWords.POV_KEY : nwItemClass.CHARACTER,
nwKeyWords.PLOT_KEY : nwItemClass.PLOT,
nwKeyWords.TIME_KEY : nwItemClass.TIMELINE,
nwKeyWords.WORLD_KEY : nwItemClass.WORLD,
nwKeyWords.OBJECT_KEY : nwItemClass.OBJECT,
nwKeyWords.ENTITY_KEY : nwItemClass.ENTITY,
nwKeyWords.CUSTOM_KEY : nwItemClass.CUSTOM,
}
def __init__(self, theProject, theParent):
@@ -55,13 +57,18 @@ class NWIndex():
self.indexBroken = False
# Indices
self.tagIndex = {}
self.refIndex = {}
self.novelIndex = {}
self.noteIndex = {}
self.tagIndex = None
self.refIndex = None
self.novelIndex = None
self.noteIndex = None
self.textCounts = None
# Lists
self.novelList = []
# TimeStamps
self.timeNovel = 0
self.timeNote = 0
self.timeIndex = 0
self.clearIndex()
return
@@ -70,13 +77,21 @@ class NWIndex():
##
def clearIndex(self):
"""Clear the index dictionaries and time stamps.
"""
self.tagIndex = {}
self.refIndex = {}
self.novelIndex = {}
self.noteIndex = {}
self.textCounts = {}
self.timeNovel = 0
self.timeNote = 0
self.timeIndex = 0
return
def deleteHandle(self, tHandle):
"""Delete all entries of a given document handle.
"""
delTags = []
for tTag in self.tagIndex:
@@ -89,6 +104,7 @@ class NWIndex():
self.refIndex.pop(tHandle, None)
self.novelIndex.pop(tHandle, None)
self.noteIndex.pop(tHandle, None)
self.textCounts.pop(tHandle, None)
return
@@ -100,8 +116,9 @@ class NWIndex():
"""Load index from last session from the project meta folder.
"""
theData = {}
theData = {}
indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
if path.isfile(indexFile):
logger.debug("Loading index file")
try:
@@ -121,24 +138,31 @@ class NWIndex():
self.novelIndex = theData["novelIndex"]
if "noteIndex" in theData.keys():
self.noteIndex = theData["noteIndex"]
if "textCounts" in theData.keys():
self.textCounts = theData["textCounts"]
self.checkIndex()
nowTime = time()
self.timeNovel = nowTime
self.timeNote = nowTime
self.timeIndex = nowTime
return True
self.checkIndex()
return False
return True
def saveIndex(self):
"""Save the current index as a json file in the project meta
folder.
data folder.
"""
indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
logger.debug("Saving index file")
if self.mainConf.debugInfo:
nIndent = 2
else:
nIndent = None
try:
with open(indexFile,mode="w+",encoding="utf8") as outFile:
outFile.write(json.dumps({
@@ -146,6 +170,7 @@ class NWIndex():
"refIndex" : self.refIndex,
"novelIndex" : self.novelIndex,
"noteIndex" : self.noteIndex,
"textCounts" : self.textCounts,
}, indent=nIndent))
except Exception as e:
logger.error("Failed to save index file")
@@ -161,29 +186,38 @@ class NWIndex():
self.indexBroken = False
for tTag in self.tagIndex:
if len(self.tagIndex[tTag]) != 3:
self.indexBroken = True
for tHandle in self.refIndex:
for tEntry in self.refIndex[tHandle]:
if len(tEntry) != 4:
try:
for tTag in self.tagIndex:
if len(self.tagIndex[tTag]) != 3:
self.indexBroken = True
for tHandle in self.novelIndex:
for tEntry in self.novelIndex[tHandle]:
if len(tEntry) != 4:
for tHandle in self.refIndex:
for sTitle in self.refIndex[tHandle]:
for tEntry in self.refIndex[tHandle][sTitle]["tags"]:
if len(tEntry) != 3:
self.indexBroken = True
for tHandle in self.novelIndex:
for sLine in self.novelIndex[tHandle]:
if len(self.novelIndex[tHandle][sLine].keys()) != 8:
self.indexBroken = True
for tHandle in self.noteIndex:
for sLine in self.noteIndex[tHandle]:
if len(self.noteIndex[tHandle][sLine].keys()) != 8:
self.indexBroken = True
for tHandle in self.textCounts:
if len(self.textCounts[tHandle]) != 3:
self.indexBroken = True
for tHandle in self.noteIndex:
for tEntry in self.noteIndex[tHandle]:
if len(tEntry) != 4:
self.indexBroken = True
except:
self.indexBroken = True
if self.indexBroken:
self.clearIndex()
self.theParent.makeAlert(
"The project index loaded from cache contains errors. Triggering Rebuild Index.",
"The index loaded from project cache contains errors. Rebuilding index.",
nwAlert.WARN
)
@@ -201,23 +235,33 @@ class NWIndex():
"""
theItem = self.theProject.getItem(tHandle)
if theItem is None: return False
if theItem.itemType != nwItemType.FILE: return False
if theItem.parHandle == self.theProject.trashRoot: return False
if theItem is None:
return False
if theItem.itemType != nwItemType.FILE:
return False
if theItem.parHandle == self.theProject.trashRoot:
return False
if theItem.itemLayout == nwItemLayout.NO_LAYOUT:
return False
itemClass = theItem.itemClass
itemLayout = theItem.itemLayout
logger.debug("Indexing item with handle %s" % tHandle)
# Check file type, and reset its old index
if itemClass == nwItemClass.NOVEL:
self.novelIndex[tHandle] = []
self.refIndex[tHandle] = []
isNovel = True
else:
self.noteIndex[tHandle] = []
self.refIndex[tHandle] = []
# Also add an entry for T0 in case the file has no title
self.refIndex[tHandle] = {}
self.refIndex[tHandle]["T0"] = {
"tags" : [],
"updated" : time(),
}
if itemLayout == nwItemLayout.NOTE:
self.noteIndex[tHandle] = {}
isNovel = False
else:
self.novelIndex[tHandle] = {}
isNovel = True
# Also clear references to file in tag index
clearTags = []
@@ -229,19 +273,47 @@ class NWIndex():
nLine = 0
nTitle = 0
for aLine in theText.splitlines():
aLine = aLine.strip()
theLines = theText.splitlines()
for aLine in theLines:
aLine = aLine
nLine += 1
nChar = len(aLine)
if nChar == 0: continue
if aLine[0] == "#":
nChar = len(aLine.strip())
if nChar == 0:
continue
if aLine.startswith(r"#"):
isTitle = self.indexTitle(tHandle, isNovel, aLine, nLine, itemLayout)
if isTitle:
if isTitle and nLine > 0:
if nTitle > 0:
lastText = "\n".join(theLines[nTitle-1:nLine-1])
self.indexWordCounts(tHandle, isNovel, lastText, nTitle)
nTitle = nLine
elif aLine[0] == "@":
elif aLine.startswith(r"@"):
self.indexNoteRef(tHandle, aLine, nLine, nTitle)
self.indexTag(tHandle, aLine, nLine, itemClass)
elif aLine.startswith(r"%synopsis:"):
if nTitle > 0:
self.indexSynopsis(tHandle, isNovel, aLine[10:].strip(), nTitle)
# Count words for remaining text after last heading
if nTitle > 0:
lastText = "\n".join(theLines[nTitle-1:nLine-1])
self.indexWordCounts(tHandle, isNovel, lastText, nTitle)
# Run word counter for whole text
cC, wC, pC = countWords(theText)
self.textCounts[tHandle] = [cC, wC, pC]
# Update timestamps for index changes
nowTime = time()
self.timeIndex = nowTime
if isNovel:
self.timeNovel = nowTime
else:
self.timeNote = nowTime
return True
def indexTitle(self, tHandle, isNovel, aLine, nLine, itemLayout):
@@ -250,30 +322,79 @@ class NWIndex():
"""
if aLine.startswith("# "):
hDepth = 1
hDepth = "H1"
hText = aLine[2:].strip()
elif aLine.startswith("## "):
hDepth = 2
hDepth = "H2"
hText = aLine[3:].strip()
elif aLine.startswith("### "):
hDepth = 3
hDepth = "H3"
hText = aLine[4:].strip()
elif aLine.startswith("#### "):
hDepth = 4
hDepth = "H4"
hText = aLine[5:].strip()
else:
return False
sTitle = "T%d" % nLine
self.refIndex[tHandle][sTitle] = {
"tags" : [],
"updated" : time(),
}
theData = {
"level" : hDepth,
"title" : hText,
"layout" : itemLayout.name,
"synopsis" : "",
"cCount" : 0,
"wCount" : 0,
"pCount" : 0,
"updated" : time(),
}
if hText != "":
if isNovel:
if tHandle in self.novelIndex:
self.novelIndex[tHandle].append([nLine, hDepth, hText, itemLayout.name])
self.novelIndex[tHandle][sTitle] = theData
else:
if tHandle in self.noteIndex:
self.noteIndex[tHandle].append([nLine, hDepth, hText, itemLayout.name])
self.noteIndex[tHandle][sTitle] = theData
return True
def indexWordCounts(self, tHandle, isNovel, theText, nTitle):
cC, wC, pC = countWords(theText)
sTitle = "T%d" % nTitle
if isNovel:
if tHandle in self.novelIndex:
if sTitle in self.novelIndex[tHandle]:
self.novelIndex[tHandle][sTitle]["cCount"] = cC
self.novelIndex[tHandle][sTitle]["wCount"] = wC
self.novelIndex[tHandle][sTitle]["pCount"] = pC
self.novelIndex[tHandle][sTitle]["updated"] = time()
else:
if tHandle in self.noteIndex:
if sTitle in self.noteIndex[tHandle]:
self.noteIndex[tHandle][sTitle]["cCount"] = cC
self.noteIndex[tHandle][sTitle]["wCount"] = wC
self.noteIndex[tHandle][sTitle]["pCount"] = pC
self.noteIndex[tHandle][sTitle]["updated"] = time()
return
def indexSynopsis(self, tHandle, isNovel, theText, nTitle):
sTitle = "T%d" % nTitle
if isNovel:
if tHandle in self.novelIndex:
if sTitle in self.novelIndex[tHandle]:
self.novelIndex[tHandle][sTitle]["synopsis"] = theText
self.novelIndex[tHandle][sTitle]["updated"] = time()
else:
if tHandle in self.noteIndex:
if sTitle in self.noteIndex[tHandle]:
self.noteIndex[tHandle][sTitle]["synopsis"] = theText
self.noteIndex[tHandle][sTitle]["updated"] = time()
return
def indexNoteRef(self, tHandle, aLine, nLine, nTitle):
"""Validate and save the information about a reference to a tag
in another file.
@@ -283,9 +404,14 @@ class NWIndex():
if not isValid or len(theBits) == 0:
return False
sTitle = "T%d" % nTitle
if sTitle not in self.refIndex[tHandle]:
logger.error("Cannot save tags to file %s, no title %s" % (tHandle, sTitle))
return False
if theBits[0] != nwKeyWords.TAG_KEY:
for aVal in theBits[1:]:
self.refIndex[tHandle].append([nLine, theBits[0], aVal, nTitle])
self.refIndex[tHandle][sTitle]["tags"].append([nLine, theBits[0], aVal])
return True
@@ -378,7 +504,7 @@ class NWIndex():
# If we're still here, we better check that the references exist
for n in range(1,nBits):
if theBits[n] in self.tagIndex:
isGood[n] = self.TAG_CLASS[theBits[0]][0].name == self.tagIndex[theBits[n]][2]
isGood[n] = self.TAG_CLASS[theBits[0]].name == self.tagIndex[theBits[n]][2]
return isGood
@@ -386,20 +512,73 @@ class NWIndex():
# Extract Data
##
def buildNovelList(self):
"""Build a list of the content of the novel.
def getNovelStructure(self):
"""Builds a list of 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.
"""
self.novelList = []
self.novelOrder = []
theStructure = []
for tHandle in self.theProject.treeOrder:
if tHandle not in self.novelIndex:
continue
for tEntry in self.novelIndex[tHandle]:
self.novelList.append(tEntry)
self.novelOrder.append("%s:%d" % (tHandle,tEntry[0]))
return True
for sTitle in sorted(self.novelIndex[tHandle].keys()):
theStructure.append("%s:%s" % (tHandle, sTitle))
def buildReferenceList(self, tHandle):
return theStructure
def getCounts(self, tHandle, sTitle=None):
"""Returns the counts for a file, or a section of a file
starting at title nTitle.
"""
cC = 0
wC = 0
pC = 0
if sTitle is None:
if tHandle in self.textCounts:
cC = self.textCounts[tHandle][0]
wC = self.textCounts[tHandle][1]
pC = self.textCounts[tHandle][2]
else:
if tHandle in self.novelIndex:
if sTitle in self.novelIndex[tHandle]:
cC = self.novelIndex[tHandle][sTitle]["cCount"]
wC = self.novelIndex[tHandle][sTitle]["wCount"]
pC = self.novelIndex[tHandle][sTitle]["pCount"]
elif tHandle in self.noteIndex:
if sTitle in self.noteIndex[tHandle]:
cC = self.noteIndex[tHandle][sTitle]["cCount"]
wC = self.noteIndex[tHandle][sTitle]["wCount"]
pC = self.noteIndex[tHandle][sTitle]["pCount"]
return cC, wC, pC
def getReferences(self, tHandle, sTitle=None):
"""Extract all references made in a file, and optionally title
section. sTitle must be a string.
"""
theRefs = {}
for tKey in self.TAG_CLASS:
theRefs[tKey] = []
if tHandle not in self.refIndex:
return theRefs
try:
for refTitle in self.refIndex[tHandle]:
for nLine, tKey, tTag in self.refIndex[tHandle][refTitle]["tags"]:
if sTitle is None or sTitle == refTitle:
theRefs[tKey].append(tTag)
except Exception as e:
logger.error("Failed to generate reference list")
logger.error(str(e))
return theRefs
def getBackReferenceList(self, tHandle):
"""Build a list of files referring back to our file, specified
by tHandle.
"""
@@ -418,9 +597,10 @@ class NWIndex():
if theTag is not None:
for tHandle in self.refIndex:
for nLine, tKey, tTag, nTitle in self.refIndex[tHandle]:
if tTag == theTag:
theRefs[tHandle] = nLine
for sTitle in self.refIndex[tHandle]:
for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]["tags"]:
if tTag == theTag:
theRefs[tHandle] = nLine
return theRefs
@@ -433,46 +613,4 @@ class NWIndex():
return theRef[1], theRef[0]
return None, 0
def buildTagNovelMap(self, theTags, theFilters=None):
"""Build a two-dimensional map of all titles of the novel and
which tags they link to from the various meta tags. This map is
used to display the timeline view.
"""
tagMap = {}
tagClass = {}
exClass = []
if theFilters is not None:
if "exClass" in theFilters.keys():
exClass = theFilters["exClass"]
for theTag in theTags:
try:
tagClass[theTag] = nwItemClass[self.tagIndex[theTag][2]]
except:
logger.error("Could not map '%s' to nwItemClass" % self.tagIndex[theTag][2])
tagClass[theTag] = None
if tagClass[theTag] not in exClass:
tagMap[theTag] = [0]*len(self.novelOrder)
for tHandle in self.refIndex:
for nLine, tKey, tTag, nTitle in self.refIndex[tHandle]:
if tTag in tagMap.keys() and tKey in self.TAG_CLASS:
try:
nPos = self.novelOrder.index("%s:%d" % (tHandle, nTitle))
if self.TAG_CLASS[tKey][0] == tagClass[tTag]:
tagMap[tTag][nPos] = self.TAG_CLASS[tKey][1]
except:
logger.error("Could not find '%s:%d' in novelOrder" % (tHandle, nTitle))
if theFilters["hUnused"]:
tagMapFiltered = {}
for theTag in tagMap.keys():
if sum(tagMap[theTag]) > 0:
tagMapFiltered[theTag] = tagMap[theTag]
return tagMapFiltered
return tagMap
# END Class NWIndex
+11
View File
@@ -67,6 +67,7 @@ class NWProject():
# Project Settings
self.spellCheck = False
self.autoOutline = True
self.statusItems = None
self.importItems = None
self.lastEdited = None
@@ -169,6 +170,7 @@ class NWProject():
self.bookAuthors = []
self.autoReplace = {}
self.spellCheck = False
self.autoOutline = True
self.statusItems = NWStatus()
self.statusItems.addEntry("New", (100,100,100))
self.statusItems.addEntry("Note", (200, 50, 0))
@@ -299,6 +301,8 @@ class NWProject():
continue
if xItem.tag == "spellCheck":
self.spellCheck = checkBool(xItem.text,False)
elif xItem.tag == "autoOutline":
self.autoOutline = checkBool(xItem.text,True)
elif xItem.tag == "lastEdited":
self.lastEdited = checkString(xItem.text,None,True)
elif xItem.tag == "lastViewed":
@@ -390,6 +394,7 @@ class NWProject():
# Save Project Settings
xSettings = etree.SubElement(nwXML, "settings")
self._saveProjectValue(xSettings, "spellCheck", self.spellCheck)
self._saveProjectValue(xSettings, "autoOutline", self.autoOutline)
self._saveProjectValue(xSettings, "lastEdited", self.lastEdited)
self._saveProjectValue(xSettings, "lastViewed", self.lastViewed)
self._saveProjectValue(xSettings, "lastWordCount", self.currWCount)
@@ -513,6 +518,12 @@ class NWProject():
self.setProjectChanged(True)
return True
def setAutoOutline(self, theMode):
if self.autoOutline != theMode:
self.autoOutline = theMode
self.setProjectChanged(True)
return True
def setTreeOrder(self, newOrder):
if len(self.treeOrder) != len(newOrder):
logger.warning("Size of new and old tree order does not match")
+12
View File
@@ -81,6 +81,18 @@ class OptionState():
self.theState[setGroup][setName] = setValue
return True
def getValue(self, getGroup, getName, defaultValue):
"""Return an arbitrary type value, if it exists. Otherwise,
return the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return self.theState[getGroup][getName]
except:
return defaultValue
return defaultValue
def getString(self, getGroup, getName, defaultValue):
"""Return the value as a string, if it exists. Otherwise, return
the default value.
@@ -3,4 +3,4 @@
@pov: Jane
@location: Earth
% We can add a chapter file, but keep the scene files separate. In the chapter file we can set the meta data that applies to the whole chapter if we wish.
%synopsis: We can add a chapter file, but keep the scene files separate. In the chapter file we can set the meta data that applies to the whole chapter if we wish to.
+8 -7
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.4.5" fileVersion="1.0" timeStamp="2020-02-19 19:55:13">
<novelWriterXML appVersion="0.4.5" fileVersion="1.0" saveCount="31" autoCount="5" timeStamp="2020-04-13 15:55:18">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
@@ -9,9 +9,10 @@
</project>
<settings>
<spellCheck>True</spellCheck>
<lastEdited>636b6aa9b697b</lastEdited>
<autoOutline>True</autoOutline>
<lastEdited>6a2d6d5f4f401</lastEdited>
<lastViewed>b3e74dbc1f584</lastViewed>
<lastWordCount>875</lastWordCount>
<lastWordCount>869</lastWordCount>
<autoReplace>
<A>B</A>
<B>E</B>
@@ -70,7 +71,7 @@
<charCount>12</charCount>
<wordCount>3</wordCount>
<paraCount>0</paraCount>
<cursorPos>45</cursorPos>
<cursorPos>15</cursorPos>
</item>
<item handle="636b6aa9b697b" order="1" parent="e7ded148d6e4a">
<name>Making a Scene</name>
@@ -239,9 +240,9 @@
<status>New</status>
<expanded>False</expanded>
<layout>SCENE</layout>
<charCount>30</charCount>
<wordCount>6</wordCount>
<paraCount>1</paraCount>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>36</cursorPos>
</item>
</content>
+1
View File
@@ -7,6 +7,7 @@
</project>
<settings>
<spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
+1
View File
@@ -7,6 +7,7 @@
</project>
<settings>
<spellCheck>True</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>31489056e0916</lastEdited>
<lastViewed>31489056e0916</lastViewed>
<lastWordCount>86</lastWordCount>
-1
View File
@@ -1 +0,0 @@
{"tagIndex": {"Jane": [3, "2fca346db6561", "CHARACTER"], "MainPlot": [3, "02d20bbd7e394", "PLOT"], "Home": [3, "7688b6ef52555", "WORLD"]}, "refIndex": {"31489056e0916": [[5, "@pov", "Jane", 3], [6, "@plot", "MainPlot", 3], [11, "@pov", "Jane", 8], [12, "@plot", "MainPlot", 8], [13, "@location", "Home", 8], [17, "@char", "Jane", 15]], "2fca346db6561": [], "02d20bbd7e394": [], "7688b6ef52555": []}, "novelIndex": {"31489056e0916": [[1, 1, "Novel", "SCENE"], [3, 2, "Chapter", "SCENE"], [8, 3, "Scene", "SCENE"], [15, 4, "Some Section", "SCENE"]]}, "noteIndex": {"2fca346db6561": [[1, 1, "Jane Doe", "NOTE"]], "02d20bbd7e394": [[1, 1, "Main Plot", "NOTE"]], "7688b6ef52555": [[1, 1, "Main Location", "NOTE"]]}}
+1
View File
@@ -9,6 +9,7 @@
</project>
<settings>
<spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
+1
View File
@@ -7,6 +7,7 @@
</project>
<settings>
<spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
+1
View File
@@ -7,6 +7,7 @@
</project>
<settings>
<spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
+1
View File
@@ -7,6 +7,7 @@
</project>
<settings>
<spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
-26
View File
@@ -9,7 +9,6 @@ from os import path, unlink
from PyQt5.QtCore import Qt
from nw.gui.dialogs.projecteditor import GuiProjectEditor
from nw.gui.dialogs.timelineview import GuiTimeLineView
from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.constants import *
@@ -246,35 +245,10 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
assert cmpFiles(refFile, path.join(nwRef,"gui","1_1489056e0916_main.nwd"))
refFile = path.join(nwTempGUI,"data_7","688b6ef52555_main.nwd")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_688b6ef52555_main.nwd"))
if sys.version_info[0] >= 3 and sys.version_info[1] >= 6:
refFile = path.join(nwTempGUI,"meta","tagsIndex.json")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_tagsIndex.json"))
nwGUI.closeMain()
# qtbot.stopForInteraction()
@pytest.mark.gui
def testTimeLineView(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
qtbot.wait(stepDelay)
# Create new, save, open project
nwGUI.theProject.handleSeed = 42
assert nwGUI.openProject(nwTempGUI)
qtbot.wait(stepDelay)
timeLine = GuiTimeLineView(nwGUI, nwGUI.theProject, nwGUI.theIndex)
qtbot.addWidget(timeLine)
assert timeLine.numRows == 4
assert timeLine.numCols == 3
# qtbot.stopForInteraction()
nwGUI.closeMain()
@pytest.mark.gui
def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
-23
View File
@@ -111,26 +111,3 @@ def testIndexScanThis(nwTempProj):
assert str(thePos) == "[0, 6, 12]"
assert theProject.closeProject()
@pytest.mark.project
def testBuildIndex(nwTempProj):
projFile = path.join(nwTempProj,"nwProject.nwx")
assert theProject.openProject(projFile)
theIndex = NWIndex(theProject,theMain)
tHandle = "31489056e0916"
theIndex.scanText(tHandle, (
"# Novel\n\n"
"## Chapter\n\n"
"### Scene\n\n"
"#### Section\n\n"
"@pov: John\n"
"@char: Jane\n"
"@location: Somewhere\n"
))
assert theIndex.buildNovelList()
assert str(theIndex.novelList) == "[[1, 1, 'Novel', 'SCENE'], [3, 2, 'Chapter', 'SCENE'], [5, 3, 'Scene', 'SCENE'], [7, 4, 'Section', 'SCENE']]"
assert str(theIndex.novelOrder) == "['31489056e0916:1', '31489056e0916:3', '31489056e0916:5', '31489056e0916:7']"
assert theProject.closeProject()