diff --git a/README.md b/README.md index af613ee4..2af3f52b 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,6 @@ of writing novels. The formatting is currently limited to: * Headings levels 1 to 4 using the `#` syntax only. * Emphasised and strongly emphasised text. These are rendered as italicised and bold text. * Strikethrough 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. diff --git a/docs/source/int_introduction.rst b/docs/source/int_introduction.rst index 900c3921..693b8dbe 100644 --- a/docs/source/int_introduction.rst +++ b/docs/source/int_introduction.rst @@ -38,8 +38,7 @@ at the same time provide a complete set of features needed for writing a novel. .. tip:: If you do need to align information in rows and columns in your notes, you can achieve this with - tabs and hard line breaks. The tab stop width can be specified in :guilabel:`Preferences` and - hard line breaks can be inserted by adding to spaces at the end of the line. + tabs and line breaks. The tab stop width can be specified in :guilabel:`Preferences`. The main window does not have a toolbar like many other applications do. This reduces clutter, and since the documents are formatted with markdown tags, is more or less redundant. However, all diff --git a/docs/source/usage_interface.rst b/docs/source/usage_interface.rst index 47038ae9..7c4f92ee 100644 --- a/docs/source/usage_interface.rst +++ b/docs/source/usage_interface.rst @@ -205,15 +205,16 @@ tricky for languages that use the same symbol for these. .. _a_ui_md: -Markdown Format -=============== +The Markdown-Like Format +======================== -The document editor uses a simplified markdown format. That is, it supports basic formatting like -emphasis (italic), strong importance (bold) and strikethrough text, as well as four levels of -headings. +The editor itself is a plaintext editor that uses formatting codes for setting meta data values and +allowing for some text formatting. The syntax is based on Markdown, but novelWriter is *not* a +Markdown editor. It supports basic formatting like emphasis (italic), strong importance (bold) +and strikethrough text, as well as four levels of headings. -Some non-standard markdown features have been added. For instance, novelWriter allows for comments, -a synopsis tag, and a set of keyword and value sets used for tags and references. +In addition to formatting codes, novelWriter allows for comments, a synopsis tag, and a set of +keyword and value sets used for tags and references. .. _a_ui_md_head: @@ -253,6 +254,33 @@ level of the novel. See :ref:`a_struct_heads` for more details. itself. See :ref:`a_struct_heads_unnum` for more details. +.. _a_ui_md_text: + +Text Paragraphs +--------------- + +A text paragraph is indicated by a blank line. That is, you need two line breaks to separate two +fragments of text into two paragraphs. Single line breaks are treated as line breaks within a +paragraph. + +In addition, the editor supports a few additional types of whitespaces. + +* A non-breaking space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Space`. +* Thin spaces are also supported, and can be inserted with :kbd:`Ctrl`:kbd:`K`, + :kbd:`Shift`:kbd:`Space`. +* Non-breaking thin space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`. + +These are all insert features, and the :guilabel:`Insert` menu has more. They are also listed +in :ref:`a_ui_shortcuts_ins`. + +Non-breaking spaces are highlighted by the syntax highlighter with an alternate coloured +background, depending on the selected theme. + +.. tip:: + Non-breaking spaces are the correct type of space to separate a number from its unit. Generally, + it prevents the line wrapping algorithms from adding line breaks where it shouldn't. + + .. _a_ui_md_emph: Text Emphasis @@ -333,34 +361,6 @@ The available tag and reference keywords are listed in the :ref:`a_struct_tags` also be inserted at the cursor position in the editor via the :guilabel:`Insert` menu. -.. _a_ui_md_add: - -Additional Markdown and Non-Standard Features ---------------------------------------------- - -The editor and viewer also support markdown standard hard line breaks, and preserve non-breaking -spaces if running with Qt 5.9 or higher. For older versions, the non-breaking spaces are lost when -the document is saved. This is unfortunately hard-coded into the Qt text editor. - -* A hard line break can be achieved by leaving two or more spaces at the end of the line. This is - standard markdown syntax. Alternatively, the user can press :kbd:`Ctrl`:kbd:`K`, :kbd:`Return` to - insert this type of line break. -* A non-breaking space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Space`. -* Thin spaces are also supported, and can be inserted with :kbd:`Ctrl`:kbd:`K`, - :kbd:`Shift`:kbd:`Space`. -* Non-breaking thin space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`. - -These are all insert features, and the :guilabel:`Insert` menu has more. They are also listed -in :ref:`a_ui_shortcuts_ins`. - -Both hard line breaks and non-breaking spaces are highlighted by the syntax highlighter as an -alternate coloured background, depending on the selected theme. - -.. tip:: - Non-breaking spaces are the correct type of space to separate a number from its unit. Generally, - it prevents the line wrapping algorithms from adding line breaks where it shouldn't. - - .. _a_ui_outline: Project Outline View @@ -514,7 +514,6 @@ a key or key combination for the inserted content. ":kbd:`Ctrl`:kbd:`K`, :kbd:`.`", "Insert an ellipsis." ":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`'`", "Insert a prime." ":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`""`", "Insert a double prime." - ":kbd:`Ctrl`:kbd:`K`, :kbd:`Return`", "Insert a hard line break." ":kbd:`Ctrl`:kbd:`K`, :kbd:`Space`", "Insert a non-breaking space." ":kbd:`Ctrl`:kbd:`K`, :kbd:`Shift`:kbd:`Space`", "Insert a thin space." ":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`", "Insert a thin non-breaking space." diff --git a/nw/assets/themes/syntax/default_dark.conf b/nw/assets/themes/syntax/default_dark.conf index 09b292ee..f015e5d6 100644 --- a/nw/assets/themes/syntax/default_dark.conf +++ b/nw/assets/themes/syntax/default_dark.conf @@ -20,6 +20,6 @@ hidden = 150, 150, 150 keyword = 200, 46, 0 value = 184, 200, 0 spellcheckline = 200, 46, 0 -tagerror = 46, 200, 0 +errorline = 46, 200, 0 replacetag = 0, 184, 46 modifier = 200, 120, 0 diff --git a/nw/assets/themes/syntax/default_light.conf b/nw/assets/themes/syntax/default_light.conf index 908fd7cb..319fbda3 100644 --- a/nw/assets/themes/syntax/default_light.conf +++ b/nw/assets/themes/syntax/default_light.conf @@ -20,6 +20,6 @@ hidden = 100, 100, 100 keyword = 200, 50, 50 value = 50, 150, 50 spellcheckline = 200, 0, 0 -tagerror = 0, 150, 0 +errorline = 0, 150, 0 replacetag = 0, 150, 0 modifier = 150, 110, 30 diff --git a/nw/assets/themes/syntax/grey_dark.conf b/nw/assets/themes/syntax/grey_dark.conf index a6bc0420..e579e8da 100644 --- a/nw/assets/themes/syntax/grey_dark.conf +++ b/nw/assets/themes/syntax/grey_dark.conf @@ -20,6 +20,6 @@ hidden = 150, 150, 150 keyword = 225, 225, 225 value = 200, 200, 200 spellcheckline = 200, 46, 0 -tagerror = 46, 200, 0 +errorline = 46, 200, 0 replacetag = 225, 225, 225 modifier = 225, 225, 225 diff --git a/nw/assets/themes/syntax/grey_light.conf b/nw/assets/themes/syntax/grey_light.conf index a44593b4..32f9561c 100644 --- a/nw/assets/themes/syntax/grey_light.conf +++ b/nw/assets/themes/syntax/grey_light.conf @@ -20,6 +20,6 @@ hidden = 100, 100, 100 keyword = 0, 0, 0 value = 20, 20, 20 spellcheckline = 200, 0, 0 -tagerror = 0, 150, 0 +errorline = 0, 150, 0 replacetag = 0, 0, 0 modifier = 0, 0, 0 diff --git a/nw/assets/themes/syntax/light_owl.conf b/nw/assets/themes/syntax/light_owl.conf index fde36665..fdd0dc67 100644 --- a/nw/assets/themes/syntax/light_owl.conf +++ b/nw/assets/themes/syntax/light_owl.conf @@ -40,6 +40,6 @@ hidden = 152, 159, 177 keyword = 222, 61, 58 value = 150, 74, 193 spellcheckline = 222, 61, 58 -tagerror = 8, 145, 106 +errorline = 8, 145, 106 replacetag = 42, 162, 152 modifier = 224, 175, 5 diff --git a/nw/assets/themes/syntax/night_owl.conf b/nw/assets/themes/syntax/night_owl.conf index 56cebf53..59ad10da 100644 --- a/nw/assets/themes/syntax/night_owl.conf +++ b/nw/assets/themes/syntax/night_owl.conf @@ -40,6 +40,6 @@ hidden = 99, 119, 119 keyword = 247, 140, 108 value = 199, 146, 234 spellcheckline = 247, 140, 108 -tagerror = 173, 219, 103 +errorline = 173, 219, 103 replacetag = 127, 219, 202 modifier = 236, 196, 141 diff --git a/nw/assets/themes/syntax/solarized_dark.conf b/nw/assets/themes/syntax/solarized_dark.conf index 965430bb..ec8a3d8d 100644 --- a/nw/assets/themes/syntax/solarized_dark.conf +++ b/nw/assets/themes/syntax/solarized_dark.conf @@ -20,6 +20,6 @@ hidden = 147, 161, 161 keyword = 133, 153, 0 value = 203, 75, 22 spellcheckline = 203, 75, 22 -tagerror = 220, 50, 47 +errorline = 220, 50, 47 replacetag = 133, 153, 0 modifier = 181, 137, 0 diff --git a/nw/assets/themes/syntax/solarized_light.conf b/nw/assets/themes/syntax/solarized_light.conf index 2d1f6d1c..319a084c 100644 --- a/nw/assets/themes/syntax/solarized_light.conf +++ b/nw/assets/themes/syntax/solarized_light.conf @@ -20,6 +20,6 @@ hidden = 88, 110, 117 keyword = 133, 153, 0 value = 203, 75, 22 spellcheckline = 203, 75, 22 -tagerror = 220, 50, 47 +errorline = 220, 50, 47 replacetag = 133, 153, 0 modifier = 181, 137, 0 diff --git a/nw/assets/themes/syntax/tomorrow.conf b/nw/assets/themes/syntax/tomorrow.conf index 85fe84ea..6333f06e 100644 --- a/nw/assets/themes/syntax/tomorrow.conf +++ b/nw/assets/themes/syntax/tomorrow.conf @@ -40,6 +40,6 @@ hidden = 142, 144, 140 keyword = 240, 40, 41 value = 137, 89, 168 spellcheckline = 240, 40, 41 -tagerror = 113, 140, 0 +errorline = 113, 140, 0 replacetag = 62, 153, 159 modifier = 245, 135, 31 diff --git a/nw/assets/themes/syntax/tomorrow_night.conf b/nw/assets/themes/syntax/tomorrow_night.conf index be8f2d4d..2a4e8751 100644 --- a/nw/assets/themes/syntax/tomorrow_night.conf +++ b/nw/assets/themes/syntax/tomorrow_night.conf @@ -40,6 +40,6 @@ hidden = 150, 152, 150 keyword = 204, 102, 102 value = 178, 148, 187 spellcheckline = 204, 102, 102 -tagerror = 181, 189, 104 +errorline = 181, 189, 104 replacetag = 138, 190, 183 modifier = 222, 147, 95 diff --git a/nw/assets/themes/syntax/tomorrow_night_blue.conf b/nw/assets/themes/syntax/tomorrow_night_blue.conf index 3738051f..1a8459bf 100644 --- a/nw/assets/themes/syntax/tomorrow_night_blue.conf +++ b/nw/assets/themes/syntax/tomorrow_night_blue.conf @@ -40,6 +40,6 @@ hidden = 114, 133, 183 keyword = 255, 157, 164 value = 235, 187, 255 spellcheckline = 255, 157, 164 -tagerror = 209, 241, 169 +errorline = 209, 241, 169 replacetag = 153, 255, 255 modifier = 255, 197, 143 diff --git a/nw/assets/themes/syntax/tomorrow_night_bright.conf b/nw/assets/themes/syntax/tomorrow_night_bright.conf index 9ce48b6a..c5bccc99 100644 --- a/nw/assets/themes/syntax/tomorrow_night_bright.conf +++ b/nw/assets/themes/syntax/tomorrow_night_bright.conf @@ -40,6 +40,6 @@ hidden = 150, 152, 150 keyword = 213, 78, 83 value = 195, 151, 216 spellcheckline = 213, 78, 83 -tagerror = 185, 202, 74 +errorline = 185, 202, 74 replacetag = 112, 192, 177 modifier = 231, 140, 69 diff --git a/nw/assets/themes/syntax/tomorrow_night_eighties.conf b/nw/assets/themes/syntax/tomorrow_night_eighties.conf index 9d611cd3..c7314f17 100644 --- a/nw/assets/themes/syntax/tomorrow_night_eighties.conf +++ b/nw/assets/themes/syntax/tomorrow_night_eighties.conf @@ -40,6 +40,6 @@ hidden = 153, 153, 153 keyword = 242, 119, 122 value = 204, 153, 204 spellcheckline = 242, 119, 122 -tagerror = 153, 204, 153 +errorline = 153, 204, 153 replacetag = 102, 204, 204 modifier = 249, 145, 57 diff --git a/nw/config.py b/nw/config.py index 65436608..16647abd 100644 --- a/nw/config.py +++ b/nw/config.py @@ -138,6 +138,7 @@ class Config: self.doJustify = False # Justify text self.showTabsNSpaces = False # Show tabs and spaces in edior self.showLineEndings = False # Show line endings in editor + self.showMultiSpaces = True # Highlight multiple spaces in the text self.doReplace = True # Enable auto-replace as you type self.doReplaceSQuote = True # Smart single quotes @@ -602,6 +603,9 @@ class Config: self.showLineEndings = self._parseLine( cnfParse, cnfSec, "showlineendings", self.CNF_BOOL, self.showLineEndings ) + self.showMultiSpaces = self._parseLine( + cnfParse, cnfSec, "showmultispaces", self.CNF_BOOL, self.showMultiSpaces + ) self.bigDocLimit = self._parseLine( cnfParse, cnfSec, "bigdoclimit", self.CNF_INT, self.bigDocLimit ) @@ -765,6 +769,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, "showmultispaces", str(self.showMultiSpaces)) cnfParse.set(cnfSec, "bigdoclimit", str(self.bigDocLimit)) cnfParse.set(cnfSec, "showfullpath", str(self.showFullPath)) cnfParse.set(cnfSec, "highlightquotes", str(self.highlightQuotes)) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 180c2da8..5586575d 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -151,7 +151,6 @@ class ToHtml(Tokenizer): thisPar = [] parStyle = None tmpResult = [] - hasHardBreak = False for tType, tLine, tText, tFormat, tStyle in self.theTokens: @@ -196,16 +195,15 @@ class ToHtml(Tokenizer): if tType == self.T_EMPTY: if parStyle is None: parStyle = "" - if hasHardBreak and self.cssStyles: + if len(thisPar) > 1 and self.cssStyles: parClass = " class='break'" else: parClass = "" if len(thisPar) > 0: - tTemp = "".join(thisPar) + tTemp = "
".join(thisPar) tmpResult.append("%s

\n" % (parStyle, parClass, tTemp.rstrip())) thisPar = [] parStyle = None - hasHardBreak = False elif tType == self.T_TITLE: tHead = tText.replace(r"\\", "
") @@ -239,11 +237,7 @@ class ToHtml(Tokenizer): parStyle = hStyle for xPos, xLen, xFmt in reversed(tFormat): tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:] - if tText.endswith(" "): - thisPar.append(tTemp.rstrip() + "
") - hasHardBreak = True - else: - thisPar.append(tTemp.rstrip() + " ") + thisPar.append(tTemp.rstrip()) elif tType == self.T_SYNOPSIS and self.doSynopsis: tmpResult.append(self._formatSynopsis(tText)) diff --git a/nw/core/tomd.py b/nw/core/tomd.py index 684a23fb..d95a6810 100644 --- a/nw/core/tomd.py +++ b/nw/core/tomd.py @@ -89,12 +89,12 @@ class ToMarkdown(Tokenizer): thisPar = [] tmpResult = [] - for tType, tLine, tText, tFormat, tStyle in self.theTokens: + for tType, _, tText, tFormat, tStyle in self.theTokens: # Process Text Type if tType == self.T_EMPTY: if len(thisPar) > 0: - tTemp = "".join(thisPar) + tTemp = " \n".join(thisPar) tmpResult.append("%s\n\n" % tTemp.rstrip(" ")) thisPar = [] @@ -128,10 +128,7 @@ class ToMarkdown(Tokenizer): tTemp = tText for xPos, xLen, xFmt in reversed(tFormat): tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:] - if tText.endswith(" "): - thisPar.append(tTemp.rstrip() + " \n") - else: - thisPar.append(tTemp.rstrip() + " ") + thisPar.append(tTemp.rstrip()) elif tType == self.T_SYNOPSIS and self.doSynopsis: tmpResult.append("**%s:** %s\n\n" % (self._localLookup("Synopsis"), tText)) diff --git a/nw/core/toodt.py b/nw/core/toodt.py index ce67e8c0..f5770c9f 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -327,8 +327,7 @@ class ToOdt(Tokenizer): thisPar = [] thisFmt = [] parStyle = None - hasHardBreak = False - for tType, tLine, tText, tFormat, tStyle in self.theTokens: + for tType, _, tText, tFormat, tStyle in self.theTokens: # Styles oStyle = ODTParagraphStyle() @@ -359,13 +358,13 @@ class ToOdt(Tokenizer): # Process Text Types if tType == self.T_EMPTY: - if hasHardBreak and parStyle is not None: + if len(thisPar) > 1 and parStyle is not None: if self.doJustify: parStyle.setTextAlign("left") if len(thisPar) > 0: - tTemp = "".join(thisPar) - fTemp = "".join(thisFmt) + tTemp = "\n".join(thisPar) + fTemp = " ".join(thisFmt) tTxt = tTemp.rstrip() tFmt = fTemp[:len(tTxt)] self._addTextPar("Text_Body", parStyle, tTxt, theFmt=tFmt) @@ -373,7 +372,6 @@ class ToOdt(Tokenizer): thisPar = [] thisFmt = [] parStyle = None - hasHardBreak = False elif tType == self.T_TITLE: tHead = tText.replace(r"\\", "\n") @@ -402,23 +400,17 @@ class ToOdt(Tokenizer): self._addTextPar("Text_Body", oStyle, "") elif tType == self.T_TEXT: - tTemp = tText if parStyle is None: parStyle = oStyle - tFmt = " "*len(tTemp) + tFmt = " "*len(tText) for xPos, xLen, xFmt in tFormat: tFmt = tFmt[:xPos] + odtTags[xFmt] + tFmt[xPos+xLen:] - tTxt = tTemp.rstrip() + tTxt = tText.rstrip() tFmt = tFmt[:len(tTxt)] - if tText.endswith(" "): - thisPar.append(tTxt + "\n") - thisFmt.append(tFmt + " ") - hasHardBreak = True - else: - thisPar.append(tTxt + " ") - thisFmt.append(tFmt + " ") + thisPar.append(tTxt) + thisFmt.append(tFmt) elif tType == self.T_SYNOPSIS and self.doSynopsis: tTemp, fTemp = self._formatSynopsis(tText) diff --git a/nw/dialogs/preferences.py b/nw/dialogs/preferences.py index 5ed23996..2680dc64 100644 --- a/nw/dialogs/preferences.py +++ b/nw/dialogs/preferences.py @@ -924,6 +924,19 @@ class GuiPreferencesSyntax(QWidget): self.tr("Applies to emphasis (italic) and strong (bold).") ) + # Text Errors + # =========== + + self.mainForm.addGroupLabel(self.tr("Text Errors")) + + self.showMultiSpaces = QSwitch() + self.showMultiSpaces.setChecked(self.mainConf.showMultiSpaces) + self.mainForm.addRow( + self.tr("Mark redundant spaces"), + self.showMultiSpaces, + self.tr("Trailing spaces or multiple spaces between words.") + ) + return def saveValues(self): @@ -940,6 +953,9 @@ class GuiPreferencesSyntax(QWidget): # Text Emphasis self.mainConf.highlightEmph = self.highlightEmph.isChecked() + # Text Errors + self.mainConf.showMultiSpaces = self.showMultiSpaces.isChecked() + self.mainConf.confChanged = True return diff --git a/nw/enum.py b/nw/enum.py index 7b3c8aaa..830e8953 100644 --- a/nw/enum.py +++ b/nw/enum.py @@ -89,17 +89,17 @@ class nwDocAction(Enum): BLOCK_TXT = 18 REPL_SNG = 19 REPL_DBL = 20 + RM_BREAKS = 21 # END Enum nwDocAction class nwDocInsert(Enum): NO_INSERT = 0 - HARD_BREAK = 1 - QUOTE_LS = 2 - QUOTE_RS = 3 - QUOTE_LD = 4 - QUOTE_RD = 5 + QUOTE_LS = 1 + QUOTE_RS = 2 + QUOTE_LD = 3 + QUOTE_RD = 4 # END Enum nwDocInsert diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 9095dea0..a6cc404e 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -791,6 +791,8 @@ class GuiDocEditor(QTextEdit): self._replaceQuotes("'", self._typSQOpen, self._typSQClose) elif theAction == nwDocAction.REPL_DBL: self._replaceQuotes("\"", self._typDQOpen, self._typDQClose) + elif theAction == nwDocAction.RM_BREAKS: + self._removeInParLineBreaks() else: logger.debug("Unknown or unsupported document action %s" % str(theAction)) self._allowAutoReplace(True) @@ -840,9 +842,7 @@ class GuiDocEditor(QTextEdit): if isinstance(theInsert, str): theText = theInsert elif isinstance(theInsert, nwDocInsert): - if theInsert == nwDocInsert.HARD_BREAK: - theText = " \n" - elif theInsert == nwDocInsert.QUOTE_LS: + if theInsert == nwDocInsert.QUOTE_LS: theText = self._typSQOpen elif theInsert == nwDocInsert.QUOTE_RS: theText = self._typSQClose @@ -1787,6 +1787,54 @@ class GuiDocEditor(QTextEdit): return True + def _removeInParLineBreaks(self): + """Strip line breaks within paragraphs in the selected text. + """ + theCursor = self.textCursor() + theDoc = self.document() + + iS = 0 + iE = theDoc.blockCount() - 1 + rS = 0 + rE = theDoc.characterCount() + if theCursor.hasSelection(): + sBlock = theDoc.findBlock(theCursor.selectionStart()) + eBlock = theDoc.findBlock(theCursor.selectionEnd()) + iS = sBlock.blockNumber() + iE = eBlock.blockNumber() + rS = sBlock.position() + rE = eBlock.position() + eBlock.length() + + # Clean up the text + currPar = [] + cleanText = "" + for i in range(iS, iE+1): + cBlock = theDoc.findBlockByNumber(i) + cText = cBlock.text() + if cText.strip() == "": + if currPar: + cleanText += " ".join(currPar) + "\n\n" + else: + cleanText += "\n" + currPar = [] + elif cText.startswith(("# ", "## ", "### ", "#### ", "@", "%")): + cleanText += cText + "\n" + else: + currPar.append(cText) + + if currPar: + cleanText += " ".join(currPar) + "\n\n" + + # Replace the text with the cleaned up text + theCursor.beginEditBlock() + theCursor.clearSelection() + theCursor.setPosition(rS) + theCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, rE-rS) + theCursor.insertText(cleanText.rstrip() + "\n") + theCursor.endEditBlock() + + return True + def _makeSelection(self, selMode): """Wrapper function to select text based on a selection mode. """ diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index ea9df173..ceeacf45 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -71,7 +71,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.colKey = QColor(0, 0, 0) self.colVal = QColor(0, 0, 0) self.colSpell = QColor(0, 0, 0) - self.colTagErr = QColor(0, 0, 0) + self.colError = QColor(0, 0, 0) self.colRepTag = QColor(0, 0, 0) self.initHighlighter() @@ -95,11 +95,11 @@ 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.colError = QColor(*self.theTheme.colError) self.colRepTag = QColor(*self.theTheme.colRepTag) self.colMod = QColor(*self.theTheme.colMod) - self.colTrail = QColor(*self.theTheme.colEmph) - self.colTrail.setAlpha(64) + self.colBreak = QColor(*self.theTheme.colEmph) + self.colBreak.setAlpha(64) self.colEmph = None if self.mainConf.highlightEmph: @@ -117,8 +117,8 @@ class GuiDocHighlighter(QSyntaxHighlighter): "bold" : self._makeFormat(self.colEmph, "bold"), "italic" : self._makeFormat(self.colEmph, "italic"), "strike" : self._makeFormat(self.colHidden, "strike"), - "trailing" : self._makeFormat(self.colTrail, "background"), - "nobreak" : self._makeFormat(self.colTrail, "background"), + "mspaces" : self._makeFormat(self.colError, "errline"), + "nobreak" : self._makeFormat(self.colBreak, "background"), "dialogue1" : self._makeFormat(self.colDialN), "dialogue2" : self._makeFormat(self.colDialD), "dialogue3" : self._makeFormat(self.colDialS), @@ -131,12 +131,13 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.hRules = [] - # Trailing Spaces, 2+ - self.hRules.append(( - r"[ ]{2,}$", { - 0 : self.hStyles["trailing"], - } - )) + # Multiple or Trailing Spaces + if self.mainConf.showMultiSpaces: + self.hRules.append(( + r"[ ]{2,}|[ ]*$", { + 0 : self.hStyles["mspaces"], + } + )) # Non-Breaking Spaces self.hRules.append(( @@ -174,7 +175,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): } )) - # Markdown + # Markdown Syntax self.hRules.append(( nwRegEx.FMT_EI, { 1 : self.hStyles["hidden"], @@ -296,7 +297,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.setFormat(xPos, xLen, self.hStyles["value"]) else: kwFmt = self.format(xPos) - kwFmt.setUnderlineColor(self.colTagErr) + kwFmt.setUnderlineColor(self.colError) kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) self.setFormat(xPos, xLen, kwFmt) @@ -391,6 +392,9 @@ class GuiDocHighlighter(QSyntaxHighlighter): theFormat.setFontItalic(True) if "strike" in fmtStyle: theFormat.setFontStrikeOut(True) + if "errline" in fmtStyle: + theFormat.setUnderlineColor(self.colError) + theFormat.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) if "underline" in fmtStyle: theFormat.setFontUnderline(True) if "background" in fmtStyle: diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 402e40a9..d259c6cd 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -634,15 +634,8 @@ class GuiMainMenu(QMenuBar): self.aInsDPrime.triggered.connect(lambda: self._docInsert(nwUnicode.U_DPRIME)) self.mInsPunct.addAction(self.aInsDPrime) - # Insert > Breaks and Spaces - self.mInsBreaks = self.insertMenu.addMenu(self.tr("Breaks and Spaces")) - - # Insert > Hard Line Break - self.aInsHardBreak = QAction(self.tr("Hard Line Break"), self) - self.aInsHardBreak.setStatusTip(self.tr("Insert a hard line break")) - self.aInsHardBreak.setShortcut("Ctrl+K, Return") - self.aInsHardBreak.triggered.connect(lambda: self._docInsert(nwDocInsert.HARD_BREAK)) - self.mInsBreaks.addAction(self.aInsHardBreak) + # Insert > White Spaces + self.mInsBreaks = self.insertMenu.addMenu(self.tr("White Spaces")) # Insert > Non-Breaking Space self.aInsNBSpace = QAction(self.tr("Non-Breaking Space"), self) @@ -750,60 +743,6 @@ class GuiMainMenu(QMenuBar): return - def _buildSearchMenu(self): - """Assemble the Search menu. - """ - # Search - self.srcMenu = self.addMenu(self.tr("&Search")) - - # Search > Find - self.aFind = QAction(self.tr("Find"), self) - self.aFind.setStatusTip(self.tr("Find text in document")) - self.aFind.setShortcut("Ctrl+F") - self.aFind.triggered.connect(lambda: self.theParent.docEditor.beginSearch()) - self.srcMenu.addAction(self.aFind) - - # Search > Replace - self.aReplace = QAction(self.tr("Replace"), self) - self.aReplace.setStatusTip(self.tr("Replace text in document")) - if self.mainConf.osDarwin: - self.aReplace.setShortcut("Ctrl+=") - else: - self.aReplace.setShortcut("Ctrl+H") - self.aReplace.triggered.connect(lambda: self.theParent.docEditor.beginReplace()) - self.srcMenu.addAction(self.aReplace) - - # Search > Find Next - self.aFindNext = QAction(self.tr("Find Next"), self) - self.aFindNext.setStatusTip(self.tr("Find next occurrence of text in document")) - if self.mainConf.osDarwin: - self.aFindNext.setShortcuts(["Ctrl+G", "F3"]) - else: - self.aFindNext.setShortcuts(["F3", "Ctrl+G"]) - self.aFindNext.triggered.connect(lambda: self.theParent.docEditor.findNext()) - self.srcMenu.addAction(self.aFindNext) - - # Search > Find Prev - self.aFindPrev = QAction(self.tr("Find Previous"), self) - self.aFindPrev.setStatusTip(self.tr("Find previous occurrence of text in document")) - if self.mainConf.osDarwin: - self.aFindPrev.setShortcuts(["Ctrl+Shift+G", "Shift+F3"]) - else: - self.aFindPrev.setShortcuts(["Shift+F3", "Ctrl+Shift+G"]) - self.aFindPrev.triggered.connect(lambda: self.theParent.docEditor.findNext(goBack=True)) - self.srcMenu.addAction(self.aFindPrev) - - # Search > Replace Next - self.aReplaceNext = QAction(self.tr("Replace Next"), self) - self.aReplaceNext.setStatusTip( - self.tr("Find and replace next occurrence of text in document") - ) - self.aReplaceNext.setShortcut("Ctrl+Shift+1") - self.aReplaceNext.triggered.connect(lambda: self.theParent.docEditor.replaceNext()) - self.srcMenu.addAction(self.aReplaceNext) - - return - def _buildFormatMenu(self): """Assemble the Format menu. """ @@ -912,6 +851,68 @@ class GuiMainMenu(QMenuBar): self.aFmtReplDbl.triggered.connect(lambda: self._docAction(nwDocAction.REPL_DBL)) self.fmtMenu.addAction(self.aFmtReplDbl) + # Format > Remove In-Paragraph Breaks + self.aFmtRmBreaks = QAction(self.tr("Remove In-Paragraph Breaks"), self) + self.aFmtRmBreaks.setStatusTip( + self.tr("Removes all line breaks within paragraphs in the selected text") + ) + self.aFmtRmBreaks.triggered.connect(lambda: self._docAction(nwDocAction.RM_BREAKS)) + self.fmtMenu.addAction(self.aFmtRmBreaks) + + return + + def _buildSearchMenu(self): + """Assemble the Search menu. + """ + # Search + self.srcMenu = self.addMenu(self.tr("&Search")) + + # Search > Find + self.aFind = QAction(self.tr("Find"), self) + self.aFind.setStatusTip(self.tr("Find text in document")) + self.aFind.setShortcut("Ctrl+F") + self.aFind.triggered.connect(lambda: self.theParent.docEditor.beginSearch()) + self.srcMenu.addAction(self.aFind) + + # Search > Replace + self.aReplace = QAction(self.tr("Replace"), self) + self.aReplace.setStatusTip(self.tr("Replace text in document")) + if self.mainConf.osDarwin: + self.aReplace.setShortcut("Ctrl+=") + else: + self.aReplace.setShortcut("Ctrl+H") + self.aReplace.triggered.connect(lambda: self.theParent.docEditor.beginReplace()) + self.srcMenu.addAction(self.aReplace) + + # Search > Find Next + self.aFindNext = QAction(self.tr("Find Next"), self) + self.aFindNext.setStatusTip(self.tr("Find next occurrence of text in document")) + if self.mainConf.osDarwin: + self.aFindNext.setShortcuts(["Ctrl+G", "F3"]) + else: + self.aFindNext.setShortcuts(["F3", "Ctrl+G"]) + self.aFindNext.triggered.connect(lambda: self.theParent.docEditor.findNext()) + self.srcMenu.addAction(self.aFindNext) + + # Search > Find Prev + self.aFindPrev = QAction(self.tr("Find Previous"), self) + self.aFindPrev.setStatusTip(self.tr("Find previous occurrence of text in document")) + if self.mainConf.osDarwin: + self.aFindPrev.setShortcuts(["Ctrl+Shift+G", "Shift+F3"]) + else: + self.aFindPrev.setShortcuts(["Shift+F3", "Ctrl+Shift+G"]) + self.aFindPrev.triggered.connect(lambda: self.theParent.docEditor.findNext(goBack=True)) + self.srcMenu.addAction(self.aFindPrev) + + # Search > Replace Next + self.aReplaceNext = QAction(self.tr("Replace Next"), self) + self.aReplaceNext.setStatusTip( + self.tr("Find and replace next occurrence of text in document") + ) + self.aReplaceNext.setShortcut("Ctrl+Shift+1") + self.aReplaceNext.triggered.connect(lambda: self.theParent.docEditor.replaceNext()) + self.srcMenu.addAction(self.aReplaceNext) + return def _buildToolsMenu(self): diff --git a/nw/gui/theme.py b/nw/gui/theme.py index cec8ee4e..32d5f0dc 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -106,7 +106,7 @@ class GuiTheme: self.colKey = [0, 0, 0] self.colVal = [0, 0, 0] self.colSpell = [0, 0, 0] - self.colTagErr = [0, 0, 0] + self.colError = [0, 0, 0] self.colRepTag = [0, 0, 0] self.colMod = [0, 0, 0] @@ -373,7 +373,7 @@ class GuiTheme: 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") + self.colError = self._loadColour(confParser, cnfSec, "errorline") self.colRepTag = self._loadColour(confParser, cnfSec, "replacetag") self.colMod = self._loadColour(confParser, cnfSec, "modifier") diff --git a/nw/guimain.py b/nw/guimain.py index e0f5c42b..2db0add2 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -1318,7 +1318,6 @@ class GuiMain(QMainWindow): self.addAction(self.mainMenu.aInsEllipsis) self.addAction(self.mainMenu.aInsPrime) self.addAction(self.mainMenu.aInsDPrime) - self.addAction(self.mainMenu.aInsHardBreak) self.addAction(self.mainMenu.aInsNBSpace) self.addAction(self.mainMenu.aInsThinSpace) self.addAction(self.mainMenu.aInsThinNBSpace) diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf index cfbf4e78..1f8ba2d7 100644 --- a/tests/reference/baseConfig_novelwriter.conf +++ b/tests/reference/baseConfig_novelwriter.conf @@ -55,6 +55,7 @@ spelltool = internal spellcheck = en showtabsnspaces = False showlineendings = False +showmultispaces = True bigdoclimit = 800 showfullpath = True highlightquotes = True diff --git a/tests/reference/guiPreferences_novelwriter.conf b/tests/reference/guiPreferences_novelwriter.conf index 80e77d34..1be38593 100644 --- a/tests/reference/guiPreferences_novelwriter.conf +++ b/tests/reference/guiPreferences_novelwriter.conf @@ -55,6 +55,7 @@ spelltool = internal spellcheck = en showtabsnspaces = True showlineendings = True +showmultispaces = True bigdoclimit = 500 showfullpath = False highlightquotes = False diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index 955963e7..4aacf491 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -93,7 +93,8 @@ def testCoreToOdt_Convert(dummyGUI): 'bold' ' and ' 'italics' - ' text text. No format' + ' text text.' + 'No format' '' ) diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 2afa6043..af28535c 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -40,7 +40,7 @@ typeDelay = 1 stepDelay = 20 @pytest.mark.gui -def testGuiEditor_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, refDir, outDir): +def testGuiEditor_Main(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir): """Test the document editor. """ # Block message box diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 9b9af54d..3da0d48c 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -236,6 +236,47 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, nwLipsum): "Also text with “double” quotes which are “less tricky”.\n\n" ) + # Remove in-paragraph line breaks + nwGUI.docEditor.setText(( + "### New Text\n\n" + "@char: Someone\n" + "@location: Somewhere\n\n" + "% Some comment ...\n\n" + "Here is some text\non multiple\nlines.\n\n" + "With another paragraph\nhere." + )) + nwGUI.mainMenu.aFmtRmBreaks.activate(QAction.Trigger) + assert nwGUI.docEditor.getText() == ( + "### New Text\n\n" + "@char: Someone\n" + "@location: Somewhere\n\n" + "% Some comment ...\n\n" + "Here is some text on multiple lines.\n\n" + "With another paragraph here.\n" + ) + + nwGUI.docEditor.setText(( + "### New Text\n\n" + "@char: Someone\n" + "@location: Somewhere\n\n" + "% Some comment ...\n\n" + "Here is some text\non multiple\nlines.\n\n" + "With another paragraph\nhere." + )) + theCursor = nwGUI.docEditor.textCursor() + theCursor.setPosition(74) + theCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, 29) + nwGUI.docEditor.setTextCursor(theCursor) + nwGUI.mainMenu.aFmtRmBreaks.activate(QAction.Trigger) + assert nwGUI.docEditor.getText() == ( + "### New Text\n\n" + "@char: Someone\n" + "@location: Somewhere\n\n" + "% Some comment ...\n\n" + "Here is some text on multiple lines.\n\n" + "With another paragraph\nhere." + ) + # Test Invalid Document Action assert not nwGUI.docEditor.docAction(nwDocAction.NO_ACTION) @@ -467,10 +508,6 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj): assert nwGUI.docEditor.getText() == nwUnicode.U_DIVIDE nwGUI.docEditor.clear() - nwGUI.mainMenu.aInsHardBreak.activate(QAction.Trigger) - assert nwGUI.docEditor.getText() == " \n" - nwGUI.docEditor.clear() - nwGUI.mainMenu.aInsNBSpace.activate(QAction.Trigger) if nwGUI.mainConf.verQtValue >= 50900: assert nwGUI.docEditor.getText() == nwUnicode.U_NBSP diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 0e6b1e34..cb9c993b 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -109,7 +109,7 @@ def testGuiTheme_Main(qtbot, monkeypatch, nwMinimal, tmpDir): assert nwGUI.theTheme.colKey == [242, 119, 122] assert nwGUI.theTheme.colVal == [204, 153, 204] assert nwGUI.theTheme.colSpell == [242, 119, 122] - assert nwGUI.theTheme.colTagErr == [153, 204, 153] + assert nwGUI.theTheme.colError == [153, 204, 153] assert nwGUI.theTheme.colRepTag == [102, 204, 204] assert nwGUI.theTheme.colMod == [249, 145, 57]