diff --git a/.gitignore b/.gitignore
index f5ebca0d..47043c35 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ __pycache__
sample/**/cache
sample/**/wordlist.txt
sample/**/*.bak
+sample/**/*.json
# PyTest
tests/temp
diff --git a/nw/__init__.py b/nw/__init__.py
index 82fb621c..0015aabd 100644
--- a/nw/__init__.py
+++ b/nw/__init__.py
@@ -151,8 +151,9 @@ def main(sysArgs):
debugGUI = True
# Set Config Options
- CONFIG.showGUI = not testMode
- CONFIG.debugGUI = debugGUI
+ CONFIG.showGUI = not testMode
+ CONFIG.debugGUI = debugGUI
+ CONFIG.debugInfo = debugLevel < logging.INFO
# Set Logging
if showTime: debugStr = timeStr+debugStr
diff --git a/nw/config.py b/nw/config.py
index 1627270f..cdf07697 100644
--- a/nw/config.py
+++ b/nw/config.py
@@ -22,9 +22,6 @@ logger = logging.getLogger(__name__)
class Config:
- WIN_WIDTH = 0
- WIN_HEIGHT = 1
-
CNF_STR = 0
CNF_INT = 1
CNF_BOOL = 2
@@ -37,6 +34,7 @@ class Config:
self.appHandle = nw.__package__.lower()
self.showGUI = True
self.debugGUI = False
+ self.debugInfo = False
# Set Paths
self.confPath = None
@@ -57,6 +55,9 @@ class Config:
self.mainPanePos = [300, 800]
self.docPanePos = [400, 400]
+ ## Dialogs
+ self.dlgTimeLine = [600, 400]
+
## Project
self.autoSaveProj = 60
self.autoSaveDoc = 30
@@ -142,6 +143,7 @@ class Config:
self.treeColWidth = self._parseLine(cnfParse, cnfSec, "treecols", self.CNF_LIST, self.treeColWidth)
self.mainPanePos = self._parseLine(cnfParse, cnfSec, "mainpane", self.CNF_LIST, self.mainPanePos)
self.docPanePos = self._parseLine(cnfParse, cnfSec, "docpane", self.CNF_LIST, self.docPanePos)
+ self.dlgTimeLine = self._parseLine(cnfParse, cnfSec, "timeline", self.CNF_LIST, self.dlgTimeLine)
## Project
cnfSec = "Project"
@@ -191,6 +193,7 @@ class Config:
cnfParse.set(cnfSec,"treecols", self._packList(self.treeColWidth))
cnfParse.set(cnfSec,"mainpane", self._packList(self.mainPanePos))
cnfParse.set(cnfSec,"docpane", self._packList(self.docPanePos))
+ cnfParse.set(cnfSec,"timeline", self._packList(self.dlgTimeLine))
## Project
cnfSec = "Project"
@@ -253,11 +256,20 @@ class Config:
return True
def setWinSize(self, newWidth, newHeight):
- if abs(self.winGeometry[self.WIN_WIDTH] - newWidth) >= 10:
- self.winGeometry[self.WIN_WIDTH] = newWidth
+ if abs(self.winGeometry[0] - newWidth) > 5:
+ self.winGeometry[0] = newWidth
self.confChanged = True
- if abs(self.winGeometry[self.WIN_HEIGHT] - newHeight) >= 10:
- self.winGeometry[self.WIN_HEIGHT] = newHeight
+ if abs(self.winGeometry[1] - newHeight) > 5:
+ self.winGeometry[1] = newHeight
+ self.confChanged = True
+ return True
+
+ def setTLineSize(self, newWidth, newHeight):
+ if abs(self.dlgTimeLine[0] - newWidth) > 5:
+ self.dlgTimeLine[0] = newWidth
+ self.confChanged = True
+ if abs(self.dlgTimeLine[1] - newHeight) > 5:
+ self.dlgTimeLine[1] = newHeight
self.confChanged = True
return True
diff --git a/nw/constants.py b/nw/constants.py
index 478a7f5f..8a545d0c 100644
--- a/nw/constants.py
+++ b/nw/constants.py
@@ -14,10 +14,11 @@ from nw.enum import nwItemClass, nwItemLayout
class nwFiles():
- APP_ICON = "novelWriter.svg"
- PROJ_FILE = "nwProject.nwx"
- PROJ_DICT = "wordlist.txt"
- SESS_INFO = "sessionInfo.log"
+ APP_ICON = "novelWriter.svg"
+ PROJ_FILE = "nwProject.nwx"
+ PROJ_DICT = "wordlist.txt"
+ SESS_INFO = "sessionInfo.log"
+ INDEX_FILE = "tagsIndex.json"
# END Class nwFiles
diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py
index c708536e..13f1092a 100644
--- a/nw/gui/doceditor.py
+++ b/nw/gui/doceditor.py
@@ -34,11 +34,12 @@ class GuiDocEditor(QTextEdit):
logger.debug("Initialising DocEditor ...")
# Class Variables
- self.mainConf = nw.CONFIG
- self.theParent = theParent
- self.docChanged = False
- self.pwlFile = None
- self.spellCheck = False
+ self.mainConf = nw.CONFIG
+ self.theParent = theParent
+ self.docChanged = False
+ self.pwlFile = None
+ self.spellCheck = False
+ self.theDocument = theParent.theDocument
# Document Variables
self.charCount = 0
@@ -54,9 +55,9 @@ class GuiDocEditor(QTextEdit):
self.typApos = self.mainConf.fmtApostrophe
# Core Elements
- self.theDoc = self.document()
+ self.theQDoc = self.document()
self.theDict = enchant.Dict(self.mainConf.spellLanguage)
- self.hLight = GuiDocHighlighter(self.theDoc, self.theParent.theTheme)
+ self.hLight = GuiDocHighlighter(self.theQDoc, self.theParent)
self.hLight.setDict(self.theDict)
# Context Menu
@@ -71,8 +72,8 @@ class GuiDocEditor(QTextEdit):
self.clearEditor()
self.initEditor()
- self.theDoc.setDocumentMargin(0)
- self.theDoc.contentsChange.connect(self._docChange)
+ self.theQDoc.setDocumentMargin(0)
+ self.theQDoc.contentsChange.connect(self._docChange)
# Custom Shortcuts
QShortcut(QKeySequence("Ctrl+."), self, context=Qt.WidgetShortcut, activated=self._openSpellContext)
@@ -109,7 +110,18 @@ class GuiDocEditor(QTextEdit):
theOpt.setTabStopDistance(self.mainConf.tabWidth)
if self.mainConf.doJustify:
theOpt.setAlignment(Qt.AlignJustify)
- self.theDoc.setDefaultTextOption(theOpt)
+ self.theQDoc.setDefaultTextOption(theOpt)
+ return True
+
+ def loadText(self, tHandle):
+ self.hLight.setHandle(tHandle)
+ self.setPlainText(self.theDocument.openDocument(tHandle))
+ self.setCursorPosition(self.theDocument.theItem.cursorPos)
+ self.lastEdit = time()
+ self._runCounter()
+ self.wcTimer.start()
+ self.setDocumentChanged(False)
+ self.setReadOnly(False)
return True
##
@@ -121,14 +133,6 @@ class GuiDocEditor(QTextEdit):
self.theParent.statusBar.setDocumentStatus(self.docChanged)
return self.docChanged
- def setText(self, theText):
- self.setPlainText(theText)
- self.lastEdit = time()
- self._runCounter()
- self.wcTimer.start()
- self.setDocumentChanged(False)
- return True
-
def getText(self):
theText = self.toPlainText()
return theText
@@ -296,7 +300,7 @@ class GuiDocEditor(QTextEdit):
if not self.wcTimer.isActive():
self.wcTimer.start()
if self.mainConf.doReplace and not self.hasSelection:
- self._docAutoReplace(self.theDoc.findBlock(thePos))
+ self._docAutoReplace(self.theQDoc.findBlock(thePos))
# logger.verbose("Doc change signal took %.3f µs" % ((time()-self.lastEdit)*1e6))
return
diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py
index 697b2dea..c99fa3ba 100644
--- a/nw/gui/dochighlight.py
+++ b/nw/gui/dochighlight.py
@@ -20,14 +20,17 @@ logger = logging.getLogger(__name__)
class GuiDocHighlighter(QSyntaxHighlighter):
- def __init__(self, theDoc, theTheme):
+ def __init__(self, theDoc, theParent):
QSyntaxHighlighter.__init__(self, theDoc)
logger.debug("Initialising DocHighlighter ...")
self.mainConf = nw.CONFIG
self.theDoc = theDoc
- self.theTheme = theTheme
+ self.theParent = theParent
+ self.theTheme = theParent.theTheme
+ self.theIndex = theParent.theIndex
self.theDict = None
+ self.theHandle = None
self.spellCheck = False
self.hRules = []
@@ -41,6 +44,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.colKey = QColor(*self.theTheme.colKey)
self.colVal = QColor(*self.theTheme.colVal)
self.colSpell = QColor(*self.theTheme.colSpell)
+ self.colTagErr = QColor(*self.theTheme.colTagErr)
self.hStyles = {
"header1" : self._makeFormat(self.colHead, "bold",1.8),
@@ -90,12 +94,12 @@ class GuiDocHighlighter(QSyntaxHighlighter):
))
# Keyword/Value
- self.hRules.append((
- r"^(@.+?)\s*:\s*(.+?)$", {
- 1 : self.hStyles["keyword"],
- 2 : self.hStyles["value"],
- }
- ))
+ # self.hRules.append((
+ # r"^(@.+?)\s*:\s*(.+?)$", {
+ # 1 : self.hStyles["keyword"],
+ # 2 : self.hStyles["value"],
+ # }
+ # ))
# Comments
self.hRules.append((
@@ -152,6 +156,10 @@ class GuiDocHighlighter(QSyntaxHighlighter):
return
+ ##
+ # Setters
+ ##
+
def setDict(self, theDict):
self.theDict = theDict
return True
@@ -160,6 +168,75 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.spellCheck = theMode
return True
+ def setHandle(self, theHandle):
+ self.theHandle = theHandle
+ return True
+
+ ##
+ # Highlight Block
+ ##
+
+ def highlightBlock(self, theText):
+
+ if self.theHandle is None:
+ self.setCurrentBlockState(0)
+ return
+
+ if theText.startswith("@"):
+ # Highlighting of keywords and commands
+ tItem = self.theParent.theProject.getItem(self.theHandle)
+ isValid, theBits, thePos = self.theIndex.scanThis(theText)
+ isGood = self.theIndex.checkThese(theBits, tItem)
+ if isValid:
+ for n in range(len(theBits)):
+ xPos = thePos[n]
+ xLen = len(theBits[n])
+ if isGood[n]:
+ if n == 0:
+ self.setFormat(xPos, xLen, self.hStyles["keyword"])
+ else:
+ self.setFormat(xPos, xLen, self.hStyles["value"])
+ else:
+ kwFmt = self.format(xPos)
+ kwFmt.setUnderlineColor(self.colTagErr)
+ kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
+ self.setFormat(xPos, xLen, kwFmt)
+
+ else:
+ # Other text just uses regex
+ for rX, xFmt in self.rules:
+ rxItt = rX.globalMatch(theText, 0)
+ while rxItt.hasNext():
+ rxMatch = rxItt.next()
+ for xM in xFmt.keys():
+ xPos = rxMatch.capturedStart(xM)
+ xLen = rxMatch.capturedLength(xM)
+ self.setFormat(xPos, xLen, xFmt[xM])
+
+ self.setCurrentBlockState(0)
+
+ if self.theDict is None or not self.spellCheck or theText.startswith("@"):
+ return
+
+ rxSpell = self.spellRx.globalMatch(theText.replace("_"," "), 0)
+ while rxSpell.hasNext():
+ rxMatch = rxSpell.next()
+ if not self.theDict.check(rxMatch.captured(0)):
+ if rxMatch.captured(0) == rxMatch.captured(0).upper():
+ continue
+ xPos = rxMatch.capturedStart(0)
+ xLen = rxMatch.capturedLength(0)
+ spFmt = self.format(xPos)
+ spFmt.setUnderlineColor(self.colSpell)
+ spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
+ self.setFormat(xPos, xLen, spFmt)
+
+ return
+
+ ##
+ # Internal Functions
+ ##
+
def _makeFormat(self, fmtCol=None, fmtStyle=None, fmtSize=None):
theFormat = QTextCharFormat()
@@ -181,34 +258,4 @@ class GuiDocHighlighter(QSyntaxHighlighter):
return theFormat
- def highlightBlock(self, theText):
-
- for rX, xFmt in self.rules:
- rxItt = rX.globalMatch(theText, 0)
- while rxItt.hasNext():
- rxMatch = rxItt.next()
- for xM in xFmt.keys():
- xPos = rxMatch.capturedStart(xM)
- xLen = rxMatch.capturedLength(xM)
- self.setFormat(xPos, xLen, xFmt[xM])
-
- self.setCurrentBlockState(0)
-
- if self.theDict is None or not self.spellCheck or theText.startswith("@"):
- return
-
- rxSpell = self.spellRx.globalMatch(theText.replace("_"," "), 0)
- while rxSpell.hasNext():
- rxMatch = rxSpell.next()
- if not self.theDict.check(rxMatch.captured(0)):
- if rxMatch.captured(0) == rxMatch.captured(0).upper(): continue
- xPos = rxMatch.capturedStart(0)
- xLen = rxMatch.capturedLength(0)
- spFmt = self.format(xPos)
- spFmt.setUnderlineColor(self.colSpell)
- spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
- self.setFormat(xPos, xLen, spFmt)
-
- return
-
# END Class DocHighlighter
diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py
index 981933ee..a0816b36 100644
--- a/nw/gui/mainmenu.py
+++ b/nw/gui/mainmenu.py
@@ -135,27 +135,27 @@ class GuiMainMenu(QMenuBar):
# Project > New Project
menuItem = QAction(QIcon.fromTheme("folder-new"), "New Project", self)
- menuItem.setStatusTip("Create New Project")
+ menuItem.setStatusTip("Create new project")
menuItem.triggered.connect(lambda : self.theParent.newProject(None))
self.projMenu.addAction(menuItem)
# Project > Open Project
menuItem = QAction(QIcon.fromTheme("folder-open"), "Open Project", self)
- menuItem.setStatusTip("Open Project")
+ menuItem.setStatusTip("Open project")
menuItem.setShortcut("Ctrl+Shift+O")
menuItem.triggered.connect(lambda : self.theParent.openProject(None))
self.projMenu.addAction(menuItem)
# Project > Save Project
menuItem = QAction(QIcon.fromTheme("document-save"), "Save Project", self)
- menuItem.setStatusTip("Save Project")
+ menuItem.setStatusTip("Save project")
menuItem.setShortcut("Ctrl+Shift+S")
menuItem.triggered.connect(self.theParent.saveProject)
self.projMenu.addAction(menuItem)
# Project > Close Project
menuItem = QAction(QIcon.fromTheme("document-revert"), "Close Project", self)
- menuItem.setStatusTip("Close Project")
+ menuItem.setStatusTip("Close project")
menuItem.setShortcut("Ctrl+Shift+W")
menuItem.triggered.connect(lambda : self.theParent.closeProject(False))
self.projMenu.addAction(menuItem)
@@ -166,7 +166,7 @@ class GuiMainMenu(QMenuBar):
# Project > Project Settings
menuItem = QAction(QIcon.fromTheme("document-properties"), "Project Settings", self)
- menuItem.setStatusTip("Project Settings")
+ menuItem.setStatusTip("Project settings")
menuItem.triggered.connect(self.theParent.editProjectDialog)
self.projMenu.addAction(menuItem)
@@ -193,7 +193,7 @@ class GuiMainMenu(QMenuBar):
# Project > New Folder
menuItem = QAction(QIcon.fromTheme("folder-new"), "Create Folder", self)
- menuItem.setStatusTip("Create Folder")
+ menuItem.setStatusTip("Create folder")
menuItem.setShortcut("Ctrl+Shift+N")
menuItem.triggered.connect(lambda : self._newTreeItem(nwItemType.FOLDER, None))
self.projMenu.addAction(menuItem)
@@ -203,14 +203,14 @@ class GuiMainMenu(QMenuBar):
# Project > Edit
menuItem = QAction(QIcon.fromTheme("document-properties"), "&Edit Item", self)
- menuItem.setStatusTip("Change Item Settings")
+ menuItem.setStatusTip("Change item settings")
menuItem.setShortcuts(["Ctrl+E", "F2"])
menuItem.triggered.connect(self.theParent.editItem)
self.projMenu.addAction(menuItem)
# Project > Delete
menuItem = QAction(QIcon.fromTheme("edit-delete"), "&Delete Item", self)
- menuItem.setStatusTip("Delete Selected Item")
+ menuItem.setStatusTip("Delete selected item")
menuItem.setShortcut("Ctrl+Del")
menuItem.triggered.connect(lambda : self.theParent.treeView.deleteItem(None))
self.projMenu.addAction(menuItem)
@@ -234,28 +234,28 @@ class GuiMainMenu(QMenuBar):
# Document > New
menuItem = QAction(QIcon.fromTheme("document-new"), "&New Document", self)
- menuItem.setStatusTip("Create New Document")
+ menuItem.setStatusTip("Create new document")
menuItem.setShortcut("Ctrl+N")
menuItem.triggered.connect(lambda : self._newTreeItem(nwItemType.FILE, None))
self.docuMenu.addAction(menuItem)
# Document > Open
menuItem = QAction(QIcon.fromTheme("document-open"), "&Open Document", self)
- menuItem.setStatusTip("Open Selected Document")
+ menuItem.setStatusTip("Open selected document")
menuItem.setShortcut("Ctrl+O")
menuItem.triggered.connect(self.theParent.openSelectedItem)
self.docuMenu.addAction(menuItem)
# Document > Save
menuItem = QAction(QIcon.fromTheme("document-save"), "&Save Document", self)
- menuItem.setStatusTip("Save Current Document")
+ menuItem.setStatusTip("Save current document")
menuItem.setShortcut("Ctrl+S")
menuItem.triggered.connect(self.theParent.saveDocument)
self.docuMenu.addAction(menuItem)
# Document > Close
menuItem = QAction(QIcon.fromTheme("document-revert"), "Close Document", self)
- menuItem.setStatusTip("Close Current Document")
+ menuItem.setStatusTip("Close current document")
menuItem.setShortcut("Ctrl+W")
menuItem.triggered.connect(self.theParent.closeDocEditor)
self.docuMenu.addAction(menuItem)
@@ -265,14 +265,14 @@ class GuiMainMenu(QMenuBar):
# Document > Preview
menuItem = QAction(QIcon.fromTheme("text-html"), "View Document", self)
- menuItem.setStatusTip("View Document in HTML")
+ menuItem.setStatusTip("View document as HTML")
menuItem.setShortcut("Ctrl+R")
menuItem.triggered.connect(lambda : self.theParent.viewDocument(None))
self.docuMenu.addAction(menuItem)
# Document > Close Preview
menuItem = QAction(QIcon.fromTheme("text-html"), "Close Document View", self)
- menuItem.setStatusTip("Close Document View Pane")
+ menuItem.setStatusTip("Close document view pane")
menuItem.setShortcut("Ctrl+Shift+R")
menuItem.triggered.connect(self.theParent.closeDocViewer)
self.docuMenu.addAction(menuItem)
@@ -299,25 +299,35 @@ class GuiMainMenu(QMenuBar):
# View > TreeView
menuItem = QAction(QIcon.fromTheme("go-home"), "TreeView", self)
- menuItem.setStatusTip("Move to TreeView Panel")
+ menuItem.setStatusTip("Move focus to project tree")
menuItem.setShortcut("Ctrl+1")
menuItem.triggered.connect(lambda : self.theParent.setFocus(1))
self.viewMenu.addAction(menuItem)
# View > Document Pane 1
menuItem = QAction(QIcon.fromTheme("go-first"), "Left Document Pane", self)
- menuItem.setStatusTip("Move to Left Document Pane")
+ menuItem.setStatusTip("Move focus to left document pane")
menuItem.setShortcut("Ctrl+2")
menuItem.triggered.connect(lambda : self.theParent.setFocus(2))
self.viewMenu.addAction(menuItem)
- # # View > Document Pane 2
+ # View > Document Pane 2
menuItem = QAction(QIcon.fromTheme("go-last"), "Right Document Pane", self)
- menuItem.setStatusTip("Move to Right Document Pane")
+ menuItem.setStatusTip("Move focus to right document pane")
menuItem.setShortcut("Ctrl+3")
menuItem.triggered.connect(lambda : self.theParent.setFocus(3))
self.viewMenu.addAction(menuItem)
+ # View > Separator
+ self.viewMenu.addSeparator()
+
+ # View > Project Timeline
+ menuItem = QAction(QIcon.fromTheme("x-office-spreadsheet"), "Show Project Timeline", self)
+ menuItem.setStatusTip("Open the project timeline window")
+ menuItem.setShortcut("Ctrl+T")
+ menuItem.triggered.connect(self.theParent.showTimeLineDialog)
+ self.viewMenu.addAction(menuItem)
+
return
def _buildEditMenu(self):
@@ -327,14 +337,14 @@ class GuiMainMenu(QMenuBar):
# Edit > Undo
menuItem = QAction(QIcon.fromTheme("edit-undo"), "Undo", self)
- menuItem.setStatusTip("Undo Last Change")
+ menuItem.setStatusTip("Undo last change")
menuItem.setShortcut("Ctrl+Z")
menuItem.triggered.connect(lambda: self._docAction(nwDocAction.UNDO))
self.editMenu.addAction(menuItem)
# Edit > Redo
menuItem = QAction(QIcon.fromTheme("edit-redo"), "Redo", self)
- menuItem.setStatusTip("Redo Last Change")
+ menuItem.setStatusTip("Redo last change")
menuItem.setShortcut("Ctrl+Y")
menuItem.triggered.connect(lambda: self._docAction(nwDocAction.REDO))
self.editMenu.addAction(menuItem)
@@ -344,21 +354,21 @@ class GuiMainMenu(QMenuBar):
# Edit > Cut
menuItem = QAction(QIcon.fromTheme("edit-cut"), "Cut", self)
- menuItem.setStatusTip("Cut Selected Text")
+ menuItem.setStatusTip("Cut selected text")
menuItem.setShortcut("Ctrl+X")
menuItem.triggered.connect(lambda: self._docAction(nwDocAction.CUT))
self.editMenu.addAction(menuItem)
# Edit > Copy
menuItem = QAction(QIcon.fromTheme("edit-copy"), "Copy", self)
- menuItem.setStatusTip("Copy Selected Text")
+ menuItem.setStatusTip("Copy selected text")
menuItem.setShortcut("Ctrl+C")
menuItem.triggered.connect(lambda: self._docAction(nwDocAction.COPY))
self.editMenu.addAction(menuItem)
# Edit > Paste
menuItem = QAction(QIcon.fromTheme("edit-paste"), "Paste", self)
- menuItem.setStatusTip("Paste Text from Clipboard")
+ menuItem.setStatusTip("Paste text from clipboard")
menuItem.setShortcut("Ctrl+V")
menuItem.triggered.connect(lambda: self._docAction(nwDocAction.PASTE))
self.editMenu.addAction(menuItem)
@@ -368,14 +378,14 @@ class GuiMainMenu(QMenuBar):
# Edit > Select All
menuItem = QAction(QIcon.fromTheme("edit-select-all"), "Select All", self)
- menuItem.setStatusTip("Select All Text in Document")
+ menuItem.setStatusTip("Select all text in document")
menuItem.setShortcut("Ctrl+A")
menuItem.triggered.connect(lambda: self._docAction(nwDocAction.SEL_ALL))
self.editMenu.addAction(menuItem)
# Edit > Select Paragraph
menuItem = QAction(QIcon.fromTheme("edit-select-all"), "Select Paragraph", self)
- menuItem.setStatusTip("Select All Text in Paragraph")
+ menuItem.setStatusTip("Select all text in paragraph")
menuItem.setShortcut("Ctrl+Shift+A")
menuItem.triggered.connect(lambda: self._docAction(nwDocAction.SEL_PARA))
self.editMenu.addAction(menuItem)
@@ -389,21 +399,21 @@ class GuiMainMenu(QMenuBar):
# Format > Bold Text
menuItem = QAction(QIcon.fromTheme("format-text-bold"), "Bold Text", self)
- menuItem.setStatusTip("Make Selected Text Bold")
+ menuItem.setStatusTip("Make selected text bold")
menuItem.setShortcut("Ctrl+B")
menuItem.triggered.connect(lambda: self._docAction(nwDocAction.BOLD))
self.fmtMenu.addAction(menuItem)
# Format > Italic Text
menuItem = QAction(QIcon.fromTheme("format-text-italic"), "Italic Text", self)
- menuItem.setStatusTip("Make Selected Text Italic")
+ menuItem.setStatusTip("Make selected text italic")
menuItem.setShortcut("Ctrl+I")
menuItem.triggered.connect(lambda: self._docAction(nwDocAction.ITALIC))
self.fmtMenu.addAction(menuItem)
# Format > Underline Text
menuItem = QAction(QIcon.fromTheme("format-text-underline"), "Underline Text", self)
- menuItem.setStatusTip("Underline Selected Text")
+ menuItem.setStatusTip("Underline selected text")
menuItem.setShortcut("Ctrl+U")
menuItem.triggered.connect(lambda: self._docAction(nwDocAction.U_LINE))
self.fmtMenu.addAction(menuItem)
@@ -413,14 +423,14 @@ class GuiMainMenu(QMenuBar):
# Format > Double Quotes
menuItem = QAction(QIcon.fromTheme("insert-text"), "Wrap Double Quotes", self)
- menuItem.setStatusTip("Wrap Selected Text in Double Quotes")
+ menuItem.setStatusTip("Wrap selected text in double quotes")
menuItem.setShortcut("Ctrl+D")
menuItem.triggered.connect(lambda: self._docAction(nwDocAction.D_QUOTE))
self.fmtMenu.addAction(menuItem)
# Format > Single Quotes
menuItem = QAction(QIcon.fromTheme("insert-text"), "Wrap Single Quotes", self)
- menuItem.setStatusTip("Wrap Selected Text in Single Quotes")
+ menuItem.setStatusTip("Wrap selected text in single quotes")
menuItem.setShortcut("Ctrl+Shift+D")
menuItem.triggered.connect(lambda: self._docAction(nwDocAction.S_QUOTE))
self.fmtMenu.addAction(menuItem)
@@ -434,14 +444,14 @@ class GuiMainMenu(QMenuBar):
# Tools > Move Up
self.toolsMoveUp = QAction(QIcon.fromTheme("go-up"), "Move Tree Item Up", self)
- self.toolsMoveUp.setStatusTip("Move Item Up")
+ self.toolsMoveUp.setStatusTip("Move item up")
self.toolsMoveUp.setShortcut("Ctrl+Shift+Up")
self.toolsMoveUp.triggered.connect(lambda : self._moveTreeItem(-1))
self.toolsMenu.addAction(self.toolsMoveUp)
# Tools > Move Down
self.toolsMoveDown = QAction(QIcon.fromTheme("go-down"), "Move Tree Item Down", self)
- self.toolsMoveDown.setStatusTip("Move Item Down")
+ self.toolsMoveDown.setStatusTip("Move item down")
self.toolsMoveDown.setShortcut("Ctrl+Shift+Down")
self.toolsMoveDown.triggered.connect(lambda : self._moveTreeItem(1))
self.toolsMenu.addAction(self.toolsMoveDown)
@@ -451,7 +461,7 @@ class GuiMainMenu(QMenuBar):
# Tools > Toggle Spell Check
self.toolsSpellCheck = QAction("Check Spelling", self)
- self.toolsSpellCheck.setStatusTip("Toggle Check Spelling")
+ self.toolsSpellCheck.setStatusTip("Toggle check spelling")
self.toolsSpellCheck.setCheckable(True)
self.toolsSpellCheck.setChecked(self.theProject.spellCheck)
self.toolsSpellCheck.toggled.connect(self._toggleSpellCheck)
@@ -460,11 +470,21 @@ class GuiMainMenu(QMenuBar):
# Tools > Update Spell Check
menuItem = QAction(QIcon.fromTheme("tools-check-spelling"), "Re-Run Spell Check", self)
- menuItem.setStatusTip("Rus the Spell Checker on Current Document")
+ menuItem.setStatusTip("Run the spell checker on current document")
menuItem.setShortcut("F7")
menuItem.triggered.connect(self.theParent.docEditor.updateSpellCheck)
self.toolsMenu.addAction(menuItem)
+ # Tools > Separator
+ self.toolsMenu.addSeparator()
+
+ # Tools > Rebuild Indices
+ menuItem = QAction(QIcon.fromTheme("edit-redo"), "Rebuild Indices", self)
+ menuItem.setStatusTip("Rebuild the tag indices and word counts")
+ menuItem.setShortcut("F9")
+ menuItem.triggered.connect(self.theParent.rebuildIndex)
+ self.toolsMenu.addAction(menuItem)
+
# # Tools > Settings
# menuItem = QAction(QIcon.fromTheme("preferences-system"), "Preferences", self)
# menuItem.setStatusTip("Preferences")
diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py
index 484e9519..79952289 100644
--- a/nw/gui/statusbar.py
+++ b/nw/gui/statusbar.py
@@ -88,7 +88,7 @@ class GuiMainStatus(QStatusBar):
self.setRefTime(None)
self.setStats(0,0)
self.setCounts(0,0,0)
- self.setDocHandleCount(None)
+ self.setDocHandle(None)
self.setProjectStatus(None)
self.setDocumentStatus(None)
self._updateTime()
@@ -132,7 +132,7 @@ class GuiMainStatus(QStatusBar):
self.boxCounts.setText("Document: {:d} : {:d} : {:d}".format(cC,wC,pC))
return
- def setDocHandleCount(self, theHandle):
+ def setDocHandle(self, theHandle):
if theHandle is None:
self.boxDocHandle.setText("0000000000000")
else:
diff --git a/nw/gui/timelineview.py b/nw/gui/timelineview.py
new file mode 100644
index 00000000..05a041a3
--- /dev/null
+++ b/nw/gui/timelineview.py
@@ -0,0 +1,130 @@
+# -*- 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 QIcon, QColor, QPixmap
+from PyQt5.QtWidgets import (
+ QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
+ QDialogButtonBox, QLabel, QPushButton, QHeaderView
+)
+
+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.theMatrix = {}
+ self.numRows = 0
+ self.numCols = 0
+
+ self.outerBox = QVBoxLayout()
+ self.bottomBox = QHBoxLayout()
+
+ self.setWindowTitle("Timeline View")
+ self.setMinimumSize(*self.mainConf.dlgTimeLine)
+
+ 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)
+
+ 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.setLayout(self.outerBox)
+ self.outerBox.addWidget(self.mainTable)
+ self.outerBox.addLayout(self.bottomBox)
+ self.bottomBox.addWidget(self.btnRebuild)
+ self.bottomBox.addWidget(self.btnRefresh)
+ self.bottomBox.addStretch()
+ self.bottomBox.addWidget(self.buttonBox)
+
+ self._buildNovelList()
+ self.buttonBox.setFocus()
+
+ self.show()
+
+ logger.debug("TimeLineView initialisation complete")
+
+ return
+
+ def _buildNovelList(self):
+
+ self.theIndex.buildNovelList()
+ self.numRows = len(self.theIndex.novelList)
+ self.numCols = len(self.theIndex.tagIndex.keys())
+
+ self.mainTable.clear()
+ self.mainTable.setRowCount(self.numRows)
+ self.mainTable.setColumnCount(self.numCols)
+
+ 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())
+ 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):
+ self.mainConf.setTLineSize(self.width(), self.height())
+ self.close()
+ return
+
+# END Class GuiItemEditor
diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py
index 0de1c27f..a4cbec70 100644
--- a/nw/gui/winmain.py
+++ b/nw/gui/winmain.py
@@ -11,6 +11,7 @@
"""
import logging
+import time
import nw
from os import path
@@ -18,7 +19,7 @@ from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QIcon, QPixmap, QColor
from PyQt5.QtWidgets import (
qApp, QWidget, QMainWindow, QVBoxLayout, QFrame, QSplitter, QFileDialog,
- QShortcut, QMessageBox
+ QShortcut, QMessageBox, QProgressDialog
)
from nw.theme import Theme
@@ -30,13 +31,16 @@ from nw.gui.mainmenu import GuiMainMenu
from nw.gui.projecteditor import GuiProjectEditor
from nw.gui.itemeditor import GuiItemEditor
from nw.gui.statusbar import GuiMainStatus
+from nw.gui.timelineview import GuiTimeLineView
from nw.project.project import NWProject
from nw.project.document import NWDoc
from nw.project.item import NWItem
+from nw.project.index import NWIndex
from nw.convert.tokenizer import Tokenizer
from nw.convert.tohtml import ToHtml
from nw.enum import nwItemType, nwAlert
from nw.constants import nwFiles
+from nw.tools.wordcount import countWords
logger = logging.getLogger(__name__)
@@ -50,6 +54,7 @@ class GuiMain(QMainWindow):
self.theTheme = Theme()
self.theProject = NWProject(self)
self.theDocument = NWDoc(self.theProject, self)
+ self.theIndex = NWIndex(self.theProject, self)
self.hasProject = False
self.resize(*self.mainConf.winGeometry)
@@ -208,6 +213,7 @@ class GuiMain(QMainWindow):
if saveOK:
self.theProject.closeProject()
+ self.theIndex.clearIndex()
self.clearGUI()
self.hasProject = False
@@ -230,6 +236,9 @@ class GuiMain(QMainWindow):
if not self.theProject.openProject(projFile):
return False
+ # Load the tag index
+ self.theIndex.loadIndex()
+
# Update GUI
self._setWindowTitle(self.theProject.projName)
self.rebuildTree()
@@ -260,6 +269,7 @@ class GuiMain(QMainWindow):
self.treeView.saveTreeOrder()
self.theProject.saveProject()
+ self.theIndex.saveIndex()
self.mainMenu.updateRecentProjects()
return True
@@ -277,9 +287,7 @@ class GuiMain(QMainWindow):
def openDocument(self, tHandle):
self.closeDocument()
- self.docEditor.setText(self.theDocument.openDocument(tHandle))
- self.docEditor.setReadOnly(False)
- self.docEditor.setCursorPosition(self.theDocument.theItem.cursorPos)
+ self.docEditor.loadText(tHandle)
self.docEditor.changeWidth()
self.docEditor.setFocus()
self.theProject.setLastEdited(tHandle)
@@ -289,12 +297,14 @@ class GuiMain(QMainWindow):
if self.theDocument.theItem is not None:
docText = self.docEditor.getText()
cursPos = self.docEditor.getCursorPosition()
- self.theDocument.theItem.setCharCount(self.docEditor.charCount)
- self.theDocument.theItem.setWordCount(self.docEditor.wordCount)
- self.theDocument.theItem.setParaCount(self.docEditor.paraCount)
- self.theDocument.theItem.setCursorPos(cursPos)
+ theItem = self.theDocument.theItem
+ theItem.setCharCount(self.docEditor.charCount)
+ theItem.setWordCount(self.docEditor.wordCount)
+ theItem.setParaCount(self.docEditor.paraCount)
+ theItem.setCursorPos(cursPos)
self.theDocument.saveDocument(docText)
self.docEditor.setDocumentChanged(False)
+ self.theIndex.scanText(theItem.itemHandle, docText)
return True
def viewDocument(self, tHandle=None):
@@ -356,9 +366,10 @@ class GuiMain(QMainWindow):
return
logger.verbose("Requesting change to item %s" % tHandle)
- dlgProj = GuiItemEditor(self, self.theProject, tHandle)
- if dlgProj.exec_():
- self.treeView.setTreeItemValues(tHandle)
+ if self.mainConf.showGUI:
+ dlgProj = GuiItemEditor(self, self.theProject, tHandle)
+ if dlgProj.exec_():
+ self.treeView.setTreeItemValues(tHandle)
return
@@ -369,6 +380,55 @@ class GuiMain(QMainWindow):
self.treeView.buildTree()
return
+ def rebuildIndex(self):
+
+ logger.debug("Rebuilding indices ...")
+
+ self.treeView.saveTreeOrder()
+ self.theIndex.clearIndex()
+ nItems = len(self.theProject.treeOrder)
+
+ dlgProg = QProgressDialog("Scanning files ...", "Cancel", 0, nItems, self)
+ dlgProg.setWindowModality(Qt.WindowModal)
+ dlgProg.setMinimumDuration(0)
+ dlgProg.setFixedWidth(480)
+ dlgProg.setLabelText("Starting file scan ...")
+ dlgProg.setValue(0)
+ dlgProg.show()
+ time.sleep(0.5)
+
+ nDone = 0
+ for tHandle in self.theProject.treeOrder:
+
+ tItem = self.theProject.getItem(tHandle)
+
+ dlgProg.setValue(nDone)
+ dlgProg.setLabelText("Scanning: %s" % tItem.itemName)
+ logger.verbose("Scanning: %s" % tItem.itemName)
+
+ if tItem is not None and tItem.itemType == nwItemType.FILE:
+ theDoc = NWDoc(self.theProject, self)
+ theText = theDoc.openDocument(tHandle, False)
+
+ # Run Word Count
+ cC, wC, pC = countWords(theText)
+ 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
+
+ dlgProg.setValue(nItems)
+
+ return
+
##
# Main Dialogs
##
@@ -413,6 +473,11 @@ class GuiMain(QMainWindow):
self._setWindowTitle(self.theProject.projName)
return True
+ def showTimeLineDialog(self):
+ dlgTLine = GuiTimeLineView(self, self.theProject, self.theIndex)
+ dlgTLine.exec_()
+ return True
+
def makeAlert(self, theMessage, theLevel=nwAlert.INFO):
"""Alert both the user and the logger at the same time. Message can be either a string or an
array of strings. Severity level is 0 = info, 1 = warning, and 2 = error.
diff --git a/nw/gui/wordcounter.py b/nw/gui/wordcounter.py
index b44d1e60..f1c53497 100644
--- a/nw/gui/wordcounter.py
+++ b/nw/gui/wordcounter.py
@@ -13,10 +13,10 @@
import logging
import nw
-from time import time
-
from PyQt5.QtCore import QThread
+from nw.tools.wordcount import countWords
+
logger = logging.getLogger(__name__)
class WordCounter(QThread):
@@ -31,52 +31,12 @@ class WordCounter(QThread):
def run(self):
- self.charCount = 0
- self.wordCount = 0
- self.paraCount = 0
+ theText = self.theParent.getText()
+ cC, wC, pC = countWords(theText)
- prevEmpty = True
-
- for n in range(self.theParent.theDoc.blockCount()):
-
- theBlock = self.theParent.theDoc.findBlockByNumber(n)
- if not theBlock.isValid():
- continue
-
- countPara = True
- theText = theBlock.text()
- theLen = len(theText)
-
- if theLen == 0:
- prevEmpty = True
- continue
- if theText[0] == "@" or theText[0] == "%":
- prevEmpty = True
- continue
-
- if theText[0:5] == "#### ":
- self.wordCount -= 1
- self.charCount -= 5
- countPara = False
- elif theText[0:4] == "### ":
- self.wordCount -= 1
- self.charCount -= 4
- countPara = False
- elif theText[0:3] == "## ":
- self.wordCount -= 1
- self.charCount -= 3
- countPara = False
- elif theText[0:2] == "# ":
- self.wordCount -= 1
- self.charCount -= 2
- countPara = False
-
- theBuff = theText.replace("–"," ").replace("—"," ")
- self.wordCount += len(theBuff.split())
- self.charCount += theLen
- if countPara and prevEmpty:
- self.paraCount += 1
- prevEmpty = countPara == False
+ self.charCount = cC
+ self.wordCount = wC
+ self.paraCount = pC
return
diff --git a/nw/project/document.py b/nw/project/document.py
index 83eaf153..5305c01b 100644
--- a/nw/project/document.py
+++ b/nw/project/document.py
@@ -41,11 +41,10 @@ class NWDoc():
self.docHandle = None
return
- def openDocument(self, tHandle):
+ def openDocument(self, tHandle, showStatus=True):
self.docHandle = tHandle
self.theItem = self.theProject.getItem(tHandle)
- self.theParent.statusBar.setDocHandleCount(tHandle)
docDir, docFile = self._assemblePath(self.FILE_MN)
logger.debug("Opening document %s" % path.join(docDir,docFile))
@@ -63,7 +62,9 @@ class NWDoc():
logger.debug("The requested document does not exist.")
return ""
- self.theParent.statusBar.setStatus("Opened Document: %s" % self.theItem.itemName)
+ if showStatus:
+ self.theParent.statusBar.setStatus("Opened Document: %s" % self.theItem.itemName)
+ self.theParent.statusBar.setDocHandle(tHandle)
return theDoc
diff --git a/nw/project/index.py b/nw/project/index.py
new file mode 100644
index 00000000..8a4c6bf1
--- /dev/null
+++ b/nw/project/index.py
@@ -0,0 +1,347 @@
+# -*- coding: utf-8 -*-
+"""novelWriter Project Index
+
+ novelWriter – Project Index
+=============================
+ Class holding the index of tags
+
+ File History:
+ Created: 2019-05-27 [0.1.4]
+
+Structure:
+
+ We need to scan all files to get the novel layout. This is done by heading depth. Each heading
+is recorded in a sorted list. Each such heading again can have a set of meta tags.
+ In the other class root folders, each file can have a tag which the meta tags in the novel files
+point to. These tags should be stored in a dictionary where the tag is the key pointing to a single
+file handle. That way we can do lookups on both keys and values.
+ The timeline view then consists of novel header elements in the horizontal header, possibly just
+truncated to a Ch or Sc abbreviation, possibly with a number. This can be extracted from the item
+layout. The vertical header column is then whatever notes we want to compare against, and the links
+from the novel files are dots on the row.
+
+"""
+
+import logging
+import json
+import nw
+
+from os import path
+
+from nw.project.document import NWDoc
+from nw.enum import nwItemType, nwItemClass
+from nw.constants import nwFiles
+
+logger = logging.getLogger(__name__)
+
+class NWIndex():
+
+ TAG_KEY = "@tag"
+ POV_KEY = "@pov"
+ CHAR_KEY = "@char"
+ PLOT_KEY = "@plot"
+ TIME_KEY = "@time"
+ WORLD_KEY = "@location"
+ OBJECT_KEY = "@object"
+ CUSTOM_KEY = "@custom"
+
+ NOTE_KEYS = [TAG_KEY]
+ NOVEL_KEYS = [PLOT_KEY, POV_KEY, CHAR_KEY, WORLD_KEY, TIME_KEY, OBJECT_KEY, CUSTOM_KEY]
+ TAG_CLASS = {
+ CHAR_KEY : [nwItemClass.CHARACTER, 1],
+ POV_KEY : [nwItemClass.CHARACTER, 2],
+ PLOT_KEY : [nwItemClass.PLOT, 1],
+ TIME_KEY : [nwItemClass.TIMELINE, 1],
+ WORLD_KEY : [nwItemClass.WORLD, 1],
+ OBJECT_KEY : [nwItemClass.OBJECT, 1],
+ CUSTOM_KEY : [nwItemClass.CUSTOM, 1],
+ }
+
+ def __init__(self, theProject, theParent):
+
+ # Internal
+ self.theProject = theProject
+ self.theParent = theParent
+ self.mainConf = self.theParent.mainConf
+
+ # Indices
+ self.tagIndex = {}
+ self.refIndex = {}
+ self.novelIndex = {}
+
+ # Lists
+ self.novelList = []
+
+ return
+
+ def clearIndex(self):
+ self.tagIndex = {}
+ self.refIndex = {}
+ self.novelIndex = {}
+ return
+
+ ##
+ # Load and Save Index to/from File
+ ##
+
+ def loadIndex(self):
+
+ theData = {}
+ indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
+ if path.isfile(indexFile):
+ logger.debug("Loading index file")
+ try:
+ with open(indexFile,mode="r") as inFile:
+ theJson = inFile.read()
+ theData = json.loads(theJson)
+ except Exception as e:
+ logger.error("Failed to load index file")
+ logger.error(str(e))
+ return False
+
+ if "tagIndex" in theData.keys():
+ self.tagIndex = theData["tagIndex"]
+ if "refIndex" in theData.keys():
+ self.refIndex = theData["refIndex"]
+ if "novelIndex" in theData.keys():
+ self.novelIndex = theData["novelIndex"]
+
+ return True
+
+ return False
+
+ def saveIndex(self):
+
+ 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+") as outFile:
+ outFile.write(json.dumps({
+ "tagIndex" : self.tagIndex,
+ "refIndex" : self.refIndex,
+ "novelIndex" : self.novelIndex,
+ }, indent=nIndent))
+ except Exception as e:
+ logger.error("Failed to save index file")
+ logger.error(str(e))
+ return False
+
+ return True
+
+ ##
+ # Index Building
+ ##
+
+ def scanText(self, tHandle, theText):
+
+ theItem = self.theProject.getItem(tHandle)
+ if theItem is None: return False
+ if theItem.itemType != nwItemType.FILE: 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:
+ isNovel = False
+
+ # Also clear references to file in tag index
+ clearTags = []
+ for aTag in self.tagIndex:
+ if self.tagIndex[aTag][1] == tHandle:
+ clearTags.append(aTag)
+ for aTag in clearTags:
+ self.tagIndex.pop(aTag)
+
+ nLine = 0
+ nTitle = 0
+ for aLine in theText.splitlines():
+ aLine = aLine.strip()
+ nLine += 1
+ nChar = len(aLine)
+ if nChar == 0: continue
+ if aLine[0] == "#":
+ if isNovel:
+ isTitle = self.indexTitle(tHandle, aLine, nLine, itemLayout)
+ if isTitle:
+ nTitle = nLine
+ elif aLine[0] == "@":
+ if isNovel:
+ self.indexNoteRef(tHandle, aLine, nLine, nTitle)
+ else:
+ self.indexTag(tHandle, aLine, nLine, itemClass)
+
+ return True
+
+ def indexTitle(self, tHandle, aLine, nLine, itemLayout):
+
+ if aLine.startswith("# "):
+ hDepth = 1
+ hText = aLine[2:].strip()
+ elif aLine.startswith("## "):
+ hDepth = 2
+ hText = aLine[3:].strip()
+ elif aLine.startswith("### "):
+ hDepth = 3
+ hText = aLine[4:].strip()
+ elif aLine.startswith("#### "):
+ hDepth = 4
+ hText = aLine[5:].strip()
+ else:
+ return False
+
+ if hText != "":
+ self.novelIndex[tHandle].append([nLine, hDepth, hText, itemLayout.name])
+
+ return True
+
+ def indexNoteRef(self, tHandle, aLine, nLine, nTitle):
+
+ isValid, theBits, thePos = self.scanThis(aLine)
+ if not isValid or len(theBits) == 0:
+ return False
+
+ theKey = theBits[0]
+ if theKey in self.NOVEL_KEYS:
+ for aVal in theBits[1:]:
+ self.refIndex[tHandle].append([nLine, theKey, aVal, nTitle])
+
+ return True
+
+ def indexTag(self, tHandle, aLine, nLine, itemClass):
+
+ isValid, theBits, thePos = self.scanThis(aLine)
+ if not isValid or len(theBits) != 2:
+ return False
+
+ if theBits[0] == self.TAG_KEY:
+ self.tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name]
+
+ return True
+
+ ##
+ # Check @ Lines
+ ##
+
+ def scanThis(self, aLine):
+
+ theBits = []
+ thePos = []
+
+ aLine = aLine.strip()
+ nChar = len(aLine)
+ if nChar < 2:
+ return False, theBits, thePos
+ if aLine[0] != "@":
+ return False, theBits, thePos
+
+ cPos = 0
+ cKey, cSep, cVals = aLine.partition(":")
+ sKey = cKey.strip()
+ if sKey == "@":
+ return False, theBits, thePos
+
+ theBits.append(sKey)
+ thePos.append(cPos)
+ cPos += len(sKey) + 1
+
+ if cVals == "":
+ # No values, so we're done
+ return True, theBits, thePos
+
+ aVals = cVals.split(",")
+ for cVal in aVals:
+ sVal = cVal.strip()
+ rLen = len(cVal.lstrip())
+ tLen = len(cVal)
+ theBits.append(sVal)
+ thePos.append(cPos+tLen-rLen)
+ cPos += tLen + 1
+
+ return True, theBits, thePos
+
+ def checkThese(self, theBits, tItem):
+
+ nBits = len(theBits)
+ isGood = [False]*nBits
+ if nBits == 0:
+ return []
+
+ # If we have a tag, only the first value is accepted, the rest is ignored
+ if theBits[0] == self.TAG_KEY and nBits > 1:
+ isGood[0] = True
+ if theBits[1] in self.tagIndex.keys():
+ if self.tagIndex[theBits[1]][1] == tItem.itemHandle:
+ isGood[1] = True
+ else:
+ isGood[1] = False
+ else:
+ isGood[1] = True
+ return isGood
+
+ # If we're still here, we better check that the references exist
+ if tItem.itemClass == nwItemClass.NOVEL:
+ isGood[0] = theBits[0] in self.NOVEL_KEYS
+ else:
+ isGood[0] = theBits[0] in self.NOTE_KEYS
+ if not isGood[0] or nBits == 1:
+ return isGood
+
+ 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]
+
+ return isGood
+
+ ##
+ # Extract Data
+ ##
+
+ def buildNovelList(self):
+
+ self.novelList = []
+ self.novelOrder = []
+ 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
+
+ def buildTagNovelMap(self, theTags):
+
+ tagMap = {}
+ tagClass = {}
+
+ for theTag in theTags:
+ tagMap[theTag] = [0]*len(self.novelOrder)
+ try:
+ tagClass[theTag] = nwItemClass[self.tagIndex[theTag][2]]
+ except:
+ logger.error("Could not map '%s' to nwItemClass" % self.tagIndex[theTag][2])
+ tagClass[theTag] = None
+
+ 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))
+
+ return tagMap
+
+# END Class NWIndex
diff --git a/nw/theme.py b/nw/theme.py
index cab4c302..a34a2895 100644
--- a/nw/theme.py
+++ b/nw/theme.py
@@ -37,6 +37,7 @@ class Theme:
self.colKey = [0,0,0]
self.colVal = [0,0,0]
self.colSpell = [0,0,0]
+ self.colTagErr = [0,0,0]
# Changeable Settings
self.guiTheme = None
@@ -94,6 +95,7 @@ class Theme:
self.colKey = self._loadColour(confParser,cnfSec,"keyword")
self.colVal = self._loadColour(confParser,cnfSec,"value")
self.colSpell = self._loadColour(confParser,cnfSec,"spellcheckline")
+ self.colTagErr = self._loadColour(confParser,cnfSec,"tagerror")
return True
diff --git a/nw/themes/default/theme.conf b/nw/themes/default/theme.conf
index 117f53ea..bac88942 100644
--- a/nw/themes/default/theme.conf
+++ b/nw/themes/default/theme.conf
@@ -9,3 +9,4 @@ hidden = 150, 150, 150
keyword = 200, 46, 0
value = 184, 200, 0
spellcheckline = 200, 46, 0
+tagerror = 46, 200, 0
diff --git a/nw/tools/wordcount.py b/nw/tools/wordcount.py
new file mode 100644
index 00000000..d6fad51a
--- /dev/null
+++ b/nw/tools/wordcount.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+"""novelWriter Word Counter
+
+ novelWriter – Word Counter
+============================
+ Simple word counter
+
+ File History:
+ Created: 2019-04-22 [0.0.1]
+ Moved: 2019-05-30 [0.1.4]
+
+"""
+
+import logging
+import nw
+
+logger = logging.getLogger(__name__)
+
+def countWords(theText):
+
+ charCount = 0
+ wordCount = 0
+ paraCount = 0
+ prevEmpty = True
+
+ for aLine in theText.splitlines():
+
+ countPara = True
+ theLen = len(aLine)
+
+ if theLen == 0:
+ prevEmpty = True
+ continue
+ if aLine[0] == "@" or aLine[0] == "%":
+ continue
+
+ if aLine[0:5] == "#### ":
+ wordCount -= 1
+ charCount -= 5
+ countPara = False
+ elif aLine[0:4] == "### ":
+ wordCount -= 1
+ charCount -= 4
+ countPara = False
+ elif aLine[0:3] == "## ":
+ wordCount -= 1
+ charCount -= 3
+ countPara = False
+ elif aLine[0:2] == "# ":
+ wordCount -= 1
+ charCount -= 2
+ countPara = False
+
+ theBuff = aLine.replace("–"," ").replace("—"," ")
+ wordCount += len(theBuff.split())
+ charCount += theLen
+ if countPara and prevEmpty:
+ paraCount += 1
+ prevEmpty = countPara == False
+
+ return charCount, wordCount, paraCount
diff --git a/sample/sampleNovel/data_1/4298de4d9524_main.nwd b/sample/sampleNovel/data_1/4298de4d9524_main.nwd
index 11127dab..9bd4961b 100644
--- a/sample/sampleNovel/data_1/4298de4d9524_main.nwd
+++ b/sample/sampleNovel/data_1/4298de4d9524_main.nwd
@@ -1,3 +1,5 @@
# John Smith
+@tag: John
+
He’s pretty cool. Not Brad Pitt though.
\ No newline at end of file
diff --git a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd
index eb71a80b..a084c76f 100644
--- a/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd
+++ b/sample/sampleNovel/data_6/36b6aa9b697b_main.nwd
@@ -3,8 +3,9 @@
## This is the Subtitle
% Begin Meta
-@POV: Sam
-@Chars: Sam, Adam, Scott
+@pov: Jane
+@char: John
+@location: Earth
% End Meta
Some text here would look good as well, and maybe some "dialogue"?
@@ -17,8 +18,3 @@ This paragraph is also meaningless. At least a bit. It’s also very short. But
This one is a bit longer. “It also has some dialogue in it” she said, before she moved on to check if the spellchecker worked. It did. “Cool,” she concluded.
-
-@ToDo: Stuff that will be done at some point
-
-
-
diff --git a/sample/sampleNovel/data_b/3e74dbc1f584_main.nwd b/sample/sampleNovel/data_b/3e74dbc1f584_main.nwd
index bcdf600f..b82ef89a 100644
--- a/sample/sampleNovel/data_b/3e74dbc1f584_main.nwd
+++ b/sample/sampleNovel/data_b/3e74dbc1f584_main.nwd
@@ -1,3 +1,5 @@
# Earth
+@tag: Earth
+
Third planet from the sun, fairly dense, and with lots of people on it.
\ No newline at end of file
diff --git a/sample/sampleNovel/data_b/b2c23b3c42cc_main.nwd b/sample/sampleNovel/data_b/b2c23b3c42cc_main.nwd
index 409a428a..1a91f10a 100644
--- a/sample/sampleNovel/data_b/b2c23b3c42cc_main.nwd
+++ b/sample/sampleNovel/data_b/b2c23b3c42cc_main.nwd
@@ -1,3 +1,5 @@
# Jane Smith
+@tag: Jane
+
She’s pretty cool. Not Angelina Jolie though.
\ No newline at end of file
diff --git a/sample/sampleNovel/data_b/c0cbd2a407f3_main.nwd b/sample/sampleNovel/data_b/c0cbd2a407f3_main.nwd
index 6edf9061..0a39ca10 100644
--- a/sample/sampleNovel/data_b/c0cbd2a407f3_main.nwd
+++ b/sample/sampleNovel/data_b/c0cbd2a407f3_main.nwd
@@ -1,3 +1,6 @@
# This is a New File!
+@pov: John
+@location: Space
+
Although, not so new now that it has text in it an everything …
\ No newline at end of file
diff --git a/sample/sampleNovel/data_f/1471bef9f2ae_main.nwd b/sample/sampleNovel/data_f/1471bef9f2ae_main.nwd
new file mode 100644
index 00000000..caae39bc
--- /dev/null
+++ b/sample/sampleNovel/data_f/1471bef9f2ae_main.nwd
@@ -0,0 +1,6 @@
+# Space
+
+@tag: Space
+
+This is somewhere in outer space.
+
diff --git a/sample/sampleNovel/meta/sessionInfo.log b/sample/sampleNovel/meta/sessionInfo.log
index 79d3266e..ecc8f9ce 100644
--- a/sample/sampleNovel/meta/sessionInfo.log
+++ b/sample/sampleNovel/meta/sessionInfo.log
@@ -13,3 +13,143 @@ Start: 2019-05-26 15:37:47 End: 2019-05-26 15:38:07 Words: 538
Start: 2019-05-26 15:38:11 End: 2019-05-26 15:43:07 Words: 2
Start: 2019-05-26 15:49:18 End: 2019-05-26 15:49:47 Words: 0
Start: 2019-05-26 15:52:39 End: 2019-05-26 15:52:56 Words: 0
+Start: 2019-05-27 21:23:07 End: 2019-05-27 21:26:41 Words: 0
+Start: 2019-05-27 21:26:56 End: 2019-05-27 21:27:48 Words: 0
+Start: 2019-05-27 21:29:11 End: 2019-05-27 21:30:30 Words: 0
+Start: 2019-05-27 21:33:30 End: 2019-05-27 21:35:14 Words: 0
+Start: 2019-05-27 21:35:20 End: 2019-05-27 21:36:03 Words: 0
+Start: 2019-05-27 21:36:07 End: 2019-05-27 21:36:23 Words: 0
+Start: 2019-05-27 21:36:27 End: 2019-05-27 21:37:46 Words: 0
+Start: 2019-05-27 21:37:50 End: 2019-05-27 21:38:05 Words: 0
+Start: 2019-05-27 21:38:14 End: 2019-05-27 21:38:24 Words: 0
+Start: 2019-05-27 21:39:08 End: 2019-05-27 21:39:23 Words: 0
+Start: 2019-05-27 21:42:21 End: 2019-05-27 21:42:39 Words: 0
+Start: 2019-05-27 21:45:07 End: 2019-05-27 21:45:18 Words: 0
+Start: 2019-05-27 21:58:30 End: 2019-05-27 21:58:43 Words: 0
+Start: 2019-05-27 22:02:01 End: 2019-05-27 22:02:09 Words: 0
+Start: 2019-05-27 22:05:47 End: 2019-05-27 22:06:02 Words: 0
+Start: 2019-05-27 22:08:25 End: 2019-05-27 22:09:35 Words: 0
+Start: 2019-05-27 22:11:40 End: 2019-05-27 22:14:18 Words: 3
+Start: 2019-05-27 22:20:55 End: 2019-05-27 22:21:40 Words: -2
+Start: 2019-05-27 22:22:38 End: 2019-05-27 22:23:12 Words: 16
+Start: 2019-05-27 22:55:34 End: 2019-05-27 22:56:09 Words: 0
+Start: 2019-05-27 22:58:47 End: 2019-05-27 23:00:47 Words: 17
+Start: 2019-05-28 18:20:15 End: 2019-05-28 18:20:30 Words: -1
+Start: 2019-05-28 18:21:04 End: 2019-05-28 18:21:24 Words: 0
+Start: 2019-05-28 18:29:41 End: 2019-05-28 18:30:16 Words: 0
+Start: 2019-05-28 18:30:59 End: 2019-05-28 18:32:06 Words: 0
+Start: 2019-05-28 18:34:33 End: 2019-05-28 18:34:48 Words: 1
+Start: 2019-05-28 18:43:08 End: 2019-05-28 18:43:21 Words: 0
+Start: 2019-05-28 18:49:29 End: 2019-05-28 18:49:40 Words: 0
+Start: 2019-05-28 19:13:20 End: 2019-05-28 19:13:24 Words: -1
+Start: 2019-05-28 19:17:05 End: 2019-05-28 19:17:13 Words: 1
+Start: 2019-05-28 19:49:44 End: 2019-05-28 19:52:23 Words: -1
+Start: 2019-05-28 19:52:29 End: 2019-05-28 19:54:42 Words: 0
+Start: 2019-05-28 19:55:33 End: 2019-05-28 19:55:56 Words: 0
+Start: 2019-05-28 19:57:29 End: 2019-05-28 20:02:33 Words: 0
+Start: 2019-05-28 20:02:38 End: 2019-05-28 20:05:19 Words: 0
+Start: 2019-05-28 20:05:23 End: 2019-05-28 20:06:48 Words: 0
+Start: 2019-05-28 20:07:00 End: 2019-05-28 20:12:26 Words: 0
+Start: 2019-05-28 20:12:31 End: 2019-05-28 20:17:58 Words: -16
+Start: 2019-05-28 20:28:31 End: 2019-05-28 20:28:53 Words: 0
+Start: 2019-05-28 20:28:57 End: 2019-05-28 20:29:05 Words: 16
+Start: 2019-05-28 21:00:49 End: 2019-05-28 21:01:45 Words: 1
+Start: 2019-05-28 21:02:01 End: 2019-05-28 21:02:06 Words: 0
+Start: 2019-05-28 21:17:57 End: 2019-05-28 21:25:48 Words: 0
+Start: 2019-05-28 21:35:39 End: 2019-05-28 21:35:47 Words: 0
+Start: 2019-05-28 22:22:54 End: 2019-05-28 22:23:12 Words: 0
+Start: 2019-05-30 11:58:19 End: 2019-05-30 11:59:15 Words: 0
+Start: 2019-05-30 11:59:19 End: 2019-05-30 11:59:26 Words: 0
+Start: 2019-05-30 11:59:55 End: 2019-05-30 12:00:09 Words: 0
+Start: 2019-05-30 12:01:47 End: 2019-05-30 12:02:08 Words: 0
+Start: 2019-05-30 12:03:56 End: 2019-05-30 12:04:23 Words: 0
+Start: 2019-05-30 12:09:24 End: 2019-05-30 12:09:47 Words: 0
+Start: 2019-05-30 12:10:13 End: 2019-05-30 12:10:40 Words: 0
+Start: 2019-05-30 12:12:14 End: 2019-05-30 12:12:33 Words: 0
+Start: 2019-05-30 12:13:56 End: 2019-05-30 12:14:08 Words: 0
+Start: 2019-05-30 12:14:24 End: 2019-05-30 12:14:44 Words: 0
+Start: 2019-05-30 12:14:53 End: 2019-05-30 12:15:31 Words: 0
+Start: 2019-05-30 12:17:36 End: 2019-05-30 12:17:49 Words: 0
+Start: 2019-05-30 12:20:12 End: 2019-05-30 12:20:24 Words: 0
+Start: 2019-05-30 12:36:42 End: 2019-05-30 12:37:21 Words: 0
+Start: 2019-05-30 12:37:42 End: 2019-05-30 12:38:01 Words: 0
+Start: 2019-05-30 12:47:13 End: 2019-05-30 12:47:27 Words: 0
+Start: 2019-05-30 12:47:47 End: 2019-05-30 12:48:03 Words: 0
+Start: 2019-05-30 12:50:01 End: 2019-05-30 12:50:15 Words: 0
+Start: 2019-05-30 12:54:26 End: 2019-05-30 12:54:34 Words: 0
+Start: 2019-05-30 15:25:50 End: 2019-05-30 15:25:59 Words: 0
+Start: 2019-05-30 15:43:36 End: 2019-05-30 15:43:44 Words: 0
+Start: 2019-05-30 15:52:46 End: 2019-05-30 15:53:12 Words: 0
+Start: 2019-05-30 15:53:44 End: 2019-05-30 15:53:56 Words: 0
+Start: 2019-05-30 15:56:29 End: 2019-05-30 15:57:35 Words: 0
+Start: 2019-05-30 15:57:41 End: 2019-05-30 15:59:58 Words: 0
+Start: 2019-05-30 16:00:01 End: 2019-05-30 16:00:28 Words: 0
+Start: 2019-05-30 16:00:56 End: 2019-05-30 16:01:06 Words: 0
+Start: 2019-05-30 16:01:32 End: 2019-05-30 16:01:48 Words: 0
+Start: 2019-05-30 16:02:35 End: 2019-05-30 16:03:03 Words: 0
+Start: 2019-05-30 17:00:23 End: 2019-05-30 17:21:07 Words: 0
+Start: 2019-05-30 17:21:11 End: 2019-05-30 17:21:24 Words: 0
+Start: 2019-05-30 17:24:41 End: 2019-05-30 17:24:46 Words: 0
+Start: 2019-05-30 17:29:08 End: 2019-05-30 17:29:19 Words: 0
+Start: 2019-05-30 18:12:59 End: 2019-05-30 18:13:07 Words: 0
+Start: 2019-05-30 18:13:32 End: 2019-05-30 18:13:42 Words: 0
+Start: 2019-05-30 18:16:13 End: 2019-05-30 18:16:18 Words: 0
+Start: 2019-05-30 18:22:24 End: 2019-05-30 18:22:30 Words: 0
+Start: 2019-05-30 18:22:43 End: 2019-05-30 18:24:11 Words: 0
+Start: 2019-05-30 18:25:40 End: 2019-05-30 18:25:45 Words: 0
+Start: 2019-05-30 18:26:01 End: 2019-05-30 18:27:56 Words: 0
+Start: 2019-05-30 18:28:00 End: 2019-05-30 18:30:22 Words: 0
+Start: 2019-05-30 18:42:16 End: 2019-05-30 18:43:10 Words: 0
+Start: 2019-05-30 18:43:24 End: 2019-05-30 18:43:31 Words: 0
+Start: 2019-05-30 18:48:54 End: 2019-05-30 18:49:19 Words: 0
+Start: 2019-05-30 18:49:23 End: 2019-05-30 18:49:53 Words: 0
+Start: 2019-05-30 18:51:06 End: 2019-05-30 19:02:40 Words: 0
+Start: 2019-05-30 19:07:17 End: 2019-05-30 19:07:26 Words: 0
+Start: 2019-05-30 19:17:16 End: 2019-05-30 19:17:39 Words: 0
+Start: 2019-05-30 19:17:44 End: 2019-05-30 19:17:55 Words: 0
+Start: 2019-05-30 19:18:44 End: 2019-05-30 19:19:15 Words: 0
+Start: 2019-05-30 19:19:20 End: 2019-05-30 19:19:47 Words: 0
+Start: 2019-05-30 19:21:41 End: 2019-05-30 19:21:44 Words: 0
+Start: 2019-05-30 19:24:00 End: 2019-05-30 19:24:03 Words: 0
+Start: 2019-05-30 19:24:45 End: 2019-05-30 19:26:48 Words: 0
+Start: 2019-05-30 19:28:23 End: 2019-05-30 19:32:41 Words: 0
+Start: 2019-05-30 19:32:57 End: 2019-05-30 19:33:18 Words: 0
+Start: 2019-05-30 19:33:22 End: 2019-05-30 19:33:39 Words: 0
+Start: 2019-05-30 19:34:08 End: 2019-05-30 19:34:31 Words: 0
+Start: 2019-05-30 19:34:43 End: 2019-05-30 19:34:45 Words: 0
+Start: 2019-05-30 19:34:58 End: 2019-05-30 19:36:33 Words: 0
+Start: 2019-05-30 19:36:38 End: 2019-05-30 19:36:47 Words: 0
+Start: 2019-05-30 19:43:30 End: 2019-05-30 19:43:35 Words: 0
+Start: 2019-05-30 19:43:42 End: 2019-05-30 19:43:49 Words: 0
+Start: 2019-05-30 19:45:34 End: 2019-05-30 19:45:59 Words: 0
+Start: 2019-05-30 19:53:25 End: 2019-05-30 19:55:00 Words: 0
+Start: 2019-05-30 19:55:21 End: 2019-05-30 19:56:16 Words: 0
+Start: 2019-05-30 20:51:36 End: 2019-05-30 20:51:40 Words: 0
+Start: 2019-05-30 20:51:40 End: 2019-05-30 20:51:45 Words: 0
+Start: 2019-05-30 20:25:18 End: 2019-05-30 21:55:25 Words: 0
+Start: 2019-05-30 21:56:09 End: 2019-05-30 21:57:09 Words: 0
+Start: 2019-05-30 21:57:14 End: 2019-05-30 21:58:29 Words: 0
+Start: 2019-05-30 21:58:33 End: 2019-05-30 21:58:59 Words: 0
+Start: 2019-05-30 21:59:33 End: 2019-05-30 22:00:08 Words: 0
+Start: 2019-05-30 22:00:27 End: 2019-05-30 22:01:18 Words: 0
+Start: 2019-05-30 22:01:35 End: 2019-05-30 22:01:57 Words: 0
+Start: 2019-05-30 22:02:05 End: 2019-05-30 22:02:24 Words: 0
+Start: 2019-05-30 22:07:43 End: 2019-05-30 22:08:48 Words: 0
+Start: 2019-05-30 22:18:15 End: 2019-05-30 22:18:24 Words: 0
+Start: 2019-05-31 00:01:48 End: 2019-05-31 00:01:57 Words: 0
+Start: 2019-05-31 00:10:01 End: 2019-05-31 00:10:07 Words: 0
+Start: 2019-05-31 00:11:12 End: 2019-05-31 00:11:21 Words: 0
+Start: 2019-05-31 00:11:49 End: 2019-05-31 00:12:04 Words: 0
+Start: 2019-05-31 00:12:42 End: 2019-05-31 00:13:24 Words: 0
+Start: 2019-05-31 00:38:11 End: 2019-05-31 00:38:38 Words: 0
+Start: 2019-05-31 00:39:35 End: 2019-05-31 00:40:23 Words: 0
+Start: 2019-05-31 00:40:27 End: 2019-05-31 00:40:54 Words: 0
+Start: 2019-05-31 00:56:15 End: 2019-05-31 00:56:23 Words: 0
+Start: 2019-05-31 00:59:56 End: 2019-05-31 01:00:05 Words: 0
+Start: 2019-05-31 01:00:39 End: 2019-05-31 01:00:56 Words: 0
+Start: 2019-05-31 19:10:29 End: 2019-05-31 19:21:11 Words: 0
+Start: 2019-05-31 19:50:56 End: 2019-05-31 19:51:03 Words: 0
+Start: 2019-05-31 19:58:24 End: 2019-05-31 19:59:04 Words: 0
+Start: 2019-05-31 20:11:22 End: 2019-05-31 20:11:56 Words: 0
+Start: 2019-05-31 20:23:06 End: 2019-05-31 20:23:18 Words: 0
+Start: 2019-05-31 20:23:56 End: 2019-05-31 20:27:02 Words: 7
diff --git a/sample/sampleNovel/nwProject.nwx b/sample/sampleNovel/nwProject.nwx
index 448da65a..7cad49d7 100644
--- a/sample/sampleNovel/nwProject.nwx
+++ b/sample/sampleNovel/nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Sample Project
Sample Project
@@ -8,9 +8,9 @@
True
- 636b6aa9b697b
+ bc0cbd2a407f3
None
- 540
+ 565
New
Notes
@@ -27,7 +27,7 @@
Main
-
+
-
Novel
ROOT
@@ -76,7 +76,7 @@
656
121
5
- 573
+ 77
-
New File
@@ -85,10 +85,10 @@
Notes
False
SCENE
- 69
- 17
+ 82
+ 19
1
- 0
+ 69
-
Characters
@@ -111,10 +111,10 @@
Minor
False
NOTE
- 42
- 8
+ 49
+ 9
1
- 0
+ 24
-
Jane Smith
@@ -123,10 +123,10 @@
Major
False
NOTE
- 51
+ 55
9
1
- 0
+ 25
-
Locations
@@ -142,10 +142,22 @@
None
False
NOTE
- 0
- 0
- 0
- 0
+ 76
+ 15
+ 1
+ 20
+
+ -
+ Space
+ FILE
+ WORLD
+ None
+ False
+ NOTE
+ 38
+ 7
+ 1
+ 57
-
Trash
diff --git a/tests/reference/gui/1_1489056e0916_main.nwd b/tests/reference/gui/1_1489056e0916_main.nwd
index 2a3624ae..c5a065c0 100644
--- a/tests/reference/gui/1_1489056e0916_main.nwd
+++ b/tests/reference/gui/1_1489056e0916_main.nwd
@@ -1,13 +1,20 @@
-# Hello World!
+# Novel
-## With a Subtitle
+## Chapter
-### An Even Subier Title
+@pov: Jane
+@plot: MainPlot
-#### Basically Not a Title at All
+### Scene
% How about a comment?
-@keyword: value
+@pov: Jane
+@plot: MainPlot
+@location: Home
+
+#### Some Section
+
+@char: Jane
This is a paragraph of dummy text.
diff --git a/tests/reference/gui/1_2d20bbd7e394_main.nwd b/tests/reference/gui/1_2d20bbd7e394_main.nwd
new file mode 100644
index 00000000..f646f17e
--- /dev/null
+++ b/tests/reference/gui/1_2d20bbd7e394_main.nwd
@@ -0,0 +1,5 @@
+# Main Plot
+
+@tag: MainPlot
+
+This is a file detailing the main plot.
diff --git a/tests/reference/gui/1_688b6ef52555_main.nwd b/tests/reference/gui/1_688b6ef52555_main.nwd
new file mode 100644
index 00000000..d4faeeb2
--- /dev/null
+++ b/tests/reference/gui/1_688b6ef52555_main.nwd
@@ -0,0 +1,5 @@
+# Main Location
+
+@tag: Home
+
+This is a file describing Jane’s home.
diff --git a/tests/reference/gui/1_fca346db6561_main.nwd b/tests/reference/gui/1_fca346db6561_main.nwd
new file mode 100644
index 00000000..4ba77a38
--- /dev/null
+++ b/tests/reference/gui/1_fca346db6561_main.nwd
@@ -0,0 +1,5 @@
+# Jane Doe
+
+@tag: Jane
+
+This is a file about Jane.
diff --git a/tests/reference/gui/1_nwProject.nwx b/tests/reference/gui/1_nwProject.nwx
index ae897362..fa8698ba 100644
--- a/tests/reference/gui/1_nwProject.nwx
+++ b/tests/reference/gui/1_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
@@ -8,7 +8,7 @@
True
31489056e0916
31489056e0916
- 69
+ 86
New
Note
@@ -22,7 +22,7 @@
Main
-
+
-
Novel
ROOT
@@ -44,31 +44,67 @@
New
False
SCENE
- 377
- 69
+ 331
+ 59
2
- 443
+ 465
-
Characters
ROOT
CHARACTER
New
+ True
+
+ -
+ New File
+ FILE
+ CHARACTER
+ New
False
+ NOTE
+ 34
+ 8
+ 1
+ 51
-
Plot
ROOT
PLOT
New
+ True
+
+ -
+ New File
+ FILE
+ PLOT
+ New
False
+ NOTE
+ 48
+ 10
+ 1
+ 69
-
World
ROOT
WORLD
New
+ True
+
+ -
+ New File
+ FILE
+ WORLD
+ New
False
+ NOTE
+ 51
+ 9
+ 1
+ 68
diff --git a/tests/reference/gui/1_tagsIndex.json b/tests/reference/gui/1_tagsIndex.json
new file mode 100644
index 00000000..57b2ed92
--- /dev/null
+++ b/tests/reference/gui/1_tagsIndex.json
@@ -0,0 +1 @@
+{"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]]}, "novelIndex": {"31489056e0916": [[1, 1, "Novel", "SCENE"], [3, 2, "Chapter", "SCENE"], [8, 3, "Scene", "SCENE"], [15, 4, "Some Section", "SCENE"]]}}
\ No newline at end of file
diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf
index cb999b5a..a7df1756 100644
--- a/tests/reference/novelwriter.conf
+++ b/tests/reference/novelwriter.conf
@@ -7,6 +7,7 @@ geometry = 1100, 650
treecols = 120, 30, 50
mainpane = 300, 800
docpane = 400, 400
+timeline = 600, 400
[Project]
autosaveproject = 60
diff --git a/tests/test_gui.py b/tests/test_gui.py
index 7f141b47..3cecee17 100644
--- a/tests/test_gui.py
+++ b/tests/test_gui.py
@@ -2,13 +2,14 @@
"""novelWriter Main GUI Class Tester
"""
-import nw, pytest
+import nw, pytest, sys
from nwtools import *
from os import path, unlink
from PyQt5.QtCore import Qt
from nw.gui.projecteditor import GuiProjectEditor
+from nw.gui.timelineview import GuiTimeLineView
from nw.gui.itemeditor import GuiItemEditor
from nw.enum import *
@@ -47,8 +48,6 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
assert cmpFiles(projFile, path.join(nwRef,"gui","0_nwProject.nwx"), [2])
qtbot.wait(stepDelay)
- # qtbot.stopForInteraction()
-
# Re-open project
assert nwGUI.openProject(nwTempGUI)
qtbot.wait(stepDelay)
@@ -75,33 +74,101 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
assert nwGUI.treeView._getTreeItem("71ee45a3c0db9") is not None
assert nwGUI.treeView._getTreeItem("811786ad1ae74") is not None
+ nwGUI.mainMenu.toolsSpellCheck.setChecked(True)
+ assert nwGUI.mainMenu._toggleSpellCheck()
+
+ # Add a Character File
+ nwGUI.setFocus(1)
+ nwGUI.treeView.clearSelection()
+ nwGUI.treeView._getTreeItem("44cb730c42048").setSelected(True)
+ nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
+ assert nwGUI.openSelectedItem()
+
+ # Type something into the document
+ nwGUI.setFocus(2)
+ for c in "# Jane Doe":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ for c in "@tag: Jane":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ for c in "This is a file about Jane.":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+
+ # Add a Plot File
+ nwGUI.setFocus(1)
+ nwGUI.treeView.clearSelection()
+ nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True)
+ nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
+ assert nwGUI.openSelectedItem()
+
+ # Type something into the document
+ nwGUI.setFocus(2)
+ for c in "# Main Plot":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ for c in "@tag: MainPlot":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ for c in "This is a file detailing the main plot.":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+
+ # Add a World File
+ nwGUI.setFocus(1)
+ nwGUI.treeView.clearSelection()
+ nwGUI.treeView._getTreeItem("811786ad1ae74").setSelected(True)
+ nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
+ assert nwGUI.openSelectedItem()
+
+ # Type something into the document
+ nwGUI.setFocus(2)
+ for c in "# Main Location":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ for c in "@tag: Home":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ for c in "This is a file describing Jane's home.":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+
# Select the 'New Scene' file
nwGUI.setFocus(1)
+ nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem("73475cb40a568").setExpanded(True)
nwGUI.treeView._getTreeItem("25fc0e7096fc6").setExpanded(True)
nwGUI.treeView._getTreeItem("31489056e0916").setSelected(True)
assert nwGUI.openSelectedItem()
- nwGUI.mainMenu.toolsSpellCheck.setChecked(True)
- assert nwGUI.mainMenu._toggleSpellCheck()
# Type something into the document
nwGUI.setFocus(2)
- for c in "# Hello World!":
+ for c in "# Novel":
qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
- for c in "## With a Subtitle":
+ for c in "## Chapter":
qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
- for c in "### An Even Subier Title":
+ for c in "@pov: Jane":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ for c in "@plot: MainPlot":
qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
- for c in "#### Basically Not a Title at All":
+ for c in "### Scene":
qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
@@ -109,7 +176,23 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
for c in "% How about a comment?":
qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
- for c in "@keyword: value":
+ for c in "@pov: Jane":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ for c in "@plot: MainPlot":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ for c in "@location: Home":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+
+ for c in "#### Some Section":
+ qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+ qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
+
+ for c in "@char: Jane":
qtbot.keyClick(nwGUI.docEditor, c, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay)
@@ -133,13 +216,14 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
qtbot.wait(stepDelay)
nwGUI.docEditor.wCounter.run()
qtbot.wait(stepDelay)
- nwGUI.docEditor._updateCounts()
# Save the document
assert nwGUI.docEditor.docChanged
assert nwGUI.saveDocument()
assert not nwGUI.docEditor.docChanged
qtbot.wait(stepDelay)
+ nwGUI.rebuildIndex()
+ qtbot.wait(stepDelay)
# Open and view the edited document
nwGUI.setFocus(3)
@@ -148,16 +232,48 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
qtbot.wait(stepDelay)
assert nwGUI.saveProject()
assert nwGUI.closeDocViewer()
+ qtbot.wait(stepDelay)
# Check the files
- projFile = path.join(nwTempGUI,"nwProject.nwx")
- assert cmpFiles(projFile, path.join(nwRef,"gui","1_nwProject.nwx"), [2])
- sceneFile = path.join(nwTempGUI,"data_3","1489056e0916_main.nwd")
- assert cmpFiles(sceneFile, path.join(nwRef,"gui","1_1489056e0916_main.nwd"))
+ refFile = path.join(nwTempGUI,"nwProject.nwx")
+ assert cmpFiles(refFile, path.join(nwRef,"gui","1_nwProject.nwx"), [2])
+ refFile = path.join(nwTempGUI,"data_0","2d20bbd7e394_main.nwd")
+ assert cmpFiles(refFile, path.join(nwRef,"gui","1_2d20bbd7e394_main.nwd"))
+ refFile = path.join(nwTempGUI,"data_2","fca346db6561_main.nwd")
+ assert cmpFiles(refFile, path.join(nwRef,"gui","1_fca346db6561_main.nwd"))
+ refFile = path.join(nwTempGUI,"data_3","1489056e0916_main.nwd")
+ 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):
+ nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI])
+ 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):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI])
diff --git a/tests/test_project.py b/tests/test_project.py
index db925c02..baeaf1a6 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -11,6 +11,7 @@ from nwdummy import DummyMain
from nw.config import Config
from nw.project.project import NWProject
from nw.project.item import NWItem
+from nw.project.index import NWIndex
from nw.enum import nwItemClass
theConf = Config()
@@ -60,3 +61,59 @@ def testProjectNewRoot(nwTempProj,nwRef):
assert theProject.saveProject()
assert cmpFiles(projFile, refFile, [2])
assert not theProject.projChanged
+
+@pytest.mark.project
+def testIndexScanThis(nwTempProj):
+ projFile = path.join(nwTempProj,"nwProject.nwx")
+ assert theProject.openProject(projFile)
+
+ theIndex = NWIndex(theProject,theMain)
+ tHandle = "31489056e0916"
+
+ isValid, theBits, thePos = theIndex.scanThis("tag: this, and this")
+ assert not isValid
+
+ isValid, theBits, thePos = theIndex.scanThis("@:")
+ assert not isValid
+
+ isValid, theBits, thePos = theIndex.scanThis("@a:")
+ assert isValid
+ assert str(theBits) == "['@a']"
+ assert str(thePos) == "[0]"
+
+ isValid, theBits, thePos = theIndex.scanThis("@a:b")
+ assert isValid
+ assert str(theBits) == "['@a', 'b']"
+ assert str(thePos) == "[0, 3]"
+
+ isValid, theBits, thePos = theIndex.scanThis("@a:b,c,d")
+ assert isValid
+ assert str(theBits) == "['@a', 'b', 'c', 'd']"
+ assert str(thePos) == "[0, 3, 5, 7]"
+
+ isValid, theBits, thePos = theIndex.scanThis("@tag: this, and this")
+ assert isValid
+ assert str(theBits) == "['@tag', 'this', 'and this']"
+ assert str(thePos) == "[0, 6, 12]"
+
+@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']"