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)