Mercurial > code > home > repos > light9
comparison bin/subcomposer @ 838:321fc6150ee3
subcomposer's nice currently-editing DnD box
Ignore-this: f2a8542a4ab38dbe61b26c864da3bace
author | drewp@bigasterisk.com |
---|---|
date | Tue, 26 Mar 2013 07:04:22 +0000 |
parents | ae359590eb8a |
children | f987eb9c3672 |
comparison
equal
deleted
inserted
replaced
837:f3364e9f5c03 | 838:321fc6150ee3 |
---|---|
1 #!bin/python | 1 #!bin/python |
2 | 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 """ | |
3 from __future__ import division, nested_scopes | 18 from __future__ import division, nested_scopes |
19 import time, logging | |
4 from optparse import OptionParser | 20 from optparse import OptionParser |
5 import logging | 21 import logging |
6 import Tkinter as tk | 22 import Tkinter as tk |
7 import louie as dispatcher | 23 import louie as dispatcher |
8 from twisted.internet import reactor, tksupport, task | 24 from twisted.internet import reactor, tksupport, task |
9 from rdflib import URIRef | 25 from rdflib import URIRef |
10 | 26 |
11 from run_local import log | 27 from run_local import log |
12 from light9.dmxchanedit import Levelbox | 28 from light9.dmxchanedit import Levelbox |
13 from light9 import dmxclient, Patch, Submaster, showconfig, prof | 29 from light9 import dmxclient, Patch, Submaster, prof |
14 from light9.uihelpers import toplevelat | 30 from light9.uihelpers import toplevelat |
15 from light9.rdfdb.syncedgraph import SyncedGraph | 31 from light9.rdfdb.syncedgraph import SyncedGraph |
16 from light9.rdfdb import clientsession | 32 from light9.rdfdb import clientsession |
17 from light9.tkdnd import initTkdnd, dragSourceRegister, dropTargetRegister | 33 from light9.tkdnd import initTkdnd, dragSourceRegister, dropTargetRegister |
18 | |
19 log.setLevel(logging.DEBUG) | |
20 | 34 |
21 class _NoNewVal(object): | 35 class _NoNewVal(object): |
22 pass | 36 pass |
23 | 37 |
24 class Observable(object): | 38 class Observable(object): |
92 b=tk.Button(text="unlink", command=self.switchToLocalSub) | 106 b=tk.Button(text="unlink", command=self.switchToLocalSub) |
93 b.pack() | 107 b.pack() |
94 | 108 |
95 | 109 |
96 def uriChanged(self, newUri): | 110 def uriChanged(self, newUri): |
111 print "chg", newUri | |
97 # i guess i show the label and icon for this | 112 # i guess i show the label and icon for this |
98 if newUri is Local: | 113 if newUri is Local: |
99 self.subIcon.config(text="(local)") | 114 self.subIcon.config(text="(local)") |
100 else: | 115 else: |
101 self.subIcon.config(text=newUri) | 116 self.subIcon.config(text=newUri) |
108 """ | 123 """ |
109 <session> l9:currentSub ?sub is the URI of the sub we're tied to for displaying and | 124 <session> l9:currentSub ?sub is the URI of the sub we're tied to for displaying and |
110 editing. If we don't have a currentSub, then we're actually | 125 editing. If we don't have a currentSub, then we're actually |
111 editing a session-local sub called <session> l9:currentSub <sessionLocalSub> | 126 editing a session-local sub called <session> l9:currentSub <sessionLocalSub> |
112 | 127 |
128 I'm not sure that Locals should even be PersistentSubmaster with | |
129 uri and graph storage, but I think that way is making fewer | |
130 special cases. | |
131 | |
113 Contains an EditChoice widget | 132 Contains an EditChoice widget |
114 | 133 |
115 UI actions: | 134 Dependencies: |
116 - drag a sub uri on here to make it the one we're editing | 135 |
117 | 136 graph (?session :currentSub ?s) -> self.currentSub |
118 - button to clear the currentSub (putting it back to | 137 self.currentSub -> graph |
119 sessionLocalSub, and also resetting sessionLocalSub to be empty | 138 self.currentSub -> self._currentChoice (which might be Local) |
120 again) | 139 self._currentChoice (which might be Local) -> self.currentSub |
121 | 140 |
122 - drag the sub uri off of here to send it to another receiver, but | 141 inside the current sub: |
123 session local sub is not easily addressable elsewhere | 142 graph -> Submaster levels (handled in Submaster) |
124 | 143 Submaster levels -> OneLevel widget |
125 - make a new sub: transfers the current data (from a shared sub or | 144 OneLevel widget -> Submaster.editLevel |
126 from the local one) to the new sub. If you're on a local sub, | 145 Submaster.editLevel -> graph (handled in Submaster) |
127 the new sub is named automatically, ideally something brief, | |
128 pretty distinct, readable, and based on the lights that are | |
129 on. If you're on a named sub, the new one starts with a | |
130 'namedsub 2' style name. The uri can also be with a '2' suffix, | |
131 although maybe that will be stupid. If you change the name | |
132 before anyone knows about this uri, we could update the current | |
133 sub's uri to a slug of the new label. | |
134 | |
135 - rename this sub: not available if you're on a local sub. Sets | |
136 the label of a named sub. Might update the uri of the named sub | |
137 if it's new enough that no one else would have that uri. Not | |
138 sure where we measure that 'new enough' value. Maybe track if | |
139 the sub has 'never been dragged out of this subcomposer | |
140 session'? But subs will also show up in other viewers and | |
141 finders. | |
142 | 146 |
143 """ | 147 """ |
144 def __init__(self, master, graph, session): | 148 def __init__(self, master, graph, session): |
145 tk.Frame.__init__(self, master, bg='black') | 149 tk.Frame.__init__(self, master, bg='black') |
146 self.graph = graph | 150 self.graph = graph |
147 self.session = session | 151 self.session = session |
148 self.currentSub = Submaster.PersistentSubmaster(graph, URIRef('http://hello')) | 152 |
149 | 153 # this is a PersistentSubmaster (even for local) or None if we're not initialized |
150 self.levelbox = Levelbox(self, graph) | 154 self.currentSub = Observable(None) |
155 | |
156 currentUri = Observable("http://curr") | |
157 | |
158 def pc(val): | |
159 print "change viewed sub to", val | |
160 currentUri.subscribe(pc) | |
161 | |
162 EditChoice(self, self.graph, self._currentChoice).frame.pack(side='top') | |
163 | |
164 def setupSubChoiceLinks(self): | |
165 | |
166 def ann(): | |
167 print "currently: session=%s currentSub=%r _currentChoice=%r" % ( | |
168 self.session, self.currentSub(), self._currentChoice()) | |
169 | |
170 @graph.addHandler | |
171 def graphChanged(): | |
172 s = graph.value(self.session, L9['currentSub']) | |
173 if s is None: | |
174 s = self.makeLocal() | |
175 self.currentSub(Submaster.PersistentSubmaster(graph, s)) | |
176 | |
177 @self.currentSub.subscribe | |
178 def subChanged(newSub): | |
179 if newSub is None: | |
180 graph.patchObject(self.session, | |
181 self.session, L9['currentSub'], None) | |
182 return | |
183 self.sendupdate() | |
184 graph.patchObject(self.session, | |
185 self.session, L9['currentSub'], newSub.uri) | |
186 | |
187 if newSub and 'local' in newSub.uri: # wrong- use rdf:type or something? | |
188 self._currentChoice(Local) | |
189 else: | |
190 # i think right here is the point that the last local | |
191 # becomes garbage, and we could clean it up. | |
192 self._currentChoice(newSub.uri) | |
193 | |
194 dispatcher.connect(self.levelsChanged, "sub levels changed") | |
195 | |
196 @self._currentChoice.subscribe | |
197 def choiceChanged(newChoice): | |
198 if newChoice is Local: | |
199 newChoice = self.makeLocal() | |
200 if newChoice is not None: | |
201 self.currentSub(Submaster.PersistentSubmaster( | |
202 graph, newChoice)) | |
203 | |
204 def levelsChanged(self, sub): | |
205 if sub == self.currentSub(): | |
206 self.sendupdate() | |
207 | |
208 def makeLocal(self): | |
209 # todo: put a type on this, so subChanged can identify it right | |
210 # todo: where will these get stored, or are they local to this | |
211 # subcomposer process and don't use PersistentSubmaster at all? | |
212 return URIRef("http://local/%s" % time.time()) | |
213 | |
214 def setupLevelboxUi(self): | |
215 self.levelbox = Levelbox(self, graph, self.currentSub) | |
151 self.levelbox.pack(side='top') | 216 self.levelbox.pack(side='top') |
152 | 217 |
153 currentUri = Observable(Local) | 218 tk.Button(self, text="All to zero", |
154 | 219 command=lambda *args: self.currentSub().clear()).pack(side='top') |
155 def pc(val): | |
156 log.info("change viewed sub to %s", val) | |
157 currentUri.subscribe(pc) | |
158 | |
159 EditChoice(self, self.graph, currentUri).frame.pack(side='top') | |
160 | |
161 def alltozero(): | |
162 for lev in self.levelbox.levels: | |
163 lev.setlevel(0) | |
164 | |
165 tk.Button(self, text="all to zero", command=alltozero).pack(side='top') | |
166 | |
167 dispatcher.connect(self.sendupdate, "levelchanged") | |
168 | |
169 def switchToLocalSub(self, *args): | |
170 """ | |
171 stop editing a shared sub and go back to our local sub | |
172 """ | |
173 | |
174 def fill_both_boxes(self, subname): | |
175 for box in [self.savebox, self.loadbox]: | |
176 box.set(subname) | |
177 | 220 |
178 def savenewsub(self, subname): | 221 def savenewsub(self, subname): |
179 leveldict={} | 222 leveldict={} |
180 for i,lev in zip(range(len(self.levels)),self.levels): | 223 for i,lev in zip(range(len(self.levels)),self.levels): |
181 if lev!=0: | 224 if lev!=0: |
182 leveldict[Patch.get_channel_name(i+1)]=lev | 225 leveldict[Patch.get_channel_name(i+1)]=lev |
183 | 226 |
184 s=Submaster.Submaster(subname,leveldict=leveldict) | 227 s=Submaster.Submaster(subname,leveldict=leveldict) |
185 s.save() | 228 s.save() |
186 | 229 |
187 # this is going to be more like 'tie to sub' and 'untied' | |
188 def loadsub(self, subname): | |
189 """puts a sub into the levels, replacing old level values""" | |
190 s=Submaster.Submasters(showconfig.getGraph()).get_sub_by_name(subname) | |
191 self.set_levels(s.get_dmx_list()) | |
192 dispatcher.send("levelchanged") | |
193 | |
194 def toDmxLevels(self): | |
195 # the dmx levels we edit and output, range is 0..1 (dmx chan 1 is | |
196 # the 0 element) | |
197 out = {} | |
198 for lev in self.levelbox.levels: | |
199 out[lev.channelnum] = lev.currentlevel | |
200 if not out: | |
201 return [] | |
202 | |
203 return [out.get(i, 0) for i in range(max(out.keys()) + 1)] | |
204 | |
205 def sendupdate(self): | 230 def sendupdate(self): |
206 dmxclient.outputlevels(self.toDmxLevels(), twisted=True) | 231 d = self.currentSub().get_dmx_list() |
207 | 232 dmxclient.outputlevels(d, twisted=True) |
208 | 233 |
209 class EntryCommand(tk.Frame): | 234 |
210 def __init__(self, master, verb="Save", cmd=None): | 235 def launch(opts, args, root, graph, session): |
211 tk.Frame.__init__(self, master, bd=2, relief='raised') | 236 if not opts.no_geometry: |
212 tk.Label(self, text="Sub name:").pack(side='left') | 237 toplevelat("subcomposer - %s" % opts.session, root, graph, session) |
213 self.cmd = cmd | 238 |
214 self.entry = tk.Entry(self) | 239 sc = Subcomposer(root, graph, session) |
215 self.entry.pack(side='left', expand=True, fill='x') | 240 sc.pack() |
216 | 241 |
217 self.entry.bind("<Return>", self.action) | 242 tk.Label(root,text="Bindings: B1 adjust level; B2 set full; B3 instant bump", |
218 tk.Button(self, text=verb, command=self.action).pack(side='left') | 243 font="Helvetica -12 italic",anchor='w').pack(side='top',fill='x') |
244 | |
245 if len(args) == 1: | |
246 # it might be a little too weird that cmdline arg to this | |
247 # process changes anything else in the same session. But also | |
248 # I'm not sure who would ever make 2 subcomposers of the same | |
249 # session (except when quitting and restarting, to get the | |
250 # same window pos), so maybe it doesn't matter. But still, | |
251 # this tool should probably default to making new sessions | |
252 # usually instead of loading the same one | |
219 | 253 |
220 def action(self, *args): | 254 def action(self, *args): |
221 subname = self.entry.get() | 255 subname = self.entry.get() |
222 self.cmd(subname) | 256 self.cmd(subname) |
223 log.info("command %s %s", self.cmd, subname) | 257 print "sub", self.cmd, subname |
224 | 258 |
225 def set(self, text): | 259 task.LoopingCall(sc.sendupdate).start(10) |
226 self.entry.delete(0, 'end') | |
227 self.entry.insert(0, text) | |
228 | 260 |
229 | 261 |
230 ############################# | 262 ############################# |
231 | 263 |
232 if __name__ == "__main__": | 264 if __name__ == "__main__": |
233 parser = OptionParser(usage="%prog [subname]") | 265 parser = OptionParser(usage="%prog [suburi]") |
234 parser.add_option('--no-geometry', action='store_true', | 266 parser.add_option('--no-geometry', action='store_true', |
235 help="don't save/restore window geometry") | 267 help="don't save/restore window geometry") |
236 clientsession.add_option(parser) | 268 clientsession.add_option(parser) |
237 opts, args = parser.parse_args() | 269 opts, args = parser.parse_args() |
238 | 270 |
271 logging.basicConfig(level=logging.DEBUG) | |
272 | |
239 root=tk.Tk() | 273 root=tk.Tk() |
240 root.config(bg='black') | 274 root.config(bg='black') |
241 root.tk_setPalette("#004633") | 275 root.tk_setPalette("#004633") |
242 | 276 |
243 initTkdnd(root.tk, 'tkdnd/trunk/') | 277 initTkdnd(root.tk, 'tkdnd/trunk/') |
244 | 278 |
245 graph = SyncedGraph("subcomposer") | 279 graph = SyncedGraph("subcomposer") |
246 session = clientsession.getUri('subcomposer', opts) | 280 session = clientsession.getUri('subcomposer', opts) |
247 | 281 |
248 if not opts.no_geometry: | 282 graph.initiallySynced.addCallback(lambda _: launch(opts, args, root, graph, session)) |
249 toplevelat("subcomposer - %s" % opts.session, root, graph, session) | |
250 | |
251 sc = Subcomposer(root, graph, session) | |
252 sc.pack() | |
253 | |
254 tk.Label(root,text="Bindings: B1 adjust level; B2 set full; B3 instant bump", | |
255 font="Helvetica -12 italic",anchor='w').pack(side='top',fill='x') | |
256 | |
257 if len(args) == 1: | |
258 root.config(bg='green') # trying to make these look distinctive | |
259 sc.loadsub(args[0]) | |
260 sc.fill_both_boxes(args[0]) | |
261 | |
262 task.LoopingCall(sc.sendupdate).start(1) | |
263 | 283 |
264 root.protocol('WM_DELETE_WINDOW', reactor.stop) | 284 root.protocol('WM_DELETE_WINDOW', reactor.stop) |
265 tksupport.install(root,ms=10) | 285 tksupport.install(root,ms=10) |
266 prof.run(reactor.run, profile=False) | 286 prof.run(reactor.run, profile=False) |