diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 1ff5da4f..e4e6af98 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -229,10 +229,12 @@ class nwUnicode: U_EMDASH = "\u2014" # Long dash U_HELLIP = "\u2026" # Ellipsis - ## Spaces + ## Spaces and Lines U_NBSP = "\u00a0" # Non-breaking space U_THNSP = "\u2009" # Thin space U_THNBSP = "\u202f" # Thin non-breaking space + U_LSEP = "\u2028" # Line separator + U_PSEP = "\u2029" # Paragraph separator ## Symbols U_CHECK = "\u2714" # Heavy check mark diff --git a/nw/constants/enum.py b/nw/constants/enum.py index 84f8d4f5..7177223c 100644 --- a/nw/constants/enum.py +++ b/nw/constants/enum.py @@ -93,6 +93,8 @@ class nwDocAction(Enum): BLOCK_H4 = 22 BLOCK_COM = 23 BLOCK_TXT = 24 + REPL_SNG = 25 + REPL_DBL = 26 # END Enum nwDocAction diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 35ac09d8..3af27d07 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -45,11 +45,12 @@ from PyQt5.QtWidgets import ( QFrame ) -from nw.core import NWDoc +from nw.core import NWDoc, NWSpellSimple, countWords from nw.gui.dochighlight import GuiDocHighlighter -from nw.core import NWSpellSimple, countWords -from nw.constants import nwUnicode, nwDocAction, nwDocInsert, nwInsertSymbols from nw.common import transferCase +from nw.constants import ( + nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwInsertSymbols +) logger = logging.getLogger(__name__) @@ -76,11 +77,9 @@ class GuiDocEditor(QTextEdit): self.paraCount = 0 self.lastEdit = 0 self.bigDoc = False + self.doReplace = False self.nonWord = "\"'" - # Document State - self.hasSelection = False - # Typography self.typDQOpen = self.mainConf.fmtDoubleQuotes[0] self.typDQClose = self.mainConf.fmtDoubleQuotes[1] @@ -157,8 +156,7 @@ class GuiDocEditor(QTextEdit): self.paraCount = 0 self.lastEdit = 0 self.bigDoc = False - - self.hasSelection = False + self.doReplace = False self.setDocumentChanged(False) self.docHeader.setTitleFromHandle(self.theHandle) @@ -261,7 +259,9 @@ class GuiDocEditor(QTextEdit): self.hLight.spellCheck = False bfTime = time() + self._allowAutoReplace(False) self.setPlainText(theDoc) + self._allowAutoReplace(True) afTime = time() logger.debug("Document highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))) @@ -388,8 +388,8 @@ class GuiDocEditor(QTextEdit): """ if self.mainConf.verQtValue >= 50900: theText = self.qDocument.toRawText() - theText = theText.replace("\u2028", "\n") # Line separators - theText = theText.replace("\u2029", "\n") # Paragraph separators + theText = theText.replace(nwUnicode.U_LSEP, "\n") # Line separators + theText = theText.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators else: theText = self.toPlainText() return theText @@ -496,6 +496,7 @@ class GuiDocEditor(QTextEdit): if not self.theParent.hasProject: logger.error("No project open") return False + self._allowAutoReplace(False) if theAction == nwDocAction.UNDO: self.undo() elif theAction == nwDocAction.REDO: @@ -544,9 +545,15 @@ class GuiDocEditor(QTextEdit): self._formatBlock(nwDocAction.BLOCK_COM) elif theAction == nwDocAction.BLOCK_TXT: self._formatBlock(nwDocAction.BLOCK_TXT) + elif theAction == nwDocAction.REPL_SNG: + self._replaceQuotes("'", self.typSQOpen, self.typSQClose) + elif theAction == nwDocAction.REPL_DBL: + self._replaceQuotes("\"", self.typDQOpen, self.typDQClose) else: logger.debug("Unknown or unsupported document action %s" % str(theAction)) + self._allowAutoReplace(True) return False + self._allowAutoReplace(True) return True def isEmpty(self): @@ -597,24 +604,23 @@ class GuiDocEditor(QTextEdit): def keyPressEvent(self, keyEvent): """Intercept key press events. - We need to intercept key presses briefly to record the state of - selection. This is in order to know whether we had a selection - prior to triggering the _docChange slot, as we do not want to - trigger autoreplace on selections. Autoreplace on selections - messes with undo/redo history. - We also need to intercept the Shift key modifier for certain key - combinations that modifies standard keys like enter and space. - However, we don't want to spend a lot of time in this function - as it is triggered on every keypress when typing. + We need to intercept a few key sequences: + * The return key redirects here even if the search box has + focus. Since we need the return key to continue search, we + block any further interaction here while it's in focus. + * The undo sequence bypasses the doAction pathway from the + menu, so we redirect it back from here. + * The default redo sequence is Ctrl+Shift+Z, which we don't + use, so we block it. """ - self.hasSelection = self.textCursor().hasSelection() if self.docSearch.searchBox.hasFocus(): - # Block the event when the focus is on the search bar as it - # seems to be capturing the return key. return - - QTextEdit.keyPressEvent(self, keyEvent) - + elif keyEvent == QKeySequence.Redo: + return + elif keyEvent == QKeySequence.Undo: + self.docAction(nwDocAction.UNDO) + else: + QTextEdit.keyPressEvent(self, keyEvent) return def mouseReleaseEvent(self, mEvent): @@ -650,7 +656,7 @@ class GuiDocEditor(QTextEdit): self.setDocumentChanged(True) if not self.wcTimer.isActive(): self.wcTimer.start() - if self.mainConf.doReplace and not self.hasSelection: + if self.doReplace and charsAdded == 1: self._docAutoReplace(self.qDocument.findBlock(thePos)) return @@ -862,6 +868,58 @@ class GuiDocEditor(QTextEdit): return + def _replaceQuotes(self, sQuote, oQuote, cQuote): + """Replace all straight quotes in the selected text. + """ + theCursor = self.textCursor() + if theCursor.hasSelection(): + posS = theCursor.selectionStart() + posE = theCursor.selectionEnd() + closeCheck = ( + " ", "\n", nwUnicode.U_LSEP, nwUnicode.U_PSEP + ) + + self._allowAutoReplace(False) + for posC in range(posS, posE+1): + theCursor.setPosition(posC) + theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2) + selText = theCursor.selectedText() + + nS = len(selText) + if nS == 2: + pC = selText[0] + cC = selText[1] + elif nS == 1: + pC = " " + cC = selText[0] + else: + continue + + if cC != sQuote: + continue + + theCursor.clearSelection() + theCursor.setPosition(posC) + if pC in closeCheck: + theCursor.beginEditBlock() + theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1) + theCursor.insertText(oQuote) + theCursor.endEditBlock() + else: + theCursor.beginEditBlock() + theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1) + theCursor.insertText(cQuote) + theCursor.endEditBlock() + + self._allowAutoReplace(True) + + else: + self.theParent.makeAlert( + "Please selection some text before calling replace quotes.", nwAlert.ERROR + ) + + 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. @@ -1225,6 +1283,15 @@ class GuiDocEditor(QTextEdit): return + def _allowAutoReplace(self, theState): + """used to enable/disable the auto-replace feature temporarily. + """ + if theState: + self.doReplace = self.mainConf.doReplace + else: + self.doReplace = False + return + # END Class GuiDocEditor # =============================================================================================== # diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index d831da81..1f78212d 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -577,7 +577,7 @@ class GuiMainMenu(QMenuBar): self.aFmtSQuote.triggered.connect(lambda: self._docAction(nwDocAction.S_QUOTE)) self.fmtMenu.addAction(self.aFmtSQuote) - # Edit > Separator + # Format > Separator self.fmtMenu.addSeparator() # Format > Header 1 @@ -622,6 +622,21 @@ class GuiMainMenu(QMenuBar): self.aFmtNoFormat.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_TXT)) self.fmtMenu.addAction(self.aFmtNoFormat) + # Format > Separator + self.fmtMenu.addSeparator() + + # Format > Replace Single Quotes + self.aFmtReplSng = QAction("Replace Single Quotes", self) + self.aFmtReplSng.setStatusTip("Replace all straight single quotes in selected text") + self.aFmtReplSng.triggered.connect(lambda: self._docAction(nwDocAction.REPL_SNG)) + self.fmtMenu.addAction(self.aFmtReplSng) + + # Format > Replace Double Quotes + self.aFmtReplDbl = QAction("Replace Double Quotes", self) + self.aFmtReplDbl.setStatusTip("Replace all straight double quotes in selected text") + self.aFmtReplDbl.triggered.connect(lambda: self._docAction(nwDocAction.REPL_DBL)) + self.fmtMenu.addAction(self.aFmtReplDbl) + return def _buildViewMenu(self):