labfy-investigation/src/views/create_relation_dialog.c
grayTerminal-sh bf56b09422 Ajouter les relations déplaçables dans le graphe
Affiche les relations comme des nœuds indépendants et persistants.
  Généralise le stockage des positions aux entités et aux relations.
  Finalise la création de relation et le rafraîchissement du graphe.
2026-07-22 10:15:05 +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;
}