feat(ui): add investigation creation and opening workflows

This commit is contained in:
grayTerminal-sh 2026-07-17 08:41:42 +02:00
parent 3260b476e5
commit a4f7eac949
16 changed files with 538 additions and 8 deletions

View file

@ -0,0 +1,301 @@
# Ticket #031 — Ouvrir une enquête existante depuis GTK
## Contexte
Les tickets #029 et #030 ont permis de :
- créer une enquête depuis GTK ;
- initialiser son arborescence et sa base SQLite ;
- ouvrir une `InvestigationSession` ;
- construire le `InvestigationTreeModel` ;
- installer la session dans `Application` ;
- afficher le nom et le chemin de lenquête dans `MainWindow`.
Lapplication ne permet cependant pas encore douvrir explicitement une enquête existante.
Lancien sélecteur automatique au démarrage a été supprimé afin de ne pas forcer lutilisateur à choisir un dossier à chaque lancement.
## Objectif
Ajouter un bouton :
```text
Ouvrir une enquête
```
Ce bouton doit permettre de sélectionner le dossier racine dune enquête existante, puis de louvrir avec le flux déjà présent :
```text
FolderDialog
investigation_session_open()
investigation_tree_builder_build()
application_install_session()
MainWindow mise à jour
```
La logique dinstallation dune session ne doit pas être dupliquée.
## Travail à réaliser
### 1. Ajouter le bouton dans `MainWindow`
Modifier :
```text
include/views/main_window.h
src/views/main_window.c
```
Ajouter un bouton visible dans la barre dactions :
```text
Ouvrir une enquête
```
La barre doit contenir au minimum :
```text
[ Nouvelle enquête ] [ Ouvrir une enquête ]
```
Ajouter dans la structure privée :
```c
GtkWidget *open_investigation_button;
```
### 2. Ajouter le type de callback
Dans `include/views/main_window.h`, ajouter :
```c
typedef void (*MainWindowOpenInvestigationCallback)(
gpointer user_data
);
```
Puis déclarer :
```c
void main_window_set_open_investigation_callback(
MainWindow *main_window,
MainWindowOpenInvestigationCallback callback,
gpointer user_data
);
```
### 3. Conserver le callback dans `MainWindow`
Ajouter dans la structure privée :
```c
MainWindowOpenInvestigationCallback
open_investigation_callback;
gpointer
open_investigation_user_data;
```
Ajouter un callback privé :
```c
static void main_window_on_open_investigation_clicked(
GtkButton *button,
gpointer user_data
);
```
Il doit appeler le callback configuré uniquement sil existe.
`MainWindow` ne doit pas ouvrir elle-même la session.
### 4. Relier le bouton à `Application`
Dans `src/core/application.c`, réactiver lutilisation de :
```c
#include "views/folder_dialog.h"
```
Ajouter :
```c
static void application_on_open_investigation_requested(
gpointer user_data
);
```
Cette fonction doit ouvrir le sélecteur avec :
```c
folder_dialog_select_folder(
main_window_get_window(application->main_window),
application_on_folder_selected,
application
);
```
### 5. Réutiliser `application_on_folder_selected()`
La fonction doit :
1. accepter lannulation ;
2. ouvrir la session avec `investigation_session_open()` ;
3. récupérer le chemin racine depuis `InvestigationProject` ;
4. construire larbre avec `investigation_tree_builder_build()` ;
5. installer les objets avec `application_install_session()`.
En cas déchec :
```text
ancienne session conservée
ancien arbre conservé
nouvelle session libérée
nouvel arbre libéré
warning explicite
```
### 6. Enregistrer le callback dans `application_on_activate()`
Après la création de `MainWindow`, appeler :
```c
main_window_set_open_investigation_callback(
application->main_window,
application_on_open_investigation_requested,
application
);
```
Le sélecteur ne doit pas être lancé automatiquement au démarrage.
## Tests manuels
### Ouverture valide
1. lancer lapplication ;
2. cliquer sur `Ouvrir une enquête` ;
3. sélectionner une enquête créée avec le ticket #030 ;
4. vérifier laffichage de larborescence ;
5. vérifier le titre ;
6. vérifier la barre détat ;
7. vérifier labsence derreur SQLite.
### Annulation
Ouvrir le sélecteur puis annuler.
Vérifier :
```text
aucun crash
aucun changement de session
aucun changement darbre
```
### Dossier invalide
Sélectionner un dossier sans base SQLite.
Vérifier :
```text
warning explicite
aucune base créée
ancienne session conservée
```
### Remplacement
1. ouvrir une enquête A ;
2. ouvrir une enquête B ;
3. vérifier que B remplace A.
### Échec pendant le remplacement
1. ouvrir une enquête valide A ;
2. tenter douvrir un dossier invalide ;
3. vérifier que A reste active.
## Critères dacceptation
- [ ] Le bouton `Ouvrir une enquête` est visible.
- [ ] Le bouton ouvre un sélecteur de dossier.
- [ ] Le sélecteur ne souvre pas automatiquement au démarrage.
- [ ] Une enquête valide peut être ouverte.
- [ ] Larborescence est affichée.
- [ ] Le titre est mis à jour.
- [ ] La barre détat est mise à jour.
- [ ] Lannulation ne modifie aucun état.
- [ ] Un dossier invalide est refusé.
- [ ] Une base absente nest pas créée.
- [ ] Lancienne session reste active en cas déchec.
- [ ] Lancien arbre reste actif en cas déchec.
- [ ] `application_install_session()` est réutilisée.
- [ ] Aucun appel SQLite direct nest ajouté.
- [ ] `make` réussit.
- [ ] `make test` réussit.
- [ ] `git diff --check` ne retourne aucune erreur.
## Audit attendu
```bash
rg -n 'open_investigation|Ouvrir une enquête' include/views/main_window.h src/views/main_window.c src/core/application.c
```
```bash
rg -n 'folder_dialog_select_folder' src/core/application.c
```
```bash
rg -n 'sqlite3_|#include <sqlite3.h>' src/core/application.c src/views/main_window.c
```
Résultat attendu pour la dernière commande :
```text
aucune sortie
```
## Fichiers principalement concernés
```text
include/views/main_window.h
src/views/main_window.c
src/core/application.c
```
## Résultat attendu
À la fin du ticket, lapplication doit proposer deux actions explicites :
```text
Nouvelle enquête
Ouvrir une enquête
```
Lutilisateur doit pouvoir créer une enquête, fermer lapplication, puis la rouvrir plus tard depuis GTK.
## Commit attendu
```bash
make clean
make
make test
git diff --check
git status --short
```
```bash
git add include/views/main_window.h src/views/main_window.c src/core/application.c
```
```bash
git commit -m "feat(ui): add investigation opening action"
git push
```

View file

@ -137,6 +137,31 @@ void main_window_set_status(
const char *status_text
);
/**
* @brief Callback appelé lorsque l'utilisateur demande l'ouverture
* d'une enquête existante.
*
* @param user_data Données utilisateur associées au callback.
*/
typedef void (*MainWindowOpenInvestigationCallback)(
gpointer user_data
);
/**
* @brief Définit le callback du bouton « Ouvrir une enquête ».
*
* La fenêtre 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_open_investigation_callback(
MainWindow *main_window,
MainWindowOpenInvestigationCallback callback,
gpointer user_data
);
/**
* @brief Libère les ressources de la fenêtre.
*

Binary file not shown.

View file

@ -12,6 +12,7 @@
#include "models/investigation_record.h"
#include "core/investigation_tree_builder.h"
#include "views/create_investigation_dialog.h"
#include "views/folder_dialog.h"
#include "views/main_window.h"
#include <gtk/gtk.h>
@ -128,6 +129,118 @@ static gboolean application_install_session(
return TRUE;
}
/**
* @brief Traite le dossier d'enquête sélectionné par l'utilisateur.
*
* @param folder_path Chemin sélectionné, ou NULL en cas d'annulation.
* @param user_data Pointeur vers Application.
*/
static void application_on_folder_selected(
const char *folder_path,
gpointer user_data
)
{
Application *application = user_data;
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)
{
g_print("Ouverture annulée.\n");
return;
}
new_session = investigation_session_open(
folder_path,
&error
);
if (new_session == NULL)
{
g_warning(
"Impossible d'ouvrir l'enquête : %s",
error != NULL
? error->message
: "erreur inconnue"
);
g_clear_error(&error);
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(
"La session ne fournit aucun chemin racine valide."
);
investigation_session_close(
new_session
);
return;
}
new_tree_model = investigation_tree_builder_build(
root_path
);
if (new_tree_model == NULL)
{
g_warning(
"Impossible de construire l'arborescence de l'enquête."
);
investigation_session_close(
new_session
);
return;
}
if (!application_install_session(
application,
new_session,
new_tree_model
))
{
g_warning(
"Impossible d'installer l'enquête dans l'application."
);
investigation_tree_model_free(
new_tree_model
);
investigation_session_close(
new_session
);
}
}
/**
* @brief Traite la demande de création d'une nouvelle enquête.
*
@ -303,6 +416,32 @@ static void application_on_new_investigation_requested(
);
}
/**
* @brief Ouvre le sélecteur d'une enquête existante.
*
* @param user_data Pointeur vers Application.
*/
static void application_on_open_investigation_requested(
gpointer user_data
)
{
Application *application = user_data;
if (application == NULL ||
application->main_window == NULL)
{
return;
}
folder_dialog_select_folder(
main_window_get_window(
application->main_window
),
application_on_folder_selected,
application
);
}
/**
* @brief Traite la sélection d'un nœud dans l'arborescence.
*
@ -414,17 +553,15 @@ static void application_on_activate(
application
);
main_window_set_open_investigation_callback(
application->main_window,
application_on_open_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)

View file

@ -51,6 +51,7 @@ struct MainWindow
GtkWidget *main_box;
GtkWidget *action_bar;
GtkWidget *new_investigation_button;
GtkWidget *open_investigation_button;
GtkWidget *main_paned;
GtkWidget *status_label;
@ -62,6 +63,12 @@ struct MainWindow
gpointer
new_investigation_user_data;
MainWindowOpenInvestigationCallback
open_investigation_callback;
gpointer
open_investigation_user_data;
};
/**
@ -90,6 +97,32 @@ static void main_window_on_new_investigation_clicked(
);
}
/**
* @brief Transmet la demande d'ouverture 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_open_investigation_clicked(
GtkButton *button,
gpointer user_data
)
{
MainWindow *main_window = user_data;
(void) button;
if (main_window == NULL ||
main_window->open_investigation_callback == NULL)
{
return;
}
main_window->open_investigation_callback(
main_window->open_investigation_user_data
);
}
MainWindow *main_window_new(GtkApplication *application)
{
MainWindow *main_window = NULL;
@ -168,11 +201,21 @@ MainWindow *main_window_new(GtkApplication *application)
"Nouvelle enquête"
);
main_window->open_investigation_button =
gtk_button_new_with_label(
"Ouvrir une enquête"
);
gtk_box_append(
GTK_BOX(main_window->action_bar),
main_window->new_investigation_button
);
gtk_box_append(
GTK_BOX(main_window->action_bar),
main_window->open_investigation_button
);
g_signal_connect(
main_window->new_investigation_button,
"clicked",
@ -182,6 +225,15 @@ MainWindow *main_window_new(GtkApplication *application)
main_window
);
g_signal_connect(
main_window->open_investigation_button,
"clicked",
G_CALLBACK(
main_window_on_open_investigation_clicked
),
main_window
);
/*
* GtkPaned sépare horizontalement le panneau latéral
* et la zone de travail.
@ -533,6 +585,21 @@ void main_window_set_new_investigation_callback(
main_window->new_investigation_user_data = user_data;
}
void main_window_set_open_investigation_callback(
MainWindow *main_window,
MainWindowOpenInvestigationCallback callback,
gpointer user_data
)
{
if (main_window == NULL)
{
return;
}
main_window->open_investigation_callback = callback;
main_window->open_investigation_user_data = user_data;
}
void main_window_set_selected_node(
MainWindow *main_window,
const InvestigationNode *node

BIN
tests/test_database Executable file

Binary file not shown.

BIN
tests/test_error Executable file

Binary file not shown.

BIN
tests/test_investigation_dao Executable file

Binary file not shown.

BIN
tests/test_investigation_node Executable file

Binary file not shown.

BIN
tests/test_investigation_project Executable file

Binary file not shown.

BIN
tests/test_investigation_record Executable file

Binary file not shown.

BIN
tests/test_investigation_session Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
tests/test_statement Executable file

Binary file not shown.

BIN
tests/test_transaction Executable file

Binary file not shown.