Commit 2d3961fa authored by dmazzoni@chromium.org's avatar dmazzoni@chromium.org

Support double-tap to click in touch accessibility controller.

This refactors the touch accessibility controller to work like a state
machine, and implements support for double-tap to send a click to the
last location where the user did touch exploration previously. This also
means reimplementing existing touch interactions so that nothing is
passed through until the double-tap timeout passes or the user moves
outside the slop region.

BUG=377900

Review URL: https://codereview.chromium.org/296403011

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@275784 0039d316-1c4b-4281-b951-d872f2087c98
parent c01bd2b4
......@@ -55,7 +55,13 @@ IN_PROC_BROWSER_TEST_F(TouchExplorationTest, ToggleOnOff) {
SwitchTouchExplorationMode(true);
aura::test::EventGenerator generator(root_window);
generator.set_current_location(gfx::Point(100, 200));
generator.PressTouchId(1);
// Since the touch exploration controller doesn't know if the user is
// double-tapping or not, touch exploration is only initiated if the
// user moves more than 8 pixels away from the initial location (the "slop"),
// or after 300 ms has elapsed.
generator.MoveTouchId(gfx::Point(109, 209), 1);
// Number of mouse events may be greater than 1 because of ET_MOUSE_ENTERED.
EXPECT_GT(event_handler->num_mouse_events(), 0);
EXPECT_EQ(0, event_handler->num_touch_events());
......@@ -68,7 +74,9 @@ IN_PROC_BROWSER_TEST_F(TouchExplorationTest, ToggleOnOff) {
event_handler->Reset();
SwitchTouchExplorationMode(true);
generator.set_current_location(gfx::Point(500, 600));
generator.PressTouchId(2);
generator.MoveTouchId(gfx::Point(509, 609), 2);
EXPECT_GT(event_handler->num_mouse_events(), 0);
EXPECT_EQ(0, event_handler->num_touch_events());
......
......@@ -7,6 +7,7 @@
#include "base/bind.h"
#include "base/memory/scoped_ptr.h"
#include "base/message_loop/message_loop_proxy.h"
#include "base/time/default_tick_clock.h"
#include "ui/aura/client/screen_position_client.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_tree_host.h"
......@@ -69,8 +70,9 @@ class TestTouchEvent : public ui::TouchEvent {
TestTouchEvent(ui::EventType type,
const gfx::Point& root_location,
int touch_id,
int flags)
: TouchEvent(type, root_location, flags, touch_id, ui::EventTimeForNow(),
int flags,
base::TimeDelta timestamp)
: TouchEvent(type, root_location, flags, touch_id, timestamp,
1.0f, 1.0f, 1.0f, 1.0f) {
}
......@@ -87,7 +89,8 @@ EventGenerator::EventGenerator(Window* root_window)
current_host_(delegate_->GetHostAt(current_location_)),
flags_(0),
grab_(false),
async_(false) {
async_(false),
tick_clock_(new base::DefaultTickClock()) {
}
EventGenerator::EventGenerator(Window* root_window, const gfx::Point& point)
......@@ -96,7 +99,8 @@ EventGenerator::EventGenerator(Window* root_window, const gfx::Point& point)
current_host_(delegate_->GetHostAt(current_location_)),
flags_(0),
grab_(false),
async_(false) {
async_(false),
tick_clock_(new base::DefaultTickClock()) {
}
EventGenerator::EventGenerator(Window* root_window, Window* window)
......@@ -105,7 +109,8 @@ EventGenerator::EventGenerator(Window* root_window, Window* window)
current_host_(delegate_->GetHostAt(current_location_)),
flags_(0),
grab_(false),
async_(false) {
async_(false),
tick_clock_(new base::DefaultTickClock()) {
}
EventGenerator::EventGenerator(EventGeneratorDelegate* delegate)
......@@ -113,7 +118,8 @@ EventGenerator::EventGenerator(EventGeneratorDelegate* delegate)
current_host_(delegate_->GetHostAt(current_location_)),
flags_(0),
grab_(false),
async_(false) {
async_(false),
tick_clock_(new base::DefaultTickClock()) {
}
EventGenerator::~EventGenerator() {
......@@ -219,7 +225,8 @@ void EventGenerator::PressTouch() {
void EventGenerator::PressTouchId(int touch_id) {
TestTouchEvent touchev(
ui::ET_TOUCH_PRESSED, GetLocationInCurrentRoot(), touch_id, flags_);
ui::ET_TOUCH_PRESSED, GetLocationInCurrentRoot(), touch_id, flags_,
Now());
Dispatch(&touchev);
}
......@@ -230,7 +237,8 @@ void EventGenerator::MoveTouch(const gfx::Point& point) {
void EventGenerator::MoveTouchId(const gfx::Point& point, int touch_id) {
current_location_ = point;
TestTouchEvent touchev(
ui::ET_TOUCH_MOVED, GetLocationInCurrentRoot(), touch_id, flags_);
ui::ET_TOUCH_MOVED, GetLocationInCurrentRoot(), touch_id, flags_,
Now());
Dispatch(&touchev);
if (!grab_)
......@@ -243,7 +251,8 @@ void EventGenerator::ReleaseTouch() {
void EventGenerator::ReleaseTouchId(int touch_id) {
TestTouchEvent touchev(
ui::ET_TOUCH_RELEASED, GetLocationInCurrentRoot(), touch_id, flags_);
ui::ET_TOUCH_RELEASED, GetLocationInCurrentRoot(), touch_id, flags_,
Now());
Dispatch(&touchev);
}
......@@ -263,7 +272,7 @@ void EventGenerator::GestureEdgeSwipe() {
0,
0,
0,
ui::EventTimeForNow(),
Now(),
ui::GestureEventDetails(ui::ET_GESTURE_WIN8_EDGE_SWIPE, 0, 0),
0);
Dispatch(&gesture);
......@@ -274,7 +283,7 @@ void EventGenerator::GestureTapAt(const gfx::Point& location) {
ui::TouchEvent press(ui::ET_TOUCH_PRESSED,
location,
kTouchId,
ui::EventTimeForNow());
Now());
Dispatch(&press);
ui::TouchEvent release(
......@@ -288,7 +297,7 @@ void EventGenerator::GestureTapDownAndUp(const gfx::Point& location) {
ui::TouchEvent press(ui::ET_TOUCH_PRESSED,
location,
kTouchId,
ui::EventTimeForNow());
Now());
Dispatch(&press);
ui::TouchEvent release(
......@@ -312,7 +321,7 @@ void EventGenerator::GestureScrollSequenceWithCallback(
int steps,
const ScrollStepCallback& callback) {
const int kTouchId = 5;
base::TimeDelta timestamp = ui::EventTimeForNow();
base::TimeDelta timestamp = Now();
ui::TouchEvent press(ui::ET_TOUCH_PRESSED, start, kTouchId, timestamp);
Dispatch(&press);
......@@ -367,7 +376,7 @@ void EventGenerator::GestureMultiFingerScrollWithDelays(
points[i] = start[i];
}
base::TimeDelta press_time_first = ui::EventTimeForNow();
base::TimeDelta press_time_first = Now();
base::TimeDelta press_time[kMaxTouchPoints];
bool pressed[kMaxTouchPoints];
for (int i = 0; i < count; ++i) {
......@@ -417,8 +426,7 @@ void EventGenerator::ScrollSequence(const gfx::Point& start,
float y_offset,
int steps,
int num_fingers) {
base::TimeDelta timestamp = base::TimeDelta::FromInternalValue(
base::TimeTicks::Now().ToInternalValue());
base::TimeDelta timestamp = Now();
ui::ScrollEvent fling_cancel(ui::ET_SCROLL_FLING_CANCEL,
start,
timestamp,
......@@ -457,7 +465,7 @@ void EventGenerator::ScrollSequence(const gfx::Point& start,
const std::vector<gfx::Point>& offsets,
int num_fingers) {
int steps = offsets.size();
base::TimeDelta timestamp = ui::EventTimeForNow();
base::TimeDelta timestamp = Now();
ui::ScrollEvent fling_cancel(ui::ET_SCROLL_FLING_CANCEL,
start,
timestamp,
......@@ -501,6 +509,17 @@ void EventGenerator::Dispatch(ui::Event* event) {
DoDispatchEvent(event, async_);
}
void EventGenerator::SetTickClock(scoped_ptr<base::TickClock> tick_clock) {
tick_clock_ = tick_clock.Pass();
}
base::TimeDelta EventGenerator::Now() {
// This is the same as what EventTimeForNow() does, but here we do it
// with a tick clock that can be replaced with a simulated clock for tests.
return base::TimeDelta::FromInternalValue(
tick_clock_->NowTicks().ToInternalValue());
}
void EventGenerator::DispatchKeyEvent(bool is_press,
ui::KeyboardCode key_code,
int flags) {
......@@ -638,6 +657,5 @@ void EventGenerator::DispatchNextPendingEvent() {
}
}
} // namespace test
} // namespace aura
......@@ -11,12 +11,13 @@
#include "base/basictypes.h"
#include "base/callback.h"
#include "base/memory/scoped_ptr.h"
#include "base/time/time.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/point.h"
namespace base {
class TimeDelta;
class TickClock;
}
namespace ui {
......@@ -317,6 +318,12 @@ class EventGenerator {
current_host_ = host;
}
// Specify an alternative tick clock to be used for simulating time in tests.
void SetTickClock(scoped_ptr<base::TickClock> tick_clock);
// Get the current time from the tick clock.
base::TimeDelta Now();
private:
// Dispatch a key event to the WindowEventDispatcher.
void DispatchKeyEvent(bool is_press, ui::KeyboardCode key_code, int flags);
......@@ -346,6 +353,7 @@ class EventGenerator {
std::list<ui::Event*> pending_events_;
// Set to true to cause events to be posted asynchronously.
bool async_;
scoped_ptr<base::TickClock> tick_clock_;
DISALLOW_COPY_AND_ASSIGN(EventGenerator);
};
......
This diff is collapsed.
......@@ -5,9 +5,11 @@
#ifndef UI_CHROMEOS_TOUCH_EXPLORATION_CONTROLLER_H_
#define UI_CHROMEOS_TOUCH_EXPLORATION_CONTROLLER_H_
#include "base/timer/timer.h"
#include "base/values.h"
#include "ui/chromeos/ui_chromeos_export.h"
#include "ui/events/event_rewriter.h"
#include "ui/events/gesture_detection/gesture_detector.h"
#include "ui/gfx/geometry/point.h"
namespace aura {
......@@ -17,17 +19,58 @@ class Window;
namespace ui {
class Event;
class EventHandler;
class TouchEvent;
// TouchExplorationController is used in tandem with "Spoken Feedback" to
// make the touch UI accessible. TouchExplorationController rewrites the
// incoming touch events as follows:
// - When one finger is touching the screen, touch events are converted to mouse
// moves. This is the "Touch Exploration Mode". (The idea is that mouse moves
// will be subsequently used by another component to move focus between UI
// elements, and the elements will be read out to the user.)
// - When more than one finger is touching the screen, touches from the
// first (i.e. "oldest") finger are ignored, and the other touches go through
// as is.
// make the touch UI accessible.
//
// ** Short version **
//
// At a high-level, single-finger events are used for accessibility -
// exploring the screen gets turned into mouse moves (which can then be
// spoken by an accessibility service running), a double-tap simulates a
// click, and gestures can be used to send high-level accessibility commands.
// When two or more fingers are pressed initially, from then on the events
// are passed through, but with the initial finger removed - so if you swipe
// down with two fingers, the running app will see a one-finger swipe.
//
// ** Long version **
//
// Here are the details of the implementation:
//
// When the first touch is pressed, a 300 ms grace period timer starts.
//
// If the user keeps their finger down for more than 300 ms and doesn't
// perform a supported accessibility gesture in that time (e.g. swipe right),
// they enter touch exploration mode, and all movements are translated into
// synthesized mouse move events.
//
// Also, if the user moves their single finger outside a certain slop region
// (without performing a gesture), they enter touch exploration mode earlier
// than 300 ms.
//
// If the user taps and releases their finger, after 300 ms from the initial
// touch, a single mouse move is fired.
//
// If the user double-taps, the second tap is passed through, allowing the
// user to click - however, the double-tap location is changed to the location
// of the last successful touch exploration - that allows the user to explore
// anywhere on the screen, hear its description, then double-tap anywhere
// to activate it.
//
// If the user adds a second finger during the grace period, they enter
// passthrough mode. In this mode, the first finger is ignored but all
// additional touch events are mostly passed through unmodified. So a
// two-finger scroll gets passed through as a one-finger scroll. However,
// once in passthrough mode, if one finger is released, the remaining fingers
// continue to pass through events, allowing the user to start a scroll
// with two fingers but finish it with one. Sometimes this requires rewriting
// the touch ids.
//
// Once either touch exploration or passthrough mode has been activated,
// it remains in that mode until all fingers have been released.
//
// The caller is expected to retain ownership of instances of this class and
// destroy them before |root_window| is destroyed.
class UI_CHROMEOS_EXPORT TouchExplorationController :
......@@ -36,28 +79,120 @@ class UI_CHROMEOS_EXPORT TouchExplorationController :
explicit TouchExplorationController(aura::Window* root_window);
virtual ~TouchExplorationController();
private:
scoped_ptr<ui::Event> CreateMouseMoveEvent(const gfx::PointF& location,
int flags);
void EnterTouchToMouseMode();
void CallTapTimerNowForTesting();
void SetEventHandlerForTesting(ui::EventHandler* event_handler_for_testing);
bool IsInNoFingersDownStateForTesting() const;
private:
// Overridden from ui::EventRewriter
virtual ui::EventRewriteStatus RewriteEvent(
const ui::Event& event, scoped_ptr<ui::Event>* rewritten_event) OVERRIDE;
virtual ui::EventRewriteStatus NextDispatchEvent(
const ui::Event& last_event, scoped_ptr<ui::Event>* new_event) OVERRIDE;
// Event handlers based on the current state - see State, below.
ui::EventRewriteStatus InNoFingersDown(
const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event);
ui::EventRewriteStatus InSingleTapPressed(
const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event);
ui::EventRewriteStatus InSingleTapReleased(
const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event);
ui::EventRewriteStatus InDoubleTapPressed(
const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event);
ui::EventRewriteStatus InTouchExploration(
const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event);
ui::EventRewriteStatus InPassthroughMinusOne(
const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event);
// This timer is started every time we get the first press event, and
// it fires after the double-click timeout elapses (300 ms by default).
// If the user taps and releases within 300 ms and doesn't press again,
// we treat that as a single mouse move (touch exploration) event.
void OnTapTimerFired();
// Dispatch a new event outside of the event rewriting flow.
void DispatchEvent(ui::Event* event);
scoped_ptr<ui::Event> CreateMouseMoveEvent(const gfx::PointF& location,
int flags);
void EnterTouchToMouseMode();
// Set the state to NO_FINGERS_DOWN and reset any other fields to their
// default value.
void ResetToNoFingersDown();
enum State {
// No fingers are down and no events are pending.
NO_FINGERS_DOWN,
// A single finger is down, but we're not yet sure if this is going
// to be touch exploration or something else.
SINGLE_TAP_PRESSED,
// The user pressed and released a single finger - a tap - but we have
// to wait until the end of the grace period to allow the user to tap the
// second time. If the second tap doesn't occurs within the grace period,
// we dispatch a mouse move at the location of the first tap.
SINGLE_TAP_RELEASED,
// The user tapped once, and before the grace period expired, pressed
// one finger down to begin a double-tap, but has not released it yet.
DOUBLE_TAP_PRESSED,
// We're in touch exploration mode. Anything other than the first finger
// is ignored, and movements of the first finger are rewritten as mouse
// move events. This mode is entered if a single finger is pressed and
// after the grace period the user hasn't added a second finger or
// moved the finger outside of the slop region. We'll stay in this
// mode until all fingers are lifted.
TOUCH_EXPLORATION,
// The user placed two or more fingers down within the grace period.
// We're now in passthrough mode until all fingers are lifted. Initially
// the first finger is ignored and other fingers are passed through
// as-is. If a finger other than the initial one is the first to be
// released, we rewrite the first finger with the touch id of the finger
// that was released, from now on. The motivation for this is that if
// the user starts a scroll with 2 fingers, they can release either one
// and continue the scrolling.
PASSTHROUGH_MINUS_ONE,
};
aura::Window* root_window_;
// A set of touch ids for fingers currently touching the screen.
std::vector<int> touch_ids_;
std::vector<int> current_touch_ids_;
// Map of touch ids to their last known location.
std::map<int, gfx::PointF> touch_locations_;
// Initialized from RewriteEvent() and dispatched in NextDispatchEvent().
scoped_ptr<ui::Event> next_dispatch_event_;
// The touch id that any events on the initial finger should be rewritten
// as in passthrough-minus-one mode. If kTouchIdUnassigned, events on the
// initial finger are discarded. If kTouchIdNone, the initial finger
// has been released and no more rewriting will be done.
int initial_touch_id_passthrough_mapping_;
aura::Window* root_window_;
// The current state.
State state_;
// A copy of the event from the initial touch press.
scoped_ptr<ui::TouchEvent> initial_press_;
// The last location where we synthesized a mouse move event.
// When the user double-taps, we send the passed-through tap here.
gfx::PointF last_touch_exploration_location_;
// A timer to fire the mouse move event after the double-tap delay.
base::OneShotTimer<TouchExplorationController> tap_timer_;
// For testing only, an event handler to use for generated events
// outside of the normal event rewriting flow.
ui::EventHandler* event_handler_for_testing_;
// A default gesture detector config, so we can share the same
// timeout and pixel slop constants.
ui::GestureDetector::Config gesture_detector_config_;
DISALLOW_COPY_AND_ASSIGN(TouchExplorationController);
};
......
......@@ -218,6 +218,7 @@
'../chromeos/chromeos.gyp:chromeos',
'aura/aura.gyp:aura_test_support',
'chromeos/ui_chromeos.gyp:ui_chromeos',
'events/events.gyp:gesture_detection',
],
'sources': [
'chromeos/touch_exploration_controller_unittest.cc'
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment