Mercurial > code > home > repos > light9
comparison 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 |
comparison
equal
deleted
inserted
replaced
2375:623836db99af | 2376:4556eebe5d73 |
---|---|
1 #!bin/python | |
2 """ | |
3 subcomposer | |
4 session | |
5 observable(currentSub), a Submaster which tracks the graph | |
6 | |
7 EditChoice widget | |
8 can change currentSub to another sub | |
9 | |
10 Levelbox widget | |
11 watch observable(currentSub) for a new sub, and also watch currentSub for edits to push to the OneLevel widgets | |
12 | |
13 OneLevel widget | |
14 UI edits are caught here and go all the way back to currentSub | |
15 | |
16 | |
17 """ | |
18 | |
19 from run_local import log | |
20 import time, logging | |
21 | |
22 log.setLevel(logging.DEBUG) | |
23 | |
24 from optparse import OptionParser | |
25 import logging, urllib.request, urllib.parse, urllib.error | |
26 import tkinter as tk | |
27 import louie as dispatcher | |
28 from twisted.internet import reactor, tksupport, task | |
29 from rdflib import URIRef, RDF, RDFS, Literal | |
30 | |
31 from light9.dmxchanedit import Levelbox | |
32 from light9 import dmxclient, Submaster, prof, showconfig, networking | |
33 from light9.Patch import get_channel_name | |
34 from light9.uihelpers import toplevelat | |
35 from rdfdb.syncedgraph import SyncedGraph | |
36 from light9 import clientsession | |
37 from light9.tkdnd import initTkdnd | |
38 from light9.namespaces import L9 | |
39 from rdfdb.patch import Patch | |
40 from light9.observable import Observable | |
41 from light9.editchoice import EditChoice, Local | |
42 from light9.subcomposer import subcomposerweb | |
43 | |
44 | |
45 class Subcomposer(tk.Frame): | |
46 """ | |
47 <session> l9:currentSub ?sub is the URI of the sub we're tied to for displaying and | |
48 editing. If we don't have a currentSub, then we're actually | |
49 editing a session-local sub called <session> l9:currentSub <sessionLocalSub> | |
50 | |
51 I'm not sure that Locals should even be PersistentSubmaster with | |
52 uri and graph storage, but I think that way is making fewer | |
53 special cases. | |
54 | |
55 Contains an EditChoice widget | |
56 | |
57 Dependencies: | |
58 | |
59 graph (?session :currentSub ?s) -> self.currentSub | |
60 self.currentSub -> graph | |
61 self.currentSub -> self._currentChoice (which might be Local) | |
62 self._currentChoice (which might be Local) -> self.currentSub | |
63 | |
64 inside the current sub: | |
65 graph -> Submaster levels (handled in Submaster) | |
66 Submaster levels -> OneLevel widget | |
67 OneLevel widget -> Submaster.editLevel | |
68 Submaster.editLevel -> graph (handled in Submaster) | |
69 | |
70 """ | |
71 | |
72 def __init__(self, master, graph, session): | |
73 tk.Frame.__init__(self, master, bg='black') | |
74 self.graph = graph | |
75 self.session = session | |
76 self.launchTime = time.time() | |
77 self.localSerial = 0 | |
78 | |
79 # this is a URIRef or Local. Strangely, the Local case still | |
80 # has a uri, which you can get from | |
81 # self.currentSub.uri. Probably that should be on the Local | |
82 # object too, or maybe Local should be a subclass of URIRef | |
83 self._currentChoice = Observable(Local) | |
84 | |
85 # this is a PersistentSubmaster (even for local) | |
86 self.currentSub = Observable( | |
87 Submaster.PersistentSubmaster(graph, self.switchToLocal())) | |
88 | |
89 def pc(val): | |
90 log.info("change viewed sub to %s", val) | |
91 | |
92 self._currentChoice.subscribe(pc) | |
93 | |
94 ec = self.editChoice = EditChoice(self, self.graph, self._currentChoice) | |
95 ec.frame.pack(side='top') | |
96 | |
97 ec.subIcon.bind("<ButtonPress-1>", self.clickSubIcon) | |
98 self.setupSubChoiceLinks() | |
99 self.setupLevelboxUi() | |
100 | |
101 def clickSubIcon(self, *args): | |
102 box = tk.Toplevel(self.editChoice.frame) | |
103 box.wm_transient(self.editChoice.frame) | |
104 tk.Label(box, text="Name this sub:").pack() | |
105 e = tk.Entry(box) | |
106 e.pack() | |
107 b = tk.Button(box, text="Make global") | |
108 b.pack() | |
109 | |
110 def clicked(*args): | |
111 self.makeGlobal(newName=e.get()) | |
112 box.destroy() | |
113 | |
114 b.bind("<Button-1>", clicked) | |
115 e.focus() | |
116 | |
117 def makeGlobal(self, newName): | |
118 """promote our local submaster into a non-local, named one""" | |
119 uri = self.currentSub().uri | |
120 newUri = showconfig.showUri() + ("/sub/%s" % | |
121 urllib.parse.quote(newName, safe='')) | |
122 with self.graph.currentState(tripleFilter=(uri, None, None)) as current: | |
123 if (uri, RDF.type, L9['LocalSubmaster']) not in current: | |
124 raise ValueError("%s is not a local submaster" % uri) | |
125 if (newUri, None, None) in current: | |
126 raise ValueError("new uri %s is in use" % newUri) | |
127 | |
128 # the local submaster was storing in ctx=self.session, but now | |
129 # we want it to be in ctx=uri | |
130 | |
131 self.relocateSub(newUri, newName) | |
132 | |
133 # these are in separate patches for clarity as i'm debugging this | |
134 self.graph.patch( | |
135 Patch(addQuads=[ | |
136 (newUri, RDFS.label, Literal(newName), newUri), | |
137 ], | |
138 delQuads=[ | |
139 (newUri, RDF.type, L9['LocalSubmaster'], newUri), | |
140 ])) | |
141 self.graph.patchObject(self.session, self.session, L9['currentSub'], | |
142 newUri) | |
143 | |
144 def relocateSub(self, newUri, newName): | |
145 # maybe this goes in Submaster | |
146 uri = self.currentSub().uri | |
147 | |
148 def repl(u): | |
149 if u == uri: | |
150 return newUri | |
151 return u | |
152 | |
153 delQuads = self.currentSub().allQuads() | |
154 addQuads = [(repl(s), p, repl(o), newUri) for s, p, o, c in delQuads] | |
155 # patch can't span contexts yet | |
156 self.graph.patch(Patch(addQuads=addQuads, delQuads=[])) | |
157 self.graph.patch(Patch(addQuads=[], delQuads=delQuads)) | |
158 | |
159 def setupSubChoiceLinks(self): | |
160 graph = self.graph | |
161 | |
162 def ann(): | |
163 print("currently: session=%s currentSub=%r _currentChoice=%r" % | |
164 (self.session, self.currentSub(), self._currentChoice())) | |
165 | |
166 @graph.addHandler | |
167 def graphChanged(): | |
168 # some bug where SC is making tons of graph edits and many | |
169 # are failing. this calms things down. | |
170 log.warn('skip graphChanged') | |
171 return | |
172 | |
173 s = graph.value(self.session, L9['currentSub']) | |
174 log.debug('HANDLER getting session currentSub from graph: %s', s) | |
175 if s is None: | |
176 s = self.switchToLocal() | |
177 self.currentSub(Submaster.PersistentSubmaster(graph, s)) | |
178 | |
179 @self.currentSub.subscribe | |
180 def subChanged(newSub): | |
181 log.debug('HANDLER currentSub changed to %s', newSub) | |
182 if newSub is None: | |
183 graph.patchObject(self.session, self.session, L9['currentSub'], | |
184 None) | |
185 return | |
186 self.sendupdate() | |
187 graph.patchObject(self.session, self.session, L9['currentSub'], | |
188 newSub.uri) | |
189 | |
190 localStmt = (newSub.uri, RDF.type, L9['LocalSubmaster']) | |
191 with graph.currentState(tripleFilter=localStmt) as current: | |
192 if newSub and localStmt in current: | |
193 log.debug(' HANDLER set _currentChoice to Local') | |
194 self._currentChoice(Local) | |
195 else: | |
196 # i think right here is the point that the last local | |
197 # becomes garbage, and we could clean it up. | |
198 log.debug(' HANDLER set _currentChoice to newSub.uri') | |
199 self._currentChoice(newSub.uri) | |
200 | |
201 dispatcher.connect(self.levelsChanged, "sub levels changed") | |
202 | |
203 @self._currentChoice.subscribe | |
204 def choiceChanged(newChoice): | |
205 log.debug('HANDLER choiceChanged to %s', newChoice) | |
206 if newChoice is Local: | |
207 newChoice = self.switchToLocal() | |
208 if newChoice is not None: | |
209 newSub = Submaster.PersistentSubmaster(graph, newChoice) | |
210 log.debug('write new choice to currentSub, from %r to %r', | |
211 self.currentSub(), newSub) | |
212 self.currentSub(newSub) | |
213 | |
214 def levelsChanged(self, sub): | |
215 if sub == self.currentSub(): | |
216 self.sendupdate() | |
217 | |
218 def switchToLocal(self): | |
219 """ | |
220 change our display to a local submaster | |
221 """ | |
222 # todo: where will these get stored, or are they local to this | |
223 # subcomposer process and don't use PersistentSubmaster at all? | |
224 localId = "%s-%s" % (self.launchTime, self.localSerial) | |
225 self.localSerial += 1 | |
226 new = URIRef("http://light9.bigasterisk.com/sub/local/%s" % localId) | |
227 log.debug('making up a local sub %s', new) | |
228 self.graph.patch( | |
229 Patch(addQuads=[ | |
230 (new, RDF.type, L9['Submaster'], self.session), | |
231 (new, RDF.type, L9['LocalSubmaster'], self.session), | |
232 ])) | |
233 | |
234 return new | |
235 | |
236 def setupLevelboxUi(self): | |
237 self.levelbox = Levelbox(self, self.graph, self.currentSub) | |
238 self.levelbox.pack(side='top') | |
239 | |
240 tk.Button( | |
241 self, | |
242 text="All to zero", | |
243 command=lambda *args: self.currentSub().clear()).pack(side='top') | |
244 | |
245 def savenewsub(self, subname): | |
246 leveldict = {} | |
247 for i, lev in zip(list(range(len(self.levels))), self.levels): | |
248 if lev != 0: | |
249 leveldict[get_channel_name(i + 1)] = lev | |
250 | |
251 s = Submaster.Submaster(subname, levels=leveldict) | |
252 s.save() | |
253 | |
254 def sendupdate(self): | |
255 d = self.currentSub().get_dmx_list() | |
256 dmxclient.outputlevels(d, twisted=True) | |
257 | |
258 | |
259 def launch(opts, args, root, graph, session): | |
260 if not opts.no_geometry: | |
261 toplevelat("subcomposer - %s" % opts.session, root, graph, session) | |
262 | |
263 sc = Subcomposer(root, graph, session) | |
264 sc.pack() | |
265 | |
266 subcomposerweb.init(graph, session, sc.currentSub) | |
267 | |
268 tk.Label(root, | |
269 text="Bindings: B1 adjust level; B2 set full; B3 instant bump", | |
270 font="Helvetica -12 italic", | |
271 anchor='w').pack(side='top', fill='x') | |
272 | |
273 if len(args) == 1: | |
274 # it might be a little too weird that cmdline arg to this | |
275 # process changes anything else in the same session. But also | |
276 # I'm not sure who would ever make 2 subcomposers of the same | |
277 # session (except when quitting and restarting, to get the | |
278 # same window pos), so maybe it doesn't matter. But still, | |
279 # this tool should probably default to making new sessions | |
280 # usually instead of loading the same one | |
281 graph.patchObject(session, session, L9['currentSub'], URIRef(args[0])) | |
282 | |
283 task.LoopingCall(sc.sendupdate).start(10) | |
284 | |
285 | |
286 ############################# | |
287 | |
288 if __name__ == "__main__": | |
289 parser = OptionParser(usage="%prog [suburi]") | |
290 parser.add_option('--no-geometry', | |
291 action='store_true', | |
292 help="don't save/restore window geometry") | |
293 parser.add_option('-v', action='store_true', help="log debug level") | |
294 | |
295 clientsession.add_option(parser) | |
296 opts, args = parser.parse_args() | |
297 | |
298 log.setLevel(logging.DEBUG if opts.v else logging.INFO) | |
299 | |
300 root = tk.Tk() | |
301 root.config(bg='black') | |
302 root.tk_setPalette("#004633") | |
303 | |
304 initTkdnd(root.tk, 'tkdnd/trunk/') | |
305 | |
306 graph = SyncedGraph(networking.rdfdb.url, "subcomposer") | |
307 session = clientsession.getUri('subcomposer', opts) | |
308 | |
309 graph.initiallySynced.addCallback(lambda _: launch(opts, args, root, graph, | |
310 session)) | |
311 | |
312 root.protocol('WM_DELETE_WINDOW', reactor.stop) | |
313 tksupport.install(root, ms=10) | |
314 prof.run(reactor.run, profile=False) |