[GTK3] Convert debugger console into GTK widget

This commit is contained in:
Maximilian Mader 2020-05-21 22:37:25 +02:00
parent bef38d16d2
commit 55a258ad0f
Signed by: Max
GPG Key ID: F71D56A3151C4FB3
9 changed files with 571 additions and 473 deletions

288
gtk3/console_window.c Normal file
View File

@ -0,0 +1,288 @@
#include "console_window.h"
#include "util.h"
#include <stdbool.h>
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;
GList *command_history;
};
G_DEFINE_TYPE(ConsoleWindow, console_window, GTK_TYPE_WINDOW);
typedef struct {
const char *message;
GB_log_attributes attributes;
bool sidebar;
} AttributedMessage;
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 = NULL;
}
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(GtkEntry *input, ConsoleWindow *self) {
const gchar *text_ptr = gtk_entry_get_text(input);
const gchar *text = NULL;
if (g_strcmp0("", text_ptr) == 0) {
GList *last = g_list_last(self->command_history);
if (last) text = last->data;
}
else {
text = text_ptr;
}
if (text) {
GList *last = g_list_last(self->command_history);
// Add command to queue unless it was the last command issued
if (!last || g_strcmp0(last->data, text) != 0) {
self->command_history = g_list_append(self->command_history, g_strdup(text));
}
g_async_queue_push(self->input_queue, (gpointer) g_strdup(text));
gtk_entry_set_text(self->input, "");
}
}
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);
GTK_WIDGET_CLASS(class)->realize = console_window_realize;
GTK_WIDGET_CLASS(class)->draw = console_window_draw;
}
ConsoleWindow *console_window_new(void) {
return g_object_new(CONSOLE_WINDOW_TYPE, 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));
}

15
gtk3/console_window.h Normal file
View File

@ -0,0 +1,15 @@
#ifndef console_window_h
#define console_window_h
#include <gtk/gtk.h>
#include <Core/gb.h>
#define CONSOLE_WINDOW_TYPE (console_window_get_type())
G_DECLARE_FINAL_TYPE(ConsoleWindow, console_window, SAMEBOY, CONSOLE_WINDOW, GtkWindow)
ConsoleWindow *console_window_new(void);
char *console_get_async_input(ConsoleWindow *self, GB_gameboy_t *gb);
char *console_get_sync_input(ConsoleWindow *self, GB_gameboy_t *gb);
void console_log(ConsoleWindow *self, const char *message, GB_log_attributes attributes);
void console_clear(ConsoleWindow *self);
#endif

View File

@ -14,6 +14,7 @@
#include "check_menu_radio_group.h" #include "check_menu_radio_group.h"
#include "gb_screen.h" #include "gb_screen.h"
#include "console_window.h"
#include "preferences_window.h" #include "preferences_window.h"
#include "vram_viewer_window.h" #include "vram_viewer_window.h"
@ -52,10 +53,6 @@ static GuiData gui_data = {
.stopping = false, .stopping = false,
.stopped = false, .stopped = false,
.in_sync_input = false,
.log_to_sidebar = false,
.should_clear_sidebar = false,
.audio_initialized = false, .audio_initialized = false,
.border_mode_changed = false, .border_mode_changed = false,
@ -355,199 +352,6 @@ static void rumble_callback(GB_gameboy_t *gb, double amp) {
} }
} }
static gboolean clear_sidebar(void) {
GtkTextView *sidebar_output = builder_get(GTK_TEXT_VIEW, "console_sidebar_output");
GtkTextBuffer *sidebar_output_text_buf = gtk_text_view_get_buffer(sidebar_output);
gtk_text_buffer_set_text(sidebar_output_text_buf, "", -1);
return false;
}
static gboolean scroll_to_bottom(GtkTextView *textview, GtkTextMark *mark) {
GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview);
GtkTextIter iter;
gtk_text_buffer_get_end_iter(buffer, &iter);
gtk_text_iter_set_line_offset(&iter, 0);
gtk_text_buffer_move_mark(buffer, mark, &iter);
gtk_text_view_scroll_to_mark(textview, mark, 0.0, true, 0.0, 0.10);
gtk_text_buffer_delete_mark(buffer, mark);
return true;
}
static gboolean append_pending_output(void) {
g_rec_mutex_lock(&gui_data.console_output_lock);
if (gui_data.should_clear_sidebar) {
clear_sidebar();
gui_data.should_clear_sidebar = false;
}
if (gtk_text_buffer_get_char_count(gui_data.pending_console_output) > 0) {
GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, gui_data.log_to_sidebar? "console_sidebar_output" : "console_screen");
GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view);
GtkTextIter start;
GtkTextIter end;
gtk_text_buffer_get_start_iter(gui_data.pending_console_output, &start);
gtk_text_buffer_get_end_iter(gui_data.pending_console_output, &end);
GtkTextIter iter;
gtk_text_buffer_get_end_iter(text_buf, &iter);
gtk_text_buffer_insert_range(text_buf, &iter, &start, &end);
scroll_to_bottom(text_view, gtk_text_buffer_create_mark(text_buf, NULL, &iter, true));
gtk_text_buffer_set_text(gui_data.pending_console_output, "", -1);
}
g_rec_mutex_unlock(&gui_data.console_output_lock);
return false;
}
static gboolean update_debugger_sidebar(GB_gameboy_t *gb) {
if (!GB_debugger_is_stopped(gb)) {
return false;
}
if (gui_data.main_thread != g_thread_self()) {
g_idle_add((GSourceFunc) update_debugger_sidebar, gb);
return false;
}
g_rec_mutex_lock(&gui_data.console_output_lock);
gui_data.should_clear_sidebar = true;
append_pending_output();
gui_data.log_to_sidebar = true;
g_rec_mutex_unlock(&gui_data.console_output_lock);
GtkTextView *sidebar_input = builder_get(GTK_TEXT_VIEW, "console_sidebar_input");
GtkTextBuffer *sidebar_input_text_buf = gtk_text_view_get_buffer(sidebar_input);
gint line_count = gtk_text_buffer_get_line_count(sidebar_input_text_buf);
for (unsigned line = 0; line < line_count; ++line) {
GtkTextIter start_iter;
GtkTextIter end_iter;
gunichar ch;
gtk_text_buffer_get_iter_at_line(sidebar_input_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(sidebar_input_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);
}
g_rec_mutex_lock(&gui_data.console_output_lock);
append_pending_output();
gui_data.log_to_sidebar = false;
g_rec_mutex_unlock(&gui_data.console_output_lock);
return false;
}
static void console_log(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) {
g_rec_mutex_lock(&gui_data.console_output_lock);
if (string != NULL && !g_str_equal("", string)) {
GtkTextIter iter;
GtkTextIter start;
// Append attributed text to "gui_data.pending_console_output" GtkTextBuffer
gtk_text_buffer_get_end_iter(gui_data.pending_console_output, &iter);
GtkTextMark *start_mark = gtk_text_buffer_create_mark(gui_data.pending_console_output, NULL, &iter, true);
gtk_text_buffer_insert(gui_data.pending_console_output, &iter, g_strdup(string), -1);
gtk_text_buffer_get_iter_at_mark(gui_data.pending_console_output, &start, start_mark);
if (attributes & GB_LOG_BOLD) {
gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "bold", &start, &iter);
}
if (attributes & GB_LOG_DASHED_UNDERLINE) {
gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "dashed_underline", &start, &iter);
}
if (attributes & GB_LOG_UNDERLINE) {
gtk_text_buffer_apply_tag_by_name(gui_data.pending_console_output, "underline", &start, &iter);
}
gtk_text_buffer_delete_mark(gui_data.pending_console_output, start_mark);
g_idle_add((GSourceFunc) append_pending_output, NULL);
}
g_rec_mutex_unlock(&gui_data.console_output_lock);
}
// Console TODO:
// TODO: clear sidebar when switching to async mode
// TODO: Command history (up / down arrow in input)
// TODO: reverse search of commands
// TODO: search in output
static char *sync_console_input(GB_gameboy_t *gb) {
update_debugger_sidebar(gb);
console_log(gb, "> ", 0);
gui_data.in_sync_input = true;
g_mutex_lock(&gui_data.debugger_input_mutex);
g_cond_wait(&gui_data.debugger_input_cond, &gui_data.debugger_input_mutex);
gchar *input = NULL;
const gchar *_input = g_ptr_array_index(gui_data.debugger_input_queue, 0);
input = g_strdup(_input);
gpointer ptr = g_ptr_array_remove_index(gui_data.debugger_input_queue, 0);
if (ptr) g_free(ptr);
g_mutex_unlock(&gui_data.debugger_input_mutex);
gui_data.in_sync_input = false;
return input;
}
static char *async_console_input(GB_gameboy_t *gb) {
// TODO: This is rather ugly
g_idle_add((GSourceFunc) clear_sidebar, NULL);
if (gui_data.debugger_input_queue->len == 0) return NULL;
g_mutex_lock(&gui_data.debugger_input_mutex);
gchar *input = NULL;
const gchar *_input = g_ptr_array_index(gui_data.debugger_input_queue, 0);
if (_input) {
input = g_strdup(_input);
gpointer ptr = g_ptr_array_remove_index(gui_data.debugger_input_queue, 0);
if (ptr) g_free(ptr);
}
g_mutex_unlock(&gui_data.debugger_input_mutex);
return input;
}
// Creating these items in the UI defintion files was buggy in some desktop // Creating these items in the UI defintion files was buggy in some desktop
// environments and the manual call of `g_signal_connect` was needed anyway // environments and the manual call of `g_signal_connect` was needed anyway
// because the UI definition cant define string arguments for signal handlers. // because the UI definition cant define string arguments for signal handlers.
@ -867,31 +671,47 @@ static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) {
} }
} }
static char *wrapped_console_get_async_input(GB_gameboy_t *gb) {
return console_get_async_input(gui_data.console, gb);
}
static char *wrapped_console_get_sync_input(GB_gameboy_t *gb) {
return console_get_sync_input(gui_data.console, gb);
}
static void wrapped_console_log(GB_gameboy_t *gb, const char *message, GB_log_attributes attributes) {
console_log(gui_data.console, message, attributes);
}
static void init(void) { static void init(void) {
if (GB_is_inited(&gb)) return; if (GB_is_inited(&gb)) return;
GB_init(&gb, config_get_model_type(&gui_data)); GB_init(&gb, config_get_model_type(&gui_data));
GB_set_vblank_callback(&gb, vblank); GB_set_vblank_callback(&gb, vblank);
GB_set_pixels_output(&gb, gb_screen_get_current_buffer(gui_data.screen));
GB_set_rgb_encode_callback(&gb, rgb_encode); GB_set_rgb_encode_callback(&gb, rgb_encode);
GB_set_sample_rate(&gb, GB_audio_get_sample_rate());
GB_set_pixels_output(&gb, gb_screen_get_current_buffer(gui_data.screen));
GB_set_color_correction_mode(&gb, config_get_color_correction_mode()); GB_set_color_correction_mode(&gb, config_get_color_correction_mode());
GB_set_highpass_filter_mode(&gb, config_get_highpass_mode());
GB_set_rewind_length(&gb, config.emulation.rewind_duration);
GB_set_update_input_hint_callback(&gb, handle_events);
GB_apu_set_sample_callback(&gb, gb_audio_callback);
GB_set_input_callback(&gb, sync_console_input);
GB_set_async_input_callback(&gb, async_console_input);
GB_set_log_callback(&gb, console_log);
GB_set_boot_rom_load_callback(&gb, load_boot_rom);
GB_set_rumble_callback(&gb, rumble_callback);
GB_set_rumble_mode(&gb, config_get_rumble_mode());
if (config_get_display_border_mode() <= GB_BORDER_ALWAYS) { if (config_get_display_border_mode() <= GB_BORDER_ALWAYS) {
GB_set_border_mode(&gb, config_get_display_border_mode()); GB_set_border_mode(&gb, config_get_display_border_mode());
} }
GB_apu_set_sample_callback(&gb, gb_audio_callback);
GB_set_sample_rate(&gb, GB_audio_get_sample_rate());
GB_set_highpass_filter_mode(&gb, config_get_highpass_mode());
GB_set_log_callback(&gb, wrapped_console_log);
GB_set_input_callback(&gb, wrapped_console_get_sync_input);
GB_set_async_input_callback(&gb, wrapped_console_get_async_input);
GB_set_boot_rom_load_callback(&gb, load_boot_rom);
GB_set_update_input_hint_callback(&gb, handle_events);
GB_set_rumble_callback(&gb, rumble_callback);
GB_set_rumble_mode(&gb, config_get_rumble_mode());
GB_set_rewind_length(&gb, config.emulation.rewind_duration);
} }
static void reset(void) { static void reset(void) {
@ -1170,11 +990,11 @@ static void startup(GApplication *app, gpointer null_ptr) {
init_config(app, gui_data.cli_options.config_path, &gui_data.config_modification_date); init_config(app, gui_data.cli_options.config_path, &gui_data.config_modification_date);
gui_data.screen = gb_screen_new(gui_data.cli_options.force_software_renderer); gui_data.screen = gb_screen_new(gui_data.cli_options.force_software_renderer);
gui_data.console = console_window_new();
gui_data.preferences = preferences_window_new(&gb); gui_data.preferences = preferences_window_new(&gb);
gui_data.vram_viewer = vram_viewer_window_new(); gui_data.vram_viewer = vram_viewer_window_new();
gui_data.memory_viewer = GTK_WINDOW(get_object("memory_viewer")); gui_data.memory_viewer = GTK_WINDOW(get_object("memory_viewer"));
gui_data.console = GTK_WINDOW(get_object("console"));
gui_data.printer = GTK_WINDOW(get_object("printer")); gui_data.printer = GTK_WINDOW(get_object("printer"));
if (config.audio.sample_rate == -1) { if (config.audio.sample_rate == -1) {
@ -1322,40 +1142,12 @@ static void connect_signal_handlers(GApplication *app) {
g_signal_connect(gui_data.preferences, "pref-update::audio-sample-rate", G_CALLBACK(on_preferences_notify_sample_rate), NULL); g_signal_connect(gui_data.preferences, "pref-update::audio-sample-rate", G_CALLBACK(on_preferences_notify_sample_rate), NULL);
} }
static void setup_console(void) {
GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, "console_screen");
GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view);
gtk_text_view_set_buffer(
builder_get(GTK_TEXT_VIEW, "console_sidebar_output"),
gtk_text_buffer_new(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);
g_mutex_init(&gui_data.debugger_input_mutex);
g_cond_init(&gui_data.debugger_input_cond);
g_rec_mutex_init(&gui_data.console_output_lock);
if (!gui_data.debugger_input_queue) {
gui_data.debugger_input_queue = g_ptr_array_sized_new(4);
}
if (!gui_data.pending_console_output) {
gui_data.pending_console_output = gtk_text_buffer_new(gtk_text_buffer_get_tag_table(text_buf));
}
}
// This function gets called when the GApplication gets activated, i.e. it is ready to show widgets. // This function gets called when the GApplication gets activated, i.e. it is ready to show widgets.
static void activate(GApplication *app, gpointer null_ptr) { static void activate(GApplication *app, gpointer null_ptr) {
init_audio(); init_audio();
init_controllers(); init_controllers();
connect_signal_handlers(app); connect_signal_handlers(app);
// create_canvas();
setup_console();
if (gui_data.cli_options.fullscreen) { if (gui_data.cli_options.fullscreen) {
gtk_window_fullscreen(GTK_WINDOW(gui_data.main_window)); gtk_window_fullscreen(GTK_WINDOW(gui_data.main_window));
@ -1401,16 +1193,17 @@ static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer
gtk_widget_hide(GTK_WIDGET(dialog)); gtk_widget_hide(GTK_WIDGET(dialog));
} }
// app.preferences GAction
// Opens the preferences window
static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(gui_data.preferences));
}
// app.show_console GAction // app.show_console GAction
// Opens the console // Opens the console
static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app) { static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app) {
if (gui_data.debugger_input_queue) { gtk_widget_show_all(GTK_WIDGET(gui_data.console));
while (gui_data.debugger_input_queue->len) {
g_ptr_array_remove_index_fast(gui_data.debugger_input_queue, gui_data.debugger_input_queue->len - 1);
}
}
gtk_widget_show_all(builder_get(GTK_WIDGET, "console"));
} }
// app.open_gtk_debugger GAction // app.open_gtk_debugger GAction
@ -1431,6 +1224,12 @@ static void activate_open_vram_viewer(GSimpleAction *action, GVariant *parameter
gtk_widget_show_all(GTK_WIDGET(gui_data.vram_viewer)); gtk_widget_show_all(GTK_WIDGET(gui_data.vram_viewer));
} }
// app.clear_console GAction
// Clears the debugger console
static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app) {
console_clear(gui_data.console);
}
// Closes a ROM // Closes a ROM
static void close_rom(void) { static void close_rom(void) {
stop(); stop();
@ -1478,30 +1277,12 @@ static void activate_close(GSimpleAction *action, GVariant *parameter, gpointer
close_rom(); close_rom();
} }
// app.preferences GAction
// Opens the preferences window
static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app) {
gtk_widget_show_all(GTK_WIDGET(gui_data.preferences));
}
// app.quit GAction // app.quit GAction
// Exits the application // Exits the application
static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app) { static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app) {
quit(); quit();
} }
// app.clear_console GAction
// Clears the debugger console
static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app) {
g_rec_mutex_lock(&gui_data.console_output_lock);
GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, "console_screen");
GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view);
gtk_text_buffer_set_text(text_buf, "", -1);
g_rec_mutex_unlock(&gui_data.console_output_lock);
}
static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) { static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data_ptr) {
config.audio.muted = g_variant_get_boolean(value); config.audio.muted = g_variant_get_boolean(value);
@ -1543,35 +1324,6 @@ G_MODULE_EXPORT void on_open_recent_activate(GtkRecentChooser *chooser, gpointer
} }
} }
G_MODULE_EXPORT void console_on_enter(GtkWidget *w, gpointer user_data_ptr) {
GtkEntry *input = GTK_ENTRY(w);
const gchar *_text = gtk_entry_get_text(input);
gchar *text = g_strdup(_text);
if (g_strcmp0("", text) == 0 && g_strcmp0("", gui_data.last_console_input) < 0) {
text = g_strdup(gui_data.last_console_input);
}
else if (text) {
if (gui_data.last_console_input != NULL) g_free(gui_data.last_console_input);
gui_data.last_console_input = g_strdup(text);
}
if (!gui_data.in_sync_input) {
console_log(&gb, "> ", 0);
}
console_log(&gb, text, 0);
console_log(&gb, "\n", 0);
g_mutex_lock(&gui_data.debugger_input_mutex);
g_ptr_array_add(gui_data.debugger_input_queue, (gpointer)text);
g_cond_signal(&gui_data.debugger_input_cond);
g_mutex_unlock(&gui_data.debugger_input_mutex);
// clear input
gtk_entry_set_text(input, "");
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
gui_data.main_thread = g_thread_self(); gui_data.main_thread = g_thread_self();

View File

@ -0,0 +1,202 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0
The MIT License (MIT)
Copyright (c) 2015-2019 Lior Halphon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Maximilian Mader
-->
<interface>
<requires lib="gtk+" version="3.22"/>
<!-- interface-license-type mit -->
<!-- interface-name SameBoy -->
<!-- interface-description SameBoy is an open source Game Boy (DMG) and Game Boy Color (CGB) emulator, written in portable C. -->
<!-- interface-copyright 2015-2019 Lior Halphon -->
<!-- interface-authors Maximilian Mader -->
<template class="ConsoleWindow">
<property name="can_focus">False</property>
<property name="title" translatable="yes">Debug Console</property>
<property name="default_width">920</property>
<property name="default_height">400</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="min_content_width">600</property>
<property name="min_content_height">376</property>
<property name="max_content_width">600</property>
<property name="max_content_height">376</property>
<child>
<object class="GtkTextView" id="output">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="wrap_mode">word</property>
<property name="left_margin">5</property>
<property name="right_margin">5</property>
<property name="top_margin">5</property>
<property name="bottom_margin">5</property>
<property name="cursor_visible">False</property>
<property name="accepts_tab">False</property>
<property name="monospace">True</property>
<style>
<class name="border-none"/>
</style>
</object>
</child>
<style>
<class name="border-none"/>
<class name="border-right"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="width_request">320</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="min_content_width">320</property>
<property name="min_content_height">80</property>
<child>
<object class="GtkTextView" id="sidebar_input">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wrap_mode">word</property>
<property name="left_margin">5</property>
<property name="right_margin">5</property>
<property name="top_margin">5</property>
<property name="bottom_margin">5</property>
<property name="accepts_tab">False</property>
<property name="monospace">True</property>
<style>
<class name="border-none"/>
</style>
</object>
</child>
<style>
<class name="border-none"/>
<class name="border-bottom"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="sidebar_output">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="wrap_mode">word</property>
<property name="left_margin">5</property>
<property name="right_margin">5</property>
<property name="top_margin">5</property>
<property name="bottom_margin">5</property>
<property name="accepts_tab">False</property>
<property name="monospace">True</property>
<style>
<class name="border-none"/>
</style>
</object>
</child>
<style>
<class name="border-none"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="border-none"/>
<class name="border-bottom"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="input">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_top">3</property>
<property name="margin_bottom">3</property>
<property name="placeholder_text" translatable="yes">Console input</property>
<signal name="activate" handler="on_input" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="titlebar">
<placeholder/>
</child>
<style>
<class name="debug-console"/>
</style>
</template>
</interface>

View File

@ -72,173 +72,6 @@ Maximilian Mader https://github.com/max-m</property>
</child> </child>
</object> </object>
<object class="GtkWindow" id="console">
<property name="can_focus">False</property>
<property name="title" translatable="yes">Debug Console</property>
<property name="default_width">920</property>
<property name="default_height">400</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="min_content_width">600</property>
<property name="min_content_height">376</property>
<property name="max_content_width">600</property>
<property name="max_content_height">376</property>
<child>
<object class="GtkTextView" id="console_screen">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="wrap_mode">word</property>
<property name="left_margin">5</property>
<property name="right_margin">5</property>
<property name="top_margin">5</property>
<property name="bottom_margin">5</property>
<property name="cursor_visible">False</property>
<property name="accepts_tab">False</property>
<property name="monospace">True</property>
<style>
<class name="border-none"/>
</style>
</object>
</child>
<style>
<class name="border-none"/>
<class name="border-right"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="width_request">320</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="min_content_width">320</property>
<property name="min_content_height">80</property>
<child>
<object class="GtkTextView" id="console_sidebar_input">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wrap_mode">word</property>
<property name="left_margin">5</property>
<property name="right_margin">5</property>
<property name="top_margin">5</property>
<property name="bottom_margin">5</property>
<property name="accepts_tab">False</property>
<property name="monospace">True</property>
<style>
<class name="border-none"/>
</style>
</object>
</child>
<style>
<class name="border-none"/>
<class name="border-bottom"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="console_sidebar_output">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="wrap_mode">word</property>
<property name="left_margin">5</property>
<property name="right_margin">5</property>
<property name="top_margin">5</property>
<property name="bottom_margin">5</property>
<property name="accepts_tab">False</property>
<property name="monospace">True</property>
<style>
<class name="border-none"/>
</style>
</object>
</child>
<style>
<class name="border-none"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="border-none"/>
<class name="border-bottom"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="console_input">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_top">3</property>
<property name="margin_bottom">3</property>
<property name="placeholder_text" translatable="yes">Console input</property>
<signal name="activate" handler="console_on_enter" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="titlebar">
<placeholder/>
</child>
<style>
<class name="debug-console"/>
</style>
</object>
<object class="GtkWindow" id="memory_viewer"> <object class="GtkWindow" id="memory_viewer">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="title" translatable="yes">Memory Viewer</property> <property name="title" translatable="yes">Memory Viewer</property>

View File

@ -2,6 +2,7 @@
<gresources> <gresources>
<gresource prefix="/io/github/sameboy"> <gresource prefix="/io/github/sameboy">
<file preprocess="xml-stripblanks" compressed="true">ui/window.ui</file> <file preprocess="xml-stripblanks" compressed="true">ui/window.ui</file>
<file preprocess="xml-stripblanks" compressed="true">ui/console_window.ui</file>
<file preprocess="xml-stripblanks" compressed="true">ui/preferences_window.ui</file> <file preprocess="xml-stripblanks" compressed="true">ui/preferences_window.ui</file>
<file preprocess="xml-stripblanks" compressed="true">ui/vram_viewer_window.ui</file> <file preprocess="xml-stripblanks" compressed="true">ui/vram_viewer_window.ui</file>
<!-- <!--

View File

@ -3,6 +3,7 @@
#include "SDL.h" #include "SDL.h"
#include "gb_screen.h" #include "gb_screen.h"
#include "console_window.h"
#include "preferences_window.h" #include "preferences_window.h"
#include "vram_viewer_window.h" #include "vram_viewer_window.h"
@ -41,24 +42,13 @@ typedef struct GuiData {
GtkBox *main_window_container; GtkBox *main_window_container;
GbScreen *screen; GbScreen *screen;
ConsoleWindow *console;
PreferencesWindow *preferences; PreferencesWindow *preferences;
VramViewerWindow *vram_viewer; VramViewerWindow *vram_viewer;
GtkWindow *memory_viewer; GtkWindow *memory_viewer;
GtkWindow *console;
GtkWindow *printer; GtkWindow *printer;
// Debugger state
GtkTextBuffer *pending_console_output;
gboolean in_sync_input;
gchar *last_console_input;
gboolean log_to_sidebar;
gboolean should_clear_sidebar;
GMutex debugger_input_mutex;
GCond debugger_input_cond;
GRecMutex console_output_lock;
GPtrArray *debugger_input_queue;
// Audio and video // Audio and video
bool audio_initialized; bool audio_initialized;
bool border_mode_changed; bool border_mode_changed;

View File

@ -142,3 +142,18 @@ void set_combo_box_row_separator_func(GtkContainer *container) {
g_list_free(children); g_list_free(children);
} }
gboolean scroll_to_bottom(GtkTextView *textview, GtkTextMark *mark) {
GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview);
GtkTextIter iter;
gtk_text_buffer_get_end_iter(buffer, &iter);
gtk_text_iter_set_line_offset(&iter, 0);
gtk_text_buffer_move_mark(buffer, mark, &iter);
gtk_text_view_scroll_to_mark(textview, mark, 0.0, true, 0.0, 0.10);
gtk_text_buffer_delete_mark(buffer, mark);
return true;
}

View File

@ -25,4 +25,6 @@ GtkWidget *menubar_to_menu(GtkMenuBar *menubar);
gboolean is_separator(GtkTreeModel *model, GtkTreeIter *iter, gpointer data); gboolean is_separator(GtkTreeModel *model, GtkTreeIter *iter, gpointer data);
void set_combo_box_row_separator_func(GtkContainer *container); void set_combo_box_row_separator_func(GtkContainer *container);
gboolean scroll_to_bottom(GtkTextView *textview, GtkTextMark *mark);
#endif #endif