From d9ba83b683a5ccc6cfff2d73c7b4e1cdb5b27b97 Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Tue, 14 Jul 2026 08:46:45 +0200 Subject: [PATCH] feat(gui): create investigation tree view --- docs/DEVELOPMENT.md | 4 + docs/tickets/open/TICKET-012.md | 167 ++++++ include/widgets/investigation_tree_view.h | 74 +++ labfy-investigation | Bin 63696 -> 88216 bytes src/widgets/investigation_tree_view.c | 606 ++++++++++++++++++++++ 5 files changed, 851 insertions(+) create mode 100644 docs/tickets/open/TICKET-012.md create mode 100644 include/widgets/investigation_tree_view.h create mode 100644 src/widgets/investigation_tree_view.c diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 7def7f6..7fae127 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -696,6 +696,10 @@ Avant d'ajouter une bibliothèque externe, il convient de vérifier : - si GTK ou GLib proposent déjà la fonctionnalité ; - si la nouvelle dépendance apporte un réel bénéfice. +Labfy Investigation est un projet GTK4 natif. +Aucun nouveau code ne doit utiliser une API dépréciée ou héritée de GTK3. +Les nouveaux développements utilisent exclusivement les composants modernes de GTK4. + --- # Licence diff --git a/docs/tickets/open/TICKET-012.md b/docs/tickets/open/TICKET-012.md new file mode 100644 index 0000000..7255571 --- /dev/null +++ b/docs/tickets/open/TICKET-012.md @@ -0,0 +1,167 @@ +# Ticket #012 + +## Titre + +Créer le composant `InvestigationTreeView`. + +--- + +## Objectif + +Créer un composant graphique spécialisé chargé d'afficher un +`InvestigationTreeModel` à l'aide des widgets modernes de GTK4. + +Ce composant servira d'adaptateur entre le modèle métier et les widgets GTK. + +À ce stade, il n'est pas encore intégré dans la `Sidebar`. + +--- + +## Architecture + +```text +InvestigationTreeModel + │ + ▼ +InvestigationTreeView + │ + ▼ +GtkTreeListModel + │ + ▼ +GtkListView +``` + +Le composant appartient à la couche graphique. + +Le Core ne dépend jamais de GTK. + +--- + +## Responsabilités + +Le module doit : + +- recevoir un `InvestigationTreeModel` ; +- créer un `GtkTreeListModel` ; +- créer un `GtkListView` ; +- utiliser un `GtkSignalListItemFactory` ; +- utiliser un `GtkTreeExpander` ; +- afficher le nom des nœuds ; +- permettre le développement et le repli des dossiers ; +- fournir un widget GTK réutilisable. + +--- + +## Hors périmètre + +Ce ticket ne doit pas : + +- intégrer le composant dans la Sidebar ; +- permettre la sélection d'un nœud ; +- afficher des icônes ; +- ouvrir des fichiers ; +- modifier l'arborescence ; +- communiquer avec SQLite ; +- rafraîchir automatiquement le modèle. + +--- + +## Principe de propriété + +Le composant ne devient jamais propriétaire du +`InvestigationTreeModel`. + +Il reçoit uniquement une référence. + +En revanche, il est propriétaire de tous les objets GTK qu'il crée. + +--- + +## Fichiers concernés + +```text +include/widgets/investigation_tree_view.h +src/widgets/investigation_tree_view.c +``` + +Aucun autre module ne doit être modifié. + +--- + +## Interface publique + +```c +typedef struct InvestigationTreeView InvestigationTreeView; +``` + +```c +InvestigationTreeView *investigation_tree_view_new(void); +``` + +```c +GtkWidget *investigation_tree_view_get_widget( + const InvestigationTreeView *tree_view +); +``` + +```c +void investigation_tree_view_set_model( + InvestigationTreeView *tree_view, + const InvestigationTreeModel *tree_model +); +``` + +```c +void investigation_tree_view_free( + InvestigationTreeView *tree_view +); +``` + +--- + +## Dépendances GTK + +Le composant doit utiliser exclusivement : + +- GtkTreeListModel +- GtkListView +- GtkTreeExpander +- GtkSignalListItemFactory + +Aucun widget GTK3. + +--- + +## Contraintes techniques + +- C17 +- GTK4 uniquement +- aucune variable globale +- aucune dépendance vers SQLite +- aucune lecture directe du système de fichiers +- documentation Doxygen +- compilation sans warning +- aucune fuite mémoire + +--- + +## Critères d'acceptation + +- [ ] Le projet compile. +- [ ] Le composant peut être créé. +- [ ] Le composant fournit un widget GTK. +- [ ] Un modèle peut être associé. +- [ ] Les dossiers peuvent être développés. +- [ ] Les fichiers apparaissent. +- [ ] Le composant ne possède pas le modèle. +- [ ] Aucun Gtk-CRITICAL. +- [ ] Aucun warning. + +--- + +## Commit attendu + +```text +feat(gui): create investigation tree view +``` diff --git a/include/widgets/investigation_tree_view.h b/include/widgets/investigation_tree_view.h new file mode 100644 index 0000000..37fbdbc --- /dev/null +++ b/include/widgets/investigation_tree_view.h @@ -0,0 +1,74 @@ +/****************************************************************************** + * @file investigation_tree_view.h + * @brief Interface publique de la vue arborescente d'une enquête. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_INVESTIGATION_TREE_VIEW_H +#define LABFY_INVESTIGATION_INVESTIGATION_TREE_VIEW_H + +#include "core/investigation_tree_model.h" + +#include + +/** + * @brief Représentation opaque de la vue arborescente d'une enquête. + * + * La structure réelle est définie dans investigation_tree_view.c. + * Les autres modules manipulent uniquement un pointeur vers + * InvestigationTreeView. + */ +typedef struct InvestigationTreeView InvestigationTreeView; + +/** + * @brief Crée une nouvelle vue arborescente vide. + * + * Aucun modèle métier n'est associé au composant lors de sa création. + * + * @return Une nouvelle vue, ou NULL si sa création échoue. + */ +InvestigationTreeView *investigation_tree_view_new(void); + +/** + * @brief Retourne le widget GTK racine de la vue arborescente. + * + * Le widget retourné appartient au module InvestigationTreeView. + * Le code appelant ne doit ni le détruire ni en libérer la référence. + * + * @param tree_view Vue arborescente à consulter. + * + * @return Le widget racine, ou NULL si tree_view vaut NULL. + */ +GtkWidget *investigation_tree_view_get_widget( + const InvestigationTreeView *tree_view +); + +/** + * @brief Associe un modèle métier à la vue arborescente. + * + * La vue ne devient pas propriétaire du modèle. Le modèle doit rester valide + * pendant toute la durée de son affichage. + * + * Passer NULL retire le modèle actuel et vide la vue. + * + * @param tree_view Vue arborescente à mettre à jour. + * @param tree_model Modèle métier en lecture seule, ou NULL. + */ +void investigation_tree_view_set_model( + InvestigationTreeView *tree_view, + const InvestigationTreeModel *tree_model +); + +/** + * @brief Libère la structure d'encapsulation de la vue. + * + * Cette fonction accepte NULL. + * + * Les objets GTK intégrés à l'arbre de widgets restent gérés par GTK. + * + * @param tree_view Vue arborescente à libérer. + */ +void investigation_tree_view_free( + InvestigationTreeView *tree_view +); + +#endif diff --git a/labfy-investigation b/labfy-investigation index 4674c74d27b3ab9bcca762be6fcd8b16df7f3cb1..9f9ac84df521a22ff12bbd1f810754d4ec9a3734 100755 GIT binary patch delta 31176 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 Ko7rzJ_W3_ft;%Bn delta 11181 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 diff --git a/src/widgets/investigation_tree_view.c b/src/widgets/investigation_tree_view.c new file mode 100644 index 0000000..4c87de9 --- /dev/null +++ b/src/widgets/investigation_tree_view.c @@ -0,0 +1,606 @@ +/****************************************************************************** + * @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 +#include +#include +#include + +/* + * InvestigationTreeItem + * --------------------- + * + * GListStore exige des objets dérivés de GObject. + * + * Cette classe privée adapte donc 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. + */ +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 + ); + + item->node = node; + + return item; +} + +/** + * @brief Retourne le nœud métier observé par l'adaptateur. + */ +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; + GtkSelectionModel *selection_model; + + const InvestigationTreeModel *tree_model; +}; + +/** + * @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 pour un fichier + * ou un dossier sans 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() + ); + + 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. + * Nous pouvons donc libérer notre référence locale juste après. + */ + 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. + * + * Une ligne contient : + * + * GtkTreeExpander + * └── GtkLabel + */ +static void investigation_tree_view_factory_setup( + GtkSignalListItemFactory *factory, + GtkListItem *list_item, + gpointer user_data +) +{ + GtkWidget *expander = NULL; + GtkWidget *label = NULL; + + (void)factory; + (void)user_data; + + expander = gtk_tree_expander_new(); + label = gtk_label_new(NULL); + + gtk_widget_set_halign( + label, + GTK_ALIGN_START + ); + + gtk_widget_set_hexpand( + label, + TRUE + ); + + gtk_tree_expander_set_child( + GTK_TREE_EXPANDER(expander), + label + ); + + gtk_list_item_set_child( + list_item, + expander + ); + + /* + * Le focus doit appartenir au GtkTreeExpander afin que ses raccourcis + * clavier d'ouverture et de fermeture fonctionnent correctement. + */ + gtk_list_item_set_focusable( + list_item, + FALSE + ); +} + +/** + * @brief Relie une ligne GTK à un nœud métier. + */ +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; + GtkWidget *expander = NULL; + GtkWidget *label = NULL; + const char *node_name = 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; + } + + label = gtk_tree_expander_get_child( + GTK_TREE_EXPANDER(expander) + ); + + if (label == NULL) + { + return; + } + + node_name = investigation_node_get_name(node); + + gtk_tree_expander_set_list_row( + GTK_TREE_EXPANDER(expander), + tree_row + ); + + 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. + */ +static void investigation_tree_view_factory_unbind( + GtkSignalListItemFactory *factory, + GtkListItem *list_item, + gpointer user_data +) +{ + GtkWidget *expander = NULL; + GtkWidget *label = NULL; + + (void)factory; + (void)user_data; + + expander = gtk_list_item_get_child(list_item); + + if (expander == NULL) + { + return; + } + + label = gtk_tree_expander_get_child( + GTK_TREE_EXPANDER(expander) + ); + + gtk_tree_expander_set_list_row( + GTK_TREE_EXPANDER(expander), + NULL + ); + + if (label != NULL) + { + gtk_label_set_text( + GTK_LABEL(label), + "" + ); + } +} + +/** + * @brief Retire le modèle GTK actuellement affiché. + */ +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; +} + +InvestigationTreeView *investigation_tree_view_new(void) +{ + InvestigationTreeView *tree_view = NULL; + + tree_view = g_new0( + InvestigationTreeView, + 1 + ); + + 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; + GtkNoSelection *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() + ); + + 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_list_model_new() prend possession de root_store. + * + * passthrough = FALSE : + * le modèle expose des GtkTreeListRow, nécessaires à GtkTreeExpander. + * + * autoexpand = FALSE : + * les dossiers commencent repliés. + */ + 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; + } + + /* + * GtkNoSelection satisfait l'interface GtkSelectionModel demandée + * par GtkListView, sans encore autoriser la sélection. + * + * Le constructeur prend possession de gtk_tree_model. + */ + selection_model = gtk_no_selection_new( + G_LIST_MODEL(gtk_tree_model) + ); + + if (selection_model == NULL) + { + g_object_unref(gtk_tree_model); + return; + } + + tree_view->selection_model = + GTK_SELECTION_MODEL(selection_model); + + tree_view->tree_model = tree_model; + + gtk_list_view_set_model( + GTK_LIST_VIEW(tree_view->list_view), + tree_view->selection_model + ); +} + +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 de widgets GTK + * une fois le composant intégré à une fenêtre. + */ + g_free(tree_view); +}