// 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) {
if (motion->x >= 0 && motion->y >= 0 &&
motion->x < menu->allocation.width &&
motion->y < menu->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;
if (x >= 0 && y >= 0 && x < button->allocation.width &&
y < button->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");
}