diff --git a/docs/tickets/closed/TICKET-014.md b/docs/tickets/closed/TICKET-014.md new file mode 100644 index 0000000..fd4edb8 --- /dev/null +++ b/docs/tickets/closed/TICKET-014.md @@ -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 +``` diff --git a/include/views/main_window.h b/include/views/main_window.h index 0d3e65f..b2c9e0a 100644 --- a/include/views/main_window.h +++ b/include/views/main_window.h @@ -7,6 +7,7 @@ #define LABFY_INVESTIGATION_MAIN_WINDOW_H #include "core/investigation_tree_model.h" +#include "widgets/investigation_tree_view.h" #include @@ -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. * diff --git a/include/widgets/investigation_tree_view.h b/include/widgets/investigation_tree_view.h index 37fbdbc..c39649a 100644 --- a/include/widgets/investigation_tree_view.h +++ b/include/widgets/investigation_tree_view.h @@ -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. * diff --git a/include/widgets/sidebar.h b/include/widgets/sidebar.h index 8171f99..f69b1fe 100644 --- a/include/widgets/sidebar.h +++ b/include/widgets/sidebar.h @@ -7,6 +7,7 @@ #define LABFY_INVESTIGATION_SIDEBAR_H #include "core/investigation_tree_model.h" +#include "widgets/investigation_tree_view.h" #include @@ -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. * diff --git a/labfy-investigation b/labfy-investigation index 65b1d25..0aea6e1 100755 Binary files a/labfy-investigation and b/labfy-investigation differ diff --git a/src/core/application.c b/src/core/application.c index d4c0243..5e93ce7 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -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( diff --git a/src/views/main_window.c b/src/views/main_window.c index c1c00aa..5771933 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -6,6 +6,7 @@ #include "views/main_window.h" #include "widgets/sidebar.h" #include "widgets/workspace.h" +#include "widgets/investigation_tree_view.h" #include @@ -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) diff --git a/src/widgets/investigation_tree_view.c b/src/widgets/investigation_tree_view.c index 4c87de9..e8090ca 100644 --- a/src/widgets/investigation_tree_view.c +++ b/src/widgets/investigation_tree_view.c @@ -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 ) diff --git a/src/widgets/sidebar.c b/src/widgets/sidebar.c index cdaff3e..b3e0989 100644 --- a/src/widgets/sidebar.c +++ b/src/widgets/sidebar.c @@ -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)