diff --git a/docs/tickets/open/TICKET-028.md b/docs/tickets/closed/TICKET-028.md similarity index 100% rename from docs/tickets/open/TICKET-028.md rename to docs/tickets/closed/TICKET-028.md diff --git a/docs/tickets/closed/TICKET-029.md b/docs/tickets/closed/TICKET-029.md new file mode 100644 index 0000000..d301f81 --- /dev/null +++ b/docs/tickets/closed/TICKET-029.md @@ -0,0 +1,933 @@ +# Ticket #029 — Intégrer `InvestigationSession` au cycle de vie GTK + +## Contexte + +Le ticket #028 a ajouté `InvestigationSession`, qui permet d’ouvrir une enquête existante de manière contrôlée. + +Une session valide possède désormais : + +- un `InvestigationProject` ; +- une connexion `Database` ouverte ; +- un `InvestigationRecord` chargé depuis SQLite. + +L’application GTK utilise encore l’ancien objet `Investigation` pour représenter le dossier sélectionné. + +Son fonctionnement actuel est approximativement le suivant : + +```text +FolderDialog + ↓ +Investigation + ↓ +InvestigationTreeBuilder + ↓ +InvestigationTreeModel + ↓ +MainWindow +``` + +Cette organisation ne conserve pas la connexion SQLite et n’utilise pas les informations persistées dans la table `investigation`. + +Il faut désormais remplacer l’ancien objet détenu par `Application` par une véritable `InvestigationSession`. + +## Objectif + +Faire de `InvestigationSession` le contexte actif de l’application. + +Après la sélection d’un dossier : + +1. ouvrir une nouvelle `InvestigationSession` ; +2. récupérer le chemin racine depuis son `InvestigationProject` ; +3. construire le nouvel `InvestigationTreeModel` ; +4. ne remplacer l’ancienne enquête qu’après validation complète ; +5. mettre à jour la fenêtre principale ; +6. conserver la session jusqu’à la fermeture de l’application. + +L’application ne doit posséder qu’une seule session active à la fois. + +## Architecture attendue + +```text +FolderDialog + │ + ▼ +Application + │ + ├── InvestigationSession + │ ├── InvestigationProject + │ ├── Database + │ └── InvestigationRecord + │ + ├── InvestigationTreeModel + │ + └── MainWindow + ├── Sidebar + ├── Workspace + └── Barre d’état +``` + +## Principe de remplacement transactionnel + +L’ouverture d’une nouvelle enquête doit suivre cet ordre : + +```text +ancienne session toujours active + ↓ +ouvrir la nouvelle session + ↓ +construire le nouvel arbre + ↓ +vérifier que les deux objets sont valides + ↓ +remplacer l’ancien arbre + ↓ +fermer l’ancienne session + ↓ +installer la nouvelle session + ↓ +mettre à jour MainWindow +``` + +En cas d’échec avant le remplacement : + +```text +ancienne session conservée +ancienne arborescence conservée +nouvelle ressource libérée +message d’erreur produit +``` + +L’application ne doit jamais perdre une enquête déjà ouverte simplement parce qu’une nouvelle sélection est invalide. + +--- + +# Travail à réaliser + +## 1. Remplacer l’ancien objet dans `Application` + +Modifier : + +```text +src/core/application.c +``` + +La structure privée actuelle contient notamment : + +```c +Investigation *investigation; +InvestigationTreeModel *tree_model; +``` + +Remplacer le premier champ par : + +```c +InvestigationSession *session; +``` + +La structure doit devenir au minimum : + +```c +struct Application +{ + GtkApplication *gtk_application; + MainWindow *main_window; + InvestigationSession *session; + InvestigationTreeModel *tree_model; +}; +``` + +Ne pas exposer cette structure dans le header public. + +## 2. Modifier les dépendances de `application.c` + +Supprimer : + +```c +#include "core/investigation.h" +``` + +Ajouter : + +```c +#include "core/investigation_session.h" +#include "core/investigation_project.h" +#include "models/investigation_record.h" +``` + +Conserver les dépendances nécessaires à : + +```text +FolderDialog +MainWindow +InvestigationTreeBuilder +InvestigationTreeModel +GTK +``` + +`application.c` ne doit pas inclure directement : + +```c +#include +``` + +et ne doit appeler aucune fonction `sqlite3_*`. + +## 3. Ouvrir la session après la sélection du dossier + +Dans : + +```c +application_on_folder_selected() +``` + +remplacer la création de l’ancien objet : + +```c +investigation_new(folder_path) +``` + +par : + +```c +investigation_session_open( + folder_path, + &error +); +``` + +Utiliser des variables temporaires : + +```c +InvestigationSession *new_session = NULL; +InvestigationTreeModel *new_tree_model = NULL; +GError *error = NULL; +``` + +Ne jamais écrire directement dans : + +```c +application->session +application->tree_model +``` + +avant que toute l’ouverture soit validée. + +## 4. Gérer une sélection annulée + +Lorsque : + +```c +folder_path == NULL +``` + +la fonction doit simplement retourner. + +L’enquête déjà ouverte doit rester active. + +Aucune session ne doit être fermée. + +Aucune erreur ne doit être affichée. + +Un message de diagnostic avec `g_print()` reste acceptable : + +```text +Sélection annulée. +``` + +## 5. Gérer l’échec d’ouverture de session + +Si : + +```c +new_session == NULL +``` + +la fonction doit : + +- afficher un avertissement contenant le message du `GError` ; +- libérer le `GError` ; +- conserver l’ancienne session ; +- conserver l’ancien arbre ; +- ne pas modifier la fenêtre principale. + +Exemple de diagnostic acceptable : + +```c +g_warning( + "Impossible d'ouvrir l'enquête : %s", + error != NULL ? error->message : "erreur inconnue" +); +``` + +Le dialogue graphique d’erreur est hors périmètre de ce ticket. + +## 6. Construire l’arborescence depuis la session + +Récupérer le projet de la nouvelle session : + +```c +const InvestigationProject *project = NULL; +``` + +avec : + +```c +project = investigation_session_get_project( + new_session +); +``` + +Récupérer ensuite son chemin racine : + +```c +const char *root_path = NULL; +``` + +avec : + +```c +root_path = investigation_project_get_root_path( + project +); +``` + +Construire le nouvel arbre avec : + +```c +new_tree_model = investigation_tree_builder_build( + root_path +); +``` + +La logique de construction du chemin ne doit pas être dupliquée dans `application.c`. + +## 7. Gérer l’échec de construction de l’arbre + +Si `InvestigationTreeBuilder` échoue après l’ouverture de la session : + +```text +new_session valide +new_tree_model == NULL +``` + +la fonction doit : + +```c +investigation_session_close(new_session); +``` + +puis retourner. + +L’ancienne session et l’ancien arbre doivent rester actifs. + +Le message suivant est acceptable : + +```text +Impossible de construire l'arborescence de l'enquête. +``` + +## 8. Remplacer les anciens objets uniquement après validation + +Lorsque `new_session` et `new_tree_model` sont tous deux valides : + +```c +investigation_tree_model_free( + application->tree_model +); + +investigation_session_close( + application->session +); + +application->tree_model = new_tree_model; +application->session = new_session; +``` + +Après le transfert : + +```c +new_tree_model = NULL; +new_session = NULL; +``` + +Cette remise à `NULL` n’est pas obligatoire si la fonction retourne immédiatement, mais elle est recommandée pour rendre la propriété explicite. + +## 9. Conserver la connexion SQLite ouverte + +Après une ouverture réussie, la session doit rester stockée dans : + +```c +application->session +``` + +Il est interdit de fermer la session à la fin de : + +```c +application_on_folder_selected() +``` + +La connexion `Database` doit rester utilisable pendant toute la durée d’ouverture de l’enquête. + +## 10. Ajouter un accesseur interne ou public + +Ajouter dans : + +```text +include/core/application.h +``` + +la déclaration suivante : + +```c +const InvestigationSession *application_get_session( + const Application *application +); +``` + +Implémenter dans : + +```text +src/core/application.c +``` + +```c +const InvestigationSession *application_get_session( + const Application *application +) +{ + if (application == NULL) + { + return NULL; + } + + return application->session; +} +``` + +Le pointeur retourné appartient à `Application`. + +Le code appelant ne doit pas fermer cette session. + +Cet accesseur servira aux futurs contrôleurs et DAO. + +## 11. Mettre à jour la fenêtre principale + +Ajouter dans : + +```text +include/views/main_window.h +``` + +la fonction : + +```c +void main_window_set_investigation( + MainWindow *main_window, + const char *investigation_name, + const char *investigation_root_path +); +``` + +Implémenter dans : + +```text +src/views/main_window.c +``` + +Cette fonction doit : + +- accepter `main_window == NULL` ; +- accepter des chaînes `NULL` sans planter ; +- mettre à jour le titre de la fenêtre ; +- mettre à jour la barre d’état. + +### Titre attendu + +Lorsque le nom est valide : + +```text +Labfy Investigation — +``` + +Exemple : + +```text +Labfy Investigation — Enquete_Session +``` + +Lorsque le nom est absent : + +```text +Labfy Investigation +``` + +Construire le titre avec GLib : + +```c +g_strdup_printf() +``` + +puis le libérer avec : + +```c +g_free() +``` + +### Barre d’état attendue + +Lorsque l’enquête est ouverte : + +```text +Enquête ouverte : +``` + +Lorsque certaines valeurs sont absentes, utiliser une formulation sûre sans déréférencer `NULL`. + +La barre d’état ne doit pas conserver un pointeur vers une chaîne temporaire. + +`gtk_label_set_text()` copie le texte fourni. + +## 12. Récupérer le nom persistant + +Dans `application_on_folder_selected()`, récupérer : + +```c +const InvestigationRecord *record = NULL; +const char *investigation_name = NULL; +``` + +avec : + +```c +record = investigation_session_get_record( + application->session +); + +investigation_name = investigation_record_get_name( + record +); +``` + +Utiliser les données du `InvestigationRecord`. + +Ne pas reconstruire le nom à partir du dernier composant du chemin. + +## 13. Mettre à jour la fenêtre après le modèle + +Après l’installation de la nouvelle session et du nouvel arbre : + +```c +main_window_set_tree_model( + application->main_window, + application->tree_model +); +``` + +puis : + +```c +main_window_set_investigation( + application->main_window, + investigation_name, + root_path +); +``` + +L’ordre attendu est : + +```text +nouvelle session installée +nouvel arbre installé +sidebar mise à jour +titre et statut mis à jour +``` + +## 14. Fermer la session dans `application_free()` + +Remplacer : + +```c +investigation_free(application->investigation); +``` + +par : + +```c +investigation_session_close( + application->session +); +``` + +L’ordre de nettoyage conseillé est : + +```c +investigation_tree_model_free( + application->tree_model +); + +investigation_session_close( + application->session +); + +main_window_free( + application->main_window +); +``` + +Puis libérer `GtkApplication` et `Application` comme actuellement. + +`application_free(NULL)` doit rester valide. + +## 15. Ne plus utiliser l’ancien objet dans l’application + +À la fin du ticket, les symboles suivants ne doivent plus apparaître dans `application.c` : + +```text +Investigation * +investigation_new +investigation_free +investigation_get_root_path +investigation_get_database_path +``` + +Le module historique `investigation.c` n’est pas supprimé dans ce ticket. + +Sa suppression éventuelle sera effectuée séparément après vérification qu’aucun autre composant ne l’utilise. + +--- + +# Tests et validations + +## 16. Tests automatisés existants + +Aucun nouveau test GTK automatisé n’est obligatoire dans ce ticket. + +Tous les tests existants doivent rester valides : + +```bash +make test +``` + +En particulier : + +```text +InvestigationProject +InvestigationSession +InvestigationTreeBuilder +InvestigationTreeModel +InvestigationDao +``` + +## 17. Test manuel d’ouverture valide + +Créer ou utiliser une enquête valide. + +Lancer : + +```bash +make +make run +``` + +Sélectionner le dossier racine de l’enquête. + +Vérifier : + +- la fenêtre reste ouverte ; +- l’arborescence apparaît dans la sidebar ; +- le titre contient le nom persistant de l’enquête ; +- la barre d’état contient le nom et le chemin racine ; +- aucune erreur SQLite n’apparaît ; +- la session reste active après la fin du callback. + +## 18. Test manuel d’annulation + +Relancer l’application et annuler la sélection. + +Vérifier : + +- aucun crash ; +- aucun warning critique ; +- la fenêtre reste ouverte ; +- la barre d’état reste sur : + +```text +Aucune enquête ouverte +``` + +## 19. Test manuel d’un dossier invalide + +Sélectionner un dossier qui ne contient pas : + +```text +00_BaseDeDonnees/Enquete.sqlite +``` + +Vérifier : + +- aucun crash ; +- un warning explicite est affiché ; +- aucun fichier SQLite n’est créé ; +- la fenêtre reste utilisable ; +- aucune fausse enquête n’apparaît dans la sidebar. + +## 20. Test manuel de remplacement + +Si l’interface permet une seconde sélection pendant la même exécution : + +1. ouvrir une enquête valide A ; +2. tenter d’ouvrir un dossier invalide ; +3. vérifier que A reste affichée ; +4. ouvrir une enquête valide B ; +5. vérifier que B remplace A. + +Si une seconde sélection n’est pas encore accessible dans l’interface, cette validation sera complétée lors de l’ajout de l’action « Ouvrir ». + +La logique du callback doit néanmoins déjà préserver l’ancienne session. + +--- + +# Gestion de la mémoire + +Les propriétaires doivent être clairement définis. + +## `Application` possède + +```text +GtkApplication +MainWindow +InvestigationSession +InvestigationTreeModel +``` + +## `InvestigationSession` possède + +```text +InvestigationProject +Database +InvestigationRecord +``` + +## Variables temporaires du callback + +```text +new_session +new_tree_model +GError +``` + +En cas d’échec : + +```text +new_session fermée si elle existe +new_tree_model libéré si nécessaire +GError libéré +ancienne session conservée +ancien tree model conservé +``` + +En cas de succès : + +```text +anciens objets libérés +nouveaux objets transférés dans Application +aucun double free +``` + +--- + +# Critères d’acceptation + +- [ ] `Application` possède un `InvestigationSession *`. +- [ ] `Application` ne possède plus d’`Investigation *`. +- [ ] Le callback ouvre une session avec `investigation_session_open()`. +- [ ] Le chemin racine provient de `InvestigationProject`. +- [ ] Le nom provient d’`InvestigationRecord`. +- [ ] L’arbre est construit depuis le chemin de la session. +- [ ] L’ancienne session reste active si l’ouverture échoue. +- [ ] L’ancien arbre reste actif si l’ouverture échoue. +- [ ] La nouvelle session est fermée si la construction de l’arbre échoue. +- [ ] Les anciens objets ne sont remplacés qu’après validation complète. +- [ ] La session reste ouverte après le callback. +- [ ] `application_free()` ferme la session. +- [ ] `application_get_session(NULL)` retourne `NULL`. +- [ ] `main_window_set_investigation()` accepte `NULL`. +- [ ] Le titre affiche le nom de l’enquête. +- [ ] La barre d’état affiche le nom et le chemin racine. +- [ ] `application.c` n’appelle aucune fonction SQLite. +- [ ] Aucun chemin SQLite n’est reconstruit dans `application.c`. +- [ ] Les anciens tests restent valides. +- [ ] `make` réussit sans warning. +- [ ] `make test` réussit. +- [ ] Le test manuel d’ouverture valide réussit. +- [ ] Le test manuel d’annulation réussit. +- [ ] Le test manuel de dossier invalide réussit. +- [ ] `git diff --check` ne retourne aucune erreur. + +--- + +# Audit attendu + +La commande suivante ne doit rien afficher : + +```bash +rg -n \ + 'investigation_new|investigation_free|investigation_get_root_path|investigation_get_database_path|Investigation \*' \ + src/core/application.c +``` + +La commande suivante ne doit rien afficher : + +```bash +rg -n \ + 'sqlite3_|#include |00_BaseDeDonnees|Enquete.sqlite' \ + src/core/application.c +``` + +Vérifier la présence de la nouvelle session : + +```bash +rg -n \ + 'InvestigationSession|investigation_session_' \ + src/core/application.c \ + include/core/application.h +``` + +Vérifier la mise à jour de la fenêtre : + +```bash +rg -n \ + 'main_window_set_investigation' \ + include/views/main_window.h \ + src/views/main_window.c \ + src/core/application.c +``` + +--- + +# Hors périmètre + +Ce ticket ne doit pas ajouter : + +- une action de menu « Ouvrir » ; +- un raccourci clavier ; +- un dialogue graphique détaillé pour les erreurs ; +- la création d’une enquête ; +- la fermeture manuelle d’une enquête ; +- la réparation d’un chemin racine incohérent ; +- la modification de la base SQLite ; +- les DAO des preuves ou des entités ; +- la restauration automatique de la dernière enquête ; +- la persistance des préférences utilisateur ; +- le verrouillage multi-instance ; +- la suppression de l’ancien module `Investigation`. + +--- + +# Fichiers principalement concernés + +```text +include/core/application.h +src/core/application.c + +include/views/main_window.h +src/views/main_window.c +``` + +Fichiers utilisés sans modification attendue : + +```text +include/core/investigation_session.h +src/core/investigation_session.c + +include/core/investigation_project.h +src/core/investigation_project.c + +include/models/investigation_record.h +src/models/investigation_record.c + +include/core/investigation_tree_builder.h +src/core/investigation_tree_builder.c +``` + +Le `Makefile` ne devrait pas nécessiter de nouvelle cible, car les sources de production sont détectées automatiquement avec : + +```make +SRC := $(shell find src -name "*.c") +``` + +--- + +# Résultat attendu + +À la fin du ticket, la sélection d’un dossier doit ouvrir une véritable session d’enquête. + +L’application doit conserver ensemble : + +```text +la connexion SQLite +les métadonnées persistées +le contexte de fichiers +l’arborescence +l’état visuel de la fenêtre +``` + +Le flux final doit être : + +```text +sélection du dossier + ↓ +InvestigationSession ouverte + ↓ +InvestigationTreeModel construit + ↓ +session installée dans Application + ↓ +arbre affiché dans MainWindow + ↓ +nom et chemin affichés +``` + +--- + +# Commit attendu + +Avant le commit : + +```bash +make clean +make +make test +git diff --check +git status --short +``` + +Préparer les fichiers : + +```bash +git add \ + include/core/application.h \ + src/core/application.c \ + include/views/main_window.h \ + src/views/main_window.c +``` + +Contrôler : + +```bash +git diff --cached --stat +git diff --cached +``` + +Créer le commit : + +```bash +git commit -m "feat(app): integrate investigation session" +``` + +Puis pousser après validation complète : + +```bash +git push +``` + diff --git a/docs/tickets/closed/TICKET-030.md b/docs/tickets/closed/TICKET-030.md new file mode 100644 index 0000000..a2bd846 --- /dev/null +++ b/docs/tickets/closed/TICKET-030.md @@ -0,0 +1,1115 @@ +# Ticket #030 — Créer une enquête depuis l’interface GTK + +## Contexte + +Le ticket #029 a intégré `InvestigationSession` au cycle de vie de l’application. + +L’application sait désormais : + +- ouvrir une enquête existante ; +- conserver sa connexion SQLite ; +- charger ses métadonnées persistées ; +- construire son arborescence ; +- afficher son nom et son chemin dans la fenêtre principale ; +- conserver l’ancienne enquête si une nouvelle ouverture échoue. + +Cependant, lorsqu’aucune enquête n’existe encore, l’utilisateur ne peut pas en créer une depuis l’interface. + +Le dossier sélectionné est actuellement toujours interprété comme une enquête existante. Si la base suivante est absente : + +```text +00_BaseDeDonnees/Enquete.sqlite +``` + +l’ouverture échoue, ce qui est volontairement sûr. + +La création d’une nouvelle enquête doit être une action explicite et séparée de l’ouverture. + +--- + +# Objectif + +Ajouter une première fonctionnalité GTK réellement utilisable : + +```text +Créer une nouvelle enquête +``` + +Le flux attendu est : + +```text +clic sur « Nouvelle enquête » + ↓ +sélection du dossier parent + ↓ +saisie du nom de l’enquête + ↓ +validation des paramètres + ↓ +investigation_project_create() + ↓ +investigation_session_open() + ↓ +construction de l’arborescence + ↓ +installation dans Application + ↓ +mise à jour de MainWindow +``` + +L’utilisateur ne doit jamais avoir à créer manuellement : + +```text +00_BaseDeDonnees +Enquete.sqlite +01_Preuves_Originales +... +09_Hash +``` + +--- + +# Architecture attendue + +```text +MainWindow + │ + └── bouton « Nouvelle enquête » + │ + ▼ +CreateInvestigationDialog + │ + ├── dossier parent + ├── nom de l’enquête + └── validation + │ + ▼ +Application + │ + ├── investigation_project_create() + ├── investigation_session_open() + ├── investigation_tree_builder_build() + └── main_window_set_investigation() +``` + +--- + +# Travail à réaliser + +## 1. Créer un module de dialogue dédié + +Créer : + +```text +include/views/create_investigation_dialog.h +src/views/create_investigation_dialog.c +``` + +Le module doit rester indépendant de SQLite. + +Il ne doit pas inclure : + +```c +#include +``` + +Il ne doit pas appeler : + +```text +database_initialize +investigation_project_create +investigation_session_open +``` + +Son rôle est uniquement de recueillir les informations saisies par l’utilisateur. + +--- + +## 2. Définir le callback public + +Dans : + +```text +include/views/create_investigation_dialog.h +``` + +déclarer : + +```c +typedef void (*CreateInvestigationDialogCallback)( + const char *parent_directory, + const char *investigation_name, + gpointer user_data +); +``` + +Puis : + +```c +void create_investigation_dialog_present( + GtkWindow *parent_window, + CreateInvestigationDialogCallback callback, + gpointer user_data +); +``` + +Le callback reçoit : + +```text +parent_directory +investigation_name +user_data +``` + +En cas d’annulation : + +```text +parent_directory == NULL +investigation_name == NULL +``` + +Les chaînes transmises au callback ne restent valides que pendant l’appel. + +Le callback doit les copier s’il souhaite les conserver. + +--- + +## 3. Concevoir le dialogue GTK + +Le dialogue doit contenir au minimum : + +```text +Titre : Nouvelle enquête + +Dossier parent : +[ chemin sélectionné ] [ Parcourir ] + +Nom de l’enquête : +[ ] + +[ Annuler ] [ Créer ] +``` + +Le bouton `Créer` doit être désactivé tant que : + +```text +aucun dossier parent valide n’est sélectionné +ou +le nom est vide +``` + +Le dialogue peut utiliser : + +```text +GtkWindow +GtkBox +GtkLabel +GtkEntry +GtkButton +GtkFileDialog +``` + +Ne pas utiliser les anciennes API GTK3 synchrones. + +--- + +## 4. Sélectionner le dossier parent + +Le bouton : + +```text +Parcourir +``` + +doit ouvrir un sélecteur de dossier GTK4. + +Le dossier choisi représente le parent dans lequel le nouveau dossier d’enquête sera créé. + +Exemple : + +```text +Dossier parent : +/home/fy59/Documents/Enquetes + +Nom : +Arnaque_Billets +``` + +Résultat attendu : + +```text +/home/fy59/Documents/Enquetes/Arnaque_Billets +``` + +Le dossier parent doit déjà exister. + +--- + +## 5. Valider le nom dans le dialogue + +Le nom doit être refusé s’il est : + +```text +NULL +vide +uniquement composé d’espaces +``` + +Il doit aussi être refusé s’il contient un séparateur de chemin : + +```text +/ +``` + +et, pour rester portable : + +```text +\ +``` + +Exemples invalides : + +```text +Enquetes/Test +Enquetes\Test +``` + +Les espaces en début et fin doivent être supprimés avant l’envoi au callback. + +Utiliser : + +```c +g_strstrip() +``` + +sur une copie allouée. + +Le dialogue ne doit pas modifier directement le contenu interne de `GtkEntry`. + +--- + +## 6. Ajouter le bouton dans `MainWindow` + +Modifier : + +```text +include/views/main_window.h +src/views/main_window.c +``` + +Ajouter un bouton visible : + +```text +Nouvelle enquête +``` + +Il peut être placé dans une barre horizontale au-dessus du `GtkPaned`. + +Organisation attendue : + +```text +MainWindow +└── main_box + ├── action_bar + │ └── bouton Nouvelle enquête + ├── main_paned + └── status_label +``` + +Ajouter dans la structure privée : + +```c +GtkWidget *action_bar; +GtkWidget *new_investigation_button; +``` + +--- + +## 7. Ajouter un callback de fenêtre + +Définir dans : + +```text +include/views/main_window.h +``` + +un type de callback : + +```c +typedef void (*MainWindowNewInvestigationCallback)( + gpointer user_data +); +``` + +Ajouter : + +```c +void main_window_set_new_investigation_callback( + MainWindow *main_window, + MainWindowNewInvestigationCallback callback, + gpointer user_data +); +``` + +`MainWindow` ne doit pas créer elle-même l’enquête. + +Elle ne doit faire que transmettre le clic au contrôleur `Application`. + +--- + +## 8. Conserver les données de callback + +Dans la structure privée de `MainWindow`, ajouter : + +```c +MainWindowNewInvestigationCallback + new_investigation_callback; + +gpointer + new_investigation_user_data; +``` + +Le bouton GTK doit être relié à un callback privé : + +```c +static void main_window_on_new_investigation_clicked( + GtkButton *button, + gpointer user_data +); +``` + +Ce callback doit appeler : + +```c +main_window->new_investigation_callback( + main_window->new_investigation_user_data +); +``` + +uniquement si le callback est défini. + +--- + +## 9. Ajouter le contrôleur dans `Application` + +Modifier : + +```text +src/core/application.c +``` + +Ajouter : + +```c +static void application_on_new_investigation_requested( + gpointer user_data +); +``` + +Cette fonction doit ouvrir : + +```c +create_investigation_dialog_present() +``` + +en utilisant : + +```c +main_window_get_window( + application->main_window +); +``` + +--- + +## 10. Traiter le résultat du dialogue + +Ajouter : + +```c +static void application_on_create_investigation( + const char *parent_directory, + const char *investigation_name, + gpointer user_data +); +``` + +En cas d’annulation : + +```c +parent_directory == NULL +investigation_name == NULL +``` + +la fonction doit simplement retourner. + +Aucun état existant ne doit être modifié. + +--- + +## 11. Créer le projet + +Appeler : + +```c +char *created_root_path = NULL; +``` + +puis : + +```c +created_root_path = investigation_project_create( + parent_directory, + investigation_name +); +``` + +Si la création échoue : + +```text +ancienne session conservée +ancien arbre conservé +aucune modification de MainWindow +warning explicite +``` + +Exemple : + +```c +g_warning( + "Impossible de créer l'enquête '%s' dans '%s'.", + investigation_name, + parent_directory +); +``` + +--- + +## 12. Ouvrir immédiatement la nouvelle enquête + +Après une création valide, ouvrir : + +```c +InvestigationSession *new_session = NULL; +GError *error = NULL; +``` + +avec : + +```c +new_session = investigation_session_open( + created_root_path, + &error +); +``` + +La nouvelle enquête doit être utilisable sans redémarrer l’application. + +--- + +## 13. Construire son arbre + +Récupérer : + +```c +const InvestigationProject *project = NULL; +const char *root_path = NULL; +``` + +Puis construire : + +```c +InvestigationTreeModel *new_tree_model = NULL; +``` + +avec : + +```c +new_tree_model = investigation_tree_builder_build( + root_path +); +``` + +--- + +## 14. Factoriser l’installation d’une session + +Le ticket #029 contient déjà une logique de remplacement dans : + +```c +application_on_folder_selected() +``` + +Cette logique ne doit pas être dupliquée. + +Créer une fonction privée : + +```c +static gboolean application_install_session( + Application *application, + InvestigationSession *new_session, + InvestigationTreeModel *new_tree_model +); +``` + +Cette fonction doit : + +1. valider ses paramètres ; +2. récupérer le projet ; +3. récupérer le record ; +4. récupérer le chemin racine ; +5. récupérer le nom ; +6. libérer l’ancien arbre ; +7. fermer l’ancienne session ; +8. installer les nouveaux objets ; +9. mettre à jour la sidebar ; +10. mettre à jour le titre et la barre d’état. + +Elle retourne : + +```text +TRUE en cas de succès +FALSE en cas d’échec +``` + +--- + +## 15. Propriété des objets dans la fonction factorisée + +Avant l’appel réussi à : + +```c +application_install_session() +``` + +le code appelant possède : + +```text +new_session +new_tree_model +``` + +En cas de succès, `Application` devient propriétaire des deux objets. + +En cas d’échec, le code appelant reste propriétaire et doit les libérer. + +Cette règle doit être documentée clairement. + +--- + +## 16. Adapter l’ouverture existante + +Modifier : + +```c +application_on_folder_selected() +``` + +pour utiliser également : + +```c +application_install_session() +``` + +Le flux devient : + +```text +investigation_session_open() + ↓ +investigation_tree_builder_build() + ↓ +application_install_session() +``` + +Cela garantit que l’ouverture et la création utilisent exactement le même mécanisme d’installation. + +--- + +## 17. Gérer un échec après création du projet + +Si le dossier et la base ont été créés avec succès mais que : + +```text +investigation_session_open() +``` + +ou : + +```text +investigation_tree_builder_build() +``` + +échouent, ne pas supprimer automatiquement le nouveau projet. + +Raison : + +```text +la création SQLite a pu réussir +le dossier contient potentiellement déjà des informations utiles +une suppression automatique après création complète serait risquée +``` + +Afficher un warning explicite indiquant que le projet a été créé mais n’a pas pu être ouvert. + +Exemple : + +```text +L’enquête a été créée dans '', mais son ouverture a échoué. +``` + +Le chemin doit être laissé à l’utilisateur pour diagnostic. + +--- + +## 18. Mettre à jour le statut pendant la création + +Optionnel mais recommandé : + +Avant la création : + +```text +Création de l’enquête… +``` + +Après succès : + +```text +Enquête ouverte : +``` + +En cas d’échec, le statut précédent doit être restauré ou conservé. + +Ne pas laisser le statut bloqué sur : + +```text +Création de l’enquête… +``` + +si l’opération échoue. + +--- + +## 19. Ajouter une fonction de statut générique + +Pour éviter que `Application` manipule directement `GtkLabel`, ajouter dans : + +```text +include/views/main_window.h +``` + +```c +void main_window_set_status( + MainWindow *main_window, + const char *status_text +); +``` + +Implémenter dans : + +```text +src/views/main_window.c +``` + +La fonction doit : + +- accepter `main_window == NULL` ; +- accepter `status_text == NULL` ; +- afficher une chaîne sûre ; +- utiliser `gtk_label_set_text()`. + +Pour `status_text == NULL`, afficher : + +```text +Aucune enquête ouverte +``` + +--- + +# Tests manuels + +## 20. Création valide + +Lancer : + +```bash +make +make run +``` + +Cliquer sur : + +```text +Nouvelle enquête +``` + +Sélectionner : + +```text +/home/fy59/Documents/Enquetes +``` + +Saisir par exemple : + +```text +Test_Enquete +``` + +Vérifier la création de : + +```text +/home/fy59/Documents/Enquetes/Test_Enquete/ +``` + +Vérifier la présence de : + +```text +00_BaseDeDonnees/Enquete.sqlite +01_Preuves_Originales +02_Preuves_Traitees +03_Chronologie +04_Entites +05_Rapports +06_Exports +07_Notes +08_Sources +09_Hash +``` + +Vérifier aussi : + +```text +arborescence visible +titre mis à jour +barre d’état mise à jour +aucune erreur SQLite +``` + +--- + +## 21. Nom vide + +Laisser le nom vide. + +Le bouton `Créer` doit rester désactivé. + +Aucun dossier ne doit être créé. + +--- + +## 22. Nom composé d’espaces + +Saisir uniquement : + +```text + +``` + +Le bouton `Créer` doit rester désactivé ou la validation doit refuser l’opération. + +Aucun dossier ne doit être créé. + +--- + +## 23. Nom contenant un séparateur + +Tester : + +```text +Test/Enquete +``` + +puis : + +```text +Test\Enquete +``` + +La création doit être refusée. + +--- + +## 24. Dossier déjà existant + +Créer une première fois : + +```text +Test_Enquete +``` + +Puis tenter de recréer le même nom dans le même dossier parent. + +Vérifier : + +```text +aucun écrasement +aucune modification du projet existant +warning explicite +ancienne session conservée +``` + +--- + +## 25. Annulation du dialogue + +Ouvrir le dialogue puis cliquer sur : + +```text +Annuler +``` + +Vérifier : + +```text +aucun crash +aucun dossier créé +ancienne session conservée +fenêtre toujours utilisable +``` + +--- + +## 26. Création après ouverture d’une enquête + +Lorsque l’action « Nouvelle enquête » est disponible pendant une session active : + +1. ouvrir une enquête A ; +2. créer une enquête B ; +3. vérifier que B remplace A uniquement après création et ouverture complètes. + +Si la création de B échoue, A doit rester active. + +--- + +# Gestion de la mémoire + +## Dialogue + +Le dialogue possède : + +```text +sa fenêtre GTK +ses widgets +ses chaînes temporaires +``` + +Il doit être détruit après : + +```text +création +annulation +fermeture de la fenêtre +``` + +## Application + +`Application` possède après succès : + +```text +InvestigationSession +InvestigationTreeModel +``` + +## Variables temporaires + +Le callback de création possède temporairement : + +```text +created_root_path +new_session +new_tree_model +GError +``` + +Tous les chemins d’échec doivent libérer les ressources qu’ils possèdent encore. + +--- + +# Critères d’acceptation + +- [ ] Un bouton `Nouvelle enquête` est visible. +- [ ] Le bouton ouvre un dialogue dédié. +- [ ] Le dialogue permet de sélectionner un dossier parent. +- [ ] Le dialogue permet de saisir un nom. +- [ ] Le nom vide est refusé. +- [ ] Le nom composé d’espaces est refusé. +- [ ] Les séparateurs `/` et `\` sont refusés. +- [ ] Le bouton `Créer` n’est actif que lorsque les données sont valides. +- [ ] La création utilise `investigation_project_create()`. +- [ ] La nouvelle enquête est ouverte avec `investigation_session_open()`. +- [ ] L’arborescence est construite automatiquement. +- [ ] La session est installée dans `Application`. +- [ ] Le nom et le chemin sont affichés. +- [ ] L’ancienne session est conservée en cas d’échec. +- [ ] L’ancien arbre est conservé en cas d’échec. +- [ ] La logique d’installation n’est pas dupliquée. +- [ ] Le dialogue ne connaît ni SQLite ni `Database`. +- [ ] Aucun dossier existant n’est écrasé. +- [ ] L’annulation ne modifie aucun état. +- [ ] Les anciens tests restent valides. +- [ ] `make` réussit sans warning. +- [ ] `make test` réussit. +- [ ] Le test manuel de création valide réussit. +- [ ] `git diff --check` ne retourne aucune erreur. + +--- + +# Audit attendu + +Le dialogue ne doit contenir aucune dépendance métier : + +```bash +rg -n \ + 'sqlite3_|database_|investigation_project_create|investigation_session_open' \ + include/views/create_investigation_dialog.h \ + src/views/create_investigation_dialog.c +``` + +Résultat attendu : + +```text +aucune sortie +``` + +Vérifier la factorisation : + +```bash +rg -n \ + 'application_install_session' \ + src/core/application.c +``` + +Vérifier que les deux flux l’utilisent : + +```text +application_on_folder_selected +application_on_create_investigation +``` + +Vérifier l’absence de SQLite dans `Application` : + +```bash +rg -n \ + 'sqlite3_|#include ' \ + src/core/application.c +``` + +Résultat attendu : + +```text +aucune sortie +``` + +--- + +# Hors périmètre + +Ce ticket ne doit pas ajouter : + +- une barre de menu complète ; +- un raccourci clavier ; +- la suppression d’une enquête ; +- le renommage d’une enquête ; +- le déplacement d’une enquête ; +- la restauration de la dernière enquête ; +- une liste des enquêtes récentes ; +- une confirmation de fermeture ; +- l’import de preuves ; +- les DAO des preuves ; +- une boîte d’erreur avancée ; +- la gestion de modèles d’enquête personnalisés. + +--- + +# Fichiers principalement concernés + +```text +include/views/create_investigation_dialog.h +src/views/create_investigation_dialog.c + +include/views/main_window.h +src/views/main_window.c + +src/core/application.c +``` + +Le fichier suivant ne devrait pas nécessiter de modification : + +```text +include/core/application.h +``` + +Le `Makefile` de production détecte automatiquement le nouveau fichier `.c` avec : + +```make +SRC := $(shell find src -name "*.c") +``` + +Aucune nouvelle cible de test n’est obligatoire pour ce ticket GTK. + +--- + +# Résultat attendu + +À la fin du ticket, l’utilisateur doit pouvoir lancer l’application sans disposer d’une enquête préalable. + +Il doit pouvoir : + +```text +ouvrir l’application + ↓ +cliquer sur « Nouvelle enquête » + ↓ +choisir ~/Documents/Enquetes + ↓ +saisir un nom + ↓ +créer l’enquête + ↓ +voir immédiatement son arborescence +``` + +Cette fonctionnalité constitue la première opération complète utilisable depuis GTK. + +--- + +# Commit attendu + +Avant le commit : + +```bash +make clean +make +make test +git diff --check +git status --short +``` + +Préparer les fichiers : + +```bash +git add \ + include/views/create_investigation_dialog.h \ + src/views/create_investigation_dialog.c \ + include/views/main_window.h \ + src/views/main_window.c \ + src/core/application.c +``` + +Contrôler : + +```bash +git diff --cached --stat +git diff --cached +``` + +Créer le commit : + +```bash +git commit -m "feat(ui): add investigation creation workflow" +``` + +Puis pousser après validation complète : + +```bash +git push +``` diff --git a/include/core/application.h b/include/core/application.h index f0c8eeb..7dd9d6e 100644 --- a/include/core/application.h +++ b/include/core/application.h @@ -6,6 +6,11 @@ #ifndef LABFY_INVESTIGATION_APPLICATION_H #define LABFY_INVESTIGATION_APPLICATION_H +/** + * @brief Représentation opaque d'une session d'enquête. + */ +typedef struct InvestigationSession InvestigationSession; + /** * @brief Représentation opaque de l'application. * @@ -29,7 +34,26 @@ Application *application_new(void); * * @return Code de sortie retourné par GTK. */ -int application_run(Application *application, int argc, char **argv); +int application_run( + Application *application, + int argc, + char **argv +); + +/** + * @brief Retourne la session d'enquête actuellement ouverte. + * + * Le pointeur retourné appartient à l'application. + * Le code appelant ne doit pas fermer cette session. + * + * @param application Application concernée. + * + * @return La session active, ou NULL si aucune enquête n'est ouverte + * ou si application vaut NULL. + */ +const InvestigationSession *application_get_session( + const Application *application +); /** * @brief Libère les ressources possédées par l'application. @@ -38,6 +62,8 @@ int application_run(Application *application, int argc, char **argv); * * @param application Application à libérer. */ -void application_free(Application *application); +void application_free( + Application *application +); #endif diff --git a/include/views/create_investigation_dialog.h b/include/views/create_investigation_dialog.h new file mode 100644 index 0000000..8af568d --- /dev/null +++ b/include/views/create_investigation_dialog.h @@ -0,0 +1,58 @@ +/****************************************************************************** + * @file create_investigation_dialog.h + * @brief Interface publique du dialogue de création d'une enquête. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_CREATE_INVESTIGATION_DIALOG_H +#define LABFY_INVESTIGATION_CREATE_INVESTIGATION_DIALOG_H + +#include + +/** + * @brief Callback appelé lorsque le dialogue est validé ou annulé. + * + * Lors d'une validation réussie : + * + * - parent_directory contient le dossier parent sélectionné ; + * - investigation_name contient le nom nettoyé de l'enquête. + * + * Lors d'une annulation : + * + * - parent_directory vaut NULL ; + * - investigation_name vaut NULL. + * + * Les chaînes transmises restent valides uniquement pendant l'appel du + * callback. Le code appelant doit les copier s'il souhaite les conserver. + * + * @param parent_directory Dossier parent sélectionné, ou NULL. + * @param investigation_name Nom de l'enquête, ou NULL. + * @param user_data Données utilisateur fournies lors de l'ouverture. + */ +typedef void (*CreateInvestigationDialogCallback)( + const char *parent_directory, + const char *investigation_name, + gpointer user_data +); + +/** + * @brief Présente le dialogue de création d'une nouvelle enquête. + * + * Le dialogue permet : + * + * - de sélectionner un dossier parent existant ; + * - de saisir le nom de l'enquête ; + * - de valider ou d'annuler la création. + * + * Cette fonction ne crée aucun fichier et n'accède pas à SQLite. + * + * @param parent_window Fenêtre GTK parente, ou NULL. + * @param callback Fonction appelée lors de la validation ou de l'annulation. + * @param user_data Données transmises au callback. + */ +void create_investigation_dialog_present( + GtkWindow *parent_window, + CreateInvestigationDialogCallback callback, + gpointer user_data +); + +#endif diff --git a/include/views/main_window.h b/include/views/main_window.h index 8253671..3fce982 100644 --- a/include/views/main_window.h +++ b/include/views/main_window.h @@ -17,6 +17,31 @@ */ typedef struct MainWindow MainWindow; +/** + * @brief Callback appelé lorsque l'utilisateur demande une nouvelle enquête. + * + * @param user_data Données utilisateur associées au callback. + */ +typedef void (*MainWindowNewInvestigationCallback)( + gpointer user_data +); + +/** + * @brief Définit le callback du bouton « Nouvelle enquête ». + * + * La fenêtre ne crée pas directement l'enquête. Elle transmet uniquement + * la demande au contrôleur. + * + * @param main_window Fenêtre principale. + * @param callback Fonction appelée lors du clic. + * @param user_data Données transmises au callback. + */ +void main_window_set_new_investigation_callback( + MainWindow *main_window, + MainWindowNewInvestigationCallback callback, + gpointer user_data +); + /** * @brief Crée une nouvelle fenêtre principale. * @@ -58,6 +83,28 @@ void main_window_set_tree_model( const InvestigationTreeModel *tree_model ); +/** + * @brief Met à jour l'identité de l'enquête affichée. + * + * La fonction met à jour : + * + * - le titre de la fenêtre ; + * - le texte de la barre d'état. + * + * Les chaînes sont copiées par GTK et restent la propriété de l'appelant. + * + * Cette fonction accepte des chaînes NULL ou vides. + * + * @param main_window Fenêtre principale à mettre à jour. + * @param investigation_name Nom persistant de l'enquête. + * @param investigation_root_path Chemin racine de l'enquête. + */ +void main_window_set_investigation( + MainWindow *main_window, + const char *investigation_name, + const char *investigation_root_path +); + void main_window_set_tree_selection_callback( MainWindow *main_window, InvestigationTreeViewSelectionCallback callback, @@ -77,6 +124,19 @@ void main_window_set_selected_node( const InvestigationNode *node ); +/** + * @brief Met à jour le texte de la barre d'état. + * + * Si status_text vaut NULL, le texte par défaut est affiché. + * + * @param main_window Fenêtre principale. + * @param status_text Nouveau texte de statut, ou NULL. + */ +void main_window_set_status( + MainWindow *main_window, + const char *status_text +); + /** * @brief Libère les ressources de la fenêtre. * diff --git a/labfy-investigation b/labfy-investigation index 6f07ba3..4c62c19 100755 Binary files a/labfy-investigation and b/labfy-investigation differ diff --git a/src/core/application.c b/src/core/application.c index 1213b1f..6afad4b 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -4,11 +4,15 @@ ******************************************************************************/ #include "core/application.h" -#include "views/folder_dialog.h" -#include "core/investigation.h" -#include "views/main_window.h" -#include "core/investigation_tree_builder.h" + +#include "core/investigation_node.h" +#include "core/investigation_project.h" +#include "core/investigation_session.h" #include "core/investigation_tree_model.h" +#include "models/investigation_record.h" +#include "core/investigation_tree_builder.h" +#include "views/create_investigation_dialog.h" +#include "views/main_window.h" #include @@ -18,101 +22,291 @@ #define APPLICATION_ID "com.labfytools.investigation" /** - * @brief Dimensions initiales de la fenêtre principale. - */ -#define DEFAULT_WINDOW_WIDTH 1000 -#define DEFAULT_WINDOW_HEIGHT 650 - -/** - * @struct Application * @brief État interne de l'application. * - * Cette structure reste privée à ce module afin d'empêcher les autres + * Cette structure reste privée au module afin d'empêcher les autres * composants de manipuler directement les objets GTK. */ struct Application { GtkApplication *gtk_application; MainWindow *main_window; - Investigation *investigation; + InvestigationSession *session; InvestigationTreeModel *tree_model; }; /** - * @brief Crée la fenêtre minimale lors de l'activation de l'application. + * @brief Installe une nouvelle session et son arbre dans l'application. * - * Cette fonction est privée au module. Elle est appelée par GTK lorsque - * l'application reçoit le signal "activate". + * En cas de succès, Application devient propriétaire de new_session + * et de new_tree_model. * - * @param gtk_application Application GTK ayant reçu le signal. - * @param user_data Données utilisateur associées au signal. + * En cas d'échec, le code appelant conserve la propriété des deux objets. + * + * @param application Application à mettre à jour. + * @param new_session Nouvelle session valide. + * @param new_tree_model Nouvel arbre valide. + * + * @return TRUE si l'installation réussit, sinon FALSE. */ +static gboolean application_install_session( + Application *application, + InvestigationSession *new_session, + InvestigationTreeModel *new_tree_model +) +{ + const InvestigationProject *project = NULL; + const InvestigationRecord *record = NULL; -static void application_on_folder_selected( - const char *folder_path, + const char *root_path = NULL; + const char *investigation_name = NULL; + + if (application == NULL || + application->main_window == NULL || + new_session == NULL || + new_tree_model == NULL) + { + return FALSE; + } + + project = investigation_session_get_project( + new_session + ); + + record = investigation_session_get_record( + new_session + ); + + if (project == NULL || + record == NULL) + { + return FALSE; + } + + root_path = investigation_project_get_root_path( + project + ); + + investigation_name = investigation_record_get_name( + record + ); + + if (root_path == NULL || + root_path[0] == '\0' || + investigation_name == NULL || + investigation_name[0] == '\0') + { + return FALSE; + } + + /* + * Les nouveaux objets sont entièrement valides. + * Les anciens peuvent maintenant être libérés. + */ + investigation_tree_model_free( + application->tree_model + ); + + investigation_session_close( + application->session + ); + + application->tree_model = new_tree_model; + application->session = new_session; + + main_window_set_tree_model( + application->main_window, + application->tree_model + ); + + main_window_set_investigation( + application->main_window, + investigation_name, + root_path + ); + + return TRUE; +} + +/** + * @brief Traite la demande de création d'une nouvelle enquête. + * + * @param parent_directory Dossier dans lequel créer l'enquête. + * @param investigation_name Nom nettoyé de l'enquête. + * @param user_data Pointeur vers Application. + */ +static void application_on_create_investigation( + const char *parent_directory, + const char *investigation_name, gpointer user_data ) { Application *application = user_data; - Investigation *new_investigation = NULL; + + char *created_root_path = NULL; + + InvestigationSession *new_session = NULL; InvestigationTreeModel *new_tree_model = NULL; + const InvestigationProject *project = NULL; + const char *root_path = NULL; + + GError *error = NULL; + if (application == NULL) { return; } - if (folder_path == NULL) + /* + * Une annulation du dialogue transmet deux pointeurs NULL. + */ + if (parent_directory == NULL || + investigation_name == NULL) { - g_print("Sélection annulée.\n"); return; } - new_investigation = investigation_new(folder_path); + created_root_path = investigation_project_create( + parent_directory, + investigation_name + ); - if (new_investigation == NULL) + if (created_root_path == NULL) { g_warning( - "Impossible de créer l'enquête à partir du dossier sélectionné." + "Impossible de créer l'enquête '%s' dans '%s'.", + investigation_name, + parent_directory ); + + return; + } + + new_session = investigation_session_open( + created_root_path, + &error + ); + + if (new_session == NULL) + { + g_warning( + "L'enquête a été créée dans '%s', mais son ouverture " + "a échoué : %s", + created_root_path, + error != NULL + ? error->message + : "erreur inconnue" + ); + + g_clear_error(&error); + g_free(created_root_path); + + return; + } + + project = investigation_session_get_project( + new_session + ); + + if (project != NULL) + { + root_path = investigation_project_get_root_path( + project + ); + } + + if (root_path == NULL || + root_path[0] == '\0') + { + g_warning( + "L'enquête a été créée dans '%s', mais la session " + "ne fournit aucun chemin racine valide.", + created_root_path + ); + + investigation_session_close(new_session); + g_free(created_root_path); + return; } new_tree_model = investigation_tree_builder_build( - investigation_get_root_path(new_investigation) + root_path ); if (new_tree_model == NULL) { g_warning( - "Impossible de construire l'arborescence de l'enquête." + "L'enquête a été créée dans '%s', mais son " + "arborescence n'a pas pu être construite.", + created_root_path ); - investigation_free(new_investigation); + investigation_session_close(new_session); + g_free(created_root_path); + return; } - /* - * Les nouveaux objets sont valides. - * On peut maintenant remplacer les anciens sans perdre l'enquête - * déjà ouverte en cas d'échec. - */ - investigation_tree_model_free(application->tree_model); - investigation_free(application->investigation); + if (!application_install_session( + application, + new_session, + new_tree_model + )) + { + g_warning( + "L'enquête a été créée dans '%s', mais la session " + "n'a pas pu être installée dans l'application.", + created_root_path + ); - application->tree_model = new_tree_model; - application->investigation = new_investigation; + investigation_tree_model_free( + new_tree_model + ); - main_window_set_tree_model( - application->main_window, - application->tree_model + investigation_session_close( + new_session + ); + + g_free(created_root_path); + + return; + } + + g_free(created_root_path); +} + +/** + * @brief Ouvre le dialogue de création d'une enquête. + * + * @param user_data Pointeur vers Application. + */ +static void application_on_new_investigation_requested( + gpointer user_data +) +{ + Application *application = user_data; + + if (application == NULL || + application->main_window == NULL) + { + return; + } + + create_investigation_dialog_present( + main_window_get_window( + application->main_window + ), + application_on_create_investigation, + application ); } /** * @brief Traite la sélection d'un nœud dans l'arborescence. * - * @param node Nœud sélectionné, ou NULL si aucune sélection. + * @param node Nœud sélectionné, ou NULL si aucune sélection. * @param user_data Pointeur vers Application. */ static void application_on_tree_node_selected( @@ -140,12 +334,19 @@ static void application_on_tree_node_selected( return; } - node_name = investigation_node_get_name(node); - node_type = investigation_node_get_type(node); + node_name = investigation_node_get_name( + node + ); + + node_type = investigation_node_get_type( + node + ); g_print( "Nœud sélectionné : %s\n", - node_name != NULL ? node_name : "(sans nom)" + node_name != NULL + ? node_name + : "(sans nom)" ); if (node_type == INVESTIGATION_NODE_DIRECTORY) @@ -158,6 +359,15 @@ static void application_on_tree_node_selected( } } +/** + * @brief Crée la fenêtre principale lors de l'activation. + * + * Le signal activate peut être reçu plusieurs fois. Si la fenêtre existe + * déjà, elle est simplement présentée. + * + * @param gtk_application Application GTK ayant reçu le signal. + * @param user_data Pointeur vers Application. + */ static void application_on_activate( GtkApplication *gtk_application, gpointer user_data @@ -170,21 +380,25 @@ static void application_on_activate( return; } - /* - * Le signal "activate" peut être reçu plusieurs fois. Si la fenêtre - * existe déjà, il suffit de la présenter au lieu d'en créer une seconde. - */ if (application->main_window != NULL) { - main_window_present(application->main_window); + main_window_present( + application->main_window + ); + return; } - application->main_window = main_window_new(gtk_application); + application->main_window = main_window_new( + gtk_application + ); if (application->main_window == NULL) { - g_warning("Impossible de créer la fenêtre principale."); + g_warning( + "Impossible de créer la fenêtre principale." + ); + return; } @@ -193,21 +407,34 @@ static void application_on_activate( application_on_tree_node_selected, application ); - - main_window_present(application->main_window); - folder_dialog_select_folder( - main_window_get_window(application->main_window), - application_on_folder_selected, + main_window_set_new_investigation_callback( + application->main_window, + application_on_new_investigation_requested, application ); + + main_window_present( + application->main_window + ); + +/* folder_dialog_select_folder( + main_window_get_window( + application->main_window + ), + application_on_folder_selected, + application + ); */ } Application *application_new(void) { Application *application = NULL; - application = g_new0(Application, 1); + application = g_new0( + Application, + 1 + ); application->gtk_application = gtk_application_new( APPLICATION_ID, @@ -231,39 +458,65 @@ Application *application_new(void) } int application_run( - Application *application, - int argc, - char **argv + Application *application, + int argc, + char **argv ) { - if (application == NULL || application->gtk_application == NULL) + if (application == NULL || + application->gtk_application == NULL) { return 1; } return g_application_run( - G_APPLICATION(application->gtk_application), + G_APPLICATION( + application->gtk_application + ), argc, argv ); } -void application_free(Application *application) +const InvestigationSession *application_get_session( + const Application *application +) +{ + if (application == NULL) + { + return NULL; + } + + return application->session; +} + +void application_free( + Application *application +) { if (application == NULL) { return; } - investigation_tree_model_free(application->tree_model); - investigation_free(application->investigation); - main_window_free(application->main_window); + investigation_tree_model_free( + application->tree_model + ); + + investigation_session_close( + application->session + ); + + main_window_free( + application->main_window + ); if (application->gtk_application != NULL) { - g_object_unref(application->gtk_application); + g_object_unref( + application->gtk_application + ); } g_free(application); } - diff --git a/src/views/create_investigation_dialog.c b/src/views/create_investigation_dialog.c new file mode 100644 index 0000000..c501b96 --- /dev/null +++ b/src/views/create_investigation_dialog.c @@ -0,0 +1,864 @@ +/****************************************************************************** + * @file create_investigation_dialog.c + * @brief Dialogue GTK de création d'une nouvelle enquête. + ******************************************************************************/ + +#include "views/create_investigation_dialog.h" + +#include +#include + +/** + * @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 + ); +} diff --git a/src/views/main_window.c b/src/views/main_window.c index 7157d52..94ab0c4 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -25,6 +25,18 @@ */ #define MAIN_WINDOW_SIDEBAR_POSITION 250 +/** + * @brief Titre par défaut de la fenêtre. + */ +#define MAIN_WINDOW_DEFAULT_TITLE \ + "Labfy Investigation" + +/** + * @brief Texte affiché lorsqu'aucune enquête n'est ouverte. + */ +#define MAIN_WINDOW_NO_INVESTIGATION_STATUS \ + "Aucune enquête ouverte" + /** * @struct MainWindow * @brief Représentation interne de la fenêtre principale. @@ -35,13 +47,49 @@ struct MainWindow { GtkWindow *window; + GtkWidget *main_box; + GtkWidget *action_bar; + GtkWidget *new_investigation_button; GtkWidget *main_paned; - Workspace *workspace; GtkWidget *status_label; + Sidebar *sidebar; + Workspace *workspace; + + MainWindowNewInvestigationCallback + new_investigation_callback; + + gpointer + new_investigation_user_data; }; +/** + * @brief Transmet la demande de création d'une enquête au contrôleur. + * + * @param button Bouton ayant reçu le clic. + * @param user_data Pointeur vers MainWindow. + */ +static void main_window_on_new_investigation_clicked( + GtkButton *button, + gpointer user_data +) +{ + MainWindow *main_window = user_data; + + (void) button; + + if (main_window == NULL || + main_window->new_investigation_callback == NULL) + { + return; + } + + main_window->new_investigation_callback( + main_window->new_investigation_user_data + ); +} + MainWindow *main_window_new(GtkApplication *application) { MainWindow *main_window = NULL; @@ -67,7 +115,7 @@ MainWindow *main_window_new(GtkApplication *application) gtk_window_set_title( main_window->window, - "Labfy Investigation" + MAIN_WINDOW_DEFAULT_TITLE ); gtk_window_set_default_size( @@ -87,6 +135,53 @@ MainWindow *main_window_new(GtkApplication *application) 0 ); + /* + * Barre regroupant les actions générales de l'application. + */ + main_window->action_bar = gtk_box_new( + GTK_ORIENTATION_HORIZONTAL, + 8 + ); + + gtk_widget_set_margin_start( + main_window->action_bar, + 8 + ); + + gtk_widget_set_margin_end( + main_window->action_bar, + 8 + ); + + gtk_widget_set_margin_top( + main_window->action_bar, + 8 + ); + + gtk_widget_set_margin_bottom( + main_window->action_bar, + 8 + ); + + main_window->new_investigation_button = + gtk_button_new_with_label( + "Nouvelle enquête" + ); + + gtk_box_append( + GTK_BOX(main_window->action_bar), + main_window->new_investigation_button + ); + + g_signal_connect( + main_window->new_investigation_button, + "clicked", + G_CALLBACK( + main_window_on_new_investigation_clicked + ), + main_window + ); + /* * GtkPaned sépare horizontalement le panneau latéral * et la zone de travail. @@ -195,7 +290,7 @@ MainWindow *main_window_new(GtkApplication *application) ); main_window->status_label = gtk_label_new( - "Aucune enquête ouverte" + MAIN_WINDOW_NO_INVESTIGATION_STATUS ); gtk_widget_set_halign( @@ -226,9 +321,15 @@ MainWindow *main_window_new(GtkApplication *application) /* * Assemblage vertical : * + * Barre d'actions * GtkPaned * Barre d'état */ + gtk_box_append( + GTK_BOX(main_window->main_box), + main_window->action_bar + ); + gtk_box_append( GTK_BOX(main_window->main_box), main_window->main_paned @@ -285,6 +386,120 @@ void main_window_set_tree_model( ); } +void main_window_set_investigation( + MainWindow *main_window, + const char *investigation_name, + const char *investigation_root_path +) +{ + char *window_title = NULL; + char *status_text = NULL; + + gboolean has_name = FALSE; + gboolean has_root_path = FALSE; + + if (main_window == NULL) + { + return; + } + + has_name = + investigation_name != NULL && + investigation_name[0] != '\0'; + + has_root_path = + investigation_root_path != NULL && + investigation_root_path[0] != '\0'; + + if (has_name) + { + window_title = g_strdup_printf( + "%s — %s", + MAIN_WINDOW_DEFAULT_TITLE, + investigation_name + ); + } + else + { + window_title = g_strdup( + MAIN_WINDOW_DEFAULT_TITLE + ); + } + + if (has_name && has_root_path) + { + status_text = g_strdup_printf( + "Enquête ouverte : %s — %s", + investigation_name, + investigation_root_path + ); + } + else if (has_name) + { + status_text = g_strdup_printf( + "Enquête ouverte : %s", + investigation_name + ); + } + else if (has_root_path) + { + status_text = g_strdup_printf( + "Enquête ouverte : %s", + investigation_root_path + ); + } + else + { + status_text = g_strdup( + MAIN_WINDOW_NO_INVESTIGATION_STATUS + ); + } + + if (main_window->window != NULL) + { + gtk_window_set_title( + main_window->window, + window_title + ); + } + + if (main_window->status_label != NULL) + { + gtk_label_set_text( + GTK_LABEL(main_window->status_label), + status_text + ); + } + + g_free(status_text); + g_free(window_title); +} + +void main_window_set_status( + MainWindow *main_window, + const char *status_text +) +{ + const char *safe_status_text = NULL; + + if (main_window == NULL || + main_window->status_label == NULL) + { + return; + } + + safe_status_text = + status_text != NULL && + status_text[0] != '\0' + ? status_text + : MAIN_WINDOW_NO_INVESTIGATION_STATUS; + + gtk_label_set_text( + GTK_LABEL(main_window->status_label), + safe_status_text + ); +} + void main_window_set_tree_selection_callback( MainWindow *main_window, InvestigationTreeViewSelectionCallback callback, @@ -303,6 +518,21 @@ void main_window_set_tree_selection_callback( ); } +void main_window_set_new_investigation_callback( + MainWindow *main_window, + MainWindowNewInvestigationCallback callback, + gpointer user_data +) +{ + if (main_window == NULL) + { + return; + } + + main_window->new_investigation_callback = callback; + main_window->new_investigation_user_data = user_data; +} + void main_window_set_selected_node( MainWindow *main_window, const InvestigationNode *node diff --git a/tests/test_database b/tests/test_database deleted file mode 100755 index 2a1ae25..0000000 Binary files a/tests/test_database and /dev/null differ diff --git a/tests/test_error b/tests/test_error deleted file mode 100755 index 1185635..0000000 Binary files a/tests/test_error and /dev/null differ diff --git a/tests/test_investigation_dao b/tests/test_investigation_dao deleted file mode 100755 index 0234ddd..0000000 Binary files a/tests/test_investigation_dao and /dev/null differ diff --git a/tests/test_investigation_node b/tests/test_investigation_node deleted file mode 100755 index b7614ef..0000000 Binary files a/tests/test_investigation_node and /dev/null differ diff --git a/tests/test_investigation_project b/tests/test_investigation_project deleted file mode 100755 index 3bd6369..0000000 Binary files a/tests/test_investigation_project and /dev/null differ diff --git a/tests/test_investigation_record b/tests/test_investigation_record deleted file mode 100755 index ba2880b..0000000 Binary files a/tests/test_investigation_record and /dev/null differ diff --git a/tests/test_investigation_session b/tests/test_investigation_session deleted file mode 100755 index 04f8d66..0000000 Binary files a/tests/test_investigation_session and /dev/null differ diff --git a/tests/test_investigation_tree_builder b/tests/test_investigation_tree_builder deleted file mode 100755 index d6cd484..0000000 Binary files a/tests/test_investigation_tree_builder and /dev/null differ diff --git a/tests/test_investigation_tree_model b/tests/test_investigation_tree_model deleted file mode 100755 index 2c823e1..0000000 Binary files a/tests/test_investigation_tree_model and /dev/null differ diff --git a/tests/test_statement b/tests/test_statement deleted file mode 100755 index 79b937b..0000000 Binary files a/tests/test_statement and /dev/null differ diff --git a/tests/test_transaction b/tests/test_transaction deleted file mode 100755 index 3533f19..0000000 Binary files a/tests/test_transaction and /dev/null differ