Repository: nvaccess/nvda

Issue:
Using the mouse in the symbol pronunciation list does not work well
**Case 1:**
Add a new symbol, eg 'test'
Click in the replacement edit field (notice that there is no cursor)
Try typing (notice that the field receives no text)

_Clicking in the replacement edit field should result in a cursor, and it should receive text._

**Case 2:**
Select 'test' from the symbol list
Press alt+r to focus the replacement edit field (notice that there is no cursor)
Try typing (text IS received)
Click in the symbol list (notice that the 'test' field is not updated)

_Using the mouse to remove focus from the replacement edit field should result in the symbol item being updated._

These issues make the dialog difficult to use with a mouse.



Discussion:

Resolving patch:
diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py
--- a/source/gui/settingsDialogs.py
+++ b/source/gui/settingsDialogs.py
@@ -1635,32 +1635,53 @@ def makeSettings(self, settingsSizer):
 		sizer.Add(self.symbolsList)
 		settingsSizer.Add(sizer)
 
-		# Translators: The label for the edit field in symbol pronunciation dialog to change the pronunciation of a symbol.
-		changeSizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Change symbol")), wx.VERTICAL)
+		# Translators: The label for the group of controls in symbol pronunciation dialog to change the pronunciation of a symbol.
+		changeSizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Change selected symbol")), 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
+		# to allow the default handling to take place. The command events are, however, normally not skipped as usually 
+		# a single command such as a button click or menu item selection must only be processed by one handler."
+		def skipEventAndCall(handler):
+			def wrapWithEventSkip(event):
+				if event:
+					event.Skip()
+				return handler()
+			return wrapWithEventSkip
+
 		sizer = wx.BoxSizer(wx.HORIZONTAL)
+		# Translators: The label for the edit field in symbol pronunciation dialog to change the replacement text of a symbol.
 		sizer.Add(wx.StaticText(self, wx.ID_ANY, _("&Replacement")))
 		self.replacementEdit = wx.TextCtrl(self, wx.ID_ANY)
-		self.replacementEdit.Bind(wx.EVT_KILL_FOCUS, self.onSymbolEdited)
+		self.replacementEdit.Bind(wx.EVT_TEXT, skipEventAndCall(self.onSymbolEdited))
 		sizer.Add(self.replacementEdit)
 		changeSizer.Add(sizer)
+
 		sizer = wx.BoxSizer(wx.HORIZONTAL)
 		# Translators: The label for the combo box in symbol pronunciation dialog to change the speech level of a symbol.
 		sizer.Add(wx.StaticText(self, wx.ID_ANY, _("&Level")))
 		symbolLevelLabels = characterProcessing.SPEECH_SYMBOL_LEVEL_LABELS
 		self.levelList = wx.Choice(self, wx.ID_ANY,choices=[
 			symbolLevelLabels[level] for level in characterProcessing.SPEECH_SYMBOL_LEVELS])
-		self.levelList.Bind(wx.EVT_KILL_FOCUS, self.onSymbolEdited)
+		self.levelList.Bind(wx.EVT_CHOICE, skipEventAndCall(self.onSymbolEdited))
 		sizer.Add(self.levelList)
 		changeSizer.Add(sizer)
+
 		sizer = wx.BoxSizer(wx.HORIZONTAL)
 		# Translators: The label for the combo box in symbol pronunciation dialog to change when a symbol is sent to the synthesizer.
 		sizer.Add(wx.StaticText(self, wx.ID_ANY, _("&Send actual symbol to synthesizer")))
 		symbolPreserveLabels = characterProcessing.SPEECH_SYMBOL_PRESERVE_LABELS
 		self.preserveList = wx.Choice(self, wx.ID_ANY,choices=[
 			symbolPreserveLabels[mode] for mode in characterProcessing.SPEECH_SYMBOL_PRESERVES])
-		self.preserveList.Bind(wx.EVT_KILL_FOCUS, self.onSymbolEdited)
+		self.preserveList.Bind(wx.EVT_CHOICE, skipEventAndCall(self.onSymbolEdited))
 		sizer.Add(self.preserveList)
 		changeSizer.Add(sizer)
+
+		# disable the "change symbol" controls until a valid item is selected.
+		self.replacementEdit.Disable()
+		self.levelList.Disable()
+		self.preserveList.Disable()
+
 		settingsSizer.Add(changeSizer)
 		entryButtonsSizer=wx.BoxSizer(wx.HORIZONTAL)
 		# Translators: The label for a button in the Symbol Pronunciation dialog to add a new symbol.
@@ -1684,26 +1705,30 @@ def updateListItem(self, item, symbol):
 		self.symbolsList.SetStringItem(item, 2, characterProcessing.SPEECH_SYMBOL_LEVEL_LABELS[symbol.level])
 		self.symbolsList.SetStringItem(item, 3, characterProcessing.SPEECH_SYMBOL_PRESERVE_LABELS[symbol.preserve])
 
-	def onSymbolEdited(self, evt):
-		if self.editingItem is None:
-			return
-		# Update the symbol the user was just editing.
-		item = self.editingItem
-		symbol = self.symbols[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 onSymbolEdited(self):
+		if self.editingItem is not None:
+			# Update the symbol the user was just editing.
+			item = self.editingItem
+			symbol = self.symbols[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]
 		self.editingItem = item
-		self.replacementEdit.Value = symbol.replacement
+		# ChangeValue and Selection property used because they do not cause EVNT_CHANGED to be fired.
+		self.replacementEdit.ChangeValue(symbol.replacement)
 		self.levelList.Selection = characterProcessing.SPEECH_SYMBOL_LEVELS.index(symbol.level)
 		self.preserveList.Selection = characterProcessing.SPEECH_SYMBOL_PRESERVES.index(symbol.preserve)
 		self.removeButton.Enabled = not self.symbolProcessor.isBuiltin(symbol.identifier)
+		self.replacementEdit.Enable()
+		self.levelList.Enable()
+		self.preserveList.Enable()
+		evt.Skip()
 
 	def onListChar(self, evt):
 		if evt.KeyCode == wx.WXK_RETURN:
@@ -1712,7 +1737,6 @@ def onListChar(self, evt):
 			# Therefore, we must catch the enter key here.
 			# Activate the OK button.
 			self.ProcessEvent(wx.CommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, wx.ID_OK))
-
 		else:
 			evt.Skip()
 
@@ -1764,7 +1788,7 @@ def OnRemoveClick(self, evt):
 		self.symbolsList.SetFocus()
 
 	def onOk(self, evt):
-		self.onSymbolEdited(None)
+		self.onSymbolEdited()
 		self.editingItem = None
 		for symbol in self.pendingRemovals.itervalues():
 			self.symbolProcessor.deleteSymbol(symbol)


Test patch: