feat(ui): add investigation session and creation workflow

This commit is contained in:
grayTerminal-sh 2026-07-16 23:55:57 +02:00
parent 4efdfe4865
commit 3260b476e5
21 changed files with 3613 additions and 74 deletions

View file

@ -0,0 +1,933 @@
# Ticket #029 — Intégrer `InvestigationSession` au cycle de vie GTK
## Contexte
Le ticket #028 a ajouté `InvestigationSession`, qui permet douvrir 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.
Lapplication GTK utilise encore lancien 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 nutilise pas les informations persistées dans la table `investigation`.
Il faut désormais remplacer lancien objet détenu par `Application` par une véritable `InvestigationSession`.
## Objectif
Faire de `InvestigationSession` le contexte actif de lapplication.
Après la sélection dun dossier :
1. ouvrir une nouvelle `InvestigationSession` ;
2. récupérer le chemin racine depuis son `InvestigationProject` ;
3. construire le nouvel `InvestigationTreeModel` ;
4. ne remplacer lancienne enquête quaprès validation complète ;
5. mettre à jour la fenêtre principale ;
6. conserver la session jusquà la fermeture de lapplication.
Lapplication ne doit posséder quune 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
Louverture dune 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 lancien arbre
fermer lancienne 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 derreur produit
```
Lapplication ne doit jamais perdre une enquête déjà ouverte simplement parce quune nouvelle sélection est invalide.
---
# Travail à réaliser
## 1. Remplacer lancien 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 lancien 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 louverture soit validée.
## 4. Gérer une sélection annulée
Lorsque :
```c
folder_path == NULL
```
la fonction doit simplement retourner.
Lenquê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 douverture de session
Si :
```c
new_session == NULL
```
la fonction doit :
- afficher un avertissement contenant le message du `GError` ;
- libérer le `GError` ;
- conserver lancienne session ;
- conserver lancien 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 derreur est hors périmètre de ce ticket.
## 6. Construire larborescence 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 larbre
Si `InvestigationTreeBuilder` échoue après louverture de la session :
```text
new_session valide
new_tree_model == NULL
```
la fonction doit :
```c
investigation_session_close(new_session);
```
puis retourner.
Lancienne session et lancien 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` nest 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 douverture de lenquê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 lenquê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 linstallation 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
);
```
Lordre 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
);
```
Lordre 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 lancien objet dans lapplication
À 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` nest pas supprimé dans ce ticket.
Sa suppression éventuelle sera effectuée séparément après vérification quaucun autre composant ne lutilise.
---
# Tests et validations
## 16. Tests automatisés existants
Aucun nouveau test GTK automatisé nest 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 douverture valide
Créer ou utiliser une enquête valide.
Lancer :
```bash
make
make run
```
Sélectionner le dossier racine de lenquête.
Vérifier :
- la fenêtre reste ouverte ;
- larborescence apparaît dans la sidebar ;
- le titre contient le nom persistant de lenquête ;
- la barre détat contient le nom et le chemin racine ;
- aucune erreur SQLite napparaît ;
- la session reste active après la fin du callback.
## 18. Test manuel dannulation
Relancer lapplication 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 dun 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 nest créé ;
- la fenêtre reste utilisable ;
- aucune fausse enquête napparaît dans la sidebar.
## 20. Test manuel de remplacement
Si linterface permet une seconde sélection pendant la même exécution :
1. ouvrir une enquête valide A ;
2. tenter douvrir 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 nest pas encore accessible dans linterface, cette validation sera complétée lors de lajout de laction « Ouvrir ».
La logique du callback doit néanmoins déjà préserver lancienne 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 dacceptation
- [ ] `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`.
- [ ] Larbre est construit depuis le chemin de la session.
- [ ] Lancienne session reste active si louverture échoue.
- [ ] Lancien arbre reste actif si louverture échoue.
- [ ] La nouvelle session est fermée si la construction de larbre échoue.
- [ ] Les anciens objets ne sont remplacés quaprè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 lenquête.
- [ ] La barre détat affiche le nom et le chemin racine.
- [ ] `application.c` nappelle aucune fonction SQLite.
- [ ] Aucun chemin SQLite nest reconstruit dans `application.c`.
- [ ] Les anciens tests restent valides.
- [ ] `make` réussit sans warning.
- [ ] `make test` réussit.
- [ ] Le test manuel douverture valide réussit.
- [ ] Le test manuel dannulation 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 dune enquête ;
- la fermeture manuelle dune enquête ;
- la réparation dun 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 lancien 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 dun dossier doit ouvrir une véritable session denquête.
Lapplication doit conserver ensemble :
```text
la connexion SQLite
les métadonnées persistées
le contexte de fichiers
larborescence
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
```

File diff suppressed because it is too large Load diff

View file

@ -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

View 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

View file

@ -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.

View file

@ -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);
}

View 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 é 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 é
* 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 é 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
);
}

View file

@ -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.

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.