feat(gui): create investigation tree view

This commit is contained in:
grayTerminal-sh 2026-07-14 08:46:45 +02:00
parent f071527d15
commit d9ba83b683
5 changed files with 851 additions and 0 deletions

View file

@ -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

View file

@ -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
```

View file

@ -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 <gtk/gtk.h>
/**
* @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

Binary file not shown.

View file

@ -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 <gio/gio.h>
#include <glib-object.h>
#include <glib.h>
#include <gtk/gtk.h>
/*
* 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);
}