Merge pull request #214 from atom/linux-menu

Implement menu API on Linux
This commit is contained in:
Cheng Zhao 2014-03-15 09:43:59 +00:00
commit 66fe74de29
16 changed files with 1367 additions and 75 deletions

View file

@ -131,6 +131,10 @@
'browser/ui/gtk/gtk_util.h',
'browser/ui/gtk/gtk_window_util.cc',
'browser/ui/gtk/gtk_window_util.h',
'browser/ui/gtk/event_utils.cc',
'browser/ui/gtk/event_utils.h',
'browser/ui/gtk/menu_gtk.cc',
'browser/ui/gtk/menu_gtk.h',
'browser/ui/message_box.h',
'browser/ui/message_box_gtk.cc',
'browser/ui/message_box_mac.mm',

View file

@ -4,6 +4,14 @@
#include "browser/api/atom_api_menu_gtk.h"
#include "browser/native_window_gtk.h"
#include "common/v8/native_type_conversions.h"
#include "content/public/browser/render_widget_host_view.h"
#include "ui/gfx/point.h"
#include "ui/gfx/screen.h"
#include "common/v8/node_common.h"
namespace atom {
namespace api {
@ -16,10 +24,34 @@ MenuGtk::~MenuGtk() {
}
void MenuGtk::Popup(NativeWindow* native_window) {
uint32_t triggering_event_time;
gfx::Point point;
GdkEventButton* event = native_window->GetWebContents()->
GetRenderWidgetHostView()->GetLastMouseDown();
if (event) {
triggering_event_time = event->time;
point = gfx::Point(event->x_root, event->y_root);
} else {
triggering_event_time = GDK_CURRENT_TIME;
point = gfx::Screen::GetNativeScreen()->GetCursorScreenPoint();
}
menu_gtk_.reset(new ::MenuGtk(this, model_.get()));
menu_gtk_->PopupAsContext(point, triggering_event_time);
}
// static
void Menu::AttachToWindow(const v8::FunctionCallbackInfo<v8::Value>& args) {
Menu* self = ObjectWrap::Unwrap<Menu>(args.This());
if (self == NULL)
return node::ThrowError("Menu is already destroyed");
NativeWindow* native_window;
if (!FromV8Arguments(args, &native_window))
return node::ThrowTypeError("Bad argument");
static_cast<NativeWindowGtk*>(native_window)->SetMenu(self->model_.get());
}
// static

View file

@ -6,12 +6,14 @@
#define ATOM_BROWSER_API_ATOM_API_MENU_GTK_H_
#include "browser/api/atom_api_menu.h"
#include "browser/ui/gtk/menu_gtk.h"
namespace atom {
namespace api {
class MenuGtk : public Menu {
class MenuGtk : public Menu,
public ::MenuGtk::Delegate {
public:
explicit MenuGtk(v8::Handle<v8::Object> wrapper);
virtual ~MenuGtk();
@ -20,6 +22,8 @@ class MenuGtk : public Menu {
virtual void Popup(NativeWindow* window) OVERRIDE;
private:
scoped_ptr<::MenuGtk> menu_gtk_;
DISALLOW_COPY_AND_ASSIGN(MenuGtk);
};

View file

@ -163,11 +163,11 @@ app.on('ready', function() {
submenu: [
{
label: 'Open',
accelerator: 'Command+O',
accelerator: 'Ctrl+O',
},
{
label: 'Close',
accelerator: 'Command+W',
accelerator: 'Ctrl+W',
click: function() { mainWindow.close(); }
},
]
@ -177,16 +177,16 @@ app.on('ready', function() {
submenu: [
{
label: 'Reload',
accelerator: 'Command+R',
accelerator: 'Ctrl+R',
click: function() { mainWindow.restart(); }
},
{
label: 'Enter Fullscreen',
click: function() { mainWindow.setFullscreen(true); }
click: function() { mainWindow.setFullScreen(true); }
},
{
label: 'Toggle DevTools',
accelerator: 'Alt+Command+I',
accelerator: 'Alt+Ctrl+I',
click: function() { mainWindow.toggleDevTools(); }
},
]

View file

@ -11,6 +11,8 @@
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_view.h"
#include "content/public/common/renderer_preferences.h"
#include "ui/base/accelerators/platform_accelerator_gtk.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/base/x/x11_util.h"
#include "ui/gfx/gtk_util.h"
#include "ui/gfx/rect.h"
@ -31,9 +33,13 @@ NativeWindowGtk::NativeWindowGtk(content::WebContents* web_contents,
base::DictionaryValue* options)
: NativeWindow(web_contents, options),
window_(GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL))),
vbox_(gtk_vbox_new(FALSE, 0)),
state_(GDK_WINDOW_STATE_WITHDRAWN),
is_always_on_top_(false) {
gtk_container_add(GTK_CONTAINER(window_),
is_always_on_top_(false),
suppress_window_raise_(false),
frame_cursor_(NULL) {
gtk_container_add(GTK_CONTAINER(window_), vbox_);
gtk_container_add(GTK_CONTAINER(vbox_),
GetWebContents()->GetView()->GetNativeView());
int width = 800, height = 600;
@ -55,6 +61,8 @@ NativeWindowGtk::NativeWindowGtk(content::WebContents* web_contents,
G_CALLBACK(OnWindowDeleteEventThunk), this);
g_signal_connect(window_, "focus-out-event",
G_CALLBACK(OnFocusOutThunk), this);
g_signal_connect(window_, "key-press-event",
G_CALLBACK(OnKeyPressThunk), this);
if (!has_frame_) {
gtk_window_set_decorated(window_, false);
@ -254,13 +262,20 @@ gfx::NativeWindow NativeWindowGtk::GetNativeWindow() {
return window_;
}
void NativeWindowGtk::SetMenu(ui::MenuModel* menu_model) {
menu_.reset(new ::MenuGtk(this, menu_model, true));
gtk_box_pack_start(GTK_BOX(vbox_), menu_->widget(), FALSE, FALSE, 0);
gtk_box_reorder_child(GTK_BOX(vbox_), menu_->widget(), 0);
RegisterAccelerators();
}
void NativeWindowGtk::UpdateDraggableRegions(
const std::vector<DraggableRegion>& regions) {
// Draggable region is not supported for non-frameless window.
if (has_frame_)
return;
SkRegion draggable_region;
draggable_region_.reset(new SkRegion);
// By default, the whole window is non-draggable. We need to explicitly
// include those draggable regions.
@ -268,15 +283,20 @@ void NativeWindowGtk::UpdateDraggableRegions(
regions.begin();
iter != regions.end(); ++iter) {
const DraggableRegion& region = *iter;
draggable_region.op(
draggable_region_->op(
region.bounds.x(),
region.bounds.y(),
region.bounds.right(),
region.bounds.bottom(),
region.draggable ? SkRegion::kUnion_Op : SkRegion::kDifference_Op);
}
}
draggable_region_ = draggable_region;
void NativeWindowGtk::RegisterAccelerators() {
DCHECK(menu_);
accelerator_table_.clear();
accelerator_util::GenerateAcceleratorTable(&accelerator_table_,
menu_->model());
}
void NativeWindowGtk::SetWebKitColorStyle() {
@ -341,18 +361,22 @@ gboolean NativeWindowGtk::OnWindowState(GtkWidget* window,
gboolean NativeWindowGtk::OnMouseMoveEvent(GtkWidget* widget,
GdkEventMotion* event) {
if (has_frame_) {
// Reset the cursor.
if (frame_cursor_) {
frame_cursor_ = NULL;
gdk_window_set_cursor(gtk_widget_get_window(GTK_WIDGET(window_)), NULL);
}
return FALSE;
}
if (!IsResizable())
return FALSE;
int win_x, win_y;
GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(window_));
gdk_window_get_origin(gdk_window, &win_x, &win_y);
gfx::Point point(static_cast<int>(event->x_root - win_x),
static_cast<int>(event->y_root - win_y));
// Update the cursor if we're on the custom frame border.
GdkWindowEdge edge;
bool has_hit_edge = GetWindowEdge(point.x(), point.y(), &edge);
bool has_hit_edge = GetWindowEdge(static_cast<int>(event->x),
static_cast<int>(event->y), &edge);
GdkCursorType new_cursor = GDK_LAST_CURSOR;
if (has_hit_edge)
new_cursor = gtk_window_util::GdkWindowEdgeToGdkCursorType(edge);
@ -371,18 +395,19 @@ gboolean NativeWindowGtk::OnMouseMoveEvent(GtkWidget* widget,
gboolean NativeWindowGtk::OnButtonPress(GtkWidget* widget,
GdkEventButton* event) {
DCHECK(!has_frame_);
// Make the button press coordinate relative to the browser window.
int win_x, win_y;
GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(window_));
gdk_window_get_origin(gdk_window, &win_x, &win_y);
bool resizable = IsResizable();
GdkWindowEdge edge;
gfx::Point point(static_cast<int>(event->x_root - win_x),
static_cast<int>(event->y_root - win_y));
bool has_hit_edge = resizable && GetWindowEdge(point.x(), point.y(), &edge);
bool has_hit_titlebar = !draggable_region_.isEmpty() &&
draggable_region_.contains(event->x, event->y);
bool has_hit_edge = IsResizable() &&
GetWindowEdge(point.x(), point.y(), &edge);
bool has_hit_titlebar =
draggable_region_ && draggable_region_->contains(event->x, event->y);
if (event->button == 1) {
if (GDK_BUTTON_PRESS == event->type) {
@ -399,11 +424,15 @@ gboolean NativeWindowGtk::OnButtonPress(GtkWidget* widget,
event->time);
return TRUE;
} else if (has_hit_titlebar) {
GdkRectangle window_bounds = {0};
gdk_window_get_frame_extents(gdk_window, &window_bounds);
gfx::Rect bounds(window_bounds.x, window_bounds.y,
window_bounds.width, window_bounds.height);
return gtk_window_util::HandleTitleBarLeftMousePress(
window_, gfx::Rect(GetPosition(), GetSize()), event);
window_, bounds, event);
}
} else if (GDK_2BUTTON_PRESS == event->type) {
if (has_hit_titlebar && resizable) {
if (has_hit_titlebar && IsResizable()) {
// Maximize/restore on double click.
if (IsMaximized())
gtk_window_unmaximize(window_);
@ -417,9 +446,17 @@ gboolean NativeWindowGtk::OnButtonPress(GtkWidget* widget,
gdk_window_lower(gdk_window);
return TRUE;
}
return FALSE;
}
gboolean NativeWindowGtk::OnKeyPress(GtkWidget* widget, GdkEventKey* event) {
ui::Accelerator accelerator = ui::AcceleratorForGdkKeyCodeAndModifier(
event->keyval, static_cast<GdkModifierType>(event->state));
return accelerator_util::TriggerAcceleratorTableCommand(
&accelerator_table_, accelerator) ? TRUE: FALSE;
}
// static
NativeWindow* NativeWindow::Create(content::WebContents* web_contents,
base::DictionaryValue* options) {

View file

@ -8,13 +8,17 @@
#include <gtk/gtk.h>
#include "browser/native_window.h"
#include "browser/ui/accelerator_util.h"
#include "browser/ui/gtk/menu_gtk.h"
#include "third_party/skia/include/core/SkRegion.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/gtk/gtk_signal.h"
#include "ui/gfx/size.h"
namespace atom {
class NativeWindowGtk : public NativeWindow {
class NativeWindowGtk : public NativeWindow,
public MenuGtk::Delegate {
public:
explicit NativeWindowGtk(content::WebContents* web_contents,
base::DictionaryValue* options);
@ -56,11 +60,17 @@ class NativeWindowGtk : public NativeWindow {
virtual bool HasModalDialog() OVERRIDE;
virtual gfx::NativeWindow GetNativeWindow() OVERRIDE;
// Set the native window menu.
void SetMenu(ui::MenuModel* menu_model);
protected:
virtual void UpdateDraggableRegions(
const std::vector<DraggableRegion>& regions) OVERRIDE;
private:
// Register accelerators supported by the menu model.
void RegisterAccelerators();
// Set WebKit's style from current theme.
void SetWebKitColorStyle();
@ -77,21 +87,27 @@ class NativeWindowGtk : public NativeWindow {
CHROMEGTK_CALLBACK_1(NativeWindowGtk, gboolean, OnFocusOut, GdkEventFocus*);
CHROMEGTK_CALLBACK_1(NativeWindowGtk, gboolean, OnWindowState,
GdkEventWindowState*);
// Mouse move and mouse button press callbacks.
CHROMEGTK_CALLBACK_1(NativeWindowGtk, gboolean, OnMouseMoveEvent,
GdkEventMotion*);
CHROMEGTK_CALLBACK_1(NativeWindowGtk, gboolean, OnButtonPress,
GdkEventButton*);
// Key press event callback.
CHROMEGTK_CALLBACK_1(NativeWindowGtk, gboolean, OnKeyPress, GdkEventKey*);
GtkWindow* window_;
GtkWidget* vbox_;
GdkWindowState state_;
bool is_always_on_top_;
gfx::Size minimum_size_;
gfx::Size maximum_size_;
// The region is treated as title bar, can be dragged to move
// and double clicked to maximize.
SkRegion draggable_region_;
// The region is treated as title bar, can be dragged to move and double
// clicked to maximize.
scoped_ptr<SkRegion> draggable_region_;
// If true, don't call gdk_window_raise() when we get a click in the title
// bar or window border. This is to work around a compiz bug.
@ -101,6 +117,12 @@ class NativeWindowGtk : public NativeWindow {
// custom frame border. We set it to NULL if we want the default cursor.
GdkCursor* frame_cursor_;
// The window menu.
scoped_ptr<MenuGtk> menu_;
// Map from accelerator to menu item's command id.
accelerator_util::AcceleratorTable accelerator_table_;
DISALLOW_COPY_AND_ASSIGN(NativeWindowGtk);
};

View file

@ -4,7 +4,6 @@
#include "browser/native_window_win.h"
#include "base/stl_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "browser/api/atom_api_menu.h"
@ -431,13 +430,8 @@ void NativeWindowWin::ViewHierarchyChanged(
bool NativeWindowWin::AcceleratorPressed(
const ui::Accelerator& accelerator) {
if (ContainsKey(accelerator_table_, accelerator)) {
const MenuItem& item = accelerator_table_[accelerator];
item.model->ActivatedAt(item.position);
return true;
} else {
return false;
}
return accelerator_util::TriggerAcceleratorTableCommand(
&accelerator_table_, accelerator);
}
void NativeWindowWin::DeleteDelegate() {
@ -530,39 +524,17 @@ void NativeWindowWin::RegisterAccelerators() {
accelerator_table_.clear();
focus_manager->UnregisterAccelerators(this);
GenerateAcceleratorTable();
for (AcceleratorTable::const_iterator iter = accelerator_table_.begin();
iter != accelerator_table_.end(); ++iter) {
accelerator_util::GenerateAcceleratorTable(&accelerator_table_,
menu_->model());
accelerator_util::AcceleratorTable::const_iterator iter;
for (iter = accelerator_table_.begin();
iter != accelerator_table_.end();
++iter) {
focus_manager->RegisterAccelerator(
iter->first, ui::AcceleratorManager::kNormalPriority, this);
}
}
void NativeWindowWin::GenerateAcceleratorTable() {
DCHECK(menu_);
ui::SimpleMenuModel* model = static_cast<ui::SimpleMenuModel*>(
menu_->model());
FillAcceleratorTable(&accelerator_table_, model);
}
void NativeWindowWin::FillAcceleratorTable(AcceleratorTable* table,
ui::MenuModel* model) {
int count = model->GetItemCount();
for (int i = 0; i < count; ++i) {
ui::MenuModel::ItemType type = model->GetTypeAt(i);
if (type == ui::MenuModel::TYPE_SUBMENU) {
ui::MenuModel* submodel = model->GetSubmenuModelAt(i);
FillAcceleratorTable(table, submodel);
} else {
ui::Accelerator accelerator;
if (model->GetAcceleratorAt(i, &accelerator)) {
MenuItem item = { i, model };
(*table)[accelerator] = item;
}
}
}
}
// static
NativeWindow* NativeWindow::Create(content::WebContents* web_contents,
base::DictionaryValue* options) {

View file

@ -8,6 +8,7 @@
#include "base/memory/scoped_ptr.h"
#include "base/strings/string16.h"
#include "browser/native_window.h"
#include "browser/ui/accelerator_util.h"
#include "ui/gfx/size.h"
#include "ui/views/widget/widget_delegate.h"
@ -113,13 +114,6 @@ class NativeWindowWin : public NativeWindow,
// Register accelerators supported by the menu model.
void RegisterAccelerators();
// Generate a table that contains memu model's accelerators and command ids.
void GenerateAcceleratorTable();
// Helper to fill the accelerator table from the model.
void FillAcceleratorTable(AcceleratorTable* table,
ui::MenuModel* model);
scoped_ptr<views::Widget> window_;
views::WebView* web_view_; // managed by window_.
@ -127,7 +121,7 @@ class NativeWindowWin : public NativeWindow,
scoped_ptr<atom::Menu2> menu_;
// Map from accelerator to menu item's command id.
AcceleratorTable accelerator_table_;
accelerator_util::AcceleratorTable accelerator_table_;
scoped_ptr<SkRegion> draggable_region_;

View file

@ -8,10 +8,11 @@
#include <string>
#include "base/strings/string_util.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "ui/base/accelerators/accelerator.h"
#include "base/strings/string_util.h"
#include "ui/base/models/simple_menu_model.h"
namespace accelerator_util {
@ -186,4 +187,32 @@ bool StringToAccelerator(const std::string& description,
return true;
}
void GenerateAcceleratorTable(AcceleratorTable* table, ui::MenuModel* model) {
int count = model->GetItemCount();
for (int i = 0; i < count; ++i) {
ui::MenuModel::ItemType type = model->GetTypeAt(i);
if (type == ui::MenuModel::TYPE_SUBMENU) {
ui::MenuModel* submodel = model->GetSubmenuModelAt(i);
GenerateAcceleratorTable(table, submodel);
} else {
ui::Accelerator accelerator;
if (model->GetAcceleratorAt(i, &accelerator)) {
MenuItem item = { i, model };
(*table)[accelerator] = item;
}
}
}
}
bool TriggerAcceleratorTableCommand(AcceleratorTable* table,
const ui::Accelerator& accelerator) {
if (ContainsKey(*table, accelerator)) {
const accelerator_util::MenuItem& item = (*table)[accelerator];
item.model->ActivatedAt(item.position);
return true;
} else {
return false;
}
}
} // namespace accelerator_util

View file

@ -5,14 +5,20 @@
#ifndef BROWSER_UI_ACCELERATOR_UTIL_H_
#define BROWSER_UI_ACCELERATOR_UTIL_H_
#include <map>
#include <string>
#include "ui/base/accelerators/accelerator.h"
namespace ui {
class Accelerator;
class MenuModel;
}
namespace accelerator_util {
typedef struct { int position; ui::MenuModel* model; } MenuItem;
typedef std::map<ui::Accelerator, MenuItem> AcceleratorTable;
// Parse a string as an accelerator.
bool StringToAccelerator(const std::string& description,
ui::Accelerator* accelerator);
@ -20,6 +26,13 @@ bool StringToAccelerator(const std::string& description,
// Set platform accelerator for the Accelerator.
void SetPlatformAccelerator(ui::Accelerator* accelerator);
// Generate a table that contains memu model's accelerators and command ids.
void GenerateAcceleratorTable(AcceleratorTable* table, ui::MenuModel* model);
// Trigger command from the accelerators table.
bool TriggerAcceleratorTableCommand(AcceleratorTable* table,
const ui::Accelerator& accelerator);
} // namespace accelerator_util
#endif // BROWSER_UI_ACCELERATOR_UTIL_H_

View file

@ -0,0 +1,44 @@
// Copyright (c) 2012 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 "browser/ui/gtk/event_utils.h"
#include "base/logging.h"
#include "ui/base/window_open_disposition.h"
#include "ui/events/event_constants.h"
namespace event_utils {
int EventFlagsFromGdkState(guint state) {
int flags = ui::EF_NONE;
flags |= (state & GDK_LOCK_MASK) ? ui::EF_CAPS_LOCK_DOWN : ui::EF_NONE;
flags |= (state & GDK_CONTROL_MASK) ? ui::EF_CONTROL_DOWN : ui::EF_NONE;
flags |= (state & GDK_SHIFT_MASK) ? ui::EF_SHIFT_DOWN : ui::EF_NONE;
flags |= (state & GDK_MOD1_MASK) ? ui::EF_ALT_DOWN : ui::EF_NONE;
flags |= (state & GDK_BUTTON1_MASK) ? ui::EF_LEFT_MOUSE_BUTTON : ui::EF_NONE;
flags |= (state & GDK_BUTTON2_MASK) ? ui::EF_MIDDLE_MOUSE_BUTTON
: ui::EF_NONE;
flags |= (state & GDK_BUTTON3_MASK) ? ui::EF_RIGHT_MOUSE_BUTTON : ui::EF_NONE;
return flags;
}
// TODO(shinyak) This function will be removed after refactoring.
WindowOpenDisposition DispositionFromGdkState(guint state) {
int event_flags = EventFlagsFromGdkState(state);
return ui::DispositionFromEventFlags(event_flags);
}
WindowOpenDisposition DispositionForCurrentButtonPressEvent() {
GdkEvent* event = gtk_get_current_event();
if (!event) {
NOTREACHED();
return NEW_FOREGROUND_TAB;
}
guint state = event->button.state;
gdk_event_free(event);
return DispositionFromGdkState(state);
}
} // namespace event_utils

View file

@ -0,0 +1,28 @@
// Copyright (c) 2012 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.
#ifndef CHROME_BROWSER_UI_GTK_EVENT_UTILS_H_
#define CHROME_BROWSER_UI_GTK_EVENT_UTILS_H_
#include <gtk/gtk.h>
#include "ui/base/window_open_disposition.h"
namespace event_utils {
// Translates event flags into plaform independent event flags.
int EventFlagsFromGdkState(guint state);
// Translates GdkEvent state into what kind of disposition they represent.
// For example, a middle click would mean to open a background tab.
WindowOpenDisposition DispositionFromGdkState(guint state);
// Get the window open disposition from the state in gtk_get_current_event().
// This is designed to be called inside a "clicked" event handler. It is an
// error to call it when gtk_get_current_event() won't return a GdkEventButton*.
WindowOpenDisposition DispositionForCurrentButtonPressEvent();
} // namespace event_utils
#endif // CHROME_BROWSER_UI_GTK_EVENT_UTILS_H_

View file

@ -59,6 +59,11 @@ GtkWidget* CreateBoldLabel(const std::string& text) {
return LeftAlignMisc(label);
}
void SetAlwaysShowImage(GtkWidget* image_menu_item) {
gtk_image_menu_item_set_always_show_image(
GTK_IMAGE_MENU_ITEM(image_menu_item), TRUE);
}
bool IsWidgetAncestryVisible(GtkWidget* widget) {
GtkWidget* parent = widget;
while (parent && gtk_widget_get_visible(parent))

View file

@ -16,6 +16,11 @@ GtkWidget* LeftAlignMisc(GtkWidget* misc);
// Create a left-aligned label with the given text in bold.
GtkWidget* CreateBoldLabel(const std::string& text);
// Show the image for the given menu item, even if the user's default is to not
// show images. Only to be used for favicons or other menus where the image is
// crucial to its functionality.
void SetAlwaysShowImage(GtkWidget* image_menu_item);
// Checks whether a widget is actually visible, i.e. whether it and all its
// ancestors up to its toplevel are visible.
bool IsWidgetAncestryVisible(GtkWidget* widget);

879
browser/ui/gtk/menu_gtk.cc Normal file
View file

@ -0,0 +1,879 @@
// Copyright (c) 2012 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 "browser/ui/gtk/menu_gtk.h"
#include <map>
#include "base/bind.h"
#include "base/i18n/rtl.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "base/stl_util.h"
#include "base/strings/utf_string_conversions.h"
#include "browser/ui/gtk/event_utils.h"
#include "browser/ui/gtk/gtk_custom_menu.h"
#include "browser/ui/gtk/gtk_custom_menu_item.h"
#include "browser/ui/gtk/gtk_util.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/accelerators/menu_label_accelerator_util_linux.h"
#include "ui/base/accelerators/platform_accelerator_gtk.h"
#include "ui/base/models/button_menu_item_model.h"
#include "ui/base/models/menu_model.h"
#include "ui/base/window_open_disposition.h"
#include "ui/gfx/gtk_util.h"
#include "ui/gfx/image/image.h"
bool MenuGtk::block_activation_ = false;
namespace {
// Sets the ID of a menu item.
void SetMenuItemID(GtkWidget* menu_item, int menu_id) {
DCHECK_GE(menu_id, 0);
// Add 1 to the menu_id to avoid setting zero (null) to "menu-id".
g_object_set_data(G_OBJECT(menu_item), "menu-id",
GINT_TO_POINTER(menu_id + 1));
}
// Gets the ID of a menu item.
// Returns true if the menu item has an ID.
bool GetMenuItemID(GtkWidget* menu_item, int* menu_id) {
gpointer id_ptr = g_object_get_data(G_OBJECT(menu_item), "menu-id");
if (id_ptr != NULL) {
*menu_id = GPOINTER_TO_INT(id_ptr) - 1;
return true;
}
return false;
}
ui::MenuModel* ModelForMenuItem(GtkMenuItem* menu_item) {
return reinterpret_cast<ui::MenuModel*>(
g_object_get_data(G_OBJECT(menu_item), "model"));
}
void SetUpButtonShowHandler(GtkWidget* button,
ui::ButtonMenuItemModel* model,
int index) {
g_object_set_data(G_OBJECT(button), "button-model",
model);
g_object_set_data(G_OBJECT(button), "button-model-id",
GINT_TO_POINTER(index));
}
void OnSubmenuShowButtonImage(GtkWidget* widget, GtkButton* button) {
MenuGtk::Delegate* delegate = reinterpret_cast<MenuGtk::Delegate*>(
g_object_get_data(G_OBJECT(button), "menu-gtk-delegate"));
int icon_idr = GPOINTER_TO_INT(g_object_get_data(
G_OBJECT(button), "button-image-idr"));
GtkIconSet* icon_set = delegate->GetIconSetForId(icon_idr);
if (icon_set) {
gtk_button_set_image(
button, gtk_image_new_from_icon_set(icon_set,
GTK_ICON_SIZE_MENU));
}
}
void SetupImageIcon(GtkWidget* button,
GtkWidget* menu,
int icon_idr,
MenuGtk::Delegate* menu_gtk_delegate) {
g_object_set_data(G_OBJECT(button), "button-image-idr",
GINT_TO_POINTER(icon_idr));
g_object_set_data(G_OBJECT(button), "menu-gtk-delegate",
menu_gtk_delegate);
g_signal_connect(menu, "show", G_CALLBACK(OnSubmenuShowButtonImage), button);
}
// Popup menus may get squished if they open up too close to the bottom of the
// screen. This function takes the size of the screen, the size of the menu,
// an optional widget, the Y position of the mouse click, and adjusts the popup
// menu's Y position to make it fit if it's possible to do so.
// Returns the new Y position of the popup menu.
int CalculateMenuYPosition(const GdkRectangle* screen_rect,
const GtkRequisition* menu_req,
GtkWidget* widget, const int y) {
CHECK(screen_rect);
CHECK(menu_req);
// If the menu would run off the bottom of the screen, and there is enough
// screen space upwards to accommodate the menu, then pop upwards. If there
// is a widget, then also move the anchor point to the top of the widget
// rather than the bottom.
const int screen_top = screen_rect->y;
const int screen_bottom = screen_rect->y + screen_rect->height;
const int menu_bottom = y + menu_req->height;
int alternate_y = y - menu_req->height;
if (widget) {
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
alternate_y -= allocation.height;
}
if (menu_bottom >= screen_bottom && alternate_y >= screen_top)
return alternate_y;
return y;
}
} // namespace
bool MenuGtk::Delegate::AlwaysShowIconForCmd(int command_id) const {
return false;
}
GtkIconSet* MenuGtk::Delegate::GetIconSetForId(int idr) { return NULL; }
GtkWidget* MenuGtk::Delegate::GetDefaultImageForLabel(
const std::string& label) {
const char* stock = NULL;
if (label == "New")
stock = GTK_STOCK_NEW;
else if (label == "Close")
stock = GTK_STOCK_CLOSE;
else if (label == "Save As")
stock = GTK_STOCK_SAVE_AS;
else if (label == "Save")
stock = GTK_STOCK_SAVE;
else if (label == "Copy")
stock = GTK_STOCK_COPY;
else if (label == "Cut")
stock = GTK_STOCK_CUT;
else if (label == "Paste")
stock = GTK_STOCK_PASTE;
else if (label == "Delete")
stock = GTK_STOCK_DELETE;
else if (label == "Undo")
stock = GTK_STOCK_UNDO;
else if (label == "Redo")
stock = GTK_STOCK_REDO;
else if (label == "Search" || label == "Find")
stock = GTK_STOCK_FIND;
else if (label == "Select All")
stock = GTK_STOCK_SELECT_ALL;
else if (label == "Clear")
stock = GTK_STOCK_SELECT_ALL;
else if (label == "Back")
stock = GTK_STOCK_GO_BACK;
else if (label == "Forward")
stock = GTK_STOCK_GO_FORWARD;
else if (label == "Reload" || label == "Refresh")
stock = GTK_STOCK_REFRESH;
else if (label == "Print")
stock = GTK_STOCK_PRINT;
else if (label == "About")
stock = GTK_STOCK_ABOUT;
else if (label == "Quit")
stock = GTK_STOCK_QUIT;
else if (label == "Help")
stock = GTK_STOCK_HELP;
return stock ? gtk_image_new_from_stock(stock, GTK_ICON_SIZE_MENU) : NULL;
}
GtkWidget* MenuGtk::Delegate::GetImageForCommandId(int command_id) const {
return NULL;
}
MenuGtk::MenuGtk(MenuGtk::Delegate* delegate,
ui::MenuModel* model,
bool is_menubar)
: delegate_(delegate),
model_(model),
is_menubar_(is_menubar),
dummy_accel_group_(gtk_accel_group_new()),
menu_(is_menubar ? gtk_menu_bar_new() : gtk_custom_menu_new()),
weak_factory_(this) {
DCHECK(model);
g_object_ref_sink(menu_);
ConnectSignalHandlers();
BuildMenuFromModel();
}
MenuGtk::~MenuGtk() {
Cancel();
gtk_widget_destroy(menu_);
g_object_unref(menu_);
g_object_unref(dummy_accel_group_);
}
void MenuGtk::ConnectSignalHandlers() {
// We connect afterwards because OnMenuShow calls SetMenuItemInfo, which may
// take a long time or even start a nested message loop.
g_signal_connect(menu_, "show", G_CALLBACK(OnMenuShowThunk), this);
g_signal_connect(menu_, "hide", G_CALLBACK(OnMenuHiddenThunk), this);
GtkWidget* toplevel_window = gtk_widget_get_toplevel(menu_);
signal_.Connect(toplevel_window, "focus-out-event",
G_CALLBACK(OnMenuFocusOutThunk), this);
}
GtkWidget* MenuGtk::AppendMenuItemWithLabel(int command_id,
const std::string& label) {
std::string converted_label = ui::ConvertAcceleratorsFromWindowsStyle(label);
GtkWidget* menu_item = BuildMenuItemWithLabel(converted_label, command_id);
return AppendMenuItem(command_id, menu_item);
}
GtkWidget* MenuGtk::AppendMenuItemWithIcon(int command_id,
const std::string& label,
const gfx::Image& icon) {
std::string converted_label = ui::ConvertAcceleratorsFromWindowsStyle(label);
GtkWidget* menu_item = BuildMenuItemWithImage(converted_label, icon);
return AppendMenuItem(command_id, menu_item);
}
GtkWidget* MenuGtk::AppendCheckMenuItemWithLabel(int command_id,
const std::string& label) {
std::string converted_label = ui::ConvertAcceleratorsFromWindowsStyle(label);
GtkWidget* menu_item =
gtk_check_menu_item_new_with_mnemonic(converted_label.c_str());
return AppendMenuItem(command_id, menu_item);
}
GtkWidget* MenuGtk::AppendSeparator() {
GtkWidget* menu_item = gtk_separator_menu_item_new();
gtk_widget_show(menu_item);
gtk_menu_shell_append(GTK_MENU_SHELL(menu_), menu_item);
return menu_item;
}
GtkWidget* MenuGtk::InsertSeparator(int position) {
GtkWidget* menu_item = gtk_separator_menu_item_new();
gtk_widget_show(menu_item);
gtk_menu_shell_insert(GTK_MENU_SHELL(menu_), menu_item, position);
return menu_item;
}
GtkWidget* MenuGtk::AppendMenuItem(int command_id, GtkWidget* menu_item) {
if (delegate_ && delegate_->AlwaysShowIconForCmd(command_id) &&
GTK_IS_IMAGE_MENU_ITEM(menu_item))
gtk_util::SetAlwaysShowImage(menu_item);
return AppendMenuItemToMenu(command_id, NULL, menu_item, menu_, true);
}
GtkWidget* MenuGtk::InsertMenuItem(int command_id, GtkWidget* menu_item,
int position) {
if (delegate_ && delegate_->AlwaysShowIconForCmd(command_id) &&
GTK_IS_IMAGE_MENU_ITEM(menu_item))
gtk_util::SetAlwaysShowImage(menu_item);
return InsertMenuItemToMenu(command_id, NULL, menu_item, menu_, position,
true);
}
GtkWidget* MenuGtk::AppendMenuItemToMenu(int index,
ui::MenuModel* model,
GtkWidget* menu_item,
GtkWidget* menu,
bool connect_to_activate) {
int children_count = g_list_length(GTK_MENU_SHELL(menu)->children);
return InsertMenuItemToMenu(index, model, menu_item, menu,
children_count, connect_to_activate);
}
GtkWidget* MenuGtk::InsertMenuItemToMenu(int index,
ui::MenuModel* model,
GtkWidget* menu_item,
GtkWidget* menu,
int position,
bool connect_to_activate) {
SetMenuItemID(menu_item, index);
// Native menu items do their own thing, so only selectively listen for the
// activate signal.
if (connect_to_activate) {
g_signal_connect(menu_item, "activate",
G_CALLBACK(OnMenuItemActivatedThunk), this);
}
// AppendMenuItemToMenu is used both internally when we control menu creation
// from a model (where the model can choose to hide certain menu items), and
// with immediate commands which don't provide the option.
if (model) {
if (model->IsVisibleAt(index))
gtk_widget_show(menu_item);
} else {
gtk_widget_show(menu_item);
}
gtk_menu_shell_insert(GTK_MENU_SHELL(menu), menu_item, position);
return menu_item;
}
void MenuGtk::PopupForWidget(GtkWidget* widget, int button,
guint32 event_time) {
gtk_menu_popup(GTK_MENU(menu_), NULL, NULL,
WidgetMenuPositionFunc,
widget,
button, event_time);
}
void MenuGtk::PopupAsContext(const gfx::Point& point, guint32 event_time) {
// gtk_menu_popup doesn't like the "const" qualifier on point.
gfx::Point nonconst_point(point);
gtk_menu_popup(GTK_MENU(menu_), NULL, NULL,
PointMenuPositionFunc, &nonconst_point,
3, event_time);
}
void MenuGtk::PopupAsContextForStatusIcon(guint32 event_time, guint32 button,
GtkStatusIcon* icon) {
gtk_menu_popup(GTK_MENU(menu_), NULL, NULL, gtk_status_icon_position_menu,
icon, button, event_time);
}
void MenuGtk::PopupAsFromKeyEvent(GtkWidget* widget) {
PopupForWidget(widget, 0, gtk_get_current_event_time());
gtk_menu_shell_select_first(GTK_MENU_SHELL(menu_), FALSE);
}
void MenuGtk::Cancel() {
if (!is_menubar_)
gtk_menu_popdown(GTK_MENU(menu_));
}
void MenuGtk::UpdateMenu() {
gtk_container_foreach(GTK_CONTAINER(menu_), SetMenuItemInfo, this);
}
GtkWidget* MenuGtk::BuildMenuItemWithImage(const std::string& label,
GtkWidget* image) {
GtkWidget* menu_item =
gtk_image_menu_item_new_with_mnemonic(label.c_str());
gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(menu_item), image);
return menu_item;
}
GtkWidget* MenuGtk::BuildMenuItemWithImage(const std::string& label,
const gfx::Image& icon) {
GtkWidget* menu_item = BuildMenuItemWithImage(label,
gtk_image_new_from_pixbuf(icon.ToGdkPixbuf()));
return menu_item;
}
GtkWidget* MenuGtk::BuildMenuItemWithLabel(const std::string& label,
int command_id) {
GtkWidget* img =
delegate_ ? delegate_->GetImageForCommandId(command_id) :
MenuGtk::Delegate::GetDefaultImageForLabel(label);
return img ? BuildMenuItemWithImage(label, img) :
gtk_menu_item_new_with_mnemonic(label.c_str());
}
void MenuGtk::BuildMenuFromModel() {
BuildSubmenuFromModel(model_, menu_);
}
void MenuGtk::BuildSubmenuFromModel(ui::MenuModel* model, GtkWidget* menu) {
std::map<int, GtkWidget*> radio_groups;
GtkWidget* menu_item = NULL;
for (int i = 0; i < model->GetItemCount(); ++i) {
gfx::Image icon;
std::string label = ui::ConvertAcceleratorsFromWindowsStyle(
base::UTF16ToUTF8(model->GetLabelAt(i)));
bool connect_to_activate = true;
switch (model->GetTypeAt(i)) {
case ui::MenuModel::TYPE_SEPARATOR:
menu_item = gtk_separator_menu_item_new();
break;
case ui::MenuModel::TYPE_CHECK:
menu_item = gtk_check_menu_item_new_with_mnemonic(label.c_str());
break;
case ui::MenuModel::TYPE_RADIO: {
std::map<int, GtkWidget*>::iterator iter =
radio_groups.find(model->GetGroupIdAt(i));
if (iter == radio_groups.end()) {
menu_item = gtk_radio_menu_item_new_with_mnemonic(
NULL, label.c_str());
radio_groups[model->GetGroupIdAt(i)] = menu_item;
} else {
menu_item = gtk_radio_menu_item_new_with_mnemonic_from_widget(
GTK_RADIO_MENU_ITEM(iter->second), label.c_str());
}
break;
}
case ui::MenuModel::TYPE_BUTTON_ITEM: {
ui::ButtonMenuItemModel* button_menu_item_model =
model->GetButtonMenuItemAt(i);
menu_item = BuildButtonMenuItem(button_menu_item_model, menu);
connect_to_activate = false;
break;
}
case ui::MenuModel::TYPE_SUBMENU:
case ui::MenuModel::TYPE_COMMAND: {
int command_id = model->GetCommandIdAt(i);
if (model->GetIconAt(i, &icon))
menu_item = BuildMenuItemWithImage(label, icon);
else
menu_item = BuildMenuItemWithLabel(label, command_id);
if (delegate_ && delegate_->AlwaysShowIconForCmd(command_id) &&
GTK_IS_IMAGE_MENU_ITEM(menu_item)) {
gtk_util::SetAlwaysShowImage(menu_item);
}
break;
}
default:
NOTREACHED();
}
if (model->GetTypeAt(i) == ui::MenuModel::TYPE_SUBMENU) {
GtkWidget* submenu = gtk_menu_new();
g_object_set_data(G_OBJECT(submenu), "menu-item", menu_item);
ui::MenuModel* submenu_model = model->GetSubmenuModelAt(i);
g_object_set_data(G_OBJECT(menu_item), "submenu-model", submenu_model);
gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu);
// We will populate the submenu on demand when shown.
g_signal_connect(submenu, "show", G_CALLBACK(OnSubMenuShowThunk), this);
g_signal_connect(submenu, "hide", G_CALLBACK(OnSubMenuHiddenThunk), this);
connect_to_activate = false;
}
ui::Accelerator accelerator;
if (model->GetAcceleratorAt(i, &accelerator)) {
gtk_widget_add_accelerator(menu_item,
"activate",
dummy_accel_group_,
ui::GetGdkKeyCodeForAccelerator(accelerator),
ui::GetGdkModifierForAccelerator(accelerator),
GTK_ACCEL_VISIBLE);
}
g_object_set_data(G_OBJECT(menu_item), "model", model);
AppendMenuItemToMenu(i, model, menu_item, menu, connect_to_activate);
menu_item = NULL;
}
}
GtkWidget* MenuGtk::BuildButtonMenuItem(ui::ButtonMenuItemModel* model,
GtkWidget* menu) {
GtkWidget* menu_item = gtk_custom_menu_item_new(
ui::RemoveWindowsStyleAccelerators(
base::UTF16ToUTF8(model->label())).c_str());
// Set up the callback to the model for when it is clicked.
g_object_set_data(G_OBJECT(menu_item), "button-model", model);
g_signal_connect(menu_item, "button-pushed",
G_CALLBACK(OnMenuButtonPressedThunk), this);
g_signal_connect(menu_item, "try-button-pushed",
G_CALLBACK(OnMenuTryButtonPressedThunk), this);
GtkSizeGroup* group = NULL;
for (int i = 0; i < model->GetItemCount(); ++i) {
GtkWidget* button = NULL;
switch (model->GetTypeAt(i)) {
case ui::ButtonMenuItemModel::TYPE_SPACE: {
gtk_custom_menu_item_add_space(GTK_CUSTOM_MENU_ITEM(menu_item));
break;
}
case ui::ButtonMenuItemModel::TYPE_BUTTON: {
button = gtk_custom_menu_item_add_button(
GTK_CUSTOM_MENU_ITEM(menu_item),
model->GetCommandIdAt(i));
int icon_idr;
if (model->GetIconAt(i, &icon_idr)) {
SetupImageIcon(button, menu, icon_idr, delegate_);
} else {
gtk_button_set_label(
GTK_BUTTON(button),
ui::RemoveWindowsStyleAccelerators(
base::UTF16ToUTF8(model->GetLabelAt(i))).c_str());
}
SetUpButtonShowHandler(button, model, i);
break;
}
case ui::ButtonMenuItemModel::TYPE_BUTTON_LABEL: {
button = gtk_custom_menu_item_add_button_label(
GTK_CUSTOM_MENU_ITEM(menu_item),
model->GetCommandIdAt(i));
gtk_button_set_label(
GTK_BUTTON(button),
ui::RemoveWindowsStyleAccelerators(
base::UTF16ToUTF8(model->GetLabelAt(i))).c_str());
SetUpButtonShowHandler(button, model, i);
break;
}
}
if (button && model->PartOfGroup(i)) {
if (!group)
group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
gtk_size_group_add_widget(group, button);
}
}
if (group)
g_object_unref(group);
return menu_item;
}
void MenuGtk::OnMenuItemActivated(GtkWidget* menu_item) {
if (block_activation_)
return;
ui::MenuModel* model = ModelForMenuItem(GTK_MENU_ITEM(menu_item));
if (!model) {
// There won't be a model for "native" submenus like the "Input Methods"
// context menu. We don't need to handle activation messages for submenus
// anyway, so we can just return here.
DCHECK(gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item)));
return;
}
// The activate signal is sent to radio items as they get deselected;
// ignore it in this case.
if (GTK_IS_RADIO_MENU_ITEM(menu_item) &&
!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menu_item))) {
return;
}
int id;
if (!GetMenuItemID(menu_item, &id))
return;
// The menu item can still be activated by hotkeys even if it is disabled.
if (model->IsEnabledAt(id))
ExecuteCommand(model, id);
}
void MenuGtk::OnMenuButtonPressed(GtkWidget* menu_item, int command_id) {
ui::ButtonMenuItemModel* model =
reinterpret_cast<ui::ButtonMenuItemModel*>(
g_object_get_data(G_OBJECT(menu_item), "button-model"));
if (model && model->IsCommandIdEnabled(command_id)) {
if (delegate_)
delegate_->CommandWillBeExecuted();
model->ActivatedCommand(command_id);
}
}
gboolean MenuGtk::OnMenuTryButtonPressed(GtkWidget* menu_item,
int command_id) {
gboolean pressed = FALSE;
ui::ButtonMenuItemModel* model =
reinterpret_cast<ui::ButtonMenuItemModel*>(
g_object_get_data(G_OBJECT(menu_item), "button-model"));
if (model &&
model->IsCommandIdEnabled(command_id) &&
!model->DoesCommandIdDismissMenu(command_id)) {
if (delegate_)
delegate_->CommandWillBeExecuted();
model->ActivatedCommand(command_id);
pressed = TRUE;
}
return pressed;
}
// static
void MenuGtk::WidgetMenuPositionFunc(GtkMenu* menu,
int* x,
int* y,
gboolean* push_in,
void* void_widget) {
GtkWidget* widget = GTK_WIDGET(void_widget);
GtkRequisition menu_req;
gtk_widget_size_request(GTK_WIDGET(menu), &menu_req);
gdk_window_get_origin(gtk_widget_get_window(widget), x, y);
GdkScreen *screen = gtk_widget_get_screen(widget);
gint monitor = gdk_screen_get_monitor_at_point(screen, *x, *y);
GdkRectangle screen_rect;
gdk_screen_get_monitor_geometry(screen, monitor,
&screen_rect);
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
if (!gtk_widget_get_has_window(widget)) {
*x += allocation.x;
*y += allocation.y;
}
*y += allocation.height;
bool start_align =
!!g_object_get_data(G_OBJECT(widget), "left-align-popup");
if (base::i18n::IsRTL())
start_align = !start_align;
if (!start_align)
*x += allocation.width - menu_req.width;
*y = CalculateMenuYPosition(&screen_rect, &menu_req, widget, *y);
*push_in = FALSE;
}
// static
void MenuGtk::PointMenuPositionFunc(GtkMenu* menu,
int* x,
int* y,
gboolean* push_in,
gpointer userdata) {
*push_in = TRUE;
gfx::Point* point = reinterpret_cast<gfx::Point*>(userdata);
*x = point->x();
*y = point->y();
GtkRequisition menu_req;
gtk_widget_size_request(GTK_WIDGET(menu), &menu_req);
GdkScreen* screen;
gdk_display_get_pointer(gdk_display_get_default(), &screen, NULL, NULL, NULL);
gint monitor = gdk_screen_get_monitor_at_point(screen, *x, *y);
GdkRectangle screen_rect;
gdk_screen_get_monitor_geometry(screen, monitor, &screen_rect);
*y = CalculateMenuYPosition(&screen_rect, &menu_req, NULL, *y);
}
void MenuGtk::ExecuteCommand(ui::MenuModel* model, int id) {
if (delegate_)
delegate_->CommandWillBeExecuted();
GdkEvent* event = gtk_get_current_event();
int event_flags = 0;
if (event && event->type == GDK_BUTTON_RELEASE)
event_flags = event_utils::EventFlagsFromGdkState(event->button.state);
model->ActivatedAt(id, event_flags);
if (event)
gdk_event_free(event);
}
void MenuGtk::OnMenuShow(GtkWidget* widget) {
model_->MenuWillShow();
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(&MenuGtk::UpdateMenu, weak_factory_.GetWeakPtr()));
}
void MenuGtk::OnMenuHidden(GtkWidget* widget) {
if (delegate_)
delegate_->StoppedShowing();
model_->MenuClosed();
}
gboolean MenuGtk::OnMenuFocusOut(GtkWidget* widget, GdkEventFocus* event) {
gtk_widget_hide(menu_);
return TRUE;
}
void MenuGtk::OnSubMenuShow(GtkWidget* submenu) {
GtkWidget* menu_item = static_cast<GtkWidget*>(
g_object_get_data(G_OBJECT(submenu), "menu-item"));
// TODO(mdm): Figure out why this can sometimes be NULL. See bug 131974.
CHECK(menu_item);
// Notify the submenu model that the menu will be shown.
ui::MenuModel* submenu_model = static_cast<ui::MenuModel*>(
g_object_get_data(G_OBJECT(menu_item), "submenu-model"));
// We're extra cautious here, and bail out if the submenu model is NULL. In
// some cases we clear it out from a parent menu; we shouldn't ever show the
// menu after that, but we play it safe since we're dealing with wacky
// injected libraries that toy with our menus. (See comments below.)
if (!submenu_model)
return;
// If the submenu is already built, then return right away. This means we
// recently showed this submenu, and have not yet processed the fact that it
// was hidden before being shown again.
if (g_object_get_data(G_OBJECT(submenu), "submenu-built"))
return;
g_object_set_data(G_OBJECT(submenu), "submenu-built", GINT_TO_POINTER(1));
submenu_model->MenuWillShow();
// Actually build the submenu and attach it to the parent menu item.
BuildSubmenuFromModel(submenu_model, submenu);
gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu);
// Update all the menu item info in the newly-generated menu.
gtk_container_foreach(GTK_CONTAINER(submenu), SetMenuItemInfo, this);
}
void MenuGtk::OnSubMenuHidden(GtkWidget* submenu) {
if (is_menubar_)
return;
// Increase the reference count of the old submenu, and schedule it to be
// deleted later. We get this hide notification before we've processed menu
// activations, so if we were to delete the submenu now, we might lose the
// activation. This also lets us reuse the menu if it is shown again before
// it gets deleted; in that case, OnSubMenuHiddenCallback() just decrements
// the reference count again. Note that the delay is just an optimization; we
// could use PostTask() and this would still work correctly.
g_object_ref(G_OBJECT(submenu));
base::MessageLoop::current()->PostDelayedTask(
FROM_HERE,
base::Bind(&MenuGtk::OnSubMenuHiddenCallback, submenu),
base::TimeDelta::FromSeconds(2));
}
namespace {
// Remove all descendant submenu-model data pointers.
void RemoveSubMenuModels(GtkWidget* menu_item, void* unused) {
if (!GTK_IS_MENU_ITEM(menu_item))
return;
g_object_steal_data(G_OBJECT(menu_item), "submenu-model");
GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item));
if (submenu)
gtk_container_foreach(GTK_CONTAINER(submenu), RemoveSubMenuModels, NULL);
}
} // namespace
// static
void MenuGtk::OnSubMenuHiddenCallback(GtkWidget* submenu) {
if (!gtk_widget_get_visible(submenu)) {
// Remove all the children of this menu, clearing out their submenu-model
// pointers in case they have pending calls to OnSubMenuHiddenCallback().
// (Normally that won't happen: we'd have hidden them first, and so they'd
// have already been deleted. But in some cases [e.g. on Ubuntu 12.04],
// GTK menu operations may be hooked to allow external applications to
// mirror the menu structure, and the hooks may show and hide menus in
// order to trigger exactly the kind of dynamic menu building we're doing.
// The result is that we see show and hide events in strange orders.)
GList* children = gtk_container_get_children(GTK_CONTAINER(submenu));
for (GList* child = children; child; child = g_list_next(child)) {
RemoveSubMenuModels(GTK_WIDGET(child->data), NULL);
gtk_container_remove(GTK_CONTAINER(submenu), GTK_WIDGET(child->data));
}
g_list_free(children);
// Clear out the bit that says the menu is built.
// We'll rebuild it next time it is shown.
g_object_steal_data(G_OBJECT(submenu), "submenu-built");
// Notify the submenu model that the menu has been hidden. This may cause
// it to delete descendant submenu models, which is why we cleared those
// pointers out above.
GtkWidget* menu_item = static_cast<GtkWidget*>(
g_object_get_data(G_OBJECT(submenu), "menu-item"));
// TODO(mdm): Figure out why this can sometimes be NULL. See bug 124110.
CHECK(menu_item);
ui::MenuModel* submenu_model = static_cast<ui::MenuModel*>(
g_object_get_data(G_OBJECT(menu_item), "submenu-model"));
if (submenu_model)
submenu_model->MenuClosed();
}
// Remove the reference we grabbed in OnSubMenuHidden() above.
g_object_unref(G_OBJECT(submenu));
}
// static
void MenuGtk::SetButtonItemInfo(GtkWidget* button, gpointer userdata) {
ui::ButtonMenuItemModel* model =
reinterpret_cast<ui::ButtonMenuItemModel*>(
g_object_get_data(G_OBJECT(button), "button-model"));
int index = GPOINTER_TO_INT(g_object_get_data(
G_OBJECT(button), "button-model-id"));
if (model->IsItemDynamicAt(index)) {
std::string label = ui::ConvertAcceleratorsFromWindowsStyle(
base::UTF16ToUTF8(model->GetLabelAt(index)));
gtk_button_set_label(GTK_BUTTON(button), label.c_str());
}
gtk_widget_set_sensitive(GTK_WIDGET(button), model->IsEnabledAt(index));
}
// static
void MenuGtk::SetMenuItemInfo(GtkWidget* widget, gpointer userdata) {
if (GTK_IS_SEPARATOR_MENU_ITEM(widget)) {
// We need to explicitly handle this case because otherwise we'll ask the
// menu delegate about something with an invalid id.
return;
}
int id;
if (!GetMenuItemID(widget, &id))
return;
ui::MenuModel* model = ModelForMenuItem(GTK_MENU_ITEM(widget));
if (!model) {
// If we're not providing the sub menu, then there's no model. For
// example, the IME submenu doesn't have a model.
return;
}
if (GTK_IS_CHECK_MENU_ITEM(widget)) {
GtkCheckMenuItem* item = GTK_CHECK_MENU_ITEM(widget);
// gtk_check_menu_item_set_active() will send the activate signal. Touching
// the underlying "active" property will also call the "activate" handler
// for this menu item. So we prevent the "activate" handler from
// being called while we set the checkbox.
// Why not use one of the glib signal-blocking functions? Because when we
// toggle a radio button, it will deactivate one of the other radio buttons,
// which we don't have a pointer to.
// Wny not make this a member variable? Because "menu" is a pointer to the
// root of the MenuGtk and we want to disable *all* MenuGtks, including
// submenus.
block_activation_ = true;
gtk_check_menu_item_set_active(item, model->IsItemCheckedAt(id));
block_activation_ = false;
}
if (GTK_IS_CUSTOM_MENU_ITEM(widget)) {
// Iterate across all the buttons to update their visible properties.
gtk_custom_menu_item_foreach_button(GTK_CUSTOM_MENU_ITEM(widget),
SetButtonItemInfo,
userdata);
}
if (GTK_IS_MENU_ITEM(widget)) {
gtk_widget_set_sensitive(widget, model->IsEnabledAt(id));
if (model->IsVisibleAt(id)) {
// Update the menu item label if it is dynamic.
if (model->IsItemDynamicAt(id)) {
std::string label = ui::ConvertAcceleratorsFromWindowsStyle(
base::UTF16ToUTF8(model->GetLabelAt(id)));
gtk_menu_item_set_label(GTK_MENU_ITEM(widget), label.c_str());
if (GTK_IS_IMAGE_MENU_ITEM(widget)) {
gfx::Image icon;
if (model->GetIconAt(id, &icon)) {
gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(widget),
gtk_image_new_from_pixbuf(
icon.ToGdkPixbuf()));
} else {
gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(widget), NULL);
}
}
}
gtk_widget_show(widget);
} else {
gtk_widget_hide(widget);
}
GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(widget));
if (submenu) {
gtk_container_foreach(GTK_CONTAINER(submenu), &SetMenuItemInfo,
userdata);
}
}
}

224
browser/ui/gtk/menu_gtk.h Normal file
View file

@ -0,0 +1,224 @@
// Copyright (c) 2012 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.
#ifndef ATOM_BROWSER_UI_GTK_MENU_GTK_H_
#define ATOM_BROWSER_UI_GTK_MENU_GTK_H_
#include <gtk/gtk.h>
#include <string>
#include <vector>
#include "base/memory/weak_ptr.h"
#include "ui/base/gtk/gtk_signal.h"
#include "ui/base/gtk/gtk_signal_registrar.h"
#include "ui/gfx/point.h"
namespace gfx {
class Image;
}
namespace ui {
class ButtonMenuItemModel;
class MenuModel;
}
class MenuGtk {
public:
// Delegate class that lets another class control the status of the menu.
class Delegate {
public:
virtual ~Delegate() {}
// Called before a command is executed. This exists for the case where a
// model is handling the actual execution of commands, but the delegate
// still needs to know that some command got executed. This is called before
// and not after the command is executed because its execution may delete
// the menu and/or the delegate.
virtual void CommandWillBeExecuted() {}
// Called when the menu stops showing. This will be called before
// ExecuteCommand if the user clicks an item, but will also be called when
// the user clicks away from the menu.
virtual void StoppedShowing() {}
// Return true if we should override the "gtk-menu-images" system setting
// when showing image menu items for this menu.
virtual bool AlwaysShowIconForCmd(int command_id) const;
// Returns a tinted image used in button in a menu.
virtual GtkIconSet* GetIconSetForId(int idr);
// Returns an icon for the menu item, if available.
virtual GtkWidget* GetImageForCommandId(int command_id) const;
static GtkWidget* GetDefaultImageForLabel(const std::string& label);
};
MenuGtk(MenuGtk::Delegate* delegate,
ui::MenuModel* model,
bool is_menubar = false);
virtual ~MenuGtk();
// Initialize GTK signal handlers.
void ConnectSignalHandlers();
// These methods are used to build the menu dynamically. The return value
// is the new menu item.
GtkWidget* AppendMenuItemWithLabel(int command_id, const std::string& label);
GtkWidget* AppendMenuItemWithIcon(int command_id, const std::string& label,
const gfx::Image& icon);
GtkWidget* AppendCheckMenuItemWithLabel(int command_id,
const std::string& label);
GtkWidget* AppendSeparator();
GtkWidget* InsertSeparator(int position);
GtkWidget* AppendMenuItem(int command_id, GtkWidget* menu_item);
GtkWidget* InsertMenuItem(int command_id, GtkWidget* menu_item, int position);
GtkWidget* AppendMenuItemToMenu(int index,
ui::MenuModel* model,
GtkWidget* menu_item,
GtkWidget* menu,
bool connect_to_activate);
GtkWidget* InsertMenuItemToMenu(int index,
ui::MenuModel* model,
GtkWidget* menu_item,
GtkWidget* menu,
int position,
bool connect_to_activate);
// Displays the menu near a widget, as if the widget were a menu bar.
// Example: the wrench menu button.
// |button| is the mouse button that brought up the menu.
// |event_time| is the time from the GdkEvent.
void PopupForWidget(GtkWidget* widget, int button, guint32 event_time);
// Displays the menu as a context menu, i.e. at the cursor location.
// It is implicit that it was brought up using the right mouse button.
// |point| is the point where to put the menu.
// |event_time| is the time of the event that triggered the menu's display.
void PopupAsContext(const gfx::Point& point, guint32 event_time);
// Displays the menu as a context menu for the passed status icon.
void PopupAsContextForStatusIcon(guint32 event_time, guint32 button,
GtkStatusIcon* icon);
// Displays the menu following a keyboard event (such as selecting |widget|
// and pressing "enter").
void PopupAsFromKeyEvent(GtkWidget* widget);
// Closes the menu.
void Cancel();
// Repositions the menu to be right under the button. Alignment is set as
// object data on |void_widget| with the tag "left_align". If "left_align"
// is true, it aligns the left side of the menu with the left side of the
// button. Otherwise it aligns the right side of the menu with the right side
// of the button. Public since some menus have odd requirements that don't
// belong in a public class.
static void WidgetMenuPositionFunc(GtkMenu* menu,
int* x,
int* y,
gboolean* push_in,
void* void_widget);
// Positions the menu to appear at the gfx::Point represented by |userdata|.
static void PointMenuPositionFunc(GtkMenu* menu,
int* x,
int* y,
gboolean* push_in,
gpointer userdata);
GtkWidget* widget() const { return menu_; }
ui::MenuModel* model() const { return model_;}
// Updates all the enabled/checked states and the dynamic labels.
void UpdateMenu();
private:
// Builds a GtkImageMenuItem.
GtkWidget* BuildMenuItemWithImage(const std::string& label,
const gfx::Image& icon);
GtkWidget* BuildMenuItemWithImage(const std::string& label,
GtkWidget* image);
GtkWidget* BuildMenuItemWithLabel(const std::string& label,
int command_id);
// A function that creates a GtkMenu from |model_|.
void BuildMenuFromModel();
// Implementation of the above; called recursively.
void BuildSubmenuFromModel(ui::MenuModel* model, GtkWidget* menu);
// Builds a menu item with buttons in it from the data in the model.
GtkWidget* BuildButtonMenuItem(ui::ButtonMenuItemModel* model,
GtkWidget* menu);
void ExecuteCommand(ui::MenuModel* model, int id);
// Callback for when a menu item is clicked.
CHROMEGTK_CALLBACK_0(MenuGtk, void, OnMenuItemActivated);
// Called when one of the buttons is pressed.
CHROMEGTK_CALLBACK_1(MenuGtk, void, OnMenuButtonPressed, int);
// Called to maybe activate a button if that button isn't supposed to dismiss
// the menu.
CHROMEGTK_CALLBACK_1(MenuGtk, gboolean, OnMenuTryButtonPressed, int);
// Updates all the menu items' state.
CHROMEGTK_CALLBACK_0(MenuGtk, void, OnMenuShow);
// Sets the activating widget back to a normal appearance.
CHROMEGTK_CALLBACK_0(MenuGtk, void, OnMenuHidden);
// Focus out event handler for the menu.
CHROMEGTK_CALLBACK_1(MenuGtk, gboolean, OnMenuFocusOut, GdkEventFocus*);
// Handles building dynamic submenus on demand when they are shown.
CHROMEGTK_CALLBACK_0(MenuGtk, void, OnSubMenuShow);
// Handles trearing down dynamic submenus when they have been closed.
CHROMEGTK_CALLBACK_0(MenuGtk, void, OnSubMenuHidden);
// Scheduled by OnSubMenuHidden() to avoid deleting submenus when hidden
// before pending activations within them are delivered.
static void OnSubMenuHiddenCallback(GtkWidget* submenu);
// Sets the enable/disabled state and dynamic labels on our menu items.
static void SetButtonItemInfo(GtkWidget* button, gpointer userdata);
// Sets the check mark, enabled/disabled state and dynamic labels on our menu
// items.
static void SetMenuItemInfo(GtkWidget* widget, void* raw_menu);
// Queries this object about the menu state.
MenuGtk::Delegate* delegate_;
// If non-NULL, the MenuModel that we use to populate and control the GTK
// menu (overriding the delegate as a controller).
ui::MenuModel* model_;
// Whether this is a menu bar.
bool is_menubar_;
// For some menu items, we want to show the accelerator, but not actually
// explicitly handle it. To this end we connect those menu items' accelerators
// to this group, but don't attach this group to any top level window.
GtkAccelGroup* dummy_accel_group_;
// gtk_menu_popup() does not appear to take ownership of popup menus, so
// MenuGtk explicitly manages the lifetime of the menu.
GtkWidget* menu_;
// True when we should ignore "activate" signals. Used to prevent
// menu items from getting activated when we are setting up the
// menu.
static bool block_activation_;
ui::GtkSignalRegistrar signal_;
base::WeakPtrFactory<MenuGtk> weak_factory_;
};
#endif // ATOM_BROWSER_UI_GTK_MENU_GTK_H_