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
check_focus_states(GtkWidget * focus_widget)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 (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 (focus_widget);
60   while (parent)
61     {
62       state = gtk_widget_get_state_flags (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 (parent);
67     }
68 }
69 
70 static char *
generate_focus_chain(GtkWidget * window,GtkDirectionType dir)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 ("");
79   GtkWidget *focus;
80   int count = 0;
81 
82   gtk_widget_show (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 (window, "move-focus", dir);
90 
91       focus = gtk_window_get_focus (GTK_WINDOW (window));
92 
93       check_focus_states (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 ("%s %s",
102                                     gtk_widget_get_name (gtk_widget_get_parent (focus)),
103                                     gtk_widget_get_name (focus));
104           else
105             name = g_strdup (gtk_widget_get_name (focus));
106 
107           key = g_strdup_printf ("%s %p", name, focus);
108         }
109       else
110         {
111           name = g_strdup ("NONE");
112           key = g_strdup (key);
113         }
114 
115       if (first && g_str_equal (key, first))
116         {
117           g_string_append (output, "WRAP\n");
118           break; /* cycle completed */
119         }
120 
121       if (last && g_str_equal (key, last))
122         {
123           g_string_append (output, "STOP\n");
124           break; /* dead end */
125         }
126 
127       g_string_append_printf (output, "%s\n", name);
128       count++;
129 
130       if (!first)
131         first = g_strdup (key);
132 
133       g_free (last);
134       last = key;
135 
136       if (count == 100)
137         {
138           g_string_append (output, "ABORT\n");
139           break;
140         }
141 
142       g_free (name);
143     }
144 
145   g_free (name);
146   g_free (key);
147   g_free (first);
148   g_free (last);
149 
150   return g_string_free (output, FALSE);
151 }
152 
153 static GtkDirectionType
get_dir_for_file(const char * path)154 get_dir_for_file (const char *path)
155 {
156   char *p;
157   int i;
158 
159   p = strrchr (path, '.');
160   p++;
161 
162   for (i = 0; i < G_N_ELEMENTS (extensions); i++)
163     {
164       if (strcmp (p, 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
quit_iteration_loop(gpointer user_data)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
load_ui_file(GFile * ui_file,GFile * ref_file,const char * ext)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 (ui_file);
200 
201   builder = gtk_builder_new_from_file (ui_path);
202   window = GTK_WIDGET (gtk_builder_get_object (builder, "window"));
203 
204   g_assert_nonnull (window);
205 
206   gtk_widget_show (window);
207 
208   timeout_handle_id = g_timeout_add (2000,
209                                      quit_iteration_loop,
210                                      &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 (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 (ext, extensions[i].ext))
226             {
227               output = generate_focus_chain (window, extensions[i].dir);
228               g_print ("%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 (ref_file);
241 
242   dir = get_dir_for_file (ref_path);
243   output = generate_focus_chain (window, dir);
244   diff = diff_with_file (ref_path, output, -1, &error);
245   g_assert_no_error (error);
246 
247   if (diff && diff[0])
248     g_print ("Resulting output doesn't match reference:\n%s", diff);
249   else
250      success = TRUE;
251   g_free (ref_path);
252   g_free (diff);
253 
254 out:
255   g_free (output);
256   g_free (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
main(int argc,char ** argv)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 ("- run focus chain tests");
278   g_option_context_add_main_entries (context, options, NULL);
279   g_option_context_set_ignore_unknown_options (context, TRUE);
280 
281   if (!g_option_context_parse (context, &argc, &argv, &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 (argv[1]);
295 
296       success = load_ui_file (ui_file, NULL, arg_generate);
297 
298       g_object_unref (ui_file);
299     }
300   else
301     {
302       g_assert_cmpint (argc, ==, 3);
303 
304       ui_file = g_file_new_for_commandline_arg (argv[1]);
305       ref_file = g_file_new_for_commandline_arg (argv[2]);
306 
307       success = load_ui_file (ui_file, ref_file, NULL);
308 
309       g_object_unref (ui_file);
310       g_object_unref (ref_file);
311     }
312 
313   return success ? 0 : 1;
314 }
315 
316