#include "console_window.h" #include "util.h" #include 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; 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; } // 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; 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) { GB_debugger_break(self->gb); gtk_window_present_with_time(GTK_WINDOW(self), time(NULL)); gtk_widget_grab_focus(GTK_WIDGET(self->input)); }