diff --git a/CHANGELOG b/CHANGELOG index a56d599..03426d7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +Unreleased +- Add Rows and RowSpacing settings to display multiple rows of entries on a single page +- Add :up and :down special commands, and Up/Down arrow key navigation when Rows is greater than 1 + v2.2 (2024-11-24) - Make application timeout configurable - Add WrapEntries feature diff --git a/config/config.ini.in b/config/config.ini.in index 3cc8b55..c0b619a 100644 --- a/config/config.ini.in +++ b/config/config.ini.in @@ -27,8 +27,10 @@ [Layout] @SETTING_MAX_BUTTONS@=@DEFAULT_MAX_BUTTONS@ +@SETTING_ROWS@=@DEFAULT_ROWS@ @SETTING_ICON_SIZE@=@DEFAULT_ICON_SIZE@ @SETTING_ICON_SPACING@=@DEFAULT_ICON_SPACING@ +@SETTING_ROW_SPACING@=@DEFAULT_ROW_SPACING@ @SETTING_VCENTER@=@DEFAULT_VCENTER@ [Titles] @@ -91,8 +93,8 @@ Hotkey1=#1B;:quit #@SETTING_GAMEPAD_MAPPINGS_FILE@= @SETTING_GAMEPAD_LSTICK_XM@=:left @SETTING_GAMEPAD_LSTICK_XP@=:right -#@SETTING_GAMEPAD_LSTICK_YM@= -#@SETTING_GAMEPAD_LSTICK_YP@= +@SETTING_GAMEPAD_LSTICK_YM@=:up +@SETTING_GAMEPAD_LSTICK_YP@=:down #@SETTING_GAMEPAD_RSTICK_XM@= #@SETTING_GAMEPAD_RSTICK_XP@= #@SETTING_GAMEPAD_RSTICK_YM@= @@ -110,8 +112,8 @@ Hotkey1=#1B;:quit #@SETTING_GAMEPAD_BUTTON_RIGHT_STICK@= #@SETTING_GAMEPAD_BUTTON_LEFT_SHOULDER@= #@SETTING_GAMEPAD_BUTTON_RIGHT_SHOULDER@= -#@SETTING_GAMEPAD_BUTTON_DPAD_UP@= -#@SETTING_GAMEPAD_BUTTON_DPAD_DOWN@= +@SETTING_GAMEPAD_BUTTON_DPAD_UP@=:up +@SETTING_GAMEPAD_BUTTON_DPAD_DOWN@=:down @SETTING_GAMEPAD_BUTTON_DPAD_LEFT@=:left @SETTING_GAMEPAD_BUTTON_DPAD_RIGHT@=:right diff --git a/config/config_settings.cmake b/config/config_settings.cmake index 13f22df..838bce7 100644 --- a/config/config_settings.cmake +++ b/config/config_settings.cmake @@ -1,6 +1,7 @@ # Config setting keys set(SETTING_DEFAULT_MENU "DefaultMenu") set(SETTING_MAX_BUTTONS "MaxButtons") +set(SETTING_ROWS "Rows") set(SETTING_VSYNC "VSync") set(SETTING_FPS_LIMIT "FPSLimit") set(SETTING_APPLICATION_TIMEOUT "ApplicationTimeout") @@ -17,6 +18,7 @@ set(SETTING_BACKGROUND_OVERLAY_COLOR "OverlayColor") set(SETTING_BACKGROUND_OVERLAY_OPACITY "OverlayOpacity") set(SETTING_ICON_SIZE "IconSize") set(SETTING_ICON_SPACING "IconSpacing") +set(SETTING_ROW_SPACING "RowSpacing") set(SETTING_TITLES_ENABLED "Enabled") set(SETTING_TITLE_FONT "Font") set(SETTING_TITLE_FONT_SIZE "FontSize") @@ -96,6 +98,7 @@ set(SETTING_GAMEPAD_BUTTON_DPAD_RIGHT "ButtonDPadRight") # Default settings set(DEFAULT_MENU "Main") set(DEFAULT_MAX_BUTTONS 4) +set(DEFAULT_ROWS 1) set(DEFAULT_VSYNC "true") set(DEFAULT_APPLICATION_TIMEOUT "15") set(DEFAULT_WRAP_ENTRIES "false") @@ -119,6 +122,7 @@ set(DEFAULT_BACKGROUND_OVERLAY_COLOR_A "7F") set(DEFAULT_BACKGROUND_OVERLAY_OPACITY "50%") set(DEFAULT_ICON_SIZE 256) set(DEFAULT_ICON_SPACING "5%") +set(DEFAULT_ROW_SPACING "6%") set(DEFAULT_FONT "OpenSans-Regular.ttf") set(DEFAULT_FONT_SIZE 36) set(DEFAULT_TITLES_ENABLED "true") diff --git a/config/launcher_config.h.in b/config/launcher_config.h.in index 5ee6bb2..d5ad9d1 100644 --- a/config/launcher_config.h.in +++ b/config/launcher_config.h.in @@ -33,6 +33,7 @@ // Config file setting names #define SETTING_DEFAULT_MENU "@SETTING_DEFAULT_MENU@" #define SETTING_MAX_BUTTONS "@SETTING_MAX_BUTTONS@" +#define SETTING_ROWS "@SETTING_ROWS@" #define SETTING_VSYNC "@SETTING_VSYNC@" #define SETTING_FPS_LIMIT "@SETTING_FPS_LIMIT@" #define SETTING_APPLICATION_TIMEOUT "@SETTING_APPLICATION_TIMEOUT@" @@ -50,6 +51,7 @@ #define SETTING_BACKGROUND_OVERLAY_OPACITY "@SETTING_BACKGROUND_OVERLAY_OPACITY@" #define SETTING_ICON_SIZE "@SETTING_ICON_SIZE@" #define SETTING_ICON_SPACING "@SETTING_ICON_SPACING@" +#define SETTING_ROW_SPACING "@SETTING_ROW_SPACING@" #define SETTING_TITLES_ENABLED "@SETTING_TITLES_ENABLED@" #define SETTING_TITLE_FONT "@SETTING_TITLE_FONT@" #define SETTING_TITLE_FONT_SIZE "@SETTING_TITLE_FONT_SIZE@" @@ -127,6 +129,7 @@ // Config file default settings #define DEFAULT_MAX_BUTTONS @DEFAULT_MAX_BUTTONS@ +#define DEFAULT_ROWS @DEFAULT_ROWS@ #define DEFAULT_VSYNC @DEFAULT_VSYNC@ #define DEFAULT_APPLICATION_TIMEOUT @DEFAULT_APPLICATION_TIMEOUT@ #define DEFAULT_WRAP_ENTRIES @DEFAULT_WRAP_ENTRIES@ @@ -147,6 +150,7 @@ #define DEFAULT_BACKGROUND_OVERLAY_OPACITY "@DEFAULT_BACKGROUND_OVERLAY_OPACITY@" #define DEFAULT_ICON_SIZE @DEFAULT_ICON_SIZE@ #define DEFAULT_ICON_SPACING "@DEFAULT_ICON_SPACING@" +#define DEFAULT_ROW_SPACING "@DEFAULT_ROW_SPACING@" #define DEFAULT_FONT_SIZE @DEFAULT_FONT_SIZE@ #define DEFAULT_TITLES_ENABLED @DEFAULT_TITLES_ENABLED@ #define DEFAULT_TITLE_FONT_COLOR_R 0x@DEFAULT_TITLE_FONT_COLOR_R@ diff --git a/docs/configuration.md b/docs/configuration.md index 1da2a67..c7b7ec2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -179,15 +179,22 @@ Default: 50% The settings in this section define the geometric layout of the launcher. - [MaxButtons](#maxbuttons) +- [Rows](#rows) - [IconSize](#iconsize) - [IconSpacing](#iconspacing) +- [RowSpacing](#rowspacing) - [VCenter](#vcenter) ##### MaxButtons -The maximum number of buttons that can be displayed on the screen. If a menu has more entries than this value, it will be split into multiple pages. A value of 3-5 is sensible for a typical TV size and viewing distance. +The maximum number of buttons that can be displayed in a single row. If a menu has more entries than fit on a page (`MaxButtons` x `Rows`), it will be split into multiple pages. A value of 3-5 is sensible for a typical TV size and viewing distance. Default: 4 +##### Rows +The number of rows of buttons that are displayed on a page, from 1 to 3. Entries fill the page row by row. A page holds up to `MaxButtons` x `Rows` entries. A menu with fewer entries than fit in one row is still displayed as a single row. The Up and Down keys (and the [:up and :down](#up) special commands) move between rows, but only when `Rows` is greater than 1, so they remain available as [hotkeys](#hotkeys) with the default value. If the rows do not fit on the screen, `RowSpacing` and the highlight vertical padding are reduced, and then the number of rows, until they do. + +Default: 1 + ##### IconSize The width and height of icons on the screen in pixels. If an icon is not the same resolution, it will be stretched accordingly. @@ -198,8 +205,13 @@ Distance between the menu entry icons, in pixels or percent of the screen width. Default: 5% +##### RowSpacing +When `Rows` is greater than 1, this defines the distance between the rows (from the bottom of one row's title to the top of the next row's icons), in pixels or percent of the screen height. + +Default: 6% + ##### VCenter -Defines the vertical centering of the menu entries in percent of the screen height. A value of 50% will cause the buttons to be centered halfway in the screen. Increasing the value will lower the buttons, and lowering it will raise them. +Defines the vertical centering of the menu entries in percent of the screen height. With multiple rows, the whole grid of rows is centered on this line. A value of 50% will cause the buttons to be centered halfway in the screen. Increasing the value will lower the buttons, and lowering it will raise them. Default: 50% @@ -416,7 +428,13 @@ Quit Flex Launcher. Move the highlight cursor left. #### :right -Move the highlight cursor right. +Move the highlight cursor right. With multiple rows, moving right from the last entry of a row moves to the first entry of the next row, and moving right from the last entry of a page loads the next page. + +#### :up +Move the highlight cursor up one row. From the top row, the previous page is loaded and the cursor moves to the bottom row of that page in the same column. Does nothing if `Rows` is 1. + +#### :down +Move the highlight cursor down one row. From the bottom row, the next page is loaded and the cursor moves to the top row of that page in the same column. If the row below is a partial row that has no entry in the current column, the last entry of the page is selected. Does nothing if `Rows` is 1. #### :select Press enter on the current selection. This special command is only available as a gamepad or hotkey command, it is forbidden for menu entries. diff --git a/src/debug.c b/src/debug.c index 9b43586..c6c9a66 100644 --- a/src/debug.c +++ b/src/debug.c @@ -123,8 +123,10 @@ void debug_settings() log_debug("======================= Layout =========================\n"); DEBUG_INT(SETTING_MAX_BUTTONS, config.max_buttons); + DEBUG_INT(SETTING_ROWS, config.rows); DEBUG_INT(SETTING_ICON_SIZE, config.icon_size); DEBUG_INT(SETTING_ICON_SPACING, config.icon_spacing); + DEBUG_INT(SETTING_ROW_SPACING, config.row_spacing); DEBUG_STR(SETTING_VCENTER, config.vcenter[0] != '\0' ? config.vcenter : "50%"); log_debug(""); diff --git a/src/launcher.c b/src/launcher.c index acc3930..d69a536 100755 --- a/src/launcher.c +++ b/src/launcher.c @@ -30,8 +30,13 @@ static void init_slideshow(void); static void init_screensaver(void); static void calculate_button_geometry(Entry *entry, int buttons); static void render_buttons(Menu *menu); +static void update_highlight(void); +static void load_page(unsigned int page, unsigned int position); +static void load_last_page(unsigned int column); static void move_left(void); static void move_right(void); +static void move_up(void); +static void move_down(void); static void load_submenu(const char *submenu); static void load_back_menu(Menu *menu); static void draw_screen(void); @@ -93,6 +98,7 @@ Config config = { .highlight_rx = DEFAULT_HIGHLIGHT_CORNER_RADIUS, .title_padding = -1, .max_buttons = DEFAULT_MAX_BUTTONS, + .rows = DEFAULT_ROWS, .icon_spacing = -1, .highlight_vpadding = -1, .highlight_hpadding = -1, @@ -101,6 +107,8 @@ Config config = { .highlight_outline_opacity[0] = '\0', .vcenter[0] = '\0', .icon_spacing_str[0] = '\0', + .row_spacing = -1, + .row_spacing_str[0] = '\0', .scroll_indicators = DEFAULT_SCROLL_INDICATORS, .scroll_indicator_fill_color.r = DEFAULT_SCROLL_INDICATOR_FILL_COLOR_R, .scroll_indicator_fill_color.g = DEFAULT_SCROLL_INDICATOR_FILL_COLOR_G, @@ -189,6 +197,12 @@ Uint32 delay_period; Uint32 repeat_period; +// A function to get the number of entries that fit on one page of a menu +static inline unsigned int page_size(void) +{ + return config.rows * config.max_buttons; +} + // A function to initialize SDL static void init_sdl() { @@ -418,6 +432,12 @@ static void handle_keypress(SDL_Keysym *key) move_left(); else if (key->sym == SDLK_RIGHT) move_right(); + + // Up and down are only reserved when there are multiple rows, otherwise they remain available as hotkeys + else if (key->sym == SDLK_UP && config.rows > 1) + move_up(); + else if (key->sym == SDLK_DOWN && config.rows > 1) + move_down(); else if (key->sym == SDLK_RETURN) { log_debug("Selected Entry:\n" "Title: %s\n" @@ -599,16 +619,13 @@ static int load_menu(Menu *menu, bool set_back_menu, bool reset_position) else current_entry = current_menu->last_selected_entry; - buttons = current_menu->num_entries - (current_menu->page)*config.max_buttons; - if (buttons > config.max_buttons) - buttons = config.max_buttons; + buttons = current_menu->num_entries - (current_menu->page)*page_size(); + if (buttons > page_size()) + buttons = page_size(); // Recalculate the screen geometry calculate_button_geometry(current_menu->root_entry, (int) buttons); - if (config.highlight) { - highlight->rect.x = current_entry->icon_rect.x - config.highlight_hpadding; - highlight->rect.y = current_entry->icon_rect.y - config.highlight_vpadding; - } + update_highlight(); return 0; } @@ -619,19 +636,41 @@ static int load_menu_by_name(const char *menu_name, bool set_back_menu, bool res return load_menu(menu, set_back_menu, reset_position); } -// A function to calculate the layout of the buttons +// A function to calculate the layout of the buttons on the current page. +// Entries fill the page row by row, all rows share the same column positions. static void calculate_button_geometry(Entry *entry, int buttons) { + int columns = MIN(buttons, (int) config.max_buttons); + + // A menu that only needs one row is laid out as a single row, even if + // more rows are configured. The number of rows is fixed for the whole + // menu so the buttons don't jump around when changing pages + int rows = MIN((int) config.rows, (int) DIV_ROUND_UP(current_menu->num_entries, config.max_buttons)); + int button_height = config.icon_size + config.title_padding + geo.font_height; + int grid_height = rows*button_height + (rows - 1)*config.row_spacing; + // Calculate proper spacing - geo.x_margin = (geo.screen_width - config.icon_size*buttons - - buttons*config.icon_spacing + config.icon_spacing) / 2; + geo.x_margin = (geo.screen_width - config.icon_size*columns - + columns*config.icon_spacing + config.icon_spacing) / 2; geo.x_advance = config.icon_size + config.icon_spacing; + geo.y_advance = button_height + config.row_spacing; + geo.y_margin = geo.vcenter - grid_height / 2; + if (rows > 1) { + // Keep the whole grid (including highlight) on the screen + int padding = config.highlight && config.highlight_vpadding > 0 ? config.highlight_vpadding : 0; + if (geo.y_margin + grid_height + padding > geo.screen_height) + geo.y_margin = geo.screen_height - grid_height - padding; + if (geo.y_margin < padding) + geo.y_margin = padding; + } geo.num_buttons = buttons; + geo.num_columns = columns; + geo.num_rows = rows; // Assign values to entries for (int i = 0; i < geo.num_buttons; i++) { - entry->icon_rect.x = geo.x_margin + i*geo.x_advance; - entry->icon_rect.y = geo.y_margin; + entry->icon_rect.x = geo.x_margin + (i % columns)*geo.x_advance; + entry->icon_rect.y = geo.y_margin + (i / columns)*geo.y_advance; entry->icon_rect.w = config.icon_size; entry->icon_rect.h = config.icon_size; entry->text_rect.x = entry->icon_rect.x + @@ -642,6 +681,52 @@ static void calculate_button_geometry(Entry *entry, int buttons) } } +// A function to move the highlight to the current entry +static void update_highlight() +{ + if (config.highlight) { + highlight->rect.x = current_entry->icon_rect.x - config.highlight_hpadding; + highlight->rect.y = current_entry->icon_rect.y - config.highlight_vpadding; + } +} + +// A function to show a page of the current menu and select the entry at +// the given position on the page. The position is clamped to the last entry +static void load_page(unsigned int page, unsigned int position) +{ + unsigned int per_page = page_size(); + Entry *root = advance_entries(current_menu->first_entry, (int) (page*per_page), DIRECTION_RIGHT); + unsigned int buttons = current_menu->num_entries - page*per_page; + if (buttons > per_page) + buttons = per_page; + if (position > buttons - 1) + position = buttons - 1; + + current_menu->page = page; + current_menu->root_entry = root; + current_menu->highlight_position = position; + current_entry = advance_entries(root, (int) position, DIRECTION_RIGHT); + calculate_button_geometry(root, (int) buttons); + update_highlight(); +} + +// A function to show the last page of the current menu and select the entry in the +// bottom-most row that is in the given column (or the nearest entry if that column is empty) +static void load_last_page(unsigned int column) +{ + unsigned int per_page = page_size(); + unsigned int num_pages = DIV_ROUND_UP(current_menu->num_entries, per_page); + unsigned int buttons = current_menu->num_entries - (num_pages - 1)*per_page; + unsigned int columns = MIN(buttons, config.max_buttons); + unsigned int last_row = (buttons - 1) / columns; + unsigned int position = last_row*columns + column; + + // Column is missing in the partial last row, use the row above + if (position > buttons - 1 && last_row > 0) + position -= columns; + load_page(num_pages - 1, position); +} + // A function to render all buttons (icon and text) for a menu static void render_buttons(Menu *menu) { @@ -662,81 +747,94 @@ static void render_buttons(Menu *menu) // A function to move the selection left when clicked by user static void move_left() { - // If we are not in leftmost position, move highlight left + // If we are not in the first position of the page, move highlight left. + // With multiple rows, this wraps to the end of the previous row if (current_menu->highlight_position > 0) { - if (config.highlight) - highlight->rect.x -= geo.x_advance; current_menu->highlight_position--; current_entry = current_entry->previous; + update_highlight(); } - // If we are in leftmost position... - else if (current_menu->highlight_position == 0 && (current_menu->page > 0 || config.wrap_entries)) { - unsigned int buttons; - current_entry = current_entry->previous; - - // Load the previous page if there is a valid previous entry - if (current_entry) { - buttons = config.max_buttons; - current_menu->root_entry = advance_entries(current_menu->root_entry, (int) buttons, DIRECTION_LEFT); - current_menu->page--; - } - - // If the user has the wrap entries setting, select the last entry in the menu - else { - current_entry = advance_entries(current_menu->first_entry, (int) current_menu->num_entries - 1, DIRECTION_RIGHT); - unsigned int num_pages = DIV_ROUND_UP(current_menu->num_entries, config.max_buttons); - current_menu->root_entry = advance_entries(current_menu->root_entry, - (int) ((num_pages - 1 - current_menu->page) * config.max_buttons), - DIRECTION_RIGHT - ); - current_menu->page = num_pages - 1; - buttons = current_menu->num_entries - current_menu->page * config.max_buttons; - } + // Load the previous page and select its last entry + else if (current_menu->page > 0) + load_page(current_menu->page - 1, page_size() - 1); - calculate_button_geometry(current_menu->root_entry, (int) buttons); - if (config.highlight) - highlight->rect.x = current_entry->icon_rect.x - config.highlight_hpadding; - current_menu->highlight_position = buttons - 1; - } + // If the user has the wrap entries setting, select the last entry in the menu + else if (config.wrap_entries) + load_page(DIV_ROUND_UP(current_menu->num_entries, page_size()) - 1, page_size() - 1); } // A function to move the selection right when clicked by the user static void move_right() { - // If we are not in the rightmost position, move highlight right + // If we are not in the last position of the page, move highlight right. + // With multiple rows, this wraps to the start of the next row if ((int) current_menu->highlight_position < (geo.num_buttons - 1)) { - if (config.highlight) - highlight->rect.x += geo.x_advance; current_menu->highlight_position++; current_entry = current_entry->next; + update_highlight(); } - // If we are in the rightmost postion, but there are more entries in the menu, load next page - else if (current_menu->highlight_position + current_menu->page*config.max_buttons < - (current_menu->num_entries - 1)) { - unsigned int buttons = current_menu->num_entries - (current_menu->page + 1)*config.max_buttons; - if (buttons > config.max_buttons) - buttons = config.max_buttons; - current_entry = current_entry->next; - current_menu->root_entry = current_entry; - calculate_button_geometry(current_menu->root_entry, (int) buttons); - if (config.highlight) - highlight->rect.x = current_entry->icon_rect.x - config.highlight_hpadding; - current_menu->page++; - current_menu->highlight_position = 0; - } + // If we are in the last postion, but there are more entries in the menu, load next page + else if (current_menu->highlight_position + current_menu->page*page_size() < + (current_menu->num_entries - 1)) + load_page(current_menu->page + 1, 0); // If user has the wrap entries setting, reset menu to first entry - else if (config.wrap_entries) { - current_entry = current_menu->first_entry; - current_menu->root_entry = current_entry; - current_menu->highlight_position = 0; - current_menu->page = 0; - if (config.highlight) - highlight->rect.x = current_entry->icon_rect.x - config.highlight_hpadding; - calculate_button_geometry(current_menu->root_entry, (int) MIN(current_menu->num_entries, config.max_buttons)); + else if (config.wrap_entries) + load_page(0, 0); +} + +// A function to move the selection up one row +static void move_up() +{ + unsigned int columns = (unsigned int) geo.num_columns; + unsigned int position = current_menu->highlight_position; + + // Move up within the page + if (position >= columns) { + current_menu->highlight_position -= columns; + current_entry = advance_entries(current_entry, (int) columns, DIRECTION_LEFT); + update_highlight(); } + + // In the top row: go to the bottom row of the previous page, same column + else if (current_menu->page > 0) + load_page(current_menu->page - 1, page_size() - config.max_buttons + position); + + // If user has the wrap entries setting, go to the bottom of the last page + else if (config.wrap_entries) + load_last_page(position); +} + +// A function to move the selection down one row +static void move_down() +{ + unsigned int columns = (unsigned int) geo.num_columns; + unsigned int position = current_menu->highlight_position; + unsigned int buttons = (unsigned int) geo.num_buttons; + + // Move down within the page + if (position + columns < buttons) { + current_menu->highlight_position += columns; + current_entry = advance_entries(current_entry, (int) columns, DIRECTION_RIGHT); + update_highlight(); + } + + // There is a partial row below with no entry in this column: select its last entry + else if (position / columns < (buttons - 1) / columns) { + current_entry = advance_entries(current_entry, (int) (buttons - 1 - position), DIRECTION_RIGHT); + current_menu->highlight_position = buttons - 1; + update_highlight(); + } + + // In the bottom row: go to the top row of the next page, same column + else if ((current_menu->page + 1)*page_size() < current_menu->num_entries) + load_page(current_menu->page + 1, position % columns); + + // If user has the wrap entries setting, go to the top of the first page + else if (config.wrap_entries) + load_page(0, position % columns); } // A function to load a submenu @@ -770,7 +868,7 @@ static void draw_screen() // Draw scroll indicators if (config.scroll_indicators && - (current_menu->page*config.max_buttons + (unsigned int) geo.num_buttons) <= (current_menu->num_entries - 1)) + (current_menu->page*page_size() + (unsigned int) geo.num_buttons) <= (current_menu->num_entries - 1)) SDL_RenderCopy(renderer, scroll->texture, NULL, &scroll->rect_right); if (config.scroll_indicators && current_menu->page > 0) @@ -842,6 +940,14 @@ static void execute_command(const char *command) move_left(); else if (!strcmp(special_command, SCMD_RIGHT)) move_right(); + else if (!strcmp(special_command, SCMD_UP)) { + if (config.rows > 1) + move_up(); + } + else if (!strcmp(special_command, SCMD_DOWN)) { + if (config.rows > 1) + move_down(); + } else if (!strcmp(special_command, SCMD_SELECT)) execute_command(current_entry->cmd); else if (!strcmp(special_command, SCMD_HOME)) diff --git a/src/launcher.h b/src/launcher.h index 9d18cda..34486b9 100644 --- a/src/launcher.h +++ b/src/launcher.h @@ -16,6 +16,7 @@ #define MIN_FPS_LIMIT 10 #define MIN_ICON_SIZE 32 #define MAX_ICON_SIZE 1024 +#define MAX_ROWS 3 #define MIN_RX_SIZE 0 #define MAX_RX_SIZE 100 #define PERCENT_MAX_CHARS 10 @@ -46,6 +47,8 @@ #define SCMD_EXIT ":exit" #define SCMD_LEFT ":left" #define SCMD_RIGHT ":right" +#define SCMD_UP ":up" +#define SCMD_DOWN ":down" #define SCMD_HOME ":home" #define SCMD_BACK ":back" #define SCMD_QUIT ":quit" @@ -197,9 +200,13 @@ typedef struct { int screen_margin; int font_height; int x_margin; // Distance between left edge of screen and x coordinate of root_entry icon - int y_margin; // Distance between top edge of screen and y coordinate of all entry icons + int y_margin; // Distance between top edge of screen and y coordinate of the first row of entry icons int x_advance; // Distance between icon x coordinate of adjacent entries + int y_advance; // Distance between icon y coordinate of adjacent rows + int vcenter; // Vertical centerline of the button grid, in pixels from the top of the screen int num_buttons; // Number of buttons shown on the screen + int num_columns; // Number of columns on the current page + int num_rows; // Number of rows in the current menu (constant across its pages) } Geometry; // Struct for highlight @@ -238,7 +245,8 @@ typedef struct { // Configuration settings typedef struct { char *default_menu; - unsigned int max_buttons; + unsigned int max_buttons; // Number of buttons per row + unsigned int rows; // Number of rows per page bool vsync; int fps_limit; Uint32 application_timeout; @@ -253,6 +261,8 @@ typedef struct { Uint16 icon_size; int icon_spacing; char icon_spacing_str[PERCENT_MAX_CHARS]; + int row_spacing; + char row_spacing_str[PERCENT_MAX_CHARS]; bool titles_enabled; char *title_font_path; // Path to title TTF font file unsigned int title_font_size; diff --git a/src/util.c b/src/util.c index 1df240b..b28f762 100755 --- a/src/util.c +++ b/src/util.c @@ -168,6 +168,11 @@ int config_handler(void *user, const char *section, const char *name, const char if (max_buttons > 0) config.max_buttons = (unsigned int) max_buttons; } + else if (MATCH(name, SETTING_ROWS)) { + int rows = atoi(value); + if (rows > 0 && rows <= MAX_ROWS) + config.rows = (unsigned int) rows; + } else if (MATCH(name, SETTING_ICON_SIZE)) { Uint16 icon_size = (Uint16) atoi(value); if (icon_size >= MIN_ICON_SIZE && icon_size <= MAX_ICON_SIZE) @@ -182,6 +187,15 @@ int config_handler(void *user, const char *section, const char *name, const char config.icon_spacing = icon_spacing; } } + else if (MATCH(name, SETTING_ROW_SPACING)) { + if (is_percent(value)) + copy_string(config.row_spacing_str, value, sizeof(config.row_spacing_str)); + else { + int row_spacing = atoi(value); + if (row_spacing > 0 || MATCH(value, "0")) + config.row_spacing = row_spacing; + } + } else if (MATCH(name, SETTING_VCENTER)) { if (is_percent(value)) copy_string(config.vcenter, value, sizeof(config.vcenter)); @@ -767,6 +781,12 @@ unsigned int calculate_width(int buttons, int icon_spacing, int icon_size, int h return (unsigned int) ((buttons - 1)*icon_spacing + buttons*icon_size + 2*highlight_hpadding); } +// A function to calculate the total height of a grid of buttons, including highlight padding +int calculate_height(int rows, int button_height, int row_spacing, int highlight_vpadding) +{ + return rows*button_height + (rows - 1)*row_spacing + 2*highlight_vpadding; +} + // A function to add a hotkey to the linked list void add_hotkey(const char *keycode, const char *cmd) { @@ -939,6 +959,16 @@ void validate_settings(Geometry *geo) config.clock_font_color.a = (Uint8) clock_opacity; } + // Set default RowSpacing if none is in the config file + if (config.row_spacing < 0) { + int row_spacing = INVALID_PERCENT_VALUE; + if (config.row_spacing_str[0] != '\0') + convert_percent_to_int(config.row_spacing_str, &row_spacing, geo->screen_height); + if (row_spacing == INVALID_PERCENT_VALUE) + convert_percent_to_int(DEFAULT_ROW_SPACING, &row_spacing, geo->screen_height); + config.row_spacing = row_spacing; + } + // Set default IconSpacing if none is in the config file if (config.icon_spacing < 0) { int icon_spacing = INVALID_PERCENT_VALUE; @@ -1024,7 +1054,50 @@ void validate_settings(Geometry *geo) vcenter = lower_limit; else if (vcenter > upper_limit) vcenter = upper_limit; - geo->y_margin = vcenter - button_height / 2; + geo->vcenter = vcenter; + + // Make sure all of the rows fit on the screen. Drop rows that can't fit even + // without any gaps, then shrink the row spacing/highlight padding to fit the rest + if (config.rows > 1) { + unsigned int rows = config.rows; + while (rows > 1 && (int) rows * button_height > geo->screen_height) + rows--; + if (rows != config.rows) { + log_error("Not enough screen space for %i rows, reducing to %i", config.rows, rows); + config.rows = rows; + } + } + if (config.rows > 1) { + int row_spacing = config.row_spacing; + int vpadding = config.highlight_vpadding > 0 ? config.highlight_vpadding : 0; + + // Reduce highlight vpadding to prevent overlaps between rows + if (vpadding > row_spacing / 2) + vpadding = row_spacing / 2; + int required_height = calculate_height((int) config.rows, button_height, row_spacing, vpadding); + for (int i = 0; i < 100 && required_height > geo->screen_height; i++) { + if (vpadding > 0) + vpadding = (vpadding * 9) / 10; + if (row_spacing > 0) + row_spacing = (row_spacing * 9) / 10; + required_height = calculate_height((int) config.rows, button_height, row_spacing, vpadding); + } + if (config.row_spacing != row_spacing) { + log_error("Row spacing value %i too large to fit screen, shrinking to %i", + config.row_spacing, + row_spacing + ); + config.row_spacing = row_spacing; + } + if (config.highlight_vpadding > 0 && config.highlight_vpadding != vpadding) { + log_error("Highlight vertical padding value %i too large for %i rows, shrinking to %i", + config.highlight_vpadding, + (int) config.rows, + vpadding + ); + config.highlight_vpadding = vpadding; + } + } // Max highlight outline int max_highlight_outline_size = (config.highlight_hpadding < config.highlight_vpadding) diff --git a/src/util.h b/src/util.h index 90dc0cf..9041b2b 100755 --- a/src/util.h +++ b/src/util.h @@ -26,6 +26,7 @@ int config_handler(void *user, const char *section, const char *name, const char int convert_percent(const char *string, int max_value); const char *get_mode_setting(int type, int value); int utf8_length(const char *string); +int calculate_height(int rows, int button_height, int row_spacing, int highlight_vpadding); unsigned int calculate_width(int buttons, int icon_spacing, int icon_size, int highlight_padding); bool hex_to_color(const char *string, SDL_Color *color); bool convert_bool(const char *string, bool *setting);