diff --git a/bin/rdfdb b/bin/rdfdb --- a/bin/rdfdb +++ b/bin/rdfdb @@ -74,6 +74,11 @@ them here. We should be able to take pat and keep updating the same data (e.g. a stream of changes as the user drags a slider) and collapse them into a single edit for clarity. +proposed rule feature: +rdfdb should be able to watch a pair of (sourceFile, rulesFile) and +rerun the rules when either one changes. Should the sourceFile be able +to specify its own rules file? That would be easier configuration. + """ from twisted.internet import reactor import twisted.internet.error diff --git a/bin/subcomposer b/bin/subcomposer --- a/bin/subcomposer +++ b/bin/subcomposer @@ -5,26 +5,21 @@ import sys,os,time,atexit from optparse import OptionParser import Tkinter as tk import louie as dispatcher +from twisted.internet import reactor, tksupport, task import run_local from light9.dmxchanedit import Levelbox -from light9 import dmxclient, Patch, Submaster, showconfig +from light9 import dmxclient, Patch, Submaster, showconfig, prof from light9.uihelpers import toplevelat +from light9.rdfdb.syncedgraph import SyncedGraph class Subcomposer(tk.Frame): - def __init__(self, master, levelboxopts=None, dmxdummy=0, numchannels=72, - use_persistentlevels=0): + def __init__(self, master, graph): tk.Frame.__init__(self, master, bg='black') - self.dmxdummy = dmxdummy - self.numchannels = numchannels + self.graph = graph - self.levels = [0]*numchannels # levels should never get overwritten, just edited - - self.levelbox = Levelbox(self, num_channels=numchannels) + self.levelbox = Levelbox(self, graph) self.levelbox.pack(side='top') - # the dmx levels we edit and output, range is 0..1 (dmx chan 1 is - # the 0 element) - self.levelbox.setlevels(self.levels) self.savebox = EntryCommand(self, cmd=self.savenewsub) self.savebox.pack(side='top') @@ -33,31 +28,17 @@ class Subcomposer(tk.Frame): self.loadbox.pack(side='top') def alltozero(): - self.set_levels([0] * self.numchannels) - dispatcher.send("levelchanged") + for lev in self.levelbox.levels: + lev.setlevel(0) tk.Button(self, text="all to zero", command=alltozero).pack(side='top') - dispatcher.connect(self.levelchanged,"levelchanged") - dispatcher.connect(self.sendupdate,"levelchanged") - - if use_persistentlevels: - self.persistentlevels() - - self.lastupdate=0 # time we last sent to dmx - - self.lastsent=[] # copy of levels + dispatcher.connect(self.sendupdate, "levelchanged") def fill_both_boxes(self, subname): for box in [self.savebox, self.loadbox]: box.set(subname) - def persistentlevels(self): - """adjusts levels from subcomposer.savedlevels, if possible; and - arranges to save the levels in that file upon exit""" - self.load_levels() - atexit.register(self.save_levels) - def save_levels(self, *args): levelfile = file("subcomposer.savedlevels","w") levelfile.write(" ".join(map(str, self.levels))) @@ -70,13 +51,6 @@ class Subcomposer(tk.Frame): except IOError: pass - def levelchanged(self, channel=None, newlevel=None): - if channel is not None and newlevel is not None: - if channel>len(self.levels): - return - self.levels[channel-1]=max(0,min(1,float(newlevel))) - self.levelbox.setlevels(self.levels) - def savenewsub(self, subname): leveldict={} for i,lev in zip(range(len(self.levels)),self.levels): @@ -92,23 +66,20 @@ class Subcomposer(tk.Frame): self.set_levels(s.get_dmx_list()) dispatcher.send("levelchanged") + def toDmxLevels(self): + # the dmx levels we edit and output, range is 0..1 (dmx chan 1 is + # the 0 element) + out = {} + for lev in self.levelbox.levels: + out[lev.channelnum] = lev.currentlevel + if not out: + return [] + + return [out.get(i, 0) for i in range(max(out.keys()) + 1)] + def sendupdate(self): - if not self.dmxdummy: - dmxclient.outputlevels(self.levels) - self.lastupdate = time.time() - self.lastsent = self.levels[:] + dmxclient.outputlevels(self.toDmxLevels(), twisted=True) - def considersendupdate(self, use_after_loop=0): - """If use_after_loop is true, it is the period of the after loop.""" - if self.lastsent != self.levels or time.time() > self.lastupdate + 1: - self.sendupdate() - if use_after_loop: - self.after(use_after_loop, self.considersendupdate, use_after_loop) - - def set_levels(self, levels): - oldLen = len(self.levels) - self.levels[:] = levels + [0] * (oldLen - len(levels)) - dispatcher.send("levelchanged") class EntryCommand(tk.Frame): def __init__(self, master, verb="Save", cmd=None): @@ -131,23 +102,6 @@ class EntryCommand(tk.Frame): self.entry.insert(0, text) -def open_sub_editing_window(subname, use_mainloop=1, dmxdummy=0): - if use_mainloop: - toplevel = tk.Tk() - else: - toplevel = tk.Toplevel() - if dmxdummy: - dummy_str = ' (dummy)' - else: - dummy_str = '' - toplevel.title("Subcomposer: %s%s" % (subname, dummy_str)) - sc = Subcomposer(toplevel, use_persistentlevels=0, dmxdummy=dmxdummy) - sc.pack(fill='both', expand=1) - sc.loadsub(subname) - sc.considersendupdate(use_after_loop=10) - if use_mainloop: - tk.mainloop() - ############################# if __name__ == "__main__": @@ -162,10 +116,9 @@ if __name__ == "__main__": if not opts.no_geometry: toplevelat("subcomposer", root) - sc = Subcomposer(root, dmxdummy=0, - #numchannels=276 # use this to see all the skyline dims - #numchannels=118 - ) + graph = SyncedGraph("subcomposer") + + sc = Subcomposer(root, graph) sc.pack() tk.Label(root,text="Bindings: B1 adjust level; B2 set full; B3 instant bump", @@ -176,11 +129,7 @@ if __name__ == "__main__": sc.loadsub(args[0]) sc.fill_both_boxes(args[0]) - while 1: - try: - root.update() - except tk.TclError: - break + task.LoopingCall(sc.sendupdate).start(1) - sc.considersendupdate() - time.sleep(.01) + tksupport.install(root,ms=10) + prof.run(reactor.run, profile=False) diff --git a/light9/dmxchanedit.py b/light9/dmxchanedit.py --- a/light9/dmxchanedit.py +++ b/light9/dmxchanedit.py @@ -3,11 +3,23 @@ widget to show all dmx channel levels and allow editing. levels might not actually match what dmxserver is outputting. +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 light9 import Patch -from light9.uihelpers import make_frame, colorlabel, eventtoparent +from rdflib import RDF +from light9.namespaces import L9 import louie as dispatcher stdfont = ('Arial', 9) @@ -18,18 +30,27 @@ def gradient(lev, low=(80,80,180), high= return col class Onelevel(tk.Frame): - """a name/level pair""" - def __init__(self, parent, channelnum): - """channelnum is 1..68, like the real dmx""" + """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 - self.channelnum=channelnum - 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(channelnum), + self.num_lab = tk.Label(self, text=str(self.channelnum), width=3, bg='grey40', fg='white', font=stdfont, @@ -37,7 +58,7 @@ class Onelevel(tk.Frame): self.num_lab.pack(side='left') # text description of channel - self.desc_lab=tk.Label(self, text=Patch.get_channel_name(channelnum), + self.desc_lab=tk.Label(self, text=self.graph.label(self.uri), width=14, font=stdfont, anchor='w', @@ -47,16 +68,9 @@ class Onelevel(tk.Frame): # current level of channel, shows intensity with color self.level_lab = tk.Label(self, width=3, bg='lightBlue', - anchor='e', + anchor='e', font=stdfont, padx=1, pady=0, bd=0, height=1) self.level_lab.pack(side='left') - # setting the font in the label somehow makes tk run a low - # slower. Magically, startup is much faster if tk can layout - # the window with some standard font in the rows (so the row - # heights are all fixed and taller?), and then I replace the - # last font. Tk resizes the window faster than you can see, - # but startup is still fast. Very weird. - self.after(1, lambda: self.level_lab.config(font=stdfont)) self.setlevel(0) self.setupmousebindings() @@ -68,15 +82,15 @@ class Onelevel(tk.Frame): self._start_lev=self.currentlevel def b1motion(ev): delta=self._start_y-ev.y - self.changelevel(self._start_lev+delta*.005) + self.setlevel(self._start_lev+delta*.005) def b1up(ev): self.desc_lab.config(bg='black') def b3up(ev): - self.changelevel(0.0) + self.setlevel(0.0) def b3down(ev): - self.changelevel(1.0) + self.setlevel(1.0) def b2down(ev): # same thing for now - self.changelevel(1.0) + self.setlevel(1.0) # make the buttons work in the child windows for w in self.winfo_children(): @@ -88,7 +102,6 @@ class Onelevel(tk.Frame): ('', b3down)): w.bind(e,func) -# w.bind(e,lambda ev,e=e: eventtoparent(ev,e)) def colorlabel(self): """color the level label based on its own text (which is 0..100)""" @@ -96,39 +109,44 @@ class Onelevel(tk.Frame): lev=float(txt)/100 self.level_lab.config(bg=gradient(lev)) - def setlevel(self,newlev): + def setlevel(self, newlev): """the main program is telling us to change our display. newlev is 0..1""" - self.currentlevel=newlev - newlev="%d"%(newlev*100) + 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) - def getlevel(self): - """returns currently displayed level, 0..1""" - return self.currentlevel - - def changelevel(self,newlev): +class Levelbox(tk.Frame): + def __init__(self, parent, graph): + tk.Frame.__init__(self,parent) - """the user is adjusting the level on this widget. the main - program needs to hear about it. then the main program will - call setlevel()""" + self.graph = graph + graph.addHandler(self.updateChannels) - dispatcher.send("levelchanged",channel=self.channelnum,newlevel=newlev) - -class Levelbox(tk.Frame): - def __init__(self, parent, num_channels=68): - tk.Frame.__init__(self,parent) + def updateChannels(self): + """(re)make Onelevel boxes for the defined channels""" + + [ch.destroy() for ch in self.winfo_children()] self.levels = [] # Onelevel objects - rows = 37 - frames = [make_frame(self) for x in range((num_channels // rows) + 1)] + chans = list(self.graph.subjects(RDF.type, L9.Channel)) + cols = 2 + rows = len(chans) // cols - for channel in range(1, num_channels+1): + 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(frames[channel // rows],channel) + f = Onelevel(columnFrames[i // rows], self.graph, channel) self.levels.append(f) f.pack(side='top') diff --git a/light9/uihelpers.py b/light9/uihelpers.py --- a/light9/uihelpers.py +++ b/light9/uihelpers.py @@ -15,11 +15,6 @@ windowlocations = { 'scenes' : '504x198+462+12', } -def make_frame(parent): - f = Frame(parent, bd=0, bg='black') - f.pack(side='left') - return f - def bindkeys(root,key, func): root.bind(key, func) for w in root.winfo_children():