feat(sidebar): add entity navigation

This commit is contained in:
grayTerminal-sh 2026-07-22 10:53:44 +02:00
parent 1fb3c71c3f
commit ab6621e95f
8 changed files with 567 additions and 1 deletions

View file

@ -130,6 +130,7 @@ Le socle actuel comprend notamment :
- arborescence des fichiers ; - arborescence des fichiers ;
- fenêtre principale GTK4 ; - fenêtre principale GTK4 ;
- barre latérale et espace de travail ; - barre latérale et espace de travail ;
- navigation et recherche locale des entités dans la barre latérale ;
- affichage graphique des erreurs ; - affichage graphique des erreurs ;
- tâches asynchrones annulables ; - tâches asynchrones annulables ;
- gestionnaire de tâches ; - gestionnaire de tâches ;

View file

@ -183,6 +183,21 @@ const EntityRecord *investigation_graph_view_get_selected_entity(
const InvestigationGraphView *graph_view const InvestigationGraphView *graph_view
); );
/**
* @brief Sélectionne une entité du graphe par son UUID.
*
* La vue reste inchangée si l'identifiant est absent du graphe.
*
* @param graph_view Vue graphique à modifier.
* @param entity_identifier UUID de l'entité.
*
* @return TRUE lorsque l'entité est trouvée et sélectionnée.
*/
gboolean investigation_graph_view_select_entity(
InvestigationGraphView *graph_view,
const char *entity_identifier
);
/** /**
* @brief Supprime la sélection courante. * @brief Supprime la sélection courante.
* *

View file

@ -9,6 +9,7 @@
#include "core/investigation_tree_model.h" #include "core/investigation_tree_model.h"
#include "widgets/investigation_tree_view.h" #include "widgets/investigation_tree_view.h"
#include "widgets/evidence_category_model.h" #include "widgets/evidence_category_model.h"
#include "models/investigation_graph_model.h"
#include <gtk/gtk.h> #include <gtk/gtk.h>
@ -34,6 +35,20 @@ typedef void (*SidebarEvidenceSelectionCallback)(
gpointer user_data gpointer user_data
); );
/**
* @brief Callback appelé lorsqu'une entité est sélectionnée.
*
* L'identifiant est emprunté et valide uniquement pendant l'appel.
* Une sélection vide transmet NULL.
*
* @param entity_identifier UUID de l'entité, ou NULL.
* @param user_data Données privées fournies par l'appelant.
*/
typedef void (*SidebarEntitySelectionCallback)(
const char *entity_identifier,
gpointer user_data
);
/** /**
* @brief Crée un nouveau panneau de navigation latéral. * @brief Crée un nouveau panneau de navigation latéral.
* *
@ -113,6 +128,32 @@ void sidebar_set_evidence_selection_callback(
gpointer user_data gpointer user_data
); );
/**
* @brief Installe les entités du graphe dans la barre latérale.
*
* La Sidebar emprunte graph_model. Passer NULL vide la liste.
*
* @param sidebar Barre latérale à actualiser.
* @param graph_model Graphe emprunté, ou NULL.
*/
void sidebar_set_entity_model(
Sidebar *sidebar,
const InvestigationGraphModel *graph_model
);
/**
* @brief Définit le callback de sélection d'une entité.
*
* @param sidebar Barre latérale à configurer.
* @param callback Callback facultatif.
* @param user_data Données privées transmises au callback.
*/
void sidebar_set_entity_selection_callback(
Sidebar *sidebar,
SidebarEntitySelectionCallback callback,
gpointer user_data
);
/** /**
* @brief Libère la structure d'encapsulation du panneau latéral. * @brief Libère la structure d'encapsulation du panneau latéral.
* *

View file

@ -127,6 +127,21 @@ void workspace_set_selected_evidence(
const EvidenceRecord *evidence_record const EvidenceRecord *evidence_record
); );
/**
* @brief Sélectionne une entité dans le graphe affiché.
*
* La sélection actualise également le volet de détails et le contexte OSINT.
*
* @param workspace Zone de travail.
* @param entity_identifier UUID de l'entité à sélectionner.
*
* @return TRUE lorsque l'entité est trouvée et sélectionnée.
*/
gboolean workspace_select_graph_entity(
Workspace *workspace,
const char *entity_identifier
);
/** /**
* @brief Définit le callback de vérification de la preuve affichée. * @brief Définit le callback de vérification de la preuve affichée.
* *

View file

@ -125,6 +125,29 @@ struct MainWindow
}; };
/**
* @brief Ouvre dans le workspace l'entité choisie dans la sidebar.
*/
static void main_window_on_sidebar_entity_selected(
const char *entity_identifier,
gpointer user_data
)
{
MainWindow *main_window = user_data;
if (main_window == NULL ||
entity_identifier == NULL ||
entity_identifier[0] == '\0')
{
return;
}
workspace_select_graph_entity(
main_window->workspace,
entity_identifier
);
}
/** /**
* @brief Transmet la demande de création d'une enquête au contrôleur. * @brief Transmet la demande de création d'une enquête au contrôleur.
* *
@ -663,6 +686,12 @@ MainWindow *main_window_new(
return NULL; return NULL;
} }
sidebar_set_entity_selection_callback(
main_window->sidebar,
main_window_on_sidebar_entity_selected,
main_window
);
workspace_set_verify_evidence_callback( workspace_set_verify_evidence_callback(
main_window->workspace, main_window->workspace,
main_window_on_verify_evidence_requested, main_window_on_verify_evidence_requested,
@ -1040,6 +1069,11 @@ void main_window_set_graph_loading(
FALSE FALSE
); );
sidebar_set_entity_model(
main_window->sidebar,
NULL
);
workspace_set_graph_loading( workspace_set_graph_loading(
main_window->workspace main_window->workspace
); );
@ -1062,6 +1096,11 @@ void main_window_set_graph(
graph_layout graph_layout
); );
sidebar_set_entity_model(
main_window->sidebar,
graph_model
);
gtk_widget_set_sensitive( gtk_widget_set_sensitive(
main_window->show_graph_button, main_window->show_graph_button,
graph_model != NULL graph_model != NULL
@ -1083,6 +1122,11 @@ void main_window_set_graph_error(
FALSE FALSE
); );
sidebar_set_entity_model(
main_window->sidebar,
NULL
);
workspace_set_graph_error( workspace_set_graph_error(
main_window->workspace, main_window->workspace,
message message
@ -1103,6 +1147,11 @@ void main_window_clear_graph(
FALSE FALSE
); );
sidebar_set_entity_model(
main_window->sidebar,
NULL
);
workspace_clear_graph( workspace_clear_graph(
main_window->workspace main_window->workspace
); );
@ -1436,6 +1485,17 @@ void main_window_free(
main_window->sidebar, main_window->sidebar,
NULL NULL
); );
sidebar_set_entity_selection_callback(
main_window->sidebar,
NULL,
NULL
);
sidebar_set_entity_model(
main_window->sidebar,
NULL
);
} }
/* /*
@ -1526,4 +1586,3 @@ void main_window_free(
main_window main_window
); );
} }

View file

@ -4257,6 +4257,40 @@ const EntityRecord *investigation_graph_view_get_selected_entity(
return graph_view->selected_node->entity_record; return graph_view->selected_node->entity_record;
} }
gboolean investigation_graph_view_select_entity(
InvestigationGraphView *graph_view,
const char *entity_identifier
)
{
InvestigationGraphNodeLayout *node_layout = NULL;
if (graph_view == NULL ||
graph_view->node_layouts_by_identifier == NULL ||
entity_identifier == NULL ||
!g_uuid_string_is_valid(entity_identifier))
{
return FALSE;
}
node_layout = g_hash_table_lookup(
graph_view->node_layouts_by_identifier,
entity_identifier
);
if (node_layout == NULL ||
node_layout->kind != INVESTIGATION_GRAPH_NODE_KIND_ENTITY)
{
return FALSE;
}
investigation_graph_view_set_selected_node(
graph_view,
node_layout
);
return TRUE;
}
void investigation_graph_view_clear_selection( void investigation_graph_view_clear_selection(
InvestigationGraphView *graph_view InvestigationGraphView *graph_view
) )

View file

@ -9,6 +9,7 @@
#include "widgets/evidence_tree_view.h" #include "widgets/evidence_tree_view.h"
#include <glib.h> #include <glib.h>
#include <string.h>
/** /**
* @brief Largeur initiale du panneau latéral. * @brief Largeur initiale du panneau latéral.
@ -27,6 +28,12 @@
#define SIDEBAR_PAGE_EVIDENCE \ #define SIDEBAR_PAGE_EVIDENCE \
"evidence" "evidence"
/**
* @brief Identifiant de la page contenant les entités.
*/
#define SIDEBAR_PAGE_ENTITY \
"entity"
/** /**
* @brief État vide de l'onglet Preuves. * @brief État vide de l'onglet Preuves.
*/ */
@ -61,6 +68,14 @@ struct Sidebar
GtkWidget *evidence_empty_page; GtkWidget *evidence_empty_page;
GtkWidget *evidence_empty_label; GtkWidget *evidence_empty_label;
GtkWidget *entity_page;
GtkWidget *entity_search_entry;
GtkWidget *entity_count_label;
GtkWidget *entity_scrolled_window;
GtkWidget *entity_list_box;
GtkWidget *entity_empty_label;
GtkWidget *entity_no_result_label;
InvestigationTreeView *tree_view; InvestigationTreeView *tree_view;
EvidenceTreeView *evidence_tree_view; EvidenceTreeView *evidence_tree_view;
@ -68,8 +83,132 @@ struct Sidebar
evidence_selection_callback; evidence_selection_callback;
gpointer evidence_selection_user_data; gpointer evidence_selection_user_data;
SidebarEntitySelectionCallback
entity_selection_callback;
gpointer entity_selection_user_data;
}; };
/**
* @brief Filtre une ligne d'entité avec le texte de recherche courant.
*/
static gboolean sidebar_filter_entity_row(
GtkListBoxRow *row,
gpointer user_data
)
{
Sidebar *sidebar = user_data;
const EntityRecord *entity_record = NULL;
const char *search_text = NULL;
char *normalized_search = NULL;
char *normalized_value = NULL;
char *normalized_type = NULL;
gboolean visible = TRUE;
if (sidebar == NULL || row == NULL)
{
return FALSE;
}
search_text = gtk_editable_get_text(
GTK_EDITABLE(sidebar->entity_search_entry)
);
if (search_text == NULL || search_text[0] == '\0')
{
return TRUE;
}
entity_record = g_object_get_data(
G_OBJECT(row),
"entity-record"
);
if (entity_record == NULL)
{
return FALSE;
}
normalized_search = g_utf8_casefold(search_text, -1);
normalized_value = g_utf8_casefold(
entity_record_get_value(entity_record),
-1
);
normalized_type = g_utf8_casefold(
entity_record_get_type_identifier(entity_record),
-1
);
visible = normalized_search != NULL &&
((normalized_value != NULL &&
strstr(normalized_value, normalized_search) != NULL) ||
(normalized_type != NULL &&
strstr(normalized_type, normalized_search) != NULL));
g_free(normalized_type);
g_free(normalized_value);
g_free(normalized_search);
return visible;
}
/**
* @brief Réapplique le filtre local des entités.
*/
static void sidebar_on_entity_search_changed(
GtkSearchEntry *search_entry,
gpointer user_data
)
{
Sidebar *sidebar = user_data;
(void) search_entry;
if (sidebar != NULL && sidebar->entity_list_box != NULL)
{
gtk_list_box_invalidate_filter(
GTK_LIST_BOX(sidebar->entity_list_box)
);
}
}
/**
* @brief Relaie la sélection provenant de la liste d'entités.
*/
static void sidebar_on_entity_row_selected(
GtkListBox *list_box,
GtkListBoxRow *row,
gpointer user_data
)
{
Sidebar *sidebar = user_data;
const EntityRecord *entity_record = NULL;
const char *entity_identifier = NULL;
(void) list_box;
if (sidebar == NULL ||
sidebar->entity_selection_callback == NULL)
{
return;
}
if (row != NULL)
{
entity_record = g_object_get_data(
G_OBJECT(row),
"entity-record"
);
entity_identifier = entity_record_get_identifier(entity_record);
}
sidebar->entity_selection_callback(
entity_identifier,
sidebar->entity_selection_user_data
);
}
/** /**
* @brief Relaie la sélection provenant de EvidenceTreeView. * @brief Relaie la sélection provenant de EvidenceTreeView.
*/ */
@ -585,6 +724,129 @@ Sidebar *sidebar_new(void)
"Preuves" "Preuves"
); );
/*
* Page Entités : recherche locale, compteur et liste sélectionnable.
*/
sidebar->entity_page = gtk_box_new(
GTK_ORIENTATION_VERTICAL,
8
);
sidebar->entity_search_entry = gtk_search_entry_new();
sidebar->entity_count_label = gtk_label_new("0 entité");
sidebar->entity_scrolled_window = gtk_scrolled_window_new();
sidebar->entity_list_box = gtk_list_box_new();
sidebar->entity_empty_label = gtk_label_new(
"Aucune entité active dans cette enquête."
);
sidebar->entity_no_result_label = gtk_label_new(
"Aucune entité ne correspond à la recherche."
);
if (sidebar->entity_page == NULL ||
sidebar->entity_search_entry == NULL ||
sidebar->entity_count_label == NULL ||
sidebar->entity_scrolled_window == NULL ||
sidebar->entity_list_box == NULL ||
sidebar->entity_empty_label == NULL ||
sidebar->entity_no_result_label == NULL)
{
sidebar_free(sidebar);
return NULL;
}
gtk_widget_set_margin_start(sidebar->entity_search_entry, 8);
gtk_widget_set_margin_end(sidebar->entity_search_entry, 8);
gtk_search_entry_set_placeholder_text(
GTK_SEARCH_ENTRY(sidebar->entity_search_entry),
"Rechercher une entité"
);
gtk_widget_set_halign(
sidebar->entity_count_label,
GTK_ALIGN_START
);
gtk_widget_set_margin_start(sidebar->entity_count_label, 12);
gtk_widget_add_css_class(
sidebar->entity_count_label,
"dim-label"
);
gtk_widget_set_hexpand(sidebar->entity_scrolled_window, TRUE);
gtk_widget_set_vexpand(sidebar->entity_scrolled_window, TRUE);
gtk_scrolled_window_set_policy(
GTK_SCROLLED_WINDOW(sidebar->entity_scrolled_window),
GTK_POLICY_NEVER,
GTK_POLICY_AUTOMATIC
);
gtk_scrolled_window_set_has_frame(
GTK_SCROLLED_WINDOW(sidebar->entity_scrolled_window),
FALSE
);
gtk_widget_set_hexpand(sidebar->entity_list_box, TRUE);
gtk_scrolled_window_set_child(
GTK_SCROLLED_WINDOW(sidebar->entity_scrolled_window),
sidebar->entity_list_box
);
gtk_list_box_set_selection_mode(
GTK_LIST_BOX(sidebar->entity_list_box),
GTK_SELECTION_SINGLE
);
gtk_list_box_set_filter_func(
GTK_LIST_BOX(sidebar->entity_list_box),
sidebar_filter_entity_row,
sidebar,
NULL
);
gtk_label_set_wrap(
GTK_LABEL(sidebar->entity_no_result_label),
TRUE
);
gtk_widget_set_margin_start(sidebar->entity_no_result_label, 20);
gtk_widget_set_margin_end(sidebar->entity_no_result_label, 20);
gtk_widget_set_margin_top(sidebar->entity_no_result_label, 20);
gtk_list_box_set_placeholder(
GTK_LIST_BOX(sidebar->entity_list_box),
sidebar->entity_no_result_label
);
gtk_widget_set_halign(sidebar->entity_empty_label, GTK_ALIGN_CENTER);
gtk_widget_set_valign(sidebar->entity_empty_label, GTK_ALIGN_CENTER);
gtk_widget_set_vexpand(sidebar->entity_empty_label, TRUE);
gtk_label_set_wrap(GTK_LABEL(sidebar->entity_empty_label), TRUE);
g_signal_connect(
sidebar->entity_search_entry,
"search-changed",
G_CALLBACK(sidebar_on_entity_search_changed),
sidebar
);
g_signal_connect(
sidebar->entity_list_box,
"row-selected",
G_CALLBACK(sidebar_on_entity_row_selected),
sidebar
);
gtk_box_append(
GTK_BOX(sidebar->entity_page),
sidebar->entity_search_entry
);
gtk_box_append(
GTK_BOX(sidebar->entity_page),
sidebar->entity_count_label
);
gtk_box_append(
GTK_BOX(sidebar->entity_page),
sidebar->entity_scrolled_window
);
gtk_box_append(
GTK_BOX(sidebar->entity_page),
sidebar->entity_empty_label
);
gtk_stack_add_titled(
GTK_STACK(sidebar->stack),
sidebar->entity_page,
SIDEBAR_PAGE_ENTITY,
"Entités"
);
/* /*
* L'onglet Dossier reste sélectionné au démarrage. * L'onglet Dossier reste sélectionné au démarrage.
*/ */
@ -751,6 +1013,126 @@ void sidebar_set_evidence_selection_callback(
user_data; user_data;
} }
void sidebar_set_entity_model(
Sidebar *sidebar,
const InvestigationGraphModel *graph_model
)
{
GPtrArray *entity_records = NULL;
GtkWidget *child = NULL;
guint entity_index = 0;
char *count_text = NULL;
if (sidebar == NULL || sidebar->entity_list_box == NULL)
{
return;
}
while ((child = gtk_widget_get_first_child(
sidebar->entity_list_box
)) != NULL)
{
gtk_list_box_remove(
GTK_LIST_BOX(sidebar->entity_list_box),
child
);
}
if (graph_model != NULL)
{
entity_records = investigation_graph_model_list_entities(
graph_model,
NULL
);
}
for (entity_index = 0;
entity_records != NULL && entity_index < entity_records->len;
entity_index++)
{
const EntityRecord *entity_record = g_ptr_array_index(
entity_records,
entity_index
);
GtkWidget *row = gtk_list_box_row_new();
GtkWidget *content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
GtkWidget *value_label = gtk_label_new(
entity_record_get_value(entity_record)
);
GtkWidget *type_label = gtk_label_new(
entity_record_get_type_identifier(entity_record)
);
if (row == NULL || content == NULL ||
value_label == NULL || type_label == NULL)
{
continue;
}
gtk_widget_set_margin_start(content, 12);
gtk_widget_set_margin_end(content, 12);
gtk_widget_set_margin_top(content, 8);
gtk_widget_set_margin_bottom(content, 8);
gtk_label_set_xalign(GTK_LABEL(value_label), 0.0F);
gtk_label_set_ellipsize(
GTK_LABEL(value_label),
PANGO_ELLIPSIZE_END
);
gtk_label_set_xalign(GTK_LABEL(type_label), 0.0F);
gtk_widget_add_css_class(type_label, "dim-label");
gtk_box_append(GTK_BOX(content), value_label);
gtk_box_append(GTK_BOX(content), type_label);
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), content);
g_object_set_data(
G_OBJECT(row),
"entity-record",
(gpointer) entity_record
);
gtk_list_box_append(
GTK_LIST_BOX(sidebar->entity_list_box),
row
);
}
count_text = g_strdup_printf(
"%u %s",
entity_records != NULL ? entity_records->len : 0U,
entity_records != NULL && entity_records->len > 1U
? "entités"
: "entité"
);
gtk_label_set_text(
GTK_LABEL(sidebar->entity_count_label),
count_text != NULL ? count_text : "0 entité"
);
gtk_widget_set_visible(
sidebar->entity_empty_label,
entity_records == NULL || entity_records->len == 0U
);
gtk_widget_set_visible(
sidebar->entity_scrolled_window,
entity_records != NULL && entity_records->len > 0U
);
g_free(count_text);
g_clear_pointer(&entity_records, g_ptr_array_unref);
}
void sidebar_set_entity_selection_callback(
Sidebar *sidebar,
SidebarEntitySelectionCallback callback,
gpointer user_data
)
{
if (sidebar == NULL)
{
return;
}
sidebar->entity_selection_callback = callback;
sidebar->entity_selection_user_data = user_data;
}
void sidebar_free(Sidebar *sidebar) void sidebar_free(Sidebar *sidebar)
{ {
if (sidebar == NULL) if (sidebar == NULL)
@ -773,6 +1155,9 @@ void sidebar_free(Sidebar *sidebar)
); );
} }
sidebar->entity_selection_callback = NULL;
sidebar->entity_selection_user_data = NULL;
/* /*
* Les widgets GTK sont intégrés dans l'arbre de widgets de la fenêtre. * Les widgets GTK sont intégrés dans l'arbre de widgets de la fenêtre.
* GTK gère leur destruction lorsque la fenêtre est détruite. * GTK gère leur destruction lorsque la fenêtre est détruite.

View file

@ -2045,6 +2045,22 @@ void workspace_set_selected_evidence(
); );
} }
gboolean workspace_select_graph_entity(
Workspace *workspace,
const char *entity_identifier
)
{
if (workspace == NULL || workspace->graph_view == NULL)
{
return FALSE;
}
return investigation_graph_view_select_entity(
workspace->graph_view,
entity_identifier
);
}
void workspace_set_graph_loading( void workspace_set_graph_loading(
Workspace *workspace Workspace *workspace
) )