From b150669e161c5f2f12fc95d326698c11d86c9bfa Mon Sep 17 00:00:00 2001
From: Aaron Cannon <cannona@fireantproductions.com>
Date: Fri, 12 Sep 2014 16:20:55 -0500
Subject: [PATCH] Added Whole Word option to speech dictionary. #1704

---
 source/gui/settingsDialogs.py | 44 ++++++++++++++++++++++++++++++++-----------
 source/speechDictHandler.py   | 21 ++++++++++++++++-----
 user_docs/en/userGuide.t2t    |  6 +++++-
 3 files changed, 54 insertions(+), 17 deletions(-)

diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py
index c833bf0..aec837e 100644
--- a/source/gui/settingsDialogs.py
+++ b/source/gui/settingsDialogs.py
@@ -1153,6 +1153,15 @@ class DocumentFormattingDialog(SettingsDialog):
 		super(DocumentFormattingDialog, self).onOk(evt)
 
 class DictionaryEntryDialog(wx.Dialog):
+	TYPE_LABELS = {
+		# Translators: This is a label for an Entry Type radio button in add dictionary entry dialog.
+		speechDictHandler.ENTRY_TYPE_ANYWHERE: _("&Anywhere"),
+		# Translators: This is a label for an Entry Type radio button in add dictionary entry dialog.
+		speechDictHandler.ENTRY_TYPE_WORD: _("Whole &word"),
+		# Translators: This is a label for an Entry Type radio button in add dictionary entry dialog.
+		speechDictHandler.ENTRY_TYPE_REGEXP: _("&Regular expression")
+	}
+	TYPE_LABELS_ORDERING = (speechDictHandler.ENTRY_TYPE_ANYWHERE, speechDictHandler.ENTRY_TYPE_WORD, speechDictHandler.ENTRY_TYPE_REGEXP)
 
 	# Translators: This is the label for the edit dictionary entry dialog.
 	def __init__(self, parent, title=_("Edit Dictionary Entry")):
@@ -1174,17 +1183,30 @@ class DictionaryEntryDialog(wx.Dialog):
 		# Translators: This is a label for a checkbox in add dictionary entry dialog.
 		self.caseSensitiveCheckBox=wx.CheckBox(self,wx.NewId(),label=_("Case &sensitive"))
 		settingsSizer.Add(self.caseSensitiveCheckBox)
-		# Translators: This is a label for a checkbox in add dictionary entry dialog.
-		self.regexpCheckBox=wx.CheckBox(self,wx.NewId(),label=_("Regular &expression"))
-		settingsSizer.Add(self.regexpCheckBox)
+
+		# Translators: This is a label for a set of radio buttons in add dictionary entry dialog.
+		self.typeRadioBox=wx.RadioBox(self,wx.NewId(),label=_("&Type"), choices=[DictionaryEntryDialog.TYPE_LABELS[i] for i in DictionaryEntryDialog.TYPE_LABELS_ORDERING])
+		settingsSizer.Add(self.typeRadioBox)
 		mainSizer.Add(settingsSizer,border=20,flag=wx.LEFT|wx.RIGHT|wx.TOP)
 		buttonSizer=self.CreateButtonSizer(wx.OK|wx.CANCEL)
 		mainSizer.Add(buttonSizer,border=20,flag=wx.LEFT|wx.RIGHT|wx.BOTTOM)
 		mainSizer.Fit(self)
 		self.SetSizer(mainSizer)
+		self.setType(speechDictHandler.ENTRY_TYPE_ANYWHERE)
 		self.patternTextCtrl.SetFocus()
 
+	def getType(self):
+		typeRadioValue = self.typeRadioBox.GetSelection()
+		if typeRadioValue == wx.NOT_FOUND:
+			return speechDictHandler.ENTRY_TYPE_ANYWHERE
+		return DictionaryEntryDialog.TYPE_LABELS_ORDERING[typeRadioValue]
+
+	def setType(self, type):
+		self.typeRadioBox.SetSelection(DictionaryEntryDialog.TYPE_LABELS_ORDERING.index(type))
+
+
 class DictionaryDialog(SettingsDialog):
+	TYPE_LABELS = {t: l.replace("&", "") for t, l in DictionaryEntryDialog.TYPE_LABELS.iteritems()}
 
 	def __init__(self,parent,title,speechDict):
 		self.title = title
@@ -1209,11 +1231,11 @@ class DictionaryDialog(SettingsDialog):
 		self.dictList.InsertColumn(2,_("Replacement"),width=150)
 		# Translators: The label for a column in dictionary entries list used to identify whether the entry is case sensitive or not.
 		self.dictList.InsertColumn(3,_("case"),width=50)
-		# Translators: The label for a column in dictionary entries list used to identify whether the entry is a regular expression or not.
-		self.dictList.InsertColumn(4,_("Regexp"),width=50)
+		# Translators: The label for a column in dictionary entries list used to identify whether the entry is a regular expression, matches whole words, or matches anywhere.
+		self.dictList.InsertColumn(4,_("Type"),width=50)
 		self.offOn = (_("off"),_("on"))
 		for entry in self.tempSpeechDict:
-			self.dictList.Append((entry.comment,entry.pattern,entry.replacement,self.offOn[int(entry.caseSensitive)],self.offOn[int(entry.regexp)]))
+			self.dictList.Append((entry.comment,entry.pattern,entry.replacement,self.offOn[int(entry.caseSensitive)],DictionaryDialog.TYPE_LABELS[entry.type]))
 		self.editingIndex=-1
 		entriesSizer.Add(self.dictList,proportion=8)
 		settingsSizer.Add(entriesSizer)
@@ -1253,8 +1275,8 @@ class DictionaryDialog(SettingsDialog):
 		# Translators: This is the label for the add dictionary entry dialog.
 		entryDialog=DictionaryEntryDialog(self,title=_("Add Dictionary Entry"))
 		if entryDialog.ShowModal()==wx.ID_OK:
-			self.tempSpeechDict.append(speechDictHandler.SpeechDictEntry(entryDialog.patternTextCtrl.GetValue(),entryDialog.replacementTextCtrl.GetValue(),entryDialog.commentTextCtrl.GetValue(),bool(entryDialog.caseSensitiveCheckBox.GetValue()),bool(entryDialog.regexpCheckBox.GetValue())))
-			self.dictList.Append((entryDialog.commentTextCtrl.GetValue(),entryDialog.patternTextCtrl.GetValue(),entryDialog.replacementTextCtrl.GetValue(),self.offOn[int(entryDialog.caseSensitiveCheckBox.GetValue())],self.offOn[int(entryDialog.regexpCheckBox.GetValue())]))
+			self.tempSpeechDict.append(speechDictHandler.SpeechDictEntry(entryDialog.patternTextCtrl.GetValue(),entryDialog.replacementTextCtrl.GetValue(),entryDialog.commentTextCtrl.GetValue(),bool(entryDialog.caseSensitiveCheckBox.GetValue()),entryDialog.getType()))
+			self.dictList.Append((entryDialog.commentTextCtrl.GetValue(),entryDialog.patternTextCtrl.GetValue(),entryDialog.replacementTextCtrl.GetValue(),self.offOn[int(entryDialog.caseSensitiveCheckBox.GetValue())],DictionaryDialog.TYPE_LABELS[entryDialog.getType()]))
 			index=self.dictList.GetFirstSelected()
 			while index>=0:
 				self.dictList.Select(index,on=0)
@@ -1276,14 +1298,14 @@ class DictionaryDialog(SettingsDialog):
 		entryDialog.replacementTextCtrl.SetValue(self.tempSpeechDict[editIndex].replacement)
 		entryDialog.commentTextCtrl.SetValue(self.tempSpeechDict[editIndex].comment)
 		entryDialog.caseSensitiveCheckBox.SetValue(self.tempSpeechDict[editIndex].caseSensitive)
-		entryDialog.regexpCheckBox.SetValue(self.tempSpeechDict[editIndex].regexp)
+		entryDialog.setType(self.tempSpeechDict[editIndex].type)
 		if entryDialog.ShowModal()==wx.ID_OK:
-			self.tempSpeechDict[editIndex]=speechDictHandler.SpeechDictEntry(entryDialog.patternTextCtrl.GetValue(),entryDialog.replacementTextCtrl.GetValue(),entryDialog.commentTextCtrl.GetValue(),bool(entryDialog.caseSensitiveCheckBox.GetValue()),bool(entryDialog.regexpCheckBox.GetValue()))
+			self.tempSpeechDict[editIndex]=speechDictHandler.SpeechDictEntry(entryDialog.patternTextCtrl.GetValue(),entryDialog.replacementTextCtrl.GetValue(),entryDialog.commentTextCtrl.GetValue(),bool(entryDialog.caseSensitiveCheckBox.GetValue()),entryDialog.getType())
 			self.dictList.SetStringItem(editIndex,0,entryDialog.commentTextCtrl.GetValue())
 			self.dictList.SetStringItem(editIndex,1,entryDialog.patternTextCtrl.GetValue())
 			self.dictList.SetStringItem(editIndex,2,entryDialog.replacementTextCtrl.GetValue())
 			self.dictList.SetStringItem(editIndex,3,self.offOn[int(entryDialog.caseSensitiveCheckBox.GetValue())])
-			self.dictList.SetStringItem(editIndex,4,self.offOn[int(entryDialog.regexpCheckBox.GetValue())])
+			self.dictList.SetStringItem(editIndex,4,DictionaryDialog.TYPE_LABELS[entryDialog.getType()])
 			self.dictList.SetFocus()
 		entryDialog.Destroy()
 
diff --git a/source/speechDictHandler.py b/source/speechDictHandler.py
index 6f83115..c4ef45e 100644
--- a/source/speechDictHandler.py
+++ b/source/speechDictHandler.py
@@ -17,18 +17,29 @@ dictionaries = {}
 dictTypes = ("temp", "voice", "default", "builtin") # ordered by their priority E.G. voice specific speech dictionary is processed before the default
 speechDictsPath=os.path.join(globalVars.appArgs.configPath, "speechDicts")
 
+# Types of speech dictionary entries:
+ENTRY_TYPE_ANYWHERE = 0 # String can match anywhere
+ENTRY_TYPE_WORD = 2 # String must have word boundaries on both sides to match
+ENTRY_TYPE_REGEXP = 1 # Regular expression
+
 class SpeechDictEntry:
 
-	def __init__(self, pattern, replacement,comment,caseSensitive=True,regexp=False):
+	def __init__(self, pattern, replacement,comment,caseSensitive=True,type=ENTRY_TYPE_ANYWHERE):
 		self.pattern = pattern
 		flags = re.U
 		if not caseSensitive: flags|=re.IGNORECASE
-		tempPattern=pattern if regexp else re.escape(pattern)
+		if type == ENTRY_TYPE_REGEXP:
+			tempPattern = pattern
+		elif type == ENTRY_TYPE_WORD:
+			tempPattern = r"\b" + re.escape(pattern) + r"\b"
+		else:
+			tempPattern= re.escape(pattern)
+			type = ENTRY_TYPE_ANYWHERE # Insure sane values.
 		self.compiled = re.compile(tempPattern,flags)
 		self.replacement = replacement
 		self.comment=comment
 		self.caseSensitive=caseSensitive
-		self.regexp=regexp
+		self.type=type
 
 	def sub(self, text):
 		replacement=self.replacement
@@ -57,7 +68,7 @@ class SpeechDict(list):
 			else:
 				temp=line.split("\t")
 				if len(temp) ==4:
-					self.append(SpeechDictEntry(temp[0].replace(r'\#','#'),temp[1].replace(r'\#','#'),comment,bool(int(temp[2])),bool(int(temp[3]))))
+					self.append(SpeechDictEntry(temp[0].replace(r'\#','#'),temp[1].replace(r'\#','#'),comment,bool(int(temp[2])),int(temp[3])))
 					comment=""
 				else:
 					log.warning("can't parse line '%s'" % line)
@@ -77,7 +88,7 @@ class SpeechDict(list):
 		for entry in self:
 			if entry.comment:
 				file.write("#%s\r\n"%entry.comment)
-			file.write("%s\t%s\t%s\t%s\r\n"%(entry.pattern.replace('#',r'\#'),entry.replacement.replace('#',r'\#'),int(entry.caseSensitive),int(entry.regexp)))
+			file.write("%s\t%s\t%s\t%s\r\n"%(entry.pattern.replace('#',r'\#'),entry.replacement.replace('#',r'\#'),int(entry.caseSensitive),entry.type))
 		file.close()
 
 	def sub(self, text):
diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t
index 0560398..26e8c3d 100644
--- a/user_docs/en/userGuide.t2t
+++ b/user_docs/en/userGuide.t2t
@@ -1069,7 +1069,11 @@ You may also want to type a description of the rule in the Comment field (someth
 NVDA's speech dictionaries however are much more powerful than simple word replacement.
 The Add rule dialog also contains a checkbox to say whether or not you want the rule to be case sensitive (meaning that NVDA should care whether the characters are uppercase or lowercase.
 NVDA ignores case by default).
-Another checkbox allows you to state whether your pattern is a "Regular expression".
+
+Finally, a set of radio buttons allows you to tell NVDA whether your pattern should match anywhere, as a complete word, or that it should be treated as a "Regular expression".
+Setting the pattern to match as a whole word means that the replacement will only be made if a non-word character (or no character at all) comes both before and after the pattern.
+Thus, using our earlier example of replacing the word "bird" with "frog", if you were to make this a whole word replacement, it would not match "birds" or "bluebird".
+
 A regular expression is a pattern containing special symbols that allow you to match on more than one character at a time, or match on just numbers, or just letters, as a few examples.
 Regular expressions are not covered in this user guide, but there are many tutorials on the web which can provide you with more information.
 
-- 
1.9.4.msysgit.1

