diff options
author | tfarina@chromium.org <tfarina@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-11-18 21:58:23 +0000 |
---|---|---|
committer | tfarina@chromium.org <tfarina@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-11-18 21:58:23 +0000 |
commit | b1ed27851db7f86d7f71dfe4c897c796a953fc25 (patch) | |
tree | b9d972e7a6770b4c9055507fe2e9d329b0974106 /ui/views/focus | |
parent | cee34b707ac41e83ff9d9045c0ffda49ec3f556f (diff) | |
download | chromium_src-b1ed27851db7f86d7f71dfe4c897c796a953fc25.zip chromium_src-b1ed27851db7f86d7f71dfe4c897c796a953fc25.tar.gz chromium_src-b1ed27851db7f86d7f71dfe4c897c796a953fc25.tar.bz2 |
views: Move bubble, events, focus and layout to ui/views/.
Left stub files that will be removed in a follow up patch after updating
the files to point to the new location.
BUG=104039
R=ben@chromium.org
Review URL: http://codereview.chromium.org/8588064
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@110761 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui/views/focus')
20 files changed, 3992 insertions, 0 deletions
diff --git a/ui/views/focus/accelerator_handler.h b/ui/views/focus/accelerator_handler.h new file mode 100644 index 0000000..c0f9591 --- /dev/null +++ b/ui/views/focus/accelerator_handler.h @@ -0,0 +1,61 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_VIEWS_FOCUS_ACCELERATOR_HANDLER_H_ +#define UI_VIEWS_FOCUS_ACCELERATOR_HANDLER_H_ +#pragma once + +#include "build/build_config.h" + +#if defined(TOOLKIT_USES_GTK) +#include <gdk/gdk.h> +#endif + +#include <set> +#include <vector> + +#include "base/message_loop.h" +#include "views/views_export.h" + +namespace views { + +#if defined(TOUCH_UI) || defined(USE_AURA) +#if defined(USE_X11) && !defined(USE_WAYLAND) +// Dispatch an XEvent to the RootView. Return true if the event was dispatched +// and handled, false otherwise. +bool VIEWS_EXPORT DispatchXEvent(XEvent* xevent); +#endif // USE_X11 && !USE_WAYLAND +#endif // TOUCH_UI || USE_AURA + +// This class delegates the key messages to the associated FocusManager class +// for the window that is receiving these messages for accelerator processing. +class VIEWS_EXPORT AcceleratorHandler : public MessageLoop::Dispatcher { + public: + AcceleratorHandler(); + + // Dispatcher method. This returns true if an accelerator was processed by the + // focus manager +#if defined(OS_WIN) + virtual bool Dispatch(const MSG& msg); +#elif defined(USE_WAYLAND) + virtual base::MessagePumpDispatcher::DispatchStatus Dispatch( + base::wayland::WaylandEvent* ev); +#elif defined(TOUCH_UI) || defined(USE_AURA) + virtual base::MessagePumpDispatcher::DispatchStatus Dispatch(XEvent* xev); +#else + virtual bool Dispatch(GdkEvent* event); +#endif + + private: +#if defined(OS_WIN) + // The keys currently pressed and consumed by the FocusManager. + std::set<WPARAM> pressed_keys_; +#endif + + DISALLOW_COPY_AND_ASSIGN(AcceleratorHandler); +}; + +} // namespace views + +#endif // UI_VIEWS_FOCUS_ACCELERATOR_HANDLER_H_ diff --git a/ui/views/focus/accelerator_handler_aura.cc b/ui/views/focus/accelerator_handler_aura.cc new file mode 100644 index 0000000..ab552899 --- /dev/null +++ b/ui/views/focus/accelerator_handler_aura.cc @@ -0,0 +1,29 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "views/focus/accelerator_handler.h" + +namespace views { + +AcceleratorHandler::AcceleratorHandler() { +} + +#if defined(OS_WIN) +bool AcceleratorHandler::Dispatch(const MSG& msg) { + TranslateMessage(&msg); + DispatchMessage(&msg); + return true; +} +#else +base::MessagePumpDispatcher::DispatchStatus AcceleratorHandler::Dispatch( + XEvent*) { + return base::MessagePumpDispatcher::EVENT_IGNORED; +} + +bool DispatchXEvent(XEvent* xev) { + return false; +} +#endif // defined(OS_WIN) + +} // namespace views diff --git a/ui/views/focus/accelerator_handler_gtk.cc b/ui/views/focus/accelerator_handler_gtk.cc new file mode 100644 index 0000000..fab3b86 --- /dev/null +++ b/ui/views/focus/accelerator_handler_gtk.cc @@ -0,0 +1,22 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "views/focus/accelerator_handler.h" + +#include <gtk/gtk.h> + +#include "views/focus/focus_manager.h" + +namespace views { + +AcceleratorHandler::AcceleratorHandler() {} + +bool AcceleratorHandler::Dispatch(GdkEvent* event) { + // The logic for handling keyboard accelerators has been moved into + // NativeWidgetGtk::OnEventKey handler (views/widget/widget_gtk.cc). + gtk_main_do_event(event); + return true; +} + +} // namespace views diff --git a/ui/views/focus/accelerator_handler_gtk_unittest.cc b/ui/views/focus/accelerator_handler_gtk_unittest.cc new file mode 100644 index 0000000..098325d --- /dev/null +++ b/ui/views/focus/accelerator_handler_gtk_unittest.cc @@ -0,0 +1,199 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <gdk/gdkkeysyms.h> + +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/gfx/rect.h" +#include "ui/base/models/accelerator.h" +#include "views/focus/accelerator_handler.h" +#include "views/focus/focus_manager.h" +#include "views/view.h" +#include "views/widget/widget.h" +#include "views/widget/widget_delegate.h" + +namespace views { + +class AcceleratorHandlerGtkTest + : public testing::Test, + public WidgetDelegate, + public ui::AcceleratorTarget { + public: + AcceleratorHandlerGtkTest() + : kMenuAccelerator(ui::VKEY_MENU, false, false, false), + kHomepageAccelerator(ui::VKEY_HOME, false, false, true), + content_view_(NULL) { + } + + virtual void SetUp() { + window_ = Widget::CreateWindowWithBounds(this, gfx::Rect(0, 0, 500, 500)); + window_->Show(); + FocusManager* focus_manager = window_->GetFocusManager(); + focus_manager->RegisterAccelerator(kMenuAccelerator, this); + focus_manager->RegisterAccelerator(kHomepageAccelerator, this); + menu_pressed_ = false; + home_pressed_ = false; + } + + virtual void TearDown() { + window_->Close(); + + // Flush the message loop to make application verifiers happy. + message_loop_.RunAllPending(); + } + + GdkEventKey CreateKeyEvent(GdkEventType type, guint keyval, guint state) { + GdkEventKey evt; + memset(&evt, 0, sizeof(evt)); + evt.type = type; + evt.keyval = keyval; + // The keyval won't be a "correct" hardware keycode for any real hardware, + // but the code should never depend on exact hardware keycodes, just the + // fact that the code for presses and releases of the same key match. + evt.hardware_keycode = keyval; + evt.state = state; + GtkWidget* widget = GTK_WIDGET(window_->GetNativeWindow()); + evt.window = widget->window; + return evt; + } + + // AcceleratorTarget implementation. + virtual bool AcceleratorPressed(const ui::Accelerator& accelerator) { + if (accelerator == kMenuAccelerator) + menu_pressed_ = true; + else if (accelerator == kHomepageAccelerator) + home_pressed_ = true; + return true; + } + + // WidgetDelegate Implementation. + virtual View* GetContentsView() { + if (!content_view_) + content_view_ = new View(); + return content_view_; + } + virtual const views::Widget* GetWidget() const { + return content_view_->GetWidget(); + } + virtual views::Widget* GetWidget() { + return content_view_->GetWidget(); + } + + virtual void InitContentView() { + } + + protected: + bool menu_pressed_; + bool home_pressed_; + + private: + ui::Accelerator kMenuAccelerator; + ui::Accelerator kHomepageAccelerator; + Widget* window_; + View* content_view_; + MessageLoopForUI message_loop_; + DISALLOW_COPY_AND_ASSIGN(AcceleratorHandlerGtkTest); +}; + +// Test that the homepage accelerator (Alt+Home) is activated on key down +// and that the menu accelerator (Alt) is never activated. +TEST_F(AcceleratorHandlerGtkTest, TestHomepageAccelerator) { + AcceleratorHandler handler; + GdkEventKey evt; + + ASSERT_FALSE(menu_pressed_); + ASSERT_FALSE(home_pressed_); + + evt = CreateKeyEvent(GDK_KEY_PRESS, GDK_Alt_L, 0); + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + ASSERT_FALSE(menu_pressed_); + ASSERT_FALSE(home_pressed_); + + evt = CreateKeyEvent(GDK_KEY_PRESS, GDK_Home, GDK_MOD1_MASK); + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + ASSERT_FALSE(menu_pressed_); + ASSERT_TRUE(home_pressed_); + + evt = CreateKeyEvent(GDK_KEY_RELEASE, GDK_Home, GDK_MOD1_MASK); + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + evt = CreateKeyEvent(GDK_KEY_RELEASE, GDK_Alt_L, 0); + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + ASSERT_FALSE(menu_pressed_); + ASSERT_TRUE(home_pressed_); +} + +// Test that the menu accelerator is activated on key up and not key down. +TEST_F(AcceleratorHandlerGtkTest, TestMenuAccelerator) { + AcceleratorHandler handler; + GdkEventKey evt; + + ASSERT_FALSE(menu_pressed_); + + evt = CreateKeyEvent(GDK_KEY_PRESS, GDK_Alt_L, 0); + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + ASSERT_FALSE(menu_pressed_); + + evt = CreateKeyEvent(GDK_KEY_RELEASE, GDK_Alt_L, 0); + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + ASSERT_TRUE(menu_pressed_); +} + +// Test that the menu accelerator isn't confused by the interaction of the +// Alt and Shift keys. Try the following sequence on Linux: +// Press Alt +// Press Shift +// Release Alt +// Release Shift +// The key codes for pressing Alt and releasing Alt are different! This +// caused a bug in a previous version of the code, which is now fixed by +// keeping track of hardware keycodes, which are consistent. +TEST_F(AcceleratorHandlerGtkTest, TestAltShiftInteraction) { + AcceleratorHandler handler; + GdkEventKey evt; + + ASSERT_FALSE(menu_pressed_); + + // Press Shift. + evt = CreateKeyEvent(GDK_KEY_PRESS, GDK_Shift_L, 0); + evt.hardware_keycode = 0x32; + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + // Press Alt - but GDK calls this Meta when Shift is also down. + evt = CreateKeyEvent(GDK_KEY_PRESS, GDK_Meta_L, 0); + evt.hardware_keycode = 0x40; + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + // Release Shift. + evt = CreateKeyEvent(GDK_KEY_RELEASE, GDK_Shift_L, 0); + evt.hardware_keycode = 0x32; + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + // Release Alt - with Shift not down, the keyval is now Alt, but + // the hardware keycode is unchanged. + evt = CreateKeyEvent(GDK_KEY_RELEASE, GDK_Alt_L, 0); + evt.hardware_keycode = 0x40; + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + ASSERT_FALSE(menu_pressed_); + + // Press Alt by itself. + evt = CreateKeyEvent(GDK_KEY_PRESS, GDK_Alt_L, 0); + evt.hardware_keycode = 0x40; + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + // This line fails if we don't keep track of hardware keycodes. + ASSERT_FALSE(menu_pressed_); + + // Release Alt - now this should trigger the menu shortcut. + evt = CreateKeyEvent(GDK_KEY_RELEASE, GDK_Alt_L, 0); + evt.hardware_keycode = 0x40; + EXPECT_TRUE(handler.Dispatch(reinterpret_cast<GdkEvent*>(&evt))); + + ASSERT_TRUE(menu_pressed_); +} + +} // namespace views diff --git a/ui/views/focus/accelerator_handler_touch.cc b/ui/views/focus/accelerator_handler_touch.cc new file mode 100644 index 0000000..1c4c9b3 --- /dev/null +++ b/ui/views/focus/accelerator_handler_touch.cc @@ -0,0 +1,187 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "views/focus/accelerator_handler.h" + +#include <bitset> +#include <gtk/gtk.h> +#include <X11/extensions/XInput2.h> + +#include "ui/base/touch/touch_factory.h" +#include "ui/views/ime/input_method.h" +#include "views/events/event.h" +#include "views/focus/focus_manager.h" +#include "views/view.h" +#include "views/widget/native_widget.h" + +namespace views { + +namespace { + +Widget* FindWidgetForGdkWindow(GdkWindow* gdk_window) { + gpointer data = NULL; + gdk_window_get_user_data(gdk_window, &data); + GtkWidget* gtk_widget = reinterpret_cast<GtkWidget*>(data); + if (!gtk_widget || !GTK_IS_WIDGET(gtk_widget)) { + DLOG(WARNING) << "no GtkWidget found for that GdkWindow"; + return NULL; + } + Widget* widget = Widget::GetWidgetForNativeView(gtk_widget); + + if (!widget) { + DLOG(WARNING) << "no NativeWidgetGtk found for that GtkWidget"; + return NULL; + } + return widget; +} + +} // namespace + +bool DispatchX2Event(Widget* widget, XEvent* xev) { + XGenericEventCookie* cookie = &xev->xcookie; + switch (cookie->evtype) { + case XI_KeyPress: + case XI_KeyRelease: { + // TODO(sad): We don't capture XInput2 events from keyboard yet. + break; + } +#if defined(USE_XI2_MT) + case XI_TouchBegin: + case XI_TouchEnd: + case XI_TouchUpdate: { + // Hide the cursor when a touch event comes in. + ui::TouchFactory::GetInstance()->SetCursorVisible(false, false); + + // If the TouchEvent is processed by |widget|, then return. + TouchEvent touch(xev); + if (widget->OnTouchEvent(touch) != ui::TOUCH_STATUS_UNKNOWN) + return true; + + // We do not want to generate a mouse event for an unprocessed touch + // event here. That is already done by the gesture manager in + // RootView::OnTouchEvent. + return false; + } +#endif + case XI_ButtonPress: + case XI_ButtonRelease: + case XI_Motion: { + XIDeviceEvent* xievent = static_cast<XIDeviceEvent*>(cookie->data); + + // Scrolling the wheel generates press/release events with button id's 4 + // and 5. In case of a wheelscroll, we do not want to show the cursor. + if (xievent->detail == 4 || xievent->detail == 5) { + MouseWheelEvent wheelev(xev); + return widget->OnMouseEvent(wheelev); + } + + ui::TouchFactory* factory = ui::TouchFactory::GetInstance(); + // Is the event coming from a touch device? + if (factory->IsTouchDevice(xievent->sourceid)) { + // Hide the cursor when a touch event comes in. + factory->SetCursorVisible(false, false); + + // With XInput 2.0, XI_ButtonPress and XI_ButtonRelease events are + // ignored, as XI_Motion events contain enough data to detect finger + // press and release. See more notes in TouchFactory::TouchParam. + if ((cookie->evtype == XI_ButtonPress || + cookie->evtype == XI_ButtonRelease) && + factory->IsRealTouchDevice(xievent->sourceid)) + return false; + + // If the TouchEvent is processed by |widget|, then return. Otherwise + // let it fall through so it can be used as a MouseEvent, if desired. + TouchEvent touch(xev); + if (widget->OnTouchEvent(touch) != ui::TOUCH_STATUS_UNKNOWN) + return true; + + // We do not want to generate a mouse event for an unprocessed touch + // event here. That is already done by the gesture manager in + // RootView::OnTouchEvent. + return false; + } else { + MouseEvent mouseev(xev); + + // Show the cursor. Start a timer to hide the cursor after a delay on + // move (not drag) events, or if the only button pressed is released. + bool start_timer = mouseev.type() == ui::ET_MOUSE_MOVED; + start_timer |= mouseev.type() == ui::ET_MOUSE_RELEASED && + (mouseev.IsOnlyLeftMouseButton() || + mouseev.IsOnlyMiddleMouseButton() || + mouseev.IsOnlyRightMouseButton()); + factory->SetCursorVisible(true, start_timer); + + return widget->OnMouseEvent(mouseev); + } + } + } + return false; +} + +bool DispatchXEvent(XEvent* xev) { + GdkDisplay* gdisp = gdk_display_get_default(); + XID xwindow = xev->xany.window; + + if (xev->type == GenericEvent) { + if (!ui::TouchFactory::GetInstance()->ShouldProcessXI2Event(xev)) + return true; // Consume the event. + + XGenericEventCookie* cookie = &xev->xcookie; + if (cookie->evtype == XI_HierarchyChanged) { + ui::TouchFactory::GetInstance()->UpdateDeviceList(cookie->display); + return true; + } + + XIDeviceEvent* xiev = static_cast<XIDeviceEvent*>(cookie->data); + xwindow = xiev->event; + } + + GdkWindow* gwind = gdk_window_lookup_for_display(gdisp, xwindow); + Widget* widget = FindWidgetForGdkWindow(gwind); + if (widget) { + switch (xev->type) { + case KeyPress: + case KeyRelease: { + KeyEvent keyev(xev); + InputMethod* ime = widget->GetInputMethod(); + // Always dispatch key events to the input method first, to make sure + // that the input method's hotkeys work all time. + if (ime) { + ime->DispatchKeyEvent(keyev); + return true; + } + return widget->OnKeyEvent(keyev); + } + case ButtonPress: + case ButtonRelease: + if (xev->xbutton.button == 4 || xev->xbutton.button == 5) { + // Scrolling the wheel triggers button press/release events. + MouseWheelEvent wheelev(xev); + return widget->OnMouseEvent(wheelev); + } + // fallthrough + case MotionNotify: { + MouseEvent mouseev(xev); + return widget->OnMouseEvent(mouseev); + } + + case GenericEvent: { + return DispatchX2Event(widget, xev); + } + } + } + + return false; +} + +AcceleratorHandler::AcceleratorHandler() {} + +base::MessagePumpDispatcher::DispatchStatus + AcceleratorHandler::Dispatch(XEvent* xev) { + return DispatchXEvent(xev) ? + base::MessagePumpDispatcher::EVENT_PROCESSED : + base::MessagePumpDispatcher::EVENT_IGNORED; +} + +} // namespace views diff --git a/ui/views/focus/accelerator_handler_wayland.cc b/ui/views/focus/accelerator_handler_wayland.cc new file mode 100644 index 0000000..a8963ee --- /dev/null +++ b/ui/views/focus/accelerator_handler_wayland.cc @@ -0,0 +1,18 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "views/focus/accelerator_handler.h" + +#include "views/focus/focus_manager.h" + +namespace views { + +AcceleratorHandler::AcceleratorHandler() {} + +base::MessagePumpDispatcher::DispatchStatus + AcceleratorHandler::Dispatch(base::wayland::WaylandEvent* ev) { + return base::MessagePumpDispatcher::EVENT_IGNORED; +} + +} // namespace views diff --git a/ui/views/focus/accelerator_handler_win.cc b/ui/views/focus/accelerator_handler_win.cc new file mode 100644 index 0000000..8f2c8e8 --- /dev/null +++ b/ui/views/focus/accelerator_handler_win.cc @@ -0,0 +1,60 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "views/focus/accelerator_handler.h" + +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/base/keycodes/keyboard_code_conversion_win.h" +#include "views/events/event.h" +#include "views/focus/focus_manager.h" +#include "views/widget/widget.h" + +namespace views { + +AcceleratorHandler::AcceleratorHandler() { +} + +bool AcceleratorHandler::Dispatch(const MSG& msg) { + bool process_message = true; + + if (msg.message >= WM_KEYFIRST && msg.message <= WM_KEYLAST) { + Widget* widget = Widget::GetTopLevelWidgetForNativeView(msg.hwnd); + FocusManager* focus_manager = widget ? widget->GetFocusManager() : NULL; + if (focus_manager) { + switch (msg.message) { + case WM_KEYDOWN: + case WM_SYSKEYDOWN: { + KeyEvent event(msg); + process_message = focus_manager->OnKeyEvent(event); + if (!process_message) { + // Record that this key is pressed so we can remember not to + // translate and dispatch the associated WM_KEYUP. + pressed_keys_.insert(msg.wParam); + } + break; + } + case WM_KEYUP: + case WM_SYSKEYUP: { + std::set<WPARAM>::iterator iter = pressed_keys_.find(msg.wParam); + if (iter != pressed_keys_.end()) { + // Don't translate/dispatch the KEYUP since we have eaten the + // associated KEYDOWN. + pressed_keys_.erase(iter); + return true; + } + break; + } + } + } + } + + if (process_message) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + return true; +} + +} // namespace views diff --git a/ui/views/focus/external_focus_tracker.cc b/ui/views/focus/external_focus_tracker.cc new file mode 100644 index 0000000..193b57e --- /dev/null +++ b/ui/views/focus/external_focus_tracker.cc @@ -0,0 +1,72 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "views/focus/external_focus_tracker.h" + +#include "base/logging.h" +#include "views/view.h" +#include "views/focus/view_storage.h" + +namespace views { + +ExternalFocusTracker::ExternalFocusTracker(View* parent_view, + FocusManager* focus_manager) + : focus_manager_(focus_manager), + parent_view_(parent_view) { + DCHECK(focus_manager); + DCHECK(parent_view); + view_storage_ = ViewStorage::GetInstance(); + last_focused_view_storage_id_ = view_storage_->CreateStorageID(); + // Store the view which is focused when we're created. + StartTracking(); +} + +ExternalFocusTracker::~ExternalFocusTracker() { + view_storage_->RemoveView(last_focused_view_storage_id_); + if (focus_manager_) + focus_manager_->RemoveFocusChangeListener(this); +} + +void ExternalFocusTracker::OnWillChangeFocus(View* focused_before, + View* focused_now) { + if (focused_now && !parent_view_->Contains(focused_now) && + parent_view_ != focused_now) { + // Store the newly focused view. + StoreLastFocusedView(focused_now); + } +} + +void ExternalFocusTracker::OnDidChangeFocus(View* focused_before, + View* focused_now) { +} + +void ExternalFocusTracker::FocusLastFocusedExternalView() { + View* last_focused_view = + view_storage_->RetrieveView(last_focused_view_storage_id_); + if (last_focused_view) + last_focused_view->RequestFocus(); +} + +void ExternalFocusTracker::SetFocusManager(FocusManager* focus_manager) { + if (focus_manager_) + focus_manager_->RemoveFocusChangeListener(this); + focus_manager_ = focus_manager; + if (focus_manager_) + StartTracking(); +} + +void ExternalFocusTracker::StoreLastFocusedView(View* view) { + view_storage_->RemoveView(last_focused_view_storage_id_); + // If the view is NULL, remove the last focused view from storage, but don't + // try to store NULL. + if (view != NULL) + view_storage_->StoreView(last_focused_view_storage_id_, view); +} + +void ExternalFocusTracker::StartTracking() { + StoreLastFocusedView(focus_manager_->GetFocusedView()); + focus_manager_->AddFocusChangeListener(this); +} + +} // namespace views diff --git a/ui/views/focus/external_focus_tracker.h b/ui/views/focus/external_focus_tracker.h new file mode 100644 index 0000000..0ea1479 --- /dev/null +++ b/ui/views/focus/external_focus_tracker.h @@ -0,0 +1,78 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_VIEWS_FOCUS_EXTERNAL_FOCUS_TRACKER_H_ +#define UI_VIEWS_FOCUS_EXTERNAL_FOCUS_TRACKER_H_ +#pragma once + +#include "views/focus/focus_manager.h" + +namespace views { + +class View; +class ViewStorage; + +// ExternalFocusTracker tracks the last focused view which belongs to the +// provided focus manager and is not either the provided parent view or one of +// its descendants. This is generally used if the parent view want to return +// focus to some other view once it is dismissed. The parent view and the focus +// manager must exist for the duration of the tracking. If the focus manager +// must be deleted before this object is deleted, make sure to call +// SetFocusManager(NULL) first. +// +// Typical use: When a view is added to the view hierarchy, it instantiates an +// ExternalFocusTracker and passes in itself and its focus manager. Then, +// when that view wants to return focus to the last focused view which is not +// itself and not a descandant of itself, (usually when it is being closed) +// it calls FocusLastFocusedExternalView. +class VIEWS_EXPORT ExternalFocusTracker : public FocusChangeListener { + public: + ExternalFocusTracker(View* parent_view, FocusManager* focus_manager); + + virtual ~ExternalFocusTracker(); + // FocusChangeListener implementation. + virtual void OnWillChangeFocus(View* focused_before, View* focused_now); + virtual void OnDidChangeFocus(View* focused_before, View* focused_now); + + // Focuses last focused view which is not a child of parent view and is not + // parent view itself. Returns true if focus for a view was requested, false + // otherwise. + void FocusLastFocusedExternalView(); + + // Sets the focus manager whose focus we are tracking. |focus_manager| can + // be NULL, but no focus changes will be tracked. This is useful if the focus + // manager went away, but you might later want to start tracking with a new + // manager later, or call FocusLastFocusedExternalView to focus the previous + // view. + void SetFocusManager(FocusManager* focus_manager); + + private: + // Store the provided view. This view will be focused when + // FocusLastFocusedExternalView is called. + void StoreLastFocusedView(View* view); + + // Store the currently focused view for our view manager and register as a + // listener for future focus changes. + void StartTracking(); + + // Focus manager which we are a listener for. + FocusManager* focus_manager_; + + // ID of the last focused view, which we store in view_storage_. + int last_focused_view_storage_id_; + + // Used to store the last focused view which is not a child of + // ExternalFocusTracker. + ViewStorage* view_storage_; + + // The parent view of views which we should not track focus changes to. We + // also do not track changes to parent_view_ itself. + View* parent_view_; + + DISALLOW_COPY_AND_ASSIGN(ExternalFocusTracker); +}; + +} // namespace views + +#endif // UI_VIEWS_FOCUS_EXTERNAL_FOCUS_TRACKER_H_ diff --git a/ui/views/focus/focus_manager.cc b/ui/views/focus/focus_manager.cc new file mode 100644 index 0000000..3ee0755 --- /dev/null +++ b/ui/views/focus/focus_manager.cc @@ -0,0 +1,413 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "views/focus/focus_manager.h" + +#include <algorithm> + +#include "base/auto_reset.h" +#include "base/logging.h" +#include "build/build_config.h" +#include "ui/base/accelerator_manager.h" +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/base/models/accelerator.h" +#include "views/focus/focus_search.h" +#include "views/focus/view_storage.h" +#include "views/focus/widget_focus_manager.h" +#include "views/view.h" +#include "views/widget/root_view.h" +#include "views/widget/widget.h" + +namespace views { + +FocusManager::FocusManager(Widget* widget) + : widget_(widget), + focused_view_(NULL), + accelerator_manager_(new ui::AcceleratorManager), + focus_change_reason_(kReasonDirectFocusChange), + is_changing_focus_(false) { + DCHECK(widget_); + stored_focused_view_storage_id_ = + ViewStorage::GetInstance()->CreateStorageID(); +} + +FocusManager::~FocusManager() { +} + +bool FocusManager::OnKeyEvent(const KeyEvent& event) { +#if defined(OS_WIN) + // If the focused view wants to process the key event as is, let it be. + // On Linux we always dispatch key events to the focused view first, so + // we should not do this check here. See also NativeWidgetGtk::OnKeyEvent(). + if (focused_view_ && focused_view_->SkipDefaultKeyEventProcessing(event)) + return true; +#endif + + // Intercept Tab related messages for focus traversal. + // Note that we don't do focus traversal if the root window is not part of the + // active window hierarchy as this would mean we have no focused view and + // would focus the first focusable view. +#if defined(OS_WIN) && !defined(USE_AURA) + HWND top_window = widget_->GetNativeView(); + HWND active_window = ::GetActiveWindow(); + if ((active_window == top_window || ::IsChild(active_window, top_window)) && + IsTabTraversalKeyEvent(event)) { + AdvanceFocus(event.IsShiftDown()); + return false; + } +#else + if (IsTabTraversalKeyEvent(event)) { + AdvanceFocus(event.IsShiftDown()); + return false; + } +#endif + + // Intercept arrow key messages to switch between grouped views. + ui::KeyboardCode key_code = event.key_code(); + if (focused_view_ && focused_view_->GetGroup() != -1 && + (key_code == ui::VKEY_UP || key_code == ui::VKEY_DOWN || + key_code == ui::VKEY_LEFT || key_code == ui::VKEY_RIGHT)) { + bool next = (key_code == ui::VKEY_RIGHT || key_code == ui::VKEY_DOWN); + View::Views views; + focused_view_->parent()->GetViewsInGroup(focused_view_->GetGroup(), &views); + View::Views::const_iterator i( + std::find(views.begin(), views.end(), focused_view_)); + DCHECK(i != views.end()); + int index = static_cast<int>(i - views.begin()); + index += next ? 1 : -1; + if (index < 0) { + index = static_cast<int>(views.size()) - 1; + } else if (index >= static_cast<int>(views.size())) { + index = 0; + } + SetFocusedViewWithReason(views[index], kReasonFocusTraversal); + return false; + } + + // Process keyboard accelerators. + // If the key combination matches an accelerator, the accelerator is + // triggered, otherwise the key event is processed as usual. + ui::Accelerator accelerator(event.key_code(), + event.IsShiftDown(), + event.IsControlDown(), + event.IsAltDown()); + if (ProcessAccelerator(accelerator)) { + // If a shortcut was activated for this keydown message, do not propagate + // the event further. + return false; + } + return true; +} + +void FocusManager::ValidateFocusedView() { + if (focused_view_) { + if (!ContainsView(focused_view_)) + ClearFocus(); + } +} + +// Tests whether a view is valid, whether it still belongs to the window +// hierarchy of the FocusManager. +bool FocusManager::ContainsView(View* view) { + Widget* widget = view->GetWidget(); + return widget ? widget->GetFocusManager() == this : false; +} + +void FocusManager::AdvanceFocus(bool reverse) { + View* v = GetNextFocusableView(focused_view_, reverse, false); + // Note: Do not skip this next block when v == focused_view_. If the user + // tabs past the last focusable element in a webpage, we'll get here, and if + // the TabContentsContainerView is the only focusable view (possible in + // fullscreen mode), we need to run this block in order to cycle around to the + // first element on the page. + if (v) { + v->AboutToRequestFocusFromTabTraversal(reverse); + SetFocusedViewWithReason(v, kReasonFocusTraversal); + } +} + +void FocusManager::ClearNativeFocus() { + // Keep the top root window focused so we get keyboard events. + widget_->ClearNativeFocus(); +} + +View* FocusManager::GetNextFocusableView(View* original_starting_view, + bool reverse, + bool dont_loop) { + FocusTraversable* focus_traversable = NULL; + + // Let's revalidate the focused view. + ValidateFocusedView(); + + View* starting_view = NULL; + if (original_starting_view) { + // Search up the containment hierarchy to see if a view is acting as + // a pane, and wants to implement its own focus traversable to keep + // the focus trapped within that pane. + View* pane_search = original_starting_view; + while (pane_search) { + focus_traversable = pane_search->GetPaneFocusTraversable(); + if (focus_traversable) { + starting_view = original_starting_view; + break; + } + pane_search = pane_search->parent(); + } + + if (!focus_traversable) { + if (!reverse) { + // If the starting view has a focus traversable, use it. + // This is the case with NativeWidgetWins for example. + focus_traversable = original_starting_view->GetFocusTraversable(); + + // Otherwise default to the root view. + if (!focus_traversable) { + focus_traversable = + original_starting_view->GetWidget()->GetFocusTraversable(); + starting_view = original_starting_view; + } + } else { + // When you are going back, starting view's FocusTraversable + // should not be used. + focus_traversable = + original_starting_view->GetWidget()->GetFocusTraversable(); + starting_view = original_starting_view; + } + } + } else { + focus_traversable = widget_->GetFocusTraversable(); + } + + // Traverse the FocusTraversable tree down to find the focusable view. + View* v = FindFocusableView(focus_traversable, starting_view, reverse); + if (v) { + return v; + } else { + // Let's go up in the FocusTraversable tree. + FocusTraversable* parent_focus_traversable = + focus_traversable->GetFocusTraversableParent(); + starting_view = focus_traversable->GetFocusTraversableParentView(); + while (parent_focus_traversable) { + FocusTraversable* new_focus_traversable = NULL; + View* new_starting_view = NULL; + // When we are going backward, the parent view might gain the next focus. + bool check_starting_view = reverse; + v = parent_focus_traversable->GetFocusSearch()->FindNextFocusableView( + starting_view, reverse, FocusSearch::UP, + check_starting_view, &new_focus_traversable, &new_starting_view); + + if (new_focus_traversable) { + DCHECK(!v); + + // There is a FocusTraversable, traverse it down. + v = FindFocusableView(new_focus_traversable, NULL, reverse); + } + + if (v) + return v; + + starting_view = focus_traversable->GetFocusTraversableParentView(); + parent_focus_traversable = + parent_focus_traversable->GetFocusTraversableParent(); + } + + // If we get here, we have reached the end of the focus hierarchy, let's + // loop. Make sure there was at least a view to start with, to prevent + // infinitely looping in empty windows. + if (!dont_loop && original_starting_view) { + // Easy, just clear the selection and press tab again. + // By calling with NULL as the starting view, we'll start from the + // top_root_view. + return GetNextFocusableView(NULL, reverse, true); + } + } + return NULL; +} + +void FocusManager::SetFocusedViewWithReason( + View* view, FocusChangeReason reason) { + if (focused_view_ == view) + return; + + AutoReset<bool> auto_changing_focus(&is_changing_focus_, true); + // Update the reason for the focus change (since this is checked by + // some listeners), then notify all listeners. + focus_change_reason_ = reason; + FOR_EACH_OBSERVER(FocusChangeListener, focus_change_listeners_, + OnWillChangeFocus(focused_view_, view)); + + View* old_focused_view = focused_view_; + focused_view_ = view; + if (old_focused_view) + old_focused_view->Blur(); + if (focused_view_) + focused_view_->Focus(); + + FOR_EACH_OBSERVER(FocusChangeListener, focus_change_listeners_, + OnDidChangeFocus(old_focused_view, focused_view_)); +} + +void FocusManager::ClearFocus() { + SetFocusedView(NULL); + ClearNativeFocus(); +} + +void FocusManager::StoreFocusedView() { + ViewStorage* view_storage = ViewStorage::GetInstance(); + if (!view_storage) { + // This should never happen but bug 981648 seems to indicate it could. + NOTREACHED(); + return; + } + + // TODO (jcampan): when a TabContents containing a popup is closed, the focus + // is stored twice causing an assert. We should find a better alternative than + // removing the view from the storage explicitly. + view_storage->RemoveView(stored_focused_view_storage_id_); + + if (!focused_view_) + return; + + view_storage->StoreView(stored_focused_view_storage_id_, focused_view_); + + View* v = focused_view_; + + { + // Temporarily disable notification. ClearFocus() will set the focus to the + // main browser window. This extra focus bounce which happens during + // deactivation can confuse registered WidgetFocusListeners, as the focus + // is not changing due to a user-initiated event. + AutoNativeNotificationDisabler local_notification_disabler; + ClearFocus(); + } + + if (v) + v->SchedulePaint(); // Remove focus border. +} + +void FocusManager::RestoreFocusedView() { + ViewStorage* view_storage = ViewStorage::GetInstance(); + if (!view_storage) { + // This should never happen but bug 981648 seems to indicate it could. + NOTREACHED(); + return; + } + + View* view = view_storage->RetrieveView(stored_focused_view_storage_id_); + if (view) { + if (ContainsView(view)) { + if (!view->IsFocusableInRootView() && + view->IsAccessibilityFocusableInRootView()) { + // RequestFocus would fail, but we want to restore focus to controls + // that had focus in accessibility mode. + SetFocusedViewWithReason(view, kReasonFocusRestore); + } else { + // This usually just sets the focus if this view is focusable, but + // let the view override RequestFocus if necessary. + view->RequestFocus(); + + // If it succeeded, the reason would be incorrect; set it to + // focus restore. + if (focused_view_ == view) + focus_change_reason_ = kReasonFocusRestore; + } + } + } +} + +void FocusManager::ClearStoredFocusedView() { + ViewStorage* view_storage = ViewStorage::GetInstance(); + if (!view_storage) { + // This should never happen but bug 981648 seems to indicate it could. + NOTREACHED(); + return; + } + view_storage->RemoveView(stored_focused_view_storage_id_); +} + +// Find the next (previous if reverse is true) focusable view for the specified +// FocusTraversable, starting at the specified view, traversing down the +// FocusTraversable hierarchy. +View* FocusManager::FindFocusableView(FocusTraversable* focus_traversable, + View* starting_view, + bool reverse) { + FocusTraversable* new_focus_traversable = NULL; + View* new_starting_view = NULL; + View* v = focus_traversable->GetFocusSearch()->FindNextFocusableView( + starting_view, + reverse, + FocusSearch::DOWN, + false, + &new_focus_traversable, + &new_starting_view); + + // Let's go down the FocusTraversable tree as much as we can. + while (new_focus_traversable) { + DCHECK(!v); + focus_traversable = new_focus_traversable; + starting_view = new_starting_view; + new_focus_traversable = NULL; + starting_view = NULL; + v = focus_traversable->GetFocusSearch()->FindNextFocusableView( + starting_view, + reverse, + FocusSearch::DOWN, + false, + &new_focus_traversable, + &new_starting_view); + } + return v; +} + +void FocusManager::RegisterAccelerator( + const ui::Accelerator& accelerator, + ui::AcceleratorTarget* target) { + accelerator_manager_->Register(accelerator, target); +} + +void FocusManager::UnregisterAccelerator(const ui::Accelerator& accelerator, + ui::AcceleratorTarget* target) { + accelerator_manager_->Unregister(accelerator, target); +} + +void FocusManager::UnregisterAccelerators(ui::AcceleratorTarget* target) { + accelerator_manager_->UnregisterAll(target); +} + +bool FocusManager::ProcessAccelerator(const ui::Accelerator& accelerator) { + return accelerator_manager_->Process(accelerator); +} + +ui::AcceleratorTarget* FocusManager::GetCurrentTargetForAccelerator( + const ui::Accelerator& accelerator) const { + return accelerator_manager_->GetCurrentTarget(accelerator); +} + +void FocusManager::FocusNativeView(gfx::NativeView native_view) { + widget_->FocusNativeView(native_view); +} + +// static +bool FocusManager::IsTabTraversalKeyEvent(const KeyEvent& key_event) { + return key_event.key_code() == ui::VKEY_TAB && !key_event.IsControlDown(); +} + +void FocusManager::ViewRemoved(View* removed) { + // If the view being removed contains (or is) the focused view, + // clear the focus. However, it's not safe to call ClearFocus() + // (and in turn ClearNativeFocus()) here because ViewRemoved() can + // be called while the top level widget is being destroyed. + if (focused_view_ && removed && removed->Contains(focused_view_)) + SetFocusedView(NULL); +} + +void FocusManager::AddFocusChangeListener(FocusChangeListener* listener) { + focus_change_listeners_.AddObserver(listener); +} + +void FocusManager::RemoveFocusChangeListener(FocusChangeListener* listener) { + focus_change_listeners_.RemoveObserver(listener); +} + +} // namespace views diff --git a/ui/views/focus/focus_manager.h b/ui/views/focus/focus_manager.h new file mode 100644 index 0000000..0fdecc1 --- /dev/null +++ b/ui/views/focus/focus_manager.h @@ -0,0 +1,283 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_VIEWS_FOCUS_FOCUS_MANAGER_H_ +#define UI_VIEWS_FOCUS_FOCUS_MANAGER_H_ +#pragma once + +#include <list> +#include <map> + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "base/observer_list.h" +#include "ui/base/models/accelerator.h" +#include "ui/gfx/native_widget_types.h" +#include "views/views_export.h" +#include "views/events/event.h" + +// The FocusManager class is used to handle focus traversal, store/restore +// focused views and handle keyboard accelerators. +// +// There are 2 types of focus: +// - the native focus, which is the focus that an gfx::NativeView has. +// - the view focus, which is the focus that a views::View has. +// +// Each native view must register with their Focus Manager so the focus manager +// gets notified when they are focused (and keeps track of the native focus) and +// as well so that the tab key events can be intercepted. +// They can provide when they register a View that is kept in synch in term of +// focus. This is used in NativeControl for example, where a View wraps an +// actual native window. +// This is already done for you if you subclass the NativeControl class or if +// you use the NativeViewHost class. +// +// When creating a top window (derived from views::Widget) that is not a child +// window, it creates and owns a FocusManager to manage the focus for itself and +// all its child windows. +// +// The FocusTraversable interface exposes the methods a class should implement +// in order to be able to be focus traversed when tab key is pressed. +// RootViews implement FocusTraversable. +// The FocusManager contains a top FocusTraversable instance, which is the top +// RootView. +// +// If you just use views, then the focus traversal is handled for you by the +// RootView. The default traversal order is the order in which the views have +// been added to their container. You can modify this order by using the View +// method SetNextFocusableView(). +// +// If you are embedding a native view containing a nested RootView (for example +// by adding a NativeControl that contains a NativeWidgetWin as its native +// component), then you need to: +// - override the View::GetFocusTraversable() method in your outer component. +// It should return the RootView of the inner component. This is used when +// the focus traversal traverse down the focus hierarchy to enter the nested +// RootView. In the example mentioned above, the NativeControl overrides +// GetFocusTraversable() and returns hwnd_view_container_->GetRootView(). +// - call Widget::SetFocusTraversableParent() on the nested RootView and point +// it to the outer RootView. This is used when the focus goes out of the +// nested RootView. In the example: +// hwnd_view_container_->GetWidget()->SetFocusTraversableParent( +// native_control->GetRootView()); +// - call RootView::SetFocusTraversableParentView() on the nested RootView with +// the parent view that directly contains the native window. This is needed +// when traversing up from the nested RootView to know which view to start +// with when going to the next/previous view. +// In our example: +// hwnd_view_container_->GetWidget()->SetFocusTraversableParent( +// native_control); +// +// Note that FocusTraversable do not have to be RootViews: AccessibleToolbarView +// is FocusTraversable. + +namespace ui { +class AcceleratorTarget; +class AcceleratorManager; +} + +namespace views { + +class FocusSearch; +class RootView; +class View; +class Widget; + +// The FocusTraversable interface is used by components that want to process +// focus traversal events (due to Tab/Shift-Tab key events). +class VIEWS_EXPORT FocusTraversable { + public: + // Return a FocusSearch object that implements the algorithm to find + // the next or previous focusable view. + virtual FocusSearch* GetFocusSearch() = 0; + + // Should return the parent FocusTraversable. + // The top RootView which is the top FocusTraversable returns NULL. + virtual FocusTraversable* GetFocusTraversableParent() = 0; + + // This should return the View this FocusTraversable belongs to. + // It is used when walking up the view hierarchy tree to find which view + // should be used as the starting view for finding the next/previous view. + virtual View* GetFocusTraversableParentView() = 0; + + protected: + virtual ~FocusTraversable() {} +}; + +// This interface should be implemented by classes that want to be notified when +// the focus is about to change. See the Add/RemoveFocusChangeListener methods. +class VIEWS_EXPORT FocusChangeListener { + public: + // No change to focus state has occurred yet when this function is called. + virtual void OnWillChangeFocus(View* focused_before, View* focused_now) = 0; + + // Called after focus state has changed. + virtual void OnDidChangeFocus(View* focused_before, View* focused_now) = 0; + + protected: + virtual ~FocusChangeListener() {} +}; + +class VIEWS_EXPORT FocusManager { + public: + // The reason why the focus changed. + enum FocusChangeReason { + // The focus changed because the user traversed focusable views using + // keys like Tab or Shift+Tab. + kReasonFocusTraversal, + + // The focus changed due to restoring the focus. + kReasonFocusRestore, + + // The focus changed due to a click or a shortcut to jump directly to + // a particular view. + kReasonDirectFocusChange + }; + + explicit FocusManager(Widget* widget); + virtual ~FocusManager(); + + // Processes the passed key event for accelerators and tab traversal. + // Returns false if the event has been consumed and should not be processed + // further. + bool OnKeyEvent(const KeyEvent& event); + + // Returns true is the specified is part of the hierarchy of the window + // associated with this FocusManager. + bool ContainsView(View* view); + + // Advances the focus (backward if reverse is true). + void AdvanceFocus(bool reverse); + + // The FocusManager keeps track of the focused view within a RootView. + View* GetFocusedView() { return focused_view_; } + const View* GetFocusedView() const { return focused_view_; } + + // Low-level methods to force the focus to change (and optionally provide + // a reason). If the focus change should only happen if the view is + // currenty focusable, enabled, and visible, call view->RequestFocus(). + void SetFocusedViewWithReason(View* view, FocusChangeReason reason); + void SetFocusedView(View* view) { + SetFocusedViewWithReason(view, kReasonDirectFocusChange); + } + + // Get the reason why the focus most recently changed. + FocusChangeReason focus_change_reason() const { + return focus_change_reason_; + } + + // Clears the focused view. The window associated with the top root view gets + // the native focus (so we still get keyboard events). + void ClearFocus(); + + // Validates the focused view, clearing it if the window it belongs too is not + // attached to the window hierarchy anymore. + void ValidateFocusedView(); + + // Stores and restores the focused view. Used when the window becomes + // active/inactive. + void StoreFocusedView(); + void RestoreFocusedView(); + + // Clears the stored focused view. + void ClearStoredFocusedView(); + + // Returns true if in the process of changing the focused view. + bool is_changing_focus() const { return is_changing_focus_; } + + // Register a keyboard accelerator for the specified target. If multiple + // targets are registered for an accelerator, a target registered later has + // higher priority. + // Note that we are currently limited to accelerators that are either: + // - a key combination including Ctrl or Alt + // - the escape key + // - the enter key + // - any F key (F1, F2, F3 ...) + // - any browser specific keys (as available on special keyboards) + void RegisterAccelerator(const ui::Accelerator& accelerator, + ui::AcceleratorTarget* target); + + // Unregister the specified keyboard accelerator for the specified target. + void UnregisterAccelerator(const ui::Accelerator& accelerator, + ui::AcceleratorTarget* target); + + // Unregister all keyboard accelerator for the specified target. + void UnregisterAccelerators(ui::AcceleratorTarget* target); + + // Activate the target associated with the specified accelerator. + // First, AcceleratorPressed handler of the most recently registered target + // is called, and if that handler processes the event (i.e. returns true), + // this method immediately returns. If not, we do the same thing on the next + // target, and so on. + // Returns true if an accelerator was activated. + bool ProcessAccelerator(const ui::Accelerator& accelerator); + + // Called by a RootView when a view within its hierarchy is removed + // from its parent. This will only be called by a RootView in a + // hierarchy of Widgets that this FocusManager is attached to the + // parent Widget of. + void ViewRemoved(View* removed); + + // Adds/removes a listener. The FocusChangeListener is notified every time + // the focused view is about to change. + void AddFocusChangeListener(FocusChangeListener* listener); + void RemoveFocusChangeListener(FocusChangeListener* listener); + + // Returns the AcceleratorTarget that should be activated for the specified + // keyboard accelerator, or NULL if no view is registered for that keyboard + // accelerator. + ui::AcceleratorTarget* GetCurrentTargetForAccelerator( + const ui::Accelerator& accelertor) const; + + // Sets the focus to the specified native view. + virtual void FocusNativeView(gfx::NativeView native_view); + + // Clears the native view having the focus. + virtual void ClearNativeFocus(); + + // Convenience method that returns true if the passed |key_event| should + // trigger tab traversal (if it is a TAB key press with or without SHIFT + // pressed). + static bool IsTabTraversalKeyEvent(const KeyEvent& key_event); + + private: + // Returns the next focusable view. + View* GetNextFocusableView(View* starting_view, bool reverse, bool dont_loop); + + // Returns the focusable view found in the FocusTraversable specified starting + // at the specified view. This traverses down along the FocusTraversable + // hierarchy. + // Returns NULL if no focusable view were found. + View* FindFocusableView(FocusTraversable* focus_traversable, + View* starting_view, + bool reverse); + + // The top-level Widget this FocusManager is associated with. + Widget* widget_; + + // The view that currently is focused. + View* focused_view_; + + // The AcceleratorManager this FocusManager is associated with. + scoped_ptr<ui::AcceleratorManager> accelerator_manager_; + + // The storage id used in the ViewStorage to store/restore the view that last + // had focus. + int stored_focused_view_storage_id_; + + // The reason why the focus most recently changed. + FocusChangeReason focus_change_reason_; + + // The list of registered FocusChange listeners. + ObserverList<FocusChangeListener, true> focus_change_listeners_; + + // See description above getter. + bool is_changing_focus_; + + DISALLOW_COPY_AND_ASSIGN(FocusManager); +}; + +} // namespace views + +#endif // UI_VIEWS_FOCUS_FOCUS_MANAGER_H_ diff --git a/ui/views/focus/focus_manager_factory.cc b/ui/views/focus/focus_manager_factory.cc new file mode 100644 index 0000000..457f60b --- /dev/null +++ b/ui/views/focus/focus_manager_factory.cc @@ -0,0 +1,55 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "views/focus/focus_manager_factory.h" + +#include "base/compiler_specific.h" +#include "views/focus/focus_manager.h" + +namespace { + +using views::FocusManager; + +class DefaultFocusManagerFactory : public views::FocusManagerFactory { + public: + DefaultFocusManagerFactory() : views::FocusManagerFactory() {} + virtual ~DefaultFocusManagerFactory() {} + + protected: + virtual FocusManager* CreateFocusManager(views::Widget* widget) OVERRIDE { + return new FocusManager(widget); + } + + private: + DISALLOW_COPY_AND_ASSIGN(DefaultFocusManagerFactory); +}; + +views::FocusManagerFactory* focus_manager_factory = NULL; + +} // namespace + +namespace views { + +FocusManagerFactory::FocusManagerFactory() { +} + +FocusManagerFactory::~FocusManagerFactory() { +} + +// static +FocusManager* FocusManagerFactory::Create(Widget* widget) { + if (!focus_manager_factory) + focus_manager_factory = new DefaultFocusManagerFactory(); + return focus_manager_factory->CreateFocusManager(widget); +} + +// static +void FocusManagerFactory::Install(FocusManagerFactory* f) { + if (f == focus_manager_factory) + return; + delete focus_manager_factory; + focus_manager_factory = f ? f : new DefaultFocusManagerFactory(); +} + +} // namespace views diff --git a/ui/views/focus/focus_manager_factory.h b/ui/views/focus/focus_manager_factory.h new file mode 100644 index 0000000..7aae11d --- /dev/null +++ b/ui/views/focus/focus_manager_factory.h @@ -0,0 +1,41 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_VIEWS_FOCUS_FOCUS_MANAGER_FACTORY_H_ +#define UI_VIEWS_FOCUS_FOCUS_MANAGER_FACTORY_H_ +#pragma once + +#include "base/basictypes.h" +#include "views/views_export.h" + +namespace views { + +class FocusManager; +class Widget; + +// A factory to create FocusManager. This is used in unit tests +// to inject a custom factory. +class VIEWS_EXPORT FocusManagerFactory { + public: + // Create a FocusManager for the given |widget| using installe Factory. + static FocusManager* Create(Widget* widget); + + // Installs FocusManagerFactory. If |factory| is NULL, it resets + // to the default factory which creates plain FocusManager. + static void Install(FocusManagerFactory* factory); + + protected: + FocusManagerFactory(); + virtual ~FocusManagerFactory(); + + // Create a FocusManager for the given |widget|. + virtual FocusManager* CreateFocusManager(Widget* widget) = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(FocusManagerFactory); +}; + +} // namespace views + +#endif // UI_VIEWS_FOCUS_FOCUS_MANAGER_FACTORY_H_ diff --git a/ui/views/focus/focus_manager_unittest.cc b/ui/views/focus/focus_manager_unittest.cc new file mode 100644 index 0000000..d4372f2 --- /dev/null +++ b/ui/views/focus/focus_manager_unittest.cc @@ -0,0 +1,1763 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/logging.h" +#include "base/string16.h" +#include "base/string_number_conversions.h" +#include "base/utf_string_conversions.h" +#include "third_party/skia/include/core/SkColor.h" +#include "ui/base/keycodes/keyboard_codes.h" +#include "ui/base/models/accelerator.h" +#include "ui/base/models/combobox_model.h" +#include "ui/gfx/rect.h" +#include "ui/views/test/views_test_base.h" +#include "ui/views/window/non_client_view.h" +#include "views/background.h" +#include "views/border.h" +#include "views/controls/button/checkbox.h" +#include "views/controls/button/radio_button.h" +#include "views/controls/combobox/combobox.h" +#include "views/controls/combobox/native_combobox_wrapper.h" +#include "views/controls/label.h" +#include "views/controls/link.h" +#include "views/controls/native/native_view_host.h" +#include "views/controls/scroll_view.h" +#include "views/controls/tabbed_pane/native_tabbed_pane_wrapper.h" +#include "views/controls/tabbed_pane/tabbed_pane.h" +#include "views/controls/textfield/textfield.h" +#include "views/focus/accelerator_handler.h" +#include "views/focus/focus_manager_factory.h" +#include "views/widget/root_view.h" +#include "views/widget/widget.h" +#include "views/widget/widget_delegate.h" + +#if defined(OS_LINUX) +#include "ui/base/keycodes/keyboard_code_conversion_gtk.h" +#endif + +namespace { +const int kWindowWidth = 600; +const int kWindowHeight = 500; + +int count = 1; + +const int kTopCheckBoxID = count++; // 1 +const int kLeftContainerID = count++; +const int kAppleLabelID = count++; +const int kAppleTextfieldID = count++; +const int kOrangeLabelID = count++; // 5 +const int kOrangeTextfieldID = count++; +const int kBananaLabelID = count++; +const int kBananaTextfieldID = count++; +const int kKiwiLabelID = count++; +const int kKiwiTextfieldID = count++; // 10 +const int kFruitButtonID = count++; +const int kFruitCheckBoxID = count++; +const int kComboboxID = count++; + +const int kRightContainerID = count++; +const int kAsparagusButtonID = count++; // 15 +const int kBroccoliButtonID = count++; +const int kCauliflowerButtonID = count++; + +const int kInnerContainerID = count++; +const int kScrollViewID = count++; +const int kRosettaLinkID = count++; // 20 +const int kStupeurEtTremblementLinkID = count++; +const int kDinerGameLinkID = count++; +const int kRidiculeLinkID = count++; +const int kClosetLinkID = count++; +const int kVisitingLinkID = count++; // 25 +const int kAmelieLinkID = count++; +const int kJoyeuxNoelLinkID = count++; +const int kCampingLinkID = count++; +const int kBriceDeNiceLinkID = count++; +const int kTaxiLinkID = count++; // 30 +const int kAsterixLinkID = count++; + +const int kOKButtonID = count++; +const int kCancelButtonID = count++; +const int kHelpButtonID = count++; + +const int kStyleContainerID = count++; // 35 +const int kBoldCheckBoxID = count++; +const int kItalicCheckBoxID = count++; +const int kUnderlinedCheckBoxID = count++; +const int kStyleHelpLinkID = count++; +const int kStyleTextEditID = count++; // 40 + +const int kSearchContainerID = count++; +const int kSearchTextfieldID = count++; +const int kSearchButtonID = count++; +const int kHelpLinkID = count++; + +const int kThumbnailContainerID = count++; // 45 +const int kThumbnailStarID = count++; +const int kThumbnailSuperStarID = count++; + +} // namespace + +namespace views { + +class FocusManagerTest : public ViewsTestBase, public WidgetDelegate { + public: + FocusManagerTest() + : window_(NULL), + content_view_(NULL), + focus_change_listener_(NULL) { + } + + ~FocusManagerTest() { + } + + virtual void SetUp() OVERRIDE { + ViewsTestBase::SetUp(); + window_ = Widget::CreateWindowWithBounds(this, bounds()); + InitContentView(); + window_->Show(); + } + + virtual void TearDown() OVERRIDE { + if (focus_change_listener_) + GetFocusManager()->RemoveFocusChangeListener(focus_change_listener_); + window_->Close(); + + // Flush the message loop to make application verifiers happy. + RunPendingMessages(); + ViewsTestBase::TearDown(); + } + + FocusManager* GetFocusManager() { + return window_->GetFocusManager(); + } + + void FocusNativeView(gfx::NativeView native_view) { +#if defined(USE_AURA) + NOTIMPLEMENTED(); +#elif defined(OS_WIN) + ::SendMessage(native_view, WM_SETFOCUS, NULL, NULL); +#else + gint return_val; + GdkEventFocus event; + event.type = GDK_FOCUS_CHANGE; + event.window = + gtk_widget_get_root_window(GTK_WIDGET(window_->GetNativeWindow())); + event.send_event = TRUE; + event.in = TRUE; + gtk_signal_emit_by_name(GTK_OBJECT(native_view), "focus-in-event", + &event, &return_val); +#endif + } + + // WidgetDelegate Implementation. + virtual View* GetContentsView() OVERRIDE { + if (!content_view_) + content_view_ = new View(); + return content_view_; + } + virtual Widget* GetWidget() OVERRIDE { + return content_view_->GetWidget(); + } + virtual const Widget* GetWidget() const OVERRIDE { + return content_view_->GetWidget(); + } + + virtual void InitContentView() { + } + + protected: + virtual gfx::Rect bounds() { + return gfx::Rect(0, 0, 500, 500); + } + + // Mocks activating/deactivating the window. + void SimulateActivateWindow() { +#if defined(USE_AURA) + NOTIMPLEMENTED(); +#elif defined(OS_WIN) + ::SendMessage(window_->GetNativeWindow(), WM_ACTIVATE, WA_ACTIVE, NULL); +#else + gboolean result; + g_signal_emit_by_name(G_OBJECT(window_->GetNativeWindow()), + "focus_in_event", 0, &result); +#endif + } + void SimulateDeactivateWindow() { +#if defined(USE_AURA) + NOTIMPLEMENTED(); +#elif defined(OS_WIN) + ::SendMessage(window_->GetNativeWindow(), WM_ACTIVATE, WA_INACTIVE, NULL); +#else + gboolean result; + g_signal_emit_by_name(G_OBJECT(window_->GetNativeWindow()), + "focus_out_event", 0, & result); +#endif + } + + Widget* window_; + View* content_view_; + + void AddFocusChangeListener(FocusChangeListener* listener) { + ASSERT_FALSE(focus_change_listener_); + focus_change_listener_ = listener; + GetFocusManager()->AddFocusChangeListener(listener); + } + +#if defined(USE_AURA) + void PostKeyDown(ui::KeyboardCode key_code) { + NOTIMPLEMENTED(); + } + + void PostKeyUp(ui::KeyboardCode key_code) { + NOTIMPLEMENTED(); + } +#elif defined(OS_WIN) + void PostKeyDown(ui::KeyboardCode key_code) { + ::PostMessage(window_->GetNativeWindow(), WM_KEYDOWN, key_code, 0); + } + + void PostKeyUp(ui::KeyboardCode key_code) { + ::PostMessage(window_->GetNativeWindow(), WM_KEYUP, key_code, 0); + } +#elif defined(OS_LINUX) + void PostKeyDown(ui::KeyboardCode key_code) { + PostKeyEvent(key_code, true); + } + + void PostKeyUp(ui::KeyboardCode key_code) { + PostKeyEvent(key_code, false); + } + + void PostKeyEvent(ui::KeyboardCode key_code, bool pressed) { + int keyval = GdkKeyCodeForWindowsKeyCode(key_code, false); + GdkKeymapKey* keys; + gint n_keys; + gdk_keymap_get_entries_for_keyval( + gdk_keymap_get_default(), + keyval, + &keys, + &n_keys); + GdkEvent* event = gdk_event_new(pressed ? GDK_KEY_PRESS : GDK_KEY_RELEASE); + GdkEventKey* key_event = reinterpret_cast<GdkEventKey*>(event); + int modifier = 0; + if (pressed) + key_event->state = modifier | GDK_KEY_PRESS_MASK; + else + key_event->state = modifier | GDK_KEY_RELEASE_MASK; + + key_event->window = GTK_WIDGET(window_->GetNativeWindow())->window; + DCHECK(key_event->window != NULL); + g_object_ref(key_event->window); + key_event->send_event = true; + key_event->time = GDK_CURRENT_TIME; + key_event->keyval = keyval; + key_event->hardware_keycode = keys[0].keycode; + key_event->group = keys[0].group; + + g_free(keys); + + gdk_event_put(event); + gdk_event_free(event); + } +#endif + + private: + FocusChangeListener* focus_change_listener_; + + DISALLOW_COPY_AND_ASSIGN(FocusManagerTest); +}; + +// BorderView is a view containing a native window with its own view hierarchy. +// It is interesting to test focus traversal from a view hierarchy to an inner +// view hierarchy. +class BorderView : public NativeViewHost { + public: + explicit BorderView(View* child) : child_(child), widget_(NULL) { + DCHECK(child); + set_focusable(false); + } + + virtual ~BorderView() {} + + virtual internal::RootView* GetContentsRootView() { + return static_cast<internal::RootView*>(widget_->GetRootView()); + } + + virtual FocusTraversable* GetFocusTraversable() { + return static_cast<internal::RootView*>(widget_->GetRootView()); + } + + virtual void ViewHierarchyChanged(bool is_add, View *parent, View *child) { + NativeViewHost::ViewHierarchyChanged(is_add, parent, child); + + if (child == this && is_add) { + if (!widget_) { + widget_ = new Widget; + Widget::InitParams params(Widget::InitParams::TYPE_CONTROL); +#if defined(OS_WIN) + params.parent = parent->GetWidget()->GetNativeView(); +#elif defined(TOOLKIT_USES_GTK) + params.parent = native_view(); +#else + NOTREACHED(); +#endif + widget_->Init(params); + widget_->SetFocusTraversableParentView(this); + widget_->SetContentsView(child_); + } + + // We have been added to a view hierarchy, attach the native view. + Attach(widget_->GetNativeView()); + // Also update the FocusTraversable parent so the focus traversal works. + static_cast<internal::RootView*>(widget_->GetRootView())-> + SetFocusTraversableParent(GetWidget()->GetFocusTraversable()); + } + } + + private: + View* child_; + Widget* widget_; + + DISALLOW_COPY_AND_ASSIGN(BorderView); +}; + +class DummyComboboxModel : public ui::ComboboxModel { + public: + virtual int GetItemCount() { return 10; } + + virtual string16 GetItemAt(int index) { + return ASCIIToUTF16("Item ") + base::IntToString16(index); + } +}; + +// A View that can act as a pane. +class PaneView : public View, public FocusTraversable { + public: + PaneView() : focus_search_(NULL) {} + + // If this method is called, this view will use GetPaneFocusTraversable to + // have this provided FocusSearch used instead of the default one, allowing + // you to trap focus within the pane. + void EnablePaneFocus(FocusSearch* focus_search) { + focus_search_ = focus_search; + } + + // Overridden from views::View: + virtual FocusTraversable* GetPaneFocusTraversable() { + if (focus_search_) + return this; + else + return NULL; + } + + // Overridden from views::FocusTraversable: + virtual views::FocusSearch* GetFocusSearch() { + return focus_search_; + } + virtual FocusTraversable* GetFocusTraversableParent() { + return NULL; + } + virtual View* GetFocusTraversableParentView() { + return NULL; + } + + private: + FocusSearch* focus_search_; +}; + +class FocusTraversalTest : public FocusManagerTest { + public: + ~FocusTraversalTest(); + + virtual void InitContentView(); + + protected: + FocusTraversalTest(); + + virtual gfx::Rect bounds() { + return gfx::Rect(0, 0, 600, 460); + } + + View* FindViewByID(int id) { + View* view = GetContentsView()->GetViewByID(id); + if (view) + return view; + if (style_tab_) + view = style_tab_->GetSelectedTab()->GetViewByID(id); + if (view) + return view; + view = search_border_view_->GetContentsRootView()->GetViewByID(id); + if (view) + return view; + return NULL; + } + + protected: + TabbedPane* style_tab_; + BorderView* search_border_view_; + DummyComboboxModel combobox_model_; + PaneView* left_container_; + PaneView* right_container_; + + DISALLOW_COPY_AND_ASSIGN(FocusTraversalTest); +}; + +//////////////////////////////////////////////////////////////////////////////// +// FocusTraversalTest +//////////////////////////////////////////////////////////////////////////////// + +FocusTraversalTest::FocusTraversalTest() + : style_tab_(NULL), + search_border_view_(NULL) { +} + +FocusTraversalTest::~FocusTraversalTest() { +} + +void FocusTraversalTest::InitContentView() { + // Create a complicated view hierarchy with lots of control types for + // use by all of the focus traversal tests. + // + // Class name, ID, and asterisk next to focusable views: + // + // View + // Checkbox * kTopCheckBoxID + // PaneView kLeftContainerID + // Label kAppleLabelID + // Textfield * kAppleTextfieldID + // Label kOrangeLabelID + // Textfield * kOrangeTextfieldID + // Label kBananaLabelID + // Textfield * kBananaTextfieldID + // Label kKiwiLabelID + // Textfield * kKiwiTextfieldID + // NativeButton * kFruitButtonID + // Checkbox * kFruitCheckBoxID + // Combobox * kComboboxID + // PaneView kRightContainerID + // RadioButton * kAsparagusButtonID + // RadioButton * kBroccoliButtonID + // RadioButton * kCauliflowerButtonID + // View kInnerContainerID + // ScrollView kScrollViewID + // View + // Link * kRosettaLinkID + // Link * kStupeurEtTremblementLinkID + // Link * kDinerGameLinkID + // Link * kRidiculeLinkID + // Link * kClosetLinkID + // Link * kVisitingLinkID + // Link * kAmelieLinkID + // Link * kJoyeuxNoelLinkID + // Link * kCampingLinkID + // Link * kBriceDeNiceLinkID + // Link * kTaxiLinkID + // Link * kAsterixLinkID + // NativeButton * kOKButtonID + // NativeButton * kCancelButtonID + // NativeButton * kHelpButtonID + // TabbedPane * kStyleContainerID + // View + // Checkbox * kBoldCheckBoxID + // Checkbox * kItalicCheckBoxID + // Checkbox * kUnderlinedCheckBoxID + // Link * kStyleHelpLinkID + // Textfield * kStyleTextEditID + // Other + // BorderView kSearchContainerID + // View + // Textfield * kSearchTextfieldID + // NativeButton * kSearchButtonID + // Link * kHelpLinkID + // View * kThumbnailContainerID + // NativeButton * kThumbnailStarID + // NativeButton * kThumbnailSuperStarID + + content_view_->set_background( + Background::CreateSolidBackground(SK_ColorWHITE)); + + Checkbox* cb = new Checkbox(ASCIIToUTF16("This is a checkbox")); + content_view_->AddChildView(cb); + // In this fast paced world, who really has time for non hard-coded layout? + cb->SetBounds(10, 10, 200, 20); + cb->set_id(kTopCheckBoxID); + + left_container_ = new PaneView(); + left_container_->set_border(Border::CreateSolidBorder(1, SK_ColorBLACK)); + left_container_->set_background( + Background::CreateSolidBackground(240, 240, 240)); + left_container_->set_id(kLeftContainerID); + content_view_->AddChildView(left_container_); + left_container_->SetBounds(10, 35, 250, 200); + + int label_x = 5; + int label_width = 50; + int label_height = 15; + int text_field_width = 150; + int y = 10; + int gap_between_labels = 10; + + Label* label = new Label(ASCIIToUTF16("Apple:")); + label->set_id(kAppleLabelID); + left_container_->AddChildView(label); + label->SetBounds(label_x, y, label_width, label_height); + + Textfield* text_field = new Textfield(); + text_field->set_id(kAppleTextfieldID); + left_container_->AddChildView(text_field); + text_field->SetBounds(label_x + label_width + 5, y, + text_field_width, label_height); + + y += label_height + gap_between_labels; + + label = new Label(ASCIIToUTF16("Orange:")); + label->set_id(kOrangeLabelID); + left_container_->AddChildView(label); + label->SetBounds(label_x, y, label_width, label_height); + + text_field = new Textfield(); + text_field->set_id(kOrangeTextfieldID); + left_container_->AddChildView(text_field); + text_field->SetBounds(label_x + label_width + 5, y, + text_field_width, label_height); + + y += label_height + gap_between_labels; + + label = new Label(ASCIIToUTF16("Banana:")); + label->set_id(kBananaLabelID); + left_container_->AddChildView(label); + label->SetBounds(label_x, y, label_width, label_height); + + text_field = new Textfield(); + text_field->set_id(kBananaTextfieldID); + left_container_->AddChildView(text_field); + text_field->SetBounds(label_x + label_width + 5, y, + text_field_width, label_height); + + y += label_height + gap_between_labels; + + label = new Label(ASCIIToUTF16("Kiwi:")); + label->set_id(kKiwiLabelID); + left_container_->AddChildView(label); + label->SetBounds(label_x, y, label_width, label_height); + + text_field = new Textfield(); + text_field->set_id(kKiwiTextfieldID); + left_container_->AddChildView(text_field); + text_field->SetBounds(label_x + label_width + 5, y, + text_field_width, label_height); + + y += label_height + gap_between_labels; + + NativeTextButton* button = new NativeTextButton(NULL, + ASCIIToUTF16("Click me")); + button->SetBounds(label_x, y + 10, 80, 30); + button->set_id(kFruitButtonID); + left_container_->AddChildView(button); + y += 40; + + cb = new Checkbox(ASCIIToUTF16("This is another check box")); + cb->SetBounds(label_x + label_width + 5, y, 180, 20); + cb->set_id(kFruitCheckBoxID); + left_container_->AddChildView(cb); + y += 20; + + Combobox* combobox = new Combobox(&combobox_model_); + combobox->SetBounds(label_x + label_width + 5, y, 150, 30); + combobox->set_id(kComboboxID); + left_container_->AddChildView(combobox); + + right_container_ = new PaneView(); + right_container_->set_border(Border::CreateSolidBorder(1, SK_ColorBLACK)); + right_container_->set_background( + Background::CreateSolidBackground(240, 240, 240)); + right_container_->set_id(kRightContainerID); + content_view_->AddChildView(right_container_); + right_container_->SetBounds(270, 35, 300, 200); + + y = 10; + int radio_button_height = 18; + int gap_between_radio_buttons = 10; + RadioButton* radio_button = new RadioButton(ASCIIToUTF16("Asparagus"), 1); + radio_button->set_id(kAsparagusButtonID); + right_container_->AddChildView(radio_button); + radio_button->SetBounds(5, y, 70, radio_button_height); + radio_button->SetGroup(1); + y += radio_button_height + gap_between_radio_buttons; + radio_button = new RadioButton(ASCIIToUTF16("Broccoli"), 1); + radio_button->set_id(kBroccoliButtonID); + right_container_->AddChildView(radio_button); + radio_button->SetBounds(5, y, 70, radio_button_height); + radio_button->SetGroup(1); + RadioButton* radio_button_to_check = radio_button; + y += radio_button_height + gap_between_radio_buttons; + radio_button = new RadioButton(ASCIIToUTF16("Cauliflower"), 1); + radio_button->set_id(kCauliflowerButtonID); + right_container_->AddChildView(radio_button); + radio_button->SetBounds(5, y, 70, radio_button_height); + radio_button->SetGroup(1); + y += radio_button_height + gap_between_radio_buttons; + + View* inner_container = new View(); + inner_container->set_border(Border::CreateSolidBorder(1, SK_ColorBLACK)); + inner_container->set_background( + Background::CreateSolidBackground(230, 230, 230)); + inner_container->set_id(kInnerContainerID); + right_container_->AddChildView(inner_container); + inner_container->SetBounds(100, 10, 150, 180); + + ScrollView* scroll_view = new ScrollView(); + scroll_view->set_id(kScrollViewID); + inner_container->AddChildView(scroll_view); + scroll_view->SetBounds(1, 1, 148, 178); + + View* scroll_content = new View(); + scroll_content->SetBounds(0, 0, 200, 200); + scroll_content->set_background( + Background::CreateSolidBackground(200, 200, 200)); + scroll_view->SetContents(scroll_content); + + static const char* const kTitles[] = { + "Rosetta", "Stupeur et tremblement", "The diner game", + "Ridicule", "Le placard", "Les Visiteurs", "Amelie", + "Joyeux Noel", "Camping", "Brice de Nice", + "Taxi", "Asterix" + }; + + static const int kIDs[] = { + kRosettaLinkID, kStupeurEtTremblementLinkID, kDinerGameLinkID, + kRidiculeLinkID, kClosetLinkID, kVisitingLinkID, kAmelieLinkID, + kJoyeuxNoelLinkID, kCampingLinkID, kBriceDeNiceLinkID, + kTaxiLinkID, kAsterixLinkID + }; + + DCHECK(arraysize(kTitles) == arraysize(kIDs)); + + y = 5; + for (size_t i = 0; i < arraysize(kTitles); ++i) { + Link* link = new Link(ASCIIToUTF16(kTitles[i])); + link->SetHorizontalAlignment(Label::ALIGN_LEFT); + link->set_id(kIDs[i]); + scroll_content->AddChildView(link); + link->SetBounds(5, y, 300, 15); + y += 15; + } + + y = 250; + int width = 60; + button = new NativeTextButton(NULL, ASCIIToUTF16("OK")); + button->set_id(kOKButtonID); + button->SetIsDefault(true); + + content_view_->AddChildView(button); + button->SetBounds(150, y, width, 30); + + button = new NativeTextButton(NULL, ASCIIToUTF16("Cancel")); + button->set_id(kCancelButtonID); + content_view_->AddChildView(button); + button->SetBounds(220, y, width, 30); + + button = new NativeTextButton(NULL, ASCIIToUTF16("Help")); + button->set_id(kHelpButtonID); + content_view_->AddChildView(button); + button->SetBounds(290, y, width, 30); + + y += 40; + + // Left bottom box with style checkboxes. + View* contents = new View(); + contents->set_background(Background::CreateSolidBackground(SK_ColorWHITE)); + cb = new Checkbox(ASCIIToUTF16("Bold")); + contents->AddChildView(cb); + cb->SetBounds(10, 10, 50, 20); + cb->set_id(kBoldCheckBoxID); + + cb = new Checkbox(ASCIIToUTF16("Italic")); + contents->AddChildView(cb); + cb->SetBounds(70, 10, 50, 20); + cb->set_id(kItalicCheckBoxID); + + cb = new Checkbox(ASCIIToUTF16("Underlined")); + contents->AddChildView(cb); + cb->SetBounds(130, 10, 70, 20); + cb->set_id(kUnderlinedCheckBoxID); + + Link* link = new Link(ASCIIToUTF16("Help")); + contents->AddChildView(link); + link->SetBounds(10, 35, 70, 10); + link->set_id(kStyleHelpLinkID); + + text_field = new Textfield(); + contents->AddChildView(text_field); + text_field->SetBounds(10, 50, 100, 20); + text_field->set_id(kStyleTextEditID); + + style_tab_ = new TabbedPane(); + style_tab_->set_id(kStyleContainerID); + content_view_->AddChildView(style_tab_); + style_tab_->SetBounds(10, y, 210, 100); + style_tab_->AddTab(ASCIIToUTF16("Style"), contents); + style_tab_->AddTab(ASCIIToUTF16("Other"), new View()); + + // Right bottom box with search. + contents = new View(); + contents->set_background(Background::CreateSolidBackground(SK_ColorWHITE)); + text_field = new Textfield(); + contents->AddChildView(text_field); + text_field->SetBounds(10, 10, 100, 20); + text_field->set_id(kSearchTextfieldID); + + button = new NativeTextButton(NULL, ASCIIToUTF16("Search")); + contents->AddChildView(button); + button->SetBounds(112, 5, 60, 30); + button->set_id(kSearchButtonID); + + link = new Link(ASCIIToUTF16("Help")); + link->SetHorizontalAlignment(Label::ALIGN_LEFT); + link->set_id(kHelpLinkID); + contents->AddChildView(link); + link->SetBounds(175, 10, 30, 20); + + search_border_view_ = new BorderView(contents); + search_border_view_->set_id(kSearchContainerID); + + content_view_->AddChildView(search_border_view_); + search_border_view_->SetBounds(300, y, 240, 50); + + y += 60; + + contents = new View(); + contents->set_focusable(true); + contents->set_background(Background::CreateSolidBackground(SK_ColorBLUE)); + contents->set_id(kThumbnailContainerID); + button = new NativeTextButton(NULL, ASCIIToUTF16("Star")); + contents->AddChildView(button); + button->SetBounds(5, 5, 50, 30); + button->set_id(kThumbnailStarID); + button = new NativeTextButton(NULL, ASCIIToUTF16("SuperStar")); + contents->AddChildView(button); + button->SetBounds(60, 5, 100, 30); + button->set_id(kThumbnailSuperStarID); + + content_view_->AddChildView(contents); + contents->SetBounds(250, y, 200, 50); + // We can only call RadioButton::SetChecked() on the radio-button is part of + // the view hierarchy. + radio_button_to_check->SetChecked(true); +} + +//////////////////////////////////////////////////////////////////////////////// +// The tests +//////////////////////////////////////////////////////////////////////////////// + +enum FocusTestEventType { + ON_FOCUS = 0, + ON_BLUR +}; + +struct FocusTestEvent { + FocusTestEvent(FocusTestEventType type, int view_id) + : type(type), + view_id(view_id) { + } + + FocusTestEventType type; + int view_id; +}; + +class SimpleTestView : public View { + public: + SimpleTestView(std::vector<FocusTestEvent>* event_list, int view_id) + : event_list_(event_list) { + set_focusable(true); + set_id(view_id); + } + + virtual void OnFocus() { + event_list_->push_back(FocusTestEvent(ON_FOCUS, id())); + } + + virtual void OnBlur() { + event_list_->push_back(FocusTestEvent(ON_BLUR, id())); + } + + private: + std::vector<FocusTestEvent>* event_list_; +}; + +// Tests that the appropriate Focus related methods are called when a View +// gets/loses focus. +TEST_F(FocusManagerTest, ViewFocusCallbacks) { + std::vector<FocusTestEvent> event_list; + const int kView1ID = 1; + const int kView2ID = 2; + + SimpleTestView* view1 = new SimpleTestView(&event_list, kView1ID); + SimpleTestView* view2 = new SimpleTestView(&event_list, kView2ID); + content_view_->AddChildView(view1); + content_view_->AddChildView(view2); + + view1->RequestFocus(); + ASSERT_EQ(1, static_cast<int>(event_list.size())); + EXPECT_EQ(ON_FOCUS, event_list[0].type); + EXPECT_EQ(kView1ID, event_list[0].view_id); + + event_list.clear(); + view2->RequestFocus(); + ASSERT_EQ(2, static_cast<int>(event_list.size())); + EXPECT_EQ(ON_BLUR, event_list[0].type); + EXPECT_EQ(kView1ID, event_list[0].view_id); + EXPECT_EQ(ON_FOCUS, event_list[1].type); + EXPECT_EQ(kView2ID, event_list[1].view_id); + + event_list.clear(); + GetFocusManager()->ClearFocus(); + ASSERT_EQ(1, static_cast<int>(event_list.size())); + EXPECT_EQ(ON_BLUR, event_list[0].type); + EXPECT_EQ(kView2ID, event_list[0].view_id); +} + +typedef std::pair<View*, View*> ViewPair; +class TestFocusChangeListener : public FocusChangeListener { + public: + virtual void OnWillChangeFocus(View* focused_before, View* focused_now) { + focus_changes_.push_back(ViewPair(focused_before, focused_now)); + } + virtual void OnDidChangeFocus(View* focused_before, View* focused_now) { + } + + const std::vector<ViewPair>& focus_changes() const { return focus_changes_; } + void ClearFocusChanges() { focus_changes_.clear(); } + + private: + // A vector of which views lost/gained focus. + std::vector<ViewPair> focus_changes_; +}; + +TEST_F(FocusManagerTest, FocusChangeListener) { + View* view1 = new View(); + view1->set_focusable(true); + View* view2 = new View(); + view2->set_focusable(true); + content_view_->AddChildView(view1); + content_view_->AddChildView(view2); + + TestFocusChangeListener listener; + AddFocusChangeListener(&listener); + + // Visual Studio 2010 has problems converting NULL to the null pointer for + // std::pair. See http://connect.microsoft.com/VisualStudio/feedback/details/520043/error-converting-from-null-to-a-pointer-type-in-std-pair + // It will work if we pass nullptr. +#if defined(_MSC_VER) && _MSC_VER >= 1600 + views::View* null_view = nullptr; +#else + views::View* null_view = NULL; +#endif + + view1->RequestFocus(); + ASSERT_EQ(1, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(null_view, view1)); + listener.ClearFocusChanges(); + + view2->RequestFocus(); + ASSERT_EQ(1, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(view1, view2)); + listener.ClearFocusChanges(); + + GetFocusManager()->ClearFocus(); + ASSERT_EQ(1, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(view2, null_view)); +} + +class TestNativeButton : public NativeTextButton { + public: + explicit TestNativeButton(const string16& text) + : NativeTextButton(NULL, text) { + }; + virtual gfx::NativeView TestGetNativeControlView() { + return GetWidget()->GetNativeView(); + } +}; + +class TestCheckbox : public Checkbox { + public: + explicit TestCheckbox(const string16& text) : Checkbox(text) { + }; + virtual gfx::NativeView TestGetNativeControlView() { + return GetWidget()->GetNativeView(); + } +}; + +class TestRadioButton : public RadioButton { + public: + explicit TestRadioButton(const string16& text) + : RadioButton(text, 1) { + } + virtual gfx::NativeView TestGetNativeControlView() { + return GetWidget()->GetNativeView(); + } +}; + +class TestTextfield : public Textfield { + public: + TestTextfield() { } + virtual gfx::NativeView TestGetNativeControlView() { + return native_wrapper_->GetTestingHandle(); + } +}; + +class TestCombobox : public Combobox, public ui::ComboboxModel { + public: + TestCombobox() : Combobox(this) { } + virtual gfx::NativeView TestGetNativeControlView() { + return native_wrapper_->GetTestingHandle(); + } + virtual int GetItemCount() { + return 10; + } + virtual string16 GetItemAt(int index) { + return ASCIIToUTF16("Hello combo"); + } +}; + +class TestTabbedPane : public TabbedPane { + public: + TestTabbedPane() { } + virtual gfx::NativeView TestGetNativeControlView() { + return native_tabbed_pane_->GetTestingHandle(); + } +}; + +#if !defined(TOUCH_UI) +// TODO(oshima): replace TOUCH_UI with PURE_VIEWS + +// Tests that NativeControls do set the focus View appropriately on the +// FocusManager. +TEST_F(FocusManagerTest, FAILS_FocusNativeControls) { + TestTextfield* textfield = new TestTextfield(); + TestTabbedPane* tabbed_pane = new TestTabbedPane(); + TestTextfield* textfield2 = new TestTextfield(); + + content_view_->AddChildView(textfield); + content_view_->AddChildView(tabbed_pane); + + tabbed_pane->AddTab(ASCIIToUTF16("Awesome textfield"), textfield2); + + // Simulate the native view getting the native focus (such as by user click). + FocusNativeView(textfield->TestGetNativeControlView()); + EXPECT_EQ(textfield, GetFocusManager()->GetFocusedView()); + + FocusNativeView(tabbed_pane->TestGetNativeControlView()); + EXPECT_EQ(tabbed_pane, GetFocusManager()->GetFocusedView()); + + FocusNativeView(textfield2->TestGetNativeControlView()); + EXPECT_EQ(textfield2, GetFocusManager()->GetFocusedView()); +} +#endif + +// On linux, we don't store/restore focused view because gtk handles +// this (and pure views will be the same). +#if defined(OS_WIN) + +// Test that when activating/deactivating the top window, the focus is stored/ +// restored properly. +TEST_F(FocusManagerTest, FocusStoreRestore) { + // Simulate an activate, otherwise the deactivate isn't going to do anything. + SimulateActivateWindow(); + + NativeTextButton* button = new NativeTextButton(NULL, + ASCIIToUTF16("Press me")); + View* view = new View(); + view->set_focusable(true); + + content_view_->AddChildView(button); + button->SetBounds(10, 10, 200, 30); + content_view_->AddChildView(view); + RunPendingMessages(); + + TestFocusChangeListener listener; + AddFocusChangeListener(&listener); + + view->RequestFocus(); + RunPendingMessages(); + // MessageLoopForUI::current()->RunWithDispatcher(new AcceleratorHandler()); + + // Visual Studio 2010 has problems converting NULL to the null pointer for + // std::pair. See http://connect.microsoft.com/VisualStudio/feedback/details/520043/error-converting-from-null-to-a-pointer-type-in-std-pair + // It will work if we pass nullptr. +#if defined(_MSC_VER) && _MSC_VER >= 1600 + views::View* null_view = nullptr; +#else + views::View* null_view = NULL; +#endif + + // Deacivate the window, it should store its focus. + SimulateDeactivateWindow(); + EXPECT_EQ(NULL, GetFocusManager()->GetFocusedView()); + ASSERT_EQ(2, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(null_view, view)); + EXPECT_TRUE(listener.focus_changes()[1] == ViewPair(view, null_view)); + listener.ClearFocusChanges(); + + // Reactivate, focus should come-back to the previously focused view. + SimulateActivateWindow(); + EXPECT_EQ(view, GetFocusManager()->GetFocusedView()); + ASSERT_EQ(1, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(null_view, view)); + listener.ClearFocusChanges(); + + // Same test with a NativeControl. + button->RequestFocus(); + SimulateDeactivateWindow(); + EXPECT_EQ(NULL, GetFocusManager()->GetFocusedView()); + ASSERT_EQ(2, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(view, button)); + EXPECT_TRUE(listener.focus_changes()[1] == ViewPair(button, null_view)); + listener.ClearFocusChanges(); + + SimulateActivateWindow(); + EXPECT_EQ(button, GetFocusManager()->GetFocusedView()); + ASSERT_EQ(1, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(null_view, button)); + listener.ClearFocusChanges(); + + /* + // Now test that while the window is inactive we can change the focused view + // (we do that in several places). + SimulateDeactivateWindow(); + // TODO: would have to mock the window being inactive (with a TestWidgetWin + // that would return false on IsActive()). + GetFocusManager()->SetFocusedView(view); + ::SendMessage(window_->GetNativeWindow(), WM_ACTIVATE, WA_ACTIVE, NULL); + + EXPECT_EQ(view, GetFocusManager()->GetFocusedView()); + ASSERT_EQ(2, static_cast<int>(listener.focus_changes().size())); + EXPECT_TRUE(listener.focus_changes()[0] == ViewPair(button, null_view)); + EXPECT_TRUE(listener.focus_changes()[1] == ViewPair(null_view, view)); + */ +} +#endif + +#if !defined(TOUCH_UI) +// TODO(oshima): There is no tabbed pane in pure views. Replace it +// with different implementation. + +TEST_F(FocusManagerTest, ContainsView) { + View* view = new View(); + scoped_ptr<View> detached_view(new View()); + TabbedPane* tabbed_pane = new TabbedPane(); + TabbedPane* nested_tabbed_pane = new TabbedPane(); + NativeTextButton* tab_button = new NativeTextButton( + NULL, ASCIIToUTF16("tab button")); + + content_view_->AddChildView(view); + content_view_->AddChildView(tabbed_pane); + // Adding a View inside a TabbedPane to test the case of nested root view. + + tabbed_pane->AddTab(ASCIIToUTF16("Awesome tab"), nested_tabbed_pane); + nested_tabbed_pane->AddTab(ASCIIToUTF16("Awesomer tab"), tab_button); + + EXPECT_TRUE(GetFocusManager()->ContainsView(view)); + EXPECT_TRUE(GetFocusManager()->ContainsView(tabbed_pane)); + EXPECT_TRUE(GetFocusManager()->ContainsView(nested_tabbed_pane)); + EXPECT_TRUE(GetFocusManager()->ContainsView(tab_button)); + EXPECT_FALSE(GetFocusManager()->ContainsView(detached_view.get())); +} +#endif + +TEST_F(FocusTraversalTest, NormalTraversal) { + const int kTraversalIDs[] = { kTopCheckBoxID, kAppleTextfieldID, + kOrangeTextfieldID, kBananaTextfieldID, kKiwiTextfieldID, + kFruitButtonID, kFruitCheckBoxID, kComboboxID, kBroccoliButtonID, + kRosettaLinkID, kStupeurEtTremblementLinkID, + kDinerGameLinkID, kRidiculeLinkID, kClosetLinkID, kVisitingLinkID, + kAmelieLinkID, kJoyeuxNoelLinkID, kCampingLinkID, kBriceDeNiceLinkID, + kTaxiLinkID, kAsterixLinkID, kOKButtonID, kCancelButtonID, kHelpButtonID, + kStyleContainerID, kBoldCheckBoxID, kItalicCheckBoxID, + kUnderlinedCheckBoxID, kStyleHelpLinkID, kStyleTextEditID, + kSearchTextfieldID, kSearchButtonID, kHelpLinkID, + kThumbnailContainerID, kThumbnailStarID, kThumbnailSuperStarID }; + + // Uncomment the following line if you want to test manually the UI of this + // test. + // MessageLoopForUI::current()->RunWithDispatcher(new AcceleratorHandler()); + + // Let's traverse the whole focus hierarchy (several times, to make sure it + // loops OK). + GetFocusManager()->ClearFocus(); + for (int i = 0; i < 3; ++i) { + for (size_t j = 0; j < arraysize(kTraversalIDs); j++) { + GetFocusManager()->AdvanceFocus(false); + View* focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->id()); + } + } + + // Let's traverse in reverse order. + GetFocusManager()->ClearFocus(); + for (int i = 0; i < 3; ++i) { + for (int j = arraysize(kTraversalIDs) - 1; j >= 0; --j) { + GetFocusManager()->AdvanceFocus(true); + View* focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->id()); + } + } +} + +TEST_F(FocusTraversalTest, TraversalWithNonEnabledViews) { + const int kDisabledIDs[] = { + kBananaTextfieldID, kFruitCheckBoxID, kComboboxID, kAsparagusButtonID, + kCauliflowerButtonID, kClosetLinkID, kVisitingLinkID, kBriceDeNiceLinkID, + kTaxiLinkID, kAsterixLinkID, kHelpButtonID, kBoldCheckBoxID, + kSearchTextfieldID, kHelpLinkID }; + + const int kTraversalIDs[] = { kTopCheckBoxID, kAppleTextfieldID, + kOrangeTextfieldID, kKiwiTextfieldID, kFruitButtonID, kBroccoliButtonID, + kRosettaLinkID, kStupeurEtTremblementLinkID, kDinerGameLinkID, + kRidiculeLinkID, kAmelieLinkID, kJoyeuxNoelLinkID, kCampingLinkID, + kOKButtonID, kCancelButtonID, kStyleContainerID, kItalicCheckBoxID, + kUnderlinedCheckBoxID, kStyleHelpLinkID, kStyleTextEditID, + kSearchButtonID, kThumbnailContainerID, kThumbnailStarID, + kThumbnailSuperStarID }; + + // Let's disable some views. + for (size_t i = 0; i < arraysize(kDisabledIDs); i++) { + View* v = FindViewByID(kDisabledIDs[i]); + ASSERT_TRUE(v != NULL); + v->SetEnabled(false); + } + + // Uncomment the following line if you want to test manually the UI of this + // test. + // MessageLoopForUI::current()->RunWithDispatcher(new AcceleratorHandler()); + + View* focused_view; + // Let's do one traversal (several times, to make sure it loops ok). + GetFocusManager()->ClearFocus(); + for (int i = 0; i < 3; ++i) { + for (size_t j = 0; j < arraysize(kTraversalIDs); j++) { + GetFocusManager()->AdvanceFocus(false); + focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->id()); + } + } + + // Same thing in reverse. + GetFocusManager()->ClearFocus(); + for (int i = 0; i < 3; ++i) { + for (int j = arraysize(kTraversalIDs) - 1; j >= 0; --j) { + GetFocusManager()->AdvanceFocus(true); + focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->id()); + } + } +} + +TEST_F(FocusTraversalTest, TraversalWithInvisibleViews) { + const int kInvisibleIDs[] = { kTopCheckBoxID, kOKButtonID, + kThumbnailContainerID }; + + const int kTraversalIDs[] = { kAppleTextfieldID, kOrangeTextfieldID, + kBananaTextfieldID, kKiwiTextfieldID, kFruitButtonID, kFruitCheckBoxID, + kComboboxID, kBroccoliButtonID, kRosettaLinkID, + kStupeurEtTremblementLinkID, kDinerGameLinkID, kRidiculeLinkID, + kClosetLinkID, kVisitingLinkID, kAmelieLinkID, kJoyeuxNoelLinkID, + kCampingLinkID, kBriceDeNiceLinkID, kTaxiLinkID, kAsterixLinkID, + kCancelButtonID, kHelpButtonID, kStyleContainerID, kBoldCheckBoxID, + kItalicCheckBoxID, kUnderlinedCheckBoxID, kStyleHelpLinkID, + kStyleTextEditID, kSearchTextfieldID, kSearchButtonID, kHelpLinkID }; + + + // Let's make some views invisible. + for (size_t i = 0; i < arraysize(kInvisibleIDs); i++) { + View* v = FindViewByID(kInvisibleIDs[i]); + ASSERT_TRUE(v != NULL); + v->SetVisible(false); + } + + // Uncomment the following line if you want to test manually the UI of this + // test. + // MessageLoopForUI::current()->RunWithDispatcher(new AcceleratorHandler()); + + View* focused_view; + // Let's do one traversal (several times, to make sure it loops ok). + GetFocusManager()->ClearFocus(); + for (int i = 0; i < 3; ++i) { + for (size_t j = 0; j < arraysize(kTraversalIDs); j++) { + GetFocusManager()->AdvanceFocus(false); + focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->id()); + } + } + + // Same thing in reverse. + GetFocusManager()->ClearFocus(); + for (int i = 0; i < 3; ++i) { + for (int j = arraysize(kTraversalIDs) - 1; j >= 0; --j) { + GetFocusManager()->AdvanceFocus(true); + focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kTraversalIDs[j], focused_view->id()); + } + } +} + +TEST_F(FocusTraversalTest, PaneTraversal) { + // Tests trapping the traversal within a pane - useful for full + // keyboard accessibility for toolbars. + + // First test the left container. + const int kLeftTraversalIDs[] = { + kAppleTextfieldID, + kOrangeTextfieldID, kBananaTextfieldID, kKiwiTextfieldID, + kFruitButtonID, kFruitCheckBoxID, kComboboxID }; + + FocusSearch focus_search_left(left_container_, true, false); + left_container_->EnablePaneFocus(&focus_search_left); + FindViewByID(kComboboxID)->RequestFocus(); + + // Traverse the focus hierarchy within the pane several times. + for (int i = 0; i < 3; ++i) { + for (size_t j = 0; j < arraysize(kLeftTraversalIDs); j++) { + GetFocusManager()->AdvanceFocus(false); + View* focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kLeftTraversalIDs[j], focused_view->id()); + } + } + + // Traverse in reverse order. + FindViewByID(kAppleTextfieldID)->RequestFocus(); + for (int i = 0; i < 3; ++i) { + for (int j = arraysize(kLeftTraversalIDs) - 1; j >= 0; --j) { + GetFocusManager()->AdvanceFocus(true); + View* focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kLeftTraversalIDs[j], focused_view->id()); + } + } + + // Now test the right container, but this time with accessibility mode. + // Make some links not focusable, but mark one of them as + // "accessibility focusable", so it should show up in the traversal. + const int kRightTraversalIDs[] = { + kBroccoliButtonID, kDinerGameLinkID, kRidiculeLinkID, + kClosetLinkID, kVisitingLinkID, kAmelieLinkID, kJoyeuxNoelLinkID, + kCampingLinkID, kBriceDeNiceLinkID, kTaxiLinkID, kAsterixLinkID }; + + FocusSearch focus_search_right(right_container_, true, true); + right_container_->EnablePaneFocus(&focus_search_right); + FindViewByID(kRosettaLinkID)->set_focusable(false); + FindViewByID(kStupeurEtTremblementLinkID)->set_focusable(false); + FindViewByID(kDinerGameLinkID)->set_accessibility_focusable(true); + FindViewByID(kDinerGameLinkID)->set_focusable(false); + FindViewByID(kAsterixLinkID)->RequestFocus(); + + // Traverse the focus hierarchy within the pane several times. + for (int i = 0; i < 3; ++i) { + for (size_t j = 0; j < arraysize(kRightTraversalIDs); j++) { + GetFocusManager()->AdvanceFocus(false); + View* focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kRightTraversalIDs[j], focused_view->id()); + } + } + + // Traverse in reverse order. + FindViewByID(kBroccoliButtonID)->RequestFocus(); + for (int i = 0; i < 3; ++i) { + for (int j = arraysize(kRightTraversalIDs) - 1; j >= 0; --j) { + GetFocusManager()->AdvanceFocus(true); + View* focused_view = GetFocusManager()->GetFocusedView(); + EXPECT_TRUE(focused_view != NULL); + if (focused_view) + EXPECT_EQ(kRightTraversalIDs[j], focused_view->id()); + } + } +} + +// Counts accelerator calls. +class TestAcceleratorTarget : public ui::AcceleratorTarget { + public: + explicit TestAcceleratorTarget(bool process_accelerator) + : accelerator_count_(0), process_accelerator_(process_accelerator) {} + + virtual bool AcceleratorPressed(const ui::Accelerator& accelerator) { + ++accelerator_count_; + return process_accelerator_; + } + + int accelerator_count() const { return accelerator_count_; } + + private: + int accelerator_count_; // number of times that the accelerator is activated + bool process_accelerator_; // return value of AcceleratorPressed + + DISALLOW_COPY_AND_ASSIGN(TestAcceleratorTarget); +}; + +TEST_F(FocusManagerTest, CallsNormalAcceleratorTarget) { + FocusManager* focus_manager = GetFocusManager(); + ui::Accelerator return_accelerator(ui::VKEY_RETURN, false, false, false); + ui::Accelerator escape_accelerator(ui::VKEY_ESCAPE, false, false, false); + + TestAcceleratorTarget return_target(true); + TestAcceleratorTarget escape_target(true); + EXPECT_EQ(return_target.accelerator_count(), 0); + EXPECT_EQ(escape_target.accelerator_count(), 0); + EXPECT_EQ(NULL, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + EXPECT_EQ(NULL, + focus_manager->GetCurrentTargetForAccelerator(escape_accelerator)); + + // Register targets. + focus_manager->RegisterAccelerator(return_accelerator, &return_target); + focus_manager->RegisterAccelerator(escape_accelerator, &escape_target); + + // Checks if the correct target is registered. + EXPECT_EQ(&return_target, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + EXPECT_EQ(&escape_target, + focus_manager->GetCurrentTargetForAccelerator(escape_accelerator)); + + // Hitting the return key. + EXPECT_TRUE(focus_manager->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(return_target.accelerator_count(), 1); + EXPECT_EQ(escape_target.accelerator_count(), 0); + + // Hitting the escape key. + EXPECT_TRUE(focus_manager->ProcessAccelerator(escape_accelerator)); + EXPECT_EQ(return_target.accelerator_count(), 1); + EXPECT_EQ(escape_target.accelerator_count(), 1); + + // Register another target for the return key. + TestAcceleratorTarget return_target2(true); + EXPECT_EQ(return_target2.accelerator_count(), 0); + focus_manager->RegisterAccelerator(return_accelerator, &return_target2); + EXPECT_EQ(&return_target2, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + + // Hitting the return key; return_target2 has the priority. + EXPECT_TRUE(focus_manager->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(return_target.accelerator_count(), 1); + EXPECT_EQ(return_target2.accelerator_count(), 1); + + // Register a target that does not process the accelerator event. + TestAcceleratorTarget return_target3(false); + EXPECT_EQ(return_target3.accelerator_count(), 0); + focus_manager->RegisterAccelerator(return_accelerator, &return_target3); + EXPECT_EQ(&return_target3, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + + // Hitting the return key. + // Since the event handler of return_target3 returns false, return_target2 + // should be called too. + EXPECT_TRUE(focus_manager->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(return_target.accelerator_count(), 1); + EXPECT_EQ(return_target2.accelerator_count(), 2); + EXPECT_EQ(return_target3.accelerator_count(), 1); + + // Unregister return_target2. + focus_manager->UnregisterAccelerator(return_accelerator, &return_target2); + EXPECT_EQ(&return_target3, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + + // Hitting the return key. return_target3 and return_target should be called. + EXPECT_TRUE(focus_manager->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(return_target.accelerator_count(), 2); + EXPECT_EQ(return_target2.accelerator_count(), 2); + EXPECT_EQ(return_target3.accelerator_count(), 2); + + // Unregister targets. + focus_manager->UnregisterAccelerator(return_accelerator, &return_target); + focus_manager->UnregisterAccelerator(return_accelerator, &return_target3); + focus_manager->UnregisterAccelerator(escape_accelerator, &escape_target); + + // Now there is no target registered. + EXPECT_EQ(NULL, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + EXPECT_EQ(NULL, + focus_manager->GetCurrentTargetForAccelerator(escape_accelerator)); + + // Hitting the return key and the escape key. Nothing should happen. + EXPECT_FALSE(focus_manager->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(return_target.accelerator_count(), 2); + EXPECT_EQ(return_target2.accelerator_count(), 2); + EXPECT_EQ(return_target3.accelerator_count(), 2); + EXPECT_FALSE(focus_manager->ProcessAccelerator(escape_accelerator)); + EXPECT_EQ(escape_target.accelerator_count(), 1); +} + +// Unregisters itself when its accelerator is invoked. +class SelfUnregisteringAcceleratorTarget : public ui::AcceleratorTarget { + public: + SelfUnregisteringAcceleratorTarget(ui::Accelerator accelerator, + FocusManager* focus_manager) + : accelerator_(accelerator), + focus_manager_(focus_manager), + accelerator_count_(0) { + } + + virtual bool AcceleratorPressed(const ui::Accelerator& accelerator) { + ++accelerator_count_; + focus_manager_->UnregisterAccelerator(accelerator, this); + return true; + } + + int accelerator_count() const { return accelerator_count_; } + + private: + ui::Accelerator accelerator_; + FocusManager* focus_manager_; + int accelerator_count_; + + DISALLOW_COPY_AND_ASSIGN(SelfUnregisteringAcceleratorTarget); +}; + +TEST_F(FocusManagerTest, CallsSelfDeletingAcceleratorTarget) { + FocusManager* focus_manager = GetFocusManager(); + ui::Accelerator return_accelerator(ui::VKEY_RETURN, false, false, false); + SelfUnregisteringAcceleratorTarget target(return_accelerator, focus_manager); + EXPECT_EQ(target.accelerator_count(), 0); + EXPECT_EQ(NULL, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + + // Register the target. + focus_manager->RegisterAccelerator(return_accelerator, &target); + EXPECT_EQ(&target, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + + // Hitting the return key. The target will be unregistered. + EXPECT_TRUE(focus_manager->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(target.accelerator_count(), 1); + EXPECT_EQ(NULL, + focus_manager->GetCurrentTargetForAccelerator(return_accelerator)); + + // Hitting the return key again; nothing should happen. + EXPECT_FALSE(focus_manager->ProcessAccelerator(return_accelerator)); + EXPECT_EQ(target.accelerator_count(), 1); +} + +class MessageTrackingView : public View { + public: + MessageTrackingView() : accelerator_pressed_(false) { + } + + virtual bool OnKeyPressed(const KeyEvent& e) { + keys_pressed_.push_back(e.key_code()); + return true; + } + + virtual bool OnKeyReleased(const KeyEvent& e) { + keys_released_.push_back(e.key_code()); + return true; + } + + virtual bool AcceleratorPressed(const ui::Accelerator& accelerator) { + accelerator_pressed_ = true; + return true; + } + + void Reset() { + accelerator_pressed_ = false; + keys_pressed_.clear(); + keys_released_.clear(); + } + + const std::vector<ui::KeyboardCode>& keys_pressed() const { + return keys_pressed_; + } + + const std::vector<ui::KeyboardCode>& keys_released() const { + return keys_released_; + } + + bool accelerator_pressed() const { + return accelerator_pressed_; + } + + private: + bool accelerator_pressed_; + std::vector<ui::KeyboardCode> keys_pressed_; + std::vector<ui::KeyboardCode> keys_released_; + + DISALLOW_COPY_AND_ASSIGN(MessageTrackingView); +}; + +#if defined(OS_WIN) +// This test is now Windows only. Linux Views port does not handle accelerator +// keys in AcceleratorHandler anymore. The logic has been moved into +// NativeWidgetGtk::OnKeyEvent(). +// Tests that the keyup messages are eaten for accelerators. +TEST_F(FocusManagerTest, IgnoreKeyupForAccelerators) { + FocusManager* focus_manager = GetFocusManager(); + MessageTrackingView* mtv = new MessageTrackingView(); + mtv->AddAccelerator(ui::Accelerator(ui::VKEY_0, false, false, false)); + mtv->AddAccelerator(ui::Accelerator(ui::VKEY_1, false, false, false)); + content_view_->AddChildView(mtv); + focus_manager->SetFocusedView(mtv); + + // First send a non-accelerator key sequence. + PostKeyDown(ui::VKEY_9); + PostKeyUp(ui::VKEY_9); + AcceleratorHandler accelerator_handler; + MessageLoopForUI::current()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); + MessageLoopForUI::current()->RunWithDispatcher(&accelerator_handler); + // Make sure we get a key-up and key-down. + ASSERT_EQ(1U, mtv->keys_pressed().size()); + EXPECT_EQ(ui::VKEY_9, mtv->keys_pressed()[0]); + ASSERT_EQ(1U, mtv->keys_released().size()); + EXPECT_EQ(ui::VKEY_9, mtv->keys_released()[0]); + EXPECT_FALSE(mtv->accelerator_pressed()); + mtv->Reset(); + + // Same thing with repeat and more than one key at once. + PostKeyDown(ui::VKEY_9); + PostKeyDown(ui::VKEY_9); + PostKeyDown(ui::VKEY_8); + PostKeyDown(ui::VKEY_9); + PostKeyDown(ui::VKEY_7); + PostKeyUp(ui::VKEY_9); + PostKeyUp(ui::VKEY_7); + PostKeyUp(ui::VKEY_8); + MessageLoopForUI::current()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); + MessageLoopForUI::current()->RunWithDispatcher(&accelerator_handler); + // Make sure we get a key-up and key-down. + ASSERT_EQ(5U, mtv->keys_pressed().size()); + EXPECT_EQ(ui::VKEY_9, mtv->keys_pressed()[0]); + EXPECT_EQ(ui::VKEY_9, mtv->keys_pressed()[1]); + EXPECT_EQ(ui::VKEY_8, mtv->keys_pressed()[2]); + EXPECT_EQ(ui::VKEY_9, mtv->keys_pressed()[3]); + EXPECT_EQ(ui::VKEY_7, mtv->keys_pressed()[4]); + ASSERT_EQ(3U, mtv->keys_released().size()); + EXPECT_EQ(ui::VKEY_9, mtv->keys_released()[0]); + EXPECT_EQ(ui::VKEY_7, mtv->keys_released()[1]); + EXPECT_EQ(ui::VKEY_8, mtv->keys_released()[2]); + EXPECT_FALSE(mtv->accelerator_pressed()); + mtv->Reset(); + + // Now send an accelerator key sequence. + PostKeyDown(ui::VKEY_0); + PostKeyUp(ui::VKEY_0); + MessageLoopForUI::current()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); + MessageLoopForUI::current()->RunWithDispatcher(&accelerator_handler); + EXPECT_TRUE(mtv->keys_pressed().empty()); + EXPECT_TRUE(mtv->keys_released().empty()); + EXPECT_TRUE(mtv->accelerator_pressed()); + mtv->Reset(); + + // Same thing with repeat and more than one key at once. + PostKeyDown(ui::VKEY_0); + PostKeyDown(ui::VKEY_1); + PostKeyDown(ui::VKEY_1); + PostKeyDown(ui::VKEY_0); + PostKeyDown(ui::VKEY_0); + PostKeyUp(ui::VKEY_1); + PostKeyUp(ui::VKEY_0); + MessageLoopForUI::current()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); + MessageLoopForUI::current()->RunWithDispatcher(&accelerator_handler); + EXPECT_TRUE(mtv->keys_pressed().empty()); + EXPECT_TRUE(mtv->keys_released().empty()); + EXPECT_TRUE(mtv->accelerator_pressed()); + mtv->Reset(); +} +#endif + +#if defined(OS_WIN) && !defined(USE_AURA) +// Test that the focus manager is created successfully for the first view +// window parented to a native dialog. +TEST_F(FocusManagerTest, CreationForNativeRoot) { + // Create a window class. + WNDCLASSEX class_ex; + memset(&class_ex, 0, sizeof(class_ex)); + class_ex.cbSize = sizeof(WNDCLASSEX); + class_ex.lpfnWndProc = &DefWindowProc; + class_ex.lpszClassName = L"TestWindow"; + ATOM atom = RegisterClassEx(&class_ex); + ASSERT_TRUE(atom); + + // Create a native dialog window. + HWND hwnd = CreateWindowEx(0, class_ex.lpszClassName, NULL, + WS_OVERLAPPEDWINDOW, 0, 0, 200, 200, + NULL, NULL, NULL, NULL); + ASSERT_TRUE(hwnd); + + // Create a view window parented to native dialog. + scoped_ptr<Widget> widget1(new Widget); + Widget::InitParams params(Widget::InitParams::TYPE_CONTROL); + params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; + params.parent = hwnd; + params.bounds = gfx::Rect(0, 0, 100, 100); + params.top_level = true; // This is top level in views hierarchy. + widget1->Init(params); + + // Get the focus manager directly from the first window. Should exist + // because the first window is the root widget. + views::FocusManager* focus_manager1 = widget1->GetFocusManager(); + EXPECT_TRUE(focus_manager1); + + // Create another view window parented to the first view window. + scoped_ptr<Widget> widget2(new Widget); + params.parent = widget1->GetNativeView(); + params.top_level = false; // This is child widget. + widget2->Init(params); + + // Access the shared focus manager directly from the second window. + views::FocusManager* focus_manager2 = widget2->GetFocusManager(); + EXPECT_EQ(focus_manager2, focus_manager1); + + // Access the shared focus manager indirectly from the first window handle. + gfx::NativeWindow native_window = widget1->GetNativeWindow(); + views::Widget* widget = + views::Widget::GetWidgetForNativeWindow(native_window); + EXPECT_EQ(widget->GetFocusManager(), focus_manager1); + + // Access the shared focus manager indirectly from the second window handle. + native_window = widget2->GetNativeWindow(); + widget = views::Widget::GetWidgetForNativeWindow(native_window); + EXPECT_EQ(widget->GetFocusManager(), focus_manager1); + + // Access the shared focus manager indirectly from the first view handle. + gfx::NativeView native_view = widget1->GetNativeView(); + widget = views::Widget::GetTopLevelWidgetForNativeView(native_view); + EXPECT_EQ(widget->GetFocusManager(), focus_manager1); + + // Access the shared focus manager indirectly from the second view handle. + native_view = widget2->GetNativeView(); + widget = views::Widget::GetTopLevelWidgetForNativeView(native_view); + EXPECT_EQ(widget->GetFocusManager(), focus_manager1); + + DestroyWindow(hwnd); +} +#endif + +class FocusManagerDtorTest : public FocusManagerTest { + protected: + typedef std::vector<std::string> DtorTrackVector; + + class FocusManagerDtorTracked : public FocusManager { + public: + FocusManagerDtorTracked(Widget* widget, DtorTrackVector* dtor_tracker) + : FocusManager(widget), + dtor_tracker_(dtor_tracker) { + } + + virtual ~FocusManagerDtorTracked() { + dtor_tracker_->push_back("FocusManagerDtorTracked"); + } + + DtorTrackVector* dtor_tracker_; + + private: + DISALLOW_COPY_AND_ASSIGN(FocusManagerDtorTracked); + }; + + class TestFocusManagerFactory : public FocusManagerFactory { + public: + explicit TestFocusManagerFactory(DtorTrackVector* dtor_tracker) + : dtor_tracker_(dtor_tracker) { + } + + FocusManager* CreateFocusManager(Widget* widget) OVERRIDE { + return new FocusManagerDtorTracked(widget, dtor_tracker_); + } + + private: + DtorTrackVector* dtor_tracker_; + DISALLOW_COPY_AND_ASSIGN(TestFocusManagerFactory); + }; + + class NativeButtonDtorTracked : public NativeTextButton { + public: + NativeButtonDtorTracked(const string16& text, + DtorTrackVector* dtor_tracker) + : NativeTextButton(NULL, text), + dtor_tracker_(dtor_tracker) { + }; + virtual ~NativeButtonDtorTracked() { + dtor_tracker_->push_back("NativeButtonDtorTracked"); + } + + DtorTrackVector* dtor_tracker_; + }; + + class WindowDtorTracked : public Widget { + public: + explicit WindowDtorTracked(DtorTrackVector* dtor_tracker) + : dtor_tracker_(dtor_tracker) { + } + + virtual ~WindowDtorTracked() { + dtor_tracker_->push_back("WindowDtorTracked"); + } + + DtorTrackVector* dtor_tracker_; + }; + + virtual void SetUp() { + ViewsTestBase::SetUp(); + FocusManagerFactory::Install(new TestFocusManagerFactory(&dtor_tracker_)); + // Create WindowDtorTracked that uses FocusManagerDtorTracked. + window_ = new WindowDtorTracked(&dtor_tracker_); + Widget::InitParams params; + params.delegate = this; + params.bounds = gfx::Rect(0, 0, 100, 100); + window_->Init(params); + + tracked_focus_manager_ = + static_cast<FocusManagerDtorTracked*>(GetFocusManager()); + window_->Show(); + } + + virtual void TearDown() { + if (window_) { + window_->Close(); + RunPendingMessages(); + } + FocusManagerFactory::Install(NULL); + ViewsTestBase::TearDown(); + } + + FocusManager* tracked_focus_manager_; + DtorTrackVector dtor_tracker_; +}; + +TEST_F(FocusManagerDtorTest, FocusManagerDestructedLast) { + // Setup views hierarchy. + TabbedPane* tabbed_pane = new TabbedPane(); + content_view_->AddChildView(tabbed_pane); + + NativeButtonDtorTracked* button = new NativeButtonDtorTracked( + ASCIIToUTF16("button"), &dtor_tracker_); + tabbed_pane->AddTab(ASCIIToUTF16("Awesome tab"), button); + + // Close the window. + window_->Close(); + RunPendingMessages(); + + // Test window, button and focus manager should all be destructed. + ASSERT_EQ(3, static_cast<int>(dtor_tracker_.size())); + + // Focus manager should be the last one to destruct. + ASSERT_STREQ("FocusManagerDtorTracked", dtor_tracker_[2].c_str()); + + // Clear window_ so that we don't try to close it again. + window_ = NULL; +} + +} // namespace views diff --git a/ui/views/focus/focus_search.cc b/ui/views/focus/focus_search.cc new file mode 100644 index 0000000..eac3f21 --- /dev/null +++ b/ui/views/focus/focus_search.cc @@ -0,0 +1,272 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/logging.h" +#include "views/focus/focus_manager.h" +#include "views/focus/focus_search.h" +#include "views/view.h" + +namespace views { + +FocusSearch::FocusSearch(View* root, bool cycle, bool accessibility_mode) + : root_(root), + cycle_(cycle), + accessibility_mode_(accessibility_mode) { +} + +View* FocusSearch::FindNextFocusableView(View* starting_view, + bool reverse, + Direction direction, + bool check_starting_view, + FocusTraversable** focus_traversable, + View** focus_traversable_view) { + *focus_traversable = NULL; + *focus_traversable_view = NULL; + + if (!root_->has_children()) { + NOTREACHED(); + // Nothing to focus on here. + return NULL; + } + + View* initial_starting_view = starting_view; + int starting_view_group = -1; + if (starting_view) + starting_view_group = starting_view->GetGroup(); + + if (!starting_view) { + // Default to the first/last child + starting_view = reverse ? root_->child_at(root_->child_count() - 1) : + root_->child_at(0); + // If there was no starting view, then the one we select is a potential + // focus candidate. + check_starting_view = true; + } else { + // The starting view should be a direct or indirect child of the root. + DCHECK(root_->Contains(starting_view)); + } + + View* v = NULL; + if (!reverse) { + v = FindNextFocusableViewImpl(starting_view, check_starting_view, + true, + (direction == DOWN) ? true : false, + starting_view_group, + focus_traversable, + focus_traversable_view); + } else { + // If the starting view is focusable, we don't want to go down, as we are + // traversing the view hierarchy tree bottom-up. + bool can_go_down = (direction == DOWN) && !IsFocusable(starting_view); + v = FindPreviousFocusableViewImpl(starting_view, check_starting_view, + true, + can_go_down, + starting_view_group, + focus_traversable, + focus_traversable_view); + } + + // Don't set the focus to something outside of this view hierarchy. + if (v && v != root_ && !root_->Contains(v)) + v = NULL; + + // If |cycle_| is true, prefer to keep cycling rather than returning NULL. + if (cycle_ && !v && initial_starting_view) { + v = FindNextFocusableView(NULL, reverse, direction, check_starting_view, + focus_traversable, focus_traversable_view); + DCHECK(IsFocusable(v)); + return v; + } + + // Doing some sanity checks. + if (v) { + DCHECK(IsFocusable(v)); + return v; + } + if (*focus_traversable) { + DCHECK(*focus_traversable_view); + return NULL; + } + // Nothing found. + return NULL; +} + +bool FocusSearch::IsViewFocusableCandidate(View* v, int skip_group_id) { + return IsFocusable(v) && + (v->IsGroupFocusTraversable() || skip_group_id == -1 || + v->GetGroup() != skip_group_id); +} + +bool FocusSearch::IsFocusable(View* v) { + if (accessibility_mode_) + return v && v->IsAccessibilityFocusableInRootView(); + + return v && v->IsFocusableInRootView(); +} + +View* FocusSearch::FindSelectedViewForGroup(View* view) { + if (view->IsGroupFocusTraversable() || + view->GetGroup() == -1) // No group for that view. + return view; + + View* selected_view = view->GetSelectedViewForGroup(view->GetGroup()); + if (selected_view) + return selected_view; + + // No view selected for that group, default to the specified view. + return view; +} + +View* FocusSearch::GetParent(View* v) { + return root_->Contains(v) ? v->parent() : NULL; +} + +// Strategy for finding the next focusable view: +// - keep going down the first child, stop when you find a focusable view or +// a focus traversable view (in that case return it) or when you reach a view +// with no children. +// - go to the right sibling and start the search from there (by invoking +// FindNextFocusableViewImpl on that view). +// - if the view has no right sibling, go up the parents until you find a parent +// with a right sibling and start the search from there. +View* FocusSearch::FindNextFocusableViewImpl( + View* starting_view, + bool check_starting_view, + bool can_go_up, + bool can_go_down, + int skip_group_id, + FocusTraversable** focus_traversable, + View** focus_traversable_view) { + if (check_starting_view) { + if (IsViewFocusableCandidate(starting_view, skip_group_id)) { + View* v = FindSelectedViewForGroup(starting_view); + // The selected view might not be focusable (if it is disabled for + // example). + if (IsFocusable(v)) + return v; + } + + *focus_traversable = starting_view->GetFocusTraversable(); + if (*focus_traversable) { + *focus_traversable_view = starting_view; + return NULL; + } + } + + // First let's try the left child. + if (can_go_down) { + if (starting_view->has_children()) { + View* v = FindNextFocusableViewImpl(starting_view->child_at(0), + true, false, true, skip_group_id, + focus_traversable, + focus_traversable_view); + if (v || *focus_traversable) + return v; + } + } + + // Then try the right sibling. + View* sibling = starting_view->GetNextFocusableView(); + if (sibling) { + View* v = FindNextFocusableViewImpl(sibling, + true, false, true, skip_group_id, + focus_traversable, + focus_traversable_view); + if (v || *focus_traversable) + return v; + } + + // Then go up to the parent sibling. + if (can_go_up) { + View* parent = GetParent(starting_view); + while (parent) { + sibling = parent->GetNextFocusableView(); + if (sibling) { + return FindNextFocusableViewImpl(sibling, + true, true, true, + skip_group_id, + focus_traversable, + focus_traversable_view); + } + parent = GetParent(parent); + } + } + + // We found nothing. + return NULL; +} + +// Strategy for finding the previous focusable view: +// - keep going down on the right until you reach a view with no children, if it +// it is a good candidate return it. +// - start the search on the left sibling. +// - if there are no left sibling, start the search on the parent (without going +// down). +View* FocusSearch::FindPreviousFocusableViewImpl( + View* starting_view, + bool check_starting_view, + bool can_go_up, + bool can_go_down, + int skip_group_id, + FocusTraversable** focus_traversable, + View** focus_traversable_view) { + // Let's go down and right as much as we can. + if (can_go_down) { + // Before we go into the direct children, we have to check if this view has + // a FocusTraversable. + *focus_traversable = starting_view->GetFocusTraversable(); + if (*focus_traversable) { + *focus_traversable_view = starting_view; + return NULL; + } + + if (starting_view->has_children()) { + View* view = + starting_view->child_at(starting_view->child_count() - 1); + View* v = FindPreviousFocusableViewImpl(view, true, false, true, + skip_group_id, + focus_traversable, + focus_traversable_view); + if (v || *focus_traversable) + return v; + } + } + + // Then look at this view. Here, we do not need to see if the view has + // a FocusTraversable, since we do not want to go down any more. + if (check_starting_view && + IsViewFocusableCandidate(starting_view, skip_group_id)) { + View* v = FindSelectedViewForGroup(starting_view); + // The selected view might not be focusable (if it is disabled for + // example). + if (IsFocusable(v)) + return v; + } + + // Then try the left sibling. + View* sibling = starting_view->GetPreviousFocusableView(); + if (sibling) { + return FindPreviousFocusableViewImpl(sibling, + true, true, true, + skip_group_id, + focus_traversable, + focus_traversable_view); + } + + // Then go up the parent. + if (can_go_up) { + View* parent = GetParent(starting_view); + if (parent) + return FindPreviousFocusableViewImpl(parent, + true, true, false, + skip_group_id, + focus_traversable, + focus_traversable_view); + } + + // We found nothing. + return NULL; +} + +} // namespace views diff --git a/ui/views/focus/focus_search.h b/ui/views/focus/focus_search.h new file mode 100644 index 0000000..9fe37a0 --- /dev/null +++ b/ui/views/focus/focus_search.h @@ -0,0 +1,122 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_VIEWS_FOCUS_FOCUS_SEARCH_H_ +#define UI_VIEWS_FOCUS_FOCUS_SEARCH_H_ +#pragma once + +#include "views/view.h" + +namespace views { + +class FocusTraversable; + +// FocusSearch is an object that implements the algorithm to find the +// next view to focus. +class VIEWS_EXPORT FocusSearch { + public: + // The direction in which the focus traversal is going. + // TODO (jcampan): add support for lateral (left, right) focus traversal. The + // goal is to switch to focusable views on the same level when using the arrow + // keys (ala Windows: in a dialog box, arrow keys typically move between the + // dialog OK, Cancel buttons). + enum Direction { + UP = 0, + DOWN + }; + + // Constructor. + // - |root| is the root of the view hierarchy to traverse. Focus will be + // trapped inside. + // - |cycle| should be true if you want FindNextFocusableView to cycle back + // to the first view within this root when the traversal reaches + // the end. If this is true, then if you pass a valid starting + // view to FindNextFocusableView you will always get a valid view + // out, even if it's the same view. + // - |accessibility_mode| should be true if full keyboard accessibility is + // needed and you want to check IsAccessibilityFocusableInRootView(), + // rather than IsFocusableInRootView(). + FocusSearch(View* root, bool cycle, bool accessibility_mode); + virtual ~FocusSearch() {} + + // Finds the next view that should be focused and returns it. If a + // FocusTraversable is found while searching for the focusable view, + // returns NULL and sets |focus_traversable| to the FocusTraversable + // and |focus_traversable_view| to the view associated with the + // FocusTraversable. + // + // Return NULL if the end of the focus loop is reached, unless this object + // was initialized with |cycle|=true, in which case it goes back to the + // beginning when it reaches the end of the traversal. + // - |starting_view| is the view that should be used as the starting point + // when looking for the previous/next view. It may be NULL (in which case + // the first/last view should be used depending if normal/reverse). + // - |reverse| whether we should find the next (reverse is false) or the + // previous (reverse is true) view. + // - |direction| specifies whether we are traversing down (meaning we should + // look into child views) or traversing up (don't look at child views). + // - |check_starting_view| is true if starting_view may obtain the next focus. + // - |focus_traversable| is set to the focus traversable that should be + // traversed if one is found (in which case the call returns NULL). + // - |focus_traversable_view| is set to the view associated with the + // FocusTraversable set in the previous parameter (it is used as the + // starting view when looking for the next focusable view). + virtual View* FindNextFocusableView(View* starting_view, + bool reverse, + Direction direction, + bool check_starting_view, + FocusTraversable** focus_traversable, + View** focus_traversable_view); + + private: + // Convenience method that returns true if a view is focusable and does not + // belong to the specified group. + bool IsViewFocusableCandidate(View* v, int skip_group_id); + + // Convenience method; returns true if a view is not NULL and is focusable + // (checking IsAccessibilityFocusableInRootView() if accessibility_mode_ is + // true). + bool IsFocusable(View* v); + + // Returns the view selected for the group of the selected view. If the view + // does not belong to a group or if no view is selected in the group, the + // specified view is returned. + View* FindSelectedViewForGroup(View* view); + + // Get the parent, but stay within the root. Returns NULL if asked for + // the parent of root_. + View* GetParent(View* view); + + // Returns the next focusable view or view containing a FocusTraversable + // (NULL if none was found), starting at the starting_view. + // |check_starting_view|, |can_go_up| and |can_go_down| controls the + // traversal of the views hierarchy. |skip_group_id| specifies a group_id, + // -1 means no group. All views from a group are traversed in one pass. + View* FindNextFocusableViewImpl(View* starting_view, + bool check_starting_view, + bool can_go_up, + bool can_go_down, + int skip_group_id, + FocusTraversable** focus_traversable, + View** focus_traversable_view); + + // Same as FindNextFocusableViewImpl but returns the previous focusable view. + View* FindPreviousFocusableViewImpl(View* starting_view, + bool check_starting_view, + bool can_go_up, + bool can_go_down, + int skip_group_id, + FocusTraversable** focus_traversable, + View** focus_traversable_view); + + View* root_; + bool cycle_; + bool accessibility_mode_; + + DISALLOW_COPY_AND_ASSIGN(FocusSearch); +}; + +} // namespace views + +#endif // UI_VIEWS_FOCUS_FOCUS_SEARCH_H_ diff --git a/ui/views/focus/view_storage.cc b/ui/views/focus/view_storage.cc new file mode 100644 index 0000000..3b9ec10 --- /dev/null +++ b/ui/views/focus/view_storage.cc @@ -0,0 +1,115 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "views/focus/view_storage.h" + +#include <algorithm> + +#include "base/logging.h" +#include "base/stl_util.h" + +namespace views { + +// static +ViewStorage* ViewStorage::GetInstance() { + return Singleton<ViewStorage>::get(); +} + +ViewStorage::ViewStorage() : view_storage_next_id_(0) { +} + +ViewStorage::~ViewStorage() { + STLDeleteContainerPairSecondPointers(view_to_ids_.begin(), + view_to_ids_.end()); +} + +int ViewStorage::CreateStorageID() { + return view_storage_next_id_++; +} + +void ViewStorage::StoreView(int storage_id, View* view) { + DCHECK(view); + std::map<int, View*>::iterator iter = id_to_view_.find(storage_id); + + if (iter != id_to_view_.end()) { + NOTREACHED(); + RemoveView(storage_id); + } + + id_to_view_[storage_id] = view; + + std::vector<int>* ids = NULL; + std::map<View*, std::vector<int>*>::iterator id_iter = + view_to_ids_.find(view); + if (id_iter == view_to_ids_.end()) { + ids = new std::vector<int>(); + view_to_ids_[view] = ids; + } else { + ids = id_iter->second; + } + ids->push_back(storage_id); +} + +View* ViewStorage::RetrieveView(int storage_id) { + std::map<int, View*>::iterator iter = id_to_view_.find(storage_id); + if (iter == id_to_view_.end()) + return NULL; + return iter->second; +} + +void ViewStorage::RemoveView(int storage_id) { + EraseView(storage_id, false); +} + +void ViewStorage::ViewRemoved(View* removed) { + // Let's first retrieve the ids for that view. + std::map<View*, std::vector<int>*>::iterator ids_iter = + view_to_ids_.find(removed); + + if (ids_iter == view_to_ids_.end()) { + // That view is not in the view storage. + return; + } + + std::vector<int>* ids = ids_iter->second; + DCHECK(!ids->empty()); + EraseView((*ids)[0], true); +} + +void ViewStorage::EraseView(int storage_id, bool remove_all_ids) { + // Remove the view from id_to_view_location_. + std::map<int, View*>::iterator view_iter = id_to_view_.find(storage_id); + if (view_iter == id_to_view_.end()) + return; + + View* view = view_iter->second; + id_to_view_.erase(view_iter); + + // Also update view_to_ids_. + std::map<View*, std::vector<int>*>::iterator ids_iter = + view_to_ids_.find(view); + DCHECK(ids_iter != view_to_ids_.end()); + std::vector<int>* ids = ids_iter->second; + + if (remove_all_ids) { + for (size_t i = 0; i < ids->size(); ++i) { + view_iter = id_to_view_.find((*ids)[i]); + if (view_iter != id_to_view_.end()) + id_to_view_.erase(view_iter); + } + ids->clear(); + } else { + std::vector<int>::iterator id_iter = + std::find(ids->begin(), ids->end(), storage_id); + DCHECK(id_iter != ids->end()); + ids->erase(id_iter); + } + + if (ids->empty()) { + delete ids; + view_to_ids_.erase(ids_iter); + } +} + +} // namespace views diff --git a/ui/views/focus/view_storage.h b/ui/views/focus/view_storage.h new file mode 100644 index 0000000..7d748e2 --- /dev/null +++ b/ui/views/focus/view_storage.h @@ -0,0 +1,70 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_VIEWS_FOCUS_VIEW_STORAGE_H_ +#define UI_VIEWS_FOCUS_VIEW_STORAGE_H_ +#pragma once + +#include "base/memory/singleton.h" +#include "views/view.h" + +// This class is a simple storage place for storing/retrieving views. It is +// used for example in the FocusManager to store/restore focused views when the +// main window becomes active/inactive. +// It automatically removes a view from the storage if the view is removed from +// the tree hierarchy. +// +// To use it, you first need to create a view storage id that can then be used +// to store/retrieve views. + +namespace views { + +class VIEWS_EXPORT ViewStorage { + public: + // Returns the global ViewStorage instance. + // It is guaranted to be non NULL. + static ViewStorage* GetInstance(); + + // Returns a unique storage id that can be used to store/retrieve views. + int CreateStorageID(); + + // Associates |view| with the specified |storage_id|. + void StoreView(int storage_id, View* view); + + // Returns the view associated with |storage_id| if any, NULL otherwise. + View* RetrieveView(int storage_id); + + // Removes the view associated with |storage_id| if any. + void RemoveView(int storage_id); + + // Notifies the ViewStorage that a view was removed from its parent somewhere. + void ViewRemoved(View* removed); + + size_t view_count() const { return view_to_ids_.size(); } + + private: + friend struct DefaultSingletonTraits<ViewStorage>; + + ViewStorage(); + ~ViewStorage(); + + // Removes the view associated with |storage_id|. If |remove_all_ids| is true, + // all other mapping pointing to the same view are removed as well. + void EraseView(int storage_id, bool remove_all_ids); + + // Next id for the view storage. + int view_storage_next_id_; + + // The association id to View used for the view storage. + std::map<int, View*> id_to_view_; + + // Association View to id, used to speed up view notification removal. + std::map<View*, std::vector<int>*> view_to_ids_; + + DISALLOW_COPY_AND_ASSIGN(ViewStorage); +}; + +} // namespace views + +#endif // UI_VIEWS_FOCUS_VIEW_STORAGE_H_ diff --git a/ui/views/focus/widget_focus_manager.cc b/ui/views/focus/widget_focus_manager.cc new file mode 100644 index 0000000..57cd2cd --- /dev/null +++ b/ui/views/focus/widget_focus_manager.cc @@ -0,0 +1,50 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "views/focus/widget_focus_manager.h" + +#include "base/memory/singleton.h" + +namespace views { + +// WidgetFocusManager ---------------------------------------------------------- + +// static +WidgetFocusManager* WidgetFocusManager::GetInstance() { + return Singleton<WidgetFocusManager>::get(); +} + +void WidgetFocusManager::AddFocusChangeListener( + WidgetFocusChangeListener* listener) { + focus_change_listeners_.AddObserver(listener); +} + +void WidgetFocusManager::RemoveFocusChangeListener( + WidgetFocusChangeListener* listener) { + focus_change_listeners_.RemoveObserver(listener); +} + +void WidgetFocusManager::OnWidgetFocusEvent(gfx::NativeView focused_before, + gfx::NativeView focused_now) { + if (enabled_) { + FOR_EACH_OBSERVER(WidgetFocusChangeListener, focus_change_listeners_, + OnNativeFocusChange(focused_before, focused_now)); + } +} + +WidgetFocusManager::WidgetFocusManager() : enabled_(true) {} + +WidgetFocusManager::~WidgetFocusManager() {} + +// AutoNativeNotificationDisabler ---------------------------------------------- + +AutoNativeNotificationDisabler::AutoNativeNotificationDisabler() { + WidgetFocusManager::GetInstance()->DisableNotifications(); +} + +AutoNativeNotificationDisabler::~AutoNativeNotificationDisabler() { + WidgetFocusManager::GetInstance()->EnableNotifications(); +} + +} // namespace views diff --git a/ui/views/focus/widget_focus_manager.h b/ui/views/focus/widget_focus_manager.h new file mode 100644 index 0000000..7a0143b --- /dev/null +++ b/ui/views/focus/widget_focus_manager.h @@ -0,0 +1,82 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_VIEWS_FOCUS_WIDGET_FOCUS_MANAGER_H_ +#define UI_VIEWS_FOCUS_WIDGET_FOCUS_MANAGER_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/observer_list.h" +#include "ui/gfx/native_widget_types.h" +#include "views/views_export.h" + +template <typename T> struct DefaultSingletonTraits; + +namespace views { + +// This interface should be implemented by classes that want to be notified when +// the native focus is about to change. Listeners implementing this interface +// will be invoked for all native focus changes across the entire Chrome +// application. FocusChangeListeners are only called for changes within the +// children of a single top-level native-view. +class WidgetFocusChangeListener { + public: + virtual void OnNativeFocusChange(gfx::NativeView focused_before, + gfx::NativeView focused_now) = 0; + + protected: + virtual ~WidgetFocusChangeListener() {} +}; + +class VIEWS_EXPORT WidgetFocusManager { + public: + // Returns the singleton instance. + static WidgetFocusManager* GetInstance(); + + // Adds/removes a WidgetFocusChangeListener |listener| to the set of + // active listeners. + void AddFocusChangeListener(WidgetFocusChangeListener* listener); + void RemoveFocusChangeListener(WidgetFocusChangeListener* listener); + + // To be called when native-focus shifts from |focused_before| to + // |focused_now|. + // TODO(port) : Invocations to this routine are only implemented for + // the Win32 platform. Calls need to be placed appropriately for + // non-Windows environments. + void OnWidgetFocusEvent(gfx::NativeView focused_before, + gfx::NativeView focused_now); + + // Enable/Disable notification of registered listeners during calls + // to OnWidgetFocusEvent. Used to prevent unwanted focus changes from + // propagating notifications. + void EnableNotifications() { enabled_ = true; } + void DisableNotifications() { enabled_ = false; } + + private: + friend struct DefaultSingletonTraits<WidgetFocusManager>; + + WidgetFocusManager(); + ~WidgetFocusManager(); + + ObserverList<WidgetFocusChangeListener> focus_change_listeners_; + + bool enabled_; + + DISALLOW_COPY_AND_ASSIGN(WidgetFocusManager); +}; + +// A basic helper class that is used to disable native focus change +// notifications within a scope. +class VIEWS_EXPORT AutoNativeNotificationDisabler { + public: + AutoNativeNotificationDisabler(); + ~AutoNativeNotificationDisabler(); + + private: + DISALLOW_COPY_AND_ASSIGN(AutoNativeNotificationDisabler); +}; + +} // namespace views + +#endif // UI_VIEWS_FOCUS_WIDGET_FOCUS_MANAGER_H_ |