Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# EnhancedFindDialog for NVDA 1.6.1
# EnhancedFindDialog for NVDA 1.7.0
Enhanced find dialog addon for NVDA, implementing search improvements:

* search history
* Search by Regular Expressions
* search wrapping, configured per profile
* case sensitivity, configured per profile
* contextual information on searches

## Download
Download the [Enhanced Find Dialog 1.6.1 addon](https://github.com/marlon-sousa/EnhancedFindDialog/releases/download/1.6.1/EnhancedFindDialog-1.6.1.nvda-addon)
Download the [Enhanced Find Dialog 1.7.0 addon](https://github.com/marlon-sousa/EnhancedFindDialog/releases/download/1.7.0/EnhancedFindDialog-1.7.0.nvda-addon)

## Note about secure mode

Expand Down Expand Up @@ -36,6 +37,27 @@ Simply install the addon. When it is activated, pressing down and up arrows on t

You can at any time type a new term as usual.

### Search by Regular Expressions

In addition to the normal search supported by NVDA, this addon supports searching by regular expressions. For more information on how to get started with Regular Expressions, you can see for example [the Regular Expression HowTo tutorial for Python](https://docs.python.org/3/howto/regex.html), however many other tutorials are available on the Internet.

Regular Expressions are specially useful to find some text that can vary on a web page.

This option is profile specific, meaning that you can have a profile where it is active and other where it isn't.

Please note that due to underlying technological implementation differences, this feature is not available on all programs (for example Microsoft Office Word documents) that support NVDA's find dialog.

#### How it works?

Simply install the addon. When it is activated, the find dialog will offer a `Search Type` radio box with two search options:

* **Normal search** will perform NVDA's default search functionality.

* **Regular Expression Search** will perform the search by Regular Expressions. Simply enter the Regular Expression on the text box and NVDA will place the cursor on the next match.


Changing this radio box and performing a search will save the new state (normal or Regular Expression search for the active profile. Canceling the search won't change its state on the active profile, even if you changed it before canceling the search.

### Search wrapping

Search Wrapping is a feature that, if configured, doesn't consider the current position you are on a text when performing searches.
Expand Down
22 changes: 22 additions & 0 deletions README.tpl.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Enhanced find dialog addon for NVDA, implementing search improvements:

* search history
* Search by Regular Expressions
* search wrapping, configured per profile
* case sensitivity, configured per profile
* contextual information on searches
Expand Down Expand Up @@ -36,6 +37,27 @@ Simply install the addon. When it is activated, pressing down and up arrows on t

You can at any time type a new term as usual.

### Search by Regular Expressions

In addition to the normal search supported by NVDA, this addon supports searching by regular expressions. For more information on how to get started with Regular Expressions, you can see for example [the Regular Expression HowTo tutorial for Python](https://docs.python.org/3/howto/regex.html), however many other tutorials are available on the Internet.

Regular Expressions are specially useful to find some text that can vary on a web page.

This option is profile specific, meaning that you can have a profile where it is active and other where it isn't.

Please note that due to underlying technological implementation differences, this feature is not available on all programs (for example Microsoft Office Word documents) that support NVDA's find dialog.

#### How it works?

Simply install the addon. When it is activated, the find dialog will offer a `Search Type` radio box with two search options:

* **Normal search** will perform NVDA's default search functionality.

* **Regular Expression Search** will perform the search by Regular Expressions. Simply enter the Regular Expression on the text box and NVDA will place the cursor on the next match.


Changing this radio box and performing a search will save the new state (normal or Regular Expression search for the active profile. Canceling the search won't change its state on the active profile, even if you changed it before canceling the search.

### Search wrapping

Search Wrapping is a feature that, if configured, doesn't consider the current position you are on a text when performing searches.
Expand Down
3 changes: 3 additions & 0 deletions addon/globalPlugins/EnhancedFindDialog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def initConfiguration():
confspec = {
"searchCaseSensitivity": "boolean( default=False)",
"searchWrap": "boolean( default=False)",
"searchType": "string( default='NORMAL')",
}
config.conf.spec[module] = confspec

Expand Down Expand Up @@ -51,3 +52,5 @@ def injectProcessing(self):

# add methods to CursorManager class
cursorManagerHelper.patchCursorManager()
# add methods to offsetsTextInfo class
cursorManagerHelper.patchOffsetsTextInfo()
117 changes: 101 additions & 16 deletions addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@
# This file is covered by the GNU General Public License.
# See the file COPYING.txt for more details.


import addonHandler
import buildVersion
import config
import controlTypes
from cursorManager import CursorManager
import gui
import textInfos.offsets
import textUtils
from . import guiHelper
from .searchHistory import SearchHistory
from .searchType import SearchType
import inspect
from logHandler import log
import re
import speech
from scriptHandler import willSayAllResume, script
Expand Down Expand Up @@ -64,6 +70,12 @@ def patchCursorManager():
CursorManager.script_find = script_enhancedFind
CursorManager.script_findNext = script_enhancedFindNext
CursorManager.script_findPrevious = script_EnhancedFindPrevious
CursorManager.supportsRegexpSearch = supportsRegexpSearch
CursorManager.getSearchEntries = getSearchEntries


def patchOffsetsTextInfo():
setattr(textInfos.offsets.OffsetsTextInfo, "findRegexp", findRegexp)


def script_enhancedFind(self, gesture, reverse=False):
Expand All @@ -75,68 +87,97 @@ def run():
profile = config.conf.getActiveProfile()

gui.mainFrame.prePopup()
d = guiHelper.EnhancedFindDialog(gui.mainFrame, self, profile, self._searchEntries, reverse)
d = guiHelper.EnhancedFindDialog(gui.mainFrame, self, profile, reverse)
d.ShowModal()
gui.mainFrame.postPopup()
wx.CallAfter(run)


def script_enhancedFindNext(self, gesture):
if not self._searchEntries:
mostRecentSearchTerm = getMostRecentSearchTerm()
if (mostRecentSearchTerm is None
or (mostRecentSearchTerm.searchType == SearchType.REGULAR_EXPRESSION.name
and not self.supportsRegexpSearch())):
self.script_find(gesture)
return
doFindText(
self,
self._searchEntries[SEARCH_HISTORY_MOST_RECENT_INDEX],
mostRecentSearchTerm,
caseSensitive=self._lastCaseSensitivity,
searchWrap=self._searchWrap,
willSayAllResume=willSayAllResume(gesture),
)


def script_EnhancedFindPrevious(self, gesture):
if not self._searchEntries:
mostRecentSearchTerm = getMostRecentSearchTerm()
if (mostRecentSearchTerm is None
or (mostRecentSearchTerm.searchType == SearchType.REGULAR_EXPRESSION.name
and not self.supportsRegexpSearch())):
self.script_find(gesture, reverse=True)
return
doFindText(
self,
self._searchEntries[SEARCH_HISTORY_MOST_RECENT_INDEX],
mostRecentSearchTerm,
reverse=True,
caseSensitive=self._lastCaseSensitivity,
searchWrap=self._searchWrap,
willSayAllResume=willSayAllResume(gesture)
)


def doFindText(cursorManagerInstance, text,
def supportsRegexpSearch(self):
info = self.makeTextInfo(textInfos.POSITION_CARET)
return isinstance(info, textInfos.offsets.OffsetsTextInfo)


def getSearchEntries(self):
searchHistory = SearchHistory.get()
if self.supportsRegexpSearch():
return searchHistory.getItems()
return searchHistory.getItems(searchType="normal")


def getMostRecentSearchTerm():
searchHistory = SearchHistory.get()
return searchHistory.getMostRecent()


def doFindText(cursorManagerInstance, searchTerm,
reverse=False, caseSensitive=False, searchWrap=False, willSayAllResume=False): # noqa: E101
if not text:
log.debug("doFindText, reverse=%s, caseSensitive=%s, searchWrap=%s", reverse, caseSensitive, searchWrap)
if not searchTerm:
return

info = cursorManagerInstance.makeTextInfo(textInfos.POSITION_CARET)
res = performSearch(cursorManagerInstance, text, info, reverse, caseSensitive, searchWrap)
res = performSearch(cursorManagerInstance, searchTerm, info, reverse, caseSensitive, searchWrap)
if res:
cursorManagerInstance.selection = info
speech.cancelSpeech()
info.move(textInfos.UNIT_LINE, 1, endPoint="start")
info.expand(textInfos.UNIT_LINE)

if searchTerm.searchType == SearchType.REGULAR_EXPRESSION.name:
info.move(textInfos.UNIT_LINE, 1, endPoint="end")
else:
info.move(textInfos.UNIT_LINE, 1, endPoint="start")
info.expand(textInfos.UNIT_LINE)

if not willSayAllResume:
speech.speakTextInfo(info, reason=controlTypes.OutputReason.CARET)
else:
wx.CallAfter(gui.messageBox, __('text "%s" not found') % text,
wx.CallAfter(gui.messageBox, __('text "%s" not found') % searchTerm.text,
FIND_ERROR_DIALOG_TITLE, wx.OK | wx.ICON_ERROR) # Noqa E101
CursorManager._lastFindText = text
CursorManager._lastFindText = searchTerm.text
CursorManager._lastCaseSensitivity = caseSensitive
CursorManager._searchWrap = searchWrap


def performSearch(cursorManager, text, info, reverse, caseSensitive, wrapSearch):
res = info.find(text, reverse=reverse, caseSensitive=caseSensitive)
def performSearch(cursorManager, searchTerm, info, reverse, caseSensitive, wrapSearch):
res = find(cursorManager, searchTerm, info, reverse=reverse, caseSensitive=caseSensitive)
# if either not interested in search wrapping or we have found a result then we are done here
if not wrapSearch or res:
return res
found = False
while info.find(text, reverse=(not reverse), caseSensitive=caseSensitive):
while find(cursorManager, searchTerm, info, reverse=(not reverse), caseSensitive=caseSensitive):
found = True
if found:
beep(440, 30)
Expand All @@ -152,12 +193,56 @@ def performSearch(cursorManager, text, info, reverse, caseSensitive, wrapSearch)
info = cursorManager.makeTextInfo(textInfos.POSITION_CARET).copy()
info.expand(textInfos.UNIT_STORY)
inText = info._get_text()
found = re.search(re.escape(text), inText, (0 if caseSensitive else re.IGNORECASE) | re.UNICODE)

if searchTerm.searchType == SearchType.REGULAR_EXPRESSION.name:
found = re.search(searchTerm.text, inText, re.UNICODE)
else:
found = re.search(re.escape(searchTerm.text), inText, (0 if caseSensitive else re.IGNORECASE) | re.UNICODE)

if found:
beep(440, 30)
return found


def find(cursorManager, searchTerm, info, reverse, caseSensitive):
if searchTerm.searchType == SearchType.REGULAR_EXPRESSION.name:
if not cursorManager.supportsRegexpSearch():
wx.CallAfter(
# Translators: Message shown when an invalid regular expression is entered.
_("current textInfo backend does not support regular expression searches"),
FIND_ERROR_DIALOG_TITLE, wx.OK | wx.ICON_ERROR
)
return None
res = info.findRegexp(searchTerm.text, reverse=reverse)
else:
res = info.find(searchTerm.text, reverse=reverse, caseSensitive=caseSensitive)
return res


def findRegexp(self, text, reverse=False):
if reverse:
inText = self._getTextRange(0, self._startOffset)
matches = list(re.finditer(text, inText, re.UNICODE))
if not matches:
return False
m = matches[-1]
else:
inText = self._getTextRange(self._startOffset + 1, self._getStoryLength())
m = re.search(text, inText, re.UNICODE)
if not m:
return False

converter = textUtils.getOffsetConverter(self.encoding)(inText)

if reverse:
offset = converter.strToEncodedOffsets(m.start())
else:
offset = self._startOffset + 1 + converter.strToEncodedOffsets(m.start())

self._startOffset = self._endOffset = offset
return True


# Patch say all functionality.
# Fix the patched script losing the reference to the decorated say all,
# documentation and other necessary attributes for the gesture
Expand Down
Loading