feat(gui): add icons to investigation tree

This commit is contained in:
grayTerminal-sh 2026-07-14 18:44:43 +02:00
parent 4ce822ee70
commit fa57acfe89
6 changed files with 504 additions and 58 deletions

View file

@ -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
```

Binary file not shown.

View file

@ -11,6 +11,7 @@
#include <glib-object.h>
#include <glib.h>
#include <gtk/gtk.h>
#include <string.h>
/*
* 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
* 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,7 +628,8 @@ static void investigation_tree_view_on_selection_changed(
return;
}
selected_item = gtk_single_selection_get_selected_item(
selected_item =
gtk_single_selection_get_selected_item(
GTK_SINGLE_SELECTION(selection_model)
);
@ -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);
}

Binary file not shown.

Binary file not shown.