I had a need to have a menu on Linux with a menu with a sub menu nested inside a sub menu. I'm not proficient in C++ so after I fumbled around for a while, I ultimately came up with this solution. I'll let it in this issue since I doubt this hackery is worthy of a PR. All edits were made inside native_context_menu_plugin_handle_method_call
:
static void native_context_menu_plugin_handle_method_call(
NativeContextMenuPlugin* self, FlMethodCall* method_call) {
g_autoptr(FlMethodResponse) response = nullptr;
const gchar* method = fl_method_call_get_name(method_call);
if (strcmp(method, kShowMenu) == 0) {
// Clear previously saved object instances.
self->last_menu_items.clear();
self->last_menu_item_selected = false;
if (self->last_menu_thread != nullptr) {
self->last_menu_thread->detach();
self->last_menu_thread.reset(nullptr);
}
auto arguments = fl_method_call_get_args(method_call);
auto device_pixel_ratio =
fl_value_lookup_string(arguments, "devicePixelRatio");
auto position = fl_value_lookup_string(arguments, "position");
GdkWindow* window = get_window(self);
GtkWidget* top_level_menu = gtk_menu_new();
auto top_level_items = fl_value_lookup_string(arguments, "items");
for (int32_t i = 0; i < fl_value_get_length(top_level_items); i++) {
int32_t top_level_id = fl_value_get_int(
fl_value_lookup_string(fl_value_get_list_value(top_level_items, i), "id"));
const char* top_level_title = fl_value_get_string(
fl_value_lookup_string(fl_value_get_list_value(top_level_items, i), "title"));
auto second_level_items =
fl_value_lookup_string(fl_value_get_list_value(top_level_items, i), "items");
self->last_menu_items.emplace_back(std::make_unique<MenuItem>(top_level_id, top_level_title));
GtkWidget* top_level_item = gtk_menu_item_new_with_label(top_level_title);
// Check for second level items & create a second level menu.
if (fl_value_get_length(second_level_items) > 0) {
GtkWidget* second_level_menu = gtk_menu_new();
for (int32_t j = 0; j < fl_value_get_length(second_level_items); j++) {
int32_t second_level_id = fl_value_get_int(fl_value_lookup_string(
fl_value_get_list_value(second_level_items, j), "id"));
const char* second_level_title = fl_value_get_string(fl_value_lookup_string(
fl_value_get_list_value(second_level_items, j), "title"));
auto third_level_items =
fl_value_lookup_string(fl_value_get_list_value(second_level_items, j), "items");
self->last_menu_items.back()->items().emplace_back(
std::make_unique<MenuItem>(second_level_id, second_level_title));
GtkWidget* second_level_item = gtk_menu_item_new_with_label(second_level_title);
// Check for third level items & create a third level menu.
if (fl_value_get_length(third_level_items) > 0) {
GtkWidget* third_level_menu = gtk_menu_new();
for (int32_t k = 0; k < fl_value_get_length(third_level_items); k++) {
int32_t third_level_id = fl_value_get_int(fl_value_lookup_string(
fl_value_get_list_value(third_level_items, k), "id"));
const char* third_level_title = fl_value_get_string(fl_value_lookup_string(
fl_value_get_list_value(third_level_items, k), "title"));
self->last_menu_items.back()->items().back()->items().emplace_back(
std::make_unique<MenuItem>(third_level_id, third_level_title));
GtkWidget* third_level_item = gtk_menu_item_new_with_label(third_level_title);
gtk_widget_show(third_level_item);
gtk_menu_shell_append(GTK_MENU_SHELL(third_level_menu), third_level_item);
g_signal_connect(
G_OBJECT(third_level_item), "activate",
G_CALLBACK(on_menu_item_clicked),
(gpointer)self->last_menu_items.back()->items().at(j).get()->items().at(k).get());
}
gtk_menu_item_set_submenu(GTK_MENU_ITEM(second_level_item), third_level_menu);
} else {
g_signal_connect(G_OBJECT(second_level_item), "activate",
G_CALLBACK(on_menu_item_clicked),
(gpointer)self->last_menu_items.back()->items().at(j).get());
}
gtk_widget_show(second_level_item);
gtk_menu_shell_append(GTK_MENU_SHELL(second_level_menu), second_level_item);
}
gtk_menu_item_set_submenu(GTK_MENU_ITEM(top_level_item), second_level_menu);
} else {
g_signal_connect(G_OBJECT(top_level_item), "activate",
G_CALLBACK(on_menu_item_clicked),
(gpointer)self->last_menu_items.back().get());
}
gtk_widget_show(top_level_item);
gtk_menu_shell_append(GTK_MENU_SHELL(top_level_menu), top_level_item);
}
GdkRectangle rectangle;
// Pass `devicePixelRatio` and `position` from Dart to show menu at
// specified coordinates. If it is not defined, WIN32 will use
// `GetCursorPos` to show the context menu at the cursor's position.
if (device_pixel_ratio != nullptr && position != nullptr) {
rectangle.x = fl_value_get_float(fl_value_get_list_value(position, 0)) *
fl_value_get_float(device_pixel_ratio);
rectangle.y = fl_value_get_float(fl_value_get_list_value(position, 1)) *
fl_value_get_float(device_pixel_ratio);
} else {
GdkDevice* mouse_device;
int x, y;
// Legacy support.
#if GTK_CHECK_VERSION(3, 20, 0)
GdkSeat* seat = gdk_display_get_default_seat(gdk_display_get_default());
mouse_device = gdk_seat_get_pointer(seat);
#else
GdkDeviceManager* devman =
gdk_display_get_device_manager(gdk_display_get_default());
mouse_device = gdk_device_manager_get_client_pointer(devman);
#endif
gdk_window_get_device_position(window, mouse_device, &x, &y, NULL);
rectangle.x = x;
rectangle.y = y;
}
g_signal_connect(G_OBJECT(top_level_menu), "deactivate",
G_CALLBACK(on_menu_deactivated), nullptr);
// `gtk_menu_popup_at_rect` is used since `gtk_menu_popup_at_pointer` will
// require event box creation & another callback will be involved. This way
// is straight forward & easy to work with.
// NOTE: GDK_GRAVITY_NORTH_WEST is hard-coded by default since no analog is
// present for it inside the Dart platform channel code (as of now). In
// summary, this will create a menu whose body is in bottom-right to the
// position of the mouse pointer.
gtk_menu_popup_at_rect(GTK_MENU(top_level_menu), window, &rectangle,
GDK_GRAVITY_NORTH_WEST, GDK_GRAVITY_NORTH_WEST,
NULL);
// Responding with `null`, click event & respective `id` of the `MenuItem`
// is notified through callback. Otherwise the GUI will become unresponsive.
// To keep the API same, a `Completer` is used in the Dart.
response =
FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_null()));
} else {
response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
}
fl_method_call_respond(method_call, response, nullptr);
}