/* Copyright (C) 2006, 2007, 2008 Igalia
 * Copyright (C) 2012-2021 MATE Developers
 *
 * This file is part of MATE Utils.
 *
 * MATE Utils 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.
 *
 * MATE Utils 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 MATE Utils.  If not, see <https://www.gnu.org/licenses/>.
 *
 * Authors:
 *   Felipe Erias <femorandeira@igalia.com>
 *   Pablo Santamaria <psantamaria@igalia.com>
 *   Jacobo Aragunde <jaragunde@igalia.com>
 *   Eduardo Lima <elima@igalia.com>
 *   Mario Sanchez <msanchez@igalia.com>
 *   Miguel Gomez <magomez@igalia.com>
 *   Henrique Ferreiro <hferreiro@igalia.com>
 *   Alejandro Pinheiro <apinheiro@igalia.com>
 *   Carlos Sanmartin <csanmartin@igalia.com>
 *   Alejandro Garcia <alex@igalia.com>
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <gtk/gtk.h>
#include <glib/gi18n.h>

/* needed for floor and ceil */
#include <math.h>

#include "baobab-chart.h"

#define SNAPSHOT_DEF_FILENAME_FORMAT "%s-disk-usage"

#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
};

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (BaobabChart, baobab_chart, GTK_TYPE_WIDGET);

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_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,
                                     cairo_t *cr);
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;
  widget_class->draw = baobab_chart_expose;
  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);
}

static void
baobab_chart_init (BaobabChart *chart)
{
  BaobabChartPrivate *priv;

  priv = baobab_chart_get_instance_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);
  attributes.event_mask = gtk_widget_get_events (widget);

  attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;

  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_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 |
                         GDK_SCROLL_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_uint (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_uint (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 (chart)->priv;

  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 (chart)->priv;

  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 (chart)->priv;

  /* 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 (chart)->priv;
  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 (data)->priv->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 (data)->priv->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 (data)->priv->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 (data)->priv->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 (data)->priv->model_changed = TRUE;

  baobab_chart_update_draw (BAOBAB_CHART (data), path);

}

static gboolean
baobab_chart_expose (GtkWidget *chart, cairo_t *cr)
{
  BaobabChartPrivate *priv;
  gint w, h;
  gdouble p, sx, sy, aux;
  GtkTreePath *root_path = NULL;
  GtkTreePath *current_path = NULL;
  GtkAllocation allocation;

  GdkRectangle area;
  gdouble x1, y1, x2, y2;
  cairo_clip_extents (cr, &x1, &y1, &x2, &y2);

  aux = floor (x1);
  area.x = (int)aux;

  aux = floor (y1);
  area.y = (int)aux;

  aux = ceil (x2);
  area.width = (int)aux - area.x;

  aux = ceil (y2);
  area.height = (int)aux - area.y;

  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,
                   area.x, area.y,
                   area.width, area.height);

  /* 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);

      baobab_chart_draw (chart, cr, area);
    }

  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,
                             guint 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 = (int) (rel_position / (100.0/3.0));
      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;

    case GDK_SCROLL_SMOOTH :
      /* since we don't add GDK_SMOOTH_SCROLL_MASK to received
         events, this is actually never reached and it's here
         just to silence compiler warnings */
      break;
    }

  return FALSE;
}

static void
baobab_chart_set_item_highlight (GtkWidget *chart,
                                 GList *node,
                                 gboolean highlighted)
{
  BaobabChartItem *item;
  BaobabChartPrivate *priv;

  if (node == NULL)
    return;

  item = (BaobabChartItem *) node->data;
  priv = BAOBAB_CHART (chart)->priv;

  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 (widget)->priv;
  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 (widget)->priv;
  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 (widget)->priv;

  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;
}

static GdkPixbuf*
baobab_chart_get_pixbuf (GtkWidget *widget)
{
  gint w, h;
  GdkPixbuf *pixbuf;

  g_return_val_if_fail (BAOBAB_IS_CHART (widget), NULL);

		w = gdk_window_get_width(gtk_widget_get_window(widget));
		h = gdk_window_get_height(gtk_widget_get_window(widget));


  pixbuf = gdk_pixbuf_get_from_window (
                                         gtk_widget_get_window (widget),
                                         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 (chart)->priv;

  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 (chart)->priv;

  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 (chart)->priv;

  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 (chart)->priv;

  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-cancel",
                                        GTK_RESPONSE_CANCEL,
                                        "gtk-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 ());

  gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (fs_dlg), TRUE);

  /* extra widget */
  vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
  gtk_container_set_border_width (GTK_CONTAINER (vbox), 0);
  gtk_file_chooser_set_extra_widget (GTK_FILE_CHOOSER (fs_dlg), vbox);

  hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 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 (chart)->priv;
  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 (chart)->priv;
  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);
}