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 | |
30 | struct { |
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 | |
42 | static void |
43 | check_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 | |
70 | static char * |
71 | generate_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 | |
153 | static GtkDirectionType |
154 | get_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 | |
173 | static gboolean |
174 | quit_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 | |
183 | static gboolean |
184 | load_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 | |
254 | out: |
255 | g_free (mem: output); |
256 | g_free (mem: ui_path); |
257 | |
258 | return success; |
259 | } |
260 | |
261 | static const char *arg_generate; |
262 | |
263 | static const GOptionEntry options[] = { |
264 | { "generate" , 0, 0, G_OPTION_ARG_STRING, &arg_generate, "DIRECTION" }, |
265 | { NULL } |
266 | }; |
267 | |
268 | int |
269 | main (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 | |