/* * baobab-chart.c * * Copyright (C) 2006, 2007, 2008 Igalia * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301 USA * * Authors: * Felipe Erias * Pablo Santamaria * Jacobo Aragunde * Eduardo Lima * Mario Sanchez * Miguel Gomez * Henrique Ferreiro * Alejandro Pinheiro * Carlos Sanmartin * Alejandro Garcia */ #include #include #include #if GTK_CHECK_VERSION (3, 0, 0) /* needed for floor and ceil */ #include #endif #include "baobab-chart.h" #define BAOBAB_CHART_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), \ BAOBAB_CHART_TYPE, \ BaobabChartPrivate)) #define SNAPSHOT_DEF_FILENAME_FORMAT "%s-disk-usage" G_DEFINE_ABSTRACT_TYPE (BaobabChart, baobab_chart, GTK_TYPE_WIDGET); #define BAOBAB_CHART_MAX_DEPTH 8 #define BAOBAB_CHART_MIN_DEPTH 1 enum { LEFT_BUTTON = 1, MIDDLE_BUTTON = 2, RIGHT_BUTTON = 3 }; struct _BaobabChartPrivate { guint name_column; guint size_column; guint info_column; guint percentage_column; guint valid_column; gboolean button_pressed; gboolean is_frozen; cairo_surface_t *memento; guint max_depth; gboolean model_changed; GtkTreeModel *model; GtkTreeRowReference *root; GList *first_item; GList *last_item; GList *highlighted_item; }; /* Signals */ enum { ITEM_ACTIVATED, LAST_SIGNAL }; static guint baobab_chart_signals [LAST_SIGNAL] = { 0 }; /* Properties */ enum { PROP_0, PROP_MAX_DEPTH, PROP_MODEL, PROP_ROOT, }; /* Colors */ const BaobabChartColor baobab_chart_tango_colors[] = {{0.94, 0.16, 0.16}, /* tango: ef2929 */ {0.68, 0.49, 0.66}, /* tango: ad7fa8 */ {0.45, 0.62, 0.82}, /* tango: 729fcf */ {0.54, 0.89, 0.20}, /* tango: 8ae234 */ {0.91, 0.73, 0.43}, /* tango: e9b96e */ {0.99, 0.68, 0.25}}; /* tango: fcaf3e */ static void baobab_chart_class_init (BaobabChartClass *class); static void baobab_chart_init (BaobabChart *object); static void baobab_chart_realize (GtkWidget *widget); static void baobab_chart_dispose (GObject *object); static void baobab_chart_size_allocate (GtkWidget *widget, GtkAllocation *allocation); static void baobab_chart_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec); static void baobab_chart_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); static void baobab_chart_free_items (GtkWidget *chart); static void baobab_chart_draw (GtkWidget *chart, cairo_t *cr, GdkRectangle area); static void baobab_chart_update_draw (BaobabChart *chart, GtkTreePath *path); static void baobab_chart_row_changed (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data); static void baobab_chart_row_inserted (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data); static void baobab_chart_row_has_child_toggled (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data); static void baobab_chart_row_deleted (GtkTreeModel *model, GtkTreePath *path, gpointer data); static void baobab_chart_rows_reordered (GtkTreeModel *model, GtkTreePath *parent, GtkTreeIter *iter, gint *new_order, gpointer data); static gboolean baobab_chart_expose (GtkWidget *chart, #if GTK_CHECK_VERSION (3, 0, 0) cairo_t *cr); #else GdkEventExpose *event); #endif static void baobab_chart_interpolate_colors (BaobabChartColor *color, BaobabChartColor colora, BaobabChartColor colorb, gdouble percentage); static gint baobab_chart_button_release (GtkWidget *widget, GdkEventButton *event); static gint baobab_chart_scroll (GtkWidget *widget, GdkEventScroll *event); static gint baobab_chart_motion_notify (GtkWidget *widget, GdkEventMotion *event); static gint baobab_chart_leave_notify (GtkWidget *widget, GdkEventCrossing *event); static inline void baobab_chart_disconnect_signals (GtkWidget *chart, GtkTreeModel *model); static inline void baobab_chart_connect_signals (GtkWidget *chart, GtkTreeModel *model); static void baobab_chart_get_items (GtkWidget *chart, GtkTreePath *root); static gboolean baobab_chart_query_tooltip (GtkWidget *widget, gint x, gint y, gboolean keyboard_mode, GtkTooltip *tooltip, gpointer user_data); static void baobab_chart_class_init (BaobabChartClass *class) { GObjectClass *obj_class; GtkWidgetClass *widget_class; obj_class = G_OBJECT_CLASS (class); widget_class = GTK_WIDGET_CLASS (class); /* GtkObject signals */ obj_class->set_property = baobab_chart_set_property; obj_class->get_property = baobab_chart_get_property; obj_class->dispose = baobab_chart_dispose; /* GtkWidget signals */ widget_class->realize = baobab_chart_realize; #if GTK_CHECK_VERSION (3, 0, 0) widget_class->draw = baobab_chart_expose; #else widget_class->expose_event = baobab_chart_expose; #endif widget_class->size_allocate = baobab_chart_size_allocate; widget_class->scroll_event = baobab_chart_scroll; /* Baobab Chart abstract methods */ class->draw_item = NULL; class->pre_draw = NULL; class->post_draw = NULL; class->calculate_item_geometry = NULL; class->is_point_over_item = NULL; class->get_item_rectangle = NULL; class->can_zoom_in = NULL; class->can_zoom_out = NULL; g_object_class_install_property (obj_class, PROP_MAX_DEPTH, g_param_spec_int ("max-depth", _("Maximum depth"), _("The maximum depth drawn in the chart from the root"), 1, BAOBAB_CHART_MAX_DEPTH, BAOBAB_CHART_MAX_DEPTH, G_PARAM_READWRITE)); g_object_class_install_property (obj_class, PROP_MODEL, g_param_spec_object ("model", _("Chart model"), _("Set the model of the chart"), GTK_TYPE_TREE_MODEL, G_PARAM_READWRITE)); g_object_class_install_property (obj_class, PROP_ROOT, g_param_spec_boxed ("root", _("Chart root node"), _("Set the root node from the model"), GTK_TYPE_TREE_ITER, G_PARAM_READWRITE)); baobab_chart_signals[ITEM_ACTIVATED] = g_signal_new ("item_activated", G_TYPE_FROM_CLASS (obj_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (BaobabChartClass, item_activated), NULL, NULL, g_cclosure_marshal_VOID__BOXED, G_TYPE_NONE, 1, GTK_TYPE_TREE_ITER); g_type_class_add_private (obj_class, sizeof (BaobabChartPrivate)); } static void baobab_chart_init (BaobabChart *chart) { BaobabChartPrivate *priv; priv = BAOBAB_CHART_GET_PRIVATE (chart); chart->priv = priv; priv->model = NULL; priv->max_depth = BAOBAB_CHART_MAX_DEPTH; priv->name_column = 0; priv->size_column = 0; priv->info_column = 0; priv->percentage_column = 0; priv->valid_column = 0; priv->button_pressed = FALSE; priv->is_frozen = FALSE; priv->memento = NULL; priv->root = NULL; priv->first_item = NULL; priv->last_item = NULL; priv->highlighted_item = NULL; } static void baobab_chart_dispose (GObject *object) { BaobabChartPrivate *priv; baobab_chart_free_items (GTK_WIDGET (object)); priv = BAOBAB_CHART (object)->priv; if (priv->model) { baobab_chart_disconnect_signals (GTK_WIDGET (object), priv->model); g_object_unref (priv->model); priv->model = NULL; } if (priv->root) { gtk_tree_row_reference_free (priv->root); priv->root = NULL; } G_OBJECT_CLASS (baobab_chart_parent_class)->dispose (object); } static void baobab_chart_realize (GtkWidget *widget) { BaobabChart *chart; GdkWindowAttr attributes; gint attributes_mask; GtkAllocation allocation; GdkWindow *window; g_return_if_fail (BAOBAB_IS_CHART (widget)); chart = BAOBAB_CHART (widget); gtk_widget_set_realized (widget, TRUE); gtk_widget_get_allocation (widget, &allocation); attributes.window_type = GDK_WINDOW_CHILD; attributes.x = allocation.x; attributes.y = allocation.y; attributes.width = allocation.width; attributes.height = allocation.height; attributes.wclass = GDK_INPUT_OUTPUT; attributes.visual = gtk_widget_get_visual (widget); #if !GTK_CHECK_VERSION (3, 0, 0) attributes.colormap = gtk_widget_get_colormap (widget); #endif attributes.event_mask = gtk_widget_get_events (widget) | GDK_EXPOSURE_MASK; #if GTK_CHECK_VERSION (3, 0, 0) attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL; #else attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP; #endif window = gdk_window_new (gtk_widget_get_parent_window (widget), &attributes, attributes_mask); gtk_widget_set_window (widget, window); gdk_window_set_user_data (window, chart); gtk_widget_style_attach (widget); gtk_style_set_background (gtk_widget_get_style (widget), window, GTK_STATE_NORMAL); gtk_widget_add_events (widget, GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_LEAVE_NOTIFY_MASK); } static void baobab_chart_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { BaobabChartPrivate *priv; BaobabChartClass *class; BaobabChartItem *item; GList *node; g_return_if_fail (BAOBAB_IS_CHART (widget)); g_return_if_fail (allocation != NULL); priv = BAOBAB_CHART (widget)->priv; class = BAOBAB_CHART_GET_CLASS (widget); gtk_widget_set_allocation (widget, allocation); if (gtk_widget_get_realized (widget)) { gdk_window_move_resize (gtk_widget_get_window (widget), allocation->x, allocation->y, allocation->width, allocation->height); node = priv->first_item; while (node != NULL) { item = (BaobabChartItem *) node->data; item->has_visible_children = FALSE; item->visible = FALSE; class->calculate_item_geometry (widget, item); node = node->next; } } } static void baobab_chart_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { BaobabChart *chart; chart = BAOBAB_CHART (object); switch (prop_id) { case PROP_MAX_DEPTH: baobab_chart_set_max_depth (GTK_WIDGET (chart), g_value_get_int (value)); break; case PROP_MODEL: baobab_chart_set_model (GTK_WIDGET (chart), g_value_get_object (value)); break; case PROP_ROOT: baobab_chart_set_root (GTK_WIDGET (chart), g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void baobab_chart_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { BaobabChartPrivate *priv; priv = BAOBAB_CHART (object)->priv; switch (prop_id) { case PROP_MAX_DEPTH: g_value_set_int (value, priv->max_depth); break; case PROP_MODEL: g_value_set_object (value, priv->model); break; case PROP_ROOT: g_value_set_object (value, priv->root); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static GList *baobab_chart_add_item (GtkWidget *chart, guint depth, gdouble rel_start, gdouble rel_size, GtkTreeIter iter) { BaobabChartPrivate *priv; BaobabChartItem *item; gchar *name; gchar *size; priv = BAOBAB_CHART_GET_PRIVATE (chart); gtk_tree_model_get (priv->model, &iter, priv->name_column, &name, -1); gtk_tree_model_get (priv->model, &iter, priv->size_column, &size, -1); item = g_new (BaobabChartItem, 1); item->name = name; item->size = size; item->depth = depth; item->rel_start = rel_start; item->rel_size = rel_size; item->has_any_child = FALSE; item->visible = FALSE; item->has_visible_children = FALSE; item->iter = iter; item->parent = NULL; item->data = NULL; priv->last_item = g_list_prepend (priv->last_item, item); return priv->last_item; } static void baobab_chart_free_items (GtkWidget *chart) { BaobabChartPrivate *priv; BaobabChartItem *item; GList *node; GList *next; priv = BAOBAB_CHART_GET_PRIVATE (chart); node = priv->first_item; while (node != NULL) { next = node->next; item = (BaobabChartItem *) node->data; g_free (item->name); g_free (item->size); g_free (item->data); item->data = NULL; g_free (item); g_list_free_1 (node); node = next; } priv->first_item = NULL; priv->last_item = NULL; priv->highlighted_item = NULL; } static void baobab_chart_get_items (GtkWidget *chart, GtkTreePath *root) { BaobabChartPrivate *priv; BaobabChartItem *item; GList *node; GtkTreeIter initial_iter = {0}; gdouble size; GtkTreePath *model_root_path; GtkTreeIter model_root_iter; BaobabChartClass *class; GtkTreeIter child_iter = {0}; GList *child_node; BaobabChartItem *child; gdouble rel_start; priv = BAOBAB_CHART_GET_PRIVATE (chart); /* First we free current item list */ baobab_chart_free_items (chart); /* Get the tree iteration corresponding to root */ if (!gtk_tree_model_get_iter (priv->model, &initial_iter, root)) { priv->model_changed = FALSE; return; } model_root_path = gtk_tree_path_new_first (); gtk_tree_model_get_iter (priv->model, &model_root_iter, model_root_path); gtk_tree_path_free (model_root_path); gtk_tree_model_get (priv->model, &model_root_iter, priv->percentage_column, &size, -1); /* Create first item */ node = baobab_chart_add_item (chart, 0, 0, 100, initial_iter); /* Iterate through childs building the list */ class = BAOBAB_CHART_GET_CLASS (chart); do { item = (BaobabChartItem *) node->data; item->has_any_child = gtk_tree_model_iter_children (priv->model, &child_iter, &(item->iter)); /* Calculate item geometry */ class->calculate_item_geometry (chart, item); if (! item->visible) { node = node->prev; continue; } /* Get item's children and add them to the list */ if ((item->has_any_child) && (item->depth < priv->max_depth + 1)) { rel_start = 0; do { gtk_tree_model_get (priv->model, &child_iter, priv->percentage_column, &size, -1); child_node = baobab_chart_add_item (chart, item->depth + 1, rel_start, size, child_iter); child = (BaobabChartItem *) child_node->data; child->parent = node; rel_start += size; } while (gtk_tree_model_iter_next (priv->model, &child_iter)); } node = node->prev; } while (node != NULL); /* Reverse the list, 'cause we created it from the tail, for efficiency reasons */ priv->first_item = g_list_reverse (priv->last_item); priv->model_changed = FALSE; } static void baobab_chart_draw (GtkWidget *chart, cairo_t *cr, GdkRectangle area) { BaobabChartPrivate *priv; BaobabChartClass *class; GList *node; BaobabChartItem *item; gboolean highlighted; priv = BAOBAB_CHART_GET_PRIVATE (chart); class = BAOBAB_CHART_GET_CLASS (chart); /* call pre-draw abstract method */ if (class->pre_draw) class->pre_draw (chart, cr); cairo_save (cr); node = priv->first_item; while (node != NULL) { item = (BaobabChartItem *) node->data; if ((item->visible) && (gdk_rectangle_intersect (&area, &item->rect, NULL)) && (item->depth <= priv->max_depth)) { highlighted = (node == priv->highlighted_item); class->draw_item (chart, cr, item, highlighted); } node = node->next; } cairo_restore (cr); /* call post-draw abstract method */ if (class->post_draw) class->post_draw (chart, cr); } static void baobab_chart_update_draw (BaobabChart* chart, GtkTreePath *path) { BaobabChartPrivate *priv; GtkTreePath *root_path = NULL; gint root_depth, node_depth; if (!gtk_widget_get_realized ( GTK_WIDGET (chart))) return; priv = BAOBAB_CHART (chart)->priv; if (priv->root != NULL) { root_path = gtk_tree_row_reference_get_path (priv->root); if (root_path == NULL) { gtk_tree_row_reference_free (priv->root); priv->root = NULL; } } if (priv->root == NULL) root_path = gtk_tree_path_new_first (); root_depth = gtk_tree_path_get_depth (root_path); node_depth = gtk_tree_path_get_depth (path); if (((node_depth-root_depth)<=priv->max_depth)&& ((gtk_tree_path_is_ancestor (root_path, path))|| (gtk_tree_path_compare (root_path, path) == 0))) { gtk_widget_queue_draw (GTK_WIDGET (chart)); } gtk_tree_path_free (root_path); } static void baobab_chart_row_changed (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { g_return_if_fail (BAOBAB_IS_CHART (data)); g_return_if_fail (path != NULL || iter != NULL); BAOBAB_CHART_GET_PRIVATE (data)->model_changed = TRUE; baobab_chart_update_draw (BAOBAB_CHART (data), path); } static void baobab_chart_row_inserted (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { g_return_if_fail (BAOBAB_IS_CHART (data)); g_return_if_fail (path != NULL || iter != NULL); BAOBAB_CHART_GET_PRIVATE (data)->model_changed = TRUE; baobab_chart_update_draw (BAOBAB_CHART (data), path); } static void baobab_chart_row_has_child_toggled (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) { g_return_if_fail (BAOBAB_IS_CHART (data)); g_return_if_fail (path != NULL || iter != NULL); BAOBAB_CHART_GET_PRIVATE (data)->model_changed = TRUE; baobab_chart_update_draw (BAOBAB_CHART (data), path); } static void baobab_chart_row_deleted (GtkTreeModel *model, GtkTreePath *path, gpointer data) { g_return_if_fail (BAOBAB_IS_CHART (data)); g_return_if_fail (path != NULL); BAOBAB_CHART_GET_PRIVATE (data)->model_changed = TRUE; baobab_chart_update_draw (BAOBAB_CHART (data), path); } static void baobab_chart_rows_reordered (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gint *new_order, gpointer data) { g_return_if_fail (BAOBAB_IS_CHART (data)); g_return_if_fail (path != NULL || iter != NULL); BAOBAB_CHART_GET_PRIVATE (data)->model_changed = TRUE; baobab_chart_update_draw (BAOBAB_CHART (data), path); } static gboolean #if GTK_CHECK_VERSION (3, 0, 0) baobab_chart_expose (GtkWidget *chart, cairo_t *cr) #else baobab_chart_expose (GtkWidget *chart, GdkEventExpose *event) #endif { #if !GTK_CHECK_VERSION (3, 0, 0) cairo_t *cr; #endif BaobabChartPrivate *priv; gint w, h; gdouble p, sx, sy; GtkTreePath *root_path = NULL; GtkTreePath *current_path = NULL; GtkAllocation allocation; #if GTK_CHECK_VERSION (3, 0, 0) GdkRectangle area; gdouble x1, y1, x2, y2; cairo_clip_extents (cr, &x1, &y1, &x2, &y2); area.x = floor (x1); area.y = floor (y1); area.width = ceil (x2) - area.x; area.height = ceil (y2) - area.y; #endif priv = BAOBAB_CHART (chart)->priv; /* the columns are not set we paint nothing */ if (priv->name_column == priv->percentage_column) return FALSE; /* get a cairo_t */ cr = gdk_cairo_create (gtk_widget_get_window (chart)); cairo_rectangle (cr, #if GTK_CHECK_VERSION (3, 0, 0) area.x, area.y, area.width, area.height); #else event->area.x, event->area.y, event->area.width, event->area.height); #endif /* there is no model we can not paint */ if ((priv->is_frozen) || (priv->model == NULL)) { if (priv->memento != NULL) { w = cairo_image_surface_get_width (priv->memento); h = cairo_image_surface_get_height (priv->memento); cairo_clip (cr); gtk_widget_get_allocation (GTK_WIDGET (chart), &allocation); if (w > 0 && h > 0 && !(allocation.width == w && allocation.height == h)) { /* minimal available proportion */ p = MIN (allocation.width / (1.0 * w), allocation.height / (1.0 * h)); sx = (gdouble) (allocation.width - w * p) / 2.0; sy = (gdouble) (allocation.height - h * p) / 2.0; cairo_translate (cr, sx, sy); cairo_scale (cr, p, p); } cairo_set_source_surface (cr, priv->memento, 0, 0); cairo_paint (cr); } } else { cairo_set_source_rgb (cr, 1, 1, 1); cairo_fill_preserve (cr); cairo_clip (cr); if (priv->root != NULL) root_path = gtk_tree_row_reference_get_path (priv->root); if (root_path == NULL) { root_path = gtk_tree_path_new_first (); priv->root = NULL; } /* Check if tree model was modified in any way */ if ((priv->model_changed) || (priv->first_item == NULL)) baobab_chart_get_items (chart, root_path); else { /* Check if root was changed */ current_path = gtk_tree_model_get_path (priv->model, &((BaobabChartItem*) priv->first_item->data)->iter); if (gtk_tree_path_compare (root_path, current_path) != 0) baobab_chart_get_items (chart, root_path); gtk_tree_path_free (current_path); } gtk_tree_path_free (root_path); #if GTK_CHECK_VERSION (3, 0, 0) baobab_chart_draw (chart, cr, area); #else baobab_chart_draw (chart, cr, event->area); #endif } #if !GTK_CHECK_VERSION (3, 0, 0) cairo_destroy (cr); #endif return FALSE; } static void baobab_chart_interpolate_colors (BaobabChartColor *color, BaobabChartColor colora, BaobabChartColor colorb, gdouble percentage) { gdouble diff; diff = colora.red - colorb.red; color->red = colora.red-diff*percentage; diff = colora.green - colorb.green; color->green = colora.green-diff*percentage; diff = colora.blue - colorb.blue; color->blue = colora.blue-diff*percentage; } void baobab_chart_get_item_color (BaobabChartColor *color, gdouble rel_position, gint depth, gboolean highlighted) { gdouble intensity; gint color_number; gint next_color_number; gdouble maximum; static const BaobabChartColor level_color = {0.83, 0.84, 0.82}; static const BaobabChartColor level_color_hl = {0.88, 0.89, 0.87}; intensity = 1 - (((depth-1)*0.3) / BAOBAB_CHART_MAX_DEPTH); if (depth == 0) *color = level_color; else { color_number = rel_position / (100/3); next_color_number = (color_number + 1) % 6; baobab_chart_interpolate_colors (color, baobab_chart_tango_colors[color_number], baobab_chart_tango_colors[next_color_number], (rel_position - color_number * 100/3) / (100/3)); color->red = color->red * intensity; color->green = color->green * intensity; color->blue = color->blue * intensity; } if (highlighted) { if (depth == 0) *color = level_color_hl; else { maximum = MAX (color->red, MAX (color->green, color->blue)); color->red /= maximum; color->green /= maximum; color->blue /= maximum; } } } static gint baobab_chart_button_release (GtkWidget *widget, GdkEventButton *event) { BaobabChartPrivate *priv; priv = BAOBAB_CHART (widget)->priv; if (priv->is_frozen) return TRUE; switch (event->button) { case LEFT_BUTTON: /* Enter into a subdir */ if (priv->highlighted_item != NULL) g_signal_emit (BAOBAB_CHART (widget), baobab_chart_signals[ITEM_ACTIVATED], 0, &((BaobabChartItem*) priv->highlighted_item->data)->iter); break; case MIDDLE_BUTTON: /* Go back to the parent dir */ baobab_chart_move_up_root (widget); break; } return FALSE; } static gint baobab_chart_scroll (GtkWidget *widget, GdkEventScroll *event) { switch (event->direction) { case GDK_SCROLL_LEFT : case GDK_SCROLL_UP : if (baobab_chart_can_zoom_out (widget)) baobab_chart_zoom_out (widget); /* change the selected item when zooming */ baobab_chart_motion_notify (widget, (GdkEventMotion *)event); break; case GDK_SCROLL_RIGHT : case GDK_SCROLL_DOWN : if (baobab_chart_can_zoom_in (widget)) baobab_chart_zoom_in (widget); break; } return FALSE; } static void baobab_chart_set_item_highlight (GtkWidget *chart, GList *node, gboolean highlighted) { BaobabChartItem *item; BaobabChartPrivate *priv; BaobabChartClass *class; if (node == NULL) return; item = (BaobabChartItem *) node->data; priv = BAOBAB_CHART_GET_PRIVATE (chart); class = BAOBAB_CHART_GET_CLASS (chart); if (highlighted) priv->highlighted_item = node; else priv->highlighted_item = NULL; gdk_window_invalidate_rect (gtk_widget_get_window ( GTK_WIDGET (chart)), &item->rect, TRUE); } static gint baobab_chart_motion_notify (GtkWidget *widget, GdkEventMotion *event) { BaobabChartPrivate *priv; BaobabChartClass *class; GList *node; BaobabChartItem *item; gboolean found = FALSE; priv = BAOBAB_CHART_GET_PRIVATE (widget); class = BAOBAB_CHART_GET_CLASS (widget); /* Check if the pointer is over an item */ node = priv->last_item; while (node != NULL) { item = (BaobabChartItem *) node->data; if ((item->visible) && (class->is_point_over_item (widget, item, event->x, event->y))) { if (priv->highlighted_item != node) { baobab_chart_set_item_highlight (widget, priv->highlighted_item, FALSE); gtk_widget_set_has_tooltip (widget, TRUE); baobab_chart_set_item_highlight (widget, node, TRUE); } found = TRUE; break; } node = node->prev; } /* If we never found a highlighted item, but there is an old highlighted item, redraw it to turn it off */ if (! found) { baobab_chart_set_item_highlight (widget, priv->highlighted_item, FALSE); gtk_widget_set_has_tooltip (widget, FALSE); } /* Continue receiving motion notifies */ gdk_event_request_motions (event); return FALSE; } static gint baobab_chart_leave_notify (GtkWidget *widget, GdkEventCrossing *event) { BaobabChartPrivate *priv; priv = BAOBAB_CHART_GET_PRIVATE (widget); baobab_chart_set_item_highlight (widget, priv->highlighted_item, FALSE); return FALSE; } static inline void baobab_chart_connect_signals (GtkWidget *chart, GtkTreeModel *model) { g_signal_connect (model, "row_changed", G_CALLBACK (baobab_chart_row_changed), chart); g_signal_connect (model, "row_inserted", G_CALLBACK (baobab_chart_row_inserted), chart); g_signal_connect (model, "row_has_child_toggled", G_CALLBACK (baobab_chart_row_has_child_toggled), chart); g_signal_connect (model, "row_deleted", G_CALLBACK (baobab_chart_row_deleted), chart); g_signal_connect (model, "rows_reordered", G_CALLBACK (baobab_chart_rows_reordered), chart); g_signal_connect (chart, "query-tooltip", G_CALLBACK (baobab_chart_query_tooltip), chart); g_signal_connect (chart, "motion-notify-event", G_CALLBACK (baobab_chart_motion_notify), chart); g_signal_connect (chart, "leave-notify-event", G_CALLBACK (baobab_chart_leave_notify), chart); g_signal_connect (chart, "button-release-event", G_CALLBACK (baobab_chart_button_release), chart); } static inline void baobab_chart_disconnect_signals (GtkWidget *chart, GtkTreeModel *model) { g_signal_handlers_disconnect_by_func (model, baobab_chart_row_changed, chart); g_signal_handlers_disconnect_by_func (model, baobab_chart_row_inserted, chart); g_signal_handlers_disconnect_by_func (model, baobab_chart_row_has_child_toggled, chart); g_signal_handlers_disconnect_by_func (model, baobab_chart_row_deleted, chart); g_signal_handlers_disconnect_by_func (model, baobab_chart_rows_reordered, chart); g_signal_handlers_disconnect_by_func (chart, baobab_chart_query_tooltip, chart); g_signal_handlers_disconnect_by_func (chart, baobab_chart_motion_notify, chart); g_signal_handlers_disconnect_by_func (chart, baobab_chart_leave_notify, chart); g_signal_handlers_disconnect_by_func (chart, baobab_chart_button_release, chart); } static gboolean baobab_chart_query_tooltip (GtkWidget *widget, gint x, gint y, gboolean keyboard_mode, GtkTooltip *tooltip, gpointer user_data) { BaobabChartPrivate *priv; BaobabChartItem *item; char *markup; priv = BAOBAB_CHART_GET_PRIVATE (widget); if (priv->highlighted_item == NULL) return FALSE; item = (BaobabChartItem *) priv->highlighted_item->data; if ( (item->name == NULL) || (item->size == NULL) ) return FALSE; gtk_tooltip_set_tip_area (tooltip, &item->rect); markup = g_strconcat (item->name, "\n", item->size, NULL); gtk_tooltip_set_markup (tooltip, markup); g_free (markup); return TRUE; } GdkPixbuf* baobab_chart_get_pixbuf (GtkWidget *widget) { gint w, h; GdkPixbuf *pixbuf; g_return_val_if_fail (BAOBAB_IS_CHART (widget), NULL); #if GTK_CHECK_VERSION(3, 0, 0) w = gdk_window_get_width(gtk_widget_get_window(widget)); h = gdk_window_get_height(gtk_widget_get_window(widget)); #else gdk_drawable_get_size(gtk_widget_get_window(widget), &w, &h); #endif #if GTK_CHECK_VERSION(3, 0, 0) pixbuf = gdk_pixbuf_get_from_window ( #else pixbuf = gdk_pixbuf_get_from_drawable (NULL, #endif gtk_widget_get_window (widget), #if !GTK_CHECK_VERSION(3, 0, 0) gdk_colormap_get_system (), 0, 0, #endif 0, 0, w, h); return pixbuf; } /* Public functions start here */ /** * baobab_chart_new: * * Constructor for the baobab_chart class * * Returns: a new #BaobabChart object * **/ GtkWidget * baobab_chart_new () { return g_object_new (BAOBAB_CHART_TYPE, NULL); } /** * baobab_chart_set_model_with_columns: * @chart: the #BaobabChart whose model is going to be set * @model: the #GtkTreeModel which is going to set as the model of * @chart * @name_column: number of column inside @model where the file name is * stored * @size_column: number of column inside @model where the file size is * stored * @info_column: number of column inside @model where the percentage * of disk usage is stored * @percentage_column: number of column inside @model where the disk * usage percentage is stored * @valid_column: number of column inside @model where the flag indicating * if the row data is right or not. * @root: a #GtkTreePath indicating the node of @model which will be * used as root. * * Sets @model as the #GtkTreeModel used by @chart. Indicates the * columns inside @model where the values file name, file * size, file information, disk usage percentage and data correction are stored, and * the node which will be used as the root of @chart. Once * the model has been successfully set, a redraw of the window is * forced. * This function is intended to be used the first time a #GtkTreeModel * is assigned to @chart, or when the columns containing the needed data * are going to change. In other cases, #baobab_chart_set_model should * be used. * This function does not change the state of the signals from the model, which * is controlled by he #baobab_chart_freeze_updates and the * #baobab_chart_thaw_updates functions. * * Fails if @chart is not a #BaobabChart or if @model is not a * #GtkTreeModel. **/ void baobab_chart_set_model_with_columns (GtkWidget *chart, GtkTreeModel *model, guint name_column, guint size_column, guint info_column, guint percentage_column, guint valid_column, GtkTreePath *root) { BaobabChartPrivate *priv; g_return_if_fail (BAOBAB_IS_CHART (chart)); g_return_if_fail (GTK_IS_TREE_MODEL (model)); priv = BAOBAB_CHART (chart)->priv; baobab_chart_set_model (chart, model); if (root != NULL) { priv->root = gtk_tree_row_reference_new (model, root); g_object_notify (G_OBJECT (chart), "root"); } priv->name_column = name_column; priv->size_column = size_column; priv->info_column = info_column; priv->percentage_column = percentage_column; priv->valid_column = valid_column; } /** * baobab_chart_set_model: * @chart: the #BaobabChart whose model is going to be set * @model: the #GtkTreeModel which is going to set as the model of * @chart * * Sets @model as the #GtkTreeModel used by @chart, and takes the needed * data from the columns especified in the last call to * #baobab_chart_set_model_with_colums. * This function does not change the state of the signals from the model, which * is controlled by he #baobab_chart_freeze_updates and the * #baobab_chart_thaw_updates functions. * * Fails if @chart is not a #BaobabChart or if @model is not a * #GtkTreeModel. **/ void baobab_chart_set_model (GtkWidget *chart, GtkTreeModel *model) { BaobabChartPrivate *priv; g_return_if_fail (BAOBAB_IS_CHART (chart)); g_return_if_fail (GTK_IS_TREE_MODEL (model)); priv = BAOBAB_CHART (chart)->priv; if (model == priv->model) return; if (priv->model) { if (! priv->is_frozen) baobab_chart_disconnect_signals (chart, priv->model); g_object_unref (priv->model); } priv->model = model; g_object_ref (priv->model); if (! priv->is_frozen) baobab_chart_connect_signals (chart, priv->model); if (priv->root) gtk_tree_row_reference_free (priv->root); priv->root = NULL; g_object_notify (G_OBJECT (chart), "model"); gtk_widget_queue_draw (chart); } /** * baobab_chart_get_model: * @chart: a #BaobabChart whose model will be returned. * * Returns the #GtkTreeModel which is the model used by @chart. * * Returns: %NULL if @chart is not a #BaobabChart. **/ GtkTreeModel * baobab_chart_get_model (GtkWidget *chart) { g_return_val_if_fail (BAOBAB_IS_CHART (chart), NULL); return BAOBAB_CHART (chart)->priv->model; } /** * baobab_chart_set_max_depth: * @chart: a #BaobabChart * @max_depth: the new maximum depth to show in the widget. * * Sets the maximum number of nested levels that are going to be show in the * wigdet, and causes a redraw of the widget to show the new maximum * depth. If max_depth is < 1 MAX_DRAWABLE_DEPTH is used. * * Fails if @chart is not a #BaobabChart. **/ void baobab_chart_set_max_depth (GtkWidget *chart, guint max_depth) { BaobabChartPrivate *priv; g_return_if_fail (BAOBAB_IS_CHART (chart)); priv = BAOBAB_CHART_GET_PRIVATE (chart); max_depth = MIN (max_depth, BAOBAB_CHART_MAX_DEPTH); max_depth = MAX (max_depth, BAOBAB_CHART_MIN_DEPTH); if (max_depth == priv->max_depth) return; priv->max_depth = max_depth; g_object_notify (G_OBJECT (chart), "max-depth"); priv->model_changed = TRUE; gtk_widget_queue_draw (chart); } /** * baobab_chart_get_max_depth: * @chart: a #BaobabChart. * * Returns the maximum number of levels that will be show in the * widget. * * Fails if @chart is not a #BaobabChart. **/ guint baobab_chart_get_max_depth (GtkWidget *chart) { g_return_val_if_fail (BAOBAB_IS_CHART (chart), 0); return BAOBAB_CHART (chart)->priv->max_depth; } /** * baobab_chart_set_root: * @chart: a #BaobabChart * @root: a #GtkTreePath indicating the node which will be used as * the widget root. * * Sets the node pointed by @root as the new root of the widget * @chart. * * Fails if @chart is not a #BaobabChart or if @chart has not * a #GtkTreeModel set. **/ void baobab_chart_set_root (GtkWidget *chart, GtkTreePath *root) { BaobabChartPrivate *priv; GtkTreePath *current_root; g_return_if_fail (BAOBAB_IS_CHART (chart)); priv = BAOBAB_CHART (chart)->priv; g_return_if_fail (priv->model != NULL); if (priv->root) { /* Check that given root is different from current */ current_root = gtk_tree_row_reference_get_path (priv->root); if ( (current_root) && (gtk_tree_path_compare (current_root, root) == 0) ) return; /* Free current root */ gtk_tree_row_reference_free (priv->root); } priv->root = gtk_tree_row_reference_new (priv->model, root); g_object_notify (G_OBJECT (chart), "root"); gtk_widget_queue_draw (chart); } /** * baobab_chart_get_root: * @chart: a #BaobabChart. * * Returns a #GtkTreePath pointing to the root of the widget. The * programmer has the responsability to free the used memory once * finished with the returned value. It returns NULL if there is no * root node defined * * Fails if @chart is not a #BaobabChart. **/ GtkTreePath* baobab_chart_get_root (GtkWidget *chart) { g_return_val_if_fail (BAOBAB_IS_CHART (chart), NULL); if (BAOBAB_CHART (chart)->priv->root) return gtk_tree_row_reference_get_path (BAOBAB_CHART (chart)->priv->root); else return NULL; } /** * baobab_chart_freeze_updates: * @chart: the #BaobabChart whose model signals are going to be frozen. * * Disconnects @chart from the signals emitted by its model, and sets * the window of @chart to a "processing" state, so that the window * ignores changes in the chart's model and mouse events. * In order to connect again the window to the model, a call to * #baobab_chart_thaw_updates must be done. * * Fails if @chart is not a #BaobabChart. **/ void baobab_chart_freeze_updates (GtkWidget *chart) { BaobabChartPrivate *priv; cairo_surface_t *surface = NULL; cairo_t *cr = NULL; GdkRectangle area; GtkAllocation allocation; g_return_if_fail (BAOBAB_IS_CHART (chart)); priv = BAOBAB_CHART_GET_PRIVATE (chart); if (priv->is_frozen) return; if (priv->model) baobab_chart_disconnect_signals (chart, priv->model); gtk_widget_get_allocation (GTK_WIDGET (chart), &allocation); surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, allocation.width, allocation.height); if (cairo_surface_status (surface) == CAIRO_STATUS_SUCCESS) { cr = cairo_create (surface); area.x = 0; area.y = 0; area.width = allocation.width; area.height = allocation.height; baobab_chart_draw (chart, cr, area); cairo_rectangle (cr, 0, 0, allocation.width, allocation.height); cairo_set_source_rgba (cr, 0.93, 0.93, 0.93, 0.5); /* tango: eeeeec */ cairo_fill_preserve (cr); cairo_clip (cr); priv->memento = surface; cairo_destroy (cr); } priv->is_frozen = TRUE; gtk_widget_queue_draw (chart); } /** * baobab_chart_thaw_updates: * @chart: the #BaobabChart whose model signals are frozen. * * Reconnects @chart to the signals emitted by its model, which * were disconnected through a call to #baobab_chart_freeze_updates. * Takes the window out of its "processing" state and forces a redraw * of the widget. * * Fails if @chart is not a #BaobabChart. **/ void baobab_chart_thaw_updates (GtkWidget *chart) { BaobabChartPrivate *priv; g_return_if_fail (BAOBAB_IS_CHART (chart)); priv = BAOBAB_CHART_GET_PRIVATE (chart); if (priv->is_frozen) { if (priv->model) baobab_chart_connect_signals (chart, priv->model); if (priv->memento) { cairo_surface_destroy (priv->memento); priv->memento = NULL; } priv->is_frozen = FALSE; priv->model_changed = TRUE; gtk_widget_queue_draw (chart); } } /** * baobab_chart_zoom_in: * @chart: the #BaobabChart requested to zoom in. * * Zooms in the chart by decreasing its maximun depth. * * Fails if @chart is not a #BaobabChart. **/ void baobab_chart_zoom_in (GtkWidget *chart) { BaobabChartPrivate *priv; BaobabChartClass *class; guint new_max_depth; g_return_if_fail (BAOBAB_IS_CHART (chart)); priv = BAOBAB_CHART (chart)->priv; class = BAOBAB_CHART_GET_CLASS (chart); if (class->can_zoom_in != NULL) new_max_depth = class->can_zoom_in (chart); else new_max_depth = priv->max_depth - 1; baobab_chart_set_max_depth (chart, new_max_depth); } /** * baobab_chart_zoom_out: * @chart: the #BaobabChart requested to zoom out. * * Zooms out the chart by increasing its maximun depth. * * Fails if @chart is not a #BaobabChart. **/ void baobab_chart_zoom_out (GtkWidget *chart) { g_return_if_fail (BAOBAB_IS_CHART (chart)); baobab_chart_set_max_depth (chart, baobab_chart_get_max_depth (chart) + 1); } /** * baobab_chart_move_up_root: * @chart: the #BaobabChart whose root is requested to move up one level. * * Move root to the inmediate parent of the current root item of @chart. * * Fails if @chart is not a #BaobabChart. **/ void baobab_chart_move_up_root (GtkWidget *chart) { BaobabChartPrivate *priv; GtkTreeIter parent_iter; GtkTreePath *path; GtkTreeIter root_iter; gint valid; GtkTreePath *parent_path; g_return_if_fail (BAOBAB_IS_CHART (chart)); priv = BAOBAB_CHART_GET_PRIVATE (chart); if (priv->root == NULL) return; path = gtk_tree_row_reference_get_path (priv->root); if (path != NULL) gtk_tree_model_get_iter (priv->model, &root_iter, path); else return; if (gtk_tree_model_iter_parent (priv->model, &parent_iter, &root_iter)) { gtk_tree_model_get (priv->model, &parent_iter, priv->valid_column, &valid, -1); if (valid == -1) return; gtk_tree_row_reference_free (priv->root); parent_path = gtk_tree_model_get_path (priv->model, &parent_iter); priv->root = gtk_tree_row_reference_new (priv->model, parent_path); gtk_tree_path_free (parent_path); g_signal_emit (BAOBAB_CHART (chart), baobab_chart_signals[ITEM_ACTIVATED], 0, &parent_iter); gtk_widget_queue_draw (chart); } gtk_tree_path_free (path); } /** * baobab_chart_save_snapshot: * @chart: the #BaobabChart requested to be exported to image. * * Opens a dialog to allow saving the current chart's image as a PNG, JPEG or * BMP image. * * Fails if @chart is not a #BaobabChart. **/ void baobab_chart_save_snapshot (GtkWidget *chart) { BaobabChartPrivate *priv; GdkPixbuf *pixbuf; GtkWidget *fs_dlg; GtkWidget *vbox; GtkWidget *hbox; GtkWidget *label; GtkWidget *opt_menu; gchar *sel_type; gchar *filename; gchar *def_filename; BaobabChartItem *item; g_return_if_fail (BAOBAB_IS_CHART (chart)); while (gtk_events_pending ()) gtk_main_iteration (); /* Get the chart's pixbuf */ pixbuf = baobab_chart_get_pixbuf (chart); if (pixbuf == NULL) { GtkWidget *dialog; dialog = gtk_message_dialog_new (NULL, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Cannot create pixbuf image!")); gtk_dialog_run (GTK_DIALOG (dialog)); gtk_widget_destroy (dialog); return; } priv = BAOBAB_CHART (chart)->priv; /* Popup the File chooser dialog */ fs_dlg = gtk_file_chooser_dialog_new (_("Save Snapshot"), NULL, GTK_FILE_CHOOSER_ACTION_SAVE, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, NULL); item = (BaobabChartItem *) priv->first_item->data; def_filename = g_strdup_printf (SNAPSHOT_DEF_FILENAME_FORMAT, item->name); gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (fs_dlg), def_filename); g_free (def_filename); gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (fs_dlg), g_get_home_dir ()); #if GTK_CHECK_VERSION(2,8,0) gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (fs_dlg), TRUE); #endif /* extra widget */ vbox = gtk_vbox_new (FALSE, 0); gtk_container_set_border_width (GTK_CONTAINER (vbox), 0); gtk_file_chooser_set_extra_widget (GTK_FILE_CHOOSER (fs_dlg), vbox); hbox = gtk_hbox_new (FALSE, 12); gtk_box_pack_start (GTK_BOX (vbox), hbox, TRUE, TRUE, 6); label = gtk_label_new_with_mnemonic (_("_Image type:")); gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0); opt_menu = gtk_combo_box_text_new (); gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (opt_menu), "png"); gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (opt_menu), "jpeg"); gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (opt_menu), "bmp"); gtk_combo_box_set_active (GTK_COMBO_BOX (opt_menu), 0); gtk_box_pack_start (GTK_BOX (hbox), opt_menu, TRUE, TRUE, 0); gtk_label_set_mnemonic_widget (GTK_LABEL (label), opt_menu); gtk_widget_show_all (vbox); if (gtk_dialog_run (GTK_DIALOG (fs_dlg)) == GTK_RESPONSE_ACCEPT) { filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (fs_dlg)); sel_type = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (opt_menu)); if (! g_str_has_suffix (filename, sel_type)) { gchar *tmp; tmp = filename; filename = g_strjoin (".", filename, sel_type, NULL); g_free (tmp); } gdk_pixbuf_save (pixbuf, filename, sel_type, NULL, NULL); g_free (filename); g_free (sel_type); } gtk_widget_destroy (fs_dlg); g_object_unref (pixbuf); } /** * baobab_chart_is_frozen: * @chart: the #BaobabChart to ask if frozen. * * Returns a boolean telling whether the chart is in a frozen state, meanning * that no actions should be taken uppon it. * * Fails if @chart is not a #BaobabChart. **/ gboolean baobab_chart_is_frozen (GtkWidget *chart) { BaobabChartPrivate *priv; g_return_val_if_fail (BAOBAB_IS_CHART (chart), FALSE); priv = BAOBAB_CHART_GET_PRIVATE (chart); return priv->is_frozen; } /** * baobab_chart_is_frozen: * @chart: the #BaobabChart to obtain the highlighted it from. * * Returns a BaobabChartItem corresponding to the item that currently has mouse * pointer over, or NULL if no item is highlighted. * * Fails if @chart is not a #BaobabChart. **/ BaobabChartItem * baobab_chart_get_highlighted_item (GtkWidget *chart) { BaobabChartPrivate *priv; g_return_val_if_fail (BAOBAB_IS_CHART (chart), NULL); priv = BAOBAB_CHART_GET_PRIVATE (chart); return (priv->highlighted_item ? (BaobabChartItem *) priv->highlighted_item->data : NULL); } /** * baobab_chart_can_zoom_in: * @chart: the #BaobabChart to ask if can be zoomed in. * * Returns a boolean telling whether the chart can be zoomed in, given its current * visualization conditions. * * Fails if @chart is not a #BaobabChart. **/ gboolean baobab_chart_can_zoom_in (GtkWidget *chart) { BaobabChartPrivate *priv; BaobabChartClass *class; g_return_val_if_fail (BAOBAB_IS_CHART (chart), FALSE); priv = BAOBAB_CHART (chart)->priv; class = BAOBAB_CHART_GET_CLASS (chart); if (class->can_zoom_in != NULL) return class->can_zoom_in (chart) > 0; else return priv->max_depth > 1; } /** * baobab_chart_can_zoom_out: * @chart: the #BaobabChart to ask if can be zoomed out. * * Returns a boolean telling whether the chart can be zoomed out, given its current * visualization conditions. * * Fails if @chart is not a #BaobabChart. **/ gboolean baobab_chart_can_zoom_out (GtkWidget *chart) { BaobabChartPrivate *priv; BaobabChartClass *class; g_return_val_if_fail (BAOBAB_IS_CHART (chart), FALSE); priv = BAOBAB_CHART (chart)->priv; class = BAOBAB_CHART_GET_CLASS (chart); if (class->can_zoom_out != NULL) return class->can_zoom_out (chart) > 0; else return (priv->max_depth < BAOBAB_CHART_MAX_DEPTH); }