/* * Copyright 2012 Canonical Ltd. * Copyright 2014 Javier S. Pedro * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Authors: Ryan Lortie * William Hua * Javier S. Pedro */ #include #include #include "../global.h" #include "../libtopmenu-client/topmenu-client.h" #include "../libtopmenu-client/topmenu-monitor.h" #include "menuitem-proxy.h" #include "appmenu.h" #include "data.h" static gboolean already_initialized = FALSE; static void (* pre_hijacked_window_realize) (GtkWidget *widget); static void (* pre_hijacked_window_unrealize) (GtkWidget *widget); #if GTK_MAJOR_VERSION == 3 static void (* pre_hijacked_application_window_realize) (GtkWidget *widget); #endif static void (* pre_hijacked_menu_bar_realize) (GtkWidget *widget); static void (* pre_hijacked_menu_bar_unrealize) (GtkWidget *widget); static void (* pre_hijacked_widget_size_allocate) (GtkWidget *widget, GtkAllocation *allocation); static void (* pre_hijacked_menu_bar_size_allocate) (GtkWidget *widget, GtkAllocation *allocation); #if GTK_MAJOR_VERSION == 2 static void (* pre_hijacked_menu_bar_size_request) (GtkWidget *widget, GtkRequisition *requisition); #elif GTK_MAJOR_VERSION == 3 static void (* pre_hijacked_menu_bar_get_preferred_width) (GtkWidget *widget, gint *minimum_width, gint *natural_width); static void (* pre_hijacked_menu_bar_get_preferred_height) (GtkWidget *widget, gint *minimum_height, gint *natural_height); static void (* pre_hijacked_menu_bar_get_preferred_width_for_height) (GtkWidget *widget, gint height, gint *minimum_width, gint *natural_width); static void (* pre_hijacked_menu_bar_get_preferred_height_for_width) (GtkWidget *widget, gint width, gint *minimum_height, gint *natural_height); #endif #if GTK_MAJOR_VERSION == 2 static void (* pre_hijacked_menu_item_select) (GtkItem *item); static void (* pre_hijacked_menu_item_deselect) (GtkItem *item); #elif GTK_MAJOR_VERSION == 3 static void (* pre_hijacked_menu_item_select) (GtkMenuItem *item); static void (* pre_hijacked_menu_item_deselect) (GtkMenuItem *item); #endif static void handle_should_hide_menubar_updated (GObject *object, GParamSpec *pspec, gpointer user_data); static void count_container_items_helper (GtkWidget *widget, gpointer data) { gint *count = data; (*count)++; } static gint count_container_items (GtkContainer *container) { gint count = 0; gtk_container_foreach (container, count_container_items_helper, &count); return count; } static gboolean topmenu_should_hide_menubar_on_window (GtkWindow *window) { TopMenuMonitor *monitor = topmenu_monitor_get_instance (); if (topmenu_is_window_blacklisted (window)) return FALSE; return monitor->available; } static gboolean topmenu_should_hide_menubar (GtkWidget *widget) { GtkWindow *window = GTK_WINDOW (gtk_widget_get_toplevel (widget)); g_return_val_if_fail (GTK_IS_MENU_SHELL (widget), FALSE); return topmenu_should_hide_menubar_on_window (window); } static void topmenu_prepare_window (GtkWindow *window) { WindowData *window_data; g_return_if_fail (GTK_IS_WINDOW (window)); g_return_if_fail (gtk_widget_get_realized (GTK_WIDGET (window))); window_data = topmenu_get_window_data (window); if (window_data == NULL) return; // Window is ignored if (window_data->appmenubar != 0) return; // Already prepared window_data->appmenubar = TOPMENU_APP_MENU_BAR (topmenu_app_menu_bar_new ()); gtk_widget_show(GTK_WIDGET(window_data->appmenubar)); topmenu_app_menu_bar_set_app_menu (window_data->appmenubar, topmenu_appmenu_build(&window_data->appmenu)); } static void topmenu_connect_window (GtkWindow *window) { WindowData *window_data; g_return_if_fail (GTK_IS_WINDOW (window)); g_return_if_fail (gtk_widget_get_realized (GTK_WIDGET (window))); window_data = topmenu_get_window_data (window); if (window_data == NULL) return; // Window is ignored if (window_data->monitor_connection_id != 0) return; // Already connected g_return_if_fail (window_data->menus != NULL); // Must contain one menu at least. g_return_if_fail (window_data->appmenubar != NULL); // Must be prepared TopMenuMonitor *monitor = topmenu_monitor_get_instance(); window_data->monitor_connection_id = g_signal_connect(monitor, "notify::available", G_CALLBACK (handle_should_hide_menubar_updated), window); topmenu_client_connect_window_widget (gtk_widget_get_window (GTK_WIDGET (window)), GTK_WIDGET (window_data->appmenubar)); } static void topmenu_disconnect_window (GtkWindow *window) { WindowData *window_data; g_return_if_fail (GTK_IS_WINDOW (window)); window_data = topmenu_get_window_data (window); if (window_data == NULL) return; // Already disconnected or ignored if (window_data->monitor_connection_id == 0) return; // Already disconnected TopMenuMonitor *monitor = topmenu_monitor_get_instance(); g_signal_handler_disconnect(monitor, window_data->monitor_connection_id); window_data->monitor_connection_id = 0; if (window_data->appmenu.menu) topmenu_appmenu_destroy(&window_data->appmenu); if (window_data->appmenubar) { gtk_widget_destroy (GTK_WIDGET (window_data->appmenubar)); window_data->appmenubar = NULL; } if (gtk_widget_get_realized (GTK_WIDGET(window))) { topmenu_client_disconnect_window (gtk_widget_get_window (GTK_WIDGET (window))); } } static gint compute_shell_position_in_appmenu (WindowData *window_data, GtkMenuShell *menu_shell) { GSList *iter; gint position = 1; // Skip app_menu_item for (iter = window_data->menus; iter; iter = g_slist_next (iter)) { if (iter->data == menu_shell) { return position; } position += count_container_items (GTK_CONTAINER (iter->data)); } return -1; } static void add_menu_item_to_appmenu (WindowData *window_data, GtkMenuItem *item, gint position) { MenuItemData *item_data = topmenu_get_menu_item_data (item); item_data->proxy = topmenu_create_proxy_menu_item (item); gtk_menu_shell_insert (GTK_MENU_SHELL (window_data->appmenubar), GTK_WIDGET (item_data->proxy), position); } static void remove_menu_item_from_appmenu (WindowData *window_data, GtkMenuItem *item) { MenuItemData *item_data = topmenu_get_menu_item_data (item); if (item_data->proxy) { gtk_widget_destroy (GTK_WIDGET (item_data->proxy)); item_data->proxy = NULL; } } static void handle_shell_insert (GtkMenuShell *menu_shell, GtkWidget *child, gint position, WindowData *window_data) { GtkMenuItem *item = GTK_MENU_ITEM (child); gint offset = compute_shell_position_in_appmenu (window_data, menu_shell); g_return_if_fail (offset >= 0); if (position < 0) { // Gtk internally handles position=-1 as "append". // So we add this item after all the other items from this shell gint n_items = count_container_items (GTK_CONTAINER (menu_shell)) - 1; // -1 because count_container_items will count the item we're just adding add_menu_item_to_appmenu (window_data, item, offset + n_items); } else { add_menu_item_to_appmenu (window_data, item, offset + position); } } static void handle_shell_remove (GtkMenuShell *menu_shell, GtkWidget *widget, WindowData *window_data) { GtkMenuItem *item = GTK_MENU_ITEM (widget); remove_menu_item_from_appmenu (window_data, item); } typedef struct _AddShellCbData { WindowData *window_data; gint position; } AddShellCbData; static void add_shell_cb (GtkWidget *widget, gpointer user_data) { AddShellCbData *data = user_data; GtkMenuItem *item = GTK_MENU_ITEM (widget); add_menu_item_to_appmenu (data->window_data, item, data->position); data->position++; } static void add_shell_to_appmenu (WindowData *window_data, GtkMenuShell *menu_shell) { AddShellCbData data; data.window_data = window_data; data.position = compute_shell_position_in_appmenu (window_data, menu_shell); g_warn_if_fail (data.position >= 0); gtk_container_foreach (GTK_CONTAINER (menu_shell), add_shell_cb, &data); topmenu_appmenu_scan_for_items (&window_data->appmenu, menu_shell); g_signal_connect (menu_shell, "insert", G_CALLBACK (handle_shell_insert), window_data); g_signal_connect (menu_shell, "remove", G_CALLBACK (handle_shell_remove), window_data); } static void remove_shell_cb (GtkWidget *widget, gpointer user_data) { WindowData *window_data = user_data; GtkMenuItem *item = GTK_MENU_ITEM (widget); remove_menu_item_from_appmenu (window_data, item); } static void remove_shell_from_appmenu (WindowData *window_data, GtkMenuShell *menu_shell) { gtk_container_foreach (GTK_CONTAINER (menu_shell), remove_shell_cb, window_data); g_signal_handlers_disconnect_by_data (menu_shell, window_data); } static void topmenu_disconnect_menu_shell (GtkWindow *window, GtkMenuShell *menu_shell) { WindowData *window_data; MenuShellData *menu_shell_data; g_return_if_fail (GTK_IS_WINDOW (window)); g_return_if_fail (GTK_IS_MENU_SHELL (menu_shell)); menu_shell_data = topmenu_get_menu_shell_data (menu_shell); g_warn_if_fail (window == menu_shell_data->window); window_data = topmenu_get_window_data (menu_shell_data->window); if (window_data != NULL) { GSList *iter; for (iter = window_data->menus; iter != NULL; iter = g_slist_next (iter)) if (GTK_MENU_SHELL(iter->data) == menu_shell) break; if (iter != NULL) { GtkMenuShell *menu_shell = GTK_MENU_SHELL(iter->data); remove_shell_from_appmenu (window_data, menu_shell); window_data->menus = g_slist_delete_link (window_data->menus, iter); } } menu_shell_data->window = NULL; } static void topmenu_connect_menu_shell (GtkWindow *window, GtkMenuShell *menu_shell) { MenuShellData *menu_shell_data; g_return_if_fail (GTK_IS_WINDOW (window)); g_return_if_fail (GTK_IS_MENU_SHELL (menu_shell)); menu_shell_data = topmenu_get_menu_shell_data (menu_shell); if (window != menu_shell_data->window) { WindowData *window_data; if (menu_shell_data->window != NULL) topmenu_disconnect_menu_shell (menu_shell_data->window, menu_shell); window_data = topmenu_get_window_data (window); if (window_data != NULL) { GSList *iter; for (iter = window_data->menus; iter != NULL; iter = g_slist_next (iter)) if (GTK_MENU_SHELL(iter->data) == menu_shell) break; if (iter == NULL) { topmenu_prepare_window (window); window_data->menus = g_slist_append (window_data->menus, menu_shell); add_shell_to_appmenu (window_data, menu_shell); topmenu_connect_window (window); // Does nothing if already connected } } menu_shell_data->window = window; } } static void handle_should_hide_menubar_updated (GObject *object, GParamSpec *pspec, gpointer user_data) { g_return_if_fail (GTK_IS_WINDOW (user_data)); GtkWindow *window = GTK_WINDOW (user_data); WindowData *window_data = topmenu_get_window_data (window); GSList *iter; for (iter = window_data->menus; iter != NULL; iter = g_slist_next (iter)) { gtk_widget_queue_resize (GTK_WIDGET (iter->data)); } } static void hijacked_window_realize (GtkWidget *widget) { g_return_if_fail (GTK_IS_WINDOW (widget)); if (pre_hijacked_window_realize != NULL) (* pre_hijacked_window_realize) (widget); #if GTK_MAJOR_VERSION == 3 if (!GTK_IS_APPLICATION_WINDOW (widget)) #endif topmenu_get_window_data (GTK_WINDOW (widget)); } static void hijacked_window_unrealize (GtkWidget *widget) { g_return_if_fail (GTK_IS_WINDOW (widget)); if (pre_hijacked_window_unrealize != NULL) (* pre_hijacked_window_unrealize) (widget); topmenu_disconnect_window (GTK_WINDOW (widget)); topmenu_remove_window_data (GTK_WINDOW (widget)); } #if GTK_MAJOR_VERSION == 3 static void hijacked_application_window_realize (GtkWidget *widget) { g_return_if_fail (GTK_IS_APPLICATION_WINDOW (widget)); if (pre_hijacked_application_window_realize != NULL) (* pre_hijacked_application_window_realize) (widget); topmenu_get_window_data (GTK_WINDOW (widget)); } #endif static void hijacked_menu_bar_realize (GtkWidget *widget) { GtkWidget *window; g_return_if_fail (GTK_IS_MENU_BAR (widget)); if (pre_hijacked_menu_bar_realize != NULL) (* pre_hijacked_menu_bar_realize) (widget); window = gtk_widget_get_toplevel (widget); if (GTK_IS_WINDOW (window)) topmenu_connect_menu_shell (GTK_WINDOW (window), GTK_MENU_SHELL (widget)); } static void hijacked_menu_bar_unrealize (GtkWidget *widget) { MenuShellData *menu_shell_data; g_return_if_fail (GTK_IS_MENU_BAR (widget)); if (pre_hijacked_menu_bar_unrealize != NULL) (* pre_hijacked_menu_bar_unrealize) (widget); menu_shell_data = topmenu_get_menu_shell_data (GTK_MENU_SHELL (widget)); if (menu_shell_data->window != NULL) topmenu_disconnect_menu_shell (menu_shell_data->window, GTK_MENU_SHELL (widget)); } static void hijacked_menu_bar_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { GtkAllocation zero = { 0, 0, 0, 0 }; GdkWindow *window; g_return_if_fail (GTK_IS_MENU_BAR (widget)); if (topmenu_should_hide_menubar (widget)) { /* * We manually assign an empty allocation to the menu bar to * prevent the container from attempting to draw it at all. */ if (pre_hijacked_widget_size_allocate != NULL) (* pre_hijacked_widget_size_allocate) (widget, &zero); /* * Then we move the GdkWindow belonging to the menu bar outside of * the clipping rectangle of the parent window so that we can't * see it. */ window = gtk_widget_get_window (widget); if (window != NULL) gdk_window_move_resize (window, -1, -1, 1, 1); } else if (pre_hijacked_menu_bar_size_allocate != NULL) (* pre_hijacked_menu_bar_size_allocate) (widget, allocation); } #if GTK_MAJOR_VERSION == 2 static void hijacked_menu_bar_size_request (GtkWidget *widget, GtkRequisition *requisition) { g_return_if_fail (GTK_IS_MENU_BAR (widget)); if (pre_hijacked_menu_bar_size_request != NULL) (* pre_hijacked_menu_bar_size_request) (widget, requisition); if (topmenu_should_hide_menubar(widget)) { requisition->width = 0; requisition->height = 0; } } #elif GTK_MAJOR_VERSION == 3 static void hijacked_menu_bar_get_preferred_width (GtkWidget *widget, gint *minimum_width, gint *natural_width) { g_return_if_fail (GTK_IS_MENU_BAR (widget)); if (pre_hijacked_menu_bar_get_preferred_width != NULL) (* pre_hijacked_menu_bar_get_preferred_width) (widget, minimum_width, natural_width); if (topmenu_should_hide_menubar(widget)) { *minimum_width = 0; *natural_width = 0; } } static void hijacked_menu_bar_get_preferred_height (GtkWidget *widget, gint *minimum_height, gint *natural_height) { g_return_if_fail (GTK_IS_MENU_BAR (widget)); if (pre_hijacked_menu_bar_get_preferred_height != NULL) (* pre_hijacked_menu_bar_get_preferred_height) (widget, minimum_height, natural_height); if (topmenu_should_hide_menubar(widget)) { *minimum_height = 0; *natural_height = 0; } } static void hijacked_menu_bar_get_preferred_width_for_height (GtkWidget *widget, gint height, gint *minimum_width, gint *natural_width) { g_return_if_fail (GTK_IS_MENU_BAR (widget)); if (pre_hijacked_menu_bar_get_preferred_width_for_height != NULL) (* pre_hijacked_menu_bar_get_preferred_width_for_height) (widget, height, minimum_width, natural_width); if (topmenu_should_hide_menubar(widget)) { *minimum_width = 0; *natural_width = 0; } } static void hijacked_menu_bar_get_preferred_height_for_width (GtkWidget *widget, gint width, gint *minimum_height, gint *natural_height) { g_return_if_fail (GTK_IS_MENU_BAR (widget)); if (pre_hijacked_menu_bar_get_preferred_height_for_width != NULL) (* pre_hijacked_menu_bar_get_preferred_height_for_width) (widget, width, minimum_height, natural_height); if (topmenu_should_hide_menubar(widget)) { *minimum_height = 0; *natural_height = 0; } } #endif static gboolean topmenu_should_allow_select_signal (GtkMenuItem *item) { MenuItemData *item_data = topmenu_get_menu_item_data (item); if (item_data) { GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (item)); if (parent) { if (GTK_IS_MENU_BAR (parent) && topmenu_should_hide_menubar (parent)) { return FALSE; } } } return TRUE; } #if GTK_MAJOR_VERSION == 2 static void hijacked_menu_item_select (GtkItem *item) { g_return_if_fail (GTK_IS_MENU_ITEM (item)); GtkMenuItem *menu_item = GTK_MENU_ITEM (item); if (topmenu_should_allow_select_signal (menu_item)) { if (pre_hijacked_menu_item_select) (* pre_hijacked_menu_item_select) (item); } } static void hijacked_menu_item_deselect (GtkItem *item) { g_return_if_fail (GTK_IS_MENU_ITEM (item)); GtkMenuItem *menu_item = GTK_MENU_ITEM (item); if (topmenu_should_allow_select_signal (menu_item)) { if (pre_hijacked_menu_item_deselect) (* pre_hijacked_menu_item_deselect) (item); } } #elif GTK_MAJOR_VERSION == 3 static void hijacked_menu_item_select (GtkMenuItem *item) { if (topmenu_should_allow_select_signal (item)) { if (pre_hijacked_menu_item_select) (* pre_hijacked_menu_item_select) (item); } } static void hijacked_menu_item_deselect (GtkMenuItem *item) { if (topmenu_should_allow_select_signal (item)) { if (pre_hijacked_menu_item_deselect) (* pre_hijacked_menu_item_deselect) (item); } } #endif static void hijack_window_class_vtable (GType type) { GtkWidgetClass *widget_class = g_type_class_ref (type); GType *children; guint n; guint i; if (widget_class->realize == pre_hijacked_window_realize) widget_class->realize = hijacked_window_realize; #if GTK_MAJOR_VERSION == 3 if (widget_class->realize == pre_hijacked_application_window_realize) widget_class->realize = hijacked_application_window_realize; #endif if (widget_class->unrealize == pre_hijacked_window_unrealize) widget_class->unrealize = hijacked_window_unrealize; children = g_type_children (type, &n); for (i = 0; i < n; i++) hijack_window_class_vtable (children[i]); g_free (children); } static void hijack_menu_bar_class_vtable (GType type) { GtkWidgetClass *widget_class = g_type_class_ref (type); GType *children; guint n; guint i; /* This fixes lp:1113008. */ widget_class->hierarchy_changed = NULL; if (widget_class->realize == pre_hijacked_menu_bar_realize) widget_class->realize = hijacked_menu_bar_realize; if (widget_class->unrealize == pre_hijacked_menu_bar_unrealize) widget_class->unrealize = hijacked_menu_bar_unrealize; if (widget_class->size_allocate == pre_hijacked_menu_bar_size_allocate) widget_class->size_allocate = hijacked_menu_bar_size_allocate; #if GTK_MAJOR_VERSION == 2 if (widget_class->size_request == pre_hijacked_menu_bar_size_request) widget_class->size_request = hijacked_menu_bar_size_request; #elif GTK_MAJOR_VERSION == 3 if (widget_class->get_preferred_width == pre_hijacked_menu_bar_get_preferred_width) widget_class->get_preferred_width = hijacked_menu_bar_get_preferred_width; if (widget_class->get_preferred_height == pre_hijacked_menu_bar_get_preferred_height) widget_class->get_preferred_height = hijacked_menu_bar_get_preferred_height; if (widget_class->get_preferred_width_for_height == pre_hijacked_menu_bar_get_preferred_width_for_height) widget_class->get_preferred_width_for_height = hijacked_menu_bar_get_preferred_width_for_height; if (widget_class->get_preferred_height_for_width == pre_hijacked_menu_bar_get_preferred_height_for_width) widget_class->get_preferred_height_for_width = hijacked_menu_bar_get_preferred_height_for_width; #endif children = g_type_children (type, &n); for (i = 0; i < n; i++) hijack_menu_bar_class_vtable (children[i]); g_free (children); } static void hijack_menu_item_class_vtable (GType type) { #if GTK_MAJOR_VERSION == 2 GtkItemClass *item_class = g_type_class_ref (type); #elif GTK_MAJOR_VERSION == 3 GtkMenuItemClass *item_class = g_type_class_ref (type); #endif if (item_class->select == pre_hijacked_menu_item_select) item_class->select = hijacked_menu_item_select; if (item_class->deselect == pre_hijacked_menu_item_select) item_class->deselect = hijacked_menu_item_deselect; guint n, i; GType *children = g_type_children (type, &n); for (i = 0; i < n; i++) hijack_menu_item_class_vtable (children[i]); g_free (children); } G_MODULE_EXPORT void gtk_module_init(void) { if (!topmenu_is_blacklisted()) { GtkWidgetClass *widget_class; /* gtk_module_init may be called more than once in a resident module */ g_return_if_fail(!already_initialized); already_initialized = TRUE; /* store the base GtkWidget size_allocate vfunc */ widget_class = g_type_class_ref (GTK_TYPE_WIDGET); pre_hijacked_widget_size_allocate = widget_class->size_allocate; #if GTK_MAJOR_VERSION == 3 /* store the base GtkApplicationWindow realize vfunc */ widget_class = g_type_class_ref (GTK_TYPE_APPLICATION_WINDOW); pre_hijacked_application_window_realize = widget_class->realize; #endif /* intercept window realize vcalls on GtkWindow */ widget_class = g_type_class_ref (GTK_TYPE_WINDOW); pre_hijacked_window_realize = widget_class->realize; pre_hijacked_window_unrealize = widget_class->unrealize; hijack_window_class_vtable (GTK_TYPE_WINDOW); /* intercept size request and allocate vcalls on GtkMenuBar (for hiding) */ widget_class = g_type_class_ref (GTK_TYPE_MENU_BAR); pre_hijacked_menu_bar_realize = widget_class->realize; pre_hijacked_menu_bar_unrealize = widget_class->unrealize; pre_hijacked_menu_bar_size_allocate = widget_class->size_allocate; #if GTK_MAJOR_VERSION == 2 pre_hijacked_menu_bar_size_request = widget_class->size_request; #elif GTK_MAJOR_VERSION == 3 pre_hijacked_menu_bar_get_preferred_width = widget_class->get_preferred_width; pre_hijacked_menu_bar_get_preferred_height = widget_class->get_preferred_height; pre_hijacked_menu_bar_get_preferred_width_for_height = widget_class->get_preferred_width_for_height; pre_hijacked_menu_bar_get_preferred_height_for_width = widget_class->get_preferred_height_for_width; #endif hijack_menu_bar_class_vtable (GTK_TYPE_MENU_BAR); /* intercept select/deselect vcalls on GtkMenuItem (for proxying without showing submenu) */ #if GTK_MAJOR_VERSION == 2 GtkItemClass *item_class = g_type_class_ref (GTK_TYPE_MENU_ITEM); #elif GTK_MAJOR_VERSION == 3 GtkMenuItemClass *item_class = g_type_class_ref (GTK_TYPE_MENU_ITEM); #endif pre_hijacked_menu_item_select = item_class->select; pre_hijacked_menu_item_deselect = item_class->deselect; hijack_menu_item_class_vtable (GTK_TYPE_MENU_ITEM); } } G_MODULE_EXPORT const gchar * g_module_check_init(GModule *module) { /* It is hard to unhijack Gtk's vtables, specially if some other module * has decided to also hijack them. * Thus, we just prevent unloading of this module. */ g_module_make_resident(module); return NULL; }