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 4674c74..9f9ac84 100755 Binary files a/labfy-investigation and b/labfy-investigation differ 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); +}