commit/StationPlaylist: 12 new changesets

  • From: commits-noreply@xxxxxxxxxxxxx
  • To: nvda-addons-commits@xxxxxxxxxxxxx
  • Date: Wed, 05 Apr 2017 18:24:05 -0000

12 new commits in StationPlaylist:

https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/c406bea090c5/
Changeset:   c406bea090c5
Branch:      None
User:        josephsl
Date:        2017-03-17 22:07:22+00:00
Summary:     Merge branch 'master' into 17.04.x

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..2ede801 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,184 @@ 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):
+               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: 
{playlistShortestTrack}").format(playlistShortestTrack = 
snapshot["PlaylistDurationMin"]))
+                       statusInfo.append(_("Longest: 
{playlistLongestTrack}").format(playlistLongestTrack = 
snapshot["PlaylistDurationMax"]))
+               if "PlaylistDurationAverage" in snapshot:
+                       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][:]))
+                       elif scriptCount == 1:
+                               artistList = []
+                               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:
+                               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(">", "")
+                                       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][:]))
+                       elif scriptCount == 1:
+                               genreList = []
+                               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:
+                       
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 +1572,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 +1582,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 +1657,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 +1677,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 +1698,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 +1721,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 +1730,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 +1744,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 +1755,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 +1771,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 +1787,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 +1811,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 +1825,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 +1847,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 +1892,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 +1901,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 +1961,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 +1970,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 +2008,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 +2021,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 +2044,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 +2078,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 +2113,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/afe47cea5353/
Changeset:   afe47cea5353
Branch:      None
User:        josephsl
Date:        2017-03-20 04:47:39+00:00
Summary:     Branch readiness (17.2-dev): instead of try builds, branch 
readiness will be used, along with change of dev filename.

Dev snapshot name will be stationPlaylist-yyyymmdd-dev instad of 
stationPlaylist-version-devyyyymmdd. This is done because there will be no more 
major releases after 17.2-dev.

Affected #:  1 file

diff --git a/sconstruct b/sconstruct
index b188807..cb62730 100755
--- a/sconstruct
+++ b/sconstruct
@@ -63,7 +63,7 @@ elif GetOption("dev"):
        import datetime
        buildDate = datetime.datetime.now()
        year, month, day = str(buildDate.year), str(buildDate.month), 
str(buildDate.day)
-       env["addon_version"] = "".join([env["addon_version"], year, 
month.zfill(2), day.zfill(2)])
+       env["addon_version"] = "".join([year, month.zfill(2), day.zfill(2), 
"-dev"])
 
 addonFile = env.File("${addon_name}-${addon_version}.nvda-addon")
 


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/d3ba0e0877b7/
Changeset:   d3ba0e0877b7
Branch:      None
User:        josephsl
Date:        2017-03-20 06:26:45+00:00
Summary:     Try builds: now based on master branch, channel change is possible.

Part of 17.2-dev: try build users will now be able to change channels.

Affected #:  2 files

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index a66f6ba..1c91197 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -1306,7 +1306,7 @@ class SayStatusDialog(wx.Dialog):
 class AdvancedOptionsDialog(wx.Dialog):
 
        # Available channels (if there's only one, channel selection list will 
not be shown).
-       _updateChannels = ("dev", "stable")
+       _updateChannels = ("try", "dev", "stable")
 
        def __init__(self, parent):
                # Translators: The title of a dialog to configure advanced SPL 
add-on options such as update checking.
@@ -1324,7 +1324,7 @@ class AdvancedOptionsDialog(wx.Dialog):
                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=advOptionsHelper.addLabeledControl(labelText, wx.Choice, 
choices=["try", "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")))

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 1b26bbd..07526f4 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -39,10 +39,9 @@ _retryAfterFailure = False
 # Stores update state.
 _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";,
-       #"beta":"http://spl.nvda-kr.org/files/get.php?file=spl-beta";,
+       "try":"http://www.josephsl.net/files/nvdaaddons/get.php?file=spl-try";,
 }
 
 # Come forth, update check routines.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/82de8b0843f3/
Changeset:   82de8b0843f3
Branch:      None
User:        josephsl
Date:        2017-03-20 06:30:02+00:00
Summary:     Merge branch 'stable' into 17.04.x

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/77c3d14d3fab/
Changeset:   77c3d14d3fab
Branch:      None
User:        josephsl
Date:        2017-03-20 06:31:58+00:00
Summary:     17.05: Location text will be friendly.

Instead of announcing object coordinates, location text will return current 
track position relative to the playlist item count.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 634a8f5..e7c4629 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -185,9 +185,9 @@ class SPLTrackItem(IAccessible):
                self.appModule._focusedTrack = self
 
        # A friendly way to report track position via location text.
-       """def _get_locationText(self):
+       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))"""
+               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.


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/07d53e550eb5/
Changeset:   07d53e550eb5
Branch:      None
User:        josephsl
Date:        2017-03-20 06:32:39+00:00
Summary:     Merge branch '17.04.x' into next

Affected #:  6 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index 634a8f5..e7c4629 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -185,9 +185,9 @@ class SPLTrackItem(IAccessible):
                self.appModule._focusedTrack = self
 
        # A friendly way to report track position via location text.
-       """def _get_locationText(self):
+       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))"""
+               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.

diff --git a/addon/doc/ar/readme.md b/addon/doc/ar/readme.md
index 8c41a3c..f121f1b 100644
--- a/addon/doc/ar/readme.md
+++ b/addon/doc/ar/readme.md
@@ -249,6 +249,12 @@ broadcast profiles.
 استخدم لمسة ب3 أصابع للانتقال لنمط اللمس, ثم استخدم أوامر اللمس المسرودة
 أعلاه لأداء المهام.
 
+## Version 17.03
+
+* NVDA will no longer appear to do anything or play an error tone when
+  switching to a time-based broadcast profile.
+* ترجمة الإضافة لمزيد من اللغات
+
 ## Version 17.01/15.5-LTS
 
 Note: 17.01.1/15.5A-LTS replaces 17.01 due to changes to location of new

diff --git a/addon/doc/es/readme.md b/addon/doc/es/readme.md
index 1d53d76..8ac6031 100644
--- a/addon/doc/es/readme.md
+++ b/addon/doc/es/readme.md
@@ -287,6 +287,12 @@ 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.
 
+## Versión 17.03
+
+* NVDA ya no parecerá no hacer nada o no reproducirá un tono de error al
+  cambiar a un perfil de transmisión basado en tiempo.
+* Traducciones actualizadas.
+
 ## Versión 17.01/15.5-LTS
 
 Nota: 17.01.1/15.5A-LTS reemplaza a 17.01 debido a cambios de la

diff --git a/addon/doc/fr/readme.md b/addon/doc/fr/readme.md
index 27dbba1..da64027 100644
--- a/addon/doc/fr/readme.md
+++ b/addon/doc/fr/readme.md
@@ -299,6 +299,12 @@ 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.03
+
+* NVDA ne semble plus rien faire ou ne lit plus une tonalité d'erreur
+  lorsque vous basculer à un profil de diffusion basé sur l'heure.
+* Mises à jour des traductions.
+
 ## Version 17.01/15.5-LTS
 
 Remarque: 17.01.1/15.5A-LTS remplace la 17.01 en raison des changements

diff --git a/addon/doc/gl/readme.md b/addon/doc/gl/readme.md
index e918f45..d7fe309 100644
--- a/addon/doc/gl/readme.md
+++ b/addon/doc/gl/readme.md
@@ -279,6 +279,12 @@ 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.
 
+## Versión 17.03
+
+* NVDA xa non parecerá non facer nada ou non reproducirá un ton de erro ao
+  cambiar a un perfil de transmisión baseado en tempo.
+* Traducións actualizadas.
+
 ## Versión 17.01/15.5-LTS
 
 Nota: 17.01.1/15.5A-LTS reemplaza a 17.01 debido aos cambios da localización

diff --git a/addon/doc/hu/readme.md b/addon/doc/hu/readme.md
index f3aafb1..4779d6a 100644
--- a/addon/doc/hu/readme.md
+++ b/addon/doc/hu/readme.md
@@ -260,6 +260,12 @@ 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.03
+
+* NVDA will no longer appear to do anything or play an error tone when
+  switching to a time-based broadcast profile.
+* Fordítások frissítése
+
 ## Version 17.01/15.5-LTS
 
 Note: 17.01.1/15.5A-LTS replaces 17.01 due to changes to location of new


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/e4e38c438e0f/
Changeset:   e4e38c438e0f
Branch:      None
User:        josephsl
Date:        2017-03-24 23:51:23+00:00
Summary:     Merged stable

Affected #:  3 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index e7c4629..c4d79e8 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 07526f4..6b058ac 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.
@@ -54,12 +54,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


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/ce203ca96f92/
Changeset:   ce203ca96f92
Branch:      None
User:        josephsl
Date:        2017-03-24 23:54:29+00:00
Summary:     Use dev channel values for updates

Affected #:  2 files

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index c4d79e8..e7c4629 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/SPLAddonGuide";)
+               
os.startfile("https://github.com/josephsl/stationplaylist/wiki/SPLDevAddonGuide";)
 
        def script_updateCheck(self, gesture):
                self.finish()

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index 6b058ac..cc213bd 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";
+SPLUpdateURL = "https://addons.nvda-project.org/files/get.php?file=spl-dev";
 _pendingChannelChange = False
 _updateNow = False
-SPLUpdateChannel = "stable"
+SPLUpdateChannel = "dev"
 # Update check timer.
 _SPLUpdateT = None
 # How long it should wait between automatic checks.
@@ -55,11 +55,11 @@ def initialize():
                if "UpdateChannel" in SPLAddonState:
                        SPLUpdateChannel = SPLAddonState["UpdateChannel"]
                        if SPLUpdateChannel in ("beta", "preview", "lts"):
-                               SPLUpdateChannel = "stable"
+                               SPLUpdateChannel = "dev"
        except IOError, KeyError:
                SPLAddonState["PDT"] = 0
                _updateNow = False
-               SPLUpdateChannel = "stable"
+               SPLUpdateChannel = "dev"
 
 def terminate():
        global SPLAddonState


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/e2937a59c658/
Changeset:   e2937a59c658
Branch:      None
User:        josephsl
Date:        2017-03-25 02:52:30+00:00
Summary:     Merged stable

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splupdate.py 
b/addon/appModules/splstudio/splupdate.py
index cc213bd..029c7a8 100755
--- a/addon/appModules/splstudio/splupdate.py
+++ b/addon/appModules/splstudio/splupdate.py
@@ -54,7 +54,7 @@ def initialize():
                if _updateNow: del SPLAddonState["pendingChannelChange"]
                if "UpdateChannel" in SPLAddonState:
                        SPLUpdateChannel = SPLAddonState["UpdateChannel"]
-                       if SPLUpdateChannel in ("beta", "preview", "lts"):
+                       if SPLUpdateChannel in ("beta", "prerelease", "lts"):
                                SPLUpdateChannel = "dev"
        except IOError, KeyError:
                SPLAddonState["PDT"] = 0


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/e0ec972eaa22/
Changeset:   e0ec972eaa22
Branch:      None
User:        josephsl
Date:        2017-03-25 02:55:20+00:00
Summary:     Present a dialog when switching to try (fast ring) build.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/splconfui.py 
b/addon/appModules/splstudio/splconfui.py
index 1c91197..f46f6b1 100755
--- a/addon/appModules/splstudio/splconfui.py
+++ b/addon/appModules/splstudio/splconfui.py
@@ -1351,6 +1351,17 @@ class AdvancedOptionsDialog(wx.Dialog):
                self.Center(wx.BOTH | wx.CENTER_ON_SCREEN)
 
        def onOk(self, evt):
+               # The try (fast ring) builds aren't for the faint of heart.
+               if len(self._updateChannels) > 1:
+                       channel = 
self._updateChannels[self.channels.GetSelection()]
+                       if channel == "try" and gui.messageBox(
+                               # Translators: The confirmation prompt 
displayed when changing to the fastest development channel (with risks 
involved).
+                               _("You are about to switch to the fastest and 
most unstable development channel. Please note that the selected channel may 
come with updates that might be unstable at times and should be used for 
testing and sending feedback to the add-on developer. If you prefer to use 
stable rleases, please answer no and switch to a more stable update channel. 
Are you sure you wish to switch to the fastest development channel?"),
+                               # Translators: The title of the channel switch 
confirmation dialog.
+                               _("Switching to unstable channel"),
+                               wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION, 
self
+                       ) == wx.NO:
+                               return
                parent = self.Parent
                parent.splConPassthrough = self.splConPassthroughCheckbox.Value
                parent.compLayer = 
self.compatibilityLayouts[self.compatibilityList.GetSelection()][0]


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/1fc6c8a1befe/
Changeset:   1fc6c8a1befe
Branch:      None
User:        josephsl
Date:        2017-03-25 16:04:11+00:00
Summary:     Requests window (17.2-dev): beep when requests window shows up. re 
#25.

Requests window (TRequests) fires a show event. Thanks to event request 
feature, it is possible to detect this from anywhere. Thus allow NVDA to react 
to this by playing a beep.

Affected #:  1 file

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index e7c4629..ebb85c7 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -583,6 +583,8 @@ class AppModule(appModuleHandler.AppModule):
                if hasattr(eventHandler, "requestEvents"):
                        eventHandler.requestEvents(eventName="nameChange", 
processId=self.processID, windowClassName="TStatusBar")
                        eventHandler.requestEvents(eventName="nameChange", 
processId=self.processID, windowClassName="TStaticText")
+                       # Also for requests window.
+                       eventHandler.requestEvents(eventName="show", 
processId=self.processID, windowClassName="TRequests")
                        self.backgroundStatusMonitor = True
                else:
                        self.backgroundStatusMonitor = False
@@ -856,6 +858,13 @@ class AppModule(appModuleHandler.AppModule):
                        except KeyError:
                                pass
 
+       # React to show events from certain windows.
+
+       def event_show(self, obj, nextHandler):
+               if obj.windowClassName == "TRequests":
+                       tones.beep(400, 100)
+               nextHandler()
+
        # Save configuration when terminating.
        def terminate(self):
                super(AppModule, self).terminate()


https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/78b40da4ae7c/
Changeset:   78b40da4ae7c
Branch:      master
User:        josephsl
Date:        2017-04-02 02:28:14+00:00
Summary:     Requests: use a wave file to announce requests popup.

A wave file from BrailleNote mPower will be employed to alert the broadcaster 
of a request, a bit better than using a pure tone.

Affected #:  2 files

diff --git a/addon/appModules/splstudio/SPL_Requests.wav 
b/addon/appModules/splstudio/SPL_Requests.wav
new file mode 100755
index 0000000..ca36690
Binary files /dev/null and b/addon/appModules/splstudio/SPL_Requests.wav differ

diff --git a/addon/appModules/splstudio/__init__.py 
b/addon/appModules/splstudio/__init__.py
index ebb85c7..0ed07db 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -862,7 +862,7 @@ class AppModule(appModuleHandler.AppModule):
 
        def event_show(self, obj, nextHandler):
                if obj.windowClassName == "TRequests":
-                       tones.beep(400, 100)
+                       
nvwave.playWaveFile(os.path.join(os.path.dirname(__file__), "SPL_Requests.wav"))
                nextHandler()
 
        # Save configuration when terminating.

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:

  • » commit/StationPlaylist: 12 new changesets - commits-noreply