changeset 214:38c1ccfb6820

keyboardcomposer works
author drewp@bigasterisk.com
date Mon, 11 Apr 2005 02:21:26 +0000
parents 87caa8906582
children 2072a0dd7b19
files Widgets/Fadable.py Widgets/FlyingFader.py bin/keyboardcomposer flax/KeyboardComposer.py light9/Fadable.py light9/FlyingFader.py light9/Submaster.py
diffstat 7 files changed, 635 insertions(+), 622 deletions(-) [+]
line wrap: on
line diff
--- a/Widgets/Fadable.py	Mon Apr 11 01:55:37 2005 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,150 +0,0 @@
-# taken from SnackMix -- now that's reusable code
-from Tix import *
-import time
-
-class Fadable:
-    """Fading mixin: must mix in with a Tk widget (or something that has
-    'after' at least) This is currently used by VolumeBox and MixerTk.
-    It's probably too specialized to be used elsewhere, but could possibly
-    work with an Entry or a Meter, I guess.  (Actually, this is used by
-    KeyboardComposer and KeyboardRecorder now too.)
-
-    var is a Tk variable that should be used to set and get the levels.
-    If use_fades is true, it will use fades to move between levels.
-    If key_bindings is true, it will install these keybindings:
-
-    Press a number to fade to that amount (e.g. '5' = 50%).  Also,
-    '`' (grave) will fade to 0 and '0' will fade to 100%.
-
-    If mouse_bindings is true, the following mouse bindings will be
-    installed: Right clicking toggles muting.  The mouse wheel will
-    raise or lower the volume.  Shift-mouse wheeling will cause a more
-    precise volume adjustment.  Control-mouse wheeling will cause a
-    longer fade."""
-    def __init__(self, var, wheel_step=5, use_fades=1, key_bindings=1,
-                 mouse_bindings=1):
-        self.use_fades = use_fades # whether increase and decrease should fade
-        self.wheel_step = wheel_step # amount that increase and descrease should
-                                     # change volume (by default)
-        
-        self.fade_start_level = 0
-        self.fade_end_level = 0
-        self.fade_start_time = 0
-        self.fade_length = 1
-        self.fade_step_time = 10
-        self.fade_var = var
-        self.fading = 0 # whether a fade is in progress
-
-        if key_bindings:
-            for k in range(1, 10):
-                self.bind("<Key-%d>" % k,
-                    lambda evt, k=k: self.fade(k / 10.0))
-            self.bind("<Key-0>", lambda evt: self.fade(1.0))
-            self.bind("<grave>", lambda evt: self.fade(0))
-
-            # up / down arrows
-            self.bind("<Key-Up>", lambda evt: self.increase())
-            self.bind("<Key-Down>", lambda evt: self.decrease())
-
-        if mouse_bindings:
-            # right mouse button toggles muting
-            # self.bind('<3>', lambda evt: self.toggle_mute())
-            # "NOT ANY MORE!" - homer
-
-            # mouse wheel
-            self.bind('<4>', lambda evt: self.increase())
-            self.bind('<5>', lambda evt: self.decrease())
-
-            # modified mouse wheel
-            self.bind('<Shift-4>', lambda evt: self.increase(multiplier=0.2))
-            self.bind('<Shift-5>', lambda evt: self.decrease(multiplier=0.2))
-            self.bind('<Control-4>', lambda evt: self.increase(length=1))
-            self.bind('<Control-5>', lambda evt: self.decrease(length=1))
-
-        self.last_level = 0 # used for muting
-    def fade(self, value, length=0.5, step_time=10):
-        """Fade to value in length seconds with steps every step_time
-        milliseconds"""
-        if length == 0: # 0 seconds fades happen right away and prevents
-                        # and prevents us from entering the fade loop,
-                        # which would cause a divide by zero
-            self.fade_var.set(value)
-            self.fading = 0 # we stop all fades
-        else: # the general case
-            self.fade_start_time = time.time()
-            self.fade_length = length
-
-            self.fade_start_level = self.fade_var.get()
-            self.fade_end_level = value
-            
-            self.fade_step_time = step_time
-            if not self.fading:
-                self.fading = 1
-                self.do_fade()
-    def do_fade(self):
-        """Actually performs the fade for Fadable.fade.  Shouldn't be called
-        directly."""
-        now = time.time()
-        elapsed = now - self.fade_start_time
-        complete = elapsed / self.fade_length
-        complete = min(1.0, complete)
-        diff = self.fade_end_level - self.fade_start_level
-        newlevel = (complete * diff) + self.fade_start_level
-        self.fade_var.set(newlevel)
-        if complete < 1:
-            self.after(self.fade_step_time, self.do_fade)
-        else:
-            self.fading = 0
-    def increase(self, multiplier=1, length=0.3):
-        """Increases the volume by multiplier * wheel_step.  If use_fades is
-        true, it do this as a fade over length time."""
-        amount = self.wheel_step * multiplier
-        if self.fading:
-            newlevel = self.fade_end_level + amount
-        else:
-            newlevel = self.fade_var.get() + amount
-        newlevel = min(100, newlevel)
-        self.set_volume(newlevel, length)
-    def decrease(self, multiplier=1, length=0.3):
-        """Descreases the volume by multiplier * wheel_step.  If use_fades
-        is true, it do this as a fade over length time."""
-        amount = self.wheel_step * multiplier
-        if self.fading:
-            newlevel = self.fade_end_level - amount
-        else:
-            newlevel = self.fade_var.get() - amount
-        newlevel = max(0, newlevel)
-        self.set_volume(newlevel, length)
-    def set_volume(self, newlevel, length=0.3):
-        """Sets the volume to newlevel, performing a fade of length if
-        use_fades is true."""
-        if self.use_fades:
-            self.fade(newlevel, length=length)
-        else:
-            self.fade_var.set(newlevel)
-    def toggle_mute(self):
-        """Toggles whether the volume is being muted."""
-        curlevel = self.fade_var.get()
-        if curlevel:
-            newlevel = 0
-            self.last_level = curlevel
-            self['bg'] = 'red' # TODO: let them choose these colors
-        else:
-            newlevel = self.last_level
-            self['bg'] = 'lightGray'
-
-        self.fade_var.set(newlevel)
-
-if __name__ == "__main__":
-    class SubScale(Scale, Fadable):
-        def __init__(self, master, *args, **kw):
-            self.scale_var = DoubleVar()
-            kw['variable'] = self.scale_var
-            Scale.__init__(self, master, *args, **kw)
-            Fadable.__init__(self, var=self.scale_var)
-
-    root = Tk()
-    root.tk_focusFollowsMouse()
-    ss = SubScale(root, from_=1, to_=0, res=0.01)
-    ss.pack()
-    mainloop()
--- a/Widgets/FlyingFader.py	Mon Apr 11 01:55:37 2005 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,191 +0,0 @@
-from Tix import *
-from time import time,sleep
-from __future__ import division
-
-class Mass:
-    def __init__(self):
-        self.x=0 # position
-        self.xgoal=0 # position goal
-        
-        self.v=0 # velocity
-        self.maxspeed = .8 # maximum speed, in position/second
-        self.maxaccel = 3 # maximum acceleration, in position/second^2
-        self.eps = .03 # epsilon - numbers within this much are considered the same
-
-        self._lastupdate=time()
-        self._stopped=1
-
-    def equal(self,a,b):
-        return abs(a-b)<self.eps
-
-    def stop(self):
-        self.v=0
-        self.xgoal=self.x
-        self._stopped=1
-        
-    def update(self):
-        t0 = self._lastupdate
-        tnow = time()
-        self._lastupdate = tnow
-
-        dt = tnow-t0
-
-        self.x += self.v*dt
-        # hitting the ends stops the slider
-        if self.x>1: self.v=max(self.v,0); self.x=1
-        if self.x<0: self.v=min(self.v,0); self.x=0
-            
-        if self.equal(self.x,self.xgoal):
-            self.x=self.xgoal # clean up value
-            self.stop()
-            return
-        
-        self._stopped=0
-        dir = (-1.0,1,0)[self.xgoal>self.x]
-
-        if abs(self.xgoal-self.x) < abs(self.v*5*dt):
-            # apply the brakes on the last 5 steps
-            dir *= -.5
-
-        self.v += dir*self.maxaccel*dt # velocity changes with acceleration in the right direction
-        self.v = min(max(self.v,-self.maxspeed),self.maxspeed) # clamp velocity
-
-        #print "x=%+.03f v=%+.03f a=%+.03f %f" % (self.x,self.v,self.maxaccel,self.xgoal)
-
-    def goto(self,newx):
-        self.xgoal=newx
-
-    def ismoving(self):
-        return not self._stopped
-
-class FlyingFader(Frame):
-    def __init__(self, master, variable, label, fadedur=1.5, font=('Arial', 8), labelwidth=12,
-                 **kw):
-        Frame.__init__(self, master)
-        self.name = label
-        self.variable = variable
-
-        self.mass = Mass()
-        
-        self.config({'bd':1, 'relief':'raised'})
-        scaleopts = {'variable' : variable, 'showvalue' : 0, 'from' : 1.0,
-                     'to' : 0, 'res' : 0.001, 'width' : 20, 'length' : 200, 'orient':'vert'}
-        scaleopts.update(kw)
-        if scaleopts['orient']=='vert':
-            side1=TOP
-            side2=BOTTOM
-        else:
-            side1=RIGHT
-            side2=LEFT
-        
-        self.scale = Scale(self, **scaleopts)
-        self.vlabel = Label(self, text="0.0", width=6, font=font)
-        self.label = Label(self, text=label, font=font, anchor='w',width=labelwidth) #wraplength=40, )
-
-        self.oldtrough = self.scale['troughcolor']
-
-        self.scale.pack(side=side2, expand=1, fill=BOTH, anchor='c')
-        self.vlabel.pack(side=side2, expand=0, fill=X)
-        self.label.pack(side=side2, expand=0, fill=X)
-
-        for k in range(1, 10):
-            self.scale.bind("<Key-%d>" % k, 
-                lambda evt, k=k: self.newfade(k / 10.0, evt))
-
-        self.scale.bind("<Key-0>", lambda evt: self.newfade(1.0, evt))
-        self.scale.bind("<grave>", lambda evt: self.newfade(0, evt))
-
-        self.scale.bind("<1>", self.cancelfade)
-        self.scale.bind("<2>", self.cancelfade)
-        self.scale.bind("<3>", self.mousefade)
-
-        self.trace_ret = self.variable.trace('w', self.updatelabel)
-        self.bind("<Destroy>",self.ondestroy)
-
-    def ondestroy(self,*ev):
-        self.variable.trace_vdelete('w',self.trace_ret)
-
-    def cancelfade(self, evt):
-        self.fadegoal = self.variable.get()
-        self.fadevel = self.fadeacc = 0
-
-        self.scale['troughcolor'] = self.oldtrough
-
-    def mousefade(self, evt):
-        target = float(self.tk.call(self.scale, 'get', evt.x, evt.y))
-        self.newfade(target, evt)
-
-    def ismoving(self):
-        return self.fadevel!=0 or self.fadeacc!=0
-
-    def newfade(self, newlevel, evt=None, length=None):
-
-        # these are currently unused-- Mass needs to accept a speed input
-        mult = 1
-        if evt.state & 8 and evt.state & 4: mult = 0.25 # both
-        elif evt.state & 8: mult = 0.5 # alt
-        elif evt.state & 4: mult = 2   # control
-
-
-        self.mass.x = self.variable.get()
-        self.mass.goto(newlevel)
-
-        self.gofade()
-
-    def gofade(self):
-        self.mass.update()
-        self.variable.set(self.mass.x)
-
-        if not self.mass.ismoving():
-            self.scale['troughcolor'] = self.oldtrough
-            return
-        
-        # blink the trough while the thing's moving
-        if time()%.4>.2:
-            # self.scale.config(troughcolor=self.oldtrough)
-            self.scale.config(troughcolor='orange')
-        else:
-            # self.scale.config(troughcolor='white')
-            self.scale.config(troughcolor='yellow')
-
-#        colorfade(self.scale, percent)
-        self.after(30, self.gofade)
-
-    def updatelabel(self, *args):
-        if self.variable:
-            self.vlabel['text'] = "%.3f" % self.variable.get()
-#        if self.fadetimes[1] == 0: # no fade
-#            self.vlabel['fg'] = 'black'
-#        elif self.curfade[1] > self.curfade[0]:
-#            self.vlabel['fg'] = 'red'
-#        else:
-#            self.vlabel['fg'] = 'blue'
-
-    def get(self):
-        return self.scale.get()
-
-    def set(self, val):
-        self.scale.set(val)
-
-def colorfade(scale, lev):
-    low = (255, 255, 255)
-    high = (0, 0, 0)
-    out = [int(l+lev*(h-l)) for h, l in zip(high,low)]
-    col="#%02X%02X%02X" % tuple(out)
-    scale.config(troughcolor=col)
-
-if __name__ == '__main__':
-    root = Tk()
-    root.tk_focusFollowsMouse()
-
-    FlyingFader(root, variable=DoubleVar(), label="suck").pack(side=LEFT, 
-        expand=1, fill=BOTH)
-    FlyingFader(root, variable=DoubleVar(), label="moof").pack(side=LEFT,
-        expand=1, fill=BOTH)
-    FlyingFader(root, variable=DoubleVar(), label="zarf").pack(side=LEFT,
-        expand=1, fill=BOTH)
-    FlyingFader(root, variable=DoubleVar(), 
-        label="long name goes here.  got it?").pack(side=LEFT, expand=1, 
-        fill=BOTH)
-
-    root.mainloop()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/keyboardcomposer	Mon Apr 11 02:21:26 2005 +0000
@@ -0,0 +1,279 @@
+#!/usr/bin/python
+
+from __future__ import division,nested_scopes
+import sys, time
+
+from twisted.internet import reactor,tksupport
+from twisted.web import xmlrpc, server
+from Tix import *
+import math, atexit, pickle
+
+import run_local
+from light9.Fadable import Fadable
+from light9.Submaster import Submasters, sub_maxes
+from light9 import dmxclient
+from light9.uihelpers import toplevelat
+
+nudge_keys = {
+    'up' : list('qwertyuiop'),
+    'down' : list('asdfghjkl')
+}
+nudge_keys['down'].append('semicolon')
+
+class SubScale(Scale, Fadable):
+    def __init__(self, master, *args, **kw):
+        self.scale_var = kw.get('variable') or DoubleVar()
+        kw.update({'variable' : self.scale_var,
+                   'from' : 1, 'to' : 0, 'showvalue' : 0,
+                   'sliderlength' : 15, 'res' : 0.01,
+                   'width' : 40, 'troughcolor' : 'black', 'bg' : 'grey40',
+                   'highlightthickness' : 1, 'bd' : 1,
+                   'highlightcolor' : 'red', 'highlightbackground' : 'black',
+                   'activebackground' : 'red'})
+        Scale.__init__(self, master, *args, **kw)
+        Fadable.__init__(self, var=self.scale_var, wheel_step=0.05)
+        self.draw_indicator_colors()
+    def draw_indicator_colors(self):
+        if self.scale_var.get() == 0:
+            self['troughcolor'] = 'black'
+        else:
+            self['troughcolor'] = 'blue'
+
+class SubmasterTk(Frame):
+    def __init__(self, master, name, current_level):
+        Frame.__init__(self, master, bd=1, relief='raised', bg='black')
+        self.slider_var = DoubleVar()
+        self.slider_var.set(current_level)
+        self.scale = SubScale(self, variable=self.slider_var, width=20)
+        namelabel = Label(self, text=name, font="Arial 11", bg='black',
+            fg='white')
+        namelabel.pack(side=TOP)
+        levellabel = Label(self, textvariable=self.slider_var, font="Arial 11",
+            bg='black', fg='white')
+        levellabel.pack(side=TOP)
+        self.scale.pack(side=BOTTOM, expand=1, fill=BOTH)
+
+class KeyboardComposer(Frame):
+    def __init__(self, root, submasters, current_sub_levels=None, dmxdummy=0):
+        Frame.__init__(self, root, bg='black')
+        self.submasters = submasters
+        self.dmxdummy = dmxdummy
+
+        self.current_sub_levels = {}
+        if current_sub_levels:
+            self.current_sub_levels = current_sub_levels
+        else:
+            try:
+                self.current_sub_levels = \
+                    pickle.load(file('.keyboardcomposer.savedlevels'))
+            except IOError:
+                pass
+
+        self.draw_ui()
+        self.send_levels_loop()
+    def draw_ui(self):
+        self.rows = [] # this holds Tk Frames for each row
+        self.slider_vars = {} # this holds subname:sub Tk vars
+        self.slider_table = {} # this holds coords:sub Tk vars
+        self.name_to_subtk = {} # subname : SubmasterTk instance
+        self.current_row = 0
+        
+        self.make_key_hints()
+        self.draw_sliders()
+        self.highlight_row(self.current_row)
+        self.rows[self.current_row].focus()
+
+        self.buttonframe = Frame(self, bg='black')
+        self.buttonframe.pack(side=BOTTOM)
+        self.refreshbutton = Button(self.buttonframe, text="Refresh", 
+            command=self.refresh, bg='black', fg='white')
+        self.refreshbutton.pack(side=LEFT)
+        self.save_stage_button = Button(self.buttonframe, text="Save", 
+            command=lambda: self.save_current_stage(self.sub_name.get()), 
+            bg='black', fg='white')
+        self.save_stage_button.pack(side=LEFT)
+        self.sub_name = Entry(self.buttonframe, bg='black', fg='white')
+        self.sub_name.pack(side=LEFT)
+        self.stop_frequent_update_time = 0
+    def make_key_hints(self):
+        keyhintrow = Frame(self)
+
+        col = 0
+        for upkey, downkey in zip(nudge_keys['up'],
+                                  nudge_keys['down']):
+            # what a hack!
+            downkey = downkey.replace('semicolon', ';')
+            upkey, downkey = (upkey.upper(), downkey.upper())
+
+            # another what a hack!
+            keylabel = Label(keyhintrow, text='%s\n%s' % (upkey, downkey), 
+                width=1, font=('Arial', 10), bg='red', fg='white', anchor='c')
+            keylabel.pack(side=LEFT, expand=1, fill=X)
+            col += 1
+
+        keyhintrow.pack(fill=X, expand=0)
+        self.keyhints = keyhintrow
+    def setup_key_nudgers(self, tkobject):
+        for d, keys in nudge_keys.items():
+            for key in keys:
+                # lowercase makes full=0
+                keysym = "<KeyPress-%s>" % key
+                tkobject.bind(keysym, \
+                    lambda evt, num=keys.index(key), d=d: \
+                        self.got_nudger(num, d))
+
+                # uppercase makes full=1
+                keysym = "<KeyPress-%s>" % key.upper()
+                keysym = keysym.replace('SEMICOLON', 'colon')
+                tkobject.bind(keysym, \
+                    lambda evt, num=keys.index(key), d=d: \
+                        self.got_nudger(num, d, full=1))
+
+        # Row changing:
+        # Page dn, C-n, and ] do down
+        # Page up, C-p, and ' do up
+        for key in '<Prior> <Next> <Control-n> <Control-p> ' \
+                   '<Key-bracketright> <Key-apostrophe>'.split():
+            tkobject.bind(key, self.change_row)
+
+    def change_row(self, event):
+        diff = 1
+        if event.keysym in ('Prior', 'p', 'bracketright'):
+            diff = -1
+        old_row = self.current_row
+        self.current_row += diff
+        self.current_row = max(0, self.current_row)
+        self.current_row = min(len(self.rows) - 1, self.current_row)
+        self.unhighlight_row(old_row)
+        self.highlight_row(self.current_row)
+        row = self.rows[self.current_row]
+        self.keyhints.pack_configure(before=row)
+    def got_nudger(self, number, direction, full=0):
+        subtk = self.slider_table[(self.current_row, number)]
+        if direction == 'up':
+            if full:
+                subtk.scale.fade(1)
+            else:
+                subtk.scale.increase()
+        else:
+            if full:
+                subtk.scale.fade(0)
+            else:
+                subtk.scale.decrease()
+    def draw_sliders(self):
+        self.tk_focusFollowsMouse()
+
+        rowcount = -1
+        col = 0
+        for sub in self.submasters.get_all_subs():
+            if col == 0: # make new row
+                row = self.make_row()
+                rowcount += 1
+            current_level = self.current_sub_levels.get(sub.name, 0)
+            subtk = self.draw_sub_slider(row, col, sub.name, current_level)
+            self.slider_table[(rowcount, col)] = subtk
+            self.name_to_subtk[sub.name] = subtk
+            col += 1
+            col %= 10
+
+            def slider_changed(x, y, z, subtk=subtk):
+                subtk.scale.draw_indicator_colors()
+                self.send_levels()
+
+            subtk.slider_var.trace('w', slider_changed)
+    def make_row(self):
+        row = Frame(self, bd=2, bg='black')
+        row.pack(expand=1, fill=BOTH)
+        self.setup_key_nudgers(row)
+        self.rows.append(row)
+        return row
+    def draw_sub_slider(self, row, col, name, current_level):
+        subtk = SubmasterTk(row, name, current_level)
+        subtk.place(relx=col * 0.1, rely=0, relwidth=0.1, relheight=1)
+        self.setup_key_nudgers(subtk.scale)
+
+        self.slider_vars[name] = subtk.slider_var
+        return subtk
+    def highlight_row(self, row):
+        row = self.rows[row]
+        row['bg'] = 'red'
+    def unhighlight_row(self, row):
+        row = self.rows[row]
+        row['bg'] = 'black'
+    def get_levels(self):
+        return dict([(name, slidervar.get()) 
+            for name, slidervar in self.slider_vars.items()])
+    def get_levels_as_sub(self):
+        scaledsubs = [self.submasters.get_sub_by_name(sub) * level \
+            for sub, level in self.get_levels().items()]
+
+        maxes = sub_maxes(*scaledsubs)
+        return maxes
+    def save_current_stage(self, subname):
+        print "saving current levels as", subname
+        sub = self.get_levels_as_sub()
+        sub.name = subname
+        sub.temporary = 0
+        sub.save()
+
+    def save(self):
+        pickle.dump(self.get_levels(), 
+                    file('.keyboardcomposer.savedlevels', 'w'))
+    def send_frequent_updates(self):
+        """called when we get a fade -- send events as quickly as possible"""
+        if time.time() <= self.stop_frequent_update_time:
+            self.send_levels()
+            self.after(10, self.send_frequent_updates)
+
+    def get_dmx_list(self):
+        maxes = self.get_levels_as_sub()
+        return maxes.get_dmx_list()
+    def send_levels(self):
+        if not self.dmxdummy:
+            levels = self.get_dmx_list()
+            dmxclient.outputlevels(levels)
+        # print "sending levels", levels
+    def send_levels_loop(self):
+        self.send_levels()
+        self.after(1000, self.send_levels_loop)
+    def refresh(self):
+        self.save()
+        self.submasters = Submasters()
+        self.current_sub_levels = \
+            pickle.load(file('.keyboardcomposer.savedlevels'))
+        for r in self.rows:
+            r.destroy()
+        self.keyhints.destroy()
+        self.buttonframe.destroy()
+        self.draw_ui()
+
+class LevelServer(xmlrpc.XMLRPC):
+    def __init__(self,name_to_subtk):
+        self.name_to_subtk = name_to_subtk
+        
+    def xmlrpc_fadesub(self,subname,level,secs):
+        """submaster will fade to level in secs"""
+        try:
+            self.name_to_subtk[subname].scale.fade(level,secs)
+            ret='ok'
+        except Exception,e:
+            ret=str(e)
+        return ret
+
+
+if __name__ == "__main__":
+    s = Submasters()
+
+    root = Tk()
+    tl = toplevelat("Keyboard Composer", existingtoplevel=root)
+    kc = KeyboardComposer(tl, s, dmxdummy=0)
+    kc.pack(fill=BOTH, expand=1)
+
+    ls = LevelServer(kc.name_to_subtk)
+    reactor.listenTCP(8050, server.Site(ls))
+
+    root.bind("<Destroy>",reactor.stop)
+    root.protocol('WM_DELETE_WINDOW', reactor.stop)
+    reactor.addSystemEventTrigger('after','shutdown',kc.save)
+    tksupport.install(root,ms=10)
+    reactor.run()
--- a/flax/KeyboardComposer.py	Mon Apr 11 01:55:37 2005 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,276 +0,0 @@
-from __future__ import division,nested_scopes
-import sys, time
-sys.path.append('..')
-from Widgets.Fadable import Fadable
-
-from twisted.internet import reactor,tksupport
-from twisted.web import xmlrpc, server
-from Tix import *
-import math, atexit, pickle
-from Submaster import Submasters, sub_maxes
-import dmxclient
-from uihelpers import toplevelat
-
-nudge_keys = {
-    'up' : list('qwertyuiop'),
-    'down' : list('asdfghjkl')
-}
-nudge_keys['down'].append('semicolon')
-
-class SubScale(Scale, Fadable):
-    def __init__(self, master, *args, **kw):
-        self.scale_var = kw.get('variable') or DoubleVar()
-        kw.update({'variable' : self.scale_var,
-                   'from' : 1, 'to' : 0, 'showvalue' : 0,
-                   'sliderlength' : 15, 'res' : 0.01,
-                   'width' : 40, 'troughcolor' : 'black', 'bg' : 'grey40',
-                   'highlightthickness' : 1, 'bd' : 1,
-                   'highlightcolor' : 'red', 'highlightbackground' : 'black',
-                   'activebackground' : 'red'})
-        Scale.__init__(self, master, *args, **kw)
-        Fadable.__init__(self, var=self.scale_var, wheel_step=0.05)
-        self.draw_indicator_colors()
-    def draw_indicator_colors(self):
-        if self.scale_var.get() == 0:
-            self['troughcolor'] = 'black'
-        else:
-            self['troughcolor'] = 'blue'
-
-class SubmasterTk(Frame):
-    def __init__(self, master, name, current_level):
-        Frame.__init__(self, master, bd=1, relief='raised', bg='black')
-        self.slider_var = DoubleVar()
-        self.slider_var.set(current_level)
-        self.scale = SubScale(self, variable=self.slider_var, width=20)
-        namelabel = Label(self, text=name, font="Arial 11", bg='black',
-            fg='white')
-        namelabel.pack(side=TOP)
-        levellabel = Label(self, textvariable=self.slider_var, font="Arial 11",
-            bg='black', fg='white')
-        levellabel.pack(side=TOP)
-        self.scale.pack(side=BOTTOM, expand=1, fill=BOTH)
-
-class KeyboardComposer(Frame):
-    def __init__(self, root, submasters, current_sub_levels=None, dmxdummy=0):
-        Frame.__init__(self, root, bg='black')
-        self.submasters = submasters
-        self.dmxdummy = dmxdummy
-
-        self.current_sub_levels = {}
-        if current_sub_levels:
-            self.current_sub_levels = current_sub_levels
-        else:
-            try:
-                self.current_sub_levels = \
-                    pickle.load(file('.keyboardcomposer.savedlevels'))
-            except IOError:
-                pass
-
-        self.draw_ui()
-        self.send_levels_loop()
-    def draw_ui(self):
-        self.rows = [] # this holds Tk Frames for each row
-        self.slider_vars = {} # this holds subname:sub Tk vars
-        self.slider_table = {} # this holds coords:sub Tk vars
-        self.name_to_subtk = {} # subname : SubmasterTk instance
-        self.current_row = 0
-        
-        self.make_key_hints()
-        self.draw_sliders()
-        self.highlight_row(self.current_row)
-        self.rows[self.current_row].focus()
-
-        self.buttonframe = Frame(self, bg='black')
-        self.buttonframe.pack(side=BOTTOM)
-        self.refreshbutton = Button(self.buttonframe, text="Refresh", 
-            command=self.refresh, bg='black', fg='white')
-        self.refreshbutton.pack(side=LEFT)
-        self.save_stage_button = Button(self.buttonframe, text="Save", 
-            command=lambda: self.save_current_stage(self.sub_name.get()), 
-            bg='black', fg='white')
-        self.save_stage_button.pack(side=LEFT)
-        self.sub_name = Entry(self.buttonframe, bg='black', fg='white')
-        self.sub_name.pack(side=LEFT)
-        self.stop_frequent_update_time = 0
-    def make_key_hints(self):
-        keyhintrow = Frame(self)
-
-        col = 0
-        for upkey, downkey in zip(nudge_keys['up'],
-                                  nudge_keys['down']):
-            # what a hack!
-            downkey = downkey.replace('semicolon', ';')
-            upkey, downkey = (upkey.upper(), downkey.upper())
-
-            # another what a hack!
-            keylabel = Label(keyhintrow, text='%s\n%s' % (upkey, downkey), 
-                width=1, font=('Arial', 10), bg='red', fg='white', anchor='c')
-            keylabel.pack(side=LEFT, expand=1, fill=X)
-            col += 1
-
-        keyhintrow.pack(fill=X, expand=0)
-        self.keyhints = keyhintrow
-    def setup_key_nudgers(self, tkobject):
-        for d, keys in nudge_keys.items():
-            for key in keys:
-                # lowercase makes full=0
-                keysym = "<KeyPress-%s>" % key
-                tkobject.bind(keysym, \
-                    lambda evt, num=keys.index(key), d=d: \
-                        self.got_nudger(num, d))
-
-                # uppercase makes full=1
-                keysym = "<KeyPress-%s>" % key.upper()
-                keysym = keysym.replace('SEMICOLON', 'colon')
-                tkobject.bind(keysym, \
-                    lambda evt, num=keys.index(key), d=d: \
-                        self.got_nudger(num, d, full=1))
-
-        # Row changing:
-        # Page dn, C-n, and ] do down
-        # Page up, C-p, and ' do up
-        for key in '<Prior> <Next> <Control-n> <Control-p> ' \
-                   '<Key-bracketright> <Key-apostrophe>'.split():
-            tkobject.bind(key, self.change_row)
-
-    def change_row(self, event):
-        diff = 1
-        if event.keysym in ('Prior', 'p', 'bracketright'):
-            diff = -1
-        old_row = self.current_row
-        self.current_row += diff
-        self.current_row = max(0, self.current_row)
-        self.current_row = min(len(self.rows) - 1, self.current_row)
-        self.unhighlight_row(old_row)
-        self.highlight_row(self.current_row)
-        row = self.rows[self.current_row]
-        self.keyhints.pack_configure(before=row)
-    def got_nudger(self, number, direction, full=0):
-        subtk = self.slider_table[(self.current_row, number)]
-        if direction == 'up':
-            if full:
-                subtk.scale.fade(1)
-            else:
-                subtk.scale.increase()
-        else:
-            if full:
-                subtk.scale.fade(0)
-            else:
-                subtk.scale.decrease()
-    def draw_sliders(self):
-        self.tk_focusFollowsMouse()
-
-        rowcount = -1
-        col = 0
-        for sub in self.submasters.get_all_subs():
-            if col == 0: # make new row
-                row = self.make_row()
-                rowcount += 1
-            current_level = self.current_sub_levels.get(sub.name, 0)
-            subtk = self.draw_sub_slider(row, col, sub.name, current_level)
-            self.slider_table[(rowcount, col)] = subtk
-            self.name_to_subtk[sub.name] = subtk
-            col += 1
-            col %= 10
-
-            def slider_changed(x, y, z, subtk=subtk):
-                subtk.scale.draw_indicator_colors()
-                self.send_levels()
-
-            subtk.slider_var.trace('w', slider_changed)
-    def make_row(self):
-        row = Frame(self, bd=2, bg='black')
-        row.pack(expand=1, fill=BOTH)
-        self.setup_key_nudgers(row)
-        self.rows.append(row)
-        return row
-    def draw_sub_slider(self, row, col, name, current_level):
-        subtk = SubmasterTk(row, name, current_level)
-        subtk.place(relx=col * 0.1, rely=0, relwidth=0.1, relheight=1)
-        self.setup_key_nudgers(subtk.scale)
-
-        self.slider_vars[name] = subtk.slider_var
-        return subtk
-    def highlight_row(self, row):
-        row = self.rows[row]
-        row['bg'] = 'red'
-    def unhighlight_row(self, row):
-        row = self.rows[row]
-        row['bg'] = 'black'
-    def get_levels(self):
-        return dict([(name, slidervar.get()) 
-            for name, slidervar in self.slider_vars.items()])
-    def get_levels_as_sub(self):
-        scaledsubs = [self.submasters.get_sub_by_name(sub) * level \
-            for sub, level in self.get_levels().items()]
-
-        maxes = sub_maxes(*scaledsubs)
-        return maxes
-    def save_current_stage(self, subname):
-        print "saving current levels as", subname
-        sub = self.get_levels_as_sub()
-        sub.name = subname
-        sub.temporary = 0
-        sub.save()
-
-    def save(self):
-        pickle.dump(self.get_levels(), 
-                    file('.keyboardcomposer.savedlevels', 'w'))
-    def send_frequent_updates(self):
-        """called when we get a fade -- send events as quickly as possible"""
-        if time.time() <= self.stop_frequent_update_time:
-            self.send_levels()
-            self.after(10, self.send_frequent_updates)
-
-    def get_dmx_list(self):
-        maxes = self.get_levels_as_sub()
-        return maxes.get_dmx_list()
-    def send_levels(self):
-        if not self.dmxdummy:
-            levels = self.get_dmx_list()
-            dmxclient.outputlevels(levels)
-        # print "sending levels", levels
-    def send_levels_loop(self):
-        self.send_levels()
-        self.after(1000, self.send_levels_loop)
-    def refresh(self):
-        self.save()
-        self.submasters = Submasters()
-        self.current_sub_levels = \
-            pickle.load(file('.keyboardcomposer.savedlevels'))
-        for r in self.rows:
-            r.destroy()
-        self.keyhints.destroy()
-        self.buttonframe.destroy()
-        self.draw_ui()
-
-class LevelServer(xmlrpc.XMLRPC):
-    def __init__(self,name_to_subtk):
-        self.name_to_subtk = name_to_subtk
-        
-    def xmlrpc_fadesub(self,subname,level,secs):
-        """submaster will fade to level in secs"""
-        try:
-            self.name_to_subtk[subname].scale.fade(level,secs)
-            ret='ok'
-        except Exception,e:
-            ret=str(e)
-        return ret
-
-
-if __name__ == "__main__":
-    s = Submasters()
-
-    root = Tk()
-    tl = toplevelat("Keyboard Composer", existingtoplevel=root)
-    kc = KeyboardComposer(tl, s, dmxdummy=0)
-    kc.pack(fill=BOTH, expand=1)
-
-    ls = LevelServer(kc.name_to_subtk)
-    reactor.listenTCP(8050, server.Site(ls))
-
-    root.bind("<Destroy>",reactor.stop)
-    root.protocol('WM_DELETE_WINDOW', reactor.stop)
-    reactor.addSystemEventTrigger('after','shutdown',kc.save)
-    tksupport.install(root,ms=10)
-    reactor.run()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/Fadable.py	Mon Apr 11 02:21:26 2005 +0000
@@ -0,0 +1,150 @@
+# taken from SnackMix -- now that's reusable code
+from Tix import *
+import time
+
+class Fadable:
+    """Fading mixin: must mix in with a Tk widget (or something that has
+    'after' at least) This is currently used by VolumeBox and MixerTk.
+    It's probably too specialized to be used elsewhere, but could possibly
+    work with an Entry or a Meter, I guess.  (Actually, this is used by
+    KeyboardComposer and KeyboardRecorder now too.)
+
+    var is a Tk variable that should be used to set and get the levels.
+    If use_fades is true, it will use fades to move between levels.
+    If key_bindings is true, it will install these keybindings:
+
+    Press a number to fade to that amount (e.g. '5' = 50%).  Also,
+    '`' (grave) will fade to 0 and '0' will fade to 100%.
+
+    If mouse_bindings is true, the following mouse bindings will be
+    installed: Right clicking toggles muting.  The mouse wheel will
+    raise or lower the volume.  Shift-mouse wheeling will cause a more
+    precise volume adjustment.  Control-mouse wheeling will cause a
+    longer fade."""
+    def __init__(self, var, wheel_step=5, use_fades=1, key_bindings=1,
+                 mouse_bindings=1):
+        self.use_fades = use_fades # whether increase and decrease should fade
+        self.wheel_step = wheel_step # amount that increase and descrease should
+                                     # change volume (by default)
+        
+        self.fade_start_level = 0
+        self.fade_end_level = 0
+        self.fade_start_time = 0
+        self.fade_length = 1
+        self.fade_step_time = 10
+        self.fade_var = var
+        self.fading = 0 # whether a fade is in progress
+
+        if key_bindings:
+            for k in range(1, 10):
+                self.bind("<Key-%d>" % k,
+                    lambda evt, k=k: self.fade(k / 10.0))
+            self.bind("<Key-0>", lambda evt: self.fade(1.0))
+            self.bind("<grave>", lambda evt: self.fade(0))
+
+            # up / down arrows
+            self.bind("<Key-Up>", lambda evt: self.increase())
+            self.bind("<Key-Down>", lambda evt: self.decrease())
+
+        if mouse_bindings:
+            # right mouse button toggles muting
+            # self.bind('<3>', lambda evt: self.toggle_mute())
+            # "NOT ANY MORE!" - homer
+
+            # mouse wheel
+            self.bind('<4>', lambda evt: self.increase())
+            self.bind('<5>', lambda evt: self.decrease())
+
+            # modified mouse wheel
+            self.bind('<Shift-4>', lambda evt: self.increase(multiplier=0.2))
+            self.bind('<Shift-5>', lambda evt: self.decrease(multiplier=0.2))
+            self.bind('<Control-4>', lambda evt: self.increase(length=1))
+            self.bind('<Control-5>', lambda evt: self.decrease(length=1))
+
+        self.last_level = 0 # used for muting
+    def fade(self, value, length=0.5, step_time=10):
+        """Fade to value in length seconds with steps every step_time
+        milliseconds"""
+        if length == 0: # 0 seconds fades happen right away and prevents
+                        # and prevents us from entering the fade loop,
+                        # which would cause a divide by zero
+            self.fade_var.set(value)
+            self.fading = 0 # we stop all fades
+        else: # the general case
+            self.fade_start_time = time.time()
+            self.fade_length = length
+
+            self.fade_start_level = self.fade_var.get()
+            self.fade_end_level = value
+            
+            self.fade_step_time = step_time
+            if not self.fading:
+                self.fading = 1
+                self.do_fade()
+    def do_fade(self):
+        """Actually performs the fade for Fadable.fade.  Shouldn't be called
+        directly."""
+        now = time.time()
+        elapsed = now - self.fade_start_time
+        complete = elapsed / self.fade_length
+        complete = min(1.0, complete)
+        diff = self.fade_end_level - self.fade_start_level
+        newlevel = (complete * diff) + self.fade_start_level
+        self.fade_var.set(newlevel)
+        if complete < 1:
+            self.after(self.fade_step_time, self.do_fade)
+        else:
+            self.fading = 0
+    def increase(self, multiplier=1, length=0.3):
+        """Increases the volume by multiplier * wheel_step.  If use_fades is
+        true, it do this as a fade over length time."""
+        amount = self.wheel_step * multiplier
+        if self.fading:
+            newlevel = self.fade_end_level + amount
+        else:
+            newlevel = self.fade_var.get() + amount
+        newlevel = min(100, newlevel)
+        self.set_volume(newlevel, length)
+    def decrease(self, multiplier=1, length=0.3):
+        """Descreases the volume by multiplier * wheel_step.  If use_fades
+        is true, it do this as a fade over length time."""
+        amount = self.wheel_step * multiplier
+        if self.fading:
+            newlevel = self.fade_end_level - amount
+        else:
+            newlevel = self.fade_var.get() - amount
+        newlevel = max(0, newlevel)
+        self.set_volume(newlevel, length)
+    def set_volume(self, newlevel, length=0.3):
+        """Sets the volume to newlevel, performing a fade of length if
+        use_fades is true."""
+        if self.use_fades:
+            self.fade(newlevel, length=length)
+        else:
+            self.fade_var.set(newlevel)
+    def toggle_mute(self):
+        """Toggles whether the volume is being muted."""
+        curlevel = self.fade_var.get()
+        if curlevel:
+            newlevel = 0
+            self.last_level = curlevel
+            self['bg'] = 'red' # TODO: let them choose these colors
+        else:
+            newlevel = self.last_level
+            self['bg'] = 'lightGray'
+
+        self.fade_var.set(newlevel)
+
+if __name__ == "__main__":
+    class SubScale(Scale, Fadable):
+        def __init__(self, master, *args, **kw):
+            self.scale_var = DoubleVar()
+            kw['variable'] = self.scale_var
+            Scale.__init__(self, master, *args, **kw)
+            Fadable.__init__(self, var=self.scale_var)
+
+    root = Tk()
+    root.tk_focusFollowsMouse()
+    ss = SubScale(root, from_=1, to_=0, res=0.01)
+    ss.pack()
+    mainloop()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/FlyingFader.py	Mon Apr 11 02:21:26 2005 +0000
@@ -0,0 +1,191 @@
+from Tix import *
+from time import time,sleep
+from __future__ import division
+
+class Mass:
+    def __init__(self):
+        self.x=0 # position
+        self.xgoal=0 # position goal
+        
+        self.v=0 # velocity
+        self.maxspeed = .8 # maximum speed, in position/second
+        self.maxaccel = 3 # maximum acceleration, in position/second^2
+        self.eps = .03 # epsilon - numbers within this much are considered the same
+
+        self._lastupdate=time()
+        self._stopped=1
+
+    def equal(self,a,b):
+        return abs(a-b)<self.eps
+
+    def stop(self):
+        self.v=0
+        self.xgoal=self.x
+        self._stopped=1
+        
+    def update(self):
+        t0 = self._lastupdate
+        tnow = time()
+        self._lastupdate = tnow
+
+        dt = tnow-t0
+
+        self.x += self.v*dt
+        # hitting the ends stops the slider
+        if self.x>1: self.v=max(self.v,0); self.x=1
+        if self.x<0: self.v=min(self.v,0); self.x=0
+            
+        if self.equal(self.x,self.xgoal):
+            self.x=self.xgoal # clean up value
+            self.stop()
+            return
+        
+        self._stopped=0
+        dir = (-1.0,1,0)[self.xgoal>self.x]
+
+        if abs(self.xgoal-self.x) < abs(self.v*5*dt):
+            # apply the brakes on the last 5 steps
+            dir *= -.5
+
+        self.v += dir*self.maxaccel*dt # velocity changes with acceleration in the right direction
+        self.v = min(max(self.v,-self.maxspeed),self.maxspeed) # clamp velocity
+
+        #print "x=%+.03f v=%+.03f a=%+.03f %f" % (self.x,self.v,self.maxaccel,self.xgoal)
+
+    def goto(self,newx):
+        self.xgoal=newx
+
+    def ismoving(self):
+        return not self._stopped
+
+class FlyingFader(Frame):
+    def __init__(self, master, variable, label, fadedur=1.5, font=('Arial', 8), labelwidth=12,
+                 **kw):
+        Frame.__init__(self, master)
+        self.name = label
+        self.variable = variable
+
+        self.mass = Mass()
+        
+        self.config({'bd':1, 'relief':'raised'})
+        scaleopts = {'variable' : variable, 'showvalue' : 0, 'from' : 1.0,
+                     'to' : 0, 'res' : 0.001, 'width' : 20, 'length' : 200, 'orient':'vert'}
+        scaleopts.update(kw)
+        if scaleopts['orient']=='vert':
+            side1=TOP
+            side2=BOTTOM
+        else:
+            side1=RIGHT
+            side2=LEFT
+        
+        self.scale = Scale(self, **scaleopts)
+        self.vlabel = Label(self, text="0.0", width=6, font=font)
+        self.label = Label(self, text=label, font=font, anchor='w',width=labelwidth) #wraplength=40, )
+
+        self.oldtrough = self.scale['troughcolor']
+
+        self.scale.pack(side=side2, expand=1, fill=BOTH, anchor='c')
+        self.vlabel.pack(side=side2, expand=0, fill=X)
+        self.label.pack(side=side2, expand=0, fill=X)
+
+        for k in range(1, 10):
+            self.scale.bind("<Key-%d>" % k, 
+                lambda evt, k=k: self.newfade(k / 10.0, evt))
+
+        self.scale.bind("<Key-0>", lambda evt: self.newfade(1.0, evt))
+        self.scale.bind("<grave>", lambda evt: self.newfade(0, evt))
+
+        self.scale.bind("<1>", self.cancelfade)
+        self.scale.bind("<2>", self.cancelfade)
+        self.scale.bind("<3>", self.mousefade)
+
+        self.trace_ret = self.variable.trace('w', self.updatelabel)
+        self.bind("<Destroy>",self.ondestroy)
+
+    def ondestroy(self,*ev):
+        self.variable.trace_vdelete('w',self.trace_ret)
+
+    def cancelfade(self, evt):
+        self.fadegoal = self.variable.get()
+        self.fadevel = self.fadeacc = 0
+
+        self.scale['troughcolor'] = self.oldtrough
+
+    def mousefade(self, evt):
+        target = float(self.tk.call(self.scale, 'get', evt.x, evt.y))
+        self.newfade(target, evt)
+
+    def ismoving(self):
+        return self.fadevel!=0 or self.fadeacc!=0
+
+    def newfade(self, newlevel, evt=None, length=None):
+
+        # these are currently unused-- Mass needs to accept a speed input
+        mult = 1
+        if evt.state & 8 and evt.state & 4: mult = 0.25 # both
+        elif evt.state & 8: mult = 0.5 # alt
+        elif evt.state & 4: mult = 2   # control
+
+
+        self.mass.x = self.variable.get()
+        self.mass.goto(newlevel)
+
+        self.gofade()
+
+    def gofade(self):
+        self.mass.update()
+        self.variable.set(self.mass.x)
+
+        if not self.mass.ismoving():
+            self.scale['troughcolor'] = self.oldtrough
+            return
+        
+        # blink the trough while the thing's moving
+        if time()%.4>.2:
+            # self.scale.config(troughcolor=self.oldtrough)
+            self.scale.config(troughcolor='orange')
+        else:
+            # self.scale.config(troughcolor='white')
+            self.scale.config(troughcolor='yellow')
+
+#        colorfade(self.scale, percent)
+        self.after(30, self.gofade)
+
+    def updatelabel(self, *args):
+        if self.variable:
+            self.vlabel['text'] = "%.3f" % self.variable.get()
+#        if self.fadetimes[1] == 0: # no fade
+#            self.vlabel['fg'] = 'black'
+#        elif self.curfade[1] > self.curfade[0]:
+#            self.vlabel['fg'] = 'red'
+#        else:
+#            self.vlabel['fg'] = 'blue'
+
+    def get(self):
+        return self.scale.get()
+
+    def set(self, val):
+        self.scale.set(val)
+
+def colorfade(scale, lev):
+    low = (255, 255, 255)
+    high = (0, 0, 0)
+    out = [int(l+lev*(h-l)) for h, l in zip(high,low)]
+    col="#%02X%02X%02X" % tuple(out)
+    scale.config(troughcolor=col)
+
+if __name__ == '__main__':
+    root = Tk()
+    root.tk_focusFollowsMouse()
+
+    FlyingFader(root, variable=DoubleVar(), label="suck").pack(side=LEFT, 
+        expand=1, fill=BOTH)
+    FlyingFader(root, variable=DoubleVar(), label="moof").pack(side=LEFT,
+        expand=1, fill=BOTH)
+    FlyingFader(root, variable=DoubleVar(), label="zarf").pack(side=LEFT,
+        expand=1, fill=BOTH)
+    FlyingFader(root, variable=DoubleVar(), 
+        label="long name goes here.  got it?").pack(side=LEFT, expand=1, 
+        fill=BOTH)
+
+    root.mainloop()
--- a/light9/Submaster.py	Mon Apr 11 01:55:37 2005 +0000
+++ b/light9/Submaster.py	Mon Apr 11 02:21:26 2005 +0000
@@ -56,6 +56,8 @@
         self.save()
     def get_levels(self):
         return self.levels
+    def all_zeros(self):
+        return not (max(self.levels.values()) > 0)
     def __mul__(self, scalar):
         return Submaster("%s*%s" % (self.name, scalar), 
             dict_scale(self.levels, scalar), temporary=1)
@@ -70,7 +72,13 @@
 
         levels = [0] * 68
         for k, v in leveldict.items():
-            dmxchan = Patch.get_dmx_channel(k) - 1
+            if v == 0:
+                continue
+            try:
+                dmxchan = Patch.get_dmx_channel(k) - 1
+            except ValueError:
+                print "error trying to compute dmx levels for submaster %s" % self.name
+                raise
             levels[dmxchan] = max(v, levels[dmxchan])
 
         return levels
@@ -112,16 +120,18 @@
     return level
 
 def sub_maxes(*subs):
-    return Submaster("max(%r)" % (subs,),
-        dict_max(*[sub.levels for sub in subs]), temporary=1)
+    nonzero_subs = [s for s in subs if not s.all_zeros()]
+    name = "max(%s)" % ", ".join([repr(s) for s in nonzero_subs])
+    return Submaster(name,
+                     dict_max(*[sub.levels for sub in nonzero_subs]),
+                     temporary=1)
 
 class Submasters:
     "Collection o' Submaster objects"
     def __init__(self):
         self.submasters = {}
 
-        import os
-        files = os.listdir('subs')
+        files = os.listdir(os.path.join(os.getenv("LIGHT9_SHOW"),'subs'))
 
         for filename in files:
             # we don't want these files