/* * Copyright (C) 2024 Michael Brown . * * 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 #include #include #include #include #include #include #include #include /** 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; }