labfy-investigation/src/widgets/workspace.c
2026-07-22 19:08:26 +02:00

3402 lines
82 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/******************************************************************************
* @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 <glib.h>
#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 *relation_evidences_label;
GtkWidget *edit_relation_button;
GtkWidget *close_relation_details_button;
char *selected_relation_identifier;
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;
GtkWidget *edit_evidence_button;
GtkWidget *analyze_eml_button;
GtkWidget *evidence_preview_stack;
GtkWidget *evidence_preview_status;
GtkWidget *evidence_preview_picture;
GtkWidget *evidence_preview_video;
GtkWidget *evidence_preview_expand_button;
char *evidence_preview_path;
gboolean evidence_preview_is_video;
char *selected_evidence_identifier;
WorkspaceVerifyEvidenceCallback
verify_evidence_callback;
gpointer
verify_evidence_user_data;
WorkspaceEditEvidenceCallback edit_evidence_callback;
gpointer edit_evidence_user_data;
WorkspaceAnalyzeEmlCallback analyze_eml_callback;
gpointer analyze_eml_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;
WorkspaceEditRelationCallback edit_relation_callback;
WorkspaceRelationSelectedCallback relation_selected_callback;
gpointer relation_selected_user_data;
gpointer edit_relation_user_data;
WorkspacePersonRoleCallback person_role_callback;
gpointer person_role_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),
g_str_has_prefix(action_identifier, "history-")
? "Ouverture de l'historique enregistré."
: "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
);
{
GtkWidget *history_button = gtk_button_new_with_label(
"Historique OSINT"
);
const char *history_action =
osint_selection_context_get_kind(context) ==
OSINT_SELECTION_CONTEXT_KIND_RELATION
? "history-relation" : "history-entity";
gtk_widget_set_tooltip_text(
history_button,
"Consulter les exécutions OSINT enregistrées pour cette sélection"
);
g_object_set_data_full(
G_OBJECT(history_button), "osint-action-identifier",
g_strdup(history_action), g_free
);
g_signal_connect(
history_button, "clicked",
G_CALLBACK(workspace_on_osint_action_clicked), workspace
);
gtk_box_append(
GTK_BOX(workspace->osint_tools_actions_box), history_button
);
}
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 Affiche une erreur asynchrone produite par le moteur vidéo. */
static void workspace_on_video_error_changed(GObject *object,
GParamSpec *parameter, gpointer user_data)
{
Workspace *workspace = user_data;
const GError *error = NULL;
(void) parameter;
if (workspace == NULL || !GTK_IS_MEDIA_STREAM(object)) return;
error = gtk_media_stream_get_error(GTK_MEDIA_STREAM(object));
if (error == NULL) return;
gtk_label_set_text(GTK_LABEL(workspace->evidence_preview_status),
error->message != NULL ? error->message :
"Le moteur vidéo ne peut pas lire ce fichier.");
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "status");
gtk_widget_set_sensitive(workspace->evidence_preview_expand_button,
FALSE);
}
/** @brief Arrête et détache le média vidéo actuellement affiché. */
static void workspace_clear_evidence_preview(Workspace *workspace)
{
GtkMediaStream *stream = NULL;
if (workspace == NULL) return;
if (workspace->evidence_preview_video != NULL)
{
stream = gtk_video_get_media_stream(
GTK_VIDEO(workspace->evidence_preview_video));
if (stream != NULL)
{
g_signal_handlers_disconnect_by_data(stream, workspace);
gtk_media_stream_pause(stream);
}
gtk_video_set_media_stream(GTK_VIDEO(
workspace->evidence_preview_video), NULL);
}
if (workspace->evidence_preview_picture != NULL)
gtk_picture_set_paintable(GTK_PICTURE(
workspace->evidence_preview_picture), NULL);
g_clear_pointer(&workspace->evidence_preview_path, g_free);
workspace->evidence_preview_is_video = FALSE;
if (workspace->evidence_preview_expand_button != NULL)
gtk_widget_set_sensitive(workspace->evidence_preview_expand_button,
FALSE);
}
/** @brief Ouvre le média courant dans une fenêtre d'aperçu agrandie. */
static void workspace_on_expand_evidence_preview(GtkButton *button,
gpointer user_data)
{
Workspace *workspace = user_data;
GtkWindow *window = NULL;
GtkWidget *content = NULL;
(void) button;
if (workspace == NULL || workspace->evidence_preview_path == NULL) return;
window = GTK_WINDOW(gtk_window_new());
gtk_window_set_title(window, "Aperçu de la preuve");
gtk_window_set_default_size(window, 960, 720);
if (workspace->evidence_preview_is_video)
{
content = gtk_video_new_for_filename(workspace->evidence_preview_path);
gtk_video_set_autoplay(GTK_VIDEO(content), FALSE);
}
else
{
content = gtk_picture_new_for_filename(workspace->evidence_preview_path);
gtk_picture_set_can_shrink(GTK_PICTURE(content), TRUE);
gtk_picture_set_content_fit(GTK_PICTURE(content), GTK_CONTENT_FIT_CONTAIN);
}
gtk_window_set_child(window, content);
gtk_window_present(window);
}
/** @brief Convertit le statut d'une relation en texte utilisateur. */
static const char *workspace_get_relation_status_text(RelationStatus status)
{
switch (status)
{
case RELATION_STATUS_ACTIVE: return "Active";
case RELATION_STATUS_ARCHIVED: return "Archivée";
case RELATION_STATUS_DELETED: return "Supprimée";
case RELATION_STATUS_DISPUTED: return "Contestée";
case RELATION_STATUS_UNKNOWN:
default: return "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
);
g_clear_pointer(&workspace->selected_relation_identifier, g_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_can_target(workspace->relation_details_panel,
relation_record != 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)
{
workspace->selected_relation_identifier = g_strdup(
relation_record_get_identifier(relation_record));
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 %%\nStatut : %s\n"
"Justification : %s\nCréée le : %s\nModifiée le : %s\nUUID : %s",
source_entity != NULL ? entity_record_get_value(source_entity) :
"Source inconnue",
target_entity != NULL ? entity_record_get_value(target_entity) :
"Cible inconnue",
relation_record_get_relation_type(relation_record),
relation_record_get_confidence(relation_record),
workspace_get_relation_status_text(
relation_record_get_status(relation_record)),
relation_record_get_justification(relation_record) != NULL
? relation_record_get_justification(relation_record)
: "Non renseignée",
relation_record_get_created_at(relation_record),
relation_record_get_updated_at(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);
if (workspace->relation_selected_callback != NULL)
workspace->relation_selected_callback(
workspace->selected_relation_identifier,
workspace->relation_selected_user_data);
}
workspace_update_osint_tools_menu(
workspace,
workspace->osint_selection_context
);
}
/** @brief Relaie la demande de modification de la relation affichée. */
static void workspace_on_edit_relation_clicked(GtkButton *button,
gpointer user_data)
{
Workspace *workspace = user_data;
(void) button;
if (workspace == NULL || workspace->edit_relation_callback == NULL ||
workspace->selected_relation_identifier == NULL)
{
return;
}
workspace->edit_relation_callback(workspace->selected_relation_identifier,
workspace->edit_relation_user_data);
}
/** @brief Ferme le volet de relation et désélectionne le graphe. */
static void workspace_on_close_relation_details_clicked(GtkButton *button,
gpointer user_data)
{
Workspace *workspace = user_data;
(void) button;
if (workspace != NULL)
investigation_graph_view_clear_selection(workspace->graph_view);
}
/**
* @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 Relaie le changement de catégorie d'une personne. */
static void workspace_on_person_role_changed(const char *entity_identifier,
PersonRole role, gpointer user_data)
{
Workspace *workspace = user_data;
if (workspace != NULL && workspace->person_role_callback != NULL)
workspace->person_role_callback(entity_identifier, role,
workspace->person_role_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
);
}
/** @brief Transmet la demande de modification de la preuve affichée. */
static void workspace_on_edit_evidence_clicked(
GtkButton *button, gpointer user_data
)
{
Workspace *workspace = user_data;
(void) button;
if (workspace == NULL || workspace->edit_evidence_callback == NULL ||
workspace->selected_evidence_identifier == NULL) return;
workspace->edit_evidence_callback(
workspace->selected_evidence_identifier,
workspace->edit_evidence_user_data);
}
/** @brief Transmet la demande d'analyse de la preuve EML affichée. */
static void workspace_on_analyze_eml_clicked(GtkButton *button, gpointer data)
{
Workspace *workspace = data; (void) button;
if (workspace != NULL && workspace->analyze_eml_callback != NULL &&
workspace->selected_evidence_identifier != NULL)
workspace->analyze_eml_callback(workspace->selected_evidence_identifier,
workspace->analyze_eml_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->evidence_preview_stack = gtk_stack_new();
workspace->evidence_preview_status = gtk_label_new(
"Aucun aperçu disponible pour cette preuve.");
workspace->evidence_preview_picture = gtk_picture_new();
workspace->evidence_preview_video = gtk_video_new();
workspace->evidence_preview_expand_button =
gtk_button_new_with_label("Agrandir laperçu");
gtk_widget_set_size_request(workspace->evidence_preview_stack, -1, 360);
gtk_widget_set_hexpand(workspace->evidence_preview_stack, TRUE);
gtk_picture_set_can_shrink(GTK_PICTURE(
workspace->evidence_preview_picture), TRUE);
gtk_picture_set_content_fit(GTK_PICTURE(
workspace->evidence_preview_picture), GTK_CONTENT_FIT_CONTAIN);
gtk_video_set_autoplay(GTK_VIDEO(workspace->evidence_preview_video), FALSE);
gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack),
workspace->evidence_preview_status, "status");
gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack),
workspace->evidence_preview_picture, "image");
gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack),
workspace->evidence_preview_video, "video");
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "status");
gtk_widget_set_halign(workspace->evidence_preview_expand_button,
GTK_ALIGN_START);
gtk_widget_set_sensitive(workspace->evidence_preview_expand_button, FALSE);
g_signal_connect(workspace->evidence_preview_expand_button, "clicked",
G_CALLBACK(workspace_on_expand_evidence_preview), workspace);
gtk_box_append(GTK_BOX(evidence_content),
workspace->evidence_preview_stack);
gtk_box_append(GTK_BOX(evidence_content),
workspace->evidence_preview_expand_button);
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
);
workspace->edit_evidence_button = gtk_button_new_with_label(
"Modifier les métadonnées");
gtk_widget_set_halign(workspace->edit_evidence_button, GTK_ALIGN_START);
gtk_widget_set_sensitive(workspace->edit_evidence_button, FALSE);
gtk_widget_set_tooltip_text(workspace->edit_evidence_button,
"Corriger le classement sans modifier le fichier original");
g_signal_connect(workspace->edit_evidence_button, "clicked",
G_CALLBACK(workspace_on_edit_evidence_clicked), workspace);
gtk_box_append(GTK_BOX(evidence_content), workspace->edit_evidence_button);
workspace->analyze_eml_button = gtk_button_new_with_label(
"Analyser les en-têtes EML");
gtk_widget_set_halign(workspace->analyze_eml_button, GTK_ALIGN_START);
gtk_widget_set_sensitive(workspace->analyze_eml_button, FALSE);
gtk_widget_set_tooltip_text(workspace->analyze_eml_button,
"Créer une copie vérifiée puis analyser localement ses en-têtes");
g_signal_connect(workspace->analyze_eml_button, "clicked",
G_CALLBACK(workspace_on_analyze_eml_clicked), workspace);
gtk_box_append(GTK_BOX(evidence_content), workspace->analyze_eml_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);
workspace->relation_evidences_label = gtk_label_new(
"Pièces jointes : chargement…");
workspace->edit_relation_button = gtk_button_new_with_label("Modifier");
workspace->close_relation_details_button =
gtk_button_new_from_icon_name("window-close-symbolic");
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->relation_evidences_label == NULL ||
workspace->edit_relation_button == NULL ||
workspace->close_relation_details_button == 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);
/* `view` fournit le fond opaque du thème, comme le volet d'entité. */
gtk_widget_add_css_class(workspace->relation_details_panel, "view");
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_widget_set_margin_start(workspace->relation_evidences_label, 16);
gtk_widget_set_margin_end(workspace->relation_evidences_label, 16);
gtk_widget_set_margin_bottom(workspace->relation_evidences_label, 12);
gtk_label_set_xalign(GTK_LABEL(workspace->relation_evidences_label), 0.0F);
gtk_label_set_wrap(GTK_LABEL(workspace->relation_evidences_label), TRUE);
gtk_label_set_selectable(GTK_LABEL(workspace->relation_evidences_label),
TRUE);
gtk_box_append(
GTK_BOX(workspace->relation_details_panel),
workspace->relation_details_title_label
);
gtk_widget_set_tooltip_text(workspace->close_relation_details_button,
"Fermer le volet");
gtk_widget_add_css_class(workspace->close_relation_details_button, "flat");
gtk_box_append(GTK_BOX(workspace->relation_details_panel),
workspace->close_relation_details_button);
gtk_box_append(
GTK_BOX(workspace->relation_details_panel),
workspace->relation_details_summary_label
);
gtk_box_append(GTK_BOX(workspace->relation_details_panel),
workspace->relation_evidences_label);
gtk_box_append(GTK_BOX(workspace->relation_details_panel),
workspace->edit_relation_button);
g_signal_connect(workspace->edit_relation_button, "clicked",
G_CALLBACK(workspace_on_edit_relation_clicked), workspace);
g_signal_connect(workspace->close_relation_details_button, "clicked",
G_CALLBACK(workspace_on_close_relation_details_clicked), workspace);
gtk_widget_set_visible(workspace->relation_details_panel, FALSE);
gtk_widget_set_can_target(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
);
entity_details_panel_set_person_role_callback(
workspace->entity_details_panel, workspace_on_person_role_changed,
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 (workspace->edit_evidence_button != NULL)
gtk_widget_set_sensitive(workspace->edit_evidence_button, FALSE);
if (workspace->analyze_eml_button != NULL)
gtk_widget_set_sensitive(workspace->analyze_eml_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;
}
workspace_clear_evidence_preview(workspace);
if (workspace->evidence_preview_stack != NULL)
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "status");
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
);
gtk_widget_set_sensitive(workspace->edit_evidence_button, TRUE);
{
const char *name = evidence_record_get_original_name(evidence_record);
char *lower = name != NULL ? g_ascii_strdown(name, -1) : NULL;
gtk_widget_set_sensitive(workspace->analyze_eml_button,
lower != NULL && g_str_has_suffix(lower, ".eml"));
g_free(lower);
}
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
);
}
void workspace_set_evidence_preview(Workspace *workspace,
const char *file_path, const char *display_name)
{
GFile *file = NULL;
GFileInfo *info = NULL;
const char *content_type = NULL;
char *mime_type = NULL;
GtkMediaStream *stream = NULL;
GError *error = NULL;
if (workspace == NULL) return;
workspace_clear_evidence_preview(workspace);
if (file_path == NULL || file_path[0] == '\0')
{
gtk_label_set_text(GTK_LABEL(workspace->evidence_preview_status),
"Aucun aperçu disponible pour cette preuve.");
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "status");
return;
}
file = g_file_new_for_path(file_path);
info = g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
G_FILE_QUERY_INFO_NONE, NULL, &error);
if (info != NULL) content_type = g_file_info_get_content_type(info);
if (content_type != NULL) mime_type = g_content_type_get_mime_type(
content_type);
if (mime_type != NULL && g_str_has_prefix(mime_type, "image/"))
{
gtk_picture_set_filename(GTK_PICTURE(
workspace->evidence_preview_picture), file_path);
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "image");
workspace->evidence_preview_is_video = FALSE;
}
else if (mime_type != NULL && g_str_has_prefix(mime_type, "video/"))
{
stream = gtk_media_file_new_for_filename(file_path);
g_signal_connect(stream, "notify::error",
G_CALLBACK(workspace_on_video_error_changed), workspace);
gtk_video_set_media_stream(GTK_VIDEO(
workspace->evidence_preview_video), stream);
g_object_unref(stream);
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "video");
workspace->evidence_preview_is_video = TRUE;
}
else
{
char *message = g_strdup_printf(
"Aperçu indisponible pour %s%s%s.",
display_name != NULL ? display_name : "ce fichier",
error != NULL ? " : " : "",
error != NULL ? error->message : "format non pris en charge");
gtk_label_set_text(GTK_LABEL(workspace->evidence_preview_status),
message);
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "status");
g_free(message);
goto cleanup;
}
workspace->evidence_preview_path = g_strdup(file_path);
gtk_widget_set_sensitive(workspace->evidence_preview_expand_button,
workspace->evidence_preview_path != NULL);
cleanup:
g_clear_error(&error);
g_free(mime_type);
g_clear_object(&info);
g_clear_object(&file);
}
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 lenquê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 lenquête"
);
gtk_label_set_text(
GTK_LABEL(
workspace->graph_details_label
),
message != NULL &&
message[0] != '\0'
? message
: "Aucun détail supplémentaire nest 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_edit_evidence_callback(
Workspace *workspace, WorkspaceEditEvidenceCallback callback,
gpointer user_data
)
{
if (workspace == NULL) return;
workspace->edit_evidence_callback = callback;
workspace->edit_evidence_user_data = user_data;
}
void workspace_set_analyze_eml_callback(Workspace *workspace,
WorkspaceAnalyzeEmlCallback callback, gpointer user_data)
{
if (workspace == NULL) return;
workspace->analyze_eml_callback = callback;
workspace->analyze_eml_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_set_edit_relation_callback(Workspace *workspace,
WorkspaceEditRelationCallback callback, gpointer user_data)
{
if (workspace == NULL) return;
workspace->edit_relation_callback = callback;
workspace->edit_relation_user_data = user_data;
}
void workspace_set_relation_selected_callback(Workspace *workspace,
WorkspaceRelationSelectedCallback callback, gpointer user_data)
{
if (workspace == NULL) return;
workspace->relation_selected_callback = callback;
workspace->relation_selected_user_data = user_data;
}
void workspace_set_relation_evidences(Workspace *workspace,
const GPtrArray *evidence_records)
{
GString *text = NULL;
if (workspace == NULL || workspace->relation_evidences_label == NULL)
return;
text = g_string_new("Pièces jointes");
if (evidence_records == NULL || evidence_records->len == 0)
g_string_append(text, " : aucune");
else
{
g_string_append_printf(text, " (%u) :", evidence_records->len);
for (guint index = 0; index < evidence_records->len; index++)
{
const EvidenceRecord *record = g_ptr_array_index(evidence_records,
index);
if (record == NULL) continue;
g_string_append_printf(text, "\n• %s — %s",
evidence_record_get_original_name(record),
evidence_record_get_type_identifier(record));
}
}
gtk_label_set_text(GTK_LABEL(workspace->relation_evidences_label),
text->str);
g_string_free(text, TRUE);
}
void workspace_set_person_role_callback(Workspace *workspace,
WorkspacePersonRoleCallback callback, gpointer user_data)
{
if (workspace == NULL) return;
workspace->person_role_callback = callback;
workspace->person_role_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;
}
workspace_clear_evidence_preview(workspace);
g_clear_pointer(
&workspace->osint_selection_context,
osint_selection_context_free
);
g_clear_pointer(&workspace->selected_relation_identifier, g_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_set_person_role_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);
}