| blc2 - Python library and frontend for running theatrical lighting and SFX. Written for the English 2041F fall 2019 theatre production of "The Cenci".
git clone https://benconnors.ca/git-repos/blc2 |
interface.py (39959B) - raw
1 import curses 2 import os 3 import datetime as dt 4 import itertools 5 import threading 6 import traceback as tb 7 8 import blc2 9 10 from blc2.functions.audio import Audio 11 from blc2.functions.scene import Scene 12 from blc2.functions.chaser import Chaser 13 from blc2.functions.chaserstep import ChaserStep 14 from blc2.functions.join import Join 15 from blc2.constants import SCENE, CHASER, AUDIO, MANUAL, INHERIT, INFTY, CHASERSTEP, JOIN, ONESHOT, LOOP, RANDOM 16 17 from blc2.topology import Fixture 18 19 from blc2.workspace import Workspace 20 21 from .globals import CURSES_LOCK 22 from .input.tabcomp import Input 23 from .channelbank import ChannelBank 24 from .chaserview import ChaserView 25 from .pager import Pager 26 from .dialog import askyesnocancel 27 from .audioview import AudioView 28 from .render import Renderer 29 from .dummy import DummyOutput 30 31 def wrap_curses(f): 32 def inner(*args, **kwargs): 33 return curses.wrapper(lambda stdscr: f(*args, stdscr, **kwargs)) 34 return inner 35 36 __version__ = "v0.1.0" 37 38 class Interface: 39 def _compute_sizes(self, height, width): 40 hb = height // 2 41 ht = height - hb 42 43 wr = width // 2 44 wl = width - wr - 1 45 46 chasers = [] 47 if self.chaser_views: 48 current = 0 49 cw = (wl // len(self.chaser_views)) - 1 50 for i in range(len(self.chaser_views)): 51 curw = (wl - current) if (i+1) == len(self.chaser_views) else cw 52 chasers.append(((hb, curw), (ht, current))) 53 current += curw + 1 54 55 return ( 56 ((ht, width), (0, 0),), 57 ((4, wr), (height-4, wl+1),), 58 ((hb - 4, wr), (ht, wl+1),), 59 *chasers 60 ) 61 62 def _resize(self): 63 ## FIXME 64 try: 65 self._actual_resize() 66 self._actual_resize() 67 except: 68 self.stdscr.addstr(0, 0, "Too small") 69 self.stdscr.refresh() 70 return False 71 return True 72 73 def _actual_resize(self): 74 for (a1, a2), (f1, f2) in zip(self._compute_sizes(*self.stdscr.getmaxyx()), ((self.channel_bank.set_dim, self.channel_bank.set_pos), (self.input.set_dim, self.input.set_pos), (self.pager.set_dim, self.pager.set_pos), *((c.set_dim, c.set_pos) for c in self.chaser_views))): 75 f1(*a1) 76 f2(*a2) 77 78 @wrap_curses 79 def main(self, stdscr): 80 height, width = stdscr.getmaxyx() 81 self.stdscr = stdscr 82 (cbd, cbp), (ind, inp), (pgd, pgp) = self._compute_sizes(height, width) 83 self.channel_bank = ChannelBank(*cbp, *cbd) 84 self.input = Input(*inp, *ind) 85 self.input.context = self.context_base 86 self.pager = Pager(*pgp, *pgd) 87 88 todisp = [ 89 "Welcome to BLC2!", 90 "Currently running library %s, interface %s" % (blc2.__version__, __version__), 91 "", 92 ] 93 94 if self._w_created: 95 todisp.append("Created a new workspace") 96 else: 97 todisp.append("Loaded workspace \"%s\" from \"%s\"" % (self.w.name, self.path)) 98 todisp.append("Authored by %s, last modified at %s" % (self.w.author, self.w.modified.strftime("%Y-%m-%d %H:%M:%S"))) 99 100 self.pager.display_many(todisp) 101 102 if not self.output.ok: 103 self.pager.display_many(("WARNING: Output is not OK!",), True) 104 105 self.verify_audio() 106 107 self.input.main(self._resize) 108 109 def verify_audio(self): 110 def gather_audio(func): 111 if func.type == AUDIO: 112 return (func,) 113 elif func.type in (CHASER, JOIN): 114 return tuple(itertools.chain(*(gather_audio(i) for i in func.steps))) 115 elif func.type == CHASERSTEP and func.function is not None: 116 return gather_audio(func.function) 117 118 return () 119 120 f = None 121 bad = [] 122 with self.w_lock: 123 funcs = set() 124 if f is None: 125 for func in self.w.functions.values(): 126 funcs.update(gather_audio(func)) 127 else: 128 funcs = gather_audio(f) 129 130 for f in sorted(funcs, key=lambda i: i.id): 131 if not os.path.isfile(f.filename): 132 bad.append("- %3d \"%s\"" % (f.id, f.filename)) 133 134 if bad: 135 self.pager.display_many(["MISSING AUDIO FILES:"] + bad) 136 else: 137 self.pager.display_many(("No missing audio files!",)) 138 139 def handle_show(self, f): 140 with self.w_lock: 141 if f is None or f.type == SCENE: 142 if not isinstance(self.channel_bank, ChannelBank): 143 (hw, yx), *_ = self._compute_sizes(*self.stdscr.getmaxyx()) 144 self.channel_bank = ChannelBank(*yx, *hw) 145 if f is None: 146 return 147 self.channel_bank.set_scope(f.scope) 148 self.channel_bank.set_values(f.values.items()) 149 elif f.type == AUDIO: 150 if not isinstance(self.channel_bank, AudioView): 151 (hw, yx), *_ = self._compute_sizes(*self.stdscr.getmaxyx()) 152 self.channel_bank = AudioView(*yx, *hw) 153 self.channel_bank.audio = f 154 self.channel_bank.title = f.type + ' "%s"' % f.name 155 self.renderer.clear_hold() 156 if f.type == SCENE: 157 self.renderer.hold(f.values) 158 159 def handle_enter(self, fid): 160 with self.w_lock: 161 if fid is None: 162 if not self.chaser_views: 163 return "No chaser loaded" 164 f = self.chaser_views[0].chaser 165 else: 166 if fid not in self.w.functions: 167 return "No such function" 168 f = self.w.functions[fid] 169 170 if self.current_cv is not None: 171 self.current_cv.highlight = False 172 173 if self.chaser is not None: 174 cv = [c for c in self.chaser_views if c.chaser == self.chaser][0].selected 175 self.chaser_stack.append((self.chaser, cv)) 176 self.chaser = None 177 178 if f.type == SCENE: 179 self.primitive = f 180 self.handle_show(f) 181 self.channel_bank.highlight = True 182 self.input.context = self.context_scene 183 elif f.type in (CHASER, JOIN): 184 self.chaser = f 185 cv = [c for c in self.chaser_views if c.chaser == f] 186 if not cv: 187 if len(self.chaser_views) == 2: 188 self.chaser_views[1].set_chaser(f, None) 189 else: 190 self.base_add(f.id) 191 self.current_cv = self.chaser_views[-1] 192 else: 193 cv = cv[0] 194 self.current_cv = cv 195 self.current_cv.highlight = True 196 if f.type == CHASER: 197 self.input.context = self.context_chaser 198 elif f.type == JOIN: 199 self.input.context = self.context_join 200 elif f.type == AUDIO: 201 self.primitive = f 202 self.handle_show(f) 203 self.channel_bank.highlight = True 204 self.input.context = self.context_audio 205 else: 206 ## FIXME 207 return "No other types allowed yet" 208 209 def base_add(self, fid): 210 with self.w_lock: 211 if fid not in self.w.functions: 212 return "No such function" 213 f = self.w.functions[fid] 214 if f.type not in (CHASER, JOIN): 215 return "Can only add chasers!" 216 with CURSES_LOCK: 217 if True in (c.chaser.id for c in self.chaser_views): 218 return "Already added!" 219 elif len(self.chaser_views) == 2: 220 ## FIXME? 221 return "Can only use two for now" 222 self.chaser_views.append(None) 223 ld, lp = self._compute_sizes(*self.stdscr.getmaxyx())[-1] 224 self.chaser_views[-1] = ChaserView(*lp, *ld) 225 self._resize() 226 self.chaser_views[-1].set_chaser(f, None) 227 228 def base_remove(self, n, index=True): 229 with self.w_lock: 230 if not index: 231 n = [j for j, i in enumerate(self.chaser_views) if i.chaser.id == n] 232 if not n: 233 return "No such chaser loaded" 234 n = n[0] 235 if n >= len(self.chaser_views) or n < 0: 236 return "Index out of range" 237 with CURSES_LOCK: 238 self.chaser_views[n].win.erase() 239 self.chaser_views[n].win.refresh() 240 self.chaser_views.pop(n) 241 self._resize() 242 243 def base_delete(self, fid): 244 with self.w_lock: 245 if fid not in self.w.functions: 246 return "No such function" 247 f = self.w.functions[fid] 248 f.delete() 249 250 def base_new(self, name, cls): 251 with self.w_lock: 252 f = cls(self.w, name=name) 253 self.handle_enter(f.id) 254 255 def _gather_channels(self, cr): 256 with self.w_lock: 257 ## Gather the affected channels 258 channels = [] 259 for f in self.w.fixtures.values(): 260 for s, e in cr[0]: 261 if (s <= f.id <= e) or (e == -1 and f.id >= s): 262 break 263 else: 264 continue 265 266 for n, c in enumerate(f.channels): 267 for s, e in cr[1]: 268 if (s <= n <= e) or (e == -1 and n >= s): 269 channels.append(c) 270 break 271 272 return channels 273 274 def scene_set(self, cr, v): 275 with self.w_lock: 276 channels = self._gather_channels(cr) 277 278 ## Set the values 279 self.primitive.update({c: v for c in channels}) 280 281 ## Update the display 282 self.channel_bank.set_scope(self.primitive.scope) 283 if v is not None: 284 self.channel_bank.set_values(((c, v) for c in channels)) 285 self.renderer.clear_hold() 286 self.renderer.hold(self.primitive.values) 287 288 def scene_edit(self, cr, force = False): 289 with self.w_lock: 290 291 channels = self._gather_channels(cr) 292 if not force: 293 channels = [c for c in channels if c in self.primitive.scope] 294 else: 295 self.primitive.update({c: 0 for c in channels if c not in self.primitive.scope}) 296 297 if not channels: 298 return "No channels given" 299 300 curses.curs_set(0) 301 values = self.primitive.values 302 303 self.channel_bank.set_active(channels, True) 304 305 while True: 306 self.channel_bank.set_scope(self.primitive.scope) 307 self.channel_bank.set_values(((c, v) for c, v in values.items())) 308 self.renderer.clear_hold() 309 self.renderer.hold(self.primitive.values) 310 311 l = self.pager.win.getch() 312 delta = 0 313 314 if l == curses.KEY_RESIZE: 315 self._resize() 316 elif l == ord('q'): 317 break 318 elif l in (ord('j'), curses.KEY_DOWN): 319 delta = -5 320 elif l in (ord('k'), curses.KEY_UP): 321 delta = 5 322 elif l == ord('h'): 323 delta = -1 324 elif l == ord('l'): 325 delta = 1 326 elif l == curses.KEY_NPAGE: 327 delta = 25 328 elif l == curses.KEY_PPAGE: 329 delta = -25 330 elif l == curses.KEY_HOME: 331 delta = 255 332 elif l == curses.KEY_END: 333 delta = -255 334 335 if delta != 0: 336 for c in channels: 337 values[c] = max(0, min(255, values[c]+delta)) 338 self.primitive.update(values) 339 340 self.channel_bank.set_active(channels, False) 341 342 curses.curs_set(1) 343 344 def scene_clear(self, cr): 345 self.scene_set(cr, None) 346 347 def scene_exit(self): 348 self.primitive = None 349 350 self.handle_exit() 351 352 def audio_exit(self): 353 self.primitive = None 354 355 self.handle_exit() 356 357 def handle_exit(self): 358 for c in self.chaser_views: 359 c.highlight = False 360 361 if self.current_cv is not None: 362 self.current_cv.selected = None 363 364 self.channel_bank.highlight = False 365 366 if self.chaser_stack: 367 ## Load the previous chaser 368 c, sel = self.chaser_stack.pop(-1) 369 370 if True not in (True for i in self.chaser_views if i.chaser.id == c.id): 371 ## We need to load it 372 ## Check if we have one for the current chaser 373 try: 374 idx = [i.chaser.id for i in self.chaser_views].index(self.chaser.id) 375 except (ValueError, AttributeError): 376 ## We don't have it 377 if len(self.chaser_views) == 2: 378 ## We don't have room for it, just replace the last one 379 self.chaser_views[1].set_chaser(c, sel) 380 else: 381 self.base_add(c.id) 382 self.current_cv = self.chaser_views[-1] 383 else: 384 self.chaser_views[idx].set_chaser(c, sel) 385 self.current_cv = self.chaser_views[idx] 386 387 self.current_cv.highlight = True 388 self.current_cv.selected = sel 389 if c.type == CHASER: 390 self.input.context = self.context_chaser 391 else: 392 self.input.context = self.context_join 393 self.chaser = c 394 else: 395 if isinstance(self.channel_bank, AudioView): 396 (hw, yx), *_ = self._compute_sizes(*self.stdscr.getmaxyx()) 397 self.channel_bank = ChannelBank(*yx, *hw) 398 self.channel_bank.set_scope(()) 399 self.channel_bank.title = "Channels" 400 401 self.input.context = self.context_base 402 self.chaser = None 403 self.current_cv = None 404 self.renderer.clear_hold() 405 406 def base_save(self, path=None): 407 with self.w_lock: 408 if path is not None: 409 self.path = path 410 if self.path is None: 411 return "No path set" 412 self.w.modified = dt.datetime.now() 413 self.w.save(self.path) 414 415 return "Saved to "+self.path 416 417 def base_quit(self): 418 if askyesnocancel(self.stdscr, "Really exit?"): 419 quit() 420 else: 421 self._resize() 422 423 def list_fixtures(self): 424 with self.w_lock: 425 td = ["FIXTURES:"] 426 for f in sorted(self.w.fixtures.values(), key=lambda a: a.id): 427 td.append("- %03d: %s" % (f.id, f.name)) 428 for c in f.channels: 429 td.append(" %03d-%03d: %s" % (f.id, c.id, c.name)) 430 self.pager.display_many(td, split=True) 431 432 def list_functions(self, typ): 433 with self.w_lock: 434 td = [typ.upper()+"S:"] 435 for f in sorted(self.w.functions.values(), key=lambda a: a.id): 436 if f.type == typ: 437 td.append("- %03d: %s" % (f.id, f.name)) 438 self.pager.display_many(td, split=True) 439 440 def page(self): 441 self.pager.user_page() 442 443 def page_clear(self): 444 self.pager.clear() 445 446 def help(self, name, commands, help_): 447 if name is None: 448 dname = "MODE HELP:" 449 else: 450 dname = name.upper() + " COMMAND:" 451 452 todisp = [dname, ""] 453 if commands: 454 if name is None: 455 todisp.append("Available commands:") 456 else: 457 todisp.append("Available forms:") 458 todisp.extend(("- "+i for i in commands)) 459 todisp.append("") 460 461 todisp.extend(help_.split('\n')) 462 463 self.pager.display_many(todisp, split=True) 464 465 def chaser_select(self, num): 466 with self.w_lock: 467 if num < 1 or num > len(self.current_cv.chaser.steps): 468 return "Out of range" 469 470 self.current_cv.selected = num - 1 471 s = self.current_cv.chaser.steps[num-1] 472 if (s.type == CHASERSTEP and s.function is not None and s.function.type in (SCENE, AUDIO,)) or s.type in (SCENE, AUDIO,): 473 self.handle_show(s.function if s.type == CHASERSTEP else s) 474 475 def chaser_edit(self, num=None): 476 with self.w_lock: 477 if num is not None: 478 r = self.chaser_select(num) 479 if r is not None: 480 return r 481 elif self.current_cv.selected is None: 482 return "No step selected" 483 484 c = self.current_cv.chaser.steps[self.current_cv.selected] 485 if c.type == CHASERSTEP and c.function is None: 486 return "No function on step" 487 488 self.handle_enter(c.function.id if c.type == CHASERSTEP else c.id) 489 490 def chaser_delete(self, num=None): 491 with self.w_lock: 492 if num is not None: 493 r = self.chaser_select(num) 494 if r is not None: 495 return r 496 elif self.current_cv.selected is None: 497 return "No step selected" 498 499 c = self.current_cv.chaser 500 s = c.steps[self.current_cv.selected] 501 sel = self.current_cv.selected 502 with CURSES_LOCK: 503 if c.type == CHASER: 504 if askyesnocancel(self.stdscr, "Really delete step %d?" % (s.index+1), resize=self._resize): 505 sel = s.index 506 s.delete() 507 else: 508 return 509 else: 510 c.delete_step(s) 511 512 if not c.steps: 513 sel = None 514 else: 515 sel = max(0, sel-1) 516 self.current_cv.set_chaser(c, sel) 517 self._resize() 518 519 def chaser_fade(self, t, out=False): 520 with self.w_lock: 521 if self.current_cv.selected is None: 522 return "No step selected" 523 c = self.current_cv.chaser 524 s = c.steps[self.current_cv.selected] 525 if out: 526 s.fade_out = t 527 else: 528 s.fade_in = t 529 self.current_cv.set_chaser(c, s.index) 530 531 def chaser_duration(self, t): 532 with self.w_lock: 533 if self.current_cv.selected is None: 534 return "No step selected" 535 c = self.current_cv.chaser 536 s = c.steps[self.current_cv.selected] 537 if s.duration_mode != MANUAL: 538 s.duration_mode = MANUAL 539 s.duration = t 540 self.current_cv.set_chaser(c, s.index) 541 542 def chaser_unset(self): 543 with self.w_lock: 544 if self.current_cv.selected is None: 545 return "No step selected" 546 c = self.current_cv.chaser 547 s = c.steps[self.current_cv.selected] 548 s.duration_mode = INHERIT 549 self.current_cv.set_chaser(c, s.index) 550 551 def chaser_new(self, index, name, fid=None): 552 with self.w_lock: 553 if fid is not None: 554 if fid not in self.w.functions: 555 return "No such function" 556 f = self.w.functions[fid] 557 if f.type not in (SCENE, AUDIO, CHASER, JOIN): 558 return "Invalid function" 559 else: 560 f = None 561 562 c = self.current_cv.chaser 563 564 if c.type == CHASER: 565 if name is None and f is not None: 566 name = "Step \"%s\"" % f.name 567 s = ChaserStep(c, index=index, name=name, function=f) 568 self.current_cv.set_chaser(c, s.index) 569 else: 570 if f.id in (i.id for i in c.steps): 571 return "Already added" 572 c.add_step(f) 573 self.current_cv.set_chaser(c, len(c.steps)-1) 574 #self.chaser_edit(s.index+1) 575 576 def chaser_new_new(self, index, name, fname, type_): 577 with self.w_lock: 578 if name is None and f is not None: 579 name = "Step \"%s\"" % f.name 580 581 f = type_(self.w, name=fname) 582 583 return self.chaser_new(index, name, fid=f.id) 584 585 def chaser_rename(self, name): 586 with self.w_lock: 587 if self.current_cv.selected is None: 588 return "No step selected" 589 c = self.current_cv.chaser 590 s = c.steps[self.current_cv.selected] 591 s.name = name 592 593 self.current_cv.set_chaser(c, s.index) 594 595 def chaser_bind(self, fid): 596 with self.w_lock: 597 if self.current_cv.selected is None: 598 return "No step selected" 599 c = self.current_cv.chaser 600 s = c.steps[self.current_cv.selected] 601 602 if fid not in self.w.functions: 603 return "No such function" 604 f = self.w.functions[fid] 605 if f.type not in (SCENE, AUDIO, CHASER, JOIN): 606 return "Invalid function" 607 608 s.function = f 609 610 self.current_cv.set_chaser(c, s.index) 611 self.chaser_select(s.index+1) 612 613 def chaser_move(self, a, b = None): 614 with self.w_lock: 615 cursel = self.current_cv.selected 616 if cursel is not None: 617 cursel = self.current_cv.chaser.steps[cursel] 618 if b is not None: 619 sel = a - 1 620 if sel < 0 or sel >= len(self.current_cv.chaser.steps): 621 return "Invalid selection" 622 to = b - 1 623 else: 624 to = a - 1 625 if self.current_cv.selected is None: 626 return "No step selected" 627 sel = self.current_cv.selected 628 if to < 0 or to >= len(self.current_cv.chaser.steps): 629 return "Invalid destination" 630 c = self.current_cv.chaser 631 s = c.steps[sel] 632 s.index = to 633 634 self.current_cv.set_chaser(c, cursel.index if cursel is not None else None) 635 636 def primitive_rename(self, name): 637 with self.w_lock: 638 self.primitive.name = name 639 self.channel_bank.title = self.primitive.type + ' "%s"' % name 640 641 def chaser_rename_self(self, name): 642 with self.w_lock: 643 c = self.current_cv.chaser 644 c.name = name 645 646 self.current_cv.set_chaser(c, self.current_cv.selected) 647 648 def audio_fade(self, t, out=False): 649 with self.w_lock: 650 if out: 651 self.primitive.fade_out = t 652 else: 653 self.primitive.fade_in = t 654 self.channel_bank.audio = self.primitive 655 656 def audio_filename(self, fname): 657 with self.w_lock: 658 self.primitive.filename = fname 659 self.channel_bank.audio = self.primitive 660 661 def _render_callback(self, t, values): 662 if not self.rendering: 663 return 664 with self.w_lock, CURSES_LOCK: 665 syx = self.input.win.getyx() 666 667 self.channel_bank.set_values(values) 668 self.channel_bank.title = "LIVE: %7.2fs" % t 669 670 for d, cv in zip(self.renderer._data, (i for i in self.chaser_views if i.chaser.type == CHASER)): 671 cv.selected = d.steps[-1].index if d.steps else None 672 673 self.input.win.move(*syx) 674 675 def base_run(self): 676 if not self.chaser_views: 677 return "No chasers loaded" 678 679 self.channel_bank.set_scope(self._channels) 680 self.channel_bank.title = "LIVE: %7.2fs" % 0 681 self.channel_bank.highlight = True 682 683 self.rendering = True 684 self.renderer.set_functions(*((cv.chaser, cv.chaser.get_data(cv.selected)) for cv in self.chaser_views if cv.chaser.type == CHASER)) 685 self.renderer.start() 686 687 self.input.context = self.context_run 688 689 return "Started running" 690 691 def chaser_run(self): 692 self.current_cv.highlight = False 693 self.renderer.clear_hold() 694 self.chaser_stack.append([c.selected for c in self.chaser_views]) 695 self.handle_show(None) 696 self.base_run() 697 698 def run_exit(self): 699 self.rendering = False 700 self.renderer.stop() 701 702 with self.w_lock: 703 self.channel_bank.title = "Channels" 704 self.channel_bank.highlight = False 705 self.channel_bank.set_scope(()) 706 707 if self.chaser_stack: 708 for i, c in zip(self.chaser_stack.pop(-1), self.chaser_views): 709 c.selected = i 710 self.current_cv.highlight = True 711 if self.current_cv.selected is not None: 712 s = self.current_cv.chaser.steps[self.current_cv.selected] 713 if s.function is not None and s.function.type in (SCENE, AUDIO,): 714 self.handle_show(s.function) 715 self.input.context = self.context_chaser 716 else: 717 self.input.context = self.context_base 718 719 for cv in self.chaser_views: 720 cv.selected = None 721 722 def run_jump(self, n, p, index): 723 if not index: 724 n = [j for j, i in enumerate(self.chaser_views) if i.chaser.id == n] 725 if not n: 726 return "No such chaser loaded" 727 n = n[0] 728 if n >= len(self.chaser_views) or n < 0: 729 return "Chaser index out of range" 730 if p is not None and (p < 1 or p > len(self.chaser_views[n].chaser.steps)): 731 return "Step index out of range" 732 self.renderer.advance((n, (p-1) if p is not None else p)) 733 734 def run_advance_all(self): 735 for n in range(len(self.chaser_views)): 736 self.renderer.advance((n, None)) 737 738 def current_status(self): 739 self.pager.display_many(( 740 "CURRENT STATUS", 741 "Output is %sOK: %s" % ("" if self.output.ok else "NOT ", self.output.status), 742 ), True) 743 744 def chaser_info(self, n): 745 with self.w_lock: 746 if n is None: 747 n = self.current_cv.selected 748 if n is None: 749 return "No step selected" 750 else: 751 n -= 1 752 if n < 0 or n >= len(self.current_cv.chaser.steps): 753 return "Invalid step" 754 f = self.current_cv.chaser.steps[n] 755 self.pager.display_many(( 756 f.name, 757 "- Fade in: %7.3f" % (f.fade_in/1000), 758 "- Duration: " + (("%7.3f" % (f.duration/1000)) if f.duration != INFTY else "infty"), 759 "- Fade out: %7.3f" % (f.fade_out/1000), 760 )) 761 762 def chaser_mode(self, mode): 763 with self.w_lock: 764 self.current_cv.chaser.advance_mode = mode 765 self.current_cv.set_chaser(self.current_cv.chaser, self.current_cv.selected) 766 767 def base_copy(self, name, num): 768 with self.w_lock: 769 ## TODO: Implement this in BLC 770 if num not in self.w.functions: 771 return "No such function" 772 f = self.w.functions[num] 773 if f.type != SCENE: 774 return "Can only close scenes" 775 f2 = Scene(self.w, name=name, values=f.values) 776 self.handle_enter(f2.id) 777 778 def __init__(self, path, output): 779 ## Have to do most of the actual initialization in the main method, as curses isn't 780 ## ready yet. 781 self.channel_bank = None 782 self.input = None 783 self.pager = None 784 self.stdscr = None 785 786 self.primitive = None 787 self.chaser = None 788 self.current_cv = None 789 790 self.path = path 791 self._w_created = False 792 if path is None or not os.path.isfile(path): 793 self.w = Workspace("", "", 0, dt.datetime.now()) 794 self._w_created = True 795 else: 796 self.w = Workspace.load(path) 797 798 self.w_lock = threading.RLock() 799 800 self.chaser_views = [] 801 802 self.chaser_stack = [] 803 804 self.context_base = Input.parse_context(( 805 ("edit $num", self.handle_enter), 806 ("edit", lambda: self.handle_enter(None)), 807 ("delete $num", self.base_delete), 808 ("new scene $quoted_string", lambda n: self.base_new(n, Scene)), 809 ("new chaser $quoted_string", lambda n: self.base_new(n, Chaser)), 810 ("new audio $quoted_string", lambda n: self.base_new(n, Audio)), 811 ("new join $quoted_string", lambda n: self.base_new(n, Join)), 812 813 ("add $num", self.base_add), 814 ("subtract $letter", lambda n: self.base_remove(n, True)), 815 ("subtract $num", lambda n: self.base_remove(n, False)), 816 817 ("run", self.base_run), 818 819 ("write", self.base_save), 820 ("write $quoted_string", self.base_save), 821 822 ("list fixtures", self.list_fixtures), 823 ("list scenes", lambda: self.list_functions(SCENE)), 824 ("list chasers", lambda: self.list_functions(CHASER)), 825 ("list audio", lambda: self.list_functions(AUDIO)), 826 ("list joins", lambda: self.list_functions(JOIN)), 827 828 ("new scene $quoted_string from $num", self.base_copy), 829 830 ("currentstatus", self.current_status), 831 832 ("pager page", self.page), 833 ("pager clear", self.page_clear), 834 ("verify audio", self.verify_audio), 835 ("quit", self.base_quit), 836 ), { 837 None: "This is the base edit mode for editing functions.", 838 "edit": "Edit the specified function.", 839 "delete": "Delete the specified function.", 840 "new": "Create a new function of the given type.", 841 "write": "Save the workspace. The path is implicitly the one loaded from if not given.", 842 "list": "List available fixtures or functions.", 843 "pager": "Control the pager. In page mode, arrow keys, page up/down, home/end, and j/k can be used to scroll, q exits.", 844 "quit": "Exit BLC.", 845 "remove": "Remove the specified chaser from the display. If a letter is used, remove the chaser at the given position, where 'a' is the left-most chaser and so forth. If a number is used, treat it as a chaser ID.", 846 "add": "Add a chaser to the display. Currently a limit of 2 visible.", 847 "currentstatus" : "Display information about the system's current status." 848 }, self.help) 849 850 self.context_chaser = Input.parse_context(( 851 ("choose $num", self.chaser_select), 852 853 ("edit", self.chaser_edit), 854 ("edit $num", self.chaser_edit), 855 856 ("delete", self.chaser_delete), 857 ("delete $num", self.chaser_delete), 858 859 ("append $quoted_string", lambda n: self.chaser_new(-1, n)), 860 ("append $quoted_string from $num", lambda n, s: self.chaser_new(-1, n, s)), 861 ("append $quoted_string from new scene $quoted_string", lambda n, s: self.chaser_new_new(-1, n, s, Scene)), 862 ("append $quoted_string from new audio $quoted_string", lambda n, s: self.chaser_new_new(-1, n, s, Audio)), 863 864 ("append", lambda: self.chaser_new(-1, "")), 865 ("append from $num", lambda s: self.chaser_new(-1, "", s)), 866 ("append from new scene $quoted_string", lambda s: self.chaser_new_new(-1, "", s, Scene)), 867 ("append from new audio $quoted_string", lambda s: self.chaser_new_new(-1, "", s, Audio)), 868 869 ("new $num $quoted_string", self.chaser_new), 870 ("new $num $quoted_string from $num", self.chaser_new), 871 ("new $num $quoted_string from new scene $quoted_string", lambda i, n, s: self.chaser_new_new(i, n, s, Scene)), 872 ("new $num $quoted_string from new audio $quoted_string", lambda i, n, s: self.chaser_new_new(i, n, s, Audio)), 873 874 ("rename $quoted_string", self.chaser_rename), 875 ("rename chaser $quoted_string", self.chaser_rename_self), 876 ("move $num to $num", self.chaser_move), 877 ("move $num", self.chaser_move), 878 ("set fade in $time", self.chaser_fade), 879 ("set fade out $time", lambda t: self.chaser_fade(t, True)), 880 ("set mode oneshot", lambda: self.chaser_mode(ONESHOT)), 881 ("set mode loop", lambda: self.chaser_mode(LOOP)), 882 ("set mode random", lambda: self.chaser_mode(RANDOM)), 883 ("set length $time", self.chaser_duration), 884 ("unbind", self.chaser_unset), 885 ("bind $num", self.chaser_bind), 886 887 ("list fixtures", self.list_fixtures), 888 ("list scenes", lambda: self.list_functions(SCENE)), 889 ("list chasers", lambda: self.list_functions(CHASER)), 890 ("list audio", lambda: self.list_functions(AUDIO)), 891 ("list joins", lambda: self.list_functions(JOIN)), 892 893 ("info", lambda: self.chaser_info(None)), 894 ("info $num", self.chaser_info), 895 896 ("trailer", self.chaser_run), 897 898 ("pager page", self.page), 899 ("pager clear", self.page_clear), 900 ("verify audio", self.verify_audio), 901 ("quit", self.handle_exit), 902 ), { 903 None: "This mode is for editing chasers. All functions (excepting select) which take a number as an argument may be called without to act on the currently selected step.", 904 "edit": "Edit the given step.", 905 "select": "Select a step.", 906 "fade": "Change the fade durations for the selected step.", 907 "length": "Set the duration for the selected step.", 908 "new": "Create a new step at the given position with the given name. Can also create a new function to use for the scene. Immediately enters edit mode for that step.", 909 "delete": "Remove the given step.", 910 "rename": "Rename the current step.", 911 "append": "Create a new step at the end of the chaser. See 'new' for details.", 912 "unset": "Unset the duration of the selected step, inheriting the step's duration from its function.", 913 "pager": "Control the pager. In page mode, arrow keys, page up/down, home/end, and j/k can be used to scroll, q exits.", 914 "move": "Move the given step to the given position", 915 "bind": "Bind the step to the given function.", 916 "quit": "Return to the previous mode.", 917 "trailer": "Preview the chaser in run mode.", 918 }, self.help) 919 920 self.context_scene = Input.parse_context(( 921 ("set $channel_range to $value", self.scene_set), 922 ("clear $channel_range", self.scene_clear), 923 924 ("edit $channel_range", self.scene_edit), 925 ("edit $channel_range force", lambda cr: self.scene_edit(cr, True)), 926 927 ("list fixtures", self.list_fixtures), 928 ("list scenes", lambda: self.list_functions(SCENE)), 929 ("list chasers", lambda: self.list_functions(CHASER)), 930 ("list audio", lambda: self.list_functions(AUDIO)), 931 ("list joins", lambda: self.list_functions(JOIN)), 932 933 ("rename $quoted_string", self.primitive_rename), 934 935 ("pager page", self.page), 936 ("pager clear", self.page_clear), 937 ("quit", self.scene_exit), 938 ), { 939 None: "This mode is for editing scene primitives for fixed lighting.", 940 "set": "Set the given channel range to the given value (0 <= value <= 255).", 941 "reset": "Remove the given channel range from the scene", 942 "list": "List available fixtures or functions.", 943 "pager": "Control the pager. In page mode, arrow keys, page up/down, home/end, and j/k can be used to scroll, q exits.", 944 "edit": "Live edit scenes using the arrow keys, page up/down, home/end, and h/j/k/l. If \"force\" is used, all matching channels are edited, instead of just the matching ones already in the scope.", 945 "quit": "Return to the previous mode.", 946 }, self.help) 947 948 self.context_audio = Input.parse_context(( 949 ("list fixtures", self.list_fixtures), 950 ("list scenes", lambda: self.list_functions(SCENE)), 951 ("list chasers", lambda: self.list_functions(CHASER)), 952 953 ("rename $quoted_string", self.primitive_rename), 954 955 ("filename $quoted_string", self.audio_filename), 956 ("set fade in $time", self.audio_fade), 957 ("set fade out $time", lambda t: self.audio_fade(t, True)), 958 959 ("pager page", self.page), 960 ("pager clear", self.page_clear), 961 ("verify audio", self.verify_audio), 962 ("quit", self.audio_exit), 963 ), { 964 None: "This mode is for editing audio primitives for single audio files.", 965 "list": "List available fixtures or functions.", 966 "rename": "Rename the function.", 967 "filename": "Set the filename for the function.", 968 "fade": "Set fade times for the function. Note that unlike lighting, fade out is done during the audio's run.", 969 "pager": "Control the pager. In page mode, arrow keys, page up/down, home/end, and j/k can be used to scroll, q exits.", 970 "quit": "Return to the previous mode.", 971 }, self.help) 972 973 self.context_run = Input.parse_context(( 974 ("quit", self.run_exit), 975 976 ("jump $letter to $num", lambda n, p: self.run_jump(n, p, True)), 977 ("jump $num to $num", lambda n, p: self.run_jump(n, p, False)), 978 979 ("advance $letter", lambda n: self.run_jump(n, None, True)), 980 ("advance $num", lambda n: self.run_jump(n, None, False)), 981 ("advance", lambda: self.run_jump(0, None, True)), 982 ("badvance", lambda: self.run_jump(1, None, True)), 983 ("", lambda: self.run_jump(0, None, True)), 984 985 ("everythingadvance", self.run_advance_all), 986 987 ("currentstatus", self.current_status), 988 ), { 989 None: "This mode is for running a show or previewing a chaser.", 990 "quit": "Return to the previous mode.", 991 "jump": "Jump the given chaser to the given location.", 992 "advance": "Advance a chaser a single step. If no letter is given, this is the first from the left.", 993 "badvance": "Advance the second chaser from the left a single step.", 994 "everythingadvance": "Advance all chasers a single step each.", 995 "currentstatus" : "Display information about the system's current status." 996 }, self.help) 997 998 self.context_join = Input.parse_context(( 999 ("choose $num", self.chaser_select), 1000 1001 ("edit", self.chaser_edit), 1002 ("edit $num", self.chaser_edit), 1003 1004 ("delete", self.chaser_delete), 1005 ("delete $num", self.chaser_delete), 1006 1007 ("add $num", lambda s: self.chaser_new(-1, "", s)), 1008 1009 ("rename $quoted_string", self.chaser_rename_self), 1010 1011 ("list fixtures", self.list_fixtures), 1012 ("list scenes", lambda: self.list_functions(SCENE)), 1013 ("list chasers", lambda: self.list_functions(CHASER)), 1014 ("list audio", lambda: self.list_functions(AUDIO)), 1015 ("list joins", lambda: self.list_functions(JOIN)), 1016 1017 ("pager page", self.page), 1018 ("pager clear", self.page_clear), 1019 ("verify audio", self.verify_audio), 1020 ("quit", self.handle_exit), 1021 )) 1022 1023 self.output = output 1024 self.renderer = Renderer(self.w, self.w_lock, self.output, self._render_callback) 1025 self.rendering = False 1026 1027 self._channels = sum((tuple(f.channels) for f in sorted(self.w.fixtures.values(), key=lambda i: i.id)), ())