Persister la position des nœuds du graphe
This commit is contained in:
parent
995939bef8
commit
c46039a2aa
22 changed files with 3223 additions and 17 deletions
3
Makefile
3
Makefile
|
|
@ -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 \
|
||||
|
|
|
|||
59
database/schema_current.sql
Normal file
59
database/schema_current.sql
Normal 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
|
||||
)
|
||||
);
|
||||
|
|
@ -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
|
||||
|
|
|
|||
133
include/dao/graph_node_position_dao.h
Normal file
133
include/dao/graph_node_position_dao.h
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
126
include/models/graph_node_position.h
Normal file
126
include/models/graph_node_position.h
Normal 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
|
||||
133
include/models/investigation_graph_layout.h
Normal file
133
include/models/investigation_graph_layout.h
Normal 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 été 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 été 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
|
||||
|
|
@ -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
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
793
src/dao/graph_node_position_dao.c
Normal file
793
src/dao/graph_node_position_dao.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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 d’annuler l’installation 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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
336
src/models/graph_node_position.c
Normal file
336
src/models/graph_node_position.c
Normal 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;
|
||||
}
|
||||
321
src/models/investigation_graph_layout.c
Normal file
321
src/models/investigation_graph_layout.c
Normal 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
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 été 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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue