From 86f00c5236b6ba13b859b90bae2bb9c3a7565294 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Fri, 3 Oct 2025 04:56:27 +0800 Subject: [PATCH 1/2] Implement lxdialog-style button dialogs This adds button dialog support matching Linux menuconfig (lxdialog) visual style and behavior. --- menuconfig.py | 355 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 324 insertions(+), 31 deletions(-) diff --git a/menuconfig.py b/menuconfig.py index 77c57cd..dc5dfc2 100755 --- a/menuconfig.py +++ b/menuconfig.py @@ -298,6 +298,15 @@ edit=fg:black,bg:white jump-edit=fg:black,bg:white text=fg:black,bg:white + shadow=fg:black,bg:black,bold + button-active=fg:black,bg:white + button-inactive=fg:white,bg:blue,bold + button-key-active=fg:red,bg:white + button-key-inactive=fg:white,bg:blue,bold + button-label-active=fg:black,bg:white,bold + button-label-inactive=fg:yellow,bg:blue,bold + dialog=fg:black,bg:white + dialog-frame=fg:white,bg:blue,bold """, # This style is forced on terminals that do not support colors @@ -934,25 +943,26 @@ def _quit_dialog(): if not _conf_changed: return "No changes to save (for '{}')".format(_conf_filename) - while True: - c = _key_dialog( - "Quit", - " Save configuration?\n" - "\n" - "(Y)es (N)o (C)ancel", - "ync") + # Use button dialog with Yes/No/Cancel buttons (matching lxdialog style) + result = _button_dialog( + None, # No title in yesno dialog + "Save configuration?", + [" Yes ", " No ", " Cancel "], + default_button=0) - if c is None or c == "c": - return None + if result is None or result == 2: # ESC or Cancel + return None - if c == "y": - # Returns a message to print - msg = _try_save(_kconf.write_config, _conf_filename, "configuration") - if msg: - return msg + if result == 0: # Yes + # Returns a message to print + msg = _try_save(_kconf.write_config, _conf_filename, "configuration") + if msg: + return msg + # If save failed, try again + return None - elif c == "n": - return "Configuration ({}) was not saved".format(_conf_filename) + elif result == 1: # No + return "Configuration ({}) was not saved".format(_conf_filename) def _init(): @@ -1367,10 +1377,16 @@ def _draw_main(): _menu_win.erase() + # Draw box around the menu window (like lxdialog's menubox) + menu_win_height, menu_win_width = _menu_win.getmaxyx() + _draw_box(_menu_win, 0, 0, menu_win_height, menu_win_width, + _style["list"], _style["list"]) + # Draw the _shown nodes starting from index _menu_scroll up to either as # many as fit in the window, or to the end of _shown + # Note: Now we need to account for the border (1 character on each side) for i in range(_menu_scroll, - min(_menu_scroll + _height(_menu_win), len(_shown))): + min(_menu_scroll + _height(_menu_win) - 2, len(_shown))): node = _shown[i] @@ -1383,7 +1399,8 @@ def _draw_main(): else: style = _style["inv-selection" if i == _sel_node_i else "inv-list"] - _safe_addstr(_menu_win, i - _menu_scroll, 0, _node_str(node), style) + # Draw inside the box (offset by 1 row and 1 column) + _safe_addstr(_menu_win, 1 + i - _menu_scroll, 1, _node_str(node), style) _menu_win.noutrefresh() @@ -1789,6 +1806,13 @@ def _resize_input_dialog(win, title, info_lines): def _draw_input_dialog(win, title, info_lines, s, i, hscroll): edit_width = _width(win) - 4 + # Get window position and size for shadow + win_y, win_x = win.getbegyx() + win_height, win_width = win.getmaxyx() + + # Draw shadow on stdscr first + _draw_shadow(_stdscr, win_y, win_x, win_height, win_width) + win.erase() # Note: Perhaps having a separate window for the input field would be nicer @@ -1983,35 +2007,304 @@ def _resize_key_dialog(win, text): def _draw_key_dialog(win, title, text): + # Get window position and size for shadow + win_y, win_x = win.getbegyx() + win_height, win_width = win.getmaxyx() + + # Draw shadow on stdscr first + _draw_shadow(_stdscr, win_y, win_x, win_height, win_width) + win.erase() + # Draw the frame first + _draw_frame(win, title) + + # Then draw text content inside the frame + win.attron(_style["body"]) for i, line in enumerate(text.split("\n")): _safe_addstr(win, 2 + i, 2, line) - - # Draw the frame last so that it overwrites the body text for small windows - _draw_frame(win, title) + win.attroff(_style["body"]) win.noutrefresh() +def _button_dialog(title, text, buttons, default_button=0): + # Dialog with button selection support, matching lxdialog's yesno/msgbox + # + # title: Dialog title (shown at top of border if provided) + # text: Dialog text content + # buttons: List of button labels (e.g., [" Yes ", " No ", " Cancel "]) + # default_button: Index of initially selected button + # + # Returns: Index of selected button, or None if ESC pressed + + win = _styled_win("dialog") + win.keypad(True) + + selected_button = default_button + + # Calculate window size based on content + lines = text.split("\n") + # Height: border(1) + text lines + blank + separator(1) + buttons + border(1) + # = 1 + len(lines) + 1 + 1 + 1 + 1 = len(lines) + 5 + win_height = min(len(lines) + 5, _height(_stdscr) - 4) + # Calculate width from longest line and button row + # Button row width includes buttons + spacing between them + # 2 buttons: spacing 13, 3+ buttons: spacing 4 + spacing = 13 if len(buttons) == 2 else 4 + button_row_width = sum(len(b) + 2 for b in buttons) + spacing * (len(buttons) - 1) + win_width = min(max(max(len(line) for line in lines) + 4, button_row_width + 4), + _width(_stdscr) - 4) + + win.resize(win_height, win_width) + win.mvwin((_height(_stdscr) - win_height)//2, + (_width(_stdscr) - win_width)//2) + + while True: + # Draw main display behind dialog + _draw_main() + + # Get window position and size + win_y, win_x = win.getbegyx() + win_height, win_width = win.getmaxyx() + + # Don't draw shadow for now - TODO: Add shadow support later + + win.erase() + + # Draw box border with proper colors + # Use dialog-frame (blue background) for both box and border to get uniform blue frame + _draw_box(win, 0, 0, win_height, win_width, + _style.get("dialog-frame", _style["dialog"]), + _style.get("dialog-frame", _style["dialog"])) + + # Draw title bar with blue background if title provided + if title: + # Fill entire top line with blue background + win.attron(_style.get("dialog-frame", _style["dialog"])) + for i in range(1, win_width - 1): + _safe_addch(win, 0, i, ord(' ')) + # Draw title text centered + title_x = (win_width - len(title)) // 2 + _safe_addstr(win, 0, title_x, title) + win.attroff(_style.get("dialog-frame", _style["dialog"])) + + # Draw horizontal separator line before buttons (height - 3) + # This line should have blue background + win.attron(_style.get("dialog-frame", _style["dialog"])) + _safe_addch(win, win_height - 3, 0, curses.ACS_LTEE) + for i in range(1, win_width - 1): + _safe_addch(win, win_height - 3, i, curses.ACS_HLINE) + _safe_addch(win, win_height - 3, win_width - 1, curses.ACS_RTEE) + win.attroff(_style.get("dialog-frame", _style["dialog"])) + + # Draw text content with blue background (dialog-frame style) + # Fill text area with blue background + win.attron(_style.get("dialog-frame", _style["dialog"])) + for i in range(1, win_height - 3): + for j in range(1, win_width - 1): + _safe_addch(win, i, j, ord(' ')) + # Draw text lines + for i, line in enumerate(lines): + if i < len(lines): + # Text starts at row 1, column 2 (inside border) + _safe_addstr(win, 1 + i, 2, line) + win.attroff(_style.get("dialog-frame", _style["dialog"])) + + # Buttons at row (height - 2) + button_y = win_height - 2 + + # Fill button row with blue background (dialog-frame style) + win.attrset(_style.get("dialog-frame", _style["dialog"])) + for i in range(1, win_width - 1): + _safe_addch(win, button_y, i, ord(' ')) + + # Calculate button positions with spacing + # For Yes/No: 13 chars spacing (from lxdialog/yesno.c) + # For 3+ buttons: smaller spacing for better fit + if len(buttons) == 2: + # Two buttons: use fixed spacing of 13 + spacing = 13 + total_width = (len(buttons[0]) + 2) + spacing + (len(buttons[1]) + 2) + button_x = (win_width - total_width) // 2 + button_positions = [ + button_x, + button_x + len(buttons[0]) + 2 + spacing + ] + else: + # Three or more buttons: use smaller spacing + spacing = 4 + total_width = sum(len(b) + 2 for b in buttons) + spacing * (len(buttons) - 1) + button_x = (win_width - total_width) // 2 + button_positions = [] + current_x = button_x + for i, b in enumerate(buttons): + button_positions.append(current_x) + current_x += len(b) + 2 + spacing # button width + spacing + + # Draw buttons at calculated positions + for i, button_label in enumerate(buttons): + _print_button(win, button_label, button_y, button_positions[i], i == selected_button) + + win.noutrefresh() + curses.doupdate() + + # Handle input + c = _getch_compat(win) + + if c == curses.KEY_RESIZE: + _resize_main() + # Recalculate window size + win.resize(win_height, win_width) + win.mvwin((_height(_stdscr) - win_height)//2, + (_width(_stdscr) - win_width)//2) + + elif c == "\x1B": # ESC + return None + + elif c == "\t" or c == curses.KEY_RIGHT: # TAB or RIGHT arrow + selected_button = (selected_button + 1) % len(buttons) + + elif c == curses.KEY_LEFT: # LEFT arrow + selected_button = (selected_button - 1) % len(buttons) + + elif c == " " or c == "\n": # SPACE or ENTER + return selected_button + + elif isinstance(c, str): + # Check for hotkey match + c_lower = c.lower() + for i, button_label in enumerate(buttons): + if button_label.strip().lower().startswith(c_lower): + return i + + +def _print_button(win, label, y, x, selected): + # Print a button matching lxdialog's print_button() + # + # Format: