/****************************************************************************** * @file main_window.c * @brief Implémentation de la fenêtre principale. ******************************************************************************/ #include "views/main_window.h" #include "widgets/sidebar.h" #include "widgets/workspace.h" #include "widgets/investigation_tree_view.h" #include "widgets/task_panel.h" #include /** * @brief Largeur initiale de la fenêtre principale. */ #define MAIN_WINDOW_DEFAULT_WIDTH 1100 /** * @brief Hauteur initiale de la fenêtre principale. */ #define MAIN_WINDOW_DEFAULT_HEIGHT 650 /** * @brief Position initiale de la séparation horizontale. */ #define MAIN_WINDOW_SIDEBAR_POSITION 320 /** * @brief Position initiale de la séparation verticale. */ #define MAIN_WINDOW_TASK_PANEL_POSITION 430 /** * @brief Titre par défaut de la fenêtre. */ #define MAIN_WINDOW_DEFAULT_TITLE \ "Labfy Investigation" /** * @brief Texte affiché lorsqu'aucune enquête n'est ouverte. */ #define MAIN_WINDOW_NO_INVESTIGATION_STATUS \ "Aucune enquête ouverte" /** * @struct MainWindow * @brief Représentation interne de la fenêtre principale. * * Cette structure reste privée au module. Elle conserve les composants * nécessaires à l'organisation générale de l'interface. */ struct MainWindow { GtkWindow *window; GtkWidget *main_box; GtkWidget *action_bar; GtkWidget *new_investigation_button; GtkWidget *open_investigation_button; GtkWidget *import_evidence_button; GtkWidget *show_graph_button; GtkWidget *content_paned; GtkWidget *main_paned; GtkWidget *status_label; GtkWidget *quit_button; Sidebar *sidebar; Workspace *workspace; TaskPanel *task_panel; MainWindowNewInvestigationCallback new_investigation_callback; gpointer new_investigation_user_data; MainWindowOpenInvestigationCallback open_investigation_callback; gpointer open_investigation_user_data; MainWindowImportEvidenceCallback import_evidence_callback; gpointer import_evidence_user_data; MainWindowShowGraphCallback show_graph_callback; gpointer show_graph_user_data; MainWindowVerifyEvidenceCallback verify_evidence_callback; gpointer verify_evidence_user_data; MainWindowGraphNodeMovedCallback graph_node_moved_callback; gpointer graph_node_moved_user_data; MainWindowResetGraphLayoutCallback reset_graph_layout_callback; gpointer reset_graph_layout_user_data; MainWindowQuitCallback quit_callback; gpointer quit_user_data; }; /** * @brief Transmet la demande de création d'une enquête au contrôleur. * * @param button Bouton ayant reçu le clic. * @param user_data Pointeur vers MainWindow. */ static void main_window_on_new_investigation_clicked( GtkButton *button, gpointer user_data ) { MainWindow *main_window = user_data; (void) button; if (main_window == NULL || main_window->new_investigation_callback == NULL) { return; } main_window->new_investigation_callback( main_window->new_investigation_user_data ); } /** * @brief Transmet la demande d'ouverture d'une enquête au contrôleur. * * @param button Bouton ayant reçu le clic. * @param user_data Pointeur vers MainWindow. */ static void main_window_on_open_investigation_clicked( GtkButton *button, gpointer user_data ) { MainWindow *main_window = user_data; (void) button; if (main_window == NULL || main_window->open_investigation_callback == NULL) { return; } main_window->open_investigation_callback( main_window->open_investigation_user_data ); } /** * @brief Transmet la demande d'import d'une preuve au contrôleur. * * @param button Bouton ayant reçu le clic. * @param user_data Pointeur vers MainWindow. */ static void main_window_on_import_evidence_clicked( GtkButton *button, gpointer user_data ) { MainWindow *main_window = user_data; (void) button; if (main_window == NULL || main_window->import_evidence_callback == NULL) { return; } main_window->import_evidence_callback( main_window->import_evidence_user_data ); } /** * @brief Transmet la demande de fermeture au contrôleur. * * @param button Bouton ayant reçu le clic. * @param user_data Pointeur vers MainWindow. */ static void main_window_on_show_graph_clicked( GtkButton *button, gpointer user_data ) { MainWindow *main_window = user_data; (void) button; if (main_window == NULL || main_window->show_graph_callback == NULL) { return; } main_window->show_graph_callback( main_window->show_graph_user_data ); } /** * @brief Transmet la demande de fermeture au contrôleur. * * @param button Bouton ayant reçu le clic. * @param user_data Pointeur vers MainWindow. */ static void main_window_on_quit_clicked( GtkButton *button, gpointer user_data ) { MainWindow *main_window = user_data; (void) button; if (main_window == NULL || main_window->quit_callback == NULL) { return; } main_window->quit_callback( main_window->quit_user_data ); } /** * @brief Crée un bouton d'action compact avec une icône. * * @param icon_name Nom de l'icône symbolique GTK. * @param tooltip_text Texte de l'infobulle. * * @return Nouveau bouton GTK. */ static GtkWidget *main_window_create_action_button( const char *icon_name, const char *tooltip_text ) { GtkWidget *button = NULL; if (icon_name == NULL || icon_name[0] == '\0') { return NULL; } button = gtk_button_new_from_icon_name( icon_name ); if (button == NULL) { return NULL; } gtk_widget_add_css_class( button, "flat" ); if (tooltip_text != NULL && tooltip_text[0] != '\0') { gtk_widget_set_tooltip_text( button, tooltip_text ); } 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. */ static void main_window_on_reset_graph_layout_requested( gpointer user_data ) { MainWindow *main_window = user_data; if (main_window == NULL || main_window->reset_graph_layout_callback == NULL) { return; } main_window->reset_graph_layout_callback( main_window->reset_graph_layout_user_data ); } /** * @brief Relaie la demande de vérification provenant du Workspace. */ static void main_window_on_verify_evidence_requested( const char *evidence_identifier, gpointer user_data ) { MainWindow *main_window = user_data; if (main_window == NULL || main_window->verify_evidence_callback == NULL || evidence_identifier == NULL || evidence_identifier[0] == '\0') { return; } main_window->verify_evidence_callback( evidence_identifier, main_window->verify_evidence_user_data ); } MainWindow *main_window_new( GtkApplication *application, TaskManager *task_manager ) { MainWindow *main_window = NULL; GtkWidget *sidebar_widget = NULL; GtkWidget *workspace_widget = NULL; GtkWidget *task_panel_widget = NULL; GtkWidget *action_spacer = NULL; if (application == NULL || task_manager == NULL) { return NULL; } main_window = g_new0(MainWindow, 1); main_window->window = GTK_WINDOW( gtk_application_window_new(application) ); if (main_window->window == NULL) { main_window_free(main_window); return NULL; } gtk_window_set_title( main_window->window, MAIN_WINDOW_DEFAULT_TITLE ); gtk_window_set_default_size( main_window->window, MAIN_WINDOW_DEFAULT_WIDTH, MAIN_WINDOW_DEFAULT_HEIGHT ); /* * La boîte principale organise verticalement : * * 1. la zone centrale ; * 2. la barre d'état. */ main_window->main_box = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); /* * Barre regroupant les actions générales de l'application. */ main_window->action_bar = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 4 ); gtk_widget_set_margin_start( main_window->action_bar, 8 ); gtk_widget_set_margin_end( main_window->action_bar, 8 ); gtk_widget_set_margin_top( main_window->action_bar, 8 ); gtk_widget_set_margin_bottom( main_window->action_bar, 8 ); main_window->new_investigation_button = main_window_create_action_button( "document-new-symbolic", "Créer une nouvelle enquête" ); main_window->open_investigation_button = main_window_create_action_button( "document-open-symbolic", "Ouvrir une enquête" ); main_window->import_evidence_button = main_window_create_action_button( "document-save-symbolic", "Importer une preuve" ); main_window->show_graph_button = main_window_create_action_button( "go-previous-symbolic", "Revenir au graphe" ); main_window->quit_button = main_window_create_action_button( "application-exit-symbolic", "Quitter Labfy Investigation" ); action_spacer = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); gtk_widget_set_hexpand( action_spacer, TRUE ); /* * Aucun import n'est autorisé sans enquête ouverte. */ gtk_widget_set_sensitive( main_window->import_evidence_button, FALSE ); gtk_widget_set_sensitive( main_window->show_graph_button, FALSE ); gtk_box_append( GTK_BOX(main_window->action_bar), main_window->new_investigation_button ); gtk_box_append( GTK_BOX(main_window->action_bar), main_window->open_investigation_button ); gtk_box_append( GTK_BOX(main_window->action_bar), main_window->import_evidence_button ); gtk_box_append( GTK_BOX(main_window->action_bar), main_window->show_graph_button ); gtk_box_append( GTK_BOX(main_window->action_bar), action_spacer ); gtk_box_append( GTK_BOX(main_window->action_bar), main_window->quit_button ); g_signal_connect( main_window->new_investigation_button, "clicked", G_CALLBACK( main_window_on_new_investigation_clicked ), main_window ); g_signal_connect( main_window->open_investigation_button, "clicked", G_CALLBACK( main_window_on_open_investigation_clicked ), main_window ); g_signal_connect( main_window->import_evidence_button, "clicked", G_CALLBACK( main_window_on_import_evidence_clicked ), main_window ); g_signal_connect( main_window->show_graph_button, "clicked", G_CALLBACK( main_window_on_show_graph_clicked ), main_window ); g_signal_connect( main_window->quit_button, "clicked", G_CALLBACK( main_window_on_quit_clicked ), main_window ); /* * GtkPaned sépare horizontalement le panneau latéral * et la zone de travail. */ main_window->main_paned = gtk_paned_new( GTK_ORIENTATION_HORIZONTAL ); gtk_widget_set_hexpand( main_window->main_paned, TRUE ); gtk_widget_set_vexpand( main_window->main_paned, TRUE ); /* * Création du panneau latéral via son propre module. * * MainWindow ne connaît pas son contenu interne. */ main_window->sidebar = sidebar_new(); if (main_window->sidebar == NULL) { main_window_free(main_window); return NULL; } sidebar_widget = sidebar_get_widget( main_window->sidebar ); if (sidebar_widget == NULL) { main_window_free(main_window); return NULL; } main_window->workspace = workspace_new(); if (main_window->workspace == NULL) { main_window_free(main_window); return NULL; } workspace_set_verify_evidence_callback( main_window->workspace, main_window_on_verify_evidence_requested, main_window ); workspace_set_graph_node_moved_callback( main_window->workspace, main_window_on_graph_node_moved, main_window ); workspace_set_reset_graph_layout_callback( main_window->workspace, main_window_on_reset_graph_layout_requested, main_window ); workspace_widget = workspace_get_widget( main_window->workspace ); if (workspace_widget == NULL) { main_window_free(main_window); return NULL; } main_window->task_panel = task_panel_new( task_manager ); if (main_window->task_panel == NULL) { main_window_free( main_window ); return NULL; } task_panel_widget = task_panel_get_widget( main_window->task_panel ); if (task_panel_widget == NULL) { main_window_free( main_window ); return NULL; } /* * Placement des deux composants dans GtkPaned. */ gtk_paned_set_start_child( GTK_PANED(main_window->main_paned), sidebar_widget ); gtk_paned_set_end_child( GTK_PANED(main_window->main_paned), workspace_widget ); /* * Position initiale de la poignée de séparation. */ gtk_paned_set_position( GTK_PANED(main_window->main_paned), MAIN_WINDOW_SIDEBAR_POSITION ); /* * La sidebar conserve sa largeur lorsque la fenêtre est agrandie. * La zone de travail récupère l'espace supplémentaire. */ gtk_paned_set_resize_start_child( GTK_PANED(main_window->main_paned), FALSE ); gtk_paned_set_resize_end_child( GTK_PANED(main_window->main_paned), TRUE ); /* * Les deux panneaux peuvent être réduits manuellement. */ gtk_paned_set_shrink_start_child( GTK_PANED(main_window->main_paned), TRUE ); gtk_paned_set_shrink_end_child( GTK_PANED(main_window->main_paned), TRUE ); main_window->content_paned = gtk_paned_new( GTK_ORIENTATION_VERTICAL ); gtk_widget_set_hexpand( main_window->content_paned, TRUE ); gtk_widget_set_vexpand( main_window->content_paned, TRUE ); gtk_paned_set_start_child( GTK_PANED(main_window->content_paned), main_window->main_paned ); gtk_paned_set_end_child( GTK_PANED(main_window->content_paned), task_panel_widget ); gtk_paned_set_position( GTK_PANED(main_window->content_paned), MAIN_WINDOW_TASK_PANEL_POSITION ); gtk_paned_set_resize_start_child( GTK_PANED(main_window->content_paned), TRUE ); gtk_paned_set_resize_end_child( GTK_PANED(main_window->content_paned), FALSE ); gtk_paned_set_shrink_start_child( GTK_PANED(main_window->content_paned), TRUE ); gtk_paned_set_shrink_end_child( GTK_PANED(main_window->content_paned), TRUE ); main_window->status_label = gtk_label_new( MAIN_WINDOW_NO_INVESTIGATION_STATUS ); gtk_widget_set_halign( main_window->status_label, GTK_ALIGN_START ); gtk_widget_set_margin_start( main_window->status_label, 8 ); gtk_widget_set_margin_end( main_window->status_label, 8 ); gtk_widget_set_margin_top( main_window->status_label, 6 ); gtk_widget_set_margin_bottom( main_window->status_label, 6 ); /* * Assemblage vertical : * * Barre d'actions * GtkPaned * Barre d'état */ gtk_box_append( GTK_BOX(main_window->main_box), main_window->action_bar ); gtk_box_append( GTK_BOX(main_window->main_box), main_window->content_paned ); gtk_box_append( GTK_BOX(main_window->main_box), main_window->status_label ); gtk_window_set_child( main_window->window, main_window->main_box ); /* * Ce volet vertical sépare la zone principale du panneau d'activité. */ return main_window; } void main_window_present(MainWindow *main_window) { if (main_window == NULL || main_window->window == NULL) { return; } gtk_window_present(main_window->window); } GtkWindow *main_window_get_window( const MainWindow *main_window ) { if (main_window == NULL) { return NULL; } return main_window->window; } void main_window_set_tree_model( MainWindow *main_window, const InvestigationTreeModel *tree_model ) { if (main_window == NULL) { return; } sidebar_set_tree_model( main_window->sidebar, tree_model ); } void main_window_set_evidence_model( MainWindow *main_window, EvidenceCategoryModel *evidence_category_model ) { if (main_window == NULL) { return; } sidebar_set_evidence_model( main_window->sidebar, evidence_category_model ); } void main_window_set_investigation( MainWindow *main_window, const char *investigation_name, const char *investigation_root_path ) { char *window_title = NULL; char *status_text = NULL; gboolean has_name = FALSE; gboolean has_root_path = FALSE; if (main_window == NULL) { return; } has_name = investigation_name != NULL && investigation_name[0] != '\0'; has_root_path = investigation_root_path != NULL && investigation_root_path[0] != '\0'; if (has_name) { window_title = g_strdup_printf( "%s — %s", MAIN_WINDOW_DEFAULT_TITLE, investigation_name ); } else { window_title = g_strdup( MAIN_WINDOW_DEFAULT_TITLE ); } if (has_name && has_root_path) { status_text = g_strdup_printf( "Enquête ouverte : %s — %s", investigation_name, investigation_root_path ); } else if (has_name) { status_text = g_strdup_printf( "Enquête ouverte : %s", investigation_name ); } else if (has_root_path) { status_text = g_strdup_printf( "Enquête ouverte : %s", investigation_root_path ); } else { status_text = g_strdup( MAIN_WINDOW_NO_INVESTIGATION_STATUS ); } if (main_window->window != NULL) { gtk_window_set_title( main_window->window, window_title ); } if (main_window->status_label != NULL) { gtk_label_set_text( GTK_LABEL(main_window->status_label), status_text ); } g_free(status_text); g_free(window_title); } void main_window_set_graph_loading( MainWindow *main_window ) { if (main_window == NULL) { return; } gtk_widget_set_sensitive( main_window->show_graph_button, FALSE ); workspace_set_graph_loading( main_window->workspace ); } void main_window_set_graph( MainWindow *main_window, const InvestigationGraphModel *graph_model, const InvestigationGraphLayout *graph_layout ) { if (main_window == NULL) { return; } workspace_set_graph( main_window->workspace, graph_model, graph_layout ); gtk_widget_set_sensitive( main_window->show_graph_button, graph_model != NULL ); } void main_window_set_graph_error( MainWindow *main_window, const char *message ) { if (main_window == NULL) { return; } gtk_widget_set_sensitive( main_window->show_graph_button, FALSE ); workspace_set_graph_error( main_window->workspace, message ); } void main_window_clear_graph( MainWindow *main_window ) { if (main_window == NULL) { return; } gtk_widget_set_sensitive( main_window->show_graph_button, FALSE ); workspace_clear_graph( main_window->workspace ); } void main_window_reset_graph_layout( MainWindow *main_window ) { if (main_window == NULL) { return; } workspace_reset_graph_layout( main_window->workspace ); } void main_window_set_status( MainWindow *main_window, const char *status_text ) { const char *safe_status_text = NULL; if (main_window == NULL || main_window->status_label == NULL) { return; } safe_status_text = status_text != NULL && status_text[0] != '\0' ? status_text : MAIN_WINDOW_NO_INVESTIGATION_STATUS; gtk_label_set_text( GTK_LABEL(main_window->status_label), safe_status_text ); } void main_window_set_tree_selection_callback( MainWindow *main_window, InvestigationTreeViewSelectionCallback callback, gpointer user_data ) { if (main_window == NULL) { return; } sidebar_set_selection_callback( main_window->sidebar, callback, user_data ); } void main_window_set_evidence_selection_callback( MainWindow *main_window, MainWindowEvidenceSelectionCallback callback, gpointer user_data ) { if (main_window == NULL) { return; } sidebar_set_evidence_selection_callback( main_window->sidebar, callback, user_data ); } void main_window_set_new_investigation_callback( MainWindow *main_window, MainWindowNewInvestigationCallback callback, gpointer user_data ) { if (main_window == NULL) { return; } main_window->new_investigation_callback = callback; main_window->new_investigation_user_data = user_data; } void main_window_set_open_investigation_callback( MainWindow *main_window, MainWindowOpenInvestigationCallback callback, gpointer user_data ) { if (main_window == NULL) { return; } main_window->open_investigation_callback = callback; main_window->open_investigation_user_data = user_data; } void main_window_set_import_evidence_callback( MainWindow *main_window, MainWindowImportEvidenceCallback callback, gpointer user_data ) { if (main_window == NULL) { return; } main_window->import_evidence_callback = callback; main_window->import_evidence_user_data = user_data; } void main_window_set_show_graph_callback( MainWindow *main_window, MainWindowShowGraphCallback callback, gpointer user_data ) { if (main_window == NULL) { return; } main_window->show_graph_callback = callback; main_window->show_graph_user_data = user_data; } void main_window_set_import_evidence_enabled( MainWindow *main_window, gboolean enabled ) { if (main_window == NULL || main_window->import_evidence_button == NULL) { return; } gtk_widget_set_sensitive( main_window->import_evidence_button, enabled ); } void main_window_set_verify_evidence_callback( MainWindow *main_window, MainWindowVerifyEvidenceCallback callback, gpointer user_data ) { if (main_window == NULL) { return; } main_window->verify_evidence_callback = callback; main_window->verify_evidence_user_data = 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_reset_graph_layout_callback( MainWindow *main_window, MainWindowResetGraphLayoutCallback callback, gpointer user_data ) { if (main_window == NULL) { return; } main_window->reset_graph_layout_callback = callback; main_window->reset_graph_layout_user_data = user_data; } void main_window_set_quit_callback( MainWindow *main_window, MainWindowQuitCallback callback, gpointer user_data ) { if (main_window == NULL) { return; } main_window->quit_callback = callback; main_window->quit_user_data = user_data; } void main_window_set_selected_node( MainWindow *main_window, const InvestigationNode *node ) { if (main_window == NULL) { return; } workspace_set_selected_node( main_window->workspace, node ); } void main_window_set_selected_evidence( MainWindow *main_window, const EvidenceRecord *evidence_record ) { if (main_window == NULL) { return; } workspace_set_selected_evidence( main_window->workspace, evidence_record ); } void main_window_free( MainWindow *main_window ) { if (main_window == NULL) { return; } /* * TaskPanel possède un timer et un callback enregistré dans * TaskManager. Ils doivent être retirés avant la destruction * des widgets GTK. */ task_panel_free( main_window->task_panel ); main_window->task_panel = NULL; /* * Empêche la destruction du modèle de transmettre une dernière * sélection à Application. */ if (main_window->sidebar != NULL) { sidebar_set_selection_callback( main_window->sidebar, NULL, NULL ); /* * Le modèle est détaché pendant que le GtkListView existe * encore. */ sidebar_set_tree_model( main_window->sidebar, NULL ); sidebar_set_evidence_selection_callback( main_window->sidebar, NULL, NULL ); sidebar_set_evidence_model( main_window->sidebar, NULL ); } /* * Les structures qui manipulent encore leurs widgets doivent être * nettoyées avant gtk_window_destroy(). */ sidebar_free( main_window->sidebar ); if (main_window->workspace != NULL) { workspace_set_verify_evidence_callback( main_window->workspace, NULL, NULL ); workspace_set_graph_node_moved_callback( main_window->workspace, NULL, NULL ); workspace_set_reset_graph_layout_callback( main_window->workspace, NULL, NULL ); main_window->graph_node_moved_callback = NULL; main_window->graph_node_moved_user_data = NULL; main_window->reset_graph_layout_callback = NULL; main_window->reset_graph_layout_user_data = NULL; main_window->show_graph_callback = NULL; main_window->show_graph_user_data = NULL; main_window_clear_graph( main_window ); } workspace_free( main_window->workspace ); main_window->sidebar = NULL; main_window->workspace = NULL; /* * Les modules ne manipulent plus leurs widgets. * GTK peut maintenant détruire tout l'arbre sans callback vers * des structures déjà libérées. */ if (main_window->window != NULL) { gtk_window_destroy( main_window->window ); main_window->window = NULL; } g_free( main_window ); }