533 lines
16 KiB
C
533 lines
16 KiB
C
#include "console_window.h"
|
|
#include "util.h"
|
|
#include <stdbool.h>
|
|
|
|
struct Selection {
|
|
gint start;
|
|
gint end;
|
|
};
|
|
|
|
struct _ConsoleWindow {
|
|
GtkWindowClass parent_class;
|
|
|
|
GtkEntry *input;
|
|
GtkTextView *output;
|
|
GtkTextView *sidebar_input;
|
|
GtkTextView *sidebar_output;
|
|
|
|
GAsyncQueue *input_queue;
|
|
GAsyncQueue *output_queue;
|
|
|
|
bool should_clear;
|
|
bool log_to_sidebar;
|
|
bool clear_sidebar;
|
|
|
|
GtkEntryCompletion *command_completion;
|
|
guint command_history_len;
|
|
gint command_history_index;
|
|
|
|
struct Selection auto_complete_range;
|
|
uintptr_t auto_complete_context;
|
|
bool ignore_auto_complete_context_reset;
|
|
|
|
bool developer_mode;
|
|
|
|
GB_gameboy_t *gb;
|
|
};
|
|
|
|
G_DEFINE_TYPE(ConsoleWindow, console_window, GTK_TYPE_WINDOW);
|
|
|
|
typedef enum {
|
|
PROP_GB_PTR = 1,
|
|
|
|
N_PROPERTIES
|
|
} ConsoleWindowProperty;
|
|
|
|
static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };
|
|
|
|
typedef struct {
|
|
const char *message;
|
|
GB_log_attributes attributes;
|
|
bool sidebar;
|
|
} AttributedMessage;
|
|
|
|
static void console_window_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) {
|
|
ConsoleWindow *self = (ConsoleWindow *) object;
|
|
|
|
switch ((ConsoleWindowProperty) property_id) {
|
|
case PROP_GB_PTR: self->gb = g_value_get_pointer(value); break;
|
|
default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void console_window_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) {
|
|
ConsoleWindow *self = (ConsoleWindow *) object;
|
|
|
|
switch ((ConsoleWindowProperty) property_id) {
|
|
case PROP_GB_PTR: g_value_set_pointer(value, self->gb); break;
|
|
default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
|
|
}
|
|
}
|
|
|
|
|
|
static gboolean on_input_key_press(GtkEntry *input, GdkEventKey *event, ConsoleWindow *self) {
|
|
switch (event->keyval) {
|
|
case GDK_KEY_Up:
|
|
if (event->type == GDK_KEY_PRESS) {
|
|
if (self->command_history_index + 1 == self->command_history_len) {
|
|
self->command_history_index = self->command_history_len - 1;
|
|
return true;
|
|
}
|
|
|
|
self->command_history_index += 1;
|
|
|
|
GtkTreeIter iter;
|
|
GtkTreeModel *model = gtk_entry_completion_get_model(self->command_completion);
|
|
|
|
if (!gtk_tree_model_iter_nth_child(model, &iter, NULL, self->command_history_index)) {
|
|
return true;
|
|
}
|
|
|
|
const char *entry = NULL;
|
|
gtk_tree_model_get(model, &iter, 0, &entry, -1);
|
|
gtk_entry_set_text(input, entry);
|
|
gtk_editable_set_position(GTK_EDITABLE(input), -1);
|
|
}
|
|
|
|
return true;
|
|
break;
|
|
|
|
case GDK_KEY_Down:
|
|
if (event->type == GDK_KEY_PRESS) {
|
|
if (self->command_history_index <= 0) {
|
|
gtk_entry_set_text(input, "");
|
|
self->command_history_index = -1;
|
|
}
|
|
|
|
if (self->command_history_index == -1) {
|
|
return true;
|
|
}
|
|
|
|
self->command_history_index -= 1;
|
|
|
|
GtkTreeIter iter;
|
|
GtkTreeModel *model = gtk_entry_completion_get_model(self->command_completion);
|
|
|
|
if (!gtk_tree_model_iter_nth_child(model, &iter, NULL, self->command_history_index)) {
|
|
return true;
|
|
}
|
|
|
|
const char *entry = NULL;
|
|
gtk_tree_model_get(model, &iter, 0, &entry, -1);
|
|
gtk_entry_set_text(input, entry);
|
|
gtk_editable_set_position(GTK_EDITABLE(input), -1);
|
|
}
|
|
|
|
return true;
|
|
break;
|
|
|
|
case GDK_KEY_Tab:
|
|
if (event->type == GDK_KEY_PRESS) {
|
|
if (self->auto_complete_context == 0) {
|
|
gint start_pos;
|
|
gint end_pos;
|
|
gtk_editable_get_selection_bounds(GTK_EDITABLE(input), &start_pos, &end_pos);
|
|
|
|
if (start_pos != end_pos) {
|
|
self->ignore_auto_complete_context_reset = true;
|
|
gtk_editable_delete_text(GTK_EDITABLE(input), start_pos, end_pos);
|
|
self->ignore_auto_complete_context_reset = false;
|
|
}
|
|
|
|
self->auto_complete_range = (struct Selection){
|
|
.start = start_pos,
|
|
.end = start_pos
|
|
};
|
|
}
|
|
|
|
gchar *substring = gtk_editable_get_chars(GTK_EDITABLE(input), 0, self->auto_complete_range.start);
|
|
|
|
uintptr_t context = self->auto_complete_context;
|
|
char *completion = GB_debugger_complete_substring(self->gb, substring, &context);
|
|
g_free(substring);
|
|
|
|
if (completion) {
|
|
self->ignore_auto_complete_context_reset = true;
|
|
|
|
gtk_editable_select_region(GTK_EDITABLE(input), self->auto_complete_range.start, self->auto_complete_range.end);
|
|
gint new_end = self->auto_complete_range.start;
|
|
gtk_editable_delete_text(GTK_EDITABLE(input), self->auto_complete_range.start, self->auto_complete_range.end);
|
|
gtk_editable_insert_text(GTK_EDITABLE(input), completion, -1, &new_end);
|
|
self->auto_complete_range.end = new_end;
|
|
|
|
gtk_editable_set_position(GTK_EDITABLE(input), self->auto_complete_range.end);
|
|
|
|
self->ignore_auto_complete_context_reset = false;
|
|
}
|
|
else {
|
|
g_debug("BEEP (no completion found)");
|
|
gdk_display_beep(gdk_display_get_default());
|
|
}
|
|
|
|
self->auto_complete_context = context;
|
|
}
|
|
|
|
return true;
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void reset_auto_completion_context(GtkEntry *input, ConsoleWindow *self) {
|
|
if (self->ignore_auto_complete_context_reset) {
|
|
return;
|
|
}
|
|
|
|
self->auto_complete_context = 0;
|
|
}
|
|
|
|
static void on_delete_text(GtkEditable *editable, int start_pos, int end_pos, ConsoleWindow *self) {
|
|
reset_auto_completion_context(GTK_ENTRY(editable), self);
|
|
}
|
|
|
|
static void on_insert_text(GtkEditable *editable, char*new_text, int new_text_length, gpointer position, ConsoleWindow *self) {
|
|
reset_auto_completion_context(GTK_ENTRY(editable), self);
|
|
}
|
|
|
|
static void on_move_cursor(GtkEntry *input, GtkMovementStep step, int count, gboolean extend_selection, ConsoleWindow *self) {
|
|
reset_auto_completion_context(input, self);
|
|
}
|
|
|
|
static void console_window_init(ConsoleWindow *self) {
|
|
gtk_widget_init_template(GTK_WIDGET(self));
|
|
|
|
GtkTextBuffer *text_buf = gtk_text_view_get_buffer(self->output);
|
|
GtkTextTagTable *tag_table = gtk_text_buffer_get_tag_table(text_buf);
|
|
|
|
gtk_text_buffer_create_tag(text_buf, "bold", "weight", PANGO_WEIGHT_BOLD, NULL);
|
|
gtk_text_buffer_create_tag(text_buf, "underline", "underline", PANGO_UNDERLINE_SINGLE, "underline-set", true, NULL);
|
|
gtk_text_buffer_create_tag(text_buf, "dashed_underline", "underline", PANGO_UNDERLINE_DOUBLE, "underline-set", true, NULL);
|
|
|
|
gtk_text_view_set_buffer(
|
|
self->sidebar_output,
|
|
gtk_text_buffer_new(tag_table)
|
|
);
|
|
|
|
gtk_text_buffer_set_text(gtk_text_view_get_buffer(self->sidebar_input), "registers\nbacktrace\n", -1);
|
|
|
|
self->input_queue = g_async_queue_new();
|
|
self->output_queue = g_async_queue_new();
|
|
self->log_to_sidebar = false;
|
|
self->clear_sidebar = false;
|
|
self->command_history_index = -1;
|
|
|
|
GtkTreeIter iter;
|
|
GtkListStore *command_list_store = gtk_list_store_new(1, G_TYPE_STRING);
|
|
self->command_completion = gtk_entry_completion_new();
|
|
gtk_entry_completion_set_model(self->command_completion, GTK_TREE_MODEL(command_list_store));
|
|
gtk_entry_completion_set_text_column(self->command_completion, 0);
|
|
gtk_entry_completion_set_popup_completion(self->command_completion, false);
|
|
gtk_entry_completion_set_inline_completion(self->command_completion, false);
|
|
gtk_entry_set_completion(self->input, self->command_completion);
|
|
gtk_entry_set_input_hints(self->input, GTK_INPUT_HINT_NO_SPELLCHECK | GTK_INPUT_HINT_NO_EMOJI);
|
|
|
|
gtk_widget_add_events(GTK_WIDGET(self), GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK);
|
|
|
|
g_signal_connect(self->input, "key-press-event", G_CALLBACK(on_input_key_press), self);
|
|
g_signal_connect(self->input, "key-release-event", G_CALLBACK(on_input_key_press), self);
|
|
g_signal_connect(self->input, "delete-text", G_CALLBACK(on_delete_text), self);
|
|
g_signal_connect(self->input, "insert-text", G_CALLBACK(on_insert_text), self);
|
|
g_signal_connect(self->input, "move-cursor", G_CALLBACK(on_move_cursor), self);
|
|
|
|
gtk_widget_grab_focus(GTK_WIDGET(self->input));
|
|
}
|
|
|
|
static void console_window_realize(GtkWidget *widget) {
|
|
ConsoleWindow *self = (ConsoleWindow *)widget;
|
|
|
|
GTK_WIDGET_CLASS(console_window_parent_class)->realize(widget);
|
|
}
|
|
|
|
// Takes ownership of message
|
|
static void log_simple(ConsoleWindow *self, const char *message) {
|
|
AttributedMessage *attr_msg = g_new(AttributedMessage, 1);
|
|
attr_msg->message = message;
|
|
attr_msg->attributes = 0;
|
|
attr_msg->sidebar = false;
|
|
|
|
g_async_queue_push(self->output_queue, attr_msg);
|
|
}
|
|
|
|
// TODO: Use command history (arrow key (↑, ↓) events)
|
|
static void on_input_enter(GtkEntry *input, ConsoleWindow *self) {
|
|
const gchar *text_ptr = gtk_entry_get_text(input);
|
|
const gchar *text = NULL;
|
|
|
|
GtkTreeModel *model = gtk_entry_completion_get_model(self->command_completion);
|
|
|
|
if (g_strcmp0("", text_ptr) == 0) {
|
|
const char *last = NULL;
|
|
GtkTreeIter iter;
|
|
if (gtk_tree_model_get_iter_first(model, &iter)) {
|
|
gtk_tree_model_get(model, &iter, 0, &last, -1);
|
|
}
|
|
|
|
if (last) text = last;
|
|
}
|
|
else {
|
|
text = text_ptr;
|
|
}
|
|
|
|
if (text) {
|
|
const char *last = NULL;
|
|
|
|
GtkTreeIter iter;
|
|
if (gtk_tree_model_get_iter_first(model, &iter)) {
|
|
gtk_tree_model_get(model, &iter, 0, &last, -1);
|
|
}
|
|
|
|
// Add command to queue unless it was the last command issued
|
|
if (!last || g_strcmp0(last, text) != 0) {
|
|
gtk_list_store_prepend(GTK_LIST_STORE(model), &iter);
|
|
gtk_list_store_set(GTK_LIST_STORE(model), &iter, 0, text, -1);
|
|
self->command_history_len += 1;
|
|
}
|
|
|
|
g_async_queue_push(self->input_queue, (gpointer) g_strdup(text));
|
|
gtk_entry_set_text(self->input, "");
|
|
|
|
self->command_history_index = -1;
|
|
}
|
|
}
|
|
|
|
static void update_sidebar(ConsoleWindow *self, GB_gameboy_t *gb) {
|
|
if (!GB_debugger_is_stopped(gb)) {
|
|
return;
|
|
}
|
|
|
|
GtkTextBuffer *text_buf = gtk_text_view_get_buffer(self->sidebar_input);
|
|
gint line_count = gtk_text_buffer_get_line_count(text_buf);
|
|
|
|
self->log_to_sidebar = true;
|
|
self->clear_sidebar = true;
|
|
|
|
for (unsigned line = 0; line < line_count; ++line) {
|
|
GtkTextIter start_iter;
|
|
GtkTextIter end_iter;
|
|
gunichar ch;
|
|
|
|
gtk_text_buffer_get_iter_at_line(text_buf, &start_iter, line);
|
|
end_iter = start_iter;
|
|
|
|
do {
|
|
ch = gtk_text_iter_get_char(&end_iter);
|
|
if (!gtk_text_iter_forward_char(&end_iter)) {
|
|
break;
|
|
}
|
|
}
|
|
while (ch != '\n');
|
|
|
|
gchar *cmd = gtk_text_buffer_get_text(text_buf, &start_iter, &end_iter, false);
|
|
g_strchug(cmd); // trim leading whitespace
|
|
g_strchomp(cmd); // trim trailing whitespace
|
|
|
|
if (g_strcmp0("", cmd) != 0) {
|
|
char *duped = g_strdup(cmd);
|
|
GB_attributed_log(gb, GB_LOG_BOLD, "%s:\n", duped);
|
|
GB_debugger_execute_command(gb, duped);
|
|
GB_log(gb, "\n");
|
|
g_free(duped);
|
|
}
|
|
|
|
g_free(cmd);
|
|
}
|
|
|
|
self->log_to_sidebar = false;
|
|
}
|
|
|
|
static gboolean console_window_draw(GtkWidget *widget, cairo_t *cr) {
|
|
ConsoleWindow *self = (ConsoleWindow *)widget;
|
|
GtkTextBuffer *main_text_buf = gtk_text_view_get_buffer(self->output);
|
|
GtkTextBuffer *sidebar_text_buf = gtk_text_view_get_buffer(self->sidebar_output);
|
|
|
|
if (self->should_clear) {
|
|
gtk_text_buffer_set_text(main_text_buf, "", -1);
|
|
gtk_text_buffer_set_text(sidebar_text_buf, "", -1);
|
|
|
|
// clear pending log messages
|
|
while (g_async_queue_try_pop(self->output_queue));
|
|
|
|
self->should_clear = false;
|
|
}
|
|
else {
|
|
GtkTextIter iter;
|
|
GtkTextIter start;
|
|
AttributedMessage *attr_msg = NULL;
|
|
|
|
GtkTextIter main_scroll_iter;
|
|
bool scroll_main = false;
|
|
|
|
GtkTextIter sidebar_scroll_iter;
|
|
bool scroll_sidebar = false;
|
|
|
|
if (self->clear_sidebar) {
|
|
gtk_text_buffer_set_text(sidebar_text_buf, "", -1);
|
|
self->clear_sidebar = false;
|
|
}
|
|
|
|
while ((attr_msg = g_async_queue_try_pop(self->output_queue))) {
|
|
GtkTextBuffer *text_buf = attr_msg->sidebar? sidebar_text_buf : main_text_buf;
|
|
|
|
gtk_text_buffer_get_end_iter(text_buf, &iter);
|
|
GtkTextMark *start_mark = gtk_text_buffer_create_mark(text_buf, NULL, &iter, true);
|
|
|
|
// give ownership of message to the text buffer
|
|
gtk_text_buffer_insert(text_buf, &iter, attr_msg->message, -1);
|
|
gtk_text_buffer_get_iter_at_mark(text_buf, &start, start_mark);
|
|
|
|
if (attr_msg->attributes & GB_LOG_BOLD) {
|
|
gtk_text_buffer_apply_tag_by_name(text_buf, "bold", &start, &iter);
|
|
}
|
|
|
|
if (attr_msg->attributes & GB_LOG_DASHED_UNDERLINE) {
|
|
gtk_text_buffer_apply_tag_by_name(text_buf, "dashed_underline", &start, &iter);
|
|
}
|
|
|
|
if (attr_msg->attributes & GB_LOG_UNDERLINE) {
|
|
gtk_text_buffer_apply_tag_by_name(text_buf, "underline", &start, &iter);
|
|
}
|
|
|
|
gtk_text_buffer_delete_mark(text_buf, start_mark);
|
|
|
|
if (attr_msg->sidebar) {
|
|
scroll_sidebar = true;
|
|
sidebar_scroll_iter = iter;
|
|
}
|
|
else {
|
|
scroll_main = true;
|
|
main_scroll_iter = iter;
|
|
}
|
|
|
|
g_free(attr_msg);
|
|
}
|
|
|
|
if (scroll_main) {
|
|
scroll_to_bottom(self->output, gtk_text_buffer_create_mark(main_text_buf, NULL, &main_scroll_iter, true));
|
|
}
|
|
|
|
if (scroll_sidebar) {
|
|
scroll_to_bottom(self->sidebar_output, gtk_text_buffer_create_mark(sidebar_text_buf, NULL, &sidebar_scroll_iter, true));
|
|
}
|
|
}
|
|
|
|
return GTK_WIDGET_CLASS(console_window_parent_class)->draw(widget, cr);
|
|
}
|
|
|
|
static void console_window_class_init(ConsoleWindowClass *class) {
|
|
gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS(class), RESOURCE_PREFIX "ui/console_window.ui");
|
|
|
|
gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), ConsoleWindow, input);
|
|
gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), ConsoleWindow, output);
|
|
gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), ConsoleWindow, sidebar_input);
|
|
gtk_widget_class_bind_template_child(GTK_WIDGET_CLASS(class), ConsoleWindow, sidebar_output);
|
|
|
|
gtk_widget_class_bind_template_callback(GTK_WIDGET_CLASS(class), on_input_enter);
|
|
|
|
GTK_WIDGET_CLASS(class)->realize = console_window_realize;
|
|
GTK_WIDGET_CLASS(class)->draw = console_window_draw;
|
|
|
|
obj_properties[PROP_GB_PTR] = g_param_spec_pointer(
|
|
"gb", "SameBoy core pointer", "SameBoy Core pointer (GB_gameboy_t)",
|
|
G_PARAM_CONSTRUCT | G_PARAM_READWRITE
|
|
);
|
|
|
|
G_OBJECT_CLASS(class)->set_property = console_window_set_property;
|
|
G_OBJECT_CLASS(class)->get_property = console_window_get_property;
|
|
|
|
g_object_class_install_properties(G_OBJECT_CLASS(class), N_PROPERTIES, obj_properties);
|
|
}
|
|
|
|
ConsoleWindow *console_window_new(GB_gameboy_t *gb) {
|
|
return g_object_new(CONSOLE_WINDOW_TYPE, "gb", gb, NULL);
|
|
}
|
|
|
|
// This function gets called every VBlank while the emulation is running.
|
|
char *console_get_async_input(ConsoleWindow *self, GB_gameboy_t *gb) {
|
|
self->clear_sidebar = true;
|
|
|
|
char *command = (char *)g_async_queue_try_pop(self->input_queue);
|
|
|
|
if (command) {
|
|
gchar *msg = g_strdup_printf("> %s\n", command);
|
|
log_simple(self, msg);
|
|
}
|
|
|
|
return command;
|
|
}
|
|
|
|
// This will only be called if the debugger is in stopped mode (after a breakpoint hit for example),
|
|
// thus we block the emulation thread until input is available.
|
|
char *console_get_sync_input(ConsoleWindow *self, GB_gameboy_t *gb) {
|
|
update_sidebar(self, gb);
|
|
|
|
char *command = (char *)g_async_queue_pop(self->input_queue);
|
|
|
|
if (command) {
|
|
gchar *msg = g_strdup_printf("> %s\n", command);
|
|
log_simple(self, msg);
|
|
}
|
|
|
|
return command;
|
|
}
|
|
|
|
void focus(ConsoleWindow *self) {
|
|
gtk_window_present_with_time(GTK_WINDOW(self), time(NULL));
|
|
gtk_widget_grab_focus(GTK_WIDGET(self->input));
|
|
}
|
|
|
|
// Queues a message to be logged to the console
|
|
void console_log(ConsoleWindow *self, const char *message, GB_log_attributes attributes) {
|
|
if (!message || g_str_equal("", message)) return;
|
|
|
|
if (self->developer_mode) {
|
|
focus(self);
|
|
}
|
|
|
|
AttributedMessage *attr_msg = g_new(AttributedMessage, 1);
|
|
attr_msg->message = g_strdup(message);
|
|
attr_msg->attributes = attributes;
|
|
attr_msg->sidebar = self->log_to_sidebar;
|
|
|
|
g_async_queue_push(self->output_queue, attr_msg);
|
|
|
|
// mark as dirty
|
|
gtk_widget_queue_draw(GTK_WIDGET(self));
|
|
}
|
|
|
|
// Marks the console as to be cleared on the next redraw
|
|
void console_clear(ConsoleWindow *self) {
|
|
self->should_clear = true;
|
|
|
|
// mark as dirty
|
|
gtk_widget_queue_draw(GTK_WIDGET(self));
|
|
}
|
|
|
|
void break_debugger(ConsoleWindow *self, bool forced) {
|
|
if (!forced && !self->developer_mode) return;
|
|
|
|
GB_debugger_break(self->gb);
|
|
focus(self);
|
|
}
|
|
|
|
// Hack to avoid deadlocking on queue reads ...
|
|
void abort_debugger(ConsoleWindow *self) {
|
|
g_async_queue_push(self->input_queue, g_strdup("c\0"));
|
|
g_async_queue_push(self->output_queue, g_strdup("c\0"));
|
|
console_clear(self);
|
|
}
|
|
|
|
void set_developer_mode(ConsoleWindow *self, bool value) {
|
|
self->developer_mode = value;
|
|
}
|