/* Copyright (C) 2008 Igalia
*
* 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 .
*
* Authors:
* Felipe Erias
* Pablo Santamaria
* Jacobo Aragunde
* Eduardo Lima
* Mario Sanchez
* Miguel Gomez
* Henrique Ferreiro
* Alejandro Pinheiro
* Carlos Sanmartin
* Alejandro Garcia
*/
#ifdef HAVE_CONFIG_H
#include
#endif
#include
#include
#include
#include
#include "baobab-chart.h"
#include "baobab-ringschart.h"
#define ITEM_BORDER_WIDTH 1
#define CHART_PADDING 13
#define ITEM_MIN_ANGLE 0.03
#define EDGE_ANGLE 0.004
#define SUBFOLDER_TIP_PADDING 3
typedef struct _BaobabRingschartItem BaobabRingschartItem;
struct _BaobabRingschartItem
{
gdouble min_radius;
gdouble max_radius;
gdouble start_angle;
gdouble angle;
gboolean continued;
};
struct _BaobabRingschartPrivate
{
gboolean subfoldertips_enabled;
BaobabChartItem *highlighted_item;
guint tips_timeout_event;
GList *subtip_items;
gboolean drawing_subtips;
guint subtip_timeout;
};
G_DEFINE_TYPE_WITH_PRIVATE (BaobabRingschart, baobab_ringschart, BAOBAB_CHART_TYPE);
static void baobab_ringschart_draw_sector (cairo_t *cr,
gdouble center_x, gdouble center_y,
gdouble radius, gdouble thickness,
gdouble init_angle, gdouble final_angle,
BaobabChartColor fill_color,
gboolean continued, guint border);
static void baobab_ringschart_draw_item (GtkWidget *chart,
cairo_t *cr,
BaobabChartItem *item,
gboolean highlighted);
static void baobab_ringschart_calculate_item_geometry (GtkWidget *chart,
BaobabChartItem *item);
static gboolean baobab_ringschart_is_point_over_item (GtkWidget *chart,
BaobabChartItem *item,
gdouble x,
gdouble y);
static void baobab_ringschart_get_point_min_rect (gdouble cx, gdouble cy,
gdouble radius, gdouble angle,
GdkRectangle *rect);
static void baobab_ringschart_get_item_rectangle (GtkWidget *chart,
BaobabChartItem *item);
static void baobab_ringschart_pre_draw (GtkWidget *chart, cairo_t *cr);
static void baobab_ringschart_post_draw (GtkWidget *chart, cairo_t *cr);
static void
baobab_ringschart_class_init (BaobabRingschartClass *class)
{
BaobabChartClass *chart_class;
chart_class = BAOBAB_CHART_CLASS (class);
/* BaobabChart abstract methods */
chart_class->draw_item = baobab_ringschart_draw_item;
chart_class->calculate_item_geometry = baobab_ringschart_calculate_item_geometry;
chart_class->is_point_over_item = baobab_ringschart_is_point_over_item;
chart_class->get_item_rectangle = baobab_ringschart_get_item_rectangle;
chart_class->pre_draw = baobab_ringschart_pre_draw;
chart_class->post_draw = baobab_ringschart_post_draw;
}
static void
baobab_ringschart_init (BaobabRingschart *chart)
{
BaobabRingschartPrivate *priv;
GtkSettings* settings;
gint timeout;
priv = baobab_ringschart_get_instance_private (chart);
priv->subfoldertips_enabled = FALSE;
priv->highlighted_item = NULL;
priv->tips_timeout_event = 0;
priv->subtip_items = NULL;
priv->drawing_subtips = FALSE;
settings = gtk_settings_get_default ();
g_object_get (G_OBJECT (settings), "gtk-tooltip-timeout", &timeout, NULL);
priv->subtip_timeout = 2 * (guint)timeout;
}
static void
baobab_ringschart_draw_sector (cairo_t *cr,
gdouble center_x,
gdouble center_y,
gdouble radius,
gdouble thickness,
gdouble init_angle,
gdouble final_angle,
BaobabChartColor fill_color,
gboolean continued,
guint border)
{
cairo_set_line_width (cr, border);
if (radius > 0)
cairo_arc (cr, center_x, center_y, radius, init_angle, final_angle);
cairo_arc_negative (cr, center_x, center_y, radius+thickness,
final_angle, init_angle);
cairo_close_path(cr);
cairo_set_source_rgb (cr, fill_color.red, fill_color.green, fill_color.blue);
cairo_fill_preserve (cr);
cairo_set_source_rgb (cr, 0, 0, 0);
cairo_stroke (cr);
if (continued)
{
cairo_set_line_width (cr, 3);
cairo_arc (cr, center_x, center_y, radius+thickness + 4,
init_angle+EDGE_ANGLE, final_angle-EDGE_ANGLE);
cairo_stroke (cr);
}
}
static void
baobab_ringschart_draw_item (GtkWidget *chart,
cairo_t *cr,
BaobabChartItem *item,
gboolean highlighted)
{
BaobabRingschartPrivate *priv;
BaobabRingschartItem *data;
BaobabChartColor fill_color;
GtkAllocation allocation;
priv = baobab_ringschart_get_instance_private (BAOBAB_RINGSCHART (chart));
data = (BaobabRingschartItem *) item->data;
if (priv->drawing_subtips)
if ( (priv->highlighted_item) && (item->parent) &&
(((BaobabChartItem *) item->parent->data) == priv->highlighted_item) )
{
GList *node;
node = g_new0 (GList, 1);
node->data = (gpointer) item;
node->next = priv->subtip_items;
if (priv->subtip_items)
priv->subtip_items->prev = node;
priv->subtip_items = node;
}
baobab_chart_get_item_color (&fill_color,
data->start_angle / M_PI * 99,
item->depth,
highlighted);
gtk_widget_get_allocation (chart, &allocation);
baobab_ringschart_draw_sector (cr,
allocation.width / 2,
allocation.height / 2,
data->min_radius,
data->max_radius - data->min_radius,
data->start_angle,
data->start_angle + data->angle,
fill_color,
data->continued,
ITEM_BORDER_WIDTH);
}
static void
baobab_ringschart_calculate_item_geometry (GtkWidget *chart,
BaobabChartItem *item)
{
BaobabRingschartItem *data;
BaobabRingschartItem p_data;
BaobabChartItem *parent = NULL;
GtkAllocation allocation;
gdouble max_radius;
gdouble thickness;
guint max_depth;
max_depth = baobab_chart_get_max_depth (chart);
if (item->data == NULL)
item->data = g_new (BaobabRingschartItem, 1);
data = (BaobabRingschartItem *) item->data;
data->continued = FALSE;
item->visible = FALSE;
gtk_widget_get_allocation (chart, &allocation);
max_radius = MIN (allocation.width / 2, allocation.height / 2) - CHART_PADDING;
thickness = max_radius / (max_depth + 1);
if (item->parent == NULL)
{
data->min_radius = 0;
data->max_radius = thickness;
data->start_angle = 0;
data->angle = 2 * M_PI;
}
else
{
parent = (BaobabChartItem *) item->parent->data;
g_memmove (&p_data, parent->data, sizeof (BaobabRingschartItem));
data->min_radius = (item->depth) * thickness;
if (data->min_radius + thickness > max_radius)
return;
else
data->max_radius = data->min_radius + thickness;
data->angle = p_data.angle * item->rel_size / 100;
if (data->angle < ITEM_MIN_ANGLE)
return;
data->start_angle = p_data.start_angle + p_data.angle * item->rel_start / 100;
data->continued = (item->has_any_child) && (item->depth == max_depth);
parent->has_visible_children = TRUE;
}
item->visible = TRUE;
baobab_ringschart_get_item_rectangle (chart, item);
}
static gboolean
baobab_ringschart_is_point_over_item (GtkWidget *chart,
BaobabChartItem *item,
gdouble x,
gdouble y)
{
BaobabRingschartItem *data;
gdouble radius, angle;
GtkAllocation allocation;
data = (BaobabRingschartItem *) item->data;
gtk_widget_get_allocation (chart, &allocation);
x = x - allocation.width / 2;
y = y - allocation.height / 2;
radius = sqrt (x*x + y*y);
angle = atan2 (y, x);
angle = (angle > 0) ? angle : angle + 2*G_PI;
return (radius >= data->min_radius) &&
(radius <= data->max_radius) &&
(angle >= data->start_angle) &&
(angle <= data->start_angle + data->angle);
}
static void
baobab_ringschart_get_point_min_rect (gdouble cx,
gdouble cy,
gdouble radius,
gdouble angle,
GdkRectangle *rect)
{
int x, y;
x = (int) (cx + cos (angle) * radius);
y = (int) (cy + sin (angle) * radius);
rect->x = MIN (rect->x, x);
rect->y = MIN (rect->y, y);
rect->width = MAX (rect->width, x);
rect->height = MAX (rect->height, y);
}
static void
baobab_ringschart_get_item_rectangle (GtkWidget *chart,
BaobabChartItem *item)
{
BaobabRingschartItem *data;
GdkRectangle rect;
gdouble cx, cy, r1, r2, a1, a2;
GtkAllocation allocation;
data = (BaobabRingschartItem *) item->data;
gtk_widget_get_allocation (chart, &allocation);
cx = allocation.width / 2;
cy = allocation.height / 2;
r1 = data->min_radius;
r2 = data->max_radius;
a1 = data->start_angle;
a2 = data->start_angle + data->angle;
rect.x = allocation.width;
rect.y = allocation.height;
rect.width = 0;
rect.height = 0;
baobab_ringschart_get_point_min_rect (cx, cy, r1, a1, &rect);
baobab_ringschart_get_point_min_rect (cx, cy, r2, a1, &rect);
baobab_ringschart_get_point_min_rect (cx, cy, r1, a2, &rect);
baobab_ringschart_get_point_min_rect (cx, cy, r2, a2, &rect);
if ( (a1 <= M_PI/2) && (a2 >= M_PI/2) )
rect.height = MAX (rect.height, (int) (cy + sin (M_PI/2) * r2));
if ( (a1 <= M_PI) && (a2 >= M_PI) )
rect.x = MIN (rect.x, (int) (cx + cos (M_PI) * r2));
if ( (a1 <= M_PI*1.5) && (a2 >= M_PI*1.5) )
rect.y = MIN (rect.y, (int) (cy + sin (M_PI*1.5) * r2));
if ( (a1 <= M_PI*2) && (a2 >= M_PI*2) )
rect.width = MAX (rect.width, (int) (cx + cos (M_PI*2) * r2));
rect.width -= rect.x;
rect.height -= rect.y;
item->rect = rect;
}
gboolean
baobab_ringschart_subfolder_tips_timeout (gpointer data)
{
BaobabRingschartPrivate *priv;
priv = baobab_ringschart_get_instance_private (BAOBAB_RINGSCHART (data));
priv->drawing_subtips = TRUE;
gtk_widget_queue_draw (GTK_WIDGET (data));
return FALSE;
}
void
baobab_ringschart_clean_subforlder_tips_state (GtkWidget *chart)
{
BaobabRingschartPrivate *priv;
GList *node;
priv = baobab_ringschart_get_instance_private (BAOBAB_RINGSCHART (chart));
if (priv->drawing_subtips)
gtk_widget_queue_draw (chart);
priv->drawing_subtips = FALSE;
if (priv->highlighted_item == NULL)
return;
if (priv->tips_timeout_event)
{
g_source_remove (priv->tips_timeout_event);
priv->tips_timeout_event = 0;
}
priv->highlighted_item = NULL;
/* Free subtip_items GList */
node = priv->subtip_items;
while (node != NULL)
{
priv->subtip_items = node->next;
g_free (node);
node = priv->subtip_items;
}
priv->subtip_items = NULL;
}
static void
baobab_ringschart_draw_subfolder_tips (GtkWidget *chart, cairo_t *cr)
{
BaobabRingschartPrivate *priv;
GList *node;
BaobabChartItem *item;
BaobabRingschartItem *data;
gdouble q_angle, q_width, q_height;
gdouble tip_x, tip_y;
gdouble middle_angle, middle_angle_n, middle_radius;
gdouble sector_center_x, sector_center_y;
gdouble a;
guint i;
GtkAllocation allocation;
PangoLayout *layout;
PangoRectangle layout_rect;
gchar *markup = NULL;
cairo_rectangle_t tooltip_rect;
GdkRectangle _rect, last_rect;
priv = baobab_ringschart_get_instance_private (BAOBAB_RINGSCHART (chart));
gtk_widget_get_allocation (chart, &allocation);
q_width = allocation.width / 2;
q_height = allocation.height / 2;
q_angle = atan2 (q_height, q_width);
memset (&last_rect, 0, sizeof (GdkRectangle));
cairo_save (cr);
node = priv->subtip_items;
while (node != NULL)
{
item = (BaobabChartItem *) node->data;
data = (BaobabRingschartItem *) item->data;
/* get the middle angle */
middle_angle = data->start_angle + data->angle / 2;
/* normalize the middle angle (take it to the first quadrant) */
middle_angle_n = middle_angle;
while (middle_angle_n > M_PI/2)
middle_angle_n -= M_PI;
middle_angle_n = ABS (middle_angle_n);
/* get the pango layout and its enclosing rectangle */
layout = gtk_widget_create_pango_layout (chart, NULL);
markup = g_strconcat ("", item->name, "", NULL);
pango_layout_set_markup (layout, markup, -1);
g_free (markup);
pango_layout_set_indent (layout, 0);
pango_layout_set_spacing (layout, 0);
pango_layout_get_pixel_extents (layout, NULL, &layout_rect);
/* get the center point of the tooltip rectangle */
if (middle_angle_n < q_angle)
{
tip_x = q_width - layout_rect.width/2 - SUBFOLDER_TIP_PADDING * 2;
tip_y = tan (middle_angle_n) * tip_x;
}
else
{
tip_y = q_height - layout_rect.height/2 - SUBFOLDER_TIP_PADDING * 2;
tip_x = tip_y / tan (middle_angle_n);
}
/* get the tooltip rectangle */
tooltip_rect.x = q_width + tip_x - layout_rect.width/2 - SUBFOLDER_TIP_PADDING;
tooltip_rect.y = q_height + tip_y - layout_rect.height/2 - SUBFOLDER_TIP_PADDING;
tooltip_rect.width = layout_rect.width + SUBFOLDER_TIP_PADDING * 2;
tooltip_rect.height = layout_rect.height + SUBFOLDER_TIP_PADDING * 2;
/* Check tooltip's width is not greater than half of the widget */
if (tooltip_rect.width > q_width)
{
g_object_unref (layout);
node = node->next;
continue;
}
/* translate tooltip rectangle and edge angles to the original quadrant */
a = middle_angle;
i = 0;
while (a > M_PI/2)
{
if (i % 2 == 0)
tooltip_rect.x = allocation.width - tooltip_rect.x
- tooltip_rect.width;
else
tooltip_rect.y = allocation.height - tooltip_rect.y
- tooltip_rect.height;
i++;
a -= M_PI/2;
}
/* get the GdkRectangle of the tooltip (with a little padding) */
_rect.x = (int) (tooltip_rect.x - 1.0);
_rect.y = (int) (tooltip_rect.y - 1.0);
_rect.width = (int) (tooltip_rect.width + 2.0);
_rect.height = (int) (tooltip_rect.height + 2.0);
/* Check if tooltip overlaps */
if (! gdk_rectangle_intersect (&_rect, &last_rect, NULL))
{
g_memmove (&last_rect, &_rect, sizeof (GdkRectangle));
/* Finally draw the tooltip to cairo! */
/* TODO: Do not hardcode colors */
/* avoid blurred lines */
tooltip_rect.x = floor (tooltip_rect.x) + 0.5;
tooltip_rect.y = floor (tooltip_rect.y) + 0.5;
middle_radius = data->min_radius + (data->max_radius - data->min_radius) / 2;
sector_center_x = q_width + middle_radius * cos (middle_angle);
sector_center_y = q_height + middle_radius * sin (middle_angle);
/* draw line from sector center to tooltip center */
cairo_set_line_width (cr, 1);
cairo_move_to (cr, sector_center_x, sector_center_y);
cairo_set_source_rgb (cr, 0.8275, 0.8431, 0.8118); /* tango: #d3d7cf */
cairo_line_to (cr, tooltip_rect.x + tooltip_rect.width / 2,
tooltip_rect.y + tooltip_rect.height / 2);
cairo_stroke (cr);
/* draw a tiny circle in sector center */
cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 0.6666 );
cairo_arc (cr, sector_center_x, sector_center_y, 1.0, 0, 2 * G_PI );
cairo_stroke (cr);
/* draw tooltip box */
cairo_set_line_width (cr, 0.5);
cairo_rectangle (cr, tooltip_rect.x, tooltip_rect.y,
tooltip_rect.width, tooltip_rect.height);
cairo_set_source_rgb (cr, 0.8275, 0.8431, 0.8118); /* tango: #d3d7cf */
cairo_fill_preserve(cr);
cairo_set_source_rgb (cr, 0.5333, 0.5412, 0.5216); /* tango: #888a85 */
cairo_stroke (cr);
/* draw the text inside the box */
cairo_set_source_rgb (cr, 0, 0, 0);
cairo_move_to (cr, tooltip_rect.x + SUBFOLDER_TIP_PADDING,
tooltip_rect.y + SUBFOLDER_TIP_PADDING);
pango_cairo_show_layout (cr, layout);
cairo_set_line_width (cr, 1);
cairo_stroke (cr);
}
/* Free stuff */
g_object_unref (layout);
node = node->next;
}
cairo_restore (cr);
}
static void
baobab_ringschart_pre_draw (GtkWidget *chart, cairo_t *cr)
{
BaobabRingschartPrivate *priv;
BaobabChartItem *hl_item;
priv = baobab_ringschart_get_instance_private (BAOBAB_RINGSCHART (chart));
hl_item = baobab_chart_get_highlighted_item (chart);
if ( (hl_item == NULL) || (! hl_item->has_visible_children) )
{
baobab_ringschart_clean_subforlder_tips_state (chart);
return;
}
if (hl_item != priv->highlighted_item)
{
baobab_ringschart_clean_subforlder_tips_state (chart);
priv->highlighted_item = hl_item;
/* Launch timer to show subfolder tooltips */
priv->tips_timeout_event = g_timeout_add (priv->subtip_timeout,
(GSourceFunc) baobab_ringschart_subfolder_tips_timeout,
(gpointer) chart);
}
}
static void
baobab_ringschart_post_draw (GtkWidget *chart, cairo_t *cr)
{
BaobabRingschartPrivate *priv;
priv = baobab_ringschart_get_instance_private (BAOBAB_RINGSCHART (chart));
if (priv->subfoldertips_enabled)
{
/* Reverse the glist, which was created from the tail */
priv->subtip_items = g_list_reverse (priv->subtip_items);
baobab_ringschart_draw_subfolder_tips (chart, cr);
}
}
/* Public functions start here */
/**
* baobab_ringschart_new:
*
* Constructor for the baobab_ringschart class
*
* Returns: a new #BaobabRingschart object
*
**/
GtkWidget *
baobab_ringschart_new (void)
{
return g_object_new (BAOBAB_RINGSCHART_TYPE, NULL);
}
/**
* baobab_ringschart_set_subfoldertips_enabled:
* @chart: the #BaobabRingschart to set the
* @enabled: boolean to determine whether to show sub-folder tips.
*
* Enables/disables drawing tooltips of sub-forlders of the highlighted sector.
*
* Fails if @chart is not a #BaobabRingschart.
*
**/
void
baobab_ringschart_set_subfoldertips_enabled (GtkWidget *chart, gboolean enabled)
{
BaobabRingschartPrivate *priv;
priv = baobab_ringschart_get_instance_private (BAOBAB_RINGSCHART (chart));
priv->subfoldertips_enabled = enabled;
if ( (! enabled) && (priv->drawing_subtips) )
{
/* Turn off currently drawn tips */
baobab_ringschart_clean_subforlder_tips_state (chart);
}
}