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)