# Bazaar merge directive format 2 (Bazaar 0.90) # revision_id: aleksey_s@voliacable.com-20081122195908-\ # kfq9fvh20mftsk8i # target_branch: ../main # testament_sha1: 773364ec3b50149e2cd61000af8b8795bd8f82aa # timestamp: 2008-11-23 00:07:57 +0200 # base_revision_id: peter.v@datagate.sk-20081122111238-\ # zldm3s4p7b92623z # # Begin patch === modified file 'source/core.py' --- source/core.py 2008-11-11 02:41:53 +0000 +++ source/core.py 2008-11-22 19:59:08 +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 @@ -70,7 +70,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() @@ -82,6 +81,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-11-11 13:42:23 +0000 +++ source/speechDictHandler.py 2008-11-22 19:59:08 +0000 @@ -14,8 +14,9 @@ import config dictionaries = {} -dictTypes = ("temp", "voice", "default", "builtin") # ordered by their priority E.G. voice specific speech dictionary is processed before the default +dictTypes = ("temp", "smart", "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") +smartDicts = [] class SpeechDictEntry: @@ -23,6 +24,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 @@ -69,9 +71,6 @@ fileName=getattr(self,'fileName',None) if not fileName: return - dirName=os.path.dirname(fileName) - if not os.path.isdir(dirName): - os.makedirs(dirName) file = codecs.open(fileName,"w","utf_8_sig",errors="replace") for entry in self: if entry.comment: @@ -84,15 +83,94 @@ 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= getFileName("smart", name=self.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: + if type == "smart": continue text=dictionaries[type].sub(text) + for smart in dictionaries["smart"]: text=smart.sub(text) return text +def getFileName(type, synth=synthDriverHandler.getSynth(), name=None): + if type is "default": + return os.path.join(speechDictsPath, "default.dic") + elif type is "smart": + return "%s/%s.sdic" % (speechDictsPath, api.filterFileName(name)) + elif type is "voice": + return "%s/%s-%s.dic"%(speechDictsPath,api.filterFileName(synth.name),api.filterFileName(synth.getVoiceInfoByID(synth.voice).name)) + elif type is "builtin": return "builtin.dic" + 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: + #create speechDicts folder if appropriate + if not os.path.isdir(speechDictsPath): + os.makedirs(speechDictsPath) + #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["default"].load(os.path.join(speechDictsPath, "default.dic")) - dictionaries["builtin"].load("builtin.dic") + dictionaries["smart"] = [] + dictionaries["default"].load(getFileName("default")) + dictionaries["builtin"].load(getFileName("builtin")) === modified file 'source/synthDriverHandler.py' --- source/synthDriverHandler.py 2008-11-11 02:15:20 +0000 +++ source/synthDriverHandler.py 2008-11-22 19:59:08 +0000 @@ -18,11 +18,8 @@ _curSynth=None def changeVoice(synth, voice): - import api - voiceName = synth.getVoiceInfoByID(voice).name synth.voice = voice - fileName=r"%s\%s-%s.dic"%(speechDictHandler.speechDictsPath,synth.name,api.filterFileName(voiceName)) - speechDictHandler.dictionaries["voice"].load(fileName) + speechDictHandler.reflectVoiceChange(synth) def getSynthList(): synthList=[] # Begin bundle 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 3xIoYA==