Merge pull request #140 from vkbo/project_outline
Synopsis and Outline Feature
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -40,3 +40,4 @@ value = 150, 74, 193
|
||||
spellcheckline = 222, 61, 58
|
||||
tagerror = 8, 145, 106
|
||||
replacetag = 42, 162, 152
|
||||
modifier = 224, 175, 5
|
||||
|
||||
@@ -40,3 +40,4 @@ value = 199, 146, 234
|
||||
spellcheckline = 247, 140, 108
|
||||
tagerror = 173, 219, 103
|
||||
replacetag = 127, 219, 202
|
||||
modifier = 236, 196, 141
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
</project>
|
||||
<settings>
|
||||
<spellCheck>False</spellCheck>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
</project>
|
||||
<settings>
|
||||
<spellCheck>True</spellCheck>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>31489056e0916</lastEdited>
|
||||
<lastViewed>31489056e0916</lastViewed>
|
||||
<lastWordCount>86</lastWordCount>
|
||||
|
||||
@@ -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"]]}}
|
||||
@@ -9,6 +9,7 @@
|
||||
</project>
|
||||
<settings>
|
||||
<spellCheck>False</spellCheck>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
</project>
|
||||
<settings>
|
||||
<spellCheck>False</spellCheck>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
</project>
|
||||
<settings>
|
||||
<spellCheck>False</spellCheck>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
</project>
|
||||
<settings>
|
||||
<spellCheck>False</spellCheck>
|
||||
<autoOutline>True</autoOutline>
|
||||
<lastEdited>None</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>0</lastWordCount>
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user