// 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* rewritten_event) { if (!event.IsTouchEvent()) return ui::EVENT_REWRITE_CONTINUE; const ui::TouchEvent& touch_event = static_cast(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(touch_id, location)); } else if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) { std::vector::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::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* new_event) { NOTREACHED(); return ui::EVENT_REWRITE_CONTINUE; } ui::EventRewriteStatus TouchExplorationController::InNoFingersDown( const ui::TouchEvent& event, scoped_ptr* 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* 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* 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* 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* 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* 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* 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 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 TouchExplorationController::CreateMouseMoveEvent( const gfx::PointF& location, int flags) { return scoped_ptr( 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