diff --git a/docs/tickets/closed/TICKET-019.md b/docs/tickets/closed/TICKET-019.md new file mode 100644 index 0000000..5612eef --- /dev/null +++ b/docs/tickets/closed/TICKET-019.md @@ -0,0 +1,208 @@ +# Ticket #019 + +## Titre + +Afficher une icône selon le type de nœud dans l'arborescence. + +--- + +## Objectif + +Améliorer la lisibilité de `InvestigationTreeView` en affichant une icône +devant chaque dossier ou fichier. + +Les icônes doivent être déterminées uniquement dans la couche graphique. + +--- + +## Architecture + +```text +InvestigationNode + │ + ▼ +InvestigationTreeView + │ + ▼ +GtkImage + GtkLabel +``` + +Le Core fournit : + +- le nom ; +- le chemin ; +- le type. + +La vue choisit l'icône à afficher. + +--- + +## Responsabilités + +### InvestigationTreeView + +Le module doit : + +- afficher une icône devant chaque nœud ; +- utiliser une icône de dossier pour les répertoires ; +- utiliser une icône générique pour les fichiers inconnus ; +- choisir certaines icônes spécialisées selon l'extension ; +- conserver le nom du nœud à côté de l'icône ; +- rester compatible avec `GtkTreeExpander`. + +--- + +## Fichiers concernés + +```text +src/widgets/investigation_tree_view.c +``` + +Éventuellement : + +```text +include/widgets/investigation_tree_view.h +``` + +uniquement si la documentation doit être précisée. + +Aucune modification du Core. + +--- + +## Extensions reconnues + +Première version minimale : + +```text +.sqlite .db +.jpg .jpeg .png .webp +.pdf +.txt .md .log +.csv +.zip .tar .gz .xz +.mp4 .mkv .avi .mov +``` + +Les extensions inconnues utilisent une icône de fichier générique. + +La comparaison doit être insensible à la casse. + +--- + +## Icônes GTK attendues + +Utiliser les noms d'icônes du thème système, par exemple : + +```text +folder-symbolic +text-x-generic-symbolic +application-pdf-symbolic +image-x-generic-symbolic +video-x-generic-symbolic +package-x-generic-symbolic +x-office-spreadsheet-symbolic +folder-database-symbolic +``` + +Si une icône spécialisée n'est pas disponible dans le thème, GTK doit +retomber proprement sur une icône générique. + +--- + +## Structure visuelle d'une ligne + +```text +GtkTreeExpander +└── GtkBox horizontal + ├── GtkImage + └── GtkLabel +``` + +Le label continue d'afficher le nom du nœud. + +--- + +## Fonctions privées suggérées + +```c +static const char *investigation_tree_view_get_icon_name( + const InvestigationNode *node +); +``` + +Cette fonction doit : + +- retourner `folder-symbolic` pour un dossier ; +- inspecter l'extension pour un fichier ; +- retourner une icône générique si aucune extension ne correspond. + +--- + +## Hors périmètre + +Ce ticket ne doit pas : + +- charger des miniatures ; +- analyser le contenu réel des fichiers ; +- lire les types MIME ; +- modifier le Core ; +- ouvrir les fichiers ; +- colorer les lignes ; +- ajouter des badges ; +- ajouter un menu contextuel. + +--- + +## Contraintes techniques + +- GTK4 uniquement ; +- utiliser `GtkImage` ; +- utiliser les icônes du thème système ; +- aucun fichier d'icône embarqué ; +- aucun état global ; +- aucune lecture directe du système de fichiers ; +- compilation sans warning ; +- aucun `Gtk-CRITICAL`. + +--- + +## Critères d'acceptation + +- [ ] Le projet compile sans warning. +- [ ] `make test` reste valide. +- [ ] Les dossiers affichent une icône de dossier. +- [ ] Les fichiers génériques affichent une icône de fichier. +- [ ] Les PDF ont une icône dédiée. +- [ ] Les images ont une icône dédiée. +- [ ] Les vidéos ont une icône dédiée. +- [ ] Les archives ont une icône dédiée. +- [ ] Les bases SQLite ont une icône dédiée. +- [ ] Le nom du nœud reste visible. +- [ ] Le développement et le repli fonctionnent toujours. +- [ ] La sélection fonctionne toujours. +- [ ] Aucun code Core n'est modifié. +- [ ] Aucun `Gtk-CRITICAL`. + +--- + +## Tests manuels + +1. Ouvrir une enquête contenant plusieurs types de fichiers. +2. Vérifier l'icône des dossiers. +3. Vérifier une image. +4. Vérifier un PDF. +5. Vérifier une base SQLite. +6. Vérifier une archive. +7. Vérifier un fichier inconnu. +8. Développer et replier plusieurs branches. +9. Sélectionner plusieurs nœuds. +10. Lancer `make test`. + +--- + +## Commit attendu + +```text +feat(gui): add icons to investigation tree +``` diff --git a/labfy-investigation b/labfy-investigation index d827f49..681950a 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 index e8090ca..f3b79e2 100644 --- a/src/widgets/investigation_tree_view.c +++ b/src/widgets/investigation_tree_view.c @@ -11,6 +11,7 @@ #include #include #include +#include /* * InvestigationTreeItem @@ -18,7 +19,7 @@ * * GListStore exige des objets dérivés de GObject. * - * Cette classe privée adapte donc un InvestigationNode à l'écosystème GTK. + * Cette classe privée adapte un InvestigationNode à l'écosystème GTK. * Elle ne possède jamais le nœud métier : elle conserve seulement son adresse. */ @@ -58,6 +59,10 @@ static void investigation_tree_item_init( * @brief Crée un adaptateur GTK pour un nœud métier. * * L'adaptateur ne devient pas propriétaire du nœud. + * + * @param node Nœud métier observé. + * + * @return Un nouvel adaptateur, ou NULL. */ static InvestigationTreeItem *investigation_tree_item_new( const InvestigationNode *node @@ -75,6 +80,11 @@ static InvestigationTreeItem *investigation_tree_item_new( NULL ); + if (item == NULL) + { + return NULL; + } + item->node = node; return item; @@ -82,6 +92,10 @@ static InvestigationTreeItem *investigation_tree_item_new( /** * @brief Retourne le nœud métier observé par l'adaptateur. + * + * @param item Adaptateur à consulter. + * + * @return Le nœud observé, ou NULL. */ static const InvestigationNode *investigation_tree_item_get_node( const InvestigationTreeItem *item @@ -108,6 +122,7 @@ struct InvestigationTreeView GtkSingleSelection *selection_model; const InvestigationTreeModel *tree_model; + InvestigationTreeViewSelectionCallback selection_callback; gpointer selection_user_data; }; @@ -121,8 +136,8 @@ struct InvestigationTreeView * @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. + * @return Une liste GTK contenant les enfants, ou NULL si le nœud ne possède + * aucun enfant. */ static GListModel *investigation_tree_view_create_children_model( gpointer item, @@ -154,7 +169,8 @@ static GListModel *investigation_tree_view_create_children_model( return NULL; } - children_count = investigation_node_get_children_count(node); + children_count = + investigation_node_get_children_count(node); if (children_count == 0) { @@ -165,6 +181,11 @@ static GListModel *investigation_tree_view_create_children_model( investigation_tree_item_get_type() ); + if (children_store == NULL) + { + return NULL; + } + for (size_t index = 0; index < children_count; ++index) { const InvestigationNode *child_node = NULL; @@ -180,7 +201,9 @@ static GListModel *investigation_tree_view_create_children_model( continue; } - child_item = investigation_tree_item_new(child_node); + child_item = investigation_tree_item_new( + child_node + ); if (child_item == NULL) { @@ -189,7 +212,6 @@ static GListModel *investigation_tree_view_create_children_model( /* * 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, @@ -205,10 +227,16 @@ static GListModel *investigation_tree_view_create_children_model( /** * @brief Crée les widgets constituant une ligne de l'arborescence. * - * Une ligne contient : + * Structure créée : * * GtkTreeExpander - * └── GtkLabel + * └── GtkBox horizontal + * ├── GtkImage + * └── GtkLabel + * + * @param factory Fabrique GTK. + * @param list_item Élément de liste à préparer. + * @param user_data Données privées inutilisées. */ static void investigation_tree_view_factory_setup( GtkSignalListItemFactory *factory, @@ -217,14 +245,28 @@ static void investigation_tree_view_factory_setup( ) { GtkWidget *expander = NULL; + GtkWidget *row_box = NULL; + GtkWidget *image = NULL; GtkWidget *label = NULL; (void)factory; (void)user_data; expander = gtk_tree_expander_new(); + + row_box = gtk_box_new( + GTK_ORIENTATION_HORIZONTAL, + 6 + ); + + image = gtk_image_new(); label = gtk_label_new(NULL); + gtk_label_set_xalign( + GTK_LABEL(label), + 0.0f + ); + gtk_widget_set_halign( label, GTK_ALIGN_START @@ -235,9 +277,19 @@ static void investigation_tree_view_factory_setup( TRUE ); + gtk_box_append( + GTK_BOX(row_box), + image + ); + + gtk_box_append( + GTK_BOX(row_box), + label + ); + gtk_tree_expander_set_child( GTK_TREE_EXPANDER(expander), - label + row_box ); gtk_list_item_set_child( @@ -246,8 +298,8 @@ static void investigation_tree_view_factory_setup( ); /* - * Le focus doit appartenir au GtkTreeExpander afin que ses raccourcis - * clavier d'ouverture et de fermeture fonctionnent correctement. + * Le focus reste dans GtkTreeExpander afin que les raccourcis + * d'ouverture et de fermeture fonctionnent. */ gtk_list_item_set_focusable( list_item, @@ -255,8 +307,87 @@ static void investigation_tree_view_factory_setup( ); } +static const char *investigation_tree_view_get_icon_name( + const InvestigationNode *node +) +{ + const char *name = NULL; + const char *extension = NULL; + + if (node == NULL) + { + return "text-x-generic-symbolic"; + } + + if (investigation_node_get_type(node) == + INVESTIGATION_NODE_DIRECTORY) + { + return "folder-symbolic"; + } + + name = investigation_node_get_name(node); + + if (name == NULL) + { + return "text-x-generic-symbolic"; + } + + extension = strrchr(name, '.'); + + if (extension == NULL) + { + return "text-x-generic-symbolic"; + } + + if (g_ascii_strcasecmp(extension, ".sqlite") == 0 || + g_ascii_strcasecmp(extension, ".db") == 0) + { + return "folder-database-symbolic"; + } + + if (g_ascii_strcasecmp(extension, ".jpg") == 0 || + g_ascii_strcasecmp(extension, ".jpeg") == 0 || + g_ascii_strcasecmp(extension, ".png") == 0 || + g_ascii_strcasecmp(extension, ".webp") == 0) + { + return "image-x-generic-symbolic"; + } + + if (g_ascii_strcasecmp(extension, ".pdf") == 0) + { + return "application-pdf-symbolic"; + } + + if (g_ascii_strcasecmp(extension, ".csv") == 0) + { + return "x-office-spreadsheet-symbolic"; + } + + if (g_ascii_strcasecmp(extension, ".zip") == 0 || + g_ascii_strcasecmp(extension, ".tar") == 0 || + g_ascii_strcasecmp(extension, ".gz") == 0 || + g_ascii_strcasecmp(extension, ".xz") == 0) + { + return "package-x-generic-symbolic"; + } + + if (g_ascii_strcasecmp(extension, ".mp4") == 0 || + g_ascii_strcasecmp(extension, ".mkv") == 0 || + g_ascii_strcasecmp(extension, ".avi") == 0 || + g_ascii_strcasecmp(extension, ".mov") == 0) + { + return "video-x-generic-symbolic"; + } + + return "text-x-generic-symbolic"; +} + /** * @brief Relie une ligne GTK à un nœud métier. + * + * @param factory Fabrique GTK. + * @param list_item Élément de liste à remplir. + * @param user_data Données privées inutilisées. */ static void investigation_tree_view_factory_bind( GtkSignalListItemFactory *factory, @@ -267,8 +398,13 @@ static void investigation_tree_view_factory_bind( GtkTreeListRow *tree_row = NULL; InvestigationTreeItem *tree_item = NULL; const InvestigationNode *node = NULL; + const char *icon_name = NULL; + GtkWidget *expander = NULL; + GtkWidget *row_box = NULL; + GtkWidget *image = NULL; GtkWidget *label = NULL; + const char *node_name = NULL; (void)factory; @@ -283,43 +419,87 @@ static void investigation_tree_view_factory_bind( return; } - tree_item = gtk_tree_list_row_get_item(tree_row); + tree_item = gtk_tree_list_row_get_item( + tree_row + ); if (tree_item == NULL) { return; } - node = investigation_tree_item_get_node(tree_item); + node = investigation_tree_item_get_node( + tree_item + ); if (node == NULL) { return; } - expander = gtk_list_item_get_child(list_item); + expander = gtk_list_item_get_child( + list_item + ); if (expander == NULL) { return; } - label = gtk_tree_expander_get_child( + row_box = gtk_tree_expander_get_child( GTK_TREE_EXPANDER(expander) ); + if (row_box == NULL) + { + return; + } + + image = gtk_widget_get_first_child( + row_box + ); + + if (image == NULL) + { + return; + } + + label = gtk_widget_get_next_sibling( + image + ); + if (label == NULL) { return; } - node_name = investigation_node_get_name(node); + node_name = investigation_node_get_name( + node + ); + + icon_name = investigation_tree_view_get_icon_name( + node + ); gtk_tree_expander_set_list_row( GTK_TREE_EXPANDER(expander), tree_row ); + /* + * Pour l'instant l'image reste vide. + * Elle sera configurée dans la suite du ticket #019. + */ + gtk_image_set_from_icon_name( + GTK_IMAGE(image), + icon_name + ); + + gtk_image_set_pixel_size( + GTK_IMAGE(image), + 16 + ); + gtk_label_set_text( GTK_LABEL(label), node_name != NULL ? node_name : "" @@ -328,6 +508,10 @@ static void investigation_tree_view_factory_bind( /** * @brief Détache les données d'une ligne réutilisée par GTK. + * + * @param factory Fabrique GTK. + * @param list_item Élément de liste à nettoyer. + * @param user_data Données privées inutilisées. */ static void investigation_tree_view_factory_unbind( GtkSignalListItemFactory *factory, @@ -336,27 +520,52 @@ static void investigation_tree_view_factory_unbind( ) { GtkWidget *expander = NULL; + GtkWidget *row_box = NULL; + GtkWidget *image = NULL; GtkWidget *label = NULL; (void)factory; (void)user_data; - expander = gtk_list_item_get_child(list_item); + expander = gtk_list_item_get_child( + list_item + ); if (expander == NULL) { return; } - label = gtk_tree_expander_get_child( + row_box = gtk_tree_expander_get_child( GTK_TREE_EXPANDER(expander) ); + if (row_box != NULL) + { + image = gtk_widget_get_first_child( + row_box + ); + + if (image != NULL) + { + label = gtk_widget_get_next_sibling( + image + ); + } + } + gtk_tree_expander_set_list_row( GTK_TREE_EXPANDER(expander), NULL ); + if (image != NULL) + { + gtk_image_clear( + GTK_IMAGE(image) + ); + } + if (label != NULL) { gtk_label_set_text( @@ -396,9 +605,6 @@ static void investigation_tree_view_emit_selection( /** * @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. @@ -422,9 +628,10 @@ static void investigation_tree_view_on_selection_changed( return; } - selected_item = gtk_single_selection_get_selected_item( - GTK_SINGLE_SELECTION(selection_model) - ); + selected_item = + gtk_single_selection_get_selected_item( + GTK_SINGLE_SELECTION(selection_model) + ); if (selected_item == NULL) { @@ -436,9 +643,13 @@ static void investigation_tree_view_on_selection_changed( return; } - tree_row = GTK_TREE_LIST_ROW(selected_item); + tree_row = GTK_TREE_LIST_ROW( + selected_item + ); - tree_item = gtk_tree_list_row_get_item(tree_row); + tree_item = gtk_tree_list_row_get_item( + tree_row + ); if (tree_item == NULL) { @@ -450,7 +661,9 @@ static void investigation_tree_view_on_selection_changed( return; } - node = investigation_tree_item_get_node(tree_item); + node = investigation_tree_item_get_node( + tree_item + ); investigation_tree_view_emit_selection( tree_view, @@ -460,6 +673,8 @@ static void investigation_tree_view_on_selection_changed( /** * @brief Retire le modèle GTK actuellement affiché. + * + * @param tree_view Vue à nettoyer. */ static void investigation_tree_view_clear_model( InvestigationTreeView *tree_view @@ -480,6 +695,7 @@ static void investigation_tree_view_clear_model( ); tree_view->tree_model = NULL; + investigation_tree_view_emit_selection( tree_view, NULL @@ -495,6 +711,11 @@ InvestigationTreeView *investigation_tree_view_new(void) 1 ); + if (tree_view == NULL) + { + return NULL; + } + tree_view->factory = GTK_LIST_ITEM_FACTORY( gtk_signal_list_item_factory_new() @@ -509,21 +730,27 @@ InvestigationTreeView *investigation_tree_view_new(void) g_signal_connect( tree_view->factory, "setup", - G_CALLBACK(investigation_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), + G_CALLBACK( + investigation_tree_view_factory_bind + ), NULL ); g_signal_connect( tree_view->factory, "unbind", - G_CALLBACK(investigation_tree_view_factory_unbind), + G_CALLBACK( + investigation_tree_view_factory_unbind + ), NULL ); @@ -538,7 +765,8 @@ InvestigationTreeView *investigation_tree_view_new(void) return NULL; } - tree_view->root_widget = gtk_scrolled_window_new(); + tree_view->root_widget = + gtk_scrolled_window_new(); if (tree_view->root_widget == NULL) { @@ -588,6 +816,7 @@ void investigation_tree_view_set_model( ) { const InvestigationNode *root_node = NULL; + InvestigationTreeItem *root_item = NULL; GListStore *root_store = NULL; GtkTreeListModel *gtk_tree_model = NULL; @@ -598,14 +827,19 @@ void investigation_tree_view_set_model( return; } - investigation_tree_view_clear_model(tree_view); + investigation_tree_view_clear_model( + tree_view + ); if (tree_model == NULL) { return; } - root_node = investigation_tree_model_get_root(tree_model); + root_node = + investigation_tree_model_get_root( + tree_model + ); if (root_node == NULL) { @@ -616,7 +850,14 @@ void investigation_tree_view_set_model( investigation_tree_item_get_type() ); - root_item = investigation_tree_item_new(root_node); + if (root_store == NULL) + { + return; + } + + root_item = investigation_tree_item_new( + root_node + ); if (root_item == NULL) { @@ -631,15 +872,6 @@ void investigation_tree_view_set_model( 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, @@ -654,12 +886,6 @@ void investigation_tree_view_set_model( 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_single_selection_new( G_LIST_MODEL(gtk_tree_model) ); @@ -683,16 +909,23 @@ void investigation_tree_view_set_model( g_signal_connect( selection_model, "notify::selected-item", - G_CALLBACK(investigation_tree_view_on_selection_changed), + G_CALLBACK( + investigation_tree_view_on_selection_changed + ), tree_view ); - tree_view->selection_model = selection_model; - tree_view->tree_model = tree_model; + tree_view->selection_model = + selection_model; + + tree_view->tree_model = + tree_model; gtk_list_view_set_model( GTK_LIST_VIEW(tree_view->list_view), - GTK_SELECTION_MODEL(tree_view->selection_model) + GTK_SELECTION_MODEL( + tree_view->selection_model + ) ); } @@ -707,8 +940,11 @@ void investigation_tree_view_set_selection_callback( return; } - tree_view->selection_callback = callback; - tree_view->selection_user_data = user_data; + tree_view->selection_callback = + callback; + + tree_view->selection_user_data = + user_data; } void investigation_tree_view_free( @@ -720,15 +956,17 @@ void investigation_tree_view_free( return; } - investigation_tree_view_clear_model(tree_view); + 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. + * root_widget et list_view appartiennent à l'arbre GTK + * une fois intégrés dans la fenêtre. */ g_free(tree_view); } diff --git a/tests/test_investigation_node b/tests/test_investigation_node deleted file mode 100755 index b7614ef..0000000 Binary files a/tests/test_investigation_node and /dev/null differ diff --git a/tests/test_investigation_tree_builder b/tests/test_investigation_tree_builder deleted file mode 100755 index d6cd484..0000000 Binary files a/tests/test_investigation_tree_builder and /dev/null differ diff --git a/tests/test_investigation_tree_model b/tests/test_investigation_tree_model deleted file mode 100755 index 2c823e1..0000000 Binary files a/tests/test_investigation_tree_model and /dev/null differ