diff bin/attic/curvecalc @ 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/curvecalc@5bcb950024af
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/attic/curvecalc	Sun May 12 19:02:10 2024 -0700
@@ -0,0 +1,574 @@
+#!bin/python
+"""
+now launches like this:
+% bin/curvecalc http://light9.bigasterisk.com/show/dance2007/song1
+
+
+
+todo: curveview should preserve more objects, for speed maybe
+
+"""
+
+import sys
+import imp
+sys.path.append('/usr/lib/python2.7/dist-packages')  # For gtk
+from twisted.internet import gtk3reactor
+gtk3reactor.install()
+from twisted.internet import reactor
+
+import time, textwrap, os, optparse, linecache, signal, traceback, json
+import gi
+from gi.repository import Gtk
+from gi.repository import GObject
+from gi.repository import Gdk
+
+from urllib.parse import parse_qsl
+import louie as dispatcher
+from rdflib import URIRef, Literal, RDF, RDFS
+import logging
+
+from run_local import log
+from light9 import showconfig, networking
+from light9.curvecalc import curveview
+from light9.curvecalc.curve import Curveset
+from light9.curvecalc.curveedit import serveCurveEdit
+from light9.curvecalc.musicaccess import Music
+from light9.curvecalc.output import Output
+from light9.curvecalc.subterm import Subterm
+from light9.curvecalc.subtermview import add_one_subterm
+from light9.editchoicegtk import EditChoice, Local
+from light9.gtkpyconsole import togglePyConsole
+from light9.namespaces import L9
+from light9.observable import Observable
+from light9 import clientsession
+from rdfdb.patch import Patch
+from rdfdb.syncedgraph import SyncedGraph
+from light9.wavelength import wavelength
+
+
+class SubtermExists(ValueError):
+    pass
+
+
+class Main(object):
+
+    def __init__(self, graph, opts, session, curveset, music):
+        self.graph, self.opts, self.session = graph, opts, session
+        self.curveset, self.music = curveset, music
+        self.lastSeenInputTime = 0
+        self.currentSubterms = [
+        ]  # Subterm objects that are synced to the graph
+
+        self.setTheme()
+        wtree = self.wtree = Gtk.Builder()
+        wtree.add_from_file("light9/curvecalc/curvecalc.glade")
+        mainwin = wtree.get_object("MainWindow")
+
+        mainwin.connect("destroy", self.onQuit)
+        wtree.connect_signals(self)
+
+        mainwin.show_all()
+
+        mainwin.connect("delete-event", lambda *args: reactor.crash())
+
+        def updateTitle():
+            mainwin.set_title(
+                "curvecalc - %s" %
+                graph.label(graph.value(session, L9['currentSong'])))
+
+        graph.addHandler(updateTitle)
+
+        songChoice = Observable(None)  # to be connected with the session song
+
+        self.registerGraphToSongChoice(wtree, session, graph, songChoice)
+        self.registerSongChoiceToGraph(session, graph, songChoice)
+        self.registerCurrentPlayerSongToUi(wtree, graph, songChoice)
+
+        ec = EditChoice(graph, songChoice, label="Editing song:")
+        wtree.get_object("currentSongEditChoice").add(ec)
+        ec.show()
+
+        wtree.get_object("subterms").connect("add", self.onSubtermChildAdded)
+
+        self.refreshCurveView()
+
+        self.makeStatusLines(wtree.get_object("status"))
+        self.setupNewSubZone()
+        self.acceptDragsOnCurveViews()
+
+        # may not work
+        wtree.get_object("paned1").set_position(600)
+
+    def registerGraphToSongChoice(self, wtree, session, graph, songChoice):
+
+        def setSong():
+            current = graph.value(session, L9['currentSong'])
+            if not wtree.get_object("followPlayerSongChoice").get_active():
+                songChoice(current)
+                dispatcher.send("song_has_changed")
+
+        graph.addHandler(setSong)
+
+    def registerSongChoiceToGraph(self, session, graph, songChoice):
+        self.muteSongChoiceUntil = 0
+
+        def songChoiceToGraph(newSong):
+            if newSong is Local:
+                raise NotImplementedError('what do i patch')
+            log.debug('songChoiceToGraph is going to set to %r', newSong)
+
+            # I get bogus newSong values in here sometimes. This
+            # workaround may not even be helping.
+            now = time.time()
+            if now < self.muteSongChoiceUntil:
+                log.debug('muted')
+                return
+            self.muteSongChoiceUntil = now + 1
+
+            graph.patchObject(context=session,
+                              subject=session,
+                              predicate=L9['currentSong'],
+                              newObject=newSong)
+
+        songChoice.subscribe(songChoiceToGraph)
+
+    def registerCurrentPlayerSongToUi(self, wtree, graph, songChoice):
+        """current_player_song 'song' param -> playerSong ui
+        and
+        current_player_song 'song' param -> songChoice, if you're in autofollow
+        """
+
+        def current_player_song(song):
+            # (this is run on every frame)
+            ps = wtree.get_object("playerSong")
+            if URIRef(ps.get_uri()) != song:
+                log.debug("update playerSong to %s", ps.get_uri())
+
+                def setLabel():
+                    ps.set_label(graph.label(song))
+
+                graph.addHandler(setLabel)
+                ps.set_uri(song)
+            if song != songChoice():
+                if wtree.get_object("followPlayerSongChoice").get_active():
+                    log.debug('followPlayerSongChoice is on')
+                    songChoice(song)
+
+        dispatcher.connect(current_player_song, "current_player_song")
+        self.current_player_song = current_player_song
+
+    def setupNewSubZone(self):
+        self.wtree.get_object("newSubZone").drag_dest_set(
+            flags=Gtk.DestDefaults.ALL,
+            targets=[Gtk.TargetEntry('text/uri-list', 0, 0)],
+            actions=Gdk.DragAction.COPY)
+
+    def acceptDragsOnCurveViews(self):
+        w = self.wtree.get_object("curves")
+        w.drag_dest_set(flags=Gtk.DestDefaults.ALL,
+                        targets=[Gtk.TargetEntry('text/uri-list', 0, 0)],
+                        actions=Gdk.DragAction.COPY)
+
+        def recv(widget, context, x, y, selection, targetType, time):
+            subUri = URIRef(selection.data.strip())
+            print("into curves", subUri)
+            with self.graph.currentState(tripleFilter=(subUri, RDFS.label,
+                                                       None)) as current:
+                subName = current.label(subUri)
+
+            if '?' in subUri:
+                subName = self.handleSubtermDrop(subUri)
+            else:
+                try:
+                    self.makeSubterm(subName,
+                                     withCurve=True,
+                                     sub=subUri,
+                                     expr="%s(t)" % subName)
+                except SubtermExists:
+                    # we're not making sure the expression/etc are
+                    # correct-- user mihgt need to fix things
+                    pass
+            curveView = self.curvesetView.row(subName).curveView
+            t = self.lastSeenInputTime  # curveView.current_time() # new curve hasn't heard the time yet. this has gotten too messy- everyone just needs to be able to reach the time source
+            print("time", t)
+            curveView.add_points([(t - .5, 0), (t, 1)])
+
+        w.connect("drag-data-received", recv)
+
+    def onDragDataInNewSubZone(self, widget, context, x, y, selection,
+                               targetType, time):
+        data = URIRef(selection.data.strip())
+        if '?' in data:
+            self.handleSubtermDrop(data)
+            return
+        with self.graph.currentState(tripleFilter=(data, None,
+                                                   None)) as current:
+            subName = current.label(data)
+        self.makeSubterm(newname=subName,
+                         withCurve=True,
+                         sub=data,
+                         expr="%s(t)" % subName)
+
+    def handleSubtermDrop(self, data):
+        params = parse_qsl(data.split('?')[1])
+        flattened = dict(params)
+        self.makeSubterm(Literal(flattened['subtermName']),
+                         expr=flattened['subtermExpr'])
+
+        for cmd, name in params:
+            if cmd == 'curve':
+                self.curveset.new_curve(name)
+        return name
+
+    def onNewCurve(self, *args):
+        dialog = self.wtree.get_object("newCurve")
+        entry = self.wtree.get_object("newCurveName")
+        # if you don't have songx, that should be the suggested name
+        entry.set_text("")
+        if dialog.run() == 1:
+            self.curveset.new_curve(entry.get_text())
+        dialog.hide()
+
+    def onRedrawCurves(self, *args):
+        dispatcher.send("all curves rebuild")
+
+    def onSubtermsMap(self, *args):
+        # if this was called too soon, like in __init__, the gtktable
+        # would get its children but it wouldn't lay anything out that
+        # I can see, and I'm not sure why. Waiting for map event is
+        # just a wild guess.
+        self.graph.addHandler(self.set_subterms_from_graph)
+
+    def onNewSubterm(self, *args):
+        self.makeSubterm(Literal(""), withCurve=False)
+        return
+
+        # pretty sure i don't want this back, but not completely sure
+        # what the UX should be to get the new curve.
+
+        dialog = self.wtree.get_object("newSubterm")
+        # the plan is to autocomplete this on existing subterm names
+        # (but let you make one up, too)
+        entry = self.wtree.get_object("newSubtermName").get_children()[0]
+        entry.set_text("")
+        entry.grab_focus()
+        if dialog.run() == 1:
+            newname = entry.get_text()
+            wc = self.wtree.get_object("newSubtermMakeCurve").get_active()
+            self.makeSubterm(newname, withCurve=wc)
+        dialog.hide()
+
+    def currentSong(self):
+
+        with self.graph.currentState(tripleFilter=(self.session,
+                                                   L9['currentSong'],
+                                                   None)) as current:
+            return current.value(self.session, L9['currentSong'])
+
+    def songSubtermsContext(self):
+        return self.currentSong()
+
+    def makeSubterm(self, newname, withCurve=False, expr=None, sub=None):
+        """
+        raises SubtermExists if we had a subterm with a sub with the given
+        name. what about a no-sub term with the same label? who knows
+        """
+        assert isinstance(newname, Literal), repr(newname)
+        if withCurve:
+            self.curveset.new_curve(newname)
+        if newname in self.all_subterm_labels():
+            raise SubtermExists("have a subterm who sub is named %r" % newname)
+        with self.graph.currentState() as current:
+            song = self.currentSong()
+            for i in range(1000):
+                uri = song + "/subterm/%d" % i
+                if (uri, None, None) not in current:
+                    break
+            else:
+                raise ValueError("can't pick a name for the new subterm")
+
+        ctx = self.songSubtermsContext()
+        quads = [
+            (uri, RDF.type, L9.Subterm, ctx),
+            (uri, RDFS.label, Literal(newname), ctx),
+            (self.currentSong(), L9['subterm'], uri, ctx),
+        ]
+        if sub is not None:
+            quads.append((uri, L9['sub'], sub, ctx))
+        if expr is not None:
+            quads.append((uri, L9['expression'], Literal(expr), ctx))
+        self.graph.patch(Patch(addQuads=quads))
+
+        return uri
+
+    def all_subterm_labels(self):
+        """
+        Literal labels of subs in subterms. doesn't currently include labels of the
+        subterm resources. I'm not sure what I'm going to do with
+        those.
+        """
+        labels = []
+        with self.graph.currentState() as current:
+            for st in current.objects(
+                    current.value(self.session, L9['currentSong']),
+                    L9['subterm']):
+                sub = current.value(st, L9['sub'])
+                if sub is not None:
+                    labels.append(current.label(sub))
+        return labels
+
+    def set_subterms_from_graph(self):
+        """rebuild all the gtktable 'subterms' widgets and the
+        self.currentSubterms list"""
+        song = self.graph.value(self.session, L9['currentSong'])
+
+        newList = []
+        for st in set(self.graph.objects(song, L9['subterm'])):
+            log.debug("song %s has subterm %s", song, st)
+            term = Subterm(self.graph, st, self.songSubtermsContext(),
+                           self.curveset)
+            newList.append(term)
+        self.currentSubterms[:] = newList
+
+        master = self.wtree.get_object("subterms")
+        log.debug("removing subterm widgets")
+        [master.remove(c) for c in master.get_children()]
+        for term in self.currentSubterms:
+            add_one_subterm(term, self.curveset, master)
+        master.show_all()
+        log.debug("%s table children showing" % len(master.get_children()))
+
+    def setTheme(self):
+        settings = Gtk.Settings.get_default()
+        settings.set_property("gtk-application-prefer-dark-theme", True)
+
+        providers = []
+        providers.append(Gtk.CssProvider())
+        providers[-1].load_from_path("theme/Just-Dark/gtk-3.0/gtk.css")
+        providers.append(Gtk.CssProvider())
+        providers[-1].load_from_data('''
+          * { font-size: 92%; }
+          .button:link { font-size: 7px }
+        ''')
+
+        screen = Gdk.Display.get_default_screen(Gdk.Display.get_default())
+        for p in providers:
+            Gtk.StyleContext.add_provider_for_screen(
+                screen, p, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
+
+    def onSubtermChildAdded(self, subtermsTable, *args):
+        # this would probably work, but isn't getting called
+        log.info("onSubtermChildAdded")
+        v = subtermsTable.get_parent().props.vadjustment
+        v.props.value = v.props.upper
+
+    def onQuit(self, *args):
+        reactor.crash()
+        # there's a hang after this, maybe in sem_wait in two
+        # threads. I don't know whose they are.
+        # This fix affects profilers who want to write output at the end.
+        os.kill(os.getpid(), signal.SIGKILL)
+
+    def onCollapseAll(self, *args):
+        self.curvesetView.collapseAll()
+
+    def onCollapseNone(self, *args):
+        self.curvesetView.collapseNone()
+
+    def onDelete(self, *args):
+        self.curvesetView.onDelete()
+
+    def onPythonConsole(self, item):
+        ns = dict()
+        ns.update(globals())
+        ns.update(self.__dict__)
+        togglePyConsole(self, item, ns)
+
+    def onSeeCurrentTime(self, item):
+        dispatcher.send("see time")
+
+    def onSeeTimeUntilEnd(self, item):
+        dispatcher.send("see time until end")
+
+    def onZoomAll(self, item):
+        dispatcher.send("show all")
+
+    def onPlayPause(self, item):
+        # since the X coord in a curveview affects the handling, one
+        # of them may be able to pick this up
+        results = dispatcher.send("onPlayPause")
+        times = [t for listener, t in results if t is not None]
+        self.music.playOrPause(t=times[0] if times else None)
+
+    def onSave(self, *args):
+        # only doing curves still. I hope to eliminate all this.
+        log.info("saving curves")
+        self.curveset.save()
+        log.info("saved")
+
+    def makeStatusLines(self, master):
+        """various labels that listen for dispatcher signals"""
+        for row, (signame, textfilter) in enumerate([
+            ('input time', lambda t: "%.2fs" % t),
+            ('output levels', lambda levels: textwrap.fill(
+                "; ".join([
+                    "%s:%.2f" % (n, v) for n, v in list(levels.items())[:2] if v
+                    > 0
+                ]), 70)),
+            ('update period', lambda t: "%.1fms" % (t * 1000)),
+            ('update status', lambda x: str(x)),
+        ]):
+            key = Gtk.Label("%s:" % signame)
+            value = Gtk.Label("")
+            master.resize(row + 1, 2)
+            master.attach(key, 0, 1, row, row + 1)
+            master.attach(value, 1, 2, row, row + 1)
+            key.set_alignment(1, 0)
+            value.set_alignment(0, 0)
+
+            dispatcher.connect(lambda val, value=value, tf=textfilter: value.
+                               set_text(tf(val)),
+                               signame,
+                               weak=False)
+        dispatcher.connect(lambda val: setattr(self, 'lastSeenInputTime', val),
+                           'input time',
+                           weak=False)
+        master.show_all()
+
+    def refreshCurveView(self):
+        wtree = self.wtree
+        mtimes = [
+            os.path.getmtime(f) for f in [
+                'light9/curvecalc/curveview.py',
+                'light9/curvecalc/zoomcontrol.py',
+            ]
+        ]
+
+        if (not hasattr(self, 'curvesetView') or
+                self.curvesetView._mtimes != mtimes):
+            print("reload curveview.py")
+            curvesVBox = wtree.get_object("curves")
+            zoomControlBox = wtree.get_object("zoomControlBox")
+            [curvesVBox.remove(c) for c in curvesVBox.get_children()]
+            [zoomControlBox.remove(c) for c in zoomControlBox.get_children()]
+            try:
+                linecache.clearcache()
+                imp.reload(curveview)
+
+                # old ones are not getting deleted right
+                if hasattr(self, 'curvesetView'):
+                    self.curvesetView.live = False
+
+                # mem problem somewhere; need to hold a ref to this
+                self.curvesetView = curveview.Curvesetview(
+                    self.graph, curvesVBox, zoomControlBox, self.curveset)
+                self.curvesetView._mtimes = mtimes
+
+                # this is scheduled after some tk shuffling, to
+                # try to minimize the number of times we redraw
+                # the curve at startup. If tk is very slow, it's
+                # ok. You'll just get some wasted redraws.
+                self.curvesetView.goLive()
+            except Exception:
+                print("reload failed:")
+                traceback.print_exc()
+        if self.opts.reload:
+            reactor.callLater(1, self.refreshCurveView)
+
+
+class MaxTime(object):
+    """
+    looks up the time in seconds for the session's current song
+    """
+
+    def __init__(self, graph, session):
+        self.graph, self.session = graph, session
+        graph.addHandler(self.update)
+
+    def update(self):
+        song = self.graph.value(self.session, L9['currentSong'])
+        if song is None:
+            self.maxtime = 0
+            return
+        musicfilename = showconfig.songOnDisk(song)
+        self.maxtime = wavelength(musicfilename)
+        log.info("new max time %r", self.maxtime)
+        dispatcher.send("max time", maxtime=self.maxtime)
+
+    def get(self):
+        return self.maxtime
+
+
+def launch(args, graph, session, opts, startTime, music):
+
+    try:
+        song = URIRef(args[0])
+        graph.patchObject(context=session,
+                          subject=session,
+                          predicate=L9['currentSong'],
+                          newObject=song)
+    except IndexError:
+        pass
+
+    curveset = Curveset(graph=graph, session=session)
+
+    log.debug("startup: output %s", time.time() - startTime)
+
+    mt = MaxTime(graph, session)
+    dispatcher.connect(lambda: mt.get(), "get max time", weak=False)
+
+    start = Main(graph, opts, session, curveset, music)
+    out = Output(graph, session, music, curveset, start.currentSubterms)
+
+    dispatcher.send("show all")
+
+    if opts.startup_only:
+        log.debug("quitting now because of --startup-only")
+        return
+
+    def hoverTimeResponse(requestHandler):
+        results = dispatcher.send("onPlayPause")
+        times = [t for listener, t in results if t is not None]
+        if not times:
+            requestHandler.set_status(404)
+            requestHandler.write("not hovering over any time")
+            return
+        with graph.currentState(tripleFilter=(session, L9['currentSong'],
+                                              None)) as g:
+            song = g.value(session, L9['currentSong'])
+            json.dump({"song": song, "hoverTime": times[0]}, requestHandler)
+
+    serveCurveEdit(networking.curveCalc.port, hoverTimeResponse, start.curveset)
+
+
+def main():
+    startTime = time.time()
+    parser = optparse.OptionParser()
+    parser.set_usage("%prog [opts] [songURI]")
+    parser.add_option("--debug", action="store_true", help="log at DEBUG")
+    parser.add_option("--reload",
+                      action="store_true",
+                      help="live reload of themes and code")
+    parser.add_option("--startup-only",
+                      action='store_true',
+                      help="quit after loading everything (for timing tests)")
+    parser.add_option("--profile", help='"hotshot" or "stat"')
+    clientsession.add_option(parser)
+    opts, args = parser.parse_args()
+
+    log.setLevel(logging.DEBUG if opts.debug else logging.INFO)
+
+    log.debug("startup: music %s", time.time() - startTime)
+
+    session = clientsession.getUri('curvecalc', opts)
+
+    music = Music()
+    graph = SyncedGraph(networking.rdfdb.url, "curvecalc")
+
+    graph.initiallySynced.addCallback(lambda _: launch(args, graph, session,
+                                                       opts, startTime, music))
+    from light9 import prof
+    prof.run(reactor.run, profile=opts.profile)
+
+
+main()