diff --git a/include/views/create_relation_dialog.h b/include/views/create_relation_dialog.h new file mode 100644 index 0000000..909f26b --- /dev/null +++ b/include/views/create_relation_dialog.h @@ -0,0 +1,160 @@ +/****************************************************************************** + * @file create_relation_dialog.h + * @brief Dialogue GTK de préparation d'une relation entre deux entités. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_CREATE_RELATION_DIALOG_H +#define LABFY_INVESTIGATION_CREATE_RELATION_DIALOG_H + +#include + +G_BEGIN_DECLS + +/** + * @brief Résultat opaque produit par le dialogue. + */ +typedef struct CreateRelationDialogResult CreateRelationDialogResult; + +/** + * @brief Codes d'erreur produits par CreateRelationDialog. + */ +typedef enum +{ + CREATE_RELATION_DIALOG_ERROR_INVALID_ARGUMENT, + CREATE_RELATION_DIALOG_ERROR_EMPTY_TARGETS, + CREATE_RELATION_DIALOG_ERROR_MEMORY, + CREATE_RELATION_DIALOG_ERROR_INVALID_DATA +} CreateRelationDialogError; + +/** + * @brief Domaine d'erreur du dialogue. + */ +#define CREATE_RELATION_DIALOG_ERROR \ + create_relation_dialog_error_quark() + +/** + * @brief Callback appelé après validation ou annulation. + * + * En cas de validation, result appartient au callback et doit être libéré + * avec create_relation_dialog_result_free(). + * + * En cas d'annulation, result vaut NULL. + * + * @param result Résultat possédé par le callback, ou NULL. + * @param user_data Données privées fournies lors de l'ouverture. + */ +typedef void (*CreateRelationDialogCallback)( + CreateRelationDialogResult *result, + gpointer user_data +); + +/** + * @brief Retourne le domaine d'erreur du dialogue. + */ +GQuark create_relation_dialog_error_quark(void); + +/** + * @brief Présente le dialogue de création d'une relation. + * + * entities doit contenir des pointeurs EntityRecord valides. Le dialogue + * copie tous les identifiants et libellés nécessaires et ne conserve aucun + * pointeur vers les modèles fournis. + * + * L'entité source est retirée de la liste des cibles. + * + * Cette fonction ne crée aucune relation et n'accède pas à SQLite. + * + * @param parent Fenêtre GTK parente. + * @param source_entity_identifier UUID de l'entité source. + * @param entities Tableau de EntityRecord. + * @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 create_relation_dialog_present( + GtkWindow *parent, + const char *source_entity_identifier, + const GPtrArray *entities, + CreateRelationDialogCallback callback, + gpointer user_data, + GError **error +); + +/** + * @brief Valide les valeurs obligatoires d'une relation. + * + * Cette fonction est indépendante de l'affichage et pourra être testée + * sans ouvrir de fenêtre GTK. + * + * @param source_entity_identifier UUID de l'entité source. + * @param target_entity_identifier UUID de l'entité cible. + * @param relation_type Type de relation non vide. + * @param confidence Confiance comprise entre 0 et 100. + * @param error Emplacement facultatif recevant une erreur. + * + * @return TRUE lorsque les valeurs sont valides. + */ +gboolean create_relation_dialog_validate_values( + const char *source_entity_identifier, + const char *target_entity_identifier, + const char *relation_type, + gint confidence, + GError **error +); + +/** + * @brief Libère un résultat. + * + * Cette fonction accepte NULL. + */ +void create_relation_dialog_result_free( + CreateRelationDialogResult *result +); + +/** + * @brief Retourne l'UUID de l'entité source. + */ +const char *create_relation_dialog_result_get_source_identifier( + const CreateRelationDialogResult *result +); + +/** + * @brief Retourne l'UUID de l'entité cible. + */ +const char *create_relation_dialog_result_get_target_identifier( + const CreateRelationDialogResult *result +); + +/** + * @brief Retourne le type de relation. + */ +const char *create_relation_dialog_result_get_relation_type( + const CreateRelationDialogResult *result +); + +/** + * @brief Retourne le libellé facultatif. + */ +const char *create_relation_dialog_result_get_label( + const CreateRelationDialogResult *result +); + +/** + * @brief Retourne la justification facultative. + */ +const char *create_relation_dialog_result_get_justification( + const CreateRelationDialogResult *result +); + +/** + * @brief Retourne le niveau de confiance. + */ +gint create_relation_dialog_result_get_confidence( + const CreateRelationDialogResult *result +); + +G_END_DECLS + +#endif diff --git a/include/views/main_window.h b/include/views/main_window.h index 184a291..0492c28 100644 --- a/include/views/main_window.h +++ b/include/views/main_window.h @@ -109,6 +109,17 @@ typedef void (*MainWindowResetGraphLayoutCallback)( gpointer user_data ); +/** + * @brief Callback appelé lors d'une demande d'ajout d'une relation. + * + * @param source_entity_identifier UUID de l'entité source. + * @param user_data Données privées du callback. + */ +typedef void (*MainWindowAddRelationCallback)( + const char *source_entity_identifier, + gpointer user_data +); + /** * @brief Définit le callback de vérification d'une preuve. * @@ -154,6 +165,21 @@ void main_window_set_reset_graph_layout_callback( gpointer user_data ); +/** + * @brief Définit le callback de demande d'ajout d'une relation. + * + * MainWindow relaie uniquement la demande provenant du Workspace. + * + * @param main_window Fenêtre principale. + * @param callback Callback facultatif. + * @param user_data Données privées transmises au callback. + */ +void main_window_set_add_relation_callback( + MainWindow *main_window, + MainWindowAddRelationCallback callback, + gpointer user_data +); + /** * @brief Définit le callback du bouton « Nouvelle enquête ». * diff --git a/include/widgets/entity_details_panel.h b/include/widgets/entity_details_panel.h index e8efea6..c8319c2 100644 --- a/include/widgets/entity_details_panel.h +++ b/include/widgets/entity_details_panel.h @@ -32,6 +32,19 @@ typedef void (*EntityDetailsPanelCloseCallback)( gpointer user_data ); +/** + * @brief Callback appelé lorsque l'utilisateur demande une relation. + * + * L'identifiant est emprunté et uniquement valide pendant l'appel. + * + * @param source_entity_identifier UUID de l'entité source. + * @param user_data Données empruntées fournies par l'appelant. + */ +typedef void (*EntityDetailsPanelAddRelationCallback)( + const char *source_entity_identifier, + gpointer user_data +); + /** * @brief Crée un volet de détails fermé. * @@ -92,6 +105,21 @@ void entity_details_panel_set_close_callback( gpointer user_data ); +/** + * @brief Définit le callback du bouton d'ajout d'une relation. + * + * Le volet transmet uniquement l'UUID de l'entité actuellement affichée. + * + * @param details_panel Volet à configurer. + * @param callback Callback à appeler, ou NULL. + * @param user_data Données empruntées du callback. + */ +void entity_details_panel_set_add_relation_callback( + EntityDetailsPanel *details_panel, + EntityDetailsPanelAddRelationCallback callback, + gpointer user_data +); + /** * @brief Indique si le volet est actuellement ouvert. * diff --git a/include/widgets/workspace.h b/include/widgets/workspace.h index 6c2e587..4a04cda 100644 --- a/include/widgets/workspace.h +++ b/include/widgets/workspace.h @@ -66,6 +66,17 @@ typedef void (*WorkspaceResetGraphLayoutCallback)( gpointer user_data ); +/** + * @brief Callback appelé lors d'une demande d'ajout d'une relation. + * + * @param source_entity_identifier UUID de l'entité source. + * @param user_data Données empruntées fournies par l'appelant. + */ +typedef void (*WorkspaceAddRelationCallback)( + const char *source_entity_identifier, + gpointer user_data +); + /** * @brief Crée une nouvelle zone de travail. * @@ -161,6 +172,21 @@ void workspace_set_reset_graph_layout_callback( gpointer user_data ); +/** + * @brief Définit le callback de demande d'ajout d'une relation. + * + * Le Workspace relaie uniquement l'événement du volet de détails. + * + * @param workspace Zone de travail. + * @param callback Callback facultatif. + * @param user_data Données empruntées transmises au callback. + */ +void workspace_set_add_relation_callback( + Workspace *workspace, + WorkspaceAddRelationCallback callback, + gpointer user_data +); + /** * @brief Affiche l'état de chargement du graphe. * diff --git a/src/core/application.c b/src/core/application.c index 47d33ac..cf710d8 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -27,7 +27,9 @@ #include "dao/evidence_type_dao.h" #include "dao/graph_node_position_dao.h" #include "views/evidence_import_dialog.h" +#include "views/create_relation_dialog.h" #include "dao/evidence_dao.h" +#include "dao/entity_dao.h" #include "models/evidence_type.h" #include "widgets/evidence_list_model.h" #include "widgets/evidence_category_model.h" @@ -3324,6 +3326,239 @@ static void application_on_evidence_selected( ); } + +/** + * @brief Traite la validation ou l'annulation du dialogue de relation. + * + * Cette première étape valide le parcours complet de l'interface. + * L'écriture SQLite sera branchée après validation de la compilation. + */ +static void application_on_create_relation_completed( + CreateRelationDialogResult *result, + gpointer user_data +) +{ + Application *application = + user_data; + + const char *source_identifier = + NULL; + + const char *target_identifier = + NULL; + + const char *relation_type = + NULL; + + char *status_message = + NULL; + + if (application == NULL || + application->main_window == NULL) + { + create_relation_dialog_result_free( + result + ); + + return; + } + + if (result == NULL) + { + main_window_set_status( + application->main_window, + "Ajout de relation annulé." + ); + + return; + } + + source_identifier = + create_relation_dialog_result_get_source_identifier( + result + ); + + target_identifier = + create_relation_dialog_result_get_target_identifier( + result + ); + + relation_type = + create_relation_dialog_result_get_relation_type( + result + ); + + g_message( + "Relation préparée : %s -> %s (%s), confiance %d %%.", + source_identifier != NULL + ? source_identifier + : "(source absente)", + target_identifier != NULL + ? target_identifier + : "(cible absente)", + relation_type != NULL + ? relation_type + : "(type absent)", + create_relation_dialog_result_get_confidence( + result + ) + ); + + status_message = + g_strdup_printf( + "Relation préparée : %s. " + "L'enregistrement SQLite sera branché à l'étape suivante.", + relation_type != NULL && + relation_type[0] != '\0' + ? relation_type + : "(type inconnu)" + ); + + main_window_set_status( + application->main_window, + status_message != NULL + ? status_message + : "Relation préparée." + ); + + g_free( + status_message + ); + + create_relation_dialog_result_free( + result + ); +} + +/** + * @brief Ouvre le dialogue de création depuis la fiche d'une entité. + */ +static void application_on_add_relation_requested( + const char *source_entity_identifier, + gpointer user_data +) +{ + Application *application = + user_data; + + Database *database = + NULL; + + EntityDao *entity_dao = + NULL; + + GPtrArray *entities = + NULL; + + GError *error = + NULL; + + if (application == NULL || + application->main_window == NULL || + application->session == NULL || + source_entity_identifier == NULL || + !g_uuid_string_is_valid( + source_entity_identifier + )) + { + return; + } + + database = + investigation_session_get_database( + application->session + ); + + if (database == NULL) + { + application_present_error( + application, + "Relations indisponibles", + "La session ne fournit aucune connexion SQLite." + ); + + return; + } + + entity_dao = + entity_dao_new( + database, + &error + ); + + if (entity_dao == NULL) + { + application_present_error( + application, + "Relations indisponibles", + error != NULL + ? error->message + : "Impossible d'accéder aux entités." + ); + + g_clear_error( + &error + ); + + return; + } + + entities = + entity_dao_list_all( + entity_dao, + &error + ); + + entity_dao_free( + entity_dao + ); + + if (entities == NULL) + { + application_present_error( + application, + "Relations indisponibles", + error != NULL + ? error->message + : "Impossible de charger les entités." + ); + + g_clear_error( + &error + ); + + return; + } + + if (!create_relation_dialog_present( + main_window_get_window( + application->main_window + ), + source_entity_identifier, + entities, + application_on_create_relation_completed, + application, + &error + )) + { + application_present_error( + application, + "Ajout de relation impossible", + error != NULL + ? error->message + : "Le dialogue de relation n'a pas pu être ouvert." + ); + + g_clear_error( + &error + ); + } + + g_ptr_array_unref( + entities + ); +} + /** * @brief Enregistre le statut d'intégrité dans l'enquête d'origine. * @@ -4600,6 +4835,12 @@ static void application_on_activate( application ); + main_window_set_add_relation_callback( + application->main_window, + application_on_add_relation_requested, + application + ); + main_window_set_new_investigation_callback( application->main_window, application_on_new_investigation_requested, diff --git a/src/views/create_relation_dialog.c b/src/views/create_relation_dialog.c new file mode 100644 index 0000000..460c753 --- /dev/null +++ b/src/views/create_relation_dialog.c @@ -0,0 +1,1632 @@ +/****************************************************************************** + * @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 + +/** + * @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 + ), + "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 + ) + ); + + g_object_unref( + target_labels + ); + + 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; +} diff --git a/src/views/main_window.c b/src/views/main_window.c index 97faeac..3e1a8c1 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -111,6 +111,12 @@ struct MainWindow gpointer reset_graph_layout_user_data; + MainWindowAddRelationCallback + add_relation_callback; + + gpointer + add_relation_user_data; + MainWindowQuitCallback quit_callback; @@ -348,6 +354,33 @@ static void main_window_on_reset_graph_layout_requested( ); } +/** + * @brief Relaie la demande d'ajout d'une relation. + */ +static void main_window_on_add_relation_requested( + const char *source_entity_identifier, + gpointer user_data +) +{ + MainWindow *main_window = + user_data; + + if (main_window == NULL || + main_window->add_relation_callback == NULL || + source_entity_identifier == NULL || + !g_uuid_string_is_valid( + source_entity_identifier + )) + { + return; + } + + main_window->add_relation_callback( + source_entity_identifier, + main_window->add_relation_user_data + ); +} + /** * @brief Relaie la demande de vérification provenant du Workspace. */ @@ -648,6 +681,12 @@ MainWindow *main_window_new( main_window ); + workspace_set_add_relation_callback( + main_window->workspace, + main_window_on_add_relation_requested, + main_window + ); + workspace_widget = workspace_get_widget( main_window->workspace ); @@ -1281,6 +1320,24 @@ void main_window_set_reset_graph_layout_callback( user_data; } +void main_window_set_add_relation_callback( + MainWindow *main_window, + MainWindowAddRelationCallback callback, + gpointer user_data +) +{ + if (main_window == NULL) + { + return; + } + + main_window->add_relation_callback = + callback; + + main_window->add_relation_user_data = + user_data; +} + void main_window_set_quit_callback( MainWindow *main_window, MainWindowQuitCallback callback, @@ -1409,6 +1466,12 @@ void main_window_free( NULL ); + workspace_set_add_relation_callback( + main_window->workspace, + NULL, + NULL + ); + main_window->graph_node_moved_callback = NULL; @@ -1421,6 +1484,12 @@ void main_window_free( main_window->reset_graph_layout_user_data = NULL; + main_window->add_relation_callback = + NULL; + + main_window->add_relation_user_data = + NULL; + main_window->show_graph_callback = NULL; diff --git a/src/widgets/entity_details_panel.c b/src/widgets/entity_details_panel.c index cf7608a..a6963a8 100644 --- a/src/widgets/entity_details_panel.c +++ b/src/widgets/entity_details_panel.c @@ -17,6 +17,7 @@ struct EntityDetailsPanel { GtkWidget *root_revealer; GtkWidget *frame; + GtkWidget *add_relation_button; GtkWidget *close_button; GtkWidget *entity_title_label; @@ -29,8 +30,16 @@ struct EntityDetailsPanel GtkWidget *entity_updated_at_label; GtkWidget *entity_identifier_label; + char *selected_entity_identifier; + EntityDetailsPanelCloseCallback close_callback; gpointer close_user_data; + + EntityDetailsPanelAddRelationCallback + add_relation_callback; + + gpointer + add_relation_user_data; }; /** @@ -191,6 +200,64 @@ static const char *entity_details_panel_get_status_text( } } +/** + * @brief Met à jour l'état du bouton d'ajout d'une relation. + */ +static void entity_details_panel_update_add_relation_button( + EntityDetailsPanel *details_panel +) +{ + gboolean enabled = + FALSE; + + if (details_panel == NULL || + details_panel->add_relation_button == NULL) + { + return; + } + + enabled = + details_panel->add_relation_callback != NULL && + details_panel->selected_entity_identifier != NULL && + g_uuid_string_is_valid( + details_panel->selected_entity_identifier + ); + + gtk_widget_set_sensitive( + details_panel->add_relation_button, + enabled + ); +} + +/** + * @brief Transmet la demande d'ajout d'une relation. + */ +static void entity_details_panel_on_add_relation_clicked( + GtkButton *button, + gpointer user_data +) +{ + EntityDetailsPanel *details_panel = + user_data; + + (void) button; + + if (details_panel == NULL || + details_panel->add_relation_callback == NULL || + details_panel->selected_entity_identifier == NULL || + !g_uuid_string_is_valid( + details_panel->selected_entity_identifier + )) + { + return; + } + + details_panel->add_relation_callback( + details_panel->selected_entity_identifier, + details_panel->add_relation_user_data + ); +} + /** * @brief Ferme le volet depuis le bouton de l'interface. */ @@ -266,6 +333,11 @@ EntityDetailsPanel *entity_details_panel_new(void) NULL ); + details_panel->add_relation_button = + gtk_button_new_from_icon_name( + "list-add-symbolic" + ); + details_panel->close_button = gtk_button_new_from_icon_name( "window-close-symbolic" @@ -307,6 +379,7 @@ EntityDetailsPanel *entity_details_panel_new(void) if (details_panel->root_revealer == NULL || details_panel->frame == NULL || + details_panel->add_relation_button == NULL || details_panel->close_button == NULL || content_box == NULL || header_box == NULL || @@ -425,6 +498,21 @@ EntityDetailsPanel *entity_details_panel_new(void) "heading" ); + gtk_widget_set_tooltip_text( + details_panel->add_relation_button, + "Ajouter une relation depuis cette entité" + ); + + gtk_widget_add_css_class( + details_panel->add_relation_button, + "flat" + ); + + gtk_widget_set_sensitive( + details_panel->add_relation_button, + FALSE + ); + gtk_widget_set_tooltip_text( details_panel->close_button, "Fermer le volet" @@ -440,6 +528,11 @@ EntityDetailsPanel *entity_details_panel_new(void) header_label ); + gtk_box_append( + GTK_BOX(header_box), + details_panel->add_relation_button + ); + gtk_box_append( GTK_BOX(header_box), details_panel->close_button @@ -645,6 +738,15 @@ EntityDetailsPanel *entity_details_panel_new(void) details_panel->frame ); + g_signal_connect( + details_panel->add_relation_button, + "clicked", + G_CALLBACK( + entity_details_panel_on_add_relation_clicked + ), + details_panel + ); + g_signal_connect( details_panel->close_button, "clicked", @@ -689,6 +791,11 @@ void entity_details_panel_set_entity( return; } + g_clear_pointer( + &details_panel->selected_entity_identifier, + g_free + ); + if (entity_record == NULL) { entity_details_panel_clear( @@ -698,6 +805,17 @@ void entity_details_panel_set_entity( return; } + details_panel->selected_entity_identifier = + g_strdup( + entity_record_get_identifier( + entity_record + ) + ); + + entity_details_panel_update_add_relation_button( + details_panel + ); + title = entity_record_get_label( entity_record @@ -820,6 +938,15 @@ void entity_details_panel_clear( return; } + g_clear_pointer( + &details_panel->selected_entity_identifier, + g_free + ); + + entity_details_panel_update_add_relation_button( + details_panel + ); + entity_details_panel_set_field_text( details_panel->entity_title_label, NULL, @@ -903,6 +1030,28 @@ void entity_details_panel_set_close_callback( user_data; } +void entity_details_panel_set_add_relation_callback( + EntityDetailsPanel *details_panel, + EntityDetailsPanelAddRelationCallback callback, + gpointer user_data +) +{ + if (details_panel == NULL) + { + return; + } + + details_panel->add_relation_callback = + callback; + + details_panel->add_relation_user_data = + user_data; + + entity_details_panel_update_add_relation_button( + details_panel + ); +} + gboolean entity_details_panel_is_open( const EntityDetailsPanel *details_panel ) @@ -929,6 +1078,14 @@ void entity_details_panel_free( return; } + if (details_panel->add_relation_button != NULL) + { + g_signal_handlers_disconnect_by_data( + details_panel->add_relation_button, + details_panel + ); + } + if (details_panel->close_button != NULL) { g_signal_handlers_disconnect_by_data( @@ -943,12 +1100,26 @@ void entity_details_panel_free( details_panel->close_user_data = NULL; + details_panel->add_relation_callback = + NULL; + + details_panel->add_relation_user_data = + NULL; + + g_clear_pointer( + &details_panel->selected_entity_identifier, + g_free + ); + details_panel->root_revealer = NULL; details_panel->frame = NULL; + details_panel->add_relation_button = + NULL; + details_panel->close_button = NULL; diff --git a/src/widgets/workspace.c b/src/widgets/workspace.c index e4cf2e9..5f87493 100644 --- a/src/widgets/workspace.c +++ b/src/widgets/workspace.c @@ -93,6 +93,12 @@ struct Workspace gpointer reset_graph_layout_user_data; + WorkspaceAddRelationCallback + add_relation_callback; + + gpointer + add_relation_user_data; + GtkWidget *node_name_label; GtkWidget *node_path_label; GtkWidget *node_type_label; @@ -324,6 +330,33 @@ static void workspace_on_graph_entity_selected( ); } +/** + * @brief Relaie la demande d'ajout d'une relation. + */ +static void workspace_on_add_relation_requested( + const char *source_entity_identifier, + gpointer user_data +) +{ + Workspace *workspace = + user_data; + + if (workspace == NULL || + workspace->add_relation_callback == NULL || + source_entity_identifier == NULL || + !g_uuid_string_is_valid( + source_entity_identifier + )) + { + return; + } + + workspace->add_relation_callback( + source_entity_identifier, + workspace->add_relation_user_data + ); +} + /** * @brief Désélectionne le nœud lorsque le volet est fermé manuellement. */ @@ -1075,6 +1108,12 @@ Workspace *workspace_new(void) workspace ); + entity_details_panel_set_add_relation_callback( + workspace->entity_details_panel, + workspace_on_add_relation_requested, + workspace + ); + gtk_overlay_set_child( GTK_OVERLAY( workspace->graph_view_page @@ -2107,6 +2146,24 @@ void workspace_set_reset_graph_layout_callback( user_data; } +void workspace_set_add_relation_callback( + Workspace *workspace, + WorkspaceAddRelationCallback callback, + gpointer user_data +) +{ + if (workspace == NULL) + { + return; + } + + workspace->add_relation_callback = + callback; + + workspace->add_relation_user_data = + user_data; +} + void workspace_reset_graph_layout( Workspace *workspace ) @@ -2157,6 +2214,12 @@ void workspace_free(Workspace *workspace) NULL ); + entity_details_panel_set_add_relation_callback( + workspace->entity_details_panel, + NULL, + NULL + ); + entity_details_panel_clear( workspace->entity_details_panel ); @@ -2203,6 +2266,12 @@ void workspace_free(Workspace *workspace) workspace->reset_graph_layout_user_data = NULL; + workspace->add_relation_callback = + NULL; + + workspace->add_relation_user_data = + NULL; + workspace->graph_toolbar = NULL;