#!/usr/bin/python from __future__ import division, nested_scopes import cgi, os, sys, time, subprocess, logging from optparse import OptionParser import webcolors, colorsys from twisted.internet import reactor, tksupport from twisted.web import xmlrpc, server, resource 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 def close(self): pass def reopen(self): 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, sub, current_level): bg = sub.graph.value(sub.uri, L9.color, default='#000000') rgb = webcolors.hex_to_rgb(bg) hsv = colorsys.rgb_to_hsv(*[x/255 for x in rgb]) darkBg = webcolors.rgb_to_hex(tuple([x * 255 for x in colorsys.hsv_to_rgb( hsv[0], hsv[1], .3)])) Frame.__init__(self, master, bd=1, relief='raised', bg=bg) self.name = sub.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=sub.name, font="Arial 7", bg=darkBg, fg='white', pady=0) namelabel.pack(side=TOP) levellabel = Label(self, textvariable=self.slider_var, font="Arial 7", bg='black', fg='white', pady=0) levellabel.pack(side=TOP) self.scale.pack(side=BOTTOM, expand=1, fill=BOTH) bindkeys(self, "", self.launch_subcomposer) def launch_subcomposer(self, *args): subprocess.Popen(["bin/subcomposer", "--no-geometry", self.name]) class KeyboardComposer(Frame, SubClient): def __init__(self, root, graph, submasters, current_sub_levels=None, hw_sliders=True): Frame.__init__(self, root, bg='black') SubClient.__init__(self) self.graph = graph self.submasters = submasters self.name_to_subtk = {} self.current_sub_levels = {} self.current_row = 0 if current_sub_levels is not None: self.current_sub_levels = current_sub_levels else: try: self.current_sub_levels, self.current_row = \ pickle.load(file('.keyboardcomposer.savedlevels')) except IOError: pass self.use_hw_sliders = hw_sliders 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.make_key_hints() self.draw_sliders() self.change_row(self.current_row) self.rows[self.current_row].focus() self.buttonframe = Frame(self, bg='black') self.buttonframe.pack(side=BOTTOM) self.sliders_status_var = IntVar() self.sliders_status_var.set(self.use_hw_sliders) self.sliders_checkbutton = Checkbutton(self.buttonframe, text="Sliders", variable=self.sliders_status_var, command=lambda: self.toggle_slider_connectedness(), bg='black', fg='white') self.sliders_checkbutton.pack(side=LEFT) 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 draw_sliders(self): self.tk_focusFollowsMouse() rowcount = -1 col = 0 last_group = None withgroups = sorted((self.graph.value(sub.uri, L9['group']), self.graph.value(sub.uri, L9['order']), sub) for sub in self.submasters.get_all_subs()) for group, order, sub in withgroups: group = self.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, 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 draw_sub_slider(self, row, col, sub, current_level): subtk = SubmasterTk(row, sub, current_level) subtk.place(relx=col / 8, rely=0, relwidth=1 / 8, relheight=1) self.setup_key_nudgers(subtk.scale) self.slider_vars[sub.name] = subtk.slider_var return subtk def toggle_slider_connectedness(self): self.use_hw_sliders = not self.use_hw_sliders if self.use_hw_sliders: self.sliders.reopen() else: self.sliders.close() self.change_row(self.current_row) self.rows[self.current_row].focus() def connect_to_hw(self, hw_sliders): if hw_sliders: try: self.sliders = Sliders(self) except IOError: print "Couldn't actually find any sliders (but really, it's no problem)" self.sliders = DummySliders() self.use_hw_sliders = False 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 = "" % key tkobject.bind(keysym, \ lambda evt, num=keys.index(key), d=d: \ self.got_nudger(num, d)) # uppercase makes full=1 keysym = "" % 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 ' ' \ ' '.split(): tkobject.bind(key, self.change_row_cb) def change_row_cb(self, event): diff = 1 if event.keysym in ('Prior', 'p', 'bracketright'): diff = -1 self.change_row(self.current_row + diff) def change_row(self, row): old_row = self.current_row self.current_row = row 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(1, 9): try: subtk = self.slider_table[(self.current_row, col - 1)] self.sliders.valueOut("button-upper%d" % col, True) except KeyError: # unfilled bottom row has holes (plus rows with incomplete # groups self.sliders.valueOut("button-upper%d" % col, False) self.sliders.valueOut("slider%d" % col, 0) continue self.send_to_hw(subtk.name, col) def got_nudger(self, number, direction, full=0): try: subtk = self.slider_table[(self.current_row, number)] except KeyError: return 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 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 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(), self.current_row), 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() graph = showconfig.getGraph() self.submasters = Submasters(graph) self.current_sub_levels, self.current_row = \ pickle.load(file('.keyboardcomposer.savedlevels')) for r in self.rows: r.destroy() self.keyhints.destroy() self.buttonframe.destroy() self.draw_ui() # possibly paranoia (but possibly not) self.change_row(self.current_row) 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) # move to web lib def postArgGetter(request): """return a function that takes arg names and returns string values. Supports args encoded in the url or in postdata. No support for repeated args.""" # this is something nevow normally does for me request.content.seek(0) fields = cgi.FieldStorage(request.content, request.received_headers, environ={'REQUEST_METHOD': 'POST'}) def getArg(n): try: return request.args[n][0] except KeyError: return fields[n].value return getArg class LevelServerHttp(resource.Resource): isLeaf = True def __init__(self,name_to_subtk): self.name_to_subtk = name_to_subtk def render_POST(self, request): arg = postArgGetter(request) if request.path == '/fadesub': # fadesub?subname=scoop&level=0&secs=.2 self.name_to_subtk[arg('subname')].scale.fade( float(arg('level')), float(arg('secs'))) return "set %s to %s" % (arg('subname'), arg('level')) else: raise NotImplementedError(repr(request)) class Sliders(BCF2000): def __init__(self, kc): BCF2000.__init__(self) self.kc = kc def valueIn(self, name, value): kc = self.kc if name.startswith("slider"): kc.hw_slider_moved(int(name[6:]) - 1, value / 127) elif name.startswith("button-upper"): kc.change_row(kc.current_row) elif name.startswith("button-lower"): col = int(name[12:]) - 1 self.valueOut(name, 0) try: tkslider = kc.slider_table[(kc.current_row, col)] except KeyError: return slider_var = tkslider.slider_var if slider_var.get() == 1: slider_var.set(0) else: slider_var.set(1) elif name.startswith("button-corner"): button_num = int(name[13:]) - 1 if button_num == 1: diff = -1 elif button_num == 3: diff = 1 else: return kc.change_row(kc.current_row + diff) self.valueOut(name, 0) if __name__ == "__main__": parser = OptionParser() parser.add_option('--nonpersistent', action="store_true", help="don't load or save levels") parser.add_option('--no-sliders', action='store_true', help="don't attach to hardware sliders") parser.add_option('-v', action='store_true', help="log info level") opts, args = parser.parse_args() logging.basicConfig(level=logging.INFO if opts.v else logging.WARN) log = logging.getLogger() graph = showconfig.getGraph() s = Submasters(graph) root = Tk() tl = toplevelat("Keyboard Composer", existingtoplevel=root) startLevels = None if opts.nonpersistent: startLevels = {} kc = KeyboardComposer(tl, graph, s, startLevels, hw_sliders=not opts.no_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: reactor.listenTCP(networking.keyboardComposer.port, server.Site(LevelServerHttp(kc.name_to_subtk))) 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)