Mercurial > code > home > repos > light9
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()