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