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)