From 34f14b005dbac1140355fa676e6517b143fef68a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Nov 2019 17:54:02 +0100 Subject: [PATCH 01/15] Release 0.4.2 --- CHANGELOG.md | 11 +++++++++++ docs/source/conf.py | 4 ++-- nw/__init__.py | 4 ++-- setup.py | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc0dd74a..236d217a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # novelWriter ChangeLog +## Version 0.4.2 [2019-11-17] + +**User Interface** + +* Distraction free mode now also hides the menu bar, but all keyboard shortcuts used for editing remain active. The rest are disabled. PR #142. + +**Bug Fixes** + +* Fixed various issues with spell checking highlighting. The highlighting and the editor didn't always agree on what words were spelled wrong. PR #141. +* The status bar now shows what spell checking language is actually loaded. Previously, it just showed the language selected in the settings. That was a bit misleading as the available dictionaries can change due to the change in installed dictionary on the system. PR #145. + ## Version 0.4.1 [2019-11-10] **Features** diff --git a/docs/source/conf.py b/docs/source/conf.py index 82b870aa..8cdb4fad 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -24,9 +24,9 @@ copyright = "2018-2019, Veronica Berglyd Olsen" author = "Veronica Berglyd Olsen" # The short X.Y version -version = "0.4.1" +version = "0.4.2" # The full version, including alpha/beta/rc tags -release = "0.4.1" +release = "0.4.2" # -- General configuration --------------------------------------------------- diff --git a/nw/__init__.py b/nw/__init__.py index eafef08e..7f1e2e6d 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -25,8 +25,8 @@ __package__ = "novelWriter" __author__ = "Veronica Berglyd Olsen" __copyright__ = "Copyright 2018–2019, Veronica Berglyd Olsen" __license__ = "GPLv3" -__version__ = "0.4.1" -__date__ = "2019-11-10" +__version__ = "0.4.2" +__date__ = "2019-11-17" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" __status__ = "Development" diff --git a/setup.py b/setup.py index 99d2851c..38e3f3c8 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ with open("README.md", "r") as inFile: setuptools.setup( name = "novelWriter", - version = "0.4.1", + version = "0.4.2", author = "Veronica Berglyd Olsen", author_email = "code@vkbo.net", description = "A markdown-like document editor for writing novels", From ba9f4716958e4297bb1578e01b184fd069ee61c9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Nov 2019 22:10:33 +0100 Subject: [PATCH 02/15] Fixed a minor issue when insert from file was cancelled. --- nw/gui/elements/doceditor.py | 8 ++++++++ nw/guimain.py | 9 ++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py index 7cc4a186..a0baeb74 100644 --- a/nw/gui/elements/doceditor.py +++ b/nw/gui/elements/doceditor.py @@ -229,6 +229,14 @@ class GuiDocEditor(QTextEdit): return True + def replaceText(self, theText): + """Replaces the text of the current document with the provided + text. This also clears undo history. + """ + self.setPlainText(theText) + self.setDocumentChanged(True) + return + def saveText(self): if self.nwDocument.theItem is None: diff --git a/nw/guimain.py b/nw/guimain.py index 0a3a87fc..8f658ce0 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -415,17 +415,20 @@ class GuiMain(QMainWindow): ) if inPath: loadFile = inPath[0] - self.mainConf.setLastPath(loadFile) else: return False + if loadFile.strip() == "": + return False + theText = None try: with open(loadFile,mode="rt",encoding="utf8") as inFile: theText = inFile.read() + self.mainConf.setLastPath(loadFile) except Exception as e: self.makeAlert( - ["Could not read file. The file cannot be a binary file.",str(e)], + ["Could not read file. The file must be an existing text file.",str(e)], nwAlert.ERROR ) return False @@ -449,7 +452,7 @@ class GuiMain(QMainWindow): else: return False - self.docEditor.setText(theText) + self.docEditor.replaceText(theText) return True From 1659d2a110f1f8372a2a4636955174bec698fe7a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 18 Nov 2019 10:30:07 +0100 Subject: [PATCH 03/15] Disable opaque resize of splitters --- nw/guimain.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nw/guimain.py b/nw/guimain.py index 0a3a87fc..853b3e33 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -106,11 +106,13 @@ class GuiMain(QMainWindow): self.viewPane.setLayout(self.docView) self.splitView = QSplitter(Qt.Horizontal) + self.splitView.setOpaqueResize(False) self.splitView.addWidget(self.editPane) self.splitView.addWidget(self.viewPane) self.splitMain = QSplitter(Qt.Horizontal) self.splitMain.setContentsMargins(4,4,4,4) + self.splitMain.setOpaqueResize(False) self.splitMain.addWidget(self.treePane) self.splitMain.addWidget(self.splitView) self.splitMain.setSizes(self.mainConf.mainPanePos) From 0a1d43630118a48f47f7a6db0668f2c12fc261ff Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 18 Nov 2019 10:49:04 +0100 Subject: [PATCH 04/15] Updated readme to clarify that this is not a full-featured markdown editor --- README.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 067e4f6b..0325cb84 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![codecov](https://codecov.io/gh/vkbo/novelWriter/branch/master/graph/badge.svg)](https://codecov.io/gh/vkbo/novelWriter) [![Documentation Status](https://readthedocs.org/projects/novelwriter/badge/?version=latest)](https://novelwriter.readthedocs.io/en/latest/?badge=latest) -novelWriter is a markdown-like text editor designed fro writing novels and larger projects of many smaller plain text documents. +novelWriter is a markdown-like text editor designed for writing novels and larger projects of many smaller plain text documents. The documentation is available here: [novelwriter.readthedocs.io](https://novelwriter.readthedocs.io/). @@ -20,6 +20,34 @@ If you do use it for real projects, please run backups frequently to avoid data There is a built in backup feature that can pack the entire project into a zip file on close. Please check the documentation for further details. +## Markdown Flavour + +novelWriter is **not** a full-feature Markdown editor. +It allows for a minimal set of formatting needed for writing text documents for novels. +These are currently limited to: + +* Headings level 1 to 4 using the `#` syntax only. +* Bold, italic and underline text. +* Hard line breaks using two or more spaces at the end of a line. + +That is it. +Features not supported in the editor are also not exported when using the export tool. + +In addition, novelWriter adds the following, which is otherwise not supported by Markdown: + +* A line starting with `%` is treated as a comment and not rendered on exports unless requested. + Comments do not count towards the word count. +* A set of meta data keyword/value sets starting with the character `@`. + This is used for tagging and inter-linking documents. +* Non-breaking spaces are supported as long as your system is using at least Qt 5.9. + For earlier version, non-breaking spaces are converted to normal spaces when saving the document. + This is done by the Qt library. +* Tabs may be rendered, depending on export format. + +The core export format that should render properly all supported features is the HTML export. +This format also forms the basis of conversion to Office type document formats with Pandoc. +Note that Pandoc itself strips some formatting from the document during conversion, so the final result may be different than expected. + ## Implementation The application is written in Python3 using Qt5 via PyQt5. From a5df5a151ebb17deabd6a057c2e359aad2ec1729 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 18 Nov 2019 19:51:31 +0100 Subject: [PATCH 05/15] Fixed crash in project countStatus when tree contains orphaned item --- nw/gui/dialogs/configeditor.py | 1 - nw/gui/dialogs/export.py | 1 - nw/project/status.py | 2 ++ 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nw/gui/dialogs/configeditor.py b/nw/gui/dialogs/configeditor.py index 8d3d4b0f..89bf6992 100644 --- a/nw/gui/dialogs/configeditor.py +++ b/nw/gui/dialogs/configeditor.py @@ -44,7 +44,6 @@ class GuiConfigEditor(QDialog): self.setWindowTitle("Preferences") self.guiDeco = self.theParent.theTheme.loadDecoration("settings",(64,64)) - self.theProject.countStatus() self.tabMain = GuiConfigEditGeneral(self.theParent) self.tabEditor = GuiConfigEditEditor(self.theParent) diff --git a/nw/gui/dialogs/export.py b/nw/gui/dialogs/export.py index 15a8ed7a..8d580f87 100644 --- a/nw/gui/dialogs/export.py +++ b/nw/gui/dialogs/export.py @@ -50,7 +50,6 @@ class GuiExport(QDialog): self.guiDeco = self.theParent.theTheme.loadDecoration("export",(64,64)) - self.theProject.countStatus() self.tabMain = GuiExportMain(self.theParent, self.theProject, self.optState) self.tabPandoc = GuiExportPandoc(self.theParent, self.theProject, self.optState) diff --git a/nw/project/status.py b/nw/project/status.py index 3edd417a..6e15f5b9 100644 --- a/nw/project/status.py +++ b/nw/project/status.py @@ -41,6 +41,8 @@ class NWStatus(): return True def lookupEntry(self, theLabel): + if theLabel is None: + return None theLabel = theLabel.strip() if theLabel in self.theMap.keys(): return self.theMap[theLabel] From b667dcc6f79b44b599e106288a3f49d167087daa Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 18 Nov 2019 19:56:12 +0100 Subject: [PATCH 06/15] Added docstring to the counting function --- nw/project/project.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nw/project/project.py b/nw/project/project.py index 49105ce8..041029a1 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -582,6 +582,9 @@ class NWProject(): return True def countStatus(self): + """Count how many times the various status flags are used in the + project tree. + """ self.statusItems.resetCounts() self.importItems.resetCounts() for nwItem in self.projTree.values(): From 5f104f9c482f1604c94ea3508fa57ac984a4482a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 18 Nov 2019 20:15:29 +0100 Subject: [PATCH 07/15] Fixed hard line breaks in markdown export --- nw/convert/text/tomarkdown.py | 5 +++-- nw/convert/text/totext.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/nw/convert/text/tomarkdown.py b/nw/convert/text/tomarkdown.py index c9ea57b7..da65d863 100644 --- a/nw/convert/text/tomarkdown.py +++ b/nw/convert/text/tomarkdown.py @@ -85,7 +85,8 @@ class ToMarkdown(Tokenizer): # indicating a new paragraph. if tType == self.T_EMPTY: if len(thisPar) > 0: - self.theResult += "%s\n\n" % " ".join(thisPar) + tTemp = "\n".join(thisPar) + self.theResult += "%s\n\n" % tTemp.rstrip() thisPar = [] elif tType == self.T_HEAD1: @@ -104,7 +105,7 @@ class ToMarkdown(Tokenizer): self.theResult += "%s\n\n" % tText elif tType == self.T_SKIP: - self.theResult += "\n\n\n\n" + self.theResult += "\n\n\n" elif tType == self.T_TEXT: thisPar.append(tText) diff --git a/nw/convert/text/totext.py b/nw/convert/text/totext.py index fa528199..1f98d5e2 100644 --- a/nw/convert/text/totext.py +++ b/nw/convert/text/totext.py @@ -122,8 +122,8 @@ class ToText(Tokenizer): tText = self._centreText(tText,self.wordWrap) self.theResult += "%s\n\n" % tText - elif tType == self.T_SEP: - self.theResult += "\n\n\n\n" + elif tType == self.T_SKIP: + self.theResult += "\n\n\n" elif tType == self.T_TEXT: thisPar.append(tText) From 78a8f5bb9cad191e3e0a3bfd55630d9958d91719 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 19 Nov 2019 10:36:43 +0100 Subject: [PATCH 08/15] Make sure a contentsChange signal is not emitted when document margins change --- nw/gui/elements/doceditor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py index 7cc4a186..c8b69df7 100644 --- a/nw/gui/elements/doceditor.py +++ b/nw/gui/elements/doceditor.py @@ -250,7 +250,7 @@ class GuiDocEditor(QTextEdit): def updateDocMargins(self): """Automatically adjust the margins so the text is centred, but - only if Config.textFixedW is set to True. + only if Config.textFixedW is enabled or we're in Zen mode. """ if self.mainConf.textFixedW or self.theParent.isZenMode: @@ -273,7 +273,13 @@ class GuiDocEditor(QTextEdit): docFormat = self.qDocument.rootFrame().frameFormat() docFormat.setLeftMargin(tM) docFormat.setRightMargin(tM) + + # Updating root frame triggers a QTextDocument->contentsChange + # signal, which we do not want as it re-runs the syntax + # highlighter and spell checker, so we block it briefly. + self.qDocument.blockSignals(True) self.qDocument.rootFrame().setFrameFormat(docFormat) + self.qDocument.blockSignals(False) return From 2217f1d89c2de0780cd8d881f3dfa74f5abc39da Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 19 Nov 2019 18:19:38 +0100 Subject: [PATCH 09/15] Improved highlighter performance and disabled initial spell checking --- nw/gui/elements/doceditor.py | 9 +++++ nw/gui/tools/dochighlight.py | 68 +++++++++++------------------------- 2 files changed, 30 insertions(+), 47 deletions(-) diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py index 3e0aba3c..725769e2 100644 --- a/nw/gui/elements/doceditor.py +++ b/nw/gui/elements/doceditor.py @@ -214,7 +214,14 @@ class GuiDocEditor(QTextEdit): return False self.hLight.setHandle(tHandle) + spTemp = self.hLight.spellCheck + self.hLight.spellCheck = False + + bfTime = time() self.setPlainText(theDoc) + afTime = time() + logger.debug("Document highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))) + self.setCursorPosition(self.nwDocument.theItem.cursorPos) self.lastEdit = time() self._runCounter() @@ -227,6 +234,8 @@ class GuiDocEditor(QTextEdit): else: self.theParent.noticeBar.showNote("This document is read only.") + self.hLight.spellCheck = spTemp + return True def replaceText(self, theText): diff --git a/nw/gui/tools/dochighlight.py b/nw/gui/tools/dochighlight.py index fd093933..1c8cd501 100644 --- a/nw/gui/tools/dochighlight.py +++ b/nw/gui/tools/dochighlight.py @@ -101,47 +101,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): "value" : self._makeFormat(self.colVal), } - # Headers self.hRules = [] - self.hRules.append(( - r"^(#{1}) (.*)[^\n]", { - 0 : self.hStyles["header1"], - 1 : self.hStyles["header1h"], - } - )) - self.hRules.append(( - r"^(#{2}) (.*)[^\n]", { - 0 : self.hStyles["header2"], - 1 : self.hStyles["header2h"], - } - )) - self.hRules.append(( - r"^(#{3}) (.*)[^\n]", { - 0 : self.hStyles["header3"], - 1 : self.hStyles["header3h"], - } - )) - self.hRules.append(( - r"^(#{4}) (.*)[^\n]", { - 0 : self.hStyles["header4"], - 1 : self.hStyles["header4h"], - } - )) - - # Keyword/Value - # self.hRules.append(( - # r"^(@.+?)\s*:\s*(.+?)$", { - # 1 : self.hStyles["keyword"], - # 2 : self.hStyles["value"], - # } - # )) - - # Comments - self.hRules.append(( - r"^%.*$", { - 0 : self.hStyles["hidden"], - } - )) # Trailing Spaces, 2+ self.hRules.append(( @@ -242,11 +202,10 @@ class GuiDocHighlighter(QSyntaxHighlighter): def highlightBlock(self, theText): - if self.theHandle is None: + if self.theHandle is None or not theText: return - if theText.startswith("@"): - # Highlighting of keywords and commands + if theText.startswith("@"): # Keywords and commands tItem = self.theParent.theProject.getItem(self.theHandle) isValid, theBits, thePos = self.theIndex.scanThis(theText) isGood = self.theIndex.checkThese(theBits, tItem) @@ -265,11 +224,26 @@ class GuiDocHighlighter(QSyntaxHighlighter): kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) self.setFormat(xPos, xLen, kwFmt) - # We're done, no need to continue - return + elif theText.startswith("# "): # Header 1 + self.setFormat(0, 1, self.hStyles["header1"]) + self.setFormat(1, len(theText), self.hStyles["header1h"]) - else: - # For other text, just use our regex rules + elif theText.startswith("## "): # Header 2 + self.setFormat(0, 2, self.hStyles["header2"]) + self.setFormat(2, len(theText), self.hStyles["header2h"]) + + elif theText.startswith("### "): # Header 3 + self.setFormat(0, 3, self.hStyles["header3"]) + self.setFormat(3, len(theText), self.hStyles["header3h"]) + + elif theText.startswith("#### "): # Header 4 + self.setFormat(0, 4, self.hStyles["header4"]) + self.setFormat(4, len(theText), self.hStyles["header4h"]) + + elif theText.startswith("%"): # Comments + self.setFormat(0, len(theText), self.hStyles["hidden"]) + + else: # Text Paragraph for rX, xFmt in self.rxRules: rxItt = rX.globalMatch(theText, 0) while rxItt.hasNext(): From 3c6350a4da1be2bf2ed36e8687c5ffa463c627e5 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 19 Nov 2019 18:48:44 +0100 Subject: [PATCH 10/15] Disable initial spell checking for large documents --- nw/config.py | 1 + nw/gui/elements/doceditor.py | 37 ++++++++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/nw/config.py b/nw/config.py index d3689ad7..5735e772 100644 --- a/nw/config.py +++ b/nw/config.py @@ -93,6 +93,7 @@ class Config: self.wordCountTimer = 5.0 self.showTabsNSpaces = False self.showLineEndings = False + self.bigDocLimit = 800 self.fmtApostrophe = nwUnicode.U_RSQUO self.fmtSingleQuotes = [nwUnicode.U_LSQUO,nwUnicode.U_RSQUO] diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py index 725769e2..018e9645 100644 --- a/nw/gui/elements/doceditor.py +++ b/nw/gui/elements/doceditor.py @@ -53,6 +53,7 @@ class GuiDocEditor(QTextEdit): self.wordCount = 0 self.paraCount = 0 self.lastEdit = 0 + self.bigDoc = False self.nonWord = "\"'" # Typography @@ -114,17 +115,22 @@ class GuiDocEditor(QTextEdit): return def clearEditor(self): + """Clear the current document and reset all document related + flags and counters. + """ self.nwDocument.clearDocument() self.setReadOnly(True) self.clear() self.wcTimer.stop() - self.theHandle = None - self.charCount = 0 - self.wordCount = 0 - self.paraCount = 0 - self.lastEdit = 0 + self.theHandle = None + self.charCount = 0 + self.wordCount = 0 + self.paraCount = 0 + self.lastEdit = 0 + self.bigDoc = False + self.hasSelection = False self.setDocumentChanged(False) @@ -214,8 +220,12 @@ class GuiDocEditor(QTextEdit): return False 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)) spTemp = self.hLight.spellCheck - self.hLight.spellCheck = False + self.hLight.spellCheck = not self.bigDoc bfTime = time() self.setPlainText(theDoc) @@ -696,9 +706,24 @@ class GuiDocEditor(QTextEdit): self.theParent.statusBar.setCounts(self.charCount,self.wordCount,self.paraCount) self.theParent.treeView.propagateCount(tHandle, self.wordCount) self.theParent.treeView.projectWordCount() + self._checkDocSize(self.charCount) return + def _checkDocSize(self, theSize): + """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 + )) + self.bigDoc = True + else: + self.bigDoc = False + return + def _wrapSelection(self, tBefore, tAfter): """Wraps the selected text in whatever is in tBefore and tAfter. If there is no selection, the autoSelect setting decides the From 2758c587af0c479b458845a41a85134d4cacc247 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 19 Nov 2019 21:12:40 +0100 Subject: [PATCH 11/15] Added spell check size limit to preferences --- nw/config.py | 4 ++++ nw/gui/dialogs/configeditor.py | 24 +++++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/nw/config.py b/nw/config.py index 5735e772..281fe863 100644 --- a/nw/config.py +++ b/nw/config.py @@ -323,6 +323,9 @@ class Config: self.showLineEndings = self._parseLine( cnfParse, cnfSec, "showlineendings", self.CNF_BOOL, self.showLineEndings ) + self.bigDocLimit = self._parseLine( + cnfParse, cnfSec, "bigdoclimit", self.CNF_INT, self.bigDocLimit + ) ## Backup cnfSec = "Backup" @@ -413,6 +416,7 @@ class Config: cnfParse.set(cnfSec,"spellcheck", str(self.spellLanguage)) cnfParse.set(cnfSec,"showtabsnspaces", str(self.showTabsNSpaces)) cnfParse.set(cnfSec,"showlineendings", str(self.showLineEndings)) + cnfParse.set(cnfSec,"bigdoclimit", str(self.bigDocLimit)) ## Backup cnfSec = "Backup" diff --git a/nw/gui/dialogs/configeditor.py b/nw/gui/dialogs/configeditor.py index 89bf6992..1932e341 100644 --- a/nw/gui/dialogs/configeditor.py +++ b/nw/gui/dialogs/configeditor.py @@ -173,11 +173,24 @@ class GuiConfigEditGeneral(QWidget): self.spellToolList.setCurrentIndex(toolIdx) self._doUpdateSpellTool(0) - self.spellLangForm.addWidget(QLabel("Provider"), 0, 0) - self.spellLangForm.addWidget(self.spellToolList, 0, 1) - self.spellLangForm.addWidget(QLabel("Language"), 1, 0) - self.spellLangForm.addWidget(self.spellLangList, 1, 1) - self.spellLangForm.setColumnStretch(2, 1) + self.spellBigDoc = QSpinBox(self) + self.spellBigDoc.setMinimum(10) + self.spellBigDoc.setMaximum(10000) + self.spellBigDoc.setSingleStep(10) + self.spellBigDoc.setToolTip(( + "Disable spell checking when loading large documents. " + "Spell checking will only run on paragraphs you edit." + )) + self.spellBigDoc.setValue(self.mainConf.bigDocLimit) + + self.spellLangForm.addWidget(QLabel("Provider"), 0, 0) + self.spellLangForm.addWidget(self.spellToolList, 0, 1, 1, 3) + self.spellLangForm.addWidget(QLabel("Language"), 1, 0) + self.spellLangForm.addWidget(self.spellLangList, 1, 1, 1, 3) + self.spellLangForm.addWidget(QLabel("Size limit"), 2, 0) + self.spellLangForm.addWidget(self.spellBigDoc, 2, 1) + self.spellLangForm.addWidget(QLabel("kb"), 2, 2) + self.spellLangForm.setColumnStretch(4, 1) # AutoSave self.autoSave = QGroupBox("Automatic Save", self) @@ -540,6 +553,7 @@ class GuiConfigEditEditor(QWidget): self.outerBox.addWidget(self.quoteStyle, 2, 1, 2, 1) self.outerBox.addWidget(self.showGuides, 4, 1) self.outerBox.setColumnStretch(2, 1) + self.outerBox.setRowStretch(5, 1) self.setLayout(self.outerBox) return From eb68f3efc014a1368971e47e78058d275d601276 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 19 Nov 2019 21:49:11 +0100 Subject: [PATCH 12/15] Change the method the document is rehighlighted. This is significantly faster --- nw/gui/dialogs/configeditor.py | 2 ++ nw/gui/elements/doceditor.py | 21 +++++++++++++++++---- nw/gui/mainmenu.py | 2 +- nw/gui/tools/dochighlight.py | 13 +++++++++++++ 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/nw/gui/dialogs/configeditor.py b/nw/gui/dialogs/configeditor.py index 1932e341..06dc5b92 100644 --- a/nw/gui/dialogs/configeditor.py +++ b/nw/gui/dialogs/configeditor.py @@ -264,6 +264,7 @@ class GuiConfigEditGeneral(QWidget): guiDark = self.guiDarkIcons.isChecked() spellTool = self.spellToolList.currentData() spellLanguage = self.spellLangList.currentData() + bigDocLimit = self.spellBigDoc.value() autoSaveDoc = self.autoSaveDoc.value() autoSaveProj = self.autoSaveProj.value() backupPath = self.projBackupPath.text() @@ -278,6 +279,7 @@ class GuiConfigEditGeneral(QWidget): self.mainConf.guiDark = guiDark self.mainConf.spellTool = spellTool self.mainConf.spellLanguage = spellLanguage + self.mainConf.bigDocLimit = bigDocLimit self.mainConf.autoSaveDoc = autoSaveDoc self.mainConf.autoSaveProj = autoSaveProj self.mainConf.backupPath = backupPath diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py index 018e9645..83706fb5 100644 --- a/nw/gui/elements/doceditor.py +++ b/nw/gui/elements/doceditor.py @@ -304,9 +304,11 @@ class GuiDocEditor(QTextEdit): # Updating root frame triggers a QTextDocument->contentsChange # signal, which we do not want as it re-runs the syntax # highlighter and spell checker, so we block it briefly. + # We then emit a signal that does not trigger re-highlighting. self.qDocument.blockSignals(True) self.qDocument.rootFrame().setFrameFormat(docFormat) self.qDocument.blockSignals(False) + self.qDocument.contentsChange.emit(0,0,0) return @@ -372,18 +374,26 @@ class GuiDocEditor(QTextEdit): self.theParent.mainMenu.setSpellCheck(theMode) self.theProject.setSpellCheck(theMode) self.hLight.setSpellCheck(theMode) - self.hLight.rehighlight() + self.reHighlightDocument() logger.verbose("Spell check is set to %s" % str(theMode)) return True - def updateSpellCheck(self): + def reHighlightDocument(self): """Rerun the highlighter to update spell checking status of the - currently loaded text. + currently loaded text. The fastest way to do this, at least as + of Qt 5.13, is to clear the text and put it back. """ + if self.spellCheck: - self.hLight.rehighlight() + theText = self.getText() + self.clear() + bfTime = time() + self.setPlainText(theText) + afTime = time() + logger.debug("Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))) + return True ## @@ -615,6 +625,9 @@ class GuiDocEditor(QTextEdit): return def _docChange(self, thePos, charsRemoved, charsAdded): + """Triggered by QTextDocument->contentsChanged. This also + triggers the syntax highlighter. + """ self.lastEdit = time() if not self.docChanged: self.setDocumentChanged(True) diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 179a6077..5966509d 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -618,7 +618,7 @@ class GuiMainMenu(QMenuBar): self.aReRunSpell = QAction("Re-Run Spell Check", self) self.aReRunSpell.setStatusTip("Run the spell checker on current document") self.aReRunSpell.setShortcut("F7") - self.aReRunSpell.triggered.connect(self.theParent.docEditor.updateSpellCheck) + self.aReRunSpell.triggered.connect(self.theParent.docEditor.reHighlightDocument) self.toolsMenu.addAction(self.aReRunSpell) # Tools > Separator diff --git a/nw/gui/tools/dochighlight.py b/nw/gui/tools/dochighlight.py index 1c8cd501..6cbd594c 100644 --- a/nw/gui/tools/dochighlight.py +++ b/nw/gui/tools/dochighlight.py @@ -201,6 +201,11 @@ class GuiDocHighlighter(QSyntaxHighlighter): ## def highlightBlock(self, theText): + """Highlight a single block. Prefer to check first character for + all formats that are defined by their initial characters. This + is significantly faster than running the regex checks we use for + text paragraphs. + """ if self.theHandle is None or not theText: return @@ -224,6 +229,10 @@ class GuiDocHighlighter(QSyntaxHighlighter): kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) self.setFormat(xPos, xLen, kwFmt) + # We never want to run the spell checker on keyword/values, + # so we force a return here + return + elif theText.startswith("# "): # Header 1 self.setFormat(0, 1, self.hStyles["header1"]) self.setFormat(1, len(theText), self.hStyles["header1h"]) @@ -277,6 +286,10 @@ class GuiDocHighlighter(QSyntaxHighlighter): ## def _makeFormat(self, fmtCol=None, fmtStyle=None, fmtSize=None): + """Generate a valid character format to be applied to the text + that is to be highlighted. + """ + theFormat = QTextCharFormat() if fmtCol is not None: From 98636a18e0900d6d6b2028bdcea5c546c5040768 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 19 Nov 2019 21:51:43 +0100 Subject: [PATCH 13/15] Fix test --- tests/reference/novelwriter.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf index a4943b1a..79fb70a7 100644 --- a/tests/reference/novelwriter.conf +++ b/tests/reference/novelwriter.conf @@ -1,5 +1,5 @@ [Main] -timestamp = 2019-11-09 14:38:32 +timestamp = 2019-11-19 21:49:29 theme = default syntax = default_light guidark = False @@ -36,6 +36,7 @@ spelltool = internal spellcheck = en showtabsnspaces = False showlineendings = False +bigdoclimit = 800 [Backup] backuppath = From d4a33aba9fe050834f711ea91f1c65680b80eeb6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 19 Nov 2019 22:04:28 +0100 Subject: [PATCH 14/15] Wrong logic ... --- nw/gui/elements/doceditor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py index 83706fb5..6fa2294f 100644 --- a/nw/gui/elements/doceditor.py +++ b/nw/gui/elements/doceditor.py @@ -225,7 +225,8 @@ class GuiDocEditor(QTextEdit): # checking. If it is too big, we switch to only check as we type self._checkDocSize(len(theDoc)) spTemp = self.hLight.spellCheck - self.hLight.spellCheck = not self.bigDoc + if self.bigDoc: + self.hLight.spellCheck = False bfTime = time() self.setPlainText(theDoc) From 8320f82c7e03323701c64d4344b6da5641f7bbce Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 19 Nov 2019 22:07:24 +0100 Subject: [PATCH 15/15] Made a mistake in header highlighting --- nw/gui/tools/dochighlight.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nw/gui/tools/dochighlight.py b/nw/gui/tools/dochighlight.py index 6cbd594c..f20aece1 100644 --- a/nw/gui/tools/dochighlight.py +++ b/nw/gui/tools/dochighlight.py @@ -234,20 +234,20 @@ class GuiDocHighlighter(QSyntaxHighlighter): return elif theText.startswith("# "): # Header 1 - self.setFormat(0, 1, self.hStyles["header1"]) - self.setFormat(1, len(theText), self.hStyles["header1h"]) + self.setFormat(0, 1, self.hStyles["header1h"]) + self.setFormat(1, len(theText), self.hStyles["header1"]) elif theText.startswith("## "): # Header 2 - self.setFormat(0, 2, self.hStyles["header2"]) - self.setFormat(2, len(theText), self.hStyles["header2h"]) + self.setFormat(0, 2, self.hStyles["header2h"]) + self.setFormat(2, len(theText), self.hStyles["header2"]) elif theText.startswith("### "): # Header 3 - self.setFormat(0, 3, self.hStyles["header3"]) - self.setFormat(3, len(theText), self.hStyles["header3h"]) + self.setFormat(0, 3, self.hStyles["header3h"]) + self.setFormat(3, len(theText), self.hStyles["header3"]) elif theText.startswith("#### "): # Header 4 - self.setFormat(0, 4, self.hStyles["header4"]) - self.setFormat(4, len(theText), self.hStyles["header4h"]) + self.setFormat(0, 4, self.hStyles["header4h"]) + self.setFormat(4, len(theText), self.hStyles["header4"]) elif theText.startswith("%"): # Comments self.setFormat(0, len(theText), self.hStyles["hidden"])