summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Connors <benconnors@outlook.com>2019-10-26 10:59:14 -0400
committerBen Connors <benconnors@outlook.com>2019-10-26 10:59:14 -0400
commit70b62ea7a6be3b3af801f58bc94924074fd5aec2 (patch)
treece56092b374d6783112223c8aa8fc76350dc10a1
parent2cb483c4812eee903295f76f30aaac0b429245d1 (diff)
More interface features
- Can now edit chasers a bit o Edit component steps o Change step durations
-rwxr-xr-xinterface/channelbank.py15
-rwxr-xr-xinterface/chaserview.py33
-rwxr-xr-xinterface/input/parsers.py39
-rw-r--r--interface/interface.py258
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)