Persister la position des nœuds du graphe

This commit is contained in:
grayTerminal-sh 2026-07-21 23:27:07 +02:00
parent 995939bef8
commit c46039a2aa
22 changed files with 3223 additions and 17 deletions

View file

@ -516,6 +516,9 @@ $(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \
src/core/investigation_graph_loader.c \
src/core/background_task.c \
src/dao/entity_dao.c \
src/models/investigation_graph_layout.c \
src/models/graph_node_position.c \
src/dao/graph_node_position_dao.c \
src/dao/relation_dao.c \
src/models/investigation_graph_model.c \
src/models/entity_record.c \

View file

@ -0,0 +1,59 @@
/******************************************************************************
* Labfy Investigation
*
* Extensions idempotentes du schéma SQLite courant V2
******************************************************************************/
/*
* Les coordonnées du graphe sont un état de présentation.
*
* Elles restent séparées des données métier de la table entites.
*
* Cette table peut être créée à chaque ouverture grâce à IF NOT EXISTS.
*/
CREATE TABLE IF NOT EXISTS graph_node_positions
(
entity_id TEXT PRIMARY KEY,
x REAL NOT NULL,
y REAL NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (entity_id)
REFERENCES entites(id)
ON UPDATE CASCADE
ON DELETE CASCADE,
CHECK (
length(trim(entity_id)) > 0
),
CHECK (
typeof(x) IN (
'integer',
'real'
)
),
CHECK (
typeof(y) IN (
'integer',
'real'
)
),
CHECK (
x = x
AND abs(x) <= 1.7976931348623157e308
),
CHECK (
y = y
AND abs(y) <= 1.7976931348623157e308
),
CHECK (
length(trim(updated_at)) > 0
)
);

View file

@ -6,6 +6,7 @@
#ifndef LABFY_INVESTIGATION_INVESTIGATION_GRAPH_LOAD_TASK_H
#define LABFY_INVESTIGATION_INVESTIGATION_GRAPH_LOAD_TASK_H
#include "models/investigation_graph_layout.h"
#include "models/investigation_graph_model.h"
#include <glib.h>
@ -74,6 +75,10 @@ typedef enum
*
* En cas de succès, graph_model appartient au destinataire du callback.
*
* La disposition associée peut être récupérée pendant le callback avec
* investigation_graph_load_task_take_layout(). Elle devient alors la
* propriété du code appelant.
*
* En cas d'échec, error est empruntée et valable uniquement pendant le
* callback.
*
@ -171,6 +176,23 @@ gboolean investigation_graph_load_task_is_running(
const InvestigationGraphLoadTask *load_task
);
/**
* @brief Transfère la disposition chargée au code appelant.
*
* Cette fonction doit être appelée après une fin de chargement réussie,
* normalement depuis InvestigationGraphLoadTaskCallback.
*
* La tâche cesse de posséder la disposition retournée.
* Un second appel retourne NULL.
*
* @param load_task Tâche terminée.
*
* @return Disposition transférée, ou NULL.
*/
InvestigationGraphLayout *investigation_graph_load_task_take_layout(
InvestigationGraphLoadTask *load_task
);
G_END_DECLS
#endif

View file

@ -0,0 +1,133 @@
/******************************************************************************
* @file graph_node_position_dao.h
* @brief Persistance des positions des nœuds du graphe dans SQLite.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_GRAPH_NODE_POSITION_DAO_H
#define LABFY_INVESTIGATION_GRAPH_NODE_POSITION_DAO_H
#include "database/database.h"
#include "models/graph_node_position.h"
#include <glib.h>
G_BEGIN_DECLS
/**
* @brief Catégories d'erreurs du DAO des positions de nœuds.
*/
typedef enum
{
GRAPH_NODE_POSITION_DAO_ERROR_INVALID_ARGUMENT,
GRAPH_NODE_POSITION_DAO_ERROR_MEMORY,
GRAPH_NODE_POSITION_DAO_ERROR_PREPARE,
GRAPH_NODE_POSITION_DAO_ERROR_BIND,
GRAPH_NODE_POSITION_DAO_ERROR_EXECUTE,
GRAPH_NODE_POSITION_DAO_ERROR_CONSTRAINT,
GRAPH_NODE_POSITION_DAO_ERROR_READ,
GRAPH_NODE_POSITION_DAO_ERROR_MODEL
} GraphNodePositionDaoError;
/**
* @brief Domaine d'erreurs du DAO des positions de nœuds.
*/
#define GRAPH_NODE_POSITION_DAO_ERROR \
graph_node_position_dao_error_quark()
/**
* @brief Retourne le domaine d'erreurs du DAO.
*/
GQuark graph_node_position_dao_error_quark(void);
/**
* @brief DAO opaque donnant accès aux positions persistées.
*
* L'objet emprunte la connexion Database reçue lors de sa création.
*/
typedef struct GraphNodePositionDao GraphNodePositionDao;
/**
* @brief Crée un DAO utilisant une connexion Database existante.
*
* La connexion est empruntée et doit rester valide pendant toute la durée
* de vie du DAO.
*
* @param database Connexion ouverte.
* @param error Adresse recevant une éventuelle erreur.
*
* @return Nouveau DAO, ou NULL en cas d'échec.
*/
GraphNodePositionDao *graph_node_position_dao_new(
Database *database,
GError **error
);
/**
* @brief Libère le DAO.
*
* La connexion Database empruntée n'est pas fermée.
* Cette fonction accepte NULL.
*
* @param position_dao DAO à libérer.
*/
void graph_node_position_dao_free(
GraphNodePositionDao *position_dao
);
/**
* @brief Charge toutes les positions dans un ordre déterministe.
*
* Le tableau retourné utilise graph_node_position_free() comme fonction
* de destruction.
*
* @param position_dao DAO valide.
* @param error Adresse recevant une éventuelle erreur.
*
* @return Nouveau GPtrArray possédé par l'appelant, ou NULL.
*/
GPtrArray *graph_node_position_dao_list_all(
GraphNodePositionDao *position_dao,
GError **error
);
/**
* @brief Insère ou met à jour la position d'une entité.
*
* La date updated_at est générée en UTC par le DAO.
*
* @param position_dao DAO valide.
* @param entity_identifier UUID de l'entité.
* @param x Coordonnée horizontale logique.
* @param y Coordonnée verticale logique.
* @param error Adresse recevant une éventuelle erreur.
*
* @return TRUE si l'enregistrement réussit, sinon FALSE.
*/
gboolean graph_node_position_dao_upsert(
GraphNodePositionDao *position_dao,
const char *entity_identifier,
double x,
double y,
GError **error
);
/**
* @brief Supprime la position persistée d'une entité.
*
* L'absence de position n'est pas une erreur.
*
* @param position_dao DAO valide.
* @param entity_identifier UUID de l'entité.
* @param error Adresse recevant une éventuelle erreur.
*
* @return TRUE si la requête réussit, sinon FALSE.
*/
gboolean graph_node_position_dao_delete(
GraphNodePositionDao *position_dao,
const char *entity_identifier,
GError **error
);
G_END_DECLS
#endif

View file

@ -54,4 +54,23 @@ bool schema_install_v2(
Database *database
);
/**
* @brief Garantit la présence des extensions du schéma courant V2.
*
* La connexion doit être valide et une transaction doit déjà être active.
*
* Cette fonction applique uniquement des opérations idempotentes destinées
* aux nouvelles bases comme aux bases V2 déjà existantes.
*
* Elle ne modifie pas la version enregistrée dans metadata.
* Elle ne réalise ni COMMIT ni ROLLBACK.
*
* @param database Connexion Database ouverte.
*
* @return true si les extensions sont présentes, sinon false.
*/
bool schema_ensure_current(
Database *database
);
#endif

View file

@ -119,6 +119,26 @@ bool database_statement_column_int64(
int64_t *value
);
/**
* @brief Lit un nombre à virgule flottante depuis une colonne.
*
* Les indices de colonnes SQLite commencent à 0.
*
* La colonne doit être de type SQLITE_FLOAT ou SQLITE_INTEGER.
* Une colonne entière est convertie en double par SQLite.
*
* @param statement Requête positionnée sur une ligne.
* @param column_index Indice de la colonne.
* @param value Destination de la valeur.
*
* @return true si le nombre a pu être lu, sinon false.
*/
bool database_statement_column_double(
DatabaseStatement *statement,
int column_index,
double *value
);
/**
* @brief Copie le contenu texte d'une colonne.
*
@ -173,6 +193,21 @@ bool database_statement_bind_int64(
int64_t value
);
/**
* @brief Lie un nombre à virgule flottante à un paramètre SQL.
*
* @param statement Requête préparée.
* @param index Indice du paramètre.
* @param value Valeur à lier.
*
* @return true en cas de succès, sinon false.
*/
bool database_statement_bind_double(
DatabaseStatement *statement,
int index,
double value
);
/**
* @brief Lie la valeur SQL NULL à un paramètre.
*

View file

@ -0,0 +1,126 @@
/******************************************************************************
* @file graph_node_position.h
* @brief Modèle représentant la position persistée d'un nœud du graphe.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_GRAPH_NODE_POSITION_H
#define LABFY_INVESTIGATION_GRAPH_NODE_POSITION_H
#include <glib.h>
G_BEGIN_DECLS
/**
* @brief Modèle opaque représentant une position de nœud.
*/
typedef struct GraphNodePosition GraphNodePosition;
/**
* @brief Codes d'erreur produits par GraphNodePosition.
*/
typedef enum
{
GRAPH_NODE_POSITION_ERROR_INVALID_ARGUMENT,
GRAPH_NODE_POSITION_ERROR_INVALID_IDENTIFIER,
GRAPH_NODE_POSITION_ERROR_INVALID_COORDINATE,
GRAPH_NODE_POSITION_ERROR_INVALID_DATE
} GraphNodePositionError;
/**
* @brief Domaine d'erreur du modèle GraphNodePosition.
*/
#define GRAPH_NODE_POSITION_ERROR \
graph_node_position_error_quark()
/**
* @brief Retourne le domaine d'erreur du modèle.
*/
GQuark graph_node_position_error_quark(void);
/**
* @brief Crée une position persistée de nœud.
*
* Toutes les chaînes sont copiées.
*
* entity_identifier doit être un UUID valide.
* x et y doivent être des nombres finis.
* updated_at doit respecter le format UTC YYYY-MM-DDTHH:MM:SSZ.
*
* @param entity_identifier UUID de l'entité.
* @param x Coordonnée horizontale logique.
* @param y Coordonnée verticale logique.
* @param updated_at Date UTC de dernière modification.
* @param error Emplacement facultatif recevant une erreur.
*
* @return Nouvelle position, ou NULL lorsque les données sont invalides.
*/
GraphNodePosition *graph_node_position_new(
const char *entity_identifier,
double x,
double y,
const char *updated_at,
GError **error
);
/**
* @brief Libère une position.
*
* Cette fonction accepte NULL.
*
* @param position Position à libérer.
*/
void graph_node_position_free(
GraphNodePosition *position
);
/**
* @brief Retourne l'UUID de l'entité.
*
* La chaîne retournée appartient au modèle.
*
* @param position Position à consulter.
*
* @return UUID emprunté, ou NULL.
*/
const char *graph_node_position_get_entity_identifier(
const GraphNodePosition *position
);
/**
* @brief Retourne la coordonnée horizontale.
*
* @param position Position à consulter.
*
* @return Coordonnée horizontale, ou 0.0 si position est NULL.
*/
double graph_node_position_get_x(
const GraphNodePosition *position
);
/**
* @brief Retourne la coordonnée verticale.
*
* @param position Position à consulter.
*
* @return Coordonnée verticale, ou 0.0 si position est NULL.
*/
double graph_node_position_get_y(
const GraphNodePosition *position
);
/**
* @brief Retourne la date UTC de dernière modification.
*
* La chaîne retournée appartient au modèle.
*
* @param position Position à consulter.
*
* @return Date empruntée, ou NULL.
*/
const char *graph_node_position_get_updated_at(
const GraphNodePosition *position
);
G_END_DECLS
#endif

View file

@ -0,0 +1,133 @@
/******************************************************************************
* @file investigation_graph_layout.h
* @brief Modèle en mémoire de la disposition du graphe d'enquête.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_INVESTIGATION_GRAPH_LAYOUT_H
#define LABFY_INVESTIGATION_INVESTIGATION_GRAPH_LAYOUT_H
#include <glib.h>
G_BEGIN_DECLS
/**
* @brief Modèle opaque représentant la disposition du graphe.
*/
typedef struct InvestigationGraphLayout InvestigationGraphLayout;
/**
* @brief Codes d'erreur du modèle de disposition.
*/
typedef enum
{
INVESTIGATION_GRAPH_LAYOUT_ERROR_INVALID_ARGUMENT,
INVESTIGATION_GRAPH_LAYOUT_ERROR_INVALID_IDENTIFIER,
INVESTIGATION_GRAPH_LAYOUT_ERROR_INVALID_COORDINATE,
INVESTIGATION_GRAPH_LAYOUT_ERROR_MEMORY
} InvestigationGraphLayoutError;
/**
* @brief Domaine d'erreur du modèle de disposition.
*/
#define INVESTIGATION_GRAPH_LAYOUT_ERROR \
investigation_graph_layout_error_quark()
/**
* @brief Retourne le domaine d'erreur du modèle.
*/
GQuark investigation_graph_layout_error_quark(void);
/**
* @brief Crée une disposition vide.
*
* @return Nouvelle disposition, ou NULL en cas d'échec.
*/
InvestigationGraphLayout *investigation_graph_layout_new(void);
/**
* @brief Enregistre ou remplace la position d'une entité.
*
* L'identifiant est copié.
*
* @param layout Disposition à modifier.
* @param entity_identifier UUID de l'entité.
* @param x Coordonnée horizontale logique.
* @param y Coordonnée verticale logique.
* @param error Adresse recevant une éventuelle erreur.
*
* @return TRUE si la position a é enregistrée, sinon FALSE.
*/
gboolean investigation_graph_layout_set_position(
InvestigationGraphLayout *layout,
const char *entity_identifier,
double x,
double y,
GError **error
);
/**
* @brief Lit la position d'une entité.
*
* @param layout Disposition à consulter.
* @param entity_identifier UUID de l'entité.
* @param x Destination facultative de la coordonnée horizontale.
* @param y Destination facultative de la coordonnée verticale.
*
* @return TRUE si une position existe, sinon FALSE.
*/
gboolean investigation_graph_layout_get_position(
const InvestigationGraphLayout *layout,
const char *entity_identifier,
double *x,
double *y
);
/**
* @brief Supprime la position d'une entité.
*
* L'absence de position n'est pas une erreur.
*
* @param layout Disposition à modifier.
* @param entity_identifier UUID de l'entité.
*
* @return TRUE si une position a é supprimée, sinon FALSE.
*/
gboolean investigation_graph_layout_remove_position(
InvestigationGraphLayout *layout,
const char *entity_identifier
);
/**
* @brief Supprime toutes les positions.
*
* @param layout Disposition à vider.
*/
void investigation_graph_layout_clear(
InvestigationGraphLayout *layout
);
/**
* @brief Retourne le nombre de positions conservées.
*
* @param layout Disposition à consulter.
*
* @return Nombre de positions.
*/
guint investigation_graph_layout_get_count(
const InvestigationGraphLayout *layout
);
/**
* @brief Libère la disposition.
*
* Cette fonction accepte NULL.
*
* @param layout Disposition à libérer.
*/
void investigation_graph_layout_free(
InvestigationGraphLayout *layout
);
G_END_DECLS
#endif

View file

@ -20,6 +20,11 @@
*/
typedef struct InvestigationGraphModel InvestigationGraphModel;
/**
* @brief Représentation opaque de la disposition du graphe.
*/
typedef struct InvestigationGraphLayout InvestigationGraphLayout;
/**
* @brief Représentation opaque de la fenêtre principale.
*/
@ -71,6 +76,21 @@ typedef void (*MainWindowVerifyEvidenceCallback)(
gpointer user_data
);
/**
* @brief Callback appelé après le déplacement effectif d'un nœud.
*
* @param entity_identifier UUID de l'entité déplacée.
* @param x Coordonnée horizontale logique.
* @param y Coordonnée verticale logique.
* @param user_data Données privées du callback.
*/
typedef void (*MainWindowGraphNodeMovedCallback)(
const char *entity_identifier,
double x,
double y,
gpointer user_data
);
/**
* @brief Définit le callback de vérification d'une preuve.
*
@ -86,6 +106,21 @@ void main_window_set_verify_evidence_callback(
gpointer user_data
);
/**
* @brief Définit le callback de fin de déplacement d'un nœud.
*
* MainWindow relaie uniquement l'événement provenant du Workspace.
*
* @param main_window Fenêtre principale.
* @param callback Callback facultatif.
* @param user_data Données privées transmises au callback.
*/
void main_window_set_graph_node_moved_callback(
MainWindow *main_window,
MainWindowGraphNodeMovedCallback callback,
gpointer user_data
);
/**
* @brief Définit le callback du bouton « Nouvelle enquête ».
*
@ -247,16 +282,20 @@ void main_window_set_graph_loading(
);
/**
* @brief Affiche le graphe chargé dans le Workspace.
* @brief Affiche le graphe chargé et sa disposition dans le Workspace.
*
* MainWindow et Workspace empruntent graph_model.
* MainWindow et Workspace empruntent graph_model et graph_layout.
*
* Passer NULL pour graph_layout conserve le placement automatique.
*
* @param main_window Fenêtre principale.
* @param graph_model Graphe emprunté, ou NULL.
* @param graph_layout Disposition empruntée, ou NULL.
*/
void main_window_set_graph(
MainWindow *main_window,
const InvestigationGraphModel *graph_model
const InvestigationGraphModel *graph_model,
const InvestigationGraphLayout *graph_layout
);
/**

View file

@ -20,6 +20,11 @@ typedef struct EntityRecord EntityRecord;
*/
typedef struct InvestigationGraphModel InvestigationGraphModel;
/**
* @brief Représentation opaque de la disposition persistée du graphe.
*/
typedef struct InvestigationGraphLayout InvestigationGraphLayout;
/**
* @brief Représentation opaque de la vue graphique.
*/
@ -38,6 +43,25 @@ typedef void (*InvestigationGraphViewSelectionCallback)(
gpointer user_data
);
/**
* @brief Callback appelé après le déplacement effectif d'un nœud.
*
* Les coordonnées sont exprimées dans l'espace logique du graphe.
* entity_identifier est emprunté au modèle et reste valable uniquement
* pendant l'appel.
*
* @param entity_identifier UUID de l'entité déplacée.
* @param x Coordonnée horizontale logique du coin supérieur gauche.
* @param y Coordonnée verticale logique du coin supérieur gauche.
* @param user_data Données empruntées fournies par l'appelant.
*/
typedef void (*InvestigationGraphViewNodeMovedCallback)(
const char *entity_identifier,
double x,
double y,
gpointer user_data
);
/**
* @brief Crée une nouvelle vue graphique vide.
*
@ -74,6 +98,25 @@ void investigation_graph_view_set_graph(
const InvestigationGraphModel *graph_model
);
/**
* @brief Associe une disposition persistée à la vue.
*
* La disposition est empruntée et doit rester valide pendant son utilisation.
*
* Lorsqu'un graphe est déjà affiché, la disposition privée de ses nœuds est
* reconstruite. Une position persistée remplace la position automatique
* uniquement lorsqu'elle existe pour l'entité concernée.
*
* Passer NULL rétablit le placement automatique.
*
* @param graph_view Vue graphique à mettre à jour.
* @param graph_layout Disposition persistée empruntée, ou NULL.
*/
void investigation_graph_view_set_layout(
InvestigationGraphView *graph_view,
const InvestigationGraphLayout *graph_layout
);
/**
* @brief Détache le graphe actuellement affiché.
*
@ -100,6 +143,24 @@ void investigation_graph_view_set_selection_callback(
gpointer user_data
);
/**
* @brief Définit le callback de fin de déplacement d'un nœud.
*
* Le callback et user_data sont empruntés par la vue.
*
* Le callback n'est appelé ni pendant le déplacement du canvas, ni pour
* un simple clic, ni pendant une reconstruction de la disposition.
*
* @param graph_view Vue graphique.
* @param callback Callback à appeler, ou NULL.
* @param user_data Données empruntées du callback.
*/
void investigation_graph_view_set_node_moved_callback(
InvestigationGraphView *graph_view,
InvestigationGraphViewNodeMovedCallback callback,
gpointer user_data
);
/**
* @brief Retourne l'entité actuellement sélectionnée.
*

View file

@ -16,6 +16,11 @@
*/
typedef struct InvestigationGraphModel InvestigationGraphModel;
/**
* @brief Représentation opaque de la disposition du graphe.
*/
typedef struct InvestigationGraphLayout InvestigationGraphLayout;
/**
* @brief Représentation opaque de la zone de travail.
*/
@ -32,6 +37,23 @@ typedef void (*WorkspaceVerifyEvidenceCallback)(
gpointer user_data
);
/**
* @brief Callback appelé après le déplacement effectif d'un nœud.
*
* Les coordonnées sont exprimées dans l'espace logique du graphe.
*
* @param entity_identifier UUID de l'entité déplacée.
* @param x Coordonnée horizontale logique.
* @param y Coordonnée verticale logique.
* @param user_data Données empruntées fournies par l'appelant.
*/
typedef void (*WorkspaceGraphNodeMovedCallback)(
const char *entity_identifier,
double x,
double y,
gpointer user_data
);
/**
* @brief Crée une nouvelle zone de travail.
*
@ -97,6 +119,21 @@ void workspace_set_verify_evidence_callback(
gpointer user_data
);
/**
* @brief Définit le callback de fin de déplacement d'un nœud.
*
* Le Workspace relaie uniquement l'événement provenant de la vue graphique.
*
* @param workspace Zone de travail.
* @param callback Callback facultatif.
* @param user_data Données empruntées transmises au callback.
*/
void workspace_set_graph_node_moved_callback(
Workspace *workspace,
WorkspaceGraphNodeMovedCallback callback,
gpointer user_data
);
/**
* @brief Affiche l'état de chargement du graphe.
*
@ -109,17 +146,21 @@ void workspace_set_graph_loading(
);
/**
* @brief Affiche le résumé du graphe chargé.
* @brief Affiche le graphe chargé avec sa disposition persistée.
*
* Le Workspace emprunte graph_model et ne le libère jamais.
* Passer NULL équivaut à workspace_clear_graph().
* Le Workspace emprunte graph_model et graph_layout et ne les libère jamais.
*
* Passer NULL pour graph_model équivaut à workspace_clear_graph().
* Passer NULL pour graph_layout conserve le placement automatique.
*
* @param workspace Zone de travail.
* @param graph_model Graphe emprunté.
* @param graph_layout Disposition empruntée, ou NULL.
*/
void workspace_set_graph(
Workspace *workspace,
const InvestigationGraphModel *graph_model
const InvestigationGraphModel *graph_model,
const InvestigationGraphLayout *graph_layout
);
/**

View file

@ -10,6 +10,7 @@
#include "core/investigation_session.h"
#include "core/investigation_tree_model.h"
#include "core/investigation_graph_load_task.h"
#include "models/investigation_graph_layout.h"
#include "models/investigation_graph_model.h"
#include "models/investigation_record.h"
#include "core/investigation_tree_builder.h"
@ -24,6 +25,7 @@
#include "core/evidence_import_task.h"
#include "models/evidence_record.h"
#include "dao/evidence_type_dao.h"
#include "dao/graph_node_position_dao.h"
#include "views/evidence_import_dialog.h"
#include "dao/evidence_dao.h"
#include "models/evidence_type.h"
@ -96,6 +98,7 @@ struct Application
InvestigationGraphLoadTask *graph_load_task;
InvestigationGraphModel *graph_model;
InvestigationGraphLayout *graph_layout;
guint64 graph_load_generation;
char *selected_evidence_identifier;
@ -291,6 +294,9 @@ static void application_clear_graph(
InvestigationGraphModel *graph_model =
NULL;
InvestigationGraphLayout *graph_layout =
NULL;
if (application == NULL)
{
return;
@ -306,12 +312,22 @@ static void application_clear_graph(
graph_model =
application->graph_model;
graph_layout =
application->graph_layout;
application->graph_model =
NULL;
application->graph_layout =
NULL;
investigation_graph_model_free(
graph_model
);
investigation_graph_layout_free(
graph_layout
);
}
/**
@ -332,6 +348,9 @@ static void application_on_graph_loaded(
Application *application =
NULL;
InvestigationGraphLayout *graph_layout =
NULL;
char *status_message =
NULL;
@ -344,6 +363,11 @@ static void application_on_graph_loaded(
context->application;
}
graph_layout =
investigation_graph_load_task_take_layout(
load_task
);
if (application != NULL)
{
is_current_result =
@ -363,6 +387,10 @@ static void application_on_graph_loaded(
graph_model
);
investigation_graph_layout_free(
graph_layout
);
investigation_graph_load_task_free(
load_task
);
@ -379,6 +407,10 @@ static void application_on_graph_loaded(
graph_model
);
investigation_graph_layout_free(
graph_layout
);
main_window_set_graph_error(
application->main_window,
error->message
@ -396,11 +428,21 @@ static void application_on_graph_loaded(
return;
}
if (graph_model == NULL)
if (graph_model == NULL ||
graph_layout == NULL)
{
investigation_graph_model_free(
graph_model
);
investigation_graph_layout_free(
graph_layout
);
main_window_set_graph_error(
application->main_window,
"Le chargement s'est terminé sans fournir de graphe."
"Le chargement s'est terminé sans fournir "
"un graphe et une disposition valides."
);
main_window_set_status(
@ -426,21 +468,28 @@ static void application_on_graph_loaded(
application->graph_model =
graph_model;
application->graph_layout =
graph_layout;
main_window_set_graph(
application->main_window,
application->graph_model
application->graph_model,
application->graph_layout
);
status_message =
g_strdup_printf(
"Graphe chargé : %" G_GUINT64_FORMAT
" entité(s), %" G_GUINT64_FORMAT
" relation(s).",
" relation(s), %u position(s) restaurée(s).",
investigation_graph_model_get_entity_count(
application->graph_model
),
investigation_graph_model_get_relation_count(
application->graph_model
),
investigation_graph_layout_get_count(
application->graph_layout
)
);
@ -4048,6 +4097,186 @@ static void application_on_verify_evidence_requested(
);
}
/**
* @brief Ferme proprement l'application.
*
* @param user_data Pointeur vers Application.
*/
/**
* @brief Enregistre la nouvelle position logique d'un nœud.
*
* Le modèle en mémoire est mis à jour avant l'UPSERT SQLite. En cas d'échec
* SQLite, le déplacement reste visible pendant la session, mais un message
* explicite indique qu'il ne survivra pas à la réouverture de l'enquête.
*/
static void application_on_graph_node_moved(
const char *entity_identifier,
double x,
double y,
gpointer user_data
)
{
Application *application =
user_data;
Database *database =
NULL;
GraphNodePositionDao *position_dao =
NULL;
GError *error =
NULL;
if (application == NULL ||
application->main_window == NULL ||
application->session == NULL ||
application->graph_layout == NULL ||
entity_identifier == NULL ||
!g_uuid_string_is_valid(
entity_identifier
))
{
return;
}
if (!investigation_graph_layout_set_position(
application->graph_layout,
entity_identifier,
x,
y,
&error
))
{
g_warning(
"Impossible de mettre à jour la position du nœud '%s' : %s",
entity_identifier,
error != NULL
? error->message
: "erreur inconnue"
);
application_present_error(
application,
"Position invalide",
error != NULL
? error->message
: "La position du nœud ne peut pas être conservée."
);
g_clear_error(
&error
);
return;
}
database =
investigation_session_get_database(
application->session
);
if (database == NULL)
{
main_window_set_status(
application->main_window,
"Nœud déplacé, mais position non enregistrée."
);
application_present_error(
application,
"Enregistrement impossible",
"La session ne fournit aucune connexion SQLite."
);
return;
}
position_dao =
graph_node_position_dao_new(
database,
&error
);
if (position_dao == NULL)
{
g_warning(
"Impossible de créer le DAO des positions : %s",
error != NULL
? error->message
: "erreur inconnue"
);
main_window_set_status(
application->main_window,
"Nœud déplacé, mais position non enregistrée."
);
application_present_error(
application,
"Enregistrement impossible",
error != NULL
? error->message
: "Impossible d'accéder aux positions du graphe."
);
g_clear_error(
&error
);
return;
}
if (!graph_node_position_dao_upsert(
position_dao,
entity_identifier,
x,
y,
&error
))
{
g_warning(
"Impossible d'enregistrer la position du nœud '%s' : %s",
entity_identifier,
error != NULL
? error->message
: "erreur inconnue"
);
main_window_set_status(
application->main_window,
"Nœud déplacé, mais position non enregistrée."
);
application_present_error(
application,
"Enregistrement impossible",
error != NULL
? error->message
: "La position du nœud n'a pas pu être enregistrée."
);
g_clear_error(
&error
);
graph_node_position_dao_free(
position_dao
);
return;
}
graph_node_position_dao_free(
position_dao
);
main_window_set_status(
application->main_window,
"Position du nœud enregistrée."
);
}
/**
* @brief Ferme proprement l'application.
*
@ -4138,6 +4367,12 @@ static void application_on_activate(
application
);
main_window_set_graph_node_moved_callback(
application->main_window,
application_on_graph_node_moved,
application
);
main_window_set_new_investigation_callback(
application->main_window,
application_on_new_investigation_requested,

View file

@ -8,7 +8,10 @@
#include "core/background_task.h"
#include "core/investigation_graph_loader.h"
#include "dao/graph_node_position_dao.h"
#include "database/database.h"
#include "models/graph_node_position.h"
#include "models/investigation_graph_layout.h"
#include <gio/gio.h>
#include <glib.h>
@ -35,6 +38,7 @@ typedef struct
typedef struct
{
InvestigationGraphModel *graph_model;
InvestigationGraphLayout *graph_layout;
} InvestigationGraphLoadResult;
/**
@ -61,6 +65,8 @@ struct InvestigationGraphLoadTask
gpointer user_data;
GDestroyNotify user_data_destroy;
InvestigationGraphLayout *completed_layout;
gboolean running;
gboolean disposed;
};
@ -484,9 +490,16 @@ static void investigation_graph_load_result_free(
load_result->graph_model
);
investigation_graph_layout_free(
load_result->graph_layout
);
load_result->graph_model =
NULL;
load_result->graph_layout =
NULL;
g_free(
load_result
);
@ -573,6 +586,13 @@ static void investigation_graph_load_task_destroy(
);
}
investigation_graph_layout_free(
load_task->completed_layout
);
load_task->completed_layout =
NULL;
g_free(
load_task->database_path
);
@ -685,15 +705,27 @@ static gboolean investigation_graph_load_task_worker(
InvestigationGraphLoader *graph_loader =
NULL;
GraphNodePositionDao *position_dao =
NULL;
GPtrArray *positions =
NULL;
InvestigationGraphModel *graph_model =
NULL;
InvestigationGraphLayout *graph_layout =
NULL;
InvestigationGraphLoadResult *load_result =
NULL;
GError *component_error =
NULL;
guint position_index =
0;
if (background_task == NULL ||
cancellable == NULL ||
worker_data == NULL ||
@ -833,6 +865,128 @@ static gboolean investigation_graph_load_task_worker(
goto failure;
}
background_task_report_progress(
background_task,
0.65,
"Chargement des positions du graphe"
);
position_dao =
graph_node_position_dao_new(
database,
&component_error
);
if (position_dao == NULL)
{
investigation_graph_load_task_set_nested_error(
error,
INVESTIGATION_GRAPH_LOAD_TASK_ERROR_LOAD,
"Impossible de préparer le chargement des positions",
component_error
);
goto failure;
}
g_clear_error(
&component_error
);
positions =
graph_node_position_dao_list_all(
position_dao,
&component_error
);
if (positions == NULL)
{
investigation_graph_load_task_set_nested_error(
error,
INVESTIGATION_GRAPH_LOAD_TASK_ERROR_LOAD,
"Impossible de charger les positions du graphe",
component_error
);
goto failure;
}
g_clear_error(
&component_error
);
graph_layout =
investigation_graph_layout_new();
if (graph_layout == NULL)
{
investigation_graph_load_task_set_error_literal(
error,
INVESTIGATION_GRAPH_LOAD_TASK_ERROR_MEMORY,
"Impossible d'allouer la disposition du graphe."
);
goto failure;
}
for (position_index = 0;
position_index < positions->len;
position_index++)
{
const GraphNodePosition *position =
g_ptr_array_index(
positions,
position_index
);
if (position == NULL)
{
continue;
}
if (!investigation_graph_layout_set_position(
graph_layout,
graph_node_position_get_entity_identifier(
position
),
graph_node_position_get_x(
position
),
graph_node_position_get_y(
position
),
&component_error
))
{
investigation_graph_load_task_set_nested_error(
error,
INVESTIGATION_GRAPH_LOAD_TASK_ERROR_LOAD,
"Impossible de construire la disposition du graphe",
component_error
);
goto failure;
}
g_clear_error(
&component_error
);
}
g_ptr_array_unref(
positions
);
positions =
NULL;
graph_node_position_dao_free(
position_dao
);
position_dao =
NULL;
investigation_graph_loader_free(
graph_loader
);
@ -875,9 +1029,15 @@ static gboolean investigation_graph_load_task_worker(
load_result->graph_model =
graph_model;
load_result->graph_layout =
graph_layout;
graph_model =
NULL;
graph_layout =
NULL;
if (investigation_graph_load_task_check_cancelled(
cancellable,
error
@ -911,6 +1071,21 @@ failure:
graph_model
);
investigation_graph_layout_free(
graph_layout
);
if (positions != NULL)
{
g_ptr_array_unref(
positions
);
}
graph_node_position_dao_free(
position_dao
);
investigation_graph_loader_free(
graph_loader
);
@ -1015,6 +1190,9 @@ static void investigation_graph_load_task_on_completed(
InvestigationGraphModel *graph_model =
NULL;
InvestigationGraphLayout *graph_layout =
NULL;
GError *callback_error =
NULL;
@ -1085,13 +1263,20 @@ static void investigation_graph_load_task_on_completed(
);
if (load_result != NULL &&
load_result->graph_model != NULL)
load_result->graph_model != NULL &&
load_result->graph_layout != NULL)
{
graph_model =
load_result->graph_model;
graph_layout =
load_result->graph_layout;
load_result->graph_model =
NULL;
load_result->graph_layout =
NULL;
}
else
{
@ -1111,6 +1296,27 @@ static void investigation_graph_load_task_on_completed(
);
}
if (graph_layout != NULL)
{
g_mutex_lock(
&load_task->mutex
);
investigation_graph_layout_free(
load_task->completed_layout
);
load_task->completed_layout =
graph_layout;
graph_layout =
NULL;
g_mutex_unlock(
&load_task->mutex
);
}
callback(
load_task,
graph_model,
@ -1448,6 +1654,13 @@ gboolean investigation_graph_load_task_start(
goto start_failure_locked;
}
investigation_graph_layout_free(
load_task->completed_layout
);
load_task->completed_layout =
NULL;
load_task->background_task =
background_task;
@ -1610,3 +1823,32 @@ gboolean investigation_graph_load_task_is_running(
return running;
}
InvestigationGraphLayout *investigation_graph_load_task_take_layout(
InvestigationGraphLoadTask *load_task
)
{
InvestigationGraphLayout *graph_layout =
NULL;
if (load_task == NULL)
{
return NULL;
}
g_mutex_lock(
&load_task->mutex
);
graph_layout =
load_task->completed_layout;
load_task->completed_layout =
NULL;
g_mutex_unlock(
&load_task->mutex
);
return graph_layout;
}

View file

@ -0,0 +1,793 @@
/******************************************************************************
* @file graph_node_position_dao.c
* @brief Persistance des positions des nœuds du graphe dans SQLite.
******************************************************************************/
#include "dao/graph_node_position_dao.h"
#include "database/error.h"
#include "database/statement.h"
#include <math.h>
#include <string.h>
/**
* @struct GraphNodePositionDao
* @brief Représentation privée du DAO.
*
* La connexion Database est empruntée.
*/
struct GraphNodePositionDao
{
Database *database;
};
/**
* @brief Requête de chargement de toutes les positions.
*/
static const char *const graph_node_position_dao_list_all_sql =
"SELECT "
" entity_id,"
" x,"
" y,"
" updated_at "
"FROM graph_node_positions "
"ORDER BY entity_id ASC;";
/**
* @brief Requête d'insertion ou de mise à jour d'une position.
*/
static const char *const graph_node_position_dao_upsert_sql =
"INSERT INTO graph_node_positions"
"("
" entity_id,"
" x,"
" y,"
" updated_at"
")"
"VALUES"
"("
" ?,"
" ?,"
" ?,"
" ?"
")"
"ON CONFLICT(entity_id)"
"DO UPDATE SET "
" x = excluded.x,"
" y = excluded.y,"
" updated_at = excluded.updated_at;";
/**
* @brief Requête de suppression d'une position.
*/
static const char *const graph_node_position_dao_delete_sql =
"DELETE FROM graph_node_positions "
"WHERE entity_id = ?;";
/**
* @brief Enregistre une erreur littérale.
*/
static void graph_node_position_dao_set_error_literal(
GError **error,
GraphNodePositionDaoError error_code,
const char *error_message
)
{
if (error == NULL)
{
return;
}
g_set_error_literal(
error,
GRAPH_NODE_POSITION_DAO_ERROR,
error_code,
error_message
);
}
/**
* @brief Transforme la dernière erreur Database en erreur du DAO.
*/
static void graph_node_position_dao_set_database_error(
GraphNodePositionDao *position_dao,
GError **error,
GraphNodePositionDaoError error_code,
const char *context
)
{
const char *database_error_message =
NULL;
if (error == NULL)
{
return;
}
if (position_dao != NULL &&
position_dao->database != NULL)
{
database_error_message =
database_error_get_message(
position_dao->database
);
}
if (database_error_message != NULL &&
database_error_message[0] != '\0')
{
g_set_error(
error,
GRAPH_NODE_POSITION_DAO_ERROR,
error_code,
"%s : %s",
context,
database_error_message
);
return;
}
g_set_error_literal(
error,
GRAPH_NODE_POSITION_DAO_ERROR,
error_code,
context
);
}
/**
* @brief Génère une date UTC au format attendu par le modèle.
*
* @return Nouvelle chaîne à libérer avec g_free(), ou NULL.
*/
static char *graph_node_position_dao_create_utc_timestamp(void)
{
GDateTime *date_time =
NULL;
char *timestamp =
NULL;
date_time =
g_date_time_new_now_utc();
if (date_time == NULL)
{
return NULL;
}
timestamp =
g_date_time_format(
date_time,
"%Y-%m-%dT%H:%M:%SZ"
);
g_date_time_unref(
date_time
);
return timestamp;
}
/**
* @brief Libère une position contenue dans un GPtrArray.
*/
static void graph_node_position_dao_record_destroy(
gpointer data
)
{
graph_node_position_free(
data
);
}
/**
* @brief Construit une position depuis la ligne SQLite courante.
*/
static GraphNodePosition *
graph_node_position_dao_read_current_record(
GraphNodePositionDao *position_dao,
DatabaseStatement *statement,
GError **error
)
{
GraphNodePosition *position =
NULL;
char *entity_identifier =
NULL;
char *updated_at =
NULL;
double x =
0.0;
double y =
0.0;
GError *model_error =
NULL;
if (position_dao == NULL ||
position_dao->database == NULL ||
statement == NULL)
{
graph_node_position_dao_set_error_literal(
error,
GRAPH_NODE_POSITION_DAO_ERROR_INVALID_ARGUMENT,
"Impossible de lire une position avec des paramètres invalides."
);
return NULL;
}
if (!database_statement_column_text(
statement,
0,
&entity_identifier
) ||
!database_statement_column_double(
statement,
1,
&x
) ||
!database_statement_column_double(
statement,
2,
&y
) ||
!database_statement_column_text(
statement,
3,
&updated_at
))
{
graph_node_position_dao_set_error_literal(
error,
GRAPH_NODE_POSITION_DAO_ERROR_READ,
"Impossible de lire les colonnes de la position du nœud."
);
goto cleanup;
}
position =
graph_node_position_new(
entity_identifier,
x,
y,
updated_at,
&model_error
);
if (position == NULL)
{
if (model_error != NULL)
{
g_set_error(
error,
GRAPH_NODE_POSITION_DAO_ERROR,
GRAPH_NODE_POSITION_DAO_ERROR_MODEL,
"La position persistée est invalide : %s",
model_error->message
);
}
else
{
graph_node_position_dao_set_error_literal(
error,
GRAPH_NODE_POSITION_DAO_ERROR_MODEL,
"La position persistée ne peut pas être reconstruite."
);
}
}
cleanup:
g_clear_error(
&model_error
);
g_free(
updated_at
);
g_free(
entity_identifier
);
return position;
}
/**
* @brief Convertit une erreur SQLite d'UPSERT en erreur du DAO.
*/
static void graph_node_position_dao_set_upsert_error(
GraphNodePositionDao *position_dao,
GError **error
)
{
const char *database_error_message =
NULL;
if (position_dao != NULL &&
position_dao->database != NULL)
{
database_error_message =
database_error_get_message(
position_dao->database
);
}
if (database_error_message != NULL &&
(
strstr(
database_error_message,
"constraint"
) != NULL ||
strstr(
database_error_message,
"CONSTRAINT"
) != NULL ||
strstr(
database_error_message,
"FOREIGN KEY"
) != NULL
))
{
graph_node_position_dao_set_database_error(
position_dao,
error,
GRAPH_NODE_POSITION_DAO_ERROR_CONSTRAINT,
"La position viole une contrainte SQLite"
);
return;
}
graph_node_position_dao_set_database_error(
position_dao,
error,
GRAPH_NODE_POSITION_DAO_ERROR_EXECUTE,
"Impossible d'enregistrer la position du nœud"
);
}
GQuark graph_node_position_dao_error_quark(void)
{
return g_quark_from_static_string(
"graph-node-position-dao-error-quark"
);
}
GraphNodePositionDao *graph_node_position_dao_new(
Database *database,
GError **error
)
{
GraphNodePositionDao *position_dao =
NULL;
g_return_val_if_fail(
error == NULL || *error == NULL,
NULL
);
if (database == NULL)
{
graph_node_position_dao_set_error_literal(
error,
GRAPH_NODE_POSITION_DAO_ERROR_INVALID_ARGUMENT,
"La connexion Database est absente."
);
return NULL;
}
position_dao =
g_try_new0(
GraphNodePositionDao,
1
);
if (position_dao == NULL)
{
graph_node_position_dao_set_error_literal(
error,
GRAPH_NODE_POSITION_DAO_ERROR_MEMORY,
"Impossible d'allouer le DAO des positions de nœuds."
);
return NULL;
}
position_dao->database =
database;
return position_dao;
}
void graph_node_position_dao_free(
GraphNodePositionDao *position_dao
)
{
if (position_dao == NULL)
{
return;
}
position_dao->database =
NULL;
g_free(
position_dao
);
}
GPtrArray *graph_node_position_dao_list_all(
GraphNodePositionDao *position_dao,
GError **error
)
{
DatabaseStatement *statement =
NULL;
GPtrArray *positions =
NULL;
GraphNodePosition *position =
NULL;
DatabaseStatementStepResult step_result =
DATABASE_STATEMENT_STEP_ERROR;
g_return_val_if_fail(
error == NULL || *error == NULL,
NULL
);
if (position_dao == NULL ||
position_dao->database == NULL)
{
graph_node_position_dao_set_error_literal(
error,
GRAPH_NODE_POSITION_DAO_ERROR_INVALID_ARGUMENT,
"Le DAO des positions de nœuds est invalide."
);
return NULL;
}
positions =
g_ptr_array_new_with_free_func(
graph_node_position_dao_record_destroy
);
if (positions == NULL)
{
graph_node_position_dao_set_error_literal(
error,
GRAPH_NODE_POSITION_DAO_ERROR_MEMORY,
"Impossible d'allouer la liste des positions de nœuds."
);
return NULL;
}
statement =
database_statement_prepare(
position_dao->database,
graph_node_position_dao_list_all_sql
);
if (statement == NULL)
{
graph_node_position_dao_set_database_error(
position_dao,
error,
GRAPH_NODE_POSITION_DAO_ERROR_PREPARE,
"Impossible de préparer le chargement des positions"
);
goto error;
}
while (TRUE)
{
step_result =
database_statement_step(
statement
);
if (step_result ==
DATABASE_STATEMENT_STEP_DONE)
{
break;
}
if (step_result ==
DATABASE_STATEMENT_STEP_ERROR)
{
graph_node_position_dao_set_database_error(
position_dao,
error,
GRAPH_NODE_POSITION_DAO_ERROR_EXECUTE,
"Impossible d'exécuter le chargement des positions"
);
goto error;
}
position =
graph_node_position_dao_read_current_record(
position_dao,
statement,
error
);
if (position == NULL)
{
goto error;
}
g_ptr_array_add(
positions,
position
);
/*
* Le tableau devient propriétaire du modèle.
*/
position =
NULL;
}
database_statement_finalize(
statement
);
return positions;
error:
graph_node_position_free(
position
);
database_statement_finalize(
statement
);
g_ptr_array_unref(
positions
);
return NULL;
}
gboolean graph_node_position_dao_upsert(
GraphNodePositionDao *position_dao,
const char *entity_identifier,
double x,
double y,
GError **error
)
{
DatabaseStatement *statement =
NULL;
char *updated_at =
NULL;
gboolean success =
FALSE;
g_return_val_if_fail(
error == NULL || *error == NULL,
FALSE
);
if (position_dao == NULL ||
position_dao->database == NULL ||
entity_identifier == NULL ||
!g_uuid_string_is_valid(
entity_identifier
) ||
!isfinite(x) ||
!isfinite(y))
{
graph_node_position_dao_set_error_literal(
error,
GRAPH_NODE_POSITION_DAO_ERROR_INVALID_ARGUMENT,
"Les paramètres de la position du nœud sont invalides."
);
return FALSE;
}
updated_at =
graph_node_position_dao_create_utc_timestamp();
if (updated_at == NULL)
{
graph_node_position_dao_set_error_literal(
error,
GRAPH_NODE_POSITION_DAO_ERROR_MEMORY,
"Impossible de générer la date de modification de la position."
);
return FALSE;
}
statement =
database_statement_prepare(
position_dao->database,
graph_node_position_dao_upsert_sql
);
if (statement == NULL)
{
graph_node_position_dao_set_database_error(
position_dao,
error,
GRAPH_NODE_POSITION_DAO_ERROR_PREPARE,
"Impossible de préparer l'enregistrement de la position"
);
goto cleanup;
}
if (!database_statement_bind_text(
statement,
1,
entity_identifier
) ||
!database_statement_bind_double(
statement,
2,
x
) ||
!database_statement_bind_double(
statement,
3,
y
) ||
!database_statement_bind_text(
statement,
4,
updated_at
))
{
graph_node_position_dao_set_database_error(
position_dao,
error,
GRAPH_NODE_POSITION_DAO_ERROR_BIND,
"Impossible de lier les données de la position"
);
goto cleanup;
}
if (database_statement_step(
statement
) != DATABASE_STATEMENT_STEP_DONE)
{
graph_node_position_dao_set_upsert_error(
position_dao,
error
);
goto cleanup;
}
success =
TRUE;
cleanup:
database_statement_finalize(
statement
);
g_free(
updated_at
);
return success;
}
gboolean graph_node_position_dao_delete(
GraphNodePositionDao *position_dao,
const char *entity_identifier,
GError **error
)
{
DatabaseStatement *statement =
NULL;
gboolean success =
FALSE;
g_return_val_if_fail(
error == NULL || *error == NULL,
FALSE
);
if (position_dao == NULL ||
position_dao->database == NULL ||
entity_identifier == NULL ||
!g_uuid_string_is_valid(
entity_identifier
))
{
graph_node_position_dao_set_error_literal(
error,
GRAPH_NODE_POSITION_DAO_ERROR_INVALID_ARGUMENT,
"L'identifiant de l'entité est invalide."
);
return FALSE;
}
statement =
database_statement_prepare(
position_dao->database,
graph_node_position_dao_delete_sql
);
if (statement == NULL)
{
graph_node_position_dao_set_database_error(
position_dao,
error,
GRAPH_NODE_POSITION_DAO_ERROR_PREPARE,
"Impossible de préparer la suppression de la position"
);
goto cleanup;
}
if (!database_statement_bind_text(
statement,
1,
entity_identifier
))
{
graph_node_position_dao_set_database_error(
position_dao,
error,
GRAPH_NODE_POSITION_DAO_ERROR_BIND,
"Impossible de lier l'identifiant de l'entité"
);
goto cleanup;
}
if (database_statement_step(
statement
) != DATABASE_STATEMENT_STEP_DONE)
{
graph_node_position_dao_set_database_error(
position_dao,
error,
GRAPH_NODE_POSITION_DAO_ERROR_EXECUTE,
"Impossible de supprimer la position du nœud"
);
goto cleanup;
}
success =
TRUE;
cleanup:
database_statement_finalize(
statement
);
return success;
}

View file

@ -668,6 +668,65 @@ rollback:
return false;
}
/**
* @brief Garantit atomiquement la présence des extensions du schéma courant.
*/
static bool database_ensure_current_schema(
Database *database
)
{
bool transaction_started = false;
if (database == NULL)
{
return false;
}
if (!database_transaction_begin(
database
))
{
return false;
}
transaction_started = true;
if (!schema_ensure_current(
database
))
{
goto rollback;
}
if (!database_transaction_commit(
database
))
{
goto rollback;
}
transaction_started = false;
return true;
rollback:
if (transaction_started)
{
if (!database_transaction_rollback(
database
))
{
g_warning(
"Impossible dannuler linstallation des extensions "
"du schéma SQLite courant."
);
}
}
return false;
}
Database *database_open(
const char *database_path
)
@ -807,6 +866,13 @@ bool database_migrate_to_latest(
}
}
if (!database_ensure_current_schema(
database
))
{
return false;
}
database->schema_version =
schema_version;
@ -897,6 +963,13 @@ bool database_initialize(
goto rollback;
}
if (!schema_ensure_current(
database
))
{
goto rollback;
}
if (!database_insert_all_metadata(
database,
created_at,

View file

@ -192,3 +192,14 @@ bool schema_install_v2(
"la migration SQLite V2"
);
}
bool schema_ensure_current(
Database *database
)
{
return schema_execute_file(
database,
"database/schema_current.sql",
"les extensions du schéma SQLite courant"
);
}

View file

@ -238,6 +238,79 @@ bool database_statement_bind_int64(
return true;
}
bool database_statement_bind_double(
DatabaseStatement *statement,
int index,
double value
)
{
sqlite3 *database_handle =
NULL;
const char *error_message =
NULL;
int result =
SQLITE_ERROR;
if (statement == NULL)
{
return false;
}
if (statement->handle == NULL ||
index <= 0)
{
database_set_error(
statement->database,
DATABASE_ERROR_INVALID_ARGUMENT,
"Paramètres invalides pour la liaison d'un nombre réel."
);
return false;
}
result =
sqlite3_bind_double(
statement->handle,
index,
value
);
if (result != SQLITE_OK)
{
database_handle =
database_get_handle(
statement->database
);
error_message =
database_handle != NULL
? sqlite3_errmsg(database_handle)
: sqlite3_errstr(result);
database_set_error(
statement->database,
DATABASE_ERROR_SQLITE,
error_message
);
g_warning(
"Impossible de lier le paramètre réel %d : %s",
index,
error_message
);
return false;
}
database_clear_error_internal(
statement->database
);
return true;
}
bool database_statement_bind_null(
DatabaseStatement *statement,
int index
@ -477,6 +550,57 @@ bool database_statement_column_is_null(
return true;
}
bool database_statement_column_double(
DatabaseStatement *statement,
int column_index,
double *value
)
{
int column_count =
0;
int column_type =
SQLITE_NULL;
if (statement == NULL ||
statement->handle == NULL ||
value == NULL)
{
return false;
}
column_count =
sqlite3_column_count(
statement->handle
);
if (column_index < 0 ||
column_index >= column_count)
{
return false;
}
column_type =
sqlite3_column_type(
statement->handle,
column_index
);
if (column_type != SQLITE_FLOAT &&
column_type != SQLITE_INTEGER)
{
return false;
}
*value =
sqlite3_column_double(
statement->handle,
column_index
);
return true;
}
bool database_statement_column_text(
DatabaseStatement *statement,
int column_index,

View file

@ -0,0 +1,336 @@
/******************************************************************************
* @file graph_node_position.c
* @brief Implémentation du modèle de position persistée d'un nœud.
******************************************************************************/
#include "models/graph_node_position.h"
#include <math.h>
#include <string.h>
/**
* @struct GraphNodePosition
* @brief Représentation privée d'une position de nœud.
*/
struct GraphNodePosition
{
char *entity_identifier;
double x;
double y;
char *updated_at;
};
/**
* @brief Copie et nettoie une chaîne.
*/
static char *graph_node_position_duplicate_trimmed(
const char *value
)
{
char *copy =
NULL;
if (value == NULL)
{
return NULL;
}
copy =
g_strdup(
value
);
if (copy == NULL)
{
return NULL;
}
g_strstrip(
copy
);
return copy;
}
/**
* @brief Vérifie une date UTC stricte YYYY-MM-DDTHH:MM:SSZ.
*/
static gboolean graph_node_position_timestamp_is_valid(
const char *timestamp
)
{
GDateTime *date_time =
NULL;
char *normalized_timestamp =
NULL;
gboolean valid =
FALSE;
gsize character_index =
0;
if (timestamp == NULL ||
strlen(timestamp) != 20U)
{
return FALSE;
}
if (timestamp[4] != '-' ||
timestamp[7] != '-' ||
timestamp[10] != 'T' ||
timestamp[13] != ':' ||
timestamp[16] != ':' ||
timestamp[19] != 'Z')
{
return FALSE;
}
for (character_index = 0;
character_index < 20U;
character_index++)
{
if (character_index == 4U ||
character_index == 7U ||
character_index == 10U ||
character_index == 13U ||
character_index == 16U ||
character_index == 19U)
{
continue;
}
if (!g_ascii_isdigit(
timestamp[character_index]
))
{
return FALSE;
}
}
date_time =
g_date_time_new_from_iso8601(
timestamp,
NULL
);
if (date_time == NULL)
{
return FALSE;
}
normalized_timestamp =
g_date_time_format(
date_time,
"%Y-%m-%dT%H:%M:%SZ"
);
valid =
normalized_timestamp != NULL &&
g_strcmp0(
normalized_timestamp,
timestamp
) == 0;
g_free(
normalized_timestamp
);
g_date_time_unref(
date_time
);
return valid;
}
GQuark graph_node_position_error_quark(void)
{
return g_quark_from_static_string(
"graph-node-position-error-quark"
);
}
GraphNodePosition *graph_node_position_new(
const char *entity_identifier,
double x,
double y,
const char *updated_at,
GError **error
)
{
GraphNodePosition *position =
NULL;
char *entity_identifier_copy =
NULL;
char *updated_at_copy =
NULL;
g_return_val_if_fail(
error == NULL || *error == NULL,
NULL
);
entity_identifier_copy =
graph_node_position_duplicate_trimmed(
entity_identifier
);
if (entity_identifier_copy == NULL ||
entity_identifier_copy[0] == '\0' ||
!g_uuid_string_is_valid(
entity_identifier_copy
))
{
g_set_error_literal(
error,
GRAPH_NODE_POSITION_ERROR,
GRAPH_NODE_POSITION_ERROR_INVALID_IDENTIFIER,
"L'identifiant de l'entité n'est pas un UUID valide."
);
goto cleanup;
}
if (!isfinite(x) ||
!isfinite(y))
{
g_set_error_literal(
error,
GRAPH_NODE_POSITION_ERROR,
GRAPH_NODE_POSITION_ERROR_INVALID_COORDINATE,
"Les coordonnées du nœud doivent être des nombres finis."
);
goto cleanup;
}
updated_at_copy =
graph_node_position_duplicate_trimmed(
updated_at
);
if (!graph_node_position_timestamp_is_valid(
updated_at_copy
))
{
g_set_error_literal(
error,
GRAPH_NODE_POSITION_ERROR,
GRAPH_NODE_POSITION_ERROR_INVALID_DATE,
"La date de modification de la position est invalide."
);
goto cleanup;
}
position =
g_try_new0(
GraphNodePosition,
1
);
if (position == NULL)
{
g_set_error_literal(
error,
GRAPH_NODE_POSITION_ERROR,
GRAPH_NODE_POSITION_ERROR_INVALID_ARGUMENT,
"Impossible d'allouer la position du nœud."
);
goto cleanup;
}
position->entity_identifier =
entity_identifier_copy;
position->x =
x;
position->y =
y;
position->updated_at =
updated_at_copy;
entity_identifier_copy =
NULL;
updated_at_copy =
NULL;
cleanup:
g_free(
updated_at_copy
);
g_free(
entity_identifier_copy
);
return position;
}
void graph_node_position_free(
GraphNodePosition *position
)
{
if (position == NULL)
{
return;
}
g_free(
position->updated_at
);
g_free(
position->entity_identifier
);
g_free(
position
);
}
const char *graph_node_position_get_entity_identifier(
const GraphNodePosition *position
)
{
return position != NULL
? position->entity_identifier
: NULL;
}
double graph_node_position_get_x(
const GraphNodePosition *position
)
{
return position != NULL
? position->x
: 0.0;
}
double graph_node_position_get_y(
const GraphNodePosition *position
)
{
return position != NULL
? position->y
: 0.0;
}
const char *graph_node_position_get_updated_at(
const GraphNodePosition *position
)
{
return position != NULL
? position->updated_at
: NULL;
}

View file

@ -0,0 +1,321 @@
/******************************************************************************
* @file investigation_graph_layout.c
* @brief Implémentation du modèle en mémoire de disposition du graphe.
******************************************************************************/
#include "models/investigation_graph_layout.h"
#include <math.h>
/**
* @brief Coordonnées privées associées à une entité.
*/
typedef struct
{
double x;
double y;
} InvestigationGraphLayoutPosition;
/**
* @struct InvestigationGraphLayout
* @brief Représentation privée de la disposition.
*/
struct InvestigationGraphLayout
{
GHashTable *positions_by_entity_identifier;
};
/**
* @brief Enregistre une erreur littérale.
*/
static void investigation_graph_layout_set_error_literal(
GError **error,
InvestigationGraphLayoutError error_code,
const char *error_message
)
{
if (error == NULL)
{
return;
}
g_set_error_literal(
error,
INVESTIGATION_GRAPH_LAYOUT_ERROR,
error_code,
error_message
);
}
/**
* @brief Vérifie un identifiant d'entité.
*/
static gboolean investigation_graph_layout_identifier_is_valid(
const char *entity_identifier
)
{
return entity_identifier != NULL &&
entity_identifier[0] != '\0' &&
g_uuid_string_is_valid(
entity_identifier
);
}
GQuark investigation_graph_layout_error_quark(void)
{
return g_quark_from_static_string(
"investigation-graph-layout-error-quark"
);
}
InvestigationGraphLayout *investigation_graph_layout_new(void)
{
InvestigationGraphLayout *layout =
NULL;
layout =
g_try_new0(
InvestigationGraphLayout,
1
);
if (layout == NULL)
{
return NULL;
}
layout->positions_by_entity_identifier =
g_hash_table_new_full(
g_str_hash,
g_str_equal,
g_free,
g_free
);
if (layout->positions_by_entity_identifier == NULL)
{
investigation_graph_layout_free(
layout
);
return NULL;
}
return layout;
}
gboolean investigation_graph_layout_set_position(
InvestigationGraphLayout *layout,
const char *entity_identifier,
double x,
double y,
GError **error
)
{
InvestigationGraphLayoutPosition *position =
NULL;
char *entity_identifier_copy =
NULL;
g_return_val_if_fail(
error == NULL || *error == NULL,
FALSE
);
if (layout == NULL ||
layout->positions_by_entity_identifier == NULL)
{
investigation_graph_layout_set_error_literal(
error,
INVESTIGATION_GRAPH_LAYOUT_ERROR_INVALID_ARGUMENT,
"La disposition du graphe est absente."
);
return FALSE;
}
if (!investigation_graph_layout_identifier_is_valid(
entity_identifier
))
{
investigation_graph_layout_set_error_literal(
error,
INVESTIGATION_GRAPH_LAYOUT_ERROR_INVALID_IDENTIFIER,
"L'identifiant de l'entité n'est pas un UUID valide."
);
return FALSE;
}
if (!isfinite(x) ||
!isfinite(y))
{
investigation_graph_layout_set_error_literal(
error,
INVESTIGATION_GRAPH_LAYOUT_ERROR_INVALID_COORDINATE,
"Les coordonnées du nœud doivent être des nombres finis."
);
return FALSE;
}
entity_identifier_copy =
g_strdup(
entity_identifier
);
position =
g_try_new0(
InvestigationGraphLayoutPosition,
1
);
if (entity_identifier_copy == NULL ||
position == NULL)
{
g_free(
position
);
g_free(
entity_identifier_copy
);
investigation_graph_layout_set_error_literal(
error,
INVESTIGATION_GRAPH_LAYOUT_ERROR_MEMORY,
"Impossible d'allouer la position du nœud."
);
return FALSE;
}
position->x =
x;
position->y =
y;
g_hash_table_replace(
layout->positions_by_entity_identifier,
entity_identifier_copy,
position
);
return TRUE;
}
gboolean investigation_graph_layout_get_position(
const InvestigationGraphLayout *layout,
const char *entity_identifier,
double *x,
double *y
)
{
const InvestigationGraphLayoutPosition *position =
NULL;
if (layout == NULL ||
layout->positions_by_entity_identifier == NULL ||
!investigation_graph_layout_identifier_is_valid(
entity_identifier
))
{
return FALSE;
}
position =
g_hash_table_lookup(
layout->positions_by_entity_identifier,
entity_identifier
);
if (position == NULL)
{
return FALSE;
}
if (x != NULL)
{
*x =
position->x;
}
if (y != NULL)
{
*y =
position->y;
}
return TRUE;
}
gboolean investigation_graph_layout_remove_position(
InvestigationGraphLayout *layout,
const char *entity_identifier
)
{
if (layout == NULL ||
layout->positions_by_entity_identifier == NULL ||
!investigation_graph_layout_identifier_is_valid(
entity_identifier
))
{
return FALSE;
}
return g_hash_table_remove(
layout->positions_by_entity_identifier,
entity_identifier
);
}
void investigation_graph_layout_clear(
InvestigationGraphLayout *layout
)
{
if (layout == NULL ||
layout->positions_by_entity_identifier == NULL)
{
return;
}
g_hash_table_remove_all(
layout->positions_by_entity_identifier
);
}
guint investigation_graph_layout_get_count(
const InvestigationGraphLayout *layout
)
{
if (layout == NULL ||
layout->positions_by_entity_identifier == NULL)
{
return 0U;
}
return g_hash_table_size(
layout->positions_by_entity_identifier
);
}
void investigation_graph_layout_free(
InvestigationGraphLayout *layout
)
{
if (layout == NULL)
{
return;
}
g_clear_pointer(
&layout->positions_by_entity_identifier,
g_hash_table_unref
);
g_free(
layout
);
}

View file

@ -92,6 +92,12 @@ struct MainWindow
gpointer
verify_evidence_user_data;
MainWindowGraphNodeMovedCallback
graph_node_moved_callback;
gpointer
graph_node_moved_user_data;
MainWindowQuitCallback
quit_callback;
@ -252,6 +258,35 @@ static GtkWidget *main_window_create_action_button(
return button;
}
/**
* @brief Relaie la demande de vérification provenant du Workspace.
*/
static void main_window_on_graph_node_moved(
const char *entity_identifier,
double x,
double y,
gpointer user_data
)
{
MainWindow *main_window =
user_data;
if (main_window == NULL ||
main_window->graph_node_moved_callback == NULL ||
entity_identifier == NULL ||
entity_identifier[0] == '\0')
{
return;
}
main_window->graph_node_moved_callback(
entity_identifier,
x,
y,
main_window->graph_node_moved_user_data
);
}
/**
* @brief Relaie la demande de vérification provenant du Workspace.
*/
@ -515,6 +550,12 @@ MainWindow *main_window_new(
main_window
);
workspace_set_graph_node_moved_callback(
main_window->workspace,
main_window_on_graph_node_moved,
main_window
);
workspace_widget = workspace_get_widget(
main_window->workspace
);
@ -870,7 +911,8 @@ void main_window_set_graph_loading(
void main_window_set_graph(
MainWindow *main_window,
const InvestigationGraphModel *graph_model
const InvestigationGraphModel *graph_model,
const InvestigationGraphLayout *graph_layout
)
{
if (main_window == NULL)
@ -880,7 +922,8 @@ void main_window_set_graph(
workspace_set_graph(
main_window->workspace,
graph_model
graph_model,
graph_layout
);
}
@ -1058,6 +1101,24 @@ void main_window_set_verify_evidence_callback(
user_data;
}
void main_window_set_graph_node_moved_callback(
MainWindow *main_window,
MainWindowGraphNodeMovedCallback callback,
gpointer user_data
)
{
if (main_window == NULL)
{
return;
}
main_window->graph_node_moved_callback =
callback;
main_window->graph_node_moved_user_data =
user_data;
}
void main_window_set_quit_callback(
MainWindow *main_window,
MainWindowQuitCallback callback,
@ -1174,6 +1235,18 @@ void main_window_free(
NULL
);
workspace_set_graph_node_moved_callback(
main_window->workspace,
NULL,
NULL
);
main_window->graph_node_moved_callback =
NULL;
main_window->graph_node_moved_user_data =
NULL;
main_window_clear_graph(
main_window
);

View file

@ -6,6 +6,7 @@
#include "widgets/investigation_graph_view.h"
#include "models/entity_record.h"
#include "models/investigation_graph_layout.h"
#include "models/investigation_graph_model.h"
#include "models/relation_record.h"
@ -57,6 +58,7 @@ struct InvestigationGraphView
GtkEventController *motion_controller;
const InvestigationGraphModel *graph_model;
const InvestigationGraphLayout *graph_layout;
GPtrArray *node_layouts;
GHashTable *node_layouts_by_identifier;
@ -92,6 +94,9 @@ struct InvestigationGraphView
InvestigationGraphViewSelectionCallback selection_callback;
gpointer selection_user_data;
InvestigationGraphViewNodeMovedCallback node_moved_callback;
gpointer node_moved_user_data;
double node_drag_pointer_offset_x;
double node_drag_pointer_offset_y;
@ -1042,6 +1047,44 @@ static void investigation_graph_view_on_drag_update(
);
}
/**
* @brief Prévient l'appelant qu'un nœud a é déplacé.
*/
static void investigation_graph_view_notify_node_moved(
InvestigationGraphView *graph_view,
const InvestigationGraphNodeLayout *node_layout
)
{
const char *entity_identifier =
NULL;
if (graph_view == NULL ||
node_layout == NULL ||
node_layout->entity_record == NULL ||
graph_view->node_moved_callback == NULL)
{
return;
}
entity_identifier =
entity_record_get_identifier(
node_layout->entity_record
);
if (entity_identifier == NULL ||
entity_identifier[0] == '\0')
{
return;
}
graph_view->node_moved_callback(
entity_identifier,
node_layout->x,
node_layout->y,
graph_view->node_moved_user_data
);
}
/**
* @brief Termine le déplacement actif.
*/
@ -1067,6 +1110,12 @@ static void investigation_graph_view_on_drag_end(
double logical_y =
0.0;
InvestigationGraphNodeLayout *moved_node =
NULL;
gboolean node_was_moved =
FALSE;
(void) gesture;
if (graph_view == NULL ||
@ -1088,6 +1137,19 @@ static void investigation_graph_view_on_drag_end(
else if (graph_view->node_dragging &&
graph_view->dragged_node != NULL)
{
moved_node =
graph_view->dragged_node;
node_was_moved =
(
(offset_x * offset_x) +
(offset_y * offset_y)
) >
(
INVESTIGATION_GRAPH_VIEW_CLICK_TOLERANCE *
INVESTIGATION_GRAPH_VIEW_CLICK_TOLERANCE
);
pointer_x =
graph_view->drag_start_x +
offset_x;
@ -1118,6 +1180,15 @@ static void investigation_graph_view_on_drag_end(
graph_view
);
if (node_was_moved &&
moved_node != NULL)
{
investigation_graph_view_notify_node_moved(
graph_view,
moved_node
);
}
gtk_widget_queue_draw(
graph_view->drawing_area
);
@ -1276,7 +1347,9 @@ static GPtrArray *investigation_graph_view_list_active_entities(
*/
static GPtrArray *investigation_graph_view_create_layout(
const GPtrArray *active_entities,
int width
const InvestigationGraphLayout *graph_layout,
int width,
gboolean apply_persisted_positions
)
{
GPtrArray *node_layouts =
@ -1361,6 +1434,15 @@ static GPtrArray *investigation_graph_view_create_layout(
return NULL;
}
const char *entity_identifier =
NULL;
double persisted_x =
0.0;
double persisted_y =
0.0;
node_layout->entity_record =
g_ptr_array_index(
active_entities,
@ -1377,6 +1459,29 @@ static GPtrArray *investigation_graph_view_create_layout(
(INVESTIGATION_GRAPH_VIEW_NODE_HEIGHT +
INVESTIGATION_GRAPH_VIEW_VERTICAL_GAP));
entity_identifier =
entity_record_get_identifier(
node_layout->entity_record
);
if (apply_persisted_positions &&
graph_layout != NULL &&
entity_identifier != NULL &&
entity_identifier[0] != '\0' &&
investigation_graph_layout_get_position(
graph_layout,
entity_identifier,
&persisted_x,
&persisted_y
))
{
node_layout->x =
persisted_x;
node_layout->y =
persisted_y;
}
g_ptr_array_add(
node_layouts,
node_layout
@ -1540,6 +1645,7 @@ static void investigation_graph_view_set_layout_error(
static gboolean investigation_graph_view_build_layout(
InvestigationGraphView *graph_view,
int width,
gboolean apply_persisted_positions,
GError **error
)
{
@ -1597,7 +1703,9 @@ static gboolean investigation_graph_view_build_layout(
node_layouts =
investigation_graph_view_create_layout(
active_entities,
width
graph_view->graph_layout,
width,
apply_persisted_positions
);
if (node_layouts == NULL)
@ -3146,6 +3254,73 @@ GtkWidget *investigation_graph_view_get_widget(
return graph_view->drawing_area;
}
/**
* @brief Reconstruit la disposition privée du graphe affiché.
*
* Le zoom et le déplacement global du canvas sont conservés.
*/
static void investigation_graph_view_rebuild_current_layout(
InvestigationGraphView *graph_view,
gboolean apply_persisted_positions
)
{
GError *error =
NULL;
int width =
640;
if (graph_view == NULL ||
graph_view->graph_model == NULL)
{
return;
}
investigation_graph_view_clear_layout(
graph_view
);
if (graph_view->drawing_area != NULL)
{
width =
gtk_widget_get_width(
graph_view->drawing_area
);
if (width <= 0)
{
width =
640;
}
}
if (!investigation_graph_view_build_layout(
graph_view,
width,
apply_persisted_positions,
&error
))
{
graph_view->layout_error_message =
g_strdup(
error != NULL
? error->message
: "Impossible de reconstruire la disposition du graphe."
);
}
g_clear_error(
&error
);
if (graph_view->drawing_area != NULL)
{
gtk_widget_queue_draw(
graph_view->drawing_area
);
}
}
void investigation_graph_view_set_graph(
InvestigationGraphView *graph_view,
const InvestigationGraphModel *graph_model
@ -3195,6 +3370,7 @@ void investigation_graph_view_set_graph(
if (!investigation_graph_view_build_layout(
graph_view,
width,
TRUE,
&error
))
{
@ -3215,6 +3391,25 @@ void investigation_graph_view_set_graph(
);
}
void investigation_graph_view_set_layout(
InvestigationGraphView *graph_view,
const InvestigationGraphLayout *graph_layout
)
{
if (graph_view == NULL)
{
return;
}
graph_view->graph_layout =
graph_layout;
investigation_graph_view_rebuild_current_layout(
graph_view,
TRUE
);
}
void investigation_graph_view_clear(
InvestigationGraphView *graph_view
)
@ -3231,6 +3426,9 @@ void investigation_graph_view_clear(
graph_view->graph_model =
NULL;
graph_view->graph_layout =
NULL;
investigation_graph_view_reset_view(
graph_view
);
@ -3254,6 +3452,24 @@ void investigation_graph_view_set_selection_callback(
user_data;
}
void investigation_graph_view_set_node_moved_callback(
InvestigationGraphView *graph_view,
InvestigationGraphViewNodeMovedCallback callback,
gpointer user_data
)
{
if (graph_view == NULL)
{
return;
}
graph_view->node_moved_callback =
callback;
graph_view->node_moved_user_data =
user_data;
}
const EntityRecord *investigation_graph_view_get_selected_entity(
const InvestigationGraphView *graph_view
)
@ -3377,6 +3593,7 @@ void investigation_graph_view_reset_layout(
if (!investigation_graph_view_build_layout(
graph_view,
width,
FALSE,
&error
))
{
@ -3416,6 +3633,12 @@ void investigation_graph_view_free(
graph_view->selection_user_data =
NULL;
graph_view->node_moved_callback =
NULL;
graph_view->node_moved_user_data =
NULL;
investigation_graph_view_clear_layout(
graph_view
);
@ -3423,6 +3646,9 @@ void investigation_graph_view_free(
graph_view->graph_model =
NULL;
graph_view->graph_layout =
NULL;
if (graph_view->drawing_area != NULL)
{
gtk_drawing_area_set_draw_func(

View file

@ -77,6 +77,12 @@ struct Workspace
gpointer
verify_evidence_user_data;
WorkspaceGraphNodeMovedCallback
graph_node_moved_callback;
gpointer
graph_node_moved_user_data;
GtkWidget *node_name_label;
GtkWidget *node_path_label;
GtkWidget *node_type_label;
@ -84,6 +90,8 @@ struct Workspace
GtkWidget *node_children_label;
const InvestigationGraphModel *graph_model;
const InvestigationGraphLayout *graph_layout;
WorkspaceGraphState graph_state;
};
@ -254,6 +262,35 @@ static const char *workspace_get_integrity_status_text(
}
}
/**
* @brief Transmet la sélection du graphe au volet de détails.
*/
static void workspace_on_graph_node_moved(
const char *entity_identifier,
double x,
double y,
gpointer user_data
)
{
Workspace *workspace =
user_data;
if (workspace == NULL ||
workspace->graph_node_moved_callback == NULL ||
entity_identifier == NULL ||
entity_identifier[0] == '\0')
{
return;
}
workspace->graph_node_moved_callback(
entity_identifier,
x,
y,
workspace->graph_node_moved_user_data
);
}
/**
* @brief Transmet la sélection du graphe au volet de détails.
*/
@ -990,6 +1027,12 @@ Workspace *workspace_new(void)
workspace
);
investigation_graph_view_set_node_moved_callback(
workspace->graph_view,
workspace_on_graph_node_moved,
workspace
);
entity_details_panel_set_close_callback(
workspace->entity_details_panel,
workspace_on_entity_details_closed,
@ -1033,6 +1076,9 @@ Workspace *workspace_new(void)
workspace->graph_model =
NULL;
workspace->graph_layout =
NULL;
workspace->graph_state =
WORKSPACE_GRAPH_STATE_EMPTY;
@ -1406,6 +1452,9 @@ void workspace_set_graph_loading(
workspace->graph_model =
NULL;
workspace->graph_layout =
NULL;
workspace->graph_state =
WORKSPACE_GRAPH_STATE_LOADING;
@ -1441,7 +1490,8 @@ void workspace_set_graph_loading(
void workspace_set_graph(
Workspace *workspace,
const InvestigationGraphModel *graph_model
const InvestigationGraphModel *graph_model,
const InvestigationGraphLayout *graph_layout
)
{
if (workspace == NULL)
@ -1475,9 +1525,21 @@ void workspace_set_graph(
workspace->entity_details_panel
);
investigation_graph_view_clear(
workspace->graph_view
);
workspace->graph_model =
graph_model;
workspace->graph_layout =
graph_layout;
investigation_graph_view_set_layout(
workspace->graph_view,
graph_layout
);
investigation_graph_view_set_graph(
workspace->graph_view,
graph_model
@ -1550,6 +1612,9 @@ void workspace_set_graph_error(
workspace->graph_model =
NULL;
workspace->graph_layout =
NULL;
workspace->graph_state =
WORKSPACE_GRAPH_STATE_ERROR;
@ -1619,6 +1684,9 @@ void workspace_clear_graph(
workspace->graph_model =
NULL;
workspace->graph_layout =
NULL;
workspace->graph_state =
WORKSPACE_GRAPH_STATE_EMPTY;
@ -1670,6 +1738,24 @@ void workspace_set_verify_evidence_callback(
user_data;
}
void workspace_set_graph_node_moved_callback(
Workspace *workspace,
WorkspaceGraphNodeMovedCallback callback,
gpointer user_data
)
{
if (workspace == NULL)
{
return;
}
workspace->graph_node_moved_callback =
callback;
workspace->graph_node_moved_user_data =
user_data;
}
void workspace_free(Workspace *workspace)
{
if (workspace == NULL)
@ -1683,6 +1769,12 @@ void workspace_free(Workspace *workspace)
NULL
);
investigation_graph_view_set_node_moved_callback(
workspace->graph_view,
NULL,
NULL
);
entity_details_panel_set_close_callback(
workspace->entity_details_panel,
NULL,
@ -1714,12 +1806,21 @@ void workspace_free(Workspace *workspace)
workspace->graph_model =
NULL;
workspace->graph_layout =
NULL;
workspace->verify_evidence_callback =
NULL;
workspace->verify_evidence_user_data =
NULL;
workspace->graph_node_moved_callback =
NULL;
workspace->graph_node_moved_user_data =
NULL;
g_clear_pointer(
&workspace->selected_evidence_identifier,
g_free