Files @ c58e781829a3
Branch filter:

Location: light9/bin/keyboardcomposer

David McClosky
keyboardcomposer: sort subs by group, then order, then name
#!/usr/bin/python

from __future__ import division, nested_scopes
import os, sys, time, subprocess
from optparse import OptionParser

from twisted.internet import reactor, tksupport
from twisted.web import xmlrpc, server
from Tix import *
import Tix as tk
import pickle

import run_local
from light9.Fadable import Fadable
from light9.Submaster import Submasters, sub_maxes
from light9.subclient import SubClient
from light9 import dmxclient, showconfig, networking, prof
from light9.uihelpers import toplevelat, bindkeys
from light9.namespaces import L9
from bcf2000 import BCF2000

nudge_keys = {
    'up'   : list('qwertyui'),
    'down' : list('asdfghjk')
}

class DummySliders:
    def valueOut(self, name, value):
        pass

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.name = name
        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 9", 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)
        bindkeys(self, "<Control-Key-l>", self.launch_subcomposer)

    def launch_subcomposer(self, *args):
        subprocess.Popen(["bin/subcomposer", self.name])

class KeyboardComposer(Frame, SubClient):
    def __init__(self, root, submasters, current_sub_levels=None,
                 hw_sliders=False):
        Frame.__init__(self, root, bg='black')
        SubClient.__init__(self)
        self.submasters = submasters
        self.name_to_subtk = {}
        self.current_sub_levels = {}
        if current_sub_levels is not None:
            self.current_sub_levels = current_sub_levels
        else:
            try:
                self.current_sub_levels = \
                    pickle.load(file('.keyboardcomposer.savedlevels'))
            except IOError:
                pass

        self.connect_to_hw(hw_sliders)
        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.clear() # 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.alltozerobutton = Button(self.buttonframe, text="All to Zero", 
            command=self.alltozero, bg='black', fg='white')
        self.alltozerobutton.pack(side='left')
        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 connect_to_hw(self, hw_sliders):
        if hw_sliders:
            self.sliders = Sliders(self.hw_slider_moved)
        else:
            self.sliders = DummySliders()

    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)

        for col in range(8):
            try:
                subtk = self.slider_table[(self.current_row, col)]
                self.sliders.valueOut("button-upper%d" % (col + 1), 127)
            except KeyError:
                # unfilled bottom row has holes (plus rows with incomplete
                # groups
                self.sliders.valueOut("button-upper%d" % (col + 1), 0)
                continue
            self.send_to_hw(subtk.name, col + 1)
            
    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 hw_slider_moved(self, col, value):
        value = int(value * 100) / 100
        try:
            subtk = self.slider_table[(self.current_row, col)]
        except KeyError:
            return # no slider assigned at that column
        subtk.scale.set(value)
                
    def draw_sliders(self):
        self.tk_focusFollowsMouse()

        rowcount = -1
        col = 0
        last_group = None
        graph = showconfig.getGraph()
        withgroups = sorted((graph.value(sub.uri, L9['group']), 
                             graph.value(sub.uri, L9['order']), 
                             sub)
            for sub in self.submasters.get_all_subs())

        for group, order, sub in withgroups:
            group = graph.value(sub.uri, L9['group'])

            if col == 0 or group != last_group: # make new row
                row = self.make_row()
                rowcount += 1
                col = 0
            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

            def slider_changed(x, y, z, subtk=subtk,
                               col=col, sub=sub, rowcount=rowcount):
                subtk.scale.draw_indicator_colors()
                self.send_levels()
                if rowcount == self.current_row:
                    self.send_to_hw(sub.name, col + 1)

            subtk.slider_var.trace('w', slider_changed)

            # initial position
            self.send_to_hw(sub.name, col + 1)
            col = (col + 1) % 8
            last_group = group

    def send_to_hw(self, subName, hwNum):
        if isinstance(self.sliders, DummySliders):
            return
            
        v = round(127 * self.slider_vars[subName].get())
        chan = "slider%s" % hwNum
        
        # workaround for some rounding issue, where we receive one
        # value and then decide to send back a value that's one step
        # lower.  -5 is a fallback for having no last value.  hopefully
        # we won't really see it
        if abs(v - self.sliders.lastValue.get(chan, -5)) <= 1:
            return
        self.sliders.valueOut(chan, v)
            
    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 / 8, rely=0, relwidth=1 / 8, 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() if level > 0.0]

        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 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()

    def alltozero(self):
        for name, subtk in self.name_to_subtk.items():
            if subtk.scale.scale_var.get() != 0:
                subtk.scale.fade(value=0.0, length=0)

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

class Sliders(BCF2000):
    def __init__(self, cb):
        BCF2000.__init__(self)
        self.cb = cb
    def valueIn(self, name, value):
        if name.startswith("slider"):
            self.cb(int(name[6:]) - 1, value / 127)

if __name__ == "__main__":
    parser = OptionParser()
    parser.add_option('--nonpersistent', action="store_true",
                      help="don't load or save levels")
    parser.add_option('--sliders', action='store_true',
                      help="attach to hardware sliders")
    opts, args = parser.parse_args()
    
    s = Submasters()

    root = Tk()
    tl = toplevelat("Keyboard Composer", existingtoplevel=root)

    startLevels = None
    if opts.nonpersistent:
        startLevels = {}
    kc = KeyboardComposer(tl, s, startLevels, hw_sliders=opts.sliders)
    kc.pack(fill=BOTH, expand=1)

    for helpline in ["Bindings: B3 mute; C-l edit levels in subcomposer"]:
        tk.Label(root,text=helpline, font="Helvetica -12 italic",
                 anchor='w').pack(side='top',fill='x')

    import twisted.internet
    try:
        ls = LevelServer(kc.name_to_subtk)
        reactor.listenTCP(networking.kcPort(), server.Site(ls))
    except twisted.internet.error.CannotListenError, e:
        print "Can't (and won't!) start level server:"
        print e

    root.protocol('WM_DELETE_WINDOW', reactor.stop)
    if not opts.nonpersistent:
        reactor.addSystemEventTrigger('after', 'shutdown', kc.save)
    
    tksupport.install(root,ms=10)


#    prof.watchPoint("/usr/lib/python2.4/site-packages/rdflib-2.3.3-py2.4-linux-i686.egg/rdflib/Graph.py", 615)
    
    prof.run(reactor.run, profile=False)