| /* |
| * Copyright (C) 2024 Michael Brown <mbrown@fensystems.co.uk>. |
| * |
| * This program is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU General Public License as |
| * published by the Free Software Foundation; either version 2 of the |
| * License, or any later version. |
| * |
| * This program is distributed in the hope that it will be useful, but |
| * WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| * 02110-1301, USA. |
| * |
| * You can also choose to distribute this program under the terms of |
| * the Unmodified Binary Distribution Licence (as given in the file |
| * COPYING.UBDL), provided that you have satisfied its requirements. |
| */ |
| |
| FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); |
| |
| /** @file |
| * |
| * Text widget forms |
| * |
| */ |
| |
| #include <stdlib.h> |
| #include <string.h> |
| #include <errno.h> |
| #include <ipxe/ansicol.h> |
| #include <ipxe/dynui.h> |
| #include <ipxe/jumpscroll.h> |
| #include <ipxe/settings.h> |
| #include <ipxe/editbox.h> |
| #include <ipxe/message.h> |
| |
| /** Form title row */ |
| #define TITLE_ROW 1U |
| |
| /** Starting control row */ |
| #define START_ROW 3U |
| |
| /** Ending control row */ |
| #define END_ROW ( LINES - 3U ) |
| |
| /** Instructions row */ |
| #define INSTRUCTION_ROW ( LINES - 2U ) |
| |
| /** Padding between instructions */ |
| #define INSTRUCTION_PAD " " |
| |
| /** Input field width */ |
| #define INPUT_WIDTH ( COLS / 2U ) |
| |
| /** Input field column */ |
| #define INPUT_COL ( ( COLS - INPUT_WIDTH ) / 2U ) |
| |
| /** A form */ |
| struct form { |
| /** Dynamic user interface */ |
| struct dynamic_ui *dynui; |
| /** Jump scroller */ |
| struct jump_scroller scroll; |
| /** Array of form controls */ |
| struct form_control *controls; |
| }; |
| |
| /** A form control */ |
| struct form_control { |
| /** Dynamic user interface item */ |
| struct dynamic_item *item; |
| /** Settings block */ |
| struct settings *settings; |
| /** Setting */ |
| struct setting setting; |
| /** Label row */ |
| unsigned int row; |
| /** Editable text box */ |
| struct edit_box editbox; |
| /** Modifiable setting name */ |
| char *name; |
| /** Modifiable setting value */ |
| char *value; |
| /** Most recent error in saving */ |
| int rc; |
| }; |
| |
| /** |
| * Allocate form |
| * |
| * @v dynui Dynamic user interface |
| * @ret form Form, or NULL on error |
| */ |
| static struct form * alloc_form ( struct dynamic_ui *dynui ) { |
| struct form *form; |
| struct form_control *control; |
| struct dynamic_item *item; |
| char *name; |
| size_t len; |
| |
| /* Calculate total length */ |
| len = sizeof ( *form ); |
| list_for_each_entry ( item, &dynui->items, list ) { |
| len += sizeof ( *control ); |
| if ( item->name ) |
| len += ( strlen ( item->name ) + 1 /* NUL */ ); |
| } |
| |
| /* Allocate and initialise structure */ |
| form = zalloc ( len ); |
| if ( ! form ) |
| return NULL; |
| control = ( ( ( void * ) form ) + sizeof ( *form ) ); |
| name = ( ( ( void * ) control ) + |
| ( dynui->count * sizeof ( *control ) ) ); |
| form->dynui = dynui; |
| form->controls = control; |
| list_for_each_entry ( item, &dynui->items, list ) { |
| control->item = item; |
| if ( item->name ) { |
| control->name = name; |
| name = ( stpcpy ( name, item->name ) + 1 /* NUL */ ); |
| } |
| control++; |
| } |
| assert ( ( ( void * ) name ) == ( ( ( void * ) form ) + len ) ); |
| |
| return form; |
| } |
| |
| /** |
| * Free form |
| * |
| * @v form Form |
| */ |
| static void free_form ( struct form *form ) { |
| unsigned int i; |
| |
| /* Free input value buffers */ |
| for ( i = 0 ; i < form->dynui->count ; i++ ) |
| free ( form->controls[i].value ); |
| |
| /* Free form */ |
| free ( form ); |
| } |
| |
| /** |
| * Assign form rows |
| * |
| * @v form Form |
| * @ret rc Return status code |
| */ |
| static int layout_form ( struct form *form ) { |
| struct form_control *control; |
| struct dynamic_item *item; |
| unsigned int labels = 0; |
| unsigned int inputs = 0; |
| unsigned int pad_control = 0; |
| unsigned int pad_label = 0; |
| unsigned int minimum; |
| unsigned int remaining; |
| unsigned int between; |
| unsigned int row; |
| unsigned int flags; |
| unsigned int i; |
| |
| /* Count labels and inputs */ |
| for ( i = 0 ; i < form->dynui->count ; i++ ) { |
| control = &form->controls[i]; |
| item = control->item; |
| if ( item->text[0] ) |
| labels++; |
| if ( item->name ) { |
| if ( ! inputs ) |
| form->scroll.current = i; |
| inputs++; |
| if ( item->flags & DYNUI_DEFAULT ) |
| form->scroll.current = i; |
| form->scroll.count = ( i + 1 ); |
| } |
| } |
| form->scroll.rows = form->scroll.count; |
| DBGC ( form, "FORM %p has %d controls (%d labels, %d inputs)\n", |
| form, form->dynui->count, labels, inputs ); |
| |
| /* Refuse to create forms with no inputs */ |
| if ( ! inputs ) |
| return -EINVAL; |
| |
| /* Calculate minimum number of rows */ |
| minimum = ( labels + ( inputs * 2 /* edit box and error message */ ) ); |
| remaining = ( END_ROW - START_ROW ); |
| DBGC ( form, "FORM %p has %d (of %d) usable rows\n", |
| form, remaining, LINES ); |
| if ( minimum > remaining ) |
| return -ERANGE; |
| remaining -= minimum; |
| |
| /* Insert blank row between controls, if space exists */ |
| between = ( form->dynui->count - 1 ); |
| if ( between <= remaining ) { |
| pad_control = 1; |
| remaining -= between; |
| DBGC ( form, "FORM %p padding between controls\n", form ); |
| } |
| |
| /* Insert blank row after label, if space exists */ |
| if ( labels <= remaining ) { |
| pad_label = 1; |
| remaining -= labels; |
| DBGC ( form, "FORM %p padding after labels\n", form ); |
| } |
| |
| /* Centre on screen */ |
| DBGC ( form, "FORM %p has %d spare rows\n", form, remaining ); |
| row = ( START_ROW + ( remaining / 2 ) ); |
| |
| /* Position each control */ |
| for ( i = 0 ; i < form->dynui->count ; i++ ) { |
| control = &form->controls[i]; |
| item = control->item; |
| if ( item->text[0] ) { |
| control->row = row; |
| row++; /* Label text */ |
| row += pad_label; |
| } |
| if ( item->name ) { |
| flags = ( ( item->flags & DYNUI_SECRET ) ? |
| WIDGET_SECRET : 0 ); |
| init_editbox ( &control->editbox, row, INPUT_COL, |
| INPUT_WIDTH, flags, &control->value ); |
| row++; /* Edit box */ |
| row++; /* Error message (if any) */ |
| } |
| row += pad_control; |
| } |
| assert ( row <= END_ROW ); |
| |
| return 0; |
| } |
| |
| /** |
| * Draw form |
| * |
| * @v form Form |
| */ |
| static void draw_form ( struct form *form ) { |
| struct form_control *control; |
| unsigned int i; |
| |
| /* Clear screen */ |
| color_set ( CPAIR_NORMAL, NULL ); |
| erase(); |
| |
| /* Draw title, if any */ |
| attron ( A_BOLD ); |
| if ( form->dynui->title ) |
| msg ( TITLE_ROW, "%s", form->dynui->title ); |
| attroff ( A_BOLD ); |
| |
| /* Draw controls */ |
| for ( i = 0 ; i < form->dynui->count ; i++ ) { |
| control = &form->controls[i]; |
| |
| /* Draw label, if any */ |
| if ( control->row ) |
| msg ( control->row, "%s", control->item->text ); |
| |
| /* Draw input, if any */ |
| if ( control->name ) |
| draw_widget ( &control->editbox.widget ); |
| } |
| |
| /* Draw instructions */ |
| msg ( INSTRUCTION_ROW, "%s", "Ctrl-X - save changes" |
| INSTRUCTION_PAD "Ctrl-C - discard changes" ); |
| } |
| |
| /** |
| * Draw (or clear) error messages |
| * |
| * @v form Form |
| */ |
| static void draw_errors ( struct form *form ) { |
| struct form_control *control; |
| unsigned int row; |
| unsigned int i; |
| |
| /* Draw (or clear) errors */ |
| for ( i = 0 ; i < form->dynui->count ; i++ ) { |
| control = &form->controls[i]; |
| |
| /* Skip non-input controls */ |
| if ( ! control->name ) |
| continue; |
| |
| /* Draw or clear error message as appropriate */ |
| row = ( control->editbox.widget.row + 1 ); |
| if ( control->rc != 0 ) { |
| color_set ( CPAIR_ALERT, NULL ); |
| msg ( row, " %s ", strerror ( control->rc ) ); |
| color_set ( CPAIR_NORMAL, NULL ); |
| } else { |
| clearmsg ( row ); |
| } |
| } |
| } |
| |
| /** |
| * Parse setting names |
| * |
| * @v form Form |
| * @ret rc Return status code |
| */ |
| static int parse_names ( struct form *form ) { |
| struct form_control *control; |
| unsigned int i; |
| int rc; |
| |
| /* Parse all setting names */ |
| for ( i = 0 ; i < form->dynui->count ; i++ ) { |
| control = &form->controls[i]; |
| |
| /* Skip labels */ |
| if ( ! control->name ) { |
| DBGC ( form, "FORM %p item %d is a label\n", form, i ); |
| continue; |
| } |
| |
| /* Parse setting name */ |
| DBGC ( form, "FORM %p item %d is for %s\n", |
| form, i, control->name ); |
| if ( ( rc = parse_setting_name ( control->name, |
| autovivify_child_settings, |
| &control->settings, |
| &control->setting ) ) != 0 ) |
| return rc; |
| |
| /* Apply default type if necessary */ |
| if ( ! control->setting.type ) |
| control->setting.type = &setting_type_string; |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * Load current input values |
| * |
| * @v form Form |
| */ |
| static void load_values ( struct form *form ) { |
| struct form_control *control; |
| unsigned int i; |
| |
| /* Fetch all current setting values */ |
| for ( i = 0 ; i < form->dynui->count ; i++ ) { |
| control = &form->controls[i]; |
| if ( ! control->name ) |
| continue; |
| fetchf_setting_copy ( control->settings, &control->setting, |
| NULL, &control->setting, |
| &control->value ); |
| } |
| } |
| |
| /** |
| * Store current input values |
| * |
| * @v form Form |
| * @ret rc Return status code |
| */ |
| static int save_values ( struct form *form ) { |
| struct form_control *control; |
| unsigned int i; |
| int rc = 0; |
| |
| /* Store all current setting values */ |
| for ( i = 0 ; i < form->dynui->count ; i++ ) { |
| control = &form->controls[i]; |
| if ( ! control->name ) |
| continue; |
| control->rc = storef_setting ( control->settings, |
| &control->setting, |
| control->value ); |
| if ( control->rc != 0 ) |
| rc = control->rc; |
| } |
| |
| return rc; |
| } |
| |
| /** |
| * Submit form |
| * |
| * @v form Form |
| * @ret rc Return status code |
| */ |
| static int submit_form ( struct form *form ) { |
| int rc; |
| |
| /* Attempt to save values */ |
| rc = save_values ( form ); |
| |
| /* Draw (or clear) errors */ |
| draw_errors ( form ); |
| |
| return rc; |
| } |
| |
| /** |
| * Form main loop |
| * |
| * @v form Form |
| * @ret rc Return status code |
| */ |
| static int form_loop ( struct form *form ) { |
| struct jump_scroller *scroll = &form->scroll; |
| struct form_control *control; |
| struct dynamic_item *item; |
| unsigned int move; |
| unsigned int i; |
| int key; |
| int rc; |
| |
| /* Main loop */ |
| while ( 1 ) { |
| |
| /* Draw current input */ |
| control = &form->controls[scroll->current]; |
| draw_widget ( &control->editbox.widget ); |
| |
| /* Process keypress */ |
| key = edit_widget ( &control->editbox.widget, getkey ( 0 ) ); |
| |
| /* Handle scroll keys */ |
| move = jump_scroll_key ( &form->scroll, key ); |
| |
| /* Handle special keys */ |
| switch ( key ) { |
| case CTRL_C: |
| case ESC: |
| /* Cancel form */ |
| return -ECANCELED; |
| case KEY_ENTER: |
| /* Attempt to do the most intuitive thing when |
| * Enter is pressed. If we are on the last |
| * input, then submit the form. If we are |
| * editing an input which failed, then |
| * resubmit the form. Otherwise, move to the |
| * next input. |
| */ |
| if ( ( control->rc == 0 ) && |
| ( scroll->current < ( scroll->count - 1 ) ) ) { |
| move = SCROLL_DOWN; |
| break; |
| } |
| /* fall through */ |
| case CTRL_X: |
| /* Submit form */ |
| if ( ( rc = submit_form ( form ) ) == 0 ) |
| return 0; |
| /* If current input is not the problem, move |
| * to the first input that needs fixing. |
| */ |
| if ( control->rc == 0 ) { |
| for ( i = 0 ; i < form->dynui->count ; i++ ) { |
| if ( form->controls[i].rc != 0 ) { |
| scroll->current = i; |
| break; |
| } |
| } |
| } |
| break; |
| default: |
| /* Move to input with matching shortcut key, if any */ |
| item = dynui_shortcut ( form->dynui, key ); |
| if ( item ) { |
| scroll->current = item->index; |
| if ( ! item->name ) |
| move = SCROLL_DOWN; |
| } |
| break; |
| } |
| |
| /* Move selection, if applicable */ |
| while ( move ) { |
| move = jump_scroll_move ( &form->scroll, move ); |
| control = &form->controls[scroll->current]; |
| if ( control->name ) |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Show form |
| * |
| * @v dynui Dynamic user interface |
| * @ret rc Return status code |
| */ |
| int show_form ( struct dynamic_ui *dynui ) { |
| struct form *form; |
| int rc; |
| |
| /* Allocate and initialise structure */ |
| form = alloc_form ( dynui ); |
| if ( ! form ) { |
| rc = -ENOMEM; |
| goto err_alloc; |
| } |
| |
| /* Parse setting names and load current values */ |
| if ( ( rc = parse_names ( form ) ) != 0 ) |
| goto err_parse_names; |
| load_values ( form ); |
| |
| /* Lay out form on screen */ |
| if ( ( rc = layout_form ( form ) ) != 0 ) |
| goto err_layout; |
| |
| /* Draw initial form */ |
| initscr(); |
| start_color(); |
| draw_form ( form ); |
| |
| /* Run main loop */ |
| if ( ( rc = form_loop ( form ) ) != 0 ) |
| goto err_loop; |
| |
| err_loop: |
| color_set ( CPAIR_NORMAL, NULL ); |
| endwin(); |
| err_layout: |
| err_parse_names: |
| free_form ( form ); |
| err_alloc: |
| return rc; |
| } |