feat(ui): add investigation session and creation workflow
This commit is contained in:
parent
4efdfe4865
commit
3260b476e5
21 changed files with 3613 additions and 74 deletions
933
docs/tickets/closed/TICKET-029.md
Normal file
933
docs/tickets/closed/TICKET-029.md
Normal file
|
|
@ -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 <sqlite3.h>
|
||||
```
|
||||
|
||||
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 — <nom>
|
||||
```
|
||||
|
||||
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 : <nom> — <chemin racine>
|
||||
```
|
||||
|
||||
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 <sqlite3.h>|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
|
||||
```
|
||||
|
||||
1115
docs/tickets/closed/TICKET-030.md
Normal file
1115
docs/tickets/closed/TICKET-030.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
58
include/views/create_investigation_dialog.h
Normal file
58
include/views/create_investigation_dialog.h
Normal file
|
|
@ -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 <gtk/gtk.h>
|
||||
|
||||
/**
|
||||
* @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
|
||||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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 <gtk/gtk.h>
|
||||
|
||||
|
|
@ -18,94 +22,284 @@
|
|||
#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.
|
||||
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
|
||||
);
|
||||
|
||||
investigation_tree_model_free(
|
||||
new_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.
|
||||
*/
|
||||
investigation_tree_model_free(application->tree_model);
|
||||
investigation_free(application->investigation);
|
||||
static void application_on_new_investigation_requested(
|
||||
gpointer user_data
|
||||
)
|
||||
{
|
||||
Application *application = user_data;
|
||||
|
||||
application->tree_model = new_tree_model;
|
||||
application->investigation = new_investigation;
|
||||
if (application == NULL ||
|
||||
application->main_window == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
main_window_set_tree_model(
|
||||
application->main_window,
|
||||
application->tree_model
|
||||
create_investigation_dialog_present(
|
||||
main_window_get_window(
|
||||
application->main_window
|
||||
),
|
||||
application_on_create_investigation,
|
||||
application
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -194,20 +408,33 @@ static void application_on_activate(
|
|||
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,
|
||||
|
|
@ -236,34 +463,60 @@ int application_run(
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
864
src/views/create_investigation_dialog.c
Normal file
864
src/views/create_investigation_dialog.c
Normal file
|
|
@ -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 <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
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
BIN
tests/test_error
BIN
tests/test_error
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in a new issue