Files @ ccdfdc8183ad
Branch filter:

Location: light9/bin/subcomposer - annotation

drewp@bigasterisk.com
remove py2-style 'object' superclass
b5efddd80dad
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
1a84c5e83d3e
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
f066d6e874db
5c04a54df635
321fc6150ee3
5c04a54df635
5c04a54df635
5c04a54df635
2c782ca93e73
f066d6e874db
f066d6e874db
5442f5d8979a
ce4fffe8e413
dcab422615ca
1a84c5e83d3e
1a84c5e83d3e
0923efae4588
167a61d3cfbf
9dd2baa41cca
6fa4288da8a6
f140153c087c
f987eb9c3672
f987eb9c3672
6fa4288da8a6
f987eb9c3672
f987eb9c3672
65f0179a9254
bf728997bfde
1a84c5e83d3e
1a84c5e83d3e
6885c2fa9369
6885c2fa9369
6885c2fa9369
6885c2fa9369
6885c2fa9369
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
bf728997bfde
bf728997bfde
321fc6150ee3
6885c2fa9369
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
bf728997bfde
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
6885c2fa9369
6885c2fa9369
7772cc48e016
f29788d1c8c9
1a84c5e83d3e
ce4fffe8e413
f29788d1c8c9
ce8a71fab284
ce8a71fab284
1a84c5e83d3e
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
4b3d7e8ba24a
1a84c5e83d3e
4b3d7e8ba24a
7772cc48e016
7772cc48e016
bf728997bfde
bf728997bfde
f987eb9c3672
7772cc48e016
f987eb9c3672
bf728997bfde
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
4b3d7e8ba24a
167a61d3cfbf
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
7772cc48e016
dcab422615ca
dcab422615ca
dcab422615ca
7772cc48e016
dcab422615ca
dcab422615ca
7772cc48e016
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
f066d6e874db
7772cc48e016
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
7772cc48e016
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
dcab422615ca
7772cc48e016
dcab422615ca
7772cc48e016
dcab422615ca
dcab422615ca
c20c2eea6fce
7772cc48e016
321fc6150ee3
167a61d3cfbf
7772cc48e016
321fc6150ee3
5bcb950024af
5bcb950024af
bf728997bfde
321fc6150ee3
321fc6150ee3
ce8a71fab284
ce8a71fab284
ce8a71fab284
ce8a71fab284
7772cc48e016
321fc6150ee3
ce8a71fab284
321fc6150ee3
92c6e4b6cabb
321fc6150ee3
1a84c5e83d3e
321fc6150ee3
321fc6150ee3
ce8a71fab284
321fc6150ee3
7772cc48e016
7772cc48e016
321fc6150ee3
321fc6150ee3
7772cc48e016
7772cc48e016
1a84c5e83d3e
4b9997ab0e4f
4b9997ab0e4f
4b9997ab0e4f
ce8a71fab284
e09e3d1d83d5
e09e3d1d83d5
e09e3d1d83d5
ce8a71fab284
ce8a71fab284
e09e3d1d83d5
321fc6150ee3
321fc6150ee3
7772cc48e016
321fc6150ee3
321fc6150ee3
ce8a71fab284
321fc6150ee3
92c6e4b6cabb
321fc6150ee3
ce8a71fab284
7772cc48e016
7772cc48e016
ce8a71fab284
1a84c5e83d3e
321fc6150ee3
321fc6150ee3
321fc6150ee3
7772cc48e016
92c6e4b6cabb
dcab422615ca
dcab422615ca
dcab422615ca
321fc6150ee3
321fc6150ee3
ce8a71fab284
ce8a71fab284
ce8a71fab284
ce8a71fab284
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
167a61d3cfbf
7772cc48e016
321fc6150ee3
167a61d3cfbf
321fc6150ee3
6885c2fa9369
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
2c782ca93e73
a6662d61ebcd
7772cc48e016
f066d6e874db
7772cc48e016
7772cc48e016
2c782ca93e73
7772cc48e016
1a84c5e83d3e
5442f5d8979a
1a84c5e83d3e
321fc6150ee3
321fc6150ee3
5442f5d8979a
1a84c5e83d3e
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
7772cc48e016
65f0179a9254
bf728997bfde
7772cc48e016
7772cc48e016
7772cc48e016
7772cc48e016
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
321fc6150ee3
7772cc48e016
2c782ca93e73
321fc6150ee3
2c782ca93e73
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
1a84c5e83d3e
321fc6150ee3
7772cc48e016
7772cc48e016
8b307310cc1b
4b9997ab0e4f
4b9997ab0e4f
f29788d1c8c9
2c782ca93e73
2c782ca93e73
4b9997ab0e4f
7772cc48e016
7772cc48e016
1a84c5e83d3e
1a84c5e83d3e
6885c2fa9369
6885c2fa9369
1a84c5e83d3e
a38955ba6f40
f29788d1c8c9
ce4fffe8e413
7772cc48e016
7772cc48e016
4e558643c952
f29788d1c8c9
7772cc48e016
ce4fffe8e413
#!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)