labfy-investigation/src/views/create_investigation_dialog.c

864 lines
18 KiB
C

/******************************************************************************
* @file create_investigation_dialog.c
* @brief Dialogue GTK de création d'une nouvelle enquête.
******************************************************************************/
#include "views/create_investigation_dialog.h"
#include <gio/gio.h>
#include <glib.h>
/**
* @brief Largeur initiale du dialogue.
*/
#define CREATE_INVESTIGATION_DIALOG_WIDTH 620
/**
* @brief Marge extérieure du dialogue.
*/
#define CREATE_INVESTIGATION_DIALOG_MARGIN 16
/**
* @brief Espacement entre les composants principaux.
*/
#define CREATE_INVESTIGATION_DIALOG_SPACING 12
/**
* @brief Contexte conservé pendant la durée de vie du dialogue.
*/
typedef struct
{
GtkWindow *window;
GtkWidget *parent_directory_entry;
GtkWidget *investigation_name_entry;
GtkWidget *create_button;
char *parent_directory;
CreateInvestigationDialogCallback callback;
gpointer user_data;
gboolean completed;
} CreateInvestigationDialogContext;
/**
* @brief Libère le contexte associé au dialogue.
*
* Les widgets GTK sont possédés par la fenêtre et ne sont donc pas
* libérés directement ici.
*
* @param context Contexte à libérer.
*/
static void create_investigation_dialog_context_free(
CreateInvestigationDialogContext *context
)
{
if (context == NULL)
{
return;
}
g_free(context->parent_directory);
g_free(context);
}
/**
* @brief Valide et normalise un nom d'enquête.
*
* La fonction :
*
* - vérifie que la chaîne est un UTF-8 valide ;
* - refuse les noms constitués uniquement d'espaces ;
* - reconnaît les espaces Unicode ;
* - refuse les séparateurs '/' et '\' ;
* - retire les espaces placés au début et à la fin.
*
* Si normalized_name n'est pas NULL, la fonction y place une nouvelle
* chaîne allouée que l'appelant devra libérer avec g_free().
*
* @param investigation_name Nom à vérifier.
* @param normalized_name Adresse recevant le nom normalisé, ou NULL.
*
* @return TRUE si le nom est valide, sinon FALSE.
*/
static gboolean create_investigation_dialog_normalize_name(
const char *investigation_name,
char **normalized_name
)
{
const char *cursor = NULL;
const char *first_character = NULL;
const char *end_after_last_character = NULL;
if (normalized_name != NULL)
{
*normalized_name = NULL;
}
if (investigation_name == NULL ||
!g_utf8_validate(
investigation_name,
-1,
NULL
))
{
return FALSE;
}
cursor = investigation_name;
while (*cursor != '\0')
{
gunichar character = 0;
const char *next_character = NULL;
character = g_utf8_get_char(
cursor
);
next_character = g_utf8_next_char(
cursor
);
if (character == '/' ||
character == '\\')
{
return FALSE;
}
if (!g_unichar_isspace(character))
{
if (first_character == NULL)
{
first_character = cursor;
}
end_after_last_character = next_character;
}
cursor = next_character;
}
/*
* Aucun caractère visible n'a été trouvé.
* Le nom était vide ou uniquement composé d'espaces.
*/
if (first_character == NULL ||
end_after_last_character == NULL)
{
return FALSE;
}
if (normalized_name != NULL)
{
*normalized_name = g_strndup(
first_character,
(gsize) (
end_after_last_character -
first_character
)
);
if (*normalized_name == NULL)
{
return FALSE;
}
}
return TRUE;
}
/**
* @brief Met à jour l'état du bouton de création.
*
* Le bouton est actif uniquement lorsqu'un dossier parent valide a été
* sélectionné et que le nom saisi est acceptable.
*
* @param context Contexte du dialogue.
*/
static void create_investigation_dialog_update_create_button(
CreateInvestigationDialogContext *context
)
{
const char *investigation_name = NULL;
gboolean has_valid_parent_directory = FALSE;
gboolean has_valid_name = FALSE;
if (context == NULL ||
context->create_button == NULL ||
context->investigation_name_entry == NULL)
{
return;
}
has_valid_parent_directory =
context->parent_directory != NULL &&
context->parent_directory[0] != '\0' &&
g_file_test(
context->parent_directory,
G_FILE_TEST_IS_DIR
);
investigation_name = gtk_editable_get_text(
GTK_EDITABLE(
context->investigation_name_entry
)
);
has_valid_name =
create_investigation_dialog_normalize_name(
investigation_name,
NULL
);
gtk_widget_set_sensitive(
context->create_button,
has_valid_parent_directory && has_valid_name
);
}
/**
* @brief Termine le dialogue et appelle son callback une seule fois.
*
* @param context Contexte du dialogue.
* @param parent_directory Dossier parent ou NULL en cas d'annulation.
* @param investigation_name Nom nettoyé ou NULL en cas d'annulation.
*/
static void create_investigation_dialog_complete(
CreateInvestigationDialogContext *context,
const char *parent_directory,
const char *investigation_name
)
{
if (context == NULL ||
context->completed)
{
return;
}
context->completed = TRUE;
if (context->callback != NULL)
{
context->callback(
parent_directory,
investigation_name,
context->user_data
);
}
if (context->window != NULL)
{
gtk_window_destroy(
context->window
);
}
}
/**
* @brief Traite les modifications du nom de l'enquête.
*
* @param editable Champ ayant été modifié.
* @param user_data Contexte du dialogue.
*/
static void create_investigation_dialog_on_name_changed(
GtkEditable *editable,
gpointer user_data
)
{
CreateInvestigationDialogContext *context = user_data;
(void) editable;
create_investigation_dialog_update_create_button(
context
);
}
/**
* @brief Traite le résultat du sélecteur de dossier parent.
*
* La référence passée dans user_data maintient la fenêtre en vie pendant
* toute l'opération asynchrone.
*
* @param source_object GtkFileDialog ayant lancé l'opération.
* @param result Résultat asynchrone.
* @param user_data Référence vers la fenêtre du dialogue.
*/
static void create_investigation_dialog_on_parent_selected(
GObject *source_object,
GAsyncResult *result,
gpointer user_data
)
{
GtkFileDialog *file_dialog = NULL;
GtkWindow *dialog_window = NULL;
CreateInvestigationDialogContext *context = NULL;
GFile *selected_folder = NULL;
GError *error = NULL;
char *selected_path = NULL;
file_dialog = GTK_FILE_DIALOG(
source_object
);
dialog_window = GTK_WINDOW(
user_data
);
context = g_object_get_data(
G_OBJECT(dialog_window),
"create-investigation-dialog-context"
);
selected_folder = gtk_file_dialog_select_folder_finish(
file_dialog,
result,
&error
);
/*
* Le dialogue de création a pu être fermé pendant que le sélecteur
* asynchrone était encore actif.
*/
if (context == NULL ||
context->completed)
{
g_clear_error(&error);
if (selected_folder != NULL)
{
g_object_unref(selected_folder);
}
g_object_unref(dialog_window);
return;
}
if (selected_folder == NULL)
{
if (error != NULL &&
!g_error_matches(
error,
GTK_DIALOG_ERROR,
GTK_DIALOG_ERROR_DISMISSED
))
{
g_warning(
"Impossible de sélectionner le dossier parent : %s",
error->message
);
}
g_clear_error(&error);
g_object_unref(dialog_window);
return;
}
selected_path = g_file_get_path(
selected_folder
);
if (selected_path == NULL)
{
g_warning(
"Le dossier sélectionné ne possède pas de chemin local."
);
g_object_unref(selected_folder);
g_object_unref(dialog_window);
return;
}
if (!g_file_test(
selected_path,
G_FILE_TEST_IS_DIR
))
{
g_warning(
"Le chemin sélectionné n'est pas un dossier existant : '%s'.",
selected_path
);
g_free(selected_path);
g_object_unref(selected_folder);
g_object_unref(dialog_window);
return;
}
g_free(context->parent_directory);
context->parent_directory = selected_path;
selected_path = NULL;
gtk_editable_set_text(
GTK_EDITABLE(
context->parent_directory_entry
),
context->parent_directory
);
create_investigation_dialog_update_create_button(
context
);
g_object_unref(selected_folder);
g_object_unref(dialog_window);
}
/**
* @brief Ouvre le sélecteur du dossier parent.
*
* @param button Bouton ayant reçu le clic.
* @param user_data Contexte du dialogue.
*/
static void create_investigation_dialog_on_browse_clicked(
GtkButton *button,
gpointer user_data
)
{
CreateInvestigationDialogContext *context = user_data;
GtkFileDialog *file_dialog = NULL;
(void) button;
if (context == NULL ||
context->completed ||
context->window == NULL)
{
return;
}
file_dialog = gtk_file_dialog_new();
gtk_file_dialog_set_title(
file_dialog,
"Sélectionner le dossier parent"
);
gtk_file_dialog_set_modal(
file_dialog,
TRUE
);
gtk_file_dialog_select_folder(
file_dialog,
context->window,
NULL,
create_investigation_dialog_on_parent_selected,
g_object_ref(context->window)
);
g_object_unref(file_dialog);
}
/**
* @brief Annule la création.
*
* @param button Bouton ayant reçu le clic.
* @param user_data Contexte du dialogue.
*/
static void create_investigation_dialog_on_cancel_clicked(
GtkButton *button,
gpointer user_data
)
{
CreateInvestigationDialogContext *context = user_data;
(void) button;
create_investigation_dialog_complete(
context,
NULL,
NULL
);
}
/**
* @brief Valide les informations et termine le dialogue.
*
* @param button Bouton ayant reçu le clic.
* @param user_data Contexte du dialogue.
*/
static void create_investigation_dialog_on_create_clicked(
GtkButton *button,
gpointer user_data
)
{
CreateInvestigationDialogContext *context = user_data;
const char *entry_text = NULL;
char *trimmed_name = NULL;
(void) button;
if (context == NULL ||
context->completed)
{
return;
}
if (context->parent_directory == NULL ||
!g_file_test(
context->parent_directory,
G_FILE_TEST_IS_DIR
))
{
create_investigation_dialog_update_create_button(
context
);
return;
}
entry_text = gtk_editable_get_text(
GTK_EDITABLE(
context->investigation_name_entry
)
);
if (!create_investigation_dialog_normalize_name(
entry_text,
&trimmed_name
))
{
create_investigation_dialog_update_create_button(
context
);
return;
}
create_investigation_dialog_complete(
context,
context->parent_directory,
trimmed_name
);
g_free(trimmed_name);
}
/**
* @brief Intercepte la fermeture de la fenêtre.
*
* Une fermeture avec le bouton système est traitée comme une annulation.
*
* @param window Fenêtre demandant sa fermeture.
* @param user_data Contexte du dialogue.
*
* @return TRUE car la destruction est gérée par le module.
*/
static gboolean create_investigation_dialog_on_close_request(
GtkWindow *window,
gpointer user_data
)
{
CreateInvestigationDialogContext *context = user_data;
(void) window;
create_investigation_dialog_complete(
context,
NULL,
NULL
);
return TRUE;
}
void create_investigation_dialog_present(
GtkWindow *parent_window,
CreateInvestigationDialogCallback callback,
gpointer user_data
)
{
CreateInvestigationDialogContext *context = NULL;
GtkWidget *main_box = NULL;
GtkWidget *parent_label = NULL;
GtkWidget *parent_row = NULL;
GtkWidget *browse_button = NULL;
GtkWidget *name_label = NULL;
GtkWidget *action_box = NULL;
GtkWidget *cancel_button = NULL;
context = g_new0(
CreateInvestigationDialogContext,
1
);
context->callback = callback;
context->user_data = user_data;
context->window = GTK_WINDOW(
gtk_window_new()
);
gtk_window_set_title(
context->window,
"Nouvelle enquête"
);
gtk_window_set_default_size(
context->window,
CREATE_INVESTIGATION_DIALOG_WIDTH,
-1
);
gtk_window_set_modal(
context->window,
TRUE
);
gtk_window_set_resizable(
context->window,
FALSE
);
if (parent_window != NULL)
{
gtk_window_set_transient_for(
context->window,
parent_window
);
gtk_window_set_destroy_with_parent(
context->window,
TRUE
);
}
/*
* Le contexte est attaché à la fenêtre.
*
* Il sera libéré lorsque la dernière référence vers la fenêtre
* disparaîtra, y compris si un sélecteur asynchrone est encore actif.
*/
g_object_set_data_full(
G_OBJECT(context->window),
"create-investigation-dialog-context",
context,
(GDestroyNotify)
create_investigation_dialog_context_free
);
main_box = gtk_box_new(
GTK_ORIENTATION_VERTICAL,
CREATE_INVESTIGATION_DIALOG_SPACING
);
gtk_widget_set_margin_start(
main_box,
CREATE_INVESTIGATION_DIALOG_MARGIN
);
gtk_widget_set_margin_end(
main_box,
CREATE_INVESTIGATION_DIALOG_MARGIN
);
gtk_widget_set_margin_top(
main_box,
CREATE_INVESTIGATION_DIALOG_MARGIN
);
gtk_widget_set_margin_bottom(
main_box,
CREATE_INVESTIGATION_DIALOG_MARGIN
);
parent_label = gtk_label_new(
"Dossier parent :"
);
gtk_widget_set_halign(
parent_label,
GTK_ALIGN_START
);
parent_row = gtk_box_new(
GTK_ORIENTATION_HORIZONTAL,
8
);
context->parent_directory_entry = gtk_entry_new();
gtk_editable_set_editable(
GTK_EDITABLE(
context->parent_directory_entry
),
FALSE
);
gtk_widget_set_hexpand(
context->parent_directory_entry,
TRUE
);
gtk_entry_set_placeholder_text(
GTK_ENTRY(
context->parent_directory_entry
),
"Aucun dossier sélectionné"
);
browse_button = gtk_button_new_with_label(
"Parcourir"
);
gtk_box_append(
GTK_BOX(parent_row),
context->parent_directory_entry
);
gtk_box_append(
GTK_BOX(parent_row),
browse_button
);
name_label = gtk_label_new(
"Nom de l'enquête :"
);
gtk_widget_set_halign(
name_label,
GTK_ALIGN_START
);
context->investigation_name_entry = gtk_entry_new();
gtk_entry_set_placeholder_text(
GTK_ENTRY(
context->investigation_name_entry
),
"Exemple : Arnaque_Billets"
);
action_box = gtk_box_new(
GTK_ORIENTATION_HORIZONTAL,
8
);
gtk_widget_set_halign(
action_box,
GTK_ALIGN_END
);
cancel_button = gtk_button_new_with_label(
"Annuler"
);
context->create_button = gtk_button_new_with_label(
"Créer"
);
gtk_widget_add_css_class(
context->create_button,
"suggested-action"
);
gtk_widget_set_sensitive(
context->create_button,
FALSE
);
gtk_box_append(
GTK_BOX(action_box),
cancel_button
);
gtk_box_append(
GTK_BOX(action_box),
context->create_button
);
gtk_box_append(
GTK_BOX(main_box),
parent_label
);
gtk_box_append(
GTK_BOX(main_box),
parent_row
);
gtk_box_append(
GTK_BOX(main_box),
name_label
);
gtk_box_append(
GTK_BOX(main_box),
context->investigation_name_entry
);
gtk_box_append(
GTK_BOX(main_box),
action_box
);
gtk_window_set_child(
context->window,
main_box
);
g_signal_connect(
context->window,
"close-request",
G_CALLBACK(
create_investigation_dialog_on_close_request
),
context
);
g_signal_connect(
browse_button,
"clicked",
G_CALLBACK(
create_investigation_dialog_on_browse_clicked
),
context
);
g_signal_connect(
context->investigation_name_entry,
"changed",
G_CALLBACK(
create_investigation_dialog_on_name_changed
),
context
);
g_signal_connect(
cancel_button,
"clicked",
G_CALLBACK(
create_investigation_dialog_on_cancel_clicked
),
context
);
g_signal_connect(
context->create_button,
"clicked",
G_CALLBACK(
create_investigation_dialog_on_create_clicked
),
context
);
gtk_window_present(
context->window
);
}