diff --git a/software/firmware/experimental/settings_menu.py b/software/firmware/experimental/settings_menu.py index 8ff81e1ae..fe5c4ae88 100644 --- a/software/firmware/experimental/settings_menu.py +++ b/software/firmware/experimental/settings_menu.py @@ -125,945 +125,6 @@ def is_visible(self, is_visible): self._is_visible = is_visible -class ChoiceMenuItem(MenuItem): - """ - A generic menu item for displaying a choice of options to the user - - :param parent: If the menu has multiple levels, what is this item's parent control? - :param children: If this menu has multiple levels, whar are this item's child controls? - :param title: The title to display at the top of the display when this control is active - :param prefix: A prefix to display before the title when this control is active - :param graphics: A dict of values mapped to FrameBuffer or bytearray objects, representing - 12x12 MONO_HLSB graphics to display along with the keyed values - :param labels: A dict of values mapped to strings, representing human-readible versions of the ConfigPoint - options - :param is_visible: Is this menu item visible by default? - """ - - SELECT_OPTION_Y = 16 - - def __init__( - self, - parent: MenuItem = None, - children: list[MenuItem] = None, - title: str = None, - prefix: str = None, - graphics: dict = None, - labels: dict = None, - is_visible: bool = True, - ): - super().__init__( - children=children, - parent=parent, - is_visible=is_visible - ) - - self.title = title - self.prefix = prefix - self.graphics = graphics - self.labels = labels - - self._is_editable = False - - def short_press(self): - """Toggle is_editable on a short press""" - self.is_editable = not self.is_editable - - def draw(self, oled=europi.oled): - """ - Draw the current item to the display object - - You MUST call the display's .show() function after calling this in order to send the buffer to the display - hardware - - :param oled: A Display instance (or compatible class) to render the item - """ - super().draw(oled) - - if self.is_editable: - display_value = self.menu.knob.choice(self.choices) - else: - display_value = self.default_choice - - text_left = 0 - prefix_left = 1 - prefix_right = len(self.prefix) * europi.CHAR_WIDTH - title_left = len(self.prefix) * europi.CHAR_WIDTH + 4 - - # If we're in a top-level menu the submenu is non-empty. In that case, the prefix in inverted text - # Otherwise, the title in inverted text to indicate we're in the sub-menu - if self.children and len(self.children) > 0: - oled.fill_rect(prefix_left - 1, 0, prefix_right + 1, europi.CHAR_HEIGHT + 2, 1) - oled.text(self.prefix, prefix_left, 1, 0) - oled.text(self.title, title_left, 1, 1) - else: - oled.fill_rect( - title_left - 1, - 0, - len(self.title) * europi.CHAR_WIDTH + 2, - europi.CHAR_HEIGHT + 2, - 1, - ) - oled.text(self.prefix, prefix_left, 1, 1) - oled.text(self.title, title_left, 1, 0) - - if self.graphics: - gfx = self.graphics.get(display_value, None) - if gfx: - text_left = 14 # graphics are 12x12, so add 2 pixel padding - if type(gfx) is bytearray: - gfx = FrameBuffer(gfx, 12, 12, MONO_HLSB) - oled.blit(gfx, 0, self.SELECT_OPTION_Y) - - if self.labels: - display_text = self.labels.get(display_value, str(display_value)) - else: - display_text = str(display_value) - - if self.is_editable: - # draw the value in inverted text - text_width = len(display_text) * europi.CHAR_WIDTH - - oled.fill_rect( - text_left, - self.SELECT_OPTION_Y, - text_left + text_width + 3, - europi.CHAR_HEIGHT + 4, - 1, - ) - oled.text(display_text, text_left + 1, self.SELECT_OPTION_Y + 2, 0) - else: - # draw the selection in normal text - oled.text(display_text, text_left + 1, self.SELECT_OPTION_Y + 2, 1) - - @property - def choices(self): - raise NotImplemented("choices(self) must be implemented by the sub-class!") - - @property - def default_choice(self): - raise NotImplemented("default_choice(self) must be implemented by the sub-class!") - - @property - def is_editable(self): - return self._is_editable - - @is_editable.setter - def is_editable(self, can_edit): - self._is_editable = can_edit - - -class SettingMenuItem(ChoiceMenuItem): - """ - A single menu item that presents a setting the user can manipulate - - The menu item is a wrapper around a ConfigPoint, and uses - that object's values as the available selections. - - If the item has a callback function defined, it will be invoked once during initialization - - :param config_point: The configration option this menu item controls - :param parent: If the menu has multiple levels, what is this item's parent control? - :param children: If this menu has multiple levels, whar are this item's child controls? - :param title: The title to display at the top of the display when this control is active - :param prefix: A prefix to display before the title when this control is active - :param graphics: A dict of values mapped to FrameBuffer or bytearray objects, representing - 12x12 MONO_HLSB graphics to display along with the keyed values - :param labels: A dict of values mapped to strings, representing human-readible versions of the ConfigPoint options - :param callback: A function to invoke when this item's value changes. Must accept - (new_value, old_value, config_point, arg=None) as parameters - :param callback_arg: An optional additional argument to pass to the callback function - :param float_resolution: The resolution of floating-point config points - (ignored if config_point is not a FloatConfigPoint) - :param value_map: An optional dict to map the underlying simple ConfigPoint values - to more complex objects e.g. map the string "CMaj" to a Quantizer object - :param is_visible: Is this menu item visible by default? - :param autoselect_knob: If True, this item gets "Knob" as an additional choice, allowing ad-hoc selection via the knob - :param autoselect_cv: If True, this item gets "AIN" as an additional choice, allowing ad-hoc selection via the CV input - """ - - def __init__( - self, - config_point: ConfigPoint = None, - parent: MenuItem = None, - children: list[MenuItem] = None, - title: str = None, - prefix: str = None, - graphics: dict = None, - labels: dict = None, - callback=lambda new_value, old_value, config_point, arg: None, - callback_arg=None, - float_resolution=2, - value_map: dict = None, - is_visible: bool = True, - autoselect_knob: bool = False, - autoselect_cv: bool = False, - ): - if title is None: - title = config_point.name - if prefix is None: - prefix = "" - - super().__init__( - parent=parent, - children=children, - title=title, - prefix=prefix, - graphics=graphics, - labels=labels, - is_visible=is_visible, - ) - - self.autoselect_cv = autoselect_cv - self.autoselect_knob = autoselect_knob - self.value_map = value_map - - # the configuration setting that we're controlling via this menu item - # convert everything to a choice configuration; this way we can add the knob/ain options too - if type(config_point) is FloatConfigPoint: - self.float_resolution = float_resolution - self.src_config = config_point - choices = self.get_option_list() - self.config_point = ChoiceConfigPoint( - config_point.name, - choices=choices, - default=config_point.default, - danger=config_point.danger, - ) - - self.NUM_AUTOINPUT_CHOICES = 0 - if self.autoselect_cv or self.autoselect_knob: - if self.autoselect_cv: - self.NUM_AUTOINPUT_CHOICES += 1 - if self.autoselect_knob: - self.NUM_AUTOINPUT_CHOICES += 1 - - if not self.graphics: - self.graphics = {} - self.graphics[AUTOSELECT_AIN] = AIN_GRAPHICS - self.graphics[AUTOSELECT_KNOB] = KNOB_GRAPHICS - - if not self.labels: - self.labels = {} - self.labels[AUTOSELECT_AIN] = AIN_LABEL - self.labels[AUTOSELECT_KNOB] = KNOB_LABEL - - self.callback_fn = callback - self.callback_arg = callback_arg - - # assign the initial value without firing any callbacks - self._value = self.config_point.default - self._value_choice = self.config_point.default - - @property - def choices(self): - return self.config_point.choices - - @property - def default_choice(self): - return self.value_choice - - def reset_to_default(self): - """ - Reset this item to its default value - """ - self.choose(self.src_config.default) - - def modify_choices(self, choices=None, new_default=None): - """ - Regenerate this item's available choices - - This is needed if we externally modify e.g. the maximum/minimum values of the underlying - config point as a result of one option needing to be within a range determined by another. - - :param choices: The list of new options we want to allow the user to choose from, excluding any autoselections - :param new_default: A value to assign to this setting if its existing value is out-of-range - """ - if choices is None: - choices = self.get_option_list() - else: - # add the autoselect items, if needed - if self.autoselect_knob: - choices.append(AUTOSELECT_KNOB) - if self.autoselect_knob: - choices.append(AUTOSELECT_AIN) - - self.config_point.choices = choices - still_valid = self.config_point.validate(self.value) - if not still_valid.is_valid: - self.choose(new_default) - - def short_press(self): - """ - Handle a short button press - - This enters edit mode, or applies the selection and - exits edit mode - """ - # don't allow edit-mode for strings! - if type(self.src_config) is StringConfigPoint: - return - - if self.is_editable: - new_choice = self.menu.knob.choice(self.config_point.choices) - if new_choice != self.value_choice: - # apply the currently-selected choice if we're in edit mode - self.choose(new_choice) - self.ui_dirty = True - self.menu.settings_dirty = True - - super().short_press() - - def get_option_list(self): - """ - Get the list of options the user can choose from - - :return: A list of choices - """ - t = type(self.src_config) - if t is FloatConfigPoint: - FLOAT_RESOLUTION = 1.0 / (10**self.float_resolution) - items = [] - x = self.src_config.minimum - while x <= self.src_config.maximum: - items.append(round(x, self.float_resolution)) - x += FLOAT_RESOLUTION - items.append(round(self.src_config.maximum, self.float_resolution)) - elif t is IntegerConfigPoint: - items = list(range(self.src_config.minimum, self.src_config.maximum + 1)) - elif t is BooleanConfigPoint: - items = [False, True] - elif t is ChoiceConfigPoint: - items = list(self.src_config.choices) # make a copy of the items so we can append the autoselect items! - elif t is StringConfigPoint: - # don't allow autoselect for strings; they're not editable/selectable - return [self.src_config.default] - else: - raise Exception(f"Unsupported ConfigPoint type: {type(self.src_config)}") - - # Add the autoselect inputs, if needed - if self.autoselect_knob: - items.append(AUTOSELECT_KNOB) - if self.autoselect_knob: - items.append(AUTOSELECT_AIN) - - return items - - def autoselect(self, percent: float): - """ - Called by the parent menu when the Knob/CV timer fires, automatically updating the value of this item - - :param percent: A value 0-1 indicating the level of the knob/cv source - """ - last_choice = len(self.config_point.choices) - self.NUM_AUTOINPUT_CHOICES - index = int( - percent * last_choice - ) - if index >= last_choice: - index = last_choice - 1 - - item = self.config_point.choices[index] - if item != self._value: - self.ui_dirty = True - old_value = self._value - self._value = item - self.callback_fn(item, old_value, self.config_point, self.callback_arg) - - def choose(self, choice): - """ - Set the raw value of this item's ConfigPoint - - :param choice: The value to assign to the ConfigPoint. - - :raises ValueError: if the given choice is not valid for this setting - """ - # choose whatever string we're given - if type(self.src_config) is StringConfigPoint: - self.src_config.default = choice - self.config_point.choices = [choice] - self._value = choice - self._value_choice = choice - return - - # kick out early if we aren't actually choosing anything - if choice == self.value_choice: - return - - validation = self.config_point.validate(choice) - if not validation.is_valid: - raise ValueError(f"{choice} is not a valid value for {self.config_point.name}") - - old_value = self._value_choice - self._value_choice = choice - - if old_value == AUTOSELECT_AIN: - self.menu.unregister_autoselect_cv(self) - elif old_value == AUTOSELECT_KNOB: - self.menu.unregister_autoselect_knob(self) - - if self._value_choice == AUTOSELECT_AIN: - self.menu.register_autoselect_cv(self) - elif self._value_choice == AUTOSELECT_KNOB: - self.menu.register_autoselect_knob(self) - else: - self._value = choice - self.callback_fn(choice, old_value, self.config_point, self.callback_arg) - - def draw(self, oled=europi.oled): - """ - Render this item to the screen - - You must call the screen's ``.show()`` method after calling this - - :param oled: The screen we're drawing to - """ - super().draw(oled) - - # show the real value in parentheses - if self.value_choice == AUTOSELECT_AIN or self.value_choice == AUTOSELECT_KNOB: - oled.text(f"({self.value})", europi.OLED_WIDTH//2, self.SELECT_OPTION_Y, 1) - - # add a ! to the lower-right corner to indicate a potentially - # volatile, edit-at-your-own-risk item - if self.config_point.danger: - fb = FrameBuffer(DANGER_GRAPHICS, 12, 12, MONO_HLSB) - oled.blit(fb, europi.OLED_WIDTH - 12, europi.OLED_HEIGHT - 12) - - if type(self.src_config) is StringConfigPoint: - oled.text( - "R/O", - europi.OLED_WIDTH - 3 * europi.CHAR_WIDTH, - europi.OLED_HEIGHT - europi.CHAR_HEIGHT, - 1 - ) - - @property - def value_choice(self): - """The value the user has chosen from the menu""" - return self._value_choice - - @property - def value(self): - """ - Get the raw value of this item's ConfigPoint - - You should use .mapped_value if you have assigned a value_map to the constructor - """ - return self._value - - @property - def mapped_value(self): - """ - Get the value of this item mapped by the value_map - - If value_map was not set by the constructor, this property returns the same - thing as .value - """ - if self.value_map: - return self.value_map[self._value] - return self._value - - -class ActionMenuItem(ChoiceMenuItem): - """ - A menu item that just invokes a callback function when selected. - - This class is similar to the SettingMenuItem, but doesn't wrap a ConfigPoint; it just has - options and fires the callback when you choose one - - :param actions: The list of choices the user can pick from. e.g. ["Cancel", "Ok"] - :param callback: The function to call when the user invokes the action. The selected item from choices is passed as the first parameter - :param callback_arg: The second parameter passed to the callback - :param parent: If the menu has multiple levels, what is this item's parent control? - :param children: If this menu has multiple levels, whar are this item's child controls? - :param title: The title to display at the top of the display when this control is active - :param prefix: A prefix to display before the title when this control is active - :param graphics: A dict of values mapped to FrameBuffer or bytearray objects, representing - 12x12 MONO_HLSB graphics to display along with the keyed values - :param labels: A dict of values mapped to strings, representing human-readible versions of the ConfigPoint options - :param is_visible: Is this menu item visible by default? - """ - - def __init__( - self, - actions: list[object], - callback = lambda x: None, - callback_arg: object = None, - parent: MenuItem = None, - children: list[MenuItem] = None, - title: str = None, - prefix: str = None, - graphics: dict = None, - labels: dict = None, - is_visible: bool = True, - ): - super().__init__( - parent=parent, - children=children, - title=title, - prefix=prefix, - graphics=graphics, - labels=labels, - is_visible=is_visible, - ) - - self.actions = actions - self.callback = callback - self.callback_arg = callback_arg - self.title = title - self.prefix = prefix - - self._is_editable = False - - @property - def choices(self): - return self.actions - - @property - def default_choice(self): - return self.choices[0] - - def short_press(self): - if self.is_editable: - # fire the callback if we're exiting edit-mode - choice = self.menu.knob.choice(self.choices) - self.callback(choice, self.callback_arg) - - super().short_press() - - - -class SettingsMenu: - """ - A menu-based GUI for any EuroPi script. - - This class is assumed to be the main interaction method for the program. - - Long/short press callbacks are invoked inside the handler for the falling edge of the button. It is recommended - to avoid any lengthy operations inside these callbacks, as they may prevent other interrupts from being - handled properly. - - :param menu_items: A list of MenuItem objects representing the top-level of the menu - :param navigation_button: The button the user presses to interact with the menu - :param navigation_knob: The knob the user turns to scroll through the menu. This may be an - experimental.knobs.KnobBank with 3 menu levels called "main_menu", "submenu" and "choice", or a raw knob like europi.k2 - :param short_press_cb: An optional callback function to invoke when the user interacts with a short-press of the button - :param long_press_cb: An optional callback function to invoke when the user interacts with a long-press of the button - :param autoselect_knob: A knob that the user can turn to select items without needing to menu-dive - :param autoselect_cv: An analogue input the user can use to select items with CV - """ - - # Treat a long press as anything more than 500ms - LONG_PRESS_MS = 500 - - def __init__( - self, - menu_items: list = None, - navigation_button: europi.Button = europi.b2, - navigation_knob: europi.Knob = europi.k2, - short_press_cb=lambda: None, - long_press_cb=lambda: None, - autoselect_knob: europi.Knob = europi.k1, - autoselect_cv: europi.AnalogueInput = europi.ain, - ): - self._knob = navigation_knob - self.button = navigation_button - - self._ui_dirty = True - - self.short_press_cb = short_press_cb - self.long_press_cb = long_press_cb - - self.button.handler(self.on_button_press) - self.button.handler_falling(self.on_button_release) - - self.items = [] - if menu_items: - for item in menu_items: - self.items.append(item) - - self.active_items = self.items - self.active_item = self.knob.choice(self.items) - - self.button_down_at = time.ticks_ms() - - # Indicates to the application that we need to save the settings to disk - self.settings_dirty = False - - # Iterate through the menu and get all of the config points - self.config_points_by_name = {} - self.menu_items_by_name = {} - for item in self.items: - item.menu = self - - if type(item) is SettingMenuItem: - self.config_points_by_name[item.config_point.name] = item.config_point - self.menu_items_by_name[item.config_point.name] = item - - if item.children: - for c in item.children: - c.menu = self - - if type(c) is SettingMenuItem: - self.config_points_by_name[c.config_point.name] = c.config_point - self.menu_items_by_name[c.config_point.name] = c - - # set up a timer for menu items that choose automatically based on the alternate knob or ain - self.autoselect_cv = autoselect_cv - self.autoselect_knob = autoselect_knob - self.autoselect_timer = Timer() - self.autoselect_cv_items = [] - self.autoselect_knob_items = [] - - @property - def knob(self): - """Get the navigation knob that controls this menu""" - if type(self._knob) is KnobBank: - return self._knob.current - else: - return self._knob - - def get_config_points(self): - """ - Get the config points for the menu so we can load/save them as needed - """ - return list(self.config_points_by_name.values()) - - def load_defaults(self, settings_file): - """ - Load the initial settings from the file - - :param settings_file: The path to a JSON file where the user's settings are saved - """ - failed_key_counts = {} - - # because we may have a situation where, via callbacks, some settings' options are dynamically - # modified, we need to load iteratively - json_data = load_json_file(settings_file) - keys = list(json_data.keys()) - max_tries = len(keys) - while len(keys) > 0: - k = keys[0] - keys.pop(0) - if k in self.menu_items_by_name: - try: - self.menu_items_by_name[k].choose(json_data[k]) - except ValueError as err: - if k not in failed_key_counts: - failed_key_counts[k] = 1 - else: - failed_key_counts[k] += 1 - # Bump this key to the back and try again; a prior key might fix the problem - if failed_key_counts[k] < max_tries: - keys.append(k) - else: - raise - - def save(self, settings_file): - """ - Save the current settings to the specified file - - :param settings_file: The path to the JSON file to generate - """ - data = {} - for item in self.menu_items_by_name.values(): - data[item.config_point.name] = item.value_choice - try: - # ensure the /config directory exists - # save_to_file won't create it for us! - os.mkdir("config") - except OSError: - pass - ConfigFile.save_to_file(settings_file, data) - self.settings_dirty = False - - def on_button_press(self): - """Handler for the rising edge of the button signal""" - self.button_down_at = time.ticks_ms() - self._ui_dirty = True - - def on_button_release(self): - """Handler for the falling edge of the button signal""" - self._ui_dirty = True - if time.ticks_diff(time.ticks_ms(), self.button_down_at) >= self.LONG_PRESS_MS: - self.long_press() - else: - self.short_press() - - def short_press(self): - """ - Handle a short button press - - This enters edit mode, or applies the selection and - exits edit mode - """ - self.active_item.short_press() - - # Cycle the knob bank, if necessary - if type(self._knob) is KnobBank: - if self.active_item.is_editable: - self._knob.set_current("choice") - if issubclass(type(self.active_item), SettingMenuItem): - # lock the knob to our current value - self._knob.current.change_lock_value( - self.active_item.choices.index(self.active_item.value_choice) / - len(self.active_item.choices) - ) - elif self.active_item.children and len(self.active_item.children) > 0: - self._knob.set_current("main_menu") - else: - self._knob.set_current("submenu") - - self.short_press_cb() - - def long_press(self): - """ - Handle a long button press - - This changes between the two menu levels (if possible) - """ - # exit editable mode when we change menu levels - self.active_item.is_editable = False - - # we're in the top-level menu; go to the submenu if it exists - if self.active_items == self.items: - if self.active_item.children: - self.active_items = self.active_item.children - if type(self.knob) is KnobBank: - self.knob.set_current("submenu") - else: - self.active_items = self.items - if type(self.knob) is KnobBank: - self.knob.set_current("main_menu") - - self.long_press_cb() - - def draw(self, oled=europi.oled): - """ - Draw the menu to the given display - - You should call the display's .fill(0) function before calling this in order to clear the screen. Otherwise - the menu item will be drawn on top of whatever is on the screen right now. (In some cases this may be the - desired result, but when in doubt, call oled.fill(0) first). - - You MUST call the display's .show() function after calling this in order to send the buffer to the display - hardware - - :param oled: The display object to draw to - """ - if not self.active_item.is_editable: - self.active_item = self.knob.choice(self.visible_items) - self.active_item.draw(oled) - self._ui_dirty = False - - def register_autoselect_cv(self, menu_item: SettingMenuItem): - """ - Connects a menu item to this menu's CV input - - :param menu_item: The item that wants to subscribe to the CV input - """ - if len(self.autoselect_cv_items) == 0 and len(self.autoselect_knob_items) == 0: - self.autoselect_timer.init(freq=10, mode=Timer.PERIODIC, callback=self.do_autoselect) - self.autoselect_cv_items.append(menu_item) - - def register_autoselect_knob(self, menu_item: SettingMenuItem): - """ - Connects a menu item to this menu's knob input - - :param menu_item: The item that wants to subscribe to the knob input - """ - if len(self.autoselect_cv_items) == 0 and len(self.autoselect_knob_items) == 0: - self.autoselect_timer.init(freq=10, mode=Timer.PERIODIC, callback=self.do_autoselect) - self.autoselect_knob_items.append(menu_item) - - def unregister_autoselect_cv(self, menu_item: SettingMenuItem): - """ - Disconnects a menu item to this menu's CV input - - :param menu_item: The item that wants to unsubscribe from the CV input - """ - self.autoselect_cv_items.remove(menu_item) - if len(self.autoselect_cv_items) == 0 and len(self.autoselect_knob_items) == 0: - self.autoselect_timer.deinit() - - def unregister_autoselect_knob(self, menu_item: SettingMenuItem): - """ - Disconnects a menu item to this menu's knob input - - :param menu_item: The item that wants to unsubscribe from the knob input - """ - self.autoselect_knob_items.remove(menu_item) - if len(self.autoselect_cv_items) == 0 and len(self.autoselect_knob_items) == 0: - self.autoselect_timer.deinit() - - def do_autoselect(self, timer): - """ - Callback function for the autoselection timer - - Reads from ain and/or the autoselect knob and applies that choice to all subscribed menu items - - :param timer: The timer instance that fired this callback - """ - if len(self.autoselect_cv_items) > 0: - ain_percent = self.autoselect_cv.percent() - for item in self.autoselect_cv_items: - item.autoselect(ain_percent) - - if len(self.autoselect_knob_items) > 0: - knob_percent = self.autoselect_knob.percent() - for item in self.autoselect_knob_items: - item.autoselect(knob_percent) - - @property - def ui_dirty(self): - """ - Is the UI currently dirty and needs re-drawing? - - This will be true if the user has pressed the button or rotated the knob sufficiently - to change the active item - """ - return self._ui_dirty or self.active_item.ui_dirty or self.active_item != self.knob.choice(self.visible_items) - - @property - def visible_items(self): - """ - Get the set of visible menu items for the current state of the menu - - Menu items can be shown/hidden by setting their is_visible property. Normally this should be done in - a value-change callback of a menu item to show/hide dependent other items. - """ - items = [] - for item in self.active_items: - if item.is_visible: - items.append(item) - return items -# Copyright 2024 Allen Synthesis -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Contains objects used for interactive settings menus - -Menu interaction is done using a knob and a button (K2 and B2 by default): -- rotate the knob to select the menu item -- short-press to enter edit mode -- rotate knob to select an option -- short-press button to apply the new option -- long-press button to change between the 2 menu levels (if possible) - -For examples of how to create and use the SettingsMenu, please refer to -- contrib/settings_menu_example.py - -Additional, more complex, examples can be found in: -- contrib/euclid.py -- contrib/pams.py (this is a very complex example) -- contrib/sequential_switch.py -- tools/conf_edit.py -""" - -import europi - -from configuration import * -from experimental.knobs import KnobBank, LockableKnob -from framebuf import FrameBuffer, MONO_HLSB -from machine import Timer -import os -import time - - -# fmt: off -AIN_GRAPHICS = bytearray(b"\x00\x00|\x00|\x00d\x00d\x00g\x80a\x80\xe1\xb0\xe1\xb0\x01\xf0\x00\x00\x00\x00") -KNOB_GRAPHICS = bytearray(b"\x06\x00\x19\x80 @@ @ \x80\x10\x82\x10A @\xa0 @\x19\x80\x06\x00") - -AIN_LABEL = "AIN" -KNOB_LABEL = "Knob" - -AUTOSELECT_AIN = "autoselect_ain" -AUTOSELECT_KNOB = "autoselect_knob" - -DANGER_GRAPHICS = bytearray(b'\x00\x00\x04\x00\n\x00\n\x00\x11\x00\x15\x00$\x80$\x80@@D@\x80 \xff\xe0') -# fmt: off - - -class MenuItem: - """ - Generic class for anything we can display in the menu - - :param parent: A MenuItem representing this item's parent, if this item is the bottom-level of a multi-level menu - :param children: A list of MenuItems representing this item's children, if this is the top-level of a multi-level menu - :param is_visible: Is this menu item visible by default? - """ - - def __init__( - self, children: list[object] = None, parent: object = None, is_visible: bool = True - ): - self.menu = None - self.parent = parent - self.children = children - self.is_visible = is_visible - - # Used to indicate that if this is the active menu item, the UI should re-render it - self.ui_dirty = False - - if parent and children: - raise Exception("Cannot specify parent and children in the same menu item") - - def short_press(self): - """ - Handler for when the user short-presses the button - - This does nothing by default, but can be overridden by child classes - """ - pass - - def draw(self, oled=europi.oled): - """ - Draw the item to the screen - - You must call the screen's ``.show()`` method after calling this - - :param oled: A Display-compatible object we draw to - """ - self.ui_dirty = False - - def add_child(self, item): - """ - Add a new child item to this item - - :param item: The menu item to add as a new child - """ - if self.children is None: - self.children = [] - self.children.append(item) - item.parent = self - item.menu = self.menu - - @property - def is_editable(self): - return False - - @is_editable.setter - def is_editable(self, can_edit): - pass - - @property - def is_visible(self): - return self._is_visible - - @is_visible.setter - def is_visible(self, is_visible): - self._is_visible = is_visible - - class ChoiceMenuItem(MenuItem): """ A generic menu item for displaying a choice of options to the user