labfy-investigation/src/widgets/investigation_tree_view.c
2026-07-23 13:58:23 +02:00

1021 lines
21 KiB
C

/******************************************************************************
* @file investigation_tree_view.c
* @brief Affichage GTK4 de l'arborescence d'une enquête.
******************************************************************************/
#include "widgets/investigation_tree_view.h"
#include "core/investigation_node.h"
#include <gio/gio.h>
#include <glib-object.h>
#include <glib.h>
#include <gtk/gtk.h>
#include <string.h>
/** @brief Prépare le chemin du fichier pour un glisser-déposer. */
static GdkContentProvider *investigation_tree_view_on_drag_prepare(
GtkDragSource *source, double x, double y, gpointer user_data)
{
const char *path = g_object_get_data(G_OBJECT(source),
"investigation-node-path");
(void) x; (void) y; (void) user_data;
if (path == NULL || path[0] == '\0') return NULL;
return gdk_content_provider_new_typed(G_TYPE_STRING, path);
}
/** @brief Vérifie qu'un nœud est un fichier `.txt` sous `Extractions`. */
static gboolean investigation_tree_view_node_is_extraction(
const InvestigationNode *node)
{
const InvestigationNode *ancestor = NULL;
const char *name = NULL;
if (node == NULL ||
investigation_node_get_type(node) != INVESTIGATION_NODE_FILE)
return FALSE;
name = investigation_node_get_name(node);
if (name == NULL || !g_str_has_suffix(name, ".txt")) return FALSE;
ancestor = investigation_node_get_parent(node);
while (ancestor != NULL)
{
if (g_strcmp0(investigation_node_get_name(ancestor),
"Extractions") == 0) return TRUE;
ancestor = investigation_node_get_parent(ancestor);
}
return FALSE;
}
/*
* InvestigationTreeItem
* ---------------------
*
* GListStore exige des objets dérivés de GObject.
*
* Cette classe privée adapte un InvestigationNode à l'écosystème GTK.
* Elle ne possède jamais le nœud métier : elle conserve seulement son adresse.
*/
typedef struct _InvestigationTreeItem
{
GObject parent_instance;
const InvestigationNode *node;
} InvestigationTreeItem;
typedef struct _InvestigationTreeItemClass
{
GObjectClass parent_class;
} InvestigationTreeItemClass;
G_DEFINE_TYPE(
InvestigationTreeItem,
investigation_tree_item,
G_TYPE_OBJECT
)
static void investigation_tree_item_class_init(
InvestigationTreeItemClass *item_class
)
{
(void)item_class;
}
static void investigation_tree_item_init(
InvestigationTreeItem *item
)
{
item->node = NULL;
}
/**
* @brief Crée un adaptateur GTK pour un nœud métier.
*
* L'adaptateur ne devient pas propriétaire du nœud.
*
* @param node Nœud métier observé.
*
* @return Un nouvel adaptateur, ou NULL.
*/
static InvestigationTreeItem *investigation_tree_item_new(
const InvestigationNode *node
)
{
InvestigationTreeItem *item = NULL;
if (node == NULL)
{
return NULL;
}
item = g_object_new(
investigation_tree_item_get_type(),
NULL
);
if (item == NULL)
{
return NULL;
}
item->node = node;
return item;
}
/**
* @brief Retourne le nœud métier observé par l'adaptateur.
*
* @param item Adaptateur à consulter.
*
* @return Le nœud observé, ou NULL.
*/
static const InvestigationNode *investigation_tree_item_get_node(
const InvestigationTreeItem *item
)
{
if (item == NULL)
{
return NULL;
}
return item->node;
}
/**
* @struct InvestigationTreeView
* @brief Représentation interne de la vue d'arborescence.
*/
struct InvestigationTreeView
{
GtkWidget *root_widget;
GtkWidget *list_view;
GtkListItemFactory *factory;
GtkSingleSelection *selection_model;
const InvestigationTreeModel *tree_model;
InvestigationTreeViewSelectionCallback selection_callback;
gpointer selection_user_data;
};
/**
* @brief Crée une liste GTK contenant les enfants d'un nœud.
*
* Cette fonction est appelée par GtkTreeListModel lorsqu'un dossier doit
* être développé.
*
* @param item Adaptateur représentant le nœud concerné.
* @param user_data Données privées inutilisées.
*
* @return Une liste GTK contenant les enfants, ou NULL si le nœud ne possède
* aucun enfant.
*/
static GListModel *investigation_tree_view_create_children_model(
gpointer item,
gpointer user_data
)
{
InvestigationTreeItem *tree_item = item;
const InvestigationNode *node = NULL;
GListStore *children_store = NULL;
size_t children_count = 0;
(void)user_data;
if (tree_item == NULL)
{
return NULL;
}
node = investigation_tree_item_get_node(tree_item);
if (node == NULL)
{
return NULL;
}
if (investigation_node_get_type(node) !=
INVESTIGATION_NODE_DIRECTORY)
{
return NULL;
}
children_count =
investigation_node_get_children_count(node);
if (children_count == 0)
{
return NULL;
}
children_store = g_list_store_new(
investigation_tree_item_get_type()
);
if (children_store == NULL)
{
return NULL;
}
for (size_t index = 0; index < children_count; ++index)
{
const InvestigationNode *child_node = NULL;
InvestigationTreeItem *child_item = NULL;
child_node = investigation_node_get_child(
node,
index
);
if (child_node == NULL)
{
continue;
}
child_item = investigation_tree_item_new(
child_node
);
if (child_item == NULL)
{
continue;
}
/*
* GListStore prend sa propre référence sur l'objet.
*/
g_list_store_append(
children_store,
child_item
);
g_object_unref(child_item);
}
return G_LIST_MODEL(children_store);
}
/**
* @brief Crée les widgets constituant une ligne de l'arborescence.
*
* Structure créée :
*
* GtkTreeExpander
* └── GtkBox horizontal
* ├── GtkImage
* └── GtkLabel
*
* @param factory Fabrique GTK.
* @param list_item Élément de liste à préparer.
* @param user_data Données privées inutilisées.
*/
static void investigation_tree_view_factory_setup(
GtkSignalListItemFactory *factory,
GtkListItem *list_item,
gpointer user_data
)
{
GtkWidget *expander = NULL;
GtkWidget *row_box = NULL;
GtkWidget *image = NULL;
GtkWidget *label = NULL;
GtkDragSource *drag_source = NULL;
(void)factory;
(void)user_data;
expander = gtk_tree_expander_new();
row_box = gtk_box_new(
GTK_ORIENTATION_HORIZONTAL,
6
);
image = gtk_image_new();
label = gtk_label_new(NULL);
gtk_label_set_xalign(
GTK_LABEL(label),
0.0f
);
gtk_widget_set_halign(
label,
GTK_ALIGN_START
);
gtk_widget_set_hexpand(
label,
TRUE
);
gtk_box_append(
GTK_BOX(row_box),
image
);
gtk_box_append(
GTK_BOX(row_box),
label
);
gtk_tree_expander_set_child(
GTK_TREE_EXPANDER(expander),
row_box
);
drag_source = gtk_drag_source_new();
gtk_drag_source_set_actions(drag_source, GDK_ACTION_COPY);
g_signal_connect(drag_source, "prepare",
G_CALLBACK(investigation_tree_view_on_drag_prepare), NULL);
g_object_set_data(G_OBJECT(row_box), "investigation-drag-source",
drag_source);
gtk_widget_add_controller(row_box, GTK_EVENT_CONTROLLER(drag_source));
gtk_list_item_set_child(
list_item,
expander
);
/*
* Le focus reste dans GtkTreeExpander afin que les raccourcis
* d'ouverture et de fermeture fonctionnent.
*/
gtk_list_item_set_focusable(
list_item,
FALSE
);
}
static const char *investigation_tree_view_get_icon_name(
const InvestigationNode *node
)
{
const char *name = NULL;
const char *extension = NULL;
if (node == NULL)
{
return "text-x-generic-symbolic";
}
if (investigation_node_get_type(node) ==
INVESTIGATION_NODE_DIRECTORY)
{
return "folder-symbolic";
}
name = investigation_node_get_name(node);
if (name == NULL)
{
return "text-x-generic-symbolic";
}
extension = strrchr(name, '.');
if (extension == NULL)
{
return "text-x-generic-symbolic";
}
if (g_ascii_strcasecmp(extension, ".sqlite") == 0 ||
g_ascii_strcasecmp(extension, ".db") == 0)
{
return "folder-database-symbolic";
}
if (g_ascii_strcasecmp(extension, ".jpg") == 0 ||
g_ascii_strcasecmp(extension, ".jpeg") == 0 ||
g_ascii_strcasecmp(extension, ".png") == 0 ||
g_ascii_strcasecmp(extension, ".webp") == 0)
{
return "image-x-generic-symbolic";
}
if (g_ascii_strcasecmp(extension, ".pdf") == 0)
{
return "application-pdf-symbolic";
}
if (g_ascii_strcasecmp(extension, ".csv") == 0)
{
return "x-office-spreadsheet-symbolic";
}
if (g_ascii_strcasecmp(extension, ".zip") == 0 ||
g_ascii_strcasecmp(extension, ".tar") == 0 ||
g_ascii_strcasecmp(extension, ".gz") == 0 ||
g_ascii_strcasecmp(extension, ".xz") == 0)
{
return "package-x-generic-symbolic";
}
if (g_ascii_strcasecmp(extension, ".mp4") == 0 ||
g_ascii_strcasecmp(extension, ".mkv") == 0 ||
g_ascii_strcasecmp(extension, ".avi") == 0 ||
g_ascii_strcasecmp(extension, ".mov") == 0)
{
return "video-x-generic-symbolic";
}
return "text-x-generic-symbolic";
}
/**
* @brief Relie une ligne GTK à un nœud métier.
*
* @param factory Fabrique GTK.
* @param list_item Élément de liste à remplir.
* @param user_data Données privées inutilisées.
*/
static void investigation_tree_view_factory_bind(
GtkSignalListItemFactory *factory,
GtkListItem *list_item,
gpointer user_data
)
{
GtkTreeListRow *tree_row = NULL;
InvestigationTreeItem *tree_item = NULL;
const InvestigationNode *node = NULL;
const char *icon_name = NULL;
GtkWidget *expander = NULL;
GtkWidget *row_box = NULL;
GtkWidget *image = NULL;
GtkWidget *label = NULL;
const char *node_name = NULL;
GtkDragSource *drag_source = NULL;
(void)factory;
(void)user_data;
tree_row = GTK_TREE_LIST_ROW(
gtk_list_item_get_item(list_item)
);
if (tree_row == NULL)
{
return;
}
tree_item = gtk_tree_list_row_get_item(
tree_row
);
if (tree_item == NULL)
{
return;
}
node = investigation_tree_item_get_node(
tree_item
);
if (node == NULL)
{
return;
}
expander = gtk_list_item_get_child(
list_item
);
if (expander == NULL)
{
return;
}
row_box = gtk_tree_expander_get_child(
GTK_TREE_EXPANDER(expander)
);
if (row_box == NULL)
{
return;
}
drag_source = GTK_DRAG_SOURCE(g_object_get_data(G_OBJECT(row_box),
"investigation-drag-source"));
if (drag_source != NULL)
g_object_set_data_full(G_OBJECT(drag_source), "investigation-node-path",
investigation_tree_view_node_is_extraction(node)
? g_strdup(investigation_node_get_path(node)) : NULL, g_free);
image = gtk_widget_get_first_child(
row_box
);
if (image == NULL)
{
return;
}
label = gtk_widget_get_next_sibling(
image
);
if (label == NULL)
{
return;
}
node_name = investigation_node_get_name(
node
);
icon_name = investigation_tree_view_get_icon_name(
node
);
gtk_tree_expander_set_list_row(
GTK_TREE_EXPANDER(expander),
tree_row
);
/*
* Pour l'instant l'image reste vide.
* Elle sera configurée dans la suite du ticket #019.
*/
gtk_image_set_from_icon_name(
GTK_IMAGE(image),
icon_name
);
gtk_image_set_pixel_size(
GTK_IMAGE(image),
16
);
gtk_label_set_text(
GTK_LABEL(label),
node_name != NULL ? node_name : ""
);
}
/**
* @brief Détache les données d'une ligne réutilisée par GTK.
*
* @param factory Fabrique GTK.
* @param list_item Élément de liste à nettoyer.
* @param user_data Données privées inutilisées.
*/
static void investigation_tree_view_factory_unbind(
GtkSignalListItemFactory *factory,
GtkListItem *list_item,
gpointer user_data
)
{
GtkWidget *expander = NULL;
GtkWidget *row_box = NULL;
GtkWidget *image = NULL;
GtkWidget *label = NULL;
(void)factory;
(void)user_data;
expander = gtk_list_item_get_child(
list_item
);
if (expander == NULL)
{
return;
}
row_box = gtk_tree_expander_get_child(
GTK_TREE_EXPANDER(expander)
);
if (row_box != NULL)
{
image = gtk_widget_get_first_child(
row_box
);
if (image != NULL)
{
label = gtk_widget_get_next_sibling(
image
);
}
}
gtk_tree_expander_set_list_row(
GTK_TREE_EXPANDER(expander),
NULL
);
if (image != NULL)
{
gtk_image_clear(
GTK_IMAGE(image)
);
}
if (label != NULL)
{
gtk_label_set_text(
GTK_LABEL(label),
""
);
}
}
/**
* @brief Transmet le nœud sélectionné au code appelant.
*
* @param tree_view Vue contenant le callback.
* @param node Nœud sélectionné, ou NULL.
*/
static void investigation_tree_view_emit_selection(
InvestigationTreeView *tree_view,
const InvestigationNode *node
)
{
if (tree_view == NULL)
{
return;
}
if (tree_view->selection_callback == NULL)
{
return;
}
tree_view->selection_callback(
node,
tree_view->selection_user_data
);
}
/**
* @brief Réagit au changement de l'élément sélectionné.
*
* @param selection_model Modèle GTK ayant changé de sélection.
* @param property_spec Propriété ayant déclenché la notification.
* @param user_data Pointeur vers InvestigationTreeView.
*/
static void investigation_tree_view_on_selection_changed(
GObject *selection_model,
GParamSpec *property_spec,
gpointer user_data
)
{
InvestigationTreeView *tree_view = user_data;
GObject *selected_item = NULL;
GtkTreeListRow *tree_row = NULL;
InvestigationTreeItem *tree_item = NULL;
const InvestigationNode *node = NULL;
(void)property_spec;
if (tree_view == NULL)
{
return;
}
selected_item =
gtk_single_selection_get_selected_item(
GTK_SINGLE_SELECTION(selection_model)
);
if (selected_item == NULL)
{
investigation_tree_view_emit_selection(
tree_view,
NULL
);
return;
}
tree_row = GTK_TREE_LIST_ROW(
selected_item
);
tree_item = gtk_tree_list_row_get_item(
tree_row
);
if (tree_item == NULL)
{
investigation_tree_view_emit_selection(
tree_view,
NULL
);
return;
}
node = investigation_tree_item_get_node(
tree_item
);
investigation_tree_view_emit_selection(
tree_view,
node
);
}
/**
* @brief Retire le modèle GTK actuellement affiché.
*
* @param tree_view Vue à nettoyer.
*/
static void investigation_tree_view_clear_model(
InvestigationTreeView *tree_view
)
{
if (tree_view == NULL)
{
return;
}
gtk_list_view_set_model(
GTK_LIST_VIEW(tree_view->list_view),
NULL
);
g_clear_object(
&tree_view->selection_model
);
tree_view->tree_model = NULL;
investigation_tree_view_emit_selection(
tree_view,
NULL
);
}
InvestigationTreeView *investigation_tree_view_new(void)
{
InvestigationTreeView *tree_view = NULL;
tree_view = g_new0(
InvestigationTreeView,
1
);
if (tree_view == NULL)
{
return NULL;
}
tree_view->factory =
GTK_LIST_ITEM_FACTORY(
gtk_signal_list_item_factory_new()
);
if (tree_view->factory == NULL)
{
investigation_tree_view_free(tree_view);
return NULL;
}
g_signal_connect(
tree_view->factory,
"setup",
G_CALLBACK(
investigation_tree_view_factory_setup
),
NULL
);
g_signal_connect(
tree_view->factory,
"bind",
G_CALLBACK(
investigation_tree_view_factory_bind
),
NULL
);
g_signal_connect(
tree_view->factory,
"unbind",
G_CALLBACK(
investigation_tree_view_factory_unbind
),
NULL
);
tree_view->list_view = gtk_list_view_new(
NULL,
g_object_ref(tree_view->factory)
);
if (tree_view->list_view == NULL)
{
investigation_tree_view_free(tree_view);
return NULL;
}
tree_view->root_widget =
gtk_scrolled_window_new();
if (tree_view->root_widget == NULL)
{
investigation_tree_view_free(tree_view);
return NULL;
}
gtk_widget_set_hexpand(
tree_view->root_widget,
TRUE
);
gtk_widget_set_vexpand(
tree_view->root_widget,
TRUE
);
gtk_scrolled_window_set_policy(
GTK_SCROLLED_WINDOW(tree_view->root_widget),
GTK_POLICY_AUTOMATIC,
GTK_POLICY_AUTOMATIC
);
gtk_scrolled_window_set_child(
GTK_SCROLLED_WINDOW(tree_view->root_widget),
tree_view->list_view
);
return tree_view;
}
GtkWidget *investigation_tree_view_get_widget(
const InvestigationTreeView *tree_view
)
{
if (tree_view == NULL)
{
return NULL;
}
return tree_view->root_widget;
}
void investigation_tree_view_set_model(
InvestigationTreeView *tree_view,
const InvestigationTreeModel *tree_model
)
{
const InvestigationNode *root_node = NULL;
InvestigationTreeItem *root_item = NULL;
GListStore *root_store = NULL;
GtkTreeListModel *gtk_tree_model = NULL;
GtkSingleSelection *selection_model = NULL;
if (tree_view == NULL)
{
return;
}
investigation_tree_view_clear_model(
tree_view
);
if (tree_model == NULL)
{
return;
}
root_node =
investigation_tree_model_get_root(
tree_model
);
if (root_node == NULL)
{
return;
}
root_store = g_list_store_new(
investigation_tree_item_get_type()
);
if (root_store == NULL)
{
return;
}
root_item = investigation_tree_item_new(
root_node
);
if (root_item == NULL)
{
g_object_unref(root_store);
return;
}
g_list_store_append(
root_store,
root_item
);
g_object_unref(root_item);
gtk_tree_model = gtk_tree_list_model_new(
G_LIST_MODEL(root_store),
FALSE,
FALSE,
investigation_tree_view_create_children_model,
NULL,
NULL
);
if (gtk_tree_model == NULL)
{
return;
}
selection_model = gtk_single_selection_new(
G_LIST_MODEL(gtk_tree_model)
);
if (selection_model == NULL)
{
g_object_unref(gtk_tree_model);
return;
}
gtk_single_selection_set_autoselect(
selection_model,
FALSE
);
gtk_single_selection_set_can_unselect(
selection_model,
TRUE
);
g_signal_connect(
selection_model,
"notify::selected-item",
G_CALLBACK(
investigation_tree_view_on_selection_changed
),
tree_view
);
tree_view->selection_model =
selection_model;
tree_view->tree_model =
tree_model;
gtk_list_view_set_model(
GTK_LIST_VIEW(tree_view->list_view),
GTK_SELECTION_MODEL(
tree_view->selection_model
)
);
}
void investigation_tree_view_set_selection_callback(
InvestigationTreeView *tree_view,
InvestigationTreeViewSelectionCallback callback,
gpointer user_data
)
{
if (tree_view == NULL)
{
return;
}
tree_view->selection_callback =
callback;
tree_view->selection_user_data =
user_data;
}
void investigation_tree_view_free(
InvestigationTreeView *tree_view
)
{
if (tree_view == NULL)
{
return;
}
investigation_tree_view_clear_model(
tree_view
);
g_clear_object(
&tree_view->factory
);
/*
* root_widget et list_view appartiennent à l'arbre GTK
* une fois intégrés dans la fenêtre.
*/
g_free(tree_view);
}