Files

264 lines
11 KiB
C

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
#include "native.h"
#include <gtk/gtk.h>
#include <gdk/gdk.h>
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;
}