From 26c3f1a26d1f282743513fb437eb3e79b2436690 Mon Sep 17 00:00:00 2001 From: Tristan Van Berkom Date: Thu, 18 Nov 2010 17:33:23 +0900 Subject: [PATCH] Adding GtkTreeMenu class. Added GtkTreeMenu class to automatically render a GtkTreeModel into a GtkMenu hierarchy (will be used by GtkComboBox for its dropdown menus). Included an accompanying testcase tests/testtreemenu --- gtk/Makefile.am | 2 + gtk/gtk.h | 1 + gtk/gtktreemenu.c | 909 +++++++++++++++++++++++++++++++++++++++++++ gtk/gtktreemenu.h | 91 +++++ tests/Makefile.am | 9 +- tests/testtreemenu.c | 245 ++++++++++++ 6 files changed, 1256 insertions(+), 1 deletion(-) create mode 100644 gtk/gtktreemenu.c create mode 100644 gtk/gtktreemenu.h create mode 100644 tests/testtreemenu.c diff --git a/gtk/Makefile.am b/gtk/Makefile.am index 215f305777..86517df6ac 100644 --- a/gtk/Makefile.am +++ b/gtk/Makefile.am @@ -325,6 +325,7 @@ gtk_public_h_sources = \ gtktoolshell.h \ gtktooltip.h \ gtktreednd.h \ + gtktreemenu.h \ gtktreemodel.h \ gtktreemodelfilter.h \ gtktreemodelsort.h \ @@ -651,6 +652,7 @@ gtk_base_c_sources = \ gtktooltip.c \ gtktreedatalist.c \ gtktreednd.c \ + gtktreemenu.c \ gtktreemodel.c \ gtktreemodelfilter.c \ gtktreemodelsort.c \ diff --git a/gtk/gtk.h b/gtk/gtk.h index 0d85793a3c..f55bdcfc3d 100644 --- a/gtk/gtk.h +++ b/gtk/gtk.h @@ -209,6 +209,7 @@ #include #include #include +#include #include #include #include diff --git a/gtk/gtktreemenu.c b/gtk/gtktreemenu.c new file mode 100644 index 0000000000..cff45419d2 --- /dev/null +++ b/gtk/gtktreemenu.c @@ -0,0 +1,909 @@ +/* gtktreemenu.c + * + * Copyright (C) 2010 Openismus GmbH + * + * Authors: + * Tristan Van Berkom + * + * 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, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include "gtkintl.h" +#include "gtktreemenu.h" +#include "gtkmenuitem.h" +#include "gtkseparatormenuitem.h" +#include "gtkcellareabox.h" +#include "gtkcellareacontext.h" +#include "gtkcelllayout.h" +#include "gtkcellview.h" +#include "gtkprivate.h" + + +/* GObjectClass */ +static GObject *gtk_tree_menu_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_properties); +static void gtk_tree_menu_dispose (GObject *object); +static void gtk_tree_menu_finalize (GObject *object); +static void gtk_tree_menu_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec); +static void gtk_tree_menu_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec); + +/* GtkWidgetClass */ +static void gtk_tree_menu_get_preferred_width (GtkWidget *widget, + gint *minimum_size, + gint *natural_size); +static void gtk_tree_menu_get_preferred_height (GtkWidget *widget, + gint *minimum_size, + gint *natural_size); +static void gtk_tree_menu_size_allocate (GtkWidget *widget, + GtkAllocation *allocation); + +/* GtkCellLayoutIface */ +static void gtk_tree_menu_cell_layout_init (GtkCellLayoutIface *iface); +static void gtk_tree_menu_cell_layout_pack_start (GtkCellLayout *layout, + GtkCellRenderer *cell, + gboolean expand); +static void gtk_tree_menu_cell_layout_pack_end (GtkCellLayout *layout, + GtkCellRenderer *cell, + gboolean expand); +static GList *gtk_tree_menu_cell_layout_get_cells (GtkCellLayout *layout); +static void gtk_tree_menu_cell_layout_clear (GtkCellLayout *layout); +static void gtk_tree_menu_cell_layout_add_attribute (GtkCellLayout *layout, + GtkCellRenderer *cell, + const gchar *attribute, + gint column); +static void gtk_tree_menu_cell_layout_set_cell_data_func (GtkCellLayout *layout, + GtkCellRenderer *cell, + GtkCellLayoutDataFunc func, + gpointer func_data, + GDestroyNotify destroy); +static void gtk_tree_menu_cell_layout_clear_attributes (GtkCellLayout *layout, + GtkCellRenderer *cell); +static void gtk_tree_menu_cell_layout_reorder (GtkCellLayout *layout, + GtkCellRenderer *cell, + gint position); +static GtkCellArea *gtk_tree_menu_cell_layout_get_area (GtkCellLayout *layout); + + +/* TreeModel/DrawingArea callbacks and building menus/submenus */ +static void gtk_tree_menu_populate (GtkTreeMenu *menu); +static GtkWidget *gtk_tree_menu_create_item (GtkTreeMenu *menu, + GtkTreeIter *iter); +static void gtk_tree_menu_set_area (GtkTreeMenu *menu, + GtkCellArea *area); +static void queue_resize_all (GtkWidget *menu); +static void context_size_changed_cb (GtkCellAreaContext *context, + GParamSpec *pspec, + GtkWidget *menu); + +struct _GtkTreeMenuPrivate +{ + /* TreeModel and parent for this menu */ + GtkTreeModel *model; + GtkTreeRowReference *root; + + /* CellArea and context for this menu */ + GtkCellArea *area; + GtkCellAreaContext *context; + + gint last_alloc_width; + gint last_alloc_height; + + /* Signals */ + gulong size_changed_id; + + /* Row separators */ + GtkTreeViewRowSeparatorFunc row_separator_func; + gpointer row_separator_data; + GDestroyNotify row_separator_destroy; +}; + +enum { + PROP_0, + PROP_MODEL, + PROP_ROOT, + PROP_CELL_AREA +}; + +G_DEFINE_TYPE_WITH_CODE (GtkTreeMenu, gtk_tree_menu, GTK_TYPE_MENU, + G_IMPLEMENT_INTERFACE (GTK_TYPE_CELL_LAYOUT, + gtk_tree_menu_cell_layout_init)); + +static void +gtk_tree_menu_init (GtkTreeMenu *menu) +{ + GtkTreeMenuPrivate *priv; + + menu->priv = G_TYPE_INSTANCE_GET_PRIVATE (menu, + GTK_TYPE_TREE_MENU, + GtkTreeMenuPrivate); + priv = menu->priv; +} + +static void +gtk_tree_menu_class_init (GtkTreeMenuClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); + + object_class->constructor = gtk_tree_menu_constructor; + object_class->dispose = gtk_tree_menu_dispose; + object_class->finalize = gtk_tree_menu_finalize; + object_class->set_property = gtk_tree_menu_set_property; + object_class->get_property = gtk_tree_menu_get_property; + + widget_class->get_preferred_width = gtk_tree_menu_get_preferred_width; + widget_class->get_preferred_height = gtk_tree_menu_get_preferred_height; + widget_class->size_allocate = gtk_tree_menu_size_allocate; + + g_object_class_install_property (object_class, + PROP_MODEL, + g_param_spec_object ("model", + P_("TreeMenu model"), + P_("The model for the tree menu"), + GTK_TYPE_TREE_MODEL, + GTK_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_ROOT, + g_param_spec_boxed ("root", + P_("TreeMenu root row"), + P_("The TreeMenu will display children of the " + "specified root"), + GTK_TYPE_TREE_ROW_REFERENCE, + GTK_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_CELL_AREA, + g_param_spec_object ("cell-area", + P_("Cell Area"), + P_("The GtkCellArea used to layout cells"), + GTK_TYPE_CELL_AREA, + GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); + + + g_type_class_add_private (object_class, sizeof (GtkTreeMenuPrivate)); +} + +/**************************************************************** + * GObjectClass * + ****************************************************************/ +static GObject * +gtk_tree_menu_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_properties) +{ + GObject *object; + GtkTreeMenu *menu; + GtkTreeMenuPrivate *priv; + + object = G_OBJECT_CLASS (gtk_tree_menu_parent_class)->constructor + (type, n_construct_properties, construct_properties); + + menu = GTK_TREE_MENU (object); + priv = menu->priv; + + if (!priv->area) + { + GtkCellArea *area = gtk_cell_area_box_new (); + + gtk_tree_menu_set_area (menu, area); + } + + priv->context = gtk_cell_area_create_context (priv->area); + priv->size_changed_id = + g_signal_connect (priv->context, "notify", + G_CALLBACK (context_size_changed_cb), menu); + + + return object; +} + +static void +gtk_tree_menu_dispose (GObject *object) +{ + GtkTreeMenu *menu; + GtkTreeMenuPrivate *priv; + + menu = GTK_TREE_MENU (object); + priv = menu->priv; + + gtk_tree_menu_set_model (menu, NULL); + gtk_tree_menu_set_area (menu, NULL); + + if (priv->context) + { + /* Disconnect signals */ + g_signal_handler_disconnect (priv->context, priv->size_changed_id); + + g_object_unref (priv->context); + priv->context = NULL; + priv->size_changed_id = 0; + } + + G_OBJECT_CLASS (gtk_tree_menu_parent_class)->dispose (object); +} + +static void +gtk_tree_menu_finalize (GObject *object) +{ + + G_OBJECT_CLASS (gtk_tree_menu_parent_class)->finalize (object); +} + +static void +gtk_tree_menu_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkTreeMenu *menu = GTK_TREE_MENU (object); + + switch (prop_id) + { + case PROP_MODEL: + gtk_tree_menu_set_model (menu, g_value_get_object (value)); + break; + + case PROP_ROOT: + gtk_tree_menu_set_root (menu, g_value_get_boxed (value)); + break; + + case PROP_CELL_AREA: + /* Construct-only, can only be assigned once */ + gtk_tree_menu_set_area (menu, (GtkCellArea *)g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_tree_menu_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkTreeMenu *menu = GTK_TREE_MENU (object); + GtkTreeMenuPrivate *priv = menu->priv; + + switch (prop_id) + { + case PROP_MODEL: + g_value_set_object (value, priv->model); + break; + + case PROP_ROOT: + g_value_set_boxed (value, priv->root); + break; + + case PROP_CELL_AREA: + g_value_set_object (value, priv->area); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +/**************************************************************** + * GtkWidgetClass * + ****************************************************************/ + +/* We tell all the menu items to reserve space for the submenu + * indicator if there is at least one submenu, this way we ensure + * that every internal cell area gets allocated the + * same width (and requested height for the same appropriate width). + */ +static void +sync_reserve_submenu_size (GtkTreeMenu *menu) +{ + GList *children, *l; + gboolean has_submenu = FALSE; + + children = gtk_container_get_children (GTK_CONTAINER (menu)); + for (l = children; l; l = l->next) + { + GtkMenuItem *item = l->data; + + if (gtk_menu_item_get_submenu (item) != NULL) + { + has_submenu = TRUE; + break; + } + } + + for (l = children; l; l = l->next) + { + GtkMenuItem *item = l->data; + + gtk_menu_item_set_reserve_indicator (item, has_submenu); + } + + g_list_free (children); +} + +static void +gtk_tree_menu_get_preferred_width (GtkWidget *widget, + gint *minimum_size, + gint *natural_size) +{ + GtkTreeMenu *menu = GTK_TREE_MENU (widget); + GtkTreeMenuPrivate *priv = menu->priv; + GtkTreePath *path = NULL; + GtkTreeIter iter; + gboolean valid = FALSE; + + g_signal_handler_block (priv->context, priv->size_changed_id); + + /* Before chaining up to the parent class and requesting the + * menu item/cell view sizes, we need to request the size of + * each row for this menu and make sure all the cellviews + * request enough space + */ + gtk_cell_area_context_flush_preferred_width (priv->context); + + sync_reserve_submenu_size (menu); + + if (priv->model) + { + if (priv->root) + path = gtk_tree_row_reference_get_path (priv->root); + + if (path) + { + GtkTreeIter parent; + + if (gtk_tree_model_get_iter (priv->model, &parent, path)) + valid = gtk_tree_model_iter_children (priv->model, &iter, &parent); + + gtk_tree_path_free (path); + } + else + valid = gtk_tree_model_iter_children (priv->model, &iter, NULL); + + while (valid) + { + gboolean is_separator = FALSE; + + if (priv->row_separator_func) + is_separator = + priv->row_separator_func (priv->model, &iter, priv->row_separator_data); + + if (!is_separator) + { + gtk_cell_area_apply_attributes (priv->area, priv->model, &iter, FALSE, FALSE); + gtk_cell_area_get_preferred_width (priv->area, priv->context, widget, NULL, NULL); + } + + valid = gtk_tree_model_iter_next (priv->model, &iter); + } + } + + gtk_cell_area_context_sum_preferred_width (priv->context); + + g_signal_handler_unblock (priv->context, priv->size_changed_id); + + /* Now that we've requested all the row's and updated priv->context properly, we can go ahead + * and calculate the sizes by requesting the menu items and thier cell views */ + GTK_WIDGET_CLASS (gtk_tree_menu_parent_class)->get_preferred_width (widget, minimum_size, natural_size); +} + +static void +gtk_tree_menu_get_preferred_height (GtkWidget *widget, + gint *minimum_size, + gint *natural_size) +{ + GtkTreeMenu *menu = GTK_TREE_MENU (widget); + GtkTreeMenuPrivate *priv = menu->priv; + GtkTreePath *path = NULL; + GtkTreeIter iter; + gboolean valid = FALSE; + + g_signal_handler_block (priv->context, priv->size_changed_id); + + /* Before chaining up to the parent class and requesting the + * menu item/cell view sizes, we need to request the size of + * each row for this menu and make sure all the cellviews + * request enough space + */ + gtk_cell_area_context_flush_preferred_height (priv->context); + + sync_reserve_submenu_size (menu); + + if (priv->model) + { + if (priv->root) + path = gtk_tree_row_reference_get_path (priv->root); + + if (path) + { + GtkTreeIter parent; + + if (gtk_tree_model_get_iter (priv->model, &parent, path)) + valid = gtk_tree_model_iter_children (priv->model, &iter, &parent); + + gtk_tree_path_free (path); + } + else + valid = gtk_tree_model_iter_children (priv->model, &iter, NULL); + + while (valid) + { + gboolean is_separator = FALSE; + + if (priv->row_separator_func) + is_separator = + priv->row_separator_func (priv->model, &iter, priv->row_separator_data); + + if (!is_separator) + { + gtk_cell_area_apply_attributes (priv->area, priv->model, &iter, FALSE, FALSE); + gtk_cell_area_get_preferred_height (priv->area, priv->context, widget, NULL, NULL); + } + + valid = gtk_tree_model_iter_next (priv->model, &iter); + } + } + + gtk_cell_area_context_sum_preferred_height (priv->context); + + g_signal_handler_unblock (priv->context, priv->size_changed_id); + + /* Now that we've requested all the row's and updated priv->context properly, we can go ahead + * and calculate the sizes by requesting the menu items and thier cell views */ + GTK_WIDGET_CLASS (gtk_tree_menu_parent_class)->get_preferred_height (widget, minimum_size, natural_size); +} + +static void +gtk_tree_menu_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + GtkTreeMenu *menu = GTK_TREE_MENU (widget); + GtkTreeMenuPrivate *priv = menu->priv; + gint new_width, new_height; + + /* flush the context allocation */ + gtk_cell_area_context_flush_allocation (priv->context); + + /* Leave it to the first cell area to allocate the size of priv->context, since + * we configure the menu to allocate all children the same width this should work fine */ + GTK_WIDGET_CLASS (gtk_tree_menu_parent_class)->size_allocate (widget, allocation); + + /* In alot of cases the menu gets allocated while the children dont need + * any reallocation, in this case we need to restore the context allocation */ + gtk_cell_area_context_get_allocation (priv->context, &new_width, &new_height); + + if (new_width <= 0 && new_height <= 0) + { + gtk_cell_area_context_allocate_width (priv->context, priv->last_alloc_width); + gtk_cell_area_context_allocate_height (priv->context, priv->last_alloc_height); + } + + /* Save the allocation for the next round */ + gtk_cell_area_context_get_allocation (priv->context, + &priv->last_alloc_width, + &priv->last_alloc_height); +} + +/**************************************************************** + * GtkCellLayoutIface * + ****************************************************************/ +/* Just forward all the GtkCellLayoutIface methods to the + * underlying GtkCellArea + */ +static void +gtk_tree_menu_cell_layout_init (GtkCellLayoutIface *iface) +{ + iface->pack_start = gtk_tree_menu_cell_layout_pack_start; + iface->pack_end = gtk_tree_menu_cell_layout_pack_end; + iface->get_cells = gtk_tree_menu_cell_layout_get_cells; + iface->clear = gtk_tree_menu_cell_layout_clear; + iface->add_attribute = gtk_tree_menu_cell_layout_add_attribute; + iface->set_cell_data_func = gtk_tree_menu_cell_layout_set_cell_data_func; + iface->clear_attributes = gtk_tree_menu_cell_layout_clear_attributes; + iface->reorder = gtk_tree_menu_cell_layout_reorder; + iface->get_area = gtk_tree_menu_cell_layout_get_area; +} + +static void +gtk_tree_menu_cell_layout_pack_start (GtkCellLayout *layout, + GtkCellRenderer *cell, + gboolean expand) +{ + GtkTreeMenu *menu = GTK_TREE_MENU (layout); + GtkTreeMenuPrivate *priv = menu->priv; + + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (priv->area), cell, expand); +} + +static void +gtk_tree_menu_cell_layout_pack_end (GtkCellLayout *layout, + GtkCellRenderer *cell, + gboolean expand) +{ + GtkTreeMenu *menu = GTK_TREE_MENU (layout); + GtkTreeMenuPrivate *priv = menu->priv; + + gtk_cell_layout_pack_end (GTK_CELL_LAYOUT (priv->area), cell, expand); +} + +static GList * +gtk_tree_menu_cell_layout_get_cells (GtkCellLayout *layout) +{ + GtkTreeMenu *menu = GTK_TREE_MENU (layout); + GtkTreeMenuPrivate *priv = menu->priv; + + return gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (priv->area)); +} + +static void +gtk_tree_menu_cell_layout_clear (GtkCellLayout *layout) +{ + GtkTreeMenu *menu = GTK_TREE_MENU (layout); + GtkTreeMenuPrivate *priv = menu->priv; + + gtk_cell_layout_clear (GTK_CELL_LAYOUT (priv->area)); +} + +static void +gtk_tree_menu_cell_layout_add_attribute (GtkCellLayout *layout, + GtkCellRenderer *cell, + const gchar *attribute, + gint column) +{ + GtkTreeMenu *menu = GTK_TREE_MENU (layout); + GtkTreeMenuPrivate *priv = menu->priv; + + gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (priv->area), cell, attribute, column); +} + +static void +gtk_tree_menu_cell_layout_set_cell_data_func (GtkCellLayout *layout, + GtkCellRenderer *cell, + GtkCellLayoutDataFunc func, + gpointer func_data, + GDestroyNotify destroy) +{ + GtkTreeMenu *menu = GTK_TREE_MENU (layout); + GtkTreeMenuPrivate *priv = menu->priv; + + gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (priv->area), cell, func, func_data, destroy); +} + +static void +gtk_tree_menu_cell_layout_clear_attributes (GtkCellLayout *layout, + GtkCellRenderer *cell) +{ + GtkTreeMenu *menu = GTK_TREE_MENU (layout); + GtkTreeMenuPrivate *priv = menu->priv; + + gtk_cell_layout_clear_attributes (GTK_CELL_LAYOUT (priv->area), cell); +} + +static void +gtk_tree_menu_cell_layout_reorder (GtkCellLayout *layout, + GtkCellRenderer *cell, + gint position) +{ + GtkTreeMenu *menu = GTK_TREE_MENU (layout); + GtkTreeMenuPrivate *priv = menu->priv; + + gtk_cell_layout_reorder (GTK_CELL_LAYOUT (priv->area), cell, position); +} + +static GtkCellArea * +gtk_tree_menu_cell_layout_get_area (GtkCellLayout *layout) +{ + GtkTreeMenu *menu = GTK_TREE_MENU (layout); + GtkTreeMenuPrivate *priv = menu->priv; + + return priv->area; +} + + +/**************************************************************** + * TreeModel callbacks/populating menus * + ****************************************************************/ +static void +context_size_changed_cb (GtkCellAreaContext *context, + GParamSpec *pspec, + GtkWidget *menu) +{ + if (!strcmp (pspec->name, "minimum-width") || + !strcmp (pspec->name, "natural-width") || + !strcmp (pspec->name, "minimum-height") || + !strcmp (pspec->name, "natural-height")) + queue_resize_all (menu); +} + +static void +queue_resize_all (GtkWidget *menu) +{ + GList *children, *l; + + children = gtk_container_get_children (GTK_CONTAINER (menu)); + for (l = children; l; l = l->next) + { + GtkWidget *widget = l->data; + + gtk_widget_queue_resize (widget); + } + + g_list_free (children); + + gtk_widget_queue_resize (menu); +} + + +static void +gtk_tree_menu_set_area (GtkTreeMenu *menu, + GtkCellArea *area) +{ + GtkTreeMenuPrivate *priv = menu->priv; + + if (priv->area) + g_object_unref (area); + + priv->area = area; + + if (priv->area) + g_object_ref_sink (area); +} + + +static GtkWidget * +gtk_tree_menu_create_item (GtkTreeMenu *menu, + GtkTreeIter *iter) +{ + GtkTreeMenuPrivate *priv = menu->priv; + GtkWidget *item, *view; + GtkTreePath *path; + + view = gtk_cell_view_new_with_context (priv->area, priv->context); + item = gtk_menu_item_new (); + gtk_widget_show (view); + gtk_widget_show (item); + + path = gtk_tree_model_get_path (priv->model, iter); + + gtk_cell_view_set_model (GTK_CELL_VIEW (view), priv->model); + gtk_cell_view_set_displayed_row (GTK_CELL_VIEW (view), path); + + gtk_tree_path_free (path); + + gtk_widget_show (view); + gtk_container_add (GTK_CONTAINER (item), view); + + return item; +} + +static void +gtk_tree_menu_populate (GtkTreeMenu *menu) +{ + GtkTreeMenuPrivate *priv = menu->priv; + GtkTreePath *path = NULL; + GtkTreeIter parent; + GtkTreeIter iter; + gboolean valid = FALSE; + GtkWidget *menu_item; + + if (!priv->model) + return; + + if (priv->root) + path = gtk_tree_row_reference_get_path (priv->root); + + if (path) + { + if (gtk_tree_model_get_iter (priv->model, &parent, path)) + valid = gtk_tree_model_iter_children (priv->model, &iter, &parent); + + gtk_tree_path_free (path); + } + else + valid = gtk_tree_model_iter_children (priv->model, &iter, NULL); + + /* Create a menu item for every row at the current depth, add a GtkTreeMenu + * submenu for iters/items that have children */ + while (valid) + { + gboolean is_separator = FALSE; + + if (priv->row_separator_func) + is_separator = + priv->row_separator_func (priv->model, &iter, + priv->row_separator_data); + + if (is_separator) + menu_item = gtk_separator_menu_item_new (); + else + { + menu_item = gtk_tree_menu_create_item (menu, &iter); + + /* Add a GtkTreeMenu submenu to render the children of this row */ + if (gtk_tree_model_iter_has_child (priv->model, &iter)) + { + GtkTreePath *row_path; + GtkWidget *submenu; + + row_path = gtk_tree_model_get_path (priv->model, &iter); + submenu = gtk_tree_menu_new_with_area (priv->area); + + gtk_tree_menu_set_model (GTK_TREE_MENU (submenu), priv->model); + gtk_tree_menu_set_root (GTK_TREE_MENU (submenu), row_path); + + gtk_menu_item_set_submenu (GTK_MENU_ITEM (menu_item), submenu); + + gtk_tree_path_free (row_path); + } + } + + gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item); + + valid = gtk_tree_model_iter_next (priv->model, &iter); + } +} + +/**************************************************************** + * API * + ****************************************************************/ +GtkWidget * +gtk_tree_menu_new (void) +{ + return (GtkWidget *)g_object_new (GTK_TYPE_TREE_MENU, NULL); +} + +GtkWidget * +gtk_tree_menu_new_with_area (GtkCellArea *area) +{ + return (GtkWidget *)g_object_new (GTK_TYPE_TREE_MENU, + "area", area, + NULL); +} + +void +gtk_tree_menu_set_model (GtkTreeMenu *menu, + GtkTreeModel *model) +{ + GtkTreeMenuPrivate *priv; + + g_return_if_fail (GTK_IS_TREE_MENU (menu)); + g_return_if_fail (model == NULL || GTK_IS_TREE_MODEL (model)); + + priv = menu->priv; + + if (priv->model != model) + { + if (priv->model) + { + /* Disconnect signals */ + + g_object_unref (priv->model); + } + + priv->model = model; + + if (priv->model) + { + /* Connect signals */ + + g_object_ref (priv->model); + } + + /* Changing the model in any way invalidates the currently set root, + * so we implicitly reset it to NULL here */ + gtk_tree_menu_set_root (menu, NULL); + + g_object_notify (G_OBJECT (menu), "model"); + } +} + +GtkTreeModel * +gtk_tree_menu_get_model (GtkTreeMenu *menu) +{ + GtkTreeMenuPrivate *priv; + + g_return_val_if_fail (GTK_IS_TREE_MENU (menu), NULL); + + priv = menu->priv; + + return priv->model; +} + +void +gtk_tree_menu_set_root (GtkTreeMenu *menu, + GtkTreePath *path) +{ + GtkTreeMenuPrivate *priv; + + g_return_if_fail (GTK_IS_TREE_MENU (menu)); + g_return_if_fail (path == NULL || menu->priv->model != NULL); + + priv = menu->priv; + + if (priv->root) + gtk_tree_row_reference_free (priv->root); + + if (path) + priv->root = gtk_tree_row_reference_new (priv->model, path); + else + priv->root = NULL; + + /* Destroy all the menu items for the previous root */ + gtk_container_foreach (GTK_CONTAINER (menu), + (GtkCallback) gtk_widget_destroy, NULL); + + /* Populate for the new root */ + gtk_tree_menu_populate (menu); +} + +GtkTreePath * +gtk_tree_menu_get_root (GtkTreeMenu *menu) +{ + GtkTreeMenuPrivate *priv; + + g_return_val_if_fail (GTK_IS_TREE_MENU (menu), NULL); + + priv = menu->priv; + + if (priv->root) + return gtk_tree_row_reference_get_path (priv->root); + + return NULL; +} + +void +gtk_tree_menu_set_row_separator_func (GtkTreeMenu *menu, + GtkTreeViewRowSeparatorFunc func, + gpointer data, + GDestroyNotify destroy) +{ + GtkTreeMenuPrivate *priv; + + g_return_if_fail (GTK_IS_TREE_MENU (menu)); + + priv = menu->priv; + + if (priv->row_separator_destroy) + priv->row_separator_destroy (priv->row_separator_data); + + priv->row_separator_func = func; + priv->row_separator_data = data; + priv->row_separator_destroy = destroy; +} + +GtkTreeViewRowSeparatorFunc +gtk_tree_menu_get_row_separator_func (GtkTreeMenu *menu) +{ + GtkTreeMenuPrivate *priv; + + g_return_val_if_fail (GTK_IS_TREE_MENU (menu), NULL); + + priv = menu->priv; + + return priv->row_separator_func; +} diff --git a/gtk/gtktreemenu.h b/gtk/gtktreemenu.h new file mode 100644 index 0000000000..994e293239 --- /dev/null +++ b/gtk/gtktreemenu.h @@ -0,0 +1,91 @@ +/* gtktreemenu.h + * + * Copyright (C) 2010 Openismus GmbH + * + * Authors: + * Tristan Van Berkom + * + * 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, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION) +#error "Only can be included directly." +#endif + +#ifndef __GTK_TREE_MENU_H__ +#define __GTK_TREE_MENU_H__ + +#include +#include +#include +#include + +G_BEGIN_DECLS + +#define GTK_TYPE_TREE_MENU (gtk_tree_menu_get_type ()) +#define GTK_TREE_MENU(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_TREE_MENU, GtkTreeMenu)) +#define GTK_TREE_MENU_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_TREE_MENU, GtkTreeMenuClass)) +#define GTK_IS_TREE_MENU(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_TREE_MENU)) +#define GTK_IS_TREE_MENU_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_TREE_MENU)) +#define GTK_TREE_MENU_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_TREE_MENU, GtkTreeMenuClass)) + +typedef struct _GtkTreeMenu GtkTreeMenu; +typedef struct _GtkTreeMenuClass GtkTreeMenuClass; +typedef struct _GtkTreeMenuPrivate GtkTreeMenuPrivate; + + +struct _GtkTreeMenu +{ + GtkMenu parent_instance; + + GtkTreeMenuPrivate *priv; +}; + +struct _GtkTreeMenuClass +{ + GtkMenuClass parent_class; + + /* Padding for future expansion */ + void (*_gtk_reserved1) (void); + void (*_gtk_reserved2) (void); + void (*_gtk_reserved3) (void); + void (*_gtk_reserved4) (void); + void (*_gtk_reserved5) (void); + void (*_gtk_reserved6) (void); + void (*_gtk_reserved7) (void); + void (*_gtk_reserved8) (void); +}; + +GType gtk_tree_menu_get_type (void) G_GNUC_CONST; + +GtkWidget *gtk_tree_menu_new (void); +GtkWidget *gtk_tree_menu_new_with_area (GtkCellArea *area); +void gtk_tree_menu_set_model (GtkTreeMenu *menu, + GtkTreeModel *model); +GtkTreeModel *gtk_tree_menu_get_model (GtkTreeMenu *menu); +void gtk_tree_menu_set_root (GtkTreeMenu *menu, + GtkTreePath *path); +GtkTreePath *gtk_tree_menu_get_root (GtkTreeMenu *menu); + +void gtk_tree_menu_set_row_separator_func (GtkTreeMenu *menu, + GtkTreeViewRowSeparatorFunc func, + gpointer data, + GDestroyNotify destroy); +GtkTreeViewRowSeparatorFunc gtk_tree_menu_get_row_separator_func (GtkTreeMenu *menu); + +G_END_DECLS + +#endif /* __GTK_TREE_MENU_H__ */ diff --git a/tests/Makefile.am b/tests/Makefile.am index 61d5270666..0932229c8c 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -98,9 +98,11 @@ noinst_PROGRAMS = $(TEST_PROGS) \ testexpander \ testvolumebutton \ testscrolledwindow \ + testswitch \ testcellarea \ testswitch \ - styleexamples + styleexamples \ + testtreemenu if USE_X11 noinst_PROGRAMS += testerrors @@ -189,6 +191,7 @@ testtooltips_DEPENDENCIES = $(TEST_DEPS) testvolumebutton_DEPENDENCIES = $(TEST_DEPS) testscrolledwindow_DEPENDENCIES = $(TEST_DEPS) testcellarea_DEPENDENCIES = $(TEST_DEPS) +testtreemenu_DEPENDENCIES = $(TEST_DEPS) testwindows_DEPENDENCIES = $(TEST_DEPS) testexpand_DEPENDENCIES = $(TEST_DEPS) testexpander_DEPENDENCIES = $(TEST_DEPS) @@ -268,6 +271,7 @@ testtooltips_LDADD = $(LDADDS) testvolumebutton_LDADD = $(LDADDS) testscrolledwindow_LDADD = $(LDADDS) testcellarea_LDADD = $(LDADDS) +testtreemenu_LDADD = $(LDADDS) testwindows_LDADD = $(LDADDS) testexpand_LDADD = $(LDADDS) testexpander_LDADD = $(LDADDS) @@ -383,6 +387,9 @@ testcellarea_SOURCES = \ cellareascaffold.c \ cellareascaffold.h +testtreemenu_SOURCES = \ + testtreemenu.c + testoffscreen_SOURCES = \ gtkoffscreenbox.c \ gtkoffscreenbox.h \ diff --git a/tests/testtreemenu.c b/tests/testtreemenu.c new file mode 100644 index 0000000000..84c856401c --- /dev/null +++ b/tests/testtreemenu.c @@ -0,0 +1,245 @@ +#include +#include "cellareascaffold.h" + +/******************************************************* + * Simple Test * + *******************************************************/ +enum { + SIMPLE_COLUMN_NAME, + SIMPLE_COLUMN_ICON, + SIMPLE_COLUMN_DESCRIPTION, + N_SIMPLE_COLUMNS +}; + +static GtkCellRenderer *cell_1 = NULL, *cell_2 = NULL, *cell_3 = NULL; + +static GtkTreeModel * +simple_tree_model (void) +{ + GtkTreeIter iter; + GtkTreeStore *store = + gtk_tree_store_new (N_SIMPLE_COLUMNS, + G_TYPE_STRING, /* name text */ + G_TYPE_STRING, /* icon name */ + G_TYPE_STRING); /* description text */ + + gtk_tree_store_append (store, &iter, NULL); + gtk_tree_store_set (store, &iter, + SIMPLE_COLUMN_NAME, "Alice in wonderland", + SIMPLE_COLUMN_ICON, "gtk-execute", + SIMPLE_COLUMN_DESCRIPTION, + "Twas brillig, and the slithy toves " + "did gyre and gimble in the wabe", + -1); + + gtk_tree_store_append (store, &iter, NULL); + gtk_tree_store_set (store, &iter, + SIMPLE_COLUMN_NAME, "Marry Poppins", + SIMPLE_COLUMN_ICON, "gtk-yes", + SIMPLE_COLUMN_DESCRIPTION, "Supercalifragilisticexpialidocious", + -1); + + gtk_tree_store_append (store, &iter, NULL); + gtk_tree_store_set (store, &iter, + SIMPLE_COLUMN_NAME, "George Bush", + SIMPLE_COLUMN_ICON, "gtk-dialog-warning", + SIMPLE_COLUMN_DESCRIPTION, "It's a very good question, very direct, " + "and I'm not going to answer it", + -1); + + gtk_tree_store_append (store, &iter, NULL); + gtk_tree_store_set (store, &iter, + SIMPLE_COLUMN_NAME, "Whinnie the pooh", + SIMPLE_COLUMN_ICON, "gtk-stop", + SIMPLE_COLUMN_DESCRIPTION, "The most wonderful thing about tiggers, " + "is tiggers are wonderful things", + -1); + + gtk_tree_store_append (store, &iter, NULL); + gtk_tree_store_set (store, &iter, + SIMPLE_COLUMN_NAME, "Aleister Crowley", + SIMPLE_COLUMN_ICON, "gtk-about", + SIMPLE_COLUMN_DESCRIPTION, + "Thou shalt do what thou wilt shall be the whole of the law", + -1); + + gtk_tree_store_append (store, &iter, NULL); + gtk_tree_store_set (store, &iter, + SIMPLE_COLUMN_NAME, "Mark Twain", + SIMPLE_COLUMN_ICON, "gtk-quit", + SIMPLE_COLUMN_DESCRIPTION, + "Giving up smoking is the easiest thing in the world. " + "I know because I've done it thousands of times.", + -1); + + + return (GtkTreeModel *)store; +} + +static GtkWidget * +simple_tree_menu (void) +{ + GtkTreeModel *model; + GtkWidget *menu; + GtkCellArea *area; + GtkCellRenderer *renderer; + + model = simple_tree_model (); + + menu = gtk_tree_menu_new (); + gtk_tree_menu_set_model (GTK_TREE_MENU (menu), model); + + area = gtk_cell_layout_get_area (GTK_CELL_LAYOUT (menu)); + + cell_1 = renderer = gtk_cell_renderer_text_new (); + gtk_cell_area_box_pack_start (GTK_CELL_AREA_BOX (area), renderer, FALSE, FALSE); + gtk_cell_area_attribute_connect (area, renderer, "text", SIMPLE_COLUMN_NAME); + + cell_2 = renderer = gtk_cell_renderer_pixbuf_new (); + g_object_set (G_OBJECT (renderer), "xalign", 0.0F, NULL); + gtk_cell_area_box_pack_start (GTK_CELL_AREA_BOX (area), renderer, TRUE, FALSE); + gtk_cell_area_attribute_connect (area, renderer, "stock-id", SIMPLE_COLUMN_ICON); + + cell_3 = renderer = gtk_cell_renderer_text_new (); + g_object_set (G_OBJECT (renderer), + "wrap-mode", PANGO_WRAP_WORD, + "wrap-width", 215, + NULL); + gtk_cell_area_box_pack_start (GTK_CELL_AREA_BOX (area), renderer, FALSE, TRUE); + gtk_cell_area_attribute_connect (area, renderer, "text", SIMPLE_COLUMN_DESCRIPTION); + + return menu; +} + +static void +align_cell_2_toggled (GtkToggleButton *toggle, + GtkTreeMenu *menu) +{ + GtkCellArea *area = gtk_cell_layout_get_area (GTK_CELL_LAYOUT (menu)); + gboolean align = gtk_toggle_button_get_active (toggle); + + gtk_cell_area_cell_set (area, cell_2, "align", align, NULL); +} + +static void +align_cell_3_toggled (GtkToggleButton *toggle, + GtkTreeMenu *menu) +{ + GtkCellArea *area = gtk_cell_layout_get_area (GTK_CELL_LAYOUT (menu)); + gboolean align = gtk_toggle_button_get_active (toggle); + + gtk_cell_area_cell_set (area, cell_3, "align", align, NULL); +} + +static void +expand_cell_1_toggled (GtkToggleButton *toggle, + GtkTreeMenu *menu) +{ + GtkCellArea *area = gtk_cell_layout_get_area (GTK_CELL_LAYOUT (menu)); + gboolean expand = gtk_toggle_button_get_active (toggle); + + gtk_cell_area_cell_set (area, cell_1, "expand", expand, NULL); +} + +static void +expand_cell_2_toggled (GtkToggleButton *toggle, + GtkTreeMenu *menu) +{ + GtkCellArea *area = gtk_cell_layout_get_area (GTK_CELL_LAYOUT (menu)); + gboolean expand = gtk_toggle_button_get_active (toggle); + + gtk_cell_area_cell_set (area, cell_2, "expand", expand, NULL); +} + +static void +expand_cell_3_toggled (GtkToggleButton *toggle, + GtkTreeMenu *menu) +{ + GtkCellArea *area = gtk_cell_layout_get_area (GTK_CELL_LAYOUT (menu)); + gboolean expand = gtk_toggle_button_get_active (toggle); + + gtk_cell_area_cell_set (area, cell_3, "expand", expand, NULL); +} + +static void +tree_menu (void) +{ + GtkWidget *window, *widget; + GtkWidget *menu, *menubar, *vbox, *menuitem; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + + gtk_window_set_title (GTK_WINDOW (window), "GtkTreeMenu"); + + menu = simple_tree_menu (); + + vbox = gtk_vbox_new (FALSE, 4); + gtk_widget_show (vbox); + + menubar = gtk_menu_bar_new (); + menuitem = gtk_menu_item_new_with_label ("Tree"); + gtk_widget_show (menu); + gtk_widget_show (menubar); + gtk_widget_show (menuitem); + gtk_menu_shell_append (GTK_MENU_SHELL (menubar), menuitem); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (menuitem), menu); + + gtk_box_pack_start (GTK_BOX (vbox), menubar, FALSE, FALSE, 0); + + /* Now add some controls */ + widget = gtk_check_button_new_with_label ("Align 2nd Cell"); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), FALSE); + gtk_widget_show (widget); + gtk_box_pack_start (GTK_BOX (vbox), widget, FALSE, FALSE, 0); + + g_signal_connect (G_OBJECT (widget), "toggled", + G_CALLBACK (align_cell_2_toggled), menu); + + widget = gtk_check_button_new_with_label ("Align 3rd Cell"); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), TRUE); + gtk_widget_show (widget); + gtk_box_pack_start (GTK_BOX (vbox), widget, FALSE, FALSE, 0); + + g_signal_connect (G_OBJECT (widget), "toggled", + G_CALLBACK (align_cell_3_toggled), menu); + + widget = gtk_check_button_new_with_label ("Expand 1st Cell"); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), FALSE); + gtk_widget_show (widget); + gtk_box_pack_start (GTK_BOX (vbox), widget, FALSE, FALSE, 0); + + g_signal_connect (G_OBJECT (widget), "toggled", + G_CALLBACK (expand_cell_1_toggled), menu); + + widget = gtk_check_button_new_with_label ("Expand 2nd Cell"); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), TRUE); + gtk_widget_show (widget); + gtk_box_pack_start (GTK_BOX (vbox), widget, FALSE, FALSE, 0); + + g_signal_connect (G_OBJECT (widget), "toggled", + G_CALLBACK (expand_cell_2_toggled), menu); + + widget = gtk_check_button_new_with_label ("Expand 3rd Cell"); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), FALSE); + gtk_widget_show (widget); + gtk_box_pack_start (GTK_BOX (vbox), widget, FALSE, FALSE, 0); + + g_signal_connect (G_OBJECT (widget), "toggled", + G_CALLBACK (expand_cell_3_toggled), menu); + + gtk_container_add (GTK_CONTAINER (window), vbox); + + gtk_widget_show (window); +} + +int +main (int argc, char *argv[]) +{ + gtk_init (NULL, NULL); + + tree_menu (); + + gtk_main (); + + return 0; +}