| // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/gtk/menu_bar_helper.h" |
| |
| #include <algorithm> |
| |
| #include "base/logging.h" |
| #include "chrome/browser/ui/gtk/gtk_util.h" |
| #include "ui/base/gtk/gtk_signal_registrar.h" |
| |
| namespace { |
| |
| // Recursively find all the GtkMenus that are attached to menu item |child| |
| // and add them to |data|, which is a vector of GtkWidgets. |
| void PopulateSubmenus(GtkWidget* child, gpointer data) { |
| std::vector<GtkWidget*>* submenus = |
| static_cast<std::vector<GtkWidget*>*>(data); |
| GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(child)); |
| if (submenu) { |
| submenus->push_back(submenu); |
| gtk_container_foreach(GTK_CONTAINER(submenu), PopulateSubmenus, submenus); |
| } |
| } |
| |
| // Is the cursor over |menu| or one of its parent menus? |
| bool MotionIsOverMenu(GtkWidget* menu, GdkEventMotion* motion) { |
| GtkAllocation allocation; |
| gtk_widget_get_allocation(menu, &allocation); |
| |
| if (motion->x >= 0 && motion->y >= 0 && |
| motion->x < allocation.width && |
| motion->y < allocation.height) { |
| return true; |
| } |
| |
| while (menu) { |
| GtkWidget* menu_item = gtk_menu_get_attach_widget(GTK_MENU(menu)); |
| if (!menu_item) |
| return false; |
| GtkWidget* parent = gtk_widget_get_parent(menu_item); |
| |
| if (gtk_util::WidgetContainsCursor(parent)) |
| return true; |
| menu = parent; |
| } |
| |
| return false; |
| } |
| |
| } // namespace |
| |
| MenuBarHelper::MenuBarHelper(Delegate* delegate) |
| : button_showing_menu_(NULL), |
| showing_menu_(NULL), |
| delegate_(delegate) { |
| DCHECK(delegate_); |
| } |
| |
| MenuBarHelper::~MenuBarHelper() { |
| } |
| |
| void MenuBarHelper::Add(GtkWidget* button) { |
| buttons_.push_back(button); |
| } |
| |
| void MenuBarHelper::Remove(GtkWidget* button) { |
| std::vector<GtkWidget*>::iterator iter = |
| find(buttons_.begin(), buttons_.end(), button); |
| if (iter == buttons_.end()) { |
| NOTREACHED(); |
| return; |
| } |
| buttons_.erase(iter); |
| } |
| |
| void MenuBarHelper::Clear() { |
| buttons_.clear(); |
| } |
| |
| void MenuBarHelper::MenuStartedShowing(GtkWidget* button, GtkWidget* menu) { |
| DCHECK(GTK_IS_MENU(menu)); |
| button_showing_menu_ = button; |
| showing_menu_ = menu; |
| |
| signal_handlers_.reset(new ui::GtkSignalRegistrar()); |
| signal_handlers_->Connect(menu, "destroy", |
| G_CALLBACK(OnMenuHiddenOrDestroyedThunk), this); |
| signal_handlers_->Connect(menu, "hide", |
| G_CALLBACK(OnMenuHiddenOrDestroyedThunk), this); |
| signal_handlers_->Connect(menu, "motion-notify-event", |
| G_CALLBACK(OnMenuMotionNotifyThunk), this); |
| signal_handlers_->Connect(menu, "move-current", |
| G_CALLBACK(OnMenuMoveCurrentThunk), this); |
| gtk_container_foreach(GTK_CONTAINER(menu), PopulateSubmenus, &submenus_); |
| |
| for (size_t i = 0; i < submenus_.size(); ++i) { |
| signal_handlers_->Connect(submenus_[i], "motion-notify-event", |
| G_CALLBACK(OnMenuMotionNotifyThunk), this); |
| } |
| } |
| |
| gboolean MenuBarHelper::OnMenuMotionNotify(GtkWidget* menu, |
| GdkEventMotion* motion) { |
| // Don't do anything if pointer is in the menu. |
| if (MotionIsOverMenu(menu, motion)) |
| return FALSE; |
| if (buttons_.empty()) |
| return FALSE; |
| |
| gint x = 0; |
| gint y = 0; |
| GtkWidget* last_button = NULL; |
| |
| for (size_t i = 0; i < buttons_.size(); ++i) { |
| GtkWidget* button = buttons_[i]; |
| // Figure out coordinates relative to this button. Avoid using |
| // gtk_widget_get_pointer() unnecessarily. |
| if (i == 0) { |
| // We have to make this call because the menu is a popup window, so it |
| // doesn't share a toplevel with the buttons and we can't just use |
| // gtk_widget_translate_coordinates(). |
| gtk_widget_get_pointer(buttons_[0], &x, &y); |
| } else { |
| gint last_x = x; |
| gint last_y = y; |
| if (!gtk_widget_translate_coordinates( |
| last_button, button, last_x, last_y, &x, &y)) { |
| // |button| may not be realized. |
| continue; |
| } |
| } |
| |
| last_button = button; |
| |
| GtkAllocation allocation; |
| gtk_widget_get_allocation(button, &allocation); |
| |
| if (x >= 0 && y >= 0 && x < allocation.width && y < allocation.height) { |
| if (button != button_showing_menu_) |
| delegate_->PopupForButton(button); |
| return TRUE; |
| } |
| } |
| |
| return FALSE; |
| } |
| |
| void MenuBarHelper::OnMenuHiddenOrDestroyed(GtkWidget* menu) { |
| DCHECK_EQ(showing_menu_, menu); |
| |
| signal_handlers_.reset(); |
| showing_menu_ = NULL; |
| button_showing_menu_ = NULL; |
| submenus_.clear(); |
| } |
| |
| void MenuBarHelper::OnMenuMoveCurrent(GtkWidget* menu, |
| GtkMenuDirectionType dir) { |
| // The menu directions are triggered by the arrow keys as follows |
| // |
| // PARENT left |
| // CHILD right |
| // NEXT down |
| // PREV up |
| // |
| // We only care about left and right. Note that for RTL, they are swapped. |
| switch (dir) { |
| case GTK_MENU_DIR_CHILD: { |
| GtkWidget* active_item = GTK_MENU_SHELL(menu)->active_menu_item; |
| // The move is going to open a submenu; don't override default behavior. |
| if (active_item && gtk_menu_item_get_submenu(GTK_MENU_ITEM(active_item))) |
| return; |
| // Fall through. |
| } |
| case GTK_MENU_DIR_PARENT: { |
| delegate_->PopupForButtonNextTo(button_showing_menu_, dir); |
| break; |
| } |
| default: |
| return; |
| } |
| |
| // This signal doesn't have a return value; we have to manually stop its |
| // propagation. |
| g_signal_stop_emission_by_name(menu, "move-current"); |
| } |