commit/StationPlaylist: josephsl: Merge branch 'master' into 6.0/categoricalSearch

  • From: commits-noreply@xxxxxxxxxxxxx
  • To: nvda-addons-commits@xxxxxxxxxxxxx
  • Date: Fri, 26 Jun 2015 18:10:45 -0000

1 new commit in StationPlaylist:

https://bitbucket.org/nvdaaddonteam/stationplaylist/commits/944c0f08a6db/
Changeset: 944c0f08a6db
Branch: 6.0/categoricalSearch
User: josephsl
Date: 2015-06-26 18:09:52+00:00
Summary: Merge branch 'master' into 6.0/categoricalSearch

Affected #: 6 files

diff --git a/addon/appModules/splstudio/__init__.py
b/addon/appModules/splstudio/__init__.py
index 8731e30..bac298c 100755
--- a/addon/appModules/splstudio/__init__.py
+++ b/addon/appModules/splstudio/__init__.py
@@ -308,6 +308,22 @@ class AppModule(appModuleHandler.AppModule):
# These items are static text items whose name changes.
# Note: There are two status bars, hence the need to exclude Up time so
it doesn't announce every minute.
# Unfortunately, Window handles and WindowControlIDs seem to change, so
can't be used.
+ # Only announce changes if told to do so via the following function.
+ def _TStatusBarChanged(self, obj):
+ name = obj.name
+ if name.startswith(" Up time:"):
+ return False
+ elif name.startswith("Scheduled for"):
+ if self.scheduledTimeCache == name: return False
+ self.scheduledTimeCache = name
+ return splconfig.SPLConfig["SayScheduledFor"]
+ elif "Listener" in name:
+ return splconfig.SPLConfig["SayListenerCount"]
+ elif name.startswith("Cart") and obj.IAccessibleChildID == 3:
+ return splconfig.SPLConfig["SayPlayingCartName"]
+ return True
+
+ # Now the actual event.
def event_nameChange(self, obj, nextHandler):
# Do not let NvDA get name for None object when SPL window is
maximized.
if not obj.name:
@@ -315,7 +331,8 @@ class AppModule(appModuleHandler.AppModule):
# Suppress announcing status messages when background
monitoring is unavailable.
if api.getForegroundObject().processID != self.processID and
not self.backgroundStatusMonitor:
nextHandler()
- if obj.windowClassName == "TStatusBar" and not
obj.name.startswith(" Up time:"):
+ # Only announce changes in status bar objects when told to do
so.
+ if obj.windowClassName == "TStatusBar" and
self._TStatusBarChanged(obj):
# Special handling for Play Status
if obj.IAccessibleChildID == 1:
if "Play status" in obj.name:
@@ -341,23 +358,7 @@ class AppModule(appModuleHandler.AppModule):
if self.libraryScanning:
self.libraryScanning = False
self.scanCount = 0
else:
- if obj.name.startswith("Scheduled for"):
- if not
splconfig.SPLConfig["SayScheduledFor"]:
- nextHandler()
- return
- if self.scheduledTimeCache == obj.name:
return
- else:
- self.scheduledTimeCache =
obj.name
- ui.message(obj.name)
- return
- elif "Listener" in obj.name and not
splconfig.SPLConfig["SayListenerCount"]:
- nextHandler()
- return
- elif not obj.name.endswith((" On", " Off")) or
(obj.name.startswith("Cart") and obj.IAccessibleChildID == 3):
- # Announce status information that does
not contain toggle messages and return immediately.
- ui.message(obj.name)
- return
- elif splconfig.SPLConfig["BeepAnnounce"]:
+ if obj.name.endswith((" On", " Off")) and
splconfig.SPLConfig["BeepAnnounce"]:
# User wishes to hear beeps instead of
words. The beeps are power on and off sounds from PAC Mate Omni.
beep = obj.name.split()
stat = beep[-1]
@@ -502,20 +503,23 @@ class AppModule(appModuleHandler.AppModule):
}

# Specific to time scripts using Studio API.
+ # 6.0: Split this into two functions: the announcer (below) and
formatter.
def announceTime(self, t, offset = None):
if t <= 0:
ui.message("00:00")
else:
+ ui.message(self._ms2time(t, offset = offset))
+
+ # Formatter: given time in milliseconds, convert it to human-readable
format.
+ def _ms2time(self, t, offset = None):
+ if t <= 0:
+ return "00:00"
+ else:
tm = (t/1000) if not offset else (t/1000)+offset
- if tm < 60:
- tm1, tm2 = "00", tm
- else:
- tm1, tm2 = divmod(tm, 60)
- if tm1 < 10:
- tm1 = "0" + str(tm1)
- if tm2 < 10:
- tm2 = "0" + str(tm2)
- ui.message("{a}:{b}".format(a = tm1, b = tm2))
+ timeComponents = divmod(tm, 60)
+ tm1 = str(timeComponents[0]).zfill(2)
+ tm2 = str(timeComponents[1]).zfill(2)
+ return ":".join([tm1, tm2])

# Scripts which rely on API.
def script_sayRemainingTime(self, gesture):
@@ -1228,6 +1232,46 @@ class AppModule(appModuleHandler.AppModule):
# Translators: Presented when library scan is already
in progress.
ui.message(_("Scanning is in progress"))

+ # Track time analysis: return total length of the selected tracks upon
request.
+ # Analysis command will be assignable.
+ _analysisMarker = None
+
+ def script_markTrackForAnalysis(self, gesture):
+ focus = api.getFocusObject()
+ if focus.role == controlTypes.ROLE_LIST:
+ ui.message("No tracks were added, cannot perform track
time analysis")
+ return
+ if scriptHandler.getLastScriptRepeatCount() == 0:
+ self._analysisMarker = focus.IAccessibleChildID-1
+ ui.message("Track time analysis activated")
+ else:
+ self._analysisMarker = None
+ ui.message("Track time analysis deactivated")
+ script_markTrackForAnalysis.__doc__=_("Marks focused track as start
marker for track time analysis")
+
+ def script_trackTimeAnalysis(self, gesture):
+ focus = api.getFocusObject()
+ if focus.role == controlTypes.ROLE_LIST:
+ ui.message("No tracks were added, cannot perform track
time analysis")
+ return
+ if self._analysisMarker is None:
+ ui.message("No track selected as start of analysis
marker, cannot perform time analysis")
+ return
+ trackPos = focus.IAccessibleChildID-1
+ if self._analysisMarker == trackPos:
+ filename = statusAPI(self._analysisMarker, 211,
ret=True)
+ statusAPI(filename, 30, func=self.announceTime)
+ else:
+ analysisBegin = min(self._analysisMarker, trackPos)
+ analysisEnd = max(self._analysisMarker, trackPos)
+ analysisRange = analysisEnd-analysisBegin+1
+ totalLength = 0
+ for track in xrange(analysisBegin, analysisEnd+1):
+ filename = statusAPI(track, 211, ret=True)
+ totalLength+=statusAPI(filename, 30, ret=True)
+ ui.message("Tracks: {numberOfSelectedTracks}, totaling
{totalTime}".format(numberOfSelectedTracks = analysisRange, totalTime =
self._ms2time(totalLength)))
+ script_trackTimeAnalysis.__doc__=_("Announces total length of tracks
between analysis start marker and the current track")
+
def script_layerHelp(self, gesture):
# Translators: The title for SPL Assistant help dialog.
wx.CallAfter(gui.messageBox, SPLAssistantHelp, _("SPL Assistant
help"))
@@ -1251,6 +1295,8 @@ class AppModule(appModuleHandler.AppModule):
"kb:s":"sayScheduledTime",
"kb:shift+p":"sayTrackPitch",
"kb:shift+r":"libraryScanMonitor",
+ "kb:f9":"markTrackForAnalysis",
+ "kb:f10":"trackTimeAnalysis",
"kb:f1":"layerHelp",
}


diff --git a/addon/appModules/splstudio/splconfig.py
b/addon/appModules/splstudio/splconfig.py
index 7c9dbb2..fee1610 100755
--- a/addon/appModules/splstudio/splconfig.py
+++ b/addon/appModules/splstudio/splconfig.py
@@ -10,12 +10,14 @@ from validate import Validator
import weakref
import globalVars
import ui
+import api
import gui
import wx
from winUser import user32

# Configuration management
SPLIni = os.path.join(globalVars.appArgs.configPath, "splstudio.ini")
+SPLProfiles = os.path.join(globalVars.appArgs.configPath, "addons",
"stationPlaylist", "profiles")
confspec = ConfigObj(StringIO("""
BeepAnnounce = boolean(default=false)
SayEndOfTrack = boolean(default=true)
@@ -28,9 +30,12 @@ LibraryScanAnnounce = option("off", "ending", "progress",
"numbers", default="of
TrackDial = boolean(default=false)
SayScheduledFor = boolean(default=true)
SayListenerCount = boolean(default=true)
+SayPlayingCartName = boolean(default=true)
"""), encoding="UTF-8", list_values=False)
confspec.newlines = "\r\n"
SPLConfig = None
+# A pool of broadcast profiles.
+SPLConfigPool = []

# List of values to be converted manually.
# This will be called only once: when upgrading from prior versions to 5.0, to
be removed in 5.1.
@@ -78,17 +83,44 @@ def resetConfig(defaults, activeConfig, intentional=False):
# Translators: Title of the reset config dialog.
_("Reset configuration"), wx.OK|wx.ICON_INFORMATION)

+# In case one or more profiles had config issues, look up the error message
form the following map.
+_configErrors = (
+ ("All settings reset to defaults"),
+ ("Some settings reset to defaults")
+)
+
# To be run in app module constructor.
# With the load function below, load the config upon request.
# 6.0: The below init function is really a vehicle that traverses through
config profiles in a loop.
+# Prompt the config error dialog only once.
+_configLoadStatus = {} # Key = filename, value is pass or no pass.
+
def initConfig():
- # Load the default config.
- # Todo (6.0: go through the config beltway, loading each config along
the way.
- global SPLConfig
- SPLConfig = unlockConfig(SPLIni)
+ # Load the default config from a list of profiles.
+ global SPLConfig, SPLConfigPool, _configLoadStatus
+ if SPLConfigPool is None: SPLConfigPool = []
+ SPLConfigPool.append(unlockConfig(SPLIni, profileName="Normal profile"))
+ try:
+ profiles = filter(lambda fn: os.path.splitext(fn)[-1] ==
".ini", os.listdir(SPLProfiles))
+ for profile in profiles:
+
SPLConfigPool.append(unlockConfig(os.path.join(SPLProfiles, profile),
profileName=os.path.splitext(profile)[0]))
+ except WindowsError:
+ pass
+ SPLConfig = SPLConfigPool[0]
+ if len(_configLoadStatus):
+ # Translators: Standard error title for configuration error.
+ title = _("Studio add-on Configuration error")
+ messages = []
+ messages.append("One or more broadcast profiles had
issues:\n\n")
+ for profile in _configLoadStatus:
+ error = _configErrors[_configLoadStatus[profile]]
+ messages.append("{profileName}:
{errorMessage}".format(profileName = profile, errorMessage = error))
+ _configLoadStatus.clear()
+ runConfigErrorDialog("\n".join(messages), title)

# 6.0: Unlock (load) profiles from files.
-def unlockConfig(path):
+def unlockConfig(path, profileName=None):
+ global _configLoadStatus # To be mutated only during unlock routine.
SPLConfigCheckpoint = ConfigObj(path, configspec = confspec,
encoding="UTF-8")
# 5.0 only: migrate 4.x format to 5.0, to be removed in 5.1.
migrated = config4to5(SPLConfigCheckpoint)
@@ -99,35 +131,29 @@ def unlockConfig(path):
# Hack: have a dummy config obj handy just for storing default
values.
SPLDefaults = ConfigObj(None, configspec = confspec,
encoding="UTF-8")
SPLDefaults.validate(val, copy=True)
- # Translators: Standard error title for configuration error.
- title = _("Studio add-on Configuration error")
if not configTest or not migrated:
# Case 1: restore settings to defaults.
# This may happen when 4.x config had parsing issues or
5.x config validation has failed on all values.
resetConfig(SPLDefaults, SPLConfigCheckpoint)
- # Translators: Standard dialog message when Studio
configuration has problems and was reset to defaults.
- errorMessage = _("Your Studio add-on configuration has
errors and was reset to factory defaults.")
+ _configLoadStatus[profileName] = 0
elif isinstance(configTest, dict):
# Case 2: For 5.x and later, attempt to reconstruct the
failed values.
for setting in configTest:
if not configTest[setting]:
SPLConfigCheckpoint[setting] =
SPLDefaults[setting]
- # Translators: Standard dialog message when some Studio
configuration settings were reset to defaults.
- errorMessage = _("Errors were found in some of your
Studio configuration settings. The affected settings were reset to defaults.")
SPLConfigCheckpoint.write()
- try:
- runConfigErrorDialog(errorMessage, title)
- except AttributeError:
- pass
+ _configLoadStatus[profileName] = 1
+ SPLConfigCheckpoint.name = profileName
return SPLConfigCheckpoint

# Save configuration database.
def saveConfig():
- # 5.0: Save the one and only SPL config database.
- # Todo for 6.0: save all config profiles.
- global SPLConfig
- if SPLConfig is not None: SPLConfig.write()
+ # Save all config profiles.
+ global SPLConfig, SPLConfigPool
+ for configuration in SPLConfigPool:
+ if configuration is not None: configuration.write()
SPLConfig = None
+ SPLConfigPool = None


# Configuration dialog.
@@ -137,7 +163,44 @@ class SPLConfigDialog(gui.SettingsDialog):

def makeSettings(self, settingsSizer):

- # Translators: the label for a setting in SPL add-on settings
to set status announcement between words and beeps.
+ # Broadcast profile controls were inspired by Config Profiles
dialog in NVDA Core.
+ sizer = wx.BoxSizer(wx.HORIZONTAL)
+ # Translators: The label for a setting in SPL add-on dialog to
select a broadcast profile.
+ label = wx.StaticText(self, wx.ID_ANY, label=_("Broadcast
&profile:"))
+ self.profiles = wx.Choice(self, wx.ID_ANY,
choices=[profile.name for profile in SPLConfigPool])
+ self.profiles.Bind(wx.EVT_CHOICE, self.onProfileSelection)
+ try:
+
self.profiles.SetSelection(SPLConfigPool.index(SPLConfig))
+ except:
+ pass
+ sizer.Add(label)
+ sizer.Add(self.profiles)
+ settingsSizer.Add(sizer, border=10, flag=wx.BOTTOM)
+
+ # Profile controls code credit: NV Access (except copy button).
+ sizer = wx.BoxSizer(wx.HORIZONTAL)
+ # Translators: The label of a button to create a new broadcast
profile.
+ item = newButton = wx.Button(self, label=_("&New"))
+ item.Bind(wx.EVT_BUTTON, self.onNew)
+ sizer.Add(item)
+ # Translators: The label of a button to copy a broadcast
profile.
+ item = copyButton = wx.Button(self, label=_("Cop&y"))
+ item.Bind(wx.EVT_BUTTON, self.onCopy)
+ sizer.Add(item)
+ # Translators: The label of a button to rename a broadcast
profile.
+ item = self.renameButton = wx.Button(self, label=_("&Rename"))
+ item.Bind(wx.EVT_BUTTON, self.onRename)
+ sizer.Add(item)
+ # Translators: The label of a button to delete a broadcast
profile.
+ item = self.deleteButton = wx.Button(self, label=_("&Delete"))
+ item.Bind(wx.EVT_BUTTON, self.onDelete)
+ sizer.Add(item)
+ if SPLConfigPool.index(SPLConfig) == 0:
+ self.renameButton.Disable()
+ self.deleteButton.Disable()
+ settingsSizer.Add(sizer)
+
+ # Translators: the label for a setting in SPL add-on settings to set
status announcement between words and beeps.

self.beepAnnounceCheckbox=wx.CheckBox(self,wx.NewId(),label=_("&Beep for status
announcements"))
self.beepAnnounceCheckbox.SetValue(SPLConfig["BeepAnnounce"])
settingsSizer.Add(self.beepAnnounceCheckbox,
border=10,flag=wx.TOP)
@@ -241,13 +304,18 @@ class SPLConfigDialog(gui.SettingsDialog):

self.listenerCountCheckbox.SetValue(SPLConfig["SayListenerCount"])
sizer.Add(self.listenerCountCheckbox, border=10,flag=wx.BOTTOM)

+ # Translators: the label for a setting in SPL add-on settings
to announce currently playing cart.
+
self.cartNameCheckbox=wx.CheckBox(self,wx.NewId(),label=_("&Announce name of
the currently playing cart"))
+ self.cartNameCheckbox.SetValue(SPLConfig["SayPlayingCartName"])
+ sizer.Add(self.listenerCountCheckbox, border=10,flag=wx.BOTTOM)
+
# Translators: The label for a button in SPL add-on
configuration dialog to reset settings to defaults.
self.resetConfigButton = wx.Button(self, wx.ID_ANY,
label=_("Reset settings"))
self.resetConfigButton.Bind(wx.EVT_BUTTON,self.onResetConfig)
sizer.Add(self.resetConfigButton)

def postInit(self):
- self.beepAnnounceCheckbox.SetFocus()
+ self.profiles.SetFocus()

def onOk(self, evt):
if not self.micAlarm.Value.isdigit():
@@ -258,6 +326,8 @@ class SPLConfigDialog(gui.SettingsDialog):
_("Error"), wx.OK|wx.ICON_ERROR,self)
self.micAlarm.SetFocus()
return
+ global SPLConfig
+ SPLConfig = SPLConfigPool[self.profiles.GetSelection()]
SPLConfig["BeepAnnounce"] = self.beepAnnounceCheckbox.Value
SPLConfig["SayEndOfTrack"] = self.outroCheckBox.Value
SPLConfig["EndOfTrackTime"] = self.endOfTrackAlarm.Value
@@ -269,6 +339,7 @@ class SPLConfigDialog(gui.SettingsDialog):
SPLConfig["TrackDial"] = self.trackDialCheckbox.Value
SPLConfig["SayScheduledFor"] = self.scheduledForCheckbox.Value
SPLConfig["SayListenerCount"] = self.listenerCountCheckbox.Value
+ SPLConfig["SayPlayingCartName"] = self.cartNameCheckbox.Value
super(SPLConfigDialog, self).onOk(evt)

# Check events for outro and intro alarms, respectively.
@@ -290,7 +361,90 @@ class SPLConfigDialog(gui.SettingsDialog):
self.introSizer.Show(self.songRampAlarm)
self.Fit()

+ # Load settings from profiles.
+ def onProfileSelection(self, evt):
+ import tones
+ tones.beep(500, 100)
+ # Don't rely on SPLConfig here, as we don't want to interupt
the show.
+ selection = self.profiles.GetSelection()
+ if selection == 0:
+ self.renameButton.Disable()
+ self.deleteButton.Disable()
+ else:
+ self.renameButton.Enable()
+ self.deleteButton.Enable()
+ selectedProfile = SPLConfigPool[selection]
+
self.beepAnnounceCheckbox.SetValue(selectedProfile["BeepAnnounce"])
+ self.outroCheckBox.SetValue(selectedProfile["SayEndOfTrack"])
+
self.endOfTrackAlarm.SetValue(long(selectedProfile["EndOfTrackTime"]))
+ self.onOutroCheck(None)
+ self.introCheckBox.SetValue(selectedProfile["SaySongRamp"])
+
self.songRampAlarm.SetValue(long(selectedProfile["SongRampTime"]))
+ self.onIntroCheck(None)
+ self.micAlarm.SetValue(str(selectedProfile["MicAlarm"]))
+
+ # Profile controls.
+ # Rename and delete events come from GUI/config profiles dialog from
NVDA core.
+ def onNew(self, evt):
+ self.Disable()
+ NewProfileDialog(self).Show()
+
+ def onCopy(self, evt):
+ self.Disable()
+ NewProfileDialog(self, copy=True).Show()
+
+ def onRename(self, evt):
+ global SPLConfigPool
+ index = self.profiles.Selection
+ oldName = SPLConfigPool[index].name
+ # Translators: The label of a field to enter a new name for a
broadcast profile.
+ with wx.TextEntryDialog(self, _("New name:"),
+ # Translators: The title of the dialog to
rename a profile.
+ _("Rename Profile"), defaultValue=oldName) as d:
+ if d.ShowModal() == wx.ID_CANCEL:
+ return
+ newName = api.filterFileName(d.Value)
+ if oldName == newName: return
+ newNamePath = newName + ".ini"
+ newProfile = os.path.join(SPLProfiles, newNamePath)
+ if oldName.lower() != newName.lower() and
os.path.isfile(newProfile):
+ # Translators: An error displayed when renaming a
configuration profile
+ # and a profile with the new name already exists.
+ gui.messageBox(_("That profile already exists. Please
choose a different name."),
+ _("Error"), wx.OK | wx.ICON_ERROR, self)
+ return
+ oldNamePath = oldName + ".ini"
+ oldProfile = os.path.join(SPLProfiles, oldNamePath)
+ os.rename(oldProfile, newProfile)
+ SPLConfigPool[index].name = newName
+ SPLConfigPool[index].filename = newProfile
+ self.profiles.SetString(index, newName)
+ self.profiles.Selection = index
+ self.profiles.SetFocus()
+
+ def onDelete(self, evt):
+ index = self.profiles.Selection
+ if gui.messageBox(
+ # Translators: The confirmation prompt displayed when
the user requests to delete a broadcast profile.
+ _("Are you sure you want to delete this profile? This
cannot be undone."),
+ # Translators: The title of the confirmation dialog for
deletion of a profile.
+ _("Confirm Deletion"),
+ wx.YES | wx.NO | wx.ICON_QUESTION, self
+ ) == wx.NO:
+ return
+ global SPLConfigPool
+ name = SPLConfigPool[index].name
+ path = SPLConfigPool[index].filename
+ del SPLConfigPool[index]
+ os.remove(path)
+ self.profiles.Delete(index)
+ self.profiles.SetString(0, SPLConfigPool[0].name)
+ self.profiles.Selection = 0
+ self.onProfileSelection(None)
+ self.profiles.SetFocus()
+
# Reset settings to defaults.
+ # This affects the currently selected profile.
def onResetConfig(self, evt):
if gui.messageBox(
# Translators: A message to warn about resetting SPL config
settings to factory defaults.
@@ -301,6 +455,9 @@ class SPLConfigDialog(gui.SettingsDialog):
val = Validator()
SPLDefaults = ConfigObj(None, configspec = confspec,
encoding="UTF-8")
SPLDefaults.validate(val, copy=True)
+ # Reset the selected config only.
+ global SPLConfig
+ SPLConfig = SPLConfigPool[self.profiles.GetSelection()]
resetConfig(SPLDefaults, SPLConfig, intentional=True)
self.Destroy()

@@ -309,7 +466,101 @@ class SPLConfigDialog(gui.SettingsDialog):
def onConfigDialog(evt):
gui.mainFrame._popupSettingsDialog(SPLConfigDialog)

-# Additional configuration dialogs
+# Helper dialogs for add-on settings dialog.
+
+# New broadcast profile dialog: Modification of new config profile dialog from
NvDA Core.
+class NewProfileDialog(wx.Dialog):
+
+ def __init__(self, parent, copy=False):
+ self.copy = copy
+ if not self.copy:
+ # Translators: The title of the dialog to create a new
broadcast profile.
+ dialogTitle = _("New Profile")
+ else:
+ # Translators: The title of the dialog to copy a
broadcast profile.
+ dialogTitle = _("Copy Profile")
+ super(NewProfileDialog, self).__init__(parent,
title=dialogTitle)
+ mainSizer = wx.BoxSizer(wx.VERTICAL)
+
+ sizer = wx.BoxSizer(wx.HORIZONTAL)
+ # Translators: The label of a field to enter the name of a new
broadcast profile.
+ sizer.Add(wx.StaticText(self, label=_("Profile name:")))
+ item = self.profileName = wx.TextCtrl(self)
+ sizer.Add(item)
+ mainSizer.Add(sizer)
+
+ sizer = wx.BoxSizer(wx.HORIZONTAL)
+ # Translators: The label for a setting in SPL add-on dialog to
select a base profile for copying.
+ label = wx.StaticText(self, wx.ID_ANY, label=_("&Base
profile:"))
+ self.baseProfiles = wx.Choice(self, wx.ID_ANY,
choices=[profile.name for profile in SPLConfigPool])
+ try:
+
self.baseProfiles.SetSelection(SPLConfigPool.index(SPLConfig))
+ except:
+ pass
+ sizer.Add(label)
+ sizer.Add(self.baseProfiles)
+ if not self.copy:
+ sizer.Hide(label)
+ sizer.Hide(self.baseProfiles)
+ mainSizer.Add(sizer, border=10, flag=wx.BOTTOM)
+
+ mainSizer.Add(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.Sizer = mainSizer
+ self.profileName.SetFocus()
+ self.Center(wx.BOTH | wx.CENTER_ON_SCREEN)
+
+ def onOk(self, evt):
+ global SPLConfigPool
+ profileNames = [profile.name for profile in SPLConfigPool]
+ name = api.filterFileName(self.profileName.Value)
+ if not name:
+ return
+ if name in profileNames:
+ # Translators: An error displayed when the user
attempts to create a profile which already exists.
+ gui.messageBox(_("That profile already exists. Please
choose a different name."),
+ _("Error"), wx.OK | wx.ICON_ERROR, self)
+ return
+ namePath = name + ".ini"
+ if not os.path.exists(SPLProfiles):
+ os.mkdir(SPLProfiles)
+ newProfile = os.path.join(SPLProfiles, namePath)
+ if self.copy:
+ import shutil
+ baseProfile =
SPLConfigPool[self.baseProfiles.GetSelection()]
+ shutil.copy2(baseProfile.filename, newProfile)
+ SPLConfigPool.append(unlockConfig(newProfile, profileName=name))
+ parent = self.Parent
+ parent.profiles.Append(name)
+ parent.profiles.Selection = parent.profiles.Count - 1
+ parent.onProfileSelection(None)
+ parent.profiles.SetFocus()
+ parent.Enable()
+ self.Destroy()
+ return
+
+ def onCancel(self, evt):
+ self.Parent.Enable()
+ self.Destroy()
+
+ """def onTriggerChoice(self, evt):
+ spec, disp, manualEdit =
self.triggers[self.triggerChoice.Selection]
+ if not spec:
+ # Manual activation shouldn't guess a name.
+ name = ""
+ elif spec.startswith("app:"):
+ name = spec[4:]
+ else:
+ name = disp
+ if self.profileName.Value == self.autoProfileName:
+ # The user hasn't changed the automatically filled
value.
+ self.profileName.Value = name
+ self.profileName.SelectAll()
+ self.autoProfileName = name"""
+
+ # Additional configuration dialogs

# A common alarm dialog
# Based on NVDA core's find dialog code (implemented by the author of this
add-on).

diff --git a/addon/appModules/tracktool.py b/addon/appModules/tracktool.py
index a5e55ed..0de2eb7 100755
--- a/addon/appModules/tracktool.py
+++ b/addon/appModules/tracktool.py
@@ -3,19 +3,19 @@
# Copyright 2014-2015 Joseph Lee and contributors, released under gPL.
# Functionality is based on JFW scripts for SPL Track Tool by Brian Hartgen.

-#import ctypes
+import ctypes
import appModuleHandler
import addonHandler
import api
import tones
-#import speech
-#import braille
+import speech
+import braille
from controlTypes import ROLE_LISTITEM
import ui
-#import winKernel
-#from winUser import sendMessage
-from NVDAObjects.IAccessible import IAccessible #, sysListView32 # Place
holder for add-on 5.1.
-#from splstudio import splconfig
+import winKernel
+from winUser import sendMessage
+from NVDAObjects.IAccessible import IAccessible, sysListView32
+from splstudio import splconfig
addonHandler.initTranslation()

# Track Tool allows a broadcaster to manage track intros, cues and so forth.
Each track is a list item with descriptions such as title, file name, intro
time and so forth.
@@ -33,8 +33,6 @@ class TrackToolItem(IAccessible):
tones.beep(550, 100)
super(TrackToolItem, self).reportFocus()

- # Incubating:
- """
def initOverlayClass(self):
if self.appModule.TTDial:
self.bindGesture("kb:rightArrow", "nextColumn")
@@ -43,6 +41,10 @@ class TrackToolItem(IAccessible):
# Add-on 5.1: Track Dial for Track Tool.

def script_toggleTrackDial(self, gesture):
+ if splconfig.SPLConfig is None:
+ # Translators: Presented when only Track Tool is
running (Track Dial requires Studio to be running as well).
+ ui.message(_("Only Track Tool is running, Track Dial is
unavailable"))
+ return
if not self.appModule.TTDial:
self.appModule.TTDial = True
self.bindGesture("kb:rightArrow", "nextColumn")
@@ -124,17 +126,13 @@ class TrackToolItem(IAccessible):

__gestures={
#"kb:control+`":"toggleTrackDial",
- }"""
- # To be enabled in 5.1 after optimizing the app module.
+ }


class AppModule(appModuleHandler.AppModule):

- #def terminate(self):
- #super(AppModule, self).terminate()
-
- #TTDial = False
- #SPLColNumber = 0
+ TTDial = False
+ SPLColNumber = 0

def chooseNVDAObjectOverlayClasses(self, obj, clsList):
if obj.windowClassName in ("TListView",
"TTntListView.UnicodeClass") and obj.role == ROLE_LISTITEM:

diff --git a/addon/installTasks.py b/addon/installTasks.py
new file mode 100755
index 0000000..d13e4af
--- /dev/null
+++ b/addon/installTasks.py
@@ -0,0 +1,20 @@
+# StationPlaylist Studio add-on installation tasks
+# Copyright 2015 Joseph Lee and others, released under GPL.
+
+# Provides needed routines during add-on installation and removal.
+# Routines are partly based on other add-ons, particularly Place Markers by
Noelia Martinez (thanks add-on authors).
+
+import addonHandler
+import os
+import shutil
+import globalVars
+
+def onInstall():
+ profiles = os.path.join(os.path.dirname(__file__), "..",
"stationPlaylist", "profiles")
+ # Import old profiles.
+ if os.path.exists(profiles):
+ newProfiles = os.path.join(os.path.dirname(__file__),
"profiles")
+ try:
+ shutil.copytree(profiles, newProfiles)
+ except IOError:
+ pass

diff --git a/buildVars.py b/buildVars.py
index 649a810..47dbea9 100755
--- a/buildVars.py
+++ b/buildVars.py
@@ -20,7 +20,7 @@ addon_info = {
"addon_description" : _("""Enhances support for Station Playlist Studio.
In addition, adds global commands for the studio from everywhere."""),
# version
- "addon_version" : "5.0-dev",
+ "addon_version" : "6.0-dev",
# Author(s)
"addon_author" : u"Geoff Shang, Joseph Lee and other contributors",
# URL for the add-on documentation support
@@ -34,7 +34,8 @@ import os.path

# Define the python files that are the sources of your add-on.
# You can use glob expressions here, they will be expanded.
-pythonSources = [os.path.join("addon", "appModules", "*.py"),
+pythonSources = [os.path.join("addon", "*.py"),
+os.path.join("addon", "appModules", "*.py"),
os.path.join("addon", "appModules", "splstudio", "*.py"),
os.path.join("addon", "globalPlugins", "SPLStudioUtils", "*.py")]


diff --git a/readme.md b/readme.md
index c313286..3bb9c86 100755
--- a/readme.md
+++ b/readme.md
@@ -36,6 +36,8 @@ The following commands are not assigned by default; if you
wish to assign it, us
* Toggling track dial on or off (works properly while a track is focused; to
assign a command to this, select a track, then open NVDA's input gestures
dialog.).
* Announcing temperature.
* Announcing title of next track if scheduled.
+* Marking current track for start of track time analysis.
+* Performing track time analysis.

Note: Input Gestures dialog is available in 2013.3 or later.

@@ -74,6 +76,8 @@ The available status information are:
* U: Studio up time.
* W: Weather and temperature if configured.
* Y: Playlist modified status (SPL 5.00 and later).
+* F9: Mark current track for track time analysis.
+* F10: Perform track time analysis.
* F1: Layer help.

## SPL Controller
@@ -119,16 +123,25 @@ To learn cart assignments, from SPL Studio, press
Control+NVDA+3. Pressing the c

## Track Dial

-You can use arrow keys to review various information about a track. To turn
Track Dial on, while a track is focused in the main playlist viewer, press
Control+Grave. Then use left and right arrow keys to review information such as
artist, duration and so on.
+You can use arrow keys to review various information about a track. To turn
Track Dial on, while a track is focused in the main playlist viewer, press the
command you assigned for toggling Track Dial. Then use left and right arrow
keys to review information such as artist, duration and so on.
+
+## Track time analysis
+
+To obtain length to play selected tracks, mark current track for start of
track time analysis (SPL Assistant, F9), then press SPL Assistant, F10 when
reaching end of selection.

## Configuration dialog

-From studio window, you can press Control+NVDA+0 to open the add-on
configuration dialog. Alternatively, go to NVDA's preferences menu and select
SPL Studio Settings item.
+From studio window, you can press Control+NVDA+0 to open the add-on
configuration dialog. Alternatively, go to NVDA's preferences menu and select
SPL Studio Settings item. This dialog is also used to manage broadcast profiles.

## SPL touch mode

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.

+## Changes for 6.0-dev
+
+* You can now ask NVDA to apply certain settings in specific situations by
defining broadcast profiles. See the add-on guide for details on broadcast
profiles.
+* You can now ask NVDA to report total length of a range of tracks via track
time analysis feature. Press SPL Assistant, F9 to mark current track as start
marker, move to end of track range and press SPL Assistant, F10. These commands
can be reassigned so one doesn't have to invoke SPL Assistant layer to perform
track time analysis.
+
## Changes for 5.0

* A dedicated settings dialog for SPL add-on has been added, accessible from
NVDA's preferences menu or by pressing Control+NVDA+0 from SPL window.
@@ -269,5 +282,3 @@ Version 4.0 supports SPL Studio 5.00 and later, with 3.x
designed to provide som
[2]: http://addons.nvda-project.org/files/get.php?file=spl-dev

[3]: https://bitbucket.org/nvdaaddonteam/stationplaylist/wiki/SPLAddonGuide
-
-[4]: https://bitbucket.org/nvdaaddonteam/stationplaylist/wiki/DownloadLegacy

Repository URL: https://bitbucket.org/nvdaaddonteam/stationplaylist/

--

This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.

Other related posts:

  • » commit/StationPlaylist: josephsl: Merge branch 'master' into 6.0/categoricalSearch - commits-noreply