diff --git a/nw/assets/themes/syntax/default_dark.conf b/nw/assets/themes/syntax/default_dark.conf index 8d1cd6b4..b5658bfc 100644 --- a/nw/assets/themes/syntax/default_dark.conf +++ b/nw/assets/themes/syntax/default_dark.conf @@ -17,3 +17,4 @@ value = 184, 200, 0 spellcheckline = 200, 46, 0 tagerror = 46, 200, 0 replacetag = 0, 184, 46 +modifier = 200, 120, 0 diff --git a/nw/assets/themes/syntax/default_light.conf b/nw/assets/themes/syntax/default_light.conf index 709a1ab2..07eea504 100644 --- a/nw/assets/themes/syntax/default_light.conf +++ b/nw/assets/themes/syntax/default_light.conf @@ -17,3 +17,4 @@ value = 50, 150, 50 spellcheckline = 200, 0, 0 tagerror = 0, 150, 0 replacetag = 0, 150, 0 +modifier = 150, 110, 30 diff --git a/nw/assets/themes/syntax/light_owl.conf b/nw/assets/themes/syntax/light_owl.conf index 8f6b54a5..788cea2c 100644 --- a/nw/assets/themes/syntax/light_owl.conf +++ b/nw/assets/themes/syntax/light_owl.conf @@ -40,3 +40,4 @@ value = 150, 74, 193 spellcheckline = 222, 61, 58 tagerror = 8, 145, 106 replacetag = 42, 162, 152 +modifier = 224, 175, 5 diff --git a/nw/assets/themes/syntax/night_owl.conf b/nw/assets/themes/syntax/night_owl.conf index 32d22ddf..33fbf47e 100644 --- a/nw/assets/themes/syntax/night_owl.conf +++ b/nw/assets/themes/syntax/night_owl.conf @@ -40,3 +40,4 @@ value = 199, 146, 234 spellcheckline = 247, 140, 108 tagerror = 173, 219, 103 replacetag = 127, 219, 202 +modifier = 236, 196, 141 diff --git a/nw/assets/themes/syntax/tomorrow.conf b/nw/assets/themes/syntax/tomorrow.conf index 909e9d65..26435950 100644 --- a/nw/assets/themes/syntax/tomorrow.conf +++ b/nw/assets/themes/syntax/tomorrow.conf @@ -40,3 +40,4 @@ value = 137, 89, 168 spellcheckline = 240, 40, 41 tagerror = 113, 140, 0 replacetag = 62, 153, 159 +modifier = 245, 135, 31 diff --git a/nw/assets/themes/syntax/tomorrow_night.conf b/nw/assets/themes/syntax/tomorrow_night.conf index 09966ff0..817f255a 100644 --- a/nw/assets/themes/syntax/tomorrow_night.conf +++ b/nw/assets/themes/syntax/tomorrow_night.conf @@ -40,3 +40,4 @@ value = 178, 148, 187 spellcheckline = 204, 102, 102 tagerror = 181, 189, 104 replacetag = 138, 190, 183 +modifier = 222, 147, 95 diff --git a/nw/assets/themes/syntax/tomorrow_night_blue.conf b/nw/assets/themes/syntax/tomorrow_night_blue.conf index d05a757b..e1c36135 100644 --- a/nw/assets/themes/syntax/tomorrow_night_blue.conf +++ b/nw/assets/themes/syntax/tomorrow_night_blue.conf @@ -40,3 +40,4 @@ value = 235, 187, 255 spellcheckline = 255, 157, 164 tagerror = 209, 241, 169 replacetag = 153, 255, 255 +modifier = 255, 197, 143 diff --git a/nw/assets/themes/syntax/tomorrow_night_bright.conf b/nw/assets/themes/syntax/tomorrow_night_bright.conf index 0d9c6c3a..1c4122e3 100644 --- a/nw/assets/themes/syntax/tomorrow_night_bright.conf +++ b/nw/assets/themes/syntax/tomorrow_night_bright.conf @@ -40,3 +40,4 @@ value = 195, 151, 216 spellcheckline = 213, 78, 83 tagerror = 185, 202, 74 replacetag = 112, 192, 177 +modifier = 231, 140, 69 diff --git a/nw/assets/themes/syntax/tomorrow_night_eighties.conf b/nw/assets/themes/syntax/tomorrow_night_eighties.conf index 5bd0c222..e75ffe31 100644 --- a/nw/assets/themes/syntax/tomorrow_night_eighties.conf +++ b/nw/assets/themes/syntax/tomorrow_night_eighties.conf @@ -40,3 +40,4 @@ value = 204, 153, 204 spellcheckline = 242, 119, 122 tagerror = 153, 204, 153 replacetag = 102, 204, 204 +modifier = 249, 145, 57 diff --git a/nw/constants/__init__.py b/nw/constants/__init__.py index 27c14997..eeab86cc 100644 --- a/nw/constants/__init__.py +++ b/nw/constants/__init__.py @@ -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", -] \ No newline at end of file + "nwOutline", +] diff --git a/nw/constants/constants.py b/nw/constants/constants.py index b39830bc..9e9adefc 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -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 diff --git a/nw/constants/enum.py b/nw/constants/enum.py index 9d47581e..4c484a9a 100644 --- a/nw/constants/enum.py +++ b/nw/constants/enum.py @@ -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 diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index 48896ba1..87dec78c 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -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", diff --git a/nw/gui/dialogs/__init__.py b/nw/gui/dialogs/__init__.py index 49b10df3..2dbc503e 100644 --- a/nw/gui/dialogs/__init__.py +++ b/nw/gui/dialogs/__init__.py @@ -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", ] diff --git a/nw/gui/dialogs/configeditor.py b/nw/gui/dialogs/configeditor.py index 06dc5b92..0f56e8dd 100644 --- a/nw/gui/dialogs/configeditor.py +++ b/nw/gui/dialogs/configeditor.py @@ -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 diff --git a/nw/gui/dialogs/projecteditor.py b/nw/gui/dialogs/projecteditor.py index 3b8c0114..775462cc 100644 --- a/nw/gui/dialogs/projecteditor.py +++ b/nw/gui/dialogs/projecteditor.py @@ -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) diff --git a/nw/gui/dialogs/timelineview.py b/nw/gui/dialogs/timelineview.py deleted file mode 100644 index 225a451c..00000000 --- a/nw/gui/dialogs/timelineview.py +++ /dev/null @@ -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 diff --git a/nw/gui/elements/__init__.py b/nw/gui/elements/__init__.py index 572473ee..1d885de0 100644 --- a/nw/gui/elements/__init__.py +++ b/nw/gui/elements/__init__.py @@ -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", ] diff --git a/nw/gui/elements/outline.py b/nw/gui/elements/outline.py new file mode 100644 index 00000000..26ce7855 --- /dev/null +++ b/nw/gui/elements/outline.py @@ -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 diff --git a/nw/gui/elements/viewdetails.py b/nw/gui/elements/viewdetails.py index 929e8026..80cd8241 100644 --- a/nw/gui/elements/viewdetails.py +++ b/nw/gui/elements/viewdetails.py @@ -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) diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 1c13d81a..a2898263 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -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") diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 8eb74502..e200361c 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -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) diff --git a/nw/gui/tools/dochighlight.py b/nw/gui/tools/dochighlight.py index f20aece1..dc679c0a 100644 --- a/nw/gui/tools/dochighlight.py +++ b/nw/gui/tools/dochighlight.py @@ -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: diff --git a/nw/guimain.py b/nw/guimain.py index 56b314b9..cb1331bc 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -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 diff --git a/nw/project/index.py b/nw/project/index.py index dc5edb75..6e344bfe 100644 --- a/nw/project/index.py +++ b/nw/project/index.py @@ -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 diff --git a/nw/project/project.py b/nw/project/project.py index 202491af..d6f13c9a 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -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") diff --git a/nw/tools/optionstate.py b/nw/tools/optionstate.py index 44a33087..881ff4e5 100644 --- a/nw/tools/optionstate.py +++ b/nw/tools/optionstate.py @@ -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. diff --git a/sample/sampleNovel/data_6/a2d6d5f4f401_main.nwd b/sample/sampleNovel/data_6/a2d6d5f4f401_main.nwd index ee002568..cee10674 100644 --- a/sample/sampleNovel/data_6/a2d6d5f4f401_main.nwd +++ b/sample/sampleNovel/data_6/a2d6d5f4f401_main.nwd @@ -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. \ No newline at end of file +%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. \ No newline at end of file diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx index 4675a881..53cd2fe6 100644 --- a/sample/sampleNovel/nwProject.nwx +++ b/sample/sampleNovel/nwProject.nwx @@ -1,5 +1,5 @@ - + Sample Project Sample Project @@ -9,9 +9,10 @@ True - 636b6aa9b697b + True + 6a2d6d5f4f401 b3e74dbc1f584 - 875 + 869 B E @@ -70,7 +71,7 @@ 12 3 0 - 45 + 15 Making a Scene @@ -239,9 +240,9 @@ New False SCENE - 30 - 6 - 1 + 0 + 0 + 0 36 diff --git a/tests/reference/gui/0_nwProject.nwx b/tests/reference/gui/0_nwProject.nwx index c3337adb..e5368533 100644 --- a/tests/reference/gui/0_nwProject.nwx +++ b/tests/reference/gui/0_nwProject.nwx @@ -7,6 +7,7 @@ False + True None None 0 diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx index 193087e6..02119a65 100644 --- a/tests/reference/gui/1_nwProject.nwx +++ b/tests/reference/gui/1_nwProject.nwx @@ -7,6 +7,7 @@ True + True 31489056e0916 31489056e0916 86 diff --git a/tests/reference/gui/1_tagsIndex.json b/tests/reference/gui/1_tagsIndex.json deleted file mode 100644 index 8c365f5c..00000000 --- a/tests/reference/gui/1_tagsIndex.json +++ /dev/null @@ -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"]]}} \ No newline at end of file diff --git a/tests/reference/gui/2_nwProject.nwx b/tests/reference/gui/2_nwProject.nwx index 02c51167..ca896ec2 100644 --- a/tests/reference/gui/2_nwProject.nwx +++ b/tests/reference/gui/2_nwProject.nwx @@ -9,6 +9,7 @@ False + True None None 0 diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index 503c011c..f0bda720 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -7,6 +7,7 @@ False + True None None 0 diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/proj/1_nwProject.nwx index 4960024b..319d24b3 100644 --- a/tests/reference/proj/1_nwProject.nwx +++ b/tests/reference/proj/1_nwProject.nwx @@ -7,6 +7,7 @@ False + True None None 0 diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/proj/2_nwProject.nwx index 0a8e6b4d..a40a2e8d 100644 --- a/tests/reference/proj/2_nwProject.nwx +++ b/tests/reference/proj/2_nwProject.nwx @@ -7,6 +7,7 @@ False + True None None 0 diff --git a/tests/test_gui.py b/tests/test_gui.py index b039a9d8..c8f8d013 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -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]) diff --git a/tests/test_project.py b/tests/test_project.py index 6723dcc7..a38d6d58 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -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()