diff --git a/README.md b/README.md index 5f795d2..dead384 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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/addon/globalPlugins/EnhancedFindDialog/__init__.py b/addon/globalPlugins/EnhancedFindDialog/__init__.py index cb5c5b9..01b5410 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 @@ -51,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 fa9cf67..1dbb215 100644 --- a/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py +++ b/addon/globalPlugins/EnhancedFindDialog/cursorManagerHelper.py @@ -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 @@ -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): @@ -75,19 +87,22 @@ 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 +110,15 @@ 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,35 +126,58 @@ 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: + 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) @@ -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 diff --git a/addon/globalPlugins/EnhancedFindDialog/guiHelper.py b/addon/globalPlugins/EnhancedFindDialog/guiHelper.py index ef79c38..e4b3b4e 100644 --- a/addon/globalPlugins/EnhancedFindDialog/guiHelper.py +++ b/addon/globalPlugins/EnhancedFindDialog/guiHelper.py @@ -7,11 +7,18 @@ 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. # however, because the way NVDA works, when you use # addon translation infrastructure by calling addonHandler.initTranslation() you loose access to the @@ -81,9 +88,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 @@ -93,9 +101,19 @@ 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() + self.updateUi() + self.bindEvents() + + 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) @@ -104,93 +122,124 @@ def __init__(self, parent, cursorManager, profile, searchEntries, reverseSearch) 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 # 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=SearchType.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 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 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) + 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._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 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 - 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 + + # 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 # 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, searchTerm, + 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): diff --git a/addon/globalPlugins/EnhancedFindDialog/searchHistory.py b/addon/globalPlugins/EnhancedFindDialog/searchHistory.py new file mode 100644 index 0000000..6fa85f7 --- /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] diff --git a/buildVars.py b/buildVars.py index 8aca028..9e8dbb1 100644 --- a/buildVars.py +++ b/buildVars.py @@ -20,9 +20,9 @@ "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 ", + "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