222 lines
9.4 KiB
C
222 lines
9.4 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 { NONE, COPY, REMOVE, CLOSE, CLEAR };
|
|
static GtkWidget *window, *filter, *tree, *text_view;
|
|
static GtkListStore *store;
|
|
static GtkTreeModel *filtered;
|
|
static GtkTreeSelection *selection;
|
|
static int action_code, monitor_mode, line_count;
|
|
static char *selected_cache;
|
|
|
|
static void action(GtkButton *button, gpointer code) {
|
|
(void)button;
|
|
if (!action_code) action_code = GPOINTER_TO_INT(code);
|
|
}
|
|
static gboolean deleted(GtkWidget *widget, GdkEvent *event, gpointer data) {
|
|
(void)widget; (void)event; (void)data;
|
|
if (!action_code) action_code = CLOSE;
|
|
return TRUE;
|
|
}
|
|
static gboolean key_press(GtkWidget *widget, GdkEventKey *event, gpointer data) {
|
|
(void)widget; (void)data;
|
|
if (event->keyval == GDK_KEY_Escape) { action_code = CLOSE; return TRUE; }
|
|
if (monitor_mode || !filter || !gtk_widget_has_focus(tree)) return FALSE;
|
|
if (event->keyval == GDK_KEY_BackSpace) {
|
|
const char *current = gtk_entry_get_text(GTK_ENTRY(filter));
|
|
glong length = g_utf8_strlen(current, -1);
|
|
if (length > 0) {
|
|
char *shorter = g_utf8_substring(current, 0, length - 1);
|
|
gtk_entry_set_text(GTK_ENTRY(filter), shorter);
|
|
gtk_editable_set_position(GTK_EDITABLE(filter), -1);
|
|
g_free(shorter);
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
if (event->state & (GDK_CONTROL_MASK | GDK_MOD1_MASK | GDK_SUPER_MASK | GDK_META_MASK)) return FALSE;
|
|
gunichar ch = gdk_keyval_to_unicode(event->keyval);
|
|
if (!ch || !g_unichar_isprint(ch)) return FALSE;
|
|
char utf8[7] = {0};
|
|
g_unichar_to_utf8(ch, utf8);
|
|
char *updated = g_strconcat(gtk_entry_get_text(GTK_ENTRY(filter)), utf8, NULL);
|
|
gtk_entry_set_text(GTK_ENTRY(filter), updated);
|
|
gtk_editable_set_position(GTK_EDITABLE(filter), -1);
|
|
gtk_widget_grab_focus(filter);
|
|
g_free(updated);
|
|
return TRUE;
|
|
}
|
|
static gboolean visible(GtkTreeModel *model, GtkTreeIter *iter, gpointer data) {
|
|
(void)data;
|
|
char *query = g_strstrip(g_strdup(gtk_entry_get_text(GTK_ENTRY(filter))));
|
|
if (!query[0]) { g_free(query); return TRUE; }
|
|
char *value = NULL;
|
|
gtk_tree_model_get(model, iter, 0, &value, -1);
|
|
char *folded_value = g_utf8_casefold(value ? value : "", -1);
|
|
char *folded_query = g_utf8_casefold(query, -1);
|
|
gboolean match = g_str_has_prefix(folded_value, folded_query);
|
|
g_free(value); g_free(query); g_free(folded_value); g_free(folded_query);
|
|
return match;
|
|
}
|
|
static void filter_changed(GtkEditable *editable, gpointer data) {
|
|
(void)editable; (void)data;
|
|
gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(filtered));
|
|
GtkTreeIter first;
|
|
if (gtk_tree_model_get_iter_first(filtered, &first)) {
|
|
GtkTreePath *path = gtk_tree_model_get_path(filtered, &first);
|
|
gtk_tree_selection_select_path(selection, path);
|
|
gtk_tree_path_free(path);
|
|
} else gtk_tree_selection_unselect_all(selection);
|
|
}
|
|
static GtkWidget *button(GtkWidget *box, const char *label, int code) {
|
|
GtkWidget *widget = gtk_button_new_with_label(label);
|
|
g_signal_connect(widget, "clicked", G_CALLBACK(action), GINT_TO_POINTER(code));
|
|
gtk_box_pack_start(GTK_BOX(box), widget, FALSE, FALSE, 0);
|
|
return widget;
|
|
}
|
|
int history_init(int monitor) {
|
|
if (!gtk_init_check(NULL, NULL)) return 0;
|
|
monitor_mode = monitor;
|
|
line_count = 0;
|
|
g_set_application_name(monitor ? "Cthulhu Speech Monitor" : "Cthulhu Speech History");
|
|
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
|
gtk_window_set_title(GTK_WINDOW(window), monitor ? "Speech Monitor - Cthulhu" : "Speech History - Cthulhu");
|
|
gtk_window_set_default_size(GTK_WINDOW(window), monitor ? 900 : 700, monitor ? 320 : 420);
|
|
gtk_container_set_border_width(GTK_CONTAINER(window), 10);
|
|
if (monitor) {
|
|
gtk_window_set_keep_above(GTK_WINDOW(window), TRUE);
|
|
gtk_window_set_focus_on_map(GTK_WINDOW(window), FALSE);
|
|
gtk_window_set_type_hint(GTK_WINDOW(window), GDK_WINDOW_TYPE_HINT_UTILITY);
|
|
} else gtk_window_set_modal(GTK_WINDOW(window), TRUE);
|
|
g_signal_connect(window, "delete-event", G_CALLBACK(deleted), NULL);
|
|
g_signal_connect(window, "key-press-event", G_CALLBACK(key_press), NULL);
|
|
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
|
gtk_container_add(GTK_CONTAINER(window), box);
|
|
if (!monitor) {
|
|
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
|
GtkWidget *label = gtk_label_new_with_mnemonic("_Filter:");
|
|
filter = gtk_entry_new();
|
|
gtk_widget_set_hexpand(filter, TRUE);
|
|
gtk_label_set_mnemonic_widget(GTK_LABEL(label), filter);
|
|
gtk_box_pack_start(GTK_BOX(row), label, FALSE, FALSE, 0);
|
|
gtk_box_pack_start(GTK_BOX(row), filter, TRUE, TRUE, 0);
|
|
gtk_box_pack_start(GTK_BOX(box), row, FALSE, FALSE, 0);
|
|
store = gtk_list_store_new(1, G_TYPE_STRING);
|
|
filtered = gtk_tree_model_filter_new(GTK_TREE_MODEL(store), NULL);
|
|
gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(filtered), visible, NULL, NULL);
|
|
g_signal_connect(filter, "changed", G_CALLBACK(filter_changed), NULL);
|
|
}
|
|
GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
|
|
gtk_widget_set_vexpand(scroll, TRUE);
|
|
gtk_box_pack_start(GTK_BOX(box), scroll, TRUE, TRUE, 0);
|
|
if (monitor) {
|
|
text_view = gtk_text_view_new();
|
|
gtk_text_view_set_editable(GTK_TEXT_VIEW(text_view), FALSE);
|
|
gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(text_view), FALSE);
|
|
gtk_widget_set_can_focus(text_view, FALSE);
|
|
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text_view), GTK_WRAP_WORD_CHAR);
|
|
AtkObject *accessible = gtk_widget_get_accessible(text_view);
|
|
if (accessible) atk_object_set_name(accessible, "Speech monitor output");
|
|
gtk_container_add(GTK_CONTAINER(scroll), text_view);
|
|
} else {
|
|
tree = gtk_tree_view_new_with_model(filtered);
|
|
GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
|
|
g_object_set(renderer, "wrap-width", 640, "wrap-mode", PANGO_WRAP_WORD_CHAR, NULL);
|
|
GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes("Spoken Text", renderer, "text", 0, NULL);
|
|
gtk_tree_view_column_set_expand(column, TRUE);
|
|
gtk_tree_view_column_set_resizable(column, TRUE);
|
|
gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
|
|
selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
|
|
gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
|
|
gtk_container_add(GTK_CONTAINER(scroll), tree);
|
|
}
|
|
GtkWidget *buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
|
gtk_widget_set_halign(buttons, GTK_ALIGN_END);
|
|
gtk_box_pack_start(GTK_BOX(box), buttons, FALSE, FALSE, 0);
|
|
if (monitor) button(buttons, "Clear", CLEAR);
|
|
else {
|
|
button(buttons, "Copy to clipboard", COPY);
|
|
button(buttons, "Remove from history", REMOVE);
|
|
}
|
|
button(buttons, "Close", CLOSE);
|
|
gtk_widget_show_all(window);
|
|
if (!monitor) gtk_widget_grab_focus(filter);
|
|
return 1;
|
|
}
|
|
void history_set_items(char **items, int count, int selected) {
|
|
if (!store) return;
|
|
gtk_list_store_clear(store);
|
|
for (int i = 0; i < count; i++) {
|
|
GtkTreeIter iter;
|
|
gtk_list_store_append(store, &iter);
|
|
gtk_list_store_set(store, &iter, 0, items[i], -1);
|
|
}
|
|
if (count > 0) {
|
|
GtkTreePath *path = gtk_tree_path_new_from_indices(selected >= 0 && selected < count ? selected : 0, -1);
|
|
gtk_tree_selection_select_path(selection, path);
|
|
gtk_tree_path_free(path);
|
|
gtk_widget_grab_focus(tree);
|
|
} else gtk_widget_grab_focus(filter);
|
|
}
|
|
void history_append(const char *text) {
|
|
if (!text_view || !text) return;
|
|
GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view));
|
|
GtkTextIter end;
|
|
gtk_text_buffer_get_end_iter(buffer, &end);
|
|
gtk_text_buffer_insert(buffer, &end, text, -1);
|
|
gtk_text_buffer_insert(buffer, &end, "\n", 1);
|
|
if (++line_count > 500) {
|
|
GtkTextIter start, cut;
|
|
gtk_text_buffer_get_start_iter(buffer, &start);
|
|
gtk_text_buffer_get_iter_at_line(buffer, &cut, line_count - 500);
|
|
gtk_text_buffer_delete(buffer, &start, &cut);
|
|
line_count = 500;
|
|
}
|
|
gtk_text_buffer_get_end_iter(buffer, &end);
|
|
GtkTextMark *mark = gtk_text_buffer_create_mark(buffer, NULL, &end, FALSE);
|
|
gtk_text_view_scroll_mark_onscreen(GTK_TEXT_VIEW(text_view), mark);
|
|
gtk_text_buffer_delete_mark(buffer, mark);
|
|
}
|
|
void history_present(void) { if (window) gtk_window_present(GTK_WINDOW(window)); }
|
|
void history_iterate(void) { while (gtk_events_pending()) gtk_main_iteration(); }
|
|
int history_action(void) { int result = action_code; action_code = NONE; return result; }
|
|
const char *history_selected(void) {
|
|
GtkTreeIter iter; GtkTreeModel *model = NULL;
|
|
if (!selection || !gtk_tree_selection_get_selected(selection, &model, &iter)) return "";
|
|
g_free(selected_cache);
|
|
gtk_tree_model_get(model, &iter, 0, &selected_cache, -1);
|
|
return selected_cache ? selected_cache : "";
|
|
}
|
|
int history_selected_row(void) {
|
|
GtkTreeIter iter; GtkTreeModel *model = NULL;
|
|
if (!selection || !gtk_tree_selection_get_selected(selection, &model, &iter)) return 0;
|
|
GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
|
|
int *indices = gtk_tree_path_get_indices(path);
|
|
int row = indices ? indices[0] : 0;
|
|
gtk_tree_path_free(path);
|
|
return row;
|
|
}
|
|
int history_copy(void) {
|
|
const char *text = history_selected();
|
|
if (!text || !text[0]) return 0;
|
|
GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
|
|
gtk_clipboard_set_text(clipboard, text, -1);
|
|
gtk_clipboard_store(clipboard);
|
|
return 1;
|
|
}
|
|
void history_clear(void) {
|
|
if (!text_view) return;
|
|
gtk_text_buffer_set_text(gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view)), "", 0);
|
|
line_count = 0;
|
|
}
|
|
void history_close(void) {
|
|
if (window) gtk_widget_destroy(window);
|
|
window = filter = tree = text_view = NULL;
|
|
store = NULL; filtered = NULL; selection = NULL;
|
|
g_free(selected_cache); selected_cache = NULL;
|
|
}
|