Merge pull request #22 from vkbo/indexer
Tags Indexer and Simple TimeLine
This commit is contained in:
@@ -9,6 +9,7 @@ __pycache__
|
||||
sample/**/cache
|
||||
sample/**/wordlist.txt
|
||||
sample/**/*.bak
|
||||
sample/**/*.json
|
||||
|
||||
# PyTest
|
||||
tests/temp
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
+19
-7
@@ -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
|
||||
|
||||
|
||||
+5
-4
@@ -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
|
||||
|
||||
|
||||
+23
-19
@@ -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
|
||||
|
||||
|
||||
+85
-38
@@ -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
|
||||
|
||||
+54
-34
@@ -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")
|
||||
|
||||
+2
-2
@@ -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("<b>Document:</b> {: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:
|
||||
|
||||
@@ -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
|
||||
+76
-11
@@ -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.
|
||||
|
||||
+7
-47
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -9,3 +9,4 @@ hidden = 150, 150, 150
|
||||
keyword = 200, 46, 0
|
||||
value = 184, 200, 0
|
||||
spellcheckline = 200, 46, 0
|
||||
tagerror = 46, 200, 0
|
||||
|
||||
@@ -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
|
||||
@@ -1,3 +1,5 @@
|
||||
# John Smith
|
||||
|
||||
@tag: John
|
||||
|
||||
He’s pretty cool. Not Brad Pitt though.
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Earth
|
||||
|
||||
@tag: Earth
|
||||
|
||||
Third planet from the sun, fairly dense, and with lots of people on it.
|
||||
@@ -1,3 +1,5 @@
|
||||
# Jane Smith
|
||||
|
||||
@tag: Jane
|
||||
|
||||
She’s pretty cool. Not Angelina Jolie though.
|
||||
@@ -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 …
|
||||
@@ -0,0 +1,6 @@
|
||||
# Space
|
||||
|
||||
@tag: Space
|
||||
|
||||
This is somewhere in outer space.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.1.4" fileVersion="1.0" timeStamp="2019-05-26 15:52:53">
|
||||
<novelWriterXML appVersion="0.1.4" fileVersion="1.0" timeStamp="2019-05-31 20:26:54">
|
||||
<project>
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
@@ -8,9 +8,9 @@
|
||||
</project>
|
||||
<settings>
|
||||
<spellCheck>True</spellCheck>
|
||||
<lastEdited>636b6aa9b697b</lastEdited>
|
||||
<lastEdited>bc0cbd2a407f3</lastEdited>
|
||||
<lastViewed>None</lastViewed>
|
||||
<lastWordCount>540</lastWordCount>
|
||||
<lastWordCount>565</lastWordCount>
|
||||
<status>
|
||||
<entry blue="100" green="100" red="100">New</entry>
|
||||
<entry blue="0" green="50" red="200">Notes</entry>
|
||||
@@ -27,7 +27,7 @@
|
||||
<entry blue="175" green="0" red="117">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="14">
|
||||
<content count="15">
|
||||
<item handle="7031beac91f75" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
@@ -76,7 +76,7 @@
|
||||
<charCount>656</charCount>
|
||||
<wordCount>121</wordCount>
|
||||
<paraCount>5</paraCount>
|
||||
<cursorPos>573</cursorPos>
|
||||
<cursorPos>77</cursorPos>
|
||||
</item>
|
||||
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
|
||||
<name>New File</name>
|
||||
@@ -85,10 +85,10 @@
|
||||
<status>Notes</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>69</charCount>
|
||||
<wordCount>17</wordCount>
|
||||
<charCount>82</charCount>
|
||||
<wordCount>19</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<cursorPos>69</cursorPos>
|
||||
</item>
|
||||
<item handle="f6622b4617424" order="1" parent="None">
|
||||
<name>Characters</name>
|
||||
@@ -111,10 +111,10 @@
|
||||
<status>Minor</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>42</charCount>
|
||||
<wordCount>8</wordCount>
|
||||
<charCount>49</charCount>
|
||||
<wordCount>9</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<cursorPos>24</cursorPos>
|
||||
</item>
|
||||
<item handle="bb2c23b3c42cc" order="1" parent="f7e2d9f330615">
|
||||
<name>Jane Smith</name>
|
||||
@@ -123,10 +123,10 @@
|
||||
<status>Major</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>51</charCount>
|
||||
<charCount>55</charCount>
|
||||
<wordCount>9</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<cursorPos>25</cursorPos>
|
||||
</item>
|
||||
<item handle="15c4492bd5107" order="2" parent="None">
|
||||
<name>Locations</name>
|
||||
@@ -142,10 +142,22 @@
|
||||
<status>None</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>0</charCount>
|
||||
<wordCount>0</wordCount>
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
<charCount>76</charCount>
|
||||
<wordCount>15</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>20</cursorPos>
|
||||
</item>
|
||||
<item handle="f1471bef9f2ae" order="1" parent="15c4492bd5107">
|
||||
<name>Space</name>
|
||||
<type>FILE</type>
|
||||
<class>WORLD</class>
|
||||
<status>None</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>38</charCount>
|
||||
<wordCount>7</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>57</cursorPos>
|
||||
</item>
|
||||
<item handle="98acd8c76c93a" order="3" parent="None">
|
||||
<name>Trash</name>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Main Plot
|
||||
|
||||
@tag: MainPlot
|
||||
|
||||
This is a file detailing the main plot.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Main Location
|
||||
|
||||
@tag: Home
|
||||
|
||||
This is a file describing Jane’s home.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Jane Doe
|
||||
|
||||
@tag: Jane
|
||||
|
||||
This is a file about Jane.
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.1.4" fileVersion="1.0" timeStamp="2019-05-25 23:29:32">
|
||||
<novelWriterXML appVersion="0.1.4" fileVersion="1.0" timeStamp="2019-06-01 20:57:28">
|
||||
<project>
|
||||
<name></name>
|
||||
<title></title>
|
||||
@@ -8,7 +8,7 @@
|
||||
<spellCheck>True</spellCheck>
|
||||
<lastEdited>31489056e0916</lastEdited>
|
||||
<lastViewed>31489056e0916</lastViewed>
|
||||
<lastWordCount>69</lastWordCount>
|
||||
<lastWordCount>86</lastWordCount>
|
||||
<status>
|
||||
<entry blue="100" green="100" red="100">New</entry>
|
||||
<entry blue="0" green="50" red="200">Note</entry>
|
||||
@@ -22,7 +22,7 @@
|
||||
<entry blue="0" green="200" red="50">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="6">
|
||||
<content count="9">
|
||||
<item handle="73475cb40a568" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
@@ -44,31 +44,67 @@
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>SCENE</layout>
|
||||
<charCount>377</charCount>
|
||||
<wordCount>69</wordCount>
|
||||
<charCount>331</charCount>
|
||||
<wordCount>59</wordCount>
|
||||
<paraCount>2</paraCount>
|
||||
<cursorPos>443</cursorPos>
|
||||
<cursorPos>465</cursorPos>
|
||||
</item>
|
||||
<item handle="44cb730c42048" order="1" parent="None">
|
||||
<name>Characters</name>
|
||||
<type>ROOT</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
</item>
|
||||
<item handle="2fca346db6561" order="0" parent="44cb730c42048">
|
||||
<name>New File</name>
|
||||
<type>FILE</type>
|
||||
<class>CHARACTER</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>34</charCount>
|
||||
<wordCount>8</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>51</cursorPos>
|
||||
</item>
|
||||
<item handle="71ee45a3c0db9" order="2" parent="None">
|
||||
<name>Plot</name>
|
||||
<type>ROOT</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
</item>
|
||||
<item handle="02d20bbd7e394" order="0" parent="71ee45a3c0db9">
|
||||
<name>New File</name>
|
||||
<type>FILE</type>
|
||||
<class>PLOT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>48</charCount>
|
||||
<wordCount>10</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>69</cursorPos>
|
||||
</item>
|
||||
<item handle="811786ad1ae74" order="3" parent="None">
|
||||
<name>World</name>
|
||||
<type>ROOT</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>True</expanded>
|
||||
</item>
|
||||
<item handle="7688b6ef52555" order="0" parent="811786ad1ae74">
|
||||
<name>New File</name>
|
||||
<type>FILE</type>
|
||||
<class>WORLD</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>NOTE</layout>
|
||||
<charCount>51</charCount>
|
||||
<wordCount>9</wordCount>
|
||||
<paraCount>1</paraCount>
|
||||
<cursorPos>68</cursorPos>
|
||||
</item>
|
||||
</content>
|
||||
</novelWriterXML>
|
||||
|
||||
@@ -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"]]}}
|
||||
@@ -7,6 +7,7 @@ geometry = 1100, 650
|
||||
treecols = 120, 30, 50
|
||||
mainpane = 300, 800
|
||||
docpane = 400, 400
|
||||
timeline = 600, 400
|
||||
|
||||
[Project]
|
||||
autosaveproject = 60
|
||||
|
||||
+131
-15
@@ -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])
|
||||
|
||||
@@ -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']"
|
||||
|
||||
Reference in New Issue
Block a user