labfy-investigation/src/views/create_relation_dialog.c
2026-07-22 10:25:59 +02:00

1632 lines
29 KiB
C

/******************************************************************************
* @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 <glib.h>
/**
* @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;
gint confidence;
};
/**
* @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;
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 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
);
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;
g_free(
justification_text
);
if (result->source_entity_identifier == NULL ||
result->target_entity_identifier == NULL ||
result->relation_type == 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;
}
gboolean create_relation_dialog_present(
GtkWindow *parent,
const char *source_entity_identifier,
const GPtrArray *entities,
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 *button_box =
NULL;
GtkWidget *cancel_button =
NULL;
GtkWidget *create_button =
NULL;
const char *source_display =
source_entity_identifier;
guint entity_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 ||
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->callback =
callback;
state->callback_data =
user_data;
target_labels =
gtk_string_list_new(
NULL
);
if (state->source_entity_identifier == NULL ||
state->target_identifiers == 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 (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;
}
state->window =
GTK_WINDOW(
gtk_window_new()
);
gtk_window_set_title(
state->window,
"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,
640,
560
);
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
),
"<b>Créer une relation entre deux entités</b>"
);
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,
0
);
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,
80.0
);
state->justification_text_view =
GTK_TEXT_VIEW(
gtk_text_view_new()
);
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
);
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(
"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;
}
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_free(
result
);
}
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;
}