menu_controller.cc revision 868fa2fe829687343ffae624259930155e16dbd8
1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "ui/views/controls/menu/menu_controller.h" 6 7#include "base/i18n/case_conversion.h" 8#include "base/i18n/rtl.h" 9#include "base/run_loop.h" 10#include "base/strings/utf_string_conversions.h" 11#include "base/time.h" 12#include "ui/base/dragdrop/drag_utils.h" 13#include "ui/base/dragdrop/os_exchange_data.h" 14#include "ui/base/events/event_constants.h" 15#include "ui/base/events/event_utils.h" 16#include "ui/base/keycodes/keyboard_codes.h" 17#include "ui/base/l10n/l10n_util.h" 18#include "ui/gfx/canvas.h" 19#include "ui/gfx/screen.h" 20#include "ui/native_theme/native_theme.h" 21#include "ui/views/controls/button/menu_button.h" 22#include "ui/views/controls/menu/menu_config.h" 23#include "ui/views/controls/menu/menu_controller_delegate.h" 24#include "ui/views/controls/menu/menu_scroll_view_container.h" 25#include "ui/views/controls/menu/submenu_view.h" 26#include "ui/views/drag_utils.h" 27#include "ui/views/view_constants.h" 28#include "ui/views/views_delegate.h" 29#include "ui/views/widget/root_view.h" 30#include "ui/views/widget/widget.h" 31 32#if defined(USE_AURA) 33#include "ui/aura/env.h" 34#include "ui/aura/root_window.h" 35#include "ui/aura/window.h" 36#endif 37 38#if defined(OS_WIN) 39#include "ui/views/win/hwnd_util.h" 40#endif 41 42#if defined(USE_X11) 43#include <X11/Xlib.h> 44#endif 45 46using base::Time; 47using base::TimeDelta; 48using ui::OSExchangeData; 49 50// Period of the scroll timer (in milliseconds). 51static const int kScrollTimerMS = 30; 52 53// Amount of time from when the drop exits the menu and the menu is hidden. 54static const int kCloseOnExitTime = 1200; 55 56// If a context menu is invoked by touch, we shift the menu by this offset so 57// that the finger does not obscure the menu. 58static const int kCenteredContextMenuYOffset = -15; 59 60namespace views { 61 62namespace { 63 64// When showing context menu on mouse down, the user might accidentally select 65// the menu item on the subsequent mouse up. To prevent this, we add the 66// following delay before the user is able to select an item. 67static int context_menu_selection_hold_time_ms = 200; 68 69// The spacing offset for the bubble tip. 70const int kBubbleTipSizeLeftRight = 12; 71const int kBubbleTipSizeTopBottom = 11; 72 73// Returns true if the mnemonic of |menu| matches key. 74bool MatchesMnemonic(MenuItemView* menu, char16 key) { 75 return menu->GetMnemonic() == key; 76} 77 78// Returns true if |menu| doesn't have a mnemonic and first character of the its 79// title is |key|. 80bool TitleMatchesMnemonic(MenuItemView* menu, char16 key) { 81 if (menu->GetMnemonic()) 82 return false; 83 84 string16 lower_title = base::i18n::ToLower(menu->title()); 85 return !lower_title.empty() && lower_title[0] == key; 86} 87 88} // namespace 89 90// Convenience for scrolling the view such that the origin is visible. 91static void ScrollToVisible(View* view) { 92 view->ScrollRectToVisible(view->GetLocalBounds()); 93} 94 95// Returns the first descendant of |view| that is hot tracked. 96static View* GetFirstHotTrackedView(View* view) { 97 if (!view) 98 return NULL; 99 100 if (!strcmp(view->GetClassName(), CustomButton::kViewClassName)) { 101 CustomButton* button = static_cast<CustomButton*>(view); 102 if (button->IsHotTracked()) 103 return button; 104 } 105 106 for (int i = 0; i < view->child_count(); ++i) { 107 View* hot_view = GetFirstHotTrackedView(view->child_at(i)); 108 if (hot_view) 109 return hot_view; 110 } 111 return NULL; 112} 113 114// Recurses through the child views of |view| returning the first view starting 115// at |start| that is focusable. A value of -1 for |start| indicates to start at 116// the first view (if |forward| is false, iterating starts at the last view). If 117// |forward| is true the children are considered first to last, otherwise last 118// to first. 119static View* GetFirstFocusableView(View* view, int start, bool forward) { 120 if (forward) { 121 for (int i = start == -1 ? 0 : start; i < view->child_count(); ++i) { 122 View* deepest = GetFirstFocusableView(view->child_at(i), -1, forward); 123 if (deepest) 124 return deepest; 125 } 126 } else { 127 for (int i = start == -1 ? view->child_count() - 1 : start; i >= 0; --i) { 128 View* deepest = GetFirstFocusableView(view->child_at(i), -1, forward); 129 if (deepest) 130 return deepest; 131 } 132 } 133 return view->IsFocusable() ? view : NULL; 134} 135 136// Returns the first child of |start| that is focusable. 137static View* GetInitialFocusableView(View* start, bool forward) { 138 return GetFirstFocusableView(start, -1, forward); 139} 140 141// Returns the next view after |start_at| that is focusable. Returns NULL if 142// there are no focusable children of |ancestor| after |start_at|. 143static View* GetNextFocusableView(View* ancestor, 144 View* start_at, 145 bool forward) { 146 DCHECK(ancestor->Contains(start_at)); 147 View* parent = start_at; 148 do { 149 View* new_parent = parent->parent(); 150 int index = new_parent->GetIndexOf(parent); 151 index += forward ? 1 : -1; 152 if (forward || index != -1) { 153 View* next = GetFirstFocusableView(new_parent, index, forward); 154 if (next) 155 return next; 156 } 157 parent = new_parent; 158 } while (parent != ancestor); 159 return NULL; 160} 161 162// MenuScrollTask -------------------------------------------------------------- 163 164// MenuScrollTask is used when the SubmenuView does not all fit on screen and 165// the mouse is over the scroll up/down buttons. MenuScrollTask schedules 166// itself with a RepeatingTimer. When Run is invoked MenuScrollTask scrolls 167// appropriately. 168 169class MenuController::MenuScrollTask { 170 public: 171 MenuScrollTask() : submenu_(NULL), is_scrolling_up_(false), start_y_(0) { 172 pixels_per_second_ = MenuItemView::pref_menu_height() * 20; 173 } 174 175 void Update(const MenuController::MenuPart& part) { 176 if (!part.is_scroll()) { 177 StopScrolling(); 178 return; 179 } 180 DCHECK(part.submenu); 181 SubmenuView* new_menu = part.submenu; 182 bool new_is_up = (part.type == MenuController::MenuPart::SCROLL_UP); 183 if (new_menu == submenu_ && is_scrolling_up_ == new_is_up) 184 return; 185 186 start_scroll_time_ = base::Time::Now(); 187 start_y_ = part.submenu->GetVisibleBounds().y(); 188 submenu_ = new_menu; 189 is_scrolling_up_ = new_is_up; 190 191 if (!scrolling_timer_.IsRunning()) { 192 scrolling_timer_.Start(FROM_HERE, 193 TimeDelta::FromMilliseconds(kScrollTimerMS), 194 this, &MenuScrollTask::Run); 195 } 196 } 197 198 void StopScrolling() { 199 if (scrolling_timer_.IsRunning()) { 200 scrolling_timer_.Stop(); 201 submenu_ = NULL; 202 } 203 } 204 205 // The menu being scrolled. Returns null if not scrolling. 206 SubmenuView* submenu() const { return submenu_; } 207 208 private: 209 void Run() { 210 DCHECK(submenu_); 211 gfx::Rect vis_rect = submenu_->GetVisibleBounds(); 212 const int delta_y = static_cast<int>( 213 (base::Time::Now() - start_scroll_time_).InMilliseconds() * 214 pixels_per_second_ / 1000); 215 vis_rect.set_y(is_scrolling_up_ ? 216 std::max(0, start_y_ - delta_y) : 217 std::min(submenu_->height() - vis_rect.height(), start_y_ + delta_y)); 218 submenu_->ScrollRectToVisible(vis_rect); 219 } 220 221 // SubmenuView being scrolled. 222 SubmenuView* submenu_; 223 224 // Direction scrolling. 225 bool is_scrolling_up_; 226 227 // Timer to periodically scroll. 228 base::RepeatingTimer<MenuScrollTask> scrolling_timer_; 229 230 // Time we started scrolling at. 231 base::Time start_scroll_time_; 232 233 // How many pixels to scroll per second. 234 int pixels_per_second_; 235 236 // Y-coordinate of submenu_view_ when scrolling started. 237 int start_y_; 238 239 DISALLOW_COPY_AND_ASSIGN(MenuScrollTask); 240}; 241 242// MenuController:SelectByCharDetails ---------------------------------------- 243 244struct MenuController::SelectByCharDetails { 245 SelectByCharDetails() 246 : first_match(-1), 247 has_multiple(false), 248 index_of_item(-1), 249 next_match(-1) { 250 } 251 252 // Index of the first menu with the specified mnemonic. 253 int first_match; 254 255 // If true there are multiple menu items with the same mnemonic. 256 bool has_multiple; 257 258 // Index of the selected item; may remain -1. 259 int index_of_item; 260 261 // If there are multiple matches this is the index of the item after the 262 // currently selected item whose mnemonic matches. This may remain -1 even 263 // though there are matches. 264 int next_match; 265}; 266 267// MenuController:State ------------------------------------------------------ 268 269MenuController::State::State() 270 : item(NULL), 271 submenu_open(false), 272 anchor(views::MenuItemView::TOPLEFT), 273 context_menu(false) {} 274 275MenuController::State::~State() {} 276 277// MenuController ------------------------------------------------------------ 278 279// static 280MenuController* MenuController::active_instance_ = NULL; 281 282// static 283MenuController* MenuController::GetActiveInstance() { 284 return active_instance_; 285} 286 287MenuItemView* MenuController::Run(Widget* parent, 288 MenuButton* button, 289 MenuItemView* root, 290 const gfx::Rect& bounds, 291 MenuItemView::AnchorPosition position, 292 bool context_menu, 293 int* result_event_flags) { 294 exit_type_ = EXIT_NONE; 295 possible_drag_ = false; 296 drag_in_progress_ = false; 297 closing_event_time_ = base::TimeDelta(); 298 menu_start_time_ = base::TimeTicks::Now(); 299 300 // If we are shown on mouse press, we will eat the subsequent mouse down and 301 // the parent widget will not be able to reset its state (it might have mouse 302 // capture from the mouse down). So we clear its state here. 303 if (parent && parent->GetRootView()) 304 parent->GetRootView()->SetMouseHandler(NULL); 305 306 bool nested_menu = showing_; 307 if (showing_) { 308 // Only support nesting of blocking_run menus, nesting of 309 // blocking/non-blocking shouldn't be needed. 310 DCHECK(blocking_run_); 311 312 // We're already showing, push the current state. 313 menu_stack_.push_back(state_); 314 315 // The context menu should be owned by the same parent. 316 DCHECK_EQ(owner_, parent); 317 } else { 318 showing_ = true; 319 } 320 321 // Reset current state. 322 pending_state_ = State(); 323 state_ = State(); 324 UpdateInitialLocation(bounds, position, context_menu); 325 326 if (owner_) 327 owner_->RemoveObserver(this); 328 owner_ = parent; 329 if (owner_) 330 owner_->AddObserver(this); 331 332 // Set the selection, which opens the initial menu. 333 SetSelection(root, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); 334 335 if (!blocking_run_) { 336 // Start the timer to hide the menu. This is needed as we get no 337 // notification when the drag has finished. 338 StartCancelAllTimer(); 339 return NULL; 340 } 341 342 if (button) 343 menu_button_ = button; 344 345 // Make sure Chrome doesn't attempt to shut down while the menu is showing. 346 if (ViewsDelegate::views_delegate) 347 ViewsDelegate::views_delegate->AddRef(); 348 349 // We need to turn on nestable tasks as in some situations (pressing alt-f for 350 // one) the menus are run from a task. If we don't do this and are invoked 351 // from a task none of the tasks we schedule are processed and the menu 352 // appears totally broken. 353 message_loop_depth_++; 354 DCHECK_LE(message_loop_depth_, 2); 355 RunMessageLoop(nested_menu); 356 message_loop_depth_--; 357 358 if (ViewsDelegate::views_delegate) 359 ViewsDelegate::views_delegate->ReleaseRef(); 360 361 // Close any open menus. 362 SetSelection(NULL, SELECTION_UPDATE_IMMEDIATELY | SELECTION_EXIT); 363 364 if (nested_menu) { 365 DCHECK(!menu_stack_.empty()); 366 // We're running from within a menu, restore the previous state. 367 // The menus are already showing, so we don't have to show them. 368 state_ = menu_stack_.back(); 369 pending_state_ = menu_stack_.back(); 370 menu_stack_.pop_back(); 371 } else { 372 showing_ = false; 373 did_capture_ = false; 374 } 375 376 MenuItemView* result = result_; 377 // In case we're nested, reset result_. 378 result_ = NULL; 379 380 if (result_event_flags) 381 *result_event_flags = accept_event_flags_; 382 383 if (exit_type_ == EXIT_OUTERMOST) { 384 SetExitType(EXIT_NONE); 385 } else { 386 if (nested_menu && result) { 387 // We're nested and about to return a value. The caller might enter 388 // another blocking loop. We need to make sure all menus are hidden 389 // before that happens otherwise the menus will stay on screen. 390 CloseAllNestedMenus(); 391 SetSelection(NULL, SELECTION_UPDATE_IMMEDIATELY | SELECTION_EXIT); 392 393 // Set exit_all_, which makes sure all nested loops exit immediately. 394 if (exit_type_ != EXIT_DESTROYED) 395 SetExitType(EXIT_ALL); 396 } 397 } 398 399 // If we stopped running because one of the menus was destroyed chances are 400 // the button was also destroyed. 401 if (exit_type_ != EXIT_DESTROYED && menu_button_) { 402 menu_button_->SetState(CustomButton::STATE_NORMAL); 403 menu_button_->SchedulePaint(); 404 } 405 406 return result; 407} 408 409void MenuController::Cancel(ExitType type) { 410 // If the menu has already been destroyed, no further cancellation is 411 // needed. We especially don't want to set the |exit_type_| to a lesser 412 // value. 413 if (exit_type_ == EXIT_DESTROYED || exit_type_ == type) 414 return; 415 416 if (!showing_) { 417 // This occurs if we're in the process of notifying the delegate for a drop 418 // and the delegate cancels us. 419 return; 420 } 421 422 MenuItemView* selected = state_.item; 423 SetExitType(type); 424 425 SendMouseCaptureLostToActiveView(); 426 427 // Hide windows immediately. 428 SetSelection(NULL, SELECTION_UPDATE_IMMEDIATELY | SELECTION_EXIT); 429 430 if (!blocking_run_) { 431 // If we didn't block the caller we need to notify the menu, which 432 // triggers deleting us. 433 DCHECK(selected); 434 showing_ = false; 435 delegate_->DropMenuClosed( 436 internal::MenuControllerDelegate::NOTIFY_DELEGATE, 437 selected->GetRootMenuItem()); 438 // WARNING: the call to MenuClosed deletes us. 439 return; 440 } 441} 442 443void MenuController::OnMousePressed(SubmenuView* source, 444 const ui::MouseEvent& event) { 445 SetSelectionOnPointerDown(source, event); 446} 447 448void MenuController::OnMouseDragged(SubmenuView* source, 449 const ui::MouseEvent& event) { 450 MenuPart part = GetMenuPart(source, event.location()); 451 UpdateScrolling(part); 452 453 if (!blocking_run_) 454 return; 455 456 if (possible_drag_) { 457 if (View::ExceededDragThreshold(event.location() - press_pt_)) 458 StartDrag(source, press_pt_); 459 return; 460 } 461 MenuItemView* mouse_menu = NULL; 462 if (part.type == MenuPart::MENU_ITEM) { 463 if (!part.menu) 464 part.menu = source->GetMenuItem(); 465 else 466 mouse_menu = part.menu; 467 SetSelection(part.menu ? part.menu : state_.item, SELECTION_OPEN_SUBMENU); 468 } else if (part.type == MenuPart::NONE) { 469 ShowSiblingMenu(source, event.location()); 470 } 471 UpdateActiveMouseView(source, event, mouse_menu); 472} 473 474void MenuController::OnMouseReleased(SubmenuView* source, 475 const ui::MouseEvent& event) { 476 if (!blocking_run_) 477 return; 478 479 DCHECK(state_.item); 480 possible_drag_ = false; 481 DCHECK(blocking_run_); 482 MenuPart part = GetMenuPart(source, event.location()); 483 if (event.IsRightMouseButton() && part.type == MenuPart::MENU_ITEM) { 484 MenuItemView* menu = part.menu; 485 // |menu| is NULL means this event is from an empty menu or a separator. 486 // If it is from an empty menu, use parent context menu instead of that. 487 if (menu == NULL && 488 part.submenu->child_count() == 1 && 489 part.submenu->child_at(0)->id() 490 == views::MenuItemView::kEmptyMenuItemViewID) 491 menu = part.parent; 492 493 if (menu != NULL && ShowContextMenu(menu, source, event)) 494 return; 495 } 496 497 // We can use Ctrl+click or the middle mouse button to recursively open urls 498 // for selected folder menu items. If it's only a left click, show the 499 // contents of the folder. 500 if (!part.is_scroll() && part.menu && 501 !(part.menu->HasSubmenu() && 502 (event.flags() & ui::EF_LEFT_MOUSE_BUTTON))) { 503 if (active_mouse_view_) { 504 SendMouseReleaseToActiveView(source, event); 505 return; 506 } 507 if (part.menu->GetDelegate()->ShouldExecuteCommandWithoutClosingMenu( 508 part.menu->GetCommand(), event)) { 509 part.menu->GetDelegate()->ExecuteCommand(part.menu->GetCommand(), 510 event.flags()); 511 return; 512 } 513 if (!part.menu->NonIconChildViewsCount() && 514 part.menu->GetDelegate()->IsTriggerableEvent(part.menu, event)) { 515 int64 time_since_menu_start = 516 (base::TimeTicks::Now() - menu_start_time_).InMilliseconds(); 517 if (!state_.context_menu || !View::ShouldShowContextMenuOnMousePress() || 518 time_since_menu_start > context_menu_selection_hold_time_ms) 519 Accept(part.menu, event.flags()); 520 return; 521 } 522 } else if (part.type == MenuPart::MENU_ITEM) { 523 // User either clicked on empty space, or a menu that has children. 524 SetSelection(part.menu ? part.menu : state_.item, 525 SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); 526 } 527 SendMouseCaptureLostToActiveView(); 528} 529 530void MenuController::OnMouseMoved(SubmenuView* source, 531 const ui::MouseEvent& event) { 532 HandleMouseLocation(source, event.location()); 533} 534 535void MenuController::OnMouseEntered(SubmenuView* source, 536 const ui::MouseEvent& event) { 537 // MouseEntered is always followed by a mouse moved, so don't need to 538 // do anything here. 539} 540 541#if defined(OS_LINUX) 542bool MenuController::OnMouseWheel(SubmenuView* source, 543 const ui::MouseWheelEvent& event) { 544 MenuPart part = GetMenuPart(source, event.location()); 545 return part.submenu && part.submenu->OnMouseWheel(event); 546} 547#endif 548 549void MenuController::OnGestureEvent(SubmenuView* source, 550 ui::GestureEvent* event) { 551 MenuPart part = GetMenuPart(source, event->location()); 552 if (event->type() == ui::ET_GESTURE_TAP_DOWN) { 553 SetSelectionOnPointerDown(source, *event); 554 event->StopPropagation(); 555 } else if (event->type() == ui::ET_GESTURE_LONG_PRESS) { 556 if (part.type == MenuPart::MENU_ITEM && part.menu) { 557 if (ShowContextMenu(part.menu, source, *event)) 558 event->StopPropagation(); 559 } 560 } else if (event->type() == ui::ET_GESTURE_TAP) { 561 if (!part.is_scroll() && part.menu && 562 !(part.menu->HasSubmenu())) { 563 if (part.menu->GetDelegate()->IsTriggerableEvent( 564 part.menu, *event)) { 565 Accept(part.menu, event->flags()); 566 } 567 event->StopPropagation(); 568 } else if (part.type == MenuPart::MENU_ITEM) { 569 // User either tapped on empty space, or a menu that has children. 570 SetSelection(part.menu ? part.menu : state_.item, 571 SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); 572 event->StopPropagation(); 573 } 574 } else if (event->type() == ui::ET_GESTURE_TAP_CANCEL && 575 part.menu && 576 part.type == MenuPart::MENU_ITEM) { 577 // Move the selection to the parent menu so that the selection in the 578 // current menu is unset. Make sure the submenu remains open by sending the 579 // appropriate SetSelectionTypes flags. 580 SetSelection(part.menu->GetParentMenuItem(), 581 SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); 582 event->StopPropagation(); 583 } 584 585 if (event->stopped_propagation()) 586 return; 587 588 if (!part.submenu) 589 return; 590 part.submenu->OnGestureEvent(event); 591} 592 593bool MenuController::GetDropFormats( 594 SubmenuView* source, 595 int* formats, 596 std::set<OSExchangeData::CustomFormat>* custom_formats) { 597 return source->GetMenuItem()->GetDelegate()->GetDropFormats( 598 source->GetMenuItem(), formats, custom_formats); 599} 600 601bool MenuController::AreDropTypesRequired(SubmenuView* source) { 602 return source->GetMenuItem()->GetDelegate()->AreDropTypesRequired( 603 source->GetMenuItem()); 604} 605 606bool MenuController::CanDrop(SubmenuView* source, const OSExchangeData& data) { 607 return source->GetMenuItem()->GetDelegate()->CanDrop(source->GetMenuItem(), 608 data); 609} 610 611void MenuController::OnDragEntered(SubmenuView* source, 612 const ui::DropTargetEvent& event) { 613 valid_drop_coordinates_ = false; 614} 615 616int MenuController::OnDragUpdated(SubmenuView* source, 617 const ui::DropTargetEvent& event) { 618 StopCancelAllTimer(); 619 620 gfx::Point screen_loc(event.location()); 621 View::ConvertPointToScreen(source, &screen_loc); 622 if (valid_drop_coordinates_ && screen_loc == drop_pt_) 623 return last_drop_operation_; 624 drop_pt_ = screen_loc; 625 valid_drop_coordinates_ = true; 626 627 MenuItemView* menu_item = GetMenuItemAt(source, event.x(), event.y()); 628 bool over_empty_menu = false; 629 if (!menu_item) { 630 // See if we're over an empty menu. 631 menu_item = GetEmptyMenuItemAt(source, event.x(), event.y()); 632 if (menu_item) 633 over_empty_menu = true; 634 } 635 MenuDelegate::DropPosition drop_position = MenuDelegate::DROP_NONE; 636 int drop_operation = ui::DragDropTypes::DRAG_NONE; 637 if (menu_item) { 638 gfx::Point menu_item_loc(event.location()); 639 View::ConvertPointToTarget(source, menu_item, &menu_item_loc); 640 MenuItemView* query_menu_item; 641 if (!over_empty_menu) { 642 int menu_item_height = menu_item->height(); 643 if (menu_item->HasSubmenu() && 644 (menu_item_loc.y() > kDropBetweenPixels && 645 menu_item_loc.y() < (menu_item_height - kDropBetweenPixels))) { 646 drop_position = MenuDelegate::DROP_ON; 647 } else { 648 drop_position = (menu_item_loc.y() < menu_item_height / 2) ? 649 MenuDelegate::DROP_BEFORE : MenuDelegate::DROP_AFTER; 650 } 651 query_menu_item = menu_item; 652 } else { 653 query_menu_item = menu_item->GetParentMenuItem(); 654 drop_position = MenuDelegate::DROP_ON; 655 } 656 drop_operation = menu_item->GetDelegate()->GetDropOperation( 657 query_menu_item, event, &drop_position); 658 659 // If the menu has a submenu, schedule the submenu to open. 660 SetSelection(menu_item, menu_item->HasSubmenu() ? SELECTION_OPEN_SUBMENU : 661 SELECTION_DEFAULT); 662 663 if (drop_position == MenuDelegate::DROP_NONE || 664 drop_operation == ui::DragDropTypes::DRAG_NONE) 665 menu_item = NULL; 666 } else { 667 SetSelection(source->GetMenuItem(), SELECTION_OPEN_SUBMENU); 668 } 669 SetDropMenuItem(menu_item, drop_position); 670 last_drop_operation_ = drop_operation; 671 return drop_operation; 672} 673 674void MenuController::OnDragExited(SubmenuView* source) { 675 StartCancelAllTimer(); 676 677 if (drop_target_) { 678 StopShowTimer(); 679 SetDropMenuItem(NULL, MenuDelegate::DROP_NONE); 680 } 681} 682 683int MenuController::OnPerformDrop(SubmenuView* source, 684 const ui::DropTargetEvent& event) { 685 DCHECK(drop_target_); 686 // NOTE: the delegate may delete us after invoking OnPerformDrop, as such 687 // we don't call cancel here. 688 689 MenuItemView* item = state_.item; 690 DCHECK(item); 691 692 MenuItemView* drop_target = drop_target_; 693 MenuDelegate::DropPosition drop_position = drop_position_; 694 695 // Close all menus, including any nested menus. 696 SetSelection(NULL, SELECTION_UPDATE_IMMEDIATELY | SELECTION_EXIT); 697 CloseAllNestedMenus(); 698 699 // Set state such that we exit. 700 showing_ = false; 701 SetExitType(EXIT_ALL); 702 703 // If over an empty menu item, drop occurs on the parent. 704 if (drop_target->id() == MenuItemView::kEmptyMenuItemViewID) 705 drop_target = drop_target->GetParentMenuItem(); 706 707 if (!IsBlockingRun()) { 708 delegate_->DropMenuClosed( 709 internal::MenuControllerDelegate::DONT_NOTIFY_DELEGATE, 710 item->GetRootMenuItem()); 711 } 712 713 // WARNING: the call to MenuClosed deletes us. 714 715 return drop_target->GetDelegate()->OnPerformDrop( 716 drop_target, drop_position, event); 717} 718 719void MenuController::OnDragEnteredScrollButton(SubmenuView* source, 720 bool is_up) { 721 MenuPart part; 722 part.type = is_up ? MenuPart::SCROLL_UP : MenuPart::SCROLL_DOWN; 723 part.submenu = source; 724 UpdateScrolling(part); 725 726 // Do this to force the selection to hide. 727 SetDropMenuItem(source->GetMenuItemAt(0), MenuDelegate::DROP_NONE); 728 729 StopCancelAllTimer(); 730} 731 732void MenuController::OnDragExitedScrollButton(SubmenuView* source) { 733 StartCancelAllTimer(); 734 SetDropMenuItem(NULL, MenuDelegate::DROP_NONE); 735 StopScrolling(); 736} 737 738void MenuController::UpdateSubmenuSelection(SubmenuView* submenu) { 739 if (submenu->IsShowing()) { 740 gfx::Point point = GetScreen()->GetCursorScreenPoint(); 741 const SubmenuView* root_submenu = 742 submenu->GetMenuItem()->GetRootMenuItem()->GetSubmenu(); 743 views::View::ConvertPointFromScreen( 744 root_submenu->GetWidget()->GetRootView(), &point); 745 HandleMouseLocation(submenu, point); 746 } 747} 748 749void MenuController::OnWidgetDestroying(Widget* widget) { 750 DCHECK_EQ(owner_, widget); 751 owner_->RemoveObserver(this); 752 owner_ = NULL; 753} 754 755// static 756void MenuController::TurnOffContextMenuSelectionHoldForTest() { 757 context_menu_selection_hold_time_ms = -1; 758} 759 760void MenuController::SetSelection(MenuItemView* menu_item, 761 int selection_types) { 762 size_t paths_differ_at = 0; 763 std::vector<MenuItemView*> current_path; 764 std::vector<MenuItemView*> new_path; 765 BuildPathsAndCalculateDiff(pending_state_.item, menu_item, ¤t_path, 766 &new_path, &paths_differ_at); 767 768 size_t current_size = current_path.size(); 769 size_t new_size = new_path.size(); 770 771 bool pending_item_changed = pending_state_.item != menu_item; 772 if (pending_item_changed && pending_state_.item) { 773 View* current_hot_view = GetFirstHotTrackedView(pending_state_.item); 774 if (current_hot_view && !strcmp(current_hot_view->GetClassName(), 775 CustomButton::kViewClassName)) { 776 CustomButton* button = static_cast<CustomButton*>(current_hot_view); 777 button->SetHotTracked(false); 778 } 779 } 780 781 // Notify the old path it isn't selected. 782 MenuDelegate* current_delegate = 783 current_path.empty() ? NULL : current_path.front()->GetDelegate(); 784 for (size_t i = paths_differ_at; i < current_size; ++i) { 785 if (current_delegate && 786 current_path[i]->GetType() == MenuItemView::SUBMENU) { 787 current_delegate->WillHideMenu(current_path[i]); 788 } 789 current_path[i]->SetSelected(false); 790 } 791 792 // Notify the new path it is selected. 793 for (size_t i = paths_differ_at; i < new_size; ++i) 794 new_path[i]->SetSelected(true); 795 796 if (menu_item && menu_item->GetDelegate()) 797 menu_item->GetDelegate()->SelectionChanged(menu_item); 798 799 DCHECK(menu_item || (selection_types & SELECTION_EXIT) != 0); 800 801 pending_state_.item = menu_item; 802 pending_state_.submenu_open = (selection_types & SELECTION_OPEN_SUBMENU) != 0; 803 804 // Stop timers. 805 StopCancelAllTimer(); 806 // Resets show timer only when pending menu item is changed. 807 if (pending_item_changed) 808 StopShowTimer(); 809 810 if (selection_types & SELECTION_UPDATE_IMMEDIATELY) 811 CommitPendingSelection(); 812 else if (pending_item_changed) 813 StartShowTimer(); 814 815 // Notify an accessibility focus event on all menu items except for the root. 816 if (menu_item && 817 (MenuDepth(menu_item) != 1 || 818 menu_item->GetType() != MenuItemView::SUBMENU)) { 819 menu_item->NotifyAccessibilityEvent( 820 ui::AccessibilityTypes::EVENT_FOCUS, true); 821 } 822} 823 824void MenuController::SetSelectionOnPointerDown(SubmenuView* source, 825 const ui::LocatedEvent& event) { 826 if (!blocking_run_) 827 return; 828 829 DCHECK(!active_mouse_view_); 830 831 MenuPart part = GetMenuPart(source, event.location()); 832 if (part.is_scroll()) 833 return; // Ignore presses on scroll buttons. 834 835 // When this menu is opened through a touch event, a simulated right-click 836 // is sent before the menu appears. Ignore it. 837 if ((event.flags() & ui::EF_RIGHT_MOUSE_BUTTON) && 838 (event.flags() & ui::EF_FROM_TOUCH)) 839 return; 840 841 if (part.type == MenuPart::NONE || 842 (part.type == MenuPart::MENU_ITEM && part.menu && 843 part.menu->GetRootMenuItem() != state_.item->GetRootMenuItem())) { 844 // Remember the time when we repost the event. The owner can then use this 845 // to figure out if this menu was finished with the same click which is 846 // sent to it thereafter. Note that the time stamp front he event cannot be 847 // used since the reposting will set a new timestamp when the event gets 848 // processed. As such it is better to take the current time which will be 849 // closer to the time when it arrives again in the menu handler. 850 closing_event_time_ = ui::EventTimeForNow(); 851 852 // Mouse wasn't pressed over any menu, or the active menu, cancel. 853 854#if defined(OS_WIN) 855 // We're going to close and we own the mouse capture. We need to repost the 856 // mouse down, otherwise the window the user clicked on won't get the 857 // event. 858 RepostEvent(source, event); 859#endif 860 861 // And close. 862 ExitType exit_type = EXIT_ALL; 863 if (!menu_stack_.empty()) { 864 // We're running nested menus. Only exit all if the mouse wasn't over one 865 // of the menus from the last run. 866 gfx::Point screen_loc(event.location()); 867 View::ConvertPointToScreen(source->GetScrollViewContainer(), &screen_loc); 868 MenuPart last_part = GetMenuPartByScreenCoordinateUsingMenu( 869 menu_stack_.back().item, screen_loc); 870 if (last_part.type != MenuPart::NONE) 871 exit_type = EXIT_OUTERMOST; 872 } 873 Cancel(exit_type); 874 875#if defined(USE_AURA) && !defined(OS_WIN) 876 // We're going to exit the menu and want to repost the event so that is 877 // is handled normally after the context menu has exited. We call 878 // RepostEvent after Cancel so that mouse capture has been released so 879 // that finding the event target is unaffected by the current capture. 880 RepostEvent(source, event); 881#endif 882 883 return; 884 } 885 886 // On a press we immediately commit the selection, that way a submenu 887 // pops up immediately rather than after a delay. 888 int selection_types = SELECTION_UPDATE_IMMEDIATELY; 889 if (!part.menu) { 890 part.menu = part.parent; 891 selection_types |= SELECTION_OPEN_SUBMENU; 892 } else { 893 if (part.menu->GetDelegate()->CanDrag(part.menu)) { 894 possible_drag_ = true; 895 press_pt_ = event.location(); 896 } 897 if (part.menu->HasSubmenu()) 898 selection_types |= SELECTION_OPEN_SUBMENU; 899 } 900 SetSelection(part.menu, selection_types); 901} 902 903void MenuController::StartDrag(SubmenuView* source, 904 const gfx::Point& location) { 905 MenuItemView* item = state_.item; 906 DCHECK(item); 907 // Points are in the coordinates of the submenu, need to map to that of 908 // the selected item. Additionally source may not be the parent of 909 // the selected item, so need to map to screen first then to item. 910 gfx::Point press_loc(location); 911 View::ConvertPointToScreen(source->GetScrollViewContainer(), &press_loc); 912 View::ConvertPointToTarget(NULL, item, &press_loc); 913 gfx::Point widget_loc(press_loc); 914 View::ConvertPointToWidget(item, &widget_loc); 915 scoped_ptr<gfx::Canvas> canvas(views::GetCanvasForDragImage( 916 source->GetWidget(), gfx::Size(item->width(), item->height()))); 917 item->PaintButton(canvas.get(), MenuItemView::PB_FOR_DRAG); 918 919 OSExchangeData data; 920 item->GetDelegate()->WriteDragData(item, &data); 921 drag_utils::SetDragImageOnDataObject(*canvas, item->size(), 922 press_loc.OffsetFromOrigin(), 923 &data); 924 StopScrolling(); 925 int drag_ops = item->GetDelegate()->GetDragOperations(item); 926 drag_in_progress_ = true; 927 // TODO(varunjain): Properly determine and send DRAG_EVENT_SOURCE below. 928 item->GetWidget()->RunShellDrag(NULL, data, widget_loc, drag_ops, 929 ui::DragDropTypes::DRAG_EVENT_SOURCE_MOUSE); 930 drag_in_progress_ = false; 931 932 if (GetActiveInstance() == this) { 933 if (showing_) { 934 // We're still showing, close all menus. 935 CloseAllNestedMenus(); 936 Cancel(EXIT_ALL); 937 } // else case, drop was on us. 938 } // else case, someone canceled us, don't do anything 939} 940 941#if defined(OS_WIN) 942bool MenuController::Dispatch(const MSG& msg) { 943 DCHECK(blocking_run_); 944 945 if (exit_type_ == EXIT_ALL || exit_type_ == EXIT_DESTROYED) { 946 // We must translate/dispatch the message here, otherwise we would drop 947 // the message on the floor. 948 TranslateMessage(&msg); 949 DispatchMessage(&msg); 950 return false; 951 } 952 953 // NOTE: we don't get WM_ACTIVATE or anything else interesting in here. 954 switch (msg.message) { 955 case WM_CONTEXTMENU: { 956 MenuItemView* item = pending_state_.item; 957 if (item && item->GetRootMenuItem() != item) { 958 gfx::Point screen_loc(0, item->height()); 959 View::ConvertPointToScreen(item, &screen_loc); 960 item->GetDelegate()->ShowContextMenu(item, item->GetCommand(), 961 screen_loc, false); 962 } 963 return true; 964 } 965 966 // NOTE: focus wasn't changed when the menu was shown. As such, don't 967 // dispatch key events otherwise the focused window will get the events. 968 case WM_KEYDOWN: { 969 bool result = OnKeyDown(ui::KeyboardCodeFromNative(msg)); 970 TranslateMessage(&msg); 971 return result; 972 } 973 case WM_CHAR: 974 return !SelectByChar(static_cast<char16>(msg.wParam)); 975 case WM_KEYUP: 976 return true; 977 978 case WM_SYSKEYUP: 979 // We may have been shown on a system key, as such don't do anything 980 // here. If another system key is pushed we'll get a WM_SYSKEYDOWN and 981 // close the menu. 982 return true; 983 984 case WM_CANCELMODE: 985 case WM_SYSKEYDOWN: 986 // Exit immediately on system keys. 987 Cancel(EXIT_ALL); 988 return false; 989 990 default: 991 break; 992 } 993 TranslateMessage(&msg); 994 DispatchMessage(&msg); 995 return exit_type_ == EXIT_NONE; 996} 997#elif defined(USE_AURA) 998bool MenuController::Dispatch(const base::NativeEvent& event) { 999 if (exit_type_ == EXIT_ALL || exit_type_ == EXIT_DESTROYED) { 1000 aura::Env::GetInstance()->GetDispatcher()->Dispatch(event); 1001 return false; 1002 } 1003 // Activates mnemonics only when it it pressed without modifiers except for 1004 // caps and shift. 1005 int flags = ui::EventFlagsFromNative(event) & 1006 ~ui::EF_CAPS_LOCK_DOWN & ~ui::EF_SHIFT_DOWN; 1007 if (flags == ui::EF_NONE) { 1008 switch (ui::EventTypeFromNative(event)) { 1009 case ui::ET_KEY_PRESSED: 1010 if (!OnKeyDown(ui::KeyboardCodeFromNative(event))) 1011 return false; 1012 1013 return !SelectByChar(ui::KeyboardCodeFromNative(event)); 1014 case ui::ET_KEY_RELEASED: 1015 return true; 1016 default: 1017 break; 1018 } 1019 } 1020 1021 aura::Env::GetInstance()->GetDispatcher()->Dispatch(event); 1022 return exit_type_ == EXIT_NONE; 1023} 1024#endif 1025 1026bool MenuController::OnKeyDown(ui::KeyboardCode key_code) { 1027 DCHECK(blocking_run_); 1028 1029 switch (key_code) { 1030 case ui::VKEY_UP: 1031 IncrementSelection(-1); 1032 break; 1033 1034 case ui::VKEY_DOWN: 1035 IncrementSelection(1); 1036 break; 1037 1038 // Handling of VK_RIGHT and VK_LEFT is different depending on the UI 1039 // layout. 1040 case ui::VKEY_RIGHT: 1041 if (base::i18n::IsRTL()) 1042 CloseSubmenu(); 1043 else 1044 OpenSubmenuChangeSelectionIfCan(); 1045 break; 1046 1047 case ui::VKEY_LEFT: 1048 if (base::i18n::IsRTL()) 1049 OpenSubmenuChangeSelectionIfCan(); 1050 else 1051 CloseSubmenu(); 1052 break; 1053 1054 case ui::VKEY_SPACE: 1055 if (SendAcceleratorToHotTrackedView() == ACCELERATOR_PROCESSED_EXIT) 1056 return false; 1057 break; 1058 1059 case ui::VKEY_RETURN: 1060 if (pending_state_.item) { 1061 if (pending_state_.item->HasSubmenu()) { 1062 OpenSubmenuChangeSelectionIfCan(); 1063 } else { 1064 SendAcceleratorResultType result = SendAcceleratorToHotTrackedView(); 1065 if (result == ACCELERATOR_NOT_PROCESSED && 1066 pending_state_.item->enabled()) { 1067 Accept(pending_state_.item, 0); 1068 return false; 1069 } else if (result == ACCELERATOR_PROCESSED_EXIT) { 1070 return false; 1071 } 1072 } 1073 } 1074 break; 1075 1076 case ui::VKEY_ESCAPE: 1077 if (!state_.item->GetParentMenuItem() || 1078 (!state_.item->GetParentMenuItem()->GetParentMenuItem() && 1079 (!state_.item->HasSubmenu() || 1080 !state_.item->GetSubmenu()->IsShowing()))) { 1081 // User pressed escape and only one menu is shown, cancel it. 1082 Cancel(EXIT_OUTERMOST); 1083 return false; 1084 } 1085 CloseSubmenu(); 1086 break; 1087 1088#if defined(OS_WIN) 1089 case VK_APPS: 1090 break; 1091#endif 1092 1093 default: 1094 break; 1095 } 1096 return true; 1097} 1098 1099MenuController::MenuController(ui::NativeTheme* theme, 1100 bool blocking, 1101 internal::MenuControllerDelegate* delegate) 1102 : blocking_run_(blocking), 1103 showing_(false), 1104 exit_type_(EXIT_NONE), 1105 did_capture_(false), 1106 result_(NULL), 1107 accept_event_flags_(0), 1108 drop_target_(NULL), 1109 drop_position_(MenuDelegate::DROP_UNKNOWN), 1110 owner_(NULL), 1111 possible_drag_(false), 1112 drag_in_progress_(false), 1113 valid_drop_coordinates_(false), 1114 last_drop_operation_(MenuDelegate::DROP_UNKNOWN), 1115 showing_submenu_(false), 1116 menu_button_(NULL), 1117 active_mouse_view_(NULL), 1118 delegate_(delegate), 1119 message_loop_depth_(0), 1120 menu_config_(theme), 1121 closing_event_time_(base::TimeDelta()), 1122 menu_start_time_(base::TimeTicks()) { 1123 active_instance_ = this; 1124} 1125 1126MenuController::~MenuController() { 1127 DCHECK(!showing_); 1128 if (owner_) 1129 owner_->RemoveObserver(this); 1130 if (active_instance_ == this) 1131 active_instance_ = NULL; 1132 StopShowTimer(); 1133 StopCancelAllTimer(); 1134} 1135 1136MenuController::SendAcceleratorResultType 1137 MenuController::SendAcceleratorToHotTrackedView() { 1138 View* hot_view = GetFirstHotTrackedView(pending_state_.item); 1139 if (!hot_view) 1140 return ACCELERATOR_NOT_PROCESSED; 1141 1142 ui::Accelerator accelerator(ui::VKEY_RETURN, ui::EF_NONE); 1143 hot_view->AcceleratorPressed(accelerator); 1144 if (!strcmp(hot_view->GetClassName(), CustomButton::kViewClassName)) { 1145 CustomButton* button = static_cast<CustomButton*>(hot_view); 1146 button->SetHotTracked(true); 1147 } 1148 return (exit_type_ == EXIT_NONE) ? 1149 ACCELERATOR_PROCESSED : ACCELERATOR_PROCESSED_EXIT; 1150} 1151 1152void MenuController::UpdateInitialLocation( 1153 const gfx::Rect& bounds, 1154 MenuItemView::AnchorPosition position, 1155 bool context_menu) { 1156 pending_state_.context_menu = context_menu; 1157 pending_state_.initial_bounds = bounds; 1158 if (bounds.height() > 1) { 1159 // Inset the bounds slightly, otherwise drag coordinates don't line up 1160 // nicely and menus close prematurely. 1161 pending_state_.initial_bounds.Inset(0, 1); 1162 } 1163 1164 // Reverse anchor position for RTL languages. 1165 if (base::i18n::IsRTL() && 1166 (position == MenuItemView::TOPRIGHT || 1167 position == MenuItemView::TOPLEFT)) { 1168 pending_state_.anchor = position == MenuItemView::TOPRIGHT ? 1169 MenuItemView::TOPLEFT : MenuItemView::TOPRIGHT; 1170 } else { 1171 pending_state_.anchor = position; 1172 } 1173 1174 // Calculate the bounds of the monitor we'll show menus on. Do this once to 1175 // avoid repeated system queries for the info. 1176 pending_state_.monitor_bounds = GetScreen()->GetDisplayNearestPoint( 1177 bounds.origin()).work_area(); 1178#if defined(USE_ASH) 1179 if (!pending_state_.monitor_bounds.Contains(bounds)) { 1180 // Use the monitor area if the work area doesn't contain the bounds. This 1181 // handles showing a menu from the launcher. 1182 gfx::Rect monitor_area = GetScreen()->GetDisplayNearestPoint( 1183 bounds.origin()).bounds(); 1184 if (monitor_area.Contains(bounds)) 1185 pending_state_.monitor_bounds = monitor_area; 1186 } 1187#endif 1188} 1189 1190void MenuController::Accept(MenuItemView* item, int event_flags) { 1191 DCHECK(IsBlockingRun()); 1192 result_ = item; 1193 if (item && !menu_stack_.empty() && 1194 !item->GetDelegate()->ShouldCloseAllMenusOnExecute(item->GetCommand())) { 1195 SetExitType(EXIT_OUTERMOST); 1196 } else { 1197 SetExitType(EXIT_ALL); 1198 } 1199 accept_event_flags_ = event_flags; 1200} 1201 1202bool MenuController::ShowSiblingMenu(SubmenuView* source, 1203 const gfx::Point& mouse_location) { 1204 if (!menu_stack_.empty() || !menu_button_) 1205 return false; 1206 1207 View* source_view = source->GetScrollViewContainer(); 1208 if (mouse_location.x() >= 0 && 1209 mouse_location.x() < source_view->width() && 1210 mouse_location.y() >= 0 && 1211 mouse_location.y() < source_view->height()) { 1212 // The mouse is over the menu, no need to continue. 1213 return false; 1214 } 1215 1216 gfx::NativeWindow window_under_mouse = 1217 GetScreen()->GetWindowAtCursorScreenPoint(); 1218 // TODO(oshima): Replace with views only API. 1219 if (!owner_ || window_under_mouse != owner_->GetNativeWindow()) 1220 return false; 1221 1222 // The user moved the mouse outside the menu and over the owning window. See 1223 // if there is a sibling menu we should show. 1224 gfx::Point screen_point(mouse_location); 1225 View::ConvertPointToScreen(source_view, &screen_point); 1226 MenuItemView::AnchorPosition anchor; 1227 bool has_mnemonics; 1228 MenuButton* button = NULL; 1229 MenuItemView* alt_menu = source->GetMenuItem()->GetDelegate()-> 1230 GetSiblingMenu(source->GetMenuItem()->GetRootMenuItem(), 1231 screen_point, &anchor, &has_mnemonics, &button); 1232 if (!alt_menu || (state_.item && state_.item->GetRootMenuItem() == alt_menu)) 1233 return false; 1234 1235 delegate_->SiblingMenuCreated(alt_menu); 1236 1237 if (!button) { 1238 // If the delegate returns a menu, they must also return a button. 1239 NOTREACHED(); 1240 return false; 1241 } 1242 1243 // There is a sibling menu, update the button state, hide the current menu 1244 // and show the new one. 1245 menu_button_->SetState(CustomButton::STATE_NORMAL); 1246 menu_button_->SchedulePaint(); 1247 menu_button_ = button; 1248 menu_button_->SetState(CustomButton::STATE_PRESSED); 1249 menu_button_->SchedulePaint(); 1250 1251 // Need to reset capture when we show the menu again, otherwise we aren't 1252 // going to get any events. 1253 did_capture_ = false; 1254 gfx::Point screen_menu_loc; 1255 View::ConvertPointToScreen(button, &screen_menu_loc); 1256 1257 // It is currently not possible to show a submenu recursively in a bubble. 1258 DCHECK(!MenuItemView::IsBubble(anchor)); 1259 // Subtract 1 from the height to make the popup flush with the button border. 1260 UpdateInitialLocation(gfx::Rect(screen_menu_loc.x(), screen_menu_loc.y(), 1261 button->width(), button->height() - 1), 1262 anchor, state_.context_menu); 1263 alt_menu->PrepareForRun( 1264 false, has_mnemonics, 1265 source->GetMenuItem()->GetRootMenuItem()->show_mnemonics_); 1266 alt_menu->controller_ = this; 1267 SetSelection(alt_menu, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); 1268 return true; 1269} 1270 1271bool MenuController::ShowContextMenu(MenuItemView* menu_item, 1272 SubmenuView* source, 1273 const ui::LocatedEvent& event) { 1274 // Set the selection immediately, making sure the submenu is only open 1275 // if it already was. 1276 int selection_types = SELECTION_UPDATE_IMMEDIATELY; 1277 if (state_.item == pending_state_.item && state_.submenu_open) 1278 selection_types |= SELECTION_OPEN_SUBMENU; 1279 SetSelection(pending_state_.item, selection_types); 1280 gfx::Point loc(event.location()); 1281 View::ConvertPointToScreen(source->GetScrollViewContainer(), &loc); 1282 1283 if (menu_item->GetDelegate()->ShowContextMenu( 1284 menu_item, menu_item->GetCommand(), loc, true)) { 1285 SendMouseCaptureLostToActiveView(); 1286 return true; 1287 } 1288 return false; 1289} 1290 1291void MenuController::CloseAllNestedMenus() { 1292 for (std::list<State>::iterator i = menu_stack_.begin(); 1293 i != menu_stack_.end(); ++i) { 1294 MenuItemView* last_item = i->item; 1295 for (MenuItemView* item = last_item; item; 1296 item = item->GetParentMenuItem()) { 1297 CloseMenu(item); 1298 last_item = item; 1299 } 1300 i->submenu_open = false; 1301 i->item = last_item; 1302 } 1303} 1304 1305MenuItemView* MenuController::GetMenuItemAt(View* source, int x, int y) { 1306 // Walk the view hierarchy until we find a menu item (or the root). 1307 View* child_under_mouse = source->GetEventHandlerForPoint(gfx::Point(x, y)); 1308 while (child_under_mouse && 1309 child_under_mouse->id() != MenuItemView::kMenuItemViewID) { 1310 child_under_mouse = child_under_mouse->parent(); 1311 } 1312 if (child_under_mouse && child_under_mouse->enabled() && 1313 child_under_mouse->id() == MenuItemView::kMenuItemViewID) { 1314 return static_cast<MenuItemView*>(child_under_mouse); 1315 } 1316 return NULL; 1317} 1318 1319MenuItemView* MenuController::GetEmptyMenuItemAt(View* source, int x, int y) { 1320 View* child_under_mouse = source->GetEventHandlerForPoint(gfx::Point(x, y)); 1321 if (child_under_mouse && 1322 child_under_mouse->id() == MenuItemView::kEmptyMenuItemViewID) { 1323 return static_cast<MenuItemView*>(child_under_mouse); 1324 } 1325 return NULL; 1326} 1327 1328bool MenuController::IsScrollButtonAt(SubmenuView* source, 1329 int x, 1330 int y, 1331 MenuPart::Type* part) { 1332 MenuScrollViewContainer* scroll_view = source->GetScrollViewContainer(); 1333 View* child_under_mouse = 1334 scroll_view->GetEventHandlerForPoint(gfx::Point(x, y)); 1335 if (child_under_mouse && child_under_mouse->enabled()) { 1336 if (child_under_mouse == scroll_view->scroll_up_button()) { 1337 *part = MenuPart::SCROLL_UP; 1338 return true; 1339 } 1340 if (child_under_mouse == scroll_view->scroll_down_button()) { 1341 *part = MenuPart::SCROLL_DOWN; 1342 return true; 1343 } 1344 } 1345 return false; 1346} 1347 1348MenuController::MenuPart MenuController::GetMenuPart( 1349 SubmenuView* source, 1350 const gfx::Point& source_loc) { 1351 gfx::Point screen_loc(source_loc); 1352 View::ConvertPointToScreen(source->GetScrollViewContainer(), &screen_loc); 1353 return GetMenuPartByScreenCoordinateUsingMenu(state_.item, screen_loc); 1354} 1355 1356MenuController::MenuPart MenuController::GetMenuPartByScreenCoordinateUsingMenu( 1357 MenuItemView* item, 1358 const gfx::Point& screen_loc) { 1359 MenuPart part; 1360 for (; item; item = item->GetParentMenuItem()) { 1361 if (item->HasSubmenu() && item->GetSubmenu()->IsShowing() && 1362 GetMenuPartByScreenCoordinateImpl(item->GetSubmenu(), screen_loc, 1363 &part)) { 1364 return part; 1365 } 1366 } 1367 return part; 1368} 1369 1370bool MenuController::GetMenuPartByScreenCoordinateImpl( 1371 SubmenuView* menu, 1372 const gfx::Point& screen_loc, 1373 MenuPart* part) { 1374 // Is the mouse over the scroll buttons? 1375 gfx::Point scroll_view_loc = screen_loc; 1376 View* scroll_view_container = menu->GetScrollViewContainer(); 1377 View::ConvertPointToTarget(NULL, scroll_view_container, &scroll_view_loc); 1378 if (scroll_view_loc.x() < 0 || 1379 scroll_view_loc.x() >= scroll_view_container->width() || 1380 scroll_view_loc.y() < 0 || 1381 scroll_view_loc.y() >= scroll_view_container->height()) { 1382 // Point isn't contained in menu. 1383 return false; 1384 } 1385 if (IsScrollButtonAt(menu, scroll_view_loc.x(), scroll_view_loc.y(), 1386 &(part->type))) { 1387 part->submenu = menu; 1388 return true; 1389 } 1390 1391 // Not over the scroll button. Check the actual menu. 1392 if (DoesSubmenuContainLocation(menu, screen_loc)) { 1393 gfx::Point menu_loc = screen_loc; 1394 View::ConvertPointToTarget(NULL, menu, &menu_loc); 1395 part->menu = GetMenuItemAt(menu, menu_loc.x(), menu_loc.y()); 1396 part->type = MenuPart::MENU_ITEM; 1397 part->submenu = menu; 1398 if (!part->menu) 1399 part->parent = menu->GetMenuItem(); 1400 return true; 1401 } 1402 1403 // While the mouse isn't over a menu item or the scroll buttons of menu, it 1404 // is contained by menu and so we return true. If we didn't return true other 1405 // menus would be searched, even though they are likely obscured by us. 1406 return true; 1407} 1408 1409bool MenuController::DoesSubmenuContainLocation(SubmenuView* submenu, 1410 const gfx::Point& screen_loc) { 1411 gfx::Point view_loc = screen_loc; 1412 View::ConvertPointToTarget(NULL, submenu, &view_loc); 1413 gfx::Rect vis_rect = submenu->GetVisibleBounds(); 1414 return vis_rect.Contains(view_loc.x(), view_loc.y()); 1415} 1416 1417void MenuController::CommitPendingSelection() { 1418 StopShowTimer(); 1419 1420 size_t paths_differ_at = 0; 1421 std::vector<MenuItemView*> current_path; 1422 std::vector<MenuItemView*> new_path; 1423 BuildPathsAndCalculateDiff(state_.item, pending_state_.item, ¤t_path, 1424 &new_path, &paths_differ_at); 1425 1426 // Hide the old menu. 1427 for (size_t i = paths_differ_at; i < current_path.size(); ++i) { 1428 if (current_path[i]->HasSubmenu()) { 1429 current_path[i]->GetSubmenu()->Hide(); 1430 } 1431 } 1432 1433 // Copy pending to state_, making sure to preserve the direction menus were 1434 // opened. 1435 std::list<bool> pending_open_direction; 1436 state_.open_leading.swap(pending_open_direction); 1437 state_ = pending_state_; 1438 state_.open_leading.swap(pending_open_direction); 1439 1440 int menu_depth = MenuDepth(state_.item); 1441 if (menu_depth == 0) { 1442 state_.open_leading.clear(); 1443 } else { 1444 int cached_size = static_cast<int>(state_.open_leading.size()); 1445 DCHECK_GE(menu_depth, 0); 1446 while (cached_size-- >= menu_depth) 1447 state_.open_leading.pop_back(); 1448 } 1449 1450 if (!state_.item) { 1451 // Nothing to select. 1452 StopScrolling(); 1453 return; 1454 } 1455 1456 // Open all the submenus preceeding the last menu item (last menu item is 1457 // handled next). 1458 if (new_path.size() > 1) { 1459 for (std::vector<MenuItemView*>::iterator i = new_path.begin(); 1460 i != new_path.end() - 1; ++i) { 1461 OpenMenu(*i); 1462 } 1463 } 1464 1465 if (state_.submenu_open) { 1466 // The submenu should be open, open the submenu if the item has a submenu. 1467 if (state_.item->HasSubmenu()) { 1468 OpenMenu(state_.item); 1469 } else { 1470 state_.submenu_open = false; 1471 } 1472 } else if (state_.item->HasSubmenu() && 1473 state_.item->GetSubmenu()->IsShowing()) { 1474 state_.item->GetSubmenu()->Hide(); 1475 } 1476 1477 if (scroll_task_.get() && scroll_task_->submenu()) { 1478 // Stop the scrolling if none of the elements of the selection contain 1479 // the menu being scrolled. 1480 bool found = false; 1481 for (MenuItemView* item = state_.item; item && !found; 1482 item = item->GetParentMenuItem()) { 1483 found = (item->HasSubmenu() && item->GetSubmenu()->IsShowing() && 1484 item->GetSubmenu() == scroll_task_->submenu()); 1485 } 1486 if (!found) 1487 StopScrolling(); 1488 } 1489} 1490 1491void MenuController::CloseMenu(MenuItemView* item) { 1492 DCHECK(item); 1493 if (!item->HasSubmenu()) 1494 return; 1495 item->GetSubmenu()->Hide(); 1496} 1497 1498void MenuController::OpenMenu(MenuItemView* item) { 1499 DCHECK(item); 1500 if (item->GetSubmenu()->IsShowing()) { 1501 return; 1502 } 1503 1504 OpenMenuImpl(item, true); 1505 did_capture_ = true; 1506} 1507 1508void MenuController::OpenMenuImpl(MenuItemView* item, bool show) { 1509 // TODO(oshima|sky): Don't show the menu if drag is in progress and 1510 // this menu doesn't support drag drop. See crbug.com/110495. 1511 if (show) { 1512 int old_count = item->GetSubmenu()->child_count(); 1513 item->GetDelegate()->WillShowMenu(item); 1514 if (old_count != item->GetSubmenu()->child_count()) { 1515 // If the number of children changed then we may need to add empty items. 1516 item->AddEmptyMenus(); 1517 } 1518 } 1519 bool prefer_leading = 1520 state_.open_leading.empty() ? true : state_.open_leading.back(); 1521 bool resulting_direction; 1522 gfx::Rect bounds = MenuItemView::IsBubble(state_.anchor) ? 1523 CalculateBubbleMenuBounds(item, prefer_leading, &resulting_direction) : 1524 CalculateMenuBounds(item, prefer_leading, &resulting_direction); 1525 state_.open_leading.push_back(resulting_direction); 1526 bool do_capture = (!did_capture_ && blocking_run_); 1527 showing_submenu_ = true; 1528 if (show) 1529 item->GetSubmenu()->ShowAt(owner_, bounds, do_capture); 1530 else 1531 item->GetSubmenu()->Reposition(bounds); 1532 showing_submenu_ = false; 1533} 1534 1535void MenuController::MenuChildrenChanged(MenuItemView* item) { 1536 DCHECK(item); 1537 // Menu shouldn't be updated during drag operation. 1538 DCHECK(!active_mouse_view_); 1539 1540 // If the current item or pending item is a descendant of the item 1541 // that changed, move the selection back to the changed item. 1542 const MenuItemView* ancestor = state_.item; 1543 while (ancestor && ancestor != item) 1544 ancestor = ancestor->GetParentMenuItem(); 1545 if (!ancestor) { 1546 ancestor = pending_state_.item; 1547 while (ancestor && ancestor != item) 1548 ancestor = ancestor->GetParentMenuItem(); 1549 if (!ancestor) 1550 return; 1551 } 1552 SetSelection(item, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); 1553 if (item->HasSubmenu()) 1554 OpenMenuImpl(item, false); 1555} 1556 1557void MenuController::BuildPathsAndCalculateDiff( 1558 MenuItemView* old_item, 1559 MenuItemView* new_item, 1560 std::vector<MenuItemView*>* old_path, 1561 std::vector<MenuItemView*>* new_path, 1562 size_t* first_diff_at) { 1563 DCHECK(old_path && new_path && first_diff_at); 1564 BuildMenuItemPath(old_item, old_path); 1565 BuildMenuItemPath(new_item, new_path); 1566 1567 size_t common_size = std::min(old_path->size(), new_path->size()); 1568 1569 // Find the first difference between the two paths, when the loop 1570 // returns, diff_i is the first index where the two paths differ. 1571 for (size_t i = 0; i < common_size; ++i) { 1572 if ((*old_path)[i] != (*new_path)[i]) { 1573 *first_diff_at = i; 1574 return; 1575 } 1576 } 1577 1578 *first_diff_at = common_size; 1579} 1580 1581void MenuController::BuildMenuItemPath(MenuItemView* item, 1582 std::vector<MenuItemView*>* path) { 1583 if (!item) 1584 return; 1585 BuildMenuItemPath(item->GetParentMenuItem(), path); 1586 path->push_back(item); 1587} 1588 1589void MenuController::StartShowTimer() { 1590 show_timer_.Start(FROM_HERE, 1591 TimeDelta::FromMilliseconds(menu_config_.show_delay), 1592 this, &MenuController::CommitPendingSelection); 1593} 1594 1595void MenuController::StopShowTimer() { 1596 show_timer_.Stop(); 1597} 1598 1599void MenuController::StartCancelAllTimer() { 1600 cancel_all_timer_.Start(FROM_HERE, 1601 TimeDelta::FromMilliseconds(kCloseOnExitTime), 1602 this, &MenuController::CancelAll); 1603} 1604 1605void MenuController::StopCancelAllTimer() { 1606 cancel_all_timer_.Stop(); 1607} 1608 1609gfx::Rect MenuController::CalculateMenuBounds(MenuItemView* item, 1610 bool prefer_leading, 1611 bool* is_leading) { 1612 DCHECK(item); 1613 1614 SubmenuView* submenu = item->GetSubmenu(); 1615 DCHECK(submenu); 1616 1617 gfx::Size pref = submenu->GetScrollViewContainer()->GetPreferredSize(); 1618 1619 // Don't let the menu go too wide. 1620 pref.set_width(std::min(pref.width(), 1621 item->GetDelegate()->GetMaxWidthForMenu(item))); 1622 if (!state_.monitor_bounds.IsEmpty()) 1623 pref.set_width(std::min(pref.width(), state_.monitor_bounds.width())); 1624 1625 // Assume we can honor prefer_leading. 1626 *is_leading = prefer_leading; 1627 1628 int x, y; 1629 1630 const MenuConfig& menu_config = item->GetMenuConfig(); 1631 1632 if (!item->GetParentMenuItem()) { 1633 // First item, position relative to initial location. 1634 x = state_.initial_bounds.x(); 1635 1636 // Offsets for context menu prevent menu items being selected by 1637 // simply opening the menu (bug 142992). 1638 if (menu_config.offset_context_menus && state_.context_menu) 1639 x += 1; 1640 1641 y = state_.initial_bounds.bottom(); 1642 if (state_.anchor == MenuItemView::TOPRIGHT) { 1643 x = x + state_.initial_bounds.width() - pref.width(); 1644 if (menu_config.offset_context_menus && state_.context_menu) 1645 x -= 1; 1646 } else if (state_.anchor == MenuItemView::BOTTOMCENTER) { 1647 x = x - (pref.width() - state_.initial_bounds.width()) / 2; 1648 if (pref.height() > 1649 state_.initial_bounds.y() + kCenteredContextMenuYOffset) { 1650 // Menu does not fit above the anchor. We move it to below. 1651 y = state_.initial_bounds.y() - kCenteredContextMenuYOffset; 1652 } else { 1653 y = std::max(0, state_.initial_bounds.y() - pref.height()) + 1654 kCenteredContextMenuYOffset; 1655 } 1656 } 1657 1658 if (!state_.monitor_bounds.IsEmpty() && 1659 y + pref.height() > state_.monitor_bounds.bottom()) { 1660 // The menu doesn't fit fully below the button on the screen. The menu 1661 // position with respect to the bounds will be preserved if it has 1662 // already been drawn. When the requested positioning is below the bounds 1663 // it will shrink the menu to make it fit below. 1664 // If the requested positioning is best fit, it will first try to fit the 1665 // menu below. If that does not fit it will try to place it above. If 1666 // that will not fit it will place it at the bottom of the work area and 1667 // moving it off the initial_bounds region to avoid overlap. 1668 // In all other requested position styles it will be flipped above and 1669 // the height will be shrunken to the usable height. 1670 if (item->actual_menu_position() == MenuItemView::POSITION_BELOW_BOUNDS) { 1671 pref.set_height(std::min(pref.height(), 1672 state_.monitor_bounds.bottom() - y)); 1673 } else if (item->actual_menu_position() == 1674 MenuItemView::POSITION_BEST_FIT) { 1675 MenuItemView::MenuPosition orientation = 1676 MenuItemView::POSITION_BELOW_BOUNDS; 1677 if (state_.monitor_bounds.height() < pref.height()) { 1678 // Handle very tall menus. 1679 pref.set_height(state_.monitor_bounds.height()); 1680 y = state_.monitor_bounds.y(); 1681 } else if (state_.monitor_bounds.y() + pref.height() < 1682 state_.initial_bounds.y()) { 1683 // Flipping upwards if there is enough space. 1684 y = state_.initial_bounds.y() - pref.height(); 1685 orientation = MenuItemView::POSITION_ABOVE_BOUNDS; 1686 } else { 1687 // It is allowed to move the menu a bit around in order to get the 1688 // best fit and to avoid showing scroll elements. 1689 y = state_.monitor_bounds.bottom() - pref.height(); 1690 } 1691 if (orientation == MenuItemView::POSITION_BELOW_BOUNDS) { 1692 // The menu should never overlap the owning button. So move it. 1693 // We use the anchor view style to determine the preferred position 1694 // relative to the owning button. 1695 if (state_.anchor == MenuItemView::TOPLEFT) { 1696 // The menu starts with the same x coordinate as the owning button. 1697 if (x + state_.initial_bounds.width() + pref.width() > 1698 state_.monitor_bounds.right()) 1699 x -= pref.width(); // Move the menu to the left of the button. 1700 else 1701 x += state_.initial_bounds.width(); // Move the menu right. 1702 } else { 1703 // The menu should end with the same x coordinate as the owning 1704 // button. 1705 if (state_.monitor_bounds.x() > 1706 state_.initial_bounds.x() - pref.width()) 1707 x = state_.initial_bounds.right(); // Move right of the button. 1708 else 1709 x = state_.initial_bounds.x() - pref.width(); // Move left. 1710 } 1711 } 1712 item->set_actual_menu_position(orientation); 1713 } else { 1714 pref.set_height(std::min(pref.height(), 1715 state_.initial_bounds.y() - state_.monitor_bounds.y())); 1716 y = state_.initial_bounds.y() - pref.height(); 1717 item->set_actual_menu_position(MenuItemView::POSITION_ABOVE_BOUNDS); 1718 } 1719 } else if (item->actual_menu_position() == 1720 MenuItemView::POSITION_ABOVE_BOUNDS) { 1721 pref.set_height(std::min(pref.height(), 1722 state_.initial_bounds.y() - state_.monitor_bounds.y())); 1723 y = state_.initial_bounds.y() - pref.height(); 1724 } else { 1725 item->set_actual_menu_position(MenuItemView::POSITION_BELOW_BOUNDS); 1726 } 1727 if (state_.monitor_bounds.width() != 0 && 1728 menu_config.offset_context_menus && state_.context_menu) { 1729 if (x + pref.width() > state_.monitor_bounds.right()) 1730 x = state_.initial_bounds.x() - pref.width() - 1; 1731 if (x < state_.monitor_bounds.x()) 1732 x = state_.monitor_bounds.x(); 1733 } 1734 } else { 1735 // Not the first menu; position it relative to the bounds of the menu 1736 // item. 1737 gfx::Point item_loc; 1738 View::ConvertPointToScreen(item, &item_loc); 1739 1740 // We must make sure we take into account the UI layout. If the layout is 1741 // RTL, then a 'leading' menu is positioned to the left of the parent menu 1742 // item and not to the right. 1743 bool layout_is_rtl = base::i18n::IsRTL(); 1744 bool create_on_the_right = (prefer_leading && !layout_is_rtl) || 1745 (!prefer_leading && layout_is_rtl); 1746 int submenu_horizontal_inset = menu_config.submenu_horizontal_inset; 1747 1748 if (create_on_the_right) { 1749 x = item_loc.x() + item->width() - submenu_horizontal_inset; 1750 if (state_.monitor_bounds.width() != 0 && 1751 x + pref.width() > state_.monitor_bounds.right()) { 1752 if (layout_is_rtl) 1753 *is_leading = true; 1754 else 1755 *is_leading = false; 1756 x = item_loc.x() - pref.width() + submenu_horizontal_inset; 1757 } 1758 } else { 1759 x = item_loc.x() - pref.width() + submenu_horizontal_inset; 1760 if (state_.monitor_bounds.width() != 0 && x < state_.monitor_bounds.x()) { 1761 if (layout_is_rtl) 1762 *is_leading = false; 1763 else 1764 *is_leading = true; 1765 x = item_loc.x() + item->width() - submenu_horizontal_inset; 1766 } 1767 } 1768 y = item_loc.y() - menu_config.menu_vertical_border_size; 1769 if (state_.monitor_bounds.width() != 0) { 1770 pref.set_height(std::min(pref.height(), state_.monitor_bounds.height())); 1771 if (y + pref.height() > state_.monitor_bounds.bottom()) 1772 y = state_.monitor_bounds.bottom() - pref.height(); 1773 if (y < state_.monitor_bounds.y()) 1774 y = state_.monitor_bounds.y(); 1775 } 1776 } 1777 1778 if (state_.monitor_bounds.width() != 0) { 1779 if (x + pref.width() > state_.monitor_bounds.right()) 1780 x = state_.monitor_bounds.right() - pref.width(); 1781 if (x < state_.monitor_bounds.x()) 1782 x = state_.monitor_bounds.x(); 1783 } 1784 return gfx::Rect(x, y, pref.width(), pref.height()); 1785} 1786 1787gfx::Rect MenuController::CalculateBubbleMenuBounds(MenuItemView* item, 1788 bool prefer_leading, 1789 bool* is_leading) { 1790 DCHECK(item); 1791 DCHECK(!item->GetParentMenuItem()); 1792 1793 // Assume we can honor prefer_leading. 1794 *is_leading = prefer_leading; 1795 1796 SubmenuView* submenu = item->GetSubmenu(); 1797 DCHECK(submenu); 1798 1799 gfx::Size pref = submenu->GetScrollViewContainer()->GetPreferredSize(); 1800 const gfx::Rect& owner_bounds = pending_state_.initial_bounds; 1801 1802 // First the size gets reduced to the possible space. 1803 if (!state_.monitor_bounds.IsEmpty()) { 1804 int max_width = state_.monitor_bounds.width(); 1805 int max_height = state_.monitor_bounds.height(); 1806 // In case of bubbles, the maximum width is limited by the space 1807 // between the display corner and the target area + the tip size. 1808 if (state_.anchor == MenuItemView::BUBBLE_LEFT) { 1809 max_width = owner_bounds.x() - state_.monitor_bounds.x() + 1810 kBubbleTipSizeLeftRight; 1811 } else if (state_.anchor == MenuItemView::BUBBLE_RIGHT) { 1812 max_width = state_.monitor_bounds.right() - owner_bounds.right() + 1813 kBubbleTipSizeLeftRight; 1814 } else if (state_.anchor == MenuItemView::BUBBLE_ABOVE) { 1815 max_height = owner_bounds.y() - state_.monitor_bounds.y() + 1816 kBubbleTipSizeTopBottom; 1817 } else if (state_.anchor == MenuItemView::BUBBLE_BELOW) { 1818 max_height = state_.monitor_bounds.bottom() - owner_bounds.bottom() + 1819 kBubbleTipSizeTopBottom; 1820 } 1821 // The space for the menu to cover should never get empty. 1822 DCHECK_GE(max_width, kBubbleTipSizeLeftRight); 1823 DCHECK_GE(max_height, kBubbleTipSizeTopBottom); 1824 pref.set_width(std::min(pref.width(), max_width)); 1825 pref.set_height(std::min(pref.height(), max_height)); 1826 } 1827 // Also make sure that the menu does not go too wide. 1828 pref.set_width(std::min(pref.width(), 1829 item->GetDelegate()->GetMaxWidthForMenu(item))); 1830 1831 int x, y; 1832 if (state_.anchor == MenuItemView::BUBBLE_ABOVE || 1833 state_.anchor == MenuItemView::BUBBLE_BELOW) { 1834 if (state_.anchor == MenuItemView::BUBBLE_ABOVE) 1835 y = owner_bounds.y() - pref.height() + kBubbleTipSizeTopBottom; 1836 else 1837 y = owner_bounds.bottom() - kBubbleTipSizeTopBottom; 1838 1839 x = owner_bounds.CenterPoint().x() - pref.width() / 2; 1840 int x_old = x; 1841 if (x < state_.monitor_bounds.x()) { 1842 x = state_.monitor_bounds.x(); 1843 } else if (x + pref.width() > state_.monitor_bounds.right()) { 1844 x = state_.monitor_bounds.right() - pref.width(); 1845 } 1846 submenu->GetScrollViewContainer()->SetBubbleArrowOffset( 1847 pref.width() / 2 - x + x_old); 1848 } else { 1849 if (state_.anchor == MenuItemView::BUBBLE_RIGHT) 1850 x = owner_bounds.right() - kBubbleTipSizeLeftRight; 1851 else 1852 x = owner_bounds.x() - pref.width() + kBubbleTipSizeLeftRight; 1853 1854 y = owner_bounds.CenterPoint().y() - pref.height() / 2; 1855 int y_old = y; 1856 if (y < state_.monitor_bounds.y()) { 1857 y = state_.monitor_bounds.y(); 1858 } else if (y + pref.height() > state_.monitor_bounds.bottom()) { 1859 y = state_.monitor_bounds.bottom() - pref.height(); 1860 } 1861 submenu->GetScrollViewContainer()->SetBubbleArrowOffset( 1862 pref.height() / 2 - y + y_old); 1863 } 1864 return gfx::Rect(x, y, pref.width(), pref.height()); 1865} 1866 1867// static 1868int MenuController::MenuDepth(MenuItemView* item) { 1869 return item ? (MenuDepth(item->GetParentMenuItem()) + 1) : 0; 1870} 1871 1872void MenuController::IncrementSelection(int delta) { 1873 MenuItemView* item = pending_state_.item; 1874 DCHECK(item); 1875 if (pending_state_.submenu_open && item->HasSubmenu() && 1876 item->GetSubmenu()->IsShowing()) { 1877 // A menu is selected and open, but none of its children are selected, 1878 // select the first menu item. 1879 if (item->GetSubmenu()->GetMenuItemCount()) { 1880 SetSelection(item->GetSubmenu()->GetMenuItemAt(0), SELECTION_DEFAULT); 1881 ScrollToVisible(item->GetSubmenu()->GetMenuItemAt(0)); 1882 return; 1883 } 1884 } 1885 1886 if (item->has_children()) { 1887 View* hot_view = GetFirstHotTrackedView(item); 1888 if (hot_view && 1889 !strcmp(hot_view->GetClassName(), CustomButton::kViewClassName)) { 1890 CustomButton* button = static_cast<CustomButton*>(hot_view); 1891 button->SetHotTracked(false); 1892 View* to_make_hot = GetNextFocusableView(item, button, delta == 1); 1893 if (to_make_hot && 1894 !strcmp(to_make_hot->GetClassName(), CustomButton::kViewClassName)) { 1895 CustomButton* button_hot = static_cast<CustomButton*>(to_make_hot); 1896 button_hot->SetHotTracked(true); 1897 return; 1898 } 1899 } else { 1900 View* to_make_hot = GetInitialFocusableView(item, delta == 1); 1901 if (to_make_hot && 1902 !strcmp(to_make_hot->GetClassName(), CustomButton::kViewClassName)) { 1903 CustomButton* button_hot = static_cast<CustomButton*>(to_make_hot); 1904 button_hot->SetHotTracked(true); 1905 return; 1906 } 1907 } 1908 } 1909 1910 MenuItemView* parent = item->GetParentMenuItem(); 1911 if (parent) { 1912 int parent_count = parent->GetSubmenu()->GetMenuItemCount(); 1913 if (parent_count > 1) { 1914 for (int i = 0; i < parent_count; ++i) { 1915 if (parent->GetSubmenu()->GetMenuItemAt(i) == item) { 1916 MenuItemView* to_select = 1917 FindNextSelectableMenuItem(parent, i, delta); 1918 if (!to_select) 1919 break; 1920 ScrollToVisible(to_select); 1921 SetSelection(to_select, SELECTION_DEFAULT); 1922 View* to_make_hot = GetInitialFocusableView(to_select, delta == 1); 1923 if (to_make_hot && !strcmp(to_make_hot->GetClassName(), 1924 CustomButton::kViewClassName)) { 1925 CustomButton* button_hot = static_cast<CustomButton*>(to_make_hot); 1926 button_hot->SetHotTracked(true); 1927 } 1928 break; 1929 } 1930 } 1931 } 1932 } 1933} 1934 1935MenuItemView* MenuController::FindNextSelectableMenuItem(MenuItemView* parent, 1936 int index, 1937 int delta) { 1938 int start_index = index; 1939 int parent_count = parent->GetSubmenu()->GetMenuItemCount(); 1940 // Loop through the menu items skipping any invisible menus. The loop stops 1941 // when we wrap or find a visible child. 1942 do { 1943 index = (index + delta + parent_count) % parent_count; 1944 if (index == start_index) 1945 return NULL; 1946 MenuItemView* child = parent->GetSubmenu()->GetMenuItemAt(index); 1947 if (child->visible()) 1948 return child; 1949 } while (index != start_index); 1950 return NULL; 1951} 1952 1953void MenuController::OpenSubmenuChangeSelectionIfCan() { 1954 MenuItemView* item = pending_state_.item; 1955 if (item->HasSubmenu() && item->enabled()) { 1956 if (item->GetSubmenu()->GetMenuItemCount() > 0) { 1957 SetSelection(item->GetSubmenu()->GetMenuItemAt(0), 1958 SELECTION_UPDATE_IMMEDIATELY); 1959 } else { 1960 // No menu items, just show the sub-menu. 1961 SetSelection(item, SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); 1962 } 1963 } 1964} 1965 1966void MenuController::CloseSubmenu() { 1967 MenuItemView* item = state_.item; 1968 DCHECK(item); 1969 if (!item->GetParentMenuItem()) 1970 return; 1971 if (item->HasSubmenu() && item->GetSubmenu()->IsShowing()) 1972 SetSelection(item, SELECTION_UPDATE_IMMEDIATELY); 1973 else if (item->GetParentMenuItem()->GetParentMenuItem()) 1974 SetSelection(item->GetParentMenuItem(), SELECTION_UPDATE_IMMEDIATELY); 1975} 1976 1977MenuController::SelectByCharDetails MenuController::FindChildForMnemonic( 1978 MenuItemView* parent, 1979 char16 key, 1980 bool (*match_function)(MenuItemView* menu, char16 mnemonic)) { 1981 SubmenuView* submenu = parent->GetSubmenu(); 1982 DCHECK(submenu); 1983 SelectByCharDetails details; 1984 1985 for (int i = 0, menu_item_count = submenu->GetMenuItemCount(); 1986 i < menu_item_count; ++i) { 1987 MenuItemView* child = submenu->GetMenuItemAt(i); 1988 if (child->enabled() && child->visible()) { 1989 if (child == pending_state_.item) 1990 details.index_of_item = i; 1991 if (match_function(child, key)) { 1992 if (details.first_match == -1) 1993 details.first_match = i; 1994 else 1995 details.has_multiple = true; 1996 if (details.next_match == -1 && details.index_of_item != -1 && 1997 i > details.index_of_item) 1998 details.next_match = i; 1999 } 2000 } 2001 } 2002 return details; 2003} 2004 2005bool MenuController::AcceptOrSelect(MenuItemView* parent, 2006 const SelectByCharDetails& details) { 2007 // This should only be invoked if there is a match. 2008 DCHECK(details.first_match != -1); 2009 DCHECK(parent->HasSubmenu()); 2010 SubmenuView* submenu = parent->GetSubmenu(); 2011 DCHECK(submenu); 2012 if (!details.has_multiple) { 2013 // There's only one match, activate it (or open if it has a submenu). 2014 if (submenu->GetMenuItemAt(details.first_match)->HasSubmenu()) { 2015 SetSelection(submenu->GetMenuItemAt(details.first_match), 2016 SELECTION_OPEN_SUBMENU | SELECTION_UPDATE_IMMEDIATELY); 2017 } else { 2018 Accept(submenu->GetMenuItemAt(details.first_match), 0); 2019 return true; 2020 } 2021 } else if (details.index_of_item == -1 || details.next_match == -1) { 2022 SetSelection(submenu->GetMenuItemAt(details.first_match), 2023 SELECTION_DEFAULT); 2024 } else { 2025 SetSelection(submenu->GetMenuItemAt(details.next_match), 2026 SELECTION_DEFAULT); 2027 } 2028 return false; 2029} 2030 2031bool MenuController::SelectByChar(char16 character) { 2032 char16 char_array[] = { character, 0 }; 2033 char16 key = base::i18n::ToLower(char_array)[0]; 2034 MenuItemView* item = pending_state_.item; 2035 if (!item->HasSubmenu() || !item->GetSubmenu()->IsShowing()) 2036 item = item->GetParentMenuItem(); 2037 DCHECK(item); 2038 DCHECK(item->HasSubmenu()); 2039 DCHECK(item->GetSubmenu()); 2040 if (item->GetSubmenu()->GetMenuItemCount() == 0) 2041 return false; 2042 2043 // Look for matches based on mnemonic first. 2044 SelectByCharDetails details = 2045 FindChildForMnemonic(item, key, &MatchesMnemonic); 2046 if (details.first_match != -1) 2047 return AcceptOrSelect(item, details); 2048 2049 // If no mnemonics found, look at first character of titles. 2050 details = FindChildForMnemonic(item, key, &TitleMatchesMnemonic); 2051 if (details.first_match != -1) 2052 return AcceptOrSelect(item, details); 2053 2054 return false; 2055} 2056 2057#if defined(OS_WIN) 2058void MenuController::RepostEvent(SubmenuView* source, 2059 const ui::LocatedEvent& event) { 2060 if (!state_.item) { 2061 // We some times get an event after closing all the menus. Ignore it. 2062 // Make sure the menu is in fact not visible. If the menu is visible, then 2063 // we're in a bad state where we think the menu isn't visibile but it is. 2064 DCHECK(!source->GetWidget()->IsVisible()); 2065 return; 2066 } 2067 2068 gfx::Point screen_loc(event.location()); 2069 View::ConvertPointToScreen(source->GetScrollViewContainer(), &screen_loc); 2070 HWND window = WindowFromPoint(screen_loc.ToPOINT()); 2071 if (window) { 2072 // Release the capture. 2073 SubmenuView* submenu = state_.item->GetRootMenuItem()->GetSubmenu(); 2074 submenu->ReleaseCapture(); 2075 2076 if (submenu->GetWidget()->GetNativeView() && 2077 GetWindowThreadProcessId( 2078 views::HWNDForNativeView(submenu->GetWidget()->GetNativeView()), 2079 NULL) != GetWindowThreadProcessId(window, NULL)) { 2080 // Even though we have mouse capture, windows generates a mouse event 2081 // if the other window is in a separate thread. Don't generate an event in 2082 // this case else the target window can get double events leading to bad 2083 // behavior. 2084 return; 2085 } 2086 2087 // Convert the coordinates to the target window. 2088 RECT window_bounds; 2089 GetWindowRect(window, &window_bounds); 2090 int window_x = screen_loc.x() - window_bounds.left; 2091 int window_y = screen_loc.y() - window_bounds.top; 2092 2093 // Determine whether the click was in the client area or not. 2094 // NOTE: WM_NCHITTEST coordinates are relative to the screen. 2095 LRESULT nc_hit_result = SendMessage(window, WM_NCHITTEST, 0, 2096 MAKELPARAM(screen_loc.x(), 2097 screen_loc.y())); 2098 const bool in_client_area = (nc_hit_result == HTCLIENT); 2099 2100 // TODO(sky): this isn't right. The event to generate should correspond 2101 // with the event we just got. MouseEvent only tells us what is down, 2102 // which may differ. Need to add ability to get changed button from 2103 // MouseEvent. 2104 int event_type; 2105 int flags = event.flags(); 2106 if (flags & ui::EF_LEFT_MOUSE_BUTTON) 2107 event_type = in_client_area ? WM_LBUTTONDOWN : WM_NCLBUTTONDOWN; 2108 else if (flags & ui::EF_MIDDLE_MOUSE_BUTTON) 2109 event_type = in_client_area ? WM_MBUTTONDOWN : WM_NCMBUTTONDOWN; 2110 else if (flags & ui::EF_RIGHT_MOUSE_BUTTON) 2111 event_type = in_client_area ? WM_RBUTTONDOWN : WM_NCRBUTTONDOWN; 2112 else 2113 event_type = 0; // Unknown mouse press. 2114 2115 if (event_type) { 2116 if (in_client_area) { 2117 PostMessage(window, event_type, event.native_event().wParam, 2118 MAKELPARAM(window_x, window_y)); 2119 } else { 2120 PostMessage(window, event_type, nc_hit_result, 2121 MAKELPARAM(screen_loc.x(), screen_loc.y())); 2122 } 2123 } 2124 } 2125} 2126#elif defined(USE_AURA) 2127void MenuController::RepostEvent(SubmenuView* source, 2128 const ui::LocatedEvent& event) { 2129 aura::RootWindow* root_window = 2130 source->GetWidget()->GetNativeWindow()->GetRootWindow(); 2131 DCHECK(root_window); 2132 root_window->RepostEvent(event); 2133} 2134#endif 2135 2136void MenuController::SetDropMenuItem( 2137 MenuItemView* new_target, 2138 MenuDelegate::DropPosition new_position) { 2139 if (new_target == drop_target_ && new_position == drop_position_) 2140 return; 2141 2142 if (drop_target_) { 2143 drop_target_->GetParentMenuItem()->GetSubmenu()->SetDropMenuItem( 2144 NULL, MenuDelegate::DROP_NONE); 2145 } 2146 drop_target_ = new_target; 2147 drop_position_ = new_position; 2148 if (drop_target_) { 2149 drop_target_->GetParentMenuItem()->GetSubmenu()->SetDropMenuItem( 2150 drop_target_, drop_position_); 2151 } 2152} 2153 2154void MenuController::UpdateScrolling(const MenuPart& part) { 2155 if (!part.is_scroll() && !scroll_task_.get()) 2156 return; 2157 2158 if (!scroll_task_.get()) 2159 scroll_task_.reset(new MenuScrollTask()); 2160 scroll_task_->Update(part); 2161} 2162 2163void MenuController::StopScrolling() { 2164 scroll_task_.reset(NULL); 2165} 2166 2167void MenuController::UpdateActiveMouseView(SubmenuView* event_source, 2168 const ui::MouseEvent& event, 2169 View* target_menu) { 2170 View* target = NULL; 2171 gfx::Point target_menu_loc(event.location()); 2172 if (target_menu && target_menu->has_children()) { 2173 // Locate the deepest child view to send events to. This code assumes we 2174 // don't have to walk up the tree to find a view interested in events. This 2175 // is currently true for the cases we are embedding views, but if we embed 2176 // more complex hierarchies it'll need to change. 2177 View::ConvertPointToScreen(event_source->GetScrollViewContainer(), 2178 &target_menu_loc); 2179 View::ConvertPointToTarget(NULL, target_menu, &target_menu_loc); 2180 target = target_menu->GetEventHandlerForPoint(target_menu_loc); 2181 if (target == target_menu || !target->enabled()) 2182 target = NULL; 2183 } 2184 if (target != active_mouse_view_) { 2185 SendMouseCaptureLostToActiveView(); 2186 active_mouse_view_ = target; 2187 if (active_mouse_view_) { 2188 gfx::Point target_point(target_menu_loc); 2189 View::ConvertPointToTarget( 2190 target_menu, active_mouse_view_, &target_point); 2191 ui::MouseEvent mouse_entered_event(ui::ET_MOUSE_ENTERED, 2192 target_point, target_point, 2193 0); 2194 active_mouse_view_->OnMouseEntered(mouse_entered_event); 2195 2196 ui::MouseEvent mouse_pressed_event(ui::ET_MOUSE_PRESSED, 2197 target_point, target_point, 2198 event.flags()); 2199 active_mouse_view_->OnMousePressed(mouse_pressed_event); 2200 } 2201 } 2202 2203 if (active_mouse_view_) { 2204 gfx::Point target_point(target_menu_loc); 2205 View::ConvertPointToTarget(target_menu, active_mouse_view_, &target_point); 2206 ui::MouseEvent mouse_dragged_event(ui::ET_MOUSE_DRAGGED, 2207 target_point, target_point, 2208 event.flags()); 2209 active_mouse_view_->OnMouseDragged(mouse_dragged_event); 2210 } 2211} 2212 2213void MenuController::SendMouseReleaseToActiveView(SubmenuView* event_source, 2214 const ui::MouseEvent& event) { 2215 if (!active_mouse_view_) 2216 return; 2217 2218 gfx::Point target_loc(event.location()); 2219 View::ConvertPointToScreen(event_source->GetScrollViewContainer(), 2220 &target_loc); 2221 View::ConvertPointToTarget(NULL, active_mouse_view_, &target_loc); 2222 ui::MouseEvent release_event(ui::ET_MOUSE_RELEASED, 2223 target_loc, target_loc, 2224 event.flags()); 2225 // Reset the active_mouse_view_ before sending mouse released. That way if it 2226 // calls back to us, we aren't in a weird state. 2227 View* active_view = active_mouse_view_; 2228 active_mouse_view_ = NULL; 2229 active_view->OnMouseReleased(release_event); 2230} 2231 2232void MenuController::SendMouseCaptureLostToActiveView() { 2233 if (!active_mouse_view_) 2234 return; 2235 2236 // Reset the active_mouse_view_ before sending mouse capture lost. That way if 2237 // it calls back to us, we aren't in a weird state. 2238 View* active_view = active_mouse_view_; 2239 active_mouse_view_ = NULL; 2240 active_view->OnMouseCaptureLost(); 2241} 2242 2243void MenuController::SetExitType(ExitType type) { 2244 exit_type_ = type; 2245 // Exit nested message loops as soon as possible. We do this as 2246 // MessageLoop::Dispatcher is only invoked before native events, which means 2247 // its entirely possible for a Widget::CloseNow() task to be processed before 2248 // the next native message. By using QuitNow() we ensures the nested message 2249 // loop returns as soon as possible and avoids having deleted views classes 2250 // (such as widgets and rootviews) on the stack when the nested message loop 2251 // stops. 2252 // 2253 // It's safe to invoke QuitNow multiple times, it only effects the current 2254 // loop. 2255 bool quit_now = ShouldQuitNow() && exit_type_ != EXIT_NONE && 2256 message_loop_depth_; 2257 2258 if (quit_now) 2259 base::MessageLoop::current()->QuitNow(); 2260} 2261 2262void MenuController::HandleMouseLocation(SubmenuView* source, 2263 const gfx::Point& mouse_location) { 2264 if (showing_submenu_) 2265 return; 2266 2267 // Ignore mouse events if we're closing the menu. 2268 if (exit_type_ != EXIT_NONE) 2269 return; 2270 2271 MenuPart part = GetMenuPart(source, mouse_location); 2272 2273 UpdateScrolling(part); 2274 2275 if (!blocking_run_) 2276 return; 2277 2278 if (part.type == MenuPart::NONE && ShowSiblingMenu(source, mouse_location)) 2279 return; 2280 2281 if (part.type == MenuPart::MENU_ITEM && part.menu) { 2282 SetSelection(part.menu, SELECTION_OPEN_SUBMENU); 2283 } else if (!part.is_scroll() && pending_state_.item && 2284 pending_state_.item->GetParentMenuItem() && 2285 (!pending_state_.item->HasSubmenu() || 2286 !pending_state_.item->GetSubmenu()->IsShowing())) { 2287 // On exit if the user hasn't selected an item with a submenu, move the 2288 // selection back to the parent menu item. 2289 SetSelection(pending_state_.item->GetParentMenuItem(), 2290 SELECTION_OPEN_SUBMENU); 2291 } 2292} 2293 2294} // namespace views 2295