fix(graph): restore viewport when reopening
This commit is contained in:
parent
f54af89e3e
commit
6b43379e75
14 changed files with 330 additions and 6 deletions
|
|
@ -1,7 +1,7 @@
|
|||
/******************************************************************************
|
||||
* Labfy Investigation
|
||||
*
|
||||
* Extensions idempotentes du schéma SQLite courant V6
|
||||
* Extensions idempotentes du schéma SQLite courant V8
|
||||
******************************************************************************/
|
||||
|
||||
/*
|
||||
|
|
@ -93,6 +93,15 @@ CREATE TABLE IF NOT EXISTS graph_layout_positions
|
|||
CHECK (length(updated_at) = 20)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS graph_viewport
|
||||
(
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
zoom REAL NOT NULL CHECK (zoom = zoom AND zoom > 0),
|
||||
offset_x REAL NOT NULL CHECK (offset_x = offset_x),
|
||||
offset_y REAL NOT NULL CHECK (offset_y = offset_y),
|
||||
updated_at TEXT NOT NULL CHECK (length(updated_at) = 20)
|
||||
);
|
||||
|
||||
/* Migration idempotente des positions d'entités déjà enregistrées. */
|
||||
INSERT OR IGNORE INTO graph_layout_positions(node_id, x, y, updated_at)
|
||||
SELECT entity_id, x, y, updated_at
|
||||
|
|
|
|||
8
database/schema_v8.sql
Normal file
8
database/schema_v8.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE IF NOT EXISTS graph_viewport
|
||||
(
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
zoom REAL NOT NULL CHECK (zoom = zoom AND zoom > 0),
|
||||
offset_x REAL NOT NULL CHECK (offset_x = offset_x),
|
||||
offset_y REAL NOT NULL CHECK (offset_y = offset_y),
|
||||
updated_at TEXT NOT NULL CHECK (length(updated_at) = 20)
|
||||
);
|
||||
|
|
@ -2269,6 +2269,12 @@ Cette stratégie préserve les positions existantes tout en permettant aux
|
|||
nœuds de relation de suivre exactement le même cycle de chargement,
|
||||
d'enregistrement et de réinitialisation que les nœuds d'entité.
|
||||
|
||||
Le cadrage global est stocké séparément dans la ligne unique de
|
||||
`graph_viewport`. Elle conserve le niveau de zoom et les décalages horizontal
|
||||
et vertical du canevas. Ces valeurs sont enregistrées à la fermeture de
|
||||
l'enquête et restaurées lors de son premier chargement, afin que la vue
|
||||
réapparaisse exactement à l'endroit où l'utilisateur l'avait laissée.
|
||||
|
||||
---
|
||||
|
||||
# 12. Conclusion
|
||||
|
|
|
|||
|
|
@ -143,6 +143,23 @@ gboolean graph_node_position_dao_delete_all(
|
|||
GError **error
|
||||
);
|
||||
|
||||
gboolean graph_node_position_dao_load_viewport(
|
||||
GraphNodePositionDao *position_dao,
|
||||
double *zoom,
|
||||
double *offset_x,
|
||||
double *offset_y,
|
||||
gboolean *found,
|
||||
GError **error
|
||||
);
|
||||
|
||||
gboolean graph_node_position_dao_upsert_viewport(
|
||||
GraphNodePositionDao *position_dao,
|
||||
double zoom,
|
||||
double offset_x,
|
||||
double offset_y,
|
||||
GError **error
|
||||
);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ bool schema_install_v5(Database *database);
|
|||
bool schema_install_v6(Database *database);
|
||||
|
||||
bool schema_install_v7(Database *database);
|
||||
bool schema_install_v8(Database *database);
|
||||
|
||||
/**
|
||||
* @brief Garantit la présence des extensions du schéma courant V2.
|
||||
|
|
|
|||
|
|
@ -526,6 +526,11 @@ void main_window_set_graph(
|
|||
const InvestigationGraphLayout *graph_layout
|
||||
);
|
||||
|
||||
gboolean main_window_get_graph_view_transform(
|
||||
MainWindow *main_window, double *zoom, double *offset_x, double *offset_y);
|
||||
void main_window_set_graph_view_transform(
|
||||
MainWindow *main_window, double zoom, double offset_x, double offset_y);
|
||||
|
||||
/** @brief Sélectionne une relation dans le graphe affiché. */
|
||||
gboolean main_window_select_graph_relation(
|
||||
MainWindow *main_window,
|
||||
|
|
|
|||
|
|
@ -447,6 +447,11 @@ void workspace_set_graph(
|
|||
const InvestigationGraphLayout *graph_layout
|
||||
);
|
||||
|
||||
gboolean workspace_get_graph_view_transform(
|
||||
Workspace *workspace, double *zoom, double *offset_x, double *offset_y);
|
||||
void workspace_set_graph_view_transform(
|
||||
Workspace *workspace, double zoom, double offset_x, double offset_y);
|
||||
|
||||
/**
|
||||
* @brief Affiche une erreur de chargement du graphe.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ struct Application
|
|||
InvestigationGraphModel *graph_model;
|
||||
InvestigationGraphLayout *graph_layout;
|
||||
guint64 graph_load_generation;
|
||||
gboolean graph_viewport_restored;
|
||||
|
||||
char *selected_evidence_identifier;
|
||||
char *pending_relation_selection_identifier;
|
||||
|
|
@ -252,6 +253,59 @@ static void application_start_graph_loading(
|
|||
const char *database_path
|
||||
);
|
||||
|
||||
static void application_save_graph_viewport(Application *application)
|
||||
{
|
||||
GraphNodePositionDao *dao = NULL;
|
||||
GError *error = NULL;
|
||||
double zoom = 0.0;
|
||||
double offset_x = 0.0;
|
||||
double offset_y = 0.0;
|
||||
|
||||
if (application == NULL || application->session == NULL ||
|
||||
application->main_window == NULL ||
|
||||
!main_window_get_graph_view_transform(application->main_window,
|
||||
&zoom, &offset_x, &offset_y))
|
||||
return;
|
||||
|
||||
dao = graph_node_position_dao_new(
|
||||
investigation_session_get_database(application->session), &error);
|
||||
if (dao == NULL || !graph_node_position_dao_upsert_viewport(
|
||||
dao, zoom, offset_x, offset_y, &error))
|
||||
g_warning("Impossible d'enregistrer le cadrage du graphe : %s",
|
||||
error != NULL ? error->message : "erreur inconnue");
|
||||
|
||||
graph_node_position_dao_free(dao);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
|
||||
static void application_restore_graph_viewport(Application *application)
|
||||
{
|
||||
GraphNodePositionDao *dao = NULL;
|
||||
GError *error = NULL;
|
||||
gboolean found = FALSE;
|
||||
double zoom = 0.0;
|
||||
double offset_x = 0.0;
|
||||
double offset_y = 0.0;
|
||||
|
||||
if (application == NULL || application->graph_viewport_restored ||
|
||||
application->session == NULL || application->main_window == NULL)
|
||||
return;
|
||||
|
||||
dao = graph_node_position_dao_new(
|
||||
investigation_session_get_database(application->session), &error);
|
||||
if (dao != NULL && graph_node_position_dao_load_viewport(
|
||||
dao, &zoom, &offset_x, &offset_y, &found, &error) && found)
|
||||
main_window_set_graph_view_transform(
|
||||
application->main_window, zoom, offset_x, offset_y);
|
||||
else if (error != NULL)
|
||||
g_warning("Impossible de restaurer le cadrage du graphe : %s",
|
||||
error->message);
|
||||
|
||||
application->graph_viewport_restored = TRUE;
|
||||
graph_node_position_dao_free(dao);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
|
||||
static void application_present_error(
|
||||
Application *application,
|
||||
const char *title,
|
||||
|
|
@ -1068,6 +1122,8 @@ static void application_on_graph_loaded(
|
|||
application->graph_layout
|
||||
);
|
||||
|
||||
application_restore_graph_viewport(application);
|
||||
|
||||
if (application->pending_relation_selection_identifier != NULL)
|
||||
{
|
||||
main_window_select_graph_relation(application->main_window,
|
||||
|
|
@ -2609,12 +2665,14 @@ static gboolean application_install_session(
|
|||
application->tree_model
|
||||
);
|
||||
|
||||
application_save_graph_viewport(application);
|
||||
investigation_session_close(
|
||||
application->session
|
||||
);
|
||||
|
||||
application->tree_model = new_tree_model;
|
||||
application->session = new_session;
|
||||
application->graph_viewport_restored = FALSE;
|
||||
|
||||
main_window_set_tree_model(
|
||||
application->main_window,
|
||||
|
|
@ -8068,6 +8126,8 @@ void application_free(
|
|||
application
|
||||
);
|
||||
|
||||
application_save_graph_viewport(application);
|
||||
|
||||
application_clear_graph(
|
||||
application
|
||||
);
|
||||
|
|
|
|||
|
|
@ -71,6 +71,16 @@ static const char *const graph_node_position_dao_delete_sql =
|
|||
static const char *const graph_node_position_dao_delete_all_sql =
|
||||
"DELETE FROM graph_layout_positions;";
|
||||
|
||||
static const char *const graph_node_position_dao_load_viewport_sql =
|
||||
"SELECT zoom, offset_x, offset_y FROM graph_viewport WHERE id = 1;";
|
||||
|
||||
static const char *const graph_node_position_dao_upsert_viewport_sql =
|
||||
"INSERT INTO graph_viewport(id, zoom, offset_x, offset_y, updated_at) "
|
||||
"VALUES(1, ?, ?, ?, ?) "
|
||||
"ON CONFLICT(id) DO UPDATE SET zoom = excluded.zoom, "
|
||||
"offset_x = excluded.offset_x, offset_y = excluded.offset_y, "
|
||||
"updated_at = excluded.updated_at;";
|
||||
|
||||
/**
|
||||
* @brief Enregistre une erreur littérale.
|
||||
*/
|
||||
|
|
@ -869,3 +879,125 @@ cleanup:
|
|||
|
||||
return success;
|
||||
}
|
||||
|
||||
gboolean graph_node_position_dao_load_viewport(
|
||||
GraphNodePositionDao *position_dao,
|
||||
double *zoom,
|
||||
double *offset_x,
|
||||
double *offset_y,
|
||||
gboolean *found,
|
||||
GError **error
|
||||
)
|
||||
{
|
||||
DatabaseStatement *statement = NULL;
|
||||
DatabaseStatementStepResult step_result;
|
||||
gboolean success = FALSE;
|
||||
|
||||
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||
if (position_dao == NULL || position_dao->database == NULL ||
|
||||
zoom == NULL || offset_x == NULL || offset_y == NULL || found == NULL)
|
||||
{
|
||||
graph_node_position_dao_set_error_literal(error,
|
||||
GRAPH_NODE_POSITION_DAO_ERROR_INVALID_ARGUMENT,
|
||||
"Les paramètres de chargement du cadrage sont invalides.");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
*found = FALSE;
|
||||
statement = database_statement_prepare(position_dao->database,
|
||||
graph_node_position_dao_load_viewport_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 du cadrage");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
step_result = database_statement_step(statement);
|
||||
if (step_result == DATABASE_STATEMENT_STEP_ROW)
|
||||
{
|
||||
if (!database_statement_column_double(statement, 0, zoom) ||
|
||||
!database_statement_column_double(statement, 1, offset_x) ||
|
||||
!database_statement_column_double(statement, 2, offset_y) ||
|
||||
!isfinite(*zoom) || *zoom <= 0.0 ||
|
||||
!isfinite(*offset_x) || !isfinite(*offset_y))
|
||||
{
|
||||
graph_node_position_dao_set_error_literal(error,
|
||||
GRAPH_NODE_POSITION_DAO_ERROR_READ,
|
||||
"Le cadrage enregistré est invalide.");
|
||||
goto cleanup;
|
||||
}
|
||||
*found = TRUE;
|
||||
}
|
||||
else if (step_result == DATABASE_STATEMENT_STEP_ERROR)
|
||||
{
|
||||
graph_node_position_dao_set_database_error(position_dao, error,
|
||||
GRAPH_NODE_POSITION_DAO_ERROR_EXECUTE,
|
||||
"Impossible de charger le cadrage");
|
||||
goto cleanup;
|
||||
}
|
||||
success = TRUE;
|
||||
|
||||
cleanup:
|
||||
database_statement_finalize(statement);
|
||||
return success;
|
||||
}
|
||||
|
||||
gboolean graph_node_position_dao_upsert_viewport(
|
||||
GraphNodePositionDao *position_dao,
|
||||
double zoom,
|
||||
double offset_x,
|
||||
double offset_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 ||
|
||||
!isfinite(zoom) || zoom <= 0.0 ||
|
||||
!isfinite(offset_x) || !isfinite(offset_y))
|
||||
{
|
||||
graph_node_position_dao_set_error_literal(error,
|
||||
GRAPH_NODE_POSITION_DAO_ERROR_INVALID_ARGUMENT,
|
||||
"Le cadrage du graphe est invalide.");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
updated_at = graph_node_position_dao_create_utc_timestamp();
|
||||
statement = database_statement_prepare(position_dao->database,
|
||||
graph_node_position_dao_upsert_viewport_sql);
|
||||
if (updated_at == NULL || statement == NULL)
|
||||
{
|
||||
graph_node_position_dao_set_database_error(position_dao, error,
|
||||
GRAPH_NODE_POSITION_DAO_ERROR_PREPARE,
|
||||
"Impossible de préparer l'enregistrement du cadrage");
|
||||
goto cleanup;
|
||||
}
|
||||
if (!database_statement_bind_double(statement, 1, zoom) ||
|
||||
!database_statement_bind_double(statement, 2, offset_x) ||
|
||||
!database_statement_bind_double(statement, 3, offset_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 du cadrage");
|
||||
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 d'enregistrer le cadrage");
|
||||
goto cleanup;
|
||||
}
|
||||
success = TRUE;
|
||||
|
||||
cleanup:
|
||||
database_statement_finalize(statement);
|
||||
g_free(updated_at);
|
||||
return success;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@
|
|||
/**
|
||||
* @brief Version actuelle du schéma SQLite.
|
||||
*/
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT 7
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT 8
|
||||
|
||||
/**
|
||||
* @brief Version actuelle sous forme textuelle pour metadata.
|
||||
*/
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "7"
|
||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "8"
|
||||
|
||||
/**
|
||||
* @brief Nom de l'application enregistré dans les métadonnées.
|
||||
|
|
@ -748,6 +748,40 @@ rollback:
|
|||
return false;
|
||||
}
|
||||
|
||||
static bool database_migrate_v6_to_v7(Database *database)
|
||||
{
|
||||
bool transaction_started = false;
|
||||
if (database == NULL || !database_transaction_begin(database))
|
||||
return false;
|
||||
transaction_started = true;
|
||||
if (!schema_install_v7(database) ||
|
||||
!database_update_schema_version(database, "7") ||
|
||||
!database_transaction_commit(database))
|
||||
goto rollback;
|
||||
return true;
|
||||
rollback:
|
||||
if (transaction_started && !database_transaction_rollback(database))
|
||||
g_warning("Impossible d’annuler la migration SQLite V6 vers V7.");
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool database_migrate_v7_to_v8(Database *database)
|
||||
{
|
||||
bool transaction_started = false;
|
||||
if (database == NULL || !database_transaction_begin(database))
|
||||
return false;
|
||||
transaction_started = true;
|
||||
if (!schema_install_v8(database) ||
|
||||
!database_update_schema_version(database, "8") ||
|
||||
!database_transaction_commit(database))
|
||||
goto rollback;
|
||||
return true;
|
||||
rollback:
|
||||
if (transaction_started && !database_transaction_rollback(database))
|
||||
g_warning("Impossible d’annuler la migration SQLite V7 vers V8.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
||||
*/
|
||||
|
|
@ -959,11 +993,17 @@ bool database_migrate_to_latest(
|
|||
break;
|
||||
|
||||
case 6:
|
||||
if (!schema_install_v7(database))
|
||||
if (!database_migrate_v6_to_v7(database))
|
||||
return false;
|
||||
schema_version = 7;
|
||||
break;
|
||||
|
||||
case 7:
|
||||
if (!database_migrate_v7_to_v8(database))
|
||||
return false;
|
||||
schema_version = 8;
|
||||
break;
|
||||
|
||||
default:
|
||||
database_set_error(
|
||||
database,
|
||||
|
|
|
|||
|
|
@ -233,6 +233,12 @@ bool schema_install_v7(Database *database)
|
|||
"la migration SQLite V7");
|
||||
}
|
||||
|
||||
bool schema_install_v8(Database *database)
|
||||
{
|
||||
return schema_execute_file(database, "database/schema_v8.sql",
|
||||
"la migration SQLite V8");
|
||||
}
|
||||
|
||||
bool schema_ensure_current(
|
||||
Database *database
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1421,6 +1421,22 @@ void main_window_set_graph(
|
|||
);
|
||||
}
|
||||
|
||||
gboolean main_window_get_graph_view_transform(
|
||||
MainWindow *main_window, double *zoom, double *offset_x, double *offset_y)
|
||||
{
|
||||
return main_window != NULL &&
|
||||
workspace_get_graph_view_transform(
|
||||
main_window->workspace, zoom, offset_x, offset_y);
|
||||
}
|
||||
|
||||
void main_window_set_graph_view_transform(
|
||||
MainWindow *main_window, double zoom, double offset_x, double offset_y)
|
||||
{
|
||||
if (main_window != NULL)
|
||||
workspace_set_graph_view_transform(
|
||||
main_window->workspace, zoom, offset_x, offset_y);
|
||||
}
|
||||
|
||||
gboolean main_window_select_graph_relation(MainWindow *main_window,
|
||||
const char *relation_identifier)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3229,6 +3229,24 @@ void workspace_set_graph(
|
|||
);
|
||||
}
|
||||
|
||||
gboolean workspace_get_graph_view_transform(
|
||||
Workspace *workspace, double *zoom, double *offset_x, double *offset_y)
|
||||
{
|
||||
if (workspace == NULL || workspace->graph_state != WORKSPACE_GRAPH_STATE_READY)
|
||||
return FALSE;
|
||||
return investigation_graph_view_get_view_transform(
|
||||
workspace->graph_view, zoom, offset_x, offset_y);
|
||||
}
|
||||
|
||||
void workspace_set_graph_view_transform(
|
||||
Workspace *workspace, double zoom, double offset_x, double offset_y)
|
||||
{
|
||||
if (workspace == NULL || workspace->graph_state != WORKSPACE_GRAPH_STATE_READY)
|
||||
return;
|
||||
investigation_graph_view_set_view_transform(
|
||||
workspace->graph_view, zoom, offset_x, offset_y);
|
||||
}
|
||||
|
||||
void workspace_set_graph_error(
|
||||
Workspace *workspace,
|
||||
const char *message
|
||||
|
|
|
|||
|
|
@ -525,7 +525,8 @@ static void test_database_initialize_valid_database(void)
|
|||
"FROM investigation;"
|
||||
);
|
||||
|
||||
assert(strcmp(schema_version, "7") == 0);
|
||||
assert(strcmp(schema_version, "8") == 0);
|
||||
test_database_assert_table_exists(database, "graph_viewport");
|
||||
test_database_assert_table_exists(database, "osint_executions");
|
||||
test_database_assert_table_exists(database, "osint_execution_entities");
|
||||
test_database_assert_table_exists(database, "osint_execution_relations");
|
||||
|
|
@ -989,7 +990,7 @@ static void test_database_migrate_v1_to_v2(void)
|
|||
assert(
|
||||
strcmp(
|
||||
schema_version,
|
||||
"6"
|
||||
"8"
|
||||
) == 0
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue