blob: a253c828a4efcfba636b5351be792f202a2ef7c1 [file] [log] [blame]
// Copyright 2014 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 "ui/chromeos/touch_exploration_controller.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/window.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_tree_host.h"
#include "ui/events/event.h"
#include "ui/events/event_processor.h"
#define VLOG_STATE() if (VLOG_IS_ON(0)) VlogState(__func__)
#define VLOG_EVENT(event) if (VLOG_IS_ON(0)) VlogEvent(event, __func__)
namespace ui {
namespace {
// The default value for initial_touch_id_passthrough_mapping_ used
// when the user has not yet released any fingers yet, so there's no
// touch id remapping yet.
const int kTouchIdUnassigned = 0;
// The value for initial_touch_id_passthrough_mapping_ if the user has
// released the first finger but some other fingers are held down. In this
// state we don't do any touch id remapping, but we distinguish it from the
// kTouchIdUnassigned state because we don't want to assign
// initial_touch_id_passthrough_mapping_ a touch id anymore,
// until all fingers are released.
const int kTouchIdNone = -1;
} // namespace
TouchExplorationController::TouchExplorationController(
aura::Window* root_window)
: root_window_(root_window),
initial_touch_id_passthrough_mapping_(kTouchIdUnassigned),
state_(NO_FINGERS_DOWN),
event_handler_for_testing_(NULL),
prev_state_(NO_FINGERS_DOWN) {
CHECK(root_window);
root_window->GetHost()->GetEventSource()->AddEventRewriter(this);
}
TouchExplorationController::~TouchExplorationController() {
root_window_->GetHost()->GetEventSource()->RemoveEventRewriter(this);
}
void TouchExplorationController::CallTapTimerNowForTesting() {
DCHECK(tap_timer_.IsRunning());
tap_timer_.Stop();
OnTapTimerFired();
}
void TouchExplorationController::SetEventHandlerForTesting(
ui::EventHandler* event_handler_for_testing) {
event_handler_for_testing_ = event_handler_for_testing;
}
bool TouchExplorationController::IsInNoFingersDownStateForTesting() const {
return state_ == NO_FINGERS_DOWN;
}
ui::EventRewriteStatus TouchExplorationController::RewriteEvent(
const ui::Event& event,
scoped_ptr<ui::Event>* rewritten_event) {
if (!event.IsTouchEvent())
return ui::EVENT_REWRITE_CONTINUE;
const ui::TouchEvent& touch_event =
static_cast<const ui::TouchEvent&>(event);
// If the tap timer should have fired by now but hasn't, run it now and
// stop the timer. This is important so that behavior is consistent with
// the timestamps of the events, and not dependent on the granularity of
// the timer.
if (tap_timer_.IsRunning() &&
touch_event.time_stamp() - initial_press_->time_stamp() >
gesture_detector_config_.double_tap_timeout) {
tap_timer_.Stop();
OnTapTimerFired();
// Note: this may change the state. We should now continue and process
// this event under this new state.
}
const ui::EventType type = touch_event.type();
const gfx::PointF& location = touch_event.location_f();
const int touch_id = touch_event.touch_id();
// Always update touch ids and touch locations, so we can use those
// no matter what state we're in.
if (type == ui::ET_TOUCH_PRESSED) {
current_touch_ids_.push_back(touch_id);
touch_locations_.insert(std::pair<int, gfx::PointF>(touch_id, location));
} else if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) {
std::vector<int>::iterator it = std::find(
current_touch_ids_.begin(), current_touch_ids_.end(), touch_id);
// Can happen if touch exploration is enabled while fingers were down.
if (it == current_touch_ids_.end())
return ui::EVENT_REWRITE_CONTINUE;
current_touch_ids_.erase(it);
touch_locations_.erase(touch_id);
} else if (type == ui::ET_TOUCH_MOVED) {
std::vector<int>::iterator it = std::find(
current_touch_ids_.begin(), current_touch_ids_.end(), touch_id);
// Can happen if touch exploration is enabled while fingers were down.
if (it == current_touch_ids_.end())
return ui::EVENT_REWRITE_CONTINUE;
touch_locations_[*it] = location;
}
VLOG_STATE();
VLOG_EVENT(touch_event);
// The rest of the processing depends on what state we're in.
switch(state_) {
case NO_FINGERS_DOWN:
return InNoFingersDown(touch_event, rewritten_event);
case SINGLE_TAP_PRESSED:
return InSingleTapPressed(touch_event, rewritten_event);
case SINGLE_TAP_RELEASED:
return InSingleTapReleased(touch_event, rewritten_event);
case DOUBLE_TAP_PRESSED:
return InDoubleTapPressed(touch_event, rewritten_event);
case TOUCH_EXPLORATION:
return InTouchExploration(touch_event, rewritten_event);
case PASSTHROUGH_MINUS_ONE:
return InPassthroughMinusOne(touch_event, rewritten_event);
case TOUCH_EXPLORE_SECOND_PRESS:
return InTouchExploreSecondPress(touch_event, rewritten_event);
}
NOTREACHED();
return ui::EVENT_REWRITE_CONTINUE;
}
ui::EventRewriteStatus TouchExplorationController::NextDispatchEvent(
const ui::Event& last_event, scoped_ptr<ui::Event>* new_event) {
NOTREACHED();
return ui::EVENT_REWRITE_CONTINUE;
}
ui::EventRewriteStatus TouchExplorationController::InNoFingersDown(
const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event) {
const ui::EventType type = event.type();
if (type == ui::ET_TOUCH_PRESSED) {
initial_press_.reset(new TouchEvent(event));
tap_timer_.Start(FROM_HERE,
gesture_detector_config_.double_tap_timeout,
this,
&TouchExplorationController::OnTapTimerFired);
state_ = SINGLE_TAP_PRESSED;
VLOG_STATE();
return ui::EVENT_REWRITE_DISCARD;
}
NOTREACHED();
return ui::EVENT_REWRITE_CONTINUE;
}
ui::EventRewriteStatus TouchExplorationController::InSingleTapPressed(
const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event) {
const ui::EventType type = event.type();
if (type == ui::ET_TOUCH_PRESSED) {
// Adding a second finger within the timeout period switches to
// passthrough.
state_ = PASSTHROUGH_MINUS_ONE;
return InPassthroughMinusOne(event, rewritten_event);
} else if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) {
DCHECK_EQ(0U, current_touch_ids_.size());
state_ = SINGLE_TAP_RELEASED;
VLOG_STATE();
return EVENT_REWRITE_DISCARD;
} else if (type == ui::ET_TOUCH_MOVED) {
// If the user moves far enough from the initial touch location (outside
// the "slop" region, jump to the touch exploration mode early.
// TODO(evy, lisayin): Add gesture recognition here instead -
// we should probably jump to gesture mode here if the velocity is
// high enough, and touch exploration if the velocity is lower.
float delta = (event.location() - initial_press_->location()).Length();
if (delta > gesture_detector_config_.touch_slop) {
EnterTouchToMouseMode();
state_ = TOUCH_EXPLORATION;
VLOG_STATE();
return InTouchExploration(event, rewritten_event);
}
return EVENT_REWRITE_DISCARD;
}
NOTREACHED() << "Unexpected event type received.";
return ui::EVENT_REWRITE_CONTINUE;
}
ui::EventRewriteStatus TouchExplorationController::InSingleTapReleased(
const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event) {
const ui::EventType type = event.type();
if (type == ui::ET_TOUCH_PRESSED) {
// This is the second tap in a double-tap (or double tap-hold).
// Rewrite at location of last touch exploration.
// If there is no touch exploration yet, discard instead.
if (!last_touch_exploration_) {
return ui::EVENT_REWRITE_DISCARD;
}
rewritten_event->reset(
new ui::TouchEvent(ui::ET_TOUCH_PRESSED,
last_touch_exploration_->location(),
event.touch_id(),
event.time_stamp()));
(*rewritten_event)->set_flags(event.flags());
state_ = DOUBLE_TAP_PRESSED;
VLOG_STATE();
return ui::EVENT_REWRITE_REWRITTEN;
}
// If the previous press was discarded, we need to also handle its release.
if (type == ui::ET_TOUCH_RELEASED && !last_touch_exploration_) {
if (current_touch_ids_.size() == 0) {
state_ = NO_FINGERS_DOWN;
}
return ui::EVENT_REWRITE_DISCARD;
}
NOTREACHED();
return ui::EVENT_REWRITE_CONTINUE;
}
ui::EventRewriteStatus TouchExplorationController::InDoubleTapPressed(
const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event) {
const ui::EventType type = event.type();
if (type == ui::ET_TOUCH_PRESSED) {
return ui::EVENT_REWRITE_DISCARD;
} else if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) {
if (current_touch_ids_.size() != 0)
return EVENT_REWRITE_DISCARD;
// Rewrite release at location of last touch exploration with the same
// id as the prevoius press.
rewritten_event->reset(
new ui::TouchEvent(ui::ET_TOUCH_RELEASED,
last_touch_exploration_->location(),
initial_press_->touch_id(),
event.time_stamp()));
(*rewritten_event)->set_flags(event.flags());
ResetToNoFingersDown();
return ui::EVENT_REWRITE_REWRITTEN;
} else if (type == ui::ET_TOUCH_MOVED) {
return ui::EVENT_REWRITE_DISCARD;
}
NOTREACHED() << "Unexpected event type received.";
return ui::EVENT_REWRITE_CONTINUE;
}
ui::EventRewriteStatus TouchExplorationController::InTouchExploration(
const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event) {
const ui::EventType type = event.type();
if (type == ui::ET_TOUCH_PRESSED) {
// Handle split-tap.
initial_press_.reset(new TouchEvent(event));
if (tap_timer_.IsRunning())
tap_timer_.Stop();
rewritten_event->reset(
new ui::TouchEvent(ui::ET_TOUCH_PRESSED,
last_touch_exploration_->location(),
event.touch_id(),
event.time_stamp()));
(*rewritten_event)->set_flags(event.flags());
state_ = TOUCH_EXPLORE_SECOND_PRESS;
VLOG_STATE();
return ui::EVENT_REWRITE_REWRITTEN;
} else if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) {
if (current_touch_ids_.size() == 0)
ResetToNoFingersDown();
} else if (type != ui::ET_TOUCH_MOVED) {
NOTREACHED() << "Unexpected event type received.";
return ui::EVENT_REWRITE_CONTINUE;
}
// Rewrite as a mouse-move event.
*rewritten_event = CreateMouseMoveEvent(event.location(), event.flags());
last_touch_exploration_.reset(new TouchEvent(event));
return ui::EVENT_REWRITE_REWRITTEN;
}
ui::EventRewriteStatus TouchExplorationController::InPassthroughMinusOne(
const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event) {
ui::EventType type = event.type();
gfx::PointF location = event.location_f();
if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) {
if (current_touch_ids_.size() == 0)
ResetToNoFingersDown();
if (initial_touch_id_passthrough_mapping_ == kTouchIdUnassigned) {
if (event.touch_id() == initial_press_->touch_id()) {
initial_touch_id_passthrough_mapping_ = kTouchIdNone;
} else {
// If the only finger now remaining is the first finger,
// rewrite as a move to the location of the first finger.
initial_touch_id_passthrough_mapping_ = event.touch_id();
rewritten_event->reset(
new ui::TouchEvent(ui::ET_TOUCH_MOVED,
touch_locations_[initial_press_->touch_id()],
initial_touch_id_passthrough_mapping_,
event.time_stamp()));
(*rewritten_event)->set_flags(event.flags());
return ui::EVENT_REWRITE_REWRITTEN;
}
}
}
if (event.touch_id() == initial_press_->touch_id()) {
if (initial_touch_id_passthrough_mapping_ == kTouchIdNone ||
initial_touch_id_passthrough_mapping_ == kTouchIdUnassigned) {
return ui::EVENT_REWRITE_DISCARD;
}
rewritten_event->reset(
new ui::TouchEvent(type,
location,
initial_touch_id_passthrough_mapping_,
event.time_stamp()));
(*rewritten_event)->set_flags(event.flags());
return ui::EVENT_REWRITE_REWRITTEN;
}
return ui::EVENT_REWRITE_CONTINUE;
}
ui::EventRewriteStatus TouchExplorationController::InTouchExploreSecondPress(
const ui::TouchEvent& event,
scoped_ptr<ui::Event>* rewritten_event) {
ui::EventType type = event.type();
gfx::PointF location = event.location_f();
if (type == ui::ET_TOUCH_PRESSED) {
return ui::EVENT_REWRITE_DISCARD;
} else if (type == ui::ET_TOUCH_MOVED) {
// Currently this is a discard, but could be something like rotor
// in the future.
return ui::EVENT_REWRITE_DISCARD;
} else if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) {
// If the touch exploration finger is lifted, there is no option to return
// to touch explore anymore. The remaining finger acts as a pending
// tap or long tap for the last touch explore location.
if (event.touch_id() == last_touch_exploration_->touch_id()){
state_ = DOUBLE_TAP_PRESSED;
VLOG_STATE();
return EVENT_REWRITE_DISCARD;
}
// Continue to release the touch only if the touch explore finger is the
// only finger remaining.
if (current_touch_ids_.size() != 1)
return EVENT_REWRITE_DISCARD;
// Continue to release the touch only if the touch explore finger is the
// only finger remaining.
if (current_touch_ids_.size() != 1)
return EVENT_REWRITE_DISCARD;
// Rewrite at location of last touch exploration.
rewritten_event->reset(
new ui::TouchEvent(ui::ET_TOUCH_RELEASED,
last_touch_exploration_->location(),
initial_press_->touch_id(),
event.time_stamp()));
(*rewritten_event)->set_flags(event.flags());
state_ = TOUCH_EXPLORATION;
VLOG_STATE();
return ui::EVENT_REWRITE_REWRITTEN;
}
NOTREACHED() << "Unexpected event type received.";
return ui::EVENT_REWRITE_CONTINUE;
}
void TouchExplorationController::OnTapTimerFired() {
if (state_ != SINGLE_TAP_RELEASED && state_ != SINGLE_TAP_PRESSED)
return;
if (state_ == SINGLE_TAP_RELEASED) {
ResetToNoFingersDown();
} else {
EnterTouchToMouseMode();
state_ = TOUCH_EXPLORATION;
VLOG_STATE();
}
scoped_ptr<ui::Event> mouse_move = CreateMouseMoveEvent(
initial_press_->location(), initial_press_->flags());
DispatchEvent(mouse_move.get());
last_touch_exploration_.reset(new TouchEvent(*initial_press_));
}
void TouchExplorationController::DispatchEvent(ui::Event* event) {
if (event_handler_for_testing_) {
event_handler_for_testing_->OnEvent(event);
return;
}
ui::EventDispatchDetails result ALLOW_UNUSED =
root_window_->GetHost()->dispatcher()->OnEventFromSource(event);
}
scoped_ptr<ui::Event> TouchExplorationController::CreateMouseMoveEvent(
const gfx::PointF& location,
int flags) {
return scoped_ptr<ui::Event>(
new ui::MouseEvent(
ui::ET_MOUSE_MOVED,
location,
location,
flags | ui::EF_IS_SYNTHESIZED | ui::EF_TOUCH_ACCESSIBILITY,
0));
}
void TouchExplorationController::EnterTouchToMouseMode() {
aura::client::CursorClient* cursor_client =
aura::client::GetCursorClient(root_window_);
if (cursor_client && !cursor_client->IsMouseEventsEnabled())
cursor_client->EnableMouseEvents();
if (cursor_client && cursor_client->IsCursorVisible())
cursor_client->HideCursor();
}
void TouchExplorationController::ResetToNoFingersDown() {
state_ = NO_FINGERS_DOWN;
initial_touch_id_passthrough_mapping_ = kTouchIdUnassigned;
VLOG_STATE();
if (tap_timer_.IsRunning())
tap_timer_.Stop();
}
void TouchExplorationController::VlogState(const char* function_name) {
if (prev_state_ == state_)
return;
prev_state_ = state_;
const char* state_string = EnumStateToString(state_);
VLOG(0) << "\n Function name: " << function_name
<< "\n State: " << state_string;
}
void TouchExplorationController::VlogEvent(const ui::TouchEvent& touch_event,
const char* function_name) {
CHECK(touch_event.IsTouchEvent());
if (prev_event_ == NULL || prev_event_->type() != touch_event.type() ||
prev_event_->touch_id() != touch_event.touch_id()) {
const std::string type = EnumEventTypeToString(touch_event.type());
const gfx::PointF& location = touch_event.location_f();
const int touch_id = touch_event.touch_id();
VLOG(0) << "\n Function name: " << function_name
<< "\n Event Type: " << type
<< "\n Location: " << location.ToString()
<< "\n Touch ID: " << touch_id
<< "\n Number of fingers down: " << current_touch_ids_.size();
prev_event_.reset(new TouchEvent(touch_event));
}
}
const char* TouchExplorationController::EnumStateToString(State state) {
switch (state) {
case NO_FINGERS_DOWN:
return "NO_FINGERS_DOWN";
case SINGLE_TAP_PRESSED:
return "SINGLE_TAP_PRESSED";
case SINGLE_TAP_RELEASED:
return "SINGLE_TAP_RELEASED";
case DOUBLE_TAP_PRESSED:
return "DOUBLE_TAP_PRESSED";
case TOUCH_EXPLORATION:
return "TOUCH_EXPLORATION";
case PASSTHROUGH_MINUS_ONE:
return "PASSTHROUGH_MINUS_ONE";
case TOUCH_EXPLORE_SECOND_PRESS:
return "TOUCH_EXPLORE_SECOND_PRESS";
}
return "Not a state";
}
std::string TouchExplorationController::EnumEventTypeToString(
ui::EventType type) {
// Add more cases later. For now, these are the most frequently seen
// event types.
switch (type) {
case ET_TOUCH_RELEASED:
return "ET_TOUCH_RELEASED";
case ET_TOUCH_PRESSED:
return "ET_TOUCH_PRESSED";
case ET_TOUCH_MOVED:
return "ET_TOUCH_MOVED";
case ET_TOUCH_CANCELLED:
return "ET_TOUCH_CANCELLED";
default:
return base::IntToString(type);
}
}
} // namespace ui