Mercurial > code > home > repos > light9
diff bin/attic/subcomposer @ 2376:4556eebe5d73
topdir reorgs; let pdm have its src/ dir; separate vite area from light9/
author | drewp@bigasterisk.com |
---|---|
date | Sun, 12 May 2024 19:02:10 -0700 |
parents | bin/subcomposer@5bcb950024af |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/subcomposer Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,314 @@ +#!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): + """ + <session> 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 <session> l9:currentSub <sessionLocalSub> + + 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("<ButtonPress-1>", 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("<Button-1>", 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)