diff --git a/include/views/create_relation_dialog.h b/include/views/create_relation_dialog.h index 3b02678..6d3d6ff 100644 --- a/include/views/create_relation_dialog.h +++ b/include/views/create_relation_dialog.h @@ -68,6 +68,8 @@ GQuark create_relation_dialog_error_quark(void); * @param parent Fenêtre GTK parente. * @param source_entity_identifier UUID de l'entité source. * @param entities Tableau de EntityRecord. + * @param evidence_records Tableau de EvidenceRecord disponibles. + * @param investigation_root_path Racine canonique de l'enquête. * @param callback Fonction appelée après validation ou annulation. * @param user_data Données privées transmises au callback. * @param error Emplacement facultatif recevant une erreur immédiate. @@ -78,6 +80,8 @@ gboolean create_relation_dialog_present( GtkWindow *parent, const char *source_entity_identifier, const GPtrArray *entities, + const GPtrArray *evidence_records, + const char *investigation_root_path, CreateRelationDialogCallback callback, gpointer user_data, GError **error @@ -88,11 +92,26 @@ gboolean create_relation_dialog_present( * * La source et la cible sont conservées ; les champs métier sont modifiables. * Le dialogue ne réalise aucune écriture SQLite. + * + * @param parent Fenêtre GTK parente. + * @param relation_record Relation existante à modifier. + * @param entities Tableau de EntityRecord. + * @param evidence_records Tableau de EvidenceRecord disponibles. + * @param selected_evidence_identifiers UUID des preuves déjà liées. + * @param investigation_root_path Racine canonique de l'enquête. + * @param callback Fonction appelée après validation ou annulation. + * @param user_data Données privées transmises au callback. + * @param error Emplacement facultatif recevant une erreur immédiate. + * + * @return TRUE si le dialogue a été créé et présenté. */ gboolean edit_relation_dialog_present( GtkWindow *parent, const RelationRecord *relation_record, const GPtrArray *entities, + const GPtrArray *evidence_records, + const GPtrArray *selected_evidence_identifiers, + const char *investigation_root_path, CreateRelationDialogCallback callback, gpointer user_data, GError **error @@ -171,6 +190,16 @@ gint create_relation_dialog_result_get_confidence( const CreateRelationDialogResult *result ); +/** + * @brief Retourne les UUID de preuves sélectionnés. + * + * @param result Résultat validé du dialogue. + * + * @return Tableau emprunté d'UUID, ou NULL. + */ +const GPtrArray *create_relation_dialog_result_get_evidence_identifiers( + const CreateRelationDialogResult *result); + G_END_DECLS #endif diff --git a/include/views/main_window.h b/include/views/main_window.h index 20d737a..5ad9a97 100644 --- a/include/views/main_window.h +++ b/include/views/main_window.h @@ -143,6 +143,12 @@ typedef void (*MainWindowEditRelationCallback)( gpointer user_data ); +/** @brief Callback appelé quand une relation est sélectionnée. */ +typedef void (*MainWindowRelationSelectedCallback)( + const char *relation_identifier, + gpointer user_data +); + /** @brief Callback appelé pour catégoriser une personne. */ typedef void (*MainWindowPersonRoleCallback)(const char *entity_identifier, PersonRole role, gpointer user_data); @@ -244,6 +250,19 @@ void main_window_set_edit_relation_callback( gpointer user_data ); +/** @brief Définit le callback de sélection d'une relation. */ +void main_window_set_relation_selected_callback( + MainWindow *main_window, + MainWindowRelationSelectedCallback callback, + gpointer user_data +); + +/** @brief Affiche les preuves associées à la relation sélectionnée. */ +void main_window_set_relation_evidences( + MainWindow *main_window, + const GPtrArray *evidence_records +); + /** @brief Définit le callback de catégorisation d'une personne. */ void main_window_set_person_role_callback(MainWindow *main_window, MainWindowPersonRoleCallback callback, gpointer user_data); diff --git a/include/widgets/workspace.h b/include/widgets/workspace.h index 16e4d8e..e263596 100644 --- a/include/widgets/workspace.h +++ b/include/widgets/workspace.h @@ -85,6 +85,12 @@ typedef void (*WorkspaceEditRelationCallback)( gpointer user_data ); +/** @brief Callback appelé quand une relation devient sélectionnée. */ +typedef void (*WorkspaceRelationSelectedCallback)( + const char *relation_identifier, + gpointer user_data +); + /** @brief Callback appelé pour catégoriser une personne. */ typedef void (*WorkspacePersonRoleCallback)(const char *entity_identifier, PersonRole role, gpointer user_data); @@ -315,6 +321,30 @@ void workspace_set_edit_relation_callback( gpointer user_data ); +/** + * @brief Définit le callback de sélection d'une relation. + * + * @param workspace Zone de travail. + * @param callback Callback facultatif. + * @param user_data Données empruntées transmises au callback. + */ +void workspace_set_relation_selected_callback( + Workspace *workspace, + WorkspaceRelationSelectedCallback callback, + gpointer user_data +); + +/** + * @brief Affiche les preuves associées à la relation sélectionnée. + * + * @param workspace Zone de travail. + * @param evidence_records Tableau emprunté de EvidenceRecord, ou NULL. + */ +void workspace_set_relation_evidences( + Workspace *workspace, + const GPtrArray *evidence_records +); + /** @brief Définit le callback de catégorisation d'une personne. */ void workspace_set_person_role_callback(Workspace *workspace, WorkspacePersonRoleCallback callback, gpointer user_data); diff --git a/src/core/application.c b/src/core/application.c index fc64a1c..7668e2f 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -41,6 +41,7 @@ #include "dao/evidence_dao.h" #include "dao/entity_dao.h" #include "dao/relation_dao.h" +#include "dao/relation_evidence_dao.h" #include "dao/osint_execution_dao.h" #include "models/evidence_type.h" #include "widgets/evidence_list_model.h" @@ -4873,7 +4874,7 @@ static void application_on_create_relation_completed( if (!relation_service_create( relation_service, relation_record, - NULL, + create_relation_dialog_result_get_evidence_identifiers(result), &error )) { @@ -5020,6 +5021,7 @@ static void application_on_edit_relation_completed( Application *application = context != NULL ? context->application : NULL; Database *database = NULL; RelationDao *relation_dao = NULL; + RelationEvidenceDao *relation_evidence_dao = NULL; RelationRecord *existing = NULL; RelationRecord *updated = NULL; const InvestigationProject *project = NULL; @@ -5033,7 +5035,8 @@ static void application_on_edit_relation_completed( database = investigation_session_get_database(application->session); project = investigation_session_get_project(application->session); relation_dao = relation_dao_new(database, &error); - if (relation_dao == NULL) goto failure; + relation_evidence_dao = relation_evidence_dao_new(database, &error); + if (relation_dao == NULL || relation_evidence_dao == NULL) goto failure; existing = relation_dao_find_by_identifier(relation_dao, context->relation_identifier, &error); if (existing == NULL) goto failure; @@ -5052,9 +5055,47 @@ static void application_on_edit_relation_completed( relation_record_get_status(existing), &error); if (updated == NULL || !database_transaction_begin(database)) goto failure; transaction_active = TRUE; - if (!relation_dao_update(relation_dao, updated, &error) || - !database_transaction_commit(database)) + if (!relation_dao_update(relation_dao, updated, &error)) goto failure; + { + GPtrArray *existing_evidence = + relation_evidence_dao_list_evidence_identifiers( + relation_evidence_dao, context->relation_identifier, &error); + const GPtrArray *selected_evidence = + create_relation_dialog_result_get_evidence_identifiers(result); + if (existing_evidence == NULL) goto failure; + for (guint index = 0; index < existing_evidence->len; index++) + { + const char *identifier = g_ptr_array_index(existing_evidence, index); + gboolean retained = FALSE; + for (guint selected_index = 0; + selected_index < selected_evidence->len; selected_index++) + if (g_strcmp0(identifier, g_ptr_array_index(selected_evidence, + selected_index)) == 0) retained = TRUE; + if (!retained && !relation_evidence_dao_unlink( + relation_evidence_dao, context->relation_identifier, + identifier, &error)) + { + g_ptr_array_unref(existing_evidence); + goto failure; + } + } + for (guint index = 0; index < selected_evidence->len; index++) + { + const char *identifier = g_ptr_array_index(selected_evidence, index); + gboolean exists = FALSE; + if (!relation_evidence_dao_exists(relation_evidence_dao, + context->relation_identifier, identifier, &exists, &error) || + (!exists && !relation_evidence_dao_link(relation_evidence_dao, + context->relation_identifier, identifier, &error))) + { + g_ptr_array_unref(existing_evidence); + goto failure; + } + } + g_ptr_array_unref(existing_evidence); + } + if (!database_transaction_commit(database)) goto failure; transaction_active = FALSE; main_window_set_status(application->main_window, "Relation modifiée. Actualisation du graphe…"); @@ -5077,6 +5118,7 @@ cleanup: relation_record_free(updated); relation_record_free(existing); relation_dao_free(relation_dao); + relation_evidence_dao_free(relation_evidence_dao); create_relation_dialog_result_free(result); application_edit_relation_context_free(context); } @@ -5089,8 +5131,13 @@ static void application_on_edit_relation_requested( Database *database = NULL; RelationDao *relation_dao = NULL; EntityDao *entity_dao = NULL; + EvidenceDao *evidence_dao = NULL; + RelationEvidenceDao *relation_evidence_dao = NULL; RelationRecord *relation = NULL; GPtrArray *entities = NULL; + GPtrArray *evidences = NULL; + GPtrArray *selected_evidences = NULL; + const InvestigationProject *project = NULL; ApplicationEditRelationContext *context = NULL; GError *error = NULL; @@ -5099,16 +5146,27 @@ static void application_on_edit_relation_requested( database = investigation_session_get_database(application->session); relation_dao = relation_dao_new(database, &error); entity_dao = entity_dao_new(database, &error); + evidence_dao = evidence_dao_new(database, &error); + relation_evidence_dao = relation_evidence_dao_new(database, &error); + project = investigation_session_get_project(application->session); if (relation_dao != NULL) relation = relation_dao_find_by_identifier(relation_dao, relation_identifier, &error); if (entity_dao != NULL) entities = entity_dao_list_all(entity_dao, &error); - if (relation == NULL || entities == NULL) goto failure; + if (evidence_dao != NULL) evidences = evidence_dao_list_all(evidence_dao, + &error); + if (relation_evidence_dao != NULL) selected_evidences = + relation_evidence_dao_list_evidence_identifiers(relation_evidence_dao, + relation_identifier, &error); + if (relation == NULL || entities == NULL || evidences == NULL || + selected_evidences == NULL || project == NULL) goto failure; context = g_new0(ApplicationEditRelationContext, 1); context->application = application; context->relation_identifier = g_strdup(relation_identifier); if (context->relation_identifier == NULL || !edit_relation_dialog_present( main_window_get_window(application->main_window), relation, entities, + evidences, selected_evidences, + investigation_project_get_root_path(project), application_on_edit_relation_completed, context, &error)) goto failure; context = NULL; @@ -5121,11 +5179,63 @@ cleanup: application_edit_relation_context_free(context); g_clear_error(&error); g_clear_pointer(&entities, g_ptr_array_unref); + g_clear_pointer(&evidences, g_ptr_array_unref); + g_clear_pointer(&selected_evidences, g_ptr_array_unref); relation_record_free(relation); entity_dao_free(entity_dao); + evidence_dao_free(evidence_dao); + relation_evidence_dao_free(relation_evidence_dao); relation_dao_free(relation_dao); } +/** @brief Charge les preuves liées à la relation affichée dans le volet. */ +static void application_on_relation_selected( + const char *relation_identifier, + gpointer user_data) +{ + Application *application = user_data; + Database *database = NULL; + RelationEvidenceDao *relation_evidence_dao = NULL; + EvidenceDao *evidence_dao = NULL; + GPtrArray *identifiers = NULL; + GPtrArray *records = NULL; + GError *error = NULL; + if (application == NULL || application->session == NULL || + relation_identifier == NULL) return; + database = investigation_session_get_database(application->session); + relation_evidence_dao = relation_evidence_dao_new(database, &error); + if (relation_evidence_dao == NULL) goto cleanup; + evidence_dao = evidence_dao_new(database, &error); + if (evidence_dao == NULL) goto cleanup; + identifiers = relation_evidence_dao_list_evidence_identifiers( + relation_evidence_dao, relation_identifier, &error); + if (identifiers == NULL) goto cleanup; + records = g_ptr_array_new_with_free_func((GDestroyNotify) + evidence_record_free); + if (records == NULL) goto cleanup; + for (guint index = 0; index < identifiers->len; index++) + { + EvidenceRecord *record = evidence_dao_find_by_identifier(evidence_dao, + g_ptr_array_index(identifiers, index), &error); + if (record == NULL) goto cleanup; + g_ptr_array_add(records, record); + } + main_window_set_relation_evidences(application->main_window, records); + +cleanup: + if (error != NULL) + { + g_warning("Impossible de charger les preuves de la relation : %s", + error->message); + main_window_set_relation_evidences(application->main_window, NULL); + } + g_clear_error(&error); + g_clear_pointer(&records, g_ptr_array_unref); + g_clear_pointer(&identifiers, g_ptr_array_unref); + evidence_dao_free(evidence_dao); + relation_evidence_dao_free(relation_evidence_dao); +} + static void application_on_add_relation_requested( const char *source_entity_identifier, gpointer user_data @@ -5140,8 +5250,12 @@ static void application_on_add_relation_requested( EntityDao *entity_dao = NULL; + EvidenceDao *evidence_dao = NULL; + const InvestigationProject *project = NULL; + GPtrArray *entities = NULL; + GPtrArray *evidences = NULL; GError *error = NULL; @@ -5161,6 +5275,7 @@ static void application_on_add_relation_requested( investigation_session_get_database( application->session ); + project = investigation_session_get_project(application->session); if (database == NULL) { @@ -5202,11 +5317,16 @@ static void application_on_add_relation_requested( &error ); + evidence_dao = evidence_dao_new(database, &error); + if (evidence_dao != NULL) + evidences = evidence_dao_list_all(evidence_dao, &error); + evidence_dao_free(evidence_dao); + entity_dao_free( entity_dao ); - if (entities == NULL) + if (entities == NULL || evidences == NULL || project == NULL) { application_present_error( application, @@ -5220,6 +5340,9 @@ static void application_on_add_relation_requested( &error ); + g_clear_pointer(&entities, g_ptr_array_unref); + g_clear_pointer(&evidences, g_ptr_array_unref); + return; } @@ -5229,6 +5352,8 @@ static void application_on_add_relation_requested( ), source_entity_identifier, entities, + evidences, + investigation_project_get_root_path(project), application_on_create_relation_completed, application, &error @@ -5250,6 +5375,7 @@ static void application_on_add_relation_requested( g_ptr_array_unref( entities ); + g_ptr_array_unref(evidences); } /** @@ -6544,6 +6670,8 @@ static void application_on_activate( main_window_set_edit_relation_callback(application->main_window, application_on_edit_relation_requested, application); + main_window_set_relation_selected_callback(application->main_window, + application_on_relation_selected, application); main_window_set_person_role_callback(application->main_window, application_on_person_role_changed, application); diff --git a/src/views/create_relation_dialog.c b/src/views/create_relation_dialog.c index 6f3c725..64188bb 100644 --- a/src/views/create_relation_dialog.c +++ b/src/views/create_relation_dialog.c @@ -6,6 +6,7 @@ #include "views/create_relation_dialog.h" #include "models/entity_record.h" +#include "models/evidence_record.h" #include "models/relation_record.h" #include @@ -21,10 +22,32 @@ struct CreateRelationDialogResult char *relation_type; char *label; char *justification; + GPtrArray *evidence_identifiers; gint confidence; }; +/** @brief Copie privée des données nécessaires à la sélection d'une preuve. */ +typedef struct +{ + char *identifier; + char *name; + char *type; + char *date; + char *sha256; + char *absolute_path; +} CreateRelationEvidenceOption; + +/** @brief Libère une option de preuve. */ +static void create_relation_evidence_option_free(gpointer data) +{ + CreateRelationEvidenceOption *option = data; + if (option == NULL) return; + g_free(option->identifier); g_free(option->name); g_free(option->type); + g_free(option->date); g_free(option->sha256); g_free(option->absolute_path); + g_free(option); +} + /** * @brief État privé conservé pendant l'affichage du dialogue. */ @@ -41,6 +64,13 @@ typedef struct char *source_entity_identifier; GPtrArray *target_identifiers; + GPtrArray *evidence_options; + GPtrArray *evidence_check_buttons; + GtkWidget *evidence_preview_stack; + GtkWidget *evidence_preview_status; + GtkWidget *evidence_preview_picture; + GtkWidget *evidence_preview_video; + GtkWidget *evidence_metadata_label; CreateRelationDialogCallback callback; gpointer callback_data; @@ -263,6 +293,75 @@ static void create_relation_dialog_show_error( ); } +/** @brief Arrête la vidéo utilisée par l'aperçu des preuves. */ +static void create_relation_dialog_clear_preview( + CreateRelationDialogState *state) +{ + GtkMediaStream *stream = NULL; + if (state == NULL || state->evidence_preview_video == NULL || + state->evidence_preview_picture == NULL) return; + stream = gtk_video_get_media_stream(GTK_VIDEO( + state->evidence_preview_video)); + if (stream != NULL) gtk_media_stream_pause(stream); + gtk_video_set_media_stream(GTK_VIDEO(state->evidence_preview_video), NULL); + gtk_picture_set_paintable(GTK_PICTURE( + state->evidence_preview_picture), NULL); +} + +/** @brief Affiche la preuve dont la case vient d'être manipulée. */ +static void create_relation_dialog_on_evidence_toggled(GtkCheckButton *button, + gpointer user_data) +{ + CreateRelationDialogState *state = user_data; + CreateRelationEvidenceOption *option = g_object_get_data( + G_OBJECT(button), "relation-evidence-option"); + GFile *file = NULL; + GFileInfo *info = NULL; + const char *content_type = NULL; + char *mime = NULL; + char *metadata = NULL; + GError *error = NULL; + if (state == NULL || option == NULL) return; + create_relation_dialog_clear_preview(state); + metadata = g_strdup_printf("%s\nType : %s\nDate : %s\nSHA-256 : %s", + option->name, option->type, option->date, option->sha256); + gtk_label_set_text(GTK_LABEL(state->evidence_metadata_label), metadata); + g_free(metadata); + if (option->absolute_path == NULL) goto unsupported; + file = g_file_new_for_path(option->absolute_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 = g_content_type_get_mime_type(content_type); + if (mime != NULL && g_str_has_prefix(mime, "image/")) + { + gtk_picture_set_filename(GTK_PICTURE(state->evidence_preview_picture), + option->absolute_path); + gtk_stack_set_visible_child_name(GTK_STACK( + state->evidence_preview_stack), "image"); + } + else if (mime != NULL && g_str_has_prefix(mime, "video/")) + { + GtkMediaStream *stream = gtk_media_file_new_for_filename( + option->absolute_path); + gtk_video_set_media_stream(GTK_VIDEO(state->evidence_preview_video), + stream); + g_object_unref(stream); + gtk_stack_set_visible_child_name(GTK_STACK( + state->evidence_preview_stack), "video"); + } + else + { +unsupported: + gtk_label_set_text(GTK_LABEL(state->evidence_preview_status), + "Aperçu indisponible pour ce format."); + gtk_stack_set_visible_child_name(GTK_STACK( + state->evidence_preview_stack), "status"); + } + g_clear_error(&error); g_free(mime); + g_clear_object(&info); g_clear_object(&file); +} + /** * @brief Libère l'état privé du dialogue. */ @@ -282,6 +381,9 @@ static void create_relation_dialog_state_free( &state->target_identifiers, g_ptr_array_unref ); + create_relation_dialog_clear_preview(state); + g_clear_pointer(&state->evidence_options, g_ptr_array_unref); + g_clear_pointer(&state->evidence_check_buttons, g_ptr_array_unref); g_free( state->source_entity_identifier @@ -529,13 +631,32 @@ static void create_relation_dialog_on_create_clicked( result->confidence = confidence; + result->evidence_identifiers = g_ptr_array_new_with_free_func(g_free); + if (result->evidence_identifiers != NULL) + { + guint evidence_index = 0; + for (evidence_index = 0; + evidence_index < state->evidence_check_buttons->len; + evidence_index++) + { + GtkCheckButton *check_button = g_ptr_array_index( + state->evidence_check_buttons, evidence_index); + CreateRelationEvidenceOption *option = g_object_get_data( + G_OBJECT(check_button), "relation-evidence-option"); + if (gtk_check_button_get_active(check_button) && option != NULL) + g_ptr_array_add(result->evidence_identifiers, + g_strdup(option->identifier)); + } + } + g_free( justification_text ); if (result->source_entity_identifier == NULL || result->target_entity_identifier == NULL || - result->relation_type == NULL) + result->relation_type == NULL || + result->evidence_identifiers == NULL) { create_relation_dialog_result_free( result @@ -689,6 +810,9 @@ static gboolean create_relation_dialog_present_internal( const char *source_entity_identifier, const RelationRecord *existing_relation, const GPtrArray *entities, + const GPtrArray *evidence_records, + const GPtrArray *selected_evidence_identifiers, + const char *investigation_root_path, CreateRelationDialogCallback callback, gpointer user_data, GError **error @@ -733,6 +857,12 @@ static gboolean create_relation_dialog_present_internal( GtkWidget *justification_scrolled_window = NULL; + GtkWidget *evidence_box = NULL; + GtkWidget *evidence_list = NULL; + GtkWidget *evidence_list_scrolled = NULL; + GtkWidget *evidence_preview_box = NULL; + GtkWidget *evidence_title = NULL; + GtkWidget *button_box = NULL; @@ -764,7 +894,8 @@ static gboolean create_relation_dialog_present_internal( !g_uuid_string_is_valid( source_entity_identifier ) || - entities == NULL || + entities == NULL || evidence_records == NULL || + investigation_root_path == NULL || callback == NULL) { g_set_error_literal( @@ -804,6 +935,10 @@ static gboolean create_relation_dialog_present_internal( g_ptr_array_new_with_free_func( g_free ); + state->evidence_options = g_ptr_array_new_with_free_func( + create_relation_evidence_option_free); + state->evidence_check_buttons = g_ptr_array_new_with_free_func( + g_object_unref); state->callback = callback; @@ -817,7 +952,8 @@ static gboolean create_relation_dialog_present_internal( ); if (state->source_entity_identifier == NULL || - state->target_identifiers == NULL || + state->target_identifiers == NULL || state->evidence_options == NULL || + state->evidence_check_buttons == NULL || target_labels == NULL) { g_clear_object( @@ -976,6 +1112,43 @@ static gboolean create_relation_dialog_present_internal( return FALSE; } + for (guint evidence_index = 0; evidence_index < evidence_records->len; + evidence_index++) + { + const EvidenceRecord *record = g_ptr_array_index(evidence_records, + evidence_index); + CreateRelationEvidenceOption *option = NULL; + char *candidate = NULL; + char *canonical_root = NULL; + char *canonical_path = NULL; + const char *date = NULL; + if (record == NULL) continue; + option = g_try_new0(CreateRelationEvidenceOption, 1); + if (option == NULL) continue; + option->identifier = g_strdup(evidence_record_get_identifier(record)); + option->name = g_strdup(evidence_record_get_original_name(record)); + option->type = g_strdup(evidence_record_get_type_identifier(record)); + date = evidence_record_get_collected_at(record); + if (date == NULL) date = evidence_record_get_imported_at(record); + option->date = g_strdup(date != NULL ? date : "Date inconnue"); + option->sha256 = g_strdup(evidence_record_get_sha256(record)); + candidate = g_build_filename(investigation_root_path, + evidence_record_get_relative_path(record), NULL); + canonical_root = g_canonicalize_filename(investigation_root_path, NULL); + canonical_path = g_canonicalize_filename(candidate, NULL); + if (canonical_root != NULL && canonical_path != NULL && + g_str_has_prefix(canonical_path, canonical_root) && + (canonical_path[strlen(canonical_root)] == G_DIR_SEPARATOR || + canonical_path[strlen(canonical_root)] == '\0')) + option->absolute_path = g_strdup(canonical_path); + g_free(canonical_path); g_free(canonical_root); g_free(candidate); + if (option->identifier == NULL || option->name == NULL || + option->type == NULL || option->sha256 == NULL) + create_relation_evidence_option_free(option); + else + g_ptr_array_add(state->evidence_options, option); + } + state->window = GTK_WINDOW( gtk_window_new() @@ -1005,8 +1178,8 @@ static gboolean create_relation_dialog_present_internal( gtk_window_set_default_size( state->window, - 640, - 560 + 920, + 780 ); root_box = @@ -1299,6 +1472,73 @@ static gboolean create_relation_dialog_present_internal( 1 ); + evidence_title = gtk_label_new("Pièces jointes à la relation"); + gtk_label_set_xalign(GTK_LABEL(evidence_title), 0.0F); + evidence_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12); + evidence_list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + evidence_list_scrolled = gtk_scrolled_window_new(); + gtk_widget_set_size_request(evidence_list_scrolled, 360, 240); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(evidence_list_scrolled), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(evidence_list_scrolled), + evidence_list); + for (guint evidence_index = 0; + evidence_index < state->evidence_options->len; evidence_index++) + { + CreateRelationEvidenceOption *option = g_ptr_array_index( + state->evidence_options, evidence_index); + char *display = g_strdup_printf("%s — %s — %s", option->name, + option->type, option->date); + GtkWidget *check = gtk_check_button_new_with_label(display); + gboolean active = FALSE; + g_free(display); + if (selected_evidence_identifiers != NULL) + for (guint selected_index = 0; + selected_index < selected_evidence_identifiers->len; + selected_index++) + if (g_strcmp0(option->identifier, g_ptr_array_index( + selected_evidence_identifiers, selected_index)) == 0) + active = TRUE; + gtk_check_button_set_active(GTK_CHECK_BUTTON(check), active); + g_object_set_data(G_OBJECT(check), "relation-evidence-option", option); + g_signal_connect(check, "toggled", G_CALLBACK( + create_relation_dialog_on_evidence_toggled), state); + gtk_box_append(GTK_BOX(evidence_list), check); + g_ptr_array_add(state->evidence_check_buttons, g_object_ref(check)); + } + if (state->evidence_options->len == 0) + gtk_box_append(GTK_BOX(evidence_list), gtk_label_new( + "Aucune preuve enregistrée dans l’enquête.")); + + evidence_preview_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_widget_set_hexpand(evidence_preview_box, TRUE); + state->evidence_metadata_label = gtk_label_new( + "Sélectionnez une preuve pour afficher son aperçu."); + gtk_label_set_xalign(GTK_LABEL(state->evidence_metadata_label), 0.0F); + gtk_label_set_wrap(GTK_LABEL(state->evidence_metadata_label), TRUE); + gtk_label_set_selectable(GTK_LABEL(state->evidence_metadata_label), TRUE); + state->evidence_preview_stack = gtk_stack_new(); + gtk_widget_set_size_request(state->evidence_preview_stack, 420, 220); + state->evidence_preview_status = gtk_label_new("Aucun aperçu."); + state->evidence_preview_picture = gtk_picture_new(); + gtk_picture_set_content_fit(GTK_PICTURE(state->evidence_preview_picture), + GTK_CONTENT_FIT_CONTAIN); + state->evidence_preview_video = gtk_video_new(); + gtk_video_set_autoplay(GTK_VIDEO(state->evidence_preview_video), FALSE); + gtk_stack_add_named(GTK_STACK(state->evidence_preview_stack), + state->evidence_preview_status, "status"); + gtk_stack_add_named(GTK_STACK(state->evidence_preview_stack), + state->evidence_preview_picture, "image"); + gtk_stack_add_named(GTK_STACK(state->evidence_preview_stack), + state->evidence_preview_video, "video"); + gtk_box_append(GTK_BOX(evidence_preview_box), + state->evidence_metadata_label); + gtk_box_append(GTK_BOX(evidence_preview_box), state->evidence_preview_stack); + gtk_box_append(GTK_BOX(evidence_box), evidence_list_scrolled); + gtk_box_append(GTK_BOX(evidence_box), evidence_preview_box); + gtk_grid_attach(GTK_GRID(grid), evidence_title, 0, 5, 2, 1); + gtk_grid_attach(GTK_GRID(grid), evidence_box, 0, 6, 2, 1); + gtk_grid_attach( GTK_GRID( grid @@ -1582,14 +1822,19 @@ static gboolean create_relation_dialog_present_internal( gboolean create_relation_dialog_present(GtkWindow *parent, const char *source_entity_identifier, const GPtrArray *entities, + const GPtrArray *evidence_records, const char *investigation_root_path, CreateRelationDialogCallback callback, gpointer user_data, GError **error) { return create_relation_dialog_present_internal(parent, - source_entity_identifier, NULL, entities, callback, user_data, error); + source_entity_identifier, NULL, entities, evidence_records, NULL, + investigation_root_path, callback, user_data, error); } gboolean edit_relation_dialog_present(GtkWindow *parent, const RelationRecord *relation_record, const GPtrArray *entities, + const GPtrArray *evidence_records, + const GPtrArray *selected_evidence_identifiers, + const char *investigation_root_path, CreateRelationDialogCallback callback, gpointer user_data, GError **error) { if (relation_record == NULL) @@ -1601,7 +1846,9 @@ gboolean edit_relation_dialog_present(GtkWindow *parent, } return create_relation_dialog_present_internal(parent, relation_record_get_source_entity_identifier(relation_record), - relation_record, entities, callback, user_data, error); + relation_record, entities, evidence_records, + selected_evidence_identifiers, investigation_root_path, + callback, user_data, error); } void create_relation_dialog_result_free( @@ -1633,11 +1880,19 @@ void create_relation_dialog_result_free( result->source_entity_identifier ); + g_clear_pointer(&result->evidence_identifiers, g_ptr_array_unref); + g_free( result ); } +const GPtrArray *create_relation_dialog_result_get_evidence_identifiers( + const CreateRelationDialogResult *result) +{ + return result != NULL ? result->evidence_identifiers : NULL; +} + const char *create_relation_dialog_result_get_source_identifier( const CreateRelationDialogResult *result ) diff --git a/src/views/main_window.c b/src/views/main_window.c index 26c2ab2..025065d 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -1672,6 +1672,21 @@ void main_window_set_edit_relation_callback(MainWindow *main_window, main_window->edit_relation_user_data = user_data; } +void main_window_set_relation_selected_callback(MainWindow *main_window, + MainWindowRelationSelectedCallback callback, gpointer user_data) +{ + if (main_window == NULL) return; + workspace_set_relation_selected_callback(main_window->workspace, + (WorkspaceRelationSelectedCallback) callback, user_data); +} + +void main_window_set_relation_evidences(MainWindow *main_window, + const GPtrArray *evidence_records) +{ + if (main_window == NULL) return; + workspace_set_relation_evidences(main_window->workspace, evidence_records); +} + void main_window_set_person_role_callback(MainWindow *main_window, MainWindowPersonRoleCallback callback, gpointer user_data) { diff --git a/src/widgets/workspace.c b/src/widgets/workspace.c index 3b8ac4b..058ce01 100644 --- a/src/widgets/workspace.c +++ b/src/widgets/workspace.c @@ -67,6 +67,7 @@ struct Workspace 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; @@ -130,6 +131,8 @@ struct Workspace 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; @@ -804,6 +807,11 @@ static void workspace_on_graph_node_selected( 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( @@ -1677,6 +1685,8 @@ Workspace *workspace_new(void) ); 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"); @@ -1686,6 +1696,7 @@ Workspace *workspace_new(void) 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) { @@ -1732,6 +1743,13 @@ Workspace *workspace_new(void) 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 @@ -1745,6 +1763,8 @@ Workspace *workspace_new(void) 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", @@ -3195,6 +3215,41 @@ void workspace_set_edit_relation_callback(Workspace *workspace, 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) {