Merge branch 'main' into better_spellcheck

This commit is contained in:
Veronica K. B. Olsen
2020-10-06 22:10:21 +02:00
12 changed files with 356 additions and 91 deletions
+10
View File
@@ -29,6 +29,8 @@ import logging
from datetime import datetime
from PyQt5.QtWidgets import qApp
from nw.constants import nwConst, nwUnicode
logger = logging.getLogger(__name__)
@@ -251,3 +253,11 @@ def makeFileNameSafe(theText):
if c.isalpha() or c.isdigit() or c == " ":
cleanName += c
return cleanName
def getGuiItem(theName):
"""Returns a QtWidget based on its objectName.
"""
for qWidget in qApp.topLevelWidgets():
if qWidget.objectName() == theName:
return qWidget
return None
+3 -1
View File
@@ -33,7 +33,9 @@ class nwConst():
fStampFmt = "%Y-%m-%d %H.%M.%S" # FileName safe format
dStampFmt = "%Y-%m-%d" # Date only format
maxDepth = 30 # Maximum folder depth of a project
maxDepth = 30 # Maximum folder depth of a project
maxDocSize = 5000000 # Maxium size of a single document
maxBuildSize = 10000000 # Maxium size of a project build
# END Class nwConst
+6 -6
View File
@@ -271,16 +271,16 @@ class NWIndex():
theRoot = self.theProject.projTree.getRootItem(tHandle)
if theItem is None:
logger.error("Not indexing unknown item %s" % tHandle)
logger.info("Not indexing unknown item %s" % tHandle)
return False
if theItem.itemType != nwItemType.FILE:
logger.error("Not indexing non-file item %s" % tHandle)
logger.info("Not indexing non-file item %s" % tHandle)
return False
if theItem.itemLayout == nwItemLayout.NO_LAYOUT:
logger.error("Not indexing no-layout item %s" % tHandle)
logger.info("Not indexing no-layout item %s" % tHandle)
return False
if theItem.parHandle is None:
logger.error("Not indexing orphaned item %s" % tHandle)
logger.info("Not indexing orphaned item %s" % tHandle)
return False
# Run word counter for the whole text
@@ -289,10 +289,10 @@ class NWIndex():
# If the file is archived or trashed, we don't index the file itself
if self.theProject.projTree.isTrashRoot(theItem.parHandle):
logger.error("Not indexing trash item %s" % tHandle)
logger.info("Not indexing trash item %s" % tHandle)
return False
if theRoot.itemClass == nwItemClass.ARCHIVE:
logger.error("Not indexing archived item %s" % tHandle)
logger.info("Not indexing archived item %s" % tHandle)
return False
itemClass = theItem.itemClass
+17 -1
View File
@@ -33,7 +33,7 @@ from PyQt5.QtCore import QRegularExpression
from nw.core.document import NWDoc
from nw.core.tools import numberToWord, numberToRoman
from nw.constants import nwItemLayout, nwItemType, nwRegEx
from nw.constants import nwConst, nwItemLayout, nwItemType, nwRegEx
logger = logging.getLogger(__name__)
@@ -120,6 +120,9 @@ class Tokenizer():
self.isNote = False
self.isNovel = False
# Error Handling
self.errData = []
return
##
@@ -212,6 +215,14 @@ class Tokenizer():
theDocument = NWDoc(self.theProject, self.theParent)
self.theText = theDocument.openDocument(theHandle)
docSize = len(self.theText)
if docSize > nwConst.maxDocSize:
errVal = "Document '%s' is too big (%.2f MB). Skipping." % (
self.theItem.itemName, docSize/1.0e6
)
self.theText = "# ERROR\n\n%s\n\n" % errVal
self.errData.append(errVal)
self.isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
self.isTitle = self.theItem.itemLayout == nwItemLayout.TITLE
self.isBook = self.theItem.itemLayout == nwItemLayout.BOOK
@@ -230,6 +241,11 @@ class Tokenizer():
"""
return self.theResult
def getResultSize(self):
"""Return the size of the result from the conversion.
"""
return len(self.theResult)
def getFilteredMarkdown(self):
"""Return the novelWriter markdown after the filters have been applied.
"""
+12 -2
View File
@@ -29,6 +29,8 @@
import logging
from nw.constants import nwUnicode
logger = logging.getLogger(__name__)
# =============================================================================================== #
@@ -44,6 +46,15 @@ def countWords(theText):
paraCount = 0
prevEmpty = True
# We need to treat dashes as word separators for counting words.
# The check+replace apprach is much faster that direct replace for
# large texts, and a bit slower for small texts, but in the latter
# case it doesn't matter.
if nwUnicode.U_ENDASH in theText:
theText = theText.replace(nwUnicode.U_ENDASH, " ")
if nwUnicode.U_EMDASH in theText:
theText = theText.replace(nwUnicode.U_EMDASH, " ")
for aLine in theText.splitlines():
countPara = True
@@ -72,8 +83,7 @@ def countWords(theText):
charCount -= 2
countPara = False
theBuff = aLine.replace("", " ").replace("", " ")
wordCount += len(theBuff.split())
wordCount += len(aLine.split())
charCount += theLen
if countPara and prevEmpty:
paraCount += 1
+62 -15
View File
@@ -36,10 +36,10 @@ from datetime import datetime
from PyQt5.QtCore import Qt, QByteArray, QTimer
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtGui import (
QPalette, QColor, QTextDocumentWriter, QFont
QPalette, QColor, QTextDocumentWriter, QFont, QCursor
)
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
qApp, QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction,
QFileDialog, QFontDialog, QSpinBox, QScrollArea, QSplitter, QWidget,
QSizePolicy
@@ -49,7 +49,7 @@ from nw.common import fuzzyTime, makeFileNameSafe
from nw.gui.custom import QSwitch
from nw.core import ToHtml
from nw.constants import (
nwAlert, nwFiles, nwItemType, nwItemLayout, nwItemClass
nwConst, nwAlert, nwFiles, nwItemType, nwItemLayout, nwItemClass
)
logger = logging.getLogger(__name__)
@@ -77,7 +77,7 @@ class GuiBuildNovel(QDialog):
self.theTheme = theParent.theTheme
self.optState = self.theProject.optState
self.htmlText = [] # List of html document
self.htmlText = [] # List of html documents
self.htmlStyle = [] # List of html styles
self.nwdText = [] # List of markdown documents
self.buildTime = 0 # The timestamp of the last build
@@ -483,7 +483,11 @@ class GuiBuildNovel(QDialog):
logger.debug("GuiBuildNovel initialisation complete")
# Load from Cache
return
def viewCachedDoc(self):
"""Load the previously generated document from cache.
"""
if self._loadCache():
textFont = self.textFont.text()
textSize = self.textSize.value()
@@ -494,14 +498,25 @@ class GuiBuildNovel(QDialog):
self.docView.clearStyleSheet()
else:
self.docView.setStyleSheet(self.htmlStyle)
self.docView.setContent(self.htmlText, self.buildTime)
htmlSize = sum([len(x) for x in self.htmlText])
if htmlSize < nwConst.maxBuildSize:
qApp.processEvents()
self.docView.setContent(self.htmlText, self.buildTime)
else:
self.docView.setText(
"Failed to generate preview. The result is too big."
)
self._enableQtSave(False)
else:
self.htmlText = []
self.htmlStyle = []
self.nwdText = []
self.buildTime = 0
return False
return
return True
##
# Slots
@@ -554,6 +569,8 @@ class GuiBuildNovel(QDialog):
self.htmlStyle = []
self.nwdText = []
htmlSize = 0
for nItt, tItem in enumerate(self.theProject.projTree):
noteRoot = noteFiles
@@ -568,6 +585,7 @@ class GuiBuildNovel(QDialog):
makeHtml.doConvert()
self.htmlText.append(makeHtml.getResult())
self.nwdText.append(makeHtml.getFilteredMarkdown())
htmlSize += makeHtml.getResultSize()
elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag):
makeHtml.setText(tItem.itemHandle)
@@ -578,6 +596,7 @@ class GuiBuildNovel(QDialog):
makeHtml.doPostProcessing()
self.htmlText.append(makeHtml.getResult())
self.nwdText.append(makeHtml.getFilteredMarkdown())
htmlSize += makeHtml.getResultSize()
except Exception as e:
logger.error("Failed to generate html of document '%s'" % tItem.itemHandle)
@@ -591,6 +610,12 @@ class GuiBuildNovel(QDialog):
# Update progress bar, also for skipped items
self.buildProgress.setValue(nItt+1)
if makeHtml.errData:
self.theParent.makeAlert((
"There were problems when building the project:"
"<br>-&nbsp;%s"
) % "<br>-&nbsp;".join(makeHtml.errData), nwAlert.ERROR)
if replaceTabs:
htmlText = []
eightSpace = "&nbsp;"*8
@@ -615,7 +640,15 @@ class GuiBuildNovel(QDialog):
self.docView.clearStyleSheet()
else:
self.docView.setStyleSheet(self.htmlStyle)
self.docView.setContent(self.htmlText, self.buildTime)
if htmlSize < nwConst.maxBuildSize:
self.docView.setContent(self.htmlText, self.buildTime)
self._enableQtSave(True)
else:
self.docView.setText(
"Failed to generate preview. The result is too big."
)
self._enableQtSave(False)
self._saveCache()
@@ -955,13 +988,24 @@ class GuiBuildNovel(QDialog):
"""Capture the user closing the window so we can save settings.
"""
self._saveSettings()
QDialog.closeEvent(self, theEvent)
self.docView.clear()
theEvent.accept()
return
##
# Internal Functions
##
def _enableQtSave(self, theState):
"""Set the enabled status of Save menu entries that depend on
the QTextDocument.
"""
self.saveODT.setEnabled(theState)
self.savePDF.setEnabled(theState)
self.saveMD.setEnabled(theState)
self.saveTXT.setEnabled(theState)
return
def _saveSettings(self):
"""Save the various user settings.
"""
@@ -1063,6 +1107,12 @@ class GuiBuildNovelDocView(QTextBrowser):
theFont.setPointSize(self.mainConf.textSize)
self.setFont(theFont)
# Set the tab stops
if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
docPalette = self.palette()
docPalette.setColor(QPalette.Base, QColor(255, 255, 255))
docPalette.setColor(QPalette.Text, QColor(0, 0, 0))
@@ -1126,17 +1176,13 @@ class GuiBuildNovelDocView(QTextBrowser):
self.buildTime = timeStamp
sPos = self.verticalScrollBar().value()
# Refresh the tab stops
if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
theText = theText.replace("\t", "!!tab!!")
theText = theText.replace("<del>", "<span style='text-decoration: line-through;'>")
theText = theText.replace("</del>", "</span>")
self.setHtml(theText)
qApp.processEvents()
while self.find("!!tab!!"):
theCursor = self.textCursor()
@@ -1148,6 +1194,7 @@ class GuiBuildNovelDocView(QTextBrowser):
# Since we change the content while it may still be rendering, we mark
# the document dirty again to make sure it's re-rendered properly.
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
qApp.restoreOverrideCursor()
return
+136 -48
View File
@@ -36,7 +36,7 @@ import logging
from time import time
from PyQt5.QtCore import (
Qt, QSize, QThread, QTimer, pyqtSlot, QRegExp, QRegularExpression
Qt, QSize, QThread, QTimer, pyqtSlot, QRegExp, QRegularExpression, QPointF
)
from PyQt5.QtGui import (
QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette,
@@ -52,7 +52,7 @@ from nw.core import NWDoc, NWSpellCheck, NWSpellSimple, countWords
from nw.gui.dochighlight import GuiDocHighlighter
from nw.common import transferCase
from nw.constants import (
nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwItemClass
nwConst, nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwItemClass
)
logger = logging.getLogger(__name__)
@@ -69,21 +69,23 @@ class GuiDocEditor(QTextEdit):
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theParent.theProject
self.docChanged = False
self.spellCheck = False
self.nwDocument = NWDoc(self.theProject, self.theParent)
self.theHandle = None
self.theDict = None
self.docChanged = False # Flag for changed status of document
self.spellCheck = False # Flag for spell checking enabled
self.theHandle = None # The handle of the open file
self.theDict = None # The current spell check dictionary
self.nonWord = "\"'" # Characters to not include in spell checking
# Document Variables
self.charCount = 0
self.wordCount = 0
self.paraCount = 0
self.lastEdit = 0
self.lastFind = None
self.bigDoc = False
self.doReplace = False
self.nonWord = "\"'"
self.charCount = 0 # Character count
self.wordCount = 0 # Word count
self.paraCount = 0 # Paragraph count
self.lastEdit = 0 # Time stamp of last edit
self.lastFind = None # Position of the last found search word
self.bigDoc = False # Flag for very large document size
self.doReplace = False # Switch to temporarily disable auto-replace
self.queuePos = None # Used for delayed change of cursor position
# Typography
self.typDQOpen = self.mainConf.fmtDoubleQuotes[0]
@@ -94,6 +96,7 @@ class GuiDocEditor(QTextEdit):
# Core Elements and Signals
self.qDocument = self.document()
self.qDocument.contentsChange.connect(self._docChange)
self.qDocument.documentLayout().documentSizeChanged.connect(self._docSizeChanged)
# Document Title
self.docHeader = GuiDocEditHeader(self)
@@ -161,8 +164,10 @@ class GuiDocEditor(QTextEdit):
self.wordCount = 0
self.paraCount = 0
self.lastEdit = 0
self.lastFind = None
self.bigDoc = False
self.doReplace = False
self.queuePos = None
self.setDocumentChanged(False)
self.docHeader.setTitleFromHandle(self.theHandle)
@@ -216,6 +221,12 @@ class GuiDocEditor(QTextEdit):
self.qDocument.setDefaultTextOption(theOpt)
# Refresh the tab stops
if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
# Initialise the syntax highlighter
self.hLight.initHighlighter()
@@ -223,21 +234,12 @@ class GuiDocEditor(QTextEdit):
# font changed, otherwise we just clear the editor entirely,
# which makes it read only.
if self.theHandle is not None:
self.reloadText()
self.redrawText()
else:
self.clearEditor()
return True
def reloadText(self):
"""Reloads the document currently being edited.
"""
if self.theHandle is not None:
tHandle = self.theHandle
self.clearEditor()
self.loadText(tHandle, showStatus=False)
return
def loadText(self, tHandle, tLine=None, showStatus=True):
"""Load text from a document into the editor. If we have an io
error, we must handle this and clear the editor so that we don't
@@ -253,12 +255,22 @@ class GuiDocEditor(QTextEdit):
self.clearEditor()
return False
docSize = len(theDoc)
if docSize > nwConst.maxDocSize:
self.theParent.makeAlert((
"The document you are trying to open is too big. "
"The document size is %.2f\u202fMB. "
"The maximum size allowed is %.2f\u202fMB."
) % (docSize/1.0e6, nwConst.maxDocSize/1.0e6), nwAlert.ERROR)
self.clearEditor()
return False
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self.hLight.setHandle(tHandle)
# Check that the document is not too big for full, initial spell
# checking. If it is too big, we switch to only check as we type
self._checkDocSize(len(theDoc))
self._checkDocSize(docSize)
spTemp = self.hLight.spellCheck
if self.bigDoc:
self.hLight.spellCheck = False
@@ -266,16 +278,12 @@ class GuiDocEditor(QTextEdit):
bfTime = time()
self._allowAutoReplace(False)
self.setPlainText(theDoc)
qApp.processEvents()
self._allowAutoReplace(True)
afTime = time()
logger.debug("Document highlighted in %.3f milliseconds" % (1000*(afTime-bfTime)))
theItem = self.nwDocument.getCurrentItem()
if tLine is None and theItem is not None:
self.setCursorPosition(theItem.cursorPos)
else:
self.setCursorLine(tLine)
self.lastEdit = time()
self._runCounter()
self.wcTimer.start()
@@ -287,24 +295,54 @@ class GuiDocEditor(QTextEdit):
self.docFooter.setHandle(self.theHandle)
self.updateDocMargins()
self.hLight.spellCheck = spTemp
theItem = self.nwDocument.getCurrentItem()
if tLine is None and theItem is not None:
# For large documents we queue the repositioning until the
# document layout has grown past the point we want to move
# the cursor to. This makes the loading significantly
# faster.
if docSize > 50000:
self.queuePos = theItem.cursorPos
else:
self.setCursorPosition(theItem.cursorPos)
else:
self.setCursorLine(tLine)
qApp.restoreOverrideCursor()
# Refresh the tab stops
if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
return True
def updateTagHighLighting(self, forceBigDoc=False):
"""Rerun the syntax highlighter on all meta data lines.
"""
self.hLight.rehighlightByType(GuiDocHighlighter.BLOCK_META)
return
def redrawText(self):
"""Redraw the text by marking the document content as "dirty".
"""
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
return
def replaceText(self, theText):
"""Replaces the text of the current document with the provided
text. This also clears undo history.
"""
docSize = len(theText)
if docSize > nwConst.maxDocSize:
self.theParent.makeAlert((
"The text you are trying to add is too big. "
"The text size is %.2f\u202fMB. "
"The maximum size allowed is %.2f\u202fMB."
) % (docSize/1.0e6, nwConst.maxDocSize/1.0e6), nwAlert.ERROR)
return False
self.setPlainText(theText)
self.setDocumentChanged(True)
self.updateDocMargins()
return
return True
def saveText(self):
"""Save the text currently in the editor to the NWDoc object,
@@ -315,11 +353,10 @@ class GuiDocEditor(QTextEdit):
return False
docText = self.getText()
cursPos = self.getCursorPosition()
theItem.setCharCount(self.charCount)
theItem.setWordCount(self.wordCount)
theItem.setParaCount(self.paraCount)
theItem.setCursorPos(cursPos)
self.saveCursorPosition()
self.nwDocument.saveDocument(docText)
self.setDocumentChanged(False)
@@ -428,6 +465,15 @@ class GuiDocEditor(QTextEdit):
"""
return self.textCursor().selectionEnd()
def saveCursorPosition(self):
"""Save the cursor position to the current project item object.
"""
theItem = self.nwDocument.getCurrentItem()
if theItem is not None:
cursPos = self.getCursorPosition()
theItem.setCursorPos(cursPos)
return
def setCursorLine(self, theLine):
"""Move the cursor to a given line in the document.
"""
@@ -731,6 +777,13 @@ class GuiDocEditor(QTextEdit):
"""
self.lastEdit = time()
self.lastFind = None
if self.qDocument.characterCount() > nwConst.maxDocSize:
self.theParent.makeAlert((
"The document has grown too big and you cannot add more text to it. "
"The maximum size of a single novelWriter document is %.2f\u202fMB."
) % (nwConst.maxDocSize/1.0e6), nwAlert.ERROR)
self.undo()
return
if not self.docChanged:
self.setDocumentChanged(True)
if not self.wcTimer.isActive():
@@ -899,6 +952,29 @@ class GuiDocEditor(QTextEdit):
return
@pyqtSlot("QSizeF")
def _docSizeChanged(self, theSize):
"""Called whenever the underlying document layout size changes.
This is used to queue the repositioning of the cursor for very
large documents to ensure the region where the cursor is being
moved to has been drawn before the move is made.
"""
if self.queuePos is not None:
thePos = self.qDocument.documentLayout().hitTest(
QPointF(theSize.width(), theSize.height()), Qt.FuzzyHit
)
if self.queuePos <= thePos:
logger.verbose(
"Allowed cursor move to %d <= %d" % (self.queuePos, thePos)
)
self.setCursorPosition(self.queuePos)
self.queuePos = None
else:
logger.verbose(
"Denied cursor move to %d > %d" % (self.queuePos, thePos)
)
return
##
# Internal Functions
##
@@ -1054,15 +1130,24 @@ class GuiDocEditor(QTextEdit):
"""Check if document size crosses the big document limit set in
config. If so, we will set the big document flag to True.
"""
if theSize > self.mainConf.bigDocLimit*1000:
logger.info(
"The document size is %d > %d, big doc mode is enabled" % (
theSize, self.mainConf.bigDocLimit*1000
newState = theSize > self.mainConf.bigDocLimit*1000
if newState != self.bigDoc:
if newState:
logger.info(
"The document size is {:n} > {:n}, big doc mode has been enabled".format(
theSize, self.mainConf.bigDocLimit*1000
)
)
)
self.bigDoc = True
else:
self.bigDoc = False
else:
logger.info(
"The document size is {:n} <= {:n}, big doc mode has been disabled".format(
theSize, self.mainConf.bigDocLimit*1000
)
)
self.bigDoc = newState
return
def _wrapSelection(self, tBefore, tAfter=None):
@@ -2182,6 +2267,9 @@ class GuiDocEditFooter(QWidget):
self.wordsText.setText("Words: {:n} ({:+n})".format(wCount, wDiff))
byteSize = self.docEditor.qDocument.characterCount()
self.wordsText.setToolTip("Document size is {:n} bytes".format(byteSize))
return
# END Class GuiDocEditFooter
+29
View File
@@ -39,6 +39,11 @@ logger = logging.getLogger(__name__)
class GuiDocHighlighter(QSyntaxHighlighter):
BLOCK_NONE = 0
BLOCK_TEXT = 1
BLOCK_META = 2
BLOCK_TITLE = 4
def __init__(self, theDoc, theParent):
QSyntaxHighlighter.__init__(self, theDoc)
@@ -229,6 +234,22 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.theHandle = theHandle
return True
##
# Methods
##
def rehighlightByType(self, theType):
"""Loop through all blocks and rehighlight those of a given
content type.
"""
qDocument = self.document()
nBlocks = qDocument.blockCount()
for i in range(nBlocks):
theBlock = qDocument.findBlockByNumber(i)
if theBlock.userState() & theType == theType:
self.rehighlightBlock(theBlock)
return
##
# Highlight Block
##
@@ -239,10 +260,12 @@ class GuiDocHighlighter(QSyntaxHighlighter):
is significantly faster than running the regex checks used for
text paragraphs.
"""
self.setCurrentBlockState(self.BLOCK_NONE)
if self.theHandle is None or not theText:
return
if theText.startswith("@"): # Keywords and commands
self.setCurrentBlockState(self.BLOCK_META)
tItem = self.theParent.theProject.projTree[self.theHandle]
isValid, theBits, thePos = self.theIndex.scanThis(theText)
isGood = self.theIndex.checkThese(theBits, tItem)
@@ -266,22 +289,27 @@ class GuiDocHighlighter(QSyntaxHighlighter):
return
elif theText.startswith("# "): # Header 1
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 1, self.hStyles["header1h"])
self.setFormat(1, len(theText), self.hStyles["header1"])
elif theText.startswith("## "): # Header 2
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 2, self.hStyles["header2h"])
self.setFormat(2, len(theText), self.hStyles["header2"])
elif theText.startswith("### "): # Header 3
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 3, self.hStyles["header3h"])
self.setFormat(3, len(theText), self.hStyles["header3"])
elif theText.startswith("#### "): # Header 4
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 4, self.hStyles["header4h"])
self.setFormat(4, len(theText), self.hStyles["header4"])
elif theText.startswith("%"): # Comments
self.setCurrentBlockState(self.BLOCK_TEXT)
toCheck = theText[1:].lstrip()
synTag = toCheck[:9].lower()
tLen = len(theText)
@@ -294,6 +322,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setFormat(0, tLen, self.hStyles["hidden"])
else: # Text Paragraph
self.setCurrentBlockState(self.BLOCK_TEXT)
for rX, xFmt in self.rxRules:
rxItt = rX.globalMatch(theText, 0)
while rxItt.hasNext():
+18 -6
View File
@@ -125,7 +125,7 @@ class GuiProjectLoad(QDialog):
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Open | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doOpenRecent)
self.buttonBox.rejected.connect(self._doClose)
self.buttonBox.rejected.connect(self._doCancel)
self.newButton = self.buttonBox.addButton("New", QDialogButtonBox.ActionRole)
self.newButton.clicked.connect(self._doNewProject)
@@ -153,7 +153,7 @@ class GuiProjectLoad(QDialog):
"""Close the dialog window with a recent project selected.
"""
logger.verbose("GuiProjectLoad open button clicked")
self._saveDialogState()
self._saveSettings()
selItems = self.listBox.selectedItems()
if selItems:
@@ -194,11 +194,12 @@ class GuiProjectLoad(QDialog):
return
def _doClose(self):
def _doCancel(self):
"""Close the dialog window without doing anything.
"""
logger.verbose("GuiProjectLoad close button clicked")
self._saveDialogState()
self.openPath = None
self.openState = self.NONE_STATE
self.close()
return
@@ -206,7 +207,7 @@ class GuiProjectLoad(QDialog):
"""Create a new project.
"""
logger.verbose("GuiProjectLoad new project button clicked")
self._saveDialogState()
self._saveSettings()
self.openPath = None
self.openState = self.NEW_STATE
self.accept()
@@ -230,11 +231,22 @@ class GuiProjectLoad(QDialog):
return
##
# Events
##
def closeEvent(self, theEvent):
"""Capture the user closing the dialog so we can save settings.
"""
self._saveSettings()
theEvent.accept()
return
##
# Internal Functions
##
def _saveDialogState(self):
def _saveSettings(self):
"""Save the changes made to the dialog.
"""
colWidths = [0, 0, 0]
+8 -3
View File
@@ -33,7 +33,7 @@ import os
from datetime import datetime
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
from PyQt5.QtGui import QPixmap, QCursor
from PyQt5.QtWidgets import (
qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout,
QLabel, QGroupBox, QMenu, QAction, QFileDialog, QSpinBox, QHBoxLayout
@@ -253,10 +253,15 @@ class GuiWritingStats(QDialog):
logger.debug("GuiWritingStats initialisation complete")
qApp.processEvents()
return
def populateGUI(self):
"""Populate list box with data from the log file.
"""
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self._loadLogFile()
self._updateListBox()
qApp.restoreOverrideCursor()
return
##
+17 -4
View File
@@ -48,6 +48,7 @@ from nw.gui import (
)
from nw.core import NWProject, NWDoc, NWIndex
from nw.constants import nwItemType, nwItemClass, nwAlert
from nw.common import getGuiItem
logger = logging.getLogger(__name__)
@@ -448,6 +449,7 @@ class GuiMain(QMainWindow):
"""Close the document and clear the editor and title field.
"""
if self.hasProject:
self.docEditor.saveCursorPosition()
if self.docEditor.docChanged:
self.saveDocument()
self.docEditor.clearEditor()
@@ -716,8 +718,7 @@ class GuiMain(QMainWindow):
tEnd = time()
self.statusBar.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0))
self.docEditor.reloadText()
self.docEditor.updateTagHighLighting()
qApp.restoreOverrideCursor()
if not beQuiet:
@@ -816,9 +817,15 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return
dlgBuild = GuiBuildNovel(self, self.theProject)
dlgBuild = getGuiItem("GuiBuildNovel")
if dlgBuild is None:
dlgBuild = GuiBuildNovel(self, self.theProject)
dlgBuild.setModal(False)
dlgBuild.show()
qApp.processEvents()
dlgBuild.viewCachedDoc()
return
def showWritingStatsDialog(self):
@@ -828,9 +835,15 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return
dlgStats = GuiWritingStats(self, self.theProject)
dlgStats = getGuiItem("GuiWritingStats")
if dlgStats is None:
dlgStats = GuiWritingStats(self, self.theProject)
dlgStats.setModal(False)
dlgStats.show()
qApp.processEvents()
dlgStats.populateGUI()
return
def showAboutNWDialog(self):
+38 -5
View File
@@ -114,7 +114,12 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR
projEdit._doSave()
# Open again, and check project settings
projEdit = GuiProjectSettings(nwGUI, nwGUI.theProject)
nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiProjectSettings") is not None, timeout=1000)
projEdit = getGuiItem("GuiProjectSettings")
assert isinstance(projEdit, GuiProjectSettings)
qtbot.addWidget(projEdit)
assert projEdit.tabMain.editName.text() == "Project Name"
assert projEdit.tabMain.editTitle.text() == "Project Title"
@@ -582,8 +587,13 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
nwBuild._doClose()
# Re-open build dialog from cahce
nwBuild = GuiBuildNovel(nwGUI, nwGUI.theProject)
nwGUI.mainMenu.aBuildProject.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiBuildNovel") is not None, timeout=1000)
nwBuild = getGuiItem("GuiBuildNovel")
assert isinstance(nwBuild, GuiBuildNovel)
assert nwBuild.viewCachedDoc()
assert nwBuild.htmlText == htmlText
assert nwBuild.htmlStyle == htmlStyle
assert nwBuild.nwdText == nwdText
@@ -659,7 +669,11 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
# Split By Scene
assert nwGUI.treeView.setSelectedHandle("73475cb40a568")
qtbot.wait(stepDelay)
nwSplit = GuiDocSplit(nwGUI, nwGUI.theProject)
nwGUI.mainMenu.aSplitDoc.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiDocSplit") is not None, timeout=1000)
nwSplit = getGuiItem("GuiDocSplit")
assert isinstance(nwSplit, GuiDocSplit)
qtbot.wait(stepDelay)
nwSplit.splitLevel.setCurrentIndex(2)
qtbot.wait(stepDelay)
@@ -691,7 +705,11 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
# Split By Section
assert nwGUI.treeView.setSelectedHandle("73475cb40a568")
qtbot.wait(stepDelay)
nwSplit = GuiDocSplit(nwGUI, nwGUI.theProject)
nwGUI.mainMenu.aSplitDoc.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiDocSplit") is not None, timeout=1000)
nwSplit = getGuiItem("GuiDocSplit")
assert isinstance(nwSplit, GuiDocSplit)
qtbot.wait(stepDelay)
nwSplit.splitLevel.setCurrentIndex(3)
qtbot.wait(stepDelay)
@@ -945,6 +963,7 @@ def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
assert nwGUI.openProject(nwMinimal)
assert nwGUI.closeProject()
qtbot.wait(stepDelay)
monkeypatch.setattr(GuiProjectLoad, "exec_", lambda *args: None)
monkeypatch.setattr(GuiProjectLoad, "result", lambda *args: QDialog.Accepted)
nwGUI.mainMenu.aOpenProject.activate(QAction.Trigger)
@@ -954,36 +973,50 @@ def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
assert isinstance(nwLoad, GuiProjectLoad)
nwLoad.show()
qtbot.wait(stepDelay)
recentCount = nwLoad.listBox.topLevelItemCount()
assert recentCount > 0
qtbot.wait(stepDelay)
selItem = nwLoad.listBox.topLevelItem(0)
selPath = selItem.data(nwLoad.C_NAME, Qt.UserRole)
assert isinstance(selItem, QTreeWidgetItem)
qtbot.wait(stepDelay)
nwLoad.selPath.setText("")
nwLoad.listBox.setCurrentItem(selItem)
nwLoad._doSelectRecent()
assert nwLoad.selPath.text() == selPath
qtbot.wait(stepDelay)
qtbot.mouseClick(nwLoad.buttonBox.button(QDialogButtonBox.Open), Qt.LeftButton)
assert nwLoad.openPath == selPath
assert nwLoad.openState == nwLoad.OPEN_STATE
# Just create a new project load from scratch for the rest of the test
del nwLoad
nwLoad = GuiProjectLoad(nwGUI)
qtbot.wait(stepDelay)
nwGUI.mainMenu.aOpenProject.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiProjectLoad") is not None, timeout=1000)
qtbot.wait(stepDelay)
nwLoad = getGuiItem("GuiProjectLoad")
assert isinstance(nwLoad, GuiProjectLoad)
nwLoad.show()
qtbot.wait(stepDelay)
qtbot.mouseClick(nwLoad.buttonBox.button(QDialogButtonBox.Cancel), Qt.LeftButton)
assert nwLoad.openPath is None
assert nwLoad.openState == nwLoad.NONE_STATE
qtbot.wait(stepDelay)
nwLoad.show()
qtbot.mouseClick(nwLoad.newButton, Qt.LeftButton)
assert nwLoad.openPath is None
assert nwLoad.openState == nwLoad.NEW_STATE
qtbot.wait(stepDelay)
nwLoad.show()
nwLoad._keyPressDelete()
assert nwLoad.listBox.topLevelItemCount() == recentCount - 1