Ajouter le dialogue de création de relation

This commit is contained in:
grayTerminal-sh 2026-07-22 08:59:57 +02:00
parent ff512593da
commit b1d0ec2a1e
9 changed files with 2422 additions and 0 deletions

View file

@ -0,0 +1,160 @@
/******************************************************************************
* @file create_relation_dialog.h
* @brief Dialogue GTK de préparation d'une relation entre deux entités.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_CREATE_RELATION_DIALOG_H
#define LABFY_INVESTIGATION_CREATE_RELATION_DIALOG_H
#include <gtk/gtk.h>
G_BEGIN_DECLS
/**
* @brief Résultat opaque produit par le dialogue.
*/
typedef struct CreateRelationDialogResult CreateRelationDialogResult;
/**
* @brief Codes d'erreur produits par CreateRelationDialog.
*/
typedef enum
{
CREATE_RELATION_DIALOG_ERROR_INVALID_ARGUMENT,
CREATE_RELATION_DIALOG_ERROR_EMPTY_TARGETS,
CREATE_RELATION_DIALOG_ERROR_MEMORY,
CREATE_RELATION_DIALOG_ERROR_INVALID_DATA
} CreateRelationDialogError;
/**
* @brief Domaine d'erreur du dialogue.
*/
#define CREATE_RELATION_DIALOG_ERROR \
create_relation_dialog_error_quark()
/**
* @brief Callback appelé après validation ou annulation.
*
* En cas de validation, result appartient au callback et doit être libéré
* avec create_relation_dialog_result_free().
*
* En cas d'annulation, result vaut NULL.
*
* @param result Résultat possédé par le callback, ou NULL.
* @param user_data Données privées fournies lors de l'ouverture.
*/
typedef void (*CreateRelationDialogCallback)(
CreateRelationDialogResult *result,
gpointer user_data
);
/**
* @brief Retourne le domaine d'erreur du dialogue.
*/
GQuark create_relation_dialog_error_quark(void);
/**
* @brief Présente le dialogue de création d'une relation.
*
* entities doit contenir des pointeurs EntityRecord valides. Le dialogue
* copie tous les identifiants et libellés nécessaires et ne conserve aucun
* pointeur vers les modèles fournis.
*
* L'entité source est retirée de la liste des cibles.
*
* Cette fonction ne crée aucune relation et n'accède pas à SQLite.
*
* @param parent Fenêtre GTK parente.
* @param source_entity_identifier UUID de l'entité source.
* @param entities Tableau de EntityRecord.
* @param callback Fonction appelée après validation ou annulation.
* @param user_data Données privées transmises au callback.
* @param error Emplacement facultatif recevant une erreur immédiate.
*
* @return TRUE si le dialogue a é créé et présenté.
*/
gboolean create_relation_dialog_present(
GtkWindow *parent,
const char *source_entity_identifier,
const GPtrArray *entities,
CreateRelationDialogCallback callback,
gpointer user_data,
GError **error
);
/**
* @brief Valide les valeurs obligatoires d'une relation.
*
* Cette fonction est indépendante de l'affichage et pourra être testée
* sans ouvrir de fenêtre GTK.
*
* @param source_entity_identifier UUID de l'entité source.
* @param target_entity_identifier UUID de l'entité cible.
* @param relation_type Type de relation non vide.
* @param confidence Confiance comprise entre 0 et 100.
* @param error Emplacement facultatif recevant une erreur.
*
* @return TRUE lorsque les valeurs sont valides.
*/
gboolean create_relation_dialog_validate_values(
const char *source_entity_identifier,
const char *target_entity_identifier,
const char *relation_type,
gint confidence,
GError **error
);
/**
* @brief Libère un résultat.
*
* Cette fonction accepte NULL.
*/
void create_relation_dialog_result_free(
CreateRelationDialogResult *result
);
/**
* @brief Retourne l'UUID de l'entité source.
*/
const char *create_relation_dialog_result_get_source_identifier(
const CreateRelationDialogResult *result
);
/**
* @brief Retourne l'UUID de l'entité cible.
*/
const char *create_relation_dialog_result_get_target_identifier(
const CreateRelationDialogResult *result
);
/**
* @brief Retourne le type de relation.
*/
const char *create_relation_dialog_result_get_relation_type(
const CreateRelationDialogResult *result
);
/**
* @brief Retourne le libellé facultatif.
*/
const char *create_relation_dialog_result_get_label(
const CreateRelationDialogResult *result
);
/**
* @brief Retourne la justification facultative.
*/
const char *create_relation_dialog_result_get_justification(
const CreateRelationDialogResult *result
);
/**
* @brief Retourne le niveau de confiance.
*/
gint create_relation_dialog_result_get_confidence(
const CreateRelationDialogResult *result
);
G_END_DECLS
#endif

View file

@ -109,6 +109,17 @@ typedef void (*MainWindowResetGraphLayoutCallback)(
gpointer user_data gpointer user_data
); );
/**
* @brief Callback appelé lors d'une demande d'ajout d'une relation.
*
* @param source_entity_identifier UUID de l'entité source.
* @param user_data Données privées du callback.
*/
typedef void (*MainWindowAddRelationCallback)(
const char *source_entity_identifier,
gpointer user_data
);
/** /**
* @brief Définit le callback de vérification d'une preuve. * @brief Définit le callback de vérification d'une preuve.
* *
@ -154,6 +165,21 @@ void main_window_set_reset_graph_layout_callback(
gpointer user_data gpointer user_data
); );
/**
* @brief Définit le callback de demande d'ajout d'une relation.
*
* MainWindow relaie uniquement la demande 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_add_relation_callback(
MainWindow *main_window,
MainWindowAddRelationCallback callback,
gpointer user_data
);
/** /**
* @brief Définit le callback du bouton « Nouvelle enquête ». * @brief Définit le callback du bouton « Nouvelle enquête ».
* *

View file

@ -32,6 +32,19 @@ typedef void (*EntityDetailsPanelCloseCallback)(
gpointer user_data gpointer user_data
); );
/**
* @brief Callback appelé lorsque l'utilisateur demande une relation.
*
* L'identifiant est emprunté et uniquement valide pendant l'appel.
*
* @param source_entity_identifier UUID de l'entité source.
* @param user_data Données empruntées fournies par l'appelant.
*/
typedef void (*EntityDetailsPanelAddRelationCallback)(
const char *source_entity_identifier,
gpointer user_data
);
/** /**
* @brief Crée un volet de détails fermé. * @brief Crée un volet de détails fermé.
* *
@ -92,6 +105,21 @@ void entity_details_panel_set_close_callback(
gpointer user_data gpointer user_data
); );
/**
* @brief Définit le callback du bouton d'ajout d'une relation.
*
* Le volet transmet uniquement l'UUID de l'entité actuellement affichée.
*
* @param details_panel Volet à configurer.
* @param callback Callback à appeler, ou NULL.
* @param user_data Données empruntées du callback.
*/
void entity_details_panel_set_add_relation_callback(
EntityDetailsPanel *details_panel,
EntityDetailsPanelAddRelationCallback callback,
gpointer user_data
);
/** /**
* @brief Indique si le volet est actuellement ouvert. * @brief Indique si le volet est actuellement ouvert.
* *

View file

@ -66,6 +66,17 @@ typedef void (*WorkspaceResetGraphLayoutCallback)(
gpointer user_data gpointer user_data
); );
/**
* @brief Callback appelé lors d'une demande d'ajout d'une relation.
*
* @param source_entity_identifier UUID de l'entité source.
* @param user_data Données empruntées fournies par l'appelant.
*/
typedef void (*WorkspaceAddRelationCallback)(
const char *source_entity_identifier,
gpointer user_data
);
/** /**
* @brief Crée une nouvelle zone de travail. * @brief Crée une nouvelle zone de travail.
* *
@ -161,6 +172,21 @@ void workspace_set_reset_graph_layout_callback(
gpointer user_data gpointer user_data
); );
/**
* @brief Définit le callback de demande d'ajout d'une relation.
*
* Le Workspace relaie uniquement l'événement du volet de détails.
*
* @param workspace Zone de travail.
* @param callback Callback facultatif.
* @param user_data Données empruntées transmises au callback.
*/
void workspace_set_add_relation_callback(
Workspace *workspace,
WorkspaceAddRelationCallback callback,
gpointer user_data
);
/** /**
* @brief Affiche l'état de chargement du graphe. * @brief Affiche l'état de chargement du graphe.
* *

View file

@ -27,7 +27,9 @@
#include "dao/evidence_type_dao.h" #include "dao/evidence_type_dao.h"
#include "dao/graph_node_position_dao.h" #include "dao/graph_node_position_dao.h"
#include "views/evidence_import_dialog.h" #include "views/evidence_import_dialog.h"
#include "views/create_relation_dialog.h"
#include "dao/evidence_dao.h" #include "dao/evidence_dao.h"
#include "dao/entity_dao.h"
#include "models/evidence_type.h" #include "models/evidence_type.h"
#include "widgets/evidence_list_model.h" #include "widgets/evidence_list_model.h"
#include "widgets/evidence_category_model.h" #include "widgets/evidence_category_model.h"
@ -3324,6 +3326,239 @@ static void application_on_evidence_selected(
); );
} }
/**
* @brief Traite la validation ou l'annulation du dialogue de relation.
*
* Cette première étape valide le parcours complet de l'interface.
* L'écriture SQLite sera branchée après validation de la compilation.
*/
static void application_on_create_relation_completed(
CreateRelationDialogResult *result,
gpointer user_data
)
{
Application *application =
user_data;
const char *source_identifier =
NULL;
const char *target_identifier =
NULL;
const char *relation_type =
NULL;
char *status_message =
NULL;
if (application == NULL ||
application->main_window == NULL)
{
create_relation_dialog_result_free(
result
);
return;
}
if (result == NULL)
{
main_window_set_status(
application->main_window,
"Ajout de relation annulé."
);
return;
}
source_identifier =
create_relation_dialog_result_get_source_identifier(
result
);
target_identifier =
create_relation_dialog_result_get_target_identifier(
result
);
relation_type =
create_relation_dialog_result_get_relation_type(
result
);
g_message(
"Relation préparée : %s -> %s (%s), confiance %d %%.",
source_identifier != NULL
? source_identifier
: "(source absente)",
target_identifier != NULL
? target_identifier
: "(cible absente)",
relation_type != NULL
? relation_type
: "(type absent)",
create_relation_dialog_result_get_confidence(
result
)
);
status_message =
g_strdup_printf(
"Relation préparée : %s. "
"L'enregistrement SQLite sera branché à l'étape suivante.",
relation_type != NULL &&
relation_type[0] != '\0'
? relation_type
: "(type inconnu)"
);
main_window_set_status(
application->main_window,
status_message != NULL
? status_message
: "Relation préparée."
);
g_free(
status_message
);
create_relation_dialog_result_free(
result
);
}
/**
* @brief Ouvre le dialogue de création depuis la fiche d'une entité.
*/
static void application_on_add_relation_requested(
const char *source_entity_identifier,
gpointer user_data
)
{
Application *application =
user_data;
Database *database =
NULL;
EntityDao *entity_dao =
NULL;
GPtrArray *entities =
NULL;
GError *error =
NULL;
if (application == NULL ||
application->main_window == NULL ||
application->session == NULL ||
source_entity_identifier == NULL ||
!g_uuid_string_is_valid(
source_entity_identifier
))
{
return;
}
database =
investigation_session_get_database(
application->session
);
if (database == NULL)
{
application_present_error(
application,
"Relations indisponibles",
"La session ne fournit aucune connexion SQLite."
);
return;
}
entity_dao =
entity_dao_new(
database,
&error
);
if (entity_dao == NULL)
{
application_present_error(
application,
"Relations indisponibles",
error != NULL
? error->message
: "Impossible d'accéder aux entités."
);
g_clear_error(
&error
);
return;
}
entities =
entity_dao_list_all(
entity_dao,
&error
);
entity_dao_free(
entity_dao
);
if (entities == NULL)
{
application_present_error(
application,
"Relations indisponibles",
error != NULL
? error->message
: "Impossible de charger les entités."
);
g_clear_error(
&error
);
return;
}
if (!create_relation_dialog_present(
main_window_get_window(
application->main_window
),
source_entity_identifier,
entities,
application_on_create_relation_completed,
application,
&error
))
{
application_present_error(
application,
"Ajout de relation impossible",
error != NULL
? error->message
: "Le dialogue de relation n'a pas pu être ouvert."
);
g_clear_error(
&error
);
}
g_ptr_array_unref(
entities
);
}
/** /**
* @brief Enregistre le statut d'intégrité dans l'enquête d'origine. * @brief Enregistre le statut d'intégrité dans l'enquête d'origine.
* *
@ -4600,6 +4835,12 @@ static void application_on_activate(
application application
); );
main_window_set_add_relation_callback(
application->main_window,
application_on_add_relation_requested,
application
);
main_window_set_new_investigation_callback( main_window_set_new_investigation_callback(
application->main_window, application->main_window,
application_on_new_investigation_requested, application_on_new_investigation_requested,

File diff suppressed because it is too large Load diff

View file

@ -111,6 +111,12 @@ struct MainWindow
gpointer gpointer
reset_graph_layout_user_data; reset_graph_layout_user_data;
MainWindowAddRelationCallback
add_relation_callback;
gpointer
add_relation_user_data;
MainWindowQuitCallback MainWindowQuitCallback
quit_callback; quit_callback;
@ -348,6 +354,33 @@ static void main_window_on_reset_graph_layout_requested(
); );
} }
/**
* @brief Relaie la demande d'ajout d'une relation.
*/
static void main_window_on_add_relation_requested(
const char *source_entity_identifier,
gpointer user_data
)
{
MainWindow *main_window =
user_data;
if (main_window == NULL ||
main_window->add_relation_callback == NULL ||
source_entity_identifier == NULL ||
!g_uuid_string_is_valid(
source_entity_identifier
))
{
return;
}
main_window->add_relation_callback(
source_entity_identifier,
main_window->add_relation_user_data
);
}
/** /**
* @brief Relaie la demande de vérification provenant du Workspace. * @brief Relaie la demande de vérification provenant du Workspace.
*/ */
@ -648,6 +681,12 @@ MainWindow *main_window_new(
main_window main_window
); );
workspace_set_add_relation_callback(
main_window->workspace,
main_window_on_add_relation_requested,
main_window
);
workspace_widget = workspace_get_widget( workspace_widget = workspace_get_widget(
main_window->workspace main_window->workspace
); );
@ -1281,6 +1320,24 @@ void main_window_set_reset_graph_layout_callback(
user_data; user_data;
} }
void main_window_set_add_relation_callback(
MainWindow *main_window,
MainWindowAddRelationCallback callback,
gpointer user_data
)
{
if (main_window == NULL)
{
return;
}
main_window->add_relation_callback =
callback;
main_window->add_relation_user_data =
user_data;
}
void main_window_set_quit_callback( void main_window_set_quit_callback(
MainWindow *main_window, MainWindow *main_window,
MainWindowQuitCallback callback, MainWindowQuitCallback callback,
@ -1409,6 +1466,12 @@ void main_window_free(
NULL NULL
); );
workspace_set_add_relation_callback(
main_window->workspace,
NULL,
NULL
);
main_window->graph_node_moved_callback = main_window->graph_node_moved_callback =
NULL; NULL;
@ -1421,6 +1484,12 @@ void main_window_free(
main_window->reset_graph_layout_user_data = main_window->reset_graph_layout_user_data =
NULL; NULL;
main_window->add_relation_callback =
NULL;
main_window->add_relation_user_data =
NULL;
main_window->show_graph_callback = main_window->show_graph_callback =
NULL; NULL;

View file

@ -17,6 +17,7 @@ struct EntityDetailsPanel
{ {
GtkWidget *root_revealer; GtkWidget *root_revealer;
GtkWidget *frame; GtkWidget *frame;
GtkWidget *add_relation_button;
GtkWidget *close_button; GtkWidget *close_button;
GtkWidget *entity_title_label; GtkWidget *entity_title_label;
@ -29,8 +30,16 @@ struct EntityDetailsPanel
GtkWidget *entity_updated_at_label; GtkWidget *entity_updated_at_label;
GtkWidget *entity_identifier_label; GtkWidget *entity_identifier_label;
char *selected_entity_identifier;
EntityDetailsPanelCloseCallback close_callback; EntityDetailsPanelCloseCallback close_callback;
gpointer close_user_data; gpointer close_user_data;
EntityDetailsPanelAddRelationCallback
add_relation_callback;
gpointer
add_relation_user_data;
}; };
/** /**
@ -191,6 +200,64 @@ static const char *entity_details_panel_get_status_text(
} }
} }
/**
* @brief Met à jour l'état du bouton d'ajout d'une relation.
*/
static void entity_details_panel_update_add_relation_button(
EntityDetailsPanel *details_panel
)
{
gboolean enabled =
FALSE;
if (details_panel == NULL ||
details_panel->add_relation_button == NULL)
{
return;
}
enabled =
details_panel->add_relation_callback != NULL &&
details_panel->selected_entity_identifier != NULL &&
g_uuid_string_is_valid(
details_panel->selected_entity_identifier
);
gtk_widget_set_sensitive(
details_panel->add_relation_button,
enabled
);
}
/**
* @brief Transmet la demande d'ajout d'une relation.
*/
static void entity_details_panel_on_add_relation_clicked(
GtkButton *button,
gpointer user_data
)
{
EntityDetailsPanel *details_panel =
user_data;
(void) button;
if (details_panel == NULL ||
details_panel->add_relation_callback == NULL ||
details_panel->selected_entity_identifier == NULL ||
!g_uuid_string_is_valid(
details_panel->selected_entity_identifier
))
{
return;
}
details_panel->add_relation_callback(
details_panel->selected_entity_identifier,
details_panel->add_relation_user_data
);
}
/** /**
* @brief Ferme le volet depuis le bouton de l'interface. * @brief Ferme le volet depuis le bouton de l'interface.
*/ */
@ -266,6 +333,11 @@ EntityDetailsPanel *entity_details_panel_new(void)
NULL NULL
); );
details_panel->add_relation_button =
gtk_button_new_from_icon_name(
"list-add-symbolic"
);
details_panel->close_button = details_panel->close_button =
gtk_button_new_from_icon_name( gtk_button_new_from_icon_name(
"window-close-symbolic" "window-close-symbolic"
@ -307,6 +379,7 @@ EntityDetailsPanel *entity_details_panel_new(void)
if (details_panel->root_revealer == NULL || if (details_panel->root_revealer == NULL ||
details_panel->frame == NULL || details_panel->frame == NULL ||
details_panel->add_relation_button == NULL ||
details_panel->close_button == NULL || details_panel->close_button == NULL ||
content_box == NULL || content_box == NULL ||
header_box == NULL || header_box == NULL ||
@ -425,6 +498,21 @@ EntityDetailsPanel *entity_details_panel_new(void)
"heading" "heading"
); );
gtk_widget_set_tooltip_text(
details_panel->add_relation_button,
"Ajouter une relation depuis cette entité"
);
gtk_widget_add_css_class(
details_panel->add_relation_button,
"flat"
);
gtk_widget_set_sensitive(
details_panel->add_relation_button,
FALSE
);
gtk_widget_set_tooltip_text( gtk_widget_set_tooltip_text(
details_panel->close_button, details_panel->close_button,
"Fermer le volet" "Fermer le volet"
@ -440,6 +528,11 @@ EntityDetailsPanel *entity_details_panel_new(void)
header_label header_label
); );
gtk_box_append(
GTK_BOX(header_box),
details_panel->add_relation_button
);
gtk_box_append( gtk_box_append(
GTK_BOX(header_box), GTK_BOX(header_box),
details_panel->close_button details_panel->close_button
@ -645,6 +738,15 @@ EntityDetailsPanel *entity_details_panel_new(void)
details_panel->frame details_panel->frame
); );
g_signal_connect(
details_panel->add_relation_button,
"clicked",
G_CALLBACK(
entity_details_panel_on_add_relation_clicked
),
details_panel
);
g_signal_connect( g_signal_connect(
details_panel->close_button, details_panel->close_button,
"clicked", "clicked",
@ -689,6 +791,11 @@ void entity_details_panel_set_entity(
return; return;
} }
g_clear_pointer(
&details_panel->selected_entity_identifier,
g_free
);
if (entity_record == NULL) if (entity_record == NULL)
{ {
entity_details_panel_clear( entity_details_panel_clear(
@ -698,6 +805,17 @@ void entity_details_panel_set_entity(
return; return;
} }
details_panel->selected_entity_identifier =
g_strdup(
entity_record_get_identifier(
entity_record
)
);
entity_details_panel_update_add_relation_button(
details_panel
);
title = title =
entity_record_get_label( entity_record_get_label(
entity_record entity_record
@ -820,6 +938,15 @@ void entity_details_panel_clear(
return; return;
} }
g_clear_pointer(
&details_panel->selected_entity_identifier,
g_free
);
entity_details_panel_update_add_relation_button(
details_panel
);
entity_details_panel_set_field_text( entity_details_panel_set_field_text(
details_panel->entity_title_label, details_panel->entity_title_label,
NULL, NULL,
@ -903,6 +1030,28 @@ void entity_details_panel_set_close_callback(
user_data; user_data;
} }
void entity_details_panel_set_add_relation_callback(
EntityDetailsPanel *details_panel,
EntityDetailsPanelAddRelationCallback callback,
gpointer user_data
)
{
if (details_panel == NULL)
{
return;
}
details_panel->add_relation_callback =
callback;
details_panel->add_relation_user_data =
user_data;
entity_details_panel_update_add_relation_button(
details_panel
);
}
gboolean entity_details_panel_is_open( gboolean entity_details_panel_is_open(
const EntityDetailsPanel *details_panel const EntityDetailsPanel *details_panel
) )
@ -929,6 +1078,14 @@ void entity_details_panel_free(
return; return;
} }
if (details_panel->add_relation_button != NULL)
{
g_signal_handlers_disconnect_by_data(
details_panel->add_relation_button,
details_panel
);
}
if (details_panel->close_button != NULL) if (details_panel->close_button != NULL)
{ {
g_signal_handlers_disconnect_by_data( g_signal_handlers_disconnect_by_data(
@ -943,12 +1100,26 @@ void entity_details_panel_free(
details_panel->close_user_data = details_panel->close_user_data =
NULL; NULL;
details_panel->add_relation_callback =
NULL;
details_panel->add_relation_user_data =
NULL;
g_clear_pointer(
&details_panel->selected_entity_identifier,
g_free
);
details_panel->root_revealer = details_panel->root_revealer =
NULL; NULL;
details_panel->frame = details_panel->frame =
NULL; NULL;
details_panel->add_relation_button =
NULL;
details_panel->close_button = details_panel->close_button =
NULL; NULL;

View file

@ -93,6 +93,12 @@ struct Workspace
gpointer gpointer
reset_graph_layout_user_data; reset_graph_layout_user_data;
WorkspaceAddRelationCallback
add_relation_callback;
gpointer
add_relation_user_data;
GtkWidget *node_name_label; GtkWidget *node_name_label;
GtkWidget *node_path_label; GtkWidget *node_path_label;
GtkWidget *node_type_label; GtkWidget *node_type_label;
@ -324,6 +330,33 @@ static void workspace_on_graph_entity_selected(
); );
} }
/**
* @brief Relaie la demande d'ajout d'une relation.
*/
static void workspace_on_add_relation_requested(
const char *source_entity_identifier,
gpointer user_data
)
{
Workspace *workspace =
user_data;
if (workspace == NULL ||
workspace->add_relation_callback == NULL ||
source_entity_identifier == NULL ||
!g_uuid_string_is_valid(
source_entity_identifier
))
{
return;
}
workspace->add_relation_callback(
source_entity_identifier,
workspace->add_relation_user_data
);
}
/** /**
* @brief Désélectionne le nœud lorsque le volet est fermé manuellement. * @brief Désélectionne le nœud lorsque le volet est fermé manuellement.
*/ */
@ -1075,6 +1108,12 @@ Workspace *workspace_new(void)
workspace workspace
); );
entity_details_panel_set_add_relation_callback(
workspace->entity_details_panel,
workspace_on_add_relation_requested,
workspace
);
gtk_overlay_set_child( gtk_overlay_set_child(
GTK_OVERLAY( GTK_OVERLAY(
workspace->graph_view_page workspace->graph_view_page
@ -2107,6 +2146,24 @@ void workspace_set_reset_graph_layout_callback(
user_data; user_data;
} }
void workspace_set_add_relation_callback(
Workspace *workspace,
WorkspaceAddRelationCallback callback,
gpointer user_data
)
{
if (workspace == NULL)
{
return;
}
workspace->add_relation_callback =
callback;
workspace->add_relation_user_data =
user_data;
}
void workspace_reset_graph_layout( void workspace_reset_graph_layout(
Workspace *workspace Workspace *workspace
) )
@ -2157,6 +2214,12 @@ void workspace_free(Workspace *workspace)
NULL NULL
); );
entity_details_panel_set_add_relation_callback(
workspace->entity_details_panel,
NULL,
NULL
);
entity_details_panel_clear( entity_details_panel_clear(
workspace->entity_details_panel workspace->entity_details_panel
); );
@ -2203,6 +2266,12 @@ void workspace_free(Workspace *workspace)
workspace->reset_graph_layout_user_data = workspace->reset_graph_layout_user_data =
NULL; NULL;
workspace->add_relation_callback =
NULL;
workspace->add_relation_user_data =
NULL;
workspace->graph_toolbar = workspace->graph_toolbar =
NULL; NULL;