# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: aleksey_s@voliacable.com-20081109124412-\ # tg0eetlrnqqau4rq # target_branch: http://bzr.nvaccess.org/nvda/main/ # testament_sha1: 783cc2236738248b00e6ba319a6cb3a0c3e333c8 # timestamp: 2008-11-09 15:17:55 +0200 # base_revision_id: jamie@jantrid.net-20081109114527-f0kqf0o8k8713bsc # # Begin patch === modified file 'source/core.py' --- source/core.py 2008-10-01 10:42:39 +0000 +++ source/core.py 2008-10-31 20:22:35 +0000 @@ -19,7 +19,7 @@ from logHandler import log def resetConfiguration(): - """Loads the configuration, installs the correct language support and initialises audio so that it will use the configured synth and speech settings. + """Loads the configuration, installs the correct language support and initializes audio so that it will use the configured synth and speech settings. """ import config import speech @@ -69,7 +69,6 @@ log.info("Using Windows version %r" % (sys.getwindowsversion(),)) log.info("Using Python version %s"%sys.version) log.info("Using comtypes version %s"%comtypes.__version__) - log.debug("Creating wx application instance") import speechDictHandler log.debug("Speech Dictionary processing") speechDictHandler.initialize() @@ -81,6 +80,7 @@ speech.speakMessage(_("Loading subsystems, please wait...")) import wx log.info("Using wx version %s"%wx.version()) + log.debug("Creating wx application instance") app = wx.App(redirect=False) # HACK: wx currently raises spurious assertion failures when a timer is stopped but there is already an event in the queue for that timer. # Unfortunately, these assertion exceptions are raised in the middle of other code, which causes problems. === modified file 'source/gui/__init__.py' --- source/gui/__init__.py 2008-10-22 16:50:30 +0000 +++ source/gui/__init__.py 2008-11-09 00:42:58 +0000 @@ -17,6 +17,7 @@ import queueHandler import core from settingsDialogs import * +from smartDictManager import SmartDictManagerDialog import speechDictHandler import languageHandler import logViewer @@ -168,6 +169,9 @@ def onTemporaryDictionaryCommand(self,evt): self._popupSettingsDialog(DictionaryDialog,_("Temporary dictionary"),speechDictHandler.dictionaries["temp"]) + def onSmartDictManagerCommand(self,evt): + self._popupSettingsDialog(SmartDictManagerDialog) + def onExitCommand(self, evt): global isExitDialog canExit=False @@ -267,6 +271,8 @@ self.Bind(wx.EVT_MENU, frame.onVoiceDictionaryCommand, item) item = subMenu_speechDicts.Append(wx.ID_ANY,_("&Temporary dictionary..."),_("dialog where you can set temporary dictionary by adding dictionary entries to the edit box")) self.Bind(wx.EVT_MENU, frame.onTemporaryDictionaryCommand, item) + item = subMenu_speechDicts.Append(wx.ID_ANY,_("&Smart Dictionary Manager..."),_("dialog where you can manage Smart dictionaries")) + self.Bind(wx.EVT_MENU, frame.onSmartDictManagerCommand, item) menu_preferences.AppendMenu(wx.ID_ANY,_("Speech &dictionaries"),subMenu_speechDicts) self.menu.AppendMenu(wx.ID_ANY,_("&Preferences"),menu_preferences) === added file 'source/gui/smartDictManager.py' --- source/gui/smartDictManager.py 1970-01-01 00:00:00 +0000 +++ source/gui/smartDictManager.py 2008-11-09 00:42:58 +0000 @@ -0,0 +1,130 @@ +#gui/smartDictManager.py +#A part of NonVisual Desktop Access (NVDA) +#Copyright (C) 2006-2008 NVDA Contributors +#This file is covered by the GNU General Public License. +#See the file COPYING for more details. + +import os +import wx +import gui +from logHandler import log +import speechDictHandler +from settingsDialogs import DictionaryDialog + +class SmartDictDialog(wx.Dialog): + """For creating|modifying smart dictionary.""" + smartDict = None + + def __init__(self,parent,_title,smartDict): + super(SmartDictDialog,self).__init__(parent,title=_title) + mainSizer=wx.BoxSizer(wx.VERTICAL) + settingsSizer=wx.BoxSizer(wx.VERTICAL) + settingsSizer.Add(wx.StaticText(self,-1,label=_("Name"))) + self.nameTextCtrl=wx.TextCtrl(self,wx.NewId()) + self.nameTextCtrl.SetValue(smartDict.name) + settingsSizer.Add(self.nameTextCtrl) + settingsSizer.Add(wx.StaticText(self,-1,label=_("Regular expression pattern"))) + self.patternTextCtrl=wx.TextCtrl(self,wx.NewId()) + self.patternTextCtrl.SetValue(smartDict.pattern) + settingsSizer.Add(self.patternTextCtrl) + 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.nameTextCtrl.SetFocus() + +class SmartDictManagerDialog(wx.Dialog): + """shows the list of smart dictionaries and provides manipulating actions""" + smartDicts = None + + def __init__(self, parent): + super(SmartDictManagerDialog, self).__init__(parent, wx.ID_ANY, _("Smart Dictionary Manager")) + self.smartDicts = list(speechDictHandler.smartDicts) + mainSizer=wx.BoxSizer(wx.VERTICAL) + #create a list of all dictionaries + dictListID=wx.NewId() + self.dictList=wx.ListCtrl(self,dictListID,style=wx.LC_REPORT|wx.LC_SINGLE_SEL) + entriesSizer=wx.BoxSizer(wx.HORIZONTAL) + entriesSizer.Add(wx.StaticText(self,-1,label=_("&Smart dictionaries"))) + self.dictList.InsertColumn(0,_("Name")) + self.dictList.InsertColumn(1,_("Pattern")) + for smartDict in self.smartDicts: + self.dictList.Append((smartDict.name, smartDict.pattern)) + entriesSizer.Add(self.dictList) + buttonsSizer=wx.BoxSizer(wx.VERTICAL) + createButtonID = wx.NewId() + createButton=wx.Button(self,createButtonID,_("&Create dictionary..."),wx.DefaultPosition) + self.Bind(wx.EVT_BUTTON,self.onCreateClick,id=createButtonID) + buttonsSizer.Add(createButton) + deleteButtonID=wx.NewId() + deleteButton=wx.Button(self,deleteButtonID,_("&Delete dictionary"),wx.DefaultPosition) + self.Bind(wx.EVT_BUTTON,self.onDeleteClick,id=deleteButtonID) + buttonsSizer.Add(deleteButton) + changeButtonID=wx.NewId() + changeButton=wx.Button(self,changeButtonID,_("&Change dictionary properties..."),wx.DefaultPosition) + self.Bind(wx.EVT_BUTTON,self.onChangeClick,id=changeButtonID) + buttonsSizer.Add(changeButton) + editButtonID=wx.NewId() + editButton=wx.Button(self,editButtonID,_("&Edit dictionary entries..."),wx.DefaultPosition) + self.Bind(wx.EVT_BUTTON,self.onEditClick,id=editButtonID) + buttonsSizer.Add(editButton) + mainSizer.Add(entriesSizer) + mainSizer.Add(buttonsSizer) + closeButtonID=wx.NewId() + closeButton=wx.Button(self,closeButtonID,_("C&lose"),wx.DefaultPosition) + self.Bind(wx.EVT_BUTTON,self.onCloseClick,id=closeButtonID) + mainSizer.Add(closeButton,border=20,flag=wx.LEFT|wx.RIGHT|wx.BOTTOM) + mainSizer.Fit(self) + self.SetSizer(mainSizer) + self.dictList.SetFocus() + + def onEditClick(self, evt): + i = self.dictList.GetFirstSelected() + if i <0: return + dict = self.smartDicts[i] + dialog = DictionaryDialog(self, dict.fileName, dict) + dialog.Show() + + def onCreateClick(self, evt): + smartDict = speechDictHandler.SmartDict() + smartDictDialog = SmartDictDialog(self, _("New smart dictionary"), smartDict) + if smartDictDialog.ShowModal()==wx.ID_OK: + smartDict.setName(smartDictDialog.nameTextCtrl.GetValue()) + smartDict.setPattern(smartDictDialog.patternTextCtrl.GetValue()) + smartDict.save() + self.smartDicts.append(smartDict) + self.dictList.Append((smartDict.name, smartDict.pattern)) + smartDictDialog.Destroy() + + def onDeleteClick(self, evt): + i = self.dictList.GetFirstSelected() + if i <0: return + dict = self.smartDicts[i] + d = wx.MessageDialog(self, _("Are you sure you want to delete dictionary '%s'?" % dict.name), _("Confirm dictionary deletion"), wx.YES|wx.NO|wx.ICON_QUESTION) + if d.ShowModal() == wx.ID_YES: + fileName = dict.fileName + del self.smartDicts[i] + if os.access(fileName, os.F_OK): os.remove(fileName) + self.dictList.DeleteItem(i) + d.Destroy() + + def onChangeClick(self, evt): + i = self.dictList.GetFirstSelected() + if i <0: return + smartDict = self.smartDicts[i] + oldName = smartDict.name + smartDictDialog = SmartDictDialog(self, _("Change smart dictionary"), smartDict) + if smartDictDialog.ShowModal()==wx.ID_OK: + if oldName != smartDictDialog.nameTextCtrl.GetValue(): + if os.access(smartDict.fileName, os.F_OK): os.remove(smartDict.fileName) + smartDict.setName(smartDictDialog.nameTextCtrl.GetValue()) + smartDict.setPattern(smartDictDialog.patternTextCtrl.GetValue()) + smartDict.save() + self.dictList.SetStringItem(i,0,smartDict.name) + self.dictList.SetStringItem(i,1,smartDict.pattern) + smartDictDialog.Destroy() + + def onCloseClick(self, evt): + speechDictHandler.smartDicts = list(self.smartDicts) + self.Destroy() === modified file 'source/keyboardHandler.py' --- source/keyboardHandler.py 2008-10-12 08:55:18 +0000 +++ source/keyboardHandler.py 2008-11-02 13:48:22 +0000 @@ -216,7 +216,7 @@ #Register internal key press event with operating system def initialize(): - """Initialises keyboard support.""" + """Initializes keyboard support.""" if keyHookLib.initialize(internal_keyDownEvent,internal_keyUpEvent) < 0: raise RuntimeError("Error initializing keyHook") === modified file 'source/speechDictHandler.py' --- source/speechDictHandler.py 2008-08-19 12:17:15 +0000 +++ source/speechDictHandler.py 2008-11-09 12:44:12 +0000 @@ -13,8 +13,9 @@ import api dictionaries = {} -dictTypes = ("temp", "voice", "default") # ordered by their priority E.G. voice specific speech dictionary is processed before the default -speechDictsPath="speechdicts" +dictTypes = ("temp", "smart", "voice", "default") # ordered by their priority E.G. voice specific speech dictionary is processed before the default +speechDictsPath=u"speechdicts" +smartDicts = [] class SpeechDictEntry: @@ -22,6 +23,7 @@ self.pattern = pattern flags=re.IGNORECASE if not caseSensitive else 0 tempPattern=pattern if regexp else re.escape(pattern) + flags = flags | re.U self.compiled = re.compile(tempPattern,flags) self.replacement = replacement self.comment=comment @@ -79,23 +81,87 @@ text = entry.sub(text) return text +class SmartDict(SpeechDict): + fileName = None + compiled = None + pattern = "" + loaded = False + name = "" + + def __init__(self, fileName=None, name=""): + """loads regexpr from the file""" + super(list,self).__init__() + self.fileName = fileName + self.name = name + if fileName is None: return + file = codecs.open(fileName,"r","utf_8_sig",errors="replace") + s = file.readline() + if not s.startswith("#!"): raise RuntimeError("No regexpr found at starting of file %s"%fileName) + s = s[2:] + self.pattern = s.rstrip('\r\n') + self.compiled = re.compile(self.pattern) + file.close() + + def setPattern(self, pattern): + self.pattern = pattern + self.compiled = re.compile(self.pattern) + + def setName(self,name): + self.name = name + self.fileName= u"%s/%s.sdic" % (speechDictsPath, api.validateFile(name)) + + def matches(self, value): + return self.compiled.search(value) is not None + + def save(self,fileName=None): + if fileName is not None: + self.fileName = fileName + file = codecs.open(self.fileName,"w","utf_8_sig",errors="replace") + file.write("#!%s\r\n\r\n" % self.pattern) + 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,entry.replacement,int(entry.caseSensitive),int(entry.regexp))) + file.close() + + def load(self): + if not self.loaded: SpeechDict.load(self,self.fileName) + self.loaded = True + def processText(text): if not globalVars.speechDictionaryProcessing: return text for type in dictTypes: - text=dictionaries[type].sub(text) + if type != "smart": + text=dictionaries[type].sub(text) + for smart in dictionaries["smart"]: text=smart.sub(text) return text -def getFileName(type): +def getFileName(type, synth=synthDriverHandler.getSynth()): if type is "default": return "%s/default.dic"%speechDictsPath elif type is "voice": - s=synthDriverHandler.getSynth() - return "%s/%s-%s.dic"%(speechDictsPath,api.validateFile(s.name),validateFile(s.getVoiceName(s.voice))) + return "%s/%s-%s.dic"%(speechDictsPath,api.validateFile(synth.name),api.validateFile(synth.getVoiceInfoByID(synth.voice).name)) return None +def reflectVoiceChange(synth): + """updates the speech dictionaries reflecting voice change""" + dictionaries["voice"].load(getFileName("voice", synth)) + name = "%s-%s" %(synth.name, synth.getVoiceInfoByID(synth.voice).name) + del dictionaries["smart"][:] + [(dict.load(), dictionaries["smart"].append(dict)) for dict in smartDicts if dict.matches(name)] + def initialize(): - for type in dictTypes: + #search for the smart dictionaries + counter = 0 + for name in os.listdir(speechDictsPath): + if not name.endswith(".sdic"): continue + smartDicts.append(SmartDict("%s/%s"%(speechDictsPath, name), name[:-5])) + counter += 1 + if counter > 0: + log.info("found %d smart dictionaries"%counter) + #create the speechDict objects for all excluding smart + for type in [x for x in dictTypes if x != "smart"]: dictionaries[type]=SpeechDict() + dictionaries["smart"] = [] dictionaries["default"].load(getFileName("default")) - === modified file 'source/synthDriverHandler.py' --- source/synthDriverHandler.py 2008-11-09 11:33:43 +0000 +++ source/synthDriverHandler.py 2008-11-01 09:39:18 +0000 @@ -18,11 +18,8 @@ _curSynth=None def changeVoice(synth, voice): - voiceName=synth.getVoiceInfoByID(voice).name - voiceName=voiceName.replace('\\','_') - fileName="%s/%s-%s.dic"%(speechDictHandler.speechDictsPath,synth.name,voiceName) - speechDictHandler.dictionaries["voice"].load(fileName) synth.voice = voice + speechDictHandler.reflectVoiceChange(synth) def getSynthList(): synthList=[] # Begin bundle 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 f/D8R+I/eP4C7kinChIJ46hFAA==