0
|
1 from __future__ import division, nested_scopes
|
|
2 import Tix as Tk
|
|
3 import time
|
|
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
|
|
23 class LabelledScale(Tk.Frame):
|
|
24 """Scale with two labels: a name and current value"""
|
|
25 def __init__(self, master, label, **opts):
|
|
26 Tk.Frame.__init__(self, master, bd=2, relief='raised', bg='black')
|
|
27 self.labelformatter = opts.get('labelformatter')
|
|
28 try:
|
|
29 del opts['labelformatter']
|
|
30 except KeyError:
|
|
31 pass
|
|
32
|
|
33 opts.setdefault('variable', Tk.DoubleVar())
|
|
34 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']
|
|
44 self.scale = Tk.Scale(self, **opts)
|
|
45 self.scale.pack(side='top', expand=1, fill='both')
|
|
46 self.name = Tk.Label(self, text=label, bg='black', fg='white')
|
|
47 self.name.pack(side='bottom')
|
|
48 self.scale_value = Tk.Label(self, bg='black', fg='white')
|
|
49 self.scale_value.pack(side='bottom')
|
|
50 self.scale_var.trace('w', self.update_value_label)
|
|
51 self.update_value_label()
|
|
52 self.disabled = (self.scale['state'] == 'disabled')
|
|
53 def set_label(self, label):
|
|
54 self.name['text'] = label
|
|
55 def update_value_label(self, *args):
|
|
56 val = self.scale_var.get() * 100
|
|
57 if self.labelformatter:
|
|
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
|
|
76 class TimedGoButton(Tk.Frame):
|
|
77 """Go button, fade time entry, and time fader"""
|
|
78 def __init__(self, master, name, scale_to_fade, **kw):
|
|
79 Tk.Frame.__init__(self, master, bg='black')
|
|
80 self.name = name
|
|
81 self.scale_to_fade = scale_to_fade
|
|
82 self.button = Tk.Button(self, text=name, command=self.start_fade, **kw)
|
|
83 self.button.pack(fill='both', expand=1, side='left')
|
|
84
|
|
85 self.timer_var = Tk.DoubleVar()
|
|
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')
|
|
94 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):
|
|
108 self.last_start_time = self.start_time
|
|
109 self.start_time = time.time()
|
|
110 self.start_level = self.scale_to_fade.scale_var.get()
|
|
111 self.end_level = end_level
|
|
112
|
|
113 if self.fading == 1: # if we're already fading
|
|
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):
|
|
137 diff = time.time() - self.start_time
|
|
138 if diff < self.fade_length:
|
|
139 percent = diff / self.fade_length
|
|
140 newlevel = self.start_level + \
|
|
141 (percent * (self.end_level - self.start_level))
|
|
142 self.scale_to_fade.scale_var.set(newlevel)
|
|
143
|
|
144 if newlevel != self.end_level:
|
|
145 self.last_after_key = self.after(10, self.do_fade)
|
|
146 else:
|
|
147 self.end_fade()
|
|
148 else:
|
|
149 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
|
|
170 class CueFader(Tk.Frame):
|
|
171 def __init__(self, master, cuelist):
|
|
172 Tk.Frame.__init__(self, master, bg='black')
|
|
173 self.cuelist = cuelist
|
|
174 self.cuelist.set_fader(self)
|
|
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
|
|
186 self.scales = {}
|
|
187 self.shift_buttons = {}
|
|
188 self.go_buttons = {}
|
|
189
|
|
190 topframe = Tk.Frame(self, bg='black')
|
|
191
|
|
192 self.set_prev_button = Tk.Button(topframe, text='Set Prev',
|
|
193 command=lambda: cuelist.set_selection_as_prev(),
|
|
194 fg='white', bg='blue')
|
|
195 self.set_prev_button.pack(side='left')
|
|
196
|
|
197 self.auto_shift = Tk.IntVar()
|
|
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,
|
|
238 res=0.0001, orient='horiz', flashtroughcolor=color,
|
|
239 labelformatter=lambda val, name=name: self.get_scale_desc(val,
|
|
240 name))
|
|
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)
|
|
245 frame.pack(side=side, fill='both', expand=1)
|
|
246
|
|
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
|
|
252 self.shift_buttons[name] = shift
|
|
253 self.go_buttons[name] = go
|
|
254
|
|
255 scale.scale_var.trace('w', \
|
|
256 lambda x, y, z, name=name, scale=scale: self.xfade(name, scale))
|
|
257 go.timer_var.trace('w',
|
|
258 lambda x, y, z, scale=scale: scale.update_value_label())
|
|
259
|
|
260 def button_press(event, name=name, scale=scale):
|
|
261 self.no_shifts_until_release = 1 # prevent shifts until release
|
|
262 def button_release(event, name=name, scale=scale):
|
|
263 self.no_shifts_until_release = 0
|
|
264 self.autoshift(name, scale)
|
|
265
|
|
266 scale.scale.bind("<ButtonPress>", button_press)
|
|
267 scale.scale.bind("<ButtonRelease>", button_release)
|
|
268 faderframe.pack(side='bottom', fill='both', expand=1)
|
|
269
|
|
270 self.current_dir = 'Next'
|
|
271 self.cues_as_subs = {}
|
|
272 self.update_cue_cache()
|
|
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:
|
|
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'
|
|
363 else:
|
|
364 # disable any dangerous shifting
|
|
365 self.shift_buttons[name]['state'] = 'disabled'
|
|
366
|
|
367 d = self.opposite_direction(name)
|
|
368 if scale_val != 0:
|
|
369 # disable illegal three part crossfades
|
|
370 self.scales[d].disable()
|
|
371 self.go_buttons[d].disable()
|
|
372 else:
|
|
373 # undo above work
|
|
374 self.scales[d].enable()
|
|
375 self.go_buttons[d].enable()
|
|
376
|
|
377 self.current_dir = name
|
|
378 self.compute_dmx_levels()
|
|
379 def opposite_direction(self, d):
|
|
380 if d == 'Next':
|
|
381 return 'Prev'
|
|
382 else:
|
|
383 return 'Next'
|
|
384
|
|
385 class Cue:
|
|
386 """A Cue has a name, a time, and any number of other attributes."""
|
|
387 def __init__(self, name, time=3, sub_levels='', **attrs):
|
|
388 self.name = name
|
|
389 self.time = time
|
|
390 self.sub_levels = sub_levels
|
|
391 self.__dict__.update(attrs)
|
|
392 def __repr__(self):
|
|
393 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
|
|
417 empty_cue = Cue('empty')
|
|
418
|
|
419 allow_class_to_be_pickled(Cue)
|
|
420
|
|
421 class CueList:
|
|
422 """Persistent list of Cues"""
|
|
423 def __init__(self, filename):
|
|
424 self.filename = filename
|
|
425 self.treedict = TreeDict()
|
|
426 try:
|
|
427 self.treedict.load(filename)
|
|
428 except IOError:
|
|
429 self.treedict.cues = []
|
|
430 self.cues = self.treedict.cues
|
|
431 self.current_cue_index = -1
|
|
432 self.next_pointer = 0
|
|
433 self.prev_pointer = None
|
|
434
|
|
435 import atexit
|
|
436 atexit.register(self.save)
|
|
437 def add_cue(self, cue, index=None):
|
|
438 """Adds a Cue object to the list. If no index is specified,
|
|
439 the cue will be added to the end."""
|
|
440 index = index or len(self.cues)
|
|
441 self.cues.insert(index, cue)
|
|
442 def shift(self, diff):
|
|
443 """Shift through cue history"""
|
|
444 old_index = self.current_cue_index
|
|
445 self.current_cue_index = None
|
|
446 if diff < 0: # if going backwards
|
|
447 if self.prev_pointer: # use a prev pointer if we have one
|
|
448 self.current_cue_index = self.prev_pointer
|
|
449 self.next_pointer = old_index
|
|
450 self.prev_pointer = None
|
|
451 else:
|
|
452 if self.next_pointer: # use a next pointer if we have one
|
|
453 self.current_cue_index = self.next_pointer
|
|
454 self.next_pointer = None
|
|
455 self.prev_pointer = old_index
|
|
456 if not self.current_cue_index:
|
|
457 self.current_cue_index = old_index + diff
|
|
458 def set_next(self, index):
|
|
459 self.next_pointer = index
|
|
460 def set_prev(self, index):
|
|
461 self.prev_pointer = index
|
|
462 def bound_index(self, index):
|
|
463 if not self.cues or index < 0:
|
|
464 return None
|
|
465 else:
|
|
466 return min(index, len(self.cues) - 1)
|
|
467 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
|
|
471 return [self.bound_index(index) for index in
|
|
472 (self.prev_pointer or cur - 1,
|
|
473 cur,
|
|
474 self.next_pointer or cur + 1)]
|
|
475 def get_current_cues(self):
|
|
476 """Returns a list of three cues: the previous cue, the current cue,
|
|
477 and the next cue."""
|
|
478 return [self.get_cue_by_index(index)
|
|
479 for index in self.get_current_cue_indices()]
|
|
480 def get_cue_by_index(self, index):
|
|
481 try:
|
|
482 return self.cues[self.bound_index(index)]
|
|
483 except TypeError:
|
|
484 return empty_cue
|
|
485 def __del__(self):
|
|
486 self.save()
|
|
487 def save(self, backup=0):
|
|
488 if backup:
|
|
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
|
|
702 if __name__ == "__main__":
|
|
703 root = Tk.Tk()
|
|
704 root.title("ShowMaster 9000")
|
|
705 root.geometry("600x670")
|
|
706 cl = TkCueList(root, 'cues/dolly')
|
|
707 cl.pack(fill='both', expand=1)
|
|
708
|
|
709 fader = CueFader(root, cl)
|
|
710 fader.pack(fill='both', expand=1)
|
|
711 try:
|
|
712 Tk.mainloop()
|
|
713 except KeyboardInterrupt:
|
|
714 root.destroy()
|