Mercurial > code > home > repos > light9
comparison flax/CueFaders.py @ 151:990a9474d0e7
early cue stuff. the CueList will supply the CueFader with the cues to
early cue stuff. the CueList will supply the CueFader with the cues to
work with, and will do crossfading sooner or later. the format of cues
is still very open. cuelist1 is a bogus cuelist for testing.
author | dmcc |
---|---|
date | Sun, 06 Jul 2003 16:33:06 +0000 |
parents | 45b12307c695 |
children | 4c3060ceddcc |
comparison
equal
deleted
inserted
replaced
150:1e4814221a64 | 151:990a9474d0e7 |
---|---|
1 from __future__ import division, nested_scopes | 1 from __future__ import division, nested_scopes |
2 import Tix as Tk | 2 import Tix as Tk |
3 import time | 3 import time |
4 from TreeDict import TreeDict, allow_class_to_be_pickled | 4 from TreeDict import TreeDict, allow_class_to_be_pickled |
5 from TLUtility import enumerate | |
6 import Submaster, dmxclient | |
7 | |
8 cue_state_indicator_colors = { | |
9 # bg fg | |
10 'prev' : ('blue', 'white'), | |
11 'cur' : ('yellow', 'black'), | |
12 'next' : ('red', 'white'), | |
13 } | |
14 | |
15 # TODO | |
16 # FIXE pause fades, set new time to be remaining about of time in the fade so | |
17 # fade can continue properly | |
18 # FIXE make fades work properly: the set_next / prev bug | |
19 # WONT find cue by page ("not necessawy!") | |
20 # WONT CueFader controls KeyboardController? unlikely | |
21 # FIXE AutoSave loop | |
22 | 5 |
23 class LabelledScale(Tk.Frame): | 6 class LabelledScale(Tk.Frame): |
24 """Scale with two labels: a name and current value""" | 7 """Scale with two labels: a name and current value""" |
25 def __init__(self, master, label, **opts): | 8 def __init__(self, master, label, **opts): |
26 Tk.Frame.__init__(self, master, bd=2, relief='raised', bg='black') | 9 Tk.Frame.__init__(self, master, bd=2, relief='raised') |
27 self.labelformatter = opts.get('labelformatter') | |
28 try: | |
29 del opts['labelformatter'] | |
30 except KeyError: | |
31 pass | |
32 | |
33 opts.setdefault('variable', Tk.DoubleVar()) | 10 opts.setdefault('variable', Tk.DoubleVar()) |
34 opts.setdefault('showvalue', 0) | 11 opts.setdefault('showvalue', 0) |
35 | |
36 self.normaltrough = opts.get('troughcolor', 'black') | |
37 self.flashtrough = opts.get('flashtroughcolor', 'red') | |
38 try: | |
39 del opts['flashtroughcolor'] | |
40 except KeyError: | |
41 pass | |
42 | |
43 self.scale_var = opts['variable'] | 12 self.scale_var = opts['variable'] |
44 self.scale = Tk.Scale(self, **opts) | 13 self.scale = Tk.Scale(self, **opts) |
45 self.scale.pack(side='top', expand=1, fill='both') | 14 self.scale.pack(side='top', expand=1, fill='both') |
46 self.name = Tk.Label(self, text=label, bg='black', fg='white') | 15 self.name = Tk.Label(self, text=label) |
47 self.name.pack(side='bottom') | 16 self.name.pack(side='bottom') |
48 self.scale_value = Tk.Label(self, bg='black', fg='white') | 17 self.scale_value = Tk.Label(self, width=6) |
49 self.scale_value.pack(side='bottom') | 18 self.scale_value.pack(side='bottom') |
50 self.scale_var.trace('w', self.update_value_label) | 19 self.scale_var.trace('w', self.update_value_label) |
51 self.update_value_label() | 20 self.update_value_label() |
52 self.disabled = (self.scale['state'] == 'disabled') | |
53 def set_label(self, label): | 21 def set_label(self, label): |
54 self.name['text'] = label | 22 self.name['text'] = label |
55 def update_value_label(self, *args): | 23 def update_value_label(self, *args): |
56 val = self.scale_var.get() * 100 | 24 val = self.scale_var.get() * 100 |
57 if self.labelformatter: | 25 self.scale_value['text'] = "%0.2f" % val |
58 format = self.labelformatter(val) | |
59 else: | |
60 format = "%0.2f" % val | |
61 self.scale_value['text'] = format | |
62 if val != 0: | |
63 self.scale['troughcolor'] = self.flashtrough | |
64 else: | |
65 self.scale['troughcolor'] = self.normaltrough | |
66 def disable(self): | |
67 if not self.disabled: | |
68 self.scale['state'] = 'disabled' | |
69 self.scale_var.set(0) | |
70 self.disabled = 1 | |
71 def enable(self): | |
72 if self.disabled: | |
73 self.scale['state'] = 'normal' | |
74 self.disabled = 0 | |
75 | 26 |
76 class TimedGoButton(Tk.Frame): | 27 class TimedGoButton(Tk.Frame): |
77 """Go button, fade time entry, and time fader""" | 28 """Go button, fade time entry, and time fader""" |
78 def __init__(self, master, name, scale_to_fade, **kw): | 29 def __init__(self, master, name, scale_to_fade): |
79 Tk.Frame.__init__(self, master, bg='black') | 30 Tk.Frame.__init__(self, master) |
80 self.name = name | 31 self.name = name |
81 self.scale_to_fade = scale_to_fade | 32 self.scale_to_fade = scale_to_fade |
82 self.button = Tk.Button(self, text=name, command=self.start_fade, **kw) | 33 self.button = Tk.Button(self, text=name, command=self.start_fade) |
83 self.button.pack(fill='both', expand=1, side='left') | 34 self.button.pack(fill='both', expand=1, side='left') |
84 | 35 self.timer_var = Tk.StringVar() |
85 self.timer_var = Tk.DoubleVar() | 36 self.timer_entry = Tk.Entry(self, textvariable=self.timer_var, width=5) |
86 self.timer_entry = Tk.Control(self, step=0.5, min=0, integer=0, | |
87 variable=self.timer_var, selectmode='immediate') | |
88 for widget in (self.timer_entry, self.timer_entry.entry, | |
89 self.timer_entry.incr, self.timer_entry.decr, self.button, self): | |
90 widget.bind("<4>", self.wheelscroll) | |
91 widget.bind("<5>", self.wheelscroll) | |
92 self.timer_entry.entry.configure(width=5, bg='black', fg='white') | |
93 self.timer_entry.pack(fill='y', side='left') | 37 self.timer_entry.pack(fill='y', side='left') |
94 self.timer_var.set(2) | 38 self.timer_var.set("2") |
95 self.disabled = (self.button['state'] == 'disabled') | |
96 self.start_time = 0 | |
97 self.fading = 0 | |
98 self.last_after_key = 0 | |
99 def manual_override(self, *args): | |
100 self.end_fade() | |
101 def wheelscroll(self, event): | |
102 """Mouse wheel increments or decrements timer.""" | |
103 if event.num == 4: # scroll up | |
104 self.timer_entry.increment() | |
105 else: # scroll down | |
106 self.timer_entry.decrement() | |
107 def start_fade(self, end_level=1): | 39 def start_fade(self, end_level=1): |
108 self.last_start_time = self.start_time | 40 try: |
41 fade_time = float(self.timer_var.get()) | |
42 except ValueError: | |
43 # TODO figure out how to handle this | |
44 print "can't fade -- bad time" | |
45 return | |
46 | |
109 self.start_time = time.time() | 47 self.start_time = time.time() |
110 self.start_level = self.scale_to_fade.scale_var.get() | 48 self.start_level = self.scale_to_fade.scale_var.get() |
111 self.end_level = end_level | 49 self.end_level = end_level |
112 | 50 self.fade_length = fade_time |
113 if self.fading == 1: # if we're already fading | 51 self.do_fade() |
114 self.fading = 'paused' | |
115 # new fade should be as long as however much was left | |
116 self.fade_length = self.fade_length - \ | |
117 (time.time() - self.last_start_time) | |
118 self.button['text'] = 'Unpause' | |
119 self.after_cancel(self.last_after_key) | |
120 else: | |
121 try: | |
122 fade_time = float(self.timer_var.get()) | |
123 except ValueError: | |
124 # since we use a TixControl now, i don't think we need to worry | |
125 # about validation any more. | |
126 print ">>> Can't fade -- bad time", self.timer_var.get() | |
127 return | |
128 | |
129 # if we're not already fading, we get our time from the entry | |
130 if self.fading != 'paused': | |
131 self.fade_length = fade_time | |
132 | |
133 self.button['text'] = 'Pause' | |
134 self.fading = 1 | |
135 self.do_fade() | |
136 def do_fade(self): | 52 def do_fade(self): |
137 diff = time.time() - self.start_time | 53 diff = time.time() - self.start_time |
138 if diff < self.fade_length: | 54 if diff < self.fade_length: |
139 percent = diff / self.fade_length | 55 percent = diff / self.fade_length |
140 newlevel = self.start_level + \ | 56 newlevel = self.start_level + \ |
141 (percent * (self.end_level - self.start_level)) | 57 (percent * (self.end_level - self.start_level)) |
142 self.scale_to_fade.scale_var.set(newlevel) | 58 self.scale_to_fade.scale_var.set(newlevel) |
143 | 59 |
144 if newlevel != self.end_level: | 60 if newlevel != self.end_level: |
145 self.last_after_key = self.after(10, self.do_fade) | 61 self.after(10, self.do_fade) |
146 else: | |
147 self.end_fade() | |
148 else: | 62 else: |
149 self.scale_to_fade.scale_var.set(self.end_level) | 63 self.scale_to_fade.scale_var.set(self.end_level) |
150 self.end_fade() | |
151 def end_fade(self): | |
152 self.button['text'] = self.name | |
153 self.fading = 0 | |
154 self.after_cancel(self.last_after_key) | |
155 def disable(self): | |
156 if not self.disabled: | |
157 self.button['state'] = 'disabled' | |
158 self.disabled = 1 | |
159 def enable(self): | |
160 if self.disabled: | |
161 self.button['state'] = 'normal' | |
162 self.disabled = 0 | |
163 def set_time(self, time): | |
164 self.timer_var.set(time) | |
165 def get_time(self): | |
166 return self.timer_var.get() | |
167 def is_fading(self): | |
168 return self.fading | |
169 | 64 |
170 class CueFader(Tk.Frame): | 65 class CueFader(Tk.Frame): |
171 def __init__(self, master, cuelist): | 66 def __init__(self, master, cuelist): |
172 Tk.Frame.__init__(self, master, bg='black') | 67 Tk.Frame.__init__(self, master) |
173 self.cuelist = cuelist | 68 self.cuelist = cuelist |
174 self.cuelist.set_fader(self) | 69 self.auto_shift = 0 |
175 | |
176 self.last_levels_sent = 0 | |
177 self.current_dmx_levels = [0] * 68 | |
178 self.after(0, self.send_dmx_levels_loop) # start DMX sending loop | |
179 | |
180 # this is a mechanism to stop Tk from autoshifting too much. | |
181 # if this variable is true, the mouse button is down. we don't want | |
182 # to shift until they release it. when it is released, we will | |
183 # set it to false and then call autoshift. | |
184 self.no_shifts_until_release = 0 | |
185 | 70 |
186 self.scales = {} | 71 self.scales = {} |
187 self.shift_buttons = {} | 72 self.shift_buttons = {} |
188 self.go_buttons = {} | 73 |
74 topframe = Tk.Frame(self) | |
75 self.current_cues = Tk.Label(topframe) | |
76 self.current_cues.pack() | |
77 self.update_cue_display() | |
78 topframe.pack() | |
189 | 79 |
190 topframe = Tk.Frame(self, bg='black') | 80 for name, start, end, side in (('Prev', 1, 0, 'left'), |
191 | 81 ('Next', 0, 1, 'right')): |
192 self.set_prev_button = Tk.Button(topframe, text='Set Prev', | 82 |
193 command=lambda: cuelist.set_selection_as_prev(), | 83 shift = Tk.Button(self, text="Shift %s" % name, state='disabled', |
194 fg='white', bg='blue') | 84 command=lambda name=name: self.shift(name)) |
195 self.set_prev_button.pack(side='left') | 85 shift.pack(side=side, fill='both', expand=1) |
196 | 86 |
197 self.auto_shift = Tk.IntVar() | 87 frame = Tk.Frame(self) |
198 self.auto_shift.set(1) | |
199 | |
200 self.auto_shift_checkbutton = Tk.Checkbutton(topframe, | |
201 variable=self.auto_shift, text='Autoshift', | |
202 command=self.toggle_autoshift, bg='black', fg='white', | |
203 highlightbackground='black') | |
204 self.auto_shift_checkbutton.pack(fill='both', side='left') | |
205 | |
206 self.auto_load_times = Tk.IntVar() | |
207 self.auto_load_times.set(1) | |
208 | |
209 self.auto_load_times_checkbutton = Tk.Checkbutton(topframe, | |
210 variable=self.auto_load_times, text='Autoload Times', | |
211 bg='black', fg='white', | |
212 highlightbackground='black') | |
213 self.auto_load_times_checkbutton.pack(fill='both', side='left') | |
214 | |
215 self.mute = Tk.IntVar() | |
216 self.mute.set(0) | |
217 | |
218 self.mutebutton = Tk.Checkbutton(topframe, | |
219 variable=self.mute, text='Mute', | |
220 bg='black', fg='white', | |
221 highlightbackground='black', | |
222 command=self.send_dmx_levels) | |
223 self.mutebutton.pack(fill='both', side='left') | |
224 | |
225 self.set_next_button = Tk.Button(topframe, text='Set Next', | |
226 command=lambda: cuelist.set_selection_as_next(), | |
227 fg='white', bg='red') | |
228 self.set_next_button.pack(side='left') | |
229 | |
230 topframe.pack(side='top') | |
231 | |
232 faderframe = Tk.Frame(self, bg='black') | |
233 self.direction_info = (('Prev', 1, 0, 'left', 'blue'), | |
234 ('Next', 0, 1, 'right', 'red')) | |
235 for name, start, end, side, color in self.direction_info: | |
236 frame = Tk.Frame(self, bg='black') | |
237 scale = LabelledScale(frame, name, from_=start, to_=end, | 88 scale = LabelledScale(frame, name, from_=start, to_=end, |
238 res=0.0001, orient='horiz', flashtroughcolor=color, | 89 res=0.01, orient='horiz') |
239 labelformatter=lambda val, name=name: self.get_scale_desc(val, | 90 scale.pack(fill='both', expand=1) |
240 name)) | 91 go = TimedGoButton(frame, 'Go %s' % name, scale) |
241 scale.pack(fill='x', expand=0) | |
242 go = TimedGoButton(frame, 'Go %s' % name, scale, bg=color, | |
243 fg='white', width=10) | |
244 go.pack(fill='both', expand=1) | 92 go.pack(fill='both', expand=1) |
245 frame.pack(side=side, fill='both', expand=1) | 93 frame.pack(side=side, fill='both', expand=1) |
246 | 94 |
247 shift = Tk.Button(frame, text="Shift %s" % name, state='disabled', | |
248 command=lambda name=name: self.shift(name), fg=color, | |
249 bg='black') | |
250 | |
251 self.scales[name] = scale | 95 self.scales[name] = scale |
252 self.shift_buttons[name] = shift | 96 self.shift_buttons[name] = shift |
253 self.go_buttons[name] = go | |
254 | 97 |
255 scale.scale_var.trace('w', \ | 98 scale.scale_var.trace('w', \ |
256 lambda x, y, z, name=name, scale=scale: self.xfade(name, scale)) | 99 lambda x, y, z, name=name, scale=scale: self.xfade(name, scale)) |
257 go.timer_var.trace('w', | 100 def shift(self, name): |
258 lambda x, y, z, scale=scale: scale.update_value_label()) | 101 for scale in self.scales.values(): |
259 | 102 scale.scale_var.set(0) |
260 def button_press(event, name=name, scale=scale): | 103 if name == 'Next': scale.update() |
261 self.no_shifts_until_release = 1 # prevent shifts until release | 104 print "shift", name |
262 def button_release(event, name=name, scale=scale): | 105 self.cuelist.shift((-1, 1)[name == 'Next']) |
263 self.no_shifts_until_release = 0 | 106 self.update_cue_display() |
264 self.autoshift(name, scale) | 107 def update_cue_display(self): |
265 | 108 current_cues = [cue.name for cue in self.cuelist.get_current_cues()] |
266 scale.scale.bind("<ButtonPress>", button_press) | 109 self.current_cues['text'] = ', '.join(current_cues) |
267 scale.scale.bind("<ButtonRelease>", button_release) | 110 def xfade(self, name, scale): |
268 faderframe.pack(side='bottom', fill='both', expand=1) | 111 scale_val = scale.scale_var.get() |
269 | 112 |
270 self.current_dir = 'Next' | 113 if scale_val == 1: |
271 self.cues_as_subs = {} | 114 if self.auto_shift: |
272 self.update_cue_cache() | 115 self.shift(name) |
273 def reload_cue_times(self): | |
274 prev, cur, next = self.cuelist.get_current_cues() | |
275 self.go_buttons['Next'].set_time(next.time) | |
276 def update_cue_cache(self, compute_dmx_levels=1): | |
277 """Rebuilds subs from the current cues. As this is expensive, we don't | |
278 do it unless necessary (i.e. whenever we shift or a cue is edited)""" | |
279 # print "update_cue_cache" | |
280 # load the subs to fade between | |
281 for cue, name in zip(self.cuelist.get_current_cues(), | |
282 ('Prev', 'Cur', 'Next')): | |
283 self.cues_as_subs[name] = cue.get_levels_as_sub() | |
284 if compute_dmx_levels: | |
285 self.compute_dmx_levels() | |
286 def compute_dmx_levels(self): | |
287 """Compute the DMX levels to send. This should get called whenever the | |
288 DMX levels could change: either during a crossfade or when a cue is | |
289 edited. Since this is called when we know that a change might occur, | |
290 we will send the new levels too.""" | |
291 cur_sub = self.cues_as_subs.get('Cur') | |
292 if cur_sub: | |
293 scale = self.scales[self.current_dir] | |
294 scale_val = scale.scale_var.get() | |
295 | |
296 other_sub = self.cues_as_subs[self.current_dir] | |
297 current_levels_as_sub = cur_sub.crossfade(other_sub, scale_val) | |
298 self.current_dmx_levels = current_levels_as_sub.get_dmx_list() | |
299 self.send_dmx_levels() | |
300 | |
301 # print "compute_dmx_levels: fade at", scale_val | |
302 # print "between", cur_sub.name, | |
303 # print "and", other_sub.name | |
304 # print | |
305 def send_dmx_levels(self, *args): | |
306 # print "send_dmx_levels", self.current_dmx_levels | |
307 if self.mute.get(): | |
308 dmxclient.outputlevels([0] * 68) | |
309 else: | |
310 dmxclient.outputlevels(self.current_dmx_levels) | |
311 self.last_levels_sent = time.time() | |
312 def send_dmx_levels_loop(self): | |
313 diff = time.time() - self.last_levels_sent | |
314 if diff >= 2: # too long since last send | |
315 self.send_dmx_levels() | |
316 self.after(200, self.send_dmx_levels_loop) | |
317 else: | |
318 self.after(int((2 - diff) * 100), self.send_dmx_levels_loop) | |
319 def get_scale_desc(self, val, name): | |
320 """Returns a description to the TimedGoButton""" | |
321 go_button = self.go_buttons.get(name) | |
322 if go_button: | |
323 time = go_button.get_time() | |
324 return "%0.2f%%, %0.1fs left" % (val, time - ((val / 100.0) * time)) | |
325 else: | |
326 return "%0.2f%%" % val | |
327 def toggle_autoshift(self): | |
328 for name, button in self.shift_buttons.items(): | |
329 if not self.auto_shift.get(): | |
330 button.pack(side='bottom', fill='both', expand=1) | |
331 else: | 116 else: |
332 button.pack_forget() | |
333 def shift(self, name): | |
334 # to prevent overshifting | |
335 if self.no_shifts_until_release: | |
336 return | |
337 # print "shift", name | |
338 | |
339 self.cuelist.shift((-1, 1)[name == 'Next']) | |
340 self.update_cue_cache(compute_dmx_levels=0) | |
341 for scale_name, scale in self.scales.items(): | |
342 # print "shift: setting scale to 0", scale_name | |
343 scale.scale.set(0) | |
344 self.go_buttons[scale_name].manual_override() | |
345 self.update_cue_cache(compute_dmx_levels=1) | |
346 | |
347 if self.auto_load_times.get(): | |
348 self.reload_cue_times() | |
349 def autoshift(self, name, scale): | |
350 scale_val = scale.scale_var.get() | |
351 | |
352 if scale_val == 1: | |
353 if self.auto_shift.get(): | |
354 self.shift(name) | |
355 def xfade(self, name, scale): | |
356 if self.auto_shift.get(): | |
357 self.autoshift(name, scale) | |
358 scale_val = scale.scale_var.get() | |
359 else: | |
360 scale_val = scale.scale_var.get() | |
361 if scale_val == 1: | |
362 self.shift_buttons[name]['state'] = 'normal' | 117 self.shift_buttons[name]['state'] = 'normal' |
363 else: | 118 else: |
364 # disable any dangerous shifting | 119 # disable any dangerous shifting |
365 self.shift_buttons[name]['state'] = 'disabled' | 120 self.shift_buttons[name]['state'] = 'disabled' |
366 | 121 |
367 d = self.opposite_direction(name) | |
368 if scale_val != 0: | 122 if scale_val != 0: |
369 # disable illegal three part crossfades | 123 # disable illegal three part crossfades |
370 self.scales[d].disable() | 124 # TODO: |
371 self.go_buttons[d].disable() | 125 # if name == 'Next': |
372 else: | 126 # disable go_prev button and slider, lock slider at 0 |
373 # undo above work | 127 pass |
374 self.scales[d].enable() | 128 else: |
375 self.go_buttons[d].enable() | 129 # undo above changes |
376 | 130 |
377 self.current_dir = name | 131 # Actually, TimedGoButton and LabelledScale can have enable/disable |
378 self.compute_dmx_levels() | 132 # methods which will only do the Tk calls if necessary |
379 def opposite_direction(self, d): | 133 pass |
380 if d == 'Next': | |
381 return 'Prev' | |
382 else: | |
383 return 'Next' | |
384 | 134 |
385 class Cue: | 135 class Cue: |
386 """A Cue has a name, a time, and any number of other attributes.""" | 136 """A Cue has a name, a time, and any number of other attributes.""" |
387 def __init__(self, name, time=3, sub_levels='', **attrs): | 137 def __init__(self, name, time=3, **attrs): |
388 self.name = name | 138 self.name = name |
389 self.time = time | 139 self.time = time |
390 self.sub_levels = sub_levels | |
391 self.__dict__.update(attrs) | 140 self.__dict__.update(attrs) |
392 def __repr__(self): | 141 def __repr__(self): |
393 return "<Cue %s, length %s>" % (self.name, self.time) | 142 return "<Cue %s, length %s>" % (self.name, self.time) |
394 def get_levels_as_sub(self): | |
395 """Get this Cue as a combined Submaster, normalized. This method | |
396 should not be called constantly, since it is somewhat expensive. It | |
397 will reload the submasters from disk, combine all subs together, and | |
398 then compute the normalized form.""" | |
399 subdict = {} | |
400 for line in self.sub_levels.split(','): | |
401 try: | |
402 line = line.strip() | |
403 if not line: | |
404 continue | |
405 sub, scale = line.split(':') | |
406 sub = sub.strip() | |
407 scale = float(scale) | |
408 subdict[sub] = scale | |
409 except ValueError: | |
410 print "Parsing error for '%s' in %s" % (self.sub_levels, self) | |
411 | |
412 s = Submaster.Submasters() | |
413 newsub = Submaster.sub_maxes(*[s[sub] * scale | |
414 for sub, scale in subdict.items()]) | |
415 return newsub.get_normalized_copy() | |
416 | 143 |
417 empty_cue = Cue('empty') | 144 empty_cue = Cue('empty') |
418 | 145 |
419 allow_class_to_be_pickled(Cue) | 146 allow_class_to_be_pickled(Cue) |
420 | 147 |
426 try: | 153 try: |
427 self.treedict.load(filename) | 154 self.treedict.load(filename) |
428 except IOError: | 155 except IOError: |
429 self.treedict.cues = [] | 156 self.treedict.cues = [] |
430 self.cues = self.treedict.cues | 157 self.cues = self.treedict.cues |
431 self.current_cue_index = -1 | 158 self.current_cue_index = 0 |
432 self.next_pointer = 0 | 159 self.next_pointer = None |
433 self.prev_pointer = None | 160 self.prev_pointer = None |
434 | 161 |
435 import atexit | 162 import atexit |
436 atexit.register(self.save) | 163 atexit.register(self.save) |
437 def add_cue(self, cue, index=None): | 164 def add_cue(self, cue, index=None): |
458 def set_next(self, index): | 185 def set_next(self, index): |
459 self.next_pointer = index | 186 self.next_pointer = index |
460 def set_prev(self, index): | 187 def set_prev(self, index): |
461 self.prev_pointer = index | 188 self.prev_pointer = index |
462 def bound_index(self, index): | 189 def bound_index(self, index): |
463 if not self.cues or index < 0: | 190 if not self.cues: |
464 return None | 191 return None |
465 else: | 192 else: |
466 return min(index, len(self.cues) - 1) | 193 return max(0, min(index, len(self.cues))) |
467 def get_current_cue_indices(self): | 194 def get_current_cue_indices(self): |
468 """Returns a list of the indices of three cues: the previous cue, | |
469 the current cue, and the next cue.""" | |
470 cur = self.current_cue_index | 195 cur = self.current_cue_index |
471 return [self.bound_index(index) for index in | 196 return [self.bound_index(index) for index in |
472 (self.prev_pointer or cur - 1, | 197 (self.prev_pointer or cur - 1, |
473 cur, | 198 cur, |
474 self.next_pointer or cur + 1)] | 199 self.next_pointer or cur + 1)] |
475 def get_current_cues(self): | 200 def get_current_cues(self): |
476 """Returns a list of three cues: the previous cue, the current cue, | 201 # print "get_current_cue_indices", self.get_current_cue_indices() |
477 and the next cue.""" | |
478 return [self.get_cue_by_index(index) | 202 return [self.get_cue_by_index(index) |
479 for index in self.get_current_cue_indices()] | 203 for index in self.get_current_cue_indices()] |
480 def get_cue_by_index(self, index): | 204 def get_cue_by_index(self, index): |
481 try: | 205 # print "get_cue_by_index", index |
206 if index: | |
482 return self.cues[self.bound_index(index)] | 207 return self.cues[self.bound_index(index)] |
483 except TypeError: | 208 else: |
484 return empty_cue | 209 return empty_cue |
485 def __del__(self): | 210 def __del__(self): |
486 self.save() | 211 self.save() |
487 def save(self, backup=0): | 212 def save(self): |
488 if backup: | 213 self.treedict.save(self.filename) |
489 | |
490 backupfilename = "%s-backup" % self.filename | |
491 print time.asctime(), "Saving backup version of cues to", \ | |
492 backupfilename | |
493 self.treedict.save(backupfilename) | |
494 else: | |
495 print time.asctime(), "Saving cues to", self.filename | |
496 self.treedict.save(self.filename) | |
497 def reload(self): | |
498 # TODO: we probably will need to make sure that indices still make | |
499 # sense, etc. | |
500 self.treedict.load(self.filename) | |
501 | |
502 class TkCueList(CueList, Tk.Frame): | |
503 def __init__(self, master, filename): | |
504 CueList.__init__(self, filename) | |
505 Tk.Frame.__init__(self, master, bg='black') | |
506 self.fader = None | |
507 | |
508 self.edit_tl = Tk.Toplevel() | |
509 self.editor = CueEditron(self.edit_tl, | |
510 changed_callback=self.cue_changed) | |
511 self.editor.pack(fill='both', expand=1) | |
512 | |
513 def edit_cue(index): | |
514 index = int(index) | |
515 self.editor.set_cue_to_edit(self.cues[index]) | |
516 | |
517 self.columns = ('name', 'time', 'page', 'cuenum', 'desc', 'sub_levels') | |
518 self.scrolled_hlist = Tk.ScrolledHList(self, | |
519 options='hlist.columns %d hlist.header 1' % len(self.columns)) | |
520 self.hlist = self.scrolled_hlist.hlist | |
521 self.hlist.configure(fg='white', bg='black', | |
522 command=self.select_callback, browsecmd=edit_cue) | |
523 self.hlist.bind("<4>", self.wheelscroll) | |
524 self.hlist.bind("<5>", self.wheelscroll) | |
525 self.scrolled_hlist.pack(fill='both', expand=1) | |
526 | |
527 boldfont = self.tk.call('tix', 'option', 'get', | |
528 'bold_font') | |
529 header_style = Tk.DisplayStyle('text', refwindow=self, | |
530 anchor='center', padx=8, pady=2, font=boldfont) | |
531 | |
532 for count, header in enumerate(self.columns): | |
533 self.hlist.header_create(count, itemtype='text', | |
534 text=header, style=header_style) | |
535 | |
536 self.cue_label_windows = {} | |
537 for count, cue in enumerate(self.cues): | |
538 self.display_cue(count, cue) | |
539 self.update_cue_indicators() | |
540 self.save_loop() | |
541 def set_fader(self, fader): | |
542 self.fader = fader | |
543 def wheelscroll(self, evt): | |
544 """Perform mouse wheel scrolling""" | |
545 if evt.num == 4: # scroll down | |
546 amount = -2 | |
547 else: # scroll up | |
548 amount = 2 | |
549 self.hlist.yview('scroll', amount, 'units') | |
550 def cue_changed(self, cue): | |
551 path = self.cues.index(cue) | |
552 for col, header in enumerate(self.columns): | |
553 try: | |
554 text = getattr(cue, header) | |
555 except AttributeError: | |
556 text = '' | |
557 | |
558 if col == 0: | |
559 self.cue_label_windows[path]['text'] = text | |
560 else: | |
561 self.hlist.item_configure(path, col, text=text) | |
562 | |
563 if cue in self.get_current_cues() and self.fader: | |
564 self.fader.update_cue_cache() | |
565 self.fader.reload_cue_times() | |
566 def display_cue(self, path, cue): | |
567 for col, header in enumerate(self.columns): | |
568 try: | |
569 text = getattr(cue, header) | |
570 except AttributeError: | |
571 text = '' | |
572 | |
573 if col == 0: | |
574 lab = Tk.Label(self.hlist, text=text, fg='white', bg='black') | |
575 def select_and_highlight(event): | |
576 self.select_callback(path) | |
577 self.hlist.selection_clear() | |
578 self.hlist.selection_set(path) | |
579 | |
580 lab.bind("<Double-1>", select_and_highlight) | |
581 self.hlist.add(path, itemtype='window', window=lab) | |
582 self.cue_label_windows[path] = lab | |
583 else: | |
584 self.hlist.item_create(path, col, text=text) | |
585 def reset_cue_indicators(self, cue_indices=None): | |
586 """If cue_indices is None, we'll reset all of them.""" | |
587 cue_indices = cue_indices or self.cue_label_windows.keys() | |
588 for key in cue_indices: | |
589 if key is None: | |
590 continue | |
591 window = self.cue_label_windows[key] | |
592 window.configure(fg='white', bg='black') | |
593 def update_cue_indicators(self): | |
594 states = dict(zip(self.get_current_cue_indices(), | |
595 ('prev', 'cur', 'next'))) | |
596 | |
597 for count, state in states.items(): | |
598 if count is None: | |
599 continue | |
600 window = self.cue_label_windows[count] | |
601 bg, fg = cue_state_indicator_colors[state] | |
602 window.configure(bg=bg, fg=fg) | |
603 def shift(self, diff): | |
604 self.reset_cue_indicators(self.get_current_cue_indices()) | |
605 CueList.shift(self, diff) | |
606 self.update_cue_indicators() | |
607 # try to see all indices, but next takes priority over all, and cur | |
608 # over prev | |
609 for index in self.get_current_cue_indices(): | |
610 if index is not None: | |
611 self.hlist.see(index) | |
612 def select_callback(self, index): | |
613 new_next = int(index) | |
614 self.set_next(new_next) | |
615 def set_next(self, index): | |
616 prev, cur, next = self.get_current_cue_indices() | |
617 self.reset_cue_indicators((next,)) | |
618 CueList.set_next(self, index) | |
619 self.update_cue_indicators() | |
620 | |
621 if self.fader: # XXX this is untested | |
622 self.fader.update_cue_cache() | |
623 def set_prev(self, index): | |
624 prev, cur, next = self.get_current_cue_indices() | |
625 self.reset_cue_indicators((prev,)) | |
626 CueList.set_prev(self, index) | |
627 self.update_cue_indicators() | |
628 | |
629 if self.fader: # XXX this is untested | |
630 self.fader.update_cue_cache() | |
631 def set_selection_as_prev(self): | |
632 sel = self.hlist.info_selection() | |
633 if sel: | |
634 self.set_prev(int(sel[0])) | |
635 def set_selection_as_next(self): | |
636 sel = self.hlist.info_selection() | |
637 if sel: | |
638 self.set_next(int(sel[0])) | |
639 def save_loop(self): | |
640 """This saves the CueList every minute.""" | |
641 self.save(backup=1) | |
642 self.after(60000, self.save_loop) | |
643 | |
644 class CueEditron(Tk.Frame): | |
645 def __init__(self, master, changed_callback=None, cue=None): | |
646 Tk.Frame.__init__(self, master, bg='black') | |
647 self.master = master | |
648 self.cue = cue | |
649 self.changed_callback = changed_callback | |
650 self.enable_callbacks = 1 | |
651 | |
652 self.setup_editing_forms() | |
653 self.set_cue_to_edit(cue) | |
654 def set_cue_to_edit(self, cue): | |
655 if cue != self.cue: | |
656 self.cue = cue | |
657 self.fill_in_cue_info() | |
658 self.set_title() | |
659 def set_title(self): | |
660 try: | |
661 self.master.title("Editing '%s'" % self.cue.name) | |
662 except AttributeError: | |
663 pass | |
664 def setup_editing_forms(self): | |
665 self.variables = {} | |
666 for row, field in enumerate(('name', 'time', 'page', 'cuenum', 'desc', | |
667 'sub_levels')): | |
668 lab = Tk.Label(self, text=field, fg='white', bg='black') | |
669 lab.grid(row=row, column=0, sticky='nsew') | |
670 | |
671 entryvar = Tk.StringVar() | |
672 entry = Tk.Entry(self, fg='white', bg='black', | |
673 textvariable=entryvar, insertbackground='white', | |
674 highlightcolor='red') # TODO this red/black is backwards | |
675 entry.grid(row=row, column=1, sticky='nsew') | |
676 | |
677 self.variables[field] = entryvar | |
678 | |
679 def field_changed(x, y, z, field=field, entryvar=entryvar): | |
680 if self.cue: | |
681 setattr(self.cue, field, entryvar.get()) | |
682 if self.enable_callbacks and self.changed_callback: | |
683 self.changed_callback(self.cue) | |
684 if field == 'name': | |
685 self.set_title() | |
686 | |
687 entryvar.trace('w', field_changed) | |
688 self.columnconfigure(1, weight=1) | |
689 def fill_in_cue_info(self): | |
690 self.enable_callbacks = 0 | |
691 for row, field in enumerate(('name', 'time', 'page', 'cuenum', 'desc', | |
692 'sub_levels')): | |
693 text = '' | |
694 if self.cue: | |
695 try: | |
696 text = getattr(self.cue, field) | |
697 except AttributeError: | |
698 pass | |
699 self.variables[field].set(text) | |
700 self.enable_callbacks = 1 | |
701 | 214 |
702 if __name__ == "__main__": | 215 if __name__ == "__main__": |
703 root = Tk.Tk() | 216 if 0: |
704 root.title("ShowMaster 9000") | 217 z = CueList('cues/cuelist1') |
705 root.geometry("600x670") | 218 z.add_cue(Cue('cue %s' % time.asctime(), time=2, a=7, b=8)) |
706 cl = TkCueList(root, 'cues/dolly') | 219 print 'cues', z.cues |
707 cl.pack(fill='both', expand=1) | 220 else: |
708 | 221 cl = CueList('cues/cuelist1') |
709 fader = CueFader(root, cl) | 222 |
710 fader.pack(fill='both', expand=1) | 223 # to populate cue list |
711 try: | 224 if 0: |
225 for x in range(20): | |
226 cl.add_cue(Cue('cue %d' % x, time=x, some_attribute=3)) | |
227 | |
228 root = Tk.Tk() | |
229 fader = CueFader(root, cl) | |
230 fader.pack(fill='both', expand=1) | |
712 Tk.mainloop() | 231 Tk.mainloop() |
713 except KeyboardInterrupt: | |
714 root.destroy() |