blob: 6cc28c369fcb3ba870ea02ee14be873492feb6a9 [file] [log] [blame]
/*
* 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;
}