#!bin/python """ subcomposer session observable(currentSub), a Submaster which tracks the graph EditChoice widget can change currentSub to another sub Levelbox widget watch observable(currentSub) for a new sub, and also watch currentSub for edits to push to the OneLevel widgets OneLevel widget UI edits are caught here and go all the way back to currentSub """ from __future__ import division, nested_scopes import time, logging from optparse import OptionParser import logging import Tkinter as tk import louie as dispatcher from twisted.internet import reactor, tksupport, task from rdflib import URIRef from run_local import log from light9.dmxchanedit import Levelbox from light9 import dmxclient, Patch, Submaster, prof from light9.uihelpers import toplevelat from light9.rdfdb.syncedgraph import SyncedGraph from light9.rdfdb import clientsession from light9.tkdnd import initTkdnd, dragSourceRegister, dropTargetRegister class _NoNewVal(object): pass class Observable(object): """ like knockout's observable. Hopefully this can be replaced by a better python one """ def __init__(self, val): self.val = val self.subscribers = set() def __call__(self, newVal=_NoNewVal): if newVal is _NoNewVal: return self.val self.val = newVal for s in self.subscribers: s(newVal) def subscribe(self, cb, callNow=True): """cb is called with new values, and also right now with the current value unless you opt out""" self.subscribers.add(cb) if callNow: cb(self.val) class Local(object): """placeholder for the local uri that EditChoice does not manage. Set resourceObservable to Local to indicate that you're unlinked""" class EditChoice(tk.Frame): """ Observable <-> linker UI widget for tying some UI to a shared resource for editing, or unlinking it (which means associating it with a local resource that's not named or shared). This object does not own the choice of resource; the caller does. """ def __init__(self, parent, graph, resourceObservable): """ getResource is called to get the URI of the currently """ self.frame = tk.Frame(parent, relief='raised',border=8) self.frame.pack(side='top') tk.Label(self.frame, text="Editing:").pack(side='left') self.currentSubFrame = tk.Frame(self.frame) self.currentSubFrame.pack(side='left') self.subIcon = tk.Label(self.currentSubFrame, text="sub1", borderwidth=2, relief='raised', padx=10, pady=10) self.subIcon.pack() self.resourceObservable = resourceObservable resourceObservable.subscribe(self.uriChanged) dragSourceRegister(self.subIcon, 'copy', 'text/uri-list', self.resourceObservable) def onEv(ev): self.resourceObservable(ev.data) return "link" self.onEv = onEv # it would be nice if I didn't receive my own drags here dropTargetRegister(self.subIcon, typeList=["*"], onDrop=onEv, hoverStyle=dict(background="#555500", bd=3, relief='groove')) tk.Label(self.currentSubFrame, text="local data (drag sub here)").pack() b=tk.Button(text="unlink", command=self.switchToLocalSub) b.pack() def uriChanged(self, newUri): print "chg", newUri # i guess i show the label and icon for this if newUri is Local: self.subIcon.config(text="(local)") else: self.subIcon.config(text=newUri) def switchToLocalSub(self): self.resourceObservable(Local) class Subcomposer(tk.Frame): """ l9:currentSub ?sub is the URI of the sub we're tied to for displaying and editing. If we don't have a currentSub, then we're actually editing a session-local sub called l9:currentSub I'm not sure that Locals should even be PersistentSubmaster with uri and graph storage, but I think that way is making fewer special cases. Contains an EditChoice widget Dependencies: graph (?session :currentSub ?s) -> self.currentSub self.currentSub -> graph self.currentSub -> self._currentChoice (which might be Local) self._currentChoice (which might be Local) -> self.currentSub inside the current sub: graph -> Submaster levels (handled in Submaster) Submaster levels -> OneLevel widget OneLevel widget -> Submaster.editLevel Submaster.editLevel -> graph (handled in Submaster) """ def __init__(self, master, graph, session): tk.Frame.__init__(self, master, bg='black') self.graph = graph self.session = session # this is a PersistentSubmaster (even for local) or None if we're not initialized self.currentSub = Observable(None) currentUri = Observable("http://curr") def pc(val): print "change viewed sub to", val currentUri.subscribe(pc) EditChoice(self, self.graph, self._currentChoice).frame.pack(side='top') def setupSubChoiceLinks(self): def ann(): print "currently: session=%s currentSub=%r _currentChoice=%r" % ( self.session, self.currentSub(), self._currentChoice()) @graph.addHandler def graphChanged(): s = graph.value(self.session, L9['currentSub']) if s is None: s = self.makeLocal() self.currentSub(Submaster.PersistentSubmaster(graph, s)) @self.currentSub.subscribe def subChanged(newSub): if newSub is None: graph.patchObject(self.session, self.session, L9['currentSub'], None) return self.sendupdate() graph.patchObject(self.session, self.session, L9['currentSub'], newSub.uri) if newSub and 'local' in newSub.uri: # wrong- use rdf:type or something? self._currentChoice(Local) else: # i think right here is the point that the last local # becomes garbage, and we could clean it up. self._currentChoice(newSub.uri) dispatcher.connect(self.levelsChanged, "sub levels changed") @self._currentChoice.subscribe def choiceChanged(newChoice): if newChoice is Local: newChoice = self.makeLocal() if newChoice is not None: self.currentSub(Submaster.PersistentSubmaster( graph, newChoice)) def levelsChanged(self, sub): if sub == self.currentSub(): self.sendupdate() def makeLocal(self): # todo: put a type on this, so subChanged can identify it right # todo: where will these get stored, or are they local to this # subcomposer process and don't use PersistentSubmaster at all? return URIRef("http://local/%s" % time.time()) def setupLevelboxUi(self): self.levelbox = Levelbox(self, graph, self.currentSub) self.levelbox.pack(side='top') tk.Button(self, text="All to zero", command=lambda *args: self.currentSub().clear()).pack(side='top') def savenewsub(self, subname): leveldict={} for i,lev in zip(range(len(self.levels)),self.levels): if lev!=0: leveldict[Patch.get_channel_name(i+1)]=lev s=Submaster.Submaster(subname,leveldict=leveldict) s.save() def sendupdate(self): d = self.currentSub().get_dmx_list() dmxclient.outputlevels(d, twisted=True) def launch(opts, args, root, graph, session): if not opts.no_geometry: toplevelat("subcomposer - %s" % opts.session, root, graph, session) sc = Subcomposer(root, graph, session) sc.pack() tk.Label(root,text="Bindings: B1 adjust level; B2 set full; B3 instant bump", font="Helvetica -12 italic",anchor='w').pack(side='top',fill='x') if len(args) == 1: # it might be a little too weird that cmdline arg to this # process changes anything else in the same session. But also # I'm not sure who would ever make 2 subcomposers of the same # session (except when quitting and restarting, to get the # same window pos), so maybe it doesn't matter. But still, # this tool should probably default to making new sessions # usually instead of loading the same one def action(self, *args): subname = self.entry.get() self.cmd(subname) print "sub", self.cmd, subname task.LoopingCall(sc.sendupdate).start(10) ############################# if __name__ == "__main__": parser = OptionParser(usage="%prog [suburi]") parser.add_option('--no-geometry', action='store_true', help="don't save/restore window geometry") clientsession.add_option(parser) opts, args = parser.parse_args() logging.basicConfig(level=logging.DEBUG) root=tk.Tk() root.config(bg='black') root.tk_setPalette("#004633") initTkdnd(root.tk, 'tkdnd/trunk/') graph = SyncedGraph("subcomposer") session = clientsession.getUri('subcomposer', opts) graph.initiallySynced.addCallback(lambda _: launch(opts, args, root, graph, session)) root.protocol('WM_DELETE_WINDOW', reactor.stop) tksupport.install(root,ms=10) prof.run(reactor.run, profile=False)