diff --git a/menuconfig.py b/menuconfig.py index 77c57cd..23cb44f 100755 --- a/menuconfig.py +++ b/menuconfig.py @@ -286,18 +286,27 @@ # Precisely matches lxdialog's set_bluetitle_theme() and set_classic_theme() "linux": """ path=fg:white,bg:blue,bold - separator=fg:black,bg:white + separator=fg:white,bg:blue list=fg:black,bg:white selection=fg:white,bg:blue,bold inv-list=fg:red,bg:white inv-selection=fg:red,bg:blue,bold - help=fg:black,bg:white + help=fg:white,bg:blue show-help=fg:black,bg:white frame=fg:white,bg:blue,bold body=fg:black,bg:white 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(): @@ -989,6 +999,10 @@ def _init(): _init_styles() + # Set stdscr background to match main list style + # This ensures areas not covered by subwindows have correct background + _stdscr.bkgd(' ', _style.get("list", 0)) + # Hide the cursor _safe_curs_set(0) @@ -1034,17 +1048,24 @@ def _resize_main(): screen_height, screen_width = _stdscr.getmaxyx() - _path_win.resize(1, screen_width) - _top_sep_win.resize(1, screen_width) - _bot_sep_win.resize(1, screen_width) - help_win_height = _SHOW_HELP_HEIGHT if _show_help else \ len(_MAIN_HELP_LINES) + # Screen layout: + # Row 0: _path_win, Row 1: _top_sep_win, Row 2+: _menu_win + # Row (2+menu_win_height): _bot_sep_win + # Row (2+menu_win_height+1): _help_win + # Total: 1 + 1 + menu_win_height + 1 + help_win_height = screen_height menu_win_height = screen_height - help_win_height - 3 + menu_win_width = screen_width + + _path_win.resize(1, screen_width) + _top_sep_win.resize(1, menu_win_width) if menu_win_height >= 1: - _menu_win.resize(menu_win_height, screen_width) + _menu_win.resize(menu_win_height, menu_win_width) + _bot_sep_win.resize(1, menu_win_width) + # _help_win uses full screen width for blue background to extend to right edge _help_win.resize(help_win_height, screen_width) _top_sep_win.mvwin(1, 0) @@ -1353,8 +1374,10 @@ def _draw_main(): _safe_hline(_top_sep_win, 0, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS) # Add the 'mainmenu' text as the title, centered at the top + # Use _top_sep_win width instead of term_width for correct centering + top_sep_width = _width(_top_sep_win) _safe_addstr(_top_sep_win, - 0, max((term_width - len(_kconf.mainmenu_text))//2, 0), + 0, max((top_sep_width - len(_kconf.mainmenu_text))//2, 0), _kconf.mainmenu_text) _top_sep_win.noutrefresh() @@ -1367,10 +1390,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 +1412,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() @@ -1735,12 +1765,22 @@ def edit_width(): # Horizontal scroll offset hscroll = max(i - edit_width() + 1, 0) + # Create shadow windows once + win_y, win_x = win.getbegyx() + win_height, win_width = win.getmaxyx() + bottom_shadow, right_shadow = _create_shadow_windows(win_y, win_x, win_height, win_width) + while True: # Draw the "main" display with the menu, etc., so that resizing still # works properly. This is like a stack of windows, only hardcoded for # now. _draw_main() + _draw_input_dialog(win, title, info_lines, s, i, hscroll) + + # Refresh shadow windows after dialog window to ensure they're on top + _refresh_shadow_windows(bottom_shadow, right_shadow) + curses.doupdate() @@ -1750,6 +1790,10 @@ def edit_width(): # Resize the main display too. The dialog floats above it. _resize_main() _resize_input_dialog(win, title, info_lines) + # Recreate shadow windows with new dialog size + win_y, win_x = win.getbegyx() + win_height, win_width = win.getmaxyx() + bottom_shadow, right_shadow = _create_shadow_windows(win_y, win_x, win_height, win_width) elif c == "\n": _safe_curs_set(0) @@ -1944,10 +1988,20 @@ def _key_dialog(title, text, keys): _resize_key_dialog(win, text) + # Create shadow windows once + win_y, win_x = win.getbegyx() + win_height, win_width = win.getmaxyx() + bottom_shadow, right_shadow = _create_shadow_windows(win_y, win_x, win_height, win_width) + while True: # See _input_dialog() _draw_main() + _draw_key_dialog(win, title, text) + + # Refresh shadow windows after dialog window to ensure they're on top + _refresh_shadow_windows(bottom_shadow, right_shadow) + curses.doupdate() @@ -1957,6 +2011,10 @@ def _key_dialog(title, text, keys): # Resize the main display too. The dialog floats above it. _resize_main() _resize_key_dialog(win, text) + # Recreate shadow windows with new dialog size + win_y, win_x = win.getbegyx() + win_height, win_width = win.getmaxyx() + bottom_shadow, right_shadow = _create_shadow_windows(win_y, win_x, win_height, win_width) elif c == "\x1B": # \x1B = ESC return None @@ -1985,33 +2043,329 @@ def _resize_key_dialog(win, text): def _draw_key_dialog(win, title, text): 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 _draw_frame(win, title): - # Draw a frame around the inner edges of 'win', with 'title' at the top +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) + # Create shadow windows once + win_y, win_x = win.getbegyx() win_height, win_width = win.getmaxyx() + bottom_shadow, right_shadow = _create_shadow_windows(win_y, win_x, win_height, win_width) - win.attron(_style["frame"]) + while True: + # Draw main display behind dialog (calls noutrefresh on all subwindows) + _draw_main() + + 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 top/bottom edge - _safe_hline(win, 0, 0, " ", win_width) - _safe_hline(win, win_height - 1, 0, " ", win_width) + # 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) - # Draw left/right edge - _safe_vline(win, 0, 0, " ", win_height) - _safe_vline(win, 0, win_width - 1, " ", win_height) + win.noutrefresh() + + # Refresh shadow windows after dialog window to ensure they're on top + _refresh_shadow_windows(bottom_shadow, right_shadow) + + 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) + # Recreate shadow windows with new dialog size + win_y, win_x = win.getbegyx() + win_height, win_width = win.getmaxyx() + bottom_shadow, right_shadow = _create_shadow_windows(win_y, win_x, win_height, win_width) + + 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: