/****************************************************************************** * @file workspace.c * @brief Implémentation de la zone principale de travail. ******************************************************************************/ #include "widgets/workspace.h" #include "models/entity_record.h" #include "models/investigation_graph_model.h" #include "models/osint_selection_context.h" #include "models/osint_action_catalog.h" #include "models/relation_record.h" #include "widgets/entity_details_panel.h" #include "widgets/investigation_graph_view.h" #include #define WORKSPACE_PAGE_WELCOME "welcome" #define WORKSPACE_PAGE_NODE_INFORMATION "node-information" #define WORKSPACE_PAGE_EVIDENCE_INFORMATION \ "evidence-information" #define WORKSPACE_PAGE_GRAPH_STATE \ "graph-state" #define WORKSPACE_PAGE_GRAPH_VIEW \ "graph-view" /** * @brief État métier représenté par la page du graphe. */ typedef enum { WORKSPACE_GRAPH_STATE_EMPTY, WORKSPACE_GRAPH_STATE_LOADING, WORKSPACE_GRAPH_STATE_READY, WORKSPACE_GRAPH_STATE_ERROR } WorkspaceGraphState; /** * @struct Workspace * @brief Représentation interne de la zone de travail. */ struct Workspace { GtkWidget *root_widget; GtkWidget *stack; GtkWidget *welcome_page; GtkWidget *graph_state_page; GtkWidget *graph_spinner; GtkWidget *graph_title_label; GtkWidget *graph_details_label; GtkWidget *graph_view_page; GtkWidget *graph_toolbar; GtkWidget *reset_graph_layout_button; GtkWidget *osint_tools_button; GtkWidget *osint_tools_popover; GtkWidget *osint_tools_context_label; GtkWidget *osint_tools_status_label; GtkWidget *osint_tools_actions_box; GtkWidget *graph_help_button; GtkWidget *graph_help_popover; InvestigationGraphView *graph_view; EntityDetailsPanel *entity_details_panel; GtkWidget *relation_details_panel; GtkWidget *relation_details_title_label; GtkWidget *relation_details_summary_label; GtkWidget *node_page; GtkWidget *evidence_page; GtkWidget *evidence_name_label; GtkWidget *evidence_type_label; GtkWidget *evidence_integrity_label; GtkWidget *evidence_size_label; GtkWidget *evidence_imported_at_label; GtkWidget *evidence_collected_at_label; GtkWidget *evidence_source_label; GtkWidget *evidence_description_label; GtkWidget *evidence_relative_path_label; GtkWidget *evidence_internal_name_label; GtkWidget *evidence_identifier_label; GtkWidget *evidence_sha256_label; GtkWidget *verify_evidence_button; char *selected_evidence_identifier; WorkspaceVerifyEvidenceCallback verify_evidence_callback; gpointer verify_evidence_user_data; WorkspaceGraphNodeMovedCallback graph_node_moved_callback; gpointer graph_node_moved_user_data; WorkspaceResetGraphLayoutCallback reset_graph_layout_callback; gpointer reset_graph_layout_user_data; WorkspaceAddRelationCallback add_relation_callback; gpointer add_relation_user_data; WorkspaceOsintActionCallback osint_action_callback; gpointer osint_action_user_data; GtkWidget *node_name_label; GtkWidget *node_path_label; GtkWidget *node_type_label; GtkWidget *node_parent_label; GtkWidget *node_children_label; const InvestigationGraphModel *graph_model; const InvestigationGraphLayout *graph_layout; OsintSelectionContext *osint_selection_context; OsintActionCatalog *osint_action_catalog; WorkspaceGraphState graph_state; }; /** * @brief Traite localement l'aperçu ou relaie une action OSINT exécutable. */ static void workspace_on_osint_action_clicked( GtkButton *button, gpointer user_data ) { Workspace *workspace = user_data; const char *action_identifier = g_object_get_data( G_OBJECT(button), "osint-action-identifier" ); const char *target_value = NULL; const char *target_identifier = NULL; if (workspace == NULL || workspace->osint_tools_status_label == NULL) { return; } if (g_strcmp0(action_identifier, "selection-preview") == 0) { gtk_label_set_text( GTK_LABEL(workspace->osint_tools_status_label), "Aperçu local : aucun outil externe exécuté." ); return; } target_value = osint_selection_context_get_value( workspace->osint_selection_context ); target_identifier = osint_selection_context_get_identifier( workspace->osint_selection_context ); if (workspace->osint_action_callback == NULL || action_identifier == NULL || target_identifier == NULL || target_value == NULL) { gtk_label_set_text( GTK_LABEL(workspace->osint_tools_status_label), "Impossible de transmettre cette action OSINT." ); return; } gtk_label_set_text( GTK_LABEL(workspace->osint_tools_status_label), "Exécution lancée ; suivez sa progression dans les tâches." ); gtk_popover_popdown( GTK_POPOVER(workspace->osint_tools_popover) ); workspace->osint_action_callback( action_identifier, target_identifier, target_value, workspace->osint_action_user_data ); } /** * @brief Actualise le menu OSINT selon la sélection du graphe. * * Aucun outil n'est encore exposé par ce ticket. Le menu prépare le point * d'entrée contextuel et explique explicitement cet état à l'utilisateur. */ static void workspace_update_osint_tools_menu( Workspace *workspace, const OsintSelectionContext *context ) { const char *selection_kind = NULL; const char *selection_identifier = NULL; char *context_text = NULL; GPtrArray *compatible_actions = NULL; GtkWidget *child = NULL; if (workspace == NULL || workspace->osint_tools_button == NULL || workspace->osint_tools_context_label == NULL || workspace->osint_tools_status_label == NULL || workspace->osint_tools_actions_box == NULL) { return; } while ((child = gtk_widget_get_first_child( workspace->osint_tools_actions_box )) != NULL) { gtk_box_remove(GTK_BOX(workspace->osint_tools_actions_box), child); } if (osint_selection_context_get_kind(context) == OSINT_SELECTION_CONTEXT_KIND_ENTITY) { selection_kind = "Entité"; selection_identifier = osint_selection_context_get_identifier(context); } else if (osint_selection_context_get_kind(context) == OSINT_SELECTION_CONTEXT_KIND_RELATION) { selection_kind = "Relation"; selection_identifier = osint_selection_context_get_identifier(context); } gtk_widget_set_sensitive( workspace->osint_tools_button, selection_identifier != NULL && selection_identifier[0] != '\0' ); if (selection_identifier == NULL || selection_identifier[0] == '\0') { gtk_label_set_text( GTK_LABEL( workspace->osint_tools_context_label ), "Sélectionnez un nœud du graphe." ); gtk_label_set_text( GTK_LABEL( workspace->osint_tools_status_label ), "Les outils compatibles apparaîtront ici." ); return; } compatible_actions = osint_action_catalog_list_compatible( workspace->osint_action_catalog, context ); for (guint action_index = 0; compatible_actions != NULL && action_index < compatible_actions->len; action_index++) { const OsintAction *action = g_ptr_array_index( compatible_actions, action_index ); const char *tool_version = osint_action_get_tool_version(action); char *button_label = tool_version != NULL && tool_version[0] != '\0' ? g_strdup_printf("%s — %s", osint_action_get_label(action), tool_version) : g_strdup(osint_action_get_label(action)); GtkWidget *action_button = gtk_button_new_with_label(button_label); g_free(button_label); if (action_button == NULL) { continue; } gtk_widget_set_sensitive( action_button, osint_action_is_available(action) ); gtk_widget_set_tooltip_text( action_button, osint_action_is_available(action) ? osint_action_get_description(action) : osint_action_get_unavailable_reason(action) ); g_object_set_data_full( G_OBJECT(action_button), "osint-action-identifier", g_strdup(osint_action_get_identifier(action)), g_free ); g_signal_connect( action_button, "clicked", G_CALLBACK(workspace_on_osint_action_clicked), workspace ); gtk_box_append( GTK_BOX(workspace->osint_tools_actions_box), action_button ); } context_text = g_strdup_printf( "%s sélectionnée\n%s", selection_kind, selection_identifier ); gtk_label_set_text( GTK_LABEL( workspace->osint_tools_context_label ), context_text != NULL ? context_text : selection_kind ); gtk_label_set_text( GTK_LABEL( workspace->osint_tools_status_label ), compatible_actions != NULL && compatible_actions->len > 0U ? "Actions compatibles avec la sélection." : "Aucune action OSINT compatible." ); g_free( context_text ); g_clear_pointer(&compatible_actions, g_ptr_array_unref); } /** * @brief Ajoute une ligne de métadonnée dans la grille. * * @return Label GTK destiné à recevoir la valeur. */ static GtkWidget *workspace_add_evidence_field( GtkGrid *grid, gint row, const char *title ) { GtkWidget *title_label = NULL; GtkWidget *value_label = NULL; if (grid == NULL || title == NULL || title[0] == '\0') { return NULL; } title_label = gtk_label_new( title ); value_label = gtk_label_new( NULL ); if (title_label == NULL || value_label == NULL) { g_clear_object( &value_label ); g_clear_object( &title_label ); return NULL; } gtk_label_set_xalign( GTK_LABEL(title_label), 0.0F ); gtk_widget_set_valign( title_label, GTK_ALIGN_START ); gtk_widget_add_css_class( title_label, "dim-label" ); gtk_label_set_xalign( GTK_LABEL(value_label), 0.0F ); gtk_label_set_wrap( GTK_LABEL(value_label), TRUE ); gtk_label_set_wrap_mode( GTK_LABEL(value_label), PANGO_WRAP_WORD_CHAR ); gtk_label_set_selectable( GTK_LABEL(value_label), TRUE ); gtk_widget_set_hexpand( value_label, TRUE ); gtk_widget_set_halign( value_label, GTK_ALIGN_FILL ); gtk_widget_set_valign( value_label, GTK_ALIGN_START ); gtk_grid_attach( grid, title_label, 0, row, 1, 1 ); gtk_grid_attach( grid, value_label, 1, row, 1, 1 ); return value_label; } /** * @brief Définit une valeur avec un texte de remplacement. */ static void workspace_set_field_text( GtkWidget *label, const char *value, const char *fallback ) { if (label == NULL) { return; } gtk_label_set_text( GTK_LABEL(label), value != NULL && value[0] != '\0' ? value : fallback ); } /** * @brief Convertit un statut d'intégrité en texte utilisateur. */ static const char *workspace_get_integrity_status_text( EvidenceIntegrityStatus integrity_status ) { switch (integrity_status) { case EVIDENCE_INTEGRITY_STATUS_UNKNOWN: return "Non vérifiée"; case EVIDENCE_INTEGRITY_STATUS_VALID: return "Valide"; case EVIDENCE_INTEGRITY_STATUS_MISSING: return "Fichier absent"; case EVIDENCE_INTEGRITY_STATUS_MODIFIED: return "Fichier modifié"; case EVIDENCE_INTEGRITY_STATUS_ERROR: return "Erreur de vérification"; default: return "Statut inconnu"; } } /** * @brief Transmet la sélection du graphe au volet de détails. */ static void workspace_on_graph_node_moved( const char *entity_identifier, double x, double y, gpointer user_data ) { Workspace *workspace = user_data; if (workspace == NULL || workspace->graph_node_moved_callback == NULL || entity_identifier == NULL || entity_identifier[0] == '\0') { return; } workspace->graph_node_moved_callback( entity_identifier, x, y, workspace->graph_node_moved_user_data ); } /** * @brief Transmet la sélection du graphe au volet de détails. */ static void workspace_on_graph_node_selected( const EntityRecord *entity_record, const RelationRecord *relation_record, gpointer user_data ) { Workspace *workspace = user_data; if (workspace == NULL) { return; } g_clear_pointer( &workspace->osint_selection_context, osint_selection_context_free ); if (entity_record != NULL) { workspace->osint_selection_context = osint_selection_context_new_entity(entity_record, NULL); } if (workspace->entity_details_panel != NULL) { entity_details_panel_set_entity( workspace->entity_details_panel, entity_record ); } 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) ); workspace->osint_selection_context = osint_selection_context_new_relation( relation_record, source_entity, target_entity, NULL ); 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, workspace->osint_selection_context ); } /** * @brief Relaie la demande d'ajout d'une relation. */ static void workspace_on_add_relation_requested( const char *source_entity_identifier, gpointer user_data ) { Workspace *workspace = user_data; if (workspace == NULL || workspace->add_relation_callback == NULL || source_entity_identifier == NULL || !g_uuid_string_is_valid( source_entity_identifier )) { return; } workspace->add_relation_callback( source_entity_identifier, workspace->add_relation_user_data ); } /** * @brief Désélectionne le nœud lorsque le volet est fermé manuellement. */ static void workspace_on_entity_details_closed( gpointer user_data ) { Workspace *workspace = user_data; if (workspace == NULL || workspace->graph_view == NULL) { return; } investigation_graph_view_clear_selection( workspace->graph_view ); } /** * @brief Affiche l'état principal courant du Workspace. * * Sans enquête, la page d'accueil est utilisée. Le chargement et les erreurs * utilisent la page d'état. Un graphe prêt utilise la vue graphique. */ static void workspace_show_default( Workspace *workspace ) { if (workspace == NULL) { return; } switch (workspace->graph_state) { case WORKSPACE_GRAPH_STATE_EMPTY: gtk_stack_set_visible_child_name( GTK_STACK(workspace->stack), WORKSPACE_PAGE_WELCOME ); return; case WORKSPACE_GRAPH_STATE_READY: gtk_stack_set_visible_child_name( GTK_STACK(workspace->stack), WORKSPACE_PAGE_GRAPH_VIEW ); return; case WORKSPACE_GRAPH_STATE_LOADING: case WORKSPACE_GRAPH_STATE_ERROR: default: gtk_stack_set_visible_child_name( GTK_STACK(workspace->stack), WORKSPACE_PAGE_GRAPH_STATE ); return; } } /** * @brief Transmet la demande de vérification de la preuve affichée. */ static void workspace_on_reset_graph_layout_clicked( GtkButton *button, gpointer user_data ) { Workspace *workspace = user_data; (void) button; if (workspace == NULL || workspace->graph_state != WORKSPACE_GRAPH_STATE_READY || workspace->graph_model == NULL || workspace->reset_graph_layout_callback == NULL) { return; } workspace->reset_graph_layout_callback( workspace->reset_graph_layout_user_data ); } /** * @brief Transmet la demande de vérification de la preuve affichée. */ static void workspace_on_verify_evidence_clicked( GtkButton *button, gpointer user_data ) { Workspace *workspace = user_data; (void) button; if (workspace == NULL || workspace->verify_evidence_callback == NULL || workspace->selected_evidence_identifier == NULL || workspace->selected_evidence_identifier[0] == '\0') { return; } workspace->verify_evidence_callback( workspace->selected_evidence_identifier, workspace->verify_evidence_user_data ); } Workspace *workspace_new(void) { GtkWidget *evidence_content = NULL; GtkWidget *evidence_separator = NULL; GtkWidget *evidence_grid = NULL; Workspace *workspace = NULL; workspace = g_new0(Workspace, 1); workspace->root_widget = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); if (workspace->root_widget == NULL) { workspace_free(workspace); return NULL; } gtk_widget_set_hexpand( workspace->root_widget, TRUE ); gtk_widget_set_vexpand( workspace->root_widget, TRUE ); workspace->stack = gtk_stack_new(); if (workspace->stack == NULL) { workspace_free(workspace); return NULL; } gtk_widget_set_hexpand( workspace->stack, TRUE ); gtk_widget_set_vexpand( workspace->stack, TRUE ); gtk_box_append( GTK_BOX(workspace->root_widget), workspace->stack ); /* * Page d'accueil. */ workspace->welcome_page = gtk_box_new( GTK_ORIENTATION_VERTICAL, 12 ); if (workspace->welcome_page == NULL) { workspace_free(workspace); return NULL; } gtk_widget_set_halign( workspace->welcome_page, GTK_ALIGN_CENTER ); gtk_widget_set_valign( workspace->welcome_page, GTK_ALIGN_CENTER ); gtk_stack_add_named( GTK_STACK(workspace->stack), workspace->welcome_page, WORKSPACE_PAGE_WELCOME ); /* * Page d'informations sur le nœud. */ workspace->node_page = gtk_box_new( GTK_ORIENTATION_VERTICAL, 12 ); if (workspace->node_page == NULL) { workspace_free(workspace); return NULL; } gtk_widget_set_halign( workspace->node_page, GTK_ALIGN_CENTER ); gtk_widget_set_valign( workspace->node_page, GTK_ALIGN_CENTER ); workspace->node_name_label = gtk_label_new(NULL); workspace->node_path_label = gtk_label_new(NULL); workspace->node_type_label = gtk_label_new(NULL); workspace->node_parent_label = gtk_label_new(NULL); workspace->node_children_label = gtk_label_new(NULL); gtk_box_append( GTK_BOX(workspace->node_page), workspace->node_name_label ); gtk_box_append( GTK_BOX(workspace->node_page), workspace->node_path_label ); gtk_label_set_wrap( GTK_LABEL(workspace->node_path_label), TRUE ); gtk_label_set_selectable( GTK_LABEL(workspace->node_path_label), TRUE ); gtk_widget_set_halign( workspace->node_path_label, GTK_ALIGN_CENTER ); gtk_box_append( GTK_BOX(workspace->node_page), workspace->node_type_label ); gtk_box_append( GTK_BOX(workspace->node_page), workspace->node_parent_label ); gtk_box_append( GTK_BOX(workspace->node_page), workspace->node_children_label ); gtk_stack_add_named( GTK_STACK(workspace->stack), workspace->node_page, WORKSPACE_PAGE_NODE_INFORMATION ); /* * Page détaillée d'une preuve. */ workspace->evidence_page = gtk_scrolled_window_new(); if (workspace->evidence_page == NULL) { workspace_free( workspace ); return NULL; } gtk_widget_set_hexpand( workspace->evidence_page, TRUE ); gtk_widget_set_vexpand( workspace->evidence_page, TRUE ); gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( workspace->evidence_page ), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC ); evidence_content = gtk_box_new( GTK_ORIENTATION_VERTICAL, 16 ); if (evidence_content == NULL) { workspace_free( workspace ); return NULL; } gtk_widget_set_margin_start( evidence_content, 24 ); gtk_widget_set_margin_end( evidence_content, 24 ); gtk_widget_set_margin_top( evidence_content, 24 ); gtk_widget_set_margin_bottom( evidence_content, 24 ); workspace->evidence_name_label = gtk_label_new( NULL ); if (workspace->evidence_name_label == NULL) { workspace_free( workspace ); return NULL; } gtk_label_set_xalign( GTK_LABEL( workspace->evidence_name_label ), 0.0F ); gtk_label_set_wrap( GTK_LABEL( workspace->evidence_name_label ), TRUE ); gtk_label_set_selectable( GTK_LABEL( workspace->evidence_name_label ), TRUE ); gtk_widget_add_css_class( workspace->evidence_name_label, "title-2" ); gtk_box_append( GTK_BOX(evidence_content), workspace->evidence_name_label ); workspace->verify_evidence_button = gtk_button_new_with_label( "Vérifier l'intégrité" ); if (workspace->verify_evidence_button == NULL) { workspace_free( workspace ); return NULL; } gtk_widget_set_halign( workspace->verify_evidence_button, GTK_ALIGN_START ); gtk_widget_set_tooltip_text( workspace->verify_evidence_button, "Recalculer le SHA-256 et le comparer à l'empreinte enregistrée" ); /* * Aucun bouton actif tant qu'aucune preuve n'est affichée. */ gtk_widget_set_sensitive( workspace->verify_evidence_button, FALSE ); g_signal_connect( workspace->verify_evidence_button, "clicked", G_CALLBACK( workspace_on_verify_evidence_clicked ), workspace ); gtk_box_append( GTK_BOX(evidence_content), workspace->verify_evidence_button ); evidence_separator = gtk_separator_new( GTK_ORIENTATION_HORIZONTAL ); gtk_box_append( GTK_BOX(evidence_content), evidence_separator ); evidence_grid = gtk_grid_new(); if (evidence_grid == NULL) { workspace_free( workspace ); return NULL; } gtk_grid_set_column_spacing( GTK_GRID(evidence_grid), 24 ); gtk_grid_set_row_spacing( GTK_GRID(evidence_grid), 12 ); gtk_widget_set_hexpand( evidence_grid, TRUE ); workspace->evidence_type_label = workspace_add_evidence_field( GTK_GRID(evidence_grid), 0, "Type" ); workspace->evidence_integrity_label = workspace_add_evidence_field( GTK_GRID(evidence_grid), 1, "Intégrité" ); workspace->evidence_size_label = workspace_add_evidence_field( GTK_GRID(evidence_grid), 2, "Taille" ); workspace->evidence_imported_at_label = workspace_add_evidence_field( GTK_GRID(evidence_grid), 3, "Importée le" ); workspace->evidence_collected_at_label = workspace_add_evidence_field( GTK_GRID(evidence_grid), 4, "Collectée le" ); workspace->evidence_source_label = workspace_add_evidence_field( GTK_GRID(evidence_grid), 5, "Source" ); workspace->evidence_description_label = workspace_add_evidence_field( GTK_GRID(evidence_grid), 6, "Description" ); workspace->evidence_relative_path_label = workspace_add_evidence_field( GTK_GRID(evidence_grid), 7, "Chemin relatif" ); workspace->evidence_internal_name_label = workspace_add_evidence_field( GTK_GRID(evidence_grid), 8, "Nom interne" ); workspace->evidence_identifier_label = workspace_add_evidence_field( GTK_GRID(evidence_grid), 9, "Identifiant" ); workspace->evidence_sha256_label = workspace_add_evidence_field( GTK_GRID(evidence_grid), 10, "SHA-256" ); if (workspace->evidence_type_label == NULL || workspace->evidence_integrity_label == NULL || workspace->evidence_size_label == NULL || workspace->evidence_imported_at_label == NULL || workspace->evidence_collected_at_label == NULL || workspace->evidence_source_label == NULL || workspace->evidence_description_label == NULL || workspace->evidence_relative_path_label == NULL || workspace->evidence_internal_name_label == NULL || workspace->evidence_identifier_label == NULL || workspace->evidence_sha256_label == NULL) { workspace_free( workspace ); return NULL; } gtk_box_append( GTK_BOX(evidence_content), evidence_grid ); gtk_scrolled_window_set_child( GTK_SCROLLED_WINDOW( workspace->evidence_page ), evidence_content ); gtk_stack_add_named( GTK_STACK(workspace->stack), workspace->evidence_page, WORKSPACE_PAGE_EVIDENCE_INFORMATION ); /* * Page d'état utilisée pendant le chargement ou en cas d'erreur. */ workspace->graph_state_page = gtk_box_new( GTK_ORIENTATION_VERTICAL, 12 ); if (workspace->graph_state_page == NULL) { workspace_free( workspace ); return NULL; } gtk_widget_set_halign( workspace->graph_state_page, GTK_ALIGN_CENTER ); gtk_widget_set_valign( workspace->graph_state_page, GTK_ALIGN_CENTER ); workspace->graph_spinner = gtk_spinner_new(); workspace->graph_title_label = gtk_label_new( NULL ); workspace->graph_details_label = gtk_label_new( NULL ); if (workspace->graph_spinner == NULL || workspace->graph_title_label == NULL || workspace->graph_details_label == NULL) { workspace_free( workspace ); return NULL; } gtk_widget_add_css_class( workspace->graph_title_label, "title-2" ); gtk_label_set_wrap( GTK_LABEL( workspace->graph_details_label ), TRUE ); gtk_label_set_justify( GTK_LABEL( workspace->graph_details_label ), GTK_JUSTIFY_CENTER ); gtk_widget_set_visible( workspace->graph_spinner, FALSE ); gtk_box_append( GTK_BOX( workspace->graph_state_page ), workspace->graph_spinner ); gtk_box_append( GTK_BOX( workspace->graph_state_page ), workspace->graph_title_label ); gtk_box_append( GTK_BOX( workspace->graph_state_page ), workspace->graph_details_label ); gtk_stack_add_named( GTK_STACK(workspace->stack), workspace->graph_state_page, WORKSPACE_PAGE_GRAPH_STATE ); /* * Page graphique : canvas principal et volet de détails superposé. */ workspace->graph_view_page = gtk_overlay_new(); if (workspace->graph_view_page == NULL) { workspace_free( workspace ); return NULL; } gtk_widget_set_hexpand( workspace->graph_view_page, TRUE ); gtk_widget_set_vexpand( workspace->graph_view_page, TRUE ); workspace->graph_view = investigation_graph_view_new(); 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->relation_details_panel == NULL || workspace->relation_details_title_label == NULL || workspace->relation_details_summary_label == NULL) { workspace_free( workspace ); 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, workspace ); investigation_graph_view_set_node_moved_callback( workspace->graph_view, workspace_on_graph_node_moved, workspace ); entity_details_panel_set_close_callback( workspace->entity_details_panel, workspace_on_entity_details_closed, workspace ); entity_details_panel_set_add_relation_callback( workspace->entity_details_panel, workspace_on_add_relation_requested, workspace ); gtk_overlay_set_child( GTK_OVERLAY( workspace->graph_view_page ), investigation_graph_view_get_widget( workspace->graph_view ) ); workspace->graph_toolbar = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 4 ); workspace->reset_graph_layout_button = gtk_button_new_from_icon_name( "view-refresh-symbolic" ); workspace->osint_tools_button = gtk_menu_button_new(); workspace->osint_tools_popover = gtk_popover_new(); GtkWidget *osint_tools_content = gtk_box_new( GTK_ORIENTATION_VERTICAL, 8 ); GtkWidget *osint_tools_title = gtk_label_new( "Outils OSINT" ); workspace->osint_tools_context_label = gtk_label_new( "Sélectionnez un nœud du graphe." ); workspace->osint_tools_status_label = gtk_label_new( "Les outils compatibles apparaîtront ici." ); workspace->osint_tools_actions_box = gtk_box_new( GTK_ORIENTATION_VERTICAL, 6 ); workspace->osint_action_catalog = osint_action_catalog_new_defaults(); if (workspace->graph_toolbar == NULL || workspace->reset_graph_layout_button == NULL || workspace->osint_tools_button == NULL || workspace->osint_tools_popover == NULL || osint_tools_content == NULL || osint_tools_title == NULL || workspace->osint_tools_context_label == NULL || workspace->osint_tools_status_label == NULL || workspace->osint_tools_actions_box == NULL || workspace->osint_action_catalog == NULL) { workspace_free( workspace ); return NULL; } gtk_widget_set_halign( workspace->graph_toolbar, GTK_ALIGN_START ); gtk_widget_set_valign( workspace->graph_toolbar, GTK_ALIGN_START ); gtk_widget_set_margin_start( workspace->graph_toolbar, 12 ); gtk_widget_set_margin_top( workspace->graph_toolbar, 12 ); gtk_widget_add_css_class( workspace->graph_toolbar, "toolbar" ); gtk_widget_add_css_class( workspace->reset_graph_layout_button, "flat" ); gtk_widget_set_tooltip_text( workspace->reset_graph_layout_button, "Réinitialiser la disposition du graphe" ); gtk_widget_set_sensitive( workspace->reset_graph_layout_button, FALSE ); gtk_menu_button_set_label( GTK_MENU_BUTTON( workspace->osint_tools_button ), "Outils OSINT" ); gtk_widget_add_css_class( workspace->osint_tools_button, "flat" ); gtk_widget_set_tooltip_text( workspace->osint_tools_button, "Afficher les outils compatibles avec la sélection" ); gtk_widget_set_sensitive( workspace->osint_tools_button, FALSE ); gtk_widget_set_margin_start( osint_tools_content, 16 ); gtk_widget_set_margin_end( osint_tools_content, 16 ); gtk_widget_set_margin_top( osint_tools_content, 14 ); gtk_widget_set_margin_bottom( osint_tools_content, 14 ); gtk_label_set_xalign( GTK_LABEL( osint_tools_title ), 0.0F ); gtk_widget_add_css_class( osint_tools_title, "heading" ); gtk_label_set_xalign( GTK_LABEL( workspace->osint_tools_context_label ), 0.0F ); gtk_label_set_selectable( GTK_LABEL( workspace->osint_tools_context_label ), TRUE ); gtk_label_set_xalign( GTK_LABEL( workspace->osint_tools_status_label ), 0.0F ); gtk_widget_add_css_class( workspace->osint_tools_status_label, "dim-label" ); gtk_box_append( GTK_BOX( osint_tools_content ), osint_tools_title ); gtk_box_append( GTK_BOX( osint_tools_content ), workspace->osint_tools_context_label ); gtk_box_append( GTK_BOX(osint_tools_content), workspace->osint_tools_actions_box ); gtk_box_append( GTK_BOX( osint_tools_content ), workspace->osint_tools_status_label ); gtk_popover_set_child( GTK_POPOVER( workspace->osint_tools_popover ), osint_tools_content ); gtk_menu_button_set_popover( GTK_MENU_BUTTON( workspace->osint_tools_button ), workspace->osint_tools_popover ); g_signal_connect( workspace->reset_graph_layout_button, "clicked", G_CALLBACK( workspace_on_reset_graph_layout_clicked ), workspace ); gtk_box_append( GTK_BOX( workspace->graph_toolbar ), workspace->reset_graph_layout_button ); gtk_box_append( GTK_BOX( workspace->graph_toolbar ), workspace->osint_tools_button ); gtk_overlay_add_overlay( GTK_OVERLAY( workspace->graph_view_page ), workspace->graph_toolbar ); gtk_overlay_set_clip_overlay( GTK_OVERLAY( workspace->graph_view_page ), workspace->graph_toolbar, TRUE ); workspace->graph_help_button = gtk_menu_button_new(); workspace->graph_help_popover = gtk_popover_new(); GtkWidget *graph_help_content = gtk_box_new( GTK_ORIENTATION_VERTICAL, 8 ); GtkWidget *graph_help_title = gtk_label_new( "Commandes du graphe" ); GtkWidget *graph_help_text = gtk_label_new( "• Clic : sélectionner un nœud\n" "• Glisser un nœud : le déplacer\n" "• Maj + glisser : déplacer la vue\n" "• Ctrl + molette : zoomer\n" "• Pincement : zoomer" ); if (workspace->graph_help_button == NULL || workspace->graph_help_popover == NULL || graph_help_content == NULL || graph_help_title == NULL || graph_help_text == NULL) { workspace_free( workspace ); return NULL; } gtk_menu_button_set_label( GTK_MENU_BUTTON( workspace->graph_help_button ), "?" ); gtk_widget_add_css_class( workspace->graph_help_button, "flat" ); gtk_widget_set_tooltip_text( workspace->graph_help_button, "Afficher les commandes du graphe" ); gtk_widget_set_can_target( workspace->graph_help_button, TRUE ); gtk_widget_set_halign( workspace->graph_help_button, GTK_ALIGN_END ); gtk_widget_set_valign( workspace->graph_help_button, GTK_ALIGN_START ); gtk_widget_set_margin_end( workspace->graph_help_button, 12 ); gtk_widget_set_margin_top( workspace->graph_help_button, 12 ); gtk_widget_set_margin_start( graph_help_content, 16 ); gtk_widget_set_margin_end( graph_help_content, 16 ); gtk_widget_set_margin_top( graph_help_content, 14 ); gtk_widget_set_margin_bottom( graph_help_content, 14 ); gtk_label_set_xalign( GTK_LABEL( graph_help_title ), 0.0F ); gtk_widget_add_css_class( graph_help_title, "heading" ); gtk_label_set_xalign( GTK_LABEL( graph_help_text ), 0.0F ); gtk_label_set_wrap( GTK_LABEL( graph_help_text ), TRUE ); gtk_label_set_wrap_mode( GTK_LABEL( graph_help_text ), PANGO_WRAP_WORD_CHAR ); gtk_label_set_max_width_chars( GTK_LABEL( graph_help_text ), 44 ); gtk_box_append( GTK_BOX( graph_help_content ), graph_help_title ); gtk_box_append( GTK_BOX( graph_help_content ), graph_help_text ); gtk_popover_set_child( GTK_POPOVER( workspace->graph_help_popover ), graph_help_content ); gtk_menu_button_set_popover( GTK_MENU_BUTTON( workspace->graph_help_button ), workspace->graph_help_popover ); gtk_overlay_add_overlay( GTK_OVERLAY( workspace->graph_view_page ), entity_details_panel_get_widget( workspace->entity_details_panel ) ); 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 ), entity_details_panel_get_widget( workspace->entity_details_panel ), TRUE ); gtk_overlay_add_overlay( GTK_OVERLAY( workspace->graph_view_page ), workspace->graph_help_button ); gtk_overlay_set_clip_overlay( GTK_OVERLAY( workspace->graph_view_page ), workspace->graph_help_button, TRUE ); gtk_stack_add_named( GTK_STACK(workspace->stack), workspace->graph_view_page, WORKSPACE_PAGE_GRAPH_VIEW ); workspace->graph_model = NULL; workspace->graph_layout = NULL; workspace->graph_state = WORKSPACE_GRAPH_STATE_EMPTY; workspace_show_default( workspace ); return workspace; } GtkWidget *workspace_get_widget( const Workspace *workspace ) { if (workspace == NULL) { return NULL; } return workspace->root_widget; } void workspace_set_selected_node( Workspace *workspace, const InvestigationNode *node ) { const char *node_name = NULL; const InvestigationNode *parent_node = NULL; const char *parent_name = NULL; const char *node_path = NULL; InvestigationNodeType node_type; size_t children_count = 0; const char *type_text = NULL; char *parent_text = NULL; char *children_text = NULL; if (workspace == NULL) { return; } g_clear_pointer( &workspace->selected_evidence_identifier, g_free ); if (workspace->verify_evidence_button != NULL) { gtk_widget_set_sensitive( workspace->verify_evidence_button, FALSE ); } if (node == NULL) { workspace_show_default(workspace); return; } node_name = investigation_node_get_name(node); node_path = investigation_node_get_path(node); node_type = investigation_node_get_type(node); parent_node = investigation_node_get_parent(node); gtk_label_set_text( GTK_LABEL(workspace->node_name_label), node_name != NULL ? node_name : "(sans nom)" ); gtk_label_set_text( GTK_LABEL(workspace->node_path_label), node_path != NULL ? node_path : "(chemin indisponible)" ); if (node_type == INVESTIGATION_NODE_DIRECTORY) { type_text = "Type : dossier"; } else { type_text = "Type : fichier"; } gtk_label_set_text( GTK_LABEL(workspace->node_type_label), type_text ); if (parent_node != NULL) { parent_name = investigation_node_get_name(parent_node); parent_text = g_strdup_printf( "Parent : %s", parent_name != NULL ? parent_name : "(sans nom)" ); } else { parent_text = g_strdup("Parent : aucun"); } gtk_label_set_text( GTK_LABEL(workspace->node_parent_label), parent_text ); if (node_type == INVESTIGATION_NODE_DIRECTORY) { children_count = investigation_node_get_children_count(node); children_text = g_strdup_printf( "Enfants : %zu", children_count ); gtk_label_set_text( GTK_LABEL(workspace->node_children_label), children_text ); gtk_widget_set_visible( workspace->node_children_label, TRUE ); } else { gtk_label_set_text( GTK_LABEL(workspace->node_children_label), "" ); gtk_widget_set_visible( workspace->node_children_label, FALSE ); } gtk_stack_set_visible_child_name( GTK_STACK(workspace->stack), WORKSPACE_PAGE_NODE_INFORMATION ); g_free(children_text); g_free(parent_text); } void workspace_set_selected_evidence( Workspace *workspace, const EvidenceRecord *evidence_record ) { char *human_size = NULL; char *size_text = NULL; guint64 size_bytes = 0; if (workspace == NULL) { return; } g_clear_pointer( &workspace->selected_evidence_identifier, g_free ); if (workspace->verify_evidence_button != NULL) { gtk_widget_set_sensitive( workspace->verify_evidence_button, FALSE ); } if (evidence_record == NULL) { workspace_show_default( workspace ); return; } workspace->selected_evidence_identifier = g_strdup( evidence_record_get_identifier( evidence_record ) ); if (workspace->selected_evidence_identifier == NULL) { workspace_show_default( workspace ); return; } gtk_widget_set_sensitive( workspace->verify_evidence_button, TRUE ); size_bytes = evidence_record_get_size_bytes( evidence_record ); human_size = g_format_size_full( size_bytes, G_FORMAT_SIZE_IEC_UNITS ); size_text = g_strdup_printf( "%s (%" G_GUINT64_FORMAT " octets)", human_size != NULL ? human_size : "taille inconnue", size_bytes ); workspace_set_field_text( workspace->evidence_name_label, evidence_record_get_original_name( evidence_record ), "(sans nom)" ); workspace_set_field_text( workspace->evidence_type_label, evidence_record_get_type_identifier( evidence_record ), "(type inconnu)" ); workspace_set_field_text( workspace->evidence_integrity_label, workspace_get_integrity_status_text( evidence_record_get_integrity_status( evidence_record ) ), "Statut inconnu" ); workspace_set_field_text( workspace->evidence_size_label, size_text, "Taille inconnue" ); workspace_set_field_text( workspace->evidence_imported_at_label, evidence_record_get_imported_at( evidence_record ), "(date inconnue)" ); workspace_set_field_text( workspace->evidence_collected_at_label, evidence_record_get_collected_at( evidence_record ), "Non renseignée" ); workspace_set_field_text( workspace->evidence_source_label, evidence_record_get_source( evidence_record ), "Non renseignée" ); workspace_set_field_text( workspace->evidence_description_label, evidence_record_get_description( evidence_record ), "Non renseignée" ); workspace_set_field_text( workspace->evidence_relative_path_label, evidence_record_get_relative_path( evidence_record ), "(chemin inconnu)" ); workspace_set_field_text( workspace->evidence_internal_name_label, evidence_record_get_internal_name( evidence_record ), "(nom interne inconnu)" ); workspace_set_field_text( workspace->evidence_identifier_label, evidence_record_get_identifier( evidence_record ), "(identifiant inconnu)" ); workspace_set_field_text( workspace->evidence_sha256_label, evidence_record_get_sha256( evidence_record ), "(empreinte inconnue)" ); gtk_stack_set_visible_child_name( GTK_STACK(workspace->stack), WORKSPACE_PAGE_EVIDENCE_INFORMATION ); g_free( size_text ); g_free( human_size ); } 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 ); } 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_osint_tool_state( Workspace *workspace, const char *tool_identifier, OsintActionToolState state, const char *version ) { if (workspace == NULL || workspace->osint_action_catalog == NULL) { return; } osint_action_catalog_update_tool_state( workspace->osint_action_catalog, tool_identifier, state, version ); workspace_update_osint_tools_menu( workspace, workspace->osint_selection_context ); } void workspace_set_osint_action_callback( Workspace *workspace, WorkspaceOsintActionCallback callback, gpointer user_data ) { if (workspace == NULL) { return; } workspace->osint_action_callback = callback; workspace->osint_action_user_data = user_data; } void workspace_set_graph_loading( Workspace *workspace ) { if (workspace == NULL) { return; } g_clear_pointer( &workspace->selected_evidence_identifier, g_free ); if (workspace->verify_evidence_button != NULL) { gtk_widget_set_sensitive( workspace->verify_evidence_button, FALSE ); } entity_details_panel_clear( workspace->entity_details_panel ); investigation_graph_view_clear( workspace->graph_view ); workspace->graph_model = NULL; workspace->graph_layout = NULL; workspace->graph_state = WORKSPACE_GRAPH_STATE_LOADING; gtk_widget_set_sensitive( workspace->reset_graph_layout_button, FALSE ); gtk_label_set_text( GTK_LABEL( workspace->graph_title_label ), "Chargement de l’enquête…" ); gtk_label_set_text( GTK_LABEL( workspace->graph_details_label ), "Lecture des entités et des relations" ); gtk_widget_set_visible( workspace->graph_spinner, TRUE ); gtk_spinner_start( GTK_SPINNER( workspace->graph_spinner ) ); workspace_show_default( workspace ); } void workspace_set_graph( Workspace *workspace, const InvestigationGraphModel *graph_model, const InvestigationGraphLayout *graph_layout ) { if (workspace == NULL) { return; } if (graph_model == NULL) { workspace_clear_graph( workspace ); return; } g_clear_pointer( &workspace->selected_evidence_identifier, g_free ); if (workspace->verify_evidence_button != NULL) { gtk_widget_set_sensitive( workspace->verify_evidence_button, FALSE ); } entity_details_panel_clear( workspace->entity_details_panel ); investigation_graph_view_clear( workspace->graph_view ); workspace->graph_model = graph_model; workspace->graph_layout = graph_layout; investigation_graph_view_set_layout( workspace->graph_view, graph_layout ); investigation_graph_view_set_graph( workspace->graph_view, graph_model ); workspace->graph_state = WORKSPACE_GRAPH_STATE_READY; gtk_widget_set_sensitive( workspace->reset_graph_layout_button, TRUE ); gtk_spinner_stop( GTK_SPINNER( workspace->graph_spinner ) ); gtk_widget_set_visible( workspace->graph_spinner, FALSE ); gtk_label_set_text( GTK_LABEL( workspace->graph_title_label ), "" ); gtk_label_set_text( GTK_LABEL( workspace->graph_details_label ), "" ); workspace_show_default( workspace ); } void workspace_set_graph_error( Workspace *workspace, const char *message ) { if (workspace == NULL) { return; } g_clear_pointer( &workspace->selected_evidence_identifier, g_free ); if (workspace->verify_evidence_button != NULL) { gtk_widget_set_sensitive( workspace->verify_evidence_button, FALSE ); } entity_details_panel_clear( workspace->entity_details_panel ); investigation_graph_view_clear( workspace->graph_view ); workspace->graph_model = NULL; workspace->graph_layout = NULL; workspace->graph_state = WORKSPACE_GRAPH_STATE_ERROR; gtk_widget_set_sensitive( workspace->reset_graph_layout_button, FALSE ); gtk_spinner_stop( GTK_SPINNER( workspace->graph_spinner ) ); gtk_widget_set_visible( workspace->graph_spinner, FALSE ); gtk_label_set_text( GTK_LABEL( workspace->graph_title_label ), "Impossible de charger le graphe de l’enquête" ); gtk_label_set_text( GTK_LABEL( workspace->graph_details_label ), message != NULL && message[0] != '\0' ? message : "Aucun détail supplémentaire n’est disponible." ); workspace_show_default( workspace ); } void workspace_clear_graph( Workspace *workspace ) { if (workspace == NULL) { return; } g_clear_pointer( &workspace->selected_evidence_identifier, g_free ); if (workspace->verify_evidence_button != NULL) { gtk_widget_set_sensitive( workspace->verify_evidence_button, FALSE ); } entity_details_panel_clear( workspace->entity_details_panel ); investigation_graph_view_clear( workspace->graph_view ); workspace->graph_model = NULL; workspace->graph_layout = NULL; workspace->graph_state = WORKSPACE_GRAPH_STATE_EMPTY; gtk_widget_set_sensitive( workspace->reset_graph_layout_button, FALSE ); gtk_spinner_stop( GTK_SPINNER( workspace->graph_spinner ) ); gtk_widget_set_visible( workspace->graph_spinner, FALSE ); gtk_label_set_text( GTK_LABEL( workspace->graph_title_label ), "" ); gtk_label_set_text( GTK_LABEL( workspace->graph_details_label ), "" ); workspace_show_default( workspace ); } void workspace_set_verify_evidence_callback( Workspace *workspace, WorkspaceVerifyEvidenceCallback callback, gpointer user_data ) { if (workspace == NULL) { return; } workspace->verify_evidence_callback = callback; workspace->verify_evidence_user_data = user_data; } void workspace_set_graph_node_moved_callback( Workspace *workspace, WorkspaceGraphNodeMovedCallback callback, gpointer user_data ) { if (workspace == NULL) { return; } workspace->graph_node_moved_callback = callback; workspace->graph_node_moved_user_data = user_data; } void workspace_set_reset_graph_layout_callback( Workspace *workspace, WorkspaceResetGraphLayoutCallback callback, gpointer user_data ) { if (workspace == NULL) { return; } workspace->reset_graph_layout_callback = callback; workspace->reset_graph_layout_user_data = user_data; } void workspace_set_add_relation_callback( Workspace *workspace, WorkspaceAddRelationCallback callback, gpointer user_data ) { if (workspace == NULL) { return; } workspace->add_relation_callback = callback; workspace->add_relation_user_data = user_data; } void workspace_reset_graph_layout( Workspace *workspace ) { if (workspace == NULL || workspace->graph_view == NULL || workspace->graph_state != WORKSPACE_GRAPH_STATE_READY || workspace->graph_model == NULL) { return; } entity_details_panel_clear( workspace->entity_details_panel ); investigation_graph_view_clear_selection( workspace->graph_view ); investigation_graph_view_reset_layout( workspace->graph_view ); } void workspace_free(Workspace *workspace) { if (workspace == NULL) { return; } g_clear_pointer( &workspace->osint_selection_context, osint_selection_context_free ); g_clear_pointer( &workspace->osint_action_catalog, osint_action_catalog_free ); investigation_graph_view_set_selection_callback( workspace->graph_view, NULL, NULL ); investigation_graph_view_set_node_moved_callback( workspace->graph_view, NULL, NULL ); entity_details_panel_set_close_callback( workspace->entity_details_panel, NULL, NULL ); entity_details_panel_set_add_relation_callback( workspace->entity_details_panel, NULL, NULL ); entity_details_panel_clear( workspace->entity_details_panel ); investigation_graph_view_clear( workspace->graph_view ); investigation_graph_view_free( workspace->graph_view ); entity_details_panel_free( workspace->entity_details_panel ); workspace->graph_view = NULL; workspace->entity_details_panel = NULL; workspace->graph_model = NULL; workspace->graph_layout = NULL; workspace->verify_evidence_callback = NULL; workspace->verify_evidence_user_data = NULL; workspace->graph_node_moved_callback = NULL; workspace->graph_node_moved_user_data = NULL; workspace->reset_graph_layout_callback = NULL; workspace->reset_graph_layout_user_data = NULL; workspace->add_relation_callback = NULL; workspace->add_relation_user_data = NULL; workspace->graph_toolbar = NULL; workspace->reset_graph_layout_button = NULL; workspace->graph_help_button = NULL; workspace->graph_help_popover = NULL; g_clear_pointer( &workspace->selected_evidence_identifier, g_free ); g_free(workspace); }