Merge branch 'main' into testing

This commit is contained in:
Veronica K. B. Olsen
2021-03-17 17:25:00 +01:00
6 changed files with 197 additions and 168 deletions
+24 -1
View File
@@ -152,6 +152,29 @@ convenient if you want to quickly look through all documents in the list in the
pressing :kbd:`F9`.
.. _a_ui_edit_search:
Search & Replace
----------------
The document editor has a search and replace bar that can be activated with :kbd:`Ctrl`:kbd:`F` for
search mode or :kbd:`Ctrl`:kbd:`H` for search/replace mode.
Pressing :kbd:`Return` while in the search box will search for the next occurrence of the word, and
:kbd:`Shift`:kbd:`Return` for the previous. Pressing :kbd:`Return` in the replace box, will replace
the highlighted text and move to the next word.
There are a number of settings for the search bar available as toggle switches above the search
box. They allows you to search for, in order:,: matched case only, whole word results only, search
using regular expressions, loop search when reaching the end of the document, and move to the next
document when reaching the end. There is also a switch that will try to match the case of the word
when the replacement is made. That is, it will try to keep the word upper, lower, or capitalised to
match the word being replaced.
The regular expression search is somewhat dependant on which version of Qt your system has. If you
have Qt 5.13 or higher, there is better support for unicode symbols in the search.
.. _a_ui_edit_auto:
Auto-Replace as You Type
@@ -432,7 +455,7 @@ Most features are available as keyboard shortcuts. These are as follows:
":kbd:`Ctrl`:kbd:`Backspace`", "Delete previous word in editor."
":kbd:`Ctrl`:kbd:`'`", "Wrap selected text, or word under cursor, in single quotes."
":kbd:`Ctrl`:kbd:`""`", "Wrap selected text, or word under cursor, in double quotes."
":kbd:`Ctrl`:kbd:`Enter`", "Open the tag or reference under the cursor in the Viewer."
":kbd:`Ctrl`:kbd:`Retrun`", "Open the tag or reference under the cursor in the Viewer."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`,`", "Open the :guilabel:`Project Settings` dialog."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`/`", "Remove block formatting for block under cursor."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`1`", "Replace occurrence of search word in current document, and search for next occurrence."
+8 -13
View File
@@ -81,19 +81,14 @@ class nwDocAction(Enum):
D_QUOTE = 10
SEL_ALL = 11
SEL_PARA = 12
FIND = 13
REPLACE = 14
GO_NEXT = 15
GO_PREV = 16
REPL_NEXT = 17
BLOCK_H1 = 18
BLOCK_H2 = 19
BLOCK_H3 = 20
BLOCK_H4 = 21
BLOCK_COM = 22
BLOCK_TXT = 23
REPL_SNG = 24
REPL_DBL = 25
BLOCK_H1 = 13
BLOCK_H2 = 14
BLOCK_H3 = 15
BLOCK_H4 = 16
BLOCK_COM = 17
BLOCK_TXT = 18
REPL_SNG = 19
REPL_DBL = 20
# END Enum nwDocAction
+151 -144
View File
@@ -684,10 +684,6 @@ class GuiDocEditor(QTextEdit):
this class when calling these actions from other classes.
"""
logger.verbose("Requesting action: %s" % theAction.name)
if not self.hasFocus():
logger.verbose("Editor does not have focus")
return False
if self.theHandle is None:
logger.error("No document open")
return False
@@ -717,16 +713,6 @@ class GuiDocEditor(QTextEdit):
self._makeSelection(QTextCursor.Document)
elif theAction == nwDocAction.SEL_PARA:
self._makeSelection(QTextCursor.BlockUnderCursor)
elif theAction == nwDocAction.FIND:
self._beginSearch()
elif theAction == nwDocAction.REPLACE:
self._beginReplace()
elif theAction == nwDocAction.GO_NEXT:
self._findNext()
elif theAction == nwDocAction.GO_PREV:
self._findNext(isBackward=True)
elif theAction == nwDocAction.REPL_NEXT:
self._replaceNext()
elif theAction == nwDocAction.BLOCK_H1:
self._formatBlock(nwDocAction.BLOCK_H1)
elif theAction == nwDocAction.BLOCK_H2:
@@ -758,6 +744,15 @@ class GuiDocEditor(QTextEdit):
"""
return self.qDocument.isEmpty()
def anyFocus(self):
"""Check if any widget or child widget has focus.
"""
if self.hasFocus():
return True
if self.isAncestorOf(qApp.focusWidget()):
return True
return False
def revealLocation(self):
"""Tell the user where on the file system the file in the editor
is saved.
@@ -851,7 +846,7 @@ class GuiDocEditor(QTextEdit):
if self.docSearch.isVisible():
self.docSearch.closeSearch()
else:
self._beginSearch()
self.beginSearch()
return
##
@@ -1169,6 +1164,144 @@ class GuiDocEditor(QTextEdit):
logger.verbose("Denied cursor move to %d > %d" % (self.queuePos, thePos))
return
##
# Search & Replace
##
def beginSearch(self):
"""Sets the selected text as the search text for the search bar.
"""
theCursor = self.textCursor()
if theCursor.hasSelection():
self.docSearch.setSearchText(theCursor.selectedText())
else:
self.docSearch.setSearchText(None)
self.updateDocMargins()
return
def beginReplace(self):
"""Opens the replace line of the search bar and sets the find
text if a selection has been made, and resets the replace text.
"""
theCursor = self.textCursor()
if theCursor.hasSelection():
self.docSearch.setSearchText(theCursor.selectedText())
else:
self.docSearch.setSearchText(None)
self.docSearch.setReplaceText("")
self.updateDocMargins()
return
def findNext(self, goBack=False):
"""Searches for the next or previous occurrence of the search
bar text in the document. Wraps around if not found and loop is
enabled, or continues to next file if next file is enabled.
"""
if not self.anyFocus():
logger.debug("Editor does not have focus")
return False
if not self.docSearch.isVisible():
self.beginSearch()
return
findOpt = QTextDocument.FindFlag(0)
if goBack:
findOpt |= QTextDocument.FindBackward
if self.docSearch.isCaseSense:
findOpt |= QTextDocument.FindCaseSensitively
if self.docSearch.isWholeWord:
findOpt |= QTextDocument.FindWholeWords
searchFor = self.docSearch.getSearchObject()
wasFound = self.find(searchFor, findOpt)
if not wasFound:
if self.docSearch.doNextFile and not goBack:
self.theParent.openNextDocument(
self.theHandle, wrapAround=self.docSearch.doLoop
)
elif self.docSearch.doLoop:
theCursor = self.textCursor()
theCursor.movePosition(
QTextCursor.End if goBack else QTextCursor.Start
)
self.setTextCursor(theCursor)
wasFound = self.find(searchFor, findOpt)
if wasFound:
theCursor = self.textCursor()
self.lastFind = (theCursor.selectionStart(), theCursor.selectionEnd())
return
def replaceNext(self):
"""Searches for the next occurrence of the search bar text in
the document and replaces it with the replace text. Calls search
next automatically when done.
"""
if not self.anyFocus():
logger.debug("Editor does not have focus")
return False
if not self.docSearch.isVisible():
# The search tool is not active, so we activate it.
self.beginSearch()
return
theCursor = self.textCursor()
if not theCursor.hasSelection():
# We have no text selected at all, so just make this a
# regular find next call.
self.findNext()
return
if self.lastFind is None and theCursor.hasSelection():
# If we have a selection but no search, it may have been the
# text we triggered the search with, in which case we search
# again from the beginning of that selection to make sure we
# have a valid result.
sPos = theCursor.selectionStart()
theCursor.clearSelection()
theCursor.setPosition(sPos)
self.setTextCursor(theCursor)
self.findNext()
theCursor = self.textCursor()
if self.lastFind is None:
# In case the above didn't find a result, we give up here.
return
searchFor = self.docSearch.getSearchText()
replWith = self.docSearch.getReplaceText()
if self.docSearch.doMatchCap:
replWith = transferCase(theCursor.selectedText(), replWith)
# Make sure the selected text was selected by an actual find
# call, and not the user.
try:
isFind = self.lastFind[0] == theCursor.selectionStart()
isFind &= self.lastFind[1] == theCursor.selectionEnd()
except Exception:
isFind = False
if isFind:
theCursor.beginEditBlock()
theCursor.removeSelectedText()
theCursor.insertText(replWith)
theCursor.endEditBlock()
theCursor.setPosition(theCursor.selectionEnd())
self.setTextCursor(theCursor)
logger.verbose("Replaced occurrence of '%s' with '%s' on line %d" % (
searchFor, replWith, theCursor.blockNumber()
))
else:
logger.error("The selected text is not a search result, skipping replace")
self.findNext()
return
##
# Internal Functions
##
@@ -1630,132 +1763,6 @@ class GuiDocEditor(QTextEdit):
self._makeSelection(selMode)
return
def _beginSearch(self):
"""Sets the selected text as the search text for the search bar.
"""
theCursor = self.textCursor()
if theCursor.hasSelection():
self.docSearch.setSearchText(theCursor.selectedText())
else:
self.docSearch.setSearchText(None)
self.updateDocMargins()
return
def _beginReplace(self):
"""Opens the replace line of the search bar and sets the find
text if a selection has been made, and resets the replace text.
"""
theCursor = self.textCursor()
if theCursor.hasSelection():
self.docSearch.setSearchText(theCursor.selectedText())
else:
self.docSearch.setSearchText(None)
self.docSearch.setReplaceText("")
self.updateDocMargins()
return
def _findNext(self, isBackward=False):
"""Searches for the next or previous occurrence of the search
bar text in the document. Wraps around if not found and loop is
enabled, or continues to next file if next file is enabled.
"""
if not self.docSearch.isVisible():
self._beginSearch()
return
findOpt = QTextDocument.FindFlag(0)
if isBackward:
findOpt |= QTextDocument.FindBackward
if self.docSearch.isCaseSense:
findOpt |= QTextDocument.FindCaseSensitively
if self.docSearch.isWholeWord:
findOpt |= QTextDocument.FindWholeWords
searchFor = self.docSearch.getSearchObject()
wasFound = self.find(searchFor, findOpt)
if not wasFound:
if self.docSearch.doNextFile and not isBackward:
self.theParent.openNextDocument(
self.theHandle, wrapAround=self.docSearch.doLoop
)
elif self.docSearch.doLoop:
theCursor = self.textCursor()
theCursor.movePosition(
QTextCursor.End if isBackward else QTextCursor.Start
)
self.setTextCursor(theCursor)
wasFound = self.find(searchFor, findOpt)
if wasFound:
theCursor = self.textCursor()
self.lastFind = (theCursor.selectionStart(), theCursor.selectionEnd())
return
def _replaceNext(self):
"""Searches for the next occurrence of the search bar text in
the document and replaces it with the replace text. Calls search
next automatically when done.
"""
if not self.docSearch.isVisible():
# The search tool is not active, so we activate it.
self._beginSearch()
return
theCursor = self.textCursor()
if not theCursor.hasSelection():
# We have no text selected at all, so just make this a
# regular find next call.
self._findNext()
return
if self.lastFind is None and theCursor.hasSelection():
# If we have a selection but no search, it may have been the
# text we triggered the search with, in which case we search
# again from the beginning of that selection to make sure we
# have a valid result.
sPos = theCursor.selectionStart()
theCursor.clearSelection()
theCursor.setPosition(sPos)
self.setTextCursor(theCursor)
self._findNext()
theCursor = self.textCursor()
if self.lastFind is None:
# In case the above didn't find a result, we give up here.
return
searchFor = self.docSearch.getSearchText()
replWith = self.docSearch.getReplaceText()
if self.docSearch.doMatchCap:
replWith = transferCase(theCursor.selectedText(), replWith)
# Make sure the selected text was selected by an actual find
# call, and not the user.
try:
isFind = self.lastFind[0] == theCursor.selectionStart()
isFind &= self.lastFind[1] == theCursor.selectionEnd()
except Exception:
isFind = False
if isFind:
theCursor.beginEditBlock()
theCursor.removeSelectedText()
theCursor.insertText(replWith)
theCursor.endEditBlock()
theCursor.setPosition(theCursor.selectionEnd())
self.setTextCursor(theCursor)
logger.verbose("Replaced occurrence of '%s' with '%s' on line %d" % (
searchFor, replWith, theCursor.blockNumber()
))
else:
logger.error("The selected text is not a search result, skipping replace")
self._findNext()
return
def _setupSpellChecking(self):
"""Create the spell checking object based on the spellTool
setting in config.
@@ -2119,15 +2126,15 @@ class GuiDocEditSearch(QFrame):
"""
modKey = qApp.keyboardModifiers()
if modKey == Qt.ShiftModifier:
self.docEditor.docAction(nwDocAction.GO_PREV)
self.docEditor.findNext(goBack=True)
else:
self.docEditor.docAction(nwDocAction.GO_NEXT)
self.docEditor.findNext()
return
def _doReplace(self):
"""Call the replace action function for the document editor.
"""
self.docEditor.docAction(nwDocAction.REPL_NEXT)
self.docEditor.replaceNext()
return
def _doToggleReplace(self, theState):
+5 -5
View File
@@ -762,7 +762,7 @@ class GuiMainMenu(QMenuBar):
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._docAction(nwDocAction.FIND))
self.aFind.triggered.connect(lambda: self.theParent.docEditor.beginSearch())
self.srcMenu.addAction(self.aFind)
# Search > Replace
@@ -772,7 +772,7 @@ class GuiMainMenu(QMenuBar):
self.aReplace.setShortcut("Ctrl+=")
else:
self.aReplace.setShortcut("Ctrl+H")
self.aReplace.triggered.connect(lambda: self._docAction(nwDocAction.REPLACE))
self.aReplace.triggered.connect(lambda: self.theParent.docEditor.beginReplace())
self.srcMenu.addAction(self.aReplace)
# Search > Find Next
@@ -782,7 +782,7 @@ class GuiMainMenu(QMenuBar):
self.aFindNext.setShortcuts(["Ctrl+G", "F3"])
else:
self.aFindNext.setShortcuts(["F3", "Ctrl+G"])
self.aFindNext.triggered.connect(lambda: self._docAction(nwDocAction.GO_NEXT))
self.aFindNext.triggered.connect(lambda: self.theParent.docEditor.findNext())
self.srcMenu.addAction(self.aFindNext)
# Search > Find Prev
@@ -792,7 +792,7 @@ class GuiMainMenu(QMenuBar):
self.aFindPrev.setShortcuts(["Ctrl+Shift+G", "Shift+F3"])
else:
self.aFindPrev.setShortcuts(["Shift+F3", "Ctrl+Shift+G"])
self.aFindPrev.triggered.connect(lambda: self._docAction(nwDocAction.GO_PREV))
self.aFindPrev.triggered.connect(lambda: self.theParent.docEditor.findNext(goBack=True))
self.srcMenu.addAction(self.aFindPrev)
# Search > Replace Next
@@ -801,7 +801,7 @@ class GuiMainMenu(QMenuBar):
self.tr("Find and replace next occurrence of text in document")
)
self.aReplaceNext.setShortcut("Ctrl+Shift+1")
self.aReplaceNext.triggered.connect(lambda: self._docAction(nwDocAction.REPL_NEXT))
self.aReplaceNext.triggered.connect(lambda: self.theParent.docEditor.replaceNext())
self.srcMenu.addAction(self.aReplaceNext)
return
+8 -4
View File
@@ -766,14 +766,18 @@ class GuiMain(QMainWindow):
return True
def passDocumentAction(self, theAction):
"""Pass on document action theAction to the document viewer if
it has focus, otherwise pass it to the document editor.
"""Pass on document action to the document viewer if it has
focus, or pass it to the document editor if it or any of
its clid widgets have focus. If neither has focus, ignore the
action.
"""
if self.docViewer.hasFocus():
self.docViewer.docAction(theAction)
else:
elif self.docEditor.hasFocus():
self.docEditor.docAction(theAction)
return True
else:
logger.debug("Action cancelled as neither editor nor viewer has focus")
return
##
# Tree Item Actions
+1 -1
View File
@@ -510,7 +510,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, nwLipsum):
assert abs(nwGUI.docEditor.getCursorPosition() - 1127) < 3
# Toggle Replace
nwGUI.docEditor._beginReplace()
nwGUI.docEditor.beginReplace()
# MonkeyPatch the focus cycle. We can't really test this very well, other than
# check that the tabs aren't captured when the main editor has focus