commit/StationPlaylist: 81 new changesets

  • From: commits-noreply@xxxxxxxxxxxxx
  • To: nvda-addons-commits@xxxxxxxxxxxxx
  • Date: Fri, 24 Mar 2017 23:58:54 -0000

81 new commits in StationPlaylist:

https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/f526698d1391/
Changeset:   f526698d1391
Branch:      None
User:        josephsl
Date:        2016-12-15 19:06:10+00:00
Summary:     Playlist snapshots (17.1-dev): Laying the foundation for Playlist 
Snapshots, gathering statistics about a playlist.

Plalist snapshots is used to gather and present statistics about playlists. 
This includes total duration of tracks, minimum and maximum and other goodies 
(all configurable).
For now, total duration of remaining times in the playlist will be returned 
using the new playlist snapshots function (destined for 17.1).

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 037b666..eaac413 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1362,22 +1362,24 @@ class AppModule(appModuleHandler.AppModule):
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
        script_manageMetadataStreams.__doc__=_("Opens a dialog to quickly 
enable or disable metadata streaming.")
 
-       # Track time analysis
+       # Track time analysis/Playlist snapshots
        # Return total length of the selected tracks upon request.
        # Analysis command (SPL Assistant) will be assignable.
+       # Also gather various data about the playlist.
        _analysisMarker = None
 
-       # Trakc time analysis requires main playlist viewer to be the 
foreground window.
+       # Trakc time analysis and playlist snapshots require main playlist 
viewer to be the foreground window.
        def _trackAnalysisAllowed(self):
                if api.getForegroundObject().windowClassName != "TStudioForm":
                        # Translators: Presented when track time anlaysis 
cannot be performed because user is not focused on playlist viewer.
-                       ui.message(_("Not in playlist viewer, cannot perform 
track time analysis"))
+                       ui.message(_("Not in playlist viewer, cannot perform 
track time analysis or gather playlist snapshot statistics"))
                        return False
                return True
 
        # Return total duration of a range of tracks.
        # This is used in track time analysis when multiple tracks are selected.
        # This is also called from playlist duration scripts.
+       # To be replaced by general track duration script, with the difference 
being start and end location.
        def totalTime(self, start, end):
                # Take care of errors such as the following.
                if start < 0 or end > statusAPI(0, 124, ret=True)-1:
@@ -1393,6 +1395,28 @@ class AppModule(appModuleHandler.AppModule):
                                totalLength+=statusAPI(filename, 30, ret=True)
                return totalLength
 
+       # Playlist snapshots
+       # Data to be gathered comes from a set of flags.
+       def playlistSnapshots(self, obj, end, snapshotFlags=None):
+               # Track count and total duration are always included.
+               snapshot = {"TrackCount":statusAPI(0, 124, ret=True)}
+               duration = obj.indexOf("Duration")
+               title = obj.indexOf("Title")
+               totalDuration = 0
+               titleDuration = []
+               while obj not in (None, end):
+                       # Technically segue.
+                       segue = obj._getColumnContent(duration)
+                       trackTitle = obj._getColumnContent(title)
+                       titleDuration.append((trackTitle, segue))
+                       if segue not in (None, "00:00"):
+                               hms = segue.split(":")
+                               totalDuration += (int(hms[-2])*60) + 
int(hms[-1])
+                               if len(hms) == 3: totalDuration += 
int(hms[0])*3600
+                       obj = obj.next
+               snapshot["DurationTotal"] = totalDuration
+               return snapshot["DurationTotal"]
+
        # Some handlers for native commands.
 
        # In Studio 5.0x, when deleting a track, NVDA announces wrong track 
item due to focus bouncing (not the case in 5.10 and later).
@@ -1586,16 +1610,7 @@ class AppModule(appModuleHandler.AppModule):
                if obj.role == controlTypes.ROLE_LIST:
                        ui.message("00:00")
                        return
-               col = obj.indexOf("Duration")
-               totalDuration = 0
-               while obj is not None:
-                       segue = obj._getColumnContent(col)
-                       if segue not in (None, "00:00"):
-                               hms = segue.split(":")
-                               totalDuration += (int(hms[-2])*60) + 
int(hms[-1])
-                               if len(hms) == 3: totalDuration += 
int(hms[0])*3600
-                       obj = obj.next
-               self.announceTime(totalDuration, ms=False)
+               self.announceTime(self.playlistSnapshots(obj, None), ms=False)
 
        def script_sayPlaylistModified(self, gesture):
                try:
@@ -1745,6 +1760,16 @@ class AppModule(appModuleHandler.AppModule):
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
        script_trackTimeAnalysis.__doc__=_("Announces total length of tracks 
between analysis start marker and the current track")
 
+       def script_takePlaylistSnapshots(self, gesture):
+               obj = api.getFocusObject() if 
api.getForegroundObject().windowClassName == "TStudioForm" else 
self._focusedTrack
+               if obj is None:
+                       ui.message("Please return to playlist viewer before 
invoking this command.")
+                       return
+               if obj.role == controlTypes.ROLE_LIST:
+                       ui.message("00:00")
+                       return
+               self.announceTime(self.playlistSnapshots(obj, None), ms=False)
+
        def script_switchProfiles(self, gesture):
                splconfig.triggerProfileSwitch() if 
splconfig._triggerProfileActive else splconfig.instantProfileSwitch()
 


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/9bf532c1cfe5/
Changeset:   9bf532c1cfe5
Branch:      None
User:        josephsl
Date:        2016-12-17 06:02:12+00:00
Summary:     Merge branch 'master' into plSnaps

Affected #:  5 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index eaac413..5d7ceca 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -962,12 +962,12 @@ class AppModule(appModuleHandler.AppModule):
                        wx.CallAfter(gui.messageBox, _("The add-on settings 
dialog is opened. Please close the settings dialog first."), _("Error"), 
wx.OK|wx.ICON_ERROR)
                        return
                try:
-                       d = splconfig.SPLAlarmDialog(gui.mainFrame, level)
+                       d = splconfui.AlarmsCenter(gui.mainFrame, level)
                        gui.mainFrame.prePopup()
                        d.Raise()
                        d.Show()
                        gui.mainFrame.postPopup()
-                       splconfig._alarmDialogOpened = True
+                       splconfui._alarmDialogOpened = True
                except RuntimeError:
                        wx.CallAfter(splconfig._alarmError)
 

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index b83144e..3351125 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -851,117 +851,9 @@ def _shouldBuildDescriptionPieces():
        or len(SPLConfig["ColumnAnnouncement"]["IncludedColumns"]) != 17))
 
 
-# Additional configuration dialogs
+# Additional configuration and miscellaneous dialogs
 # See splconfui module for basic configuration dialogs.
 
-# A common alarm dialog
-# Based on NVDA core's find dialog code (implemented by the author of this 
add-on).
-# Extended in 2016 to handle microphone alarms.
-# Only one instance can be active at a given moment (code borrowed from GUI's 
exit dialog routine).
-_alarmDialogOpened = False
-
-# A common alarm error dialog.
-def _alarmError():
-       # Translators: Text of the dialog when another alarm dialog is open.
-       gui.messageBox(_("Another alarm dialog is 
open."),_("Error"),style=wx.OK | wx.ICON_ERROR)
-
-class SPLAlarmDialog(wx.Dialog):
-       """A dialog providing common alarm settings.
-       This dialog contains a number entry field for alarm duration and a 
check box to enable or disable the alarm.
-       For one particular case, it consists of two number entry fields.
-       """
-
-       # The following comes from exit dialog class from GUI package (credit: 
NV Access and Zahari from Bulgaria).
-       _instance = None
-
-       def __new__(cls, parent, *args, **kwargs):
-               # Make this a singleton and prompt an error dialog if it isn't.
-               if _alarmDialogOpened:
-                       raise RuntimeError("An instance of alarm dialog is 
opened")
-               inst = cls._instance() if cls._instance else None
-               if not inst:
-                       return super(cls, cls).__new__(cls, parent, *args, 
**kwargs)
-               return inst
-
-       def __init__(self, parent, level=0):
-               inst = SPLAlarmDialog._instance() if SPLAlarmDialog._instance 
else None
-               if inst:
-                       return
-               # Use a weakref so the instance can die.
-               import weakref
-               SPLAlarmDialog._instance = weakref.ref(self)
-
-               # Now the actual alarm dialog code.
-               # 8.0: Apart from level 0 (all settings shown), levels change 
title.
-               titles = (_("Alarms Center"), _("End of track alarm"), _("Song 
intro alarm"), _("Microphone alarm"))
-               super(SPLAlarmDialog, self).__init__(parent, wx.ID_ANY, 
titles[level])
-               self.level = level
-               mainSizer = wx.BoxSizer(wx.VERTICAL)
-               # 17.1: Utilize various enhancements from GUI helper (added in 
NVDA 2016.4).
-               contentSizerHelper = gui.guiHelper.BoxSizerHelper(self, 
orientation=wx.VERTICAL)
-
-               if level in (0, 1):
-                       timeVal = 
SPLConfig["IntroOutroAlarms"]["EndOfTrackTime"]
-                       alarmLabel = _("Enter &end of track alarm time in 
seconds (currently {curAlarmSec})").format(curAlarmSec = timeVal)
-                       self.outroAlarmEntry = 
contentSizerHelper.addLabeledControl(alarmLabel, 
gui.nvdaControls.SelectOnFocusSpinCtrl, min=1, max=59, initial=timeVal)
-                       
self.outroToggleCheckBox=contentSizerHelper.addItem(wx.CheckBox(self, 
label=_("&Notify when end of track is approaching")))
-                       
self.outroToggleCheckBox.SetValue(SPLConfig["IntroOutroAlarms"]["SayEndOfTrack"])
-
-               if level in (0, 2):
-                       rampVal = SPLConfig["IntroOutroAlarms"]["SongRampTime"]
-                       alarmLabel = _("Enter song &intro alarm time in seconds 
(currently {curRampSec})").format(curRampSec = rampVal)
-                       self.introAlarmEntry = 
contentSizerHelper.addLabeledControl(alarmLabel, 
gui.nvdaControls.SelectOnFocusSpinCtrl, min=1, max=9, initial=rampVal)
-                       
self.introToggleCheckBox=contentSizerHelper.addItem(wx.CheckBox(self, 
label=_("&Notify when end of introduction is approaching")))
-                       
self.introToggleCheckBox.SetValue(SPLConfig["IntroOutroAlarms"]["SaySongRamp"])
-
-               if level in (0, 3):
-                       micAlarm = SPLConfig["MicrophoneAlarm"]["MicAlarm"]
-                       micAlarmInterval = 
SPLConfig["MicrophoneAlarm"]["MicAlarmInterval"]
-                       if 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)
-                       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)")
-                       micIntervalMSG = _("Microphone alarm interval")
-                       self.micAlarmEntry = 
contentSizerHelper.addLabeledControl(timeMSG, 
gui.nvdaControls.SelectOnFocusSpinCtrl, min=0, max=7200, initial=micAlarm)
-                       self.micIntervalEntry = 
contentSizerHelper.addLabeledControl(micIntervalMSG, 
gui.nvdaControls.SelectOnFocusSpinCtrl, min=0, max=60, initial=micAlarmInterval)
-
-               
contentSizerHelper.addDialogDismissButtons(self.CreateButtonSizer(wx.OK | 
wx.CANCEL))
-               self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK)
-               self.Bind(wx.EVT_BUTTON, self.onCancel, id=wx.ID_CANCEL)
-               mainSizer.Add(contentSizerHelper.sizer, 
border=gui.guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL)
-               mainSizer.Fit(self)
-               self.SetSizer(mainSizer)
-               self.Center(wx.BOTH | wx.CENTER_ON_SCREEN)
-               if level in (0, 1): self.outroAlarmEntry.SetFocus()
-               elif level == 2: self.introAlarmEntry.SetFocus()
-               elif level == 3: self.micAlarmEntry.SetFocus()
-
-       def onOk(self, evt):
-               global SPLConfig, _alarmDialogOpened
-               # Optimization: don't bother if Studio is dead and if the same 
value has been entered.
-               import winUser
-               if winUser.user32.FindWindowA("SPLStudio", None):
-                       # Gather settings to be applied in section/key format.
-                       if self.level in (0, 1):
-                               SPLConfig["IntroOutroAlarms"]["EndOfTrackTime"] 
= self.outroAlarmEntry.GetValue()
-                               SPLConfig["IntroOutroAlarms"]["SayEndOfTrack"] 
= self.outroToggleCheckBox.GetValue()
-                       elif self.level in (0, 2):
-                               SPLConfig["IntroOutroAlarms"]["SongRampTime"] = 
self.introAlarmEntry.GetValue()
-                               SPLConfig["IntroOutroAlarms"]["SaySongRamp"] = 
self.introToggleCheckBox.GetValue()
-                       elif self.level in (0, 3):
-                               SPLConfig["MicrophoneAlarm"]["MicAlarm"] = 
self.micAlarmEntry.GetValue()
-                               
SPLConfig["MicrophoneAlarm"]["MicAlarmInterval"] = 
self.micIntervalEntry.GetValue()
-               self.Destroy()
-               _alarmDialogOpened = False
-
-       def onCancel(self, evt):
-               self.Destroy()
-               global _alarmDialogOpened
-               _alarmDialogOpened = False
-
-
 # Startup dialogs.
 
 # Audio ducking reminder (NVDA 2016.1 and later).

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 32d41bb..5d6d0df 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -92,40 +92,15 @@ class SPLConfigDialog(gui.SettingsDialog):
                except:
                        pass
 
-               self.outroSizer = wx.BoxSizer(wx.HORIZONTAL)
-               # Check box hiding method comes from Alberto Buffolino's 
Columns Review add-on.
-               # Translators: Label for a check box in SPL add-on settings to 
notify when end of track (outro) is approaching.
-               self.outroCheckBox=wx.CheckBox(self,wx.NewId(),label=_("&Notify 
when end of track is approaching"))
-               
self.outroCheckBox.SetValue(splconfig.SPLConfig["IntroOutroAlarms"]["SayEndOfTrack"])
-               self.outroCheckBox.Bind(wx.EVT_CHECKBOX, self.onOutroCheck)
-               self.outroSizer.Add(self.outroCheckBox, 
border=10,flag=wx.BOTTOM)
-
-               # Translators: The label for a setting in SPL Add-on settings 
to specify end of track (outro) alarm.
-               self.outroAlarmLabel = wx.StaticText(self, wx.ID_ANY, 
label=_("&End of track alarm in seconds"))
-               self.outroSizer.Add(self.outroAlarmLabel)
-               self.endOfTrackAlarm = wx.SpinCtrl(self, wx.ID_ANY, min=1, 
max=59)
-               
self.endOfTrackAlarm.SetValue(long(splconfig.SPLConfig["IntroOutroAlarms"]["EndOfTrackTime"]))
-               self.endOfTrackAlarm.SetSelection(-1, -1)
-               self.outroSizer.Add(self.endOfTrackAlarm)
-               self.onOutroCheck(None)
-               SPLConfigHelper.addItem(self.outroSizer)
-
-               self.introSizer = wx.BoxSizer(wx.HORIZONTAL)
-               # Translators: Label for a check box in SPL add-on settings to 
notify when end of intro is approaching.
-               self.introCheckBox=wx.CheckBox(self,wx.NewId(),label=_("&Notify 
when end of introduction is approaching"))
-               
self.introCheckBox.SetValue(splconfig.SPLConfig["IntroOutroAlarms"]["SaySongRamp"])
-               self.introCheckBox.Bind(wx.EVT_CHECKBOX, self.onIntroCheck)
-               self.introSizer.Add(self.introCheckBox, 
border=10,flag=wx.BOTTOM)
-
-               # Translators: The label for a setting in SPL Add-on settings 
to specify track intro alarm.
-               self.introAlarmLabel = wx.StaticText(self, wx.ID_ANY, 
label=_("&Track intro alarm in seconds"))
-               self.introSizer.Add(self.introAlarmLabel)
-               self.songRampAlarm = wx.SpinCtrl(self, wx.ID_ANY, min=1, max=9)
-               
self.songRampAlarm.SetValue(long(splconfig.SPLConfig["IntroOutroAlarms"]["SongRampTime"]))
-               self.songRampAlarm.SetSelection(-1, -1)
-               self.introSizer.Add(self.songRampAlarm)
-               self.onIntroCheck(None)
-               SPLConfigHelper.addItem(self.introSizer)
+               self.endOfTrackTime = 
splconfig.SPLConfig["IntroOutroAlarms"]["EndOfTrackTime"]
+               self.sayEndOfTrack = 
splconfig.SPLConfig["IntroOutroAlarms"]["SayEndOfTrack"]
+               self.songRampTime = 
splconfig.SPLConfig["IntroOutroAlarms"]["SongRampTime"]
+               self.saySongRamp = 
splconfig.SPLConfig["IntroOutroAlarms"]["SaySongRamp"]
+               self.micAlarm = 
splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarm"]
+               self.micAlarmInterval = 
splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarmInterval"]
+               # Translators: The label of a button to open a dialog to 
configure various alarms.
+               alarmsCenterButton = SPLConfigHelper.addItem(wx.Button(self, 
label=_("&Alarms Center...")))
+               alarmsCenterButton.Bind(wx.EVT_BUTTON, self.onAlarmsCenter)
 
                self.brailleTimerValues=[("off",_("Off")),
                # Translators: One of the braille timer settings.
@@ -143,13 +118,6 @@ class SPLConfigDialog(gui.SettingsDialog):
                except:
                        pass
 
-               sizer = gui.guiHelper.BoxSizerHelper(self, 
orientation=wx.HORIZONTAL)
-               # Translators: The label for a setting in SPL Add-on settings 
to change microphone alarm setting.
-               self.micAlarm = sizer.addLabeledControl(_("&Microphone alarm in 
seconds"), gui.nvdaControls.SelectOnFocusSpinCtrl, min=0, max=7200, 
initial=splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarm"])
-               # Translators: The label for a setting in SPL Add-on settings 
to specify mic alarm interval.
-               self.micAlarmInterval = sizer.addLabeledControl(_("Microphone 
alarm &interval in seconds"), gui.nvdaControls.SelectOnFocusSpinCtrl, min=0, 
max=60, initial=splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarmInterval"])
-               SPLConfigHelper.addItem(sizer)
-
                # Translators: One of the alarm notification options.
                self.alarmAnnounceValues=[("beep",_("beep")),
                # Translators: One of the alarm notification options.
@@ -299,13 +267,13 @@ class SPLConfigDialog(gui.SettingsDialog):
                        
splconfig.SPLConfig.swapProfiles(splconfig.SPLConfig.activeProfile, 
selectedProfile)
                splconfig.SPLConfig["General"]["BeepAnnounce"] = 
self.beepAnnounceCheckbox.Value
                splconfig.SPLConfig["General"]["MessageVerbosity"] = 
self.verbosityLevels[self.verbosityList.GetSelection()][0]
-               splconfig.SPLConfig["IntroOutroAlarms"]["SayEndOfTrack"] = 
self.outroCheckBox.Value
-               splconfig.SPLConfig["IntroOutroAlarms"]["EndOfTrackTime"] = 
self.endOfTrackAlarm.Value
-               splconfig.SPLConfig["IntroOutroAlarms"]["SaySongRamp"] = 
self.introCheckBox.Value
-               splconfig.SPLConfig["IntroOutroAlarms"]["SongRampTime"] = 
self.songRampAlarm.Value
+               splconfig.SPLConfig["IntroOutroAlarms"]["EndOfTrackTime"] = 
self.endOfTrackTime
+               splconfig.SPLConfig["IntroOutroAlarms"]["SayEndOfTrack"] = 
self.sayEndOfTrack
+               splconfig.SPLConfig["IntroOutroAlarms"]["SongRampTime"] = 
self.songRampTime
+               splconfig.SPLConfig["IntroOutroAlarms"]["SaySongRamp"] = 
self.saySongRamp
+               splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarm"] = 
self.micAlarm
+               splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarmInterval"] = 
self.micAlarmInterval
                splconfig.SPLConfig["General"]["BrailleTimer"] = 
self.brailleTimerValues[self.brailleTimerList.GetSelection()][0]
-               splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarm"] = 
self.micAlarm.Value
-               splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarmInterval"] = 
self.micAlarmInterval.Value
                splconfig.SPLConfig["General"]["AlarmAnnounce"] = 
self.alarmAnnounceValues[self.alarmAnnounceList.GetSelection()][0]
                splconfig.SPLConfig["General"]["LibraryScanAnnounce"] = 
self.libScanValues[self.libScanList.GetSelection()][0]
                splconfig.SPLConfig["General"]["TimeHourAnnounce"] = 
self.hourAnnounceCheckbox.Value
@@ -391,25 +359,6 @@ class SPLConfigDialog(gui.SettingsDialog):
                else:
                        if splupdate._SPLUpdateT is None: splconfig.updateInit()
 
-       # Check events for outro and intro alarms, respectively.
-       def onOutroCheck(self, evt):
-               if not self.outroCheckBox.IsChecked():
-                       self.outroSizer.Hide(self.outroAlarmLabel)
-                       self.outroSizer.Hide(self.endOfTrackAlarm)
-               else:
-                       self.outroSizer.Show(self.outroAlarmLabel)
-                       self.outroSizer.Show(self.endOfTrackAlarm)
-               self.Fit()
-
-       def onIntroCheck(self, evt):
-               if not self.introCheckBox.IsChecked():
-                       self.introSizer.Hide(self.introAlarmLabel)
-                       self.introSizer.Hide(self.songRampAlarm)
-               else:
-                       self.introSizer.Show(self.introAlarmLabel)
-                       self.introSizer.Show(self.songRampAlarm)
-               self.Fit()
-
        # Include profile flags such as instant profile string for display 
purposes.
        def displayProfiles(self, profiles):
                for index in xrange(len(profiles)):
@@ -435,14 +384,12 @@ class SPLConfigDialog(gui.SettingsDialog):
                        self.deleteButton.Enable()
                        self.triggerButton.Enable()
                curProfile = splconfig.getProfileByName(selectedProfile)
-               
self.outroCheckBox.SetValue(curProfile["IntroOutroAlarms"]["SayEndOfTrack"])
-               
self.endOfTrackAlarm.SetValue(long(curProfile["IntroOutroAlarms"]["EndOfTrackTime"]))
-               self.onOutroCheck(None)
-               
self.introCheckBox.SetValue(curProfile["IntroOutroAlarms"]["SaySongRamp"])
-               
self.songRampAlarm.SetValue(long(curProfile["IntroOutroAlarms"]["SongRampTime"]))
-               self.onIntroCheck(None)
-               
self.micAlarm.SetValue(long(curProfile["MicrophoneAlarm"]["MicAlarm"]))
-               
self.micAlarmInterval.SetValue(long(curProfile["MicrophoneAlarm"]["MicAlarmInterval"]))
+               self.endOfTrackTime = 
curProfile["IntroOutroAlarms"]["EndOfTrackTime"]
+               self.sayEndOfTrack = 
curProfile["IntroOutroAlarms"]["SayEndOfTrack"]
+               self.songRampTime = 
curProfile["IntroOutroAlarms"]["SongRampTime"]
+               self.saySongRamp = curProfile["IntroOutroAlarms"]["SaySongRamp"]
+               self.micAlarm = curProfile["MicrophoneAlarm"]["MicAlarm"]
+               self.micAlarmInterval = 
curProfile["MicrophoneAlarm"]["MicAlarmInterval"]
                # 6.1: Take care of profile-specific column and metadata 
settings.
                self.metadataStreams = 
curProfile["MetadataStreaming"]["MetadataEnabled"]
                
self.columnOrderCheckbox.SetValue(curProfile["ColumnAnnouncement"]["UseScreenColumnOrder"])
@@ -561,7 +508,12 @@ class SPLConfigDialog(gui.SettingsDialog):
                action(flag)
                self.profiles.SetString(index, profile if not len(flags) else 
"{0} <{1}>".format(profile, ", ".join(flags)))
 
-       # Manage metadata streaming.
+       # Alarms Center.
+       def onAlarmsCenter(self, evt):
+               self.Disable()
+               AlarmsCenter(self).Show()
+
+               # Manage metadata streaming.
        def onManageMetadata(self, evt):
                self.Disable()
                MetadataStreamingDialog(self).Show()
@@ -602,7 +554,7 @@ class SPLConfigDialog(gui.SettingsDialog):
 # Open the above dialog upon request.
 def onConfigDialog(evt):
        # 5.2: Guard against alarm dialogs.
-       if splconfig._alarmDialogOpened or _metadataDialogOpened:
+       if _alarmDialogOpened or _metadataDialogOpened:
                # Translators: Presented when an alarm dialog is opened.
                wx.CallAfter(gui.messageBox, _("Another add-on settings dialog 
is open. Please close the previously opened dialog first."), _("Error"), 
wx.OK|wx.ICON_ERROR)
        else: gui.mainFrame._popupSettingsDialog(SPLConfigDialog)
@@ -815,6 +767,115 @@ class TriggersDialog(wx.Dialog):
                        prompt.Enable() if self.timeSwitchCheckbox.IsChecked() 
else prompt.Disable()
                self.Fit()
 
+# A common alarm dialog (Alarms Center)
+# Based on NVDA core's find dialog code (implemented by the author of this 
add-on).
+# Extended in 2016 to handle microphone alarms.
+# Only one instance can be active at a given moment (code borrowed from GUI's 
exit dialog routine).
+_alarmDialogOpened = False
+
+# A common alarm error dialog.
+def _alarmError():
+       # Translators: Text of the dialog when another alarm dialog is open.
+       gui.messageBox(_("Another alarm dialog is 
open."),_("Error"),style=wx.OK | wx.ICON_ERROR)
+
+class AlarmsCenter(wx.Dialog):
+       """A dialog providing common alarm settings.
+       This dialog contains a number entry field for alarm duration and a 
check box to enable or disable the alarm.
+       For one particular case, it consists of two number entry fields.
+       """
+
+       # The following comes from exit dialog class from GUI package (credit: 
NV Access and Zahari from Bulgaria).
+       _instance = None
+
+       def __new__(cls, parent, *args, **kwargs):
+               # Make this a singleton and prompt an error dialog if it isn't.
+               if _alarmDialogOpened:
+                       raise RuntimeError("An instance of alarm dialog is 
opened")
+               inst = cls._instance() if cls._instance else None
+               if not inst:
+                       return super(cls, cls).__new__(cls, parent, *args, 
**kwargs)
+               return inst
+
+       def __init__(self, parent, level=0):
+               inst = AlarmsCenter._instance() if AlarmsCenter._instance else 
None
+               if inst:
+                       return
+               # Use a weakref so the instance can die.
+               import weakref
+               AlarmsCenter._instance = weakref.ref(self)
+
+               # Now the actual alarm dialog code.
+               # 8.0: Apart from level 0 (all settings shown), levels change 
title.
+               titles = (_("Alarms Center"), _("End of track alarm"), _("Song 
intro alarm"), _("Microphone alarm"))
+               super(AlarmsCenter, self).__init__(parent, wx.ID_ANY, 
titles[level])
+               self.level = level
+               mainSizer = wx.BoxSizer(wx.VERTICAL)
+               alarmsCenterHelper = gui.guiHelper.BoxSizerHelper(self, 
orientation=wx.VERTICAL)
+
+               if level in (0, 1):
+                       timeVal = parent.endOfTrackTime if level == 0 else 
splconfig.SPLConfig["IntroOutroAlarms"]["EndOfTrackTime"]
+                       self.outroAlarmEntry = 
alarmsCenterHelper.addLabeledControl(_("&End of track alarm in seconds"), 
gui.nvdaControls.SelectOnFocusSpinCtrl, min=1, max=59, initial=timeVal)
+                       
self.outroToggleCheckBox=alarmsCenterHelper.addItem(wx.CheckBox(self, 
label=_("&Notify when end of track is approaching")))
+                       self.outroToggleCheckBox.SetValue(parent.sayEndOfTrack 
if level == 0 else splconfig.SPLConfig["IntroOutroAlarms"]["SayEndOfTrack"])
+
+               if level in (0, 2):
+                       rampVal = parent.songRampTime if level == 0 else 
splconfig.SPLConfig["IntroOutroAlarms"]["SongRampTime"]
+                       self.introAlarmEntry = 
alarmsCenterHelper.addLabeledControl(_("&Track intro alarm in seconds"), 
gui.nvdaControls.SelectOnFocusSpinCtrl, min=1, max=9, initial=rampVal)
+                       
self.introToggleCheckBox=alarmsCenterHelper.addItem(wx.CheckBox(self, 
label=_("&Notify when end of introduction is approaching")))
+                       self.introToggleCheckBox.SetValue(parent.saySongRamp if 
level == 0 else splconfig.SPLConfig["IntroOutroAlarms"]["SaySongRamp"])
+
+               if level in (0, 3):
+                       micAlarm = 
splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarm"] if level == 3 else 
parent.micAlarm
+                       micAlarmInterval = 
splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarmInterval"] if level == 3 else 
parent.micAlarmInterval
+                       # Translators: A dialog message to set microphone 
active alarm.
+                       self.micAlarmEntry = 
alarmsCenterHelper.addLabeledControl(_("&Microphone alarm in seconds (0 
disables the alarm)"), gui.nvdaControls.SelectOnFocusSpinCtrl, min=0, max=7200, 
initial=micAlarm)
+                       self.micIntervalEntry = 
alarmsCenterHelper.addLabeledControl(_("Microphone alarm &interval in 
seconds"), gui.nvdaControls.SelectOnFocusSpinCtrl, min=0, max=60, 
initial=micAlarmInterval)
+
+               
alarmsCenterHelper.addDialogDismissButtons(self.CreateButtonSizer(wx.OK | 
wx.CANCEL))
+               self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK)
+               self.Bind(wx.EVT_BUTTON, self.onCancel, id=wx.ID_CANCEL)
+               mainSizer.Add(alarmsCenterHelper.sizer, 
border=gui.guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL)
+               mainSizer.Fit(self)
+               self.SetSizer(mainSizer)
+               self.Center(wx.BOTH | wx.CENTER_ON_SCREEN)
+               if level in (0, 1): self.outroAlarmEntry.SetFocus()
+               elif level == 2: self.introAlarmEntry.SetFocus()
+               elif level == 3: self.micAlarmEntry.SetFocus()
+
+       def onOk(self, evt):
+               global _alarmDialogOpened
+               # Optimization: don't bother if Studio is dead and if the same 
value has been entered (only when standalone versions are opened).
+               if self.level > 0 and user32.FindWindowA("SPLStudio", None):
+                       # Gather settings to be applied in section/key format.
+                       if self.level == 1:
+                               SPLConfig["IntroOutroAlarms"]["EndOfTrackTime"] 
= self.outroAlarmEntry.GetValue()
+                               SPLConfig["IntroOutroAlarms"]["SayEndOfTrack"] 
= self.outroToggleCheckBox.GetValue()
+                       elif self.level == 2:
+                               SPLConfig["IntroOutroAlarms"]["SongRampTime"] = 
self.introAlarmEntry.GetValue()
+                               SPLConfig["IntroOutroAlarms"]["SaySongRamp"] = 
self.introToggleCheckBox.GetValue()
+                       elif self.level == 3:
+                               SPLConfig["MicrophoneAlarm"]["MicAlarm"] = 
self.micAlarmEntry.GetValue()
+                               
SPLConfig["MicrophoneAlarm"]["MicAlarmInterval"] = 
self.micIntervalEntry.GetValue()
+               elif self.level == 0:
+                       parent = self.Parent
+                       parent.endOfTrackTime = self.outroAlarmEntry.GetValue()
+                       parent.sayEndOfTrack = 
self.outroToggleCheckBox.GetValue()
+                       parent.songRampTime = self.introAlarmEntry.GetValue()
+                       parent.saySongRamp = self.introToggleCheckBox.GetValue()
+                       parent.micAlarm = self.micAlarmEntry.GetValue()
+                       parent.micAlarmInterval = 
self.micIntervalEntry.GetValue()
+                       self.Parent.profiles.SetFocus()
+                       self.Parent.Enable()
+               self.Destroy()
+               _alarmDialogOpened = False
+
+       def onCancel(self, evt):
+               if self.level == 0:
+                       self.Parent.Enable()
+               self.Destroy()
+               global _alarmDialogOpened
+               _alarmDialogOpened = False
+
 # Metadata reminder controller.
 # Select notification/streaming URL's for metadata streaming.
 _metadataDialogOpened = False

diff --git a/addon/globalPlugins/SPLStudioUtils/__init__.py 
b/addon/globalPlugins/SPLStudioUtils/__init__.py
index faa4f39..1ac7977 100755
--- a/addon/globalPlugins/SPLStudioUtils/__init__.py
+++ b/addon/globalPlugins/SPLStudioUtils/__init__.py
@@ -40,6 +40,7 @@ SPLWin = 0 # A handle to studio window.
 SPLMSG = winUser.WM_USER
 
 # Various SPL IPC tags.
+SPLVersion = 2
 SPLPlay = 12
 SPLStop = 13
 SPLPause = 15
@@ -48,6 +49,7 @@ SPLMic = 17
 SPLLineIn = 18
 SPLLibraryScanCount = 32
 SPLListenerCount = 35
+SPLStatusInfo = 39 #Studio 5.20 and later.
 SPL_TrackPlaybackStatus = 104
 SPLCurTrackPlaybackTime = 105
 
@@ -73,6 +75,7 @@ S: Stop with fade.
 T: Instant stop.
 E: Announce if any encoders are being monitored.
 I: Announce listener count.
+Q: Announce Studio status information.
 R: Remaining time for the playing track.
 Shift+R: Library scan progress.""")
 
@@ -264,6 +267,31 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin):
                encoders.announceNumMonitoringEncoders()
                self.finish()
 
+       def script_statusInfo(self, gesture):
+               # For consistency reasons (because of the Studio status bar), 
messages in this method will remain in English.
+               statusInfo = []
+               # 17.1: For Studio 5.10 and up, announce playback and 
automation status.
+               playingNow = winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPL_TrackPlaybackStatus)
+               statusInfo.append("Play status: playing" if playingNow else 
"Play status: stopped")
+               # For automation, Studio 5.11 and earlier does not have an easy 
way to detect this flag, thus resort to using playback status.
+               if winUser.sendMessage(SPLWin, SPLMSG, 0, SPLVersion) < 520:
+                       statusInfo.append("Automation on" if playingNow == 2 
else "Automation off")
+               else:
+                       statusInfo.append("Automation on" if 
winUser.sendMessage(SPLWin, SPLMSG, 1, SPLStatusInfo) else "Automation off")
+                       # 5.20 and later.
+                       statusInfo.append("Microphone on" if 
winUser.sendMessage(SPLWin, SPLMSG, 2, SPLStatusInfo) else "Microphone off")
+                       statusInfo.append("Line-inon" if 
winUser.sendMessage(SPLWin, SPLMSG, 3, SPLStatusInfo) else "Line-in off")
+                       statusInfo.append("Record to file on" if 
winUser.sendMessage(SPLWin, SPLMSG, 4, SPLStatusInfo) else "Record to file off")
+                       cartEdit = winUser.sendMessage(SPLWin, SPLMSG, 5, 
SPLStatusInfo)
+                       cartInsert = winUser.sendMessage(SPLWin, SPLMSG, 6, 
SPLStatusInfo)
+                       if not cartEdit and not cartInsert: 
statusInfo.append("Cart edit off")
+                       elif cartEdit and not cartInsert: 
statusInfo.append("Cart edit on")
+                       elif not cartEdit and cartInsert: 
statusInfo.append("Cart insert on")
+               ui.message("; ".join(statusInfo))
+               self.finish()
+       # Translators: Input help message for a SPL Controller command.
+       script_statusInfo.__doc__ = _("Announces Studio status such as track 
playback status from other programs")
+
        def script_conHelp(self, gesture):
                # Translators: The title for SPL Controller help dialog.
                wx.CallAfter(gui.messageBox, SPLConHelp, _("SPL Controller 
help"))
@@ -286,6 +314,7 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin):
                "kb:r":"remainingTime",
                "kb:e":"announceNumMonitoringEncoders",
                "kb:i":"listenerCount",
+               "kb:q":"statusInfo",
                "kb:f1":"conHelp"
        }
 

diff --git a/readme.md b/readme.md
index d9285a0..6038fd4 100755
--- a/readme.md
+++ b/readme.md
@@ -9,7 +9,7 @@ This add-on package provides improved usage of StationPlaylist 
Studio, as well a
 
 For more information about the add-on, read the [add-on guide][4]. For 
developers seeking to know how to build the add-on, see buildInstructions.txt 
located at the root of the add-on source code repository.
 
-IMPORTANT: This add-on requires NVDA 2016.4 or later and StationPlaylist 
Studio 5.10 or later. If you have installed NVDA 2016.1 or later on Windows 8 
and later, disable audio ducking mode. Also, add-on 8.0/16.10 requires Studio 
5.10 and later, and for broadcasters using Studio 5.0x, a long-term support 
version (15.x) is available.
+IMPORTANT: This add-on requires NVDA 2016.4 or later and StationPlaylist 
Studio 5.10 or later. If using Windows 8 or later, for best experience, disable 
audio ducking mode. Also, add-on 8.0/16.10 requires Studio 5.10 and later, and 
for broadcasters using Studio 5.0x, a long-term support version (15.x) is 
available.
 
 ## Shortcut keys
 
@@ -36,6 +36,7 @@ The following commands are not assigned by default; if you 
wish to assign them,
 
 * Switching to SPL Studio window from any program.
 * SPL Controller layer.
+* Announcing Studio status such as track playback from other programs.
 * SPL Assistant layer from SPL Studio.
 * Announce time including seconds from SPL Studio.
 * Announcing temperature.
@@ -127,6 +128,7 @@ The available SPL Controller commands are:
 * Press Shift+R to get a report on library scan progress.
 * Press E to get count and labels for encoders being monitored.
 * Press I to obtain listener count.
+* Press Q to obtain various status information about Studio including whether 
a track is playing, microphone is on and others.
 * Press F1 to show a help dialog which lists available commands.
 
 ## Track alarms
@@ -170,7 +172,10 @@ If you are using Studio on a touchscreen computer running 
Windows 8 or later and
 * Improvements to presentation of various add-on dialogs thanks to NVDA 2016.4 
features.
 * Added ability to press Control+Alt+up or down arrow keys to move between 
tracks (specifically, track columns) vertically just as one is moving to next 
or previous row in a table.
 * Added a combo box in add-on settings dialog to set which column should be 
announced when moving through columns vertically.
+* Moved end of track , intro and microphone alarm controls from add-on 
settings to the new Alarms Center.
+* In Alarms Center, end of track and track intro edit fields are always shown 
regardless of state of alarm notification checkboxes.
 * Removed Track Dial (NVDA's version of enhanced arrow keys), replaced by 
Columns explorer and Column Navigator/table navigation commands). This affects 
Studio and Track Tool.
+* Added a new command in SPL Controller layer to announce Studio status such 
as track playback and microphone status (Q).
 
 ## Version 16.12.1
 


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/5c200bf729ab/
Changeset:   5c200bf729ab
Branch:      None
User:        josephsl
Date:        2016-12-17 17:50:18+00:00
Summary:     Playlist snapshots (17.1-dev): formally introduce Playlist 
Snapshots.

SPL Assistant, F8 is used to obtain playlist snapshots (track count, total 
duration, shortest, longest and average track lengths, top categhories). SPL 
Assistant, F11 would have been ideal, but it conflicts with Remote add-on, 
hence F8 will be used for now.
This is reserved for add-on 17.1.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 5d7ceca..9f9ad00 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1379,8 +1379,23 @@ class AppModule(appModuleHandler.AppModule):
        # Return total duration of a range of tracks.
        # This is used in track time analysis when multiple tracks are selected.
        # This is also called from playlist duration scripts.
-       # To be replaced by general track duration script, with the difference 
being start and end location.
-       def totalTime(self, start, end):
+       def playlistDuration(self, start=None, end=None):
+               if start is None: start = api.getFocusObject()
+               duration = start.indexOf("Duration")
+               totalDuration = 0
+               obj = start
+               while obj not in (None, end):
+                       # Technically segue.
+                       segue = obj._getColumnContent(duration)
+                       if segue not in (None, "00:00"):
+                               hms = segue.split(":")
+                               totalDuration += (int(hms[-2])*60) + 
int(hms[-1])
+                               if len(hms) == 3: totalDuration += 
int(hms[0])*3600
+                       obj = obj.next
+               return totalDuration
+
+       # Segue version of this will be used in some places (the below is the 
raw duration).)
+       def playlistDurationRaw(self, start, end):
                # Take care of errors such as the following.
                if start < 0 or end > statusAPI(0, 124, ret=True)-1:
                        raise ValueError("Track range start or end position out 
of range")
@@ -1399,23 +1414,73 @@ class AppModule(appModuleHandler.AppModule):
        # Data to be gathered comes from a set of flags.
        def playlistSnapshots(self, obj, end, snapshotFlags=None):
                # Track count and total duration are always included.
-               snapshot = {"TrackCount":statusAPI(0, 124, ret=True)}
+               snapshot = {}
+               if snapshotFlags is None:
+                       snapshotFlags = ["PlaylistDurationMinMax", 
"PlaylistCategoryCount", "PlaylistDurationAverage"]
                duration = obj.indexOf("Duration")
                title = obj.indexOf("Title")
+               min, max = None, None
+               minTitle, maxTitle = None, None
+               category = obj.indexOf("Category")
                totalDuration = 0
-               titleDuration = []
+               categories = []
+               # A specific version of the playlist duration loop is needed in 
order to gather statistics.
                while obj not in (None, end):
-                       # Technically segue.
                        segue = obj._getColumnContent(duration)
                        trackTitle = obj._getColumnContent(title)
-                       titleDuration.append((trackTitle, segue))
+                       categories.append(obj._getColumnContent(category))
+                       # Shortest and longest tracks.
+                       if min is None: min = segue
+                       if segue and segue < min:
+                               min = segue
+                               minTitle = trackTitle
+                       if segue and segue > max:
+                               max = segue
+                               maxTitle = trackTitle
                        if segue not in (None, "00:00"):
                                hms = segue.split(":")
                                totalDuration += (int(hms[-2])*60) + 
int(hms[-1])
                                if len(hms) == 3: totalDuration += 
int(hms[0])*3600
                        obj = obj.next
-               snapshot["DurationTotal"] = totalDuration
-               return snapshot["DurationTotal"]
+               if end is None: snapshot["PlaylistTrackCount"] = statusAPI(0, 
124, ret=True)
+               snapshot["PlaylistDurationTotal"] = 
self._ms2time(totalDuration, ms=False)
+               if "PlaylistDurationMinMax" in snapshotFlags:
+                       snapshot["PlaylistDurationMin"] = "%s (%s)"%(minTitle, 
min)
+                       snapshot["PlaylistDurationMax"] = "%s (%s)"%(maxTitle, 
max)
+               if "PlaylistDurationAverage" in snapshotFlags:
+                       snapshot["PlaylistDurationAverage"] = 
self._ms2time(totalDuration/snapshot["PlaylistTrackCount"], ms=False)
+               if "PlaylistCategoryCount" in snapshotFlags:
+                       import collections
+                       snapshot["PlaylistCategoryCount"] = 
collections.Counter(categories)
+               return snapshot
+
+# Output formatter for playlist snapshots.
+# Pressed once will speak and/or braille it, pressing twice or more will 
output this info to an HTML file.
+       def playlistSnapshotOutput(self, snapshot, scriptCount):
+               scriptCount = 1
+               statusInfo = ["Tracks: %s"%snapshot["PlaylistTrackCount"]]
+               statusInfo.append("Duration: 
%s"%snapshot["PlaylistDurationTotal"])
+               if "PlaylistDurationMin" in snapshot:
+                       statusInfo.append("Shortest: 
%s"%snapshot["PlaylistDurationMin"])
+                       statusInfo.append("Longest: 
%s"%snapshot["PlaylistDurationMax"])
+               if "PlaylistDurationAverage" in snapshot:
+                       statusInfo.append("Average: 
%s"%snapshot["PlaylistDurationAverage"])
+               if "PlaylistCategoryCount" in snapshot:
+                       categories = 
snapshot["PlaylistCategoryCount"].most_common()
+                       if scriptCount == 0:
+                               statusInfo.append("Top category: %s 
(%s)"%(categories[0]))
+                       else:
+                               categoryList = []
+                               for item in categories:
+                                       category, count = item
+                                       category = category.replace("<", "")
+                                       category = category.replace(">", "")
+                                       categoryList.append("<li>%s 
(%s)</li>"%(category, count))
+                               statusInfo.append("".join(["Categories:<ol>", 
"\n".join(categoryList), "</ol>"]))
+               if scriptCount == 0:
+                       ui.message(", ".join(statusInfo))
+               else:
+                       
ui.browseableMessage("<p>".join(statusInfo),title="Playlist snapshot", 
isHtml=True)
 
        # Some handlers for native commands.
 
@@ -1610,7 +1675,7 @@ class AppModule(appModuleHandler.AppModule):
                if obj.role == controlTypes.ROLE_LIST:
                        ui.message("00:00")
                        return
-               self.announceTime(self.playlistSnapshots(obj, None), ms=False)
+               self.announceTime(self.playlistDuration(start=obj), ms=False)
 
        def script_sayPlaylistModified(self, gesture):
                try:
@@ -1751,7 +1816,7 @@ class AppModule(appModuleHandler.AppModule):
                        analysisBegin = min(self._analysisMarker, trackPos)
                        analysisEnd = max(self._analysisMarker, trackPos)
                        analysisRange = analysisEnd-analysisBegin+1
-                       totalLength = self.totalTime(analysisBegin, analysisEnd)
+                       totalLength = self.playlistDurationRaw(analysisBegin, 
analysisEnd)
                        if analysisRange == 1:
                                self.announceTime(totalLength)
                        else:
@@ -1766,9 +1831,10 @@ class AppModule(appModuleHandler.AppModule):
                        ui.message("Please return to playlist viewer before 
invoking this command.")
                        return
                if obj.role == controlTypes.ROLE_LIST:
-                       ui.message("00:00")
+                       ui.message(_("You need to add tracks before invoking 
this command"))
                        return
-               self.announceTime(self.playlistSnapshots(obj, None), ms=False)
+               # Speak and braille on the first press, display a decorated 
HTML message for subsequent presses.
+               
self.playlistSnapshotOutput(self.playlistSnapshots(obj.parent.firstChild, 
None), scriptHandler.getLastScriptRepeatCount())
 
        def script_switchProfiles(self, gesture):
                splconfig.triggerProfileSwitch() if 
splconfig._triggerProfileActive else splconfig.instantProfileSwitch()
@@ -1886,6 +1952,7 @@ class AppModule(appModuleHandler.AppModule):
                "kb:shift+s":"sayScheduledToPlay",
                "kb:shift+p":"sayTrackPitch",
                "kb:shift+r":"libraryScanMonitor",
+               "kb:f8":"takePlaylistSnapshots",
                "kb:f9":"markTrackForAnalysis",
                "kb:f10":"trackTimeAnalysis",
                "kb:f12":"switchProfiles",


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/f6ac03cff7bf/
Changeset:   f6ac03cff7bf
Branch:      None
User:        josephsl
Date:        2016-12-23 23:23:31+00:00
Summary:     Merge branch 'master' into plSnaps

Affected #:  2 files

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 5d6d0df..5e0511e 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -102,6 +102,21 @@ class SPLConfigDialog(gui.SettingsDialog):
                alarmsCenterButton = SPLConfigHelper.addItem(wx.Button(self, 
label=_("&Alarms Center...")))
                alarmsCenterButton.Bind(wx.EVT_BUTTON, self.onAlarmsCenter)
 
+               # Translators: One of the alarm notification options.
+               self.alarmAnnounceValues=[("beep",_("beep")),
+               # Translators: One of the alarm notification options.
+               ("message",_("message")),
+               # Translators: One of the alarm notification options.
+               ("both",_("both beep and message"))]
+                               # Translators: The label for a setting in SPL 
add-on dialog to control alarm announcement type.
+               self.alarmAnnounceList = 
SPLConfigHelper.addLabeledControl(_("&Alarm notification:"), wx.Choice, 
choices=[x[1] for x in self.alarmAnnounceValues])
+               
alarmAnnounceCurValue=splconfig.SPLConfig["General"]["AlarmAnnounce"]
+               selection = (x for x,y in enumerate(self.alarmAnnounceValues) 
if y[0]==alarmAnnounceCurValue).next()
+               try:
+                       self.alarmAnnounceList.SetSelection(selection)
+               except:
+                       pass
+
                self.brailleTimerValues=[("off",_("Off")),
                # Translators: One of the braille timer settings.
                ("outro",_("Track ending")),
@@ -118,21 +133,6 @@ class SPLConfigDialog(gui.SettingsDialog):
                except:
                        pass
 
-               # Translators: One of the alarm notification options.
-               self.alarmAnnounceValues=[("beep",_("beep")),
-               # Translators: One of the alarm notification options.
-               ("message",_("message")),
-               # Translators: One of the alarm notification options.
-               ("both",_("both beep and message"))]
-                               # Translators: The label for a setting in SPL 
add-on dialog to control alarm announcement type.
-               self.alarmAnnounceList = 
SPLConfigHelper.addLabeledControl(_("&Alarm notification:"), wx.Choice, 
choices=[x[1] for x in self.alarmAnnounceValues])
-               
alarmAnnounceCurValue=splconfig.SPLConfig["General"]["AlarmAnnounce"]
-               selection = (x for x,y in enumerate(self.alarmAnnounceValues) 
if y[0]==alarmAnnounceCurValue).next()
-               try:
-                       self.alarmAnnounceList.SetSelection(selection)
-               except:
-                       pass
-
                self.libScanValues=[("off",_("Off")),
                # Translators: One of the library scan announcement settings.
                ("ending",_("Start and end only")),
@@ -273,8 +273,8 @@ class SPLConfigDialog(gui.SettingsDialog):
                splconfig.SPLConfig["IntroOutroAlarms"]["SaySongRamp"] = 
self.saySongRamp
                splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarm"] = 
self.micAlarm
                splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarmInterval"] = 
self.micAlarmInterval
-               splconfig.SPLConfig["General"]["BrailleTimer"] = 
self.brailleTimerValues[self.brailleTimerList.GetSelection()][0]
                splconfig.SPLConfig["General"]["AlarmAnnounce"] = 
self.alarmAnnounceValues[self.alarmAnnounceList.GetSelection()][0]
+               splconfig.SPLConfig["General"]["BrailleTimer"] = 
self.brailleTimerValues[self.brailleTimerList.GetSelection()][0]
                splconfig.SPLConfig["General"]["LibraryScanAnnounce"] = 
self.libScanValues[self.libScanList.GetSelection()][0]
                splconfig.SPLConfig["General"]["TimeHourAnnounce"] = 
self.hourAnnounceCheckbox.Value
                splconfig.SPLConfig["General"]["CategorySounds"] = 
self.categorySoundsCheckbox.Value

diff --git a/addon/globalPlugins/SPLStudioUtils/__init__.py 
b/addon/globalPlugins/SPLStudioUtils/__init__.py
index 1ac7977..9701601 100755
--- a/addon/globalPlugins/SPLStudioUtils/__init__.py
+++ b/addon/globalPlugins/SPLStudioUtils/__init__.py
@@ -284,9 +284,9 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin):
                        statusInfo.append("Record to file on" if 
winUser.sendMessage(SPLWin, SPLMSG, 4, SPLStatusInfo) else "Record to file off")
                        cartEdit = winUser.sendMessage(SPLWin, SPLMSG, 5, 
SPLStatusInfo)
                        cartInsert = winUser.sendMessage(SPLWin, SPLMSG, 6, 
SPLStatusInfo)
-                       if not cartEdit and not cartInsert: 
statusInfo.append("Cart edit off")
-                       elif cartEdit and not cartInsert: 
statusInfo.append("Cart edit on")
+                       if cartEdit: statusInfo.append("Cart edit on")
                        elif not cartEdit and cartInsert: 
statusInfo.append("Cart insert on")
+                       else: statusInfo.append("Cart edit off")
                ui.message("; ".join(statusInfo))
                self.finish()
        # Translators: Input help message for a SPL Controller command.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/3575064f0116/
Changeset:   3575064f0116
Branch:      None
User:        josephsl
Date:        2016-12-30 01:26:38+00:00
Summary:     Merge branch 'master' into plSnaps

Affected #:  20 files

diff --git a/addon/appModules/splcreator.py b/addon/appModules/splcreator.py
new file mode 100755
index 0000000..31b99da
--- /dev/null
+++ b/addon/appModules/splcreator.py
@@ -0,0 +1,15 @@
+# StationPlaylist Creator
+# An app module and global plugin package for NVDA
+# Copyright 2016-2017 Joseph Lee and others, released under GPL.
+
+# Basic support for StationPlaylist Creator.
+
+import appModuleHandler
+
+
+class AppModule(appModuleHandler.AppModule):
+
+       def chooseNVDAObjectOverlayClasses(self, obj, clsList):
+               if obj.windowClassName in ("TDemoRegForm", "TAboutForm"):
+                       from NVDAObjects.behaviors import Dialog
+                       clsList.insert(0, Dialog)

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 9f9ad00..f076683 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1,6 +1,6 @@
 # StationPlaylist Studio
 # An app module and global plugin package for NVDA
-# Copyright 2011, 2013-2016, Geoff Shang, Joseph Lee and others, released 
under GPL.
+# Copyright 2011, 2013-2017, Geoff Shang, Joseph Lee and others, released 
under GPL.
 # The primary function of this appModule is to provide meaningful feedback to 
users of SplStudio
 # by allowing speaking of items which cannot be easily found.
 # Version 0.01 - 7 April 2011:
@@ -17,10 +17,7 @@ import threading
 import controlTypes
 import appModuleHandler
 import api
-import review
-import eventHandler
 import scriptHandler
-import queueHandler
 import ui
 import nvwave
 import speech
@@ -157,7 +154,7 @@ class SPLTrackItem(IAccessible):
                if splconfig.SPLConfig["General"]["TrackCommentAnnounce"] != 
"off":
                        self.announceTrackComment(0)
                # 6.3: Catch an unusual case where screen order is off yet 
column order is same as screen order and NvDA is told to announce all columns.
-               # 17.1: Even if vertical column commands are performed, build 
description pieces for consistency.
+               # 17.04: Even if vertical column commands are performed, build 
description pieces for consistency.
                if splconfig._shouldBuildDescriptionPieces():
                        descriptionPieces = []
                        columnsToInclude = 
splconfig.SPLConfig["ColumnAnnouncement"]["IncludedColumns"]
@@ -202,7 +199,7 @@ class SPLTrackItem(IAccessible):
 
        # Announce column content if any.
        # 7.0: Add an optional header in order to announce correct header 
information in columns explorer.
-       # 17.1: Allow checked status in 5.1x and later to be announced if this 
is such a case (vertical column navigation).)
+       # 17.04: Allow checked status in 5.1x and later to be announced if this 
is such a case (vertical column navigation).)
        def announceColumnContent(self, colNumber, header=None, 
reportStatus=False):
                columnHeader = header if header is not None else 
self.appModule._columnHeaderNames[colNumber]
                columnContent = 
self._getColumnContent(self.indexOf(columnHeader))
@@ -574,6 +571,7 @@ class AppModule(appModuleHandler.AppModule):
                # Announce status changes while using other programs.
                # This requires NVDA core support and will be available in 6.0 
and later (cannot be ported to earlier versions).
                # For now, handle all background events, but in the end, make 
this configurable.
+               import eventHandler
                if hasattr(eventHandler, "requestEvents"):
                        eventHandler.requestEvents(eventName="nameChange", 
processId=self.processID, windowClassName="TStatusBar")
                        eventHandler.requestEvents(eventName="nameChange", 
processId=self.processID, windowClassName="TStaticText")
@@ -591,6 +589,7 @@ class AppModule(appModuleHandler.AppModule):
                # LTS: Only do this if channel hasn't changed.
                if splconfig.SPLConfig["Update"]["AutoUpdateCheck"] or 
splupdate._updateNow:
                        # 7.0: Have a timer call the update function indirectly.
+                       import queueHandler
                        queueHandler.queueFunction(queueHandler.eventQueue, 
splconfig.updateInit)
                # Display startup dialogs if any.
                wx.CallAfter(splconfig.showStartupDialogs)
@@ -628,6 +627,7 @@ class AppModule(appModuleHandler.AppModule):
                        obj.role = controlTypes.ROLE_GROUPING
                # In certain edit fields and combo boxes, the field name is 
written to the screen, and there's no way to fetch the object for this text. 
Thus use review position text.
                elif obj.windowClassName in ("TEdit", "TComboBox") and not 
obj.name:
+                       import review
                        fieldName, fieldObj  = review.getScreenPosition(obj)
                        fieldName.expand(textInfos.UNIT_LINE)
                        if obj.windowClassName == "TComboBox":
@@ -694,8 +694,7 @@ class AppModule(appModuleHandler.AppModule):
                                                if self.scanCount%100 == 0:
                                                        
self._libraryScanAnnouncer(obj.name[1:obj.name.find("]")], 
splconfig.SPLConfig["General"]["LibraryScanAnnounce"])
                                        if not self.libraryScanning:
-                                               if self.productVersion not in 
noLibScanMonitor:
-                                                       if not 
self.backgroundStatusMonitor: self.libraryScanning = True
+                                               if self.productVersion not in 
noLibScanMonitor: self.libraryScanning = True
                                elif "match" in obj.name:
                                        if 
splconfig.SPLConfig["General"]["LibraryScanAnnounce"] != "off" and 
self.libraryScanning:
                                                if 
splconfig.SPLConfig["General"]["BeepAnnounce"]: tones.beep(370, 100)
@@ -773,14 +772,19 @@ class AppModule(appModuleHandler.AppModule):
 
        # Perform extra action in specific situations (mic alarm, for example).
        def doExtraAction(self, status):
+               # Be sure to only deal with cart mode changes if Cart Explorer 
is on.
+               # Optimization: Return early if the below condition is true.
+               if self.cartExplorer and status.startswith("Cart"):
+                       # 17.01: The best way to detect Cart Edit off is 
consulting file modification time.
+                       # Automatically reload cart information if this is the 
case.
+                       studioTitle = api.getForegroundObject().name
+                       if splmisc.shouldCartExplorerRefresh(studioTitle):
+                               self.carts = 
splmisc.cartExplorerInit(studioTitle)
+                       # Translators: Presented when cart edit mode is toggled 
on while cart explorer is on.
+                       ui.message(_("Cart explorer is active"))
+                       return
+               # Microphone alarm and alarm interval if defined.
                micAlarm = splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarm"]
-               if self.cartExplorer:
-                       if status == "Cart Edit On":
-                               # Translators: Presented when cart edit mode is 
toggled on while cart explorer is on.
-                               ui.message(_("Cart explorer is active"))
-                       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:
                        # Play an alarm sound (courtesy of Jerry Mader from 
Mader Radio).
                        global micAlarmT, micAlarmT2
@@ -853,9 +857,9 @@ class AppModule(appModuleHandler.AppModule):
                # 6.3: Memory leak results if encoder flag sets and other 
encoder support maps aren't cleaned up.
                # This also could have allowed a hacker to modify the flags set 
(highly unlikely) so NvDA could get confused next time Studio loads.
                import sys
-               if "globalPlugins.SPLStudioUtils.encoders" in sys.modules:
-                       import globalPlugins.SPLStudioUtils.encoders
-                       globalPlugins.SPLStudioUtils.encoders.cleanup()
+               if "globalPlugins.splUtils.encoders" in sys.modules:
+                       import globalPlugins.splUtils.encoders
+                       globalPlugins.splUtils.encoders.cleanup()
                splconfig.saveConfig()
                # Delete focused track reference.
                self._focusedTrack = None
@@ -868,6 +872,8 @@ class AppModule(appModuleHandler.AppModule):
                # Manually clear the following dictionaries.
                self.carts.clear()
                self._cachedStatusObjs.clear()
+               # Don't forget to reset timestamps for cart files.
+               splmisc._cartEditTimestamps = [0, 0, 0, 0]
                # Just to make sure:
                global _SPLWin
                if _SPLWin: _SPLWin = None
@@ -1256,40 +1262,40 @@ class AppModule(appModuleHandler.AppModule):
                global libScanT
                if libScanT and libScanT.isAlive() and 
api.getForegroundObject().windowClassName == "TTrackInsertForm":
                        return
-               countA = statusAPI(1, 32, ret=True)
-               if countA == 0:
+               if statusAPI(1, 32, ret=True) < 0:
                        self.libraryScanning = False
                        return
                time.sleep(0.1)
                if api.getForegroundObject().windowClassName == 
"TTrackInsertForm" and self.productVersion in noLibScanMonitor:
                        self.libraryScanning = False
                        return
-               # Sometimes, a second call is needed to obtain the real scan 
count in Studio 5.10 and later.
-               countB = statusAPI(1, 32, ret=True)
-               if countA == countB:
+               # 17.04: Library scan may have finished while this thread was 
sleeping.
+               if statusAPI(1, 32, ret=True) < 0:
                        self.libraryScanning = False
-                       countB = statusAPI(0, 32, ret=True)
                        # Translators: Presented when library scanning is 
finished.
-                       ui.message(_("{itemCount} items in the 
library").format(itemCount = countB))
+                       ui.message(_("{itemCount} items in the 
library").format(itemCount = statusAPI(0, 32, ret=True)))
                else:
-                       libScanT = 
threading.Thread(target=self.libraryScanReporter, args=(_SPLWin, countA, 
countB, 1))
+                       libScanT = 
threading.Thread(target=self.libraryScanReporter)
                        libScanT.daemon = True
                        libScanT.start()
 
-       def libraryScanReporter(self, _SPLWin, countA, countB, parem):
+       def libraryScanReporter(self):
                scanIter = 0
-               while countA != countB:
+               # 17.04: Use the constant directly, as 5.10 and later provides 
a convenient method to detect completion of library scans.
+               scanCount = statusAPI(1, 32, ret=True)
+               while scanCount >= 0:
                        if not self.libraryScanning: return
-                       countA = countB
                        time.sleep(1)
                        # Do not continue if we're back on insert tracks form 
or library scan is finished.
                        if api.getForegroundObject().windowClassName == 
"TTrackInsertForm" or not self.libraryScanning:
                                return
-                       countB, scanIter = statusAPI(parem, 32, ret=True), 
scanIter+1
-                       if countB < 0:
+                       # Scan count may have changed during sleep.
+                       scanCount = statusAPI(1, 32, ret=True)
+                       if scanCount < 0:
                                break
+                       scanIter+=1
                        if scanIter%5 == 0 and 
splconfig.SPLConfig["General"]["LibraryScanAnnounce"] not in ("off", "ending"):
-                               self._libraryScanAnnouncer(countB, 
splconfig.SPLConfig["General"]["LibraryScanAnnounce"])
+                               self._libraryScanAnnouncer(scanCount, 
splconfig.SPLConfig["General"]["LibraryScanAnnounce"])
                self.libraryScanning = False
                if self.backgroundStatusMonitor: return
                if splconfig.SPLConfig["General"]["LibraryScanAnnounce"] != 
"off":
@@ -1297,7 +1303,7 @@ class AppModule(appModuleHandler.AppModule):
                                tones.beep(370, 100)
                        else:
                                # Translators: Presented after library scan is 
done.
-                               ui.message(_("Scan complete with {itemCount} 
items").format(itemCount = countB))
+                               ui.message(_("Scan complete with {itemCount} 
items").format(itemCount = statusAPI(0, 32, ret=True)))
 
        # Take care of library scanning announcement.
        def _libraryScanAnnouncer(self, count, announcementType):
@@ -1769,10 +1775,8 @@ class AppModule(appModuleHandler.AppModule):
 
        def script_libraryScanMonitor(self, gesture):
                if not self.libraryScanning:
-                       scanning = statusAPI(1, 32, ret=True)
-                       if scanning < 0:
-                               items = statusAPI(0, 32, ret=True)
-                               ui.message(_("{itemCount} items in the 
library").format(itemCount = items))
+                       if statusAPI(1, 32, ret=True) < 0:
+                               ui.message(_("{itemCount} items in the 
library").format(itemCount = statusAPI(0, 32, ret=True)))
                                return
                        self.libraryScanning = True
                        # Translators: Presented when attempting to start 
library scan.
@@ -2056,5 +2060,4 @@ class AppModule(appModuleHandler.AppModule):
                "kb:Shift+numpadDelete":"deleteTrack",
                "kb:escape":"escape",
                "kb:control+nvda+-":"sendFeedbackEmail",
-               #"kb:control+nvda+`":"SPLAssistantToggle"
        }

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 3351125..a5b448e 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -1,6 +1,6 @@
 # SPL Studio Configuration Manager
 # An app module and global plugin package for NVDA
-# Copyright 2015-2016 Joseph Lee and others, released under GPL.
+# Copyright 2015-2017 Joseph Lee and others, released under GPL.
 # Provides the configuration management package for SPL Studio app module.
 # For miscellaneous dialogs and tool, see SPLMisc module.
 # For UI surrounding this module, see splconfui module.
@@ -12,10 +12,8 @@ from validate import Validator
 import time
 import datetime
 import cPickle
-import copy
 import globalVars
 import ui
-import api
 import gui
 import wx
 import splupdate
@@ -77,7 +75,6 @@ UpdateInterval = integer(min=1, max=30, default=7)
 [Startup]
 AudioDuckingReminder = boolean(default=true)
 WelcomeDialog = boolean(default=true)
-Studio500 = boolean(default=true)
 """), encoding="UTF-8", list_values=False)
 confspec7.newlines = "\r\n"
 SPLConfig = None
@@ -111,6 +108,12 @@ class ConfigHub(ChainMap):
                self.profileNames.append(None) # Signifying normal profile.
                # Always cache normal profile upon startup.
                self._cacheConfig(self.maps[0])
+               # Remove deprecated keys.
+               # This action must be performed after caching, otherwise the 
newly modified profile will not be saved.
+               deprecatedKeys = {"General":"TrackDial", "Startup":"Studio500"}
+               for section, key in deprecatedKeys.iteritems():
+                       if key in section: del self.maps[0][section][key]
+               # Moving onto broadcast profiles if any.
                try:
                        profiles = filter(lambda fn: os.path.splitext(fn)[-1] 
== ".ini", os.listdir(SPLProfiles))
                        for profile in profiles:
@@ -236,6 +239,7 @@ class ConfigHub(ChainMap):
 
        def _cacheConfig(self, conf):
                global _SPLCache
+               import copy
                if _SPLCache is None: _SPLCache = {}
                key = None if conf.filename == SPLIni else conf.name
                _SPLCache[key] = {}
@@ -475,7 +479,7 @@ def _extraInitSteps(conf, profileName=None):
                else:
                        _configLoadStatus[profileName] = "metadataReset"
                conf["MetadataStreaming"]["MetadataEnabled"] = [False, False, 
False, False, False]
-       # 17.1: If vertical column announcement value is "None", transform this 
to NULL.
+       # 17.04: If vertical column announcement value is "None", transform 
this to NULL.
        if conf["General"]["VerticalColumnAnnounce"] == "None":
                conf["General"]["VerticalColumnAnnounce"] = None
 
@@ -944,9 +948,10 @@ Thank you.""")
                self.Destroy()
 
 # Old version reminder.
-class OldVersionReminder(wx.Dialog):
-       """A dialog shown when using add-on 8.x under Studio 5.0x.
-       """
+# Only used when there is a LTS version.
+"""class OldVersionReminder(wx.Dialog):
+       #A dialog shown when using add-on 8.x under Studio 5.0x.
+       #
 
        def __init__(self, parent):
                # Translators: Title of a dialog displayed when the add-on 
starts reminding broadcasters about old Studio releases.
@@ -961,7 +966,7 @@ class OldVersionReminder(wx.Dialog):
                sizer = wx.BoxSizer(wx.HORIZONTAL)
                # Translators: A checkbox to turn off old version reminder 
message.
                self.oldVersionReminder=wx.CheckBox(self,wx.NewId(),label=_("Do 
not show this message again"))
-               self.oldVersionReminder.SetValue(not 
SPLConfig["Startup"]["Studio500"])
+               self.oldVersionReminder.SetValue(not 
SPLConfig["Startup"]["OldSPLVersionReminder"])
                sizer.Add(self.oldVersionReminder, border=10,flag=wx.TOP)
                mainSizer.Add(sizer, border=10, flag=wx.BOTTOM)
 
@@ -975,15 +980,16 @@ class OldVersionReminder(wx.Dialog):
        def onOk(self, evt):
                global SPLConfig
                if self.oldVersionReminder.Value:
-                       SPLConfig["Startup"]["Studio500"] = not 
self.oldVersionReminder.Value
-               self.Destroy()
+                       SPLConfig["Startup"]["OldSPLVersionReminder"] = not 
self.oldVersionReminder.Value
+               self.Destroy()"""
 
 # And to open the above dialog and any other dialogs.
 def showStartupDialogs(oldVer=False):
-       if oldVer and SPLConfig["Startup"]["Studio500"]:
-               gui.mainFrame.prePopup()
-               OldVersionReminder(gui.mainFrame).Show()
-               gui.mainFrame.postPopup()
+       # Old version reminder if this is such a case.
+       #if oldVer and SPLConfig["Startup"]["OldSPLVersionReminder"]:
+               #gui.mainFrame.prePopup()
+               #OldVersionReminder(gui.mainFrame).Show()
+               #gui.mainFrame.postPopup()
        if SPLConfig["Startup"]["WelcomeDialog"]:
                gui.mainFrame.prePopup()
                WelcomeDialog(gui.mainFrame).Show()
@@ -993,14 +999,11 @@ def showStartupDialogs(oldVer=False):
                #if gui.messageBox("The next major version of the add-on (15.x) 
will be the last version to support Studio versions earlier than 5.10, with 
add-on 15.x being designated as a long-term support version. Would you like to 
switch to long-term support release?", "Long-Term Support version", wx.YES | 
wx.NO | wx.CANCEL | wx.CENTER | wx.ICON_QUESTION) == wx.YES:
                        #splupdate.SPLUpdateChannel = "lts"
                        #os.remove(os.path.join(globalVars.appArgs.configPath, 
"addons", "stationPlaylist", "ltsprep"))
-       try:
-               import audioDucking
-               if SPLConfig["Startup"]["AudioDuckingReminder"] and 
audioDucking.isAudioDuckingSupported():
-                       gui.mainFrame.prePopup()
-                       AudioDuckingReminder(gui.mainFrame).Show()
-                       gui.mainFrame.postPopup()
-       except ImportError:
-               pass
+       import audioDucking
+       if SPLConfig["Startup"]["AudioDuckingReminder"] and 
audioDucking.isAudioDuckingSupported():
+               gui.mainFrame.prePopup()
+               AudioDuckingReminder(gui.mainFrame).Show()
+               gui.mainFrame.postPopup()
 
 
 # Message verbosity pool.

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 5e0511e..743aab1 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -1,15 +1,12 @@
 # SPL Studio Configuration user interfaces
 # An app module and global plugin package for NVDA
-# Copyright 2016 Joseph Lee and others, released under GPL.
+# Copyright 2016-2017 Joseph Lee and others, released under GPL.
 # Split from SPL config module in 2016.
 # Provides the configuration management UI package for SPL Studio app module.
 # For code which provides foundation for code in this module, see splconfig 
module.
 
 import os
 import weakref
-import datetime
-import calendar
-import ui
 import api
 import gui
 import wx
@@ -666,6 +663,7 @@ class TriggersDialog(wx.Dialog):
 
                daysSizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, 
_("Day")), wx.HORIZONTAL)
                self.triggerDays = []
+               import calendar
                for day in xrange(len(calendar.day_name)):
                        triggerDay=wx.CheckBox(self, 
wx.NewId(),label=calendar.day_name[day])
                        triggerDay.SetValue((64 >> day & 
self.Parent._profileTriggersConfig[profile][0]) if profile in 
self.Parent._profileTriggersConfig else 0)
@@ -738,6 +736,7 @@ class TriggersDialog(wx.Dialog):
                                # Otherwise trigger flag will be added each 
time this is called (either this handler or the add-on settings' flags 
retriever must retrieve the flags set).
                                if not self.profile in 
parent._profileTriggersConfig:
                                        parent.setProfileFlags(self.selection, 
"add", _("time-based"))
+                               import datetime
                                parent._profileTriggersConfig[self.profile] = 
splconfig.setNextTimedProfile(self.profile, bit, datetime.time(hour, min))
                                parent._profileTriggersConfig[self.profile][6] 
= duration
                        else:
@@ -913,7 +912,7 @@ class MetadataStreamingDialog(wx.Dialog):
 
                # WX's CheckListBox isn't user friendly.
                # Therefore use checkboxes laid out across the top.
-               # 17.1: instead of two loops, just use one loop, with labels 
deriving from the below tuple.
+               # 17.04: instead of two loops, just use one loop, with labels 
deriving from the below tuple.
                # Only one loop is needed as helper.addLabelControl returns the 
checkbox itself and that can be appended.
                streamLabels = ("DSP encoder", "URL 1", "URL 2", "URL 3", "URL 
4")
                self.checkedStreams = []
@@ -983,7 +982,7 @@ class ColumnAnnouncementsDialog(wx.Dialog):
                # Same as metadata dialog (wx.CheckListBox isn't user friendly).
                # Gather values for checkboxes except artist and title.
                # 6.1: Split these columns into rows.
-               # 17.1: Gather items into a single list instead of three.
+               # 17.04: Gather items into a single list instead of three.
                self.checkedColumns = []
                sizer = gui.guiHelper.BoxSizerHelper(self, 
orientation=wx.HORIZONTAL)
                for column in ("Duration", "Intro", "Category", "Filename"):
@@ -1003,7 +1002,7 @@ class ColumnAnnouncementsDialog(wx.Dialog):
 
                # WXPython Phoenix contains RearrangeList to allow item orders 
to be changed automatically.
                # Because WXPython Classic doesn't include this, work around by 
using a variant of list box and move up/down buttons.
-               # 17.1: The label for the list below is above the list, so move 
move up/down buttons to the right of the list box.
+               # 17.04: The label for the list below is above the list, so 
move move up/down buttons to the right of the list box.
                # Translators: The label for a setting in SPL add-on dialog to 
select column announcement order.
                self.trackColumns = 
colAnnouncementsHelper.addLabeledControl(_("Column &order:"), wx.ListBox, 
choices=parent.columnOrder)
                self.trackColumns.Bind(wx.EVT_LISTBOX,self.onColumnSelection)
@@ -1101,7 +1100,7 @@ class ColumnsExplorerDialog(wx.Dialog):
                colExplorerHelper = gui.guiHelper.BoxSizerHelper(self, 
orientation=wx.VERTICAL)
 
                # 7.0: Studio 5.0x columns.
-               # 17.1: Five by two grid layout as 5.0x is no longer supported.
+               # 17.04: Five by two grid layout as 5.0x is no longer supported.
                sizer = gui.guiHelper.BoxSizerHelper(self, 
orientation=wx.HORIZONTAL)
                for slot in xrange(5):
                        # Translators: The label for a setting in SPL add-on 
dialog to select column for this column slot.
@@ -1329,9 +1328,9 @@ class ResetDialog(wx.Dialog):
                        if self.resetEncodersCheckbox.Value:
                                if 
os.path.exists(os.path.join(globalVars.appArgs.configPath, 
"splStreamLabels.ini")):
                                        
os.remove(os.path.join(globalVars.appArgs.configPath, "splStreamLabels.ini"))
-                               if "globalPlugins.SPLStudioUtils.encoders" in 
sys.modules:
-                                       import 
globalPlugins.SPLStudioUtils.encoders
-                                       
globalPlugins.SPLStudioUtils.encoders.cleanup()
+                               if "globalPlugins.splUtils.encoders" in 
sys.modules:
+                                       import globalPlugins.splUtils.encoders
+                                       
globalPlugins.splUtils.encoders.cleanup()
                        _configDialogOpened = False
                        # Translators: A dialog message shown when settings 
were reset to defaults.
                        wx.CallAfter(gui.messageBox, _("Successfully applied 
default add-on settings."),

diff --git a/addon/appModules/splstudio/splmisc.py 
b/addon/appModules/splstudio/splmisc.py
index d7a9940..e3355bc 100755
--- a/addon/appModules/splstudio/splmisc.py
+++ b/addon/appModules/splstudio/splmisc.py
@@ -1,6 +1,6 @@
 # SPL Studio Miscellaneous User Interfaces and internal services
 # An app module and global plugin package for NVDA
-# Copyright 2015-2016 Joseph Lee and others, released under GPL.
+# Copyright 2015-2017 Joseph Lee and others, released under GPL.
 # Miscellaneous functions and user interfaces
 # Split from config module in 2015.
 
@@ -13,7 +13,6 @@ from csv import reader # For cart explorer.
 import gui
 import wx
 import ui
-from NVDAObjects.IAccessible import sysListView32
 from winUser import user32, sendMessage
 
 # Locate column content.
@@ -23,6 +22,7 @@ from winUser import user32, sendMessage
 # In track finder, this is used when encountering the track item but NVDA says 
otherwise.
 def _getColumnContent(obj, col):
        import winKernel
+       from NVDAObjects.IAccessible import sysListView32
        # Borrowed from SysListView32 implementation.
        buffer=None
        processHandle=obj.processHandle
@@ -262,9 +262,12 @@ def _populateCarts(carts, cartlst, modifier, 
standardEdition=False):
                else: cart = "%s+%s"%(modifier, identifier)
                carts[cart] = cartName
 
-# Initialize Cart Explorer i.e. fetch carts.
+# Cart file timestamps.
+_cartEditTimestamps = [0, 0, 0, 0]
+               # Initialize Cart Explorer i.e. fetch carts.
 # Cart files list is for future use when custom cart names are used.
 def cartExplorerInit(StudioTitle, cartFiles=None):
+       global _cartEditTimestamps
        # Use cart files in SPL's data folder to build carts dictionary.
        # use a combination of SPL user name and static cart location to locate 
cart bank files.
        # Once the cart banks are located, use the routines in the populate 
method above to assign carts.
@@ -294,10 +297,31 @@ def cartExplorerInit(StudioTitle, cartFiles=None):
                        continue
                with open(cartFile) as cartInfo:
                        cl = [row for row in reader(cartInfo)]
+                       # 17.01: Look up file modification date to signal the 
app module that Cart Explorer reentry should occur.
+                       _cartEditTimestamps[cartFiles.index(f)] = 
os.path.getmtime(cartFile)
                _populateCarts(carts, cl[1], mod, 
standardEdition=carts["standardLicense"]) # See the comment for _populate 
method above.
        carts["faultyCarts"] = faultyCarts
        return carts
 
+# See if cart files were modified.
+# This is needed in order to announce Cart Explorer reentry command.
+def shouldCartExplorerRefresh(StudioTitle):
+       global _cartEditTimestamps
+       cartsDataPath = 
os.path.join(os.environ["PROGRAMFILES"],"StationPlaylist","Data") # Provided 
that Studio was installed using default path.
+       userNameIndex = StudioTitle.find("-")
+       # Until NVDA core moves to Python 3, assume that file names aren't 
unicode.
+       cartFiles = [u"main carts.cart", u"shift carts.cart", u"ctrl 
carts.cart", u"alt carts.cart"]
+       if userNameIndex >= 0:
+               cartFiles = [StudioTitle[userNameIndex+2:]+" "+cartFile for 
cartFile in cartFiles]
+       for f in cartFiles:
+               # No need to check for faulty carts here, as Cart Explorer 
activation checked it already.
+               timestamp = os.path.getmtime(os.path.join(cartsDataPath,f))
+               # 17.01: Look up file modification date to signal the app 
module that Cart Explorer reentry should occur.
+               # Optimization: Short-circuit if even one cart file has been 
modified.
+               if _cartEditTimestamps[cartFiles.index(f)] != timestamp:
+                       return True
+       return False
+
 
 # Countdown timer.
 # This is utilized by many services, chiefly profile triggers routine.
@@ -349,7 +373,7 @@ def _metadataAnnouncer(reminder=False, handle=None):
        # DSP is treated specially.
        dsp = sendMessage(handle, 1024, 0, 36)
        # For others, a simple list.append will do.
-       # 17.1: Use a conditional list comprehension.
+       # 17.04: Use a conditional list comprehension.
        streamCount = [str(pos) for pos in xrange(1, 5) if sendMessage(handle, 
1024, pos, 36)]
        # Announce streaming status when told to do so.
        status = None

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index c9e3a3a..218d98a 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -1,17 +1,13 @@
 # StationPlaylist Studio update checker
 # A support module for SPL add-on
-# Copyright 2015-2016, Joseph Lee, released under GPL.
+# Copyright 2015-2017 Joseph Lee, released under GPL.
 
 # Provides update check facility, basics borrowed from NVDA Core's update 
checker class.
 
-import urllib
 import os # Essentially, update download is no different than file downloads.
 import cPickle
-import threading
 import gui
 import wx
-import tones
-import time
 import addonHandler
 import globalVars
 
@@ -101,6 +97,7 @@ def updateCheck(auto=False, continuous=False, 
confUpdateInterval=1):
                return
        global _SPLUpdateT, SPLAddonCheck, _retryAfterFailure, _progressDialog, 
_updateNow
        if _updateNow: _updateNow = False
+       import time
        # Regardless of whether it is an auto check, update the check time.
        # However, this shouldnt' be done if this is a retry after a failed 
attempt.
        if not _retryAfterFailure: SPLAddonCheck = time.time()
@@ -112,6 +109,7 @@ def updateCheck(auto=False, continuous=False, 
confUpdateInterval=1):
        updateCandidate = False
        updateURL = SPLUpdateURL if SPLUpdateChannel not in channels else 
channels[SPLUpdateChannel]
        try:
+               import urllib
                # Look up the channel if different from the default.
                url = urllib.urlopen(updateURL)
                url.close()

diff --git a/addon/appModules/tracktool.py b/addon/appModules/tracktool.py
index df79410..ce45e94 100755
--- a/addon/appModules/tracktool.py
+++ b/addon/appModules/tracktool.py
@@ -1,15 +1,11 @@
 # StationPlaylist Track Tool
 # An app module for NVDA
-# Copyright 2014-2016 Joseph Lee and contributors, released under gPL.
+# Copyright 2014-2017 Joseph Lee and contributors, released under gPL.
 # Functionality is based on JFW scripts for SPL Track Tool by Brian Hartgen.
 
 import appModuleHandler
 import addonHandler
-import api
 import tones
-import speech
-import braille
-from controlTypes import ROLE_LISTITEM
 import ui
 from NVDAObjects.IAccessible import IAccessible
 from splstudio import splconfig
@@ -60,6 +56,7 @@ class TrackToolItem(IAccessible):
                                # Translators: Presented when some info is not 
defined for a track in Track Tool (example: cue not found)
                                ui.message(_("{header} not 
found").format(header = columnHeader))
                        else:
+                               import speech, braille
                                speech.speakMessage(_("{header}: 
blank").format(header = columnHeader))
                                braille.handler.message(_("{header}: 
()").format(header = columnHeader))
 
@@ -85,8 +82,7 @@ class TrackToolItem(IAccessible):
 
        def script_columnExplorer(self, gesture):
                # Just like the main app module, due to the below formula, 
columns explorer will be restricted to number commands.
-               columnPos = int(gesture.displayName.split("+")[-1])-1
-               header = 
splconfig.SPLConfig["General"]["ExploreColumnsTT"][columnPos]
+               header = 
splconfig.SPLConfig["General"]["ExploreColumnsTT"][int(gesture.displayName.split("+")[-1])-1]
                # Several corner cases.
                # Look up track name if artist is the header name.
                if header == "Artist":
@@ -102,8 +98,7 @@ class TrackToolItem(IAccessible):
                        ui.message(_("Introduction not set"))
                else:
                        try:
-                               pos = 
indexOf(self.appModule.productVersion).index(header)
-                               self.announceColumnContent(pos, 
columnHeader=header)
+                               
self.announceColumnContent(indexOf(self.appModule.productVersion).index(header),
 columnHeader=header)
                        except ValueError:
                                # Translators: Presented when some info is not 
defined for a track in Track Tool (example: cue not found)
                                ui.message(_("{headerText} not 
found").format(headerText = header))
@@ -119,6 +114,6 @@ class AppModule(appModuleHandler.AppModule):
        SPLColNumber = 0
 
        def chooseNVDAObjectOverlayClasses(self, obj, clsList):
-               if obj.windowClassName in ("TListView", 
"TTntListView.UnicodeClass") and obj.role == ROLE_LISTITEM:
+               import controlTypes
+               if obj.windowClassName in ("TListView", 
"TTntListView.UnicodeClass") and obj.role == controlTypes.ROLE_LISTITEM:
                        clsList.insert(0, TrackToolItem)
-

diff --git a/addon/doc/ar/readme.md b/addon/doc/ar/readme.md
index d38cd83..83680a9 100644
--- a/addon/doc/ar/readme.md
+++ b/addon/doc/ar/readme.md
@@ -249,6 +249,11 @@ broadcast profiles.
 استخدم لمسة ب3 أصابع للانتقال لنمط اللمس, ثم استخدم أوامر اللمس المسرودة
 أعلاه لأداء المهام.
 
+## Version 16.12.1
+
+* Corrected user interface presentation for SPL add-on settings dialog.
+* ترجمة الإضافة لمزيد من اللغات
+
 ## Version 16.12/15.4-LTS
 
 * More work on supporting Studio 5.20, including announcing cart insert mode

diff --git a/addon/doc/es/readme.md b/addon/doc/es/readme.md
index 3e9db61..72e3e18 100644
--- a/addon/doc/es/readme.md
+++ b/addon/doc/es/readme.md
@@ -287,6 +287,11 @@ realizar algunas órdenes de Studio desde la pantalla 
táctil. Primero utiliza
 un toque con tres dedos para cambiar a modo SPL, entonces utiliza las
 órdenes táctiles listadas arriba para llevar a cabo tareas.
 
+## Version 16.12.1
+
+* Corrected user interface presentation for SPL add-on settings dialog.
+* Traducciones actualizadas.
+
 ## Versión 16.12/15.4-LTS
 
 * Más trabajo sobre el soporte de Studio 5.20, incluyendo el anunciado del

diff --git a/addon/doc/fr/readme.md b/addon/doc/fr/readme.md
index 2d68631..c496723 100644
--- a/addon/doc/fr/readme.md
+++ b/addon/doc/fr/readme.md
@@ -297,6 +297,11 @@ un écran tactile. Tout d'abord utiliser une tape à trois 
doigts pour
 basculer en mode SPL, puis utilisez les commandes tactile énumérées
 ci-dessus pour exécuter des commandes.
 
+## Version 16.12.1
+
+* Corrected user interface presentation for SPL add-on settings dialog.
+* Mises à jour des traductions.
+
 ## Version 16.12/15.4-LTS
 
 * More work on supporting Studio 5.20, including announcing cart insert mode

diff --git a/addon/doc/gl/readme.md b/addon/doc/gl/readme.md
index 8933f44..8ebec55 100644
--- a/addon/doc/gl/readme.md
+++ b/addon/doc/gl/readme.md
@@ -279,6 +279,11 @@ realizar algunhas ordes do Studio dende a pantalla tactil. 
Primeiro usa un
 toque con tgres dedos para cambiar a modo SPL, logo usa as ordes tactiles
 listadas arriba para realizar ordes.
 
+## Version 16.12.1
+
+* Corrected user interface presentation for SPL add-on settings dialog.
+* Traducións actualizadas.
+
 ## Versión 16.12/15.4-LTS
 
 * Máis traballo no soporte do Studio 5.20, incluindo o anunciado do estado

diff --git a/addon/doc/hu/readme.md b/addon/doc/hu/readme.md
index 5ddcfac..4e65bd7 100644
--- a/addon/doc/hu/readme.md
+++ b/addon/doc/hu/readme.md
@@ -260,6 +260,11 @@ Amennyiben érintőképernyős számítógépen használja a 
Studiot Windows 8,
 parancsokat végrehajthat az érintőképernyőn is. Először 3 ujjas koppintással
 váltson SPL módra, és utána már használhatók az alább felsorolt parancsok.
 
+## Version 16.12.1
+
+* Corrected user interface presentation for SPL add-on settings dialog.
+* Fordítások frissítése
+
 ## Version 16.12/15.4-LTS
 
 * More work on supporting Studio 5.20, including announcing cart insert mode

diff --git a/addon/globalPlugins/SPLStudioUtils/__init__.py 
b/addon/globalPlugins/SPLStudioUtils/__init__.py
deleted file mode 100755
index 9701601..0000000
--- a/addon/globalPlugins/SPLStudioUtils/__init__.py
+++ /dev/null
@@ -1,339 +0,0 @@
-# StationPlaylist Utilities
-# Author: Joseph Lee
-# Copyright 2013-2016, released under GPL.
-# Adds a few utility features such as switching focus to the SPL Studio window 
and some global scripts.
-# For encoder support, see the encoders package.
-
-from functools import wraps
-import os
-import globalPluginHandler
-import api
-from controlTypes import ROLE_LISTITEM
-import ui
-import globalVars
-from NVDAObjects.IAccessible import getNVDAObjectFromEvent
-import winUser
-import tones
-import nvwave
-import gui
-import wx
-import addonHandler
-addonHandler.initTranslation()
-
-# Layer environment: same as the app module counterpart.
-
-def finally_(func, final):
-       """Calls final after func, even if it fails."""
-       def wrap(f):
-               @wraps(f)
-               def new(*args, **kwargs):
-                       try:
-                               func(*args, **kwargs)
-                       finally:
-                               final()
-               return new
-       return wrap(final)
-
-# SPL Studio uses WM messages to send and receive data, similar to Winamp (see 
NVDA sources/appModules/winamp.py for more information).
-user32 = winUser.user32 # user32.dll.
-SPLWin = 0 # A handle to studio window.
-SPLMSG = winUser.WM_USER
-
-# Various SPL IPC tags.
-SPLVersion = 2
-SPLPlay = 12
-SPLStop = 13
-SPLPause = 15
-SPLAutomate = 16
-SPLMic = 17
-SPLLineIn = 18
-SPLLibraryScanCount = 32
-SPLListenerCount = 35
-SPLStatusInfo = 39 #Studio 5.20 and later.
-SPL_TrackPlaybackStatus = 104
-SPLCurTrackPlaybackTime = 105
-
-
-# On/off toggle wave files.
-onFile = os.path.join(os.path.dirname(__file__), "..", "..", "appModules", 
"splstudio", "SPL_on.wav")
-offFile = os.path.join(os.path.dirname(__file__), "..", "..", "appModules", 
"splstudio", "SPL_off.wav")
-
-# Help message for SPL Controller
-# Translators: the dialog text for SPL Controller help.
-SPLConHelp=_("""
-After entering SPL Controller, press:
-A: Turn automation on.
-Shift+A: Turn automation off.
-M: Turn microphone on.
-Shift+M: Turn microphone off.
-N: Turn microphone on without fade.
-L: Turn line in on.
-Shift+L: Turn line in off.
-P: Play.
-U: Pause.
-S: Stop with fade.
-T: Instant stop.
-E: Announce if any encoders are being monitored.
-I: Announce listener count.
-Q: Announce Studio status information.
-R: Remaining time for the playing track.
-Shift+R: Library scan progress.""")
-
-# Try to see if SPL foreground object can be fetched. This is used for 
switching to SPL Studio window from anywhere and to switch to Studio window 
from SAM encoder window.
-
-def fetchSPLForegroundWindow():
-       # Turns out NVDA core does have a method to fetch desktop objects, so 
use this to find SPL window from among its children.
-       dt = api.getDesktopObject()
-       fg = None
-       fgCount = 0
-       for possibleFG in dt.children:
-               if "splstudio" in possibleFG.appModule.appModuleName:
-                       fg = possibleFG
-                       fgCount+=1
-       # Just in case the window is really minimized (not to the system tray)
-       if fgCount == 1:
-               fg = getNVDAObjectFromEvent(user32.FindWindowA("TStudioForm", 
None), winUser.OBJID_CLIENT, 0)
-       return fg
-
-
-class GlobalPlugin(globalPluginHandler.GlobalPlugin):
-
-       # Translators: Script category for Station Playlist commands in input 
gestures dialog.
-       scriptCategory = _("StationPlaylist Studio")
-
-       #Global layer environment (see the app module for more information).
-       SPLController = False # Control SPL from anywhere.
-
-       def getScript(self, gesture):
-               if not self.SPLController:
-                       return globalPluginHandler.GlobalPlugin.getScript(self, 
gesture)
-               script = globalPluginHandler.GlobalPlugin.getScript(self, 
gesture)
-               if not script:
-                       script = finally_(self.script_error, self.finish)
-               return finally_(script, self.finish)
-
-       def finish(self):
-               self.SPLController = False
-               self.clearGestureBindings()
-               self.bindGestures(self.__gestures)
-
-       def script_error(self, gesture):
-               tones.beep(120, 100)
-
-       # Switch focus to SPL Studio window from anywhere.
-       def script_focusToSPLWindow(self, gesture):
-               # 7.4: Forget it if this is the case like the following.
-               if globalVars.appArgs.secure: return
-               # Don't do anything if we're already focus on SPL Studio.
-               if "splstudio" in 
api.getForegroundObject().appModule.appModuleName: return
-               else:
-                       SPLHwnd = user32.FindWindowA("SPLStudio", None) # Used 
ANSI version, as Wide char version always returns 0.
-                       if SPLHwnd == 0: ui.message(_("SPL Studio is not 
running."))
-                       else:
-                               SPLFG = fetchSPLForegroundWindow()
-                               if SPLFG == None:
-                                       # Translators: Presented when Studio is 
minimized to system tray (notification area).
-                                       ui.message(_("SPL minimized to system 
tray."))
-                               else: SPLFG.setFocus()
-       # Translators: Input help mode message for a command to switch to 
Station Playlist Studio from any program.
-       script_focusToSPLWindow.__doc__=_("Moves to SPL Studio window from 
other programs.")
-
-       # The SPL Controller:
-       # This layer set allows the user to control various aspects of SPL 
Studio from anywhere.
-       def script_SPLControllerPrefix(self, gesture):
-               # 7.4: Red flag...
-               if globalVars.appArgs.secure: return
-               global SPLWin
-               # Error checks:
-               # 1. If SPL Studio is not running, print an error message.
-               # 2. If we're already  in SPL, ask the app module if SPL 
Assistant can be invoked with this command.
-               if "splstudio" in 
api.getForegroundObject().appModule.appModuleName:
-                       if not 
api.getForegroundObject().appModule.SPLConPassthrough():
-                               # Translators: Presented when NVDA cannot enter 
SPL Controller layer since SPL Studio is focused.
-                               ui.message(_("You are already in SPL Studio 
window. For status commands, use SPL Assistant commands."))
-                               self.finish()
-                               return
-                       else:
-                               
api.getForegroundObject().appModule.script_SPLAssistantToggle(gesture)
-                               return
-               SPLWin = user32.FindWindowA("SPLStudio", None)
-               if SPLWin == 0:
-                       # Translators: Presented when Station Playlist Studio 
is not running.
-                       ui.message(_("SPL Studio is not running."))
-                       self.finish()
-                       return
-               # No errors, so continue.
-               if not self.SPLController:
-                       self.bindGestures(self.__SPLControllerGestures)
-                       self.SPLController = True
-                       # Translators: The name of a layer command set for 
Station Playlist Studio.
-                       # Hint: it is better to translate it as "SPL Control 
Panel."
-                       ui.message(_("SPL Controller"))
-               else:
-                       self.script_error(gesture)
-                       self.finish()
-       # Translators: Input help mode message for a layer command in Station 
Playlist Studio.
-       script_SPLControllerPrefix.__doc__=_("SPl Controller layer command. See 
add-on guide for available commands.")
-
-       # The layer commands themselves. Calls user32.SendMessage method for 
each script.
-
-       def script_automateOn(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,1,SPLAutomate)
-               self.finish()
-
-       def script_automateOff(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,0,SPLAutomate)
-               self.finish()
-
-       def script_micOn(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,1,SPLMic)
-               nvwave.playWaveFile(onFile)
-               self.finish()
-
-       def script_micOff(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,0,SPLMic)
-               nvwave.playWaveFile(offFile)
-               self.finish()
-
-       def script_micNoFade(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,2,SPLMic)
-               self.finish()
-
-       def script_lineInOn(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,1,SPLLineIn)
-               self.finish()
-
-       def script_lineInOff(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,0,SPLLineIn)
-               self.finish()
-
-       def script_stopFade(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,0,SPLStop)
-               self.finish()
-
-       def script_stopInstant(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,1,SPLStop)
-               self.finish()
-
-       def script_play(self, gesture):
-               winUser.sendMessage(SPLWin, SPLMSG, 0, SPLPlay)
-               self.finish()
-
-       def script_pause(self, gesture):
-               playingNow = winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPL_TrackPlaybackStatus)
-               # Translators: Presented when no track is playing in Station 
Playlist Studio.
-               if not playingNow: ui.message(_("There is no track playing. Try 
pausing while a track is playing."))
-               elif playingNow == 3: winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPLPause)
-               else: winUser.sendMessage(SPLWin, SPLMSG, 1, SPLPause)
-               self.finish()
-
-       def script_libraryScanProgress(self, gesture):
-               scanned = winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPLLibraryScanCount)
-               # Translators: Announces number of items in the Studio's track 
library (example: 1000 items scanned).
-               ui.message(_("{itemCount} items scanned").format(itemCount = 
scanned))
-               self.finish()
-
-       def script_listenerCount(self, gesture):
-               count = winUser.sendMessage(SPLWin, SPLMSG, 0, SPLListenerCount)
-               # Translators: Announces number of stream listeners.
-               ui.message(_("Listener count: 
{listenerCount}").format(listenerCount = count))
-               self.finish()
-
-       def script_remainingTime(self, gesture):
-               remainingTime = winUser.sendMessage(SPLWin, SPLMSG, 3, 
SPLCurTrackPlaybackTime)
-               # Translators: Presented when no track is playing in Station 
Playlist Studio.
-               if remainingTime < 0: ui.message(_("There is no track 
playing."))
-               else:
-                       # 7.0: Present remaining time in hh:mm:ss format for 
enhanced experience (borrowed from the app module).
-                       remainingTime = (remainingTime/1000)+1
-                       if remainingTime == 0: ui.message("00:00")
-                       elif 1 <= remainingTime <= 59: 
ui.message("00:{0}".format(str(remainingTime).zfill(2)))
-                       else:
-                               mm, ss = divmod(remainingTime, 60)
-                               if mm > 59:
-                                       hh, mm = divmod(mm, 60)
-                                       t0 = str(hh).zfill(2)
-                                       t1 = str(mm).zfill(2)
-                                       t2 = str(ss).zfill(2)
-                                       ui.message(":".join([t0, t1, t2]))
-                               else:
-                                       t1 = str(mm).zfill(2)
-                                       t2 = str(ss).zfill(2)
-                                       ui.message(":".join([t1, t2]))
-               self.finish()
-
-       def script_announceNumMonitoringEncoders(self, gesture):
-               import encoders
-               encoders.announceNumMonitoringEncoders()
-               self.finish()
-
-       def script_statusInfo(self, gesture):
-               # For consistency reasons (because of the Studio status bar), 
messages in this method will remain in English.
-               statusInfo = []
-               # 17.1: For Studio 5.10 and up, announce playback and 
automation status.
-               playingNow = winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPL_TrackPlaybackStatus)
-               statusInfo.append("Play status: playing" if playingNow else 
"Play status: stopped")
-               # For automation, Studio 5.11 and earlier does not have an easy 
way to detect this flag, thus resort to using playback status.
-               if winUser.sendMessage(SPLWin, SPLMSG, 0, SPLVersion) < 520:
-                       statusInfo.append("Automation on" if playingNow == 2 
else "Automation off")
-               else:
-                       statusInfo.append("Automation on" if 
winUser.sendMessage(SPLWin, SPLMSG, 1, SPLStatusInfo) else "Automation off")
-                       # 5.20 and later.
-                       statusInfo.append("Microphone on" if 
winUser.sendMessage(SPLWin, SPLMSG, 2, SPLStatusInfo) else "Microphone off")
-                       statusInfo.append("Line-inon" if 
winUser.sendMessage(SPLWin, SPLMSG, 3, SPLStatusInfo) else "Line-in off")
-                       statusInfo.append("Record to file on" if 
winUser.sendMessage(SPLWin, SPLMSG, 4, SPLStatusInfo) else "Record to file off")
-                       cartEdit = winUser.sendMessage(SPLWin, SPLMSG, 5, 
SPLStatusInfo)
-                       cartInsert = winUser.sendMessage(SPLWin, SPLMSG, 6, 
SPLStatusInfo)
-                       if cartEdit: statusInfo.append("Cart edit on")
-                       elif not cartEdit and cartInsert: 
statusInfo.append("Cart insert on")
-                       else: statusInfo.append("Cart edit off")
-               ui.message("; ".join(statusInfo))
-               self.finish()
-       # Translators: Input help message for a SPL Controller command.
-       script_statusInfo.__doc__ = _("Announces Studio status such as track 
playback status from other programs")
-
-       def script_conHelp(self, gesture):
-               # Translators: The title for SPL Controller help dialog.
-               wx.CallAfter(gui.messageBox, SPLConHelp, _("SPL Controller 
help"))
-               self.finish()
-
-
-       __SPLControllerGestures={
-               "kb:p":"play",
-               "kb:a":"automateOn",
-               "kb:shift+a":"automateOff",
-               "kb:m":"micOn",
-               "kb:shift+m":"micOff",
-               "kb:n":"micNoFade",
-               "kb:l":"lineInOn",
-               "kb:shift+l":"lineInOff",
-               "kb:shift+r":"libraryScanProgress",
-               "kb:s":"stopFade",
-               "kb:t":"stopInstant",
-               "kb:u":"pause",
-               "kb:r":"remainingTime",
-               "kb:e":"announceNumMonitoringEncoders",
-               "kb:i":"listenerCount",
-               "kb:q":"statusInfo",
-               "kb:f1":"conHelp"
-       }
-
-
-       __gestures={
-               #"kb:nvda+shift+`":"focusToSPLWindow",
-               #"kb:nvda+`":"SPLControllerPrefix"
-       }
-
-       # Support for Encoders
-       # Each encoder is an overlay class, thus makes it easier to add 
encoders in the future by implementing overlay objects.
-       # Each encoder, at a minimum, must support connection monitoring 
routines.
-
-       def chooseNVDAObjectOverlayClasses(self, obj, clsList):
-               if obj.appModule.appName in ("splengine", "splstreamer"):
-                       import encoders
-                       if obj.windowClassName == "TListView":
-                               clsList.insert(0, encoders.SAMEncoder)
-                       elif obj.windowClassName == "SysListView32":
-                               if obj.role == ROLE_LISTITEM:
-                                       clsList.insert(0, encoders.SPLEncoder)
-

diff --git a/addon/globalPlugins/SPLStudioUtils/encoders.py 
b/addon/globalPlugins/SPLStudioUtils/encoders.py
deleted file mode 100755
index b24eb40..0000000
--- a/addon/globalPlugins/SPLStudioUtils/encoders.py
+++ /dev/null
@@ -1,856 +0,0 @@
-# StationPlaylist encoders support
-# Author: Joseph Lee
-# Copyright 2015-2016, released under GPL.
-# Split from main global plugin in 2015.
-
-import threading
-import os
-import time
-import weakref
-from configobj import ConfigObj
-import api
-import ui
-import speech
-import globalVars
-import scriptHandler
-from NVDAObjects.IAccessible import IAccessible, getNVDAObjectFromEvent
-import winUser
-import winKernel
-import tones
-import gui
-import wx
-
-
-# SPL Studio uses WM messages to send and receive data, similar to Winamp (see 
NVDA sources/appModules/winamp.py for more information).
-user32 = winUser.user32 # user32.dll.
-SPLWin = 0 # A handle to studio window.
-SPLMSG = winUser.WM_USER
-
-# Various SPL IPC tags.
-SPLPlay = 12
-SPL_TrackPlaybackStatus = 104
-
-# Needed in Encoder support:
-SPLFocusToStudio = set() # Whether to focus to Studio or not.
-SPLPlayAfterConnecting = set()
-SPLBackgroundMonitor = set()
-SPLNoConnectionTone = set()
-
-# Customized for each encoder type.
-SAMStreamLabels= {} # A dictionary to store custom labels for each stream.
-SPLStreamLabels= {} # Same as above but optimized for SPL encoders (Studio 
5.00 and later).
-SAMMonitorThreads = {}
-SPLMonitorThreads = {}
-encoderMonCount = {"SAM":0, "SPL":0}
-
-# Configuration management.
-streamLabels = None
-
-# Load stream labels (and possibly other future goodies) from a file-based 
database.
-def loadStreamLabels():
-       global streamLabels, SAMStreamLabels, SPLStreamLabels, 
SPLFocusToStudio, SPLPlayAfterConnecting, SPLBackgroundMonitor, 
SPLNoConnectionTone
-       streamLabels = ConfigObj(os.path.join(globalVars.appArgs.configPath, 
"splStreamLabels.ini"))
-       # Read stream labels.
-       try:
-               SAMStreamLabels = dict(streamLabels["SAMEncoders"])
-       except KeyError:
-               SAMStreamLabels = {}
-       try:
-               SPLStreamLabels = dict(streamLabels["SPLEncoders"])
-       except KeyError:
-               SPLStreamLabels = {}
-       # Read other settings.
-       if "FocusToStudio" in streamLabels:
-               SPLFocusToStudio = set(streamLabels["FocusToStudio"])
-       if "PlayAfterConnecting" in streamLabels:
-               SPLPlayAfterConnecting = 
set(streamLabels["PlayAfterConnecting"])
-       if "BackgroundMonitor" in streamLabels:
-               SPLBackgroundMonitor = set(streamLabels["BackgroundMonitor"])
-       if "ConnectionTone" in streamLabels:
-               SPLNoConnectionTone = set(streamLabels["NoConnectionTone"])
-
-# Report number of encoders being monitored.
-# 6.0: Refactor the below function to use the newer encoder config format.
-def getStreamLabel(identifier):
-       encoderType, id = identifier.split()
-       # 5.2: Use a static map.
-       # 6.0: Look up the encoder type.
-       if encoderType == "SAM": labels = SAMStreamLabels
-       elif encoderType == "SPL": labels = SPLStreamLabels
-       if id in labels: return labels[id]
-       return None
-
-def announceNumMonitoringEncoders():
-       monitorCount = len(SPLBackgroundMonitor)
-       if not monitorCount:
-               # Translators: Message presented when there are no encoders 
being monitored.
-               ui.message(_("No encoders are being monitored"))
-       else:
-               # Locate stream labels if any.
-               labels = []
-               for identifier in SPLBackgroundMonitor:
-                       label = getStreamLabel(identifier)
-                       if label is None: labels.append(identifier)
-                       else: labels.append("{encoderID} 
({streamLabel})".format(encoderID = identifier, streamLabel=label))
-               # Translators: Announces number of encoders being monitored in 
the background.
-               ui.message(_("Number of encoders monitored: {numberOfEncoders}: 
{streamLabels}").format(numberOfEncoders = monitorCount, streamLabels=", 
".join(labels)))
-
-# Remove encoder ID from various settings maps.
-# This is a private module level function in order for it to be invoked by 
humans alone.
-_encoderConfigRemoved = None
-def _removeEncoderID(encoderType, pos):
-       global _encoderConfigRemoved
-       # For now, store the key to map.
-       # This might become a module-level constant if other functions require 
this dictionary.
-       key2map = {"FocusToStudio":SPLFocusToStudio, 
"PlayAfterConnecting":SPLPlayAfterConnecting, 
"BackgroundMonitor":SPLBackgroundMonitor, 
"NoConnectionTone":SPLNoConnectionTone}
-       encoderID = " ".join([encoderType, pos])
-       # Go through each feature map, remove the encoder ID and manipulate 
encoder positions in these sets.
-       # For each set, have a list of set items handy, otherwise set 
cardinality error (RuntimeError) will occur if items are removed on the fly.
-       for key in key2map:
-               map = key2map[key]
-               if encoderID in map:
-                       map.remove(encoderID)
-                       _encoderConfigRemoved = True
-               # If not sorted, encoders will appear in random order (a 
downside of using sets, as their ordering is quite unpredictable).
-               currentEncoders = sorted(filter(lambda x: 
x.startswith(encoderType), map))
-               if len(currentEncoders) and encoderID < currentEncoders[-1]:
-                       # Same algorithm as stream label remover.
-                       start = 0
-                       if encoderID > currentEncoders[0]:
-                               for candidate in currentEncoders:
-                                       if encoderID < candidate:
-                                               start = 
currentEncoders.index(candidate)
-                       # Do set entry manipulations (remove first, then add).
-                       for item in currentEncoders[start:]:
-                               map.remove(item)
-                               map.add(" ".join([encoderType, 
"%s"%(int(item.split()[-1])-1)]))
-               _encoderConfigRemoved = True
-               if len(map): streamLabels[key] = list(map)
-               else:
-                       try:
-                               del streamLabels[key]
-                       except KeyError:
-                               pass
-
-# Nullify various flag sets, otherwise memory leak occurs.
-def cleanup():
-       global streamLabels, SAMStreamLabels, SPLStreamLabels, 
SPLFocusToStudio, SPLPlayAfterConnecting, SPLBackgroundMonitor, 
SPLNoConnectionTone, encoderMonCount, SAMMonitorThreads, SPLMonitorThreads
-       for map in [streamLabels, SAMStreamLabels, SPLStreamLabels, 
SPLFocusToStudio, SPLPlayAfterConnecting, SPLBackgroundMonitor, 
SPLNoConnectionTone, SAMMonitorThreads, SPLMonitorThreads]:
-               if map is not None: map.clear()
-       # Nullify stream labels.
-       streamLabels = None
-       # Without resetting monitor count, we end up with higher and higher 
value for this.
-       # 7.0: Destroy threads also.
-       encoderMonCount = {"SAM":0, "SPL":0}
-
-
-# Try to see if SPL foreground object can be fetched. This is used for 
switching to SPL Studio window from anywhere and to switch to Studio window 
from SAM encoder window.
-
-def fetchSPLForegroundWindow():
-       # Turns out NVDA core does have a method to fetch desktop objects, so 
use this to find SPL window from among its children.
-       dt = api.getDesktopObject()
-       fg = None
-       fgCount = 0
-       for possibleFG in dt.children:
-               if "splstudio" in possibleFG.appModule.appModuleName:
-                       fg = possibleFG
-                       fgCount+=1
-       # Just in case the window is really minimized (not to the system tray)
-       if fgCount == 1:
-               fg = getNVDAObjectFromEvent(user32.FindWindowA("TStudioForm", 
None), winUser.OBJID_CLIENT, 0)
-       return fg
-
-
-# Encoder configuration dialog.
-_configDialogOpened = False
-
-# Presented if the config dialog for another encoder is opened.
-def _configDialogError():
-       # Translators: Text of the dialog when another alarm dialog is open.
-       gui.messageBox(_("Another encoder settings dialog is 
open."),_("Error"),style=wx.OK | wx.ICON_ERROR)
-
-class EncoderConfigDialog(wx.Dialog):
-
-       # The following comes from exit dialog class from GUI package (credit: 
NV Access and Zahari from Bulgaria).
-       _instance = None
-
-       def __new__(cls, parent, *args, **kwargs):
-               # Make this a singleton and prompt an error dialog if it isn't.
-               if _configDialogOpened:
-                       raise RuntimeError("An instance of encoder settings 
dialog is opened")
-               inst = cls._instance() if cls._instance else None
-               if not inst:
-                       return super(cls, cls).__new__(cls, parent, *args, 
**kwargs)
-               return inst
-
-       def __init__(self, parent, obj):
-               inst = EncoderConfigDialog._instance() if 
EncoderConfigDialog._instance else None
-               if inst:
-                       return
-               # Use a weakref so the instance can die.
-               EncoderConfigDialog._instance = weakref.ref(self)
-
-               self.obj = obj
-               self.curStreamLabel, title = obj.getStreamLabel(getTitle=True)
-               # Translators: The title of the encoder settings dialog 
(example: Encoder settings for SAM 1").
-               super(EncoderConfigDialog, self).__init__(parent, wx.ID_ANY, 
_("Encoder settings for {name}").format(name = title))
-               mainSizer = wx.BoxSizer(wx.VERTICAL)
-               encoderConfigHelper = gui.guiHelper.BoxSizerHelper(self, 
orientation=wx.VERTICAL)
-
-               # Translators: An edit field in encoder settings to set stream 
label for this encoder.
-               self.streamLabel = 
encoderConfigHelper.addLabeledControl(_("Stream &label"), wx.TextCtrl)
-               self.streamLabel.SetValue(self.curStreamLabel if 
self.curStreamLabel is not None else "")
-
-               # Translators: A checkbox in encoder settings to set if NvDA 
should switch focus to Studio window when connected.
-               self.focusToStudio = 
encoderConfigHelper.addItem(wx.CheckBox(self, label=_("&Focus to Studio when 
connected")))
-               self.focusToStudio.SetValue(obj.getEncoderId() in 
SPLFocusToStudio)
-               # Translators: A checkbox in encoder settings to set if NvDA 
should play the next track when connected.
-               self.playAfterConnecting = 
encoderConfigHelper.addItem(wx.CheckBox(self, label=_("&Play first track when 
connected")))
-               self.playAfterConnecting.SetValue(obj.getEncoderId() in 
SPLPlayAfterConnecting)
-               # Translators: A checkbox in encoder settings to set if NvDA 
should monitor the status of this encoder in the background.
-               self.backgroundMonitor = 
encoderConfigHelper.addItem(wx.CheckBox(self, label=_("Enable background 
connection &monitoring")))
-               self.backgroundMonitor.SetValue(obj.getEncoderId() in 
SPLBackgroundMonitor)
-               # Translators: A checkbox in encoder settings to set if NvDA 
should play connection progress tone.
-               self.noConnectionTone = 
encoderConfigHelper.addItem(wx.CheckBox(self, label=_("Play connection status 
&beep while connecting")))
-               self.noConnectionTone.SetValue(obj.getEncoderId() not in 
SPLNoConnectionTone)
-
-               
encoderConfigHelper.addDialogDismissButtons(self.CreateButtonSizer(wx.OK | 
wx.CANCEL))
-               self.Bind(wx.EVT_BUTTON,self.onOk,id=wx.ID_OK)
-               self.Bind(wx.EVT_BUTTON,self.onCancel,id=wx.ID_CANCEL)
-               mainSizer.Add(encoderConfigHelper.sizer, border = 
gui.guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL)
-               mainSizer.Fit(self)
-               self.SetSizer(mainSizer)
-               self.Center(wx.BOTH | wx.CENTER_ON_SCREEN)
-               self.streamLabel.SetFocus()
-
-       def onOk(self, evt):
-               self.obj._set_Flags(self.obj.getEncoderId(), 
self.focusToStudio.Value, SPLFocusToStudio, "FocusToStudio", save=False)
-               self.obj._set_Flags(self.obj.getEncoderId(), 
self.playAfterConnecting.Value, SPLPlayAfterConnecting, "PlayAfterConnecting", 
save=False)
-               self.obj._set_Flags(self.obj.getEncoderId(), 
self.backgroundMonitor.Value, SPLBackgroundMonitor, "BackgroundMonitor", 
save=False)
-               # Invert the following only.
-               self.obj._set_Flags(self.obj.getEncoderId(), not 
self.noConnectionTone.Value, SPLNoConnectionTone, "NoConnectionTone", 
save=False)
-               newStreamLabel = self.streamLabel.Value
-               if newStreamLabel is None: newStreamLabel = ""
-               if newStreamLabel == self.curStreamLabel:
-                       streamLabels.write() # Only flag(s) have changed.
-               else: self.obj.setStreamLabel(newStreamLabel)
-               self.Destroy()
-
-       def onCancel(self, evt):
-               self.Destroy()
-
-
-# Support for various encoders.
-# Each encoder must support connection routines.
-
-class Encoder(IAccessible):
-       """Represents an encoder from within StationPlaylist Studio or Streamer.
-       This base encoder provides scripts for all encoders such as stream 
labeler and toggling focusing to Studio when connected.
-       Subclasses must provide scripts to handle encoder connection and 
connection announcement routines.
-       In addition, they must implement the required actions to set options 
such as focusing to Studio, storing stream labels and so on, as each subclass 
relies on a feature map.
-       For example, for SAM encoder class, the feature map is SAM* where * 
denotes the feature in question.
-       Lastly, each encoder class must provide a unique identifying string to 
identify the type of the encoder (e.g. SAM for SAM encoder).
-       """
-
-       # Few useful variables for encoder list:
-       focusToStudio = False # If true, Studio will gain focus after encoder 
connects.
-       playAfterConnecting = False # When connected, the first track will be 
played.
-       backgroundMonitor = False # Monitor this encoder for connection status 
changes.
-       connectionTone = True # Play connection tone while connecting.
-
-
-       # Some helper functions
-
-       # Get the encoder identifier.
-       # This consists of two or three letter abbreviations for the encoder 
and the child ID.
-       def getEncoderId(self):
-               return " ".join([self.encoderType, 
str(self.IAccessibleChildID)])
-
-       # Format the status message to prepare for monitoring multiple encoders.
-       def encoderStatusMessage(self, message, id):
-               if encoderMonCount[self.encoderType] > 1:
-                       # Translators: Status message for encoder monitoring.
-                       ui.message(_("{encoder} {encoderNumber}: 
{status}").format(encoder = self.encoderType, encoderNumber = id, status = 
message))
-               else:
-                       ui.message(message)
-
-       # A master flag setter.
-       # Set or clear a given flag for the encoder given its ID, flag and flag 
container (currently a feature set).
-       # Also take in the flag key for storing it into the settings file.
-       # The flag will then be written to the configuration file.
-       # 7.0: Don't dump flags to disk unless told.
-       def _set_Flags(self, encoderId, flag, flagMap, flagKey, save=True):
-               if flag and not encoderId in flagMap:
-                       flagMap.add(encoderId)
-               elif not flag and encoderId in flagMap:
-                       flagMap.remove(encoderId)
-               # No need to store an empty flag map.
-               if len(flagMap): streamLabels[flagKey] = list(flagMap)
-               else:
-                       try:
-                               del streamLabels[flagKey]
-                       except KeyError:
-                               pass
-               if save: streamLabels.write()
-
-       # Now the flag configuration scripts.
-       # Project Rainbow: a new way to configure these will be created.
-
-       def script_toggleFocusToStudio(self, gesture):
-               if not self.focusToStudio:
-                       self.focusToStudio = True
-                       # Translators: Presented when toggling the setting to 
switch to Studio when connected to a streaming server.
-                       ui.message(_("Switch to Studio after connecting"))
-               else:
-                       self.focusToStudio = False
-                       # Translators: Presented when toggling the setting to 
switch to Studio when connected to a streaming server.
-                       ui.message(_("Do not switch to Studio after 
connecting"))
-               self._set_Flags(self.getEncoderId(), self.focusToStudio, 
SPLFocusToStudio, "FocusToStudio")
-       # Translators: Input help mode message in SAM Encoder window.
-       script_toggleFocusToStudio.__doc__=_("Toggles whether NVDA will switch 
to Studio when connected to a streaming server.")
-
-       def script_togglePlay(self, gesture):
-               if not self.playAfterConnecting:
-                       self.playAfterConnecting = True
-                       # Translators: Presented when toggling the setting to 
play selected song when connected to a streaming server.
-                       ui.message(_("Play first track after connecting"))
-               else:
-                       self.playAfterConnecting = False
-                       # Translators: Presented when toggling the setting to 
switch to Studio when connected to a streaming server.
-                       ui.message(_("Do not play first track after 
connecting"))
-               self._set_Flags(self.getEncoderId(), self.playAfterConnecting, 
SPLPlayAfterConnecting, "PlayAfterConnecting")
-       # Translators: Input help mode message in SAM Encoder window.
-       script_togglePlay.__doc__=_("Toggles whether Studio will play the first 
song when connected to a streaming server.")
-
-       def script_toggleBackgroundEncoderMonitor(self, gesture):
-               if scriptHandler.getLastScriptRepeatCount()==0:
-                       if not self.backgroundMonitor:
-                               self.backgroundMonitor = True
-                               encoderMonCount[self.encoderType] += 1 # 
Multiple encoders.
-                               # Translators: Presented when toggling the 
setting to monitor the selected encoder.
-                               ui.message(_("Monitoring encoder 
{encoderNumber}").format(encoderNumber = self.IAccessibleChildID))
-                       else:
-                               self.backgroundMonitor = False
-                               encoderMonCount[self.encoderType] -= 1
-                               # Translators: Presented when toggling the 
setting to monitor the selected encoder.
-                               ui.message(_("Encoder {encoderNumber} will not 
be monitored").format(encoderNumber = self.IAccessibleChildID))
-                       threadPool = self.setBackgroundMonitor()
-                       if self.backgroundMonitor:
-                               try:
-                                       monitoring = 
threadPool[self.IAccessibleChildID].isAlive()
-                               except KeyError:
-                                       monitoring = False
-                               if not monitoring:
-                                       statusThread = 
threading.Thread(target=self.reportConnectionStatus)
-                                       statusThread.name = "Connection Status 
Reporter " + str(self.IAccessibleChildID)
-                                       statusThread.start()
-                                       threadPool[self.IAccessibleChildID] = 
statusThread
-               else:
-                       for encoderType in encoderMonCount:
-                               encoderMonCount[encoderType] = 0
-                       SPLBackgroundMonitor.clear()
-                       # Translators: Announced when background encoder 
monitoring is canceled.
-                       ui.message(_("Encoder monitoring canceled"))
-       # Translators: Input help mode message in SAM Encoder window.
-       script_toggleBackgroundEncoderMonitor.__doc__=_("Toggles whether NVDA 
will monitor the selected encoder in the background.")
-
-       def script_streamLabeler(self, gesture):
-               curStreamLabel, title = self.getStreamLabel(getTitle=True)
-               if not curStreamLabel: curStreamLabel = ""
-               # Translators: The title of the stream labeler dialog (example: 
stream labeler for 1).
-               streamTitle = _("Stream labeler for 
{streamEntry}").format(streamEntry = title)
-               # Translators: The text of the stream labeler dialog.
-               streamText = _("Enter the label for this stream")
-               dlg = wx.TextEntryDialog(gui.mainFrame,
-               streamText, streamTitle, defaultValue=curStreamLabel)
-               def callback(result):
-                       if result == wx.ID_OK:
-                               newStreamLabel = dlg.GetValue()
-                               if newStreamLabel == curStreamLabel:
-                                       return # No need to write to disk.
-                               else: self.setStreamLabel(newStreamLabel)
-               gui.runScriptModalDialog(dlg, callback)
-       # Translators: Input help mode message in SAM Encoder window.
-       script_streamLabeler.__doc__=_("Opens a dialog to label the selected 
encoder.")
-
-       def script_streamLabelEraser(self, gesture):
-               # Translators: The title of the stream configuration eraser 
dialog.
-               streamEraserTitle = _("Stream label and settings eraser")
-               # Translators: The text of the stream configuration eraser 
dialog.
-               streamEraserText = _("Enter the position of the encoder you 
wish to delete or will delete")
-               dlg = wx.NumberEntryDialog(gui.mainFrame,
-               streamEraserText, "", streamEraserTitle, 
self.IAccessibleChildID, 1, self.simpleParent.childCount)
-               def callback(result):
-                       if result == wx.ID_OK:
-                               self.removeStreamConfig(str(dlg.GetValue()))
-               gui.runScriptModalDialog(dlg, callback)
-       # Translators: Input help mode message in SAM Encoder window.
-       script_streamLabelEraser.__doc__=_("Opens a dialog to erase stream 
labels and settings from an encoder that was deleted.")
-
-       # stream settings.
-       def script_encoderSettings(self, gesture):
-               try:
-                       d = EncoderConfigDialog(gui.mainFrame, self)
-                       gui.mainFrame.prePopup()
-                       d.Raise()
-                       d.Show()
-                       gui.mainFrame.postPopup()
-               except RuntimeError:
-                       wx.CallAfter(ui.message, "A settings dialog is opened")
-       # Translators: Input help mode message for a command in Station 
Playlist Studio.
-       script_encoderSettings.__doc__=_("Shows encoder configuration dialog to 
configure various encoder settings such as stream label.")
-       script_encoderSettings.category=_("Station Playlist Studio")
-
-       # Announce complete time including seconds (slight change from global 
commands version).
-       def script_encoderDateTime(self, gesture):
-               if scriptHandler.getLastScriptRepeatCount()==0:
-                       
text=winKernel.GetTimeFormat(winKernel.LOCALE_USER_DEFAULT, 0, None, None)
-               else:
-                       
text=winKernel.GetDateFormat(winKernel.LOCALE_USER_DEFAULT, 
winKernel.DATE_LONGDATE, None, None)
-               ui.message(text)
-       # Translators: Input help mode message for report date and time command.
-       script_encoderDateTime.__doc__=_("If pressed once, reports the current 
time including seconds. If pressed twice, reports the current date")
-       script_encoderDateTime.category=_("Station Playlist Studio")
-
-       # Various column announcement scripts.
-       # This base class implements encoder position and stream labels.
-       def script_announceEncoderPosition(self, gesture):
-               ui.message(_("Position: {pos}").format(pos = 
self.IAccessibleChildID))
-
-       def script_announceEncoderLabel(self, gesture):
-               try:
-                       streamLabel = self.getStreamLabel()[0]
-               except TypeError:
-                       streamLabel = None
-               if streamLabel:
-                       ui.message(_("Label: {label}").format(label = 
streamLabel))
-               else:
-                       ui.message(_("No stream label"))
-
-
-       def initOverlayClass(self):
-               global encoderMonCount
-               # Load stream labels upon request.
-               if streamLabels is None: loadStreamLabels()
-               encoderIdentifier = self.getEncoderId()
-               # Can I switch to Studio when connected to a streaming server?
-               try:
-                       self.focusToStudio = encoderIdentifier in 
SPLFocusToStudio
-               except KeyError:
-                       pass
-               # Can I play tracks when connected?
-               try:
-                       self.playAfterConnecting = encoderIdentifier in 
SPLPlayAfterConnecting
-               except KeyError:
-                       pass
-               # Am I being monitored for connection changes?
-               try:
-                       self.backgroundMonitor = encoderIdentifier in 
SPLBackgroundMonitor
-               except KeyError:
-                       pass
-               # 6.2: Make sure background monitor threads are started if the 
flag is set.
-               if self.backgroundMonitor:
-                       if self.encoderType == "SAM": threadPool = 
SAMMonitorThreads
-                       elif self.encoderType == "SPL": threadPool = 
SPLMonitorThreads
-                       if self.IAccessibleChildID in threadPool:
-                               if not 
threadPool[self.IAccessibleChildID].is_alive():
-                                       del threadPool[self.IAccessibleChildID]
-                               # If it is indeed alive... Otherwise another 
thread will be created to keep an eye on this encoder (undesirable).
-                               else: return
-                       statusThread = 
threading.Thread(target=self.reportConnectionStatus)
-                       statusThread.name = "Connection Status Reporter " + 
str(self.IAccessibleChildID)
-                       statusThread.start()
-                       threadPool[self.IAccessibleChildID] = statusThread
-                       encoderMonCount[self.encoderType] += 1
-               # Can I play connection beeps?
-               try:
-                       self.connectionTone = encoderIdentifier not in 
SPLNoConnectionTone
-               except KeyError:
-                       pass
-
-       def reportFocus(self):
-               try:
-                       streamLabel = self.getStreamLabel()[0]
-               except TypeError:
-                       streamLabel = None
-               # Announce stream label if it exists.
-               if streamLabel is not None:
-                       try:
-                               self.name = "(" + streamLabel + ") " + self.name
-                       except TypeError:
-                               pass
-               super(Encoder, self).reportFocus()
-
-
-       __gestures={
-               "kb:f11":"toggleFocusToStudio",
-               "kb:shift+f11":"togglePlay",
-               "kb:control+f11":"toggleBackgroundEncoderMonitor",
-               "kb:f12":"streamLabeler",
-               "kb:control+f12":"streamLabelEraser",
-               "kb:NVDA+F12":"encoderDateTime",
-               "kb:alt+NVDA+0":"encoderSettings",
-               "kb:control+NVDA+1":"announceEncoderPosition",
-               "kb:control+NVDA+2":"announceEncoderLabel",
-       }
-
-
-class SAMEncoder(Encoder):
-       # Support for Sam Encoders.
-
-       encoderType = "SAM"
-
-       def reportConnectionStatus(self, connecting=False):
-               # Keep an eye on the stream's description field for connection 
changes.
-               # In order to not block NVDA commands, this will be done using 
a different thread.
-               SPLWin = user32.FindWindowA("SPLStudio", None)
-               toneCounter = 0
-               messageCache = ""
-               # Status message flags.
-               idle = False
-               error = False
-               encoding = False
-               alreadyEncoding = False
-               while True:
-                       time.sleep(0.001)
-                       try:
-                               if messageCache != 
self.description[self.description.find("Status")+8:]:
-                                       messageCache = 
self.description[self.description.find("Status")+8:]
-                                       if not 
messageCache.startswith("Encoding"):
-                                               
self.encoderStatusMessage(messageCache, self.IAccessibleChildID)
-                       except AttributeError:
-                               return
-                       if messageCache.startswith("Idle"):
-                               if alreadyEncoding: alreadyEncoding = False
-                               if encoding: encoding = False
-                               if not idle:
-                                       tones.beep(250, 250)
-                                       idle = True
-                                       toneCounter = 0
-                       elif messageCache.startswith("Error"):
-                               # Announce the description of the error.
-                               if connecting: connecting= False
-                               if not error:
-                                       error = True
-                                       toneCounter = 0
-                               if alreadyEncoding: alreadyEncoding = False
-                       elif messageCache.startswith("Encoding"):
-                               if connecting: connecting = False
-                               # We're on air, so exit unless told to monitor 
for connection changes.
-                               if not encoding:
-                                       tones.beep(1000, 150)
-                                       self.encoderStatusMessage(messageCache, 
self.IAccessibleChildID)
-                               if self.focusToStudio and not encoding:
-                                       if api.getFocusObject().appModule == 
"splstudio":
-                                               continue
-                                       try:
-                                               
fetchSPLForegroundWindow().setFocus()
-                                       except AttributeError:
-                                               pass
-                               if self.playAfterConnecting and not encoding:
-                                       # Do not interupt the currently playing 
track.
-                                       if winUser.sendMessage(SPLWin, SPLMSG, 
0, SPL_TrackPlaybackStatus) == 0:
-                                               winUser.sendMessage(SPLWin, 
SPLMSG, 0, SPLPlay)
-                               if not encoding: encoding = True
-                       else:
-                               if alreadyEncoding: alreadyEncoding = False
-                               if encoding: encoding = False
-                               elif "Error" not in self.description and error: 
error = False
-                               toneCounter+=1
-                               if toneCounter%250 == 0 and self.connectionTone:
-                                       tones.beep(500, 50)
-                       if connecting: continue
-                       if not " ".join([self.encoderType, 
str(self.IAccessibleChildID)]) in SPLBackgroundMonitor: return
-
-       def script_connect(self, gesture):
-               gesture.send()
-               # Translators: Presented when an Encoder is trying to connect 
to a streaming server.
-               ui.message(_("Connecting..."))
-               # Oi, status thread, can you keep an eye on the connection 
status for me?
-               # To be packaged into a new function in 7.0.
-               if not self.backgroundMonitor:
-                       statusThread = 
threading.Thread(target=self.reportConnectionStatus, 
kwargs=dict(connecting=True))
-                       statusThread.name = "Connection Status Reporter " + 
str(self.IAccessibleChildID)
-                       statusThread.start()
-                       SAMMonitorThreads[self.IAccessibleChildID] = 
statusThread
-
-       def script_disconnect(self, gesture):
-               gesture.send()
-               # Translators: Presented when an Encoder is disconnecting from 
a streaming server.
-               ui.message(_("Disconnecting..."))
-
-       # Connecting/disconnecting all encoders at once.
-       # Control+F9/Control+F10 hotkeys are broken. Thankfully, context menu 
retains these commands.
-       # Use object navigation and key press emulation hack.
-
-       def _samContextMenu(self, pos):
-               def _samContextMenuActivate(pos):
-                       speech.cancelSpeech()
-                       focus =api.getFocusObject()
-                       focus.children[pos].doAction()
-               import keyboardHandler
-               contextMenu = 
keyboardHandler.KeyboardInputGesture.fromName("applications")
-               contextMenu.send()
-               wx.CallLater(100, _samContextMenuActivate, pos)
-               time.sleep(0.2)
-
-       def script_connectAll(self, gesture):
-               ui.message(_("Connecting..."))
-               speechMode = speech.speechMode
-               speech.speechMode = 0
-               wx.CallAfter(self._samContextMenu, 7)
-               # Oi, status thread, can you keep an eye on the connection 
status for me?
-               if not self.backgroundMonitor:
-                       statusThread = 
threading.Thread(target=self.reportConnectionStatus, 
kwargs=dict(connecting=True))
-                       statusThread.name = "Connection Status Reporter " + 
str(self.IAccessibleChildID)
-                       statusThread.start()
-                       SAMMonitorThreads[self.IAccessibleChildID] = 
statusThread
-               speech.speechMode = speechMode
-
-       def script_disconnectAll(self, gesture):
-               ui.message(_("Disconnecting..."))
-               speechMode = speech.speechMode
-               speech.speechMode = 0
-               wx.CallAfter(self._samContextMenu, 8)
-               time.sleep(0.5)
-               speech.speechMode = speechMode
-               speech.cancelSpeech()
-
-
-       # Announce SAM columns: encoder name/type, status and description.
-       def script_announceEncoderFormat(self, gesture):
-               typeIndex = self.description.find(", Status: ")
-               ui.message(self.description[:typeIndex])
-
-       def script_announceEncoderStatus(self, gesture):
-               typeIndex = self.description.find(", Status: ")
-               statusIndex = self.description.find(", Description: ")
-               ui.message(self.description[typeIndex+2:statusIndex])
-
-       def script_announceEncoderStatusDesc(self, gesture):
-               statusIndex = self.description.find(", Description: ")
-               ui.message(self.description[statusIndex+2:])
-
-
-       def setBackgroundMonitor(self):
-               self._set_Flags(self.getEncoderId(), self.backgroundMonitor, 
SPLBackgroundMonitor, "BackgroundMonitor")
-               return SAMMonitorThreads
-
-
-       def getStreamLabel(self, getTitle=False):
-               if str(self.IAccessibleChildID) in SAMStreamLabels:
-                       streamLabel = 
SAMStreamLabels[str(self.IAccessibleChildID)]
-                       return streamLabel, self.IAccessibleChildID if getTitle 
else streamLabel
-               return None, self.IAccessibleChildID if getTitle else None
-
-       def setStreamLabel(self, newStreamLabel):
-               if len(newStreamLabel):
-                       SAMStreamLabels[str(self.IAccessibleChildID)] = 
newStreamLabel
-               else:
-                       try:
-                               del 
SAMStreamLabels[str(self.IAccessibleChildID)]
-                       except KeyError:
-                               pass
-               streamLabels["SAMEncoders"] = SAMStreamLabels
-               streamLabels.write()
-
-       def removeStreamConfig(self, pos):
-               # An application of map successor algorithm.
-               global _encoderConfigRemoved
-               # Manipulate SAM encoder settings and labels.
-               _removeEncoderID("SAM", pos)
-               labelLength = len(SAMStreamLabels)
-               if not labelLength or pos > max(SAMStreamLabels.keys()):
-                       if _encoderConfigRemoved is not None:
-                               streamLabels.write()
-                               _encoderConfigRemoved = None
-                       return
-               elif labelLength  == 1:
-                       if not pos in SAMStreamLabels:
-                               pos = SAMStreamLabels.keys()[0]
-                               oldPosition = int(pos)
-                               SAMStreamLabels[str(oldPosition-1)] = 
SAMStreamLabels[pos]
-                       del SAMStreamLabels[pos]
-               else:
-                       encoderPositions = sorted(SAMStreamLabels.keys())
-                       # What if the position happens to be the last stream 
label position?
-                       if pos == max(encoderPositions): del 
SAMStreamLabels[pos]
-                       else:
-                               # Find the exact or closest successor.
-                               startPosition = 0
-                               if pos == min(encoderPositions):
-                                       del SAMStreamLabels[pos]
-                                       startPosition = 1
-                               elif pos > min(encoderPositions):
-                                       for candidate in encoderPositions:
-                                               if candidate >= pos:
-                                                       startPositionCandidate 
= encoderPositions.index(candidate)
-                                                       startPosition = 
startPositionCandidate+1 if candidate == pos else startPositionCandidate
-                                                       break
-                               # Now move them forward.
-                               for position in 
encoderPositions[startPosition:]:
-                                       oldPosition = int(position)
-                                       SAMStreamLabels[str(oldPosition-1)] = 
SAMStreamLabels[position]
-                                       del SAMStreamLabels[position]
-               streamLabels["SAMEncoders"] = SAMStreamLabels
-               streamLabels.write()
-
-
-       __gestures={
-               "kb:f9":"connect",
-               "kb:control+f9":"connectAll",
-               "kb:f10":"disconnect",
-               "kb:control+f10":"disconnectAll",
-               "kb:control+NVDA+3":"announceEncoderFormat",
-               "kb:control+NVDA+4":"announceEncoderStatus",
-               "kb:control+NVDA+5":"announceEncoderStatusDesc"
-       }
-
-
-class SPLEncoder(Encoder):
-       # Support for SPL Encoder window.
-
-       encoderType = "SPL"
-
-       def reportConnectionStatus(self, connecting=False):
-               # Same routine as SAM encoder: use a thread to prevent blocking 
NVDA commands.
-               SPLWin = user32.FindWindowA("SPLStudio", None)
-               attempt = 0
-               messageCache = ""
-               # Status flags.
-               connected = False
-               while True:
-                       time.sleep(0.001)
-                       try:
-                               # An inner try block is required because 
statChild may say the base class is gone.
-                               try:
-                                       statChild = self.children[1]
-                               except NotImplementedError:
-                                       return # Only seen when the encoder 
dies.
-                       except IndexError:
-                               return # Don't leave zombie objects around.
-                       if messageCache != statChild.name:
-                               messageCache = statChild.name
-                               if not messageCache: return
-                               if "Kbps" not in messageCache:
-                                       self.encoderStatusMessage(messageCache, 
self.IAccessibleChildID)
-                       if messageCache == "Disconnected":
-                               connected = False
-                               if connecting: continue
-                       elif messageCache == "Connected":
-                               connecting = False
-                               # We're on air, so exit.
-                               if not connected: tones.beep(1000, 150)
-                               if self.focusToStudio and not connected:
-                                       try:
-                                               
fetchSPLForegroundWindow().setFocus()
-                                       except AttributeError:
-                                               pass
-                               if self.playAfterConnecting and not connected:
-                                       if winUser.sendMessage(SPLWin, SPLMSG, 
0, SPL_TrackPlaybackStatus) == 0:
-                                               winUser.sendMessage(SPLWin, 
SPLMSG, 0, SPLPlay)
-                               if not connected: connected = True
-                       elif "Unable to connect" in messageCache or "Failed" in 
messageCache or statChild.name == "AutoConnect stopped.":
-                               if connected: connected = False
-                       else:
-                               if connected: connected = False
-                               if not "Kbps" in messageCache:
-                                       attempt += 1
-                                       if attempt%250 == 0 and 
self.connectionTone:
-                                               tones.beep(500, 50)
-                                               if attempt>= 500 and 
statChild.name == "Disconnected":
-                                                       tones.beep(250, 250)
-                               if connecting: continue
-                       if not " ".join([self.encoderType, 
str(self.IAccessibleChildID)]) in SPLBackgroundMonitor: return
-
-       def script_connect(self, gesture):
-               # Same as SAM's connection routine, but this time, keep an eye 
on self.name and a different connection flag.
-               connectButton = api.getForegroundObject().children[2]
-               if connectButton.name == "Disconnect": return
-               ui.message(_("Connecting..."))
-               # Juggle the focus around.
-               connectButton.doAction()
-               self.setFocus()
-               # Same as SAM encoders.
-               if not self.backgroundMonitor:
-                       statusThread = 
threading.Thread(target=self.reportConnectionStatus, 
kwargs=dict(connecting=True))
-                       statusThread.name = "Connection Status Reporter"
-                       statusThread.start()
-                       SPLMonitorThreads[self.IAccessibleChildID] = 
statusThread
-       script_connect.__doc__=_("Connects to a streaming server.")
-
-       # Announce SPL Encoder columns: encoder settings and transfer rate.
-       def script_announceEncoderSettings(self, gesture):
-               ui.message(_("Encoder Settings: {setting}").format(setting = 
self.children[0].name))
-
-       def script_announceEncoderTransfer(self, gesture):
-               ui.message(_("Transfer Rate: 
{transferRate}").format(transferRate = self.children[1].name))
-
-       def setBackgroundMonitor(self):
-               self._set_Flags(self.getEncoderId(), self.backgroundMonitor, 
SPLBackgroundMonitor, "BackgroundMonitor")
-               return SPLMonitorThreads
-
-       def getStreamLabel(self, getTitle=False):
-               if str(self.IAccessibleChildID) in SPLStreamLabels:
-                       streamLabel = 
SPLStreamLabels[str(self.IAccessibleChildID)]
-                       return streamLabel, self.firstChild.name if getTitle 
else streamLabel
-               return (None, self.firstChild.name) if getTitle else None
-
-       def setStreamLabel(self, newStreamLabel):
-               if len(newStreamLabel):
-                       SPLStreamLabels[str(self.IAccessibleChildID)] = 
newStreamLabel
-               else:
-                       try:
-                               del 
SPLStreamLabels[str(self.IAccessibleChildID)]
-                       except KeyError:
-                               pass
-               streamLabels["SPLEncoders"] = SPLStreamLabels
-               streamLabels.write()
-
-       def removeStreamConfig(self, pos):
-               global _encoderConfigRemoved
-               # This time, manipulate SPL ID entries.
-               _removeEncoderID("SPL", pos)
-               labelLength = len(SPLStreamLabels)
-               if not labelLength or pos > max(SPLStreamLabels.keys()):
-                       if _encoderConfigRemoved is not None:
-                               streamLabels.write()
-                               _encoderConfigRemoved = None
-                       return
-               elif labelLength  == 1:
-                       if not pos in SPLStreamLabels:
-                               pos = SPLStreamLabels.keys()[0]
-                               oldPosition = int(pos)
-                               SPLStreamLabels[str(oldPosition-1)] = 
SPLStreamLabels[pos]
-                       del SPLStreamLabels[pos]
-               else:
-                       encoderPositions = sorted(SPLStreamLabels.keys())
-                       if pos == max(encoderPositions): del 
SPLStreamLabels[pos]
-                       else:
-                               # Find the exact or closest successor.
-                               startPosition = 0
-                               if pos == min(encoderPositions):
-                                       del SPLStreamLabels[pos]
-                                       startPosition = 1
-                               elif pos > min(encoderPositions):
-                                       for candidate in encoderPositions:
-                                               if candidate >= pos:
-                                                       startPositionCandidate 
= encoderPositions.index(candidate)
-                                                       startPosition = 
startPositionCandidate+1 if candidate == pos else startPositionCandidate
-                                                       break
-                               # Now move them forward.
-                               for position in 
encoderPositions[startPosition:]:
-                                       oldPosition = int(position)
-                                       SPLStreamLabels[str(oldPosition-1)] = 
SPLStreamLabels[position]
-                                       del SPLStreamLabels[position]
-               streamLabels["SPLEncoders"] = SPLStreamLabels
-               streamLabels.write()
-
-
-       __gestures={
-               "kb:f9":"connect",
-               "kb:control+NVDA+3":"announceEncoderSettings",
-               "kb:control+NVDA+4":"announceEncoderTransfer"
-       }
-
-

This diff is so big that we needed to truncate the remainder.

https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/e187589de342/
Changeset:   e187589de342
Branch:      None
User:        josephsl
Date:        2016-12-30 01:31:30+00:00
Summary:     Playlist snapshots (17.1-dev): Document playlist snapshots command 
(F8 for now).

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index f076683..541f269 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -405,6 +405,7 @@ U: Studio up time.
 W: Weather and temperature.
 Y: Playlist modification.
 1 through 0: Announce columns via Columns Explorer (0 is tenth column slot).
+F8: Take playlist snapshots such as track count, longest track and so on.
 F9: Mark current track as start of track time analysis.
 F10: Perform track time analysis.
 F12: Switch to an instant switch profile.
@@ -437,6 +438,7 @@ U: Studio up time.
 W: Weather and temperature.
 Y: Playlist modification.
 1 through 0: Announce columns via Columns Explorer (0 is tenth column slot).
+F8: Take playlist snapshots such as track count, longest track and so on.
 F9: Mark current track as start of track time analysis.
 F10: Perform track time analysis.
 F12: Switch to an instant switch profile.
@@ -471,6 +473,7 @@ U: Studio up time.
 W: Weather and temperature.
 Y: Playlist modification.
 1 through 0: Announce columns via Columns Explorer (0 is tenth column slot).
+F8: Take playlist snapshots such as track count, longest track and so on.
 F9: Mark current track as start of track time analysis.
 F10: Perform track time analysis.
 F12: Switch to an instant switch profile.
@@ -1990,6 +1993,7 @@ class AppModule(appModuleHandler.AppModule):
                "kb:shift+s":"sayScheduledToPlay",
                "kb:shift+p":"sayTrackPitch",
                "kb:shift+r":"libraryScanMonitor",
+               "kb:f8":"takePlaylistSnapshots",
                "kb:f9":"markTrackForAnalysis",
                "kb:f10":"trackTimeAnalysis",
                "kb:f12":"switchProfiles",
@@ -2024,6 +2028,7 @@ class AppModule(appModuleHandler.AppModule):
                "kb:shift+s":"sayScheduledToPlay",
                "kb:shift+p":"sayTrackPitch",
                "kb:shift+r":"libraryScanMonitor",
+               "kb:f8":"takePlaylistSnapshots",
                "kb:f9":"markTrackForAnalysis",
                "kb:f10":"trackTimeAnalysis",
                "kb:f12":"switchProfiles",


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/f39a725ba771/
Changeset:   f39a725ba771
Branch:      None
User:        josephsl
Date:        2017-01-01 17:51:52+00:00
Summary:     Merge branch 'master' into plSnaps

Affected #:  3 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 541f269..0ce67b3 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -777,13 +777,14 @@ class AppModule(appModuleHandler.AppModule):
        def doExtraAction(self, status):
                # Be sure to only deal with cart mode changes if Cart Explorer 
is on.
                # Optimization: Return early if the below condition is true.
-               if self.cartExplorer and status.startswith("Cart"):
+               if self.cartExplorer and status.startswith("Cart") and 
status.endswith((" On", " Off")):
                        # 17.01: The best way to detect Cart Edit off is 
consulting file modification time.
                        # Automatically reload cart information if this is the 
case.
-                       studioTitle = api.getForegroundObject().name
-                       if splmisc.shouldCartExplorerRefresh(studioTitle):
-                               self.carts = 
splmisc.cartExplorerInit(studioTitle)
-                       # Translators: Presented when cart edit mode is toggled 
on while cart explorer is on.
+                       if status in ("Cart Edit Off", "Cart Insert On"):
+                               studioTitle = api.getForegroundObject().name
+                               if 
splmisc.shouldCartExplorerRefresh(studioTitle):
+                                       self.carts = 
splmisc.cartExplorerInit(studioTitle)
+                       # Translators: Presented when cart modes are toggled 
while cart explorer is on.
                        ui.message(_("Cart explorer is active"))
                        return
                # Microphone alarm and alarm interval if defined.

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index a5b448e..30072e9 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -872,17 +872,18 @@ class AudioDuckingReminder(wx.Dialog):
                mainSizer = wx.BoxSizer(wx.VERTICAL)
 
                # Translators: A message displayed if audio ducking should be 
disabled.
-               label = wx.StaticText(self, wx.ID_ANY, label=_("NVDA 2016.1 and 
later allows NVDA to decrease volume of background audio including that of 
Studio. In order to not disrupt the listening experience of your listeners, 
please disable audio ducking by opening synthesizer dialog in NVDA and 
selecting 'no ducking' from audio ducking mode combo box or press 
NVDA+Shift+D."))
+               label = wx.StaticText(self, wx.ID_ANY, label=_("""NVDA 2016.1 
and later allows NVDA to decrease volume of background audio including that of 
Studio.
+               In order to not disrupt the listening experience of your 
listeners, please disable audio ducking either by:
+               * Opening synthesizer dialog in NVDA and selecting 'no ducking' 
from audio ducking mode combo box.
+               * Press NVDA+Shift+D to set it to 'no ducking'."""))
                mainSizer.Add(label,border=20,flag=wx.LEFT|wx.RIGHT|wx.TOP)
 
-               sizer = wx.BoxSizer(wx.HORIZONTAL)
                # Translators: A checkbox to turn off audio ducking reminder 
message.
                
self.audioDuckingReminder=wx.CheckBox(self,wx.NewId(),label=_("Do not show this 
message again"))
                self.audioDuckingReminder.SetValue(not 
SPLConfig["Startup"]["AudioDuckingReminder"])
-               sizer.Add(self.audioDuckingReminder, border=10,flag=wx.TOP)
-               mainSizer.Add(sizer, border=10, flag=wx.BOTTOM)
+               mainSizer.Add(self.audioDuckingReminder, border=10,flag=wx.TOP)
 
-               mainSizer.Add(self.CreateButtonSizer(wx.OK))
+               mainSizer.Add(self.CreateButtonSizer(wx.OK), 
flag=wx.ALIGN_CENTER_HORIZONTAL)
                self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK)
                mainSizer.Fit(self)
                self.Sizer = mainSizer
@@ -928,14 +929,12 @@ Thank you.""")
                label = wx.StaticText(self, wx.ID_ANY, 
label=self.welcomeMessage)
                mainSizer.Add(label,border=20,flag=wx.LEFT|wx.RIGHT|wx.TOP)
 
-               sizer = wx.BoxSizer(wx.HORIZONTAL)
                # Translators: A checkbox to show welcome dialog.
                
self.showWelcomeDialog=wx.CheckBox(self,wx.NewId(),label=_("Show welcome dialog 
when I start Studio"))
                
self.showWelcomeDialog.SetValue(SPLConfig["Startup"]["WelcomeDialog"])
-               sizer.Add(self.showWelcomeDialog, border=10,flag=wx.TOP)
-               mainSizer.Add(sizer, border=10, flag=wx.BOTTOM)
+               mainSizer.Add(self.showWelcomeDialog, border=10,flag=wx.TOP)
 
-               mainSizer.Add(self.CreateButtonSizer(wx.OK))
+               mainSizer.Add(self.CreateButtonSizer(wx.OK), 
flag=wx.ALIGN_CENTER_HORIZONTAL)
                self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK)
                mainSizer.Fit(self)
                self.Sizer = mainSizer

diff --git a/readme.md b/readme.md
index abc6f63..0f0378f 100755
--- a/readme.md
+++ b/readme.md
@@ -181,6 +181,12 @@ If you are using Studio on a touchscreen computer running 
Windows 8 or later and
 * Initial support for StationPlaylist Creator.
 * Added a new command in SPL Controller layer to announce Studio status such 
as track playback and microphone status (Q).
 
+## Version 17.01/15.5-LTS
+
+* Improved responsiveness and reliability when using the add-on to switch to 
Studio, either using focus to Studio command from other programs or when an 
encoder is connected and NVDA is told to switch to Studio when this happens. If 
Studio is minimized, Studio window will be shown as unavailable. If so, restore 
Studio window from system tray.
+* If editing carts while Cart Explorer is active, it is no longer necessary to 
reenter Cart Explorer to view updated cart assignments when Cart Edit mode is 
turned off. Consequently, Cart Explorer reentry message is no longer announced.
+* In add-on 15.5-LTS, corrected user interface presentation for SPL add-on 
settings dialog.
+
 ## Version 16.12.1
 
 * Corrected user interface presentation for SPL add-on settings dialog.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/893780951c52/
Changeset:   893780951c52
Branch:      None
User:        josephsl
Date:        2017-01-04 19:30:33+00:00
Summary:     Correctly remove deprecated keys.

Logic error found: only the deprecated sections were checked instead of 
checking the 'real' thing (the Config Hub), which caused a bug where deprecated 
keys reminaed in the config file (now fixed).

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 30072e9..5fc24e8 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -112,7 +112,7 @@ class ConfigHub(ChainMap):
                # This action must be performed after caching, otherwise the 
newly modified profile will not be saved.
                deprecatedKeys = {"General":"TrackDial", "Startup":"Studio500"}
                for section, key in deprecatedKeys.iteritems():
-                       if key in section: del self.maps[0][section][key]
+                       if key in self.maps[0][section]: del 
self.maps[0][section][key]
                # Moving onto broadcast profiles if any.
                try:
                        profiles = filter(lambda fn: os.path.splitext(fn)[-1] 
== ".ini", os.listdir(SPLProfiles))


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/9969b2eb416e/
Changeset:   9969b2eb416e
Branch:      None
User:        josephsl
Date:        2017-01-04 20:05:55+00:00
Summary:     SPLDefaults7 is now SPLDefaults

Affected #:  4 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 7de90c2..dbab6bb 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -365,7 +365,7 @@ class SPL510TrackItem(SPLTrackItem):
 
        # Studio 5.10 version of original index finder.
        def _origIndexOf(self, columnHeader):
-               return 
splconfig._SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"].index(columnHeader)+1
+               return 
splconfig._SPLDefaults["ColumnAnnouncement"]["ColumnOrder"].index(columnHeader)+1
 
        # Handle track dial for SPL 5.10.
        def _leftmostcol(self):

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 5fc24e8..444d4ff 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -180,7 +180,7 @@ class ConfigHub(ChainMap):
                                # Case 1: restore settings to defaults when 5.x 
config validation has failed on all values.
                                # 6.0: In case this is a user profile, apply 
base configuration.
                                # 8.0: Call copy profile function directly to 
reduce overhead.
-                               copyProfile(_SPLDefaults7, SPLConfigCheckpoint, 
complete=SPLConfigCheckpoint.filename == SPLIni)
+                               copyProfile(_SPLDefaults, SPLConfigCheckpoint, 
complete=SPLConfigCheckpoint.filename == SPLIni)
                                _configLoadStatus[profileName] = "completeReset"
                        elif isinstance(configTest, dict):
                                # Case 2: For 5.x and later, attempt to 
reconstruct the failed values.
@@ -190,7 +190,7 @@ class ConfigHub(ChainMap):
                                        if isinstance(configTest[setting], 
dict):
                                                for failedKey in 
configTest[setting].keys():
                                                        # 7.0 optimization: 
just reload from defaults dictionary, as broadcast profiles contain 
profile-specific settings only.
-                                                       
SPLConfigCheckpoint[setting][failedKey] = _SPLDefaults7[setting][failedKey]
+                                                       
SPLConfigCheckpoint[setting][failedKey] = _SPLDefaults[setting][failedKey]
                                # 7.0: Disqualified from being cached this time.
                                SPLConfigCheckpoint.write()
                                _configLoadStatus[profileName] = "partialReset"
@@ -301,9 +301,9 @@ class ConfigHub(ChainMap):
                        profilePath = conf.filename
                        conf.reset()
                        conf.filename = profilePath
-                       resetConfig(_SPLDefaults7, conf)
+                       resetConfig(_SPLDefaults, conf)
                        # Convert certain settings to a different format.
-                       conf["ColumnAnnouncement"]["IncludedColumns"] = 
set(_SPLDefaults7["ColumnAnnouncement"]["IncludedColumns"])
+                       conf["ColumnAnnouncement"]["IncludedColumns"] = 
set(_SPLDefaults["ColumnAnnouncement"]["IncludedColumns"])
                # Switch back to normal profile via a custom variant of swap 
routine.
                if self.profiles[0].name != _("Normal profile"):
                        npIndex = self.profileIndexByName(_("Normal profile"))
@@ -364,9 +364,9 @@ class ConfigHub(ChainMap):
 
 # Default config spec container.
 # To be moved to a different place in 8.0.
-_SPLDefaults7 = ConfigObj(None, configspec = confspec7, encoding="UTF-8")
+_SPLDefaults = ConfigObj(None, configspec = confspec7, encoding="UTF-8")
 _val = Validator()
-_SPLDefaults7.validate(_val, copy=True)
+_SPLDefaults.validate(_val, copy=True)
 
 # Display an error dialog when configuration validation fails.
 def runConfigErrorDialog(errorText, errorType):
@@ -458,7 +458,7 @@ def _extraInitSteps(conf, profileName=None):
        global _configLoadStatus
        columnOrder = conf["ColumnAnnouncement"]["ColumnOrder"]
        # Catch suttle errors.
-       fields = _SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"]
+       fields = _SPLDefaults["ColumnAnnouncement"]["ColumnOrder"]
        invalidFields = 0
        for field in fields:
                if field not in columnOrder: invalidFields+=1
@@ -706,7 +706,7 @@ def _preSave(conf):
                for setting in conf.keys():
                        for key in conf[setting].keys():
                                try:
-                                       if conf[setting][key] == 
_SPLDefaults7[setting][key]:
+                                       if conf[setting][key] == 
_SPLDefaults[setting][key]:
                                                del conf[setting][key]
                                except KeyError:
                                        pass
@@ -851,7 +851,7 @@ def updateInit():
 # To be renamed and used in other places in 7.0.
 def _shouldBuildDescriptionPieces():
        return (not SPLConfig["ColumnAnnouncement"]["UseScreenColumnOrder"]
-       and (SPLConfig["ColumnAnnouncement"]["ColumnOrder"] != 
_SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"]
+       and (SPLConfig["ColumnAnnouncement"]["ColumnOrder"] != 
_SPLDefaults["ColumnAnnouncement"]["ColumnOrder"]
        or len(SPLConfig["ColumnAnnouncement"]["IncludedColumns"]) != 17))
 
 

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 743aab1..6bbd046 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -153,7 +153,7 @@ class SPLConfigDialog(gui.SettingsDialog):
                # Translators: The label for a setting in SPL add-on dialog to 
set vertical column.
                verticalColLabel = _("&Vertical column navigation 
announcement:")
                # Translators: One of the options for vertical column 
navigation denoting NVDA will announce current column positoin (e.g. second 
column position from the left).
-               self.verticalColumnsList = 
SPLConfigHelper.addLabeledControl(verticalColLabel, wx.Choice, 
choices=[_("whichever column I am reviewing"), "Status"] + 
splconfig._SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"])
+               self.verticalColumnsList = 
SPLConfigHelper.addLabeledControl(verticalColLabel, wx.Choice, 
choices=[_("whichever column I am reviewing"), "Status"] + 
splconfig._SPLDefaults["ColumnAnnouncement"]["ColumnOrder"])
                verticalColumn = 
splconfig.SPLConfig["General"]["VerticalColumnAnnounce"]
                selection = self.verticalColumnsList.FindString(verticalColumn) 
if verticalColumn is not None else 0
                try:
@@ -1087,7 +1087,7 @@ class ColumnsExplorerDialog(wx.Dialog):
                if not tt:
                        # Translators: The title of Columns Explorer 
configuration dialog.
                        actualTitle = _("Columns Explorer")
-                       cols = 
splconfig._SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"]
+                       cols = 
splconfig._SPLDefaults["ColumnAnnouncement"]["ColumnOrder"]
                else:
                        # Translators: The title of Columns Explorer 
configuration dialog.
                        actualTitle = _("Columns Explorer for Track Tool")

diff --git a/addon/appModules/splstudio/splmisc.py 
b/addon/appModules/splstudio/splmisc.py
index e3355bc..b4123b4 100755
--- a/addon/appModules/splstudio/splmisc.py
+++ b/addon/appModules/splstudio/splmisc.py
@@ -96,7 +96,7 @@ class SPLFindDialog(wx.Dialog):
                        columnSizer = wx.BoxSizer(wx.HORIZONTAL)
                        # Translators: The label in track finder to search 
columns.
                        label = wx.StaticText(self, wx.ID_ANY, label=_("C&olumn 
to search:"))
-                       self.columnHeaders = wx.Choice(self, wx.ID_ANY, 
choices=splconfig._SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"])
+                       self.columnHeaders = wx.Choice(self, wx.ID_ANY, 
choices=splconfig._SPLDefaults["ColumnAnnouncement"]["ColumnOrder"])
                        self.columnHeaders.SetSelection(0)
                        columnSizer.Add(label)
                        columnSizer.Add(self.columnHeaders)


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/8f7c87a4d3d0/
Changeset:   8f7c87a4d3d0
Branch:      None
User:        josephsl
Date:        2017-01-04 20:06:51+00:00
Summary:     Merge branch 'master' into plSnaps

Affected #:  4 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 0ce67b3..3725fa1 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -365,7 +365,7 @@ class SPL510TrackItem(SPLTrackItem):
 
        # Studio 5.10 version of original index finder.
        def _origIndexOf(self, columnHeader):
-               return 
splconfig._SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"].index(columnHeader)+1
+               return 
splconfig._SPLDefaults["ColumnAnnouncement"]["ColumnOrder"].index(columnHeader)+1
 
        # Handle track dial for SPL 5.10.
        def _leftmostcol(self):

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 30072e9..444d4ff 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -112,7 +112,7 @@ class ConfigHub(ChainMap):
                # This action must be performed after caching, otherwise the 
newly modified profile will not be saved.
                deprecatedKeys = {"General":"TrackDial", "Startup":"Studio500"}
                for section, key in deprecatedKeys.iteritems():
-                       if key in section: del self.maps[0][section][key]
+                       if key in self.maps[0][section]: del 
self.maps[0][section][key]
                # Moving onto broadcast profiles if any.
                try:
                        profiles = filter(lambda fn: os.path.splitext(fn)[-1] 
== ".ini", os.listdir(SPLProfiles))
@@ -180,7 +180,7 @@ class ConfigHub(ChainMap):
                                # Case 1: restore settings to defaults when 5.x 
config validation has failed on all values.
                                # 6.0: In case this is a user profile, apply 
base configuration.
                                # 8.0: Call copy profile function directly to 
reduce overhead.
-                               copyProfile(_SPLDefaults7, SPLConfigCheckpoint, 
complete=SPLConfigCheckpoint.filename == SPLIni)
+                               copyProfile(_SPLDefaults, SPLConfigCheckpoint, 
complete=SPLConfigCheckpoint.filename == SPLIni)
                                _configLoadStatus[profileName] = "completeReset"
                        elif isinstance(configTest, dict):
                                # Case 2: For 5.x and later, attempt to 
reconstruct the failed values.
@@ -190,7 +190,7 @@ class ConfigHub(ChainMap):
                                        if isinstance(configTest[setting], 
dict):
                                                for failedKey in 
configTest[setting].keys():
                                                        # 7.0 optimization: 
just reload from defaults dictionary, as broadcast profiles contain 
profile-specific settings only.
-                                                       
SPLConfigCheckpoint[setting][failedKey] = _SPLDefaults7[setting][failedKey]
+                                                       
SPLConfigCheckpoint[setting][failedKey] = _SPLDefaults[setting][failedKey]
                                # 7.0: Disqualified from being cached this time.
                                SPLConfigCheckpoint.write()
                                _configLoadStatus[profileName] = "partialReset"
@@ -301,9 +301,9 @@ class ConfigHub(ChainMap):
                        profilePath = conf.filename
                        conf.reset()
                        conf.filename = profilePath
-                       resetConfig(_SPLDefaults7, conf)
+                       resetConfig(_SPLDefaults, conf)
                        # Convert certain settings to a different format.
-                       conf["ColumnAnnouncement"]["IncludedColumns"] = 
set(_SPLDefaults7["ColumnAnnouncement"]["IncludedColumns"])
+                       conf["ColumnAnnouncement"]["IncludedColumns"] = 
set(_SPLDefaults["ColumnAnnouncement"]["IncludedColumns"])
                # Switch back to normal profile via a custom variant of swap 
routine.
                if self.profiles[0].name != _("Normal profile"):
                        npIndex = self.profileIndexByName(_("Normal profile"))
@@ -364,9 +364,9 @@ class ConfigHub(ChainMap):
 
 # Default config spec container.
 # To be moved to a different place in 8.0.
-_SPLDefaults7 = ConfigObj(None, configspec = confspec7, encoding="UTF-8")
+_SPLDefaults = ConfigObj(None, configspec = confspec7, encoding="UTF-8")
 _val = Validator()
-_SPLDefaults7.validate(_val, copy=True)
+_SPLDefaults.validate(_val, copy=True)
 
 # Display an error dialog when configuration validation fails.
 def runConfigErrorDialog(errorText, errorType):
@@ -458,7 +458,7 @@ def _extraInitSteps(conf, profileName=None):
        global _configLoadStatus
        columnOrder = conf["ColumnAnnouncement"]["ColumnOrder"]
        # Catch suttle errors.
-       fields = _SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"]
+       fields = _SPLDefaults["ColumnAnnouncement"]["ColumnOrder"]
        invalidFields = 0
        for field in fields:
                if field not in columnOrder: invalidFields+=1
@@ -706,7 +706,7 @@ def _preSave(conf):
                for setting in conf.keys():
                        for key in conf[setting].keys():
                                try:
-                                       if conf[setting][key] == 
_SPLDefaults7[setting][key]:
+                                       if conf[setting][key] == 
_SPLDefaults[setting][key]:
                                                del conf[setting][key]
                                except KeyError:
                                        pass
@@ -851,7 +851,7 @@ def updateInit():
 # To be renamed and used in other places in 7.0.
 def _shouldBuildDescriptionPieces():
        return (not SPLConfig["ColumnAnnouncement"]["UseScreenColumnOrder"]
-       and (SPLConfig["ColumnAnnouncement"]["ColumnOrder"] != 
_SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"]
+       and (SPLConfig["ColumnAnnouncement"]["ColumnOrder"] != 
_SPLDefaults["ColumnAnnouncement"]["ColumnOrder"]
        or len(SPLConfig["ColumnAnnouncement"]["IncludedColumns"]) != 17))
 
 

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 743aab1..6bbd046 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -153,7 +153,7 @@ class SPLConfigDialog(gui.SettingsDialog):
                # Translators: The label for a setting in SPL add-on dialog to 
set vertical column.
                verticalColLabel = _("&Vertical column navigation 
announcement:")
                # Translators: One of the options for vertical column 
navigation denoting NVDA will announce current column positoin (e.g. second 
column position from the left).
-               self.verticalColumnsList = 
SPLConfigHelper.addLabeledControl(verticalColLabel, wx.Choice, 
choices=[_("whichever column I am reviewing"), "Status"] + 
splconfig._SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"])
+               self.verticalColumnsList = 
SPLConfigHelper.addLabeledControl(verticalColLabel, wx.Choice, 
choices=[_("whichever column I am reviewing"), "Status"] + 
splconfig._SPLDefaults["ColumnAnnouncement"]["ColumnOrder"])
                verticalColumn = 
splconfig.SPLConfig["General"]["VerticalColumnAnnounce"]
                selection = self.verticalColumnsList.FindString(verticalColumn) 
if verticalColumn is not None else 0
                try:
@@ -1087,7 +1087,7 @@ class ColumnsExplorerDialog(wx.Dialog):
                if not tt:
                        # Translators: The title of Columns Explorer 
configuration dialog.
                        actualTitle = _("Columns Explorer")
-                       cols = 
splconfig._SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"]
+                       cols = 
splconfig._SPLDefaults["ColumnAnnouncement"]["ColumnOrder"]
                else:
                        # Translators: The title of Columns Explorer 
configuration dialog.
                        actualTitle = _("Columns Explorer for Track Tool")

diff --git a/addon/appModules/splstudio/splmisc.py 
b/addon/appModules/splstudio/splmisc.py
index e3355bc..b4123b4 100755
--- a/addon/appModules/splstudio/splmisc.py
+++ b/addon/appModules/splstudio/splmisc.py
@@ -96,7 +96,7 @@ class SPLFindDialog(wx.Dialog):
                        columnSizer = wx.BoxSizer(wx.HORIZONTAL)
                        # Translators: The label in track finder to search 
columns.
                        label = wx.StaticText(self, wx.ID_ANY, label=_("C&olumn 
to search:"))
-                       self.columnHeaders = wx.Choice(self, wx.ID_ANY, 
choices=splconfig._SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"])
+                       self.columnHeaders = wx.Choice(self, wx.ID_ANY, 
choices=splconfig._SPLDefaults["ColumnAnnouncement"]["ColumnOrder"])
                        self.columnHeaders.SetSelection(0)
                        columnSizer.Add(label)
                        columnSizer.Add(self.columnHeaders)


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/4c92a6ced82d/
Changeset:   4c92a6ced82d
Branch:      None
User:        josephsl
Date:        2017-01-04 21:33:00+00:00
Summary:     Playlist snapshots (17.1-dev): Add playlist snapshots config key 
to ConfigHub as a set.

For compact representation, it is best to add playlist snapshots flags as a set 
(similar to 'included columns' key), and now this set has been included as part 
of COnfigHub. In the app module, this set will be looked up as a last resort of 
no flags are specified. The next commit will be the user-visible config dialog.

Affected #:  2 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 3725fa1..3388ca6 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1422,11 +1422,12 @@ class AppModule(appModuleHandler.AppModule):
 
        # Playlist snapshots
        # Data to be gathered comes from a set of flags.
+       # By default, playlist duration (including shortest and average), 
category summary and other statistics will be gathered.
        def playlistSnapshots(self, obj, end, snapshotFlags=None):
                # Track count and total duration are always included.
                snapshot = {}
                if snapshotFlags is None:
-                       snapshotFlags = ["PlaylistDurationMinMax", 
"PlaylistCategoryCount", "PlaylistDurationAverage"]
+                       snapshotFlags = 
splconfig.SPLConfig["General"]["PlaylistSnapshots"]
                duration = obj.indexOf("Duration")
                title = obj.indexOf("Title")
                min, max = None, None
@@ -1490,7 +1491,7 @@ class AppModule(appModuleHandler.AppModule):
                if scriptCount == 0:
                        ui.message(", ".join(statusInfo))
                else:
-                       
ui.browseableMessage("<p>".join(statusInfo),title="Playlist snapshot", 
isHtml=True)
+                       
ui.browseableMessage("<p>".join(statusInfo),title="Playlist snapshots", 
isHtml=True)
 
        # Some handlers for native commands.
 

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 444d4ff..db90af8 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -46,6 +46,7 @@ TimeHourAnnounce = boolean(default=true)
 ExploreColumns = 
string_list(default=list("Artist","Title","Duration","Intro","Category","Filename","Year","Album","Genre","Time
 Scheduled"))
 ExploreColumnsTT = 
string_list(default=list("Artist","Title","Duration","Cue","Overlap","Intro","Segue","Filename","Album","CD
 Code"))
 VerticalColumnAnnounce = 
option(None,"Status","Artist","Title","Duration","Intro","Outro","Category","Year","Album","Genre","Mood","Energy","Tempo","BPM","Gender","Rating","Filename","Time
 Scheduled",default=None)
+PlaylistSnapshots = 
string_list(default=list("PlaylistDurationMinMax","PlaylistDurationAverage","PlaylistCategoryCount"))
 [IntroOutroAlarms]
 SayEndOfTrack = boolean(default=true)
 EndOfTrackTime = integer(min=1, max=59, default=5)
@@ -268,6 +269,8 @@ class ConfigHub(ChainMap):
                        # 6.1: Transform column inclusion data structure (for 
normal profile) now.
                        # 7.0: This will be repeated for broadcast profiles 
later.
                        # 8.0: Conversion will happen here, as conversion to 
list is necessary before writing it to disk (if told to do so).
+                       # 17.04: Playlist snapshots is a global key, so only 
normal profile will go through list conversion.
+                       
self.profiles[normalProfile]["General"]["PlaylistSnapshots"] = 
list(self.profiles[normalProfile]["General"]["PlaylistSnapshots"])
                        
self.profiles[normalProfile]["ColumnAnnouncement"]["IncludedColumns"] = 
list(self.profiles[normalProfile]["ColumnAnnouncement"]["IncludedColumns"])
                        self.profiles[normalProfile].write()
                del self.profiles[normalProfile]
@@ -482,6 +485,8 @@ def _extraInitSteps(conf, profileName=None):
        # 17.04: If vertical column announcement value is "None", transform 
this to NULL.
        if conf["General"]["VerticalColumnAnnounce"] == "None":
                conf["General"]["VerticalColumnAnnounce"] = None
+       # 17.04: Convert playlist snapshots list to a proper set.
+       conf["General"]["PlaylistSnapshots"] = 
set(conf["General"]["PlaylistSnapshots"])
 
 # Cache a copy of the loaded config.
 # This comes in handy when saving configuration to disk. For the most part, no 
change occurs to config.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/3dc1c5a9230b/
Changeset:   3dc1c5a9230b
Branch:      None
User:        josephsl
Date:        2017-01-06 17:55:22+00:00
Summary:     SPL Utils: Removed unneeded imports, microphone toggle via SPL 
Controller will no longer let sounds to be played.

In the past, when background event monitor wasn't there, microphone toggle 
command from SPL Controller will tell NVDA to play a toggle beep sound (same as 
name change handler in the app module). This is no longer necessary as long as 
Studio is not minimized.

Affected #:  2 files

diff --git a/addon/globalPlugins/splUtils/__init__.py 
b/addon/globalPlugins/splUtils/__init__.py
index cb63d5a..12856ab 100755
--- a/addon/globalPlugins/splUtils/__init__.py
+++ b/addon/globalPlugins/splUtils/__init__.py
@@ -5,14 +5,11 @@
 # For encoder support, see the encoders package.
 
 from functools import wraps
-import os
 import globalPluginHandler
 import api
 import ui
 import globalVars
-from NVDAObjects.IAccessible import getNVDAObjectFromEvent
 import winUser
-import nvwave
 import addonHandler
 addonHandler.initTranslation()
 
@@ -160,12 +157,10 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin):
 
        def script_micOn(self, gesture):
                winUser.sendMessage(SPLWin,SPLMSG,1,SPLMic)
-               nvwave.playWaveFile(os.path.join(os.path.dirname(__file__), 
"..", "..", "appModules", "splstudio", "SPL_on.wav"))
                self.finish()
 
        def script_micOff(self, gesture):
                winUser.sendMessage(SPLWin,SPLMSG,0,SPLMic)
-               nvwave.playWaveFile(os.path.join(os.path.dirname(__file__), 
"..", "..", "appModules", "splstudio", "SPL_off.wav"))
                self.finish()
 
        def script_micNoFade(self, gesture):

diff --git a/addon/globalPlugins/splUtils/encoders.py 
b/addon/globalPlugins/splUtils/encoders.py
index b25486a..9e3b876 100755
--- a/addon/globalPlugins/splUtils/encoders.py
+++ b/addon/globalPlugins/splUtils/encoders.py
@@ -9,7 +9,7 @@ import api
 import ui
 import speech
 import scriptHandler
-from NVDAObjects.IAccessible import IAccessible, getNVDAObjectFromEvent
+from NVDAObjects.IAccessible import IAccessible
 import winUser
 import tones
 import gui


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/a5ee2f28701a/
Changeset:   a5ee2f28701a
Branch:      None
User:        josephsl
Date:        2017-01-07 23:52:51+00:00
Summary:     Update check: removed unneeded keys.

Really old PSZ and PCH keys are no longer required in update checks, so don't 
check for it anymore. Also, why is dev branch set to get updates from 'stable' 
builds? (fixed)

Affected #:  2 files

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 444d4ff..fe7db3e 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -817,7 +817,6 @@ def triggerProfileSwitch():
                        _SPLTriggerEndTimer.Stop()
                        _SPLTriggerEndTimer = None
 
-
 # Automatic update checker.
 
 # The function below is called as part of the update check timer.
@@ -846,7 +845,6 @@ def updateInit():
        splupdate._SPLUpdateT = wx.PyTimer(autoUpdateCheck)
        splupdate._SPLUpdateT.Start(interval * 1000, True)
 
-
 # Let SPL track item know if it needs to build description pieces.
 # To be renamed and used in other places in 7.0.
 def _shouldBuildDescriptionPieces():
@@ -854,7 +852,6 @@ def _shouldBuildDescriptionPieces():
        and (SPLConfig["ColumnAnnouncement"]["ColumnOrder"] != 
_SPLDefaults["ColumnAnnouncement"]["ColumnOrder"]
        or len(SPLConfig["ColumnAnnouncement"]["IncludedColumns"]) != 17))
 
-
 # Additional configuration and miscellaneous dialogs
 # See splconfui module for basic configuration dialogs.
 

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 218d98a..3664986 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -46,10 +46,8 @@ def initialize():
        try:
                SPLAddonState = cPickle.load(file(_updatePickle, "r"))
                SPLAddonCheck = SPLAddonState["PDT"]
-               if "PSZ" in SPLAddonState: del SPLAddonState["PSZ"]
-               if "PCH" in SPLAddonState: del SPLAddonState["PCH"]
                _updateNow = "pendingChannelChange" in SPLAddonState
-               if "pendingChannelChange" in SPLAddonState: del 
SPLAddonState["pendingChannelChange"]
+               if _updateNow: del SPLAddonState["pendingChannelChange"]
                if "UpdateChannel" in SPLAddonState:
                        SPLUpdateChannel = SPLAddonState["UpdateChannel"]
                        if SPLUpdateChannel in ("beta", "lts"):
@@ -57,7 +55,7 @@ def initialize():
        except IOError, KeyError:
                SPLAddonState["PDT"] = 0
                _updateNow = False
-               SPLUpdateChannel = "stable"
+               SPLUpdateChannel = "dev"
 
 def terminate():
        global SPLAddonState
@@ -72,7 +70,6 @@ def terminate():
                cPickle.dump(SPLAddonState, file(_updatePickle, "wb"))
        SPLAddonState = None
 
-
 def _versionFromURL(url):
        # 7.3: Be sure to handle both GitHub and old URL format.
        filename = url.split("/")[-1]
@@ -155,4 +152,3 @@ def updateCheck(auto=False, continuous=False, 
confUpdateInterval=1):
 def getUpdateResponse(message, caption, updateURL):
        if gui.messageBox(message, caption, wx.YES | wx.NO | wx.CANCEL | 
wx.CENTER | wx.ICON_QUESTION) == wx.YES:
                os.startfile(updateURL)
-


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/29eea9fb85a5/
Changeset:   29eea9fb85a5
Branch:      None
User:        josephsl
Date:        2017-01-08 19:22:19+00:00
Summary:     Playlist snapshots (17.1-dev): Add a configuration dialog to 
configure playlsit snapshots info flags.

A new dialog (partially powered by code from Say Status dialog and Column 
Announcements dialog) has been added to configure playlist snapshot flags 
(using set operations if necessary). Because the dialog could be confusing at 
first, an explanatory text has been added.
This is destined for add-on 17.04.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 6bbd046..2c1a909 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -185,6 +185,11 @@ class SPLConfigDialog(gui.SettingsDialog):
                self.topBottomCheckbox = 
SPLConfigHelper.addItem(wx.CheckBox(self, label=_("Notify when located at &top 
or bottom of playlist viewer")))
                
self.topBottomCheckbox.SetValue(splconfig.SPLConfig["General"]["TopBottomAnnounce"])
 
+               self.playlistSnapshots = 
set(splconfig.SPLConfig["General"]["PlaylistSnapshots"])
+               # Translators: The label of a button to manage playlist 
snapshot flags.
+               playlistSnapshotFlagsButton = 
SPLConfigHelper.addItem(wx.Button(self, label=_("&Playlist snapshots...")))
+               playlistSnapshotFlagsButton.Bind(wx.EVT_BUTTON, 
self.onPlaylistSnapshotFlags)
+
                sizer = gui.guiHelper.BoxSizerHelper(self, 
orientation=wx.HORIZONTAL)
                self.metadataValues=[("off",_("Off")),
                # Translators: One of the metadata notification settings.
@@ -277,6 +282,7 @@ class SPLConfigDialog(gui.SettingsDialog):
                splconfig.SPLConfig["General"]["CategorySounds"] = 
self.categorySoundsCheckbox.Value
                splconfig.SPLConfig["General"]["TrackCommentAnnounce"] = 
self.trackCommentValues[self.trackCommentList.GetSelection()][0]
                splconfig.SPLConfig["General"]["TopBottomAnnounce"] = 
self.topBottomCheckbox.Value
+               splconfig.SPLConfig["General"]["PlaylistSnapshots"] = 
self.playlistSnapshots
                splconfig.SPLConfig["General"]["MetadataReminder"] = 
self.metadataValues[self.metadataList.GetSelection()][0]
                splconfig.SPLConfig["MetadataStreaming"]["MetadataEnabled"] = 
self.metadataStreams
                
splconfig.SPLConfig["ColumnAnnouncement"]["UseScreenColumnOrder"] = 
self.columnOrderCheckbox.Value
@@ -510,7 +516,12 @@ class SPLConfigDialog(gui.SettingsDialog):
                self.Disable()
                AlarmsCenter(self).Show()
 
-               # Manage metadata streaming.
+       # Configure playlist snapshot flags.
+       def onPlaylistSnapshotFlags(self, evt):
+               self.Disable()
+               PlaylistSnapshotsDialog(self).Show()
+
+       # Manage metadata streaming.
        def onManageMetadata(self, evt):
                self.Disable()
                MetadataStreamingDialog(self).Show()
@@ -875,6 +886,57 @@ class AlarmsCenter(wx.Dialog):
                global _alarmDialogOpened
                _alarmDialogOpened = False
 
+# Playlist snapshot flags
+# For things such as checkboxes for average duration and top category count.
+class PlaylistSnapshotsDialog(wx.Dialog):
+
+       def __init__(self, parent):
+               # Translators: Title of a dialog to configure playlist snapshot 
information.
+               super(PlaylistSnapshotsDialog, self).__init__(parent, 
title=_("Playlist snapshots"))
+
+               mainSizer = wx.BoxSizer(wx.VERTICAL)
+               playlistSnapshotsHelper = gui.guiHelper.BoxSizerHelper(self, 
orientation=wx.VERTICAL)
+
+               # Translators: Help text for playlist snapshots dialog.
+               labelText = _("""Select information to be included when 
obtaining playlist snapshots.
+               Track count and total duration are always included.""")
+               playlistSnapshotsHelper.addItem(wx.StaticText(self, 
label=labelText))
+
+               # Translators: the label for a setting in SPL add-on settings 
to include shortest and longest track duration in playlist snapshots window.
+               
self.playlistDurationMinMaxCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Shortest and longest tracks")))
+               
self.playlistDurationMinMaxCheckbox.SetValue("PlaylistDurationMinMax" in 
parent.playlistSnapshots)
+               # Translators: the label for a setting in SPL add-on settings 
to include average track duration in playlist snapshots window.
+               
self.playlistDurationAverageCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Average track duration")))
+               
self.playlistDurationAverageCheckbox.SetValue("PlaylistDurationAverage" in 
parent.playlistSnapshots)
+               # Translators: the label for a setting in SPL add-on settings 
to include track category count in playlist snapshots window.
+               
self.playlistCategoryCountCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Category count")))
+               
self.playlistCategoryCountCheckbox.SetValue("PlaylistCategoryCount" in 
parent.playlistSnapshots)
+
+               
playlistSnapshotsHelper.addDialogDismissButtons(self.CreateButtonSizer(wx.OK | 
wx.CANCEL))
+               self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK)
+               self.Bind(wx.EVT_BUTTON, self.onCancel, id=wx.ID_CANCEL)
+               mainSizer.Add(playlistSnapshotsHelper.sizer, 
border=gui.guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL)
+               mainSizer.Fit(self)
+               self.Sizer = mainSizer
+               self.playlistDurationMinMaxCheckbox.SetFocus()
+               self.Center(wx.BOTH | wx.CENTER_ON_SCREEN)
+
+       def onOk(self, evt):
+               playlistSnapshots = set()
+               if self.playlistDurationMinMaxCheckbox.Value: 
playlistSnapshots.add("PlaylistDurationMinMax")
+               if self.playlistDurationAverageCheckbox.Value: 
playlistSnapshots.add("PlaylistDurationAverage")
+               if self.playlistCategoryCountCheckbox.Value: 
playlistSnapshots.add("PlaylistCategoryCount")
+               parent = self.Parent
+               parent.playlistSnapshots = playlistSnapshots
+               parent.profiles.SetFocus()
+               parent.Enable()
+               self.Destroy()
+               return
+
+       def onCancel(self, evt):
+               self.Parent.Enable()
+               self.Destroy()
+
 # Metadata reminder controller.
 # Select notification/streaming URL's for metadata streaming.
 _metadataDialogOpened = False


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/125b3d7a372a/
Changeset:   125b3d7a372a
Branch:      None
User:        josephsl
Date:        2017-01-09 01:34:38+00:00
Summary:     Playlist snapshots (17.1-dev): Use a dedicated section for 
playlist snapshots variables, now booleans.

A conscious decision due to inconsistency: dialogs that are branches off from 
add-on settings sets keys that are in different section, so why was it that 
playlist snapshots did not follow this rule? Now corrected.
Also, for ease of readability and maintenance, playlist snapshots keys are now 
booleans 9or would be integers later). The caveat is that playlist snapshots 
function in the pap module must convert dictionaries to lists (via list 
comprehension), a necessary thing but acceptable for now.

Affected #:  3 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 3388ca6..27588e1 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1427,7 +1427,7 @@ class AppModule(appModuleHandler.AppModule):
                # Track count and total duration are always included.
                snapshot = {}
                if snapshotFlags is None:
-                       snapshotFlags = 
splconfig.SPLConfig["General"]["PlaylistSnapshots"]
+                       snapshotFlags = [flag for flag in 
splconfig.SPLConfig["PlaylistSnapshots"] if 
splconfig.SPLConfig["PlaylistSnapshots"][flag]]
                duration = obj.indexOf("Duration")
                title = obj.indexOf("Title")
                min, max = None, None

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index db90af8..e00aef4 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -46,7 +46,10 @@ TimeHourAnnounce = boolean(default=true)
 ExploreColumns = 
string_list(default=list("Artist","Title","Duration","Intro","Category","Filename","Year","Album","Genre","Time
 Scheduled"))
 ExploreColumnsTT = 
string_list(default=list("Artist","Title","Duration","Cue","Overlap","Intro","Segue","Filename","Album","CD
 Code"))
 VerticalColumnAnnounce = 
option(None,"Status","Artist","Title","Duration","Intro","Outro","Category","Year","Album","Genre","Mood","Energy","Tempo","BPM","Gender","Rating","Filename","Time
 Scheduled",default=None)
-PlaylistSnapshots = 
string_list(default=list("PlaylistDurationMinMax","PlaylistDurationAverage","PlaylistCategoryCount"))
+[PlaylistSnapshots]
+PlaylistDurationMinMax = boolean(default=true)
+PlaylistDurationAverage = boolean(default=true)
+PlaylistCategoryCount = boolean(default=true)
 [IntroOutroAlarms]
 SayEndOfTrack = boolean(default=true)
 EndOfTrackTime = integer(min=1, max=59, default=5)
@@ -114,6 +117,8 @@ class ConfigHub(ChainMap):
                deprecatedKeys = {"General":"TrackDial", "Startup":"Studio500"}
                for section, key in deprecatedKeys.iteritems():
                        if key in self.maps[0][section]: del 
self.maps[0][section][key]
+               # January 2017 only: playlist snapshots is now its own 
dedicated section.
+               if "PlaylistSnapshots" in self.maps[0]["General"]: del 
self.maps[0]["General"]["PlaylistSnapshots"]
                # Moving onto broadcast profiles if any.
                try:
                        profiles = filter(lambda fn: os.path.splitext(fn)[-1] 
== ".ini", os.listdir(SPLProfiles))
@@ -269,8 +274,6 @@ class ConfigHub(ChainMap):
                        # 6.1: Transform column inclusion data structure (for 
normal profile) now.
                        # 7.0: This will be repeated for broadcast profiles 
later.
                        # 8.0: Conversion will happen here, as conversion to 
list is necessary before writing it to disk (if told to do so).
-                       # 17.04: Playlist snapshots is a global key, so only 
normal profile will go through list conversion.
-                       
self.profiles[normalProfile]["General"]["PlaylistSnapshots"] = 
list(self.profiles[normalProfile]["General"]["PlaylistSnapshots"])
                        
self.profiles[normalProfile]["ColumnAnnouncement"]["IncludedColumns"] = 
list(self.profiles[normalProfile]["ColumnAnnouncement"]["IncludedColumns"])
                        self.profiles[normalProfile].write()
                del self.profiles[normalProfile]
@@ -485,8 +488,6 @@ def _extraInitSteps(conf, profileName=None):
        # 17.04: If vertical column announcement value is "None", transform 
this to NULL.
        if conf["General"]["VerticalColumnAnnounce"] == "None":
                conf["General"]["VerticalColumnAnnounce"] = None
-       # 17.04: Convert playlist snapshots list to a proper set.
-       conf["General"]["PlaylistSnapshots"] = 
set(conf["General"]["PlaylistSnapshots"])
 
 # Cache a copy of the loaded config.
 # This comes in handy when saving configuration to disk. For the most part, no 
change occurs to config.

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 2c1a909..5f7cbf6 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -185,7 +185,9 @@ class SPLConfigDialog(gui.SettingsDialog):
                self.topBottomCheckbox = 
SPLConfigHelper.addItem(wx.CheckBox(self, label=_("Notify when located at &top 
or bottom of playlist viewer")))
                
self.topBottomCheckbox.SetValue(splconfig.SPLConfig["General"]["TopBottomAnnounce"])
 
-               self.playlistSnapshots = 
set(splconfig.SPLConfig["General"]["PlaylistSnapshots"])
+               self.playlistDurationMinMax = 
splconfig.SPLConfig["PlaylistSnapshots"]["PlaylistDurationMinMax"]
+               self.playlistDurationAverage = 
splconfig.SPLConfig["PlaylistSnapshots"]["PlaylistDurationAverage"]
+               self.playlistCategoryCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["PlaylistCategoryCount"]
                # Translators: The label of a button to manage playlist 
snapshot flags.
                playlistSnapshotFlagsButton = 
SPLConfigHelper.addItem(wx.Button(self, label=_("&Playlist snapshots...")))
                playlistSnapshotFlagsButton.Bind(wx.EVT_BUTTON, 
self.onPlaylistSnapshotFlags)
@@ -282,7 +284,9 @@ class SPLConfigDialog(gui.SettingsDialog):
                splconfig.SPLConfig["General"]["CategorySounds"] = 
self.categorySoundsCheckbox.Value
                splconfig.SPLConfig["General"]["TrackCommentAnnounce"] = 
self.trackCommentValues[self.trackCommentList.GetSelection()][0]
                splconfig.SPLConfig["General"]["TopBottomAnnounce"] = 
self.topBottomCheckbox.Value
-               splconfig.SPLConfig["General"]["PlaylistSnapshots"] = 
self.playlistSnapshots
+               
splconfig.SPLConfig["PlaylistSnapshots"]["PlaylistDurationMinMax"] = 
self.playlistDurationMinMax
+               
splconfig.SPLConfig["PlaylistSnapshots"]["PlaylistDurationAverage"] = 
self.playlistDurationAverage
+               
splconfig.SPLConfig["PlaylistSnapshots"]["PlaylistCategoryCount"] = 
self.playlistCategoryCount
                splconfig.SPLConfig["General"]["MetadataReminder"] = 
self.metadataValues[self.metadataList.GetSelection()][0]
                splconfig.SPLConfig["MetadataStreaming"]["MetadataEnabled"] = 
self.metadataStreams
                
splconfig.SPLConfig["ColumnAnnouncement"]["UseScreenColumnOrder"] = 
self.columnOrderCheckbox.Value
@@ -904,13 +908,13 @@ class PlaylistSnapshotsDialog(wx.Dialog):
 
                # Translators: the label for a setting in SPL add-on settings 
to include shortest and longest track duration in playlist snapshots window.
                
self.playlistDurationMinMaxCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Shortest and longest tracks")))
-               
self.playlistDurationMinMaxCheckbox.SetValue("PlaylistDurationMinMax" in 
parent.playlistSnapshots)
+               
self.playlistDurationMinMaxCheckbox.SetValue(parent.playlistDurationMinMax)
                # Translators: the label for a setting in SPL add-on settings 
to include average track duration in playlist snapshots window.
                
self.playlistDurationAverageCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Average track duration")))
-               
self.playlistDurationAverageCheckbox.SetValue("PlaylistDurationAverage" in 
parent.playlistSnapshots)
+               
self.playlistDurationAverageCheckbox.SetValue(parent.playlistDurationAverage)
                # Translators: the label for a setting in SPL add-on settings 
to include track category count in playlist snapshots window.
                
self.playlistCategoryCountCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Category count")))
-               
self.playlistCategoryCountCheckbox.SetValue("PlaylistCategoryCount" in 
parent.playlistSnapshots)
+               
self.playlistCategoryCountCheckbox.SetValue(parent.playlistCategoryCount)
 
                
playlistSnapshotsHelper.addDialogDismissButtons(self.CreateButtonSizer(wx.OK | 
wx.CANCEL))
                self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK)
@@ -922,12 +926,10 @@ class PlaylistSnapshotsDialog(wx.Dialog):
                self.Center(wx.BOTH | wx.CENTER_ON_SCREEN)
 
        def onOk(self, evt):
-               playlistSnapshots = set()
-               if self.playlistDurationMinMaxCheckbox.Value: 
playlistSnapshots.add("PlaylistDurationMinMax")
-               if self.playlistDurationAverageCheckbox.Value: 
playlistSnapshots.add("PlaylistDurationAverage")
-               if self.playlistCategoryCountCheckbox.Value: 
playlistSnapshots.add("PlaylistCategoryCount")
                parent = self.Parent
-               parent.playlistSnapshots = playlistSnapshots
+               parent.playlistDurationMinMax = 
self.playlistDurationMinMaxCheckbox.Value
+               parent.playlistDurationAverage = 
self.playlistDurationAverageCheckbox.Value
+               parent.playlistCategoryCount = 
self.playlistCategoryCountCheckbox.Value
                parent.profiles.SetFocus()
                parent.Enable()
                self.Destroy()


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/02f04586e2f2/
Changeset:   02f04586e2f2
Branch:      None
User:        josephsl
Date:        2017-01-10 17:03:47+00:00
Summary:     Update check (17.1-dev): Use a regular expression to locate new 
version string.

A better solution than splitting strings: because the file name for the add-on 
package will stay the same for a long time, just use regular expressions to 
locate the new version string (stationplaylist-[version].nvda-addon). This 
improves update version check routine and is resistant to URL scheme changes.
This is destined for add-on 17.04.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 3664986..3191f1c 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -70,17 +70,14 @@ def terminate():
                cPickle.dump(SPLAddonState, file(_updatePickle, "wb"))
        SPLAddonState = None
 
-def _versionFromURL(url):
-       # 7.3: Be sure to handle both GitHub and old URL format.
-       filename = url.split("/")[-1]
-       return filename.split("stationPlaylist-")[1].split(".nvda-addon")[0]
-
 def updateQualify(url):
-       # The add-on version is of the form "major.minor". The "-dev" suffix 
indicates development release.
+       # The add-on version is of the form "x.y.z". The "-dev" suffix 
indicates development release.
        # Anything after "-dev" indicates a try or a custom build.
        # LTS: Support upgrading between LTS releases.
        # 7.0: Just worry about version label differences (suggested by Jamie 
Teh from NV Access).
-       version = _versionFromURL(url.url)
+       # 17.04: Version is of the form year.month.revision, and regular 
expression will be employed (looks cleaner).
+       import re
+       version = re.search("stationPlaylist-(?P<version>.*).nvda-addon", 
url.url).groupdict()["version"]
        return None if version == SPLAddonVersion else version
 
 _progressDialog = None


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/e7d43f26e442/
Changeset:   e7d43f26e442
Branch:      None
User:        josephsl
Date:        2017-01-10 18:32:42+00:00
Summary:     Merge branch 'master' into plSnaps

Affected #:  4 files

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index e00aef4..1ff7398 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -823,7 +823,6 @@ def triggerProfileSwitch():
                        _SPLTriggerEndTimer.Stop()
                        _SPLTriggerEndTimer = None
 
-
 # Automatic update checker.
 
 # The function below is called as part of the update check timer.
@@ -852,7 +851,6 @@ def updateInit():
        splupdate._SPLUpdateT = wx.PyTimer(autoUpdateCheck)
        splupdate._SPLUpdateT.Start(interval * 1000, True)
 
-
 # Let SPL track item know if it needs to build description pieces.
 # To be renamed and used in other places in 7.0.
 def _shouldBuildDescriptionPieces():
@@ -860,7 +858,6 @@ def _shouldBuildDescriptionPieces():
        and (SPLConfig["ColumnAnnouncement"]["ColumnOrder"] != 
_SPLDefaults["ColumnAnnouncement"]["ColumnOrder"]
        or len(SPLConfig["ColumnAnnouncement"]["IncludedColumns"]) != 17))
 
-
 # Additional configuration and miscellaneous dialogs
 # See splconfui module for basic configuration dialogs.
 

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 218d98a..3191f1c 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -46,10 +46,8 @@ def initialize():
        try:
                SPLAddonState = cPickle.load(file(_updatePickle, "r"))
                SPLAddonCheck = SPLAddonState["PDT"]
-               if "PSZ" in SPLAddonState: del SPLAddonState["PSZ"]
-               if "PCH" in SPLAddonState: del SPLAddonState["PCH"]
                _updateNow = "pendingChannelChange" in SPLAddonState
-               if "pendingChannelChange" in SPLAddonState: del 
SPLAddonState["pendingChannelChange"]
+               if _updateNow: del SPLAddonState["pendingChannelChange"]
                if "UpdateChannel" in SPLAddonState:
                        SPLUpdateChannel = SPLAddonState["UpdateChannel"]
                        if SPLUpdateChannel in ("beta", "lts"):
@@ -57,7 +55,7 @@ def initialize():
        except IOError, KeyError:
                SPLAddonState["PDT"] = 0
                _updateNow = False
-               SPLUpdateChannel = "stable"
+               SPLUpdateChannel = "dev"
 
 def terminate():
        global SPLAddonState
@@ -72,18 +70,14 @@ def terminate():
                cPickle.dump(SPLAddonState, file(_updatePickle, "wb"))
        SPLAddonState = None
 
-
-def _versionFromURL(url):
-       # 7.3: Be sure to handle both GitHub and old URL format.
-       filename = url.split("/")[-1]
-       return filename.split("stationPlaylist-")[1].split(".nvda-addon")[0]
-
 def updateQualify(url):
-       # The add-on version is of the form "major.minor". The "-dev" suffix 
indicates development release.
+       # The add-on version is of the form "x.y.z". The "-dev" suffix 
indicates development release.
        # Anything after "-dev" indicates a try or a custom build.
        # LTS: Support upgrading between LTS releases.
        # 7.0: Just worry about version label differences (suggested by Jamie 
Teh from NV Access).
-       version = _versionFromURL(url.url)
+       # 17.04: Version is of the form year.month.revision, and regular 
expression will be employed (looks cleaner).
+       import re
+       version = re.search("stationPlaylist-(?P<version>.*).nvda-addon", 
url.url).groupdict()["version"]
        return None if version == SPLAddonVersion else version
 
 _progressDialog = None
@@ -155,4 +149,3 @@ def updateCheck(auto=False, continuous=False, 
confUpdateInterval=1):
 def getUpdateResponse(message, caption, updateURL):
        if gui.messageBox(message, caption, wx.YES | wx.NO | wx.CANCEL | 
wx.CENTER | wx.ICON_QUESTION) == wx.YES:
                os.startfile(updateURL)
-

diff --git a/addon/globalPlugins/splUtils/__init__.py 
b/addon/globalPlugins/splUtils/__init__.py
index cb63d5a..12856ab 100755
--- a/addon/globalPlugins/splUtils/__init__.py
+++ b/addon/globalPlugins/splUtils/__init__.py
@@ -5,14 +5,11 @@
 # For encoder support, see the encoders package.
 
 from functools import wraps
-import os
 import globalPluginHandler
 import api
 import ui
 import globalVars
-from NVDAObjects.IAccessible import getNVDAObjectFromEvent
 import winUser
-import nvwave
 import addonHandler
 addonHandler.initTranslation()
 
@@ -160,12 +157,10 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin):
 
        def script_micOn(self, gesture):
                winUser.sendMessage(SPLWin,SPLMSG,1,SPLMic)
-               nvwave.playWaveFile(os.path.join(os.path.dirname(__file__), 
"..", "..", "appModules", "splstudio", "SPL_on.wav"))
                self.finish()
 
        def script_micOff(self, gesture):
                winUser.sendMessage(SPLWin,SPLMSG,0,SPLMic)
-               nvwave.playWaveFile(os.path.join(os.path.dirname(__file__), 
"..", "..", "appModules", "splstudio", "SPL_off.wav"))
                self.finish()
 
        def script_micNoFade(self, gesture):

diff --git a/addon/globalPlugins/splUtils/encoders.py 
b/addon/globalPlugins/splUtils/encoders.py
index b25486a..9e3b876 100755
--- a/addon/globalPlugins/splUtils/encoders.py
+++ b/addon/globalPlugins/splUtils/encoders.py
@@ -9,7 +9,7 @@ import api
 import ui
 import speech
 import scriptHandler
-from NVDAObjects.IAccessible import IAccessible, getNVDAObjectFromEvent
+from NVDAObjects.IAccessible import IAccessible
 import winUser
 import tones
 import gui


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/c665ea96a2eb/
Changeset:   c665ea96a2eb
Branch:      None
User:        josephsl
Date:        2017-01-10 19:36:52+00:00
Summary:     Playlist snapshots (17.1-dev): allow artist count to be included.

Artist count is now included (at least top artists will be displayed). The 
config option for this flag is next.

Affected #:  2 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 27588e1..7c79544 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1430,6 +1430,8 @@ class AppModule(appModuleHandler.AppModule):
                        snapshotFlags = [flag for flag in 
splconfig.SPLConfig["PlaylistSnapshots"] if 
splconfig.SPLConfig["PlaylistSnapshots"][flag]]
                duration = obj.indexOf("Duration")
                title = obj.indexOf("Title")
+               artist = obj.indexOf("Artist")
+               artists = []
                min, max = None, None
                minTitle, maxTitle = None, None
                category = obj.indexOf("Category")
@@ -1440,6 +1442,7 @@ class AppModule(appModuleHandler.AppModule):
                        segue = obj._getColumnContent(duration)
                        trackTitle = obj._getColumnContent(title)
                        categories.append(obj._getColumnContent(category))
+                       if categories[-1] != "Hour Marker": 
artists.append(obj._getColumnContent(artist))
                        # Shortest and longest tracks.
                        if min is None: min = segue
                        if segue and segue < min:
@@ -1460,9 +1463,10 @@ class AppModule(appModuleHandler.AppModule):
                        snapshot["PlaylistDurationMax"] = "%s (%s)"%(maxTitle, 
max)
                if "PlaylistDurationAverage" in snapshotFlags:
                        snapshot["PlaylistDurationAverage"] = 
self._ms2time(totalDuration/snapshot["PlaylistTrackCount"], ms=False)
-               if "PlaylistCategoryCount" in snapshotFlags:
+               if "PlaylistCategoryCount" in snapshotFlags or 
"PlaylistArtistCount" in snapshotFlags:
                        import collections
-                       snapshot["PlaylistCategoryCount"] = 
collections.Counter(categories)
+                       if "PlaylistCategoryCount" in snapshotFlags: 
snapshot["PlaylistCategoryCount"] = collections.Counter(categories)
+                       if "PlaylistArtistCount" in snapshotFlags: 
snapshot["PlaylistArtistCount"] = collections.Counter(artists)
                return snapshot
 
 # Output formatter for playlist snapshots.
@@ -1476,6 +1480,21 @@ class AppModule(appModuleHandler.AppModule):
                        statusInfo.append("Longest: 
%s"%snapshot["PlaylistDurationMax"])
                if "PlaylistDurationAverage" in snapshot:
                        statusInfo.append("Average: 
%s"%snapshot["PlaylistDurationAverage"])
+               if "PlaylistArtistCount" in snapshot:
+                       artists = snapshot["PlaylistArtistCount"].most_common()
+                       if scriptCount == 0:
+                               statusInfo.append("Top artist: %s 
(%s)"%(artists[0]))
+                       else:
+                               artistList = []
+                               for item in artists:
+                                       artist, count = item
+                                       try:
+                                               artist = artist.replace("<", "")
+                                               artist = artist.replace(">", "")
+                                               artistList.append("<li>%s 
(%s)</li>"%(artist, count))
+                                       except AttributeError:
+                                               artistList.append("<li> No 
artist information (%s)</li>"%(count))
+                               statusInfo.append("".join(["Top artists:<ol>", 
"\n".join(artistList), "</ol>"]))
                if "PlaylistCategoryCount" in snapshot:
                        categories = 
snapshot["PlaylistCategoryCount"].most_common()
                        if scriptCount == 0:

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 1ff7398..595c032 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -49,7 +49,10 @@ VerticalColumnAnnounce = 
option(None,"Status","Artist","Title","Duration","Intro
 [PlaylistSnapshots]
 PlaylistDurationMinMax = boolean(default=true)
 PlaylistDurationAverage = boolean(default=true)
+PlaylistArtistCount = boolean(default=true)
+ArtistCountLimit = integer(min=0, max=10, default=5)
 PlaylistCategoryCount = boolean(default=true)
+CategoryCountLimit = integer(min=0, max=10, default=5)
 [IntroOutroAlarms]
 SayEndOfTrack = boolean(default=true)
 EndOfTrackTime = integer(min=1, max=59, default=5)


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/99c3115b5296/
Changeset:   99c3115b5296
Branch:      None
User:        josephsl
Date:        2017-01-10 20:57:50+00:00
Summary:     Playlist snapshots (17.1-dev): Simplfied key names for flags.

Quite verbose to say ['Playlistsnapshtos']['Playlist*'] for key names, thus 
only say ['PlaylistSnapshots']['*'] as the section itself deals with playlist 
snapshots.

Affected #:  3 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 7c79544..555dbab 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1458,15 +1458,15 @@ class AppModule(appModuleHandler.AppModule):
                        obj = obj.next
                if end is None: snapshot["PlaylistTrackCount"] = statusAPI(0, 
124, ret=True)
                snapshot["PlaylistDurationTotal"] = 
self._ms2time(totalDuration, ms=False)
-               if "PlaylistDurationMinMax" in snapshotFlags:
+               if "DurationMinMax" in snapshotFlags:
                        snapshot["PlaylistDurationMin"] = "%s (%s)"%(minTitle, 
min)
                        snapshot["PlaylistDurationMax"] = "%s (%s)"%(maxTitle, 
max)
-               if "PlaylistDurationAverage" in snapshotFlags:
+               if "DurationAverage" in snapshotFlags:
                        snapshot["PlaylistDurationAverage"] = 
self._ms2time(totalDuration/snapshot["PlaylistTrackCount"], ms=False)
-               if "PlaylistCategoryCount" in snapshotFlags or 
"PlaylistArtistCount" in snapshotFlags:
+               if "CategoryCount" in snapshotFlags or "ArtistCount" in 
snapshotFlags:
                        import collections
-                       if "PlaylistCategoryCount" in snapshotFlags: 
snapshot["PlaylistCategoryCount"] = collections.Counter(categories)
-                       if "PlaylistArtistCount" in snapshotFlags: 
snapshot["PlaylistArtistCount"] = collections.Counter(artists)
+                       if "CategoryCount" in snapshotFlags: 
snapshot["PlaylistCategoryCount"] = collections.Counter(categories)
+                       if "ArtistCount" in snapshotFlags: 
snapshot["PlaylistArtistCount"] = collections.Counter(artists)
                return snapshot
 
 # Output formatter for playlist snapshots.

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 595c032..05c18a1 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -47,11 +47,11 @@ ExploreColumns = 
string_list(default=list("Artist","Title","Duration","Intro","C
 ExploreColumnsTT = 
string_list(default=list("Artist","Title","Duration","Cue","Overlap","Intro","Segue","Filename","Album","CD
 Code"))
 VerticalColumnAnnounce = 
option(None,"Status","Artist","Title","Duration","Intro","Outro","Category","Year","Album","Genre","Mood","Energy","Tempo","BPM","Gender","Rating","Filename","Time
 Scheduled",default=None)
 [PlaylistSnapshots]
-PlaylistDurationMinMax = boolean(default=true)
-PlaylistDurationAverage = boolean(default=true)
-PlaylistArtistCount = boolean(default=true)
+DurationMinMax = boolean(default=true)
+DurationAverage = boolean(default=true)
+ArtistCount = boolean(default=true)
 ArtistCountLimit = integer(min=0, max=10, default=5)
-PlaylistCategoryCount = boolean(default=true)
+CategoryCount = boolean(default=true)
 CategoryCountLimit = integer(min=0, max=10, default=5)
 [IntroOutroAlarms]
 SayEndOfTrack = boolean(default=true)
@@ -122,6 +122,10 @@ class ConfigHub(ChainMap):
                        if key in self.maps[0][section]: del 
self.maps[0][section][key]
                # January 2017 only: playlist snapshots is now its own 
dedicated section.
                if "PlaylistSnapshots" in self.maps[0]["General"]: del 
self.maps[0]["General"]["PlaylistSnapshots"]
+               for key in ("PlaylistDurationMinMax", 
"PlaylistDurationAverage", "PlaylistCategoryCount"):
+                       if key in self.maps[0]["PlaylistSnapshots"]:
+                               self.maps[0]["PlaylistSnapshots"][key[8:]] = 
self.maps[0]["PlaylistSnapshots"][key]
+                               del self.maps[0]["PlaylistSnapshots"][key]
                # Moving onto broadcast profiles if any.
                try:
                        profiles = filter(lambda fn: os.path.splitext(fn)[-1] 
== ".ini", os.listdir(SPLProfiles))

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 5f7cbf6..0981885 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -185,9 +185,9 @@ class SPLConfigDialog(gui.SettingsDialog):
                self.topBottomCheckbox = 
SPLConfigHelper.addItem(wx.CheckBox(self, label=_("Notify when located at &top 
or bottom of playlist viewer")))
                
self.topBottomCheckbox.SetValue(splconfig.SPLConfig["General"]["TopBottomAnnounce"])
 
-               self.playlistDurationMinMax = 
splconfig.SPLConfig["PlaylistSnapshots"]["PlaylistDurationMinMax"]
-               self.playlistDurationAverage = 
splconfig.SPLConfig["PlaylistSnapshots"]["PlaylistDurationAverage"]
-               self.playlistCategoryCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["PlaylistCategoryCount"]
+               self.playlistDurationMinMax = 
splconfig.SPLConfig["PlaylistSnapshots"]["DurationMinMax"]
+               self.playlistDurationAverage = 
splconfig.SPLConfig["PlaylistSnapshots"]["DurationAverage"]
+               self.playlistCategoryCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCount"]
                # Translators: The label of a button to manage playlist 
snapshot flags.
                playlistSnapshotFlagsButton = 
SPLConfigHelper.addItem(wx.Button(self, label=_("&Playlist snapshots...")))
                playlistSnapshotFlagsButton.Bind(wx.EVT_BUTTON, 
self.onPlaylistSnapshotFlags)
@@ -284,9 +284,9 @@ class SPLConfigDialog(gui.SettingsDialog):
                splconfig.SPLConfig["General"]["CategorySounds"] = 
self.categorySoundsCheckbox.Value
                splconfig.SPLConfig["General"]["TrackCommentAnnounce"] = 
self.trackCommentValues[self.trackCommentList.GetSelection()][0]
                splconfig.SPLConfig["General"]["TopBottomAnnounce"] = 
self.topBottomCheckbox.Value
-               
splconfig.SPLConfig["PlaylistSnapshots"]["PlaylistDurationMinMax"] = 
self.playlistDurationMinMax
-               
splconfig.SPLConfig["PlaylistSnapshots"]["PlaylistDurationAverage"] = 
self.playlistDurationAverage
-               
splconfig.SPLConfig["PlaylistSnapshots"]["PlaylistCategoryCount"] = 
self.playlistCategoryCount
+               splconfig.SPLConfig["PlaylistSnapshots"]["DurationMinMax"] = 
self.playlistDurationMinMax
+               splconfig.SPLConfig["PlaylistSnapshots"]["DurationAverage"] = 
self.playlistDurationAverage
+               splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCount"] = 
self.playlistCategoryCount
                splconfig.SPLConfig["General"]["MetadataReminder"] = 
self.metadataValues[self.metadataList.GetSelection()][0]
                splconfig.SPLConfig["MetadataStreaming"]["MetadataEnabled"] = 
self.metadataStreams
                
splconfig.SPLConfig["ColumnAnnouncement"]["UseScreenColumnOrder"] = 
self.columnOrderCheckbox.Value


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/c3a719c1c375/
Changeset:   c3a719c1c375
Branch:      None
User:        josephsl
Date:        2017-01-10 21:23:29+00:00
Summary:     Removed legacy variable names for confspec and friends.

No longer called 'confspec7' or 'mutatablesettings7' - drop '7' from these 
names, as the 7.x style config is in effect.

Affected #:  2 files

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index fe7db3e..ec84cdf 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -31,7 +31,7 @@ except ImportError:
 SPLIni = os.path.join(globalVars.appArgs.configPath, "splstudio.ini")
 SPLProfiles = os.path.join(globalVars.appArgs.configPath, "addons", 
"stationPlaylist", "profiles")
 # New (7.0) style config.
-confspec7 = ConfigObj(StringIO("""
+confspec = ConfigObj(StringIO("""
 [General]
 BeepAnnounce = boolean(default=false)
 MessageVerbosity = option("beginner", "advanced", default="beginner")
@@ -76,13 +76,13 @@ UpdateInterval = integer(min=1, max=30, default=7)
 AudioDuckingReminder = boolean(default=true)
 WelcomeDialog = boolean(default=true)
 """), encoding="UTF-8", list_values=False)
-confspec7.newlines = "\r\n"
+confspec.newlines = "\r\n"
 SPLConfig = None
 # The following settings can be changed in profiles:
-_mutatableSettings7=("IntroOutroAlarms", "MicrophoneAlarm", 
"MetadataStreaming", "ColumnAnnouncement")
+_mutatableSettings=("IntroOutroAlarms", "MicrophoneAlarm", 
"MetadataStreaming", "ColumnAnnouncement")
 # 7.0: Profile-specific confspec (might be removed once a more optimal way to 
validate sections is found).
 # Dictionary comprehension is better here.
-confspecprofiles = {sect:key for sect, key in confspec7.iteritems() if sect in 
_mutatableSettings7}
+confspecprofiles = {sect:key for sect, key in confspec.iteritems() if sect in 
_mutatableSettings}
 
 # 8.0: Run-time config storage and management will use ConfigHub data 
structure, a subclass of chain map.
 # A chain map allows a dictionary to look up predefined mappings to locate a 
key.
@@ -152,10 +152,10 @@ class ConfigHub(ChainMap):
                # 7.0: What if profiles have parsing errors?
                # If so, reset everything back to factory defaults.
                try:
-                       SPLConfigCheckpoint = ConfigObj(path, configspec = 
confspec7 if prefill else confspecprofiles, encoding="UTF-8")
+                       SPLConfigCheckpoint = ConfigObj(path, configspec = 
confspec if prefill else confspecprofiles, encoding="UTF-8")
                except:
                        open(path, "w").close()
-                       SPLConfigCheckpoint = ConfigObj(path, configspec = 
confspec7 if prefill else confspecprofiles, encoding="UTF-8")
+                       SPLConfigCheckpoint = ConfigObj(path, configspec = 
confspec if prefill else confspecprofiles, encoding="UTF-8")
                        _configLoadStatus[profileName] = "fileReset"
                # 5.2 and later: check to make sure all values are correct.
                # 7.0: Make sure errors are displayed as config keys are now 
sections and may need to go through subkeys.
@@ -249,7 +249,7 @@ class ConfigHub(ChainMap):
 
        def __delitem__(self, key):
                # Consult profile-specific key first before deleting anything.
-               pos = 0 if key in _mutatableSettings7 else [profile.name for 
profile in self.maps].index(_("Normal Profile"))
+               pos = 0 if key in _mutatableSettings else [profile.name for 
profile in self.maps].index(_("Normal Profile"))
                try:
                        del self.maps[pos][key]
                except KeyError:
@@ -364,7 +364,7 @@ class ConfigHub(ChainMap):
 
 # Default config spec container.
 # To be moved to a different place in 8.0.
-_SPLDefaults = ConfigObj(None, configspec = confspec7, encoding="UTF-8")
+_SPLDefaults = ConfigObj(None, configspec = confspec, encoding="UTF-8")
 _val = Validator()
 _SPLDefaults.validate(_val, copy=True)
 
@@ -659,7 +659,7 @@ def getProfileByName(name):
 # Setting complete flag controls whether profile-specific settings are applied 
(true otherwise, only set when resetting profiles).
 # 8.0: Simplified thanks to in-place swapping.
 def copyProfile(sourceProfile, targetProfile, complete=False):
-       for section in sourceProfile.keys() if complete else 
_mutatableSettings7:
+       for section in sourceProfile.keys() if complete else _mutatableSettings:
                targetProfile[section] = dict(sourceProfile[section])
 
 # Last but not least...

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 6bbd046..d513340 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -610,7 +610,7 @@ class NewProfileDialog(wx.Dialog):
                # LTS optimization: just build base profile dictionary here if 
copying a profile.
                if self.copy:
                        baseConfig = 
splconfig.getProfileByName(self.baseProfiles.GetStringSelection())
-                       baseProfile = {sect:key for sect, key in 
baseConfig.iteritems() if sect in splconfig._mutatableSettings7}
+                       baseProfile = {sect:key for sect, key in 
baseConfig.iteritems() if sect in splconfig._mutatableSettings}
                else: baseProfile = None
                splconfig.SPLConfig.createProfile(newProfilePath, 
profileName=name, parent=baseProfile)
                parent.profileNames.append(name)


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/9d3d3ef088bb/
Changeset:   9d3d3ef088bb
Branch:      None
User:        josephsl
Date:        2017-01-10 21:35:25+00:00
Summary:     Readme: mention vertical column navigation commands.

Affected #:  1 file

diff --git a/readme.md b/readme.md
index 0f0378f..29f22f3 100755
--- a/readme.md
+++ b/readme.md
@@ -24,6 +24,7 @@ IMPORTANT: This add-on requires NVDA 2016.4 or later and 
StationPlaylist Studio
 * Alt+NVDA+R from Studio window: Steps through library scan announcement 
settings.
 * Control+Shift+X from Studio window: Steps through braille timer settings.
 * Control+Alt+right/left arrow (while focused on a track): Announce 
next/previous track column.
+* Control+Alt+down/up arrow (while focused on a track): Move to next or 
previous track and announce specific columns (unavailable in add-on 15.x).
 * Control+NVDA+1 through 0 (6 for Studio 5.0x): Announce column content for a 
specified column.
 * Alt+NVDA+C while focused on a track: announces track comments if any.
 * Alt+NVDA+0 from Studio window: Opens the Studio add-on configuration dialog.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/18b1dcd9d1ce/
Changeset:   18b1dcd9d1ce
Branch:      None
User:        josephsl
Date:        2017-01-10 21:47:08+00:00
Summary:     Status info (17.1-dev): Check if Studio is running before invoking 
this command.

SPL Controller, Q: if a custom gesture has been defined for this command, the 
script will assume that Studio is running (but it might not be running at all). 
In case Studio isn't running, catch this and report it (same routine as other 
SP lUtils commands where custom gestures can be defined).

Affected #:  1 file

diff --git a/addon/globalPlugins/splUtils/__init__.py 
b/addon/globalPlugins/splUtils/__init__.py
index 12856ab..c527bae 100755
--- a/addon/globalPlugins/splUtils/__init__.py
+++ b/addon/globalPlugins/splUtils/__init__.py
@@ -239,6 +239,11 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin):
                self.finish()
 
        def script_statusInfo(self, gesture):
+               SPLWin = user32.FindWindowA("SPLStudio", None) # Used ANSI 
version, as Wide char version always returns 0.
+               if not SPLWin:
+                       ui.message(_("SPL Studio is not running."))
+                       self.finish()
+                       return
                # For consistency reasons (because of the Studio status bar), 
messages in this method will remain in English.
                statusInfo = []
                # 17.04: For Studio 5.10 and up, announce playback and 
automation status.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/0d3714f516b1/
Changeset:   0d3714f516b1
Branch:      None
User:        josephsl
Date:        2017-01-12 05:00:38+00:00
Summary:     Code cleanup: Renamed 'statusAPI' to 'studioAPI' to better reflect 
its purpose, use hardcoded constant for user32.WM_USER.

In previous versions of the add-on, statusAPI did what it is supposed to do - 
obtain status information. However, with the advent of metadata streaming and 
other features, it became clear that this function needed to be renamed to 
something else. Because this function is a thin wrapper around 
user32.SendMessage with the handle to Studio and message type filled in, and 
because this function is now used to set some options, it is more appropriate 
to call it 'studioAPI'.
Also, for global plugin, WM_USER will be hardcoded (1024 or 0x400) for 
consistency reasons (other places uses the constant directly, while the global 
plugin used variable names, causing consistency headahce).
These changes are destined for add-on 17.04 and later.

Affected #:  3 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index dbab6bb..a7e31a3 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -90,10 +90,10 @@ def micAlarmManager(micAlarmWav, micAlarmMessage):
                micAlarmT2 = wx.PyTimer(_micAlarmAnnouncer)
                
micAlarmT2.Start(splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarmInterval"] * 
1000)
 
-# Call SPL API to obtain needed values.
+# Use SPL Studio API to obtain needed values.
 # A thin wrapper around user32.SendMessage and calling a callback if defined.
 # Offset is used in some time commands.
-def statusAPI(arg, command, func=None, ret=False, offset=None):
+def studioAPI(arg, command, func=None, ret=False, offset=None):
        if _SPLWin is None: return
        val = sendMessage(_SPLWin, 1024, arg, command)
        if ret:
@@ -103,8 +103,8 @@ def statusAPI(arg, command, func=None, ret=False, 
offset=None):
 
 # Select a track upon request.
 def selectTrack(trackIndex):
-       statusAPI(-1, 121)
-       statusAPI(trackIndex, 121)
+       studioAPI(-1, 121)
+       studioAPI(trackIndex, 121)
 
 # Category sounds dictionary (key = category, value = tone pitch).
 _SPLCategoryTones = {
@@ -922,12 +922,12 @@ class AppModule(appModuleHandler.AppModule):
 
        # Scripts which rely on API.
        def script_sayRemainingTime(self, gesture):
-               statusAPI(3, 105, self.announceTime, offset=1)
+               studioAPI(3, 105, self.announceTime, offset=1)
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
        script_sayRemainingTime.__doc__=_("Announces the remaining track time.")
 
        def script_sayElapsedTime(self, gesture):
-               statusAPI(0, 105, self.announceTime)
+               studioAPI(0, 105, self.announceTime)
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
        script_sayElapsedTime.__doc__=_("Announces the elapsed time for the 
currently playing track.")
 
@@ -1144,7 +1144,7 @@ class AppModule(appModuleHandler.AppModule):
        def script_timeRangeFinder(self, gesture):
                if self._trackFinderCheck(2):
                        try:
-                               d = splmisc.SPLTimeRangeDialog(gui.mainFrame, 
api.getFocusObject(), statusAPI)
+                               d = splmisc.SPLTimeRangeDialog(gui.mainFrame, 
api.getFocusObject(), studioAPI)
                                gui.mainFrame.prePopup()
                                d.Raise()
                                d.Show()
@@ -1263,7 +1263,7 @@ class AppModule(appModuleHandler.AppModule):
                global libScanT
                if libScanT and libScanT.isAlive() and 
api.getForegroundObject().windowClassName == "TTrackInsertForm":
                        return
-               if statusAPI(1, 32, ret=True) < 0:
+               if studioAPI(1, 32, ret=True) < 0:
                        self.libraryScanning = False
                        return
                time.sleep(0.1)
@@ -1271,10 +1271,10 @@ class AppModule(appModuleHandler.AppModule):
                        self.libraryScanning = False
                        return
                # 17.04: Library scan may have finished while this thread was 
sleeping.
-               if statusAPI(1, 32, ret=True) < 0:
+               if studioAPI(1, 32, ret=True) < 0:
                        self.libraryScanning = False
                        # Translators: Presented when library scanning is 
finished.
-                       ui.message(_("{itemCount} items in the 
library").format(itemCount = statusAPI(0, 32, ret=True)))
+                       ui.message(_("{itemCount} items in the 
library").format(itemCount = studioAPI(0, 32, ret=True)))
                else:
                        libScanT = 
threading.Thread(target=self.libraryScanReporter)
                        libScanT.daemon = True
@@ -1283,7 +1283,7 @@ class AppModule(appModuleHandler.AppModule):
        def libraryScanReporter(self):
                scanIter = 0
                # 17.04: Use the constant directly, as 5.10 and later provides 
a convenient method to detect completion of library scans.
-               scanCount = statusAPI(1, 32, ret=True)
+               scanCount = studioAPI(1, 32, ret=True)
                while scanCount >= 0:
                        if not self.libraryScanning: return
                        time.sleep(1)
@@ -1291,7 +1291,7 @@ class AppModule(appModuleHandler.AppModule):
                        if api.getForegroundObject().windowClassName == 
"TTrackInsertForm" or not self.libraryScanning:
                                return
                        # Scan count may have changed during sleep.
-                       scanCount = statusAPI(1, 32, ret=True)
+                       scanCount = studioAPI(1, 32, ret=True)
                        if scanCount < 0:
                                break
                        scanIter+=1
@@ -1304,7 +1304,7 @@ class AppModule(appModuleHandler.AppModule):
                                tones.beep(370, 100)
                        else:
                                # Translators: Presented after library scan is 
done.
-                               ui.message(_("Scan complete with {itemCount} 
items").format(itemCount = statusAPI(0, 32, ret=True)))
+                               ui.message(_("Scan complete with {itemCount} 
items").format(itemCount = studioAPI(0, 32, ret=True)))
 
        # Take care of library scanning announcement.
        def _libraryScanAnnouncer(self, count, announcementType):
@@ -1358,7 +1358,7 @@ class AppModule(appModuleHandler.AppModule):
                        return
                try:
                        # Passing in the function object is enough to change 
the dialog UI.
-                       d = splconfui.MetadataStreamingDialog(gui.mainFrame, 
func=statusAPI)
+                       d = splconfui.MetadataStreamingDialog(gui.mainFrame, 
func=studioAPI)
                        gui.mainFrame.prePopup()
                        d.Raise()
                        d.Show()
@@ -1387,17 +1387,17 @@ class AppModule(appModuleHandler.AppModule):
        # This is also called from playlist duration scripts.
        def totalTime(self, start, end):
                # Take care of errors such as the following.
-               if start < 0 or end > statusAPI(0, 124, ret=True)-1:
+               if start < 0 or end > studioAPI(0, 124, ret=True)-1:
                        raise ValueError("Track range start or end position out 
of range")
                        return
                totalLength = 0
                if start == end:
-                       filename = statusAPI(start, 211, ret=True)
-                       totalLength = statusAPI(filename, 30, ret=True)
+                       filename = studioAPI(start, 211, ret=True)
+                       totalLength = studioAPI(filename, 30, ret=True)
                else:
                        for track in xrange(start, end+1):
-                               filename = statusAPI(track, 211, ret=True)
-                               totalLength+=statusAPI(filename, 30, ret=True)
+                               filename = studioAPI(track, 211, ret=True)
+                               totalLength+=studioAPI(filename, 30, ret=True)
                return totalLength
 
        # Some handlers for native commands.
@@ -1547,7 +1547,7 @@ class AppModule(appModuleHandler.AppModule):
                if self.SPLCurVersion < "5.20":
                        status = 
self.status(self.SPLPlayStatus).getChild(index).name
                else:
-                       status = 
self._statusBarMessages[index][statusAPI(index, 39, ret=True)]
+                       status = 
self._statusBarMessages[index][studioAPI(index, 39, ret=True)]
                ui.message(status if 
splconfig.SPLConfig["General"]["MessageVerbosity"] == "beginner" else 
status.split()[-1])
 
        # The layer commands themselves.
@@ -1570,8 +1570,8 @@ class AppModule(appModuleHandler.AppModule):
        def script_sayCartEditStatus(self, gesture):
                # 16.12: Because cart edit status also shows cart insert 
status, verbosity control will not apply.
                if self.productVersion >= "5.20":
-                       cartEdit = statusAPI(5, 39, ret=True)
-                       cartInsert = statusAPI(6, 39, ret=True)
+                       cartEdit = studioAPI(5, 39, ret=True)
+                       cartInsert = studioAPI(6, 39, ret=True)
                        if cartEdit: ui.message("Cart Edit On")
                        elif not cartEdit and cartInsert: ui.message("Cart 
Insert On")
                        else: ui.message("Cart Edit Off")
@@ -1579,11 +1579,11 @@ class AppModule(appModuleHandler.AppModule):
                        
ui.message(self.status(self.SPLPlayStatus).getChild(5).name)
 
        def script_sayHourTrackDuration(self, gesture):
-               statusAPI(0, 27, self.announceTime)
+               studioAPI(0, 27, self.announceTime)
 
        def script_sayHourRemaining(self, gesture):
                # 7.0: Split from playlist remaining script (formerly the 
playlist remainder command).
-               statusAPI(1, 27, self.announceTime)
+               studioAPI(1, 27, self.announceTime)
 
        def script_sayPlaylistRemainingDuration(self, gesture):
                obj = api.getFocusObject() if 
api.getForegroundObject().windowClassName == "TStudioForm" else 
self._focusedTrack
@@ -1660,7 +1660,7 @@ class AppModule(appModuleHandler.AppModule):
                # 16.12: use Studio API if using 5.20.
                if self.productVersion >= "5.20":
                        # Sometimes, hour markers return seconds.999 due to 
rounding error, hence this must be taken care of here.
-                       trackStarts = divmod(statusAPI(3, 27, ret=True), 1000)
+                       trackStarts = divmod(studioAPI(3, 27, ret=True), 1000)
                        # For this method, all three components of time display 
(hour, minute, second) must be present.
                        # In case it is midnight (0.0 but sometimes shown as 
86399.999 due to rounding error), just say "midnight".
                        if trackStarts in ((86399, 999), (0, 0)): 
ui.message("00:00:00")
@@ -1674,7 +1674,7 @@ class AppModule(appModuleHandler.AppModule):
                # 16.12: Use Studio 5.20 API (faster and more reliable).
                if self.productVersion >= "5.20":
                        # This is the only time hour announcement should not be 
used in order to conform to what's displayed on screen.
-                       self.announceTime(statusAPI(4, 27, ret=True), 
includeHours=False)
+                       self.announceTime(studioAPI(4, 27, ret=True), 
includeHours=False)
                else:
                        obj = self.status(self.SPLScheduledToPlay).firstChild
                        ui.message(obj.name)
@@ -1696,8 +1696,8 @@ class AppModule(appModuleHandler.AppModule):
 
        def script_libraryScanMonitor(self, gesture):
                if not self.libraryScanning:
-                       if statusAPI(1, 32, ret=True) < 0:
-                               ui.message(_("{itemCount} items in the 
library").format(itemCount = statusAPI(0, 32, ret=True)))
+                       if studioAPI(1, 32, ret=True) < 0:
+                               ui.message(_("{itemCount} items in the 
library").format(itemCount = studioAPI(0, 32, ret=True)))
                                return
                        self.libraryScanning = True
                        # Translators: Presented when attempting to start 
library scan.
@@ -1792,7 +1792,7 @@ class AppModule(appModuleHandler.AppModule):
        # Gesture(s) for the following script cannot be changed by users.
        def script_metadataEnabled(self, gesture):
                url = int(gesture.displayName[-1])
-               if statusAPI(url, 36, ret=True):
+               if studioAPI(url, 36, ret=True):
                        # 0 is DSP encoder status, others are servers.
                        if url:
                                # Translators: Status message for metadata 
streaming.

diff --git a/addon/globalPlugins/splUtils/__init__.py 
b/addon/globalPlugins/splUtils/__init__.py
index c527bae..15564ad 100755
--- a/addon/globalPlugins/splUtils/__init__.py
+++ b/addon/globalPlugins/splUtils/__init__.py
@@ -30,7 +30,6 @@ def finally_(func, final):
 # SPL Studio uses WM messages to send and receive data, similar to Winamp (see 
NVDA sources/appModules/winamp.py for more information).
 user32 = winUser.user32 # user32.dll.
 SPLWin = 0 # A handle to studio window.
-SPLMSG = winUser.WM_USER
 
 # Various SPL IPC tags.
 SPLVersion = 2
@@ -148,70 +147,70 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin):
        # The layer commands themselves. Calls user32.SendMessage method for 
each script.
 
        def script_automateOn(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,1,SPLAutomate)
+               winUser.sendMessage(SPLWin,1024,1,SPLAutomate)
                self.finish()
 
        def script_automateOff(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,0,SPLAutomate)
+               winUser.sendMessage(SPLWin,1024,0,SPLAutomate)
                self.finish()
 
        def script_micOn(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,1,SPLMic)
+               winUser.sendMessage(SPLWin,1024,1,SPLMic)
                self.finish()
 
        def script_micOff(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,0,SPLMic)
+               winUser.sendMessage(SPLWin,1024,0,SPLMic)
                self.finish()
 
        def script_micNoFade(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,2,SPLMic)
+               winUser.sendMessage(SPLWin,1024,2,SPLMic)
                self.finish()
 
        def script_lineInOn(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,1,SPLLineIn)
+               winUser.sendMessage(SPLWin,1024,1,SPLLineIn)
                self.finish()
 
        def script_lineInOff(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,0,SPLLineIn)
+               winUser.sendMessage(SPLWin,1024,0,SPLLineIn)
                self.finish()
 
        def script_stopFade(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,0,SPLStop)
+               winUser.sendMessage(SPLWin,1024,0,SPLStop)
                self.finish()
 
        def script_stopInstant(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,1,SPLStop)
+               winUser.sendMessage(SPLWin,1024,1,SPLStop)
                self.finish()
 
        def script_play(self, gesture):
-               winUser.sendMessage(SPLWin, SPLMSG, 0, SPLPlay)
+               winUser.sendMessage(SPLWin, 1024, 0, SPLPlay)
                self.finish()
 
        def script_pause(self, gesture):
-               playingNow = winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPL_TrackPlaybackStatus)
+               playingNow = winUser.sendMessage(SPLWin, 1024, 0, 
SPL_TrackPlaybackStatus)
                # Translators: Presented when no track is playing in Station 
Playlist Studio.
                if not playingNow: ui.message(_("There is no track playing. Try 
pausing while a track is playing."))
-               elif playingNow == 3: winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPLPause)
-               else: winUser.sendMessage(SPLWin, SPLMSG, 1, SPLPause)
+               elif playingNow == 3: winUser.sendMessage(SPLWin, 1024, 0, 
SPLPause)
+               else: winUser.sendMessage(SPLWin, 1024, 1, SPLPause)
                self.finish()
 
        def script_libraryScanProgress(self, gesture):
-               scanned = winUser.sendMessage(SPLWin, SPLMSG, 1, 
SPLLibraryScanCount)
+               scanned = winUser.sendMessage(SPLWin, 1024, 1, 
SPLLibraryScanCount)
                if scanned >= 0:
                        # Translators: Announces number of items in the 
Studio's track library (example: 1000 items scanned).
                        ui.message(_("Scan in progress with {itemCount} items 
scanned").format(itemCount = scanned))
                else:
                        # Translators: Announces number of items in the 
Studio's track library (example: 1000 items scanned).
-                       ui.message(_("Scan complete with {itemCount} items 
scanned").format(itemCount = winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPLLibraryScanCount)))
+                       ui.message(_("Scan complete with {itemCount} items 
scanned").format(itemCount = winUser.sendMessage(SPLWin, 1024, 0, 
SPLLibraryScanCount)))
                self.finish()
 
        def script_listenerCount(self, gesture):
                # Translators: Announces number of stream listeners.
-               ui.message(_("Listener count: 
{listenerCount}").format(listenerCount = winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPLListenerCount)))
+               ui.message(_("Listener count: 
{listenerCount}").format(listenerCount = winUser.sendMessage(SPLWin, 1024, 0, 
SPLListenerCount)))
                self.finish()
 
        def script_remainingTime(self, gesture):
-               remainingTime = winUser.sendMessage(SPLWin, SPLMSG, 3, 
SPLCurTrackPlaybackTime)
+               remainingTime = winUser.sendMessage(SPLWin, 1024, 3, 
SPLCurTrackPlaybackTime)
                # Translators: Presented when no track is playing in Station 
Playlist Studio.
                if remainingTime < 0: ui.message(_("There is no track 
playing."))
                else:
@@ -247,19 +246,19 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin):
                # For consistency reasons (because of the Studio status bar), 
messages in this method will remain in English.
                statusInfo = []
                # 17.04: For Studio 5.10 and up, announce playback and 
automation status.
-               playingNow = winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPL_TrackPlaybackStatus)
+               playingNow = winUser.sendMessage(SPLWin, 1024, 0, 
SPL_TrackPlaybackStatus)
                statusInfo.append("Play status: playing" if playingNow else 
"Play status: stopped")
                # For automation, Studio 5.11 and earlier does not have an easy 
way to detect this flag, thus resort to using playback status.
-               if winUser.sendMessage(SPLWin, SPLMSG, 0, SPLVersion) < 520:
+               if winUser.sendMessage(SPLWin, 1024, 0, SPLVersion) < 520:
                        statusInfo.append("Automation on" if playingNow == 2 
else "Automation off")
                else:
-                       statusInfo.append("Automation on" if 
winUser.sendMessage(SPLWin, SPLMSG, 1, SPLStatusInfo) else "Automation off")
+                       statusInfo.append("Automation on" if 
winUser.sendMessage(SPLWin, 1024, 1, SPLStatusInfo) else "Automation off")
                        # 5.20 and later.
-                       statusInfo.append("Microphone on" if 
winUser.sendMessage(SPLWin, SPLMSG, 2, SPLStatusInfo) else "Microphone off")
-                       statusInfo.append("Line-inon" if 
winUser.sendMessage(SPLWin, SPLMSG, 3, SPLStatusInfo) else "Line-in off")
-                       statusInfo.append("Record to file on" if 
winUser.sendMessage(SPLWin, SPLMSG, 4, SPLStatusInfo) else "Record to file off")
-                       cartEdit = winUser.sendMessage(SPLWin, SPLMSG, 5, 
SPLStatusInfo)
-                       cartInsert = winUser.sendMessage(SPLWin, SPLMSG, 6, 
SPLStatusInfo)
+                       statusInfo.append("Microphone on" if 
winUser.sendMessage(SPLWin, 1024, 2, SPLStatusInfo) else "Microphone off")
+                       statusInfo.append("Line-inon" if 
winUser.sendMessage(SPLWin, 1024, 3, SPLStatusInfo) else "Line-in off")
+                       statusInfo.append("Record to file on" if 
winUser.sendMessage(SPLWin, 1024, 4, SPLStatusInfo) else "Record to file off")
+                       cartEdit = winUser.sendMessage(SPLWin, 1024, 5, 
SPLStatusInfo)
+                       cartInsert = winUser.sendMessage(SPLWin, 1024, 6, 
SPLStatusInfo)
                        if cartEdit: statusInfo.append("Cart edit on")
                        elif not cartEdit and cartInsert: 
statusInfo.append("Cart insert on")
                        else: statusInfo.append("Cart edit off")

diff --git a/addon/globalPlugins/splUtils/encoders.py 
b/addon/globalPlugins/splUtils/encoders.py
index 9e3b876..8143753 100755
--- a/addon/globalPlugins/splUtils/encoders.py
+++ b/addon/globalPlugins/splUtils/encoders.py
@@ -19,7 +19,6 @@ import wx
 # SPL Studio uses WM messages to send and receive data, similar to Winamp (see 
NVDA sources/appModules/winamp.py for more information).
 user32 = winUser.user32 # user32.dll.
 SPLWin = 0 # A handle to studio window.
-SPLMSG = winUser.WM_USER
 
 # Various SPL IPC tags.
 SPLPlay = 12
@@ -526,8 +525,8 @@ class SAMEncoder(Encoder):
                                        
user32.SetForegroundWindow(user32.FindWindowA("TStudioForm", None))
                                if self.playAfterConnecting and not encoding:
                                        # Do not interupt the currently playing 
track.
-                                       if winUser.sendMessage(SPLWin, SPLMSG, 
0, SPL_TrackPlaybackStatus) == 0:
-                                               winUser.sendMessage(SPLWin, 
SPLMSG, 0, SPLPlay)
+                                       if winUser.sendMessage(SPLWin, 1024, 0, 
SPL_TrackPlaybackStatus) == 0:
+                                               winUser.sendMessage(SPLWin, 
1024, 0, SPLPlay)
                                if not encoding: encoding = True
                        else:
                                if alreadyEncoding: alreadyEncoding = False
@@ -721,8 +720,8 @@ class SPLEncoder(Encoder):
                                if self.focusToStudio and not connected:
                                        
user32.SetForegroundWindow(user32.FindWindowA("TStudioForm", None))
                                if self.playAfterConnecting and not connected:
-                                       if winUser.sendMessage(SPLWin, SPLMSG, 
0, SPL_TrackPlaybackStatus) == 0:
-                                               winUser.sendMessage(SPLWin, 
SPLMSG, 0, SPLPlay)
+                                       if winUser.sendMessage(SPLWin, 1024, 0, 
SPL_TrackPlaybackStatus) == 0:
+                                               winUser.sendMessage(SPLWin, 
1024, 0, SPLPlay)
                                if not connected: connected = True
                        elif "Unable to connect" in messageCache or "Failed" in 
messageCache or statChild.name == "AutoConnect stopped.":
                                if connected: connected = False


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/479206f6b05a/
Changeset:   479206f6b05a
Branch:      None
User:        josephsl
Date:        2017-01-12 05:02:34+00:00
Summary:     Merge branch 'master' into plSnaps

Affected #:  6 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 555dbab..8f3320e 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -90,10 +90,10 @@ def micAlarmManager(micAlarmWav, micAlarmMessage):
                micAlarmT2 = wx.PyTimer(_micAlarmAnnouncer)
                
micAlarmT2.Start(splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarmInterval"] * 
1000)
 
-# Call SPL API to obtain needed values.
+# Use SPL Studio API to obtain needed values.
 # A thin wrapper around user32.SendMessage and calling a callback if defined.
 # Offset is used in some time commands.
-def statusAPI(arg, command, func=None, ret=False, offset=None):
+def studioAPI(arg, command, func=None, ret=False, offset=None):
        if _SPLWin is None: return
        val = sendMessage(_SPLWin, 1024, arg, command)
        if ret:
@@ -103,8 +103,8 @@ def statusAPI(arg, command, func=None, ret=False, 
offset=None):
 
 # Select a track upon request.
 def selectTrack(trackIndex):
-       statusAPI(-1, 121)
-       statusAPI(trackIndex, 121)
+       studioAPI(-1, 121)
+       studioAPI(trackIndex, 121)
 
 # Category sounds dictionary (key = category, value = tone pitch).
 _SPLCategoryTones = {
@@ -925,12 +925,12 @@ class AppModule(appModuleHandler.AppModule):
 
        # Scripts which rely on API.
        def script_sayRemainingTime(self, gesture):
-               statusAPI(3, 105, self.announceTime, offset=1)
+               studioAPI(3, 105, self.announceTime, offset=1)
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
        script_sayRemainingTime.__doc__=_("Announces the remaining track time.")
 
        def script_sayElapsedTime(self, gesture):
-               statusAPI(0, 105, self.announceTime)
+               studioAPI(0, 105, self.announceTime)
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
        script_sayElapsedTime.__doc__=_("Announces the elapsed time for the 
currently playing track.")
 
@@ -1147,7 +1147,7 @@ class AppModule(appModuleHandler.AppModule):
        def script_timeRangeFinder(self, gesture):
                if self._trackFinderCheck(2):
                        try:
-                               d = splmisc.SPLTimeRangeDialog(gui.mainFrame, 
api.getFocusObject(), statusAPI)
+                               d = splmisc.SPLTimeRangeDialog(gui.mainFrame, 
api.getFocusObject(), studioAPI)
                                gui.mainFrame.prePopup()
                                d.Raise()
                                d.Show()
@@ -1266,7 +1266,7 @@ class AppModule(appModuleHandler.AppModule):
                global libScanT
                if libScanT and libScanT.isAlive() and 
api.getForegroundObject().windowClassName == "TTrackInsertForm":
                        return
-               if statusAPI(1, 32, ret=True) < 0:
+               if studioAPI(1, 32, ret=True) < 0:
                        self.libraryScanning = False
                        return
                time.sleep(0.1)
@@ -1274,10 +1274,10 @@ class AppModule(appModuleHandler.AppModule):
                        self.libraryScanning = False
                        return
                # 17.04: Library scan may have finished while this thread was 
sleeping.
-               if statusAPI(1, 32, ret=True) < 0:
+               if studioAPI(1, 32, ret=True) < 0:
                        self.libraryScanning = False
                        # Translators: Presented when library scanning is 
finished.
-                       ui.message(_("{itemCount} items in the 
library").format(itemCount = statusAPI(0, 32, ret=True)))
+                       ui.message(_("{itemCount} items in the 
library").format(itemCount = studioAPI(0, 32, ret=True)))
                else:
                        libScanT = 
threading.Thread(target=self.libraryScanReporter)
                        libScanT.daemon = True
@@ -1286,7 +1286,7 @@ class AppModule(appModuleHandler.AppModule):
        def libraryScanReporter(self):
                scanIter = 0
                # 17.04: Use the constant directly, as 5.10 and later provides 
a convenient method to detect completion of library scans.
-               scanCount = statusAPI(1, 32, ret=True)
+               scanCount = studioAPI(1, 32, ret=True)
                while scanCount >= 0:
                        if not self.libraryScanning: return
                        time.sleep(1)
@@ -1294,7 +1294,7 @@ class AppModule(appModuleHandler.AppModule):
                        if api.getForegroundObject().windowClassName == 
"TTrackInsertForm" or not self.libraryScanning:
                                return
                        # Scan count may have changed during sleep.
-                       scanCount = statusAPI(1, 32, ret=True)
+                       scanCount = studioAPI(1, 32, ret=True)
                        if scanCount < 0:
                                break
                        scanIter+=1
@@ -1307,7 +1307,7 @@ class AppModule(appModuleHandler.AppModule):
                                tones.beep(370, 100)
                        else:
                                # Translators: Presented after library scan is 
done.
-                               ui.message(_("Scan complete with {itemCount} 
items").format(itemCount = statusAPI(0, 32, ret=True)))
+                               ui.message(_("Scan complete with {itemCount} 
items").format(itemCount = studioAPI(0, 32, ret=True)))
 
        # Take care of library scanning announcement.
        def _libraryScanAnnouncer(self, count, announcementType):
@@ -1361,7 +1361,7 @@ class AppModule(appModuleHandler.AppModule):
                        return
                try:
                        # Passing in the function object is enough to change 
the dialog UI.
-                       d = splconfui.MetadataStreamingDialog(gui.mainFrame, 
func=statusAPI)
+                       d = splconfui.MetadataStreamingDialog(gui.mainFrame, 
func=studioAPI)
                        gui.mainFrame.prePopup()
                        d.Raise()
                        d.Show()
@@ -1407,17 +1407,17 @@ class AppModule(appModuleHandler.AppModule):
        # Segue version of this will be used in some places (the below is the 
raw duration).)
        def playlistDurationRaw(self, start, end):
                # Take care of errors such as the following.
-               if start < 0 or end > statusAPI(0, 124, ret=True)-1:
+               if start < 0 or end > studioAPI(0, 124, ret=True)-1:
                        raise ValueError("Track range start or end position out 
of range")
                        return
                totalLength = 0
                if start == end:
-                       filename = statusAPI(start, 211, ret=True)
-                       totalLength = statusAPI(filename, 30, ret=True)
+                       filename = studioAPI(start, 211, ret=True)
+                       totalLength = studioAPI(filename, 30, ret=True)
                else:
                        for track in xrange(start, end+1):
-                               filename = statusAPI(track, 211, ret=True)
-                               totalLength+=statusAPI(filename, 30, ret=True)
+                               filename = studioAPI(track, 211, ret=True)
+                               totalLength+=studioAPI(filename, 30, ret=True)
                return totalLength
 
        # Playlist snapshots
@@ -1659,7 +1659,7 @@ class AppModule(appModuleHandler.AppModule):
                if self.SPLCurVersion < "5.20":
                        status = 
self.status(self.SPLPlayStatus).getChild(index).name
                else:
-                       status = 
self._statusBarMessages[index][statusAPI(index, 39, ret=True)]
+                       status = 
self._statusBarMessages[index][studioAPI(index, 39, ret=True)]
                ui.message(status if 
splconfig.SPLConfig["General"]["MessageVerbosity"] == "beginner" else 
status.split()[-1])
 
        # The layer commands themselves.
@@ -1682,8 +1682,8 @@ class AppModule(appModuleHandler.AppModule):
        def script_sayCartEditStatus(self, gesture):
                # 16.12: Because cart edit status also shows cart insert 
status, verbosity control will not apply.
                if self.productVersion >= "5.20":
-                       cartEdit = statusAPI(5, 39, ret=True)
-                       cartInsert = statusAPI(6, 39, ret=True)
+                       cartEdit = studioAPI(5, 39, ret=True)
+                       cartInsert = studioAPI(6, 39, ret=True)
                        if cartEdit: ui.message("Cart Edit On")
                        elif not cartEdit and cartInsert: ui.message("Cart 
Insert On")
                        else: ui.message("Cart Edit Off")
@@ -1691,11 +1691,11 @@ class AppModule(appModuleHandler.AppModule):
                        
ui.message(self.status(self.SPLPlayStatus).getChild(5).name)
 
        def script_sayHourTrackDuration(self, gesture):
-               statusAPI(0, 27, self.announceTime)
+               studioAPI(0, 27, self.announceTime)
 
        def script_sayHourRemaining(self, gesture):
                # 7.0: Split from playlist remaining script (formerly the 
playlist remainder command).
-               statusAPI(1, 27, self.announceTime)
+               studioAPI(1, 27, self.announceTime)
 
        def script_sayPlaylistRemainingDuration(self, gesture):
                obj = api.getFocusObject() if 
api.getForegroundObject().windowClassName == "TStudioForm" else 
self._focusedTrack
@@ -1763,7 +1763,7 @@ class AppModule(appModuleHandler.AppModule):
                # 16.12: use Studio API if using 5.20.
                if self.productVersion >= "5.20":
                        # Sometimes, hour markers return seconds.999 due to 
rounding error, hence this must be taken care of here.
-                       trackStarts = divmod(statusAPI(3, 27, ret=True), 1000)
+                       trackStarts = divmod(studioAPI(3, 27, ret=True), 1000)
                        # For this method, all three components of time display 
(hour, minute, second) must be present.
                        # In case it is midnight (0.0 but sometimes shown as 
86399.999 due to rounding error), just say "midnight".
                        if trackStarts in ((86399, 999), (0, 0)): 
ui.message("00:00:00")
@@ -1777,7 +1777,7 @@ class AppModule(appModuleHandler.AppModule):
                # 16.12: Use Studio 5.20 API (faster and more reliable).
                if self.productVersion >= "5.20":
                        # This is the only time hour announcement should not be 
used in order to conform to what's displayed on screen.
-                       self.announceTime(statusAPI(4, 27, ret=True), 
includeHours=False)
+                       self.announceTime(studioAPI(4, 27, ret=True), 
includeHours=False)
                else:
                        obj = self.status(self.SPLScheduledToPlay).firstChild
                        ui.message(obj.name)
@@ -1799,8 +1799,8 @@ class AppModule(appModuleHandler.AppModule):
 
        def script_libraryScanMonitor(self, gesture):
                if not self.libraryScanning:
-                       if statusAPI(1, 32, ret=True) < 0:
-                               ui.message(_("{itemCount} items in the 
library").format(itemCount = statusAPI(0, 32, ret=True)))
+                       if studioAPI(1, 32, ret=True) < 0:
+                               ui.message(_("{itemCount} items in the 
library").format(itemCount = studioAPI(0, 32, ret=True)))
                                return
                        self.libraryScanning = True
                        # Translators: Presented when attempting to start 
library scan.
@@ -1906,7 +1906,7 @@ class AppModule(appModuleHandler.AppModule):
        # Gesture(s) for the following script cannot be changed by users.
        def script_metadataEnabled(self, gesture):
                url = int(gesture.displayName[-1])
-               if statusAPI(url, 36, ret=True):
+               if studioAPI(url, 36, ret=True):
                        # 0 is DSP encoder status, others are servers.
                        if url:
                                # Translators: Status message for metadata 
streaming.

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 05c18a1..c58c6b2 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -31,7 +31,7 @@ except ImportError:
 SPLIni = os.path.join(globalVars.appArgs.configPath, "splstudio.ini")
 SPLProfiles = os.path.join(globalVars.appArgs.configPath, "addons", 
"stationPlaylist", "profiles")
 # New (7.0) style config.
-confspec7 = ConfigObj(StringIO("""
+confspec = ConfigObj(StringIO("""
 [General]
 BeepAnnounce = boolean(default=false)
 MessageVerbosity = option("beginner", "advanced", default="beginner")
@@ -83,13 +83,13 @@ UpdateInterval = integer(min=1, max=30, default=7)
 AudioDuckingReminder = boolean(default=true)
 WelcomeDialog = boolean(default=true)
 """), encoding="UTF-8", list_values=False)
-confspec7.newlines = "\r\n"
+confspec.newlines = "\r\n"
 SPLConfig = None
 # The following settings can be changed in profiles:
-_mutatableSettings7=("IntroOutroAlarms", "MicrophoneAlarm", 
"MetadataStreaming", "ColumnAnnouncement")
+_mutatableSettings=("IntroOutroAlarms", "MicrophoneAlarm", 
"MetadataStreaming", "ColumnAnnouncement")
 # 7.0: Profile-specific confspec (might be removed once a more optimal way to 
validate sections is found).
 # Dictionary comprehension is better here.
-confspecprofiles = {sect:key for sect, key in confspec7.iteritems() if sect in 
_mutatableSettings7}
+confspecprofiles = {sect:key for sect, key in confspec.iteritems() if sect in 
_mutatableSettings}
 
 # 8.0: Run-time config storage and management will use ConfigHub data 
structure, a subclass of chain map.
 # A chain map allows a dictionary to look up predefined mappings to locate a 
key.
@@ -165,10 +165,10 @@ class ConfigHub(ChainMap):
                # 7.0: What if profiles have parsing errors?
                # If so, reset everything back to factory defaults.
                try:
-                       SPLConfigCheckpoint = ConfigObj(path, configspec = 
confspec7 if prefill else confspecprofiles, encoding="UTF-8")
+                       SPLConfigCheckpoint = ConfigObj(path, configspec = 
confspec if prefill else confspecprofiles, encoding="UTF-8")
                except:
                        open(path, "w").close()
-                       SPLConfigCheckpoint = ConfigObj(path, configspec = 
confspec7 if prefill else confspecprofiles, encoding="UTF-8")
+                       SPLConfigCheckpoint = ConfigObj(path, configspec = 
confspec if prefill else confspecprofiles, encoding="UTF-8")
                        _configLoadStatus[profileName] = "fileReset"
                # 5.2 and later: check to make sure all values are correct.
                # 7.0: Make sure errors are displayed as config keys are now 
sections and may need to go through subkeys.
@@ -262,7 +262,7 @@ class ConfigHub(ChainMap):
 
        def __delitem__(self, key):
                # Consult profile-specific key first before deleting anything.
-               pos = 0 if key in _mutatableSettings7 else [profile.name for 
profile in self.maps].index(_("Normal Profile"))
+               pos = 0 if key in _mutatableSettings else [profile.name for 
profile in self.maps].index(_("Normal Profile"))
                try:
                        del self.maps[pos][key]
                except KeyError:
@@ -377,7 +377,7 @@ class ConfigHub(ChainMap):
 
 # Default config spec container.
 # To be moved to a different place in 8.0.
-_SPLDefaults = ConfigObj(None, configspec = confspec7, encoding="UTF-8")
+_SPLDefaults = ConfigObj(None, configspec = confspec, encoding="UTF-8")
 _val = Validator()
 _SPLDefaults.validate(_val, copy=True)
 
@@ -672,7 +672,7 @@ def getProfileByName(name):
 # Setting complete flag controls whether profile-specific settings are applied 
(true otherwise, only set when resetting profiles).
 # 8.0: Simplified thanks to in-place swapping.
 def copyProfile(sourceProfile, targetProfile, complete=False):
-       for section in sourceProfile.keys() if complete else 
_mutatableSettings7:
+       for section in sourceProfile.keys() if complete else _mutatableSettings:
                targetProfile[section] = dict(sourceProfile[section])
 
 # Last but not least...

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 0981885..ba9a496 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -625,7 +625,7 @@ class NewProfileDialog(wx.Dialog):
                # LTS optimization: just build base profile dictionary here if 
copying a profile.
                if self.copy:
                        baseConfig = 
splconfig.getProfileByName(self.baseProfiles.GetStringSelection())
-                       baseProfile = {sect:key for sect, key in 
baseConfig.iteritems() if sect in splconfig._mutatableSettings7}
+                       baseProfile = {sect:key for sect, key in 
baseConfig.iteritems() if sect in splconfig._mutatableSettings}
                else: baseProfile = None
                splconfig.SPLConfig.createProfile(newProfilePath, 
profileName=name, parent=baseProfile)
                parent.profileNames.append(name)

diff --git a/addon/globalPlugins/splUtils/__init__.py 
b/addon/globalPlugins/splUtils/__init__.py
index 12856ab..15564ad 100755
--- a/addon/globalPlugins/splUtils/__init__.py
+++ b/addon/globalPlugins/splUtils/__init__.py
@@ -30,7 +30,6 @@ def finally_(func, final):
 # SPL Studio uses WM messages to send and receive data, similar to Winamp (see 
NVDA sources/appModules/winamp.py for more information).
 user32 = winUser.user32 # user32.dll.
 SPLWin = 0 # A handle to studio window.
-SPLMSG = winUser.WM_USER
 
 # Various SPL IPC tags.
 SPLVersion = 2
@@ -148,70 +147,70 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin):
        # The layer commands themselves. Calls user32.SendMessage method for 
each script.
 
        def script_automateOn(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,1,SPLAutomate)
+               winUser.sendMessage(SPLWin,1024,1,SPLAutomate)
                self.finish()
 
        def script_automateOff(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,0,SPLAutomate)
+               winUser.sendMessage(SPLWin,1024,0,SPLAutomate)
                self.finish()
 
        def script_micOn(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,1,SPLMic)
+               winUser.sendMessage(SPLWin,1024,1,SPLMic)
                self.finish()
 
        def script_micOff(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,0,SPLMic)
+               winUser.sendMessage(SPLWin,1024,0,SPLMic)
                self.finish()
 
        def script_micNoFade(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,2,SPLMic)
+               winUser.sendMessage(SPLWin,1024,2,SPLMic)
                self.finish()
 
        def script_lineInOn(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,1,SPLLineIn)
+               winUser.sendMessage(SPLWin,1024,1,SPLLineIn)
                self.finish()
 
        def script_lineInOff(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,0,SPLLineIn)
+               winUser.sendMessage(SPLWin,1024,0,SPLLineIn)
                self.finish()
 
        def script_stopFade(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,0,SPLStop)
+               winUser.sendMessage(SPLWin,1024,0,SPLStop)
                self.finish()
 
        def script_stopInstant(self, gesture):
-               winUser.sendMessage(SPLWin,SPLMSG,1,SPLStop)
+               winUser.sendMessage(SPLWin,1024,1,SPLStop)
                self.finish()
 
        def script_play(self, gesture):
-               winUser.sendMessage(SPLWin, SPLMSG, 0, SPLPlay)
+               winUser.sendMessage(SPLWin, 1024, 0, SPLPlay)
                self.finish()
 
        def script_pause(self, gesture):
-               playingNow = winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPL_TrackPlaybackStatus)
+               playingNow = winUser.sendMessage(SPLWin, 1024, 0, 
SPL_TrackPlaybackStatus)
                # Translators: Presented when no track is playing in Station 
Playlist Studio.
                if not playingNow: ui.message(_("There is no track playing. Try 
pausing while a track is playing."))
-               elif playingNow == 3: winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPLPause)
-               else: winUser.sendMessage(SPLWin, SPLMSG, 1, SPLPause)
+               elif playingNow == 3: winUser.sendMessage(SPLWin, 1024, 0, 
SPLPause)
+               else: winUser.sendMessage(SPLWin, 1024, 1, SPLPause)
                self.finish()
 
        def script_libraryScanProgress(self, gesture):
-               scanned = winUser.sendMessage(SPLWin, SPLMSG, 1, 
SPLLibraryScanCount)
+               scanned = winUser.sendMessage(SPLWin, 1024, 1, 
SPLLibraryScanCount)
                if scanned >= 0:
                        # Translators: Announces number of items in the 
Studio's track library (example: 1000 items scanned).
                        ui.message(_("Scan in progress with {itemCount} items 
scanned").format(itemCount = scanned))
                else:
                        # Translators: Announces number of items in the 
Studio's track library (example: 1000 items scanned).
-                       ui.message(_("Scan complete with {itemCount} items 
scanned").format(itemCount = winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPLLibraryScanCount)))
+                       ui.message(_("Scan complete with {itemCount} items 
scanned").format(itemCount = winUser.sendMessage(SPLWin, 1024, 0, 
SPLLibraryScanCount)))
                self.finish()
 
        def script_listenerCount(self, gesture):
                # Translators: Announces number of stream listeners.
-               ui.message(_("Listener count: 
{listenerCount}").format(listenerCount = winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPLListenerCount)))
+               ui.message(_("Listener count: 
{listenerCount}").format(listenerCount = winUser.sendMessage(SPLWin, 1024, 0, 
SPLListenerCount)))
                self.finish()
 
        def script_remainingTime(self, gesture):
-               remainingTime = winUser.sendMessage(SPLWin, SPLMSG, 3, 
SPLCurTrackPlaybackTime)
+               remainingTime = winUser.sendMessage(SPLWin, 1024, 3, 
SPLCurTrackPlaybackTime)
                # Translators: Presented when no track is playing in Station 
Playlist Studio.
                if remainingTime < 0: ui.message(_("There is no track 
playing."))
                else:
@@ -239,22 +238,27 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin):
                self.finish()
 
        def script_statusInfo(self, gesture):
+               SPLWin = user32.FindWindowA("SPLStudio", None) # Used ANSI 
version, as Wide char version always returns 0.
+               if not SPLWin:
+                       ui.message(_("SPL Studio is not running."))
+                       self.finish()
+                       return
                # For consistency reasons (because of the Studio status bar), 
messages in this method will remain in English.
                statusInfo = []
                # 17.04: For Studio 5.10 and up, announce playback and 
automation status.
-               playingNow = winUser.sendMessage(SPLWin, SPLMSG, 0, 
SPL_TrackPlaybackStatus)
+               playingNow = winUser.sendMessage(SPLWin, 1024, 0, 
SPL_TrackPlaybackStatus)
                statusInfo.append("Play status: playing" if playingNow else 
"Play status: stopped")
                # For automation, Studio 5.11 and earlier does not have an easy 
way to detect this flag, thus resort to using playback status.
-               if winUser.sendMessage(SPLWin, SPLMSG, 0, SPLVersion) < 520:
+               if winUser.sendMessage(SPLWin, 1024, 0, SPLVersion) < 520:
                        statusInfo.append("Automation on" if playingNow == 2 
else "Automation off")
                else:
-                       statusInfo.append("Automation on" if 
winUser.sendMessage(SPLWin, SPLMSG, 1, SPLStatusInfo) else "Automation off")
+                       statusInfo.append("Automation on" if 
winUser.sendMessage(SPLWin, 1024, 1, SPLStatusInfo) else "Automation off")
                        # 5.20 and later.
-                       statusInfo.append("Microphone on" if 
winUser.sendMessage(SPLWin, SPLMSG, 2, SPLStatusInfo) else "Microphone off")
-                       statusInfo.append("Line-inon" if 
winUser.sendMessage(SPLWin, SPLMSG, 3, SPLStatusInfo) else "Line-in off")
-                       statusInfo.append("Record to file on" if 
winUser.sendMessage(SPLWin, SPLMSG, 4, SPLStatusInfo) else "Record to file off")
-                       cartEdit = winUser.sendMessage(SPLWin, SPLMSG, 5, 
SPLStatusInfo)
-                       cartInsert = winUser.sendMessage(SPLWin, SPLMSG, 6, 
SPLStatusInfo)
+                       statusInfo.append("Microphone on" if 
winUser.sendMessage(SPLWin, 1024, 2, SPLStatusInfo) else "Microphone off")
+                       statusInfo.append("Line-inon" if 
winUser.sendMessage(SPLWin, 1024, 3, SPLStatusInfo) else "Line-in off")
+                       statusInfo.append("Record to file on" if 
winUser.sendMessage(SPLWin, 1024, 4, SPLStatusInfo) else "Record to file off")
+                       cartEdit = winUser.sendMessage(SPLWin, 1024, 5, 
SPLStatusInfo)
+                       cartInsert = winUser.sendMessage(SPLWin, 1024, 6, 
SPLStatusInfo)
                        if cartEdit: statusInfo.append("Cart edit on")
                        elif not cartEdit and cartInsert: 
statusInfo.append("Cart insert on")
                        else: statusInfo.append("Cart edit off")

diff --git a/addon/globalPlugins/splUtils/encoders.py 
b/addon/globalPlugins/splUtils/encoders.py
index 9e3b876..8143753 100755
--- a/addon/globalPlugins/splUtils/encoders.py
+++ b/addon/globalPlugins/splUtils/encoders.py
@@ -19,7 +19,6 @@ import wx
 # SPL Studio uses WM messages to send and receive data, similar to Winamp (see 
NVDA sources/appModules/winamp.py for more information).
 user32 = winUser.user32 # user32.dll.
 SPLWin = 0 # A handle to studio window.
-SPLMSG = winUser.WM_USER
 
 # Various SPL IPC tags.
 SPLPlay = 12
@@ -526,8 +525,8 @@ class SAMEncoder(Encoder):
                                        
user32.SetForegroundWindow(user32.FindWindowA("TStudioForm", None))
                                if self.playAfterConnecting and not encoding:
                                        # Do not interupt the currently playing 
track.
-                                       if winUser.sendMessage(SPLWin, SPLMSG, 
0, SPL_TrackPlaybackStatus) == 0:
-                                               winUser.sendMessage(SPLWin, 
SPLMSG, 0, SPLPlay)
+                                       if winUser.sendMessage(SPLWin, 1024, 0, 
SPL_TrackPlaybackStatus) == 0:
+                                               winUser.sendMessage(SPLWin, 
1024, 0, SPLPlay)
                                if not encoding: encoding = True
                        else:
                                if alreadyEncoding: alreadyEncoding = False
@@ -721,8 +720,8 @@ class SPLEncoder(Encoder):
                                if self.focusToStudio and not connected:
                                        
user32.SetForegroundWindow(user32.FindWindowA("TStudioForm", None))
                                if self.playAfterConnecting and not connected:
-                                       if winUser.sendMessage(SPLWin, SPLMSG, 
0, SPL_TrackPlaybackStatus) == 0:
-                                               winUser.sendMessage(SPLWin, 
SPLMSG, 0, SPLPlay)
+                                       if winUser.sendMessage(SPLWin, 1024, 0, 
SPL_TrackPlaybackStatus) == 0:
+                                               winUser.sendMessage(SPLWin, 
1024, 0, SPLPlay)
                                if not connected: connected = True
                        elif "Unable to connect" in messageCache or "Failed" in 
messageCache or statChild.name == "AutoConnect stopped.":
                                if connected: connected = False

diff --git a/readme.md b/readme.md
index 0f0378f..29f22f3 100755
--- a/readme.md
+++ b/readme.md
@@ -24,6 +24,7 @@ IMPORTANT: This add-on requires NVDA 2016.4 or later and 
StationPlaylist Studio
 * Alt+NVDA+R from Studio window: Steps through library scan announcement 
settings.
 * Control+Shift+X from Studio window: Steps through braille timer settings.
 * Control+Alt+right/left arrow (while focused on a track): Announce 
next/previous track column.
+* Control+Alt+down/up arrow (while focused on a track): Move to next or 
previous track and announce specific columns (unavailable in add-on 15.x).
 * Control+NVDA+1 through 0 (6 for Studio 5.0x): Announce column content for a 
specified column.
 * Alt+NVDA+C while focused on a track: announces track comments if any.
 * Alt+NVDA+0 from Studio window: Opens the Studio add-on configuration dialog.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/545caf68bed4/
Changeset:   545caf68bed4
Branch:      None
User:        josephsl
Date:        2017-01-12 05:28:19+00:00
Summary:     Playlist snapshots (17.1-dev): Allow artist count to be 
configurable.

Just like category count, allow users to configure announcement of top 
artistsfor a playlist, while also incorporating studioAPI function renaming 
logic from master branch.

Affected #:  2 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 8f3320e..eb69d2d 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1456,7 +1456,7 @@ class AppModule(appModuleHandler.AppModule):
                                totalDuration += (int(hms[-2])*60) + 
int(hms[-1])
                                if len(hms) == 3: totalDuration += 
int(hms[0])*3600
                        obj = obj.next
-               if end is None: snapshot["PlaylistTrackCount"] = statusAPI(0, 
124, ret=True)
+               if end is None: snapshot["PlaylistTrackCount"] = studioAPI(0, 
124, ret=True)
                snapshot["PlaylistDurationTotal"] = 
self._ms2time(totalDuration, ms=False)
                if "DurationMinMax" in snapshotFlags:
                        snapshot["PlaylistDurationMin"] = "%s (%s)"%(minTitle, 
min)

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index ba9a496..b9d418f 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -185,12 +185,14 @@ class SPLConfigDialog(gui.SettingsDialog):
                self.topBottomCheckbox = 
SPLConfigHelper.addItem(wx.CheckBox(self, label=_("Notify when located at &top 
or bottom of playlist viewer")))
                
self.topBottomCheckbox.SetValue(splconfig.SPLConfig["General"]["TopBottomAnnounce"])
 
-               self.playlistDurationMinMax = 
splconfig.SPLConfig["PlaylistSnapshots"]["DurationMinMax"]
-               self.playlistDurationAverage = 
splconfig.SPLConfig["PlaylistSnapshots"]["DurationAverage"]
-               self.playlistCategoryCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCount"]
                # Translators: The label of a button to manage playlist 
snapshot flags.
                playlistSnapshotFlagsButton = 
SPLConfigHelper.addItem(wx.Button(self, label=_("&Playlist snapshots...")))
                playlistSnapshotFlagsButton.Bind(wx.EVT_BUTTON, 
self.onPlaylistSnapshotFlags)
+               # Playlist snapshot flags to be manipulated by the 
configuration dialog.
+               self.playlistDurationMinMax = 
splconfig.SPLConfig["PlaylistSnapshots"]["DurationMinMax"]
+               self.playlistDurationAverage = 
splconfig.SPLConfig["PlaylistSnapshots"]["DurationAverage"]
+               self.playlistArtistCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["ArtistCount"]
+               self.playlistCategoryCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCount"]
 
                sizer = gui.guiHelper.BoxSizerHelper(self, 
orientation=wx.HORIZONTAL)
                self.metadataValues=[("off",_("Off")),
@@ -286,6 +288,7 @@ class SPLConfigDialog(gui.SettingsDialog):
                splconfig.SPLConfig["General"]["TopBottomAnnounce"] = 
self.topBottomCheckbox.Value
                splconfig.SPLConfig["PlaylistSnapshots"]["DurationMinMax"] = 
self.playlistDurationMinMax
                splconfig.SPLConfig["PlaylistSnapshots"]["DurationAverage"] = 
self.playlistDurationAverage
+               splconfig.SPLConfig["PlaylistSnapshots"]["ArtistCount"] = 
self.playlistArtistCount
                splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCount"] = 
self.playlistCategoryCount
                splconfig.SPLConfig["General"]["MetadataReminder"] = 
self.metadataValues[self.metadataList.GetSelection()][0]
                splconfig.SPLConfig["MetadataStreaming"]["MetadataEnabled"] = 
self.metadataStreams
@@ -912,6 +915,9 @@ class PlaylistSnapshotsDialog(wx.Dialog):
                # Translators: the label for a setting in SPL add-on settings 
to include average track duration in playlist snapshots window.
                
self.playlistDurationAverageCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Average track duration")))
                
self.playlistDurationAverageCheckbox.SetValue(parent.playlistDurationAverage)
+               # Translators: the label for a setting in SPL add-on settings 
to include track artist count in playlist snapshots window.
+               
self.playlistArtistCountCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Artist count")))
+               
self.playlistArtistCountCheckbox.SetValue(parent.playlistArtistCount)
                # Translators: the label for a setting in SPL add-on settings 
to include track category count in playlist snapshots window.
                
self.playlistCategoryCountCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Category count")))
                
self.playlistCategoryCountCheckbox.SetValue(parent.playlistCategoryCount)
@@ -929,6 +935,7 @@ class PlaylistSnapshotsDialog(wx.Dialog):
                parent = self.Parent
                parent.playlistDurationMinMax = 
self.playlistDurationMinMaxCheckbox.Value
                parent.playlistDurationAverage = 
self.playlistDurationAverageCheckbox.Value
+               parent.playlistArtistCount = 
self.playlistArtistCountCheckbox.Value
                parent.playlistCategoryCount = 
self.playlistCategoryCountCheckbox.Value
                parent.profiles.SetFocus()
                parent.Enable()


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/bdbe8e594db6/
Changeset:   bdbe8e594db6
Branch:      None
User:        josephsl
Date:        2017-01-12 18:40:01+00:00
Summary:     Playlist snapshots (17.1-dev): Genre statistics will be gathered 
if told to do so.

Genre statistics as in top genres represented in the playlist (such as 'Rock' 
and so on). Also, a corresponding setting to toggle this off has been added. As 
of this commit basics of playlist snapshots is complete.

Affected #:  3 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index eb69d2d..ef7c46d 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1434,15 +1434,18 @@ class AppModule(appModuleHandler.AppModule):
                artists = []
                min, max = None, None
                minTitle, maxTitle = None, None
-               category = obj.indexOf("Category")
                totalDuration = 0
+               category = obj.indexOf("Category")
                categories = []
+               genre = obj.indexOf("Genre")
+               genres = []
                # A specific version of the playlist duration loop is needed in 
order to gather statistics.
                while obj not in (None, end):
                        segue = obj._getColumnContent(duration)
                        trackTitle = obj._getColumnContent(title)
                        categories.append(obj._getColumnContent(category))
                        if categories[-1] != "Hour Marker": 
artists.append(obj._getColumnContent(artist))
+                       genres.append(obj._getColumnContent(genre))
                        # Shortest and longest tracks.
                        if min is None: min = segue
                        if segue and segue < min:
@@ -1463,10 +1466,11 @@ class AppModule(appModuleHandler.AppModule):
                        snapshot["PlaylistDurationMax"] = "%s (%s)"%(maxTitle, 
max)
                if "DurationAverage" in snapshotFlags:
                        snapshot["PlaylistDurationAverage"] = 
self._ms2time(totalDuration/snapshot["PlaylistTrackCount"], ms=False)
-               if "CategoryCount" in snapshotFlags or "ArtistCount" in 
snapshotFlags:
+               if "CategoryCount" in snapshotFlags or "ArtistCount" in 
snapshotFlags or "GenreCount" in snapshotFlags:
                        import collections
                        if "CategoryCount" in snapshotFlags: 
snapshot["PlaylistCategoryCount"] = collections.Counter(categories)
                        if "ArtistCount" in snapshotFlags: 
snapshot["PlaylistArtistCount"] = collections.Counter(artists)
+                       if "GenreCount" in snapshotFlags: 
snapshot["PlaylistGenreCount"] = collections.Counter(genres)
                return snapshot
 
 # Output formatter for playlist snapshots.
@@ -1507,6 +1511,21 @@ class AppModule(appModuleHandler.AppModule):
                                        category = category.replace(">", "")
                                        categoryList.append("<li>%s 
(%s)</li>"%(category, count))
                                statusInfo.append("".join(["Categories:<ol>", 
"\n".join(categoryList), "</ol>"]))
+               if "PlaylistGenreCount" in snapshot:
+                       genres = snapshot["PlaylistGenreCount"].most_common()
+                       if scriptCount == 0:
+                               statusInfo.append("Top genre: %s 
(%s)"%(genres[0]))
+                       else:
+                               genreList = []
+                               for item in genres:
+                                       genre, count = item
+                                       try:
+                                               genre = genre.replace("<", "")
+                                               genre = genre.replace(">", "")
+                                               genreList.append("<li>%s 
(%s)</li>"%(genre, count))
+                                       except AttributeError:
+                                               genreList.append("<li> No genre 
information (%s)</li>"%(count))
+                               statusInfo.append("".join(["Top genres:<ol>", 
"\n".join(genreList), "</ol>"]))
                if scriptCount == 0:
                        ui.message(", ".join(statusInfo))
                else:

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index c58c6b2..7c4699f 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -53,6 +53,8 @@ ArtistCount = boolean(default=true)
 ArtistCountLimit = integer(min=0, max=10, default=5)
 CategoryCount = boolean(default=true)
 CategoryCountLimit = integer(min=0, max=10, default=5)
+GenreCount = boolean(default=true)
+GenreCountLimit = integer(min=0, max=10, default=5)
 [IntroOutroAlarms]
 SayEndOfTrack = boolean(default=true)
 EndOfTrackTime = integer(min=1, max=59, default=5)

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index b9d418f..7ee7532 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -193,6 +193,7 @@ class SPLConfigDialog(gui.SettingsDialog):
                self.playlistDurationAverage = 
splconfig.SPLConfig["PlaylistSnapshots"]["DurationAverage"]
                self.playlistArtistCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["ArtistCount"]
                self.playlistCategoryCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCount"]
+               self.playlistGenreCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["GenreCount"]
 
                sizer = gui.guiHelper.BoxSizerHelper(self, 
orientation=wx.HORIZONTAL)
                self.metadataValues=[("off",_("Off")),
@@ -290,6 +291,7 @@ class SPLConfigDialog(gui.SettingsDialog):
                splconfig.SPLConfig["PlaylistSnapshots"]["DurationAverage"] = 
self.playlistDurationAverage
                splconfig.SPLConfig["PlaylistSnapshots"]["ArtistCount"] = 
self.playlistArtistCount
                splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCount"] = 
self.playlistCategoryCount
+               splconfig.SPLConfig["PlaylistSnapshots"]["GenreCount"] = 
self.playlistGenreCount
                splconfig.SPLConfig["General"]["MetadataReminder"] = 
self.metadataValues[self.metadataList.GetSelection()][0]
                splconfig.SPLConfig["MetadataStreaming"]["MetadataEnabled"] = 
self.metadataStreams
                
splconfig.SPLConfig["ColumnAnnouncement"]["UseScreenColumnOrder"] = 
self.columnOrderCheckbox.Value
@@ -921,6 +923,9 @@ class PlaylistSnapshotsDialog(wx.Dialog):
                # Translators: the label for a setting in SPL add-on settings 
to include track category count in playlist snapshots window.
                
self.playlistCategoryCountCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Category count")))
                
self.playlistCategoryCountCheckbox.SetValue(parent.playlistCategoryCount)
+               # Translators: the label for a setting in SPL add-on settings 
to include track genre count in playlist snapshots window.
+               
self.playlistGenreCountCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Genre count")))
+               
self.playlistGenreCountCheckbox.SetValue(parent.playlistGenreCount)
 
                
playlistSnapshotsHelper.addDialogDismissButtons(self.CreateButtonSizer(wx.OK | 
wx.CANCEL))
                self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK)
@@ -937,6 +942,7 @@ class PlaylistSnapshotsDialog(wx.Dialog):
                parent.playlistDurationAverage = 
self.playlistDurationAverageCheckbox.Value
                parent.playlistArtistCount = 
self.playlistArtistCountCheckbox.Value
                parent.playlistCategoryCount = 
self.playlistCategoryCountCheckbox.Value
+               parent.playlistGenreCount = 
self.playlistGenreCountCheckbox.Value
                parent.profiles.SetFocus()
                parent.Enable()
                self.Destroy()


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/a8d5492f7a3a/
Changeset:   a8d5492f7a3a
Branch:      None
User:        josephsl
Date:        2017-01-12 22:22:21+00:00
Summary:     Playlist snapshots (17.1-dev): No longer allow old config format 
(playlist snapshots key in general settings) to be ported, playlist snapshots 
is ready.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 7c4699f..313e7f8 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -122,12 +122,6 @@ class ConfigHub(ChainMap):
                deprecatedKeys = {"General":"TrackDial", "Startup":"Studio500"}
                for section, key in deprecatedKeys.iteritems():
                        if key in self.maps[0][section]: del 
self.maps[0][section][key]
-               # January 2017 only: playlist snapshots is now its own 
dedicated section.
-               if "PlaylistSnapshots" in self.maps[0]["General"]: del 
self.maps[0]["General"]["PlaylistSnapshots"]
-               for key in ("PlaylistDurationMinMax", 
"PlaylistDurationAverage", "PlaylistCategoryCount"):
-                       if key in self.maps[0]["PlaylistSnapshots"]:
-                               self.maps[0]["PlaylistSnapshots"][key[8:]] = 
self.maps[0]["PlaylistSnapshots"][key]
-                               del self.maps[0]["PlaylistSnapshots"][key]
                # Moving onto broadcast profiles if any.
                try:
                        profiles = filter(lambda fn: os.path.splitext(fn)[-1] 
== ".ini", os.listdir(SPLProfiles))


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/73b61c93f408/
Changeset:   73b61c93f408
Branch:      None
User:        josephsl
Date:        2017-01-13 19:31:59+00:00
Summary:     Playlist snapshots (17.04-dev): apply limits on artist, category 
and genre count when announcing results.

The limit values for artist, category and genre counts are now used to limit 
top results to certain number of items, with values being 0 to 10 (0 being 
report all results). The configuration UI is next.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index ef7c46d..6320c26 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1485,25 +1485,25 @@ class AppModule(appModuleHandler.AppModule):
                if "PlaylistDurationAverage" in snapshot:
                        statusInfo.append("Average: 
%s"%snapshot["PlaylistDurationAverage"])
                if "PlaylistArtistCount" in snapshot:
-                       artists = snapshot["PlaylistArtistCount"].most_common()
                        if scriptCount == 0:
-                               statusInfo.append("Top artist: %s 
(%s)"%(artists[0]))
+                               statusInfo.append("Top artist: %s 
(%s)"%(snapshot["PlaylistArtistCount"][0]))
                        else:
+                               artistCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["ArtistCountLimit"]
+                               artists = 
snapshot["PlaylistArtistCount"].most_common(None if not artistCount else 
artistCount)
                                artistList = []
                                for item in artists:
                                        artist, count = item
-                                       try:
-                                               artist = artist.replace("<", "")
-                                               artist = artist.replace(">", "")
+                                       if artist is None:
+                                               artistList.append("<li>No 
artist information (%s)</li>"%(count))
+                                       else:
                                                artistList.append("<li>%s 
(%s)</li>"%(artist, count))
-                                       except AttributeError:
-                                               artistList.append("<li> No 
artist information (%s)</li>"%(count))
                                statusInfo.append("".join(["Top artists:<ol>", 
"\n".join(artistList), "</ol>"]))
                if "PlaylistCategoryCount" in snapshot:
-                       categories = 
snapshot["PlaylistCategoryCount"].most_common()
                        if scriptCount == 0:
-                               statusInfo.append("Top category: %s 
(%s)"%(categories[0]))
+                               statusInfo.append("Top category: %s 
(%s)"%(snapshot["PlaylistCategoryCount"][0]))
                        else:
+                               categoryCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCountLimit"]
+                               categories = 
snapshot["PlaylistCategoryCount"].most_common(None if not categoryCount else 
categoryCount)
                                categoryList = []
                                for item in categories:
                                        category, count = item
@@ -1512,19 +1512,18 @@ class AppModule(appModuleHandler.AppModule):
                                        categoryList.append("<li>%s 
(%s)</li>"%(category, count))
                                statusInfo.append("".join(["Categories:<ol>", 
"\n".join(categoryList), "</ol>"]))
                if "PlaylistGenreCount" in snapshot:
-                       genres = snapshot["PlaylistGenreCount"].most_common()
                        if scriptCount == 0:
-                               statusInfo.append("Top genre: %s 
(%s)"%(genres[0]))
+                               statusInfo.append("Top genre: %s 
(%s)"%(snapshot["PlaylistGenreCount"][0]))
                        else:
+                               genreCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["GenreCountLimit"]
+                               genres = 
snapshot["PlaylistGenreCount"].most_common(None if not genreCount else 
genreCount)
                                genreList = []
                                for item in genres:
                                        genre, count = item
-                                       try:
-                                               genre = genre.replace("<", "")
-                                               genre = genre.replace(">", "")
+                                       if genre is None:
+                                               genreList.append("<li>No genre 
information (%s)</li>"%(count))
+                                       else:
                                                genreList.append("<li>%s 
(%s)</li>"%(genre, count))
-                                       except AttributeError:
-                                               genreList.append("<li> No genre 
information (%s)</li>"%(count))
                                statusInfo.append("".join(["Top genres:<ol>", 
"\n".join(genreList), "</ol>"]))
                if scriptCount == 0:
                        ui.message(", ".join(statusInfo))


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/5f16cdaffbd7/
Changeset:   5f16cdaffbd7
Branch:      None
User:        josephsl
Date:        2017-01-14 03:50:13+00:00
Summary:     Playlist snapshots (17.04-dev): allow artist, category and genre 
count limit to be configurable.

Users can now configure how many artists, categories or genres would be counted 
(0 means count all) via three new number edit fields.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 7ee7532..454dbb1 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -192,8 +192,11 @@ class SPLConfigDialog(gui.SettingsDialog):
                self.playlistDurationMinMax = 
splconfig.SPLConfig["PlaylistSnapshots"]["DurationMinMax"]
                self.playlistDurationAverage = 
splconfig.SPLConfig["PlaylistSnapshots"]["DurationAverage"]
                self.playlistArtistCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["ArtistCount"]
+               self.playlistArtistCountLimit = 
splconfig.SPLConfig["PlaylistSnapshots"]["ArtistCountLimit"]
                self.playlistCategoryCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCount"]
+               self.playlistCategoryCountLimit = 
splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCountLimit"]
                self.playlistGenreCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["GenreCount"]
+               self.playlistGenreCountLimit = 
splconfig.SPLConfig["PlaylistSnapshots"]["GenreCountLimit"]
 
                sizer = gui.guiHelper.BoxSizerHelper(self, 
orientation=wx.HORIZONTAL)
                self.metadataValues=[("off",_("Off")),
@@ -290,8 +293,11 @@ class SPLConfigDialog(gui.SettingsDialog):
                splconfig.SPLConfig["PlaylistSnapshots"]["DurationMinMax"] = 
self.playlistDurationMinMax
                splconfig.SPLConfig["PlaylistSnapshots"]["DurationAverage"] = 
self.playlistDurationAverage
                splconfig.SPLConfig["PlaylistSnapshots"]["ArtistCount"] = 
self.playlistArtistCount
+               splconfig.SPLConfig["PlaylistSnapshots"]["ArtistCountLimit"] = 
self.playlistArtistCountLimit
                splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCount"] = 
self.playlistCategoryCount
+               splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCountLimit"] 
= self.playlistCategoryCountLimit
                splconfig.SPLConfig["PlaylistSnapshots"]["GenreCount"] = 
self.playlistGenreCount
+               splconfig.SPLConfig["PlaylistSnapshots"]["GenreCountLimit"] = 
self.playlistGenreCountLimit
                splconfig.SPLConfig["General"]["MetadataReminder"] = 
self.metadataValues[self.metadataList.GetSelection()][0]
                splconfig.SPLConfig["MetadataStreaming"]["MetadataEnabled"] = 
self.metadataStreams
                
splconfig.SPLConfig["ColumnAnnouncement"]["UseScreenColumnOrder"] = 
self.columnOrderCheckbox.Value
@@ -920,12 +926,15 @@ class PlaylistSnapshotsDialog(wx.Dialog):
                # Translators: the label for a setting in SPL add-on settings 
to include track artist count in playlist snapshots window.
                
self.playlistArtistCountCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Artist count")))
                
self.playlistArtistCountCheckbox.SetValue(parent.playlistArtistCount)
+               
self.playlistArtistCountLimit=playlistSnapshotsHelper.addLabeledControl(_("Top 
artist count (0 displays all artists)"), 
gui.nvdaControls.SelectOnFocusSpinCtrl, min=0, max=10, 
initial=parent.playlistArtistCountLimit)
                # Translators: the label for a setting in SPL add-on settings 
to include track category count in playlist snapshots window.
                
self.playlistCategoryCountCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Category count")))
                
self.playlistCategoryCountCheckbox.SetValue(parent.playlistCategoryCount)
+               
self.playlistCategoryCountLimit=playlistSnapshotsHelper.addLabeledControl(_("Top
 category count (0 displays all categories)"), 
gui.nvdaControls.SelectOnFocusSpinCtrl, min=0, max=10, 
initial=parent.playlistCategoryCountLimit)
                # Translators: the label for a setting in SPL add-on settings 
to include track genre count in playlist snapshots window.
                
self.playlistGenreCountCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Genre count")))
                
self.playlistGenreCountCheckbox.SetValue(parent.playlistGenreCount)
+               
self.playlistGenreCountLimit=playlistSnapshotsHelper.addLabeledControl(_("Top 
genre count (0 displays all genres)"), gui.nvdaControls.SelectOnFocusSpinCtrl, 
min=0, max=10, initial=parent.playlistGenreCountLimit)
 
                
playlistSnapshotsHelper.addDialogDismissButtons(self.CreateButtonSizer(wx.OK | 
wx.CANCEL))
                self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK)
@@ -941,8 +950,11 @@ class PlaylistSnapshotsDialog(wx.Dialog):
                parent.playlistDurationMinMax = 
self.playlistDurationMinMaxCheckbox.Value
                parent.playlistDurationAverage = 
self.playlistDurationAverageCheckbox.Value
                parent.playlistArtistCount = 
self.playlistArtistCountCheckbox.Value
+               parent.playlistArtistCountLimit = 
self.playlistArtistCountLimit.GetValue()
                parent.playlistCategoryCount = 
self.playlistCategoryCountCheckbox.Value
+               parent.playlistCategoryCountLimit = 
self.playlistCategoryCountLimit.GetValue()
                parent.playlistGenreCount = 
self.playlistGenreCountCheckbox.Value
+               parent.playlistGenreCountLimit = 
self.playlistGenreCountLimit.GetValue()
                parent.profiles.SetFocus()
                parent.Enable()
                self.Destroy()


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/e8cf7a633b24/
Changeset:   e8cf7a633b24
Branch:      None
User:        josephsl
Date:        2017-01-14 06:34:34+00:00
Summary:     Playlist snapshots (17.04-dev): Allow custom commands to be 
defined for playlist snapshtos script, some presentation tweaks.

It is now possible for broadcasters to assign a hotkey to invoke playlist 
snapshots. Pressing once will speak and braille brief snapshot info, while 
pressing it twice will present the full snapshots display. To prevent mishaps 
with multiple HTML windows appearing and slowing down performance, the command 
will not work if pressed more than twice.
Also tweaked how snapshot info is presented. Specificlaly, because the top 
artist and others will be announced when the script is invoked once, counters 
will be fetched early.
All this work is destined for add-on 17.04.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 6320c26..30c2c44 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1476,7 +1476,6 @@ class AppModule(appModuleHandler.AppModule):
 # Output formatter for playlist snapshots.
 # Pressed once will speak and/or braille it, pressing twice or more will 
output this info to an HTML file.
        def playlistSnapshotOutput(self, snapshot, scriptCount):
-               scriptCount = 1
                statusInfo = ["Tracks: %s"%snapshot["PlaylistTrackCount"]]
                statusInfo.append("Duration: 
%s"%snapshot["PlaylistDurationTotal"])
                if "PlaylistDurationMin" in snapshot:
@@ -1485,11 +1484,11 @@ class AppModule(appModuleHandler.AppModule):
                if "PlaylistDurationAverage" in snapshot:
                        statusInfo.append("Average: 
%s"%snapshot["PlaylistDurationAverage"])
                if "PlaylistArtistCount" in snapshot:
+                       artistCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["ArtistCountLimit"]
+                       artists = 
snapshot["PlaylistArtistCount"].most_common(None if not artistCount else 
artistCount)
                        if scriptCount == 0:
-                               statusInfo.append("Top artist: %s 
(%s)"%(snapshot["PlaylistArtistCount"][0]))
-                       else:
-                               artistCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["ArtistCountLimit"]
-                               artists = 
snapshot["PlaylistArtistCount"].most_common(None if not artistCount else 
artistCount)
+                               statusInfo.append("Top artist: %s 
(%s)"%(artists[0][:]))
+                       elif scriptCount == 1:
                                artistList = []
                                for item in artists:
                                        artist, count = item
@@ -1499,11 +1498,11 @@ class AppModule(appModuleHandler.AppModule):
                                                artistList.append("<li>%s 
(%s)</li>"%(artist, count))
                                statusInfo.append("".join(["Top artists:<ol>", 
"\n".join(artistList), "</ol>"]))
                if "PlaylistCategoryCount" in snapshot:
+                       categoryCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCountLimit"]
+                       categories = 
snapshot["PlaylistCategoryCount"].most_common(None if not categoryCount else 
categoryCount)
                        if scriptCount == 0:
-                               statusInfo.append("Top category: %s 
(%s)"%(snapshot["PlaylistCategoryCount"][0]))
-                       else:
-                               categoryCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCountLimit"]
-                               categories = 
snapshot["PlaylistCategoryCount"].most_common(None if not categoryCount else 
categoryCount)
+                               statusInfo.append("Top category: %s 
(%s)"%(categories[0][:]))
+                       elif scriptCount == 1:
                                categoryList = []
                                for item in categories:
                                        category, count = item
@@ -1512,11 +1511,11 @@ class AppModule(appModuleHandler.AppModule):
                                        categoryList.append("<li>%s 
(%s)</li>"%(category, count))
                                statusInfo.append("".join(["Categories:<ol>", 
"\n".join(categoryList), "</ol>"]))
                if "PlaylistGenreCount" in snapshot:
+                       genreCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["GenreCountLimit"]
+                       genres = 
snapshot["PlaylistGenreCount"].most_common(None if not genreCount else 
genreCount)
                        if scriptCount == 0:
-                               statusInfo.append("Top genre: %s 
(%s)"%(snapshot["PlaylistGenreCount"][0]))
-                       else:
-                               genreCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["GenreCountLimit"]
-                               genres = 
snapshot["PlaylistGenreCount"].most_common(None if not genreCount else 
genreCount)
+                               statusInfo.append("Top genre: %s 
(%s)"%(genres[0][:]))
+                       elif scriptCount == 1:
                                genreList = []
                                for item in genres:
                                        genre, count = item
@@ -1875,12 +1874,22 @@ class AppModule(appModuleHandler.AppModule):
                obj = api.getFocusObject() if 
api.getForegroundObject().windowClassName == "TStudioForm" else 
self._focusedTrack
                if obj is None:
                        ui.message("Please return to playlist viewer before 
invoking this command.")
+                       self.finish()
                        return
                if obj.role == controlTypes.ROLE_LIST:
                        ui.message(_("You need to add tracks before invoking 
this command"))
+                       self.finish()
                        return
-               # Speak and braille on the first press, display a decorated 
HTML message for subsequent presses.
-               
self.playlistSnapshotOutput(self.playlistSnapshots(obj.parent.firstChild, 
None), scriptHandler.getLastScriptRepeatCount())
+               scriptCount = scriptHandler.getLastScriptRepeatCount()
+               # Never allow this to be invoked more than twice, as it causes 
performance degredation and multiple HTML windows are opened.
+               if scriptCount >= 2:
+                       self.finish()
+                       return
+                       # Speak and braille on the first press, display a 
decorated HTML message for subsequent presses.
+               
self.playlistSnapshotOutput(self.playlistSnapshots(obj.parent.firstChild, 
None), scriptCount)
+               self.finish()
+       # Translators: Input help mode message for a command in Station 
Playlist Studio.
+       script_takePlaylistSnapshots.__doc__=_("Presents playlist snapshot 
information such as number of tracks and top artists")
 
        def script_switchProfiles(self, gesture):
                splconfig.triggerProfileSwitch() if 
splconfig._triggerProfileActive else splconfig.instantProfileSwitch()


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/9fe9e5c484f9/
Changeset:   9fe9e5c484f9
Branch:      None
User:        josephsl
Date:        2017-01-14 06:49:39+00:00
Summary:     Readme entry for playlist snapshots.

Affected #:  1 file

diff --git a/readme.md b/readme.md
index 29f22f3..748477a 100755
--- a/readme.md
+++ b/readme.md
@@ -45,6 +45,7 @@ The following commands are not assigned by default; if you 
wish to assign them,
 * Announcing title of the currently playing track.
 * Marking current track for start of track time analysis.
 * Performing track time analysis.
+* Take playlist snapshots.
 * Find text in specific columns.
 * Find tracks with duration that falls within a given range via time range 
finder.
 * Quickly enable or disable metadata streaming.
@@ -107,6 +108,7 @@ The available commands are:
 * W: Weather and temperature if configured.
 * Y: Playlist modified status.
 * 1 through 0 (6 for Studio 5.0x): Announce column content for a specified 
column.
+* F8: Take playlist snapshots (number of tracks, longest track, etc.).
 * F9: Mark current track for track time analysis (playlist viewer only).
 * F10: Perform track time analysis (playlist viewer only).
 * F12: Switch between current and a predefined profile.
@@ -160,6 +162,10 @@ To obtain length to play selected tracks, mark current 
track for start of track
 
 By pressing Control+NVDA+1 through 0 (6 for Studio 5.0x) or SPL Assistant, 1 
through 0 (6 for Studio 5.01 and earlier), you can obtain contents of specific 
columns. By default, these are artist, title, duration, intro, category and 
filename (Studio 5.10 adds year, album, genre and time scheduled). You can 
configure which columns will be explored via columns explorer dialog found in 
add-on settings dialog.
 
+## Playlist snapshots
+
+You can press SPL Assistant, F8 while focused on a playlist in Studio to 
obtain various statistics about a playlist, including number of tracks in the 
playlist, longest track, top artists and so on. After assigning a custom 
command for this feature, pressing the custom command twice will cause NVDA to 
present playlist snapshot information as a webpage so you can use browse mode 
to navigate (press escape to close).
+
 ## Configuration dialog
 
 From studio window, you can press Alt+NVDA+0 to open the add-on configuration 
dialog. Alternatively, go to NVDA's preferences menu and select SPL Studio 
Settings item. This dialog is also used to manage broadcast profiles.
@@ -175,6 +181,8 @@ If you are using Studio on a touchscreen computer running 
Windows 8 or later and
 * Added a combo box in add-on settings dialog to set which column should be 
announced when moving through columns vertically.
 * Moved end of track , intro and microphone alarm controls from add-on 
settings to the new Alarms Center.
 * In Alarms Center, end of track and track intro edit fields are always shown 
regardless of state of alarm notification checkboxes.
+* Added a command in SPL Assistant to obtain playlist snapshots such as number 
of tracks, longest track, top artists and so on (F8). You can also add a custom 
command for this feature.
+* Pressing the custom gesture for playlist snapshots command once will let 
NVDA speak and braile a short snapshot information. Pressing the command twice 
will cause NVDA to open a webpage containing a fuller playlist snapshot 
information. Press escape to close this webpage.
 * Removed Track Dial (NVDA's version of enhanced arrow keys), replaced by 
Columns explorer and Column Navigator/table navigation commands). This affects 
Studio and Track Tool.
 * After closing Insert Tracks dialog while a library scan is in progress, it 
is no longer required to press SPL Assistant, Shift+R to monitor scan progress.
 * Improved accuracy of detecting and reporting completion of library scans in 
Studio 5.10 and later. This fixes a problem where library scan monitor will end 
prematurely when there are more tracks to be scanned, necessitating restarting 
library scan monitor.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/2c667a1eaa0e/
Changeset:   2c667a1eaa0e
Branch:      None
User:        josephsl
Date:        2017-01-14 20:07:00+00:00
Summary:     SPL gestures: do not allow built-in and custom SPL add-on commands 
to proceed if handle to studio is not present.

In studio Demo, before the main window appears, registration screen is shown. 
At this time, main window handle to Studio isn't present, which causes commands 
such as remainig time of the track, temperature, playlist snapshots and others 
to fail (not announcing anything, playing error tones, or giving wrong info). 
This has been mitigated via a new function that'll announce an error message if 
this is such a case.
This fix is destined for add-on 17.04.

Affected #:  2 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 30c2c44..4069f79 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -101,6 +101,15 @@ def studioAPI(arg, command, func=None, ret=False, 
offset=None):
        if func:
                func(val) if not offset else func(val, offset)
 
+# Check if Studio itself is running.
+# This is to make sure custom commands for SPL Assistant comamnds and other 
app module gestures display appropriate error messages.
+def studioIsRunning():
+       if _SPLWin is None:
+               # Translators: A message informing users that Studio is not 
running so certain commands will not work.
+               ui.message(_("Studio main window not found"))
+               return False
+       return True
+
 # Select a track upon request.
 def selectTrack(trackIndex):
        studioAPI(-1, 121)
@@ -925,16 +934,17 @@ class AppModule(appModuleHandler.AppModule):
 
        # Scripts which rely on API.
        def script_sayRemainingTime(self, gesture):
-               studioAPI(3, 105, self.announceTime, offset=1)
+               if studioIsRunning(): studioAPI(3, 105, self.announceTime, 
offset=1)
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
        script_sayRemainingTime.__doc__=_("Announces the remaining track time.")
 
        def script_sayElapsedTime(self, gesture):
-               studioAPI(0, 105, self.announceTime)
+               if studioIsRunning(): studioAPI(0, 105, self.announceTime)
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
        script_sayElapsedTime.__doc__=_("Announces the elapsed time for the 
currently playing track.")
 
        def script_sayBroadcasterTime(self, gesture):
+               if not studioIsRunning(): return
                # Says things such as "25 minutes to 2" and "5 past 11".
                # Parse the local time and say it similar to how Studio 
presents broadcaster time.
                h, m = time.localtime()[3], time.localtime()[4]
@@ -956,6 +966,7 @@ class AppModule(appModuleHandler.AppModule):
        script_sayBroadcasterTime.__doc__=_("Announces broadcaster time.")
 
        def script_sayCompleteTime(self, gesture):
+               if not studioIsRunning(): return
                import winKernel
                # Says complete time in hours, minutes and seconds via 
kernel32's routines.
                
ui.message(winKernel.GetTimeFormat(winKernel.LOCALE_USER_DEFAULT, 0, None, 
None))
@@ -1083,6 +1094,7 @@ class AppModule(appModuleHandler.AppModule):
        # But first, check if track finder can be invoked.
        # Attempt level specifies which track finder to open (0 = Track Finder, 
1 = Column Search, 2 = Time range).
        def _trackFinderCheck(self, attemptLevel):
+               if not studioIsRunning(): return False
                if api.getForegroundObject().windowClassName != "TStudioForm":
                        if attemptLevel == 0:
                                # Translators: Presented when a user attempts 
to find tracks but is not at the track list.
@@ -1194,6 +1206,7 @@ class AppModule(appModuleHandler.AppModule):
                        self.bindGestures(self.__gestures)
 
        def script_toggleCartExplorer(self, gesture):
+               if not studioIsRunning(): return
                if not self.cartExplorer:
                        # Prevent cart explorer from being engaged outside of 
playlist viewer.
                        # Todo for 6.0: Let users set cart banks.
@@ -1352,7 +1365,7 @@ class AppModule(appModuleHandler.AppModule):
        def script_manageMetadataStreams(self, gesture):
                # Do not even think about opening this dialog if handle to 
Studio isn't found.
                if _SPLWin is None:
-                       # Translators: Presented when stremaing dialog cannot 
be shown.
+                       # Translators: Presented when streaming dialog cannot 
be shown.
                        ui.message(_("Cannot open metadata streaming dialog"))
                        return
                if splconfui._configDialogOpened or 
splconfui._metadataDialogOpened:
@@ -1380,6 +1393,8 @@ class AppModule(appModuleHandler.AppModule):
 
        # Trakc time analysis and playlist snapshots require main playlist 
viewer to be the foreground window.
        def _trackAnalysisAllowed(self):
+               if not studioIsRunning():
+                       return False
                if api.getForegroundObject().windowClassName != "TStudioForm":
                        # Translators: Presented when track time anlaysis 
cannot be performed because user is not focused on playlist viewer.
                        ui.message(_("Not in playlist viewer, cannot perform 
track time analysis or gather playlist snapshot statistics"))
@@ -1551,7 +1566,6 @@ class AppModule(appModuleHandler.AppModule):
                os.startfile("mailto:joseph.lee22590@xxxxxxxxx";)
        script_sendFeedbackEmail.__doc__="Opens the default email client to 
send an email to the add-on developer"
 
-
        # SPL Assistant: reports status on playback, operation, etc.
        # Used layer command approach to save gesture assignments.
        # Most were borrowed from JFW and Window-Eyes layer scripts.
@@ -1733,6 +1747,9 @@ class AppModule(appModuleHandler.AppModule):
                        ui.message(_("Playlist modification not available"))
 
        def script_sayNextTrackTitle(self, gesture):
+               if not studioIsRunning():
+                       self.finish()
+                       return
                try:
                        obj = self.status(self.SPLNextTrackTitle).firstChild
                        # Translators: Presented when there is no information 
for the next track.
@@ -1746,6 +1763,9 @@ class AppModule(appModuleHandler.AppModule):
        script_sayNextTrackTitle.__doc__=_("Announces title of the next track 
if any")
 
        def script_sayCurrentTrackTitle(self, gesture):
+               if not studioIsRunning():
+                       self.finish()
+                       return
                try:
                        obj = self.status(self.SPLCurrentTrackTitle).firstChild
                        # Translators: Presented when there is no information 
for the current track.
@@ -1759,6 +1779,9 @@ class AppModule(appModuleHandler.AppModule):
        script_sayCurrentTrackTitle.__doc__=_("Announces title of the currently 
playing track")
 
        def script_sayTemperature(self, gesture):
+               if not studioIsRunning():
+                       self.finish()
+                       return
                try:
                        obj = self.status(self.SPLTemperature).firstChild
                        # Translators: Presented when there is no weather or 
temperature information.
@@ -1871,6 +1894,9 @@ class AppModule(appModuleHandler.AppModule):
        script_trackTimeAnalysis.__doc__=_("Announces total length of tracks 
between analysis start marker and the current track")
 
        def script_takePlaylistSnapshots(self, gesture):
+               if not studioIsRunning():
+                       self.finish()
+                       return
                obj = api.getFocusObject() if 
api.getForegroundObject().windowClassName == "TStudioForm" else 
self._focusedTrack
                if obj is None:
                        ui.message("Please return to playlist viewer before 
invoking this command.")

diff --git a/readme.md b/readme.md
index 748477a..efa7b0b 100755
--- a/readme.md
+++ b/readme.md
@@ -187,6 +187,7 @@ If you are using Studio on a touchscreen computer running 
Windows 8 or later and
 * After closing Insert Tracks dialog while a library scan is in progress, it 
is no longer required to press SPL Assistant, Shift+R to monitor scan progress.
 * Improved accuracy of detecting and reporting completion of library scans in 
Studio 5.10 and later. This fixes a problem where library scan monitor will end 
prematurely when there are more tracks to be scanned, necessitating restarting 
library scan monitor.
 * Improved library scan status reporting via SPL Controller (Shift+R) by 
announcing scan count if scan is indeed happening.
+* In studio Demo, when registration screen appears when starting Studio, 
commands such as remaining time for a track will no longer cause NVDA to do 
nothing, play error tones, or give wrong information. An error message will be 
announced instead. Commands such as these will require Studio's main window 
handle to be present.
 * Initial support for StationPlaylist Creator.
 * Added a new command in SPL Controller layer to announce Studio status such 
as track playback and microphone status (Q).
 


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/3feff4e9ec7d/
Changeset:   3feff4e9ec7d
Branch:      None
User:        josephsl
Date:        2017-01-15 05:03:41+00:00
Summary:     Merged stable

Affected #:  2 files

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 3191f1c..3d1827f 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -37,6 +37,7 @@ _updatePickle = os.path.join(globalVars.appArgs.configPath, 
"splupdate.pickle")
 # Not all update channels are listed. The one not listed here is the default 
("stable" for this branch).
 channels={
        "stable":"http://addons.nvda-project.org/files/get.php?file=spl";,
+       "lts":"http://josephsl.net/files/nvdaaddons/get.php?file=spl-lts16";,
        #"beta":"http://spl.nvda-kr.org/files/get.php?file=spl-beta";,
 }
 

diff --git a/readme.md b/readme.md
index efa7b0b..d7786b1 100755
--- a/readme.md
+++ b/readme.md
@@ -193,6 +193,9 @@ If you are using Studio on a touchscreen computer running 
Windows 8 or later and
 
 ## Version 17.01/15.5-LTS
 
+Note: 17.01.1/15.5A-LTS replaces 17.01 due to changes to location of new 
add-on files.
+
+* 17.01.1/15.5A-LTS: Changed where updates are downloaded from for long-term 
support releases. Installing this version is mandatory.
 * Improved responsiveness and reliability when using the add-on to switch to 
Studio, either using focus to Studio command from other programs or when an 
encoder is connected and NVDA is told to switch to Studio when this happens. If 
Studio is minimized, Studio window will be shown as unavailable. If so, restore 
Studio window from system tray.
 * If editing carts while Cart Explorer is active, it is no longer necessary to 
reenter Cart Explorer to view updated cart assignments when Cart Edit mode is 
turned off. Consequently, Cart Explorer reentry message is no longer announced.
 * In add-on 15.5-LTS, corrected user interface presentation for SPL add-on 
settings dialog.
@@ -527,6 +530,6 @@ Version 4.0 supports SPL Studio 5.00 and later, with 3.x 
designed to provide som
 
 [2]: http://addons.nvda-project.org/files/get.php?file=spl-dev
 
-[3]: http://spl.nvda-kr.org/files/get.php?file=spl-lts7
+[3]: http://josephsl.net/files/nvdaaddons/get.php?file=spl-lts7
 
 [4]: https://github.com/josephsl/stationplaylist/wiki/SPLAddonGuide


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/e2a9ace7a735/
Changeset:   e2a9ace7a735
Branch:      None
User:        josephsl
Date:        2017-01-15 16:56:41+00:00
Summary:     Merged stable

Affected #:  2 files

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 3d1827f..2acd22a 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -37,7 +37,7 @@ _updatePickle = os.path.join(globalVars.appArgs.configPath, 
"splupdate.pickle")
 # Not all update channels are listed. The one not listed here is the default 
("stable" for this branch).
 channels={
        "stable":"http://addons.nvda-project.org/files/get.php?file=spl";,
-       "lts":"http://josephsl.net/files/nvdaaddons/get.php?file=spl-lts16";,
+       "lts":"http://www.josephsl.net/files/nvdaaddons/get.php?file=spl-lts16";,
        #"beta":"http://spl.nvda-kr.org/files/get.php?file=spl-beta";,
 }
 

diff --git a/readme.md b/readme.md
index d7786b1..3019fe4 100755
--- a/readme.md
+++ b/readme.md
@@ -530,6 +530,6 @@ Version 4.0 supports SPL Studio 5.00 and later, with 3.x 
designed to provide som
 
 [2]: http://addons.nvda-project.org/files/get.php?file=spl-dev
 
-[3]: http://josephsl.net/files/nvdaaddons/get.php?file=spl-lts7
+[3]: http://www.josephsl.net/files/nvdaaddons/get.php?file=spl-lts7
 
 [4]: https://github.com/josephsl/stationplaylist/wiki/SPLAddonGuide


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/fc6344772dbc/
Changeset:   fc6344772dbc
Branch:      None
User:        josephsl
Date:        2017-01-15 16:58:20+00:00
Summary:     Removed LTS branch from dev channels list

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 2acd22a..3191f1c 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -37,7 +37,6 @@ _updatePickle = os.path.join(globalVars.appArgs.configPath, 
"splupdate.pickle")
 # Not all update channels are listed. The one not listed here is the default 
("stable" for this branch).
 channels={
        "stable":"http://addons.nvda-project.org/files/get.php?file=spl";,
-       "lts":"http://www.josephsl.net/files/nvdaaddons/get.php?file=spl-lts16";,
        #"beta":"http://spl.nvda-kr.org/files/get.php?file=spl-beta";,
 }
 


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/7e80df8e8e38/
Changeset:   7e80df8e8e38
Branch:      None
User:        josephsl
Date:        2017-01-16 22:01:51+00:00
Summary:     Merge branch 'stable'

Affected #:  6 files

diff --git a/addon/doc/ar/readme.md b/addon/doc/ar/readme.md
index 83680a9..8358bef 100644
--- a/addon/doc/ar/readme.md
+++ b/addon/doc/ar/readme.md
@@ -1,4 +1,5 @@
-# StationPlaylist Studio #
+
+[[!meta title="StationPlaylist Studio"]]
 
 * مطورو الإضافة: Geoff Shang, Joseph Lee وآخرون
 * تحميل [الإصدار النهائي][1]
@@ -249,6 +250,20 @@ broadcast profiles.
 استخدم لمسة ب3 أصابع للانتقال لنمط اللمس, ثم استخدم أوامر اللمس المسرودة
 أعلاه لأداء المهام.
 
+## Version 17.01/15.5-LTS
+
+* Improved responsiveness and reliability when using the add-on to switch to
+  Studio, either using focus to Studio command from other programs or when
+  an encoder is connected and NVDA is told to switch to Studio when this
+  happens. If Studio is minimized, Studio window will be shown as
+  unavailable. If so, restore Studio window from system tray.
+* If editing carts while Cart Explorer is active, it is no longer necessary
+  to reenter Cart Explorer to view updated cart assignments when Cart Edit
+  mode is turned off. Consequently, Cart Explorer reentry message is no
+  longer announced.
+* In add-on 15.5-LTS, corrected user interface presentation for SPL add-on
+  settings dialog.
+
 ## Version 16.12.1
 
 * Corrected user interface presentation for SPL add-on settings dialog.

diff --git a/addon/doc/es/readme.md b/addon/doc/es/readme.md
index 72e3e18..ebfa84a 100644
--- a/addon/doc/es/readme.md
+++ b/addon/doc/es/readme.md
@@ -1,4 +1,5 @@
-# StationPlaylist Studio #
+
+[[!meta title="StationPlaylist Studio"]]
 
 * Autores: Geoff Shang, Joseph Lee y otros colaboradores
 * Descargar [Versión estable][1]
@@ -287,9 +288,26 @@ realizar algunas órdenes de Studio desde la pantalla 
táctil. Primero utiliza
 un toque con tres dedos para cambiar a modo SPL, entonces utiliza las
 órdenes táctiles listadas arriba para llevar a cabo tareas.
 
-## Version 16.12.1
-
-* Corrected user interface presentation for SPL add-on settings dialog.
+## Versión 17.01/15.5-LTS
+
+* Mejorada la respuesta y la fiabilidad al utilizar el complemento para
+  cambiar a Studio, o utilizando el foco para órdenes de Studio desde otros
+  programas o cuando un codificador está conectado y se le pide a NVDA que
+  cambie a Studio cuando esto ocurra. Si Studio se minimiza, la ventana de
+  Studio se mostrará como no disponible. Si es así, restaura la ventana de
+  Studio desde la bandeja del sistema.
+* Si se editan carts mientras el explorador de Cart está activado, ya no es
+  necesario reintroducir el explorador de Cart para ver las asignaciones de
+  cart actualizadas cuando el modo Edición de Cart se
+  desactive. Consecuentemente, el mensaje reintroducir explorador de Cart ya
+  no se anuncia.
+* En el complemento 15.5-LTS, se corrigió la presentación de la interfaz de
+  usuario para el diálogo Opciones del complemento SPL.
+
+## Versión 16.12.1
+
+* Corregida la presentación de la interfaz de usuario para el diálogo
+  Opciones del complemento SPL.
 * Traducciones actualizadas.
 
 ## Versión 16.12/15.4-LTS

diff --git a/addon/doc/fr/readme.md b/addon/doc/fr/readme.md
index c496723..99d5c8d 100644
--- a/addon/doc/fr/readme.md
+++ b/addon/doc/fr/readme.md
@@ -1,4 +1,5 @@
-# StationPlaylist Studio #
+
+[[!meta title="StationPlaylist Studio"]]
 
 * Auteurs: Geoff Shang, Joseph Lee et d'autres contributeurs.
 * Télécharger [version stable][1]
@@ -15,11 +16,12 @@ module complémentaire][4]. Pour les développeurs cherchant 
à savoir comment
 construire le module complémentaire, voir buildInstructions.txt situé à la
 racine du code source du module complémentaire du référentiel.
 
-IMPORTANT: This add-on requires NVDA 2015.3 or later and StationPlaylist
-Studio 5.00 or later. If you have installed NVDA 2016.1 or later on Windows
-8 and later, disable audio ducking mode. Also, add-on 8.0/16.10 requires
-Studio 5.10 and later, and for broadcasters using Studio 5.0x, a long-term
-support version (15.x) is available.
+IMPORTANT : Ce module complémentaire nécessite NVDA 2015.3 ou plus récent et
+StationPlaylist Studio 5.00 ou version ultérieure. Si vous avez installé
+NVDA 2016.1 ou version ultérieure sur Windows 8 et supérieur désactiver le
+Mode d'atténuation audio. En outre,le module complémentaire 8.0/16.10
+nécessite Studio 5.10 et ultérieure, et pour les diffusions utilisant Studio
+5.0x, une version prise en charge à long terme (15.x) est disponible.
 
 ## Raccourcis clavier
 
@@ -164,9 +166,10 @@ Les commandes disponibles sont :
 * R (Maj+E dans la disposition  de JAWS et Window-Eyes) : Enregistrer dans
   un fichier activé/désactivé.
 * Maj+R : Contrôle du balayage de la bibliothèque en cours.
-* S: Track starts (scheduled).
-* Shift+S: Time until selected track will play (track starts in).
-* T: Cart edit/insert mode on/off.
+* S : Piste débute (planifié).
+* Maj+S : Durée jusqu'à la piste sélectionnée qui va être jouer (piste
+  débute dans).
+* T : Mode édition/insertion chariot activé/désactivé.
 * U: temps de fonctionnement Studio.
 * Contrôle+Maj+U : Rechercher les mises à jour du module complémentaire.
 * W: Météo et température si configurée.
@@ -297,22 +300,44 @@ un écran tactile. Tout d'abord utiliser une tape à trois 
doigts pour
 basculer en mode SPL, puis utilisez les commandes tactile énumérées
 ci-dessus pour exécuter des commandes.
 
+## Version 17.01/15.5-LTS
+
+* Amélioration de la réactivité et de la fiabilité lors de l'utilisation du
+  module complémentaire pour basculer à Studio, en utilisant le focus sur la
+  commande Studio à partir d'autres programmes ou lorsqu'un encodeur est
+  connecté et NVDA est invité à basculer vers Studio lorsque cela se
+  produit. Si Studio est minimisé, la fenêtre Studio s'affichera comme
+  indisponible. Dans ce cas, restaurez la fenêtre Studio depuis la barre
+  d'état système.
+* Si vous modifier des chariots pendant que l'Explorateur de Chariot est
+  actif, il n'est plus nécessaire d'entrer à nouveau dans l'Explorateur de
+  Chariot pour afficher la mise à jour des assignations de chariot lorsque
+  le mode édition chariot est désactivé. Par conséquent, le message pour
+  entrer à nouveau dans l'Explorateur de Chariot n'est plus annoncé.
+* Dans le module complémentaire 15.5-LTS, correction de la présentation de
+  l'interface utilisateur pour le dialogue Paramètres module complémentaire
+  SPL.
+
 ## Version 16.12.1
 
-* Corrected user interface presentation for SPL add-on settings dialog.
+* Correction de la présentation de l'interface utilisateur pour le dialogue
+  Paramètres module complémentaire SPL.
 * Mises à jour des traductions.
 
 ## Version 16.12/15.4-LTS
 
-* More work on supporting Studio 5.20, including announcing cart insert mode
-  status (if turned on) from SPL Assistant layer (T).
-* Cart edit/insert mode toggle is no longer affected by message verbosity
-  nor status announcement type settings (this status will always be
-  announced via speech and/or braille).
-* It is no longer possible to add comments to timed break notes.
-* Support for Track Tool 5.20, including fixed an issue where wrong
-  information is announced when using Columns Explorer commands to announce
-  column information.
+* Plus de travail sur le support Studio 5.20, incluant l'annonce du statut
+  en mode insertion chariot (si celui-ci est activé) depuis la couche
+  Assistant SPL (T).
+* Le basculement du Mode édition/insertion chariot n'est plus affecté par
+  les paramètres de type verbosité du message ni par le statut (ce statut
+  sera toujours annoncé par la parole et / ou le braille).
+* Il n'est plus possible d'ajouter des commentaires aux notes de pause
+  temporisées.
+* Support pour l'Outil de Piste 5.20, incluant la résolution d'un problème
+  où des informations erronées sont annoncées lors de l'utilisation des
+  commandes dans l'Explorateur de Colonnes pour annoncer les informations
+  sur les colonnes.
 
 ## Version 16.11/15.3-LTS
 
@@ -337,12 +362,14 @@ ci-dessus pour exécuter des commandes.
 
 ## Version 8.0/16.10/15.0-LTS
 
-Version 8.0 (also known as 16.10) supports SPL Studio 5.10 and later, with
-15.0-LTS (formerly 7.x) designed to provide some new features from 8.0 for
-users using earlier versions of Studio. Unless otherwise noted, entries
-below apply to both 8.0 and 7.x. A warning dialog will be shown the first
-time you use add-on 8.0 with Studio 5.0x installed, asking you to use 15.x
-LTS version.
+La version 8.0 (également connu sous le nom de 16.10) prend en charge la
+version SPL Studio 5.10 et ultérieure, avec la 15.0-LTS (anciennement la
+7.x) conçu pour fournir de nouvelles fonctionnalités depuis la 8.0 pour les
+utilisateurs des versions antérieures de Studio. À moins que dans le cas
+contraire les rubriques ci-dessous s’appliquent à les deux, 8.0 et 7.x. Un
+dialogue d'avertissement apparaît la première fois que vous utilisez le
+module complémentaire 8.0 avec Studio 5.0x installé, vous demandant
+d’utiliser la version  15.x LTS.
 
 * Le Schéma de la version a changé pour refléter la version year.month au
   lieu de major.minor. Au cours de la période de transition (jusqu'au

diff --git a/addon/doc/gl/readme.md b/addon/doc/gl/readme.md
index 8ebec55..6d22cba 100644
--- a/addon/doc/gl/readme.md
+++ b/addon/doc/gl/readme.md
@@ -1,4 +1,5 @@
-# StationPlaylist Studio #
+
+[[!meta title="StationPlaylist Studio"]]
 
 * Autores: Geoff Shang, Joseph Lee e outros colaboradores
 * Descargar [versión estable][1]
@@ -279,9 +280,26 @@ realizar algunhas ordes do Studio dende a pantalla tactil. 
Primeiro usa un
 toque con tgres dedos para cambiar a modo SPL, logo usa as ordes tactiles
 listadas arriba para realizar ordes.
 
-## Version 16.12.1
-
-* Corrected user interface presentation for SPL add-on settings dialog.
+## Versión 17.01/15.5-LTS
+
+* Mellorada a resposta e a fiabilidade ao se usar o complemento para cambiar
+  ao Studio, ou usando o foco para ordes do Studio dende outros programas ou
+  cando un codificador está conectado e se lle pide ao NVDA que cambie ao
+  Studio cando esto ocurra. Se o Studio se minimiza, a ventá do Studio
+  amosarase como non dispoñible. Se é así, restaura a ventá do Studio dende
+  a bandexa do sistema.
+* Se se editan carts mentres o explorador de Cart está activado, xa non é
+  necesario reintroducir o explorador de Cart para ver as asignacións de
+  cart actualizadas cando o modo Edición de Cart se
+  desactive. Consecuentemente, a mensaxe reintroducir explorador de Cart xa
+  non se anuncia.
+* No complemento 15.5-LTS,  correxiuse a presentación da interfaz do usuario
+  para o diálogo de opcións do complemento SPL.
+
+## Versión 16.12.1
+
+* Correxida a presentación da interfaz do usuario para o diálogo de opcións
+  do complemento SPL.
 * Traducións actualizadas.
 
 ## Versión 16.12/15.4-LTS

diff --git a/addon/doc/hu/readme.md b/addon/doc/hu/readme.md
index 4e65bd7..16ba864 100644
--- a/addon/doc/hu/readme.md
+++ b/addon/doc/hu/readme.md
@@ -1,4 +1,5 @@
-# StationPlaylist Studio #
+
+[[!meta title="StationPlaylist Studio"]]
 
 * Készítők: Geoff Shang, Joseph Lee, és további közreműködők
 * Letöltés [Stabil verzió][1]
@@ -260,6 +261,20 @@ Amennyiben érintőképernyős számítógépen használja a 
Studiot Windows 8,
 parancsokat végrehajthat az érintőképernyőn is. Először 3 ujjas koppintással
 váltson SPL módra, és utána már használhatók az alább felsorolt parancsok.
 
+## Version 17.01/15.5-LTS
+
+* Improved responsiveness and reliability when using the add-on to switch to
+  Studio, either using focus to Studio command from other programs or when
+  an encoder is connected and NVDA is told to switch to Studio when this
+  happens. If Studio is minimized, Studio window will be shown as
+  unavailable. If so, restore Studio window from system tray.
+* If editing carts while Cart Explorer is active, it is no longer necessary
+  to reenter Cart Explorer to view updated cart assignments when Cart Edit
+  mode is turned off. Consequently, Cart Explorer reentry message is no
+  longer announced.
+* In add-on 15.5-LTS, corrected user interface presentation for SPL add-on
+  settings dialog.
+
 ## Version 16.12.1
 
 * Corrected user interface presentation for SPL add-on settings dialog.

diff --git a/addon/locale/fr/LC_MESSAGES/nvda.po 
b/addon/locale/fr/LC_MESSAGES/nvda.po
index 723e04d..2773fb9 100755
--- a/addon/locale/fr/LC_MESSAGES/nvda.po
+++ b/addon/locale/fr/LC_MESSAGES/nvda.po
@@ -8,14 +8,14 @@ msgstr ""
 "Project-Id-Version: StationPlaylist 4.1\n"
 "Report-Msgid-Bugs-To: nvda-translations@xxxxxxxxxxxxx\n"
 "POT-Creation-Date: \n"
-"PO-Revision-Date: 2016-11-21 07:53-0800\n"
+"PO-Revision-Date: 2016-12-20 13:07+0100\n"
 "Last-Translator: Rémy Ruiz <remyruiz@xxxxxxxxx>\n"
 "Language-Team: Rémy Ruiz <remyruiz@xxxxxxxxx>\n"
 "Language: fr\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 1.5.7\n"
+"X-Generator: Poedit 1.8.9\n"
 "X-Poedit-SourceCharset: UTF-8\n"
 
 #. Translators: Presented when only Track Tool is running (Track Dial requires 
Studio to be running as well).
@@ -144,7 +144,6 @@ msgid "Status: {name}"
 msgstr "Statut : {name}"
 
 #. Translators: The text of the help command in SPL Assistant layer.
-#, fuzzy
 msgid ""
 "After entering SPL Assistant, press:\n"
 "A: Automation.\n"
@@ -199,7 +198,7 @@ msgstr ""
 "Maj+R : Contrôle du balayage de la bibliothèque.\n"
 "S : Heure prévue pour la piste.\n"
 "Maj+S : Durée jusqu'à la piste sélectionnée qui va être jouer.\n"
-"T : Mode édition chariot.\n"
+"T : Mode édition/insertion chariot.\n"
 "U : Temps de fonctionnement Studio.\n"
 "W : Météo et température.\n"
 "Y : Modification de la playlist.\n"
@@ -212,7 +211,6 @@ msgstr ""
 "Maj+F1 : Ouvre le guide de l'utilisateur en ligne."
 
 #. Translators: The text of the help command in SPL Assistant layer when JFW 
layer is active.
-#, fuzzy
 msgid ""
 "After entering SPL Assistant, press:\n"
 "A: Automation.\n"
@@ -271,7 +269,7 @@ msgstr ""
 "Maj+R : Contrôle du balayage de la bibliothèque.\n"
 "S : Heure prévue pour la piste.\n"
 "Maj+S : Durée jusqu'à la piste sélectionnée qui va être jouer.\n"
-"T : Mode édition chariot.\n"
+"T : Mode édition/insertion chariot.\n"
 "U : Temps de fonctionnement Studio.\n"
 "W : Météo et température.\n"
 "Y : Modification de la playlist.\n"
@@ -284,7 +282,6 @@ msgstr ""
 "Maj+F1 : Ouvre le guide de l'utilisateur en ligne."
 
 #. Translators: The text of the help command in SPL Assistant layer when 
Window-Eyes layer is active.
-#, fuzzy
 msgid ""
 "After entering SPL Assistant, press:\n"
 "A: Automation.\n"
@@ -347,7 +344,7 @@ msgstr ""
 "Maj+R : Contrôle du balayage de la bibliothèque.\n"
 "S : Heure prévue pour la piste.\n"
 "Maj+S : Durée jusqu'à la piste sélectionnée qui va être jouer.\n"
-"T : Mode édition chariot.\n"
+"T : Mode édition/insertion chariot.\n"
 "U : Temps de fonctionnement Studio.\n"
 "W : Météo et température.\n"
 "Y : Modification de la playlist.\n"


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/fd7ebf4ae3bd/
Changeset:   fd7ebf4ae3bd
Branch:      None
User:        josephsl
Date:        2017-01-17 00:42:30+00:00
Summary:     Vertical column nav optimization: let column tracker be managed by 
the overlay class instead of exposing this in the app module.

in the app module, column nav tracker (the variable used to track which column 
a broadcaster is reviewing) should be handled by the overlay class. This 
prevents someone from changing column nav behavior by modifying the app module 
and allows the object (track item) itself to record which column one is 
consulting.
This is destined for add-on 17.04. A similar change will be made to track Tool 
app module.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 4069f79..d85628a 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -126,6 +126,9 @@ _SPLCategoryTones = {
 class SPLTrackItem(IAccessible):
        """A base class for providing utility scripts when track entries are 
focused, such as track dial."""
 
+       # Keep a record of which column is being looked at.
+       _curColumnNumber = None
+
        def initOverlayClass(self):
                # LTS: Take a greater role in assigning enhanced Columns 
Explorer command at the expense of limiting where this can be invoked.
                # 8.0: Just assign number row.
@@ -148,6 +151,8 @@ class SPLTrackItem(IAccessible):
                        return None
 
        def reportFocus(self):
+               # initialize column navigation tracker.
+               if self.__class__._curColumnNumber is None: 
self.__class__._curColumnNumber = 0
                # 7.0: Cache column header data structures if meeting track 
items for the first time.
                # It is better to do it while reporting focus, otherwise Python 
throws recursion limit exceeded error when initOverlayClass does this.
                if self.appModule._columnHeaders is None:
@@ -180,10 +185,10 @@ class SPLTrackItem(IAccessible):
                else:
                        self.appModule._announceColumnOnly = None
                        verticalColumnAnnounce = 
splconfig.SPLConfig["General"]["VerticalColumnAnnounce"]
-                       if verticalColumnAnnounce == "Status" or 
(verticalColumnAnnounce is None and self.appModule.SPLColNumber == 0):
+                       if verticalColumnAnnounce == "Status" or 
(verticalColumnAnnounce is None and self._curColumnNumber == 0):
                                self._leftmostcol()
                        else:
-                               
self.announceColumnContent(self.appModule.SPLColNumber if 
verticalColumnAnnounce is None else self.indexOf(verticalColumnAnnounce), 
header=verticalColumnAnnounce, reportStatus=self.name is not None)
+                               
self.announceColumnContent(self._curColumnNumber if verticalColumnAnnounce is 
None else self.indexOf(verticalColumnAnnounce), header=verticalColumnAnnounce, 
reportStatus=self.name is not None)
                # 7.0: Let the app module keep a reference to this track.
                self.appModule._focusedTrack = self
 
@@ -225,29 +230,29 @@ class SPLTrackItem(IAccessible):
        # Now the scripts.
 
        def script_nextColumn(self, gesture):
-               if (self.appModule.SPLColNumber+1) == 
self.appModule._columnHeaders.childCount:
+               if (self._curColumnNumber+1) == 
self.appModule._columnHeaders.childCount:
                        tones.beep(2000, 100)
                else:
-                       self.appModule.SPLColNumber +=1
-               self.announceColumnContent(self.appModule.SPLColNumber)
+                       self.__class__._curColumnNumber +=1
+               self.announceColumnContent(self._curColumnNumber)
 
        def script_prevColumn(self, gesture):
-               if self.appModule.SPLColNumber <= 0:
+               if self._curColumnNumber <= 0:
                        tones.beep(2000, 100)
                else:
-                       self.appModule.SPLColNumber -=1
-               if self.appModule.SPLColNumber == 0:
+                       self.__class__._curColumnNumber -=1
+               if self._curColumnNumber == 0:
                        self._leftmostcol()
                else:
-                       self.announceColumnContent(self.appModule.SPLColNumber)
+                       self.announceColumnContent(self._curColumnNumber)
 
        def script_firstColumn(self, gesture):
-               self.appModule.SPLColNumber = 0
+               self.__class__._curColumnNumber = 0
                self._leftmostcol()
 
        def script_lastColumn(self, gesture):
-               self.appModule.SPLColNumber = 
self.appModule._columnHeaders.childCount-1
-               self.announceColumnContent(self.appModule.SPLColNumber)
+               self.__class__._curColumnNumber = 
self.appModule._columnHeaders.childCount-1
+               self.announceColumnContent(self._curColumnNumber)
 
        # Track movement scripts.
        # Detects top/bottom of a playlist if told to do so.
@@ -270,6 +275,7 @@ class SPLTrackItem(IAccessible):
                        tones.beep(2000, 100)
                else:
                        self.appModule._announceColumnOnly = True
+                       newTrack._curColumnNumber = self._curColumnNumber
                        newTrack.setFocus(), newTrack.setFocus()
                        selectTrack(newTrack.IAccessibleChildID-1)
 
@@ -279,6 +285,7 @@ class SPLTrackItem(IAccessible):
                        tones.beep(2000, 100)
                else:
                        self.appModule._announceColumnOnly = True
+                       newTrack._curColumnNumber = self._curColumnNumber
                        newTrack.setFocus(), newTrack.setFocus()
                        selectTrack(newTrack.IAccessibleChildID-1)
 
@@ -665,8 +672,6 @@ class AppModule(appModuleHandler.AppModule):
        scanCount = 0
        # Prevent NVDA from announcing scheduled time multiple times.
        scheduledTimeCache = ""
-       # Track Dial (A.K.A. enhanced arrow keys)
-       SPLColNumber = 0
 
        # Automatically announce mic, line in, etc changes
        # These items are static text items whose name changes.
@@ -874,6 +879,8 @@ class AppModule(appModuleHandler.AppModule):
                        import globalPlugins.splUtils.encoders
                        globalPlugins.splUtils.encoders.cleanup()
                splconfig.saveConfig()
+               # reset column number for column navigation commands.
+               if self._focusedTrack: 
self._focusedTrack.__class__._curColumnNumber = None
                # Delete focused track reference.
                self._focusedTrack = None
                try:


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/71475c3dec87/
Changeset:   71475c3dec87
Branch:      None
User:        josephsl
Date:        2017-01-20 10:48:19+00:00
Summary:     17.02: Do not allow update download nor profile deletion to 
proceed if NVDA unexpectedly quits while the dialogs are opened.

Based on an issue with another add-on: if a dialog opens (such as new updates) 
and NVDA unexpectedly quits, the default choice will be invoked (update 
downloads, profile being deleted, etc.). Thus prevent this by setting 'no' 
button to be the default.

Affected #:  2 files

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index a80a8b2..c3c2ed3 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -579,7 +579,7 @@ class SPLConfigDialog(gui.SettingsDialog):
                        _("Are you sure you want to delete this profile? This 
cannot be undone."),
                        # Translators: The title of the confirmation dialog for 
deletion of a profile.
                        _("Confirm Deletion"),
-                       wx.YES | wx.NO | wx.ICON_QUESTION, self
+                       wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION, self
                ) == wx.NO:
                        return
                splconfig.SPLConfig.deleteProfile(name)

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 81f4001..af41dc9 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -153,6 +153,6 @@ def updateCheck(auto=False, continuous=False, 
confUpdateInterval=1):
        else: wx.CallAfter(getUpdateResponse, checkMessage, _("Studio add-on 
update"), updateURL)
 
 def getUpdateResponse(message, caption, updateURL):
-       if gui.messageBox(message, caption, wx.YES | wx.NO | wx.CANCEL | 
wx.CENTER | wx.ICON_QUESTION) == wx.YES:
+       if gui.messageBox(message, caption, wx.YES_NO | wx.NO_DEFAULT | 
wx.CANCEL | wx.CENTER | wx.ICON_QUESTION) == wx.YES:
                os.startfile(updateURL)
 


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/82f2ab7cfe98/
Changeset:   82f2ab7cfe98
Branch:      None
User:        josephsl
Date:        2017-01-22 20:38:37+00:00
Summary:     Merge branch '16.10.x'

Affected #:  7 files

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 454dbb1..1e7167b 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -484,7 +484,7 @@ class SPLConfigDialog(gui.SettingsDialog):
                        _("Are you sure you want to delete this profile? This 
cannot be undone."),
                        # Translators: The title of the confirmation dialog for 
deletion of a profile.
                        _("Confirm Deletion"),
-                       wx.YES | wx.NO | wx.ICON_QUESTION, self
+                       wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION, self
                ) == wx.NO:
                        return
                splconfig.SPLConfig.deleteProfile(name)

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 3191f1c..ff7b209 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -147,5 +147,5 @@ def updateCheck(auto=False, continuous=False, 
confUpdateInterval=1):
        else: wx.CallAfter(getUpdateResponse, checkMessage, _("Studio add-on 
update"), updateURL)
 
 def getUpdateResponse(message, caption, updateURL):
-       if gui.messageBox(message, caption, wx.YES | wx.NO | wx.CANCEL | 
wx.CENTER | wx.ICON_QUESTION) == wx.YES:
+       if gui.messageBox(message, caption, wx.YES_NO | wx.NO_DEFAULT | 
wx.CANCEL | wx.CENTER | wx.ICON_QUESTION) == wx.YES:
                os.startfile(updateURL)

diff --git a/addon/doc/ar/readme.md b/addon/doc/ar/readme.md
index 8358bef..8c41a3c 100644
--- a/addon/doc/ar/readme.md
+++ b/addon/doc/ar/readme.md
@@ -1,5 +1,4 @@
-
-[[!meta title="StationPlaylist Studio"]]
+# StationPlaylist Studio #
 
 * مطورو الإضافة: Geoff Shang, Joseph Lee وآخرون
 * تحميل [الإصدار النهائي][1]
@@ -252,6 +251,11 @@ broadcast profiles.
 
 ## Version 17.01/15.5-LTS
 
+Note: 17.01.1/15.5A-LTS replaces 17.01 due to changes to location of new
+add-on files.
+
+* 17.01.1/15.5A-LTS: Changed where updates are downloaded from for long-term
+  support releases. Installing this version is mandatory.
 * Improved responsiveness and reliability when using the add-on to switch to
   Studio, either using focus to Studio command from other programs or when
   an encoder is connected and NVDA is told to switch to Studio when this
@@ -857,6 +861,6 @@ for stable releases.
 
 [2]: http://addons.nvda-project.org/files/get.php?file=spl-dev [2]: ;
 
-[3]: http://spl.nvda-kr.org/files/get.php?file=spl-lts16
+[3]: http://josephsl.net/files/nvdaaddons/get.php?file=spl-lts16
 
 [4]: https://github.com/josephsl/stationplaylist/wiki/SPLAddonGuide

diff --git a/addon/doc/es/readme.md b/addon/doc/es/readme.md
index ebfa84a..1d53d76 100644
--- a/addon/doc/es/readme.md
+++ b/addon/doc/es/readme.md
@@ -1,5 +1,4 @@
-
-[[!meta title="StationPlaylist Studio"]]
+# StationPlaylist Studio #
 
 * Autores: Geoff Shang, Joseph Lee y otros colaboradores
 * Descargar [Versión estable][1]
@@ -290,6 +289,12 @@ un toque con tres dedos para cambiar a modo SPL, entonces 
utiliza las
 
 ## Versión 17.01/15.5-LTS
 
+Nota: 17.01.1/15.5A-LTS reemplaza a 17.01 debido a cambios de la
+localización de los ficheros nuevos del complemento.
+
+* 17.01.1/15.5A-LTS: se cambió de dónde se descargan las actualizaciones
+  para las versiones de soporte a largo plazo. Es obligatoria la instalación
+  de esta versión.
 * Mejorada la respuesta y la fiabilidad al utilizar el complemento para
   cambiar a Studio, o utilizando el foco para órdenes de Studio desde otros
   programas o cuando un codificador está conectado y se le pide a NVDA que
@@ -1014,6 +1019,6 @@ utilizando versiones anteriores de Studio.
 
 [2]: http://addons.nvda-project.org/files/get.php?file=spl-dev
 
-[3]: http://spl.nvda-kr.org/files/get.php?file=spl-lts16
+[3]: http://josephsl.net/files/nvdaaddons/get.php?file=spl-lts16
 
 [4]: https://github.com/josephsl/stationplaylist/wiki/SPLAddonGuide

diff --git a/addon/doc/fr/readme.md b/addon/doc/fr/readme.md
index 99d5c8d..27dbba1 100644
--- a/addon/doc/fr/readme.md
+++ b/addon/doc/fr/readme.md
@@ -1,5 +1,4 @@
-
-[[!meta title="StationPlaylist Studio"]]
+# StationPlaylist Studio #
 
 * Auteurs: Geoff Shang, Joseph Lee et d'autres contributeurs.
 * Télécharger [version stable][1]
@@ -302,6 +301,12 @@ ci-dessus pour exécuter des commandes.
 
 ## Version 17.01/15.5-LTS
 
+Remarque: 17.01.1/15.5A-LTS remplace la 17.01 en raison des changements
+apportés à l'emplacement des nouveaux fichiers du module complémentaire.
+
+* 17.01.1/15.5A-LTS: Modifié à partir duquel les mises à jour sont
+  téléchargées pour les versions prises en charges à long
+  terme. L'installation de cette version est obligatoire.
 * Amélioration de la réactivité et de la fiabilité lors de l'utilisation du
   module complémentaire pour basculer à Studio, en utilisant le focus sur la
   commande Studio à partir d'autres programmes ou lorsqu'un encodeur est
@@ -1080,6 +1085,6 @@ utilisateurs des versions antérieures de Studio.
 
 [2]: http://addons.nvda-project.org/files/get.php?file=spl-dev
 
-[3]: http://spl.nvda-kr.org/files/get.php?file=spl-lts16
+[3]: http://josephsl.net/files/nvdaaddons/get.php?file=spl-lts16
 
 [4]: https://github.com/josephsl/stationplaylist/wiki/SPLAddonGuide

diff --git a/addon/doc/gl/readme.md b/addon/doc/gl/readme.md
index 6d22cba..e918f45 100644
--- a/addon/doc/gl/readme.md
+++ b/addon/doc/gl/readme.md
@@ -1,5 +1,4 @@
-
-[[!meta title="StationPlaylist Studio"]]
+# StationPlaylist Studio #
 
 * Autores: Geoff Shang, Joseph Lee e outros colaboradores
 * Descargar [versión estable][1]
@@ -282,6 +281,12 @@ listadas arriba para realizar ordes.
 
 ## Versión 17.01/15.5-LTS
 
+Nota: 17.01.1/15.5A-LTS reemplaza a 17.01 debido aos cambios da localización
+dos ficheiros novos do complemento.
+
+* 17.01.1/15.5A-LTS: cambiouse de onde se descargan as actualizacións para
+  as versións de soporte a longo prazo. É obrigatoria a instalación desta
+  versión.
 * Mellorada a resposta e a fiabilidade ao se usar o complemento para cambiar
   ao Studio, ou usando o foco para ordes do Studio dende outros programas ou
   cando un codificador está conectado e se lle pide ao NVDA que cambie ao
@@ -988,6 +993,6 @@ utilicen versións anteriores de Studio.
 
 [2]: http://addons.nvda-project.org/files/get.php?file=spl-dev
 
-[3]: http://spl.nvda-kr.org/files/get.php?file=spl-lts16
+[3]: http://josephsl.net/files/nvdaaddons/get.php?file=spl-lts16
 
 [4]: https://github.com/josephsl/stationplaylist/wiki/SPLAddonGuide

diff --git a/addon/doc/hu/readme.md b/addon/doc/hu/readme.md
index 16ba864..f3aafb1 100644
--- a/addon/doc/hu/readme.md
+++ b/addon/doc/hu/readme.md
@@ -1,5 +1,4 @@
-
-[[!meta title="StationPlaylist Studio"]]
+# StationPlaylist Studio #
 
 * Készítők: Geoff Shang, Joseph Lee, és további közreműködők
 * Letöltés [Stabil verzió][1]
@@ -263,6 +262,11 @@ váltson SPL módra, és utána már használhatók az alább 
felsorolt parancso
 
 ## Version 17.01/15.5-LTS
 
+Note: 17.01.1/15.5A-LTS replaces 17.01 due to changes to location of new
+add-on files.
+
+* 17.01.1/15.5A-LTS: Changed where updates are downloaded from for long-term
+  support releases. Installing this version is mandatory.
 * Improved responsiveness and reliability when using the add-on to switch to
   Studio, either using focus to Studio command from other programs or when
   an encoder is connected and NVDA is told to switch to Studio when this
@@ -923,6 +927,6 @@ A kiegészítő 4.0 verziója a Studio 5.00 és későbbi 
kiadásait támogatja.
 
 [2]: http://addons.nvda-project.org/files/get.php?file=spl-dev
 
-[3]: http://spl.nvda-kr.org/files/get.php?file=spl-lts16
+[3]: http://josephsl.net/files/nvdaaddons/get.php?file=spl-lts16
 
 [4]: https://github.com/josephsl/stationplaylist/wiki/SPLAddonGuide


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/57a837a14289/
Changeset:   57a837a14289
Branch:      None
User:        josephsl
Date:        2017-01-23 06:07:55+00:00
Summary:     Code deduplication/optimization: just use one copy of layer helper.

Code duplication: layer helper is defined in both the app module and the global 
pulgin, and they are identical. Thus it is better to let the app module use the 
global plugin (not the other way around as it causes import error, and if 
fooled to ignore nonexist SPL window, causes bytecode to be generated when the 
app isn't running) in order to let just one version deal with both Controller 
and Assistant layers.

Affected #:  2 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index d85628a..24656e2 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -10,7 +10,6 @@
 
 # Minimum version: SPL 5.10, NvDA 2016.4.
 
-from functools import wraps
 import os
 import time
 import threading
@@ -38,20 +37,6 @@ import splupdate
 import addonHandler
 addonHandler.initTranslation()
 
-
-# The finally function for status announcement scripts in this module (source: 
Tyler Spivey's code).
-def finally_(func, final):
-       """Calls final after func, even if it fails."""
-       def wrap(f):
-               @wraps(f)
-               def new(*args, **kwargs):
-                       try:
-                               func(*args, **kwargs)
-                       finally:
-                               final()
-               return new
-       return wrap(final)
-
 # Make sure the broadcaster is running a compatible version.
 SPLMinVersion = "5.10"
 
@@ -1583,8 +1568,10 @@ class AppModule(appModuleHandler.AppModule):
                        return appModuleHandler.AppModule.getScript(self, 
gesture)
                script = appModuleHandler.AppModule.getScript(self, gesture)
                if not script:
-                       script = finally_(self.script_error, self.finish)
-               return finally_(script, self.finish)
+                       script = self.script_error
+               # Just use finally function from the global plugin to reduce 
code duplication.
+               import globalPlugins.splUtils
+               return globalPlugins.splUtils.finally_(script, self.finish)
 
        def finish(self):
                self.SPLAssistant = False

diff --git a/addon/globalPlugins/splUtils/__init__.py 
b/addon/globalPlugins/splUtils/__init__.py
index 15564ad..6a3db64 100755
--- a/addon/globalPlugins/splUtils/__init__.py
+++ b/addon/globalPlugins/splUtils/__init__.py
@@ -13,8 +13,7 @@ import winUser
 import addonHandler
 addonHandler.initTranslation()
 
-# Layer environment: same as the app module counterpart.
-
+# The finally function for status announcement scripts in this module (source: 
Tyler Spivey's code).
 def finally_(func, final):
        """Calls final after func, even if it fails."""
        def wrap(f):
@@ -80,7 +79,7 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin):
                        return globalPluginHandler.GlobalPlugin.getScript(self, 
gesture)
                script = globalPluginHandler.GlobalPlugin.getScript(self, 
gesture)
                if not script:
-                       script = finally_(self.script_error, self.finish)
+                       script = self.script_error
                return finally_(script, self.finish)
 
        def finish(self):


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/dae960baff1c/
Changeset:   dae960baff1c
Branch:      None
User:        josephsl
Date:        2017-01-26 19:40:22+00:00
Summary:     Update downloader (17.1-dev only): allow installed copy of nVDA to 
download updates on its own. re #20.

Using NVDA Core's update downloader facility, SPL add-on can check for and 
download updates on its won. However, for security reasons and for consistent 
user experience with that of NVDA Core, only installed copies will do this.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index ff7b209..45fcf35 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -6,10 +6,20 @@
 
 import os # Essentially, update download is no different than file downloads.
 import cPickle
+import threading
+import tempfile
+import hashlib
+import ctypes.wintypes
+import ssl
+import wx
+import shellapi
 import gui
 import wx
 import addonHandler
 import globalVars
+import updateCheck as coreUpdateCheck
+import config
+import winUser
 
 # Add-on manifest routine (credit: various add-on authors including Noelia 
Martinez).
 # Do not rely on using absolute path to open to manifest, as installation 
directory may change in a future NVDA Core version (highly unlikely, but...).
@@ -148,4 +158,158 @@ def updateCheck(auto=False, continuous=False, 
confUpdateInterval=1):
 
 def getUpdateResponse(message, caption, updateURL):
        if gui.messageBox(message, caption, wx.YES_NO | wx.NO_DEFAULT | 
wx.CANCEL | wx.CENTER | wx.ICON_QUESTION) == wx.YES:
-               os.startfile(updateURL)
+               SPLUpdateDownloader([updateURL]).start() if 
config.isInstalledCopy() else os.startfile(updateURL)
+
+# Update downloader (credit: NV Access)
+# Customized for SPL add-on.
+
+#: The download block size in bytes.
+DOWNLOAD_BLOCK_SIZE = 8192 # 8 kb
+
+def checkForUpdate(auto=False):
+       """Check for an updated version of NVDA.
+       This will block, so it generally shouldn't be called from the main 
thread.
+       @param auto: Whether this is an automatic check for updates.
+       @type auto: bool
+       @return: Information about the update or C{None} if there is no update.
+       @rtype: dict
+       @raise RuntimeError: If there is an error checking for an update.
+       """
+       params = {
+               "autoCheck": auto,
+               "version": versionInfo.version,
+               "versionType": versionInfo.updateVersionType,
+               "osVersion": winVersion.winVersionText,
+               "x64": os.environ.get("PROCESSOR_ARCHITEW6432") == "AMD64",
+               "language": languageHandler.getLanguage(),
+               "installed": config.isInstalledCopy(),
+       }
+       url = "%s?%s" % (CHECK_URL, urllib.urlencode(params))
+       try:
+               res = urllib.urlopen(url)
+       except IOError as e:
+               if isinstance(e.strerror, ssl.SSLError) and e.strerror.reason 
== "CERTIFICATE_VERIFY_FAILED":
+                       # #4803: Windows fetches trusted root certificates on 
demand.
+                       # Python doesn't trigger this fetch 
(PythonIssue:20916), so try it ourselves
+                       _updateWindowsRootCertificates()
+                       # and then retry the update check.
+                       res = urllib.urlopen(url)
+               else:
+                       raise
+       if res.code != 200:
+               raise RuntimeError("Checking for update failed with code %d" % 
res.code)
+       info = {}
+       for line in res:
+               line = line.rstrip()
+               try:
+                       key, val = line.split(": ", 1)
+               except ValueError:
+                       raise RuntimeError("Error in update check output")
+               info[key] = val
+       if not info:
+               return None
+       return info
+
+
+class SPLUpdateDownloader(coreUpdateCheck.UpdateDownloader):
+       """Overrides NVDA Core's downloader.)
+       No hash checking for now, and URL's and temp file paths are different.
+       """
+
+       def __init__(self, urls, fileHash=None):
+               """Constructor.
+               @param urls: URLs to try for the update file.
+               @type urls: list of str
+               @param fileHash: The SHA-1 hash of the file as a hex string.
+               @type fileHash: basestring
+               """
+               super(SPLUpdateDownloader, self).__init__(urls, fileHash)
+               self.urls = urls
+               self.destPath = 
tempfile.mktemp(prefix="stationPlaylist_update-", suffix=".nvda-addon")
+               self.fileHash = fileHash
+
+       def start(self):
+               """Start the download.
+               """
+               self._shouldCancel = False
+               # Use a timer because timers aren't re-entrant.
+               self._guiExecTimer = wx.PyTimer(self._guiExecNotify)
+               gui.mainFrame.prePopup()
+               # Translators: The title of the dialog displayed while 
downloading add-on update.
+               self._progressDialog = wx.ProgressDialog(_("Downloading Add-on 
Update"),
+                       # Translators: The progress message indicating that a 
connection is being established.
+                       _("Connecting"),
+                       # PD_AUTO_HIDE is required because 
ProgressDialog.Update blocks at 100%
+                       # and waits for the user to press the Close button.
+                       style=wx.PD_CAN_ABORT | wx.PD_ELAPSED_TIME | 
wx.PD_REMAINING_TIME | wx.PD_AUTO_HIDE,
+                       parent=gui.mainFrame)
+               self._progressDialog.Raise()
+               t = threading.Thread(target=self._bg)
+               t.daemon = True
+               t.start()
+
+       def _error(self):
+               self._stopped()
+               gui.messageBox(
+                       # Translators: A message indicating that an error 
occurred while downloading an update to NVDA.
+                       _("Error downloading add-on update."),
+                       _("Error"),
+                       wx.OK | wx.ICON_ERROR)
+
+       def _downloadSuccess(self):
+               self._stopped()
+               # Translators: The message presented when the update has been 
successfully downloaded
+               # and is about to be installed.
+               gui.messageBox(_("Add-on update downloaded. It will now be 
installed."),
+                       # Translators: The title of the dialog displayed when 
the update is about to be installed.
+                       _("Install Add-on Update"))
+               # #4475: ensure that the new process shows its first window, by 
providing SW_SHOWNORMAL
+               shellapi.ShellExecute(None, None,
+                       self.destPath.decode("mbcs"),
+                       None, None, winUser.SW_SHOWNORMAL)
+
+
+# These structs are only complete enough to achieve what we need.
+class CERT_USAGE_MATCH(ctypes.Structure):
+       _fields_ = (
+               ("dwType", ctypes.wintypes.DWORD),
+               # CERT_ENHKEY_USAGE struct
+               ("cUsageIdentifier", ctypes.wintypes.DWORD),
+               ("rgpszUsageIdentifier", ctypes.c_void_p), # LPSTR *
+       )
+
+class CERT_CHAIN_PARA(ctypes.Structure):
+       _fields_ = (
+               ("cbSize", ctypes.wintypes.DWORD),
+               ("RequestedUsage", CERT_USAGE_MATCH),
+               ("RequestedIssuancePolicy", CERT_USAGE_MATCH),
+               ("dwUrlRetrievalTimeout", ctypes.wintypes.DWORD),
+               ("fCheckRevocationFreshnessTime", ctypes.wintypes.BOOL),
+               ("dwRevocationFreshnessTime", ctypes.wintypes.DWORD),
+               ("pftCacheResync", ctypes.c_void_p), # LPFILETIME
+               ("pStrongSignPara", ctypes.c_void_p), # PCCERT_STRONG_SIGN_PARA
+               ("dwStrongSignFlags", ctypes.wintypes.DWORD),
+       )
+
+# Borrowed from NVDA Core (the only difference is the URL).
+def _updateWindowsRootCertificates():
+       crypt = ctypes.windll.crypt32
+       # Get the server certificate.
+       sslCont = ssl._create_unverified_context()
+       u = urllib.urlopen("https://www.nvaccess.org/nvdaUpdateCheck", ;
context=sslCont)
+       cert = u.fp._sock.getpeercert(True)
+       u.close()
+       # Convert to a form usable by Windows.
+       certCont = crypt.CertCreateCertificateContext(
+               0x00000001, # X509_ASN_ENCODING
+               cert,
+               len(cert))
+       # Ask Windows to build a certificate chain, thus triggering a root 
certificate update.
+       chainCont = ctypes.c_void_p()
+       crypt.CertGetCertificateChain(None, certCont, None, None,
+               
ctypes.byref(CERT_CHAIN_PARA(cbSize=ctypes.sizeof(CERT_CHAIN_PARA),
+                       RequestedUsage=CERT_USAGE_MATCH())),
+               0, None,
+               ctypes.byref(chainCont))
+       crypt.CertFreeCertificateChain(chainCont)
+       crypt.CertFreeCertificateContext(certCont)


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/27d64877c636/
Changeset:   27d64877c636
Branch:      None
User:        josephsl
Date:        2017-01-27 02:48:28+00:00
Summary:     Update downloader (17.1-dev): Change splupdate.updateCheck to 
updateChecker to avoid name clashes with nVDA Core's own update check module.

Affected #:  3 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 24656e2..2f2a239 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -2004,7 +2004,7 @@ class AppModule(appModuleHandler.AppModule):
                        _("Add-on update check"),
                        # Translators: The message displayed while checking for 
newer version of Studio add-on.
                        _("Checking for new version of Studio add-on..."))
-               threading.Thread(target=splupdate.updateCheck, 
kwargs={"continuous":splconfig.SPLConfig["Update"]["AutoUpdateCheck"], 
"confUpdateInterval":splconfig.SPLConfig["Update"]["UpdateInterval"]}).start()
+               threading.Thread(target=splupdate.updateChecker, 
kwargs={"continuous":splconfig.SPLConfig["Update"]["AutoUpdateCheck"], 
"confUpdateInterval":splconfig.SPLConfig["Update"]["UpdateInterval"]}).start()
 
 
        __SPLAssistantGestures={

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 313e7f8..77f25ca 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -832,14 +832,14 @@ def triggerProfileSwitch():
 # Its only job is to call the update check function (splupdate) with the auto 
check enabled.
 # The update checker will not be engaged if an instant switch profile is 
active or it is not time to check for it yet (check will be done every 24 
hours).
 def autoUpdateCheck():
-       splupdate.updateCheck(auto=True, 
continuous=SPLConfig["Update"]["AutoUpdateCheck"], 
confUpdateInterval=SPLConfig["Update"]["UpdateInterval"])
+       splupdate.updateChecker(auto=True, 
continuous=SPLConfig["Update"]["AutoUpdateCheck"], 
confUpdateInterval=SPLConfig["Update"]["UpdateInterval"])
 
 # The timer itself.
 # A bit simpler than NVDA Core's auto update checker.
 def updateInit():
        # LTS: Launch updater if channel change is detected.
        if splupdate._updateNow:
-               splupdate.updateCheck(auto=True) # No repeat here.
+               splupdate.updateChecker(auto=True) # No repeat here.
                splupdate._SPLUpdateT = wx.PyTimer(autoUpdateCheck)
                splupdate._updateNow = False
                return
@@ -850,7 +850,7 @@ def updateInit():
        elif splupdate.SPLAddonCheck < nextCheck < currentTime:
                interval = SPLConfig["Update"]["UpdateInterval"]* 86400
                # Call the update check now.
-               splupdate.updateCheck(auto=True) # No repeat here.
+               splupdate.updateChecker(auto=True) # No repeat here.
        splupdate._SPLUpdateT = wx.PyTimer(autoUpdateCheck)
        splupdate._SPLUpdateT.Start(interval * 1000, True)
 

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 45fcf35..369553d 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -17,7 +17,7 @@ import gui
 import wx
 import addonHandler
 import globalVars
-import updateCheck as coreUpdateCheck
+import updateCheck
 import config
 import winUser
 
@@ -95,7 +95,7 @@ _progressDialog = None
 # The update check routine.
 # Auto is whether to respond with UI (manual check only), continuous takes in 
auto update check variable for restarting the timer.
 # ConfUpdateInterval comes from add-on config dictionary.
-def updateCheck(auto=False, continuous=False, confUpdateInterval=1):
+def updateChecker(auto=False, continuous=False, confUpdateInterval=1):
        if _pendingChannelChange:
                wx.CallAfter(gui.messageBox, _("Did you recently tell SPL 
add-on to use a different update channel? If so, please restart NVDA before 
checking for add-on updates."), _("Update channel changed"), wx.ICON_ERROR)
                return
@@ -211,7 +211,7 @@ def checkForUpdate(auto=False):
        return info
 
 
-class SPLUpdateDownloader(coreUpdateCheck.UpdateDownloader):
+class SPLUpdateDownloader(updateCheck.UpdateDownloader):
        """Overrides NVDA Core's downloader.)
        No hash checking for now, and URL's and temp file paths are different.
        """


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/169ae5a8c61b/
Changeset:   169ae5a8c61b
Branch:      None
User:        josephsl
Date:        2017-01-28 17:59:50+00:00
Summary:     Merged stable

Affected #:  0 files



https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/80f5e7abd3eb/
Changeset:   80f5e7abd3eb
Branch:      None
User:        josephsl
Date:        2017-01-28 18:08:41+00:00
Summary:     Update channel selection: add channels attribute to advanced 
options dialog, useful for specifying what channels are available from which 
release.

In case try build will gain ability to switch to development channel: use 
update channels tuple from advanced options dialog to select release channels. 
This will not be the case if a release supports one channel.
This is destined for add-on 17.04 and later.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 1e7167b..1b9434e 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -1299,6 +1299,9 @@ class SayStatusDialog(wx.Dialog):
 # 7.0: Auto update check will be configurable from this dialog.
 class AdvancedOptionsDialog(wx.Dialog):
 
+       # Available channels (if there's only one, channel selection list will 
not be shown).
+       _updateChannels = ("dev", "stable")
+
        def __init__(self, parent):
                # Translators: The title of a dialog to configure advanced SPL 
add-on options such as update checking.
                super(AdvancedOptionsDialog, self).__init__(parent, 
title=_("Advanced options"))
@@ -1311,12 +1314,12 @@ class AdvancedOptionsDialog(wx.Dialog):
                self.autoUpdateCheckbox.SetValue(self.Parent.autoUpdateCheck)
                # Translators: The label for a setting in SPL add-on 
settings/advanced options to select automatic update interval in days.
                
self.updateInterval=advOptionsHelper.addLabeledControl(_("Update &interval in 
days"), gui.nvdaControls.SelectOnFocusSpinCtrl, min=1, max=30, 
initial=parent.updateInterval)
-               # LTS and 8.x only.
-               # Translators: The label for a combo box to select update 
channel.
-               labelText = _("&Add-on update channel:")
-               self.channels=advOptionsHelper.addLabeledControl(labelText, 
wx.Choice, choices=["development", "stable"])
-               self.updateChannels = ("dev", "stable")
-               
self.channels.SetSelection(self.updateChannels.index(self.Parent.updateChannel))
+               # For releases that support channel switching.
+               if len(self._updateChannels) > 1:
+                       # Translators: The label for a combo box to select 
update channel.
+                       labelText = _("&Add-on update channel:")
+                       
self.channels=advOptionsHelper.addLabeledControl(labelText, wx.Choice, 
choices=["development", "stable"])
+                       
self.channels.SetSelection(self._updateChannels.index(self.Parent.updateChannel))
                # Translators: A checkbox to toggle if SPL Controller command 
can be used to invoke Assistant layer.
                
self.splConPassthroughCheckbox=advOptionsHelper.addItem(wx.CheckBox(self, 
label=_("Allow SPL C&ontroller command to invoke SPL Assistant layer")))
                
self.splConPassthroughCheckbox.SetValue(self.Parent.splConPassthrough)
@@ -1347,7 +1350,7 @@ class AdvancedOptionsDialog(wx.Dialog):
                parent.compLayer = 
self.compatibilityLayouts[self.compatibilityList.GetSelection()][0]
                parent.autoUpdateCheck = self.autoUpdateCheckbox.Value
                parent.updateInterval = self.updateInterval.Value
-               parent.updateChannel = ("dev", 
"stable")[self.channels.GetSelection()]
+               if len(self._updateChannels) > 1: parent.updateChannel = 
self.updateChannels[self.channels.GetSelection()]
                parent.profiles.SetFocus()
                parent.Enable()
                self.Destroy()


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/25c6ad9dee7e/
Changeset:   25c6ad9dee7e
Branch:      None
User:        josephsl
Date:        2017-01-28 18:09:27+00:00
Summary:     Merge branch 'master' into updateDownloader

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 1e7167b..1b9434e 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -1299,6 +1299,9 @@ class SayStatusDialog(wx.Dialog):
 # 7.0: Auto update check will be configurable from this dialog.
 class AdvancedOptionsDialog(wx.Dialog):
 
+       # Available channels (if there's only one, channel selection list will 
not be shown).
+       _updateChannels = ("dev", "stable")
+
        def __init__(self, parent):
                # Translators: The title of a dialog to configure advanced SPL 
add-on options such as update checking.
                super(AdvancedOptionsDialog, self).__init__(parent, 
title=_("Advanced options"))
@@ -1311,12 +1314,12 @@ class AdvancedOptionsDialog(wx.Dialog):
                self.autoUpdateCheckbox.SetValue(self.Parent.autoUpdateCheck)
                # Translators: The label for a setting in SPL add-on 
settings/advanced options to select automatic update interval in days.
                
self.updateInterval=advOptionsHelper.addLabeledControl(_("Update &interval in 
days"), gui.nvdaControls.SelectOnFocusSpinCtrl, min=1, max=30, 
initial=parent.updateInterval)
-               # LTS and 8.x only.
-               # Translators: The label for a combo box to select update 
channel.
-               labelText = _("&Add-on update channel:")
-               self.channels=advOptionsHelper.addLabeledControl(labelText, 
wx.Choice, choices=["development", "stable"])
-               self.updateChannels = ("dev", "stable")
-               
self.channels.SetSelection(self.updateChannels.index(self.Parent.updateChannel))
+               # For releases that support channel switching.
+               if len(self._updateChannels) > 1:
+                       # Translators: The label for a combo box to select 
update channel.
+                       labelText = _("&Add-on update channel:")
+                       
self.channels=advOptionsHelper.addLabeledControl(labelText, wx.Choice, 
choices=["development", "stable"])
+                       
self.channels.SetSelection(self._updateChannels.index(self.Parent.updateChannel))
                # Translators: A checkbox to toggle if SPL Controller command 
can be used to invoke Assistant layer.
                
self.splConPassthroughCheckbox=advOptionsHelper.addItem(wx.CheckBox(self, 
label=_("Allow SPL C&ontroller command to invoke SPL Assistant layer")))
                
self.splConPassthroughCheckbox.SetValue(self.Parent.splConPassthrough)
@@ -1347,7 +1350,7 @@ class AdvancedOptionsDialog(wx.Dialog):
                parent.compLayer = 
self.compatibilityLayouts[self.compatibilityList.GetSelection()][0]
                parent.autoUpdateCheck = self.autoUpdateCheckbox.Value
                parent.updateInterval = self.updateInterval.Value
-               parent.updateChannel = ("dev", 
"stable")[self.channels.GetSelection()]
+               if len(self._updateChannels) > 1: parent.updateChannel = 
self.updateChannels[self.channels.GetSelection()]
                parent.profiles.SetFocus()
                parent.Enable()
                self.Destroy()


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/c88ba82d4f53/
Changeset:   c88ba82d4f53
Branch:      None
User:        josephsl
Date:        2017-01-28 18:15:50+00:00
Summary:     Update downloader: allow portable copy to download add-ons in the 
background, add-on download success message is no longer displayed.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 369553d..403c212 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -158,7 +158,7 @@ def updateChecker(auto=False, continuous=False, 
confUpdateInterval=1):
 
 def getUpdateResponse(message, caption, updateURL):
        if gui.messageBox(message, caption, wx.YES_NO | wx.NO_DEFAULT | 
wx.CANCEL | wx.CENTER | wx.ICON_QUESTION) == wx.YES:
-               SPLUpdateDownloader([updateURL]).start() if 
config.isInstalledCopy() else os.startfile(updateURL)
+               SPLUpdateDownloader([updateURL]).start()
 
 # Update downloader (credit: NV Access)
 # Customized for SPL add-on.
@@ -258,15 +258,8 @@ class SPLUpdateDownloader(updateCheck.UpdateDownloader):
 
        def _downloadSuccess(self):
                self._stopped()
-               # Translators: The message presented when the update has been 
successfully downloaded
-               # and is about to be installed.
-               gui.messageBox(_("Add-on update downloaded. It will now be 
installed."),
-                       # Translators: The title of the dialog displayed when 
the update is about to be installed.
-                       _("Install Add-on Update"))
-               # #4475: ensure that the new process shows its first window, by 
providing SW_SHOWNORMAL
-               shellapi.ShellExecute(None, None,
-                       self.destPath.decode("mbcs"),
-                       None, None, winUser.SW_SHOWNORMAL)
+               from gui import addonGui
+               wx.CallAfter(addonGui.AddonsDialog.handleRemoteAddonInstall, 
self.destPath.decode("mbcs"))
 
 
 # These structs are only complete enough to achieve what we need.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/b660537a6856/
Changeset:   b660537a6856
Branch:      None
User:        josephsl
Date:        2017-01-31 05:49:52+00:00
Summary:     Readme entry for add-on updates. fixes #20

Affected #:  1 file

diff --git a/readme.md b/readme.md
index 3019fe4..ea471b7 100755
--- a/readme.md
+++ b/readme.md
@@ -177,6 +177,7 @@ If you are using Studio on a touchscreen computer running 
Windows 8 or later and
 ## Version 17.04-dev
 
 * Improvements to presentation of various add-on dialogs thanks to NVDA 2016.4 
features.
+* NVDA will download add-on updates in the background if you say "yes" when 
asked to update the add-on. Consequently, file download notifications from web 
browsers will no longer be shown.
 * Added ability to press Control+Alt+up or down arrow keys to move between 
tracks (specifically, track columns) vertically just as one is moving to next 
or previous row in a table.
 * Added a combo box in add-on settings dialog to set which column should be 
announced when moving through columns vertically.
 * Moved end of track , intro and microphone alarm controls from add-on 
settings to the new Alarms Center.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/ed8eaf7a87d7/
Changeset:   ed8eaf7a87d7
Branch:      None
User:        josephsl
Date:        2017-01-31 15:42:23+00:00
Summary:     Oops, nonexistent variable should not block channel changes from 
being saved.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 1b9434e..95b7250 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -1350,7 +1350,7 @@ class AdvancedOptionsDialog(wx.Dialog):
                parent.compLayer = 
self.compatibilityLayouts[self.compatibilityList.GetSelection()][0]
                parent.autoUpdateCheck = self.autoUpdateCheckbox.Value
                parent.updateInterval = self.updateInterval.Value
-               if len(self._updateChannels) > 1: parent.updateChannel = 
self.updateChannels[self.channels.GetSelection()]
+               if len(self._updateChannels) > 1: parent.updateChannel = 
self._updateChannels[self.channels.GetSelection()]
                parent.profiles.SetFocus()
                parent.Enable()
                self.Destroy()


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/6ce76569e1fb/
Changeset:   6ce76569e1fb
Branch:      None
User:        josephsl
Date:        2017-02-01 02:01:02+00:00
Summary:     Update check addresses are now HTTPS.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 403c212..83601ca 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -31,7 +31,7 @@ SPLAddonCheck = 0
 # Update metadata storage.
 SPLAddonState = {}
 # Update URL (the only way to change it is installing a different version from 
a different branch).
-SPLUpdateURL = "http://addons.nvda-project.org/files/get.php?file=spl-dev";
+SPLUpdateURL = "https://addons.nvda-project.org/files/get.php?file=spl-dev";
 _pendingChannelChange = False
 _updateNow = False
 SPLUpdateChannel = "dev"
@@ -46,7 +46,7 @@ _updatePickle = os.path.join(globalVars.appArgs.configPath, 
"splupdate.pickle")
 
 # Not all update channels are listed. The one not listed here is the default 
("stable" for this branch).
 channels={
-       "stable":"http://addons.nvda-project.org/files/get.php?file=spl";,
+       "stable":"https://addons.nvda-project.org/files/get.php?file=spl";,
        #"beta":"http://spl.nvda-kr.org/files/get.php?file=spl-beta";,
 }
 
@@ -289,7 +289,7 @@ def _updateWindowsRootCertificates():
        crypt = ctypes.windll.crypt32
        # Get the server certificate.
        sslCont = ssl._create_unverified_context()
-       u = urllib.urlopen("https://www.nvaccess.org/nvdaUpdateCheck", ;
context=sslCont)
+       u = urllib.urlopen("https://addons.nvda-project.org", context=sslCont)
        cert = u.fp._sock.getpeercert(True)
        u.close()
        # Convert to a form usable by Windows.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/7e6eaf4d60f4/
Changeset:   7e6eaf4d60f4
Branch:      None
User:        josephsl
Date:        2017-02-01 03:22:57+00:00
Summary:     Update check dictionary (17.04/17.2-dev): fetch add-on update info 
via a dictionary, similar to NVDA Core does it. re #21.

NVDA Core uses a dictionary to pass update info between connection routine and 
update GUI. Thus emulate this in SPL add-on via a new experimental function. 
Due to potential for regression, only parts of it will make it to 17.04, the 
rest will be incorporated into 17.2-dev later in 2017.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 83601ca..f8df6c5 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -80,25 +80,92 @@ def terminate():
                cPickle.dump(SPLAddonState, file(_updatePickle, "wb"))
        SPLAddonState = None
 
-def updateQualify(url):
+def checkForAddonUpdate():
+       import urllib
+       updateURL = SPLUpdateURL if SPLUpdateChannel not in channels else 
channels[SPLUpdateChannel]
+       try:
+               # Look up the channel if different from the default.
+               res = urllib.urlopen(updateURL)
+               res.close()
+       except IOError as e:
+               # NVDA Core 2015.1 and later.
+               if isinstance(e.strerror, ssl.SSLError) and e.strerror.reason 
== "CERTIFICATE_VERIFY_FAILED":
+                       _updateWindowsRootCertificates()
+                       res = urllib.urlopen(updateURL)
+               else:
+                       raise
+       if res.code != 200:
+               raise RuntimeError("Checking for update failed with code %d" % 
res.code)
+       # Build emulated add-on update dictionary if there is indeed a new 
verison.
        # The add-on version is of the form "x.y.z". The "-dev" suffix 
indicates development release.
        # Anything after "-dev" indicates a try or a custom build.
        # LTS: Support upgrading between LTS releases.
        # 7.0: Just worry about version label differences (suggested by Jamie 
Teh from NV Access).
        # 17.04: Version is of the form year.month.revision, and regular 
expression will be employed (looks cleaner).
        import re
-       version = re.search("stationPlaylist-(?P<version>.*).nvda-addon", 
url.url).groupdict()["version"]
-       return None if version == SPLAddonVersion else version
+       version = re.search("stationPlaylist-(?P<version>.*).nvda-addon", 
res.url).groupdict()["version"]
+       if version != SPLAddonVersion:
+               return {"curVersion": SPLAddonVersion, "newVersion": version, 
"path": res.url}
+       return None
 
 _progressDialog = None
+updateDictionary = True
 
 # The update check routine.
 # Auto is whether to respond with UI (manual check only), continuous takes in 
auto update check variable for restarting the timer.
 # ConfUpdateInterval comes from add-on config dictionary.
+def updateCheckerEx(auto=False, continuous=False, confUpdateInterval=1):
+       global _SPLUpdateT, SPLAddonCheck, _retryAfterFailure, _progressDialog, 
_updateNow
+       if _updateNow: _updateNow = False
+       import time
+       from logHandler import log
+       # Regardless of whether it is an auto check, update the check time.
+       # However, this shouldnt' be done if this is a retry after a failed 
attempt.
+       if not _retryAfterFailure: SPLAddonCheck = time.time()
+       updateInterval = confUpdateInterval*_updateInterval*1000
+       # Should the timer be set again?
+       if continuous and not _retryAfterFailure: 
_SPLUpdateT.Start(updateInterval, True)
+       # Auto disables UI portion of this function if no updates are pending.
+       try:
+               info = checkForAddonUpdate()
+       except:
+               log.debugWarning("Error checking for update", exc_info=True)
+               _retryAfterFailure = True
+               if not auto:
+                       wx.CallAfter(_progressDialog.done)
+                       _progressDialog = None
+                       # Translators: Error text shown when add-on update 
check fails.
+                       wx.CallAfter(gui.messageBox, _("Error checking for 
update."), _("Studio add-on update"), wx.ICON_ERROR)
+               if continuous: _SPLUpdateT.Start(600000, True)
+               return
+       if _retryAfterFailure:
+               _retryAfterFailure = False
+               # Now is the time to update the check time if this is a retry.
+               SPLAddonCheck = time.time()
+       if info is None:
+               if auto:
+                       if continuous: _SPLUpdateT.Start(updateInterval, True)
+                       return # No need to interact with the user.
+               # Translators: Presented when no add-on update is available.
+               checkMessage = _("No add-on update available.")
+       else:
+               # Translators: Text shown if an add-on update is available.
+               checkMessage = _("Studio add-on {newVersion} is available. 
Would you like to update?").format(newVersion = info["newVersion"])
+               updateCandidate = True
+       if not auto:
+               wx.CallAfter(_progressDialog.done)
+               _progressDialog = None
+       # Translators: Title of the add-on update check dialog.
+       if not updateCandidate: wx.CallAfter(gui.messageBox, checkMessage, 
_("Studio add-on update"))
+       else: wx.CallAfter(getUpdateResponse, checkMessage, _("Studio add-on 
update"), info["path"])
+
 def updateChecker(auto=False, continuous=False, confUpdateInterval=1):
        if _pendingChannelChange:
                wx.CallAfter(gui.messageBox, _("Did you recently tell SPL 
add-on to use a different update channel? If so, please restart NVDA before 
checking for add-on updates."), _("Update channel changed"), wx.ICON_ERROR)
                return
+       if updateDictionary:
+               updateCheckerEx(auto=auto, continuous=continuous, 
confUpdateInterval=confUpdateInterval)
+               return
        global _SPLUpdateT, SPLAddonCheck, _retryAfterFailure, _progressDialog, 
_updateNow
        if _updateNow: _updateNow = False
        import time
@@ -138,8 +205,9 @@ def updateChecker(auto=False, continuous=False, 
confUpdateInterval=1):
                checkMessage = _("Add-on update check failed.")
        else:
                # Am I qualified to update?
-               qualified = updateQualify(url)
-               if qualified is None:
+               import re
+               version = 
re.search("stationPlaylist-(?P<version>.*).nvda-addon", 
res.url).groupdict()["version"]
+               if version == SPLAddonVersion:
                        if auto:
                                if continuous: 
_SPLUpdateT.Start(updateInterval, True)
                                return
@@ -147,7 +215,7 @@ def updateChecker(auto=False, continuous=False, 
confUpdateInterval=1):
                        checkMessage = _("No add-on update available.")
                else:
                        # Translators: Text shown if an add-on update is 
available.
-                       checkMessage = _("Studio add-on {newVersion} is 
available. Would you like to update?").format(newVersion = qualified)
+                       checkMessage = _("Studio add-on {newVersion} is 
available. Would you like to update?").format(newVersion = version)
                        updateCandidate = True
        if not auto:
                wx.CallAfter(_progressDialog.done)
@@ -166,51 +234,6 @@ def getUpdateResponse(message, caption, updateURL):
 #: The download block size in bytes.
 DOWNLOAD_BLOCK_SIZE = 8192 # 8 kb
 
-def checkForUpdate(auto=False):
-       """Check for an updated version of NVDA.
-       This will block, so it generally shouldn't be called from the main 
thread.
-       @param auto: Whether this is an automatic check for updates.
-       @type auto: bool
-       @return: Information about the update or C{None} if there is no update.
-       @rtype: dict
-       @raise RuntimeError: If there is an error checking for an update.
-       """
-       params = {
-               "autoCheck": auto,
-               "version": versionInfo.version,
-               "versionType": versionInfo.updateVersionType,
-               "osVersion": winVersion.winVersionText,
-               "x64": os.environ.get("PROCESSOR_ARCHITEW6432") == "AMD64",
-               "language": languageHandler.getLanguage(),
-               "installed": config.isInstalledCopy(),
-       }
-       url = "%s?%s" % (CHECK_URL, urllib.urlencode(params))
-       try:
-               res = urllib.urlopen(url)
-       except IOError as e:
-               if isinstance(e.strerror, ssl.SSLError) and e.strerror.reason 
== "CERTIFICATE_VERIFY_FAILED":
-                       # #4803: Windows fetches trusted root certificates on 
demand.
-                       # Python doesn't trigger this fetch 
(PythonIssue:20916), so try it ourselves
-                       _updateWindowsRootCertificates()
-                       # and then retry the update check.
-                       res = urllib.urlopen(url)
-               else:
-                       raise
-       if res.code != 200:
-               raise RuntimeError("Checking for update failed with code %d" % 
res.code)
-       info = {}
-       for line in res:
-               line = line.rstrip()
-               try:
-                       key, val = line.split(": ", 1)
-               except ValueError:
-                       raise RuntimeError("Error in update check output")
-               info[key] = val
-       if not info:
-               return None
-       return info
-
-
 class SPLUpdateDownloader(updateCheck.UpdateDownloader):
        """Overrides NVDA Core's downloader.)
        No hash checking for now, and URL's and temp file paths are different.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/7bce7303b8af/
Changeset:   7bce7303b8af
Branch:      None
User:        josephsl
Date:        2017-02-01 18:03:43+00:00
Summary:     Playlist snapshots: do not record artist and genre information for 
an hur marker, add item count to snapshots output.

reported by a broadcaster: hour marker should not be listed as a track, leads 
to confusion. Therefore artist and genre information will not be recorded for 
hour markers, and to minimize confusion, item count will be added (item count 
includes hour markers). The track count will now be used to count actual track 
count.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 2f2a239..5c197bd 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1451,8 +1451,10 @@ class AppModule(appModuleHandler.AppModule):
                        segue = obj._getColumnContent(duration)
                        trackTitle = obj._getColumnContent(title)
                        categories.append(obj._getColumnContent(category))
-                       if categories[-1] != "Hour Marker": 
artists.append(obj._getColumnContent(artist))
-                       genres.append(obj._getColumnContent(genre))
+                       # Don't record artist and genre information for an hour 
marker (reported by a broadcaster).
+                       if categories[-1] != "Hour Marker":
+                               artists.append(obj._getColumnContent(artist))
+                               genres.append(obj._getColumnContent(genre))
                        # Shortest and longest tracks.
                        if min is None: min = segue
                        if segue and segue < min:
@@ -1466,7 +1468,8 @@ class AppModule(appModuleHandler.AppModule):
                                totalDuration += (int(hms[-2])*60) + 
int(hms[-1])
                                if len(hms) == 3: totalDuration += 
int(hms[0])*3600
                        obj = obj.next
-               if end is None: snapshot["PlaylistTrackCount"] = studioAPI(0, 
124, ret=True)
+               if end is None: snapshot["PlaylistItemCount"] = studioAPI(0, 
124, ret=True)
+               snapshot["PlaylistTrackCount"] = len(artists)
                snapshot["PlaylistDurationTotal"] = 
self._ms2time(totalDuration, ms=False)
                if "DurationMinMax" in snapshotFlags:
                        snapshot["PlaylistDurationMin"] = "%s (%s)"%(minTitle, 
min)
@@ -1483,7 +1486,8 @@ class AppModule(appModuleHandler.AppModule):
 # Output formatter for playlist snapshots.
 # Pressed once will speak and/or braille it, pressing twice or more will 
output this info to an HTML file.
        def playlistSnapshotOutput(self, snapshot, scriptCount):
-               statusInfo = ["Tracks: %s"%snapshot["PlaylistTrackCount"]]
+               statusInfo = ["Items: %s"%snapshot["PlaylistItemCount"]]
+               statusInfo.append("Tracks: %s"%snapshot["PlaylistTrackCount"])
                statusInfo.append("Duration: 
%s"%snapshot["PlaylistDurationTotal"])
                if "PlaylistDurationMin" in snapshot:
                        statusInfo.append("Shortest: 
%s"%snapshot["PlaylistDurationMin"])


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/a29500eb384e/
Changeset:   a29500eb384e
Branch:      None
User:        josephsl
Date:        2017-02-01 23:17:12+00:00
Summary:     Playlist snapshots: shortest track is recognized correctly if the 
first track of a plyalist is the shortest track of them all. re #22.

Reported by a broadcaster and noted as a logic error: just because shortest 
track segue is None doesn't mean it should be assigned to the first segue, thus 
causing inequality to fail (the inequality checks for current min, which could 
very well be the very first segue). Also this takes care of a problem where all 
tracks in a playlistlist have the same length.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 5c197bd..14ba829 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1456,8 +1456,8 @@ class AppModule(appModuleHandler.AppModule):
                                artists.append(obj._getColumnContent(artist))
                                genres.append(obj._getColumnContent(genre))
                        # Shortest and longest tracks.
-                       if min is None: min = segue
-                       if segue and segue < min:
+                       # #22: assign min to the first segue in order to not 
forget title of the shortest track.
+                       if segue and (min is None or segue < min):
                                min = segue
                                minTitle = trackTitle
                        if segue and segue > max:


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/d15e85dbfca8/
Changeset:   d15e85dbfca8
Branch:      None
User:        josephsl
Date:        2017-02-01 23:20:16+00:00
Summary:     Merge branch 'master' into updateInfoDictionary

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 2f2a239..14ba829 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1451,11 +1451,13 @@ class AppModule(appModuleHandler.AppModule):
                        segue = obj._getColumnContent(duration)
                        trackTitle = obj._getColumnContent(title)
                        categories.append(obj._getColumnContent(category))
-                       if categories[-1] != "Hour Marker": 
artists.append(obj._getColumnContent(artist))
-                       genres.append(obj._getColumnContent(genre))
+                       # Don't record artist and genre information for an hour 
marker (reported by a broadcaster).
+                       if categories[-1] != "Hour Marker":
+                               artists.append(obj._getColumnContent(artist))
+                               genres.append(obj._getColumnContent(genre))
                        # Shortest and longest tracks.
-                       if min is None: min = segue
-                       if segue and segue < min:
+                       # #22: assign min to the first segue in order to not 
forget title of the shortest track.
+                       if segue and (min is None or segue < min):
                                min = segue
                                minTitle = trackTitle
                        if segue and segue > max:
@@ -1466,7 +1468,8 @@ class AppModule(appModuleHandler.AppModule):
                                totalDuration += (int(hms[-2])*60) + 
int(hms[-1])
                                if len(hms) == 3: totalDuration += 
int(hms[0])*3600
                        obj = obj.next
-               if end is None: snapshot["PlaylistTrackCount"] = studioAPI(0, 
124, ret=True)
+               if end is None: snapshot["PlaylistItemCount"] = studioAPI(0, 
124, ret=True)
+               snapshot["PlaylistTrackCount"] = len(artists)
                snapshot["PlaylistDurationTotal"] = 
self._ms2time(totalDuration, ms=False)
                if "DurationMinMax" in snapshotFlags:
                        snapshot["PlaylistDurationMin"] = "%s (%s)"%(minTitle, 
min)
@@ -1483,7 +1486,8 @@ class AppModule(appModuleHandler.AppModule):
 # Output formatter for playlist snapshots.
 # Pressed once will speak and/or braille it, pressing twice or more will 
output this info to an HTML file.
        def playlistSnapshotOutput(self, snapshot, scriptCount):
-               statusInfo = ["Tracks: %s"%snapshot["PlaylistTrackCount"]]
+               statusInfo = ["Items: %s"%snapshot["PlaylistItemCount"]]
+               statusInfo.append("Tracks: %s"%snapshot["PlaylistTrackCount"])
                statusInfo.append("Duration: 
%s"%snapshot["PlaylistDurationTotal"])
                if "PlaylistDurationMin" in snapshot:
                        statusInfo.append("Shortest: 
%s"%snapshot["PlaylistDurationMin"])


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/3101d8e1c489/
Changeset:   3101d8e1c489
Branch:      None
User:        josephsl
Date:        2017-02-01 23:27:14+00:00
Summary:     Merge branch 'stable' into 16.10.x

Affected #:  1 file

diff --git a/buildVars.py b/buildVars.py
index f2c81b4..8256280 100755
--- a/buildVars.py
+++ b/buildVars.py
@@ -20,7 +20,7 @@ addon_info = {
        "addon_description" : _("""Enhances support for StationPlaylist Studio.
 In addition, adds global commands for the studio from everywhere."""),
        # version
-       "addon_version" : "17.01",
+       "addon_version" : "17.02",
        # Author(s)
        "addon_author" : u"Geoff Shang, Joseph Lee and other contributors",
        # URL for the add-on documentation support


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/9418e531ae16/
Changeset:   9418e531ae16
Branch:      None
User:        josephsl
Date:        2017-02-01 23:55:17+00:00
Summary:     Add-on update check: use a background thread to check for updates 
at startup in order to avoid a freeze. re #23.

When a user changes channels or when the update interval elapses at startup, 
NVDA will be told to check for add-on updates. If this happens, NVDA will 
appear to freeze because urllib blocks. Thus use a background thread to check 
for updates.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 8a10737..e9a7fe9 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -822,9 +822,13 @@ def autoUpdateCheck():
 # A bit simpler than NVDA Core's auto update checker.
 def updateInit():
        # LTS: Launch updater if channel change is detected.
+       # Use a background thread for this as urllib blocks.
+       import threading
        if splupdate._updateNow:
-               splupdate.updateCheck(auto=True) # No repeat here.
+               t = threading.Thread(target=splupdate.updateCheck, 
kwargs={"auto": True}) # No repeat here.
+               t.daemon = True
                splupdate._SPLUpdateT = wx.PyTimer(autoUpdateCheck)
+               t.start()
                splupdate._updateNow = False
                return
        currentTime = time.time()
@@ -834,7 +838,9 @@ def updateInit():
        elif splupdate.SPLAddonCheck < nextCheck < currentTime:
                interval = SPLConfig["Update"]["UpdateInterval"]* 86400
                # Call the update check now.
-               splupdate.updateCheck(auto=True) # No repeat here.
+               t = threading.Thread(target=splupdate.updateCheck, 
kwargs={"auto": True}) # No repeat here.
+               t.daemon = True
+               t.start()
        splupdate._SPLUpdateT = wx.PyTimer(autoUpdateCheck)
        splupdate._SPLUpdateT.Start(interval * 1000, True)
 


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/407df812941b/
Changeset:   407df812941b
Branch:      None
User:        josephsl
Date:        2017-02-01 23:58:04+00:00
Summary:     Merged 16.10.x

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 77f25ca..a3d9b38 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -838,9 +838,13 @@ def autoUpdateCheck():
 # A bit simpler than NVDA Core's auto update checker.
 def updateInit():
        # LTS: Launch updater if channel change is detected.
+       # Use a background thread for this as urllib blocks.
+       import threading
        if splupdate._updateNow:
-               splupdate.updateChecker(auto=True) # No repeat here.
+               t = threading.Thread(target=splupdate.updateChecker, 
kwargs={"auto": True}) # No repeat here.
+               t.daemon = True
                splupdate._SPLUpdateT = wx.PyTimer(autoUpdateCheck)
+               t.start()
                splupdate._updateNow = False
                return
        currentTime = time.time()
@@ -850,7 +854,9 @@ def updateInit():
        elif splupdate.SPLAddonCheck < nextCheck < currentTime:
                interval = SPLConfig["Update"]["UpdateInterval"]* 86400
                # Call the update check now.
-               splupdate.updateChecker(auto=True) # No repeat here.
+               t = threading.Thread(target=splupdate.updateChecker, 
kwargs={"auto": True}) # No repeat here.
+               t.daemon = True
+               t.start()
        splupdate._SPLUpdateT = wx.PyTimer(autoUpdateCheck)
        splupdate._SPLUpdateT.Start(interval * 1000, True)
 


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/e4bb7df4a67e/
Changeset:   e4bb7df4a67e
Branch:      None
User:        josephsl
Date:        2017-02-02 11:20:55+00:00
Summary:     Debugging framework (17.04/17.2-dev): initial foundation for 
debugging framework with a debug logging printer. re #24.

In NVDA 2017.1, it is possible to allow NVDA to provide debug logging for a 
single session. Thus take advantage of this by introducing a basic debugging 
framework that'll let the add-on output important debugging information (such 
as command execution steps) to the log for analysis later.
An initial solution will be in place in 17.04, with functionality coming in via 
subsequent stable releases, full picture to be ready by 17.2-dev (summer/fall).

Affected #:  2 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 14ba829..3f0a7d4 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -36,6 +36,7 @@ import splmisc
 import splupdate
 import addonHandler
 addonHandler.initTranslation()
+from spldebugging import debugOutput
 
 # Make sure the broadcaster is running a compatible version.
 SPLMinVersion = "5.10"
@@ -78,9 +79,14 @@ def micAlarmManager(micAlarmWav, micAlarmMessage):
 # Use SPL Studio API to obtain needed values.
 # A thin wrapper around user32.SendMessage and calling a callback if defined.
 # Offset is used in some time commands.
+# If debugging framework is on, print arg, command and other values.
 def studioAPI(arg, command, func=None, ret=False, offset=None):
-       if _SPLWin is None: return
+       if _SPLWin is None:
+               debugOutput("SPL: Studio handle not found")
+               return
+       debugOutput("SPL: Studio API wParem is %s, lParem is %s"%(arg, command))
        val = sendMessage(_SPLWin, 1024, arg, command)
+       debugOutput("SPL: Studio API result is %s"%val)
        if ret:
                return val
        if func:
@@ -90,6 +96,7 @@ def studioAPI(arg, command, func=None, ret=False, 
offset=None):
 # This is to make sure custom commands for SPL Assistant comamnds and other 
app module gestures display appropriate error messages.
 def studioIsRunning():
        if _SPLWin is None:
+               debugOutput("SPL: Studio handle not found")
                # Translators: A message informing users that Studio is not 
running so certain commands will not work.
                ui.message(_("Studio main window not found"))
                return False
@@ -571,6 +578,7 @@ class AppModule(appModuleHandler.AppModule):
                        ui.message(_("Using SPL Studio version 
{SPLVersion}").format(SPLVersion = self.SPLCurVersion))
                except IOError, AttributeError:
                        pass
+               debugOutput("SPL: loading add-on settings")
                splconfig.initConfig()
                # Announce status changes while using other programs.
                # This requires NVDA core support and will be available in 6.0 
and later (cannot be ported to earlier versions).
@@ -592,6 +600,7 @@ class AppModule(appModuleHandler.AppModule):
                # Check for add-on update if told to do so.
                # LTS: Only do this if channel hasn't changed.
                if splconfig.SPLConfig["Update"]["AutoUpdateCheck"] or 
splupdate._updateNow:
+                       debugOutput("SPL: checking for add-on updates from %s 
channel"%splupdate.updateChannel)
                        # 7.0: Have a timer call the update function indirectly.
                        import queueHandler
                        queueHandler.queueFunction(queueHandler.eventQueue, 
splconfig.updateInit)
@@ -853,16 +862,17 @@ class AppModule(appModuleHandler.AppModule):
                        except KeyError:
                                pass
 
-
        # Save configuration when terminating.
        def terminate(self):
                super(AppModule, self).terminate()
+               debugOutput("SPL: terminating app module")
                # 6.3: Memory leak results if encoder flag sets and other 
encoder support maps aren't cleaned up.
                # This also could have allowed a hacker to modify the flags set 
(highly unlikely) so NvDA could get confused next time Studio loads.
                import sys
                if "globalPlugins.splUtils.encoders" in sys.modules:
                        import globalPlugins.splUtils.encoders
                        globalPlugins.splUtils.encoders.cleanup()
+               debugOutput("SPL: saving add-on settings")
                splconfig.saveConfig()
                # reset column number for column navigation commands.
                if self._focusedTrack: 
self._focusedTrack.__class__._curColumnNumber = None
@@ -883,7 +893,6 @@ class AppModule(appModuleHandler.AppModule):
                global _SPLWin
                if _SPLWin: _SPLWin = None
 
-
        # Script sections (for ease of maintenance):
        # Time-related: elapsed time, end of track alarm, etc.
        # Misc scripts: track finder and others.

diff --git a/addon/appModules/splstudio/spldebugging.py 
b/addon/appModules/splstudio/spldebugging.py
new file mode 100755
index 0000000..b9f3498
--- /dev/null
+++ b/addon/appModules/splstudio/spldebugging.py
@@ -0,0 +1,17 @@
+# SPL Studio add-on debugging framework
+# An app module and global plugin package for NVDA
+# Copyright 2017 Joseph Lee and others, released under GPL.
+# Provides debug output and other diagnostics probes.
+
+from logHandler import log
+
+try:
+       import globalVars
+       SPLDebuggingFramework = globalVars.appArgs.debugLogging
+except AttributeError:
+       SPLDebuggingFramework = None
+
+def debugOutput(message):
+       if SPLDebuggingFramework:
+               log.debug(message)
+


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/a1d1bfa93af1/
Changeset:   a1d1bfa93af1
Branch:      None
User:        josephsl
Date:        2017-02-02 18:51:04+00:00
Summary:     Merge branch 'master' into updateInfoDictionary

Affected #:  3 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 14ba829..3f0a7d4 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -36,6 +36,7 @@ import splmisc
 import splupdate
 import addonHandler
 addonHandler.initTranslation()
+from spldebugging import debugOutput
 
 # Make sure the broadcaster is running a compatible version.
 SPLMinVersion = "5.10"
@@ -78,9 +79,14 @@ def micAlarmManager(micAlarmWav, micAlarmMessage):
 # Use SPL Studio API to obtain needed values.
 # A thin wrapper around user32.SendMessage and calling a callback if defined.
 # Offset is used in some time commands.
+# If debugging framework is on, print arg, command and other values.
 def studioAPI(arg, command, func=None, ret=False, offset=None):
-       if _SPLWin is None: return
+       if _SPLWin is None:
+               debugOutput("SPL: Studio handle not found")
+               return
+       debugOutput("SPL: Studio API wParem is %s, lParem is %s"%(arg, command))
        val = sendMessage(_SPLWin, 1024, arg, command)
+       debugOutput("SPL: Studio API result is %s"%val)
        if ret:
                return val
        if func:
@@ -90,6 +96,7 @@ def studioAPI(arg, command, func=None, ret=False, 
offset=None):
 # This is to make sure custom commands for SPL Assistant comamnds and other 
app module gestures display appropriate error messages.
 def studioIsRunning():
        if _SPLWin is None:
+               debugOutput("SPL: Studio handle not found")
                # Translators: A message informing users that Studio is not 
running so certain commands will not work.
                ui.message(_("Studio main window not found"))
                return False
@@ -571,6 +578,7 @@ class AppModule(appModuleHandler.AppModule):
                        ui.message(_("Using SPL Studio version 
{SPLVersion}").format(SPLVersion = self.SPLCurVersion))
                except IOError, AttributeError:
                        pass
+               debugOutput("SPL: loading add-on settings")
                splconfig.initConfig()
                # Announce status changes while using other programs.
                # This requires NVDA core support and will be available in 6.0 
and later (cannot be ported to earlier versions).
@@ -592,6 +600,7 @@ class AppModule(appModuleHandler.AppModule):
                # Check for add-on update if told to do so.
                # LTS: Only do this if channel hasn't changed.
                if splconfig.SPLConfig["Update"]["AutoUpdateCheck"] or 
splupdate._updateNow:
+                       debugOutput("SPL: checking for add-on updates from %s 
channel"%splupdate.updateChannel)
                        # 7.0: Have a timer call the update function indirectly.
                        import queueHandler
                        queueHandler.queueFunction(queueHandler.eventQueue, 
splconfig.updateInit)
@@ -853,16 +862,17 @@ class AppModule(appModuleHandler.AppModule):
                        except KeyError:
                                pass
 
-
        # Save configuration when terminating.
        def terminate(self):
                super(AppModule, self).terminate()
+               debugOutput("SPL: terminating app module")
                # 6.3: Memory leak results if encoder flag sets and other 
encoder support maps aren't cleaned up.
                # This also could have allowed a hacker to modify the flags set 
(highly unlikely) so NvDA could get confused next time Studio loads.
                import sys
                if "globalPlugins.splUtils.encoders" in sys.modules:
                        import globalPlugins.splUtils.encoders
                        globalPlugins.splUtils.encoders.cleanup()
+               debugOutput("SPL: saving add-on settings")
                splconfig.saveConfig()
                # reset column number for column navigation commands.
                if self._focusedTrack: 
self._focusedTrack.__class__._curColumnNumber = None
@@ -883,7 +893,6 @@ class AppModule(appModuleHandler.AppModule):
                global _SPLWin
                if _SPLWin: _SPLWin = None
 
-
        # Script sections (for ease of maintenance):
        # Time-related: elapsed time, end of track alarm, etc.
        # Misc scripts: track finder and others.

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 77f25ca..a3d9b38 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -838,9 +838,13 @@ def autoUpdateCheck():
 # A bit simpler than NVDA Core's auto update checker.
 def updateInit():
        # LTS: Launch updater if channel change is detected.
+       # Use a background thread for this as urllib blocks.
+       import threading
        if splupdate._updateNow:
-               splupdate.updateChecker(auto=True) # No repeat here.
+               t = threading.Thread(target=splupdate.updateChecker, 
kwargs={"auto": True}) # No repeat here.
+               t.daemon = True
                splupdate._SPLUpdateT = wx.PyTimer(autoUpdateCheck)
+               t.start()
                splupdate._updateNow = False
                return
        currentTime = time.time()
@@ -850,7 +854,9 @@ def updateInit():
        elif splupdate.SPLAddonCheck < nextCheck < currentTime:
                interval = SPLConfig["Update"]["UpdateInterval"]* 86400
                # Call the update check now.
-               splupdate.updateChecker(auto=True) # No repeat here.
+               t = threading.Thread(target=splupdate.updateChecker, 
kwargs={"auto": True}) # No repeat here.
+               t.daemon = True
+               t.start()
        splupdate._SPLUpdateT = wx.PyTimer(autoUpdateCheck)
        splupdate._SPLUpdateT.Start(interval * 1000, True)
 

diff --git a/addon/appModules/splstudio/spldebugging.py 
b/addon/appModules/splstudio/spldebugging.py
new file mode 100755
index 0000000..b9f3498
--- /dev/null
+++ b/addon/appModules/splstudio/spldebugging.py
@@ -0,0 +1,17 @@
+# SPL Studio add-on debugging framework
+# An app module and global plugin package for NVDA
+# Copyright 2017 Joseph Lee and others, released under GPL.
+# Provides debug output and other diagnostics probes.
+
+from logHandler import log
+
+try:
+       import globalVars
+       SPLDebuggingFramework = globalVars.appArgs.debugLogging
+except AttributeError:
+       SPLDebuggingFramework = None
+
+def debugOutput(message):
+       if SPLDebuggingFramework:
+               log.debug(message)
+


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/047473c1acf8/
Changeset:   047473c1acf8
Branch:      None
User:        josephsl
Date:        2017-02-02 21:19:35+00:00
Summary:     Cart Explorer 3 (17.04): refresh carts via the init function in 
order to reduce loop traversals.

Optimization: In add-on 17.01, when refreshing cart banks, cart files were read 
up to two times (two for loops). The best case was only one loop (no refresh 
needed). To reduce loop traversal, combine aspects of cart explorer init and 
refresh functions into cart explorer init that'll also be able to refresh carts 
if the timestamps for carts exists (error conditions to be taken care of 
later). This optimization results in code reuse and allows just one function to 
provide debug output, and in some cases only one loop will be run (check the 
timestamp, and if they are equal for all, don't update carts by processing cart 
files), as well as saving the add-on from processing all carts unnecessarily.
This is destined for add-on 17.04.

Affected #:  2 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 3f0a7d4..e43a279 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -789,9 +789,7 @@ class AppModule(appModuleHandler.AppModule):
                        # 17.01: The best way to detect Cart Edit off is 
consulting file modification time.
                        # Automatically reload cart information if this is the 
case.
                        if status in ("Cart Edit Off", "Cart Insert On"):
-                               studioTitle = api.getForegroundObject().name
-                               if 
splmisc.shouldCartExplorerRefresh(studioTitle):
-                                       self.carts = 
splmisc.cartExplorerInit(studioTitle)
+                               self.carts = 
splmisc.cartExplorerInit(api.getForegroundObject().name, refresh=True)
                        # Translators: Presented when cart modes are toggled 
while cart explorer is on.
                        ui.message(_("Cart explorer is active"))
                        return
@@ -1230,6 +1228,7 @@ class AppModule(appModuleHandler.AppModule):
                        self.cartExplorer = False
                        self.cartsBuilder(build=False)
                        self.carts.clear()
+                       splmisc._cartEditTimestamps = None
                        # Translators: Presented when cart explorer is off.
                        ui.message(_("Exiting cart explorer"))
        # Translators: Input help mode message for a command in Station 
Playlist Studio.

diff --git a/addon/appModules/splstudio/splmisc.py 
b/addon/appModules/splstudio/splmisc.py
index b4123b4..9b4cadd 100755
--- a/addon/appModules/splstudio/splmisc.py
+++ b/addon/appModules/splstudio/splmisc.py
@@ -14,6 +14,7 @@ import gui
 import wx
 import ui
 from winUser import user32, sendMessage
+from spldebugging import debugOutput
 
 # Locate column content.
 # Given an object and the column number, locate text in the given column.
@@ -263,11 +264,13 @@ def _populateCarts(carts, cartlst, modifier, 
standardEdition=False):
                carts[cart] = cartName
 
 # Cart file timestamps.
-_cartEditTimestamps = [0, 0, 0, 0]
+_cartEditTimestamps = None
                # Initialize Cart Explorer i.e. fetch carts.
 # Cart files list is for future use when custom cart names are used.
-def cartExplorerInit(StudioTitle, cartFiles=None):
+# if told to refresh, timestamps will be checked and updated banks will be 
reassigned.
+def cartExplorerInit(StudioTitle, cartFiles=None, refresh=False):
        global _cartEditTimestamps
+       debugOutput("SPL: refreshing Cart Explorer" if refresh else "SPL: 
preparing cart Explorer")
        # Use cart files in SPL's data folder to build carts dictionary.
        # use a combination of SPL user name and static cart location to locate 
cart bank files.
        # Once the cart banks are located, use the routines in the populate 
method above to assign carts.
@@ -284,7 +287,10 @@ def cartExplorerInit(StudioTitle, cartFiles=None):
                if userNameIndex >= 0:
                        cartFiles = [StudioTitle[userNameIndex+2:]+" "+cartFile 
for cartFile in cartFiles]
        faultyCarts = False
+       if not refresh:
+               _cartEditTimestamps = []
        for f in cartFiles:
+               # Only do this if told to build cart banks from scratch, as 
refresh flag is set if cart explorer is active in the first place.
                try:
                        mod = f.split()[-2] # Checking for modifier string such 
as ctrl.
                        # Todo: Check just in case some SPL flavors doesn't 
ship with a particular cart file.
@@ -292,36 +298,24 @@ def cartExplorerInit(StudioTitle, cartFiles=None):
                        faultyCarts = True # In a rare event that the 
broadcaster has saved the cart bank with the name like "carts.cart".
                        continue
                cartFile = os.path.join(cartsDataPath,f)
-               if not os.path.isfile(cartFile): # Cart explorer will fail if 
whitespaces are in the beginning or at the end of a user name.
+               # Cart explorer can safely assume that the cart bank exists if 
refresh flag is set.
+               if not refresh and not os.path.isfile(cartFile): # Cart 
explorer will fail if whitespaces are in the beginning or at the end of a user 
name.
                        faultyCarts = True
                        continue
+               debugOutput("SPL: examining carts from file %s"%cartFile)
+               cartTimestamp = os.path.getmtime(cartFile)
+               if refresh and _cartEditTimestamps[cartFiles.index(f)] == 
cartTimestamp:
+                       debugOutput("SPL: no changes to cart bank, skipping")
+                       continue
+               _cartEditTimestamps.append(cartTimestamp)
                with open(cartFile) as cartInfo:
                        cl = [row for row in reader(cartInfo)]
-                       # 17.01: Look up file modification date to signal the 
app module that Cart Explorer reentry should occur.
-                       _cartEditTimestamps[cartFiles.index(f)] = 
os.path.getmtime(cartFile)
                _populateCarts(carts, cl[1], mod, 
standardEdition=carts["standardLicense"]) # See the comment for _populate 
method above.
+               debugOutput("SPL: carts processed so far: %s"%(len(carts)-1))
        carts["faultyCarts"] = faultyCarts
+       debugOutput("SPL: total carts processed: %s"%(len(carts)-2))
        return carts
 
-# See if cart files were modified.
-# This is needed in order to announce Cart Explorer reentry command.
-def shouldCartExplorerRefresh(StudioTitle):
-       global _cartEditTimestamps
-       cartsDataPath = 
os.path.join(os.environ["PROGRAMFILES"],"StationPlaylist","Data") # Provided 
that Studio was installed using default path.
-       userNameIndex = StudioTitle.find("-")
-       # Until NVDA core moves to Python 3, assume that file names aren't 
unicode.
-       cartFiles = [u"main carts.cart", u"shift carts.cart", u"ctrl 
carts.cart", u"alt carts.cart"]
-       if userNameIndex >= 0:
-               cartFiles = [StudioTitle[userNameIndex+2:]+" "+cartFile for 
cartFile in cartFiles]
-       for f in cartFiles:
-               # No need to check for faulty carts here, as Cart Explorer 
activation checked it already.
-               timestamp = os.path.getmtime(os.path.join(cartsDataPath,f))
-               # 17.01: Look up file modification date to signal the app 
module that Cart Explorer reentry should occur.
-               # Optimization: Short-circuit if even one cart file has been 
modified.
-               if _cartEditTimestamps[cartFiles.index(f)] != timestamp:
-                       return True
-       return False
-
 
 # Countdown timer.
 # This is utilized by many services, chiefly profile triggers routine.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/0d1ed6879047/
Changeset:   0d1ed6879047
Branch:      None
User:        josephsl
Date:        2017-02-03 01:22:11+00:00
Summary:     Cart Explorer 3 (17.04): cart assignment optimization, fixed an 
issue where unmodified cart banks could be lost if toggling cart edit mode 
while cart explorer is active.

Consider the following scenario: a user starts Studio, enters Cart Explorer, 
turns on cart edit, adds a cart assignment to a brand new bank, then turns off 
cart edit. If this happens, unmodified cart banks will be lost, caused by the 
fact that previous optimization did not take current carts state into account. 
To mitigate this, a new function (cartExplorerRefresh) is now used to change 
modified assignments (additions, changes, deletions). This also makes the app 
module routine for Cart Explorer refresh (do extra function) reader friendly.
The populate carts function has been modified to treat the incoming cart as the 
active Cart Explorer carts dictionary when refresh flag is active. This allows 
entry type to be saved and consulted when checking for cart edit changes.

Affected #:  2 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index e43a279..6d1c201 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -789,7 +789,7 @@ class AppModule(appModuleHandler.AppModule):
                        # 17.01: The best way to detect Cart Edit off is 
consulting file modification time.
                        # Automatically reload cart information if this is the 
case.
                        if status in ("Cart Edit Off", "Cart Insert On"):
-                               self.carts = 
splmisc.cartExplorerInit(api.getForegroundObject().name, refresh=True)
+                               self.carts = 
splmisc.cartExplorerRefresh(api.getForegroundObject().name, self.carts)
                        # Translators: Presented when cart modes are toggled 
while cart explorer is on.
                        ui.message(_("Cart explorer is active"))
                        return

diff --git a/addon/appModules/splstudio/splmisc.py 
b/addon/appModules/splstudio/splmisc.py
index 9b4cadd..119fe4a 100755
--- a/addon/appModules/splstudio/splmisc.py
+++ b/addon/appModules/splstudio/splmisc.py
@@ -242,13 +242,15 @@ class SPLTimeRangeDialog(wx.Dialog):
 
 # Cart Explorer helper.
 
-def _populateCarts(carts, cartlst, modifier, standardEdition=False):
+def _populateCarts(carts, cartlst, modifier, standardEdition=False, 
refresh=False):
        # The real cart string parser, a helper for cart explorer for building 
cart entries.
        # 5.2: Discard number row if SPL Standard is in use.
        if standardEdition: cartlst = cartlst[:12]
        for entry in cartlst:
                # An unassigned cart is stored with three consecutive commas, 
so skip it.
-               if ",,," in entry: continue
+               # 17.04: If refresh is on, the cart we're dealing with is the 
actual carts dictionary that was built previously.
+               noEntry = ",,," in entry
+               if noEntry and not refresh: continue
                # Pos between 1 and 12 = function carts, 13 through 24 = number 
row carts, modifiers are checked.
                pos = cartlst.index(entry)+1
                # If a cart name has commas or other characters, SPL surrounds 
the cart name with quotes (""), so parse it as well.
@@ -259,23 +261,27 @@ def _populateCarts(carts, cartlst, modifier, 
standardEdition=False):
                elif pos == 22: identifier = "0"
                elif pos == 23: identifier = "-"
                else: identifier = "="
-               if modifier == "main": cart = identifier
-               else: cart = "%s+%s"%(modifier, identifier)
-               carts[cart] = cartName
+               cart = identifier if not modifier else "+".join([modifier, 
identifier])
+               if noEntry and refresh:
+                       if cart in carts: del carts[cart]
+               else:
+                       carts[cart] = cartName
 
 # Cart file timestamps.
 _cartEditTimestamps = None
                # Initialize Cart Explorer i.e. fetch carts.
 # Cart files list is for future use when custom cart names are used.
 # if told to refresh, timestamps will be checked and updated banks will be 
reassigned.
-def cartExplorerInit(StudioTitle, cartFiles=None, refresh=False):
+# Carts dictionary is used if and only if refresh is on, as it'll modify live 
cats.
+def cartExplorerInit(StudioTitle, cartFiles=None, refresh=False, carts=None):
        global _cartEditTimestamps
        debugOutput("SPL: refreshing Cart Explorer" if refresh else "SPL: 
preparing cart Explorer")
        # Use cart files in SPL's data folder to build carts dictionary.
        # use a combination of SPL user name and static cart location to locate 
cart bank files.
        # Once the cart banks are located, use the routines in the populate 
method above to assign carts.
        # Since sstandard edition does not support number row carts, skip them 
if told to do so.
-       carts = {"standardLicense":StudioTitle.startswith("StationPlaylist 
Studio Standard")}
+       if carts is None: carts = 
{"standardLicense":StudioTitle.startswith("StationPlaylist Studio Standard")}
+       if refresh: carts["modifiedBanks"] = []
        # Obtain the "real" path for SPL via environment variables and open the 
cart data folder.
        cartsDataPath = 
os.path.join(os.environ["PROGRAMFILES"],"StationPlaylist","Data") # Provided 
that Studio was installed using default path.
        if cartFiles is None:
@@ -310,12 +316,19 @@ def cartExplorerInit(StudioTitle, cartFiles=None, 
refresh=False):
                _cartEditTimestamps.append(cartTimestamp)
                with open(cartFile) as cartInfo:
                        cl = [row for row in reader(cartInfo)]
-               _populateCarts(carts, cl[1], mod, 
standardEdition=carts["standardLicense"]) # See the comment for _populate 
method above.
-               debugOutput("SPL: carts processed so far: %s"%(len(carts)-1))
+               # 17.04 (optimization): let empty string represent main cart 
bank to avoid this being partially consulted up to 24 times.
+               # The below method will just check for string length, which is 
faster than looking for specific substring.
+               _populateCarts(carts, cl[1], mod if mod != "main" else "", 
standardEdition=carts["standardLicense"], refresh=refresh) # See the comment 
for _populate method above.
+               if not refresh:
+                       debugOutput("SPL: carts processed so far: 
%s"%(len(carts)-1))
        carts["faultyCarts"] = faultyCarts
        debugOutput("SPL: total carts processed: %s"%(len(carts)-2))
        return carts
 
+# Refresh carts upon request.
+# calls cart explorer init with special (internal) flags.
+def cartExplorerRefresh(studioTitle, currentCarts):
+       return cartExplorerInit(studioTitle, refresh=True, carts=currentCarts)
 
 # Countdown timer.
 # This is utilized by many services, chiefly profile triggers routine.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/f9962fe49034/
Changeset:   f9962fe49034
Branch:      None
User:        josephsl
Date:        2017-02-07 02:09:27+00:00
Summary:     Merge branch 'master' into updateInfoDictionary

Affected #:  2 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 3f0a7d4..6d1c201 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -789,9 +789,7 @@ class AppModule(appModuleHandler.AppModule):
                        # 17.01: The best way to detect Cart Edit off is 
consulting file modification time.
                        # Automatically reload cart information if this is the 
case.
                        if status in ("Cart Edit Off", "Cart Insert On"):
-                               studioTitle = api.getForegroundObject().name
-                               if 
splmisc.shouldCartExplorerRefresh(studioTitle):
-                                       self.carts = 
splmisc.cartExplorerInit(studioTitle)
+                               self.carts = 
splmisc.cartExplorerRefresh(api.getForegroundObject().name, self.carts)
                        # Translators: Presented when cart modes are toggled 
while cart explorer is on.
                        ui.message(_("Cart explorer is active"))
                        return
@@ -1230,6 +1228,7 @@ class AppModule(appModuleHandler.AppModule):
                        self.cartExplorer = False
                        self.cartsBuilder(build=False)
                        self.carts.clear()
+                       splmisc._cartEditTimestamps = None
                        # Translators: Presented when cart explorer is off.
                        ui.message(_("Exiting cart explorer"))
        # Translators: Input help mode message for a command in Station 
Playlist Studio.

diff --git a/addon/appModules/splstudio/splmisc.py 
b/addon/appModules/splstudio/splmisc.py
index b4123b4..119fe4a 100755
--- a/addon/appModules/splstudio/splmisc.py
+++ b/addon/appModules/splstudio/splmisc.py
@@ -14,6 +14,7 @@ import gui
 import wx
 import ui
 from winUser import user32, sendMessage
+from spldebugging import debugOutput
 
 # Locate column content.
 # Given an object and the column number, locate text in the given column.
@@ -241,13 +242,15 @@ class SPLTimeRangeDialog(wx.Dialog):
 
 # Cart Explorer helper.
 
-def _populateCarts(carts, cartlst, modifier, standardEdition=False):
+def _populateCarts(carts, cartlst, modifier, standardEdition=False, 
refresh=False):
        # The real cart string parser, a helper for cart explorer for building 
cart entries.
        # 5.2: Discard number row if SPL Standard is in use.
        if standardEdition: cartlst = cartlst[:12]
        for entry in cartlst:
                # An unassigned cart is stored with three consecutive commas, 
so skip it.
-               if ",,," in entry: continue
+               # 17.04: If refresh is on, the cart we're dealing with is the 
actual carts dictionary that was built previously.
+               noEntry = ",,," in entry
+               if noEntry and not refresh: continue
                # Pos between 1 and 12 = function carts, 13 through 24 = number 
row carts, modifiers are checked.
                pos = cartlst.index(entry)+1
                # If a cart name has commas or other characters, SPL surrounds 
the cart name with quotes (""), so parse it as well.
@@ -258,21 +261,27 @@ def _populateCarts(carts, cartlst, modifier, 
standardEdition=False):
                elif pos == 22: identifier = "0"
                elif pos == 23: identifier = "-"
                else: identifier = "="
-               if modifier == "main": cart = identifier
-               else: cart = "%s+%s"%(modifier, identifier)
-               carts[cart] = cartName
+               cart = identifier if not modifier else "+".join([modifier, 
identifier])
+               if noEntry and refresh:
+                       if cart in carts: del carts[cart]
+               else:
+                       carts[cart] = cartName
 
 # Cart file timestamps.
-_cartEditTimestamps = [0, 0, 0, 0]
+_cartEditTimestamps = None
                # Initialize Cart Explorer i.e. fetch carts.
 # Cart files list is for future use when custom cart names are used.
-def cartExplorerInit(StudioTitle, cartFiles=None):
+# if told to refresh, timestamps will be checked and updated banks will be 
reassigned.
+# Carts dictionary is used if and only if refresh is on, as it'll modify live 
cats.
+def cartExplorerInit(StudioTitle, cartFiles=None, refresh=False, carts=None):
        global _cartEditTimestamps
+       debugOutput("SPL: refreshing Cart Explorer" if refresh else "SPL: 
preparing cart Explorer")
        # Use cart files in SPL's data folder to build carts dictionary.
        # use a combination of SPL user name and static cart location to locate 
cart bank files.
        # Once the cart banks are located, use the routines in the populate 
method above to assign carts.
        # Since sstandard edition does not support number row carts, skip them 
if told to do so.
-       carts = {"standardLicense":StudioTitle.startswith("StationPlaylist 
Studio Standard")}
+       if carts is None: carts = 
{"standardLicense":StudioTitle.startswith("StationPlaylist Studio Standard")}
+       if refresh: carts["modifiedBanks"] = []
        # Obtain the "real" path for SPL via environment variables and open the 
cart data folder.
        cartsDataPath = 
os.path.join(os.environ["PROGRAMFILES"],"StationPlaylist","Data") # Provided 
that Studio was installed using default path.
        if cartFiles is None:
@@ -284,7 +293,10 @@ def cartExplorerInit(StudioTitle, cartFiles=None):
                if userNameIndex >= 0:
                        cartFiles = [StudioTitle[userNameIndex+2:]+" "+cartFile 
for cartFile in cartFiles]
        faultyCarts = False
+       if not refresh:
+               _cartEditTimestamps = []
        for f in cartFiles:
+               # Only do this if told to build cart banks from scratch, as 
refresh flag is set if cart explorer is active in the first place.
                try:
                        mod = f.split()[-2] # Checking for modifier string such 
as ctrl.
                        # Todo: Check just in case some SPL flavors doesn't 
ship with a particular cart file.
@@ -292,36 +304,31 @@ def cartExplorerInit(StudioTitle, cartFiles=None):
                        faultyCarts = True # In a rare event that the 
broadcaster has saved the cart bank with the name like "carts.cart".
                        continue
                cartFile = os.path.join(cartsDataPath,f)
-               if not os.path.isfile(cartFile): # Cart explorer will fail if 
whitespaces are in the beginning or at the end of a user name.
+               # Cart explorer can safely assume that the cart bank exists if 
refresh flag is set.
+               if not refresh and not os.path.isfile(cartFile): # Cart 
explorer will fail if whitespaces are in the beginning or at the end of a user 
name.
                        faultyCarts = True
                        continue
+               debugOutput("SPL: examining carts from file %s"%cartFile)
+               cartTimestamp = os.path.getmtime(cartFile)
+               if refresh and _cartEditTimestamps[cartFiles.index(f)] == 
cartTimestamp:
+                       debugOutput("SPL: no changes to cart bank, skipping")
+                       continue
+               _cartEditTimestamps.append(cartTimestamp)
                with open(cartFile) as cartInfo:
                        cl = [row for row in reader(cartInfo)]
-                       # 17.01: Look up file modification date to signal the 
app module that Cart Explorer reentry should occur.
-                       _cartEditTimestamps[cartFiles.index(f)] = 
os.path.getmtime(cartFile)
-               _populateCarts(carts, cl[1], mod, 
standardEdition=carts["standardLicense"]) # See the comment for _populate 
method above.
+               # 17.04 (optimization): let empty string represent main cart 
bank to avoid this being partially consulted up to 24 times.
+               # The below method will just check for string length, which is 
faster than looking for specific substring.
+               _populateCarts(carts, cl[1], mod if mod != "main" else "", 
standardEdition=carts["standardLicense"], refresh=refresh) # See the comment 
for _populate method above.
+               if not refresh:
+                       debugOutput("SPL: carts processed so far: 
%s"%(len(carts)-1))
        carts["faultyCarts"] = faultyCarts
+       debugOutput("SPL: total carts processed: %s"%(len(carts)-2))
        return carts
 
-# See if cart files were modified.
-# This is needed in order to announce Cart Explorer reentry command.
-def shouldCartExplorerRefresh(StudioTitle):
-       global _cartEditTimestamps
-       cartsDataPath = 
os.path.join(os.environ["PROGRAMFILES"],"StationPlaylist","Data") # Provided 
that Studio was installed using default path.
-       userNameIndex = StudioTitle.find("-")
-       # Until NVDA core moves to Python 3, assume that file names aren't 
unicode.
-       cartFiles = [u"main carts.cart", u"shift carts.cart", u"ctrl 
carts.cart", u"alt carts.cart"]
-       if userNameIndex >= 0:
-               cartFiles = [StudioTitle[userNameIndex+2:]+" "+cartFile for 
cartFile in cartFiles]
-       for f in cartFiles:
-               # No need to check for faulty carts here, as Cart Explorer 
activation checked it already.
-               timestamp = os.path.getmtime(os.path.join(cartsDataPath,f))
-               # 17.01: Look up file modification date to signal the app 
module that Cart Explorer reentry should occur.
-               # Optimization: Short-circuit if even one cart file has been 
modified.
-               if _cartEditTimestamps[cartFiles.index(f)] != timestamp:
-                       return True
-       return False
-
+# Refresh carts upon request.
+# calls cart explorer init with special (internal) flags.
+def cartExplorerRefresh(studioTitle, currentCarts):
+       return cartExplorerInit(studioTitle, refresh=True, carts=currentCarts)
 
 # Countdown timer.
 # This is utilized by many services, chiefly profile triggers routine.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/d45d6079eb60/
Changeset:   d45d6079eb60
Branch:      None
User:        josephsl
Date:        2017-02-07 17:15:43+00:00
Summary:     Update check (17.04): no more install prompt during add-on updates.

During add-on updates, just update the add-on and never prompt to install it. 
However, the reboot requirement prompt wil be displayed at the end.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index f8df6c5..d833e0c 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -281,8 +281,50 @@ class SPLUpdateDownloader(updateCheck.UpdateDownloader):
 
        def _downloadSuccess(self):
                self._stopped()
+               # Emulate add-on update (don't prompt to install).
                from gui import addonGui
-               wx.CallAfter(addonGui.AddonsDialog.handleRemoteAddonInstall, 
self.destPath.decode("mbcs"))
+               closeAfter = addonGui.AddonsDialog._instance is None
+               try:
+                       try:
+                               
bundle=addonHandler.AddonBundle(self.destPath.decode("mbcs"))
+                       except:
+                               log.error("Error opening addon bundle from 
%s"%addonPath,exc_info=True)
+                               # Translators: The message displayed when an 
error occurs when opening an add-on package for adding. 
+                               gui.messageBox(_("Failed to open add-on package 
file at %s - missing file or invalid file format")%addonPath,
+                                       # Translators: The title of a dialog 
presented when an error occurs.
+                                       _("Error"),
+                                       wx.OK | wx.ICON_ERROR)
+                               return
+                       bundleName=bundle.manifest['name']
+                       for addon in addonHandler.getAvailableAddons():
+                               if not addon.isPendingRemove and 
bundleName==addon.manifest['name']:
+                                       addon.requestRemove()
+                                       break
+                       progressDialog = 
gui.IndeterminateProgressDialog(gui.mainFrame,
+                       # Translators: The title of the dialog presented while 
an Addon is being updated.
+                       _("Updating Add-on"),
+                       # Translators: The message displayed while an addon is 
being updated.
+                       _("Please wait while the add-on is being updated."))
+                       try:
+                               
gui.ExecAndPump(addonHandler.installAddonBundle,bundle)
+                       except:
+                               log.error("Error installing  addon bundle from 
%s"%addonPath,exc_info=True)
+                               if not closeAfter: 
addonGui.AddonsDialog(gui.mainFrame).refreshAddonsList()
+                               progressDialog.done()
+                               del progressDialog
+                               # Translators: The message displayed when an 
error occurs when installing an add-on package.
+                               gui.messageBox(_("Failed to update add-on  from 
%s")%addonPath,
+                                       # Translators: The title of a dialog 
presented when an error occurs.
+                                       _("Error"),
+                                       wx.OK | wx.ICON_ERROR)
+                               return
+                       else:
+                               if not closeAfter: 
addonGui.AddonsDialog(gui.mainFrame).refreshAddonsList(activeIndex=-1)
+                               progressDialog.done()
+                               del progressDialog
+               finally:
+                       if closeAfter:
+                               wx.CallLater(1, 
addonGui.AddonsDialog(gui.mainFrame).Close)
 
 
 # These structs are only complete enough to achieve what we need.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/c0ae59d6141b/
Changeset:   c0ae59d6141b
Branch:      None
User:        josephsl
Date:        2017-02-07 18:19:53+00:00
Summary:     Update check (17.04): use update info dictionary for update 
results. fixes #21.

Emulating NVDA Core: an update info dictionary wil be reutrned that'll include 
current version and new version, simulating how NVDA Core fetches update 
inormation.
This is destined for add-on 17.04.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index d833e0c..71aa4e0 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -109,12 +109,14 @@ def checkForAddonUpdate():
        return None
 
 _progressDialog = None
-updateDictionary = True
 
 # The update check routine.
 # Auto is whether to respond with UI (manual check only), continuous takes in 
auto update check variable for restarting the timer.
 # ConfUpdateInterval comes from add-on config dictionary.
-def updateCheckerEx(auto=False, continuous=False, confUpdateInterval=1):
+def updateChecker(auto=False, continuous=False, confUpdateInterval=1):
+       if _pendingChannelChange:
+               wx.CallAfter(gui.messageBox, _("Did you recently tell SPL 
add-on to use a different update channel? If so, please restart NVDA before 
checking for add-on updates."), _("Update channel changed"), wx.ICON_ERROR)
+               return
        global _SPLUpdateT, SPLAddonCheck, _retryAfterFailure, _progressDialog, 
_updateNow
        if _updateNow: _updateNow = False
        import time
@@ -159,71 +161,6 @@ def updateCheckerEx(auto=False, continuous=False, 
confUpdateInterval=1):
        if not updateCandidate: wx.CallAfter(gui.messageBox, checkMessage, 
_("Studio add-on update"))
        else: wx.CallAfter(getUpdateResponse, checkMessage, _("Studio add-on 
update"), info["path"])
 
-def updateChecker(auto=False, continuous=False, confUpdateInterval=1):
-       if _pendingChannelChange:
-               wx.CallAfter(gui.messageBox, _("Did you recently tell SPL 
add-on to use a different update channel? If so, please restart NVDA before 
checking for add-on updates."), _("Update channel changed"), wx.ICON_ERROR)
-               return
-       if updateDictionary:
-               updateCheckerEx(auto=auto, continuous=continuous, 
confUpdateInterval=confUpdateInterval)
-               return
-       global _SPLUpdateT, SPLAddonCheck, _retryAfterFailure, _progressDialog, 
_updateNow
-       if _updateNow: _updateNow = False
-       import time
-       # Regardless of whether it is an auto check, update the check time.
-       # However, this shouldnt' be done if this is a retry after a failed 
attempt.
-       if not _retryAfterFailure: SPLAddonCheck = time.time()
-       updateInterval = confUpdateInterval*_updateInterval*1000
-       # Should the timer be set again?
-       if continuous and not _retryAfterFailure: 
_SPLUpdateT.Start(updateInterval, True)
-       # Auto disables UI portion of this function if no updates are pending.
-       # All the information will be stored in the URL object, so just close 
it once the headers are downloaded.
-       updateCandidate = False
-       updateURL = SPLUpdateURL if SPLUpdateChannel not in channels else 
channels[SPLUpdateChannel]
-       try:
-               import urllib
-               # Look up the channel if different from the default.
-               url = urllib.urlopen(updateURL)
-               url.close()
-       except IOError:
-               _retryAfterFailure = True
-               if not auto:
-                       wx.CallAfter(_progressDialog.done)
-                       _progressDialog = None
-                       # Translators: Error text shown when add-on update 
check fails.
-                       wx.CallAfter(gui.messageBox, _("Error checking for 
update."), _("Studio add-on update"), wx.ICON_ERROR)
-               if continuous: _SPLUpdateT.Start(600000, True)
-               return
-       if _retryAfterFailure:
-               _retryAfterFailure = False
-               # Now is the time to update the check time if this is a retry.
-               SPLAddonCheck = time.time()
-       if url.code != 200:
-               if auto:
-                       if continuous: _SPLUpdateT.Start(updateInterval, True)
-                       return # No need to interact with the user.
-               # Translators: Text shown when update check fails for some odd 
reason.
-               checkMessage = _("Add-on update check failed.")
-       else:
-               # Am I qualified to update?
-               import re
-               version = 
re.search("stationPlaylist-(?P<version>.*).nvda-addon", 
res.url).groupdict()["version"]
-               if version == SPLAddonVersion:
-                       if auto:
-                               if continuous: 
_SPLUpdateT.Start(updateInterval, True)
-                               return
-                       # Translators: Presented when no add-on update is 
available.
-                       checkMessage = _("No add-on update available.")
-               else:
-                       # Translators: Text shown if an add-on update is 
available.
-                       checkMessage = _("Studio add-on {newVersion} is 
available. Would you like to update?").format(newVersion = version)
-                       updateCandidate = True
-       if not auto:
-               wx.CallAfter(_progressDialog.done)
-               _progressDialog = None
-       # Translators: Title of the add-on update check dialog.
-       if not updateCandidate: wx.CallAfter(gui.messageBox, checkMessage, 
_("Studio add-on update"))
-       else: wx.CallAfter(getUpdateResponse, checkMessage, _("Studio add-on 
update"), updateURL)
-
 def getUpdateResponse(message, caption, updateURL):
        if gui.messageBox(message, caption, wx.YES_NO | wx.NO_DEFAULT | 
wx.CANCEL | wx.CENTER | wx.ICON_QUESTION) == wx.YES:
                SPLUpdateDownloader([updateURL]).start()


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/6df1d2312713/
Changeset:   6df1d2312713
Branch:      None
User:        josephsl
Date:        2017-02-07 18:35:06+00:00
Summary:     Readme entries for update check fix and debugging framework. fixes 
#34, #24

Affected #:  1 file

diff --git a/readme.md b/readme.md
index ea471b7..bdad363 100755
--- a/readme.md
+++ b/readme.md
@@ -176,8 +176,10 @@ If you are using Studio on a touchscreen computer running 
Windows 8 or later and
 
 ## Version 17.04-dev
 
+* Added a basic add-on debugging support by logging various information while 
the add-on is active with NVDA set to debug logging (requires NVDA 2017.1 and 
later). To use this, after installing NVDA 2017.1, from Exit NVDA dialog, 
choose "restart with debug logging enabled" option.
 * Improvements to presentation of various add-on dialogs thanks to NVDA 2016.4 
features.
 * NVDA will download add-on updates in the background if you say "yes" when 
asked to update the add-on. Consequently, file download notifications from web 
browsers will no longer be shown.
+* NVDA will no longer appear to freeze when checking for update at startup due 
to add-on update channel change.
 * Added ability to press Control+Alt+up or down arrow keys to move between 
tracks (specifically, track columns) vertically just as one is moving to next 
or previous row in a table.
 * Added a combo box in add-on settings dialog to set which column should be 
announced when moving through columns vertically.
 * Moved end of track , intro and microphone alarm controls from add-on 
settings to the new Alarms Center.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/1dc065d224a8/
Changeset:   1dc065d224a8
Branch:      None
User:        josephsl
Date:        2017-02-08 22:59:18+00:00
Summary:     SPL Status global command (17.04): Cart edit/insert mode output 
now follows that of SPL's own output.

Affected #:  1 file

diff --git a/addon/globalPlugins/splUtils/__init__.py 
b/addon/globalPlugins/splUtils/__init__.py
index 6a3db64..68e5268 100755
--- a/addon/globalPlugins/splUtils/__init__.py
+++ b/addon/globalPlugins/splUtils/__init__.py
@@ -258,9 +258,9 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin):
                        statusInfo.append("Record to file on" if 
winUser.sendMessage(SPLWin, 1024, 4, SPLStatusInfo) else "Record to file off")
                        cartEdit = winUser.sendMessage(SPLWin, 1024, 5, 
SPLStatusInfo)
                        cartInsert = winUser.sendMessage(SPLWin, 1024, 6, 
SPLStatusInfo)
-                       if cartEdit: statusInfo.append("Cart edit on")
-                       elif not cartEdit and cartInsert: 
statusInfo.append("Cart insert on")
-                       else: statusInfo.append("Cart edit off")
+                       if cartEdit: statusInfo.append("Cart Edit on")
+                       elif not cartEdit and cartInsert: 
statusInfo.append("Cart Insert on")
+                       else: statusInfo.append("Cart Edit off")
                ui.message("; ".join(statusInfo))
                self.finish()
        # Translators: Input help message for a SPL Controller command.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/d24a17c677b6/
Changeset:   d24a17c677b6
Branch:      None
User:        josephsl
Date:        2017-02-09 02:39:53+00:00
Summary:     Playlist snapshots (17.04): add an option to display the results 
window when snapshots command was first pressed.

Requested by a broadcaster: add an option to allow the snapshots results window 
to come up when snapshots command (SPL Assistant, F8 or a custom gesture) is 
pressed once. This is achieved by adding a 1 to script count if this flag is in 
effect (by default, it'll be off).

Affected #:  3 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 6d1c201..67667be 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1913,11 +1913,14 @@ class AppModule(appModuleHandler.AppModule):
                        self.finish()
                        return
                scriptCount = scriptHandler.getLastScriptRepeatCount()
+               # Display the decorated HTML window on the first press if told 
to do so.
+               if 
splconfig.SPLConfig["PlaylistSnapshots"]["ShowResultsWindowOnFirstPress"]:
+                       scriptCount += 1
                # Never allow this to be invoked more than twice, as it causes 
performance degredation and multiple HTML windows are opened.
                if scriptCount >= 2:
                        self.finish()
                        return
-                       # Speak and braille on the first press, display a 
decorated HTML message for subsequent presses.
+               # Speak and braille on the first press, display a decorated 
HTML message for subsequent presses.
                
self.playlistSnapshotOutput(self.playlistSnapshots(obj.parent.firstChild, 
None), scriptCount)
                self.finish()
        # Translators: Input help mode message for a command in Station 
Playlist Studio.

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index a3d9b38..fa531a2 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -55,6 +55,7 @@ CategoryCount = boolean(default=true)
 CategoryCountLimit = integer(min=0, max=10, default=5)
 GenreCount = boolean(default=true)
 GenreCountLimit = integer(min=0, max=10, default=5)
+ShowResultsWindowOnFirstPress = boolean(default=false)
 [IntroOutroAlarms]
 SayEndOfTrack = boolean(default=true)
 EndOfTrackTime = integer(min=1, max=59, default=5)

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 95b7250..a66f6ba 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -197,6 +197,7 @@ class SPLConfigDialog(gui.SettingsDialog):
                self.playlistCategoryCountLimit = 
splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCountLimit"]
                self.playlistGenreCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["GenreCount"]
                self.playlistGenreCountLimit = 
splconfig.SPLConfig["PlaylistSnapshots"]["GenreCountLimit"]
+               self.resultsWindowOnFirstPress = 
splconfig.SPLConfig["PlaylistSnapshots"]["ShowResultsWindowOnFirstPress"]
 
                sizer = gui.guiHelper.BoxSizerHelper(self, 
orientation=wx.HORIZONTAL)
                self.metadataValues=[("off",_("Off")),
@@ -298,6 +299,7 @@ class SPLConfigDialog(gui.SettingsDialog):
                splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCountLimit"] 
= self.playlistCategoryCountLimit
                splconfig.SPLConfig["PlaylistSnapshots"]["GenreCount"] = 
self.playlistGenreCount
                splconfig.SPLConfig["PlaylistSnapshots"]["GenreCountLimit"] = 
self.playlistGenreCountLimit
+               
splconfig.SPLConfig["PlaylistSnapshots"]["ShowResultsWindowOnFirstPress"] = 
self.resultsWindowOnFirstPress
                splconfig.SPLConfig["General"]["MetadataReminder"] = 
self.metadataValues[self.metadataList.GetSelection()][0]
                splconfig.SPLConfig["MetadataStreaming"]["MetadataEnabled"] = 
self.metadataStreams
                
splconfig.SPLConfig["ColumnAnnouncement"]["UseScreenColumnOrder"] = 
self.columnOrderCheckbox.Value
@@ -935,6 +937,9 @@ class PlaylistSnapshotsDialog(wx.Dialog):
                
self.playlistGenreCountCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("Genre count")))
                
self.playlistGenreCountCheckbox.SetValue(parent.playlistGenreCount)
                
self.playlistGenreCountLimit=playlistSnapshotsHelper.addLabeledControl(_("Top 
genre count (0 displays all genres)"), gui.nvdaControls.SelectOnFocusSpinCtrl, 
min=0, max=10, initial=parent.playlistGenreCountLimit)
+               # Translators: the label for a setting in SPL add-on settings 
to show playlist snaphsots window when the snapshots command is pressed once.
+               
self.resultsWindowOnFirstPressCheckbox=playlistSnapshotsHelper.addItem(wx.CheckBox(self,
 label=_("&Show results window when playlist snapshots command is performed 
once")))
+               
self.resultsWindowOnFirstPressCheckbox.SetValue(parent.resultsWindowOnFirstPress)
 
                
playlistSnapshotsHelper.addDialogDismissButtons(self.CreateButtonSizer(wx.OK | 
wx.CANCEL))
                self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK)
@@ -955,6 +960,7 @@ class PlaylistSnapshotsDialog(wx.Dialog):
                parent.playlistCategoryCountLimit = 
self.playlistCategoryCountLimit.GetValue()
                parent.playlistGenreCount = 
self.playlistGenreCountCheckbox.Value
                parent.playlistGenreCountLimit = 
self.playlistGenreCountLimit.GetValue()
+               parent.resultsWindowOnFirstPress = 
self.resultsWindowOnFirstPressCheckbox.Value
                parent.profiles.SetFocus()
                parent.Enable()
                self.Destroy()


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/079843bcd4f0/
Changeset:   079843bcd4f0
Branch:      None
User:        josephsl
Date:        2017-02-13 03:36:54+00:00
Summary:     Merge branch 'stable'

Affected #:  1 file

diff --git a/addon/locale/he/LC_MESSAGES/nvda.po 
b/addon/locale/he/LC_MESSAGES/nvda.po
index 70f5a7d..80f95ab 100644
--- a/addon/locale/he/LC_MESSAGES/nvda.po
+++ b/addon/locale/he/LC_MESSAGES/nvda.po
@@ -8,7 +8,7 @@ msgstr ""
 "Project-Id-Version: stationPlaylist 5.5\n"
 "Report-Msgid-Bugs-To: nvda-translations@xxxxxxxxxxxxx\n"
 "POT-Creation-Date: 2015-09-25 19:00+1000\n"
-"PO-Revision-Date: 2016-11-21 21:09+0200\n"
+"PO-Revision-Date: 2017-02-05 01:29+0200\n"
 "Last-Translator:  <shmuel_naaman@xxxxxxxxx>\n"
 "Language-Team: \n"
 "Language: he\n"
@@ -141,7 +141,6 @@ msgid "Status: {name}"
 msgstr "מצב: {name}"
 
 #. Translators: The text of the help command in SPL Assistant layer.
-#, fuzzy
 msgid ""
 "After entering SPL Assistant, press:\n"
 "A: Automation.\n"
@@ -176,7 +175,7 @@ msgid ""
 "Shift+F1: Open online user guide."
 msgstr ""
 "לאחר הכניסה למסייע של SPL נא להקיש :\n"
-"A: אוטומציה\n"
+" A: אוטומציה\n"
 "C :  מיתוג סייר\n"
 "Shift+C: הקראת שם הרצועה המנגנת כעת\n"
 "D: הזמן הנותר ברשימת הנגינה\n"
@@ -208,7 +207,6 @@ msgstr ""
 "Shift+F1: פתיחת המדריך המקוון"
 
 #. Translators: The text of the help command in SPL Assistant layer when JFW 
layer is active.
-#, fuzzy
 msgid ""
 "After entering SPL Assistant, press:\n"
 "A: Automation.\n"
@@ -246,7 +244,7 @@ msgid ""
 msgstr ""
 "לאחר הכניסה למסייע של SPL נא להקיש :\n"
 "A: אוטומציה\n"
-"C :  מיתוג סייר\n"
+" C :  מיתוג סייר\n"
 "Shift+C: הקראת שם הרצועה המנגנת כעת\n"
 "D: הזמן הנותר ברשימת הנגינה\n"
 "E: קבלת מידע כולל על זרימת המטאדאטה\n"
@@ -277,7 +275,6 @@ msgstr ""
 "Shift+F1: פתיחת המדריך המקוון"
 
 #. Translators: The text of the help command in SPL Assistant layer when 
Window-Eyes layer is active.
-#, fuzzy
 msgid ""
 "After entering SPL Assistant, press:\n"
 "A: Automation.\n"
@@ -317,7 +314,7 @@ msgid ""
 msgstr ""
 "לאחר הכניסה למסייע של SPL נא להקיש :\n"
 "A: אוטומציה\n"
-"C :  מיתוג סייר\n"
+" C :  מיתוג סייר\n"
 "Shift+C: הקראת שם הרצועה המנגנת כעת\n"
 "D: הזמן הנותר ברשימת הנגינה\n"
 "E: קבלת מידע כולל על זרימת המטאדאטה\n"
@@ -738,9 +735,8 @@ msgid "Normal profile"
 msgstr "פרופיל בסיסי"
 
 #. Consult profile-specific key first before deleting anything.
-#, fuzzy
 msgid "Normal Profile"
-msgstr "פרופיל בסיסי"
+msgstr "פרופיל בסיסי "
 
 #. Translators: Presented when trying to switch to an instant switch profile 
when add-on settings dialog is active.
 msgid "Add-on settings dialog is open, cannot switch profiles"
@@ -864,9 +860,8 @@ msgstr ""
 "נא להקיש אורך זמן התראת ההקלטה בשניות (כרגע לא פעיל. ) ערך 0, פירושו ללא "
 "ההתראה."
 
-#, fuzzy
 msgid "Microphone alarm interval"
-msgstr "התראה על ההקלטה ב&פרקי זמן קצובים"
+msgstr "התראה על ההקלטה ב&פרקי זמן קצובים "
 
 #. Translators: Title of a dialog displayed when the add-on starts reminding 
broadcasters to disable audio ducking.
 msgid "SPL Studio and audio ducking"
@@ -919,9 +914,8 @@ msgid ""
 msgstr "ברוכים הבאים לתוסף NVDA לStation Playlist."
 
 #. Translators: Title of a dialog displayed when the add-on starts presenting 
basic information, similar to NVDA's own welcome dialog.
-#, fuzzy
 msgid "Welcome to StationPlaylist Studio add-on"
-msgstr "StationPlaylist Studio"
+msgstr "ברוך הבא להרחבת StationPlaylist "
 
 #. Translators: A checkbox to show welcome dialog.
 msgid "Show welcome dialog when I start Studio"
@@ -1127,19 +1121,16 @@ msgid "&Beep for different track categories"
 msgstr "צל&יל שונה עבור סוגים של רצועות"
 
 #. Translators: the label for a setting in SPL add-on settings to set how 
track comments are announced.
-#, fuzzy
 msgid "&Track comment announcement:"
-msgstr "הקראת שם הר&צועות"
+msgstr "הקראת שם הר&צועות "
 
 #. Translators: One of the track comment notification settings.
-#, fuzzy
 msgid "Message"
-msgstr "במלל"
+msgstr "הודעה"
 
 #. Translators: One of the track comment notification settings.
-#, fuzzy
 msgid "Beep"
-msgstr "בצלילים"
+msgstr "צלילים"
 
 #. Translators: the label for a setting in SPL add-on settings to toggle top 
and bottom notification.
 msgid "Notify when located at &top or bottom of playlist viewer"
@@ -1174,9 +1165,8 @@ msgid "Columns E&xplorer..."
 msgstr "סייר &הפקודות..."
 
 #. Translators: The label of a button to configure columns explorer slots for 
Track Tool (SPL Assistant, number row keys to announce specific columns).
-#, fuzzy
 msgid "Columns Explorer for &Track Tool..."
-msgstr "סייר &הפקודות..."
+msgstr "סייר &הפקודות... "
 
 #. Translators: The label of a button to open advanced options such as using 
SPL Controller command to invoke Assistant layer.
 msgid "&Status announcements..."
@@ -1187,9 +1177,8 @@ msgid "&Advanced options..."
 msgstr "אפשרויות מתק&דמות..."
 
 #. Translators: The label for a button in SPL add-on configuration dialog to 
reset settings to defaults.
-#, fuzzy
 msgid "Reset settings..."
-msgstr "שחזור הגדרות"
+msgstr "שחזור הגדרות ברירת מחדל"
 
 #. Translators: A dialog message shown when add-on update channel has changed.
 msgid ""
@@ -1347,9 +1336,8 @@ msgid "Columns Explorer"
 msgstr "סייר העמודות"
 
 #. Translators: The title of Columns Explorer configuration dialog.
-#, fuzzy
 msgid "Columns Explorer for Track Tool"
-msgstr "סייר העמודות"
+msgstr "סייר העמודות "
 
 #. Translators: The label for a setting in SPL add-on dialog to select column 
for this column slot.
 #, python-brace-format
@@ -1417,24 +1405,20 @@ msgid "Reset settings"
 msgstr "שחזור הגדרות"
 
 #. Translators: the label for resetting profile triggers.
-#, fuzzy
 msgid "Reset instant switch profile"
-msgstr "זהו פרופיל ל&החלפה מיידית"
+msgstr "זהו פרופיל ל&החלפה מיידית "
 
 #. Translators: the label for resetting profile triggers.
-#, fuzzy
 msgid "Delete time-based profile database"
-msgstr "פרופילים מבוססי זמן חסרים."
+msgstr "פרופילים מבוססי זמן חסרים. "
 
 #. Translators: the label for resetting encoder settings.
-#, fuzzy
 msgid "Remove encoder settings"
-msgstr "שגיות בהגדרות מנגנון הקידוד."
+msgstr "הסר הגדרות קידוד"
 
 #. Translators: the label for resetting track comments.
-#, fuzzy
 msgid "Erase track comments"
-msgstr "סיום הרצועות בברייל"
+msgstr "מחר רצועת הערה"
 
 #. Translators: A message to warn about resetting SPL config settings to 
factory defaults.
 msgid "Are you sure you wish to reset SPL add-on settings to defaults?"


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/570769537b08/
Changeset:   570769537b08
Branch:      None
User:        josephsl
Date:        2017-02-13 04:47:13+00:00
Summary:     Update check: unused imports removed, certain data structures 
removed.

Some data structures, especially used for SSL, are now pulled in from update 
check module (NVDA Core). Also, unused imports were removed, and logging has 
been improved (no longer referencing a nonexistent addonPath variable).

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 71aa4e0..c00942d 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -8,18 +8,13 @@ import os # Essentially, update download is no different than 
file downloads.
 import cPickle
 import threading
 import tempfile
-import hashlib
-import ctypes.wintypes
+import ctypes
 import ssl
-import wx
-import shellapi
 import gui
 import wx
 import addonHandler
 import globalVars
 import updateCheck
-import config
-import winUser
 
 # Add-on manifest routine (credit: various add-on authors including Noelia 
Martinez).
 # Do not rely on using absolute path to open to manifest, as installation 
directory may change in a future NVDA Core version (highly unlikely, but...).
@@ -96,7 +91,7 @@ def checkForAddonUpdate():
                        raise
        if res.code != 200:
                raise RuntimeError("Checking for update failed with code %d" % 
res.code)
-       # Build emulated add-on update dictionary if there is indeed a new 
verison.
+       # Build emulated add-on update dictionary if there is indeed a new 
version.
        # The add-on version is of the form "x.y.z". The "-dev" suffix 
indicates development release.
        # Anything after "-dev" indicates a try or a custom build.
        # LTS: Support upgrading between LTS releases.
@@ -167,10 +162,6 @@ def getUpdateResponse(message, caption, updateURL):
 
 # Update downloader (credit: NV Access)
 # Customized for SPL add-on.
-
-#: The download block size in bytes.
-DOWNLOAD_BLOCK_SIZE = 8192 # 8 kb
-
 class SPLUpdateDownloader(updateCheck.UpdateDownloader):
        """Overrides NVDA Core's downloader.)
        No hash checking for now, and URL's and temp file paths are different.
@@ -225,9 +216,9 @@ class SPLUpdateDownloader(updateCheck.UpdateDownloader):
                        try:
                                
bundle=addonHandler.AddonBundle(self.destPath.decode("mbcs"))
                        except:
-                               log.error("Error opening addon bundle from 
%s"%addonPath,exc_info=True)
+                               log.error("Error opening addon bundle from 
%s"%self.destPath,exc_info=True)
                                # Translators: The message displayed when an 
error occurs when opening an add-on package for adding. 
-                               gui.messageBox(_("Failed to open add-on package 
file at %s - missing file or invalid file format")%addonPath,
+                               gui.messageBox(_("Failed to open add-on package 
file at %s - missing file or invalid file format")%self.destPath,
                                        # Translators: The title of a dialog 
presented when an error occurs.
                                        _("Error"),
                                        wx.OK | wx.ICON_ERROR)
@@ -245,12 +236,12 @@ class SPLUpdateDownloader(updateCheck.UpdateDownloader):
                        try:
                                
gui.ExecAndPump(addonHandler.installAddonBundle,bundle)
                        except:
-                               log.error("Error installing  addon bundle from 
%s"%addonPath,exc_info=True)
+                               log.error("Error installing  addon bundle from 
%s"%self.destPath,exc_info=True)
                                if not closeAfter: 
addonGui.AddonsDialog(gui.mainFrame).refreshAddonsList()
                                progressDialog.done()
                                del progressDialog
                                # Translators: The message displayed when an 
error occurs when installing an add-on package.
-                               gui.messageBox(_("Failed to update add-on  from 
%s")%addonPath,
+                               gui.messageBox(_("Failed to update add-on  from 
%s")%self.destPath,
                                        # Translators: The title of a dialog 
presented when an error occurs.
                                        _("Error"),
                                        wx.OK | wx.ICON_ERROR)
@@ -260,33 +251,15 @@ class SPLUpdateDownloader(updateCheck.UpdateDownloader):
                                progressDialog.done()
                                del progressDialog
                finally:
+                       try:
+                               os.remove(self.destPath)
+                       except OSError:
+                               pass
                        if closeAfter:
                                wx.CallLater(1, 
addonGui.AddonsDialog(gui.mainFrame).Close)
 
 
-# These structs are only complete enough to achieve what we need.
-class CERT_USAGE_MATCH(ctypes.Structure):
-       _fields_ = (
-               ("dwType", ctypes.wintypes.DWORD),
-               # CERT_ENHKEY_USAGE struct
-               ("cUsageIdentifier", ctypes.wintypes.DWORD),
-               ("rgpszUsageIdentifier", ctypes.c_void_p), # LPSTR *
-       )
-
-class CERT_CHAIN_PARA(ctypes.Structure):
-       _fields_ = (
-               ("cbSize", ctypes.wintypes.DWORD),
-               ("RequestedUsage", CERT_USAGE_MATCH),
-               ("RequestedIssuancePolicy", CERT_USAGE_MATCH),
-               ("dwUrlRetrievalTimeout", ctypes.wintypes.DWORD),
-               ("fCheckRevocationFreshnessTime", ctypes.wintypes.BOOL),
-               ("dwRevocationFreshnessTime", ctypes.wintypes.DWORD),
-               ("pftCacheResync", ctypes.c_void_p), # LPFILETIME
-               ("pStrongSignPara", ctypes.c_void_p), # PCCERT_STRONG_SIGN_PARA
-               ("dwStrongSignFlags", ctypes.wintypes.DWORD),
-       )
-
-# Borrowed from NVDA Core (the only difference is the URL).
+# Borrowed from NVDA Core (the only difference is the URL and where structures 
are coming from).
 def _updateWindowsRootCertificates():
        crypt = ctypes.windll.crypt32
        # Get the server certificate.
@@ -302,8 +275,8 @@ def _updateWindowsRootCertificates():
        # Ask Windows to build a certificate chain, thus triggering a root 
certificate update.
        chainCont = ctypes.c_void_p()
        crypt.CertGetCertificateChain(None, certCont, None, None,
-               
ctypes.byref(CERT_CHAIN_PARA(cbSize=ctypes.sizeof(CERT_CHAIN_PARA),
-                       RequestedUsage=CERT_USAGE_MATCH())),
+               
ctypes.byref(updateCheck.CERT_CHAIN_PARA(cbSize=ctypes.sizeof(updateCheck.CERT_CHAIN_PARA),
+                       RequestedUsage=updateCheck.CERT_USAGE_MATCH())),
                0, None,
                ctypes.byref(chainCont))
        crypt.CertFreeCertificateChain(chainCont)


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/4f2b0487f7c4/
Changeset:   4f2b0487f7c4
Branch:      None
User:        josephsl
Date:        2017-02-15 17:11:09+00:00
Summary:     Debug logging (17.04): oops, splupdate.updateChannel does not 
exist (typo).

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 67667be..dea0c15 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -600,7 +600,7 @@ class AppModule(appModuleHandler.AppModule):
                # Check for add-on update if told to do so.
                # LTS: Only do this if channel hasn't changed.
                if splconfig.SPLConfig["Update"]["AutoUpdateCheck"] or 
splupdate._updateNow:
-                       debugOutput("SPL: checking for add-on updates from %s 
channel"%splupdate.updateChannel)
+                       debugOutput("SPL: checking for add-on updates from %s 
channel"%splupdate.SPLUpdateChannel)
                        # 7.0: Have a timer call the update function indirectly.
                        import queueHandler
                        queueHandler.queueFunction(queueHandler.eventQueue, 
splconfig.updateInit)


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/148d4c230b81/
Changeset:   148d4c230b81
Branch:      None
User:        josephsl
Date:        2017-02-15 19:47:03+00:00
Summary:     Update check: assign the update candidate variable until update 
dictionary is fully used.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index c00942d..6663174 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -123,6 +123,7 @@ def updateChecker(auto=False, continuous=False, 
confUpdateInterval=1):
        # Should the timer be set again?
        if continuous and not _retryAfterFailure: 
_SPLUpdateT.Start(updateInterval, True)
        # Auto disables UI portion of this function if no updates are pending.
+       updateCandidate = False
        try:
                info = checkForAddonUpdate()
        except:


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/3116816d0b97/
Changeset:   3116816d0b97
Branch:      None
User:        josephsl
Date:        2017-02-17 18:21:50+00:00
Summary:     Update check: simplified UI code.

Because an update dictionary will be returned, there's really no need to say 
update candidate flag. Also simplified the UI code for displaying update result 
dialog.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 6663174..1b26bbd 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -123,7 +123,6 @@ def updateChecker(auto=False, continuous=False, 
confUpdateInterval=1):
        # Should the timer be set again?
        if continuous and not _retryAfterFailure: 
_SPLUpdateT.Start(updateInterval, True)
        # Auto disables UI portion of this function if no updates are pending.
-       updateCandidate = False
        try:
                info = checkForAddonUpdate()
        except:
@@ -140,22 +139,21 @@ def updateChecker(auto=False, continuous=False, 
confUpdateInterval=1):
                _retryAfterFailure = False
                # Now is the time to update the check time if this is a retry.
                SPLAddonCheck = time.time()
+       if not auto:
+               wx.CallAfter(_progressDialog.done)
+               _progressDialog = None
+       # Translators: Title of the add-on update check dialog.
+       dialogTitle = _("Studio add-on update")
        if info is None:
                if auto:
                        if continuous: _SPLUpdateT.Start(updateInterval, True)
                        return # No need to interact with the user.
                # Translators: Presented when no add-on update is available.
-               checkMessage = _("No add-on update available.")
+               wx.CallAfter(gui.messageBox, _("No add-on update available."), 
dialogTitle)
        else:
                # Translators: Text shown if an add-on update is available.
                checkMessage = _("Studio add-on {newVersion} is available. 
Would you like to update?").format(newVersion = info["newVersion"])
-               updateCandidate = True
-       if not auto:
-               wx.CallAfter(_progressDialog.done)
-               _progressDialog = None
-       # Translators: Title of the add-on update check dialog.
-       if not updateCandidate: wx.CallAfter(gui.messageBox, checkMessage, 
_("Studio add-on update"))
-       else: wx.CallAfter(getUpdateResponse, checkMessage, _("Studio add-on 
update"), info["path"])
+               wx.CallAfter(getUpdateResponse, checkMessage, dialogTitle, 
info["path"])
 
 def getUpdateResponse(message, caption, updateURL):
        if gui.messageBox(message, caption, wx.YES_NO | wx.NO_DEFAULT | 
wx.CANCEL | wx.CENTER | wx.ICON_QUESTION) == wx.YES:


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/82ec06f140ad/
Changeset:   82ec06f140ad
Branch:      None
User:        josephsl
Date:        2017-02-19 21:11:06+00:00
Summary:     Translatable strings: Normal Profile -> Normal profile, update 
add-on component files to point to SPL Utils global plugin.

Affected #:  2 files

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index fa531a2..6ef7fca 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -93,6 +93,8 @@ _mutatableSettings=("IntroOutroAlarms", "MicrophoneAlarm", 
"MetadataStreaming",
 # 7.0: Profile-specific confspec (might be removed once a more optimal way to 
validate sections is found).
 # Dictionary comprehension is better here.
 confspecprofiles = {sect:key for sect, key in confspec.iteritems() if sect in 
_mutatableSettings}
+# Translators: The name of the default (normal) profile.
+defaultProfileName = _("Normal profile")
 
 # 8.0: Run-time config storage and management will use ConfigHub data 
structure, a subclass of chain map.
 # A chain map allows a dictionary to look up predefined mappings to locate a 
key.
@@ -113,8 +115,7 @@ class ConfigHub(ChainMap):
                super(ConfigHub, self).__init__()
                # For presentational purposes.
                self.profileNames = []
-               # Translators: The name of the default (normal) profile.
-               self.maps[0] = self._unlockConfig(SPLIni, profileName=_("Normal 
profile"), prefill=True, validateNow=True)
+               self.maps[0] = self._unlockConfig(SPLIni, 
profileName=defaultProfileName, prefill=True, validateNow=True)
                self.profileNames.append(None) # Signifying normal profile.
                # Always cache normal profile upon startup.
                self._cacheConfig(self.maps[0])
@@ -237,7 +238,7 @@ class ConfigHub(ChainMap):
        def deleteProfile(self, name):
                # Bring normal profile to the front if it isn't.
                # Optimization: Tell the swapper that we need index to the 
normal profile for this case.
-               configPos = self.swapProfiles(name, _("Normal profile"), 
showSwitchIndex=True) if self.profiles[0].name == name else 
self.profileIndexByName(name)
+               configPos = self.swapProfiles(name, defaultProfileName, 
showSwitchIndex=True) if self.profiles[0].name == name else 
self.profileIndexByName(name)
                profilePos = self.profileNames.index(name)
                try:
                        os.remove(self.profiles[configPos].filename)
@@ -259,7 +260,7 @@ class ConfigHub(ChainMap):
 
        def __delitem__(self, key):
                # Consult profile-specific key first before deleting anything.
-               pos = 0 if key in _mutatableSettings else [profile.name for 
profile in self.maps].index(_("Normal Profile"))
+               pos = 0 if key in _mutatableSettings else [profile.name for 
profile in self.maps].index(defaultProfileName)
                try:
                        del self.maps[pos][key]
                except KeyError:
@@ -270,7 +271,7 @@ class ConfigHub(ChainMap):
                # 7.0: Save normal profile first.
                # Temporarily merge normal profile.
                # 8.0: Locate the index instead.
-               normalProfile = self.profileIndexByName(_("Normal profile"))
+               normalProfile = self.profileIndexByName(defaultProfileName)
                _preSave(self.profiles[normalProfile])
                # Disk write optimization check please.
                # 8.0: Bypass this if profiles were reset.
@@ -315,10 +316,10 @@ class ConfigHub(ChainMap):
                        # Convert certain settings to a different format.
                        conf["ColumnAnnouncement"]["IncludedColumns"] = 
set(_SPLDefaults["ColumnAnnouncement"]["IncludedColumns"])
                # Switch back to normal profile via a custom variant of swap 
routine.
-               if self.profiles[0].name != _("Normal profile"):
-                       npIndex = self.profileIndexByName(_("Normal profile"))
+               if self.profiles[0].name != defaultProfileName:
+                       npIndex = self.profileIndexByName(defaultProfileName)
                        self.profiles[0], self.profiles[npIndex] = 
self.profiles[npIndex], self.profiles[0]
-                       self.activeProfile = _("Normal profile")
+                       self.activeProfile = defaultProfileName
                # 8.0 optimization: Tell other modules that reset was done in 
order to postpone disk writes until the end.
                self.resetHappened = True
 
@@ -770,7 +771,7 @@ def switchProfile(prevProfile, newProfile):
        SPLConfig.switchProfile(prevProfile, newProfile)
        SPLPrevProfile = prevProfile
        # 8.0: Cache other profiles this time.
-       if newProfile != _("Normal profile") and newProfile not in _SPLCache:
+       if newProfile != defaultProfileName and newProfile not in _SPLCache:
                _cacheConfig(getProfileByName(selectedProfile))
 
 # Called from within the app module.

diff --git a/buildVars.py b/buildVars.py
index 26e6739..5b65263 100755
--- a/buildVars.py
+++ b/buildVars.py
@@ -37,7 +37,7 @@ import os.path
 pythonSources = [os.path.join("addon", "*.py"),
 os.path.join("addon", "appModules", "*.py"),
 os.path.join("addon", "appModules", "splstudio", "*.py"),
-os.path.join("addon", "globalPlugins", "SPLStudioUtils", "*.py")]
+os.path.join("addon", "globalPlugins", "splUtils", "*.py")]
 
 # Files that contain strings for translation. Usually your python sources
 i18nSources = pythonSources + ["buildVars.py"]


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/b4bac115c783/
Changeset:   b4bac115c783
Branch:      None
User:        josephsl
Date:        2017-02-24 17:18:41+00:00
Summary:     Playlist snapshots (17.04): translatable strings.

For some, the percent character has been employed in order to avoid index error 
exception. Some uses numeric interpolation substitutions, while others have 
friendly variable names defined (will be explained in translator comments).

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index dea0c15..1a64e89 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1494,59 +1494,65 @@ class AppModule(appModuleHandler.AppModule):
 # Output formatter for playlist snapshots.
 # Pressed once will speak and/or braille it, pressing twice or more will 
output this info to an HTML file.
        def playlistSnapshotOutput(self, snapshot, scriptCount):
-               statusInfo = ["Items: %s"%snapshot["PlaylistItemCount"]]
-               statusInfo.append("Tracks: %s"%snapshot["PlaylistTrackCount"])
-               statusInfo.append("Duration: 
%s"%snapshot["PlaylistDurationTotal"])
+               statusInfo = [_("Items: 
{playlistItemCount}").format(playlistItemCount = snapshot["PlaylistItemCount"])]
+               statusInfo.append(_("Tracks: 
{playlistTrackCount}").format(playlistTrackCount = 
snapshot["PlaylistTrackCount"]))
+               statusInfo.append(_("Duration: 
{playlistTotalDuration}").format(playlistTotalDuration = 
snapshot["PlaylistDurationTotal"]))
                if "PlaylistDurationMin" in snapshot:
-                       statusInfo.append("Shortest: 
%s"%snapshot["PlaylistDurationMin"])
-                       statusInfo.append("Longest: 
%s"%snapshot["PlaylistDurationMax"])
+                       statusInfo.append(_("Shortest: 
{playlistShortestTrack}").format(playlistShortestTrack = 
snapshot["PlaylistDurationMin"]))
+                       statusInfo.append(_("Longest: 
{playlistLongestTrack}").format(playlistLongestTrack = 
snapshot["PlaylistDurationMax"]))
                if "PlaylistDurationAverage" in snapshot:
-                       statusInfo.append("Average: 
%s"%snapshot["PlaylistDurationAverage"])
+                       statusInfo.append(_("Average: 
{playlistAverageDuration}").format(playlistAverageDuration = 
snapshot["PlaylistDurationAverage"]))
                if "PlaylistArtistCount" in snapshot:
                        artistCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["ArtistCountLimit"]
                        artists = 
snapshot["PlaylistArtistCount"].most_common(None if not artistCount else 
artistCount)
                        if scriptCount == 0:
-                               statusInfo.append("Top artist: %s 
(%s)"%(artists[0][:]))
+                               statusInfo.append(_("Top artist: %s 
(%s)")%(artists[0][:]))
                        elif scriptCount == 1:
                                artistList = []
+                               header = _("Top artists:")
                                for item in artists:
                                        artist, count = item
                                        if artist is None:
-                                               artistList.append("<li>No 
artist information (%s)</li>"%(count))
+                                               info = _("No artist information 
({artistCount})").format(artistCount = count)
                                        else:
-                                               artistList.append("<li>%s 
(%s)</li>"%(artist, count))
-                               statusInfo.append("".join(["Top artists:<ol>", 
"\n".join(artistList), "</ol>"]))
+                                               info = _("{artistName} 
({artistCount})").format(artistName = artist, artistCount = count)
+                                       artistList.append("<li>%s</li>"%info)
+                               statusInfo.append("".join([header, "<ol>", 
"\n".join(artistList), "</ol>"]))
                if "PlaylistCategoryCount" in snapshot:
                        categoryCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCountLimit"]
                        categories = 
snapshot["PlaylistCategoryCount"].most_common(None if not categoryCount else 
categoryCount)
                        if scriptCount == 0:
-                               statusInfo.append("Top category: %s 
(%s)"%(categories[0][:]))
+                               statusInfo.append(_("Top category: %s 
(%s)")%(categories[0][:]))
                        elif scriptCount == 1:
                                categoryList = []
+                               header = _("Categories:")
                                for item in categories:
                                        category, count = item
                                        category = category.replace("<", "")
                                        category = category.replace(">", "")
-                                       categoryList.append("<li>%s 
(%s)</li>"%(category, count))
-                               statusInfo.append("".join(["Categories:<ol>", 
"\n".join(categoryList), "</ol>"]))
+                                       info = _("{categoryName} 
({categoryCount})").format(categoryName = category, categoryCount = count)
+                                       categoryList.append("<li>%s</li>"%info)
+                               statusInfo.append("".join([header, "<ol>", 
"\n".join(categoryList), "</ol>"]))
                if "PlaylistGenreCount" in snapshot:
                        genreCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["GenreCountLimit"]
                        genres = 
snapshot["PlaylistGenreCount"].most_common(None if not genreCount else 
genreCount)
                        if scriptCount == 0:
-                               statusInfo.append("Top genre: %s 
(%s)"%(genres[0][:]))
+                               statusInfo.append(_("Top genre: %s 
(%s)")%(genres[0][:]))
                        elif scriptCount == 1:
                                genreList = []
+                               header = _("Top genres:")
                                for item in genres:
                                        genre, count = item
                                        if genre is None:
-                                               genreList.append("<li>No genre 
information (%s)</li>"%(count))
+                                               info = _("No genre information 
({genreCount})").format(genreCount = count)
                                        else:
-                                               genreList.append("<li>%s 
(%s)</li>"%(genre, count))
-                               statusInfo.append("".join(["Top genres:<ol>", 
"\n".join(genreList), "</ol>"]))
+                                               info = _("{genreName} 
({genreCount})").format(genreName = genre, genreCount = count)
+                                       genreList.append("<li>%s</li>"%info)
+                               statusInfo.append("".join([header, "<ol>", 
"\n".join(genreList), "</ol>"]))
                if scriptCount == 0:
                        ui.message(", ".join(statusInfo))
                else:
-                       
ui.browseableMessage("<p>".join(statusInfo),title="Playlist snapshots", 
isHtml=True)
+                       
ui.browseableMessage("<p>".join(statusInfo),title=_("Playlist snapshots"), 
isHtml=True)
 
        # Some handlers for native commands.
 


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/e25a3f2e72d1/
Changeset:   e25a3f2e72d1
Branch:      None
User:        josephsl
Date:        2017-03-02 16:00:25+00:00
Summary:     Track item (17.04): announce item position when NVDA+Delete 
(Numpad delete) is pressed.

Instead of a generic object rectangle, announce current position within a 
playlist when location command is invoked, similar to PowerPoint and Excel 
support in NVDA Core and other places.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 1a64e89..118a876 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -184,6 +184,11 @@ class SPLTrackItem(IAccessible):
                # 7.0: Let the app module keep a reference to this track.
                self.appModule._focusedTrack = self
 
+       # A friendly way to report track position via location text.
+       def _get_locationText(self):
+               # Translaotrs: location text for a playlist item (example: item 
1 of 10).
+               return _("Item {current} of {total}").format(current = 
self.IAccessibleChildID, total = studioAPI(0, 124, ret=True))
+
        # Track Dial: using arrow keys to move through columns.
        # This is similar to enhanced arrow keys in other screen readers.
 


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/1f8299ab798c/
Changeset:   1f8299ab798c
Branch:      None
User:        josephsl
Date:        2017-03-02 16:02:11+00:00
Summary:     Track Dial (17.04): second to last step - pass gestures along.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 118a876..5cdccac 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -193,7 +193,7 @@ class SPLTrackItem(IAccessible):
        # This is similar to enhanced arrow keys in other screen readers.
 
        def script_toggleTrackDial(self, gesture):
-               ui.message("Track Dial is deprecated in 2017, please unassign 
Track Dial toggle command via Input Gestures dialog")
+               gesture.send()
        # Translators: Input help mode message for SPL track item.
        script_toggleTrackDial.__doc__=_("Toggles track dial on and off.")
        script_toggleTrackDial.category = _("StationPlaylist Studio")


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/636b879d3f81/
Changeset:   636b879d3f81
Branch:      None
User:        josephsl
Date:        2017-03-02 20:46:43+00:00
Summary:     Merged stable

Affected #:  2 files

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 6ef7fca..0e41fef 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -772,7 +772,7 @@ def switchProfile(prevProfile, newProfile):
        SPLPrevProfile = prevProfile
        # 8.0: Cache other profiles this time.
        if newProfile != defaultProfileName and newProfile not in _SPLCache:
-               _cacheConfig(getProfileByName(selectedProfile))
+               _cacheConfig(getProfileByName(newProfile))
 
 # Called from within the app module.
 def instantProfileSwitch():

diff --git a/readme.md b/readme.md
index bdad363..320f7ab 100755
--- a/readme.md
+++ b/readme.md
@@ -194,6 +194,11 @@ If you are using Studio on a touchscreen computer running 
Windows 8 or later and
 * Initial support for StationPlaylist Creator.
 * Added a new command in SPL Controller layer to announce Studio status such 
as track playback and microphone status (Q).
 
+## Version 17.03
+
+* NVDA will no longer appear to do anything or play an error tone when 
switching to a time-based broadcast profile.
+* Updated translations.
+
 ## Version 17.01/15.5-LTS
 
 Note: 17.01.1/15.5A-LTS replaces 17.01 due to changes to location of new 
add-on files.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/3cce1ccb0c7c/
Changeset:   3cce1ccb0c7c
Branch:      None
User:        josephsl
Date:        2017-03-13 23:27:03+00:00
Summary:     Track Dial and location text (17.04): delay location text 
announcement to 17.05, Track Dial is now part of history.

As 17.04 release is close at hand, it would be advisable to freeze features, 
thus location text announcement will be delayed to 17.05.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 5cdccac..9c671c3 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -116,7 +116,7 @@ _SPLCategoryTones = {
 
 # Routines for track items themselves (prepare for future work).
 class SPLTrackItem(IAccessible):
-       """A base class for providing utility scripts when track entries are 
focused, such as track dial."""
+       """A base class for providing utility scripts when track entries are 
focused, such as location text and column navigation."""
 
        # Keep a record of which column is being looked at.
        _curColumnNumber = None
@@ -185,18 +185,9 @@ class SPLTrackItem(IAccessible):
                self.appModule._focusedTrack = self
 
        # A friendly way to report track position via location text.
-       def _get_locationText(self):
-               # Translaotrs: location text for a playlist item (example: item 
1 of 10).
-               return _("Item {current} of {total}").format(current = 
self.IAccessibleChildID, total = studioAPI(0, 124, ret=True))
-
-       # 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):
-               gesture.send()
-       # Translators: Input help mode message for SPL track item.
-       script_toggleTrackDial.__doc__=_("Toggles track dial on and off.")
-       script_toggleTrackDial.category = _("StationPlaylist Studio")
+       """def _get_locationText(self):
+               # Translators: location text for a playlist item (example: item 
1 of 10).
+               return _("Item {current} of {total}").format(current = 
self.IAccessibleChildID, total = studioAPI(0, 124, ret=True))"""
 
        # Some helper functions to handle corner cases.
        # Each track item provides its own version.
@@ -380,7 +371,7 @@ class SPL510TrackItem(SPLTrackItem):
        def _origIndexOf(self, columnHeader):
                return 
splconfig._SPLDefaults["ColumnAnnouncement"]["ColumnOrder"].index(columnHeader)+1
 
-       # Handle track dial for SPL 5.10.
+       # Handle column announcement for SPL 5.10.
        def _leftmostcol(self):
                if not self.name:
                        # Translators: Presented when no track status is found 
in Studio 5.10.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/b6b9852e2575/
Changeset:   b6b9852e2575
Branch:      None
User:        josephsl
Date:        2017-03-15 18:33:50+00:00
Summary:     Docstrings: corrected case for end of track and song intro scripts

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 9c671c3..2ede801 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -992,14 +992,14 @@ class AppModule(appModuleHandler.AppModule):
        def script_setEndOfTrackTime(self, gesture):
                self.alarmDialog(1)
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
-       script_setEndOfTrackTime.__doc__=_("sets end of track alarm (default is 
5 seconds).")
+       script_setEndOfTrackTime.__doc__=_("Sets end of track alarm (default is 
5 seconds).")
 
        # Set song ramp (introduction) time between 1 and 9 seconds.
 
        def script_setSongRampTime(self, gesture):
                self.alarmDialog(2)
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
-       script_setSongRampTime.__doc__=_("sets song intro alarm (default is 5 
seconds).")
+       script_setSongRampTime.__doc__=_("Sets song intro alarm (default is 5 
seconds).")
 
        # Tell NVDA to play a sound when mic was active for a long time, as 
well as contorl the alarm interval.
        # 8.0: This dialog will let users configure mic alarm interval as well.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/abcc41644ecd/
Changeset:   abcc41644ecd
Branch:      None
User:        josephsl
Date:        2017-03-19 20:09:08+00:00
Summary:     Translatable strings, translator comments, readme updates, release 
candidate is up

Affected #:  2 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 2ede801..634a8f5 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -1490,21 +1490,29 @@ class AppModule(appModuleHandler.AppModule):
 # Output formatter for playlist snapshots.
 # Pressed once will speak and/or braille it, pressing twice or more will 
output this info to an HTML file.
        def playlistSnapshotOutput(self, snapshot, scriptCount):
+               # Translators: one of the results for playlist snapshots 
feature for announcing total number of items in a playlist.
                statusInfo = [_("Items: 
{playlistItemCount}").format(playlistItemCount = snapshot["PlaylistItemCount"])]
+               # Translators: one of the results for playlist snapshots 
feature for announcing total number of tracks in a playlist.
                statusInfo.append(_("Tracks: 
{playlistTrackCount}").format(playlistTrackCount = 
snapshot["PlaylistTrackCount"]))
+               # Translators: one of the results for playlist snapshots 
feature for announcing total duration of a playlist.
                statusInfo.append(_("Duration: 
{playlistTotalDuration}").format(playlistTotalDuration = 
snapshot["PlaylistDurationTotal"]))
                if "PlaylistDurationMin" in snapshot:
+                       # Translators: one of the results for playlist 
snapshots feature for announcing shortest track name and duration of a playlist.
                        statusInfo.append(_("Shortest: 
{playlistShortestTrack}").format(playlistShortestTrack = 
snapshot["PlaylistDurationMin"]))
+                       # Translators: one of the results for playlist 
snapshots feature for announcing longest track name and duration of a playlist.
                        statusInfo.append(_("Longest: 
{playlistLongestTrack}").format(playlistLongestTrack = 
snapshot["PlaylistDurationMax"]))
                if "PlaylistDurationAverage" in snapshot:
+                       # Translators: one of the results for playlist 
snapshots feature for announcing average duration for tracks in a playlist.
                        statusInfo.append(_("Average: 
{playlistAverageDuration}").format(playlistAverageDuration = 
snapshot["PlaylistDurationAverage"]))
                if "PlaylistArtistCount" in snapshot:
                        artistCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["ArtistCountLimit"]
                        artists = 
snapshot["PlaylistArtistCount"].most_common(None if not artistCount else 
artistCount)
                        if scriptCount == 0:
+                               # Translators: one of the results for playlist 
snapshots feature for announcing top artist in a playlist.
                                statusInfo.append(_("Top artist: %s 
(%s)")%(artists[0][:]))
                        elif scriptCount == 1:
                                artistList = []
+                               # Translators: one of the results for playlist 
snapshots feature, a heading for a group of items.
                                header = _("Top artists:")
                                for item in artists:
                                        artist, count = item
@@ -1518,9 +1526,11 @@ class AppModule(appModuleHandler.AppModule):
                        categoryCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCountLimit"]
                        categories = 
snapshot["PlaylistCategoryCount"].most_common(None if not categoryCount else 
categoryCount)
                        if scriptCount == 0:
+                               # Translators: one of the results for playlist 
snapshots feature for announcing top track category in a playlist.
                                statusInfo.append(_("Top category: %s 
(%s)")%(categories[0][:]))
                        elif scriptCount == 1:
                                categoryList = []
+                               # Translators: one of the results for playlist 
snapshots feature, a heading for a group of items.
                                header = _("Categories:")
                                for item in categories:
                                        category, count = item
@@ -1533,9 +1543,11 @@ class AppModule(appModuleHandler.AppModule):
                        genreCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["GenreCountLimit"]
                        genres = 
snapshot["PlaylistGenreCount"].most_common(None if not genreCount else 
genreCount)
                        if scriptCount == 0:
+                               # Translators: one of the results for playlist 
snapshots feature for announcing top genre in a playlist.
                                statusInfo.append(_("Top genre: %s 
(%s)")%(genres[0][:]))
                        elif scriptCount == 1:
                                genreList = []
+                               # Translators: one of the results for playlist 
snapshots feature, a heading for a group of items.
                                header = _("Top genres:")
                                for item in genres:
                                        genre, count = item
@@ -1548,6 +1560,7 @@ class AppModule(appModuleHandler.AppModule):
                if scriptCount == 0:
                        ui.message(", ".join(statusInfo))
                else:
+                       # Translators: The title of a window for displaying 
playlist snapshots information.
                        
ui.browseableMessage("<p>".join(statusInfo),title=_("Playlist snapshots"), 
isHtml=True)
 
        # Some handlers for native commands.

diff --git a/readme.md b/readme.md
index 320f7ab..be808b2 100755
--- a/readme.md
+++ b/readme.md
@@ -174,7 +174,7 @@ From studio window, you can press Alt+NVDA+0 to open the 
add-on configuration di
 
 If you are using Studio on a touchscreen computer running Windows 8 or later 
and have NVDA 2012.3 or later installed, you can perform some Studio commands 
from the touchscreen. First use three finger tap to switch to SPL mode, then 
use the touch commands listed above to perform commands.
 
-## Version 17.04-dev
+## Version 17.04
 
 * Added a basic add-on debugging support by logging various information while 
the add-on is active with NVDA set to debug logging (requires NVDA 2017.1 and 
later). To use this, after installing NVDA 2017.1, from Exit NVDA dialog, 
choose "restart with debug logging enabled" option.
 * Improvements to presentation of various add-on dialogs thanks to NVDA 2016.4 
features.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/bf43ec356e3a/
Changeset:   bf43ec356e3a
Branch:      None
User:        josephsl
Date:        2017-03-20 06:28:55+00:00
Summary:     Merge branch 'master' into stable

Affected #:  15 files

diff --git a/addon/appModules/splcreator.py b/addon/appModules/splcreator.py
new file mode 100755
index 0000000..31b99da
--- /dev/null
+++ b/addon/appModules/splcreator.py
@@ -0,0 +1,15 @@
+# StationPlaylist Creator
+# An app module and global plugin package for NVDA
+# Copyright 2016-2017 Joseph Lee and others, released under GPL.
+
+# Basic support for StationPlaylist Creator.
+
+import appModuleHandler
+
+
+class AppModule(appModuleHandler.AppModule):
+
+       def chooseNVDAObjectOverlayClasses(self, obj, clsList):
+               if obj.windowClassName in ("TDemoRegForm", "TAboutForm"):
+                       from NVDAObjects.behaviors import Dialog
+                       clsList.insert(0, Dialog)

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index d15c3d0..634a8f5 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -8,19 +8,15 @@
 # Additional work done by Joseph Lee and other contributors.
 # For SPL Studio Controller, focus movement, SAM Encoder support and other 
utilities, see the global plugin version of this app module.
 
-# Minimum version: SPL 5.00, NvDA 2015.3.
+# Minimum version: SPL 5.10, NvDA 2016.4.
 
-from functools import wraps
 import os
 import time
 import threading
 import controlTypes
 import appModuleHandler
 import api
-import review
-import eventHandler
 import scriptHandler
-import queueHandler
 import ui
 import nvwave
 import speech
@@ -40,23 +36,10 @@ import splmisc
 import splupdate
 import addonHandler
 addonHandler.initTranslation()
-
-
-# The finally function for status announcement scripts in this module (source: 
Tyler Spivey's code).
-def finally_(func, final):
-       """Calls final after func, even if it fails."""
-       def wrap(f):
-               @wraps(f)
-               def new(*args, **kwargs):
-                       try:
-                               func(*args, **kwargs)
-                       finally:
-                               final()
-               return new
-       return wrap(final)
+from spldebugging import debugOutput
 
 # Make sure the broadcaster is running a compatible version.
-SPLMinVersion = "5.00"
+SPLMinVersion = "5.10"
 
 # Cache the handle to main Studio window.
 _SPLWin = None
@@ -93,21 +76,36 @@ def micAlarmManager(micAlarmWav, micAlarmMessage):
                micAlarmT2 = wx.PyTimer(_micAlarmAnnouncer)
                
micAlarmT2.Start(splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarmInterval"] * 
1000)
 
-# Call SPL API to obtain needed values.
+# Use SPL Studio API to obtain needed values.
 # A thin wrapper around user32.SendMessage and calling a callback if defined.
 # Offset is used in some time commands.
-def statusAPI(arg, command, func=None, ret=False, offset=None):
-       if _SPLWin is None: return
+# If debugging framework is on, print arg, command and other values.
+def studioAPI(arg, command, func=None, ret=False, offset=None):
+       if _SPLWin is None:
+               debugOutput("SPL: Studio handle not found")
+               return
+       debugOutput("SPL: Studio API wParem is %s, lParem is %s"%(arg, command))
        val = sendMessage(_SPLWin, 1024, arg, command)
+       debugOutput("SPL: Studio API result is %s"%val)
        if ret:
                return val
        if func:
                func(val) if not offset else func(val, offset)
 
+# Check if Studio itself is running.
+# This is to make sure custom commands for SPL Assistant comamnds and other 
app module gestures display appropriate error messages.
+def studioIsRunning():
+       if _SPLWin is None:
+               debugOutput("SPL: Studio handle not found")
+               # Translators: A message informing users that Studio is not 
running so certain commands will not work.
+               ui.message(_("Studio main window not found"))
+               return False
+       return True
+
 # Select a track upon request.
 def selectTrack(trackIndex):
-       statusAPI(-1, 121)
-       statusAPI(trackIndex, 121)
+       studioAPI(-1, 121)
+       studioAPI(trackIndex, 121)
 
 # Category sounds dictionary (key = category, value = tone pitch).
 _SPLCategoryTones = {
@@ -118,13 +116,12 @@ _SPLCategoryTones = {
 
 # 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."""
+       """A base class for providing utility scripts when track entries are 
focused, such as location text and column navigation."""
+
+       # Keep a record of which column is being looked at.
+       _curColumnNumber = None
 
        def initOverlayClass(self):
-               if splconfig.SPLConfig["General"]["TrackDial"]:
-                       self.bindGesture("kb:rightArrow", "nextColumn")
-                       self.bindGesture("kb:leftArrow", "prevColumn")
                # LTS: Take a greater role in assigning enhanced Columns 
Explorer command at the expense of limiting where this can be invoked.
                # 8.0: Just assign number row.
                for i in xrange(10):
@@ -134,19 +131,20 @@ class SPLTrackItem(IAccessible):
        # This is response to a situation where columns were rearranged yet 
testing shows in-memory arrangement remains the same.
        # Subclasses must provide this function.
        def _origIndexOf(self, columnHeader):
-               return 
splconfig._SPLDefaults7["General"]["ExploreColumns"].index(columnHeader)
+               return None
 
        # Read selected columns.
        # But first, find where the requested column lives.
        # 8.0: Make this a public function.
        def indexOf(self, columnHeader):
-               # Handle both 5.0x and 5.10 column headers.
                try:
                        return self._origIndexOf(columnHeader)
                except ValueError:
                        return None
 
        def reportFocus(self):
+               # initialize column navigation tracker.
+               if self.__class__._curColumnNumber is None: 
self.__class__._curColumnNumber = 0
                # 7.0: Cache column header data structures if meeting track 
items for the first time.
                # It is better to do it while reporting focus, otherwise Python 
throws recursion limit exceeded error when initOverlayClass does this.
                if self.appModule._columnHeaders is None:
@@ -162,71 +160,39 @@ class SPLTrackItem(IAccessible):
                if splconfig.SPLConfig["General"]["TrackCommentAnnounce"] != 
"off":
                        self.announceTrackComment(0)
                # 6.3: Catch an unusual case where screen order is off yet 
column order is same as screen order and NvDA is told to announce all columns.
+               # 17.04: Even if vertical column commands are performed, build 
description pieces for consistency.
                if splconfig._shouldBuildDescriptionPieces():
                        descriptionPieces = []
+                       columnsToInclude = 
splconfig.SPLConfig["ColumnAnnouncement"]["IncludedColumns"]
                        for header in 
splconfig.SPLConfig["ColumnAnnouncement"]["ColumnOrder"]:
-                               # Artist field should not be included in Studio 
5.0x, as the checkbox serves this role.
-                               if header == "Artist" and 
self.appModule.productVersion.startswith("5.0"):
-                                       continue
-                               if header in 
splconfig.SPLConfig["ColumnAnnouncement"]["IncludedColumns"]:
+                               if header in columnsToInclude:
                                        index = self.indexOf(header)
                                        if index is None: continue # Header not 
found, mostly encountered in Studio 5.0x.
                                        content = self._getColumnContent(index)
                                        if content:
                                                descriptionPieces.append("%s: 
%s"%(header, content))
                        self.description = ", ".join(descriptionPieces)
-               super(IAccessible, self).reportFocus()
+               if self.appModule._announceColumnOnly is None:
+                       super(IAccessible, self).reportFocus()
+               else:
+                       self.appModule._announceColumnOnly = None
+                       verticalColumnAnnounce = 
splconfig.SPLConfig["General"]["VerticalColumnAnnounce"]
+                       if verticalColumnAnnounce == "Status" or 
(verticalColumnAnnounce is None and self._curColumnNumber == 0):
+                               self._leftmostcol()
+                       else:
+                               
self.announceColumnContent(self._curColumnNumber if verticalColumnAnnounce is 
None else self.indexOf(verticalColumnAnnounce), header=verticalColumnAnnounce, 
reportStatus=self.name is not None)
                # 7.0: Let the app module keep a reference to this track.
                self.appModule._focusedTrack = self
 
-       # 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 splconfig.SPLConfig["General"]["TrackDial"]:
-                       splconfig.SPLConfig["General"]["TrackDial"] = True
-                       self.bindGesture("kb:rightArrow", "nextColumn")
-                       self.bindGesture("kb:leftArrow", "prevColumn")
-                       # Translators: Reported when track dial is on.
-                       dialText = _("Track Dial on")
-                       if self.appModule.SPLColNumber > 0:
-                               # Translators: Announced when located on a 
column other than the leftmost column while using track dial.
-                               dialText+= _(", located at column 
{columnHeader}").format(columnHeader = self.appModule.SPLColNumber+1)
-                       dialTone = 780
-               else:
-                       splconfig.SPLConfig["General"]["TrackDial"] = False
-                       try:
-                               self.removeGestureBinding("kb:rightArrow")
-                               self.removeGestureBinding("kb:leftArrow")
-                       except KeyError:
-                               pass
-                       # Translators: Reported when track dial is off.
-                       dialText = _("Track Dial off")
-                       dialTone = 390
-               if not splconfig.SPLConfig["General"]["BeepAnnounce"]:
-                       ui.message(dialText)
-               else:
-                       tones.beep(dialTone, 100)
-                       braille.handler.message(dialText)
-                       if splconfig.SPLConfig["General"]["TrackDial"] and 
self.appModule.SPLColNumber > 0:
-                               # Translators: Spoken when enabling track dial 
while status message is set to beeps.
-                               speech.speakMessage(_("Column 
{columnNumber}").format(columnNumber = self.appModule.SPLColNumber+1))
-       # Translators: Input help mode message for SPL track item.
-       script_toggleTrackDial.__doc__=_("Toggles track dial on and off.")
-       script_toggleTrackDial.category = _("StationPlaylist Studio")
+       # A friendly way to report track position via location text.
+       """def _get_locationText(self):
+               # Translators: location text for a playlist item (example: item 
1 of 10).
+               return _("Item {current} of {total}").format(current = 
self.IAccessibleChildID, total = studioAPI(0, 124, ret=True))"""
 
        # Some helper functions to handle corner cases.
        # Each track item provides its own version.
        def _leftmostcol(self):
-               if self.appModule._columnHeaders is None:
-                       self.appModule._columnHeaders = self.parent.children[-1]
-               leftmost = self.appModule._columnHeaders.firstChild.name
-               if not self.name or self.name == "":
-                       # Translators: Announced when leftmost column has no 
text while track dial is active.
-                       ui.message(_("{leftmostColumn} not 
found").format(leftmostColumn = leftmost))
-               else:
-                       # Translators: Standard message for announcing column 
content.
-                       ui.message(_("{leftmostColumn}: 
{leftmostContent}").format(leftmostColumn = leftmost, leftmostContent = 
self.name))
+               pass
 
        # Locate column content.
        # This is merely the proxy of the module level function defined in the 
misc module.
@@ -235,44 +201,46 @@ class SPLTrackItem(IAccessible):
 
        # Announce column content if any.
        # 7.0: Add an optional header in order to announce correct header 
information in columns explorer.
-       def announceColumnContent(self, colNumber, header=None):
+       # 17.04: Allow checked status in 5.1x and later to be announced if this 
is such a case (vertical column navigation).)
+       def announceColumnContent(self, colNumber, header=None, 
reportStatus=False):
                columnHeader = header if header is not None else 
self.appModule._columnHeaderNames[colNumber]
                columnContent = 
self._getColumnContent(self.indexOf(columnHeader))
+               status = self.name + " " if reportStatus else ""
                if columnContent:
                        # Translators: Standard message for announcing column 
content.
-                       ui.message(unicode(_("{header}: 
{content}")).format(header = columnHeader, content = columnContent))
+                       ui.message(unicode(_("{checkStatus}{header}: 
{content}")).format(checkStatus = status, header = columnHeader, content = 
columnContent))
                else:
                        # Translators: Spoken when column content is blank.
-                       speech.speakMessage(_("{header}: blank").format(header 
= columnHeader))
+                       speech.speakMessage(_("{checkStatus}{header}: 
blank").format(checkStatus = status, header = columnHeader))
                        # Translators: Brailled to indicate empty column 
content.
-                       braille.handler.message(_("{header}: ()").format(header 
= columnHeader))
+                       braille.handler.message(_("{checkStatus}{header}: 
()").format(checkStatus = status, header = columnHeader))
 
        # Now the scripts.
 
        def script_nextColumn(self, gesture):
-               if (self.appModule.SPLColNumber+1) == 
self.appModule._columnHeaders.childCount:
+               if (self._curColumnNumber+1) == 
self.appModule._columnHeaders.childCount:
                        tones.beep(2000, 100)
                else:
-                       self.appModule.SPLColNumber +=1
-               self.announceColumnContent(self.appModule.SPLColNumber)
+                       self.__class__._curColumnNumber +=1
+               self.announceColumnContent(self._curColumnNumber)
 
        def script_prevColumn(self, gesture):
-               if self.appModule.SPLColNumber <= 0:
+               if self._curColumnNumber <= 0:
                        tones.beep(2000, 100)
                else:
-                       self.appModule.SPLColNumber -=1
-               if self.appModule.SPLColNumber == 0:
+                       self.__class__._curColumnNumber -=1
+               if self._curColumnNumber == 0:
                        self._leftmostcol()
                else:
-                       self.announceColumnContent(self.appModule.SPLColNumber)
+                       self.announceColumnContent(self._curColumnNumber)
 
        def script_firstColumn(self, gesture):
-               self.appModule.SPLColNumber = 0
+               self.__class__._curColumnNumber = 0
                self._leftmostcol()
 
        def script_lastColumn(self, gesture):
-               self.appModule.SPLColNumber = 
self.appModule._columnHeaders.childCount-1
-               self.announceColumnContent(self.appModule.SPLColNumber)
+               self.__class__._curColumnNumber = 
self.appModule._columnHeaders.childCount-1
+               self.announceColumnContent(self._curColumnNumber)
 
        # Track movement scripts.
        # Detects top/bottom of a playlist if told to do so.
@@ -287,7 +255,29 @@ class SPLTrackItem(IAccessible):
                if self.IAccessibleChildID == 1 and 
splconfig.SPLConfig["General"]["TopBottomAnnounce"]:
                        tones.beep(2000, 100)
 
-       # Overlay class version of Columns Explorer.
+       # Vertical column navigation.
+
+       def script_nextRowColumn(self, gesture):
+               newTrack = self.next
+               if newTrack is None and 
splconfig.SPLConfig["General"]["TopBottomAnnounce"]:
+                       tones.beep(2000, 100)
+               else:
+                       self.appModule._announceColumnOnly = True
+                       newTrack._curColumnNumber = self._curColumnNumber
+                       newTrack.setFocus(), newTrack.setFocus()
+                       selectTrack(newTrack.IAccessibleChildID-1)
+
+       def script_prevRowColumn(self, gesture):
+               newTrack = self.previous
+               if newTrack is None and 
splconfig.SPLConfig["General"]["TopBottomAnnounce"]:
+                       tones.beep(2000, 100)
+               else:
+                       self.appModule._announceColumnOnly = True
+                       newTrack._curColumnNumber = self._curColumnNumber
+                       newTrack.setFocus(), newTrack.setFocus()
+                       selectTrack(newTrack.IAccessibleChildID-1)
+
+                       # Overlay class version of Columns Explorer.
 
        def script_columnExplorer(self, gesture):
                # LTS: Just in case Control+NVDA+number row command is 
pressed...
@@ -355,9 +345,10 @@ class SPLTrackItem(IAccessible):
        __gestures={
                "kb:control+alt+rightArrow":"nextColumn",
                "kb:control+alt+leftArrow":"prevColumn",
+               "kb:control+alt+downArrow":"nextRowColumn",
+               "kb:control+alt+upArrow":"prevRowColumn",
                "kb:control+alt+home":"firstColumn",
                "kb:control+alt+end":"lastColumn",
-               #"kb:control+`":"toggleTrackDial",
                "kb:downArrow":"nextTrack",
                "kb:upArrow":"prevTrack",
                "kb:Alt+NVDA+C":"announceTrackComment"
@@ -378,9 +369,9 @@ class SPL510TrackItem(SPLTrackItem):
 
        # Studio 5.10 version of original index finder.
        def _origIndexOf(self, columnHeader):
-               return 
splconfig._SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"].index(columnHeader)+1
+               return 
splconfig._SPLDefaults["ColumnAnnouncement"]["ColumnOrder"].index(columnHeader)+1
 
-       # Handle track dial for SPL 5.10.
+       # Handle column announcement for SPL 5.10.
        def _leftmostcol(self):
                if not self.name:
                        # Translators: Presented when no track status is found 
in Studio 5.10.
@@ -417,7 +408,8 @@ T: Cart edit/insert mode.
 U: Studio up time.
 W: Weather and temperature.
 Y: Playlist modification.
-1 through 0 (6 for Studio 5.01 and earlier): Announce columns via Columns 
Explorer (0 is tenth column slot).
+1 through 0: Announce columns via Columns Explorer (0 is tenth column slot).
+F8: Take playlist snapshots such as track count, longest track and so on.
 F9: Mark current track as start of track time analysis.
 F10: Perform track time analysis.
 F12: Switch to an instant switch profile.
@@ -449,7 +441,8 @@ T: Cart edit/insert mode.
 U: Studio up time.
 W: Weather and temperature.
 Y: Playlist modification.
-1 through 0 (6 for Studio 5.01 and earlier): Announce columns via Columns 
Explorer (0 is tenth column slot).
+1 through 0: Announce columns via Columns Explorer (0 is tenth column slot).
+F8: Take playlist snapshots such as track count, longest track and so on.
 F9: Mark current track as start of track time analysis.
 F10: Perform track time analysis.
 F12: Switch to an instant switch profile.
@@ -483,7 +476,8 @@ T: Cart edit/insert mode.
 U: Studio up time.
 W: Weather and temperature.
 Y: Playlist modification.
-1 through 0 (6 for Studio 5.01 and earlier): Announce columns via Columns 
Explorer (0 is tenth column slot).
+1 through 0: Announce columns via Columns Explorer (0 is tenth column slot).
+F8: Take playlist snapshots such as track count, longest track and so on.
 F9: Mark current track as start of track time analysis.
 F10: Perform track time analysis.
 F12: Switch to an instant switch profile.
@@ -568,6 +562,7 @@ class AppModule(appModuleHandler.AppModule):
        _columnHeaders = None
        _columnHeaderNames = None
        _focusedTrack = None
+       _announceColumnOnly = None # Used only if vertical column navigation 
commands are used.
 
        # Prepare the settings dialog among other things.
        def __init__(self, *args, **kwargs):
@@ -579,10 +574,12 @@ class AppModule(appModuleHandler.AppModule):
                        ui.message(_("Using SPL Studio version 
{SPLVersion}").format(SPLVersion = self.SPLCurVersion))
                except IOError, AttributeError:
                        pass
+               debugOutput("SPL: loading add-on settings")
                splconfig.initConfig()
                # Announce status changes while using other programs.
                # This requires NVDA core support and will be available in 6.0 
and later (cannot be ported to earlier versions).
                # For now, handle all background events, but in the end, make 
this configurable.
+               import eventHandler
                if hasattr(eventHandler, "requestEvents"):
                        eventHandler.requestEvents(eventName="nameChange", 
processId=self.processID, windowClassName="TStatusBar")
                        eventHandler.requestEvents(eventName="nameChange", 
processId=self.processID, windowClassName="TStaticText")
@@ -599,10 +596,12 @@ class AppModule(appModuleHandler.AppModule):
                # Check for add-on update if told to do so.
                # LTS: Only do this if channel hasn't changed.
                if splconfig.SPLConfig["Update"]["AutoUpdateCheck"] or 
splupdate._updateNow:
+                       debugOutput("SPL: checking for add-on updates from %s 
channel"%splupdate.SPLUpdateChannel)
                        # 7.0: Have a timer call the update function indirectly.
+                       import queueHandler
                        queueHandler.queueFunction(queueHandler.eventQueue, 
splconfig.updateInit)
                # Display startup dialogs if any.
-               wx.CallAfter(splconfig.showStartupDialogs, 
oldVer=self.SPLCurVersion < "5.10")
+               wx.CallAfter(splconfig.showStartupDialogs)
 
        # Locate the handle for main window for caching purposes.
        def _locateSPLHwnd(self):
@@ -637,6 +636,7 @@ class AppModule(appModuleHandler.AppModule):
                        obj.role = controlTypes.ROLE_GROUPING
                # In certain edit fields and combo boxes, the field name is 
written to the screen, and there's no way to fetch the object for this text. 
Thus use review position text.
                elif obj.windowClassName in ("TEdit", "TComboBox") and not 
obj.name:
+                       import review
                        fieldName, fieldObj  = review.getScreenPosition(obj)
                        fieldName.expand(textInfos.UNIT_LINE)
                        if obj.windowClassName == "TComboBox":
@@ -650,8 +650,6 @@ class AppModule(appModuleHandler.AppModule):
                windowStyle = obj.windowStyle
                if obj.windowClassName == "TTntListView.UnicodeClass" and role 
== controlTypes.ROLE_LISTITEM and abs(windowStyle - 1443991625)%0x100000 == 0:
                        clsList.insert(0, SPL510TrackItem)
-               elif obj.windowClassName == "TListView" and role == 
controlTypes.ROLE_CHECKBOX and abs(windowStyle - 1442938953)%0x100000 == 0:
-                       clsList.insert(0, SPLTrackItem)
                # 7.2: Recognize known dialogs.
                elif obj.windowClassName in ("TDemoRegForm", "TOpenPlaylist"):
                        clsList.insert(0, Dialog)
@@ -662,10 +660,8 @@ class AppModule(appModuleHandler.AppModule):
        # Keep an eye on library scans in insert tracks window.
        libraryScanning = False
        scanCount = 0
-       # For 5.0X and earlier: prevent NVDA from announcing scheduled time 
multiple times.
+       # Prevent NVDA from announcing scheduled time multiple times.
        scheduledTimeCache = ""
-       # Track Dial (A.K.A. enhanced arrow keys)
-       SPLColNumber = 0
 
        # Automatically announce mic, line in, etc changes
        # These items are static text items whose name changes.
@@ -705,8 +701,7 @@ class AppModule(appModuleHandler.AppModule):
                                                if self.scanCount%100 == 0:
                                                        
self._libraryScanAnnouncer(obj.name[1:obj.name.find("]")], 
splconfig.SPLConfig["General"]["LibraryScanAnnounce"])
                                        if not self.libraryScanning:
-                                               if self.productVersion not in 
noLibScanMonitor:
-                                                       if not 
self.backgroundStatusMonitor: self.libraryScanning = True
+                                               if self.productVersion not in 
noLibScanMonitor: self.libraryScanning = True
                                elif "match" in obj.name:
                                        if 
splconfig.SPLConfig["General"]["LibraryScanAnnounce"] != "off" and 
self.libraryScanning:
                                                if 
splconfig.SPLConfig["General"]["BeepAnnounce"]: tones.beep(370, 100)
@@ -721,7 +716,7 @@ class AppModule(appModuleHandler.AppModule):
                                        self._toggleMessage(obj.name)
                                else:
                                        ui.message(obj.name)
-                               if self.cartExplorer or 
int(splconfig.SPLConfig["MicrophoneAlarm"]["MicAlarm"]):
+                               if self.cartExplorer or 
splconfig.SPLConfig["MicrophoneAlarm"]["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.
@@ -790,9 +785,7 @@ class AppModule(appModuleHandler.AppModule):
                        # 17.01: The best way to detect Cart Edit off is 
consulting file modification time.
                        # Automatically reload cart information if this is the 
case.
                        if status in ("Cart Edit Off", "Cart Insert On"):
-                               studioTitle = api.getForegroundObject().name
-                               if 
splmisc.shouldCartExplorerRefresh(studioTitle):
-                                       self.carts = 
splmisc.cartExplorerInit(studioTitle)
+                               self.carts = 
splmisc.cartExplorerRefresh(api.getForegroundObject().name, self.carts)
                        # Translators: Presented when cart modes are toggled 
while cart explorer is on.
                        ui.message(_("Cart explorer is active"))
                        return
@@ -863,17 +856,20 @@ class AppModule(appModuleHandler.AppModule):
                        except KeyError:
                                pass
 
-
        # Save configuration when terminating.
        def terminate(self):
                super(AppModule, self).terminate()
+               debugOutput("SPL: terminating app module")
                # 6.3: Memory leak results if encoder flag sets and other 
encoder support maps aren't cleaned up.
                # This also could have allowed a hacker to modify the flags set 
(highly unlikely) so NvDA could get confused next time Studio loads.
                import sys
-               if "globalPlugins.SPLStudioUtils.encoders" in sys.modules:
-                       import globalPlugins.SPLStudioUtils.encoders
-                       globalPlugins.SPLStudioUtils.encoders.cleanup()
+               if "globalPlugins.splUtils.encoders" in sys.modules:
+                       import globalPlugins.splUtils.encoders
+                       globalPlugins.splUtils.encoders.cleanup()
+               debugOutput("SPL: saving add-on settings")
                splconfig.saveConfig()
+               # reset column number for column navigation commands.
+               if self._focusedTrack: 
self._focusedTrack.__class__._curColumnNumber = None
                # Delete focused track reference.
                self._focusedTrack = None
                try:
@@ -891,7 +887,6 @@ class AppModule(appModuleHandler.AppModule):
                global _SPLWin
                if _SPLWin: _SPLWin = None
 
-
        # Script sections (for ease of maintenance):
        # Time-related: elapsed time, end of track alarm, etc.
        # Misc scripts: track finder and others.
@@ -934,16 +929,17 @@ class AppModule(appModuleHandler.AppModule):
 
        # Scripts which rely on API.
        def script_sayRemainingTime(self, gesture):
-               statusAPI(3, 105, self.announceTime, offset=1)
+               if studioIsRunning(): studioAPI(3, 105, self.announceTime, 
offset=1)
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
        script_sayRemainingTime.__doc__=_("Announces the remaining track time.")
 
        def script_sayElapsedTime(self, gesture):
-               statusAPI(0, 105, self.announceTime)
+               if studioIsRunning(): studioAPI(0, 105, self.announceTime)
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
        script_sayElapsedTime.__doc__=_("Announces the elapsed time for the 
currently playing track.")
 
        def script_sayBroadcasterTime(self, gesture):
+               if not studioIsRunning(): return
                # Says things such as "25 minutes to 2" and "5 past 11".
                # Parse the local time and say it similar to how Studio 
presents broadcaster time.
                h, m = time.localtime()[3], time.localtime()[4]
@@ -965,6 +961,7 @@ class AppModule(appModuleHandler.AppModule):
        script_sayBroadcasterTime.__doc__=_("Announces broadcaster time.")
 
        def script_sayCompleteTime(self, gesture):
+               if not studioIsRunning(): return
                import winKernel
                # Says complete time in hours, minutes and seconds via 
kernel32's routines.
                
ui.message(winKernel.GetTimeFormat(winKernel.LOCALE_USER_DEFAULT, 0, None, 
None))
@@ -981,12 +978,12 @@ class AppModule(appModuleHandler.AppModule):
                        wx.CallAfter(gui.messageBox, _("The add-on settings 
dialog is opened. Please close the settings dialog first."), _("Error"), 
wx.OK|wx.ICON_ERROR)
                        return
                try:
-                       d = splconfig.SPLAlarmDialog(gui.mainFrame, level)
+                       d = splconfui.AlarmsCenter(gui.mainFrame, level)
                        gui.mainFrame.prePopup()
                        d.Raise()
                        d.Show()
                        gui.mainFrame.postPopup()
-                       splconfig._alarmDialogOpened = True
+                       splconfui._alarmDialogOpened = True
                except RuntimeError:
                        wx.CallAfter(splconfig._alarmError)
 
@@ -995,14 +992,14 @@ class AppModule(appModuleHandler.AppModule):
        def script_setEndOfTrackTime(self, gesture):
                self.alarmDialog(1)
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
-       script_setEndOfTrackTime.__doc__=_("sets end of track alarm (default is 
5 seconds).")
+       script_setEndOfTrackTime.__doc__=_("Sets end of track alarm (default is 
5 seconds).")
 
        # Set song ramp (introduction) time between 1 and 9 seconds.
 
        def script_setSongRampTime(self, gesture):
                self.alarmDialog(2)
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
-       script_setSongRampTime.__doc__=_("sets song intro alarm (default is 5 
seconds).")
+       script_setSongRampTime.__doc__=_("Sets song intro alarm (default is 5 
seconds).")
 
        # Tell NVDA to play a sound when mic was active for a long time, as 
well as contorl the alarm interval.
        # 8.0: This dialog will let users configure mic alarm interval as well.
@@ -1028,14 +1025,6 @@ class AppModule(appModuleHandler.AppModule):
 
        # Other commands (track finder and others)
 
-       # Toggle whether beeps should be heard instead of toggle announcements.
-       # Deprecated in 8.0, may come back later.
-
-       #def script_toggleBeepAnnounce(self, gesture):
-               #splconfig.SPLConfig["General"]["BeepAnnounce"] = not 
splconfig.SPLConfig["General"]["BeepAnnounce"]
-               #splconfig.message("BeepAnnounce", 
splconfig.SPLConfig["General"]["BeepAnnounce"])
-       #script_toggleBeepAnnounce.__doc__=_("Toggles status announcements 
between words and beeps.")
-
        # Braille timer.
        # Announce end of track and other info via braille.
 
@@ -1072,8 +1061,7 @@ class AppModule(appModuleHandler.AppModule):
                        # 16.10.1/15.2 LTS: Just select this track in order to 
prevent a dispute between NVDA and SPL in regards to focused track.
                        # 16.11: Call setFocus if it is post-5.01, as SPL API 
can be used to select the desired track.
                        selectTrack(track.IAccessibleChildID-1)
-                       if self.productVersion >= "5.10":
-                               track.setFocus(), track.setFocus()
+                       track.setFocus(), track.setFocus()
                else:
                        wx.CallAfter(gui.messageBox,
                        # Translators: Standard dialog message when an item one 
wishes to search is not found (copy this from main nvda.po).
@@ -1101,6 +1089,7 @@ class AppModule(appModuleHandler.AppModule):
        # But first, check if track finder can be invoked.
        # Attempt level specifies which track finder to open (0 = Track Finder, 
1 = Column Search, 2 = Time range).
        def _trackFinderCheck(self, attemptLevel):
+               if not studioIsRunning(): return False
                if api.getForegroundObject().windowClassName != "TStudioForm":
                        if attemptLevel == 0:
                                # Translators: Presented when a user attempts 
to find tracks but is not at the track list.
@@ -1165,7 +1154,7 @@ class AppModule(appModuleHandler.AppModule):
        def script_timeRangeFinder(self, gesture):
                if self._trackFinderCheck(2):
                        try:
-                               d = splmisc.SPLTimeRangeDialog(gui.mainFrame, 
api.getFocusObject(), statusAPI)
+                               d = splmisc.SPLTimeRangeDialog(gui.mainFrame, 
api.getFocusObject(), studioAPI)
                                gui.mainFrame.prePopup()
                                d.Raise()
                                d.Show()
@@ -1212,6 +1201,7 @@ class AppModule(appModuleHandler.AppModule):
                        self.bindGestures(self.__gestures)
 
        def script_toggleCartExplorer(self, gesture):
+               if not studioIsRunning(): return
                if not self.cartExplorer:
                        # Prevent cart explorer from being engaged outside of 
playlist viewer.
                        # Todo for 6.0: Let users set cart banks.
@@ -1234,6 +1224,7 @@ class AppModule(appModuleHandler.AppModule):
                        self.cartExplorer = False
                        self.cartsBuilder(build=False)
                        self.carts.clear()
+                       splmisc._cartEditTimestamps = None
                        # Translators: Presented when cart explorer is off.
                        ui.message(_("Exiting cart explorer"))
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
@@ -1284,41 +1275,40 @@ class AppModule(appModuleHandler.AppModule):
                global libScanT
                if libScanT and libScanT.isAlive() and 
api.getForegroundObject().windowClassName == "TTrackInsertForm":
                        return
-               parem = 0 if self.SPLCurVersion < "5.10" else 1
-               countA = statusAPI(parem, 32, ret=True)
-               if countA == 0:
+               if studioAPI(1, 32, ret=True) < 0:
                        self.libraryScanning = False
                        return
                time.sleep(0.1)
                if api.getForegroundObject().windowClassName == 
"TTrackInsertForm" and self.productVersion in noLibScanMonitor:
                        self.libraryScanning = False
                        return
-               countB = statusAPI(parem, 32, ret=True)
-               if countA == countB:
+               # 17.04: Library scan may have finished while this thread was 
sleeping.
+               if studioAPI(1, 32, ret=True) < 0:
                        self.libraryScanning = False
-                       if self.SPLCurVersion >= "5.10":
-                               countB = statusAPI(0, 32, ret=True)
                        # Translators: Presented when library scanning is 
finished.
-                       ui.message(_("{itemCount} items in the 
library").format(itemCount = countB))
+                       ui.message(_("{itemCount} items in the 
library").format(itemCount = studioAPI(0, 32, ret=True)))
                else:
-                       libScanT = 
threading.Thread(target=self.libraryScanReporter, args=(_SPLWin, countA, 
countB, parem))
+                       libScanT = 
threading.Thread(target=self.libraryScanReporter)
                        libScanT.daemon = True
                        libScanT.start()
 
-       def libraryScanReporter(self, _SPLWin, countA, countB, parem):
+       def libraryScanReporter(self):
                scanIter = 0
-               while countA != countB:
+               # 17.04: Use the constant directly, as 5.10 and later provides 
a convenient method to detect completion of library scans.
+               scanCount = studioAPI(1, 32, ret=True)
+               while scanCount >= 0:
                        if not self.libraryScanning: return
-                       countA = countB
                        time.sleep(1)
                        # Do not continue if we're back on insert tracks form 
or library scan is finished.
                        if api.getForegroundObject().windowClassName == 
"TTrackInsertForm" or not self.libraryScanning:
                                return
-                       countB, scanIter = statusAPI(parem, 32, ret=True), 
scanIter+1
-                       if countB < 0:
+                       # Scan count may have changed during sleep.
+                       scanCount = studioAPI(1, 32, ret=True)
+                       if scanCount < 0:
                                break
+                       scanIter+=1
                        if scanIter%5 == 0 and 
splconfig.SPLConfig["General"]["LibraryScanAnnounce"] not in ("off", "ending"):
-                               self._libraryScanAnnouncer(countB, 
splconfig.SPLConfig["General"]["LibraryScanAnnounce"])
+                               self._libraryScanAnnouncer(scanCount, 
splconfig.SPLConfig["General"]["LibraryScanAnnounce"])
                self.libraryScanning = False
                if self.backgroundStatusMonitor: return
                if splconfig.SPLConfig["General"]["LibraryScanAnnounce"] != 
"off":
@@ -1326,7 +1316,7 @@ class AppModule(appModuleHandler.AppModule):
                                tones.beep(370, 100)
                        else:
                                # Translators: Presented after library scan is 
done.
-                               ui.message(_("Scan complete with {itemCount} 
items").format(itemCount = countB))
+                               ui.message(_("Scan complete with {itemCount} 
items").format(itemCount = studioAPI(0, 32, ret=True)))
 
        # Take care of library scanning announcement.
        def _libraryScanAnnouncer(self, count, announcementType):
@@ -1371,7 +1361,7 @@ class AppModule(appModuleHandler.AppModule):
        def script_manageMetadataStreams(self, gesture):
                # Do not even think about opening this dialog if handle to 
Studio isn't found.
                if _SPLWin is None:
-                       # Translators: Presented when stremaing dialog cannot 
be shown.
+                       # Translators: Presented when streaming dialog cannot 
be shown.
                        ui.message(_("Cannot open metadata streaming dialog"))
                        return
                if splconfui._configDialogOpened or 
splconfui._metadataDialogOpened:
@@ -1380,7 +1370,7 @@ class AppModule(appModuleHandler.AppModule):
                        return
                try:
                        # Passing in the function object is enough to change 
the dialog UI.
-                       d = splconfui.MetadataStreamingDialog(gui.mainFrame, 
func=statusAPI)
+                       d = splconfui.MetadataStreamingDialog(gui.mainFrame, 
func=studioAPI)
                        gui.mainFrame.prePopup()
                        d.Raise()
                        d.Show()
@@ -1391,54 +1381,197 @@ class AppModule(appModuleHandler.AppModule):
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
        script_manageMetadataStreams.__doc__=_("Opens a dialog to quickly 
enable or disable metadata streaming.")
 
-       # Track time analysis
+       # Track time analysis/Playlist snapshots
        # Return total length of the selected tracks upon request.
        # Analysis command (SPL Assistant) will be assignable.
+       # Also gather various data about the playlist.
        _analysisMarker = None
 
-       # Trakc time analysis requires main playlist viewer to be the 
foreground window.
+       # Trakc time analysis and playlist snapshots require main playlist 
viewer to be the foreground window.
        def _trackAnalysisAllowed(self):
+               if not studioIsRunning():
+                       return False
                if api.getForegroundObject().windowClassName != "TStudioForm":
                        # Translators: Presented when track time anlaysis 
cannot be performed because user is not focused on playlist viewer.
-                       ui.message(_("Not in playlist viewer, cannot perform 
track time analysis"))
+                       ui.message(_("Not in playlist viewer, cannot perform 
track time analysis or gather playlist snapshot statistics"))
                        return False
                return True
 
        # Return total duration of a range of tracks.
        # This is used in track time analysis when multiple tracks are selected.
        # This is also called from playlist duration scripts.
-       def totalTime(self, start, end):
+       def playlistDuration(self, start=None, end=None):
+               if start is None: start = api.getFocusObject()
+               duration = start.indexOf("Duration")
+               totalDuration = 0
+               obj = start
+               while obj not in (None, end):
+                       # Technically segue.
+                       segue = obj._getColumnContent(duration)
+                       if segue not in (None, "00:00"):
+                               hms = segue.split(":")
+                               totalDuration += (int(hms[-2])*60) + 
int(hms[-1])
+                               if len(hms) == 3: totalDuration += 
int(hms[0])*3600
+                       obj = obj.next
+               return totalDuration
+
+       # Segue version of this will be used in some places (the below is the 
raw duration).)
+       def playlistDurationRaw(self, start, end):
                # Take care of errors such as the following.
-               if start < 0 or end > statusAPI(0, 124, ret=True)-1:
+               if start < 0 or end > studioAPI(0, 124, ret=True)-1:
                        raise ValueError("Track range start or end position out 
of range")
                        return
                totalLength = 0
                if start == end:
-                       filename = statusAPI(start, 211, ret=True)
-                       totalLength = statusAPI(filename, 30, ret=True)
+                       filename = studioAPI(start, 211, ret=True)
+                       totalLength = studioAPI(filename, 30, ret=True)
                else:
                        for track in xrange(start, end+1):
-                               filename = statusAPI(track, 211, ret=True)
-                               totalLength+=statusAPI(filename, 30, ret=True)
+                               filename = studioAPI(track, 211, ret=True)
+                               totalLength+=studioAPI(filename, 30, ret=True)
                return totalLength
 
+       # Playlist snapshots
+       # Data to be gathered comes from a set of flags.
+       # By default, playlist duration (including shortest and average), 
category summary and other statistics will be gathered.
+       def playlistSnapshots(self, obj, end, snapshotFlags=None):
+               # Track count and total duration are always included.
+               snapshot = {}
+               if snapshotFlags is None:
+                       snapshotFlags = [flag for flag in 
splconfig.SPLConfig["PlaylistSnapshots"] if 
splconfig.SPLConfig["PlaylistSnapshots"][flag]]
+               duration = obj.indexOf("Duration")
+               title = obj.indexOf("Title")
+               artist = obj.indexOf("Artist")
+               artists = []
+               min, max = None, None
+               minTitle, maxTitle = None, None
+               totalDuration = 0
+               category = obj.indexOf("Category")
+               categories = []
+               genre = obj.indexOf("Genre")
+               genres = []
+               # A specific version of the playlist duration loop is needed in 
order to gather statistics.
+               while obj not in (None, end):
+                       segue = obj._getColumnContent(duration)
+                       trackTitle = obj._getColumnContent(title)
+                       categories.append(obj._getColumnContent(category))
+                       # Don't record artist and genre information for an hour 
marker (reported by a broadcaster).
+                       if categories[-1] != "Hour Marker":
+                               artists.append(obj._getColumnContent(artist))
+                               genres.append(obj._getColumnContent(genre))
+                       # Shortest and longest tracks.
+                       # #22: assign min to the first segue in order to not 
forget title of the shortest track.
+                       if segue and (min is None or segue < min):
+                               min = segue
+                               minTitle = trackTitle
+                       if segue and segue > max:
+                               max = segue
+                               maxTitle = trackTitle
+                       if segue not in (None, "00:00"):
+                               hms = segue.split(":")
+                               totalDuration += (int(hms[-2])*60) + 
int(hms[-1])
+                               if len(hms) == 3: totalDuration += 
int(hms[0])*3600
+                       obj = obj.next
+               if end is None: snapshot["PlaylistItemCount"] = studioAPI(0, 
124, ret=True)
+               snapshot["PlaylistTrackCount"] = len(artists)
+               snapshot["PlaylistDurationTotal"] = 
self._ms2time(totalDuration, ms=False)
+               if "DurationMinMax" in snapshotFlags:
+                       snapshot["PlaylistDurationMin"] = "%s (%s)"%(minTitle, 
min)
+                       snapshot["PlaylistDurationMax"] = "%s (%s)"%(maxTitle, 
max)
+               if "DurationAverage" in snapshotFlags:
+                       snapshot["PlaylistDurationAverage"] = 
self._ms2time(totalDuration/snapshot["PlaylistTrackCount"], ms=False)
+               if "CategoryCount" in snapshotFlags or "ArtistCount" in 
snapshotFlags or "GenreCount" in snapshotFlags:
+                       import collections
+                       if "CategoryCount" in snapshotFlags: 
snapshot["PlaylistCategoryCount"] = collections.Counter(categories)
+                       if "ArtistCount" in snapshotFlags: 
snapshot["PlaylistArtistCount"] = collections.Counter(artists)
+                       if "GenreCount" in snapshotFlags: 
snapshot["PlaylistGenreCount"] = collections.Counter(genres)
+               return snapshot
+
+# Output formatter for playlist snapshots.
+# Pressed once will speak and/or braille it, pressing twice or more will 
output this info to an HTML file.
+       def playlistSnapshotOutput(self, snapshot, scriptCount):
+               # Translators: one of the results for playlist snapshots 
feature for announcing total number of items in a playlist.
+               statusInfo = [_("Items: 
{playlistItemCount}").format(playlistItemCount = snapshot["PlaylistItemCount"])]
+               # Translators: one of the results for playlist snapshots 
feature for announcing total number of tracks in a playlist.
+               statusInfo.append(_("Tracks: 
{playlistTrackCount}").format(playlistTrackCount = 
snapshot["PlaylistTrackCount"]))
+               # Translators: one of the results for playlist snapshots 
feature for announcing total duration of a playlist.
+               statusInfo.append(_("Duration: 
{playlistTotalDuration}").format(playlistTotalDuration = 
snapshot["PlaylistDurationTotal"]))
+               if "PlaylistDurationMin" in snapshot:
+                       # Translators: one of the results for playlist 
snapshots feature for announcing shortest track name and duration of a playlist.
+                       statusInfo.append(_("Shortest: 
{playlistShortestTrack}").format(playlistShortestTrack = 
snapshot["PlaylistDurationMin"]))
+                       # Translators: one of the results for playlist 
snapshots feature for announcing longest track name and duration of a playlist.
+                       statusInfo.append(_("Longest: 
{playlistLongestTrack}").format(playlistLongestTrack = 
snapshot["PlaylistDurationMax"]))
+               if "PlaylistDurationAverage" in snapshot:
+                       # Translators: one of the results for playlist 
snapshots feature for announcing average duration for tracks in a playlist.
+                       statusInfo.append(_("Average: 
{playlistAverageDuration}").format(playlistAverageDuration = 
snapshot["PlaylistDurationAverage"]))
+               if "PlaylistArtistCount" in snapshot:
+                       artistCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["ArtistCountLimit"]
+                       artists = 
snapshot["PlaylistArtistCount"].most_common(None if not artistCount else 
artistCount)
+                       if scriptCount == 0:
+                               # Translators: one of the results for playlist 
snapshots feature for announcing top artist in a playlist.
+                               statusInfo.append(_("Top artist: %s 
(%s)")%(artists[0][:]))
+                       elif scriptCount == 1:
+                               artistList = []
+                               # Translators: one of the results for playlist 
snapshots feature, a heading for a group of items.
+                               header = _("Top artists:")
+                               for item in artists:
+                                       artist, count = item
+                                       if artist is None:
+                                               info = _("No artist information 
({artistCount})").format(artistCount = count)
+                                       else:
+                                               info = _("{artistName} 
({artistCount})").format(artistName = artist, artistCount = count)
+                                       artistList.append("<li>%s</li>"%info)
+                               statusInfo.append("".join([header, "<ol>", 
"\n".join(artistList), "</ol>"]))
+               if "PlaylistCategoryCount" in snapshot:
+                       categoryCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["CategoryCountLimit"]
+                       categories = 
snapshot["PlaylistCategoryCount"].most_common(None if not categoryCount else 
categoryCount)
+                       if scriptCount == 0:
+                               # Translators: one of the results for playlist 
snapshots feature for announcing top track category in a playlist.
+                               statusInfo.append(_("Top category: %s 
(%s)")%(categories[0][:]))
+                       elif scriptCount == 1:
+                               categoryList = []
+                               # Translators: one of the results for playlist 
snapshots feature, a heading for a group of items.
+                               header = _("Categories:")
+                               for item in categories:
+                                       category, count = item
+                                       category = category.replace("<", "")
+                                       category = category.replace(">", "")
+                                       info = _("{categoryName} 
({categoryCount})").format(categoryName = category, categoryCount = count)
+                                       categoryList.append("<li>%s</li>"%info)
+                               statusInfo.append("".join([header, "<ol>", 
"\n".join(categoryList), "</ol>"]))
+               if "PlaylistGenreCount" in snapshot:
+                       genreCount = 
splconfig.SPLConfig["PlaylistSnapshots"]["GenreCountLimit"]
+                       genres = 
snapshot["PlaylistGenreCount"].most_common(None if not genreCount else 
genreCount)
+                       if scriptCount == 0:
+                               # Translators: one of the results for playlist 
snapshots feature for announcing top genre in a playlist.
+                               statusInfo.append(_("Top genre: %s 
(%s)")%(genres[0][:]))
+                       elif scriptCount == 1:
+                               genreList = []
+                               # Translators: one of the results for playlist 
snapshots feature, a heading for a group of items.
+                               header = _("Top genres:")
+                               for item in genres:
+                                       genre, count = item
+                                       if genre is None:
+                                               info = _("No genre information 
({genreCount})").format(genreCount = count)
+                                       else:
+                                               info = _("{genreName} 
({genreCount})").format(genreName = genre, genreCount = count)
+                                       genreList.append("<li>%s</li>"%info)
+                               statusInfo.append("".join([header, "<ol>", 
"\n".join(genreList), "</ol>"]))
+               if scriptCount == 0:
+                       ui.message(", ".join(statusInfo))
+               else:
+                       # Translators: The title of a window for displaying 
playlist snapshots information.
+                       
ui.browseableMessage("<p>".join(statusInfo),title=_("Playlist snapshots"), 
isHtml=True)
+
        # Some handlers for native commands.
 
-       # In Studio 5.0x, when deleting a track, NVDA announces wrong track 
item due to focus bouncing.
+       # In Studio 5.0x, when deleting a track, NVDA announces wrong track 
item due to focus bouncing (not the case in 5.10 and later).
        # The below hack is sensitive to changes in NVDA core.
        deletedFocusObj = False
 
        def script_deleteTrack(self, gesture):
                self.preTrackRemoval()
                gesture.send()
-               if self.productVersion.startswith("5.0"):
-                       if api.getForegroundObject().windowClassName == 
"TStudioForm":
-                               focus = api.getFocusObject()
-                               if focus.IAccessibleChildID < 
focus.parent.childCount:
-                                       self.deletedFocusObj = True
-                                       focus.setFocus()
-                                       self.deletedFocusObj = False
-                                       focus.setFocus()
 
        # When Escape is pressed, activate background library scan if 
conditions are right.
        def script_escape(self, gesture):
@@ -1452,7 +1585,6 @@ class AppModule(appModuleHandler.AppModule):
                os.startfile("mailto:joseph.lee22590@xxxxxxxxx";)
        script_sendFeedbackEmail.__doc__="Opens the default email client to 
send an email to the add-on developer"
 
-
        # SPL Assistant: reports status on playback, operation, etc.
        # Used layer command approach to save gesture assignments.
        # Most were borrowed from JFW and Window-Eyes layer scripts.
@@ -1463,8 +1595,10 @@ class AppModule(appModuleHandler.AppModule):
                        return appModuleHandler.AppModule.getScript(self, 
gesture)
                script = appModuleHandler.AppModule.getScript(self, gesture)
                if not script:
-                       script = finally_(self.script_error, self.finish)
-               return finally_(script, self.finish)
+                       script = self.script_error
+               # Just use finally function from the global plugin to reduce 
code duplication.
+               import globalPlugins.splUtils
+               return globalPlugins.splUtils.finally_(script, self.finish)
 
        def finish(self):
                self.SPLAssistant = False
@@ -1536,13 +1670,13 @@ class AppModule(appModuleHandler.AppModule):
        # These are scattered throughout the screen, so one can use 
foreground.getChild(index) to fetch them (getChild tip from Jamie Teh (NV 
Access)).
        # Because 5.x (an perhaps future releases) uses different screen 
layout, look up the needed constant from the table below (row = info needed, 
column = version).
        statusObjs={
-               SPLPlayStatus:[5, 6], # Play status, mic, etc.
-               SPLSystemStatus:[-3, -2], # The second status bar containing 
system status such as up time.
-               SPLScheduledToPlay:[18, 19], # In case the user selects one or 
more tracks in a given hour.
-               SPLScheduled:[19, 20], # Time when the selected track will 
begin.
-               SPLNextTrackTitle:[7, 8], # Name and duration of the next track 
if any.
-               SPLCurrentTrackTitle:[8, 9], # Name of the currently playing 
track.
-               SPLTemperature:[6, 7], # Temperature for the current city.
+               SPLPlayStatus: 6, # Play status, mic, etc.
+               SPLSystemStatus: -2, # The second status bar containing system 
status such as up time.
+               SPLScheduledToPlay: 19, # In case the user selects one or more 
tracks in a given hour.
+               SPLScheduled: 20, # Time when the selected track will begin.
+               SPLNextTrackTitle: 8, # Name and duration of the next track if 
any.
+               SPLCurrentTrackTitle: 9, # Name of the currently playing track.
+               SPLTemperature: 7, # Temperature for the current city.
        }
 
        _cachedStatusObjs = {}
@@ -1556,8 +1690,7 @@ class AppModule(appModuleHandler.AppModule):
                        if fg is not None and fg.windowClassName != 
"TStudioForm":
                                # 6.1: Allow gesture-based functions to look up 
status information even if Studio window isn't focused.
                                fg = 
getNVDAObjectFromEvent(user32.FindWindowA("TStudioForm", None), OBJID_CLIENT, 0)
-                       if not self.productVersion >= "5.10": statusObj = 
self.statusObjs[infoIndex][0]
-                       else: statusObj = self.statusObjs[infoIndex][1]
+                       statusObj = self.statusObjs[infoIndex]
                        # 7.0: sometimes (especially when first loaded), 
OBJID_CLIENT fails, so resort to retrieving focused object instead.
                        if fg is not None and fg.childCount > 1:
                                self._cachedStatusObjs[infoIndex] = 
fg.getChild(statusObj)
@@ -1578,7 +1711,7 @@ class AppModule(appModuleHandler.AppModule):
                if self.SPLCurVersion < "5.20":
                        status = 
self.status(self.SPLPlayStatus).getChild(index).name
                else:
-                       status = 
self._statusBarMessages[index][statusAPI(index, 39, ret=True)]
+                       status = 
self._statusBarMessages[index][studioAPI(index, 39, ret=True)]
                ui.message(status if 
splconfig.SPLConfig["General"]["MessageVerbosity"] == "beginner" else 
status.split()[-1])
 
        # The layer commands themselves.
@@ -1601,8 +1734,8 @@ class AppModule(appModuleHandler.AppModule):
        def script_sayCartEditStatus(self, gesture):
                # 16.12: Because cart edit status also shows cart insert 
status, verbosity control will not apply.
                if self.productVersion >= "5.20":
-                       cartEdit = statusAPI(5, 39, ret=True)
-                       cartInsert = statusAPI(6, 39, ret=True)
+                       cartEdit = studioAPI(5, 39, ret=True)
+                       cartInsert = studioAPI(6, 39, ret=True)
                        if cartEdit: ui.message("Cart Edit On")
                        elif not cartEdit and cartInsert: ui.message("Cart 
Insert On")
                        else: ui.message("Cart Edit Off")
@@ -1610,11 +1743,11 @@ class AppModule(appModuleHandler.AppModule):
                        
ui.message(self.status(self.SPLPlayStatus).getChild(5).name)
 
        def script_sayHourTrackDuration(self, gesture):
-               statusAPI(0, 27, self.announceTime)
+               studioAPI(0, 27, self.announceTime)
 
        def script_sayHourRemaining(self, gesture):
                # 7.0: Split from playlist remaining script (formerly the 
playlist remainder command).
-               statusAPI(1, 27, self.announceTime)
+               studioAPI(1, 27, self.announceTime)
 
        def script_sayPlaylistRemainingDuration(self, gesture):
                obj = api.getFocusObject() if 
api.getForegroundObject().windowClassName == "TStudioForm" else 
self._focusedTrack
@@ -1624,15 +1757,7 @@ class AppModule(appModuleHandler.AppModule):
                if obj.role == controlTypes.ROLE_LIST:
                        ui.message("00:00")
                        return
-               col = obj.indexOf("Duration")
-               totalDuration = 0
-               while obj is not None:
-                       segue = obj._getColumnContent(col)
-                       if segue is not None:
-                               hms = segue.split(":")
-                               totalDuration += (int(hms[0])*3600) + 
(int(hms[1])*60) + int(hms[2]) if len(hms) == 3 else (int(hms[0])*60) + 
int(hms[1])
-                       obj = obj.next
-               self.announceTime(totalDuration, ms=False)
+               self.announceTime(self.playlistDuration(start=obj), ms=False)
 
        def script_sayPlaylistModified(self, gesture):
                try:
@@ -1643,6 +1768,9 @@ class AppModule(appModuleHandler.AppModule):
                        ui.message(_("Playlist modification not available"))
 
        def script_sayNextTrackTitle(self, gesture):
+               if not studioIsRunning():
+                       self.finish()
+                       return
                try:
                        obj = self.status(self.SPLNextTrackTitle).firstChild
                        # Translators: Presented when there is no information 
for the next track.
@@ -1656,6 +1784,9 @@ class AppModule(appModuleHandler.AppModule):
        script_sayNextTrackTitle.__doc__=_("Announces title of the next track 
if any")
 
        def script_sayCurrentTrackTitle(self, gesture):
+               if not studioIsRunning():
+                       self.finish()
+                       return
                try:
                        obj = self.status(self.SPLCurrentTrackTitle).firstChild
                        # Translators: Presented when there is no information 
for the current track.
@@ -1669,6 +1800,9 @@ class AppModule(appModuleHandler.AppModule):
        script_sayCurrentTrackTitle.__doc__=_("Announces title of the currently 
playing track")
 
        def script_sayTemperature(self, gesture):
+               if not studioIsRunning():
+                       self.finish()
+                       return
                try:
                        obj = self.status(self.SPLTemperature).firstChild
                        # Translators: Presented when there is no weather or 
temperature information.
@@ -1690,7 +1824,7 @@ class AppModule(appModuleHandler.AppModule):
                # 16.12: use Studio API if using 5.20.
                if self.productVersion >= "5.20":
                        # Sometimes, hour markers return seconds.999 due to 
rounding error, hence this must be taken care of here.
-                       trackStarts = divmod(statusAPI(3, 27, ret=True), 1000)
+                       trackStarts = divmod(studioAPI(3, 27, ret=True), 1000)
                        # For this method, all three components of time display 
(hour, minute, second) must be present.
                        # In case it is midnight (0.0 but sometimes shown as 
86399.999 due to rounding error), just say "midnight".
                        if trackStarts in ((86399, 999), (0, 0)): 
ui.message("00:00:00")
@@ -1704,7 +1838,7 @@ class AppModule(appModuleHandler.AppModule):
                # 16.12: Use Studio 5.20 API (faster and more reliable).
                if self.productVersion >= "5.20":
                        # This is the only time hour announcement should not be 
used in order to conform to what's displayed on screen.
-                       self.announceTime(statusAPI(4, 27, ret=True), 
includeHours=False)
+                       self.announceTime(studioAPI(4, 27, ret=True), 
includeHours=False)
                else:
                        obj = self.status(self.SPLScheduledToPlay).firstChild
                        ui.message(obj.name)
@@ -1726,12 +1860,9 @@ class AppModule(appModuleHandler.AppModule):
 
        def script_libraryScanMonitor(self, gesture):
                if not self.libraryScanning:
-                       if self.productVersion >= "5.10":
-                               scanning = statusAPI(1, 32, ret=True)
-                               if scanning < 0:
-                                       items = statusAPI(0, 32, ret=True)
-                                       ui.message(_("{itemCount} items in the 
library").format(itemCount = items))
-                                       return
+                       if studioAPI(1, 32, ret=True) < 0:
+                               ui.message(_("{itemCount} items in the 
library").format(itemCount = studioAPI(0, 32, ret=True)))
+                               return
                        self.libraryScanning = True
                        # Translators: Presented when attempting to start 
library scan.
                        ui.message(_("Monitoring library scan"))
@@ -1774,7 +1905,7 @@ class AppModule(appModuleHandler.AppModule):
                        analysisBegin = min(self._analysisMarker, trackPos)
                        analysisEnd = max(self._analysisMarker, trackPos)
                        analysisRange = analysisEnd-analysisBegin+1
-                       totalLength = self.totalTime(analysisBegin, analysisEnd)
+                       totalLength = self.playlistDurationRaw(analysisBegin, 
analysisEnd)
                        if analysisRange == 1:
                                self.announceTime(totalLength)
                        else:
@@ -1783,6 +1914,33 @@ class AppModule(appModuleHandler.AppModule):
        # Translators: Input help mode message for a command in Station 
Playlist Studio.
        script_trackTimeAnalysis.__doc__=_("Announces total length of tracks 
between analysis start marker and the current track")
 
+       def script_takePlaylistSnapshots(self, gesture):
+               if not studioIsRunning():
+                       self.finish()
+                       return
+               obj = api.getFocusObject() if 
api.getForegroundObject().windowClassName == "TStudioForm" else 
self._focusedTrack
+               if obj is None:
+                       ui.message("Please return to playlist viewer before 
invoking this command.")
+                       self.finish()
+                       return
+               if obj.role == controlTypes.ROLE_LIST:
+                       ui.message(_("You need to add tracks before invoking 
this command"))
+                       self.finish()
+                       return
+               scriptCount = scriptHandler.getLastScriptRepeatCount()
+               # Display the decorated HTML window on the first press if told 
to do so.
+               if 
splconfig.SPLConfig["PlaylistSnapshots"]["ShowResultsWindowOnFirstPress"]:
+                       scriptCount += 1
+               # Never allow this to be invoked more than twice, as it causes 
performance degredation and multiple HTML windows are opened.
+               if scriptCount >= 2:
+                       self.finish()
+                       return
+               # Speak and braille on the first press, display a decorated 
HTML message for subsequent presses.
+               
self.playlistSnapshotOutput(self.playlistSnapshots(obj.parent.firstChild, 
None), scriptCount)
+               self.finish()
+       # Translators: Input help mode message for a command in Station 
Playlist Studio.
+       script_takePlaylistSnapshots.__doc__=_("Presents playlist snapshot 
information such as number of tracks and top artists")
+
        def script_switchProfiles(self, gesture):
                splconfig.triggerProfileSwitch() if 
splconfig._triggerProfileActive else splconfig.instantProfileSwitch()
 
@@ -1816,8 +1974,7 @@ class AppModule(appModuleHandler.AppModule):
                        track = self._trackLocator(self.placeMarker[1], 
obj=api.getFocusObject().parent.firstChild, columns=[self.placeMarker[0]])
                        # 16.11: Just like Track Finder, use select track 
function to select the place marker track.
                        selectTrack(track.IAccessibleChildID-1)
-                       if self.productVersion >= "5.10":
-                               track.setFocus(), track.setFocus()
+                       track.setFocus(), track.setFocus()
 
        def script_metadataStreamingAnnouncer(self, gesture):
                # 8.0: Call the module-level function directly.
@@ -1826,8 +1983,7 @@ class AppModule(appModuleHandler.AppModule):
        # Gesture(s) for the following script cannot be changed by users.
        def script_metadataEnabled(self, gesture):
                url = int(gesture.displayName[-1])
-               checked = statusAPI(url, 36, ret=True)
-               if checked == 1:
+               if studioAPI(url, 36, ret=True):
                        # 0 is DSP encoder status, others are servers.
                        if url:
                                # Translators: Status message for metadata 
streaming.
@@ -1865,7 +2021,7 @@ class AppModule(appModuleHandler.AppModule):
                wx.CallAfter(gui.messageBox, SPLAssistantHelp[compatibility], 
title)
 
        def script_openOnlineDoc(self, gesture):
-               
os.startfile("https://github.com/josephsl/stationplaylist/wiki/SPLAddonGuide";)
+               
os.startfile("https://github.com/josephsl/stationplaylist/wiki/SPLDevAddonGuide";)
 
        def script_updateCheck(self, gesture):
                self.finish()
@@ -1878,7 +2034,7 @@ class AppModule(appModuleHandler.AppModule):
                        _("Add-on update check"),
                        # Translators: The message displayed while checking for 
newer version of Studio add-on.
                        _("Checking for new version of Studio add-on..."))
-               threading.Thread(target=splupdate.updateCheck, 
kwargs={"continuous":splconfig.SPLConfig["Update"]["AutoUpdateCheck"], 
"confUpdateInterval":splconfig.SPLConfig["Update"]["UpdateInterval"]}).start()
+               threading.Thread(target=splupdate.updateChecker, 
kwargs={"continuous":splconfig.SPLConfig["Update"]["AutoUpdateCheck"], 
"confUpdateInterval":splconfig.SPLConfig["Update"]["UpdateInterval"]}).start()
 
 
        __SPLAssistantGestures={
@@ -1901,6 +2057,7 @@ class AppModule(appModuleHandler.AppModule):
                "kb:shift+s":"sayScheduledToPlay",
                "kb:shift+p":"sayTrackPitch",
                "kb:shift+r":"libraryScanMonitor",
+               "kb:f8":"takePlaylistSnapshots",
                "kb:f9":"markTrackForAnalysis",
                "kb:f10":"trackTimeAnalysis",
                "kb:f12":"switchProfiles",
@@ -1934,6 +2091,7 @@ class AppModule(appModuleHandler.AppModule):
                "kb:shift+s":"sayScheduledToPlay",
                "kb:shift+p":"sayTrackPitch",
                "kb:shift+r":"libraryScanMonitor",
+               "kb:f8":"takePlaylistSnapshots",
                "kb:f9":"markTrackForAnalysis",
                "kb:f10":"trackTimeAnalysis",
                "kb:f12":"switchProfiles",
@@ -1968,6 +2126,7 @@ class AppModule(appModuleHandler.AppModule):
                "kb:shift+s":"sayScheduledToPlay",
                "kb:shift+p":"sayTrackPitch",
                "kb:shift+r":"libraryScanMonitor",
+               "kb:f8":"takePlaylistSnapshots",
                "kb:f9":"markTrackForAnalysis",
                "kb:f10":"trackTimeAnalysis",
                "kb:f12":"switchProfiles",

diff --git a/addon/appModules/splstudio/splconfig.py 
b/addon/appModules/splstudio/splconfig.py
index 7ae5c1f..0e41fef 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -12,7 +12,6 @@ from validate import Validator
 import time
 import datetime
 import cPickle
-import copy
 import globalVars
 import ui
 import gui
@@ -32,7 +31,7 @@ except ImportError:
 SPLIni = os.path.join(globalVars.appArgs.configPath, "splstudio.ini")
 SPLProfiles = os.path.join(globalVars.appArgs.configPath, "addons", 
"stationPlaylist", "profiles")
 # New (7.0) style config.
-confspec7 = ConfigObj(StringIO("""
+confspec = ConfigObj(StringIO("""
 [General]
 BeepAnnounce = boolean(default=false)
 MessageVerbosity = option("beginner", "advanced", default="beginner")
@@ -40,13 +39,23 @@ BrailleTimer = option("off", "intro", "outro", "both", 
default="off")
 AlarmAnnounce = option("beep", "message", "both", default="beep")
 TrackCommentAnnounce = option("off", "beep", "message", "both", default="off")
 LibraryScanAnnounce = option("off", "ending", "progress", "numbers", 
default="off")
-TrackDial = boolean(default=false)
 CategorySounds = boolean(default=false)
 TopBottomAnnounce = boolean(default=true)
 MetadataReminder = option("off", "startup", "instant", default="off")
 TimeHourAnnounce = boolean(default=true)
 ExploreColumns = 
string_list(default=list("Artist","Title","Duration","Intro","Category","Filename","Year","Album","Genre","Time
 Scheduled"))
 ExploreColumnsTT = 
string_list(default=list("Artist","Title","Duration","Cue","Overlap","Intro","Segue","Filename","Album","CD
 Code"))
+VerticalColumnAnnounce = 
option(None,"Status","Artist","Title","Duration","Intro","Outro","Category","Year","Album","Genre","Mood","Energy","Tempo","BPM","Gender","Rating","Filename","Time
 Scheduled",default=None)
+[PlaylistSnapshots]
+DurationMinMax = boolean(default=true)
+DurationAverage = boolean(default=true)
+ArtistCount = boolean(default=true)
+ArtistCountLimit = integer(min=0, max=10, default=5)
+CategoryCount = boolean(default=true)
+CategoryCountLimit = integer(min=0, max=10, default=5)
+GenreCount = boolean(default=true)
+GenreCountLimit = integer(min=0, max=10, default=5)
+ShowResultsWindowOnFirstPress = boolean(default=false)
 [IntroOutroAlarms]
 SayEndOfTrack = boolean(default=true)
 EndOfTrackTime = integer(min=1, max=59, default=5)
@@ -76,15 +85,16 @@ UpdateInterval = integer(min=1, max=30, default=7)
 [Startup]
 AudioDuckingReminder = boolean(default=true)
 WelcomeDialog = boolean(default=true)
-Studio500 = boolean(default=true)
 """), encoding="UTF-8", list_values=False)
-confspec7.newlines = "\r\n"
+confspec.newlines = "\r\n"
 SPLConfig = None
 # The following settings can be changed in profiles:
-_mutatableSettings7=("IntroOutroAlarms", "MicrophoneAlarm", 
"MetadataStreaming", "ColumnAnnouncement")
+_mutatableSettings=("IntroOutroAlarms", "MicrophoneAlarm", 
"MetadataStreaming", "ColumnAnnouncement")
 # 7.0: Profile-specific confspec (might be removed once a more optimal way to 
validate sections is found).
 # Dictionary comprehension is better here.
-confspecprofiles = {sect:key for sect, key in confspec7.iteritems() if sect in 
_mutatableSettings7}
+confspecprofiles = {sect:key for sect, key in confspec.iteritems() if sect in 
_mutatableSettings}
+# Translators: The name of the default (normal) profile.
+defaultProfileName = _("Normal profile")
 
 # 8.0: Run-time config storage and management will use ConfigHub data 
structure, a subclass of chain map.
 # A chain map allows a dictionary to look up predefined mappings to locate a 
key.
@@ -105,11 +115,16 @@ class ConfigHub(ChainMap):
                super(ConfigHub, self).__init__()
                # For presentational purposes.
                self.profileNames = []
-               # Translators: The name of the default (normal) profile.
-               self.maps[0] = self._unlockConfig(SPLIni, profileName=_("Normal 
profile"), prefill=True, validateNow=True)
+               self.maps[0] = self._unlockConfig(SPLIni, 
profileName=defaultProfileName, prefill=True, validateNow=True)
                self.profileNames.append(None) # Signifying normal profile.
                # Always cache normal profile upon startup.
                self._cacheConfig(self.maps[0])
+               # Remove deprecated keys.
+               # This action must be performed after caching, otherwise the 
newly modified profile will not be saved.
+               deprecatedKeys = {"General":"TrackDial", "Startup":"Studio500"}
+               for section, key in deprecatedKeys.iteritems():
+                       if key in self.maps[0][section]: del 
self.maps[0][section][key]
+               # Moving onto broadcast profiles if any.
                try:
                        profiles = filter(lambda fn: os.path.splitext(fn)[-1] 
== ".ini", os.listdir(SPLProfiles))
                        for profile in profiles:
@@ -148,10 +163,10 @@ class ConfigHub(ChainMap):
                # 7.0: What if profiles have parsing errors?
                # If so, reset everything back to factory defaults.
                try:
-                       SPLConfigCheckpoint = ConfigObj(path, configspec = 
confspec7 if prefill else confspecprofiles, encoding="UTF-8")
+                       SPLConfigCheckpoint = ConfigObj(path, configspec = 
confspec if prefill else confspecprofiles, encoding="UTF-8")
                except:
                        open(path, "w").close()
-                       SPLConfigCheckpoint = ConfigObj(path, configspec = 
confspec7 if prefill else confspecprofiles, encoding="UTF-8")
+                       SPLConfigCheckpoint = ConfigObj(path, configspec = 
confspec if prefill else confspecprofiles, encoding="UTF-8")
                        _configLoadStatus[profileName] = "fileReset"
                # 5.2 and later: check to make sure all values are correct.
                # 7.0: Make sure errors are displayed as config keys are now 
sections and may need to go through subkeys.
@@ -176,7 +191,7 @@ class ConfigHub(ChainMap):
                                # Case 1: restore settings to defaults when 5.x 
config validation has failed on all values.
                                # 6.0: In case this is a user profile, apply 
base configuration.
                                # 8.0: Call copy profile function directly to 
reduce overhead.
-                               copyProfile(_SPLDefaults7, SPLConfigCheckpoint, 
complete=SPLConfigCheckpoint.filename == SPLIni)
+                               copyProfile(_SPLDefaults, SPLConfigCheckpoint, 
complete=SPLConfigCheckpoint.filename == SPLIni)
                                _configLoadStatus[profileName] = "completeReset"
                        elif isinstance(configTest, dict):
                                # Case 2: For 5.x and later, attempt to 
reconstruct the failed values.
@@ -186,7 +201,7 @@ class ConfigHub(ChainMap):
                                        if isinstance(configTest[setting], 
dict):
                                                for failedKey in 
configTest[setting].keys():
                                                        # 7.0 optimization: 
just reload from defaults dictionary, as broadcast profiles contain 
profile-specific settings only.
-                                                       
SPLConfigCheckpoint[setting][failedKey] = _SPLDefaults7[setting][failedKey]
+                                                       
SPLConfigCheckpoint[setting][failedKey] = _SPLDefaults[setting][failedKey]
                                # 7.0: Disqualified from being cached this time.
                                SPLConfigCheckpoint.write()
                                _configLoadStatus[profileName] = "partialReset"
@@ -223,7 +238,7 @@ class ConfigHub(ChainMap):
        def deleteProfile(self, name):
                # Bring normal profile to the front if it isn't.
                # Optimization: Tell the swapper that we need index to the 
normal profile for this case.
-               configPos = self.swapProfiles(name, _("Normal profile"), 
showSwitchIndex=True) if self.profiles[0].name == name else 
self.profileIndexByName(name)
+               configPos = self.swapProfiles(name, defaultProfileName, 
showSwitchIndex=True) if self.profiles[0].name == name else 
self.profileIndexByName(name)
                profilePos = self.profileNames.index(name)
                try:
                        os.remove(self.profiles[configPos].filename)
@@ -235,6 +250,7 @@ class ConfigHub(ChainMap):
 
        def _cacheConfig(self, conf):
                global _SPLCache
+               import copy
                if _SPLCache is None: _SPLCache = {}
                key = None if conf.filename == SPLIni else conf.name
                _SPLCache[key] = {}
@@ -244,7 +260,7 @@ class ConfigHub(ChainMap):
 
        def __delitem__(self, key):
                # Consult profile-specific key first before deleting anything.
-               pos = 0 if key in _mutatableSettings7 else [profile.name for 
profile in self.maps].index(_("Normal Profile"))
+               pos = 0 if key in _mutatableSettings else [profile.name for 
profile in self.maps].index(defaultProfileName)
                try:
                        del self.maps[pos][key]
                except KeyError:
@@ -255,7 +271,7 @@ class ConfigHub(ChainMap):
                # 7.0: Save normal profile first.
                # Temporarily merge normal profile.
                # 8.0: Locate the index instead.
-               normalProfile = self.profileIndexByName(_("Normal profile"))
+               normalProfile = self.profileIndexByName(defaultProfileName)
                _preSave(self.profiles[normalProfile])
                # Disk write optimization check please.
                # 8.0: Bypass this if profiles were reset.
@@ -296,14 +312,14 @@ class ConfigHub(ChainMap):
                        profilePath = conf.filename
                        conf.reset()
                        conf.filename = profilePath
-                       resetConfig(_SPLDefaults7, conf)
+                       resetConfig(_SPLDefaults, conf)
                        # Convert certain settings to a different format.
-                       conf["ColumnAnnouncement"]["IncludedColumns"] = 
set(_SPLDefaults7["ColumnAnnouncement"]["IncludedColumns"])
+                       conf["ColumnAnnouncement"]["IncludedColumns"] = 
set(_SPLDefaults["ColumnAnnouncement"]["IncludedColumns"])
                # Switch back to normal profile via a custom variant of swap 
routine.
-               if self.profiles[0].name != _("Normal profile"):
-                       npIndex = self.profileIndexByName(_("Normal profile"))
+               if self.profiles[0].name != defaultProfileName:
+                       npIndex = self.profileIndexByName(defaultProfileName)
                        self.profiles[0], self.profiles[npIndex] = 
self.profiles[npIndex], self.profiles[0]
-                       self.activeProfile = _("Normal profile")
+                       self.activeProfile = defaultProfileName
                # 8.0 optimization: Tell other modules that reset was done in 
order to postpone disk writes until the end.
                self.resetHappened = True
 
@@ -359,9 +375,9 @@ class ConfigHub(ChainMap):
 
 # Default config spec container.
 # To be moved to a different place in 8.0.
-_SPLDefaults7 = ConfigObj(None, configspec = confspec7, encoding="UTF-8")
+_SPLDefaults = ConfigObj(None, configspec = confspec, encoding="UTF-8")
 _val = Validator()
-_SPLDefaults7.validate(_val, copy=True)
+_SPLDefaults.validate(_val, copy=True)
 
 # Display an error dialog when configuration validation fails.
 def runConfigErrorDialog(errorText, errorType):
@@ -453,7 +469,7 @@ def _extraInitSteps(conf, profileName=None):
        global _configLoadStatus
        columnOrder = conf["ColumnAnnouncement"]["ColumnOrder"]
        # Catch suttle errors.
-       fields = _SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"]
+       fields = _SPLDefaults["ColumnAnnouncement"]["ColumnOrder"]
        invalidFields = 0
        for field in fields:
                if field not in columnOrder: invalidFields+=1
@@ -474,6 +490,9 @@ def _extraInitSteps(conf, profileName=None):
                else:
                        _configLoadStatus[profileName] = "metadataReset"
                conf["MetadataStreaming"]["MetadataEnabled"] = [False, False, 
False, False, False]
+       # 17.04: If vertical column announcement value is "None", transform 
this to NULL.
+       if conf["General"]["VerticalColumnAnnounce"] == "None":
+               conf["General"]["VerticalColumnAnnounce"] = None
 
 # Cache a copy of the loaded config.
 # This comes in handy when saving configuration to disk. For the most part, no 
change occurs to config.
@@ -651,7 +670,7 @@ def getProfileByName(name):
 # Setting complete flag controls whether profile-specific settings are applied 
(true otherwise, only set when resetting profiles).
 # 8.0: Simplified thanks to in-place swapping.
 def copyProfile(sourceProfile, targetProfile, complete=False):
-       for section in sourceProfile.keys() if complete else 
_mutatableSettings7:
+       for section in sourceProfile.keys() if complete else _mutatableSettings:
                targetProfile[section] = dict(sourceProfile[section])
 
 # Last but not least...
@@ -698,7 +717,7 @@ def _preSave(conf):
                for setting in conf.keys():
                        for key in conf[setting].keys():
                                try:
-                                       if conf[setting][key] == 
_SPLDefaults7[setting][key]:
+                                       if conf[setting][key] == 
_SPLDefaults[setting][key]:
                                                del conf[setting][key]
                                except KeyError:
                                        pass
@@ -752,7 +771,7 @@ def switchProfile(prevProfile, newProfile):
        SPLConfig.switchProfile(prevProfile, newProfile)
        SPLPrevProfile = prevProfile
        # 8.0: Cache other profiles this time.
-       if newProfile != _("Normal profile") and newProfile not in _SPLCache:
+       if newProfile != defaultProfileName and newProfile not in _SPLCache:
                _cacheConfig(getProfileByName(newProfile))
 
 # Called from within the app module.
@@ -809,22 +828,25 @@ def triggerProfileSwitch():
                        _SPLTriggerEndTimer.Stop()
                        _SPLTriggerEndTimer = None
 
-
 # Automatic update checker.
 
 # The function below is called as part of the update check timer.
 # Its only job is to call the update check function (splupdate) with the auto 
check enabled.
 # The update checker will not be engaged if an instant switch profile is 
active or it is not time to check for it yet (check will be done every 24 
hours).
 def autoUpdateCheck():
-       splupdate.updateCheck(auto=True, 
continuous=SPLConfig["Update"]["AutoUpdateCheck"], 
confUpdateInterval=SPLConfig["Update"]["UpdateInterval"])
+       splupdate.updateChecker(auto=True, 
continuous=SPLConfig["Update"]["AutoUpdateCheck"], 
confUpdateInterval=SPLConfig["Update"]["UpdateInterval"])
 
 # The timer itself.
 # A bit simpler than NVDA Core's auto update checker.
 def updateInit():
        # LTS: Launch updater if channel change is detected.
+       # Use a background thread for this as urllib blocks.
+       import threading
        if splupdate._updateNow:
-               splupdate.updateCheck(auto=True) # No repeat here.
+               t = threading.Thread(target=splupdate.updateChecker, 
kwargs={"auto": True}) # No repeat here.
+               t.daemon = True
                splupdate._SPLUpdateT = wx.PyTimer(autoUpdateCheck)
+               t.start()
                splupdate._updateNow = False
                return
        currentTime = time.time()
@@ -834,153 +856,22 @@ def updateInit():
        elif splupdate.SPLAddonCheck < nextCheck < currentTime:
                interval = SPLConfig["Update"]["UpdateInterval"]* 86400
                # Call the update check now.
-               splupdate.updateCheck(auto=True) # No repeat here.
+               t = threading.Thread(target=splupdate.updateChecker, 
kwargs={"auto": True}) # No repeat here.
+               t.daemon = True
+               t.start()
        splupdate._SPLUpdateT = wx.PyTimer(autoUpdateCheck)
        splupdate._SPLUpdateT.Start(interval * 1000, True)
 
-
 # Let SPL track item know if it needs to build description pieces.
 # To be renamed and used in other places in 7.0.
 def _shouldBuildDescriptionPieces():
        return (not SPLConfig["ColumnAnnouncement"]["UseScreenColumnOrder"]
-       and (SPLConfig["ColumnAnnouncement"]["ColumnOrder"] != 
_SPLDefaults7["ColumnAnnouncement"]["ColumnOrder"]
+       and (SPLConfig["ColumnAnnouncement"]["ColumnOrder"] != 
_SPLDefaults["ColumnAnnouncement"]["ColumnOrder"]
        or len(SPLConfig["ColumnAnnouncement"]["IncludedColumns"]) != 17))
 
-
-# Additional configuration dialogs
+# Additional configuration and miscellaneous dialogs
 # See splconfui module for basic configuration dialogs.
 
-# A common alarm dialog
-# Based on NVDA core's find dialog code (implemented by the author of this 
add-on).
-# Extended in 2016 to handle microphone alarms.
-# Only one instance can be active at a given moment (code borrowed from GUI's 
exit dialog routine).
-_alarmDialogOpened = False
-
-# A common alarm error dialog.
-def _alarmError():
-       # Translators: Text of the dialog when another alarm dialog is open.
-       gui.messageBox(_("Another alarm dialog is 
open."),_("Error"),style=wx.OK | wx.ICON_ERROR)
-
-class SPLAlarmDialog(wx.Dialog):
-       """A dialog providing common alarm settings.
-       This dialog contains a number entry field for alarm duration and a 
check box to enable or disable the alarm.
-       For one particular case, it consists of two number entry fields.
-       """
-
-       # The following comes from exit dialog class from GUI package (credit: 
NV Access and Zahari from Bulgaria).
-       _instance = None
-
-       def __new__(cls, parent, *args, **kwargs):
-               # Make this a singleton and prompt an error dialog if it isn't.
-               if _alarmDialogOpened:
-                       raise RuntimeError("An instance of alarm dialog is 
opened")
-               inst = cls._instance() if cls._instance else None
-               if not inst:
-                       return super(cls, cls).__new__(cls, parent, *args, 
**kwargs)
-               return inst
-
-       def __init__(self, parent, level=0):
-               inst = SPLAlarmDialog._instance() if SPLAlarmDialog._instance 
else None
-               if inst:
-                       return
-               # Use a weakref so the instance can die.
-               import weakref
-               SPLAlarmDialog._instance = weakref.ref(self)
-
-               # Now the actual alarm dialog code.
-               # 8.0: Apart from level 0 (all settings shown), levels change 
title.
-               titles = (_("Alarms Center"), _("End of track alarm"), _("Song 
intro alarm"), _("Microphone alarm"))
-               super(SPLAlarmDialog, self).__init__(parent, wx.ID_ANY, 
titles[level])
-               self.level = level
-               mainSizer = wx.BoxSizer(wx.VERTICAL)
-
-               if level in (0, 1):
-                       timeVal = 
SPLConfig["IntroOutroAlarms"]["EndOfTrackTime"]
-                       alarmSizer = wx.BoxSizer(wx.HORIZONTAL)
-                       alarmMessage = wx.StaticText(self, wx.ID_ANY, 
label=_("Enter &end of track alarm time in seconds (currently 
{curAlarmSec})").format(curAlarmSec = timeVal))
-                       alarmSizer.Add(alarmMessage)
-                       self.outroAlarmEntry = wx.SpinCtrl(self, wx.ID_ANY, 
min=1, max=59)
-                       self.outroAlarmEntry.SetValue(timeVal)
-                       self.outroAlarmEntry.SetSelection(-1, -1)
-                       alarmSizer.Add(self.outroAlarmEntry)
-                       
mainSizer.Add(alarmSizer,border=20,flag=wx.LEFT|wx.RIGHT|wx.TOP)
-                       
self.outroToggleCheckBox=wx.CheckBox(self,wx.NewId(),label=_("&Notify when end 
of track is approaching"))
-                       
self.outroToggleCheckBox.SetValue(SPLConfig["IntroOutroAlarms"]["SayEndOfTrack"])
-                       
mainSizer.Add(self.outroToggleCheckBox,border=10,flag=wx.BOTTOM)
-
-               if level in (0, 2):
-                       rampVal = SPLConfig["IntroOutroAlarms"]["SongRampTime"]
-                       alarmSizer = wx.BoxSizer(wx.HORIZONTAL)
-                       alarmMessage = wx.StaticText(self, wx.ID_ANY, 
label=_("Enter song &intro alarm time in seconds (currently 
{curRampSec})").format(curRampSec = rampVal))
-                       alarmSizer.Add(alarmMessage)
-                       self.introAlarmEntry = wx.SpinCtrl(self, wx.ID_ANY, 
min=1, max=9)
-                       self.introAlarmEntry.SetValue(rampVal)
-                       self.introAlarmEntry.SetSelection(-1, -1)
-                       alarmSizer.Add(self.introAlarmEntry)
-                       
mainSizer.Add(alarmSizer,border=20,flag=wx.LEFT|wx.RIGHT|wx.TOP)
-                       
self.introToggleCheckBox=wx.CheckBox(self,wx.NewId(),label=_("&Notify when end 
of introduction is approaching"))
-                       
self.introToggleCheckBox.SetValue(SPLConfig["IntroOutroAlarms"]["SaySongRamp"])
-                       
mainSizer.Add(self.introToggleCheckBox,border=10,flag=wx.BOTTOM)
-
-               if level in (0, 3):
-                       micAlarm = SPLConfig["MicrophoneAlarm"]["MicAlarm"]
-                       micAlarmInterval = 
SPLConfig["MicrophoneAlarm"]["MicAlarmInterval"]
-                       if 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)
-                       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)")
-                       alarmSizer = wx.BoxSizer(wx.VERTICAL)
-                       alarmMessage = wx.StaticText(self, wx.ID_ANY, 
label=timeMSG)
-                       alarmSizer.Add(alarmMessage)
-                       self.micAlarmEntry = wx.SpinCtrl(self, wx.ID_ANY, 
min=0, max=7200)
-                       self.micAlarmEntry.SetValue(micAlarm)
-                       self.micAlarmEntry.SetSelection(-1, -1)
-                       alarmSizer.Add(self.micAlarmEntry)
-                       alarmMessage = wx.StaticText(self, wx.ID_ANY, 
label=_("Microphone alarm interval"))
-                       alarmSizer.Add(alarmMessage)
-                       self.micIntervalEntry = wx.SpinCtrl(self, wx.ID_ANY, 
min=0, max=60)
-                       self.micIntervalEntry.SetValue(micAlarmInterval)
-                       self.micIntervalEntry.SetSelection(-1, -1)
-                       alarmSizer.Add(self.micIntervalEntry)
-                       
mainSizer.Add(alarmSizer,border=20,flag=wx.LEFT|wx.RIGHT|wx.TOP)
-
-               mainSizer.AddSizer(self.CreateButtonSizer(wx.OK|wx.CANCEL))
-               self.Bind(wx.EVT_BUTTON,self.onOk,id=wx.ID_OK)
-               self.Bind(wx.EVT_BUTTON,self.onCancel,id=wx.ID_CANCEL)
-               mainSizer.Fit(self)
-               self.SetSizer(mainSizer)
-               self.Center(wx.BOTH | wx.CENTER_ON_SCREEN)
-               if level in (0, 1): self.outroAlarmEntry.SetFocus()
-               elif level == 2: self.introAlarmEntry.SetFocus()
-               elif level == 3: self.micAlarmEntry.SetFocus()
-
-       def onOk(self, evt):
-               global SPLConfig, _alarmDialogOpened
-               # Optimization: don't bother if Studio is dead and if the same 
value has been entered.
-               import winUser
-               if winUser.user32.FindWindowA("SPLStudio", None):
-                       # Gather settings to be applied in section/key format.
-                       settings = []
-                       if self.level in (0, 1):
-                               SPLConfig["IntroOutroAlarms"]["EndOfTrackTime"] 
= self.outroAlarmEntry.GetValue()
-                               SPLConfig["IntroOutroAlarms"]["SayEndOfTrack"] 
= self.outroToggleCheckBox.GetValue()
-                       elif self.level in (0, 2):
-                               SPLConfig["IntroOutroAlarms"]["SongRampTime"] = 
self.introAlarmEntry.GetValue()
-                               SPLConfig["IntroOutroAlarms"]["SaySongRamp"] = 
self.introToggleCheckBox.GetValue()
-                       elif self.level in (0, 3):
-                               SPLConfig["MicrophoneAlarm"]["MicAlarm"] = 
self.micAlarmEntry.GetValue()
-                               
SPLConfig["MicrophoneAlarm"]["MicAlarmInterval"] = 
self.micIntervalEntry.GetValue()
-               self.Destroy()
-               _alarmDialogOpened = False
-
-       def onCancel(self, evt):
-               self.Destroy()
-               global _alarmDialogOpened
-               _alarmDialogOpened = False
-
-
 # Startup dialogs.
 
 # Audio ducking reminder (NVDA 2016.1 and later).
@@ -995,17 +886,18 @@ class AudioDuckingReminder(wx.Dialog):
                mainSizer = wx.BoxSizer(wx.VERTICAL)
 
                # Translators: A message displayed if audio ducking should be 
disabled.
-               label = wx.StaticText(self, wx.ID_ANY, label=_("NVDA 2016.1 and 
later allows NVDA to decrease volume of background audio including that of 
Studio. In order to not disrupt the listening experience of your listeners, 
please disable audio ducking by opening synthesizer dialog in NVDA and 
selecting 'no ducking' from audio ducking mode combo box or press 
NVDA+Shift+D."))
+               label = wx.StaticText(self, wx.ID_ANY, label=_("""NVDA 2016.1 
and later allows NVDA to decrease volume of background audio including that of 
Studio.
+               In order to not disrupt the listening experience of your 
listeners, please disable audio ducking either by:
+               * Opening synthesizer dialog in NVDA and selecting 'no ducking' 
from audio ducking mode combo box.
+               * Press NVDA+Shift+D to set it to 'no ducking'."""))
                mainSizer.Add(label,border=20,flag=wx.LEFT|wx.RIGHT|wx.TOP)
 
-               sizer = wx.BoxSizer(wx.HORIZONTAL)
                # Translators: A checkbox to turn off audio ducking reminder 
message.
                
self.audioDuckingReminder=wx.CheckBox(self,wx.NewId(),label=_("Do not show this 
message again"))
                self.audioDuckingReminder.SetValue(not 
SPLConfig["Startup"]["AudioDuckingReminder"])
-               sizer.Add(self.audioDuckingReminder, border=10,flag=wx.TOP)
-               mainSizer.Add(sizer, border=10, flag=wx.BOTTOM)
+               mainSizer.Add(self.audioDuckingReminder, border=10,flag=wx.TOP)
 
-               mainSizer.Add(self.CreateButtonSizer(wx.OK))
+               mainSizer.Add(self.CreateButtonSizer(wx.OK), 
flag=wx.ALIGN_CENTER_HORIZONTAL)
                self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK)
                mainSizer.Fit(self)
                self.Sizer = mainSizer
@@ -1051,14 +943,12 @@ Thank you.""")
                label = wx.StaticText(self, wx.ID_ANY, 
label=self.welcomeMessage)
                mainSizer.Add(label,border=20,flag=wx.LEFT|wx.RIGHT|wx.TOP)
 
-               sizer = wx.BoxSizer(wx.HORIZONTAL)
                # Translators: A checkbox to show welcome dialog.
                
self.showWelcomeDialog=wx.CheckBox(self,wx.NewId(),label=_("Show welcome dialog 
when I start Studio"))
                
self.showWelcomeDialog.SetValue(SPLConfig["Startup"]["WelcomeDialog"])
-               sizer.Add(self.showWelcomeDialog, border=10,flag=wx.TOP)
-               mainSizer.Add(sizer, border=10, flag=wx.BOTTOM)
+               mainSizer.Add(self.showWelcomeDialog, border=10,flag=wx.TOP)
 
-               mainSizer.Add(self.CreateButtonSizer(wx.OK))
+               mainSizer.Add(self.CreateButtonSizer(wx.OK), 
flag=wx.ALIGN_CENTER_HORIZONTAL)
                self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK)
                mainSizer.Fit(self)
                self.Sizer = mainSizer
@@ -1071,9 +961,10 @@ Thank you.""")
                self.Destroy()
 
 # Old version reminder.
-class OldVersionReminder(wx.Dialog):
-       """A dialog shown when using add-on 8.x under Studio 5.0x.
-       """
+# Only used when there is a LTS version.
+"""class OldVersionReminder(wx.Dialog):
+       #A dialog shown when using add-on 8.x under Studio 5.0x.
+       #
 
        def __init__(self, parent):
                # Translators: Title of a dialog displayed when the add-on 
starts reminding broadcasters about old Studio releases.
@@ -1088,7 +979,7 @@ class OldVersionReminder(wx.Dialog):
                sizer = wx.BoxSizer(wx.HORIZONTAL)
                # Translators: A checkbox to turn off old version reminder 
message.
                self.oldVersionReminder=wx.CheckBox(self,wx.NewId(),label=_("Do 
not show this message again"))
-               self.oldVersionReminder.SetValue(not 
SPLConfig["Startup"]["Studio500"])
+               self.oldVersionReminder.SetValue(not 
SPLConfig["Startup"]["OldSPLVersionReminder"])
                sizer.Add(self.oldVersionReminder, border=10,flag=wx.TOP)
                mainSizer.Add(sizer, border=10, flag=wx.BOTTOM)
 
@@ -1102,15 +993,16 @@ class OldVersionReminder(wx.Dialog):
        def onOk(self, evt):
                global SPLConfig
                if self.oldVersionReminder.Value:
-                       SPLConfig["Startup"]["Studio500"] = not 
self.oldVersionReminder.Value
-               self.Destroy()
+                       SPLConfig["Startup"]["OldSPLVersionReminder"] = not 
self.oldVersionReminder.Value
+               self.Destroy()"""
 
 # And to open the above dialog and any other dialogs.
 def showStartupDialogs(oldVer=False):
-       if oldVer and SPLConfig["Startup"]["Studio500"]:
-               gui.mainFrame.prePopup()
-               OldVersionReminder(gui.mainFrame).Show()
-               gui.mainFrame.postPopup()
+       # Old version reminder if this is such a case.
+       #if oldVer and SPLConfig["Startup"]["OldSPLVersionReminder"]:
+               #gui.mainFrame.prePopup()
+               #OldVersionReminder(gui.mainFrame).Show()
+               #gui.mainFrame.postPopup()
        if SPLConfig["Startup"]["WelcomeDialog"]:
                gui.mainFrame.prePopup()
                WelcomeDialog(gui.mainFrame).Show()
@@ -1120,14 +1012,11 @@ def showStartupDialogs(oldVer=False):
                #if gui.messageBox("The next major version of the add-on (15.x) 
will be the last version to support Studio versions earlier than 5.10, with 
add-on 15.x being designated as a long-term support version. Would you like to 
switch to long-term support release?", "Long-Term Support version", wx.YES | 
wx.NO | wx.CANCEL | wx.CENTER | wx.ICON_QUESTION) == wx.YES:
                        #splupdate.SPLUpdateChannel = "lts"
                        #os.remove(os.path.join(globalVars.appArgs.configPath, 
"addons", "stationPlaylist", "ltsprep"))
-       try:
-               import audioDucking
-               if SPLConfig["Startup"]["AudioDuckingReminder"] and 
audioDucking.isAudioDuckingSupported():
-                       gui.mainFrame.prePopup()
-                       AudioDuckingReminder(gui.mainFrame).Show()
-                       gui.mainFrame.postPopup()
-       except ImportError:
-               pass
+       import audioDucking
+       if SPLConfig["Startup"]["AudioDuckingReminder"] and 
audioDucking.isAudioDuckingSupported():
+               gui.mainFrame.prePopup()
+               AudioDuckingReminder(gui.mainFrame).Show()
+               gui.mainFrame.postPopup()
 
 
 # Message verbosity pool.

This diff is so big that we needed to truncate the remainder.

https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/87b528628407/
Changeset:   87b528628407
Branch:      stable
User:        josephsl
Date:        2017-03-24 23:48:50+00:00
Summary:     SPL Studio add-on 17.04 official

Signed-off-by: Joseph Lee <joseph.lee22590@xxxxxxxxx>

Affected #:  3 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 634a8f5..297d0e8 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -2021,7 +2021,7 @@ class AppModule(appModuleHandler.AppModule):
                wx.CallAfter(gui.messageBox, SPLAssistantHelp[compatibility], 
title)
 
        def script_openOnlineDoc(self, gesture):
-               
os.startfile("https://github.com/josephsl/stationplaylist/wiki/SPLDevAddonGuide";)
+               
os.startfile("https://github.com/josephsl/stationplaylist/wiki/SPLAddonGuide";)
 
        def script_updateCheck(self, gesture):
                self.finish()

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 1b26bbd..a00e9d9 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -26,10 +26,10 @@ SPLAddonCheck = 0
 # Update metadata storage.
 SPLAddonState = {}
 # Update URL (the only way to change it is installing a different version from 
a different branch).
-SPLUpdateURL = "https://addons.nvda-project.org/files/get.php?file=spl-dev";
+SPLUpdateURL = "https://addons.nvda-project.org/files/get.php?file=spl";
 _pendingChannelChange = False
 _updateNow = False
-SPLUpdateChannel = "dev"
+SPLUpdateChannel = "stable"
 # Update check timer.
 _SPLUpdateT = None
 # How long it should wait between automatic checks.
@@ -41,7 +41,7 @@ _updatePickle = os.path.join(globalVars.appArgs.configPath, 
"splupdate.pickle")
 
 # Not all update channels are listed. The one not listed here is the default 
("stable" for this branch).
 channels={
-       "stable":"https://addons.nvda-project.org/files/get.php?file=spl";,
+       "dev":"https://addons.nvda-project.org/files/get.php?file=spl-dev";,
        #"beta":"http://spl.nvda-kr.org/files/get.php?file=spl-beta";,
 }
 
@@ -55,12 +55,12 @@ def initialize():
                if _updateNow: del SPLAddonState["pendingChannelChange"]
                if "UpdateChannel" in SPLAddonState:
                        SPLUpdateChannel = SPLAddonState["UpdateChannel"]
-                       if SPLUpdateChannel in ("beta", "lts"):
-                               SPLUpdateChannel = "dev"
+                       if SPLUpdateChannel in ("beta", "preview", "lts"):
+                               SPLUpdateChannel = "stable"
        except IOError, KeyError:
                SPLAddonState["PDT"] = 0
                _updateNow = False
-               SPLUpdateChannel = "dev"
+               SPLUpdateChannel = "stable"
 
 def terminate():
        global SPLAddonState

diff --git a/buildVars.py b/buildVars.py
index 5b65263..c12b7d6 100755
--- a/buildVars.py
+++ b/buildVars.py
@@ -20,7 +20,7 @@ addon_info = {
        "addon_description" : _("""Enhances support for StationPlaylist Studio.
 In addition, adds global commands for the studio from everywhere."""),
        # version
-       "addon_version" : "17.04-dev",
+       "addon_version" : "17.04",
        # 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.

Other related posts: