1/*
2 * Copyright (C) 2019 Red Hat Inc.
3 *
4 * Author:
5 * Matthias Clasen <mclasen@redhat.com>
6 *
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Library General Public
9 * License as published by the Free Software Foundation; either
10 * version 2 of the License, or (at your option) any later version.
11 *
12 * This library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Library General Public License for more details.
16 *
17 * You should have received a copy of the GNU Library General Public
18 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#include <string.h>
22#include <glib/gstdio.h>
23#include <gtk/gtk.h>
24#include "testsuite/testutils.h"
25
26#ifdef G_OS_WIN32
27# include <io.h>
28#endif
29
30struct {
31 GtkDirectionType dir;
32 const char *ext;
33} extensions[] = {
34 { GTK_DIR_TAB_FORWARD, "tab" },
35 { GTK_DIR_TAB_BACKWARD, "tab-backward" },
36 { GTK_DIR_UP, "up" },
37 { GTK_DIR_DOWN, "down" },
38 { GTK_DIR_LEFT, "left" },
39 { GTK_DIR_RIGHT, "right" }
40};
41
42static void
43check_focus_states (GtkWidget *focus_widget)
44{
45 GtkStateFlags state;
46 GtkWidget *parent;
47
48 if (focus_widget == NULL)
49 return;
50
51 /* Check that we set :focus and :focus-within on the focus_widget,
52 * and :focus-within on its ancestors
53 */
54
55 state = gtk_widget_get_state_flags (widget: focus_widget);
56 g_assert_true ((state & (GTK_STATE_FLAG_FOCUSED|GTK_STATE_FLAG_FOCUS_WITHIN)) ==
57 (GTK_STATE_FLAG_FOCUSED|GTK_STATE_FLAG_FOCUS_WITHIN));
58
59 parent = gtk_widget_get_parent (widget: focus_widget);
60 while (parent)
61 {
62 state = gtk_widget_get_state_flags (widget: parent);
63 g_assert_true ((state & GTK_STATE_FLAG_FOCUS_WITHIN) == GTK_STATE_FLAG_FOCUS_WITHIN);
64 g_assert_true ((state & GTK_STATE_FLAG_FOCUSED) == 0);
65
66 parent = gtk_widget_get_parent (widget: parent);
67 }
68}
69
70static char *
71generate_focus_chain (GtkWidget *window,
72 GtkDirectionType dir)
73{
74 char *first = NULL;
75 char *last = NULL;
76 char *name = NULL;
77 char *key = NULL;
78 GString *output = g_string_new (init: "");
79 GtkWidget *focus;
80 int count = 0;
81
82 gtk_widget_show (widget: window);
83
84 /* start without focus */
85 gtk_window_set_focus (GTK_WINDOW (window), NULL);
86
87 while (TRUE)
88 {
89 g_signal_emit_by_name (instance: window, detailed_signal: "move-focus", dir);
90
91 focus = gtk_window_get_focus (GTK_WINDOW (window));
92
93 check_focus_states (focus_widget: focus);
94
95 if (focus)
96 {
97 /* ui files can't put a name on the embedded text,
98 * so include the parent entry here
99 */
100 if (GTK_IS_TEXT (focus))
101 name = g_strdup_printf (format: "%s %s",
102 gtk_widget_get_name (widget: gtk_widget_get_parent (widget: focus)),
103 gtk_widget_get_name (widget: focus));
104 else
105 name = g_strdup (str: gtk_widget_get_name (widget: focus));
106
107 key = g_strdup_printf (format: "%s %p", name, focus);
108 }
109 else
110 {
111 name = g_strdup (str: "NONE");
112 key = g_strdup (str: key);
113 }
114
115 if (first && g_str_equal (v1: key, v2: first))
116 {
117 g_string_append (string: output, val: "WRAP\n");
118 break; /* cycle completed */
119 }
120
121 if (last && g_str_equal (v1: key, v2: last))
122 {
123 g_string_append (string: output, val: "STOP\n");
124 break; /* dead end */
125 }
126
127 g_string_append_printf (string: output, format: "%s\n", name);
128 count++;
129
130 if (!first)
131 first = g_strdup (str: key);
132
133 g_free (mem: last);
134 last = key;
135
136 if (count == 100)
137 {
138 g_string_append (string: output, val: "ABORT\n");
139 break;
140 }
141
142 g_free (mem: name);
143 }
144
145 g_free (mem: name);
146 g_free (mem: key);
147 g_free (mem: first);
148 g_free (mem: last);
149
150 return g_string_free (string: output, FALSE);
151}
152
153static GtkDirectionType
154get_dir_for_file (const char *path)
155{
156 char *p;
157 int i;
158
159 p = strrchr (s: path, c: '.');
160 p++;
161
162 for (i = 0; i < G_N_ELEMENTS (extensions); i++)
163 {
164 if (strcmp (s1: p, s2: extensions[i].ext) == 0)
165 return extensions[i].dir;
166 }
167
168 g_error ("Could not find direction for %s", path);
169
170 return 0;
171}
172
173static gboolean
174quit_iteration_loop (gpointer user_data)
175{
176 gboolean *keep_running = user_data;
177
178 *keep_running = FALSE;
179
180 return G_SOURCE_REMOVE;
181}
182
183static gboolean
184load_ui_file (GFile *ui_file,
185 GFile *ref_file,
186 const char *ext)
187{
188 GtkBuilder *builder;
189 GtkWidget *window;
190 char *output;
191 char *diff;
192 char *ui_path, *ref_path;
193 GError *error = NULL;
194 GtkDirectionType dir;
195 gboolean success = FALSE;
196 gboolean keep_running = TRUE;
197 guint timeout_handle_id;
198
199 ui_path = g_file_get_path (file: ui_file);
200
201 builder = gtk_builder_new_from_file (filename: ui_path);
202 window = GTK_WIDGET (gtk_builder_get_object (builder, "window"));
203
204 g_assert_nonnull (window);
205
206 gtk_widget_show (widget: window);
207
208 timeout_handle_id = g_timeout_add (interval: 2000,
209 function: quit_iteration_loop,
210 data: &keep_running);
211 while (keep_running)
212 {
213 if (!g_main_context_iteration (NULL, FALSE))
214 break;
215 }
216 if (keep_running)
217 g_source_remove (tag: timeout_handle_id);
218
219 if (ext)
220 {
221 int i;
222
223 for (i = 0; i < G_N_ELEMENTS (extensions); i++)
224 {
225 if (g_str_equal (v1: ext, v2: extensions[i].ext))
226 {
227 output = generate_focus_chain (window, dir: extensions[i].dir);
228 g_print (format: "%s", output);
229 success = TRUE;
230 goto out;
231 }
232 }
233
234 g_error ("Not a supported direction: %s", ext);
235 goto out;
236 }
237
238 g_assert_nonnull (ref_file);
239
240 ref_path = g_file_get_path (file: ref_file);
241
242 dir = get_dir_for_file (path: ref_path);
243 output = generate_focus_chain (window, dir);
244 diff = diff_with_file (file1: ref_path, text: output, len: -1, error: &error);
245 g_assert_no_error (error);
246
247 if (diff && diff[0])
248 g_print (format: "Resulting output doesn't match reference:\n%s", diff);
249 else
250 success = TRUE;
251 g_free (mem: ref_path);
252 g_free (mem: diff);
253
254out:
255 g_free (mem: output);
256 g_free (mem: ui_path);
257
258 return success;
259}
260
261static const char *arg_generate;
262
263static const GOptionEntry options[] = {
264 { "generate", 0, 0, G_OPTION_ARG_STRING, &arg_generate, "DIRECTION" },
265 { NULL }
266};
267
268int
269main (int argc, char **argv)
270{
271 GOptionContext *context;
272 GFile *ui_file;
273 GFile *ref_file;
274 GError *error = NULL;
275 gboolean success;
276
277 context = g_option_context_new (parameter_string: "- run focus chain tests");
278 g_option_context_add_main_entries (context, entries: options, NULL);
279 g_option_context_set_ignore_unknown_options (context, TRUE);
280
281 if (!g_option_context_parse (context, argc: &argc, argv: &argv, error: &error))
282 {
283 g_error ("Option parsing failed: %s\n", error->message);
284 return 1;
285 }
286 g_option_context_free (context);
287
288 gtk_init ();
289
290 if (arg_generate)
291 {
292 g_assert_cmpint (argc, ==, 2);
293
294 ui_file = g_file_new_for_commandline_arg (arg: argv[1]);
295
296 success = load_ui_file (ui_file, NULL, ext: arg_generate);
297
298 g_object_unref (object: ui_file);
299 }
300 else
301 {
302 g_assert_cmpint (argc, ==, 3);
303
304 ui_file = g_file_new_for_commandline_arg (arg: argv[1]);
305 ref_file = g_file_new_for_commandline_arg (arg: argv[2]);
306
307 success = load_ui_file (ui_file, ref_file, NULL);
308
309 g_object_unref (object: ui_file);
310 g_object_unref (object: ref_file);
311 }
312
313 return success ? 0 : 1;
314}
315
316

source code of gtk/testsuite/gtk/test-focus-chain.c