diff --git a/src/xenia/ui/loop_gtk.cc b/src/xenia/ui/loop_gtk.cc index 4a753c853..a2b704aeb 100644 --- a/src/xenia/ui/loop_gtk.cc +++ b/src/xenia/ui/loop_gtk.cc @@ -17,15 +17,6 @@ namespace xe { namespace ui { -class PostedFn { - public: - explicit PostedFn(std::function fn) : fn_(std::move(fn)) {} - void Call() { fn_(); } - - private: - std::function fn_; -}; - std::unique_ptr Loop::Create() { return std::make_unique(); } GTKLoop::GTKLoop() : thread_id_() { @@ -56,24 +47,33 @@ bool GTKLoop::is_on_loop_thread() { } gboolean _posted_fn_thunk(gpointer posted_fn) { - PostedFn* Fn = reinterpret_cast(posted_fn); - Fn->Call(); + // convert gpointer back to std::function, call it, then free std::function + std::function* f = + reinterpret_cast*>(posted_fn); + std::function& func = *f; + func(); + delete f; + // Tells GDK we don't want to run this again return G_SOURCE_REMOVE; } void GTKLoop::Post(std::function fn) { assert_true(thread_id_ != std::thread::id()); - gdk_threads_add_idle(_posted_fn_thunk, - reinterpret_cast(new PostedFn(std::move(fn)))); + // converting std::function to a generic pointer for gdk + gdk_threads_add_idle(_posted_fn_thunk, reinterpret_cast( + new std::function(fn))); } void GTKLoop::PostDelayed(std::function fn, uint64_t delay_millis) { gdk_threads_add_timeout( delay_millis, _posted_fn_thunk, - reinterpret_cast(new PostedFn(std::move(fn)))); + reinterpret_cast(new std::function(fn))); } -void GTKLoop::Quit() { assert_true(thread_id_ != std::thread::id()); } +void GTKLoop::Quit() { + assert_true(thread_id_ != std::thread::id()); + Post([]() { gtk_main_quit(); }); +} void GTKLoop::AwaitQuit() { quit_fence_.Wait(); } diff --git a/src/xenia/ui/window.h b/src/xenia/ui/window.h index accdc65b9..d8d3ac00b 100644 --- a/src/xenia/ui/window.h +++ b/src/xenia/ui/window.h @@ -172,8 +172,9 @@ class Window { Loop* loop_ = nullptr; std::unique_ptr main_menu_; std::string title_; - int32_t width_ = 0; - int32_t height_ = 0; + // GTK must have a default value here that isn't 0 + int32_t width_ = 1280; + int32_t height_ = 720; bool has_focus_ = true; bool is_cursor_visible_ = true; bool is_imgui_input_enabled_ = false; diff --git a/src/xenia/ui/window_gtk.cc b/src/xenia/ui/window_gtk.cc index 7d11d316c..a785af2df 100644 --- a/src/xenia/ui/window_gtk.cc +++ b/src/xenia/ui/window_gtk.cc @@ -7,11 +7,13 @@ ****************************************************************************** */ +#include #include #include #include "xenia/base/assert.h" +#include "xenia/base/clock.h" #include "xenia/base/logging.h" #include "xenia/base/platform_linux.h" #include "xenia/ui/window_gtk.h" @@ -19,15 +21,6 @@ namespace xe { namespace ui { -class FnWrapper { - public: - explicit FnWrapper(std::function fn) : fn_(std::move(fn)) {} - void Call() { fn_(); } - - private: - std::function fn_; -}; - std::unique_ptr Window::Create(Loop* loop, const std::string& title) { return std::make_unique(loop, title); } @@ -38,14 +31,14 @@ GTKWindow::GTKWindow(Loop* loop, const std::string& title) GTKWindow::~GTKWindow() { OnDestroy(); if (window_) { - gtk_widget_destroy(window_); + if (GTK_IS_WIDGET(window_)) gtk_widget_destroy(window_); window_ = nullptr; } } bool GTKWindow::Initialize() { return OnCreate(); } -void gtk_event_handler_(GtkWidget* widget, GdkEvent* event, gpointer data) { +gboolean gtk_event_handler(GtkWidget* widget, GdkEvent* event, gpointer data) { GTKWindow* window = reinterpret_cast(data); switch (event->type) { case GDK_OWNER_CHANGE: @@ -59,34 +52,80 @@ void gtk_event_handler_(GtkWidget* widget, GdkEvent* event, gpointer data) { window->HandleKeyboard(&(event->key)); break; case GDK_SCROLL: - case GDK_BUTTON_PRESS: case GDK_MOTION_NOTIFY: + case GDK_BUTTON_PRESS: + case GDK_BUTTON_RELEASE: window->HandleMouse(&(event->any)); break; case GDK_FOCUS_CHANGE: window->HandleWindowFocus(&(event->focus_change)); break; case GDK_CONFIGURE: - window->HandleWindowResize(&(event->configure)); + // Only handle the event for the drawing area so we don't save + // a width and height that includes the menu bar on the full window + if (event->configure.window == + gtk_widget_get_window(window->drawing_area_)) { + window->HandleWindowResize(&(event->configure)); + } + break; default: // Do nothing - return; + break; } + // Propagate the event to other handlers + return GDK_EVENT_PROPAGATE; +} + +gboolean draw_callback(GtkWidget* widget, GdkFrameClock* frame_clock, + gpointer data) { + GTKWindow* window = reinterpret_cast(data); + window->HandleWindowPaint(); + return G_SOURCE_CONTINUE; +} + +gboolean close_callback(GtkWidget* widget, gpointer data) { + GTKWindow* window = reinterpret_cast(data); + window->Close(); + return G_SOURCE_CONTINUE; } void GTKWindow::Create() { + // GTK optionally allows passing argv and argc here for parsing gtk specific + // options. We won't bother window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); - gtk_window_set_title(GTK_WINDOW(window_), (gchar*)title_.c_str()); - gtk_window_set_default_size(GTK_WINDOW(window_), 1280, 720); - gtk_widget_show_all(window_); - g_signal_connect(G_OBJECT(window_), "destroy", G_CALLBACK(gtk_main_quit), - NULL); - g_signal_connect(G_OBJECT(window_), "event", G_CALLBACK(gtk_event_handler_), + gtk_window_set_resizable(GTK_WINDOW(window_), true); + gtk_window_set_title(GTK_WINDOW(window_), title_.c_str()); + gtk_window_set_default_size(GTK_WINDOW(window_), width_, height_); + // Drawing area is where we will attach our vulkan/gl context + drawing_area_ = gtk_drawing_area_new(); + // tick callback is for the refresh rate of the window + gtk_widget_add_tick_callback(drawing_area_, draw_callback, + reinterpret_cast(this), nullptr); + // Attach our event handler to both the main window (for keystrokes) and the + // drawing area (for mouse input, resize event, etc) + g_signal_connect(G_OBJECT(drawing_area_), "event", + G_CALLBACK(gtk_event_handler), reinterpret_cast(this)); GdkDisplay* gdk_display = gtk_widget_get_display(window_); assert(GDK_IS_X11_DISPLAY(gdk_display)); connection_ = XGetXCBConnection(gdk_x11_display_get_xdisplay(gdk_display)); + + g_signal_connect(G_OBJECT(window_), "event", G_CALLBACK(gtk_event_handler), + reinterpret_cast(this)); + // When the window manager kills the window (ie, the user hits X) + g_signal_connect(G_OBJECT(window_), "destroy", G_CALLBACK(close_callback), + reinterpret_cast(this)); + // Enable only keyboard events (so no mouse) for the top window + gtk_widget_set_events(window_, GDK_KEY_PRESS | GDK_KEY_RELEASE); + // Enable all events for the drawing area + gtk_widget_add_events(drawing_area_, GDK_ALL_EVENTS_MASK); + // Place the drawing area in a container (which later will hold the menu) + // then let it fill the whole area + box_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_box_pack_end(GTK_BOX(box_), drawing_area_, TRUE, TRUE, 0); + gtk_container_add(GTK_CONTAINER(window_), box_); + gtk_widget_show_all(window_); } bool GTKWindow::OnCreate() { @@ -128,7 +167,6 @@ void GTKWindow::ToggleFullscreen(bool fullscreen) { } fullscreen_ = fullscreen; - if (fullscreen) { gtk_window_fullscreen(GTK_WINDOW(window_)); } else { @@ -145,7 +183,6 @@ void GTKWindow::set_bordered(bool enabled) { // Don't screw with the borders if we're fullscreen. return; } - gtk_window_set_decorated(GTK_WINDOW(window_), enabled); } @@ -176,31 +213,26 @@ void GTKWindow::set_focus(bool value) { } void GTKWindow::Resize(int32_t width, int32_t height) { + width_ = width; + height_ = height; gtk_window_resize(GTK_WINDOW(window_), width, height); } void GTKWindow::Resize(int32_t left, int32_t top, int32_t right, int32_t bottom) { - // TODO(dougvj) Verify that this is the desired behavior from this call + int32_t width = right - left; + int32_t height = bottom - top; + width_ = width; + height_ = height; gtk_window_move(GTK_WINDOW(window_), left, top); - gtk_window_resize(GTK_WINDOW(window_), left - right, top - bottom); + gtk_window_resize(GTK_WINDOW(window_), width, height); } -void GTKWindow::OnResize(UIEvent* e) { - int32_t width; - int32_t height; - gtk_window_get_size(GTK_WINDOW(window_), &width, &height); - if (width != width_ || height != height_) { - width_ = width; - height_ = height; - Layout(); - } - super::OnResize(e); -} +void GTKWindow::OnResize(UIEvent* e) { super::OnResize(e); } void GTKWindow::Invalidate() { + // gtk_widget_queue_draw(drawing_area_); super::Invalidate(); - // TODO(dougvj) I am not sure what this is supposed to do } void GTKWindow::Close() { @@ -214,14 +246,15 @@ void GTKWindow::Close() { void GTKWindow::OnMainMenuChange() { // We need to store the old handle for detachment - static GtkWidget* box = nullptr; + static int count = 0; auto main_menu = reinterpret_cast(main_menu_.get()); - if (main_menu && !is_fullscreen()) { - if (box) gtk_widget_destroy(box); - GtkWidget* menu = main_menu->handle(); - box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); - gtk_box_pack_start(GTK_BOX(box), menu, FALSE, FALSE, 3); - gtk_container_add(GTK_CONTAINER(window_), box); + if (main_menu && main_menu->handle()) { + if (!is_fullscreen()) { + gtk_box_pack_start(GTK_BOX(box_), main_menu->handle(), FALSE, FALSE, 0); + gtk_widget_show_all(window_); + } else { + gtk_container_remove(GTK_CONTAINER(box_), main_menu->handle()); + } } } @@ -239,9 +272,22 @@ bool GTKWindow::HandleWindowOwnerChange(GdkEventOwnerChange* event) { return false; } +bool GTKWindow::HandleWindowPaint() { + auto e = UIEvent(this); + OnPaint(&e); + return true; +} + bool GTKWindow::HandleWindowResize(GdkEventConfigure* event) { if (event->type == GDK_CONFIGURE) { + int32_t width = event->width; + int32_t height = event->height; auto e = UIEvent(this); + if (width != width_ || height != height_) { + width_ = width; + height_ = height; + Layout(); + } OnResize(&e); return true; } @@ -356,18 +402,20 @@ bool GTKWindow::HandleKeyboard(GdkEventKey* event) { bool ctrl_pressed = modifiers & GDK_CONTROL_MASK; bool alt_pressed = modifiers & GDK_META_MASK; bool super_pressed = modifiers & GDK_SUPER_MASK; + uint32_t key_char = gdk_keyval_to_unicode(event->keyval); auto e = KeyEvent(this, event->hardware_keycode, 1, event->type == GDK_KEY_RELEASE, shift_pressed, ctrl_pressed, alt_pressed, super_pressed); switch (event->type) { case GDK_KEY_PRESS: OnKeyDown(&e); + if (key_char > 0) { + OnKeyChar(&e); + } break; case GDK_KEY_RELEASE: OnKeyUp(&e); break; - // TODO(dougvj) GDK doesn't have a KEY CHAR event, so we will have to - // figure out its equivalent here to call OnKeyChar(&e); default: return false; } @@ -381,23 +429,35 @@ std::unique_ptr MenuItem::Create(Type type, return std::make_unique(type, text, hotkey, callback); } -static void _menu_activate_callback(GtkWidget* menu, gpointer data) { - auto fn = reinterpret_cast(data); - fn->Call(); - delete fn; +static void _menu_activate_callback(GtkWidget* gtk_menu, gpointer data) { + GTKMenuItem* menu = reinterpret_cast(data); + menu->Activate(); +} + +void GTKMenuItem::Activate() { + try { + callback_(); + } catch (const std::bad_function_call& e) { + // Ignore + } } GTKMenuItem::GTKMenuItem(Type type, const std::string& text, const std::string& hotkey, std::function callback) : MenuItem(type, text, hotkey, std::move(callback)) { + std::string label = text; + // TODO(dougvj) Would we ever need to escape underscores? + // Replace & with _ for gtk to see the memonic + std::replace(label.begin(), label.end(), '&', '_'); + const gchar* gtk_label = reinterpret_cast(label.c_str()); switch (type) { case MenuItem::Type::kNormal: default: menu_ = gtk_menu_bar_new(); break; case MenuItem::Type::kPopup: - menu_ = gtk_menu_item_new_with_label((gchar*)text.c_str()); + menu_ = gtk_menu_item_new_with_mnemonic(gtk_label); break; case MenuItem::Type::kSeparator: menu_ = gtk_separator_menu_item_new(); @@ -407,12 +467,12 @@ GTKMenuItem::GTKMenuItem(Type type, const std::string& text, if (!hotkey.empty()) { full_name += "\t" + hotkey; } - menu_ = gtk_menu_item_new_with_label((gchar*)full_name.c_str()); + menu_ = gtk_menu_item_new_with_mnemonic(gtk_label); break; } if (GTK_IS_MENU_ITEM(menu_)) g_signal_connect(menu_, "activate", G_CALLBACK(_menu_activate_callback), - (gpointer) new FnWrapper(callback)); + (gpointer)this); } GTKMenuItem::~GTKMenuItem() { @@ -460,4 +520,5 @@ void GTKMenuItem::OnChildRemoved(MenuItem* generic_child_item) { } } // namespace ui + } // namespace xe diff --git a/src/xenia/ui/window_gtk.h b/src/xenia/ui/window_gtk.h index ce28cfcd0..fba1886a3 100644 --- a/src/xenia/ui/window_gtk.h +++ b/src/xenia/ui/window_gtk.h @@ -34,7 +34,7 @@ class GTKWindow : public Window { NativePlatformHandle native_platform_handle() const override { return connection_; } - NativeWindowHandle native_handle() const override { return window_; } + NativeWindowHandle native_handle() const override { return drawing_area_; } void EnableMainMenu() override {} void DisableMainMenu() override {} @@ -75,15 +75,22 @@ class GTKWindow : public Window { private: void Create(); GtkWidget* window_; + GtkWidget* box_; + GtkWidget* drawing_area_; xcb_connection_t* connection_; - friend void gtk_event_handler_(GtkWidget*, GdkEvent*, gpointer); + // C Callback shims for GTK + friend gboolean gtk_event_handler(GtkWidget*, GdkEvent*, gpointer); + friend gboolean close_callback(GtkWidget*, gpointer); + friend gboolean draw_callback(GtkWidget*, GdkFrameClock*, gpointer); + bool HandleMouse(GdkEventAny* event); bool HandleKeyboard(GdkEventKey* event); bool HandleWindowResize(GdkEventConfigure* event); bool HandleWindowFocus(GdkEventFocus* event); bool HandleWindowVisibility(GdkEventVisibility* event); bool HandleWindowOwnerChange(GdkEventOwnerChange* event); + bool HandleWindowPaint(); bool closing_ = false; bool fullscreen_ = false; @@ -97,6 +104,7 @@ class GTKMenuItem : public MenuItem { GtkWidget* handle() { return menu_; } using MenuItem::OnSelected; + void Activate(); void EnableMenuItem(Window& window) override {} void DisableMenuItem(Window& window) override {}