3 new commits in StationPlaylist: https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/21d83bebae49/ Changeset: 21d83bebae49 Branch: None User: josephsl Date: 2015-02-03 12:11:00+00:00 Summary: Background monitoring (5.0-dev, possibly 4.x later): huge refactor on name change event to take advantage of request events incubating in NVDA core/nexe. At this time, NVDA core contains a small change which may allow background announcement of status bar contents such as mic status and library scan announcements. The new routine (incubating in NVDA next) works, although some small hack is introduced to delete the requested events manually when the app module exits. This hack in terminate method will be removed when a public API to remove requested events is implemented. Due to name change event refactor, this will be available in add-on 5.0 and later, with an optoin to backport this to 4.x. Additional work: * Name change event: when checking for status bar content, foreground windows will no longer be used. * In app module constructor, check if the new request events routine is present, and if so, allow background monitoring of status content. To support older NvDA releases, the background monitor flag will be assumed false. More work to be done later. Affected #: 1 file diff --git a/addon/appModules/splstudio.py b/addon/appModules/splstudio.py index 4654a49..96c59d7 100644 --- a/addon/appModules/splstudio.py +++ b/addon/appModules/splstudio.py @@ -10,9 +10,13 @@ # Minimum version: SPL 5.00, NvDA 2014.3. +import ctypes +from ctypes import wintypes from functools import wraps import os +from cStringIO import StringIO from configobj import ConfigObj +from validate import Validator import time import threading import controlTypes @@ -20,6 +24,7 @@ import appModuleHandler import api import globalVars import review +import eventHandler import scriptHandler import ui import nvwave @@ -28,8 +33,8 @@ import braille import gui import wx from winUser import user32, sendMessage -from winKernel import GetTimeFormat, LOCALE_USER_DEFAULT -from NVDAObjects.IAccessible import IAccessible +import winKernel +from NVDAObjects.IAccessible import IAccessible, sysListView32 # SysListView32 is used for Track Dial (see below). import textInfos import tones import addonHandler @@ -51,13 +56,40 @@ def finally_(func, final): # Make sure the broadcaster is running a compatible version. SPLMinVersion = "5.00" -# Configuration management )4.0 and later; will not be ported to 3.x). -SPLConfig = ConfigObj(os.path.join(globalVars.appArgs.configPath, "splstudio.ini")) +# Configuration management +SPLIni = os.path.join(globalVars.appArgs.configPath, "splstudio.ini") +confspec = ConfigObj(StringIO(""" +BeepAnnounce = boolean(default=false) +EndOfTrackTime = integer(min=1, max=59, default=5) +SongRampTime = integer(min=1, max=9, default=5) +MicAlarm = integer(min=0, default="0") +"""), encoding="UTF-8", list_values=False) +confspec.newlines = "\r\n" +SPLConfig = None + +# List of values to be converted manually. +# This will be called only once: when upgrading from prior versions to 5.0, to be removed in 5.1. +configConversions=("EndOfTrackTime", "SongRampTime") + +# The accompanying function for config conversion. +# Returns config=false if errors occur, to be checked in the app module constructor. +def config4to5(): + global SPLConfig, configConversions + for setting in configConversions: + oldValue = str(SPLConfig[setting]) + if oldValue.isdigit(): + continue + # If the old value doesn't conform to below conditions, start from a fresh config spec. + if (len(oldValue) != 5 + and not oldValue.startswith("00:") + and not oldValue.split(":")[1].isdigit()): + return False + newValue = SPLConfig[setting].split(":")[1] + SPLConfig[setting] = int(newValue) + return True # Display an error dialog when configuration is wrong. def runConfigErrorDialog(): - global SPLConfig - if SPLConfig is not None: SPLConfig.write() wx.CallAfter(gui.messageBox, # Translators: Standard dialog message when Studio configuration has problems. _("Your Studio configuration has errors and was reset to factory defaults."), @@ -78,10 +110,104 @@ def messageSound(wavFile, message): # Routines for track items themselves (prepare for future work). class SPLTrackItem(IAccessible): - """Track item for earlier versions of Studio such as 5.00.""" - pass + """Track item for earlier versions of Studio such as 5.00. + A base class for providing utility scripts when track entries are focused, such as track dial.""" + + def initOverlayClass(self): + if self.appModule.SPLTrackDial: + self.bindGesture("kb:rightArrow", "nextColumn") + self.bindGesture("kb:leftArrow", "prevColumn") + + # Track Dial: using arrow keys to move through columns. + # This is similar to enhanced arrow keys in other screen readers. + + def script_toggleTrackDial(self, gesture): + if not self.appModule.SPLTrackDial: + self.appModule.SPLTrackDial = True + self.bindGesture("kb:rightArrow", "nextColumn") + self.bindGesture("kb:leftArrow", "prevColumn") + dialText = "Track Dial on" + if self.appModule.SPLColNumber > 0: + dialText+= ", located at column {columnHeader}".format(columnHeader = self.appModule.SPLColNumber+1) + ui.message(dialText) + else: + self.appModule.SPLTrackDial = False + try: + self.removeGestureBinding("kb:rightArrow") + self.removeGestureBinding("kb:leftArrow") + except KeyError: + pass + ui.message("Track Dial off") + + # Some helper functions to handle corner cases. + # Each track item provides its own version. + def _leftmostcol(self): + leftmost = self.columnHeaders.firstChild.name + if not self.name or self.name == "": + ui.message("{leftmostColumn} not found".format(leftmostColumn = leftmost)) + else: + ui.message("{leftmostColumn}: {leftmostContent}".format(leftmostColumn = self.columnHeaders.children[self.appModule.SPLColNumber].name, leftmostContent = self.name)) + + # Locate column content. + def _getColumnContent(self, col): + # Borrowed from SysListView32 implementation. + # For add-on 6.0: see if track items can be subclassed from SysListView32.ListItem. + buffer=None + processHandle=self.processHandle + # Because each process in an OS has separate memory spaces, use VM functions to find column content. + internalItem=winKernel.virtualAllocEx(processHandle,None,ctypes.sizeof(sysListView32.LVITEM),winKernel.MEM_COMMIT,winKernel.PAGE_READWRITE) + try: + internalText=winKernel.virtualAllocEx(processHandle,None,520,winKernel.MEM_COMMIT,winKernel.PAGE_READWRITE) + try: + item=sysListView32.LVITEM(iItem=self.IAccessibleChildID-1,mask=sysListView32.LVIF_TEXT|sysListView32.LVIF_COLUMNS,iSubItem=col,pszText=internalText,cchTextMax=260) + winKernel.writeProcessMemory(processHandle,internalItem,ctypes.byref(item),ctypes.sizeof(sysListView32.LVITEM),None) + len = sendMessage(self.windowHandle,sysListView32.LVM_GETITEMTEXTW, (self.IAccessibleChildID-1), internalItem) + if len: + winKernel.readProcessMemory(processHandle,internalItem,ctypes.byref(item),ctypes.sizeof(sysListView32.LVITEM),None) + buffer=ctypes.create_unicode_buffer(len) + winKernel.readProcessMemory(processHandle,item.pszText,buffer,ctypes.sizeof(buffer),None) + finally: + winKernel.virtualFreeEx(processHandle,internalText,0,winKernel.MEM_RELEASE) + finally: + winKernel.virtualFreeEx(processHandle,internalItem,0,winKernel.MEM_RELEASE) + return buffer.value if buffer else None + + # Announce column content if any. + def announceColumnContent(self, colNumber): + columnHeader = self.columnHeaders.children[colNumber].name + columnContent = self._getColumnContent(colNumber) + if columnContent: + ui.message("{header}: {content}".format(header = columnHeader, content = columnContent)) + else: + speech.speakMessage("{header}: blank".format(header = columnHeader)) + braille.handler.message("{header}: ()".format(header = columnHeader)) + + # Now the scripts. + + def script_nextColumn(self, gesture): + self.columnHeaders = self.parent.children[-1] + if (self.appModule.SPLColNumber+1) == self.columnHeaders.childCount: + tones.beep(2000, 100) + else: + self.appModule.SPLColNumber +=1 + self.announceColumnContent(self.appModule.SPLColNumber) -class SPL510TrackItem(IAccessible): + def script_prevColumn(self, gesture): + self.columnHeaders = self.parent.children[-1] + if self.appModule.SPLColNumber <= 0: + tones.beep(2000, 100) + else: + self.appModule.SPLColNumber -=1 + if self.appModule.SPLColNumber == 0: + self._leftmostcol() + else: + self.announceColumnContent(self.appModule.SPLColNumber) + + __gestures={ + "kb:control+`":"toggleTrackDial", + } + +class SPL510TrackItem(SPLTrackItem): """ Track item for Studio 5.10 and later.""" def script_select(self, gesture): @@ -89,14 +215,105 @@ class SPL510TrackItem(IAccessible): speech.speakMessage(self.name) braille.handler.handleUpdate(self) + # Handle track dial for SPL 5.10. + def _leftmostcol(self): + if not self.name: + ui.message("Status not found") + else: + ui.message("Status: {n}".format(n = self.name)) + __gestures={"kb:space":"select"} +# Configuration dialog. +class SPLConfigDialog(gui.SettingsDialog): + # Translators: This is the label for the StationPlaylist Studio configuration dialog. + title = _("Studio Add-on Settings") + + def makeSettings(self, settingsSizer): + + sizer = wx.BoxSizer(wx.HORIZONTAL) + # Translators: the label for a setting in SPL add-on settings to set status announcement between words and beeps. + self.beepAnnounceCheckbox=wx.CheckBox(self,wx.NewId(),label=_("&Beep for status announcements")) + self.beepAnnounceCheckbox.SetValue(SPLConfig["BeepAnnounce"]) + sizer.Add(self.beepAnnounceCheckbox, border=10,flag=wx.BOTTOM) + + # Translators: The label for a setting in SPL Add-on settings to specify end of track (outro) alarm. + label = wx.StaticText(self, wx.ID_ANY, label=_("&End of track alarm in seconds")) + sizer.Add(label) + self.endOfTrackAlarm = wx.SpinCtrl(self, wx.ID_ANY, min=1, max=59) + self.endOfTrackAlarm.SetValue(long(SPLConfig["EndOfTrackTime"])) + sizer.Add(self.endOfTrackAlarm) + settingsSizer.Add(sizer, border=10, flag=wx.BOTTOM) + + sizer = wx.BoxSizer(wx.HORIZONTAL) + # Translators: The label for a setting in SPL Add-on settings to specify track intro alarm. + label = wx.StaticText(self, wx.ID_ANY, label=_("Track &intro alarm in seconds")) + sizer.Add(label) + self.introAlarm = wx.SpinCtrl(self, wx.ID_ANY, min=1, max=9) + self.introAlarm.SetValue(long(SPLConfig["SongRampTime"])) + sizer.Add(self.introAlarm) + settingsSizer.Add(sizer, border=10, flag=wx.BOTTOM) + + sizer = wx.BoxSizer(wx.HORIZONTAL) + # Translators: The label for a setting in SPL Add-on settings to change microphone alarm setting. + label = wx.StaticText(self, wx.ID_ANY, label=_("&Microphone alarm in seconds")) + sizer.Add(label) + self.micAlarm = wx.TextCtrl(self, wx.ID_ANY) + self.micAlarm.SetValue(str(SPLConfig["MicAlarm"])) + sizer.Add(self.micAlarm) + settingsSizer.Add(sizer, border=10, flag=wx.BOTTOM) + + def postInit(self): + self.beepAnnounceCheckbox.SetFocus() + + def onOk(self, evt): + if not self.micAlarm.Value.isdigit(): + gui.messageBox( + # Translators: Message to report wrong value for microphone alarm. + _("Incorrect microphone alarm value entered."), + # Translators: The title of the message box + _("Error"), wx.OK|wx.ICON_ERROR,self) + self.micAlarm.SetFocus() + return + SPLConfig["EndOfTrackTime"] = self.endOfTrackAlarm.Value + SPLConfig["SongRampTime"] = self.introAlarm.Value + SPLConfig["MicAlarm"] = self.micAlarm.Value + super(SPLConfigDialog, self).onOk(evt) + class AppModule(appModuleHandler.AppModule): # Translators: Script category for Station Playlist commands in input gestures dialog. scriptCategory = _("Station Playlist Studio") + # Prepare the settings dialog among other things. + def __init__(self, *args, **kwargs): + super(AppModule, self).__init__(*args, **kwargs) + global SPLConfig, SPLIni, confspec + SPLConfig = ConfigObj(SPLIni, configspec = confspec, encoding="UTF-8") + val = Validator() + configTest = SPLConfig.validate(val, copy=True) + if isinstance(configTest, dict) and not config4to5(): + os.remove(SPLIni) + SPLConfig2 = ConfigObj(SPLIni, configspec = confspec, encoding="UTF-8") + SPLConfig2.write() + SPLConfig = ConfigObj(SPLIni, configspec = confspec, encoding="UTF-8") + try: + runConfigErrorDialog() + except AttributeError: + pass + # Announce status changes while using other programs. + # This requires NVDA core support and will be available in 5.0 and later (cannot be ported to earlier versions). + # For now, handle all background events, but in the end, make this configurable. + if hasattr(eventHandler, "requestEvents"): + eventHandler.requestEvents(eventName="nameChange", processId=self.processID, windowClassName="TStatusBar") + self.backgroundStatusMonitor = True + else: + self.backgroundStatusMonitor = False + self.prefsMenu = gui.mainFrame.sysTrayIcon.preferencesMenu + self.SPLSettings = self.prefsMenu.Append(wx.ID_ANY, _("SPL Studio Settings..."), _("SPL settings")) + gui.mainFrame.sysTrayIcon.Bind(wx.EVT_MENU, self.script_openConfigDialog, self.SPLSettings) + # Play beeps instead of announcing toggles. beepAnnounce = False SPLCurVersion = appModuleHandler.AppModule.productVersion @@ -119,96 +336,59 @@ class AppModule(appModuleHandler.AppModule): fg = api.getForegroundObject() if fg: role = obj.role - # Ignore hour markers when using add-on 3.x and 4.x with Studio 5.10. - if obj.windowClassName == "TTntListView.UnicodeClass" and fg.windowClassName == "TStudioForm" and role == controlTypes.ROLE_LISTITEM and obj.name: + if obj.windowClassName == "TTntListView.UnicodeClass" and fg.windowClassName == "TStudioForm" and role == controlTypes.ROLE_LISTITEM: clsList.insert(0, SPL510TrackItem) - # For now, ignore track entries for earlier Studio versions. - """elif obj.windowClassName == "TListView" and fg.windowClassName == "TStudioForm" and role == controlTypes.ROLE_CHECKBOX: - clsList.insert(0, SPLTrackItem)""" - - # populate end of track and intro time alarm settings separately. - unexpectedConfig = False - try: - # Manually correct config mistakes (it'll be automated in add-on 5.0). - SPLEndOfTrackTime = SPLConfig["EndOfTrackTime"] - if (len(SPLEndOfTrackTime) != 5 - or not SPLEndOfTrackTime.startswith("00:") - or SPLEndOfTrackTime[2] != ":" - or not SPLEndOfTrackTime.split(":")[1].isdigit()): - SPLEndOfTrackTime = "00:05" - # Tell NVDA to write the config again. - unexpectedConfig = True - SPLConfig["EndOfTrackTime"] = SPLEndOfTrackTime - except KeyError: - SPLEndOfTrackTime = "00:05" - try: - SPLSongRampTime = SPLConfig["SongRampTime"] - if (len(SPLSongRampTime) != 5 - or not SPLSongRampTime.startswith("00:0") - or SPLSongRampTime[2] != ":" - or not SPLSongRampTime[-1].isdigit()): - SPLSongRampTime = "00:05" - unexpectedConfig = True - SPLConfig["SongRampTime"] = SPLSongRampTime - except KeyError: - SPLSongRampTime = "00:05" + elif obj.windowClassName == "TListView" and fg.windowClassName == "TStudioForm" and role == controlTypes.ROLE_CHECKBOX: + clsList.insert(0, SPLTrackItem) + # Keep an eye on library scans in insert tracks window. libraryScanning = False scanCount = 0 - # Microphone alarm. - try: - try: - micAlarm = int(SPLConfig["MicAlarm"]) - except ValueError: - micAlarm = 0 - unexpectedConfig = True - SPLConfig["MicAlarm"] = micAlarm - if micAlarm < 0: - micAlarm = 0 - unexpectedConfig = True - SPLConfig["MicAlarm"] = micAlarm - except KeyError: - micAlarm = 0 - # Rewrite the config. - if unexpectedConfig: - runConfigErrorDialog() - unexpectedConfig = False # For SPL 5.10: take care of some object child constant changes across builds. spl510used = False # For 5.0X and earlier: prevent NVDA from announcing scheduled time multiple times. scheduledTimeCache = "" + # Track Dial (A.K.A. enhanced arrow keys) + SPLTrackDial = False + SPLColNumber = 0 # Automatically announce mic, line in, etc changes # These items are static text items whose name changes. # Note: There are two status bars, hence the need to exclude Up time so it doesn't announce every minute. # Unfortunately, Window handles and WindowControlIDs seem to change, so can't be used. def event_nameChange(self, obj, nextHandler): - # Do not let NvDA get name for None object when SPL window is maximized. global noLibScanMonitor + # Do not let NvDA get name for None object when SPL window is maximized. if not obj.name: return + # Suppress announcing status messages when background monitoring is unavailable. + if api.getForegroundObject().processID != self.processID and not self.backgroundStatusMonitor: + nextHandler() if obj.windowClassName == "TStatusBar" and not obj.name.startswith(" Up time:"): + #tones.beep(512, 200) # Special handling for Play Status - fgWinClass = api.getForegroundObject().windowClassName if obj.IAccessibleChildID == 1: - if fgWinClass == "TStudioForm": + if "Play status" in obj.name: # Strip off " Play status: " for brevity only in main playlist window. - ui.message(obj.name[15:]) - elif fgWinClass == "TTrackInsertForm" and self.libraryScanProgress > 0: - # If library scan is in progress, announce its progress. - self.scanCount+=1 - if self.scanCount%100 == 0: - if self.libraryScanProgress == self.libraryScanMessage: - tones.beep(550, 100) if self.beepAnnounce else ui.message("Scanning") - elif self.libraryScanProgress == self.libraryScanNumbers: - if self.beepAnnounce: tones.beep(550, 100) - ui.message(obj.name[1:obj.name.find("]")]) - if "Loading" in obj.name and not self.libraryScanning: - if self.productVersion not in noLibScanMonitor: self.libraryScanning = True - elif "match" in obj.name: + ui.message(obj.name.split(":")[1]) + elif "Loading" in obj.name: + if self.libraryScanProgress > 0: + # If library scan is in progress, announce its progress. + self.scanCount+=1 + if self.scanCount%100 == 0: + if self.libraryScanProgress == self.libraryScanMessage: + tones.beep(550, 100) if self.beepAnnounce else ui.message("Scanning") + elif self.libraryScanProgress == self.libraryScanNumbers: + if self.beepAnnounce: tones.beep(550, 100) + ui.message(obj.name[1:obj.name.find("]")]) + if not self.libraryScanning: + if self.productVersion not in noLibScanMonitor: + if not self.backgroundStatusMonitor: self.libraryScanning = True + elif "match" in obj.name: + if self.libraryScanProgress: tones.beep(370, 100) if self.beepAnnounce else ui.message("Scan complete") - if self.libraryScanning: self.libraryScanning = False - self.scanCount = 0 + if self.libraryScanning: self.libraryScanning = False + self.scanCount = 0 else: if obj.name.startswith("Scheduled for"): if self.scheduledTimeCache == obj.name: return @@ -237,9 +417,9 @@ class AppModule(appModuleHandler.AppModule): return else: ui.message(obj.name) - if self.cartExplorer or self.micAlarm: - # Activate mic alarm or announce when cart explorer is active. - self.doExtraAction(obj.name) + if self.cartExplorer or int(SPLConfig["MicAlarm"]): + # Activate mic alarm or announce when cart explorer is active. + self.doExtraAction(obj.name) # Monitor the end of track and song intro time and announce it. elif obj.windowClassName == "TStaticText": # For future extensions. if obj.simplePrevious != None: @@ -247,21 +427,21 @@ class AppModule(appModuleHandler.AppModule): # End of track for SPL 5.x. if self.brailleTimer in [self.brailleTimerEnding, self.brailleTimerBoth]: #and "00:00" < obj.name <= self.SPLEndOfTrackTime: braille.handler.message(obj.name) - if obj.name == self.SPLEndOfTrackTime: + if obj.name == "00:{0:02d}".format(SPLConfig["EndOfTrackTime"]): tones.beep(440, 200) if obj.simplePrevious.name == "Remaining Song Ramp": # Song intro for SPL 5.x. if self.brailleTimer in [self.brailleTimerIntro, self.brailleTimerBoth]: #and "00:00" < obj.name <= self.SPLSongRampTime: braille.handler.message(obj.name) - if obj.name == self.SPLSongRampTime: + if obj.name == "00:{0:02d}".format(SPLConfig["SongRampTime"]): tones.beep(512, 400) - # Clean this mess with a more elegant solution. nextHandler() # JL's additions # Perform extra action in specific situations (mic alarm, for example). def doExtraAction(self, status): + micAlarm = int(SPLConfig["MicAlarm"]) if self.cartExplorer: if status == "Cart Edit On": # Translators: Presented when cart edit mode is toggled on while cart explorer is on. @@ -269,7 +449,7 @@ class AppModule(appModuleHandler.AppModule): elif status == "Cart Edit Off": # Translators: Presented when cart edit mode is toggled off while cart explorer is on. ui.message(_("Please reenter cart explorer to view updated cart assignments")) - if self.micAlarm: + if micAlarm: # Play an alarm sound from Braille Sense U2. global micAlarmT micAlarmWav = os.path.join(os.path.dirname(__file__), "SPL_MicAlarm.wav") @@ -277,11 +457,11 @@ class AppModule(appModuleHandler.AppModule): micAlarmMessage = _("Warning: Microphone active") # Use a timer to play a tone when microphone was active for more than the specified amount. if status == "Microphone On": - micAlarmT = threading.Timer(self.micAlarm, messageSound, args=[micAlarmWav, micAlarmMessage]) + micAlarmT = threading.Timer(micAlarm, messageSound, args=[micAlarmWav, micAlarmMessage]) try: micAlarmT.start() except RuntimeError: - micAlarmT = threading.Timer(self.micAlarm, messageSound, args=[micAlarmWav, micAlarmMessage]) + micAlarmT = threading.Timer(micAlarm, messageSound, args=[micAlarmWav, micAlarmMessage]) micAlarmT.start() elif status == "Microphone Off": if micAlarmT is not None: micAlarmT.cancel() @@ -298,16 +478,38 @@ class AppModule(appModuleHandler.AppModule): # Continue monitoring library scans among other focus loss management. def event_loseFocus(self, obj, nextHandler): fg = api.getForegroundObject() - if fg.windowClassName == "TTrackInsertForm" and self.libraryScanning: + if fg.windowClassName == "TTrackInsertForm" and self.libraryScanning and not self.backgroundStatusMonitor: global libScanT if not libScanT or (libScanT and not libScanT.isAlive()): self.monitorLibraryScan() nextHandler() + # Call SPL API to obtain needed values. + # This is needed for some Assistant and time commands. + # A thin wrapper around user32.SendMessage and calling a callback if defined. + def statusAPI(self, arg, command, func=None, ret=False, offset=None): + #c = time.clock() + SPLWin = user32.FindWindowA("SPLStudio", None) + val = sendMessage(SPLWin, 1024, arg, command) + #ui.message("{c}".format(c = time.clock()-c)) + if ret: + return val + if func: + func(val) if not offset else func(val, offset) + # Save configuration when terminating. def terminate(self): super(AppModule, self).terminate() + global SPLConfig if SPLConfig is not None: SPLConfig.write() + # Hack: until the public API is available, remove SPL entry from accepted events manually. + eventHandler._acceptEvents.remove(("nameChange", self.processID, "TStatusBar")) + try: + self.prefsMenu.RemoveItem(self.SPLSettings) + except wx.PyDeadObjectError: + pass + SPLConfig = None + # Script sections (for ease of maintenance): # Time-related: elapsed time, end of track alarm, etc. @@ -329,22 +531,20 @@ class AppModule(appModuleHandler.AppModule): 4:_("Cannot obtain time in hours, minutes and seconds") } - # Call SPL API for important time messages. - def timeAPI(self, arg): - SPLWin = user32.FindWindowA("SPLStudio", None) - t = sendMessage(SPLWin, 1024, arg, 105) - if t < 0: + # Specific to time scripts using Studio API. + def announceTime(self, t, offset = None): + if t <= 0: ui.message("00:00") else: - tm = (t/1000)+1 + tm = (t/1000) if not offset else (t/1000)+offset if tm < 60: tm1, tm2 = "00", tm else: tm1, tm2 = tm/60, tm%60 - if tm1 < 10: - tm1 = "0" + str(tm1) - if tm2 < 10: - tm2 = "0" + str(tm2) + if tm1 < 10: + tm1 = "0" + str(tm1) + if tm2 < 10: + tm2 = "0" + str(tm2) ui.message("{a}:{b}".format(a = tm1, b = tm2)) # Scripts which rely on API. @@ -352,7 +552,7 @@ class AppModule(appModuleHandler.AppModule): if self.SPLCurVersion >= SPLMinVersion: fgWindow = api.getForegroundObject() if fgWindow.windowClassName == "TStudioForm": - self.timeAPI(3) + self.statusAPI(3, 105, self.announceTime, offset=1) else: ui.message(self.timeMessageErrors[1]) else: @@ -365,7 +565,7 @@ class AppModule(appModuleHandler.AppModule): if self.SPLCurVersion >= SPLMinVersion: fgWindow = api.getForegroundObject() if fgWindow.windowClassName == "TStudioForm": - self.timeAPI(0) + self.statusAPI(0, 105, self.announceTime, offset=1) else: ui.message(self.timeMessageErrors[2]) else: @@ -405,7 +605,7 @@ class AppModule(appModuleHandler.AppModule): # Says complete time in hours, minutes and seconds via kernel32's routines. if self.SPLCurVersion >= SPLMinVersion : if api.getForegroundObject().windowClassName == "TStudioForm": - ui.message(GetTimeFormat(LOCALE_USER_DEFAULT, 0, None, None)) + ui.message(winKernel.GetTimeFormat(winKernel.LOCALE_USER_DEFAULT, 0, None, None)) else: ui.message(self.timeMessageErrors[4]) else: @@ -416,7 +616,7 @@ class AppModule(appModuleHandler.AppModule): # Set the end of track alarm time between 1 and 59 seconds. def script_setEndOfTrackTime(self, gesture): - timeVal = long(self.SPLEndOfTrackTime[-2:]) + timeVal = long(SPLConfig["EndOfTrackTime"]) # Translators: A dialog message to set end of track alarm (curAlarmSec is the current end of track alarm in seconds). timeMSG = _("Enter end of track alarm time in seconds (currently {curAlarmSec})").format(curAlarmSec = timeVal) dlg = wx.NumberEntryDialog(gui.mainFrame, @@ -427,10 +627,7 @@ class AppModule(appModuleHandler.AppModule): def callback(result): global SPLConfig if result == wx.ID_OK: - if dlg.GetValue() <= 9: newAlarmSec = "0" + str(dlg.GetValue()) - else: newAlarmSec = str(dlg.GetValue()) - self.SPLEndOfTrackTime = self.SPLEndOfTrackTime.replace(self.SPLEndOfTrackTime[-2:], newAlarmSec) - if SPLConfig is not None: SPLConfig["EndOfTrackTime"] = self.SPLEndOfTrackTime + SPLConfig["EndOfTrackTime"] = dlg.GetValue() gui.runScriptModalDialog(dlg, callback) # Translators: Input help mode message for a command in Station Playlist Studio. script_setEndOfTrackTime.__doc__=_("sets end of track alarm (default is 5 seconds).") @@ -438,7 +635,7 @@ class AppModule(appModuleHandler.AppModule): # Set song ramp (introduction) time between 1 and 9 seconds. def script_setSongRampTime(self, gesture): - rampVal = long(self.SPLSongRampTime[-1]) + rampVal = long(SPLConfig["SongRampTime"]) # Translators: A dialog message to set song ramp alarm (curRampSec is the current intro monitoring alarm in seconds). timeMSG = _("Enter song intro alarm time in seconds (currently {curRampSec})").format(curRampSec = rampVal) dlg = wx.NumberEntryDialog(gui.mainFrame, @@ -448,8 +645,7 @@ class AppModule(appModuleHandler.AppModule): rampVal, 1, 9) def callback(result): if result == wx.ID_OK: - self.SPLSongRampTime = self.SPLSongRampTime.replace(self.SPLSongRampTime[-1], str(dlg.GetValue())) - if SPLConfig is not None: SPLConfig["SongRampTime"] = self.SPLSongRampTime + SPLConfig["SongRampTime"] = dlg.GetValue() gui.runScriptModalDialog(dlg, callback) # Translators: Input help mode message for a command in Station Playlist Studio. script_setSongRampTime.__doc__=_("sets song intro alarm (default is 5 seconds).") @@ -457,9 +653,10 @@ class AppModule(appModuleHandler.AppModule): # Tell NVDA to play a sound when mic was active for a long time. def script_setMicAlarm(self, gesture): - if self.micAlarm: + micAlarm = str(SPLConfig["MicAlarm"]) + if int(micAlarm): # Translators: A dialog message to set microphone active alarm (curAlarmSec is the current mic monitoring alarm in seconds). - timeMSG = _("Enter microphone alarm time in seconds (currently {curAlarmSec}, 0 disables the alarm)").format(curAlarmSec = self.micAlarm) + timeMSG = _("Enter microphone alarm time in seconds (currently {curAlarmSec}, 0 disables the alarm)").format(curAlarmSec = micAlarm) else: # Translators: A dialog message when microphone alarm is disabled (set to 0). timeMSG = _("Enter microphone alarm time in seconds (currently disabled, 0 disables the alarm)") @@ -467,7 +664,7 @@ class AppModule(appModuleHandler.AppModule): timeMSG, # Translators: The title of mic alarm dialog. _("Microphone alarm"), - defaultValue=str(self.micAlarm)) + defaultValue=micAlarm) def callback(result): if result == wx.ID_OK: if not dlg.GetValue().isdigit(): @@ -476,12 +673,16 @@ class AppModule(appModuleHandler.AppModule): # Translators: Standard title for error dialog (copy this from main nvda.po file). _("Error"),wx.OK|wx.ICON_ERROR) else: - self.micAlarm = int(dlg.GetValue()) - if SPLConfig is not None: SPLConfig["MicAlarm"] = self.micAlarm + if SPLConfig is not None: SPLConfig["MicAlarm"] = dlg.GetValue() gui.runScriptModalDialog(dlg, callback) # Translators: Input help mode message for a command in Station Playlist Studio. script_setMicAlarm.__doc__=_("Sets microphone alarm (default is 5 seconds).") + # SPL Config management. + + def script_openConfigDialog(self, gesture): + gui.mainFrame._popupSettingsDialog(SPLConfigDialog) + # Other commands (track finder and others) # Toggle whether beeps should be heard instead of toggle announcements. @@ -882,10 +1083,10 @@ class AppModule(appModuleHandler.AppModule): # Status table keys SPLPlayStatus = 0 SPLSystemStatus = 1 - SPLHourTrackDuration = 2 + #SPLHourTrackDuration = 2 SPLHourSelectedDuration = 3 SPLNextTrackTitle = 4 - SPLPlaylistRemainingDuration = 5 + #SPLPlaylistRemainingDuration = 5 SPLTemperature = 6 SPLScheduled = 7 @@ -895,28 +1096,34 @@ class AppModule(appModuleHandler.AppModule): statusObjs={ SPLPlayStatus:[0, 5, 6], # Play status, mic, etc. SPLSystemStatus:[-2, -3, -2], # The second status bar containing system status such as up time. - SPLHourTrackDuration:[13, 17, 18], # For track duration for the given hour marker. + #SPLHourTrackDuration:[13, 17, 18], # For track duration for the given hour marker. SPLHourSelectedDuration:[14, 18, 19], # In case the user selects one or more tracks in a given hour. SPLScheduled:[15, 19, 20], # Time when the selected track will begin. SPLNextTrackTitle:[2, 7, 8], # Name and duration of the next track if any. - SPLPlaylistRemainingDuration:[12, 16, 17], # Remaining time for the current playlist. + #SPLPlaylistRemainingDuration:[12, 16, 17], # Remaining time for the current playlist. SPLTemperature:[1, 6, 7], # Temperature for the current city. } # Called in the layer commands themselves. def status(self, infoIndex): + #c = time.clock() ver, fg = self.productVersion, api.getForegroundObject() if ver.startswith("4"): statusObj = self.statusObjs[infoIndex][0] elif ver.startswith("5"): if not self.spl510used: statusObj = self.statusObjs[infoIndex][1] else: statusObj = self.statusObjs[infoIndex][2] + #ui.message("{c}".format(c = time.clock()-c)) return fg.children[statusObj] # The layer commands themselves. def script_sayPlayStatus(self, gesture): - obj = self.status(self.SPLPlayStatus).children[0] - ui.message(obj.name) + """obj = self.status(self.SPLPlayStatus).children[0] + ui.message(obj.name)""" + if self.statusAPI(0, 104, ret=True): + ui.message("Play status: Playing") + else: + ui.message("Play status: Stopped") def script_sayAutomationStatus(self, gesture): obj = self.status(self.SPLPlayStatus).children[1] @@ -939,16 +1146,18 @@ class AppModule(appModuleHandler.AppModule): ui.message(obj.name) def script_sayHourTrackDuration(self, gesture): - obj = self.status(self.SPLHourTrackDuration).firstChild - ui.message(obj.name) + """obj = self.status(self.SPLHourTrackDuration).firstChild + ui.message(obj.name)""" + self.statusAPI(0, 27, self.announceTime) def script_sayHourSelectedTrackDuration(self, gesture): obj = self.status(self.SPLHourSelectedDuration).firstChild ui.message(obj.name) def script_sayPlaylistRemainingDuration(self, gesture): - obj = self.status(self.SPLPlaylistRemainingDuration).children[1] - ui.message(obj.name) + """obj = self.status(self.SPLPlaylistRemainingDuration).children[1] + ui.message(obj.name)""" + self.statusAPI(1, 27, self.announceTime) def script_sayPlaylistModified(self, gesture): try: @@ -1044,6 +1253,7 @@ class AppModule(appModuleHandler.AppModule): "kb:alt+nvda+r":"setLibraryScanProgress", "kb:control+shift+r":"startScanFromInsertTracks", "kb:control+shift+x":"setBrailleTimer", + "kb:control+NVDA+0":"openConfigDialog", "kb:Shift+delete":"deleteTrack", "kb:Shift+numpadDelete":"deleteTrack", #"kb:control+nvda+`":"SPLAssistantToggle" https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/5380380676e7/ Changeset: 5380380676e7 Branch: None User: josephsl Date: 2015-02-03 12:18:00+00:00 Summary: Background monitoring is meant for 5.0 branch Affected #: 1 file diff --git a/addon/appModules/splstudio.py b/addon/appModules/splstudio.py index 96c59d7..4654a49 100644 --- a/addon/appModules/splstudio.py +++ b/addon/appModules/splstudio.py @@ -10,13 +10,9 @@ # Minimum version: SPL 5.00, NvDA 2014.3. -import ctypes -from ctypes import wintypes from functools import wraps import os -from cStringIO import StringIO from configobj import ConfigObj -from validate import Validator import time import threading import controlTypes @@ -24,7 +20,6 @@ import appModuleHandler import api import globalVars import review -import eventHandler import scriptHandler import ui import nvwave @@ -33,8 +28,8 @@ import braille import gui import wx from winUser import user32, sendMessage -import winKernel -from NVDAObjects.IAccessible import IAccessible, sysListView32 # SysListView32 is used for Track Dial (see below). +from winKernel import GetTimeFormat, LOCALE_USER_DEFAULT +from NVDAObjects.IAccessible import IAccessible import textInfos import tones import addonHandler @@ -56,40 +51,13 @@ def finally_(func, final): # Make sure the broadcaster is running a compatible version. SPLMinVersion = "5.00" -# Configuration management -SPLIni = os.path.join(globalVars.appArgs.configPath, "splstudio.ini") -confspec = ConfigObj(StringIO(""" -BeepAnnounce = boolean(default=false) -EndOfTrackTime = integer(min=1, max=59, default=5) -SongRampTime = integer(min=1, max=9, default=5) -MicAlarm = integer(min=0, default="0") -"""), encoding="UTF-8", list_values=False) -confspec.newlines = "\r\n" -SPLConfig = None - -# List of values to be converted manually. -# This will be called only once: when upgrading from prior versions to 5.0, to be removed in 5.1. -configConversions=("EndOfTrackTime", "SongRampTime") - -# The accompanying function for config conversion. -# Returns config=false if errors occur, to be checked in the app module constructor. -def config4to5(): - global SPLConfig, configConversions - for setting in configConversions: - oldValue = str(SPLConfig[setting]) - if oldValue.isdigit(): - continue - # If the old value doesn't conform to below conditions, start from a fresh config spec. - if (len(oldValue) != 5 - and not oldValue.startswith("00:") - and not oldValue.split(":")[1].isdigit()): - return False - newValue = SPLConfig[setting].split(":")[1] - SPLConfig[setting] = int(newValue) - return True +# Configuration management )4.0 and later; will not be ported to 3.x). +SPLConfig = ConfigObj(os.path.join(globalVars.appArgs.configPath, "splstudio.ini")) # Display an error dialog when configuration is wrong. def runConfigErrorDialog(): + global SPLConfig + if SPLConfig is not None: SPLConfig.write() wx.CallAfter(gui.messageBox, # Translators: Standard dialog message when Studio configuration has problems. _("Your Studio configuration has errors and was reset to factory defaults."), @@ -110,104 +78,10 @@ def messageSound(wavFile, message): # Routines for track items themselves (prepare for future work). class SPLTrackItem(IAccessible): - """Track item for earlier versions of Studio such as 5.00. - A base class for providing utility scripts when track entries are focused, such as track dial.""" - - def initOverlayClass(self): - if self.appModule.SPLTrackDial: - self.bindGesture("kb:rightArrow", "nextColumn") - self.bindGesture("kb:leftArrow", "prevColumn") - - # Track Dial: using arrow keys to move through columns. - # This is similar to enhanced arrow keys in other screen readers. - - def script_toggleTrackDial(self, gesture): - if not self.appModule.SPLTrackDial: - self.appModule.SPLTrackDial = True - self.bindGesture("kb:rightArrow", "nextColumn") - self.bindGesture("kb:leftArrow", "prevColumn") - dialText = "Track Dial on" - if self.appModule.SPLColNumber > 0: - dialText+= ", located at column {columnHeader}".format(columnHeader = self.appModule.SPLColNumber+1) - ui.message(dialText) - else: - self.appModule.SPLTrackDial = False - try: - self.removeGestureBinding("kb:rightArrow") - self.removeGestureBinding("kb:leftArrow") - except KeyError: - pass - ui.message("Track Dial off") - - # Some helper functions to handle corner cases. - # Each track item provides its own version. - def _leftmostcol(self): - leftmost = self.columnHeaders.firstChild.name - if not self.name or self.name == "": - ui.message("{leftmostColumn} not found".format(leftmostColumn = leftmost)) - else: - ui.message("{leftmostColumn}: {leftmostContent}".format(leftmostColumn = self.columnHeaders.children[self.appModule.SPLColNumber].name, leftmostContent = self.name)) - - # Locate column content. - def _getColumnContent(self, col): - # Borrowed from SysListView32 implementation. - # For add-on 6.0: see if track items can be subclassed from SysListView32.ListItem. - buffer=None - processHandle=self.processHandle - # Because each process in an OS has separate memory spaces, use VM functions to find column content. - internalItem=winKernel.virtualAllocEx(processHandle,None,ctypes.sizeof(sysListView32.LVITEM),winKernel.MEM_COMMIT,winKernel.PAGE_READWRITE) - try: - internalText=winKernel.virtualAllocEx(processHandle,None,520,winKernel.MEM_COMMIT,winKernel.PAGE_READWRITE) - try: - item=sysListView32.LVITEM(iItem=self.IAccessibleChildID-1,mask=sysListView32.LVIF_TEXT|sysListView32.LVIF_COLUMNS,iSubItem=col,pszText=internalText,cchTextMax=260) - winKernel.writeProcessMemory(processHandle,internalItem,ctypes.byref(item),ctypes.sizeof(sysListView32.LVITEM),None) - len = sendMessage(self.windowHandle,sysListView32.LVM_GETITEMTEXTW, (self.IAccessibleChildID-1), internalItem) - if len: - winKernel.readProcessMemory(processHandle,internalItem,ctypes.byref(item),ctypes.sizeof(sysListView32.LVITEM),None) - buffer=ctypes.create_unicode_buffer(len) - winKernel.readProcessMemory(processHandle,item.pszText,buffer,ctypes.sizeof(buffer),None) - finally: - winKernel.virtualFreeEx(processHandle,internalText,0,winKernel.MEM_RELEASE) - finally: - winKernel.virtualFreeEx(processHandle,internalItem,0,winKernel.MEM_RELEASE) - return buffer.value if buffer else None - - # Announce column content if any. - def announceColumnContent(self, colNumber): - columnHeader = self.columnHeaders.children[colNumber].name - columnContent = self._getColumnContent(colNumber) - if columnContent: - ui.message("{header}: {content}".format(header = columnHeader, content = columnContent)) - else: - speech.speakMessage("{header}: blank".format(header = columnHeader)) - braille.handler.message("{header}: ()".format(header = columnHeader)) - - # Now the scripts. - - def script_nextColumn(self, gesture): - self.columnHeaders = self.parent.children[-1] - if (self.appModule.SPLColNumber+1) == self.columnHeaders.childCount: - tones.beep(2000, 100) - else: - self.appModule.SPLColNumber +=1 - self.announceColumnContent(self.appModule.SPLColNumber) + """Track item for earlier versions of Studio such as 5.00.""" + pass - def script_prevColumn(self, gesture): - self.columnHeaders = self.parent.children[-1] - if self.appModule.SPLColNumber <= 0: - tones.beep(2000, 100) - else: - self.appModule.SPLColNumber -=1 - if self.appModule.SPLColNumber == 0: - self._leftmostcol() - else: - self.announceColumnContent(self.appModule.SPLColNumber) - - __gestures={ - "kb:control+`":"toggleTrackDial", - } - -class SPL510TrackItem(SPLTrackItem): +class SPL510TrackItem(IAccessible): """ Track item for Studio 5.10 and later.""" def script_select(self, gesture): @@ -215,105 +89,14 @@ class SPL510TrackItem(SPLTrackItem): speech.speakMessage(self.name) braille.handler.handleUpdate(self) - # Handle track dial for SPL 5.10. - def _leftmostcol(self): - if not self.name: - ui.message("Status not found") - else: - ui.message("Status: {n}".format(n = self.name)) - __gestures={"kb:space":"select"} -# Configuration dialog. -class SPLConfigDialog(gui.SettingsDialog): - # Translators: This is the label for the StationPlaylist Studio configuration dialog. - title = _("Studio Add-on Settings") - - def makeSettings(self, settingsSizer): - - sizer = wx.BoxSizer(wx.HORIZONTAL) - # Translators: the label for a setting in SPL add-on settings to set status announcement between words and beeps. - self.beepAnnounceCheckbox=wx.CheckBox(self,wx.NewId(),label=_("&Beep for status announcements")) - self.beepAnnounceCheckbox.SetValue(SPLConfig["BeepAnnounce"]) - sizer.Add(self.beepAnnounceCheckbox, border=10,flag=wx.BOTTOM) - - # Translators: The label for a setting in SPL Add-on settings to specify end of track (outro) alarm. - label = wx.StaticText(self, wx.ID_ANY, label=_("&End of track alarm in seconds")) - sizer.Add(label) - self.endOfTrackAlarm = wx.SpinCtrl(self, wx.ID_ANY, min=1, max=59) - self.endOfTrackAlarm.SetValue(long(SPLConfig["EndOfTrackTime"])) - sizer.Add(self.endOfTrackAlarm) - settingsSizer.Add(sizer, border=10, flag=wx.BOTTOM) - - sizer = wx.BoxSizer(wx.HORIZONTAL) - # Translators: The label for a setting in SPL Add-on settings to specify track intro alarm. - label = wx.StaticText(self, wx.ID_ANY, label=_("Track &intro alarm in seconds")) - sizer.Add(label) - self.introAlarm = wx.SpinCtrl(self, wx.ID_ANY, min=1, max=9) - self.introAlarm.SetValue(long(SPLConfig["SongRampTime"])) - sizer.Add(self.introAlarm) - settingsSizer.Add(sizer, border=10, flag=wx.BOTTOM) - - sizer = wx.BoxSizer(wx.HORIZONTAL) - # Translators: The label for a setting in SPL Add-on settings to change microphone alarm setting. - label = wx.StaticText(self, wx.ID_ANY, label=_("&Microphone alarm in seconds")) - sizer.Add(label) - self.micAlarm = wx.TextCtrl(self, wx.ID_ANY) - self.micAlarm.SetValue(str(SPLConfig["MicAlarm"])) - sizer.Add(self.micAlarm) - settingsSizer.Add(sizer, border=10, flag=wx.BOTTOM) - - def postInit(self): - self.beepAnnounceCheckbox.SetFocus() - - def onOk(self, evt): - if not self.micAlarm.Value.isdigit(): - gui.messageBox( - # Translators: Message to report wrong value for microphone alarm. - _("Incorrect microphone alarm value entered."), - # Translators: The title of the message box - _("Error"), wx.OK|wx.ICON_ERROR,self) - self.micAlarm.SetFocus() - return - SPLConfig["EndOfTrackTime"] = self.endOfTrackAlarm.Value - SPLConfig["SongRampTime"] = self.introAlarm.Value - SPLConfig["MicAlarm"] = self.micAlarm.Value - super(SPLConfigDialog, self).onOk(evt) - class AppModule(appModuleHandler.AppModule): # Translators: Script category for Station Playlist commands in input gestures dialog. scriptCategory = _("Station Playlist Studio") - # Prepare the settings dialog among other things. - def __init__(self, *args, **kwargs): - super(AppModule, self).__init__(*args, **kwargs) - global SPLConfig, SPLIni, confspec - SPLConfig = ConfigObj(SPLIni, configspec = confspec, encoding="UTF-8") - val = Validator() - configTest = SPLConfig.validate(val, copy=True) - if isinstance(configTest, dict) and not config4to5(): - os.remove(SPLIni) - SPLConfig2 = ConfigObj(SPLIni, configspec = confspec, encoding="UTF-8") - SPLConfig2.write() - SPLConfig = ConfigObj(SPLIni, configspec = confspec, encoding="UTF-8") - try: - runConfigErrorDialog() - except AttributeError: - pass - # Announce status changes while using other programs. - # This requires NVDA core support and will be available in 5.0 and later (cannot be ported to earlier versions). - # For now, handle all background events, but in the end, make this configurable. - if hasattr(eventHandler, "requestEvents"): - eventHandler.requestEvents(eventName="nameChange", processId=self.processID, windowClassName="TStatusBar") - self.backgroundStatusMonitor = True - else: - self.backgroundStatusMonitor = False - self.prefsMenu = gui.mainFrame.sysTrayIcon.preferencesMenu - self.SPLSettings = self.prefsMenu.Append(wx.ID_ANY, _("SPL Studio Settings..."), _("SPL settings")) - gui.mainFrame.sysTrayIcon.Bind(wx.EVT_MENU, self.script_openConfigDialog, self.SPLSettings) - # Play beeps instead of announcing toggles. beepAnnounce = False SPLCurVersion = appModuleHandler.AppModule.productVersion @@ -336,59 +119,96 @@ class AppModule(appModuleHandler.AppModule): fg = api.getForegroundObject() if fg: role = obj.role - if obj.windowClassName == "TTntListView.UnicodeClass" and fg.windowClassName == "TStudioForm" and role == controlTypes.ROLE_LISTITEM: + # Ignore hour markers when using add-on 3.x and 4.x with Studio 5.10. + if obj.windowClassName == "TTntListView.UnicodeClass" and fg.windowClassName == "TStudioForm" and role == controlTypes.ROLE_LISTITEM and obj.name: clsList.insert(0, SPL510TrackItem) - elif obj.windowClassName == "TListView" and fg.windowClassName == "TStudioForm" and role == controlTypes.ROLE_CHECKBOX: - clsList.insert(0, SPLTrackItem) - + # For now, ignore track entries for earlier Studio versions. + """elif obj.windowClassName == "TListView" and fg.windowClassName == "TStudioForm" and role == controlTypes.ROLE_CHECKBOX: + clsList.insert(0, SPLTrackItem)""" + + # populate end of track and intro time alarm settings separately. + unexpectedConfig = False + try: + # Manually correct config mistakes (it'll be automated in add-on 5.0). + SPLEndOfTrackTime = SPLConfig["EndOfTrackTime"] + if (len(SPLEndOfTrackTime) != 5 + or not SPLEndOfTrackTime.startswith("00:") + or SPLEndOfTrackTime[2] != ":" + or not SPLEndOfTrackTime.split(":")[1].isdigit()): + SPLEndOfTrackTime = "00:05" + # Tell NVDA to write the config again. + unexpectedConfig = True + SPLConfig["EndOfTrackTime"] = SPLEndOfTrackTime + except KeyError: + SPLEndOfTrackTime = "00:05" + try: + SPLSongRampTime = SPLConfig["SongRampTime"] + if (len(SPLSongRampTime) != 5 + or not SPLSongRampTime.startswith("00:0") + or SPLSongRampTime[2] != ":" + or not SPLSongRampTime[-1].isdigit()): + SPLSongRampTime = "00:05" + unexpectedConfig = True + SPLConfig["SongRampTime"] = SPLSongRampTime + except KeyError: + SPLSongRampTime = "00:05" # Keep an eye on library scans in insert tracks window. libraryScanning = False scanCount = 0 + # Microphone alarm. + try: + try: + micAlarm = int(SPLConfig["MicAlarm"]) + except ValueError: + micAlarm = 0 + unexpectedConfig = True + SPLConfig["MicAlarm"] = micAlarm + if micAlarm < 0: + micAlarm = 0 + unexpectedConfig = True + SPLConfig["MicAlarm"] = micAlarm + except KeyError: + micAlarm = 0 + # Rewrite the config. + if unexpectedConfig: + runConfigErrorDialog() + unexpectedConfig = False # For SPL 5.10: take care of some object child constant changes across builds. spl510used = False # For 5.0X and earlier: prevent NVDA from announcing scheduled time multiple times. scheduledTimeCache = "" - # Track Dial (A.K.A. enhanced arrow keys) - SPLTrackDial = False - SPLColNumber = 0 # Automatically announce mic, line in, etc changes # These items are static text items whose name changes. # Note: There are two status bars, hence the need to exclude Up time so it doesn't announce every minute. # Unfortunately, Window handles and WindowControlIDs seem to change, so can't be used. def event_nameChange(self, obj, nextHandler): - global noLibScanMonitor # Do not let NvDA get name for None object when SPL window is maximized. + global noLibScanMonitor if not obj.name: return - # Suppress announcing status messages when background monitoring is unavailable. - if api.getForegroundObject().processID != self.processID and not self.backgroundStatusMonitor: - nextHandler() if obj.windowClassName == "TStatusBar" and not obj.name.startswith(" Up time:"): - #tones.beep(512, 200) # Special handling for Play Status + fgWinClass = api.getForegroundObject().windowClassName if obj.IAccessibleChildID == 1: - if "Play status" in obj.name: + if fgWinClass == "TStudioForm": # Strip off " Play status: " for brevity only in main playlist window. - ui.message(obj.name.split(":")[1]) - elif "Loading" in obj.name: - if self.libraryScanProgress > 0: - # If library scan is in progress, announce its progress. - self.scanCount+=1 - if self.scanCount%100 == 0: - if self.libraryScanProgress == self.libraryScanMessage: - tones.beep(550, 100) if self.beepAnnounce else ui.message("Scanning") - elif self.libraryScanProgress == self.libraryScanNumbers: - if self.beepAnnounce: tones.beep(550, 100) - ui.message(obj.name[1:obj.name.find("]")]) - if not self.libraryScanning: - if self.productVersion not in noLibScanMonitor: - if not self.backgroundStatusMonitor: self.libraryScanning = True - elif "match" in obj.name: - if self.libraryScanProgress: + ui.message(obj.name[15:]) + elif fgWinClass == "TTrackInsertForm" and self.libraryScanProgress > 0: + # If library scan is in progress, announce its progress. + self.scanCount+=1 + if self.scanCount%100 == 0: + if self.libraryScanProgress == self.libraryScanMessage: + tones.beep(550, 100) if self.beepAnnounce else ui.message("Scanning") + elif self.libraryScanProgress == self.libraryScanNumbers: + if self.beepAnnounce: tones.beep(550, 100) + ui.message(obj.name[1:obj.name.find("]")]) + if "Loading" in obj.name and not self.libraryScanning: + if self.productVersion not in noLibScanMonitor: self.libraryScanning = True + elif "match" in obj.name: tones.beep(370, 100) if self.beepAnnounce else ui.message("Scan complete") - if self.libraryScanning: self.libraryScanning = False - self.scanCount = 0 + if self.libraryScanning: self.libraryScanning = False + self.scanCount = 0 else: if obj.name.startswith("Scheduled for"): if self.scheduledTimeCache == obj.name: return @@ -417,9 +237,9 @@ class AppModule(appModuleHandler.AppModule): return else: ui.message(obj.name) - if self.cartExplorer or int(SPLConfig["MicAlarm"]): - # Activate mic alarm or announce when cart explorer is active. - self.doExtraAction(obj.name) + if self.cartExplorer or self.micAlarm: + # Activate mic alarm or announce when cart explorer is active. + self.doExtraAction(obj.name) # Monitor the end of track and song intro time and announce it. elif obj.windowClassName == "TStaticText": # For future extensions. if obj.simplePrevious != None: @@ -427,21 +247,21 @@ class AppModule(appModuleHandler.AppModule): # End of track for SPL 5.x. if self.brailleTimer in [self.brailleTimerEnding, self.brailleTimerBoth]: #and "00:00" < obj.name <= self.SPLEndOfTrackTime: braille.handler.message(obj.name) - if obj.name == "00:{0:02d}".format(SPLConfig["EndOfTrackTime"]): + if obj.name == self.SPLEndOfTrackTime: tones.beep(440, 200) if obj.simplePrevious.name == "Remaining Song Ramp": # Song intro for SPL 5.x. if self.brailleTimer in [self.brailleTimerIntro, self.brailleTimerBoth]: #and "00:00" < obj.name <= self.SPLSongRampTime: braille.handler.message(obj.name) - if obj.name == "00:{0:02d}".format(SPLConfig["SongRampTime"]): + if obj.name == self.SPLSongRampTime: tones.beep(512, 400) + # Clean this mess with a more elegant solution. nextHandler() # JL's additions # Perform extra action in specific situations (mic alarm, for example). def doExtraAction(self, status): - micAlarm = int(SPLConfig["MicAlarm"]) if self.cartExplorer: if status == "Cart Edit On": # Translators: Presented when cart edit mode is toggled on while cart explorer is on. @@ -449,7 +269,7 @@ class AppModule(appModuleHandler.AppModule): elif status == "Cart Edit Off": # Translators: Presented when cart edit mode is toggled off while cart explorer is on. ui.message(_("Please reenter cart explorer to view updated cart assignments")) - if micAlarm: + if self.micAlarm: # Play an alarm sound from Braille Sense U2. global micAlarmT micAlarmWav = os.path.join(os.path.dirname(__file__), "SPL_MicAlarm.wav") @@ -457,11 +277,11 @@ class AppModule(appModuleHandler.AppModule): micAlarmMessage = _("Warning: Microphone active") # Use a timer to play a tone when microphone was active for more than the specified amount. if status == "Microphone On": - micAlarmT = threading.Timer(micAlarm, messageSound, args=[micAlarmWav, micAlarmMessage]) + micAlarmT = threading.Timer(self.micAlarm, messageSound, args=[micAlarmWav, micAlarmMessage]) try: micAlarmT.start() except RuntimeError: - micAlarmT = threading.Timer(micAlarm, messageSound, args=[micAlarmWav, micAlarmMessage]) + micAlarmT = threading.Timer(self.micAlarm, messageSound, args=[micAlarmWav, micAlarmMessage]) micAlarmT.start() elif status == "Microphone Off": if micAlarmT is not None: micAlarmT.cancel() @@ -478,38 +298,16 @@ class AppModule(appModuleHandler.AppModule): # Continue monitoring library scans among other focus loss management. def event_loseFocus(self, obj, nextHandler): fg = api.getForegroundObject() - if fg.windowClassName == "TTrackInsertForm" and self.libraryScanning and not self.backgroundStatusMonitor: + if fg.windowClassName == "TTrackInsertForm" and self.libraryScanning: global libScanT if not libScanT or (libScanT and not libScanT.isAlive()): self.monitorLibraryScan() nextHandler() - # Call SPL API to obtain needed values. - # This is needed for some Assistant and time commands. - # A thin wrapper around user32.SendMessage and calling a callback if defined. - def statusAPI(self, arg, command, func=None, ret=False, offset=None): - #c = time.clock() - SPLWin = user32.FindWindowA("SPLStudio", None) - val = sendMessage(SPLWin, 1024, arg, command) - #ui.message("{c}".format(c = time.clock()-c)) - if ret: - return val - if func: - func(val) if not offset else func(val, offset) - # Save configuration when terminating. def terminate(self): super(AppModule, self).terminate() - global SPLConfig if SPLConfig is not None: SPLConfig.write() - # Hack: until the public API is available, remove SPL entry from accepted events manually. - eventHandler._acceptEvents.remove(("nameChange", self.processID, "TStatusBar")) - try: - self.prefsMenu.RemoveItem(self.SPLSettings) - except wx.PyDeadObjectError: - pass - SPLConfig = None - # Script sections (for ease of maintenance): # Time-related: elapsed time, end of track alarm, etc. @@ -531,20 +329,22 @@ class AppModule(appModuleHandler.AppModule): 4:_("Cannot obtain time in hours, minutes and seconds") } - # Specific to time scripts using Studio API. - def announceTime(self, t, offset = None): - if t <= 0: + # Call SPL API for important time messages. + def timeAPI(self, arg): + SPLWin = user32.FindWindowA("SPLStudio", None) + t = sendMessage(SPLWin, 1024, arg, 105) + if t < 0: ui.message("00:00") else: - tm = (t/1000) if not offset else (t/1000)+offset + tm = (t/1000)+1 if tm < 60: tm1, tm2 = "00", tm else: tm1, tm2 = tm/60, tm%60 - if tm1 < 10: - tm1 = "0" + str(tm1) - if tm2 < 10: - tm2 = "0" + str(tm2) + if tm1 < 10: + tm1 = "0" + str(tm1) + if tm2 < 10: + tm2 = "0" + str(tm2) ui.message("{a}:{b}".format(a = tm1, b = tm2)) # Scripts which rely on API. @@ -552,7 +352,7 @@ class AppModule(appModuleHandler.AppModule): if self.SPLCurVersion >= SPLMinVersion: fgWindow = api.getForegroundObject() if fgWindow.windowClassName == "TStudioForm": - self.statusAPI(3, 105, self.announceTime, offset=1) + self.timeAPI(3) else: ui.message(self.timeMessageErrors[1]) else: @@ -565,7 +365,7 @@ class AppModule(appModuleHandler.AppModule): if self.SPLCurVersion >= SPLMinVersion: fgWindow = api.getForegroundObject() if fgWindow.windowClassName == "TStudioForm": - self.statusAPI(0, 105, self.announceTime, offset=1) + self.timeAPI(0) else: ui.message(self.timeMessageErrors[2]) else: @@ -605,7 +405,7 @@ class AppModule(appModuleHandler.AppModule): # Says complete time in hours, minutes and seconds via kernel32's routines. if self.SPLCurVersion >= SPLMinVersion : if api.getForegroundObject().windowClassName == "TStudioForm": - ui.message(winKernel.GetTimeFormat(winKernel.LOCALE_USER_DEFAULT, 0, None, None)) + ui.message(GetTimeFormat(LOCALE_USER_DEFAULT, 0, None, None)) else: ui.message(self.timeMessageErrors[4]) else: @@ -616,7 +416,7 @@ class AppModule(appModuleHandler.AppModule): # Set the end of track alarm time between 1 and 59 seconds. def script_setEndOfTrackTime(self, gesture): - timeVal = long(SPLConfig["EndOfTrackTime"]) + timeVal = long(self.SPLEndOfTrackTime[-2:]) # Translators: A dialog message to set end of track alarm (curAlarmSec is the current end of track alarm in seconds). timeMSG = _("Enter end of track alarm time in seconds (currently {curAlarmSec})").format(curAlarmSec = timeVal) dlg = wx.NumberEntryDialog(gui.mainFrame, @@ -627,7 +427,10 @@ class AppModule(appModuleHandler.AppModule): def callback(result): global SPLConfig if result == wx.ID_OK: - SPLConfig["EndOfTrackTime"] = dlg.GetValue() + if dlg.GetValue() <= 9: newAlarmSec = "0" + str(dlg.GetValue()) + else: newAlarmSec = str(dlg.GetValue()) + self.SPLEndOfTrackTime = self.SPLEndOfTrackTime.replace(self.SPLEndOfTrackTime[-2:], newAlarmSec) + if SPLConfig is not None: SPLConfig["EndOfTrackTime"] = self.SPLEndOfTrackTime gui.runScriptModalDialog(dlg, callback) # Translators: Input help mode message for a command in Station Playlist Studio. script_setEndOfTrackTime.__doc__=_("sets end of track alarm (default is 5 seconds).") @@ -635,7 +438,7 @@ class AppModule(appModuleHandler.AppModule): # Set song ramp (introduction) time between 1 and 9 seconds. def script_setSongRampTime(self, gesture): - rampVal = long(SPLConfig["SongRampTime"]) + rampVal = long(self.SPLSongRampTime[-1]) # Translators: A dialog message to set song ramp alarm (curRampSec is the current intro monitoring alarm in seconds). timeMSG = _("Enter song intro alarm time in seconds (currently {curRampSec})").format(curRampSec = rampVal) dlg = wx.NumberEntryDialog(gui.mainFrame, @@ -645,7 +448,8 @@ class AppModule(appModuleHandler.AppModule): rampVal, 1, 9) def callback(result): if result == wx.ID_OK: - SPLConfig["SongRampTime"] = dlg.GetValue() + self.SPLSongRampTime = self.SPLSongRampTime.replace(self.SPLSongRampTime[-1], str(dlg.GetValue())) + if SPLConfig is not None: SPLConfig["SongRampTime"] = self.SPLSongRampTime gui.runScriptModalDialog(dlg, callback) # Translators: Input help mode message for a command in Station Playlist Studio. script_setSongRampTime.__doc__=_("sets song intro alarm (default is 5 seconds).") @@ -653,10 +457,9 @@ class AppModule(appModuleHandler.AppModule): # Tell NVDA to play a sound when mic was active for a long time. def script_setMicAlarm(self, gesture): - micAlarm = str(SPLConfig["MicAlarm"]) - if int(micAlarm): + if self.micAlarm: # Translators: A dialog message to set microphone active alarm (curAlarmSec is the current mic monitoring alarm in seconds). - timeMSG = _("Enter microphone alarm time in seconds (currently {curAlarmSec}, 0 disables the alarm)").format(curAlarmSec = micAlarm) + timeMSG = _("Enter microphone alarm time in seconds (currently {curAlarmSec}, 0 disables the alarm)").format(curAlarmSec = self.micAlarm) else: # Translators: A dialog message when microphone alarm is disabled (set to 0). timeMSG = _("Enter microphone alarm time in seconds (currently disabled, 0 disables the alarm)") @@ -664,7 +467,7 @@ class AppModule(appModuleHandler.AppModule): timeMSG, # Translators: The title of mic alarm dialog. _("Microphone alarm"), - defaultValue=micAlarm) + defaultValue=str(self.micAlarm)) def callback(result): if result == wx.ID_OK: if not dlg.GetValue().isdigit(): @@ -673,16 +476,12 @@ class AppModule(appModuleHandler.AppModule): # Translators: Standard title for error dialog (copy this from main nvda.po file). _("Error"),wx.OK|wx.ICON_ERROR) else: - if SPLConfig is not None: SPLConfig["MicAlarm"] = dlg.GetValue() + self.micAlarm = int(dlg.GetValue()) + if SPLConfig is not None: SPLConfig["MicAlarm"] = self.micAlarm gui.runScriptModalDialog(dlg, callback) # Translators: Input help mode message for a command in Station Playlist Studio. script_setMicAlarm.__doc__=_("Sets microphone alarm (default is 5 seconds).") - # SPL Config management. - - def script_openConfigDialog(self, gesture): - gui.mainFrame._popupSettingsDialog(SPLConfigDialog) - # Other commands (track finder and others) # Toggle whether beeps should be heard instead of toggle announcements. @@ -1083,10 +882,10 @@ class AppModule(appModuleHandler.AppModule): # Status table keys SPLPlayStatus = 0 SPLSystemStatus = 1 - #SPLHourTrackDuration = 2 + SPLHourTrackDuration = 2 SPLHourSelectedDuration = 3 SPLNextTrackTitle = 4 - #SPLPlaylistRemainingDuration = 5 + SPLPlaylistRemainingDuration = 5 SPLTemperature = 6 SPLScheduled = 7 @@ -1096,34 +895,28 @@ class AppModule(appModuleHandler.AppModule): statusObjs={ SPLPlayStatus:[0, 5, 6], # Play status, mic, etc. SPLSystemStatus:[-2, -3, -2], # The second status bar containing system status such as up time. - #SPLHourTrackDuration:[13, 17, 18], # For track duration for the given hour marker. + SPLHourTrackDuration:[13, 17, 18], # For track duration for the given hour marker. SPLHourSelectedDuration:[14, 18, 19], # In case the user selects one or more tracks in a given hour. SPLScheduled:[15, 19, 20], # Time when the selected track will begin. SPLNextTrackTitle:[2, 7, 8], # Name and duration of the next track if any. - #SPLPlaylistRemainingDuration:[12, 16, 17], # Remaining time for the current playlist. + SPLPlaylistRemainingDuration:[12, 16, 17], # Remaining time for the current playlist. SPLTemperature:[1, 6, 7], # Temperature for the current city. } # Called in the layer commands themselves. def status(self, infoIndex): - #c = time.clock() ver, fg = self.productVersion, api.getForegroundObject() if ver.startswith("4"): statusObj = self.statusObjs[infoIndex][0] elif ver.startswith("5"): if not self.spl510used: statusObj = self.statusObjs[infoIndex][1] else: statusObj = self.statusObjs[infoIndex][2] - #ui.message("{c}".format(c = time.clock()-c)) return fg.children[statusObj] # The layer commands themselves. def script_sayPlayStatus(self, gesture): - """obj = self.status(self.SPLPlayStatus).children[0] - ui.message(obj.name)""" - if self.statusAPI(0, 104, ret=True): - ui.message("Play status: Playing") - else: - ui.message("Play status: Stopped") + obj = self.status(self.SPLPlayStatus).children[0] + ui.message(obj.name) def script_sayAutomationStatus(self, gesture): obj = self.status(self.SPLPlayStatus).children[1] @@ -1146,18 +939,16 @@ class AppModule(appModuleHandler.AppModule): ui.message(obj.name) def script_sayHourTrackDuration(self, gesture): - """obj = self.status(self.SPLHourTrackDuration).firstChild - ui.message(obj.name)""" - self.statusAPI(0, 27, self.announceTime) + obj = self.status(self.SPLHourTrackDuration).firstChild + ui.message(obj.name) def script_sayHourSelectedTrackDuration(self, gesture): obj = self.status(self.SPLHourSelectedDuration).firstChild ui.message(obj.name) def script_sayPlaylistRemainingDuration(self, gesture): - """obj = self.status(self.SPLPlaylistRemainingDuration).children[1] - ui.message(obj.name)""" - self.statusAPI(1, 27, self.announceTime) + obj = self.status(self.SPLPlaylistRemainingDuration).children[1] + ui.message(obj.name) def script_sayPlaylistModified(self, gesture): try: @@ -1253,7 +1044,6 @@ class AppModule(appModuleHandler.AppModule): "kb:alt+nvda+r":"setLibraryScanProgress", "kb:control+shift+r":"startScanFromInsertTracks", "kb:control+shift+x":"setBrailleTimer", - "kb:control+NVDA+0":"openConfigDialog", "kb:Shift+delete":"deleteTrack", "kb:Shift+numpadDelete":"deleteTrack", #"kb:control+nvda+`":"SPLAssistantToggle" https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/027b04f3da63/ Changeset: 027b04f3da63 Branch: stable User: josephsl Date: 2015-02-03 19:51:43+00:00 Summary: Add-on 4.2 maintenance version, fixing encoder retention issue and a handler for corrupted configuration file Affected #: 2 files diff --git a/addon/appModules/splstudio.py b/addon/appModules/splstudio.py index 4654a49..649b26d 100644 --- a/addon/appModules/splstudio.py +++ b/addon/appModules/splstudio.py @@ -60,9 +60,9 @@ def runConfigErrorDialog(): if SPLConfig is not None: SPLConfig.write() wx.CallAfter(gui.messageBox, # Translators: Standard dialog message when Studio configuration has problems. - _("Your Studio configuration has errors and was reset to factory defaults."), + _("Your Studio add-on configuration has errors and was reset to factory defaults."), # Translators: Standard error title for configuration error. - _("Studio Configuration error"),wx.OK|wx.ICON_ERROR) + _("Studio add-on Configuration error"),wx.OK|wx.ICON_ERROR) # Threads pool. micAlarmT = None diff --git a/buildVars.py b/buildVars.py index 238e616..9a70011 100755 --- a/buildVars.py +++ b/buildVars.py @@ -20,7 +20,7 @@ addon_info = { "addon_description" : _("""Enhances support for Station Playlist Studio. In addition, adds global commands for the studio from everywhere."""), # version - "addon_version" : "4.1", + "addon_version" : "4.2", # Author(s) "addon_author" : u"Geoff Shang, Joseph Lee and other contributors", # URL for the add-on documentation support Repository URL: https://bitbucket.org/nvdaaddonteam/stationplaylist/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.