From 31066a697b42ad8ef6f4d28c069dfa74072b5f32 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Tue, 11 Jun 2019 13:14:25 +0000 Subject: [PATCH 1/2] window: Fix focus wraparound If tab focus falls off the end, and we have an empty headerbar, we end up with window->focus == NULL. Don't let that happen --- gtk/gtkwindow.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gtk/gtkwindow.c b/gtk/gtkwindow.c index 1450873a45..7d9a8f803c 100644 --- a/gtk/gtkwindow.c +++ b/gtk/gtkwindow.c @@ -6305,7 +6305,7 @@ gtk_window_focus (GtkWidget *widget, if (old_focus_child) { if (gtk_widget_child_focus (old_focus_child, direction)) - return TRUE; + return TRUE; } if (priv->focus_widget) @@ -6348,7 +6348,9 @@ gtk_window_focus (GtkWidget *widget, priv->title_box != child && gtk_widget_child_focus (priv->title_box, direction)) return TRUE; - + else if (priv->title_box == child && + gtk_widget_child_focus (gtk_bin_get_child (bin), direction)) + return TRUE; } return FALSE; From 436d7cc0d1fbe8a31819efb2f3873d5bbb259d99 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Tue, 11 Jun 2019 14:11:28 +0000 Subject: [PATCH 2/2] Add a focus chain test Add a test that enumerates the focus chain by emitting move-focus repeatedly, and compares the result to expected output. The test expects a ui file and a reference file as input. The reference file can be created using the --generate option. --- testsuite/gtk/focus-chain/basic.tab | 2 + testsuite/gtk/focus-chain/basic.tab-backward | 2 + testsuite/gtk/focus-chain/basic.ui | 18 ++ testsuite/gtk/meson.build | 29 ++ testsuite/gtk/test-focus-chain.c | 284 +++++++++++++++++++ 5 files changed, 335 insertions(+) create mode 100644 testsuite/gtk/focus-chain/basic.tab create mode 100644 testsuite/gtk/focus-chain/basic.tab-backward create mode 100644 testsuite/gtk/focus-chain/basic.ui create mode 100644 testsuite/gtk/test-focus-chain.c diff --git a/testsuite/gtk/focus-chain/basic.tab b/testsuite/gtk/focus-chain/basic.tab new file mode 100644 index 0000000000..120555fb03 --- /dev/null +++ b/testsuite/gtk/focus-chain/basic.tab @@ -0,0 +1,2 @@ +entry1 GtkText +entry2 GtkText diff --git a/testsuite/gtk/focus-chain/basic.tab-backward b/testsuite/gtk/focus-chain/basic.tab-backward new file mode 100644 index 0000000000..7a00af5387 --- /dev/null +++ b/testsuite/gtk/focus-chain/basic.tab-backward @@ -0,0 +1,2 @@ +entry2 GtkText +entry1 GtkText diff --git a/testsuite/gtk/focus-chain/basic.ui b/testsuite/gtk/focus-chain/basic.ui new file mode 100644 index 0000000000..571707519c --- /dev/null +++ b/testsuite/gtk/focus-chain/basic.ui @@ -0,0 +1,18 @@ + + + + + + + entry1 + + + + + entry2 + + + + + + diff --git a/testsuite/gtk/meson.build b/testsuite/gtk/meson.build index 9be60a278e..60163207fe 100644 --- a/testsuite/gtk/meson.build +++ b/testsuite/gtk/meson.build @@ -130,6 +130,35 @@ if add_languages('cpp', required: false) endif endif +focus_chain_tests = [ + # test direction + [ 'basic', 'tab' ], + [ 'basic', 'tab-backward' ], +] + +focus_chain = executable( + 'test-focus-chain', + ['test-focus-chain.c'], + dependencies: libgtk_dep, + install: get_option('install-tests'), + install_dir: testexecdir +) + +foreach test : focus_chain_tests + test(test[0] + ' ' + test[1], focus_chain, + args: [ join_paths(meson.current_source_dir(), 'focus-chain', test[0] + '.ui'), + join_paths(meson.current_source_dir(), 'focus-chain', test[0] + '.' + test[1]) ], + env: [ 'GIO_USE_VOLUME_MONITOR=unix', + 'GSETTINGS_BACKEND=memory', + 'GTK_CSD=1', + 'G_ENABLE_DIAGNOSTIC=0', + 'G_TEST_SRCDIR=@0@'.format(meson.current_source_dir()), + 'G_TEST_BUILDDIR=@0@'.format(meson.current_build_dir()) + ], + suite: [ 'gtk', 'focus' ]) +endforeach + + if get_option('install-tests') foreach t : tests test_name = t.get(0) diff --git a/testsuite/gtk/test-focus-chain.c b/testsuite/gtk/test-focus-chain.c new file mode 100644 index 0000000000..f8ac319947 --- /dev/null +++ b/testsuite/gtk/test-focus-chain.c @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2019 Red Hat Inc. + * + * Author: + * Matthias Clasen + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ + +#include +#include +#include + +#ifdef G_OS_WIN32 +# include +#endif + +struct { + GtkDirectionType dir; + const char *ext; +} extensions[] = { + { GTK_DIR_TAB_FORWARD, "tab" }, + { GTK_DIR_TAB_BACKWARD, "tab-backward" }, + { GTK_DIR_UP, "up" }, + { GTK_DIR_DOWN, "down" }, + { GTK_DIR_LEFT, "left" }, + { GTK_DIR_RIGHT, "right" } +}; + +static char * +diff_with_file (const char *file1, + char *text, + gssize len, + GError **error) +{ + const char *command[] = { "diff", "-u", file1, NULL, NULL }; + char *diff, *tmpfile; + int fd; + + diff = NULL; + + if (len < 0) + len = strlen (text); + + /* write the text buffer to a temporary file */ + fd = g_file_open_tmp (NULL, &tmpfile, error); + if (fd < 0) + return NULL; + + if (write (fd, text, len) != (int) len) + { + close (fd); + g_set_error (error, + G_FILE_ERROR, G_FILE_ERROR_FAILED, + "Could not write data to temporary file '%s'", tmpfile); + goto done; + } + close (fd); + command[3] = tmpfile; + + /* run diff command */ + g_spawn_sync (NULL, + (char **) command, + NULL, + G_SPAWN_SEARCH_PATH, + NULL, NULL, + &diff, + NULL, NULL, + error); + +done: + g_unlink (tmpfile); + g_free (tmpfile); + + return diff; +} + +static char * +generate_focus_chain (GtkWidget *window, + GtkDirectionType dir) +{ + char *first = NULL; + char *last = NULL; + char *name; + GString *output = g_string_new (""); + GtkWidget *focus; + + gtk_widget_show (window); + + /* start without focus */ + gtk_window_set_focus (GTK_WINDOW (window), NULL); + + while (TRUE) + { + g_signal_emit_by_name (window, "move-focus", dir); + + focus = gtk_window_get_focus (GTK_WINDOW (window)); + + if (focus) + { + /* ui files can't put a name on the embedded text, + * so include the parent entry here + */ + if (GTK_IS_TEXT (focus)) + name = g_strdup_printf ("%s %s", + gtk_widget_get_name (gtk_widget_get_parent (focus)), + gtk_widget_get_name (focus)); + else + name = g_strdup (gtk_widget_get_name (focus)); + } + else + name = g_strdup ("NONE"); + + if (first && g_str_equal (name, first)) + break; /* cycle completed */ + + if (last && g_str_equal (name, last)) + break; /* dead end */ + + g_string_append_printf (output, "%s\n", name); + + if (!first) + first = g_strdup (name); + + g_free (last); + last = g_strdup (name); + + g_free (name); + } + + g_free (first); + g_free (last); + + return g_string_free (output, FALSE); +} + +static GtkDirectionType +get_dir_for_file (const char *path) +{ + char *p; + int i; + + p = strrchr (path, '.'); + p++; + + for (i = 0; i < G_N_ELEMENTS (extensions); i++) + { + if (strcmp (p, extensions[i].ext) == 0) + return extensions[i].dir; + } + + g_error ("Could not find direction for %s", path); + + return 0; +} + +static gboolean +load_ui_file (GFile *ui_file, + GFile *ref_file, + const char *ext) +{ + GtkBuilder *builder; + GtkWidget *window; + char *output; + char *diff; + char *ui_path, *ref_path; + GError *error = NULL; + GtkDirectionType dir; + gboolean success = FALSE; + + ui_path = g_file_get_path (ui_file); + + builder = gtk_builder_new_from_file (ui_path); + window = GTK_WIDGET (gtk_builder_get_object (builder, "window")); + + g_assert (window != NULL); + + if (ext) + { + int i; + + for (i = 0; i < G_N_ELEMENTS (extensions); i++) + { + if (g_str_equal (ext, extensions[i].ext)) + { + output = generate_focus_chain (window, extensions[i].dir); + g_print ("%s", output); + success = TRUE; + goto out; + } + } + + g_error ("Not a supported direction: %s", ext); + goto out; + } + + g_assert (ref_file != NULL); + + ref_path = g_file_get_path (ref_file); + + dir = get_dir_for_file (ref_path); + output = generate_focus_chain (window, dir); + diff = diff_with_file (ref_path, output, -1, &error); + g_assert_no_error (error); + + if (diff && diff[0]) + g_print ("Resulting output doesn't match reference:\n%s", diff); + else + success = TRUE; + g_free (ref_path); + g_free (diff); + +out: + g_free (output); + g_free (ui_path); + + return success; +} + +static const char *arg_generate; + +static const GOptionEntry options[] = { + { "generate", 0, 0, G_OPTION_ARG_STRING, &arg_generate, "DIRECTION" }, + { NULL } +}; + +int +main (int argc, char **argv) +{ + GOptionContext *context; + GFile *ui_file; + GFile *ref_file; + GError *error = NULL; + gboolean success; + + context = g_option_context_new ("- run focus chain tests"); + g_option_context_add_main_entries (context, options, NULL); + g_option_context_set_ignore_unknown_options (context, TRUE); + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_error ("Option parsing failed: %s\n", error->message); + return 1; + } + + gtk_init (); + + if (arg_generate) + { + g_assert (argc == 2); + + ui_file = g_file_new_for_commandline_arg (argv[1]); + + success = load_ui_file (ui_file, NULL, arg_generate); + + g_object_unref (ui_file); + } + else + { + g_assert (argc == 3); + + ui_file = g_file_new_for_commandline_arg (argv[1]); + ref_file = g_file_new_for_commandline_arg (argv[2]); + + success = load_ui_file (ui_file, ref_file, NULL); + + g_object_unref (ui_file); + g_object_unref (ref_file); + } + + return success ? 0 : 1; +} +