Changeset - 295b867fd810
[Not reviewed]
default
0 8 0
drewp@bigasterisk.com - 12 years ago 2013-01-15 21:02:08
drewp@bigasterisk.com
just whitespace (hopefully)
Ignore-this: a364fab649d9e795f703cfe794f07ca6
8 files changed with 92 insertions and 88 deletions:
0 comments (0 inline, 0 general)
bin/keyboardcomposer
Show inline comments
 
@@ -31,551 +31,554 @@ nudge_keys = {
 
    '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 SubmasterBox(Frame):
 
    """
 
    this object owns the level of the submaster (the rdf graph is the
 
    real authority)
 
    """
 
    def __init__(self, master, sub, session):
 
        self.sub = sub
 
        self.session = session
 
        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.pauseTrace = False
 
        self.scale = SubScale(self, variable=self.slider_var, width=20)
 
        
 

	
 
        self.namelabel = Label(self, font="Arial 7", bg=darkBg,
 
            fg='white', pady=0)
 
        self.sub.graph.addHandler(self.updateName)
 
        
 

	
 
        self.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, "<Control-Key-l>", self.launch_subcomposer)
 

	
 
        for w in [self, self.namelabel, levellabel]:
 
            dragSourceRegister(w, 'copy', 'text/uri-list', sub.uri)
 

	
 
        self._slider_var_trace = self.slider_var.trace('w', self.slider_changed)
 

	
 
        sub.graph.addHandler(self.updateLevelFromGraph)
 

	
 
        # initial position
 
#        self.send_to_hw(sub.name, col + 1) # needs fix
 

	
 
    def cleanup(self):
 
        self.slider_var.trace_vdelete('w', self._slider_var_trace)
 
    
 

	
 
    def slider_changed(self, *args):
 
        self.scale.draw_indicator_colors()
 

	
 
        if self.pauseTrace:
 
            return
 
        self.updateGraphWithLevel(self.sub.uri, self.slider_var.get())
 
        # dispatcher.send("level changed") # in progress
 
        ###self.send_levels() # use dispatcher? 
 
        ###self.send_levels() # use dispatcher?
 

	
 
        # needs fixing: plan is to use dispatcher or a method call to tell a hardware-mapping object who changed, and then it can make io if that's a current hw slider
 
        #if rowcount == self.current_row:
 
        #    self.send_to_hw(sub.name, col + 1)
 

	
 
    def updateGraphWithLevel(self, uri, level):
 
        """in our per-session graph, we maintain SubSetting objects like this:
 

	
 
           ?session :subSetting [a :SubSetting; :sub ?s; :level ?l]
 
        """
 
        # move to syncedgraph patchMapping
 
        with self.sub.graph.currentState() as graph:
 
            adds = set([])
 
            for setting in graph.objects(self.session, L9['subSetting']):
 
                if graph.value(setting, L9['sub']) == uri:
 
                    break
 
            else:
 
                setting = URIRef(self.session + "/setting/%s" %
 
                                 random.randrange(999999999))
 
                adds.update([
 
                    (self.session, L9['subSetting'], setting, self.session),
 
                    (setting, RDF.type, L9['SubSetting'], self.session),
 
                    (setting, L9['sub'], uri, self.session),
 
                    ])
 
            dels = set([])
 
            for prev in graph.objects(setting, L9['level']):
 
                dels.add((setting, L9['level'], prev, self.session))
 
            adds.add((setting, L9['level'], Literal(level), self.session))
 

	
 
            if adds != dels:
 
                self.sub.graph.patch(Patch(delQuads=dels, addQuads=adds))
 

	
 
    def updateLevelFromGraph(self):
 
        """read rdf level, write it to subbox.slider_var"""
 
        # move this to syncedgraph readMapping
 
        graph = self.sub.graph
 

	
 
        for setting in graph.objects(self.session, L9['subSetting']):
 
            if graph.value(setting, L9['sub']) == self.sub.uri:
 
                self.pauseTrace = True # don't bounce this update back to server
 
                try:
 
                    self.slider_var.set(graph.value(setting, L9['level']))
 
                finally:
 
                    self.pauseTrace = False
 

	
 
    def updateName(self):
 
        self.namelabel.config(text=self.sub.graph.label(self.sub.uri))
 

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

	
 
class KeyboardComposer(Frame, SubClient):
 
    def __init__(self, root, graph, session,
 
                 hw_sliders=True):
 
        Frame.__init__(self, root, bg='black')
 
        SubClient.__init__(self)
 
        self.graph = graph
 
        self.session = session
 
        self.submasters = Submasters(graph)
 
        self.subbox = {} # sub uri : SubmasterBox
 
        self.slider_table = {} # coords : SubmasterBox
 
        self.rows = [] # this holds Tk Frames for each row
 
        
 

	
 
        self.current_row = 0 # should come from session graph
 

	
 
        self.use_hw_sliders = hw_sliders
 
        self.connect_to_hw(hw_sliders)
 

	
 
        self.make_key_hints()
 
        self.make_buttons()
 

	
 
        self.graph.addHandler(self.redraw_sliders)
 
        self.send_levels_loop()
 
        self.graph.addHandler(self.rowFromGraph)
 

	
 
    def make_buttons(self):
 
        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, 
 
        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", 
 
        self.alltozerobutton = Button(self.buttonframe, text="All to Zero",
 
            command=self.alltozero, bg='black', fg='white')
 
        self.alltozerobutton.pack(side='left')
 

	
 
        self.save_stage_button = Button(self.buttonframe, text="Save", 
 
            command=lambda: self.save_current_stage(self.sub_name.get()), 
 
        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)
 

	
 
    def redraw_sliders(self):
 
        self.graph.addHandler(self.draw_sliders)
 
        if len(self.rows):
 
            self.change_row(self.current_row)
 
            self.rows[self.current_row].focus()
 

	
 
        self.stop_frequent_update_time = 0
 

	
 
    def onNewSub(self, sub):
 
        log.info("new %s", sub)
 
        self.graph.addHandler(self.draw_sliders)
 

	
 
    def onLostSub(self, subUri):
 
        log.info("lost %s", subUri)
 
        self.graph.addHandler(self.draw_sliders)
 
    
 

	
 
    def draw_sliders(self):
 
        for r in self.rows:
 
            r.destroy()
 
        self.rows = []
 
        for b in self.subbox.values():
 
            b.cleanup()
 
        self.subbox.clear()
 
        self.slider_table.clear()
 
        
 

	
 
        self.tk_focusFollowsMouse()
 

	
 
        rowcount = -1
 
        col = 0
 
        last_group = None
 

	
 
        # there are unlikely to be any subs at startup because we
 
        # probably haven't been called back with the graph data yet
 
        
 
        withgroups = sorted((self.graph.value(sub.uri, L9['group']), 
 
                             self.graph.value(sub.uri, L9['order']), 
 

	
 
        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())
 
        dispatcher.connect(self.onNewSub, "new submaster")
 
        dispatcher.connect(self.onLostSub, "lost submaster")
 
        log.info("withgroups %s", withgroups)
 

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

	
 
            if col == 0 or group != last_group:
 
                row = self.make_row()
 
                rowcount += 1
 
                col = 0
 

	
 
            subbox = SubmasterBox(row, sub, self.session)
 
            subbox.place(relx=col / 8, rely=0, relwidth=1 / 8, relheight=1)
 
            self.subbox[sub.uri] = self.slider_table[(rowcount, col)] = subbox
 

	
 
            self.setup_key_nudgers(subbox.scale)
 

	
 
            col = (col + 1) % 8
 
            last_group = group
 
            
 

	
 
    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:
 
                log.info("no hardware sliders")
 
                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), 
 
            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_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 rowFromGraph(self):
 
        self.change_row(int(self.graph.value(self.session, L9['currentRow'], default=0)), fromGraph=True)
 

	
 
    def change_row(self, row, fromGraph=False):
 
        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)
 
        try:
 
            row = self.rows[self.current_row]
 
        except IndexError:
 
            # if we're mid-load, this row might still appear soon. If
 
            # we changed interactively, the user is out of bounds and
 
            # needs to be brought back in
 
            if fromGraph:
 
                return
 
            raise
 

	
 
        self.unhighlight_row(old_row)
 
        self.highlight_row(self.current_row)
 
        self.keyhints.pack_configure(before=row)
 

	
 
        if not fromGraph:
 
            self.graph.patchObject(self.session, self.session, L9['currentRow'],
 
                                   Literal(self.current_row))
 

	
 
        for col in range(1, 9):
 
            try:
 
                subbox = 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(subbox.name, col)
 
            
 

	
 
    def got_nudger(self, number, direction, full=0):
 
        try:
 
            subbox = self.slider_table[(self.current_row, number)]
 
        except KeyError:
 
            return
 

	
 
        if direction == 'up':
 
            if full:
 
                subbox.scale.fade(1)
 
            else:
 
                subbox.scale.increase()
 
        else:
 
            if full:
 
                subbox.scale.fade(0)
 
            else:
 
                subbox.scale.decrease()
 

	
 
    def hw_slider_moved(self, col, value):
 
        value = int(value * 100) / 100
 
        try:
 
            subbox = self.slider_table[(self.current_row, col)]
 
        except KeyError:
 
            return # no slider assigned at that column
 
        subbox.scale.set(value)
 

	
 
    def send_to_hw(self, subUri, hwNum):
 
        if isinstance(self.sliders, DummySliders):
 
            return
 
            
 

	
 
        v = round(127 * self.slider_vars[subUri].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([(uri, box.slider_var.get()) 
 
        return dict([(uri, box.slider_var.get())
 
            for uri, box in self.subbox.items()])
 

	
 
    def get_levels_as_sub(self):
 
        scaledsubs = [self.submasters.get_sub_by_uri(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):
 
        log.info("saving current levels as %s", subname)
 
        sub = self.get_levels_as_sub()
 
        sub.name = subname
 
        sub.temporary = 0
 
        sub.save()
 

	
 
    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 alltozero(self):
 
        for uri, subbox in self.subbox.items():
 
            if subbox.scale.scale_var.get() != 0:
 
                subbox.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_subbox):
 
        self.name_to_subbox = name_to_subbox
 

	
 
    def render_POST(self, request):
 
        arg = postArgGetter(request)
 
        
 

	
 
        if request.path == '/fadesub':
 
            # fadesub?subname=scoop&level=0&secs=.2
 
            self.name_to_subbox[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):
 
        devices = ['/dev/snd/midiC1D0', '/dev/snd/midiC2D0', '/dev/snd/midiC3D0']
 
        for dev in devices:
 
            try:
 
                BCF2000.__init__(self, dev=dev)
 
            except IOError, e:
 
                if dev is devices[-1]:
 
                    raise
 
            else:
 
                break
 

	
 
        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('--no-sliders', action='store_true',
 
                      help="don't attach to hardware sliders")
 
    clientsession.add_option(parser)
 
    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('keyboardcomposer')
 

	
 
    graph = SyncedGraph("keyboardcomposer")
 

	
 
    # i think this also needs delayed start (like subcomposer has), to have a valid graph
 
    # before setting any stuff from the ui
 
    
 
    root = Tk()
 
    initTkdnd(root.tk, 'tkdnd/trunk/')
 

	
 
    session = clientsession.getUri('keyboardcomposer', opts)
 

	
 
    tl = toplevelat("Keyboard Composer - %s" % opts.session,
 
                    existingtoplevel=root, graph=graph, session=session)
 

	
 
    kc = KeyboardComposer(tl, graph, session,
 
                          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')
 

	
 
    if 0: # needs fixing, or maybe it's obsolete because other progs can just patch the rdf graph
 
        import twisted.internet
 
        try:
 
            reactor.listenTCP(networking.keyboardComposer.port,
 
                              server.Site(LevelServerHttp(kc.name_to_subbox)))
 
        except twisted.internet.error.CannotListenError, e:
 
            log.warn("Can't (and won't!) start level server:")
 
            log.warn(e)
 

	
 
    root.protocol('WM_DELETE_WINDOW', reactor.stop)
 
    
 

	
 
    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)
bin/rdfdb
Show inline comments
 
@@ -106,290 +106,290 @@ Our web ui:
 
    something. clicking any resource from the other displays takes you
 
    to this, focused on that resource
 

	
 
"""
 
from twisted.internet import reactor
 
import twisted.internet.error
 
import sys, optparse, logging, json, os
 
import cyclone.web, cyclone.httpclient, cyclone.websocket
 
sys.path.append(".")
 
from light9 import networking, showconfig, prof
 
from rdflib import ConjunctiveGraph, URIRef, Graph
 
from light9.rdfdb.graphfile import GraphFile
 
from light9.rdfdb.patch import Patch, ALLSTMTS
 
from light9.rdfdb.rdflibpatch import patchQuads
 
from light9.rdfdb import syncedgraph
 

	
 
from twisted.internet.inotify import INotify
 
logging.basicConfig(level=logging.DEBUG)
 
log = logging.getLogger()
 

	
 
try:
 
    import sys
 
    sys.path.append("../homeauto/lib")
 
    from cycloneerr import PrettyErrorHandler
 
except ImportError:
 
    class PrettyErrorHandler(object):
 
        pass
 

	
 
class Client(object):
 
    """
 
    one of our syncedgraph clients
 
    """
 
    def __init__(self, updateUri, label, db):
 
        self.db = db
 
        self.label = label
 
        self.updateUri = updateUri
 
        self.sendAll()
 

	
 
    def __repr__(self):
 
        return "<%s client at %s>" % (self.label, self.updateUri)
 

	
 
    def sendAll(self):
 
        """send the client the whole graph contents"""
 
        log.info("sending all graphs to %s at %s" %
 
                 (self.label, self.updateUri))
 
        self.sendPatch(Patch(
 
            addQuads=self.db.graph.quads(ALLSTMTS),
 
            delQuads=[]))
 
        
 

	
 
    def sendPatch(self, p):
 
        return syncedgraph.sendPatch(self.updateUri, p)
 

	
 
class Db(object):
 
    """
 
    the master graph, all the connected clients, all the files we're watching
 
    """
 
    def __init__(self):
 
        # files from cwd become uris starting with this. *should* be
 
        # building uris from the show uri in $LIGHT9_SHOW/URI
 
        # instead. Who wants to keep their data in the same dir tree
 
        # as the source code?!
 
        self.topUri = URIRef("http://light9.bigasterisk.com/")
 

	
 
        self.clients = []
 
        self.graph = ConjunctiveGraph()
 

	
 
        self.notifier = INotify()
 
        self.notifier.startReading()
 
        self.graphFiles = {} # context uri : GraphFile
 

	
 
        self.findAndLoadFiles()
 

	
 
    def findAndLoadFiles(self):
 
        self.initialLoad = True
 
        try:
 
            dirs = [
 
                "show/dance2012/sessions",
 
                "show/dance2012/subs",
 
                "show/dance2012/subterms",
 
                ]
 

	
 
            for topdir in dirs:
 
                for dirpath, dirnames, filenames in os.walk(topdir):
 
                    for base in filenames:
 
                        self.watchFile(os.path.join(dirpath, base))
 
                # todo: also notice new files in this dir
 

	
 
            self.watchFile("show/dance2012/config.n3")
 
            self.watchFile("show/dance2012/patch.n3")
 
        finally:
 
            self.initialLoad = False
 
            
 

	
 
        self.summarizeToLog()
 

	
 
    def uriFromFile(self, filename):
 
        if filename.endswith('.n3'):
 
            # some legacy files don't end with n3. when we write them
 
            # back this might not go so well
 
            filename = filename[:-len('.n3')]
 
        return URIRef(self.topUri + filename)
 
        
 

	
 
    def fileForUri(self, ctx):
 
        if not ctx.startswith(self.topUri):
 
            raise ValueError("don't know what filename to use for %s" % ctx)
 
        return ctx[len(self.topUri):] + ".n3"
 

	
 
    def watchFile(self, inFile):
 
        ctx = self.uriFromFile(inFile)
 
        gf = GraphFile(self.notifier, inFile, ctx, self.patch, self.getSubgraph)
 
        self.graphFiles[ctx] = gf
 
        gf.reread()
 
        
 

	
 
    def patch(self, p, dueToFileChange=False):
 
        """
 
        apply this patch to the master graph then notify everyone about it
 

	
 
        dueToFileChange if this is a patch describing an edit we read
 
        *from* the file (such that we shouldn't write it back to the file)
 

	
 
        if p has a senderUpdateUri attribute, we won't send this patch
 
        back to the sender with that updateUri
 
        """
 
        ctx = p.getContext()
 
        log.info("patching graph %s -%d +%d" % (
 
            ctx, len(p.delQuads), len(p.addQuads)))
 

	
 
        patchQuads(self.graph, p.delQuads, p.addQuads, perfect=True)
 
        senderUpdateUri = getattr(p, 'senderUpdateUri', None)
 
        #if not self.initialLoad:
 
        #    self.summarizeToLog()
 
        for c in self.clients:
 
            if c.updateUri == senderUpdateUri:
 
                # this client has self-applied the patch already
 
                continue
 
            d = c.sendPatch(p)
 
            d.addErrback(self.clientErrored, c)
 
        if not dueToFileChange:
 
            self.dirtyFiles([ctx])
 
        sendToLiveClients(asJson=p.jsonRepr)
 

	
 
    def dirtyFiles(self, ctxs):
 
        """mark dirty the files that we watch in these contexts.
 

	
 
        the ctx might not be a file that we already read; it might be
 
        for a new file we have to create, or it might be for a
 
        transient context that we're not going to save
 

	
 
        if it's a ctx with no file, error
 
        """
 
        for ctx in ctxs:
 
            g = self.getSubgraph(ctx)
 

	
 
            if ctx not in self.graphFiles:
 
                outFile = self.fileForUri(ctx)
 
                self.graphFiles[ctx] = GraphFile(self.notifier, outFile, ctx,
 
                                                 self.patch, self.getSubgraph)
 
            
 

	
 
            self.graphFiles[ctx].dirty(g)
 

	
 
    def clientErrored(self, err, c):
 
        err.trap(twisted.internet.error.ConnectError)
 
        log.info("connection error- dropping client %r" % c)
 
        self.clients.remove(c)
 
        self.sendClientsToAllLivePages()        
 
        self.sendClientsToAllLivePages()
 

	
 
    def summarizeToLog(self):
 
        log.info("contexts in graph (%s total stmts):" % len(self.graph))
 
        for c in self.graph.contexts():
 
            log.info("  %s: %s statements" %
 
                     (c.identifier, len(self.getSubgraph(c.identifier))))
 

	
 
    def getSubgraph(self, uri):
 
        """
 
        this is meant to return a live view of the given subgraph, but
 
        if i'm still working around an rdflib bug, it might return a
 
        copy
 

	
 
        and it's returning triples, but I think quads would be better
 
        """
 
        # this is returning an empty Graph :(
 
        #return self.graph.get_context(uri)
 

	
 
        g = Graph()
 
        for s in self.graph.triples(ALLSTMTS, uri):
 
            g.add(s)
 
        return g
 
    
 

	
 
    def addClient(self, updateUri, label):
 
        [self.clients.remove(c)
 
         for c in self.clients if c.updateUri == updateUri]
 

	
 
        log.info("new client %s at %s" % (label, updateUri))
 
        self.clients.append(Client(updateUri, label, self))
 
        self.sendClientsToAllLivePages()
 

	
 
    def sendClientsToAllLivePages(self):
 
        sendToLiveClients({"clients":[
 
            dict(updateUri=c.updateUri, label=c.label)
 
            for c in self.clients]})        
 
            for c in self.clients]})
 

	
 
class GraphResource(PrettyErrorHandler, cyclone.web.RequestHandler):
 
    def get(self):
 
        pass
 
    
 
        self.write(self.settings.db.graph.serialize(format='n3'))
 

	
 
class Patches(PrettyErrorHandler, cyclone.web.RequestHandler):
 
    def __init__(self, *args, **kw):
 
        cyclone.web.RequestHandler.__init__(self, *args, **kw)
 
        p = syncedgraph.makePatchEndpointPutMethod(self.settings.db.patch)
 
        self.put = lambda: p(self)
 

	
 
    def get(self):
 
        pass
 

	
 
class GraphClients(PrettyErrorHandler, cyclone.web.RequestHandler):
 
    def get(self):
 
        pass
 
    
 

	
 
    def post(self):
 
        upd = self.get_argument("clientUpdate")
 
        try:
 
            self.settings.db.addClient(upd, self.get_argument("label"))
 
        except:
 
            import traceback
 
            traceback.print_exc()
 
            raise
 

	
 
liveClients = set()
 
def sendToLiveClients(d=None, asJson=None):
 
    j = asJson or json.dumps(d)
 
    for c in liveClients:
 
        c.sendMessage(j)
 

	
 
class Live(cyclone.websocket.WebSocketHandler):
 
    
 

	
 
    def connectionMade(self, *args, **kwargs):
 
        log.info("websocket opened")
 
        liveClients.add(self)
 
        self.settings.db.sendClientsToAllLivePages()
 

	
 
    def connectionLost(self, reason):
 
        log.info("websocket closed")
 
        liveClients.remove(self)
 

	
 
    def messageReceived(self, message):
 
        log.info("got message %s" % message)
 
        self.sendMessage(message)
 

	
 
class NoExts(cyclone.web.StaticFileHandler):
 
    # .xhtml pages can be get() without .xhtml on them
 
    def get(self, path, *args, **kw):
 
        if path and '.' not in path:
 
            path = path + ".xhtml"
 
        cyclone.web.StaticFileHandler.get(self, path, *args, **kw)
 

	
 
if __name__ == "__main__":
 
    logging.basicConfig()
 
    log = logging.getLogger()
 

	
 
    parser = optparse.OptionParser()
 
    parser.add_option('--show',
 
        help='show URI, like http://light9.bigasterisk.com/show/dance2008',
 
                      default=showconfig.showUri())
 
    parser.add_option("-v", "--verbose", action="store_true",
 
                      help="logging.DEBUG")
 
    (options, args) = parser.parse_args()
 

	
 
    log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 

	
 
    if not options.show:
 
        raise ValueError("missing --show http://...")
 

	
 
    db = Db()
 
    
 

	
 
    from twisted.python import log as twlog
 
    twlog.startLogging(sys.stdout)
 
            
 

	
 
    port = 8051
 
    reactor.listenTCP(port, cyclone.web.Application(handlers=[
 
        (r'/live', Live),
 
        (r'/graph', GraphResource),
 
        (r'/patches', Patches),
 
        (r'/graphClients', GraphClients),
 

	
 
        (r'/(.*)', NoExts,
 
         {"path" : "light9/rdfdb/web",
 
          "default_filename" : "index.xhtml"}),
 

	
 
        ], debug=True, db=db))
 
    log.info("serving on %s" % port)
 
    prof.run(reactor.run, profile=False)
light9/Submaster.py
Show inline comments
 
from __future__ import division
 
import os, logging, time
 
from rdflib import Graph, RDF
 
from rdflib import RDFS, Literal, BNode
 
from light9.namespaces import L9, XSD
 
from light9.TLUtility import dict_scale, dict_max
 
from light9 import Patch, showconfig
 
from louie import dispatcher
 
log = logging.getLogger('submaster')
 

	
 
class Submaster(object):
 
    "Contain a dictionary of levels, but you didn't need to know that"
 
    def __init__(self, name, levels):
 
        """this sub has a name just for debugging. It doesn't get persisted. 
 
        """this sub has a name just for debugging. It doesn't get persisted.
 
        See PersistentSubmaster.
 

	
 
        levels is a dict
 
        """
 
        self.name = name
 
        self.levels = levels
 

	
 
        self.temporary = True
 
        
 

	
 
        if not self.temporary:
 
            # obsolete
 
            dispatcher.connect(log.error, 'reload all subs')
 
            
 

	
 
        log.debug("%s initial levels %s", self.name, self.levels)
 

	
 
    def _editedLevels(self):
 
        pass
 
    
 

	
 
    def set_level(self, channelname, level, save=True):
 
        self.levels[Patch.resolve_name(channelname)] = level
 
        self._editedLevels()
 

	
 
    def set_all_levels(self, leveldict):
 
        self.levels.clear()
 
        for k, v in leveldict.items():
 
            # this may call _editedLevels too many times
 
            self.set_level(k, v, save=0)
 

	
 
    def get_levels(self):
 
        return self.levels
 
    
 

	
 
    def no_nonzero(self):
 
        return (not self.levels.values()) or not (max(self.levels.values()) > 0)
 

	
 
    def __mul__(self, scalar):
 
        return Submaster("%s*%s" % (self.name, scalar), 
 
        return Submaster("%s*%s" % (self.name, scalar),
 
                         levels=dict_scale(self.levels, scalar))
 
    __rmul__ = __mul__
 
    def max(self, *othersubs):
 
        return sub_maxes(self, *othersubs)
 

	
 
    def __add__(self, other):
 
        return self.max(other)
 

	
 
    def ident(self):
 
        return (self.name, tuple(sorted(self.levels.items())))
 

	
 
    def __repr__(self):
 
        items = getattr(self, 'levels', {}).items()
 
        items.sort()
 
        levels = ' '.join(["%s:%.2f" % item for item in items])
 
        return "<'%s': [%s]>" % (getattr(self, 'name', 'no name yet'), levels)
 
    
 

	
 
    def __cmp__(self, other):
 
        # not sure how useful this is
 
        return cmp(self.ident(), other.ident())
 
    
 

	
 
    def __hash__(self):
 
        return hash(self.ident())
 

	
 
    def get_dmx_list(self):
 
        leveldict = self.get_levels() # gets levels of sub contents
 

	
 
        levels = []
 
        for k, v in leveldict.items():
 
            if v == 0:
 
                continue
 
            try:
 
                dmxchan = Patch.get_dmx_channel(k) - 1
 
            except ValueError:
 
                log.error("error trying to compute dmx levels for submaster %s" % self.name)
 
                raise
 
            if dmxchan >= len(levels):
 
                levels.extend([0] * (dmxchan - len(levels) + 1))
 
            levels[dmxchan] = max(v, levels[dmxchan])
 

	
 
        return levels
 
    
 

	
 
    def normalize_patch_names(self):
 
        """Use only the primary patch names."""
 
        # possibly busted -- don't use unless you know what you're doing
 
        self.set_all_levels(self.levels.copy())
 

	
 
    def get_normalized_copy(self):
 
        """Get a copy of this sumbaster that only uses the primary patch 
 
        """Get a copy of this sumbaster that only uses the primary patch
 
        names.  The levels will be the same."""
 
        newsub = Submaster("%s (normalized)" % self.name, {})
 
        newsub.set_all_levels(self.levels)
 
        return newsub
 
    
 

	
 
    def crossfade(self, othersub, amount):
 
        """Returns a new sub that is a crossfade between this sub and
 
        another submaster.  
 
        
 
        another submaster.
 

	
 
        NOTE: You should only crossfade between normalized submasters."""
 
        otherlevels = othersub.get_levels()
 
        keys_set = {}
 
        for k in self.levels.keys() + otherlevels.keys():
 
            keys_set[k] = 1
 
        all_keys = keys_set.keys()
 

	
 
        xfaded_sub = Submaster("xfade", {})
 
        for k in all_keys:
 
            xfaded_sub.set_level(k, 
 
            xfaded_sub.set_level(k,
 
                                 linear_fade(self.levels.get(k, 0),
 
                                             otherlevels.get(k, 0),
 
                                             amount))
 

	
 
        return xfaded_sub
 

	
 
class PersistentSubmaster(Submaster):
 
    def __init__(self, graph, uri):
 
        self.graph, self.uri = graph, uri
 
        self.graph.addHandler(self.setName)
 
        self.graph.addHandler(self.setLevels)
 
        Submaster.__init__(self, self.name, self.levels)
 
        self.graph = graph
 
        self.uri = uri
 
        self.temporary = False
 

	
 
    def ident(self):
 
        return self.uri
 
        
 

	
 
    def _editedLevels(self):
 
        self.save()
 
        
 

	
 
    def setName(self):
 
        log.info("sub update name %s %s", self.uri, self.graph.label(self.uri))
 
        self.name = self.graph.label(self.uri)
 
        
 

	
 
    def setLevels(self):
 
        log.info("sub update levels")
 
        oldLevels = getattr(self, 'levels', {}).copy()
 
        self.setLevelsFromGraph()
 
        if oldLevels != self.levels:
 
            log.info("sub %s changed" % self.name)
 
        
 

	
 
    def setLevelsFromGraph(self):
 
        if hasattr(self, 'levels'):
 
            self.levels.clear()
 
        else:
 
            self.levels = {}
 
        for lev in self.graph.objects(self.uri, L9['lightLevel']):
 
            chan = self.graph.value(lev, L9['channel'])
 

	
 
            name = self.graph.label(chan)
 
            if not name:
 
                log.error("sub %r has channel %r with no name- leaving out that channel" % (self.name, chan))
 
                continue
 
            val = self.graph.value(lev, L9['level'])
 
            self.levels[name] = float(val)
 

	
 
    def save(self):
 
        if self.temporary:
 
            log.info("not saving temporary sub named %s",self.name)
 
            return
 

	
 
        graph = Graph()
 
        subUri = L9['sub/%s' % self.name]
 
        graph.add((subUri, RDFS.label, Literal(self.name)))
 
        for chan in self.levels.keys():
 
            try:
 
                chanUri = Patch.get_channel_uri(chan)
 
            except KeyError:
 
                log.error("saving dmx channels with no :Channel node is not supported yet. Give channel %s a URI for it to be saved. Omitting this channel from the sub." % chan)
 
                continue
 
            lev = BNode()
 
            graph.add((subUri, L9['lightLevel'], lev))
 
            graph.add((lev, L9['channel'], chanUri))
 
            graph.add((lev, L9['level'],
 
                       Literal(self.levels[chan], datatype=XSD['decimal'])))
 

	
 
        graph.serialize(showconfig.subFile(self.name), format="nt")
 

	
 
                                            
 

	
 
def linear_fade(start, end, amount):
 
    """Fades between two floats by an amount.  amount is a float between
 
    0 and 1.  If amount is 0, it will return the start value.  If it is 1,
 
    the end value will be returned."""
 
    level = start + (amount * (end - start))
 
    return level
 

	
 
def sub_maxes(*subs):
 
    nonzero_subs = [s for s in subs if not s.no_nonzero()]
 
    name = "max(%s)" % ", ".join([repr(s) for s in nonzero_subs])
 
    return Submaster(name,
 
                     levels=dict_max(*[sub.levels for sub in nonzero_subs]))
 

	
 
def combine_subdict(subdict, name=None, permanent=False):
 
    """A subdict is { Submaster objects : levels }.  We combine all
 
    submasters first by multiplying the submasters by their corresponding
 
    levels and then max()ing them together.  Returns a new Submaster
 
    object.  You can give it a better name than the computed one that it
 
    will get or make it permanent if you'd like it to be saved to disk.
 
    Serves 8."""
 
    scaledsubs = [sub * level for sub, level in subdict.items()]
 
    maxes = sub_maxes(*scaledsubs)
 
    if name:
 
        maxes.name = name
 
    if permanent:
 
        maxes.temporary = False
 

	
 
    return maxes
 

	
 
class Submasters:
 
    "Collection o' Submaster objects"
 
    def __init__(self, graph):
 
        self.submasters = {}
 
        self.graph = graph
 
        
 

	
 
        graph.addHandler(self.findSubs)
 

	
 
    def findSubs(self):
 
        current = set()
 

	
 
        for s in self.graph.subjects(RDF.type, L9['Submaster']):
 
            log.info("found sub %s", s)
 
            if s not in self.submasters:
 
                sub = self.submasters[s] = PersistentSubmaster(self.graph, s)
 
                dispatcher.send("new submaster", sub=sub)
 
                current.add(s)
 
        for s in set(self.submasters.keys()) - current:
 
            del self.submasters[s]
 
            dispatcher.send("lost submaster", subUri=s)
 
        log.info("findSubs finished %s", self.submasters)
 

	
 
    def get_all_subs(self):
 
        "All Submaster objects"
 
        l = self.submasters.items()
 
        l.sort()
 
        l = [x[1] for x in l]
 
        songs = []
 
        notsongs = []
 
        for s in l:
 
            if s.name and s.name.startswith('song'):
 
                songs.append(s)
 
            else:
 
                notsongs.append(s)
 
        combined = notsongs + songs
 

	
 
        return combined
 

	
 
    def get_sub_by_uri(self, uri):
 
        return self.submasters[uri]
 

	
 
def fullsub(*chans):
 
    """Make a submaster with chans at full."""
 
    return Submaster('%r' % chans,
 
        leveldict=dict([(c, 1.0) for c in chans]), temporary=True)
 

	
 
# a global instance of Submasters, created on demand
 
_submasters = None
 

	
 
def get_global_submasters(graph):
 
    """Get (and make on demand) the global instance of Submasters."""
 
    global _submasters
 
    if _submasters is None:
 
        _submasters = Submasters(graph)
light9/dmxchanedit.py
Show inline comments
 
@@ -6,159 +6,159 @@ not actually match what dmxserver is out
 
proposal for new focus and edit system:
 
- rows can be selected
 
- the chan number or label can be used to select rows. dragging over rows brings all of them into or out of the current selection
 
- numbers drag up and down (like today)
 
- if you drag a number in a selected row, all the selected numbers change
 
- if you start dragging a number in an unselected row, your row becomes the new selection and then the edit works
 

	
 

	
 
proposal for new attribute system:
 
- we always want to plan some attributes for each light: where to center; what stage to cover; what color gel to apply; whether the light is burned out
 
- we have to stop packing these into the names. Names should be like 'b33' or 'blue3' or just '44'. maybe 'blacklight'.
 

	
 
"""
 
from __future__ import nested_scopes,division
 
import Tkinter as tk
 
from rdflib import RDF
 
from light9.namespaces import L9
 
import louie as dispatcher
 

	
 
stdfont = ('Arial', 9)
 

	
 
def gradient(lev, low=(80,80,180), high=(255,55,50)):
 
     out = [int(l+lev*(h-l)) for h,l in zip(high,low)]
 
     col="#%02X%02X%02X" % tuple(out)
 
     return col
 

	
 
class Onelevel(tk.Frame):
 
    """a name/level pair
 

	
 
    source data is like this:
 
        ch:b11-c     a :Channel;
 
         :output dmx:c54;
 
         rdfs:label "b11-c" .
 
    """
 
    def __init__(self, parent, graph, channelUri):
 
        tk.Frame.__init__(self,parent, height=20)
 
        self.graph = graph
 
        self.uri = channelUri
 
        self.currentlevel = 0 # the level we're displaying, 0..1
 

	
 
        # no statement yet
 
        self.channelnum = int(
 
             self.graph.value(self.uri, L9['output']).rsplit('/c')[-1])
 

	
 
        # 3 widgets, left-to-right:
 

	
 
        # channel number -- will turn yellow when being altered
 
        self.num_lab = tk.Label(self, text=str(self.channelnum),
 
                                width=3, bg='grey40', 
 
                                width=3, bg='grey40',
 
                                fg='white',
 
                                font=stdfont,
 
                                padx=0, pady=0, bd=0, height=1)
 
        self.num_lab.pack(side='left')
 

	
 
        # text description of channel
 
        self.desc_lab=tk.Label(self,
 
                               width=14,
 
                               font=stdfont,
 
                               anchor='w',
 
                               padx=0, pady=0, bd=0, 
 
                               padx=0, pady=0, bd=0,
 
                 height=1, bg='black', fg='white')
 
        self.graph.addHandler(self.updateLabel)
 
        self.desc_lab.pack(side='left')
 

	
 
        # current level of channel, shows intensity with color
 
        self.level_lab = tk.Label(self, width=3, bg='lightBlue',
 
                                  anchor='e', font=stdfont,
 
                                  padx=1, pady=0, bd=0, height=1)
 
        self.level_lab.pack(side='left')
 

	
 
        self.setlevel(0)
 
        self.setupmousebindings()
 

	
 
    def updateLabel(self):
 
         self.desc_lab.config(text=self.graph.label(self.uri))
 
        
 

	
 
    def setupmousebindings(self):
 
        def b1down(ev):
 
            self.desc_lab.config(bg='cyan')
 
            self._start_y=ev.y
 
            self._start_lev=self.currentlevel
 
        def b1motion(ev):
 
            delta=self._start_y-ev.y
 
            self.setlevel(self._start_lev+delta*.005)
 
        def b1up(ev):
 
            self.desc_lab.config(bg='black')
 
        def b3up(ev):
 
            self.setlevel(0.0)
 
        def b3down(ev):
 
            self.setlevel(1.0)
 
        def b2down(ev): # same thing for now
 
            self.setlevel(1.0)
 

	
 
        # make the buttons work in the child windows
 
        for w in self.winfo_children():
 
            for e,func in (('<ButtonPress-1>',b1down),
 
                           ('<B1-Motion>',b1motion),
 
                           ('<ButtonRelease-1>',b1up),
 
                           ('<ButtonPress-2>', b2down),
 
                           ('<ButtonRelease-3>', b3up),
 
                           ('<ButtonPress-3>', b3down)):
 

	
 
                w.bind(e,func)
 
        
 

	
 
    def colorlabel(self):
 
        """color the level label based on its own text (which is 0..100)"""
 
        txt=self.level_lab['text'] or "0"
 
        lev=float(txt)/100
 
        self.level_lab.config(bg=gradient(lev))
 

	
 
    def setlevel(self, newlev):
 
        """the main program is telling us to change our
 
        display. newlev is 0..1"""
 
        self.currentlevel = min(1, max(0, newlev))
 
        newlev = "%d" % (self.currentlevel * 100)
 
        olddisplay=self.level_lab.cget('text')
 
        if newlev!=olddisplay:
 
            self.level_lab.config(text=newlev)
 
            self.colorlabel()
 
        dispatcher.send("levelchanged", channel=self.uri, newlevel=newlev)
 

	
 
class Levelbox(tk.Frame):
 
    def __init__(self, parent, graph):
 
        tk.Frame.__init__(self,parent)
 

	
 
        self.graph = graph
 
        graph.addHandler(self.updateChannels)
 

	
 
    def updateChannels(self):
 
        """(re)make Onelevel boxes for the defined channels"""
 

	
 
        [ch.destroy() for ch in self.winfo_children()]
 
        self.levels = [] # Onelevel objects
 

	
 
        chans = list(self.graph.subjects(RDF.type, L9.Channel))
 
        chans.sort(key=lambda c: int(self.graph.value(c, L9.output).rsplit('/c')[-1]))
 
        cols = 2
 
        rows = len(chans) // cols
 

	
 
        def make_frame(parent):
 
             f = tk.Frame(parent, bd=0, bg='black')
 
             f.pack(side='left')
 
             return f
 
        
 

	
 
        columnFrames = [make_frame(self) for x in range(cols)]
 

	
 
        for i, channel in enumerate(chans): # sort?
 
            # frame for this channel
 
            f = Onelevel(columnFrames[i // rows], self.graph, channel)
 

	
 
            self.levels.append(f)
 
            f.pack(side='top')
 
        #dispatcher.connect(setalevel,"setlevel")
 

	
 
    def setlevels(self,newlevels):
 
        """sets levels to the new list of dmx levels (0..1). list can
 
        be any length"""
 
        for l,newlev in zip(self.levels,newlevels):
 
            l.setlevel(newlev)
light9/rdfdb/rdflibpatch.py
Show inline comments
 
@@ -23,140 +23,140 @@ def patchQuads(graph, deleteQuads, addQu
 
        if perfect:
 
            if not any(graph.store.triples(stmt, c)):
 
                raise ValueError("%r not in %r" % (stmt, c))
 
            else:
 
                toDelete.append((c, stmt))
 
        else:
 
            graph.store.remove(stmt, context=c)
 
    for c, stmt in toDelete:
 
        graph.store.remove(stmt, context=c)
 

	
 
    if perfect:
 
        addQuads = list(addQuads)
 
        for spoc in addQuads:
 
            if spoc in graph:
 
                raise ValueError("%r already in %r" % (spoc[:3], spoc[3]))
 
    graph.addN(addQuads)
 

	
 

	
 

	
 
def graphFromQuads(q):
 
    g = ConjunctiveGraph()
 
    #g.addN(q) # no effect on nquad output
 
    for s,p,o,c in q:
 
        #g.get_context(c).add((s,p,o)) # kind of works with broken rdflib nquad serializer code
 
        g.store.add((s,p,o), c) # no effect on nquad output
 
    return g
 

	
 
def graphFromNQuad(text):
 
    """
 
    g.parse(data=self.nqOut, format='nquads')
 
    makes a graph that serializes to nothing
 
    """
 
    g1 = ConjunctiveGraph()
 
    g1.parse(data=text, format='nquads')
 
    g2 = ConjunctiveGraph()
 
    for s,p,o,c in g1.quads((None,None,None)):
 
        #g2.get_context(c).add((s,p,o))
 
        g2.store.add((s,p,o), c)
 
    #import pprint; pprint.pprint(g2.store.__dict__)
 
    return g2
 

	
 
from rdflib.plugins.serializers.nt import _xmlcharref_encode
 
def serializeQuad(g):
 
    """replacement for graph.serialize(format='nquads')"""
 
    out = ""
 
    for s,p,o,c in g.quads((None,None,None)):
 
        out += u"%s %s %s %s .\n" % (s.n3(),
 
                                p.n3(),
 
                                _xmlcharref_encode(o.n3()), 
 
                                _xmlcharref_encode(o.n3()),
 
                                c.n3())
 
    return out
 

	
 
def inContext(graph, newContext):
 
    """
 
    make a ConjunctiveGraph where all the triples in the given graph
 
    are in newContext
 
    """
 
    return graphFromQuads([(s,p,o,newContext) for s,p,o in graph])
 

	
 
def contextsForStatement(graph, triple):
 
    return [q[3] for q in graph.quads(triple)]
 

	
 

	
 
A = U("http://a"); B = U("http://b")
 
class TestContextsForStatement(unittest.TestCase):
 
    def testNotFound(self):
 
        g = graphFromQuads([(A,A,A,A)])
 
        self.assertEqual(contextsForStatement(g, (B,B,B)), [])
 
    def testOneContext(self):
 
        g = graphFromQuads([(A,A,A,A), (A,A,B,B)])
 
        self.assertEqual(contextsForStatement(g, (A,A,A)), [A])
 
    def testTwoContexts(self):
 
        g = graphFromQuads([(A,A,A,A), (A,A,A,B)])
 
        self.assertEqual(sorted(contextsForStatement(g, (A,A,A))), sorted([A,B]))
 

	
 

	
 

	
 
class TestGraphFromQuads(unittest.TestCase):
 
    nqOut = '<http://example.com/> <http://example.com/> <http://example.com/> <http://example.com/> .\n'
 
    def testSerializes(self):
 
        n = U("http://example.com/")
 
        g = graphFromQuads([(n,n,n,n)])
 
        out = serializeQuad(g)
 
        self.assertEqual(out.strip(), self.nqOut.strip())
 

	
 
    def testNquadParserSerializes(self):
 
        g = graphFromNQuad(self.nqOut)
 
        self.assertEqual(len(g), 1)
 
        out = serializeQuad(g)
 
        self.assertEqual(out.strip(), self.nqOut.strip())
 
        
 

	
 

	
 
stmt1 = U('http://a'), U('http://b'), U('http://c'), U('http://ctx1')
 
stmt2 = U('http://a'), U('http://b'), U('http://c'), U('http://ctx2')
 
class TestPatchQuads(unittest.TestCase):
 
    def testAddsToNewContext(self):
 
        g = ConjunctiveGraph()
 
        patchQuads(g, [], [stmt1])
 
        self.assert_(len(g), 1)
 
        quads = list(g.quads((None,None,None)))
 
        self.assertEqual(quads, [stmt1])
 

	
 
    def testDeletes(self):
 
        g = ConjunctiveGraph()
 
        patchQuads(g, [], [stmt1])
 
        patchQuads(g, [stmt1], [])
 
        quads = list(g.quads((None,None,None)))
 
        self.assertEqual(quads, [])
 

	
 
    def testDeleteRunsBeforeAdd(self):
 
        g = ConjunctiveGraph()
 
        patchQuads(g, [stmt1], [stmt1])
 
        quads = list(g.quads((None,None,None)))
 
        self.assertEqual(quads, [stmt1])
 
        
 

	
 
    def testPerfectAddRejectsExistingStmt(self):
 
        g = ConjunctiveGraph()
 
        patchQuads(g, [], [stmt1])
 
        self.assertRaises(ValueError, patchQuads, g, [], [stmt1], perfect=True)
 

	
 
    def testPerfectAddAllowsExistingStmtInNewContext(self):
 
        g = ConjunctiveGraph()
 
        patchQuads(g, [], [stmt1])
 
        patchQuads(g, [], [stmt2], perfect=True)
 
        self.assertEqual(len(list(g.quads((None,None,None)))), 2)
 

	
 
    def testPerfectDeleteRejectsAbsentStmt(self):
 
        g = ConjunctiveGraph()
 
        self.assertRaises(ValueError, patchQuads, g, [stmt1], [], perfect=True)
 
        
 

	
 
    def testPerfectDeleteAllowsRemovalOfStmtInMultipleContexts(self):
 
        g = ConjunctiveGraph()
 
        patchQuads(g, [], [stmt1, stmt2])
 
        patchQuads(g, [stmt1], [], perfect=True)
 

	
 
    def testRedundantStmtOkForAddOrDelete(self):
 
        g = ConjunctiveGraph()
 
        patchQuads(g, [], [stmt1, stmt1], perfect=True)
 
        patchQuads(g, [stmt1, stmt1], [], perfect=True)
 
        
 

	
light9/rdfdb/syncedgraph.py
Show inline comments
 
@@ -26,213 +26,213 @@ def sendPatch(putUri, patch, **kw):
 

	
 
def makePatchEndpointPutMethod(cb):
 
    def put(self):
 
        try:
 
            p = Patch(jsonRepr=self.request.body)
 
            log.info("received patch -%d +%d" % (len(p.delGraph), len(p.addGraph)))
 
            cb(p)
 
        except:
 
            traceback.print_exc()
 
            raise
 
    return put
 

	
 
def makePatchEndpoint(cb):
 
    class Update(cyclone.web.RequestHandler):
 
        put = makePatchEndpointPutMethod(cb)
 
    return Update
 

	
 
class GraphWatchers(object):
 
    """
 
    store the current handlers that care about graph changes
 
    """
 
    def __init__(self):
 
        self._handlersSp = {} # (s,p): set(handlers)
 
        self._handlersPo = {} # (p,o): set(handlers)
 

	
 
    def addSubjPredWatcher(self, func, s, p):
 
        if func is None:
 
            return
 
        key = s, p
 
        try:
 
            self._handlersSp.setdefault(key, set()).add(func)
 
        except Exception:
 
            log.error("with key %r and func %r" % (key, func))
 
            raise
 

	
 
    def addPredObjWatcher(self, func, p, o):
 
        self._handlersPo.setdefault((p, o), set()).add(func)
 

	
 
    def whoCares(self, patch):
 
        """what handler functions would care about the changes in this patch?
 

	
 
        this removes the handlers that it gives you
 
        """
 
        #self.dependencies()
 
        affectedSubjPreds = set([(s, p) for s, p, o, c in patch.addQuads]+
 
                                [(s, p) for s, p, o, c in patch.delQuads])
 
        affectedPredObjs = set([(p, o) for s, p, o, c in patch.addQuads]+
 
                                [(p, o) for s, p, o, c in patch.delQuads])
 
        
 

	
 
        ret = set()
 
        for (s, p), funcs in self._handlersSp.iteritems():
 
            if (s, p) in affectedSubjPreds:
 
                ret.update(funcs)
 
                funcs.clear()
 
                
 

	
 
        for (p, o), funcs in self._handlersPo.iteritems():
 
            if (p, o) in affectedPredObjs:
 
                ret.update(funcs)
 
                funcs.clear()
 

	
 
        return ret
 

	
 
    def dependencies(self):
 
        """
 
        for debugging, make a list of all the active handlers and what
 
        data they depend on. This is meant for showing on the web ui
 
        for browsing.
 
        """
 
        log.info("whocares:")
 
        from pprint import pprint
 
        pprint(self._handlersSp)
 
        
 

	
 

	
 
class PatchSender(object):
 
    """
 
    SyncedGraph may generate patches faster than we can send
 
    them. This object buffers and may even collapse patches before
 
    they go the server
 
    """
 
    def __init__(self, target, myUpdateResource):
 
        self.target = target
 
        self.myUpdateResource = myUpdateResource
 
        self._patchesToSend = []
 
        self._currentSendPatchRequest = None
 

	
 
    def sendPatch(self, p):
 
        sendResult = defer.Deferred()
 
        self._patchesToSend.append((p, sendResult))
 
        self._continueSending()
 
        return sendResult
 

	
 
    def _continueSending(self):
 
        if not self._patchesToSend or self._currentSendPatchRequest:
 
            return
 
        if len(self._patchesToSend) > 1:
 
            log.info("%s patches left to send", len(self._patchesToSend))
 
            # this is where we could concatenate little patches into a
 
            # bigger one. Often, many statements will cancel each
 
            # other out. not working yet:
 
            if 0:
 
                p = self._patchesToSend[0].concat(self._patchesToSend[1:])
 
                print "concat down to"
 
                print 'dels'
 
                for q in p.delQuads: print q
 
                print 'adds'
 
                for q in p.addQuads: print q
 
                print "----"
 
            else:
 
                p, sendResult = self._patchesToSend.pop(0)
 
        else:
 
            p, sendResult = self._patchesToSend.pop(0)
 
            
 

	
 
        self._currentSendPatchRequest = sendPatch(
 
            self.target, p, senderUpdateUri=self.myUpdateResource)
 
        self._currentSendPatchRequest.addCallbacks(self._sendPatchDone,
 
                                                   self._sendPatchErr)
 
        self._currentSendPatchRequest.chainDeferred(sendResult)
 

	
 
    def _sendPatchDone(self, result):
 
        self._currentSendPatchRequest = None
 
        self._continueSending()
 

	
 
    def _sendPatchErr(self, e):
 
        self._currentSendPatchRequest = None
 
        # we're probably out of sync with the master now, since
 
        # SyncedGraph.patch optimistically applied the patch to our
 
        # local graph already. What happens to this patch? What
 
        # happens to further pending patches? Some of the further
 
        # patches, especially, may be commutable with the bad one and
 
        # might still make sense to apply to the master graph.
 

	
 
        # if someday we are folding pending patches together, this
 
        # would be the time to UNDO that and attempt the original
 
        # separate patches again
 

	
 
        # this should screen for 409 conflict responses and raise a
 
        # special exception for that, so SyncedGraph.sendFailed can
 
        # screen for only that type
 

	
 
        # this code is going away; we're going to raise an exception that contains all the pending patches
 
        log.error("_sendPatchErr")
 
        log.error(e)
 
        self._continueSending()
 
        
 

	
 

	
 
class SyncedGraph(object):
 
    """
 
    graph for clients to use. Changes are synced with the master graph
 
    in the rdfdb process.
 

	
 
    This api is like rdflib.Graph but it can also call you back when
 
    there are graph changes to the parts you previously read.
 

	
 
    If we get out of sync, we abandon our local graph (even any
 
    pending local changes) and get the data again from the
 
    server.
 
    """
 
    def __init__(self, label):
 
        """
 
        label is a string that the server will display in association
 
        with your connection
 
        """
 
        _graph = self._graph = ConjunctiveGraph()
 
        self._watchers = GraphWatchers()
 
        
 

	
 
        def onPatch(p):
 
            """
 
            central server has sent us a patch
 
            """
 
            patchQuads(_graph, p.delQuads, p.addQuads, perfect=True)
 
            log.info("graph now has %s statements" % len(_graph))
 
            try:
 
                self.updateOnPatch(p)
 
            except Exception:
 
                # don't reflect this back to the server; we did
 
                # receive its patch correctly.
 
                traceback.print_exc()
 

	
 
        listen = reactor.listenTCP(0, cyclone.web.Application(handlers=[
 
            (r'/update', makePatchEndpoint(onPatch)),
 
        ]))
 
        port = listen._realPortNumber  # what's the right call for this?
 
        self.updateResource = 'http://localhost:%s/update' % port
 
        log.info("listening on %s" % port)
 
        self.register(label)
 
        self.currentFuncs = [] # stack of addHandler callers
 
        self._sender = PatchSender('http://localhost:8051/patches',
 
                                   self.updateResource)
 

	
 
    def resync(self):
 
        """
 
        get the whole graph again from the server (e.g. we had a
 
        conflict while applying a patch and want to return to the
 
        truth).
 

	
 
        To avoid too much churn, we remember our old graph and diff it
 
        against the replacement. This way, our callers only see the
 
        corrections.
 

	
 
        Edits you make during a resync will surely be lost, so I
 
        should just fail them. There should be a notification back to
 
        UIs who want to show that we're doing a resync.
 
        """
 
        return cyclone.httpclient.fetch(
 
            url="http://localhost:8051/graph",
 
            method="GET",
 
            headers={'Accept':'x-trig'},
 
            ).addCallback(self._resyncGraph)
 

	
 
    def _resyncGraph(self, response):
 
        pass
 
        #diff against old entire graph
 
        #broadcast that change
 
@@ -295,100 +295,100 @@ class SyncedGraph(object):
 
        """
 

	
 
    def addHandler(self, func):
 
        """
 
        run this (idempotent) func, noting what graph values it
 
        uses. Run it again in the future if there are changes to those
 
        graph values. The func might use different values during that
 
        future call, and those will be what we watch for next.
 
        """
 

	
 
        # if we saw this func before, we need to forget the old
 
        # callbacks it wanted and replace with the new ones we see
 
        # now.
 

	
 
        # if one handler func calls another, does that break anything?
 
        # maybe not?
 

	
 
        # no plan for sparql queries yet. Hook into a lower layer that
 
        # reveals all their statement fetches? Just make them always
 
        # new? Cache their results, so if i make the query again and
 
        # it gives the same result, I don't call the handler?
 

	
 
        self.currentFuncs.append(func)
 
        try:
 
            func()
 
        finally:
 
            self.currentFuncs.pop()
 

	
 
    def updateOnPatch(self, p):
 
        """
 
        patch p just happened to the graph; call everyone back who
 
        might care, and then notice what data they depend on now
 
        """
 
        for func in self._watchers.whoCares(p):
 
            # todo: forget the old handlers for this func
 
            self.addHandler(func)
 

	
 
    def currentState(self, context=None):
 
        """
 
        a graph you can read without being in an addHandler
 
        """
 
        class Mgr(object):
 
            def __enter__(self2):
 
                # this should be a readonly view of the existing graph
 
                g = Graph()
 
                for s in self._graph.triples((None, None, None), context):
 
                    g.add(s)
 
                return g
 
            
 

	
 
            def __exit__(self, type, val, tb):
 
                return
 

	
 
        return Mgr()
 

	
 
    def _getCurrentFunc(self):
 
        if not self.currentFuncs:
 
            # this may become a warning later
 
            raise ValueError("asked for graph data outside of a handler")
 

	
 
        # we add the watcher to the deepest function, since that
 
        # should be the cheapest way to update when this part of the
 
        # data changes
 
        return self.currentFuncs[-1]
 

	
 
    # these just call through to triples() so it might be possible to
 
    # watch just that one.
 

	
 
    # if you get a bnode in your response, maybe the answer to
 
    # dependency tracking is to say that you depended on the triple
 
    # that got you that bnode, since it is likely to change to another
 
    # bnode later. This won't work if the receiver stores bnodes
 
    # between calls, but probably most of them don't do that (they
 
    # work from a starting uri)
 
    
 

	
 
    def value(self, subject=None, predicate=RDF.value, object=None,
 
              default=None, any=True):
 
        if object is not None:
 
            raise NotImplementedError()
 
        func = self._getCurrentFunc()
 
        self._watchers.addSubjPredWatcher(func, subject, predicate)
 
        return self._graph.value(subject, predicate, object=object,
 
                                 default=default, any=any)
 

	
 
    def objects(self, subject=None, predicate=None):
 
        func = self._getCurrentFunc()
 
        self._watchers.addSubjPredWatcher(func, subject, predicate)
 
        return self._graph.objects(subject, predicate)
 
    
 

	
 
    def label(self, uri):
 
        return self.value(uri, RDFS.label)
 

	
 
    def subjects(self, predicate=None, object=None):
 
        func = self._getCurrentFunc()
 
        self._watchers.addPredObjWatcher(func, predicate, object)
 
        return self._graph.subjects(predicate, object)
 

	
 
    # i find myself wanting 'patch' versions of these calls that tell
 
    # you only what results have just appeared or disappeared. I think
 
    # I'm going to be repeating that logic a lot. Maybe just for the
 
    # subjects(RDF.type, t) call
light9/tkdnd.py
Show inline comments
 
@@ -77,65 +77,65 @@ def dragSourceRegister(widget,
 
    widget.tk.call('tkdnd::drag_source', 'register', widget._w)
 

	
 
    # with normal Tkinter bind(), the result of your handler isn't
 
    # actually returned so the drag doesn't get launched. This is a
 
    # corrected version of what bind() does when you pass a function,
 
    # but I don't block my tuple from getting returned (as a tcl list)
 

	
 
    def init():
 
        return (action, datatype, data() if callable(data) else data)
 
    
 
    funcId = widget._register(init, 
 
                              widget._substitute,
 
                              1 # needscleanup
 
                              )
 
    widget.bind("<<DragInitCmd>>", funcId)
 

	
 
def dropTargetRegister(widget, typeList=None,
 
                       onDropEnter=None,
 
                       onDropPosition=None,
 
                       onDropLeave=None,
 
                       onDrop=None,
 
                       hoverStyle=None,
 
                       ):
 
    """
 
    the optional callbacks will be called with a TkdndEvent
 
    argument.
 

	
 
    onDropEnter, onDropPosition, and onDrop are supposed to return an
 
    action (perhaps the value in event.action). The return value seems
 
    to have no effect, but it might just be that errors are getting
 
    silenced.
 

	
 
    Passing hoverStyle sets onDropEnter to call
 
    widget.configure(**hoverStyle) and onDropLeave to restore the
 
    widget's style. onDrop is also wrapped to do a restore.
 
    """
 

	
 
    if hoverStyle is not None:
 
        hover = Hover(widget, hoverStyle)
 
        def wrappedDrop(ev):
 
            hover.restore(ev)
 
            if onDrop:
 
                return onDrop(ev)
 
        return dropTargetRegister(widget, typeList=typeList,
 
                                  onDropEnter=hover.set,
 
                                  onDropLeave=hover.restore,
 
                                  onDropPosition=onDropPosition,
 
                                  onDrop=wrappedDrop)
 
    
 

	
 
    if typeList is None:
 
        typeList = ['*']
 
    widget.tk.call(*(['tkdnd::drop_target', 'register', widget._w]+typeList))
 

	
 
    for sequence, handler in [
 
        ('<<DropEnter>>', onDropEnter),
 
        ('<<DropPosition>>', onDropPosition),
 
        ('<<DropLeave>>', onDropLeave),
 
        ('<<Drop>>', onDrop),
 
        ]:
 
        if not handler:
 
            continue
 
        func = widget._register(handler, subst=TkdndEvent.makeEvent, needcleanup=1)
 
        widget.bind(sequence, func + " " + TkdndEvent.tclSubstitutions)
 

	
 

	
light9/uihelpers.py
Show inline comments
 
@@ -10,245 +10,246 @@ windowlocations = {
 
    'sub' : '425x738+00+00',
 
    'console' : '168x24+848+000',
 
    'leveldisplay' : '144x340+870+400',
 
    'cuefader' : '314x212+546+741',
 
    'effect' : '24x24+0963+338',
 
    'stage' : '823x683+37+030',
 
    'scenes' : '504x198+462+12',
 
}
 

	
 
def bindkeys(root,key, func):
 
    root.bind(key, func)
 
    for w in root.winfo_children():
 
        w.bind(key, func)
 

	
 

	
 
def toplevel_savegeometry(tl,name):
 
    try:
 
        geo = tl.geometry()
 
        if not geo.startswith("1x1"):
 
            f=open(".light9-window-geometry-%s" % name.replace(' ','_'),'w')
 
            f.write(tl.geometry())
 
        # else the window never got mapped
 
    except Exception, e:
 
        # it's ok if there's no saved geometry
 
        pass
 

	
 
def toplevelat(name, existingtoplevel=None, graph=None, session=None):
 
    tl = existingtoplevel or Toplevel()
 
    tl.title(name)
 

	
 
    lastSaved = [None]
 
    setOnce = [False]
 
    def setPosFromGraphOnce():
 
        """
 
        the graph is probably initially empty, but as soon as it gives
 
        us one window position, we stop reading them
 
        """
 
        if setOnce[0]:
 
            return
 
        geo = graph.value(session, L9.windowGeometry)
 

	
 
        if geo is not None and geo != lastSaved[0]:
 
            setOnce[0] = True
 
            tl.geometry(geo)
 
            lastSaved[0] = geo
 

	
 
    def savePos():
 
        geo = tl.geometry()
 

	
 
        # todo: need a way to filter out the startup window sizes that
 
        # weren't set by the user
 
        if geo.startswith("1x1") or geo.startswith(("378x85", "378x86")):
 
            return
 
        if geo == lastSaved[0]:
 
            return
 
        lastSaved[0] = geo
 
        graph.patchObject(session, session, L9.windowGeometry, Literal(geo))
 

	
 
    if graph is not None and session is not None:
 
        graph.addHandler(setPosFromGraphOnce)
 

	
 
    if name in windowlocations:
 
        tl.geometry(positionOnCurrentDesktop(windowlocations[name]))
 

	
 
    if graph is not None:
 
        tl._toplevelat_funcid = tl.bind("<Configure>",lambda ev,tl=tl,name=name: savePos())
 

	
 
    return tl
 

	
 
def positionOnCurrentDesktop(xform, screenWidth=1920, screenHeight=1440):
 
    size, x, y = xform.split('+')
 
    x = int(x) % screenWidth
 
    y = int(y) % screenHeight
 
    return "%s+%s+%s" % (size, x, y)
 
    
 

	
 

	
 
def toggle_slider(s):
 
    if s.get() == 0:
 
        s.set(100)
 
    else:
 
        s.set(0)
 

	
 
# for lambda callbacks    
 
# for lambda callbacks
 
def printout(t):
 
    print t
 

	
 
def printevent(ev):
 
    for k in dir(ev):
 
        if not k.startswith('__'):
 
            print k,getattr(ev,k)
 
    print ""
 
    
 

	
 
def eventtoparent(ev,sequence):
 
    "passes an event to the parent, screws up TixComboBoxes"
 

	
 
    wid_class = str(ev.widget.__class__)
 
    if wid_class == 'Tix.ComboBox' or wid_class == 'Tix.TixSubWidget':
 
        return
 

	
 
    evdict={}
 
    for x in ['state', 'time', 'y', 'x', 'serial']:
 
        evdict[x]=getattr(ev,x)
 
#    evdict['button']=ev.num
 
    par=ev.widget.winfo_parent()
 
    if par!=".":
 
        ev.widget.nametowidget(par).event_generate(sequence,**evdict)
 
    #else the event made it all the way to the top, unhandled
 

	
 
def colorlabel(label):
 
    """color a label based on its own text"""
 
    txt=label['text'] or "0"
 
    lev=float(txt)/100
 
    low=(80,80,180)
 
    high=(255,55,050)
 
    out = [int(l+lev*(h-l)) for h,l in zip(high,low)]
 
    col="#%02X%02X%02X" % tuple(out)
 
    label.config(bg=col)
 

	
 
# TODO: get everyone to use this
 
def colorfade(low, high, percent):
 
    '''not foolproof.  make sure 0 < percent < 1'''
 
    out = [int(l+percent*(h-l)) for h,l in zip(high,low)]
 
    col="#%02X%02X%02X" % tuple(out)
 
    return col
 

	
 
def colortotuple(anytkobj, colorname):
 
    'pass any tk object and a color name, like "yellow"'
 
    rgb = anytkobj.winfo_rgb(colorname)
 
    return [v / 256 for v in rgb]
 

	
 
class Togglebutton(Button):
 
    """works like a single radiobutton, but it's a button so the
 
    label's on the button face, not to the side. the optional command
 
    callback is called on button set, not on unset. takes a variable
 
    just like a checkbutton"""
 
    def __init__(self,parent,variable=None,command=None,downcolor='red',**kw):
 

	
 
        self.oldcommand = command
 
        Button.__init__(self,parent,command=self.invoke,**kw)
 

	
 
        self._origbkg = self.cget('bg')
 
        self.downcolor = downcolor
 

	
 
        self._variable = variable
 
        if self._variable:
 
            self._variable.trace('w',self._varchanged)
 
            self._setstate(self._variable.get())
 
        else:
 
            self._setstate(0)
 

	
 
        self.bind("<Return>",self.invoke)
 
        self.bind("<1>",self.invoke)
 
        self.bind("<space>",self.invoke)
 

	
 
    def _varchanged(self,*args):
 
        self._setstate(self._variable.get())
 
        
 

	
 
    def invoke(self,*ev):
 
        if self._variable:
 
            self._variable.set(not self.state)
 
        else:
 
            self._setstate(not self.state)
 
        
 

	
 
        if self.oldcommand and self.state: # call command only when state goes to 1
 
            self.oldcommand()
 
        return "break"
 

	
 
    def _setstate(self,newstate):
 
        self.state = newstate
 
        if newstate: # set
 
            self.config(bg=self.downcolor,relief='sunken')
 
        else: # unset
 
            self.config(bg=self._origbkg,relief='raised')
 
        return "break"
 

	
 

	
 
class FancyDoubleVar(DoubleVar):
 
    def __init__(self,master=None):
 
        DoubleVar.__init__(self,master)
 
        self.callbacklist = {} # cbname : mode
 
        self.namedtraces = {} # name : cbname
 
    def trace_variable(self,mode,callback):
 
        """Define a trace callback for the variable.
 

	
 
        MODE is one of "r", "w", "u" for read, write, undefine.
 
        CALLBACK must be a function which is called when
 
        the variable is read, written or undefined.
 

	
 
        Return the name of the callback.
 
        """
 
        cbname = self._master._register(callback)
 
        self._tk.call("trace", "variable", self._name, mode, cbname)
 
        
 

	
 
        # we build a list of the trace callbacks (the py functrions and the tcl functionnames)
 
        self.callbacklist[cbname] = mode
 
#        print "added trace:",callback,cbname
 
        
 

	
 
        return cbname
 
    trace=trace_variable
 
    def disable_traces(self):
 
        for cb,mode in self.callbacklist.items():
 
#            DoubleVar.trace_vdelete(self,v[0],k)
 
            self._tk.call("trace", "vdelete", self._name, mode,cb)
 
            # but no master delete!
 
            
 

	
 
    def recreate_traces(self):
 
        for cb,mode in self.callbacklist.items():
 
#            self.trace_variable(v[0],v[1])
 
            self._tk.call("trace", "variable", self._name, mode,cb)
 

	
 
    def trace_named(self, name, callback):
 
        if name in self.namedtraces:
 
            print "FancyDoubleVar: already had a trace named %s - replacing it" % name
 
            self.delete_named(name)
 

	
 
        cbname = self.trace_variable('w',callback) # this will register in self.callbacklist too
 
        
 

	
 
        self.namedtraces[name] = cbname
 
        return cbname
 
        
 

	
 
    def delete_named(self, name):
 
        if name in self.namedtraces:
 

	
 
            cbname = self.namedtraces[name]
 
            
 

	
 
            self.trace_vdelete('w',cbname)
 
	    #self._tk.call("trace","vdelete",self._name,'w',cbname)
 
            print "FancyDoubleVar: successfully deleted trace named %s" % name
 
        else:
 
            print "FancyDoubleVar: attempted to delete named %s which wasn't set to any function" % name
 

	
 
def get_selection(listbox):
 
    'Given a listbox, returns first selection as integer'
 
    selection = int(listbox.curselection()[0]) # blech
 
    return selection
 

	
 
if __name__=='__main__':
 
    root=Tk()
 
    root.tk_focusFollowsMouse()
 
    iv=IntVar()
 
    def cb():
 
        print "cb!"
 
    t = Togglebutton(root,text="testbutton",command=cb,variable=iv)
 
    t.pack()
 
    Entry(root,textvariable=iv).pack()
 
    root.mainloop()
0 comments (0 inline, 0 general)