From 4d65101bac5ff8cc2cc8f042c13d58d1dd1761b6 Mon Sep 17 00:00:00 2001 From: Marlon Sousa Date: Mon, 27 May 2024 09:16:44 -0300 Subject: [PATCH 01/12] gui interface --- .../EnhancedFindDialog/__init__.py | 1 + .../EnhancedFindDialog/guiHelper.py | 121 +++++++++++++++--- 2 files changed, 103 insertions(+), 19 deletions(-) diff --git a/addon/globalPlugins/EnhancedFindDialog/__init__.py b/addon/globalPlugins/EnhancedFindDialog/__init__.py index cb5c5b9..4a9cc95 100644 --- a/addon/globalPlugins/EnhancedFindDialog/__init__.py +++ b/addon/globalPlugins/EnhancedFindDialog/__init__.py @@ -18,6 +18,7 @@ def initConfiguration(): confspec = { "searchCaseSensitivity": "boolean( default=False)", "searchWrap": "boolean( default=False)", + "searchType": "string( default='NORMAL')", } config.conf.spec[module] = confspec diff --git a/addon/globalPlugins/EnhancedFindDialog/guiHelper.py b/addon/globalPlugins/EnhancedFindDialog/guiHelper.py index ef79c38..78055d4 100644 --- a/addon/globalPlugins/EnhancedFindDialog/guiHelper.py +++ b/addon/globalPlugins/EnhancedFindDialog/guiHelper.py @@ -4,6 +4,7 @@ # This file is covered by the GNU General Public License. # See the file COPYING.txt for more details. +from enum import Enum, unique import addonHandler import config import core @@ -12,6 +13,9 @@ from gui import contextHelp, guiHelper import wx +from logHandler import log + + # this addon mostly complements NVDA functionalities. # however, because the way NVDA works, when you use # addon translation infrastructure by calling addonHandler.initTranslation() you loose access to the @@ -74,6 +78,41 @@ def setConfig(profile, key, value): profile[module][key] = value +class InvalidTypeName(Exception): + pass + + +@unique +class SearchType(Enum): + # Translators: normal + NORMAL = _("normal") + # Translators: regular expression + REGULAR_EXPRESSION = _("regular expression") + + @staticmethod + def getByIndex(index): + return list(SearchType)[index] + + @staticmethod + def getIndexByName(name): + log.debug(f"searching for {name}") + for index, type in enumerate(SearchType): + if type.name == name: + return index + raise InvalidTypeName(f"No variant with name '{name}' found in SearchType Enum") + + @staticmethod + def getByName(name): + for type in SearchType: + if type.name == name: + return type + raise InvalidTypeName(f"No variant with name '{name}' found in SearchType Enum") + + +def getSearchTypes(): + return [i.value for i in SearchType] + + class EnhancedFindDialog(contextHelp.ContextHelpMixin, wx.Dialog): # Noqa: E101 """A dialog used to specify text to find in a cursor manager. @@ -93,9 +132,15 @@ def __init__(self, parent, cursorManager, profile, searchEntries, reverseSearch) # this is needed because whenever the find dialog is opened the default profile is loaded. We, however, want # to retrieve state from the active profile when the find dialog was loaded self.profile = profile - caseSensitivity = strToBool(getConfig(profile, "searchCaseSensitivity")) - searchWrap = strToBool(getConfig(profile, "searchWrap")) - + self.caseSensitivity = strToBool(getConfig(profile, "searchCaseSensitivity")) + self.searchWrap = strToBool(getConfig(profile, "searchWrap")) + self.searchType = SearchType.getByName(getConfig(profile, "searchType")).name + self.buildGui(searchEntries) + self.updateUi() + self.bindEvents() + + def buildGui(self, searchEntries): + log.debug("called buildGui") mainSizer = wx.BoxSizer(wx.VERTICAL) sHelper = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) @@ -111,31 +156,56 @@ def __init__(self, parent, cursorManager, profile, searchEntries, reverseSearch) # present the last searched term selected by default if searchEntries: self.findTextField.Select(SEARCH_HISTORY_MOST_RECENT_INDEX) - + searchTypeHelper = guiHelper.BoxSizerHelper( + self, orientation=wx.HORIZONTAL) + self._searchTypeCtrl = searchTypeHelper.addItem(wx.RadioBox( + self, + # Translators: A radio box to select the search type. + label=__("Search type"), choices=getSearchTypes(), + majorDimension=1, style=wx.RA_SPECIFY_ROWS)) + sHelper.addItem(searchTypeHelper) # Translators: An option in find dialog to perform case-sensitive search. self.caseSensitiveCheckBox = wx.CheckBox(self, wx.ID_ANY, label=__("Case &sensitive")) - self.caseSensitiveCheckBox.SetValue(caseSensitivity) sHelper.addItem(self.caseSensitiveCheckBox) - self.caseSensitiveCheckBox.Bind(wx.EVT_CHECKBOX, self.onStatChange) # Translators: An option in find dialog to perform search wrapping self.searchWrapCheckBox = wx.CheckBox(self, wx.ID_ANY, label=_("Search &wrap")) - self.searchWrapCheckBox.SetValue(searchWrap) sHelper.addItem(self.searchWrapCheckBox) - self.searchWrapCheckBox.Bind(wx.EVT_CHECKBOX, self.onStatChange) sHelper.addDialogDismissButtons(self.CreateButtonSizer(wx.OK | wx.CANCEL)) mainSizer.Add(sHelper.sizer, border=guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) - self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK) - self.Bind(wx.EVT_BUTTON, self.onCancel, id=wx.ID_CANCEL) - mainSizer.Fit(self) self.SetSizer(mainSizer) self.CentreOnScreen() self.findTextField.SetFocus() + def updateUi(self): + log.debug("called update ui") + self.caseSensitiveCheckBox.SetValue(self.caseSensitivity) + self.searchWrapCheckBox.SetValue(self.searchWrap) + self._searchTypeCtrl.SetSelection(SearchType.getIndexByName(self.searchType)) + if(self.searchType == SearchType.NORMAL.name): + self.caseSensitiveCheckBox.Enable(True) + else: + self.caseSensitiveCheckBox.Enable(False) + + def bindEvents(self): + log.debug("called bind events") + self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK) + self.Bind(wx.EVT_BUTTON, self.onCancel, id=wx.ID_CANCEL) + self.caseSensitiveCheckBox.Bind(wx.EVT_CHECKBOX, self.onStatChange) + self.searchWrapCheckBox.Bind(wx.EVT_CHECKBOX, self.onStatChange) + self._searchTypeCtrl.Bind(wx.EVT_RADIOBOX, self.OnSearchTypeChanged) + self.self._searchTypeCtrl.Bind(wx.EVT_CHECKBOX, self.onStatChange) + + def OnSearchTypeChanged(self, evt): + log.debug("called OnSearchTypeChanged") + self.searchType = SearchType.getByIndex(self._searchTypeCtrl.GetSelection()).name + self.updateUi() + self.onStatChange(evt) + def updateSearchEntries(self, searchEntries, currentSearchTerm): if not currentSearchTerm: return @@ -166,31 +236,44 @@ def updateSearchEntries(self, searchEntries, currentSearchTerm): searchEntries.insert(SEARCH_HISTORY_MOST_RECENT_INDEX, currentSearchTerm) def onOk(self, evt): + log.debug("called onOk") text = self.findTextField.GetValue() # update the list of searched entries so that it can be exibited in the next find dialog call self.updateSearchEntries(self.activeCursorManager._searchEntries, text) - caseSensitive = self.caseSensitiveCheckBox.GetValue() - setConfig(self.profile, "searchCaseSensitivity", caseSensitive) + self.caseSensitive = self.caseSensitiveCheckBox.GetValue() - searchWrap = self.searchWrapCheckBox.GetValue() - setConfig(self.profile, "searchWrap", searchWrap) + self.searchWrap = self.searchWrapCheckBox.GetValue() - if self._mustSaveProfile: - scheduleProfileSave(self.profile) + self.searchType = SearchType.getByIndex(self._searchTypeCtrl.GetSelection()).name + + self.updateProfile() # We must use core.callLater rather than wx.CallLater to # ensure that the callback runs within NVDA's core pump. # If it didn't, and it directly or indirectly called wx.Yield, it # could start executing NVDA's core pump from within the yield, causing recursion. - core.callLater(100, cursorManagerHelper.doFindText, self.activeCursorManager, text, - caseSensitive=caseSensitive, searchWrap=searchWrap, reverse=self.reverseSearch) # Noqa: E101 + core.callLater( + 100, cursorManagerHelper.doFindText, self.activeCursorManager, text, + caseSensitive=self.caseSensitive, searchWrap=self.searchWrap, + reverse=self.reverseSearch) # Noqa: E101 self.Destroy() def onCancel(self, evt): + log.debug("called onCancel") self.Destroy() + def updateProfile(self): + log.debug("called updateProfile") + setConfig(self.profile, "searchType", self.searchType) + setConfig(self.profile, "searchCaseSensitivity", self.caseSensitiveCheckBox.GetValue()) + setConfig(self.profile, "searchWrap", self.searchWrapCheckBox.GetValue()) + + if self._mustSaveProfile: + scheduleProfileSave(self.profile) + def onStatChange(self, evt): + log.debug("called onStatChange") self._mustSaveProfile = True def _truncateSearchHistory(self, entries): From f7f70c1d44a09da6d359ad949311d39c49ea19b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marlon=20Brand=C3=A3o=20de=20Sousa?= Date: Tue, 28 May 2024 11:22:03 -0300 Subject: [PATCH 02/12] Update addon/globalPlugins/EnhancedFindDialog/guiHelper.py Co-authored-by: Cyrille Bougot --- addon/globalPlugins/EnhancedFindDialog/guiHelper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/globalPlugins/EnhancedFindDialog/guiHelper.py b/addon/globalPlugins/EnhancedFindDialog/guiHelper.py index 78055d4..c847cf7 100644 --- a/addon/globalPlugins/EnhancedFindDialog/guiHelper.py +++ b/addon/globalPlugins/EnhancedFindDialog/guiHelper.py @@ -198,7 +198,7 @@ def bindEvents(self): self.caseSensitiveCheckBox.Bind(wx.EVT_CHECKBOX, self.onStatChange) self.searchWrapCheckBox.Bind(wx.EVT_CHECKBOX, self.onStatChange) self._searchTypeCtrl.Bind(wx.EVT_RADIOBOX, self.OnSearchTypeChanged) - self.self._searchTypeCtrl.Bind(wx.EVT_CHECKBOX, self.onStatChange) + self._searchTypeCtrl.Bind(wx.EVT_CHECKBOX, self.onStatChange) def OnSearchTypeChanged(self, evt): log.debug("called OnSearchTypeChanged") From 7c96b510b80a5d67b763c97d975c8e258be3ec5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marlon=20Brand=C3=A3o=20de=20Sousa?= Date: Tue, 28 May 2024 11:22:48 -0300 Subject: [PATCH 03/12] Update addon/globalPlugins/EnhancedFindDialog/guiHelper.py Co-authored-by: Cyrille Bougot --- addon/globalPlugins/EnhancedFindDialog/guiHelper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/globalPlugins/EnhancedFindDialog/guiHelper.py b/addon/globalPlugins/EnhancedFindDialog/guiHelper.py index c847cf7..28b162d 100644 --- a/addon/globalPlugins/EnhancedFindDialog/guiHelper.py +++ b/addon/globalPlugins/EnhancedFindDialog/guiHelper.py @@ -161,7 +161,7 @@ def buildGui(self, searchEntries): self._searchTypeCtrl = searchTypeHelper.addItem(wx.RadioBox( self, # Translators: A radio box to select the search type. - label=__("Search type"), choices=getSearchTypes(), + label=_("Search type:"), choices=getSearchTypes(), majorDimension=1, style=wx.RA_SPECIFY_ROWS)) sHelper.addItem(searchTypeHelper) # Translators: An option in find dialog to perform case-sensitive search. From fcc1dca5cd48cf24b0346904980a5a8e3f95ba4e Mon Sep 17 00:00:00 2001 From: Marlon Sousa Date: Sun, 20 Apr 2025 10:09:14 -0300 Subject: [PATCH 04/12] gui handling --- .../EnhancedFindDialog/cursorManagerHelper.py | 69 ++++++++--- .../EnhancedFindDialog/guiHelper.py | 110 ++++++------------ .../EnhancedFindDialog/searchHistory.py | 54 +++++++++ .../EnhancedFindDialog/searchType.py | 66 +++++++++++ 4 files changed, 213 insertions(+), 86 deletions(-) create mode 100644 addon/globalPlugins/EnhancedFindDialog/searchHistory.py create mode 100644 addon/globalPlugins/EnhancedFindDialog/searchType.py diff --git a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py index fa9cf67..14c71d1 100644 --- a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py +++ b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py @@ -4,14 +4,19 @@ # 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 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 @@ -64,6 +69,8 @@ def patchCursorManager(): CursorManager.script_find = script_enhancedFind CursorManager.script_findNext = script_enhancedFindNext CursorManager.script_findPrevious = script_EnhancedFindPrevious + CursorManager.supportsRegexpSearch = supportsRegexpSearch + CursorManager.getSearchEntries = getSearchEntries def script_enhancedFind(self, gesture, reverse=False): @@ -75,19 +82,20 @@ 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), @@ -95,12 +103,13 @@ def script_enhancedFindNext(self, 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, @@ -108,13 +117,30 @@ def script_EnhancedFindPrevious(self, 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: + 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() @@ -123,20 +149,20 @@ def doFindText(cursorManagerInstance, text, 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) @@ -152,12 +178,27 @@ 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) + 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, caseSensitive=caseSensitive) + else: + res = info.find(searchTerm.text, reverse=reverse, caseSensitive=caseSensitive) + return res + + # Patch say all functionality. # Fix the patched script losing the reference to the decorated say all, # documentation and other necessary attributes for the gesture diff --git a/addon/globalPlugins/EnhancedFindDialog/guiHelper.py b/addon/globalPlugins/EnhancedFindDialog/guiHelper.py index 28b162d..fa539b0 100644 --- a/addon/globalPlugins/EnhancedFindDialog/guiHelper.py +++ b/addon/globalPlugins/EnhancedFindDialog/guiHelper.py @@ -8,12 +8,16 @@ import addonHandler import config import core +import gui from . import cursorManagerHelper +from .searchHistory import SearchHistory, SearchTerm +from .searchType import SearchType from gui import contextHelp, guiHelper import wx from logHandler import log +import re # this addon mostly complements NVDA functionalities. @@ -78,41 +82,6 @@ def setConfig(profile, key, value): profile[module][key] = value -class InvalidTypeName(Exception): - pass - - -@unique -class SearchType(Enum): - # Translators: normal - NORMAL = _("normal") - # Translators: regular expression - REGULAR_EXPRESSION = _("regular expression") - - @staticmethod - def getByIndex(index): - return list(SearchType)[index] - - @staticmethod - def getIndexByName(name): - log.debug(f"searching for {name}") - for index, type in enumerate(SearchType): - if type.name == name: - return index - raise InvalidTypeName(f"No variant with name '{name}' found in SearchType Enum") - - @staticmethod - def getByName(name): - for type in SearchType: - if type.name == name: - return type - raise InvalidTypeName(f"No variant with name '{name}' found in SearchType Enum") - - -def getSearchTypes(): - return [i.value for i in SearchType] - - class EnhancedFindDialog(contextHelp.ContextHelpMixin, wx.Dialog): # Noqa: E101 """A dialog used to specify text to find in a cursor manager. @@ -120,9 +89,10 @@ class EnhancedFindDialog(contextHelp.ContextHelpMixin, helpId = "SearchingForText" - def __init__(self, parent, cursorManager, profile, searchEntries, reverseSearch): + def __init__(self, parent, cursorManager, profile, reverseSearch): # Translators: Title of a dialog to find text. super().__init__(parent, title=__("Find")) + self.searchHistory = SearchHistory.get() self.reverseSearch = reverseSearch # if checkboxes change during this dialog we need to save the profile with the new values self._mustSaveProfile = False @@ -135,12 +105,16 @@ def __init__(self, parent, cursorManager, profile, searchEntries, reverseSearch) self.caseSensitivity = strToBool(getConfig(profile, "searchCaseSensitivity")) self.searchWrap = strToBool(getConfig(profile, "searchWrap")) self.searchType = SearchType.getByName(getConfig(profile, "searchType")).name - self.buildGui(searchEntries) + self.buildGui() self.updateUi() self.bindEvents() - def buildGui(self, searchEntries): + def buildGui(self): log.debug("called buildGui") + supportsRegexp = self.activeCursorManager.supportsRegexpSearch() + searchEntries = self.searchHistory.getItems(None if supportsRegexp else SearchType.NORMAL.name) + # if the search type is not supported, remove it from the list of search entries + searchTerms = [entry.text for entry in searchEntries] mainSizer = wx.BoxSizer(wx.VERTICAL) sHelper = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) @@ -149,7 +123,7 @@ def buildGui(self, searchEntries): textToFind = wx.StaticText(self, wx.ID_ANY, label=__("Type the text you wish to find")) hSizer.Add(textToFind, flag=wx.ALIGN_CENTER_VERTICAL) hSizer.AddSpacer(guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_HORIZONTAL) - self.findTextField = wx.ComboBox(self, wx.ID_ANY, choices=searchEntries, style=wx.CB_DROPDOWN) + self.findTextField = wx.ComboBox(self, wx.ID_ANY, choices=searchTerms, style=wx.CB_DROPDOWN) hSizer.Add(self.findTextField) sHelper.addItem(hSizer) # if there is a previous list of searched entries, make sure we @@ -161,7 +135,7 @@ def buildGui(self, searchEntries): self._searchTypeCtrl = searchTypeHelper.addItem(wx.RadioBox( self, # Translators: A radio box to select the search type. - label=_("Search type:"), choices=getSearchTypes(), + label=_("Search type:"), choices=SearchType.getSearchTypes(), majorDimension=1, style=wx.RA_SPECIFY_ROWS)) sHelper.addItem(searchTypeHelper) # Translators: An option in find dialog to perform case-sensitive search. @@ -185,6 +159,9 @@ def updateUi(self): log.debug("called update ui") self.caseSensitiveCheckBox.SetValue(self.caseSensitivity) self.searchWrapCheckBox.SetValue(self.searchWrap) + if not self.activeCursorManager.supportsRegexpSearch(): + self.searchType = SearchType.NORMAL.name + self._searchTypeCtrl.Enable(False) self._searchTypeCtrl.SetSelection(SearchType.getIndexByName(self.searchType)) if(self.searchType == SearchType.NORMAL.name): self.caseSensitiveCheckBox.Enable(True) @@ -206,47 +183,36 @@ def OnSearchTypeChanged(self, evt): self.updateUi() self.onStatChange(evt) - def updateSearchEntries(self, searchEntries, currentSearchTerm): - if not currentSearchTerm: - return - if not searchEntries: - searchEntries.insert(SEARCH_HISTORY_MOST_RECENT_INDEX, currentSearchTerm) - return - # we can not accept entries that differ only on text case - # because of a wxComboBox limitation on MS Windows - # see https://wxpython.org/Phoenix/docs/html/wx.ComboBox.html - # notice also that python 2 does not offer caseFold functionality - # so lower is the best we can have for comparing strings - for index, item in enumerate(searchEntries): - if(item.lower() == currentSearchTerm.lower()): - # if the user has selected a previous search term in the list or retyped - # an already listed term ,we need to make sure the - # current search term becomes the first item of the list, so that it - # will appear - # selected by default when the dialog is - # shown again. If the current search term - # differs from the current item only in case letters, we will choose to store the - # new search as we can not store both. - searchEntries.pop(index) - searchEntries.insert(SEARCH_HISTORY_MOST_RECENT_INDEX, currentSearchTerm) - return - # not yet listed. Save it. - if len(searchEntries) > SEARCH_HISTORY_LEAST_RECENT_INDEX: - self._truncateSearchHistory(searchEntries) - searchEntries.insert(SEARCH_HISTORY_MOST_RECENT_INDEX, currentSearchTerm) + def updateSearchHistory(self, currentSearchText): + if not currentSearchText: + return None + searchTerm = SearchTerm(currentSearchText, self.searchType) + self.searchHistory.append(searchTerm) + return searchTerm def onOk(self, evt): log.debug("called onOk") text = self.findTextField.GetValue() - # update the list of searched entries so that it can be exibited in the next find dialog call - self.updateSearchEntries(self.activeCursorManager._searchEntries, text) - + if self.searchType == SearchType.REGULAR_EXPRESSION.name: + try: + re.compile(text) + except re.error: + wx.CallAfter(gui.messageBox, + # Translators: Message shown when an invalid regular expression is entered. + _("The entered text is not a valid regular expression."), + cursorManagerHelper.FIND_ERROR_DIALOG_TITLE, wx.OK | wx.ICON_ERROR + ) # Noqa E101 + return + self.caseSensitive = self.caseSensitiveCheckBox.GetValue() self.searchWrap = self.searchWrapCheckBox.GetValue() self.searchType = SearchType.getByIndex(self._searchTypeCtrl.GetSelection()).name + # update the list of searched entries so that it can be exibited in the next find dialog call + searchTerm = self.updateSearchHistory(text) + self.updateProfile() # We must use core.callLater rather than wx.CallLater to @@ -254,7 +220,7 @@ def onOk(self, evt): # If it didn't, and it directly or indirectly called wx.Yield, it # could start executing NVDA's core pump from within the yield, causing recursion. core.callLater( - 100, cursorManagerHelper.doFindText, self.activeCursorManager, text, + 100, cursorManagerHelper.doFindText, self.activeCursorManager, searchTerm, caseSensitive=self.caseSensitive, searchWrap=self.searchWrap, reverse=self.reverseSearch) # Noqa: E101 self.Destroy() diff --git a/addon/globalPlugins/EnhancedFindDialog/searchHistory.py b/addon/globalPlugins/EnhancedFindDialog/searchHistory.py new file mode 100644 index 0000000..930f907 --- /dev/null +++ b/addon/globalPlugins/EnhancedFindDialog/searchHistory.py @@ -0,0 +1,54 @@ +# -*- coding: UTF-8 -*- +# A part of the EnhancedFind addon for NVDA +# Copyright (C) 2020 Marlon Sousa +# This file is covered by the GNU General Public License. +# See the file COPYING.txt for more details. + + +from logHandler import log + + +class SearchHistory: + _instance = None + + @classmethod + def get(cls): + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def __init__(self): + self._terms = [] + + def getMostRecent(self): + return self._terms[0] if self._terms else None + + def getItems(self, searchType=None): + log.debug(dir(self)) + if searchType is None: + return self._terms + return list(filter(lambda t: t.searchType == searchType, self._terms)) + + def getItemByText(self, text): + return next((term for term in self._terms if term.text == text), None) + + def append(self, term): + if not term.text: + return + if term in self._terms: + self._terms.remove(term) + self._terms.insert(0, term) + if len(self._terms) > 20: + self._terms.pop() + + +class SearchTerm: + def __init__(self, text, searchType): + self.text = text + self.searchType = searchType + + def __eq__(self, other): + # we can not accept entries that differ only on text case + # because of a wxComboBox limitation on MS Windows + # see https://wxpython.org/Phoenix/docs/html/wx.ComboBox.html + return self.text.casefold() == other.text.casefold() diff --git a/addon/globalPlugins/EnhancedFindDialog/searchType.py b/addon/globalPlugins/EnhancedFindDialog/searchType.py new file mode 100644 index 0000000..32e89cc --- /dev/null +++ b/addon/globalPlugins/EnhancedFindDialog/searchType.py @@ -0,0 +1,66 @@ +# -*- coding: UTF-8 -*- +# A part of the EnhancedFind addon for NVDA +# Copyright (C) 2020 Marlon Sousa +# This file is covered by the GNU General Public License. +# See the file COPYING.txt for more details. + +from enum import Enum, unique +import addonHandler + + +from logHandler import log + + +# this addon mostly complements NVDA functionalities. +# however, because the way NVDA works, when you use +# addon translation infrastructure by calling addonHandler.initTranslation() you loose access to the +# NVDA translated strings +# It is all or nothing: if you call addonHandler.initTranslation() the _(str) function looks for translations +# only in the addon localization files. +# if you don't, then the _(str) function looks for translations in the NVDA +# localization files, but not in the addon localization files +# In this addon, we add new elements to specific dialogs. Of course +# the translations for these elements are not available in nvda localization files. +# In the other hand, we use lots of strings that are already translated in NVDA +# So now we need to access the nvda localization files to have already defined translations, but we +# also need to access addon translation files to translate custom dialogs +# What we did is we saved the nvda translator to the __ variable +# while _ variable now is used to translate addon strings + +__ = _ +addonHandler.initTranslation() + + +class InvalidTypeName(Exception): + pass + + +@unique +class SearchType(Enum): + # Translators: normal + NORMAL = _("normal") + # Translators: regular expression + REGULAR_EXPRESSION = _("regular expression") + + @staticmethod + def getByIndex(index): + return list(SearchType)[index] + + @staticmethod + def getIndexByName(name): + log.debug(f"searching for {name}") + for index, type in enumerate(SearchType): + if type.name == name: + return index + raise InvalidTypeName(f"No variant with name '{name}' found in SearchType Enum") + + @staticmethod + def getByName(name): + for type in SearchType: + if type.name == name: + return type + raise InvalidTypeName(f"No variant with name '{name}' found in SearchType Enum") + + @staticmethod + def getSearchTypes(): + return [i.value for i in SearchType] From 058925ce61bb5e73e3411a06013055f2e62439c3 Mon Sep 17 00:00:00 2001 From: Marlon Sousa Date: Sun, 20 Apr 2025 18:47:32 -0300 Subject: [PATCH 05/12] forward regexp search --- .../EnhancedFindDialog/__init__.py | 2 ++ .../EnhancedFindDialog/cursorManagerHelper.py | 33 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/addon/globalPlugins/EnhancedFindDialog/__init__.py b/addon/globalPlugins/EnhancedFindDialog/__init__.py index 4a9cc95..e102b7e 100644 --- a/addon/globalPlugins/EnhancedFindDialog/__init__.py +++ b/addon/globalPlugins/EnhancedFindDialog/__init__.py @@ -52,3 +52,5 @@ def injectProcessing(self): # add methods to CursorManager class cursorManagerHelper.patchCursorManager() + # add methods to offsetsTextInfo class + cursorManagerHelper.patchOffsetsTextInfo() diff --git a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py index 14c71d1..342b844 100644 --- a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py +++ b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py @@ -12,6 +12,7 @@ from cursorManager import CursorManager import gui import textInfos.offsets +import textUtils from . import guiHelper from .searchHistory import SearchHistory from .searchType import SearchType @@ -73,6 +74,10 @@ def patchCursorManager(): CursorManager.getSearchEntries = getSearchEntries +def patchOffsetsTextInfo(): + setattr(textInfos.offsets.OffsetsTextInfo, "findRegexp", findRegexp) + + def script_enhancedFind(self, gesture, reverse=False): # #8566: We need this to be a modal dialog, but it mustn't block this script. def run(): @@ -136,6 +141,7 @@ def getMostRecentSearchTerm(): def doFindText(cursorManagerInstance, searchTerm, reverse=False, caseSensitive=False, searchWrap=False, willSayAllResume=False): # noqa: E101 + log.debug("doFindText, reverse=%s, caseSensitive=%s, searchWrap=%s", reverse, caseSensitive, searchWrap) if not searchTerm: return @@ -157,6 +163,7 @@ def doFindText(cursorManagerInstance, searchTerm, def performSearch(cursorManager, searchTerm, info, reverse, caseSensitive, wrapSearch): + log.info("performSearch, reverse=%s, caseSensitive=%s, wrapSearch=%s", 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: @@ -193,12 +200,36 @@ def find(cursorManager, searchTerm, info, reverse, caseSensitive): FIND_ERROR_DIALOG_TITLE, wx.OK | wx.ICON_ERROR ) return None - res = info.findRegexp(searchTerm.text, reverse=reverse, caseSensitive=caseSensitive) + 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: + log.info('backward') + log.info(0) + log.info(self._startOffset) + inText = self._getTextRange(0, self._startOffset) + matches = list(re.finditer(text, inText, re.UNICODE)) + if not matches: + return False + m = matches[-1] + else: + log.info('forward') + log.info(self._startOffset + 1) + log.info(self._getStoryLength()) + 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) + 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 From 8fe160886f5e00da3d15b392d8ba5d5885f09f34 Mon Sep 17 00:00:00 2001 From: Thiago Seus Date: Mon, 21 Apr 2025 07:10:34 -0300 Subject: [PATCH 06/12] fix reverse search by regular expression causing the cursor to go to the end of the text --- .../EnhancedFindDialog/cursorManagerHelper.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py index 342b844..cee999b 100644 --- a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py +++ b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py @@ -209,23 +209,17 @@ def find(cursorManager, searchTerm, info, reverse, caseSensitive): def findRegexp(self, text, reverse=False): if reverse: log.info('backward') - log.info(0) - log.info(self._startOffset) inText = self._getTextRange(0, self._startOffset) matches = list(re.finditer(text, inText, re.UNICODE)) if not matches: return False m = matches[-1] else: - log.info('forward') - log.info(self._startOffset + 1) - log.info(self._getStoryLength()) 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) - offset = self._startOffset + 1 + converter.strToEncodedOffsets(m.start()) + offset = m.start() self._startOffset = self._endOffset = offset return True From 48a4380e7c40128f79d447c64caed799c56765cf Mon Sep 17 00:00:00 2001 From: Thiago Seus Date: Wed, 23 Apr 2025 20:35:50 -0300 Subject: [PATCH 07/12] Fix reverse search (for real at this time) --- .../EnhancedFindDialog/cursorManagerHelper.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py index cee999b..21faca2 100644 --- a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py +++ b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py @@ -208,7 +208,6 @@ def find(cursorManager, searchTerm, info, reverse, caseSensitive): def findRegexp(self, text, reverse=False): if reverse: - log.info('backward') inText = self._getTextRange(0, self._startOffset) matches = list(re.finditer(text, inText, re.UNICODE)) if not matches: @@ -219,7 +218,14 @@ def findRegexp(self, text, reverse=False): m = re.search(text, inText, re.UNICODE) if not m: return False - offset = m.start() + + 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 From 25316513129af08d5dc393a40ae0722cd1bb7343 Mon Sep 17 00:00:00 2001 From: Thiago Seus Date: Wed, 23 Apr 2025 21:39:25 -0300 Subject: [PATCH 08/12] speak only the regexp match, not the entire line, when using regexp search --- .../EnhancedFindDialog/cursorManagerHelper.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py index 21faca2..a879419 100644 --- a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py +++ b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py @@ -150,8 +150,13 @@ def doFindText(cursorManagerInstance, searchTerm, 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: From 74a37d9a28bd2bf3fb48eda615e3355a00f91776 Mon Sep 17 00:00:00 2001 From: Thiago Seus Date: Wed, 23 Apr 2025 21:55:31 -0300 Subject: [PATCH 09/12] Fix search wrap not finding the regexp match if there is only one match --- .../EnhancedFindDialog/cursorManagerHelper.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py index a879419..a4951a3 100644 --- a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py +++ b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py @@ -168,7 +168,6 @@ def doFindText(cursorManagerInstance, searchTerm, def performSearch(cursorManager, searchTerm, info, reverse, caseSensitive, wrapSearch): - log.info("performSearch, reverse=%s, caseSensitive=%s, wrapSearch=%s", 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: @@ -190,7 +189,12 @@ def performSearch(cursorManager, searchTerm, info, reverse, caseSensitive, wrapS info = cursorManager.makeTextInfo(textInfos.POSITION_CARET).copy() info.expand(textInfos.UNIT_STORY) inText = info._get_text() - found = re.search(re.escape(searchTerm.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 From 3c04e3d70da1c8e87e751c8ce00a862cd2f39b8d Mon Sep 17 00:00:00 2001 From: Thiago Seus Date: Wed, 23 Apr 2025 23:11:28 -0300 Subject: [PATCH 10/12] fix problems reported by pre-commit --- .../EnhancedFindDialog/__init__.py | 2 +- .../EnhancedFindDialog/cursorManagerHelper.py | 32 +++++++++++-------- .../EnhancedFindDialog/guiHelper.py | 8 ++--- .../EnhancedFindDialog/searchHistory.py | 4 +-- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/addon/globalPlugins/EnhancedFindDialog/__init__.py b/addon/globalPlugins/EnhancedFindDialog/__init__.py index e102b7e..01b5410 100644 --- a/addon/globalPlugins/EnhancedFindDialog/__init__.py +++ b/addon/globalPlugins/EnhancedFindDialog/__init__.py @@ -52,5 +52,5 @@ def injectProcessing(self): # add methods to CursorManager class cursorManagerHelper.patchCursorManager() - # add methods to offsetsTextInfo class + # add methods to offsetsTextInfo class cursorManagerHelper.patchOffsetsTextInfo() diff --git a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py index a4951a3..1dbb215 100644 --- a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py +++ b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py @@ -95,7 +95,9 @@ def run(): def script_enhancedFindNext(self, gesture): mostRecentSearchTerm = getMostRecentSearchTerm() - if mostRecentSearchTerm is None or (mostRecentSearchTerm.searchType == SearchType.REGULAR_EXPRESSION.name and not self.supportsRegexpSearch()): + if (mostRecentSearchTerm is None + or (mostRecentSearchTerm.searchType == SearchType.REGULAR_EXPRESSION.name + and not self.supportsRegexpSearch())): self.script_find(gesture) return doFindText( @@ -109,7 +111,9 @@ def script_enhancedFindNext(self, gesture): def script_EnhancedFindPrevious(self, gesture): mostRecentSearchTerm = getMostRecentSearchTerm() - if mostRecentSearchTerm is None or (mostRecentSearchTerm.searchType == SearchType.REGULAR_EXPRESSION.name and not self.supportsRegexpSearch()): + if (mostRecentSearchTerm is None + or (mostRecentSearchTerm.searchType == SearchType.REGULAR_EXPRESSION.name + and not self.supportsRegexpSearch())): self.script_find(gesture, reverse=True) return doFindText( @@ -201,18 +205,18 @@ def performSearch(cursorManager, searchTerm, info, reverse, caseSensitive, wrapS 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 + 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): diff --git a/addon/globalPlugins/EnhancedFindDialog/guiHelper.py b/addon/globalPlugins/EnhancedFindDialog/guiHelper.py index fa539b0..e4b3b4e 100644 --- a/addon/globalPlugins/EnhancedFindDialog/guiHelper.py +++ b/addon/globalPlugins/EnhancedFindDialog/guiHelper.py @@ -4,7 +4,6 @@ # This file is covered by the GNU General Public License. # See the file COPYING.txt for more details. -from enum import Enum, unique import addonHandler import config import core @@ -194,16 +193,17 @@ def onOk(self, evt): log.debug("called onOk") text = self.findTextField.GetValue() if self.searchType == SearchType.REGULAR_EXPRESSION.name: - try: + try: re.compile(text) except re.error: - wx.CallAfter(gui.messageBox, + wx.CallAfter( + gui.messageBox, # Translators: Message shown when an invalid regular expression is entered. _("The entered text is not a valid regular expression."), cursorManagerHelper.FIND_ERROR_DIALOG_TITLE, wx.OK | wx.ICON_ERROR ) # Noqa E101 return - + self.caseSensitive = self.caseSensitiveCheckBox.GetValue() self.searchWrap = self.searchWrapCheckBox.GetValue() diff --git a/addon/globalPlugins/EnhancedFindDialog/searchHistory.py b/addon/globalPlugins/EnhancedFindDialog/searchHistory.py index 930f907..6fa85f7 100644 --- a/addon/globalPlugins/EnhancedFindDialog/searchHistory.py +++ b/addon/globalPlugins/EnhancedFindDialog/searchHistory.py @@ -30,8 +30,8 @@ def getItems(self, searchType=None): return list(filter(lambda t: t.searchType == searchType, self._terms)) def getItemByText(self, text): - return next((term for term in self._terms if term.text == text), None) - + return next((term for term in self._terms if term.text == text), None) + def append(self, term): if not term.text: return From a3374708c32afff005ed43bc1768e0ef3e5d177c Mon Sep 17 00:00:00 2001 From: Thiago Seus Date: Tue, 29 Apr 2025 12:47:58 -0300 Subject: [PATCH 11/12] update documentation --- README.md | 22 ++++++++++++++++++++++ README.tpl.md | 22 ++++++++++++++++++++++ buildVars.py | 2 +- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f795d2..7e45fc2 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/README.tpl.md b/README.tpl.md index 002ca1c..7fb0f9d 100644 --- a/README.tpl.md +++ b/README.tpl.md @@ -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 @@ -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. diff --git a/buildVars.py b/buildVars.py index 8aca028..ce4fe53 100644 --- a/buildVars.py +++ b/buildVars.py @@ -22,7 +22,7 @@ # version "addon_version" : "1.6.1", # Author(s) - "addon_author" : u"Marlon Brandão de Sousa ", + "addon_author" : u"Marlon Brandão de Sousa , Thiago Seus ", # URL for the add-on documentation support "addon_url" : "https://github.com/marlon-sousa/EnhancedFindDialog", # Documentation file name From 9ba59a286a63bde03ea3b0fa8513007145f902b3 Mon Sep 17 00:00:00 2001 From: Thiago Seus Date: Tue, 29 Apr 2025 12:52:03 -0300 Subject: [PATCH 12/12] update minor release --- README.md | 4 ++-- buildVars.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7e45fc2..dead384 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# EnhancedFindDialog for NVDA 1.6.1 +# EnhancedFindDialog for NVDA 1.7.0 Enhanced find dialog addon for NVDA, implementing search improvements: * search history @@ -8,7 +8,7 @@ Enhanced find dialog addon for NVDA, implementing search improvements: * 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 diff --git a/buildVars.py b/buildVars.py index ce4fe53..9e8dbb1 100644 --- a/buildVars.py +++ b/buildVars.py @@ -20,7 +20,7 @@ "addon_description" : _("""This addon introduces improvements to the NVDA find dialog. It is now possible to have your previous searches history available on a list during the NVDA session, which will enable you to quickly select and search for previous searched terms."""), # version - "addon_version" : "1.6.1", + "addon_version" : "1.7.0", # Author(s) "addon_author" : u"Marlon Brandão de Sousa , Thiago Seus ", # URL for the add-on documentation support