Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
test_tagging_completion.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Guillaume Stutin.
4 Copyright (C) 2026 Paolo SANTUCCI.
5
6 Ansel is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Ansel is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
18*/
19
21
22#include <stdarg.h>
23#include <stddef.h>
24#include <setjmp.h> // NOLINT(misc-include-cleaner)
25#include <stdint.h>
26#include <cmocka.h>
27
28#ifdef _WIN32
29#include "win/main_wrapper.h"
30#endif
31
33{
35 GtkEntry *entry;
36 GtkListStore *store;
37 GtkEntryCompletion *completion;
39
45
46static void _append_completion(dt_tagging_completion_test_t *test, const char *text)
47{
48 GtkTreeIter iter;
49 gtk_list_store_append(test->store, &iter);
50 gtk_list_store_set(test->store, &iter, 0, text, -1);
51}
52
53static void _drain_gtk_events()
54{
55 while(gtk_events_pending()) gtk_main_iteration();
56}
57
58static gboolean _quit_main_loop(GMainLoop *loop)
59{
60 g_main_loop_quit(loop);
61 return G_SOURCE_REMOVE;
62}
63
65{
66 GMainLoop *loop = g_main_loop_new(NULL, FALSE);
67 g_timeout_add(250, (GSourceFunc)_quit_main_loop, loop);
68 g_main_loop_run(loop);
69 g_main_loop_unref(loop);
70}
71
72static gboolean _completion_popup_is_mapped(GtkWidget *entry_window)
73{
74 gboolean mapped = FALSE;
75 GList *toplevels = gtk_window_list_toplevels();
76 for(GList *toplevel = toplevels; toplevel; toplevel = g_list_next(toplevel))
77 {
78 GtkWidget *widget = GTK_WIDGET(toplevel->data);
79 if(widget != entry_window && gtk_widget_get_mapped(widget)) mapped = TRUE;
80 }
81 g_list_free(toplevels);
82 return mapped;
83}
84
85static gboolean _send_key(GtkWidget *widget, const guint keyval)
86{
87 GdkEventKey event = { 0 };
88 event.type = GDK_KEY_PRESS;
89 event.window = g_object_ref(gtk_widget_get_window(widget));
90 event.send_event = TRUE;
91 event.time = GDK_CURRENT_TIME;
92 event.keyval = keyval;
93 gboolean handled = FALSE;
94 g_signal_emit_by_name(widget, "key-press-event", &event, &handled);
95 g_object_unref(event.window);
96 return handled;
97}
98
99/* GtkEntryCompletion maps its popup only for text typed through an event: the popup needs the
100 * device of the event being handled when the text changed, and text inserted from code, or a key
101 * emitted straight on the widget, has none. This key goes through GTK's event dispatch instead. */
102static void _type_key(GtkWidget *widget, const guint keyval)
103{
104 GdkDisplay *display = gtk_widget_get_display(widget);
105 GdkEvent *event = gdk_event_new(GDK_KEY_PRESS);
106 event->key.window = g_object_ref(gtk_widget_get_window(gtk_widget_get_toplevel(widget)));
107 event->key.send_event = TRUE;
108 event->key.time = GDK_CURRENT_TIME;
109 event->key.keyval = keyval;
110 gdk_event_set_device(event, gdk_seat_get_keyboard(gdk_display_get_default_seat(display)));
111 gtk_main_do_event(event);
112 gdk_event_free(event);
113}
114
115static gboolean _match_last_hierarchical_token(GtkEntryCompletion *completion, const gchar *key,
116 GtkTreeIter *iter, gpointer user_data)
117{
118 gchar *candidate = NULL;
119 gtk_tree_model_get(gtk_entry_completion_get_model(completion), iter, 0, &candidate, -1);
120 const gchar *token = g_strrstr(key, ",");
121 token = token ? token + 1 : key;
122 while(*token == ' ') token++;
123 const gboolean matches = g_str_has_prefix(candidate, token);
124 g_free(candidate);
125 return matches;
126}
127
128static int _setup_completion(void **state)
129{
130 dt_tagging_completion_test_t *test = g_malloc0(sizeof(*test));
131 test->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
132 test->entry = GTK_ENTRY(gtk_entry_new());
133 // IBus commits a typed key after gtk_main_do_event() returns; the simple input method commits it
134 // within the event, where GTK takes the device the completion popup needs (see _type_key()).
135 g_object_set(test->entry, "im-module", "gtk-im-context-simple", NULL);
136 test->store = gtk_list_store_new(1, G_TYPE_STRING);
137 _append_completion(test, "subjects|animals");
138 _append_completion(test, "places|france|paris");
139 test->completion = gtk_entry_completion_new();
140 gtk_entry_completion_set_model(test->completion, GTK_TREE_MODEL(test->store));
141 gtk_entry_completion_set_text_column(test->completion, 0);
142 gtk_entry_completion_set_inline_completion(test->completion, TRUE);
143 gtk_entry_completion_set_popup_completion(test->completion, TRUE);
144 gtk_entry_completion_set_minimum_key_length(test->completion, 1);
145 gtk_entry_completion_set_match_func(test->completion, _match_last_hierarchical_token, NULL, NULL);
146 g_signal_connect(test->completion, "match-selected",
147 G_CALLBACK(dt_tagging_completion_match_selected), NULL);
148 gtk_entry_set_completion(test->entry, test->completion);
149 gtk_container_add(GTK_CONTAINER(test->window), GTK_WIDGET(test->entry));
150 gtk_widget_show_all(test->window);
151 gtk_window_set_focus(GTK_WINDOW(test->window), GTK_WIDGET(test->entry));
152 gtk_window_present(GTK_WINDOW(test->window));
153 gdk_window_focus(gtk_widget_get_window(test->window), GDK_CURRENT_TIME);
155 GdkEvent *focus_event = gdk_event_new(GDK_FOCUS_CHANGE);
156 focus_event->focus_change.window = g_object_ref(gtk_widget_get_window(GTK_WIDGET(test->entry)));
157 focus_event->focus_change.in = TRUE;
158 gtk_widget_send_focus_change(GTK_WIDGET(test->entry), focus_event);
159 gdk_event_free(focus_event);
160 assert_true(gtk_widget_has_focus(GTK_WIDGET(test->entry)));
161 *state = test;
162 return 0;
163}
164
165static int _teardown_completion(void **state)
166{
168 gtk_entry_set_completion(test->entry, NULL);
169 gtk_widget_destroy(test->window);
171 g_object_unref(test->completion);
172 g_object_unref(test->store);
173 g_free(test);
174 return 0;
175}
176
177static gboolean _record_plain_return(GtkWidget *entry, GdkEventKey *event, dt_return_handler_state_t *state)
178{
179 if(event->keyval != GDK_KEY_Return) return FALSE;
180 state->entry_calls++;
181 g_free(state->text);
182 state->text = g_strdup(gtk_entry_get_text(GTK_ENTRY(entry)));
183 return FALSE;
184}
185
187{
189
190 gtk_entry_set_text(test->entry, "subjects");
191 dt_tagging_entry_clear(test->entry, NULL, NULL);
192 gtk_list_store_clear(test->store);
193 _append_completion(test, "subjects|animals");
194 _append_completion(test, "subjects|architecture");
196
197 assert_string_equal(gtk_entry_get_text(test->entry), "");
198 assert_ptr_equal(gtk_entry_get_completion(test->entry), test->completion);
199 assert_false(_completion_popup_is_mapped(test->window));
200}
201
203{
205
206 gtk_entry_set_text(test->entry, "subjects");
207 dt_tagging_entry_clear(test->entry, NULL, NULL);
209 assert_string_equal(gtk_entry_get_text(test->entry), "");
210 assert_false(_completion_popup_is_mapped(test->window));
211
212 gtk_entry_set_text(test->entry, "subjects|animals, places");
213 dt_tagging_entry_clear(test->entry, NULL, NULL);
215 assert_string_equal(gtk_entry_get_text(test->entry), "");
216 assert_false(_completion_popup_is_mapped(test->window));
217
218 gint position = 0;
219 gtk_editable_insert_text(GTK_EDITABLE(test->entry), "places", -1, &position);
221 gtk_entry_completion_insert_prefix(test->completion);
223
224 assert_ptr_equal(gtk_entry_get_completion(test->entry), test->completion);
225 assert_string_equal(gtk_entry_get_text(test->entry), "places|france|paris");
226}
227
229{
230 GtkEntry *entry = GTK_ENTRY(gtk_entry_new());
231 gtk_entry_set_text(entry, "places|france");
232
233 dt_tagging_entry_clear(entry, NULL, NULL);
234
235 assert_string_equal(gtk_entry_get_text(entry), "");
236 assert_null(gtk_entry_get_completion(entry));
237 gtk_widget_destroy(GTK_WIDGET(entry));
238}
239
241{
243 gtk_entry_completion_set_inline_completion(test->completion, FALSE);
244 gtk_entry_set_text(test->entry, "subjects|animals, pla");
245 gtk_editable_set_position(GTK_EDITABLE(test->entry), -1);
246 gtk_entry_completion_set_inline_completion(test->completion, TRUE);
247 GtkTreeIter iter;
248 assert_true(gtk_tree_model_get_iter_first(GTK_TREE_MODEL(test->store), &iter));
249 assert_true(gtk_tree_model_iter_next(GTK_TREE_MODEL(test->store), &iter));
250
251 gboolean handled = FALSE;
252 g_signal_emit_by_name(test->completion, "match-selected",
253 GTK_TREE_MODEL(test->store), &iter, &handled);
255
256 assert_true(handled);
257 assert_string_equal(gtk_entry_get_text(test->entry),
258 "subjects|animals,places|france|paris");
259 assert_true(gtk_entry_completion_get_inline_completion(test->completion));
260 assert_ptr_equal(gtk_entry_get_completion(test->entry), test->completion);
261 assert_false(_completion_popup_is_mapped(test->window));
262}
263
265{
267 dt_return_handler_state_t handler_state = { 0 };
268
269 // Connected once, after the completion, as tagging.c connects its entry's key handler.
270 g_signal_connect(test->entry, "key-press-event", G_CALLBACK(_record_plain_return), &handler_state);
271 dt_tagging_entry_clear(test->entry, G_CALLBACK(_record_plain_return), &handler_state);
272 gint position = 0;
273 gtk_editable_insert_text(GTK_EDITABLE(test->entry), "subjects|animals, pl", -1, &position);
274 gtk_editable_set_position(GTK_EDITABLE(test->entry), -1);
275 _type_key(GTK_WIDGET(test->entry), GDK_KEY_a);
277 assert_string_equal(gtk_entry_get_text(test->entry), "subjects|animals, pla");
278 assert_true(_completion_popup_is_mapped(test->window));
279 assert_true(_send_key(GTK_WIDGET(test->entry), GDK_KEY_Down));
280 assert_true(_send_key(GTK_WIDGET(test->entry), GDK_KEY_Return));
282
283 assert_string_equal(gtk_entry_get_text(test->entry),
284 "subjects|animals,places|france|paris");
285 assert_int_equal(handler_state.entry_calls, 0);
286 assert_false(_completion_popup_is_mapped(test->window));
287 assert_true(gtk_entry_completion_get_inline_completion(test->completion));
288
289 _send_key(GTK_WIDGET(test->entry), GDK_KEY_Return);
290 assert_int_equal(handler_state.entry_calls, 1);
291 assert_string_equal(handler_state.text, "subjects|animals,places|france|paris");
292
293 g_signal_handlers_disconnect_by_func(test->entry, _record_plain_return, &handler_state);
294 g_free(handler_state.text);
295}
296
297int main(int argc, char **argv)
298{
299 if(!gtk_init_check(&argc, &argv)) return 77;
300 const struct CMUnitTest tests[] = {
301 cmocka_unit_test_setup_teardown(test_clear_cancels_completion_before_model_refresh,
306 cmocka_unit_test_setup_teardown(test_selected_completion_replaces_only_the_active_token,
308 cmocka_unit_test_setup_teardown(test_completion_keeps_return_priority_after_reattach,
310 };
311 return cmocka_run_group_tests(tests, NULL, NULL);
312}
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
int position()
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
char * key
int main()
Definition prova.c:47
const float uint32_t state[4]
gboolean dt_tagging_completion_match_selected(GtkEntryCompletion *completion, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
void dt_tagging_entry_clear(GtkEntry *entry, GCallback key_handler, gpointer user_data)
static void test_selected_completion_replaces_only_the_active_token(void **state)
static int _teardown_completion(void **state)
static void test_completion_survives_repeated_single_and_hierarchical_clears(void **state)
static void test_clear_cancels_completion_before_model_refresh(void **state)
static gboolean _send_key(GtkWidget *widget, const guint keyval)
static gboolean _quit_main_loop(GMainLoop *loop)
static void _type_key(GtkWidget *widget, const guint keyval)
static void _append_completion(dt_tagging_completion_test_t *test, const char *text)
static void _drain_gtk_events()
static gboolean _match_last_hierarchical_token(GtkEntryCompletion *completion, const gchar *key, GtkTreeIter *iter, gpointer user_data)
static gboolean _record_plain_return(GtkWidget *entry, GdkEventKey *event, dt_return_handler_state_t *state)
static void test_clear_supports_entry_without_completion(void **state)
static void test_completion_keeps_return_priority_after_reattach(void **state)
static void _wait_for_completion_sources()
static int _setup_completion(void **state)
static gboolean _completion_popup_is_mapped(GtkWidget *entry_window)