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.