/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2013 Red Hat, Inc.
*/
/**
* SECTION:nmt-newt-form
* @short_description: The top-level NmtNewt widget
*
* #NmtNewtForm is the top-level widget that contains and presents a
* "form" (aka dialog) to the user.
*/
#include "libnm-client-aux-extern/nm-default-client.h"
#include <fcntl.h>
#include <unistd.h>
#include "nmt-newt-form.h"
#include "nmt-newt-button.h"
#include "nmt-newt-grid.h"
#include "nmt-newt-utils.h"
G_DEFINE_TYPE(NmtNewtForm, nmt_newt_form, NMT_TYPE_NEWT_CONTAINER)
#define NMT_NEWT_FORM_GET_PRIVATE(o) \
(G_TYPE_INSTANCE_GET_PRIVATE((o), NMT_TYPE_NEWT_FORM, NmtNewtFormPrivate))
typedef struct {
newtComponent form;
NmtNewtWidget *content;
guint x, y, width, height;
guint padding;
gboolean fixed_x, fixed_y;
gboolean fixed_width, fixed_height;
char * title_lc;
gboolean dirty, escape_exits;
NmtNewtWidget *focus;
#ifdef HAVE_NEWTFORMGETSCROLLPOSITION
int scroll_position = 0;
#endif
} NmtNewtFormPrivate;
enum {
PROP_0,
PROP_TITLE,
PROP_FULLSCREEN,
PROP_FULLSCREEN_VERTICAL,
PROP_FULLSCREEN_HORIZONTAL,
PROP_X,
PROP_Y,
PROP_WIDTH,
PROP_HEIGHT,
PROP_PADDING,
PROP_ESCAPE_EXITS,
LAST_PROP
};
enum {
QUIT,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = {0};
static void nmt_newt_form_redraw(NmtNewtForm *form);
/**
* nmt_newt_form_new:
* @title: (allow-none): the form title
*
* Creates a new form, which will be shown centered on the screen.
* Compare nmt_newt_form_new_fullscreen(). You can also position a
* form manually by setting its #NmtNewtForm:x and #NmtNewtForm:y
* properties at construct time, and/or by setting
* #NmtNewtForm:fullscreen, #NmtNewtform:fullscreen-horizontal, or
* #NmtNewtForm:fullscreen-vertical.
*
* If @title is NULL, the form will have no title.
*
* Returns: a new #NmtNewtForm
*/
NmtNewtForm *
nmt_newt_form_new(const char *title)
{
return g_object_new(NMT_TYPE_NEWT_FORM, "title", title, NULL);
}
/**
* nmt_newt_form_new_fullscreen:
* @title: (allow-none): the form title
*
* Creates a new fullscreen form. Compare nmt_newt_form_new().
*
* If @title is NULL, the form will have no title.
*
* Returns: a new #NmtNewtForm
*/
NmtNewtForm *
nmt_newt_form_new_fullscreen(const char *title)
{
return g_object_new(NMT_TYPE_NEWT_FORM, "title", title, "fullscreen", TRUE, NULL);
}
static void
nmt_newt_form_init(NmtNewtForm *form)
{
g_object_ref_sink(form);
}
static void
nmt_newt_form_finalize(GObject *object)
{
NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(object);
g_free(priv->title_lc);
g_clear_object(&priv->focus);
G_OBJECT_CLASS(nmt_newt_form_parent_class)->finalize(object);
}
static void
nmt_newt_form_needs_rebuild(NmtNewtWidget *widget)
{
NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(widget);
if (!priv->dirty) {
priv->dirty = TRUE;
nmt_newt_form_redraw(NMT_NEWT_FORM(widget));
}
}
static void
nmt_newt_form_remove(NmtNewtContainer *container, NmtNewtWidget *widget)
{
NmtNewtFormPrivate * priv = NMT_NEWT_FORM_GET_PRIVATE(container);
NmtNewtContainerClass *parent_class = NMT_NEWT_CONTAINER_CLASS(nmt_newt_form_parent_class);
g_return_if_fail(widget == priv->content);
parent_class->remove(container, widget);
priv->content = NULL;
}
/**
* nmt_newt_form_set_content:
* @form: the #NmtNewtForm
* @content: the form's content
*
* Sets @form's content to be @content.
*/
void
nmt_newt_form_set_content(NmtNewtForm *form, NmtNewtWidget *content)
{
NmtNewtFormPrivate * priv = NMT_NEWT_FORM_GET_PRIVATE(form);
NmtNewtContainerClass *parent_class = NMT_NEWT_CONTAINER_CLASS(nmt_newt_form_parent_class);
if (priv->content)
nmt_newt_form_remove(NMT_NEWT_CONTAINER(form), priv->content);
priv->content = content;
if (priv->content)
parent_class->add(NMT_NEWT_CONTAINER(form), content);
}
static void
nmt_newt_form_build(NmtNewtForm *form)
{
NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(form);
int screen_height, screen_width, form_height, form_width;
newtComponent * cos;
int i;
priv->dirty = FALSE;
nmt_newt_widget_realize(NMT_NEWT_WIDGET(form));
nmt_newt_widget_size_request(priv->content, &form_width, &form_height);
newtGetScreenSize(&screen_width, &screen_height);
if (!priv->fixed_width)
priv->width = MIN(form_width + 2 * priv->padding, screen_width - 2);
if (!priv->fixed_height)
priv->height = MIN(form_height + 2 * priv->padding, screen_height - 2);
if (!priv->fixed_x)
priv->x = (screen_width - form_width) / 2;
if (!priv->fixed_y)
priv->y = (screen_height - form_height) / 2;
nmt_newt_widget_size_allocate(priv->content,
priv->padding,
priv->padding,
priv->width - 2 * priv->padding,
priv->height - 2 * priv->padding);
if (priv->height - 2 * priv->padding < form_height) {
newtComponent scroll_bar = newtVerticalScrollbar(priv->width - 1,
0,
priv->height,
NEWT_COLORSET_WINDOW,
NEWT_COLORSET_ACTCHECKBOX);
priv->form = newtForm(scroll_bar, NULL, NEWT_FLAG_NOF12);
newtFormAddComponent(priv->form, scroll_bar);
newtFormSetHeight(priv->form, priv->height - 2);
} else
priv->form = newtForm(NULL, NULL, NEWT_FLAG_NOF12);
if (priv->escape_exits)
newtFormAddHotKey(priv->form, NEWT_KEY_ESCAPE);
cos = nmt_newt_widget_get_components(priv->content);
for (i = 0; cos[i]; i++)
newtFormAddComponent(priv->form, cos[i]);
g_free(cos);
if (priv->focus) {
newtComponent fco;
fco = nmt_newt_widget_get_focus_component(priv->focus);
if (fco)
newtFormSetCurrent(priv->form, fco);
}
#ifdef HAVE_NEWTFORMGETSCROLLPOSITION
if (priv->scroll_position)
newtFormSetScrollPosition(priv->form, priv->scroll_position);
#endif
newtOpenWindow(priv->x, priv->y, priv->width, priv->height, priv->title_lc);
}
static void
nmt_newt_form_destroy(NmtNewtForm *form)
{
NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(form);
#ifdef HAVE_NEWTFORMGETSCROLLPOSITION
priv->scroll_position = newtFormGetScrollPosition(priv->form);
#endif
newtFormDestroy(priv->form);
priv->form = NULL;
newtPopWindowNoRefresh();
nmt_newt_widget_unrealize(NMT_NEWT_WIDGET(form));
}
/* A "normal" newt program would call newtFormRun() to run newt's main loop
* and process events. But we want to let GLib's main loop control the program
* (eg, so libnm can process D-Bus notifications). So we call this function
* to run a single iteration of newt's main loop (or rather, to run newt's
* main loop for 1ms) whenever there are events for newt to process (redrawing
* or keypresses).
*/
static void
nmt_newt_form_iterate(NmtNewtForm *form)
{
NmtNewtFormPrivate * priv = NMT_NEWT_FORM_GET_PRIVATE(form);
NmtNewtWidget * focus;
struct newtExitStruct es;
if (priv->dirty) {
nmt_newt_form_destroy(form);
nmt_newt_form_build(form);
}
newtFormSetTimer(priv->form, 1);
newtFormRun(priv->form, &es);
if (es.reason == NEWT_EXIT_HOTKEY || es.reason == NEWT_EXIT_ERROR) {
/* The user hit Esc or there was an error. */
g_clear_object(&priv->focus);
nmt_newt_form_quit(form);
return;
}
if (es.reason == NEWT_EXIT_COMPONENT) {
/* The user hit Return/Space on a component; update the form focus
* to point that component, and activate it.
*/
focus = nmt_newt_widget_find_component(priv->content, es.u.co);
if (focus) {
nmt_newt_form_set_focus(form, focus);
nmt_newt_widget_activated(focus);
}
} else {
/* The 1ms timer ran out. Update focus but don't do anything else. */
focus = nmt_newt_widget_find_component(priv->content, newtFormGetCurrent(priv->form));
if (focus)
nmt_newt_form_set_focus(form, focus);
}
}
/* @form_stack keeps track of all currently-displayed forms, from top to bottom.
* @keypress_source is the global stdin-monitoring GSource. When it triggers,
* nmt_newt_form_keypress_callback() iterates the top-most form, so it can
* process the keypress.
*/
static GSList * form_stack;
static GSource *keypress_source;
static gboolean
nmt_newt_form_keypress_callback(int fd, GIOCondition condition, gpointer user_data)
{
g_return_val_if_fail(form_stack != NULL, FALSE);
nmt_newt_form_iterate(form_stack->data);
return TRUE;
}
static gboolean
nmt_newt_form_timeout_callback(gpointer user_data)
{
if (form_stack)
nmt_newt_form_iterate(form_stack->data);
return FALSE;
}
static void
nmt_newt_form_redraw(NmtNewtForm *form)
{
g_timeout_add(0, nmt_newt_form_timeout_callback, NULL);
}
static void
nmt_newt_form_real_show(NmtNewtForm *form)
{
if (!keypress_source) {
keypress_source = nm_g_unix_fd_source_new(STDIN_FILENO,
G_IO_IN,
G_PRIORITY_DEFAULT,
nmt_newt_form_keypress_callback,
NULL,
NULL);
g_source_set_can_recurse(keypress_source, TRUE);
g_source_attach(keypress_source, NULL);
}
nmt_newt_form_build(form);
form_stack = g_slist_prepend(form_stack, g_object_ref(form));
nmt_newt_form_redraw(form);
}
/**
* nmt_newt_form_show:
* @form: an #NmtNewtForm
*
* Displays @form and begins running it asynchronously in the default
* #GMainContext. If another form is currently running, it will remain
* visible in the background, but will not be able to receive keyboard
* input until @form exits.
*
* Call nmt_newt_form_quit() to quit the form.
*/
void
nmt_newt_form_show(NmtNewtForm *form)
{
NMT_NEWT_FORM_GET_CLASS(form)->show(form);
}
/**
* nmt_newt_form_run_sync:
* @form: an #NmtNewtForm
*
* Displays @form as with nmt_newt_form_show(), but then iterates the
* #GMainContext internally until @form exits.
*
* Returns: the widget whose activation caused @form to exit, or
* %NULL if it was not caused by a widget. FIXME: this exit value is
* sort of weird and may not be 100% accurate anyway.
*/
NmtNewtWidget *
nmt_newt_form_run_sync(NmtNewtForm *form)
{
NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(form);
nmt_newt_form_show(form);
while (priv->form)
g_main_context_iteration(NULL, TRUE);
return priv->focus;
}
/**
* nmt_newt_form_quit:
* @form: an #NmtNewtForm
*
* Causes @form to exit.
*/
void
nmt_newt_form_quit(NmtNewtForm *form)
{
NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(form);
g_return_if_fail(priv->form != NULL);
nmt_newt_form_destroy(form);
form_stack = g_slist_remove(form_stack, form);
if (form_stack)
nmt_newt_form_iterate(form_stack->data);
else
nm_clear_g_source_inst(&keypress_source);
g_signal_emit(form, signals[QUIT], 0);
g_object_unref(form);
}
/**
* nmt_newt_form_set_focus:
* @form: an #NmtNewtForm
* @widget: the widget to focus
*
* Focuses @widget in @form.
*/
void
nmt_newt_form_set_focus(NmtNewtForm *form, NmtNewtWidget *widget)
{
NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(form);
g_return_if_fail(priv->form != NULL);
if (priv->focus == widget)
return;
if (priv->focus)
g_object_unref(priv->focus);
priv->focus = widget;
if (priv->focus)
g_object_ref(priv->focus);
}
static void
nmt_newt_form_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(object);
int screen_width, screen_height;
switch (prop_id) {
case PROP_TITLE:
if (g_value_get_string(value)) {
priv->title_lc = nmt_newt_locale_from_utf8(g_value_get_string(value));
} else
priv->title_lc = NULL;
break;
case PROP_FULLSCREEN:
if (g_value_get_boolean(value)) {
newtGetScreenSize(&screen_width, &screen_height);
priv->x = priv->y = 2;
priv->fixed_x = priv->fixed_y = TRUE;
priv->width = screen_width - 4;
priv->height = screen_height - 4;
priv->fixed_width = priv->fixed_height = TRUE;
}
break;
case PROP_FULLSCREEN_VERTICAL:
if (g_value_get_boolean(value)) {
newtGetScreenSize(&screen_width, &screen_height);
priv->y = 2;
priv->fixed_y = TRUE;
priv->height = screen_height - 4;
priv->fixed_height = TRUE;
}
break;
case PROP_FULLSCREEN_HORIZONTAL:
if (g_value_get_boolean(value)) {
newtGetScreenSize(&screen_width, &screen_height);
priv->x = 2;
priv->fixed_x = TRUE;
priv->width = screen_width - 4;
priv->fixed_width = TRUE;
}
break;
case PROP_X:
if (g_value_get_uint(value)) {
priv->x = g_value_get_uint(value);
priv->fixed_x = TRUE;
}
break;
case PROP_Y:
if (g_value_get_uint(value)) {
priv->y = g_value_get_uint(value);
priv->fixed_y = TRUE;
}
break;
case PROP_WIDTH:
if (g_value_get_uint(value)) {
priv->width = g_value_get_uint(value);
priv->fixed_width = TRUE;
}
break;
case PROP_HEIGHT:
if (g_value_get_uint(value)) {
priv->height = g_value_get_uint(value);
priv->fixed_height = TRUE;
}
break;
case PROP_PADDING:
priv->padding = g_value_get_uint(value);
break;
case PROP_ESCAPE_EXITS:
priv->escape_exits = g_value_get_boolean(value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
break;
}
}
static void
nmt_newt_form_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(object);
switch (prop_id) {
case PROP_TITLE:
if (priv->title_lc) {
g_value_take_string(value, nmt_newt_locale_to_utf8(priv->title_lc));
} else
g_value_set_string(value, NULL);
break;
case PROP_X:
g_value_set_uint(value, priv->x);
break;
case PROP_Y:
g_value_set_uint(value, priv->y);
break;
case PROP_WIDTH:
g_value_set_uint(value, priv->width);
break;
case PROP_HEIGHT:
g_value_set_uint(value, priv->height);
break;
case PROP_PADDING:
g_value_set_uint(value, priv->padding);
break;
case PROP_ESCAPE_EXITS:
g_value_set_boolean(value, priv->escape_exits);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
break;
}
}
static void
nmt_newt_form_class_init(NmtNewtFormClass *form_class)
{
GObjectClass * object_class = G_OBJECT_CLASS(form_class);
NmtNewtContainerClass *container_class = NMT_NEWT_CONTAINER_CLASS(form_class);
NmtNewtWidgetClass * widget_class = NMT_NEWT_WIDGET_CLASS(form_class);
g_type_class_add_private(form_class, sizeof(NmtNewtFormPrivate));
/* virtual methods */
object_class->set_property = nmt_newt_form_set_property;
object_class->get_property = nmt_newt_form_get_property;
object_class->finalize = nmt_newt_form_finalize;
widget_class->needs_rebuild = nmt_newt_form_needs_rebuild;
container_class->remove = nmt_newt_form_remove;
form_class->show = nmt_newt_form_real_show;
/* signals */
/**
* NmtNewtForm::quit:
* @form: the #NmtNewtForm
*
* Emitted when the form quits.
*/
signals[QUIT] = g_signal_new("quit",
G_OBJECT_CLASS_TYPE(object_class),
G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET(NmtNewtFormClass, quit),
NULL,
NULL,
NULL,
G_TYPE_NONE,
0);
/**
* NmtNewtForm:title:
*
* The form's title. If non-%NULL, this will be displayed above
* the form in its border.
*/
g_object_class_install_property(
object_class,
PROP_TITLE,
g_param_spec_string("title",
"",
"",
NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
/**
* NmtNewtForm:fullscreen:
*
* If %TRUE, the form will fill the entire "screen" (ie, terminal
* window).
*/
g_object_class_install_property(
object_class,
PROP_FULLSCREEN,
g_param_spec_boolean("fullscreen",
"",
"",
FALSE,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
/**
* NmtNewtForm:fullscreen-vertical:
*
* If %TRUE, the form will fill the entire "screen" (ie, terminal
* window) vertically, but not necessarily horizontally.
*/
g_object_class_install_property(
object_class,
PROP_FULLSCREEN_VERTICAL,
g_param_spec_boolean("fullscreen-vertical",
"",
"",
FALSE,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
/**
* NmtNewtForm:fullscreen-horizontal:
*
* If %TRUE, the form will fill the entire "screen" (ie, terminal
* window) horizontally, but not necessarily vertically.
*/
g_object_class_install_property(
object_class,
PROP_FULLSCREEN_HORIZONTAL,
g_param_spec_boolean("fullscreen-horizontal",
"",
"",
FALSE,
G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
/**
* NmtNewtForm:x:
*
* The form's x coordinate. By default, the form will be centered
* on the screen.
*/
g_object_class_install_property(
object_class,
PROP_X,
g_param_spec_uint("x",
"",
"",
0,
G_MAXUINT,
0,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
/**
* NmtNewtForm:y:
*
* The form's y coordinate. By default, the form will be centered
* on the screen.
*/
g_object_class_install_property(
object_class,
PROP_Y,
g_param_spec_uint("y",
"",
"",
0,
G_MAXUINT,
0,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
/**
* NmtNewtForm:width:
*
* The form's width. By default, this will be determined by the
* width of the form's content.
*/
g_object_class_install_property(
object_class,
PROP_WIDTH,
g_param_spec_uint("width",
"",
"",
0,
G_MAXUINT,
0,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
/**
* NmtNewtForm:height:
*
* The form's height. By default, this will be determined by the
* height of the form's content.
*/
g_object_class_install_property(
object_class,
PROP_HEIGHT,
g_param_spec_uint("height",
"",
"",
0,
G_MAXUINT,
0,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
/**
* NmtNewtForm:padding:
*
* The padding between the form's content and its border.
*/
g_object_class_install_property(
object_class,
PROP_PADDING,
g_param_spec_uint("padding",
"",
"",
0,
G_MAXUINT,
1,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
/**
* NmtNewtForm:escape-exits:
*
* If %TRUE, then hitting the Escape key will cause the form to
* exit.
*/
g_object_class_install_property(
object_class,
PROP_ESCAPE_EXITS,
g_param_spec_boolean("escape-exits",
"",
"",
FALSE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
}