Merge branch 'main' into testing
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user