mirror of
https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git
synced 2026-06-20 03:38:33 +02:00
nmtui: add vim-style '/' search to connection lists
The connection lists in "nmtui connect" and "nmtui edit" have no way to
filter, so finding one profile among many means scrolling. Press '/' to
reveal a search box that hides non-matching rows as you type; Enter keeps
the filter applied, Esc clears it and restores the list.
A row below the list is always present, so revealing the box does not
resize the form. While typing it shows "Search:"; once a filter is
applied and the box is hidden, it reports the active filter and the match
count ("Matching '...' (N)") so it is clear the list is filtered. Key
interception uses the NmtNewtForm hotkey mechanism.
This commit is contained in:
parent
9da53cb4a7
commit
5e2360afc1
13 changed files with 449 additions and 8 deletions
|
|
@ -199,6 +199,7 @@ src/nmtui/nmt-password-fields.c
|
|||
src/nmtui/nmt-port-list.c
|
||||
src/nmtui/nmt-route-editor.c
|
||||
src/nmtui/nmt-route-table.c
|
||||
src/nmtui/nmt-utils.c
|
||||
src/nmtui/nmt-widget-list.c
|
||||
src/nmtui/nmt-wireguard-peer-editor.c
|
||||
src/nmtui/nmt-wireguard-peer-list.c
|
||||
|
|
|
|||
|
|
@ -31,9 +31,11 @@ typedef struct {
|
|||
NmtNewtWidget *content;
|
||||
|
||||
guint x, y, width, height;
|
||||
guint form_width_max;
|
||||
guint padding;
|
||||
gboolean fixed_x, fixed_y;
|
||||
gboolean fixed_width, fixed_height;
|
||||
gboolean stable_width;
|
||||
char *title_lc;
|
||||
|
||||
gboolean dirty;
|
||||
|
|
@ -184,6 +186,15 @@ nmt_newt_form_build(NmtNewtForm *form)
|
|||
nmt_newt_widget_size_request(priv->content, &form_width, &form_height);
|
||||
newtGetScreenSize(&screen_width, &screen_height);
|
||||
|
||||
if (priv->stable_width) {
|
||||
/* Never let the form get narrower across rebuilds. Otherwise content
|
||||
* that shrinks (eg, a filtered list) makes the window recenter and
|
||||
* "jump". Only forms that opt in get this; see
|
||||
* nmt_newt_form_set_stable_width(). */
|
||||
priv->form_width_max = NM_MAX(priv->form_width_max, (guint) NM_MAX(form_width, 0));
|
||||
form_width = priv->form_width_max;
|
||||
}
|
||||
|
||||
if (!priv->fixed_width)
|
||||
priv->width = NM_MIN(form_width + 2 * ((gint64) priv->padding), screen_width - 2);
|
||||
if (!priv->fixed_height)
|
||||
|
|
@ -476,6 +487,21 @@ nmt_newt_form_add_hotkey(NmtNewtForm *form, int key)
|
|||
newtFormAddHotKey(priv->form, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* nmt_newt_form_set_stable_width:
|
||||
* @form: an #NmtNewtForm
|
||||
*
|
||||
* Stops @form from getting narrower across rebuilds: its width grows to fit
|
||||
* content but never shrinks. Use for forms whose content shrinks in place (eg,
|
||||
* a list filtered by a search box) where recentering would make the window
|
||||
* "jump".
|
||||
*/
|
||||
void
|
||||
nmt_newt_form_set_stable_width(NmtNewtForm *form)
|
||||
{
|
||||
NMT_NEWT_FORM_GET_PRIVATE(form)->stable_width = TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
nmt_newt_form_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -48,4 +48,6 @@ void nmt_newt_form_set_focus(NmtNewtForm *form, NmtNewtWidget *widget);
|
|||
|
||||
void nmt_newt_form_add_hotkey(NmtNewtForm *form, int key);
|
||||
|
||||
void nmt_newt_form_set_stable_width(NmtNewtForm *form);
|
||||
|
||||
#endif /* NMT_NEWT_FORM_H */
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#include <sys/wait.h>
|
||||
|
||||
#include "libnm-glib-aux/nm-io-utils.h"
|
||||
#include "libnm-glib-aux/nm-str-buf.h"
|
||||
|
||||
static void
|
||||
nmt_newt_dialog_g_log_handler(const char *log_domain,
|
||||
|
|
@ -386,6 +387,44 @@ nmt_newt_text_width(const char *str)
|
|||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* nmt_newt_text_truncate
|
||||
* @str: a UTF-8 string
|
||||
* @max_width: the maximum width in terminal columns
|
||||
*
|
||||
* Truncates @str to at most @max_width columns, appending an ellipsis if
|
||||
* anything was dropped (the ellipsis is counted in @max_width).
|
||||
*
|
||||
* Returns: (transfer full): a newly-allocated truncated copy of @str.
|
||||
*/
|
||||
char *
|
||||
nmt_newt_text_truncate(const char *str, int max_width)
|
||||
{
|
||||
nm_auto_str_buf NMStrBuf buf = NM_STR_BUF_INIT(0, FALSE);
|
||||
const char *p;
|
||||
int width = 0;
|
||||
|
||||
if (max_width <= 0)
|
||||
return g_strdup("");
|
||||
if (nmt_newt_text_width(str) <= max_width)
|
||||
return g_strdup(str);
|
||||
|
||||
max_width--; /* reserve one column for the ellipsis */
|
||||
|
||||
for (p = str; *p; p = g_utf8_next_char(p)) {
|
||||
gunichar ch = g_utf8_get_char(p);
|
||||
int w = g_unichar_iszerowidth(ch) ? 0 : (g_unichar_iswide(ch) ? 2 : 1);
|
||||
|
||||
if (width + w > max_width)
|
||||
break;
|
||||
width += w;
|
||||
nm_str_buf_append_len(&buf, p, g_utf8_next_char(p) - p);
|
||||
}
|
||||
nm_str_buf_append(&buf, "\xe2\x80\xa6"); /* U+2026 HORIZONTAL ELLIPSIS */
|
||||
|
||||
return nm_str_buf_dup_str(&buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* nmt_newt_edit_string:
|
||||
* @data: data to edit
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ char *nmt_newt_locale_from_utf8(const char *str_utf8);
|
|||
|
||||
int nmt_newt_text_width(const char *str);
|
||||
|
||||
char *nmt_newt_text_truncate(const char *str, int max_width);
|
||||
|
||||
void nmt_newt_message_dialog(const char *message, ...) _nm_printf(1, 2);
|
||||
int nmt_newt_choice_dialog(const char *button1, const char *button2, const char *message, ...)
|
||||
_nm_printf(3, 4);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
#include "nmtui.h"
|
||||
#include "nmt-connect-connection-list.h"
|
||||
#include "nmt-utils.h"
|
||||
#include "libnmc-base/nm-client-utils.h"
|
||||
|
||||
G_DEFINE_TYPE(NmtConnectConnectionList, nmt_connect_connection_list, NMT_TYPE_NEWT_LISTBOX)
|
||||
|
|
@ -47,6 +48,8 @@ typedef struct {
|
|||
|
||||
typedef struct {
|
||||
GSList *nmt_devices;
|
||||
char *filter_text;
|
||||
int match_count;
|
||||
} NmtConnectConnectionListPrivate;
|
||||
|
||||
/**
|
||||
|
|
@ -444,6 +447,13 @@ connection_find_ac(NMConnection *conn, const GPtrArray *acs)
|
|||
return NULL;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
connection_matches(NmtConnectConnection *nmtconn, const char *needle)
|
||||
{
|
||||
return nmt_utils_filter_match(nmtconn->name, needle)
|
||||
|| nmt_utils_filter_match(nmtconn->ssid, needle);
|
||||
}
|
||||
|
||||
static void
|
||||
nmt_connect_connection_list_rebuild(NmtConnectConnectionList *list)
|
||||
{
|
||||
|
|
@ -456,6 +466,8 @@ nmt_connect_connection_list_rebuild(NmtConnectConnectionList *list)
|
|||
GSList *nmt_devices, *diter, *citer;
|
||||
NmtConnectDevice *nmtdev;
|
||||
NmtConnectConnection *nmtconn;
|
||||
gboolean did_group;
|
||||
int n_matches = 0;
|
||||
|
||||
g_slist_free_full(priv->nmt_devices, (GDestroyNotify) nmt_connect_device_free);
|
||||
priv->nmt_devices = NULL;
|
||||
|
|
@ -486,18 +498,32 @@ nmt_connect_connection_list_rebuild(NmtConnectConnectionList *list)
|
|||
}
|
||||
}
|
||||
|
||||
did_group = FALSE;
|
||||
for (diter = nmt_devices; diter; diter = diter->next) {
|
||||
gboolean dev_matches = FALSE;
|
||||
|
||||
nmtdev = diter->data;
|
||||
|
||||
if (nmtdev->conns) {
|
||||
if (diter != nmt_devices)
|
||||
nmt_newt_listbox_append(listbox, "", NULL);
|
||||
nmt_newt_listbox_append(listbox, nmtdev->name, NULL);
|
||||
for (citer = nmtdev->conns; citer; citer = citer->next) {
|
||||
if (connection_matches(citer->data, priv->filter_text)) {
|
||||
dev_matches = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!dev_matches)
|
||||
continue;
|
||||
|
||||
if (did_group)
|
||||
nmt_newt_listbox_append(listbox, "", NULL);
|
||||
nmt_newt_listbox_append(listbox, nmtdev->name, NULL);
|
||||
did_group = TRUE;
|
||||
|
||||
for (citer = nmtdev->conns; citer; citer = citer->next) {
|
||||
nmtconn = citer->data;
|
||||
|
||||
if (!connection_matches(nmtconn, priv->filter_text))
|
||||
continue;
|
||||
|
||||
if (nmtconn->conn)
|
||||
nmtconn->active = connection_find_ac(nmtconn->conn, acs);
|
||||
if (nmtconn->active) {
|
||||
|
|
@ -523,15 +549,36 @@ nmt_connect_connection_list_rebuild(NmtConnectConnectionList *list)
|
|||
|
||||
nmt_newt_listbox_append(listbox, row, nmtconn);
|
||||
g_free(row);
|
||||
n_matches++;
|
||||
}
|
||||
}
|
||||
|
||||
priv->nmt_devices = nmt_devices;
|
||||
priv->match_count = n_matches;
|
||||
|
||||
g_object_notify(G_OBJECT(listbox), "active");
|
||||
g_object_notify(G_OBJECT(listbox), "active-key");
|
||||
}
|
||||
|
||||
void
|
||||
nmt_connect_connection_list_set_filter_text(NmtConnectConnectionList *list, const char *text)
|
||||
{
|
||||
NmtConnectConnectionListPrivate *priv = NMT_CONNECT_CONNECTION_LIST_GET_PRIVATE(list);
|
||||
|
||||
if (nm_streq0(text, priv->filter_text))
|
||||
return;
|
||||
|
||||
g_free(priv->filter_text);
|
||||
priv->filter_text = g_strdup(text);
|
||||
nmt_connect_connection_list_rebuild(list);
|
||||
}
|
||||
|
||||
int
|
||||
nmt_connect_connection_list_get_match_count(NmtConnectConnectionList *list)
|
||||
{
|
||||
return NMT_CONNECT_CONNECTION_LIST_GET_PRIVATE(list)->match_count;
|
||||
}
|
||||
|
||||
static void
|
||||
rebuild_on_property_changed(GObject *object, GParamSpec *spec, gpointer list)
|
||||
{
|
||||
|
|
@ -567,6 +614,7 @@ nmt_connect_connection_list_finalize(GObject *object)
|
|||
NmtConnectConnectionListPrivate *priv = NMT_CONNECT_CONNECTION_LIST_GET_PRIVATE(object);
|
||||
|
||||
g_slist_free_full(priv->nmt_devices, (GDestroyNotify) nmt_connect_device_free);
|
||||
nm_clear_g_free(&priv->filter_text);
|
||||
|
||||
g_signal_handlers_disconnect_by_func(nm_client,
|
||||
G_CALLBACK(rebuild_on_property_changed),
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ GType nmt_connect_connection_list_get_type(void);
|
|||
|
||||
NmtNewtWidget *nmt_connect_connection_list_new(void);
|
||||
|
||||
void nmt_connect_connection_list_set_filter_text(NmtConnectConnectionList *list, const char *text);
|
||||
|
||||
int nmt_connect_connection_list_get_match_count(NmtConnectConnectionList *list);
|
||||
|
||||
gboolean nmt_connect_connection_list_get_connection(NmtConnectConnectionList *list,
|
||||
const char *identifier,
|
||||
NMConnection **connection,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#include "nmtui-edit.h"
|
||||
#include "nmt-edit-connection-list.h"
|
||||
#include "nmt-editor.h"
|
||||
#include "nmt-utils.h"
|
||||
|
||||
#include "nm-editor-utils.h"
|
||||
|
||||
|
|
@ -35,6 +36,10 @@ typedef struct {
|
|||
NmtNewtListbox *listbox;
|
||||
NmtNewtButtonBox *buttons;
|
||||
|
||||
NmtSearch *search;
|
||||
char *filter_text;
|
||||
int match_count;
|
||||
|
||||
NmtNewtWidget *add;
|
||||
NmtNewtWidget *edit;
|
||||
NmtNewtWidget *delete;
|
||||
|
|
@ -68,13 +73,16 @@ static void add_clicked(NmtNewtButton *button, gpointer list);
|
|||
static void edit_clicked(NmtNewtButton *button, gpointer list);
|
||||
static void delete_clicked(NmtNewtButton *button, gpointer list);
|
||||
static void listbox_activated(NmtNewtWidget *listbox, gpointer list);
|
||||
static void edit_search_apply(gpointer list, const char *text);
|
||||
static int edit_search_count(gpointer list);
|
||||
|
||||
static void
|
||||
nmt_edit_connection_list_init(NmtEditConnectionList *list)
|
||||
{
|
||||
NmtEditConnectionListPrivate *priv = NMT_EDIT_CONNECTION_LIST_GET_PRIVATE(list);
|
||||
NmtNewtWidget *listbox, *buttons;
|
||||
NmtNewtWidget *listbox, *buttons, *search_row, *search_label, *search;
|
||||
NmtNewtGrid *grid = NMT_NEWT_GRID(list);
|
||||
NmtNewtGrid *search_grid;
|
||||
|
||||
listbox = g_object_new(NMT_TYPE_NEWT_LISTBOX,
|
||||
"flags",
|
||||
|
|
@ -90,6 +98,28 @@ nmt_edit_connection_list_init(NmtEditConnectionList *list)
|
|||
| NMT_NEWT_GRID_EXPAND_Y);
|
||||
g_signal_connect(priv->listbox, "activated", G_CALLBACK(listbox_activated), list);
|
||||
|
||||
/* Search row below the listbox. The row is always present so revealing the
|
||||
* entry does not resize the form; vim-style '/' shows the entry, and once a
|
||||
* filter is applied the label reports it ("Matching '...' (N)"). */
|
||||
search_row = nmt_newt_grid_new();
|
||||
search_grid = NMT_NEWT_GRID(search_row);
|
||||
nmt_newt_grid_add(grid, search_row, 0, 1);
|
||||
nmt_newt_grid_set_flags(grid, search_row, NMT_NEWT_GRID_FILL_X | NMT_NEWT_GRID_EXPAND_X);
|
||||
|
||||
search_label = nmt_newt_label_new("");
|
||||
nmt_newt_grid_add(search_grid, search_label, 0, 0);
|
||||
|
||||
search = nmt_newt_entry_new(NMT_SEARCH_ENTRY_WIDTH, 0);
|
||||
nmt_newt_grid_add(search_grid, search, 1, 0);
|
||||
nmt_newt_widget_set_padding(search, 1, 0, 0, 0);
|
||||
|
||||
priv->search = nmt_search_new(NMT_NEWT_ENTRY(search),
|
||||
NMT_NEWT_LABEL(search_label),
|
||||
NMT_NEWT_WIDGET(priv->listbox),
|
||||
edit_search_apply,
|
||||
edit_search_count,
|
||||
list);
|
||||
|
||||
buttons = nmt_newt_button_box_new(NMT_NEWT_BUTTON_BOX_VERTICAL);
|
||||
priv->buttons = NMT_NEWT_BUTTON_BOX(buttons);
|
||||
nmt_newt_grid_add(grid, buttons, 1, 0);
|
||||
|
|
@ -157,7 +187,7 @@ nmt_edit_connection_list_rebuild(NmtEditConnectionList *list)
|
|||
gboolean did_header = FALSE, did_vpn = FALSE, did_any = FALSE;
|
||||
NMEditorConnectionTypeData **types;
|
||||
NMConnection *conn, *selected_conn;
|
||||
int i, row, selected_row;
|
||||
int i, row, selected_row, n_matches = 0;
|
||||
|
||||
selected_row = nmt_newt_listbox_get_active(priv->listbox);
|
||||
selected_conn = nmt_newt_listbox_get_active_key(priv->listbox);
|
||||
|
|
@ -185,17 +215,21 @@ nmt_edit_connection_list_rebuild(NmtEditConnectionList *list)
|
|||
|
||||
if (!priv->grouped) {
|
||||
/* Just add the connections in order */
|
||||
for (iter = priv->connections, row = 0; iter; iter = iter->next, row++) {
|
||||
for (iter = priv->connections, row = 0; iter; iter = iter->next) {
|
||||
conn = iter->data;
|
||||
if (!nmt_utils_filter_match(nm_connection_get_id(conn), priv->filter_text))
|
||||
continue;
|
||||
nmt_newt_listbox_append(priv->listbox, nm_connection_get_id(conn), conn);
|
||||
if (conn == selected_conn)
|
||||
selected_row = row;
|
||||
row++;
|
||||
n_matches++;
|
||||
}
|
||||
if (selected_row >= row)
|
||||
selected_row = row - 1;
|
||||
nmt_newt_listbox_set_active(priv->listbox, selected_row);
|
||||
|
||||
did_any = !!priv->connections;
|
||||
did_any = n_matches > 0;
|
||||
|
||||
goto done;
|
||||
}
|
||||
|
|
@ -220,6 +254,8 @@ nmt_edit_connection_list_rebuild(NmtEditConnectionList *list)
|
|||
continue;
|
||||
if (!nm_connection_is_type(conn, nm_setting_get_name(setting)))
|
||||
continue;
|
||||
if (!nmt_utils_filter_match(nm_connection_get_id(conn), priv->filter_text))
|
||||
continue;
|
||||
|
||||
if (!did_header) {
|
||||
nmt_newt_listbox_append(priv->listbox, types[i]->name, NULL);
|
||||
|
|
@ -240,6 +276,7 @@ nmt_edit_connection_list_rebuild(NmtEditConnectionList *list)
|
|||
if (conn == selected_conn)
|
||||
selected_row = row;
|
||||
row++;
|
||||
n_matches++;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -248,6 +285,9 @@ nmt_edit_connection_list_rebuild(NmtEditConnectionList *list)
|
|||
nmt_newt_listbox_set_active(priv->listbox, selected_row);
|
||||
|
||||
done:
|
||||
priv->match_count = n_matches;
|
||||
if (priv->search)
|
||||
nmt_search_update_label(priv->search);
|
||||
nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(priv->edit), did_any);
|
||||
nmt_newt_component_set_sensitive(NMT_NEWT_COMPONENT(priv->delete), did_any);
|
||||
}
|
||||
|
|
@ -315,6 +355,31 @@ listbox_activated(NmtNewtWidget *listbox, gpointer list)
|
|||
edit_clicked(NMT_NEWT_BUTTON(priv->edit), list);
|
||||
}
|
||||
|
||||
static void
|
||||
edit_search_apply(gpointer list, const char *text)
|
||||
{
|
||||
NmtEditConnectionListPrivate *priv = NMT_EDIT_CONNECTION_LIST_GET_PRIVATE(list);
|
||||
|
||||
if (nm_streq0(text, priv->filter_text))
|
||||
return;
|
||||
|
||||
g_free(priv->filter_text);
|
||||
priv->filter_text = g_strdup(text);
|
||||
nmt_edit_connection_list_rebuild(list);
|
||||
}
|
||||
|
||||
static int
|
||||
edit_search_count(gpointer list)
|
||||
{
|
||||
return NMT_EDIT_CONNECTION_LIST_GET_PRIVATE(list)->match_count;
|
||||
}
|
||||
|
||||
void
|
||||
nmt_edit_connection_list_bind_search(NmtEditConnectionList *list, NmtNewtForm *form)
|
||||
{
|
||||
nmt_search_bind_form(NMT_EDIT_CONNECTION_LIST_GET_PRIVATE(list)->search, form);
|
||||
}
|
||||
|
||||
static void
|
||||
connection_saved(GObject *conn, GAsyncResult *result, gpointer user_data)
|
||||
{
|
||||
|
|
@ -348,6 +413,8 @@ nmt_edit_connection_list_finalize(GObject *object)
|
|||
|
||||
free_connections(NMT_EDIT_CONNECTION_LIST(object));
|
||||
g_clear_object(&priv->extra);
|
||||
nm_clear_pointer(&priv->search, g_free);
|
||||
nm_clear_g_free(&priv->filter_text);
|
||||
|
||||
G_OBJECT_CLASS(nmt_edit_connection_list_parent_class)->finalize(object);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,4 +42,6 @@ typedef gboolean (*NmtEditConnectionListFilter)(NmtEditConnectionList *list,
|
|||
|
||||
void nmt_edit_connection_list_recommit(NmtEditConnectionList *list);
|
||||
|
||||
void nmt_edit_connection_list_bind_search(NmtEditConnectionList *list, NmtNewtForm *form);
|
||||
|
||||
#endif /* NMT_EDIT_CONNECTION_LIST_H */
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
|
||||
#include "nmt-utils.h"
|
||||
|
||||
#include "libnmt-newt/nmt-newt.h"
|
||||
|
||||
/**
|
||||
* NmtSyncOp:
|
||||
*
|
||||
|
|
@ -128,3 +130,178 @@ nmt_sync_op_complete_pointer(NmtSyncOp *op, gpointer result, GError *error)
|
|||
real->error = error ? g_error_copy(error) : NULL;
|
||||
real->complete = GUINT_TO_POINTER(TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* nmt_utils_filter_match:
|
||||
* @haystack: (nullable): the string to search in
|
||||
* @needle: (nullable): the search term
|
||||
*
|
||||
* Case-insensitive UTF-8 substring test. An empty or %NULL @needle matches
|
||||
* anything; a %NULL @haystack matches only an empty @needle.
|
||||
*
|
||||
* Returns: %TRUE if @haystack contains @needle.
|
||||
*/
|
||||
gboolean
|
||||
nmt_utils_filter_match(const char *haystack, const char *needle)
|
||||
{
|
||||
gs_free char *h = NULL;
|
||||
gs_free char *n = NULL;
|
||||
|
||||
if (!needle || !needle[0])
|
||||
return TRUE;
|
||||
if (!haystack)
|
||||
return FALSE;
|
||||
|
||||
h = g_utf8_casefold(haystack, -1);
|
||||
n = g_utf8_casefold(needle, -1);
|
||||
return strstr(h, n) != NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Renders the search state into @label: "Search:" while typing,
|
||||
* "Matching '...' (N)" once a filter is applied with the entry hidden, or
|
||||
* empty when idle.
|
||||
*
|
||||
* When the entry is hidden the text is padded to the width the row occupies
|
||||
* while searching ("Search:" plus the entry), so revealing or hiding the entry
|
||||
* never changes the form's width.
|
||||
*/
|
||||
static void
|
||||
set_search_label(NmtNewtLabel *label, const char *filter_text, int match_count, gboolean searching)
|
||||
{
|
||||
gs_free char *body = NULL;
|
||||
int reserve, body_width;
|
||||
|
||||
if (searching) {
|
||||
nmt_newt_label_set_text(label, _("Search:"));
|
||||
return;
|
||||
}
|
||||
|
||||
reserve = nmt_newt_text_width(_("Search:")) + 1 + NMT_SEARCH_ENTRY_WIDTH;
|
||||
|
||||
if (!nm_str_is_empty(filter_text)) {
|
||||
gs_free char *shown = NULL;
|
||||
int overhead;
|
||||
|
||||
/* Echo the filter, but elide it so the confirmed label never exceeds
|
||||
* the reserved width; otherwise the form grows when a long search is
|
||||
* confirmed with Enter. */
|
||||
body = g_strdup_printf(_("Matching '%s' (%d)"), "", match_count);
|
||||
overhead = nmt_newt_text_width(body);
|
||||
nm_clear_g_free(&body);
|
||||
|
||||
shown = nmt_newt_text_truncate(filter_text, reserve - overhead);
|
||||
body = g_strdup_printf(_("Matching '%s' (%d)"), shown, match_count);
|
||||
} else
|
||||
body = g_strdup("");
|
||||
|
||||
body_width = nmt_newt_text_width(body);
|
||||
if (body_width < reserve) {
|
||||
gs_free char *padded = NULL;
|
||||
|
||||
padded = g_strdup_printf("%s%*s", body, reserve - body_width, "");
|
||||
nmt_newt_label_set_text(label, padded);
|
||||
} else
|
||||
nmt_newt_label_set_text(label, body);
|
||||
}
|
||||
|
||||
struct _NmtSearch {
|
||||
NmtNewtEntry *entry;
|
||||
NmtNewtLabel *label;
|
||||
NmtNewtWidget *focus;
|
||||
NmtSearchApplyFunc apply;
|
||||
NmtSearchCountFunc count;
|
||||
gpointer user_data;
|
||||
};
|
||||
|
||||
void
|
||||
nmt_search_update_label(NmtSearch *search)
|
||||
{
|
||||
set_search_label(search->label,
|
||||
nmt_newt_entry_get_text(search->entry),
|
||||
search->count(search->user_data),
|
||||
nmt_newt_widget_get_visible(NMT_NEWT_WIDGET(search->entry)));
|
||||
}
|
||||
|
||||
static void
|
||||
search_text_changed(GObject *entry, GParamSpec *pspec, gpointer user_data)
|
||||
{
|
||||
NmtSearch *search = user_data;
|
||||
|
||||
search->apply(search->user_data, nmt_newt_entry_get_text(search->entry));
|
||||
nmt_search_update_label(search);
|
||||
}
|
||||
|
||||
static void
|
||||
search_activated(NmtNewtWidget *entry, gpointer user_data)
|
||||
{
|
||||
NmtSearch *search = user_data;
|
||||
NmtNewtForm *form = nmt_newt_widget_get_form(search->focus);
|
||||
|
||||
/* Enter: hide the entry but keep the filter; the label now reports it. */
|
||||
nmt_newt_widget_set_visible(NMT_NEWT_WIDGET(search->entry), FALSE);
|
||||
nmt_search_update_label(search);
|
||||
if (form)
|
||||
nmt_newt_form_set_focus(form, search->focus);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
search_hotkey(NmtNewtForm *form, int key, gpointer user_data)
|
||||
{
|
||||
NmtSearch *search = user_data;
|
||||
const char *filter = nmt_newt_entry_get_text(search->entry);
|
||||
|
||||
if (key == '/') {
|
||||
nmt_newt_widget_set_visible(NMT_NEWT_WIDGET(search->entry), TRUE);
|
||||
nmt_search_update_label(search);
|
||||
nmt_newt_form_set_focus(form, NMT_NEWT_WIDGET(search->entry));
|
||||
return TRUE;
|
||||
}
|
||||
if (key == NEWT_KEY_ESCAPE
|
||||
&& (nmt_newt_widget_get_visible(NMT_NEWT_WIDGET(search->entry))
|
||||
|| !nm_str_is_empty(filter))) {
|
||||
/* Esc: clear the filter (via notify::text) and return to the list. */
|
||||
nmt_newt_entry_set_text(search->entry, "");
|
||||
nmt_newt_widget_set_visible(NMT_NEWT_WIDGET(search->entry), FALSE);
|
||||
nmt_search_update_label(search);
|
||||
nmt_newt_form_set_focus(form, search->focus);
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
NmtSearch *
|
||||
nmt_search_new(NmtNewtEntry *entry,
|
||||
NmtNewtLabel *label,
|
||||
NmtNewtWidget *focus,
|
||||
NmtSearchApplyFunc apply,
|
||||
NmtSearchCountFunc count,
|
||||
gpointer user_data)
|
||||
{
|
||||
NmtSearch *search = g_new0(NmtSearch, 1);
|
||||
|
||||
search->entry = entry;
|
||||
search->label = label;
|
||||
search->focus = focus;
|
||||
search->apply = apply;
|
||||
search->count = count;
|
||||
search->user_data = user_data;
|
||||
|
||||
nmt_newt_widget_set_visible(NMT_NEWT_WIDGET(entry), FALSE);
|
||||
g_signal_connect(entry, "notify::text", G_CALLBACK(search_text_changed), search);
|
||||
g_signal_connect(entry, "activated", G_CALLBACK(search_activated), search);
|
||||
|
||||
return search;
|
||||
}
|
||||
|
||||
void
|
||||
nmt_search_bind_form(NmtSearch *search, NmtNewtForm *form)
|
||||
{
|
||||
g_signal_connect(form, "hotkey", G_CALLBACK(search_hotkey), search);
|
||||
nmt_newt_form_add_hotkey(form, '/');
|
||||
nmt_newt_form_set_stable_width(form);
|
||||
|
||||
/* Reserve the search row's width up front so the form does not grow when
|
||||
* the entry is first revealed. */
|
||||
nmt_search_update_label(search);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
#ifndef NMT_UTILS_H
|
||||
#define NMT_UTILS_H
|
||||
|
||||
#include "libnmt-newt/nmt-newt-types.h"
|
||||
|
||||
typedef struct {
|
||||
gpointer private[3];
|
||||
} NmtSyncOp;
|
||||
|
|
@ -18,4 +20,34 @@ void nmt_sync_op_complete_boolean(NmtSyncOp *op, gboolean result, GError *er
|
|||
gpointer nmt_sync_op_wait_pointer(NmtSyncOp *op, GError **error);
|
||||
void nmt_sync_op_complete_pointer(NmtSyncOp *op, gpointer result, GError *error);
|
||||
|
||||
gboolean nmt_utils_filter_match(const char *haystack, const char *needle);
|
||||
|
||||
#define NMT_SEARCH_ENTRY_WIDTH 34
|
||||
|
||||
/**
|
||||
* NmtSearch:
|
||||
*
|
||||
* Vim-style '/' search glue shared by the connection lists. The caller builds
|
||||
* the @label and @entry into its own layout and hands them over; NmtSearch
|
||||
* wires '/' to reveal the entry, Enter to confirm, Esc to clear, keeps the
|
||||
* status label in sync, and pins the form width so the list does not jump.
|
||||
*
|
||||
* @apply is invoked with the current text whenever the filter changes; @count
|
||||
* returns the live match count for the label.
|
||||
*/
|
||||
typedef struct _NmtSearch NmtSearch;
|
||||
|
||||
typedef void (*NmtSearchApplyFunc)(gpointer user_data, const char *text);
|
||||
typedef int (*NmtSearchCountFunc)(gpointer user_data);
|
||||
|
||||
NmtSearch *nmt_search_new(NmtNewtEntry *entry,
|
||||
NmtNewtLabel *label,
|
||||
NmtNewtWidget *focus,
|
||||
NmtSearchApplyFunc apply,
|
||||
NmtSearchCountFunc count,
|
||||
gpointer user_data);
|
||||
|
||||
void nmt_search_bind_form(NmtSearch *search, NmtNewtForm *form);
|
||||
void nmt_search_update_label(NmtSearch *search);
|
||||
|
||||
#endif /* NMT_UTILS_H */
|
||||
|
|
|
|||
|
|
@ -583,12 +583,26 @@ wifi_rescan(NmtNewtButton *button, gpointer data_batch)
|
|||
}
|
||||
}
|
||||
|
||||
static void
|
||||
connect_search_apply(gpointer list, const char *text)
|
||||
{
|
||||
nmt_connect_connection_list_set_filter_text(NMT_CONNECT_CONNECTION_LIST(list), text);
|
||||
}
|
||||
|
||||
static int
|
||||
connect_search_count(gpointer list)
|
||||
{
|
||||
return nmt_connect_connection_list_get_match_count(NMT_CONNECT_CONNECTION_LIST(list));
|
||||
}
|
||||
|
||||
static NmtNewtForm *
|
||||
nmt_connect_connection_list(gboolean is_top)
|
||||
{
|
||||
int screen_width, screen_height;
|
||||
NmtNewtForm *form;
|
||||
NmtNewtWidget *list, *activate, *quit, *bbox, *grid, *rescan;
|
||||
NmtNewtWidget *search_row, *search_label, *search_entry;
|
||||
NmtSearch *search;
|
||||
RescanBatch *batch_data;
|
||||
gs_unref_ptrarray GPtrArray *all_active_wifi_devices = NULL;
|
||||
|
||||
|
|
@ -634,6 +648,31 @@ nmt_connect_connection_list(gboolean is_top)
|
|||
quit = nmt_newt_button_box_add_end(NMT_NEWT_BUTTON_BOX(bbox), is_top ? _("Quit") : _("Back"));
|
||||
nmt_newt_widget_set_exit_on_activate(quit, TRUE);
|
||||
|
||||
/* Search row below the list. The row is always present so revealing the
|
||||
* entry does not resize the form; vim-style '/' shows the entry, and once a
|
||||
* filter is applied the label reports it ("Matching '...' (N)"). */
|
||||
search_row = nmt_newt_grid_new();
|
||||
nmt_newt_grid_add(NMT_NEWT_GRID(grid), search_row, 0, 1);
|
||||
nmt_newt_grid_set_flags(NMT_NEWT_GRID(grid),
|
||||
search_row,
|
||||
NMT_NEWT_GRID_FILL_X | NMT_NEWT_GRID_EXPAND_X);
|
||||
|
||||
search_label = nmt_newt_label_new("");
|
||||
nmt_newt_grid_add(NMT_NEWT_GRID(search_row), search_label, 0, 0);
|
||||
|
||||
search_entry = nmt_newt_entry_new(NMT_SEARCH_ENTRY_WIDTH, 0);
|
||||
nmt_newt_grid_add(NMT_NEWT_GRID(search_row), search_entry, 1, 0);
|
||||
nmt_newt_widget_set_padding(search_entry, 1, 0, 0, 0);
|
||||
|
||||
search = nmt_search_new(NMT_NEWT_ENTRY(search_entry),
|
||||
NMT_NEWT_LABEL(search_label),
|
||||
list,
|
||||
connect_search_apply,
|
||||
connect_search_count,
|
||||
list);
|
||||
nmt_search_bind_form(search, form);
|
||||
g_object_set_data_full(G_OBJECT(form), "search-data", search, g_free);
|
||||
|
||||
nmt_newt_form_set_content(form, grid);
|
||||
return form;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ nmt_edit_main_connection_list(gboolean is_top)
|
|||
g_signal_connect(list, "edit-connection", G_CALLBACK(list_edit_connection), form);
|
||||
g_signal_connect(list, "remove-connection", G_CALLBACK(list_remove_connection), form);
|
||||
|
||||
nmt_edit_connection_list_bind_search(NMT_EDIT_CONNECTION_LIST(list), form);
|
||||
|
||||
nmt_newt_form_set_content(form, list);
|
||||
return form;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue