/****************************************************************************** * @file task_panel.c * @brief Implémentation du panneau GTK affichant les tâches. ******************************************************************************/ #include "widgets/task_panel.h" /** * @brief Intervalle de rafraîchissement du panneau, en millisecondes. */ #define TASK_PANEL_REFRESH_INTERVAL_MS 250 /** * @brief Texte affiché lorsqu’aucune tâche n’est suivie. */ #define TASK_PANEL_EMPTY_TEXT \ "Aucune tâche en cours ou terminée" /** * @struct TaskPanelRow * @brief Widgets persistants associés à une tâche. * * Les widgets sont créés une seule fois. Le rafraîchissement périodique * modifie seulement leur contenu. */ typedef struct { BackgroundTask *task; GtkWidget *frame; GtkWidget *state_label; GtkWidget *progress_bar; GtkWidget *detail_label; GtkWidget *cancel_button; } TaskPanelRow; /** * @struct TaskPanel * @brief État interne du panneau d’activité. * * TaskManager est emprunté et doit rester valide pendant toute la durée * de vie du panneau. */ struct TaskPanel { TaskManager *task_manager; GtkWidget *root_box; GtkWidget *header_box; GtkWidget *title_label; GtkWidget *clear_button; GtkWidget *scrolled_window; GtkWidget *task_list_box; GPtrArray *rows; guint refresh_source_id; }; /** * @brief Retourne le libellé correspondant à l’état d’une tâche. * * @param state État de la tâche. * * @return Libellé statique. */ static const char *task_panel_get_state_label( BackgroundTaskState state ) { switch (state) { case BACKGROUND_TASK_STATE_PENDING: return "En attente"; case BACKGROUND_TASK_STATE_RUNNING: return "En cours"; case BACKGROUND_TASK_STATE_COMPLETED: return "Terminée"; case BACKGROUND_TASK_STATE_FAILED: return "Échouée"; case BACKGROUND_TASK_STATE_CANCELLED: return "Annulée"; default: return "État inconnu"; } } /** * @brief Supprime tous les widgets enfants d’une boîte GTK. * * @param box Boîte à vider. */ static void task_panel_clear_box( GtkWidget *box ) { GtkWidget *child = NULL; if (box == NULL) { return; } child = gtk_widget_get_first_child( box ); while (child != NULL) { gtk_box_remove( GTK_BOX(box), child ); child = gtk_widget_get_first_child( box ); } } /** * @brief Demande l’annulation d’une tâche. * * La tâche est conservée dans les données du bouton avec une référence * indépendante. * * @param button Bouton ayant reçu le clic. * @param user_data Pointeur vers TaskPanel. */ static void task_panel_on_cancel_clicked( GtkButton *button, gpointer user_data ) { TaskPanel *task_panel = user_data; BackgroundTask *task = NULL; if (button == NULL || task_panel == NULL) { return; } task = g_object_get_data( G_OBJECT(button), "labfy-background-task" ); if (task == NULL) { return; } background_task_cancel( task ); } /** * @brief Retire les tâches terminées du gestionnaire. * * @param button Bouton ayant reçu le clic. * @param user_data Pointeur vers TaskPanel. */ static void task_panel_on_clear_clicked( GtkButton *button, gpointer user_data ) { TaskPanel *task_panel = user_data; (void) button; if (task_panel == NULL || task_panel->task_manager == NULL) { return; } task_manager_remove_finished( task_panel->task_manager ); } /** * @brief Réagit aux changements structurels du gestionnaire. * * @param task_manager Gestionnaire ayant changé. * @param user_data Pointeur vers TaskPanel. */ static void task_panel_on_manager_changed( TaskManager *task_manager, gpointer user_data ) { TaskPanel *task_panel = user_data; if (task_panel == NULL || task_manager == NULL || task_panel->task_manager != task_manager) { return; } task_panel_refresh( task_panel ); } /** * @brief Met à jour les lignes existantes sans reconstruire la liste. * * @param task_panel Panneau à mettre à jour. */ static void task_panel_update_rows( TaskPanel *task_panel ); /** * @brief Rafraîchit périodiquement les progressions. * * @param user_data Pointeur vers TaskPanel. * * @return G_SOURCE_CONTINUE. */ static gboolean task_panel_on_refresh_timeout( gpointer user_data ) { TaskPanel *task_panel = user_data; if (task_panel == NULL) { return G_SOURCE_REMOVE; } task_panel_update_rows( task_panel ); return G_SOURCE_CONTINUE; } /** * @brief Crée le texte secondaire d’une tâche. * * @param task Tâche concernée. * @param state État courant. * @param progress Progression courante. * @param status_message Message de progression facultatif. * @param error Erreur finale facultative. * * @return Nouvelle chaîne à libérer avec g_free(). */ static char *task_panel_create_detail_text( const BackgroundTask *task, BackgroundTaskState state, double progress, const char *status_message, const GError *error ) { const char *state_label = NULL; if (task == NULL) { return g_strdup( "Tâche invalide" ); } state_label = task_panel_get_state_label( state ); if (state == BACKGROUND_TASK_STATE_RUNNING) { if (status_message != NULL && status_message[0] != '\0') { return g_strdup_printf( "%s — %.0f %% — %s", state_label, progress * 100.0, status_message ); } return g_strdup_printf( "%s — %.0f %%", state_label, progress * 100.0 ); } if (state == BACKGROUND_TASK_STATE_FAILED && error != NULL) { return g_strdup_printf( "%s — %s", state_label, error->message ); } if (status_message != NULL && status_message[0] != '\0') { return g_strdup_printf( "%s — %s", state_label, status_message ); } return g_strdup( state_label ); } /** * @brief Libère les données privées d'une ligne. * * @param user_data Pointeur vers TaskPanelRow. */ static void task_panel_row_free( gpointer user_data ) { TaskPanelRow *task_row = user_data; if (task_row == NULL) { return; } background_task_unref( task_row->task ); task_row->task = NULL; g_free( task_row ); } /** * @brief Met à jour l'affichage d'une ligne existante. * * Aucun widget n'est détruit ici. * * @param task_row Ligne à mettre à jour. */ static void task_panel_row_update( TaskPanelRow *task_row ) { BackgroundTaskState state; double progress = 0.0; char *status_message = NULL; char *detail_text = NULL; GError *error = NULL; if (task_row == NULL || task_row->task == NULL) { return; } state = background_task_get_state( task_row->task ); progress = background_task_get_progress( task_row->task ); status_message = background_task_dup_status_message( task_row->task ); error = background_task_dup_error( task_row->task ); detail_text = task_panel_create_detail_text( task_row->task, state, progress, status_message, error ); gtk_label_set_text( GTK_LABEL(task_row->state_label), task_panel_get_state_label( state ) ); gtk_progress_bar_set_fraction( GTK_PROGRESS_BAR( task_row->progress_bar ), state == BACKGROUND_TASK_STATE_COMPLETED ? 1.0 : progress ); gtk_label_set_text( GTK_LABEL(task_row->detail_label), detail_text != NULL ? detail_text : "" ); /* * Le bouton existe en permanence, mais n'est visible que lorsque * la tâche peut être annulée. */ gtk_widget_set_visible( task_row->cancel_button, state == BACKGROUND_TASK_STATE_RUNNING ); gtk_widget_set_sensitive( task_row->cancel_button, state == BACKGROUND_TASK_STATE_RUNNING ); g_clear_error( &error ); g_free( detail_text ); g_free( status_message ); } /** * @brief Crée une ligne GTK persistante pour une tâche. * * @param task_panel Panneau propriétaire. * @param task Tâche à afficher. * * @return Nouvelle ligne, ou NULL. */ static TaskPanelRow *task_panel_create_task_row( TaskPanel *task_panel, BackgroundTask *task ) { TaskPanelRow *task_row = NULL; GtkWidget *row_box = NULL; GtkWidget *header_box = NULL; GtkWidget *title_label = NULL; const char *title = NULL; if (task_panel == NULL || task == NULL) { return NULL; } task_row = g_new0( TaskPanelRow, 1 ); task_row->task = background_task_ref( task ); task_row->frame = gtk_frame_new( NULL ); row_box = gtk_box_new( GTK_ORIENTATION_VERTICAL, 6 ); gtk_widget_set_margin_start( row_box, 10 ); gtk_widget_set_margin_end( row_box, 10 ); gtk_widget_set_margin_top( row_box, 8 ); gtk_widget_set_margin_bottom( row_box, 8 ); header_box = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 8 ); title = background_task_get_title( task ); title_label = gtk_label_new( title != NULL ? title : "Tâche sans titre" ); gtk_widget_set_hexpand( title_label, TRUE ); gtk_widget_set_halign( title_label, GTK_ALIGN_START ); gtk_label_set_xalign( GTK_LABEL(title_label), 0.0F ); gtk_label_set_ellipsize( GTK_LABEL(title_label), PANGO_ELLIPSIZE_END ); task_row->state_label = gtk_label_new( NULL ); gtk_widget_set_halign( task_row->state_label, GTK_ALIGN_END ); task_row->cancel_button = gtk_button_new_with_label( "Annuler" ); gtk_widget_set_halign( task_row->cancel_button, GTK_ALIGN_END ); gtk_widget_set_valign( task_row->cancel_button, GTK_ALIGN_CENTER ); /* * TaskPanelRow conserve déjà une référence sur la tâche. * Le bouton emprunte donc simplement ce pointeur. */ g_object_set_data( G_OBJECT(task_row->cancel_button), "labfy-background-task", task_row->task ); g_signal_connect( task_row->cancel_button, "clicked", G_CALLBACK( task_panel_on_cancel_clicked ), task_panel ); gtk_box_append( GTK_BOX(header_box), title_label ); gtk_box_append( GTK_BOX(header_box), task_row->state_label ); gtk_box_append( GTK_BOX(header_box), task_row->cancel_button ); task_row->progress_bar = gtk_progress_bar_new(); gtk_progress_bar_set_show_text( GTK_PROGRESS_BAR( task_row->progress_bar ), TRUE ); task_row->detail_label = gtk_label_new( NULL ); gtk_widget_set_halign( task_row->detail_label, GTK_ALIGN_START ); gtk_label_set_xalign( GTK_LABEL(task_row->detail_label), 0.0F ); gtk_label_set_wrap( GTK_LABEL(task_row->detail_label), TRUE ); gtk_label_set_wrap_mode( GTK_LABEL(task_row->detail_label), PANGO_WRAP_WORD_CHAR ); gtk_label_set_selectable( GTK_LABEL(task_row->detail_label), TRUE ); gtk_box_append( GTK_BOX(row_box), header_box ); gtk_box_append( GTK_BOX(row_box), task_row->progress_bar ); gtk_box_append( GTK_BOX(row_box), task_row->detail_label ); gtk_frame_set_child( GTK_FRAME(task_row->frame), row_box ); task_panel_row_update( task_row ); return task_row; } /** * @brief Supprime les widgets et les données de toutes les lignes. * * @param task_panel Panneau à vider. */ static void task_panel_clear_rows( TaskPanel *task_panel ) { if (task_panel == NULL) { return; } task_panel_clear_box( task_panel->task_list_box ); if (task_panel->rows != NULL) { g_ptr_array_set_size( task_panel->rows, 0 ); } } /** * @brief Met à jour toutes les lignes persistantes. * * @param task_panel Panneau concerné. */ static void task_panel_update_rows( TaskPanel *task_panel ) { guint index = 0; if (task_panel == NULL || task_panel->rows == NULL) { return; } for (index = 0; index < task_panel->rows->len; index++) { TaskPanelRow *task_row = NULL; task_row = g_ptr_array_index( task_panel->rows, index ); task_panel_row_update( task_row ); } } TaskPanel *task_panel_new( TaskManager *task_manager ) { TaskPanel *task_panel = NULL; if (task_manager == NULL) { return NULL; } task_panel = g_new0( TaskPanel, 1 ); task_panel->task_manager = task_manager; task_panel->rows = g_ptr_array_new_with_free_func( task_panel_row_free ); task_panel->root_box = gtk_box_new( GTK_ORIENTATION_VERTICAL, 8 ); task_panel->header_box = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 8 ); gtk_widget_set_margin_start( task_panel->header_box, 8 ); gtk_widget_set_margin_end( task_panel->header_box, 8 ); gtk_widget_set_margin_top( task_panel->header_box, 8 ); task_panel->title_label = gtk_label_new( "Activité" ); gtk_widget_set_hexpand( task_panel->title_label, TRUE ); gtk_widget_set_halign( task_panel->title_label, GTK_ALIGN_START ); gtk_label_set_xalign( GTK_LABEL(task_panel->title_label), 0.0F ); task_panel->clear_button = gtk_button_new_with_label( "Nettoyer" ); g_signal_connect( task_panel->clear_button, "clicked", G_CALLBACK( task_panel_on_clear_clicked ), task_panel ); gtk_box_append( GTK_BOX(task_panel->header_box), task_panel->title_label ); gtk_box_append( GTK_BOX(task_panel->header_box), task_panel->clear_button ); task_panel->task_list_box = gtk_box_new( GTK_ORIENTATION_VERTICAL, 8 ); gtk_widget_set_margin_start( task_panel->task_list_box, 8 ); gtk_widget_set_margin_end( task_panel->task_list_box, 8 ); gtk_widget_set_margin_bottom( task_panel->task_list_box, 8 ); task_panel->scrolled_window = gtk_scrolled_window_new(); gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( task_panel->scrolled_window ), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC ); gtk_scrolled_window_set_child( GTK_SCROLLED_WINDOW( task_panel->scrolled_window ), task_panel->task_list_box ); gtk_widget_set_vexpand( task_panel->scrolled_window, TRUE ); gtk_box_append( GTK_BOX(task_panel->root_box), task_panel->header_box ); gtk_box_append( GTK_BOX(task_panel->root_box), task_panel->scrolled_window ); task_manager_set_changed_callback( task_manager, task_panel_on_manager_changed, task_panel, NULL ); task_panel->refresh_source_id = g_timeout_add( TASK_PANEL_REFRESH_INTERVAL_MS, task_panel_on_refresh_timeout, task_panel ); if (task_panel->refresh_source_id == 0) { task_manager_set_changed_callback( task_manager, NULL, NULL, NULL ); g_ptr_array_unref( task_panel->rows ); task_panel->rows = NULL; g_free( task_panel ); return NULL; } task_panel_refresh( task_panel ); return task_panel; } GtkWidget *task_panel_get_widget( const TaskPanel *task_panel ) { if (task_panel == NULL) { return NULL; } return task_panel->root_box; } void task_panel_refresh( TaskPanel *task_panel ) { gsize task_count = 0; gsize index = 0; if (task_panel == NULL || task_panel->task_manager == NULL || task_panel->task_list_box == NULL || task_panel->rows == NULL) { return; } /* * La liste n'est reconstruite que lorsque TaskManager signale * un ajout ou une suppression. */ task_panel_clear_rows( task_panel ); task_count = task_manager_get_count( task_panel->task_manager ); gtk_widget_set_sensitive( task_panel->clear_button, task_count > 0 ); if (task_count == 0) { GtkWidget *empty_label = NULL; empty_label = gtk_label_new( TASK_PANEL_EMPTY_TEXT ); gtk_widget_set_margin_top( empty_label, 16 ); gtk_widget_set_margin_bottom( empty_label, 16 ); gtk_widget_set_halign( empty_label, GTK_ALIGN_CENTER ); gtk_box_append( GTK_BOX(task_panel->task_list_box), empty_label ); return; } for (index = 0; index < task_count; index++) { BackgroundTask *task = NULL; TaskPanelRow *task_row = NULL; task = task_manager_get_task( task_panel->task_manager, index ); if (task == NULL) { continue; } task_row = task_panel_create_task_row( task_panel, task ); if (task_row != NULL) { gtk_box_append( GTK_BOX( task_panel->task_list_box ), task_row->frame ); g_ptr_array_add( task_panel->rows, task_row ); } background_task_unref( task ); } } void task_panel_free( TaskPanel *task_panel ) { if (task_panel == NULL) { return; } /* * Le callback du manager ne doit plus pouvoir utiliser TaskPanel. */ if (task_panel->task_manager != NULL) { task_manager_set_changed_callback( task_panel->task_manager, NULL, NULL, NULL ); } /* * Arrêt du rafraîchissement périodique. */ if (task_panel->refresh_source_id != 0) { g_source_remove( task_panel->refresh_source_id ); task_panel->refresh_source_id = 0; } if (task_panel->clear_button != NULL) { g_signal_handlers_disconnect_by_data( task_panel->clear_button, task_panel ); } task_panel_clear_rows( task_panel ); if (task_panel->rows != NULL) { g_ptr_array_unref( task_panel->rows ); task_panel->rows = NULL; } task_panel->task_manager = NULL; g_free( task_panel ); }