feat(gui): add investigation tree selection
This commit is contained in:
parent
efe032eefc
commit
6938529fb2
9 changed files with 517 additions and 6 deletions
244
docs/tickets/closed/TICKET-014.md
Normal file
244
docs/tickets/closed/TICKET-014.md
Normal 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
|
||||
```
|
||||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue