diff --git a/po/POTFILES.in b/po/POTFILES.in index 605683df42..f1b5c98120 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -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 diff --git a/src/libnmt-newt/nmt-newt-form.c b/src/libnmt-newt/nmt-newt-form.c index 9f372e0b8d..aefc7eca77 100644 --- a/src/libnmt-newt/nmt-newt-form.c +++ b/src/libnmt-newt/nmt-newt-form.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) { diff --git a/src/libnmt-newt/nmt-newt-form.h b/src/libnmt-newt/nmt-newt-form.h index 7e5b7dd60c..63b64be6a3 100644 --- a/src/libnmt-newt/nmt-newt-form.h +++ b/src/libnmt-newt/nmt-newt-form.h @@ -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 */ diff --git a/src/libnmt-newt/nmt-newt-utils.c b/src/libnmt-newt/nmt-newt-utils.c index 36a47b2b61..1864b4a443 100644 --- a/src/libnmt-newt/nmt-newt-utils.c +++ b/src/libnmt-newt/nmt-newt-utils.c @@ -17,6 +17,7 @@ #include #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 diff --git a/src/libnmt-newt/nmt-newt-utils.h b/src/libnmt-newt/nmt-newt-utils.h index 04089f9f8b..32237ae298 100644 --- a/src/libnmt-newt/nmt-newt-utils.h +++ b/src/libnmt-newt/nmt-newt-utils.h @@ -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); diff --git a/src/nmtui/nmt-connect-connection-list.c b/src/nmtui/nmt-connect-connection-list.c index 8e7dc807de..1980c30d90 100644 --- a/src/nmtui/nmt-connect-connection-list.c +++ b/src/nmtui/nmt-connect-connection-list.c @@ -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), diff --git a/src/nmtui/nmt-connect-connection-list.h b/src/nmtui/nmt-connect-connection-list.h index bfc0c0073e..7b8d4a4be8 100644 --- a/src/nmtui/nmt-connect-connection-list.h +++ b/src/nmtui/nmt-connect-connection-list.h @@ -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, diff --git a/src/nmtui/nmt-edit-connection-list.c b/src/nmtui/nmt-edit-connection-list.c index 1ea4400709..825e14941a 100644 --- a/src/nmtui/nmt-edit-connection-list.c +++ b/src/nmtui/nmt-edit-connection-list.c @@ -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); } diff --git a/src/nmtui/nmt-edit-connection-list.h b/src/nmtui/nmt-edit-connection-list.h index 0d23f8b157..0846893f7c 100644 --- a/src/nmtui/nmt-edit-connection-list.h +++ b/src/nmtui/nmt-edit-connection-list.h @@ -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 */ diff --git a/src/nmtui/nmt-utils.c b/src/nmtui/nmt-utils.c index 7a50054258..148391efec 100644 --- a/src/nmtui/nmt-utils.c +++ b/src/nmtui/nmt-utils.c @@ -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); +} diff --git a/src/nmtui/nmt-utils.h b/src/nmtui/nmt-utils.h index 3b780ae76f..d73b88c49d 100644 --- a/src/nmtui/nmt-utils.h +++ b/src/nmtui/nmt-utils.h @@ -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 */ diff --git a/src/nmtui/nmtui-connect.c b/src/nmtui/nmtui-connect.c index e2d91e7ffc..492bc13d05 100644 --- a/src/nmtui/nmtui-connect.c +++ b/src/nmtui/nmtui-connect.c @@ -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; } diff --git a/src/nmtui/nmtui-edit.c b/src/nmtui/nmtui-edit.c index 0ba3bd0da7..1bc1424579 100644 --- a/src/nmtui/nmtui-edit.c +++ b/src/nmtui/nmtui-edit.c @@ -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; }