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']"