feat(gui): add investigation tree selection

This commit is contained in:
grayTerminal-sh 2026-07-14 10:19:23 +02:00
parent efe032eefc
commit 6938529fb2
9 changed files with 517 additions and 6 deletions

View file

@ -0,0 +1,244 @@
# Ticket #014
## Titre
Ajouter la sélection d'un nœud dans l'arborescence.
---
## Objectif
Permettre à l'utilisateur de sélectionner un dossier ou un fichier dans
`InvestigationTreeView`.
Le nœud sélectionné doit être transmis jusqu'au module `Application`, qui
reste responsable de la coordination entre l'arborescence et le reste de
l'interface.
Ce ticket ne doit encore ouvrir aucun fichier.
---
## Architecture
```text
GtkListView
InvestigationTreeView
Sidebar
MainWindow
Application
```
Le Core reste indépendant de GTK.
---
## Responsabilités
### InvestigationTreeView
Le module doit :
- remplacer `GtkNoSelection` par `GtkSingleSelection` ;
- détecter les changements de sélection ;
- retrouver le `InvestigationNode` métier correspondant ;
- appeler un callback public avec le nœud sélectionné ;
- transmettre `NULL` lorsqu'aucun nœud n'est sélectionné.
### Sidebar
Le module doit :
- recevoir un callback de sélection ;
- le transmettre à `InvestigationTreeView` ;
- ne contenir aucune logique métier liée à la sélection.
### MainWindow
Le module doit :
- recevoir le callback depuis `Application` ;
- le transmettre à `Sidebar`.
### Application
Le module doit :
- recevoir le nœud sélectionné ;
- afficher temporairement son nom et son type dans le terminal ;
- ne pas modifier ni libérer le nœud reçu.
---
## Principe de propriété
Le nœud sélectionné appartient toujours au
`InvestigationTreeModel`, lui-même possédé par `Application`.
Le callback reçoit uniquement une référence non propriétaire :
```c
const InvestigationNode *node;
```
Le code appelé ne doit jamais faire :
```c
investigation_node_free(node);
```
La référence reste valide tant que le modèle courant n'est pas remplacé ou
détruit.
---
## Interfaces publiques à ajouter
### InvestigationTreeView
```c
typedef void (*InvestigationTreeViewSelectionCallback)(
const InvestigationNode *node,
gpointer user_data
);
```
```c
void investigation_tree_view_set_selection_callback(
InvestigationTreeView *tree_view,
InvestigationTreeViewSelectionCallback callback,
gpointer user_data
);
```
### Sidebar
```c
void sidebar_set_selection_callback(
Sidebar *sidebar,
InvestigationTreeViewSelectionCallback callback,
gpointer user_data
);
```
### MainWindow
```c
void main_window_set_tree_selection_callback(
MainWindow *main_window,
InvestigationTreeViewSelectionCallback callback,
gpointer user_data
);
```
---
## Comportement attendu
Lorsque l'utilisateur sélectionne :
```text
Enquete.sqlite
```
le terminal affiche temporairement :
```text
Nœud sélectionné : Enquete.sqlite
Type : fichier
```
Pour un dossier :
```text
Nœud sélectionné : 00_BaseDeDonnees
Type : dossier
```
Lorsqu'aucun élément n'est sélectionné, le callback reçoit `NULL`.
---
## Hors périmètre
Ce ticket ne doit pas :
- ouvrir un fichier ;
- modifier le Workspace ;
- afficher un aperçu ;
- sélectionner plusieurs nœuds ;
- ajouter un menu contextuel ;
- afficher des icônes ;
- modifier le Core ;
- modifier le système de fichiers.
---
## Contraintes techniques
- GTK4 uniquement ;
- utiliser `GtkSingleSelection` ;
- aucun `GtkTreeView` ;
- aucun état global ;
- callback documenté avec Doxygen ;
- aucune destruction du nœud sélectionné ;
- compilation sans warning ;
- aucun `Gtk-CRITICAL`.
---
## Gestion des changements de modèle
Lorsqu'un nouveau modèle est installé :
- la sélection précédente doit être supprimée ;
- aucun callback ne doit conserver une référence vers un nœud de l'ancien
modèle ;
- la nouvelle vue doit démarrer sans sélection.
---
## Critères d'acceptation
- [ ] Le projet compile sans warning.
- [ ] `make test` reste entièrement valide.
- [ ] Un seul nœud peut être sélectionné.
- [ ] La sélection d'un dossier est détectée.
- [ ] La sélection d'un fichier est détectée.
- [ ] Le bon `InvestigationNode` est transmis au callback.
- [ ] `Application` reçoit l'événement.
- [ ] Le nom et le type sont affichés dans le terminal.
- [ ] Le changement d'enquête efface l'ancienne sélection.
- [ ] Aucun module graphique ne libère le nœud.
- [ ] Aucun `Gtk-CRITICAL`.
---
## Tests manuels
1. Lancer l'application.
2. Ouvrir le dossier `Template`.
3. Développer `00_BaseDeDonnees`.
4. Sélectionner `Enquete.sqlite`.
5. Vérifier le nom et le type dans le terminal.
6. Sélectionner un dossier.
7. Vérifier que le type affiché est `dossier`.
8. Ouvrir une autre enquête.
9. Vérifier qu'aucune ancienne sélection n'est conservée.
10. Lancer `make test`.
---
## Commit attendu
```text
feat(gui): add investigation tree selection
```

View file

@ -7,6 +7,7 @@
#define LABFY_INVESTIGATION_MAIN_WINDOW_H
#include "core/investigation_tree_model.h"
#include "widgets/investigation_tree_view.h"
#include <gtk/gtk.h>
@ -56,6 +57,12 @@ void main_window_set_tree_model(
const InvestigationTreeModel *tree_model
);
void main_window_set_tree_selection_callback(
MainWindow *main_window,
InvestigationTreeViewSelectionCallback callback,
gpointer user_data
);
/**
* @brief Libère les ressources de la fenêtre.
*

View file

@ -19,6 +19,35 @@
*/
typedef struct InvestigationTreeView InvestigationTreeView;
/**
* @brief Callback appelé lorsqu'un nœud est sélectionné.
*
* @param node Nœud sélectionné, ou NULL si aucune sélection n'est active.
* @param user_data Données privées fournies lors de l'enregistrement
* du callback.
*/
typedef void (*InvestigationTreeViewSelectionCallback)(
const InvestigationNode *node,
gpointer user_data
);
/**
* @brief Définit le callback de sélection.
*
* Le callback est appelé à chaque changement de sélection.
*
* Le composant ne devient pas propriétaire du callback ni de user_data.
*
* @param tree_view Vue arborescente.
* @param callback Fonction appelée lors d'une sélection.
* @param user_data Données privées transmises au callback.
*/
void investigation_tree_view_set_selection_callback(
InvestigationTreeView *tree_view,
InvestigationTreeViewSelectionCallback callback,
gpointer user_data
);
/**
* @brief Crée une nouvelle vue arborescente vide.
*

View file

@ -7,6 +7,7 @@
#define LABFY_INVESTIGATION_SIDEBAR_H
#include "core/investigation_tree_model.h"
#include "widgets/investigation_tree_view.h"
#include <gtk/gtk.h>
@ -55,6 +56,21 @@ void sidebar_set_tree_model(
const InvestigationTreeModel *tree_model
);
/**
* @brief Définit le callback appelé lors de la sélection d'un nœud.
*
* La Sidebar transmet simplement ce callback à InvestigationTreeView.
*
* @param sidebar Barre latérale à configurer.
* @param callback Fonction appelée lors d'un changement de sélection.
* @param user_data Données privées transmises au callback.
*/
void sidebar_set_selection_callback(
Sidebar *sidebar,
InvestigationTreeViewSelectionCallback callback,
gpointer user_data
);
/**
* @brief Libère la structure d'encapsulation du panneau latéral.
*

Binary file not shown.

View file

@ -109,6 +109,50 @@ static void application_on_folder_selected(
);
}
/**
* @brief Traite la sélection d'un nœud dans l'arborescence.
*
* @param node Nœud sélectionné, ou NULL si aucune sélection.
* @param user_data Pointeur vers Application.
*/
static void application_on_tree_node_selected(
const InvestigationNode *node,
gpointer user_data
)
{
Application *application = user_data;
const char *node_name = NULL;
InvestigationNodeType node_type;
if (application == NULL)
{
return;
}
if (node == NULL)
{
g_print("Aucun nœud sélectionné.\n");
return;
}
node_name = investigation_node_get_name(node);
node_type = investigation_node_get_type(node);
g_print(
"Nœud sélectionné : %s\n",
node_name != NULL ? node_name : "(sans nom)"
);
if (node_type == INVESTIGATION_NODE_DIRECTORY)
{
g_print("Type : dossier\n");
}
else
{
g_print("Type : fichier\n");
}
}
static void application_on_activate(
GtkApplication *gtk_application,
gpointer user_data
@ -139,6 +183,12 @@ static void application_on_activate(
return;
}
main_window_set_tree_selection_callback(
application->main_window,
application_on_tree_node_selected,
application
);
main_window_present(application->main_window);
folder_dialog_select_folder(

View file

@ -6,6 +6,7 @@
#include "views/main_window.h"
#include "widgets/sidebar.h"
#include "widgets/workspace.h"
#include "widgets/investigation_tree_view.h"
#include <glib.h>
@ -284,6 +285,24 @@ void main_window_set_tree_model(
);
}
void main_window_set_tree_selection_callback(
MainWindow *main_window,
InvestigationTreeViewSelectionCallback callback,
gpointer user_data
)
{
if (main_window == NULL)
{
return;
}
sidebar_set_selection_callback(
main_window->sidebar,
callback,
user_data
);
}
void main_window_free(MainWindow *main_window)
{
if (main_window == NULL)

View file

@ -105,9 +105,11 @@ struct InvestigationTreeView
GtkWidget *list_view;
GtkListItemFactory *factory;
GtkSelectionModel *selection_model;
GtkSingleSelection *selection_model;
const InvestigationTreeModel *tree_model;
InvestigationTreeViewSelectionCallback selection_callback;
gpointer selection_user_data;
};
/**
@ -364,6 +366,98 @@ static void investigation_tree_view_factory_unbind(
}
}
/**
* @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é.
*
* GtkSingleSelection expose une GtkTreeListRow. Cette fonction récupère
* l'adaptateur privé contenu dans cette ligne, puis le nœud métier associé.
*
* @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é.
*/
@ -386,6 +480,10 @@ static void investigation_tree_view_clear_model(
);
tree_view->tree_model = NULL;
investigation_tree_view_emit_selection(
tree_view,
NULL
);
}
InvestigationTreeView *investigation_tree_view_new(void)
@ -493,7 +591,7 @@ void investigation_tree_view_set_model(
InvestigationTreeItem *root_item = NULL;
GListStore *root_store = NULL;
GtkTreeListModel *gtk_tree_model = NULL;
GtkNoSelection *selection_model = NULL;
GtkSingleSelection *selection_model = NULL;
if (tree_view == NULL)
{
@ -562,7 +660,7 @@ void investigation_tree_view_set_model(
*
* Le constructeur prend possession de gtk_tree_model.
*/
selection_model = gtk_no_selection_new(
selection_model = gtk_single_selection_new(
G_LIST_MODEL(gtk_tree_model)
);
@ -572,17 +670,47 @@ void investigation_tree_view_set_model(
return;
}
tree_view->selection_model =
GTK_SELECTION_MODEL(selection_model);
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),
tree_view->selection_model
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
)

View file

@ -196,6 +196,24 @@ void sidebar_set_tree_model(
);
}
void sidebar_set_selection_callback(
Sidebar *sidebar,
InvestigationTreeViewSelectionCallback callback,
gpointer user_data
)
{
if (sidebar == NULL)
{
return;
}
investigation_tree_view_set_selection_callback(
sidebar->tree_view,
callback,
user_data
);
}
void sidebar_free(Sidebar *sidebar)
{
if (sidebar == NULL)