diff --git a/README.md b/README.md index 99a9dde..dff85b1 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,7 @@ Le socle actuel comprend notamment : - fenêtre principale GTK4 ; - barre latérale et espace de travail ; - navigation et recherche locale des entités dans la barre latérale ; +- navigation et recherche locale des relations dans la barre latérale ; - affichage graphique des erreurs ; - tâches asynchrones annulables ; - gestionnaire de tâches ; diff --git a/include/widgets/investigation_graph_view.h b/include/widgets/investigation_graph_view.h index 3e9a742..43e41c2 100644 --- a/include/widgets/investigation_graph_view.h +++ b/include/widgets/investigation_graph_view.h @@ -198,6 +198,19 @@ gboolean investigation_graph_view_select_entity( const char *entity_identifier ); +/** + * @brief Sélectionne une relation du graphe par son UUID. + * + * @param graph_view Vue graphique à modifier. + * @param relation_identifier UUID de la relation. + * + * @return TRUE lorsque la relation est trouvée et sélectionnée. + */ +gboolean investigation_graph_view_select_relation( + InvestigationGraphView *graph_view, + const char *relation_identifier +); + /** * @brief Supprime la sélection courante. * diff --git a/include/widgets/sidebar.h b/include/widgets/sidebar.h index 4a6d062..ab1e07f 100644 --- a/include/widgets/sidebar.h +++ b/include/widgets/sidebar.h @@ -49,6 +49,17 @@ typedef void (*SidebarEntitySelectionCallback)( gpointer user_data ); +/** + * @brief Callback appelé lorsqu'une relation est sélectionnée. + * + * @param relation_identifier UUID emprunté de la relation, ou NULL. + * @param user_data Données privées fournies par l'appelant. + */ +typedef void (*SidebarRelationSelectionCallback)( + const char *relation_identifier, + gpointer user_data +); + /** * @brief Crée un nouveau panneau de navigation latéral. * @@ -129,14 +140,14 @@ void sidebar_set_evidence_selection_callback( ); /** - * @brief Installe les entités du graphe dans la barre latérale. + * @brief Installe les entités et relations 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( +void sidebar_set_graph_model( Sidebar *sidebar, const InvestigationGraphModel *graph_model ); @@ -154,6 +165,19 @@ void sidebar_set_entity_selection_callback( gpointer user_data ); +/** + * @brief Définit le callback de sélection d'une relation. + * + * @param sidebar Barre latérale à configurer. + * @param callback Callback facultatif. + * @param user_data Données privées transmises au callback. + */ +void sidebar_set_relation_selection_callback( + Sidebar *sidebar, + SidebarRelationSelectionCallback callback, + gpointer user_data +); + /** * @brief Libère la structure d'encapsulation du panneau latéral. * diff --git a/include/widgets/workspace.h b/include/widgets/workspace.h index 11f08b2..8555183 100644 --- a/include/widgets/workspace.h +++ b/include/widgets/workspace.h @@ -142,6 +142,19 @@ gboolean workspace_select_graph_entity( const char *entity_identifier ); +/** + * @brief Sélectionne une relation dans le graphe affiché. + * + * @param workspace Zone de travail. + * @param relation_identifier UUID de la relation à sélectionner. + * + * @return TRUE lorsque la relation est trouvée et sélectionnée. + */ +gboolean workspace_select_graph_relation( + Workspace *workspace, + const char *relation_identifier +); + /** * @brief Définit le callback de vérification de la preuve affichée. * diff --git a/src/views/main_window.c b/src/views/main_window.c index d11369f..f65d69f 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -148,6 +148,29 @@ static void main_window_on_sidebar_entity_selected( ); } +/** + * @brief Ouvre dans le workspace la relation choisie dans la sidebar. + */ +static void main_window_on_sidebar_relation_selected( + const char *relation_identifier, + gpointer user_data +) +{ + MainWindow *main_window = user_data; + + if (main_window == NULL || + relation_identifier == NULL || + relation_identifier[0] == '\0') + { + return; + } + + workspace_select_graph_relation( + main_window->workspace, + relation_identifier + ); +} + /** * @brief Transmet la demande de création d'une enquête au contrôleur. * @@ -692,6 +715,12 @@ MainWindow *main_window_new( main_window ); + sidebar_set_relation_selection_callback( + main_window->sidebar, + main_window_on_sidebar_relation_selected, + main_window + ); + workspace_set_verify_evidence_callback( main_window->workspace, main_window_on_verify_evidence_requested, @@ -1069,7 +1098,7 @@ void main_window_set_graph_loading( FALSE ); - sidebar_set_entity_model( + sidebar_set_graph_model( main_window->sidebar, NULL ); @@ -1096,7 +1125,7 @@ void main_window_set_graph( graph_layout ); - sidebar_set_entity_model( + sidebar_set_graph_model( main_window->sidebar, graph_model ); @@ -1122,7 +1151,7 @@ void main_window_set_graph_error( FALSE ); - sidebar_set_entity_model( + sidebar_set_graph_model( main_window->sidebar, NULL ); @@ -1147,7 +1176,7 @@ void main_window_clear_graph( FALSE ); - sidebar_set_entity_model( + sidebar_set_graph_model( main_window->sidebar, NULL ); @@ -1492,7 +1521,13 @@ void main_window_free( NULL ); - sidebar_set_entity_model( + sidebar_set_relation_selection_callback( + main_window->sidebar, + NULL, + NULL + ); + + sidebar_set_graph_model( main_window->sidebar, NULL ); diff --git a/src/widgets/investigation_graph_view.c b/src/widgets/investigation_graph_view.c index 3157c6c..bebc64f 100644 --- a/src/widgets/investigation_graph_view.c +++ b/src/widgets/investigation_graph_view.c @@ -4291,6 +4291,35 @@ gboolean investigation_graph_view_select_entity( return TRUE; } +gboolean investigation_graph_view_select_relation( + InvestigationGraphView *graph_view, + const char *relation_identifier +) +{ + InvestigationGraphNodeLayout *node_layout = NULL; + + if (graph_view == NULL || + graph_view->node_layouts_by_identifier == NULL || + relation_identifier == NULL || + !g_uuid_string_is_valid(relation_identifier)) + { + return FALSE; + } + + node_layout = g_hash_table_lookup( + graph_view->node_layouts_by_identifier, + relation_identifier + ); + if (node_layout == NULL || + node_layout->kind != INVESTIGATION_GRAPH_NODE_KIND_RELATION) + { + return FALSE; + } + + investigation_graph_view_set_selected_node(graph_view, node_layout); + return TRUE; +} + void investigation_graph_view_clear_selection( InvestigationGraphView *graph_view ) diff --git a/src/widgets/sidebar.c b/src/widgets/sidebar.c index ed06a87..d911ca0 100644 --- a/src/widgets/sidebar.c +++ b/src/widgets/sidebar.c @@ -34,6 +34,12 @@ #define SIDEBAR_PAGE_ENTITY \ "entity" +/** + * @brief Identifiant de la page contenant les relations. + */ +#define SIDEBAR_PAGE_RELATION \ + "relation" + /** * @brief État vide de l'onglet Preuves. */ @@ -77,6 +83,14 @@ struct Sidebar GtkWidget *entity_empty_label; GtkWidget *entity_no_result_label; + GtkWidget *relation_page; + GtkWidget *relation_search_entry; + GtkWidget *relation_count_label; + GtkWidget *relation_scrolled_window; + GtkWidget *relation_list_box; + GtkWidget *relation_empty_label; + GtkWidget *relation_no_result_label; + InvestigationTreeView *tree_view; EvidenceTreeView *evidence_tree_view; @@ -89,8 +103,140 @@ struct Sidebar entity_selection_callback; gpointer entity_selection_user_data; + + SidebarRelationSelectionCallback + relation_selection_callback; + + gpointer relation_selection_user_data; + + const InvestigationGraphModel *graph_model; }; +/** + * @brief Vérifie si un texte contient une recherche sans distinction de casse. + */ +static gboolean sidebar_text_contains_search( + const char *text, + const char *search_text +) +{ + char *normalized_text = NULL; + char *normalized_search = NULL; + gboolean matches = FALSE; + + if (text == NULL || search_text == NULL) + { + return FALSE; + } + + normalized_text = g_utf8_casefold(text, -1); + normalized_search = g_utf8_casefold(search_text, -1); + matches = normalized_text != NULL && normalized_search != NULL && + strstr(normalized_text, normalized_search) != NULL; + g_free(normalized_search); + g_free(normalized_text); + + return matches; +} + +/** + * @brief Filtre une ligne de relation avec le texte courant. + */ +static gboolean sidebar_filter_relation_row( + GtkListBoxRow *row, + gpointer user_data +) +{ + Sidebar *sidebar = user_data; + const RelationRecord *relation_record = NULL; + const EntityRecord *source_entity = NULL; + const EntityRecord *target_entity = NULL; + const char *search_text = NULL; + + if (sidebar == NULL || row == NULL) + { + return FALSE; + } + + search_text = gtk_editable_get_text( + GTK_EDITABLE(sidebar->relation_search_entry) + ); + if (search_text == NULL || search_text[0] == '\0') + { + return TRUE; + } + + relation_record = g_object_get_data(G_OBJECT(row), "relation-record"); + if (relation_record == NULL || sidebar->graph_model == NULL) + { + return FALSE; + } + + source_entity = investigation_graph_model_find_entity( + sidebar->graph_model, + relation_record_get_source_entity_identifier(relation_record) + ); + target_entity = investigation_graph_model_find_entity( + sidebar->graph_model, + relation_record_get_target_entity_identifier(relation_record) + ); + + return sidebar_text_contains_search( + relation_record_get_label(relation_record), search_text) || + sidebar_text_contains_search( + relation_record_get_relation_type(relation_record), search_text) || + sidebar_text_contains_search( + entity_record_get_value(source_entity), search_text) || + sidebar_text_contains_search( + entity_record_get_value(target_entity), search_text); +} + +/** + * @brief Réapplique le filtre local des relations. + */ +static void sidebar_on_relation_search_changed( + GtkSearchEntry *search_entry, + gpointer user_data +) +{ + Sidebar *sidebar = user_data; + (void) search_entry; + + if (sidebar != NULL && sidebar->relation_list_box != NULL) + { + gtk_list_box_invalidate_filter(GTK_LIST_BOX(sidebar->relation_list_box)); + } +} + +/** + * @brief Relaie la sélection provenant de la liste des relations. + */ +static void sidebar_on_relation_row_selected( + GtkListBox *list_box, + GtkListBoxRow *row, + gpointer user_data +) +{ + Sidebar *sidebar = user_data; + const RelationRecord *relation_record = NULL; + (void) list_box; + + if (sidebar == NULL || sidebar->relation_selection_callback == NULL) + { + return; + } + + if (row != NULL) + { + relation_record = g_object_get_data(G_OBJECT(row), "relation-record"); + } + + sidebar->relation_selection_callback( + relation_record_get_identifier(relation_record), + sidebar->relation_selection_user_data + ); +} + /** * @brief Filtre une ligne d'entité avec le texte de recherche courant. */ @@ -900,6 +1046,92 @@ Sidebar *sidebar_new(void) "Entités" ); + /* + * Page Relations : recherche locale et liste verticale défilable. + */ + sidebar->relation_page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + sidebar->relation_search_entry = gtk_search_entry_new(); + sidebar->relation_count_label = gtk_label_new("0 relation"); + sidebar->relation_scrolled_window = gtk_scrolled_window_new(); + sidebar->relation_list_box = gtk_list_box_new(); + sidebar->relation_empty_label = gtk_label_new( + "Aucune relation active dans cette enquête." + ); + sidebar->relation_no_result_label = gtk_label_new( + "Aucune relation ne correspond à la recherche." + ); + + if (sidebar->relation_page == NULL || + sidebar->relation_search_entry == NULL || + sidebar->relation_count_label == NULL || + sidebar->relation_scrolled_window == NULL || + sidebar->relation_list_box == NULL || + sidebar->relation_empty_label == NULL || + sidebar->relation_no_result_label == NULL) + { + sidebar_free(sidebar); + return NULL; + } + + gtk_widget_set_margin_start(sidebar->relation_search_entry, 8); + gtk_widget_set_margin_end(sidebar->relation_search_entry, 8); + gtk_search_entry_set_placeholder_text( + GTK_SEARCH_ENTRY(sidebar->relation_search_entry), + "Rechercher une relation" + ); + gtk_widget_set_halign(sidebar->relation_count_label, GTK_ALIGN_START); + gtk_widget_set_margin_start(sidebar->relation_count_label, 12); + gtk_widget_add_css_class(sidebar->relation_count_label, "dim-label"); + gtk_widget_set_hexpand(sidebar->relation_scrolled_window, TRUE); + gtk_widget_set_vexpand(sidebar->relation_scrolled_window, TRUE); + gtk_scrolled_window_set_policy( + GTK_SCROLLED_WINDOW(sidebar->relation_scrolled_window), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC + ); + gtk_scrolled_window_set_child( + GTK_SCROLLED_WINDOW(sidebar->relation_scrolled_window), + sidebar->relation_list_box + ); + gtk_list_box_set_filter_func( + GTK_LIST_BOX(sidebar->relation_list_box), + sidebar_filter_relation_row, + sidebar, + NULL + ); + gtk_label_set_wrap(GTK_LABEL(sidebar->relation_no_result_label), TRUE); + gtk_widget_set_margin_top(sidebar->relation_no_result_label, 20); + gtk_list_box_set_placeholder( + GTK_LIST_BOX(sidebar->relation_list_box), + sidebar->relation_no_result_label + ); + gtk_label_set_wrap(GTK_LABEL(sidebar->relation_empty_label), TRUE); + gtk_widget_set_halign(sidebar->relation_empty_label, GTK_ALIGN_CENTER); + gtk_widget_set_valign(sidebar->relation_empty_label, GTK_ALIGN_CENTER); + gtk_widget_set_vexpand(sidebar->relation_empty_label, TRUE); + g_signal_connect( + sidebar->relation_search_entry, + "search-changed", + G_CALLBACK(sidebar_on_relation_search_changed), + sidebar + ); + g_signal_connect( + sidebar->relation_list_box, + "row-selected", + G_CALLBACK(sidebar_on_relation_row_selected), + sidebar + ); + gtk_box_append(GTK_BOX(sidebar->relation_page), sidebar->relation_search_entry); + gtk_box_append(GTK_BOX(sidebar->relation_page), sidebar->relation_count_label); + gtk_box_append(GTK_BOX(sidebar->relation_page), sidebar->relation_scrolled_window); + gtk_box_append(GTK_BOX(sidebar->relation_page), sidebar->relation_empty_label); + gtk_stack_add_titled( + GTK_STACK(sidebar->stack), + sidebar->relation_page, + SIDEBAR_PAGE_RELATION, + "Relations" + ); + /* * L'onglet Dossier reste sélectionné au démarrage. */ @@ -1066,14 +1298,16 @@ void sidebar_set_evidence_selection_callback( user_data; } -void sidebar_set_entity_model( +void sidebar_set_graph_model( Sidebar *sidebar, const InvestigationGraphModel *graph_model ) { GPtrArray *entity_records = NULL; + GPtrArray *relation_records = NULL; GtkWidget *child = NULL; guint entity_index = 0; + guint relation_index = 0; char *count_text = NULL; if (sidebar == NULL || sidebar->entity_list_box == NULL) @@ -1081,6 +1315,8 @@ void sidebar_set_entity_model( return; } + sidebar->graph_model = graph_model; + while ((child = gtk_widget_get_first_child( sidebar->entity_list_box )) != NULL) @@ -1168,7 +1404,105 @@ void sidebar_set_entity_model( ); g_free(count_text); + count_text = NULL; g_clear_pointer(&entity_records, g_ptr_array_unref); + + while ((child = gtk_widget_get_first_child( + sidebar->relation_list_box + )) != NULL) + { + gtk_list_box_remove(GTK_LIST_BOX(sidebar->relation_list_box), child); + } + + if (graph_model != NULL) + { + relation_records = investigation_graph_model_list_relations( + graph_model, + NULL + ); + } + + for (relation_index = 0; + relation_records != NULL && relation_index < relation_records->len; + relation_index++) + { + const RelationRecord *relation_record = g_ptr_array_index( + relation_records, + relation_index + ); + const EntityRecord *source_entity = investigation_graph_model_find_entity( + graph_model, + relation_record_get_source_entity_identifier(relation_record) + ); + const EntityRecord *target_entity = investigation_graph_model_find_entity( + graph_model, + relation_record_get_target_entity_identifier(relation_record) + ); + const char *label = relation_record_get_label(relation_record); + char *details = g_strdup_printf( + "%s → %s · %d %%", + entity_record_get_value(source_entity), + entity_record_get_value(target_entity), + relation_record_get_confidence(relation_record) + ); + GtkWidget *row = gtk_list_box_row_new(); + GtkWidget *content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); + GtkWidget *title_label = gtk_label_new( + label != NULL && label[0] != '\0' + ? label + : relation_record_get_relation_type(relation_record) + ); + GtkWidget *details_label = gtk_label_new(details); + + g_free(details); + if (row == NULL || content == NULL || + title_label == NULL || details_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(title_label), 0.0F); + gtk_label_set_ellipsize(GTK_LABEL(title_label), PANGO_ELLIPSIZE_END); + gtk_label_set_xalign(GTK_LABEL(details_label), 0.0F); + gtk_label_set_ellipsize(GTK_LABEL(details_label), PANGO_ELLIPSIZE_END); + gtk_widget_add_css_class(details_label, "dim-label"); + gtk_box_append(GTK_BOX(content), title_label); + gtk_box_append(GTK_BOX(content), details_label); + gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), content); + g_object_set_data( + G_OBJECT(row), + "relation-record", + (gpointer) relation_record + ); + gtk_list_box_append(GTK_LIST_BOX(sidebar->relation_list_box), row); + } + + count_text = g_strdup_printf( + "%u %s", + relation_records != NULL ? relation_records->len : 0U, + relation_records != NULL && relation_records->len > 1U + ? "relations" + : "relation" + ); + gtk_label_set_text( + GTK_LABEL(sidebar->relation_count_label), + count_text != NULL ? count_text : "0 relation" + ); + gtk_widget_set_visible( + sidebar->relation_empty_label, + relation_records == NULL || relation_records->len == 0U + ); + gtk_widget_set_visible( + sidebar->relation_scrolled_window, + relation_records != NULL && relation_records->len > 0U + ); + + g_free(count_text); + g_clear_pointer(&relation_records, g_ptr_array_unref); } void sidebar_set_entity_selection_callback( @@ -1186,6 +1520,21 @@ void sidebar_set_entity_selection_callback( sidebar->entity_selection_user_data = user_data; } +void sidebar_set_relation_selection_callback( + Sidebar *sidebar, + SidebarRelationSelectionCallback callback, + gpointer user_data +) +{ + if (sidebar == NULL) + { + return; + } + + sidebar->relation_selection_callback = callback; + sidebar->relation_selection_user_data = user_data; +} + void sidebar_free(Sidebar *sidebar) { if (sidebar == NULL) @@ -1210,6 +1559,9 @@ void sidebar_free(Sidebar *sidebar) sidebar->entity_selection_callback = NULL; sidebar->entity_selection_user_data = NULL; + sidebar->relation_selection_callback = NULL; + sidebar->relation_selection_user_data = NULL; + sidebar->graph_model = NULL; /* * Les widgets GTK sont intégrés dans l'arbre de widgets de la fenêtre. diff --git a/src/widgets/workspace.c b/src/widgets/workspace.c index 8aa22ac..d4b0f17 100644 --- a/src/widgets/workspace.c +++ b/src/widgets/workspace.c @@ -6,6 +6,7 @@ #include "widgets/workspace.h" #include "models/entity_record.h" +#include "models/investigation_graph_model.h" #include "models/relation_record.h" #include "widgets/entity_details_panel.h" #include "widgets/investigation_graph_view.h" @@ -60,6 +61,9 @@ struct Workspace InvestigationGraphView *graph_view; EntityDetailsPanel *entity_details_panel; + GtkWidget *relation_details_panel; + GtkWidget *relation_details_title_label; + GtkWidget *relation_details_summary_label; GtkWidget *node_page; @@ -442,6 +446,49 @@ static void workspace_on_graph_node_selected( ); } + if (workspace->relation_details_panel != NULL) + { + gtk_widget_set_visible( + workspace->relation_details_panel, + relation_record != NULL + ); + } + + if (relation_record != NULL && + workspace->relation_details_title_label != NULL && + workspace->relation_details_summary_label != NULL) + { + const EntityRecord *source_entity = + investigation_graph_model_find_entity( + workspace->graph_model, + relation_record_get_source_entity_identifier(relation_record) + ); + const EntityRecord *target_entity = + investigation_graph_model_find_entity( + workspace->graph_model, + relation_record_get_target_entity_identifier(relation_record) + ); + const char *label = relation_record_get_label(relation_record); + char *summary = g_strdup_printf( + "%s → %s\n\nType : %s\nConfiance : %d %%\nUUID : %s", + entity_record_get_value(source_entity), + entity_record_get_value(target_entity), + relation_record_get_relation_type(relation_record), + relation_record_get_confidence(relation_record), + relation_record_get_identifier(relation_record) + ); + + gtk_label_set_text( + GTK_LABEL(workspace->relation_details_title_label), + label != NULL && label[0] != '\0' ? label : "Relation" + ); + gtk_label_set_text( + GTK_LABEL(workspace->relation_details_summary_label), + summary != NULL ? summary : "Informations indisponibles" + ); + g_free(summary); + } + workspace_update_osint_tools_menu( workspace, entity_record, @@ -1199,8 +1246,18 @@ Workspace *workspace_new(void) workspace->entity_details_panel = entity_details_panel_new(); + workspace->relation_details_panel = gtk_box_new( + GTK_ORIENTATION_VERTICAL, + 12 + ); + workspace->relation_details_title_label = gtk_label_new(NULL); + workspace->relation_details_summary_label = gtk_label_new(NULL); + if (workspace->graph_view == NULL || - workspace->entity_details_panel == NULL) + workspace->entity_details_panel == NULL || + workspace->relation_details_panel == NULL || + workspace->relation_details_title_label == NULL || + workspace->relation_details_summary_label == NULL) { workspace_free( workspace @@ -1209,6 +1266,50 @@ Workspace *workspace_new(void) return NULL; } + gtk_widget_set_size_request(workspace->relation_details_panel, 300, -1); + gtk_widget_set_halign(workspace->relation_details_panel, GTK_ALIGN_END); + gtk_widget_set_valign(workspace->relation_details_panel, GTK_ALIGN_START); + gtk_widget_set_margin_end(workspace->relation_details_panel, 12); + gtk_widget_set_margin_top(workspace->relation_details_panel, 56); + gtk_widget_set_margin_start(workspace->relation_details_panel, 12); + gtk_widget_set_margin_bottom(workspace->relation_details_panel, 12); + gtk_widget_add_css_class(workspace->relation_details_panel, "card"); + gtk_widget_set_margin_start(workspace->relation_details_title_label, 16); + gtk_widget_set_margin_end(workspace->relation_details_title_label, 16); + gtk_widget_set_margin_top(workspace->relation_details_title_label, 16); + gtk_label_set_xalign( + GTK_LABEL(workspace->relation_details_title_label), + 0.0F + ); + gtk_widget_add_css_class( + workspace->relation_details_title_label, + "title-3" + ); + gtk_widget_set_margin_start(workspace->relation_details_summary_label, 16); + gtk_widget_set_margin_end(workspace->relation_details_summary_label, 16); + gtk_widget_set_margin_bottom(workspace->relation_details_summary_label, 16); + gtk_label_set_xalign( + GTK_LABEL(workspace->relation_details_summary_label), + 0.0F + ); + gtk_label_set_wrap( + GTK_LABEL(workspace->relation_details_summary_label), + TRUE + ); + gtk_label_set_selectable( + GTK_LABEL(workspace->relation_details_summary_label), + TRUE + ); + gtk_box_append( + GTK_BOX(workspace->relation_details_panel), + workspace->relation_details_title_label + ); + gtk_box_append( + GTK_BOX(workspace->relation_details_panel), + workspace->relation_details_summary_label + ); + gtk_widget_set_visible(workspace->relation_details_panel, FALSE); + investigation_graph_view_set_selection_callback( workspace->graph_view, workspace_on_graph_node_selected, @@ -1667,6 +1768,17 @@ Workspace *workspace_new(void) ) ); + gtk_overlay_add_overlay( + GTK_OVERLAY(workspace->graph_view_page), + workspace->relation_details_panel + ); + + gtk_overlay_set_clip_overlay( + GTK_OVERLAY(workspace->graph_view_page), + workspace->relation_details_panel, + TRUE + ); + gtk_overlay_set_clip_overlay( GTK_OVERLAY( workspace->graph_view_page @@ -2061,6 +2173,22 @@ gboolean workspace_select_graph_entity( ); } +gboolean workspace_select_graph_relation( + Workspace *workspace, + const char *relation_identifier +) +{ + if (workspace == NULL || workspace->graph_view == NULL) + { + return FALSE; + } + + return investigation_graph_view_select_relation( + workspace->graph_view, + relation_identifier + ); +} + void workspace_set_graph_loading( Workspace *workspace )