gtk2/gtk/gtkmenusectionbox.c
Christian Hergert 17bcd0d127 menusectionbox: add support for "text-direction" attribute
This allows the use of a "text-direction" hint set to one of "none", "rtl",
or "ltr" to enforce the text direction of a "horizontal-buttons"
display-hint.

This is useful when a menu has buttons that map to physical space in the
UI and therefore must match the application widgetry.

https://bugzilla.gnome.org/show_bug.cgi?id=772775
2016-10-14 10:44:57 -04:00

554 lines
18 KiB
C

/*
* Copyright © 2014 Canonical Limited
* Copyright © 2013 Carlos Garnacho
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the licence, 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Author: Ryan Lortie <desrt@desrt.ca>
*/
#include "config.h"
#include "gtkmenusectionbox.h"
#include "gtkwidgetprivate.h"
#include "gtklabel.h"
#include "gtkmenutracker.h"
#include "gtkmodelbutton.h"
#include "gtkseparator.h"
#include "gtksizegroup.h"
#include "gtkstack.h"
#include "gtkstylecontext.h"
#include "gtkpopover.h"
#include "gtkorientable.h"
typedef GtkBoxClass GtkMenuSectionBoxClass;
struct _GtkMenuSectionBox
{
GtkBox parent_instance;
GtkMenuSectionBox *toplevel;
GtkMenuTracker *tracker;
GtkBox *item_box;
GtkWidget *separator;
guint separator_sync_idle;
gboolean iconic;
gint depth;
};
typedef struct
{
gint n_items;
gboolean previous_is_iconic;
} MenuData;
G_DEFINE_TYPE (GtkMenuSectionBox, gtk_menu_section_box, GTK_TYPE_BOX)
static void gtk_menu_section_box_sync_separators (GtkMenuSectionBox *box,
MenuData *data);
static void gtk_menu_section_box_new_submenu (GtkMenuTrackerItem *item,
GtkMenuSectionBox *toplevel,
GtkWidget *focus,
const gchar *name);
static GtkWidget * gtk_menu_section_box_new_section (GtkMenuTrackerItem *item,
GtkMenuSectionBox *parent);
static void
gtk_menu_section_box_sync_item (GtkWidget *widget,
gpointer user_data)
{
MenuData *data = (MenuData *)user_data;
if (GTK_IS_MENU_SECTION_BOX (widget))
gtk_menu_section_box_sync_separators (GTK_MENU_SECTION_BOX (widget), data);
else
data->n_items++;
}
/* We are trying to implement the following rules here:
*
* rule 1: never ever show separators for empty sections
* rule 2: always show a separator if there is a label
* rule 3: don't show a separator for the first section
* rule 4: don't show a separator for the following sections if there are
* no items before it
* rule 5: never show separators directly above or below an iconic box
* (rule 6: these rules don't apply exactly the same way for subsections)
*/
static void
gtk_menu_section_box_sync_separators (GtkMenuSectionBox *box,
MenuData *data)
{
gboolean previous_section_is_iconic;
gboolean should_have_separator;
gboolean should_have_top_margin = FALSE;
gboolean is_not_empty_item;
gboolean has_separator;
gboolean has_label;
gboolean separator_condition;
gint n_items_before;
n_items_before = data->n_items;
previous_section_is_iconic = data->previous_is_iconic;
gtk_container_foreach (GTK_CONTAINER (box->item_box), gtk_menu_section_box_sync_item, data);
is_not_empty_item = (data->n_items > n_items_before);
if (is_not_empty_item)
data->previous_is_iconic = box->iconic;
if (box->separator == NULL)
return;
has_separator = gtk_widget_get_parent (box->separator) != NULL;
has_label = !GTK_IS_SEPARATOR (box->separator);
separator_condition = has_label ? TRUE : n_items_before > 0 &&
box->depth <= 1 &&
!previous_section_is_iconic &&
!box->iconic;
should_have_separator = separator_condition && is_not_empty_item;
should_have_top_margin = !should_have_separator &&
(box->depth <= 1 || box->iconic) &&
n_items_before > 0 &&
is_not_empty_item;
gtk_widget_set_margin_top (GTK_WIDGET (box->item_box), should_have_top_margin ? 10 : 0);
if (should_have_separator == has_separator)
return;
if (should_have_separator)
gtk_box_pack_start (GTK_BOX (box), box->separator, FALSE, FALSE, 0);
else
gtk_container_remove (GTK_CONTAINER (box), box->separator);
}
static gboolean
gtk_menu_section_box_handle_sync_separators (gpointer user_data)
{
GtkMenuSectionBox *box = user_data;
MenuData data;
data.n_items = 0;
data.previous_is_iconic = FALSE;
gtk_menu_section_box_sync_separators (box, &data);
box->separator_sync_idle = 0;
return G_SOURCE_REMOVE;
}
static void
gtk_menu_section_box_schedule_separator_sync (GtkMenuSectionBox *box)
{
box = box->toplevel;
if (!box->separator_sync_idle)
box->separator_sync_idle = gdk_threads_add_idle_full (G_PRIORITY_HIGH_IDLE, /* before resize... */
gtk_menu_section_box_handle_sync_separators,
box, NULL);
}
static void
gtk_popover_item_activate (GtkWidget *button,
gpointer user_data)
{
GtkMenuTrackerItem *item = user_data;
GtkWidget *popover = NULL;
if (gtk_menu_tracker_item_get_role (item) == GTK_MENU_TRACKER_ITEM_ROLE_NORMAL)
{
/* Activating the item could cause the popover
* to be free'd, for example if it is a Quit item
*/
popover = g_object_ref (gtk_widget_get_ancestor (button,
GTK_TYPE_POPOVER));
}
gtk_menu_tracker_item_activated (item);
if (popover != NULL)
{
gtk_widget_hide (popover);
g_object_unref (popover);
}
}
static void
gtk_menu_section_box_remove_func (gint position,
gpointer user_data)
{
GtkMenuSectionBox *box = user_data;
GtkMenuTrackerItem *item;
GtkWidget *widget;
GList *children;
children = gtk_container_get_children (GTK_CONTAINER (box->item_box));
widget = g_list_nth_data (children, position);
item = g_object_get_data (G_OBJECT (widget), "GtkMenuTrackerItem");
if (gtk_menu_tracker_item_get_has_link (item, G_MENU_LINK_SUBMENU))
{
GtkWidget *stack, *subbox;
stack = gtk_widget_get_ancestor (GTK_WIDGET (box->toplevel), GTK_TYPE_STACK);
subbox = gtk_stack_get_child_by_name (GTK_STACK (stack), gtk_menu_tracker_item_get_label (item));
if (subbox != NULL)
gtk_container_remove (GTK_CONTAINER (stack), subbox);
}
gtk_widget_destroy (g_list_nth_data (children, position));
g_list_free (children);
gtk_menu_section_box_schedule_separator_sync (box);
}
static gboolean
get_ancestors (GtkWidget *widget,
GType widget_type,
GtkWidget **ancestor,
GtkWidget **below)
{
GtkWidget *a, *b;
a = NULL;
b = widget;
while (b != NULL)
{
a = gtk_widget_get_parent (b);
if (!a)
return FALSE;
if (g_type_is_a (G_OBJECT_TYPE (a), widget_type))
break;
b = a;
}
*below = b;
*ancestor = a;
return TRUE;
}
static void
close_submenu (GtkWidget *button,
gpointer data)
{
GtkMenuTrackerItem *item = data;
GtkWidget *focus;
if (gtk_menu_tracker_item_get_should_request_show (item))
gtk_menu_tracker_item_request_submenu_shown (item, FALSE);
focus = GTK_WIDGET (g_object_get_data (G_OBJECT (button), "focus"));
gtk_widget_grab_focus (focus);
}
static void
open_submenu (GtkWidget *button,
gpointer data)
{
GtkMenuTrackerItem *item = data;
GtkWidget *focus;
if (gtk_menu_tracker_item_get_should_request_show (item))
gtk_menu_tracker_item_request_submenu_shown (item, TRUE);
focus = GTK_WIDGET (g_object_get_data (G_OBJECT (button), "focus"));
gtk_widget_grab_focus (focus);
}
static void
gtk_menu_section_box_insert_func (GtkMenuTrackerItem *item,
gint position,
gpointer user_data)
{
GtkMenuSectionBox *box = user_data;
GtkWidget *widget;
if (gtk_menu_tracker_item_get_is_separator (item))
{
widget = gtk_menu_section_box_new_section (item, box);
}
else if (gtk_menu_tracker_item_get_has_link (item, G_MENU_LINK_SUBMENU))
{
GtkWidget *stack = NULL;
GtkWidget *parent = NULL;
gchar *name;
widget = g_object_new (GTK_TYPE_MODEL_BUTTON,
"menu-name", gtk_menu_tracker_item_get_label (item),
NULL);
g_object_bind_property (item, "label", widget, "text", G_BINDING_SYNC_CREATE);
g_object_bind_property (item, "icon", widget, "icon", G_BINDING_SYNC_CREATE);
g_object_bind_property (item, "sensitive", widget, "sensitive", G_BINDING_SYNC_CREATE);
get_ancestors (GTK_WIDGET (box->toplevel), GTK_TYPE_STACK, &stack, &parent);
gtk_container_child_get (GTK_CONTAINER (stack), parent, "name", &name, NULL);
gtk_menu_section_box_new_submenu (item, box->toplevel, widget, name);
g_free (name);
}
else
{
widget = gtk_model_button_new ();
g_object_bind_property (item, "label", widget, "text", G_BINDING_SYNC_CREATE);
if (box->iconic)
{
g_object_bind_property (item, "verb-icon", widget, "icon", G_BINDING_SYNC_CREATE);
g_object_set (widget, "iconic", TRUE, "centered", TRUE, NULL);
}
else
g_object_bind_property (item, "icon", widget, "icon", G_BINDING_SYNC_CREATE);
g_object_bind_property (item, "sensitive", widget, "sensitive", G_BINDING_SYNC_CREATE);
g_object_bind_property (item, "role", widget, "role", G_BINDING_SYNC_CREATE);
g_object_bind_property (item, "toggled", widget, "active", G_BINDING_SYNC_CREATE);
g_signal_connect (widget, "clicked", G_CALLBACK (gtk_popover_item_activate), item);
}
gtk_widget_show (widget);
g_object_set_data_full (G_OBJECT (widget), "GtkMenuTrackerItem", g_object_ref (item), g_object_unref);
gtk_widget_set_halign (widget, GTK_ALIGN_FILL);
if (box->iconic)
gtk_box_pack_start (GTK_BOX (box->item_box), widget, TRUE, TRUE, 0);
else
gtk_container_add (GTK_CONTAINER (box->item_box), widget);
gtk_box_reorder_child (GTK_BOX (box->item_box), widget, position);
gtk_menu_section_box_schedule_separator_sync (box);
}
static void
gtk_menu_section_box_init (GtkMenuSectionBox *box)
{
GtkWidget *item_box;
gtk_orientable_set_orientation (GTK_ORIENTABLE (box), GTK_ORIENTATION_VERTICAL);
box->toplevel = box;
item_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
box->item_box = GTK_BOX (item_box);
gtk_box_pack_end (GTK_BOX (box), item_box, FALSE, FALSE, 0);
gtk_widget_set_halign (GTK_WIDGET (item_box), GTK_ALIGN_FILL);
gtk_widget_show (item_box);
gtk_widget_set_halign (GTK_WIDGET (box), GTK_ALIGN_FILL);
g_object_set (box, "margin", 0, NULL);
}
static void
gtk_menu_section_box_dispose (GObject *object)
{
GtkMenuSectionBox *box = GTK_MENU_SECTION_BOX (object);
if (box->separator_sync_idle)
{
g_source_remove (box->separator_sync_idle);
box->separator_sync_idle = 0;
}
g_clear_object (&box->separator);
if (box->tracker)
{
gtk_menu_tracker_free (box->tracker);
box->tracker = NULL;
}
G_OBJECT_CLASS (gtk_menu_section_box_parent_class)->dispose (object);
}
static void
gtk_menu_section_box_class_init (GtkMenuSectionBoxClass *class)
{
G_OBJECT_CLASS (class)->dispose = gtk_menu_section_box_dispose;
}
static void
update_popover_position_cb (GObject *source,
GParamSpec *spec,
gpointer *user_data)
{
GtkPopover *popover = GTK_POPOVER (source);
GtkMenuSectionBox *box = GTK_MENU_SECTION_BOX (user_data);
GtkPositionType new_pos = gtk_popover_get_position (popover);
GList *children = gtk_container_get_children (GTK_CONTAINER (gtk_widget_get_parent (GTK_WIDGET (box))));
GList *l;
for (l = children;
l != NULL;
l = l->next)
{
GtkWidget *w = l->data;
if (new_pos == GTK_POS_BOTTOM)
gtk_widget_set_valign (w, GTK_ALIGN_START);
else if (new_pos == GTK_POS_TOP)
gtk_widget_set_valign (w, GTK_ALIGN_END);
else
gtk_widget_set_valign (w, GTK_ALIGN_CENTER);
}
g_list_free (children);
}
void
gtk_menu_section_box_new_toplevel (GtkStack *stack,
GMenuModel *model,
const gchar *action_namespace,
GtkPopover *popover)
{
GtkMenuSectionBox *box;
box = g_object_new (GTK_TYPE_MENU_SECTION_BOX, "margin", 10, NULL);
gtk_stack_add_named (stack, GTK_WIDGET (box), "main");
box->tracker = gtk_menu_tracker_new (GTK_ACTION_OBSERVABLE (_gtk_widget_get_action_muxer (GTK_WIDGET (box), TRUE)),
model, TRUE, FALSE, FALSE, action_namespace,
gtk_menu_section_box_insert_func,
gtk_menu_section_box_remove_func, box);
g_signal_connect (G_OBJECT (popover), "notify::position", G_CALLBACK (update_popover_position_cb), box);
gtk_widget_show (GTK_WIDGET (box));
}
static void
gtk_menu_section_box_new_submenu (GtkMenuTrackerItem *item,
GtkMenuSectionBox *toplevel,
GtkWidget *focus,
const gchar *name)
{
GtkMenuSectionBox *box;
GtkWidget *button;
box = g_object_new (GTK_TYPE_MENU_SECTION_BOX, "margin", 10, NULL);
button = g_object_new (GTK_TYPE_MODEL_BUTTON,
"menu-name", name,
"inverted", TRUE,
"centered", TRUE,
NULL);
g_object_bind_property (item, "label", button, "text", G_BINDING_SYNC_CREATE);
g_object_bind_property (item, "icon", button, "icon", G_BINDING_SYNC_CREATE);
g_object_set_data (G_OBJECT (button), "focus", focus);
g_object_set_data (G_OBJECT (focus), "focus", button);
gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0);
gtk_widget_show (button);
g_signal_connect (focus, "clicked", G_CALLBACK (open_submenu), item);
g_signal_connect (button, "clicked", G_CALLBACK (close_submenu), item);
gtk_stack_add_named (GTK_STACK (gtk_widget_get_ancestor (GTK_WIDGET (toplevel), GTK_TYPE_STACK)),
GTK_WIDGET (box), gtk_menu_tracker_item_get_label (item));
gtk_widget_show (GTK_WIDGET (box));
box->tracker = gtk_menu_tracker_new_for_item_link (item, G_MENU_LINK_SUBMENU, FALSE, FALSE,
gtk_menu_section_box_insert_func,
gtk_menu_section_box_remove_func,
box);
}
static GtkWidget *
gtk_menu_section_box_new_section (GtkMenuTrackerItem *item,
GtkMenuSectionBox *parent)
{
GtkMenuSectionBox *box;
const gchar *label;
const gchar *hint;
const gchar *text_direction;
box = g_object_new (GTK_TYPE_MENU_SECTION_BOX, NULL);
box->toplevel = parent->toplevel;
box->depth = parent->depth + 1;
label = gtk_menu_tracker_item_get_label (item);
hint = gtk_menu_tracker_item_get_display_hint (item);
text_direction = gtk_menu_tracker_item_get_text_direction (item);
if (hint && g_str_equal (hint, "horizontal-buttons"))
{
gtk_orientable_set_orientation (GTK_ORIENTABLE (box->item_box), GTK_ORIENTATION_HORIZONTAL);
gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (box->item_box)), GTK_STYLE_CLASS_LINKED);
box->iconic = TRUE;
if (text_direction)
{
GtkTextDirection dir = GTK_TEXT_DIR_NONE;
if (g_str_equal (text_direction, "rtl"))
dir = GTK_TEXT_DIR_RTL;
else if (g_str_equal (text_direction, "ltr"))
dir = GTK_TEXT_DIR_LTR;
gtk_widget_set_direction (GTK_WIDGET (box->item_box), dir);
}
}
if (label != NULL)
{
GtkWidget *separator;
GtkWidget *title;
box->separator = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);
g_object_ref_sink (box->separator);
separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
gtk_widget_set_valign (separator, GTK_ALIGN_CENTER);
gtk_box_pack_start (GTK_BOX (box->separator), separator, TRUE, TRUE, 0);
title = gtk_label_new (label);
g_object_bind_property (item, "label", title, "label", G_BINDING_SYNC_CREATE);
gtk_style_context_add_class (gtk_widget_get_style_context (title), GTK_STYLE_CLASS_SEPARATOR);
gtk_widget_set_halign (title, GTK_ALIGN_START);
gtk_box_pack_start (GTK_BOX (box->separator), title, FALSE, FALSE, 0);
separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
gtk_widget_set_valign (separator, GTK_ALIGN_CENTER);
gtk_box_pack_start (GTK_BOX (box->separator), separator, TRUE, TRUE, 0);
gtk_widget_show_all (box->separator);
}
else
{
box->separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
g_object_ref_sink (box->separator);
gtk_widget_show (box->separator);
}
box->tracker = gtk_menu_tracker_new_for_item_link (item, G_MENU_LINK_SECTION, FALSE, FALSE,
gtk_menu_section_box_insert_func,
gtk_menu_section_box_remove_func,
box);
return GTK_WIDGET (box);
}