/****************************************************************************** * @file create_relation_dialog.c * @brief Dialogue GTK de préparation d'une relation entre deux entités. ******************************************************************************/ #include "views/create_relation_dialog.h" #include "models/entity_record.h" #include "models/evidence_record.h" #include "models/relation_record.h" #include /** * @struct CreateRelationDialogResult * @brief Valeurs validées par le dialogue. */ struct CreateRelationDialogResult { char *source_entity_identifier; char *target_entity_identifier; 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. */ typedef struct { GtkWindow *window; GtkDropDown *target_drop_down; GtkEntry *relation_type_entry; GtkEntry *label_entry; GtkTextView *justification_text_view; GtkSpinButton *confidence_spin_button; GtkLabel *error_label; 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; gboolean completed; } CreateRelationDialogState; /** * @brief Retourne une copie nettoyée ou NULL pour une valeur vide. */ static char *create_relation_dialog_duplicate_optional_text( const char *value ) { char *copy = NULL; if (value == NULL) { return NULL; } copy = g_strdup( value ); if (copy == NULL) { return NULL; } g_strstrip( copy ); if (copy[0] == '\0') { g_free( copy ); return NULL; } return copy; } /** * @brief Retourne une copie nettoyée d'un texte obligatoire. */ static char *create_relation_dialog_duplicate_required_text( const char *value ) { char *copy = create_relation_dialog_duplicate_optional_text( value ); return copy; } /** * @brief Retourne le texte principal d'une entité. */ static const char *create_relation_dialog_get_entity_title( const EntityRecord *entity_record ) { const char *value = NULL; if (entity_record == NULL) { return NULL; } value = entity_record_get_label( entity_record ); if (value != NULL && value[0] != '\0') { return value; } value = entity_record_get_value( entity_record ); if (value != NULL && value[0] != '\0') { return value; } return entity_record_get_identifier( entity_record ); } /** * @brief Construit le libellé d'une entité dans une liste. */ static char *create_relation_dialog_duplicate_entity_display( const EntityRecord *entity_record ) { const char *title = NULL; const char *type_identifier = NULL; if (entity_record == NULL) { return NULL; } title = create_relation_dialog_get_entity_title( entity_record ); type_identifier = entity_record_get_type_identifier( entity_record ); if (title == NULL || title[0] == '\0') { return NULL; } if (type_identifier == NULL || type_identifier[0] == '\0') { return g_strdup( title ); } return g_strdup_printf( "%s — %s", title, type_identifier ); } /** * @brief Retourne le texte complet de la justification. */ static char *create_relation_dialog_get_justification_text( CreateRelationDialogState *state ) { GtkTextBuffer *buffer = NULL; GtkTextIter start; GtkTextIter end; if (state == NULL || state->justification_text_view == NULL) { return NULL; } buffer = gtk_text_view_get_buffer( state->justification_text_view ); gtk_text_buffer_get_bounds( buffer, &start, &end ); return gtk_text_buffer_get_text( buffer, &start, &end, FALSE ); } /** * @brief Affiche une erreur de validation dans le dialogue. */ static void create_relation_dialog_show_error( CreateRelationDialogState *state, const char *message ) { if (state == NULL || state->error_label == NULL) { return; } gtk_label_set_text( state->error_label, message != NULL && message[0] != '\0' ? message : "Les informations de la relation sont invalides." ); gtk_widget_set_visible( GTK_WIDGET( state->error_label ), TRUE ); } /** @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. */ static void create_relation_dialog_state_free( gpointer user_data ) { CreateRelationDialogState *state = user_data; if (state == NULL) { return; } g_clear_pointer( &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 ); g_free( state ); } /** * @brief Termine le dialogue avec une annulation. */ static void create_relation_dialog_cancel( CreateRelationDialogState *state ) { if (state == NULL || state->completed) { return; } state->completed = TRUE; if (state->callback != NULL) { state->callback( NULL, state->callback_data ); } } /** * @brief Traite la fermeture native de la fenêtre. */ static gboolean create_relation_dialog_on_close_requested( GtkWindow *window, gpointer user_data ) { CreateRelationDialogState *state = user_data; (void) window; create_relation_dialog_cancel( state ); return FALSE; } /** * @brief Traite le bouton Annuler. */ static void create_relation_dialog_on_cancel_clicked( GtkButton *button, gpointer user_data ) { CreateRelationDialogState *state = user_data; (void) button; if (state == NULL) { return; } create_relation_dialog_cancel( state ); gtk_window_close( state->window ); } /** * @brief Traite la validation du formulaire. */ static void create_relation_dialog_on_create_clicked( GtkButton *button, gpointer user_data ) { CreateRelationDialogState *state = user_data; CreateRelationDialogResult *result = NULL; guint selected_index = GTK_INVALID_LIST_POSITION; const char *target_identifier = NULL; const char *relation_type = NULL; const char *label = NULL; char *justification_text = NULL; gint confidence = 0; GError *validation_error = NULL; (void) button; if (state == NULL || state->completed) { return; } selected_index = gtk_drop_down_get_selected( state->target_drop_down ); if (selected_index == GTK_INVALID_LIST_POSITION || selected_index >= state->target_identifiers->len) { create_relation_dialog_show_error( state, "Sélectionnez une entité cible." ); return; } target_identifier = g_ptr_array_index( state->target_identifiers, selected_index ); relation_type = gtk_editable_get_text( GTK_EDITABLE( state->relation_type_entry ) ); label = gtk_editable_get_text( GTK_EDITABLE( state->label_entry ) ); confidence = gtk_spin_button_get_value_as_int( state->confidence_spin_button ); justification_text = create_relation_dialog_get_justification_text( state ); if (!create_relation_dialog_validate_values( state->source_entity_identifier, target_identifier, relation_type, confidence, &validation_error )) { create_relation_dialog_show_error( state, validation_error != NULL ? validation_error->message : "Les informations de la relation sont invalides." ); g_clear_error( &validation_error ); g_free( justification_text ); return; } result = g_try_new0( CreateRelationDialogResult, 1 ); if (result == NULL) { create_relation_dialog_show_error( state, "Impossible d'allouer le résultat du dialogue." ); g_free( justification_text ); return; } result->source_entity_identifier = g_strdup( state->source_entity_identifier ); result->target_entity_identifier = g_strdup( target_identifier ); result->relation_type = create_relation_dialog_duplicate_required_text( relation_type ); result->label = create_relation_dialog_duplicate_optional_text( label ); result->justification = create_relation_dialog_duplicate_optional_text( justification_text ); 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->evidence_identifiers == NULL) { create_relation_dialog_result_free( result ); create_relation_dialog_show_error( state, "Impossible de conserver les informations de la relation." ); return; } state->completed = TRUE; if (state->callback != NULL) { state->callback( result, state->callback_data ); } else { create_relation_dialog_result_free( result ); } gtk_window_close( state->window ); } GQuark create_relation_dialog_error_quark(void) { return g_quark_from_static_string( "create-relation-dialog-error-quark" ); } gboolean create_relation_dialog_validate_values( const char *source_entity_identifier, const char *target_entity_identifier, const char *relation_type, gint confidence, GError **error ) { char *normalized_relation_type = NULL; gboolean valid = FALSE; g_return_val_if_fail( error == NULL || *error == NULL, FALSE ); if (source_entity_identifier == NULL || !g_uuid_string_is_valid( source_entity_identifier )) { g_set_error_literal( error, CREATE_RELATION_DIALOG_ERROR, CREATE_RELATION_DIALOG_ERROR_INVALID_DATA, "L'identifiant de l'entité source est invalide." ); return FALSE; } if (target_entity_identifier == NULL || !g_uuid_string_is_valid( target_entity_identifier )) { g_set_error_literal( error, CREATE_RELATION_DIALOG_ERROR, CREATE_RELATION_DIALOG_ERROR_INVALID_DATA, "L'identifiant de l'entité cible est invalide." ); return FALSE; } if (g_strcmp0( source_entity_identifier, target_entity_identifier ) == 0) { g_set_error_literal( error, CREATE_RELATION_DIALOG_ERROR, CREATE_RELATION_DIALOG_ERROR_INVALID_DATA, "Une entité ne peut pas être reliée à elle-même." ); return FALSE; } normalized_relation_type = create_relation_dialog_duplicate_required_text( relation_type ); if (normalized_relation_type == NULL) { g_set_error_literal( error, CREATE_RELATION_DIALOG_ERROR, CREATE_RELATION_DIALOG_ERROR_INVALID_DATA, "Le type de relation est obligatoire." ); return FALSE; } if (confidence < 0 || confidence > 100) { g_set_error_literal( error, CREATE_RELATION_DIALOG_ERROR, CREATE_RELATION_DIALOG_ERROR_INVALID_DATA, "Le niveau de confiance doit être compris entre 0 et 100." ); goto cleanup; } valid = TRUE; cleanup: g_free( normalized_relation_type ); return valid; } static gboolean create_relation_dialog_present_internal( GtkWindow *parent, 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 ) { CreateRelationDialogState *state = NULL; GtkStringList *target_labels = NULL; GtkWidget *root_box = NULL; GtkWidget *title_label = NULL; GtkWidget *source_title_label = NULL; GtkWidget *source_value_label = NULL; GtkWidget *grid = NULL; GtkWidget *target_label = NULL; GtkWidget *relation_type_label = NULL; GtkWidget *label_label = NULL; GtkWidget *confidence_label = NULL; GtkWidget *justification_label = NULL; 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; GtkWidget *cancel_button = NULL; GtkWidget *create_button = NULL; const char *source_display = source_entity_identifier; guint entity_index = 0; guint selected_target_index = 0; g_return_val_if_fail( error == NULL || *error == NULL, FALSE ); if (parent == NULL || !GTK_IS_WINDOW( parent ) || source_entity_identifier == NULL || !g_uuid_string_is_valid( source_entity_identifier ) || entities == NULL || evidence_records == NULL || investigation_root_path == NULL || callback == NULL) { g_set_error_literal( error, CREATE_RELATION_DIALOG_ERROR, CREATE_RELATION_DIALOG_ERROR_INVALID_ARGUMENT, "Les arguments du dialogue de relation sont invalides." ); return FALSE; } state = g_try_new0( CreateRelationDialogState, 1 ); if (state == NULL) { g_set_error_literal( error, CREATE_RELATION_DIALOG_ERROR, CREATE_RELATION_DIALOG_ERROR_MEMORY, "Impossible d'allouer l'état du dialogue." ); return FALSE; } state->source_entity_identifier = g_strdup( source_entity_identifier ); state->target_identifiers = 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; state->callback_data = user_data; target_labels = gtk_string_list_new( NULL ); if (state->source_entity_identifier == NULL || state->target_identifiers == NULL || state->evidence_options == NULL || state->evidence_check_buttons == NULL || target_labels == NULL) { g_clear_object( &target_labels ); create_relation_dialog_state_free( state ); g_set_error_literal( error, CREATE_RELATION_DIALOG_ERROR, CREATE_RELATION_DIALOG_ERROR_MEMORY, "Impossible de préparer les listes du dialogue." ); return FALSE; } for (entity_index = 0; entity_index < entities->len; entity_index++) { const EntityRecord *entity_record = g_ptr_array_index( entities, entity_index ); const char *entity_identifier = NULL; char *display_text = NULL; char *identifier_copy = NULL; if (entity_record == NULL) { continue; } entity_identifier = entity_record_get_identifier( entity_record ); if (entity_identifier == NULL || !g_uuid_string_is_valid( entity_identifier )) { continue; } if (g_strcmp0( entity_identifier, source_entity_identifier ) == 0) { const char *candidate_source_display = create_relation_dialog_get_entity_title( entity_record ); if (candidate_source_display != NULL && candidate_source_display[0] != '\0') { source_display = candidate_source_display; } continue; } display_text = create_relation_dialog_duplicate_entity_display( entity_record ); identifier_copy = g_strdup( entity_identifier ); if (existing_relation != NULL && g_strcmp0(entity_identifier, relation_record_get_target_entity_identifier( existing_relation)) == 0) { selected_target_index = state->target_identifiers->len; } if (display_text == NULL || identifier_copy == NULL) { g_free( identifier_copy ); g_free( display_text ); g_object_unref( target_labels ); create_relation_dialog_state_free( state ); g_set_error_literal( error, CREATE_RELATION_DIALOG_ERROR, CREATE_RELATION_DIALOG_ERROR_MEMORY, "Impossible de copier une entité cible." ); return FALSE; } gtk_string_list_append( target_labels, display_text ); g_ptr_array_add( state->target_identifiers, identifier_copy ); g_free( display_text ); } if (state->target_identifiers->len == 0) { g_object_unref( target_labels ); create_relation_dialog_state_free( state ); g_set_error_literal( error, CREATE_RELATION_DIALOG_ERROR, CREATE_RELATION_DIALOG_ERROR_EMPTY_TARGETS, "Aucune autre entité ne peut être utilisée comme cible." ); 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() ); gtk_window_set_title( state->window, existing_relation != NULL ? "Modifier la relation" : "Ajouter une relation" ); gtk_window_set_transient_for( state->window, parent ); gtk_window_set_modal( state->window, TRUE ); gtk_window_set_destroy_with_parent( state->window, TRUE ); gtk_window_set_default_size( state->window, 920, 780 ); root_box = gtk_box_new( GTK_ORIENTATION_VERTICAL, 12 ); gtk_widget_set_margin_top( root_box, 18 ); gtk_widget_set_margin_bottom( root_box, 18 ); gtk_widget_set_margin_start( root_box, 18 ); gtk_widget_set_margin_end( root_box, 18 ); title_label = gtk_label_new( NULL ); gtk_label_set_markup( GTK_LABEL( title_label ), existing_relation != NULL ? "Modifier la relation" : "Créer une relation entre deux entités" ); gtk_label_set_xalign( GTK_LABEL( title_label ), 0.0F ); source_title_label = gtk_label_new( "Entité source" ); gtk_label_set_xalign( GTK_LABEL( source_title_label ), 0.0F ); source_value_label = gtk_label_new( source_display ); gtk_label_set_xalign( GTK_LABEL( source_value_label ), 0.0F ); gtk_label_set_wrap( GTK_LABEL( source_value_label ), TRUE ); gtk_label_set_selectable( GTK_LABEL( source_value_label ), TRUE ); grid = gtk_grid_new(); gtk_grid_set_row_spacing( GTK_GRID( grid ), 10 ); gtk_grid_set_column_spacing( GTK_GRID( grid ), 12 ); gtk_widget_set_vexpand( grid, TRUE ); target_label = gtk_label_new( "Entité cible" ); relation_type_label = gtk_label_new( "Type de relation" ); label_label = gtk_label_new( "Libellé" ); confidence_label = gtk_label_new( "Confiance" ); justification_label = gtk_label_new( "Justification" ); gtk_label_set_xalign( GTK_LABEL( target_label ), 0.0F ); gtk_label_set_xalign( GTK_LABEL( relation_type_label ), 0.0F ); gtk_label_set_xalign( GTK_LABEL( label_label ), 0.0F ); gtk_label_set_xalign( GTK_LABEL( confidence_label ), 0.0F ); gtk_label_set_xalign( GTK_LABEL( justification_label ), 0.0F ); state->target_drop_down = GTK_DROP_DOWN( gtk_drop_down_new( G_LIST_MODEL( target_labels ), NULL ) ); /* * Le constructeur prend possession de la référence du modèle. * Le dialogue ne doit donc pas appeler g_object_unref() ici. */ target_labels = NULL; gtk_drop_down_set_selected( state->target_drop_down, selected_target_index ); gtk_widget_set_sensitive(GTK_WIDGET(state->target_drop_down), existing_relation == NULL); gtk_widget_set_hexpand( GTK_WIDGET( state->target_drop_down ), TRUE ); state->relation_type_entry = GTK_ENTRY( gtk_entry_new() ); gtk_entry_set_placeholder_text( state->relation_type_entry, "Ex. : utilise, contrôle, appartient à" ); state->label_entry = GTK_ENTRY( gtk_entry_new() ); gtk_entry_set_placeholder_text( state->label_entry, "Libellé facultatif affiché dans le graphe" ); state->confidence_spin_button = GTK_SPIN_BUTTON( gtk_spin_button_new_with_range( 0.0, 100.0, 1.0 ) ); gtk_spin_button_set_value( state->confidence_spin_button, existing_relation != NULL ? relation_record_get_confidence(existing_relation) : 80.0 ); state->justification_text_view = GTK_TEXT_VIEW( gtk_text_view_new() ); if (existing_relation != NULL) { const char *relation_type = relation_record_get_relation_type( existing_relation); const char *label = relation_record_get_label(existing_relation); const char *justification = relation_record_get_justification( existing_relation); gtk_editable_set_text(GTK_EDITABLE(state->relation_type_entry), relation_type != NULL ? relation_type : ""); gtk_editable_set_text(GTK_EDITABLE(state->label_entry), label != NULL ? label : ""); gtk_text_buffer_set_text(gtk_text_view_get_buffer( state->justification_text_view), justification != NULL ? justification : "", -1); } gtk_text_view_set_wrap_mode( state->justification_text_view, GTK_WRAP_WORD_CHAR ); justification_scrolled_window = gtk_scrolled_window_new(); gtk_scrolled_window_set_child( GTK_SCROLLED_WINDOW( justification_scrolled_window ), GTK_WIDGET( state->justification_text_view ) ); gtk_widget_set_size_request( justification_scrolled_window, -1, 140 ); gtk_grid_attach( GTK_GRID( grid ), target_label, 0, 0, 1, 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 ), GTK_WIDGET( state->target_drop_down ), 1, 0, 1, 1 ); gtk_grid_attach( GTK_GRID( grid ), relation_type_label, 0, 1, 1, 1 ); gtk_grid_attach( GTK_GRID( grid ), GTK_WIDGET( state->relation_type_entry ), 1, 1, 1, 1 ); gtk_grid_attach( GTK_GRID( grid ), label_label, 0, 2, 1, 1 ); gtk_grid_attach( GTK_GRID( grid ), GTK_WIDGET( state->label_entry ), 1, 2, 1, 1 ); gtk_grid_attach( GTK_GRID( grid ), confidence_label, 0, 3, 1, 1 ); gtk_grid_attach( GTK_GRID( grid ), GTK_WIDGET( state->confidence_spin_button ), 1, 3, 1, 1 ); gtk_grid_attach( GTK_GRID( grid ), justification_label, 0, 4, 1, 1 ); gtk_grid_attach( GTK_GRID( grid ), justification_scrolled_window, 1, 4, 1, 1 ); state->error_label = GTK_LABEL( gtk_label_new( NULL ) ); gtk_label_set_xalign( state->error_label, 0.0F ); gtk_label_set_wrap( state->error_label, TRUE ); gtk_widget_add_css_class( GTK_WIDGET( state->error_label ), "error" ); gtk_widget_set_visible( GTK_WIDGET( state->error_label ), FALSE ); button_box = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 8 ); gtk_widget_set_halign( button_box, GTK_ALIGN_END ); cancel_button = gtk_button_new_with_label( "Annuler" ); create_button = gtk_button_new_with_label( existing_relation != NULL ? "Enregistrer" : "Ajouter" ); gtk_widget_add_css_class( create_button, "suggested-action" ); gtk_box_append( GTK_BOX( button_box ), cancel_button ); gtk_box_append( GTK_BOX( button_box ), create_button ); gtk_box_append( GTK_BOX( root_box ), title_label ); gtk_box_append( GTK_BOX( root_box ), source_title_label ); gtk_box_append( GTK_BOX( root_box ), source_value_label ); gtk_box_append( GTK_BOX( root_box ), grid ); gtk_box_append( GTK_BOX( root_box ), GTK_WIDGET( state->error_label ) ); gtk_box_append( GTK_BOX( root_box ), button_box ); gtk_window_set_child( state->window, root_box ); g_object_set_data_full( G_OBJECT( state->window ), "labfy-create-relation-dialog-state", state, create_relation_dialog_state_free ); g_signal_connect( state->window, "close-request", G_CALLBACK( create_relation_dialog_on_close_requested ), state ); g_signal_connect( cancel_button, "clicked", G_CALLBACK( create_relation_dialog_on_cancel_clicked ), state ); g_signal_connect( create_button, "clicked", G_CALLBACK( create_relation_dialog_on_create_clicked ), state ); gtk_window_set_default_widget( state->window, create_button ); gtk_widget_grab_focus( GTK_WIDGET( state->relation_type_entry ) ); gtk_window_present( state->window ); return TRUE; } 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, 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) { g_set_error_literal(error, CREATE_RELATION_DIALOG_ERROR, CREATE_RELATION_DIALOG_ERROR_INVALID_ARGUMENT, "La relation à modifier est invalide."); return FALSE; } return create_relation_dialog_present_internal(parent, relation_record_get_source_entity_identifier(relation_record), relation_record, entities, evidence_records, selected_evidence_identifiers, investigation_root_path, callback, user_data, error); } void create_relation_dialog_result_free( CreateRelationDialogResult *result ) { if (result == NULL) { return; } g_free( result->justification ); g_free( result->label ); g_free( result->relation_type ); g_free( result->target_entity_identifier ); g_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 ) { return result != NULL ? result->source_entity_identifier : NULL; } const char *create_relation_dialog_result_get_target_identifier( const CreateRelationDialogResult *result ) { return result != NULL ? result->target_entity_identifier : NULL; } const char *create_relation_dialog_result_get_relation_type( const CreateRelationDialogResult *result ) { return result != NULL ? result->relation_type : NULL; } const char *create_relation_dialog_result_get_label( const CreateRelationDialogResult *result ) { return result != NULL ? result->label : NULL; } const char *create_relation_dialog_result_get_justification( const CreateRelationDialogResult *result ) { return result != NULL ? result->justification : NULL; } gint create_relation_dialog_result_get_confidence( const CreateRelationDialogResult *result ) { return result != NULL ? result->confidence : 0; }