diff options
40 files changed, 807 insertions, 52 deletions
diff --git a/chrome/app/chrome_dll.rc b/chrome/app/chrome_dll.rc index 7ed9edf..14cee6d 100644 --- a/chrome/app/chrome_dll.rc +++ b/chrome/app/chrome_dll.rc @@ -48,6 +48,8 @@ BEGIN VK_F6, IDC_FOCUS_LOCATION, VIRTKEY "D", IDC_FOCUS_LOCATION, VIRTKEY, ALT "L", IDC_FOCUS_LOCATION, VIRTKEY, CONTROL + VK_F10, IDC_FOCUS_MENU_BAR, VIRTKEY + VK_MENU, IDC_FOCUS_MENU_BAR, VIRTKEY "K", IDC_FOCUS_SEARCH, VIRTKEY, CONTROL "E", IDC_FOCUS_SEARCH, VIRTKEY, CONTROL "T", IDC_FOCUS_TOOLBAR, VIRTKEY, SHIFT, ALT diff --git a/chrome/app/chrome_dll_resource.h b/chrome/app/chrome_dll_resource.h index 1d5caff..9a3f282 100644 --- a/chrome/app/chrome_dll_resource.h +++ b/chrome/app/chrome_dll_resource.h @@ -189,6 +189,7 @@ #define IDC_FOCUS_TOOLBAR 39000 #define IDC_FOCUS_LOCATION 39001 #define IDC_FOCUS_SEARCH 39002 +#define IDC_FOCUS_MENU_BAR 39003 // Show various bits of UI #define IDC_OPEN_FILE 40000 diff --git a/chrome/browser/automation/automation_provider.cc b/chrome/browser/automation/automation_provider.cc index a2b7d8d..d8bfa76 100644 --- a/chrome/browser/automation/automation_provider.cc +++ b/chrome/browser/automation/automation_provider.cc @@ -86,6 +86,11 @@ #include "chrome/browser/automation/ui_controls.h" #endif // !defined(OS_MACOSX) +#if defined(TOOLKIT_VIEWS) +#include "views/focus/focus_manager.h" +#include "views/view.h" +#endif // defined(TOOLKIT_VIEWS) + using base::Time; class AutomationInterstitialPage : public InterstitialPage { @@ -345,7 +350,11 @@ void AutomationProvider::OnMessageReceived(const IPC::Message& message) { IPC_MESSAGE_HANDLER(AutomationMsg_ConstrainedWindowCount, GetConstrainedWindowCount) IPC_MESSAGE_HANDLER(AutomationMsg_FindInPage, HandleFindInPageRequest) +#if defined(TOOLKIT_VIEWS) IPC_MESSAGE_HANDLER(AutomationMsg_GetFocusedViewID, GetFocusedViewID) + IPC_MESSAGE_HANDLER_DELAY_REPLY(AutomationMsg_WaitForFocusedViewIDToChange, + WaitForFocusedViewIDToChange) +#endif IPC_MESSAGE_HANDLER_DELAY_REPLY(AutomationMsg_InspectElement, HandleInspectElementRequest) IPC_MESSAGE_HANDLER(AutomationMsg_DownloadDirectory, GetDownloadDirectory) @@ -497,6 +506,7 @@ void AutomationProvider::OnMessageReceived(const IPC::Message& message) { IPC_MESSAGE_HANDLER_DELAY_REPLY(AutomationMsg_LoginWithUserAndPass, LoginWithUserAndPass) #endif + IPC_MESSAGE_HANDLER(AutomationMsg_IsPopUpMenuOpen, IsPopUpMenuOpen) IPC_END_MESSAGE_MAP() } @@ -2528,3 +2538,99 @@ void AutomationProvider::SetContentSetting( *success = true; } } + +#if defined(TOOLKIT_VIEWS) +void AutomationProvider::GetFocusedViewID(int handle, int* view_id) { + *view_id = -1; + if (window_tracker_->ContainsHandle(handle)) { + gfx::NativeWindow window = window_tracker_->GetResource(handle); + views::FocusManager* focus_manager = + views::FocusManager::GetFocusManagerForNativeWindow(window); + DCHECK(focus_manager); + views::View* focused_view = focus_manager->GetFocusedView(); + if (focused_view) + *view_id = focused_view->GetID(); + } +} + +// Helper class that waits until the focus has changed to a view other +// than the one with the provided view id. +class ViewFocusChangeWaiter : public views::FocusChangeListener { + public: + ViewFocusChangeWaiter(views::FocusManager* focus_manager, + int previous_view_id, + AutomationProvider* automation, + IPC::Message* reply_message) + : focus_manager_(focus_manager), + previous_view_id_(previous_view_id), + automation_(automation), + reply_message_(reply_message), + ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)) { + focus_manager_->AddFocusChangeListener(this); + // Call the focus change notification once in case the focus has + // already changed. + FocusWillChange(NULL, focus_manager_->GetFocusedView()); + } + + ~ViewFocusChangeWaiter() { + focus_manager_->RemoveFocusChangeListener(this); + } + + // Inherited from FocusChangeListener + virtual void FocusWillChange(views::View* focused_before, + views::View* focused_now) { + // This listener is called before focus actually changes. Post a task + // that will get run after focus changes. + MessageLoop::current()->PostTask( + FROM_HERE, + method_factory_.NewRunnableMethod( + &ViewFocusChangeWaiter::FocusChanged, + focused_before, + focused_now)); + } + + private: + void FocusChanged(views::View* focused_before, + views::View* focused_now) { + if (focused_now && focused_now->GetID() != previous_view_id_) { + AutomationMsg_WaitForFocusedViewIDToChange::WriteReplyParams( + reply_message_, true, focused_now->GetID()); + + automation_->Send(reply_message_); + delete this; + } + } + + views::FocusManager* focus_manager_; + int previous_view_id_; + AutomationProvider* automation_; + IPC::Message* reply_message_; + ScopedRunnableMethodFactory<ViewFocusChangeWaiter> method_factory_; + + DISALLOW_COPY_AND_ASSIGN(ViewFocusChangeWaiter); +}; + +void AutomationProvider::WaitForFocusedViewIDToChange( + int handle, int previous_view_id, IPC::Message* reply_message) { + if (!window_tracker_->ContainsHandle(handle)) + return; + gfx::NativeWindow window = window_tracker_->GetResource(handle); + views::FocusManager* focus_manager = + views::FocusManager::GetFocusManagerForNativeWindow(window); + + // The waiter will respond to the IPC and delete itself when done. + new ViewFocusChangeWaiter(focus_manager, + previous_view_id, + this, + reply_message); +} +#else +void AutomationProvider::GetFocusedViewID(int handle, int* view_id) { + NOTIMPLEMENTED(); +}; + +void AutomationProvider::WaitForFocusedViewIDToChange( + int handle, int previous_view_id, IPC::Message* reply_message) { + NOTIMPLEMENTED(); +} +#endif // defined(TOOLKIT_VIEWS) diff --git a/chrome/browser/automation/automation_provider.h b/chrome/browser/automation/automation_provider.h index e5b36bf..29aba68 100644 --- a/chrome/browser/automation/automation_provider.h +++ b/chrome/browser/automation/automation_provider.h @@ -247,6 +247,9 @@ class AutomationProvider : public base::RefCounted<AutomationProvider>, #endif // defined(OS_WIN) void GetFocusedViewID(int handle, int* view_id); + void WaitForFocusedViewIDToChange(int handle, + int previous_view_id, + IPC::Message* reply_message); // Helper function to find the browser window that contains a given // NavigationController and activate that tab. @@ -579,6 +582,10 @@ class AutomationProvider : public base::RefCounted<AutomationProvider>, // Returns NULL on failure. RenderViewHost* GetViewForTab(int tab_handle); + // Returns true if any popup menu is open. Currently only fully implemented + // on Windows and doesn't support bookmark menus. + void IsPopUpMenuOpen(int handle, bool* success, bool* is_open); + typedef ObserverList<NotificationObserver> NotificationObserverList; typedef std::map<NavigationController*, LoginHandler*> LoginHandlerMap; typedef std::map<int, ExtensionPortContainer*> PortContainerMap; diff --git a/chrome/browser/automation/automation_provider_gtk.cc b/chrome/browser/automation/automation_provider_gtk.cc index 601e81a..c617f3f 100644 --- a/chrome/browser/automation/automation_provider_gtk.cc +++ b/chrome/browser/automation/automation_provider_gtk.cc @@ -79,10 +79,6 @@ void AutomationProvider::IsWindowMaximized(int handle, bool* is_maximized, NOTIMPLEMENTED(); } -void AutomationProvider::GetFocusedViewID(int handle, int* view_id) { - NOTIMPLEMENTED(); -} - void AutomationProvider::PrintAsync(int tab_handle) { NOTIMPLEMENTED(); } @@ -228,3 +224,11 @@ void AutomationProvider::GetWindowTitle(int handle, string16* text) { text->assign(UTF8ToUTF16(title)); } +void AutomationProvider::IsPopUpMenuOpen( + int handle, bool* success, bool* is_open) { + *success = true; + *is_open = true; + // A GTK implementation of this method is not currently needed; GTK menus + // pop up immediately so there's no need to wait until they appear. + NOTIMPLEMENTED(); +} diff --git a/chrome/browser/automation/automation_provider_mac.mm b/chrome/browser/automation/automation_provider_mac.mm index f58b7a8..08a9575 100644 --- a/chrome/browser/automation/automation_provider_mac.mm +++ b/chrome/browser/automation/automation_provider_mac.mm @@ -66,10 +66,6 @@ void AutomationProvider::IsWindowMaximized(int handle, bool* is_maximized, NOTIMPLEMENTED(); } -void AutomationProvider::GetFocusedViewID(int handle, int* view_id) { - NOTIMPLEMENTED(); -} - void AutomationProvider::PrintAsync(int tab_handle) { NOTIMPLEMENTED(); } @@ -159,3 +155,9 @@ void AutomationProvider::GetWindowTitle(int handle, string16* text) { text->assign(base::SysNSStringToUTF16(title)); } +void AutomationProvider::IsPopUpMenuOpen( + int handle, bool* success, bool* is_open) { + *success = false; + *is_open = false; + NOTIMPLEMENTED(); +} diff --git a/chrome/browser/automation/automation_provider_views.cc b/chrome/browser/automation/automation_provider_views.cc index f161105..a827b06 100644 --- a/chrome/browser/automation/automation_provider_views.cc +++ b/chrome/browser/automation/automation_provider_views.cc @@ -33,4 +33,3 @@ void AutomationProvider::WindowGetViewBounds(int handle, int view_id, } } } - diff --git a/chrome/browser/automation/automation_provider_win.cc b/chrome/browser/automation/automation_provider_win.cc index 9539498..9474e90 100644 --- a/chrome/browser/automation/automation_provider_win.cc +++ b/chrome/browser/automation/automation_provider_win.cc @@ -231,19 +231,6 @@ void AutomationProvider::WindowSimulateDrag(int handle, } } -void AutomationProvider::GetFocusedViewID(int handle, int* view_id) { - *view_id = -1; - if (window_tracker_->ContainsHandle(handle)) { - HWND hwnd = window_tracker_->GetResource(handle); - views::FocusManager* focus_manager = - views::FocusManager::GetFocusManagerForNativeView(hwnd); - DCHECK(focus_manager); - views::View* focused_view = focus_manager->GetFocusedView(); - if (focused_view) - *view_id = focused_view->GetID(); - } -} - void AutomationProvider::GetWindowBounds(int handle, gfx::Rect* bounds, bool* success) { *success = false; @@ -506,3 +493,13 @@ void AutomationProvider::GetWindowTitle(int handle, string16* text) { ::GetWindowText(window, WriteInto(&result, length), length); text->assign(WideToUTF16(result)); } + +void AutomationProvider::IsPopUpMenuOpen( + int handle, bool* success, bool* is_open) { + *success = true; + + // Check for the existence of a pop-up menu using its + // window class (#32768). Note that this won't cover + // bookmark menus. + *is_open = (::FindWindow(L"#32768", 0) != NULL); +} diff --git a/chrome/browser/automation/ui_controls_win.cc b/chrome/browser/automation/ui_controls_win.cc index 3ff1a5c..aba7248 100644 --- a/chrome/browser/automation/ui_controls_win.cc +++ b/chrome/browser/automation/ui_controls_win.cc @@ -166,6 +166,21 @@ bool SendKeyPressImpl(base::KeyboardCode key, scoped_refptr<InputDispatcher> dispatcher( task ? new InputDispatcher(task, WM_KEYUP) : NULL); + // If a pop-up menu is open, it won't receive events sent using SendInput. + // Check for a pop-up menu using its window class (#32768) and if one + // exists, send the key event directly there. + HWND popup_menu = ::FindWindow(L"#32768", 0); + if (popup_menu != NULL) { + WPARAM w_param = win_util::KeyboardCodeToWin(key); + LPARAM l_param = 0; + ::SendMessage(popup_menu, WM_KEYDOWN, w_param, l_param); + ::SendMessage(popup_menu, WM_KEYUP, w_param, l_param); + + if (dispatcher.get()) + dispatcher->AddRef(); + return true; + } + INPUT input[8] = { 0 }; // 8, assuming all the modifiers are activated int i = 0; diff --git a/chrome/browser/browser.cc b/chrome/browser/browser.cc index f2779af..3ddc3a8 100644 --- a/chrome/browser/browser.cc +++ b/chrome/browser/browser.cc @@ -873,6 +873,9 @@ void Browser::UpdateCommandsForFullscreenMode(bool is_fullscreen) { command_updater_.UpdateCommandEnabled(IDC_FOCUS_TOOLBAR, show_main_ui); command_updater_.UpdateCommandEnabled(IDC_FOCUS_LOCATION, show_main_ui); command_updater_.UpdateCommandEnabled(IDC_FOCUS_SEARCH, show_main_ui); + command_updater_.UpdateCommandEnabled( + IDC_FOCUS_MENU_BAR, + show_main_ui && !is_fullscreen && (type() & TYPE_POPUP) == 0); // Show various bits of UI command_updater_.UpdateCommandEnabled(IDC_DEVELOPER_MENU, show_main_ui); @@ -1306,6 +1309,11 @@ void Browser::FocusToolbar() { window_->FocusToolbar(); } +void Browser::FocusPageAndAppMenus() { + UserMetrics::RecordAction("EnterMenuBarEmulationMode", profile_); + window_->FocusPageAndAppMenus(); +} + void Browser::FocusLocationBar() { UserMetrics::RecordAction("FocusLocation", profile_); window_->SetFocusToLocationBar(); @@ -1729,6 +1737,7 @@ void Browser::ExecuteCommandWithDisposition( case IDC_FOCUS_TOOLBAR: FocusToolbar(); break; case IDC_FOCUS_LOCATION: FocusLocationBar(); break; case IDC_FOCUS_SEARCH: FocusSearch(); break; + case IDC_FOCUS_MENU_BAR: FocusPageAndAppMenus(); break; // Show various bits of UI case IDC_OPEN_FILE: OpenFile(); break; diff --git a/chrome/browser/browser.h b/chrome/browser/browser.h index 51470f7..fad8295 100644 --- a/chrome/browser/browser.h +++ b/chrome/browser/browser.h @@ -460,6 +460,7 @@ class Browser : public TabStripModelDelegate, void FocusToolbar(); void FocusLocationBar(); void FocusSearch(); + void FocusPageAndAppMenus(); // Show various bits of UI void OpenFile(); diff --git a/chrome/browser/browser_window.h b/chrome/browser/browser_window.h index a4ff572..c2e3911 100644 --- a/chrome/browser/browser_window.h +++ b/chrome/browser/browser_window.h @@ -150,6 +150,11 @@ class BrowserWindow { // Focuses the toolbar (for accessibility). virtual void FocusToolbar() = 0; + // Focuses the page and app menus like they were a menu bar. + // + // Not used on the Mac, which has a "normal" menu bar. + virtual void FocusPageAndAppMenus() = 0; + // Returns whether the bookmark bar is visible or not. virtual bool IsBookmarkBarVisible() const = 0; diff --git a/chrome/browser/cocoa/browser_window_cocoa.h b/chrome/browser/cocoa/browser_window_cocoa.h index b0ace80..4de884a 100644 --- a/chrome/browser/cocoa/browser_window_cocoa.h +++ b/chrome/browser/cocoa/browser_window_cocoa.h @@ -57,6 +57,7 @@ class BrowserWindowCocoa : public BrowserWindow, virtual void UpdateToolbar(TabContents* contents, bool should_restore_state); virtual void FocusToolbar(); + virtual void FocusPageAndAppMenus(); virtual bool IsBookmarkBarVisible() const; virtual bool IsBookmarkBarAnimating() const; virtual bool IsToolbarVisible() const; diff --git a/chrome/browser/cocoa/browser_window_cocoa.mm b/chrome/browser/cocoa/browser_window_cocoa.mm index 01d592e..3b9ac25 100644 --- a/chrome/browser/cocoa/browser_window_cocoa.mm +++ b/chrome/browser/cocoa/browser_window_cocoa.mm @@ -237,6 +237,10 @@ void BrowserWindowCocoa::FocusToolbar() { NOTIMPLEMENTED(); } +void BrowserWindowCocoa::FocusPageAndAppMenus(); + // Chrome uses the standard Mac OS X menu bar, so this isn't needed. +} + bool BrowserWindowCocoa::IsBookmarkBarVisible() const { return browser_->profile()->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar); } diff --git a/chrome/browser/gtk/browser_window_gtk.cc b/chrome/browser/gtk/browser_window_gtk.cc index e0d60adf..fa63d58 100644 --- a/chrome/browser/gtk/browser_window_gtk.cc +++ b/chrome/browser/gtk/browser_window_gtk.cc @@ -783,6 +783,10 @@ void BrowserWindowGtk::FocusToolbar() { NOTIMPLEMENTED(); } +void BrowserWindowGtk::FocusPageAndAppMenus() { + NOTIMPLEMENTED(); +} + bool BrowserWindowGtk::IsBookmarkBarVisible() const { return browser_->SupportsWindowFeature(Browser::FEATURE_BOOKMARKBAR) && bookmark_bar_.get(); diff --git a/chrome/browser/gtk/browser_window_gtk.h b/chrome/browser/gtk/browser_window_gtk.h index f24e094..67613b6 100644 --- a/chrome/browser/gtk/browser_window_gtk.h +++ b/chrome/browser/gtk/browser_window_gtk.h @@ -75,6 +75,7 @@ class BrowserWindowGtk : public BrowserWindow, virtual void UpdateToolbar(TabContents* contents, bool should_restore_state); virtual void FocusToolbar(); + virtual void FocusPageAndAppMenus(); virtual bool IsBookmarkBarVisible() const; virtual bool IsBookmarkBarAnimating() const; virtual bool IsToolbarVisible() const; diff --git a/chrome/browser/views/accelerator_table_gtk.cc b/chrome/browser/views/accelerator_table_gtk.cc index 28a630b..f3a7a54 100644 --- a/chrome/browser/views/accelerator_table_gtk.cc +++ b/chrome/browser/views/accelerator_table_gtk.cc @@ -20,6 +20,8 @@ const AcceleratorMapping kAcceleratorMap[] = { { base::VKEY_L, false, true, false, IDC_FOCUS_LOCATION }, { base::VKEY_D, false, false, true, IDC_FOCUS_LOCATION }, { base::VKEY_F6, false, false, false, IDC_FOCUS_LOCATION }, + { base::VKEY_F10, false, false, false, IDC_FOCUS_MENU_BAR }, + { base::VKEY_MENU, false, false, false, IDC_FOCUS_MENU_BAR }, // Tab/window controls. { base::VKEY_T, false, true, false, IDC_NEW_TAB }, diff --git a/chrome/browser/views/frame/browser_view.cc b/chrome/browser/views/frame/browser_view.cc index d484765..328b954 100644 --- a/chrome/browser/views/frame/browser_view.cc +++ b/chrome/browser/views/frame/browser_view.cc @@ -638,10 +638,10 @@ void BrowserView::TraverseNextAccessibleToolbar(bool forward) { // TODO(mohamed) This needs to be smart, that applies to all toolbars. // Currently it just traverses between bookmarks and toolbar. if (!forward && toolbar_->IsVisible() && toolbar_->IsEnabled()) { - toolbar_->InitiateTraversal(last_focused_view_storage_id_); + toolbar_->RequestFocus(); } else if (forward && bookmark_bar_view_->IsVisible() && bookmark_bar_view_->IsEnabled()) { - bookmark_bar_view_->InitiateTraversal(last_focused_view_storage_id_); + bookmark_bar_view_->RequestFocus(); } } @@ -863,17 +863,31 @@ void BrowserView::UpdateToolbar(TabContents* contents, } void BrowserView::FocusToolbar() { - // Remove existing views in the storage, traversal should be restarted. + // Start the traversal within the main toolbar, passing it the storage id + // of the view where focus should be returned if the user exits the toolbar. + SaveFocusedView(); + toolbar_->InitiateTraversal(last_focused_view_storage_id_); +} + +void BrowserView::FocusPageAndAppMenus() { + // Chrome doesn't have a traditional menu bar, but it has menu buttons in + // the main toolbar that play the same role. If the user presses a key + // that would typically focus the menu bar, tell the toolbar to focus + // the first menu button. Pass it the storage id of the view where + // focus should be returned if the user presses escape. + // + // Not used on the Mac, which has a normal menu bar. + SaveFocusedView(); + toolbar_->EnterMenuBarEmulationMode(last_focused_view_storage_id_, NULL); +} + +void BrowserView::SaveFocusedView() { views::ViewStorage* view_storage = views::ViewStorage::GetSharedInstance(); if (view_storage->RetrieveView(last_focused_view_storage_id_)) view_storage->RemoveView(last_focused_view_storage_id_); - - // Store the last focused view into the storage, to handle existing traversal. - view_storage->StoreView(last_focused_view_storage_id_, - GetRootView()->GetFocusedView()); - - // Start the traversal within the main toolbar. - toolbar_->InitiateTraversal(last_focused_view_storage_id_); + views::View* focused_view = GetRootView()->GetFocusedView(); + if (focused_view) + view_storage->StoreView(last_focused_view_storage_id_, focused_view); } void BrowserView::DestroyBrowser() { diff --git a/chrome/browser/views/frame/browser_view.h b/chrome/browser/views/frame/browser_view.h index bf753e9..026fa53 100644 --- a/chrome/browser/views/frame/browser_view.h +++ b/chrome/browser/views/frame/browser_view.h @@ -270,6 +270,7 @@ class BrowserView : public BrowserBubbleHost, virtual void UpdateStopGoState(bool is_loading, bool force); virtual void UpdateToolbar(TabContents* contents, bool should_restore_state); virtual void FocusToolbar(); + virtual void FocusPageAndAppMenus(); virtual void DestroyBrowser(); virtual bool IsBookmarkBarVisible() const; virtual bool IsBookmarkBarAnimating() const; @@ -472,6 +473,9 @@ class BrowserView : public BrowserBubbleHost, // Initialize the hung plugin detector. void InitHangMonitor(); + // Save the current focused view to view storage + void SaveFocusedView(); + // Initialize class statics. static void InitClass(); diff --git a/chrome/browser/views/toolbar_view.cc b/chrome/browser/views/toolbar_view.cc index 9670c24..0153012 100644 --- a/chrome/browser/views/toolbar_view.cc +++ b/chrome/browser/views/toolbar_view.cc @@ -35,6 +35,7 @@ #include "chrome/browser/views/bookmark_menu_button.h" #include "chrome/browser/views/browser_actions_container.h" #include "chrome/browser/views/event_utils.h" +#include "chrome/browser/views/frame/browser_view.h" #include "chrome/browser/views/go_button.h" #include "chrome/browser/views/location_bar_view.h" #include "chrome/browser/views/toolbar_star_toggle.h" @@ -52,6 +53,8 @@ #include "views/controls/label.h" #include "views/controls/menu/menu_2.h" #include "views/drag_utils.h" +#include "views/focus/view_storage.h" +#include "views/widget/tooltip_manager.h" #include "views/window/non_client_view.h" #include "views/window/window.h" @@ -90,7 +93,9 @@ ToolbarView::ToolbarView(Browser* browser) bookmark_menu_(NULL), profile_(NULL), browser_(browser), - profiles_menu_contents_(NULL) { + profiles_menu_contents_(NULL), + last_focused_view_storage_id_(-1), + ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)) { SetID(VIEW_ID_TOOLBAR); browser_->command_updater()->AddCommandObserver(IDC_BACK, this); browser_->command_updater()->AddCommandObserver(IDC_FORWARD, this); @@ -108,6 +113,14 @@ ToolbarView::ToolbarView(Browser* browser) } } +ToolbarView::~ToolbarView() { + if (page_menu_->HasFocus() || app_menu_->HasFocus()) { + views::FocusManager* focus_manager = GetFocusManager(); + focus_manager->UnregisterAccelerators(this); + focus_manager->RemoveFocusChangeListener(this); + } +} + void ToolbarView::Init(Profile* profile) { back_menu_model_.reset(new BackForwardMenuModel( browser_, BackForwardMenuModel::BACKWARD_MENU)); @@ -148,6 +161,66 @@ void ToolbarView::SetAppMenuModel(AppMenuModel* model) { app_menu_menu_.reset(new views::Menu2(app_menu_model_.get())); } +void ToolbarView::EnterMenuBarEmulationMode(int last_focused_view_storage_id, + views::MenuButton* menu_to_focus) { + last_focused_view_storage_id_ = last_focused_view_storage_id; + if (!menu_to_focus) + menu_to_focus = page_menu_; + + // If we're already in the menu bar emulation mode, just set the focus. + if (page_menu_->IsFocusable() && app_menu_->IsFocusable()) { + menu_to_focus->RequestFocus(); + return; + } + + // Make the menus focusable and set focus to the initial menu. + page_menu_->SetFocusable(true); + app_menu_->SetFocusable(true); + menu_to_focus->RequestFocus(); + + // Listen so we know when focus has moved to something other than one + // of these menus. + views::FocusManager* focus_manager = GetFocusManager(); + focus_manager->AddFocusChangeListener(this); + + // Add accelerators so that the usual keys used to interact with a + // menu bar work as expected. + views::Accelerator return_key(base::VKEY_RETURN, false, false, false); + focus_manager->RegisterAccelerator(return_key, this); + views::Accelerator space(base::VKEY_SPACE, false, false, false); + focus_manager->RegisterAccelerator(space, this); + views::Accelerator escape(base::VKEY_ESCAPE, false, false, false); + focus_manager->RegisterAccelerator(escape, this); + views::Accelerator down(base::VKEY_DOWN, false, false, false); + focus_manager->RegisterAccelerator(down, this); + views::Accelerator up(base::VKEY_UP, false, false, false); + focus_manager->RegisterAccelerator(up, this); + views::Accelerator left(base::VKEY_LEFT, false, false, false); + focus_manager->RegisterAccelerator(left, this); + views::Accelerator right(base::VKEY_RIGHT, false, false, false); + focus_manager->RegisterAccelerator(right, this); +} + +//////////////////////////////////////////////////////////////////////////////// +// ToolbarView, FocusChangeListener overrides: + +void ToolbarView::FocusWillChange(views::View* focused_before, + views::View* focused_now) { + // If the focus is switching to something outside the menu bar, + // take it out of the focus traversal. + if (focused_now != NULL && + focused_now != page_menu_ && + focused_now != app_menu_) { + // Post ExitMenuBarEmulationMode to the queue rather than running it + // right away, because otherwise we'll remove ourselves from the + // list of listeners while FocusManager is in the middle of iterating + // over that list. + MessageLoop::current()->PostTask( + FROM_HERE, method_factory_.NewRunnableMethod( + &ToolbarView::ExitMenuBarEmulationMode)); + } +} + //////////////////////////////////////////////////////////////////////////////// // ToolbarView, AccessibleToolbarView overrides: @@ -324,6 +397,43 @@ void ToolbarView::ExecuteCommand(int command_id) { //////////////////////////////////////////////////////////////////////////////// // ToolbarView, views::View overrides: +bool ToolbarView::AcceleratorPressed( + const views::Accelerator& accelerator) { + // The only accelerators we handle here are if the menus are focused. + views::View* focused_view = GetFocusManager()->GetFocusedView(); + if (focused_view != page_menu_ && focused_view != app_menu_) { + ExitMenuBarEmulationMode(); + return false; + } + + // Safe to cast, given the check above. + views::MenuButton* menu = static_cast<views::MenuButton*>(focused_view); + switch (accelerator.GetKeyCode()) { + case base::VKEY_ESCAPE: + RestoreLastFocusedView(); + return true; + case base::VKEY_LEFT: + case base::VKEY_RIGHT: + if (menu == app_menu_) + page_menu_->RequestFocus(); + else + app_menu_->RequestFocus(); + return true; + case base::VKEY_UP: + case base::VKEY_DOWN: + case base::VKEY_RETURN: + case base::VKEY_SPACE: + // Hide the tooltip before activating a menu button. + if (GetWidget()->GetTooltipManager()) + GetWidget()->GetTooltipManager()->HideKeyboardTooltip(); + + ActivateMenuButton(menu); + return true; + default: + return false; + } +} + gfx::Size ToolbarView::GetPreferredSize() { if (IsDisplayModeNormal()) { int min_width = kControlIndent + back_->GetPreferredSize().width() + @@ -709,10 +819,86 @@ void ToolbarView::RunPageMenu(const gfx::Point& pt) { page_menu_model_.reset(new PageMenuModel(this, browser_)); page_menu_menu_.reset(new views::Menu2(page_menu_model_.get())); page_menu_menu_->RunMenuAt(pt, views::Menu2::ALIGN_TOPRIGHT); + SwitchToOtherMenuIfNeeded(page_menu_menu_.get(), app_menu_); } void ToolbarView::RunAppMenu(const gfx::Point& pt) { if (app_menu_model_->BuildProfileSubMenu()) app_menu_menu_->Rebuild(); app_menu_menu_->RunMenuAt(pt, views::Menu2::ALIGN_TOPRIGHT); + SwitchToOtherMenuIfNeeded(app_menu_menu_.get(), page_menu_); +} + +void ToolbarView::SwitchToOtherMenuIfNeeded( + views::Menu2* previous_menu, views::MenuButton* next_menu_button) { + // If the user tried to move to the right or left, switch from the + // app menu to the page menu. Switching to the next menu is delayed + // until the next event loop so that the call stack that initiated + // activating the first menu can return. (If we didn't do this, the + // call stack would grow each time the user switches menus, and + // the actions taken after the user finally exits a menu would cause + // flicker.) + views::MenuWrapper::MenuAction action = previous_menu->GetMenuAction(); + if (action == views::MenuWrapper::MENU_ACTION_NEXT || + action == views::MenuWrapper::MENU_ACTION_PREVIOUS) { + MessageLoop::current()->PostTask( + FROM_HERE, method_factory_.NewRunnableMethod( + &ToolbarView::ActivateMenuButton, + next_menu_button)); + } +} + +void ToolbarView::ActivateMenuButton(views::MenuButton* menu_button) { +#if defined(OS_LINUX) + // Under GTK, opening a pop-up menu causes the main window to lose focus. + // Focus is automatically returned when the menu closes. + // + // Make sure that the menu button being activated has focus, so that + // when the user escapes from the menu without selecting anything, focus + // will be returned here. + if (!menu_button->HasFocus()) { + menu_button->RequestFocus(); + GetFocusManager()->StoreFocusedView(); + } +#endif + +#if defined(OS_WIN) + // On Windows, we have to explicitly clear the focus before opening + // the pop-up menu, then set the focus again when it closes. + GetFocusManager()->ClearFocus(); +#endif + + // Tell the menu button to activate, opening its pop-up menu. + menu_button->Activate(); + +#if defined(OS_WIN) + EnterMenuBarEmulationMode(last_focused_view_storage_id_, menu_button); +#endif +} + +void ToolbarView::ExitMenuBarEmulationMode() { + if (page_menu_->HasFocus() || app_menu_->HasFocus()) + RestoreLastFocusedView(); + + views::FocusManager* focus_manager = GetFocusManager(); + focus_manager->UnregisterAccelerators(this); + focus_manager->RemoveFocusChangeListener(this); + page_menu_->SetFocusable(false); + app_menu_->SetFocusable(false); +} + +void ToolbarView::RestoreLastFocusedView() { + views::ViewStorage* view_storage = views::ViewStorage::GetSharedInstance(); + views::View* last_focused_view = + view_storage->RetrieveView(last_focused_view_storage_id_); + if (last_focused_view) { + last_focused_view->RequestFocus(); + } else { + // Focus the location bar + views::View* view = GetAncestorWithClassName(BrowserView::kViewClassName); + if (view) { + BrowserView* browser_view = static_cast<BrowserView*>(view); + browser_view->SetFocusToLocationBar(); + } + } } diff --git a/chrome/browser/views/toolbar_view.h b/chrome/browser/views/toolbar_view.h index 23f5232..9425b56 100644 --- a/chrome/browser/views/toolbar_view.h +++ b/chrome/browser/views/toolbar_view.h @@ -36,6 +36,7 @@ class Menu2; class ToolbarView : public AccessibleToolbarView, public views::ViewMenuDelegate, public views::DragController, + public views::FocusChangeListener, public menus::SimpleMenuModel::Delegate, public LocationBarView::Delegate, public NotificationObserver, @@ -44,7 +45,7 @@ class ToolbarView : public AccessibleToolbarView, public BubblePositioner { public: explicit ToolbarView(Browser* browser); - virtual ~ToolbarView() { } + virtual ~ToolbarView(); // Create the contents of the Browser Toolbar void Init(Profile* profile); @@ -62,6 +63,19 @@ class ToolbarView : public AccessibleToolbarView, // Sets the app menu model. void SetAppMenuModel(AppMenuModel* model); + // Focuses the page menu and enters a special mode where the page + // and app menus are focusable and allow for keyboard navigation just + // like a normal menu bar. As soon as focus leaves one of the menus, + // the special mode is exited. + // + // Pass it the storage id of the view where focus should be returned + // if the user escapes, and the menu button to focus initially. If + // |menu_to_focus| is NULL, it will focus the page menu by default. + // + // Not used on the Mac, which has a "normal" menu bar. + void EnterMenuBarEmulationMode(int last_focused_view_storage_id, + views::MenuButton* menu_to_focus); + // Accessors... Browser* browser() const { return browser_; } BrowserActionsContainer* browser_actions() const { return browser_actions_; } @@ -71,6 +85,10 @@ class ToolbarView : public AccessibleToolbarView, views::MenuButton* page_menu() const { return page_menu_; } views::MenuButton* app_menu() const { return app_menu_; } + // Overridden from views::FocusChangeListener: + virtual void FocusWillChange(views::View* focused_before, + views::View* focused_now); + // Overridden from AccessibleToolbarView: virtual bool IsAccessibleViewTraversable(views::View* view); @@ -106,6 +124,7 @@ class ToolbarView : public AccessibleToolbarView, virtual void ExecuteCommand(int command_id); // Overridden from views::View: + virtual bool AcceleratorPressed(const views::Accelerator& accelerator); virtual gfx::Size GetPreferredSize(); virtual void Layout(); virtual void Paint(gfx::Canvas* canvas); @@ -138,6 +157,13 @@ class ToolbarView : public AccessibleToolbarView, void RunPageMenu(const gfx::Point& pt); void RunAppMenu(const gfx::Point& pt); + // Check if the menu exited with a code indicating the user wants to + // switch to the other menu, and then switch to that other menu. + void SwitchToOtherMenuIfNeeded(views::Menu2* previous_menu, + views::MenuButton* next_menu_button); + + void ActivateMenuButton(views::MenuButton* menu_button); + // Types of display mode this toolbar can have. enum DisplayMode { DISPLAYMODE_NORMAL, // Normal toolbar with buttons, etc. @@ -148,6 +174,14 @@ class ToolbarView : public AccessibleToolbarView, return display_mode_ == DISPLAYMODE_NORMAL; } + // Take the menus out of the focus traversal, unregister accelerators, + // and stop listening to focus change events. + void ExitMenuBarEmulationMode(); + + // Restore the view that was focused before EnterMenuBarEmulationMode + // was called. + void RestoreLastFocusedView(); + scoped_ptr<BackForwardMenuModel> back_menu_model_; scoped_ptr<BackForwardMenuModel> forward_menu_model_; @@ -186,6 +220,13 @@ class ToolbarView : public AccessibleToolbarView, // TODO(beng): build these into MenuButton. scoped_ptr<views::Menu2> page_menu_menu_; scoped_ptr<views::Menu2> app_menu_menu_; + + // Storage id for the last view that was focused before focus + // was given to one of the toolbar views. + int last_focused_view_storage_id_; + + // Used to post tasks to switch to the next/previous menu. + ScopedRunnableMethodFactory<ToolbarView> method_factory_; }; #endif // CHROME_BROWSER_VIEWS_TOOLBAR_VIEW_H_ diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index 410946b..6ca4beb 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -230,6 +230,7 @@ 'test/automated_ui_tests/automated_ui_test_interactive_test.cc', 'test/automated_ui_tests/automated_ui_tests.cc', 'test/automated_ui_tests/automated_ui_tests.h', + 'test/automated_ui_tests/keyboard_access_uitest.cc', ], 'conditions': [ ['OS=="linux"', { diff --git a/chrome/test/automated_ui_tests/keyboard_access_uitest.cc b/chrome/test/automated_ui_tests/keyboard_access_uitest.cc new file mode 100644 index 0000000..fd7cdda --- /dev/null +++ b/chrome/test/automated_ui_tests/keyboard_access_uitest.cc @@ -0,0 +1,99 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/keyboard_codes.h" +#include "chrome/test/automated_ui_tests/automated_ui_test_base.h" +#include "chrome/test/automation/browser_proxy.h" +#include "chrome/test/automation/tab_proxy.h" +#include "chrome/test/automation/window_proxy.h" +#include "googleurl/src/gurl.h" + +// Menu keyboard access currently works on Windows and on Linux when +// toolkit_views is defined (i.e. for Chrome OS). It's not needed +// on the Mac, and it's not yet implemented on Linux. +#if defined(TOOLKIT_VIEWS) + +namespace { + +class KeyboardAccessTest : public AutomatedUITestBase { + public: + KeyboardAccessTest() { + dom_automation_enabled_ = true; + show_window_ = true; + } + + // Use the keyboard to select "New Tab" from the app menu. + // This test depends on the fact that there are two menus and that + // New Tab is the first item in the app menu. If the menus change, + // this test will need to be changed to reflect that. + // + // If alternate_key_sequence is true, use "Alt" instead of "F10" to + // open the menu bar, and "Down" instead of "Enter" to open a menu. + void TestMenuKeyboardAccess(bool alternate_key_sequence); + + DISALLOW_COPY_AND_ASSIGN(KeyboardAccessTest); +}; + +void KeyboardAccessTest::TestMenuKeyboardAccess(bool alternate_key_sequence) { + scoped_refptr<BrowserProxy> browser = automation()->GetBrowserWindow(0); + scoped_refptr<WindowProxy> window = browser->GetWindow(); + + // Navigate to a page in the first tab, which makes sure that focus is + // set to the browser window. + scoped_refptr<TabProxy> tab(GetActiveTab()); + ASSERT_TRUE(tab.get()); + ASSERT_TRUE(tab->NavigateToURL(GURL("about:"))); + + // The initial tab index should be 0. + int tab_index = -1; + ASSERT_TRUE(browser->GetActiveTabIndex(&tab_index)); + ASSERT_EQ(0, tab_index); + + // Get the focused view ID, then press a key to activate the + // page menu, then wait until the focused view changes. + int original_view_id = -1; + ASSERT_TRUE(window->GetFocusedViewID(&original_view_id)); + + if (alternate_key_sequence) + ASSERT_TRUE(window->SimulateOSKeyPress(base::VKEY_MENU, 0)); + else + ASSERT_TRUE(window->SimulateOSKeyPress(base::VKEY_F10, 0)); + + int new_view_id = -1; + ASSERT_TRUE(window->WaitForFocusedViewIDToChange( + original_view_id, &new_view_id)); + + // Press RIGHT to focus the app menu, then RETURN or DOWN to open it. + ASSERT_TRUE(window->SimulateOSKeyPress(base::VKEY_RIGHT, 0)); + if (alternate_key_sequence) + ASSERT_TRUE(window->SimulateOSKeyPress(base::VKEY_DOWN, 0)); + else + ASSERT_TRUE(window->SimulateOSKeyPress(base::VKEY_RETURN, 0)); + + // Wait until the popup menu actually opens. + ASSERT_TRUE(window->WaitForPopupMenuOpen(sleep_timeout_ms())); + + // Press DOWN to select the first item, then RETURN to select it. + ASSERT_TRUE(window->SimulateOSKeyPress(base::VKEY_DOWN, 0)); + ASSERT_TRUE(window->SimulateOSKeyPress(base::VKEY_RETURN, 0)); + + // Wait for the new tab to appear. + ASSERT_TRUE(browser->WaitForTabCountToBecome(2, sleep_timeout_ms())); + + // Make sure that the new tab index is 1. + ASSERT_TRUE(browser->GetActiveTabIndex(&tab_index)); + ASSERT_EQ(1, tab_index); +} + +TEST_F(KeyboardAccessTest, TestMenuKeyboardAccess) { + TestMenuKeyboardAccess(false); +} + +TEST_F(KeyboardAccessTest, TestAltMenuKeyboardAccess) { + TestMenuKeyboardAccess(true); +} + +} // namespace + +#endif // defined(TOOLKIT_VIEWS) diff --git a/chrome/test/automation/automation_messages_internal.h b/chrome/test/automation/automation_messages_internal.h index 7447020..b73ef4b 100644 --- a/chrome/test/automation/automation_messages_internal.h +++ b/chrome/test/automation/automation_messages_internal.h @@ -348,6 +348,14 @@ IPC_BEGIN_MESSAGES(Automation) int /* view_handle */, int /* focused_view_id */) + // Block until the focused view id changes to something other than + // |previous_view_id|. + IPC_SYNC_MESSAGE_ROUTED2_2(AutomationMsg_WaitForFocusedViewIDToChange, + int /* window handle */, + int /* previous_view_id */, + bool /* success */, + int /* new_view_id */) + // This message shows/hides the window. IPC_SYNC_MESSAGE_ROUTED2_1(AutomationMsg_SetWindowVisible, int /* view_handle */, @@ -1288,4 +1296,10 @@ IPC_BEGIN_MESSAGES(Automation) int64 /* id */, bool /* success */) + // Determine if a pop-up menu is open. + IPC_SYNC_MESSAGE_ROUTED1_2(AutomationMsg_IsPopUpMenuOpen, + int /* window handle */, + bool /* success */, + bool /* is_open */) + IPC_END_MESSAGES(Automation) diff --git a/chrome/test/automation/window_proxy.cc b/chrome/test/automation/window_proxy.cc index 295f05b..5f18f6b 100644 --- a/chrome/test/automation/window_proxy.cc +++ b/chrome/test/automation/window_proxy.cc @@ -8,6 +8,7 @@ #include <algorithm> #include "base/logging.h" +#include "base/time.h" #include "chrome/test/automation/automation_constants.h" #include "chrome/test/automation/automation_messages.h" #include "chrome/test/automation/automation_proxy.h" @@ -16,6 +17,9 @@ #include "gfx/rect.h" #include "googleurl/src/gurl.h" +using base::TimeDelta; +using base::TimeTicks; + bool WindowProxy::SimulateOSClick(const gfx::Point& click, int flags) { if (!is_valid()) return false; @@ -123,6 +127,14 @@ bool WindowProxy::GetFocusedViewID(int* view_id) { view_id)); } +bool WindowProxy::WaitForFocusedViewIDToChange( + int old_view_id, int* new_view_id) { + bool result = false; + sender_->Send(new AutomationMsg_WaitForFocusedViewIDToChange + (0, handle_, old_view_id, &result, new_view_id)); + return result; +} + scoped_refptr<BrowserProxy> WindowProxy::GetBrowser() { return GetBrowserWithTimeout(base::kNoTimeout, NULL); } @@ -163,3 +175,22 @@ bool WindowProxy::IsMaximized(bool* maximized) { &result)); return result; } + +bool WindowProxy::WaitForPopupMenuOpen(uint32 timeout_ms) { + const TimeTicks start = TimeTicks::Now(); + const TimeDelta timeout = TimeDelta::FromMilliseconds(timeout_ms); + while (TimeTicks::Now() - start < timeout) { + PlatformThread::Sleep(automation::kSleepTime); + + bool is_open = false; + bool success = false; + if (!sender_->Send(new AutomationMsg_IsPopUpMenuOpen( + 0, handle_, &success, &is_open))) + return false; + if (!success) + return false; + if (is_open) + return true; + } + return false; +} diff --git a/chrome/test/automation/window_proxy.h b/chrome/test/automation/window_proxy.h index 3c5deca..be7ed64 100644 --- a/chrome/test/automation/window_proxy.h +++ b/chrome/test/automation/window_proxy.h @@ -90,6 +90,14 @@ class WindowProxy : public AutomationResourceProxy { // was retrieved. bool GetFocusedViewID(int* view_id); + // Waits until the focused view ID changes to something other than + // |old_view_id|. Returns true if the focused view ID did change. + bool WaitForFocusedViewIDToChange(int old_view_id, int* new_view_id); + + // Waits until a pop-up menu is opened. Returns true on success, or false + // if a pop-up menu is not opened within the timeout period. + bool WaitForPopupMenuOpen(uint32 timeout_ms); + // Returns the browser this window corresponds to, or NULL if this window // is not a browser. The caller owns the returned BrowserProxy. scoped_refptr<BrowserProxy> GetBrowser(); diff --git a/chrome/test/test_browser_window.h b/chrome/test/test_browser_window.h index 169afdb..1390dd8 100644 --- a/chrome/test/test_browser_window.h +++ b/chrome/test/test_browser_window.h @@ -48,6 +48,7 @@ class TestBrowserWindow : public BrowserWindow { virtual void UpdateToolbar(TabContents* contents, bool should_restore_state) {} virtual void FocusToolbar() {} + virtual void FocusPageAndAppMenus() {} virtual void ShowPageMenu() {} virtual void ShowAppMenu() {} virtual bool PreHandleKeyboardEvent(const NativeWebKeyboardEvent& event, diff --git a/views/controls/menu/menu_2.cc b/views/controls/menu/menu_2.cc index 2228b72..2fe090a 100644 --- a/views/controls/menu/menu_2.cc +++ b/views/controls/menu/menu_2.cc @@ -42,4 +42,8 @@ void Menu2::UpdateStates() { wrapper_->UpdateStates(); } +MenuWrapper::MenuAction Menu2::GetMenuAction() const { + return wrapper_->GetMenuAction(); +} + } // namespace diff --git a/views/controls/menu/menu_2.h b/views/controls/menu/menu_2.h index 77ff3d0..adb1c0f 100644 --- a/views/controls/menu/menu_2.h +++ b/views/controls/menu/menu_2.h @@ -1,6 +1,6 @@ -// Copyright (c) 2010 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. +// Copyright (c) 2010 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_MENU_2_H_ #define VIEWS_CONTROLS_MENU_MENU_2_H_ @@ -61,6 +61,11 @@ class Menu2 { // For submenus. gfx::NativeMenu GetNativeMenu() const; + // Get the result of the last call to RunMenuAt to determine whether an + // item was selected, the user navigated to a next or previous menu, or + // nothing. + MenuWrapper::MenuAction GetMenuAction() const; + // Accessors. menus::MenuModel* model() const { return model_; } @@ -78,4 +83,3 @@ class Menu2 { } // namespace views #endif // VIEWS_CONTROLS_MENU_MENU_2_H_ - diff --git a/views/controls/menu/menu_wrapper.h b/views/controls/menu/menu_wrapper.h index b985387..f23bd4f 100644 --- a/views/controls/menu/menu_wrapper.h +++ b/views/controls/menu/menu_wrapper.h @@ -1,6 +1,6 @@ -// Copyright (c) 2009 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. +// Copyright (c) 2010 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_MENU_WRAPPER_H_ #define VIEWS_CONTROLS_MENU_MENU_WRAPPER_H_ @@ -18,6 +18,14 @@ class Menu2; // An interface that wraps an object that implements a menu. class MenuWrapper { public: + // All of the possible actions that can result from RunMenuAt. + enum MenuAction { + MENU_ACTION_NONE, // Menu cancelled, or never opened. + MENU_ACTION_SELECTED, // An item was selected. + MENU_ACTION_PREVIOUS, // User wants to navigate to the previous menu. + MENU_ACTION_NEXT, // User wants to navigate to the next menu. + }; + virtual ~MenuWrapper() {} // Runs the menu at the specified point. This blocks until done. @@ -37,6 +45,11 @@ class MenuWrapper { // Retrieve a native menu handle. virtual gfx::NativeMenu GetNativeMenu() const = 0; + // Get the result of the last call to RunMenuAt to determine whether an + // item was selected, the user navigated to a next or previous menu, or + // nothing. + virtual MenuAction GetMenuAction() const = 0; + // Creates the appropriate instance of this wrapper for the current platform. static MenuWrapper* CreateWrapper(Menu2* menu); }; diff --git a/views/controls/menu/native_menu_gtk.cc b/views/controls/menu/native_menu_gtk.cc index 395e9a5..5ff5c00 100644 --- a/views/controls/menu/native_menu_gtk.cc +++ b/views/controls/menu/native_menu_gtk.cc @@ -76,7 +76,8 @@ NativeMenuGtk::NativeMenuGtk(Menu2* menu) activated_menu_(NULL), activated_index_(-1), activate_factory_(this), - host_menu_(menu) { + host_menu_(menu), + menu_action_(MENU_ACTION_NONE) { } NativeMenuGtk::~NativeMenuGtk() { @@ -93,6 +94,7 @@ NativeMenuGtk::~NativeMenuGtk() { void NativeMenuGtk::RunMenuAt(const gfx::Point& point, int alignment) { activated_menu_ = NULL; activated_index_ = -1; + menu_action_ = MENU_ACTION_NONE; UpdateStates(); Position position = { point, static_cast<Menu2::Alignment>(alignment) }; @@ -104,13 +106,18 @@ void NativeMenuGtk::RunMenuAt(const gfx::Point& point, int alignment) { menu_shown_ = true; // Listen for "hide" signal so that we know when to return from the blocking // RunMenuAt call. - gint handle_id = + gint hide_handle_id = g_signal_connect(menu_, "hide", G_CALLBACK(OnMenuHidden), this); + gint move_handle_id = + g_signal_connect(menu_, "move-current", G_CALLBACK(OnMenuMoveCurrent), + this); + // Block until menu is no longer shown by running a nested message loop. MessageLoopForUI::current()->Run(NULL); - g_signal_handler_disconnect(G_OBJECT(menu_), handle_id); + g_signal_handler_disconnect(G_OBJECT(menu_), hide_handle_id); + g_signal_handler_disconnect(G_OBJECT(menu_), move_handle_id); menu_shown_ = false; if (activated_menu_) { @@ -174,6 +181,10 @@ gfx::NativeMenu NativeMenuGtk::GetNativeMenu() const { return menu_; } +NativeMenuGtk::MenuAction NativeMenuGtk::GetMenuAction() const { + return menu_action_; +} + //////////////////////////////////////////////////////////////////////////////// // NativeMenuGtk, private: @@ -188,6 +199,26 @@ void NativeMenuGtk::OnMenuHidden(GtkWidget* widget, NativeMenuGtk* menu) { MessageLoop::current()->Quit(); } +// static +void NativeMenuGtk::OnMenuMoveCurrent(GtkMenu* menu_widget, + GtkMenuDirectionType focus_direction, + NativeMenuGtk* menu) { + GtkWidget* parent = GTK_MENU_SHELL(menu_widget)->parent_menu_shell; + GtkWidget* menu_item = GTK_MENU_SHELL(menu_widget)->active_menu_item; + GtkWidget* submenu = NULL; + if (menu_item) { + submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item)); + } + + if (focus_direction == GTK_MENU_DIR_CHILD && submenu == NULL) { + menu->GetAncestor()->menu_action_ = MENU_ACTION_NEXT; + gtk_menu_popdown(menu_widget); + } else if (focus_direction == GTK_MENU_DIR_PARENT && parent == NULL) { + menu->GetAncestor()->menu_action_ = MENU_ACTION_PREVIOUS; + gtk_menu_popdown(menu_widget); + } +} + void NativeMenuGtk::AddSeparatorAt(int index) { GtkWidget* separator = gtk_separator_menu_item_new(); gtk_widget_show(separator); @@ -374,6 +405,7 @@ void NativeMenuGtk::OnActivate(GtkMenuItem* menu_item) { NativeMenuGtk* ancestor = GetAncestor(); ancestor->activated_menu_ = this; activated_index_ = position; + ancestor->menu_action_ = MENU_ACTION_SELECTED; } } diff --git a/views/controls/menu/native_menu_gtk.h b/views/controls/menu/native_menu_gtk.h index 2c69ba3..2ea0c84c 100644 --- a/views/controls/menu/native_menu_gtk.h +++ b/views/controls/menu/native_menu_gtk.h @@ -1,6 +1,6 @@ -// Copyright (c) 2009 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. +// Copyright (c) 2010 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_NATIVE_MENU_GTK_H_ #define VIEWS_CONTROLS_MENU_NATIVE_MENU_GTK_H_ @@ -36,9 +36,13 @@ class NativeMenuGtk : public MenuWrapper { virtual void Rebuild(); virtual void UpdateStates(); virtual gfx::NativeMenu GetNativeMenu() const; + virtual MenuAction GetMenuAction() const; private: static void OnMenuHidden(GtkWidget* widget, NativeMenuGtk* menu); + static void OnMenuMoveCurrent(GtkMenu* widget, + GtkMenuDirectionType focus_direction, + NativeMenuGtk* menu); void AddSeparatorAt(int index); GtkWidget* AddMenuItemAt(int index, GtkRadioMenuItem* radio_group, @@ -111,6 +115,9 @@ class NativeMenuGtk : public MenuWrapper { Menu2* host_menu_; gulong destroy_handler_id_; + // The action that took place during the call to RunMenuAt. + MenuAction menu_action_; + DISALLOW_COPY_AND_ASSIGN(NativeMenuGtk); }; diff --git a/views/controls/menu/native_menu_win.cc b/views/controls/menu/native_menu_win.cc index 75b8d4c..d959b85 100644 --- a/views/controls/menu/native_menu_win.cc +++ b/views/controls/menu/native_menu_win.cc @@ -306,7 +306,8 @@ NativeMenuWin::NativeMenuWin(menus::MenuModel* model, HWND system_menu_for) owner_draw_(l10n_util::NeedOverrideDefaultUIFont(NULL, NULL) && !system_menu_for), system_menu_for_(system_menu_for), - first_item_index_(0) { + first_item_index_(0), + menu_action_(MENU_ACTION_NONE) { } NativeMenuWin::~NativeMenuWin() { @@ -322,10 +323,24 @@ void NativeMenuWin::RunMenuAt(const gfx::Point& point, int alignment) { UpdateStates(); UINT flags = TPM_LEFTBUTTON | TPM_RECURSE; flags |= GetAlignmentFlags(alignment); + menu_action_ = MENU_ACTION_NONE; + + // Set a hook function so we can listen for keyboard events while the + // menu is open, and store a pointer to this object in a static + // variable so the hook has access to it (ugly, but it's the + // only way). + open_native_menu_win_ = this; + HHOOK hhook = SetWindowsHookEx(WH_MSGFILTER, MenuMessageHook, + GetModuleHandle(NULL), ::GetCurrentThreadId()); + // Command dispatch is done through WM_MENUCOMMAND, handled by the host // window. + HWND hwnd = host_window_->hwnd(); TrackPopupMenuEx(menu_, flags, point.x(), point.y(), host_window_->hwnd(), NULL); + + UnhookWindowsHookEx(hhook); + open_native_menu_win_ = NULL; } void NativeMenuWin::CancelMenu() { @@ -370,9 +385,58 @@ gfx::NativeMenu NativeMenuWin::GetNativeMenu() const { return menu_; } +NativeMenuWin::MenuAction NativeMenuWin::GetMenuAction() const { + return menu_action_; +} + //////////////////////////////////////////////////////////////////////////////// // NativeMenuWin, private: +// static +NativeMenuWin* NativeMenuWin::open_native_menu_win_ = NULL; + +// static +bool NativeMenuWin::GetHighlightedMenuItemInfo( + HMENU menu, bool* has_parent, bool* has_submenu) { + for (int i = 0; i < ::GetMenuItemCount(menu); i++) { + UINT state = ::GetMenuState(menu, i, MF_BYPOSITION); + if (state & MF_HILITE) { + if (state & MF_POPUP) { + HMENU submenu = GetSubMenu(menu, i); + if (GetHighlightedMenuItemInfo(submenu, has_parent, has_submenu)) + *has_parent = true; + else + *has_submenu = true; + } + return true; + } + } + return false; +} + +// static +LRESULT CALLBACK NativeMenuWin::MenuMessageHook( + int n_code, WPARAM w_param, LPARAM l_param) { + LRESULT result = CallNextHookEx(NULL, n_code, w_param, l_param); + + MSG* msg = reinterpret_cast<MSG*>(l_param); + if (msg->message == WM_KEYDOWN) { + NativeMenuWin* this_ptr = open_native_menu_win_; + bool has_parent = false; + bool has_submenu = false; + GetHighlightedMenuItemInfo(this_ptr->menu_, &has_parent, &has_submenu); + if (msg->wParam == VK_LEFT && !has_parent) { + this_ptr->menu_action_ = MENU_ACTION_PREVIOUS; + ::EndMenu(); + } else if (msg->wParam == VK_RIGHT && !has_parent && !has_submenu) { + this_ptr->menu_action_ = MENU_ACTION_NEXT; + ::EndMenu(); + } + } + + return result; +} + bool NativeMenuWin::IsSeparatorItemAt(int menu_index) const { MENUITEMINFO mii = {0}; mii.cbSize = sizeof(mii); diff --git a/views/controls/menu/native_menu_win.h b/views/controls/menu/native_menu_win.h index ba0144e..5d6f621 100644 --- a/views/controls/menu/native_menu_win.h +++ b/views/controls/menu/native_menu_win.h @@ -1,6 +1,6 @@ -// Copyright (c) 2009 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. +// Copyright (c) 2010 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_NATIVE_MENU_WIN_H_ #define VIEWS_CONTROLS_MENU_NATIVE_MENU_WIN_H_ @@ -29,6 +29,7 @@ class NativeMenuWin : public MenuWrapper { virtual void Rebuild(); virtual void UpdateStates(); virtual gfx::NativeMenu GetNativeMenu() const; + virtual MenuAction GetMenuAction() const; private: // IMPORTANT: Note about indices. @@ -80,6 +81,20 @@ class NativeMenuWin : public MenuWrapper { // Creates the host window that receives notifications from the menu. void CreateHostWindow(); + // Given a menu that's currently popped-up, find the currently + // highlighted item and return whether or not that item has a parent + // (i.e. it's in a submenu), and whether or not that item leads to a + // submenu. Returns true if a highlighted item was found. This + // method is called to determine if the right and left arrow keys + // should be used to switch between menus, or to open and close + // submenus. + static bool GetHighlightedMenuItemInfo( + HMENU menu, bool* has_parent, bool* has_submenu); + + // Hook to receive keyboard events while the menu is open. + static LRESULT CALLBACK MenuMessageHook( + int n_code, WPARAM w_param, LPARAM l_param); + // Our attached model and delegate. menus::MenuModel* model_; @@ -106,6 +121,14 @@ class NativeMenuWin : public MenuWrapper { // The index of the first item in the model in the menu. int first_item_index_; + // The action that took place during the call to RunMenuAt. + MenuAction menu_action_; + + // Ugly: a static pointer to the instance of this class that currently + // has a menu open, because our hook function that receives keyboard + // events doesn't have a mechanism to get a user data pointer. + static NativeMenuWin* open_native_menu_win_; + DISALLOW_COPY_AND_ASSIGN(NativeMenuWin); }; diff --git a/views/focus/accelerator_handler_gtk.cc b/views/focus/accelerator_handler_gtk.cc index 1ff5d81..da7bd94 100644 --- a/views/focus/accelerator_handler_gtk.cc +++ b/views/focus/accelerator_handler_gtk.cc @@ -4,6 +4,9 @@ #include <gtk/gtk.h> +#include "base/keyboard_code_conversion_gtk.h" +#include "base/keyboard_codes.h" +#include "views/accelerator.h" #include "views/focus/accelerator_handler.h" #include "views/focus/focus_manager.h" #include "views/widget/widget_gtk.h" @@ -48,6 +51,14 @@ bool AcceleratorHandler::Dispatch(GdkEvent* event) { if (event->type == GDK_KEY_PRESS) { KeyEvent view_key_event(key_event); + + // If it's the Alt key, don't send it to the focus manager until release + // (to handle focusing the menu bar). + if (view_key_event.GetKeyCode() == base::VKEY_MENU) { + last_key_pressed_ = key_event->keyval; + return true; + } + // FocusManager::OnKeyPressed and OnKeyReleased return false if this // message has been consumed and should not be propagated further. if (!focus_manager->OnKeyEvent(view_key_event)) { @@ -60,6 +71,14 @@ bool AcceleratorHandler::Dispatch(GdkEvent* event) { // as accelerators to avoid unpaired key release. if (event->type == GDK_KEY_RELEASE && key_event->keyval == last_key_pressed_) { + // Special case: the Alt key can trigger an accelerator on release + // rather than on press. + if (base::WindowsKeyCodeForGdkKeyCode(key_event->keyval) == + base::VKEY_MENU) { + Accelerator accelerator(base::VKEY_MENU, false, false, false); + focus_manager->ProcessAccelerator(accelerator); + } + last_key_pressed_ = 0; return true; } diff --git a/views/focus/focus_manager.h b/views/focus/focus_manager.h index bad9dba..2150560 100644 --- a/views/focus/focus_manager.h +++ b/views/focus/focus_manager.h @@ -278,6 +278,10 @@ class FocusManager { static FocusManager* GetFocusManagerForNativeView( gfx::NativeView native_view); + // Retrieves the FocusManager associated with the passed native view. + static FocusManager* GetFocusManagerForNativeWindow( + gfx::NativeWindow native_window); + private: // Returns the next focusable view. View* GetNextFocusableView(View* starting_view, bool reverse, bool dont_loop); diff --git a/views/focus/focus_manager_gtk.cc b/views/focus/focus_manager_gtk.cc index 3038d93..a52a6d9 100644 --- a/views/focus/focus_manager_gtk.cc +++ b/views/focus/focus_manager_gtk.cc @@ -49,4 +49,10 @@ FocusManager* FocusManager::GetFocusManagerForNativeView( return focus_manager; } +// static +FocusManager* FocusManager::GetFocusManagerForNativeWindow( + gfx::NativeWindow native_window) { + return GetFocusManagerForNativeView(GTK_WIDGET(native_window)); +} + } // namespace views diff --git a/views/focus/focus_manager_win.cc b/views/focus/focus_manager_win.cc index fbccf7b..faf3815 100644 --- a/views/focus/focus_manager_win.cc +++ b/views/focus/focus_manager_win.cc @@ -27,5 +27,10 @@ FocusManager* FocusManager::GetFocusManagerForNativeView( return widget ? widget->GetFocusManager() : NULL; } -} // namespace views +// static +FocusManager* FocusManager::GetFocusManagerForNativeWindow( + gfx::NativeWindow native_window) { + return GetFocusManagerForNativeView(native_window); +} +} // namespace views diff --git a/views/view.h b/views/view.h index f83793b..f2469f7 100644 --- a/views/view.h +++ b/views/view.h @@ -609,6 +609,12 @@ class View : public AcceleratorTarget { // accessibility focus. virtual View* GetAccFocusedChildView() { return NULL; } + // Try to give accessibility focus to a given child view. Returns true on + // success. Returns false if this view isn't already focused, if it doesn't + // support accessibility focus for children, or if the given view isn't a + // valid child view that can receive accessibility focus. + virtual bool SetAccFocusedChildView(View* child_view) { return false; } + // Utility functions // Note that the utility coordinate conversions functions always operate on diff --git a/views/window/window_win.cc b/views/window/window_win.cc index b58bcd4..17a3f1b 100644 --- a/views/window/window_win.cc +++ b/views/window/window_win.cc @@ -1086,6 +1086,15 @@ void WindowWin::OnSysCommand(UINT notification_code, CPoint click) { } } + // Handle SC_KEYMENU, which means that the user has pressed the ALT + // key and released it, so we should focus the menu bar. + if ((notification_code & sc_mask) == SC_KEYMENU && click.x == 0) { + Accelerator accelerator(win_util::WinToKeyboardCode(VK_MENU), + false, false, false); + GetFocusManager()->ProcessAccelerator(accelerator); + return; + } + // First see if the delegate can handle it. if (window_delegate_->ExecuteWindowsCommand(notification_code)) return; |