/* Chrysalide - Outil d'analyse de fichiers binaires
* diagram.c - composant d'affichage avec de digrammes
*
* Copyright (C) 2018 Cyrille Bagard
*
* This file is part of Chrysalide.
*
* Chrysalide 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 3 of the License, or
* (at your option) any later version.
*
* Chrysalide 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 Chrysalide. If not, see .
*/
#include "diagram.h"
#include
#include
#include
/* Composant de dessin de diagramme (instance) */
struct _GtkDiagram
{
GtkDrawingArea parent; /* A laisser en premier */
DiagramRenderingType rendering; /* Type de représentation */
GdkRGBA fore_color; /* Couleur principale */
diagram_stat_t *stats; /* Statistiques fournies */
size_t count; /* Quantité de ces éléments */
};
/* Composant de dessin de diagramme (classe) */
struct _GtkDiagramClass
{
GtkDrawingAreaClass parent; /* A laisser en premier */
};
/* Initialise la classe des dessins de diagramme. */
static void gtk_diagram_class_init(GtkDiagramClass *);
/* Initialise une instance de dessin de diagramme. */
static void gtk_diagram_init(GtkDiagram *);
/* Supprime toutes les références externes. */
static void gtk_diagram_dispose(GtkDiagram *);
/* Procède à la libération totale de la mémoire. */
static void gtk_diagram_finalize(GtkDiagram *);
/* Applique une police choisie à un contexte de rendu. */
static void gtk_diagram_set_font(GtkWidget *, cairo_t *);
/* Assure le dessin du diagramme courant. */
static gboolean gtk_diagram_draw(GtkWidget *, cairo_t *);
/* Dessine un diagramme en camembert. */
static void gtk_diagram_draw_pie(GtkWidget *, cairo_t *, const GdkRGBA *, const diagram_stat_t *, size_t);
/* Dessine un diagramme en barres. */
static void gtk_diagram_draw_histo(GtkWidget *, cairo_t *, const GdkRGBA *, const diagram_stat_t *, size_t);
/* Indique le mode privilégié pour la détermination de taille. */
static GtkSizeRequestMode gtk_diagram_get_request_mode(GtkWidget *);
/* Indique le mode privilégié pour la détermination de taille. */
static void gtk_diagram_get_preferred_width_for_height(GtkWidget *, gint, gint *, gint *);
/* Détermine le type de l'afficheur de diagramme. */
G_DEFINE_TYPE(GtkDiagram, gtk_diagram, GTK_TYPE_DRAWING_AREA)
/******************************************************************************
* *
* Paramètres : klass = classe GTK à initialiser. *
* *
* Description : Initialise la classe des dessins de diagramme. *
* *
* Retour : - *
* *
* Remarques : - *
* *
******************************************************************************/
static void gtk_diagram_class_init(GtkDiagramClass *klass)
{
GObjectClass *object; /* Autre version de la classe */
GtkWidgetClass *widget; /* Composant GTK générique */
object = G_OBJECT_CLASS(klass);
object->dispose = (GObjectFinalizeFunc/* ! */)gtk_diagram_dispose;
object->finalize = (GObjectFinalizeFunc)gtk_diagram_finalize;
widget = GTK_WIDGET_CLASS(klass);
widget->draw = gtk_diagram_draw;
widget->get_request_mode = gtk_diagram_get_request_mode;
widget->get_preferred_width_for_height = gtk_diagram_get_preferred_width_for_height;
}
/******************************************************************************
* *
* Paramètres : diag = instance GTK à initialiser. *
* *
* Description : Initialise une instance de dessin de diagramme. *
* *
* Retour : - *
* *
* Remarques : - *
* *
******************************************************************************/
static void gtk_diagram_init(GtkDiagram *diagram)
{
diagram->stats = NULL;
diagram->count = 0;
}
/******************************************************************************
* *
* Paramètres : diag = instance d'objet GLib à traiter. *
* *
* Description : Supprime toutes les références externes. *
* *
* Retour : - *
* *
* Remarques : - *
* *
******************************************************************************/
static void gtk_diagram_dispose(GtkDiagram *diagram)
{
G_OBJECT_CLASS(gtk_diagram_parent_class)->dispose(G_OBJECT(diagram));
}
/******************************************************************************
* *
* Paramètres : diag = instance d'objet GLib à traiter. *
* *
* Description : Procède à la libération totale de la mémoire. *
* *
* Retour : - *
* *
* Remarques : - *
* *
******************************************************************************/
static void gtk_diagram_finalize(GtkDiagram *diagram)
{
size_t i; /* Boucle de parcours */
for (i = 0; i < diagram->count; i++)
if (diagram->stats[i].desc != NULL)
free(diagram->stats[i].desc);
if (diagram->stats != NULL)
free(diagram->stats);
G_OBJECT_CLASS(gtk_diagram_parent_class)->finalize(G_OBJECT(diagram));
}
/******************************************************************************
* *
* Paramètres : rendering = type de rendu des données. *
* color = couleur complémentaire pour le dessin. *
* *
* Description : Crée une nouvelle instance de dessinateur de diagramme. *
* *
* Retour : Composant GTK mis en place. *
* *
* Remarques : - *
* *
******************************************************************************/
GtkWidget *gtk_diagram_new(DiagramRenderingType rendering, const GdkRGBA *color)
{
GtkDiagram *result; /* Composant à retourner */
result = g_object_new(GTK_TYPE_DIAGRAM, NULL);
result->rendering = rendering;
result->fore_color = *color;
return GTK_WIDGET(result);
}
/******************************************************************************
* *
* Paramètres : widget = composant graphique à redessiner. *
* cr = contexte graphique à utiliser. *
* *
* Description : Applique une police choisie à un contexte de rendu. *
* *
* Retour : - *
* *
* Remarques : - *
* *
******************************************************************************/
static void gtk_diagram_set_font(GtkWidget *widget, cairo_t *cr)
{
GtkStyleContext *context; /* Contexte du style courant */
const PangoFontDescription *fdesc; /* Description de police */
double dpi; /* Résolution de l'écran */
context = gtk_widget_get_style_context(widget);
gtk_style_context_save(context);
gtk_style_context_add_class(context, GTK_STYLE_CLASS_LABEL);
gtk_style_context_get(context, gtk_style_context_get_state(context),
GTK_STYLE_PROPERTY_FONT, &fdesc, NULL);
cairo_select_font_face(cr, pango_font_description_get_family(fdesc),
CAIRO_FONT_SLANT_NORMAL,
CAIRO_FONT_WEIGHT_BOLD);
dpi = gdk_screen_get_resolution(gtk_widget_get_screen(widget));
cairo_set_font_size(cr, (pango_font_description_get_size(fdesc) * dpi) / (PANGO_SCALE * 72.0));
gtk_style_context_restore(context);
}
/******************************************************************************
* *
* Paramètres : widget = composant graphique à redessiner. *
* cr = contexte graphique à utiliser. *
* *
* Description : Assure le dessin du diagramme courant. *
* *
* Retour : FALSE pour poursuivre la propagation de l'événement. *
* *
* Remarques : - *
* *
******************************************************************************/
static gboolean gtk_diagram_draw(GtkWidget *widget, cairo_t *cr)
{
GtkDiagram *diagram; /* Autre version du composant */
diagram = GTK_DIAGRAM(widget);
gtk_diagram_set_font(widget, cr);
if (diagram->count > 0)
switch (diagram->rendering)
{
case DRT_PIE:
gtk_diagram_draw_pie(widget, cr, &diagram->fore_color, diagram->stats, diagram->count);
break;
case DRT_HISTO:
gtk_diagram_draw_histo(widget, cr, &diagram->fore_color, diagram->stats, diagram->count);
break;
}
return FALSE;
}
/******************************************************************************
* *
* Paramètres : widget = composant graphique à redessiner. *
* cr = contexte graphique à utiliser. *
* color = couleur d'impression principale. *
* stats = élements statistiques à présenter. *
* count = quantité de ces éléments. *
* *
* Description : Dessine un diagramme en camembert. *
* *
* Retour : - *
* *
* Remarques : - *
* *
******************************************************************************/
static void gtk_diagram_draw_pie(GtkWidget *widget, cairo_t *cr, const GdkRGBA *color, const diagram_stat_t *stats, size_t count)
{
guint height; /* Hauteur de l'espace dispo */
double cx; /* Centre du camember #1 */
double cy; /* Centre du camember #2 */
double radius; /* Taille dudit camember */
double sum; /* Somme de toutes les valeurs */
size_t i; /* Boucle de parcours */
double init_angle; /* Angle de départ */
double last_angle; /* Dernier angle utilisé */
const diagram_stat_t *stat; /* Statistique courante */
double angle_1; /* Angle de départ */
double angle_2; /* Angle d'arrivée */
GdkRGBA tmp; /* Stockage temporaire */
double tx; /* Abscisse du texte de légende*/
cairo_text_extents_t extents; /* Taille de la police */
double ty; /* Ordonnée du texte de légende*/
/* Préparatifs */
height = gtk_widget_get_allocated_height(widget);
cx = height / 2;
cy = height / 2;
radius = (height - 2 * DIAGRAM_MARGIN) / 2;
sum = 0;
for (i = 0; i < count; i++)
sum += stats[i].value;
init_angle = 0;
for (i = 0; i < count; i++)
{
init_angle = G_PI - (stats[i].value * 2 * G_PI) / 200;
if (stats[i].value != 0)
break;
}
assert(i < count);
/* Contenu */
cairo_set_line_width(cr, 2.0);
last_angle = init_angle;
for (; i < count; i++)
{
stat = &stats[i];
if (stat->value > 0)
{
angle_1 = last_angle;
if ((i + 1) == count)
angle_2 = (init_angle != 0 ? init_angle : G_PI);
else
angle_2 = angle_1 + (stat->value * 2 * G_PI) / sum;
cairo_move_to(cr, cx, cy);
cairo_arc(cr, cx, cy, radius, angle_1, angle_2);
cairo_line_to(cr, cx, cy);
tmp = stat->color;
tmp.alpha /= 2;
gdk_cairo_set_source_rgba(cr, &tmp);
cairo_fill_preserve(cr);
gdk_cairo_set_source_rgba(cr, color);
cairo_stroke(cr);
last_angle = angle_2;
}
}
/* Bordures */
cairo_set_line_width(cr, 10.0);
last_angle = init_angle;
for (i = 0; i < count; i++)
{
stat = &stats[i];
if (stat->value > 0)
{
angle_1 = last_angle;
if ((i + 1) == count)
angle_2 = (init_angle != 0 ? init_angle : G_PI);
else
angle_2 = angle_1 + (stat->value * 2 * G_PI) / sum;
cairo_arc(cr, cx, cy, radius, angle_1, angle_2);
gdk_cairo_set_source_rgba(cr, &stat->color);
cairo_stroke(cr);
last_angle = angle_2;
}
}
/* Légende */
cairo_set_line_width(cr, 2.0);
tx = height + DIAGRAM_MARGIN;
cairo_text_extents(cr, "A", &extents);
ty = (height - extents.height - 3 * extents.height * (count - 1)) / 2;
for (i = 0; i < count; i++)
{
stat = &stats[i];
cairo_rectangle(cr, tx, ty, 2 * extents.height, extents.height);
tmp = stat->color;
tmp.alpha /= 2;
gdk_cairo_set_source_rgba(cr, &tmp);
cairo_fill_preserve(cr);
gdk_cairo_set_source_rgba(cr, &stat->color);
cairo_stroke(cr);
cairo_move_to(cr, tx + 3 * extents.height, ty - extents.y_bearing);
cairo_show_text(cr, stat->desc);
ty += 3 * extents.height;
}
}
/******************************************************************************
* *
* Paramètres : widget = composant graphique à redessiner. *
* cr = contexte graphique à utiliser. *
* color = couleur d'impression principale. *
* stats = élements statistiques à présenter. *
* count = quantité de ces éléments. *
* *
* Description : Dessine un diagramme en barres. *
* *
* Retour : - *
* *
* Remarques : - *
* *
******************************************************************************/
static void gtk_diagram_draw_histo(GtkWidget *widget, cairo_t *cr, const GdkRGBA *color, const diagram_stat_t *stats, size_t count)
{
guint height; /* Hauteur de l'espace dispo */
cairo_text_extents_t extents; /* Taille de la police */
guint graph_height; /* Hauteur du graphique */
guint zero_x; /* Abscisse de l'origine */
guint zero_y; /* Ordonnée de l'origine */
double sum; /* Somme de toutes les valeurs */
guint graph_width; /* Largeur du graphique */
size_t i; /* Boucle de parcours */
const diagram_stat_t *stat; /* Statistique courante */
double ty; /* Ordonnée du texte de légende*/
double tx; /* Abscisse du texte de légende*/
static const char *scale[4] = { "0% ", "25% ", "50% ", "100% " };
/* Préparatifs */
height = gtk_widget_get_allocated_height(widget);
cairo_text_extents(cr, scale[3], &extents);
graph_height = height - DIAGRAM_MARGIN * 2 - extents.height;
zero_x = extents.x_advance;
zero_y = DIAGRAM_MARGIN + graph_height;
sum = 0;
graph_width = 0;
for (i = 0; i < count; i++)
{
stat = &stats[i];
sum += stat->value;
cairo_text_extents(cr, stat->desc, &extents);
graph_width += extents.x_advance + DIAGRAM_MARGIN;
}
/* Echelles et légende */
gdk_cairo_set_source_rgba(cr, color);
cairo_move_to(cr, zero_x, zero_y);
cairo_line_to(cr, zero_x, zero_y - graph_height);
cairo_stroke(cr);
cairo_move_to(cr, zero_x, zero_y);
cairo_line_to(cr, zero_x + graph_width, zero_y);
cairo_stroke(cr);
for (i = 0; i < 4; i++)
{
ty = zero_y - (i * graph_height) / 3;
cairo_move_to(cr, zero_x - 2, ty);
cairo_line_to(cr, zero_x + 2, ty);
cairo_stroke(cr);
cairo_text_extents(cr, scale[i], &extents);
cairo_move_to(cr, zero_x - extents.x_advance, ty - extents.y_bearing / 2);
cairo_show_text(cr, scale[i]);
}
tx = zero_x;
ty = zero_y + DIAGRAM_MARGIN / 2;
for (i = 0; i < count; i++)
{
stat = &stats[i];
cairo_text_extents(cr, stat->desc, &extents);
gdk_cairo_set_source_rgba(cr, color);
cairo_move_to(cr, tx + DIAGRAM_MARGIN / 2 + extents.x_advance / 2, zero_y - 2);
cairo_line_to(cr, tx + DIAGRAM_MARGIN / 2 + extents.x_advance / 2, zero_y + 2);
cairo_stroke(cr);
gdk_cairo_set_source_rgba(cr, &stat->color);
cairo_move_to(cr, tx + DIAGRAM_MARGIN / 2, ty - extents.y_bearing);
cairo_show_text(cr, stat->desc);
tx += extents.x_advance + DIAGRAM_MARGIN;
}
/* Représentation des valeurs */
tx = zero_x;
ty = zero_y + DIAGRAM_MARGIN / 2;
cairo_set_line_width(cr, 8);
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
for (i = 0; i < count; i++)
{
stat = &stats[i];
cairo_text_extents(cr, stat->desc, &extents);
if (stat->value > 0)
{
gdk_cairo_set_source_rgba(cr, &stat->color);
cairo_move_to(cr, tx + DIAGRAM_MARGIN / 2 + extents.x_advance / 2, zero_y);
cairo_line_to(cr, tx + DIAGRAM_MARGIN / 2 + extents.x_advance / 2,
zero_y - (stat->value * graph_height) / sum);
cairo_stroke(cr);
}
tx += extents.x_advance + DIAGRAM_MARGIN;
}
}
/******************************************************************************
* *
* Paramètres : widget = composant graphique à consulter. *
* *
* Description : Indique le mode privilégié pour la détermination de taille. *
* *
* Retour : Toujours la largeur à partir de la hauteur. *
* *
* Remarques : - *
* *
******************************************************************************/
static GtkSizeRequestMode gtk_diagram_get_request_mode(GtkWidget *widget)
{
return GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT;
}
/******************************************************************************
* *
* Paramètres : widget = composant graphique à consulter. *
* height = hauteur à considérer. *
* minimum = largeur minimale correspondante. *
* natural = largeur idéale correspondante. *
* *
* Description : Indique le mode privilégié pour la détermination de taille. *
* *
* Retour : - *
* *
* Remarques : - *
* *
******************************************************************************/
static void gtk_diagram_get_preferred_width_for_height(GtkWidget *widget, gint height, gint *minimum, gint *natural)
{
GtkDiagram *diagram; /* Autre version du composant */
gint text_width; /* Plus grande longueur */
cairo_surface_t *surface; /* Espace graphique de support */
cairo_t *cr; /* Contexte de rendu */
size_t i; /* Boucle de parcours */
cairo_text_extents_t extents; /* Taille de la police */
diagram = GTK_DIAGRAM(widget);
text_width = 0;
surface = gdk_window_create_similar_surface(gtk_widget_get_window(widget), CAIRO_CONTENT_COLOR, 1, 1);
cr = cairo_create(surface);
gtk_diagram_set_font(widget, cr);
switch (diagram->rendering)
{
case DRT_PIE:
for (i = 0; i < diagram->count; i++)
{
if (diagram->stats[i].desc == NULL)
continue;
cairo_text_extents(cr, diagram->stats[i].desc, &extents);
if (extents.width > text_width)
text_width = extents.width;
}
if (text_width > 0)
*minimum = height + 2 * DIAGRAM_MARGIN + 3 * extents.height + text_width;
else
*minimum = 0;
break;
case DRT_HISTO:
cairo_text_extents(cr, "100% ", &extents);
*minimum = extents.x_advance;
for (i = 0; i < diagram->count; i++)
{
cairo_text_extents(cr, diagram->stats[i].desc, &extents);
*minimum += extents.x_advance + DIAGRAM_MARGIN;
}
*minimum += DIAGRAM_MARGIN;
}
cairo_destroy(cr);
cairo_surface_destroy(surface);
/* Répercution */
*natural = *minimum;
}
/******************************************************************************
* *
* Paramètres : diagram = diagramme à vider. *
* *
* Description : Supprime tous les éléments représentés dans le diagramme. *
* *
* Retour : - *
* *
* Remarques : - *
* *
******************************************************************************/
void gtk_diagram_clear_stats(GtkDiagram *diagram)
{
size_t i; /* Boucle de parcours */
for (i = 0; i < diagram->count; i++)
{
if (diagram->stats[i].desc != NULL)
free(diagram->stats[i].desc);
}
if (diagram->stats != NULL)
free(diagram->stats);
diagram->stats = NULL;
diagram->count = 0;
}
/******************************************************************************
* *
* Paramètres : diagram = diagramme à compléter. *
* stats = nouvelles statistiques à intégrer. *
* count = quantité de ces statistiques. *
* *
* Description : Ajoute des éléments à représenter dans le diagramme. *
* *
* Retour : - *
* *
* Remarques : - *
* *
******************************************************************************/
void gtk_diagram_add_stats(GtkDiagram *diagram, const diagram_stat_t *stats, size_t count)
{
size_t i; /* Boucle de parcours */
diagram_stat_t *dest; /* Destination d'une copie */
diagram->stats = (diagram_stat_t *)realloc(diagram->stats, (diagram->count + count) * sizeof(diagram_stat_t));
for (i = 0; i < count; i++)
{
dest = &diagram->stats[diagram->count + i];
dest->value = stats[i].value;
dest->color = stats[i].color;
if (stats[i].desc == NULL)
dest->desc = NULL;
else
dest->desc = strdup(stats[i].desc);
}
diagram->count += count;
}