diff options
author | tfarina@chromium.org <tfarina@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-11-28 15:33:33 +0000 |
---|---|---|
committer | tfarina@chromium.org <tfarina@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-11-28 15:33:33 +0000 |
commit | 5e554e8fbf652534b50c89e2ffafaeb96161cc80 (patch) | |
tree | 26750feec0b3e886ee55f5b8b3eee37414aa1860 /ui/views | |
parent | 23b8a3ca4a6f95b5f9e8fd9ae0553ad141b8e991 (diff) | |
download | chromium_src-5e554e8fbf652534b50c89e2ffafaeb96161cc80.zip chromium_src-5e554e8fbf652534b50c89e2ffafaeb96161cc80.tar.gz chromium_src-5e554e8fbf652534b50c89e2ffafaeb96161cc80.tar.bz2 |
views: Move menu directory to ui/views/controls/.
BUG=104039
R=maruel@chromium.org
TBR=ben@chromium.org
Review URL: http://codereview.chromium.org/8718004
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@111701 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui/views')
67 files changed, 10906 insertions, 20 deletions
diff --git a/ui/views/controls/button/button_dropdown.cc b/ui/views/controls/button/button_dropdown.cc index e344271..a9cbb94 100644 --- a/ui/views/controls/button/button_dropdown.cc +++ b/ui/views/controls/button/button_dropdown.cc @@ -12,10 +12,10 @@ #include "ui/base/accessibility/accessible_view_state.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/models/menu_model.h" +#include "ui/views/controls/menu/menu_item_view.h" +#include "ui/views/controls/menu/menu_model_adapter.h" +#include "ui/views/controls/menu/menu_runner.h" #include "ui/views/widget/widget.h" -#include "views/controls/menu/menu_item_view.h" -#include "views/controls/menu/menu_model_adapter.h" -#include "views/controls/menu/menu_runner.h" namespace views { diff --git a/ui/views/controls/button/menu_button.cc b/ui/views/controls/button/menu_button.cc index 8a61cbb..56ebbea 100644 --- a/ui/views/controls/button/menu_button.cc +++ b/ui/views/controls/button/menu_button.cc @@ -14,10 +14,10 @@ #include "ui/gfx/canvas.h" #include "ui/gfx/screen.h" #include "ui/views/controls/button/button.h" +#include "ui/views/controls/menu/view_menu_delegate.h" #include "ui/views/events/event.h" #include "ui/views/widget/root_view.h" #include "ui/views/widget/widget.h" -#include "views/controls/menu/view_menu_delegate.h" using base::Time; using base::TimeDelta; diff --git a/ui/views/controls/combobox/native_combobox_views.cc b/ui/views/controls/combobox/native_combobox_views.cc index 1e8e535..4c3bc63 100644 --- a/ui/views/controls/combobox/native_combobox_views.cc +++ b/ui/views/controls/combobox/native_combobox_views.cc @@ -16,13 +16,13 @@ #include "ui/gfx/font.h" #include "ui/gfx/path.h" #include "ui/views/controls/combobox/combobox.h" +#include "ui/views/controls/menu/menu_runner.h" +#include "ui/views/controls/menu/submenu_view.h" #include "ui/views/widget/root_view.h" #include "ui/views/widget/widget.h" #include "views/background.h" #include "views/border.h" #include "views/controls/focusable_border.h" -#include "views/controls/menu/menu_runner.h" -#include "views/controls/menu/submenu_view.h" #if defined(OS_LINUX) #include "ui/gfx/gtk_util.h" diff --git a/ui/views/controls/combobox/native_combobox_views.h b/ui/views/controls/combobox/native_combobox_views.h index 571b70b..784539e 100644 --- a/ui/views/controls/combobox/native_combobox_views.h +++ b/ui/views/controls/combobox/native_combobox_views.h @@ -7,7 +7,7 @@ #pragma once #include "ui/views/controls/combobox/native_combobox_wrapper.h" -#include "views/controls/menu/menu_delegate.h" +#include "ui/views/controls/menu/menu_delegate.h" #include "views/view.h" namespace gfx { diff --git a/ui/views/controls/menu/menu.cc b/ui/views/controls/menu/menu.cc new file mode 100644 index 0000000..2607cde --- /dev/null +++ b/ui/views/controls/menu/menu.cc @@ -0,0 +1,102 @@ +// 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 "ui/views/controls/menu/menu.h" + +#include "base/i18n/rtl.h" +#include "third_party/skia/include/core/SkBitmap.h" + +namespace views { + +bool Menu::Delegate::IsRightToLeftUILayout() const { + return base::i18n::IsRTL(); +} + +const SkBitmap& Menu::Delegate::GetEmptyIcon() const { + static const SkBitmap* empty_icon = new SkBitmap(); + return *empty_icon; +} + +Menu::Menu(Delegate* delegate, AnchorPoint anchor) + : delegate_(delegate), + anchor_(anchor) { +} + +Menu::Menu(Menu* parent) + : delegate_(parent->delegate_), + anchor_(parent->anchor_) { +} + +Menu::~Menu() { +} + +void Menu::AppendMenuItem(int item_id, + const string16& label, + MenuItemType type) { + AddMenuItem(-1, item_id, label, type); +} + +void Menu::AddMenuItem(int index, + int item_id, + const string16& label, + MenuItemType type) { + if (type == SEPARATOR) + AddSeparator(index); + else + AddMenuItemInternal(index, item_id, label, SkBitmap(), type); +} + +Menu* Menu::AppendSubMenu(int item_id, const string16& label) { + return AddSubMenu(-1, item_id, label); +} + +Menu* Menu::AddSubMenu(int index, int item_id, const string16& label) { + return AddSubMenuWithIcon(index, item_id, label, SkBitmap()); +} + +Menu* Menu::AppendSubMenuWithIcon(int item_id, + const string16& label, + const SkBitmap& icon) { + return AddSubMenuWithIcon(-1, item_id, label, icon); +} + +void Menu::AppendMenuItemWithLabel(int item_id, const string16& label) { + AddMenuItemWithLabel(-1, item_id, label); +} + +void Menu::AddMenuItemWithLabel(int index, + int item_id, + const string16& label) { + AddMenuItem(index, item_id, label, Menu::NORMAL); +} + +void Menu::AppendDelegateMenuItem(int item_id) { + AddDelegateMenuItem(-1, item_id); +} + +void Menu::AddDelegateMenuItem(int index, int item_id) { + AddMenuItem(index, item_id, string16(), Menu::NORMAL); +} + +void Menu::AppendSeparator() { + AddSeparator(-1); +} + +void Menu::AppendMenuItemWithIcon(int item_id, + const string16& label, + const SkBitmap& icon) { + AddMenuItemWithIcon(-1, item_id, label, icon); +} + +void Menu::AddMenuItemWithIcon(int index, + int item_id, + const string16& label, + const SkBitmap& icon) { + AddMenuItemInternal(index, item_id, label, icon, Menu::NORMAL); +} + +Menu::Menu() : delegate_(NULL), anchor_(TOPLEFT) { +} + +} // namespace views diff --git a/ui/views/controls/menu/menu.h b/ui/views/controls/menu/menu.h new file mode 100644 index 0000000..ed20389 --- /dev/null +++ b/ui/views/controls/menu/menu.h @@ -0,0 +1,277 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_H_ +#pragma once + +#include <string> + +#include "base/basictypes.h" +#include "base/string16.h" +#include "ui/gfx/native_widget_types.h" +#include "views/views_export.h" + +class SkBitmap; + +namespace gfx { +class Point; +} + +namespace ui { +class Accelerator; +} + +namespace views { + +class VIEWS_EXPORT Menu { + public: + ///////////////////////////////////////////////////////////////////////////// + // + // Delegate Interface + // + // Classes implement this interface to tell the menu system more about each + // item as it is created. + // + ///////////////////////////////////////////////////////////////////////////// + class VIEWS_EXPORT Delegate { + public: + virtual ~Delegate() {} + + // Whether or not an item should be shown as checked. + virtual bool IsItemChecked(int id) const { + return false; + } + + // Whether or not an item should be shown as the default (using bold). + // There can only be one default menu item. + virtual bool IsItemDefault(int id) const { + return false; + } + + // The string shown for the menu item. + virtual string16 GetLabel(int id) const { + return string16(); + } + + // The delegate needs to implement this function if it wants to display + // the shortcut text next to each menu item. If there is an accelerator + // for a given item id, the implementor must return it. + virtual bool GetAcceleratorInfo(int id, ui::Accelerator* accel) { + return false; + } + + // The icon shown for the menu item. + virtual const SkBitmap& GetIcon(int id) const { + return GetEmptyIcon(); + } + + // The number of items to show in the menu + virtual int GetItemCount() const { + return 0; + } + + // Whether or not an item is a separator. + virtual bool IsItemSeparator(int id) const { + return false; + } + + // Shows the context menu with the specified id. This is invoked when the + // user does the appropriate gesture to show a context menu. The id + // identifies the id of the menu to show the context menu for. + // is_mouse_gesture is true if this is the result of a mouse gesture. + // If this is not the result of a mouse gesture |p| is the recommended + // location to display the content menu at. In either case, |p| is in + // screen coordinates. + virtual void ShowContextMenu(Menu* source, + int id, + const gfx::Point& p, + bool is_mouse_gesture) { + } + + // Whether an item has an icon. + virtual bool HasIcon(int id) const { + return false; + } + + // Notification that the menu is about to be popped up. + virtual void MenuWillShow() { + } + + // Whether to create a right-to-left menu. The default implementation + // returns true if the locale's language is a right-to-left language (such + // as Hebrew) and false otherwise. This is generally the right behavior + // since there is no reason to show left-to-right menus for right-to-left + // locales. However, subclasses can override this behavior so that the menu + // is a right-to-left menu only if the view's layout is right-to-left + // (since the view can use a different layout than the locale's language + // layout). + virtual bool IsRightToLeftUILayout() const; + + // Controller + virtual bool SupportsCommand(int id) const { + return true; + } + virtual bool IsCommandEnabled(int id) const { + return true; + } + virtual bool GetContextualLabel(int id, string16* out) const { + return false; + } + virtual void ExecuteCommand(int id) { + } + + protected: + // Returns an empty icon. + const SkBitmap& GetEmptyIcon() const; + }; + + // How this popup should align itself relative to the point it is run at. + enum AnchorPoint { + TOPLEFT, + TOPRIGHT + }; + + // Different types of menu items + enum MenuItemType { + NORMAL, + CHECKBOX, + RADIO, + SEPARATOR + }; + + // Construct a Menu using the specified controller to determine command + // state. + // delegate A Menu::Delegate implementation that provides more + // information about the Menu presentation. + // anchor An alignment hint for the popup menu. + // owner The window that the menu is being brought up relative + // to. Not actually used for anything but must not be + // NULL. + Menu(Delegate* delegate, AnchorPoint anchor); + Menu(); + virtual ~Menu(); + + static Menu* Create(Delegate* delegate, + AnchorPoint anchor, + gfx::NativeView parent); + + // Creates a new menu with the contents of the system menu for the given + // parent window. The caller owns the returned pointer. + static Menu* GetSystemMenu(gfx::NativeWindow parent); + + void set_delegate(Delegate* delegate) { delegate_ = delegate; } + Delegate* delegate() const { return delegate_; } + + AnchorPoint anchor() const { return anchor_; } + + // Adds an item to this menu. + // item_id The id of the item, used to identify it in delegate callbacks + // or (if delegate is NULL) to identify the command associated + // with this item with the controller specified in the ctor. Note + // that this value should not be 0 as this has a special meaning + // ("NULL command, no item selected") + // label The text label shown. + // type The type of item. + void AppendMenuItem(int item_id, + const string16& label, + MenuItemType type); + void AddMenuItem(int index, + int item_id, + const string16& label, + MenuItemType type); + + // Append a submenu to this menu. + // The returned pointer is owned by this menu. + Menu* AppendSubMenu(int item_id, + const string16& label); + Menu* AddSubMenu(int index, int item_id, const string16& label); + + // Append a submenu with an icon to this menu + // The returned pointer is owned by this menu. + // Unless the icon is empty, calling this function forces the Menu class + // to draw the menu, instead of relying on Windows. + Menu* AppendSubMenuWithIcon(int item_id, + const string16& label, + const SkBitmap& icon); + virtual Menu* AddSubMenuWithIcon(int index, + int item_id, + const string16& label, + const SkBitmap& icon) = 0; + + // This is a convenience for standard text label menu items where the label + // is provided with this call. + void AppendMenuItemWithLabel(int item_id, const string16& label); + void AddMenuItemWithLabel(int index, int item_id, const string16& label); + + // This is a convenience for text label menu items where the label is + // provided by the delegate. + void AppendDelegateMenuItem(int item_id); + void AddDelegateMenuItem(int index, int item_id); + + // Adds a separator to this menu + void AppendSeparator(); + virtual void AddSeparator(int index) = 0; + + // Appends a menu item with an icon. This is for the menu item which + // needs an icon. Calling this function forces the Menu class to draw + // the menu, instead of relying on Windows. + void AppendMenuItemWithIcon(int item_id, + const string16& label, + const SkBitmap& icon); + virtual void AddMenuItemWithIcon(int index, + int item_id, + const string16& label, + const SkBitmap& icon); + + // Enables or disables the item with the specified id. + virtual void EnableMenuItemByID(int item_id, bool enabled) = 0; + virtual void EnableMenuItemAt(int index, bool enabled) = 0; + + // Sets menu label at specified index. + virtual void SetMenuLabel(int item_id, const string16& label) = 0; + + // Sets an icon for an item with a given item_id. Calling this function + // also forces the Menu class to draw the menu, instead of relying on Windows. + // Returns false if the item with |item_id| is not found. + virtual bool SetIcon(const SkBitmap& icon, int item_id) = 0; + + // Shows the menu, blocks until the user dismisses the menu or selects an + // item, and executes the command for the selected item (if any). + // Warning: Blocking call. Will implicitly run a message loop. + virtual void RunMenuAt(int x, int y) = 0; + + // Cancels the menu. + virtual void Cancel() = 0; + + // Returns the number of menu items. + virtual int ItemCount() = 0; + +#if defined(OS_WIN) + // Returns the underlying menu handle + virtual HMENU GetMenuHandle() const = 0; +#endif // defined(OS_WIN) + + protected: + explicit Menu(Menu* parent); + + virtual void AddMenuItemInternal(int index, + int item_id, + const string16& label, + const SkBitmap& icon, + MenuItemType type) = 0; + + private: + // The delegate that is being used to get information about the presentation. + Delegate* delegate_; + + // How this popup menu should be aligned relative to the point it is run at. + AnchorPoint anchor_; + + DISALLOW_COPY_AND_ASSIGN(Menu); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_H_ diff --git a/ui/views/controls/menu/menu_2.cc b/ui/views/controls/menu/menu_2.cc new file mode 100644 index 0000000..d9aad1e --- /dev/null +++ b/ui/views/controls/menu/menu_2.cc @@ -0,0 +1,61 @@ +// 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 "ui/views/controls/menu/menu_2.h" + +#include "ui/base/models/menu_model.h" +#include "ui/views/controls/menu/menu_listener.h" + +namespace views { + +Menu2::Menu2(ui::MenuModel* model) + : model_(model), + ALLOW_THIS_IN_INITIALIZER_LIST( + wrapper_(MenuWrapper::CreateWrapper(this))) { + Rebuild(); +} + +Menu2::~Menu2() {} + +gfx::NativeMenu Menu2::GetNativeMenu() const { + return wrapper_->GetNativeMenu(); +} + +void Menu2::RunMenuAt(const gfx::Point& point, Alignment alignment) { + wrapper_->RunMenuAt(point, alignment); +} + +void Menu2::RunContextMenuAt(const gfx::Point& point) { + RunMenuAt(point, ALIGN_TOPLEFT); +} + +void Menu2::CancelMenu() { + wrapper_->CancelMenu(); +} + +void Menu2::Rebuild() { + wrapper_->Rebuild(); +} + +void Menu2::UpdateStates() { + wrapper_->UpdateStates(); +} + +MenuWrapper::MenuAction Menu2::GetMenuAction() const { + return wrapper_->GetMenuAction(); +} + +void Menu2::AddMenuListener(MenuListener* listener) { + wrapper_->AddMenuListener(listener); +} + +void Menu2::RemoveMenuListener(MenuListener* listener) { + wrapper_->RemoveMenuListener(listener); +} + +void Menu2::SetMinimumWidth(int width) { + wrapper_->SetMinimumWidth(width); +} + +} // namespace diff --git a/ui/views/controls/menu/menu_2.h b/ui/views/controls/menu/menu_2.h new file mode 100644 index 0000000..898596e3 --- /dev/null +++ b/ui/views/controls/menu/menu_2.h @@ -0,0 +1,100 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_2_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_2_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "ui/gfx/native_widget_types.h" +#include "ui/views/controls/menu/menu_wrapper.h" +#include "views/views_export.h" + +namespace gfx { +class Point; +} + +namespace ui { +class MenuModel; +} + +namespace views { + +class NativeMenuGtk; + +// A menu. Populated from a model, and relies on a delegate to execute commands. +// +// WARNING: do NOT create and use Menu2 on the stack. Menu2 notifies the model +// of selection AFTER a delay. This means that if use a Menu2 on the stack +// ActivatedAt is never invoked. +class VIEWS_EXPORT Menu2 { + public: + // How the menu is aligned relative to the point it is shown at. + // The alignment is reversed by menu if text direction is right to left. + enum Alignment { + ALIGN_TOPLEFT, + ALIGN_TOPRIGHT + }; + + // Creates a new menu populated with the contents of |model|. + // WARNING: this populates the menu on construction by invoking methods on + // the model. As such, it is typically not safe to use this as the model + // from the constructor. EG: + // MyClass : menu_(this) {} + // is likely to have problems. + explicit Menu2(ui::MenuModel* model); + virtual ~Menu2(); + + // Runs the menu at the specified point. This method blocks until done. + // RunContextMenuAt is the same, but the alignment is the default for a + // context menu. + void RunMenuAt(const gfx::Point& point, Alignment alignment); + void RunContextMenuAt(const gfx::Point& point); + + // Cancels the active menu. + void CancelMenu(); + + // Called when the model supplying data to this menu has changed, and the menu + // must be rebuilt. + void Rebuild(); + + // Called when the states of the menu items in the menu should be refreshed + // from the model. + void UpdateStates(); + + // For submenus. + gfx::NativeMenu GetNativeMenu() const; + + // Get the result of the last call to RunMenuAt to determine whether an + // item was selected, the user navigated to a next or previous menu, or + // nothing. + MenuWrapper::MenuAction GetMenuAction() const; + + // Add a listener to receive a callback when the menu opens. + void AddMenuListener(MenuListener* listener); + + // Remove a menu listener. + void RemoveMenuListener(MenuListener* listener); + + // Accessors. + ui::MenuModel* model() const { return model_; } + + // Sets the minimum width of the menu. + void SetMinimumWidth(int width); + + private: + friend class NativeMenuGtk; + + ui::MenuModel* model_; + + // The object that actually implements the menu. + scoped_ptr<MenuWrapper> wrapper_; + + DISALLOW_COPY_AND_ASSIGN(Menu2); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_2_H_ diff --git a/ui/views/controls/menu/menu_config.cc b/ui/views/controls/menu/menu_config.cc new file mode 100644 index 0000000..8fd9d0b --- /dev/null +++ b/ui/views/controls/menu/menu_config.cc @@ -0,0 +1,53 @@ +// 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 "ui/views/controls/menu/menu_config.h" + +#include "build/build_config.h" + +namespace views { + +static MenuConfig* config_instance = NULL; + +MenuConfig::MenuConfig() + : text_color(SK_ColorBLACK), + item_top_margin(3), + item_bottom_margin(4), + item_no_icon_top_margin(1), + item_no_icon_bottom_margin(3), + item_left_margin(4), + label_to_arrow_padding(10), + arrow_to_edge_padding(5), + icon_to_label_padding(8), + gutter_to_label(5), + check_width(16), + check_height(16), + radio_width(16), + radio_height(16), + arrow_height(9), + arrow_width(9), + gutter_width(0), + separator_height(6), + render_gutter(false), + show_mnemonics(false), + scroll_arrow_height(3), + label_to_accelerator_padding(10), + show_accelerators(true) { +} + +MenuConfig::~MenuConfig() {} + +void MenuConfig::Reset() { + delete config_instance; + config_instance = NULL; +} + +// static +const MenuConfig& MenuConfig::instance() { + if (!config_instance) + config_instance = Create(); + return *config_instance; +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_config.h b/ui/views/controls/menu/menu_config.h new file mode 100644 index 0000000..92014f9 --- /dev/null +++ b/ui/views/controls/menu/menu_config.h @@ -0,0 +1,102 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_CONFIG_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_CONFIG_H_ +#pragma once + +#include "third_party/skia/include/core/SkColor.h" +#include "ui/gfx/font.h" +#include "views/views_export.h" + +namespace views { + +// Layout type information for menu items. Use the instance() method to obtain +// the MenuConfig for the current platform. +struct VIEWS_EXPORT MenuConfig { + MenuConfig(); + ~MenuConfig(); + + // Resets the single shared MenuConfig instance. The next time instance() is + // invoked a new MenuConfig is created and configured. + static void Reset(); + + // Returns the single shared MenuConfig instance, creating if necessary. + static const MenuConfig& instance(); + + // Font used by menus. + gfx::Font font; + + // Normal text color. + SkColor text_color; + + // Margins between the top of the item and the label. + int item_top_margin; + + // Margins between the bottom of the item and the label. + int item_bottom_margin; + + // Margins used if the menu doesn't have icons. + int item_no_icon_top_margin; + int item_no_icon_bottom_margin; + + // Margins between the left of the item and the icon. + int item_left_margin; + + // Padding between the label and submenu arrow. + int label_to_arrow_padding; + + // Padding between the arrow and the edge. + int arrow_to_edge_padding; + + // Padding between the icon and label. + int icon_to_label_padding; + + // Padding between the gutter and label. + int gutter_to_label; + + // Size of the check. + int check_width; + int check_height; + + // Size of the radio bullet. + int radio_width; + int radio_height; + + // Size of the submenu arrow. + int arrow_height; + int arrow_width; + + // Width of the gutter. Only used if render_gutter is true. + int gutter_width; + + // Height of the separator. + int separator_height; + + // Whether or not the gutter should be rendered. The gutter is specific to + // Vista. + bool render_gutter; + + // Are mnemonics shown? + bool show_mnemonics; + + // Height of the scroll arrow. + int scroll_arrow_height; + + // Padding between the label and accelerator. Only used if there is an + // accelerator. + int label_to_accelerator_padding; + + // Whether the keyboard accelerators are visible. + bool show_accelerators; + + private: + // Creates and configures a new MenuConfig as appropriate for the current + // platform. + static MenuConfig* Create(); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_CONFIG_H_ diff --git a/ui/views/controls/menu/menu_config_aura.cc b/ui/views/controls/menu/menu_config_aura.cc new file mode 100644 index 0000000..31c4203 --- /dev/null +++ b/ui/views/controls/menu/menu_config_aura.cc @@ -0,0 +1,25 @@ +// 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 "ui/views/controls/menu/menu_config.h" + +#include "grit/ui_resources.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "ui/base/resource/resource_bundle.h" + +namespace views { + +// static +MenuConfig* MenuConfig::Create() { + MenuConfig* config = new MenuConfig(); + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + config->font = rb.GetFont(ResourceBundle::BaseFont); + config->arrow_width = rb.GetBitmapNamed(IDR_MENU_ARROW)->width(); + // Add 4 to force some padding between check and label. + config->check_width = rb.GetBitmapNamed(IDR_MENU_CHECK)->width() + 4; + config->check_height = rb.GetBitmapNamed(IDR_MENU_CHECK)->height(); + return config; +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_config_linux.cc b/ui/views/controls/menu/menu_config_linux.cc new file mode 100644 index 0000000..31c4203 --- /dev/null +++ b/ui/views/controls/menu/menu_config_linux.cc @@ -0,0 +1,25 @@ +// 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 "ui/views/controls/menu/menu_config.h" + +#include "grit/ui_resources.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "ui/base/resource/resource_bundle.h" + +namespace views { + +// static +MenuConfig* MenuConfig::Create() { + MenuConfig* config = new MenuConfig(); + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + config->font = rb.GetFont(ResourceBundle::BaseFont); + config->arrow_width = rb.GetBitmapNamed(IDR_MENU_ARROW)->width(); + // Add 4 to force some padding between check and label. + config->check_width = rb.GetBitmapNamed(IDR_MENU_CHECK)->width() + 4; + config->check_height = rb.GetBitmapNamed(IDR_MENU_CHECK)->height(); + return config; +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_config_win.cc b/ui/views/controls/menu/menu_config_win.cc new file mode 100644 index 0000000..07681fe --- /dev/null +++ b/ui/views/controls/menu/menu_config_win.cc @@ -0,0 +1,104 @@ +// 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 "ui/views/controls/menu/menu_config.h" + +#include <windows.h> +#include <uxtheme.h> +#include <Vssym32.h> + +#include "base/logging.h" +#include "base/win/scoped_gdi_object.h" +#include "base/win/win_util.h" +#include "ui/base/l10n/l10n_util_win.h" +#include "ui/gfx/native_theme_win.h" + +using gfx::NativeTheme; +using gfx::NativeThemeWin; + +namespace views { + +// static +MenuConfig* MenuConfig::Create() { + MenuConfig* config = new MenuConfig(); + + config->text_color = NativeThemeWin::instance()->GetThemeColorWithDefault( + NativeThemeWin::MENU, MENU_POPUPITEM, MPI_NORMAL, TMT_TEXTCOLOR, + COLOR_MENUTEXT); + + NONCLIENTMETRICS metrics; + base::win::GetNonClientMetrics(&metrics); + l10n_util::AdjustUIFont(&(metrics.lfMenuFont)); + { + base::win::ScopedHFONT font(CreateFontIndirect(&metrics.lfMenuFont)); + DLOG_ASSERT(font.Get()); + config->font = gfx::Font(font); + } + NativeTheme::ExtraParams extra; + extra.menu_check.is_radio = false; + extra.menu_check.is_selected = false; + gfx::Size check_size = NativeTheme::instance()->GetPartSize( + NativeTheme::kMenuCheck, NativeTheme::kNormal, extra); + if (!check_size.IsEmpty()) { + config->check_width = check_size.width(); + config->check_height = check_size.height(); + } else { + config->check_width = GetSystemMetrics(SM_CXMENUCHECK); + config->check_height = GetSystemMetrics(SM_CYMENUCHECK); + } + + extra.menu_check.is_radio = true; + gfx::Size radio_size = NativeTheme::instance()->GetPartSize( + NativeTheme::kMenuCheck, NativeTheme::kNormal, extra); + if (!radio_size.IsEmpty()) { + config->radio_width = radio_size.width(); + config->radio_height = radio_size.height(); + } else { + config->radio_width = GetSystemMetrics(SM_CXMENUCHECK); + config->radio_height = GetSystemMetrics(SM_CYMENUCHECK); + } + + gfx::Size arrow_size = NativeTheme::instance()->GetPartSize( + NativeTheme::kMenuPopupArrow, NativeTheme::kNormal, extra); + if (!arrow_size.IsEmpty()) { + config->arrow_width = arrow_size.width(); + config->arrow_height = arrow_size.height(); + } else { + // Sadly I didn't see a specify metrics for this. + config->arrow_width = GetSystemMetrics(SM_CXMENUCHECK); + config->arrow_height = GetSystemMetrics(SM_CYMENUCHECK); + } + + gfx::Size gutter_size = NativeTheme::instance()->GetPartSize( + NativeTheme::kMenuPopupGutter, NativeTheme::kNormal, extra); + if (!gutter_size.IsEmpty()) { + config->gutter_width = gutter_size.width(); + config->render_gutter = true; + } else { + config->gutter_width = 0; + config->render_gutter = false; + } + + gfx::Size separator_size = NativeTheme::instance()->GetPartSize( + NativeTheme::kMenuPopupSeparator, NativeTheme::kNormal, extra); + if (!separator_size.IsEmpty()) { + config->separator_height = separator_size.height(); + } else { + // -1 makes separator centered. + config->separator_height = GetSystemMetrics(SM_CYMENU) / 2 - 1; + } + + // On Windows, having some menus use wider spacing than others looks wrong. + // See http://crbug.com/88875 + config->item_no_icon_bottom_margin = config->item_bottom_margin; + config->item_no_icon_top_margin = config->item_top_margin; + + BOOL show_cues; + config->show_mnemonics = + (SystemParametersInfo(SPI_GETKEYBOARDCUES, 0, &show_cues, 0) && + show_cues == TRUE); + return config; +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_controller.cc b/ui/views/controls/menu/menu_controller.cc new file mode 100644 index 0000000..9678a81 --- /dev/null +++ b/ui/views/controls/menu/menu_controller.cc @@ -0,0 +1,1955 @@ +// 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 "ui/views/controls/menu/menu_controller.h" + +#include "base/i18n/case_conversion.h" +#include "base/i18n/rtl.h" +#include "base/time.h" +#include "base/utf_string_conversions.h" +#include "ui/base/dragdrop/os_exchange_data.h" +#include "ui/base/events.h" +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/gfx/canvas_skia.h" +#include "ui/gfx/screen.h" +#include "ui/views/controls/button/menu_button.h" +#include "ui/views/controls/menu/menu_controller_delegate.h" +#include "ui/views/controls/menu/menu_scroll_view_container.h" +#include "ui/views/controls/menu/submenu_view.h" +#include "ui/views/widget/root_view.h" +#include "ui/views/widget/widget.h" +#include "views/drag_utils.h" +#include "views/view_constants.h" +#include "views/views_delegate.h" + +#if defined(USE_AURA) +#include "ui/aura/desktop.h" +#elif defined(TOOLKIT_USES_GTK) +#include "ui/base/keycodes/keyboard_code_conversion_gtk.h" +#endif + +using base::Time; +using base::TimeDelta; +using ui::OSExchangeData; + +// Period of the scroll timer (in milliseconds). +static const int kScrollTimerMS = 30; + +// Delay, in ms, between when menus are selected are moused over and the menu +// appears. +static const int kShowDelay = 400; + +// Amount of time from when the drop exits the menu and the menu is hidden. +static const int kCloseOnExitTime = 1200; + +// Amount to inset submenus. +static const int kSubmenuHorizontalInset = 3; + +namespace views { + +namespace { + +// Returns true if the mnemonic of |menu| matches key. +bool MatchesMnemonic(MenuItemView* menu, char16 key) { + return menu->GetMnemonic() == key; +} + +// Returns true if |menu| doesn't have a mnemonic and first character of the its +// title is |key|. +bool TitleMatchesMnemonic(MenuItemView* menu, char16 key) { + if (menu->GetMnemonic()) + return false; + + string16 lower_title = base::i18n::ToLower(menu->title()); + return !lower_title.empty() && lower_title[0] == key; +} + +} // namespace + +// Convenience for scrolling the view such that the origin is visible. +static void ScrollToVisible(View* view) { + view->ScrollRectToVisible(view->GetLocalBounds()); +} + +// Returns the first descendant of |view| that is hot tracked. +static View* GetFirstHotTrackedView(View* view) { + if (!view) + return NULL; + + if (view->IsHotTracked()) + return view; + + for (int i = 0; i < view->child_count(); ++i) { + View* hot_view = GetFirstHotTrackedView(view->child_at(i)); + if (hot_view) + return hot_view; + } + return NULL; +} + +// Recurses through the child views of |view| returning the first view starting +// at |start| that is focusable. A value of -1 for |start| indicates to start at +// the first view (if |forward| is false, iterating starts at the last view). If +// |forward| is true the children are considered first to last, otherwise last +// to first. +static View* GetFirstFocusableView(View* view, int start, bool forward) { + if (forward) { + for (int i = start == -1 ? 0 : start; i < view->child_count(); ++i) { + View* deepest = GetFirstFocusableView(view->child_at(i), -1, forward); + if (deepest) + return deepest; + } + } else { + for (int i = start == -1 ? view->child_count() - 1 : start; i >= 0; --i) { + View* deepest = GetFirstFocusableView(view->child_at(i), -1, forward); + if (deepest) + return deepest; + } + } + return view->IsFocusableInRootView() ? view : NULL; +} + +// Returns the first child of |start| that is focusable. +static View* GetInitialFocusableView(View* start, bool forward) { + return GetFirstFocusableView(start, -1, forward); +} + +// Returns the next view after |start_at| that is focusable. Returns NULL if +// there are no focusable children of |ancestor| after |start_at|. +static View* GetNextFocusableView(View* ancestor, + View* start_at, + bool forward) { + DCHECK(ancestor->Contains(start_at)); + View* parent = start_at; + do { + View* new_parent = parent->parent(); + int index = new_parent->GetIndexOf(parent); + index += forward ? 1 : -1; + if (forward || index != -1) { + View* next = GetFirstFocusableView(new_parent, index, forward); + if (next) + return next; + } + parent = new_parent; + } while (parent != ancestor); + return NULL; +} + +// MenuScrollTask -------------------------------------------------------------- + +// MenuScrollTask is used when the SubmenuView does not all fit on screen and +// the mouse is over the scroll up/down buttons. MenuScrollTask schedules +// itself with a RepeatingTimer. When Run is invoked MenuScrollTask scrolls +// appropriately. + +class MenuController::MenuScrollTask { + public: + MenuScrollTask() : submenu_(NULL) { + pixels_per_second_ = MenuItemView::pref_menu_height() * 20; + } + + void Update(const MenuController::MenuPart& part) { + if (!part.is_scroll()) { + StopScrolling(); + return; + } + DCHECK(part.submenu); + SubmenuView* new_menu = part.submenu; + bool new_is_up = (part.type == MenuController::MenuPart::SCROLL_UP); + if (new_menu == submenu_ && is_scrolling_up_ == new_is_up) + return; + + start_scroll_time_ = Time::Now(); + start_y_ = part.submenu->GetVisibleBounds().y(); + submenu_ = new_menu; + is_scrolling_up_ = new_is_up; + + if (!scrolling_timer_.IsRunning()) { + scrolling_timer_.Start(FROM_HERE, + TimeDelta::FromMilliseconds(kScrollTimerMS), + this, &MenuScrollTask::Run); + } + } + + void StopScrolling() { + if (scrolling_timer_.IsRunning()) { + scrolling_timer_.Stop(); + submenu_ = NULL; + } + } + + // The menu being scrolled. Returns null if not scrolling. + SubmenuView* submenu() const { return submenu_; } + + private: + void Run() { + DCHECK(submenu_); + gfx::Rect vis_rect = submenu_->GetVisibleBounds(); + const int delta_y = static_cast<int>( + (Time::Now() - start_scroll_time_).InMilliseconds() * + pixels_per_second_ / 1000); + vis_rect.set_y(is_scrolling_up_ ? + std::max(0, start_y_ - delta_y) : + std::min(submenu_->height() - vis_rect.height(), start_y_ + delta_y)); + submenu_->ScrollRectToVisible(vis_rect); + } + + // SubmenuView being scrolled. + SubmenuView* submenu_; + + // Direction scrolling. + bool is_scrolling_up_; + + // Timer to periodically scroll. + base::RepeatingTimer<MenuScrollTask> scrolling_timer_; + + // Time we started scrolling at. + Time start_scroll_time_; + + // How many pixels to scroll per second. + int pixels_per_second_; + + // Y-coordinate of submenu_view_ when scrolling started. + int start_y_; + + DISALLOW_COPY_AND_ASSIGN(MenuScrollTask); +}; + +// MenuController:SelectByCharDetails ---------------------------------------- + +struct MenuController::SelectByCharDetails { + SelectByCharDetails() + : first_match(-1), + has_multiple(false), + index_of_item(-1), + next_match(-1) { + } + + // Index of the first menu with the specified mnemonic. + int first_match; + + // If true there are multiple menu items with the same mnemonic. + bool has_multiple; + + // Index of the selected item; may remain -1. + int index_of_item; + + // If there are multiple matches this is the index of the item after the + // currently selected item whose mnemonic matches. This may remain -1 even + // though there are matches. + int next_match; +}; + +// MenuController:State ------------------------------------------------------ + +MenuController::State::State() : item(NULL), submenu_open(false) {} + +MenuController::State::~State() {} + +// MenuController ------------------------------------------------------------ + +// static +MenuController* MenuController::active_instance_ = NULL; + +// static +MenuController* MenuController::GetActiveInstance() { + return active_instance_; +} + +MenuItemView* MenuController::Run(Widget* parent, + MenuButton* button, + MenuItemView* root, + const gfx::Rect& bounds, + MenuItemView::AnchorPosition position, + int* result_mouse_event_flags) { + exit_type_ = EXIT_NONE; + possible_drag_ = false; + drag_in_progress_ = false; + + bool nested_menu = showing_; + if (showing_) { + // Only support nesting of blocking_run menus, nesting of + // blocking/non-blocking shouldn't be needed. + DCHECK(blocking_run_); + + // We're already showing, push the current state. + menu_stack_.push_back(state_); + + // The context menu should be owned by the same parent. + DCHECK_EQ(owner_, parent); + } else { + showing_ = true; + } + + // Reset current state. + pending_state_ = State(); + state_ = State(); + UpdateInitialLocation(bounds, position); + + owner_ = parent; + + // Set the selection, which opens the initial menu. + SetSelection(root, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); + + if (!blocking_run_) { + // Start the timer to hide the menu. This is needed as we get no + // notification when the drag has finished. + StartCancelAllTimer(); + return NULL; + } else if (button) { + menu_button_ = button; + } + + // Make sure Chrome doesn't attempt to shut down while the menu is showing. + if (ViewsDelegate::views_delegate) + ViewsDelegate::views_delegate->AddRef(); + + // We need to turn on nestable tasks as in some situations (pressing alt-f for + // one) the menus are run from a task. If we don't do this and are invoked + // from a task none of the tasks we schedule are processed and the menu + // appears totally broken. + MessageLoopForUI* loop = MessageLoopForUI::current(); + bool did_allow_task_nesting = loop->NestableTasksAllowed(); + loop->SetNestableTasksAllowed(true); + loop->RunWithDispatcher(this); + loop->SetNestableTasksAllowed(did_allow_task_nesting); + + if (ViewsDelegate::views_delegate) + ViewsDelegate::views_delegate->ReleaseRef(); + + // Close any open menus. + SetSelection(NULL, SELECTION_UPDATE_IMMEDIATELY | SELECTION_EXIT); + + if (nested_menu) { + DCHECK(!menu_stack_.empty()); + // We're running from within a menu, restore the previous state. + // The menus are already showing, so we don't have to show them. + state_ = menu_stack_.back(); + pending_state_ = menu_stack_.back(); + menu_stack_.pop_back(); + } else { + showing_ = false; + did_capture_ = false; + } + + MenuItemView* result = result_; + // In case we're nested, reset result_. + result_ = NULL; + + if (result_mouse_event_flags) + *result_mouse_event_flags = result_mouse_event_flags_; + + if (exit_type_ == EXIT_OUTERMOST) { + SetExitType(EXIT_NONE); + } else { + if (nested_menu && result) { + // We're nested and about to return a value. The caller might enter + // another blocking loop. We need to make sure all menus are hidden + // before that happens otherwise the menus will stay on screen. + CloseAllNestedMenus(); + SetSelection(NULL, SELECTION_UPDATE_IMMEDIATELY | SELECTION_EXIT); + + // Set exit_all_, which makes sure all nested loops exit immediately. + if (exit_type_ != EXIT_DESTROYED) + SetExitType(EXIT_ALL); + } + } + + // If we stopped running because one of the menus was destroyed chances are + // the button was also destroyed. + if (exit_type_ != EXIT_DESTROYED && menu_button_) { + menu_button_->SetState(CustomButton::BS_NORMAL); + menu_button_->SchedulePaint(); + } + + return result; +} + +void MenuController::Cancel(ExitType type) { + // If the menu has already been destroyed, no further cancellation is + // needed. We especially don't want to set the |exit_type_| to a lesser + // value. + if (exit_type_ == EXIT_DESTROYED || exit_type_ == type) + return; + + if (!showing_) { + // This occurs if we're in the process of notifying the delegate for a drop + // and the delegate cancels us. + return; + } + + MenuItemView* selected = state_.item; + SetExitType(type); + + SendMouseCaptureLostToActiveView(); + + // Hide windows immediately. + SetSelection(NULL, SELECTION_UPDATE_IMMEDIATELY | SELECTION_EXIT); + + if (!blocking_run_) { + // If we didn't block the caller we need to notify the menu, which + // triggers deleting us. + DCHECK(selected); + showing_ = false; + delegate_->DropMenuClosed( + internal::MenuControllerDelegate::NOTIFY_DELEGATE, + selected->GetRootMenuItem()); + // WARNING: the call to MenuClosed deletes us. + return; + } +} + +void MenuController::OnMousePressed(SubmenuView* source, + const MouseEvent& event) { + if (!blocking_run_) + return; + + DCHECK(!active_mouse_view_); + + MenuPart part = GetMenuPart(source, event.location()); + if (part.is_scroll()) + return; // Ignore presses on scroll buttons. + + if (part.type == MenuPart::NONE || + (part.type == MenuPart::MENU_ITEM && part.menu && + part.menu->GetRootMenuItem() != state_.item->GetRootMenuItem())) { + // Mouse wasn't pressed over any menu, or the active menu, cancel. + + // We're going to close and we own the mouse capture. We need to repost the + // mouse down, otherwise the window the user clicked on won't get the + // event. +#if defined(OS_WIN) && !defined(USE_AURA) + RepostEvent(source, event); + // NOTE: not reposting on linux seems fine. +#endif + + // And close. + ExitType exit_type = EXIT_ALL; + if (!menu_stack_.empty()) { + // We're running nested menus. Only exit all if the mouse wasn't over one + // of the menus from the last run. + gfx::Point screen_loc(event.location()); + View::ConvertPointToScreen(source->GetScrollViewContainer(), &screen_loc); + MenuPart last_part = GetMenuPartByScreenCoordinateUsingMenu( + menu_stack_.back().item, screen_loc); + if (last_part.type != MenuPart::NONE) + exit_type = EXIT_OUTERMOST; + } + Cancel(exit_type); + return; + } + + // On a press we immediately commit the selection, that way a submenu + // pops up immediately rather than after a delay. + int selection_types = SELECTION_UPDATE_IMMEDIATELY; + if (!part.menu) { + part.menu = part.parent; + selection_types |= SELECTION_OPEN_SUBMENU; + } else { + if (part.menu->GetDelegate()->CanDrag(part.menu)) { + possible_drag_ = true; + press_pt_ = event.location(); + } + if (part.menu->HasSubmenu()) + selection_types |= SELECTION_OPEN_SUBMENU; + } + SetSelection(part.menu, selection_types); +} + +void MenuController::OnMouseDragged(SubmenuView* source, + const MouseEvent& event) { + MenuPart part = GetMenuPart(source, event.location()); + UpdateScrolling(part); + + if (!blocking_run_) + return; + + if (possible_drag_) { + if (View::ExceededDragThreshold(event.x() - press_pt_.x(), + event.y() - press_pt_.y())) { + MenuItemView* item = state_.item; + DCHECK(item); + // Points are in the coordinates of the submenu, need to map to that of + // the selected item. Additionally source may not be the parent of + // the selected item, so need to map to screen first then to item. + gfx::Point press_loc(press_pt_); + View::ConvertPointToScreen(source->GetScrollViewContainer(), &press_loc); + View::ConvertPointToView(NULL, item, &press_loc); + // TODO(beng): Convert to CanvasSkia + gfx::CanvasSkia canvas(item->width(), item->height(), false); + item->PaintButton(&canvas, MenuItemView::PB_FOR_DRAG); + + OSExchangeData data; + item->GetDelegate()->WriteDragData(item, &data); + drag_utils::SetDragImageOnDataObject(canvas, item->size(), press_loc, + &data); + StopScrolling(); + int drag_ops = item->GetDelegate()->GetDragOperations(item); + drag_in_progress_ = true; + item->GetWidget()->RunShellDrag(NULL, data, drag_ops); + drag_in_progress_ = false; + + if (GetActiveInstance() == this) { + if (showing_) { + // We're still showing, close all menus. + CloseAllNestedMenus(); + Cancel(EXIT_ALL); + } // else case, drop was on us. + } // else case, someone canceled us, don't do anything + } + return; + } + MenuItemView* mouse_menu = NULL; + if (part.type == MenuPart::MENU_ITEM) { + if (!part.menu) + part.menu = source->GetMenuItem(); + else + mouse_menu = part.menu; + SetSelection(part.menu ? part.menu : state_.item, SELECTION_OPEN_SUBMENU); + } else if (part.type == MenuPart::NONE) { + ShowSiblingMenu(source, event); + } + UpdateActiveMouseView(source, event, mouse_menu); +} + +void MenuController::OnMouseReleased(SubmenuView* source, + const MouseEvent& event) { + if (!blocking_run_) + return; + + DCHECK(state_.item); + possible_drag_ = false; + DCHECK(blocking_run_); + MenuPart part = GetMenuPart(source, event.location()); + if (event.IsRightMouseButton() && (part.type == MenuPart::MENU_ITEM && + part.menu)) { + // Set the selection immediately, making sure the submenu is only open + // if it already was. + int selection_types = SELECTION_UPDATE_IMMEDIATELY; + if (state_.item == pending_state_.item && state_.submenu_open) + selection_types |= SELECTION_OPEN_SUBMENU; + SetSelection(pending_state_.item, selection_types); + gfx::Point loc(event.location()); + View::ConvertPointToScreen(source->GetScrollViewContainer(), &loc); + + // If we open a context menu just return now + if (part.menu->GetDelegate()->ShowContextMenu( + part.menu, part.menu->GetCommand(), loc, true)) { + SendMouseCaptureLostToActiveView(); + return; + } + } + + // We can use Ctrl+click or the middle mouse button to recursively open urls + // for selected folder menu items. If it's only a left click, show the + // contents of the folder. + if (!part.is_scroll() && part.menu && + !(part.menu->HasSubmenu() && + (event.flags() == ui::EF_LEFT_BUTTON_DOWN))) { + if (active_mouse_view_) { + SendMouseReleaseToActiveView(source, event); + return; + } + if (part.menu->GetDelegate()->IsTriggerableEvent(part.menu, event)) { + Accept(part.menu, event.flags()); + return; + } + } else if (part.type == MenuPart::MENU_ITEM) { + // User either clicked on empty space, or a menu that has children. + SetSelection(part.menu ? part.menu : state_.item, + SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); + } + SendMouseCaptureLostToActiveView(); +} + +void MenuController::OnMouseMoved(SubmenuView* source, + const MouseEvent& event) { + if (showing_submenu_) + return; + + MenuPart part = GetMenuPart(source, event.location()); + + UpdateScrolling(part); + + if (!blocking_run_) + return; + + if (part.type == MenuPart::NONE && ShowSiblingMenu(source, event)) + return; + + if (part.type == MenuPart::MENU_ITEM && part.menu) { + SetSelection(part.menu, SELECTION_OPEN_SUBMENU); + } else if (!part.is_scroll() && pending_state_.item && + pending_state_.item->GetParentMenuItem() && + (!pending_state_.item->HasSubmenu() || + !pending_state_.item->GetSubmenu()->IsShowing())) { + // On exit if the user hasn't selected an item with a submenu, move the + // selection back to the parent menu item. + SetSelection(pending_state_.item->GetParentMenuItem(), + SELECTION_OPEN_SUBMENU); + } +} + +void MenuController::OnMouseEntered(SubmenuView* source, + const MouseEvent& event) { + // MouseEntered is always followed by a mouse moved, so don't need to + // do anything here. +} + +#if defined(OS_LINUX) +bool MenuController::OnMouseWheel(SubmenuView* source, + const MouseWheelEvent& event) { + MenuPart part = GetMenuPart(source, event.location()); + return part.submenu && part.submenu->OnMouseWheel(event); +} +#endif + +bool MenuController::GetDropFormats( + SubmenuView* source, + int* formats, + std::set<OSExchangeData::CustomFormat>* custom_formats) { + return source->GetMenuItem()->GetDelegate()->GetDropFormats( + source->GetMenuItem(), formats, custom_formats); +} + +bool MenuController::AreDropTypesRequired(SubmenuView* source) { + return source->GetMenuItem()->GetDelegate()->AreDropTypesRequired( + source->GetMenuItem()); +} + +bool MenuController::CanDrop(SubmenuView* source, const OSExchangeData& data) { + return source->GetMenuItem()->GetDelegate()->CanDrop(source->GetMenuItem(), + data); +} + +void MenuController::OnDragEntered(SubmenuView* source, + const DropTargetEvent& event) { + valid_drop_coordinates_ = false; +} + +int MenuController::OnDragUpdated(SubmenuView* source, + const DropTargetEvent& event) { + StopCancelAllTimer(); + + gfx::Point screen_loc(event.location()); + View::ConvertPointToScreen(source, &screen_loc); + if (valid_drop_coordinates_ && screen_loc == drop_pt_) + return last_drop_operation_; + drop_pt_ = screen_loc; + valid_drop_coordinates_ = true; + + MenuItemView* menu_item = GetMenuItemAt(source, event.x(), event.y()); + bool over_empty_menu = false; + if (!menu_item) { + // See if we're over an empty menu. + menu_item = GetEmptyMenuItemAt(source, event.x(), event.y()); + if (menu_item) + over_empty_menu = true; + } + MenuDelegate::DropPosition drop_position = MenuDelegate::DROP_NONE; + int drop_operation = ui::DragDropTypes::DRAG_NONE; + if (menu_item) { + gfx::Point menu_item_loc(event.location()); + View::ConvertPointToView(source, menu_item, &menu_item_loc); + MenuItemView* query_menu_item; + if (!over_empty_menu) { + int menu_item_height = menu_item->height(); + if (menu_item->HasSubmenu() && + (menu_item_loc.y() > kDropBetweenPixels && + menu_item_loc.y() < (menu_item_height - kDropBetweenPixels))) { + drop_position = MenuDelegate::DROP_ON; + } else { + drop_position = (menu_item_loc.y() < menu_item_height / 2) ? + MenuDelegate::DROP_BEFORE : MenuDelegate::DROP_AFTER; + } + query_menu_item = menu_item; + } else { + query_menu_item = menu_item->GetParentMenuItem(); + drop_position = MenuDelegate::DROP_ON; + } + drop_operation = menu_item->GetDelegate()->GetDropOperation( + query_menu_item, event, &drop_position); + + // If the menu has a submenu, schedule the submenu to open. + SetSelection(menu_item, menu_item->HasSubmenu() ? SELECTION_OPEN_SUBMENU : + SELECTION_DEFAULT); + + if (drop_position == MenuDelegate::DROP_NONE || + drop_operation == ui::DragDropTypes::DRAG_NONE) + menu_item = NULL; + } else { + SetSelection(source->GetMenuItem(), SELECTION_OPEN_SUBMENU); + } + SetDropMenuItem(menu_item, drop_position); + last_drop_operation_ = drop_operation; + return drop_operation; +} + +void MenuController::OnDragExited(SubmenuView* source) { + StartCancelAllTimer(); + + if (drop_target_) { + StopShowTimer(); + SetDropMenuItem(NULL, MenuDelegate::DROP_NONE); + } +} + +int MenuController::OnPerformDrop(SubmenuView* source, + const DropTargetEvent& event) { + DCHECK(drop_target_); + // NOTE: the delegate may delete us after invoking OnPerformDrop, as such + // we don't call cancel here. + + MenuItemView* item = state_.item; + DCHECK(item); + + MenuItemView* drop_target = drop_target_; + MenuDelegate::DropPosition drop_position = drop_position_; + + // Close all menus, including any nested menus. + SetSelection(NULL, SELECTION_UPDATE_IMMEDIATELY | SELECTION_EXIT); + CloseAllNestedMenus(); + + // Set state such that we exit. + showing_ = false; + SetExitType(EXIT_ALL); + + // If over an empty menu item, drop occurs on the parent. + if (drop_target->id() == MenuItemView::kEmptyMenuItemViewID) + drop_target = drop_target->GetParentMenuItem(); + + if (!IsBlockingRun()) { + delegate_->DropMenuClosed( + internal::MenuControllerDelegate::DONT_NOTIFY_DELEGATE, + item->GetRootMenuItem()); + } + + // WARNING: the call to MenuClosed deletes us. + + return drop_target->GetDelegate()->OnPerformDrop( + drop_target, drop_position, event); +} + +void MenuController::OnDragEnteredScrollButton(SubmenuView* source, + bool is_up) { + MenuPart part; + part.type = is_up ? MenuPart::SCROLL_UP : MenuPart::SCROLL_DOWN; + part.submenu = source; + UpdateScrolling(part); + + // Do this to force the selection to hide. + SetDropMenuItem(source->GetMenuItemAt(0), MenuDelegate::DROP_NONE); + + StopCancelAllTimer(); +} + +void MenuController::OnDragExitedScrollButton(SubmenuView* source) { + StartCancelAllTimer(); + SetDropMenuItem(NULL, MenuDelegate::DROP_NONE); + StopScrolling(); +} + +void MenuController::OnWidgetActivationChanged() { + if (!drag_in_progress_) + Cancel(EXIT_ALL); +} + +void MenuController::SetSelection(MenuItemView* menu_item, + int selection_types) { + size_t paths_differ_at = 0; + std::vector<MenuItemView*> current_path; + std::vector<MenuItemView*> new_path; + BuildPathsAndCalculateDiff(pending_state_.item, menu_item, ¤t_path, + &new_path, &paths_differ_at); + + size_t current_size = current_path.size(); + size_t new_size = new_path.size(); + + if (pending_state_.item != menu_item && pending_state_.item) { + View* current_hot_view = GetFirstHotTrackedView(pending_state_.item); + if (current_hot_view) + current_hot_view->SetHotTracked(false); + } + + // Notify the old path it isn't selected. + MenuDelegate* current_delegate = + current_path.empty() ? NULL : current_path.front()->GetDelegate(); + for (size_t i = paths_differ_at; i < current_size; ++i) { + if (current_delegate && + current_path[i]->GetType() == MenuItemView::SUBMENU) { + current_delegate->WillHideMenu(current_path[i]); + } + current_path[i]->SetSelected(false); + } + + // Notify the new path it is selected. + for (size_t i = paths_differ_at; i < new_size; ++i) + new_path[i]->SetSelected(true); + + if (menu_item && menu_item->GetDelegate()) + menu_item->GetDelegate()->SelectionChanged(menu_item); + + // TODO(sky): convert back to DCHECK when figure out 93471. + CHECK(menu_item || (selection_types & SELECTION_EXIT) != 0); + + pending_state_.item = menu_item; + pending_state_.submenu_open = (selection_types & SELECTION_OPEN_SUBMENU) != 0; + + // Stop timers. + StopShowTimer(); + StopCancelAllTimer(); + + if (selection_types & SELECTION_UPDATE_IMMEDIATELY) + CommitPendingSelection(); + else + StartShowTimer(); + + // Notify an accessibility focus event on all menu items except for the root. + if (menu_item && + (MenuDepth(menu_item) != 1 || + menu_item->GetType() != MenuItemView::SUBMENU)) { + menu_item->GetWidget()->NotifyAccessibilityEvent( + menu_item, ui::AccessibilityTypes::EVENT_FOCUS, true); + } +} + +#if defined(OS_WIN) +bool MenuController::Dispatch(const MSG& msg) { + DCHECK(blocking_run_); + + if (exit_type_ == EXIT_ALL || exit_type_ == EXIT_DESTROYED) { + // We must translate/dispatch the message here, otherwise we would drop + // the message on the floor. + TranslateMessage(&msg); + DispatchMessage(&msg); + return false; + } + + // NOTE: we don't get WM_ACTIVATE or anything else interesting in here. + switch (msg.message) { + case WM_CONTEXTMENU: { + MenuItemView* item = pending_state_.item; + if (item && item->GetRootMenuItem() != item) { + gfx::Point screen_loc(0, item->height()); + View::ConvertPointToScreen(item, &screen_loc); + item->GetDelegate()->ShowContextMenu(item, item->GetCommand(), + screen_loc, false); + } + return true; + } + + // NOTE: focus wasn't changed when the menu was shown. As such, don't + // dispatch key events otherwise the focused window will get the events. + case WM_KEYDOWN: { + bool result = OnKeyDown(ui::KeyboardCodeFromNative(msg)); + TranslateMessage(&msg); + return result; + } + case WM_CHAR: + return !SelectByChar(static_cast<char16>(msg.wParam)); + + case WM_KEYUP: + return true; + + case WM_SYSKEYUP: + // We may have been shown on a system key, as such don't do anything + // here. If another system key is pushed we'll get a WM_SYSKEYDOWN and + // close the menu. + return true; + + case WM_CANCELMODE: + case WM_SYSKEYDOWN: + // Exit immediately on system keys. + Cancel(EXIT_ALL); + return false; + + default: + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + return exit_type_ == EXIT_NONE; +} +#elif defined(USE_WAYLAND) +base::MessagePumpDispatcher::DispatchStatus + MenuController::Dispatch(base::wayland::WaylandEvent* ev) { + return exit_type_ != EXIT_NONE ? + base::MessagePumpDispatcher::EVENT_QUIT : + base::MessagePumpDispatcher::EVENT_PROCESSED; +} + +#elif defined(USE_AURA) +base::MessagePumpDispatcher::DispatchStatus + MenuController::Dispatch(XEvent* xev) { + if (exit_type_ == EXIT_ALL || exit_type_ == EXIT_DESTROYED) { + aura::Desktop::GetInstance()->GetDispatcher()->Dispatch(xev); + return base::MessagePumpDispatcher::EVENT_QUIT; + } + switch (ui::EventTypeFromNative(xev)) { + case ui::ET_KEY_PRESSED: + OnKeyDown(ui::KeyboardCodeFromNative(xev)); + // OnKeyDown may have set exit_type_. + if (exit_type_ != EXIT_NONE) + return base::MessagePumpDispatcher::EVENT_QUIT; + + // TODO(oshima): support SelectChar + break; + case ui::ET_KEY_RELEASED: + return base::MessagePumpDispatcher::EVENT_PROCESSED; + default: + break; + } + + // TODO(oshima): Update Windows' Dispatcher to return DispatchStatus + // instead of bool. + if (aura::Desktop::GetInstance()->GetDispatcher()->Dispatch(xev) == + base::MessagePumpDispatcher::EVENT_IGNORED) + return EVENT_IGNORED; + return exit_type_ != EXIT_NONE ? + base::MessagePumpDispatcher::EVENT_QUIT : + base::MessagePumpDispatcher::EVENT_PROCESSED; +} +#else +bool MenuController::Dispatch(GdkEvent* event) { + if (exit_type_ == EXIT_ALL || exit_type_ == EXIT_DESTROYED) { + gtk_main_do_event(event); + return false; + } + + switch (event->type) { + case GDK_KEY_PRESS: { + if (!OnKeyDown(ui::WindowsKeyCodeForGdkKeyCode(event->key.keyval))) + return false; + + // OnKeyDown may have set exit_type_. + if (exit_type_ != EXIT_NONE) + return false; + + guint32 keycode = gdk_keyval_to_unicode(event->key.keyval); + if (keycode) + return !SelectByChar(keycode); + return true; + } + + case GDK_KEY_RELEASE: + return true; + + default: + break; + } + + // We don't want Gtk to handle keyboard events, otherwise if they get + // handled by Gtk, unexpected behavior may occur. For example Tab key + // may cause unexpected focus traversing. + gtk_main_do_event(event); + return exit_type_ == EXIT_NONE; +} +#endif + +bool MenuController::OnKeyDown(ui::KeyboardCode key_code) { + DCHECK(blocking_run_); + + switch (key_code) { + case ui::VKEY_UP: + IncrementSelection(-1); + break; + + case ui::VKEY_DOWN: + IncrementSelection(1); + break; + + // Handling of VK_RIGHT and VK_LEFT is different depending on the UI + // layout. + case ui::VKEY_RIGHT: + if (base::i18n::IsRTL()) + CloseSubmenu(); + else + OpenSubmenuChangeSelectionIfCan(); + break; + + case ui::VKEY_LEFT: + if (base::i18n::IsRTL()) + OpenSubmenuChangeSelectionIfCan(); + else + CloseSubmenu(); + break; + + case ui::VKEY_SPACE: + SendAcceleratorToHotTrackedView(); + break; + + case ui::VKEY_RETURN: + if (pending_state_.item) { + if (pending_state_.item->HasSubmenu()) { + OpenSubmenuChangeSelectionIfCan(); + } else if (!SendAcceleratorToHotTrackedView() && + pending_state_.item->IsEnabled()) { + Accept(pending_state_.item, 0); + return false; + } + } + break; + + case ui::VKEY_ESCAPE: + if (!state_.item->GetParentMenuItem() || + (!state_.item->GetParentMenuItem()->GetParentMenuItem() && + (!state_.item->HasSubmenu() || + !state_.item->GetSubmenu()->IsShowing()))) { + // User pressed escape and only one menu is shown, cancel it. + Cancel(EXIT_OUTERMOST); + return false; + } + CloseSubmenu(); + break; + +#if defined(OS_WIN) + case VK_APPS: + break; +#endif + + default: + break; + } + return true; +} + +MenuController::MenuController(bool blocking, + internal::MenuControllerDelegate* delegate) + : blocking_run_(blocking), + showing_(false), + exit_type_(EXIT_NONE), + did_capture_(false), + result_(NULL), + result_mouse_event_flags_(0), + drop_target_(NULL), + drop_position_(MenuDelegate::DROP_UNKNOWN), + owner_(NULL), + possible_drag_(false), + drag_in_progress_(false), + valid_drop_coordinates_(false), + last_drop_operation_(MenuDelegate::DROP_UNKNOWN), + showing_submenu_(false), + menu_button_(NULL), + active_mouse_view_(NULL), + delegate_(delegate) { + active_instance_ = this; +} + +MenuController::~MenuController() { + DCHECK(!showing_); + if (active_instance_ == this) + active_instance_ = NULL; + StopShowTimer(); + StopCancelAllTimer(); +} + +bool MenuController::SendAcceleratorToHotTrackedView() { + View* hot_view = GetFirstHotTrackedView(pending_state_.item); + if (!hot_view) + return false; + + ui::Accelerator accelerator(ui::VKEY_RETURN, false, false, false); + hot_view->AcceleratorPressed(accelerator); + hot_view->SetHotTracked(true); + return true; +} + +void MenuController::UpdateInitialLocation( + const gfx::Rect& bounds, + MenuItemView::AnchorPosition position) { + pending_state_.initial_bounds = bounds; + if (bounds.height() > 1) { + // Inset the bounds slightly, otherwise drag coordinates don't line up + // nicely and menus close prematurely. + pending_state_.initial_bounds.Inset(0, 1); + } + + // Reverse anchor position for RTL languages. + if (base::i18n::IsRTL()) { + pending_state_.anchor = position == MenuItemView::TOPRIGHT ? + MenuItemView::TOPLEFT : MenuItemView::TOPRIGHT; + } else { + pending_state_.anchor = position; + } + + // Calculate the bounds of the monitor we'll show menus on. Do this once to + // avoid repeated system queries for the info. + pending_state_.monitor_bounds = gfx::Screen::GetMonitorWorkAreaNearestPoint( + bounds.origin()); +} + +void MenuController::Accept(MenuItemView* item, int mouse_event_flags) { + DCHECK(IsBlockingRun()); + result_ = item; + if (item && !menu_stack_.empty() && + !item->GetDelegate()->ShouldCloseAllMenusOnExecute(item->GetCommand())) { + SetExitType(EXIT_OUTERMOST); + } else { + SetExitType(EXIT_ALL); + } + result_mouse_event_flags_ = mouse_event_flags; +} + +bool MenuController::ShowSiblingMenu(SubmenuView* source, + const MouseEvent& event) { + if (!menu_stack_.empty() || !menu_button_) + return false; + + View* source_view = source->GetScrollViewContainer(); + if (event.x() >= 0 && event.x() < source_view->width() && event.y() >= 0 && + event.y() < source_view->height()) { + // The mouse is over the menu, no need to continue. + return false; + } + + gfx::NativeWindow window_under_mouse = + gfx::Screen::GetWindowAtCursorScreenPoint(); + // TODO(oshima): Replace with views only API. + if (window_under_mouse != owner_->GetNativeWindow()) + return false; + + // The user moved the mouse outside the menu and over the owning window. See + // if there is a sibling menu we should show. + gfx::Point screen_point(event.location()); + View::ConvertPointToScreen(source_view, &screen_point); + MenuItemView::AnchorPosition anchor; + bool has_mnemonics; + MenuButton* button = NULL; + MenuItemView* alt_menu = source->GetMenuItem()->GetDelegate()-> + GetSiblingMenu(source->GetMenuItem()->GetRootMenuItem(), + screen_point, &anchor, &has_mnemonics, &button); + if (!alt_menu || (state_.item && state_.item->GetRootMenuItem() == alt_menu)) + return false; + + delegate_->SiblingMenuCreated(alt_menu); + + if (!button) { + // If the delegate returns a menu, they must also return a button. + NOTREACHED(); + return false; + } + + // There is a sibling menu, update the button state, hide the current menu + // and show the new one. + menu_button_->SetState(CustomButton::BS_NORMAL); + menu_button_->SchedulePaint(); + menu_button_ = button; + menu_button_->SetState(CustomButton::BS_PUSHED); + menu_button_->SchedulePaint(); + + // Need to reset capture when we show the menu again, otherwise we aren't + // going to get any events. + did_capture_ = false; + gfx::Point screen_menu_loc; + View::ConvertPointToScreen(button, &screen_menu_loc); + // Subtract 1 from the height to make the popup flush with the button border. + UpdateInitialLocation(gfx::Rect(screen_menu_loc.x(), screen_menu_loc.y(), + button->width(), button->height() - 1), + anchor); + alt_menu->PrepareForRun( + has_mnemonics, + source->GetMenuItem()->GetRootMenuItem()->show_mnemonics_); + alt_menu->controller_ = this; + SetSelection(alt_menu, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); + return true; +} + +void MenuController::CloseAllNestedMenus() { + for (std::list<State>::iterator i = menu_stack_.begin(); + i != menu_stack_.end(); ++i) { + MenuItemView* last_item = i->item; + for (MenuItemView* item = last_item; item; + item = item->GetParentMenuItem()) { + CloseMenu(item); + last_item = item; + } + i->submenu_open = false; + i->item = last_item; + } +} + +MenuItemView* MenuController::GetMenuItemAt(View* source, int x, int y) { + // Walk the view hierarchy until we find a menu item (or the root). + View* child_under_mouse = source->GetEventHandlerForPoint(gfx::Point(x, y)); + while (child_under_mouse && + child_under_mouse->id() != MenuItemView::kMenuItemViewID) { + child_under_mouse = child_under_mouse->parent(); + } + if (child_under_mouse && child_under_mouse->IsEnabled() && + child_under_mouse->id() == MenuItemView::kMenuItemViewID) { + return static_cast<MenuItemView*>(child_under_mouse); + } + return NULL; +} + +MenuItemView* MenuController::GetEmptyMenuItemAt(View* source, int x, int y) { + View* child_under_mouse = source->GetEventHandlerForPoint(gfx::Point(x, y)); + if (child_under_mouse && + child_under_mouse->id() == MenuItemView::kEmptyMenuItemViewID) { + return static_cast<MenuItemView*>(child_under_mouse); + } + return NULL; +} + +bool MenuController::IsScrollButtonAt(SubmenuView* source, + int x, + int y, + MenuPart::Type* part) { + MenuScrollViewContainer* scroll_view = source->GetScrollViewContainer(); + View* child_under_mouse = + scroll_view->GetEventHandlerForPoint(gfx::Point(x, y)); + if (child_under_mouse && child_under_mouse->IsEnabled()) { + if (child_under_mouse == scroll_view->scroll_up_button()) { + *part = MenuPart::SCROLL_UP; + return true; + } + if (child_under_mouse == scroll_view->scroll_down_button()) { + *part = MenuPart::SCROLL_DOWN; + return true; + } + } + return false; +} + +MenuController::MenuPart MenuController::GetMenuPart( + SubmenuView* source, + const gfx::Point& source_loc) { + gfx::Point screen_loc(source_loc); + View::ConvertPointToScreen(source->GetScrollViewContainer(), &screen_loc); + return GetMenuPartByScreenCoordinateUsingMenu(state_.item, screen_loc); +} + +MenuController::MenuPart MenuController::GetMenuPartByScreenCoordinateUsingMenu( + MenuItemView* item, + const gfx::Point& screen_loc) { + MenuPart part; + for (; item; item = item->GetParentMenuItem()) { + if (item->HasSubmenu() && item->GetSubmenu()->IsShowing() && + GetMenuPartByScreenCoordinateImpl(item->GetSubmenu(), screen_loc, + &part)) { + return part; + } + } + return part; +} + +bool MenuController::GetMenuPartByScreenCoordinateImpl( + SubmenuView* menu, + const gfx::Point& screen_loc, + MenuPart* part) { + // Is the mouse over the scroll buttons? + gfx::Point scroll_view_loc = screen_loc; + View* scroll_view_container = menu->GetScrollViewContainer(); + View::ConvertPointToView(NULL, scroll_view_container, &scroll_view_loc); + if (scroll_view_loc.x() < 0 || + scroll_view_loc.x() >= scroll_view_container->width() || + scroll_view_loc.y() < 0 || + scroll_view_loc.y() >= scroll_view_container->height()) { + // Point isn't contained in menu. + return false; + } + if (IsScrollButtonAt(menu, scroll_view_loc.x(), scroll_view_loc.y(), + &(part->type))) { + part->submenu = menu; + return true; + } + + // Not over the scroll button. Check the actual menu. + if (DoesSubmenuContainLocation(menu, screen_loc)) { + gfx::Point menu_loc = screen_loc; + View::ConvertPointToView(NULL, menu, &menu_loc); + part->menu = GetMenuItemAt(menu, menu_loc.x(), menu_loc.y()); + part->type = MenuPart::MENU_ITEM; + part->submenu = menu; + if (!part->menu) + part->parent = menu->GetMenuItem(); + return true; + } + + // While the mouse isn't over a menu item or the scroll buttons of menu, it + // is contained by menu and so we return true. If we didn't return true other + // menus would be searched, even though they are likely obscured by us. + return true; +} + +bool MenuController::DoesSubmenuContainLocation(SubmenuView* submenu, + const gfx::Point& screen_loc) { + gfx::Point view_loc = screen_loc; + View::ConvertPointToView(NULL, submenu, &view_loc); + gfx::Rect vis_rect = submenu->GetVisibleBounds(); + return vis_rect.Contains(view_loc.x(), view_loc.y()); +} + +void MenuController::CommitPendingSelection() { + StopShowTimer(); + + size_t paths_differ_at = 0; + std::vector<MenuItemView*> current_path; + std::vector<MenuItemView*> new_path; + BuildPathsAndCalculateDiff(state_.item, pending_state_.item, ¤t_path, + &new_path, &paths_differ_at); + + // Hide the old menu. + for (size_t i = paths_differ_at; i < current_path.size(); ++i) { + if (current_path[i]->HasSubmenu()) { + current_path[i]->GetSubmenu()->Hide(); + } + } + + // Copy pending to state_, making sure to preserve the direction menus were + // opened. + std::list<bool> pending_open_direction; + state_.open_leading.swap(pending_open_direction); + state_ = pending_state_; + state_.open_leading.swap(pending_open_direction); + + int menu_depth = MenuDepth(state_.item); + if (menu_depth == 0) { + state_.open_leading.clear(); + } else { + int cached_size = static_cast<int>(state_.open_leading.size()); + DCHECK_GE(menu_depth, 0); + while (cached_size-- >= menu_depth) + state_.open_leading.pop_back(); + } + + if (!state_.item) { + // Nothing to select. + StopScrolling(); + return; + } + + // Open all the submenus preceeding the last menu item (last menu item is + // handled next). + if (new_path.size() > 1) { + for (std::vector<MenuItemView*>::iterator i = new_path.begin(); + i != new_path.end() - 1; ++i) { + OpenMenu(*i); + } + } + + if (state_.submenu_open) { + // The submenu should be open, open the submenu if the item has a submenu. + if (state_.item->HasSubmenu()) { + OpenMenu(state_.item); + } else { + state_.submenu_open = false; + } + } else if (state_.item->HasSubmenu() && + state_.item->GetSubmenu()->IsShowing()) { + state_.item->GetSubmenu()->Hide(); + } + + if (scroll_task_.get() && scroll_task_->submenu()) { + // Stop the scrolling if none of the elements of the selection contain + // the menu being scrolled. + bool found = false; + for (MenuItemView* item = state_.item; item && !found; + item = item->GetParentMenuItem()) { + found = (item->HasSubmenu() && item->GetSubmenu()->IsShowing() && + item->GetSubmenu() == scroll_task_->submenu()); + } + if (!found) + StopScrolling(); + } +} + +void MenuController::CloseMenu(MenuItemView* item) { + DCHECK(item); + if (!item->HasSubmenu()) + return; + item->GetSubmenu()->Hide(); +} + +void MenuController::OpenMenu(MenuItemView* item) { + DCHECK(item); + if (item->GetSubmenu()->IsShowing()) { + return; + } + + OpenMenuImpl(item, true); + did_capture_ = true; +} + +void MenuController::OpenMenuImpl(MenuItemView* item, bool show) { + if (show) { + int old_count = item->GetSubmenu()->child_count(); + item->GetDelegate()->WillShowMenu(item); + if (old_count != item->GetSubmenu()->child_count()) { + // If the number of children changed then we may need to add empty items. + item->AddEmptyMenus(); + } + } + bool prefer_leading = + state_.open_leading.empty() ? true : state_.open_leading.back(); + bool resulting_direction; + gfx::Rect bounds = + CalculateMenuBounds(item, prefer_leading, &resulting_direction); + state_.open_leading.push_back(resulting_direction); + bool do_capture = (!did_capture_ && blocking_run_); + showing_submenu_ = true; + if (show) + item->GetSubmenu()->ShowAt(owner_, bounds, do_capture); + else + item->GetSubmenu()->Reposition(bounds); + showing_submenu_ = false; +} + +void MenuController::MenuChildrenChanged(MenuItemView* item) { + DCHECK(item); + + // If the current item or pending item is a descendant of the item + // that changed, move the selection back to the changed item. + const MenuItemView* ancestor = state_.item; + while (ancestor && ancestor != item) + ancestor = ancestor->GetParentMenuItem(); + if (!ancestor) { + ancestor = pending_state_.item; + while (ancestor && ancestor != item) + ancestor = ancestor->GetParentMenuItem(); + if (!ancestor) + return; + } + SetSelection(item, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); + if (item->HasSubmenu()) + OpenMenuImpl(item, false); +} + +void MenuController::BuildPathsAndCalculateDiff( + MenuItemView* old_item, + MenuItemView* new_item, + std::vector<MenuItemView*>* old_path, + std::vector<MenuItemView*>* new_path, + size_t* first_diff_at) { + DCHECK(old_path && new_path && first_diff_at); + BuildMenuItemPath(old_item, old_path); + BuildMenuItemPath(new_item, new_path); + + size_t common_size = std::min(old_path->size(), new_path->size()); + + // Find the first difference between the two paths, when the loop + // returns, diff_i is the first index where the two paths differ. + for (size_t i = 0; i < common_size; ++i) { + if ((*old_path)[i] != (*new_path)[i]) { + *first_diff_at = i; + return; + } + } + + *first_diff_at = common_size; +} + +void MenuController::BuildMenuItemPath(MenuItemView* item, + std::vector<MenuItemView*>* path) { + if (!item) + return; + BuildMenuItemPath(item->GetParentMenuItem(), path); + path->push_back(item); +} + +void MenuController::StartShowTimer() { + show_timer_.Start(FROM_HERE, TimeDelta::FromMilliseconds(kShowDelay), this, + &MenuController::CommitPendingSelection); +} + +void MenuController::StopShowTimer() { + show_timer_.Stop(); +} + +void MenuController::StartCancelAllTimer() { + cancel_all_timer_.Start(FROM_HERE, + TimeDelta::FromMilliseconds(kCloseOnExitTime), + this, &MenuController::CancelAll); +} + +void MenuController::StopCancelAllTimer() { + cancel_all_timer_.Stop(); +} + +gfx::Rect MenuController::CalculateMenuBounds(MenuItemView* item, + bool prefer_leading, + bool* is_leading) { + DCHECK(item); + + SubmenuView* submenu = item->GetSubmenu(); + DCHECK(submenu); + + gfx::Size pref = submenu->GetScrollViewContainer()->GetPreferredSize(); + + // Don't let the menu go to wide. + pref.set_width(std::min(pref.width(), + item->GetDelegate()->GetMaxWidthForMenu(item))); + if (!state_.monitor_bounds.IsEmpty()) + pref.set_width(std::min(pref.width(), state_.monitor_bounds.width())); + + // Assume we can honor prefer_leading. + *is_leading = prefer_leading; + + int x, y; + + if (!item->GetParentMenuItem()) { + // First item, position relative to initial location. + x = state_.initial_bounds.x(); + y = state_.initial_bounds.bottom(); + if (state_.anchor == MenuItemView::TOPRIGHT) + x = x + state_.initial_bounds.width() - pref.width(); + if (!state_.monitor_bounds.IsEmpty() && + y + pref.height() > state_.monitor_bounds.bottom()) { + // The menu doesn't fit on screen. The menu position with + // respect to the bounds will be preserved if it has already + // been drawn. On the first drawing if the first location is + // above the half way point then show from the mouse location to + // bottom of screen, otherwise show from the top of the screen + // to the location of the mouse. While odd, this behavior + // matches IE. + if (item->actual_menu_position() == MenuItemView::POSITION_BELOW_BOUNDS || + (item->actual_menu_position() == MenuItemView::POSITION_BEST_FIT && + y < (state_.monitor_bounds.y() + + state_.monitor_bounds.height() / 2))) { + pref.set_height(std::min(pref.height(), + state_.monitor_bounds.bottom() - y)); + item->set_actual_menu_position(MenuItemView::POSITION_BELOW_BOUNDS); + } else { + pref.set_height(std::min(pref.height(), + state_.initial_bounds.y() - state_.monitor_bounds.y())); + y = state_.initial_bounds.y() - pref.height(); + item->set_actual_menu_position(MenuItemView::POSITION_ABOVE_BOUNDS); + } + } else if (item->actual_menu_position() == + MenuItemView::POSITION_ABOVE_BOUNDS) { + // The menu would fit below the bounds, but it has already been + // drawn above so keep it there. + pref.set_height(std::min(pref.height(), + state_.initial_bounds.y() - state_.monitor_bounds.y())); + y = state_.initial_bounds.y() - pref.height(); + item->set_actual_menu_position(MenuItemView::POSITION_ABOVE_BOUNDS); + } else { + item->set_actual_menu_position(MenuItemView::POSITION_BELOW_BOUNDS); + } + } else { + // Not the first menu; position it relative to the bounds of the menu + // item. + gfx::Point item_loc; + View::ConvertPointToScreen(item, &item_loc); + + // We must make sure we take into account the UI layout. If the layout is + // RTL, then a 'leading' menu is positioned to the left of the parent menu + // item and not to the right. + bool layout_is_rtl = base::i18n::IsRTL(); + bool create_on_the_right = (prefer_leading && !layout_is_rtl) || + (!prefer_leading && layout_is_rtl); + + if (create_on_the_right) { + x = item_loc.x() + item->width() - kSubmenuHorizontalInset; + if (state_.monitor_bounds.width() != 0 && + x + pref.width() > state_.monitor_bounds.right()) { + if (layout_is_rtl) + *is_leading = true; + else + *is_leading = false; + x = item_loc.x() - pref.width() + kSubmenuHorizontalInset; + } + } else { + x = item_loc.x() - pref.width() + kSubmenuHorizontalInset; + if (state_.monitor_bounds.width() != 0 && x < state_.monitor_bounds.x()) { + if (layout_is_rtl) + *is_leading = false; + else + *is_leading = true; + x = item_loc.x() + item->width() - kSubmenuHorizontalInset; + } + } + y = item_loc.y() - SubmenuView::kSubmenuBorderSize; + if (state_.monitor_bounds.width() != 0) { + pref.set_height(std::min(pref.height(), state_.monitor_bounds.height())); + if (y + pref.height() > state_.monitor_bounds.bottom()) + y = state_.monitor_bounds.bottom() - pref.height(); + if (y < state_.monitor_bounds.y()) + y = state_.monitor_bounds.y(); + } + } + + if (state_.monitor_bounds.width() != 0) { + if (x + pref.width() > state_.monitor_bounds.right()) + x = state_.monitor_bounds.right() - pref.width(); + if (x < state_.monitor_bounds.x()) + x = state_.monitor_bounds.x(); + } + return gfx::Rect(x, y, pref.width(), pref.height()); +} + +// static +int MenuController::MenuDepth(MenuItemView* item) { + return item ? (MenuDepth(item->GetParentMenuItem()) + 1) : 0; +} + +void MenuController::IncrementSelection(int delta) { + MenuItemView* item = pending_state_.item; + DCHECK(item); + if (pending_state_.submenu_open && item->HasSubmenu() && + item->GetSubmenu()->IsShowing()) { + // A menu is selected and open, but none of its children are selected, + // select the first menu item. + if (item->GetSubmenu()->GetMenuItemCount()) { + SetSelection(item->GetSubmenu()->GetMenuItemAt(0), SELECTION_DEFAULT); + ScrollToVisible(item->GetSubmenu()->GetMenuItemAt(0)); + return; + } + } + + if (item->has_children()) { + View* hot_view = GetFirstHotTrackedView(item); + if (hot_view) { + hot_view->SetHotTracked(false); + View* to_make_hot = GetNextFocusableView(item, hot_view, delta == 1); + if (to_make_hot) { + to_make_hot->SetHotTracked(true); + return; + } + } else { + View* to_make_hot = GetInitialFocusableView(item, delta == 1); + if (to_make_hot) { + to_make_hot->SetHotTracked(true); + return; + } + } + } + + MenuItemView* parent = item->GetParentMenuItem(); + if (parent) { + int parent_count = parent->GetSubmenu()->GetMenuItemCount(); + if (parent_count > 1) { + for (int i = 0; i < parent_count; ++i) { + if (parent->GetSubmenu()->GetMenuItemAt(i) == item) { + MenuItemView* to_select = + FindNextSelectableMenuItem(parent, i, delta); + if (!to_select) + break; + ScrollToVisible(to_select); + SetSelection(to_select, SELECTION_DEFAULT); + View* to_make_hot = GetInitialFocusableView(to_select, delta == 1); + if (to_make_hot) + to_make_hot->SetHotTracked(true); + break; + } + } + } + } +} + +MenuItemView* MenuController::FindNextSelectableMenuItem(MenuItemView* parent, + int index, + int delta) { + int start_index = index; + int parent_count = parent->GetSubmenu()->GetMenuItemCount(); + // Loop through the menu items skipping any invisible menus. The loop stops + // when we wrap or find a visible child. + do { + index = (index + delta + parent_count) % parent_count; + if (index == start_index) + return NULL; + MenuItemView* child = parent->GetSubmenu()->GetMenuItemAt(index); + if (child->IsVisible()) + return child; + } while (index != start_index); + return NULL; +} + +void MenuController::OpenSubmenuChangeSelectionIfCan() { + MenuItemView* item = pending_state_.item; + if (item->HasSubmenu() && item->IsEnabled()) { + if (item->GetSubmenu()->GetMenuItemCount() > 0) { + SetSelection(item->GetSubmenu()->GetMenuItemAt(0), + SELECTION_UPDATE_IMMEDIATELY); + } else { + // No menu items, just show the sub-menu. + SetSelection(item, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); + } + } +} + +void MenuController::CloseSubmenu() { + MenuItemView* item = state_.item; + DCHECK(item); + if (!item->GetParentMenuItem()) + return; + if (item->HasSubmenu() && item->GetSubmenu()->IsShowing()) + SetSelection(item, SELECTION_UPDATE_IMMEDIATELY); + else if (item->GetParentMenuItem()->GetParentMenuItem()) + SetSelection(item->GetParentMenuItem(), SELECTION_UPDATE_IMMEDIATELY); +} + +MenuController::SelectByCharDetails MenuController::FindChildForMnemonic( + MenuItemView* parent, + char16 key, + bool (*match_function)(MenuItemView* menu, char16 mnemonic)) { + SubmenuView* submenu = parent->GetSubmenu(); + DCHECK(submenu); + SelectByCharDetails details; + + for (int i = 0, menu_item_count = submenu->GetMenuItemCount(); + i < menu_item_count; ++i) { + MenuItemView* child = submenu->GetMenuItemAt(i); + if (child->IsEnabled() && child->IsVisible()) { + if (child == pending_state_.item) + details.index_of_item = i; + if (match_function(child, key)) { + if (details.first_match == -1) + details.first_match = i; + else + details.has_multiple = true; + if (details.next_match == -1 && details.index_of_item != -1 && + i > details.index_of_item) + details.next_match = i; + } + } + } + return details; +} + +bool MenuController::AcceptOrSelect(MenuItemView* parent, + const SelectByCharDetails& details) { + // This should only be invoked if there is a match. + DCHECK(details.first_match != -1); + DCHECK(parent->HasSubmenu()); + SubmenuView* submenu = parent->GetSubmenu(); + DCHECK(submenu); + if (!details.has_multiple) { + // There's only one match, activate it (or open if it has a submenu). + if (submenu->GetMenuItemAt(details.first_match)->HasSubmenu()) { + SetSelection(submenu->GetMenuItemAt(details.first_match), + SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); + } else { + Accept(submenu->GetMenuItemAt(details.first_match), 0); + return true; + } + } else if (details.index_of_item == -1 || details.next_match == -1) { + SetSelection(submenu->GetMenuItemAt(details.first_match), + SELECTION_DEFAULT); + } else { + SetSelection(submenu->GetMenuItemAt(details.next_match), + SELECTION_DEFAULT); + } + return false; +} + +bool MenuController::SelectByChar(char16 character) { + char16 char_array[] = { character, 0 }; + char16 key = base::i18n::ToLower(char_array)[0]; + MenuItemView* item = pending_state_.item; + if (!item->HasSubmenu() || !item->GetSubmenu()->IsShowing()) + item = item->GetParentMenuItem(); + DCHECK(item); + DCHECK(item->HasSubmenu()); + DCHECK(item->GetSubmenu()); + if (item->GetSubmenu()->GetMenuItemCount() == 0) + return false; + + // Look for matches based on mnemonic first. + SelectByCharDetails details = + FindChildForMnemonic(item, key, &MatchesMnemonic); + if (details.first_match != -1) + return AcceptOrSelect(item, details); + + if (item->GetRootMenuItem()->has_mnemonics()) { + // Don't guess at mnemonics if the menu explicitly has them. + return false; + } + + // If no mnemonics found, look at first character of titles. + details = FindChildForMnemonic(item, key, &TitleMatchesMnemonic); + if (details.first_match != -1) + return AcceptOrSelect(item, details); + + return false; +} + +#if defined(OS_WIN) && !defined(USE_AURA) +void MenuController::RepostEvent(SubmenuView* source, + const MouseEvent& event) { + if (!state_.item) { + // We some times get an event after closing all the menus. Ignore it. + // Make sure the menu is in fact not visible. If the menu is visible, then + // we're in a bad state where we think the menu isn't visibile but it is. + DCHECK(!source->GetWidget()->IsVisible()); + return; + } + + gfx::Point screen_loc(event.location()); + View::ConvertPointToScreen(source->GetScrollViewContainer(), &screen_loc); + HWND window = WindowFromPoint(screen_loc.ToPOINT()); + if (window) { + // Release the capture. + SubmenuView* submenu = state_.item->GetRootMenuItem()->GetSubmenu(); + submenu->ReleaseCapture(); + + if (submenu->GetWidget()->GetNativeView() && + GetWindowThreadProcessId(submenu->GetWidget()->GetNativeView(), NULL) != + GetWindowThreadProcessId(window, NULL)) { + // Even though we have mouse capture, windows generates a mouse event + // if the other window is in a separate thread. Don't generate an event in + // this case else the target window can get double events leading to bad + // behavior. + return; + } + + // Convert the coordinates to the target window. + RECT window_bounds; + GetWindowRect(window, &window_bounds); + int window_x = screen_loc.x() - window_bounds.left; + int window_y = screen_loc.y() - window_bounds.top; + + // Determine whether the click was in the client area or not. + // NOTE: WM_NCHITTEST coordinates are relative to the screen. + LRESULT nc_hit_result = SendMessage(window, WM_NCHITTEST, 0, + MAKELPARAM(screen_loc.x(), + screen_loc.y())); + const bool in_client_area = (nc_hit_result == HTCLIENT); + + // TODO(sky): this isn't right. The event to generate should correspond + // with the event we just got. MouseEvent only tells us what is down, + // which may differ. Need to add ability to get changed button from + // MouseEvent. + int event_type; + if (event.IsLeftMouseButton()) + event_type = in_client_area ? WM_LBUTTONDOWN : WM_NCLBUTTONDOWN; + else if (event.IsMiddleMouseButton()) + event_type = in_client_area ? WM_MBUTTONDOWN : WM_NCMBUTTONDOWN; + else if (event.IsRightMouseButton()) + event_type = in_client_area ? WM_RBUTTONDOWN : WM_NCRBUTTONDOWN; + else + event_type = 0; // Unknown mouse press. + + if (event_type) { + if (in_client_area) { + PostMessage(window, event_type, event.native_event().wParam, + MAKELPARAM(window_x, window_y)); + } else { + PostMessage(window, event_type, nc_hit_result, + MAKELPARAM(screen_loc.x(), screen_loc.y())); + } + } + } +} +#endif // defined(OS_WIN) + +void MenuController::SetDropMenuItem( + MenuItemView* new_target, + MenuDelegate::DropPosition new_position) { + if (new_target == drop_target_ && new_position == drop_position_) + return; + + if (drop_target_) { + drop_target_->GetParentMenuItem()->GetSubmenu()->SetDropMenuItem( + NULL, MenuDelegate::DROP_NONE); + } + drop_target_ = new_target; + drop_position_ = new_position; + if (drop_target_) { + drop_target_->GetParentMenuItem()->GetSubmenu()->SetDropMenuItem( + drop_target_, drop_position_); + } +} + +void MenuController::UpdateScrolling(const MenuPart& part) { + if (!part.is_scroll() && !scroll_task_.get()) + return; + + if (!scroll_task_.get()) + scroll_task_.reset(new MenuScrollTask()); + scroll_task_->Update(part); +} + +void MenuController::StopScrolling() { + scroll_task_.reset(NULL); +} + +void MenuController::UpdateActiveMouseView(SubmenuView* event_source, + const MouseEvent& event, + View* target_menu) { + View* target = NULL; + gfx::Point target_menu_loc(event.location()); + if (target_menu && target_menu->has_children()) { + // Locate the deepest child view to send events to. This code assumes we + // don't have to walk up the tree to find a view interested in events. This + // is currently true for the cases we are embedding views, but if we embed + // more complex hierarchies it'll need to change. + View::ConvertPointToScreen(event_source->GetScrollViewContainer(), + &target_menu_loc); + View::ConvertPointToView(NULL, target_menu, &target_menu_loc); + target = target_menu->GetEventHandlerForPoint(target_menu_loc); + if (target == target_menu || !target->IsEnabled()) + target = NULL; + } + if (target != active_mouse_view_) { + SendMouseCaptureLostToActiveView(); + active_mouse_view_ = target; + if (active_mouse_view_) { + gfx::Point target_point(target_menu_loc); + View::ConvertPointToView(target_menu, active_mouse_view_, &target_point); + MouseEvent mouse_entered_event(ui::ET_MOUSE_ENTERED, + target_point.x(), target_point.y(), 0); + active_mouse_view_->OnMouseEntered(mouse_entered_event); + + MouseEvent mouse_pressed_event(ui::ET_MOUSE_PRESSED, + target_point.x(), target_point.y(), + event.flags()); + active_mouse_view_->OnMousePressed(mouse_pressed_event); + } + } + + if (active_mouse_view_) { + gfx::Point target_point(target_menu_loc); + View::ConvertPointToView(target_menu, active_mouse_view_, &target_point); + MouseEvent mouse_dragged_event(ui::ET_MOUSE_DRAGGED, + target_point.x(), target_point.y(), + event.flags()); + active_mouse_view_->OnMouseDragged(mouse_dragged_event); + } +} + +void MenuController::SendMouseReleaseToActiveView(SubmenuView* event_source, + const MouseEvent& event) { + if (!active_mouse_view_) + return; + + gfx::Point target_loc(event.location()); + View::ConvertPointToScreen(event_source->GetScrollViewContainer(), + &target_loc); + View::ConvertPointToView(NULL, active_mouse_view_, &target_loc); + MouseEvent release_event(ui::ET_MOUSE_RELEASED, target_loc.x(), + target_loc.y(), event.flags()); + // Reset the active_mouse_view_ before sending mouse released. That way if it + // calls back to us, we aren't in a weird state. + View* active_view = active_mouse_view_; + active_mouse_view_ = NULL; + active_view->OnMouseReleased(release_event); +} + +void MenuController::SendMouseCaptureLostToActiveView() { + if (!active_mouse_view_) + return; + + // Reset the active_mouse_view_ before sending mouse capture lost. That way if + // it calls back to us, we aren't in a weird state. + View* active_view = active_mouse_view_; + active_mouse_view_ = NULL; + active_view->OnMouseCaptureLost(); +} + +void MenuController::SetExitType(ExitType type) { + exit_type_ = type; +#if defined(USE_AURA) + // On aura, closing menu may not trigger next native event, which + // is necessary to exit from nested loop (See Dispatch methods). + // Send non-op event so that Dispatch method will always be called. + // crbug.com/104684. + if (exit_type_ == EXIT_ALL || exit_type_ == EXIT_DESTROYED) + aura::Desktop::GetInstance()->PostNativeEvent(ui::CreateNoopEvent()); +#endif +} + + +} // namespace views diff --git a/ui/views/controls/menu/menu_controller.h b/ui/views/controls/menu/menu_controller.h new file mode 100644 index 0000000..59bdc8b --- /dev/null +++ b/ui/views/controls/menu/menu_controller.h @@ -0,0 +1,513 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_CONTROLLER_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_CONTROLLER_H_ +#pragma once + +#include "build/build_config.h" + +#include <list> +#include <set> +#include <vector> + +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop.h" +#include "base/timer.h" +#include "ui/base/events.h" +#include "ui/views/controls/menu/menu_delegate.h" +#include "ui/views/controls/menu/menu_item_view.h" + +namespace ui { +class OSExchangeData; +} +using ui::OSExchangeData; + +namespace views { + +class DropTargetEvent; +class MenuButton; +class MenuHostRootView; +class MouseEvent; +class SubmenuView; +class View; + +namespace internal { +class MenuControllerDelegate; +class MenuRunnerImpl; +} + +// MenuController ------------------------------------------------------------- + +// MenuController is used internally by the various menu classes to manage +// showing, selecting and drag/drop for menus. All relevant events are +// forwarded to the MenuController from SubmenuView and MenuHost. +class VIEWS_EXPORT MenuController : public MessageLoop::Dispatcher { + public: + // Enumeration of how the menu should exit. + enum ExitType { + // Don't exit. + EXIT_NONE, + + // All menus, including nested, should be exited. + EXIT_ALL, + + // Only the outermost menu should be exited. + EXIT_OUTERMOST, + + // This is set if the menu is being closed as the result of one of the menus + // being destroyed. + EXIT_DESTROYED + }; + + // If a menu is currently active, this returns the controller for it. + static MenuController* GetActiveInstance(); + + // Runs the menu at the specified location. If the menu was configured to + // block, the selected item is returned. If the menu does not block this + // returns NULL immediately. + MenuItemView* Run(Widget* parent, + MenuButton* button, + MenuItemView* root, + const gfx::Rect& bounds, + MenuItemView::AnchorPosition position, + int* mouse_event_flags); + + // Whether or not Run blocks. + bool IsBlockingRun() const { return blocking_run_; } + + // Whether or not drag operation is in progress. + bool drag_in_progress() const { return drag_in_progress_; } + + // Cancels the current Run. See ExitType for a description of what happens + // with the various parameters. + void Cancel(ExitType type); + + // An alternative to Cancel(EXIT_ALL) that can be used with a OneShotTimer. + void CancelAll() { Cancel(EXIT_ALL); } + + // Returns the current exit type. This returns a value other than EXIT_NONE if + // the menu is being canceled. + ExitType exit_type() const { return exit_type_; } + + // Various events, forwarded from the submenu. + // + // NOTE: the coordinates of the events are in that of the + // MenuScrollViewContainer. + void OnMousePressed(SubmenuView* source, const MouseEvent& event); + void OnMouseDragged(SubmenuView* source, const MouseEvent& event); + void OnMouseReleased(SubmenuView* source, const MouseEvent& event); + void OnMouseMoved(SubmenuView* source, const MouseEvent& event); + void OnMouseEntered(SubmenuView* source, const MouseEvent& event); +#if defined(OS_LINUX) + bool OnMouseWheel(SubmenuView* source, const MouseWheelEvent& event); +#endif + + bool GetDropFormats( + SubmenuView* source, + int* formats, + std::set<OSExchangeData::CustomFormat>* custom_formats); + bool AreDropTypesRequired(SubmenuView* source); + bool CanDrop(SubmenuView* source, const OSExchangeData& data); + void OnDragEntered(SubmenuView* source, const DropTargetEvent& event); + int OnDragUpdated(SubmenuView* source, const DropTargetEvent& event); + void OnDragExited(SubmenuView* source); + int OnPerformDrop(SubmenuView* source, const DropTargetEvent& event); + + // Invoked from the scroll buttons of the MenuScrollViewContainer. + void OnDragEnteredScrollButton(SubmenuView* source, bool is_up); + void OnDragExitedScrollButton(SubmenuView* source); + + // Invoked once for any Widget activation change. This allows the menu + // to be canceled if the window manager changes the active window. + void OnWidgetActivationChanged(); + + private: + friend class internal::MenuRunnerImpl; + friend class MenuHostRootView; + friend class MenuItemView; + friend class SubmenuView; + + class MenuScrollTask; + + struct SelectByCharDetails; + + // Values supplied to SetSelection. + enum SetSelectionTypes { + SELECTION_DEFAULT = 0, + + // If set submenus are opened immediately, otherwise submenus are only + // openned after a timer fires. + SELECTION_UPDATE_IMMEDIATELY = 1 << 0, + + // If set and the menu_item has a submenu, the submenu is shown. + SELECTION_OPEN_SUBMENU = 1 << 1, + + // SetSelection is being invoked as the result exiting or cancelling the + // menu. This is used for debugging. + SELECTION_EXIT = 1 << 2, + }; + + // Tracks selection information. + struct State { + State(); + ~State(); + + // The selected menu item. + MenuItemView* item; + + // If item has a submenu this indicates if the submenu is showing. + bool submenu_open; + + // Bounds passed to the run menu. Used for positioning the first menu. + gfx::Rect initial_bounds; + + // Position of the initial menu. + MenuItemView::AnchorPosition anchor; + + // The direction child menus have opened in. + std::list<bool> open_leading; + + // Bounds for the monitor we're showing on. + gfx::Rect monitor_bounds; + }; + + // Used by GetMenuPart to indicate the menu part at a particular location. + struct MenuPart { + // Type of part. + enum Type { + NONE, + MENU_ITEM, + SCROLL_UP, + SCROLL_DOWN + }; + + MenuPart() : type(NONE), menu(NULL), parent(NULL), submenu(NULL) {} + + // Convenience for testing type == SCROLL_DOWN or type == SCROLL_UP. + bool is_scroll() const { return type == SCROLL_DOWN || type == SCROLL_UP; } + + // Type of part. + Type type; + + // If type is MENU_ITEM, this is the menu item the mouse is over, otherwise + // this is NULL. + // NOTE: if type is MENU_ITEM and the mouse is not over a valid menu item + // but is over a menu (for example, the mouse is over a separator or + // empty menu), this is NULL and parent is the menu the mouse was + // clicked on. + MenuItemView* menu; + + // If type is MENU_ITEM but the mouse is not over a menu item this is the + // parent of the menu item the user clicked on. Otherwise this is NULL. + MenuItemView* parent; + + // This is the submenu the mouse is over. + SubmenuView* submenu; + }; + + // Sets the selection to menu_item a value of NULL unselects + // everything. |types| is a bitmask of |SetSelectionTypes|. + // + // Internally this updates pending_state_ immediatley. state_ is only updated + // immediately if SELECTION_UPDATE_IMMEDIATELY is set. If + // SELECTION_UPDATE_IMMEDIATELY is not set CommitPendingSelection is invoked + // to show/hide submenus and update state_. + void SetSelection(MenuItemView* menu_item, int types); + +#if defined(OS_WIN) + // Dispatcher method. This returns true if the menu was canceled, or + // if the message is such that the menu should be closed. + virtual bool Dispatch(const MSG& msg) OVERRIDE; +#elif defined(USE_WAYLAND) + virtual base::MessagePumpDispatcher::DispatchStatus Dispatch( + base::wayland::WaylandEvent* event) OVERRIDE; +#elif defined(USE_AURA) + virtual base::MessagePumpDispatcher::DispatchStatus Dispatch( + XEvent* xevent) OVERRIDE; +#else + virtual bool Dispatch(GdkEvent* event) OVERRIDE; +#endif + + // Key processing. The return value of this is returned from Dispatch. + // In other words, if this returns false (which happens if escape was + // pressed, or a matching mnemonic was found) the message loop returns. + bool OnKeyDown(ui::KeyboardCode key_code); + + // Creates a MenuController. If |blocking| is true a nested message loop is + // started in |Run|. + MenuController(bool blocking, internal::MenuControllerDelegate* delegate); + + virtual ~MenuController(); + + // If there is a hot tracked view AcceleratorPressed is invoked on it and + // true is returned. + bool SendAcceleratorToHotTrackedView(); + + void UpdateInitialLocation(const gfx::Rect& bounds, + MenuItemView::AnchorPosition position); + + // Invoked when the user accepts the selected item. This is only used + // when blocking. This schedules the loop to quit. + void Accept(MenuItemView* item, int mouse_event_flags); + + bool ShowSiblingMenu(SubmenuView* source, const MouseEvent& event); + + // Closes all menus, including any menus of nested invocations of Run. + void CloseAllNestedMenus(); + + // Gets the enabled menu item at the specified location. + // If over_any_menu is non-null it is set to indicate whether the location + // is over any menu. It is possible for this to return NULL, but + // over_any_menu to be true. For example, the user clicked on a separator. + MenuItemView* GetMenuItemAt(View* menu, int x, int y); + + // If there is an empty menu item at the specified location, it is returned. + MenuItemView* GetEmptyMenuItemAt(View* source, int x, int y); + + // Returns true if the coordinate is over the scroll buttons of the + // SubmenuView's MenuScrollViewContainer. If true is returned, part is set to + // indicate which scroll button the coordinate is. + bool IsScrollButtonAt(SubmenuView* source, + int x, + int y, + MenuPart::Type* part); + + // Returns the target for the mouse event. The coordinates are in terms of + // source's scroll view container. + MenuPart GetMenuPart(SubmenuView* source, const gfx::Point& source_loc); + + // Returns the target for mouse events. The search is done through |item| and + // all its parents. + MenuPart GetMenuPartByScreenCoordinateUsingMenu(MenuItemView* item, + const gfx::Point& screen_loc); + + // Implementation of GetMenuPartByScreenCoordinate for a single menu. Returns + // true if the supplied SubmenuView contains the location in terms of the + // screen. If it does, part is set appropriately and true is returned. + bool GetMenuPartByScreenCoordinateImpl(SubmenuView* menu, + const gfx::Point& screen_loc, + MenuPart* part); + + // Returns true if the SubmenuView contains the specified location. This does + // NOT included the scroll buttons, only the submenu view. + bool DoesSubmenuContainLocation(SubmenuView* submenu, + const gfx::Point& screen_loc); + + // Opens/Closes the necessary menus such that state_ matches that of + // pending_state_. This is invoked if submenus are not opened immediately, + // but after a delay. + void CommitPendingSelection(); + + // If item has a submenu, it is closed. This does NOT update the selection + // in anyway. + void CloseMenu(MenuItemView* item); + + // If item has a submenu, it is opened. This does NOT update the selection + // in anyway. + void OpenMenu(MenuItemView* item); + + // Implementation of OpenMenu. If |show| is true, this invokes show on the + // menu, otherwise Reposition is invoked. + void OpenMenuImpl(MenuItemView* item, bool show); + + // Invoked when the children of a menu change and the menu is showing. + // This closes any submenus and resizes the submenu. + void MenuChildrenChanged(MenuItemView* item); + + // Builds the paths of the two menu items into the two paths, and + // sets first_diff_at to the location of the first difference between the + // two paths. + void BuildPathsAndCalculateDiff(MenuItemView* old_item, + MenuItemView* new_item, + std::vector<MenuItemView*>* old_path, + std::vector<MenuItemView*>* new_path, + size_t* first_diff_at); + + // Builds the path for the specified item. + void BuildMenuItemPath(MenuItemView* item, std::vector<MenuItemView*>* path); + + // Starts/stops the timer that commits the pending state to state + // (opens/closes submenus). + void StartShowTimer(); + void StopShowTimer(); + + // Starts/stops the timer cancel the menu. This is used during drag and + // drop when the drop enters/exits the menu. + void StartCancelAllTimer(); + void StopCancelAllTimer(); + + // Calculates the bounds of the menu to show. is_leading is set to match the + // direction the menu opened in. + gfx::Rect CalculateMenuBounds(MenuItemView* item, + bool prefer_leading, + bool* is_leading); + + // Returns the depth of the menu. + static int MenuDepth(MenuItemView* item); + + // Selects the next/previous menu item. + void IncrementSelection(int delta); + + // Returns the next selectable child menu item of |parent| starting at |index| + // and incrementing index by |delta|. If there are no more selected menu items + // NULL is returned. + MenuItemView* FindNextSelectableMenuItem(MenuItemView* parent, + int index, + int delta); + + // If the selected item has a submenu and it isn't currently open, the + // the selection is changed such that the menu opens immediately. + void OpenSubmenuChangeSelectionIfCan(); + + // If possible, closes the submenu. + void CloseSubmenu(); + + // Returns details about which menu items match the mnemonic |key|. + // |match_function| is used to determine which menus match. + SelectByCharDetails FindChildForMnemonic( + MenuItemView* parent, + char16 key, + bool (*match_function)(MenuItemView* menu, char16 mnemonic)); + + // Selects or accepts the appropriate menu item based on |details|. Returns + // true if |Accept| was invoked (which happens if there aren't multiple item + // with the same mnemonic and the item to select does not have a submenu). + bool AcceptOrSelect(MenuItemView* parent, const SelectByCharDetails& details); + + // Selects by mnemonic, and if that doesn't work tries the first character of + // the title. Returns true if a match was selected and the menu should exit. + bool SelectByChar(char16 key); + +#if defined(OS_WIN) + // If there is a window at the location of the event, a new mouse event is + // generated and posted to it. + void RepostEvent(SubmenuView* source, const MouseEvent& event); +#endif + + // Sets the drop target to new_item. + void SetDropMenuItem(MenuItemView* new_item, + MenuDelegate::DropPosition position); + + // Starts/stops scrolling as appropriate. part gives the part the mouse is + // over. + void UpdateScrolling(const MenuPart& part); + + // Stops scrolling. + void StopScrolling(); + + // Updates |active_mouse_view_| from the location of the event and sends it + // the appropriate events. This is used to send mouse events to child views so + // that they react to click-drag-release as if the user clicked on the view + // itself. + void UpdateActiveMouseView(SubmenuView* event_source, + const MouseEvent& event, + View* target_menu); + + // Sends a mouse release event to the current |active_mouse_view_| and sets + // it to null. + void SendMouseReleaseToActiveView(SubmenuView* event_source, + const MouseEvent& event); + + // Sends a mouse capture lost event to the current |active_mouse_view_| and + // sets it to null. + void SendMouseCaptureLostToActiveView(); + + // Set exit type. + void SetExitType(ExitType type); + + // The active instance. + static MenuController* active_instance_; + + // If true, Run blocks. If false, Run doesn't block and this is used for + // drag and drop. Note that the semantics for drag and drop are slightly + // different: cancel timer is kicked off any time the drag moves outside the + // menu, mouse events do nothing... + bool blocking_run_; + + // If true, we're showing. + bool showing_; + + // Indicates what to exit. + ExitType exit_type_; + + // Whether we did a capture. We do a capture only if we're blocking and + // the mouse was down when Run. + bool did_capture_; + + // As the user drags the mouse around pending_state_ changes immediately. + // When the user stops moving/dragging the mouse (or clicks the mouse) + // pending_state_ is committed to state_, potentially resulting in + // opening or closing submenus. This gives a slight delayed effect to + // submenus as the user moves the mouse around. This is done so that as the + // user moves the mouse all submenus don't immediately pop. + State pending_state_; + State state_; + + // If the user accepted the selection, this is the result. + MenuItemView* result_; + + // The mouse event flags when the user clicked on a menu. Is 0 if the + // user did not use the mouse to select the menu. + int result_mouse_event_flags_; + + // If not empty, it means we're nested. When Run is invoked from within + // Run, the current state (state_) is pushed onto menu_stack_. This allows + // MenuController to restore the state when the nested run returns. + std::list<State> menu_stack_; + + // As the mouse moves around submenus are not opened immediately. Instead + // they open after this timer fires. + base::OneShotTimer<MenuController> show_timer_; + + // Used to invoke CancelAll(). This is used during drag and drop to hide the + // menu after the mouse moves out of the of the menu. This is necessitated by + // the lack of an ability to detect when the drag has completed from the drop + // side. + base::OneShotTimer<MenuController> cancel_all_timer_; + + // Drop target. + MenuItemView* drop_target_; + MenuDelegate::DropPosition drop_position_; + + // Owner of child windows. + Widget* owner_; + + // Indicates a possible drag operation. + bool possible_drag_; + + // True when drag operation is in progress. + bool drag_in_progress_; + + // Location the mouse was pressed at. Used to detect d&d. + gfx::Point press_pt_; + + // We get a slew of drag updated messages as the mouse is over us. To avoid + // continually processing whether we can drop, we cache the coordinates. + bool valid_drop_coordinates_; + gfx::Point drop_pt_; + int last_drop_operation_; + + // If true, we're in the middle of invoking ShowAt on a submenu. + bool showing_submenu_; + + // Task for scrolling the menu. If non-null indicates a scroll is currently + // underway. + scoped_ptr<MenuScrollTask> scroll_task_; + + MenuButton* menu_button_; + + // If non-null mouse drag events are forwarded to this view. See + // UpdateActiveMouseView for details. + View* active_mouse_view_; + + internal::MenuControllerDelegate* delegate_; + + DISALLOW_COPY_AND_ASSIGN(MenuController); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_CONTROLLER_H_ diff --git a/ui/views/controls/menu/menu_controller_delegate.h b/ui/views/controls/menu/menu_controller_delegate.h new file mode 100644 index 0000000..9d31c95 --- /dev/null +++ b/ui/views/controls/menu/menu_controller_delegate.h @@ -0,0 +1,41 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_CONTROLLER_DELEGATE_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_CONTROLLER_DELEGATE_H_ +#pragma once + +namespace views { + +class MenuItemView; + +// This is internal as there should be no need for usage of this class outside +// of views. +namespace internal { + +// Used by MenuController to notify of interesting events that are intended for +// the class using MenuController. This is implemented by MenuRunnerImpl. +class MenuControllerDelegate { + public: + enum NotifyType { + NOTIFY_DELEGATE, + DONT_NOTIFY_DELEGATE + }; + + // Invoked when MenuController closes a menu and the MenuController was + // configured for drop (MenuRunner::FOR_DROP). + virtual void DropMenuClosed(NotifyType type, MenuItemView* menu) = 0; + + // Invoked when the MenuDelegate::GetSiblingMenu() returns non-NULL. + virtual void SiblingMenuCreated(MenuItemView* menu) = 0; + + protected: + virtual ~MenuControllerDelegate() {} +}; + +} // namespace internal + +} // namespace view + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_CONTROLLER_DELEGATE_H_ diff --git a/ui/views/controls/menu/menu_delegate.cc b/ui/views/controls/menu/menu_delegate.cc new file mode 100644 index 0000000..0663659 --- /dev/null +++ b/ui/views/controls/menu/menu_delegate.cc @@ -0,0 +1,127 @@ +// 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 "ui/views/controls/menu/menu_config.h" +#include "ui/views/controls/menu/menu_delegate.h" + +namespace views { + +MenuDelegate::~MenuDelegate() {} + +bool MenuDelegate::IsItemChecked(int id) const { + return false; +} + +string16 MenuDelegate::GetLabel(int id) const { + return string16(); +} + +const gfx::Font& MenuDelegate::GetLabelFont(int id) const { + return MenuConfig::instance().font; +} + +string16 MenuDelegate::GetTooltipText(int id, + const gfx::Point& screen_loc) const { + return string16(); +} + +bool MenuDelegate::GetAccelerator(int id, ui::Accelerator* accelerator) { + return false; +} + +bool MenuDelegate::ShowContextMenu(MenuItemView* source, + int id, + const gfx::Point& p, + bool is_mouse_gesture) { + return false; +} + +bool MenuDelegate::SupportsCommand(int id) const { + return true; +} + +bool MenuDelegate::IsCommandEnabled(int id) const { + return true; +} + +bool MenuDelegate::GetContextualLabel(int id, string16* out) const { + return false; +} + +bool MenuDelegate::ShouldCloseAllMenusOnExecute(int id) { + return true; +} + +void MenuDelegate::ExecuteCommand(int id, int mouse_event_flags) { + ExecuteCommand(id); +} + +bool MenuDelegate::IsTriggerableEvent(MenuItemView* source, + const MouseEvent& e) { + return e.IsLeftMouseButton() || e.IsRightMouseButton(); +} + +bool MenuDelegate::CanDrop(MenuItemView* menu, const OSExchangeData& data) { + return false; +} + +bool MenuDelegate::GetDropFormats( + MenuItemView* menu, + int* formats, + std::set<OSExchangeData::CustomFormat>* custom_formats) { + return false; +} + +bool MenuDelegate::AreDropTypesRequired(MenuItemView* menu) { + return false; +} + +int MenuDelegate::GetDropOperation(MenuItemView* item, + const DropTargetEvent& event, + DropPosition* position) { + NOTREACHED() << "If you override CanDrop, you need to override this too"; + return ui::DragDropTypes::DRAG_NONE; +} + +int MenuDelegate::OnPerformDrop(MenuItemView* menu, + DropPosition position, + const DropTargetEvent& event) { + NOTREACHED() << "If you override CanDrop, you need to override this too"; + return ui::DragDropTypes::DRAG_NONE; +} + +bool MenuDelegate::CanDrag(MenuItemView* menu) { + return false; +} + +void MenuDelegate::WriteDragData(MenuItemView* sender, OSExchangeData* data) { + NOTREACHED() << "If you override CanDrag, you must override this too."; +} + +int MenuDelegate::GetDragOperations(MenuItemView* sender) { + NOTREACHED() << "If you override CanDrag, you must override this too."; + return 0; +} + +MenuItemView* MenuDelegate::GetSiblingMenu(MenuItemView* menu, + const gfx::Point& screen_point, + MenuItemView::AnchorPosition* anchor, + bool* has_mnemonics, + MenuButton** button) { + return NULL; +} + +int MenuDelegate::GetMaxWidthForMenu(MenuItemView* menu) { + // NOTE: this needs to be large enough to accommodate the wrench menu with + // big fonts. + return 800; +} + +void MenuDelegate::WillShowMenu(MenuItemView* menu) { +} + +void MenuDelegate::WillHideMenu(MenuItemView* menu) { +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_delegate.h b/ui/views/controls/menu/menu_delegate.h new file mode 100644 index 0000000..a740a43 --- /dev/null +++ b/ui/views/controls/menu/menu_delegate.h @@ -0,0 +1,209 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_DELEGATE_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_DELEGATE_H_ +#pragma once + +#include <set> +#include <string> + +#include "base/logging.h" +#include "base/string16.h" +#include "ui/base/dragdrop/drag_drop_types.h" +#include "ui/base/dragdrop/os_exchange_data.h" +#include "ui/views/controls/menu/menu_item_view.h" +#include "ui/views/events/event.h" + +using ui::OSExchangeData; + +namespace gfx { + +class Font; + +} // namespace gfx + +namespace ui { + +class Accelerator; + +} // namespace ui + +namespace views { + +class DropTargetEvent; +class MenuButton; + +// MenuDelegate -------------------------------------------------------------- + +// Delegate for a menu. This class is used as part of MenuItemView, see it +// for details. +// TODO(sky): merge this with ui::MenuModel. +class VIEWS_EXPORT MenuDelegate { + public: + // Used during drag and drop to indicate where the drop indicator should + // be rendered. + enum DropPosition { + DROP_UNKNOWN = -1, + + // Indicates a drop is not allowed here. + DROP_NONE, + + // Indicates the drop should occur before the item. + DROP_BEFORE, + + // Indicates the drop should occur after the item. + DROP_AFTER, + + // Indicates the drop should occur on the item. + DROP_ON + }; + + virtual ~MenuDelegate(); + + // Whether or not an item should be shown as checked. This is invoked for + // radio buttons and check buttons. + virtual bool IsItemChecked(int id) const; + + // The string shown for the menu item. This is only invoked when an item is + // added with an empty label. + virtual string16 GetLabel(int id) const; + + // The font for the menu item label. + virtual const gfx::Font& GetLabelFont(int id) const; + + // The tooltip shown for the menu item. This is invoked when the user + // hovers over the item, and no tooltip text has been set for that item. + virtual string16 GetTooltipText(int id, const gfx::Point& screen_loc) const; + + // If there is an accelerator for the menu item with id |id| it is set in + // |accelerator| and true is returned. + virtual bool GetAccelerator(int id, ui::Accelerator* accelerator); + + // Shows the context menu with the specified id. This is invoked when the + // user does the appropriate gesture to show a context menu. The id + // identifies the id of the menu to show the context menu for. + // is_mouse_gesture is true if this is the result of a mouse gesture. + // If this is not the result of a mouse gesture |p| is the recommended + // location to display the content menu at. In either case, |p| is in + // screen coordinates. + // Returns true if a context menu was displayed, otherwise false + virtual bool ShowContextMenu(MenuItemView* source, + int id, + const gfx::Point& p, + bool is_mouse_gesture); + + // Controller + virtual bool SupportsCommand(int id) const; + virtual bool IsCommandEnabled(int id) const; + virtual bool GetContextualLabel(int id, string16* out) const; + virtual void ExecuteCommand(int id) { + } + + // If nested menus are showing (nested menus occur when a menu shows a context + // menu) this is invoked to determine if all the menus should be closed when + // the user selects the menu with the command |id|. This returns true to + // indicate that all menus should be closed. Return false if only the + // context menu should be closed. + virtual bool ShouldCloseAllMenusOnExecute(int id); + + // Executes the specified command. mouse_event_flags give the flags of the + // mouse event that triggered this to be invoked (views::MouseEvent + // flags). mouse_event_flags is 0 if this is triggered by a user gesture + // other than a mouse event. + virtual void ExecuteCommand(int id, int mouse_event_flags); + + // Returns true if the specified mouse event is one the user can use + // to trigger, or accept, the mouse. Defaults to left or right mouse buttons. + virtual bool IsTriggerableEvent(MenuItemView* view, const MouseEvent& e); + + // Invoked to determine if drops can be accepted for a submenu. This is + // ONLY invoked for menus that have submenus and indicates whether or not + // a drop can occur on any of the child items of the item. For example, + // consider the following menu structure: + // + // A + // B + // C + // + // Where A has a submenu with children B and C. This is ONLY invoked for + // A, not B and C. + // + + // To restrict which children can be dropped on override GetDropOperation. + virtual bool CanDrop(MenuItemView* menu, const OSExchangeData& data); + + // See view for a description of this method. + virtual bool GetDropFormats( + MenuItemView* menu, + int* formats, + std::set<OSExchangeData::CustomFormat>* custom_formats); + + // See view for a description of this method. + virtual bool AreDropTypesRequired(MenuItemView* menu); + + // Returns the drop operation for the specified target menu item. This is + // only invoked if CanDrop returned true for the parent menu. position + // is set based on the location of the mouse, reset to specify a different + // position. + // + // If a drop should not be allowed, returned ui::DragDropTypes::DRAG_NONE. + virtual int GetDropOperation(MenuItemView* item, + const DropTargetEvent& event, + DropPosition* position); + + // Invoked to perform the drop operation. This is ONLY invoked if + // canDrop returned true for the parent menu item, and GetDropOperation + // returned an operation other than ui::DragDropTypes::DRAG_NONE. + // + // menu indicates the menu the drop occurred on. + virtual int OnPerformDrop(MenuItemView* menu, + DropPosition position, + const DropTargetEvent& event); + + // Invoked to determine if it is possible for the user to drag the specified + // menu item. + virtual bool CanDrag(MenuItemView* menu); + + // Invoked to write the data for a drag operation to data. sender is the + // MenuItemView being dragged. + virtual void WriteDragData(MenuItemView* sender, OSExchangeData* data); + + // Invoked to determine the drag operations for a drag session of sender. + // See DragDropTypes for possible values. + virtual int GetDragOperations(MenuItemView* sender); + + // Notification the menu has closed. This is only sent when running the + // menu for a drop. + virtual void DropMenuClosed(MenuItemView* menu) { + } + + // Notification that the user has highlighted the specified item. + virtual void SelectionChanged(MenuItemView* menu) { + } + + // If the user drags the mouse outside the bounds of the menu the delegate + // is queried for a sibling menu to show. If this returns non-null the + // current menu is hidden, and the menu returned from this method is shown. + // + // The delegate owns the returned menu, not the controller. + virtual MenuItemView* GetSiblingMenu(MenuItemView* menu, + const gfx::Point& screen_point, + MenuItemView::AnchorPosition* anchor, + bool* has_mnemonics, + MenuButton** button); + + // Returns the max width menus can grow to be. + virtual int GetMaxWidthForMenu(MenuItemView* menu); + + // Invoked prior to a menu being shown. + virtual void WillShowMenu(MenuItemView* menu); + + // Invoked prior to a menu being hidden. + virtual void WillHideMenu(MenuItemView* menu); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_DELEGATE_H_ diff --git a/ui/views/controls/menu/menu_gtk.cc b/ui/views/controls/menu/menu_gtk.cc new file mode 100644 index 0000000..49972b2 --- /dev/null +++ b/ui/views/controls/menu/menu_gtk.cc @@ -0,0 +1,82 @@ +// 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 "ui/views/controls/menu/menu_gtk.h" + +#include "base/logging.h" + +namespace views { + +// static +Menu* Menu::Create(Delegate* delegate, + AnchorPoint anchor, + gfx::NativeView parent) { + return new MenuGtk(delegate, anchor, parent); +} + +// static +Menu* Menu::GetSystemMenu(gfx::NativeWindow parent) { + NOTIMPLEMENTED(); + return NULL; +} + +MenuGtk::MenuGtk(Delegate* d, AnchorPoint anchor, gfx::NativeView owner) + : Menu(d, anchor) { + DCHECK(delegate()); +} + +MenuGtk::~MenuGtk() { +} + +Menu* MenuGtk::AddSubMenuWithIcon(int index, + int item_id, + const string16& label, + const SkBitmap& icon) { + NOTIMPLEMENTED(); + return NULL; +} + +void MenuGtk::AddSeparator(int index) { + NOTIMPLEMENTED(); +} + +void MenuGtk::EnableMenuItemByID(int item_id, bool enabled) { + NOTIMPLEMENTED(); +} + +void MenuGtk::EnableMenuItemAt(int index, bool enabled) { + NOTIMPLEMENTED(); +} + +void MenuGtk::SetMenuLabel(int item_id, const string16& label) { + NOTIMPLEMENTED(); +} + +bool MenuGtk::SetIcon(const SkBitmap& icon, int item_id) { + NOTIMPLEMENTED(); + return false; +} + +void MenuGtk::RunMenuAt(int x, int y) { + NOTIMPLEMENTED(); +} + +void MenuGtk::Cancel() { + NOTIMPLEMENTED(); +} + +int MenuGtk::ItemCount() { + NOTIMPLEMENTED(); + return 0; +} + +void MenuGtk::AddMenuItemInternal(int index, + int item_id, + const string16& label, + const SkBitmap& icon, + MenuItemType type) { + NOTIMPLEMENTED(); +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_gtk.h b/ui/views/controls/menu/menu_gtk.h new file mode 100644 index 0000000..0d9eeaf --- /dev/null +++ b/ui/views/controls/menu/menu_gtk.h @@ -0,0 +1,54 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_GTK_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_GTK_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "ui/views/controls/menu/menu.h" + +namespace views { + +class MenuGtk : public Menu { + public: + // Construct a Menu using the specified controller to determine command + // state. + // delegate A Menu::Delegate implementation that provides more + // information about the Menu presentation. + // anchor An alignment hint for the popup menu. + // owner The window that the menu is being brought up relative + // to. Not actually used for anything but must not be + // NULL. + MenuGtk(Delegate* d, AnchorPoint anchor, gfx::NativeView owner); + virtual ~MenuGtk(); + + // Overridden from Menu: + virtual Menu* AddSubMenuWithIcon(int index, + int item_id, + const string16& label, + const SkBitmap& icon) OVERRIDE; + virtual void AddSeparator(int index) OVERRIDE; + virtual void EnableMenuItemByID(int item_id, bool enabled) OVERRIDE; + virtual void EnableMenuItemAt(int index, bool enabled) OVERRIDE; + virtual void SetMenuLabel(int item_id, const string16& label) OVERRIDE; + virtual bool SetIcon(const SkBitmap& icon, int item_id) OVERRIDE; + virtual void RunMenuAt(int x, int y) OVERRIDE; + virtual void Cancel() OVERRIDE; + virtual int ItemCount() OVERRIDE; + + protected: + virtual void AddMenuItemInternal(int index, + int item_id, + const string16& label, + const SkBitmap& icon, + MenuItemType type) OVERRIDE; + + DISALLOW_COPY_AND_ASSIGN(MenuGtk); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_GTK_H_ diff --git a/ui/views/controls/menu/menu_host.cc b/ui/views/controls/menu/menu_host.cc new file mode 100644 index 0000000..701056f --- /dev/null +++ b/ui/views/controls/menu/menu_host.cc @@ -0,0 +1,110 @@ +// 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 "ui/views/controls/menu/menu_host.h" + +#include "ui/views/controls/menu/menu_controller.h" +#include "ui/views/controls/menu/menu_host_root_view.h" +#include "ui/views/controls/menu/menu_item_view.h" +#include "ui/views/controls/menu/native_menu_host.h" +#include "ui/views/controls/menu/submenu_view.h" +#include "ui/views/widget/native_widget_private.h" +#include "ui/views/widget/widget.h" + +namespace views { + +//////////////////////////////////////////////////////////////////////////////// +// MenuHost, public: + +MenuHost::MenuHost(SubmenuView* submenu) + : submenu_(submenu), + destroying_(false), + ignore_capture_lost_(false) { +} + +MenuHost::~MenuHost() { +} + +void MenuHost::InitMenuHost(Widget* parent, + const gfx::Rect& bounds, + View* contents_view, + bool do_capture) { + Widget::InitParams params(Widget::InitParams::TYPE_MENU); + params.has_dropshadow = true; + params.parent_widget = parent; + params.bounds = bounds; + Init(params); + SetContentsView(contents_view); + ShowMenuHost(do_capture); +} + +bool MenuHost::IsMenuHostVisible() { + return IsVisible(); +} + +void MenuHost::ShowMenuHost(bool do_capture) { + // Doing a capture may make us get capture lost. Ignore it while we're in the + // process of showing. + ignore_capture_lost_ = true; + Show(); + if (do_capture) + native_widget_private()->SetMouseCapture(); + ignore_capture_lost_ = false; +} + +void MenuHost::HideMenuHost() { + ignore_capture_lost_ = true; + ReleaseMenuHostCapture(); + Hide(); + ignore_capture_lost_ = false; +} + +void MenuHost::DestroyMenuHost() { + HideMenuHost(); + destroying_ = true; + static_cast<MenuHostRootView*>(GetRootView())->ClearSubmenu(); + Close(); +} + +void MenuHost::SetMenuHostBounds(const gfx::Rect& bounds) { + SetBounds(bounds); +} + +void MenuHost::ReleaseMenuHostCapture() { + if (native_widget_private()->HasMouseCapture()) + native_widget_private()->ReleaseMouseCapture(); +} + +//////////////////////////////////////////////////////////////////////////////// +// MenuHost, Widget overrides: + +internal::RootView* MenuHost::CreateRootView() { + return new MenuHostRootView(this, submenu_); +} + +bool MenuHost::ShouldReleaseCaptureOnMouseReleased() const { + return false; +} + +void MenuHost::OnMouseCaptureLost() { + if (destroying_ || ignore_capture_lost_) + return; + MenuController* menu_controller = + submenu_->GetMenuItem()->GetMenuController(); + if (menu_controller && !menu_controller->drag_in_progress()) + menu_controller->CancelAll(); + Widget::OnMouseCaptureLost(); +} + +void MenuHost::OnNativeWidgetDestroyed() { + if (!destroying_) { + // We weren't explicitly told to destroy ourselves, which means the menu was + // deleted out from under us (the window we're parented to was closed). Tell + // the SubmenuView to drop references to us. + submenu_->MenuHostDestroyed(); + } + Widget::OnNativeWidgetDestroyed(); +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_host.h b/ui/views/controls/menu/menu_host.h new file mode 100644 index 0000000..bbf9242 --- /dev/null +++ b/ui/views/controls/menu/menu_host.h @@ -0,0 +1,79 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_HOST_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_HOST_H_ +#pragma once + +#include "base/compiler_specific.h" +#include "ui/gfx/rect.h" +#include "ui/views/controls/menu/native_menu_host_delegate.h" +#include "ui/views/widget/widget.h" + +namespace views { + +class NativeWidget; +class SubmenuView; +class View; +class Widget; + +// SubmenuView uses a MenuHost to house the SubmenuView. +// +// SubmenuView owns the MenuHost. When SubmenuView is done with the MenuHost +// |DestroyMenuHost| is invoked. The one exception to this is if the native +// OS destroys the widget out from under us, in which case |MenuHostDestroyed| +// is invoked back on the SubmenuView and the SubmenuView then drops references +// to the MenuHost. +class MenuHost : public Widget { + public: + explicit MenuHost(SubmenuView* submenu); + virtual ~MenuHost(); + + // Initializes and shows the MenuHost. + void InitMenuHost(Widget* parent, + const gfx::Rect& bounds, + View* contents_view, + bool do_capture); + + // Returns true if the menu host is visible. + bool IsMenuHostVisible(); + + // Shows the menu host. If |do_capture| is true the menu host should do a + // mouse grab. + void ShowMenuHost(bool do_capture); + + // Hides the menu host. + void HideMenuHost(); + + // Destroys and deletes the menu host. + void DestroyMenuHost(); + + // Sets the bounds of the menu host. + void SetMenuHostBounds(const gfx::Rect& bounds); + + // Releases a mouse grab installed by |ShowMenuHost|. + void ReleaseMenuHostCapture(); + + private: + // Overridden from Widget: + virtual internal::RootView* CreateRootView() OVERRIDE; + virtual bool ShouldReleaseCaptureOnMouseReleased() const OVERRIDE; + virtual void OnMouseCaptureLost() OVERRIDE; + virtual void OnNativeWidgetDestroyed() OVERRIDE; + + // The view we contain. + SubmenuView* submenu_; + + // If true, DestroyMenuHost has been invoked. + bool destroying_; + + // If true and capture is lost we don't notify the delegate. + bool ignore_capture_lost_; + + DISALLOW_COPY_AND_ASSIGN(MenuHost); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_HOST_H_ diff --git a/ui/views/controls/menu/menu_host_root_view.cc b/ui/views/controls/menu/menu_host_root_view.cc new file mode 100644 index 0000000..44ed97f --- /dev/null +++ b/ui/views/controls/menu/menu_host_root_view.cc @@ -0,0 +1,69 @@ +// 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 "ui/views/controls/menu/menu_host_root_view.h" + +#include "ui/views/controls/menu/menu_controller.h" +#include "ui/views/controls/menu/submenu_view.h" + +namespace views { + +MenuHostRootView::MenuHostRootView(Widget* widget, + SubmenuView* submenu) + : internal::RootView(widget), + submenu_(submenu), + forward_drag_to_menu_controller_(true) { +} + +bool MenuHostRootView::OnMousePressed(const MouseEvent& event) { + forward_drag_to_menu_controller_ = + !GetLocalBounds().Contains(event.location()) || + !RootView::OnMousePressed(event); + if (forward_drag_to_menu_controller_ && GetMenuController()) + GetMenuController()->OnMousePressed(submenu_, event); + return true; +} + +bool MenuHostRootView::OnMouseDragged(const MouseEvent& event) { + if (forward_drag_to_menu_controller_ && GetMenuController()) { + GetMenuController()->OnMouseDragged(submenu_, event); + return true; + } + return RootView::OnMouseDragged(event); +} + +void MenuHostRootView::OnMouseReleased(const MouseEvent& event) { + RootView::OnMouseReleased(event); + if (forward_drag_to_menu_controller_ && GetMenuController()) { + forward_drag_to_menu_controller_ = false; + GetMenuController()->OnMouseReleased(submenu_, event); + } +} + +void MenuHostRootView::OnMouseMoved(const MouseEvent& event) { + RootView::OnMouseMoved(event); + if (GetMenuController()) + GetMenuController()->OnMouseMoved(submenu_, event); +} + +bool MenuHostRootView::OnMouseWheel(const MouseWheelEvent& event) { +#if defined(OS_LINUX) + // ChromeOS uses MenuController to forward events like other + // mouse events. + return GetMenuController() && + GetMenuController()->OnMouseWheel(submenu_, event); +#else + // Windows uses focus_util_win::RerouteMouseWheel to forward events to + // the right menu. + // RootView::OnMouseWheel forwards to the focused view. We don't have a + // focused view, so we need to override this then forward to the menu. + return submenu_->OnMouseWheel(event); +#endif +} + +MenuController* MenuHostRootView::GetMenuController() { + return submenu_ ? submenu_->GetMenuItem()->GetMenuController() : NULL; +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_host_root_view.h b/ui/views/controls/menu/menu_host_root_view.h new file mode 100644 index 0000000..15f61bb --- /dev/null +++ b/ui/views/controls/menu/menu_host_root_view.h @@ -0,0 +1,50 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_HOST_ROOT_VIEW_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_HOST_ROOT_VIEW_H_ +#pragma once + +#include "ui/views/widget/root_view.h" + +namespace views { + +class MenuController; +class SubmenuView; + +// MenuHostRootView is the RootView of the window showing the menu. +// SubmenuView's scroll view is added as a child of MenuHostRootView. +// MenuHostRootView forwards relevant events to the MenuController. +// +// As all the menu items are owned by the root menu item, care must be taken +// such that when MenuHostRootView is deleted it doesn't delete the menu items. +class MenuHostRootView : public internal::RootView { + public: + MenuHostRootView(Widget* widget, SubmenuView* submenu); + + void ClearSubmenu() { submenu_ = NULL; } + + // Overridden from View: + virtual bool OnMousePressed(const MouseEvent& event) OVERRIDE; + virtual bool OnMouseDragged(const MouseEvent& event) OVERRIDE; + virtual void OnMouseReleased(const MouseEvent& event) OVERRIDE; + virtual void OnMouseMoved(const MouseEvent& event) OVERRIDE; + virtual bool OnMouseWheel(const MouseWheelEvent& event) OVERRIDE; + + private: + // Returns the MenuController for this MenuHostRootView. + MenuController* GetMenuController(); + + // The SubmenuView we contain. + SubmenuView* submenu_; + + // Whether mouse dragged/released should be forwarded to the MenuController. + bool forward_drag_to_menu_controller_; + + DISALLOW_COPY_AND_ASSIGN(MenuHostRootView); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_HOST_ROOT_VIEW_H_ diff --git a/ui/views/controls/menu/menu_image_util.cc b/ui/views/controls/menu/menu_image_util.cc new file mode 100644 index 0000000..061b3f5 --- /dev/null +++ b/ui/views/controls/menu/menu_image_util.cc @@ -0,0 +1,116 @@ +// 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 "ui/views/controls/menu/menu_image_util.h" + +#include "base/i18n/rtl.h" +#include "grit/ui_resources.h" +#include "third_party/skia/include/effects/SkGradientShader.h" +#include "ui/base/resource/resource_bundle.h" +#include "ui/gfx/canvas_skia.h" +#include "ui/gfx/point.h" + +namespace { + +// Size of the radio button inciator. +const int kSelectedIndicatorSize = 5; +const int kIndicatorSize = 10; + +// Used for the radio indicator. See theme_draw for details. +const double kGradientStop = .5; +const SkColor kGradient0 = SkColorSetRGB(255, 255, 255); +const SkColor kGradient1 = SkColorSetRGB(255, 255, 255); +const SkColor kGradient2 = SkColorSetRGB(0xD8, 0xD8, 0xD8); +const SkColor kBaseStroke = SkColorSetRGB(0x8F, 0x8F, 0x8F); +const SkColor kRadioButtonIndicatorGradient0 = SkColorSetRGB(0, 0, 0); +const SkColor kRadioButtonIndicatorGradient1 = SkColorSetRGB(0x83, 0x83, 0x83); +const SkColor kIndicatorStroke = SkColorSetRGB(0, 0, 0); + +SkBitmap* CreateRadioButtonImage(bool selected) { + // + 2 (1px on each side) to cover rounding error. + gfx::CanvasSkia canvas(kIndicatorSize + 2, kIndicatorSize + 2, false); + canvas.Translate(gfx::Point(1, 1)); + + SkPoint gradient_points[3]; + gradient_points[0].set(SkIntToScalar(0), SkIntToScalar(0)); + gradient_points[1].set( + SkIntToScalar(0), + SkIntToScalar(static_cast<int>(kIndicatorSize * kGradientStop))); + gradient_points[2].set(SkIntToScalar(0), SkIntToScalar(kIndicatorSize)); + SkColor gradient_colors[3] = { kGradient0, kGradient1, kGradient2 }; + SkShader* shader = SkGradientShader::CreateLinear( + gradient_points, gradient_colors, NULL, arraysize(gradient_points), + SkShader::kClamp_TileMode, NULL); + SkPaint paint; + paint.setStyle(SkPaint::kFill_Style); + paint.setAntiAlias(true); + paint.setShader(shader); + shader->unref(); + int radius = kIndicatorSize / 2; + canvas.sk_canvas()->drawCircle(radius, radius, radius, paint); + + paint.setStrokeWidth(SkIntToScalar(0)); + paint.setShader(NULL); + paint.setStyle(SkPaint::kStroke_Style); + paint.setColor(kBaseStroke); + canvas.sk_canvas()->drawCircle(radius, radius, radius, paint); + + if (selected) { + SkPoint selected_gradient_points[2]; + selected_gradient_points[0].set(SkIntToScalar(0), SkIntToScalar(0)); + selected_gradient_points[1].set( + SkIntToScalar(0), + SkIntToScalar(kSelectedIndicatorSize)); + SkColor selected_gradient_colors[2] = { kRadioButtonIndicatorGradient0, + kRadioButtonIndicatorGradient1 }; + shader = SkGradientShader::CreateLinear( + selected_gradient_points, selected_gradient_colors, NULL, + arraysize(selected_gradient_points), SkShader::kClamp_TileMode, NULL); + paint.setShader(shader); + shader->unref(); + paint.setStyle(SkPaint::kFill_Style); + canvas.sk_canvas()->drawCircle(radius, radius, + kSelectedIndicatorSize / 2, paint); + + paint.setStrokeWidth(SkIntToScalar(0)); + paint.setShader(NULL); + paint.setStyle(SkPaint::kStroke_Style); + paint.setColor(kIndicatorStroke); + canvas.sk_canvas()->drawCircle(radius, radius, + kSelectedIndicatorSize / 2, paint); + } + return new SkBitmap(canvas.ExtractBitmap()); +} + +SkBitmap* GetRtlSubmenuArrowImage() { + static SkBitmap* kRtlArrow = NULL; + if (!kRtlArrow) { + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + SkBitmap* r = rb.GetBitmapNamed(IDR_MENU_ARROW); + gfx::CanvasSkia canvas(r->width(), r->height(), false); + canvas.Scale(-1, 1); + canvas.DrawBitmapInt(*r, - r->width(), 0); + kRtlArrow = new SkBitmap(canvas.ExtractBitmap()); + } + return kRtlArrow; +} + +} // namespace + +namespace views { + +const SkBitmap* GetRadioButtonImage(bool selected) { + static const SkBitmap* kRadioOn = CreateRadioButtonImage(true); + static const SkBitmap* kRadioOff = CreateRadioButtonImage(false); + + return selected ? kRadioOn : kRadioOff; +} + +const SkBitmap* GetSubmenuArrowImage() { + return base::i18n::IsRTL() ? + GetRtlSubmenuArrowImage() : + ResourceBundle::GetSharedInstance().GetBitmapNamed(IDR_MENU_ARROW); +} + +} // namespace views; diff --git a/ui/views/controls/menu/menu_image_util.h b/ui/views/controls/menu/menu_image_util.h new file mode 100644 index 0000000..7d98035 --- /dev/null +++ b/ui/views/controls/menu/menu_image_util.h @@ -0,0 +1,24 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_IMAGE_UTIL_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_IMAGE_UTIL_H_ +#pragma once + +#include "third_party/skia/include/core/SkBitmap.h" + +namespace views { + +// Return the RadioButton image for given state. +// It returns the "selected" image when |selected| is +// true, or the "unselected" image if false. +// The returned image is global object and should not be freed. +const SkBitmap* GetRadioButtonImage(bool selected); + +// Returns the image for submenu arrow for current RTL setting. +const SkBitmap* GetSubmenuArrowImage(); + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_IMAGE_UTIL_H_ diff --git a/ui/views/controls/menu/menu_item_view.cc b/ui/views/controls/menu/menu_item_view.cc new file mode 100644 index 0000000..372beae --- /dev/null +++ b/ui/views/controls/menu/menu_item_view.cc @@ -0,0 +1,779 @@ +// 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 "ui/views/controls/menu/menu_item_view.h" + +#include "base/i18n/case_conversion.h" +#include "base/stl_util.h" +#include "base/utf_string_conversions.h" +#include "grit/ui_strings.h" +#include "ui/base/accessibility/accessible_view_state.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/models/menu_model.h" +#include "ui/gfx/canvas.h" +#include "ui/views/controls/button/menu_button.h" +#include "ui/views/controls/button/text_button.h" +#include "ui/views/controls/menu/menu_config.h" +#include "ui/views/controls/menu/menu_controller.h" +#include "ui/views/controls/menu/menu_separator.h" +#include "ui/views/controls/menu/submenu_view.h" + +namespace views { + +namespace { + +// EmptyMenuMenuItem --------------------------------------------------------- + +// EmptyMenuMenuItem is used when a menu has no menu items. EmptyMenuMenuItem +// is itself a MenuItemView, but it uses a different ID so that it isn't +// identified as a MenuItemView. + +class EmptyMenuMenuItem : public MenuItemView { + public: + explicit EmptyMenuMenuItem(MenuItemView* parent) + : MenuItemView(parent, 0, EMPTY) { + // Set this so that we're not identified as a normal menu item. + set_id(kEmptyMenuItemViewID); + SetTitle(l10n_util::GetStringUTF16(IDS_APP_MENU_EMPTY_SUBMENU)); + SetEnabled(false); + } + + virtual bool GetTooltipText(const gfx::Point& p, + string16* tooltip) const OVERRIDE { + // Empty menu items shouldn't have a tooltip. + return false; + } + + private: + DISALLOW_COPY_AND_ASSIGN(EmptyMenuMenuItem); +}; + +} // namespace + +// Padding between child views. +static const int kChildXPadding = 8; + +// MenuItemView --------------------------------------------------------------- + +// static +const int MenuItemView::kMenuItemViewID = 1001; + +// static +const int MenuItemView::kEmptyMenuItemViewID = + MenuItemView::kMenuItemViewID + 1; + +// static +int MenuItemView::label_start_; + +// static +int MenuItemView::item_right_margin_; + +// static +int MenuItemView::pref_menu_height_; + +// static +const char MenuItemView::kViewClassName[] = "views/MenuItemView"; + +MenuItemView::MenuItemView(MenuDelegate* delegate) + : delegate_(delegate), + controller_(NULL), + canceled_(false), + parent_menu_item_(NULL), + type_(SUBMENU), + selected_(false), + command_(0), + submenu_(NULL), + has_mnemonics_(false), + show_mnemonics_(false), + has_icons_(false), + top_margin_(-1), + bottom_margin_(-1), + requested_menu_position_(POSITION_BEST_FIT), + actual_menu_position_(requested_menu_position_) { + // NOTE: don't check the delegate for NULL, UpdateMenuPartSizes supplies a + // NULL delegate. + Init(NULL, 0, SUBMENU, delegate); +} + +void MenuItemView::ChildPreferredSizeChanged(View* child) { + pref_size_.SetSize(0, 0); + PreferredSizeChanged(); +} + +bool MenuItemView::GetTooltipText(const gfx::Point& p, + string16* tooltip) const { + *tooltip = tooltip_; + if (!tooltip->empty()) + return true; + + if (GetType() == SEPARATOR) + return false; + + const MenuController* controller = GetMenuController(); + if (!controller || controller->exit_type() != MenuController::EXIT_NONE) { + // Either the menu has been closed or we're in the process of closing the + // menu. Don't attempt to query the delegate as it may no longer be valid. + return false; + } + + const MenuItemView* root_menu_item = GetRootMenuItem(); + if (root_menu_item->canceled_) { + // TODO(sky): if |canceled_| is true, controller->exit_type() should be + // something other than EXIT_NONE, but crash reports seem to indicate + // otherwise. Figure out why this is needed. + return false; + } + + const MenuDelegate* delegate = GetDelegate(); + CHECK(delegate); + gfx::Point location(p); + ConvertPointToScreen(this, &location); + *tooltip = delegate->GetTooltipText(command_, location); + return !tooltip->empty(); +} + +void MenuItemView::GetAccessibleState(ui::AccessibleViewState* state) { + state->role = ui::AccessibilityTypes::ROLE_MENUITEM; + + string16 item_text; + if (IsContainer()) { + // The first child is taking over, just use its accessible name instead of + // |title_|. + View* child = child_at(0); + ui::AccessibleViewState state; + child->GetAccessibleState(&state); + item_text = state.name; + } else { + item_text = title_; + } + state->name = GetAccessibleNameForMenuItem(item_text, GetAcceleratorText()); + + switch (GetType()) { + case SUBMENU: + state->state |= ui::AccessibilityTypes::STATE_HASPOPUP; + break; + case CHECKBOX: + case RADIO: + state->state |= GetDelegate()->IsItemChecked(GetCommand()) ? + ui::AccessibilityTypes::STATE_CHECKED : 0; + break; + case NORMAL: + case SEPARATOR: + case EMPTY: + // No additional accessibility states currently for these menu states. + break; + } +} + +// static +string16 MenuItemView::GetAccessibleNameForMenuItem( + const string16& item_text, const string16& accelerator_text) { + string16 accessible_name = item_text; + + // Filter out the "&" for accessibility clients. + size_t index = 0; + const char16 amp = '&'; + while ((index = accessible_name.find(amp, index)) != string16::npos && + index + 1 < accessible_name.length()) { + accessible_name.replace(index, accessible_name.length() - index, + accessible_name.substr(index + 1)); + + // Special case for "&&" (escaped for "&"). + if (accessible_name[index] == '&') + ++index; + } + + // Append accelerator text. + if (!accelerator_text.empty()) { + accessible_name.push_back(' '); + accessible_name.append(accelerator_text); + } + + return accessible_name; +} + +void MenuItemView::Cancel() { + if (controller_ && !canceled_) { + canceled_ = true; + controller_->Cancel(MenuController::EXIT_ALL); + } +} + +MenuItemView* MenuItemView::AddMenuItemAt(int index, + int item_id, + const string16& label, + const SkBitmap& icon, + Type type) { + DCHECK_NE(type, EMPTY); + DCHECK_LE(0, index); + if (!submenu_) + CreateSubmenu(); + DCHECK_GE(submenu_->child_count(), index); + if (type == SEPARATOR) { + submenu_->AddChildViewAt(new MenuSeparator(), index); + return NULL; + } + MenuItemView* item = new MenuItemView(this, item_id, type); + if (label.empty() && GetDelegate()) + item->SetTitle(GetDelegate()->GetLabel(item_id)); + else + item->SetTitle(label); + item->SetIcon(icon); + if (type == SUBMENU) + item->CreateSubmenu(); + submenu_->AddChildViewAt(item, index); + return item; +} + +void MenuItemView::RemoveMenuItemAt(int index) { + DCHECK(submenu_); + DCHECK_LE(0, index); + DCHECK_GT(submenu_->child_count(), index); + + View* item = submenu_->child_at(index); + DCHECK(item); + submenu_->RemoveChildView(item); + + // RemoveChildView() does not delete the item, which is a good thing + // in case a submenu is being displayed while items are being removed. + // Deletion will be done by ChildrenChanged() or at destruction. + removed_items_.push_back(item); +} + +MenuItemView* MenuItemView::AppendMenuItem(int item_id, + const string16& label, + Type type) { + return AppendMenuItemImpl(item_id, label, SkBitmap(), type); +} + +MenuItemView* MenuItemView::AppendSubMenu(int item_id, + const string16& label) { + return AppendMenuItemImpl(item_id, label, SkBitmap(), SUBMENU); +} + +MenuItemView* MenuItemView::AppendSubMenuWithIcon(int item_id, + const string16& label, + const SkBitmap& icon) { + return AppendMenuItemImpl(item_id, label, icon, SUBMENU); +} + +MenuItemView* MenuItemView::AppendMenuItemWithLabel(int item_id, + const string16& label) { + return AppendMenuItem(item_id, label, NORMAL); +} + +MenuItemView* MenuItemView::AppendDelegateMenuItem(int item_id) { + return AppendMenuItem(item_id, string16(), NORMAL); +} + +void MenuItemView::AppendSeparator() { + AppendMenuItemImpl(0, string16(), SkBitmap(), SEPARATOR); +} + +MenuItemView* MenuItemView::AppendMenuItemWithIcon(int item_id, + const string16& label, + const SkBitmap& icon) { + return AppendMenuItemImpl(item_id, label, icon, NORMAL); +} + +MenuItemView* MenuItemView::AppendMenuItemFromModel(ui::MenuModel* model, + int index, + int id) { + SkBitmap icon; + string16 label; + MenuItemView::Type type; + ui::MenuModel::ItemType menu_type = model->GetTypeAt(index); + switch (menu_type) { + case ui::MenuModel::TYPE_COMMAND: + model->GetIconAt(index, &icon); + type = MenuItemView::NORMAL; + label = model->GetLabelAt(index); + break; + case ui::MenuModel::TYPE_CHECK: + type = MenuItemView::CHECKBOX; + label = model->GetLabelAt(index); + break; + case ui::MenuModel::TYPE_RADIO: + type = MenuItemView::RADIO; + label = model->GetLabelAt(index); + break; + case ui::MenuModel::TYPE_SEPARATOR: + type = MenuItemView::SEPARATOR; + break; + case ui::MenuModel::TYPE_SUBMENU: + model->GetIconAt(index, &icon); + type = MenuItemView::SUBMENU; + label = model->GetLabelAt(index); + break; + default: + NOTREACHED(); + type = MenuItemView::NORMAL; + break; + } + + return AppendMenuItemImpl(id, label, icon, type); +} + +MenuItemView* MenuItemView::AppendMenuItemImpl(int item_id, + const string16& label, + const SkBitmap& icon, + Type type) { + const int index = submenu_ ? submenu_->child_count() : 0; + return AddMenuItemAt(index, item_id, label, icon, type); +} + +SubmenuView* MenuItemView::CreateSubmenu() { + if (!submenu_) + submenu_ = new SubmenuView(this); + return submenu_; +} + +bool MenuItemView::HasSubmenu() const { + return (submenu_ != NULL); +} + +SubmenuView* MenuItemView::GetSubmenu() const { + return submenu_; +} + +void MenuItemView::SetTitle(const string16& title) { + title_ = title; + pref_size_.SetSize(0, 0); // Triggers preferred size recalculation. +} + +void MenuItemView::SetSelected(bool selected) { + selected_ = selected; + SchedulePaint(); +} + +void MenuItemView::SetTooltip(const string16& tooltip, int item_id) { + MenuItemView* item = GetMenuItemByID(item_id); + DCHECK(item); + item->tooltip_ = tooltip; +} + +void MenuItemView::SetIcon(const SkBitmap& icon, int item_id) { + MenuItemView* item = GetMenuItemByID(item_id); + DCHECK(item); + item->SetIcon(icon); +} + +void MenuItemView::SetIcon(const SkBitmap& icon) { + icon_ = icon; + SchedulePaint(); +} + +void MenuItemView::OnPaint(gfx::Canvas* canvas) { + PaintButton(canvas, PB_NORMAL); +} + +gfx::Size MenuItemView::GetPreferredSize() { + if (pref_size_.IsEmpty()) + pref_size_ = CalculatePreferredSize(); + return pref_size_; +} + +MenuController* MenuItemView::GetMenuController() { + return GetRootMenuItem()->controller_; +} + +const MenuController* MenuItemView::GetMenuController() const { + return GetRootMenuItem()->controller_; +} + +MenuDelegate* MenuItemView::GetDelegate() { + return GetRootMenuItem()->delegate_; +} + +const MenuDelegate* MenuItemView::GetDelegate() const { + return GetRootMenuItem()->delegate_; +} + +MenuItemView* MenuItemView::GetRootMenuItem() { + return const_cast<MenuItemView*>( + static_cast<const MenuItemView*>(this)->GetRootMenuItem()); +} + +const MenuItemView* MenuItemView::GetRootMenuItem() const { + const MenuItemView* item = this; + for (const MenuItemView* parent = GetParentMenuItem(); parent; + parent = item->GetParentMenuItem()) + item = parent; + return item; +} + +char16 MenuItemView::GetMnemonic() { + if (!GetRootMenuItem()->has_mnemonics_) + return 0; + + size_t index = 0; + do { + index = title_.find('&', index); + if (index != string16::npos) { + if (index + 1 != title_.size() && title_[index + 1] != '&') { + char16 char_array[] = { title_[index + 1], 0 }; + // TODO(jshin): What about Turkish locale? See http://crbug.com/81719. + // If the mnemonic is capital I and the UI language is Turkish, + // lowercasing it results in 'small dotless i', which is different + // from a 'dotted i'. Similar issues may exist for az and lt locales. + return base::i18n::ToLower(char_array)[0]; + } + index++; + } + } while (index != string16::npos); + return 0; +} + +MenuItemView* MenuItemView::GetMenuItemByID(int id) { + if (GetCommand() == id) + return this; + if (!HasSubmenu()) + return NULL; + for (int i = 0; i < GetSubmenu()->child_count(); ++i) { + View* child = GetSubmenu()->child_at(i); + if (child->id() == MenuItemView::kMenuItemViewID) { + MenuItemView* result = static_cast<MenuItemView*>(child)-> + GetMenuItemByID(id); + if (result) + return result; + } + } + return NULL; +} + +void MenuItemView::ChildrenChanged() { + MenuController* controller = GetMenuController(); + if (controller) { + // Handles the case where we were empty and are no longer empty. + RemoveEmptyMenus(); + + // Handles the case where we were not empty, but now are. + AddEmptyMenus(); + + controller->MenuChildrenChanged(this); + + if (submenu_) { + // Force a paint and layout. This handles the case of the top + // level window's size remaining the same, resulting in no + // change to the submenu's size and no layout. + submenu_->Layout(); + submenu_->SchedulePaint(); + } + } + + STLDeleteElements(&removed_items_); +} + +void MenuItemView::Layout() { + if (!has_children()) + return; + + if (IsContainer()) { + View* child = child_at(0); + gfx::Size size = child->GetPreferredSize(); + child->SetBounds(0, GetTopMargin(), size.width(), size.height()); + } else { + // Child views are laid out right aligned and given the full height. To + // right align start with the last view and progress to the first. + for (int i = child_count() - 1, x = width() - item_right_margin_; i >= 0; + --i) { + View* child = child_at(i); + int width = child->GetPreferredSize().width(); + child->SetBounds(x - width, 0, width, height()); + x -= width - kChildXPadding; + } + } +} + +int MenuItemView::GetAcceleratorTextWidth() { + string16 text = GetAcceleratorText(); + return text.empty() ? 0 : GetFont().GetStringWidth(text); +} + +void MenuItemView::SetMargins(int top_margin, int bottom_margin) { + top_margin_ = top_margin; + bottom_margin_ = bottom_margin; + + // invalidate GetPreferredSize() cache + pref_size_.SetSize(0,0); +} + +MenuItemView::MenuItemView(MenuItemView* parent, + int command, + MenuItemView::Type type) + : delegate_(NULL), + controller_(NULL), + canceled_(false), + parent_menu_item_(parent), + type_(type), + selected_(false), + command_(command), + submenu_(NULL), + has_mnemonics_(false), + show_mnemonics_(false), + has_icons_(false), + top_margin_(-1), + bottom_margin_(-1), + requested_menu_position_(POSITION_BEST_FIT), + actual_menu_position_(requested_menu_position_) { + Init(parent, command, type, NULL); +} + +MenuItemView::~MenuItemView() { + delete submenu_; + STLDeleteElements(&removed_items_); +} + +std::string MenuItemView::GetClassName() const { + return kViewClassName; +} + +// Calculates all sizes that we can from the OS. +// +// This is invoked prior to Running a menu. +void MenuItemView::UpdateMenuPartSizes(bool has_icons) { + MenuConfig::Reset(); + const MenuConfig& config = MenuConfig::instance(); + + item_right_margin_ = config.label_to_arrow_padding + config.arrow_width + + config.arrow_to_edge_padding; + + if (has_icons) { + label_start_ = config.item_left_margin + config.check_width + + config.icon_to_label_padding; + } else { + // If there are no icons don't pad by the icon to label padding. This + // makes us look close to system menus. + label_start_ = config.item_left_margin + config.check_width; + } + if (config.render_gutter) + label_start_ += config.gutter_width + config.gutter_to_label; + + MenuItemView menu_item(NULL); + menu_item.SetTitle(ASCIIToUTF16("blah")); // Text doesn't matter here. + pref_menu_height_ = menu_item.GetPreferredSize().height(); +} + +void MenuItemView::Init(MenuItemView* parent, + int command, + MenuItemView::Type type, + MenuDelegate* delegate) { + delegate_ = delegate; + controller_ = NULL; + canceled_ = false; + parent_menu_item_ = parent; + type_ = type; + selected_ = false; + command_ = command; + submenu_ = NULL; + show_mnemonics_ = false; + // Assign our ID, this allows SubmenuItemView to find MenuItemViews. + set_id(kMenuItemViewID); + has_icons_ = false; + + // Don't request enabled status from the root menu item as it is just + // a container for real items. EMPTY items will be disabled. + MenuDelegate* root_delegate = GetDelegate(); + if (parent && type != EMPTY && root_delegate) + SetEnabled(root_delegate->IsCommandEnabled(command)); +} + +void MenuItemView::PrepareForRun(bool has_mnemonics, bool show_mnemonics) { + // Currently we only support showing the root. + DCHECK(!parent_menu_item_); + + // Force us to have a submenu. + CreateSubmenu(); + actual_menu_position_ = requested_menu_position_; + canceled_ = false; + + has_mnemonics_ = has_mnemonics; + show_mnemonics_ = has_mnemonics && show_mnemonics; + + AddEmptyMenus(); + + if (!MenuController::GetActiveInstance()) { + // Only update the menu size if there are no menus showing, otherwise + // things may shift around. + UpdateMenuPartSizes(has_icons_); + } +} + +int MenuItemView::GetDrawStringFlags() { + int flags = 0; + if (base::i18n::IsRTL()) + flags |= gfx::Canvas::TEXT_ALIGN_RIGHT; + else + flags |= gfx::Canvas::TEXT_ALIGN_LEFT; + + if (has_mnemonics_) { + if (MenuConfig::instance().show_mnemonics || + GetRootMenuItem()->show_mnemonics_) { + flags |= gfx::Canvas::SHOW_PREFIX; + } else { + flags |= gfx::Canvas::HIDE_PREFIX; + } + } + return flags; +} + +const gfx::Font& MenuItemView::GetFont() { + // Check for item-specific font. + const MenuDelegate* delegate = GetDelegate(); + return delegate ? + delegate->GetLabelFont(GetCommand()) : MenuConfig::instance().font; +} + +void MenuItemView::AddEmptyMenus() { + DCHECK(HasSubmenu()); + if (!submenu_->has_children()) { + submenu_->AddChildViewAt(new EmptyMenuMenuItem(this), 0); + } else { + for (int i = 0, item_count = submenu_->GetMenuItemCount(); i < item_count; + ++i) { + MenuItemView* child = submenu_->GetMenuItemAt(i); + if (child->HasSubmenu()) + child->AddEmptyMenus(); + } + } +} + +void MenuItemView::RemoveEmptyMenus() { + DCHECK(HasSubmenu()); + // Iterate backwards as we may end up removing views, which alters the child + // view count. + for (int i = submenu_->child_count() - 1; i >= 0; --i) { + View* child = submenu_->child_at(i); + if (child->id() == MenuItemView::kMenuItemViewID) { + MenuItemView* menu_item = static_cast<MenuItemView*>(child); + if (menu_item->HasSubmenu()) + menu_item->RemoveEmptyMenus(); + } else if (child->id() == EmptyMenuMenuItem::kEmptyMenuItemViewID) { + submenu_->RemoveChildView(child); + delete child; + child = NULL; + } + } +} + +void MenuItemView::AdjustBoundsForRTLUI(gfx::Rect* rect) const { + rect->set_x(GetMirroredXForRect(*rect)); +} + +void MenuItemView::PaintAccelerator(gfx::Canvas* canvas) { + string16 accel_text = GetAcceleratorText(); + if (accel_text.empty()) + return; + + const gfx::Font& font = GetFont(); + int available_height = height() - GetTopMargin() - GetBottomMargin(); + int max_accel_width = + parent_menu_item_->GetSubmenu()->max_accelerator_width(); + gfx::Rect accel_bounds(width() - item_right_margin_ - max_accel_width, + GetTopMargin(), max_accel_width, available_height); + accel_bounds.set_x(GetMirroredXForRect(accel_bounds)); + int flags = GetRootMenuItem()->GetDrawStringFlags() | + gfx::Canvas::TEXT_VALIGN_MIDDLE; + flags &= ~(gfx::Canvas::TEXT_ALIGN_RIGHT | gfx::Canvas::TEXT_ALIGN_LEFT); + if (base::i18n::IsRTL()) + flags |= gfx::Canvas::TEXT_ALIGN_LEFT; + else + flags |= gfx::Canvas::TEXT_ALIGN_RIGHT; + canvas->DrawStringInt( + accel_text, font, TextButton::kDisabledColor, + accel_bounds.x(), accel_bounds.y(), accel_bounds.width(), + accel_bounds.height(), flags); +} + +void MenuItemView::DestroyAllMenuHosts() { + if (!HasSubmenu()) + return; + + submenu_->Close(); + for (int i = 0, item_count = submenu_->GetMenuItemCount(); i < item_count; + ++i) { + submenu_->GetMenuItemAt(i)->DestroyAllMenuHosts(); + } +} + +int MenuItemView::GetTopMargin() { + if (top_margin_ >= 0) + return top_margin_; + + MenuItemView* root = GetRootMenuItem(); + return root && root->has_icons_ + ? MenuConfig::instance().item_top_margin : + MenuConfig::instance().item_no_icon_top_margin; +} + +int MenuItemView::GetBottomMargin() { + if (bottom_margin_ >= 0) + return bottom_margin_; + + MenuItemView* root = GetRootMenuItem(); + return root && root->has_icons_ + ? MenuConfig::instance().item_bottom_margin : + MenuConfig::instance().item_no_icon_bottom_margin; +} + +gfx::Size MenuItemView::GetChildPreferredSize() { + if (!has_children()) + return gfx::Size(); + + if (IsContainer()) { + View* child = child_at(0); + return child->GetPreferredSize(); + } + + int width = 0; + for (int i = 0; i < child_count(); ++i) { + if (i) + width += kChildXPadding; + width += child_at(i)->GetPreferredSize().width(); + } + // Return a height of 0 to indicate that we should use the title height + // instead. + return gfx::Size(width, 0); +} + +gfx::Size MenuItemView::CalculatePreferredSize() { + gfx::Size child_size = GetChildPreferredSize(); + if (IsContainer()) { + return gfx::Size( + child_size.width(), + child_size.height() + GetBottomMargin() + GetTopMargin()); + } + + const gfx::Font& font = GetFont(); + int height = font.GetHeight(); + return gfx::Size( + font.GetStringWidth(title_) + label_start_ + + item_right_margin_ + child_size.width(), + std::max(height, child_size.height()) + GetBottomMargin() + + GetTopMargin()); +} + +string16 MenuItemView::GetAcceleratorText() { + if (id() == kEmptyMenuItemViewID) { + // Don't query the delegate for menus that represent no children. + return string16(); + } + + if(!MenuConfig::instance().show_accelerators) + return string16(); + + ui::Accelerator accelerator; + return (GetDelegate() && + GetDelegate()->GetAccelerator(GetCommand(), &accelerator)) ? + accelerator.GetShortcutText() : string16(); +} + +bool MenuItemView::IsContainer() const { + // Let the first child take over |this| when we only have one child and no + // title. Note that what child_count() returns is the number of children, + // not the number of menu items. + return child_count() == 1 && title_.empty(); +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_item_view.h b/ui/views/controls/menu/menu_item_view.h new file mode 100644 index 0000000..28481e8 --- /dev/null +++ b/ui/views/controls/menu/menu_item_view.h @@ -0,0 +1,473 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_ITEM_VIEW_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_ITEM_VIEW_H_ +#pragma once + +#include <string> +#include <vector> + +#include "base/compiler_specific.h" +#include "base/logging.h" +#include "base/string16.h" +#include "build/build_config.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "views/view.h" + +#if defined(OS_WIN) +#include <windows.h> + +#include "ui/gfx/native_theme.h" +#endif + +namespace gfx { +class Font; +} + +namespace ui { +class MenuModel; +} + +namespace views { + +namespace internal { +class MenuRunnerImpl; +} + +struct MenuConfig; +class MenuController; +class MenuDelegate; +class SubmenuView; + +// MenuItemView -------------------------------------------------------------- + +// MenuItemView represents a single menu item with a label and optional icon. +// Each MenuItemView may also contain a submenu, which in turn may contain +// any number of child MenuItemViews. +// +// To use a menu create an initial MenuItemView using the constructor that +// takes a MenuDelegate, then create any number of child menu items by way +// of the various AddXXX methods. +// +// MenuItemView is itself a View, which means you can add Views to each +// MenuItemView. This is normally NOT want you want, rather add other child +// Views to the submenu of the MenuItemView. Any child views of the MenuItemView +// that are focusable can be navigated to by way of the up/down arrow and can be +// activated by way of space/return keys. Activating a focusable child results +// in |AcceleratorPressed| being invoked. Note, that as menus try not to steal +// focus from the hosting window child views do not actually get focus. Instead +// |SetHotTracked| is used as the user navigates around. +// +// To show the menu use MenuRunner. See MenuRunner for details on how to run +// (show) the menu as well as for details on the life time of the menu. + +class VIEWS_EXPORT MenuItemView : public View { + public: + friend class MenuController; + + // The menu item view's class name. + static const char kViewClassName[]; + + // ID used to identify menu items. + static const int kMenuItemViewID; + + // ID used to identify empty menu items. + static const int kEmptyMenuItemViewID; + + // Different types of menu items. EMPTY is a special type for empty + // menus that is only used internally. + enum Type { + NORMAL, + SUBMENU, + CHECKBOX, + RADIO, + SEPARATOR, + EMPTY + }; + + // Where the menu should be anchored to for non-RTL languages. The + // opposite position will be used if base::i18n:IsRTL() is true. + enum AnchorPosition { + TOPLEFT, + TOPRIGHT + }; + + // Where the menu should be drawn, above or below the bounds (when + // the bounds is non-empty). POSITION_BEST_FIT (default) positions + // the menu below the bounds unless the menu does not fit on the + // screen and the re is more space above. + enum MenuPosition { + POSITION_BEST_FIT, + POSITION_ABOVE_BOUNDS, + POSITION_BELOW_BOUNDS + }; + + // Constructor for use with the top level menu item. This menu is never + // shown to the user, rather its use as the parent for all menu items. + explicit MenuItemView(MenuDelegate* delegate); + + // Overridden from View: + virtual bool GetTooltipText(const gfx::Point& p, + string16* tooltip) const OVERRIDE; + virtual void GetAccessibleState(ui::AccessibleViewState* state) OVERRIDE; + + // Returns the preferred height of menu items. This is only valid when the + // menu is about to be shown. + static int pref_menu_height() { return pref_menu_height_; } + + // X-coordinate of where the label starts. + static int label_start() { return label_start_; } + + // Returns the accessible name to be used with screen readers. Mnemonics are + // removed and the menu item accelerator text is appended. + static string16 GetAccessibleNameForMenuItem( + const string16& item_text, const string16& accelerator_text); + + // Hides and cancels the menu. This does nothing if the menu is not open. + void Cancel(); + + // Add an item to the menu at a specified index. ChildrenChanged() should + // called after adding menu items if the menu may be active. + MenuItemView* AddMenuItemAt(int index, + int item_id, + const string16& label, + const SkBitmap& icon, + Type type); + + // Remove an item from the menu at a specified index. + // ChildrenChanged() should be called after removing menu items (whether + // the menu may be active or not). + void RemoveMenuItemAt(int index); + + // Appends an item to this menu. + // item_id The id of the item, used to identify it in delegate callbacks + // or (if delegate is NULL) to identify the command associated + // with this item with the controller specified in the ctor. Note + // that this value should not be 0 as this has a special meaning + // ("NULL command, no item selected") + // label The text label shown. + // type The type of item. + MenuItemView* AppendMenuItem(int item_id, + const string16& label, + Type type); + + // Append a submenu to this menu. + // The returned pointer is owned by this menu. + MenuItemView* AppendSubMenu(int item_id, + const string16& label); + + // Append a submenu with an icon to this menu. + // The returned pointer is owned by this menu. + MenuItemView* AppendSubMenuWithIcon(int item_id, + const string16& label, + const SkBitmap& icon); + + // This is a convenience for standard text label menu items where the label + // is provided with this call. + MenuItemView* AppendMenuItemWithLabel(int item_id, + const string16& label); + + // This is a convenience for text label menu items where the label is + // provided by the delegate. + MenuItemView* AppendDelegateMenuItem(int item_id); + + // Adds a separator to this menu + void AppendSeparator(); + + // Appends a menu item with an icon. This is for the menu item which + // needs an icon. Calling this function forces the Menu class to draw + // the menu, instead of relying on Windows. + MenuItemView* AppendMenuItemWithIcon(int item_id, + const string16& label, + const SkBitmap& icon); + + // Creates a menu item for the specified entry in the model and appends it as + // a child. |index| should be offset by GetFirstItemIndex() before calling + // this function. + MenuItemView* AppendMenuItemFromModel(ui::MenuModel* model, + int index, + int id); + + // All the AppendXXX methods funnel into this. + MenuItemView* AppendMenuItemImpl(int item_id, + const string16& label, + const SkBitmap& icon, + Type type); + + // Returns the view that contains child menu items. If the submenu has + // not been creates, this creates it. + virtual SubmenuView* CreateSubmenu(); + + // Returns true if this menu item has a submenu. + virtual bool HasSubmenu() const; + + // Returns the view containing child menu items. + virtual SubmenuView* GetSubmenu() const; + + // Returns the parent menu item. + MenuItemView* GetParentMenuItem() { return parent_menu_item_; } + const MenuItemView* GetParentMenuItem() const { return parent_menu_item_; } + + // Sets/Gets the title. + void SetTitle(const string16& title); + const string16& title() const { return title_; } + + // Returns the type of this menu. + const Type& GetType() const { return type_; } + + // Sets whether this item is selected. This is invoked as the user moves + // the mouse around the menu while open. + void SetSelected(bool selected); + + // Returns true if the item is selected. + bool IsSelected() const { return selected_; } + + // Sets the |tooltip| for a menu item view with |item_id| identifier. + void SetTooltip(const string16& tooltip, int item_id); + + // Sets the icon for the descendant identified by item_id. + void SetIcon(const SkBitmap& icon, int item_id); + + // Sets the icon of this menu item. + void SetIcon(const SkBitmap& icon); + + // Returns the icon. + const SkBitmap& GetIcon() const { return icon_; } + + // Sets the command id of this menu item. + void SetCommand(int command) { command_ = command; } + + // Returns the command id of this item. + int GetCommand() const { return command_; } + + // Paints the menu item. + virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE; + + // Returns the preferred size of this item. + virtual gfx::Size GetPreferredSize() OVERRIDE; + + // Returns the object responsible for controlling showing the menu. + MenuController* GetMenuController(); + const MenuController* GetMenuController() const; + + // Returns the delegate. This returns the delegate of the root menu item. + MenuDelegate* GetDelegate(); + const MenuDelegate* GetDelegate() const; + void set_delegate(MenuDelegate* delegate) { delegate_ = delegate; } + + // Returns the root parent, or this if this has no parent. + MenuItemView* GetRootMenuItem(); + const MenuItemView* GetRootMenuItem() const; + + // Returns the mnemonic for this MenuItemView, or 0 if this MenuItemView + // doesn't have a mnemonic. + char16 GetMnemonic(); + + // Do we have icons? This only has effect on the top menu. Turning this on + // makes the menus slightly wider and taller. + void set_has_icons(bool has_icons) { + has_icons_ = has_icons; + } + + // Returns the descendant with the specified command. + MenuItemView* GetMenuItemByID(int id); + + // Invoke if you remove/add children to the menu while it's showing. This + // recalculates the bounds. + void ChildrenChanged(); + + // Sizes any child views. + virtual void Layout() OVERRIDE; + + // Returns the amount of space needed to accomodate the accelerator. The + // space needed for the accelerator is NOT included in the preferred width. + int GetAcceleratorTextWidth(); + + // Returns true if the menu has mnemonics. This only useful on the root menu + // item. + bool has_mnemonics() const { return has_mnemonics_; } + + // Set top and bottom margins in pixels. If no margin is set or a + // negative margin is specified then MenuConfig values are used. + void SetMargins(int top_margin, int bottom_margin); + + // Set the position of the menu with respect to the bounds (top + // level only). + void set_menu_position(MenuPosition menu_position) { + requested_menu_position_ = menu_position; + } + + protected: + // Creates a MenuItemView. This is used by the various AddXXX methods. + MenuItemView(MenuItemView* parent, int command, Type type); + + // MenuRunner owns MenuItemView and should be the only one deleting it. + virtual ~MenuItemView(); + + virtual void ChildPreferredSizeChanged(View* child) OVERRIDE; + + virtual std::string GetClassName() const OVERRIDE; + + private: + friend class internal::MenuRunnerImpl; // For access to ~MenuItemView. + + // Calculates all sizes that we can from the OS. + // + // This is invoked prior to Running a menu. + static void UpdateMenuPartSizes(bool has_icons); + + // Called by the two constructors to initialize this menu item. + void Init(MenuItemView* parent, + int command, + MenuItemView::Type type, + MenuDelegate* delegate); + + // The RunXXX methods call into this to set up the necessary state before + // running. + void PrepareForRun(bool has_mnemonics, bool show_mnemonics); + + // Returns the flags passed to DrawStringInt. + int GetDrawStringFlags(); + + // Returns the font to use for menu text. + const gfx::Font& GetFont(); + + // If this menu item has no children a child is added showing it has no + // children. Otherwise AddEmtpyMenus is recursively invoked on child menu + // items that have children. + void AddEmptyMenus(); + + // Undoes the work of AddEmptyMenus. + void RemoveEmptyMenus(); + + // Given bounds within our View, this helper routine mirrors the bounds if + // necessary. + void AdjustBoundsForRTLUI(gfx::Rect* rect) const; + + // Actual paint implementation. If mode is PB_FOR_DRAG, portions of the menu + // are not rendered. + enum PaintButtonMode { PB_NORMAL, PB_FOR_DRAG }; + void PaintButton(gfx::Canvas* canvas, PaintButtonMode mode); + +#if defined(OS_WIN) + enum SelectionState { SELECTED, UNSELECTED }; + + // Paints the check/radio button indicator. + void PaintCheck(gfx::Canvas* canvas, + gfx::NativeTheme::State state, + SelectionState selection_state, + const MenuConfig& config); +#endif + + // Paints the accelerator. + void PaintAccelerator(gfx::Canvas* canvas); + + // Destroys the window used to display this menu and recursively destroys + // the windows used to display all descendants. + void DestroyAllMenuHosts(); + + // Returns the accelerator text. + string16 GetAcceleratorText(); + + // Returns the various margins. + int GetTopMargin(); + int GetBottomMargin(); + + // Returns the preferred size (and padding) of any children. + gfx::Size GetChildPreferredSize(); + + // Calculates the preferred size. + gfx::Size CalculatePreferredSize(); + + // Used by MenuController to cache the menu position in use by the + // active menu. + MenuPosition actual_menu_position() const { return actual_menu_position_; } + void set_actual_menu_position(MenuPosition actual_menu_position) { + actual_menu_position_ = actual_menu_position; + } + + void set_controller(MenuController* controller) { controller_ = controller; } + + // Returns true if this MenuItemView contains a single child + // that is responsible for rendering the content. + bool IsContainer() const; + + // The delegate. This is only valid for the root menu item. You shouldn't + // use this directly, instead use GetDelegate() which walks the tree as + // as necessary. + MenuDelegate* delegate_; + + // The controller for the run operation, or NULL if the menu isn't showing. + MenuController* controller_; + + // Used to detect when Cancel was invoked. + bool canceled_; + + // Our parent. + MenuItemView* parent_menu_item_; + + // Type of menu. NOTE: MenuItemView doesn't itself represent SEPARATOR, + // that is handled by an entirely different view class. + Type type_; + + // Whether we're selected. + bool selected_; + + // Command id. + int command_; + + // Submenu, created via CreateSubmenu. + SubmenuView* submenu_; + + // Title. + string16 title_; + + // Icon. + SkBitmap icon_; + + // Does the title have a mnemonic? Only useful on the root menu item. + bool has_mnemonics_; + + // Should we show the mnemonic? Mnemonics are shown if this is true or + // MenuConfig says mnemonics should be shown. Only used on the root menu item. + bool show_mnemonics_; + + bool has_icons_; + + // The tooltip to show on hover for this menu item. + string16 tooltip_; + + // X-coordinate of where the label starts. + static int label_start_; + + // Margins between the right of the item and the label. + static int item_right_margin_; + + // Preferred height of menu items. Reset every time a menu is run. + static int pref_menu_height_; + + // Previously calculated preferred size to reduce GetStringWidth calls in + // GetPreferredSize. + gfx::Size pref_size_; + + // Removed items to be deleted in ChildrenChanged(). + std::vector<View*> removed_items_; + + // Margins in pixels. + int top_margin_; + int bottom_margin_; + + // |menu_position_| is the requested position with respect to the bounds. + // |actual_menu_position_| is used by the controller to cache the + // position of the menu being shown. + MenuPosition requested_menu_position_; + MenuPosition actual_menu_position_; + + DISALLOW_COPY_AND_ASSIGN(MenuItemView); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_ITEM_VIEW_H_ diff --git a/ui/views/controls/menu/menu_item_view_aura.cc b/ui/views/controls/menu/menu_item_view_aura.cc new file mode 100644 index 0000000..33a8dff --- /dev/null +++ b/ui/views/controls/menu/menu_item_view_aura.cc @@ -0,0 +1,107 @@ +// 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 "ui/views/controls/menu/menu_item_view.h" + +#include "base/utf_string_conversions.h" +#include "grit/ui_resources.h" +#include "third_party/skia/include/effects/SkGradientShader.h" +#include "ui/base/resource/resource_bundle.h" +#include "ui/gfx/canvas_skia.h" +#include "ui/gfx/favicon_size.h" +#include "ui/views/controls/button/text_button.h" +#include "ui/views/controls/menu/menu_config.h" +#include "ui/views/controls/menu/menu_image_util.h" +#include "ui/views/controls/menu/submenu_view.h" + +namespace views { + +// Background color when the menu item is selected. +static const SkColor kSelectedBackgroundColor = SkColorSetRGB(246, 249, 253); + +void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) { + const MenuConfig& config = MenuConfig::instance(); + bool render_selection = + (mode == PB_NORMAL && IsSelected() && + parent_menu_item_->GetSubmenu()->GetShowSelection(this) && + !has_children()); + + int icon_x = config.item_left_margin; + int top_margin = GetTopMargin(); + int bottom_margin = GetBottomMargin(); + int icon_y = top_margin + (height() - config.item_top_margin - + bottom_margin - config.check_height) / 2; + int icon_height = config.check_height; + int available_height = height() - top_margin - bottom_margin; + + // Render the background. As MenuScrollViewContainer draws the background, we + // only need the background when we want it to look different, as when we're + // selected. + if (render_selection) + canvas->GetSkCanvas()->drawColor(kSelectedBackgroundColor, + SkXfermode::kSrc_Mode); + + // Render the check. + if (type_ == CHECKBOX && GetDelegate()->IsItemChecked(GetCommand())) { + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + SkBitmap* check = rb.GetBitmapNamed(IDR_MENU_CHECK); + // Don't use config.check_width here as it's padded to force more padding. + gfx::Rect check_bounds(icon_x, icon_y, check->width(), icon_height); + AdjustBoundsForRTLUI(&check_bounds); + canvas->DrawBitmapInt(*check, check_bounds.x(), check_bounds.y()); + } else if (type_ == RADIO) { + const SkBitmap* image = + GetRadioButtonImage(GetDelegate()->IsItemChecked(GetCommand())); + gfx::Rect radio_bounds(icon_x, + top_margin + + (height() - top_margin - bottom_margin - + image->height()) / 2, + image->width(), + image->height()); + AdjustBoundsForRTLUI(&radio_bounds); + canvas->DrawBitmapInt(*image, radio_bounds.x(), radio_bounds.y()); + } + + // Render the foreground. + SkColor fg_color = + IsEnabled() ? TextButton::kEnabledColor : TextButton::kDisabledColor; + const gfx::Font& font = GetFont(); + int accel_width = parent_menu_item_->GetSubmenu()->max_accelerator_width(); + int width = this->width() - item_right_margin_ - label_start_ - accel_width; + gfx::Rect text_bounds(label_start_, top_margin + + (available_height - font.GetHeight()) / 2, width, + font.GetHeight()); + text_bounds.set_x(GetMirroredXForRect(text_bounds)); + canvas->DrawStringInt(title(), font, fg_color, + text_bounds.x(), text_bounds.y(), text_bounds.width(), + text_bounds.height(), + GetRootMenuItem()->GetDrawStringFlags()); + + PaintAccelerator(canvas); + + // Render the icon. + if (icon_.width() > 0) { + gfx::Rect icon_bounds(config.item_left_margin, + top_margin + (height() - top_margin - + bottom_margin - icon_.height()) / 2, + icon_.width(), + icon_.height()); + icon_bounds.set_x(GetMirroredXForRect(icon_bounds)); + canvas->DrawBitmapInt(icon_, icon_bounds.x(), icon_bounds.y()); + } + + // Render the submenu indicator (arrow). + if (HasSubmenu()) { + gfx::Rect arrow_bounds(this->width() - item_right_margin_ + + config.label_to_arrow_padding, + top_margin + (available_height - + config.arrow_width) / 2, + config.arrow_width, height()); + AdjustBoundsForRTLUI(&arrow_bounds); + canvas->DrawBitmapInt(*GetSubmenuArrowImage(), + arrow_bounds.x(), arrow_bounds.y()); + } +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_item_view_linux.cc b/ui/views/controls/menu/menu_item_view_linux.cc new file mode 100644 index 0000000..a9e738d --- /dev/null +++ b/ui/views/controls/menu/menu_item_view_linux.cc @@ -0,0 +1,116 @@ +// 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 "ui/views/controls/menu/menu_item_view.h" + +#include "base/utf_string_conversions.h" +#include "grit/ui_resources.h" +#include "third_party/skia/include/effects/SkGradientShader.h" +#include "ui/base/resource/resource_bundle.h" +#include "ui/gfx/canvas_skia.h" +#include "ui/gfx/favicon_size.h" +#include "ui/views/controls/button/text_button.h" +#include "ui/views/controls/menu/menu_config.h" +#include "ui/views/controls/menu/menu_image_util.h" +#include "ui/views/controls/menu/submenu_view.h" + +namespace views { + +// Background color when the menu item is selected. +#if defined(OS_CHROMEOS) +static const SkColor kSelectedBackgroundColor = SkColorSetRGB(0xDC, 0xE4, 0xFA); +#else +static const SkColor kSelectedBackgroundColor = SkColorSetRGB(246, 249, 253); +#endif + +void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) { + const MenuConfig& config = MenuConfig::instance(); + bool render_selection = + (mode == PB_NORMAL && IsSelected() && + parent_menu_item_->GetSubmenu()->GetShowSelection(this) && + !has_children()); + + int icon_x = config.item_left_margin; + int top_margin = GetTopMargin(); + int bottom_margin = GetBottomMargin(); + int icon_y = top_margin + (height() - config.item_top_margin - + bottom_margin - config.check_height) / 2; + int icon_height = config.check_height; + int available_height = height() - top_margin - bottom_margin; + + // Render the background. As MenuScrollViewContainer draws the background, we + // only need the background when we want it to look different, as when we're + // selected. + if (render_selection) + canvas->GetSkCanvas()->drawColor(kSelectedBackgroundColor, + SkXfermode::kSrc_Mode); + + // Render the check. + if (type_ == CHECKBOX && GetDelegate()->IsItemChecked(GetCommand())) { + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + SkBitmap* check = rb.GetBitmapNamed(IDR_MENU_CHECK); + // Don't use config.check_width here as it's padded to force more padding. + gfx::Rect check_bounds(icon_x, icon_y, check->width(), icon_height); + AdjustBoundsForRTLUI(&check_bounds); + canvas->DrawBitmapInt(*check, check_bounds.x(), check_bounds.y()); + } else if (type_ == RADIO) { + const SkBitmap* image = + GetRadioButtonImage(GetDelegate()->IsItemChecked(GetCommand())); + gfx::Rect radio_bounds(icon_x, + top_margin + + (height() - top_margin - bottom_margin - + image->height()) / 2, + image->width(), + image->height()); + AdjustBoundsForRTLUI(&radio_bounds); + canvas->DrawBitmapInt(*image, radio_bounds.x(), radio_bounds.y()); + } + + // Render the foreground. +#if defined(OS_CHROMEOS) + SkColor fg_color = + IsEnabled() ? SK_ColorBLACK : SkColorSetRGB(0x80, 0x80, 0x80); +#else + SkColor fg_color = + IsEnabled() ? TextButton::kEnabledColor : TextButton::kDisabledColor; +#endif + const gfx::Font& font = GetFont(); + int accel_width = parent_menu_item_->GetSubmenu()->max_accelerator_width(); + int width = this->width() - item_right_margin_ - label_start_ - accel_width; + gfx::Rect text_bounds(label_start_, top_margin + + (available_height - font.GetHeight()) / 2, width, + font.GetHeight()); + text_bounds.set_x(GetMirroredXForRect(text_bounds)); + canvas->DrawStringInt(title(), font, fg_color, + text_bounds.x(), text_bounds.y(), text_bounds.width(), + text_bounds.height(), + GetRootMenuItem()->GetDrawStringFlags()); + + PaintAccelerator(canvas); + + // Render the icon. + if (icon_.width() > 0) { + gfx::Rect icon_bounds(config.item_left_margin, + top_margin + (height() - top_margin - + bottom_margin - icon_.height()) / 2, + icon_.width(), + icon_.height()); + icon_bounds.set_x(GetMirroredXForRect(icon_bounds)); + canvas->DrawBitmapInt(icon_, icon_bounds.x(), icon_bounds.y()); + } + + // Render the submenu indicator (arrow). + if (HasSubmenu()) { + gfx::Rect arrow_bounds(this->width() - item_right_margin_ + + config.label_to_arrow_padding, + top_margin + (available_height - + config.arrow_width) / 2, + config.arrow_width, height()); + AdjustBoundsForRTLUI(&arrow_bounds); + canvas->DrawBitmapInt(*GetSubmenuArrowImage(), + arrow_bounds.x(), arrow_bounds.y()); + } +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_item_view_win.cc b/ui/views/controls/menu/menu_item_view_win.cc new file mode 100644 index 0000000..b50a295 --- /dev/null +++ b/ui/views/controls/menu/menu_item_view_win.cc @@ -0,0 +1,171 @@ +// 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 "ui/views/controls/menu/menu_item_view.h" + +#include <uxtheme.h> +#include <Vssym32.h> + +#include "grit/ui_strings.h" +#include "ui/gfx/canvas_skia.h" +#include "ui/gfx/native_theme_win.h" +#include "ui/views/controls/menu/menu_config.h" +#include "ui/views/controls/menu/submenu_view.h" + +using gfx::NativeTheme; + +namespace views { + +void MenuItemView::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) { + const MenuConfig& config = MenuConfig::instance(); + bool render_selection = + (mode == PB_NORMAL && IsSelected() && + parent_menu_item_->GetSubmenu()->GetShowSelection(this) && + !has_children()); + int default_sys_color; + int state; + NativeTheme::State control_state; + + if (IsEnabled()) { + if (render_selection) { + control_state = NativeTheme::kHovered; + state = MPI_HOT; + default_sys_color = COLOR_HIGHLIGHTTEXT; + } else { + control_state = NativeTheme::kNormal; + state = MPI_NORMAL; + default_sys_color = COLOR_MENUTEXT; + } + } else { + state = MPI_DISABLED; + default_sys_color = COLOR_GRAYTEXT; + control_state = NativeTheme::kDisabled; + } + + // The gutter is rendered before the background. + if (config.render_gutter && mode == PB_NORMAL) { + gfx::Rect gutter_bounds(label_start_ - config.gutter_to_label - + config.gutter_width, 0, config.gutter_width, + height()); + AdjustBoundsForRTLUI(&gutter_bounds); + NativeTheme::ExtraParams extra; + NativeTheme::instance()->Paint(canvas->GetSkCanvas(), + NativeTheme::kMenuPopupGutter, + NativeTheme::kNormal, + gutter_bounds, + extra); + } + + // Render the background. + if (mode == PB_NORMAL) { + gfx::Rect item_bounds(0, 0, width(), height()); + NativeTheme::ExtraParams extra; + extra.menu_item.is_selected = render_selection; + AdjustBoundsForRTLUI(&item_bounds); + NativeTheme::instance()->Paint(canvas->GetSkCanvas(), + NativeTheme::kMenuItemBackground, control_state, item_bounds, extra); + } + + int top_margin = GetTopMargin(); + int bottom_margin = GetBottomMargin(); + + if ((type_ == RADIO || type_ == CHECKBOX) && + GetDelegate()->IsItemChecked(GetCommand())) { + PaintCheck(canvas, control_state, render_selection ? SELECTED : UNSELECTED, + config); + } + + // Render the foreground. + // Menu color is specific to Vista, fallback to classic colors if can't + // get color. + SkColor fg_color = gfx::NativeThemeWin::instance()->GetThemeColorWithDefault( + gfx::NativeThemeWin::MENU, MENU_POPUPITEM, state, TMT_TEXTCOLOR, + default_sys_color); + const gfx::Font& font = GetFont(); + int accel_width = parent_menu_item_->GetSubmenu()->max_accelerator_width(); + int width = this->width() - item_right_margin_ - label_start_ - accel_width; + gfx::Rect text_bounds(label_start_, top_margin, width, font.GetHeight()); + text_bounds.set_x(GetMirroredXForRect(text_bounds)); + if (mode == PB_FOR_DRAG) { + // With different themes, it's difficult to tell what the correct + // foreground and background colors are for the text to draw the correct + // halo. Instead, just draw black on white, which will look good in most + // cases. + canvas->AsCanvasSkia()->DrawStringWithHalo( + title(), font, 0x00000000, 0xFFFFFFFF, text_bounds.x(), + text_bounds.y(), text_bounds.width(), text_bounds.height(), + GetRootMenuItem()->GetDrawStringFlags()); + } else { + canvas->DrawStringInt(title(), font, fg_color, + text_bounds.x(), text_bounds.y(), text_bounds.width(), + text_bounds.height(), + GetRootMenuItem()->GetDrawStringFlags()); + } + + PaintAccelerator(canvas); + + if (icon_.width() > 0) { + gfx::Rect icon_bounds(config.item_left_margin, + top_margin + (height() - top_margin - + bottom_margin - icon_.height()) / 2, + icon_.width(), + icon_.height()); + icon_bounds.set_x(GetMirroredXForRect(icon_bounds)); + canvas->DrawBitmapInt(icon_, icon_bounds.x(), icon_bounds.y()); + } + + if (HasSubmenu()) { + int state_id = IsEnabled() ? MSM_NORMAL : MSM_DISABLED; + gfx::Rect arrow_bounds(this->width() - item_right_margin_ + + config.label_to_arrow_padding, 0, + config.arrow_width, height()); + AdjustBoundsForRTLUI(&arrow_bounds); + + // If our sub menus open from right to left (which is the case when the + // locale is RTL) then we should make sure the menu arrow points to the + // right direction. + gfx::NativeTheme::ExtraParams extra; + extra.menu_arrow.pointing_right = !base::i18n::IsRTL(); + extra.menu_arrow.is_selected = render_selection; + gfx::NativeTheme::instance()->Paint(canvas->GetSkCanvas(), + gfx::NativeTheme::kMenuPopupArrow, control_state, arrow_bounds, extra); + } +} + +void MenuItemView::PaintCheck(gfx::Canvas* canvas, + NativeTheme::State state, + SelectionState selection_state, + const MenuConfig& config) { + int icon_width; + int icon_height; + if (type_ == RADIO) { + icon_width = config.radio_width; + icon_height = config.radio_height; + } else { + icon_width = config.check_width; + icon_height = config.check_height; + } + + int top_margin = GetTopMargin(); + int icon_x = MenuConfig::instance().item_left_margin; + int icon_y = top_margin + + (height() - top_margin - GetBottomMargin() - icon_height) / 2; + NativeTheme::ExtraParams extra; + extra.menu_check.is_radio = type_ == RADIO; + extra.menu_check.is_selected = selection_state == SELECTED; + + // Draw the background. + gfx::Rect bg_bounds(0, 0, icon_x + icon_width, height()); + AdjustBoundsForRTLUI(&bg_bounds); + NativeTheme::instance()->Paint(canvas->GetSkCanvas(), + NativeTheme::kMenuCheckBackground, state, bg_bounds, extra); + + // And the check. + gfx::Rect icon_bounds(icon_x / 2, icon_y, icon_width, icon_height); + AdjustBoundsForRTLUI(&icon_bounds); + NativeTheme::instance()->Paint(canvas->GetSkCanvas(), + NativeTheme::kMenuCheck, state, bg_bounds, extra); +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_listener.h b/ui/views/controls/menu/menu_listener.h new file mode 100644 index 0000000..37eb8c0 --- /dev/null +++ b/ui/views/controls/menu/menu_listener.h @@ -0,0 +1,25 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_LISTENER_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_LISTENER_H_ +#pragma once + +#include "views/views_export.h" + +namespace views { + +// An interface for clients that want a notification when a menu is opened. +class VIEWS_EXPORT MenuListener { + public: + // This will be called after the menu has actually opened. + virtual void OnMenuOpened() = 0; + + protected: + virtual ~MenuListener() {} +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_LISTENER_H_ diff --git a/ui/views/controls/menu/menu_model_adapter.cc b/ui/views/controls/menu/menu_model_adapter.cc new file mode 100644 index 0000000..3848093 --- /dev/null +++ b/ui/views/controls/menu/menu_model_adapter.cc @@ -0,0 +1,200 @@ +// 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 "ui/views/controls/menu/menu_model_adapter.h" + +#include "base/logging.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/models/menu_model.h" +#include "ui/views/controls/menu/submenu_view.h" +#include "views/views_delegate.h" + +namespace views { + +MenuModelAdapter::MenuModelAdapter(ui::MenuModel* menu_model) + : menu_model_(menu_model), + triggerable_event_flags_(ui::EF_LEFT_BUTTON_DOWN | + ui::EF_RIGHT_BUTTON_DOWN) { + DCHECK(menu_model); +} + +MenuModelAdapter::~MenuModelAdapter() { +} + +void MenuModelAdapter::BuildMenu(MenuItemView* menu) { + DCHECK(menu); + + // Clear the menu. + if (menu->HasSubmenu()) { + const int subitem_count = menu->GetSubmenu()->child_count(); + for (int i = 0; i < subitem_count; ++i) + menu->RemoveMenuItemAt(0); + } + + // Leave entries in the map if the menu is being shown. This + // allows the map to find the menu model of submenus being closed + // so ui::MenuModel::MenuClosed() can be called. + if (!menu->GetMenuController()) + menu_map_.clear(); + menu_map_[menu] = menu_model_; + + // Repopulate the menu. + BuildMenuImpl(menu, menu_model_); + menu->ChildrenChanged(); +} + +MenuItemView* MenuModelAdapter::CreateMenu() { + MenuItemView* item = new MenuItemView(this); + BuildMenu(item); + return item; +} + +// MenuModelAdapter, MenuDelegate implementation: + +void MenuModelAdapter::ExecuteCommand(int id) { + ui::MenuModel* model = menu_model_; + int index = 0; + if (ui::MenuModel::GetModelAndIndexForCommandId(id, &model, &index)) { + model->ActivatedAt(index); + return; + } + + NOTREACHED(); +} + +void MenuModelAdapter::ExecuteCommand(int id, int mouse_event_flags) { + ui::MenuModel* model = menu_model_; + int index = 0; + if (ui::MenuModel::GetModelAndIndexForCommandId(id, &model, &index)) { + model->ActivatedAt(index, mouse_event_flags); + return; + } + + NOTREACHED(); +} + +bool MenuModelAdapter::IsTriggerableEvent(MenuItemView* source, + const MouseEvent& e) { + return (triggerable_event_flags_ & e.flags()) != 0; +} + +bool MenuModelAdapter::GetAccelerator(int id, + ui::Accelerator* accelerator) { + ui::MenuModel* model = menu_model_; + int index = 0; + if (ui::MenuModel::GetModelAndIndexForCommandId(id, &model, &index)) + return model->GetAcceleratorAt(index, accelerator); + + NOTREACHED(); + return false; +} + +string16 MenuModelAdapter::GetLabel(int id) const { + ui::MenuModel* model = menu_model_; + int index = 0; + if (ui::MenuModel::GetModelAndIndexForCommandId(id, &model, &index)) + return model->GetLabelAt(index); + + NOTREACHED(); + return string16(); +} + +const gfx::Font& MenuModelAdapter::GetLabelFont(int id) const { + ui::MenuModel* model = menu_model_; + int index = 0; + if (ui::MenuModel::GetModelAndIndexForCommandId(id, &model, &index)) { + const gfx::Font* font = model->GetLabelFontAt(index); + return font ? *font : MenuDelegate::GetLabelFont(id); + } + + // This line may be reached for the empty menu item. + return MenuDelegate::GetLabelFont(id); +} + +bool MenuModelAdapter::IsCommandEnabled(int id) const { + ui::MenuModel* model = menu_model_; + int index = 0; + if (ui::MenuModel::GetModelAndIndexForCommandId(id, &model, &index)) + return model->IsEnabledAt(index); + + NOTREACHED(); + return false; +} + +bool MenuModelAdapter::IsItemChecked(int id) const { + ui::MenuModel* model = menu_model_; + int index = 0; + if (ui::MenuModel::GetModelAndIndexForCommandId(id, &model, &index)) + return model->IsItemCheckedAt(index); + + NOTREACHED(); + return false; +} + +void MenuModelAdapter::SelectionChanged(MenuItemView* menu) { + // Ignore selection of the root menu. + if (menu == menu->GetRootMenuItem()) + return; + + const int id = menu->GetCommand(); + ui::MenuModel* model = menu_model_; + int index = 0; + if (ui::MenuModel::GetModelAndIndexForCommandId(id, &model, &index)) { + model->HighlightChangedTo(index); + return; + } + + NOTREACHED(); +} + +void MenuModelAdapter::WillShowMenu(MenuItemView* menu) { + // Look up the menu model for this menu. + const std::map<MenuItemView*, ui::MenuModel*>::const_iterator map_iterator = + menu_map_.find(menu); + if (map_iterator != menu_map_.end()) { + map_iterator->second->MenuWillShow(); + return; + } + + NOTREACHED(); +} + +void MenuModelAdapter::WillHideMenu(MenuItemView* menu) { + // Look up the menu model for this menu. + const std::map<MenuItemView*, ui::MenuModel*>::const_iterator map_iterator = + menu_map_.find(menu); + if (map_iterator != menu_map_.end()) { + map_iterator->second->MenuClosed(); + return; + } + + NOTREACHED(); +} + +// MenuModelAdapter, private: + +void MenuModelAdapter::BuildMenuImpl(MenuItemView* menu, ui::MenuModel* model) { + DCHECK(menu); + DCHECK(model); + const int item_count = model->GetItemCount(); + for (int i = 0; i < item_count; ++i) { + const int index = i + model->GetFirstItemIndex(NULL); + MenuItemView* item = menu->AppendMenuItemFromModel( + model, index, model->GetCommandIdAt(index)); + + if (model->GetTypeAt(index) == ui::MenuModel::TYPE_SUBMENU) { + DCHECK(item); + DCHECK_EQ(MenuItemView::SUBMENU, item->GetType()); + ui::MenuModel* submodel = model->GetSubmenuModelAt(index); + DCHECK(submodel); + BuildMenuImpl(item, submodel); + + menu_map_[item] = submodel; + } + } + + menu->set_has_icons(model->HasIcons()); +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_model_adapter.h b/ui/views/controls/menu/menu_model_adapter.h new file mode 100644 index 0000000..6bc9e51 --- /dev/null +++ b/ui/views/controls/menu/menu_model_adapter.h @@ -0,0 +1,80 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_MODEL_ADAPTER_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_MODEL_ADAPTER_H_ +#pragma once + +#include <map> + +#include "ui/views/controls/menu/menu_delegate.h" + +namespace ui { +class MenuModel; +} + +namespace views { +class MenuItemView; + +// This class wraps an instance of ui::MenuModel with the +// views::MenuDelegate interface required by views::MenuItemView. +class VIEWS_EXPORT MenuModelAdapter : public MenuDelegate { + public: + // The caller retains ownership of the ui::MenuModel instance and + // must ensure it exists for the lifetime of the adapter. + explicit MenuModelAdapter(ui::MenuModel* menu_model); + virtual ~MenuModelAdapter(); + + // Populate a MenuItemView menu with the ui::MenuModel items + // (including submenus). + virtual void BuildMenu(MenuItemView* menu); + + // Convenience for creating and populating a menu. The caller owns the + // returned MenuItemView. + MenuItemView* CreateMenu(); + + void set_triggerable_event_flags(int triggerable_event_flags) { + triggerable_event_flags_ = triggerable_event_flags; + } + int triggerable_event_flags() const { return triggerable_event_flags_; } + + protected: + // views::MenuDelegate implementation. + virtual void ExecuteCommand(int id) OVERRIDE; + virtual void ExecuteCommand(int id, int mouse_event_flags) OVERRIDE; + virtual bool IsTriggerableEvent(MenuItemView* source, + const MouseEvent& e) OVERRIDE; + virtual bool GetAccelerator(int id, + ui::Accelerator* accelerator) OVERRIDE; + virtual string16 GetLabel(int id) const OVERRIDE; + virtual const gfx::Font& GetLabelFont(int id) const OVERRIDE; + virtual bool IsCommandEnabled(int id) const OVERRIDE; + virtual bool IsItemChecked(int id) const OVERRIDE; + virtual void SelectionChanged(MenuItemView* menu) OVERRIDE; + virtual void WillShowMenu(MenuItemView* menu) OVERRIDE; + virtual void WillHideMenu(MenuItemView* menu) OVERRIDE; + + private: + // Implementation of BuildMenu(). index_offset is both input and output; + // on input it contains the offset from index to command id for the model, + // and on output it contains the offset for the next model. + void BuildMenuImpl(MenuItemView* menu, ui::MenuModel* model); + + // Container of ui::MenuModel pointers as encountered by preorder + // traversal. The first element is always the top-level model + // passed to the constructor. + ui::MenuModel* menu_model_; + + // Mouse event flags which can trigger menu actions. + int triggerable_event_flags_; + + // Map MenuItems to MenuModels. Used to implement WillShowMenu(). + std::map<MenuItemView*, ui::MenuModel*> menu_map_; + + DISALLOW_COPY_AND_ASSIGN(MenuModelAdapter); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_MODEL_ADAPTER_H_ diff --git a/ui/views/controls/menu/menu_model_adapter_unittest.cc b/ui/views/controls/menu/menu_model_adapter_unittest.cc new file mode 100644 index 0000000..4b35883 --- /dev/null +++ b/ui/views/controls/menu/menu_model_adapter_unittest.cc @@ -0,0 +1,307 @@ +// 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 "base/utf_string_conversions.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/models/menu_model.h" +#include "ui/base/models/menu_model_delegate.h" +#include "ui/views/controls/menu/menu_item_view.h" +#include "ui/views/controls/menu/menu_model_adapter.h" +#include "ui/views/controls/menu/menu_runner.h" +#include "ui/views/controls/menu/submenu_view.h" +#include "ui/views/test/views_test_base.h" + +namespace { + +// Base command id for test menu and its submenu. +const int kRootIdBase = 100; +const int kSubmenuIdBase = 200; + +// Offset to return for GetFirstItemIndex(). This is an arbitrary +// number to ensure that we aren't assuming it is 0. +const int kFirstItemIndex = 25; + +class MenuModelBase : public ui::MenuModel { + public: + MenuModelBase(int command_id_base) : command_id_base_(command_id_base), + last_activation_(-1) { + } + + virtual ~MenuModelBase() { + } + + // ui::MenuModel implementation: + + virtual bool HasIcons() const OVERRIDE { + return false; + } + + virtual int GetFirstItemIndex(gfx::NativeMenu native_menu) const OVERRIDE { + return kFirstItemIndex; + } + + virtual int GetItemCount() const OVERRIDE { + return static_cast<int>(items_.size()); + } + + virtual ItemType GetTypeAt(int index) const OVERRIDE { + return items_[index - GetFirstItemIndex(NULL)].type; + } + + virtual int GetCommandIdAt(int index) const OVERRIDE { + return index - GetFirstItemIndex(NULL) + command_id_base_; + } + + string16 GetLabelAt(int index) const OVERRIDE { + return items_[index - GetFirstItemIndex(NULL)].label; + } + + virtual bool IsItemDynamicAt(int index) const OVERRIDE { + return false; + } + + virtual const gfx::Font* GetLabelFontAt(int index) const OVERRIDE { + return NULL; + } + + virtual bool GetAcceleratorAt(int index, + ui::Accelerator* accelerator) const OVERRIDE { + return false; + } + + virtual bool IsItemCheckedAt(int index) const OVERRIDE { + return false; + } + + virtual int GetGroupIdAt(int index) const OVERRIDE { + return 0; + } + + virtual bool GetIconAt(int index, SkBitmap* icon) OVERRIDE { + return false; + } + + virtual ui::ButtonMenuItemModel* GetButtonMenuItemAt( + int index) const OVERRIDE { + return NULL; + } + + virtual bool IsEnabledAt(int index) const OVERRIDE { + return true; + } + + virtual bool IsVisibleAt(int index) const OVERRIDE { + return true; + } + + virtual MenuModel* GetSubmenuModelAt(int index) const OVERRIDE { + return items_[index - GetFirstItemIndex(NULL)].submenu; + } + + virtual void HighlightChangedTo(int index) OVERRIDE { + } + + virtual void ActivatedAt(int index) OVERRIDE { + set_last_activation(index); + } + + virtual void ActivatedAt(int index, int event_flags) OVERRIDE { + ActivatedAt(index); + } + + virtual void MenuWillShow() OVERRIDE { + } + + virtual void MenuClosed() OVERRIDE { + } + + virtual void SetMenuModelDelegate( + ui::MenuModelDelegate* delegate) OVERRIDE { + } + + // Item definition. + struct Item { + Item(ItemType item_type, + const std::string& item_label, + ui::MenuModel* item_submenu) + : type(item_type), + label(ASCIIToUTF16(item_label)), + submenu(item_submenu) { + } + + ItemType type; + string16 label; + ui::MenuModel* submenu; + }; + + const Item& GetItemDefinition(int index) { + return items_[index]; + } + + // Access index argument to ActivatedAt(). + int last_activation() const { return last_activation_; } + void set_last_activation(int last_activation) { + last_activation_ = last_activation; + } + + protected: + std::vector<Item> items_; + + private: + int command_id_base_; + int last_activation_; + + DISALLOW_COPY_AND_ASSIGN(MenuModelBase); +}; + +class SubmenuModel : public MenuModelBase { + public: + SubmenuModel() : MenuModelBase(kSubmenuIdBase) { + items_.push_back(Item(TYPE_COMMAND, "submenu item 0", NULL)); + items_.push_back(Item(TYPE_COMMAND, "submenu item 1", NULL)); + } + + virtual ~SubmenuModel() { + } + + private: + DISALLOW_COPY_AND_ASSIGN(SubmenuModel); +}; + +class RootModel : public MenuModelBase { + public: + RootModel() : MenuModelBase(kRootIdBase) { + submenu_model_.reset(new SubmenuModel); + + items_.push_back(Item(TYPE_COMMAND, "command 0", NULL)); + items_.push_back(Item(TYPE_CHECK, "check 1", NULL)); + items_.push_back(Item(TYPE_SEPARATOR, "", NULL)); + items_.push_back(Item(TYPE_SUBMENU, "submenu 3", submenu_model_.get())); + items_.push_back(Item(TYPE_RADIO, "radio 4", NULL)); + } + + virtual ~RootModel() { + } + + private: + scoped_ptr<MenuModel> submenu_model_; + + DISALLOW_COPY_AND_ASSIGN(RootModel); +}; + +} // namespace + +namespace views { + +typedef ViewsTestBase MenuModelAdapterTest; + +TEST_F(MenuModelAdapterTest, BasicTest) { + // Build model and adapter. + RootModel model; + views::MenuModelAdapter delegate(&model); + + // Create menu. Build menu twice to check that rebuilding works properly. + MenuItemView* menu = new views::MenuItemView(&delegate); + // MenuRunner takes ownership of menu. + scoped_ptr<MenuRunner> menu_runner(new MenuRunner(menu)); + delegate.BuildMenu(menu); + delegate.BuildMenu(menu); + EXPECT_TRUE(menu->HasSubmenu()); + + // Check top level menu items. + views::SubmenuView* item_container = menu->GetSubmenu(); + EXPECT_EQ(5, item_container->child_count()); + + for (int i = 0; i < item_container->child_count(); ++i) { + const MenuModelBase::Item& model_item = model.GetItemDefinition(i); + + const int id = i + kRootIdBase; + MenuItemView* item = menu->GetMenuItemByID(id); + if (!item) { + EXPECT_EQ(ui::MenuModel::TYPE_SEPARATOR, model_item.type); + continue; + } + + // Check placement. + EXPECT_EQ(i, menu->GetSubmenu()->GetIndexOf(item)); + + // Check type. + switch (model_item.type) { + case ui::MenuModel::TYPE_COMMAND: + EXPECT_EQ(views::MenuItemView::NORMAL, item->GetType()); + break; + case ui::MenuModel::TYPE_CHECK: + EXPECT_EQ(views::MenuItemView::CHECKBOX, item->GetType()); + break; + case ui::MenuModel::TYPE_RADIO: + EXPECT_EQ(views::MenuItemView::RADIO, item->GetType()); + break; + case ui::MenuModel::TYPE_SEPARATOR: + case ui::MenuModel::TYPE_BUTTON_ITEM: + break; + case ui::MenuModel::TYPE_SUBMENU: + EXPECT_EQ(views::MenuItemView::SUBMENU, item->GetType()); + break; + } + + // Check activation. + static_cast<views::MenuDelegate*>(&delegate)->ExecuteCommand(id); + EXPECT_EQ(i + kFirstItemIndex, model.last_activation()); + model.set_last_activation(-1); + } + + // Check submenu items. + views::MenuItemView* submenu = menu->GetMenuItemByID(103); + views::SubmenuView* subitem_container = submenu->GetSubmenu(); + EXPECT_EQ(2, subitem_container->child_count()); + + for (int i = 0; i < subitem_container->child_count(); ++i) { + MenuModelBase* submodel = static_cast<MenuModelBase*>( + model.GetSubmenuModelAt(3 + kFirstItemIndex)); + EXPECT_TRUE(submodel); + + const MenuModelBase::Item& model_item = submodel->GetItemDefinition(i); + + const int id = i + kSubmenuIdBase; + MenuItemView* item = menu->GetMenuItemByID(id); + if (!item) { + EXPECT_EQ(ui::MenuModel::TYPE_SEPARATOR, model_item.type); + continue; + } + + // Check placement. + EXPECT_EQ(i, submenu->GetSubmenu()->GetIndexOf(item)); + + // Check type. + switch (model_item.type) { + case ui::MenuModel::TYPE_COMMAND: + EXPECT_EQ(views::MenuItemView::NORMAL, item->GetType()); + break; + case ui::MenuModel::TYPE_CHECK: + EXPECT_EQ(views::MenuItemView::CHECKBOX, item->GetType()); + break; + case ui::MenuModel::TYPE_RADIO: + EXPECT_EQ(views::MenuItemView::RADIO, item->GetType()); + break; + case ui::MenuModel::TYPE_SEPARATOR: + case ui::MenuModel::TYPE_BUTTON_ITEM: + break; + case ui::MenuModel::TYPE_SUBMENU: + EXPECT_EQ(views::MenuItemView::SUBMENU, item->GetType()); + break; + } + + // Check activation. + static_cast<views::MenuDelegate*>(&delegate)->ExecuteCommand(id); + EXPECT_EQ(i + kFirstItemIndex, submodel->last_activation()); + submodel->set_last_activation(-1); + } + + // Check that selecting the root item is safe. The MenuModel does + // not care about the root so MenuModelAdapter should do nothing + // (not hit the NOTREACHED check) when the root is selected. + static_cast<views::MenuDelegate*>(&delegate)->SelectionChanged(menu); +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_runner.cc b/ui/views/controls/menu/menu_runner.cc new file mode 100644 index 0000000..db33ad0 --- /dev/null +++ b/ui/views/controls/menu/menu_runner.cc @@ -0,0 +1,275 @@ +// 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 "ui/views/controls/menu/menu_runner.h" + +#include <set> + +#include "ui/views/controls/button/menu_button.h" +#include "ui/views/controls/menu/menu_controller.h" +#include "ui/views/controls/menu/menu_controller_delegate.h" +#include "ui/views/controls/menu/menu_delegate.h" + +#if defined(OS_WIN) +#include "base/win/win_util.h" +#endif + +namespace views { + +namespace internal { + +// Manages the menu. To destroy a MenuRunnerImpl invoke Release(). Release() +// deletes immediately if the menu isn't showing. If the menu is showing +// Release() cancels the menu and when the nested RunMenuAt() call returns +// deletes itself and the menu. +class MenuRunnerImpl : public internal::MenuControllerDelegate { + public: + explicit MenuRunnerImpl(MenuItemView* menu); + + MenuItemView* menu() { return menu_; } + + // See description above class for details. + void Release(); + + // Runs the menu. + MenuRunner::RunResult RunMenuAt(Widget* parent, + MenuButton* button, + const gfx::Rect& bounds, + MenuItemView::AnchorPosition anchor, + int32 types) WARN_UNUSED_RESULT; + + void Cancel(); + + // MenuControllerDelegate: + virtual void DropMenuClosed(NotifyType type, MenuItemView* menu) OVERRIDE; + virtual void SiblingMenuCreated(MenuItemView* menu) OVERRIDE; + + private: + ~MenuRunnerImpl(); + + // Cleans up after the menu is no longer showing. |result| is the menu that + // the user selected, or NULL if nothing was selected. + MenuRunner::RunResult MenuDone(MenuItemView* result, int mouse_event_flags); + + // Returns true if mnemonics should be shown in the menu. + bool ShouldShowMnemonics(MenuButton* button); + + // The menu. We own this. We don't use scoped_ptr as the destructor is + // protected and we're a friend. + MenuItemView* menu_; + + // Any sibling menus. Does not include |menu_|. We own these too. + std::set<MenuItemView*> sibling_menus_; + + // Created and set as the delegate of the MenuItemView if Release() is + // invoked. This is done to make sure the delegate isn't notified after + // Release() is invoked. We do this as we assume the delegate is no longer + // valid if MenuRunner has been deleted. + scoped_ptr<MenuDelegate> empty_delegate_; + + // Are we in run waiting for it to return? + bool running_; + + // Set if |running_| and Release() has been invoked. + bool delete_after_run_; + + // Are we running for a drop? + bool for_drop_; + + // The controller. + MenuController* controller_; + + // Do we own the controller? + bool owns_controller_; + + DISALLOW_COPY_AND_ASSIGN(MenuRunnerImpl); +}; + +MenuRunnerImpl::MenuRunnerImpl(MenuItemView* menu) + : menu_(menu), + running_(false), + delete_after_run_(false), + for_drop_(false), + controller_(NULL), + owns_controller_(false) { +} + +void MenuRunnerImpl::Release() { + if (running_) { + if (delete_after_run_) + return; // We already canceled. + + // The menu is running a nested message loop, we can't delete it now + // otherwise the stack would be in a really bad state (many frames would + // have deleted objects on them). Instead cancel the menu, when it returns + // Holder will delete itself. + delete_after_run_ = true; + + // Swap in a different delegate. That way we know the original MenuDelegate + // won't be notified later on (when it's likely already been deleted). + if (!empty_delegate_.get()) + empty_delegate_.reset(new MenuDelegate()); + menu_->set_delegate(empty_delegate_.get()); + + DCHECK(controller_); + // Release is invoked when MenuRunner is destroyed. Assume this is happening + // because the object referencing the menu has been destroyed and the menu + // button is no longer valid. + controller_->Cancel(MenuController::EXIT_DESTROYED); + } else { + delete this; + } +} + +MenuRunner::RunResult MenuRunnerImpl::RunMenuAt( + Widget* parent, + MenuButton* button, + const gfx::Rect& bounds, + MenuItemView::AnchorPosition anchor, + int32 types) { + if (running_) { + // Ignore requests to show the menu while it's already showing. MenuItemView + // doesn't handle this very well (meaning it crashes). + return MenuRunner::NORMAL_EXIT; + } + + MenuController* controller = MenuController::GetActiveInstance(); + if (controller) { + if ((types & MenuRunner::IS_NESTED) != 0) { + if (!controller->IsBlockingRun()) { + controller->CancelAll(); + controller = NULL; + } + } else { + // There's some other menu open and we're not nested. Cancel the menu. + controller->CancelAll(); + if ((types & MenuRunner::FOR_DROP) == 0) { + // We can't open another menu, otherwise the message loop would become + // twice nested. This isn't necessarily a problem, but generally isn't + // expected. + return MenuRunner::NORMAL_EXIT; + } + // Drop menus don't block the message loop, so it's ok to create a new + // MenuController. + controller = NULL; + } + } + running_ = true; + for_drop_ = (types & MenuRunner::FOR_DROP) != 0; + bool has_mnemonics = (types & MenuRunner::HAS_MNEMONICS) != 0 && !for_drop_; + menu_->PrepareForRun(has_mnemonics, + !for_drop_ && ShouldShowMnemonics(button)); + int mouse_event_flags = 0; + owns_controller_ = false; + if (!controller) { + // No menus are showing, show one. + controller = new MenuController(!for_drop_, this); + owns_controller_ = true; + } + controller_ = controller; + menu_->set_controller(controller_); + + // Run the loop. + MenuItemView* result = controller->Run(parent, button, menu_, bounds, anchor, + &mouse_event_flags); + + if (for_drop_) { + // Drop menus return immediately. We finish processing in DropMenuClosed. + return MenuRunner::NORMAL_EXIT; + } + + return MenuDone(result, mouse_event_flags); +} + +void MenuRunnerImpl::Cancel() { + if (running_) + controller_->Cancel(MenuController::EXIT_ALL); +} + +void MenuRunnerImpl::DropMenuClosed(NotifyType type, MenuItemView* menu) { + MenuDone(NULL, 0); + + if (type == NOTIFY_DELEGATE && menu->GetDelegate()) { + // Delegate is null when invoked from the destructor. + menu->GetDelegate()->DropMenuClosed(menu); + } +} + +void MenuRunnerImpl::SiblingMenuCreated(MenuItemView* menu) { + if (menu != menu_ && sibling_menus_.count(menu) == 0) + sibling_menus_.insert(menu); +} + +MenuRunnerImpl::~MenuRunnerImpl() { + delete menu_; + for (std::set<MenuItemView*>::iterator i = sibling_menus_.begin(); + i != sibling_menus_.end(); ++i) + delete *i; +} + +MenuRunner::RunResult MenuRunnerImpl::MenuDone(MenuItemView* result, + int mouse_event_flags) { + menu_->RemoveEmptyMenus(); + menu_->set_controller(NULL); + + if (owns_controller_) { + // We created the controller and need to delete it. + delete controller_; + owns_controller_ = false; + } + controller_ = NULL; + // Make sure all the windows we created to show the menus have been + // destroyed. + menu_->DestroyAllMenuHosts(); + if (delete_after_run_) { + delete this; + return MenuRunner::MENU_DELETED; + } + running_ = false; + if (result && menu_->GetDelegate()) { + menu_->GetDelegate()->ExecuteCommand(result->GetCommand(), + mouse_event_flags); + } + return MenuRunner::NORMAL_EXIT; +} + +bool MenuRunnerImpl::ShouldShowMnemonics(MenuButton* button) { + // Show mnemonics if the button has focus or alt is pressed. + bool show_mnemonics = button ? button->HasFocus() : false; +#if defined(OS_WIN) + // We don't currently need this on gtk as showing the menu gives focus to the + // button first. + if (!show_mnemonics) + show_mnemonics = base::win::IsAltPressed(); +#endif + return show_mnemonics; +} + +} // namespace internal + +MenuRunner::MenuRunner(MenuItemView* menu) + : holder_(new internal::MenuRunnerImpl(menu)) { +} + +MenuRunner::~MenuRunner() { + holder_->Release(); +} + +MenuItemView* MenuRunner::GetMenu() { + return holder_->menu(); +} + +MenuRunner::RunResult MenuRunner::RunMenuAt(Widget* parent, + MenuButton* button, + const gfx::Rect& bounds, + MenuItemView::AnchorPosition anchor, + int32 types) { + return holder_->RunMenuAt(parent, button, bounds, anchor, types); +} + +void MenuRunner::Cancel() { + holder_->Cancel(); +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_runner.h b/ui/views/controls/menu/menu_runner.h new file mode 100644 index 0000000..6a3c572 --- /dev/null +++ b/ui/views/controls/menu/menu_runner.h @@ -0,0 +1,99 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_RUNNER_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_RUNNER_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "ui/views/controls/menu/menu_item_view.h" + +namespace views { + +class MenuButton; +class Widget; + +namespace internal { +class MenuRunnerImpl; +} + +// MenuRunner is responsible for showing (running) the menu and additionally +// owning the MenuItemView. RunMenuAt() runs a nested message loop. It is safe +// to delete MenuRunner at any point, but MenuRunner internally only deletes the +// MenuItemView *after* the nested message loop completes. If MenuRunner is +// deleted while the menu is showing the delegate of the menu is reset. This is +// done to ensure delegates aren't notified after they may have been deleted. +// +// NOTE: while you can delete a MenuRunner at any point, the nested message loop +// won't return immediately. This means if you delete the object that owns +// the MenuRunner while the menu is running, your object is effectively still +// on the stack. A return value of MENU_DELETED indicated this. In most cases +// if RunMenuAt() returns MENU_DELETED, you should return immediately. +// +// Similarly you should avoid creating MenuRunner on the stack. Doing so means +// MenuRunner may not be immediately destroyed if your object is destroyed, +// resulting in possible callbacks to your now deleted object. Instead you +// should define MenuRunner as a scoped_ptr in your class so that when your +// object is destroyed MenuRunner initiates the proper cleanup and ensures your +// object isn't accessed again. +class VIEWS_EXPORT MenuRunner { + public: + enum RunTypes { + // The menu has mnemonics. + HAS_MNEMONICS = 1 << 0, + + // The menu is a nested context menu. For example, click a folder on the + // bookmark bar, then right click an entry to get its context menu. + IS_NESTED = 1 << 1, + + // Used for showing a menu during a drop operation. This does NOT block the + // caller, instead the delegate is notified when the menu closes via the + // DropMenuClosed method. + FOR_DROP = 1 << 2, + }; + + enum RunResult { + // Indicates RunMenuAt is returning because the MenuRunner was deleted. + MENU_DELETED, + + // Indicates RunMenuAt returned and MenuRunner was not deleted. + NORMAL_EXIT + }; + + // Creates a new MenuRunner. MenuRunner owns the supplied menu. + explicit MenuRunner(MenuItemView* menu); + ~MenuRunner(); + + // Returns the menu. + MenuItemView* GetMenu(); + + // Takes ownership of |menu|, deleting it when MenuRunner is deleted. You + // only need call this if you create additional menus from + // MenuDelegate::GetSiblingMenu. + void OwnMenu(MenuItemView* menu); + + // Runs the menu. |types| is a bitmask of RunTypes. If this returns + // MENU_DELETED the method is returning because the MenuRunner was deleted. + // Typically callers should NOT do any processing if this returns + // MENU_DELETED. + RunResult RunMenuAt(Widget* parent, + MenuButton* button, + const gfx::Rect& bounds, + MenuItemView::AnchorPosition anchor, + int32 types) WARN_UNUSED_RESULT; + + // Hides and cancels the menu. This does nothing if the menu is not open. + void Cancel(); + + private: + internal::MenuRunnerImpl* holder_; + + DISALLOW_COPY_AND_ASSIGN(MenuRunner); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_RUNNER_H_ diff --git a/ui/views/controls/menu/menu_scroll_view_container.cc b/ui/views/controls/menu/menu_scroll_view_container.cc new file mode 100644 index 0000000..775218f --- /dev/null +++ b/ui/views/controls/menu/menu_scroll_view_container.cc @@ -0,0 +1,247 @@ +// 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 "ui/views/controls/menu/menu_scroll_view_container.h" + +#if defined(OS_WIN) +#include <windows.h> +#include <uxtheme.h> +#include <Vssym32.h> +#endif + +#include "ui/base/accessibility/accessible_view_state.h" +#include "ui/gfx/canvas_skia.h" +#include "ui/gfx/color_utils.h" +#include "ui/gfx/native_theme.h" +#include "ui/views/controls/menu/menu_config.h" +#include "ui/views/controls/menu/menu_controller.h" +#include "ui/views/controls/menu/menu_item_view.h" +#include "ui/views/controls/menu/submenu_view.h" +#include "views/border.h" + +using gfx::NativeTheme; + +// Height of the scroll arrow. +// This goes up to 4 with large fonts, but this is close enough for now. +static const int scroll_arrow_height = 3; + +namespace views { + +namespace { + +// MenuScrollButton ------------------------------------------------------------ + +// MenuScrollButton is used for the scroll buttons when not all menu items fit +// on screen. MenuScrollButton forwards appropriate events to the +// MenuController. + +class MenuScrollButton : public View { + public: + MenuScrollButton(SubmenuView* host, bool is_up) + : host_(host), + is_up_(is_up), + // Make our height the same as that of other MenuItemViews. + pref_height_(MenuItemView::pref_menu_height()) { + } + + virtual gfx::Size GetPreferredSize() { + return gfx::Size(MenuConfig::instance().scroll_arrow_height * 2 - 1, + pref_height_); + } + + virtual bool CanDrop(const OSExchangeData& data) { + DCHECK(host_->GetMenuItem()->GetMenuController()); + return true; // Always return true so that drop events are targeted to us. + } + + virtual void OnDragEntered(const DropTargetEvent& event) { + DCHECK(host_->GetMenuItem()->GetMenuController()); + host_->GetMenuItem()->GetMenuController()->OnDragEnteredScrollButton( + host_, is_up_); + } + + virtual int OnDragUpdated(const DropTargetEvent& event) { + return ui::DragDropTypes::DRAG_NONE; + } + + virtual void OnDragExited() { + DCHECK(host_->GetMenuItem()->GetMenuController()); + host_->GetMenuItem()->GetMenuController()->OnDragExitedScrollButton(host_); + } + + virtual int OnPerformDrop(const DropTargetEvent& event) { + return ui::DragDropTypes::DRAG_NONE; + } + + virtual void OnPaint(gfx::Canvas* canvas) { + const MenuConfig& config = MenuConfig::instance(); + + // The background. + gfx::Rect item_bounds(0, 0, width(), height()); + NativeTheme::ExtraParams extra; + extra.menu_item.is_selected = false; + NativeTheme::instance()->Paint(canvas->GetSkCanvas(), + NativeTheme::kMenuItemBackground, + NativeTheme::kNormal, item_bounds, extra); +#if defined(OS_WIN) + SkColor arrow_color = color_utils::GetSysSkColor(COLOR_MENUTEXT); +#else + SkColor arrow_color = SK_ColorBLACK; +#endif + + // Then the arrow. + int x = width() / 2; + int y = (height() - config.scroll_arrow_height) / 2; + int delta_y = 1; + if (!is_up_) { + delta_y = -1; + y += config.scroll_arrow_height; + } + for (int i = 0; i < config.scroll_arrow_height; ++i, --x, y += delta_y) + canvas->FillRect(arrow_color, gfx::Rect(x, y, (i * 2) + 1, 1)); + } + + private: + // SubmenuView we were created for. + SubmenuView* host_; + + // Direction of the button. + bool is_up_; + + // Preferred height. + int pref_height_; + + DISALLOW_COPY_AND_ASSIGN(MenuScrollButton); +}; + +} // namespace + +// MenuScrollView -------------------------------------------------------------- + +// MenuScrollView is a viewport for the SubmenuView. It's reason to exist is so +// that ScrollRectToVisible works. +// +// NOTE: It is possible to use ScrollView directly (after making it deal with +// null scrollbars), but clicking on a child of ScrollView forces the window to +// become active, which we don't want. As we really only need a fraction of +// what ScrollView does, so we use a one off variant. + +class MenuScrollViewContainer::MenuScrollView : public View { + public: + explicit MenuScrollView(View* child) { + AddChildView(child); + } + + virtual void ScrollRectToVisible(const gfx::Rect& rect) { + // NOTE: this assumes we only want to scroll in the y direction. + + // Convert rect.y() to view's coordinates and make sure we don't show past + // the bottom of the view. + View* child = GetContents(); + child->SetY(-std::max(0, std::min( + child->GetPreferredSize().height() - this->height(), + rect.y() - child->y()))); + } + + // Returns the contents, which is the SubmenuView. + View* GetContents() { + return child_at(0); + } + + private: + DISALLOW_COPY_AND_ASSIGN(MenuScrollView); +}; + +// MenuScrollViewContainer ---------------------------------------------------- + +MenuScrollViewContainer::MenuScrollViewContainer(SubmenuView* content_view) + : content_view_(content_view) { + scroll_up_button_ = new MenuScrollButton(content_view, true); + scroll_down_button_ = new MenuScrollButton(content_view, false); + AddChildView(scroll_up_button_); + AddChildView(scroll_down_button_); + + scroll_view_ = new MenuScrollView(content_view); + AddChildView(scroll_view_); + + set_border(Border::CreateEmptyBorder( + SubmenuView::kSubmenuBorderSize, + SubmenuView::kSubmenuBorderSize, + SubmenuView::kSubmenuBorderSize, + SubmenuView::kSubmenuBorderSize)); +} + +void MenuScrollViewContainer::OnPaintBackground(gfx::Canvas* canvas) { + if (background()) { + View::OnPaintBackground(canvas); + return; + } + +#if defined(OS_WIN) + HDC dc = canvas->BeginPlatformPaint(); +#endif + gfx::Rect bounds(0, 0, width(), height()); + NativeTheme::ExtraParams extra; + NativeTheme::instance()->Paint(canvas->GetSkCanvas(), + NativeTheme::kMenuPopupBackground, NativeTheme::kNormal, bounds, extra); +#if defined(OS_WIN) + canvas->EndPlatformPaint(); +#endif +} + +void MenuScrollViewContainer::Layout() { + gfx::Insets insets = GetInsets(); + int x = insets.left(); + int y = insets.top(); + int width = View::width() - insets.width(); + int content_height = height() - insets.height(); + if (!scroll_up_button_->IsVisible()) { + scroll_view_->SetBounds(x, y, width, content_height); + scroll_view_->Layout(); + return; + } + + gfx::Size pref = scroll_up_button_->GetPreferredSize(); + scroll_up_button_->SetBounds(x, y, width, pref.height()); + content_height -= pref.height(); + + const int scroll_view_y = y + pref.height(); + + pref = scroll_down_button_->GetPreferredSize(); + scroll_down_button_->SetBounds(x, height() - pref.height() - insets.top(), + width, pref.height()); + content_height -= pref.height(); + + scroll_view_->SetBounds(x, scroll_view_y, width, content_height); + scroll_view_->Layout(); +} + +gfx::Size MenuScrollViewContainer::GetPreferredSize() { + gfx::Size prefsize = scroll_view_->GetContents()->GetPreferredSize(); + gfx::Insets insets = GetInsets(); + prefsize.Enlarge(insets.width(), insets.height()); + return prefsize; +} + +void MenuScrollViewContainer::GetAccessibleState( + ui::AccessibleViewState* state) { + // Get the name from the submenu view. + content_view_->GetAccessibleState(state); + + // Now change the role. + state->role = ui::AccessibilityTypes::ROLE_MENUBAR; + // Some AT (like NVDA) will not process focus events on menu item children + // unless a parent claims to be focused. + state->state = ui::AccessibilityTypes::STATE_FOCUSED; +} + +void MenuScrollViewContainer::OnBoundsChanged( + const gfx::Rect& previous_bounds) { + gfx::Size content_pref = scroll_view_->GetContents()->GetPreferredSize(); + scroll_up_button_->SetVisible(content_pref.height() > height()); + scroll_down_button_->SetVisible(content_pref.height() > height()); + Layout(); +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_scroll_view_container.h b/ui/views/controls/menu/menu_scroll_view_container.h new file mode 100644 index 0000000..a19b73a --- /dev/null +++ b/ui/views/controls/menu/menu_scroll_view_container.h @@ -0,0 +1,54 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_SCROLL_VIEW_CONTAINER_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_SCROLL_VIEW_CONTAINER_H_ +#pragma once + +#include "views/view.h" + +namespace views { + +class SubmenuView; + +// MenuScrollViewContainer contains the SubmenuView (through a MenuScrollView) +// and two scroll buttons. The scroll buttons are only visible and enabled if +// the preferred height of the SubmenuView is bigger than our bounds. +class MenuScrollViewContainer : public View { + public: + explicit MenuScrollViewContainer(SubmenuView* content_view); + + // Returns the buttons for scrolling up/down. + View* scroll_down_button() const { return scroll_down_button_; } + View* scroll_up_button() const { return scroll_up_button_; } + + // View overrides. + virtual void OnPaintBackground(gfx::Canvas* canvas) OVERRIDE; + virtual void Layout() OVERRIDE; + virtual gfx::Size GetPreferredSize() OVERRIDE; + virtual void GetAccessibleState(ui::AccessibleViewState* state) OVERRIDE; + + protected: + // View override. + virtual void OnBoundsChanged(const gfx::Rect& previous_bounds) OVERRIDE; + + private: + class MenuScrollView; + + // The scroll buttons. + View* scroll_up_button_; + View* scroll_down_button_; + + // The scroll view. + MenuScrollView* scroll_view_; + + // The content view. + SubmenuView* content_view_; + + DISALLOW_COPY_AND_ASSIGN(MenuScrollViewContainer); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_SCROLL_VIEW_CONTAINER_H_ diff --git a/ui/views/controls/menu/menu_separator.h b/ui/views/controls/menu/menu_separator.h new file mode 100644 index 0000000..4e968a3 --- /dev/null +++ b/ui/views/controls/menu/menu_separator.h @@ -0,0 +1,28 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_SEPARATOR_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_SEPARATOR_H_ +#pragma once + +#include "base/compiler_specific.h" +#include "views/view.h" + +namespace views { + +class MenuSeparator : public View { + public: + MenuSeparator() {} + + // View overrides. + virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE; + virtual gfx::Size GetPreferredSize() OVERRIDE; + + private: + DISALLOW_COPY_AND_ASSIGN(MenuSeparator); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_SEPARATOR_H_ diff --git a/ui/views/controls/menu/menu_separator_aura.cc b/ui/views/controls/menu/menu_separator_aura.cc new file mode 100644 index 0000000..0b914cf --- /dev/null +++ b/ui/views/controls/menu/menu_separator_aura.cc @@ -0,0 +1,24 @@ +// 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 "ui/views/controls/menu/menu_separator.h" + +#include "third_party/skia/include/core/SkColor.h" +#include "ui/gfx/canvas.h" +#include "ui/views/controls/menu/menu_config.h" + +namespace views { + +static const SkColor kSeparatorColor = SkColorSetARGB(50, 00, 00, 00); + +void MenuSeparator::OnPaint(gfx::Canvas* canvas) { + canvas->DrawLineInt(kSeparatorColor, 0, height() / 2, width(), height() / 2); +} + +gfx::Size MenuSeparator::GetPreferredSize() { + return gfx::Size(10, // Just in case we're the only item in a menu. + MenuConfig::instance().separator_height); +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_separator_linux.cc b/ui/views/controls/menu/menu_separator_linux.cc new file mode 100644 index 0000000..0b914cf --- /dev/null +++ b/ui/views/controls/menu/menu_separator_linux.cc @@ -0,0 +1,24 @@ +// 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 "ui/views/controls/menu/menu_separator.h" + +#include "third_party/skia/include/core/SkColor.h" +#include "ui/gfx/canvas.h" +#include "ui/views/controls/menu/menu_config.h" + +namespace views { + +static const SkColor kSeparatorColor = SkColorSetARGB(50, 00, 00, 00); + +void MenuSeparator::OnPaint(gfx::Canvas* canvas) { + canvas->DrawLineInt(kSeparatorColor, 0, height() / 2, width(), height() / 2); +} + +gfx::Size MenuSeparator::GetPreferredSize() { + return gfx::Size(10, // Just in case we're the only item in a menu. + MenuConfig::instance().separator_height); +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_separator_win.cc b/ui/views/controls/menu/menu_separator_win.cc new file mode 100644 index 0000000..2692f79 --- /dev/null +++ b/ui/views/controls/menu/menu_separator_win.cc @@ -0,0 +1,48 @@ +// 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 "ui/views/controls/menu/menu_separator.h" + +#include <windows.h> +#include <uxtheme.h> +#include <Vssym32.h> + +#include "ui/gfx/canvas_skia.h" +#include "ui/gfx/native_theme.h" +#include "ui/gfx/rect.h" +#include "ui/views/controls/menu/menu_config.h" +#include "ui/views/controls/menu/menu_item_view.h" + +namespace views { + +void MenuSeparator::OnPaint(gfx::Canvas* canvas) { + const MenuConfig& config = MenuConfig::instance(); + // The gutter is rendered before the background. + int start_x = 0; + const gfx::NativeTheme* theme = gfx::NativeTheme::instance(); + if (config.render_gutter) { + // If render_gutter is true, we're on Vista and need to render the + // gutter, then indent the separator from the gutter. + gfx::Rect gutter_bounds(MenuItemView::label_start() - + config.gutter_to_label - config.gutter_width, 0, + config.gutter_width, height()); + gfx::NativeTheme::ExtraParams extra; + theme->Paint(canvas->GetSkCanvas(), gfx::NativeTheme::kMenuPopupGutter, + gfx::NativeTheme::kNormal, gutter_bounds, extra); + start_x = gutter_bounds.x() + config.gutter_width; + } + + gfx::Rect separator_bounds(start_x, 0, width(), height()); + gfx::NativeTheme::ExtraParams extra; + extra.menu_separator.has_gutter = config.render_gutter; + theme->Paint(canvas->GetSkCanvas(), gfx::NativeTheme::kMenuPopupSeparator, + gfx::NativeTheme::kNormal, separator_bounds, extra); +} + +gfx::Size MenuSeparator::GetPreferredSize() { + return gfx::Size(10, // Just in case we're the only item in a menu. + MenuConfig::instance().separator_height); +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_win.cc b/ui/views/controls/menu/menu_win.cc new file mode 100644 index 0000000..e1ad7a5 --- /dev/null +++ b/ui/views/controls/menu/menu_win.cc @@ -0,0 +1,574 @@ +// 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 "ui/views/controls/menu/menu_win.h" + +#include <string> + +#include "base/logging.h" +#include "base/stl_util.h" +#include "base/string_util.h" +#include "ui/base/accelerators/accelerator.h" +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/l10n/l10n_util_win.h" +#include "ui/base/win/window_impl.h" +#include "ui/gfx/canvas_skia.h" +#include "ui/gfx/font.h" +#include "ui/gfx/rect.h" + +namespace views { + +// The width of an icon, including the pixels between the icon and +// the item label. +const int kIconWidth = 23; +// Margins between the top of the item and the label. +const int kItemTopMargin = 3; +// Margins between the bottom of the item and the label. +const int kItemBottomMargin = 4; +// Margins between the left of the item and the icon. +const int kItemLeftMargin = 4; +// Margins between the right of the item and the label. +const int kItemRightMargin = 10; +// The width for displaying the sub-menu arrow. +const int kArrowWidth = 10; + +// Current active MenuHostWindow. If NULL, no menu is active. +static MenuHostWindow* active_host_window = NULL; + +// The data of menu items needed to display. +struct MenuWin::ItemData { + string16 label; + SkBitmap icon; + bool submenu; +}; + +namespace { + +static int ChromeGetMenuItemID(HMENU hMenu, int pos) { + // The built-in Windows GetMenuItemID doesn't work for submenus, + // so here's our own implementation. + MENUITEMINFO mii = {0}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_ID; + GetMenuItemInfo(hMenu, pos, TRUE, &mii); + return mii.wID; +} + +// MenuHostWindow ------------------------------------------------------------- + +// MenuHostWindow is the HWND the HMENU is parented to. MenuHostWindow is used +// to intercept right clicks on the HMENU and notify the delegate as well as +// for drawing icons. +// +class MenuHostWindow : public ui::WindowImpl { + public: + MenuHostWindow(MenuWin* menu, HWND parent_window) : menu_(menu) { + int extended_style = 0; + // If the menu needs to be created with a right-to-left UI layout, we must + // set the appropriate RTL flags (such as WS_EX_LAYOUTRTL) property for the + // underlying HWND. + if (menu_->delegate()->IsRightToLeftUILayout()) + extended_style |= l10n_util::GetExtendedStyles(); + set_window_style(WS_CHILD); + set_window_ex_style(extended_style); + Init(parent_window, gfx::Rect()); + } + + ~MenuHostWindow() { + DestroyWindow(hwnd()); + } + + BEGIN_MSG_MAP_EX(MenuHostWindow); + MSG_WM_RBUTTONUP(OnRButtonUp) + MSG_WM_MEASUREITEM(OnMeasureItem) + MSG_WM_DRAWITEM(OnDrawItem) + END_MSG_MAP(); + + private: + // NOTE: I really REALLY tried to use WM_MENURBUTTONUP, but I ran into + // two problems in using it: + // 1. It doesn't contain the coordinates of the mouse. + // 2. It isn't invoked for menuitems representing a submenu that have children + // menu items (not empty). + + void OnRButtonUp(UINT w_param, const CPoint& loc) { + int id; + if (menu_->delegate() && FindMenuIDByLocation(menu_, loc, &id)) + menu_->delegate()->ShowContextMenu(menu_, id, gfx::Point(loc), true); + } + + void OnMeasureItem(WPARAM w_param, MEASUREITEMSTRUCT* lpmis) { + MenuWin::ItemData* data = + reinterpret_cast<MenuWin::ItemData*>(lpmis->itemData); + if (data != NULL) { + gfx::Font font; + lpmis->itemWidth = font.GetStringWidth(data->label) + kIconWidth + + kItemLeftMargin + kItemRightMargin - + GetSystemMetrics(SM_CXMENUCHECK); + if (data->submenu) + lpmis->itemWidth += kArrowWidth; + // If the label contains an accelerator, make room for tab. + if (data->label.find(L'\t') != string16::npos) + lpmis->itemWidth += font.GetStringWidth(L" "); + lpmis->itemHeight = font.GetHeight() + kItemBottomMargin + kItemTopMargin; + } else { + // Measure separator size. + lpmis->itemHeight = GetSystemMetrics(SM_CYMENU) / 2; + lpmis->itemWidth = 0; + } + } + + void OnDrawItem(UINT wParam, DRAWITEMSTRUCT* lpdis) { + HDC hDC = lpdis->hDC; + COLORREF prev_bg_color, prev_text_color; + + // Set background color and text color + if (lpdis->itemState & ODS_SELECTED) { + prev_bg_color = SetBkColor(hDC, GetSysColor(COLOR_HIGHLIGHT)); + prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_HIGHLIGHTTEXT)); + } else { + prev_bg_color = SetBkColor(hDC, GetSysColor(COLOR_MENU)); + if (lpdis->itemState & ODS_DISABLED) + prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_GRAYTEXT)); + else + prev_text_color = SetTextColor(hDC, GetSysColor(COLOR_MENUTEXT)); + } + + if (lpdis->itemData) { + MenuWin::ItemData* data = + reinterpret_cast<MenuWin::ItemData*>(lpdis->itemData); + + // Draw the background. + HBRUSH hbr = CreateSolidBrush(GetBkColor(hDC)); + FillRect(hDC, &lpdis->rcItem, hbr); + DeleteObject(hbr); + + // Draw the label. + RECT rect = lpdis->rcItem; + rect.top += kItemTopMargin; + // Should we add kIconWidth only when icon.width() != 0 ? + rect.left += kItemLeftMargin + kIconWidth; + rect.right -= kItemRightMargin; + UINT format = DT_TOP | DT_SINGLELINE; + // Check whether the mnemonics should be underlined. + BOOL underline_mnemonics; + SystemParametersInfo(SPI_GETKEYBOARDCUES, 0, &underline_mnemonics, 0); + if (!underline_mnemonics) + format |= DT_HIDEPREFIX; + gfx::Font font; + HGDIOBJ old_font = + static_cast<HFONT>(SelectObject(hDC, font.GetNativeFont())); + + // If an accelerator is specified (with a tab delimiting the rest of the + // label from the accelerator), we have to justify the fist part on the + // left and the accelerator on the right. + // TODO(jungshik): This will break in RTL UI. Currently, he/ar use the + // window system UI font and will not hit here. + string16 label = data->label; + string16 accel; + string16::size_type tab_pos = label.find(L'\t'); + if (tab_pos != string16::npos) { + accel = label.substr(tab_pos); + label = label.substr(0, tab_pos); + } + DrawTextEx(hDC, const_cast<wchar_t*>(label.data()), + static_cast<int>(label.size()), &rect, format | DT_LEFT, NULL); + if (!accel.empty()) + DrawTextEx(hDC, const_cast<wchar_t*>(accel.data()), + static_cast<int>(accel.size()), &rect, + format | DT_RIGHT, NULL); + SelectObject(hDC, old_font); + + // Draw the icon after the label, otherwise it would be covered + // by the label. + if (data->icon.width() != 0 && data->icon.height() != 0) { + gfx::CanvasSkia canvas(data->icon.width(), data->icon.height(), false); + canvas.sk_canvas()->drawColor(SK_ColorBLACK, SkXfermode::kClear_Mode); + canvas.DrawBitmapInt(data->icon, 0, 0); + skia::DrawToNativeContext( + canvas.sk_canvas(), hDC, lpdis->rcItem.left + kItemLeftMargin, + lpdis->rcItem.top + (lpdis->rcItem.bottom - lpdis->rcItem.top - + data->icon.height()) / 2, NULL); + } + + } else { + // Draw the separator + lpdis->rcItem.top += (lpdis->rcItem.bottom - lpdis->rcItem.top) / 3; + DrawEdge(hDC, &lpdis->rcItem, EDGE_ETCHED, BF_TOP); + } + + SetBkColor(hDC, prev_bg_color); + SetTextColor(hDC, prev_text_color); + } + + bool FindMenuIDByLocation(MenuWin* menu, const CPoint& loc, int* id) { + int index = MenuItemFromPoint(NULL, menu->menu_, loc); + if (index != -1) { + *id = ChromeGetMenuItemID(menu->menu_, index); + return true; + } else { + for (std::vector<MenuWin*>::iterator i = menu->submenus_.begin(); + i != menu->submenus_.end(); ++i) { + if (FindMenuIDByLocation(*i, loc, id)) + return true; + } + } + return false; + } + + // The menu that created us. + MenuWin* menu_; + + DISALLOW_COPY_AND_ASSIGN(MenuHostWindow); +}; + +} // namespace + +// static +Menu* Menu::Create(Delegate* delegate, + AnchorPoint anchor, + gfx::NativeView parent) { + return new MenuWin(delegate, anchor, parent); +} + +// static +Menu* Menu::GetSystemMenu(gfx::NativeWindow parent) { + return new views::MenuWin(::GetSystemMenu(parent, FALSE)); +} + +MenuWin::MenuWin(Delegate* d, AnchorPoint anchor, HWND owner) + : Menu(d, anchor), + menu_(CreatePopupMenu()), + owner_(owner), + is_menu_visible_(false), + owner_draw_(l10n_util::NeedOverrideDefaultUIFont(NULL, NULL)) { + DCHECK(delegate()); +} + +MenuWin::MenuWin(HMENU hmenu) + : Menu(NULL, TOPLEFT), + menu_(hmenu), + owner_(NULL), + is_menu_visible_(false), + owner_draw_(false) { + DCHECK(menu_); +} + +MenuWin::~MenuWin() { + STLDeleteContainerPointers(submenus_.begin(), submenus_.end()); + STLDeleteContainerPointers(item_data_.begin(), item_data_.end()); + DestroyMenu(menu_); +} + +void MenuWin::AddMenuItemWithIcon(int index, + int item_id, + const string16& label, + const SkBitmap& icon) { + owner_draw_ = true; + Menu::AddMenuItemWithIcon(index, item_id, label, icon); +} + +Menu* MenuWin::AddSubMenuWithIcon(int index, + int item_id, + const string16& label, + const SkBitmap& icon) { + MenuWin* submenu = new MenuWin(this); + submenus_.push_back(submenu); + AddMenuItemInternal(index, item_id, label, icon, submenu->menu_, NORMAL); + return submenu; +} + +void MenuWin::AddSeparator(int index) { + MENUITEMINFO mii; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_FTYPE; + mii.fType = MFT_SEPARATOR; + InsertMenuItem(menu_, index, TRUE, &mii); +} + +void MenuWin::EnableMenuItemByID(int item_id, bool enabled) { + UINT enable_flags = enabled ? MF_ENABLED : MF_DISABLED | MF_GRAYED; + EnableMenuItem(menu_, item_id, MF_BYCOMMAND | enable_flags); +} + +void MenuWin::EnableMenuItemAt(int index, bool enabled) { + UINT enable_flags = enabled ? MF_ENABLED : MF_DISABLED | MF_GRAYED; + EnableMenuItem(menu_, index, MF_BYPOSITION | enable_flags); +} + +void MenuWin::SetMenuLabel(int item_id, const string16& label) { + MENUITEMINFO mii = {0}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_STRING; + mii.dwTypeData = const_cast<wchar_t*>(label.c_str()); + mii.cch = static_cast<UINT>(label.size()); + SetMenuItemInfo(menu_, item_id, false, &mii); +} + +bool MenuWin::SetIcon(const SkBitmap& icon, int item_id) { + if (!owner_draw_) + owner_draw_ = true; + + const int num_items = GetMenuItemCount(menu_); + int sep_count = 0; + for (int i = 0; i < num_items; ++i) { + if (!(GetMenuState(menu_, i, MF_BYPOSITION) & MF_SEPARATOR)) { + if (ChromeGetMenuItemID(menu_, i) == item_id) { + item_data_[i - sep_count]->icon = icon; + // When the menu is running, we use SetMenuItemInfo to let Windows + // update the item information so that the icon being displayed + // could change immediately. + if (active_host_window) { + MENUITEMINFO mii; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_FTYPE | MIIM_DATA; + mii.fType = MFT_OWNERDRAW; + mii.dwItemData = + reinterpret_cast<ULONG_PTR>(item_data_[i - sep_count]); + SetMenuItemInfo(menu_, item_id, false, &mii); + } + return true; + } + } else { + ++sep_count; + } + } + + // Continue searching for the item in submenus. + for (size_t i = 0; i < submenus_.size(); ++i) { + if (submenus_[i]->SetIcon(icon, item_id)) + return true; + } + + return false; +} + +void MenuWin::RunMenuAt(int x, int y) { + SetMenuInfo(); + + delegate()->MenuWillShow(); + + // NOTE: we don't use TPM_RIGHTBUTTON here as it breaks selecting by way of + // press, drag, release. See bugs 718 and 8560. + UINT flags = + GetTPMAlignFlags() | TPM_LEFTBUTTON | TPM_RETURNCMD | TPM_RECURSE; + is_menu_visible_ = true; + DCHECK(owner_); + // In order for context menus on menus to work, the context menu needs to + // share the same window as the first menu is parented to. + bool created_host = false; + if (!active_host_window) { + created_host = true; + active_host_window = new MenuHostWindow(this, owner_); + } + UINT selected_id = + TrackPopupMenuEx(menu_, flags, x, y, active_host_window->hwnd(), NULL); + if (created_host) { + delete active_host_window; + active_host_window = NULL; + } + is_menu_visible_ = false; + + // Execute the chosen command + if (selected_id != 0) + delegate()->ExecuteCommand(selected_id); +} + +void MenuWin::Cancel() { + DCHECK(is_menu_visible_); + EndMenu(); +} + +int MenuWin::ItemCount() { + return GetMenuItemCount(menu_); +} + +void MenuWin::AddMenuItemInternal(int index, + int item_id, + const string16& label, + const SkBitmap& icon, + MenuItemType type) { + AddMenuItemInternal(index, item_id, label, icon, NULL, type); +} + +void MenuWin::AddMenuItemInternal(int index, + int item_id, + const string16& label, + const SkBitmap& icon, + HMENU submenu, + MenuItemType type) { + DCHECK(type != SEPARATOR) << "Call AddSeparator instead!"; + + if (!owner_draw_ && !icon.empty()) + owner_draw_ = true; + + if (label.empty() && !delegate()) { + // No label and no delegate; don't add an empty menu. + // It appears under some circumstance we're getting an empty label + // (l10n_util::GetStringUTF16(IDS_TASK_MANAGER) returns ""). This shouldn't + // happen, but I'm working over the crash here. + NOTREACHED(); + return; + } + + MENUITEMINFO mii; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_FTYPE | MIIM_ID; + if (submenu) { + mii.fMask |= MIIM_SUBMENU; + mii.hSubMenu = submenu; + } + + // Set the type and ID. + if (!owner_draw_) { + mii.fType = MFT_STRING; + mii.fMask |= MIIM_STRING; + } else { + mii.fType = MFT_OWNERDRAW; + } + + if (type == RADIO) + mii.fType |= MFT_RADIOCHECK; + + mii.wID = item_id; + + // Set the item data. + MenuWin::ItemData* data = new ItemData; + item_data_.push_back(data); + data->submenu = submenu != NULL; + + string16 actual_label(label.empty() ? delegate()->GetLabel(item_id) : label); + + // Find out if there is a shortcut we need to append to the label. + ui::Accelerator accelerator(ui::VKEY_UNKNOWN, false, false, false); + if (delegate() && delegate()->GetAcceleratorInfo(item_id, &accelerator)) { + actual_label += L'\t'; + actual_label += accelerator.GetShortcutText(); + } + labels_.push_back(actual_label); + + if (owner_draw_) { + if (icon.width() != 0 && icon.height() != 0) + data->icon = icon; + else + data->icon = delegate()->GetIcon(item_id); + } else { + mii.dwTypeData = const_cast<wchar_t*>(labels_.back().c_str()); + } + + InsertMenuItem(menu_, index, TRUE, &mii); +} + +MenuWin::MenuWin(MenuWin* parent) + : Menu(parent->delegate(), parent->anchor()), + menu_(CreatePopupMenu()), + owner_(parent->owner_), + is_menu_visible_(false), + owner_draw_(parent->owner_draw_) { +} + +void MenuWin::SetMenuInfo() { + const int num_items = GetMenuItemCount(menu_); + int sep_count = 0; + for (int i = 0; i < num_items; ++i) { + MENUITEMINFO mii_info; + mii_info.cbSize = sizeof(mii_info); + // Get the menu's original type. + mii_info.fMask = MIIM_FTYPE; + GetMenuItemInfo(menu_, i, MF_BYPOSITION, &mii_info); + // Set item states. + if (!(mii_info.fType & MF_SEPARATOR)) { + const int id = ChromeGetMenuItemID(menu_, i); + + MENUITEMINFO mii; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_STATE | MIIM_FTYPE | MIIM_DATA | MIIM_STRING; + // We also need MFT_STRING for owner drawn items in order to let Windows + // handle the accelerators for us. + mii.fType = MFT_STRING; + if (owner_draw_) + mii.fType |= MFT_OWNERDRAW; + // If the menu originally has radiocheck type, we should follow it. + if (mii_info.fType & MFT_RADIOCHECK) + mii.fType |= MFT_RADIOCHECK; + mii.fState = GetStateFlagsForItemID(id); + + // Validate the label. If there is a contextual label, use it, otherwise + // default to the static label + string16 label; + if (!delegate()->GetContextualLabel(id, &label)) + label = labels_[i - sep_count]; + + if (owner_draw_) { + item_data_[i - sep_count]->label = label; + mii.dwItemData = reinterpret_cast<ULONG_PTR>(item_data_[i - sep_count]); + } + mii.dwTypeData = const_cast<wchar_t*>(label.c_str()); + mii.cch = static_cast<UINT>(label.size()); + SetMenuItemInfo(menu_, i, true, &mii); + } else { + // Set data for owner drawn separators. Set dwItemData NULL to indicate + // a separator. + if (owner_draw_) { + MENUITEMINFO mii; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_FTYPE; + mii.fType = MFT_SEPARATOR | MFT_OWNERDRAW; + mii.dwItemData = NULL; + SetMenuItemInfo(menu_, i, true, &mii); + } + ++sep_count; + } + } + + for (size_t i = 0; i < submenus_.size(); ++i) + submenus_[i]->SetMenuInfo(); +} + +UINT MenuWin::GetStateFlagsForItemID(int item_id) const { + // Use the delegate to get enabled and checked state. + UINT flags = + delegate()->IsCommandEnabled(item_id) ? MFS_ENABLED : MFS_DISABLED; + + if (delegate()->IsItemChecked(item_id)) + flags |= MFS_CHECKED; + + if (delegate()->IsItemDefault(item_id)) + flags |= MFS_DEFAULT; + + return flags; +} + +DWORD MenuWin::GetTPMAlignFlags() const { + // The manner in which we handle the menu alignment depends on whether or not + // the menu is displayed within a mirrored view. If the UI is mirrored, the + // alignment needs to be fliped so that instead of aligning the menu to the + // right of the point, we align it to the left and vice versa. + DWORD align_flags = TPM_TOPALIGN; + switch (anchor()) { + case TOPLEFT: + if (delegate()->IsRightToLeftUILayout()) { + align_flags |= TPM_RIGHTALIGN; + } else { + align_flags |= TPM_LEFTALIGN; + } + break; + + case TOPRIGHT: + if (delegate()->IsRightToLeftUILayout()) { + align_flags |= TPM_LEFTALIGN; + } else { + align_flags |= TPM_RIGHTALIGN; + } + break; + + default: + NOTREACHED(); + return 0; + } + return align_flags; +} + +} // namespace views diff --git a/ui/views/controls/menu/menu_win.h b/ui/views/controls/menu/menu_win.h new file mode 100644 index 0000000..3b20f27 --- /dev/null +++ b/ui/views/controls/menu/menu_win.h @@ -0,0 +1,140 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_WIN_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_WIN_H_ +#pragma once + +#include <windows.h> + +#include <vector> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "ui/views/controls/menu/menu.h" + +namespace views { + +namespace { +class MenuHostWindow; +} + +/////////////////////////////////////////////////////////////////////////////// +// +// Menu class +// +// A wrapper around a Win32 HMENU handle that provides convenient APIs for +// menu construction, display and subsequent command execution. +// +/////////////////////////////////////////////////////////////////////////////// +class MenuWin : public Menu { + public: + // Construct a Menu using the specified controller to determine command + // state. + // delegate A Menu::Delegate implementation that provides more + // information about the Menu presentation. + // anchor An alignment hint for the popup menu. + // owner The window that the menu is being brought up relative + // to. Not actually used for anything but must not be + // NULL. + MenuWin(Delegate* d, AnchorPoint anchor, HWND owner); + // Alternatively, a Menu object can be constructed wrapping an existing + // HMENU. This can be used to use the convenience methods to insert + // menu items and manage label string ownership. However this kind of + // Menu object cannot use the delegate. + explicit MenuWin(HMENU hmenu); + virtual ~MenuWin(); + + // Overridden from Menu: + virtual void AddMenuItemWithIcon(int index, + int item_id, + const string16& label, + const SkBitmap& icon) OVERRIDE; + virtual Menu* AddSubMenuWithIcon(int index, + int item_id, + const string16& label, + const SkBitmap& icon) OVERRIDE; + virtual void AddSeparator(int index) OVERRIDE; + virtual void EnableMenuItemByID(int item_id, bool enabled) OVERRIDE; + virtual void EnableMenuItemAt(int index, bool enabled) OVERRIDE; + virtual void SetMenuLabel(int item_id, const string16& label) OVERRIDE; + virtual bool SetIcon(const SkBitmap& icon, int item_id) OVERRIDE; + virtual void RunMenuAt(int x, int y) OVERRIDE; + virtual void Cancel() OVERRIDE; + virtual int ItemCount() OVERRIDE; + + virtual HMENU GetMenuHandle() const { + return menu_; + } + + // Gets the Win32 TPM alignment flags for the specified AnchorPoint. + DWORD GetTPMAlignFlags() const; + + protected: + virtual void AddMenuItemInternal(int index, + int item_id, + const string16& label, + const SkBitmap& icon, + MenuItemType type) OVERRIDE; + + private: + friend class MenuHostWindow; + + // The data of menu items needed to display. + struct ItemData; + + void AddMenuItemInternal(int index, + int item_id, + const string16& label, + const SkBitmap& icon, + HMENU submenu, + MenuItemType type); + + explicit MenuWin(MenuWin* parent); + + // Sets menu information before displaying, including sub-menus. + void SetMenuInfo(); + + // Get all the state flags for the |fState| field of MENUITEMINFO for the + // item with the specified id. |delegate| is consulted if non-NULL about + // the state of the item in preference to |controller_|. + UINT GetStateFlagsForItemID(int item_id) const; + + // The Win32 Menu Handle we wrap + HMENU menu_; + + // The window that would receive WM_COMMAND messages when the user selects + // an item from the menu. + HWND owner_; + + // This list is used to store the default labels for the menu items. + // We may use contextual labels when RunMenu is called, so we must save + // a copy of default ones here. + std::vector<string16> labels_; + + // A flag to indicate whether this menu will be drawn by the Menu class. + // If it's true, all the menu items will be owner drawn. Otherwise, + // all the drawing will be done by Windows. + bool owner_draw_; + + // This list is to store the string labels and icons to display. It's used + // when owner_draw_ is true. We give MENUITEMINFO pointers to these + // structures to specify what we'd like to draw. If owner_draw_ is false, + // we only give MENUITEMINFO pointers to the labels_. + // The label member of the ItemData structure comes from either labels_ or + // the GetContextualLabel. + std::vector<ItemData*> item_data_; + + // Our sub-menus, if any. + std::vector<MenuWin*> submenus_; + + // Whether the menu is visible. + bool is_menu_visible_; + + DISALLOW_COPY_AND_ASSIGN(MenuWin); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_WIN_H_ diff --git a/ui/views/controls/menu/menu_wrapper.h b/ui/views/controls/menu/menu_wrapper.h new file mode 100644 index 0000000..2d21953 --- /dev/null +++ b/ui/views/controls/menu/menu_wrapper.h @@ -0,0 +1,71 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_MENU_WRAPPER_H_ +#define UI_VIEWS_CONTROLS_MENU_MENU_WRAPPER_H_ +#pragma once + +#include "ui/gfx/native_widget_types.h" +#include "views/views_export.h" + +namespace gfx { +class Point; +} + +namespace views { + +class Menu2; +class MenuListener; + +// An interface that wraps an object that implements a menu. +class VIEWS_EXPORT MenuWrapper { + public: + // All of the possible actions that can result from RunMenuAt. + enum MenuAction { + MENU_ACTION_NONE, // Menu cancelled, or never opened. + MENU_ACTION_SELECTED, // An item was selected. + MENU_ACTION_PREVIOUS, // User wants to navigate to the previous menu. + MENU_ACTION_NEXT, // User wants to navigate to the next menu. + }; + + virtual ~MenuWrapper() {} + + // Runs the menu at the specified point. This blocks until done. + virtual void RunMenuAt(const gfx::Point& point, int alignment) = 0; + + // Cancels the active menu. + virtual void CancelMenu() = 0; + + // Called when the model supplying data to this menu has changed, and the menu + // must be rebuilt. + virtual void Rebuild() = 0; + + // Called when the states of the items in the menu must be updated from the + // model. + virtual void UpdateStates() = 0; + + // Retrieve a native menu handle. + virtual gfx::NativeMenu GetNativeMenu() const = 0; + + // Get the result of the last call to RunMenuAt to determine whether an + // item was selected, the user navigated to a next or previous menu, or + // nothing. + virtual MenuAction GetMenuAction() const = 0; + + // Add a listener to receive a callback when the menu opens. + virtual void AddMenuListener(MenuListener* listener) = 0; + + // Remove a menu listener. + virtual void RemoveMenuListener(MenuListener* listener) = 0; + + // Sets the minimum width of the menu. + virtual void SetMinimumWidth(int width) = 0; + + // Creates the appropriate instance of this wrapper for the current platform. + static MenuWrapper* CreateWrapper(Menu2* menu); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_MENU_WRAPPER_H_ diff --git a/ui/views/controls/menu/native_menu_gtk.cc b/ui/views/controls/menu/native_menu_gtk.cc new file mode 100644 index 0000000..97ca02a --- /dev/null +++ b/ui/views/controls/menu/native_menu_gtk.cc @@ -0,0 +1,628 @@ +// 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 "ui/views/controls/menu/native_menu_gtk.h" + +#include <algorithm> +#include <map> +#include <string> + +#include "base/bind.h" +#include "base/i18n/rtl.h" +#include "base/message_loop.h" +#include "base/time.h" +#include "base/utf_string_conversions.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "ui/base/accelerators/accelerator.h" +#include "ui/base/keycodes/keyboard_code_conversion_gtk.h" +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/base/models/menu_model.h" +#include "ui/gfx/font.h" +#include "ui/gfx/gtk_util.h" +#include "ui/views/controls/menu/menu_2.h" +#include "ui/views/controls/menu/menu_listener.h" +#include "ui/views/controls/menu/nested_dispatcher_gtk.h" +#include "ui/views/widget/native_widget_gtk.h" +#include "views/views_delegate.h" + +namespace { + +const char kPositionString[] = "position"; +const char kAccelGroupString[] = "accel_group"; + +// Key for the property set on the gtk menu that gives the handle to the hosting +// NativeMenuGtk. +const char kNativeMenuGtkString[] = "native_menu_gtk"; + +// Data passed to the MenuPositionFunc from gtk_menu_popup +struct Position { + // The point to run the menu at. + gfx::Point point; + // The alignment of the menu at that point. + views::Menu2::Alignment alignment; +}; + +// Returns true if the menu item type specified can be executed as a command. +bool MenuTypeCanExecute(ui::MenuModel::ItemType type) { + return type == ui::MenuModel::TYPE_COMMAND || + type == ui::MenuModel::TYPE_CHECK || + type == ui::MenuModel::TYPE_RADIO; +} + +// A callback to gtk_container_foreach to remove all children. +// See |NativeMenuGtk::ResetMenu| for the usage. +void RemoveChildWidget(GtkWidget* widget, gpointer data) { + GtkWidget* parent = gtk_widget_get_parent(widget); + gtk_container_remove(GTK_CONTAINER(parent), widget); +} + +} // namespace + +namespace views { + +//////////////////////////////////////////////////////////////////////////////// +// NativeMenuGtk, public: + +NativeMenuGtk::NativeMenuGtk(Menu2* menu) + : parent_(NULL), + model_(menu->model()), + menu_(NULL), + menu_hidden_(true), + suppress_activate_signal_(false), + activated_menu_(NULL), + activated_index_(-1), + activate_factory_(this), + host_menu_(menu), + destroy_handler_id_(0), + expose_handler_id_(0), + menu_action_(MENU_ACTION_NONE), + nested_dispatcher_(NULL), + ignore_button_release_(true) { +} + +NativeMenuGtk::~NativeMenuGtk() { + if (nested_dispatcher_) { + // Menu is destroyed while its in message loop. + // Let nested dispatcher know the creator is deleted. + nested_dispatcher_->CreatorDestroyed(); + } + if (menu_) { + DCHECK(destroy_handler_id_); + // Don't call MenuDestroyed because menu2 has already been destroyed. + g_signal_handler_disconnect(menu_, destroy_handler_id_); + gtk_widget_destroy(menu_); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeMenuGtk, MenuWrapper implementation: + +void NativeMenuGtk::RunMenuAt(const gfx::Point& point, int alignment) { + activated_menu_ = NULL; + activated_index_ = -1; + menu_action_ = MENU_ACTION_NONE; + // ignore button release event unless mouse is pressed or moved. + ignore_button_release_ = true; + + UpdateStates(); + // Set the FREEZE UPDATE property to the menu's window so that WM maps + // the menu after the menu painted itself. + GtkWidget* popup_window = gtk_widget_get_ancestor(menu_, GTK_TYPE_WINDOW); + CHECK(popup_window); + NativeWidgetGtk::UpdateFreezeUpdatesProperty(GTK_WINDOW(popup_window), + true /* add */); + expose_handler_id_ = g_signal_connect_after(G_OBJECT(menu_), "expose_event", + G_CALLBACK(&OnExposeThunk), this); + + Position position = { point, static_cast<Menu2::Alignment>(alignment) }; + // TODO(beng): value of '1' will not work for context menus! + gtk_menu_popup(GTK_MENU(menu_), NULL, NULL, MenuPositionFunc, &position, 1, + gtk_get_current_event_time()); + DCHECK(menu_hidden_); + menu_hidden_ = false; + + FOR_EACH_OBSERVER(MenuListener, listeners_, OnMenuOpened()); + + // Listen for "hide" signal so that we know when to return from the blocking + // RunMenuAt call. + gint hide_handle_id = + g_signal_connect(menu_, "hide", G_CALLBACK(OnMenuHiddenThunk), this); + + gint move_handle_id = + g_signal_connect(menu_, "move-current", + G_CALLBACK(OnMenuMoveCurrentThunk), this); + gint after_move_handle_id = + g_signal_connect_after(menu_, "move-current", + G_CALLBACK(AfterMenuMoveCurrentThunk), this); + + model_->MenuWillShow(); + + // Block until menu is no longer shown by running a nested message loop. + nested_dispatcher_ = new NestedDispatcherGtk(this, true); + bool deleted = nested_dispatcher_->RunAndSelfDestruct(); + if (deleted) { + // The menu was destryed while menu is shown, so return immediately. + // Don't touch the instance which is already deleted. + return; + } + nested_dispatcher_ = NULL; + if (!menu_hidden_) { + // If this happens it means we haven't yet gotten the hide signal and + // someone else quit the message loop on us. + NOTREACHED(); + menu_hidden_ = true; + } + + g_signal_handler_disconnect(G_OBJECT(menu_), hide_handle_id); + g_signal_handler_disconnect(G_OBJECT(menu_), move_handle_id); + g_signal_handler_disconnect(G_OBJECT(menu_), after_move_handle_id); + + if (activated_menu_) { + MessageLoop::current()->PostTask(FROM_HERE, + base::Bind(&NativeMenuGtk::ProcessActivate, + activate_factory_.GetWeakPtr())); + } + + model_->MenuClosed(); +} + +void NativeMenuGtk::CancelMenu() { + gtk_widget_hide(menu_); +} + +void NativeMenuGtk::Rebuild() { + activated_menu_ = NULL; + + ResetMenu(); + + // Try to retrieve accelerator group as data from menu_; if null, create new + // one and store it as data into menu_. + // We store it as data so as to use the destroy notifier to get rid of initial + // reference count. For some reason, when we unref it ourselves (even in + // destructor), it would cause random crashes, depending on when gtk tries to + // access it. + GtkAccelGroup* accel_group = static_cast<GtkAccelGroup*>( + g_object_get_data(G_OBJECT(menu_), kAccelGroupString)); + if (!accel_group) { + accel_group = gtk_accel_group_new(); + g_object_set_data_full(G_OBJECT(menu_), kAccelGroupString, accel_group, + g_object_unref); + } + + std::map<int, GtkRadioMenuItem*> radio_groups_; + for (int i = 0; i < model_->GetItemCount(); ++i) { + ui::MenuModel::ItemType type = model_->GetTypeAt(i); + if (type == ui::MenuModel::TYPE_SEPARATOR) { + AddSeparatorAt(i); + } else if (type == ui::MenuModel::TYPE_RADIO) { + const int radio_group_id = model_->GetGroupIdAt(i); + std::map<int, GtkRadioMenuItem*>::const_iterator iter + = radio_groups_.find(radio_group_id); + if (iter == radio_groups_.end()) { + GtkWidget* new_menu_item = AddMenuItemAt(i, NULL, accel_group); + // |new_menu_item| is the first menu item for |radio_group_id| group. + radio_groups_.insert( + std::make_pair(radio_group_id, GTK_RADIO_MENU_ITEM(new_menu_item))); + } else { + AddMenuItemAt(i, iter->second, accel_group); + } + } else { + AddMenuItemAt(i, NULL, accel_group); + } + } + if (!menu_hidden_) + gtk_menu_reposition(GTK_MENU(menu_)); +} + +void NativeMenuGtk::UpdateStates() { + gtk_container_foreach(GTK_CONTAINER(menu_), &UpdateStateCallback, this); +} + +gfx::NativeMenu NativeMenuGtk::GetNativeMenu() const { + return menu_; +} + +NativeMenuGtk::MenuAction NativeMenuGtk::GetMenuAction() const { + return menu_action_; +} + +void NativeMenuGtk::AddMenuListener(MenuListener* listener) { + listeners_.AddObserver(listener); +} + +void NativeMenuGtk::RemoveMenuListener(MenuListener* listener) { + listeners_.RemoveObserver(listener); +} + +void NativeMenuGtk::SetMinimumWidth(int width) { + gtk_widget_set_size_request(menu_, width, -1); +} + +bool NativeMenuGtk::Dispatch(GdkEvent* event) { + if (menu_hidden_) { + // The menu has been closed but the message loop is still nested. Don't + // dispatch a message, otherwise we might spawn another message loop. + return false; // Exits the nested message loop. + } + switch (event->type) { + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: { + ignore_button_release_ = false; + gpointer data = NULL; + gdk_window_get_user_data(((GdkEventAny*)event)->window, &data); + GtkWidget* widget = reinterpret_cast<GtkWidget*>(data); + if (widget) { + GtkWidget* root = gtk_widget_get_toplevel(widget); + if (!g_object_get_data(G_OBJECT(root), kNativeMenuGtkString)) { + // The button event is not targeted at a menu, hide the menu and eat + // the event. If we didn't do this the button press is dispatched from + // the nested message loop and bad things can happen (like trying to + // spawn another menu. + gtk_menu_popdown(GTK_MENU(menu_)); + // In some cases we may not have gotten the hide event, but the menu + // will be in the process of hiding so set menu_hidden_ anyway. + menu_hidden_ = true; + return false; // Exits the nested message loop. + } + } + break; + } + case GDK_MOTION_NOTIFY: { + ignore_button_release_ = false; + break; + } + case GDK_BUTTON_RELEASE: { + if (ignore_button_release_) { + // Ignore if a release event happened without press event. + // Normally, release event is eaten by gtk when menu is opened + // in response to mouse press event. Since the renderer opens + // the context menu asyncrhonous after press event is handled, + // gtk sometimes does not eat it, which causes the menu to be + // closed. + return true; + } + break; + } + default: + break; + } + gtk_main_do_event(event); + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeMenuGtk, private: + +void NativeMenuGtk::OnMenuHidden(GtkWidget* widget) { + if (menu_hidden_) { + // The menu has been already hidden by us and we're in the process of + // quiting the message loop.. + return; + } + // Quit the nested message loop we spawned in RunMenuAt. + MessageLoop::current()->Quit(); + + // Menu can be closed before the menu is shown. + if (expose_handler_id_) { + g_signal_handler_disconnect(menu_, expose_handler_id_); + expose_handler_id_ = 0; + } + + menu_hidden_ = true; +} + +void NativeMenuGtk::OnMenuMoveCurrent(GtkWidget* menu_widget, + GtkMenuDirectionType focus_direction) { + GtkWidget* parent = GTK_MENU_SHELL(menu_widget)->parent_menu_shell; + GtkWidget* menu_item = GTK_MENU_SHELL(menu_widget)->active_menu_item; + GtkWidget* submenu = NULL; + if (menu_item) { + submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item)); + } + if (focus_direction == GTK_MENU_DIR_CHILD && submenu == NULL) { + GetAncestor()->menu_action_ = MENU_ACTION_NEXT; + gtk_menu_popdown(GTK_MENU(menu_widget)); + } else if (focus_direction == GTK_MENU_DIR_PARENT && parent == NULL) { + GetAncestor()->menu_action_ = MENU_ACTION_PREVIOUS; + gtk_menu_popdown(GTK_MENU(menu_widget)); + } +} + +void NativeMenuGtk::AfterMenuMoveCurrent(GtkWidget* menu_widget, + GtkMenuDirectionType focus_direction) { + SendAccessibilityEvent(); +} + +gboolean NativeMenuGtk::OnExpose(GtkWidget* widget, GdkEventExpose* event) { + GtkWidget* popup_window = gtk_widget_get_ancestor(menu_, GTK_TYPE_WINDOW); + CHECK(popup_window); + DCHECK(expose_handler_id_); + NativeWidgetGtk::UpdateFreezeUpdatesProperty(GTK_WINDOW(popup_window), + false /* remove */); + if (expose_handler_id_) { + g_signal_handler_disconnect(menu_, expose_handler_id_); + expose_handler_id_ = 0; + } + return false; +} + +void NativeMenuGtk::AddSeparatorAt(int index) { + GtkWidget* separator = gtk_separator_menu_item_new(); + gtk_widget_show(separator); + gtk_menu_append(menu_, separator); +} + +GtkWidget* NativeMenuGtk::AddMenuItemAt(int index, + GtkRadioMenuItem* radio_group, + GtkAccelGroup* accel_group) { + GtkWidget* menu_item = NULL; + std::string label = gfx::ConvertAcceleratorsFromWindowsStyle(UTF16ToUTF8( + model_->GetLabelAt(index))); + + ui::MenuModel::ItemType type = model_->GetTypeAt(index); + switch (type) { + case ui::MenuModel::TYPE_CHECK: + menu_item = gtk_check_menu_item_new_with_mnemonic(label.c_str()); + break; + case ui::MenuModel::TYPE_RADIO: + if (radio_group) { + menu_item = gtk_radio_menu_item_new_with_mnemonic_from_widget( + radio_group, label.c_str()); + } else { + // The item does not belong to any existing radio button groups. + menu_item = gtk_radio_menu_item_new_with_mnemonic(NULL, label.c_str()); + } + break; + case ui::MenuModel::TYPE_SUBMENU: + case ui::MenuModel::TYPE_COMMAND: { + SkBitmap icon; + // Create menu item with icon if icon exists. + if (model_->HasIcons() && model_->GetIconAt(index, &icon)) { + menu_item = gtk_image_menu_item_new_with_mnemonic(label.c_str()); + GdkPixbuf* pixbuf = gfx::GdkPixbufFromSkBitmap(&icon); + gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(menu_item), + gtk_image_new_from_pixbuf(pixbuf)); + g_object_unref(pixbuf); + + // Show the image even if the "gtk-menu-images" setting is turned off. + gtk_image_menu_item_set_always_show_image( + GTK_IMAGE_MENU_ITEM(menu_item), TRUE); + } else { + menu_item = gtk_menu_item_new_with_mnemonic(label.c_str()); + } + break; + } + default: + NOTREACHED(); + break; + } + + // Label font. + const gfx::Font* font = model_->GetLabelFontAt(index); + if (font) { + // The label item is the first child of the menu item. + GtkWidget* label_widget = GTK_BIN(menu_item)->child; + DCHECK(label_widget && GTK_IS_LABEL(label_widget)); + PangoFontDescription* pfd = font->GetNativeFont(); + gtk_widget_modify_font(label_widget, pfd); + pango_font_description_free(pfd); + } + + if (type == ui::MenuModel::TYPE_SUBMENU) { + Menu2* submenu = new Menu2(model_->GetSubmenuModelAt(index)); + static_cast<NativeMenuGtk*>(submenu->wrapper_.get())->set_parent(this); + g_object_set_data(G_OBJECT(menu_item), "submenu", submenu); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), + submenu->GetNativeMenu()); + g_signal_connect(submenu->GetNativeMenu(), "move-current", + G_CALLBACK(OnMenuMoveCurrentThunk), this); + } + + ui::Accelerator accelerator(ui::VKEY_UNKNOWN, false, false, false); + if (accel_group && model_->GetAcceleratorAt(index, &accelerator)) { + int gdk_modifiers = 0; + if (accelerator.IsShiftDown()) + gdk_modifiers |= GDK_SHIFT_MASK; + if (accelerator.IsCtrlDown()) + gdk_modifiers |= GDK_CONTROL_MASK; + if (accelerator.IsAltDown()) + gdk_modifiers |= GDK_MOD1_MASK; + gtk_widget_add_accelerator(menu_item, "activate", accel_group, + ui::GdkKeyCodeForWindowsKeyCode(accelerator.key_code(), false), + static_cast<GdkModifierType>(gdk_modifiers), GTK_ACCEL_VISIBLE); + } + + g_object_set_data(G_OBJECT(menu_item), kPositionString, + reinterpret_cast<void*>(index)); + g_signal_connect(menu_item, "activate", G_CALLBACK(CallActivate), this); + UpdateMenuItemState(menu_item, false); + gtk_widget_show(menu_item); + gtk_menu_append(menu_, menu_item); + + return menu_item; +} + +void NativeMenuGtk::ResetMenu() { + if (!menu_) { + menu_ = gtk_menu_new(); + g_object_set_data( + G_OBJECT(GTK_MENU(menu_)->toplevel), kNativeMenuGtkString, this); + destroy_handler_id_ = g_signal_connect( + menu_, "destroy", G_CALLBACK(NativeMenuGtk::MenuDestroyed), host_menu_); + } else { + gtk_container_foreach(GTK_CONTAINER(menu_), RemoveChildWidget, NULL); + } +} + +void NativeMenuGtk::UpdateMenuItemState(GtkWidget* menu_item, bool recurse) { + int index = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menu_item), + kPositionString)); + + gtk_widget_set_sensitive(menu_item, model_->IsEnabledAt(index)); + if (GTK_IS_CHECK_MENU_ITEM(menu_item)) { + suppress_activate_signal_ = true; + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menu_item), + model_->IsItemCheckedAt(index)); + suppress_activate_signal_ = false; + } + + if (recurse && GTK_IS_MENU_ITEM(menu_item)) { + // Recurse into submenus. + if (gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item))) { + Menu2* submenu = + reinterpret_cast<Menu2*>(g_object_get_data(G_OBJECT(menu_item), + "submenu")); + if (submenu) + submenu->UpdateStates(); + } + } +} + +// static +void NativeMenuGtk::UpdateStateCallback(GtkWidget* menu_item, gpointer data) { + NativeMenuGtk* menu = reinterpret_cast<NativeMenuGtk*>(data); + menu->UpdateMenuItemState(menu_item, true); +} + +// static +void NativeMenuGtk::MenuPositionFunc(GtkMenu* menu, + int* x, + int* y, + gboolean* push_in, + void* data) { + Position* position = reinterpret_cast<Position*>(data); + + GtkRequisition menu_req; + gtk_widget_size_request(GTK_WIDGET(menu), &menu_req); + + *x = position->point.x(); + *y = position->point.y(); + views::Menu2::Alignment alignment = position->alignment; + if (base::i18n::IsRTL()) { + switch (alignment) { + case Menu2::ALIGN_TOPRIGHT: + alignment = Menu2::ALIGN_TOPLEFT; + break; + case Menu2::ALIGN_TOPLEFT: + alignment = Menu2::ALIGN_TOPRIGHT; + break; + default: + NOTREACHED(); + break; + } + } + if (alignment == Menu2::ALIGN_TOPRIGHT) + *x -= menu_req.width; + + // Make sure the popup fits on screen. + GdkScreen* screen = gtk_widget_get_screen(GTK_WIDGET(menu)); + *x = std::max(0, std::min(gdk_screen_get_width(screen) - menu_req.width, *x)); + *y = std::max(0, std::min(gdk_screen_get_height(screen) - menu_req.height, + *y)); + + *push_in = FALSE; +} + +void NativeMenuGtk::OnActivate(GtkMenuItem* menu_item) { + if (suppress_activate_signal_) + return; + int position = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menu_item), + kPositionString)); + // Ignore the signal if it's sent to an inactive checked radio item. + // + // Suppose there are three radio items A, B, C, and A is now being + // checked. If you click C, "activate" signal will be sent to A and C. + // Here, we ignore the signal sent to A. + if (GTK_IS_RADIO_MENU_ITEM(menu_item) && + !gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menu_item))) { + return; + } + + // NOTE: we get activate messages for submenus when first shown. + if (model_->IsEnabledAt(position) && + MenuTypeCanExecute(model_->GetTypeAt(position))) { + NativeMenuGtk* ancestor = GetAncestor(); + ancestor->activated_menu_ = this; + activated_index_ = position; + ancestor->menu_action_ = MENU_ACTION_SELECTED; + } +} + +// static +void NativeMenuGtk::CallActivate(GtkMenuItem* menu_item, + NativeMenuGtk* native_menu) { + native_menu->OnActivate(menu_item); +} + +NativeMenuGtk* NativeMenuGtk::GetAncestor() { + NativeMenuGtk* ancestor = this; + while (ancestor->parent_) + ancestor = ancestor->parent_; + return ancestor; +} + +void NativeMenuGtk::ProcessActivate() { + if (activated_menu_) + activated_menu_->Activate(); +} + +void NativeMenuGtk::Activate() { + if (model_->IsEnabledAt(activated_index_) && + MenuTypeCanExecute(model_->GetTypeAt(activated_index_))) { + model_->ActivatedAt(activated_index_); + } +} + +void NativeMenuGtk::SendAccessibilityEvent() { + // Find the focused menu item, recursing into submenus as needed. + GtkWidget* menu = menu_; + GtkWidget* menu_item = GTK_MENU_SHELL(menu_)->active_menu_item; + if (!menu_item) + return; + GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item)); + while (submenu && GTK_MENU_SHELL(submenu)->active_menu_item) { + menu = submenu; + menu_item = GTK_MENU_SHELL(menu)->active_menu_item; + if (!menu_item) + return; + submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item)); + } + + // Figure out the item index and total number of items. + GList* items = gtk_container_get_children(GTK_CONTAINER(menu)); + guint count = g_list_length(items); + int index = g_list_index(items, static_cast<gconstpointer>(menu_item)); + + // Get the menu item's label. + std::string name; + name = gtk_menu_item_get_label(GTK_MENU_ITEM(menu_item)); + + if (ViewsDelegate::views_delegate) { + ViewsDelegate::views_delegate->NotifyMenuItemFocused(string16(), + UTF8ToUTF16(name), + index, + count, + submenu != NULL); + } +} + +// static +void NativeMenuGtk::MenuDestroyed(GtkWidget* widget, Menu2* menu2) { + NativeMenuGtk* native_menu = + static_cast<NativeMenuGtk*>(menu2->wrapper_.get()); + // The native gtk widget has already been destroyed. + native_menu->menu_ = NULL; + delete menu2; +} + +//////////////////////////////////////////////////////////////////////////////// +// MenuWrapper, public: + +// static +MenuWrapper* MenuWrapper::CreateWrapper(Menu2* menu) { + return new NativeMenuGtk(menu); +} + +} // namespace views diff --git a/ui/views/controls/menu/native_menu_gtk.h b/ui/views/controls/menu/native_menu_gtk.h new file mode 100644 index 0000000..debc254 --- /dev/null +++ b/ui/views/controls/menu/native_menu_gtk.h @@ -0,0 +1,162 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_GTK_H_ +#define UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_GTK_H_ +#pragma once + +#include <gtk/gtk.h> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/weak_ptr.h" +#include "base/message_loop.h" +#include "base/observer_list.h" +#include "ui/base/gtk/gtk_signal.h" +#include "ui/views/controls/menu/menu_wrapper.h" + +namespace ui { +class MenuModel; +} + +namespace views { + +class NestedDispatcherGtk; + +// A Gtk implementation of MenuWrapper. +// +// NOTE: On windows the activate message is not sent immediately when an item +// is selected. Instead a message is added to the queue that is processed later. +// To simulate that (and avoid having to deal with different behavior between +// the platforms) we mimick that by posting a task after a menu item is selected +// then notify. +// +// TODO(beng): rename to MenuGtk once the old class is dead. +class NativeMenuGtk : public MenuWrapper, + public MessageLoopForUI::Dispatcher { + public: + explicit NativeMenuGtk(Menu2* menu); + virtual ~NativeMenuGtk(); + + // Overridden from MenuWrapper: + virtual void RunMenuAt(const gfx::Point& point, int alignment) OVERRIDE; + virtual void CancelMenu() OVERRIDE; + virtual void Rebuild() OVERRIDE; + virtual void UpdateStates() OVERRIDE; + virtual gfx::NativeMenu GetNativeMenu() const OVERRIDE; + virtual MenuAction GetMenuAction() const OVERRIDE; + virtual void AddMenuListener(MenuListener* listener) OVERRIDE; + virtual void RemoveMenuListener(MenuListener* listener) OVERRIDE; + virtual void SetMinimumWidth(int width) OVERRIDE; + + // Overriden from MessageLoopForUI::Dispatcher: + virtual bool Dispatch(GdkEvent* event) OVERRIDE; + + private: + CHROMEGTK_CALLBACK_0(NativeMenuGtk, void, OnMenuHidden); + CHROMEGTK_CALLBACK_1(NativeMenuGtk, void, OnMenuMoveCurrent, + GtkMenuDirectionType); + CHROMEGTK_CALLBACK_1(NativeMenuGtk, void, AfterMenuMoveCurrent, + GtkMenuDirectionType); + CHROMEGTK_CALLBACK_1(NativeMenuGtk, gboolean, OnExpose, GdkEventExpose*); + + void AddSeparatorAt(int index); + GtkWidget* AddMenuItemAt(int index, GtkRadioMenuItem* radio_group, + GtkAccelGroup* accel_group); + + void ResetMenu(); + + // Updates the menu item's state. + void UpdateMenuItemState(GtkWidget* menu_item, bool recurse); + + static void UpdateStateCallback(GtkWidget* menu_item, gpointer data); + + // Callback for gtk_menu_popup to position the menu. + static void MenuPositionFunc(GtkMenu* menu, int* x, int* y, gboolean* push_in, + void* data); + + // Event handlers: + void OnActivate(GtkMenuItem* menu_item); + + // Gtk signal handlers. + static void CallActivate(GtkMenuItem* menu_item, NativeMenuGtk* native_menu); + + // Sets the parent of this menu. + void set_parent(NativeMenuGtk* parent) { parent_ = parent; } + + // Returns the root of the menu tree. + NativeMenuGtk* GetAncestor(); + + // Callback that we should really process the menu activation. + // See description above class for why we delay processing activation. + void ProcessActivate(); + + // Notifies the model the user selected an item. + void Activate(); + + void SendAccessibilityEvent(); + + // A callback to delete menu2 object when the native widget is + // destroyed first. + static void MenuDestroyed(GtkWidget* widget, Menu2* menu2); + + // If we're a submenu, this is the parent. + NativeMenuGtk* parent_; + + ui::MenuModel* model_; + + GtkWidget* menu_; + + // Has the menu been hidden? + // NOTE: this is maintained by us and do to the asynchronous nature of X may + // be out of sync with whether the window is actually hidden. None-the-less if + // true the menu is either truly hidden or in the process of hiding. + bool menu_hidden_; + + // A flag used to avoid misfiring ActivateAt call on the menu model. + // This is necessary as GTK menu fires an activate signal even when the + // state is changed by |UpdateStates()| API. + bool suppress_activate_signal_; + + // If the user selects something from the menu this is the menu they selected + // it from. When an item is selected menu_activated_ on the root ancestor is + // set to the menu the user selected and after the nested message loop exits + // Activate is invoked on this menu. + NativeMenuGtk* activated_menu_; + + // The index of the item the user selected. This is set on the actual menu the + // user selects and not the root. + int activated_index_; + + // Used when a menu item is selected. See description above class as to why + // we do this. + base::WeakPtrFactory<NativeMenuGtk> activate_factory_; + + // A eference to the hosting menu2 object and signal handler id + // used to delete the menu2 when its native menu gtk is destroyed first. + Menu2* host_menu_; + gulong destroy_handler_id_; + gulong expose_handler_id_; + + // The action that took place during the call to RunMenuAt. + MenuAction menu_action_; + + // A list of listeners to call when the menu opens. + ObserverList<MenuListener> listeners_; + + // Nested dispatcher object that can outlive this object. + // This is to deal with the menu being deleted while the nested + // message loop is handled. see http://crosbug.com/7228 . + NestedDispatcherGtk* nested_dispatcher_; + + // A flag used to detect a button release event without button press or move. + // see http://crosbug.com/8718. + bool ignore_button_release_; + + DISALLOW_COPY_AND_ASSIGN(NativeMenuGtk); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_GTK_H_ diff --git a/ui/views/controls/menu/native_menu_host.h b/ui/views/controls/menu/native_menu_host.h new file mode 100644 index 0000000..789f6f6 --- /dev/null +++ b/ui/views/controls/menu/native_menu_host.h @@ -0,0 +1,34 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_HOST_H_ +#define UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_HOST_H_ +#pragma once + +#include "ui/gfx/native_widget_types.h" + +namespace views { + +class NativeWidget; + +namespace internal { +class NativeMenuHostDelegate; +} + +class NativeMenuHost { + public: + virtual ~NativeMenuHost() {} + + static NativeMenuHost* CreateNativeMenuHost( + internal::NativeMenuHostDelegate* delegate); + + // Starts capturing input events. + virtual void StartCapturing() = 0; + + virtual NativeWidget* AsNativeWidget() = 0; +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_HOST_H_ diff --git a/ui/views/controls/menu/native_menu_host_delegate.h b/ui/views/controls/menu/native_menu_host_delegate.h new file mode 100644 index 0000000..592cf6c --- /dev/null +++ b/ui/views/controls/menu/native_menu_host_delegate.h @@ -0,0 +1,32 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_HOST_DELEGATE_H_ +#define UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_HOST_DELEGATE_H_ +#pragma once +namespace views { + +class MenuHost; + +namespace internal { + +class NativeWidgetDelegate; + +class NativeMenuHostDelegate { + public: + virtual ~NativeMenuHostDelegate() {} + + // Called when the NativeMenuHost is being destroyed. + virtual void OnNativeMenuHostDestroy() = 0; + + // Called when the NativeMenuHost is losing input capture. + virtual void OnNativeMenuHostCancelCapture() = 0; + + virtual NativeWidgetDelegate* AsNativeWidgetDelegate() = 0; +}; + +} // namespace internal +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_HOST_DELEGATE_H_ diff --git a/ui/views/controls/menu/native_menu_views.cc b/ui/views/controls/menu/native_menu_views.cc new file mode 100644 index 0000000..9abc19d --- /dev/null +++ b/ui/views/controls/menu/native_menu_views.cc @@ -0,0 +1,171 @@ +// 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 "ui/views/controls/menu/native_menu_views.h" + +#include "base/logging.h" +#include "base/utf_string_conversions.h" +#include "ui/base/models/menu_model.h" +#include "ui/gfx/canvas_skia.h" +#include "ui/gfx/skia_util.h" +#include "ui/views/controls/menu/menu_2.h" +#include "ui/views/controls/menu/menu_item_view.h" +#include "ui/views/controls/menu/menu_listener.h" +#include "ui/views/controls/menu/menu_runner.h" +#include "ui/views/controls/menu/submenu_view.h" + +namespace views { + +NativeMenuViews::NativeMenuViews(Menu2* menu) + : model_(menu->model()), + ALLOW_THIS_IN_INITIALIZER_LIST(root_(new MenuItemView(this))), + menu_runner_(new MenuRunner(root_)) { +} + +NativeMenuViews::~NativeMenuViews() { +} + +// MenuWrapper implementation: +void NativeMenuViews::RunMenuAt(const gfx::Point& point, int alignment) { + // TODO: this should really return the value from MenuRunner. + UpdateStates(); + if (menu_runner_->RunMenuAt(NULL, NULL, gfx::Rect(point, gfx::Size()), + alignment == Menu2::ALIGN_TOPLEFT ? MenuItemView::TOPLEFT : + MenuItemView::TOPRIGHT, MenuRunner::HAS_MNEMONICS) == + MenuRunner::MENU_DELETED) + return; +} + +void NativeMenuViews::CancelMenu() { + NOTIMPLEMENTED(); +} + +void NativeMenuViews::Rebuild() { + if (SubmenuView* submenu = root_->GetSubmenu()) + submenu->RemoveAllChildViews(true); + AddMenuItemsFromModel(root_, model_); +} + +void NativeMenuViews::UpdateStates() { + SubmenuView* submenu = root_->CreateSubmenu(); + UpdateMenuFromModel(submenu, model_); +} + +gfx::NativeMenu NativeMenuViews::GetNativeMenu() const { + NOTIMPLEMENTED(); + return NULL; +} + +MenuWrapper::MenuAction NativeMenuViews::GetMenuAction() const { + NOTIMPLEMENTED(); + return MENU_ACTION_NONE; +} + +void NativeMenuViews::AddMenuListener(MenuListener* listener) { + NOTIMPLEMENTED(); +} + +void NativeMenuViews::RemoveMenuListener(MenuListener* listener) { + NOTIMPLEMENTED(); +} + +void NativeMenuViews::SetMinimumWidth(int width) { + NOTIMPLEMENTED(); +} + +// MenuDelegate implementation + +bool NativeMenuViews::IsItemChecked(int cmd) const { + int index; + ui::MenuModel* model = model_; + if (!ui::MenuModel::GetModelAndIndexForCommandId(cmd, &model, &index)) + return false; + return model->IsItemCheckedAt(index); +} + +bool NativeMenuViews::IsCommandEnabled(int cmd) const { + int index; + ui::MenuModel* model = model_; + if (!ui::MenuModel::GetModelAndIndexForCommandId(cmd, &model, &index)) + return false; + return model->IsEnabledAt(index); +} + +void NativeMenuViews::ExecuteCommand(int cmd) { + int index; + ui::MenuModel* model = model_; + if (!ui::MenuModel::GetModelAndIndexForCommandId(cmd, &model, &index)) + return; + model->ActivatedAt(index); +} + +bool NativeMenuViews::GetAccelerator(int id, ui::Accelerator* accelerator) { + int index; + ui::MenuModel* model = model_; + if (!ui::MenuModel::GetModelAndIndexForCommandId(id, &model, &index)) + return false; + + ui::Accelerator menu_accelerator; + if (!model->GetAcceleratorAt(index, &menu_accelerator)) + return false; + + *accelerator = ui::Accelerator(menu_accelerator.key_code(), + menu_accelerator.modifiers()); + return true; +} + +// private +void NativeMenuViews::AddMenuItemsFromModel(MenuItemView* parent, + ui::MenuModel* model) { + for (int i = 0; i < model->GetItemCount(); ++i) { + int index = i + model->GetFirstItemIndex(NULL); + MenuItemView* child = parent->AppendMenuItemFromModel(model, index, + model->GetCommandIdAt(index)); + + if (child && child->GetType() == MenuItemView::SUBMENU) { + AddMenuItemsFromModel(child, model->GetSubmenuModelAt(index)); + } + } +} + +void NativeMenuViews::UpdateMenuFromModel(SubmenuView* menu, + ui::MenuModel* model) { + for (int i = 0, sep = 0; i < model->GetItemCount(); ++i) { + int index = i + model->GetFirstItemIndex(NULL); + if (model->GetTypeAt(index) == ui::MenuModel::TYPE_SEPARATOR) { + ++sep; + continue; + } + + // The submenu excludes the separators when counting the menu-items + // in it. So exclude the number of separators to get the correct index. + MenuItemView* mitem = menu->GetMenuItemAt(index - sep); + mitem->SetVisible(model->IsVisibleAt(index)); + mitem->SetEnabled(model->IsEnabledAt(index)); + if (model->IsItemDynamicAt(index)) { + mitem->SetTitle(model->GetLabelAt(index)); + } + + SkBitmap icon; + if (model->GetIconAt(index, &icon)) { + // TODO(atwilson): Support removing the icon dynamically + // (http://crbug.com/66508). + mitem->SetIcon(icon); + } + + if (model->GetTypeAt(index) == ui::MenuModel::TYPE_SUBMENU) { + DCHECK(mitem->HasSubmenu()); + UpdateMenuFromModel(mitem->GetSubmenu(), model->GetSubmenuModelAt(index)); + } + } +} + +// MenuWrapper, public: + +// static +MenuWrapper* MenuWrapper::CreateWrapper(Menu2* menu) { + return new NativeMenuViews(menu); +} + +} // namespace views diff --git a/ui/views/controls/menu/native_menu_views.h b/ui/views/controls/menu/native_menu_views.h new file mode 100644 index 0000000..9f5d92d --- /dev/null +++ b/ui/views/controls/menu/native_menu_views.h @@ -0,0 +1,62 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_VIEWS_H_ +#define UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_VIEWS_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "ui/views/controls/menu/menu_delegate.h" +#include "ui/views/controls/menu/menu_wrapper.h" + +namespace ui { +class MenuModel; +} + +namespace views { + +class MenuItemView; +class MenuRunner; + +// A views implementation of MenuWrapper, used currently for touchui. +class NativeMenuViews : public MenuWrapper, + public MenuDelegate { + public: + explicit NativeMenuViews(Menu2* menu); + virtual ~NativeMenuViews(); + + // Overridden from MenuWrapper: + virtual void RunMenuAt(const gfx::Point& point, int alignment) OVERRIDE; + virtual void CancelMenu() OVERRIDE; + virtual void Rebuild() OVERRIDE; + virtual void UpdateStates() OVERRIDE; + virtual gfx::NativeMenu GetNativeMenu() const OVERRIDE; + virtual MenuAction GetMenuAction() const OVERRIDE; + virtual void AddMenuListener(MenuListener* listener) OVERRIDE; + virtual void RemoveMenuListener(MenuListener* listener) OVERRIDE; + virtual void SetMinimumWidth(int width) OVERRIDE; + + // Overridden from MenuDelegate: + virtual bool IsItemChecked(int id) const OVERRIDE; + virtual bool IsCommandEnabled(int id) const OVERRIDE; + virtual void ExecuteCommand(int id) OVERRIDE; + virtual bool GetAccelerator(int id, ui::Accelerator* accelerator) OVERRIDE; + + private: + void AddMenuItemsFromModel(MenuItemView* parent, ui::MenuModel* model); + void UpdateMenuFromModel(SubmenuView* menu, ui::MenuModel* model); + + // The attached model and delegate. Does not assume ownership. + ui::MenuModel* model_; + MenuItemView* root_; + scoped_ptr<MenuRunner> menu_runner_; + + DISALLOW_COPY_AND_ASSIGN(NativeMenuViews); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_VIEWS_H_ diff --git a/ui/views/controls/menu/native_menu_win.cc b/ui/views/controls/menu/native_menu_win.cc new file mode 100644 index 0000000..f0dff18 --- /dev/null +++ b/ui/views/controls/menu/native_menu_win.cc @@ -0,0 +1,765 @@ +// 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 "ui/views/controls/menu/native_menu_win.h" + +#include <Windowsx.h> + +#include "base/bind.h" +#include "base/logging.h" +#include "base/message_loop.h" +#include "base/stl_util.h" +#include "base/string_util.h" +#include "base/win/wrapped_window_proc.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "ui/base/accelerators/accelerator.h" +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/l10n/l10n_util_win.h" +#include "ui/base/win/hwnd_util.h" +#include "ui/gfx/canvas_skia.h" +#include "ui/gfx/font.h" +#include "ui/gfx/native_theme.h" +#include "ui/gfx/rect.h" +#include "ui/views/controls/menu/menu_2.h" +#include "ui/views/controls/menu/menu_config.h" +#include "ui/views/controls/menu/menu_listener.h" + +using gfx::NativeTheme; + +namespace views { + +// The width of an icon, including the pixels between the icon and +// the item label. +static const int kIconWidth = 23; +// Margins between the top of the item and the label. +static const int kItemTopMargin = 3; +// Margins between the bottom of the item and the label. +static const int kItemBottomMargin = 4; +// Margins between the left of the item and the icon. +static const int kItemLeftMargin = 4; +// Margins between the right of the item and the label. +static const int kItemRightMargin = 10; +// The width for displaying the sub-menu arrow. +static const int kArrowWidth = 10; + +struct NativeMenuWin::ItemData { + // The Windows API requires that whoever creates the menus must own the + // strings used for labels, and keep them around for the lifetime of the + // created menu. So be it. + string16 label; + + // Someone needs to own submenus, it may as well be us. + scoped_ptr<Menu2> submenu; + + // We need a pointer back to the containing menu in various circumstances. + NativeMenuWin* native_menu_win; + + // The index of the item within the menu's model. + int model_index; +}; + +// Returns the NativeMenuWin for a particular HMENU. +static NativeMenuWin* GetNativeMenuWinFromHMENU(HMENU hmenu) { + MENUINFO mi = {0}; + mi.cbSize = sizeof(mi); + mi.fMask = MIM_MENUDATA | MIM_STYLE; + GetMenuInfo(hmenu, &mi); + return reinterpret_cast<NativeMenuWin*>(mi.dwMenuData); +} + +// A window that receives messages from Windows relevant to the native menu +// structure we have constructed in NativeMenuWin. +class NativeMenuWin::MenuHostWindow { + public: + MenuHostWindow(NativeMenuWin* parent) : parent_(parent) { + RegisterClass(); + hwnd_ = CreateWindowEx(l10n_util::GetExtendedStyles(), kWindowClassName, + L"", 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL); + ui::CheckWindowCreated(hwnd_); + ui::SetWindowUserData(hwnd_, this); + } + + ~MenuHostWindow() { + DestroyWindow(hwnd_); + } + + HWND hwnd() const { return hwnd_; } + + private: + static const wchar_t* kWindowClassName; + + void RegisterClass() { + static bool registered = false; + if (registered) + return; + + WNDCLASSEX wcex = {0}; + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.style = CS_DBLCLKS; + wcex.lpfnWndProc = base::win::WrappedWindowProc<&MenuHostWindowProc>; + wcex.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW+1); + wcex.lpszClassName = kWindowClassName; + ATOM clazz = RegisterClassEx(&wcex); + DCHECK(clazz); + registered = true; + } + + // Converts the WPARAM value passed to WM_MENUSELECT into an index + // corresponding to the menu item that was selected. + int GetMenuItemIndexFromWPARAM(HMENU menu, WPARAM w_param) const { + int count = GetMenuItemCount(menu); + // For normal command menu items, Windows passes a command id as the LOWORD + // of WPARAM for WM_MENUSELECT. We need to walk forward through the menu + // items to find an item with a matching ID. Ugh! + for (int i = 0; i < count; ++i) { + MENUITEMINFO mii = {0}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_ID; + GetMenuItemInfo(menu, i, MF_BYPOSITION, &mii); + if (mii.wID == w_param) + return i; + } + // If we didn't find a matching command ID, this means a submenu has been + // selected instead, and rather than passing a command ID in + // LOWORD(w_param), Windows has actually passed us a position, so we just + // return it. + return w_param; + } + + NativeMenuWin::ItemData* GetItemData(ULONG_PTR item_data) { + return reinterpret_cast<NativeMenuWin::ItemData*>(item_data); + } + + // Called when the user selects a specific item. + void OnMenuCommand(int position, HMENU menu) { + NativeMenuWin* menu_win = GetNativeMenuWinFromHMENU(menu); + ui::MenuModel* model = menu_win->model_; + NativeMenuWin* root_menu = menu_win; + while (root_menu->parent_) + root_menu = root_menu->parent_; + + // Only notify the model if it didn't already send out notification. + // See comment in MenuMessageHook for details. + if (root_menu->menu_action_ == MenuWrapper::MENU_ACTION_NONE) + model->ActivatedAt(position); + } + + // Called as the user moves their mouse or arrows through the contents of the + // menu. + void OnMenuSelect(WPARAM w_param, HMENU menu) { + if (!menu) + return; // menu is null when closing on XP. + + int position = GetMenuItemIndexFromWPARAM(menu, w_param); + if (position >= 0) + GetNativeMenuWinFromHMENU(menu)->model_->HighlightChangedTo(position); + } + + // Called by Windows to measure the size of an owner-drawn menu item. + void OnMeasureItem(WPARAM w_param, MEASUREITEMSTRUCT* measure_item_struct) { + NativeMenuWin::ItemData* data = GetItemData(measure_item_struct->itemData); + if (data) { + gfx::Font font; + measure_item_struct->itemWidth = font.GetStringWidth(data->label) + + kIconWidth + kItemLeftMargin + kItemRightMargin - + GetSystemMetrics(SM_CXMENUCHECK); + if (data->submenu.get()) + measure_item_struct->itemWidth += kArrowWidth; + // If the label contains an accelerator, make room for tab. + if (data->label.find(L'\t') != string16::npos) + measure_item_struct->itemWidth += font.GetStringWidth(L" "); + measure_item_struct->itemHeight = + font.GetHeight() + kItemBottomMargin + kItemTopMargin; + } else { + // Measure separator size. + measure_item_struct->itemHeight = GetSystemMetrics(SM_CYMENU) / 2; + measure_item_struct->itemWidth = 0; + } + } + + // Called by Windows to paint an owner-drawn menu item. + void OnDrawItem(UINT w_param, DRAWITEMSTRUCT* draw_item_struct) { + HDC dc = draw_item_struct->hDC; + COLORREF prev_bg_color, prev_text_color; + + // Set background color and text color + if (draw_item_struct->itemState & ODS_SELECTED) { + prev_bg_color = SetBkColor(dc, GetSysColor(COLOR_HIGHLIGHT)); + prev_text_color = SetTextColor(dc, GetSysColor(COLOR_HIGHLIGHTTEXT)); + } else { + prev_bg_color = SetBkColor(dc, GetSysColor(COLOR_MENU)); + if (draw_item_struct->itemState & ODS_DISABLED) + prev_text_color = SetTextColor(dc, GetSysColor(COLOR_GRAYTEXT)); + else + prev_text_color = SetTextColor(dc, GetSysColor(COLOR_MENUTEXT)); + } + + if (draw_item_struct->itemData) { + NativeMenuWin::ItemData* data = GetItemData(draw_item_struct->itemData); + // Draw the background. + HBRUSH hbr = CreateSolidBrush(GetBkColor(dc)); + FillRect(dc, &draw_item_struct->rcItem, hbr); + DeleteObject(hbr); + + // Draw the label. + RECT rect = draw_item_struct->rcItem; + rect.top += kItemTopMargin; + // Should we add kIconWidth only when icon.width() != 0 ? + rect.left += kItemLeftMargin + kIconWidth; + rect.right -= kItemRightMargin; + UINT format = DT_TOP | DT_SINGLELINE; + // Check whether the mnemonics should be underlined. + BOOL underline_mnemonics; + SystemParametersInfo(SPI_GETKEYBOARDCUES, 0, &underline_mnemonics, 0); + if (!underline_mnemonics) + format |= DT_HIDEPREFIX; + gfx::Font font; + HGDIOBJ old_font = + static_cast<HFONT>(SelectObject(dc, font.GetNativeFont())); + + // If an accelerator is specified (with a tab delimiting the rest of the + // label from the accelerator), we have to justify the fist part on the + // left and the accelerator on the right. + // TODO(jungshik): This will break in RTL UI. Currently, he/ar use the + // window system UI font and will not hit here. + string16 label = data->label; + string16 accel; + string16::size_type tab_pos = label.find(L'\t'); + if (tab_pos != string16::npos) { + accel = label.substr(tab_pos); + label = label.substr(0, tab_pos); + } + DrawTextEx(dc, const_cast<wchar_t*>(label.data()), + static_cast<int>(label.size()), &rect, format | DT_LEFT, NULL); + if (!accel.empty()) + DrawTextEx(dc, const_cast<wchar_t*>(accel.data()), + static_cast<int>(accel.size()), &rect, + format | DT_RIGHT, NULL); + SelectObject(dc, old_font); + + ui::MenuModel::ItemType type = + data->native_menu_win->model_->GetTypeAt(data->model_index); + + // Draw the icon after the label, otherwise it would be covered + // by the label. + SkBitmap icon; + if (data->native_menu_win->model_->GetIconAt(data->model_index, &icon)) { + // We currently don't support items with both icons and checkboxes. + DCHECK(type != ui::MenuModel::TYPE_CHECK); + gfx::CanvasSkia canvas(icon.width(), icon.height(), false); + canvas.sk_canvas()->drawColor(SK_ColorBLACK, SkXfermode::kClear_Mode); + canvas.DrawBitmapInt(icon, 0, 0); + skia::DrawToNativeContext( + canvas.sk_canvas(), dc, + draw_item_struct->rcItem.left + kItemLeftMargin, + draw_item_struct->rcItem.top + (draw_item_struct->rcItem.bottom - + draw_item_struct->rcItem.top - icon.height()) / 2, NULL); + } else if (type == ui::MenuModel::TYPE_CHECK && + data->native_menu_win->model_->IsItemCheckedAt( + data->model_index)) { + // Manually render a checkbox. + const MenuConfig& config = MenuConfig::instance(); + NativeTheme::State state; + if (draw_item_struct->itemState & ODS_DISABLED) { + state = NativeTheme::kDisabled; + } else { + state = draw_item_struct->itemState & ODS_SELECTED ? + NativeTheme::kHovered : NativeTheme::kNormal; + } + int height = + draw_item_struct->rcItem.bottom - draw_item_struct->rcItem.top; + int icon_y = kItemTopMargin + + (height - kItemTopMargin - kItemBottomMargin - + config.check_height) / 2; + gfx::CanvasSkia canvas(config.check_width, config.check_height, false); + NativeTheme::ExtraParams extra; + extra.menu_check.is_radio = false; + gfx::Rect bounds(0, 0, config.check_width, config.check_height); + + // Draw the background and the check. + NativeTheme::instance()->Paint( + canvas.sk_canvas(), NativeTheme::kMenuCheckBackground, + state, bounds, extra); + NativeTheme::instance()->Paint( + canvas.sk_canvas(), NativeTheme::kMenuCheck, state, bounds, extra); + + // Draw checkbox to menu. + skia::DrawToNativeContext(canvas.sk_canvas(), dc, + draw_item_struct->rcItem.left + kItemLeftMargin, + draw_item_struct->rcItem.top + (draw_item_struct->rcItem.bottom - + draw_item_struct->rcItem.top - config.check_height) / 2, NULL); + } + + } else { + // Draw the separator + draw_item_struct->rcItem.top += + (draw_item_struct->rcItem.bottom - draw_item_struct->rcItem.top) / 3; + DrawEdge(dc, &draw_item_struct->rcItem, EDGE_ETCHED, BF_TOP); + } + + SetBkColor(dc, prev_bg_color); + SetTextColor(dc, prev_text_color); + } + + bool ProcessWindowMessage(HWND window, + UINT message, + WPARAM w_param, + LPARAM l_param, + LRESULT* l_result) { + switch (message) { + case WM_MENUCOMMAND: + OnMenuCommand(w_param, reinterpret_cast<HMENU>(l_param)); + *l_result = 0; + return true; + case WM_MENUSELECT: + OnMenuSelect(LOWORD(w_param), reinterpret_cast<HMENU>(l_param)); + *l_result = 0; + return true; + case WM_MEASUREITEM: + OnMeasureItem(w_param, reinterpret_cast<MEASUREITEMSTRUCT*>(l_param)); + *l_result = 0; + return true; + case WM_DRAWITEM: + OnDrawItem(w_param, reinterpret_cast<DRAWITEMSTRUCT*>(l_param)); + *l_result = 0; + return true; + // TODO(beng): bring over owner draw from old menu system. + } + return false; + } + + static LRESULT CALLBACK MenuHostWindowProc(HWND window, + UINT message, + WPARAM w_param, + LPARAM l_param) { + MenuHostWindow* host = + reinterpret_cast<MenuHostWindow*>(ui::GetWindowUserData(window)); + // host is null during initial construction. + LRESULT l_result = 0; + if (!host || !host->ProcessWindowMessage(window, message, w_param, l_param, + &l_result)) { + return DefWindowProc(window, message, w_param, l_param); + } + return l_result; + } + + HWND hwnd_; + NativeMenuWin* parent_; + + DISALLOW_COPY_AND_ASSIGN(MenuHostWindow); +}; + +struct NativeMenuWin::HighlightedMenuItemInfo { + HighlightedMenuItemInfo() + : has_parent(false), + has_submenu(false), + menu(NULL), + position(-1) { + } + + bool has_parent; + bool has_submenu; + + // The menu and position. These are only set for non-disabled menu items. + NativeMenuWin* menu; + int position; +}; + +// static +const wchar_t* NativeMenuWin::MenuHostWindow::kWindowClassName = + L"ViewsMenuHostWindow"; + +//////////////////////////////////////////////////////////////////////////////// +// NativeMenuWin, public: + +NativeMenuWin::NativeMenuWin(ui::MenuModel* model, HWND system_menu_for) + : model_(model), + menu_(NULL), + owner_draw_(l10n_util::NeedOverrideDefaultUIFont(NULL, NULL) && + !system_menu_for), + system_menu_for_(system_menu_for), + first_item_index_(0), + menu_action_(MENU_ACTION_NONE), + menu_to_select_(NULL), + position_to_select_(-1), + ALLOW_THIS_IN_INITIALIZER_LIST(menu_to_select_factory_(this)), + parent_(NULL), + destroyed_flag_(NULL) { +} + +NativeMenuWin::~NativeMenuWin() { + if (destroyed_flag_) + *destroyed_flag_ = true; + STLDeleteContainerPointers(items_.begin(), items_.end()); + DestroyMenu(menu_); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeMenuWin, MenuWrapper implementation: + +void NativeMenuWin::RunMenuAt(const gfx::Point& point, int alignment) { + CreateHostWindow(); + UpdateStates(); + UINT flags = TPM_LEFTBUTTON | TPM_RIGHTBUTTON | TPM_RECURSE; + flags |= GetAlignmentFlags(alignment); + menu_action_ = MENU_ACTION_NONE; + + // Set a hook function so we can listen for keyboard events while the + // menu is open, and store a pointer to this object in a static + // variable so the hook has access to it (ugly, but it's the + // only way). + open_native_menu_win_ = this; + HHOOK hhook = SetWindowsHookEx(WH_MSGFILTER, MenuMessageHook, + GetModuleHandle(NULL), ::GetCurrentThreadId()); + + // Mark that any registered listeners have not been called for this particular + // opening of the menu. + listeners_called_ = false; + + // Command dispatch is done through WM_MENUCOMMAND, handled by the host + // window. + HWND hwnd = host_window_->hwnd(); + menu_to_select_ = NULL; + position_to_select_ = -1; + menu_to_select_factory_.InvalidateWeakPtrs(); + bool destroyed = false; + destroyed_flag_ = &destroyed; + model_->MenuWillShow(); + TrackPopupMenu(menu_, flags, point.x(), point.y(), 0, host_window_->hwnd(), + NULL); + UnhookWindowsHookEx(hhook); + open_native_menu_win_ = NULL; + if (destroyed) + return; + destroyed_flag_ = NULL; + if (menu_to_select_) { + // Folks aren't too happy if we notify immediately. In particular, notifying + // the delegate can cause destruction leaving the stack in a weird + // state. Instead post a task, then notify. This mirrors what WM_MENUCOMMAND + // does. + menu_to_select_factory_.InvalidateWeakPtrs(); + MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&NativeMenuWin::DelayedSelect, + menu_to_select_factory_.GetWeakPtr())); + menu_action_ = MENU_ACTION_SELECTED; + } + // Send MenuClosed after we schedule the select, otherwise MenuClosed is + // processed after the select (MenuClosed posts a delayed task too). + model_->MenuClosed(); +} + +void NativeMenuWin::CancelMenu() { + EndMenu(); +} + +void NativeMenuWin::Rebuild() { + ResetNativeMenu(); + items_.clear(); + + owner_draw_ = model_->HasIcons() || owner_draw_; + first_item_index_ = model_->GetFirstItemIndex(GetNativeMenu()); + for (int menu_index = first_item_index_; + menu_index < first_item_index_ + model_->GetItemCount(); ++menu_index) { + int model_index = menu_index - first_item_index_; + if (model_->GetTypeAt(model_index) == ui::MenuModel::TYPE_SEPARATOR) + AddSeparatorItemAt(menu_index, model_index); + else + AddMenuItemAt(menu_index, model_index); + } +} + +void NativeMenuWin::UpdateStates() { + // A depth-first walk of the menu items, updating states. + int model_index = 0; + std::vector<ItemData*>::const_iterator it; + for (it = items_.begin(); it != items_.end(); ++it, ++model_index) { + int menu_index = model_index + first_item_index_; + SetMenuItemState(menu_index, model_->IsEnabledAt(model_index), + model_->IsItemCheckedAt(model_index), false); + if (model_->IsItemDynamicAt(model_index)) { + // TODO(atwilson): Update the icon as well (http://crbug.com/66508). + SetMenuItemLabel(menu_index, model_index, + model_->GetLabelAt(model_index)); + } + Menu2* submenu = (*it)->submenu.get(); + if (submenu) + submenu->UpdateStates(); + } +} + +gfx::NativeMenu NativeMenuWin::GetNativeMenu() const { + return menu_; +} + +NativeMenuWin::MenuAction NativeMenuWin::GetMenuAction() const { + return menu_action_; +} + +void NativeMenuWin::AddMenuListener(MenuListener* listener) { + listeners_.AddObserver(listener); +} + +void NativeMenuWin::RemoveMenuListener(MenuListener* listener) { + listeners_.RemoveObserver(listener); +} + +void NativeMenuWin::SetMinimumWidth(int width) { + NOTIMPLEMENTED(); +} + +//////////////////////////////////////////////////////////////////////////////// +// NativeMenuWin, private: + +// static +NativeMenuWin* NativeMenuWin::open_native_menu_win_ = NULL; + +void NativeMenuWin::DelayedSelect() { + if (menu_to_select_) + menu_to_select_->model_->ActivatedAt(position_to_select_); +} + +// static +bool NativeMenuWin::GetHighlightedMenuItemInfo( + HMENU menu, + HighlightedMenuItemInfo* info) { + for (int i = 0; i < ::GetMenuItemCount(menu); i++) { + UINT state = ::GetMenuState(menu, i, MF_BYPOSITION); + if (state & MF_HILITE) { + if (state & MF_POPUP) { + HMENU submenu = GetSubMenu(menu, i); + if (GetHighlightedMenuItemInfo(submenu, info)) + info->has_parent = true; + else + info->has_submenu = true; + } else if (!(state & MF_SEPARATOR) && !(state & MF_DISABLED)) { + info->menu = GetNativeMenuWinFromHMENU(menu); + info->position = i; + } + return true; + } + } + return false; +} + +// static +LRESULT CALLBACK NativeMenuWin::MenuMessageHook( + int n_code, WPARAM w_param, LPARAM l_param) { + LRESULT result = CallNextHookEx(NULL, n_code, w_param, l_param); + + NativeMenuWin* this_ptr = open_native_menu_win_; + if (!this_ptr) + return result; + + // The first time this hook is called, that means the menu has successfully + // opened, so call the callback function on all of our listeners. + if (!this_ptr->listeners_called_) { + FOR_EACH_OBSERVER(MenuListener, this_ptr->listeners_, OnMenuOpened()); + this_ptr->listeners_called_ = true; + } + + MSG* msg = reinterpret_cast<MSG*>(l_param); + if (msg->message == WM_LBUTTONUP || msg->message == WM_RBUTTONUP) { + HighlightedMenuItemInfo info; + if (GetHighlightedMenuItemInfo(this_ptr->menu_, &info) && info.menu) { + // It appears that when running a menu by way of TrackPopupMenu(Ex) win32 + // gets confused if the underlying window paints itself. As its very easy + // for the underlying window to repaint itself (especially since some menu + // items trigger painting of the tabstrip on mouse over) we have this + // workaround. When the mouse is released on a menu item we remember the + // menu item and end the menu. When the nested message loop returns we + // schedule a task to notify the model. It's still possible to get a + // WM_MENUCOMMAND, so we have to be careful that we don't notify the model + // twice. + this_ptr->menu_to_select_ = info.menu; + this_ptr->position_to_select_ = info.position; + EndMenu(); + } + } else if (msg->message == WM_KEYDOWN) { + HighlightedMenuItemInfo info; + if (GetHighlightedMenuItemInfo(this_ptr->menu_, &info)) { + if (msg->wParam == VK_LEFT && !info.has_parent) { + this_ptr->menu_action_ = MENU_ACTION_PREVIOUS; + ::EndMenu(); + } else if (msg->wParam == VK_RIGHT && !info.has_parent && + !info.has_submenu) { + this_ptr->menu_action_ = MENU_ACTION_NEXT; + ::EndMenu(); + } + } + } + + return result; +} + +bool NativeMenuWin::IsSeparatorItemAt(int menu_index) const { + MENUITEMINFO mii = {0}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_FTYPE; + GetMenuItemInfo(menu_, menu_index, MF_BYPOSITION, &mii); + return !!(mii.fType & MF_SEPARATOR); +} + +void NativeMenuWin::AddMenuItemAt(int menu_index, int model_index) { + MENUITEMINFO mii = {0}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_FTYPE | MIIM_ID | MIIM_DATA; + if (!owner_draw_) + mii.fType = MFT_STRING; + else + mii.fType = MFT_OWNERDRAW; + + ItemData* item_data = new ItemData; + item_data->label = string16(); + ui::MenuModel::ItemType type = model_->GetTypeAt(model_index); + if (type == ui::MenuModel::TYPE_SUBMENU) { + item_data->submenu.reset(new Menu2(model_->GetSubmenuModelAt(model_index))); + mii.fMask |= MIIM_SUBMENU; + mii.hSubMenu = item_data->submenu->GetNativeMenu(); + GetNativeMenuWinFromHMENU(mii.hSubMenu)->parent_ = this; + } else { + if (type == ui::MenuModel::TYPE_RADIO) + mii.fType |= MFT_RADIOCHECK; + mii.wID = model_->GetCommandIdAt(model_index); + } + item_data->native_menu_win = this; + item_data->model_index = model_index; + items_.insert(items_.begin() + model_index, item_data); + mii.dwItemData = reinterpret_cast<ULONG_PTR>(item_data); + UpdateMenuItemInfoForString(&mii, model_index, + model_->GetLabelAt(model_index)); + InsertMenuItem(menu_, menu_index, TRUE, &mii); +} + +void NativeMenuWin::AddSeparatorItemAt(int menu_index, int model_index) { + MENUITEMINFO mii = {0}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_FTYPE; + mii.fType = MFT_SEPARATOR; + // Insert a dummy entry into our label list so we can index directly into it + // using item indices if need be. + items_.insert(items_.begin() + model_index, new ItemData); + InsertMenuItem(menu_, menu_index, TRUE, &mii); +} + +void NativeMenuWin::SetMenuItemState(int menu_index, bool enabled, bool checked, + bool is_default) { + if (IsSeparatorItemAt(menu_index)) + return; + + UINT state = enabled ? MFS_ENABLED : MFS_DISABLED; + if (checked) + state |= MFS_CHECKED; + if (is_default) + state |= MFS_DEFAULT; + + MENUITEMINFO mii = {0}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_STATE; + mii.fState = state; + SetMenuItemInfo(menu_, menu_index, MF_BYPOSITION, &mii); +} + +void NativeMenuWin::SetMenuItemLabel(int menu_index, + int model_index, + const string16& label) { + if (IsSeparatorItemAt(menu_index)) + return; + + MENUITEMINFO mii = {0}; + mii.cbSize = sizeof(mii); + UpdateMenuItemInfoForString(&mii, model_index, label); + SetMenuItemInfo(menu_, menu_index, MF_BYPOSITION, &mii); +} + +void NativeMenuWin::UpdateMenuItemInfoForString(MENUITEMINFO* mii, + int model_index, + const string16& label) { + string16 formatted = label; + ui::MenuModel::ItemType type = model_->GetTypeAt(model_index); + // Strip out any tabs, otherwise they get interpreted as accelerators and can + // lead to weird behavior. + ReplaceSubstringsAfterOffset(&formatted, 0, L"\t", L" "); + if (type != ui::MenuModel::TYPE_SUBMENU) { + // Add accelerator details to the label if provided. + ui::Accelerator accelerator(ui::VKEY_UNKNOWN, false, false, false); + if (model_->GetAcceleratorAt(model_index, &accelerator)) { + formatted += L"\t"; + formatted += accelerator.GetShortcutText(); + } + } + + // Update the owned string, since Windows will want us to keep this new + // version around. + items_[model_index]->label = formatted; + + // Give Windows a pointer to the label string. + mii->fMask |= MIIM_STRING; + mii->dwTypeData = + const_cast<wchar_t*>(items_[model_index]->label.c_str()); +} + +UINT NativeMenuWin::GetAlignmentFlags(int alignment) const { + UINT alignment_flags = TPM_TOPALIGN; + if (alignment == Menu2::ALIGN_TOPLEFT) + alignment_flags |= TPM_LEFTALIGN; + else if (alignment == Menu2::ALIGN_TOPRIGHT) + alignment_flags |= TPM_RIGHTALIGN; + return alignment_flags; +} + +void NativeMenuWin::ResetNativeMenu() { + if (IsWindow(system_menu_for_)) { + if (menu_) + GetSystemMenu(system_menu_for_, TRUE); + menu_ = GetSystemMenu(system_menu_for_, FALSE); + } else { + if (menu_) + DestroyMenu(menu_); + menu_ = CreatePopupMenu(); + // Rather than relying on the return value of TrackPopupMenuEx, which is + // always a command identifier, instead we tell the menu to notify us via + // our host window and the WM_MENUCOMMAND message. + MENUINFO mi = {0}; + mi.cbSize = sizeof(mi); + mi.fMask = MIM_STYLE | MIM_MENUDATA; + mi.dwStyle = MNS_NOTIFYBYPOS; + mi.dwMenuData = reinterpret_cast<ULONG_PTR>(this); + SetMenuInfo(menu_, &mi); + } +} + +void NativeMenuWin::CreateHostWindow() { + // This only gets called from RunMenuAt, and as such there is only ever one + // host window per menu hierarchy, no matter how many NativeMenuWin objects + // exist wrapping submenus. + if (!host_window_.get()) + host_window_.reset(new MenuHostWindow(this)); +} + +//////////////////////////////////////////////////////////////////////////////// +// SystemMenuModel: + +SystemMenuModel::SystemMenuModel(ui::SimpleMenuModel::Delegate* delegate) + : SimpleMenuModel(delegate) { +} + +SystemMenuModel::~SystemMenuModel() { +} + +int SystemMenuModel::GetFirstItemIndex(gfx::NativeMenu native_menu) const { + // We allow insertions before last item (Close). + return std::max(0, GetMenuItemCount(native_menu) - 1); +} + +//////////////////////////////////////////////////////////////////////////////// +// MenuWrapper, public: + +// static +MenuWrapper* MenuWrapper::CreateWrapper(Menu2* menu) { + return new NativeMenuWin(menu->model(), NULL); +} + +} // namespace views diff --git a/ui/views/controls/menu/native_menu_win.h b/ui/views/controls/menu/native_menu_win.h new file mode 100644 index 0000000..91c7db5 --- /dev/null +++ b/ui/views/controls/menu/native_menu_win.h @@ -0,0 +1,184 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_WIN_H_ +#define UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_WIN_H_ +#pragma once + +#include <vector> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "ui/base/models/simple_menu_model.h" +#include "ui/views/controls/menu/menu_wrapper.h" +#include "views/views_export.h" + +namespace views { + +// A Windows implementation of MenuWrapper. +// TODO(beng): rename to MenuWin once the old class is dead. +class VIEWS_EXPORT NativeMenuWin : public MenuWrapper { + public: + // Construct a NativeMenuWin, with a model and delegate. If |system_menu_for| + // is non-NULL, the NativeMenuWin wraps the system menu for that window. + // The caller owns the model and the delegate. + NativeMenuWin(ui::MenuModel* model, HWND system_menu_for); + virtual ~NativeMenuWin(); + + // Overridden from MenuWrapper: + virtual void RunMenuAt(const gfx::Point& point, int alignment) OVERRIDE; + virtual void CancelMenu() OVERRIDE; + virtual void Rebuild() OVERRIDE; + virtual void UpdateStates() OVERRIDE; + virtual gfx::NativeMenu GetNativeMenu() const OVERRIDE; + virtual MenuAction GetMenuAction() const OVERRIDE; + virtual void AddMenuListener(MenuListener* listener) OVERRIDE; + virtual void RemoveMenuListener(MenuListener* listener) OVERRIDE; + virtual void SetMinimumWidth(int width) OVERRIDE; + + private: + // IMPORTANT: Note about indices. + // Functions in this class deal in two index spaces: + // 1. menu_index - the index of an item within the actual Windows + // native menu. + // 2. model_index - the index of the item within our model. + // These two are most often but not always the same value! The + // notable exception is when this object is used to wrap the + // Windows System Menu. In this instance, the model indices start + // at 0, but the insertion index into the existing menu is not. + // It is important to take this into consideration when editing the + // code in the functions in this class. + + struct HighlightedMenuItemInfo; + + // Returns true if the item at the specified index is a separator. + bool IsSeparatorItemAt(int menu_index) const; + + // Add items. See note above about indices. + void AddMenuItemAt(int menu_index, int model_index); + void AddSeparatorItemAt(int menu_index, int model_index); + + // Sets the state of the item at the specified index. + void SetMenuItemState(int menu_index, + bool enabled, + bool checked, + bool is_default); + + // Sets the label of the item at the specified index. + void SetMenuItemLabel(int menu_index, + int model_index, + const string16& label); + + // Updates the local data structure with the correctly formatted version of + // |label| at the specified model_index, and adds string data to |mii| if + // the menu is not owner-draw. That's a mouthful. This function exists because + // of the peculiarities of the Windows menu API. + void UpdateMenuItemInfoForString(MENUITEMINFO* mii, + int model_index, + const string16& label); + + // Returns the alignment flags to be passed to TrackPopupMenuEx, based on the + // supplied alignment and the UI text direction. + UINT GetAlignmentFlags(int alignment) const; + + // Resets the native menu stored in |menu_| by destroying any old menu then + // creating a new empty one. + void ResetNativeMenu(); + + // Creates the host window that receives notifications from the menu. + void CreateHostWindow(); + + // Callback from task to notify menu it was selected. + void DelayedSelect(); + + // Given a menu that's currently popped-up, find the currently highlighted + // item. Returns true if a highlighted item was found. + static bool GetHighlightedMenuItemInfo(HMENU menu, + HighlightedMenuItemInfo* info); + + // Hook to receive keyboard events while the menu is open. + static LRESULT CALLBACK MenuMessageHook( + int n_code, WPARAM w_param, LPARAM l_param); + + // Our attached model and delegate. + ui::MenuModel* model_; + + HMENU menu_; + + // True if the contents of menu items in this menu are drawn by the menu host + // window, rather than Windows. + bool owner_draw_; + + // An object that collects all of the data associated with an individual menu + // item. + struct ItemData; + std::vector<ItemData*> items_; + + // The window that receives notifications from the menu. + class MenuHostWindow; + friend MenuHostWindow; + scoped_ptr<MenuHostWindow> host_window_; + + // The HWND this menu is the system menu for, or NULL if the menu is not a + // system menu. + HWND system_menu_for_; + + // The index of the first item in the model in the menu. + int first_item_index_; + + // The action that took place during the call to RunMenuAt. + MenuAction menu_action_; + + // A list of listeners to call when the menu opens. + ObserverList<MenuListener> listeners_; + + // Keep track of whether the listeners have already been called at least + // once. + bool listeners_called_; + + // See comment in MenuMessageHook for details on these. + NativeMenuWin* menu_to_select_; + int position_to_select_; + base::WeakPtrFactory<NativeMenuWin> menu_to_select_factory_; + + // If we're a submenu, this is our parent. + NativeMenuWin* parent_; + + // If non-null the destructor sets this to true. This is set to non-null while + // the menu is showing. It is used to detect if the menu was deleted while + // running. + bool* destroyed_flag_; + + // Ugly: a static pointer to the instance of this class that currently + // has a menu open, because our hook function that receives keyboard + // events doesn't have a mechanism to get a user data pointer. + static NativeMenuWin* open_native_menu_win_; + + DISALLOW_COPY_AND_ASSIGN(NativeMenuWin); +}; + +// A SimpleMenuModel subclass that allows the system menu for a window to be +// wrapped. +class VIEWS_EXPORT SystemMenuModel : public ui::SimpleMenuModel { + public: + explicit SystemMenuModel(Delegate* delegate); + virtual ~SystemMenuModel(); + + // Overridden from ui::MenuModel: + virtual int GetFirstItemIndex(gfx::NativeMenu native_menu) const; + + protected: + // Overridden from SimpleMenuModel: + virtual int FlipIndex(int index) const { return GetItemCount() - index - 1; } + + private: + DISALLOW_COPY_AND_ASSIGN(SystemMenuModel); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_NATIVE_MENU_WIN_H_ diff --git a/ui/views/controls/menu/nested_dispatcher_gtk.cc b/ui/views/controls/menu/nested_dispatcher_gtk.cc new file mode 100644 index 0000000..d83b9cc --- /dev/null +++ b/ui/views/controls/menu/nested_dispatcher_gtk.cc @@ -0,0 +1,35 @@ +// 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 "ui/views/controls/menu/nested_dispatcher_gtk.h" + +namespace views { + +NestedDispatcherGtk::NestedDispatcherGtk(MessageLoopForUI::Dispatcher* creator, + bool allow_nested_task) + : creator_(creator), + allow_nested_task_(allow_nested_task) { +} + +bool NestedDispatcherGtk::RunAndSelfDestruct() { + bool nestable = MessageLoopForUI::current()->NestableTasksAllowed(); + if (allow_nested_task_) + MessageLoopForUI::current()->SetNestableTasksAllowed(true); + MessageLoopForUI::current()->RunWithDispatcher(this); + if (allow_nested_task_) + MessageLoopForUI::current()->SetNestableTasksAllowed(nestable); + bool creator_is_deleted = creator_ == NULL; + delete this; + return creator_is_deleted; +} + +void NestedDispatcherGtk::CreatorDestroyed() { + creator_ = NULL; +} + +bool NestedDispatcherGtk::Dispatch(GdkEvent* event) { + return creator_ && creator_->Dispatch(event); +} + +} // namespace views diff --git a/ui/views/controls/menu/nested_dispatcher_gtk.h b/ui/views/controls/menu/nested_dispatcher_gtk.h new file mode 100644 index 0000000..dc02f36 --- /dev/null +++ b/ui/views/controls/menu/nested_dispatcher_gtk.h @@ -0,0 +1,51 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_NESTED_DISPATCHER_GTK_H_ +#define UI_VIEWS_CONTROLS_MENU_NESTED_DISPATCHER_GTK_H_ +#pragma once + +#include "base/compiler_specific.h" +#include "base/message_loop.h" + +namespace views { + +// A nested dispatcher that can out-live the creator of this +// dispatcher. This is to deal with the scenario where a menu object +// is deleted while it's handling the message loop. Note that +// |RunAndSelfDestruct| method always delete itself regardless of +// whether or not the menu object is deleted, so a menu object should +// create a new instance for each open request. +// http://crosbug.com/7228, http://crosbug.com/7929 +class NestedDispatcherGtk : public MessageLoopForUI::Dispatcher { + public: + NestedDispatcherGtk(MessageLoopForUI::Dispatcher* creator, + bool allow_nested_task); + + // Run the messsage loop and returns if the menu has been + // deleted in the nested loop. Returns true if the menu is + // deleted, or false otherwise. + bool RunAndSelfDestruct(); + + // Tells the nested dispatcher that creator has been destroyed. + void CreatorDestroyed(); + + private: + virtual ~NestedDispatcherGtk() {} + + // Overriden from MessageLoopForUI::Dispatcher: + virtual bool Dispatch(GdkEvent* event) OVERRIDE; + + // Creator of the nested loop. + MessageLoopForUI::Dispatcher* creator_; + + // True to allow nested task in the message loop. + bool allow_nested_task_; + + DISALLOW_COPY_AND_ASSIGN(NestedDispatcherGtk); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_NESTED_DISPATCHER_GTK_H_ diff --git a/ui/views/controls/menu/submenu_view.cc b/ui/views/controls/menu/submenu_view.cc new file mode 100644 index 0000000..a462689 --- /dev/null +++ b/ui/views/controls/menu/submenu_view.cc @@ -0,0 +1,398 @@ +// 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 "ui/views/controls/menu/submenu_view.h" + +#include "ui/base/accessibility/accessible_view_state.h" +#include "ui/gfx/canvas.h" +#include "ui/views/controls/menu/menu_config.h" +#include "ui/views/controls/menu/menu_controller.h" +#include "ui/views/controls/menu/menu_host.h" +#include "ui/views/controls/menu/menu_scroll_view_container.h" +#include "ui/views/widget/root_view.h" +#include "ui/views/widget/widget.h" + +namespace { + +// Height of the drop indicator. This should be an even number. +const int kDropIndicatorHeight = 2; + +// Color of the drop indicator. +const SkColor kDropIndicatorColor = SK_ColorBLACK; + +} // namespace + +namespace views { + +// static +const int SubmenuView::kSubmenuBorderSize = 3; + +// static +const char SubmenuView::kViewClassName[] = "views/SubmenuView"; + +SubmenuView::SubmenuView(MenuItemView* parent) + : parent_menu_item_(parent), + host_(NULL), + drop_item_(NULL), + drop_position_(MenuDelegate::DROP_NONE), + scroll_view_container_(NULL), + max_accelerator_width_(0), + minimum_preferred_width_(0), + resize_open_menu_(false) { + DCHECK(parent); + // We'll delete ourselves, otherwise the ScrollView would delete us on close. + set_parent_owned(false); +} + +SubmenuView::~SubmenuView() { + // The menu may not have been closed yet (it will be hidden, but not + // necessarily closed). + Close(); + + delete scroll_view_container_; +} + +int SubmenuView::GetMenuItemCount() { + int count = 0; + for (int i = 0; i < child_count(); ++i) { + if (child_at(i)->id() == MenuItemView::kMenuItemViewID) + count++; + } + return count; +} + +MenuItemView* SubmenuView::GetMenuItemAt(int index) { + for (int i = 0, count = 0; i < child_count(); ++i) { + if (child_at(i)->id() == MenuItemView::kMenuItemViewID && + count++ == index) { + return static_cast<MenuItemView*>(child_at(i)); + } + } + NOTREACHED(); + return NULL; +} + +void SubmenuView::ChildPreferredSizeChanged(View* child) { + if (!resize_open_menu_) + return; + + MenuItemView *item = GetMenuItem(); + MenuController* controller = item->GetMenuController(); + + if (controller) { + bool dir; + gfx::Rect bounds = controller->CalculateMenuBounds(item, false, &dir); + Reposition(bounds); + } +} + +void SubmenuView::Layout() { + // We're in a ScrollView, and need to set our width/height ourselves. + if (!parent()) + return; + + // Use our current y, unless it means part of the menu isn't visible anymore. + int pref_height = GetPreferredSize().height(); + int new_y; + if (pref_height > parent()->height()) + new_y = std::max(parent()->height() - pref_height, y()); + else + new_y = 0; + SetBounds(x(), new_y, parent()->width(), pref_height); + + gfx::Insets insets = GetInsets(); + int x = insets.left(); + int y = insets.top(); + int menu_item_width = width() - insets.width(); + for (int i = 0; i < child_count(); ++i) { + View* child = child_at(i); + if (child->IsVisible()) { + gfx::Size child_pref_size = child->GetPreferredSize(); + child->SetBounds(x, y, menu_item_width, child_pref_size.height()); + y += child_pref_size.height(); + } + } +} + +gfx::Size SubmenuView::GetPreferredSize() { + if (!has_children()) + return gfx::Size(); + + max_accelerator_width_ = 0; + int max_width = 0; + int height = 0; + for (int i = 0; i < child_count(); ++i) { + View* child = child_at(i); + gfx::Size child_pref_size = child->IsVisible() ? + child->GetPreferredSize() : gfx::Size(); + max_width = std::max(max_width, child_pref_size.width()); + height += child_pref_size.height(); + if (child->id() == MenuItemView::kMenuItemViewID) { + MenuItemView* menu = static_cast<MenuItemView*>(child); + max_accelerator_width_ = + std::max(max_accelerator_width_, menu->GetAcceleratorTextWidth()); + } + } + if (max_accelerator_width_ > 0) { + max_accelerator_width_ += + MenuConfig::instance().label_to_accelerator_padding; + } + gfx::Insets insets = GetInsets(); + return gfx::Size( + std::max(max_width + max_accelerator_width_ + insets.width(), + minimum_preferred_width_ - 2 * kSubmenuBorderSize), + height + insets.height()); +} + +void SubmenuView::GetAccessibleState(ui::AccessibleViewState* state) { + // Inherit most of the state from the parent menu item, except the role. + if (GetMenuItem()) + GetMenuItem()->GetAccessibleState(state); + state->role = ui::AccessibilityTypes::ROLE_MENUPOPUP; +} + +void SubmenuView::PaintChildren(gfx::Canvas* canvas) { + View::PaintChildren(canvas); + + if (drop_item_ && drop_position_ != MenuDelegate::DROP_ON) + PaintDropIndicator(canvas, drop_item_, drop_position_); +} + +bool SubmenuView::GetDropFormats( + int* formats, + std::set<OSExchangeData::CustomFormat>* custom_formats) { + DCHECK(GetMenuItem()->GetMenuController()); + return GetMenuItem()->GetMenuController()->GetDropFormats(this, formats, + custom_formats); +} + +bool SubmenuView::AreDropTypesRequired() { + DCHECK(GetMenuItem()->GetMenuController()); + return GetMenuItem()->GetMenuController()->AreDropTypesRequired(this); +} + +bool SubmenuView::CanDrop(const OSExchangeData& data) { + DCHECK(GetMenuItem()->GetMenuController()); + return GetMenuItem()->GetMenuController()->CanDrop(this, data); +} + +void SubmenuView::OnDragEntered(const DropTargetEvent& event) { + DCHECK(GetMenuItem()->GetMenuController()); + GetMenuItem()->GetMenuController()->OnDragEntered(this, event); +} + +int SubmenuView::OnDragUpdated(const DropTargetEvent& event) { + DCHECK(GetMenuItem()->GetMenuController()); + return GetMenuItem()->GetMenuController()->OnDragUpdated(this, event); +} + +void SubmenuView::OnDragExited() { + DCHECK(GetMenuItem()->GetMenuController()); + GetMenuItem()->GetMenuController()->OnDragExited(this); +} + +int SubmenuView::OnPerformDrop(const DropTargetEvent& event) { + DCHECK(GetMenuItem()->GetMenuController()); + return GetMenuItem()->GetMenuController()->OnPerformDrop(this, event); +} + +bool SubmenuView::OnMouseWheel(const MouseWheelEvent& e) { + gfx::Rect vis_bounds = GetVisibleBounds(); + int menu_item_count = GetMenuItemCount(); + if (vis_bounds.height() == height() || !menu_item_count) { + // All menu items are visible, nothing to scroll. + return true; + } + + // Find the index of the first menu item whose y-coordinate is >= visible + // y-coordinate. + int i = 0; + while ((i < menu_item_count) && (GetMenuItemAt(i)->y() < vis_bounds.y())) + ++i; + if (i == menu_item_count) + return true; + int first_vis_index = std::max(0, + (GetMenuItemAt(i)->y() == vis_bounds.y()) ? i : i - 1); + + // If the first item isn't entirely visible, make it visible, otherwise make + // the next/previous one entirely visible. + int delta = abs(e.offset() / MouseWheelEvent::kWheelDelta); + for (bool scroll_up = (e.offset() > 0); delta != 0; --delta) { + int scroll_target; + if (scroll_up) { + if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) { + if (first_vis_index == 0) + break; + first_vis_index--; + } + scroll_target = GetMenuItemAt(first_vis_index)->y(); + } else { + if (first_vis_index + 1 == menu_item_count) + break; + scroll_target = GetMenuItemAt(first_vis_index + 1)->y(); + if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) + first_vis_index++; + } + ScrollRectToVisible(gfx::Rect(gfx::Point(0, scroll_target), + vis_bounds.size())); + vis_bounds = GetVisibleBounds(); + } + + return true; +} + +bool SubmenuView::IsShowing() { + return host_ && host_->IsMenuHostVisible(); +} + +void SubmenuView::ShowAt(Widget* parent, + const gfx::Rect& bounds, + bool do_capture) { + if (host_) { + host_->ShowMenuHost(do_capture); + } else { + host_ = new MenuHost(this); + // Force construction of the scroll view container. + GetScrollViewContainer(); + // Make sure the first row is visible. + ScrollRectToVisible(gfx::Rect(gfx::Point(), gfx::Size(1, 1))); + host_->InitMenuHost(parent, bounds, scroll_view_container_, do_capture); + } + + GetScrollViewContainer()->GetWidget()->NotifyAccessibilityEvent( + GetScrollViewContainer(), + ui::AccessibilityTypes::EVENT_MENUSTART, + true); + GetWidget()->NotifyAccessibilityEvent( + this, + ui::AccessibilityTypes::EVENT_MENUPOPUPSTART, + true); +} + +void SubmenuView::Reposition(const gfx::Rect& bounds) { + if (host_) + host_->SetMenuHostBounds(bounds); +} + +void SubmenuView::Close() { + if (host_) { + GetWidget()->NotifyAccessibilityEvent( + this, + ui::AccessibilityTypes::EVENT_MENUPOPUPEND, + true); + GetScrollViewContainer()->GetWidget()->NotifyAccessibilityEvent( + GetScrollViewContainer(), + ui::AccessibilityTypes::EVENT_MENUEND, + true); + + host_->DestroyMenuHost(); + host_ = NULL; + } +} + +void SubmenuView::Hide() { + if (host_) + host_->HideMenuHost(); +} + +void SubmenuView::ReleaseCapture() { + if (host_) + host_->ReleaseMenuHostCapture(); +} + +bool SubmenuView::SkipDefaultKeyEventProcessing(const views::KeyEvent& e) { + return views::FocusManager::IsTabTraversalKeyEvent(e); +} + +MenuItemView* SubmenuView::GetMenuItem() const { + return parent_menu_item_; +} + +void SubmenuView::SetDropMenuItem(MenuItemView* item, + MenuDelegate::DropPosition position) { + if (drop_item_ == item && drop_position_ == position) + return; + SchedulePaintForDropIndicator(drop_item_, drop_position_); + drop_item_ = item; + drop_position_ = position; + SchedulePaintForDropIndicator(drop_item_, drop_position_); +} + +bool SubmenuView::GetShowSelection(MenuItemView* item) { + if (drop_item_ == NULL) + return true; + // Something is being dropped on one of this menus items. Show the + // selection if the drop is on the passed in item and the drop position is + // ON. + return (drop_item_ == item && drop_position_ == MenuDelegate::DROP_ON); +} + +MenuScrollViewContainer* SubmenuView::GetScrollViewContainer() { + if (!scroll_view_container_) { + scroll_view_container_ = new MenuScrollViewContainer(this); + // Otherwise MenuHost would delete us. + scroll_view_container_->set_parent_owned(false); + } + return scroll_view_container_; +} + +void SubmenuView::MenuHostDestroyed() { + host_ = NULL; + GetMenuItem()->GetMenuController()->Cancel(MenuController::EXIT_DESTROYED); +} + +std::string SubmenuView::GetClassName() const { + return kViewClassName; +} + +void SubmenuView::OnBoundsChanged(const gfx::Rect& previous_bounds) { + SchedulePaint(); +} + +void SubmenuView::PaintDropIndicator(gfx::Canvas* canvas, + MenuItemView* item, + MenuDelegate::DropPosition position) { + if (position == MenuDelegate::DROP_NONE) + return; + + gfx::Rect bounds = CalculateDropIndicatorBounds(item, position); + canvas->FillRect(kDropIndicatorColor, bounds); +} + +void SubmenuView::SchedulePaintForDropIndicator( + MenuItemView* item, + MenuDelegate::DropPosition position) { + if (item == NULL) + return; + + if (position == MenuDelegate::DROP_ON) { + item->SchedulePaint(); + } else if (position != MenuDelegate::DROP_NONE) { + SchedulePaintInRect(CalculateDropIndicatorBounds(item, position)); + } +} + +gfx::Rect SubmenuView::CalculateDropIndicatorBounds( + MenuItemView* item, + MenuDelegate::DropPosition position) { + DCHECK(position != MenuDelegate::DROP_NONE); + gfx::Rect item_bounds = item->bounds(); + switch (position) { + case MenuDelegate::DROP_BEFORE: + item_bounds.Offset(0, -kDropIndicatorHeight / 2); + item_bounds.set_height(kDropIndicatorHeight); + return item_bounds; + + case MenuDelegate::DROP_AFTER: + item_bounds.Offset(0, item_bounds.height() - kDropIndicatorHeight / 2); + item_bounds.set_height(kDropIndicatorHeight); + return item_bounds; + + default: + // Don't render anything for on. + return gfx::Rect(); + } +} + +} // namespace views diff --git a/ui/views/controls/menu/submenu_view.h b/ui/views/controls/menu/submenu_view.h new file mode 100644 index 0000000..693a538 --- /dev/null +++ b/ui/views/controls/menu/submenu_view.h @@ -0,0 +1,204 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_SUBMENU_VIEW_H_ +#define UI_VIEWS_CONTROLS_MENU_SUBMENU_VIEW_H_ +#pragma once + +#include <string> + +#include "base/compiler_specific.h" +#include "ui/views/controls/menu/menu_delegate.h" +#include "views/view.h" + +namespace views { + +class MenuHost; +class MenuItemView; +class MenuScrollViewContainer; + +// SubmenuView is the parent of all menu items. +// +// SubmenuView has the following responsibilities: +// . It positions and sizes all child views (any type of View may be added, +// not just MenuItemViews). +// . Forwards the appropriate events to the MenuController. This allows the +// MenuController to update the selection as the user moves the mouse around. +// . Renders the drop indicator during a drop operation. +// . Shows and hides the window (a NativeWidgetWin) when the menu is shown on +// screen. +// +// SubmenuView is itself contained in a MenuScrollViewContainer. +// MenuScrollViewContainer handles showing as much of the SubmenuView as the +// screen allows. If the SubmenuView is taller than the screen, scroll buttons +// are provided that allow the user to see all the menu items. +class VIEWS_EXPORT SubmenuView : public View { + public: + // The submenu's class name. + static const char kViewClassName[]; + + // Creates a SubmenuView for the specified menu item. + explicit SubmenuView(MenuItemView* parent); + virtual ~SubmenuView(); + + // Returns the number of child views that are MenuItemViews. + // MenuItemViews are identified by ID. + int GetMenuItemCount(); + + // Returns the MenuItemView at the specified index. + MenuItemView* GetMenuItemAt(int index); + + // Positions and sizes the child views. This tiles the views vertically, + // giving each child the available width. + virtual void Layout() OVERRIDE; + virtual gfx::Size GetPreferredSize() OVERRIDE; + + // Override from View. + virtual void GetAccessibleState(ui::AccessibleViewState* state) OVERRIDE; + + // Painting. + virtual void PaintChildren(gfx::Canvas* canvas) OVERRIDE; + + // Drag and drop methods. These are forwarded to the MenuController. + virtual bool GetDropFormats( + int* formats, + std::set<OSExchangeData::CustomFormat>* custom_formats) OVERRIDE; + virtual bool AreDropTypesRequired() OVERRIDE; + virtual bool CanDrop(const OSExchangeData& data) OVERRIDE; + virtual void OnDragEntered(const DropTargetEvent& event) OVERRIDE; + virtual int OnDragUpdated(const DropTargetEvent& event) OVERRIDE; + virtual void OnDragExited() OVERRIDE; + virtual int OnPerformDrop(const DropTargetEvent& event) OVERRIDE; + + // Scrolls on menu item boundaries. + virtual bool OnMouseWheel(const MouseWheelEvent& e) OVERRIDE; + + // Returns true if the menu is showing. + bool IsShowing(); + + // Shows the menu at the specified location. Coordinates are in screen + // coordinates. max_width gives the max width the view should be. + void ShowAt(Widget* parent, + const gfx::Rect& bounds, + bool do_capture); + + // Resets the bounds of the submenu to |bounds|. + void Reposition(const gfx::Rect& bounds); + + // Closes the menu, destroying the host. + void Close(); + + // Hides the hosting window. + // + // The hosting window is hidden first, then deleted (Close) when the menu is + // done running. This is done to avoid deletion ordering dependencies. In + // particular, during drag and drop (and when a modal dialog is shown as + // a result of choosing a context menu) it is possible that an event is + // being processed by the host, so that host is on the stack when we need to + // close the window. If we closed the window immediately (and deleted it), + // when control returned back to host we would crash as host was deleted. + void Hide(); + + // If mouse capture was grabbed, it is released. Does nothing if mouse was + // not captured. + void ReleaseCapture(); + + // Overriden from View to prevent tab from doing anything. + virtual bool SkipDefaultKeyEventProcessing(const KeyEvent& e) OVERRIDE; + + // Returns the parent menu item we're showing children for. + MenuItemView* GetMenuItem() const; + + // Set the drop item and position. + void SetDropMenuItem(MenuItemView* item, + MenuDelegate::DropPosition position); + + // Returns whether the selection should be shown for the specified item. + // The selection is NOT shown during drag and drop when the drop is over + // the menu. + bool GetShowSelection(MenuItemView* item); + + // Returns the container for the SubmenuView. + MenuScrollViewContainer* GetScrollViewContainer(); + + // Invoked if the menu is prematurely destroyed. This can happen if the window + // closes while the menu is shown. If invoked the SubmenuView must drop all + // references to the MenuHost as the MenuHost is about to be deleted. + void MenuHostDestroyed(); + + // Max width of accelerators in child menu items. This doesn't include + // children's children, only direct children. + int max_accelerator_width() const { return max_accelerator_width_; } + + // Minimum width of menu in pixels (default 0). This becomes the smallest + // width returned by GetPreferredSize(). + void set_minimum_preferred_width(int minimum_preferred_width) { + minimum_preferred_width_ = minimum_preferred_width; + } + + // Automatically resize menu if a subview's preferred size changes. + bool resize_open_menu() const { return resize_open_menu_; } + void set_resize_open_menu(bool resize_open_menu) { + resize_open_menu_ = resize_open_menu; + } + + // Padding around the edges of the submenu. + static const int kSubmenuBorderSize; + + protected: + // View override. + virtual std::string GetClassName() const OVERRIDE; + + // View method. Overridden to schedule a paint. We do this so that when + // scrolling occurs, everything is repainted correctly. + virtual void OnBoundsChanged(const gfx::Rect& previous_bounds) OVERRIDE; + + virtual void ChildPreferredSizeChanged(View* child) OVERRIDE; + + private: + // Paints the drop indicator. This is only invoked if item is non-NULL and + // position is not DROP_NONE. + void PaintDropIndicator(gfx::Canvas* canvas, + MenuItemView* item, + MenuDelegate::DropPosition position); + + void SchedulePaintForDropIndicator(MenuItemView* item, + MenuDelegate::DropPosition position); + + // Calculates the location of th edrop indicator. + gfx::Rect CalculateDropIndicatorBounds(MenuItemView* item, + MenuDelegate::DropPosition position); + + // Parent menu item. + MenuItemView* parent_menu_item_; + + // Widget subclass used to show the children. This is deleted when we invoke + // |DestroyMenuHost|, or |MenuHostDestroyed| is invoked back on us. + MenuHost* host_; + + // If non-null, indicates a drop is in progress and drop_item is the item + // the drop is over. + MenuItemView* drop_item_; + + // Position of the drop. + MenuDelegate::DropPosition drop_position_; + + // Ancestor of the SubmenuView, lazily created. + MenuScrollViewContainer* scroll_view_container_; + + // See description above getter. + int max_accelerator_width_; + + // Minimum width returned in GetPreferredSize(). + int minimum_preferred_width_; + + // Reposition open menu when contained views change size. + bool resize_open_menu_; + + DISALLOW_COPY_AND_ASSIGN(SubmenuView); +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_SUBMENU_VIEW_H_ diff --git a/ui/views/controls/menu/view_menu_delegate.h b/ui/views/controls/menu/view_menu_delegate.h new file mode 100644 index 0000000..ef243e0 --- /dev/null +++ b/ui/views/controls/menu/view_menu_delegate.h @@ -0,0 +1,40 @@ +// 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. + +#ifndef UI_VIEWS_CONTROLS_MENU_VIEW_MENU_DELEGATE_H_ +#define UI_VIEWS_CONTROLS_MENU_VIEW_MENU_DELEGATE_H_ +#pragma once + +#include "ui/gfx/native_widget_types.h" + +namespace gfx { +class Point; +} + +namespace views { + +class View; + +//////////////////////////////////////////////////////////////////////////////// +// +// ViewMenuDelegate +// +// An interface that allows a component to tell a View about a menu that it +// has constructed that the view can show (e.g. for MenuButton views, or as a +// context menu.) +// +//////////////////////////////////////////////////////////////////////////////// +class ViewMenuDelegate { + public: + // Create and show a menu at the specified position. Source is the view the + // ViewMenuDelegate was set on. + virtual void RunMenu(View* source, const gfx::Point& pt) = 0; + + protected: + virtual ~ViewMenuDelegate() {} +}; + +} // namespace views + +#endif // UI_VIEWS_CONTROLS_MENU_VIEW_MENU_DELEGATE_H_ diff --git a/ui/views/controls/scrollbar/base_scroll_bar.cc b/ui/views/controls/scrollbar/base_scroll_bar.cc index 01194c1..3afb238 100644 --- a/ui/views/controls/scrollbar/base_scroll_bar.cc +++ b/ui/views/controls/scrollbar/base_scroll_bar.cc @@ -16,10 +16,10 @@ #include "ui/base/keycodes/keyboard_codes.h" #include "ui/base/l10n/l10n_util.h" #include "ui/gfx/canvas.h" +#include "ui/views/controls/menu/menu_item_view.h" +#include "ui/views/controls/menu/menu_runner.h" #include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h" #include "ui/views/widget/widget.h" -#include "views/controls/menu/menu_item_view.h" -#include "views/controls/menu/menu_runner.h" #include "views/controls/scroll_view.h" #if defined(OS_LINUX) diff --git a/ui/views/controls/scrollbar/base_scroll_bar.h b/ui/views/controls/scrollbar/base_scroll_bar.h index 3624c1d..8dee9b9 100644 --- a/ui/views/controls/scrollbar/base_scroll_bar.h +++ b/ui/views/controls/scrollbar/base_scroll_bar.h @@ -7,9 +7,9 @@ #pragma once #include "ui/views/controls/button/image_button.h" +#include "ui/views/controls/menu/menu_delegate.h" #include "ui/views/controls/scrollbar/scroll_bar.h" #include "views/context_menu_controller.h" -#include "views/controls/menu/menu_delegate.h" #include "views/repeat_controller.h" namespace views { diff --git a/ui/views/controls/scrollbar/bitmap_scroll_bar.cc b/ui/views/controls/scrollbar/bitmap_scroll_bar.cc index a3c76e7..6aa44d5 100644 --- a/ui/views/controls/scrollbar/bitmap_scroll_bar.cc +++ b/ui/views/controls/scrollbar/bitmap_scroll_bar.cc @@ -17,9 +17,9 @@ #include "ui/base/keycodes/keyboard_codes.h" #include "ui/base/l10n/l10n_util.h" #include "ui/gfx/canvas.h" +#include "ui/views/controls/menu/menu.h" #include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h" #include "ui/views/widget/widget.h" -#include "views/controls/menu/menu.h" #include "views/controls/scroll_view.h" #if defined(OS_LINUX) diff --git a/ui/views/controls/textfield/native_textfield_views.cc b/ui/views/controls/textfield/native_textfield_views.cc index 8cba8de..3e6a5f8 100644 --- a/ui/views/controls/textfield/native_textfield_views.cc +++ b/ui/views/controls/textfield/native_textfield_views.cc @@ -18,6 +18,9 @@ #include "ui/gfx/canvas.h" #include "ui/gfx/insets.h" #include "ui/gfx/render_text.h" +#include "ui/views/controls/menu/menu_item_view.h" +#include "ui/views/controls/menu/menu_model_adapter.h" +#include "ui/views/controls/menu/menu_runner.h" #include "ui/views/controls/textfield/textfield.h" #include "ui/views/controls/textfield/textfield_controller.h" #include "ui/views/controls/textfield/textfield_views_model.h" @@ -27,9 +30,6 @@ #include "views/background.h" #include "views/border.h" #include "views/controls/focusable_border.h" -#include "views/controls/menu/menu_item_view.h" -#include "views/controls/menu/menu_model_adapter.h" -#include "views/controls/menu/menu_runner.h" #include "views/metrics.h" #include "views/views_delegate.h" diff --git a/ui/views/controls/textfield/native_textfield_win.cc b/ui/views/controls/textfield/native_textfield_win.cc index 0741fd6..bbbdbd4 100644 --- a/ui/views/controls/textfield/native_textfield_win.cc +++ b/ui/views/controls/textfield/native_textfield_win.cc @@ -23,6 +23,8 @@ #include "ui/base/range/range.h" #include "ui/base/win/mouse_wheel_util.h" #include "ui/gfx/native_theme_win.h" +#include "ui/views/controls/menu/menu_2.h" +#include "ui/views/controls/menu/menu_win.h" #include "ui/views/controls/native/native_view_host.h" #include "ui/views/controls/textfield/native_textfield_views.h" #include "ui/views/controls/textfield/textfield.h" @@ -30,8 +32,6 @@ #include "ui/views/focus/focus_manager.h" #include "ui/views/widget/widget.h" #include "views/controls/label.h" -#include "views/controls/menu/menu_2.h" -#include "views/controls/menu/menu_win.h" #include "views/metrics.h" #include "views/views_delegate.h" diff --git a/ui/views/examples/menu_example.cc b/ui/views/examples/menu_example.cc index fd20aee..f158ded 100644 --- a/ui/views/examples/menu_example.cc +++ b/ui/views/examples/menu_example.cc @@ -10,9 +10,9 @@ #include "ui/base/models/simple_menu_model.h" #include "ui/views/controls/button/menu_button.h" #include "ui/views/controls/button/text_button.h" +#include "ui/views/controls/menu/menu_2.h" +#include "ui/views/controls/menu/view_menu_delegate.h" #include "ui/views/layout/fill_layout.h" -#include "views/controls/menu/menu_2.h" -#include "views/controls/menu/view_menu_delegate.h" #include "views/view.h" namespace { diff --git a/ui/views/touchui/touch_selection_controller_impl.cc b/ui/views/touchui/touch_selection_controller_impl.cc index b1ac3a0..1d64a0a 100644 --- a/ui/views/touchui/touch_selection_controller_impl.cc +++ b/ui/views/touchui/touch_selection_controller_impl.cc @@ -20,11 +20,11 @@ #include "ui/views/controls/button/button.h" #include "ui/views/controls/button/custom_button.h" #include "ui/views/controls/button/text_button.h" +#include "ui/views/controls/menu/menu_config.h" #include "ui/views/layout/box_layout.h" #include "ui/views/widget/widget.h" #include "views/background.h" #include "views/controls/label.h" -#include "views/controls/menu/menu_config.h" namespace { diff --git a/ui/views/widget/widget.cc b/ui/views/widget/widget.cc index 4503720..8d8fad7 100644 --- a/ui/views/widget/widget.cc +++ b/ui/views/widget/widget.cc @@ -13,6 +13,7 @@ #include "ui/gfx/compositor/compositor.h" #include "ui/gfx/compositor/layer.h" #include "ui/gfx/screen.h" +#include "ui/views/controls/menu/menu_controller.h" #include "ui/views/focus/focus_manager.h" #include "ui/views/focus/focus_manager_factory.h" #include "ui/views/focus/view_storage.h" @@ -24,7 +25,6 @@ #include "ui/views/widget/tooltip_manager.h" #include "ui/views/widget/widget_delegate.h" #include "ui/views/window/custom_frame_view.h" -#include "views/controls/menu/menu_controller.h" #include "views/views_delegate.h" namespace { |