/* gtkshortcutswindow.c * * Copyright (C) 2015 Christian Hergert * * 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 "config.h" #include "gtkshortcutswindowprivate.h" #include "gtkbindings.h" #include "gtkbox.h" #include "gtkgrid.h" #include "gtkheaderbar.h" #include "gtkintl.h" #include "gtklabel.h" #include "gtklistbox.h" #include "gtkmain.h" #include "gtkmenubutton.h" #include "gtkpopover.h" #include "gtkprivate.h" #include "gtkscrolledwindow.h" #include "gtksearchbar.h" #include "gtksearchentry.h" #include "gtkshortcutssection.h" #include "gtkshortcutsgroup.h" #include "gtkshortcutsshortcutprivate.h" #include "gtksizegroup.h" #include "gtkstack.h" #include "gtkstylecontext.h" #include "gtktogglebutton.h" #include "gtktypebuiltins.h" #include "gtkwidgetprivate.h" /** * SECTION:gtkshortcutswindow * @Title: GtkShortcutsWindow * @Short_description: Toplevel which shows help for shortcuts * * A GtkShortcutsWindow shows brief information about the keyboard shortcuts * and gestures of an application. The shortcuts can be grouped, and you can * have multiple sections in this window, corresponding to the major modes of * your application. * * Additionally, the shortcuts can be filtered by the current view, to avoid * showing information that is not relevant in the current application context. * * The recommended way to construct a GtkShortcutsWindow is with GtkBuilder, * by populating a #GtkShortcutsWindow with one or more #GtkShortcutsSection * objects, which contain #GtkShortcutsGroups that in turn contain objects of * class #GtkShortcutsShortcut. * * # A simple example: * * ![](gedit-shortcuts.png) * * This example has as single section. As you can see, the shortcut groups * are arranged in columns, and spread across several pages if there are too * many to find on a single page. * * The .ui file for this example can be found [here](https://gitlab.gnome.org/GNOME/gtk/tree/master/demos/gtk-demo/shortcuts-gedit.ui). * * # An example with multiple views: * * ![](clocks-shortcuts.png) * * This example shows a #GtkShortcutsWindow that has been configured to show only * the shortcuts relevant to the "stopwatch" view. * * The .ui file for this example can be found [here](https://gitlab.gnome.org/GNOME/gtk/tree/master/demos/gtk-demo/shortcuts-clocks.ui). * * # An example with multiple sections: * * ![](builder-shortcuts.png) * * This example shows a #GtkShortcutsWindow with two sections, "Editor Shortcuts" * and "Terminal Shortcuts". * * The .ui file for this example can be found [here](https://gitlab.gnome.org/GNOME/gtk/tree/master/demos/gtk-demo/shortcuts-builder.ui). */ typedef struct { GHashTable *keywords; gchar *initial_section; gchar *last_section_name; gchar *view_name; GtkSizeGroup *search_text_group; GtkSizeGroup *search_image_group; GHashTable *search_items_hash; GtkStack *stack; GtkStack *title_stack; GtkMenuButton *menu_button; GtkLabel *menu_label; GtkSearchBar *search_bar; GtkSearchEntry *search_entry; GtkHeaderBar *header_bar; GtkWidget *main_box; GtkPopover *popover; GtkListBox *list_box; GtkBox *search_gestures; GtkBox *search_shortcuts; GtkWindow *window; gulong keys_changed_id; } GtkShortcutsWindowPrivate; typedef struct { GtkShortcutsWindow *self; GtkBuilder *builder; GQueue *stack; gchar *property_name; guint translatable : 1; } ViewsParserData; G_DEFINE_TYPE_WITH_PRIVATE (GtkShortcutsWindow, gtk_shortcuts_window, GTK_TYPE_WINDOW) enum { CLOSE, SEARCH, LAST_SIGNAL }; enum { PROP_0, PROP_SECTION_NAME, PROP_VIEW_NAME, LAST_PROP }; static GParamSpec *properties[LAST_PROP]; static guint signals[LAST_SIGNAL]; static gint number_of_children (GtkContainer *container) { GList *children; gint n; children = gtk_container_get_children (container); n = g_list_length (children); g_list_free (children); return n; } static void update_title_stack (GtkShortcutsWindow *self) { GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); GtkWidget *visible_child; visible_child = gtk_stack_get_visible_child (priv->stack); if (GTK_IS_SHORTCUTS_SECTION (visible_child)) { if (number_of_children (GTK_CONTAINER (priv->stack)) > 3) { gchar *title; gtk_stack_set_visible_child_name (priv->title_stack, "sections"); g_object_get (visible_child, "title", &title, NULL); gtk_label_set_label (priv->menu_label, title); g_free (title); } else { gtk_stack_set_visible_child_name (priv->title_stack, "title"); } } else if (visible_child != NULL) { gtk_stack_set_visible_child_name (priv->title_stack, "search"); } } static void gtk_shortcuts_window_add_search_item (GtkWidget *child, gpointer data) { GtkShortcutsWindow *self = data; GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); GtkWidget *item; gchar *accelerator = NULL; gchar *title = NULL; gchar *hash_key = NULL; GIcon *icon = NULL; gboolean icon_set = FALSE; gboolean subtitle_set = FALSE; GtkTextDirection direction; GtkShortcutType shortcut_type; gchar *action_name = NULL; gchar *subtitle; gchar *str; gchar *keywords; if (GTK_IS_SHORTCUTS_SHORTCUT (child)) { GEnumClass *class; GEnumValue *value; g_object_get (child, "accelerator", &accelerator, "title", &title, "direction", &direction, "icon-set", &icon_set, "subtitle-set", &subtitle_set, "shortcut-type", &shortcut_type, "action-name", &action_name, NULL); class = G_ENUM_CLASS (g_type_class_ref (GTK_TYPE_SHORTCUT_TYPE)); value = g_enum_get_value (class, shortcut_type); hash_key = g_strdup_printf ("%s-%s-%s", title, value->value_nick, accelerator); g_type_class_unref (class); if (g_hash_table_contains (priv->search_items_hash, hash_key)) { g_free (hash_key); g_free (title); g_free (accelerator); return; } g_hash_table_insert (priv->search_items_hash, hash_key, GINT_TO_POINTER (1)); item = g_object_new (GTK_TYPE_SHORTCUTS_SHORTCUT, "accelerator", accelerator, "title", title, "direction", direction, "shortcut-type", shortcut_type, "accel-size-group", priv->search_image_group, "title-size-group", priv->search_text_group, "action-name", action_name, NULL); if (icon_set) { g_object_get (child, "icon", &icon, NULL); g_object_set (item, "icon", icon, NULL); g_clear_object (&icon); } if (subtitle_set) { g_object_get (child, "subtitle", &subtitle, NULL); g_object_set (item, "subtitle", subtitle, NULL); g_free (subtitle); } str = g_strdup_printf ("%s %s", accelerator, title); keywords = g_utf8_strdown (str, -1); g_hash_table_insert (priv->keywords, item, keywords); if (shortcut_type == GTK_SHORTCUT_ACCELERATOR) gtk_container_add (GTK_CONTAINER (priv->search_shortcuts), item); else gtk_container_add (GTK_CONTAINER (priv->search_gestures), item); g_free (title); g_free (accelerator); g_free (str); g_free (action_name); } else if (GTK_IS_CONTAINER (child)) { gtk_container_foreach (GTK_CONTAINER (child), gtk_shortcuts_window_add_search_item, self); } } static void section_notify_cb (GObject *section, GParamSpec *pspec, gpointer data) { GtkShortcutsWindow *self = data; GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); if (strcmp (pspec->name, "section-name") == 0) { gchar *name; g_object_get (section, "section-name", &name, NULL); g_object_set (gtk_stack_get_page (priv->stack, GTK_WIDGET (section)), "name", name, NULL); g_free (name); } else if (strcmp (pspec->name, "title") == 0) { gchar *title; GtkWidget *label; label = g_object_get_data (section, "gtk-shortcuts-title"); g_object_get (section, "title", &title, NULL); gtk_label_set_label (GTK_LABEL (label), title); g_free (title); } } static void gtk_shortcuts_window_add_section (GtkShortcutsWindow *self, GtkShortcutsSection *section) { GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); GtkListBoxRow *row; gchar *title; gchar *name; const gchar *visible_section; GtkWidget *label; gtk_container_foreach (GTK_CONTAINER (section), gtk_shortcuts_window_add_search_item, self); g_object_get (section, "section-name", &name, "title", &title, NULL); g_signal_connect (section, "notify", G_CALLBACK (section_notify_cb), self); if (name == NULL) name = g_strdup ("shortcuts"); gtk_stack_add_titled (priv->stack, GTK_WIDGET (section), name, title); visible_section = gtk_stack_get_visible_child_name (priv->stack); if (strcmp (visible_section, "internal-search") == 0 || (priv->initial_section && strcmp (priv->initial_section, visible_section) == 0)) gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (section)); row = g_object_new (GTK_TYPE_LIST_BOX_ROW, NULL); g_object_set_data (G_OBJECT (row), "gtk-shortcuts-section", section); label = g_object_new (GTK_TYPE_LABEL, "margin", 6, "label", title, "xalign", 0.5f, NULL); g_object_set_data (G_OBJECT (section), "gtk-shortcuts-title", label); gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (label)); gtk_container_add (GTK_CONTAINER (priv->list_box), GTK_WIDGET (row)); update_title_stack (self); g_free (name); g_free (title); } static void gtk_shortcuts_window_add (GtkContainer *container, GtkWidget *widget) { GtkShortcutsWindow *self = (GtkShortcutsWindow *)container; if (GTK_IS_SHORTCUTS_SECTION (widget)) gtk_shortcuts_window_add_section (self, GTK_SHORTCUTS_SECTION (widget)); else g_warning ("Can't add children of type %s to %s", G_OBJECT_TYPE_NAME (widget), G_OBJECT_TYPE_NAME (container)); } static void gtk_shortcuts_window_remove (GtkContainer *container, GtkWidget *widget) { GtkShortcutsWindow *self = (GtkShortcutsWindow *)container; GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); g_signal_handlers_disconnect_by_func (widget, section_notify_cb, self); if (widget == (GtkWidget *)priv->header_bar || widget == (GtkWidget *)priv->main_box) GTK_CONTAINER_CLASS (gtk_shortcuts_window_parent_class)->remove (container, widget); else gtk_container_remove (GTK_CONTAINER (priv->stack), widget); } static void gtk_shortcuts_window_forall (GtkContainer *container, GtkCallback callback, gpointer callback_data) { GtkShortcutsWindow *self = (GtkShortcutsWindow *)container; GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); if (priv->stack) { GList *children, *l; GtkWidget *search; GtkWidget *empty; search = gtk_stack_get_child_by_name (GTK_STACK (priv->stack), "internal-search"); empty = gtk_stack_get_child_by_name (GTK_STACK (priv->stack), "no-search-results"); children = gtk_container_get_children (GTK_CONTAINER (priv->stack)); for (l = children; l; l = l->next) { GtkWidget *child = l->data; if (child != search && child != empty) callback (child, callback_data); } g_list_free (children); } } static void gtk_shortcuts_window_set_view_name (GtkShortcutsWindow *self, const gchar *view_name) { GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); GList *sections, *l; g_free (priv->view_name); priv->view_name = g_strdup (view_name); sections = gtk_container_get_children (GTK_CONTAINER (priv->stack)); for (l = sections; l; l = l->next) { GtkShortcutsSection *section = l->data; if (GTK_IS_SHORTCUTS_SECTION (section)) g_object_set (section, "view-name", priv->view_name, NULL); } g_list_free (sections); } static void gtk_shortcuts_window_set_section_name (GtkShortcutsWindow *self, const gchar *section_name) { GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); GtkWidget *section = NULL; g_free (priv->initial_section); priv->initial_section = g_strdup (section_name); if (section_name) section = gtk_stack_get_child_by_name (priv->stack, section_name); if (section) gtk_stack_set_visible_child (priv->stack, section); } static void update_accels_cb (GtkWidget *widget, gpointer data) { GtkShortcutsWindow *self = data; GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); if (GTK_IS_SHORTCUTS_SHORTCUT (widget)) gtk_shortcuts_shortcut_update_accel (GTK_SHORTCUTS_SHORTCUT (widget), priv->window); else if (GTK_IS_CONTAINER (widget)) gtk_container_foreach (GTK_CONTAINER (widget), update_accels_cb, self); } static void update_accels_for_actions (GtkShortcutsWindow *self) { GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); if (priv->window) gtk_container_forall (GTK_CONTAINER (self), update_accels_cb, self); } static void keys_changed_handler (GtkWindow *window, GtkShortcutsWindow *self) { update_accels_for_actions (self); } void gtk_shortcuts_window_set_window (GtkShortcutsWindow *self, GtkWindow *window) { GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); if (priv->keys_changed_id) { g_signal_handler_disconnect (priv->window, priv->keys_changed_id); priv->keys_changed_id = 0; } priv->window = window; if (priv->window) priv->keys_changed_id = g_signal_connect (window, "keys-changed", G_CALLBACK (keys_changed_handler), self); update_accels_for_actions (self); } static void gtk_shortcuts_window__list_box__row_activated (GtkShortcutsWindow *self, GtkListBoxRow *row, GtkListBox *list_box) { GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); GtkWidget *section; section = g_object_get_data (G_OBJECT (row), "gtk-shortcuts-section"); gtk_stack_set_visible_child (priv->stack, section); gtk_popover_popdown (priv->popover); } static gboolean hidden_by_direction (GtkWidget *widget) { if (GTK_IS_SHORTCUTS_SHORTCUT (widget)) { GtkTextDirection dir; g_object_get (widget, "direction", &dir, NULL); if (dir != GTK_TEXT_DIR_NONE && dir != gtk_widget_get_direction (widget)) return TRUE; } return FALSE; } static void gtk_shortcuts_window__entry__changed (GtkShortcutsWindow *self, GtkSearchEntry *search_entry) { GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); gchar *downcase = NULL; GHashTableIter iter; const gchar *text; const gchar *last_section_name; gpointer key; gpointer value; gboolean has_result; text = gtk_editable_get_text (GTK_EDITABLE (search_entry)); if (!text || !*text) { if (priv->last_section_name != NULL) { gtk_stack_set_visible_child_name (priv->stack, priv->last_section_name); return; } } last_section_name = gtk_stack_get_visible_child_name (priv->stack); if (g_strcmp0 (last_section_name, "internal-search") != 0 && g_strcmp0 (last_section_name, "no-search-results") != 0) { g_free (priv->last_section_name); priv->last_section_name = g_strdup (last_section_name); } downcase = g_utf8_strdown (text, -1); g_hash_table_iter_init (&iter, priv->keywords); has_result = FALSE; while (g_hash_table_iter_next (&iter, &key, &value)) { GtkWidget *widget = key; const gchar *keywords = value; gboolean match; if (hidden_by_direction (widget)) match = FALSE; else match = strstr (keywords, downcase) != NULL; gtk_widget_set_visible (widget, match); has_result |= match; } g_free (downcase); if (has_result) gtk_stack_set_visible_child_name (priv->stack, "internal-search"); else gtk_stack_set_visible_child_name (priv->stack, "no-search-results"); } static void gtk_shortcuts_window__search_mode__changed (GtkShortcutsWindow *self) { GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); if (!gtk_search_bar_get_search_mode (priv->search_bar)) { if (priv->last_section_name != NULL) gtk_stack_set_visible_child_name (priv->stack, priv->last_section_name); } } static void gtk_shortcuts_window_close (GtkShortcutsWindow *self) { gtk_window_close (GTK_WINDOW (self)); } static void gtk_shortcuts_window_search (GtkShortcutsWindow *self) { GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); gtk_search_bar_set_search_mode (priv->search_bar, TRUE); } static void gtk_shortcuts_window_constructed (GObject *object) { GtkShortcutsWindow *self = (GtkShortcutsWindow *)object; GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); G_OBJECT_CLASS (gtk_shortcuts_window_parent_class)->constructed (object); if (priv->initial_section != NULL) gtk_stack_set_visible_child_name (priv->stack, priv->initial_section); } static void gtk_shortcuts_window_finalize (GObject *object) { GtkShortcutsWindow *self = (GtkShortcutsWindow *)object; GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); g_clear_pointer (&priv->keywords, g_hash_table_unref); g_clear_pointer (&priv->initial_section, g_free); g_clear_pointer (&priv->view_name, g_free); g_clear_pointer (&priv->last_section_name, g_free); g_clear_pointer (&priv->search_items_hash, g_hash_table_unref); g_clear_object (&priv->search_image_group); g_clear_object (&priv->search_text_group); G_OBJECT_CLASS (gtk_shortcuts_window_parent_class)->finalize (object); } static void gtk_shortcuts_window_dispose (GObject *object) { GtkShortcutsWindow *self = (GtkShortcutsWindow *)object; GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); if (priv->stack) g_signal_handlers_disconnect_by_func (priv->stack, G_CALLBACK (update_title_stack), self); gtk_shortcuts_window_set_window (self, NULL); if (priv->header_bar) { gtk_container_remove (GTK_CONTAINER (self), GTK_WIDGET (priv->header_bar)); priv->header_bar = NULL; priv->popover = NULL; } if (priv->main_box) { gtk_container_remove (GTK_CONTAINER (self), GTK_WIDGET (priv->main_box)); priv->main_box = NULL; priv->stack = NULL; priv->search_bar = NULL; } G_OBJECT_CLASS (gtk_shortcuts_window_parent_class)->dispose (object); } static void gtk_shortcuts_window_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GtkShortcutsWindow *self = (GtkShortcutsWindow *)object; GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); switch (prop_id) { case PROP_SECTION_NAME: { GtkWidget *child = gtk_stack_get_visible_child (priv->stack); if (child != NULL) { gchar *name = NULL; g_object_get (gtk_stack_get_page (priv->stack, child), "name", &name, NULL); g_value_take_string (value, name); } } break; case PROP_VIEW_NAME: g_value_set_string (value, priv->view_name); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gtk_shortcuts_window_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GtkShortcutsWindow *self = (GtkShortcutsWindow *)object; switch (prop_id) { case PROP_SECTION_NAME: gtk_shortcuts_window_set_section_name (self, g_value_get_string (value)); break; case PROP_VIEW_NAME: gtk_shortcuts_window_set_view_name (self, g_value_get_string (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gtk_shortcuts_window_unmap (GtkWidget *widget) { GtkShortcutsWindow *self = (GtkShortcutsWindow *)widget; GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); if (priv->search_bar) gtk_search_bar_set_search_mode (priv->search_bar, FALSE); GTK_WIDGET_CLASS (gtk_shortcuts_window_parent_class)->unmap (widget); } static GType gtk_shortcuts_window_child_type (GtkContainer *container) { return GTK_TYPE_SHORTCUTS_SECTION; } static void gtk_shortcuts_window_class_init (GtkShortcutsWindowClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); GtkBindingSet *binding_set = gtk_binding_set_by_class (klass); object_class->constructed = gtk_shortcuts_window_constructed; object_class->finalize = gtk_shortcuts_window_finalize; object_class->get_property = gtk_shortcuts_window_get_property; object_class->set_property = gtk_shortcuts_window_set_property; object_class->dispose = gtk_shortcuts_window_dispose; widget_class->unmap = gtk_shortcuts_window_unmap; container_class->add = gtk_shortcuts_window_add; container_class->remove = gtk_shortcuts_window_remove; container_class->child_type = gtk_shortcuts_window_child_type; container_class->forall = gtk_shortcuts_window_forall; klass->close = gtk_shortcuts_window_close; klass->search = gtk_shortcuts_window_search; /** * GtkShortcutsWindow:section-name: * * The name of the section to show. * * This should be the section-name of one of the #GtkShortcutsSection * objects that are in this shortcuts window. */ properties[PROP_SECTION_NAME] = g_param_spec_string ("section-name", P_("Section Name"), P_("Section Name"), "internal-search", (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); /** * GtkShortcutsWindow:view-name: * * The view name by which to filter the contents. * * This should correspond to the #GtkShortcutsGroup:view property of some of * the #GtkShortcutsGroup objects that are inside this shortcuts window. * * Set this to %NULL to show all groups. */ properties[PROP_VIEW_NAME] = g_param_spec_string ("view-name", P_("View Name"), P_("View Name"), NULL, (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_properties (object_class, LAST_PROP, properties); /** * GtkShortcutsWindow::close: * * The ::close signal is a * [keybinding signal][GtkBindingSignal] * which gets emitted when the user uses a keybinding to close * the window. * * The default binding for this signal is the Escape key. */ signals[CLOSE] = g_signal_new (I_("close"), G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkShortcutsWindowClass, close), NULL, NULL, NULL, G_TYPE_NONE, 0); /** * GtkShortcutsWindow::search: * * The ::search signal is a * [keybinding signal][GtkBindingSignal] * which gets emitted when the user uses a keybinding to start a search. * * The default binding for this signal is Control-F. */ signals[SEARCH] = g_signal_new (I_("search"), G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkShortcutsWindowClass, search), NULL, NULL, NULL, G_TYPE_NONE, 0); gtk_binding_entry_add_signal (binding_set, GDK_KEY_Escape, 0, "close", 0); gtk_binding_entry_add_signal (binding_set, GDK_KEY_f, GDK_CONTROL_MASK, "search", 0); g_type_ensure (GTK_TYPE_SHORTCUTS_GROUP); g_type_ensure (GTK_TYPE_SHORTCUTS_SHORTCUT); } static void gtk_shortcuts_window_init (GtkShortcutsWindow *self) { GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self); GtkWidget *search_button; GtkBox *menu_box; GtkBox *box; GtkWidget *arrow; GtkWidget *scroller; GtkWidget *label; GtkWidget *empty; PangoAttrList *attributes; gtk_window_set_resizable (GTK_WINDOW (self), FALSE); gtk_window_set_type_hint (GTK_WINDOW (self), GDK_SURFACE_TYPE_HINT_DIALOG); priv->keywords = g_hash_table_new_full (NULL, NULL, NULL, g_free); priv->search_items_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); priv->search_text_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); priv->search_image_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); priv->header_bar = g_object_new (GTK_TYPE_HEADER_BAR, "show-title-buttons", TRUE, NULL); gtk_window_set_titlebar (GTK_WINDOW (self), GTK_WIDGET (priv->header_bar)); search_button = g_object_new (GTK_TYPE_TOGGLE_BUTTON, "icon-name", "edit-find-symbolic", NULL); gtk_container_add (GTK_CONTAINER (priv->header_bar), search_button); priv->main_box = g_object_new (GTK_TYPE_BOX, "orientation", GTK_ORIENTATION_VERTICAL, NULL); GTK_CONTAINER_CLASS (gtk_shortcuts_window_parent_class)->add (GTK_CONTAINER (self), GTK_WIDGET (priv->main_box)); priv->search_bar = g_object_new (GTK_TYPE_SEARCH_BAR, NULL); g_object_bind_property (priv->search_bar, "search-mode-enabled", search_button, "active", G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); gtk_container_add (GTK_CONTAINER (priv->main_box), GTK_WIDGET (priv->search_bar)); gtk_search_bar_set_key_capture_widget (GTK_SEARCH_BAR (priv->search_bar), GTK_WIDGET (self)); priv->stack = g_object_new (GTK_TYPE_STACK, "expand", TRUE, "homogeneous", TRUE, "transition-type", GTK_STACK_TRANSITION_TYPE_CROSSFADE, NULL); gtk_container_add (GTK_CONTAINER (priv->main_box), GTK_WIDGET (priv->stack)); priv->title_stack = g_object_new (GTK_TYPE_STACK, NULL); gtk_header_bar_set_custom_title (priv->header_bar, GTK_WIDGET (priv->title_stack)); /* Translators: This is the window title for the shortcuts window in normal mode */ label = gtk_label_new (_("Shortcuts")); gtk_style_context_add_class (gtk_widget_get_style_context (label), GTK_STYLE_CLASS_TITLE); gtk_stack_add_named (priv->title_stack, label, "title"); /* Translators: This is the window title for the shortcuts window in search mode */ label = gtk_label_new (_("Search Results")); gtk_style_context_add_class (gtk_widget_get_style_context (label), GTK_STYLE_CLASS_TITLE); gtk_stack_add_named (priv->title_stack, label, "search"); priv->menu_button = g_object_new (GTK_TYPE_MENU_BUTTON, "focus-on-click", FALSE, "relief", GTK_RELIEF_NONE, NULL); gtk_stack_add_named (priv->title_stack, GTK_WIDGET (priv->menu_button), "sections"); menu_box = g_object_new (GTK_TYPE_BOX, "orientation", GTK_ORIENTATION_HORIZONTAL, "spacing", 6, "visible", TRUE, NULL); gtk_container_add (GTK_CONTAINER (priv->menu_button), GTK_WIDGET (menu_box)); priv->menu_label = g_object_new (GTK_TYPE_LABEL, NULL); gtk_container_add (GTK_CONTAINER (menu_box), GTK_WIDGET (priv->menu_label)); arrow = gtk_image_new_from_icon_name ("pan-down-symbolic"); gtk_container_add (GTK_CONTAINER (menu_box), GTK_WIDGET (arrow)); priv->popover = g_object_new (GTK_TYPE_POPOVER, "relative-to", priv->menu_button, "position", GTK_POS_BOTTOM, NULL); gtk_menu_button_set_popover (priv->menu_button, GTK_WIDGET (priv->popover)); priv->list_box = g_object_new (GTK_TYPE_LIST_BOX, "selection-mode", GTK_SELECTION_NONE, NULL); g_signal_connect_object (priv->list_box, "row-activated", G_CALLBACK (gtk_shortcuts_window__list_box__row_activated), self, G_CONNECT_SWAPPED); gtk_container_add (GTK_CONTAINER (priv->popover), GTK_WIDGET (priv->list_box)); priv->search_entry = GTK_SEARCH_ENTRY (gtk_search_entry_new ()); gtk_container_add (GTK_CONTAINER (priv->search_bar), GTK_WIDGET (priv->search_entry)); g_object_set (priv->search_entry, /* Translators: This is placeholder text for the search entry in the shortcuts window */ "placeholder-text", _("Search Shortcuts"), "width-chars", 40, NULL); g_signal_connect_object (priv->search_entry, "search-changed", G_CALLBACK (gtk_shortcuts_window__entry__changed), self, G_CONNECT_SWAPPED); g_signal_connect_object (priv->search_bar, "notify::search-mode-enabled", G_CALLBACK (gtk_shortcuts_window__search_mode__changed), self, G_CONNECT_SWAPPED); scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW, NULL); box = g_object_new (GTK_TYPE_BOX, "halign", GTK_ALIGN_CENTER, "orientation", GTK_ORIENTATION_VERTICAL, NULL); gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (box)), "shortcuts-search-results"); gtk_container_add (GTK_CONTAINER (scroller), GTK_WIDGET (box)); gtk_stack_add_named (priv->stack, scroller, "internal-search"); priv->search_shortcuts = g_object_new (GTK_TYPE_BOX, "halign", GTK_ALIGN_CENTER, "spacing", 6, "orientation", GTK_ORIENTATION_VERTICAL, NULL); gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (priv->search_shortcuts)); priv->search_gestures = g_object_new (GTK_TYPE_BOX, "halign", GTK_ALIGN_CENTER, "spacing", 6, "orientation", GTK_ORIENTATION_VERTICAL, NULL); gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (priv->search_gestures)); empty = g_object_new (GTK_TYPE_GRID, "row-spacing", 12, "margin", 12, "hexpand", TRUE, "vexpand", TRUE, "halign", GTK_ALIGN_CENTER, "valign", GTK_ALIGN_CENTER, NULL); gtk_style_context_add_class (gtk_widget_get_style_context (empty), GTK_STYLE_CLASS_DIM_LABEL); gtk_grid_attach (GTK_GRID (empty), g_object_new (GTK_TYPE_IMAGE, "icon-name", "edit-find-symbolic", "pixel-size", 72, NULL), 0, 0, 1, 1); attributes = pango_attr_list_new (); pango_attr_list_insert (attributes, pango_attr_weight_new (PANGO_WEIGHT_BOLD)); pango_attr_list_insert (attributes, pango_attr_scale_new (1.44)); label = g_object_new (GTK_TYPE_LABEL, "label", _("No Results Found"), "attributes", attributes, NULL); pango_attr_list_unref (attributes); gtk_grid_attach (GTK_GRID (empty), label, 0, 1, 1, 1); label = g_object_new (GTK_TYPE_LABEL, "label", _("Try a different search"), NULL); gtk_grid_attach (GTK_GRID (empty), label, 0, 2, 1, 1); gtk_stack_add_named (priv->stack, empty, "no-search-results"); g_signal_connect_object (priv->stack, "notify::visible-child", G_CALLBACK (update_title_stack), self, G_CONNECT_SWAPPED); }