1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/ui/gtk/menu_bar_helper.h" 6 7 #include <algorithm> 8 9 #include "base/logging.h" 10 #include "chrome/browser/ui/gtk/gtk_util.h" 11 #include "ui/base/gtk/gtk_signal_registrar.h" 12 13 namespace { 14 15 // Recursively find all the GtkMenus that are attached to menu item |child| 16 // and add them to |data|, which is a vector of GtkWidgets. 17 void PopulateSubmenus(GtkWidget* child, gpointer data) { 18 std::vector<GtkWidget*>* submenus = 19 static_cast<std::vector<GtkWidget*>*>(data); 20 GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(child)); 21 if (submenu) { 22 submenus->push_back(submenu); 23 gtk_container_foreach(GTK_CONTAINER(submenu), PopulateSubmenus, submenus); 24 } 25 } 26 27 // Is the cursor over |menu| or one of its parent menus? 28 bool MotionIsOverMenu(GtkWidget* menu, GdkEventMotion* motion) { 29 GtkAllocation allocation; 30 gtk_widget_get_allocation(menu, &allocation); 31 32 if (motion->x >= 0 && motion->y >= 0 && 33 motion->x < allocation.width && 34 motion->y < allocation.height) { 35 return true; 36 } 37 38 while (menu) { 39 GtkWidget* menu_item = gtk_menu_get_attach_widget(GTK_MENU(menu)); 40 if (!menu_item) 41 return false; 42 GtkWidget* parent = gtk_widget_get_parent(menu_item); 43 44 if (gtk_util::WidgetContainsCursor(parent)) 45 return true; 46 menu = parent; 47 } 48 49 return false; 50 } 51 52 } // namespace 53 54 MenuBarHelper::MenuBarHelper(Delegate* delegate) 55 : button_showing_menu_(NULL), 56 showing_menu_(NULL), 57 delegate_(delegate) { 58 DCHECK(delegate_); 59 } 60 61 MenuBarHelper::~MenuBarHelper() { 62 } 63 64 void MenuBarHelper::Add(GtkWidget* button) { 65 buttons_.push_back(button); 66 } 67 68 void MenuBarHelper::Remove(GtkWidget* button) { 69 std::vector<GtkWidget*>::iterator iter = 70 find(buttons_.begin(), buttons_.end(), button); 71 if (iter == buttons_.end()) { 72 NOTREACHED(); 73 return; 74 } 75 buttons_.erase(iter); 76 } 77 78 void MenuBarHelper::Clear() { 79 buttons_.clear(); 80 } 81 82 void MenuBarHelper::MenuStartedShowing(GtkWidget* button, GtkWidget* menu) { 83 DCHECK(GTK_IS_MENU(menu)); 84 button_showing_menu_ = button; 85 showing_menu_ = menu; 86 87 signal_handlers_.reset(new ui::GtkSignalRegistrar()); 88 signal_handlers_->Connect(menu, "destroy", 89 G_CALLBACK(OnMenuHiddenOrDestroyedThunk), this); 90 signal_handlers_->Connect(menu, "hide", 91 G_CALLBACK(OnMenuHiddenOrDestroyedThunk), this); 92 signal_handlers_->Connect(menu, "motion-notify-event", 93 G_CALLBACK(OnMenuMotionNotifyThunk), this); 94 signal_handlers_->Connect(menu, "move-current", 95 G_CALLBACK(OnMenuMoveCurrentThunk), this); 96 gtk_container_foreach(GTK_CONTAINER(menu), PopulateSubmenus, &submenus_); 97 98 for (size_t i = 0; i < submenus_.size(); ++i) { 99 signal_handlers_->Connect(submenus_[i], "motion-notify-event", 100 G_CALLBACK(OnMenuMotionNotifyThunk), this); 101 } 102 } 103 104 gboolean MenuBarHelper::OnMenuMotionNotify(GtkWidget* menu, 105 GdkEventMotion* motion) { 106 // Don't do anything if pointer is in the menu. 107 if (MotionIsOverMenu(menu, motion)) 108 return FALSE; 109 if (buttons_.empty()) 110 return FALSE; 111 112 gint x = 0; 113 gint y = 0; 114 GtkWidget* last_button = NULL; 115 116 for (size_t i = 0; i < buttons_.size(); ++i) { 117 GtkWidget* button = buttons_[i]; 118 // Figure out coordinates relative to this button. Avoid using 119 // gtk_widget_get_pointer() unnecessarily. 120 if (i == 0) { 121 // We have to make this call because the menu is a popup window, so it 122 // doesn't share a toplevel with the buttons and we can't just use 123 // gtk_widget_translate_coordinates(). 124 gtk_widget_get_pointer(buttons_[0], &x, &y); 125 } else { 126 gint last_x = x; 127 gint last_y = y; 128 if (!gtk_widget_translate_coordinates( 129 last_button, button, last_x, last_y, &x, &y)) { 130 // |button| may not be realized. 131 continue; 132 } 133 } 134 135 last_button = button; 136 137 GtkAllocation allocation; 138 gtk_widget_get_allocation(button, &allocation); 139 140 if (x >= 0 && y >= 0 && x < allocation.width && y < allocation.height) { 141 if (button != button_showing_menu_) 142 delegate_->PopupForButton(button); 143 return TRUE; 144 } 145 } 146 147 return FALSE; 148 } 149 150 void MenuBarHelper::OnMenuHiddenOrDestroyed(GtkWidget* menu) { 151 DCHECK_EQ(showing_menu_, menu); 152 153 signal_handlers_.reset(); 154 showing_menu_ = NULL; 155 button_showing_menu_ = NULL; 156 submenus_.clear(); 157 } 158 159 void MenuBarHelper::OnMenuMoveCurrent(GtkWidget* menu, 160 GtkMenuDirectionType dir) { 161 // The menu directions are triggered by the arrow keys as follows 162 // 163 // PARENT left 164 // CHILD right 165 // NEXT down 166 // PREV up 167 // 168 // We only care about left and right. Note that for RTL, they are swapped. 169 switch (dir) { 170 case GTK_MENU_DIR_CHILD: { 171 GtkWidget* active_item = GTK_MENU_SHELL(menu)->active_menu_item; 172 // The move is going to open a submenu; don't override default behavior. 173 if (active_item && gtk_menu_item_get_submenu(GTK_MENU_ITEM(active_item))) 174 return; 175 // Fall through. 176 } 177 case GTK_MENU_DIR_PARENT: { 178 delegate_->PopupForButtonNextTo(button_showing_menu_, dir); 179 break; 180 } 181 default: 182 return; 183 } 184 185 // This signal doesn't have a return value; we have to manually stop its 186 // propagation. 187 g_signal_stop_emission_by_name(menu, "move-current"); 188 } 189