/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2025 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #include #include #include #include "xenia/base/assert.h" #include "xenia/base/logging.h" #include "xenia/base/platform_linux.h" #include "xenia/ui/surface_gnulinux.h" #include "xenia/ui/virtual_key.h" #include "xenia/ui/window_gtk.h" namespace xe { namespace ui { std::unique_ptr Window::Create(WindowedAppContext& app_context, const std::string_view title, uint32_t desired_logical_width, uint32_t desired_logical_height) { return std::make_unique(app_context, title, desired_logical_width, desired_logical_height); } GTKWindow::GTKWindow(WindowedAppContext& app_context, const std::string_view title, uint32_t desired_logical_width, uint32_t desired_logical_height) : Window(app_context, title, desired_logical_width, desired_logical_height) {} GTKWindow::~GTKWindow() { EnterDestructor(); if (window_) { // Set window_ to null to ignore events from now on since this ui::GTKWindow // is entering an indeterminate state. GtkWidget* window = window_; window_ = nullptr; // Destroying the top-level window also destroys its children. drawing_area_ = nullptr; box_ = nullptr; gtk_widget_destroy(window); } } bool GTKWindow::OpenImpl() { window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window_), GetTitle().c_str()); // Create the vertical box container for the main menu and the drawing area. box_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_container_add(GTK_CONTAINER(window_), box_); // Add the main menu (even if fullscreen was requested, for the initial layout // calculation). const auto* main_menu = dynamic_cast(GetMainMenu()); GtkWidget* main_menu_widget = main_menu ? main_menu->handle() : nullptr; if (main_menu_widget) { gtk_box_pack_start(GTK_BOX(box_), main_menu_widget, false, false, 0); } // Create the drawing area for creating the surface for, which will be the // client area of the window occupying all the window space not taken by the // main menu. drawing_area_ = gtk_drawing_area_new(); gtk_box_pack_end(GTK_BOX(box_), drawing_area_, true, true, 0); // The desired size is the client (drawing) area size. Let GTK auto-size the // entire window around it (as well as the width of the menu actually if it // happens to be bigger - the desired size in the Window will be updated later // to reflect that). gtk_widget_set_size_request(drawing_area_, GetDesiredLogicalWidth(), GetDesiredLogicalHeight()); // Attach the event handlers. // Keyboard events are processed by the window, mouse events are processed // within, and by, the drawing (client) area. gtk_widget_set_events(window_, GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_FOCUS_CHANGE_MASK); gtk_widget_set_events(drawing_area_, GDK_POINTER_MOTION_MASK | GDK_BUTTON_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_SCROLL_MASK); g_signal_connect(G_OBJECT(window_), "event", G_CALLBACK(WindowEventHandlerThunk), reinterpret_cast(this)); g_signal_connect(G_OBJECT(drawing_area_), "event", G_CALLBACK(DrawingAreaEventHandlerThunk), reinterpret_cast(this)); g_signal_connect(G_OBJECT(drawing_area_), "draw", G_CALLBACK(DrawHandler), reinterpret_cast(this)); // Finally show all the widgets in the window, including the main menu. gtk_widget_show_all(window_); // Remove the size request after finishing the initial layout because it makes // it impossible to make the window smaller. gtk_widget_set_size_request(drawing_area_, -1, -1); // After setting up the initial layout for non-fullscreen, enter fullscreen if // requested. if (IsFullscreen()) { if (main_menu_widget) { gtk_container_remove(GTK_CONTAINER(box_), main_menu_widget); } gtk_window_fullscreen(GTK_WINDOW(window_)); } // Make sure the initial state after opening is reported to the common Window // class no matter how GTK sends the events. { WindowDestructionReceiver destruction_receiver(this); // TODO(Triang3l): Report the desired client area size. GtkAllocation drawing_area_allocation; gtk_widget_get_allocation(drawing_area_, &drawing_area_allocation); OnActualSizeUpdate(uint32_t(drawing_area_allocation.width), uint32_t(drawing_area_allocation.height), WindowResizeAction::kManual, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return true; } if (gtk_window_has_toplevel_focus(GTK_WINDOW(window_))) { OnFocusUpdate(true, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return true; } } } return true; } void GTKWindow::RequestCloseImpl() { gtk_window_close(GTK_WINDOW(window_)); } void GTKWindow::ApplyNewFullscreen() { // Various functions here may trigger events that may result in the listeners // being invoked, and potentially cause the destruction of the window or // fullscreen being toggled from inside this function. WindowDestructionReceiver destruction_receiver(this); const auto* main_menu = dynamic_cast(GetMainMenu()); GtkWidget* main_menu_widget = main_menu ? main_menu->handle() : nullptr; // Changing the menu and the fullscreen state may change the size of the // drawing area too, don't handle the resize multiple times (also potentially // with the listeners changing the desired fullscreen if called from the // handling of some event like GDK_CONFIGURE). BeginBatchedSizeUpdate(); if (IsFullscreen()) { if (main_menu_widget) { gtk_container_remove(GTK_CONTAINER(box_), main_menu_widget); if (destruction_receiver.IsWindowDestroyedOrClosed()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } } gtk_window_fullscreen(GTK_WINDOW(window_)); if (destruction_receiver.IsWindowDestroyedOrClosed()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } } else { gtk_window_unfullscreen(GTK_WINDOW(window_)); if (destruction_receiver.IsWindowDestroyedOrClosed()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } if (main_menu_widget) { gtk_box_pack_start(GTK_BOX(box_), main_menu_widget, false, false, 0); if (destruction_receiver.IsWindowDestroyedOrClosed()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } // If the new menu is used for the first time, it will be in the hidden // state initially. The menu might have been changed while in fullscreen // without having shown it. gtk_widget_show_all(main_menu_widget); if (destruction_receiver.IsWindowDestroyedOrClosed()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } } } EndBatchedSizeUpdate(destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return; } } void GTKWindow::ApplyNewTitle() { gtk_window_set_title(GTK_WINDOW(window_), GetTitle().c_str()); } void GTKWindow::ApplyNewMainMenu(MenuItem* old_main_menu) { if (IsFullscreen()) { // The menu will be set when exiting fullscreen. return; } // The fullscreen state may have been changed by some callback invoked, such // as the configure (resize) one, recheck it after making changes also. WindowDestructionReceiver destruction_receiver(this); // Changing the menu may change the size of the drawing area too, and here the // menu may be changed twice (to detach the old one and to attach the new), // don't handle the resize multiple times. BeginBatchedSizeUpdate(); if (old_main_menu) { const GTKMenuItem& old_gtk_main_menu = *dynamic_cast(old_main_menu); gtk_container_remove(GTK_CONTAINER(box_), old_gtk_main_menu.handle()); if (destruction_receiver.IsWindowDestroyedOrClosed() || IsFullscreen()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } } const auto* new_main_menu = dynamic_cast(GetMainMenu()); if (!new_main_menu) { EndBatchedSizeUpdate(destruction_receiver); return; } GtkWidget* new_main_menu_widget = new_main_menu->handle(); gtk_box_pack_start(GTK_BOX(box_), new_main_menu_widget, false, false, 0); if (destruction_receiver.IsWindowDestroyedOrClosed() || IsFullscreen()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } // If the new menu is used for the first time, it will be in the hidden state // initially. gtk_widget_show_all(new_main_menu_widget); if (destruction_receiver.IsWindowDestroyedOrClosed() || IsFullscreen()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } EndBatchedSizeUpdate(destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return; } } void GTKWindow::FocusImpl() { gtk_window_activate_focus(GTK_WINDOW(window_)); } std::unique_ptr GTKWindow::CreateSurfaceImpl( Surface::TypeFlags allowed_types) { GdkDisplay* display = gtk_widget_get_display(window_); GdkWindow* drawing_area_window = gtk_widget_get_window(drawing_area_); bool type_known = false; bool type_supported_by_display = false; if (allowed_types & Surface::kTypeFlag_XcbWindow) { type_known = true; if (GDK_IS_X11_DISPLAY(display)) { type_supported_by_display = true; return std::make_unique( XGetXCBConnection(gdk_x11_display_get_xdisplay(display)), gdk_x11_window_get_xid(drawing_area_window)); } } // TODO(Triang3l): Wayland surface. if (type_known && !type_supported_by_display) { XELOGE( "GTKWindow: The window system of the GTK window is not supported by " "Xenia"); } return nullptr; } void GTKWindow::RequestPaintImpl() { gtk_widget_queue_draw(drawing_area_); } void GTKWindow::HandleSizeUpdate( WindowDestructionReceiver& destruction_receiver) { if (!drawing_area_) { // Batched size update ended when the window has already been closed, for // instance. return; } // TODO(Triang3l): Report the desired client area size. GtkAllocation drawing_area_allocation; gtk_widget_get_allocation(drawing_area_, &drawing_area_allocation); OnActualSizeUpdate(uint32_t(drawing_area_allocation.width), uint32_t(drawing_area_allocation.height), WindowResizeAction::kManual, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return; } } void GTKWindow::BeginBatchedSizeUpdate() { // It's okay if batched_size_update_contained_* are not false when beginning // a batched update, in case the new batched update was started by a window // listener called from within EndBatchedSizeUpdate. ++batched_size_update_depth_; } void GTKWindow::EndBatchedSizeUpdate( WindowDestructionReceiver& destruction_receiver) { assert_not_zero(batched_size_update_depth_); if (--batched_size_update_depth_) { return; } // Resetting batched_size_update_contained_* in closing, not opening, because // a listener may start a new batch, and finish it, and there won't be need to // handle the deferred messages twice. if (batched_size_update_contained_configure_) { batched_size_update_contained_configure_ = false; HandleSizeUpdate(destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } } if (batched_size_update_contained_draw_) { batched_size_update_contained_draw_ = false; RequestPaint(); } } VirtualKey GTKWindow::TranslateVirtualKey(guint keyval) { switch (keyval) { case GDK_KEY_a: case GDK_KEY_A: return VirtualKey::kA; case GDK_KEY_b: case GDK_KEY_B: return VirtualKey::kB; case GDK_KEY_c: case GDK_KEY_C: return VirtualKey::kC; case GDK_KEY_d: case GDK_KEY_D: return VirtualKey::kD; case GDK_KEY_e: case GDK_KEY_E: return VirtualKey::kE; case GDK_KEY_f: case GDK_KEY_F: return VirtualKey::kF; case GDK_KEY_g: case GDK_KEY_G: return VirtualKey::kG; case GDK_KEY_h: case GDK_KEY_H: return VirtualKey::kH; case GDK_KEY_i: case GDK_KEY_I: return VirtualKey::kI; case GDK_KEY_j: case GDK_KEY_J: return VirtualKey::kJ; case GDK_KEY_k: case GDK_KEY_K: return VirtualKey::kK; case GDK_KEY_l: case GDK_KEY_L: return VirtualKey::kL; case GDK_KEY_m: case GDK_KEY_M: return VirtualKey::kM; case GDK_KEY_n: case GDK_KEY_N: return VirtualKey::kN; case GDK_KEY_o: case GDK_KEY_O: return VirtualKey::kO; case GDK_KEY_p: case GDK_KEY_P: return VirtualKey::kP; case GDK_KEY_q: case GDK_KEY_Q: return VirtualKey::kQ; case GDK_KEY_r: case GDK_KEY_R: return VirtualKey::kR; case GDK_KEY_s: case GDK_KEY_S: return VirtualKey::kS; case GDK_KEY_t: case GDK_KEY_T: return VirtualKey::kT; case GDK_KEY_u: case GDK_KEY_U: return VirtualKey::kU; case GDK_KEY_v: case GDK_KEY_V: return VirtualKey::kV; case GDK_KEY_w: case GDK_KEY_W: return VirtualKey::kW; case GDK_KEY_x: case GDK_KEY_X: return VirtualKey::kX; case GDK_KEY_y: case GDK_KEY_Y: return VirtualKey::kY; case GDK_KEY_z: case GDK_KEY_Z: return VirtualKey::kZ; case GDK_KEY_0: return VirtualKey::k0; case GDK_KEY_1: return VirtualKey::k1; case GDK_KEY_2: return VirtualKey::k2; case GDK_KEY_3: return VirtualKey::k3; case GDK_KEY_4: return VirtualKey::k4; case GDK_KEY_5: return VirtualKey::k5; case GDK_KEY_6: return VirtualKey::k6; case GDK_KEY_7: return VirtualKey::k7; case GDK_KEY_8: return VirtualKey::k8; case GDK_KEY_9: return VirtualKey::k9; case GDK_KEY_semicolon: return VirtualKey::kOem1; case GDK_KEY_apostrophe: return VirtualKey::kOem7; case GDK_KEY_comma: return VirtualKey::kOemComma; case GDK_KEY_period: return VirtualKey::kOemPeriod; case GDK_KEY_Up: return VirtualKey::kUp; case GDK_KEY_Down: return VirtualKey::kDown; case GDK_KEY_Left: return VirtualKey::kLeft; case GDK_KEY_Right: return VirtualKey::kRight; case GDK_KEY_BackSpace: return VirtualKey::kBack; case GDK_KEY_Tab: return VirtualKey::kTab; case GDK_KEY_Return: return VirtualKey::kReturn; case GDK_KEY_Control_L: return VirtualKey::kLControl; case GDK_KEY_Control_R: return VirtualKey::kRControl; case GDK_KEY_Alt_L: return VirtualKey::kLMenu; case GDK_KEY_Alt_R: return VirtualKey::kRMenu; case GDK_KEY_Shift_L: return VirtualKey::kLShift; case GDK_KEY_Shift_R: return VirtualKey::kRShift; case GDK_KEY_space: return VirtualKey::kSpace; case GDK_KEY_Caps_Lock: return VirtualKey::kCapital; case GDK_KEY_Escape: return VirtualKey::kEscape; case GDK_KEY_F1: return VirtualKey::kF1; case GDK_KEY_F2: return VirtualKey::kF2; case GDK_KEY_F3: return VirtualKey::kF3; case GDK_KEY_F4: return VirtualKey::kF4; case GDK_KEY_F5: return VirtualKey::kF5; case GDK_KEY_F6: return VirtualKey::kF6; case GDK_KEY_F7: return VirtualKey::kF7; case GDK_KEY_F8: return VirtualKey::kF8; case GDK_KEY_F9: return VirtualKey::kF9; case GDK_KEY_F10: return VirtualKey::kF10; case GDK_KEY_F11: return VirtualKey::kF11; case GDK_KEY_F12: return VirtualKey::kF12; case GDK_KEY_KP_Multiply: return VirtualKey::kMultiply; case GDK_KEY_KP_Add: return VirtualKey::kAdd; case GDK_KEY_KP_Subtract: return VirtualKey::kSubtract; case GDK_KEY_KP_Divide: return VirtualKey::kDivide; case GDK_KEY_Pause: case GDK_KEY_Break: return VirtualKey::kPause; default: XELOGW("Unhandled key code: {}", keyval); return VirtualKey(keyval); } } bool GTKWindow::HandleMouse(GdkEvent* event, WindowDestructionReceiver& destruction_receiver) { MouseEvent::Button button = MouseEvent::Button::kNone; int32_t x = 0; int32_t y = 0; int32_t scroll_x = 0; int32_t scroll_y = 0; switch (event->type) { case GDK_MOTION_NOTIFY: { auto motion_event = reinterpret_cast(event); x = motion_event->x; y = motion_event->y; } break; case GDK_BUTTON_PRESS: case GDK_BUTTON_RELEASE: { auto button_event = reinterpret_cast(event); switch (button_event->button) { case 1: button = MouseEvent::Button::kLeft; break; case 2: button = MouseEvent::Button::kMiddle; break; case 3: button = MouseEvent::Button::kRight; break; case 4: button = MouseEvent::Button::kX1; break; case 5: button = MouseEvent::Button::kX2; break; default: // Still handle the movement. break; } x = button_event->x; y = button_event->y; } break; case GDK_SCROLL: { auto scroll_event = reinterpret_cast(event); x = scroll_event->x; y = scroll_event->y; scroll_x = scroll_event->delta_x * MouseEvent::kScrollPerDetent; // In GDK, positive is towards the bottom of the screen, not forward from // the user. scroll_y = -scroll_event->delta_y * MouseEvent::kScrollPerDetent; } break; default: return false; } MouseEvent e(this, button, x, y, scroll_x, scroll_y); switch (event->type) { case GDK_MOTION_NOTIFY: OnMouseMove(e, destruction_receiver); break; case GDK_BUTTON_PRESS: OnMouseDown(e, destruction_receiver); break; case GDK_BUTTON_RELEASE: OnMouseUp(e, destruction_receiver); break; case GDK_SCROLL: OnMouseWheel(e, destruction_receiver); break; default: break; } // Returning immediately anyway - no need to check // destruction_receiver.IsWindowDestroyed(). return e.is_handled(); } bool GTKWindow::HandleKeyboard( GdkEventKey* event, WindowDestructionReceiver& destruction_receiver) { unsigned int modifiers = event->state; bool shift_pressed = modifiers & GDK_SHIFT_MASK; bool ctrl_pressed = modifiers & GDK_CONTROL_MASK; bool alt_pressed = modifiers & GDK_META_MASK; bool super_pressed = modifiers & GDK_SUPER_MASK; // Translate GTK to VK VirtualKey vk = TranslateVirtualKey(event->keyval); uint32_t unicode_char = gdk_keyval_to_unicode(event->keyval); bool is_key_pressed = false; // Backspace has unicode value but is not printable therefore we want // OnKeyDown not OnKeyChar if (unicode_char > 0) { if (std::isprint(unicode_char)) { is_key_pressed = true; vk = static_cast(unicode_char); } } KeyEvent e(this, vk, 1, event->type == GDK_KEY_RELEASE, shift_pressed, ctrl_pressed, alt_pressed, super_pressed); switch (event->type) { case GDK_KEY_PRESS: if (is_key_pressed) { OnKeyChar(e, destruction_receiver); } else { OnKeyDown(e, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return e.is_handled(); } } break; case GDK_KEY_RELEASE: OnKeyUp(e, destruction_receiver); break; default: break; } // Returning immediately anyway - no need to check // destruction_receiver.IsWindowDestroyed(). return e.is_handled(); } gboolean GTKWindow::WindowEventHandler(GdkEvent* event) { switch (event->type) { case GDK_DELETE: // In case the widget was somehow forcibly destroyed without GDK_DELETE. case GDK_DESTROY: { WindowDestructionReceiver destruction_receiver(this); OnBeforeClose(destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { break; } // Set window_ to null to ignore events from now on since this // ui::GTKWindow is entering an indeterminate state - this should be done // at some point in closing anyway. GtkWidget* window = window_; window_ = nullptr; // Destroying the top-level window also destroys its children. drawing_area_ = nullptr; box_ = nullptr; if (event->type != GDK_DESTROY) { gtk_widget_destroy(window); } OnAfterClose(); } break; case GDK_FOCUS_CHANGE: { auto focus_event = reinterpret_cast(event); WindowDestructionReceiver destruction_receiver(this); OnFocusUpdate(bool(focus_event->in), destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { break; } } break; case GDK_KEY_PRESS: case GDK_KEY_RELEASE: { WindowDestructionReceiver destruction_receiver(this); HandleKeyboard(reinterpret_cast(event), destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { break; } } break; default: break; } // The window might have been destroyed by the handlers, don't interact with // *this in this function from now on. return GDK_EVENT_PROPAGATE; } gboolean GTKWindow::WindowEventHandlerThunk(GtkWidget* widget, GdkEvent* event, gpointer user_data) { auto* window = static_cast(user_data); if (!window || widget != window->window_ || reinterpret_cast(event)->window != gtk_widget_get_window(window->window_)) { return GDK_EVENT_PROPAGATE; } return window->WindowEventHandler(event); } gboolean GTKWindow::DrawingAreaEventHandler(GdkEvent* event) { switch (event->type) { case GDK_MOTION_NOTIFY: case GDK_BUTTON_PRESS: case GDK_BUTTON_RELEASE: case GDK_SCROLL: { WindowDestructionReceiver destruction_receiver(this); HandleMouse(event, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { break; } } break; case GDK_CONFIGURE: { if (batched_size_update_depth_) { batched_size_update_contained_configure_ = true; } else { WindowDestructionReceiver destruction_receiver(this); HandleSizeUpdate(destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { break; } } } break; default: break; } // The window might have been destroyed by the handlers, don't interact with // *this in this function from now on. return GDK_EVENT_PROPAGATE; } gboolean GTKWindow::DrawingAreaEventHandlerThunk(GtkWidget* widget, GdkEvent* event, gpointer user_data) { auto* window = static_cast(user_data); if (!window || widget != window->drawing_area_ || reinterpret_cast(event)->window != gtk_widget_get_window(window->drawing_area_)) { return GDK_EVENT_PROPAGATE; } return window->DrawingAreaEventHandler(event); } gboolean GTKWindow::DrawHandler(GtkWidget* widget, cairo_t* cr, gpointer user_data) { auto* window = static_cast(user_data); if (!window || widget != window->drawing_area_) { return false; } if (window->batched_size_update_depth_) { window->batched_size_update_contained_draw_ = true; } else { window->OnPaint(); } return true; } std::unique_ptr MenuItem::Create(Type type, const std::string& text, const std::string& hotkey, std::function callback) { return std::make_unique(type, text, hotkey, callback); } void GTKMenuItem::ActivateHandler(GtkWidget* menu_item, gpointer user_data) { static_cast(user_data)->OnSelected(); // The menu item might have been destroyed by its OnSelected, don't do // anything with it here from now on. } 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 auto* gtk_label = 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_mnemonic(gtk_label); break; case MenuItem::Type::kSeparator: menu_ = gtk_separator_menu_item_new(); break; case MenuItem::Type::kString: if (!hotkey.empty()) { // Create a box to hold the label and the accelerator GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // Create the main label with mnemonic GtkWidget* label_widget = gtk_label_new_with_mnemonic(gtk_label); gtk_label_set_xalign(GTK_LABEL(label_widget), 0.0); gtk_box_pack_start(GTK_BOX(box), label_widget, FALSE, FALSE, 0); // Create the accelerator label (right-aligned) GtkWidget* accel_label = gtk_label_new(hotkey.c_str()); gtk_widget_set_margin_start(accel_label, 20); gtk_box_pack_end(GTK_BOX(box), accel_label, FALSE, FALSE, 0); // Create menu item and add the box menu_ = gtk_menu_item_new(); gtk_container_add(GTK_CONTAINER(menu_), box); gtk_widget_show_all(box); } else { menu_ = gtk_menu_item_new_with_mnemonic(gtk_label); } break; } if (menu_) { // Own the object because it may be detached from and re-attached to a // Window. g_object_ref_sink(menu_); if (GTK_IS_MENU_ITEM(menu_)) { g_signal_connect(menu_, "activate", G_CALLBACK(ActivateHandler), this); } } } GTKMenuItem::~GTKMenuItem() { if (menu_) { g_object_unref(menu_); } } void GTKMenuItem::OnChildAdded(MenuItem* generic_child_item) { auto child_item = dynamic_cast(generic_child_item); GtkWidget* submenu = nullptr; switch (child_item->type()) { case Type::kNormal: // Nothing special. break; case Type::kPopup: if (GTK_IS_MENU_ITEM(menu_)) { submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_)); // Get sub menu and if it doesn't exist create it if (submenu == nullptr) { submenu = gtk_menu_new(); gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_), submenu); } gtk_menu_shell_append(GTK_MENU_SHELL(submenu), child_item->handle()); } else { gtk_menu_shell_append(GTK_MENU_SHELL(menu_), child_item->handle()); } break; case Type::kSeparator: case Type::kString: assert(GTK_IS_MENU_ITEM(menu_)); // Get sub menu and if it doesn't exist create it submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_)); if (submenu == nullptr) { submenu = gtk_menu_new(); gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_), submenu); } gtk_menu_shell_append(GTK_MENU_SHELL(submenu), child_item->handle()); break; } } void GTKMenuItem::OnChildRemoved(MenuItem* generic_child_item) {} } // namespace ui } // namespace xe