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 spellcheckline = 200, 46, 0
tagerror = 46, 200, 0 tagerror = 46, 200, 0
replacetag = 0, 184, 46 replacetag = 0, 184, 46
modifier = 200, 120, 0
@@ -17,3 +17,4 @@ value = 50, 150, 50
spellcheckline = 200, 0, 0 spellcheckline = 200, 0, 0
tagerror = 0, 150, 0 tagerror = 0, 150, 0
replacetag = 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 spellcheckline = 222, 61, 58
tagerror = 8, 145, 106 tagerror = 8, 145, 106
replacetag = 42, 162, 152 replacetag = 42, 162, 152
modifier = 224, 175, 5
+1
View File
@@ -40,3 +40,4 @@ value = 199, 146, 234
spellcheckline = 247, 140, 108 spellcheckline = 247, 140, 108
tagerror = 173, 219, 103 tagerror = 173, 219, 103
replacetag = 127, 219, 202 replacetag = 127, 219, 202
modifier = 236, 196, 141
+1
View File
@@ -40,3 +40,4 @@ value = 137, 89, 168
spellcheckline = 240, 40, 41 spellcheckline = 240, 40, 41
tagerror = 113, 140, 0 tagerror = 113, 140, 0
replacetag = 62, 153, 159 replacetag = 62, 153, 159
modifier = 245, 135, 31
@@ -40,3 +40,4 @@ value = 178, 148, 187
spellcheckline = 204, 102, 102 spellcheckline = 204, 102, 102
tagerror = 181, 189, 104 tagerror = 181, 189, 104
replacetag = 138, 190, 183 replacetag = 138, 190, 183
modifier = 222, 147, 95
@@ -40,3 +40,4 @@ value = 235, 187, 255
spellcheckline = 255, 157, 164 spellcheckline = 255, 157, 164
tagerror = 209, 241, 169 tagerror = 209, 241, 169
replacetag = 153, 255, 255 replacetag = 153, 255, 255
modifier = 255, 197, 143
@@ -40,3 +40,4 @@ value = 195, 151, 216
spellcheckline = 213, 78, 83 spellcheckline = 213, 78, 83
tagerror = 185, 202, 74 tagerror = 185, 202, 74
replacetag = 112, 192, 177 replacetag = 112, 192, 177
modifier = 231, 140, 69
@@ -40,3 +40,4 @@ value = 204, 153, 204
spellcheckline = 242, 119, 122 spellcheckline = 242, 119, 122
tagerror = 153, 204, 153 tagerror = 153, 204, 153
replacetag = 102, 204, 204 replacetag = 102, 204, 204
modifier = 249, 145, 57
+2 -1
View File
@@ -4,7 +4,7 @@ from nw.constants.constants import (
nwConst, nwFiles, nwKeyWords, nwLabels, nwDependencies, nwQuotes, nwUnicode nwConst, nwFiles, nwKeyWords, nwLabels, nwDependencies, nwQuotes, nwUnicode
) )
from nw.constants.enum import ( from nw.constants.enum import (
nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline
) )
__all__ = [ __all__ = [
@@ -22,4 +22,5 @@ __all__ = [
"nwItemClass", "nwItemClass",
"nwItemLayout", "nwItemLayout",
"nwItemType", "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(): class nwConst():
@@ -104,6 +104,24 @@ class nwLabels():
nwKeyWords.ENTITY_KEY : "Entities", nwKeyWords.ENTITY_KEY : "Entities",
nwKeyWords.CUSTOM_KEY : "Custom", 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 # END Class nwLabels
+21
View File
@@ -88,3 +88,24 @@ class nwAlert(Enum):
BUG = 3 BUG = 3
# END Enum nwAlert # 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.projecteditor import GuiProjectEditor
from nw.gui.dialogs.projectload import GuiProjectLoad from nw.gui.dialogs.projectload import GuiProjectLoad
from nw.gui.dialogs.sessionlog import GuiSessionLogView from nw.gui.dialogs.sessionlog import GuiSessionLogView
from nw.gui.dialogs.timelineview import GuiTimeLineView
# GUI Elements # GUI Elements
from nw.gui.elements.docdetails import GuiDocDetails 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.doctree import GuiDocTree
from nw.gui.elements.docviewer import GuiDocViewer from nw.gui.elements.docviewer import GuiDocViewer
from nw.gui.elements.noticebar import GuiNoticeBar 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.searchbar import GuiSearchBar
from nw.gui.elements.viewdetails import GuiDocViewDetails from nw.gui.elements.viewdetails import GuiDocViewDetails
@@ -43,12 +43,12 @@ __all__ = [
"GuiProjectEditor", "GuiProjectEditor",
"GuiProjectLoad", "GuiProjectLoad",
"GuiSessionLogView", "GuiSessionLogView",
"GuiTimeLineView",
"GuiDocDetails", "GuiDocDetails",
"GuiDocEditor", "GuiDocEditor",
"GuiDocTree", "GuiDocTree",
"GuiDocViewer", "GuiDocViewer",
"GuiNoticeBar", "GuiNoticeBar",
"GuiProjectOutline",
"GuiSearchBar", "GuiSearchBar",
"GuiDocViewDetails", "GuiDocViewDetails",
"GuiDocHighlighter", "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.projecteditor import GuiProjectEditor
from nw.gui.dialogs.projectload import GuiProjectLoad from nw.gui.dialogs.projectload import GuiProjectLoad
from nw.gui.dialogs.sessionlog import GuiSessionLogView from nw.gui.dialogs.sessionlog import GuiSessionLogView
from nw.gui.dialogs.timelineview import GuiTimeLineView
__all__ = [ __all__ = [
"GuiConfigEditor", "GuiConfigEditor",
@@ -19,5 +18,4 @@ __all__ = [
"GuiProjectEditor", "GuiProjectEditor",
"GuiProjectLoad", "GuiProjectLoad",
"GuiSessionLogView", "GuiSessionLogView",
"GuiTimeLineView",
] ]
+5 -5
View File
@@ -44,11 +44,11 @@ class GuiConfigEditor(QDialog):
self.setWindowTitle("Preferences") self.setWindowTitle("Preferences")
self.guiDeco = self.theParent.theTheme.loadDecoration("settings",(64,64)) self.guiDeco = self.theParent.theTheme.loadDecoration("settings",(64,64))
self.tabMain = GuiConfigEditGeneral(self.theParent) self.tabMain = GuiConfigEditGeneral(self.theParent)
self.tabEditor = GuiConfigEditEditor(self.theParent) self.tabEditor = GuiConfigEditEditor(self.theParent)
self.tabWidget = QTabWidget() self.tabWidget = QTabWidget()
self.tabWidget.addTab(self.tabMain, "General") self.tabWidget.addTab(self.tabMain, "General")
self.tabWidget.addTab(self.tabEditor, "Editor") self.tabWidget.addTab(self.tabEditor, "Editor")
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
@@ -79,11 +79,11 @@ class GuiConfigEditor(QDialog):
validEntries = True validEntries = True
needsRestart = False needsRestart = False
retA, retB = self.tabMain.saveValues() retA, retB = self.tabMain.saveValues()
validEntries &= retA validEntries &= retA
needsRestart |= retB needsRestart |= retB
retA, retB = self.tabEditor.saveValues() retA, retB = self.tabEditor.saveValues()
validEntries &= retA validEntries &= retA
needsRestart |= retB needsRestart |= retB
+3 -3
View File
@@ -162,10 +162,10 @@ class GuiProjectEditStatus(QWidget):
self.colChanged = False self.colChanged = False
self.selColour = None self.selColour = None
self.mainBox = QHBoxLayout() self.mainBox = QHBoxLayout()
self.mainForm = QVBoxLayout() self.mainForm = QVBoxLayout()
self.listBox = QListWidget() self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.InternalMove) self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
self.listBox.itemSelectionChanged.connect(self._selectedItem) self.listBox.itemSelectionChanged.connect(self._selectedItem)
self.listBox.model().rowsMoved.connect(self._rowsMoved) 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.doctree import GuiDocTree
from nw.gui.elements.docviewer import GuiDocViewer from nw.gui.elements.docviewer import GuiDocViewer
from nw.gui.elements.noticebar import GuiNoticeBar 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.searchbar import GuiSearchBar
from nw.gui.elements.viewdetails import GuiDocViewDetails from nw.gui.elements.viewdetails import GuiDocViewDetails
@@ -14,6 +15,7 @@ __all__ = [
"GuiDocTree", "GuiDocTree",
"GuiDocViewer", "GuiDocViewer",
"GuiNoticeBar", "GuiNoticeBar",
"GuiProjectOutline",
"GuiSearchBar", "GuiSearchBar",
"GuiDocViewDetails", "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(): if self.isSticky.isChecked():
return return
theRefs = self.theParent.theIndex.buildReferenceList(tHandle) theRefs = self.theParent.theIndex.getBackReferenceList(tHandle)
theList = [] theList = []
for tHandle in theRefs: for tHandle in theRefs:
tItem = self.theProject.getItem(tHandle) tItem = self.theProject.getItem(tHandle)
+31 -10
View File
@@ -69,6 +69,13 @@ class GuiMainMenu(QMenuBar):
self.aSpellCheck.setChecked(theMode) self.aSpellCheck.setChecked(theMode)
return return
def setAutoOutline(self, theMode):
"""Set the auto outline check box to theMode. Used during
initialisation.
"""
self.aAutoOutline.setChecked(theMode)
return
## ##
# Menu Action # Menu Action
## ##
@@ -85,6 +92,12 @@ class GuiMainMenu(QMenuBar):
self.theParent.docEditor.setSpellCheck(None) self.theParent.docEditor.setSpellCheck(None)
return True return True
def _toggleAutoOutline(self, theMode):
"""Toggle auto outline when the menu entry is checked.
"""
self.theProject.setAutoOutline(theMode)
return True
def _toggleViewComments(self): def _toggleViewComments(self):
self.mainConf.setViewComments(self.aViewDocComments.isChecked()) self.mainConf.setViewComments(self.aViewDocComments.isChecked())
self.theParent.docViewer.reloadText() self.theParent.docViewer.reloadText()
@@ -396,16 +409,6 @@ class GuiMainMenu(QMenuBar):
self.aFullScreen.triggered.connect(self.theParent.toggleFullScreenMode) self.aFullScreen.triggered.connect(self.theParent.toggleFullScreenMode)
self.viewMenu.addAction(self.aFullScreen) 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 return
def _buildEditMenu(self): def _buildEditMenu(self):
@@ -656,6 +659,24 @@ class GuiMainMenu(QMenuBar):
self.aRebuildIndex.triggered.connect(self.theParent.rebuildIndex) self.aRebuildIndex.triggered.connect(self.theParent.rebuildIndex)
self.toolsMenu.addAction(self.aRebuildIndex) 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 # Tools > Backup
self.aBackupProject = QAction("Backup Project", self) self.aBackupProject = QAction("Backup Project", self)
self.aBackupProject.setStatusTip("Backup Project") self.aBackupProject.setStatusTip("Backup Project")
+2
View File
@@ -78,6 +78,7 @@ class GuiTheme:
self.colSpell = [ 0, 0, 0] self.colSpell = [ 0, 0, 0]
self.colTagErr = [ 0, 0, 0] self.colTagErr = [ 0, 0, 0]
self.colRepTag = [ 0, 0, 0] self.colRepTag = [ 0, 0, 0]
self.colMod = [ 0, 0, 0]
# Changeable Settings # Changeable Settings
self.guiTheme = None self.guiTheme = None
@@ -217,6 +218,7 @@ class GuiTheme:
self.colSpell = self._loadColour(confParser, cnfSec, "spellcheckline") self.colSpell = self._loadColour(confParser, cnfSec, "spellcheckline")
self.colTagErr = self._loadColour(confParser, cnfSec, "tagerror") self.colTagErr = self._loadColour(confParser, cnfSec, "tagerror")
self.colRepTag = self._loadColour(confParser, cnfSec, "replacetag") self.colRepTag = self._loadColour(confParser, cnfSec, "replacetag")
self.colMod = self._loadColour(confParser, cnfSec, "modifier")
logger.info("Loaded syntax theme '%s'" % self.guiSyntax) 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.colSpell = QColor(*self.theTheme.colSpell)
self.colTagErr = QColor(*self.theTheme.colTagErr) self.colTagErr = QColor(*self.theTheme.colTagErr)
self.colRepTag = QColor(*self.theTheme.colRepTag) self.colRepTag = QColor(*self.theTheme.colRepTag)
self.colMod = QColor(*self.theTheme.colMod)
self.colTrail = QColor(*self.theTheme.colEmph,64) self.colTrail = QColor(*self.theTheme.colEmph,64)
self.hStyles = { self.hStyles = {
@@ -98,6 +99,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
"replace" : self._makeFormat(self.colRepTag), "replace" : self._makeFormat(self.colRepTag),
"hidden" : self._makeFormat(self.colComm), "hidden" : self._makeFormat(self.colComm),
"keyword" : self._makeFormat(self.colKey), "keyword" : self._makeFormat(self.colKey),
"modifier" : self._makeFormat(self.colMod),
"value" : self._makeFormat(self.colVal), "value" : self._makeFormat(self.colVal),
} }
@@ -250,7 +252,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setFormat(4, len(theText), self.hStyles["header4"]) self.setFormat(4, len(theText), self.hStyles["header4"])
elif theText.startswith("%"): # Comments 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 else: # Text Paragraph
for rX, xFmt in self.rxRules: 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.QtGui import QIcon, QPixmap, QColor, QKeySequence
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
qApp, QMainWindow, QVBoxLayout, QFrame, QSplitter, QFileDialog, QShortcut, qApp, QMainWindow, QVBoxLayout, QFrame, QSplitter, QFileDialog, QShortcut,
QMessageBox, QProgressDialog, QDialog QMessageBox, QProgressDialog, QDialog, QTabWidget
) )
from nw.gui import ( from nw.gui import (
GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiExport, GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiExport,
GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails, GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails,
GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiTimeLineView, GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiProjectOutline,
GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad
) )
from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup
@@ -76,6 +76,7 @@ class GuiMain(QMainWindow):
self.searchBar = GuiSearchBar(self) self.searchBar = GuiSearchBar(self)
self.treeMeta = GuiDocDetails(self, self.theProject) self.treeMeta = GuiDocDetails(self, self.theProject)
self.treeView = GuiDocTree(self, self.theProject) self.treeView = GuiDocTree(self, self.theProject)
self.projView = GuiProjectOutline(self, self.theProject)
self.mainMenu = GuiMainMenu(self, self.theProject) self.mainMenu = GuiMainMenu(self, self.theProject)
# Minor Gui Elements # Minor Gui Elements
@@ -111,20 +112,33 @@ class GuiMain(QMainWindow):
self.splitView.addWidget(self.editPane) self.splitView.addWidget(self.editPane)
self.splitView.addWidget(self.viewPane) 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 = QSplitter(Qt.Horizontal)
self.splitMain.setContentsMargins(4,4,4,4) self.splitMain.setContentsMargins(4,4,4,4)
self.splitMain.setOpaqueResize(False) self.splitMain.setOpaqueResize(False)
self.splitMain.addWidget(self.treePane) self.splitMain.addWidget(self.treePane)
self.splitMain.addWidget(self.splitView) self.splitMain.addWidget(self.tabWidget)
self.splitMain.setSizes(self.mainConf.mainPanePos) self.splitMain.setSizes(self.mainConf.mainPanePos)
self.setCentralWidget(self.splitMain) self.setCentralWidget(self.splitMain)
self.idxTree = self.splitMain.indexOf(self.treePane) 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.idxEditor = self.splitView.indexOf(self.editPane)
self.idxViewer = self.splitView.indexOf(self.viewPane) 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.idxTree, False)
self.splitMain.setCollapsible(self.idxMain, False) self.splitMain.setCollapsible(self.idxMain, False)
self.splitView.setCollapsible(self.idxEditor, False) self.splitView.setCollapsible(self.idxEditor, False)
@@ -296,6 +310,7 @@ class GuiMain(QMainWindow):
if saveOK: if saveOK:
self.closeDocument() self.closeDocument()
self.projView.closeOutline()
self.theProject.closeProject() self.theProject.closeProject()
self.theIndex.clearIndex() self.theIndex.clearIndex()
self.clearGUI() self.clearGUI()
@@ -318,6 +333,9 @@ class GuiMain(QMainWindow):
if not self.closeProject(): if not self.closeProject():
return False return False
# Switch main tab to editor view
self.tabWidget.setCurrentWidget(self.splitView)
# Try to open the project # Try to open the project
if not self.theProject.openProject(projFile): if not self.theProject.openProject(projFile):
if self.theProject.lockedBy is not None: if self.theProject.lockedBy is not None:
@@ -368,6 +386,7 @@ class GuiMain(QMainWindow):
self.rebuildTree() self.rebuildTree()
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.docEditor.setSpellCheck(self.theProject.spellCheck) self.docEditor.setSpellCheck(self.theProject.spellCheck)
self.mainMenu.setAutoOutline(self.theProject.autoOutline)
self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setRefTime(self.theProject.projOpened)
# Restore previously open documents, if any # Restore previously open documents, if any
@@ -420,6 +439,7 @@ class GuiMain(QMainWindow):
def openDocument(self, tHandle): def openDocument(self, tHandle):
if self.hasProject: if self.hasProject:
self.closeDocument() self.closeDocument()
self.tabWidget.setCurrentWidget(self.splitView)
if self.docEditor.loadText(tHandle): if self.docEditor.loadText(tHandle):
self.docEditor.setFocus() self.docEditor.setFocus()
self.theProject.setLastEdited(tHandle) self.theProject.setLastEdited(tHandle)
@@ -446,6 +466,9 @@ class GuiMain(QMainWindow):
logger.debug("No document selected, giving up") logger.debug("No document selected, giving up")
return False 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(): if self.docViewer.loadText(tHandle) and not self.viewPane.isVisible():
bPos = self.splitMain.sizes() bPos = self.splitMain.sizes()
self.viewPane.setVisible(True) self.viewPane.setVisible(True)
@@ -615,17 +638,17 @@ class GuiMain(QMainWindow):
theDoc = NWDoc(self.theProject, self) theDoc = NWDoc(self.theProject, self)
theText = theDoc.openDocument(tHandle, False) theText = theDoc.openDocument(tHandle, False)
# Run Word Count # Build tag index
cC, wC, pC = countWords(theText) self.theIndex.scanText(tHandle, theText)
# Get Word Counts
cC, wC, pC = self.theIndex.getCounts(tHandle)
tItem.setCharCount(cC) tItem.setCharCount(cC)
tItem.setWordCount(wC) tItem.setWordCount(wC)
tItem.setParaCount(pC) tItem.setParaCount(pC)
self.treeView.propagateCount(tHandle, wC) self.treeView.propagateCount(tHandle, wC)
self.treeView.projectWordCount() self.treeView.projectWordCount()
# Build tag index
self.theIndex.scanText(tHandle, theText)
nDone += 1 nDone += 1
if dlgProg.wasCanceled(): if dlgProg.wasCanceled():
break break
@@ -634,6 +657,14 @@ class GuiMain(QMainWindow):
return True 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 # Main Dialogs
## ##
@@ -696,12 +727,6 @@ class GuiMain(QMainWindow):
dlgExport.exec_() dlgExport.exec_()
return True return True
def showTimeLineDialog(self):
if self.hasProject:
dlgTLine = GuiTimeLineView(self, self.theProject, self.theIndex)
dlgTLine.exec_()
return True
def showSessionLogDialog(self): def showSessionLogDialog(self):
if self.hasProject: if self.hasProject:
dlgTLine = GuiSessionLogView(self, self.theProject) dlgTLine = GuiSessionLogView(self, self.theProject)
@@ -767,7 +792,8 @@ class GuiMain(QMainWindow):
return False return False
logger.info("Exiting %s" % nw.__package__) logger.info("Exiting %s" % nw.__package__)
self.closeProject(True) if self.hasProject:
self.closeProject(True)
self.mainConf.setTreeColWidths(self.treeView.getColumnSizes()) self.mainConf.setTreeColWidths(self.treeView.getColumnSizes())
if not self.mainConf.isFullScreen: if not self.mainConf.isFullScreen:
@@ -817,6 +843,7 @@ class GuiMain(QMainWindow):
self.isZenMode = not self.isZenMode self.isZenMode = not self.isZenMode
if self.isZenMode: if self.isZenMode:
logger.debug("Activating Zen mode") logger.debug("Activating Zen mode")
self.tabWidget.setCurrentWidget(self.splitView)
else: else:
logger.debug("Deactivating Zen mode") logger.debug("Deactivating Zen mode")
@@ -824,6 +851,7 @@ class GuiMain(QMainWindow):
self.treePane.setVisible(isVisible) self.treePane.setVisible(isVisible)
self.statusBar.setVisible(isVisible) self.statusBar.setVisible(isVisible)
self.mainMenu.setVisible(isVisible) self.mainMenu.setVisible(isVisible)
self.tabWidget.tabBar().setVisible(isVisible)
if self.viewPane.isVisible(): if self.viewPane.isVisible():
self.viewPane.setVisible(False) self.viewPane.setVisible(False)
@@ -866,7 +894,6 @@ class GuiMain(QMainWindow):
self.addAction(self.mainMenu.aFileDetails) self.addAction(self.mainMenu.aFileDetails)
self.addAction(self.mainMenu.aZenMode) self.addAction(self.mainMenu.aZenMode)
self.addAction(self.mainMenu.aFullScreen) self.addAction(self.mainMenu.aFullScreen)
self.addAction(self.mainMenu.aViewTimeLine)
self.addAction(self.mainMenu.aEditUndo) self.addAction(self.mainMenu.aEditUndo)
self.addAction(self.mainMenu.aEditRedo) self.addAction(self.mainMenu.aEditRedo)
self.addAction(self.mainMenu.aEditCut) self.addAction(self.mainMenu.aEditCut)
@@ -972,7 +999,7 @@ class GuiMain(QMainWindow):
def _keyPressEscape(self): def _keyPressEscape(self):
"""When the escape key is pressed somewhere in the main window, """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(): if self.searchBar.isVisible():
self.searchBar.setVisible(False) self.searchBar.setVisible(False)
@@ -981,4 +1008,15 @@ class GuiMain(QMainWindow):
self.toggleZenMode() self.toggleZenMode()
return 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 # END Class GuiMain
+252 -114
View File
@@ -15,10 +15,12 @@ import json
import nw import nw
from os import path from os import path
from time import time
from nw.constants import ( from nw.constants import (
nwFiles, nwKeyWords, nwItemType, nwItemClass, nwAlert nwFiles, nwKeyWords, nwItemType, nwItemClass, nwItemLayout, nwAlert
) )
from nw.tools import countWords
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -36,14 +38,14 @@ class NWIndex():
nwKeyWords.CUSTOM_KEY nwKeyWords.CUSTOM_KEY
] ]
TAG_CLASS = { TAG_CLASS = {
nwKeyWords.CHAR_KEY : [nwItemClass.CHARACTER, 1], nwKeyWords.CHAR_KEY : nwItemClass.CHARACTER,
nwKeyWords.POV_KEY : [nwItemClass.CHARACTER, 2], nwKeyWords.POV_KEY : nwItemClass.CHARACTER,
nwKeyWords.PLOT_KEY : [nwItemClass.PLOT, 1], nwKeyWords.PLOT_KEY : nwItemClass.PLOT,
nwKeyWords.TIME_KEY : [nwItemClass.TIMELINE, 1], nwKeyWords.TIME_KEY : nwItemClass.TIMELINE,
nwKeyWords.WORLD_KEY : [nwItemClass.WORLD, 1], nwKeyWords.WORLD_KEY : nwItemClass.WORLD,
nwKeyWords.OBJECT_KEY : [nwItemClass.OBJECT, 1], nwKeyWords.OBJECT_KEY : nwItemClass.OBJECT,
nwKeyWords.ENTITY_KEY : [nwItemClass.ENTITY, 1], nwKeyWords.ENTITY_KEY : nwItemClass.ENTITY,
nwKeyWords.CUSTOM_KEY : [nwItemClass.CUSTOM, 1], nwKeyWords.CUSTOM_KEY : nwItemClass.CUSTOM,
} }
def __init__(self, theProject, theParent): def __init__(self, theProject, theParent):
@@ -55,13 +57,18 @@ class NWIndex():
self.indexBroken = False self.indexBroken = False
# Indices # Indices
self.tagIndex = {} self.tagIndex = None
self.refIndex = {} self.refIndex = None
self.novelIndex = {} self.novelIndex = None
self.noteIndex = {} self.noteIndex = None
self.textCounts = None
# Lists # TimeStamps
self.novelList = [] self.timeNovel = 0
self.timeNote = 0
self.timeIndex = 0
self.clearIndex()
return return
@@ -70,13 +77,21 @@ class NWIndex():
## ##
def clearIndex(self): def clearIndex(self):
"""Clear the index dictionaries and time stamps.
"""
self.tagIndex = {} self.tagIndex = {}
self.refIndex = {} self.refIndex = {}
self.novelIndex = {} self.novelIndex = {}
self.noteIndex = {} self.noteIndex = {}
self.textCounts = {}
self.timeNovel = 0
self.timeNote = 0
self.timeIndex = 0
return return
def deleteHandle(self, tHandle): def deleteHandle(self, tHandle):
"""Delete all entries of a given document handle.
"""
delTags = [] delTags = []
for tTag in self.tagIndex: for tTag in self.tagIndex:
@@ -89,6 +104,7 @@ class NWIndex():
self.refIndex.pop(tHandle, None) self.refIndex.pop(tHandle, None)
self.novelIndex.pop(tHandle, None) self.novelIndex.pop(tHandle, None)
self.noteIndex.pop(tHandle, None) self.noteIndex.pop(tHandle, None)
self.textCounts.pop(tHandle, None)
return return
@@ -100,8 +116,9 @@ class NWIndex():
"""Load index from last session from the project meta folder. """Load index from last session from the project meta folder.
""" """
theData = {} theData = {}
indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
if path.isfile(indexFile): if path.isfile(indexFile):
logger.debug("Loading index file") logger.debug("Loading index file")
try: try:
@@ -121,24 +138,31 @@ class NWIndex():
self.novelIndex = theData["novelIndex"] self.novelIndex = theData["novelIndex"]
if "noteIndex" in theData.keys(): if "noteIndex" in theData.keys():
self.noteIndex = theData["noteIndex"] 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): def saveIndex(self):
"""Save the current index as a json file in the project meta """Save the current index as a json file in the project meta
folder. data folder.
""" """
indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
logger.debug("Saving index file") logger.debug("Saving index file")
if self.mainConf.debugInfo: if self.mainConf.debugInfo:
nIndent = 2 nIndent = 2
else: else:
nIndent = None nIndent = None
try: try:
with open(indexFile,mode="w+",encoding="utf8") as outFile: with open(indexFile,mode="w+",encoding="utf8") as outFile:
outFile.write(json.dumps({ outFile.write(json.dumps({
@@ -146,6 +170,7 @@ class NWIndex():
"refIndex" : self.refIndex, "refIndex" : self.refIndex,
"novelIndex" : self.novelIndex, "novelIndex" : self.novelIndex,
"noteIndex" : self.noteIndex, "noteIndex" : self.noteIndex,
"textCounts" : self.textCounts,
}, indent=nIndent)) }, indent=nIndent))
except Exception as e: except Exception as e:
logger.error("Failed to save index file") logger.error("Failed to save index file")
@@ -161,29 +186,38 @@ class NWIndex():
self.indexBroken = False self.indexBroken = False
for tTag in self.tagIndex: try:
if len(self.tagIndex[tTag]) != 3: for tTag in self.tagIndex:
self.indexBroken = True if len(self.tagIndex[tTag]) != 3:
for tHandle in self.refIndex:
for tEntry in self.refIndex[tHandle]:
if len(tEntry) != 4:
self.indexBroken = True self.indexBroken = True
for tHandle in self.novelIndex: for tHandle in self.refIndex:
for tEntry in self.novelIndex[tHandle]: for sTitle in self.refIndex[tHandle]:
if len(tEntry) != 4: 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 self.indexBroken = True
for tHandle in self.noteIndex: except:
for tEntry in self.noteIndex[tHandle]: self.indexBroken = True
if len(tEntry) != 4:
self.indexBroken = True
if self.indexBroken: if self.indexBroken:
self.clearIndex() self.clearIndex()
self.theParent.makeAlert( 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 nwAlert.WARN
) )
@@ -201,23 +235,33 @@ class NWIndex():
""" """
theItem = self.theProject.getItem(tHandle) theItem = self.theProject.getItem(tHandle)
if theItem is None: return False if theItem is None:
if theItem.itemType != nwItemType.FILE: return False return False
if theItem.parHandle == self.theProject.trashRoot: 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 itemClass = theItem.itemClass
itemLayout = theItem.itemLayout itemLayout = theItem.itemLayout
logger.debug("Indexing item with handle %s" % tHandle) logger.debug("Indexing item with handle %s" % tHandle)
# Check file type, and reset its old index # Check file type, and reset its old index
if itemClass == nwItemClass.NOVEL: # Also add an entry for T0 in case the file has no title
self.novelIndex[tHandle] = [] self.refIndex[tHandle] = {}
self.refIndex[tHandle] = [] self.refIndex[tHandle]["T0"] = {
isNovel = True "tags" : [],
else: "updated" : time(),
self.noteIndex[tHandle] = [] }
self.refIndex[tHandle] = [] if itemLayout == nwItemLayout.NOTE:
self.noteIndex[tHandle] = {}
isNovel = False isNovel = False
else:
self.novelIndex[tHandle] = {}
isNovel = True
# Also clear references to file in tag index # Also clear references to file in tag index
clearTags = [] clearTags = []
@@ -229,19 +273,47 @@ class NWIndex():
nLine = 0 nLine = 0
nTitle = 0 nTitle = 0
for aLine in theText.splitlines(): theLines = theText.splitlines()
aLine = aLine.strip() for aLine in theLines:
aLine = aLine
nLine += 1 nLine += 1
nChar = len(aLine) nChar = len(aLine.strip())
if nChar == 0: continue if nChar == 0:
if aLine[0] == "#": continue
if aLine.startswith(r"#"):
isTitle = self.indexTitle(tHandle, isNovel, aLine, nLine, itemLayout) 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 nTitle = nLine
elif aLine[0] == "@":
elif aLine.startswith(r"@"):
self.indexNoteRef(tHandle, aLine, nLine, nTitle) self.indexNoteRef(tHandle, aLine, nLine, nTitle)
self.indexTag(tHandle, aLine, nLine, itemClass) 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 return True
def indexTitle(self, tHandle, isNovel, aLine, nLine, itemLayout): def indexTitle(self, tHandle, isNovel, aLine, nLine, itemLayout):
@@ -250,30 +322,79 @@ class NWIndex():
""" """
if aLine.startswith("# "): if aLine.startswith("# "):
hDepth = 1 hDepth = "H1"
hText = aLine[2:].strip() hText = aLine[2:].strip()
elif aLine.startswith("## "): elif aLine.startswith("## "):
hDepth = 2 hDepth = "H2"
hText = aLine[3:].strip() hText = aLine[3:].strip()
elif aLine.startswith("### "): elif aLine.startswith("### "):
hDepth = 3 hDepth = "H3"
hText = aLine[4:].strip() hText = aLine[4:].strip()
elif aLine.startswith("#### "): elif aLine.startswith("#### "):
hDepth = 4 hDepth = "H4"
hText = aLine[5:].strip() hText = aLine[5:].strip()
else: else:
return False 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 hText != "":
if isNovel: if isNovel:
if tHandle in self.novelIndex: if tHandle in self.novelIndex:
self.novelIndex[tHandle].append([nLine, hDepth, hText, itemLayout.name]) self.novelIndex[tHandle][sTitle] = theData
else: else:
if tHandle in self.noteIndex: if tHandle in self.noteIndex:
self.noteIndex[tHandle].append([nLine, hDepth, hText, itemLayout.name]) self.noteIndex[tHandle][sTitle] = theData
return True 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): def indexNoteRef(self, tHandle, aLine, nLine, nTitle):
"""Validate and save the information about a reference to a tag """Validate and save the information about a reference to a tag
in another file. in another file.
@@ -283,9 +404,14 @@ class NWIndex():
if not isValid or len(theBits) == 0: if not isValid or len(theBits) == 0:
return False 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: if theBits[0] != nwKeyWords.TAG_KEY:
for aVal in theBits[1:]: 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 return True
@@ -378,7 +504,7 @@ class NWIndex():
# If we're still here, we better check that the references exist # If we're still here, we better check that the references exist
for n in range(1,nBits): for n in range(1,nBits):
if theBits[n] in self.tagIndex: 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 return isGood
@@ -386,20 +512,73 @@ class NWIndex():
# Extract Data # Extract Data
## ##
def buildNovelList(self): def getNovelStructure(self):
"""Build a list of the content of the novel. """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: for tHandle in self.theProject.treeOrder:
if tHandle not in self.novelIndex: if tHandle not in self.novelIndex:
continue continue
for tEntry in self.novelIndex[tHandle]: for sTitle in sorted(self.novelIndex[tHandle].keys()):
self.novelList.append(tEntry) theStructure.append("%s:%s" % (tHandle, sTitle))
self.novelOrder.append("%s:%d" % (tHandle,tEntry[0]))
return True
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 """Build a list of files referring back to our file, specified
by tHandle. by tHandle.
""" """
@@ -418,9 +597,10 @@ class NWIndex():
if theTag is not None: if theTag is not None:
for tHandle in self.refIndex: for tHandle in self.refIndex:
for nLine, tKey, tTag, nTitle in self.refIndex[tHandle]: for sTitle in self.refIndex[tHandle]:
if tTag == theTag: for nLine, tKey, tTag in self.refIndex[tHandle][sTitle]["tags"]:
theRefs[tHandle] = nLine if tTag == theTag:
theRefs[tHandle] = nLine
return theRefs return theRefs
@@ -433,46 +613,4 @@ class NWIndex():
return theRef[1], theRef[0] return theRef[1], theRef[0]
return None, 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 # END Class NWIndex
+11
View File
@@ -67,6 +67,7 @@ class NWProject():
# Project Settings # Project Settings
self.spellCheck = False self.spellCheck = False
self.autoOutline = True
self.statusItems = None self.statusItems = None
self.importItems = None self.importItems = None
self.lastEdited = None self.lastEdited = None
@@ -169,6 +170,7 @@ class NWProject():
self.bookAuthors = [] self.bookAuthors = []
self.autoReplace = {} self.autoReplace = {}
self.spellCheck = False self.spellCheck = False
self.autoOutline = True
self.statusItems = NWStatus() self.statusItems = NWStatus()
self.statusItems.addEntry("New", (100,100,100)) self.statusItems.addEntry("New", (100,100,100))
self.statusItems.addEntry("Note", (200, 50, 0)) self.statusItems.addEntry("Note", (200, 50, 0))
@@ -299,6 +301,8 @@ class NWProject():
continue continue
if xItem.tag == "spellCheck": if xItem.tag == "spellCheck":
self.spellCheck = checkBool(xItem.text,False) self.spellCheck = checkBool(xItem.text,False)
elif xItem.tag == "autoOutline":
self.autoOutline = checkBool(xItem.text,True)
elif xItem.tag == "lastEdited": elif xItem.tag == "lastEdited":
self.lastEdited = checkString(xItem.text,None,True) self.lastEdited = checkString(xItem.text,None,True)
elif xItem.tag == "lastViewed": elif xItem.tag == "lastViewed":
@@ -390,6 +394,7 @@ class NWProject():
# Save Project Settings # Save Project Settings
xSettings = etree.SubElement(nwXML, "settings") xSettings = etree.SubElement(nwXML, "settings")
self._saveProjectValue(xSettings, "spellCheck", self.spellCheck) self._saveProjectValue(xSettings, "spellCheck", self.spellCheck)
self._saveProjectValue(xSettings, "autoOutline", self.autoOutline)
self._saveProjectValue(xSettings, "lastEdited", self.lastEdited) self._saveProjectValue(xSettings, "lastEdited", self.lastEdited)
self._saveProjectValue(xSettings, "lastViewed", self.lastViewed) self._saveProjectValue(xSettings, "lastViewed", self.lastViewed)
self._saveProjectValue(xSettings, "lastWordCount", self.currWCount) self._saveProjectValue(xSettings, "lastWordCount", self.currWCount)
@@ -513,6 +518,12 @@ class NWProject():
self.setProjectChanged(True) self.setProjectChanged(True)
return True return True
def setAutoOutline(self, theMode):
if self.autoOutline != theMode:
self.autoOutline = theMode
self.setProjectChanged(True)
return True
def setTreeOrder(self, newOrder): def setTreeOrder(self, newOrder):
if len(self.treeOrder) != len(newOrder): if len(self.treeOrder) != len(newOrder):
logger.warning("Size of new and old tree order does not match") 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 self.theState[setGroup][setName] = setValue
return True 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): def getString(self, getGroup, getName, defaultValue):
"""Return the value as a string, if it exists. Otherwise, return """Return the value as a string, if it exists. Otherwise, return
the default value. the default value.
@@ -3,4 +3,4 @@
@pov: Jane @pov: Jane
@location: Earth @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'?> <?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> <project>
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
@@ -9,9 +9,10 @@
</project> </project>
<settings> <settings>
<spellCheck>True</spellCheck> <spellCheck>True</spellCheck>
<lastEdited>636b6aa9b697b</lastEdited> <autoOutline>True</autoOutline>
<lastEdited>6a2d6d5f4f401</lastEdited>
<lastViewed>b3e74dbc1f584</lastViewed> <lastViewed>b3e74dbc1f584</lastViewed>
<lastWordCount>875</lastWordCount> <lastWordCount>869</lastWordCount>
<autoReplace> <autoReplace>
<A>B</A> <A>B</A>
<B>E</B> <B>E</B>
@@ -70,7 +71,7 @@
<charCount>12</charCount> <charCount>12</charCount>
<wordCount>3</wordCount> <wordCount>3</wordCount>
<paraCount>0</paraCount> <paraCount>0</paraCount>
<cursorPos>45</cursorPos> <cursorPos>15</cursorPos>
</item> </item>
<item handle="636b6aa9b697b" order="1" parent="e7ded148d6e4a"> <item handle="636b6aa9b697b" order="1" parent="e7ded148d6e4a">
<name>Making a Scene</name> <name>Making a Scene</name>
@@ -239,9 +240,9 @@
<status>New</status> <status>New</status>
<expanded>False</expanded> <expanded>False</expanded>
<layout>SCENE</layout> <layout>SCENE</layout>
<charCount>30</charCount> <charCount>0</charCount>
<wordCount>6</wordCount> <wordCount>0</wordCount>
<paraCount>1</paraCount> <paraCount>0</paraCount>
<cursorPos>36</cursorPos> <cursorPos>36</cursorPos>
</item> </item>
</content> </content>
+1
View File
@@ -7,6 +7,7 @@
</project> </project>
<settings> <settings>
<spellCheck>False</spellCheck> <spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
+1
View File
@@ -7,6 +7,7 @@
</project> </project>
<settings> <settings>
<spellCheck>True</spellCheck> <spellCheck>True</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>31489056e0916</lastEdited> <lastEdited>31489056e0916</lastEdited>
<lastViewed>31489056e0916</lastViewed> <lastViewed>31489056e0916</lastViewed>
<lastWordCount>86</lastWordCount> <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> </project>
<settings> <settings>
<spellCheck>False</spellCheck> <spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
+1
View File
@@ -7,6 +7,7 @@
</project> </project>
<settings> <settings>
<spellCheck>False</spellCheck> <spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
+1
View File
@@ -7,6 +7,7 @@
</project> </project>
<settings> <settings>
<spellCheck>False</spellCheck> <spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
+1
View File
@@ -7,6 +7,7 @@
</project> </project>
<settings> <settings>
<spellCheck>False</spellCheck> <spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
-26
View File
@@ -9,7 +9,6 @@ from os import path, unlink
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from nw.gui.dialogs.projecteditor import GuiProjectEditor from nw.gui.dialogs.projecteditor import GuiProjectEditor
from nw.gui.dialogs.timelineview import GuiTimeLineView
from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.constants import * 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")) assert cmpFiles(refFile, path.join(nwRef,"gui","1_1489056e0916_main.nwd"))
refFile = path.join(nwTempGUI,"data_7","688b6ef52555_main.nwd") refFile = path.join(nwTempGUI,"data_7","688b6ef52555_main.nwd")
assert cmpFiles(refFile, path.join(nwRef,"gui","1_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() nwGUI.closeMain()
# qtbot.stopForInteraction() # 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 @pytest.mark.gui
def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp): def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % 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 str(thePos) == "[0, 6, 12]"
assert theProject.closeProject() 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()