diff options
-rwxr-xr-x | interface/channelbank.py | 15 | ||||
-rwxr-xr-x | interface/chaserview.py | 33 | ||||
-rwxr-xr-x | interface/input/parsers.py | 39 | ||||
-rw-r--r-- | interface/interface.py | 258 |
4 files changed, 304 insertions, 41 deletions
diff --git a/interface/channelbank.py b/interface/channelbank.py index e3dec7b..2ca964e 100755 --- a/interface/channelbank.py +++ b/interface/channelbank.py @@ -172,7 +172,19 @@ class ChannelBank: def _put_title(self): self.win.border() pos = min(self._width-2-len(self._title), (3*self._width)//4 - (len(self._title) // 2)) - self.win.addstr(self._height-1, pos, self._title) + self.win.addstr(self._height-1, pos, self._title, curses.A_REVERSE if self._highlight else 0) + + @property + def highlight(self): + return self._highlight + + @highlight.setter + def highlight(self, value): + if value != self._highlight: + self._highlight = value + with CURSES_LOCK: + self._put_title() + self.win.refresh() @title.setter def title(self, v): @@ -187,6 +199,7 @@ class ChannelBank: with CURSES_LOCK: self.win = curses.newwin(height, width, y, x) self.win.keypad(True) + self._highlight = False self.scope = frozenset(scope) self._sscope = [] self._title = "Channels" diff --git a/interface/chaserview.py b/interface/chaserview.py index 2c88343..e5b0c2b 100755 --- a/interface/chaserview.py +++ b/interface/chaserview.py @@ -4,8 +4,7 @@ import curses import math import threading -import blc2 -from blc2.constants import INFTY +from blc2.constants import INFTY, MANUAL CURSES_LOCK = threading.RLock() @@ -57,6 +56,17 @@ class ChaserView: self._x = x self.win.noutrefresh() + @property + def highlight(self): + return self._highlight + + @highlight.setter + def highlight(self, value): + with self._lock: + if self._highlight != value: + self._highlight = value + self._redraw() + def _redraw(self): self.win.erase() self.win.border() @@ -70,7 +80,7 @@ class ChaserView: c = self._chaser w = self._width - 2 - self.win.addstr(1, 1, self.fit(("%d: "% c.id) + c.name, w-2)) + self.win.addstr(1, 1, self.fit(("%d: "% c.id) + c.name, w-2, True), curses.A_REVERSE if self._highlight else 0) maxsteps = self._height - 4 if maxsteps < len(c.steps): @@ -91,8 +101,15 @@ class ChaserView: attrs = curses.A_REVERSE else: attrs = 0 - t = "%s:%s:%s" % (format_time(s.fade_in), format_time(s.duration), format_time(s.fade_out)) - self.win.addstr(n+2, 1, self.fit((self._numformat % (s.index+1)) + ": " + s.name, w-8, pad=True)+t, attrs) + if s.function is not None: + ft = s.function.type[0].upper() + fid = str(s.function.id) + else: + ft = "-" + fid = "---" + + t = "%s%3s%s|%s:%s:%s" % (ft, fid, '*' if s.duration_mode == MANUAL else ' ', format_time(s.fade_in), format_time(s.duration), format_time(s.fade_out)) + self.win.addstr(n+2, 1, self.fit((self._numformat % (s.index+1)) + ": " + s.name, w-len(t), pad=True)+t, attrs) if first > 0: self.win.addch(3, self._width//2, '⯅') @@ -122,6 +139,11 @@ class ChaserView: self._numformat = "%%%dd" % math.ceil(math.log10(len(chaser.steps))) self._redraw() + @property + def chaser(self): + with self._lock: + return self._chaser + def __init__(self, y, x, height, width): with CURSES_LOCK: self.win = curses.newwin(height, width, y, x) @@ -133,6 +155,7 @@ class ChaserView: self._x = -1 self._chaser = None + self._highlight = False self._numformat = "" self._selected = -1 diff --git a/interface/input/parsers.py b/interface/input/parsers.py index 5e53e03..f97dbf1 100755 --- a/interface/input/parsers.py +++ b/interface/input/parsers.py @@ -73,7 +73,7 @@ def parse_channelrange(s): def parse_value(s): if not s: - return None, s + return None, s, None buff = "" while s: @@ -87,7 +87,7 @@ def parse_value(s): def parse_num(s): if not s: - return None, s + return None, s, None buff = "" while s: @@ -99,6 +99,39 @@ def parse_num(s): return (None if not buff else int(buff)), s, buff +_POSTFIXES = "mcisahkegtp" + +def parse_time(s): + if not s: + return None, s, None + + v, s, d = parse_num(s) + if v is None: + return None, s, None + + if not s: + return v, s, d + + try: + idx = _POSTFIXES.index(s[0].lower()) + except ValueError: + return v, s, d + + v *= 10**idx + return v, s[1:], d+s[0].lower() + +_ALL_LETTERS = "abcdefghijklmnopqrstuvwxyz" +def parse_any_letter(s): + if not s: + return None, s + + l = s[0].lower() + + if l not in _ALL_LETTERS: + return None, s, None + + return _ALL_LETTERS.index(l), s[1:], l + def make_parse_letter(letter, display): def inner(s): if not s or s[0] != letter: @@ -162,4 +195,6 @@ PARSE_MAP = { "$string": parse_string, "$num": parse_num, "$quoted_string": parse_quotedstring, + "$letter": parse_any_letter, + "$time": parse_time, } diff --git a/interface/interface.py b/interface/interface.py index 65cd1d2..57624d8 100644 --- a/interface/interface.py +++ b/interface/interface.py @@ -9,7 +9,7 @@ from blc2.functions.audio import Audio from blc2.functions.scene import Scene from blc2.functions.chaser import Chaser from blc2.functions.chaserstep import ChaserStep -from blc2.constants import SCENE +from blc2.constants import SCENE, CHASER, AUDIO, MANUAL, INHERIT from blc2.topology import Fixture @@ -29,37 +29,48 @@ def wrap_curses(f): __version__ = "v0.0.1" class Interface: - @staticmethod - def _compute_sizes(height, width): + def _compute_sizes(self, height, width): hb = height // 2 ht = height - hb wr = width // 2 wl = width - wr - 1 + chasers = [] + if self.chaser_views: + current = 0 + cw = (wl // len(self.chaser_views)) - 1 + for i in range(len(self.chaser_views)): + curw = (wl - current) if (i+1) == len(self.chaser_views) else cw + chasers.append(((hb, curw), (ht, current))) + current += curw + 1 + return ( - (ht, width), (0, 0), - (4, wr), (height-4, wl+1), - (hb - 4, wr), (ht, wl+1), - (hb, wl//2 - 1), (ht, 0), - (hb, wl - (wl//2)), (ht, wl//2) + ((ht, width), (0, 0),), + ((4, wr), (height-4, wl+1),), + ((hb - 4, wr), (ht, wl+1),), + *chasers ) def _resize(self): - for a, f 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, self.chaser_views[0].set_dim, self.chaser_views[0].set_pos, self.chaser_views[1].set_dim, self.chaser_views[1].set_pos)): - f(*a) + ## FIXME + self._actual_resize() + self._actual_resize() + + def _actual_resize(self): + 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))): + f1(*a1) + f2(*a2) @wrap_curses def main(self, stdscr): height, width = stdscr.getmaxyx() self.stdscr = stdscr - cbd, cbp, ind, inp, pgd, pgp, cv0d, cv0p, cv1d, cv1p = self._compute_sizes(height, width) + (cbd, cbp), (ind, inp), (pgd, pgp) = self._compute_sizes(height, width) self.channel_bank = ChannelBank(*cbp, *cbd) self.input = Input(*inp, *ind) self.input.context = self.context_base self.pager = Pager(*pgp, *pgd) - self.chaser_views[0] = ChaserView(*cv0p, *cv0d) - self.chaser_views[1] = ChaserView(*cv1p, *cv1d) todisp = [ "=== Welcome to BLC2!", @@ -76,20 +87,78 @@ class Interface: self.pager.display_many(todisp) self.input.main(self._resize) - def base_edit(self, fid): + def handle_enter(self, fid): with self.w_lock: if fid not in self.w.functions: return "No such function" f = self.w.functions[fid] - if f.type != SCENE: - ## FIXME - return "Can only edit scenes so far" - with CURSES_LOCK: + + if self.current_cv is not None: + self.current_cv.highlight = False + + if self.chaser is not None: + cv = [c for c in self.chaser_views if c.chaser == self.chaser][0].selected + self.chaser_stack.append((self.chaser, cv)) + self.chaser = None + + if f.type == SCENE: self.primitive = f self.channel_bank.set_scope(f.scope) self.channel_bank.set_values(f.values.items()) self.channel_bank.title = f.type + ' "%s"' % f.name + self.channel_bank.highlight = True self.input.context = self.context_scene + elif f.type == CHASER: + self.chaser = f + cv = [c for c in self.chaser_views if c.chaser == f] + if not cv: + if len(self.chaser_views) == 2: + self.chaser_views[1].set_chaser(f, None) + else: + self.base_add(f.id) + self.current_cv = self.chaser_views[-1] + else: + cv = cv[0] + self.current_cv = cv + self.current_cv.highlight = True + self.input.context = self.context_chaser + else: + ## FIXME + return "No other types allowed yet" + + def base_add(self, fid): + with self.w_lock: + if fid not in self.w.functions: + return "No such function" + f = self.w.functions[fid] + if f.type != CHASER: + return "Can only add chasers!" + with CURSES_LOCK: + if True in (c.chaser.id for c in self.chaser_views): + return "Already added!" + elif len(self.chaser_views) == 2: + ## FIXME? + return "Can only use two for now" + self.chaser_views.append(None) + ld, lp = self._compute_sizes(*self.stdscr.getmaxyx())[-1] + self.chaser_views[-1] = ChaserView(*lp, *ld) + self._resize() + self.chaser_views[-1].set_chaser(f, None) + + def base_remove(self, n, index=True): + with self.w_lock: + if not index: + n = [j for j, i in enumerate(self.chaser_views) if i.chaser.id == n] + if not n: + return "No such chaser loaded" + n = n[0] + if n >= len(self.chaser_views) or n < 0: + return "Index out of range" + with CURSES_LOCK: + self.chaser_views[n].win.erase() + self.chaser_views[n].win.refresh() + self.chaser_views.pop(n) + self._resize() def base_delete(self, fid): with self.w_lock: @@ -101,7 +170,7 @@ class Interface: def base_new_scene(self, name): with self.w_lock: f = Scene(self.w, name=name) - self.base_edit(f.id) + self.handle_enter(f.id) def _gather_channels(self, cr): with self.w_lock: @@ -138,11 +207,51 @@ class Interface: self.scene_set(cr, None) def scene_exit(self): - self.input.context = self.context_base self.primitive = None self.channel_bank.set_scope(()) self.channel_bank.title = "Channels" + self.handle_exit() + + def handle_exit(self): + for c in self.chaser_views: + c.highlight = False + + if self.current_cv is not None: + self.current_cv.selected = None + + self.channel_bank.highlight = False + + if self.chaser_stack: + ## Load the previous chaser + c, sel = self.chaser_stack.pop(-1) + + if True not in (True for i in self.chaser_views if i.chaser.id == c.id): + ## We need to load it + ## Check if we have one for the current chaser + try: + idx = [i.chaser.id for i in self.chaser_views].index(self.chaser.id) + except (ValueError, AttributeError): + ## We don't have it + if len(self.chaser_views) == 2: + ## We don't have room for it, just replace the last one + self.chaser_views[1].set_chaser(c, sel) + else: + self.base_add(c.id) + self.current_cv = self.chaser_views[-1] + else: + self.chaser_views[idx].set_chaser(c, sel) + self.current_cv = self.chaser_views[idx] + + self.current_cv.highlight = True + self.current_cv.selected = sel + self.input.context = self.context_chaser + self.chaser = c + else: + self.input.context = self.context_base + self.chaser = None + self.current_cv = None + def base_save(self, path=None): with self.w_lock: if path is not None: @@ -199,6 +308,58 @@ class Interface: self.pager.display_many(todisp, split=True) + def chaser_select(self, num): + with self.w_lock: + if num < 1 or num > len(self.current_cv.chaser.steps): + return "Out of range" + self.current_cv.selected = num - 1 + + def chaser_edit(self, num=None): + with self.w_lock: + if num is not None: + r = self.chaser_select(num) + if r is not None: + return r + elif self.current_cv.selected is None: + return "No step selected" + + c = self.current_cv.chaser.steps[self.current_cv.selected] + if c.function is None: + return "No function on step" + + self.handle_enter(c.function.id) + + def chaser_fade(self, t, out=False): + with self.w_lock: + if self.current_cv.selected is None: + return "No step selected" + c = self.current_cv.chaser + s = c.steps[self.current_cv.selected] + if out: + s.fade_out = t + else: + s.fade_in = t + self.current_cv.set_chaser(c, s.index) + + def chaser_duration(self, t): + with self.w_lock: + if self.current_cv.selected is None: + return "No step selected" + c = self.current_cv.chaser + s = c.steps[self.current_cv.selected] + if s.duration_mode != MANUAL: + s.duration_mode = MANUAL + s.duration = t + self.current_cv.set_chaser(c, s.index) + + def chaser_unset(self): + with self.w_lock: + if self.current_cv.selected is None: + return "No step selected" + c = self.current_cv.chaser + s = c.steps[self.current_cv.selected] + s.duration_mode = INHERIT + self.current_cv.set_chaser(c, s.index) def __init__(self, path): ## Have to do most of the actual initialization in the main method, as curses isn't @@ -209,7 +370,8 @@ class Interface: self.stdscr = None self.primitive = None - self.chasers = [] + self.chaser = None + self.current_cv = None self.path = path self._w_created = False @@ -221,21 +383,28 @@ class Interface: self.w_lock = threading.RLock() - self.chaser_views = [None, None] + self.chaser_views = [] + + self.chaser_stack = [] self.context_base = Input.parse_context(( - ("edit $num", self.base_edit), + ("edit $num", self.handle_enter), ("delete $num", self.base_delete), ("new scene $quoted_string", self.base_new_scene), + ("add $num", self.base_add), + ("remove $letter", lambda n: self.base_remove(n, True)), + ("remove $num", lambda n: self.base_remove(n, False)), + ("save", self.base_save), ("save $quoted_string", self.base_save), ("list fixtures", self.list_fixtures), ("list scenes", lambda: self.list_functions(SCENE)), + ("list chasers", lambda: self.list_functions(CHASER)), - ("page", self.page), - ("clrpage", self.page_clear), + ("pager page", self.page), + ("pager clear", self.page_clear), ("quit", self.base_quit), ), { None: "This is the base edit mode for editing functions.", @@ -244,9 +413,32 @@ class Interface: "new": "Create a new function of the given type.", "save": "Save the workspace. The path is implicitly the one loaded from if not given.", "list": "List available fixtures or functions.", - "page": "Page through the output window. Arrow keys, page up/down, home/end, and j/k can be used to scroll, q exits.", - "clrpage": "Clear the output window.", - "quit": "Exit BLC." + "pager": "Control the pager. In page mode, arrow keys, page up/down, home/end, and j/k can be used to scroll, q exits.", + "quit": "Exit BLC.", + "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.", + "add": "Add a chaser to the display. Currently a limit of 2 visible.", + }, self.help) + + self.context_chaser = Input.parse_context(( + ("select $num", self.chaser_select), + + ("edit", self.chaser_edit), + ("edit $num", self.chaser_edit), + + ("fade in $time", self.chaser_fade), + ("fade out $time", lambda t: self.chaser_fade(t, True)), + ("duration $time", self.chaser_duration), + ("unset", self.chaser_unset), + + ("quit", self.handle_exit), + ), { + 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.", + "edit": "Edit the given step.", + "select": "Select a step.", + "fade": "Change the fade durations for the selected step.", + "duration": "Set the duration for the selected step.", + "unset": "Unset the duration of the selected step, inheriting the step's duration from its function.", + "quit": "Return to the previous mode.", }, self.help) self.context_scene = Input.parse_context(( @@ -255,16 +447,16 @@ class Interface: ("list fixtures", self.list_fixtures), ("list scenes", lambda: self.list_functions(SCENE)), + ("list chasers", lambda: self.list_functions(CHASER)), - ("page", self.page), - ("clrpage", self.page_clear), + ("pager page", self.page), + ("pager clear", self.page_clear), ("quit", self.scene_exit), ), { None: "This mode is for editing scene primitives for fixed lighting.", "set": "Set the given channel range to the given value (0 <= value <= 255).", "reset": "Remove the given channel range from the scene", "list": "List available fixtures or functions.", - "page": "Page through the output window. Arrow keys, page up/down, home/end, and j/k can be used to scroll, q exits.", - "clrpage": "Clear the output window.", - "quit": "Return to the previous mode." + "pager": "Control the pager. In page mode, arrow keys, page up/down, home/end, and j/k can be used to scroll, q exits.", + "quit": "Return to the previous mode.", }, self.help) |