diff options
author | ben@chromium.org <ben@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-05-08 00:34:05 +0000 |
---|---|---|
committer | ben@chromium.org <ben@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-05-08 00:34:05 +0000 |
commit | 2362e4fe2905ab75d3230ebc3e307ae53e2b8362 (patch) | |
tree | e6d88357a2021811e0e354f618247217be8bb3da /views/controls/menu | |
parent | db23ac3e713dc17509b2b15d3ee634968da45715 (diff) | |
download | chromium_src-2362e4fe2905ab75d3230ebc3e307ae53e2b8362.zip chromium_src-2362e4fe2905ab75d3230ebc3e307ae53e2b8362.tar.gz chromium_src-2362e4fe2905ab75d3230ebc3e307ae53e2b8362.tar.bz2 |
Move src/chrome/views to src/views. RS=darin http://crbug.com/11387
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@15604 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'views/controls/menu')
-rw-r--r-- | views/controls/menu/chrome_menu.cc | 2816 | ||||
-rw-r--r-- | views/controls/menu/chrome_menu.h | 948 | ||||
-rw-r--r-- | views/controls/menu/controller.h | 33 | ||||
-rw-r--r-- | views/controls/menu/menu.cc | 626 | ||||
-rw-r--r-- | views/controls/menu/menu.h | 355 | ||||
-rw-r--r-- | views/controls/menu/view_menu_delegate.h | 34 |
6 files changed, 4812 insertions, 0 deletions
diff --git a/views/controls/menu/chrome_menu.cc b/views/controls/menu/chrome_menu.cc new file mode 100644 index 0000000..a887fd5 --- /dev/null +++ b/views/controls/menu/chrome_menu.cc @@ -0,0 +1,2816 @@ +// Copyright (c) 2006-2008 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 "views/controls/menu/chrome_menu.h" + +#include <windows.h> +#include <uxtheme.h> +#include <Vssym32.h> + +#include "app/gfx/chrome_canvas.h" +#include "app/l10n_util.h" +#include "app/l10n_util_win.h" +#include "app/os_exchange_data.h" +#include "base/base_drag_source.h" +#include "base/gfx/native_theme.h" +#include "base/message_loop.h" +#include "base/task.h" +#include "base/timer.h" +#include "base/win_util.h" +// TODO(beng): (Cleanup) remove this browser dep. +#include "chrome/browser/drag_utils.h" +#include "chrome/common/gfx/color_utils.h" +#include "grit/generated_resources.h" +#include "skia/ext/skia_utils_win.h" +#include "views/border.h" +#include "views/view_constants.h" +#include "views/widget/root_view.h" +#include "views/widget/widget_win.h" + +#undef min +#undef max + +using base::Time; +using base::TimeDelta; + +// 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 used if the menu doesn't have icons. +static const int kItemNoIconTopMargin = 1; +static const int kItemNoIconBottomMargin = 3; + +// Margins between the left of the item and the icon. +static const int kItemLeftMargin = 4; + +// Padding between the label and submenu arrow. +static const int kLabelToArrowPadding = 10; + +// Padding between the arrow and the edge. +static const int kArrowToEdgePadding = 5; + +// Padding between the icon and label. +static const int kIconToLabelPadding = 8; + +// Padding between the gutter and label. +static const int kGutterToLabel = 5; + +// Height of the scroll arrow. +// This goes up to 4 with large fonts, but this is close enough for now. +static const int kScrollArrowHeight = 3; + +// Size of the check. This comes from the OS. +static int check_width; +static int check_height; + +// Size of the submenu arrow. This comes from the OS. +static int arrow_width; +static int arrow_height; + +// Width of the gutter. Only used if render_gutter is true. +static int gutter_width; + +// Margins between the right of the item and the label. +static int item_right_margin; + +// X-coordinate of where the label starts. +static int label_start; + +// Height of the separator. +static int separator_height; + +// Padding around the edges of the submenu. +static const int kSubmenuBorderSize = 3; + +// Amount to inset submenus. +static const int kSubmenuHorizontalInset = 3; + +// 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; + +// Height of the drop indicator. This should be an event number. +static const int kDropIndicatorHeight = 2; + +// Color of the drop indicator. +static const SkColor kDropIndicatorColor = SK_ColorBLACK; + +// Whether or not the gutter should be rendered. The gutter is specific to +// Vista. +static bool render_gutter = false; + +// Max width of a menu. There does not appear to be an OS value for this, yet +// both IE and FF restrict the max width of a menu. +static const int kMaxMenuWidth = 400; + +// Period of the scroll timer (in milliseconds). +static const int kScrollTimerMS = 30; + +// Preferred height of menu items. Reset every time a menu is run. +static int pref_menu_height; + +// Are mnemonics shown? This is updated before the menus are shown. +static bool show_mnemonics; + +using gfx::NativeTheme; + +namespace views { + +namespace { + +// Returns the font menus are to use. +ChromeFont GetMenuFont() { + NONCLIENTMETRICS metrics; + win_util::GetNonClientMetrics(&metrics); + + l10n_util::AdjustUIFont(&(metrics.lfMenuFont)); + HFONT font = CreateFontIndirect(&metrics.lfMenuFont); + DLOG_ASSERT(font); + return ChromeFont::CreateFont(font); +} + +// Calculates all sizes that we can from the OS. +// +// This is invoked prior to Running a menu. +void UpdateMenuPartSizes(bool has_icons) { + HDC dc = GetDC(NULL); + RECT bounds = { 0, 0, 200, 200 }; + SIZE check_size; + if (NativeTheme::instance()->GetThemePartSize( + NativeTheme::MENU, dc, MENU_POPUPCHECK, MC_CHECKMARKNORMAL, &bounds, + TS_TRUE, &check_size) == S_OK) { + check_width = check_size.cx; + check_height = check_size.cy; + } else { + check_width = GetSystemMetrics(SM_CXMENUCHECK); + check_height = GetSystemMetrics(SM_CYMENUCHECK); + } + + SIZE arrow_size; + if (NativeTheme::instance()->GetThemePartSize( + NativeTheme::MENU, dc, MENU_POPUPSUBMENU, MSM_NORMAL, &bounds, + TS_TRUE, &arrow_size) == S_OK) { + arrow_width = arrow_size.cx; + arrow_height = arrow_size.cy; + } else { + // Sadly I didn't see a specify metrics for this. + arrow_width = GetSystemMetrics(SM_CXMENUCHECK); + arrow_height = GetSystemMetrics(SM_CYMENUCHECK); + } + + SIZE gutter_size; + if (NativeTheme::instance()->GetThemePartSize( + NativeTheme::MENU, dc, MENU_POPUPGUTTER, MSM_NORMAL, &bounds, + TS_TRUE, &gutter_size) == S_OK) { + gutter_width = gutter_size.cx; + render_gutter = true; + } else { + gutter_width = 0; + render_gutter = false; + } + + SIZE separator_size; + if (NativeTheme::instance()->GetThemePartSize( + NativeTheme::MENU, dc, MENU_POPUPSEPARATOR, MSM_NORMAL, &bounds, + TS_TRUE, &separator_size) == S_OK) { + separator_height = separator_size.cy; + } else { + separator_height = GetSystemMetrics(SM_CYMENU) / 2; + } + + item_right_margin = kLabelToArrowPadding + arrow_width + kArrowToEdgePadding; + + if (has_icons) { + label_start = kItemLeftMargin + check_width + kIconToLabelPadding; + } 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 = kItemLeftMargin + check_width; + } + if (render_gutter) + label_start += gutter_width + kGutterToLabel; + + ReleaseDC(NULL, dc); + + MenuItemView menu_item(NULL); + menu_item.SetTitle(L"blah"); // Text doesn't matter here. + pref_menu_height = menu_item.GetPreferredSize().height(); +} + +// Convenience for scrolling the view such that the origin is visible. +static void ScrollToVisible(View* view) { + view->ScrollRectToVisible(0, 0, view->width(), view->height()); +} + +// 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 MenuScrollTask { + public: + MenuScrollTask() : submenu_(NULL) { + pixels_per_second_ = 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(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); + int target_y = start_y_; + if (is_scrolling_up_) + target_y = std::max(0, target_y - delta_y); + else + target_y = std::min(submenu_->height() - vis_rect.height(), + target_y + delta_y); + submenu_->ScrollRectToVisible(vis_rect.x(), target_y, vis_rect.width(), + vis_rect.height()); + } + + // 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); +}; + +// 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: + explicit MenuScrollButton(SubmenuView* host, bool is_up) + : host_(host), + is_up_(is_up), + // Make our height the same as that of other MenuItemViews. + pref_height_(pref_menu_height) { + } + + virtual gfx::Size GetPreferredSize() { + return gfx::Size(kScrollArrowHeight * 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 DragDropTypes::DRAG_NONE; + } + + virtual void OnDragExited() { + DCHECK(host_->GetMenuItem()->GetMenuController()); + host_->GetMenuItem()->GetMenuController()->OnDragExitedScrollButton(host_); + } + + virtual int OnPerformDrop(const DropTargetEvent& event) { + return DragDropTypes::DRAG_NONE; + } + + virtual void Paint(ChromeCanvas* canvas) { + HDC dc = canvas->beginPlatformPaint(); + + // The background. + RECT item_bounds = { 0, 0, width(), height() }; + NativeTheme::instance()->PaintMenuItemBackground( + NativeTheme::MENU, dc, MENU_POPUPITEM, MPI_NORMAL, false, + &item_bounds); + + // Then the arrow. + int x = width() / 2; + int y = (height() - kScrollArrowHeight) / 2; + int delta_y = 1; + if (!is_up_) { + delta_y = -1; + y += kScrollArrowHeight; + } + SkColor arrow_color = color_utils::GetSysSkColor(COLOR_MENUTEXT); + for (int i = 0; i < kScrollArrowHeight; ++i, --x, y += delta_y) + canvas->FillRectInt(arrow_color, x, y, (i * 2) + 1, 1); + + canvas->endPlatformPaint(); + } + + 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); +}; + +// 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, we use a one off variant. + +class MenuScrollView : public View { + public: + explicit MenuScrollView(View* child) { + AddChildView(child); + } + + virtual void ScrollRectToVisible(int x, int y, int width, int height) { + // NOTE: this assumes we only want to scroll in the y direction. + + View* child = GetContents(); + // Convert y to view's coordinates. + y -= child->y(); + gfx::Size pref = child->GetPreferredSize(); + // Constrain y to make sure we don't show past the bottom of the view. + y = std::max(0, std::min(pref.height() - this->height(), y)); + child->SetY(-y); + } + + // Returns the contents, which is the SubmenuView. + View* GetContents() { + return GetChildViewAt(0); + } + + private: + DISALLOW_COPY_AND_ASSIGN(MenuScrollView); +}; + +// MenuScrollViewContainer ----------------------------------------------------- + +// 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) { + 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( + kSubmenuBorderSize, kSubmenuBorderSize, + kSubmenuBorderSize, kSubmenuBorderSize)); + } + + virtual void Paint(ChromeCanvas* canvas) { + HDC dc = canvas->beginPlatformPaint(); + CRect bounds(0, 0, width(), height()); + NativeTheme::instance()->PaintMenuBackground( + NativeTheme::MENU, dc, MENU_POPUPBACKGROUND, 0, &bounds); + canvas->endPlatformPaint(); + } + + View* scroll_down_button() { return scroll_down_button_; } + + View* scroll_up_button() { return scroll_up_button_; } + + virtual void 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(); + } + + virtual void DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current) { + gfx::Size content_pref = scroll_view_->GetContents()->GetPreferredSize(); + scroll_up_button_->SetVisible(content_pref.height() > height()); + scroll_down_button_->SetVisible(content_pref.height() > height()); + } + + virtual gfx::Size GetPreferredSize() { + gfx::Size prefsize = scroll_view_->GetContents()->GetPreferredSize(); + gfx::Insets insets = GetInsets(); + prefsize.Enlarge(insets.width(), insets.height()); + return prefsize; + } + + private: + // The scroll buttons. + View* scroll_up_button_; + View* scroll_down_button_; + + // The scroll view. + MenuScrollView* scroll_view_; + + DISALLOW_COPY_AND_ASSIGN(MenuScrollViewContainer); +}; + +// MenuSeparator --------------------------------------------------------------- + +// Renders a separator. + +class MenuSeparator : public View { + public: + MenuSeparator() { + } + + void Paint(ChromeCanvas* canvas) { + // The gutter is rendered before the background. + int start_x = 0; + int start_y = height() / 3; + HDC dc = canvas->beginPlatformPaint(); + if (render_gutter) { + // If render_gutter is true, we're on Vista and need to render the + // gutter, then indent the separator from the gutter. + RECT gutter_bounds = { label_start - kGutterToLabel - gutter_width, 0, 0, + height() }; + gutter_bounds.right = gutter_bounds.left + gutter_width; + NativeTheme::instance()->PaintMenuGutter(dc, MENU_POPUPGUTTER, MPI_NORMAL, + &gutter_bounds); + start_x = gutter_bounds.left + gutter_width; + start_y = 0; + } + RECT separator_bounds = { start_x, start_y, width(), height() }; + NativeTheme::instance()->PaintMenuSeparator(dc, MENU_POPUPSEPARATOR, + MPI_NORMAL, &separator_bounds); + canvas->endPlatformPaint(); + } + + gfx::Size GetPreferredSize() { + return gfx::Size(10, // Just in case we're the only item in a menu. + separator_height); + } + + private: + DISALLOW_COPY_AND_ASSIGN(MenuSeparator); +}; + +// MenuHostRootView ---------------------------------------------------------- + +// 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 RootView { + public: + explicit MenuHostRootView(Widget* widget, + SubmenuView* submenu) + : RootView(widget), + submenu_(submenu), + forward_drag_to_menu_controller_(true), + suspend_events_(false) { +#ifdef DEBUG_MENU + DLOG(INFO) << " new MenuHostRootView " << this; +#endif + } + + virtual bool OnMousePressed(const MouseEvent& event) { + if (suspend_events_) + return true; + + forward_drag_to_menu_controller_ = + ((event.x() < 0 || event.y() < 0 || event.x() >= width() || + event.y() >= height()) || + !RootView::OnMousePressed(event)); + if (forward_drag_to_menu_controller_) + GetMenuController()->OnMousePressed(submenu_, event); + return true; + } + + virtual bool OnMouseDragged(const MouseEvent& event) { + if (suspend_events_) + return true; + + if (forward_drag_to_menu_controller_) { +#ifdef DEBUG_MENU + DLOG(INFO) << " MenuHostRootView::OnMouseDragged source=" << submenu_; +#endif + GetMenuController()->OnMouseDragged(submenu_, event); + return true; + } + return RootView::OnMouseDragged(event); + } + + virtual void OnMouseReleased(const MouseEvent& event, bool canceled) { + if (suspend_events_) + return; + + RootView::OnMouseReleased(event, canceled); + if (forward_drag_to_menu_controller_) { + forward_drag_to_menu_controller_ = false; + if (canceled) { + GetMenuController()->Cancel(true); + } else { + GetMenuController()->OnMouseReleased(submenu_, event); + } + } + } + + virtual void OnMouseMoved(const MouseEvent& event) { + if (suspend_events_) + return; + + RootView::OnMouseMoved(event); + GetMenuController()->OnMouseMoved(submenu_, event); + } + + virtual void ProcessOnMouseExited() { + if (suspend_events_) + return; + + RootView::ProcessOnMouseExited(); + } + + virtual bool ProcessMouseWheelEvent(const MouseWheelEvent& e) { + // RootView::ProcessMouseWheelEvent 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(e); + } + + void SuspendEvents() { + suspend_events_ = true; + } + + private: + MenuController* GetMenuController() { + return submenu_->GetMenuItem()->GetMenuController(); + } + + // The SubmenuView we contain. + SubmenuView* submenu_; + + // Whether mouse dragged/released should be forwarded to the MenuController. + bool forward_drag_to_menu_controller_; + + // Whether events are suspended. If true, no events are forwarded to the + // MenuController. + bool suspend_events_; + + DISALLOW_COPY_AND_ASSIGN(MenuHostRootView); +}; + +// MenuHost ------------------------------------------------------------------ + +// MenuHost is the window responsible for showing a single menu. +// +// Similar to MenuHostRootView, care must be taken such that when MenuHost is +// deleted, it doesn't delete the menu items. MenuHost is closed via a +// DelayedClosed, which avoids timing issues with deleting the window while +// capture or events are directed at it. + +class MenuHost : public WidgetWin { + public: + explicit MenuHost(SubmenuView* submenu) + : closed_(false), + submenu_(submenu), + owns_capture_(false) { + set_window_style(WS_POPUP); + set_initial_class_style( + (win_util::GetWinVersion() < win_util::WINVERSION_XP) ? + 0 : CS_DROPSHADOW); + is_mouse_down_ = + ((GetKeyState(VK_LBUTTON) & 0x80) || + (GetKeyState(VK_RBUTTON) & 0x80) || + (GetKeyState(VK_MBUTTON) & 0x80) || + (GetKeyState(VK_XBUTTON1) & 0x80) || + (GetKeyState(VK_XBUTTON2) & 0x80)); + // Mouse clicks shouldn't give us focus. + set_window_ex_style(WS_EX_TOPMOST | WS_EX_NOACTIVATE); + } + + void Init(HWND parent, + const gfx::Rect& bounds, + View* contents_view, + bool do_capture) { + WidgetWin::Init(parent, bounds, true); + SetContentsView(contents_view); + // We don't want to take focus away from the hosting window. + ShowWindow(SW_SHOWNA); + owns_capture_ = do_capture; + if (do_capture) { + SetCapture(); + has_capture_ = true; +#ifdef DEBUG_MENU + DLOG(INFO) << "Doing capture"; +#endif + } + } + + virtual void Hide() { + if (closed_) { + // We're already closed, nothing to do. + // This is invoked twice if the first time just hid us, and the second + // time deleted Closed (deleted) us. + return; + } + // The menus are freed separately, and possibly before the window is closed, + // remove them so that View doesn't try to access deleted objects. + static_cast<MenuHostRootView*>(GetRootView())->SuspendEvents(); + GetRootView()->RemoveAllChildViews(false); + closed_ = true; + ReleaseCapture(); + WidgetWin::Hide(); + } + + virtual void HideWindow() { + // Make sure we release capture before hiding. + ReleaseCapture(); + WidgetWin::Hide(); + } + + virtual void OnCaptureChanged(HWND hwnd) { + WidgetWin::OnCaptureChanged(hwnd); + owns_capture_ = false; +#ifdef DEBUG_MENU + DLOG(INFO) << "Capture changed"; +#endif + } + + void ReleaseCapture() { + if (owns_capture_) { +#ifdef DEBUG_MENU + DLOG(INFO) << "released capture"; +#endif + owns_capture_ = false; + ::ReleaseCapture(); + } + } + + protected: + // Overriden to create MenuHostRootView. + virtual RootView* CreateRootView() { + return new MenuHostRootView(this, submenu_); + } + + virtual void OnCancelMode() { + if (!closed_) { +#ifdef DEBUG_MENU + DLOG(INFO) << "OnCanceMode, closing menu"; +#endif + submenu_->GetMenuItem()->GetMenuController()->Cancel(true); + } + } + + // Overriden to return false, we do NOT want to release capture on mouse + // release. + virtual bool ReleaseCaptureOnMouseReleased() { + return false; + } + + private: + // If true, we've been closed. + bool closed_; + + // If true, we own the capture and need to release it. + bool owns_capture_; + + // The view we contain. + SubmenuView* submenu_; + + DISALLOW_COPY_AND_ASSIGN(MenuHost); +}; + +// 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: + // ID used for EmptyMenuMenuItem. + static const int kEmptyMenuItemViewID; + + explicit EmptyMenuMenuItem(MenuItemView* parent) : + MenuItemView(parent, 0, NORMAL) { + SetTitle(l10n_util::GetString(IDS_MENU_EMPTY_SUBMENU)); + // Set this so that we're not identified as a normal menu item. + SetID(kEmptyMenuItemViewID); + SetEnabled(false); + } + + private: + DISALLOW_COPY_AND_ASSIGN(EmptyMenuMenuItem); +}; + +// static +const int EmptyMenuMenuItem::kEmptyMenuItemViewID = + MenuItemView::kMenuItemViewID + 1; + +} // namespace + +// SubmenuView --------------------------------------------------------------- + +SubmenuView::SubmenuView(MenuItemView* parent) + : parent_menu_item_(parent), + host_(NULL), + drop_item_(NULL), + drop_position_(MenuDelegate::DROP_NONE), + scroll_view_container_(NULL) { + DCHECK(parent); + // We'll delete ourselves, otherwise the ScrollView would delete us on close. + SetParentOwned(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 < GetChildViewCount(); ++i) { + if (GetChildViewAt(i)->GetID() == MenuItemView::kMenuItemViewID) + count++; + } + return count; +} + +MenuItemView* SubmenuView::GetMenuItemAt(int index) { + for (int i = 0, count = 0; i < GetChildViewCount(); ++i) { + if (GetChildViewAt(i)->GetID() == MenuItemView::kMenuItemViewID && + count++ == index) { + return static_cast<MenuItemView*>(GetChildViewAt(i)); + } + } + NOTREACHED(); + return NULL; +} + +void SubmenuView::Layout() { + // We're in a ScrollView, and need to set our width/height ourselves. + View* parent = GetParent(); + if (!parent) + return; + SetBounds(x(), y(), parent->width(), GetPreferredSize().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 < GetChildViewCount(); ++i) { + View* child = GetChildViewAt(i); + 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 (GetChildViewCount() == 0) + return gfx::Size(); + + int max_width = 0; + int height = 0; + for (int i = 0; i < GetChildViewCount(); ++i) { + View* child = GetChildViewAt(i); + gfx::Size child_pref_size = child->GetPreferredSize(); + max_width = std::max(max_width, child_pref_size.width()); + height += child_pref_size.height(); + } + gfx::Insets insets = GetInsets(); + return gfx::Size(max_width + insets.width(), height + insets.height()); +} + +void SubmenuView::DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current) { + SchedulePaint(); +} + +void SubmenuView::PaintChildren(ChromeCanvas* canvas) { + View::PaintChildren(canvas); + + if (drop_item_ && drop_position_ != MenuDelegate::DROP_ON) + PaintDropIndicator(canvas, drop_item_, drop_position_); +} + +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 first_vis_index = -1; + for (int i = 0; i < menu_item_count; ++i) { + MenuItemView* menu_item = GetMenuItemAt(i); + if (menu_item->y() == vis_bounds.y()) { + first_vis_index = i; + break; + } else if (menu_item->y() > vis_bounds.y()) { + first_vis_index = std::max(0, i - 1); + break; + } + } + if (first_vis_index == -1) + return true; + + // If the first item isn't entirely visible, make it visible, otherwise make + // the next/previous one entirely visible. + int delta = abs(e.GetOffset() / WHEEL_DELTA); + bool scroll_up = (e.GetOffset() > 0); + while (delta-- > 0) { + int scroll_amount = 0; + if (scroll_up) { + if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) { + if (first_vis_index != 0) { + scroll_amount = GetMenuItemAt(first_vis_index - 1)->y() - + vis_bounds.y(); + first_vis_index--; + } else { + break; + } + } else { + scroll_amount = GetMenuItemAt(first_vis_index)->y() - vis_bounds.y(); + } + } else { + if (first_vis_index + 1 == GetMenuItemCount()) + break; + scroll_amount = GetMenuItemAt(first_vis_index + 1)->y() - + vis_bounds.y(); + if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) + first_vis_index++; + } + ScrollRectToVisible(0, vis_bounds.y() + scroll_amount, vis_bounds.width(), + vis_bounds.height()); + vis_bounds = GetVisibleBounds(); + } + + return true; +} + +bool SubmenuView::IsShowing() { + return host_ && host_->IsVisible(); +} + +void SubmenuView::ShowAt(HWND parent, + const gfx::Rect& bounds, + bool do_capture) { + if (host_) { + host_->ShowWindow(SW_SHOWNA); + return; + } + + host_ = new MenuHost(this); + // Force construction of the scroll view container. + GetScrollViewContainer(); + // Make sure the first row is visible. + ScrollRectToVisible(0, 0, 1, 1); + host_->Init(parent, bounds, scroll_view_container_, do_capture); +} + +void SubmenuView::Close() { + if (host_) { + host_->Close(); + host_ = NULL; + } +} + +void SubmenuView::Hide() { + if (host_) + host_->HideWindow(); +} + +void SubmenuView::ReleaseCapture() { + host_->ReleaseCapture(); +} + +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_->SetParentOwned(false); + } + return scroll_view_container_; +} + +void SubmenuView::PaintDropIndicator(ChromeCanvas* canvas, + MenuItemView* item, + MenuDelegate::DropPosition position) { + if (position == MenuDelegate::DROP_NONE) + return; + + gfx::Rect bounds = CalculateDropIndicatorBounds(item, position); + canvas->FillRectInt(kDropIndicatorColor, bounds.x(), bounds.y(), + bounds.width(), bounds.height()); +} + +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) { + gfx::Rect bounds = CalculateDropIndicatorBounds(item, position); + SchedulePaint(bounds.x(), bounds.y(), bounds.width(), bounds.height()); + } +} + +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(); + } +} + +// MenuItemView --------------------------------------------------------------- + +// static +const int MenuItemView::kMenuItemViewID = 1001; + +// static +bool MenuItemView::allow_task_nesting_during_run_ = false; + +MenuItemView::MenuItemView(MenuDelegate* delegate) { + // NOTE: don't check the delegate for NULL, UpdateMenuPartSizes supplies a + // NULL delegate. + Init(NULL, 0, SUBMENU, delegate); +} + +MenuItemView::~MenuItemView() { + if (controller_) { + // We're currently showing. + + // We can't delete ourselves while we're blocking. + DCHECK(!controller_->IsBlockingRun()); + + // Invoking Cancel is going to call us back and notify the delegate. + // Notifying the delegate from the destructor can be problematic. To avoid + // this the delegate is set to NULL. + delegate_ = NULL; + + controller_->Cancel(true); + } + delete submenu_; +} + +void MenuItemView::RunMenuAt(HWND parent, + const gfx::Rect& bounds, + AnchorPosition anchor, + bool has_mnemonics) { + PrepareForRun(has_mnemonics); + + int mouse_event_flags; + + MenuController* controller = MenuController::GetActiveInstance(); + if (controller && !controller->IsBlockingRun()) { + // A menu is already showing, but it isn't a blocking menu. Cancel it. + // We can get here during drag and drop if the user right clicks on the + // menu quickly after the drop. + controller->Cancel(true); + controller = NULL; + } + bool owns_controller = false; + if (!controller) { + // No menus are showing, show one. + controller = new MenuController(true); + MenuController::SetActiveInstance(controller); + owns_controller = true; + } else { + // A menu is already showing, use the same controller. + + // Don't support blocking from within non-blocking. + DCHECK(controller->IsBlockingRun()); + } + + controller_ = controller; + + // Run the loop. + MenuItemView* result = + controller->Run(parent, this, bounds, anchor, &mouse_event_flags); + + RemoveEmptyMenus(); + + controller_ = NULL; + + if (owns_controller) { + // We created the controller and need to delete it. + if (MenuController::GetActiveInstance() == controller) + MenuController::SetActiveInstance(NULL); + delete controller; + } + // Make sure all the windows we created to show the menus have been + // destroyed. + DestroyAllMenuHosts(); + if (result && delegate_) + delegate_->ExecuteCommand(result->GetCommand(), mouse_event_flags); +} + +void MenuItemView::RunMenuForDropAt(HWND parent, + const gfx::Rect& bounds, + AnchorPosition anchor) { + PrepareForRun(false); + + // If there is a menu, hide it so that only one menu is shown during dnd. + MenuController* current_controller = MenuController::GetActiveInstance(); + if (current_controller) { + current_controller->Cancel(true); + } + + // Always create a new controller for non-blocking. + controller_ = new MenuController(false); + + // Set the instance, that way it can be canceled by another menu. + MenuController::SetActiveInstance(controller_); + + controller_->Run(parent, this, bounds, anchor, NULL); +} + +void MenuItemView::Cancel() { + if (controller_ && !canceled_) { + canceled_ = true; + controller_->Cancel(true); + } +} + +SubmenuView* MenuItemView::CreateSubmenu() { + if (!submenu_) + submenu_ = new SubmenuView(this); + return submenu_; +} + +void MenuItemView::SetSelected(bool selected) { + selected_ = selected; + SchedulePaint(); +} + +void MenuItemView::SetIcon(const SkBitmap& icon, int item_id) { + MenuItemView* item = GetDescendantByID(item_id); + DCHECK(item); + item->SetIcon(icon); +} + +void MenuItemView::SetIcon(const SkBitmap& icon) { + icon_ = icon; + SchedulePaint(); +} + +void MenuItemView::Paint(ChromeCanvas* canvas) { + Paint(canvas, false); +} + +gfx::Size MenuItemView::GetPreferredSize() { + ChromeFont& font = GetRootMenuItem()->font_; + return gfx::Size( + font.GetStringWidth(title_) + label_start + item_right_margin, + font.height() + GetBottomMargin() + GetTopMargin()); +} + +MenuController* MenuItemView::GetMenuController() { + return GetRootMenuItem()->controller_; +} + +MenuDelegate* MenuItemView::GetDelegate() { + return GetRootMenuItem()->delegate_; +} + +MenuItemView* MenuItemView::GetRootMenuItem() { + MenuItemView* item = this; + while (item) { + MenuItemView* parent = item->GetParentMenuItem(); + if (!parent) + return item; + item = parent; + } + NOTREACHED(); + return NULL; +} + +wchar_t MenuItemView::GetMnemonic() { + if (!has_mnemonics_) + return 0; + + const std::wstring& title = GetTitle(); + size_t index = 0; + do { + index = title.find('&', index); + if (index != std::wstring::npos) { + if (index + 1 != title.size() && title[index + 1] != '&') + return title[index + 1]; + index++; + } + } while (index != std::wstring::npos); + return 0; +} + +MenuItemView::MenuItemView(MenuItemView* parent, + int command, + MenuItemView::Type type) { + Init(parent, command, type, NULL); +} + +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; + // Assign our ID, this allows SubmenuItemView to find MenuItemViews. + SetID(kMenuItemViewID); + has_icons_ = false; + + MenuDelegate* root_delegate = GetDelegate(); + if (root_delegate) + SetEnabled(root_delegate->IsCommandEnabled(command)); +} + +MenuItemView* MenuItemView::AppendMenuItemInternal(int item_id, + const std::wstring& label, + const SkBitmap& icon, + Type type) { + if (!submenu_) + CreateSubmenu(); + if (type == SEPARATOR) { + submenu_->AddChildView(new MenuSeparator()); + 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_->AddChildView(item); + return item; +} + +MenuItemView* MenuItemView::GetDescendantByID(int id) { + if (GetCommand() == id) + return this; + if (!HasSubmenu()) + return NULL; + for (int i = 0; i < GetSubmenu()->GetChildViewCount(); ++i) { + View* child = GetSubmenu()->GetChildViewAt(i); + if (child->GetID() == MenuItemView::kMenuItemViewID) { + MenuItemView* result = static_cast<MenuItemView*>(child)-> + GetDescendantByID(id); + if (result) + return result; + } + } + return NULL; +} + +void MenuItemView::DropMenuClosed(bool notify_delegate) { + DCHECK(controller_); + DCHECK(!controller_->IsBlockingRun()); + if (MenuController::GetActiveInstance() == controller_) + MenuController::SetActiveInstance(NULL); + delete controller_; + controller_ = NULL; + + RemoveEmptyMenus(); + + if (notify_delegate && delegate_) { + // Our delegate is null when invoked from the destructor. + delegate_->DropMenuClosed(this); + } + // WARNING: its possible the delegate deleted us at this point. +} + +void MenuItemView::PrepareForRun(bool has_mnemonics) { + // Currently we only support showing the root. + DCHECK(!parent_menu_item_); + + // Don't invoke run from within run on the same menu. + DCHECK(!controller_); + + // Force us to have a submenu. + CreateSubmenu(); + + canceled_ = false; + + has_mnemonics_ = has_mnemonics; + + AddEmptyMenus(); + + if (!MenuController::GetActiveInstance()) { + // Only update the menu size if there are no menus showing, otherwise + // things may shift around. + UpdateMenuPartSizes(has_icons_); + } + + font_ = GetMenuFont(); + + BOOL show_cues; + show_mnemonics = + (SystemParametersInfo(SPI_GETKEYBOARDCUES, 0, &show_cues, 0) && + show_cues == TRUE); +} + +int MenuItemView::GetDrawStringFlags() { + int flags = 0; + if (UILayoutIsRightToLeft()) + flags |= ChromeCanvas::TEXT_ALIGN_RIGHT; + else + flags |= ChromeCanvas::TEXT_ALIGN_LEFT; + + if (has_mnemonics_) { + if (show_mnemonics) + flags |= ChromeCanvas::SHOW_PREFIX; + else + flags |= ChromeCanvas::HIDE_PREFIX; + } + return flags; +} + +void MenuItemView::AddEmptyMenus() { + DCHECK(HasSubmenu()); + if (submenu_->GetChildViewCount() == 0) { + submenu_->AddChildView(0, new EmptyMenuMenuItem(this)); + } 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_->GetChildViewCount() - 1; i >= 0; --i) { + View* child = submenu_->GetChildViewAt(i); + if (child->GetID() == MenuItemView::kMenuItemViewID) { + MenuItemView* menu_item = static_cast<MenuItemView*>(child); + if (menu_item->HasSubmenu()) + menu_item->RemoveEmptyMenus(); + } else if (child->GetID() == EmptyMenuMenuItem::kEmptyMenuItemViewID) { + submenu_->RemoveChildView(child); + } + } +} + +void MenuItemView::AdjustBoundsForRTLUI(RECT* rect) const { + gfx::Rect mirrored_rect(*rect); + mirrored_rect.set_x(MirroredLeftPointForRect(mirrored_rect)); + *rect = mirrored_rect.ToRECT(); +} + +void MenuItemView::Paint(ChromeCanvas* canvas, bool for_drag) { + bool render_selection = + (!for_drag && IsSelected() && + parent_menu_item_->GetSubmenu()->GetShowSelection(this)); + int state = render_selection ? MPI_HOT : + (IsEnabled() ? MPI_NORMAL : MPI_DISABLED); + HDC dc = canvas->beginPlatformPaint(); + + // The gutter is rendered before the background. + if (render_gutter && !for_drag) { + RECT gutter_bounds = { label_start - kGutterToLabel - gutter_width, 0, 0, + height() }; + gutter_bounds.right = gutter_bounds.left + gutter_width; + AdjustBoundsForRTLUI(&gutter_bounds); + NativeTheme::instance()->PaintMenuGutter(dc, MENU_POPUPGUTTER, MPI_NORMAL, + &gutter_bounds); + } + + // Render the background. + if (!for_drag) { + RECT item_bounds = { 0, 0, width(), height() }; + AdjustBoundsForRTLUI(&item_bounds); + NativeTheme::instance()->PaintMenuItemBackground( + NativeTheme::MENU, dc, MENU_POPUPITEM, state, render_selection, + &item_bounds); + } + + int icon_x = kItemLeftMargin; + int top_margin = GetTopMargin(); + int bottom_margin = GetBottomMargin(); + int icon_y = top_margin + (height() - kItemTopMargin - + bottom_margin - check_height) / 2; + int icon_height = check_height; + int icon_width = check_width; + + if (type_ == CHECKBOX && GetDelegate()->IsItemChecked(GetCommand())) { + // Draw the check background. + RECT check_bg_bounds = { 0, 0, icon_x + icon_width, height() }; + const int bg_state = IsEnabled() ? MCB_NORMAL : MCB_DISABLED; + AdjustBoundsForRTLUI(&check_bg_bounds); + NativeTheme::instance()->PaintMenuCheckBackground( + NativeTheme::MENU, dc, MENU_POPUPCHECKBACKGROUND, bg_state, + &check_bg_bounds); + + // And the check. + RECT check_bounds = { icon_x, icon_y, icon_x + icon_width, + icon_y + icon_height }; + const int check_state = IsEnabled() ? MC_CHECKMARKNORMAL : + MC_CHECKMARKDISABLED; + AdjustBoundsForRTLUI(&check_bounds); + NativeTheme::instance()->PaintMenuCheck( + NativeTheme::MENU, dc, MENU_POPUPCHECK, check_state, &check_bounds, + render_selection); + } + + // Render the foreground. + // Menu color is specific to Vista, fallback to classic colors if can't + // get color. + int default_sys_color = render_selection ? COLOR_HIGHLIGHTTEXT : + (IsEnabled() ? COLOR_MENUTEXT : COLOR_GRAYTEXT); + SkColor fg_color = NativeTheme::instance()->GetThemeColorWithDefault( + NativeTheme::MENU, MENU_POPUPITEM, state, TMT_TEXTCOLOR, + default_sys_color); + int width = this->width() - item_right_margin - label_start; + ChromeFont& font = GetRootMenuItem()->font_; + gfx::Rect text_bounds(label_start, top_margin, width, font.height()); + text_bounds.set_x(MirroredLeftPointForRect(text_bounds)); + if (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->DrawStringWithHalo(GetTitle(), font, 0x00000000, 0xFFFFFFFF, + text_bounds.x(), text_bounds.y(), + text_bounds.width(), text_bounds.height(), + GetRootMenuItem()->GetDrawStringFlags()); + } else { + canvas->DrawStringInt(GetTitle(), font, fg_color, + text_bounds.x(), text_bounds.y(), text_bounds.width(), + text_bounds.height(), + GetRootMenuItem()->GetDrawStringFlags()); + } + + if (icon_.width() > 0) { + gfx::Rect icon_bounds(kItemLeftMargin, + top_margin + (height() - top_margin - + bottom_margin - icon_.height()) / 2, + icon_.width(), + icon_.height()); + icon_bounds.set_x(MirroredLeftPointForRect(icon_bounds)); + canvas->DrawBitmapInt(icon_, icon_bounds.x(), icon_bounds.y()); + } + + if (HasSubmenu()) { + int state_id = IsEnabled() ? MSM_NORMAL : MSM_DISABLED; + RECT arrow_bounds = { + this->width() - item_right_margin + kLabelToArrowPadding, + 0, + 0, + height() + }; + arrow_bounds.right = arrow_bounds.left + arrow_width; + 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. + NativeTheme::MenuArrowDirection arrow_direction; + if (UILayoutIsRightToLeft()) + arrow_direction = NativeTheme::LEFT_POINTING_ARROW; + else + arrow_direction = NativeTheme::RIGHT_POINTING_ARROW; + + NativeTheme::instance()->PaintMenuArrow( + NativeTheme::MENU, dc, MENU_POPUPSUBMENU, state_id, &arrow_bounds, + arrow_direction, render_selection); + } + canvas->endPlatformPaint(); +} + +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() { + MenuItemView* root = GetRootMenuItem(); + return root && root->has_icons_ ? kItemTopMargin : kItemNoIconTopMargin; +} + +int MenuItemView::GetBottomMargin() { + MenuItemView* root = GetRootMenuItem(); + return root && root->has_icons_ ? + kItemBottomMargin : kItemNoIconBottomMargin; +} + +// MenuController ------------------------------------------------------------ + +// static +MenuController* MenuController::active_instance_ = NULL; + +// static +MenuController* MenuController::GetActiveInstance() { + return active_instance_; +} + +#ifdef DEBUG_MENU +static int instance_count = 0; +static int nested_depth = 0; +#endif + +MenuItemView* MenuController::Run(HWND parent, + MenuItemView* root, + const gfx::Rect& bounds, + MenuItemView::AnchorPosition position, + int* result_mouse_event_flags) { + exit_all_ = false; + possible_drag_ = 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(owner_ == parent); + } else { + showing_ = true; + } + + // Reset current state. + pending_state_ = State(); + state_ = State(); + 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); + } + pending_state_.anchor = position; + owner_ = parent; + + // Calculate the bounds of the monitor we'll show menus on. Do this once to + // avoid repeated system queries for the info. + POINT initial_loc = { bounds.x(), bounds.y() }; + HMONITOR monitor = MonitorFromPoint(initial_loc, MONITOR_DEFAULTTONEAREST); + if (monitor) { + MONITORINFO mi = {0}; + mi.cbSize = sizeof(mi); + GetMonitorInfo(monitor, &mi); + // Menus appear over the taskbar. + pending_state_.monitor_bounds = gfx::Rect(mi.rcMonitor); + } + + // Set the selection, which opens the initial menu. + SetSelection(root, true, true); + + 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; + } + +#ifdef DEBUG_MENU + nested_depth++; + DLOG(INFO) << " entering nested loop, depth=" << nested_depth; +#endif + + MessageLoopForUI* loop = MessageLoopForUI::current(); + if (MenuItemView::allow_task_nesting_during_run_) { + bool did_allow_task_nesting = loop->NestableTasksAllowed(); + loop->SetNestableTasksAllowed(true); + loop->Run(this); + loop->SetNestableTasksAllowed(did_allow_task_nesting); + } else { + loop->Run(this); + } + +#ifdef DEBUG_MENU + nested_depth--; + DLOG(INFO) << " exiting nested loop, depth=" << nested_depth; +#endif + + // Close any open menus. + SetSelection(NULL, false, true); + + 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 (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(); + + // Set exit_all_ to true, which makes sure all nested loops exit + // immediately. + exit_all_ = true; + } + + return result; +} + +void MenuController::SetSelection(MenuItemView* menu_item, + bool open_submenu, + bool update_immediately) { + 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(); + + // Notify the old path it isn't selected. + for (size_t i = paths_differ_at; i < current_size; ++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); + + pending_state_.item = menu_item; + pending_state_.submenu_open = open_submenu; + + // Stop timers. + StopShowTimer(); + StopCancelAllTimer(); + + if (update_immediately) + CommitPendingSelection(); + else + StartShowTimer(); +} + +void MenuController::Cancel(bool all) { + 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; + exit_all_ = all; + + // Hide windows immediately. + SetSelection(NULL, false, true); + + if (!blocking_run_) { + // If we didn't block the caller we need to notify the menu, which + // triggers deleting us. + DCHECK(selected); + showing_ = false; + selected->GetRootMenuItem()->DropMenuClosed(true); + // WARNING: the call to MenuClosed deletes us. + return; + } +} + +void MenuController::OnMousePressed(SubmenuView* source, + const MouseEvent& event) { +#ifdef DEBUG_MENU + DLOG(INFO) << "OnMousePressed source=" << source; +#endif + if (!blocking_run_) + return; + + MenuPart part = + GetMenuPartByScreenCoordinate(source, event.x(), event.y()); + 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. + RepostEvent(source, event); + + // And close. + Cancel(true); + return; + } + + bool open_submenu = false; + if (!part.menu) { + part.menu = source->GetMenuItem(); + open_submenu = true; + } else { + if (part.menu->GetDelegate()->CanDrag(part.menu)) { + possible_drag_ = true; + press_x_ = event.x(); + press_y_ = event.y(); + } + if (part.menu->HasSubmenu()) + open_submenu = true; + } + // On a press we immediately commit the selection, that way a submenu + // pops up immediately rather than after a delay. + SetSelection(part.menu, open_submenu, true); +} + +void MenuController::OnMouseDragged(SubmenuView* source, + const MouseEvent& event) { +#ifdef DEBUG_MENU + DLOG(INFO) << "OnMouseDragged source=" << source; +#endif + MenuPart part = + GetMenuPartByScreenCoordinate(source, event.x(), event.y()); + UpdateScrolling(part); + + if (!blocking_run_) + return; + + if (possible_drag_) { + if (View::ExceededDragThreshold(event.x() - press_x_, + event.y() - press_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_x_, press_y_); + View::ConvertPointToScreen(source->GetScrollViewContainer(), &press_loc); + View::ConvertPointToView(NULL, item, &press_loc); + gfx::Point drag_loc(event.location()); + View::ConvertPointToScreen(source->GetScrollViewContainer(), &drag_loc); + View::ConvertPointToView(NULL, item, &drag_loc); + ChromeCanvas canvas(item->width(), item->height(), false); + item->Paint(&canvas, true); + + scoped_refptr<OSExchangeData> data(new OSExchangeData); + item->GetDelegate()->WriteDragData(item, data.get()); + drag_utils::SetDragImageOnDataObject(canvas, item->width(), + item->height(), press_loc.x(), + press_loc.y(), data); + + scoped_refptr<BaseDragSource> drag_source(new BaseDragSource); + int drag_ops = item->GetDelegate()->GetDragOperations(item); + DWORD effects; + StopScrolling(); + DoDragDrop(data, drag_source, + DragDropTypes::DragOperationToDropEffect(drag_ops), + &effects); + if (GetActiveInstance() == this) { + if (showing_) { + // We're still showing, close all menus. + CloseAllNestedMenus(); + Cancel(true); + } // else case, drop was on us. + } // else case, someone canceled us, don't do anything + } + return; + } + if (part.type == MenuPart::MENU_ITEM) { + if (!part.menu) + part.menu = source->GetMenuItem(); + SetSelection(part.menu ? part.menu : state_.item, true, false); + } +} + +void MenuController::OnMouseReleased(SubmenuView* source, + const MouseEvent& event) { +#ifdef DEBUG_MENU + DLOG(INFO) << "OnMouseReleased source=" << source; +#endif + if (!blocking_run_) + return; + + DCHECK(state_.item); + possible_drag_ = false; + DCHECK(blocking_run_); + MenuPart part = + GetMenuPartByScreenCoordinate(source, event.x(), event.y()); + 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. + bool open_submenu = (state_.item == pending_state_.item && + state_.submenu_open); + SetSelection(pending_state_.item, open_submenu, true); + 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.x(), loc.y(), true)) + return; + } + + if (!part.is_scroll() && part.menu && !part.menu->HasSubmenu()) { + if (part.menu->GetDelegate()->IsTriggerableEvent(event)) { + Accept(part.menu, event.GetFlags()); + 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, true, true); + } +} + +void MenuController::OnMouseMoved(SubmenuView* source, + const MouseEvent& event) { +#ifdef DEBUG_MENU + DLOG(INFO) << "OnMouseMoved source=" << source; +#endif + if (showing_submenu_) + return; + + MenuPart part = + GetMenuPartByScreenCoordinate(source, event.x(), event.y()); + + UpdateScrolling(part); + + if (!blocking_run_) + return; + + if (part.type == MenuPart::MENU_ITEM && part.menu) { + SetSelection(part.menu, true, false); + } else if (!part.is_scroll() && pending_state_.item && + (!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(), true, false); + } +} + +void MenuController::OnMouseEntered(SubmenuView* source, + const MouseEvent& event) { + // MouseEntered is always followed by a mouse moved, so don't need to + // do anything here. +} + +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.x() == drop_x_ && + screen_loc.y() == drop_y_) { + return last_drop_operation_; + } + drop_x_ = screen_loc.x(); + drop_y_ = screen_loc.y(); + 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 = 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 if (menu_item_loc.y() < menu_item_height / 2) { + drop_position = MenuDelegate::DROP_BEFORE; + } else { + drop_position = 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 (menu_item->HasSubmenu()) { + // The menu has a submenu, schedule the submenu to open. + SetSelection(menu_item, true, false); + } else { + SetSelection(menu_item, false, false); + } + + if (drop_position == MenuDelegate::DROP_NONE || + drop_operation == DragDropTypes::DRAG_NONE) { + menu_item = NULL; + } + } else { + SetSelection(source->GetMenuItem(), true, false); + } + 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, false, true); + CloseAllNestedMenus(); + + // Set state such that we exit. + showing_ = false; + exit_all_ = true; + + if (!IsBlockingRun()) + item->GetRootMenuItem()->DropMenuClosed(false); + + // WARNING: the call to MenuClosed deletes us. + + // If over an empty menu item, drop occurs on the parent. + if (drop_target->GetID() == EmptyMenuMenuItem::kEmptyMenuItemViewID) + drop_target = drop_target->GetParentMenuItem(); + + 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(); +} + +// static +void MenuController::SetActiveInstance(MenuController* controller) { + active_instance_ = controller; +} + +bool MenuController::Dispatch(const MSG& msg) { + DCHECK(blocking_run_); + + if (exit_all_) { + // 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.x(), screen_loc.y(), 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: + return OnKeyDown(msg); + + case WM_CHAR: + return OnChar(msg); + + 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(true); + return false; + + default: + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + return !exit_all_; +} + +bool MenuController::OnKeyDown(const MSG& msg) { + DCHECK(blocking_run_); + + switch (msg.wParam) { + case VK_UP: + IncrementSelection(-1); + break; + + case VK_DOWN: + IncrementSelection(1); + break; + + // Handling of VK_RIGHT and VK_LEFT is different depending on the UI + // layout. + case VK_RIGHT: + if (l10n_util::TextDirection() == l10n_util::RIGHT_TO_LEFT) + CloseSubmenu(); + else + OpenSubmenuChangeSelectionIfCan(); + break; + + case VK_LEFT: + if (l10n_util::TextDirection() == l10n_util::RIGHT_TO_LEFT) + OpenSubmenuChangeSelectionIfCan(); + else + CloseSubmenu(); + break; + + case VK_RETURN: + if (pending_state_.item) { + if (pending_state_.item->HasSubmenu()) { + OpenSubmenuChangeSelectionIfCan(); + } else if (pending_state_.item->IsEnabled()) { + Accept(pending_state_.item, 0); + return false; + } + } + break; + + case VK_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(false); + return false; + } else { + CloseSubmenu(); + } + break; + + case VK_APPS: + break; + + default: + TranslateMessage(&msg); + break; + } + return true; +} + +bool MenuController::OnChar(const MSG& msg) { + DCHECK(blocking_run_); + + return !SelectByChar(static_cast<wchar_t>(msg.wParam)); +} + +MenuController::MenuController(bool blocking) + : blocking_run_(blocking), + showing_(false), + exit_all_(false), + did_capture_(false), + result_(NULL), + drop_target_(NULL), + owner_(NULL), + possible_drag_(false), + valid_drop_coordinates_(false), + showing_submenu_(false), + result_mouse_event_flags_(0) { +#ifdef DEBUG_MENU + instance_count++; + DLOG(INFO) << "created MC, count=" << instance_count; +#endif +} + +MenuController::~MenuController() { + DCHECK(!showing_); + StopShowTimer(); + StopCancelAllTimer(); +#ifdef DEBUG_MENU + instance_count--; + DLOG(INFO) << "destroyed MC, count=" << instance_count; +#endif +} + +void MenuController::Accept(MenuItemView* item, int mouse_event_flags) { + DCHECK(IsBlockingRun()); + result_ = item; + exit_all_ = true; + result_mouse_event_flags_ = mouse_event_flags; +} + +void MenuController::CloseAllNestedMenus() { + for (std::list<State>::iterator i = menu_stack_.begin(); + i != menu_stack_.end(); ++i) { + MenuItemView* item = i->item; + MenuItemView* last_item = item; + while (item) { + CloseMenu(item); + last_item = item; + item = item->GetParentMenuItem(); + } + i->submenu_open = false; + i->item = last_item; + } +} + +MenuItemView* MenuController::GetMenuItemAt(View* source, int x, int y) { + View* child_under_mouse = source->GetViewForPoint(gfx::Point(x, y)); + if (child_under_mouse && child_under_mouse->IsEnabled() && + child_under_mouse->GetID() == 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->GetViewForPoint(gfx::Point(x, y)); + if (child_under_mouse && + child_under_mouse->GetID() == EmptyMenuMenuItem::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->GetViewForPoint(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::GetMenuPartByScreenCoordinate( + SubmenuView* source, + int source_x, + int source_y) { + MenuPart part; + + gfx::Point screen_loc(source_x, source_y); + View::ConvertPointToScreen(source->GetScrollViewContainer(), &screen_loc); + + MenuItemView* item = state_.item; + while (item) { + if (item->HasSubmenu() && item->GetSubmenu()->IsShowing() && + GetMenuPartByScreenCoordinateImpl(item->GetSubmenu(), screen_loc, + &part)) { + return part; + } + item = item->GetParentMenuItem(); + } + + 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; + 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(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; + MenuItemView* item = state_.item; + while (item && !found) { + found = (item->HasSubmenu() && item->GetSubmenu()->IsShowing() && + item->GetSubmenu() == scroll_task_->submenu()); + item = item->GetParentMenuItem(); + } + 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; + } + + 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; + item->GetSubmenu()->ShowAt(owner_, bounds, do_capture); + showing_submenu_ = false; + did_capture_ = true; +} + +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(TimeDelta::FromMilliseconds(kShowDelay), this, + &MenuController::CommitPendingSelection); +} + +void MenuController::StopShowTimer() { + show_timer_.Stop(); +} + +void MenuController::StartCancelAllTimer() { + cancel_all_timer_.Start(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. This is some where between what IE and FF + // do. + pref.set_width(std::min(pref.width(), kMaxMenuWidth)); + 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. If the first location is above the + // half way point, 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 (y < (state_.monitor_bounds.y() + + state_.monitor_bounds.height() / 2)) { + pref.set_height(std::min(pref.height(), + state_.monitor_bounds.bottom() - y)); + } else { + pref.set_height(std::min(pref.height(), + state_.initial_bounds.y() - state_.monitor_bounds.y())); + y = state_.initial_bounds.y() - pref.height(); + } + } + } 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 = item->UILayoutIsRightToLeft(); + 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() - 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) { + if (!item) + return 0; + return MenuDepth(item->GetParentMenuItem()) + 1; +} + +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), false, false); + ScrollToVisible(item->GetSubmenu()->GetMenuItemAt(0)); + return; // return so else case can fall through. + } + } + if (item->GetParentMenuItem()) { + MenuItemView* parent = item->GetParentMenuItem(); + int parent_count = parent->GetSubmenu()->GetMenuItemCount(); + if (parent_count > 1) { + for (int i = 0; i < parent_count; ++i) { + if (parent->GetSubmenu()->GetMenuItemAt(i) == item) { + int next_index = (i + delta + parent_count) % parent_count; + ScrollToVisible(parent->GetSubmenu()->GetMenuItemAt(next_index)); + SetSelection(parent->GetSubmenu()->GetMenuItemAt(next_index), false, + false); + break; + } + } + } + } +} + +void MenuController::OpenSubmenuChangeSelectionIfCan() { + MenuItemView* item = pending_state_.item; + if (item->HasSubmenu()) { + if (item->GetSubmenu()->GetMenuItemCount() > 0) { + SetSelection(item->GetSubmenu()->GetMenuItemAt(0), false, true); + } else { + // No menu items, just show the sub-menu. + SetSelection(item, true, true); + } + } +} + +void MenuController::CloseSubmenu() { + MenuItemView* item = state_.item; + DCHECK(item); + if (!item->GetParentMenuItem()) + return; + if (item->HasSubmenu() && item->GetSubmenu()->IsShowing()) { + SetSelection(item, false, true); + } else if (item->GetParentMenuItem()->GetParentMenuItem()) { + SetSelection(item->GetParentMenuItem(), false, true); + } +} + +bool MenuController::IsMenuWindow(MenuItemView* item, HWND window) { + if (!item) + return false; + return ((item->HasSubmenu() && item->GetSubmenu()->IsShowing() && + item->GetSubmenu()->GetWidget()->GetNativeView() == window) || + IsMenuWindow(item->GetParentMenuItem(), window)); +} + +bool MenuController::SelectByChar(wchar_t character) { + wchar_t char_array[1] = { character }; + wchar_t key = l10n_util::ToLower(char_array)[0]; + MenuItemView* item = pending_state_.item; + if (!item->HasSubmenu() || !item->GetSubmenu()->IsShowing()) + item = item->GetParentMenuItem(); + DCHECK(item); + DCHECK(item->HasSubmenu()); + SubmenuView* submenu = item->GetSubmenu(); + DCHECK(submenu); + int menu_item_count = submenu->GetMenuItemCount(); + if (!menu_item_count) + return false; + for (int i = 0; i < menu_item_count; ++i) { + MenuItemView* child = submenu->GetMenuItemAt(i); + if (child->GetMnemonic() == key && child->IsEnabled()) { + Accept(child, 0); + return true; + } + } + + // No matching mnemonic, search through items that don't have mnemonic + // based on first character of the title. + int first_match = -1; + bool has_multiple = false; + int next_match = -1; + int index_of_item = -1; + for (int i = 0; i < menu_item_count; ++i) { + MenuItemView* child = submenu->GetMenuItemAt(i); + if (!child->GetMnemonic() && child->IsEnabled()) { + std::wstring lower_title = l10n_util::ToLower(child->GetTitle()); + if (child == pending_state_.item) + index_of_item = i; + if (lower_title.length() && lower_title[0] == key) { + if (first_match == -1) + first_match = i; + else + has_multiple = true; + if (next_match == -1 && index_of_item != -1 && i > index_of_item) + next_match = i; + } + } + } + if (first_match != -1) { + if (!has_multiple) { + if (submenu->GetMenuItemAt(first_match)->HasSubmenu()) { + SetSelection(submenu->GetMenuItemAt(first_match), true, false); + } else { + Accept(submenu->GetMenuItemAt(first_match), 0); + return true; + } + } else if (index_of_item == -1 || next_match == -1) { + SetSelection(submenu->GetMenuItemAt(first_match), false, false); + } else { + SetSelection(submenu->GetMenuItemAt(next_match), false, false); + } + } + return false; +} + +void MenuController::RepostEvent(SubmenuView* source, + const MouseEvent& event) { + gfx::Point screen_loc(event.location()); + View::ConvertPointToScreen(source->GetScrollViewContainer(), &screen_loc); + HWND window = WindowFromPoint(screen_loc.ToPOINT()); + if (window) { +#ifdef DEBUG_MENU + DLOG(INFO) << "RepostEvent on press"; +#endif + + // Release the capture. + SubmenuView* submenu = state_.item->GetRootMenuItem()->GetSubmenu(); + submenu->ReleaseCapture(); + + if (submenu->host() && submenu->host()->GetNativeView() && + GetWindowThreadProcessId(submenu->host()->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.GetWindowsFlags(), + MAKELPARAM(window_x, window_y)); + } else { + PostMessage(window, event_type, nc_hit_result, + MAKELPARAM(screen_loc.x(), screen_loc.y())); + } + } + } +} + +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); +} + +} // namespace views diff --git a/views/controls/menu/chrome_menu.h b/views/controls/menu/chrome_menu.h new file mode 100644 index 0000000..02caaed --- /dev/null +++ b/views/controls/menu/chrome_menu.h @@ -0,0 +1,948 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_MENU_CHROME_MENU_H_ +#define VIEWS_CONTROLS_MENU_CHROME_MENU_H_ + +#include <list> +#include <vector> + +#include "app/drag_drop_types.h" +#include "app/gfx/chrome_font.h" +#include "base/gfx/point.h" +#include "base/gfx/rect.h" +#include "base/message_loop.h" +#include "base/task.h" +#include "skia/include/SkBitmap.h" +#include "views/controls/menu/controller.h" +#include "views/view.h" + +namespace views { + +class WidgetWin; +class MenuController; +class MenuItemView; +class SubmenuView; + +namespace { +class MenuHost; +class MenuHostRootView; +class MenuScrollTask; +class MenuScrollViewContainer; +} + +// MenuDelegate -------------------------------------------------------------- + +// Delegate for the menu. + +class MenuDelegate : Controller { + public: + // Used during drag and drop to indicate where the drop indicator should + // be rendered. + enum DropPosition { + // 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 + }; + + // Whether or not an item should be shown as checked. + // TODO(sky): need checked support. + virtual bool IsItemChecked(int id) const { + return false; + } + + // The string shown for the menu item. This is only invoked when an item is + // added with an empty label. + virtual std::wstring GetLabel(int id) const { + return std::wstring(); + } + + // 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 x/y is the recommended + // location to display the content menu at. In either case, x/y is in + // screen coordinates. + // Returns true if a context menu was displayed, otherwise false + virtual bool ShowContextMenu(MenuItemView* source, + int id, + int x, + int y, + bool is_mouse_gesture) { + return false; + } + + // Controller + virtual bool SupportsCommand(int id) const { + return true; + } + virtual bool IsCommandEnabled(int id) const { + return true; + } + virtual bool GetContextualLabel(int id, std::wstring* out) const { + return false; + } + virtual void ExecuteCommand(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) { + ExecuteCommand(id); + } + + // 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(const MouseEvent& e) { + return e.IsLeftMouseButton() || e.IsRightMouseButton(); + } + + // 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) { + return false; + } + + // 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 DragDropTypes::DRAG_NONE. + virtual int GetDropOperation(MenuItemView* item, + const DropTargetEvent& event, + DropPosition* position) { + NOTREACHED() << "If you override CanDrop, you need to override this too"; + return DragDropTypes::DRAG_NONE; + } + + // 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 DragDropTypes::DRAG_NONE. + // + // menu indicates the menu the drop occurred on. + virtual int OnPerformDrop(MenuItemView* menu, + DropPosition position, + const DropTargetEvent& event) { + NOTREACHED() << "If you override CanDrop, you need to override this too"; + return DragDropTypes::DRAG_NONE; + } + + // Invoked to determine if it is possible for the user to drag the specified + // menu item. + virtual bool CanDrag(MenuItemView* menu) { + return false; + } + + // Invoked to write the data for a drag operation to data. sender is the + // MenuItemView being dragged. + virtual void WriteDragData(MenuItemView* sender, OSExchangeData* data) { + NOTREACHED() << "If you override CanDrag, you must override this too."; + } + + // Invoked to determine the drag operations for a drag session of sender. + // See DragDropTypes for possible values. + virtual int GetDragOperations(MenuItemView* sender) { + NOTREACHED() << "If you override CanDrag, you must override this too."; + return 0; + } + + // 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) { + } +}; + +// 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 normally NOT want you want, rather add other child Views +// to the submenu of the MenuItemView. +// +// There are two ways to show a MenuItemView: +// 1. Use RunMenuAt. This blocks the caller, executing the selected command +// on success. +// 2. Use RunMenuForDropAt. This is intended for use during a drop session +// and does NOT block the caller. Instead the delegate is notified when the +// menu closes via the DropMenuClosed method. + +class MenuItemView : public View { + friend class MenuController; + + public: + // ID used to identify menu items. + static const int kMenuItemViewID; + + // If true SetNestableTasksAllowed(true) is invoked before MessageLoop::Run + // is invoked. This is only useful for testing and defaults to false. + static bool allow_task_nesting_during_run_; + + // Different types of menu items. + enum Type { + NORMAL, + SUBMENU, + CHECKBOX, + RADIO, + SEPARATOR + }; + + // Where the menu should be anchored to. + enum AnchorPosition { + TOPLEFT, + TOPRIGHT + }; + + // 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); + + virtual ~MenuItemView(); + + // Run methods. See description above class for details. Both Run methods take + // a rectangle, which is used to position the menu. |has_mnemonics| indicates + // whether the items have mnemonics. Mnemonics are identified by way of the + // character following the '&'. + void RunMenuAt(HWND parent, + const gfx::Rect& bounds, + AnchorPosition anchor, + bool has_mnemonics); + void RunMenuForDropAt(HWND parent, + const gfx::Rect& bounds, + AnchorPosition anchor); + + // Hides and cancels the menu. This does nothing if the menu is not open. + void Cancel(); + + // 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 std::wstring& label, + Type type) { + AppendMenuItemInternal(item_id, label, SkBitmap(), type); + } + + // Append a submenu to this menu. + // The returned pointer is owned by this menu. + MenuItemView* AppendSubMenu(int item_id, + const std::wstring& label) { + return AppendMenuItemInternal(item_id, label, SkBitmap(), SUBMENU); + } + + // Append a submenu with an icon to this menu. + // The returned pointer is owned by this menu. + MenuItemView* AppendSubMenuWithIcon(int item_id, + const std::wstring& label, + const SkBitmap& icon) { + return AppendMenuItemInternal(item_id, label, icon, SUBMENU); + } + + // This is a convenience for standard text label menu items where the label + // is provided with this call. + void AppendMenuItemWithLabel(int item_id, + const std::wstring& label) { + AppendMenuItem(item_id, label, NORMAL); + } + + // This is a convenience for text label menu items where the label is + // provided by the delegate. + void AppendDelegateMenuItem(int item_id) { + AppendMenuItem(item_id, std::wstring(), NORMAL); + } + + // Adds a separator to this menu + void AppendSeparator() { + AppendMenuItemInternal(0, std::wstring(), SkBitmap(), SEPARATOR); + } + + // 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 std::wstring& label, + const SkBitmap& icon) { + AppendMenuItemInternal(item_id, label, icon, NORMAL); + } + + // 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 { return (submenu_ != NULL); } + + // Returns the view containing child menu items. + virtual SubmenuView* GetSubmenu() const { return submenu_; } + + // Returns the parent menu item. + MenuItemView* GetParentMenuItem() const { return parent_menu_item_; } + + // Sets the font. + void SetFont(const ChromeFont& font) { font_ = font; } + + // Sets the title + void SetTitle(const std::wstring& title) { + title_ = title; + } + + // Returns the title. + const std::wstring& GetTitle() const { return title_; } + + // 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 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 Paint(ChromeCanvas* canvas); + + // Returns the preferred size of this item. + virtual gfx::Size GetPreferredSize(); + + // Returns the object responsible for controlling showing the menu. + MenuController* GetMenuController(); + + // Returns the delegate. This returns the delegate of the root menu item. + MenuDelegate* GetDelegate(); + + // Returns the root parent, or this if this has no parent. + MenuItemView* GetRootMenuItem(); + + // Returns the mnemonic for this MenuItemView, or 0 if this MenuItemView + // doesn't have a mnemonic. + wchar_t 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; + } + + protected: + // Creates a MenuItemView. This is used by the various AddXXX methods. + MenuItemView(MenuItemView* parent, int command, Type type); + + private: + // Called by the two constructors to initialize this menu item. + void Init(MenuItemView* parent, + int command, + MenuItemView::Type type, + MenuDelegate* delegate); + + // All the AddXXX methods funnel into this. + MenuItemView* AppendMenuItemInternal(int item_id, + const std::wstring& label, + const SkBitmap& icon, + Type type); + + // Returns the descendant with the specified command. + MenuItemView* GetDescendantByID(int id); + + // Invoked by the MenuController when the menu closes as the result of + // drag and drop run. + void DropMenuClosed(bool notify_delegate); + + // The RunXXX methods call into this to set up the necessary state before + // running. + void PrepareForRun(bool has_mnemonics); + + // Returns the flags passed to DrawStringInt. + int GetDrawStringFlags(); + + // If this menu item has no children a child is added showing it has no + // children. Otherwise AddEmtpyMenuIfNecessary 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(RECT* rect) const; + + // Actual paint implementation. If for_drag is true, portions of the menu + // are not rendered. + void Paint(ChromeCanvas* canvas, bool for_drag); + + // Destroys the window used to display this menu and recursively destroys + // the windows used to display all descendants. + void DestroyAllMenuHosts(); + + // Returns the various margins. + int GetTopMargin(); + int GetBottomMargin(); + + // 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_; + + // Returns 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_; + + // Font. + ChromeFont font_; + + // Title. + std::wstring title_; + + // Icon. + SkBitmap icon_; + + // Does the title have a mnemonic? + bool has_mnemonics_; + + bool has_icons_; + + DISALLOW_EVIL_CONSTRUCTORS(MenuItemView); +}; + +// SubmenuView ---------------------------------------------------------------- + +// 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 WidgetWin) 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 SubmenuView : public View { + public: + // Creates a SubmenuView for the specified menu item. + explicit SubmenuView(MenuItemView* parent); + ~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(); + virtual gfx::Size GetPreferredSize(); + + // View method. Overriden to schedule a paint. We do this so that when + // scrolling occurs, everything is repainted correctly. + virtual void DidChangeBounds(const gfx::Rect& previous, + const gfx::Rect& current); + + // Painting. + void PaintChildren(ChromeCanvas* canvas); + + // Drag and drop methods. These are forwarded to the MenuController. + virtual bool CanDrop(const OSExchangeData& data); + virtual void OnDragEntered(const DropTargetEvent& event); + virtual int OnDragUpdated(const DropTargetEvent& event); + virtual void OnDragExited(); + virtual int OnPerformDrop(const DropTargetEvent& event); + + // Scrolls on menu item boundaries. + virtual bool OnMouseWheel(const MouseWheelEvent& e); + + // 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(HWND parent, const gfx::Rect& bounds, bool do_capture); + + // 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(); + + // Returns the parent menu item we're showing children for. + MenuItemView* GetMenuItem() const { return parent_menu_item_; } + + // Overriden to return true. This prevents tab from doing anything. + virtual bool CanProcessTabKeyEvents() { return true; } + + // 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(); + + // Returns the host of the menu. Returns NULL if not showing. + MenuHost* host() const { return host_; } + + private: + // Paints the drop indicator. This is only invoked if item is non-NULL and + // position is not DROP_NONE. + void PaintDropIndicator(ChromeCanvas* 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_; + + // WidgetWin subclass used to show the children. + 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_; + + DISALLOW_EVIL_CONSTRUCTORS(SubmenuView); +}; + +// MenuController ------------------------------------------------------------- + +// MenuController manages showing, selecting and drag/drop for menus. +// All relevant events are forwarded to the MenuController from SubmenuView +// and MenuHost. + +class MenuController : public MessageLoopForUI::Dispatcher { + public: + friend class MenuHostRootView; + friend class MenuItemView; + friend class MenuScrollTask; + + // 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(HWND parent, + MenuItemView* root, + const gfx::Rect& bounds, + MenuItemView::AnchorPosition position, + int* mouse_event_flags); + + // Whether or not Run blocks. + bool IsBlockingRun() const { return blocking_run_; } + + // Sets the selection to menu_item, a value of NULL unselects everything. + // If open_submenu is true and menu_item has a submenu, the submenu is shown. + // If update_immediately is true, submenus are opened immediately, otherwise + // submenus are only opened after a timer fires. + // + // Internally this updates pending_state_ immediatley, and if + // update_immediately is true, CommitPendingSelection is invoked to + // show/hide submenus and update state_. + void SetSelection(MenuItemView* menu_item, + bool open_submenu, + bool update_immediately); + + // Cancels the current Run. If all is true, any nested loops are canceled + // as well. This immediately hides all menus. + void Cancel(bool all); + + // An alternative to Cancel(true) that can be used with a OneShotTimer. + void CancelAll() { return Cancel(true); } + + // 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); + 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); + + private: + // Tracks selection information. + struct State { + State() : item(NULL), submenu_open(false) {} + + // 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 GetMenuPartByScreenCoordinate 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), 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. + MenuItemView* menu; + + // If type is SCROLL_*, this is the submenu the mouse is over. + SubmenuView* submenu; + }; + + // Sets the active MenuController. + static void SetActiveInstance(MenuController* controller); + + // 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); + + // Key processing. The return value of these is returned from Dispatch. + // In other words, if these return false (which they do if escape was + // pressed, or a matching mnemonic was found) the message loop returns. + bool OnKeyDown(const MSG& msg); + bool OnChar(const MSG& msg); + + // Creates a MenuController. If blocking is true, Run blocks the caller + explicit MenuController(bool blocking); + + ~MenuController(); + + // 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); + + // 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. + MenuPart GetMenuPartByScreenCoordinate(SubmenuView* source, + int source_x, + int source_y); + + // 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); + + // 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); + + // 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 true if window is the window used to show item, or any of + // items ancestors. + bool IsMenuWindow(MenuItemView* item, HWND window); + + // 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(wchar_t key); + + // 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); + + // 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(); + + // 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_; + + // If true, all nested run loops should be exited. + bool exit_all_; + + // 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 mousee 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. + HWND owner_; + + // Indicates a possible drag operation. + bool possible_drag_; + + // Location the mouse was pressed at. Used to detect d&d. + int press_x_; + int press_y_; + + // 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_; + int drop_x_; + int drop_y_; + 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_; + + DISALLOW_EVIL_CONSTRUCTORS(MenuController); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_MENU_CHROME_MENU_H_ diff --git a/views/controls/menu/controller.h b/views/controls/menu/controller.h new file mode 100644 index 0000000..6a693cc --- /dev/null +++ b/views/controls/menu/controller.h @@ -0,0 +1,33 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_MENU_CONTROLLER_H_ +#define VIEWS_CONTROLS_MENU_CONTROLLER_H_ + +#include <string> + +// TODO(beng): remove this interface and fold it into MenuDelegate. + +class Controller { + public: + virtual ~Controller() { } + + // Whether or not a command is supported by this controller. + virtual bool SupportsCommand(int id) const = 0; + + // Whether or not a command is enabled. + virtual bool IsCommandEnabled(int id) const = 0; + + // Assign the provided string with a contextual label. Returns true if a + // contextual label exists and false otherwise. This method can be used when + // implementing a menu or button that needs to have a different label + // depending on the context. If this method returns false, the default + // label used when creating the button or menu is used. + virtual bool GetContextualLabel(int id, std::wstring* out) const = 0; + + // Executes a command. + virtual void ExecuteCommand(int id) = 0; +}; + +#endif // VIEWS_CONTROLS_MENU_CONTROLLER_H_ diff --git a/views/controls/menu/menu.cc b/views/controls/menu/menu.cc new file mode 100644 index 0000000..1597da5 --- /dev/null +++ b/views/controls/menu/menu.cc @@ -0,0 +1,626 @@ +// Copyright (c) 2006-2008 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 "views/controls/menu/menu.h" + +#include <atlbase.h> +#include <atlapp.h> +#include <atlwin.h> +#include <atlcrack.h> +#include <atlframe.h> +#include <atlmisc.h> +#include <string> + +#include "app/gfx/chrome_canvas.h" +#include "app/gfx/chrome_font.h" +#include "app/l10n_util.h" +#include "app/l10n_util_win.h" +#include "base/gfx/rect.h" +#include "base/logging.h" +#include "base/stl_util-inl.h" +#include "base/string_util.h" +#include "views/accelerator.h" + +const SkBitmap* Menu::Delegate::kEmptyIcon = 0; + +// 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; + +// Current active MenuHostWindow. If NULL, no menu is active. +static MenuHostWindow* active_host_window = NULL; + +// The data of menu items needed to display. +struct Menu::ItemData { + std::wstring 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 CWindowImpl<MenuHostWindow, CWindow, + CWinTraits<WS_CHILD>> { + public: + MenuHostWindow(Menu* 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(); + Create(parent_window, gfx::Rect().ToRECT(), 0, 0, extended_style); + } + + ~MenuHostWindow() { + DestroyWindow(); + } + + DECLARE_FRAME_WND_CLASS(L"MenuHostWindow", NULL); + BEGIN_MSG_MAP(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, loc.x, loc.y, true); + } + + void OnMeasureItem(WPARAM w_param, MEASUREITEMSTRUCT* lpmis) { + Menu::ItemData* data = reinterpret_cast<Menu::ItemData*>(lpmis->itemData); + if (data != NULL) { + ChromeFont 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') != std::wstring::npos) + lpmis->itemWidth += font.GetStringWidth(L" "); + lpmis->itemHeight = font.height() + 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) { + Menu::ItemData* data = + reinterpret_cast<Menu::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; + ChromeFont font; + HGDIOBJ old_font = static_cast<HFONT>(SelectObject(hDC, font.hfont())); + int fontsize = font.FontSize(); + + // 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. + std::wstring label = data->label; + std::wstring accel; + std::wstring::size_type tab_pos = label.find(L'\t'); + if (tab_pos != std::wstring::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) { + ChromeCanvas canvas(data->icon.width(), data->icon.height(), false); + canvas.drawColor(SK_ColorBLACK, SkPorterDuff::kClear_Mode); + canvas.DrawBitmapInt(data->icon, 0, 0); + canvas.getTopPlatformDevice().drawToHDC(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(Menu* 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<Menu*>::iterator i = menu->submenus_.begin(); + i != menu->submenus_.end(); ++i) { + if (FindMenuIDByLocation(*i, loc, id)) + return true; + } + } + return false; + } + + // The menu that created us. + Menu* menu_; + + DISALLOW_EVIL_CONSTRUCTORS(MenuHostWindow); +}; + +} // namespace + +bool Menu::Delegate::IsRightToLeftUILayout() const { + return l10n_util::GetTextDirection() == l10n_util::RIGHT_TO_LEFT; +} + +const SkBitmap& Menu::Delegate::GetEmptyIcon() const { + if (kEmptyIcon == NULL) + kEmptyIcon = new SkBitmap(); + return *kEmptyIcon; +} + +Menu::Menu(Delegate* delegate, AnchorPoint anchor, HWND owner) + : delegate_(delegate), + menu_(CreatePopupMenu()), + anchor_(anchor), + owner_(owner), + is_menu_visible_(false), + owner_draw_(l10n_util::NeedOverrideDefaultUIFont(NULL, NULL)) { + DCHECK(delegate_); +} + +Menu::Menu(Menu* parent) + : delegate_(parent->delegate_), + menu_(CreatePopupMenu()), + anchor_(parent->anchor_), + owner_(parent->owner_), + is_menu_visible_(false), + owner_draw_(parent->owner_draw_) { +} + +Menu::Menu(HMENU hmenu) + : delegate_(NULL), + menu_(hmenu), + anchor_(TOPLEFT), + owner_(NULL), + is_menu_visible_(false), + owner_draw_(false) { + DCHECK(menu_); +} + +Menu::~Menu() { + STLDeleteContainerPointers(submenus_.begin(), submenus_.end()); + STLDeleteContainerPointers(item_data_.begin(), item_data_.end()); + DestroyMenu(menu_); +} + +UINT Menu::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; +} + +void Menu::AddMenuItemInternal(int index, + int item_id, + const std::wstring& label, + const SkBitmap& icon, + HMENU submenu, + MenuItemType type) { + DCHECK(type != SEPARATOR) << "Call AddSeparator instead!"; + + 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::GetString(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. + Menu::ItemData* data = new ItemData; + item_data_.push_back(data); + data->submenu = submenu != NULL; + + std::wstring actual_label(label.empty() ? + delegate_->GetLabel(item_id) : label); + + // Find out if there is a shortcut we need to append to the label. + views::Accelerator accelerator(0, 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); +} + +void Menu::AppendMenuItem(int item_id, + const std::wstring& label, + MenuItemType type) { + AddMenuItem(-1, item_id, label, type); +} + +void Menu::AddMenuItem(int index, + int item_id, + const std::wstring& label, + MenuItemType type) { + if (type == SEPARATOR) + AddSeparator(index); + else + AddMenuItemInternal(index, item_id, label, SkBitmap(), NULL, type); +} + +Menu* Menu::AppendSubMenu(int item_id, const std::wstring& label) { + return AddSubMenu(-1, item_id, label); +} + +Menu* Menu::AddSubMenu(int index, int item_id, const std::wstring& label) { + return AddSubMenuWithIcon(index, item_id, label, SkBitmap()); +} + +Menu* Menu::AppendSubMenuWithIcon(int item_id, + const std::wstring& label, + const SkBitmap& icon) { + return AddSubMenuWithIcon(-1, item_id, label, icon); +} + +Menu* Menu::AddSubMenuWithIcon(int index, + int item_id, + const std::wstring& label, + const SkBitmap& icon) { + if (!owner_draw_ && icon.width() != 0 && icon.height() != 0) + owner_draw_ = true; + + Menu* submenu = new Menu(this); + submenus_.push_back(submenu); + AddMenuItemInternal(index, item_id, label, icon, submenu->menu_, NORMAL); + return submenu; +} + +void Menu::AppendMenuItemWithLabel(int item_id, const std::wstring& label) { + AddMenuItemWithLabel(-1, item_id, label); +} + +void Menu::AddMenuItemWithLabel(int index, int item_id, + const std::wstring& 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, std::wstring(), Menu::NORMAL); +} + +void Menu::AppendSeparator() { + AddSeparator(-1); +} + +void Menu::AddSeparator(int index) { + MENUITEMINFO mii; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_FTYPE; + mii.fType = MFT_SEPARATOR; + InsertMenuItem(menu_, index, TRUE, &mii); +} + +void Menu::AppendMenuItemWithIcon(int item_id, + const std::wstring& label, + const SkBitmap& icon) { + AddMenuItemWithIcon(-1, item_id, label, icon); +} + +void Menu::AddMenuItemWithIcon(int index, + int item_id, + const std::wstring& label, + const SkBitmap& icon) { + if (!owner_draw_) + owner_draw_ = true; + AddMenuItemInternal(index, item_id, label, icon, NULL, Menu::NORMAL); +} + +void Menu::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 Menu::EnableMenuItemAt(int index, bool enabled) { + UINT enable_flags = enabled ? MF_ENABLED : MF_DISABLED | MF_GRAYED; + EnableMenuItem(menu_, index, MF_BYPOSITION | enable_flags); +} + +void Menu::SetMenuLabel(int item_id, const std::wstring& 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); +} + +DWORD Menu::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; +} + +bool Menu::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 Menu::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 + std::wstring 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(); +} + +void Menu::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->m_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 Menu::Cancel() { + DCHECK(is_menu_visible_); + EndMenu(); +} + +int Menu::ItemCount() { + return GetMenuItemCount(menu_); +} diff --git a/views/controls/menu/menu.h b/views/controls/menu/menu.h new file mode 100644 index 0000000..0be9126 --- /dev/null +++ b/views/controls/menu/menu.h @@ -0,0 +1,355 @@ +// Copyright (c) 2006-2008 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 CONTROLS_MENU_VIEWS_MENU_H_ +#define CONTROLS_MENU_VIEWS_MENU_H_ + +#include <windows.h> + +#include <vector> + +#include "base/basictypes.h" +#include "views/controls/menu/controller.h" + +class SkBitmap; + +namespace { +class MenuHostWindow; +} + +namespace views { +class Accelerator; +} + +/////////////////////////////////////////////////////////////////////////////// +// +// Menu class +// +// A wrapper around a Win32 HMENU handle that provides convenient APIs for +// menu construction, display and subsequent command execution. +// +/////////////////////////////////////////////////////////////////////////////// +class Menu { + friend class MenuHostWindow; + + public: + ///////////////////////////////////////////////////////////////////////////// + // + // Delegate Interface + // + // Classes implement this interface to tell the menu system more about each + // item as it is created. + // + ///////////////////////////////////////////////////////////////////////////// + class Delegate : public Controller { + 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 std::wstring GetLabel(int id) const { + return std::wstring(); + } + + // 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, views::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 x/y is the recommended + // location to display the content menu at. In either case, x/y is in + // screen coordinates. + virtual void ShowContextMenu(Menu* source, + int id, + int x, + int y, + 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, std::wstring* out) const { + return false; + } + virtual void ExecuteCommand(int id) { + } + + protected: + // Returns an empty icon. Will initialize kEmptyIcon if it hasn't been + // initialized. + const SkBitmap& GetEmptyIcon() const; + + private: + // Will be initialized to an icon of 0 width and 0 height when first using. + // An empty icon means we don't need to draw it. + static const SkBitmap* kEmptyIcon; + }; + + // This class is a helper that simply wraps a controller and forwards all + // state and execution actions to it. Use this when you're not defining your + // own custom delegate, but just hooking a context menu to some existing + // controller elsewhere. + class BaseControllerDelegate : public Delegate { + public: + explicit BaseControllerDelegate(Controller* wrapped) + : controller_(wrapped) { + } + + // Overridden from Menu::Delegate + virtual bool SupportsCommand(int id) const { + return controller_->SupportsCommand(id); + } + virtual bool IsCommandEnabled(int id) const { + return controller_->IsCommandEnabled(id); + } + virtual void ExecuteCommand(int id) { + controller_->ExecuteCommand(id); + } + virtual bool GetContextualLabel(int id, std::wstring* out) const { + return controller_->GetContextualLabel(id, out); + } + + private: + // The internal controller that we wrap to forward state and execution + // actions to. + Controller* controller_; + + DISALLOW_COPY_AND_ASSIGN(BaseControllerDelegate); + }; + + // 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, 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 Menu(HMENU hmenu); + virtual ~Menu(); + + void set_delegate(Delegate* delegate) { delegate_ = delegate; } + + // 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 std::wstring& label, + MenuItemType type); + void AddMenuItem(int index, + int item_id, + const std::wstring& label, + MenuItemType type); + + // Append a submenu to this menu. + // The returned pointer is owned by this menu. + Menu* AppendSubMenu(int item_id, + const std::wstring& label); + Menu* AddSubMenu(int index, int item_id, const std::wstring& 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 std::wstring& label, + const SkBitmap& icon); + Menu* AddSubMenuWithIcon(int index, + int item_id, + const std::wstring& label, + const SkBitmap& icon); + + // This is a convenience for standard text label menu items where the label + // is provided with this call. + void AppendMenuItemWithLabel(int item_id, const std::wstring& label); + void AddMenuItemWithLabel(int index, int item_id, const std::wstring& 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(); + void AddSeparator(int index); + + // 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 std::wstring& label, + const SkBitmap& icon); + void AddMenuItemWithIcon(int index, + int item_id, + const std::wstring& label, + const SkBitmap& icon); + + // Enables or disables the item with the specified id. + void EnableMenuItemByID(int item_id, bool enabled); + void EnableMenuItemAt(int index, bool enabled); + + // Sets menu label at specified index. + void SetMenuLabel(int item_id, const std::wstring& label); + + // 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. + bool SetIcon(const SkBitmap& icon, int item_id); + + // 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. + void RunMenuAt(int x, int y); + + // Cancels the menu. + virtual void Cancel(); + + // Returns the number of menu items. + int ItemCount(); + + protected: + // The delegate that is being used to get information about the presentation. + Delegate* delegate_; + + private: + // The data of menu items needed to display. + struct ItemData; + + explicit Menu(Menu* parent); + + void AddMenuItemInternal(int index, + int item_id, + const std::wstring& label, + const SkBitmap& icon, + HMENU submenu, + MenuItemType type); + + // 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; + + // Gets the Win32 TPM alignment flags for the specified AnchorPoint. + DWORD GetTPMAlignFlags() 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<std::wstring> 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_; + + // How this popup menu should be aligned relative to the point it is run at. + AnchorPoint anchor_; + + // 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<Menu*> submenus_; + + // Whether the menu is visible. + bool is_menu_visible_; + + DISALLOW_COPY_AND_ASSIGN(Menu); +}; + +#endif // CONTROLS_MENU_VIEWS_MENU_H_ diff --git a/views/controls/menu/view_menu_delegate.h b/views/controls/menu/view_menu_delegate.h new file mode 100644 index 0000000..bb72ed3 --- /dev/null +++ b/views/controls/menu/view_menu_delegate.h @@ -0,0 +1,34 @@ +// Copyright (c) 2006-2008 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 VIEWS_CONTROLS_MENU_VIEW_MENU_DELEGATE_H_ +#define VIEWS_CONTROLS_MENU_VIEW_MENU_DELEGATE_H_ + +#include "base/gfx/native_widget_types.h" + +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 CPoint& pt, + gfx::NativeView hwnd) = 0; +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_MENU_VIEW_MENU_DELEGATE_H_ |