// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later #include "native.h" #include #include enum { ACTION_NONE, ACTION_COPY, ACTION_DISMISS, ACTION_CLEAR, ACTION_CLOSE, ACTION_SELECTION, ACTION_INVOKE }; static GtkWidget *dialog; static GtkListStore *model; static GtkTreeSelection *selection; static GtkWidget *dismiss_button; static GtkWidget *actions_box; static GtkWidget *actions_status; static int action_code; static int suppress_selection; static char *selected_action_key; static char *selected_id_cache; static void set_action(int code) { if (!action_code) action_code = code; } static void response(GtkDialog *widget, int response_id, gpointer data) { (void)widget; (void)data; if (response_id == ACTION_COPY) set_action(ACTION_COPY); else if (response_id == ACTION_DISMISS) set_action(ACTION_DISMISS); else if (response_id == ACTION_CLEAR) set_action(ACTION_CLEAR); else if (response_id == ACTION_CLOSE) set_action(ACTION_CLOSE); } static void selection_changed(GtkTreeSelection *widget, gpointer data) { (void)widget; (void)data; if (!suppress_selection) set_action(ACTION_SELECTION); } static void action_clicked(GtkButton *button, gpointer data) { (void)data; const char *key = g_object_get_data(G_OBJECT(button), "cthulhu-action-key"); g_free(selected_action_key); selected_action_key = g_strdup(key ? key : ""); set_action(ACTION_INVOKE); } static void clear_action_buttons(void) { if (!actions_box) return; GList *children = gtk_container_get_children(GTK_CONTAINER(actions_box)); for (GList *item = children; item; item = item->next) gtk_widget_destroy(GTK_WIDGET(item->data)); g_list_free(children); } static void set_actions(char **keys, char **labels, int count) { clear_action_buttons(); for (int index = 0; index < count; index++) { const char *label = labels && labels[index] && labels[index][0] ? labels[index] : keys[index]; GtkWidget *button = gtk_button_new_with_label(label ? label : ""); char *key = g_strdup(keys && keys[index] ? keys[index] : ""); g_object_set_data_full(G_OBJECT(button), "cthulhu-action-key", key, g_free); g_signal_connect(button, "clicked", G_CALLBACK(action_clicked), NULL); gtk_box_pack_start(GTK_BOX(actions_box), button, FALSE, FALSE, 0); } gtk_widget_show_all(actions_box); } int notification_list_init(const char *title, const char *message_header, const char *received_header) { if (!gtk_init_check(NULL, NULL)) return 0; g_set_application_name("Cthulhu Notifications"); dialog = gtk_dialog_new(); gtk_window_set_title(GTK_WINDOW(dialog), title ? title : "Notifications"); gtk_window_set_default_size(GTK_WINDOW(dialog), 600, 400); gtk_window_set_modal(GTK_WINDOW(dialog), TRUE); gtk_dialog_add_button(GTK_DIALOG(dialog), "Copy", ACTION_COPY); dismiss_button = gtk_dialog_add_button(GTK_DIALOG(dialog), "Dismiss Notification", ACTION_DISMISS); gtk_dialog_add_button(GTK_DIALOG(dialog), "Clear", ACTION_CLEAR); gtk_dialog_add_button(GTK_DIALOG(dialog), "Close", ACTION_CLOSE); gtk_dialog_set_default_response(GTK_DIALOG(dialog), ACTION_CLOSE); g_signal_connect(dialog, "response", G_CALLBACK(response), NULL); GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); GtkWidget *grid = gtk_grid_new(); gtk_grid_set_row_spacing(GTK_GRID(grid), 12); gtk_container_add(GTK_CONTAINER(content), grid); GtkWidget *scrolled = gtk_scrolled_window_new(NULL, NULL); gtk_widget_set_hexpand(scrolled, TRUE); gtk_widget_set_vexpand(scrolled, TRUE); gtk_container_add(GTK_CONTAINER(grid), scrolled); model = gtk_list_store_new(3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); GtkWidget *tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(model)); gtk_widget_set_hexpand(tree, TRUE); gtk_widget_set_vexpand(tree, TRUE); AtkObject *tree_accessible = gtk_widget_get_accessible(tree); if (tree_accessible) atk_object_set_name(tree_accessible, title ? title : "Notifications"); for (int column = 0; column < 2; column++) { const char *header = column == 0 ? message_header : received_header; GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); GtkTreeViewColumn *view = gtk_tree_view_column_new_with_attributes(header, renderer, "text", column, NULL); if (header && header[0]) gtk_tree_view_column_set_sort_column_id(view, column); gtk_tree_view_append_column(GTK_TREE_VIEW(tree), view); } selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE); g_signal_connect(selection, "changed", G_CALLBACK(selection_changed), NULL); gtk_container_add(GTK_CONTAINER(scrolled), tree); GtkWidget *frame = gtk_frame_new("Notification Actions"); gtk_frame_set_label_align(GTK_FRAME(frame), 0.0, 0.5); AtkObject *frame_accessible = gtk_widget_get_accessible(frame); if (frame_accessible) atk_object_set_name(frame_accessible, "Notification Actions"); gtk_grid_attach_next_to(GTK_GRID(grid), frame, scrolled, GTK_POS_BOTTOM, 1, 1); GtkWidget *actions_content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); g_object_set(actions_content, "margin-top", 6, "margin-bottom", 6, "margin-start", 6, "margin-end", 6, NULL); gtk_container_add(GTK_CONTAINER(frame), actions_content); actions_status = gtk_label_new(""); gtk_label_set_xalign(GTK_LABEL(actions_status), 0); gtk_label_set_line_wrap(GTK_LABEL(actions_status), TRUE); gtk_widget_set_no_show_all(actions_status, TRUE); gtk_box_pack_start(GTK_BOX(actions_content), actions_status, FALSE, FALSE, 0); actions_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); gtk_box_pack_start(GTK_BOX(actions_content), actions_box, FALSE, FALSE, 0); return 1; } void notification_list_set_entries(char **messages, char **received, int count, char **ids, const char *selected, int can_control, char **action_keys, char **action_labels, int action_count) { if (!model) return; suppress_selection = 1; gtk_list_store_clear(model); for (int index = 0; index < count; index++) { GtkTreeIter iterator; gtk_list_store_append(model, &iterator); gtk_list_store_set(model, &iterator, 0, messages && messages[index] ? messages[index] : "", 1, received && received[index] ? received[index] : "", 2, ids && ids[index] ? ids[index] : "", -1); } GtkTreeIter iterator; gboolean found = FALSE; if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iterator)) { do { char *value = NULL; gtk_tree_model_get(GTK_TREE_MODEL(model), &iterator, 2, &value, -1); if (value && selected && !g_strcmp0(value, selected)) found = TRUE; g_free(value); if (found) { GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(model), &iterator); gtk_tree_selection_select_path(selection, path); gtk_tree_path_free(path); break; } } while (gtk_tree_model_iter_next(GTK_TREE_MODEL(model), &iterator)); } if (!found) gtk_tree_selection_unselect_all(selection); gtk_widget_set_sensitive(dismiss_button, can_control != 0); if (!can_control) { clear_action_buttons(); gtk_label_set_text(GTK_LABEL(actions_status), "Notification is no longer available"); gtk_widget_show(actions_status); } else if (!action_count) { clear_action_buttons(); gtk_label_set_text(GTK_LABEL(actions_status), "No notification actions available"); gtk_widget_show(actions_status); } else { set_actions(action_keys, action_labels, action_count); gtk_label_set_text(GTK_LABEL(actions_status), ""); gtk_widget_hide(actions_status); } suppress_selection = 0; } void notification_list_show(void) { if (!dialog) return; gtk_widget_show_all(dialog); gtk_window_present(GTK_WINDOW(dialog)); } void notification_list_present(void) { if (!dialog) return; gtk_window_present(GTK_WINDOW(dialog)); } void notification_list_iterate(void) { while (gtk_events_pending()) gtk_main_iteration(); } int notification_list_action(void) { int result = action_code; action_code = ACTION_NONE; return result; } int notification_list_selected(void) { GtkTreeIter iterator; GtkTreeModel *selected_model = NULL; if (!selection || !gtk_tree_selection_get_selected(selection, &selected_model, &iterator)) return -1; GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(model), &iterator); if (!path) return -1; int *indices = gtk_tree_path_get_indices(path); int selected = indices && indices[0] >= 0 ? indices[0] : -1; gtk_tree_path_free(path); return selected; } int notification_list_selected_row(void) { int selected = notification_list_selected(); return selected >= 0 ? selected : 0; } const char *notification_list_selected_id(void) { GtkTreeIter iterator; GtkTreeModel *selected_model = NULL; if (!selection || !gtk_tree_selection_get_selected(selection, &selected_model, &iterator)) return ""; char *identifier = NULL; gtk_tree_model_get(selected_model, &iterator, 2, &identifier, -1); g_free(selected_id_cache); selected_id_cache = identifier ? identifier : g_strdup(""); return selected_id_cache; } const char *notification_list_selected_action(void) { return selected_action_key ? selected_action_key : ""; } int notification_list_copy(void) { GtkTreeIter iterator; GtkTreeModel *selected_model = NULL; if (!selection || !gtk_tree_selection_get_selected(selection, &selected_model, &iterator)) return 0; char *message = NULL; char *received = NULL; gtk_tree_model_get(GTK_TREE_MODEL(model), &iterator, 0, &message, 1, &received, -1); char *text = g_strjoin("\t", message ? message : "", received && received[0] ? received : NULL, NULL); GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); gtk_clipboard_set_text(clipboard, text, -1); gtk_clipboard_store(clipboard); g_free(text); g_free(message); g_free(received); return 1; } void notification_list_close(void) { if (!dialog) return; gtk_widget_destroy(dialog); dialog = NULL; model = NULL; selection = NULL; dismiss_button = NULL; actions_box = NULL; actions_status = NULL; g_free(selected_action_key); selected_action_key = NULL; g_free(selected_id_cache); selected_id_cache = NULL; }