diff --git a/ui/control_ui.py b/ui/control_ui.py index c5a9db4..7dba967 100644 --- a/ui/control_ui.py +++ b/ui/control_ui.py @@ -37,6 +37,10 @@ field_mapping, help_text = parse_ini_file(translation_file) def display_menu(current_menu, menu_path, selected_index, show_save_option, help_text): min_help_window_height = 6 num_items = len(current_menu) + (1 if show_save_option else 0) + # Track visible range + global start_index + if 'start_index' not in globals(): + start_index = [0] # Initialize if not set # Determine the available height for the menu max_menu_height = curses.LINES @@ -90,7 +94,7 @@ def display_menu(current_menu, menu_path, selected_index, show_save_option, help menu_win.refresh() menu_pad.refresh( - 0, 0, + start_index[-1], 0, menu_win.getbegyx()[0] + 3, menu_win.getbegyx()[1] + 4, menu_win.getbegyx()[0] + 3 + menu_win.getmaxyx()[0] - 5 - (2 if show_save_option else 0), menu_win.getbegyx()[1] + menu_win.getmaxyx()[1] - 8 @@ -241,36 +245,45 @@ def get_wrapped_help_text(help_text, transformed_path, selected_option, width, m def move_highlight(old_idx, new_idx, options, show_save_option, menu_win, menu_pad, help_win, help_text, menu_path, max_help_lines): - if old_idx == new_idx: # No-op return max_index = len(options) + (1 if show_save_option else 0) - 1 + visible_height = menu_win.getmaxyx()[0] - 5 - (2 if show_save_option else 0) - if show_save_option and old_idx == max_index: # Special case un-highlight "Save" option + # Adjust start_index only when moving out of visible range + if new_idx < start_index[-1]: # Moving above the visible area + start_index[-1] = new_idx + elif new_idx >= start_index[-1] + visible_height: # Moving below the visible area + start_index[-1] = new_idx - visible_height + + # Ensure start_index is within bounds + start_index[-1] = max(0, min(start_index[-1], max_index - visible_height + 1)) + + # Clear old selection + if show_save_option and old_idx == max_index: menu_win.chgat(menu_win.getmaxyx()[0] - 2, (width - len(save_option)) // 2, len(save_option), get_color("settings_save")) else: menu_pad.chgat(old_idx, 0, menu_pad.getmaxyx()[1], get_color("settings_sensitive") if options[old_idx] in sensitive_settings else get_color("settings_default")) - if show_save_option and new_idx == max_index: # Special case highlight "Save" option + # Highlight new selection + if show_save_option and new_idx == max_index: menu_win.chgat(menu_win.getmaxyx()[0] - 2, (width - len(save_option)) // 2, len(save_option), get_color("settings_save", reverse=True)) else: menu_pad.chgat(new_idx, 0, menu_pad.getmaxyx()[1], get_color("settings_sensitive", reverse=True) if options[new_idx] in sensitive_settings else get_color("settings_default", reverse=True)) menu_win.refresh() - - start_index = max(0, new_idx - (menu_win.getmaxyx()[0] - 5 - (2 if show_save_option else 0)) - (1 if show_save_option and new_idx == max_index else 0)) - menu_pad.refresh(start_index, 0, + + # Refresh pad only if scrolling is needed + menu_pad.refresh(start_index[-1], 0, menu_win.getbegyx()[0] + 3, menu_win.getbegyx()[1] + 4, - menu_win.getbegyx()[0] + 3 + menu_win.getmaxyx()[0] - 5 - (2 if show_save_option else 0), + menu_win.getbegyx()[0] + 3 + visible_height, menu_win.getbegyx()[1] + menu_win.getmaxyx()[1] - 8) - # Transform menu path + # Update help window transformed_path = transform_menu_path(menu_path) selected_option = options[new_idx] if new_idx < len(options) else None help_y = menu_win.getbegyx()[0] + menu_win.getmaxyx()[0] - - # Call helper function to update the help window help_win = update_help_window(help_win, help_text, transformed_path, selected_option, max_help_lines, width, help_y, menu_win.getbegyx()[1]) @@ -337,6 +350,7 @@ def settings_menu(stdscr, interface): elif key == curses.KEY_RIGHT or key == ord('\n'): need_redraw = True + start_index.append(0) menu_win.erase() help_win.erase() @@ -372,6 +386,7 @@ def settings_menu(stdscr, interface): filename = get_text_input("Enter a filename for the config file") if not filename: logging.info("Export aborted: No filename provided.") + start_index.pop() continue # Go back to the menu if not filename.lower().endswith(".yaml"): filename += ".yaml" @@ -384,14 +399,14 @@ def settings_menu(stdscr, interface): overwrite = get_list_input(f"{filename} already exists. Overwrite?", None, ["Yes", "No"]) if overwrite == "No": logging.info("Export cancelled: User chose not to overwrite.") + start_index.pop() continue # Return to menu - os.makedirs(os.path.dirname(yaml_file_path), exist_ok=True) with open(yaml_file_path, "w", encoding="utf-8") as file: file.write(config_text) logging.info(f"Config file saved to {yaml_file_path}") dialog(stdscr, "Config File Saved:", yaml_file_path) - + start_index.pop() continue except PermissionError: logging.error(f"Permission denied: Unable to write to {yaml_file_path}") @@ -399,8 +414,9 @@ def settings_menu(stdscr, interface): logging.error(f"OS error while saving config: {e}") except Exception as e: logging.error(f"Unexpected error: {e}") + start_index.pop() continue - + elif selected_option == "Load Config File": folder_path = os.path.join(app_directory, config_folder) @@ -422,6 +438,7 @@ def settings_menu(stdscr, interface): overwrite = get_list_input(f"Are you sure you want to load {filename}?", None, ["Yes", "No"]) if overwrite == "Yes": config_import(interface, file_path) + start_index.pop() continue elif selected_option == "Config URL": @@ -433,6 +450,7 @@ def settings_menu(stdscr, interface): if overwrite == "Yes": interface.localNode.setURL(new_value) logging.info(f"New Config URL sent to node") + start_index.pop() continue elif selected_option == "Reboot": @@ -440,6 +458,7 @@ def settings_menu(stdscr, interface): if confirmation == "Yes": interface.localNode.reboot() logging.info(f"Node Reboot Requested by menu") + start_index.pop() continue elif selected_option == "Reset Node DB": @@ -447,6 +466,7 @@ def settings_menu(stdscr, interface): if confirmation == "Yes": interface.localNode.resetNodeDb() logging.info(f"Node DB Reset Requested by menu") + start_index.pop() continue elif selected_option == "Shutdown": @@ -454,6 +474,7 @@ def settings_menu(stdscr, interface): if confirmation == "Yes": interface.localNode.shutdown() logging.info(f"Node Shutdown Requested by menu") + start_index.pop() continue elif selected_option == "Factory Reset": @@ -461,14 +482,15 @@ def settings_menu(stdscr, interface): if confirmation == "Yes": interface.localNode.factoryReset() logging.info(f"Factory Reset Requested by menu") + start_index.pop() continue - elif selected_option == "App Settings": - menu_win.clear() - menu_win.refresh() - json_editor(stdscr) # Open the App Settings menu - continue - # need_redraw = True + # elif selected_option == "App Settings": + # menu_win.clear() + # menu_win.refresh() + # json_editor(stdscr) # Open the App Settings menu + # continue + # # need_redraw = True field_info = current_menu.get(selected_option) if isinstance(field_info, tuple): @@ -495,6 +517,8 @@ def settings_menu(stdscr, interface): for option, (field, value) in current_menu.items(): modified_settings[option] = value + start_index.pop() + elif selected_option in ['latitude', 'longitude', 'altitude']: new_value = get_text_input(f"{human_readable_name} is currently: {current_value}") new_value = current_value if new_value is None else new_value @@ -504,37 +528,47 @@ def settings_menu(stdscr, interface): if option in current_menu: modified_settings[option] = current_menu[option][1] + start_index.pop() + elif selected_option == "admin_key": new_values = get_admin_key_input(current_value) new_value = current_value if new_values is None else [base64.b64decode(key) for key in new_values] + start_index.pop() elif field.type == 8: # Handle boolean type new_value = get_list_input(human_readable_name, str(current_value), ["True", "False"]) new_value = new_value == "True" or new_value is True + start_index.pop() elif field.label == field.LABEL_REPEATED: # Handle repeated field - Not currently used new_value = get_repeated_input(current_value) new_value = current_value if new_value is None else new_value.split(", ") + start_index.pop() elif field.enum_type: # Enum field enum_options = {v.name: v.number for v in field.enum_type.values} new_value_name = get_list_input(human_readable_name, current_value, list(enum_options.keys())) new_value = enum_options.get(new_value_name, current_value) + start_index.pop() elif field.type == 7: # Field type 7 corresponds to FIXED32 new_value = get_fixed32_input(current_value) + start_index.pop() elif field.type == 13: # Field type 13 corresponds to UINT32 new_value = get_text_input(f"{human_readable_name} is currently: {current_value}") new_value = current_value if new_value is None else int(new_value) + start_index.pop() elif field.type == 2: # Field type 13 corresponds to INT64 new_value = get_text_input(f"{human_readable_name} is currently: {current_value}") new_value = current_value if new_value is None else float(new_value) + start_index.pop() else: # Handle other field types new_value = get_text_input(f"{human_readable_name} is currently: {current_value}") new_value = current_value if new_value is None else new_value + start_index.pop() for key in menu_path[3:]: # Skip "Main Menu" modified_settings = modified_settings.setdefault(key, {}) @@ -554,6 +588,7 @@ def settings_menu(stdscr, interface): menu_index.append(selected_index) selected_index = 0 + elif key == curses.KEY_LEFT: need_redraw = True @@ -576,7 +611,8 @@ def settings_menu(stdscr, interface): for step in menu_path[1:]: current_menu = current_menu.get(step, {}) selected_index = menu_index.pop() - + start_index.pop() + elif key == 27: # Escape key menu_win.erase() menu_win.refresh() @@ -599,5 +635,4 @@ def set_region(interface): new_region_number = region_name_to_number.get(new_region_name, 0) # Default to 0 if not found node.localConfig.lora.region = new_region_number - node.writeConfig("lora") - \ No newline at end of file + node.writeConfig("lora") \ No newline at end of file diff --git a/utilities/input_handlers.py b/utilities/input_handlers.py index 4977cfe..1aac5da 100644 --- a/utilities/input_handlers.py +++ b/utilities/input_handlers.py @@ -391,18 +391,38 @@ def get_list_input(prompt, current_option, list_options): def move_highlight(old_idx, new_idx, options, list_win, list_pad): + + global scroll_offset + if 'scroll_offset' not in globals(): + scroll_offset = 0 # Initialize if not set + if old_idx == new_idx: - return # no-op + return # No-op + max_index = len(options) - 1 + visible_height = list_win.getmaxyx()[0] - 5 + + # Adjust scroll_offset only when moving out of visible range + if new_idx < scroll_offset: # Moving above the visible area + scroll_offset = new_idx + elif new_idx >= scroll_offset + visible_height: # Moving below the visible area + scroll_offset = new_idx - visible_height + + # Ensure scroll_offset is within bounds + scroll_offset = max(0, min(scroll_offset, max_index - visible_height + 1)) + + # Clear old highlight list_pad.chgat(old_idx, 0, list_pad.getmaxyx()[1], get_color("settings_default")) - list_pad.chgat(new_idx, 0, list_pad.getmaxyx()[1], get_color("settings_default", reverse = True)) + + # Highlight new selection + list_pad.chgat(new_idx, 0, list_pad.getmaxyx()[1], get_color("settings_default", reverse=True)) list_win.refresh() - - start_index = max(0, new_idx - (list_win.getmaxyx()[0] - 5)) - - list_win.refresh() - list_pad.refresh(start_index, 0, + + # Refresh pad only if scrolling is needed + list_pad.refresh(scroll_offset, 0, list_win.getbegyx()[0] + 3, list_win.getbegyx()[1] + 4, - list_win.getbegyx()[0] + list_win.getmaxyx()[0] - 2, list_win.getbegyx()[1] + 4 + list_win.getmaxyx()[1] - 4) - \ No newline at end of file + list_win.getbegyx()[0] + 3 + visible_height, + list_win.getbegyx()[1] + list_win.getmaxyx()[1] - 4) + + return scroll_offset # Return updated scroll_offset to be stored externally \ No newline at end of file