Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add filtering functionality to the symbols list #8790

Merged
merged 17 commits into from Apr 23, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions source/gui/guiHelper.py
Expand Up @@ -152,6 +152,7 @@ def __init__(self, parent, labelText, wxCtrlClass, **kwargs):
@param labelText: The text to associate with a wx control.
@type labelText: string
@param wxCtrlClass: The class to associate with the label, eg: wx.TextCtrl
@type wxCtrlClass: class
LeonarddeR marked this conversation as resolved.
Show resolved Hide resolved
@param kwargs: The keyword arguments used to instantiate the wxCtrlClass
"""
object.__init__(self)
Expand Down
61 changes: 53 additions & 8 deletions source/gui/nvdaControls.py
Expand Up @@ -11,20 +11,65 @@
import oleacc
import winUser
import winsound
try:
# Python 3 import
from collections.abc import Callable
except ImportError:
# Python 2 import
from collections import Callable

class AutoWidthColumnListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
"""
A list control that allows you to specify a column to resize to take up the remaining width of a wx.ListCtrl
A list control that allows you to specify a column to resize to take up the remaining width of a wx.ListCtrl.
It also changes L{OnGetItemText} to call an optionally provided callable,
and adds a l{sendListItemFocusedEvent} method.
"""
def __init__(self, parent, id=wx.ID_ANY, autoSizeColumnIndex="LAST", pos=wx.DefaultPosition, size=wx.DefaultSize, style=0):

def __init__(
self,
parent,
id=wx.ID_ANY,
autoSizeColumn="LAST",
itemTextCallable=None,
pos=wx.DefaultPosition,
size=wx.DefaultSize,
style=0
):
""" initialiser
Takes the same parameter as a wx.ListCtrl with the following additions:
autoSizeColumnIndex - defaults to "LAST" which results in the last column being resized. Pass the index of
the column to be resized.
@param autoSizeColumn: defaults to "LAST" which results in the last column being resized.
Pass the column number to be resized, valid values: 1 to N
@type autoSizeColumn: int
@param itemTextCallable: A callable to be called to get the item text for a particular item's column in the list.
It should accept the same parameters as L{OnGetItemText},
@type itemTextCallable: L{callable}
"""
wx.ListCtrl.__init__(self, parent, id, pos, size, style)
if itemTextCallable is not None:
if not isinstance(itemTextCallable, Callable):
raise TypeError("itemTextCallable should be None or a callable")
self._itemTextCallable = itemTextCallable
else:
self._itemTextCallable = self._super_itemTextCallable
wx.ListCtrl.__init__(self, parent, id=id, pos=pos, size=size, style=style)
listmix.ListCtrlAutoWidthMixin.__init__(self)
self.setResizeColumn(autoSizeColumnIndex)
self.setResizeColumn(autoSizeColumn)
self.Bind(wx.EVT_WINDOW_DESTROY, source=self, id=self.GetId, handler=self._onDestroy)

def _onDestroy(self, evt):
evt.Skip()
self._itemTextCallable = None

def _super_itemTextCallable(self, item, column):
return super(AutoWidthColumnListCtrl, self).OnGetItemText(item, column)

def OnGetItemText(self, item, column):
return self._itemTextCallable(item, column)

def sendListItemFocusedEvent(self, index):
evt = wx.ListEvent(wx.wxEVT_LIST_ITEM_FOCUSED, self.Id)
evt.EventObject = self
evt.Index = index
self.ProcessEvent(evt)

class SelectOnFocusSpinCtrl(wx.SpinCtrl):
"""
Expand Down Expand Up @@ -140,10 +185,10 @@ class AutoWidthColumnCheckListCtrl(AutoWidthColumnListCtrl, listmix.CheckListCtr
This event is only fired when an item is toggled with the mouse or keyboard.
"""

def __init__(self, parent, id=wx.ID_ANY, autoSizeColumnIndex="LAST", pos=wx.DefaultPosition, size=wx.DefaultSize, style=0,
def __init__(self, parent, id=wx.ID_ANY, autoSizeColumn="LAST", pos=wx.DefaultPosition, size=wx.DefaultSize, style=0,
check_image=None, uncheck_image=None, imgsz=(16, 16)
):
AutoWidthColumnListCtrl.__init__(self, parent, id=id, pos=pos, size=size, style=style)
AutoWidthColumnListCtrl.__init__(self, parent, id=id, pos=pos, size=size, style=style, autoSizeColumn=autoSizeColumn)
listmix.CheckListCtrlMixin.__init__(self, check_image, uncheck_image, imgsz)
# Register object with COM to fix accessibility bugs in wx.
self.server = ListCtrlAccPropServer(self)
Expand Down
180 changes: 142 additions & 38 deletions source/gui/settingsDialogs.py
Expand Up @@ -423,7 +423,7 @@ def makeSettings(self, settingsSizer):

self.catListCtrl = nvdaControls.AutoWidthColumnListCtrl(
self,
autoSizeColumnIndex=0,
autoSizeColumn=1,
size=catListDim,
style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_NO_HEADER
)
Expand Down Expand Up @@ -2825,32 +2825,57 @@ def __init__(self,parent):
# Translators: This is the label for the symbol pronunciation dialog.
# %s is replaced by the language for which symbol pronunciation is being edited.
self.title = _("Symbol Pronunciation (%s)")%languageHandler.getLanguageDescription(self.symbolProcessor.locale)
super(SpeechSymbolsDialog, self).__init__(parent)
super(SpeechSymbolsDialog, self).__init__(
parent,
resizeable=True,
)

def makeSettings(self, settingsSizer):
symbols = self.symbols = [copy.copy(symbol) for symbol in self.symbolProcessor.computedSymbols.itervalues()]
self.filteredSymbols = self.symbols = [
copy.copy(symbol) for symbol in self.symbolProcessor.computedSymbols.itervalues()
]
self.pendingRemovals = {}

sHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer)
# Translators: The label of a text field to search for symbols in the speech symbols dialog.
filterText = pgettext("speechSymbols", "&Filter by:")
self.filterEdit = sHelper.addLabeledControl(
labelText = filterText,
wxCtrlClass=wx.TextCtrl,
size=self.scaleSize((310, -1)),
)
self.filterEdit.Bind(wx.EVT_TEXT, self.onFilterEditTextChange)

# Translators: The label for symbols list in symbol pronunciation dialog.
symbolsText = _("&Symbols")
self.symbolsList = sHelper.addLabeledControl(symbolsText, nvdaControls.AutoWidthColumnListCtrl, autoSizeColumnIndex=0, style=wx.LC_REPORT | wx.LC_SINGLE_SEL)
self.symbolsList = sHelper.addLabeledControl(
symbolsText,
nvdaControls.AutoWidthColumnListCtrl,
autoSizeColumn=2, # The replacement column is likely to need the most space
itemTextCallable=self.getItemTextForList,
style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VIRTUAL
)

# Translators: The label for a column in symbols list used to identify a symbol.
self.symbolsList.InsertColumn(0, _("Symbol"))
self.symbolsList.InsertColumn(0, _("Symbol"), width=self.scaleSize(150))
self.symbolsList.InsertColumn(1, _("Replacement"))
# Translators: The label for a column in symbols list used to identify a symbol's speech level (either none, some, most, all or character).
self.symbolsList.InsertColumn(2, _("Level"))
# Translators: The label for a column in symbols list which specifies when the actual symbol will be sent to the synthesizer (preserved).
# See the "Punctuation/Symbol Pronunciation" section of the User Guide for details.
self.symbolsList.InsertColumn(3, _("Preserve"))
for symbol in symbols:
item = self.symbolsList.Append((symbol.displayName,))
self.updateListItem(item, symbol)
self.symbolsList.Bind(wx.EVT_LIST_ITEM_FOCUSED, self.onListItemFocused)

# Translators: The label for the group of controls in symbol pronunciation dialog to change the pronunciation of a symbol.
changeSymbolText = _("Change selected symbol")
changeSymbolHelper = sHelper.addItem(guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(wx.StaticBox(self, label=changeSymbolText), wx.VERTICAL)))
changeSymbolHelper = sHelper.addItem(guiHelper.BoxSizerHelper(
parent=self,
sizer=wx.StaticBoxSizer(
parent=self,
label=changeSymbolText,
orient=wx.VERTICAL,
)
))

# Used to ensure that event handlers call Skip(). Not calling skip can cause focus problems for controls. More
# generally the advice on the wx documentation is: "In general, it is recommended to skip all non-command events
Expand All @@ -2865,7 +2890,11 @@ def wrapWithEventSkip(event):

# Translators: The label for the edit field in symbol pronunciation dialog to change the replacement text of a symbol.
replacementText = _("&Replacement")
self.replacementEdit = changeSymbolHelper.addLabeledControl(replacementText, wx.TextCtrl)
self.replacementEdit = sHelper.addLabeledControl(
labelText=replacementText,
wxCtrlClass=wx.TextCtrl,
size=self.scaleSize((300, -1)),
)
self.replacementEdit.Bind(wx.EVT_TEXT, skipEventAndCall(self.onSymbolEdited))

# Translators: The label for the combo box in symbol pronunciation dialog to change the speech level of a symbol.
Expand All @@ -2882,12 +2911,6 @@ def wrapWithEventSkip(event):
self.preserveList = changeSymbolHelper.addLabeledControl(preserveText, wx.Choice, choices=preserveChoices)
self.preserveList.Bind(wx.EVT_CHOICE, skipEventAndCall(self.onSymbolEdited))

# disable the "change symbol" controls until a valid item is selected.
self.replacementEdit.Disable()
self.levelList.Disable()
self.preserveList.Disable()


bHelper = sHelper.addItem(guiHelper.ButtonHelper(orientation=wx.HORIZONTAL))
# Translators: The label for a button in the Symbol Pronunciation dialog to add a new symbol.
addButton = bHelper.addButton(self, label=_("&Add"))
Expand All @@ -2899,30 +2922,87 @@ def wrapWithEventSkip(event):
addButton.Bind(wx.EVT_BUTTON, self.OnAddClick)
self.removeButton.Bind(wx.EVT_BUTTON, self.OnRemoveClick)

self.editingItem = None
# Populate the unfiltered list with symbols.
self.filter()

def postInit(self):
size = self.GetBestSize()
self.SetSizeHints(
minW=size.GetWidth(),
minH=size.GetHeight(),
maxH=size.GetHeight(),
)
self.symbolsList.SetFocus()

def updateListItem(self, item, symbol):
self.symbolsList.SetItem(item, 1, symbol.replacement)
self.symbolsList.SetItem(item, 2, characterProcessing.SPEECH_SYMBOL_LEVEL_LABELS[symbol.level])
self.symbolsList.SetItem(item, 3, characterProcessing.SPEECH_SYMBOL_PRESERVE_LABELS[symbol.preserve])
def filter(self, filterText=''):
NONE_SELECTED = -1
previousSelectionValue = None
previousIndex = self.symbolsList.GetFirstSelected() # may return NONE_SELECTED
if previousIndex != NONE_SELECTED:
previousSelectionValue = self.filteredSymbols[previousIndex]

if not filterText:
self.filteredSymbols = self.symbols
else:
# Do case-insensitive matching by lowering both filterText and each symbols's text.
filterText = filterText.lower()
self.filteredSymbols = [
symbol for symbol in self.symbols
if filterText in symbol.displayName.lower()
or filterText in symbol.replacement.lower()
]
self.symbolsList.ItemCount = len(self.filteredSymbols)

# sometimes filtering may result in an empty list.
if not self.symbolsList.ItemCount:
self.editingItem = None
# disable the "change symbol" controls, since there are no items in the list.
self.replacementEdit.Disable()
self.levelList.Disable()
self.preserveList.Disable()
self.removeButton.Disable()
return # exit early, no need to select an item.

# If there was a selection before filtering, try to preserve it
newIndex = 0 # select first item by default.
if previousSelectionValue:
try:
newIndex = self.filteredSymbols.index(previousSelectionValue)
except ValueError:
pass

# Change the selection
self.symbolsList.Select(newIndex)
self.symbolsList.Focus(newIndex)
# We don't get a new focus event with the new index.
self.symbolsList.sendListItemFocusedEvent(newIndex)

def getItemTextForList(self, item, column):
symbol = self.filteredSymbols[item]
if column == 0:
return symbol.displayName
elif column == 1:
return symbol.replacement
elif column == 2:
return characterProcessing.SPEECH_SYMBOL_LEVEL_LABELS[symbol.level]
elif column == 3:
return characterProcessing.SPEECH_SYMBOL_PRESERVE_LABELS[symbol.preserve]
else:
raise ValueError("Unknown column: %d" % column)

def onSymbolEdited(self):
if self.editingItem is not None:
# Update the symbol the user was just editing.
item = self.editingItem
symbol = self.symbols[item]
symbol = self.filteredSymbols[item]
symbol.replacement = self.replacementEdit.Value
symbol.level = characterProcessing.SPEECH_SYMBOL_LEVELS[self.levelList.Selection]
symbol.preserve = characterProcessing.SPEECH_SYMBOL_PRESERVES[self.preserveList.Selection]
self.updateListItem(item, symbol)

def onListItemFocused(self, evt):
# Update the editing controls to reflect the newly selected symbol.
item = evt.GetIndex()
symbol = self.symbols[item]
symbol = self.filteredSymbols[item]
self.editingItem = item
# ChangeValue and Selection property used because they do not cause EVNT_CHANGED to be fired.
self.replacementEdit.ChangeValue(symbol.replacement)
Expand All @@ -2941,6 +3021,9 @@ def OnAddClick(self, evt):
identifier = entryDialog.identifierTextCtrl.GetValue()
if not identifier:
return
# Clean the filter, so we can select the new entry.
self.filterEdit.Value=""
self.filter()
for index, symbol in enumerate(self.symbols):
if identifier == symbol.identifier:
# Translators: An error reported in the Symbol Pronunciation dialog when adding a symbol that is already present.
Expand All @@ -2960,25 +3043,36 @@ def OnAddClick(self, evt):
addedSymbol.level = characterProcessing.SYMLVL_ALL
addedSymbol.preserve = characterProcessing.SYMPRES_NEVER
self.symbols.append(addedSymbol)
item = self.symbolsList.Append((addedSymbol.displayName,))
self.updateListItem(item, addedSymbol)
self.symbolsList.Select(item)
self.symbolsList.Focus(item)
self.symbolsList.ItemCount = len(self.symbols)
index = self.symbolsList.ItemCount - 1
self.symbolsList.Select(index)
self.symbolsList.Focus(index)
# We don't get a new focus event with the new index.
self.symbolsList.sendListItemFocusedEvent(index)
self.symbolsList.SetFocus()

def OnRemoveClick(self, evt):
index = self.symbolsList.GetFirstSelected()
symbol = self.symbols[index]
symbol = self.filteredSymbols[index]
self.pendingRemovals[symbol.identifier] = symbol
# Deleting from self.symbolsList focuses the next item before deleting,
# so it must be done *before* we delete from self.symbols.
self.symbolsList.DeleteItem(index)
del self.symbols[index]
index = min(index, self.symbolsList.ItemCount - 1)
self.symbolsList.Select(index)
self.symbolsList.Focus(index)
# We don't get a new focus event with the new index, so set editingItem.
self.editingItem = index
del self.filteredSymbols[index]
if self.filteredSymbols is not self.symbols:
self.symbols.remove(symbol)
self.symbolsList.ItemCount = len(self.filteredSymbols)
# sometimes removing may result in an empty list.
if not self.symbolsList.ItemCount:
self.editingItem = None
# disable the "change symbol" controls, since there are no items in the list.
self.replacementEdit.Disable()
self.levelList.Disable()
self.preserveList.Disable()
self.removeButton.Disable()
else:
index = min(index, self.symbolsList.ItemCount - 1)
self.symbolsList.Select(index)
self.symbolsList.Focus(index)
# We don't get a new focus event with the new index.
self.symbolsList.sendListItemFocusedEvent(index)
self.symbolsList.SetFocus()

def onOk(self, evt):
Expand All @@ -2997,6 +3091,16 @@ def onOk(self, evt):
characterProcessing._localeSpeechSymbolProcessors.invalidateLocaleData(self.symbolProcessor.locale)
super(SpeechSymbolsDialog, self).onOk(evt)

def _refreshVisibleItems(self):
count = self.symbolsList.GetCountPerPage()
first = self.symbolsList.GetTopItem()
self.symbolsList.RefreshItems(first, first+count)

def onFilterEditTextChange(self, evt):
self.filter(self.filterEdit.Value)
self._refreshVisibleItems()
evt.Skip()

class InputGesturesDialog(SettingsDialog):
# Translators: The title of the Input Gestures dialog where the user can remap input gestures for commands.
title = _("Input Gestures")
Expand Down
1 change: 1 addition & 0 deletions user_docs/en/userGuide.t2t
Expand Up @@ -1721,6 +1721,7 @@ The language for which symbol pronunciation is being edited will be shown in the
Note that this dialog respects the "Trust voice's language for processing symbols and characters" option found in the [Speech category #SpeechSettings] of the [NVDA Settings #NVDASettings] dialog; i.e. it uses the voice language rather than the NVDA global language setting when this option is enabled.

To change a symbol, first select it in the Symbols list.
You can filter the symbols by entering the symbol or a part of the symbol's replacement into the Filter by edit box.

- The Replacement field allows you to change the text that should be spoken in place of this symbol.
- Using the Level field, you can adjust the lowest symbol level at which this symbol should be spoken.
Expand Down