#!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 run_local import log import time, logging log.setLevel(logging.DEBUG) from optparse import OptionParser import logging, urllib.request, urllib.parse, urllib.error import tkinter as tk import louie as dispatcher from twisted.internet import reactor, tksupport, task from rdflib import URIRef, RDF, RDFS, Literal from light9.dmxchanedit import Levelbox from light9 import dmxclient, Submaster, prof, showconfig, networking from light9.Patch import get_channel_name from light9.uihelpers import toplevelat from rdfdb.syncedgraph import SyncedGraph from light9 import clientsession from light9.tkdnd import initTkdnd from light9.namespaces import L9 from rdfdb.patch import Patch from light9.observable import Observable from light9.editchoice import EditChoice, Local from light9.subcomposer import subcomposerweb 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 self.launchTime = time.time() self.localSerial = 0 # this is a URIRef or Local. Strangely, the Local case still # has a uri, which you can get from # self.currentSub.uri. Probably that should be on the Local # object too, or maybe Local should be a subclass of URIRef self._currentChoice = Observable(Local) # this is a PersistentSubmaster (even for local) self.currentSub = Observable( Submaster.PersistentSubmaster(graph, self.switchToLocal())) def pc(val): log.info("change viewed sub to %s", val) self._currentChoice.subscribe(pc) ec = self.editChoice = EditChoice(self, self.graph, self._currentChoice) ec.frame.pack(side='top') ec.subIcon.bind("", self.clickSubIcon) self.setupSubChoiceLinks() self.setupLevelboxUi() def clickSubIcon(self, *args): box = tk.Toplevel(self.editChoice.frame) box.wm_transient(self.editChoice.frame) tk.Label(box, text="Name this sub:").pack() e = tk.Entry(box) e.pack() b = tk.Button(box, text="Make global") b.pack() def clicked(*args): self.makeGlobal(newName=e.get()) box.destroy() b.bind("", clicked) e.focus() def makeGlobal(self, newName): """promote our local submaster into a non-local, named one""" uri = self.currentSub().uri newUri = showconfig.showUri() + ("/sub/%s" % urllib.parse.quote(newName, safe='')) with self.graph.currentState(tripleFilter=(uri, None, None)) as current: if (uri, RDF.type, L9['LocalSubmaster']) not in current: raise ValueError("%s is not a local submaster" % uri) if (newUri, None, None) in current: raise ValueError("new uri %s is in use" % newUri) # the local submaster was storing in ctx=self.session, but now # we want it to be in ctx=uri self.relocateSub(newUri, newName) # these are in separate patches for clarity as i'm debugging this self.graph.patch( Patch(addQuads=[ (newUri, RDFS.label, Literal(newName), newUri), ], delQuads=[ (newUri, RDF.type, L9['LocalSubmaster'], newUri), ])) self.graph.patchObject(self.session, self.session, L9['currentSub'], newUri) def relocateSub(self, newUri, newName): # maybe this goes in Submaster uri = self.currentSub().uri def repl(u): if u == uri: return newUri return u delQuads = self.currentSub().allQuads() addQuads = [(repl(s), p, repl(o), newUri) for s, p, o, c in delQuads] # patch can't span contexts yet self.graph.patch(Patch(addQuads=addQuads, delQuads=[])) self.graph.patch(Patch(addQuads=[], delQuads=delQuads)) def setupSubChoiceLinks(self): graph = self.graph def ann(): print("currently: session=%s currentSub=%r _currentChoice=%r" % (self.session, self.currentSub(), self._currentChoice())) @graph.addHandler def graphChanged(): # some bug where SC is making tons of graph edits and many # are failing. this calms things down. log.warn('skip graphChanged') return s = graph.value(self.session, L9['currentSub']) log.debug('HANDLER getting session currentSub from graph: %s', s) if s is None: s = self.switchToLocal() self.currentSub(Submaster.PersistentSubmaster(graph, s)) @self.currentSub.subscribe def subChanged(newSub): log.debug('HANDLER currentSub changed to %s', 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) localStmt = (newSub.uri, RDF.type, L9['LocalSubmaster']) with graph.currentState(tripleFilter=localStmt) as current: if newSub and localStmt in current: log.debug(' HANDLER set _currentChoice to Local') self._currentChoice(Local) else: # i think right here is the point that the last local # becomes garbage, and we could clean it up. log.debug(' HANDLER set _currentChoice to newSub.uri') self._currentChoice(newSub.uri) dispatcher.connect(self.levelsChanged, "sub levels changed") @self._currentChoice.subscribe def choiceChanged(newChoice): log.debug('HANDLER choiceChanged to %s', newChoice) if newChoice is Local: newChoice = self.switchToLocal() if newChoice is not None: newSub = Submaster.PersistentSubmaster(graph, newChoice) log.debug('write new choice to currentSub, from %r to %r', self.currentSub(), newSub) self.currentSub(newSub) def levelsChanged(self, sub): if sub == self.currentSub(): self.sendupdate() def switchToLocal(self): """ change our display to a local submaster """ # todo: where will these get stored, or are they local to this # subcomposer process and don't use PersistentSubmaster at all? localId = "%s-%s" % (self.launchTime, self.localSerial) self.localSerial += 1 new = URIRef("http://light9.bigasterisk.com/sub/local/%s" % localId) log.debug('making up a local sub %s', new) self.graph.patch( Patch(addQuads=[ (new, RDF.type, L9['Submaster'], self.session), (new, RDF.type, L9['LocalSubmaster'], self.session), ])) return new def setupLevelboxUi(self): self.levelbox = Levelbox(self, 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(list(range(len(self.levels))), self.levels): if lev != 0: leveldict[get_channel_name(i + 1)] = lev s = Submaster.Submaster(subname, levels=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() subcomposerweb.init(graph, session, sc.currentSub) 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 graph.patchObject(session, session, L9['currentSub'], URIRef(args[0])) 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") parser.add_option('-v', action='store_true', help="log debug level") clientsession.add_option(parser) opts, args = parser.parse_args() log.setLevel(logging.DEBUG if opts.v else logging.INFO) root = tk.Tk() root.config(bg='black') root.tk_setPalette("#004633") initTkdnd(root.tk, 'tkdnd/trunk/') graph = SyncedGraph(networking.rdfdb.url, "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)