from __future__ import annotations from adafruit_display_text import label from adafruit_display_shapes import rect import displayio import json import os from .util import FONT_10, led_map, hsv_to_rgb from .core import Mode, Key BLUE = hsv_to_rgb(0.48, 1.0, 1.0) BLUE_DIM = hsv_to_rgb(0.48, 1.0, 0.15) CHOICE_NEUTRAL = hsv_to_rgb(0.0, 0.0, 0.0) RED = hsv_to_rgb(0.95, 1.0, 1.0) RED_DIM = hsv_to_rgb(0.95, 1.0, 0.15) GREEN = hsv_to_rgb(0.27, 1.0, 1.0) GREEN_DIM = hsv_to_rgb(0.27, 1.0, 0.15) def _thresh(val, t): return val >= t def _thresh_offor(val, t): if t == 0: return val == t return val >= t def _eq(val, t): return val == t def _color(val, active: bool) -> tuple[float, float, float]: return BLUE if active else BLUE_DIM def _color_offor(val, active: bool) -> tuple[float, float, float]: if val: return BLUE if active else BLUE_DIM else: return RED if active else RED_DIM def _fmt_offorpct(val): if val == 0: return "off" return "{}%".format(val) class Setting: name: str default: Any _value: Any def __init__(self, name: str, default: Any): self.name = name self.default = default self._value = default def restore_default(self): self._value = self.default @property def value(self): return self._value @value.setter def value(self, val): self._value = val class SliderSetting(Setting): name: str max: int fmt: str | None _value: int offset: int width: int def __init__( self, name: str, max: int, default: int = 0, fmt=None, thresh=_thresh, color=_color, ): super().__init__(name, default) self.max = max self.fmt = fmt self.width = min(23, max) self.offset = 12 if self.width <= 12 else 0 self.thresh = thresh self.color = color def press_prev(self): self._value = (self._value - 1) % (self.max + 1) def press_next(self): self._value = (self._value + 1) % (self.max + 1) def press_key(self, i): i = i - self.offset if i < 0: return if i > self.width: return self._value = int(self.max * i / self.width) def get_colors(self) -> list[int]: colors = [0] * self.offset for i in range(self.width + 1): thresh = int(self.max * i / self.width) active = self.thresh(self.value, thresh) colors.append(self.color(thresh, active)) return colors def format(self) -> str: val = self.value if isinstance(self.fmt, str): return str.format(self.fmt, val) elif self.fmt: return self.fmt(val) if val == True: return "on" elif val == False: return "off" return str(val) class Slider16Setting(SliderSetting): def __init__( self, name: str, default: int = 0, fmt=None, thresh=_thresh, color=_color, ): super().__init__(name, 16, default, fmt, thresh, color) def press_key(self, i): if i >= 4 and i < 12: i = i - 4 elif i >= 16 and i < 24: i = i - 8 else: return self._value = i def get_colors(self) -> list[int]: colors = [0, 0, 0, 0] for i in range(8): thresh = int(self.max * i / self.width) active = self.thresh(self.value, thresh) colors.append(self.color(thresh, active)) colors.extend([0, 0, 0, 0]) for i in range(8, 16): thresh = int(self.max * i / self.width) active = self.thresh(self.value, thresh) colors.append(self.color(thresh, active)) return colors class ChoiceSetting(SliderSetting): def __init__(self, name: str, values: list, default=0, **kwargs): default = values.index(default) super().__init__( name, len(values) - 1, default=default, thresh=lambda v, t: v == t, **kwargs ) self.values = values @property def value(self): return self.values[self._value] @value.setter def value(self, val): self._value = self.values.index(val) def get_colors(self) -> list[int]: colors = [0] * self.offset value = self.value for i in range(self.width + 1): thresh = self.values[int(self.max * i / self.width)] active = self.thresh(value, thresh) colors.append(self.color(thresh, active)) return colors class Settings: settings: dict[str, Setting] profile_setting: Setting readonly: bool order: list[str] change_handlers: dict[str, Callable[Any, Any]] def __init__(self, settings: dict[str, Setting], readonly=False): self.settings = settings self.readonly = readonly self.change_handlers = {} self.profile_setting = SliderSetting( "CURRENT PROFILE", 24, fmt="Profile {}", thresh=_eq ) self.order = [ "layout_name", "layout_offset", "scale_name", "scale_root", "flip", ] for id in self.settings: if id not in self.order: self.order.append(id) def on(self, id: str, fn): if id in self.change_handlers: raise ValueError("already have a handler for {}".format(id)) self.change_handlers[id] = fn def dispatch(self, id: str, last_val=None): self.change_handlers[id](self.settings[id].value, last_val) self.change_handlers["*"](id, self.settings[id].value, last_val) def get(self, id: str) -> Setting: return self.settings[id] def load(self, i=0): try: with open(f"profiles/hex33board_{i}.json", "r") as f: print(f"loading profile{i}") data = json.load(f) for id in self.order: if id in data: self.settings[id].value = data[id] else: self.settings[id].restore_default() self.dispatch(id) except OSError: print(f"couldn't load profile{i}, falling back to defaults") for id in self.order: self.settings[id].restore_default() self.dispatch(id) self.profile_setting.value = i def loadstring(self, input: str, restore=True): data = json.loads(input) for id in self.order: if id in data: self.settings[id].value = data[id] elif restore: self.settings[id].restore_default() self.dispatch(id) self.profile_setting.value = None def store(self, i=None): if i == None: i = self.profile_setting.value if i is None: return if self.readonly: print(f"readonly, skipped storing profile{i}") return data = {} for id in self.settings: data[id] = self.settings[id].value try: try: os.mkdir("profiles") except OSError: pass with open(f"profiles/hex33board_{i}.json", "w") as f: json.dump(data, f) except OSError as e: print("error while storing:", e) def storestring(self, i=None, only=None) -> str: data = {} for id in self.settings: if only and not any(id.startswith(p) for p in only): continue data[id] = self.settings[id].value return json.dumps(data) def __iter__(self): for id in self.settings: yield id, self.settings[id].value class MenuMode(Mode): color = (1.0, 1.0, 0.5) settings: Settings ui_setting_ids: list[list[str]] ui_settings: list[list[Setting]] active: tuple[int, int] GROUP_ACTIVE = hsv_to_rgb(0.7, 1.0, 1.0) GROUP_ACTIVE_DIM = hsv_to_rgb(0.7, 1.0, 0.15) SETTING_ACTIVE = hsv_to_rgb(0.12, 1.0, 1.0) SETTING_ACTIVE_DIM = hsv_to_rgb(0.12, 1.0, 0.15) def __init__(self, *args, settings: Settings, groups: list[list[str]]): super().__init__(*args) groups.insert(0, []) self.settings = settings self.ui_setting_ids = groups self.ui_settings = [[settings.get(id) for id in group] for group in groups] self.active = (0, 0) self.group = displayio.Group() self.label_settings = label.Label( FONT_10, text="volume", color=0xFFFFFF, anchor_point=(0, 0), anchored_position=(0, 0), ) self.label_value = label.Label( FONT_10, text="100%", color=0xFFFFFF, anchor_point=(1, 0), anchored_position=(110, 12), ) self.group.append(self.label_settings) self.group.append(self.label_value) self.group.append( label.Label( FONT_10, text="VALUE", color=0xFFFFFF, anchor_point=(1, 0.5), anchored_position=(128, 16), label_direction="DWR", ) ) self.group.append( rect.Rect( x=118, y=0, width=1, height=32, fill=0xFFFFFF, ) ) def enter(self): self.update_display() super().enter() def exit(self): super().exit() self.settings.store() @property def setting(self) -> Setting: gi, si = self.active if gi == 0: return self.settings.profile_setting return self.ui_settings[gi][si] def update_pixels(self, pixels: NeoPixel): pixels.fill(0) super().update_pixels(pixels) pixels[2] = BLUE_DIM pixels[3] = BLUE_DIM gi, si = self.active for i, setting in enumerate(self.ui_settings): pixels[led_map[i]] = self.GROUP_ACTIVE_DIM pixels[led_map[gi]] = self.GROUP_ACTIVE if gi != 0: num_settings = len(self.ui_settings[gi]) so = 12 - num_settings for i in range(num_settings): pixels[led_map[so + i]] = self.SETTING_ACTIVE_DIM pixels[led_map[so + si]] = self.SETTING_ACTIVE for i, color in enumerate(self.setting.get_colors()): pixels[led_map[24 + i]] = color def update_display(self): self.label_settings.text = self.setting.name + ":" self.label_value.text = str(self.setting.format()) def key_event(self, i: int, pressed: bool) -> bool: if pressed and i == Key.MENU_I: self.keyboard.mode = self.keyboard.modes["base"] return True if not pressed: return False if i < len(self.ui_settings) and i != self.active[0]: self.active = (i, 0) self.update_display() return True gi, si = self.active so = 12 - len(self.ui_settings[gi]) if i >= so and i < 12: self.active = (gi, i - so) self.update_display() return True last_val = self.setting.value handled = False if 23 < i < 48: self.setting.press_key(i - 24) handled = True if i == Key.PREV_I: self.setting.press_prev() handled = True if i == Key.NEXT_I: self.setting.press_next() handled = True if handled: self.update_display() if gi == 0: # profile switched self.settings.store(last_val) self.settings.load(self.setting.value) else: self.settings.dispatch(self.ui_setting_ids[gi][si], last_val)